Interrupt Service Routines
The PDQ Board's MC9S12 processor provides flexible support for numerous hardware- and software-generated interrupts. On this page you'll learn about MC9S12/HCS12 interrupts, and how to use them to respond to external or timing events. Writing interrupt handlers 1) will enable you to write fast, responsive C language application programs for your PDQ Board. ISRs are often essential for high performance real-time control of your application or instrument.
The on-chip resources of the HCS12 MCU are controlled and enhanced by numerous interrupts. These ISR-controlled resources include:
- A 16-channel 10-bit A/D converter,
- Pulse-Width Modulated (PWM) outputs on PortP,
- Enhanced Capture/Timer (ECT) I/O on PortT,
- A watchdog timer (COP),
- A clock monitor,
- Maskable and nonmaskable external interrupt pins,
- Real-Time Interrupt (RTI),
- Dual serial communications ports,
- A high speed serial peripheral interface (SPI)
- An inter-IC (IIC) serial bus, and,
- General purpose digital I/O
In some caes several interrupts are associated with a subsystem. For example, the ECT timer system alone provides input captures, output compares, pulse accumulators, a modulus down-counter, and more. Dozens of interrupts associated with these functions can enhance the real-time performance of the PDQ Controller. Interrupts allow rapid response to time-critical events that often occur in measurement and control applications.
Control of these I/O and timing functions is simplified by built-in drivers that are described in this User Guide. The processor's interrupt sources which can be used by the programmer are named with constants declared in the INTERRUPT.h
file in the \INCLUDE\MOSAIC
directory. (The reset, FLASH and EEPROM interrupt identifiers are not named, because the operating system deals with these). Examples of these identifiers include ECT1_ID, PULSE_EDGE_ID
, IIC_ID, etc. These identifiers are passed to the ATTACH() routine as described below to post an interrupt handler to respond to the named interrupt.
What is an interrupt handler (interrupt service routine)?
An interrupt is a hardware or software signal to the MCU's processor indicating an event that requires a timely response. The event is usually a high-priority condition that warrants interrupting the processor and temporarily suspending the current code being executed. The processor should respond by stopping the currently executing code, saving its state, and executing an interrupt service routine (ISR) to handle the event. After the interrupt handler finishes its job, the processor restores the state of the previously executing code and resumes its execution, right where it left off.
The tutorials of this page assume you are familiar with the basics of interrupt processing. To brush up on interrupts you might want to consult the Wikipedia page on interrupts.
Interrupt recognition and servicing
The MC9S12 processor includes an interrupt processing block, called INT, for decoding and processing interrupt requests. Figure 12) is a block diagram of the internal workings of the INT block:
Referring to Figure 1, Freescale's documentation explains that the interrupt sub-block decodes the priority of system exception requests and provides the applicable vector for processing the exception. This interrupt hardware decodes both I-bit maskable and X-bit maskable interrupts, a nonmaskable Unimplemented Opcode Trap, a nonmaskable software interrupt (SWI) or Background Debug Mode request, and three system reset vector requests. All interrupt related exception requests are handled by the hardware interrupt block shown in the figure.
But you don't need to understand the internal processing of interrupts to use them effectively, even at the block diagram level of Figure 1. If you're interested in the hardware and timing details of internal interrupt processing you can consult Freescale's description: Freescale MC9S12 Interrupt (INT) Module V1.
What you do need to understand is that the processor's interrupts fall into two main categories: maskable and nonmaskable. The following sections show you how to use each.
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 (or handled) 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 current interrupt handler 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). Nonmaskable interrupts (reset, clock monitor failure, COP failure, illegal opcode trap, 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. 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, re-enables interrupts by clearing the I bit in the CCR (condition code register), and resumes execution of the previously executing program.
In most cases, the interrupt handler must reset the interrupt flag bit (not the mask bit) by writing a 1 to it. (If the "fast clear" mode of a hardware subsystem such as the ECT timer or ATD converter is enabled, then the interrupt flag bit is reset automatically when an associated hardware register is accessed). The interrupt handler must perform any tasks necessary to service the interrupt before exiting. When the interrupt handler has finished, normal program flow resumes, and any other pending interrupts can be serviced.
Nonmaskable Interrupts
Six of the processor's 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, 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. The operating system 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.
Nonmaskable Interrupts | |
---|---|
Interrupt Name IRQ Identifier | Description |
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 operating system restart sequence. You can not install a custom ISR to change this behavior, but you can install a custom AUTOSTART routine to be executed after the reset. |
Clock Monitor Failure CLOCK_MONITOR_ID | Enabled or disabled via the CME (clock monitor enable) bit in the PLLCTL register, this interrupt is recognized if the external oscillator frequency drops below 200 kHz. It resets the processor hardware and executes a user-defined handler. QED-Forth installs a default service routine for this interrupt that performs the standard restart sequence. |
COP Failure COP_ID | 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. The operating system installs a default service routine for this interrupt that performs the standard restart sequence. |
Illegal Opcode Trap TRAP_ID | This interrupt occurs when the processor encounters an unknown opcode. The operating system installs a default service routine for this interrupt that performs the standard restart sequence. |
SWI SWI_ID | 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. |
/XIRQ XIRQ_ID | 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. |
The interrupt handler 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 entries for InitVitalIRQsOnCold() and NoVitalIRQInit() 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. 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 typically clears the interrupt flag bit set by the trigger event and performs any tasks necessary to service the interrupt. The service routine that you post terminates with a standard RTS or RTC opcode as do all other functions; the ATTACH() routine supplies the required 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 are not recommended
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 31 available maskable interrupts in order of highest to lowest priority. Each entry lists the following:
- IRQ Identifier – the constant identifier (defined in the
INTERRUPT.h
file) that can be passed to ATTACH() to post an interrupt handler; - Enable Reg – the register name and bitname of the local interrupt mask;
- HIPRIO – the value that must be stored into the HIPRIO register to elevate this interrupt to have the highest priority; and,
- Description – a description of the interrupt.
Maskable Interrupts, from Highest to Lowest Priority | |||
---|---|---|---|
IRQ Identifier | Enable Reg (bitname) | HIPRIO | Description |
ATD0_ID | ATD0CTL2 (ASCIE) | 0xD2 | An interrupt is recognized when the requested Analog-To-Digital subsystem 0 (pins AN0-7) sequence has completed. |
ATD1_ID | ATD1CTL2 (ASCIE) | 0xD0 | An interrupt is recognized when the requested Analog-To-Digital subsystem 1 (pins AN8-15) sequence has completed. |
COUNTER_UNDERFLOW_ID | MCCTL (MCZI) | 0xCA | A Modulus Counter underflow interrupt is recognized when the 16-bit MCCNT modulus down-counter register underflows. |
ECT_OVERFLOW_ID | TSRC2 (TOF) | 0xDE | A timer overflow interrupt is recognized when TCNT rolls over from 0xFFFF to 0x0000. |
ECT0_ID | TIE (C0I) | 0xEE | An IC interrupt is recognized when a specified signal transition is sensed on PortT0; an OC interrupt is recognized when TCNT becomes equal to the TC0 timer compare register. |
ECT1_ID | TIE (C1I) | 0xEC | An IC interrupt is recognized when a specified signal transition is sensed on PortT1; an OC interrupt is recognized when TCNT becomes equal to the TC1 timer compare register. |
ECT2_ID | TIE (C2I) | 0xEA | An IC interrupt is recognized when a specified signal transition is sensed on PortT2; an OC interrupt is recognized when TCNT becomes equal to the TC2 timer compare register. |
ECT3_ID | TIE (C3I) | 0xE8 | An IC interrupt is recognized when a specified signal transition is sensed on PortT3; an OC interrupt is recognized when TCNT becomes equal to the TC3 timer compare register. |
ECT4_ID | TIE (C4I) | 0xE6 | An IC interrupt is recognized when a specified signal transition is sensed on PortT4; an OC interrupt is recognized when TCNT becomes equal to the TC4 timer compare register. |
ECT5_ID | TIE (C5I) | 0xE4 | An IC interrupt is recognized when a specified signal transition is sensed on PortT5; an OC interrupt is recognized when TCNT becomes equal to the TC5 timer compare register. |
ECT6_ID | TIE (C6I) | 0xE2 | An IC interrupt is recognized when a specified signal transition is sensed on PortT6; an OC interrupt is recognized when TCNT becomes equal to the TC6 timer compare register. |
ECT7_ID | TIE (C7I) | 0xE0 | An IC interrupt is recognized when a specified signal transition is sensed on PortT7; an OC interrupt is recognized when TCNT becomes equal to the TC7 timer compare register. |
EEPROM_ID | ECNFG (CCIE, CBEIE) | 0xBA | An interrupt is recognized during programming of the EEPROM on the HCS12 chip. The operating system manages EEPROM programming, so EEPROM_ID`` is not defined. |
FLASH_ID | FCNFG (CCIE, CBEIE) | 0xB8 | An interrupt is recognized during programming of the on-chip Flash memory on the HCS12 chip. The operating system manages Flash programming, so FLASH_ID`` is not defined. |
IIC_ID | IBCR (IBIE) | 0xC0 | An IIC bus interrupt is recognized when bus arbitration is lost, a byte transfer is complete, or the IIC bus controller has been addressed as a slave. |
IRQ_ID | IRQCR (IRQEN) | 0xF2 | IRQ is an active-low external hardware interrupt which is recognized when the signal on the /IRQ pin of the HCS12 is pulled low. |
PORTJ_ID | PIEJ (PIEJx) | 0xCE | An interrupt is recognized when a specified signal edge (configured using the PPSJ register) occurs on the corresponding PortJ pin. |
PORTH_ID | PIEH (PIEHx) | 0xCC | An interrupt is recognized when a specified signal edge (configured using the PPSH register) occurs on the corresponding PortH pin. |
PORTP_ID | PIEP (PIEPx) | 0x8E | An interrupt is recognized when a specified signal edge (configured using the PPSP register) occurs on the corresponding PortP pin. |
PULSE_A_OVERFLOW_ID | PACTL (PAOVI) | 0xDC | A Pulse Accumulator A overflow interrupt is recognized when the 16-bit Pulse Accumulator A overflows from 0xFFFF to 0x0000, or when 8-bit Pulse Accumulator 3 on PT3 overflows from 0xFF to 0x00. |
PULSE_A_EDGE_ID | PACTL (PAI) | 0xDA | A Pulse Accumulator A interrupt is recognized when the selected edge is detected at the PT7 input pin. |
PULSE_B_OVERFLOW_ID | PBCTL (PBOVI) | 0xC8 | A Pulse Accumulator B overflow interrupt is recognized when the 16-bit Pulse Accumulator B overflows from 0xFFFF to 0x0000, or when 8-bit Pulse Accumulator 1 on PT1 overflows from 0xFF to 0x00. |
PWM_SHUTDOWN_ID | PWMSDN (PWM7ENA, PWMIE) | 0x8C | An interrupt is recognized if the PWM emergency shutdown feature is enabled, and a signal edge occurs on a PWM pin. |
PLL_LOCK_ID | CRGINT (LOCKIE) | 0xC6 | An interrupt is recognized when the Phase-Locked Loop clock generator is locked onto the target frequency. |
RTI_ID | CRGINT (RTIE) | 0xF0 | The RTI provides a programmable periodic interrupt |
SCI0_ID | SCI0CR2 (TIE, TCIE, RIE, ILIE) | 0xD6 | An SCI0 (PDQ serial port 1) interrupt is recognized if the transmit data register is empty, or the transmission is complete, or the receive data register is full, or the serial line is idle. |
SCI1_ID | SCI1CR2 (TIE, TCIE, RIE, ILIE) | 0xD4 | An SCI1 (PDQ serial port 2) interrupt is recognized if the transmit data register is empty, or the transmission is complete, or the receive data register is full, or the serial line is idle. |
SELF_CLOCK_ID | CRGINT (SCMIE) | 0xC4 | An interrupt is recognized when the status of the processor clock changes to or from normal to self-clocked mode. |
SPI0_ID | SPI0CR1 (SPIE, SPTIE) | 0xD8 | An SPI channel0 interrupt is recognized after the eighth SCK in a data transfer, or when a mode fault is detected (if /SS input goes low while the SPI channel is configured as a master). |
SPI1_ID | SPI1CR1 (SPIE, SPTIE) | 0xBE | An SPI channel1 interrupt is recognized after the eighth SCK in a data transfer, or when a mode fault is detected (if /SS input goes low while the SPI channel is configured as a master). |
SPI2_ID | SPI2CR1 (SPIE, SPTIE) | 0xBC | An SPI channel2 interrupt is recognized after the eighth SCK in a data transfer, or when a mode fault is detected (if /SS input goes low while the SPI channel is configured as a master). |
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 PDQ Board'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.
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 (by writing a 1 to it) 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.
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 HCS12 processor. The / prefix to each of these names indicates that the signals are active-low. Pull-up resistors on the PDQ Board 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 pin is typically not available on the PDQ Board; contact Mosaic Industries if you need to access this pin.
The /IRQ pin is accessed and controlled via the Wildcard Port Headers (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 PORTH, PORTJ, PORTP, and PORTT pins can also be configured to interrupt the processor when an external event occurs.
Configuring the /IRQ interrupt
In its default state, after each reset or restart, the /IRQ pin is configured as an edge-triggered input. In this mode, the HCS12 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) which is bit 7 in the IRQCR register. IRQE is a "write once bit"; after you write to it one time, subsequent writes have no effect until a hardware reset occurs. To enable level-sensitive /IRQ operation, write 0x40 to the IRQCR register. No modification of the IRQCR register is needed if the default edge-sensitive /IRQ operation is acceptable.
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 C Glossary. If interrupts have not yet been enabled globally, then execute ENABLE_INTERRUPTS. Whenever the /IRQ pin 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 the Functions that disable interrupts section of the C Glossary. A review of that list will assist you in planning the time-critical aspects of your application.
How to temporarily disable interrupts in functions
In multitasking applications, it is sometimes necessary to disable interrupts for brief periods. For example, let's assume that a 32-bit floating point variable is written to and read by several different tasks, and that the timeslice multitasker (based on the RTI interrupt) is running. We want to avoid the possibility of reading a timeslice interrupt occurring between the accesses of the first and second 16-bit portions of this floating point value, as this could lead to a corrupted access.
The C keyword functions lock()
and restore()
solve this problem. Inside the function where you need to disable interrupts, declare inside the function an unsigned integer (uint
) local variable, and assign to it the return value of the lock()
function. The lock()
function retrieves and returns the prior condition code register (CCR) contents that contain the I-bit which determines whether interrupts are globally disabled. Then the lock()
function globally disables interrupts by setting the I bit. Place the function lines that are to operate while interrupts are disabled after the lock() function invocation. To restore interrupts to their prior state, pass the local variable that holds the lock() return value to the restore() function.
The TURNKEY.c
program file discussed in the "A Turnkeyed C Application Program" chapter contains two examples of this approach. One is presented in the following code listing for a function that temporarily disables interrupts, writes to a floating point variable, and restores interrupts to their prior state (enabled or disabled).
_Q float PeekFloatUninterrupted( float* source ) // fetches and returns the contents from the address specified by source; // the lock() and unlock() keywords guarantee that // interrupts are disabled while the fetch operation occurs; // this prevents corruption of data when two tasks are accessing the same // 4-byte variables { float source_contents; // declare local variables uint prior_ccr_state; prior_ccr_state = lock(); // save prior I-bit, disable irqs source_contents = *source; // fetch the value while irqs are disabled unlock( prior_ccr_state ); // restore prior I-bit state return source_contents; }
The lock()
and restore()
functions are not in the C glossary because they are reserved keywords defined in the GNU C compiler source. Do not confuse the lower case restore()
function described here with the QED-Forth operating system/debugger RESTORE function which restores memory map parameters.
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. This latency includes the time required to re-vector the service request via the EEPROM to allow the programmer to modify the vectors, and to change the page. The latency of service routines installed with ATTACH() is 75 machine cycles, or 3.75 µs. That is, the first opcode of the user's service routine is executed 3.75 µs after interrupt service begins. After the service routine's concluding RTS executes, an additional 56 cycles (2.8 µs) lapses before the originally interrupted program resumes execution.
Time to Leave an Interrupt Service Routine: 2.8 µsec
Writing interrupt handlers (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 (automatically compiled by ATTACH()) 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 HCS12.
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 documentation and theHCS12REGS.h
file (in theC:\MosaicPlus\c\libraries\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. In most cases, 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, but it must be followed by
MAKE_ISR(functionName)
. This call to MAKE_ISR() must be in the global scope (not inside any function) and should come directly after the closing}
of the interrupt service routine. - 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 the
CCR
to globally enable interrupts. This can be accomplished by executing ENABLE_INTERRUPTS().
ATTACH() an interrupt service routine
It is easy to define an interrupt handler and ATTACH it to a specified interrupt. You define your service routine in either assembly code or in high level C. After the closing bracket of your function, call MAKE_ISR() with the function name as the single paramater. You then call ATTACH() to bind the service routine to the interrupt. The ATTACH() macro expects as inputs a function pointer to your service routine, and an interrupt identifier as defined in the C:\MosaicPlus\c\libraries\include\mosaic\interrupt.h
file. The interrupt identifiers are listed in table above. ATTACH() 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.
timekeep.c:137: error: 'FunctionTimer_ISR_xaddr' undeclared
Implementation details
The interrupt vectors near the top of memory are in write-protected on-chip kernel flash, locations that cannot be modified by the programmer. The contents of these locations point to a series of locations in the EEPROM which can be modified via the ATTACH() routine. 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 RTC, return (in assembly code) or }
(in high-level C) which makes debugging much easier.
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
once per second.
Adapted From TIMEKEEP.C, An Example of an Interrupt Service Routine
1: // ********** Interrupt Service Routine for Timed Function Calling ********** 2: 3: // The default prescaler set at reset in TSCR2 is decimal 32 (101), 4: // resulting in a free-running TCNT with 1.6us period and a 104.8ms rollover time. 5: // Available prescales are 1 through 128 in powers of 2 (0.05us - 6.4us); 6: // see ECTPrescaler(). 7: // Note that TCNT is not currently used by any kernel or operating system routines; 8: // the timeslicer of the V6.xx kernel uses the RTI (real-time interrupt). 9: 10: #include <mosaic\allqed.h> 11: 12: // #define OC3_MASK 0x08 // could be used to set/clear OC3 irq flag and mask, 13: // but we use high level functions to do this. 14: // simply by passing the argument 3 to indicate OC3 15: #define ONE_MS 625 // 625 counts of 1.6us TCNT = 1 ms 16: #define DEFAULT_TIME_PERIOD 1000 // Execute TheFunction() once per second 17: // (that is, every 1000 milliseconds) 18: 19: int ms_counter = 0; // runs from 0 to 65,535 before rolling over 20: 21: unsigned int time_period = DEFAULT_TIME_PERIOD; 22: // specifies time in ms between calls to TheFunction() 23: // making time_period a variable allows you to change it interactively 24: 25: int next_execution_time = DEFAULT_TIME_PERIOD; 26: // value of ms_counter when TheFunction() is scheduled to be called next 27: 28: int action_variable = 0; // state is toggled by TheFunction() 29: 30: _Q void TheFunction( void ) 31: { 32: action_variable = !action_variable; 33: } 34: 35: _Q void StopFunctionTimer( void ) 36: { 37: ECTInterruptDisable( 3 ); // locally disable OC3, same as: TIE &= ~OC3_MASK; 38: } 39: 40: _Q void FunctionTimer( void ) 41: // OC3-based clock, calls TheFunction() periodically 42: { 43: if( ++ms_counter == next_execution_time ) 44: { 45: TheFunction(); 46: next_execution_time += time_period; 47: } 48: 49: TC3 += ONE_MS; // set OC3 count for next interrupt in 1 ms. 50: // A slower way to do this is: OCRegWrite( TCNT+ONE_MS, 3 ); 51: // Because we executed ECTFastClear(), this access to TC3 52: // automatically resets the OC3 interrupt flag 53: // so that new OC3 interrupts will be recognized. 54: } 55: 56: 57: // The next line is crucial to the operation of the interrupt. 58: MAKE_ISR( FunctionTimer ); // Make the function into an ISR 59: 60: 61: _Q void StartFunctionTimer( void ) 62: // inits variables and locally enables OC3 interrupt; 63: // does not globally enable interrupts! 64: { 65: StopFunctionTimer(); // locally disable OC3 while we set it up 66: ATTACH( FunctionTimer, ECT3_ID ); // post the interrupt service routine 67: ECTFastClear(); 68: ms_counter = 0; 69: time_period = next_execution_time = DEFAULT_TIME_PERIOD; // once per second 70: action_variable = 0; // state is toggled by TheFunction() 71: OCAction( OC_NO_ACTION, 3 ); // confirm that no automatic pin action occurs on PT3 72: OutputCompare( 3 ); // set channel 3 as output compare 73: OCRegWrite( TCNT + ONE_MS, 3 ); // starts in 1 ms, same as: TC3 = TCNT + ONE_MS; 74: ECTClearInterruptFlag( 3 ); // clear flag, same as: TFLG1 = OC3_MASK; 75: ECTInterruptEnable( 3 ); // locally enable OC3, same as: TMSK1 |= OC3_MASK; 76: } 77: 78: _Q void See( void ) 79: // a diagnostic routine that lets us monitor action_variable 80: // in real time from the interactive monitor while 81: // the interrupt routine updates action_variable 82: { 83: int prior_action = 0; 84: Emit( '\n' ); // start on new line 85: while( AskKey() == 0 ) // stop when any key is pressed 86: { 87: MicrosecDelay( 50000 ); // delay in this loop slightly reduces power consumption 88: // if other tasks were running we would call Pause() instead 89: if( prior_action != action_variable ) // print when it changes 90: { 91: prior_action = action_variable; 92: iprintf( "%d\n", action_variable ); 93: } 94: } 95: // read the key the user pressed, so it is not left 96: Key(); // in the serial port buffer 97: } 98: 99: int main( void ) 100: { 101: // Disable libc output buffering, which causes unexpected behavior on embedded systems. 102: // If I/O buffering would benefit your application, see the Queued Serial demo. 103: setbuf( stdout, NULL ); 104: 105: // StartFunctionTimer() must be called *before* interrupts are enabled. 106: // This initializes the interrupt handler, and without it running main 107: // after a software reset would cause the interrupt to be triggered 108: // immediately without a handler installed when interrupts are enabled. 109: StartFunctionTimer(); // enable OC3 interrupt to call TheFunction() each second 110: ENABLE_INTERRUPTS(); // globally enable interrupts to actually begin counting 111: 112: return 0; 113: }
Because a full suite of device driver functions for the ECT (Enhanced Capture Timer) system is included in the operating system, you don’t have to research all the relevant registers and define lots of bitmask constants. Rather, you can let the driver functions (described in detail in a later chapter) do the work for you.
This program uses the OC3 (Output Compare 3) interrupt to perform the timing. Output compare 3 can generate an interrupt when the value of the free-running TCNT
register matches the value in the TC3 register. This program configures OC3 to generate an interrupt every millisecond (ms), and to call TheFunction() every 1000 ms. While OC3 can optionally control PortT pin 3, this program disables pin control by specifying OC_NO_ACTION in the call to the OCAction() function in StartFunctionTimer()
.
The free-running TCNT
timer has a default period of 1.6 microseconds; this can be changed using the ECTPrescaler() function. Using the default, we define the ONE_MS
constant equal to 625, because 625 times 1.6 microseconds equals one millisecond. We define the DEFAULT_TIME_PERIOD
constant as 1000 milliseconds so that TheFunction()
will be invoked once per second.
The ms_counter
variable is incremented once per millisecond, and we define a time_period
variable so you can change it interactively using the interactive debugger commands described in prior chapters. The next_execution_time
variable keeps track of when the next 1000 millisecond count will be reached. The action_variable
is toggled by TheFunction()
, and can be examined using the interactive debugger or the See()
function as defined in the TIMEKEEP.c
file.
The FunctionTimer()
routine is the interrupt service routine for the OC3 interrupt posted by ATTACH(). When called each millisecond, it increments the ms_counter
and, if 1000 counts have elapsed, calls TheFunction()
and updates the next_execution_time
variable. The FunctionTimer()
routine then adds the ONE_MS
constant to the TC3 register so that the next interrupt will occur in 1 ms. Because the StartFunctionTimer()
initialization function invokes ECTFastClear(), the FunctionTimer()
interrupt service routine does not have to explicitly clear the interrupt flag bit. Rather, the HCS12 hardware automatically clears the flag when the TC3 register is accessed by the interrupt service routine.
StartFunctionTimer()
initializes the program. It first locally disables the OC3 interrupt by calling StopFunctionTimer()
so there are no unwanted interrupts during the setup process. It passes a pointer to the FunctionTimer
function, and the OC3_ID
interrupt specifier to ATTACH() to post the service routine. It calls ECTFastClear() so that the service routine does not have to explicitly clear the interrupt flag bit by writing a 1 to it. After initializing the variables, StartFunctionTimer()
invokes OCAction() and OutputCompare() to configure ECT channel 3 as an output compare that does not control its port pin. To set the first OC3 interrupt to occur in 1 ms, it calls OCRegWrite(), passing the current value of TCNT
plus the ONE_MS
constant as the value to be stored in the TC3
register. StartFunctionTimer()
concludes by clearing the OC3 interrupt flag (to avoid an immediate interrupt), and locally enabling the interrupt mask by calling ECTInterruptEnable(3)
.
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 TC3
, and an extra delay of 105 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 A Turnkeyed C Application Program chapter.
Cautions and restrictions
Note that the RTI real-time 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.
"Blocking" functions which call the Pause() (task-switch) function should not be called from within an interrupt service routine. This would cause interrupts to remain disabled while waiting and pausing, which typically plays havoc with real-time applications. The C Glossary includes a list of functions that call Pause().
Unlike prior HC11-based kernels, the PDQ HCS12 operating system does not restrict the use of kernel functions in interrupt service routines.
Summary
Using interrupts properly is essential for writing real-time, event-driven C language application programs. Using interrupts requires three steps:
- coding an interrupt service routine;
- using
ATTACH()
to bind it to the appropriate interrupt; and, - enabling its local interrupt mask.
See also →