Skip to content

Build Flags

Every OpenSkyhawk firmware project is configured by -D flags in its platformio.ini. There are 32 of them, and they come from three different places. That mixed provenance is the first thing to understand, because it decides who you have to convince before you change one:

Group Count Changing one is…
Ours — behavioural 13 a local decision — we own the code that reads it
Third-party, set by us 7 not a local decision — STM32duino, arduino-pico, and DCS-BIOS own these
Test seams 12 bench-only — never present in a shipped build

This page is an inventory: what each flag defaults to, where it's set, and where it's read. Deep per-flag semantics live in the library READMEs under Firmware/Libraries/ and in Firmware/ScratchPad/TechSpec/. When a table here and a header disagree, the header wins — and the grep recipe at the bottom will tell you which.

For a working platformio.ini with the common flags in context, see PlatformIO Setup. If you are building a panel right now, start with the next section instead — the tables below are reference, not a checklist.

Starting a new panel

You copied Firmware/Templates/PanelGroup/ and you're looking at its build_flags. Here is what to do with each line. The walkthrough covers the surrounding steps; this is just the flags.

Required — set these or the build fails

Flag What to set
NODE_ID Your board's unique CAN address, 1–63, claimed from the registry in Firmware/NODE_IDS.md — see NODE_ID & CAN Addressing. No default. Omitting it is a compile error, not a silent fallback.
HAL_CAN_MODULE_ENABLED Nothing — keep it as-is. STM32duino does not enable the bxCAN HAL driver by default, so without this line there is no CAN driver to link against.

Already correct in the template — keep, don't tune

Flag Why to leave it alone
HSE_VALUE=8000000 Already matches both your board's crystal and the framework's own F1 default. Change it only if your board genuinely carries a different crystal — and if you do, get it right first time: a wrong value is undetectable at runtime (see the warning below).
USB_NONE Inert. It documents that CAN owns PA11/PA12; it does not enforce it. Harmless to keep, harmless to drop.

Add only if your panel needs them

Flag Add when
I2C_TIMEOUT_TICK=10 Your panel has I²C devices (MCP23017, ADS1115, OLEDs) — shortens the stall when one is absent or miswired.
SHIFTBUS_SCK / MISO / MOSI / LOAD / LATCH You are relocating the shift-register bus off its default pins. Otherwise leave all five out.
SHIFTBUS_ISR_HZ You want timer-ISR sampling for encoder feel instead of polled sampling.
SERIAL_RX_BUFFER_SIZE=256 PanelBridge only. A PanelGroup has no use for it.
NODE_HEALTH_TELEM=0 You specifically want health telemetry off. It is on by default — omitting the line does not disable it.

Never in a panel

FORCE_CLOCK_FALLBACK, PINREF_DEBUG, NODE_OVERHEAT_C, and any *_TEST seam. These are bench and debug instruments. If one appears in a panel's platformio.ini, it is a mistake — see Test seams and Deliberate non-defaults for what each actually does.

Ours — behavioural (13)

Flag Default Set where Read at
NODE_ID none — omitting it fails to compile every STM32 env that links our libraries; WiringCheck omits it deliberately CANProtocol.cpp:197, PanelGroup.cpp:391, STM32Board.h:29
PANELBRIDGE_NODE_STATUS off on in Templates/PanelBridge PanelBridge.cpp:42 + ~10 more
NODE_HEALTH_TELEM on never set PanelBridge.cpp:468, PanelGroup.cpp:399
NODE_OVERHEAT_C unset — by design bench envs only CANProtocol.cpp:396, PanelGroup.cpp:409
FORCE_CLOCK_FALLBACK off Tests/STM32Board env test_adc_clock_fallback STM32Board.cpp:179
PINREF_DEBUG off never set PinRef.cpp:8 + ~9 more
SHIFTBUS_SCK PB3 never set ShiftBus.cpp:13
SHIFTBUS_MISO PB4 never set ShiftBus.cpp:16
SHIFTBUS_MOSI PB5 never set ShiftBus.cpp:19
SHIFTBUS_LOAD PB8 never set ShiftBus.cpp:22
SHIFTBUS_LATCH PB9 never set ShiftBus.cpp:25
SHIFTBUS_ISR_HZ unset → polled sampling Tests/ShiftBus, =1000 PanelGroup.cpp:125,240
SHIFTBUS_ISR_TIM TIM2 never set PanelGroup.cpp:241

What they do, briefly. NODE_ID is the board's CAN address — see NODE_ID & CAN Addressing. PANELBRIDGE_NODE_STATUS makes PanelBridge report which PanelGroup nodes it can see, up to the host over DCS-BIOS. NODE_HEALTH_TELEM is the periodic health frame (die temperature, fault bitmap) at half the heartbeat rate. FORCE_CLOCK_FALLBACK exercises the dead-crystal fault path without a dead crystal; the test_adc_clock_fallback env uses it to prove SystemClock_Config sets the ADC prescaler on its fault path as well as its verified-72 MHz path (#263). PINREF_DEBUG adds assertions to the pin abstraction. The SHIFTBUS_* pin flags relocate the shift-register bus off its default SPI1-remap pins; SHIFTBUS_ISR_HZ switches that bus from polled to timer-ISR sampling (encoder feel) on the timer named by SHIFTBUS_ISR_TIM.

Set NODE_ID in platformio.ini, never in main.cpp

A #define NODE_ID in a sketch is invisible to library translation units — CANProtocol and PanelGroup are compiled separately. That mistake is caught at build time rather than shipped: those TUs include STM32Board.h, whose file-scope static_assert fails with error: 'NODE_ID' was not declared in this scope. There is no default and no silent fallback — the undefined-evaluates-to-zero rule applies inside #if, not in a static_assert expression.

Sketches that link none of those libraries are the only exception: the standalone Examples/E2E_DCS_Test/WiringCheck env omits -DNODE_ID deliberately, because it is a raw GPIO/ADC diagnostic with no OpenSkyhawk libraries at all.

Note that 0 is a legal value — it is PanelBridge's reserved address. It is just never correct for a PanelGroup node.

Deliberate non-defaults

Three of the above look like oversights and are not. They are choices, and this is the record of them:

NODE_OVERHEAT_C is unset on purpose — OVERHEAT never raises in a shipped build

The STM32's internal die-temperature sensor is uncalibrated. Rather than ship a threshold nobody has field data for, the overheat bit is computed only when a build defines NODE_OVERHEAT_C — and no shipped environment defines it: not the templates, not Panels/Center_Armament, not the E2E examples. The OVERHEAT flag in the node-health frame is therefore never set on any real cockpit node.

Two bench environments force it deliberately, and only to exercise the path: Tests/CANProtocol sets =0 (any reading trips it, to test overheat and degraded coexisting) and Tests/PanelGroup sets =20 (a room-temperature node reports overheat, to test the client's rendering). Enabling it for real is a one-line -DNODE_OVERHEAT_C=N once someone has calibration data. See FirmwarePlan/00-decisions.md.

NODE_HEALTH_TELEM is ON by default — disable with =0, not by omission

It's the only flag in this group that's on when absent (#if !defined(X) || (X)). Leaving it out gets you health telemetry; you need an explicit -DNODE_HEALTH_TELEM=0 to turn it off.

PANELBRIDGE_NODE_STATUS is off in the library but on in every shipped PanelBridge

The library defaults it off, so the table above says "off" — but the PanelBridge template enables it, and so does the E2E example. Every build you would actually flash to a cockpit has it on. (Most of the 13 Tests/PanelBridge envs leave it off; they are testing other things.) The client half is already released, which is why the default flipped in the template rather than in the library.

Third-party, set by us (7)

These belong to STM32duino, arduino-pico, and the DCS-BIOS library. We only set them — we don't own the code that reads them, and a framework update can move a default out from under us. Two of these have already cost real debugging time.

Flag Framework default Set where Read at
HAL_CAN_MODULE_ENABLED not enabled every STM32 env that links our libraries; WiringCheck omits it deliberately stm32f1xx_hal_conf_default.h:321
HSE_VALUE already 8000000U on F1 =8000000 in all templates, panels and examples, plus the CAN/board tests; 13 library-test envs omit it — harmlessly, since it equals the default stm32f1xx_hal_conf_default.h:82
SERIAL_RX_BUFFER_SIZE 64 PanelBridge envs, =256 HardwareSerial.h:41
I2C_TIMEOUT_TICK 100 one E2E PanelGroup env, =10 Wire/src/utility/twi.c:48
USB_NONE no such macro 10 platformio.ini files nowhere — see below
USE_TINYUSB off every RP2040 env arduino-pico platformio-build.py:242,325
DCSBIOS_DEFAULT_SERIAL off #define in the sketch, not -D PanelBridge.cpp:11

HAL_CAN_MODULE_ENABLED is genuinely required: CAN is absent from STM32duino's default-enabled HAL module list, so without our -D there is no bxCAN driver to link against. SERIAL_RX_BUFFER_SIZE=256 gives PanelBridge room for DCS-BIOS export bursts on UART2. I2C_TIMEOUT_TICK=10 shortens the Wire library's stall when an I²C device is absent. DCSBIOS_DEFAULT_SERIAL is the odd one out — it's #defined at the top of the sketch's main.cpp rather than passed as -D, because PanelBridge.cpp has to include DcsBios.h without it and then restore it, to avoid an ODR violation.

HSE_VALUE does not select HSE — and it already matches the framework default

-DHSE_VALUE=8000000 tells HAL what the crystal is. It does not switch the clock tree to it, and 8000000U is already what STM32duino defaults the F1 to, so the flag changes nothing on its own. What actually selects HSE is STM32Board's strong override of SystemClock_Config (STM32Board.cpp:164), which every OpenSkyhawk STM32 node links. Without it, the variant runs HSI-PLL at 64 MHz → APB1 32 MHz → CAN at 444 kbps against a 500 kbps bus.

The corollary: a wrong-value crystal is undetectable at runtime. HAL_RCC_Get*Freq() derives from the compile-time HSE_VALUE, not from a measurement, so a 12 MHz part would compute as 72 MHz while really running 108. Correct crystal value is a BOM guarantee.

USE_TINYUSB is required for HID — omitting it silently builds the wrong USB stack

On RP2040 this selects TinyUSB in place of the default Pico stack. Adafruit_TinyUSB.h and SimGateway's whole HID backend depend on it. The arduino-pico builder branches on it in two places; without it the project still compiles, which is what makes it expensive to diagnose.

USB_NONE is inert — nothing reads it

It appears on 25 lines across 10 platformio.ini files and has no effect. STM32duino has no such macro: its "USB support: None" board option sets nothing at all, and only the CDC and HID options add defines. Nothing in the framework, the PlatformIO builder, or our code reads USB_NONE.

The intent behind it is real, though. On the F103, USB D+/D− are PA11/PA12 — exactly the pins the CAN transceiver uses, so CAN genuinely does own them and USB must stay off. What keeps it off is that USBCON is never defined, and no build config or source under Firmware/ defines it. The flag documents the decision; it does not enforce it.

Test seams (12)

Each of these widens a library's visibility so its on-target tests can assert against internal state. A seam never changes shipped behaviour, and no shipped environment defines one — each is set only by its own Firmware/Tests/<Library>/platformio.ini.

Flag Library it opens First read at
ANALOGINPUT_TEST AnalogInput Inputs/AnalogInput/AnalogInput.h:85
ANALOGMULTIPOS_TEST AnalogMultiPos Inputs/AnalogMultiPos/AnalogMultiPos.h:90
DRUMDISPLAY_TEST DrumDisplay DrumDisplay/DrumDisplay.h:246
LED_TEST LED Outputs/LED/LED.h:68
MULTIPOS_TEST MultiPosInput Inputs/MultiPosInput/MultiPosInput.h:60
NEEDLEGAUGE_TEST NeedleGauge Outputs/NeedleGauge/NeedleGauge.h:78
PANELBRIDGE_TEST PanelBridge PanelBridge/PanelBridge.h:84
ROTARYENCODER_TEST RotaryEncoder Inputs/RotaryEncoder/RotaryEncoder.h:96
SHIFTBUS_TEST ShiftBus Helpers/ShiftBus/ShiftBus.h:141
SIMGATEWAY_TEST SimGateway SimGateway/SimGateway.cpp:11 (no-op HID stubs)
STEPPERMOTOR_TEST StepperMotor Drivers/StepperMotor/StepperMotor.h:158
STM32BOARD_TEST STM32Board STM32Board/STM32Board.h:32

Paths are relative to Firmware/Libraries/, and the PanelGroup ones to Firmware/Libraries/PanelGroup/. Most seams are read in several places; the column gives the first, which is where the guarded block starts.

Two test projects set a second library's seam because they drive it: Tests/NeedleGauge also sets STEPPERMOTOR_TEST, and Tests/AnalogMultiPos also sets MULTIPOS_TEST. Five test projects have no seam of their own at all — Tests/PinRef, Tests/CANProtocol, Tests/NodeStatus, Tests/Switch2Pos and Tests/PanelGroup test through the public API.

SIMGATEWAY_TEST is the one that does more than widen visibility: it swaps the real HID backend for no-op stubs so SimGateway logic can be tested without a USB host.

Re-verifying this page

Rather than trust the tables, re-derive them. Run both blocks from the repo root — their union must be 32. If it isn't, this page is stale.

Be clear about what that proves. Both lists come from greps of the same shape, so the union check catches drift — a flag added, removed, or renamed after this page was written. It does not prove the inventory is complete: a flag consumed in some way neither grep looks for would be missed by the check exactly as it was missed by the page. Treat 32 as the current count, not a guarantee.

grep -rhE '^[^;#]*-D[A-Za-z_]' --include=platformio.ini Firmware | sed -E 's/[;#].*//' | grep -oE '[[:space:]]-D[A-Za-z_][A-Za-z0-9_]*' | sed 's/.*-D//' | sort -u

That's every flag set by some environment — 22 of them.

grep -rhoE '#[[:space:]]*(ifdef|ifndef|if|elif)[^A-Za-z_]*defined[^A-Za-z_]*[A-Za-z_][A-Za-z0-9_]*|#[[:space:]]*(ifdef|ifndef)[[:space:]]+[A-Za-z_][A-Za-z0-9_]*' --include='*.h' --include='*.cpp' Firmware/Libraries | grep -oE '[A-Za-z_][A-Za-z0-9_]*$' | sort -u | grep -vE '^(ARDUINO_ARCH_|STM32F1xx$|ATEMP$|guard$|_)'

That's every flag read by a library conditional — 25 of them. The trailing filter drops architecture macros, the ATEMP pin macro, and internal include guards; none are build flags.

The two lists deliberately don't match, and the differences are the interesting part:

  • Set but not readNODE_ID (used as a value, canIdEvt(NODE_ID), never in a conditional) plus the six framework flags, which are read inside STM32duino and arduino-pico rather than in our tree.
  • Read but never set — the nine flags whose defaults always apply in every committed environment (the five ShiftBus pins, SHIFTBUS_ISR_TIM, NODE_HEALTH_TELEM, FORCE_CLOCK_FALLBACK, PINREF_DEBUG), plus DCSBIOS_DEFAULT_SERIAL, which is #defined in sketch source instead of passed as -D.

To check the USB_NONE claim specifically, both of these must return nothing:

grep -rn 'USBCON\|USBD_USE_' Firmware
grep -rn 'USB_NONE' ~/.platformio/platforms ~/.platformio/packages/framework-arduinoststm32