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 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.
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 */
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."
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]
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
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.
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 |
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.
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."
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);
}
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.
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);
}
}
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.
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.
#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