Chapter 9: Structures
What's in Chapter 9?
Structure Declarations
Accessing elements of a structure
Initialization of structure
data
Using pointers to access structures
Passing structures as parameters to
functions
Example
of a Linear Linked List
Example of a Huffman Code
A structure is a collection of variables that share a single name. In an array, each element has the same format. With structures we specify the types and names of each of the elements or members of the structure. The individual members of a structure are referenced by their subname. Therefore, to access data stored in a structure, we must give both the name of the collection and the name of the element. Structures are one of the most powerful features of the C language. In the same way that functions allow us to extend the C language to include new operations, structures provide a mechanism for extending the data types. With structures we can add new data types derived from an aggregate of existing types.
Like other elements of C programming, the structure must be declared before it can be used. The declaration specifies the tagname of the structure and the names and types of the individual members. The following example has three members: one 8-bit integer and two word pointers
struct
theport{
unsigned char
mask; // defines
which bits are active
unsigned long volatile
*addr; // pointer to its address
unsigned long
volatile
*ddr;}; // pointer to its direction reg
The above declaration does not create any variables or allocate any space. Therefore to use a structure we must define a global or local variable of this type. The tagname (theport) along with the keyword struct can be used to define variables of this new data type:
struct
theport PortA,PortB,PortE;
The above line defines the three variables and allocates 9 bytes for each of variable. Because the pointers will be 32-bit aligned the compiler will skip three bytes between mask and addr, so each object will occupy 12 bytes. If you knew you needed just three copies of structures of this type, you could have defined them as
struct
theport{
unsigned
char
mask; // defines which bits are
active
unsigned
long
volatile *addr;
unsigned long
volatile
*ddr;}PortA,PortB,PortE;
Definitions like the above are hard to extend, so to improve code reuse we can use typedef to actually create a new data type (called port in the example below) that behaves syntactically like char int short etc.
struct
theport{
unsigned
char
mask; // defines which bits are
active
unsigned
long
volatile
*addr; // address
unsigned long
volatile
*ddr;}; // direction reg
typedef struct theport port_t;
port_t PortA,PortB,PortE;
Once we have used typedef to create port_t, we don't need access to the name theport anymore. Consequently, some programmers use to following short-cut:
typedef
struct {
unsigned
char
mask; // defines which bits are
active
unsigned
long
volatile
*addr; //
address
unsigned long
volatile
*ddr;}port_t; // direction reg
port_t PortA,PortB,PortE;
Similarly, I have also seen the following approach to creating structures that uses the same structure name as the typedef name:
struct
port{
unsigned
char
mask; // defines which bits are
active
unsigned
long
volatile
*addr; // address
unsigned long
volatile
*ddr;}; // direction reg
typedef struct port port;
port PortA,PortB,PortE;
Most compilers support all of the above methods of declaring and defining structures.
Accessing Members of a Structure
We need to specify both the structure name (name of the variable) and the member name when accessing information stored in a structure. The following examples show accesses to individual members:
PortB.mask
= 0xFF; // the TM4C123
has 8 bits on PORTB
PortB.addr = (unsigned long volatile *)(0x400053FC
);
PortB.ddr
= (unsigned
long
volatile *)(0x40005400);
PortE.mask
=
0x3F; //
the
TM4C123
has 6 bits on PORTE
PortE.addr =
(unsigned
long
volatile *)(0x400243FC);
PortE.ddr = (unsigned long
volatile *)(0x40024400);
(*PortE.ddr) =
0; //
specify PortE as inputs
(*PortB.addr) = (*PortE.addr); // copy from PortE to
PortB
The syntax can get a little complicated when a member of a structure is another structure as illustrated in the next example:
struct
theline{
int
x1,y1; // starting point
int
x2,y2; // starting point
unsigned char color;}; // color
typedef struct theline line_t;
struct thepath{
line_t L1,L2; // two
lines
char direction;};
typedef struct thepath path_t;
path_t
p; //
global
void Setp(void){ line_t myLine; path_t q;
p.L1.x1 = 5; // black line from 5,6 to 10,12
p.L1.y1 = 6;
p.L1.x2 = 10;
p.L1.y2 = 12;
p.L1.color = 255;
p.L2.x1 = 0; // white line from 0,1 to 2,3
p.L2.y1 = 1;
p.L2.x2 = 2;
p.L2.y2 = 3;
p.L2.color = 0;
p.direction = -1;
myLine = p.L1;
q = p;
};
The local variable declaration line myLine; will allocate 9 bytes on the stack while path q; will allocate 19 bytes on the stack. In actuality most C compilers in an attempt to maintain addresses as 32-bit aligned numbers will actually allocate 12 and 28 bytes respectively. In particular, the Cortex M executes faster if accesses occur on word-aligned addresses. For example, a 32-bit data access to an odd address requires two bus cycles, while a 32-bit data access to a word-aligned address requires only one bus cycle. Notice that the expression p.L1.x1 is of the type int, the term p.L1 has the type line, while just p has the type path. The expression q=p; will copy the entire 15 bytes that constitute the structure from p to q.
Just like any variable, we can specify the initial value of a structure at the time of its definition.
path_t
thePath={{0,0,5,6,128},{5,6,-10,6,128},1};
line_t theLine={0,0,5,6,128};
port_t PortE={0x3F,
(unsigned long volatile *)(0x400243FC
),
(unsigned long
volatile *)(
0x40024400
)};
If we leave part of the initialization blank it is filled with zeros.
path_t
thePath={{0,0,5,6,128},};
line_t thePath={5,6,10,12,};
port_t PortE={1, (unsigned char volatile *)(0x100A),};
To place a structure in ROM, we define it as a global constant. In the following example the structure fsm[3] will be allocated and initialized in ROM-space. The linked-structure finite syatem machine is a good example of a ROM-based structure. For more information about finite state machines see either Section 6.5 of Embedded Systems: Introduction to ARM Cortex M Microcontrollers by Jonathan W. Valvano, or Section 3.5 of Embedded Systems: Real-Time Interfacing to ARM Cortex M Microcontrollers by Jonathan W. Valvano.
struct State{
unsigned char Out; /* Output to Port
B */
unsigned short Wait; /* Time (62.5ns
cycles) to wait */
unsigned char AndMask[4];
unsigned char EquMask[4];
const struct State *Next[4];}; /* Next states */
typedef const struct State state_t;
typedef state_t
* StatePtr;
#define stop &fsm[0]
#define turn &fsm[1]
#define bend &fsm[2]
state_t
fsm[3]={
{0x34,
16000, // stop 1 ms
{0xFF, 0xF0, 0x27, 0x00},
{0x51, 0xA0, 0x07, 0x00},
{turn, stop, turn, bend}},
{0xB3,40000, // turn 2.5 ms
{0x80, 0xF0, 0x00, 0x00},
{0x00, 0x90, 0x00, 0x00},
{bend, stop, turn, turn}},
{0x75,32000, // bend 2 ms
{0xFF, 0x0F, 0x01, 0x00},
{0x12, 0x05, 0x00, 0x00},
{stop, stop, turn, stop}}};
Using pointers to access structures
Just like other variables we can use pointers to access information stored in a structure. The syntax is illustrated in the following examples:
void Setp(void){ path_t *ppt;
ppt = &p; //
pointer to an existing global variable
ppt->L1.x1 = 5; // black line from 5,6 to 10,12
ppt->L1.y1 = 6;
ppt->L1.x2 = 10;
ppt->L1.y2 = 12;
ppt->L1.color = 255;
ppt->L2.x1 = 0; // white line from 0,1 to 2,3
ppt->L2.y1 = 1;
ppt->L2.x2 = 2;
ppt->L2.y2 = 3;
ppt->L2.color = 0;
ppt->direction = -1;
(*ppt).direction = -1;
};
Notice that the syntax ppt->direction is equivalent to (*ppt).direction. The parentheses in this access are required, because along with () and [], the operators . and -> have the highest precedence and associate from left to right. Therefore *ppt.direction would be a syntax error because ppt.direction can not be evaluated.
As an another example of pointer access consider the finite state machine controller for the fsm[3] structure shown above. The state machine is illustrated in Figure 9-1, and the program shown in Listing 9-4. There is an example in Chapter 10 that extends this machine to implement function pointers.
Figure 9-1: State machinevoid control(void){ StatePtr Pt;
unsigned char Input; unsigned int i;
SysTick_Init();
Port_Init();
Pt =
stop;
// Initial State
while(1){
GPIO_PORTA_DATA_R =
Pt->Out; // 1) output
SysTick_Wait(Pt->Wait);
// 2) wait
Input =
GPIO_PORTB_DATA_R;
// 3) input
for(i=0;i<4;i++)
if((Input&Pt->AndMask[i])==Pt->EquMask[i]){
Pt
=
Pt->Next[i]; // 4) next depends on input
i=4; }}};
Passing Structures to Functions
Like any other data type, we can pass structures as parameters to functions. Because most structures occupy a large number of bytes, it makes more sense to pass the structure by reference rather than by value. In the following "call by value" example, the entire 12-byte structure is copied on the stack when the function is called.
unsigned
long Input(port_t thePort){
return (*thePort.addr);}
When we use "call by reference", a pointer to the structure is passed when the function is called.
typedef const struct {
unsigned
char
mask; // defines which bits are
active
unsigned
long
volatile *addr; // address
unsigned long
volatile *ddr;}port; // direction reg
port_t
PortE={0x3F,
(unsigned long volatile *)(0x400243FC
),
(unsigned long
volatile *)(
0x40024400
)};
port_t
PortF={0x1F,
(unsigned long
volatile *)(0x400253FC),
(unsigned long
volatile *)(0x40025400)};
int MakeOutput(port_t
*ppt){
(*ppt->ddr) = ppt->mask
; // make output
return 1;}
int MakeInput(port_t
*ppt){
(*ppt->ddr )=
0x00; // make input
return 1;}
unsigned char Input(
port_t
*ppt){
return (*ppt->addr);}
void Output(port_t
*ppt, unsigned char data){
(*ppt->addr) = data;
}
int
main(void){ unsigned char MyData;
MakeInput(&PortE);
MakeOutput(&PortF);
Output(&PortF,0);
MyData=Input(&PortE);
return 1;}
One of the applications of structures involves linking elements together with pointers. A linear linked list is a simple 1-D data structure where the nodes are chained together one after another. Each node contains data and a link to the next node. The first node is pointed to by the HeadPt and the last node has a null-pointer in the next field. A node could be defined as
struct
node{
unsigned short data; // 16 bit
information
struct node
*next; // pointer to the next
};
typedef struct node node_t;
node_t
*HeadPt;
Figure 9-3: Linear linked list with 3 nodes
In order to store more data in the structure, we will first create a new node then link it into the list. The routine StoreData will return a true value if successful.
#include
<stdlib.h>;
int StoreData(unsigned short info){ node_t
*pt;
pt=malloc(sizeof(node_t
)); //
create a new entry
if(pt){
pt->data=info; //
store data
pt->next=HeadPt; //
link into existing
HeadPt=pt;
return(1);
}
return(0); //
out of memory
};
In order to search the list we start at the HeadPt, and stop when the pointer becomes null. The routine Search will return a pointer to the node if found, and it will return a null-pointer if the data is not found.
node_t
*Search(unsigned
short info){
node_t
*pt;
pt=HeadPt;
while(pt){
if(pt->data==info)
return
(pt);
pt=pt->next; //
link to next
}
return(pt); //
not found
};
To count the number of elements, we again start at the HeadPt, and stop when the pointer becomes null. The routine Count will return the number of elements in the list.
unsigned
short Count(void){
node_t
*pt;
unsigned short cnt;
cnt = 0;
pt = HeadPt;
while(pt){
cnt++;
pt =
pt->next; //
link to next
}
return(cnt);
};
If we wanted to maintain a sorted list, then we can insert new data at the proper place, in between data elements smaller and larger than the one we are inserting. In the following figure we are inserting the element 250 in between elements 200 and 300.
Figure 9-4: Inserting a node in sorted orderIn case 1, the list is initially empty, and this new element is the first and only one. In case 2, the new element is inserted at the front of the list because it has the smallest data value. Case 3 is the general case depicted in the above figure. In this situation, the new element is placed in between firstPt and secondPt. In case 4, the new element is placed at the end of the list because it has the largest data value.
int
InsertData(unsigned short info){
node_t
*firstPt,*secondPt,*newPt;
newPt = malloc(sizeof(node_t
)); //
create a new entry
if(newPt){
newPt->data =
info; //
store data
if(HeadPt==0){ //
case 1
newPt->next
= HeadPt; //
only element
HeadPt =
newPt;
return(1);
}
if(info<=HeadPt->data){ //
case 2
newPt->next
= HeadPt; //
first element in list
HeadPt =
newPt;
return(1);
}
firstPt =
HeadPt; //
search from beginning
secondPt =
HeadPt->next;
while(secondPt){
if(info
<= secondPt->data){ //
case 3
newPt->next
= secondPt; //
insert element here
firstPt->next
= newPt;
return(1);
}
firstPt =
secondPt; //
search next
secondPt =
secondPt->next;
}
newPt->next =
secondPt; //
case 4, insert at end
firstPt->next = newPt;
return(1);
}
return(0); //
out of memory
};
The following function will search and remove a node from the linked list. Case 1 is the situation in which an attempt is made to remove an element from an empty list. The return value of zero signifies the attempt failed. In case 2, the first element is removed. In this situation the HeadPt must be updated to now point to the second element. It is possible the second element does not exist, because the list orginally had only one element. This is OK because in this situation HeadPt will be set to null signifying the list is now empty. Case 3 is the general situation in which the element at secondPt is removed. The element before, firstPt, is now linked to the element after. Case 4 is the situation where the element that was requested to be removed did not exist. In this case, the return value of zero signifies the request failed.
int
Remove(unsigned short info){
node_t *firstPt,*secondPt;
if(HeadPt==0) // case 1
return(0); //
empty list
firstPt = HeadPt;
secondPt = HeadPt->next;
if(info==HeadPt->data){ //
case 2
HeadPt = secondPt; //
remove first element in list
free(firstPt);
//
return unneeded memory to heap
return(1);
}
while(secondPt){
if(secondPt->data==info){ //
case 3
firstPt->next=secondPt->next; //
remove this one
free(secondPt); //
return unneeded memory to heap
return(1);
}
firstPt =
secondPt; //
search next
secondPt =
secondPt->next;
}
return(0); //
case 4, not found
};
When information is stored or transmitted there is a fixed cost for each bit. Data compression and decompression provide a means to reduce this cost without loss of information. If the sending computer compresses a message before transmission and the receiving computer decompresses it at the destination, the effective bandwidth is increased. In particular, this example introduces a way to process bit streams using Huffman encoding and decoding. A typical application is illustrated by the following flow diagram.
Figure 9-5: Data flow diagram showing a typical application of Huffman encoding and decoding
The Huffman code is similar to the Morse code in that they both use short patterns for letters that occur more frequently. In regular ASCII, all characters are encoded with the same number of bits (8). Conversely, with the Huffman code, we assign codes where the number of bits to encode each letter varies. In this way, we can use short codes for letter like "e s i a t o n" (that have a higher probability of occurrence) and long codes for seldom used consonants like "j q w z" (that have a lower probability of occurrence). To illustrate the encode-decode operations, consider the following Huffman code for the letters M,I,P,S. S is encoded as "0", I as "10", P as "110" and M as "111". We can store a Huffman code as a binary tree.
Figure 9-6: Huffman code for the letters S I P M
If "MISSISSIPPI" were to be stored in ASCII, it would require 10 bytes or 80 bits. With this simple Huffman code, the same string can be stored in 21 bits.
Figure 9-7: Huffman encoding for MISSISSIPPI
Of course, this Huffman code can only handle 4 letters, while the ASCII code has 128 possibilities, so it is not fair to claim we have an 80 to 21 bit savings. Nevertheless, for information that has a wide range of individual probabilities of occurrence, a Huffman code will be efficient. In the following implementation the functions BitPut() and BitGet() are called to save and recover binary data. The implementations of these two functions were presented back in Chapter 2.
struct Node{
char Letter0; // ASCII code if binary 0
char Letter1; // ASCII code if binary 1
// Letter1 is NULL(0) if Link is pointer to another node
const struct Node *Link;}; // binary tree pointer
typedef const struct Node node_t
;
// Huffman tree
node_t
twentysixth= {'Q','Z',0};
node_t
twentyfifth= {'X',0,&twentysixth};
node_t
twentyfourth={'G',0,&twentyfifth};
node_t
twentythird= {'J',0,&twentyfourth};
node_t
twentysecond={'W',0,&twentythird};
node_t
twentyfirst= {'V',0,&twentysecond};
node_t
twentyth= {'H',0,&twentyfirst};
node_t
ninteenth= {'F',0,&twentyth};
node_t
eighteenth= {'B',0,&ninteenth};
node_t
seventeenth= {'K',0,&eighteenth};
node_t
sixteenth= {'D',0,&seventeenth};
node_t
fifteenth= {'P',0,&sixteenth};
node_t
fourteenth= {'M',0,&fifteenth};
node_t
thirteenth= {'Y',0,&fourteenth};
node_t
twelfth= {'L',0,&thirteenth};
node_t
eleventh= {'U',0,&twelfth};
node_t
tenth= {'R',0,&eleventh};
node_t
ninth= {'C',0,&tenth};
node_t
eighth= {'O',0,&ninth};
node_t
seventh= {' ',0,&eighth};
node_t
sixth= {'N',0,&seventh};
node_t
fifth= {'I',0,&sixth};
node_t
fourth= {'S',0,&fifth};
node_t
third= {'T',0,&fourth};
node_t
second= {'A',0,&third};
node_t
root= {'E',0,&second};
//********encode***************
// convert ASCII string to Huffman bit sequence
// returns bit count if OK
// returns 0 if BitFifo Full
// returns 0xFFFF if illegal character
int encode(char *sPt){ // null-terminated ASCII string
int NotFound; char data;
int BitCount=0; // number of bits created
node_t
*hpt; // pointer into Huffman tree
while (data=(*sPt)){
sPt++; // next character
hpt=&root; // start search at root
NotFound=1; // changes to 0 when found
while(NotFound){
if ((hpt->Letter0)==data){
if(!BitPut(0))
return (0); // data structure full
BitCount++;
NotFound=0; }
else {
if(!BitPut(1))
return (0); // data structure full
BitCount++;
if ((hpt->Letter1)==data)
NotFound=0;
else { // doesn't match either Letter0 or Letter1
hpt=hpt->Link;
if (hpt==0) return (0xFFFF); // illegal, end of tree?
}
}
}
}
return BitCount;
}
//********decode***************
// convert Huffman bit sequence to ASCII
// will remove from the BitFifo until it is empty
// returns character count
int decode(char *sPt){ // null-terminated ASCII string
int CharCount=0; // number of ASCII characters created
unsigned int data;
node_t
*hpt;
// pointer into Huffman tree
hpt=&root; // start search at root
while (BitGet(&data)){
if (data==0){
(*sPt)= (hpt->Letter0);
sPt++;
CharCount++;
hpt=&root;} // start over and search at root
else //data is 1
if((hpt->Link)==0){
(*sPt)= (hpt->Letter1);
sPt++;
CharCount++;
hpt=&root;} // start over and search at root
else { // doesn't match either Letter0 or Letter1
hpt=hpt->Link;
}
}
(*sPt)=0; // null terminated
return CharCount;
}
Go to Chapter 10 on Functions Return to Table of Contents