Which timer to use for AVR preemptive multitasking

Hi All, I have implemented a preemptive multitasking library for AVR boards and I want to share it with the community but not sure which timer interrupt I should use to be most compatible with hobby projects that use popular libraries that rely on timer interrupts. On Uno as I understand there are only 3 timers, so my options are Timer0 CompA/CompB or Timer1/2 or make it somehow configurable, which is tricky since I need to provide ISR implementation or let the user provide their own… Unfortunately there’s no hook for task switching in Timer0… any recommendations how to make it more reusable and compatible with typical hobby needs with respect to timer interrupts? thank you!

Hi @glutio

The FreeRTOS for AVR boards use the microcontroller's internal watchdog timer (WDT), driven by its own dedicated 128kHz timer. The timer isn't that accurate, but good enough for most software timing purposes.

Using the WDT has the added benefit of freeing up timers 0, 1 and 2, etc... for the Arduino timing functions and other libraries.

1 Like

We can already multitask Arduinos without the overhead of a slicer, thank you. It does take learning but works great even with very limited RAM.

2 Likes

Good, and yet I am going to share it :slight_smile:

I know the thrill of doing it. Once built a real time operative for the Zilog Z80. Preemptive, priority, time slice, memory management....
Using common good practise when coding those tiny microcontrollers has been good enough so far.

Mine is simple, it’s not trying to be an rtos, it’s a task switcher, you have your runTask() and SyncVar<> to synchronize access for simple types (ie all operators are overloaded to add noInterrupts/interrupts), this is inefficient but simple.

SyncVar<bool> _on(false);

/* templated argument, instead of (void*) */
void On(int) {
  while(1) {
      if (_on) { 
          digitalWrite(LED_BUILTIN, HIGH); delay(500);
         _on = false;
      }
  }
}

void Off(int) {
  while(1) {
      if (!_on) { 
          digitalWrite(LED_BUILTIN, LOW); delay(500);
         _on = true;
      }
  }
}

void setup() {
   noInterrupts();
   setupTasks();
   runTask(On, 0 /* unused arg */);
   runTask(Off, 0);
   interrupts();
}

I started building a hobby project and quickly fun turned into tedious maintenance of the state so I ended up implementing event delegates and multitasking but not anything like an rtos. Thanks for feedback!

1 Like

Impressive anyway!

Timer0 (overflow) is used to provide the Arduino millis() function, so it's probably the least likely to be re-used for other purposes. You can attach an interrupt to one of the compare channels, and get an interrupt every 1024us without interfering with the millis timer, and I think even without interfering with PWM (since the timer is configured to wrap around, any compare value will interrupt at that 1024us interval, regardless of whether it fiddles with a pin value.)

The AVR has a pretty unfortunate ratio of context (37 bytes) to data memory (2k for m328), when it comes to implementing any sort of full multitasking....

1 Like

An example of the above:

// set up 1024 usec interrupt

volatile unsigned long currentMillis;
void setup() {
  // Timer0 is already used for millis() - we'll just interrupt somewhere
  // in the middle and call the "Compare A" function below
  OCR0A = 0xAF;
  TIMSK0 |= _BV(OCIE0A);
  Serial.begin(115200);
}

// Interrupt is called about once a ms
ISR(TIMER0_COMPA_vect) 
{
  currentMillis = millis();
}

void loop() {
// get protected copy of time
  noInterrupts();
  unsigned long time=currentMillis;
  interrupts();
Serial.println(time);
delay(100);
}
1 Like

I must be missing something. If it gets into this while(1) loop, what mechanism do you use to pre-emptively terminate it ?

Correct, in this exmple the task never terminates because it is in an infinite loop. The task will terminate (be removed from the tasks list) if it exits the task function or someone calls killTask(); But the preemption itself happens via the timer interrupt. So even though there is an infinite loop each task gets a chance to run. Currently my implementation switches tasks every 1ms for simplicity.

Well you don't make big claims so okay, but for what you're doing now the task should be able to kill itself by sending a signal. 1ms is 16000 cycles, to me that's a LOT of latency! It will not run a motor well but you made no claim that it can.

IIf you can make your task exits non-blocking, another task can use the otherwise wasted cycles. A small task might only take < 100 cycles so this is an area that you can make a major speed gain.

Thanks, yes I implemented both interrupt and yield so a task that does something and waits for next iteration can yield control to other tasks. In the example above delay() internally calls yield() which calls the task switching. I am also working on SAMD port which so far works only for interrupt or yield but not together :frowning: but the AVR implementation already works, now just need to decide which timer interrupt to use

[quote="glutio, post:13, topic:1131798"]
does something and waits for next iteration
[/quote]So much for efficient when 16000 cycles is an iteration.

westfw gave you a perfect answer on timing 1024 usec interval interrupts.

What works with far less control is slicing the tasks into short steps, often through use of state machines. The task never hogs, all tasks run together with Input Tasks able to sense with the least latency. I'd hate to waste the 85 cycle interrupt overhead to run a routine < 50 cycle task. A fat slice to me is makung a 10-bit > 1600 cycle analog read.

You can't use yield and also pre-emptive tasking with milli intervals?
If your interrupt sets a flag to run the next then why can't the ending task?

Yes, good question :slight_smile: some stack corruption, I ordered Arduino zero with EDBG to help debug this, already messed up 3 smaller boards trying to solder SWD wires, I am a beginner at this…

As far as efficiency, that’s not my goal, instead I want to provide a simple api to add multitasking as a different way to break down your sketch. It came out of my hobby needs where separating it into tasks simplified code and state management. So it’s a balance between simplicity and efficiency. Thanks for feedback!

Geeze. don't be such a glutton for punishment! Buy a board with an SWD connector, or at least a footprint for one...
Microchip Curiosity Nano SAMD21 (builtin SWD debugger, too!)
Adafruit Metro M0
Sparkfun Samd21 dev board
For that matter, a genuine Arduino Zero has a debug chip as well.


1ms is 16000 cycles, to me that's a LOT of latency!

1ms is a lot of latency for hardware events, but is pretty small as the interval for preemption (aka "Time Slice") by an RTOS scheduler. Windows claims to be using ~20ms, as did ye olde mainframes (sometimes 1/60s to match up with line frequency, probably for historical reasons.)

I might make one task that blinks a led on-off-done and another task to trigger it and if that needs modification, a task to do that. I will make tasks to reduce indent levels in tasks and reduce debugging time. I am no fan to big knot code though in the beginning over 40 years ago I did that a lot.

So far most code between delays runs in a few micros but scanning a 10x10 button matrix, I only check one button per pass through loop() so that other inputs can be polled too. 100 buttons takes about 2 ms plus debouncing millis time for pin state changes without blocking anything at all. Adding tasks to that is open and I use a loop counter task to tell me what load new code puts on the sketch. When I saw my example code running lopp() with a few small tasks run loop() at 67KHz, that changed how I thought about Arduino.

I'm working myself up to getting a couple of RPi Picos. 133MHz dual core AMD with beaucoup shared RAM for light lunch money.

How can we implement a 1ms timer then? I imagine that with 1ms scheduler users could write a task like

void timerTask() {
  while(1) {
     delay(1);
     // do stuff 
   }
}

delay() uses yield() internally so it basically becomes a coop multitasking and tasks can be switched faster than 1ms and the timer task can trigger close to 1ms even if there’s more tasks as long as they all yield..

I implemented 3 priority levels for tasks. Tasks in Priority 0 get 50% of cpu time, priority 2 35% and 3 the rest. So a high resolution timer task would be priority 0 with yield() while a regular task can run at priority 1 and a sleepy task priority 2. The question is if I use WDT which is a 16ms timer as I read, then how can users implement a timer task with less than 16ms delay? By yielding and making it effectively a coop multitasking? Maybe that’s a solution… thanks!

Bit 10 of micros flips every 1024 seconds. 32 bit mask is 0x00000400.

1 Like

By the way, this is the SAMD21 code issue repro. The code works if you comment out task switching trigger either in sysTickHook or in yield but if both are active then there's a hard fault. I think this is something ARM CortexM0+ related, not my code but I may be wrong too.. What is the best category to post it in to get some assistance in debugging it?

#define SSIZE 1024

void task() {
  while (1) {
    SerialUSB.println("task");
    yield();
    //delay(1000);
  }
}

struct Ctx {
  uint32_t r8;
  uint32_t r9;
  uint32_t r10;
  uint32_t r11;
  uint32_t r4;
  uint32_t r5;
  uint32_t r6;
  uint32_t r7;
  uint32_t r0;
  uint32_t r1;
  uint32_t r2;
  uint32_t r3;
  uint32_t r12;
  uint32_t lr;
  uint32_t pc;
  uint32_t psr;
};

struct TaskInfo {
  uint8_t* sp;
  uint8_t stack[SSIZE];
};

TaskInfo* _tasks[2];
volatile int _current_task = 0;
volatile bool _initialized = false;

void init_task(struct TaskInfo* taskInfo) {
  taskInfo->sp = &taskInfo->stack[SSIZE - 1];
  // 8 bytes align per ARM Cortex+ requirement when entering interrupt
  taskInfo->sp = (uint8_t*)((uintptr_t)taskInfo->sp & ~0x7);

  // clear registers
  for (unsigned i = 0; i < sizeof(Ctx); ++i) {
    *--taskInfo->sp = 0;
  }

  auto ctx = (Ctx*)taskInfo->sp;
  // compiler/architecture specific
  ctx->psr = 0x01000000;
  ctx->pc = (uintptr_t)task;
}

uint8_t* swap_stack(uint8_t* sp) {
  if (_initialized) {
    _tasks[_current_task]->sp = sp;
    _current_task = (_current_task + 1) % 2;
    sp = _tasks[_current_task]->sp;
  }
  return sp;
}

extern "C" {
  void __attribute__((naked)) PendSV_Handler() {
    noInterrupts();
    asm volatile("push {r4-r7}");
    asm volatile("mov r4,r8");
    asm volatile("mov r5,r9");
    asm volatile("mov r6,r10");
    asm volatile("mov r7,r11");
    asm volatile("push {r4-r7}");

    asm volatile("mov r0, sp");
    asm volatile("push {lr}");
    asm volatile("blx %0"
                 :
                 : "r"(swap_stack)
                 : "r0");
    asm volatile("mov r12, r0");
    asm volatile("pop {r0}");
    asm volatile("mov lr, r0");

    asm volatile("mov sp, r12");
    asm volatile("pop {r4-r7}");
    asm volatile("mov r8,r4");
    asm volatile("mov r9,r5");
    asm volatile("mov r10,r6");
    asm volatile("mov r11,r7");
    asm volatile("pop {r4-r7}");
    interrupts();
    asm volatile("bx lr");
  }

  int sysTickHook() {
    SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;  // comment this out
  }
}

void yield() {
  SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;  // comment this out
}

void setup() {
  SerialUSB.begin(115200);
  while (!SerialUSB)
    ;
  noInterrupts();
  _tasks[0] = new TaskInfo();
  _tasks[1] = new TaskInfo();
  init_task(_tasks[1]);
  _initialized = true;
  interrupts();
}

void loop() {
  SerialUSB.println("loop");
  yield();
  //delay(1000);
}

In the HardFault_Handler the stacked PC is 0xFFFFFFF8 ad LR is pointing to next instruction after the call to sysTickHook from SysTick_Handler. I have no idea why there's an issue. It works fine under IAR emulator but not on real hardware.