Interrupt Service Routines
On this page you'll learn all about 68HC11 interrupts, and how to use them to respond to external or timing events. Understanding interrupts will enable you to write fast, responsive C language programs for your QCard Controller to provide real-time control of your instrument.
The QCard Controller's 68HC11 microcontroller provides 21 interrupts to enhance the performance of its on-chip resources, including the following:
- Analog to digital converter (A/D)
- Timer subsystem and pulse accumulator
- Watchdog timer
- Serial communications port
- High speed serial peripheral interface (SPI), and,
- General purpose digital I/O
Interrupts allow rapid response to time-critical events that often occur in measurement and control applications. For example, you can use interrupts to create a pulse-width modulated (PWM) output signal.
Identifying the interrupt you need
Each interrupt has a unique ID which you use in your software to refer to the interrupt. For example, to attach an interrupt service routine to the interrupt using ATTACH() you use the interrupt ID as one of the arguments of ATTACH()
.
The following tables provides interrupt IDs and descriptions for both the non-maskable and maskable interrupts of the 68HC11:
Non-maskable Interrupt IDs | |
---|---|
Interrupt ID | Name and Description |
(none) | Reset — Recognized when the /RESET (active-low reset) pin is pulled low, this highest priority nonmaskable interrupt resets the machine immediately upon recognition and executes the standard QED-Forth restart sequence. |
XIRQ_ID | IRQ external pin — This pseudo-nonmaskable interrupt is enabled by clearing the X bit in the condition code register. An /XIRQ interrupt is recognized when the /XIRQ (active-low nonmaskable interrupt) pin is pulled low. This interrupt is serviced immediately upon recognition. It requires an appropriate user-installed interrupt handler. |
SWI_ID | Software interrupt — Software interrupts are triggered by execution of the SWI opcode. After being recognized, an SWI interrupt is always the next interrupt serviced provided that no reset, clock monitor, COP, or illegal opcode interrupt occurs. SWI requires a user-installed interrupt handler. |
ILLEGAL_OPCODE_ID | Illegal opcode trap — This interrupt occurs when the processor encounters an unknown opcode. QED-Forth installs a default service routine for this interrupt that performs the standard restart sequence. |
COP_ID | COP failure/reset — After enabling the computer operating properly (COP) subsystem, failure to update COP registers within a predetermined timeout period triggers this interrupt which resets the processor and executes a user-defined service routine. QED-Forth installs a default service routine for this interrupt that performs the standard restart sequence. |
CLOCK_MONITOR_ID | Clock monitor failure/reset — Enabled or disabled via the CME (clock monitor enable) bit in the OPTION register, this interrupt is recognized if the E-clock frequency drops below 10 kHz. It resets the processor hardware and executes a user-defined service routine. QED-Forth installs a default service routine for this interrupt that performs the standard restart sequence. |
Maskable Interrupt IDs | |
---|---|
Interrupt ID | Name and Description |
IRQ_ID | /IRQ — /IRQ is an active-low external hardware interrupt which is recognized when the signal on the /IRQ pin of the 68HC11 is pulled low (MC68HC11F1 Technical Data Manual, p.2-5). |
RTI_ID | Real Time Interrupt — The RTI provides a programmable periodic interrupt (MC68HC11F1 Technical Data Manual, p.5-5). |
IC1_ID | Input Capture 1 — An IC1 interrupt is recognized when a specified signal transition is sensed on port A, pin 2 (MC68HC11F1 Technical Data Manual, p.9-1 ff.). |
IC2_ID | Input Capture 2 — An IC2 interrupt is recognized when a specified signal transition is sensed on port A, pin 1 (MC68HC11F1 Technical Data Manual, p.9-1 ff.). |
IC3_ID | Input Capture 3 — An IC3 interrupt is recognized when a specified signal transition is sensed on port A, pin 0 (MC68HC11F1 Technical Data Manual, p.9-1 ff.). |
OC1_ID | Output Compare 1 — An OC1 interrupt is recognized when the main timer’s count becomes equal to OC1’s timer compare register (MC68HC11F1 Technical Data Manual,p.9-6 ff.). |
OC2_ID | Output Compare 2 — An OC2 interrupt is recognized when the main timer’s count becomes equal to OC2’s timer compare register (MC68HC11F1 Technical Data Manual,p.9-6 ff.). |
OC3_ID | Output Compare 3 — An OC3 interrupt is recognized when the main timer’s count becomes equal to OC3’s timer compare register (MC68HC11F1 Technical Data Manual, p.9-6 ff.). |
OC4_ID | Output Compare 4 — An OC4 interrupt is recognized when the main timer’s count becomes equal to OC4’s timer compare register (MC68HC11F1 Technical Data Manual, p.9-6 ff.). |
IC4_OC5_ID | I4O5 — Depending on its configuration, an I4O5 (input capture 4/output compare 5) interrupt is recognized when a specified signal transition is sensed on port A, pin 3, or when I4O5’s timer compare register is equal to the main timer’s count (MC68HC11F1 Technical Data Manual, p.9-1 ff.). |
TIMER_OVERFLOW_ID | Timer Overflow — A TOF interrupt occurs when the free-running count in the TCNT (timer count) register overflows from FFFFH to 0000H (MC68HC11F1 Technical Data Manual, p.9-1). |
PULSE_OVERFLOW_ID | Pulse Accum Overflow — A PAOVF interrupt occurs when the PACNT (pulse accumulator count) register overflows from FFH to 00H (MC68HC11F1 Technical Data Manual, p.9-15 ff.). |
PULSE_EDGE_ID | Pulse Accum Edge — A PEDGE interrupt occurs after a signal edge is detected on port A, pin 7 (MC68HC11F1 Technical Data Manual, p.9-15 ff.). |
SPI_ID | SPI Event Interrupt — An SPI (serial peripheral interface) interrupt occurs after a byte transfer is completed, or a write collision or a mode fault is detected (MC68HC11F1 Technical Data Manual, p.8-1 ff.). |
SCI_ID | SCI Event Interrupt — An SCI (serial communications interface) interrupt occurs when the transmit data register is empty, or the transmission is complete, or the receive data register is full, or an idle line is detected. The handler must determine which of these four events caused the interrupt (M68HC11 Reference Manual, Section 9.5.2). |
Interrupt recognition and servicing
Maskable interrupts
Maskable interrupts may be freely enabled and disabled by software. They may be generated as a result of a variety of events, including signal level changes on a pin, completion of predetermined time intervals, overflows of special counting registers, and communications events.
Recognition and servicing of maskable interrupts are controlled by a global interrupt enable bit (the I bit in the condition code register) and a set of local interrupt mask bits in the hardware control registers. If a local interrupt mask bit is not enabled, then the interrupt is masked and will not be recognized. If the relevant local mask bit is enabled and the interrupt event occurs, the interrupt is recognized and its interrupt flag bit is set to indicate that the interrupt is pending. It is serviced when and if the global interrupt bit (the I
bit) is enabled. An interrupt that is not in one of these states is inactive; it may be disabled, or enabled and waiting for a triggering event.
When an interrupt is both recognized and serviced, the processor pushes the programming registers onto the stack to save the machine state, and automatically globally disables all maskable interrupts by setting the I bit in the condition code register until the service routine is over. Other maskable interrupts can become pending during this time, but will not be serviced until interrupts are again globally enabled when the service routine ends. (The programmer can also explicitly re-enable global interrupts inside an interrupt service routine to allow nesting of interrupts, but this is not recommended in multitasking applications). Non-maskable interrupts (reset, clock monitor failure, COP failure, illegal opcode, software interrupt, and XIRQ) are serviced regardless of the state of the I bit.
When an interrupt is serviced, execution of the main program is halted. The programming registers (CCR
, ACCD
, IX
, IY
, PC
) and the current page are pushed onto the return stack. This saves the state of execution of the main program at the moment the interrupt became serviceable. Next, the processor automatically sets the I bit in the condition code register. This disables interrupts to prevent the servicing of other maskable interrupts. The processor then fetches an address from the interrupt vector associated with the recognized interrupt, and starts executing the code at the specified address. It is the programmer’s responsibility to ensure that a valid interrupt service routine, or "interrupt handler" is stored at the address pointed to by the interrupt vector.
The CPU then executes the appropriate interrupt handler routine. It is the programmer’s responsibility to ensure that a valid interrupt service routine is stored at the address pointed to by the interrupt vector. The interrupt vectors are near the top of memory in the onboard ROM. The ROM revectors the interrupts (using jump instructions) to point to specified locations in the EEPROM. The ATTACH() routine installs a call to the interrupt service routine at the appropriate location in the EEPROM so that the programmer’s specified service function is automatically executed when the interrupt is serviced. ATTACH()
also supplies the required RTI
(return from interrupt) instruction that unstacks the programming registers and resumes execution of the previously executing program.
An interrupt handler must reset the interrupt flag bit (not the mask bit) by writing a 1 to it, and perform any tasks necessary to service the interrupt. When the interrupt handler has finished, it executes the RTI
(return from interrupt) assembly code instruction. RTI
restores the CPU registers to their prior values based on the contents saved on the return stack. As explained below, this also re-enables interrupts by clearing the I
bit in the CCR
(condition code register). Thus other interrupts can be serviced after the current interrupt service routine has completed.
Nonmaskable interrupts
Six of the 68HC11F1’s 21 interrupts are nonmaskable, meaning that they are serviced regardless of the state of the global interrupt mask (the I
bit in the CCR
). Events that cause nonmaskable interrupts include resets, clock monitor failure (triggered when the E-clock frequency drops below 10 kHz), Computer-Operating-Properly (COP) failure (triggered when a programmer-specified timeout condition has occurred), execution of illegal opcodes, execution of the SWI
(software interrupt) instruction, and an active low signal on the nonmaskable interrupt request pin named /XIRQ
.
Three types of interrupts initiate a hardware reset of the 68HC11:
- Power-on or activation of the reset button
- Computer-Operating-Properly (COP) timeout
- Clock monitor failure
These are the highest priority interrupts, and are nonmaskable. Serviced immediately, they initialize the hardware registers and then execute a specified interrupt service routine. QED-Forth sets the interrupt vectors of these interrupts so that they execute the standard startup sequence. The service routines for all but the main reset interrupt may be changed by the programmer with the Attach() utility.
If a nonmaskable interrupt is enabled, it is serviced immediately upon being recognized. The importance of these interrupts is reflected by the fact that most cause a hardware reset when serviced. The following table gives the name of each nonmaskable interrupt and a description of its operation. They are listed in order of priority from highest to lowest:
Priority of Non-maskable Interrupts | ||
---|---|---|
Priority | Interrupt ID | Name |
Highest | (none) | Reset |
↓ | CLOCK_MONITOR_ID | Clock monitor failure/reset |
↓ | COP_ID | COP failure/reset |
↓ | ILLEGAL_OPCODE_ID | Illegal opcode trap |
↓ | SWI_ID | Software interrupt |
Lowest | XIRQ_ID | IRQ external pin |
The service routine for the reset interrupt cannot be modified by the programmer. The service routines for the clock monitor, COP failure, and illegal opcode trap interrupts are initialized to perform the restart sequence, but this action may be changed by the programmer (see the Glossary entry for InitVitalIRQsOnCold() for more details). No default actions are installed for the SWI
and /XIRQ
interrupts, so before invoking these interrupts the user should install an appropriate interrupt service routine using the ATTACH() command.
Servicing maskable interrupts
Maskable interrupts are controlled by the I bit in the condition code register (M68HC11 Reference Manual, mc68hc11rm.rev4.1.pdf, Sections.5.7 and 5.8[DJS1]). When the I bit is set, interrupts are disabled, and maskable interrupts cannot be serviced. When clear, interrupts can be serviced, with the highest priority pending interrupt being serviced first. In sum, a locally enabled maskable interrupt is serviced if:
- it has been recognized, and
- it has the highest priority, and
- the I bit in the condition code register is clear.
If a maskable interrupt meets these criteria, the following steps are taken to service it. First, the programming registers and page are automatically saved on the return stack. Note that the condition code register, CCR
, is one of the registers saved, and that the saved value of the I
bit in the CCR
is 0
. Next, the CPU automatically sets the I
bit to 1
to temporarily prevent maskable interrupts from being serviced. Control is then passed to the interrupt handler code, which you must provide and post using ATTACH(). The interrupt handler clears the interrupt flag bit set by the trigger event and performs any tasks necessary to service the interrupt. It terminates with an RTI
instruction which restores the saved values to the programming registers. Execution then resumes where it left off.
Recall that when the interrupt service began, the processor’s first action was to store the programming registers on the return stack. At that time, the I
bit in the CCR
equaled 0
indicating that interrupts were enabled, and the bit was stored as 0
on the return stack. After stacking the machine state, the processor set the I bit to disable interrupts during the service routine. When the programming registers are restored to their prior values by RTI
, note that the I
bit is restored to its prior cleared state, indicating that interrupts are again enabled. In this manner the processor automatically disables interrupts when entering a service routine, and re-enables interrupts when exiting a service routine so that other pending interrupts can be serviced.
Nested interrupts
While the programmer can explicitly clear the I bit inside an interrupt service routine to allow nesting of interrupts, this is not recommended as it can cause crashes in multitasking applications.
Interrupt priority
Multiple pending interrupts are serviced in the order determined by their priority. Interrupts have a fixed priority, except that the programmer may elevate one interrupt to have the highest priority using the HIPRIO
register. Nonmaskable interrupts always have the highest priority when they are recognized, and are immediately serviced. The following table lists the fifteen available maskable interrupts in order of highest to lowest priority:
Priority of Maskable Interrupts | ||
---|---|---|
Priority | Interrupt ID | Name |
Highest | IRQ_ID | /IRQ |
↓ | RTI_ID | Real Time Interrupt |
↓ | IC1_ID | Input Capture 1 |
↓ | IC2_ID | Input Capture 2 |
↓ | IC3_ID | Input Capture 3 |
↓ | OC1_ID | Output Compare 1 |
↓ | OC2_ID | Output Compare 2 |
↓ | OC3_ID | Output Compare 3 |
↓ | OC4_ID | Output Compare 4 |
↓ | IC4_OC5_ID | I4O5 |
↓ | TIMER_OVERFLOW_ID | Timer Overflow |
↓ | PULSE_OVERFLOW_ID | Pulse Accum Overflow |
↓ | PULSE_EDGE_ID | Pulse Accum Edge |
↓ | SPI_ID | SPI Event Interrupt |
Lowest | SCI_ID | SCI Event Interrupt |
Elevated priority
After being recognized, a locally enabled maskable interrupt will be serviced when the I
bit is clear, and when it has the highest priority among the pending interrupts. Note that interrupts are not necessarily serviced in the order in which they are recognized, but in order of priority among those pending.
You can elevate one maskable interrupt at a time to receive highest priority servicing. This is accomplished by configuring four priority-selection bits named PSEL0
, PSEL1
, PSEL2
, and PSEL3
located in the HPRIO
(high priority) register (MC68HC11F1 Technical Data Manual, p.5-7). The default highest priority maskable interrupt is /IRQ
. A table in MC68HC11F1 Technical Data Manual, p.5-8 lists the states of the priority selection bits needed to elevate an interrupt’s status to the highest priority.
Interrupt flag and mask bits
Each maskable interrupt is enabled and disabled by a local mask bit. An interrupt is enabled when its local mask bit is set. When an interrupt’s trigger event occurs, the processor sets the interrupt’s flag bit.
The local mask bit should be used to enable and disable individual interrupts. In general, you should avoid setting the global I bit in the condition code register (CCR
) using DISABLE_INTERRUPTS() unless you are sure that you want to disable all interrupts. Time-critical interrupt service routines such as the timesliced multitasker cannot perform their functions when interrupts are globally disabled.
Some of the QCard Controller’s library functions globally disable interrupts for short periods to facilitate multitasking and access to shared resources. A list of these functions is presented in the Control-C Glossary document.
Interrupt trigger events can occur whether or not the interrupt is enabled. For this reason, it is common for flag bits to be set before an interrupt is ready to be used. Unless an interrupt’s flag bit is cleared before it is enabled, setting the local mask bit will force the system to recognize an interrupt immediately. Unfortunately, the event which set the interrupt’s flag bit occurred at an unknown time before the interrupt was enabled. Depending on the interrupt handler’s task, this can cause erratic initial behavior, collection of an incorrect initial data point, or begin an improper sequence of events (for example, cause a phase shift in an output waveform). To avoid these problems, it is recommended that you enable an interrupt by first clearing its flag bit and then immediately setting its mask bit.
To clear a specified flag bit, write a pattern to the flag register with a 1 in the bit position of the flag that must be cleared. All of the other flag bits in the flag register then remain unchanged. See M68HC11 Reference Manual Section 10.4.4 for examples.
External hardware interrupts /IRQ and /XIRQ
Two external interrupts, /IRQ (active-low interrupt request) and /XIRQ
(active-low nonmaskable interrupt request) allow external hardware to interrupt the 68HC11F1 (M68HC11 Reference Manual, Section 2.4.6). The / prefix to each of these names indicates that the signals are active-low. Pull-up resistors on the QCard Controller hold these signals high during normal operation, and an interrupt is recognized when either signal is pulled low by an external source. The /IRQ
input is maskable and is not serviced unless the I bit in the condition code register is clear. If the CPU is servicing an interrupt when the /IRQ
line goes low, the external interrupt will not be recognized until the interrupt being serviced has been handled. Unlike all the other maskable interrupts, /IRQ
does not have a local interrupt mask. The /XIRQ
external interrupt is not available on the QCard.
The /IRQ
pin is accessed and controlled via the Wildcard Port Header (for pin locations see Appendix A). It operates as an active-low input to the processor. An external device can drive the line LOW to signal an interrupt. Alternatively, several open-collector devices can be wired together on the same line, so that any one of them can interrupt the processor by pulling the request line low. This is called wired-or operation. In either case, the external device must pull the line low long enough to be detected by the CPU.
Note that the PORTA
input capture lines can also be configured to interrupt the processor when an external event occurs.
Configuring /IRQ interrupts
In its default state, after each reset or restart, the /IRQ
pin is configured as an edge-triggered input. In this mode, the 68HC11 latches the falling edge, causing an interrupt to be recognized. This frees peripheral devices from having to hold the /IRQ
line low until the CPU senses the interrupt, and prevents multiple servicing of a single external event.
The disadvantage of this configuration is that multiple edge-triggered interrupts cannot be reliably detected when used with wired-OR interrupt sources. If you are using multiple wire-or /IRQ
inputs, you can specify level-sensitive interrupt recognition by clearing a bit named IRQE
(IRQ
edge-sensitive) in the OPTION register (M68HC11 Reference Manual, Section 5.8.1). IRQE
is a "protected bit" in OPTION that must be written within the first 64 E cycles after a reset. The QED-Forth word InstallRegisterInits() (described in the glossary) may be used to specify a value that is automatically stored into OPTION
upon each reset.
Using /IRQ
To use the /IRQ
external interrupt, define an interrupt handler and install it using the pre-defined identifier IRQ.ID and the interrupt Attach() utility, as described in the Glossary entry for Attach().
If interrupts have not yet been enabled globally, then execute:
ENABLE_INTERRUPTS
Whenever /IRQ
is pulled low, your interrupt handler will be executed. Note that there is no local interrupt mask for the /IRQ
interrupt, so your interrupt handler routine need not clear an interrupt request flag.
Routines that temporarily disable interrupts
Certain kernel routines temporarily disable interrupts by setting the I
bit in the condition code register. These routines are summarized in,
A review of that list will assist you in planning the time-critical aspects of your application.
Interrupt latency
The time required between the processor’s initiation of interrupt servicing and the execution of the first byte of the specified service routine is called the interrupt latency. Most of the 68HC11’s interrupts have an inherent latency of 12 machine cycles during which the registers are saved on the return stack and the interrupt vector is fetched. This corresponds to 3 microseconds (µs). QED-Forth’s interrupt latency is longer because the interrupts are re-vectored via the EEPROM to allow the programmer to modify the vectors, and because the page must be changed. The latency of service routines installed with ATTACH() is 34 machine cycles, or 8.5 µs. That is, the first opcode of the user’s service routine is executed 8.5 µs after interrupt service begins. After the service routine’s concluding RTS
executes, an additional 20 cycles (5 µs) lapses before the originally interrupted program resumes execution. 12 of these cycles are accounted for by the RTI
instruction, and the other 8 cycles are required to restore the original page.
Time to Leave an Interrupt Service Routine: 5.0 µsec
Writing interrupt service routines
Maskable interrupts have a local mask bit which enables and disables the interrupt, and a flag bit which is set when a trigger event occurs. For maskable interrupts, an interrupt triggering event is recognized when the flag and mask bits are both set. In order to avoid premature recognition of a maskable interrupt, it should be enabled by first clearing its flag bit and then setting its mask bit. Once an interrupt has been recognized, it will be serviced if it is not masked by the I
bit in the CCR
. Multiple pending interrupts are serviced in the order determined by their priority. Interrupts have a fixed priority, except that the programmer may elevate one interrupt to have the highest priority. Nonmaskable interrupts always have the highest priority when they are recognized, and are immediately serviced.
When an interrupt is serviced, the machine state (specified by the programming registers) is saved and the I
bit in the CCR
register is set. This prevents other pending interrupts from being serviced. The CPU then executes the appropriate interrupt handler routine. The interrupt handler is responsible for clearing the interrupt flag bit. For most interrupts this is accomplished by writing a one to the flag bit. After completing its tasks, the interrupt handler executes an RTI
instruction to restore the machine state, subsequently clearing the I
bit in the CCR
. The CPU is now ready to service the next, highest priority, pending interrupt. If there is none, processing of the main program continues.
To use interrupts you need to create and post an interrupt service routine using the ATTACH() macro. We’ll look at this process in detail, then discuss how interrupts are implemented on the 68HC11.
To use an interrupt to respond to events, follow these four steps:
- Use
#define
to name all required bit masks related to servicing the interrupt, and look in the Motorola 68HC11F1 documentation and theQEDREGS.H
file (in the\MOSAIC\FABIUS\INCLUDE\MOSAIC
directory) to find the names of all registers that relate to the interrupt. These bit mask and register names will simplify the creation of a readable service routine. - Use C or assembly code to define an interrupt service routine which will be executed every time the interrupt occurs. The function must have a
void
stack picture; it cannot return a value or expect input parameters. This function must reset the interrupt request flag (by writing a 1 to it!) and perform any necessary actions to service the interrupt event. Note that the service routine is a standard function; it is not defined using the_interrupt
keyword. - Write a function that installs the interrupt service routine using the ATTACH() command. ATTACH() initializes the interrupt vector in EEPROM to call the specified service routine, and ATTACH() also supplies the
RTI
(return from interrupt) instruction that correctly terminates the service routine. - Write functions to enable and disable the interrupt. Enabling the interrupt is accomplished by clearing the interrupt’s flag bit by writing a 1 to it, and then setting its mask bit. It may also be necessary to clear the
I
bit in theCCR
to globally enable interrupts. This can be accomplished by executing ENABLE_INTERRUPTS().
ATTACH() an interrupt service routine
It is easy to define an interrupt service routine and ATTACH it to a specified interrupt. You define your service routine in either assembly code or in high level C. Thus the service routine can be debugged just like any other C function. You then call ATTACH() to bind the service routine to the interrupt.
The following constants have been defined as identifiers for the 68HC11 interrupts in the INTERUPT.H
file in the \MOSAIC\FABIUS\INCLUDE\MOSAIC
directory:
68HC11 Interrupt IDs | |
---|---|
Identifier | Interrupt description |
SCI_ID | Serial communications interface |
SPI_ID | Serial peripheral interface |
PULSE_EDGE_ID | Pulse accumulator edge detection |
PULSE_OVERFLOW_ID | Pulse accumulator overflow |
TIMER_OVERFLOW_ID | Timer overflow |
IC4_OC5_ID | Timer input capture 4/output compare 5 |
OC4_ID | Timer output compare 4 |
OC3_ID | Timer output compare 3 |
OC2_ID | Timer output compare 2 |
OC1_ID | Timer output compare 1 |
IC3_ID | Timer input capture 3 |
IC2_ID | Timer input capture 2 |
IC1_ID | Timer input capture 1 |
RTI_ID | Real-time interrupt |
IRQ_ID | IRQ external pin |
XIRQ_ID | IRQ external pin (pseudo-nonmaskable) |
SWI_ID | Software interrupt |
ILLEGAL_OPCODE_ID | Illegal opcode trap |
COP_ID | COP failure (reset) |
CLOCK_MONITOR_ID | Clock monitor failure (reset) |
The ATTACH() macro expects as inputs a function pointer to your service routine, and an interrupt identifier. It sets up the interrupt vector in EEPROM so that subsequent interrupts will execute the specified service routine. The code installed by ATTACH() includes the RTI
instruction that terminates the interrupt service sequence. The StartFunctionTimer()
routine presented earlier shows how ATTACH() is called.
Implementation details
The interrupt vectors near the top of memory are in ROM; locations that cannot be modified by the programmer. The contents of these locations point to a series of locations in the EEPROM (at AE20-AEBFH
) which can be modified and, if desired, write-protected using the BPROT
register. ATTACH() writes some code at the EEPROM locations corresponding to the specified interrupt. This code loads the code field address of the user’s service function into registers and jumps to a routine that saves the current page, changes the page to that of the user’s service function, and calls the service function as a subroutine. When the user-defined service function returns, the code installed by ATTACH() restores the original page and executes RTI
(return from interrupt) to complete the interrupt service process. This calling scheme ensures that the interrupt service will be properly called no matter which page the processor is operating in when the interrupt occurs. And because the interrupt calling routine which is installed by ATTACH()
ends with an RTI
, your service routine can end with a standard RTS
, return or }
which makes debugging much easier.
The following sections explain how to define and install interrupt service routines that enhance the usefulness of many of the 68HC11’s hardware features.
The following example illustrates how to write and use an interrupt service routine.
An example: periodically calling a specified function
Many times a program needs to execute a specified action every X milliseconds, where X is a specified time increment. We can use an output compare interrupt to accomplish this. We’ll set up an interrupt service routine that executes once per millisecond (ms), and maintains a ms_counter
variable that is incremented every millisecond. The variable time_period
specifies the time increment, in ms, between calls to the specified function which is named TheFunction()
. For this simple example, TheFunction()
inverts the contents of the static variable named action_variable
.
TIMEKEEP.C, An Example of an Interrupt Service Routine
1: #define OC3_MASK 0x20 // used to set/clear OC3 interrupt flag and mask 2: #define ONE_MS 500 // 500 counts of 2us TCNT = 1 ms 3: #define DEFAULT_TIME_PERIOD 1000 // Execute once per second 4: 5: static int ms_counter; // runs from 0 to 65535 before rolling over 6: static uint time_period; // specifies time in ms between function calls 7: static int next_execution_time; // next scheduled value of ms_counter 8: static int action_variable; // state is toggled by TheFunction() 9: 10: _Q void TheFunction(void) // the function simply complements 11: { action_variable = !action_variable; // a variable 12: } 13: 14: _Q void FunctionTimer(void) 15: // This interrupt service routine is called by an OC3-based clock interrupt and 16: // simply calls TheFunction periodically. 17: { ms_counter++; 18: if(ms_counter == next_execution_time) 19: { TheFunction(); 20: next_execution_time = next_execution_time + time_period; 21: } 22: TOC3 += ONE_MS; // set OC3 count for next interrupt in 1 ms 23: TFLG1 = OC3_MASK; // reset the oc3 interrupt flag by writing a 1 24: } 25: 26: _Q void StopFunctionTimer(void) 27: { TMSK1 &= ~OC3_MASK; // clear OC3I to locally disable OC3 28: } 29: 30: _Q void StartFunctionTimer(void) 31: // inits variables and locally enables OC3 interrupt; 32: // does not globally enable interrupts! 33: { StopFunctionTimer(); // locally disable OC3 while we set it up 34: ATTACH(FunctionTimer, OC3_ID); // post the interrupt service routine 35: ms_counter = 0; 36: time_period = next_execution_time = DEFAULT_TIME_PERIOD; // 1/second 37: action_variable = 0; // state is toggled by TheFunction() 38: TOC3 = TCNT + ONE_MS; // start after a 1 ms delay 39: TFLG1 = OC3_MASK; // clear interrupt flag OC3F 40: TMSK1 |= OC3_MASK; // set OC3I to locally enable OC3 41: }
In this program, we define a bit mask named OC3_MASK
which has bit 5 set and all other bits clear. From inspection of the register summary in the Motorola 68HC11F1 booklet, we see that this mask isolates the Output Compare 3 (OC3
) mask bit in the TMSK1
register, and isolates the OC3
interrupt flag bit in the TFLG1
register. The other relevant registers are the 16 bit free-running counter register named TCNT
which increments every 2 microseconds, and the Timer Output Compare 3 register named TOC3
. If the OC3
interrupt is enabled by setting its mask bit = 1 in TMSK1
and by globally enabling interrupts using ENABLE_INTERRUPTS() or the assembly instruction CLI
, then an interrupt occurs when the count in TCNT
matches the count in TOC3
. Thus we can control when the next interrupt occurs by writing a specified count to TOC3
.
TheFunction()
is our prototypical function that simply toggles the action_variable
between the values 0 and 1. The goal of our interrupt service routine is to call TheFunction()
exactly once per second.
FunctionTimer()
is the OC3
interrupt service routine. It increments the ms_counter
variable, and checks if ms_counter
equals next_execution_time
. If so, it calls TheFunction()
and updates next_execution_time
by adding time_period
to it. Then FunctionTimer()
increments the contents of the TOC3
register to set up the next interrupt in 1 ms, and clears the interrupt request flag by writing a 1 to the OC3
flag bit in the TFLG1
register. Note that FunctionTimer()
does not have any input parameters or a return value. Moreover, it is not defined using the _interrupt
keyword which would insert an RTI
(return from interrupt) instruction at the end of the function. Rather, ATTACH() will supply the RTI
instruction for us. Because FunctionTimer()
does not end with an RTI
, we can easily test the FunctionTimer()
service routine using our standard interactive debugging techniques.
StopFunctionTimer()
simply clears the local OC3
interrupt mask bit in the TMSK1
register to disable the OC3
interrupt.
StartFunctionTimer()
first locally disables OC3
to prevent an interrupt while the service routine is being posted. Then it calls:
ATTACH(FunctionTimer, OC3_ID);
to ensure that FunctionTimer()
is called every time the OC3
interrupt occurs; ATTACH() also installs a return sequence that supplies the required RTI
(return from interrupt) opcode. ATTACH() is described in detail later in this chapter. Note that its input parameters are a pointer to the interrupt service routine FunctionTimer
, and a pre-defined constant named OC3_ID
that identifies the interrupt. All of the interrupt identifier constants are summarized in Table 6-3.
After calling ATTACH(), StartFunctionTimer()
initializes the timing variables, and initializes TOC3
so that the first interrupt will occur in 1 ms. It then clears the interrupt flag by writing a 1
to the OC3F
flag bit in the TFLG1
register using the statement:
TFLG1 = OC3_MASK;
Clearing the interrupt flag bit before enabling the interrupt is a highly recommended procedure that ensures that all prior pending OC3
interrupts are cleared before the interrupt occurs. Finally, StartFunctionTimer()
locally enables the OC3
interrupt by setting the mask bit in TMSK1
with the statement:
TMSK1 <html>|</html>= OC3_MASK;
To start the interrupt, main() simply calls StartFunctionTimer()
followed by ENABLE_INTERRUPTS()
. After you compile and download the TIMEKEEP.C
program and type:
main↓
from your terminal, the OC3
interrupt is running in the background. To monitor the state of the action_variable
, interactively type at your terminal:
See( )↓
and you will see the variable’s value change from 0 to 1 exactly once per second. Type any key to terminate the See( )
function.
This short program provides a template that shows how a function can be periodically called with a period specified by the variable time_period
. Of course, in your application the called function would perform a more useful action than does TheFunction()
in this simple example. You could make other enhancements; for example, a foreground task could manipulate the contents of time_period
to change the frequency at which TheFunction()
is called, and you could use ms_counter
to measure elapsed time with 1 ms resolution.
Note that, to maintain timing accuracy, the interrupt service routine should have a worst-case execution time of under 2 ms; otherwise the FunctionTimer()
will miss the interrupt when TCNT
matches TOC3
, and an extra delay of 131 ms will occur while the TCNT
timer rolls over. In general, interrupt service routines should be short and simple. Complex calculations should be done by foreground tasks, and the interrupt routines should perform the minimum actions necessary to service the time-critical events. An example of this approach is presented in the Turnkeyed Application Program.
Cautions and restrictions
Note that the OC2 interrupt is used as the multitasker’s timeslice clock. Before using this interrupt for another purpose, make sure that you don’t need the services provided by the timeslicer which supports the multitasking executive and the elapsed time clock.
The main restriction on interrupt service routines is that they must not call _forth
(kernel) library functions unless the instructions in the next section are followed. The Glossary document and the header files in the \MOSAIC\FABIUS\INCLUDE\MOSAIC
directory specify which functions are of the _forth
type.
Calling kernel functions from within ISRs
There is a special consideration when calling _forth
library functions from interrupt service routines. Fortunately, this restriction can be overcome by simply including the FORTHIRQ.C
file with your source code, and following the simple example presented in the file. FORTHIRQ.C
is present in the \MOSAIC\DEMOS_AND_DRIVERS\MISC\C EXAMPLES
directory.
The method is very simple: just place a call to the function
BeginForthInterrupt();
at the top of your interrupt service routine (or, at the minimum, before any _forth
functions are called). Before the final exit point of the interrupt service routine, place a call to the function
EndForthInterrupt();
That's all there is to it. The ability to call _forth
library functions from interrupt service routines makes it easier to manage page-mapped I/O devices on an event-driven basis.
Summary
Using interrupts requires:
- coding an interrupt service routine;
- using ATTACH() to bind it to the appropriate interrupt; and,
- enabling its local interrupt mask.