Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Introduction to Microcontrollers. Architecture, Programming, and Interfacing of the Motorola 68HC12 (G.J. Lipovski, 1999).pdf
Скачиваний:
191
Добавлен:
12.08.2013
Размер:
29.57 Mб
Скачать

io

Elementary Data Structures

In all the earlier chapters, we have used data structures along with our examples. While you should therefore be somewhat familiar with them, they need to be systematically studied. There are endless alternatives to the ways that data are stored, and so there is a potential for disorder. Before you get into a crisis due to the general disarray of your data and then convince yourself of the need for data structures, we want you to have the tools needed to handle that crisis. In this chapter, we systematically cover the data structures that are most useful in microcomputer systems.

The first section discusses what a data structure is in more detail. Indexable structures, includingthe frequently used vector, are discussed in the second section. The third section discusses sequential structures, which include the string and the stack structures. The linked list is briefly discussed next, only to give you an idea of what it is, while the conclusions summarize the chapter with recommendations for further reading on data structures.

At the end of this chapter, you should be able to use simple data structures, such as vectors and strings, with ease. You should be able to handle deques and their derivatives, stacks and queues, and you should know a linked list structure when you see one. This chapter should provide you with the tools that you need to handle most of the problems that cause confusion when storing data in your microcomputer programs.

10.1 What a Data Structure Is

In previous chapters, we described a data structure as the way data are stored in memory. While this description was adequate for those earlier discussions, we now want to be more precise. A data structure is more or less the way data are stored and accessed. This section expands on this definition.

A data structure is an abstract idea that is used as a reference for storing data. It is like a template for a drawing. For example, a vector is a data structure that we have used since Chapter 3. Several sets of data can be stored in a vector in the same program and the same "template" is used to store each set. You may write or see a program that uses vectors that have five 1-byte elements. While writing another program, you may recognize the need for a vector that has five 1-byte elements and, by using the same

291

294

Chapter 10 Elementary Data Structures

Figure 10.2. A Histogram

If the precision is 2 and the origin is 1, then if i is in accumulator B, Z(i) can be loaded

into D with

 

 

 

LDX

#Z

; Point X to Z

 

DECS

 

; i-1 into B

(3)

ASLB

 

; 2*(i - 1) into B

 

LDD

B f X

;Z(i)intoD

 

The origin is 1 for Z in the segments (2) and (3). It seems obvious by now that for assembly language or C programming, an origin of 0 has a distinct advantage because the DECS instruction can be eliminated from segments (2) and (3) if Z has an origin of 0. Unless stated otherwise, we will assume an origin of 0 for all of our indexable data structures. Accessing the elements of vectors with higher precision is straightforward and left to the problems at the end of the chapter.

A histogram is implemented with a vector data structure. In a histogram, there are, say, 20 counters, numbered zero through nineteen. Initially all counters are zero. A stream of numbers arrives, each between zero and nineteen. As each number i arrives, counter i is incremented. This vector of counts is the histogram.

Figure 10.2 illustrates the first six counts of the histogram Z, An item "2" arrives, so counter 2 should be incremented. In C, if the vector is Z and the number "2" is in i, then z [i]++; increments the counter for number "2"; and, in assembly language, if this number "2" is in index register X, then the instruction in (4) will increment the

counter:

 

 

INC Z, X

; increment the Xth count

(4)

Histograms are useful in gathering statistics. We used them to "reverse engineer" a TV infrared remote control; the counts enabled us to determine how a "1" and a "0" were encoded as pulse widths and how commands were encoded into 1 's and O's. Note that the data structure is a vector. Counts are accessed in random order as items arrive.

A list is similar to a vector except that each element in the list may have a different precision. Lists are stored in memory in buffers just like vectors, successive elements in successive memory locations. Like a vector, there is an origin and a length and each element of the list can be accessed. However, you cannot access the ith element of a list by the simple arithmetic computation used for a vector. Consider the following example of a list L that consists of 30 bytes for*a person's name (ASCII), followed by 4 bytes for his or her Social Security number (in C, an unsigned long), followed by a 45-byte address (ASCII), and another 4 bytes for the person's telephone number (an unsigned long). This list then has four elements, which we can label LO,LI, L2, and L3, and

10.3 Sequential Data Structures

301

Figure 10.5. Deque Data Structure

bottom elements can be accessed. Pushing an element onto the top (or bottom) makes the old top (or bottom) the next-to-top (or next-to-bottom) element and the element pushed becomes the new top (or bottom) element. Popping or pulling an element reads the top (or bottom) element, removes it from the deque, and makes the former next-to- top (or next-to-bottom) element the new top (or bottom) element (see Figure 10.5). You start at some point in memory and allow bytes to be pushed or pulled from the bottom as well as the top.

In C, two pointers can be used, as a pointer was used in the string data structure, or else indexes can be used to read or write on the top or bottom of a deque, and a counter is used to detect overflow or underflow. We use indexes in this example and invite the reader to use pointers in an exercise at the end of the chapter.

The deque buffer is implemented as a 50-element global vector deque, and the indexes as global unsigned chars top and hot initialized to the first element of the deque, as in the C declaration

unsigned char deque [50], size,error, top, bot;

Figure 10.6. Buffer for a Deque Wrapped on a Drum

10.3 Sequential Data Structures

 

303

DEQUE:

DS

 

50

PSHTP:

CMPA

#50

 

 

LBEQ

ERROR

; Go to error routine

 

INCA

 

 

 

CPX

#DEQUE+50

; Pointer on top?

 

BNE

LI

 

 

LDX

#DEQUE

; Move to bottom

LI:

STAB

1,X+

 

 

RTS

 

 

PLTP:

DECA

 

 

 

LBMI

ERROR

; Go to error routine

 

CPX

#DEQUE

; Pointer at bottom?

 

BNE

L2

 

 

LDX

#DEQUE+50

; Move to top

L2:

LDAB

1, -X

 

 

RTS

 

 

Figure 10.7. Subroutines for Pushing and Pulling B from the Top of the Deque

at the start of our program. If accumulator A contains the number of elements in the deque and if X and Y are the top and bottom pointers, we would initialize the deque with

CLRA

 

; Initialize deque count to 0

LDX

#DEQUE

; First push onto top into DEQUE

LDY

#DEQUE

; First push onto bottom into DEQUE+49

Pushing and pulling bytes between B and the top of the deque could be done with the subroutines PSHTP and PLTP, shown in Figure 10.7. The index register X points to the top of the deque, while the index register Y points to the deque's bottom.

Similar subroutines can be written for pushing and pulling bytes between B and the bottom of the deque. In this example, if the first byte is pushed onto the top of the deque, it will go into location DEQUE, whereas, if pushed onto the bottom, it will go into location DEQUE+4 9. Accumulator A keeps count of the number of bytes in the deque and location ERROR is the beginning of the program segment that handles underflow and overflow in the deque.

Usually, you do not tie up two index registers and an accumulator to implement a deque as we have done above. The pointers to the top and bottom of the deque and the count of the number of elements in the deque can be kept in memory together with the buffer for the deque elements. The subroutines for this implementation are easy variations of those shown in Figure 10.7. (See the problems at the end of the chapter.)

A queue is a deque where elements can only be pushed on one end and pulled on the other. We can implement a queue exactly like a deque but now only allowing, say, pushing onto the top and pulling from the bottom. The queue is a far more common sequential structure -than the deque because the queue models requests waiting to be serviced on a first-in first-out basis. Another very common variation of the deque, which is close to the queue structure, is the shift register or first-in first-out buffer. The shift register is a full deque that only takes pushes onto the top, and each push on the top is

10.4 Linked List Structures

307

Figure 10.11. Flowchart for Scanning Tree

In C, the linkage can be by means of indexes into a table, which is a vector of structs. The following procedure is intially called as in scan ( 0 ) ;

typedef struct node{char c; unsigned char l,r; } node; node table[10]; void scan(unsigned char i)

{if(i!=0xff) {scan(table[i].l);outch(table[i].c);scan(table[i].r); }}

A similar approach is used in assembly language, in the subroutine shown in Figure 10.12. It simply implements the flowchart, with some modifications to improve static efficiency. First, index register X points to the Oth list, so that LINK can be input as a parameter in accumulator B. This link value is multiplied by three to get the address of the character of the list. That address, with one added, gets to get the link to the left successor, and that address, with two added, gets the link to the right successor. The subroutine computes the value 3 * LINK and saves this value on the stack. In processing the left successor, the saved value is recalled, and one is added. The number at this location, relative to X, is put in B, and the subroutine is called. To print the letter, the saved value is recalled, and the character at that location is passed to the subroutine DUTCH, which causes the character to be printed. The saved value is pulled from the stack (because this is the last time it is needed), and two is added. The number at this location relative to X is passed in B as the subroutine is called again. A minor twist is used in the last call to the subroutine. Rather than doing it in the obvious way, with a BSR SCAN followed by an RTS, we simply do a BRA SCAN.The BRA will call the subroutine, but the return from that subroutine will return to the caller of this subroutine. This is a technique that you can always use to improve dynamic efficiency. You are invited, of course, to try out this little program.

308 Chapter 10 Elementary Data Structures

* SUBROUTINE SCAN scans the linked list TREE from the left, putting out the

* characters in alphabetical order. The calling sequence below scans TREE

*

*

LDX

#TREE

 

*

CLRB

 

 

; Put LINK to 0

*

BSR

SCAN

 

*

 

 

 

 

SCAN:

CMPB

#$FF

 

 

BEQ

L

 

 

 

LDAA

#3

 

 

 

MUL

 

 

 

 

PSHB

 

 

; Save 3 * B on stack

 

INCB

 

 

; 3 * (B) + 1 into B

 

LDAB

B,

X

; Left successor link into B

 

BSR

SCAN

 

 

LDAA

0,

SP

; Recover 3 * B

 

LDAA

A,

X

 

 

JSR

DUTCH

; Put out next character

 

PULB

 

 

; Recover 3 * B from stack, remove from stack

 

ADDB

#2

 

 

 

LDAB

B,

X

; Link to right successor into B

 

BRA

SCAN

 

L:RTS

Figure 10.12. Subroutine SCAN Using Indexes

The main idea of linked lists is that the list generally has an element that is the number of another list, or it has several elements that are numbers of other lists. The number, or link, allows the program to go from one list to a related list, such as the list representing a node to the list representing a successor of that node, by loading a register with the link element. The register is used to access the list. This is contrasted to a sequential search of consecutive rows of a table, which is a vector of lists. In a table, one

Location

Letter

Left

Right

0x800

t

0x803

0x806

0x803

c

OxSOc

OxSOf

0x806

X

0x809

0

0x809

u

0

0

0x8Oc

a

0

0x812

OxSOf

f

0

0

0x812

b

0

0

Figure 10.13. Linked List Data Structure for SCAN

PROBLEMS

 

 

 

 

315

 

LDA

ABB

ALP

DCB

$01

 

ADD

BAB

GAM

DCB

$00

 

STA

BBA

DEL

DCB

$04

 

SWI

 

BET

DCB

$03

ABB

DCB

$01

 

LDA

ALP

BAB

DCB

$02

 

ADD

BET

BBA

DCB

$00

 

ADD

DEL

 

END

 

 

STA

GAM

 

 

 

 

SWI

 

 

 

 

 

END

 

A two-pass assembler is required, and labels and opcodes must be stored as linked lists. Use subroutines GETS and CHKEND (Problem 19) to input labels or opcode mnemonics, and FIND (Problem 20) to search both the symbol table and the mnemonics, in your assembler. Show the storage structure for your mnemonic's binary tree (it is preloaded) following the graph shown in Figure 10.15. On the first pass, just get the lengths of each instruction or dkectives and save the labels and their addresses in a linked list. End pass one when END is encountered. On pass two, put the opcodes and addresses in the sring OBJ.

Figure 10.15. Graph of Linked List for Problem 10

11.2 Parallel Ports

321

Note that DDRA is declared an unsigned

char variable. Then, any time after that, to

output a char variable i to PORTA, put

 

PORTA = i;

Note that PORTA is declared an unsigned

char variable. To make PORTA an input

port, we can write

 

DDRA = 0;

Then, any time after, to input PORTA into an unsigned char variable i we write

i = PORTA;

Generally, the direction port is written into before the port is used the first time and need not be written into again. However, one can change the direction port from time to time, PORTA and PORTS together, and their direction ports DDRA and DDRB together, can be treated as a 16-bit port because they occupy consecutive locations. Therefore, they can

be read from or written into using LDD and STD instructions. To make PORTAB an output port, we can write in assembly language:

LDD

#$FFFF

; generate all ones

STD

$2

; put them in direction bits for output

Then, any time after that, to output accumulator D to PORTAB we can write

STD

$0

; output accumulator D

To make PORTAB an input port, we can write

 

CLR

$2

; put zeros in high direction bits for input

CLR

$3

; put zeros in low direction bits for input

Then, any time after that, to input PORTAB into accumulator D we can write

LDD

$0

; read PORTA into accumulator D

Also, some of the 16 bits can be made input, and some can be output. In manner similar to how 8-bit ports are accessed in C, 16-bit ports can be declared in a header file that is #included in each program as follows:

int PORTAB@0, DDRAB@2;

To make PORTA and PORTS an output port, we can write: DDRAB = Oxf ff f; . Note that DDRAB is declared an int variable. Then, any time after that, to output an int variable i, high byte to PORTA and low byte to PORTS, we can write PORTAB = i;. Note that PORTAB is declared an int variable. To make PORTA and PORTS an input port, we write: DDRAB = 0;. Then, any time after that, to input PORTA (as high byte) and PORTB (as low byte) into an int variable i we can write i = PORTAB;. The ports A and B, or the combined port AB, can be made an input or output port and can be easily accessed in assembly language or in C.

As a simple example of the use of an input port, consider a home security system. See Figure 11.4a. Three switches, each attached to a different window, are normally closed. When any window opens, its switch opens, and the pull-up resistor makes PORTA's input high; otherwise the input is low. This signal is sensed in PORTA bit 0. The C program statement if (PORTA & 1) alarm(); will execute procedure alarm if any switch is opened. It is optimally programmed into assembly language as follows: