Firmware Tiers
OpenSkyhawk firmware runs across three tiers. Each owns a clear slice of the job and hands off to the next over a defined link. The contracts between tiers — what each tier does not do — matter as much as what it does. Get them wrong and you'll duplicate logic or break the byte-relay guarantees the architecture depends on.
PC (DCS + DCS-BIOS)
│ USB — single connection, composite CDC + HID
▼
SimGateway (RP2040)
│ UART @ 250 kbps
▼
PanelBridge (STM32F103C8, CAN master)
│ CAN bus @ 500 kbps
▼
PanelGroup (STM32F103C8, one per panel group)
│ I²C over JST-XH
▼
Breakout boards (MCP23017, ADS1115)
The three tiers at a glance
| SimGateway | PanelBridge | PanelGroup | |
|---|---|---|---|
| MCU | RP2040 | STM32F103C8 | STM32F103C8 |
| Library | SimGateway.h |
PanelBridge.h |
OpenSkyhawk.h |
| Role | USB bridge + HID | CAN master + DCS-BIOS | Panel I/O |
| Upstream | USB to PC | UART to SimGateway | CAN to PanelBridge |
| Downstream | UART to PanelBridge | CAN to PanelGroup nodes | I²C to breakouts |
| Count | 1 per cockpit | 1 per cockpit | 1 per panel group |
Three more libraries are shared rather than owned by a single tier: CANProtocol (packet
structs, CAN IDs, the controlId namespace), STM32Board (CAN/UART/status-LED hardware
init for both STM32 tiers), and HIDControls (the HID controlId allocations, shared
between SimGateway and CANProtocol).
STM32F103 variant: STM32F103C8 by default on every board
Both STM32 tiers use the STM32F103 family, and both default to the STM32F103C8 (64 KB
flash). PanelBridge carries the full DCS-BIOS input map on top of the DCS-BIOS library,
yet still compiles to ~26 KB flash (~40% of the C8's 64 KB), so it fits the STM32F103C8.
PanelGroup nodes only hold the DCSIN_* constants they use, so they fit easily too. The
STM32F103CB (128 KB) is a drop-in fallback on the identical LQFP48 footprint, available
if a future build ever exceeds 64 KB — no board currently needs it.
SimGateway (RP2040)
The single USB device the PC sees. A composite device: USB CDC for the DCS-BIOS stream, USB
HID for flight-control axes and buttons. The CDC interface advertises its own name —
A-4E Skyhawk DCS-BIOS (iInterface) — so the serial port is self-identifying, not just a
generic VID/PID + CDC class. It relays the DCS-BIOS byte stream transparently to PanelBridge
over UART, and intercepts HID frames coming back up to drive the joystick report.
What it does not do:
- It does not run the DCS-BIOS library. It never parses or interprets the DCS-BIOS stream — every byte is relayed verbatim.
- It has no knowledge of DCS-BIOS control names, addresses, or the input map.
- It does not buffer or delay DCS-BIOS bytes — relay is immediate, so PanelBridge sees the stream with USB-CDC latency only.
See SimGateway for the full breakdown.
PanelBridge (STM32F103)
The CAN master, and the only tier that runs the DCS-BIOS library. It receives the DCS-BIOS
stream from SimGateway over UART, fires the DCS-BIOS export listener, and broadcasts every
DCS output onto the CAN bus as CTRL_BCAST frames. In the other direction it collects input
events (EVT_n) from the nodes and routes each one: DCS-BIOS inputs go out as ASCII commands
over UART; HID inputs are wrapped in HID frames for SimGateway. It also tracks which PanelGroup
nodes are alive (from their heartbeats) and — behind a build flag — reports that roster + health
to the host over DCS-BIOS itself (_NODE_STATUS messages; request address 0x86FE), so the client
can show connected panels without any SimGateway change
(#86).
What it does not do:
- It does not touch USB at runtime. (USB and CAN share pins PA11/PA12 on the STM32 — they can't both be active. That's the whole reason SimGateway exists.)
- It does not track which node sent an input event — PanelBridge has no per-node input state. Multiple controls may map to the same DCS command.
- It does not drive panel hardware decisions; nodes own their own I/O.
NODE_ID is always 0 for PanelBridge — reserved, and never transmitted as a heartbeat on
the bus.
PanelGroup (STM32F103)
One per panel group. Each node drives its panel hardware directly — GPIO, or MCP23017
(digital I/O) and ADS1115 (analog) breakouts over I²C — and carries a unique NODE_ID (1–63)
baked in at compile time. It receives CTRL_BCAST frames and dispatches them to registered
output objects (LEDs, gauges); it polls input objects and fires EVT_n frames back to
PanelBridge.
What it does not do:
- It does not decide DCS-BIOS vs HID routing. A node just fires a
controlIdand a value; the routing happens upstream (PanelBridge for DCS-BIOS, SimGateway for HID). See DCS-BIOS vs HID. - It does not run the DCS-BIOS library or know any DCS command strings — it works in
compact
controlId/ value terms only. - It does not talk to other nodes. All traffic is node ↔ PanelBridge.
Why the split
Each boundary exists for a concrete reason — USB reliability, peripheral conflicts, keeping nodes dumb and routing centralised. Those reasons, with the failure data behind them, are in Design Decisions.