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 6811 and 6812
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 ICC11 and ICC12 pointers contain 16-bit unsigned absolute addresses. This means that the ICC12 compiler does not provide for direct support of the extended memory available on the MC68HC812A4 microcomputer. There is an example from Valvano's Embedded Microcomputer Systems: Real Time Interfacing shown in Chapter 9, which, while illustrating structures, implements an extended address mechanism for the MC68HC812A4.

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 ICC11/ICC12/Metrowerks will zero global variables). The arrow identifies the execution location. Assume addresses 0x0810 through 0x081A exist in RAM.

Figure 7-1: Pointer Referencing

 

The expression &buffer[1] returns the address of the second 16 bit element of the buffer (0x0816). Therefore the line pt=&buffer[1]; makes pt point to 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 16-bit signed integer at 0x0816". I like to add the parentheses () to clarify that *and pt are one object. In this case the parentheses are not needed. Later when we perform address arithmetic, the parentheses will be important. Therefore the line (*pt)=0x1234; sets buffer[1] to 0x1234.

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 16-bit information from buffer[1] into data.)

We can get a better understanding of pointers by observing the assembly generated by our compiler. The following 6811 assembly was generated by ICC11 Version 4 when the above pointer example (figure 7-1) was compiled.

    .area text
    .globl _main
_main:  ldd #_buffer+2  ; pt=&buffer[1];
    std  _pt
    ldy  _pt    ;  (*pt)=0x1234;
    ldd  #4660
    std  0,y
    ldy  _pt    ;  data=(*pt);
    ldd  0,y
    std  _data
    rts
    .area bss
    .globl _pt
_pt:     .blkb 2
    .globl _data
_data:   .blkb 2
    .globl _buffer
_buffer: .blkb 8

The following 6812 assembly was generated by ICC12 Version 5.1 when the above pointer example (figure 7-1) was compiled.

    .area text
_main:: 
    movw #_buffer+2,_pt  ; pt=&buffer[1];
    ldd  #4660   ;  (*pt)=0x1234;
    ldy  _pt
    std  0,y
    ldy  _pt     ;  data=(*pt);
    ldy  0,y
    sty  _data
    rts
    .area bss
_pt::     .blkb 2
_data::   .blkb 2
_buffer:: .blkb 8

The following 6812 assembly was generated by Metrowerks Version 3.1 when the above pointer example (figure 7-1) was compiled. Notice that the Metrowerks compiler is highly optimized.

main:
    MOVW  #buffer:2,pt ;pt = &buffer[1];
    LDD   #4660        ;(*pt) = 0x1234;
    STD   [pt,PCR]
    STD   data         ;data = (*pt);
    RTS  

 

 

Memory Addressing

The size of a pointer depends on the architecture of the CPU and the implementation of the C compiler. Both the 6811 and 6812 employ an absolute memory addressing scheme in which an effective address is composed simply of a single 16-bit unsigned value. In particular the 6811 and 6812 registers are shown in Figure 7-5. The MC68HC812A4 does provide for extended addressing. For more information on this feature see Chapter 9 of Valvano's Embedded Microcomputer Systems: Real Time Interfacing.

Figure 7-2: The 6811 and 6812 have 16 bit address registers, X Y, SP PC.

 

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. The other types are usually loaded with object code from our S19 file and our program is allowed only to read the data. Table 7-1 shows the various types of memory available in the 6811 and 6812 microcomputer. 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 Freescale 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. For more information on how to write C code that dynamically writes EEPROM see Chapter 1 of Valvano's Embedded Microcomputer Systems: Real Time Interfacing. 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.

 

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 10,000 times
Flash nonvolatile easily reprogrammed 100 times
OTP PROM nonvolatile can be easily programmed once
ROM nonvolatile programmed at the factory once

Table 7-1: Various types of memory available for the 6811 and 6812.

 

From a logical standpoint we define implement segmentation when we group together in memory information that has similar properties or usage. Typical software segments include global variables (data section), the heap, local variables, fixed constants (idata section), and machine instructions (text section). Global variables are permanently allocated and usually accessible by more than one program. We must use global variables for information that must be permanently available, or for information that is to be shared by more than one module. We will see the first-in-first-out (FIFO) queue is a global data structure that is shared by more than one module. Imagecraft and Metrowerks both allow the use of a heap to dynamically allocate and release memory. This information can be shared or not shared depending on which modules have pointers to the data. The heap is efficient in situations where storage is needed for only a limited amount of time. Local variables are usually allocated on the stack at the beginning of the function, used within the function, and deallocated at the end of the function. Local variables are not shared with other modules. Fixed constants do not change and include information such as numbers, strings, sounds and pictures. Just like the heap the fixed constants can be shared or not shared depending on which modules have pointers to the data.

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 0xF000 and pt1 contains 0x1000, the expression will yield true, since 0xF000 is a higher unsigned value than 0x1000.

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 */
#define START_CRITICAL() asm(" tpa\n staa %SaveSP\n sei")
#define END_CRITICAL() asm( ldaa %SaveSP\n tap")
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) {unsigned char SaveSP;
    START_CRITICAL(); /* make atomic, entering critical section */
    PUTPT=GETPT=&Fifo[0]; /* Empty when PUTPT=GETPT */
    END_CRITICAL(); /* end critical section */
}
int PutFifo (char data) { char *Ppt; /* Temporary put pointer */
unsigned char SaveSP;
    START_CRITICAL(); /* make atomic, entering critical section */
    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 ){
        END_CRITICAL();  /* end critical section */
        return(0);}   /* Failed, fifo was full */
    else{
        PUTPT=Ppt;
        END_CRITICAL();  /* end critical section */
        return(-1);   /* Successful */
    }
}
int GetFifo (char *datapt) {unsigned char SaveSP;
    if (PUTPT== GETPT){
        return(0);}   /* Empty if PUTPT=GETPT */
    else{
        START_CRITICAL(); /* make atomic, entering critical section */
        *datapt=*(GETPT++);
        if (GETPT == &Fifo[FifoSize])
            GETPT = &Fifo[0];
        END_CRITICAL();  /* end critical section */
        return(-1);
    }
}

Listing 7-3: Fifo queue implemented with pointers

The START_CRITICAL and END_CRITICAL macros are specific to ICC11/ICC12, otherwise this example will operate using Metrowerks.

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 as shown.

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 format used by both the Imagecraft and Metrowerks compilers fits the following model. The following listing shows one 8-bit and two 16-bit 6811 I/O ports. The line TFLG1=0x08; generates an 8-bit I/O write operation to the port at address 0x1023. The TCNT on the right hand side of the assignment statement generates a 16-bit I/O read operation from the port at address 0x100E. The TOC5 on the left hand side of the assignment statement generates a 16-bit I/O write operation from the port at address 0x101E. The TFLG1 inside the while loop generates repeated 8-bit I/O read operations until bit 3 is set.

#define TFLG1 *(unsigned char volatile *)(0x1023)
#define TCNT *(unsigned short volatile *)(0x100E)
#define
TOC5 *(unsigned short volatile *)(0x101E)
void wait(unsigned short delay){
   TFLG1=0x08; /* clear OC5F */
   TOC5=TCNT+delay; /* TCNT at end of wait */
   while((TFLG1&0x08)==0){}; /* wait for OC5F*/
}

Listing 7-4: Sample ICC11/Metrowerks Program that accesses I/O ports

A similar 6812 program is shown below.

#define TFLG1 *(unsigned char volatile *)(0x004E)
#define TCNT *(unsigned short volatile *)(0x0044)
#define
TC5 *(unsigned short volatile *)(0x005A)
void wait(unsigned short delay){
   TFLG1 = 0x20; /* clear C5F */
   TC5 = TCNT+delay; /* TCNT at end of wait */
   while((TFLG1&0x20)==0){}; /* wait for C5F*/
}

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

It was mentioned earlier that the volatile modifier will prevent the compiler from optimizing I/O programs. I.e., these examples would not work if the compiler read TFLG1 once, the used the same data over and over inside the while loop.

To understand this syntax we break it into parts. Starting on the right is the absolute address of the I/O port. For example the 6811 TFLG1 register is at location 0x1023. The parentheses are necessary because the definition might be used in an arithmetic calculation. For example the following two lines are quite different:

   TheTime=*(unsigned char volatile *)(0x1023)+100;
   TheTime=*(unsigned char volatile *)0x1023+100;

In the second (incorrect) case the addition 0x01023+100 is performed on the address, not the data. The next part of the definition is a type casting. C allows you to change the type of an expression. For example (unsigned char volatile *) specifies that 0x1023 is an address that points at an 8-bit unsigned char. The * at the beginning of the definition causes the data to be fetched from the I/O port if the expression exists on the right-hand side of an assignment statement. The * also causes the data to be stored at the I/O port if the expression in on the left-hand side of the assignment statement. In this last way, I/O port accesses are indeed similar to pointers. For example the above example could have be implemented as:

unsigned char volatile *pTFLG1;
unsigned short volatile *pTCNT;
unsigned short volatile *p
TC5;
void wait(unsigned short delay){
   pTFLG1=(unsigned char volatile *)(0x004E);
   pTCNT=(unsigned short volatile *)(0x0044);
   p
TC5=(unsigned short volatile *)(0x005A);
   (*pTFLG1)=0x20;
   (*pTC5)=(*pTCNT)+delay;
   while(((*pTFLG1)&0x20)==0){};
}

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

This function first sets the three I/O pointers then accesses the I/O ports indirectly through the pointers.

There is a problem when using pointer variables to I/O ports on the 6812. The null pointer typically is defined as address 0, and PORTA also has address 0.  On the 6811, address 0 is RAM, so a similar confusion may arise if a pointer variable is set to access RAM location 0, then this pointer will look like a null pointer.

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