Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/firmware-platformio-impact.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,38 @@
"Firmware/Examples/E2E_DCS_Test/PanelGroup",
"Firmware/Tests/DrumDisplay"
],
"FirmwareImage": [
"Firmware/Examples/E2E_DCS_Test/PanelBridge",
"Firmware/Examples/E2E_DCS_Test/PanelGroup",
"Firmware/Examples/E2E_DCS_Test/PanelGroupInputs",
"Firmware/Panels/Center_Armament",
"Firmware/Panels/PDU_Center",
"Firmware/Panels/PDU_Left",
"Firmware/Panels/PDU_Right",
"Firmware/Templates/PanelBridge",
"Firmware/Templates/PanelGroup",
"Firmware/Tests/ActionButton",
"Firmware/Tests/AnalogInput",
"Firmware/Tests/AnalogMultiPos",
"Firmware/Tests/AngleSensorInput",
"Firmware/Tests/Dimmer",
"Firmware/Tests/DrumDisplay",
"Firmware/Tests/IntegerOutput",
"Firmware/Tests/LED",
"Firmware/Tests/NeedleGauge",
"Firmware/Tests/PanelBridge",
"Firmware/Tests/PanelGroup",
"Firmware/Tests/PinRef",
"Firmware/Tests/PowerMonitor",
"Firmware/Tests/RotaryAcceleratedEncoder",
"Firmware/Tests/RotaryEncoder",
"Firmware/Tests/ShiftBus",
"Firmware/Tests/StepperMotor",
"Firmware/Tests/Switch2Pos",
"Firmware/Tests/Switch3Pos",
"Firmware/Tests/SwitchMultiPos",
"Firmware/Tests/SwitchWithCover2Pos"
],
"FirmwareVersion": [
"Firmware/Examples/E2E_DCS_Test/PanelBridge",
"Firmware/Examples/E2E_DCS_Test/PanelGroup",
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/firmware-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ jobs:
# Pure Python, so this stays on PRs even though the build job does not.
- name: Check PlatformIO impact map is up to date
run: python3 .github/scripts/gen-platformio-impact-map.py --check
# The post-build stamp's CRC and offsets must match FirmwareImage.h — the bootloader and
# the uploader recompute them (#362). Pure Python, so it runs on PRs.
- name: Install pytest
run: python -m pip install --quiet pytest
- name: Test the image stamp
run: python -m pytest Firmware/shared/tests -q

firmware:
name: Build supported PlatformIO projects
Expand Down
7 changes: 7 additions & 0 deletions Firmware/Examples/E2E_DCS_Test/PanelBridge/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,10 @@ upload_protocol = stlink
; -c
; set CPUTAPID 0
monitor_speed = 115200

; Production build: the identity block + post-build stamp shared by every released STM32 image (#362).
[platformio]
extra_configs = ../../../shared/stm32.ini

[osh]
firmware = ${PROJECT_DIR}/../../..
7 changes: 7 additions & 0 deletions Firmware/Examples/E2E_DCS_Test/PanelGroup/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,10 @@ upload_protocol = stlink
; set CPUTAPID 0
monitor_speed = 115200
; monitor_port = /dev/cu.usbserial-XXXX ; set to your USB-serial adapter port

; Production build: the identity block + post-build stamp shared by every released STM32 image (#362).
[platformio]
extra_configs = ../../../shared/stm32.ini

[osh]
firmware = ${PROJECT_DIR}/../../..
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,10 @@ lib_deps =

upload_protocol = stlink
monitor_speed = 115200

; Production build: the identity block + post-build stamp shared by every released STM32 image (#362).
[platformio]
extra_configs = ../../../shared/stm32.ini

[osh]
firmware = ${PROJECT_DIR}/../../..
63 changes: 63 additions & 0 deletions Firmware/Libraries/FirmwareImage/FirmwareImage.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#pragma once
/**
* @file FirmwareImage.h
* @brief The image identity block every production STM32 image carries (#362).
*
* 32 bytes, little-endian, at a fixed offset from the app's start (IDENTITY_OFFSET). The bootloader
* (#363) checks it at every boot and before it commits an update; the Gateway uploader (#366) and the
* client (SkyHawkClient#177) read it from an image file before anything is sent. The shared linker
* script (Firmware/shared/stm32f103.ld) places the block; Firmware/shared/osh_image.py fills in
* imageLength and imageCrc after linking. Everything else is set at compile time.
*
* The block is defined by the role library — PanelGroup, PduNode, PanelBridge — with its NodeKind,
* through OSH_FIRMWARE_IDENTITY(). A sketch without a role has no block.
*/
#include <stdint.h>
#include <FirmwareVersion.h>
#include <NodeStatus.h> // NodeKind

namespace OpenSkyhawk {
namespace FirmwareImage {

constexpr uint32_t IDENTITY_OFFSET = 0x200; ///< from the app's start (the vector table's address)
constexpr uint32_t MAGIC = 0x594B534Fu; ///< "OSKY" in memory order
constexpr uint16_t LAYOUT_VERSION = 1;
constexpr uint32_t UNSTAMPED = 0xFFFFFFFFu; ///< imageLength / imageCrc before osh_image.py runs

struct Identity {
uint32_t magic; ///< 0 MAGIC
uint16_t layoutVersion; ///< 4 LAYOUT_VERSION
uint16_t blockSize; ///< 6 sizeof(Identity); a newer layout may grow — readers parse the prefix they know
uint32_t imageLength; ///< 8 bytes from the app's start, padded to 4 — stamped after linking
uint32_t imageCrc; ///< 12 CRC-32/MPEG-2 over imageLength bytes, this word skipped — stamped after linking
uint8_t nodeId; ///< 16 NODE_ID (the build flag, D10)
uint8_t nodeKind; ///< 17 NodeKind of the role
uint8_t fwMajor; ///< 18 FirmwareVersion.h — the same version READY_n reports
uint8_t fwMinor; ///< 19
uint8_t fwPatch; ///< 20
uint8_t reserved[11]; ///< 21 0
};
static_assert(sizeof(Identity) == 32, "the identity block is 32 bytes");

// Field offsets inside the block — for readers that work on raw bytes (client sync, uploader, tests).
constexpr uint32_t OFF_MAGIC = 0, OFF_LAYOUT = 4, OFF_SIZE = 6, OFF_LENGTH = 8, OFF_CRC = 12,
OFF_NODE_ID = 16, OFF_NODE_KIND = 17, OFF_FW_MAJOR = 18, OFF_FW_MINOR = 19, OFF_FW_PATCH = 20;

// imageCrc algorithm: CRC-32/MPEG-2 — poly 0x04C11DB7, init 0xFFFFFFFF, no reflection, no final XOR —
// over 32-bit little-endian words (what the F103's hardware CRC unit computes). Test vectors:
constexpr uint32_t CRC_VECTOR_WORD_12345678 = 0xDF8A8A2Bu; ///< one word, 0x12345678
constexpr uint32_t CRC_VECTOR_BYTES_00_0F = 0x081B46CAu; ///< bytes 0x00..0x0F (four words)

} // namespace FirmwareImage
} // namespace OpenSkyhawk

/// Define this image's identity block. Exactly one per image — in the role library.
#define OSH_FIRMWARE_IDENTITY(kind) \
extern "C" __attribute__((section(".osh_identity"), used)) \
const ::OpenSkyhawk::FirmwareImage::Identity osh_identity = { \
::OpenSkyhawk::FirmwareImage::MAGIC, ::OpenSkyhawk::FirmwareImage::LAYOUT_VERSION, \
(uint16_t)sizeof(::OpenSkyhawk::FirmwareImage::Identity), \
::OpenSkyhawk::FirmwareImage::UNSTAMPED, ::OpenSkyhawk::FirmwareImage::UNSTAMPED, \
(uint8_t)(NODE_ID), (uint8_t)(kind), \
OPENSKYHAWK_FW_MAJOR, OPENSKYHAWK_FW_MINOR, OPENSKYHAWK_FW_PATCH, {0} \
}
10 changes: 10 additions & 0 deletions Firmware/Libraries/FirmwareImage/library.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "FirmwareImage",
"version": "0.2.0",
"description": "The OpenSkyhawk image identity block — node, kind, version, length and CRC — that the bootloader and uploader check",
"frameworks": "arduino",
"platforms": "*",
"build": {
"srcFilter": "-<*>"
}
}
4 changes: 4 additions & 0 deletions Firmware/Libraries/PanelBridge/PanelBridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,13 @@ namespace DcsBios {
#include <NodeStatus.h> // NODE_STATUS_REQ_ADDR / NODE_STATUS_MSG_NAME (node-status reporting, #86)
#include <STM32Board.h>
#include <FirmwareVersion.h> // OPENSKYHAWK_FW_* — the bridge's own node-0 row version (#295)
#include <FirmwareImage.h> // the image identity block (#362)
#include <A4EC_InputMap.h>
#include <string.h>

// This image's identity block — the bootloader and the uploader check it (#362).
OSH_FIRMWARE_IDENTITY(NodeKind::BRIDGE);

namespace {

// ── Node tracking ─────────────────────────────────────────────────────────────
Expand Down
1 change: 1 addition & 0 deletions Firmware/Libraries/PanelBridge/library.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"STM32Board": "*",
"CANProtocol": "*",
"FirmwareVersion": "*",
"FirmwareImage": "*",
"NodeStatus": "*",
"A4EC": "*",
"DCS-BIOS": "^0.3.10"
Expand Down
4 changes: 4 additions & 0 deletions Firmware/Libraries/PanelGroup/PanelGroup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

#include "PanelGroup.h" // pulls in Helpers/ShiftBus/ShiftBus.h
#include <STM32Board.h>
#include <FirmwareImage.h> // the image identity block (#362)

// This image's identity block — the bootloader and the uploader check it (#362).
OSH_FIRMWARE_IDENTITY(NodeKind::PANEL_GROUP);

// ── Internal state ────────────────────────────────────────────────────────────

Expand Down
1 change: 1 addition & 0 deletions Firmware/Libraries/PanelGroup/library.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"dependencies": {
"STM32Board": "*",
"CANProtocol": "*",
"FirmwareImage": "*",
"blemasle/MCP23017": "^2.0.0",
"adafruit/Adafruit ADS1X15": "^2.0.0"
}
Expand Down
4 changes: 4 additions & 0 deletions Firmware/Libraries/PowerMonitor/PduNode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <FirmwareImage.h> // the image identity block (#362)

// This image's identity block — the bootloader and the uploader check it (#362).
OSH_FIRMWARE_IDENTITY(NodeKind::PDU);

using namespace OpenSkyhawk;
using namespace OpenSkyhawk::Pdu;
Expand Down
3 changes: 3 additions & 0 deletions Firmware/Libraries/PowerMonitor/library.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
},
{
"name": "NodeStatus"
},
{
"name": "FirmwareImage"
}
]
}
7 changes: 7 additions & 0 deletions Firmware/Panels/PDU_Center/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,10 @@ lib_deps =

upload_protocol = stlink
monitor_speed = 115200

; Production build: the identity block + post-build stamp shared by every released STM32 image (#362).
[platformio]
extra_configs = ../../shared/stm32.ini

[osh]
firmware = ${PROJECT_DIR}/../..
7 changes: 7 additions & 0 deletions Firmware/Panels/PDU_Left/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,10 @@ lib_deps =

upload_protocol = stlink
monitor_speed = 115200

; Production build: the identity block + post-build stamp shared by every released STM32 image (#362).
[platformio]
extra_configs = ../../shared/stm32.ini

[osh]
firmware = ${PROJECT_DIR}/../..
7 changes: 7 additions & 0 deletions Firmware/Panels/PDU_Right/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,10 @@ lib_deps =

upload_protocol = stlink
monitor_speed = 115200

; Production build: the identity block + post-build stamp shared by every released STM32 image (#362).
[platformio]
extra_configs = ../../shared/stm32.ini

[osh]
firmware = ${PROJECT_DIR}/../..
32 changes: 32 additions & 0 deletions Firmware/ScratchPad/FirmwarePlan/00-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -635,3 +635,35 @@ state when its queue is full.

**Affects:** `PanelBridge.cpp`, `02-can-protocol.md`, `06-panelbridge-api.md`, `10-implementation-plan.md`,
TechSpec `PanelBridge`, PanelBridge README, `docs/architecture/can-bus.md`.

---

## D25 — Every production image carries a stamped identity block (#362)

**Decision:** Each production STM32 image has a 32-byte identity block at image start + `0x200`:
magic, layout version, size, image length, image CRC, NODE_ID, NodeKind and the firmware version.
The role library defines it; a post-build step stamps the length and CRC (CRC-32/MPEG-2 over 32-bit
words, gaps filled 0xFF). The block carries the version only — no commit id. Layout in `08`.

**Rationale:**
- *The image must say what it is before it is sent.* The uploader and the client refuse a file for
the wrong board kind or the wrong NODE_ID, and show the version it holds, without flashing it or
asking the board. A raw `.bin` has no metadata of its own.
- *The bootloader needs a cheap "is the app whole?" check.* Length + CRC in the image lets it reject
an interrupted write at every boot. CRC-32/MPEG-2 over words is what the F103's CRC unit computes,
so the bootloader verifies 60 KB in hardware without a table.
- *Fixed offset, not a symbol.* Readers (bootloader, uploader, client) find it without an ELF, from
the flat image or flash. `0x200` clears the F103 vector table (`0x10C`) with room to spare and
moves with the image when the bootloader shifts the app.
- *0xFF gaps.* An ST-Link flash leaves gaps erased (0xFF) while `objcopy` fills them with 0x00, so
without the fill the `.bin`, the `.elf` and the board would give three answers for one image.
- *Version only.* A release build is cut from its tag, so the version names the commit; dev builds
keep the last release's version (accepted). A commit id would only be known after the commit.
- *Per-role, not per-sketch.* NODE_ID is already a build flag and the role fixes the NodeKind, so a
sketch author writes nothing. `Tests/` builds keep the default linker script — the block is
dropped there.

**Affects:** `08-hardware-firmware-contracts.md`, TechSpec `FirmwareImage` + `README`,
`Firmware/shared/` (linker script, `stm32.ini`, `osh_image.py`), the role libraries, every
production `platformio.ini`, `docs/firmware/platformio-setup.md`. Consumers: #363 (bootloader),
#366 (uploader), SkyHawkClient#177.
39 changes: 39 additions & 0 deletions Firmware/ScratchPad/FirmwarePlan/08-hardware-firmware-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,45 @@ netlist; overridable as `PDU_PIN_*`:

---

## Image Layout & Identity Block (#362)

Every production STM32 image — the Templates, `Panels/`, and the `Examples/` nodes; not the
per-library `Tests/` — carries a 32-byte **identity block** at a fixed offset from the image's start.
The bootloader (#363) checks it at every boot and before it commits an update; the uploader (#366)
and the client read it from the file before anything is sent. Struct, constants and CRC test
vectors: `Libraries/FirmwareImage/FirmwareImage.h` (TechSpec `FirmwareImage`). Rationale: D25.

| Offset in block | Size | Field | Value |
|---|---|---|---|
| 0 | 4 | `magic` | `0x594B534F` ("OSKY" in memory order) |
| 4 | 2 | `layoutVersion` | 1 |
| 6 | 2 | `blockSize` | 32 — a newer layout may grow; readers parse the prefix they know |
| 8 | 4 | `imageLength` | bytes from the image's start, padded to 4 — stamped after linking |
| 12 | 4 | `imageCrc` | CRC-32/MPEG-2 over `imageLength` bytes, this word skipped — stamped after linking |
| 16 | 1 | `nodeId` | `NODE_ID` (D10) |
| 17 | 1 | `nodeKind` | `NodeKind` of the role library (PANEL_GROUP / PDU / BRIDGE) |
| 18–20 | 3 | `fwMajor/Minor/Patch` | `FirmwareVersion.h` — the version `READY_n` reports |
| 21 | 11 | reserved | 0 |

- **Where.** Image start + `0x200`, after the vector table (0x10C bytes on the F103). The shared
linker script `Firmware/shared/stm32f103.ld` places it, so it moves with the image: `0x08000200`
today, `0x08001200` once the bootloader takes the first 4 KB (`board_build.flash_offset = 0x1000`,
enabled by #363 in `Firmware/shared/stm32.ini`).
- **CRC.** Poly `0x04C11DB7`, init `0xFFFFFFFF`, no reflection, no final XOR, over 32-bit
little-endian words — what the F103's hardware CRC unit computes. The stamped `imageLength` is
inside the CRC; only the `imageCrc` word is skipped.
- **Gaps are 0xFF.** Erased flash reads 0xFF and an ST-Link upload leaves gaps erased, so the linker
fills the gap before the block with 0xFF and the post-build step (`Firmware/shared/osh_image.py`)
writes `firmware.bin` with `--gap-fill 0xFF`. The `.bin`, the `.elf` and a board's flash read back
over `imageLength` all give the same CRC (bench-checked on a Gateway's bridge, 2026-10-25).
- **Outside the image.** Flash past `imageLength` is not covered — the PDU calibration page
(`0x0800FC00`, below) keeps its own record CRCs.
- **Who defines it.** The role library (`PanelGroup.cpp`, `PduNode.cpp`, `PanelBridge.cpp`) through
`OSH_FIRMWARE_IDENTITY(kind)`. A sketch without a role library has no block, and the post-build
step says so instead of stamping.

---

## Status LED — Bi-Color (Red/Green)

Each custom OpenSkyhawk STM32 board has a bi-color red/green status LED driven by two dedicated
Expand Down
Loading
Loading