embassy_boot_ch32/lib.rs
1//! [`embassy-boot`](https://github.com/embassy-rs/embassy) support for WCH CH32
2//! microcontrollers driven by
3//! [`ch32-hal`](https://github.com/ch32-rs/ch32-hal).
4//!
5//! This crate is the CH32 counterpart of [`embassy-boot-stm32`]: it re-exports
6//! the generic [`embassy_boot`] types and adds the two things a CH32 needs to
7//! actually boot an application:
8//!
9//! * [`CoarseFlash`] - presents the CH32 internal flash (which is erased in
10//! 256 byte pages) with a coarser erase granularity, which is required by the
11//! `embassy-boot` swap algorithm to keep the state partition reasonably small.
12//! * [`BootLoader::load`] - hands control over to the application running in the
13//! active partition. On QingKe cores the application re-initializes `mtvec`,
14//! the global pointer and the stack pointer itself during its startup code, so
15//! all the bootloader has to do is to mask interrupts and jump.
16//!
17//! Note that the flash driver in [ch32-hal] is only implemented for the `v3`
18//! flash IP used by the CH32V2 and CH32V3 lines (CH32V203, CH32V208, CH32V303,
19//! CH32V305, CH32V307, ...). For the older parts (`v0`, `v1`, `x0`, `l1`) the
20//! driver compiles to `unimplemented!()` stubs, so a runtime bootloader cannot
21//! be built for them yet.
22//!
23//! The partition geometry is expected to be described by the usual
24//! `__bootloader_active_start`/`__bootloader_dfu_start`/`__bootloader_state_start`
25//! (and matching `_end`) linker symbols, holding offsets from the start of
26//! internal flash, exactly like in the `embassy-boot` examples. See
27//! [`BootLoaderConfig::from_linkerfile_blocking`] and
28//! `FirmwareUpdaterConfig::from_linkerfile_blocking()` (the latter is only
29//! available for `target_os = "none"`, so it is not linked here).
30//!
31//! ```ignore
32//! use core::cell::RefCell;
33//!
34//! use ch32_hal::flash::{Blocking, Flash};
35//! use embassy_boot_ch32::{BootLoader, BootLoaderConfig, CoarseFlash};
36//! use embassy_sync::blocking_mutex::raw::NoopRawMutex;
37//! use embassy_sync::blocking_mutex::Mutex;
38//!
39//! type BootFlash = CoarseFlash<Flash<'static, Blocking>, 8192>;
40//!
41//! let flash: Mutex<NoopRawMutex, RefCell<BootFlash>> =
42//! Mutex::new(RefCell::new(CoarseFlash(Flash::new_blocking(p.FLASH))));
43//!
44//! let config = BootLoaderConfig::from_linkerfile_blocking(&flash, &flash, &flash);
45//! let loader = BootLoader::prepare::<_, _, _, 1024>(config);
46//!
47//! unsafe { loader.load(embassy_boot_ch32::active_start()) }
48//! ```
49//!
50//! The [repository](https://github.com/alexxy/embassy-boot-ch32) contains a
51//! complete example pair: a blocking
52//! [bootloader](https://github.com/alexxy/embassy-boot-ch32/tree/main/examples/bootloader)
53//! and an embassy
54//! [application](https://github.com/alexxy/embassy-boot-ch32/tree/main/examples/application),
55//! documented in
56//! its [README](https://github.com/alexxy/embassy-boot-ch32/blob/main/README.md)
57//! (chip matrix, partition maps, serial console, USB DFU and CAN-bus
58//! transports). The
59//! examples are standalone workspaces, so they are not part of these docs and
60//! every link here points at GitHub.
61//!
62//! [`embassy-boot-stm32`]: https://docs.rs/embassy-boot-stm32
63//! [ch32-hal]: https://github.com/ch32-rs/ch32-hal
64
65#![no_std]
66#![warn(missing_docs)]
67
68use embedded_storage::nor_flash::{ErrorType, NorFlash, ReadNorFlash};
69
70pub use embassy_boot::{
71 AlignedBuffer, BlockingFirmwareState, BlockingFirmwareUpdater, BootError, BootLoaderConfig,
72 FirmwareState, FirmwareUpdater, FirmwareUpdaterConfig, FirmwareUpdaterError, State,
73};
74pub use embedded_storage;
75
76pub mod can;
77
78/// Address of the beginning of the internal flash array as seen by the core.
79///
80/// All offsets used by the flash driver, by the partition symbols and by
81/// [`BootLoader::load`] are relative to this address.
82pub const FLASH_BASE: u32 = 0x0800_0000;
83
84/// The PFIC configuration register, used to trigger a system reset.
85const PFIC_CFGR: *mut u32 = 0xE000_E048 as *mut u32;
86/// Write key required by [`PFIC_CFGR`].
87const PFIC_KEY3: u32 = 0xBEEF_0000;
88/// `SYSRESETREQ` bit of [`PFIC_CFGR`].
89const PFIC_SYSRESET: u32 = 1 << 7;
90
91unsafe extern "C" {
92 static __bootloader_active_start: u32;
93 static __bootloader_active_end: u32;
94 static __bootloader_dfu_start: u32;
95 static __bootloader_dfu_end: u32;
96 static __bootloader_state_start: u32;
97 static __bootloader_state_end: u32;
98}
99
100/// Offset of the active partition from [`FLASH_BASE`], as described by the
101/// `__bootloader_active_start` linker symbol.
102///
103/// This is the value that should be handed to [`BootLoader::load`].
104pub fn active_start() -> u32 {
105 unsafe { &__bootloader_active_start as *const u32 as u32 }
106}
107
108/// End offset (exclusive) of the active partition, see [`active_start`].
109pub fn active_end() -> u32 {
110 unsafe { &__bootloader_active_end as *const u32 as u32 }
111}
112
113/// Size of the active partition in bytes.
114pub fn active_size() -> u32 {
115 active_end() - active_start()
116}
117
118/// Offset of the dfu partition from [`FLASH_BASE`], as described by the
119/// `__bootloader_dfu_start` linker symbol.
120pub fn dfu_start() -> u32 {
121 unsafe { &__bootloader_dfu_start as *const u32 as u32 }
122}
123
124/// End offset (exclusive) of the dfu partition, see [`dfu_start`].
125pub fn dfu_end() -> u32 {
126 unsafe { &__bootloader_dfu_end as *const u32 as u32 }
127}
128
129/// Size of the dfu partition in bytes.
130///
131/// An incoming image larger than this will not fit in the dfu partition.
132/// Note that [`embassy_boot::BlockingFirmwareUpdater::write_firmware`] happily
133/// asserts on that, so checking the length before writing is recommended.
134pub fn dfu_size() -> u32 {
135 dfu_end() - dfu_start()
136}
137
138/// Offset of the state partition from [`FLASH_BASE`], as described by the
139/// `__bootloader_state_start` linker symbol.
140pub fn state_start() -> u32 {
141 unsafe { &__bootloader_state_start as *const u32 as u32 }
142}
143
144/// End offset (exclusive) of the state partition, see [`state_start`].
145pub fn state_end() -> u32 {
146 unsafe { &__bootloader_state_end as *const u32 as u32 }
147}
148
149/// Size of the state partition in bytes.
150pub fn state_size() -> u32 {
151 state_end() - state_start()
152}
153
154/// Masks all maskable interrupts.
155///
156/// Clears `mie` (all individual interrupt enables) and `mstatus.MIE` (the global
157/// enable, which on QingKe cores is aliased by the `gintenr` CSR).
158///
159/// Peripheral interrupt enables are *not* touched, so a peripheral that was
160/// configured by the current program keeps requesting service once interrupts
161/// are enabled again. Use [`system_reset`] to get a clean state.
162pub fn disable_interrupts() {
163 #[cfg(target_arch = "riscv32")]
164 unsafe {
165 core::arch::asm!("csrw mie, zero", "csrc mstatus, 8", options(nostack));
166 }
167}
168
169/// Requests a system reset through the PFIC and waits for it.
170///
171/// This is the QingKe equivalent of `NVIC_SystemReset()` from the WCH EVT and is
172/// the recommended way for an application to enter the bootloader after it has
173/// set the DFU state: unlike a plain jump it also clears the state of every
174/// peripheral.
175pub fn system_reset() -> ! {
176 disable_interrupts();
177 unsafe {
178 core::ptr::write_volatile(PFIC_CFGR, PFIC_KEY3 | PFIC_SYSRESET);
179 }
180 loop {
181 core::hint::spin_loop();
182 }
183}
184
185/// A flash adapter that reports a coarser erase granularity than the underlying
186/// driver.
187///
188/// The CH32 internal flash is erased in single pages of `WRITE_SIZE` bytes (256
189/// on the CH32V30x family), and `ch32-hal` therefore reports
190/// `ERASE_SIZE == WRITE_SIZE == 256`. `embassy-boot` uses
191/// `PAGE_SIZE = max(ACTIVE::ERASE_SIZE, DFU::ERASE_SIZE)` both as the unit of
192/// the copy/swap algorithm and as the divisor of its state size requirement:
193///
194/// ```text
195/// 2 + 4 * (active_size / PAGE_SIZE) <= state_size / STATE::WRITE_SIZE
196/// ```
197///
198/// With a 256 byte page and a 48 KiB application that would need a state
199/// partition of nearly 200 KiB, which no CH32V305 has. Wrapping the flash in a
200/// `CoarseFlash<_, 8192>` makes the swap algorithm work in 8 KiB blocks, which
201/// reduces the state requirement to a few 8 KiB blocks while only costing a few
202/// extra page erase commands.
203///
204/// Erases are performed as a sequence of hardware erases of `F::ERASE_SIZE`
205/// bytes; everything else is passed through unchanged.
206pub struct CoarseFlash<F, const ERASE_SIZE: usize>(pub F);
207
208impl<F, const ERASE_SIZE: usize> CoarseFlash<F, ERASE_SIZE> {
209 /// Get a reference to the wrapped flash driver.
210 pub fn inner(&self) -> &F {
211 &self.0
212 }
213
214 /// Get a mutable reference to the wrapped flash driver.
215 pub fn inner_mut(&mut self) -> &mut F {
216 &mut self.0
217 }
218
219 /// Unwrap the adapter, returning the wrapped flash driver.
220 pub fn into_inner(self) -> F {
221 self.0
222 }
223}
224
225impl<F, const ERASE_SIZE: usize> ErrorType for CoarseFlash<F, ERASE_SIZE>
226where
227 F: ErrorType,
228{
229 type Error = F::Error;
230}
231
232impl<F, const ERASE_SIZE: usize> ReadNorFlash for CoarseFlash<F, ERASE_SIZE>
233where
234 F: ReadNorFlash,
235{
236 const READ_SIZE: usize = F::READ_SIZE;
237
238 fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> {
239 self.0.read(offset, bytes)
240 }
241
242 fn capacity(&self) -> usize {
243 self.0.capacity()
244 }
245}
246
247impl<F, const ERASE_SIZE: usize> NorFlash for CoarseFlash<F, ERASE_SIZE>
248where
249 F: NorFlash,
250{
251 const WRITE_SIZE: usize = F::WRITE_SIZE;
252 const ERASE_SIZE: usize = ERASE_SIZE;
253
254 fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> {
255 const {
256 assert!(ERASE_SIZE > 0, "CoarseFlash erase size must not be zero");
257 assert!(
258 ERASE_SIZE.is_multiple_of(F::ERASE_SIZE),
259 "CoarseFlash erase size must be a multiple of the hardware erase size"
260 );
261 }
262
263 assert!(
264 from.is_multiple_of(ERASE_SIZE as u32) && to.is_multiple_of(ERASE_SIZE as u32),
265 "erase range must be aligned to the coarse erase size"
266 );
267 assert!(from < to, "erase range must not be empty");
268
269 let mut address = from;
270 while address < to {
271 self.0.erase(address, address + F::ERASE_SIZE as u32)?;
272 address += F::ERASE_SIZE as u32;
273 }
274 Ok(())
275 }
276
277 fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> {
278 self.0.write(offset, bytes)
279 }
280}
281
282/// A bootloader for CH32 devices.
283pub struct BootLoader {
284 /// The reported state of the bootloader after preparing for boot
285 pub state: State,
286}
287
288impl BootLoader {
289 /// Inspect the bootloader state and perform actions required before booting, such as swapping firmware.
290 ///
291 /// Panics if the swap can not be performed, see [`BootLoader::try_prepare`] for the fallible version.
292 pub fn prepare<ACTIVE: NorFlash, DFU: NorFlash, STATE: NorFlash, const BUFFER_SIZE: usize>(
293 config: BootLoaderConfig<ACTIVE, DFU, STATE>,
294 ) -> Self {
295 match Self::try_prepare::<ACTIVE, DFU, STATE, BUFFER_SIZE>(config) {
296 Ok(loader) => loader,
297 // Keep the panic message free of `{e:?}` so that this also works when
298 // `BootError` does not implement `Debug` in some future version.
299 Err(_) => panic!("Boot prepare error"),
300 }
301 }
302
303 /// Inspect the bootloader state and perform actions required before booting, such as swapping firmware.
304 pub fn try_prepare<
305 ACTIVE: NorFlash,
306 DFU: NorFlash,
307 STATE: NorFlash,
308 const BUFFER_SIZE: usize,
309 >(
310 config: BootLoaderConfig<ACTIVE, DFU, STATE>,
311 ) -> Result<Self, BootError> {
312 let mut aligned_buf = AlignedBuffer([0; BUFFER_SIZE]);
313 let mut boot = embassy_boot::BootLoader::new(config);
314 let state = boot.prepare_boot(aligned_buf.as_mut())?;
315 Ok(Self { state })
316 }
317
318 /// Boots the application.
319 ///
320 /// `start` is an offset from the start of internal flash (the same numbering
321 /// as the `__bootloader_*` symbols), *not* a bus address, so
322 /// `loader.load(active_start())` jumps to `FLASH_BASE + active_start()`.
323 ///
324 /// All interrupts are masked before jumping. The application startup code of
325 /// `qingke-rt` sets up the stack pointer, the global pointer, `mtvec` and the
326 /// interrupt controller itself, so nothing else has to be done.
327 ///
328 /// # Safety
329 ///
330 /// This jumps to the code (and uses the stack) placed in the active
331 /// partition. The caller must make sure a valid application has been
332 /// programmed there.
333 pub unsafe fn load(self, start: u32) -> ! {
334 #[cfg(target_arch = "riscv32")]
335 unsafe {
336 let entry = FLASH_BASE + start;
337 core::arch::asm!(
338 "csrw mie, zero",
339 "csrc mstatus, 8",
340 "jr {0}",
341 in(reg) entry,
342 options(noreturn, nostack),
343 );
344 }
345
346 #[cfg(not(target_arch = "riscv32"))]
347 {
348 let _ = start;
349 unimplemented!("embassy-boot-ch32 can only load firmware on riscv32");
350 }
351 }
352}