Skip to content

ChibiOS Threading Model

The NanoVNA-H firmware runs on ChibiOS/RT, a compact real-time operating system designed for embedded systems. ChibiOS provides preemptive multithreading, allowing the firmware to handle measurement, user interface, and USB communication simultaneously.

The firmware uses two main threads plus background interrupt handlers:

flowchart TB
  subgraph KERNEL[ChibiOS Kernel]
      SCHED[Scheduler<br/>100 kHz tick]
  end

  subgraph THREADS[Application Threads]
      MAIN[Main Thread<br/>Normal Priority]
      SWEEP[Sweep Thread<br/>High Priority]
  end

  subgraph ISR[Interrupt Handlers]
      DMA[I2S DMA ISR]
      USB[USB ISR]
      SYSTICK[SysTick ISR]
  end

  SCHED --> MAIN
  SCHED --> SWEEP
  DMA --> |Audio Data| SWEEP
  USB --> MAIN
  SYSTICK --> SCHED

The main thread handles:

  • User interface (touch, display updates)
  • USB shell command processing
  • Calibration calculations
  • Non-time-critical operations
int main(void) {
// Initialize hardware and peripherals
halInit();
chSysInit();
// Start the sweep thread
chThdCreateStatic(waThread1, sizeof(waThread1),
NORMALPRIO + 10, Thread1, NULL);
// Main loop: UI and shell processing
while (1) {
if (sweep_mode & SWEEP_ENABLE) {
// UI updates, touch handling
ui_process();
}
if (shell_line_received) {
// Process USB commands
VNAShell_executeCMDLine(shell_line);
}
}
}

The sweep thread runs at higher priority and handles:

  • Frequency synthesizer control
  • DSP accumulation coordination
  • Measurement timing
  • Calibration application
static THD_FUNCTION(Thread1, arg) {
(void)arg;
while (1) {
if (sweep_mode & SWEEP_ENABLE) {
// Perform frequency sweep
sweep(true, get_sweep_mask());
// Apply transform domain processing (if enabled)
transform_domain(get_sweep_mask());
// Signal display to update
redraw_request |= REDRAW_CELLS;
}
chThdSleepMilliseconds(10); // Yield when idle
}
}

The system tick drives the scheduler and timing functions:

// From chconf.h
#define CH_CFG_ST_FREQUENCY 100000 // 100 kHz tick rate
#define CH_CFG_ST_TIMEDELTA 0 // Tickless mode disabled
// Time conversion macros
#define US2ST(usec) ((systime_t)(((uint32_t)(usec) * CH_CFG_ST_FREQUENCY) / 1000000UL))
#define MS2ST(msec) ((systime_t)(((uint32_t)(msec) * CH_CFG_ST_FREQUENCY) / 1000UL))

The 100 kHz tick rate provides 10 microsecond timing resolution, essential for precise PLL settling delays.

ChibiOS supports both scheduling modes. The NanoVNA uses preemptive scheduling:

  • Higher-priority threads interrupt lower-priority threads
  • The sweep thread can preempt the main thread at any time
  • Interrupt handlers have highest priority
// Example: sweep timing with precise delays
static bool sweep(...) {
for (p_sweep = 0; p_sweep < sweep_points; p_sweep++) {
// Set frequency (may yield during I2C transactions)
delay = set_frequency(frequency);
// Precise delay for PLL settling
chThdSleep(delay); // Does not block lower priority threads
// DSP accumulation (interrupt-driven)
DSP_START(delay);
// ... calibration lookup while accumulating ...
DSP_WAIT; // Blocks until DMA complete
}
}

Used to signal between threads and ISRs:

// Wait for DMA completion
static void DSP_WAIT(void) {
chEvtWaitOne(EVT_DMA_COMPLETE);
}
// Signaled from I2S DMA interrupt
void i2s_lld_serve_rx_interrupt(...) {
dsp_process(buffer, length);
if (--wait_count == 0)
chEvtBroadcastI(&dsp_complete_event);
}

Protect shared resources:

// Protect I2C bus access
MUTEX_DECL(i2c_mutex);
void i2c_transfer(...) {
chMtxLock(&i2c_mutex);
// ... I2C transaction ...
chMtxUnlock(&i2c_mutex);
}

Highest-priority interrupt, processes audio samples:

OSAL_IRQ_HANDLER(STM32_I2S_SPI_HANDLER) {
OSAL_IRQ_PROLOGUE();
// Process samples from DMA buffer
dsp_process(rx_buffer + offset, AUDIO_BUFFER_LEN);
// Signal sweep thread if accumulation complete
if (--wait_count == 0)
chSysLockFromISR();
chEvtBroadcastI(&dsp_event);
chSysUnlockFromISR();
OSAL_IRQ_EPILOGUE();
}

Handles USB CDC (virtual COM port) communication:

OSAL_IRQ_HANDLER(STM32_USB_HANDLER) {
OSAL_IRQ_PROLOGUE();
// ChibiOS USB driver handles the details
usb_lld_serve_interrupt(&USBD1);
OSAL_IRQ_EPILOGUE();
}

Drives the scheduler and system time:

OSAL_IRQ_HANDLER(SysTick_Handler) {
OSAL_IRQ_PROLOGUE();
chSysLockFromISR();
chSysTimerHandlerI(); // Advance system time, check timeouts
chSysUnlockFromISR();
OSAL_IRQ_EPILOGUE();
}

Each thread needs its own stack. Stack sizes are carefully tuned to minimize RAM usage:

// Sweep thread stack (needs space for DSP and calibration)
static THD_WORKING_AREA(waThread1, 768);
// Shell thread stack (needs buffer space for command parsing)
static THD_WORKING_AREA(waThread_shell, 512);

A typical sweep cycle shows thread interactions:

Time -->
Main Thread: [UI]---[idle]---[UI]---[idle]---
| |
Sweep Thread: -------[sweep point]--[sweep point]--
| | | | |
I2S DMA ISR: ^ ^ ^ ^ ^
| | | | |
Buffer interrupts

Typical CPU usage during sweep:

ActivityCPU %Notes
DSP processing30-50%Depends on bandwidth setting
Frequency synthesis5-10%I2C transactions
Display update10-20%SPI transfers
USB/Shell1-5%Only when active
Idle15-50%Available for other tasks

ChibiOS places thread stacks and kernel structures in RAM:

RAM Layout (F072 - 16 KB):
0x20000000 +------------------+
| Vector table |
+------------------+
| Global variables |
| measured[] |
| cal_data[] |
+------------------+
| Heap (minimal) |
+------------------+
| Thread 1 stack |
+------------------+
| Main stack |
0x20003FFF +------------------+

ChibiOS provides assertion and panic mechanisms:

// Assertion in kernel code
chDbgAssert(ptr != NULL, "null pointer");
// Panic handler (called on fatal errors)
void _unhandled_exception(void) {
chSysHalt("exception");
}

In release builds, assertions are disabled to save code space. The firmware generally attempts to continue operation even after errors.

ComponentRolePriority
Main ThreadUI, shell, configNormal
Sweep ThreadMeasurementNormal + 10
I2S DMA ISRAudio processingInterrupt
USB ISRCommunicationInterrupt
SysTick ISRSchedulingInterrupt

Learn how the firmware efficiently updates the display in Display Pipeline.