Skip to content

board

Board manifest for FlintOS.

Each supported board is a submodule that exports:

  • BOARD — the board as one [Board] value, the facts an application asks about gathered with Option fields (see [Board])
  • TARGET_BUSES — physical bus definitions
  • TARGET_DEVICES — logical device attachments
  • TARGET_PERIPHERALS — direct peripheral mappings (bus controllers are not repeated here)

Every fact about the board that an application would otherwise have to look up in a datasheet: pins, base addresses, IRQ numbers, and the shape of anything attached. A pin without the count of what is on it is half a fact — RGB_LED_GPIO alone let an application drive one LED of a 25-LED panel and look correct while 24 stayed dark.

The active board is chosen at compile time via Cargo features, one per supported board. Exactly one must be enabled — the guards below turn “zero selected” and “more than one selected” into build failures instead of a silently wrong manifest, which on real hardware shows up as a very confusing bring-up bug (wrong pins, wrong IRQ, etc).

Downstream crates (namely kernel) never name a board module directly; they use board::active, which this crate re-exports to whichever board module was selected.

cargo build -p kernel --no-default-features --features board-m5-atom-matrix
cargo build -p kernel --no-default-features --features board-esp32-devkitc
cargo build -p kernel # default: board-esp32-wrover

Adding a new board: run make new-board NAME=<name> FROM=<board>, which clones an existing manifest to src/<name>.rs and does the wiring — the board-<name> feature in this crate and kernel, the pub mod and pub use … as active lines below, the SELECTED count, and (for an RP2040 board) the Makefile’s RP2040_BOARDS. You then edit only what differs (a pin). The device accessors (user_led, console, …) are keyed on the driver bundle (rp2040-drivers / esp32-drivers), not on board names, so a cloned board of a known family gets them for free. See the “Add a board” tutorial.

struct Board {
pub name: &'static str,
pub imu: Option<I2cAttachment>,
pub pmic: Option<PmicAttachment>,
pub touch: Option<I2cAttachment>,
pub display: Option<DisplayAttachment>,
pub rgb_led: Option<RgbLed>,
pub grove: Option<GrovePort>,
pub selftest: SelftestPads,
pub console: ConsolePins,
}

Everything an application asks a board manifest about, as one value.

Implements: Clone, Copy, Debug

References: ConsolePins, DisplayAttachment, GrovePort, I2cAttachment, PmicAttachment, RgbLed, SelftestPads

struct ConsolePins {
pub tx: u8,
pub rx: u8,
pub baud: u32,
}

The board’s console pins and baud.

Every board FlintOS runs puts its console on UART0, so the controller is implied; only the pins and baud are a board fact. Held as plain numbers so the one struct serves both SoC families — each board’s console_init builds the SoC-specific port from these.

Implements: Clone, Copy, Debug

struct DisplayAttachment {
pub port: soc_esp32::SpiPort,
pub dc: u8,
pub cs: u8,
}

An SPI display panel wired onto the board: the SPI port it is driven over and the two GPIOs — data/command and chip-select — the transport toggles. The panel’s own reset (an AXP192 GPIO on the Core2) is the driver’s init closure, not a manifest pin.

Implements: Clone, Copy, Debug

References: SpiPort

struct GrovePort {
pub sda: u8,
pub scl: u8,
}

A Grove/expansion port’s two free pins, if the board exposes one. The pwm example drives one of these; a board without a port sets None.

Implements: Clone, Copy, Debug, Eq, PartialEq, StructuralPartialEq

struct I2cAttachment {
pub port: soc_esp32::I2cPort,
pub addr: u8,
}

An I2C device wired onto the board: which controller and pins bring it up (an I2cPort, so “which controller” is no longer half-declared), and the 7-bit address it answers at.

Implements: Clone, Copy, Debug

References: I2cPort

struct LoopbackPads {
pub scratch: u8,
pub aux: (u8, u8),
}

The two spare pads a folded single-pad loopback needs, plus the pad the fold routes onto. See [loopback_pads].

Implements: Clone, Copy, Debug

struct PmicAttachment {
pub port: soc_esp32::I2cPort,
pub addr: u8,
pub rails: &'static [RailSetup],
}

A power-management IC wired onto the board: which controller and pins reach it, its address, and the ordered list of rails to bring up at boot.

This is the manifest fact that finally gives the board a power ordering to express — which rail must be up before the peripherals that sit on it. The system rail (DCDC1 on the Core2, powering the ESP32 itself) must never appear in rails; power_init refuses it rather than brown the CPU out.

Implements: Clone, Copy, Debug

References: I2cPort, RailSetup

struct RailSetup {
pub rail: axp192::Rail,
pub millivolts: u16,
}

One rail to bring up at boot: which Rail and at what voltage. A board’s [PmicAttachment::rails] is an ordered list of these.

The order is the inter-rail ordering the PMIC introduces — a peripheral rail up before the rail that depends on it. power_init walks the list in order, setting each rail’s voltage then enabling it.

Implements: Clone, Copy, Debug

References: Rail

struct RgbLed {
pub gpio: u8,
pub count: usize,
pub layout: Option<led_matrix::Layout>,
}

A board-owned addressable-LED strip or panel.

layout is None for a single LED (no geometry to fold) and Some for a panel — the same fact the RGB_LED_LAYOUT const carries, gathered here with the pin and the count so an application takes all three as a unit.

Implements: Clone, Copy, Debug

References: Layout

struct SelftestPads {
pub scratch: Option<u8>,
pub aux: Option<(u8, u8)>,
pub spi_slave: Option<[u8; 4]>,
pub pcnt: Option<u8>,
pub touch: Option<u8>,
pub mcpwm: Option<[u8; 3]>,
pub adc_external_high: Option<u8>,
}

Electrically-free pads a board offers the on-chip loopback self-tests and the bus/stream porting examples.

Each mirrors a loose *_GPIO const; gathered here so BOARD.selftest is one value. None on any field means “no free pad for that test on this board”, and the test or example skips and says so. Plain pin numbers, so the struct is arch-neutral.

Implements: Clone, Copy, Debug, Default

fn console() -> Option<&'static dyn hal::stream::ByteStream>

The board’s console as a ByteStream, or None if this board has no console driver.

fn console_init() -> bool

Bring up the console. Called once, first thing in startup::init.

Returns whether it came up at the board’s configured framing. false means the port rejected the config and this fell back to the bootloader’s settings (usually 115200 8N1, so still readable) — the kernel logs a warning on false. A board with no console driver returns true (nothing to warn about) and [console] returns None.

fn display_interface() -> hal::Result<Esp32DisplayInterface>

A ready DisplayInterface for the board’s panel: the shared SPI controller plus its data/command and chip-select pins, with DMA pixel streaming. Error::Other if this board declares no display.

Wrap the returned interface in a panel driver (ili9342c::Ili9342c::new). Allocates DMA buffers, so call it from the task that will draw. The caller must also connect the SPI controller’s interrupt for DMA completion — see [Esp32DisplayInterface].

Runs the panel at the board manifest’s declared speed. An application that wants a different clock uses [display_interface_at].

References: Result

fn display_interface_at(speed: hal::bus::BusSpeed) -> hal::Result<Esp32DisplayInterface>

Like [display_interface], but runs the panel at speed — clamped to the board manifest’s declared maximum.

The split is deliberate. The manifest’s max_speed is a hardware fact the board author owns: how fast this panel and its wiring can be clocked without glitching. An application owns a different question — whether to trade some of that speed for margin (long jumper wires, a stacked module, EMI headroom) — so it may ask for less. It may not ask for more: a request above the manifest ceiling is capped at the ceiling, never granted, because the board, not the app, is what knows the wire.

References: BusSpeed, Result

fn fold_spi_loopback() -> hal::Result<()>

Fold SPI2’s MOSI and MISO onto the loopback scratch pad, making the on-chip loopback the spitxrx example runs on. Error if this board declares no loopback pads.

Kept separate from [loopback_spi] because init refuses two signals on one pad: the bus is brought up on three distinct pins first, then this folds MISO onto the MOSI pad. Routing is safe — ownership of the pad is the proof — so the app calls this instead of reaching for Esp32PinMux.

References: Result

fn imu_bus() -> hal::Result<&'static i2c_bus::I2cController<esp32_i2c::Esp32I2c>>

The onboard IMU’s I2C bus. Error::Other if this board declares no IMU (BOARD.imu is None).

The returned I2cController is shared with any other device on the same controller (see [i2c_controller]); take a device handle with .device(addr) for the address to talk to.

References: Esp32I2c, I2cController, Result

fn lcd_spi() -> hal::Result<&'static esp32_spi::Esp32Spi>

The display panel’s SPI controller, opened once and cached.

Returned as the concrete Esp32Spi rather than a bus, because the display transport streams pixels by DMA (exchange_async), which the byte-capped Bus layer does not expose — and because the DMA completion interrupt’s top-half needs this same shared controller to acknowledge it. Error::Other if this board declares no display.

References: Esp32Spi, Result

fn led() -> Option<RgbLed>

The board’s addressable LED, if it has one.

This returns the LED’s identity — its pin, count and fold — not a driven strip. Driving a WS2812 over the RMT needs an interrupt handler and a frame buffer sized to the LED count, which an application owns (see apps/examples/blink); the board’s job is to say which LED is there. An accessor that returned a live strip would have to move that ISR into the board crate, which no board consumer wants today.

References: RgbLed

fn led_rmt(divider: u8) -> hal::Result<rmt::Rmt>

Open RMT channel 0 on the board’s addressable-LED pin, with divider as the clock divider (the caller sizes it from the LED’s pulse timing). Error if this board declares no addressable LED.

The board gates the clock, routes the signal and enables the pad’s output; the app keeps the frame buffer and the interrupt handler. The claim inside [rmt::Rmt::on_pin] proves single ownership, so there is no static mut.

References: Result, Rmt

fn loopback_pads() -> Option<LoopbackPads>

The free pads for the loopback bus/stream porting examples, or None if this board declares none.

scratch is the single data pad the fold routes onto; aux are the two spare pads init needs to see distinct pins before the fold. See the DevKitC manifest for the electrical story.

References: LoopbackPads

fn loopback_spi() -> hal::Result<&'static spi_bus::SpiBus<esp32_spi::Esp32Spi>>

A SPI bus opened on the board’s loopback pads (SPI2, mode 0, 4 MHz), for the spitxrx porting example. Error::Other if this board declares no loopback pads.

The bus is brought up on three distinct pads; the caller then folds MISO onto the MOSI pad to make the on-chip loopback (init refuses two signals on one pad, so the fold cannot be part of bring-up).

References: Esp32Spi, Result, SpiBus

fn pmic_bus() -> hal::Result<&'static i2c_bus::I2cController<esp32_i2c::Esp32I2c>>

The PMIC’s I2C bus: opens the controller on the board’s PMIC pins and caches it. Error::Other if this board declares no PMIC (BOARD.pmic is None).

Returns the I2cController; take a device with .device(axp192::ADDR) and wrap it in [axp192::Axp192]. Shares the controller with any other device on the same bus (the Core2’s IMU sits on the same I2C0) via [i2c_controller], so the two never fight to open it.

References: Esp32I2c, I2cController, Result

fn power_init() -> bool

Bring the board’s power rails up, in the manifest’s order. Called once by startup::init, after the console and before any rail-dependent device.

Returns true if every declared rail came up (or the board has no PMIC — nothing to do). A false means at least one rail write failed, or the manifest listed the system rail (DCDC1), which this refuses to touch: moving it under the running CPU browns the board out, so a board that lists it is a manifest bug caught here rather than a dead board.

Also switches on the battery ADC so a later [pmic_bus] caller can read charge state; a failure there is not fatal to bring-up and is ignored.

fn pwm(pin: u8, freq_hz: u32, res_bits: u8) -> hal::Result<PwmChannel>

Open LEDC high-speed channel 0 on pin at freq_hz/res_bits, with the pad left readable so the caller can measure its own output.

The claim inside the constructors proves single ownership, so there is no unsafe and no static mut. pin is passed in rather than read from the manifest so this serves any board’s free pad; pwm supplies its Grove-port SDA pin.

References: PwmChannel, Result

fn read_pwm_pin(pin: u8) -> bool

Read the level of pin back through the GPIO controller. true is high.

The counterpart to [pwm]: sampling the pad LEDC drives is how the example measures its own duty cycle with no instrument attached.

fn touch_bus() -> hal::Result<&'static i2c_bus::I2cController<esp32_i2c::Esp32I2c>>

The touch controller’s I2C bus. Error::Other if this board declares no touch panel (BOARD.touch is None).

Shares the controller with any other device on the same bus (the Core2’s touch, IMU and PMIC are all on I2C0) via [i2c_controller]; take a device handle with .device(addr).

References: Esp32I2c, I2cController, Result

fn uart_loopback() -> hal::Result<&'static dyn hal::stream::ByteStream>

Open UART2 in internal loopback on the board’s free pads and return it as a byte stream. Error if this board declares no loopback pads.

The port is cached in an api::Once, so repeated calls hand back the same stream. The pins are routed for real (bring-up is exercised) but the data travels the on-chip TX→RX path, so no pad edge can mis-frame it.

References: Result

const ESP32_CONSOLE: ConsolePins;

The ESP32 console: UART0 on GPIO1 (TXD0) / GPIO3 (RXD0) at 115200 8N1 — the same on every ESP32 board here. The one source of truth for both a manifest’s console field and its UART0 BusConfig::uart_8n1(..), so the pins and baud cannot drift between the two.

References: ConsolePins

const LED_RMT_IRQ: u8;

The chip’s RMT interrupt source, for wiring the channel to a CPU interrupt with api::interrupt::connect.