Chapter 7: Pointers

What's in Chapter 7?

Definitions of address and pointer
Declarations of pointers define the type and allocate space in memory
How do we use pointers
Memory architecture of the TM4C123
Pointer math
Pointer comparisons
FIFO queue implemented with pointers
I/O port access

The ability to work with memory addresses is an important feature of the C language. This feature allows programmers the freedom to perform operations similar to assembly language. Unfortunately, along with the power comes the potential danger of hard-to-find and serious run-time errors. In many situations, array elements can be reached more efficiently through pointers than by subscripting. It also allows pointers and pointer chains to be used in data structures. Without pointers the run-time dynamic memory allocation and deallocation using the heap would not be possible. We will also use a format similar to pointers to develop mechanisms for accessing I/O ports. These added degrees of flexibility are absolutely essential for embedded systems.

Addresses and Pointers

Addresses that can be stored and changed are called pointers. A pointer is really just a variable that contains an address. Although, they can be used to reach objects in memory, their greatest advantage lies in their ability to enter into arithmetic (and other) operations, and to be changed. Just like other variables, pointers have a type. In other words, the compiler knows the format (8-bit 16-bit 32-bit, unsigned signed) of the data pointed to by the address.

Not every address is a pointer. For instance, we can write &var when we want the address of the variable var. The result will be an address that is not a pointer since it does not have a name or a place in memory. It cannot, therefore, have its value altered.

Other examples include an array or a structure name. As we shall see in Chapter 8, an unsubscripted array name yields the address of the array. In Chapter 9, a structure name yields the address of the structure. But, since arrays and structures cannot be moved around in memory, their addresses are not variable. So, although, such addresses have a name, they do not exist as objects in memory (the array does, but its address does not) and cannot, therefore, be changed.

A third example is a character string. Chapter 3 indicated that a character string yields the address of the character array specified by the string. In this case the address has neither a name or a place in memory, so it too is not a pointer.

 

Pointer Declarations

The syntax for declaring pointers is like that for variables (Chapter 4) except that pointers are distinguished by an asterisk that prefixes their names. Listing 7-1 illustrates several legitimate pointer declarations. Notice, in the third example, that we may mix pointers and variables in a single declaration. I.e., the variable data and the pointer pt3 are declared in the same statement. Also notice that the data type of a pointer declaration specifies the type of object to which the pointer refers, not the type of the pointer itself. As we shall see, all pointers on the Cortex M contain 32-bit unsigned absolute addresses. 

short *pt1;  /* define pt1, declare as a pointer to a 16-bit integer */
char *pt2;   /* define pt2, declare as a pointer to an 8-bit character */
unsigned short data,*pt3;    /* define data and pt3,
    declare data as an unsigned 16-bit integer and
    declare pt3 as a pointer to a 16-bit unsigned integer */
long *pt4;   /* define pt4, declare as a pointer to a 32-bit integer */
extern short *pt5;    /* declare pt5 as a pointer to an integer */

Listing 7-1: Example showing a pointer declarations

 

The best way to think of the asterisk is to imagine that it stands for the phrase "object at" or "object pointed to by." The first declaration in Listing 7-1 then reads "the object at (pointed to by) pt1 is a 16-bit signed integer."

Pointer Referencing

We can use the pointer to retrieve data from memory or to store data into memory. Both operations are classified as pointer references. The syntax for using pointers is like that for variables except that pointers are distinguished by an asterisk that prefixes their names. Figures 7-1 through 7-4 illustrate several legitimate pointer references. In the first figure, the global variables contain unknown data (actually we know the compiler will zero global variables). The arrow identifies the execution location. Assume addresses 0x20000000 through 0x20000017 exist in RAM.

long *pt;       // pointer to 32-bit data
long data;      // 32-bit
long buffer[4]; // array of 4 32-bit numbers
int main(void){
  pt = &buffer[1];
  *pt = 1234;
  data = *pt;
  return 1;
}
address      data          contents
0x20000000   0x00000000    pt
0x20000004
   0x00000000    data
0x20000008
   0x00000000    buffer[0]
0x2000000C
   0x00000000    buffer[1]
0x20000010
   0x00000000    buffer[2]
0x20000014
   0x00000000    buffer[3]
Figure 7-1: Pointer Referencing

The C code pt=&buffer[1]; will set the pt to point to buffer[1]. The expression &buffer[1] returns the address of the second 32-bit element of the buffer (0x2000000C). Therefore the line pt=&buffer[1]; makes pt point to buffer[1].

address      data          contents
0x20000000  
0x2000000C    pt
0x20000004
   0x00000000    data
0x20000008
   0x00000000    buffer[0]
0x2000000C
   0x00000000    buffer[1]
0x20000010
   0x00000000    buffer[2]
0x20000014
   0x00000000    buffer[3]

The C code (*pt)=0x1234; will store 0x1234 into the place pointed to by pt. In particular, it stores 0x1234 into  buffer[1]. When the *pt occurs on the left-hand-side of an assignment statement data is stored into memory at the address. Recall the *pt means "the 32-bit signed integer at 0x2000000C". I like to add the parentheses () to clarify that *and pt are one object. Therefore the line (*pt)=0x1234; sets buffer[1] to 0x1234.

address      data          contents
0x20000000  
0x2000000C    pt
0x20000004
   0x00000000    data
0x20000008
   0x00000000    buffer[0]
0x2000000C
   0x00001234    buffer[1]
0x20000010
   0x00000000    buffer[2]
0x20000014
   0x00000000    buffer[3]

The C code data=(*pt); will read memory from address pointed to by pointer pt into the place pointed to by data. In particular, it stores 0x1234 into  data. When the *pt occurs on the right-hand-side of an assignment statement data is retrieved from memory at the address. Again, I like to add the parentheses () to clarify that * and pt are one object. Therefore the line data=(*pt); sets data to 0x1234 (more precisely, it copies the 32-bit information from buffer[1] into data.)

address      data          contents
0x20000000  
0x2000000C    pt
0x20000004
   0x00001234    data
0x20000008
   0x00000000    buffer[0]
0x2000000C
   0x00001234    buffer[1]
0x20000010
   0x00000000    buffer[2]
0x20000014
   0x00000000    buffer[3]

The following Cortex M assembly was generated by Keil uVision when the above pointer example was compiled. 


    48:         pt = &buffer[1];
0x000003C4 4806      LDR           r0,[pc,#24]  ; @0x000003E0
0x000003C6 4907      LDR           r1,[pc,#28]  ; @0x000003E4
0x000003C8 6008      STR           r0,[r1,#0x00]
    49:         *pt = 1234;
0x000003CA F24040D2  MOVW          r0,#0x4D2
0x000003CE 6809      LDR           r1,[r1,#0x00]
0x000003D0 6008      STR           r0,[r1,#0x00]
    50:         data = *pt;
0x000003D2 4804      LDR           r0,[pc,#16]  ; @0x000003E4
0x000003D4 6800      LDR           r0,[r0,#0x00]
0x000003D6 6800      LDR           r0,[r0,#0x00]
0x000003D8 4903      LDR           r1,[pc,#12]  ; @0x000003E8
0x000003DA 6008      STR           r0,[r1,#0x00]
    51:         return 1;
0x000003DC 2001      MOVS          r0,#0x01
    52: }
0x000003DE 4770      BX            lr
 

 

Memory Addressing

The size of a pointer depends on the architecture of the CPU and the implementation of the C compiler. For more information on the architectureof the TM4C see Chapter 3 of Embedded Systems: Introduction to ARM Cortex M Microcontrollers by Jonathan W. Valvano.

Figure 7-2: Memory map of the TM4C123 and TM4C1294.

 

Most embedded systems employ a segmented memory architecture. From a physical standpoint we might have a mixture of regular RAM, battery-backed-up RAM, regular EEPROM, flash EPROM, regular PROM, one-time-programmable PROM and ROM. RAM is the only memory structure that allows the program both read and write access. Table 7-1 shows the various types of memory available on most microcomputers. The RAM contains temporary information that is lost when the power is shunt off. This means that all variables allocated in RAM must be explicitly initialized at run time by the software. If the embedded system includes a separate battery for the RAM, then information is not lost when the main power is removed. Some microcomputers have EEPROM. The number of erase/program cycles depends on the memory technology. EEPROM is often used as the main program memory during product development. In the final product we can use EEPROM for configuration constants and even nonvolatile data logging. The one-time-programmable PROM is a simple nonvolatile storage used in small volume products that can be programmed only once with inexpensive equipment. The ROM is a low-cost nonvolatile storage used in large volume products that can be programmed only once at the factory. ***For the number of write cycles available for ROM see the appropriate data sheet for that microcontroller.

 

Memory When power is removed Ability to Read/Write Program cycles
RAM volatile random and fast access infinite
battery-backed RAM nonvolatile random and fast access infinite
EEPROM nonvolatile easily reprogrammed ***
Flash nonvolatile easily reprogrammed ***
OTP PROM nonvolatile can be easily programmed once
ROM nonvolatile programmed at the factory once

Table 7-1: Various types of memory available for microntrollers.

 

In an embedded application, we usually put global variables, the heap, and local variables in RAM because these types of information can change during execution. When software is to be executed on a regular computer, the machine instructions are usually read from a mass storage device (like a disk) and loaded into memory. Because the embedded system usually has no mass storage device, the machine instructions and fixed constants must be stored in nonvolatile memory. If there is both EEPROM and ROM on our microcomputer, we put some fixed constants in EEPROM and some in ROM. If it is information that we may wish to change in the future, we could put it in EEPROM. Examples include language-specific strings, calibration constants, finite state machines, and system ID numbers. This allows us to make minor modifications to the system by reprogramming the EEPROM without throwing the chip away. If our project involves producing a small number of devices then the program can be placed in EPROM or EEPROM. For a project with a large volume it will be cost effective to place the machine instructions in ROM.

 

Pointer Arithmetic

A major difference between addresses and ordinary variables or constants has to do with the interpretation of addresses. Since an address points to an object of some particular type, adding one (for instance) to an address should direct it to the next object, not necessarily the next byte. If the address points to integers, then it should end up pointing to the next integer. But, since integers occupy two bytes, adding one to an integer address must actually increase the address by two. Likewise, if the address points to long integers, then adding one to an address should end up pointing to the next long integer by increasing the address by four. A similar consideration applies to subtraction. In other words, values added to or subtracted from an address must be scaled according to the size of the objects being addressed. This automatic correction saves the programmer a lot of thought and makes programs less complex since the scaling need not be coded explicitly. The scaling factor for long integers is four; the scaling factor for integers is two; the scaling factor for characters is one. Therefore, character addresses do not receive special handling. It should be obvious that when define structures (see Chapter 9) of other sizes, the appropriate factors would have to be used.

A related consideration arises when we imagine the meaning of the difference of two addresses. Such a result is interpreted as the number of objects between the two addresses. If the objects are integers, the result must be divided by two in order to yield a value which is consistent with this meaning. See Chapter 8 for more on address arithmetic.

When an address is operated on, the result is always another address of the same type. Thus, if ptr is a signed 16-bit integer pointer, then ptr+1 is also points to a signed 16-bit integer.

Precedence determines the order of evaluation. See a table of precedence. One of the most common mistakes results when the programmer meglects the fact the * used as a unary pointer reference has precedence over all binary operators. This means the expression *ptr+1 is the same as (*ptr)+1 and not *(ptr+1). This is an important point so I'll mention it again, "When confused about precedence (and aren't we all) add parentheses to clarify the expression."

 

Pointer Comparisons

One major difference between pointers and other variables is that pointers are always considered to be unsigned. This should be obvious since memory addresses are not signed. This property of pointers (actually all addresses) ensures that only unsigned operations will be performed on them. It further means that the other operand in a binary operation will also be regarded as unsigned (whether or not it actually is). In the following example, pt1 and pt2[5] return the current values of the addresses. For instance, if the array pt2[] contains addresses, then it would make sense to write

 

short *pt1;      /* define 16-bit integer pointer */
short *pt2[10];  /* define ten 16-bit integer pointers */
short done(void){  /* returns true if pt1 is higher than pt2[5] */
    if(pt1>pt2[5]) return(1);
    return(0);
}

Listing 7-2: Example showing a pointer comparisons

 

which performs an unsigned comparison since pt1 and pt2 are pointers. Thus, if pt2[5] contains 0x2000F000 and pt1 contains 0x20001000, the expression will yield true, since 0x2000F000 is a higher unsigned value than 0x20001000.

It makes no sense to compare a pointer to anything but another address or zero. C guarantees that valid addresses can never be zero, so that particular value is useful in representing the absence of an address in a pointer.

Furthermore, to avoid portability problems, only addresses within a single array should be compared for relative value (e.g., which pointer is larger). To do otherwise would necessarily involve assumptions about how the compiler organizes memory. Comparisons for equality, however, need not observe this restriction, since they make no assumption about the relative positions of objects. For example if pt1 points into one data array and pt2 points into a different array, then comparing pt1 to pt2 would be meaningless. Which pointer is larger would depend on where in memory the two arrays were assigned.

A FIFO Queue Example

To illustrate the use of pointers we will design a two-pointer FIFO. The first in first out circular queue (FIFO) is also useful for data flow problems. It is a very common data structure used for I/O interfacing. The order preserving data structure temporarily saves data created by the source (producer) before it is processed by the sink (consumer). The class of FIFO’s studied in this section will be statically allocated global structures. Because they are global variables, it means they will exist permanently and can be shared by more than one program. The advantage of using a FIFO structure for a data flow problem is that we can decouple the source and sink processes. Without the FIFO we would have to produce 1 piece of data, then process it, produce another piece of data, then process it. With the FIFO, the source process can continue to produce data without having to wait for the sink to finish processing the previous data. This decoupling can significantly improve system performance.

GETPT points to the data that will be removed by the next call to GET, and PUTPT points to the empty space where the data will stored by the next call to PUT. If the FIFO is full when PUT is called then the subroutine should return a full error (e.g., V=1.) Similarly, if the FIFO is empty when GET is called, then the subroutine should return an empty error (e.g., V=1.) The PUTPT and GETPT must be wrapped back up to the top when they reach the bottom.

 

Figure 7-3: Fifo example showing the PUTPT and GETPT wrap.

 

There are two mechanisms to determine whether the FIFO is empty or full. A simple method is to implement a counter containing the number of bytes currently stored in the FIFO. GET would decrement the counter and PUT would increment the counter. The second method is to prevent the FIFO from being completely full. For example, if the FIFO had 100 bytes allocated, then the PUT subroutine would allow a maximum of 99 bytes to be stored. If there were already 99 bytes in the FIFO and another PUT were called, then the FIFO would not be modified and a full error would be returned. In this way if PUTPT equals GETPT at the beginning of GET, then the FIFO is empty. Similarly, if PUTPT+1 equals GETPT at the beginning of PUT, then the FIFO is full. Be careful to wrap the PUTPT+1 before comparing it to GETPT. This second method does not require the length to be stored or calculated.

/* Pointer implementation of the FIFO */
#define FifoSize 10 /* Number of 8 bit data in the Fifo */
char *PUTPT;    /* Pointer of where to put next */
char *GETPT;    /* Pointer of where to get next */
/* FIFO is empty if PUTPT=GETPT */
/* FIFO is full if PUTPT+1=GETPT */
char Fifo[FifoSize]; /* The statically allocated fifo data */
void InitFifo(void) {
    PUTPT=GETPT=&Fifo[0]; /* Empty when PUTPT=GETPT */
}
int PutFifo (char data) { char *Ppt; /* Temporary put pointer */

    Ppt=PUTPT; /* Copy of put pointer */
    *(Ppt++)=data; /* Try to put data into fifo */
    if (Ppt == &Fifo[FifoSize]) Ppt = &Fifo[0]; /* Wrap */
    if (Ppt == GETPT ){
        return(0);}   /* Failed, fifo was full */
    else{
        PUTPT=Ppt;
        return(-1);   /* Successful */
    }
}
int GetFifo (char *datapt) {
    if (PUTPT== GETPT){
        return(0);}   /* Empty if PUTPT=GETPT */
    else{

        *datapt=*(GETPT++);
        if (GETPT == &Fifo[FifoSize])
            GETPT = &Fifo[0];

        return(-1);
    }
}

Listing 7-3: Fifo queue implemented with pointers

Since these routines have read modify write accesses to global variables the three functions (InitFifo, PutFifo, GetFifo) are themselves not reentrant. Consequently interrupts are temporarily disabled, to prevent one thread from reentering these Fifo functions. One advantage of this pointer implementation is that if you have a single thread that calls the GetFifo (e.g., the main program) and a single thread that calls the PutFifo (e.g., the serial port receive interrupt handler), then this PutFifo function can interrupt this GetFifo function without loss of data. So in this particular situation, interrupts would not have to be disabled. It would also operate properly if there were a single interrupt thread calling GetFifo (e.g., the serial port transmit interrupt handler) and a single thread calling PutFifo (e.g., the main program.) On the other hand, if the situation is more general, and multiple threads could call PutFifo or multiple threads could call GetFifo, then the interrupts would have to be temporarily disabled.

I/O Port Access

Even though the mechanism to access I/O ports technically does not fit the definition of pointer, it is included in this chapter because it involves addresses. The line NVIC_ST_RELOAD_R = delay-1; generates a 32-bit I/O write operation to the port at address 0xE000E014. The GPIO_PORTF_DATA_R on the right side of an assignment statement generates a 32-bit read from address 0x400253FC. The NVIC_ST_CTRL_R in the while loop generates a 32-bit I/O read operation from the port at address 0xE000E010. 

#define NVIC_ST_CTRL_R          (*((volatile unsigned long *)0xE000E010))
#define NVIC_ST_RELOAD_R        (*((volatile
unsigned long *)0xE000E014))
#define NVIC_ST_CURRENT_R       (*((volatile 
unsigned long *)0xE000E018))
#define GPIO_PORTF_DATA_R       (*((volatile unsigned long *)0x400253FC))

// The delay parameter is in units of the 80 MHz core clock. (12.5 ns)
void SysTick_Wait(
unsigned long delay){ unsigned long data;
  NVIC_ST_RELOAD_R = delay-1;  // number of counts to wait
  NVIC_ST_CURRENT_R = 0;       // any value written to CURRENT clears
  data =
GPIO_PORTF_DATA_R;
  while((NVIC_ST_CTRL_R&0x00010000)==0){ // wait for count flag
  }
}

Listing 7-5: Sample Program that accesses I/O ports

To understand the port definitions in C, we remember #define is simply a copy paste. E.g.,

    #define PA5   (*((volatile unsigned long *)0x40004080))
    data = PA5;

becomes

    data = (*((volatile unsigned long *)0x40004080));

To understand why we define ports this way, let’s break this port definition into pieces. First, 0x40004080 is the address of Port A bit 5. If we write just #define PA5 0x40004080 it will create

    data = 0x40004080;

which does not read the contents of PA5 as desired. This means we need to dereference the address. If we write #define PA5 (*0x40004080) it will create

    data = (*0x40004080);

This will attempt to read the contents at 0x40004080, but doesn’t know whether to read 8, 16, or 32 bits. So the compiler gives a syntax error because the type of data does not match the type of (*0x40004080).  To solve a type mismatch in C we typecast, placing a (new type) in front of the object we wish to convert. We wish force the type conversion to unsigned 32 bits, so we modify the definition to include the typecast. 

The volatile is added because the value of a port can change beyond the direct action of the software. It forces the C compiler to read a new value each time through a loop and not rely on the previous value.  

#define PA5   (*((volatile unsigned long *)0x40004080))
void wait(void){
  while((PA5&0x20)==0){};
}
void wait2(void){
  while(((*((volatile unsigned long *)0x40004080))&0x20)==0){};
}
void wait3(void){
  volatile unsigned long *pt;
  pt = ((volatile unsigned long *)0x40004080);
  while(((*pt)&0x20)==0){};
}

Listing 7-6:  Program that accesses I/O ports using pointers

The function wait3 first sets the I/O pointer then accesses the I/O port indirectly through the pointer.

Go to Chapter 8 on Arrays and Strings Return to Table of Contents