Building Bare-Metal ARM Systems with GNU: Part 10 - Test Strategies
(Editor's note: In this series of ten articles Miro Samek of
Quantum Leaps details developing apps on the ARM processor using QNU,
complete with source code in C and C++.)
In this final part of this article series I explain the "Blinky" example application included in the code accompanying this article. I also give some tips for manual testing of various interrupt preemption scenarios.
The Blinky Example Application
The example project is called "Blinky" because it blinks the 4 user LEDs of the AT91SAM7S-EK evaluation board (Figure 1 below). Blinky is just a primitive foreground/background (main+ISRs) application, but has been carefully design to demonstrate all the features and techniques discussed in this multi-part article.
|Figure 1 Atmel AT91SAM7S-EK evaluation board executing the Blinky application.|
Specifically, Blinky relies on proper initialization of the .data and .bss sections, as well as .rodata, .text, and .stack sections. The application performs the low-level initialization in C to setup the PLL to generate the 48MHz clock for the AT91SAM MCU and to perform the memory remap operation (See Part 2 of this article series).
I've fine-tuned the application by placing hot-spot functions in RAM for fast execution (See Part 5), whereas I used both ways of locating functions in RAM: the explicit section assignment with the __attribute__ ((section (".text.fastcode"))), and the direct placement of functions in the linker script blinky.ld. I've also compiled selected files in the application to ARM and others to Thumb to demonstrate ARM-Thumb interworking (See the Makefile).
The C++ version of Blinky (located in the cpp_blinky directory) relies on static constructor invocation and uses virtual functions with late binding and early binding demonstrated in the code. As you can check by comparing the map files, the overhead of the C++ version with respect to the C version is only 500 bytes, but please keep in mind that the C++ version does significantly more than the C version because it supports polymorphism.
Blinky uses interrupts extensively at a very high rate (about 47kHz on average, or every 1000 CPU clock cycles). The interrupt level of the application (foreground) consists of three interrupts configured as follows:
1. Programmable Interval Timer of the AT91SAM7 (PIT) firing 100 times per second, configured as low-priority IRQ;
2. Timer0 RC-compare occurring every 1000 MCK/2 clocks (about 24kHz rate), configured as high-priority IRQ, and
3. Timer1 RC-compare occurring every 999 MCK/2 clocks (about 24kHz rate) configured as FIQ by means of the "fast forcing" feature of the AT91SAM7S MCU .
I've specifically configured the clocks of Timer0 and Timer1 to the maximum available frequency (MCK/2) and have chosen their periods very close to each other so that the relative phasing of these interrupts shifts slowly by just 2 CPU clock cycles over the interrupt period.
This causes the interrupts to overlap for a long time, so that virtually every machine instruction of the IRQ handler as well as the FIQ handler gets "hit" by the interrupt. Timer0 IRQ and Timer1 FIQ overlap at the beat frequency of MCK/2/(1000*999), which is about 27 times per second.
The ISRs communicate with the background loop (and potentially among themselves) by means of flags grouped into the shared bitmask. All ISRs signal events to the background loop by means of the function eventFlagSet(), which internally uses the critical section described in Part 7 of this article series to protect the shared bitmask.
Listing 1 below shows the structure of the background loop. The loop checks the shared event flags for occurrences of events by calling the function eventFlagCheck().
This function tests a given flag inside a critical section and also clears the flag if it's been set. For each event flag that has been set, the background loop calls the dispatch() method on behalf of the corresponding object of class Blinky that encapsulates one LED of the AT91SAM7S-EK board.
|Listing 1 The background loop of the Blink application (C++ version).|
From the user's perspective, the main job of each Blinky object is to decimate the high rate of the dispatched events so that the LED blinks at a lower rate observable by the human eye.
Figure 2 below shows the lifecycle of the Blinky class, which is a simple counting state machine alternating between ON and OFF states. In each state the state machine down-counts the number of events (number of calls to the Blinky::dispatch() method) from the pre-configured delay value for this state. The state machine transitions to the opposite state when the down-counter reaches zero.
For example, the pre-configured delays in Figure 2 are three ticks in the OFF state and two ticks in the ON state, which results in the LED blink rate of 1/5 of the original event rate with the duty cycle of 2/3.
(NOTE: The C version of the Blinky application emulates the class concept in C. This simple technique might be interesting in itself. You can also quite easily implement single inheritance and even polymorphism in C [see www.quantum-leaps.com/devzone/cookbook.htm#OOP  for more information]).
|Figure 2 - Lifecycle of a Blinky object: state machine (a), and sequence diagram (b).|
Manual Testing of Interrupt
The Blinky application performs quite extensive testing of the interrupt handling implementation discussed in Parts 6, 7, 8 and 9 of this multi-part series.
The high interrupt rate and the constantly changing relative phasing of the interrupts offer ample opportunities for preemptions among IRQs, the FIQ, and the background loop.
Here I would like to share with you a complementary technique for manual testing of various interrupt scenarios, so that you can easily trigger an interrupt at any machine instruction and observe the preemptions it causes.
The Blinky example application includes special instrumentation for manual testing of interrupts. When you uncomment the definition of the macro MANUAL_TEST at the top of the bsp.c file, you'll configure the Timer0 and Timer1 interrupts for manual triggering. As shown in Listing 2 below, the timers are configured with just one count in the RC-compare register, and the software triggers are NOT applied.
|Listing 2 Configuring Blinky for manual testing|
With this setup, you can manually trigger the timers from the debugger by writing 0x05 (SWTRG + CLKEN) to the timer's Control Register (TCx_CR). The TC0_CR is located at the addresses 0xFFFA0000, and the TC1_CR is located at 0xFFFA0040 . Because of just one count in the RC-comapre register, the timer expires immediately and triggers the desired interrupt before the next machine instruction gets executed.
The general testing strategy is to break into the application at an interesting point for preemption, set breakpoints to verify which path through the code is taken, and trigger the desired timer.
Next, you need to free-run the code (don't use single stepping) so that the interrupt controller can perform prioritization. You observe the order in which the breakpoints are hit. This procedure will become clearer after some examples.
The first interesting test is verifying the prioritization of IRQ interrupts by the AIC. To test this scenario, you place a breakpoint inside ISR_pit() (configured as low-priority IRQ).
When the breakpoint is hit, you move the original breakpoint to the very next machine instruction. You also set another breakpoint on the first instruction of the ARM_irq low-level interrupt handler. Next you trigger the Timer0 interrupt by writing 0x5 to the address 0xFFFA0000. You hit the Run button.
The pass criteria of this test are as follows:
1. The first breakpoint hit is the one inside the ARM_irq handler, which means that Timer0 IRQ preempted the PIT IRQ. NOTE: After this breakpoint is hit, you need to disable Timer0 from expiring again, by writing 0x2 (CLKDIS) to the address 0xFFFA0000.
2. The second breakpoint hit is the one inside ISR_pit(), which means that the low-priority IRQ continues after the Timer0 IRQ completes. You need to remove all breakpoints before proceeding to the next test.
The next interesting test is verifying that FIQ preempts any IRQ, but only outside a critical section. You start by setting a breakpoint inside any critical section, say in the eventCheckFlag() function. You run the application.
After the breakpoint is hit, you trigger both Timer0 and Timer1, by writing 0x5 to 0xFFFA0000 and 0xFFFA0040. Before you free-run again, you move the original breakpoint to the next machine instruction and set another breakpoint one machine instruction after the exit from the critical section (at the BX r14 instruction). You also set breakpoints on the first instruction of ARM_irq and ARM_fiq.
The pass criteria of this test are as follows:
1. The first breakpoint hit is the one inside the eventCheckFlag() function, which means that critical section cannot be preempted by any pending interrupts.
2. The second breakpoint hit is the one in ARM_fiq interrupt handler, which means that FIQ wins the head-to-head run against the IRQ. NOTE: At this point you need to disable the Timer1 clock from expiring again, by writing 0x2 (CLKDIS) to the address 0xFFFA0040.
3. The third breakpoint is the one in ARM_irq, which means that IRQ follows back-to-back the FIQ, before returning to the task-level code. NOTE: At this point you need to disable the Timer0 clock from expiring again, by writing 0x2 (CLKDIS) to the address 0xFFFA0000.
4. Finally, the breakpoint at the end of eventCheckFlag() function is hit, which means that the task continues only after all interrupts complete.
I'm sure that this technique can help you in performing many more interesting tests to study various preemption scenarios.
In this 10-part article series I've explained most of the code and techniques that you will need to build-bare metal ARM systems with the GNU toolchain. I hope that you will find this article a significant time-saver, which will not only help you to jump-start your project, but also form the backbone of your final ARM-based product.
To read Part 1, go to What's
need to get started.
To read Part 2, go to Startup code and the low level initialization
To read Part 3, go to The Linker Script.
To read Part 4, go to C and C++ compiler options
To read Part 5, go to Fine-tuning the application
To read Part 6, go to General description of interrupt handling
To read Part 7, go to Interrupt Locking and Unlocking
To read Part 8, go to Low-level Interrupt Wrapper Functions.
To read Part 9 go to C-Level ISRs and Other ARM Exceptions
 Atmel Datasheet "AT91 ARM/Thumb-based Microcontrollers, AT91SAM7S64 " available online..
 Quantum Leaps, LLC, Object-oriented programming in C.