Mbed OS ISR linkage on Arduino 33 BLE

What’s going on with Arduino 33 BLE interrupt service routine specification? Or - How do I specify an interrupt service routine for Arduino33BLE (nRF52840) peripherals? For example, if I want to program up some timer interrupts, in addition to correctly setting up the TIMERn registers, I need to define an ISR. First, the easy stuff.

  1. Define the ISR using extern "C". No surprise here. This is common across operating systems. When the OS vectors to an interrupt service routine, it does not prepare the stack with things needed by C++ functions.

  2. ISR’s don’t get parameters. When the hardware event takes place to trigger the ISR, there are no software parameters to pass the ISR.

So the ISR needs to look like this:

extern "C" void myISR(void) {
}

  1. Next comes the question of how to I link my ISR to the hardware event of interest. In this case, it’s a TIMERn interrupt. After some stumbling and digging I’ve found two approaches.

Approach 1 - Use an Mbed OS predefined ISR name. In the case of the nRF timers the predefined names are TIMER0_IRQHandler_v thru TIMER4_IRQHandler_v. Apparently these are EXPORTED by Mbed OS as WEAK links. Basically, Mbed OS provides default interrupt handlers with those predefined names, and the linker will use your version if you provide it with the predefined name. So your Timer1 ISR should be declared as:

extern “C” void TIMER1_IRQHandler_v() {
}

Here are two references on what’s going on with WEAK links.
https://developer.arm.com/documentation/dui0489/i/directives-reference/export-or-global
https://developer.arm.com/documentation/dui0474/i/image-structure-and-generation/about-weak-references-and-definitions

I have to admit that I was mystified when I studied my first TimerISR example and found an ISR named this way, without some corresponding call or address mapping to tell Mbed OS how to get to the locally defined ISR. It’s the linker and WEAK links! Cool.

You can find these WEAK exported names in the open source code available on GitHub. I found them in startup_NRF52840.s. You can search on GitHub - ARMmbed/mbed-os: Arm Mbed OS is a platform operating system designed for the internet of thin. I searched for TIMER0_IRQHandler_v since I knew that was a valid ISR name.

Here is a TIMER4 example that works for me on an Arduino33BLE. The setupTimer function was modeled on an example from profrob. Thanks.

#include <nrf_timer.h>

// Set both of these to same TIMERn - You can try NRF_TIMER0 thru NRF_TIMER4.
NRF_TIMER_Type* my_timer     = NRF_TIMER4;
IRQn_Type       my_timer_irq = TIMER4_IRQn;

// Rename this to match the selected timer to override WEAK link.
// Name does not matter if you use NVIC_SetVector
extern "C" void TIMER4_IRQHandler_v() {
//extern "C" void myIsrName() {
  static bool toggle = true;

  if (my_timer->EVENTS_COMPARE[0] == 1)
  {   
    my_timer->EVENTS_COMPARE[0] = 0;
    
    digitalWrite(13, toggle ? HIGH : LOW);
    toggle = !toggle;
  }
}

// Serial.print one line of status for given timer.  
void showTimerStatus(NRF_TIMER_Type* nrf_timer){

  int num_cc_regs = 4;
  if (nrf_timer == NRF_TIMER3 || nrf_timer == NRF_TIMER4) {
    num_cc_regs = 6; // Timers 3 and 4 have 6 cc regs.
  }

  Serial.print("Local ISR Addr:");
  Serial.print((uint32_t) TIMER4_IRQHandler_v, 16); // EDIT TO MACH ISR NAME
//  Serial.print((uint32_t) myIsrName, 16); // EDIT TO MACH ISR NAME

  Serial.print(" NVIC Addr:"); // hopefully same as local addr
  Serial.print(NVIC_GetVector((IRQn_Type)my_timer_irq), 16);

	Serial.print(" Mode:");
	Serial.print((int)nrf_timer_mode_get(nrf_timer));

  Serial.print(" BitMode:");
  Serial.print((int)nrf_timer_bit_width_get(nrf_timer));

	Serial.print(" INTENSET Bits: ");
	for (int i = 0; i < num_cc_regs; i++)
	{
 		bool enset = nrf_timer_int_enable_check(nrf_timer, (nrf_timer_cc_channel_t)1<<(16+i));
		Serial.print(enset ? 1 : 0);
		Serial.print(" ");
	}

	Serial.print("  CC Regs(0x): ");
	for (int i = 0; i < num_cc_regs; i++)
	{
		uint32_t cc = nrf_timer_cc_read(nrf_timer, (nrf_timer_cc_channel_t)i);
		Serial.print(cc,16);
		Serial.print(" ");
	}
	Serial.println(" ");
}

void setupTimer(NRF_TIMER_Type* nrf_timer, unsigned int period_us)
{
  nrf_timer->TASKS_STOP  = 1;  // just in case it's running
  nrf_timer->TASKS_CLEAR = 1;

  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;
  nrf_timer->INTENSET = 1UL << TIMER_INTENSET_COMPARE0_Pos;
  nrf_timer->SHORTS = 1UL << TIMER_SHORTS_COMPARE0_CLEAR_Pos;
 
  // Only need NVIC_SetVector for TIMER1 or custom ISR name
//  NVIC_SetVector(TIMER1_IRQn, (uint32_t)TIMER1_IRQHandler_v);
//  NVIC_SetVector(my_timer_irq, (uint32_t)myIsrName);
 
  NVIC_EnableIRQ(my_timer_irq);
  nrf_timer->TASKS_START = 1;
}

void setup() {
  Serial.begin(9600);
	while (Serial.available() == 0){} // wait for char from terminal
	while (Serial.read() != -1);     // clear buffer
	Serial.println("OK - Begin");

  pinMode(13, OUTPUT); // built in LED

  // Look at timer registers before doing anything
  Serial.println("Before setupTimer");
  showTimerStatus(my_timer);

  // Setup timer registers and start timer
  setupTimer(my_timer, 500000);

  // Look at timer registers after setup
  Serial.println("After setupTimer");
  showTimerStatus(my_timer);
}

void loop() {

}

Approach 2 - Strangely enough, the above example worked for TIMER0, TIMER2, TIMER3 and TIMER4 but did not work for TIMER1. That lead me to find this alternate approach of setting the link at run time. We are not limited to the predefined names! This is what I expected to find to begin with, and was sidetracked by the pre-defined names. It still takes a little detective work. You have to find the interrupt number of your desired hardware interrupt. I found them in nrf52840.h. In the case of TIMER1, it’s TIMER1_IRQn = 9. Once you have that, you can do this:
NVIC_SetVector((IRQn_Type)9, (uint32_t)TIMER1_IRQHandler_v);
This sets the vector to my local version of TIMER1_IRQHandler_v, but this works with any ISR name. So you can stick with the predefined names, or use any name you like.

I assume I had to do this because some other Mbed OS or Arduino module already defined a link to its own TIMER1_IRQHandler_v. My TIMER1_IRQHandler_v was not getting called. I came to this conclusion after checking the TIMER registers before my code made any modifications, and noticed that TIMER1 already had its INTEN bit set. Some other Mbed OS or Arduino module is using TIMER1. I have no idea what. I can see that it’s set to generate a TIMER1 interrupt about every 30 minutes. Generally it’s not a good thing to simply take a resource (timer1 in this case) from the OS, but I did it anyway as part of this experiment. The following change to the above code worked as desired, causing my ISR to run as specified by the TIMER registers.

One final observation. The code excerpts above are from a project where I use three hardware timers. I’m using TIMER1, TIMER3, and TIMER4. I’ve read in other places that Mbed OS uses TIMER0 and TIMER2, although I’ve also seen examples of folks using TIMER2 in their application. Another thing that led me to the second approach was that I checked the addresses of the ISR’s with
NVIC_GetVector((IRQn_Type)int_num);
I could see the TIMER3 and TIMER4 address vectors were very close to each other (next to each other in my code), but TIMER1 was completely different, leading me to the conclusion that the linker was not using my version of TIMER1_IRQHandler_v.

Clearly a little study of the NVIC functionality might be helpful. I haven’t found a good reference for this — but I haven’t tried real hard either.

I feel like I’ve got to be reinventing the wheel here. There must be a clear explanation of this someplace else.

I welcome feedback on what I might have wrong in this, or where I’m making bad assumptions. Similarly, I welcome any pointers to documentation that might guide me through what’s really going on here, or an NVIC overview, or what TIMERs Mbed OS and Arduino are using and what TIMERs are safe for applications to use. I’ve got my three timers working but I still have more questions than answers.

Thanks,
BB

Have a look at my NRF52_MBED_TimerInterrupt Library and

Yes I have, and I have NRF52_MBED_TimerInterrupt.h open right now. I've used it with a single hardware timer on other projects. It's very nice. I really like the multiple timers that can be associated with each hardware timer. Really nice job. When I tried to use it for three hardware timers, it gave me the behavior I described in this post. TIMER3 and TIMER4 work as designed, but TIMER1_IRQHandler_v would not run until I called NVIC_SetVector. The other reason I started looking a little deeper, is that I want to adjust the hardware timer period from within the ISR. Basically a setFrequency() without elements of enableTimer(). I thought that would be an easy addition, and I got sidetracked on the TIMER1_IRQHandler_v issue. Your example has been very helpful. Thanks.