embassy-boot-ch32
embassy-boot support for WCH CH32
microcontrollers driven by
ch32-hal: a NorFlash adapter, a
linker-script driven partition map, and a pair of
bootloader / application examples
that build for 20 parts across 5 chip families. The update transport is
selectable: a serial console on every part, USB DFU 1.1 on the ten parts that
have a working ch32-hal USB controller, and CAN bus on the thirteen parts
that have a CAN controller. Tested on real hardware on the
CH32V305RBT6.
The examples default to the
nanoCH32V305 board: its status LED is
on PA3, active low, and both consoles live on USART1 (PA9 TX /
PA10 RX) at 115200 8N1. Both are one-line changes in src/main.rs for
another board.
The crate provides the glue that embassy-boot needs on top of ch32-hal:
CoarseFlash— wraps the 256 byte page flash driver so embassy-boot sees a coarser (and much cheaper) erase granularity,- partition accessors generated from the linker script symbols
(
__bootloader_active_start,__bootloader_dfu_start,__bootloader_state_start), BootLoader::load()— jumps from the bootloader into the active partition,system_reset()— a software reset through the QingKe PFIC.
Supported chips
One cargo feature per part (the lower case part number, same spelling as in
ch32-hal); build.rs of each example resolves it to a partition map and to
the target the part needs.
| family | core | target | parts | partition map |
|---|---|---|---|---|
| CH32V203 | QingKe V4B, no FPU | riscv32imc-unknown-none-elf |
C8T6, C8U6, F8P6, F8U6, G8R6, K8T6 | flash64k-ram20k.x |
| CH32V208 | QingKe V4C + Bluetooth, no FPU | riscv32imc-unknown-none-elf |
CBU6, GBU6, RBT6, WBU6 | flash128k-ram64k.x, -usb |
| CH32V303 | QingKe V4F | riscv32imfc-unknown-none-elf |
CBT6, RBT6 (128 KiB), RCT6, VCT6 (256 KiB) | flash128k-ram32k.x, flash256k-ram64k.x |
| CH32V305 | QingKe V4F, USB OTG HS | riscv32imfc-unknown-none-elf |
FBP6, GBU6, RBT6 | flash128k-ram32k.x, -usb |
| CH32V307 | QingKe V4F, Ethernet | riscv32imfc-unknown-none-elf |
RCT6, VCT6, WCU6 | flash256k-ram64k.x, -usb |
The canonical list lives in examples/chips.rs, which both
build.rs files include!, so the two binaries can never disagree about the
geometry. tools/build_matrix.sh --list prints it.
The ten parts marked -usb above can also serve updates over USB DFU
(see USB DFU): the four CH32V208 parts (USBD), the CH32V305 FBP6 /
GBU6 (USBHS) and RBT6 (OTG_FS), and the three CH32V307 parts (OTG_FS). The
CH32V203 parts that have a USBD controller only have 64 KiB of flash, too little
for the 32 KiB USB bootloader, and the usb/v2fs block of the CH32V303 family
has no driver in ch32-hal, so both families stay serial-only.
The thirteen parts with an on-chip CAN controller — the four CH32V208 parts, the four CH32V303 parts, the CH32V305 GBU6 / RBT6 and the three CH32V307 parts (CH32V305FBP6 has no CAN controller) — can also take updates over CAN bus (see CAN bus), which is built for reprogramming one specific node on a multi-node bus. The CH32V203 family again stays out: with 64 KiB of flash there is no room for the 32 KiB CAN bootloader.
Not (yet) supported
A runtime bootloader needs a working flash driver, and ch32-hal only has one
for the flash IP version v3 — the CH32V2 and CH32V3 lines. For every other
version ch32-hal selects a stub (src/flash/mod.rs picks v3.rs under
cfg(flash_v3) and other.rs, where every operation is unimplemented!(),
otherwise):
| family | flash IP version |
state |
|---|---|---|
| CH32V103 | v1 |
no driver upstream |
| CH32V003, CH641 | v0 |
no driver upstream (16 KiB flash / 2 KiB RAM, too small anyway) |
| CH32X033, CH32X035 | x0 |
no driver upstream |
| CH32L103 | l1 |
no driver upstream |
Two further exclusions:
- CH32V203RBT6 is left out even though its flash IP is
v3, becausech32-haldoes not compile for it at all (upstream bug, verified on0766331). It is the only CH32V203 part with a 32-bit general purpose timer (TIM5, declared asGPTM32inch32-metapac), and inch32-hal/src/timer/mod.rstheforeach_interrupt!arm fortimer, GPTM32, UPexpands toTimerBits::Bits32unconditionally, while theBits32variant of the enum is#[cfg(any(ch32l1, ch32v208))]. Building any crate withch32-hal/ch32v203rbt6therefore fails witherror[E0599]: no variant ... named Bits32 found for enum timer::TimerBitsbefore a single line of this repository is compiled. - 32 KiB flash parts (CH32V203C6T6/F6P6/G6U6/K6T6 and friends) are too small for a 16 KiB bootloader plus two images plus a state partition.
Layout
.
├── src/lib.rs the adapter crate
├── src/can.rs CAN update protocol codec (tested on the host)
├── build.rs rejects `log` and `defmt` at the same time
├── partition-map/
│ ├── flash64k-ram20k.x one map per (flash, RAM) geometry
│ ├── flash128k-ram32k.x
│ ├── flash128k-ram64k.x
│ ├── flash256k-ram64k.x
│ ├── *-usb.x bigger-bootloader variants for USB DFU
│ └── *-can.x bigger-bootloader variants for CAN updates
├── examples/
│ ├── chips.rs chip -> map -> target table, included by both build.rs
│ ├── bootloader/ blocking bootloader: serial, USB DFU or CAN
│ └── application/ embassy application with mark_booted()/mark_dfu()
├── tools/
│ ├── serial_update.py host side of the serial update protocol
│ ├── can_update.py host side of the CAN update protocol
│ ├── check_size.sh partition fit + entry point guard
│ ├── check_partitions.py validates the partition maps statically
│ └── build_matrix.sh builds (and checks) every supported chip
└── .github/workflows/ci.yml GitHub Actions CI
Both examples are standalone cargo workspaces (they build for a different target
and use a different memory.x than the crate). Neither carries a memory.x in
the repository: build.rs generates one that INCLUDEs the partition map
selected by the chip feature and aliases FLASH to BOOTLOADER (bootloader) or
ACTIVE (application), so the two binaries can never disagree about where the
partitions are.
Each example has its own README with the details of its feature selection, console and pairing rules: examples/bootloader/README.md and examples/application/README.md.
Partition maps
Maps are named after the nominal application flash and the SRAM they are written
for, because several parts share one geometry. All sizes are multiples of the
8 KiB erase granularity embassy-boot sees (see below), and the DFU partition is
always at least one 8 KiB block larger than ACTIVE, as the swap requires a
spare block.
| map | flash | RAM | BOOTLOADER |
ACTIVE |
DFU |
BOOTLOADER_STATE |
|---|---|---|---|---|---|---|
flash64k-ram20k.x |
64 KiB | 20 KiB | 16 KiB 0x0800_0000 |
16 KiB 0x0800_4000 |
24 KiB 0x0800_8000 |
8 KiB 0x0800_E000 |
flash128k-ram32k.x |
128 KiB | 32 KiB | 16 KiB 0x0800_0000 |
48 KiB 0x0800_4000 |
56 KiB 0x0801_0000 |
8 KiB 0x0801_E000 |
flash128k-ram64k.x |
128 KiB | 64 KiB | 16 KiB 0x0800_0000 |
48 KiB 0x0800_4000 |
56 KiB 0x0801_0000 |
8 KiB 0x0801_E000 |
flash256k-ram64k.x |
256 KiB | 64 KiB | 16 KiB 0x0800_0000 |
104 KiB 0x0800_4000 |
120 KiB 0x0801_E000 |
16 KiB 0x0803_C000 |
flash128k-ram32k-usb.x |
128 KiB | 32 KiB | 32 KiB 0x0800_0000 |
32 KiB 0x0800_8000 |
56 KiB 0x0801_0000 |
8 KiB 0x0801_E000 |
flash128k-ram64k-usb.x |
128 KiB | 64 KiB | 32 KiB 0x0800_0000 |
32 KiB 0x0800_8000 |
56 KiB 0x0801_0000 |
8 KiB 0x0801_E000 |
flash256k-ram64k-usb.x |
256 KiB | 64 KiB | 32 KiB 0x0800_0000 |
96 KiB 0x0800_8000 |
112 KiB 0x0802_0000 |
16 KiB 0x0803_C000 |
flash128k-ram32k-can.x |
128 KiB | 32 KiB | 32 KiB 0x0800_0000 |
32 KiB 0x0800_8000 |
56 KiB 0x0801_0000 |
8 KiB 0x0801_E000 |
flash128k-ram64k-can.x |
128 KiB | 64 KiB | 32 KiB 0x0800_0000 |
32 KiB 0x0800_8000 |
56 KiB 0x0801_0000 |
8 KiB 0x0801_E000 |
flash256k-ram64k-can.x |
256 KiB | 64 KiB | 32 KiB 0x0800_0000 |
96 KiB 0x0800_8000 |
112 KiB 0x0802_0000 |
16 KiB 0x0803_C000 |
The -usb and -can maps give the bootloader 32 KiB instead of 16 KiB
(embassy-usb plus embassy-usb-dfu — and the CAN driver plus the CAN update
protocol codec — do not fit into the serial bootloader's partition) and pay
for it out of ACTIVE. A board has to be flashed with a matching set: a
bootloader and an application linked against the same map, because they
disagree about where ACTIVE starts otherwise. build.rs of both examples
picks the -usb map automatically for transport-usb / usb-dfu builds and
the -can map for transport-can / can-runtime builds, rejecting the
feature on parts whose map_usb / map_can is empty.
tools/check_partitions.py validates every map in partition-map/ (contiguity,
page alignment, the embassy-boot state capacity inequality, total size, RAM size
taken from the file name) and runs in CI.
Why CoarseFlash and why an 8 KiB state partition
ch32-hal exposes the internal flash with WRITE_SIZE = ERASE_SIZE = 256
bytes. embassy-boot's assert_partitions requires
2 + 4 * (active_size / PAGE_SIZE) <= state_capacity / STATE::WRITE_SIZE
With a 256 byte page size and a 48 KiB active partition that would need
2 + 4 * 192 = 770 slots of 256 bytes, i.e. ~193 KiB of state on a 128 KiB
chip. Raising the erase granularity that embassy-boot sees to 8 KiB brings the
requirement down to 2 + 4 * 6 = 26 slots, i.e. 6.5 KiB, which fits into a
single 8 KiB block.
CoarseFlash<Flash, 8192> implements NorFlash with ERASE_SIZE = 8192 by
erasing 32 hardware pages per erase() call. The cost is that a swap erases
whole 8 KiB blocks instead of only the 256 byte pages it overwrites; the benefit
is that the swap can be interrupted by a power loss at any time and still be
resumed or reverted.
Other constraints that shaped the table above (all checked at compile time by
assert_partitions, and statically by tools/check_partitions.py):
active_size % 8192 == 0,dfu_size % 8192 == 0,dfu_size - active_size >= 8192(the swap needs one spare block),- the bootloader's copy buffer (1024 bytes) must divide the page size and be a
multiple of
WRITE_SIZE.
Building
Prerequisites: a Rust nightly with rust-src (both examples pin it in
rust-toolchain.toml; build-std is used because
riscv32imfc-unknown-none-elf is a JSON target spec), and optionally WCH's
wlink command line tool (shipped with MounRiver Studio) for flashing.
# the adapter crate (host target, only checks that it compiles)
# the bootloader and the application for the default chip (CH32V305RBT6)
&&
&&
For another part, pick its feature and the target it needs (the table above,
or tools/build_matrix.sh --list):
The bootloader additionally needs exactly one transport-* feature
(transport-uart by default, or transport-usb / transport-can); the
application gets optional usb-dfu and can-runtime features, of which at
most one can be enabled. The USB variants are covered in USB DFU,
the CAN ones in CAN bus.
build.rs refuses to continue if the feature and the target disagree, so a
--target mistake is a short error message instead of a link failure. The
output binaries are called bootloader and application.
To build (and size check) everything:
tools/check_size.sh checks that a binary still fits its partition and that
its entry point is where the partition map expects it. Both the limit and the
expected entry point are derived from the __bootloader_active_start /
__bootloader_active_end symbols in the ELF itself, so the numbers are never
duplicated outside the map:
)
)
--role application uses the ACTIVE partition instead. The script uses the
llvm-tools of the active toolchain (rustup component add llvm-tools) and can
be pointed at another tool with $LLVM_SIZE, $LLVM_NM, … If the bootloader
does not fit anymore, either shrink it or move ACTIVE/DFU up in the
partition map it uses.
Flashing
The first time, flash both binaries: the bootloader at 0x0800_0000 and the
application at 0x0800_4000 (where it is linked; 0x0800_8000 for a -usb
or -can map, see USB DFU and CAN bus). A blank state partition reads
as 0xFF, which embassy-boot treats as State::Boot, so the bootloader will
simply boot whatever is in ACTIVE.
# with wlink (the `cargo run` runner does the same thing)
A nanoCH32V305 can also be programmed over its USB1 port without a debugger,
with wchisp: hold BOOT, press and release
RST, release BOOT, then
wchisp understands ELF and programs each segment at its linked address, so the
application lands in ACTIVE without an intermediate .bin.
# raw binary, if your tool wants a .bin
# (`llvm-objcopy`, or `rust-objcopy` after `rustup component add llvm-tools`)
From then on the application can be updated over the serial console without a debugger.
Serial console
USART1, 115200 8N1, PA9 = TX, PA10 = RX (3.3 V levels).
Within 3 seconds after a reset any key press holds the board in the bootloader; otherwise the active partition is booted immediately. The bootloader example README describes the console, its variants and the size budget in detail.
b / Enter boot the active partition
i print the partition map and the bootloader state
d / u start an update session
? / h help
Update protocol
An update session speaks a trivial line + raw data protocol:
- the host sends
f <hex length>followed by CR or LF, e.g.f a800, - the host sends exactly that many raw bytes of the application image,
- the bootloader acknowledges every 256 byte chunk after it has been written
to flash with a single
.(with aN / M bytesprogress line every 16 KiB), and the host must wait for that acknowledgment before sending the next chunk: the USART has no FIFO and flash programming takes milliseconds, so a host that just streams will overrun the receiver, - after the last chunk the image is marked updated, the session ends, the
bootloader swaps
DFUintoACTIVEand boots it.
The tail of the last chunk is padded with 0xFF, which is what erased flash
reads as.
The image is a raw binary, not an ELF: the bootloader copies the bytes
straight into the DFU partition, which is linked to run at 0x0800_4000.
A host script (Python + pyserial) is
provided:
It resets the board over RTS (wire RTS to RESET, otherwise reset it by hand),
holds the bootloader during the grace window, and then stops and waits for the
. acknowledgment after every 256 byte chunk.
Rollback
The application is only trusted once it calls
mark_booted(). Until then the state
partition keeps the Swap magic and the bootloader reverts the image on the next
reset, so a bad image cannot brick the board:
mark_updated()writes theSwapmagic,- the bootloader swaps and boots the new image, state stays
Swap, - the new application runs its self-check and calls
mark_booted(), - if it resets or hangs before that, step 2 is undone and the previous image boots again.
An application can also ask to stay in the bootloader by calling mark_dfu()
and resetting (d in the example application). mark_dfu() does not touch the
active image; it only makes the bootloader wait for a new one.
USB DFU
The ten -usb parts (see Supported chips) can take updates
over USB DFU 1.1 through
embassy-usb-dfu instead of the
serial console. There are two independent halves, each behind its own feature:
- a
transport-usbbootloader is a DFU 1.1 download device:dfu-utilwrites the application image into theDFUpartition, the bootloader marks it updated and resets; the next boot swapsDFUintoACTIVEand boots it, - a
usb-dfuapplication exposes only the DFU runtime interface: adfu-util -e(DFU_DETACH) makes it write the DFU magic into the state partition and reset, which lands in the USB bootloader.
Both halves must be built against the same -usb partition map, and the
bootloader has to be a transport-usb one for the runtime detach to have
somewhere to land.
The first time, flash the bootloader at 0x0800_0000 and the application at
0x0800_8000 (the ACTIVE of the -usb maps); wchisp flash programs an ELF
segment by segment, so both ELFs go in as they are. From then on neither a
debugger nor a serial cable is needed:
# while the application runs: ask the board to reset into the USB bootloader
# while the bootloader waits in DFU mode: send a raw binary and reboot
embassy-usb-dfu resets the chip by itself once the download has been
manifested, and the same rollback rules as for a serial update apply
(mark_booted() in the application, revert on failure).
USB controllers
The controller is selected per part in examples/chips.rs:
| controller | parts | data pins | interrupt | speed |
|---|---|---|---|---|
usbd |
CH32V208 CBU6, GBU6, RBT6, WBU6 | PA12 (D+), PA11 (D-) | USB_LP_CAN1_RX0 |
full speed |
otg_fs |
CH32V305RBT6, CH32V307 RCT6/VCT6/WCU6 | PA12 (DP), PA11 (DM) | OTG_FS |
full speed |
usbhs |
CH32V305FBP6, CH32V305GBU6 | PB7 (DP), PB6 (DM) | USBHS (+ USBHS_WKUP) |
high speed |
On the nanoCH32V305 the USB-C socket
is wired to OTG_FS, the same controller the on-chip ISP monitor uses, so
wchisp, DFU and the running application all share one port — which is why
ch32v305rbt6 prefers otg_fs over the usbhs block it also has.
USB caveats
- 48 MHz clock. The USB block needs a 48 MHz clock derived from a PLL at
48, 96 or 144 MHz, so both USB examples run the core at
SYSCLK_FREQ_144MHZ_HSI(HSI rather than HSE so boards without a crystal work too). Serial builds keep thech32-haldefault clock. - Demo VID/PID. Both examples use
0xc0de:0xcafe; replace it with your own allocation before shipping anything. - The DFU bootloader busy-polls. It is still blocking code driven through
embassy_futures::block_on, which just polls the USB device in a loop. Fine for flashing an image, but there is no waker-driven driver and no idle path. - A
-usbbuild shrinks the application. 32 KiB of bootloader instead of 16 takes 8–16 KiB away fromACTIVE;tools/check_size.shand CI keep both variants inside their maps.
CAN bus
The thirteen parts with an on-chip CAN controller (see
Supported chips) can take updates over CAN instead of —
or next to — the serial console. The protocol is specified in
docs/can-update-protocol.md; the frame-level
codec (embassy_boot_ch32::can, in src/can.rs) is shared by the firmware,
and tools/can_update.py mirrors its constants on the host.
Unlike serial or USB, which are point-to-point, a CAN bus usually carries many
nodes, and an update targets exactly one of them. Every node is given a 7-bit
NODE_ID: requests arrive on 0x400 + (node << 2) + sub and answers go out
on 0x600 + ..., so the controller's hardware acceptance filters only wake
the addressed node. As a second guard — two boards may end up sharing a
NODE_ID — the ENTER_UPDATE command carries the first 7 bytes of the
target's factory 96-bit unique device ID, which a node checks against its own
before doing anything. The application listens on the bus; a targeted
ENTER_UPDATE makes it answer, reset, and hand over to the bootloader, which
also polls the bus at boot and performs the flash transfer.
There are two halves, each behind its own feature:
- a
transport-canbootloader servesPING,GET_INFO,SESSION_OPEN,BEGIN, the page data frames,QUERY,ABORTandFINISH, writes the image to theDFUpartition, verifies the CRC32 and resets into the swap (the serial console keeps working next to it), - a
can-runtimeapplication answersPINGandGET_INFOand jumps into the bootloader on a matchingENTER_UPDATE.
Both sides are linked against the -can maps automatically (see
Partition maps), but they also have to agree on NODE_ID
and the bit rate: both are single constants at the top of src/can_update.rs
and src/can_runtime.rs, DEFAULT_NODE_ID = 1 and 1 Mbit/s by default.
transport-usb + transport-can and usb-dfu + can-runtime are rejected
by build.rs.
Wiring
The examples use CAN1 on PA11 (RX) / PA12 (TX), which on the
nanoCH32V305 are also the USB-C data pins — either leave the USB cable
unplugged during CAN updates, or build both binaries with the can-pb8-pb9
feature, which moves CAN1 to PB8 (RX) / PB9 (TX) (the REMAP1 pin set).
The chip only has the CAN controller; an external 3.3 V transceiver is needed (a 5 V TJA1050 will work on a 5 V bus, but on 3.3 V use e.g. a SN65HVD230), wired TX→TXD-adjacent pin, RX→RXD-adjacent pin, CANH↔CANH and CANL↔CANL to the host adapter, with 120 Ω termination at both ends of the bus.
Flashing over CAN
On a Linux host with any SocketCAN adapter:
The tool needs python-can. It
discovers the node with a GET_INFO broadcast on the functional ID, moves it
from the application into the bootloader if needed, then streams the image in
256-byte pages with per-page ACKs; if a page is lost it asks the node where it
stands (QUERY) and resumes from there instead of restarting. Pass
--uid <24 hex digits> to pin the update to one specific chip. The same
rollback rules as for serial and USB apply (the application calls
mark_booted(); a bad image is reverted).
CAN caveats
- The bootloader busy-polls the bus with 1 ms sleeps between
try_recvattempts, in the same blocking style as the USB bootloader; the application-side listener is async and shares the console loop. - A
-canbuild shrinks the application like a-usbone (32 KiB bootloader); the CAN bootloader is ~14 KiB of the 32 KiB partition and CI keeps it inside. - Session timeout. A half-dead session (host vanishes mid-transfer) is
dropped by the bootloader after 5 s of bus silence; the host simply
re-opens a session and
QUERYfinds the resume offset.
Caveats
- Only the CH32V2/V3 flash controller is implemented upstream. Everything
above the line "Not (yet) supported" is a limitation of
ch32-hal, not of this crate: the adapter itself is chip agnostic. ch32-metapacover-reports flash size. It reportsFLASH_SIZEas the size of the largest member of the family plus its extraUSR_2region (480 KiB for a 128 KiB CH32V305RBT6), and the flash driver bounds its checks with that constant. Nothing will stop a partition map that runs past the physical flash; keeping the map inside the part's nominal flash is our responsibility, which is whattools/check_partitions.pychecks.- Option bytes. This example assumes the factory configuration: the chip
boots through the on-chip monitor ROM into the user flash at
0x0800_0000. If you have used WCHISPTool orwlinkto change the boot configuration (boot from RAM, read protection, etc.), restore the defaults before testing, or the reset vector will not land in the bootloader. ch32-halneeds itsembassyfeature to build today: its USB modules callembassy_timeunconditionally. The bootloader example is plain blocking code and does not use an executor or a time driver, but it still has to enable the feature.- No
defmtfor now.ch32-halpinsdefmt0.3 whileembassy-bootusesdefmt1.x, so thedefmtfeature of this crate cannot be combined withch32-hal/defmt. The examples therefore uselog. loganddefmtare exclusive. Cargo cannot express that in[features], sobuild.rsturns the combination into a build error (cargo::error=, Rust 1.84+) instead of letting it fail deep insideembassy-boot.SDIPrintcan hang without a debugger.ch32_hal::debug::SDIPrintspins on a busy flag that only a connected WCH-Link clears, so all normal output in both examples goes to USART1.- The bootloader's panic handler deliberately does not print the panic message
or location: formatting a
PanicInfopulls incore::fmt's debug and unicode tables, which cost ~12 KiB here. Rebuild with a debug profile if you need the details.
Porting to another chip
If the part has the v3 flash IP, adding it is three steps:
- pick the geometry: reuse a map in
partition-map/, or add one for the new(flash, RAM)pair and make suretools/check_partitions.pyis happy with it, - add a
Chip { part, map, target, usb, map_usb }entry toexamples/chips.rsand ach32XXXX = ["ch32-hal/ch32XXXX"]feature to both examples'Cargo.toml; leaveusbandmap_usbempty unlessch32-halhas a driver for the part's USB controller and the flash can host a-usbmap with a 32 KiB bootloader, - adjust the LED pin and the UART instance/pins in the examples if the board differs from a nanoCH32V305.
tools/build_matrix.sh --chip <part> then builds and size checks both binaries,
and CI picks the part up automatically (its matrix is generated from
examples/chips.rs).
For a part with a different flash controller the real work is upstream: a driver
in ch32-hal for that flash IP version.
Continuous integration
.github/workflows/ci.yml runs on pushes to main,
on pull requests and manually:
| job | toolchains | what it does |
|---|---|---|
crate |
stable, nightly | cargo fmt --check, cargo clippy --all-targets -D warnings, the same for --features log and --features defmt separately, cargo doc with RUSTDOCFLAGS=-D warnings, python3 tools/check_partitions.py |
chips |
— | turns examples/chips.rs into the chip matrix for the next job |
firmware (one job per part) |
nightly | tools/build_matrix.sh --chip <part>: builds every transport of the bootloader and the application for the target that part needs (the USB/CAN ones only where a -usb/-can map exists), checks every binary with tools/check_size.sh, uploads dist/<part>/*.bin as firmware-<part> |
examples-lint |
nightly | cargo fmt --check and cargo clippy --release -D warnings for both examples, in the default (serial), the USB and the CAN variant (default part) |
A few things worth knowing:
--all-featuresis never used, becauseloganddefmtare mutually exclusive: the two back ends are separate permutations. One step of thecratejob does runcargo check --all-featuresand requires it to fail with the exclusion message frombuild.rs, so the guard cannot rot away.- Everything runs with
--locked. The examples depend onch32-halfrom git, so the committedCargo.lockis what pins the exact upstream revision; update it on purpose withcargo update -p ch32-haland commit the new lock. - No partition sizes or entry points are duplicated in the workflow: the limits
come from the ELF symbols and the chip list from
examples/chips.rs. - The
firmwarejobs cache only the downloaded registry and git checkouts (cache-targets: false): every part is a different feature set, so cachingtarget/would store 20 near-identical trees. - The nightly leg exists to catch new lints early, which also means it can go
red on clippy churn that is not a real problem; pin a known-good nightly in a
rust-toolchain.tomlif that bothers you.
The badges above (and the workflow itself) start working with the first push to GitHub. The docs.rs badge turns green once docs.rs has built the published version.
Roadmap
- CAN-bus updates are implemented (see CAN bus); feedback on the protocol specification (docs/can-update-protocol.md) is still very welcome.
- Re-enable CH32V203RBT6 once the upstream
ch32-haltimer-variant bug is fixed (see Not (yet) supported).
License
MIT OR Apache-2.0.