- •Acknowledgments
- •About the Author
- •1.1 Basic Computer Structure
- •1.3 A Few Instructions and Some Simple Programs
- •2 The Instruction Set
- •3.1 Op Code Byte Addressing Modes
- •4.2 Assembler Directives
- •4.3 Mechanics of a Two-Pass Assembler
- •4.6 Summary
- •5.1 Cross Assemblers and Downloaders
- •5 Problems
- •6.3 Passing Arguments by Value, Reference, and Name
- •7 Arithmetic Operations
- •7.2 Integer Conversion
- •8 Programming in C and C++
- •8.1 Compilers and Interpreters
- •9 Implementation of C Procedures
- •9.2 Expressions and Assignment Statements
- •9.4 Loop Statements, Arrays, and Structs
- •10 Elementary Data Structures
- •10.1 What a Data Structure Is
- •11.4 Synchronization Hardware
- •12.4 The 68300 Series
- •A2.1 Loading HiWare Software
- •A2.2 Opening the HiWare Toolbox
- •A2.3 Running Examples From the ManualProgramFolder
- •A2.6 POD-Mode BDM Interface
- •Index
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:
