Note: The mercurial server is disabled at the moment while I investigate whether it can run with an acceptably low CPU load – Mike.
Interrupt latency – an experiment (Digital Systems)
[Needs editing to match micro:bian conventions.]
Let's define interrupt latency to be the time delay between a physical event happening to cause an interrupt and the beginning of the specific code we write to deal with it. To measure this delay, we will need some cooperation from the hardware, but luckily the timers on the Nordic chip provide everything we need. We will program a timer to request an interrupt when it overflows, and also set a GPIO pin low – predictably enough, one of the pins on the micro:bit that are connected to LEDs. Then we can set the GPIO pin high again in the interrupt handler, or in the process connected to the interrupt, and measure the pulse width on the GPIO pin to find the interrupt latency in each case.
Simpler microcontrollers than the Nordic chip have specialised hardware for using a timer to control a GPIO pin, because that is a common method for producing a pseudo-analog output using pulse width modulation (PWM). On the Nordic chip, things are a bit more complicated, and we will need to chain together the GPIO pin with a GPIO tasks and events (GPIOTE) channel, a programmable peripheral interconnect (PPI) channel and the timer to achieve the same effect. I've included the exact setup code below in case you want to perform a similar experiment.
We will actually configure two GPIO pins in this way, and reset one of them in a specialised interrupt handler and the other in a micro:bian device driver process: that will allow us to assess the difference in latency between the two approaches. Here's the code for the interrupt handler:
void timer2_handler(void) { // Reset one of the GPIO pins GPIOTE_OUT[0] = 1; // Send INTERRUPT message to the driver disable_irq(TIMER2_IRQ); interrupt(DRIVER); }
The first line of actual code triggers the GPIOTE task a second time to reset the pin, and the other two lines of code are equivalent to the standard micro:bian interrupt handler: disable the interrupt so it does not continue to fire, then send an INTERRUPT
message to the driver.
The driver process itself is simple: in a loop, it waits for an interrupt, then turns off the second I/O pin and resets the interrupt.
void timer_task(int arg) { message m; GPIO_DIRSET = 0xfff0; GPIO_OUT = 0x3ff0; // Set up the timer (see below) while (1) { receive(HARDWARE, &m); // Reset the second GPIO pin GPIOTE_OUT[1] = 1; // Acknowledge the interrupt TIMER2_COMPARE[0] = 0; reconnect(TIMER2_IRQ); } }
Running the program produces negative-going pulses on both I/O pins, but of very different lengths. The horizontal scale is 5μs per division.
The upper trace shows the pulse that is finished by the interrupt handler; it is about 1.44μs long, or 23 clock cycles at 16MHz. Interrupts on the nRF51 have a published latency of 16 cycles, and examining the machine code for the interrupt handler suggests that it will take an additional 8 cycles to set the GPIO line high again. The comes to 24 cycles rather than 23, but there is a one-cycle latency in the PPI module at the start of the pulse, so that agrees nicely.
The lower trace shows the performance of the device driver process, with a much longer latency of about 20μs. That is still fast enough for many purposes, but for very time-sensitive events, the direct interrupt handler clearly has a significant advantage.
Timings
- To start of interrupt handler: 1.44 mu = 23 cycles
- Immediately after start: 1.82 mu = 29 cycles
- In
interrupt
, before message send: 4.89 mu = 78 cycles - In
interrupt
, aftermake_ready
call: 8.89 mu = 142 cycles - At end of
interrupt
: 10.45 mu = 167 cycles
- In
- End of interrupt handler: 10.89 mu = 174 cycles
- Context switch
- Save context ...
- Start of
pendsv_handler
: 11.89 mu = 190 cycles - After
isave
: 13.76 mu = 220 cycles
- Start of
- Start of
cxtswitch
: 14.20 mu = 227 cycles - After
make_ready(current)
: 15.57 mu = 249 cycles - After
choose_proc()
: 17.39 mu = 278 cycles - Restore context ...
- After
cxtswitch
: 18.01 mu = 288 cycles - After
irestore
: 19.57 mu = 313 cycles
- After
- Save context ...
- In driver process: 20.64 mu = 330 cycles
Setting up the hardware
- We first connect timer 2 to a 16MHz clock and arrange for it to reset itself with a
TIMER2_COMPARE[0]
event once every 16,000 counts or 1ms. The resetting is accomplished by enabling the 'shortcut'TIMER_COMPARE0_CLEAR
– a shortcut because the same effect could be achieved with an interrupt handler or with the PPI system we are about to introduce. We also set the timer to request an interrupt on theCOMPARE[0]
event.
TIMER2_STOP = 1; TIMER2_MODE = TIMER_Mode_Timer; TIMER2_BITMODE = TIMER_16Bit; TIMER2_PRESCALER = 0; // 16 MHz TIMER2_CLEAR = 1; TIMER2_CC[0] = 16000; // Period 1 ms TIMER2_SHORTS = BIT(TIMER_COMPARE0_CLEAR); TIMER2_INTENSET = BIT(TIMER_INT_COMPARE0);
- Next, we will configure two channels of the PPI system to connect the
COMPARE[0]
event of the timer to tasks that toggle the two GPIO pins. Each PPI channel has an event endpointEEP
that we connect to the timer, and a task endpoint that we connect to the GPIOTE hardware. After connecting them, we must enable the two channels.
PPI_CH[0].EEP = &TIMER2_COMPARE[0]; PPI_CH[0].TEP = &GPIOTE_OUT[0]; PPI_CH[1].EEP = &TIMER2_COMPARE[0]; PPI_CH[1].TEP = &GPIOTE_OUT[1]; PPI_CHENSET = BIT(0) | BIT(1);
- Third, we must configure the two GPIOTE channels. Each is in task mode (rather than event mode), is connected to a certain GPIO pin (
PSEL
), and toggles the pin from 1 to 0 or 0 to 1 when triggered. (It's a bit of a flaw of the nRF51's GPIOTE system that thePOLARITY
setting 0-to-1 and 1-to-0 are pretty useless, because there's no easy way of setting the state back again.) The initial state of each pin is 1.
// GPIOTE channels 0 and 1 toggle the two pins GPIOTE_CONFIG[0] = FIELD(GPIOTE_CONFIG_MODE, GPIOTE_Mode_Task) | FIELD(GPIOTE_CONFIG_PSEL, 4) | FIELD(GPIOTE_CONFIG_POLARITY, GPIOTE_Polarity_Toggle) | FIELD(GPIOTE_CONFIG_OUTINIT, 1); GPIOTE_CONFIG[1] = FIELD(GPIOTE_CONFIG_MODE, GPIOTE_Mode_Task) | FIELD(GPIOTE_CONFIG_PSEL, 5) | FIELD(GPIOTE_CONFIG_POLARITY, GPIOTE_Polarity_Toggle) | FIELD(GPIOTE_CONFIG_OUTINIT, 1);
Now we are ready to connect the driver process to the interrupt and start the timer.
connect(TIMER2_IRQ); TIMER2_START = 1;