Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,31 @@

## [Unreleased]

### Added

- **CH32V00x support** — full bootloader and app support for CH32V002/V004/V005/V006/V007 (system flash at `0x1FFF0000`, 3KB + 256B).
- `tinyboot-ch32-rt` crate: minimal `_start` + `link.x` for bootloader binaries that can't afford full `qingke-rt`.
- V103 split BOOT/BOOT2 system-flash regions: UART transport placed in `.text2` (second region) so all features fit.
- `tx_en` support in the CH32V00x example app for RS-485 / DXL TTL loopback.

### Changed

- Switched `ch32-metapac` to git in preparation for V00x support.
- **Breaking:** protocol address reduced to 24 bits; addr byte 3 is now a per-command `Flags` byte. `WriteFlags::FLUSH` replaces the standalone `Cmd::Flush`; `ResetFlags::BOOTLOADER` replaces `addr=1` signaling on Reset.
- **Breaking:** `Frame::addr: u32` split into `addr_lo: u16` + `addr_hi: u8` + `flags: Flags` union; use `Frame::addr()` / `Frame::set_addr()` accessors.
- **Breaking:** merged `tinyboot-ch32-hal`, `tinyboot-ch32-boot`, and `tinyboot-ch32-app` into a single `tinyboot-ch32` crate (`::boot`, `::app`, `::hal` modules).
- **Breaking:** replaced `BootMode` with `RunMode { HandOff, Service }`, and reshaped `BootCtl` around `run_mode()`/`set_run_mode()`, `reset()`, and `hand_off()`.
- **Breaking:** `tinyboot_core::traits` flattened; `BootClient` gone, app behaviour moved to `tinyboot_core::app::App`.
- **Breaking:** V103 + `system-flash` `BootCtl::new` now takes `(Pin, Level, u32)` for the external BOOT0 circuit; other combinations stay unit-arg.
- **Breaking:** removed `Storage::unlock()` — flash lock/unlock is now scoped per operation inside the HAL.
- Switched `ch32-metapac` to git for V00x support.
- Run-mode persistence split into variant-specific backends (`BOOT_MODE` register on V003 system-flash, RAM magic word elsewhere, placed at the last 4 bytes of RAM to avoid colliding with `qingke-rt`'s highcode-init flag).
- Non-HAL `bool` parameters converted to enums (`Duplex`, `Level`, `Pull`, `RunMode`, `BootSrc`).
- Dispatcher refactored for readability and reduced size (eliminated `.rodata` jump table; shared `frame.send` path).

### Added
### Fixed

- `tinyboot-ch32-rt` crate: minimal `_start` + `link.x` for bootloader binaries that can't afford full `qingke-rt`.
- Dispatcher now flushes the transport after send — required for RS-485 / DXL TTL half-duplex.
- Ring buffer properly reset after flush.

## [0.3.0] - 2026-04-15

Expand Down
65 changes: 32 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,30 @@ tinyboot currently supports **UART / RS-485** transport. The table below tracks

✅ Verified | ❓ Untested (same die, likely works — volunteer needed) | 📋 Planned

| Chip | Feature Flag | System Flash | Status | Blocker |
| ------------ | -------------- | ------------------------- | ------ | -------------------------------------- |
| CH32V003F4P6 | `ch32v003f4p6` | `0x1FFFF000` (1920B) | ✅ | -- |
| CH32V003A4M6 | `ch32v003a4m6` | `0x1FFFF000` (1920B) | ❓ | -- |
| CH32V003F4U6 | `ch32v003f4u6` | `0x1FFFF000` (1920B) | ❓ | -- |
| CH32V003J4M6 | `ch32v003j4m6` | `0x1FFFF000` (1920B) | ❓ | -- |
| CH32V103C6T6 | `ch32v103c6t6` | `0x1FFFF000` (2048B) | ❓ | -- |
| CH32V103C8T6 | `ch32v103c8t6` | `0x1FFFF000` (2048B) | ✅ | -- |
| CH32V103C8U6 | `ch32v103c8u6` | `0x1FFFF000` (2048B) | ❓ | -- |
| CH32V103R8T6 | `ch32v103r8t6` | `0x1FFFF000` (2048B) | ❓ | -- |
| CH32V002X4X6 | `ch32v002x4x6` | `0x1FFF0000` (3KB + 256B) | 📋 | `flash_v00x` HAL ([#29][ch32-data-29]) |
| CH32V004X6X1 | `ch32v004x6x1` | `0x1FFF0000` (3KB + 256B) | 📋 | `flash_v00x` HAL ([#29][ch32-data-29]) |
| CH32V005X6X6 | `ch32v005x6x6` | `0x1FFF0000` (3KB + 256B) | 📋 | `flash_v00x` HAL ([#29][ch32-data-29]) |
| CH32V006X8X6 | `ch32v006x8x6` | `0x1FFF0000` (3KB + 256B) | 📋 | `flash_v00x` HAL ([#29][ch32-data-29]) |
| CH32V007X8X6 | `ch32v007x8x6` | `0x1FFF0000` (3KB + 256B) | 📋 | `flash_v00x` HAL ([#29][ch32-data-29]) |
| CH32X033F8P6 | `ch32x033f8p6` | `0x1FFF0000` (3KB + 256B) | 📋 | -- |
| CH32X034F8P6 | `ch32x034f8p6` | `0x1FFF0000` (3KB + 256B) | 📋 | -- |
| CH32X034F8U6 | `ch32x034f8u6` | `0x1FFF0000` (3KB + 256B) | 📋 | -- |
| CH32X035C8T6 | `ch32x035c8t6` | `0x1FFF0000` (3KB + 256B) | 📋 | -- |
| CH32X035F7P6 | `ch32x035f7p6` | `0x1FFF0000` (3KB + 256B) | 📋 | -- |
| CH32X035F8U6 | `ch32x035f8u6` | `0x1FFF0000` (3KB + 256B) | 📋 | -- |
| CH32X035G8R6 | `ch32x035g8r6` | `0x1FFF0000` (3KB + 256B) | 📋 | -- |
| CH32X035G8U6 | `ch32x035g8u6` | `0x1FFF0000` (3KB + 256B) | 📋 | -- |
| CH32X035R8T6 | `ch32x035r8t6` | `0x1FFF0000` (3KB + 256B) | 📋 | -- |
| Chip | Feature Flag | System Flash | Status |
| ------------ | -------------- | ------------------------- | ------ |
| CH32V003F4P6 | `ch32v003f4p6` | `0x1FFFF000` (1920B) | ✅ |
| CH32V003A4M6 | `ch32v003a4m6` | `0x1FFFF000` (1920B) | ❓ |
| CH32V003F4U6 | `ch32v003f4u6` | `0x1FFFF000` (1920B) | ❓ |
| CH32V003J4M6 | `ch32v003j4m6` | `0x1FFFF000` (1920B) | ❓ |
| CH32V103C6T6 | `ch32v103c6t6` | `0x1FFFF000` (2048B) | ❓ |
| CH32V103C8T6 | `ch32v103c8t6` | `0x1FFFF000` (2048B) | ✅ |
| CH32V103C8U6 | `ch32v103c8u6` | `0x1FFFF000` (2048B) | ❓ |
| CH32V103R8T6 | `ch32v103r8t6` | `0x1FFFF000` (2048B) | ❓ |
| CH32V002X4X6 | `ch32v002x4x6` | `0x1FFF0000` (3KB + 256B) | |
| CH32V004X6X1 | `ch32v004x6x1` | `0x1FFF0000` (3KB + 256B) | |
| CH32V005X6X6 | `ch32v005x6x6` | `0x1FFF0000` (3KB + 256B) | |
| CH32V006X8X6 | `ch32v006x8x6` | `0x1FFF0000` (3KB + 256B) | |
| CH32V007X8X6 | `ch32v007x8x6` | `0x1FFF0000` (3KB + 256B) | |
| CH32X033F8P6 | `ch32x033f8p6` | `0x1FFF0000` (3KB + 256B) | 📋 |
| CH32X034F8P6 | `ch32x034f8p6` | `0x1FFF0000` (3KB + 256B) | 📋 |
| CH32X034F8U6 | `ch32x034f8u6` | `0x1FFF0000` (3KB + 256B) | 📋 |
| CH32X035C8T6 | `ch32x035c8t6` | `0x1FFF0000` (3KB + 256B) | 📋 |
| CH32X035F7P6 | `ch32x035f7p6` | `0x1FFF0000` (3KB + 256B) | 📋 |
| CH32X035F8U6 | `ch32x035f8u6` | `0x1FFF0000` (3KB + 256B) | 📋 |
| CH32X035G8R6 | `ch32x035g8r6` | `0x1FFF0000` (3KB + 256B) | 📋 |
| CH32X035G8U6 | `ch32x035g8u6` | `0x1FFF0000` (3KB + 256B) | 📋 |
| CH32X035R8T6 | `ch32x035r8t6` | `0x1FFF0000` (3KB + 256B) | 📋 |

## Features

Expand Down Expand Up @@ -86,16 +86,17 @@ ch32/ CH32 implementation
cli/ tinyboot — host CLI flasher

examples/ch32/v003/ CH32V003 boot + app examples
examples/ch32/v00x/ CH32V00x (V002/V004/V005/V006/V007) boot + app examples
examples/ch32/v103/ CH32V103 boot + app examples
```

| Crate | Description |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------- |
| [`tinyboot-core`](lib/core/) | Platform-agnostic bootloader core (protocol dispatcher, boot state machine, app validation) |
| [`tinyboot-protocol`](lib/protocol/) | Wire protocol (frame format, CRC16, commands) |
| [`tinyboot-ch32`](ch32/) | CH32 HAL and tinyboot platform — use `boot` for bootloader binaries, `app` for application binaries |
| [`tinyboot-ch32-rt`](ch32/rt/) | Minimal CH32 runtime for bootloader binaries that can't afford full `qingke-rt` |
| [`tinyboot`](cli/) | CLI firmware flasher over UART |
| Crate | Description |
| ------------------------------------ | --------------------------------------------------------------------------------------------------- |
| [`tinyboot-core`](lib/core/) | Platform-agnostic bootloader core (protocol dispatcher, boot state machine, app validation) |
| [`tinyboot-protocol`](lib/protocol/) | Wire protocol (frame format, CRC16, commands) |
| [`tinyboot-ch32`](ch32/) | CH32 HAL and tinyboot platform — use `boot` for bootloader binaries, `app` for application binaries |
| [`tinyboot-ch32-rt`](ch32/rt/) | Minimal CH32 runtime for bootloader binaries that can't afford full `qingke-rt` |
| [`tinyboot`](cli/) | CLI firmware flasher over UART |

## Rust Version

Expand Down Expand Up @@ -204,5 +205,3 @@ Please [open an issue](https://github.com/OpenServoCore/tinyboot/issues) before
## License

Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or [MIT License](LICENSE-MIT) at your option.

[ch32-data-29]: https://github.com/ch32-rs/ch32-data/pull/29
5 changes: 5 additions & 0 deletions ch32/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ ch32v003f4p6 = ["ch32-metapac/ch32v003f4p6"]
ch32v003a4m6 = ["ch32-metapac/ch32v003a4m6"]
ch32v003f4u6 = ["ch32-metapac/ch32v003f4u6"]
ch32v003j4m6 = ["ch32-metapac/ch32v003j4m6"]
ch32v002x4x6 = ["ch32-metapac/ch32v002x4x6"]
ch32v004x6x1 = ["ch32-metapac/ch32v004x6x1"]
ch32v005x6x6 = ["ch32-metapac/ch32v005x6x6"]
ch32v006x8x6 = ["ch32-metapac/ch32v006x8x6"]
ch32v007x8x6 = ["ch32-metapac/ch32v007x8x6"]
ch32v103c6t6 = ["ch32-metapac/ch32v103c6t6"]
ch32v103c8t6 = ["ch32-metapac/ch32v103c8t6"]
ch32v103c8u6 = ["ch32-metapac/ch32v103c8u6"]
Expand Down
27 changes: 14 additions & 13 deletions ch32/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@

Part of the [tinyboot](https://github.com/OpenServoCore/tinyboot) project — see the main README to get started.

CH32 HAL and tinyboot platform for CH32V003 and CH32V103. Exposes a bootloader-side entry point ([`boot`]) and an app-side client ([`app`]) built on a small in-crate HAL ([`hal`]).
CH32 HAL and tinyboot platform for CH32V003, CH32V00x (V002/V004/V005/V006/V007), and CH32V103. Exposes a bootloader-side entry point ([`boot`]) and an app-side client ([`app`]) built on a small in-crate HAL ([`hal`]).

## Modules

| Module | For | What it provides |
| ---------- | ----------------------- | --------------------------------------------------------------------------------------------------- |
| `boot` | Bootloader binaries | `run()`, `BootCtl`, USART transport (`Usart`, `UsartConfig`, `BaudRate`, `Duplex`, `TxEnConfig`) |
| `app` | Application binaries | `new_app()`, `App`, `BootCtl`, the `tinyboot_core::app` types |
| `hal` | Both | `flash`, `gpio`, `usart`, `afio`, `rcc`, `pfic`, `iwdg`; auto-generated `Pin` and `UsartMapping` |
| `platform` | (internal) | `tinyboot_core::traits` impls for Storage, Transport, BootCtl, BootMetaStore |
| Module | For | What it provides |
| ---------- | -------------------- | ------------------------------------------------------------------------------------------------ |
| `boot` | Bootloader binaries | `run()`, `BootCtl`, USART transport (`Usart`, `UsartConfig`, `BaudRate`, `Duplex`, `TxEnConfig`) |
| `app` | Application binaries | `new_app()`, `App`, `BootCtl`, the `tinyboot_core::app` types |
| `hal` | Both | `flash`, `gpio`, `usart`, `afio`, `rcc`, `pfic`, `iwdg`; auto-generated `Pin` and `UsartMapping` |
| `platform` | (internal) | `tinyboot_core::traits` impls for Storage, Transport, BootCtl, BootMetaStore |

## Bootloader example

Expand Down Expand Up @@ -79,13 +79,14 @@ For CH32V103 `system-flash` apps, pass the same `BootCtl::new(pin, level, delay)

## Features

| Feature | Description |
| --------------------------------------------------- | --------------------------------------------------------- |
| `ch32v003f4p6` / `a4m6` / `f4u6` / `j4m6` | CH32V003 chip variants |
| `ch32v103c6t6` / `c8t6` / `c8u6` / `r8t6` | CH32V103 chip variants |
| `system-flash` | Build for the system-flash bootloader region |
| Feature | Description |
| ------------------------------------------------------------------ | -------------------------------------------- |
| `ch32v003f4p6` / `a4m6` / `f4u6` / `j4m6` | CH32V003 chip variants |
| `ch32v002x4x6` / `v004x6x1` / `v005x6x6` / `v006x8x6` / `v007x8x6` | CH32V00x chip variants |
| `ch32v103c6t6` / `c8t6` / `c8u6` / `r8t6` | CH32V103 chip variants |
| `system-flash` | Build for the system-flash bootloader region |

Complete boot + app examples live in [`examples/ch32/v003`](../examples/ch32/v003/) and [`examples/ch32/v103`](../examples/ch32/v103/).
Complete boot + app examples live in [`examples/ch32/v003`](../examples/ch32/v003/), [`examples/ch32/v00x`](../examples/ch32/v00x/), and [`examples/ch32/v103`](../examples/ch32/v103/).

## Linker scripts

Expand Down
15 changes: 14 additions & 1 deletion ch32/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {

// boot_pin: chips with a hardware BOOT0 pin (V103) need an external
// RC/flip-flop; others (V003) select via the BOOT_MODE register.
let boot_pin = !cfgs.iter().any(|c| c == "flash_v0");
let boot_pin = !cfgs.iter().any(|c| c == "flash_v0" || c == "flash_v00x");
println!("cargo::rustc-check-cfg=cfg(boot_pin)");
if boot_pin {
println!("cargo:rustc-cfg=boot_pin");
Expand Down Expand Up @@ -66,6 +66,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}

// split_sysflash: system flash is split into two regions by option bytes.
// Flash HAL functions are placed in the second region (.text2).
println!("cargo::rustc-check-cfg=cfg(split_sysflash)");
if system_flash && boot_pin {
println!("cargo:rustc-cfg=split_sysflash");
cfgs.push("split_sysflash".to_string());
}

println!("cargo:cfgs={}", cfgs.join(","));

generate_pin_and_usart_mapping(out)?;
Expand All @@ -75,6 +83,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
fs::copy("tb-run-mode.x", out.join("tb-run-mode.x"))?;
println!("cargo:rerun-if-changed=tb-run-mode.x");

if system_flash && boot_pin {
fs::copy("split-sysflash.x", out.join("split-sysflash.x"))?;
println!("cargo:rerun-if-changed=split-sysflash.x");
}

println!("cargo:rustc-link-search={}", out.display());
println!("cargo:rerun-if-changed=build.rs");

Expand Down
10 changes: 10 additions & 0 deletions ch32/split-sysflash.x
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* Split system-flash: places .text2 into the second system-flash region
* (CODE2/BOOT2). Only linked when the memory layout defines those regions. */
SECTIONS
{
.text2 : ALIGN(4)
{
*(.text2 .text2.*);
} > CODE2 AT> BOOT2
}
INSERT AFTER .rodata;
1 change: 1 addition & 0 deletions ch32/src/hal/afio/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#[cfg_attr(afio_v003, path = "v0.rs")]
#[cfg_attr(afio_v00x, path = "v00x.rs")]
#[cfg_attr(afio_v3, path = "v3.rs")]
mod family;

Expand Down
12 changes: 12 additions & 0 deletions ch32/src/hal/afio/v00x.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#[inline(always)]
pub fn set_usart_remap(n: u8, remap: u8) {
if remap == 0 {
return;
}
let afio = ch32_metapac::AFIO;
match n {
1 => afio.pcfr1().modify(|w| w.set_usart1_rm(remap)),
2 => afio.pcfr1().modify(|w| w.set_usart2_rm(remap)),
_ => {}
}
}
1 change: 1 addition & 0 deletions ch32/src/hal/flash/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#[cfg_attr(flash_v0, path = "v0.rs")]
#[cfg_attr(flash_v00x, path = "v00x.rs")]
#[cfg_attr(flash_v1, path = "v1.rs")]
mod family;

Expand Down
106 changes: 106 additions & 0 deletions ch32/src/hal/flash/v00x.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
const KEY1: u32 = 0x4567_0123;
const KEY2: u32 = 0xCDEF_89AB;
const FLASH: ch32_metapac::flash::Flash = ch32_metapac::FLASH;

#[inline(always)]
fn wait_busy() {
while FLASH.statr().read().bsy() {}
debug_assert!(
!FLASH.statr().read().wrprterr(),
"flash write protection error"
);
// Clear EOP (W1C) — required after every BUFRST, BUFLOAD, STRT.
FLASH.statr().write(|w| w.set_eop(true));
}

fn unlock() {
FLASH.keyr().write(|w| w.set_keyr(KEY1));
FLASH.keyr().write(|w| w.set_keyr(KEY2));
FLASH.modekeyr().write(|w| w.set_modekeyr(KEY1));
FLASH.modekeyr().write(|w| w.set_modekeyr(KEY2));
}

#[inline(always)]
fn lock() {
FLASH.ctlr().write(|w| {
w.set_lock(true);
w.set_flock(true);
});
}

pub const PAGE_SIZE: usize = 256;
const BUF_LOAD_SIZE: usize = 4;

/// Erase one 256-byte page (RM §18.4.6).
pub fn erase(addr: u32) {
unlock();
FLASH.ctlr().write(|w| w.set_fter(true));
FLASH.addr().write(|w| w.set_far(addr));
FLASH.ctlr().write(|w| {
w.set_fter(true);
w.set_strt(true);
});
wait_busy();
FLASH.ctlr().write(|_| {});
lock();
}

/// Fast-page write (RM §18.4.5). `addr` 4-byte aligned, `data.len()`
/// a multiple of 4, must not cross a page boundary.
pub fn write(addr: u32, data: &[u8]) {
let page_base = addr & !(PAGE_SIZE as u32 - 1);
debug_assert!(
(addr as usize & (BUF_LOAD_SIZE - 1)) == 0,
"write: addr not word-aligned"
);
debug_assert!(
data.len().is_multiple_of(BUF_LOAD_SIZE),
"write: len not word-aligned"
);
debug_assert!(
addr + data.len() as u32 <= page_base + PAGE_SIZE as u32,
"write: crosses page boundary"
);

unlock();
FLASH.ctlr().write(|w| w.set_ftpg(true));
FLASH.ctlr().write(|w| {
w.set_ftpg(true);
w.set_bufrst(true);
});
wait_busy();

// Load each 4-byte chunk into the buffer.
let mut buf_addr = addr;
let mut ptr = data.as_ptr() as *const u32;
for _ in 0..data.len() / BUF_LOAD_SIZE {
let word = unsafe { ptr.read() };
unsafe { core::ptr::write_volatile(buf_addr as *mut u32, word) };
FLASH.ctlr().write(|w| {
w.set_ftpg(true);
w.set_bufload(true);
});
wait_busy();
buf_addr += BUF_LOAD_SIZE as u32;
ptr = unsafe { ptr.add(1) };
}

FLASH.addr().write(|w| w.set_far(page_base));
FLASH.ctlr().write(|w| {
w.set_ftpg(true);
w.set_strt(true);
});
wait_busy();
FLASH.ctlr().write(|_| {});
lock();
}

pub fn boot_mode() -> bool {
FLASH.statr().read().boot_mode()
}

pub fn set_boot_mode(mode: bool) {
FLASH.boot_modekeyp().write(|w| w.set_modekeyr(KEY1));
FLASH.boot_modekeyp().write(|w| w.set_modekeyr(KEY2));
FLASH.statr().write(|w| w.set_boot_mode(mode));
}
1 change: 1 addition & 0 deletions ch32/src/hal/rcc/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#[cfg_attr(rcc_v003, path = "v0.rs")]
#[cfg_attr(rcc_v00x, path = "v00x.rs")]
#[cfg_attr(rcc_v1, path = "v1.rs")]
mod family;

Expand Down
Loading