Project 5: Digilent Pro MX7 and Interrupts

Process Speed Controls Using Interrupts

The purpose of this project is to explore detecting events using interrupts or by using preemption that implements a nested interrupt management scheme. Concepts concerning processor context are introduced, as well as interrupt prioritization. The functional requirement for Project 5 are essentially identical to those for Project 3. The biggest difference is that time and button action events are detected by interrupts rather than by software that polls processor flags and logic levels applied to input pins.


Prerequisites

Inventory


Interrupt Programming

“In computing, preemption is the act of temporarily interrupting a task being carried out by a computer system, without requiring its cooperation, and with the intention of resuming the task at a later time. It is normally carried out by a privileged task or part of the system that has the power to interrupt, and later resume, other tasks in the system.”¹ In software, a task is a basic unit of programming that has a constrained, defined purpose. For example, reading the state of the input pins to determine if buttons are pressed or not can be considered a task. Multitasking implies that more than one task is running at the same time. Single-core processors appear to be multitasking by time-sharing the processor resources. No time sharing is required in Projects 3 and 4, where each task in the program ran to its completion before the next task could begin. When multitasking with a single-core processor, some tasks are suspended so that higher priority tasks can be serviced (executed).


Pmod STEP Parts Layout

PIC32 Interrupts

Mechanics of Interrupts

Preemption by an interrupt causes the processor to stop the execution of the current code flow and begin execution of a special function called an “interrupt handler” or “interrupt service routine” (ISR). In previous projects, the order in which tasks were started and completed was strictly controlled by the order in which code was executed in the program. In this project, tasks are started and completed based on a programmed prioritization.

An interrupt is a signal triggered by an event. The computer suspends what would normally be the next instruction of a task in one part of a program and begins executing code to complete an entirely different task. This occurrence of events can be periodic (deterministic) or aperiodic (sporadic). An example of a deterministic event is a timer flag being set. Sporadic interrupts are the result of an unpredictable event such as a divide by zero error, low power detection, serial data received flag or even pressing a button. These signals cause the microprocessor to implement a sequence of operations that results in an orderly way of suspending one task, executing an entirely different task, and resuming the original task.

The three main types of interrupts are software, internal hardware, and external hardware. Interrupts that are initiated by some event inside the microprocessor are also commonly referred to as exceptions or traps. Software interrupts are explicitly internally triggered by some instruction within the current program and are commonly referred to as trap instructions. Exceptions are internally generated hardware interrupts triggered by errors detected during software execution, such as illegal math operations (overflow, divide-by-zero), debugging (single-stepping, breakpoints), and invalid machine instructions or operational codes (opcodes). Finally, external hardware interrupts are exceptions initiated by hardware other than the CPU. Both the software code and the processor architecture determine how exceptions are prioritized and processed.

The context defines the state or operating conditions of the processor. In order to properly resume executing the task that was interrupted, the ISR is responsible for saving and restoring the context of the code that is interrupted. At the very least, the context consists of the processor register values, the flag register, and the program address of the next instruction to be executed when the ISR has completed. The C compiler is directed to generate code segments called a prologue and an epilogue. The prologue code saves the processor's context prior to executing the code that you have generated to service the interrupt. The epilogue code restores the context at the end of the ISR and redirects the execution of computer code back to the instruction that was preempted. For the PIC32 processor, the C compiler generates 33 assembly language instructions to save the processor context and an equal number of instructions to restore the context, thus requiring approximately 0.85 μs before and after executing the ISR code.

Interrupt latency is defined as the time between the instant when an event generates the interrupt signal and the time when the first useful instruction in the ISR is executed. This latency is made up of two major components: the time required to save the processor context and the amount of time that an interrupt is disabled or deferred by higher priority interrupts.

Managing Interrupts

Preemptive programs usually consist of two types of tasks: foreground and background. Foreground tasks are those preemptive tasks that the processor executes as soon as the need arises. Interrupts are assigned priority levels that dictate the order in which interrupts will be serviced. In the case of multiple foreground tasks both needing service at the same time, the higher the interrupt priority, the sooner the microprocessor executes the code to service that interrupt. Code executed immediately following a power-up reset runs at priority level zero and has no preemptive capability. This initial background task is responsible for setting up the resources for tasks that will eventually run as foreground tasks.

Foreground tasks have preemptive capability by having higher priorities than background tasks. The foreground task ensures adequate response times, while the background task manages deferred processing of the foreground data. Background tasks are those that the processor executes whenever it has available time and is waiting for something more important to do. Background tasks generally run at the interrupt priority level zero within a while(1) { … } loop, where event detection by polling is common. Interrupt only systems that only have foreground processes respond quickly to both periodic and sporadic events but ignore potential work that can be allocated to a background process. The use of interrupts falls under the broad category of real-time task scheduling, and a thorough investigation of this topic is beyond the scope of this project.

There are two types of preemptive operating schemes: nested and non-nested. Nested interrupt schemes allow higher priority level interrupts to preempt code that is servicing a lower priority interrupt. A non-nested interrupt scheme completes the execution of the code currently servicing an interrupt before it begins to service the code for any interrupt awaiting service regardless of interrupt priorities.

Non-nested preemptive schemes are usually easier to manage, but they can result in priority inversion, where a low priority task blocks a higher priority task from running. This scheme is easier to manage, because the developer only has to focus on one ISR, and the operation that determines which interrupts to service resembles the process of polling that we did in Project 4. For non-nested interrupt schemes, we must individually check the flags set by the interrupting event to determine which event to service. All interrupts are assigned to priority level 1 and are vectored (sent) to the same ISR where the particular interrupt flag must be cleared. For multiple simultaneous interrupts, tasks are serviced in the order that the interrupt flags are polled. A non-nested priority scheme results whenever interrupts are enabled using the instruction INTEnableSystemSingleVectoredINT(); provided in the peripheral library.

Nested priority schemes are generally more responsive, taking advantage of the fact that higher priority tasks can preempt (interrupt) lower priority tasks. The highest priority interrupt is guaranteed to have the lowest latency. However, nested priority schemes require more computer resources, such as time and memory, to accommodate context saving and restoring. Some low-end microcontrollers simply do not have sufficient memory and/or speed to support fully nested priority schemes. In reality, many embedded systems manage tasks using both nested and non-nested preemptive schemes as a result of either hardware and/or software limitations. Either interrupt management scheme can be used in combination with polling to detect events, as will be the case for Project 5.

The PIC32MX family of processors can use polling, nested priority, and non-nested priority schemes simultaneously. The PIC32MX processors utilize two major software level priorities called the “Group Priority” and the “Subgroup Priority.” There are seven levels that can be assigned for the group priority (1-7), with one being the lowest priority and seven being the highest. An interrupt with a higher group priority will preempt an interrupt of a lower priority. There are four subgroup priority levels that can be assigned at each of the seven levels of group priority. Interrupts assigned to different group priority levels operate as nested interrupts.

Multiple events can be assigned interrupt priorities at the same group level but at different subgroup levels. The sub-priority will not cause preemption of an interrupt in the same group priority level; rather, if two interrupts with the same group priority level are pending, the interrupt with the highest sub-priority will be handled first. The natural (hardware) priority scheme is asserted whenever multiple interrupts are generated simultaneously for events that are set for the same group and subgroup priority levels. (The notion of “simultaneous events” must be expanded to mean “if two interrupts are detected as waiting for service,” whether or not they occur at the same instant of time.) See Section 8 of the PIC32MX family reference manual for additional details of interrupt operations on the PIC32MX family of processors.

In any case, for the PIC32MX family of processors, the interrupt flag bit must be cleared in software prior to completing the service of the interrupt, otherwise the same interrupt will be serviced again without an event to initiate its service.


PIC32 Interrupts

General Interrupt Requirements

There are four essential code elements required for a program to process interrupts using C—the declaration of the functions that will be used to service the interrupts, the code to initialize the resources that generate interrupts, the ISR code that will be executed in response to an interrupt and the instructions that enable interrupts in a global sense. There must be an ISR to handle any and all enable interrupts that includes an instruction to clear the specific interrupt flag. Failing to clear the interrupt will cause the processor to repeatedly execute the ISR, thus preventing the processor from executing any other application code.

Functions that have been declared as an ISR cannot be called by any other C function. There are two ways that the ISR code will be executed: either in response to the event that sets the interrupt flag through hardware or by setting the corresponding bit in the interrupt flag register using a software instruction. A function that is declared as an ISR cannot have any variables passed to it (no argument list) and must return a void data type.

Normally, the code to initialize the resources that will generate the interrupt is executed only once for a given application. In this project, two different interrupts will be generated: Timer 1 interrupt and an I/O pin change notice interrupt. The programming requirements for each of these two interrupts are discussed below. The two statements in Listing 1 apply to all interrupts and should be executed only once after all interrupts have been initialized. In this instance, the program is using multi-vectored interrupts. Selected segments of the code used in an application can be protected from disruption by any and all interrupts by bracketing the code segment with the instructions INTEnalbeInterrupts(); and INTDisableInterrupts();.

Listing 1. Global Enabling of Global Interrupts.

// Enable multi vectored interrupts
      INTConfigureSystem(INT_SYSTEM_CONFIG_MULT_VECTOR); //done only once
      INTEnableInterrupts();      //use as needed

Timer Interrupts

In Project 4, you configured Timer 1 to set the interrupt flag once each millisecond. However, no interrupt was ever generated. In order to set up a timer to generate interrupts, you must first initialize the timer to run as you did in Project 4. This is accomplished using the OpenTimer1 instruction shown in Listing 2. The next three macro functions enable Timer 1 interrupts. The first macro instruction sets the Timer 1 interrupt priority level to 2, the second sets the Timer 1 sub priority level to 0, and the third enables Timer 1 interrupts.

Listing 2. Initializing Timer 1 Interrupts.

#define T1_INTR_RATE 10000  // For 1 ms interrupt rate with T1 clock=10E6
 
void timer1_interrupt_initialize(void)
{
 
//configure Timer 1 using internal clock, 1:1 prescale, PR1 = T1_TICK-1
        OpenTimer1( (T1_ON | T1_SOURCE_INT | T1_PS_1_1), (T1_INTR_RATE - 1) );
 
// set up the timer interrupt with a priority of 2, sub priority 0
mT1SetIntPriority(2);       // Group priority range: 1 to 7
mT1SetIntSubPriority(0);    // Subgroup priority range: 0 to 3
mT1IntEnable(1);            // Enable T1 interrupts
 
// Global interrupts must enabled to complete the initialization. – see
// Listing 1.
 
}

Timer 1 interrupts can be disabled at any point in the application software by using the instruction mT1IntEnable(0);. Other methods for initializing Timer 1 interrupts are shown in the orange tab on the top right.

Listing 3's code shows how to both declare a function to be an ISR and the general format of an ISR function. For this example, the parameter ipl2 sets the Timer 1 interrupt level to 2. (Note: the MPLAB X editor uses Courier font, and the lower case “L” is difficult to distinguish from the number “1” in the aforementioned font. As an example, consider setting the interrupt level to 1 using the parameter ipl1.) This method of declaring an ISR eliminates the requirement of a function prototype. Additional methods are provided in the PIC32 Interrupts section (button below) because they may be used by other sources you may find.

PIC32 Interrupts

Listing 3. Timer 1 ISR at Interrupt Level 2.

void __ISR(_TIMER_1_VECTOR, ipl2) Timer1Handler(void) 
{
 
/* User generated code to service the interrupt is inserted here */
 
mT1ClearIntFlag();  // Macro function to clear the interrupt flag
 
}

Change Notice Interrupts

Change notice (CN) interrupts are generated on selected enabled digital I/O pins whenever the voltage of the pin changes, causing the processor to read a logic value that is different from the previous reading of the PORT register. Pins designated as CN interrupt pins are shown in Table 1. Only BTN1 and BTN2 on the Digilent Pro MX7 processor board have the capability to generate CN interrupts. The I/O pin associated with BTN3 cannot generate a CN interrupt.

CNx Port
CN0 RC13
CN1 RC14
CN2 RB0
CN3 RB1
CN4 RB2
CN5 RB3
CN6 RB4
CN7 RB5
CN8 RG6 — BTN1
CN9 RG7 — BTN2
CN10 RG8
CN11 RG9
CN12 RB15
CN13 RD4
CN14 RD5
CN15 RD6
CN16 RD7
CN17 RF4
CN18 RF5
CN19 N/A

Table 1. I/O pins with CN interrupt capability.

CN interrupts can be disabled to protect segments of code using mCNIntEnable(0); to disable the interrupts and using mCNIntEnable(1); to enable them again . Listing 4 shows how to setup change notice interrupts using peripheral library code macros. In this code, CN interrupts are turned on for CN8 and CN9, which correspond to BTN1 (RG6) and BTN2 (RG7). The last parameter with value zero directs that internal pull-up resistors be disabled, because these pins have external pull-up resistors on the chipKIT Pro MX7 processor board. The code in Listing 4 enable CN interrupts at priority level 1 and sub priority level zero.

Listing 4. Initializing CHANGE NOTICE interrupts.

/* Declaration of interrupt ISR for CN at interrupt level 1 */
void cn_interrupt_initialize(void) // This code is executed only once
{
 
// BTN1 and BTN2 pins are set as inputs by including the file 
// chipKit_Pro_MX7.h and calling chipKit_Pro_MX7_Setup();
// PORTSetPinsDigitalIn(BIT_6 | BIT7); 
 
// Enable CN for BTN1 and BTN2
mCNOpen(CN_ON,(CN8_ENABLE | CN9_ENABLE), 0);
 
// Set CN interrupts priority level 1 sub priority level 0
mCNSetIntPriority(1);   // Group priority range: 1 to 7
mCNSetIntSubPriority(0);    // Subgroup priority range: 0 to 3
mCNIntEnable(1);        // Enable T1 interrupts
 
// Global interrupts must enabled to complete the initialization.
}

A single interrupt vector is used for all CN interrupts. The interrupt does not tell you if the pin went high or low; only that the condition on one of the selected pins has changed. The ISR code must return a type void and have no parameters passed to it. The CN interrupt flag must be cleared prior to exiting the ISR using the instructions mCNClearIntFlag();.

Listing 5. Change Notice interrupt service routine.

void __ISR(__CHANGE_NOTICE_VECTOR, ipl1) CNIntHandler(void) 
{
/* User ISR code inserted here */
 
/* Required to clear the interrupt flag in the ISR */
mCNClearIntFlag();      // Macro function
}

Project Tasks

This project implements a system that runs entirely using foreground processes. The Timer 1 interrupt is used to generate an interrupt once each millisecond. The PIC32 change notice interrupt generates an interrupt when a button is pressed or released. The project consists of writing a program that meets the specifications listed below:

  1. Functionality for main function.
    1. Calls system_init function (see step 2).
    2. Continuously executes the while(1); statement.
  2. Create a system_init function that implements a fully nested interrupt scheme for Timer 1 and change notice interrupts using the following steps.
    1. Initialize the processor board using the function chipKit_Pro_MX7_Setup();. This function initializes the PIC32 processor for:
      1. Set the I/O PORT B pins for LEDA, LEDB, LEDC, and stepper motor as outputs.
      2. Set the I/O PORT G to read the button inputs for BTN1 and BTN2 as inputs.
    2. Initialize Timer 1 to generate an interrupt once each ms . Set the group priority for level 2 and the subgroup level for 0. (Refer to Listing 2.)
    3. Initialize the PIC32MX7 system for change notice detection. Set change notice interrupts to detect activity on BTN1 and BTN2 only, the group priority level 1 and the subgroup level 0. (Refer to Listing 4.)
    4. Set the system for multiple vectored interrupts. (Refer to Listing 1.)
  3. Functionality for change_notice_ISR (replaces read_buttons function in Project 4). (Refer to Listing 5.)
    1. Set LEDC on at the beginning of the ISR and off at the end of the ISR.
    2. Call the sw_delay function for 20 ms delay to debounce button contacts.
    3. Assign port data to the button_status variable.
    4. Call decode_buttons function with arguments of button status.
    5. Clear CNIntFLag.
  4. Functionality for Timer1_ISR . (Refer to Listing 3.)
    1. Set for a 1 ms interval (Refer to Project 4).
    2. Toggle LEDA.
    3. Decrement the step_delay variable.
    4. When step delay is zero:
      1. Call stepper_state_machine function.
      2. Reset step_delay variable to the value stored in the step_period global variable.
    5. Clear T1IntFlag.
  5. Functionality for decode_buttons (Refer to Project 4).
    1. Determine values for global variables step_dir, step_mode, and step_period.
  6. Functionality for stepper_state_machine (Refer to Project 4).
    1. Toggle LEDB.
    2. Determine stepper motor output signals for step_code (local).
    3. Call output_to_stepper_motor function with value of step_code.
  7. Functionality for output_to_stepper_motor (Refer to Project4).
    1. Output step_code to Port B using the read-modify-write sequence.
  8. Functionality for sw_delay (Refer to Project 2).
    1. Implement a hardware assisted software delay using the core timer that can be preempted.


Project Testing

There are significant timing requirements for this project: the Timer 1 interrupt rate, the stepper motor step rate, and the button debounce delay. Table 2 is provided below to assist you in determining where to place the instructions to control the instrumentation LEDs. The red tab above shows where to connect an oscilloscope or logic analyzer to the test points for LEDA through LEDC on the PmodSTEP to instrument this project. An example of a logic analyzer screen capture is shown below in Fig. 1.

LED Operation When
LEDA Toggle Each millisecond
LEDB Toggle Each step
LEDC SET (on) Start of CN ISR
LEDC Cleared (off) End of CN ISR

Table 2. Instrumentation LED Operations.

Figure 1. Screen capture for the instrumentation of Project 5. Figure 1. Screen capture for the instrumentation of Project 5. Screenshot of Digilent WaveForms running on Microsoft Windows 7.


Test Your Knowledge!

Now that you've completed this project, you should look at the following questions/problems:

  1. Develop a DFD and a CFD for this project.

  2. ANSWER: The data flow diagram shown in Fig. 2 is essentially the same as it was for Project 3. However, the control flow diagram shown in Fig. 3 now consists of three independent programs. The background code initializes the system. The two foreground processes run as independent programs and run to a termination.
Figure 2. Data flow diagram.

Figure 3. Control flow diagram.

  1. Explain how the oscilloscope capture required for step 4 demonstrates that your program implements a nested interrupt preemption scheme.

  2. ANSWER: The signal for LEDC is high during the period that the Change Notice interrupt is being served. The signal for LEDA continues to toggle due to servicing the Timer 1 interrupt. Hence, the timer interrupt is interrupting the Change Notice interrupt.

  3. What are the requirements that dictate which sections of code must be protected from interrupts?

  4. Port B is changed in the Timer ISR (changing LEDA and SM1 through SM4) and LEDC is modified in the Change Notice ISR. If the instruction to modify the LEDC bit of Port B is not atomic (meaning that the order of execution cannot be changed), then it would be possible that a Timer 1 interrupt could change the contents of the LATB register in between instructions in the Change Notice ISR. Two of the three conditions needed for requiring protecting code are met: using a global variable (LATB) and modification in two ISR operating at different levels. If the modification of the global variable is not atomic, then the third condition is met and the code will need to be protected.

  5. What constitutes the worst-case latency for systems that use polling to detect events?

  6. ANSWER: It is the main function loop time—all functions in the main function while(1) loop must be completed within the required time or some functions will be executed more slowly that the design requires.

  7. Explain why the stepper motor is potentially running at a more accurate and consistent speed compared to the Project 4 implementation.

  8. ANSWER: The interrupt that causes the stepper motor to step is at the highest level. The latency is constant and predictable regardless of when the interrupt occurs. Hence the latency between the Timer 1 interrupt and the time when the stepper motor actually steps is always the same.

  9. Discuss the advantages and disadvantages of polling versus interrupts in real-time control.

  10. ANSWER: The C code to implement polling is relatively simple to write and verify. The C code to setting up and ISR can be confusing to someone who is unfamiliar with the PIC32 processor. There are no memory requirements to store the processor context. Systems that do not use interrupts are deterministic albeit predictably slow.

  11. Describe how you would change the Project 5 program to allow you to control the stepper motor at speeds other than those that are multiples of one millisecond.

  12. ANSWER: Increase the Timer 1 interrupt frequency.

  13. Why are interrupt service routines restricted so that no variables can be passed to or returned from the function?

  14. ANSWER: Since one cannot predict when the ISR code will be executed, there is no way to prepare the processor registers to receive or return data. The only means of passing data to and from ISRs is by using global variables.