Link here

Real Time Programming

The Timeslicer and Task Switching

The QVGA Controller’s multitasking executive maintains a built-in 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 = FetchLongProtected( (xaddr)((unsigned)(&TIMESLICE_COUNT)) );
}
_Q void PrintElapsedTime(void)
{   long elapsed_ms =
DEFAULT_TIMESLICE_PERIOD*(FetchLongProtected( (xaddr)((unsigned)(&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 (the Make icon) to compile the program, and use the terminal to send TIMEKEEP.DLF to the QED Board. 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 handshaking.

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 but 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) 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) 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 6-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 pp.5-22...5-26). 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 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 sets the automaticallyI bit to 1 to temporarily prevent maskable interrupts from being serviced. Control is then passed to the interrupt handler code, which you must provide, pointed to by the contents of the interrupt vector. 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

The programmer can explicitly clear the I bit inside an interrupt service routine to allow nesting of interrupts. Make sure that the return stack is large enough to accommodate the register contents placed there by the nested interrupts. The default size of the return stack is 768 bytes, and it can be easily resized.

 

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 6-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-4).
Real Time Interrupt The RTI provides a programmable periodic interrupt (MC68HC11F1 Technical Data Manual, p.6-8).
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.7-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.7-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.7-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.7-4 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.7-4 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.7-4 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.7-4 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.7-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.7-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.7-11 ff.).
Pulse Accum Edge A PEDGE interrupt occurs after a signal edge is detected on port A, pin 7 (MC68HC11F1 Technical Data Manual, p.7-11 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.10-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 Manua p.9-16).
 

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, pp.6-15...6-16). The default highest priority maskable interrupt is /IRQ. A table in MC68HC11F1 Technical Data Manual,, p. 6-16 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 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 QVGA 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 one must be stored into the flag bit’s location.

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 p.10-14 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, p.2-17). The / prefix to each of these names indicates that the signals are active-low. Pull-up resistors on the QVGA 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.

/XIRQ is nonmaskable. After a one-time enabling (clearing) of its X control bit in the CCR, an /XIRQ request will be immediately serviced. For this reason it can be used to warn of critical situations such as impending power failure.

The /IRQ and /XIRQ pins are accessed and controlled via the Digital I/O and Control bus (for pin locations see Appendix A). They operate as active-low inputs to the processor. An external device can drive either of these lines 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 described in Chapter 4 can also be configured to interrupt the processor when an external event occurs.

 

Configuring /IRQ and /XIRQ Interrupts

In its default state after each reset or restart /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, p.5-24). IRQE is a "protected bit" in OPTION that must be written within the first 64 E cycles after a reset. The QED-Forth word INSTALL.REGISTER.INITS (described in the glossary) may be used to specify a value that is automatically stored into OPTION upon each reset.

The /XIRQ external interrupt supports only level-sensitive operation, and is a nonmaskable interrupt. Most interrupts, including /IRQ, are globally disabled ("masked") if the I bit in the condition code register (CCR) is set. /XIRQ is not affected by the state of the I bit. Rather, it is controlled by the X bit in the CCR. Whenever the 68HC11 is reset, the X bit is set in the CCR, thus disabling the /XIRQ external interrupt. The QED-Forth startup routine does not modify the X bit. To use the /XIRQ interrupt, install an appropriate interrupt handler using the ATTACH command, and then enable the /XIRQ interrupt after each reset by clearing the X bit. A short routine that clears the X bit is presented below.

After being enabled, only an in-process /XIRQ service routine or a reset can prevent an /XIRQ interrupt request from being recognized. The /XIRQ interrupt is commonly used to detect power loss as described below.

 

Using /IRQ and /XIRQ

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.

Similarly, to use /XIRQ, install an interrupt handler using Attach.

Next enable the interrupt by clearing the X bit in the CCR.

Now that /XIRQ is enabled, your external interrupt handler will be called whenever the /XIRQ line on the I/O and Control Bus is pulled low. A 68HC11 reset will again set the X bit.

 

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) if the board is clocked at 16 MHz. 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 with a 16 MHz crystal. 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 (or, for the /XIRQ interrupt, by the X 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 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 6-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 chapters 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() simply logically inverts the contents of the static variable named action_variable .

Listing 6-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 later in this chapter.

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 in the next chapter.

 

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 library functions; the Glossary document and the header files in the \MOSAIC\FABIUS\INCLUDE\MOSAIC directory specify which functions are of the _forth type. (For the curious: The reason is that _forth functions assume that the Y register has been pre-initialized to point to common RAM that is usable as a data stack. Because C functions use the Y register for other purposes, it is difficult to ensure that the Y register is properly initialized for _forth functions upon entry into an interrupt routine. )

 

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 of the C compiler.

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.

 

Multitasking

The QVGA Controller provides a real time operating system that allows cooperative and preemptive multitasking. Multitasking allows the programmer to partition the processor’s time among tasks so that time critical tasks don’t have to wait unnecessarily for the completion of less critical tasks.

For example, suppose that in a mission critical application an instrument must check several conditions frequently, say every 10 milliseconds, and if an error condition is detected immediately invoke a shut-down sequence or signal an alarm. Suppose too, that the instrument must periodically compute the rms average value of a 1000-point waveform. Written in C this latter task would take over a second. Without a multitasker the processor would need to wait the full second for the completion of the computationally intensive task before returning to the time critical task. With multitasking, each activity is set up as an autonomously running task and the operating system switches rapidly between them at a rate the programmer chooses. The tasks appear to be operating simultaneously. If in this example we cause a task switch every 5 msec the time critical task would be guaranteed the opportunity to check for its error conditions at least every 10 msec. In the majority of cases in which it does not find an error it can just return immediately to the other task, and is called again in 5 msec. In this way the time critical task has guaranteed periodic use of the processor while still not significantly robbing time from the computationally intensive task.

Multitasking has other less obvious but equally important advantages: it allows a complex program to be decomposed into modules that can be coded and debugged independently; it simplifies the structure of the code making it more understandable; and, it allows an instrument to accomplish diverse tasks with less chance of unintentional interactions among them.

Multitasking requires little overhead on the QVGA Controller. The following table provides the overhead for task switching and interrupts on a QVGA Controller clocked at 16 MHz:

Table 6 4 Multitasking and Interrrupt Overhead Times

Operation Time (microseconds)
Task switch time 30
Interrupt latency 9
Interrupt exit time 5
Using ACTIVATE()
When a new task is activated using ACTIVATE() it inherits the condition code register at the time of activation, which includes the interrupt mask bit. Consequently, before activating a task make sure that interrupts are globally enabled. That way, when the task’s action routine is entered the first time, interrupts are enabled, ensuring operation of the timeslicer. If interrupts are globally disabled when the task is activated, then when it is first entered interrupts will be disabled, and the timeslicer will stop – freezing execution in that task if you are using only preemptive multitasking.
 

The Battery-Backed Real Time Clock

The QVGA Controller supports an optional battery-backed real time clock. This smart watch and integrated battery are packaged as a sealed 32-pin DIP memory device. When this device is installed in socket S3 on the QED-Flash Board, it provides a battery backed calendar and watch that can be set and read by built-in functions. Accuracy is better than ±1 minute per month. These sealed units are guaranteed to run for at least ten years. The watch correctly accounts for the differing numbers of days in each month, and also handles leap years.

 

Setting and Reading the Real Time Clock

The built-in library functions SetWatch() and ReadWatch() make it easy to set and read the real time clock. These functions gain access to the smart watch by sending a special 64 bit pattern on the lowest order data bit; when the smart watch detects this pattern, it takes control of the next 64 read or write cycles to set or read the time.

The Smart Watch Disables Interrupts for a Short Time

The routines that access the watch automatically disable all maskable interrupts while the watch is in control (about 0.5 msec). 1)

The SetWatch() and ReadWatch() functions use the top 16 bytes of the 68HC11F1’s on-chip RAM as a buffer to hold intermediate results during the data transfers, and the top 8 bytes at addresses 0xB3F8-0xB3FF serve as the watch_results structure that contains the results returned by the most recent call to ReadWatch() as described below.

The battery-backed clock is pre-set at the factory to Pacific Time in the United States. To re-set the smart watch to your time zone, your program can call the function:

void SetWatch(hundredth_seconds,seconds,minute,hour,day,date,month,year)

The interactively callable routine named SetTheWatch() is defined in the TIMEKEEP.C file so you can set the watch from your terminal. As explained in the Control-C Glossary, the hour parameter ranges from 0 to 23, the day from 1 to 7 (Monday=1 in this example), and the year parameter ranges from 00 to 99. For example, if it is now 10 seconds past 5:24 PM on Wednesday, May 26, 2004, you could interactively set the watch by typing:

SetTheWatch( 0, 10, 24, 17, 3, 26, 5, 4)

The watch is set and read using 24-hour time, where midnight is hour 0, noon is hour 12, and 11 PM is hour 23. Note that you may assign any day of the week as "day number 1".

The function ReadWatch() writes the current time and date information into a structure named watch_results that occupies the top 8 bytes of on-chip RAM at addresses 0xB3F8-0xB3FF. Pre-coded macros name the structure elements so it is easy to access the time and date information. For example, the following simple function in the TIMEKEEP.C file reads the smart watch and prints the time and date:

_Q void SayDate(void)
{   ReadWatch();     // results are placed in watch_results structure
printf("\nIt is now %d:%d:%d on %d/%d/%d.\n",
WATCH_HOUR, WATCH_MINUTE, WATCH_SECONDS,
WATCH_MONTH, WATCH_DATE, WATCH_YEAR);
}

It simply calls ReadWatch() , and then executes a printf() statement using the pre-coded structure macros to reference the time parameters in the watch_results structure. If you called this function immediately after setting the watch as described in the prior section, the response at your terminal might be:

It is now 17:24:31 on 5/26/04.

After compiling TIMEKEEP.C , you can interactively type at your terminal:

SayDate( )↓

at any time to see a display of the current time and date.

 
Notes:
For the QED-Board, because the actual RAM in socket S2 cannot be accessed during this data exchange, interrupts must be disabled while the smart watch is being accessed. If an interrupt were to occur during this time, the processor would not be successful when it tried to save the machine state on the return stack, and a crash would result. To prevent this situation, the routines that access the watch automatically disable all maskable interrupts while the watch is in control (about 0.5 msec with a 16 MHz crystal). It is the programmer’s responsibility to make sure that no non-maskable interrupts (such as /XIRQ) try to access the RAM while the watch is being set or read. For the QVGA Controller we don’t have to worry about this, because the watch is placed in S3 and does not disable any RAM. So we don’t need to warn about /XIRQ.
This page is about: Multitasking, Event-driven C Language Application Programs on GUI-touchscreen Instrument Controller, C Language Real Time Operating System RTOS, 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 C-language software applications to respond to interrupts and real-time events in a real time embedded system using a real time operating system RTOS. interrupt priority, maskable and non-maskable interrupts, interrupt latency, enabling and disabling interrupts
 
 
Navigation