Skip to content

The FreeRTOS Posix port

The Linux/macOS port is not a normal FreeRTOS target. Three behaviours drove real decisions in this code; changing them will break things in ways that look like protocol bugs.

This applies to the five node binaries only. can_hub, egse, egse_tm and egse_tc are not flight software — plain POSIX C, no FreeRTOS, no can_drv — and use ordinary blocking / select() calls. Don’t add the workarounds below to them. sim/ is not flight software either, but it is linked into adcs_node and eps_node, so it does carry them.


1. A task blocked in a syscall starves everything at or below its priority

Tasks are pthreads, and the kernel can’t tell one is parked in recvfrom() — the task still looks Running, so vTaskSwitchContext() re-selects it forever. A plain blocking recvfrom() in the RX task deadlocks the node.

This applies to every socket call in a node, which is why TTC’s TCP link task bounds its recv() the same way, and why TTC dials out instead of calling accept().

Bounding alone only protects the tasks at the same priority. A bounded loop returns to the scheduler, so it time-slices against its equals — but it never leaves the Ready state, and FreeRTOS always runs the highest-priority Ready task, so everything below it is starved outright. TTC’s link task proved it: bounded correctly at PRIO_APP, its housekeeping task one priority down at PRIO_TLM was never scheduled once, so TTC emitted no telemetry for the life of the run — nothing logged, no error anywhere — until the smoke test caught four APIDs where there should have been five. The fix is the same vTaskDelay(1) shape as CAN_RX_BATCH below.

The CAN driver (common/can_drv.c) does two things, and needs both:

  • SO_RCVTIMEO (one tick) bounds each call, covering the idle bus.
  • CAN_RX_BATCH sleeps every 16 frames, covering the busy bus. Under sustained traffic recvfrom() never times out, so the timeout alone never yields and the queue overflows while the dispatcher sits Ready but unscheduled. taskYIELD() can’t substitute: FreeRTOS always runs the highest-priority ready task, so the RX task must genuinely leave the Ready state.

There are now two drivers with this shape. common/spw_drv.c, the SpaceWire mission data link, repeats all three workarounds line for line, with SPW_RX_BATCH (8) in place of CAN_RX_BATCH. The batch yield matters more there: a transfer never lets the receive timeout fire, so without it the receive task holds the CPU for a whole file.

Blocking on a FreeRTOS primitive (xQueueReceive, xStreamBufferReceive) is fine — the scheduler knows the task is Blocked. Only raw syscalls are the hazard.

The same reasoning is why the NVRAM mirror is never written on the telecommand path: dpNvramSync only marks the mirror dirty, and prvOrchestrateTask writes it on a cadence divider. The failure mode is starvation, not slowness, so the file’s size is irrelevant. Reading it is fine only in main(), before vTaskStartScheduler().

2. Every blocking syscall gets EINTR, once per tick

The tick handler is installed without SA_RESTART. The retry loops around recvfrom() and sendto() are required, not defensive.

This is also why configTICK_RATE_HZ is 100 and not 1000: on this port the tick rate is the EINTR rate.

3. printf is not task-safe

The kernel can switch tasks while a thread holds a libc-internal lock, and the next printf deadlocks. All node output goes through the mutex in logPrintfLevel() (common/log.c) via the LOG*() macros. Logging is level-gated (LOG_ERRORLOG_TRACE, default INFO, set per node from the log_level config key); LOG() is a back-compat alias for LOG_INFO.


4. Heap sizing

configTOTAL_HEAP_SIZE is 4 MiB. The port ignores stack sizes (it never calls pthread_attr_setstacksize), but stack depth still drives heap consumption, and it is counted in words — so configMINIMAL_STACK_SIZE (16384) costs 128 KiB per task on a 64-bit host.

That 16384 is a pinned literal and must not go back to PTHREAD_STACK_MIN the way the upstream Posix demo has it. Since glibc 2.34 the macro is a runtime sysconf(), and on aarch64 Linux it is 131072 rather than the 16384 macOS and x86-64 report — the demo’s (unsigned short) cast then truncates that to zero, so every task asks pvPortMalloc for 0 bytes, gets NULL, and trips the malloc-failed hook before the scheduler starts. Pinning it costs nothing on the hosts that worked and is what makes the platform run under Docker on Apple Silicon, where the containers are Linux/aarch64.

NodeTasksRoughly
DHSdispatch, orchestrate, fdir, hk-poll, downlink + canrx + spwrx7 × 128 KiB
PAYLOADdispatch, camera, gnss, storage + canrx + spwrx6 × 128 KiB
TTCdispatch, link, hk + canrx4 × 128 KiB
ADCSctrl (10 Hz, runs the plant), dispatch, hk + canrx4 × 128 KiB
EPSmodel (runs the plant), dispatch, hk + canrx4 × 128 KiB

The ADCS and EPS plants cost no extra task: sim/ is stepped synchronously at the top of the node’s own control/model task, which is the whole of the synchronisation design in both.

Adding a task can overflow the heap and abort inside vTaskStartScheduler() with no message — if a node dies there, this is why. Only tasks cost heap: the SSMM, event log, verification table and data-pool tables are static, the mission data link’s buffers (the 128 KiB mass memory file partition, the payload’s 256 KiB flash, the RMAP transmit and receive buffers) are .bss, and the pool’s “dynamic” block is a statically sized arena with an allocation bitmap for exactly this reason — there is no pvPortMalloc in it.

configCHECK_FOR_STACK_OVERFLOW is 0 deliberately: pthreads own the stacks, so there is nothing for FreeRTOS to watermark and the check would be theatre.


5. Debugging on macOS

The port uses SIGUSR1 for context switches. Without this, LLDB traps on every one:

echo 'process handle SIGUSR1 -n true -p false -s false' >> ~/.lldbinit

6. Known risks

  • Upstream CI compiles the Posix port on macOS but never runs it. Verified running here on macOS arm64, but not guarded upstream.
  • An open forum thread reports unresolved macOS races around blocking and signals. Not reproduced here.
  • The port is explicitly not real-time. Scheduling is illustrative; the on-board clock is disciplined to whatever UTC the ground sets (Time in packets).

7. Lock hierarchy

Not a port property, but the other invariant that fails like a protocol bug: the DHS’s three mutexes never nest. Owned by dhs.md §13.

Last updated on