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_BATCHsleeps every 16 frames, covering the busy bus. Under sustained trafficrecvfrom()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_ERROR…LOG_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.
| Node | Tasks | Roughly |
|---|---|---|
| DHS | dispatch, orchestrate, fdir, hk-poll, downlink + canrx + spwrx | 7 × 128 KiB |
| PAYLOAD | dispatch, camera, gnss, storage + canrx + spwrx | 6 × 128 KiB |
| TTC | dispatch, link, hk + canrx | 4 × 128 KiB |
| ADCS | ctrl (10 Hz, runs the plant), dispatch, hk + canrx | 4 × 128 KiB |
| EPS | model (runs the plant), dispatch, hk + canrx | 4 × 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' >> ~/.lldbinit6. 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.