Skip to Content
Viewing feat/fpga-bringup docs — not the stable branch. Changes vs main · Go to main
axiomOSfeat/fpga-bringupPlatformDrivers and Control Link

Drivers and Control Link

Relevant source files

This page covers the device-model crates, the drivers in kernel/src/driver, the console and watchdog hooks, and the Shrike control link that connects the Pi 5 to an external RP2040/FPGA safety sidecar.

Device model

Three small no_std crates under kernel/crates hold the portable part of the model; the kernel instantiates them with KernelDeviceId, a process-wide counter.

CrateContents
kernel_deviceDevice and DeviceId traits; BlockDevice trait with a const sector size (512 in the kernel); RawDevice trait (exposes a physical frame range) and RawDeviceRegistry; RegisterDeviceError::AlreadyRegistered
kernel_pciPciAddress (bus, device, function) with typed getters for vendor, device, header type, BARs and subsystem id; ReadConfig/WriteConfig/ConfigurationAccess traits; PortCam (x86 port I/O config access at 0xCF8/0xCFC)
kernel_devfsDevFs filesystem implementing the kernel_vfs FileSystem trait; DevFile nodes Null, Zero, Serial and BlockDeviceFile; ArcLockedDevFs wrapper

Driver availability by target

Driverx86_64aarch64 virtaarch64 rpi5
PCI bus scan and driver registryYesNo (module gated to x86_64)No
VirtIO blockYes, over PCIYes, over MMIONo
VirtIO GPUYes, over PCI (draws a test pattern)NoNo
Embedded RAM diskNoNoYes (root filesystem)
IIO managerYesYesYes
Serial consoleCOM1 16550PL011PL011 UART10
GPIO, PWM, control linkNoNoYes (RP1)

PCI and VirtIO (x86_64)

pci::init in driver/pci.rs scans all 256 buses and 32 slots with a port-I/O configuration access method, expands multifunction devices to 8 functions, and matches each address against PCI_DRIVERS. That registry is a linkme distributed slice: a driver registers a PciDriverDescriptor with a name, a type (Generic or Specific), a probe function and an init function.

Selection rules:

  • A Specific driver wins over a Generic one.
  • Two matches of the same type is an error: the device is skipped and an “ambiguous PCI driver match” error is logged.
  • A failing init is logged and does not stop the scan.

Registered drivers:

DriverMatchBehavior
virtio-blkvendor 0x1af4, device 0x1000 to 0x103f, subsystem 0x02Builds a VirtIO block device using the virtio_drivers crate and registers it as a block device
virtio-gpuvendor 0x1af4, device 0x1050Sets up a framebuffer, fills it with a gradient pattern, flushes, and registers a RawDevice that exposes the framebuffer’s physical frames

virtio/hal.rs implements the virtio_drivers Hal trait on the kernel’s physical and virtual memory managers (DMA allocation, MMIO mapping, address translation).

VirtIO MMIO (QEMU virt)

virtio/mmio.rs, compiled only for aarch64 with virt, probes 32 fixed slots from 0x0A00_0000 in 0x200-byte steps. MmioTransport::new validates the magic value and version; empty slots are skipped silently. Only DeviceType::Block is initialized; other device types are recognized and ignored, and the source carries a TODO for network, console and GPU.

Block layer and devfs

driver/block.rs keeps a global map of block devices. BlockDevices::register_block_device assigns a sequential id, publishes a node named blkN in the devfs (mounted at /dev, so it appears as /dev/blkN), and then inserts the device in the registry while holding the devfs write lock so the node cannot be observed before the registry entry. Errors are typed (invalid generated path, devfs failure) and the registry is unchanged on error. At boot the kernel looks up block device 0 and mounts it as an ext2 root filesystem.

RamBlockDevice (driver/ram.rs) is a 512-byte-sector block device backed by heap memory. On rpi5 it is created from a disk image compiled into the kernel (EMBEDDED_DISK_PATH) and registered as block device 0. This is how the Pi 5 image gets its root filesystem today; there is no SD, NVMe or USB storage driver.

RawDevices (driver/raw.rs) registers raw devices in a registry; its own TODO notes that no VFS entry is created for them yet.

The devfs is initialized in file/devfs.rs with /serial, /stdin (a Null device), /stdout and /stderr (both Serial writers to the kernel console), under a source TODO for proper stdio. Serial is write-only: reads return end-of-file.

Serial console

serial.rs provides serial_print/serial_println per architecture:

  • x86_64: a lazily initialized COM1 (0x3F8) 16550 port; interrupts are disabled around each write.
  • aarch64 rpi5: the PL011 UART10 UART global, with IRQs masked around the lock if they were enabled. In bench builds, once the bench is active, output goes to a deferred buffer instead (bench_buffer.rs): 16384 bytes, 1024-byte records, drained at most 64 bytes per timer tick with non-blocking sends. Overflow produces a PI5_BENCH_LOG_LOSS marker, and any loss invalidates serial-derived evidence.
  • aarch64 virt: the PL011 at 0x0900_0000.

Industrial I/O (IIO)

driver/iio.rs is the sensor-event path that feeds BPF programs. Its header says that “in the future” it will interface with real I2C/SPI drivers and that for now it only provides the mechanism to inject sensor events and trigger BPF hooks. IioManager holds IioDevice entries with channels (for example AccelX, AccelY, AccelZ) and dispatch_event runs the attached IIO hooks.

At boot init_simulated_device registers a simulated accelerometer (id 0). On x86_64 and non-Pi aarch64 builds a background task generates events; on rpi5 that task is skipped on purpose so it does not preempt init or benchmarks. The Sensor frames arriving from the Shrike link are injected into IIO with a synthetic device id for an ultrasonic proximity channel; there is no real I2C or SPI sensor driver.

Watchdog

watchdog.rs is ten lines: trigger_estop and invariant_failed(reason) log and call actuation::watchdog_estop_trigger, which latches the actuation monitor, applies safe-drive values to the outputs, notifies the control link to assert a soft e-stop, and prints a V04_ESTOP ... source=watchdog record. It is a software hook, not a hardware watchdog: the README lists “No hardware watchdog integration” (issue 72) under known limitations.

Actuation seam

actuation.rs is the single path through which actuation requests (guard_pwm, guard_motor, guard_motor_pair, guard_gpio, e-stop operations) are decided by a global monitor from kernel_bpf::actuation and then applied to RP1 MMIO or sent over the control link. The decision logic is a pure, host-tested crate; this file owns the global monitor, an APPLY_LOCK that is always taken with IRQs masked (it is reachable from both thread context and BPF hooks firing in IRQ context), and the MMIO application. Details of the policy belong to the BPF and safety documentation.

What Shrike is

From firmware/shrike/README.md: “RP2040 safety/control sidecar and simulation.” The hardware profile is Shrike-lite V1.0/R0.4, whose electrical sources are hash-pinned under vendor/, together with an upstream ForgeFPGA scaffold. So it is a board pairing an RP2040 microcontroller with a ForgeFPGA, not an FPGA soft core running axiomOS. Firmware lives outside the kernel workspace and is built and versioned separately.

DirectoryRole
firmware/shrike/controlPlatform-independent no_std control and motor state machines (shrike_control)
firmware/shrike/rp2040Physical RP2040 firmware and target config (shrike_rp2040)
firmware/shrike/simulationMock-HAL host simulation and state-machine tests (shrike_rp2040_host_sim)
firmware/shrike/fpgashrike_safety_gate.sv reference and testbench for the ForgeFPGA project
firmware/shrike/vendor, recoveryPinned vendor sources and recovery material

The README’s R0.4 pin plan reserves RP2040 GPIO0 to 3 for FPGA SPI, GPIO12/13 for FPGA power and enable, GPIO14/15 for the post-configuration FPGA link and GPIO4 for the LED, with provisional assignments for the e-stop observe line (GPIO5), motor direction (GPIO6 to 9), ultrasonic (GPIO10/11) and the Pi UART (GPIO16/17).

On feat/fpga-bringup, GPIO14/15 are retained MCU-to-FPGA interconnects whose PWM the RTL ignores: GPIO14 now drives the FPGA runtime reset rst_n (started low as a push-pull output and driven low in the disabled adapter’s force_safe path), replacing the former left-PWM channel. GPIO6 to 9 belong to the legacy MCU direction adapter; the operational path uses the FPGA’s own direction outputs. GPIO18/19 are unused and must not be connected to an L298N.

The Shrike README states that the current RP2040 firmware “deliberately remains safe-low and non-operational” because the validated bitstream/timing manifest and concrete paired UART/watchdog/e-stop runtime adapter are absent, and that a fpga-runtime feature is a compile error until those exist. The FPGA safety gate is described as a reference whose 50 MHz clock and 20 kHz carrier defaults “require measurement and motor-driver validation on assembled hardware”, and the runbook says the RTL gate proves only the truth table. This branch adds a synthesized, routed design with setup-timing evidence and Forge-confirmed pins (see Shrike FPGA Safety Gate), but nothing has been programmed and hold/pulse timing, continuity and physical acceptance remain open. Both RP2040 firmware and the Pi 5 are tiered “Supported after HIL” in ADR-0003, which means that HIL evidence is still required.

Final PWM authority

Per the FPGA README, the FPGA generates the motor PWM from accepted signed commands and ignores the retained *_pwm_in pins. The physical e-stop is an active-low latching line routed directly to the FPGA and must not be generated by RP2040 firmware. A single invalid command disables both motors; e-stop assertion forces both outputs low combinationally; deassertion goes through a two-stage synchronizer and stays disarmed until a fresh, complete motor transaction arrives. The runbook’s HIL step probes both outputs around a plus or minus 800 per-mille limit. The FPGA also owns motor direction, and receives commands over SPI as a 12-byte frame followed by a separate A5 00 status read; see Shrike FPGA Safety Gate.

Paired motor sink

On this branch the RP2040 control loop in shrike_control no longer writes the two wheels independently. control::run takes a MotorPairSink:

pub trait MotorPairSink { type Error; fn apply(&mut self, seq: u8, left: i16, right: i16) -> Result<(), Self::Error>; /// Trusted fail-safe path; implementations must make both outputs safe. fn inhibit(&mut self); }

FpgaLifecycle implements it: apply calls runtime_command with the accepted Pi sequence and both signed duties, and inhibit calls fail_safe. Behavior recorded in the source and bench plan:

  • Each fresh pair is submitted once, in decode order; cached watchdog output is never replayed, and heartbeats or status reads do not manufacture freshness.
  • Zero-before-reverse ordering holds even when both frames arrive in one RX batch. The separate A5 00 acknowledgement with a matching sequence is required.
  • The caller must configure the sink and establish zero output before run. The loop never configures, re-arms or creates a link session.
  • run returns a RunSummary with a RunTermination of IterationLimit, Stop(StopReason) (HardwareEstop, SoftwareEstop, WatchdogExpired) or Fault(FaultReason) (Decode, UnexpectedMessage, MotorSink, Io). On any stop or fault it calls inhibit, resets the transport and hands the borrowed peripherals back for explicit requalification; it never automatically resumes.
  • ByteIo changed from a best-effort write to a fallible try_write (returning Ok(0) on backpressure) plus reset, with an associated Error type.

The host simulation adds paired_control.rs (515 lines) and fpga_lifecycle.rs, and its mocks replace MockMotor with MockMotorPair.

Reverse telemetry transport

The new transport module bounds Shrike-to-Pi transmission:

  • TelemetryTx owns at most one active and one pending frame. Overflow rejects a whole message and the caller counts the loss (RunSummary::telemetry_frames_dropped). It carries telemetry only, not safe acknowledgements.
  • service makes at most 2 * MAX_FRAME one-byte nonblocking attempts. A byte advances only when the UART accepts it, using the new shrike_link::tx::TxState::peek_byte. bytes_written counts accepted bytes, not observed delivery; hardware FIFO loss on reset is unknown.
  • LinkQuiescence is a local reset-drain prerequisite requiring QUIET_US = 200_000 (200 ms) of continuous quiet. Each poll drains at most 64 bytes, any received byte restarts the interval, and clock regression poisons readiness. Since commit 41a5a01 the timer starts after reset completes and each receive attempt is bracketed by the monotonic clock, so reset and receive handling time do not count as quiet. It is not a session or re-arm token, and host tests cannot prove the remote peer or UART is quiescent.

kernel/crates/shrike_link is the shared no_std crate used by both the kernel and firmware. Frame layout:

SYNC(0x7E) VER(0x01) TYPE LEN PAYLOAD[LEN] CRC_LO CRC_HI

CRC16-CCITT/FALSE (polynomial 0x1021, init 0xFFFF) covers version through payload. Maximum payload is 16 bytes. The decoder is a byte-fed state machine; once LEN is accepted, payload and CRC are consumed by count, so 0x7E inside a payload is data and no COBS framing is needed.

TYPEDirectionMessage
0x01Pi to ShrikeMotorSetpoint: sequence, signed left and right duty in per-mille (not clamped by the codec)
0x02Pi to ShrikeEstop: advisory soft e-stop
0x03Pi to ShrikeHeartbeatToShrike
0x81Shrike to PiSensor: ultrasonic echo in microseconds, e-stop line state, flags
0x82Shrike to PiHeartbeatToPi

The codec does not clamp duty: the kernel actuation monitor and the FPGA envelope are the clamp authorities. Unknown-type frames with a valid CRC are consumed so the stream stays aligned.

Other modules: watchdog (Shrike-side fail-safe: SafeStop on command silence, e-stop latch that dominates a simultaneous fresh setpoint, anti-replay by sequence number, cold-start SafeStop; heartbeats cannot restore motor authority), session (Pi-side liveness and heartbeat scheduling), tx (transmit state with motor-command priority, replacement and expiry), ring (fixed byte ring buffer) and motor (per-mille to PWM and direction math; v0.4 is forward-only on the ARM-A path, reverse is deferred).

Kernel transport on the Pi 5

control_link.rs runs the transport in a dedicated kernel thread (spawn(), called from main after init) so RX side effects (IIO dispatch, e-stop) and motor TX happen in thread context where taking the BPF manager and apply locks is safe. The timer IRQ does nothing for the link.

ParameterValue
UARTRP1 UART0, GPIO14/15, 115200 baud, Pl011 driver, polled
RX ring256 bytes; per-poll caps of 64 bytes RX and 64 bytes TX
Inbound-silence timeout80 ms (kept below the 100 ms RP2040 link timeout noted in the source)
Heartbeat period20 ms (50 Hz) while alive
Motor channelsActuation chip 0, channel 1 = left, channel 2 = right; other channels keep local RP1 PWM
Failure behaviorFail-closed: link replaces local PWM for mapped channels; send of the safe motor pair is available

Inbound Sensor frames feed the IIO hook and the hardware e-stop path; outbound motor commands are the monitor-clamped values. Public helpers are link_alive, rx_overflow_count, send_motor_pair, send_safe_motor_pair, command_estop and send. Side effects run after the link lock is released to avoid nesting with the BPF manager or apply lock.

The runbook’s section 4.3 is the verification plan: flash only the reviewed RP2040 binary, capture startup, framing, watchdog/fail-safe, invalid-frame and loss-of-link behavior with motors disconnected, and confirm that e-stop and out-of-range commands yield a motor-disabled state before any powered test. The repository documents these as pending checklist items, not as completed results.

Last updated on