Five Hardware Timers Example

Here is a small class definition that provides basic access to the Arduino Nano 33 BLE 33's nRF52840 hardware timers. Based on looking at the timer registers, Timer1 is the only timer used by a default empty sketch. I'm sure various Arduino functions engage timers, so we need to make sure our selected timers don't conflict with features being used.

Even though I can see that Timer1 was being used (15 minute interval), I took it in this example and blink one Nano 33 BLE LED for each hardware timer.

I'm interested any anyone's experience with what Arduino/MbedOS features use what hardware timers. For now it's going to be trial and error for me.

//
// Created by B Bryant on 9/15/2021
//
// Access the five nRF52840 32-bit hardware timers.
// User must ensure selected hardware timers do not conflict with application
// utilized MBED OS features or other library timer usage.
//
// For each timer used:
//  - Call setupTimer() to set initial period and callback function.
//  - Call startTimer() and stopTimer() as needed.
//  - Use updatePeriod() to change period of a running timer.
//    updatePeriod() can be called from within the user callback.
//
// Note that the callback provided to setupTimer() is called by the timer
// interrupt service routine (ISR), so do not call resources that depend on
// interrupts. Also, be sure callback execution time will not exceed the timer
// period, or the system will lock up.
//
// Although array bounds are managed using BBTimerIdType indexes,
// there is no explicit parameter, bounds, or nullptr error checking.

#pragma once
#include <nrf_timer.h>

typedef enum {
	BB_TIMER0 = 0,
	BB_TIMER1,
	BB_TIMER2,
	BB_TIMER3,
	BB_TIMER4,
	NUM_TIMERS
} BBTimerIdType;

typedef void (*TimerCallback)();

class BBTimer
{
public: // members

private: // Static, per class, members
	static NRF_TIMER_Type* const NRF_TIMER_LIST[NUM_TIMERS];
	static const IRQn_Type       IRQ_LIST[NUM_TIMERS];
	static const void*           ISR_LIST[NUM_TIMERS];
	static BBTimer*              timer_object[NUM_TIMERS];

private: // per object members
	BBTimerIdType   timer_id;
	NRF_TIMER_Type* nrf_timer;
	IRQn_Type       timer_irq;
	TimerCallback   isr_callback;

public: // methods
	/// BBTimer - Select hardware timer.  May construct at global scope.
	/// No system calls in constructor. OS may overwrite vectors after global
	/// constructors, so wait for setup() to call NVIC_SetVector.
	explicit BBTimer(BBTimerIdType id){
		timer_id  = id;
		nrf_timer = NRF_TIMER_LIST[timer_id];
		timer_irq = IRQ_LIST[timer_id];
		isr_callback = nullptr;

		timer_object[timer_id] = this;  // static object enables ISR to find this instance
  }

	/// setupTimer - Set timer period.  The provided callback function will be
	/// called periodically at the timer period after start() is called.
	/// Call setupTimer() in or after setup().
	void setupTimer(unsigned int period_us, TimerCallback callback)
	{
		// save pointer to callback function to be called by timer ISR
		isr_callback = callback;

		// Initialize hardware timer's registers
		nrf_timer->TASKS_STOP  = 1;  // Stop counter, just in case already running
		nrf_timer->TASKS_CLEAR = 1;  // counter to zero

		nrf_timer->BITMODE   = 3UL;       // 32 bit
		nrf_timer->MODE      = 0UL;       // timer, not counter
		nrf_timer->PRESCALER = 4UL;       // freq = 16Mhz / 2^prescaler = 1Mhz
		nrf_timer->CC[0]     = period_us; // Counter is compared to this
		nrf_timer->INTENSET  = 1UL << TIMER_INTENSET_COMPARE0_Pos;     // interrupt on compare event.
		nrf_timer->SHORTS    = 1UL << TIMER_SHORTS_COMPARE0_CLEAR_Pos; // clear counter on compare event.

		// It's also possible to add a SHORT to STOP counter upon compare.
		// I leave counter running to keep period constant. The downside is that
		// the ISR must complete before period expires, or it will lock up.

		// Point to one of the five static ISR handlers, which will call this
		// object's myObjectISR() via the static timer_object list.
		NVIC_SetVector(timer_irq, (uint32_t)ISR_LIST[timer_id]);
	}

	/// updatePeriod - Change the period of a setup, started, or stopped timer.
	/// May be called from the callback function.
	/// Must call setupTimer(), once, before using updatePeriod().
	inline void updatePeriod(unsigned int new_period_us) const
	{
		nrf_timer->CC[0] = new_period_us;
	}

	inline void timerStart() const
	{
		NVIC_EnableIRQ(timer_irq);
		nrf_timer->TASKS_START = 1;
	}

	inline void timerStop() const
	{
		NVIC_DisableIRQ(timer_irq);
		nrf_timer->TASKS_STOP = 1;
	}

private: // methods
	static void timer0Isr()
	{
		NVIC_DisableIRQ(IRQ_LIST[BB_TIMER0]);
		BBTimer::timer_object[BB_TIMER0]->instanceIsr();
		NVIC_EnableIRQ(IRQ_LIST[BB_TIMER0]);
	}
	static void timer1Isr()
	{
		NVIC_DisableIRQ(IRQ_LIST[BB_TIMER1]);
		BBTimer::timer_object[BB_TIMER1]->instanceIsr();
		NVIC_EnableIRQ(IRQ_LIST[BB_TIMER1]);
	}
	static void timer2Isr()
	{
		NVIC_DisableIRQ(IRQ_LIST[BB_TIMER2]);
		BBTimer::timer_object[BB_TIMER2]->instanceIsr();
		NVIC_EnableIRQ(IRQ_LIST[BB_TIMER2]);
	}

	static void timer3Isr()
	{
		NVIC_DisableIRQ(IRQ_LIST[BB_TIMER3]);
		BBTimer::timer_object[BB_TIMER3]->instanceIsr();
		NVIC_EnableIRQ(IRQ_LIST[BB_TIMER3]);
	}

	static void timer4Isr()
	{
		NVIC_DisableIRQ(IRQ_LIST[BB_TIMER4]);
		BBTimer::timer_object[BB_TIMER4]->instanceIsr();
		NVIC_EnableIRQ(IRQ_LIST[BB_TIMER4]);
	}

	/// instanceIsr - called by static ISR after IRQ disabled.
	/// Provides ISR access to BBTimer instance member variables.
	void instanceIsr()
	{
		if (nrf_timer->EVENTS_COMPARE[0] == 1) // make sure this is a compare event
		{
			nrf_timer->EVENTS_COMPARE[0] = 0; // clear event flag
			// Counter is cleared automatically via SHORTS

			isr_callback(); // caller's callback function
		}
	}
};

/// Declare and initialize Static BBTimer class members
BBTimer* BBTimer::timer_object[NUM_TIMERS] = {
		nullptr, nullptr, nullptr, nullptr, nullptr
};

// const pointers to non const register structures
NRF_TIMER_Type* const BBTimer::NRF_TIMER_LIST[NUM_TIMERS] = {
		NRF_TIMER0, NRF_TIMER1, NRF_TIMER2, NRF_TIMER3, NRF_TIMER4
};

const IRQn_Type BBTimer::IRQ_LIST[NUM_TIMERS] = {
		TIMER0_IRQn, TIMER1_IRQn, TIMER2_IRQn, TIMER3_IRQn, TIMER4_IRQn
};

const void* BBTimer::ISR_LIST[NUM_TIMERS] = {
		(void*)BBTimer::timer0Isr, (void*)BBTimer::timer1Isr,
		(void*)BBTimer::timer2Isr, (void*)BBTimer::timer3Isr,
		(void*)BBTimer::timer4Isr
};


#include <Arduino.h>
#include "BBTimer.hpp"

// Example using BBTimer class to run five hardware timers on Nano33BLE.  
// See BBTimer.hpp for usage details. 
// Include BBTimer.hpp header within project folder. 

// Construct five timers.  Can construct a global scope, or within setup(), or static in loop().
BBTimer my_t0(BB_TIMER0);
BBTimer my_t1(BB_TIMER1);
BBTimer my_t2(BB_TIMER2);
BBTimer my_t3(BB_TIMER3);
BBTimer my_t4(BB_TIMER4);

// global logicals for example synchronization between timer callback functions and loop().
bool red_on = false;
bool green_on = false;
bool blue_on = false;

// One callback for each timer.  
void t0Callback()
{
	static bool toggle = true;
	digitalWrite(LED_BUILTIN, toggle ? HIGH : LOW);
	toggle = !toggle;

	// example changing period from within callback
	static uint32_t period = 10000;
	period += 10000;
	if (period > 5e5) period = 10000;
	my_t0.updatePeriod(period);
}

void t1Callback()
{
	static bool toggle = true;

	digitalWrite(LED_POWER, toggle ? HIGH : LOW);
	toggle = !toggle;
}

void t2Callback()
{
	red_on = !red_on;
}

void t3Callback()
{
	green_on = !green_on;
}

void t4Callback()
{
	blue_on = !blue_on;
}


void setup() {

	pinMode(LED_BUILTIN, OUTPUT);

	my_t0.setupTimer(500000, t0Callback);
	my_t0.timerStart();

	my_t1.setupTimer(450000, t1Callback);
	my_t1.timerStart();

	my_t2.setupTimer(420000, t2Callback);
	my_t2.timerStart();

	my_t3.setupTimer(390000, t3Callback);
	my_t3.timerStart();

	my_t4.setupTimer(340000, t4Callback);
	my_t4.timerStart();
}

void loop() {

	// these don't like to be set from inside a callback.
	digitalWrite(LED_RED, red_on ? LOW : HIGH);
	digitalWrite(LED_GREEN, green_on ? LOW : HIGH);
	digitalWrite(LED_BLUE, blue_on ? LOW : HIGH);
}
1 Like

There have been issues with Timer1 and recent core versions.
https://github.com/khoih-prog/NRF52_MBED_TimerInterrupt/issues/6

Interesting. Thanks for the pointer. I had noticed that Timer1 was in use, and the normal approach to linking to its ISR did not work for it. See: Mbed OS ISR linkage on Arduino 33 BLE

Arduino and/or Mbed are definitely using Timer1 now. You can still take it, but certainly there will be a loss of some functionality. The example in this post uses Timer1.

I've also noticed that BLE functionality ties up Timer0, Timer1, and Timer2. So Timer3 and Timer4 are the only safe ones if you are running a BLE application.

From https://forum.arduino.cc/t/mbed-os-isr-linkage-on-arduino-33-ble/898811

TIMER3 and TIMER4 work as designed, but TIMER1_IRQHandler_v would not run until I called NVIC_SetVector.

Arduino and/or Mbed are definitely using Timer1 now. You can still take it, but certainly there will be a loss of some functionality.

Unclear from your statement if the Timer1 interrupt is available under some circumstances with the 2.0.0+ core.

So Timer3 and Timer4 are the only safe ones if you are running a BLE application.

Yes.

The hardware will definitely let you run Timer1 interrupts if you setup the interrupt vector yourself - as in the BBTimer class I posted. I'm just not sure what you lose by using Timer1 for your own purpose. I know you lose BLE functionality. Timer3 and Timer4 seem like the only generally safe to use timers, whether you use NRF52MBEDTimerInterrupt or something like the BBTimer class I posted.

I found that the Timer1 interrupt is not working when using the Weak link technique NRF52_MBED_TimerInterrupt uses to grab the interrupt vector. Something in the 2.0.0+ core is taking the Timer1 interrupt vector. The BBTimer class uses NVIC_SetVector to set the vector, which works for all five Timers. You just have to accept that the core was using Timer1 for something, that will no longer work. I know BLE won't work if you use Timer1 for your own purposes. I don't know what else is lost by taking Timer1. The example I posted today processes interrupts from all five timers. So it's available at the cost of losing some core functionality.

I like and will try your your forceful approach using NVIC to grab and own the HW Timer.

But I suggest to get and store the original core ISR's address to restore whenever we need to remove or disable a Timer.

But for not losing the core functionality of barely used Timer such as Timer1, we can try to chain the ISRs together by using NVIC_GetVector((IRQn_Type)int_num) to get the core's original core ISR then calling it (core's original core ISR) inside your void t1Callback().

I'll try use this way in my libraries for reclaiming Timer1 in mbed_nano core v2.0.0+ and appreciate if you can suggest and test if there would be any issue.

Thanks for the feedback. My first impulse to your chaining suggestion was "Oh yea, why not?".
But after thinking about it a minute, I realized that if we chained like that, we would be calling that core ISR at our timer period. So if you implement a quick 150 micro second period ISR of your own, it would be calling the core ISR at that same blistering rate. Probably not good for something the core planned to do every 30 minutes. I'd skip the chaining.

1 Like

I think this is the much better strategy if we know the period the core original ISR is doing (every 30mins).

Another way is that if we can read the Timer regs and know what is the actual core's ISR period, then we'll check and call the core's ISR correspondingly to not losing the original functionality. What do you think ?

I have noticed that the 'core's ISR' will change Timer1's CC0 and CC1 registers. If you read the Timer1 CC regs and call the 'core's ISR' at its expected time, the core's ISR will overwrite your CC0 register and change your Timer1's period. So I don't think there is an option to keep the core's Timer1 ISR running in parallel with your own. I recommend sticking with Timer3 and Timer4 which seem to be completely free for us to use. If more than two hardware timers are required, and if you were willing to add the complexity of managing multiple ISR periods on one hardware timer, I recommend doing it with Timer3 or Timer4 and using additional CC registers to track the various hardware interrupt rates. Of course, if an application works with multiples of a base timer frequency, I'd stick with NRF52_MBED_TimerInterrupt, which still seems to work great on Timer3 and Timer4.

I've been thinking about how I would manage multiple timer periods with one hardware timer using more of it's CC registers with individual compare events. If we omit TIMER_SHORTS_COMPAREn_CLEAR_Pos, we can have a different hardware interrupt period on each of the CC registers. Each ISR would have to update it's own CC register by adding the current period to it. The top level ISR handler would need to check to see which compare event caused the interrupt to select which ISR to call. That gives you six different hardware interrupts for Timer3 and six for Timer4. I haven't thought it all the way through though. If you are running a microsecond clock, the 32 bit counter will overflow every 1.2 hours. So, it seems that we need to clear the counter occasionally. I'm thinking of either clearing on the longest period timer, or whenever it passes half of full count. That implies sorting timers, or at least checking for the longest period. And when you clear the counter, each CC register will have to be updated. There is probably a cool and efficient way to do this, and I just have not thought it through yet.

1 Like

I'm really impressed with the investigation you've done on Timer1. Do we have access to the code of that core ISR (or it's still in the hidden .a part of the core) to know what's its feature and why it must be designed so aggressively, from mbed_portenta core v2.0.0

I'm currently OK so far with Timer3 and Timer4. But as Timer is rare, even one more is better for users.

Many thanks,

Thank you. I don't know if we have access to the code. I've done a little searching, but have not found it. I am assuming that it is in the .a part.

I do know that if I take Timer1, as in my example, it breaks BLE functionality. So my current approach is to user Timer1 if I want a third timer and I am not using BLE, and leave Timer1 alone if I'm using BLE.

Thanks again and good luck with future modifications to NRF52_MBED_TimerInterrupt.