C Programming • Hardware • Projects
Last time, we put an ATmega644 on a breadboard and I promised that this month we’d transfer the design to a PCB so that we would have an easy-to-use open-source hardware platform to learn with. Unfortunately, I messed up. I had it all working and even tested the PCB with my bootloader. Then it quit working. As the deadline for this article began to stand up and glare at me with tendrils of smoke and occasional puffs of fire directed my way, I did what any sensible person does. I panicked and messed things up even worse. Am I mortified? You betcha! So, the PCB will be delayed and this month we will begin a mini-series on AVR memory architecture with the goal of understanding each type, providing examples on how to use each, and in the final episode we will use what we’ve learned to write a bootloader in C. Somewhere along the line I should get the BeAVR PCB and associated software working again, and we’ll see it then.
Whether it is a bank of switches, a strip of cardboard with holes punched in it, or a microscopic slab of very pure sand with metals etched onto it — computers require memory for programs and data.
Computer memory has two distinct aspects. First, there is the physical medium that holds the data — usually a configuration of transistors that can be set to hold a voltage representing single-bit values of either 1 or 0. The circuits are grouped in bytes (eight bits) or other sized groups that are dealt with as a unit. Groups of these physical data storage units are arrayed so that they can be accessed individually by their addresses. Addresses are the second distinct aspect of computer memory and are independent of the physical method used to store the data.
We thus think of memory as being a sequence of physical locations to store data of a certain size (eight bits for AVRs) that each have a unique address that we use to read or write data from that specific location. Let’s do a thought experiment to help understand this. If we have a 2K memory, we have 2048 physical locations to store data. [Traditionally, we use ‘K’ to denote 1024 in computers since the number system is binary and 2^10 is 1024. In decimal systems, we use the lower case ‘k’ for 1000. Since there are no real standards for this, it can get confusing.] Each of these 2048 locations has an address from 0 to 2047 — but since we like to express these numbers in hexadecimal, they would number from 0x0000 to 0x07FF.
|FIGURE 1. Hello World! in memory.|
Figure 1 illustrates the separation of the address concept from the real (physical) location where data is stored. The first thing we can see about the difference between the stored data and the address of the stored data is that the stored data is eight bits meaning that it can encode 256 unique values — 0 to 255 — but the total number of these eight bit locations is a much larger number: 2048. So, we require more bits to uniquely encode the addresses of those locations. An 11-bit number could encode 2048 values, but with small microcontrollers like the AVR we typically deal with only two sizes of numbers in hardware: either eight-bit bytes or 16-bit words. So, logically we would use the larger words to encode addresses. We express bytes as two digit hex numbers (0x00 to 0xFF) and words as four digit hex numbers (0x0000 to 0xFFFF) [where the leading 0x just means that it is a hex number]. It then follows that in Figure 1, the addresses are words and the data are bytes. We use ‘…’ to indicate that there are addresses and data before and after what is shown.
In this specific case, we see addresses 0x0400 to 0x040C (decimal 1024 to 1036) and we see a random appearing sequence of data for each address beginning with 0x48 and ending with 0x00. Then, we see ‘…’ to show us that further along at the addresses 0x0678 and 0x0679 we have the data value 0x04 and 0x00. We assume that there is lots of other data at the other addresses, but since we aren’t showing them they aren’t needed for this explanation. The first sequence of bytes aren’t as random as they appear. They are the ASCII codes [www.asciitable.com] for the characters ‘Hello World!’ followed by the null byte 0x00 that to C indicates the end of a string.
Originally, C had data types with names like char or int and the size of these data types was machine dependent. K&R (The C Programming Language, Brian Kernighan and Dennis Ritchie, 2nd edition) defines char: ‘a single byte, capable of holding one character from the local character set.’ Since that character set is ASCII, you only need seven bits to encode it. The extra bit is used as a sign bit leading to the bizarre concept (IMHO) of signed and unsigned char. A signed character has values of –128 to 127, and an unsigned character has a value of 0 to 255. There are no negative ASCII characters nor are there any above 127 (in the basic set). The sign lets you use chars as ordinary numbers within the ranges shown above but confuses novices who ask: “Is char a number or a character?”
C has grown a bit since K&R and the C99 standards library defined terms that help make data type more specific and portable by giving them known numbers of bits for a given type. This convention uses the data types int8_t and uint8_t for the unsigned and signed eight-bit types (formerly char), and int16_t and uint16_t for 16-bit data types (formerly int). That’s a lot clearer, isn’t it? Well, not to me, but after we get past this introductory article, we will stop using the original K&R C names and start using those from C99.
Suppose you have the task of writing a function that sends strings of characters out the serial port. There would be many ways to do this, but one way would be to provide that function the address of the first character in a string. Then, the function could send out that character, add one to the address, and send each subsequent character until it runs into the character that C considers to end a string: nul (0x00) that we mentioned above. Such a function operating on the data in Figure 1 would send ‘Hello World!’ on the serial port. A function like this would need some way to get the address of the string. In C, we can send that address in the function parameter list by using a pointer. In C, we use ‘*’ to indicate that a variable is a pointer. We’ll see this again shortly.
Please don’t glaze over here, you are going to have to understand this to move on with C programming and the next concept is the one that trips up most folks just starting out with C. The final two addresses shown in Figure 1 (0x0678 and 0x0679) are for the values 0x04 and 0x00. You could read these two bytes and combine them into the word 0x0400 and use that as the address of some other location in the memory which — by no accident — is the address of the first item in the data sequence for the ‘Hello World!’ string that begins at address 0x0400. This is a key concept in programming: data values can be the address of other data values. When data values are used as addresses of other data, they are called pointers. In our example, the data at addresses 0x678 and 0x679 are used as a pointer to the ‘Hello World!’ string that begins at 0x0400. I admit that this discussion has been repetitious, but for some folks, pointers are the single most difficult concept to get past in all of computer programming.
Pointers are the reason that many refer to C as a mid-level rather than a high level programming language. In high level languages, the programmer only deals with data and the compiler makes all the decisions about the addresses. In low level languages (like assemblers), the programmer assigns the addresses to the data. In C, we are not required to play with addresses, but are allowed to if we want to. Some things can only be done using pointers. Pointers also allow us to do many things more efficiently and cleverly than would otherwise be the case. But they can be dangerous. To quote K&R, p 93: “Pointers have been lumped with the goto statement as a marvelous way to create impossible to understand programs. This is certainly true when they are used carelessly, and it is easy to create pointers that point somewhere unexpected. With discipline, however, pointers can also be used to achieve clarity and simplicity.” This would be a good time to assert that if you really want to learn the C programming language, you should get K&R. Books like my C Programming for Microcontrollers (available from Nuts & Volts) are also good for getting started specifically with micros, but to really get the religion, you need a copy of the “bible” and K&R is it.
As an example of a dangerous misuse of pointers, I once used a pointer to sequentially access the video buffer of an IBM PC. I made a simple ‘fence-post’ error, that is, I started a count with 1 instead of 0, and wrote the last data byte to an address that was one location outside of the video buffer. That byte was only occasionally important enough to crash the system and I nearly went nuts trying to figure out what was wrong. When your device crashes intermittently with no apparent rhyme or reason, you may well be suffering from a bad pointer use.
We declare a variable to be a pointer by preceding its name with an *, called the indirection or dereferencing operator:
char *q; // q is a pointer to a char
We get the address of a variable using &, called the address of operator:
// create a character variable initialized to 0x48
char v = ‘H’;
// put the address of v in the pointer q
q = &v;
In the case of the AVR, the compiler will know to create a 16-bit storage location for the ‘*q’ variable just like it knows to create an eight-bit location for the ‘v’ variable. Now, take a deep breath and commit all that to your memory so that the next time you see something like *myOhmy, you’ll know that it is the address of something — a pointer — and if you see &myAmi you’ll know that this is an operation that yields the address of the variable myAmi. Then, the statement myOhmy = &myAmi will make perfect sense.
To help our understanding, let’s play with these concepts on a PC using a simple and free C compiler. We’ll switch over to AVRs next month. Let’s use Pelles C that you can download from www.smorgasbordet.com/pellesc/.
This is canonic since it comes from the C bible
(K&R p. 6).
Interestingly, the Pelles C has a wizard application that creates a version of this program as a template for writing other programs.
|FIGURE 2. Peles C New Project.|
Open Pelles C, click on the File menu, and select New\Project. Figure 2 shows the resulting window with ‘Console Application Wizard’ highlighted and Hello_World typed into the ‘Name:’ field.
|FIGURE 3. Pelles C Console Wizard Part 1.||FIGURE 4. Console Wizard Part 2.|
Click OK and you’ll see the window shown in Figure 3. Check the ‘A “Hello, world” program. Yes, the Hello World program is so basic that it is included for you!
|FIGURE 5. Pelles C Hello World!.|
Click Next and you’ll see the window in Figure 4. Now click the finish button. As if by magic, Pelles will write your first Hello World program for you as shown in Figure 5. Next click the ‘Compile’ button, the ‘Execute’ button, and you’ll see the console output shown in Figure 6.
|FIGURE 6. Console Hello World!.|
Whoa! That’s so easy that it almost makes us forget that there are some not so easy things going on under the hood, and our job is to learn about those not so easy things. So, for the time being we will forget about the easy way to say hello and revert to some more basic C functions that are closer to the discussion about memory and pointers.
Using an Array
In C, when we want to create a sequence of characters in memory like we saw in Figure 1, we have several options. However, the one most closely related to the memory discussion is to define an array (a continuous sequence of memory locations) and initialize it with the data we want stored sequentially in memory. Pelles C allows us to revert to the original C data types: char for eight-bit and int for 16-bit (let’s not quibble at this point about signed and unsigned okay?). So, we create our data sequence by using:
char greet = “Hello, World!\n\0”;
This tells the compiler to store the indicated characters as a sequence in memory. The ‘\n’ and ‘\0’ are special non-printable characters; the first is newline (an instruction to the output device to create a new line) and the second is nul, with a value of 0. We added the ‘\n’ (which isn’t in Figure 1) to separate the output line in the console display. The following program will output the data to our terminal:
int main(int argc, char *argv)
char greet = “Hello, World!\n\0”;
for(i =0 ; greet[i] != ‘\0’; i++)
This program uses the putchar() function from the stdio library and reads each character from the memory one at a time, and outputs each character individually. In the first program, we used the printf() function, but under the hood the printf() function calls some code not unlike what we see here. The ‘for’ loop sends a character from the greet array, increments the address of the array, then if the value isn’t equal (!=) to ‘\0’ it sends the character. It loops through each address until it finds the ‘\0’ character, then it stops.
Using a Pointer
Now suppose we have a bunch of arrays of data and we want to simplify our lives by writing a function whose job it is to send out a nul-terminated array (a string) to the console. We could write a function, consoleOut() and use it as follows:
void consoleOut(char *); //declare the function
int main(int argc, char *argv)
char greet = “Hello, World!\n\0”;
char south = “Howdy, ya’ll!\n\0”;
char north = “Get oudda my face!\n\0”;
void consoleOut(char *x)
for(i =0 ; *x != ‘\0’; i++)
In C, the name of an array without the following square brackets is a pointer to the first element in the array data sequence. For instance, if the C compiler just happened to follow our example and put the characters in the greet array beginning at address 0x0400 as shown in Figure 1, then the word ‘greet’ will be a pointer to address 0x0400. (Our compiler might, theoretically, store that address at addresses 0x0678 and 0x0679 also as shown in Figure 1.) So, we define the consoleOut(char *) to tell the compiler that this function takes a pointer to a character as a parameter (char *). Then, when we use consoleOut(greet) the compiler knows that ‘greet’ is a pointer to a char and to use the address of the array named ‘greet’ (hypothetically 0x0400) and not any element of that array. We send the specific ‘greet’ pointer in the parameter list, but the function is generic so it receives the pointer as ‘*x’ and uses it as a pointer to the data in the greet array. So, when the ‘for’ loop begins, *x is pointing to ‘H’, then we increment the pointer. Notice that when we increment the address we are using ‘x’ which the compiler knows is the address, so x++ adds one to the address.
Here’s one more example to show a use of the ‘address-of’ operator &.
int main(int argc, char *argv)
char c1 = ‘H’;
char c2 = ‘e’;
// Assign the address of c2 to ptr
char *ptr = &c2;
printf(“c1 has the hex value 0x%X (%c) and is stored at %p\n”\
,c1, c1,(void *)&c1);
printf(“c2 has the hex value 0x%X (%c) and is stored at %p\n”\
, c2, c2,(void *)&c2);
printf(“ptr has the value %p and is stored at %p\n”\
, ptr, (void *)&ptr);
printf(“The value of the char pointed to by ptr is 0x%X (%c)\n”\
Note that the ‘\’ at the end of the line allows the printf() statements to continue on the next line which is only necessary here to reduce the width of the source code to make it fit the text. Also, don’t worry so much about all the ‘stuff’ like 0x%X in the printf() statements which are out of the scope for this discussion. Just pay attention to the use of ‘*’ and ‘&’. Executing this code yields the results shown in Figure 7.
|FIGURE 7. Console pointer demo.|
Next time, we will apply some of this hard-won knowledge to the AVR and give examples that run on microcontrollers. Meanwhile, if you want to get a leg up on C, buy the book C Programming for Microcontrollers from Nuts & Volts, and if you want to get two legs up, get the combo with the book and hardware projects kit. It will come in handy. NV