Файл: Programming Microcontrollers in C, 2-nd edit (Ted Van Sickle, 2001).pdf

ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 15.06.2025

Просмотров: 4086

Скачиваний: 1

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.

Summary 209

The numeric display takes four inputs that are merely the BCD value of the number to be shown. With this particular display, a 4-bit num­ ber between the values of 0 and 9 will be displayed. If each of the four input lines to the display is turned on, the display will be turned off. The parameters passed to the function—high and low—are flags to indicate whether the corresponding output is to be turned on.

void display(int high, int low)

/* Display the contents of units and tens on the appropriate LED displays. High corresponds to tens, and low corresponds to units. If the proper argument is TRUE, the corresponding LED will be turned on. If the argument is FALSE, the LED will be turned off. */

{

unsigned int save; save = tens<<4; save |= units70x0f; PORTA=save; if(!high)

PORTA |= 0xf0; if(!low)

PORTA |= 0xf;

}

Summary

In this chapter, we have discussed programming techniques for a few of the more important peripheral components found on microcontrollers. Timers and ADC applications will be reconsidered in later chapters. In later chapters, serial communications, attendant programming of look-up tables, interpolation between data points in look-up tables, and synchronous communications from standard digi­ tal I/O pins rather than an SPI will be covered. Some small 8-bit microcontrollers have pulse width modulation (PWM) outputs that can be used as a digital-to-analog converter (DAC). Often the ranges available from these fixed PWM systems are not satisfactory for the required application. Other methods of accomplishing the PWM operation will be discussed in later chapters.

210 Chapter 4 Small 8-Bit Systems

Many of the peripheral components found on the small 8-bit de­ vices are found on larger microcontrollers. The next chapter introduces a group of larger microcontrollers, the M68HC11 family. The com­ piler used for the development of the M68HC05 code does not extend to the M68HC11 family. Therefore, the source code will look a little different in the following chapters, but it will still be all C. To gener­ ate code for the M68HC05, the compiler has had to “bend” the concepts of ANSI C to create code that would work with that family. Larger microcontrollers accommodate more of the large computer features so use of ANSI C is possible.


Chapter 5

Programming Large 8-Bit Systems

This chapter on the programming of large 8-bit systems will make use of the MC68HC11 microcontroller. It is absolutely necessary that any programmer understand the device when writing code for a microcontroller application. If you are not familiar with the MC68HC11 family, then read the M68HC11 Reference Manual and the M68HC11 E Series Technical Data Manual on the accompanying CD-ROM to get the needed background to be able to continue the work in this chapter.

Header File

The CD-ROM contains the C header file HC11E9.H. This file should be included with any program that is going to be used on the MC68HC11E9 or the MC68HC711E9. An abbreviated version of this header file is listed below. The header file has about 400 lines of source code, and most of that code is repeats of the portions of code shown in the following listing.

#ifndef HC11e9 #define HC11e9

unsigned int Register_Set = 0x1000;

typedef struct {

unsigned char bit0 : 1; unsigned char bit1 : 1; unsigned char bit2 : 1; unsigned char bit3 : 1;

211

212 Chapter 5 Programming Large 8-Bit Systems

unsigned char bit4 : 1; unsigned char bit5 : 1; unsigned char bit6 : 1; unsigned char bit7 : 1;

} Register;

#define PORTA (*(volatile Register*)(Register_Set+0))

typedef struct { unsigned char STAF :1; unsigned char STAI :1; unsigned char CWOM :1; unsigned char HNDS :1; unsigned char OIN :1; unsigned char PLS :1; unsigned char EGA :1; unsigned char INVB :1;

}Pioc;

#define PIOC (*(volatile Pioc*)(Register_Set+2))

#define PORTC (*(volatile Register*)(Register_Set+3)) #define PORTB (*(volatile Register*)(Register_Set+4))

.

.

.

#define TCNT (*(unsigned int *)(Register_Set+0xE)) #define TIC1 (*(unsigned int *)(Register_Set+0x10))

.

.

.

/* To clear bits in the flag registers use the form

TFLG1 = OC1

to clear OC1F in TFLG1. Use this form only in the two flag registers TFLG1 and TFLG2 below.

*/

Header File 213

#define OC1F 0x80 #define OC2F 0x40 #define OC3F 0x20 #define OC4F 0x10 #define I4O5F 0x08 #define IC1F 0x04 #define IC2F 0x02 #define IC3F 0x01

#define TFLG1 (*(unsigned char*)(Register_Set+0x23))

.

.

.

typedef struct { unsigned char TCLR :1; unsigned char SCP :2; unsigned char RCKB :1; unsigned char SCR :3;

}Baud;

#define BAUD (*(volatile Baud*)(Register_Set+0x2B))

.

.

.

/*

Macros and function to permit interrupt service

routine programming from C.

To use the vector call, do vector(isr,

vector_address)where isr is a pointer to the

interrupt service routine, and vector_address

is the vector address where the isr pointer

must be stored.

*/

#define vector(isr,vector_address) (*(void **)(vector_address)=(isr))

#define cli() _asm(“cli\n”)

#define sei() _asm(“sei\n”)


214Chapter 5 Programming Large 8-Bit Systems

#ifndef NULL

#define NULL (void *)0 enum {FALSE,TRUE}; enum {STOP,START}; enum {OFF,ON};

#define FOREVER while(TRUE) typedef unsigned int WORD; typedef unsigned char BYTE;

#endif

#endif

The first instruction in the file is

#ifndef HC11E9 #define HC11E9

and the last entry in the file is

#endif

These lines of code are useful to prevent multiple definitions of the items defined within the file. If the header file has not been previously compiled as a part of the program when the above statement is seen, HC11E9will not be defined. The first instruction determines if HC11E9 is not defined, and if it is not, the second line defines it. Then all of the code until the matching #endif will be compiled. If HC11E9is already defined, then the compiler will skip the code lines until the matching #endif is found. Therefore, the programmer can put an

#include <hc11E9.h>

at the beginning of each program module, and it will be used in the program only once even if several modules are combined into one and the above statement is included several times in a single file. This approach is convenient because it allows each module to be compiled and tested and then the several modules can be merged and compiled as a unit without worry about multiply defined variables and values found in header file.

The file next entry is a simple typedef and declaration of a structure called Register. This structure contains eight 1-bit entries. Each of these bits corresponds to a specific bit in a register field, and

Header File 215

the bits are given the names bit0,bit1, . . . bit7. Therefore, when dealing with specific bits within a type Register, the programmer should use bitx, where x is the number of the bit being referenced.

In the second portion of the file that follows, all of the I/O registers are declared.

The external variable Register_Set is given a value 0x1000. This value is the initial location of the I/O register map in the system. In the MC68HC11, bit manipulation assembly instructions have a onebyte address that can be an offset from an index register. With the indexed version of the instruction, any single byte or bit in the entire memory map can be accessed or tested with a single instruction. However, the address operation must be indexed. If you use a single address with offsets to each of the registers as is done in the header file, the compiler will automatically place the value of Register_Set into an index register and use the offsets specified to allow indexed access to the data in the registers from anywhere in the program.

The value of Register_Set is not fixed by the microcontroller. It can be changed within the first 64 clock cycles following reset. To make this change, the programmer must assign the correct value to the INIT register in the I/O memory space. This value should be changed in the initialization routine for the program, which is usually written in assembly language. After the INIT register is changed, a new proper value assigned to Register_Set will allow the desired access to all registers and bits in the I/O memory map.

The definition of Register shows that any instance of this variable type is a collection of eight bits. Any location that is defined as a type Register is truly a collection of bits and each bit must be processed individually. For example, PORTA is declared to be of the type Register. Therefore, an expression like

PORTA = 0x3f;

will result in an illegal assignment error because PORTA is of the type Register, not char.

It is possible to make assignments to ports defined in the above manner as either a byte-wide field or as bit fields. Return to the initial declaration of a register in the header file:

typedef struct


216Chapter 5 Programming Large 8-Bit Systems

{

signed char bit 0 :1;

. . .

unsigned char bit 7 :1; } Register;

We can now declare:

typedef union

{

char byte; Register bits; } Mix_Register;

and thus

#define PORTA(*volatile Mix_Register*)(Register_Set +0)

will allow the programmer to use

PORTA.byte = 0x2E

to set the value of all bits in the port with one instruction and in the same program to use

PORTA.bits.bit3 = 1;

to set, reset, or test the individual bits inside of the port. In this book, we will use the bit fields only as shown in header file.

Since PORTA is of the type Register, it is possible to deal with the individual bits within this location by normal C constructs such as

PORTA.bit3 = 1; if(PORTA.bit6 == 0)

...

Of course, it is much better to define practical names to the various bits within the port to achieve even clearer code:

#define ON TRUE #define MOTOR bit3

#define PUSH_BUTTON bit6

.

Header File 217

.

.

PORTA.MOTOR = ON; /* turn the motor on */

.

.

.

if(PORTA.PUSH_BUTTON==ON)

{

do push button things

}

With this compiler, an int is a 16-bit value. Therefore, the registers that are two bytes are cast onto the type int. Usually these registers are accessed as ints only and there is no need to have the individual bit access afforded by the use of the Register type. The timer counter register (TCNT) is one such register that is accessed as an int only. There are also a few one byte, or 8-bit, registers that are accessed as bytes only. No bit accesses within these registers are needed. In these cases, the register is cast onto the type char. The several ADRx registers are examples of this type. The ADRx registers contain the result of an analog-to-digital conversion that is usually handled as an 8-bit unit only.

In most instances, register locations should be unsigned. The ADRx registers each contain the result of analog-to-digital conversions. These results are all unsigned. Therefore, these registers should be cast as unsignedchar. Also note that all of the timer count and input capture or output compare registers are also declared as unsigned.

In the listing above, you will note that there are two parts to the declaration of each register. The first identifies all of the bits in the register through a structure typedef. Then a macro definition of the port name causes each instance of the port name in the program to be replaced by the dereferenced value of the register address cast onto a pointer of the correct type. The port name is the name of the port found in the data manual, and the bit names given the bits in the structure are the names found in the data manual. Therefore, if you wish to set the bit named HNDS found in the register PIOC, you need to use

PIOC.HDNS=ON;

There are two register locations that work differently from the rest. These are the two flag registers whose bits are set by the

218 Chapter 5 Programming Large 8-Bit Systems

occurrence of an interrupt. The bits in these registers are turned off when the code instructs the bit to be set. Since these two registers are so different from the remainder of the registers in the system, I handle them differently. Rather than declare a structure for these registers, the individual bits are #defined as power-of-two values. Therefore, the bit OC2F is defined as 0x40 and the bit IC2F is #defined as 0x02. The flag1 register T F L G 1 is forced to the address Register_Set+0x23. Now to clear the bit OC2F, you need to set

TFLG1=OC2F;

The final section of the file contains several macros that are helpful in handling interrupt service routines. The first macro is

#define vector(a,b) ((*(void **)b) = (a))

This macro is used to place the address of an interrupt service routine into a vector address. The argument a is a pointer to the interrupt service routine, and b is the vector address. If one asks what b is, you must say that b is a pointer to a location that contains the address of the interrupt service routine. The interrupt service routine address is also a pointer to function that has no (void) return. Therefore, the vector address is a pointer to a pointer to the type void and must be cast as such before it can be used. This value must be dereferenced to be able to place the address of the interrupt service routine into it. You can use the macro vector(a,b) to place the address of each interrupt service routine used into the proper vector location.

This macro will create code that copies the address of an interrupt service routine to the specified memory location. This operation is needed whenever the vector table is stored in RAM, so that the vector table must be rebuilt each time the microcontroller is powered up. Another approach must be used to place interrupt service routine addresses in the vector table when this table is contained in ROM. This latter case is probably more common than the former. In this case, we are trying to fill a memory array with the values of the addresses of the several interrupt service routines that the program might use. One way to do this operation is to build an array that contains these addresses, compile this array, and then at link time force the array to be linked to the memory location corresponding to the beginning of the vector table. Consider the following code sequence.


Header File 219

extern void IC1_Isr(), OC3_Isr(),_stext();

void (* const vector[])()={0,0,0,0,0,0,0,OC3_Isr,0, 0,0,0,IC1_Isr, 0,0,0,0,0,0,0,_stext};

This two-line sequence identifies three functions, each of which returns nothing. The first two are interrupt service routines that will process interrupts from input capture 1 and output compare 3, respectively. The third entry is the name of the entry point in the start-up routine that is linked to the C program. The second line of code indicates that vector is an array of const pointers to functions of the type void . There is one entry in this array for each interrupt vector and the reset vector for the micro-controller. This array is initialized with either zeros or the addresses of the interrupt service routines in the proper locations. The address of the start-up routine is placed in the last location in the array. This little program will be compiled and linked to the final program. The name of the file that contains this file is interrup.c , and it must be modified for each program in which it is used. At link time, the address of vector[ ] will be forced to 0xffd6 which is the beginning of the vector table in the MC68HC11 family.

Some programmers will initialize the vector table with a known address rather than 0. In the event that a spurious interrupt occurs and takes the processor to an unused vector location, the processor would certainly get lost if the vector table were filled with zeros. Placing a known program into all unused vector locations will prevent this problem.

Another problem can occur in the operation of unattended microcontrollers. It is possible that control of the microcontroller could be diverted to unused ROM locations by serious noise spikes. Such a loss of control will not be devastating if the system uses a computer operating properly (COP) system. However, another safety back-up that the programmer can incorporate into the program is to fill all unused memory with the one-byte instruction swi (software interrupt). This instruction causes the program to save the machine status and pass control to the function addressed in the SWI vector location. If this value contains the address of the start-up program, the system will be restarted immediately if control is accidentally moved to unused ROM.

220 Chapter 5 Programming Large 8-Bit Systems

There are several small routines associated with the processing of interrupts that are not in the C library. These routines are written as functions to make it easier to access these important operations. The cli() and sei() functions allow the program to clear or set the interrupt bit in the condition code register (CCR). The programmer can with these two functions and the interrupt masks in the I/O memory map control all interrupt operations of the part.

The compiler must be notified that a function is to be an interrupt service routine. An interrupt service routine can have no arguments, and it must return nothing. Therefore, the function prototype of an interrupt service routine might look like

void isr_clock( void );

However, the compiler would still have no way of knowing that this function is an interrupt service routine. The reason that the compiler must know an isr is that the MC68HC11 family stacks the complete machine status when an interrupt is accepted by the device. This status must be restored when the program control is returned to the interrupted program. The machine status is restored when an rti instruction is executed. Therefore, any return from an interrupt must be the assembly instruction rti rather than the instruction rts usually used to return from a function. An interrupt service routine is identified to the compiler by the sequence @port ahead of the definition of the function in the function prototype. The function prototype of an interrupt service routine should be

@port void isr_clock( void );

This flag will cause all returns from the function to be rti instructions. There is no guarantee that there will be macro definitions for useful numbers and functions. A series of enumerations that define TRUE, FALSE, ON, OFF, START, STOP, etc., are included to pro­ vide mnemonics for these often-used values. The pointer constant NULL is also defined by a macro here. These constants are often defined in other header files, so the protection against including these constants more than one time is also included. Also useful is the

macro FOREVER which is included.