Software/Arduino Core/Advanced

FreeRTOS Under the Hood

The FreeRTOS scheduler beneath a Neuro N6 sketch, the core's tasks and priorities, and guidance for sketch-created tasks.

advanced3 min read

The Neuro N6 core boots a FreeRTOS scheduler before setup() runs. loop() executes as one task, and the camera, USB and optional network paths execute as separate tasks. Standard Arduino calls behave as expected; the scheduler is visible only to sketches that create tasks of their own or that block loop() for long periods.

Core tasks

TaskPriorityStackFunction
ArduinoLoopidle + 216 KiBsetup() and loop(). Vision inference runs here
n6_captureidle + 34 KiBCamera frame to JPEG encode, metadata composition across slots, posting to the transport queue
n6_usbidle + 38 KiBTinyUSB servicing, the vendor endpoint transmit pump, the CDC console and the bootloader command, and draining the USB frame queue

With a WiFi transport and the MayaW476 library linked, two more are created inside Vision.begin():

TaskPriorityStackFunction
maya_w4_connidle + 24 KiBOne shot: wait for the module, associate or host the access point, then idle
maya_w4_vididle + 36 KiBDrains the WiFi frame queue into a TCP socket

The capture task posts to whichever queue matches cfg.transport. The USB task runs regardless, so the console and the bootloader command remain available while video flows over WiFi. Both frame queues exist unconditionally; the WiFi consumer attaches to the second when the library is present.

Behaviour of inference

Vision.run(), runWith() and runRoi() are synchronous. They block the calling task while the NPU executes, but block on a FreeRTOS semaphore that the NPU interrupt signals, so the capture and USB tasks continue during inference. Video throughput and inference throughput are therefore independent.

Frames are dropped rather than queued when the consumer is busy. Heavy work in loop() between inference calls lowers the visible frame rate but does not block it.

Sketch-created tasks

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

static void blink_task(void *) {
  for (;;) {
    digitalWrite(LED_BUILTIN, HIGH);
    vTaskDelay(pdMS_TO_TICKS(500));
    digitalWrite(LED_BUILTIN, LOW);
    vTaskDelay(pdMS_TO_TICKS(500));
  }
}

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  xTaskCreate(blink_task, "Blink", 1024 / sizeof(StackType_t), NULL,
              tskIDLE_PRIORITY + 1, NULL);
  Vision.begin(&NN_Instance_yolov8_iseg, PostProcess_YOLOv8_ISEG(0.4f, 0.5f));
}

Tasks are created in setup(). A task without a real time deadline is given a priority below loop(). Long waits use vTaskDelay(). A task at or above the capture task's priority that does not yield stalls the camera pipeline.

Configuration

ParameterValue
KernelFreeRTOS V10.6.2
PortCortex-M55, ARM_CM55_NTZ/non_secure
Tick1 kHz
Heap96 KiB, with both static and dynamic allocation enabled
Priorities56 levels

The port owns SVC_Handler, PendSV_Handler and SysTick_Handler. The HAL time base runs on TIM16, and HAL_GetTick() returns the FreeRTOS tick count once the scheduler is running. The NPU runtime is compiled with its FreeRTOS abstraction, so its wait-for-event call blocks on a semaphore rather than a WFE instruction.

Idle hook

configUSE_IDLE_HOOK is enabled and the core provides a weak, empty vApplicationIdleHook. The power save library supplies a strong override that executes __WFI(). Without it the idle task spins at the full core clock, measured at about 100 mA during a blocking wait. Sketches that do not link the library are unaffected; the two cases were verified to produce different symbol sizes in the linked ELF.

Source locations

The kernel is in cores/NeuroN6/FreeRTOS/Source. Configuration is in cores/NeuroN6/FreeRTOSConfig.h. The capture and USB tasks are in cores/NeuroN6/NeuroN6.c, with the frame queue interface in NeuroN6_FrameQueue.h.