Efficient wait until event.

samm_flynn

Active member
I’m implementing a real-time control system on a Teensy 4.1 and wanted to keep the loop() function completely empty, handling all computation inside interrupts. To achieve this, I tried the following approach:
C++:
const uint32_t maxUnsignedLong = ~0UL;  // Maximum value of uint32_t
void setup()
{

}
void loop()
{
  delay(maxUnsignedLong);
}
However, after looking into the source code of delay.c , I found this :
C++:
// Wait for a number of milliseconds.  During this time, interrupts remain
// active, but the rest of your program becomes effectively stalled.  Usually
// delay() is used in very simple programs.  To achieve delay without waiting
// use millis() or elapsedMillis.  For shorter delay, use delayMicroseconds()
// or delayNanoseconds().
void delay(uint32_t msec)
{
    uint32_t start;

    if (msec == 0) return;
    start = micros();
    while (1) {
        while ((micros() - start) >= 1000) {
            if (--msec == 0) return;
            start += 1000;
        }
        yield();
    }
    // TODO...
}
To better understand its behaviour, I roughly translated this into Python:
Python:
import time

def delay(msec):
    if msec == 0:
        return
    start = time.monotonic_ns()  # Get time in nanoseconds
    while True:
        while (time.monotonic_ns() - start) >= 1_000_000:
            msec -= 1
            if msec == 0:
                return
            start += 1_000_000


delay(10000)
When I run this in Python, one CPU core maxes out for 10 seconds, which makes me wonder if the same thing happens on the Teensy.
Now to get around this is python one could do the following -

Python:
import time
import threading


def delay(msec):
    if msec == 0:
        return
    event = threading.Event()
    event.wait(msec / 1000)


delay(10000)
time.sleep(10000)

Does calling delay(maxUnsignedLong); cause a performance penalty by keeping the core busy? If so, is there a way to release the core while waiting, similar to time.sleep() or event.wait() in Python?

Thanks for taking the time to read my question!
 
asm volatile("wfi"); will put the CPU to sleep until an interrupt occurs. Just be aware if an unmasked interrupt never arrives, the CPU will never wake...
 
Thanks @jmarsh
follow up question -
Is this a good idea?
C++:
#include <IntervalTimer.h>
String inputString = "";
IntervalTimer myTimer;
// void yield()
// {
//
// }
void timerISR()
{
  yield();
}
void setup()
{
  myTimer.priority(255);
  myTimer.begin(timerISR, 1000);
  pinMode(LED_BUILTIN, OUTPUT);
}
void loop()
{
  asm volatile("wfi");
}
void serialEvent()
{
  digitalWrite(LED_BUILTIN, HIGH);
  int availableChars = Serial.available();
  if (availableChars > 0)
  {
    for (int i = 0; i < availableChars; i++)
    {
      char inChar = (char)Serial.read();
      inputString += inChar;
    }
    if (inputString.endsWith("\n"))
    {
      digitalWrite(LED_BUILTIN, LOW);
      inputString = "";
    }
  }
}
I am calling the yield() function, every 1 ms, with a very lowest priority. This way serial event works, Intend to use the serial input to set flags for the rest of the code. or calling yield from inside an interrupt is a bad idea over all?
 
I’m implementing a real-time control system on a Teensy 4.1 and wanted to keep the loop() function completely empty
But you didn't keep loop() empty, you put a delay into it which will stall the event handling. Why not simply use:
Code:
void loop()
{
}
 
But you didn't keep loop() empty, you put a delay into it which will stall the event handling. Why not simply use:
Code:
void loop()
{
}
@MarkT , could you please specify which event handling are you talking about? inside the source code for delay.c i found delay calls yield at 1 KHz,

which in turn calls the serialEvent. So even If put delay(infinity), serialEvent is called at 1 KHz, and interrupt callbacks takes priority, atleast that is my under standing, and if I don't put anything in void loop. It just loops over and over again consuming cpu cycles.

At least that is what I understand, I might be wrong(I probably am), Please help me understand.

Again my main goal is to reduce cpu usage.
 
I think it would be better to leave timerISR empty. yield() already gets called whenever loop() finishes (so it would be getting called twice each time the timer interrupts) and it's a bad idea to call it from an interrupt handler.
 
One other thing about wfi that I forgot to mention: by default it stops the systick counter, which means the value returned by millis() will not increase while the CPU is waiting.
 
As @jmarsh says, yield() is called once per loop() because Arduino's main() looks something like this:

Code:
void main(void) {
  setup();
  while(1) {
    loop();
    yield();
  }
}
 
by default it stops the systick counter,
What is the 'it' here? This? " asm volatile("wfi"); " ? That would stop it, unless the code (you wrote once?) to enable the sys_tick interrupt as a normal interrupt.

delay() counts on normal advance of micros()?
 
What is the 'it' here?
The "wfi" opcode/instruction, because systick is a function of the ARM CPU and by default wfi causes the CPU clock to stop. So the systick counter will never reach its threshold and trigger the interrupt, unless the extra code has configured the IMXRT to keep the CPU clock running during sleep.

millis() / micros() / delay() all rely on the systick interrupt handler incrementing the systick_millis_count variable once every millisecond.
If the CPU goes to sleep (using wfi) between calls to millis(), the difference between the values won't be an accurate measurement of the time passed because from the CPU's perspective, time was halted (unless the CPU clock was configured to stay running).
 
@MarkT , could you please specify which event handling are you talking about? inside the source code for delay.c i found delay calls yield at 1 KHz,
In the loop in main that calls loop() there is a call to yield()... In the Arduino this does serial event handling, I presume Teensy framework does similar in yield?
 
If you're interested in another method, I'm attaching a cooperative OS and example sketch. I call it an "OS" rather than a scheduler because it includes a timer tick for a non-blocking os_delay(), and also has inter-task communication via mailboxes or queues, with timeouts. yield() is overridden to be a cooperative task switch. I usually don't call yield() directly, but any call to an OS function with a timeout implies yield(), as you can see by looking at the source.

I have used this in many control systems where I want periodic i/o and control tasks and "background" communication via UART or Ethernet, which spend the great majority of time waiting. You can have a task or tasks that process events similarly to EventHandler. I find this model easier to think about and design for than the EventHandler, mostly because I've used it for a long time, and I'm used to the idea of tasks signaling each other. The example has two instances of a print task that prints to the serial monitor with a delay, and a main task that executes every 1024 ms and prints the number of task swaps over that time.

This works on Teensy LC, 3.x, 4.x, and Micromod, and also on Adafruit, Sparkfun, and Seeed Studio boards with ARM Cortex M0/M3/M4/M7. The Adafruit and Seeed Arduino cores have a hook for the SysTick handler, so you can use that rather than an IntervalTimer as in this example.

The task creation and switch take from the Zilch cooperative scheduler by user @duff
 

Attachments

  • FsKernel.cpp
    7.7 KB · Views: 15
  • FsKernel.h
    3 KB · Views: 15
  • FsOS.cpp
    14 KB · Views: 13
  • FsOS.h
    4.3 KB · Views: 11
  • fsos_0.ino
    1.9 KB · Views: 16
  • ISQueue.c
    3.6 KB · Views: 12
  • ISQueue.h
    1.8 KB · Views: 14
Back
Top