Файл: Programming Microcontrollers in C, 2-nd edit (Ted Van Sickle, 2001).pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 15.06.2025
Просмотров: 4053
Скачиваний: 1
418 Chapter 8 MCORE, A RISC Machine
save these registers on the stack. The following two assembly instructions load the address of table into R2 and the address of fIpnd1 into R3. We want the contents of the value found in the FPIND register. Therefore, we must dereference the value in R3 twice, once to get the address of FPIND and then to get the contents of FPIND. This value has a bit set for every pending fast interrupt. When this value is operated on by the FF1 instruction, the register R3 will contain the 32 minus the bit number of the highest priority pending interrupt. If you look at how table() is constructed, you will find that the highest priority interrupts are listed first down to the lowest priority. Each entry in the table requires four bytes to hold a function pointer with this system. Therefore, we must multiply the value found in R3 by 4, shift it left 2, and then add the result to the value in R2 to find the address of the desired interrupt service routine.
All of that is exactly what is done with the assembly language insert shown above. The last instruction jsr R2 passes control of the computer to the specified interrupt service routine. If you look at the C code generated by handler() you will find first that the necessary computer status is saved and then the assembly code in the macro Do_Interrupt() is executed. The last instruction here is the jsr R2 instruction mentioned above. After the code needed for the interrupt service routine is completed, control is returned to handler() where the machine status is restored and control is returned to the interrupted program with an rfi, return from fast interrupt, instruction.
The compiler has a #pragma interrupt isr_function. This #pragma causes the function specified by the isr_function to be compiled as an interrupt service routine. These functions save a portion of the machine status, execute the code specified in the isr_function, and restore the machine status before returning to the interrupted program with an rfi instruction. When writing a general function such as the one above, it was found that the partial status save was not enough. No functions could be called from within the interrupt service routine. This limitation was too great for a general-purpose handler like that above, so in this routine, the entire status of the computer is saved and restored rather than the partial save and restore generated by the #pragma. It is safe to execute functions within the interrupt service routines in the handler above.
A Clock Program 419
Table 8-2 Interrupt Controller Assignments
Bit |
Use |
|
Number |
||
0, 1, 2 |
Software |
|
3 |
unused |
|
4 |
unused |
|
5 |
UART0 RTS_DELTA |
|
6 |
KPP control |
|
7 |
Time-of-day alarm |
|
8 |
PIT |
|
9 |
unused |
|
10 |
PWM0 |
|
11 |
PWM1 |
|
12 |
PWM2 |
|
13 |
PWM3 |
|
14 |
PWM4 |
|
15 |
PWM5 |
|
16 |
UART0 transmit |
|
17 |
UART1 transmit |
|
18 |
UART0 receive |
|
19 |
UART1 receive |
|
20 |
ISPI |
|
21 |
INT0 |
|
22 |
INT1 |
|
23 |
INT2 |
|
24 |
INT3 |
|
25 |
INT4 |
|
26 |
INT5 |
|
27 |
INT6 |
|
28 |
INT7 |
|
29 |
unused |
|
30 |
unused |
|
31 |
unused |
A Clock Program
A clock program is an excellent program for demonstrating the use of interrupts on a microcontroller. The MMC2001 has a rather extensive timer subsystem. It is broken into three parts. There is a time of day clock, TOD, with a built-in alarm, a watchdog timer and a programmable interval timer, PIT. We will use the PIT in this example. Briefly, the timers are driven by a separate clock. The clock here is a 32768-Hz clock controlled by a watch crystal. The TOD
420 Chapter 8 MCORE, A RISC Machine
clock is driven by the low-frequency oscillator frequency divided by 128 or 256 Hz. The watchdog and the PIT are both driven by the low-frequency oscillator frequency divided by 4 or 8196 Hz. The TOD keeps track of the time as the number of seconds since a specified time, perhaps midnight. There is no provision for automatic roll over of the registers at the end of the day, so if you want to work from midnight each day, you must reset the TOD clock to zero each midnight. This clock also has an 8-bit fractional second register in addition to the regular 32-bit second count register.
The alarm system also contains an 8-bit fractional second register and a 32-bit second register. When the time in the alarm registers matches the time in the time of day register, a flag is set that can be polled or an interrupt can be requested.
The PIT system contains a register that is counted down. When this register underflows, it can be reloaded automatically, set a flag to be polled, or request an interrupt. Loading this register is done through a register called the ITDR, the Interval Timer Data Register. If a bit is set, data written to the ITDR will automatically transfer to the ITADR, the register that counts down. When this register underflows, the contents of the ITDR is automatically transferred to the ITADR. The contents of the ITDR can be changed at any time, and it can be changed without altering the contents of the ITADR until the next underflow.
The ITADR is clocked 8192 times per second, approximately 122 microseconds. Since it is clocked an exact number of times per second, it is possible to count in exact second intervals. It makes no difference how you break up the time intervals, the clock will decrement exactly 8192 times in one second. Therefore, if you want a basic interval time for other uses in the system, you can cause interrupts to occur at this faster rate and then count the number of interrupts until you achieve the magic number of 8192 ticks each second. For example, many systems need a 1 or 2 millisecond time interval. The system can be set to interrupt about every 2 milliseconds and then in the ISR, a counter incremented. This counter counts to about 500 each second and can be easily used in a clock controller. Of course, “about” is not allowed. The interrupt time must be 0.001953125 seconds and there will be exactly 512 interrupts per second.
The program starts out with the creation of a function that we will call keep_time(). This function is executed repeatedly inside
A Clock Program 421
of a loop. There are four external variables, hours, seconds, minutes, and count accessed by keep_time(). The variable count is incremented in an interrupt service routine that is executed each 1.953125 milliseconds. The constant TIME_COUNT will have a value of 511. When count becomes greater than TIME_COUNT, count is reset to zero and the parameter seconds are incremented. When seconds exceeds the value 59, seconds is reset to zero and minutes is incremented. When minutes exceeds 59, it is reset to zero and hours is incremented. Finally, when hours becomes greater than 12, it is reset to 1 which corresponds to one o’clock. Once each second, the function output_time() is executed.
#include “mmc2001.h” #include “timer.h”
#define TIME_COUNT |
511 |
|
#define MAX_SECONDS |
59 |
|
#define MAX_MINUTES |
MAX_SECONDS |
|
#define |
MAX_HOURS |
12 |
#define |
MIN_HOURS |
1 |
/* function prototypes */ void output_time(void); void keep_time(void);
/* external—global variables */ WORD seconds,minutes,hours,count;
/* the main applications program. count is incremented in the isr 512 times each second. Therefore, this routine must be executed at least once every two milliseconds. */
void keep_time(void)
{
if(count>TIME_COUNT)
{
count=0; if(++seconds>MAX_SECONDS)
{
seconds=0; if(++minutes>MAX_MINUTES)
{
minutes=0; if(++hours>MAX_HOURS)
hours=MIN_HOURS;
422 Chapter 8 MCORE, A RISC Machine
}
}
output_time();
}
}
Here we are starting to write a program. Therefore, it is smart to do all things correctly. For example, we will attempt to avoid magic numbers by defining mnemonics for the numbers used in the program. Also, whenever a new function is written, its function prototype will be immediately inserted into a function prototype list at the beginning of the program. A series of external variables is used. It is usually better to use local variables when working with parameters in several different functions. In that case, the parameters can be passed as arguments to the functions when they are called. This approach avoids debug problems where it becomes difficult to determine where variables are changed in different functions. In the case here, the variables hours, minutes and seconds are changed in only one function, keep_time(), and the variable count is changed only in the interrupt service routine or the reset time function. Therefore, there is no uncertainty as to where the variables are changed.
Note the structure of the if() statements in the function keep_time(). The nesting of these statements
if()
{
.
if()
{
.
if()
{
.
}
}
}
causes the first test to be executed every time the function is executed. The argument of the first test must be TRUE when the second test is executed, and the argument of the second test must be TRUE when the third test is executed, and so forth. Most of the time when the function is executed, the total effort required by the above nested if
A Clock Program 423
sequence is the outside test only. Such an arrangement will require a minimum amount of computer time each time the function is executed. Many programmers will not nest the above tests so that each test is executed each time the function is called. It works, but it requires more computer resources than the nesting shown above.
In the function keep_time() above, note that the arguments of the several if() statements all involve a “greater than” test. This particular approach is more robust than any test that involves an “is equal to” test. In all cases but the hours, the tested parameter is reset to zero. Therefore, the zero is counted in the sequence and the maximum value is one less than the number that might be expected. For example, the range of seconds is 0..59 not 1..60, the range of count is 0.. 511 not 1..512. When the respective count exceeds the specified maximum value, the parameter is reset to its minimum value. If lightning should strike the chip and the value of seconds be set at 100, the “greater than” test would fix the error the next time that second were tested. In the event that an “is equal to” test were used, it would be a long time before the seconds would count through a wrap-around and be equal to the MAX_SECONDS value again.
Also note the if argument
if(++seconds>MAX_SECONDS)
{
The seconds are first incremented and then the test is completed. This sequence could be completed in two statements,
seconds=seconds+1; if(seconds>MAX_SECONDS)
{
Some people prefer the latter approach, but the two approaches accomplish exactly the same thing. The important item is that seconds must be incremented before the test is completed rather than after.
At the bottom of the seconds loop, a call to output_time() is executed. This function will send the current time to the output device. Here again, the call to output_time() could be placed anywhere in the loop, but the bottom, as is shown above, is best because the output takes place after all of the parameters are updated and the time displayed will be now rather than before all of the updates are completed.
424 Chapter 8 MCORE, A RISC Machine |
A Clock Program |
Let us now look at output_time(). For this program, we will send the time to the serial port to be displayed on a terminal. All of the parameters hours, minutes, and seconds each have a range of 0 or 1 to some maximum two-digit value. Therefore, these numbers will contain only tens and units. There will be no hundreds or thousands or fractions, or minus signs for that matter. To convert these values to characters to be sent to a function like putchar(), we have to do two things. First count the number of tens, convert this number to an ASCII digit and use it as an argument to putchar(). Then calculate the number of units in the number, convert it to an ASCII digit and send it to putchar(). These two operations are relatively easy to program. In fact, these little functions can be written as function-like macros easily, as follows:
#define hi(x) ((x)/10+’0’)
#define lo(x) ((x)%10+’0’)
Remember, the value passed to these two macro functions must lie between 0 and 99. The first function hi(x) determines the number of tens in the number and converts the result to an ASCII digit by adding the character zero to the result. The second calculates the number of units in the number by calculating the number modulo 10. This value is converted to an ASCII digit when the character zero is added.
With these two little macros in hand, the output_time() function is easy to write:
void output_time(void)
{
putchar(‘\r’);
putchar(hi(hours));
putchar(lo(hours));
putchar(‘:’);
putchar(hi(minutes));
putchar(lo(minutes));
putchar(‘:’);
putchar(hi(seconds));
putchar(lo(seconds));
}
The output_time() function consists of a series of putchar() function calls. The first has an argument ‘\r’. This command causes the cursor on the screen to be returned to its leftmost
A Clock Program 425
position. The next two calls send the hours to the screen. Then a colon is printed. This sequence is repeated for the minutes and the seconds except no colon is sent out after the seconds.
Our next problem is to set up the PIT. The PIT is to interrupt once each 1.953125 milliseconds. The clock that drives the PIT runs at 8192 Hz and we need a clock rate of 512 Hz. Therefore, we need to count 16 clock ticks between interrupts. I will call this time TWO_MS in the following code. The function below is an initialization function to set up the PIT to operate as needed. Recall, data written to the ITDR is transferred to the ITADR when the ITADR underflows. Data written to the ITDR is automatically transferred to the ITADR if the OVW bit is set when the ITDR is written. This bit will be set first and then the value TWO_MS is written to the ITDR. Next, the reload mode is enabled by setting the RLD bit in the ITCSR register. In this mode, the value in the ITDR is automatically reloaded into the ITADR when the ITADR underflows. This operation assures a periodic interrupt at the period set by the value stored in the ITDR.
The interrupt handler shown in the section “Handling Interrupts” will be used here. Recall that function contains a general-purpose interrupt service routine that can be used for any of the autovector interrupts in the MMC2001. For our purposes here, the interrupt vector number 8 in Listing 8-9 will have to be changed from unused_vector to the name of the PIT interrupt service routine. We will call that routine pit_isr and it will be written in the next section. The macro function vector() in the mmc2001.h header file will be used to put the address of the header into the Fast Autovector vector location. Also in the Interrupt Handler section you will find Table 8-1. This table shows the allocation of the interrupt vectors in the base vector table. This table is placed in a specified location by the linker program. With the set-up used for our programs in this book, I placed the system RAM at the address 0x30000000. The first 0x200 entries in this table are the vector table whose outline is shown in Table 8-1 and whose values are established by the interrupt handler routine in Listing 8-9. There you will note that the Fast Interrupt Autovector is offset 0x2c into the vector table. Therefore, the address of the FAST_AUTOVECTOR in our program must be 0x3000002c. The next instruction in the code below will place the address of the pit_isr interrupt service routine in this address.