Software/Arduino Core/Basics

Timing and Interrupts

Timing functions on the Neuro N6, their interaction with the FreeRTOS scheduler, and the external interrupt constraints.

beginner3 min read

millis(), micros(), delay() and delayMicroseconds() behave as on other Arduino boards. The core runs FreeRTOS, and loop() executes as one task among several. This affects how blocking calls interact with the camera pipeline.

Time sources

FunctionSourceResolutionNotes
millis()FreeRTOS tick1 msWraps after approximately 49 days
micros()Cortex-M cycle counter1 usDerived from the 800 MHz core clock
delay(ms)RTOS sleep1 msYields to other tasks
delayMicroseconds(us)Busy wait1 usDoes not yield

The HAL tick runs on TIM16 rather than SysTick, which the RTOS owns. HAL_GetTick() and millis() return the same value.

delay and the scheduler

delay() suspends the calling task and allows other tasks to run: the camera capture task, the USB task and, when present, the WiFi tasks. A delay(1000) in loop() therefore does not interrupt video streaming. The camera-only example remains in delay(1000) indefinitely while frames continue to stream.

A long computation in loop() also does not stop video, because the capture and USB tasks run at higher priority and pre-empt loop() when a frame is ready.

Idle task and current

When no task is ready the RTOS idle task runs. By default it spins, which at 800 MHz draws roughly 100 mA while the sketch waits on a slow peripheral. Linking the power save library replaces the idle hook with a wait-for-interrupt instruction. Sketches that do not link it are unaffected. See Low Power.

Non-blocking timing

The standard pattern of comparing millis() against a stored value is unchanged:

cpp
const unsigned long interval = 1000;
unsigned long previous = 0;
int state = HIGH;

void loop() {
  unsigned long now = millis();
  if (now - previous >= interval) {
    previous = now;
    state = (state == LOW) ? HIGH : LOW;
    digitalWrite(LED_BUILTIN, state);
  }
}

Additional tasks

A sketch may create FreeRTOS tasks in setup(). A task with no real time deadline is given a priority below loop() and uses vTaskDelay for waiting.

cpp
extern "C" {
  #include "FreeRTOS.h"
  #include "task.h"
}

static void blinkTask(void *) {
  for (;;) {
    digitalToggle(LEDB);
    vTaskDelay(pdMS_TO_TICKS(250));
  }
}

void setup() {
  xTaskCreate(blinkTask, "blink", 1024 / sizeof(StackType_t), NULL,
              tskIDLE_PRIORITY + 1, NULL);
}

FreeRTOS Under the Hood lists the core's tasks and priorities.

External interrupts

cpp
volatile uint32_t edges = 0;

void onEdge() { edges++; }

void setup() {
  pinMode(D5, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(D5), onEdge, FALLING);
}

Modes are RISING, FALLING and CHANGE. detachInterrupt(pin) removes a handler. noInterrupts() and interrupts() mask and unmask globally; the camera and USB paths depend on interrupts, so masked sections are kept to a few instructions.

Pins with the same number within their ports share an interrupt line. On the header, A2 and D9 share line 12 and cannot both have handlers. The second attach warns on the console and has no effect.

Handlers run in interrupt context and must not call Serial.print, delay or other blocking functions. State is shared through volatile variables or a FreeRTOS queue.

Precise measurement

micros() reads the cycle counter and is not affected by tick jitter. DWT->CYCCNT provides cycle resolution at 800 cycles per microsecond. pulseIn(pin, HIGH, timeout_us) measures a pulse width with microsecond resolution.

The STM32N6 clock tree provides a 64 MHz HSI, a 4 MHz MSI and a 32 kHz LSI internally, accepts a 16 to 48 MHz HSE and a 32.768 kHz LSE, and has four PLLs: one for the system, one for the NPU and two for peripheral kernel clocks. The power save library's clock profiles and the RTC timing described in Low Power follow from this arrangement.

Watchdog

The core arms the STM32N6's independent watchdog (IWDG) with an 8 second period once the USB task is running, and feeds it from that task. The IWDG is a 12-bit down counter on its own clock in the VDD domain, so it keeps running through low power modes and cannot be disabled once started. The chip also has a system window watchdog, which the core does not use. A stalled loop() does not trigger it, because the USB task continues to run; it guards against a system-wide hang. The power save library chunks long sleeps to feed it.

An external window watchdog with an application-level heartbeat is supported by the power save library. See Low Power.