Link here

Real Time Programming

How to write multitasking and interrupt-driven C-language programs

This chapter provides an introduction to real time programming. You’ll learn:

  • About the timeslice clock and how to use it;
  • All about interrupts, interrupt service routines, and how to use them to respond to events.

The QScreen Controller combines an embedded computer based on the 68HC11 microcontroller with a touch panel and LCD (liquid crystal display) graphic user interface (GUI) that is ideal for instrument control and automation. A built-in multitasking RTOS (real time operating system) simplifies the design of time-aware and event-driven programming for instrument controllers.

 

The timeslicer and task switching

The built-in elapsed time clock

The QScreen Controller’s multitasking executive maintains an elapsed time clock whenever the timeslicer is active. Please consult the TIMEKEEP.C program in the \MOSAIC\DEMOS_AND_DRIVERS\MISC\C EXAMPLES directory for examples of using the elapsed time clock.

Your program can start the timeslice clock by calling the function:

StartTimeslicer()

The timeslicer increments the long variable named TIMESLICE_COUNT each timeslice period. The default timeslice period is 5 milliseconds (ms), and this can be modified by calling

ChangeTaskerPeriod() 

as described in the Control-C Glossary. The function

InitElapsedTime()

sets TIMESLICE_COUNT equal to zero. The function

ReadElapsedSeconds()

returns a long result representing the number of elapsed seconds since InitElapsedTime() was called.

To attain the full 5 millisecond resolution of the elapsed time counter, we can write a simple function that converts the TIMESLICE_COUNT into elapsed seconds as well as the number of milliseconds since the last integral second. For example, let’s examine some code from the TIMEKEEP.C file in the \MOSAIC\DEMOS_AND_DRIVERS\MISC\C EXAMPLES directory:

#define DEFAULT_TIMESLICE_PERIOD 5 // {ms}; system default
#define MS_PER_SECOND 1000
static long start_time; // saves starting count of TIMESLICE_COUNT
_Q void MarkTime(void)
{ start_time = TIMESLICE_COUNT;
}
_Q void PrintElapsedTime(void)
{ long elapsed_ms =
DEFAULT_TIMESLICE_PERIOD*(TIMESLICE_COUNT - start_time);
long seconds = elapsed_ms / MS_PER_SECOND;
int ms_after_second = elapsed_ms % MS_PER_SECOND;
printf("\nTime since mark is: %ld seconds and %d ms.\n",
seconds, ms_after_second);
}

The MarkTime() function simply stores the TIMESLICE_COUNT in the start_time variable. The timeslicer is continually incrementing its counter, and when you later call PrintElapsedTime(), start_time is subtracted from the latest TIMESLICE_COUNT and multiplied by the timeslice period to calculate the elapsed number of milliseconds. This is converted into the elapsed seconds by dividing by 1000, and the remainder is the number of milliseconds since the last integral elapsed second.

To try it out, use the Mosaic IDE’s editor to open the TIMEKEEP.C file in the \MOSAIC\DEMOS_AND_DRIVERS\MISC\C EXAMPLES directory, click on the Make Tool to compile the program, and use the terminal to send TIMEKEEP.DLF to the QScreen. At your terminal, type:

main↓

to start the timeslicer and initialize the program. Now at any time you can mark a starting time by typing at the terminal:

MarkTime( )

and you can print the elapsed seconds and ms since the last mark by typing:

PrintElapsedTime( )

which will produce a response of the form:

Time since mark is: 3 seconds and 45 ms.
 

Using interrupt service routines (ISRs)

The on-chip resources of the 68HC11 include an A/D converter, timer system, pulse accumulator, watchdog timer, serial communications port, high speed serial peripheral interface, and general purpose digital I/O. The 68HC11’s 21 interrupts can enhance the performance of these facilities. Interrupts allow rapid response to time-critical events that often occur in measurement and control applications. For example, you can use an interrupt to create a pulse-width modulated (PWM) output signal.

 

Interrupt recognition and servicing

68HC11 interrupts fall into two main categories: nonmaskable and maskable.

 

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:

Table 5-1 Nonmaskable Interrupts.

Interrupt Name 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 QED-Forth restart sequence.
Clock Monitor Failure 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.
COP Failure 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.
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.
SWI 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 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 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] FIXME). 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:

Table 5-2 Maskable Interrupts, from Highest to Lowest Priority.

Interrupt Name Description
/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).
Real Time Interrupt The RTI provides a programmable periodic interrupt (MC68HC11F1 Technical Data Manual, p.5-5).
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.).
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.).
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.).
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.).
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.).
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.).
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.).
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 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 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 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 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 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).
 

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 QScreen 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.

! An Interrupt Flag Bit Is Cleared By Writing a 1 to it !

Although mask bits can be set and cleared by storing the desired value in them, flag bits are unusual. Since flag bits are set by trigger events, it is not possible to set them via software. In order to clear an interrupt flag bit, a logical one must be stored into the flag bit’s location – that clears it to zero!

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 QScreen 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 QScreen.

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 the "Library Functions that Disable Interrupts" chapter of the Control-C Glossary. 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.

Interrupt Latency

Time to Enter an Interrupt Service Routine: 8.5 µsec
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:

  1. Use #define to name all required bit masks related to servicing the interrupt, and look in the Motorola 68HC11F1 documentation and the QEDREGS.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.
  2. 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.
  3. 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.
  4. 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() makes it simple

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:

Table 5-3 68HC11 Interrupts.

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.

Listing 5-1 TIMEKEEP.C, An Example of an Interrupt Service Routine

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 5-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>&#124;</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.

 

Summary

Using interrupts requires:

  • coding an interrupt service routine;
  • using ATTACH() to bind it to the appropriate interrupt; and,
  • enabling its local interrupt mask.
 

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.

 
This page is about: C Language Real Time Operating System RTOS on 68HC11 Microcontroller Based GUI-touchscreen Instrument Controller, Multitasking and Event-driven Application Programs, Timeslicer and Elapsed-time Clock, Interrupt Servicing, Responding to Real-time Events, Attaching Interrupt Handler, Writing Interrupt Service Routine (ISR) – How to write multitasking and event-driven application programs to respond to interrupts and other real-time events in an embedded systems programs. interrupt priority, maskable and non-maskable interrupts, interrupt latency, enabling and disabling interrupts
 
 
Navigation