Skip to main content

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}