9  Interrupts and Timers

This chapter is based on the projectlet:

- 07_timers

9.1 Projectlet Goals

While time and its passage has been at the core of all the projects thus far, we took the easy route by and large depending primarily on the builtin delay to provide the desired timing. In addition we activated the onboard real time clock (RTC) to maintain an independent (from the run time libraries of Ada) clock.

The logfiles generated revealed that the notion of time, clock are rather tricky - dependent as they are on the hardware availability and their accuracy thereof. The hardware used in these efforts namely the STM32F4 Discovery board is a development tool and cannot be expected to feature super high accuracy as would be required in a production environment - particularly safety critical applications.

In addition, the application design that relies on the delay statement operates on a cadence that is not too predictable. At the minimum, the cost of the operation being performed for example digitizing an analog value as we observed in other projects - may be a reasonable fraction of the delay time and thus the cadence could be significantly different from the expectation. In addition the application load on the microcontroller may well have an impact on this as well. The situation will be unacceptable in a hard real time system.

In this projectlet, we explore the built in hardware timers. Supposedly these timers are highly accurate and we leverage Interrupts generated by the timers for the periodicity of application functions. Leveraging the logging support from the toolkit which in turn relies on the RTC, we can compare the performance of the clocks.

Also explored is the pwm mode of the timers. Some timers eg. Timer_3 and Timer_8 have this feature and this is applied to fadeing an external LED.

9.1.1 Basic Approach

The Discovery board features quite a few timers. For this exploration we depend on the simplest of them Timer_6 and Timer_7. The timers can generate Interrupts at a desired rate and not much more. One of these timers we will use to just maintain a counter (1 second) and the other will be used for Analog to Digital conversion of onboard sensors.

The digitized values and the counter itself will be logged which can be analyzed to understand the drift.

9.2 Implementation

9.2.1 Toolkit Support

Timers are of course very dependent on the hardware - the microcontroller architecture and the board design. The package toolkit.timers is designed for the discovery board we are using in this set of projectlets. This pattern will lend itself and has to be adapted to the specific hardware.

Listing 9.1: Basic Timers
File : toolkit-timers.ads

0008 | 
0009 |    -- Following provide a very basic timer on the STM32F4 Discovery board
0010 |    -- Timerclock: 84 MHz
0011 |    -- 16 bit Prescaler and Reload Counters
0012 |    basic_1 : constant access stm32.timers.Timer := stm32.Device.Timer_6'access;
0013 |    basic_2 : constant access stm32.timers.Timer := stm32.Device.Timer_7'access;
0014 | 
0015 |    type UserCallback is access procedure (timerno : Integer);
0016 | 
0017 |    -- Some combination of Prescaler and the Counter can result in
0018 |    -- different cadences
0019 |    type cadence_type is (One_Hz, One_KHz, One_Fourth_Hz);
0020 | 
0021 |    procedure SetupBasic
0022 |      (tmr     : access stm32.timers.Timer;
0023 |       cb      : UserCallback;
0024 |       cadence : cadence_type := One_Hz);
0025 | 

In this projectlet, we use the 2 most basic timers on our circuit board. The initializations required are very simple:

Listing 9.2: Initialization - basic timers
File : toolkit-timers.adb

0048 |       STM32.Device.Enable_Clock (tmr.all);
0049 |       case cadence is
0050 |          when One_Hz =>
0051 |             stm32.Timers.Configure
0052 |               (tmr.all, Prescaler => 8399, Period => 9999);
0053 | 
0054 |          when One_KHz =>
0055 |             stm32.Timers.Configure
0056 |               (tmr.all, Prescaler => 16799, Period => 499);
0057 | 
0058 |          when One_Fourth_Hz =>
0059 |             stm32.Timers.Configure
0060 |               (tmr.all, Prescaler => 41999, Period => 7999);
0061 |       end case;
0062 |       stm32.timers.Set_Autoreload_Preload (tmr.all, true);
0063 |       stm32.timers.Enable_Interrupt
0064 |         (tmr.all, stm32.timers.Timer_Update_Interrupt);
0065 |       stm32.timers.Enable (tmr.all);

The toolkit provides an intermediary ISR in order to take care of required housekeeping such as dismissing the interrupt and then the application callback is invoked:

Listing 9.3: Interrupt Service Routine
File : toolkit-timers.adb

0024 |       procedure ISR7 is
0025 |       begin
0026 |          -- Required to clear the interrupt state with minimal delay.
0027 |          -- The user callback can focus on its own functions
0028 |          STM32.Timers.Clear_Pending_Interrupt
0029 |            (STM32.Device.Timer_7, STM32.Timers.Timer_Update_Interrupt);
0030 |          -- invoke user callback. The callback should also be extremely fast
0031 |          ucb_isr7.all (7);
0032 |       end ISR7;

9.2.2 Interfacing the Interrupts and application

Interrupts are serviced by Interrupt Service Routine (ISR)s. In other words, the application installs an ISR (handled in our design by the toolkit) which is invoked when the interrupt is generated. The ISR in turn needs to perform the function needed by the application. The ISR is required to be fast and does not have the flexibility to perform lengthy operations. For the specific timers used in this projectlet, 2 callbacks are used.

Listing 9.4: Callback Interface
File : cb.ads

0006 |    LED_Toggle_Count : Integer := 0;
0007 |    procedure LED_Toggle_Callback (timerno : Integer);
0008 | 
0009 |    subtype DutyCycleRange is Integer range 75 .. 95;
0010 |    dutycycle : Integer := DutyCycleRange'first;
0011 | 
0012 |    procedure SetDutyCycle_Callback (timerno : Integer);
0013 | 
0014 |    TimeToReadSensors : Ada.Synchronous_Task_Control.Suspension_Object;
0015 |    procedure SensorsOnboard_Callback (timerno : Integer);
0016 | 

The above specifies the two ISRs. The LED_Toggle_Callback will as its name suggests toggle an LED which can be expected to be a fast operation.

If the application needs to perform lengthy operations based on the (timer) interrupt then the ISR will have to delegate it to a different task which is illustrated in the SensorsOnboard_Callback above. In this case a Suspension_Object is provided so trigger the operations.

Listing 9.5: Report ticker value
File : cb.adb

0025 |       loop
0026 |          delay 1.0;
0027 |          if oldcounter /= LED_Toggle_Count then
0028 |             oldcounter := LED_Toggle_Count;
0029 |             toolkit.logs.logger.Put_Line
0030 |               ("Ticker " & oldcounter'Image, source => myname);
0031 |          end if;
0032 |       end loop;

In the case of the LED_Toggle_Callback, the ISR performs only an increment of a counter after toggling the LED. An independent task logs the counter without interfering with the ISR thus removing the lengthy logging operation from the ISR.

Listing 9.6: Eventflag interface
File : cb.adb

0055 |    procedure SensorsOnboard_Callback (timerno : Integer) is
0056 |    begin
0057 |       if timerno = 6 then
0058 |          stm32.Board.Toggle (stm32.Board.Red_LED);
0059 |          Ada.Synchronous_Task_Control.Set_True (TimeToReadSensors);
0060 |       end if;
0061 |    end SensorsOnboard_Callback;

In the case of the SensorsOnboard_Callback the ISR will just indicate that the interrupt was received using the Synchronization_Object. This is expected to be a fast operation.

Listing 9.7: Wait for timer
File : sensorsob.adb

0048 |    loop
0049 |       Ada.Synchronous_Task_Control.Suspend_Until_True (cb.TimeToReadSensors);
0050 |       toolkit.adc.Read (VBat, Temp);
0051 |       VBatf := toolkit.adc.Raw_To_Volts_Vbat (VBat);
0052 |       TempF := toolkit.adc.Raw_To_Celsius (Temp);
0053 |       toolkit.logs.logger.Put_Line
0054 |         (" VBat= "
0055 |          & VBat'Image
0056 |          & " VBatF= "
0057 |          & VBatF'Image
0058 |          & " Temp= "
0059 |          & Temp'Image
0060 |          & " TempF= "
0061 |          & TempF'Image,
0062 |          source => myname);
0063 |    end loop;

The application will wait on the Suspension_Object to perform the (lengthy) operation such as ADC as illustrated above. In addition to conversion, the data is sent to a log which is also somewhat lengthy. With this synchronization method there are no delays needed.

Listing 9.8: Setup the PWM enabled timer
File : pwmfader.adb

0031 |    toolkit.timers.SetupPWM
0032 |      (modulator,
0033 |       toolkit.timers.pwm_1,
0034 |       stm32.timers.Channel_1,
0035 |       toolkit.timers.pwm_pin_1,
0036 |       stm32.Device.GPIO_AF_TIM3_2);
0037 |    loop
0038 | 
0039 |       if dc /= cb.dutycycle then
0040 |          dc := cb.dutycycle;
0041 |          modulator.Set_Duty_Cycle (dc);
0042 |          toolkit.logs.logger.Put_Line
0043 |            ("Main " & cb.dutycycle'Image, source => myname);
0044 |       end if;
0045 |       delay 0.1;
0046 |    end loop;

As illustrated, the duty cycle is independently modified by an ISR attached to a different timer. Periodically the PWM timer’s duty cycle is changed thus resulting in the LED brightness changing.

9.2.3 Pulse width modulation and fading the LED

The projectlet pwmfader sets up a timer to generate square waves. Blinking an LED essentially is achieved by applying a voltage to the LED; when it is LOW the LED being off. By verying the duty cycle the effective voltage applied to the LED is varied and thus the brightness of the light emitted can be varied.

9.2.4 Log generation

We had briefly reviewed the logging support in an earlier chapter. To summarize, from the microcontroller, an outgoing log channel can be established which can be handled by a host using a serial communication discipline. In a typical device, there are multiple independent tasks each performing a distinct operation. For example, an application may feature a task (thread) to: read a temperature sensor, read an ambient light intensity sensor and another to keep a set of annunciators to indicate the state of the application.

Services that are available at the controller level will then be shared among the different tasks; logging being such a service. Physically the logging service might utilize a Serial port at the low level, transmitting and receiving octets. Different tasks then will queue their log message to a central resource protected in Ada terminology to serialize access to this channel. Another task controlling the serial port will then dequeue the messages and have them transmitted.

The cli service discussed earlier performed the inverse namely received octets on the serial port and executed the recognized command lines so received.

The key insight we gain is a design to implement shared resources.

9.3 Results

Sample logfile from the application sensorsob after almost a day:

23:20:47   -I-   [cb    ]  Ticker  81136
23:20:48   -I-   [sensor]   VBat=  1395 VBatF=  4.4 Temp=  1062 TempF=  63.3
23:20:48   -I-   [cb    ]  Ticker  81137
23:20:49   -I-   [cb    ]  Ticker  81138
23:20:50   -I-   [cb    ]  Ticker  81139
23:20:51   -I-   [cb    ]  Ticker  81140
23:20:52   -I-   [sensor]   VBat=  1419 VBatF=  4.5 Temp=  1059 TempF=  62.3
23:20:52   -I-   [cb    ]  Ticker  81141
23:20:53   -I-   [cb    ]  Ticker  81142
23:20:54   -I-   [cb    ]  Ticker  81143
23:20:55   -I-   [cb    ]  Ticker  81144
23:20:56   -I-   [sensor]   VBat=  1393 VBatF=  4.4 Temp=  1060 TempF=  62.6

Analyzing the above the difference is the timestamp (from the RTC) and the ticker is:

timestamp counter diff
1087 1053 34
52836 51016 1820
52860 51039 1821
84037 81126 2911
84058 81146 2912
91024 87866 3158
91045 87886 3159

As the above table indicates the RTC (the column timestamp) appears to be ahead of the timer interrupts. In fact over a day, the difference is approaching 1 hour!

9.4 Development Insights

Hard realtime applications forces us to analyze the performance aspects very carefully. By necessity computations that can be tightly bound and predictable may be implemented in ISRs while more lengthy operations such as ADC or unpredictable operations like convergence in some computation are not appropriate for ISRs.

In this projectlet, the language runtime provided Ada.Synchronous_Task_Control is utilized as a synchronization mechanism. The ISR sets the event to true and the application can wait for this before performing one of the expensive operations. In a general purpose RTOS (Real Time Operating System) such as Zephyr the analog might be event flags.

9.4.1 Inter task messaging

The interface provided to the Logging facility can be generalized as an inter task messaging facility. Ada tasks on a platform such as linux support such messaging; in the embedded profiles we are using, every task can have its own message queue in the above pattern.