What's in Chapter 7?
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.
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 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
*/
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
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.
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
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.
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 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);
}
}
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.
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.
No comments:
Post a Comment