Deep dive · FPGA gateware
Under the onboard flight software sits a deterministic FPGA gateware layer — the "when" to the software's "what." Eight SystemVerilog blocks guarantee jitter-free IMU sampling, four bit-exact DShot600 motor streams, closed-loop motor-RPM decode, and a hardware failsafe the software cannot override. Written vendor-neutral for Microchip PolarFire, verified, and ready for Siemens Questa.
The partition
Control laws are math that changes with tuning and mission — they belong in software. Sub-microsecond timing, four synchronized motor waveforms and a fail-safe that must hold even if the software hangs are hardware guarantees — they belong in an FPGA. Splitting them cleanly is what makes the vehicle both flexible and safe.
Software — the "what"
The onboard flight software — control laws, mixing and mode/failsafe logic — validated end-to-end in the Amesim 6-DOF model (position hold, gust rejection, waypoint mission). It sets the motor targets; it never touches the wire timing.
FPGA — the "when"
A single-clock (48 MHz) gateware layer that samples the IMU on an exact 8 kHz beat, emits four bit-exact DShot600 streams, timestamps every sample, and enforces a hardware arm/failsafe that software cannot override. Written vendor-neutral for Microchip PolarFire.
FPGA · Siemens EDA
Eight SystemVerilog blocks, all on a single 48 MHz clock so DShot600 lands on exact integer cycle counts (bit = 80, T1H = 60, T0H = 30 — zero timing rounding). Each block ships with a self-checking testbench; because Questa wasn't yet installed when they were written, each was also pre-validated against a cycle-accurate Python model before being marked ready.
| Block | Function | Verification |
|---|---|---|
| B1 | DShot600 TX ×4 → DSHOT_M1..M4 (bit-exact serializer + CRC) | TB rebuilds each frame from pin HIGH-widths · Python model, 7 vectors PASS |
| B3 | IMU SPI — MPU-6000 (mode-3, 8 MHz; config writes + 14-byte burst read) | TB vs. an MPU-6000 bus-functional model · edge-level Python model PASS |
| B5 | Sample-sync — 8 kHz IMU / 50 Hz baro / 1 µs ticks + 32-bit timestamp | TB checks tick periods + timestamp latch |
| B7 | Host register file — SPI slave (the MCU↔FPGA bus) | MCU-side SPI-master BFM exercises the map · Python protocol model, 13 checks PASS |
| B6+B9 | Safety interlock + watchdog — hardware arm/failsafe, LED/buzzer | FSM TB, 14 checks + invariant · two concurrent formal SVA properties |
| B8 | Clock / reset / PLL — 48 MHz clk_sys, sync'd reset | TB checks period, lock-gated release, 2-flop CDC latency |
| B2 | eRPM decode — bidirectional-DShot GCR / CRC decode datapath (closed-loop motor RPM) | GCR/CRC round-trip model, 16 vectors + reject PASS |
| B4 | Barometer I2C — DPS310 (400 kHz open-drain, config + pressure/temp read) | byte/ACK sequence PASS · ⚠ bring-up (I2C bit-phase to confirm in sim) |
The seam — HW ↔ SW
The MCU talks to the FPGA over a plain SPI-slave register file — deliberately not AXI + a soft-CPU — for a clean physical HW/SW split, MCU-vendor independence and a small verification surface (ADR-001). Software writes motor setpoints and reads sensor data + timestamps; the FPGA answers within a single clock.
| Address | Register | Dir |
|---|---|---|
| 0x00 | WHOAMI (returns 0xC2) | read |
| 0x01 / 0x02 | STATUS / FAULT | read |
| 0x10–0x1D | IMU sample (accel / temp / gyro, ×7 hi/lo) | read |
| 0x1E–0x21 | Sample timestamp (32-bit) | read |
| 0x40–0x47 | Motor 1–4 setpoints | write |
| 0x48 | CONTROL (bit 0 = arm) | write |
| 0x49 | HEARTBEAT (feeds the watchdog) | write |
Where it sits in the E/E design: the FPGA slots between the flight controller and the ESC, driving the existing DSHOT_M1..M4 nets and reading the IMU/baro buses. Adding it as its own device to the AI-generated Capital schematic — so the compute layer joins the same electrical thread — is the next electrical step (today's Capital design carries the ESC, dual flight controllers and power rails, not yet the FPGA).
The code
This isn't pseudocode — it's the actual SystemVerilog the agent wrote: self-documenting, vendor-neutral, and verified. Two excerpts below; every block's full source is one click away.
// The hardware safety gate. Motor outputs are forced to DShot "0" unless the // system is ARMED — and it can only be ARMED while ALL of: the MCU arm-request, // a fresh MCU heartbeat, and a valid RC link. Lose any one and it disarms // instantly and latches a lockout (no silent re-arm on a flickering link). // // SAFETY INVARIANT (formal-proven): (armed == 0) -> (all motors == 0) // The motor gate is combinational from `armed`, so it holds by construction: assign motor_out1 = armed ? motor_in1 : 16'd0; assign motor_out2 = armed ? motor_in2 : 16'd0; assign motor_out3 = armed ? motor_in3 : 16'd0; assign motor_out4 = armed ? motor_in4 : 16'd0;
The entire hardware failsafe is those four lines: motors can be non-zero only while armed — and the arm FSM above them sets armed only when the MCU, the heartbeat and the RC link all agree. That's why the safety property is provable, not merely tested.
// Deterministic DShot600 serializer for one BLHeli_32 ESC channel. // Frame = 16 bits, MSB-first: [15:5] throttle · [4] telemetry · [3:0] CRC. // Each bit is one BIT period, held HIGH for T1H (a "1") or T0H (a "0"). // DShot600: T = 1667 ns, T1H = 1250 ns, T0H = 625 ns. // At 48 MHz these land on EXACT integer cycle counts — zero timing error: localparam int BIT_TICKS = CLK_HZ / 600_000; // 80 -> 1666.7 ns localparam int T1H_TICKS = (BIT_TICKS * 3) / 4; // 60 -> 1250 ns localparam int T0H_TICKS = (BIT_TICKS * 3) / 8; // 30 -> 625 ns
Choosing a 48 MHz clock makes the DShot600 pulse widths fall on exact cycle counts — no rounding, no jitter. This is the determinism the FPGA exists to give.