Boards: the store and your own collections
A project is usually built on boards someone else designed: an ESP32 devkit, an
STM32 Nucleo, a Raspberry Pi, a Jetson. archec ships a store of known boards
(arche-hw-store), and you keep your own in collections of the same shape.
A board says what it is, the processors on it (what archec gen --target embassy
builds for), and its connectors with the pins they are wired to.
Using a board#
archec board list # the store, and your collections
archec board show nucleo-g474re # processors, connectors, pins
archec board add main.arche nucleo-g474re --as ctl
board add writes three things, and keeps them only if the project builds as
well as before:
project Rover {
uses arche-hw-store "^0.1" // the store, built into archec
}
include "arche-hw-store/boards/nucleo-g474re/nucleo-g474re.arche"
system RoverHw "Hardware" { // the deployment's platform, or this one
part ctl: NucleoG474RE
}
A binary on the board runs on its processor; a board with more than one
(the Kria KR260: apu, rpu) asks which:
deployment D {
system Rover
platform RoverHw
binary fw on ctl { thread t { runs controller } } // ctl.mcu, the STM32G474RE
route controller.bus -> motor.bus over can_bus { id 0x120 }
}
The bus is linked to the board's connector (ctl.can0 -> can_bus), and
archec gen --target embassy drives the CAN controller on the board's pins —
FDCAN1 on PA11/PA12 here, an MCP2515 on the nRF52840 DK's Arduino header
(P1_15, P1_13, P1_14, CS P1_12, INT P1_03) — instead of its defaults.
--copy copies the board's folder into components/ instead, when you want to
change it; --into <System> picks the system; --as names the part.
What a board is#
/// ST NUCLEO-G474RE: an STM32G474RE (Cortex-M4F with FDCAN) on a Nucleo-64.
component NucleoG474RE "NUCLEO-G474RE" {
kind board
description "One 170 MHz Cortex-M4F, 128 KB SRAM, three FDCAN controllers…"
product { vendor "STMicroelectronics"; part-number "NUCLEO-G474RE"; url "https://…" }
component mcu "STM32G474RE" {
kind processor
compute { cores 1; core-type "Cortex-M4F"; hal stm32; chip "stm32g474re"; freq 170MHz; ram 128KiB; os bare-metal }
}
inout can0: can "FDCAN1, CAN FD (add a transceiver)" { pins { tx PA12; rx PA11 } }
inout uart2: uart "USART2 (ST-LINK virtual COM)" { pins { tx PA2; rx PA3 } }
in pwr: power "5 V USB"
}
kind boardis built in, likeprocessorandbus.product { vendor; part-number; url }is what you would order.- A processor says
halandchip(bare metal: what the Embassy crate builds for) oros(Linux: a host crate). pins { role pin; … }on a connector names the chip's pins, as the HAL crate does:GPIO4(esp32),PA11(stm32),PIN_16(rp),P1_15(nrf). The roles archec reads aretx,rxfor a CAN controller andsck,mosi,miso,cs,intfor an SPI one (the MCP2515), plussda,scl.
A board folder is a component folder: it uses only what it declares, the built-in port types and the standard library, so it works where it is and as a copy.
Pin planning#
archec pins main.arche
Hw.ctl — NUCLEO-G474RE · STM32G474RE (stm32)
PA2 can0 tx -> can_bus (the part's)
PA2 uart2 tx -> esp.uart0
PA3 uart2 rx -> esp.uart0
PA11 can0 rx -> can_bus
prints each board's pins: the port and role on each, where the port is linked,
and whether the part wrote it (part ctl: NucleoG474RE { inout can0 { pins { tx PA2 } } }
over the board's PA12). Then come the pin plan's warnings, which archec check
and the editor give too:
pin-conflict— one pin used by two ports of a board, among those the system links or whose pins a part writes (a board's connectors are alternatives until then: the Pico's SPI0 and its MCP2515 share pins), or by two roles of one port. A component thatextendsa board is told when a pin it writes is one of its board's connectors';pin-function— a pin its chip cannot use so: on the RP2040, UART TX only on GPIO 0, 4, 8…, RX on 1, 5, 9…, I2C SDA on even pins, SPI SCK/MOSI/MISO on their own, and one port's pins on one peripheral (TX on UART1, RX on UART0 is wrong); on the ESP32, GPIO34–39 are inputs, and the pins wired to the module's flash (GPIO6–11; 12–17 on the C3, 26–32 on the S3) are taken. The STM32 and nRF chips route their functions through tables (or any pin) archec does not hold: nothing is said there;pin-pair— a link whose ends do not pair:txmeets the other end'srx(SCK, MOSI, MISO, SDA, SCL meet the same role), and a CAN controller on a bus needs bothtxandrx;pin-level— a link between ports whosepins { level … }differ:pins { tx PIN_0; rx PIN_1; level 3.3V }on one side,level 5Von the other.levelis not a pin; the store's boards do not say theirs yet, so write it on the part (or in your own board) where it matters.
Zephyr devicetree#
archec gen main.arche --target devicetree -o generated
writes a devicetree overlay for each binary of the deployment that runs on a
board's microcontroller, named as west names the board
(generated/devicetree/steer_fw/boards/nucleo_g474re.overlay): copy its boards/
into the Zephyr application. In it, each CAN, UART, I2C and SPI connector of the
board that the platform links (steer_mcu.can0 -> chassis_can):
/ {
aliases {
arche-chassis-can = &fdcan1;
};
chosen {
zephyr,canbus = &fdcan1;
};
};
/* steer_mcu.can0 — FDCAN1, CAN FD (add a transceiver): tx PA12, rx PA11 */
&fdcan1 {
status = "okay";
pinctrl-0 = <&fdcan1_tx_pa12 &fdcan1_rx_pa11>;
pinctrl-names = "default";
bitrate = <500000>;
};
- the pins are the port's (a part's own
pinsover the board's), as each HAL names them: hal_stm32's pinctrl nodes,NRF_PSEL(…)groups, the RP2040 and ESP32 pinmux macros. The peripheral is the one the connector's name says (USART2,FDCAN1,TWAI0), on the RP2040 the one its pins belong to; - the speed is the link's
bitrate, else the bus's:bitratefor CAN (1 Mbit/s at most, a CAN FD bus addsbitrate-data),current-speedfor a UART,clock-frequencyfor I2C; - a CAN on SPI (the nRF52840 DK's and the Pico's MCP2515) is an
mcp2515node under the SPI, its CS and INT as GPIOs; an I2C device linked to the board is a node at itsaddress, the compatible yours to add; - each bus has an alias,
arche-<bus>: under Zephyr (__ZEPHYR__), thedrivers.cofarchec gen --target csends a CAN or UART bus's frames onDEVICE_DT_GET(DT_ALIAS(arche_<bus>)); itsmain.creturns and leaves the threads running. The C's platform layer isarche_rt_posix.con Zephyr's POSIX API (CONFIG_POSIX_API, in theprj.confwritten beside the overlay, once).
The overlays are checked with dtc in the tests, against stand-ins for the
board's own nodes; they have not been built with west against a Zephyr tree.
KiCad netlist#
archec kicad main.arche [CarHw] -o car.net
writes the wiring of a system (the one named, else the deployment's platform) as a KiCad netlist, which pcbnew reads (File → Import → Netlist) or eeschema compares with a schematic:
- each part whose ports carry signals is a symbol with no footprint: a board
is
A1,A2…, anything elseU1… (what is on a board is the board's). Its pins are its ports' signals: the board's pins by role (uart2.TX/PA2), else the port type'swires (wire canh "CAN_H"), else the usual ones of a built-in type (CAN_H/CAN_L, TX/RX, SDA/SCL, SCK/MOSI/MISO/CS, V+/GND). A part whose pins are its own (part pico: RaspberryPiPico { inout i2c0 { pins { … } } }) gets a symbol of its own; - a link is a net per signal: TX to RX crossed (named after the TX end,
pico_uart0_TX), SDA to SDA; everything linked to akind buscomponent shares its nets (chassis_can_CAN_H), and two TX/RX ends on one bus cross; every GND is one net; - a CAN connector with controller pins (
tx/rx, or an MCP2515's SPI) puts CAN_H/CAN_L on the bus side, and a note says the transceiver goes between.
archec kicad checks its output by reading it back in the tests (every net's
pins exist, no pin is on two nets); KiCad itself (kicad-cli) was not run on it.
Your own collection#
archec board new carrier-v2 --in ~/arche-boards --from nucleo-g474re --name "Carrier v2"
archec board check ~/arche-boards
archec board extract main.arche RoverHw.ctl --in ~/arche-boards # a board the model defines
A collection is a library: a folder with a library.arche (board new makes
it) and boards/<id>/<id>.arche, with pictures in boards/<id>/assets/. A
project sees it through libraries "<folder>" (board add writes it), and every
project on the machine does when it is in ARCHE_BOARDS (a path list) — the VS
Code setting arche.boardCollections sets that for the editor and its language
server. arche.lock records its version and fingerprint like any library's.
board check builds each board on its own and checks that its processors say
hal/chip or os, that its pins are pins of its chip, and that its picture is
there.
In VS Code#
The Boards view of the Arche side bar lists the boards the model uses (click: go to the part), the store, and your collections:
- on a board: Add to Model… (from the collection or as a copy, into which system), Show Board Details (processors, connectors, pins), Open Definition, Duplicate to My Collection…;
- on a collection: New Board…, Check Board Collection (the Problems view), Reveal Collection Folder;
- in the title bar: add, filter (name, vendor, chip, HAL, core), refresh, New Board…, Add Board Collection…;
- on a component of the Model view: Save as Board in My Collection…;
- drag a board from the view into a
.archeeditor:part esp32_c3_devkitm_1: Esp32C3DevKitM1is written where it is dropped (the part's name selected, to change it), in a newsystem <Project>Hw "Hardware"when it is dropped outside any block, and the board'sincludeand the project'susesare written asboard addwrites them. Dropped in another editor, it is thepartline as text.
The editor knows the boards too:
- completion after
part x:(andextends) offers the boards of the store and of your collections, with vendor and part number; choosing one the model does not include yet also writes itsinclude(anduses arche-hw-store "^0.1", orlibrariesandusesfor a collection of yours, in the file that has the project block). Inpins { … }on a port it offers the roles, each with the pin the board gives it on a port of that type (tx PA12), then, after a role, the board's pins with what they are wired to (PA11 — can0 rx, FDCAN1), the one of the same role and port type first. In a part of a board (part ctl: NucleoG474RE { … }, or a component thatextendsone) the board's connectors come with their type, name and pins, and choosing one writes it with its pins, to change them:inout uart2 { pins { tx PA2; rx PA3 } }. - hover on a board's name, or on a part of one, shows its vendor, part
number and URL, its processors (core, clock, RAM,
halandchip), its connectors with their pins, and the collection it comes from; on a pin inpins { … }, the chip it is on and what the board wires to it (PA11 — can0 rx, FDCAN1); on a role, what it is for. - a pin that the board's chip does not name so, on a board, a part of one or a
component that extends one, is a warning (
board-pin, also inarchec check), with the nearest pin as a quick fix and the pins the board's connectors use:NUCLEO-G474RE has no pin 'PB99': its STM32G474RE (stm32) names them like PA11. Did you mean 'PB9'?. So is a role archec does not read (tz). The check knows how each HAL names its pins, not each chip's pinout:PK15passes on a chip that has no port K.
The store#
| Board | Processor | Builds for | CAN |
|---|---|---|---|
| ESP32-C3-DevKitM-1 | ESP32-C3, RISC-V | hal esp32, esp32c3 |
TWAI on GPIO2/GPIO3 |
| ESP32-S3-DevKitC-1 | ESP32-S3, 2× Xtensa LX7 | hal esp32, esp32s3 |
TWAI on GPIO4/GPIO5 |
| ESP32-DevKitC | ESP32, 2× Xtensa LX6 | hal esp32, esp32 |
TWAI on GPIO21/GPIO22 |
| NUCLEO-G474RE | STM32G474RE, Cortex-M4F | hal stm32, stm32g474re |
FDCAN1 on PA12/PA11 |
| NUCLEO-F446RE | STM32F446RE, Cortex-M4F | hal stm32, stm32f446re |
CAN1 on PA12/PA11 |
| NUCLEO-H743ZI2 | STM32H743ZI, Cortex-M7 | hal stm32, stm32h743zi |
FDCAN1 on PD1/PD0 |
| Raspberry Pi Pico, Pico W | RP2040, 2× Cortex-M0+ | hal rp, rp2040 |
MCP2515 on SPI0 |
| nRF52840 DK | nRF52840, Cortex-M4F | hal nrf, nrf52840 |
MCP2515 on the Arduino header |
| Raspberry Pi 4, Pi 5 | BCM2711 / BCM2712 | Linux (std) | an MCP2515 HAT, SocketCAN |
| Jetson Orin Nano Developer Kit | 6× Cortex-A78AE | Linux (std) | — |
| Kria KR260 | K26: 4× Cortex-A53, 2× Cortex-R5F, PL | Linux (apu), FreeRTOS (rpu) |
— |
| Raspberry Pi 4 and 5 (Go driver) | BCM2711 / BCM2712 | Linux (Go: raspi.NewAdaptor) |
— |
| Jetson Nano Developer Kit | Tegra X1, 4× Cortex-A57 | Linux (Go: jetson.NewAdaptor) |
— |
| BeagleBone Black, PocketBeagle | AM3358, Cortex-A8 | Linux (Go: beaglebone, pocketbeagle) |
— |
| ASUS Tinker Board, Tinker Board 2 | RK3288 / RK3399 | Linux (Go) | — |
| FriendlyELEC NanoPi NEO, NanoPC-T6 | Allwinner H3 / RK3588 | Linux (Go) | — |
| Orange Pi 5 Pro | RK3588S | Linux (Go) | — |
| PINE64 ROCK64, Radxa ROCK Pi 4, Radxa Zero | RK3328 / RK3399 / S905Y2 | Linux (Go) | — |
None of these boards has a CAN transceiver: wire one between the pins and the bus.
In Renode#
archec gen main.arche --target renode --deployment D runs a deployment's
microcontrollers in Renode (1.17), without the boards: a
machine per processor with a binary, Renode's own description of its chip, the
binary's ELF loaded, a CANHub per CAN bus and a UARTHub per UART link of the
platform. archec board show says it per processor (Renode: …).
| Chip (boards) | Renode platform | CAN | Notes |
|---|---|---|---|
stm32h743zi (NUCLEO-H743ZI2) |
platforms/cpus/stm32h743.repl |
FDCAN1 (MCAN) | Renode's RCC does not keep FDCANSEL: the .resc makes it read back PLL1_Q, the clock the generated main.rs picks |
stm32f446re (NUCLEO-F446RE) |
platforms/cpus/stm32f4.repl |
CAN1 (bxCAN) | an STM32F407: more flash and RAM, the same CAN and USARTs at the same addresses |
nrf52840 (nRF52840 DK) |
platforms/cpus/nrf52840.repl |
— | Renode has no MCP2515: the board's CAN is not on the bus |
stm32g474re (NUCLEO-G474RE) |
— | no STM32G4 in Renode | |
rp2040 (Pico, Pico W) |
— | no RP2040 in Renode | |
esp32, esp32c3, esp32s3 |
— | no Espressif chip in Renode | |
| Linux processors (Pi, Jetson, Kria's APU…) | — | not emulated: their binary runs on a host; the .resc has a SocketCAN bridge for it, commented (Linux hosts) |
A board's CAN or UART port is the Renode peripheral its text names ("FDCAN1, CAN FD…",
"USART2 (ST-LINK virtual COM)"); a CAN port that names none is the first controller,
the one the generated firmware drives. A UART that names none is said, not guessed.
archec gen main.arche --target embassy --deployment Bench -o out
archec gen main.arche --target renode --deployment Bench -o out
(cd out/embassy/brake_fw && cargo build --release)
renode out/renode/Bench/Bench.resc # the machines, running
renode-test out/renode/Bench/Bench.robot # the checks
The ELF paths are variables at the top of machines.resc ($brake_fw), for a firmware
built elsewhere or in C. The Robot suite checks that each binary that talks on a bus has
started its CAN controller, and, for each test case, what Renode can see: a send from a
part it does not run is put on the bus (encoded as the codec lays the message out) and must
be read by the firmware; an expect sent("MSG") counts the frames the machines sent with
that message's CAN id. A part's state, inside the firmware, is not checked there.
examples/car-bench is the car's actuator firmware run this way.
Devices and their drivers#
The store also has devices, one folder each under devices/ (87 of them: sensors,
displays — below —, motors, LEDs, ADCs…), and the Linux boards above have their driver in Go
under boards/<board>/drivers/go/. Both come from Gobot v2.6.0
(Apache-2.0, see LICENSE-gobot and NOTICE), adapted so they import nothing of Gobot:
/// BME280 temperature, pressure and humidity sensor.
component Bme280 "BME280" {
kind device
product { vendor "Bosch Sensortec"; part-number "BME280"; driver "i2c.NewBME280Driver" }
inout i2c: i2c "I²C" { address 0x77 }
fn temperature -> float32 "Temperature (°C)"
fn humidity -> float32 "Humidity (%RH)"
}
drivernames the device's Go constructor indevices/go/<family>/(i2c, spi, gpio, aio, onewire); a board's names its adaptor.addressis the device's I²C address. Two devices at the same address on one bus is an error (bus-address); a part can move one (part env2: Bme280 { inout i2c { address 0x76 } }).- A Linux board exposes each header pin its driver knows as a port (
pin11, orp8_07on a BeagleBone), so a device's pin is linked to the board's:
system Bench {
part pi: RaspberryPi4
part env: Bme280
part led: Led
pi.i2c1 -> env.i2c
pi.pin11 -> led.pin
}
The Go code lives in one module, archehw (lib/arche-hw-store/go.mod): the runtime in
runtime/go/ (Gobot's interfaces as archert, the Linux access for I²C, SPI, GPIO, PWM,
analog and 1-Wire), the device drivers in devices/go/, the boards' drivers beside each board.
It needs nothing outside Go. Gobot's own tests come with it (go test ./... in
lib/arche-hw-store; npm test runs them).
Two tools keep it in step with Gobot:
go run ./tools/gobot-port <gobot checkout> lib/arche-hw-store "v2.6.0 (798be8c)" # the Go code
go run ./tools/gobot-catalog lib/arche-hw-store # devices/ and the boards' .arche
The vendors, part numbers, units and boards' processors are not in Gobot's code: they are in
tools/gobot-catalog/known.go and known_boards.go. The Raspberry Pi adaptor was written
before the Pi 5 and has not been tried on one.
Displays#
The screens of the store say what a ui on them takes: their hardware {display: {…}} —
the size in pixels, the density, the input, the controller.
| Device | Display | Input | Bus |
|---|---|---|---|
ili9341 |
2.8" 320×240, 143 dpi, RGB565 | touch (XPT2046) | SPI, DC, reset, backlight, touch IRQ |
st7789 |
2.0" 240×320, 200 dpi | keys | SPI, DC, reset, backlight |
st7789-240x240 |
1.3" 240×240, 261 dpi | keys | SPI, DC, reset, backlight |
ssd1306, spi-ssd1306 |
0.96" 128×64 mono, 149 dpi | keys | I²C 0x3C, SPI (Gobot's drivers) |
hdmi-display |
1920×1080, 92 dpi | pointer | HDMI, USB |
archec device list --displays
archec device add main.arche ili9341 --into RoverHw
A component with a ui { } and no display of its own takes the one display part beside
it — in a system it is a part of, else in the platform of a deployment of that system
(archec ui says "from the hardware"); the ui's own input wins over the display's. A
display of your own is a component with the same record:
component MyPanel "7\" panel" {
kind device
hardware {display: {width: 1024, height: 600, dpi: 170, input: "touch"}}
in hdmi: hdmi
}
archec gen --target ui-lvgl and ui-slint write the ui for it (ui).
The SSD1306's record is in tools/gobot-catalog/known.go, so the catalog writes it again.