diff --git a/.github/workflows/book.yml b/.github/workflows/book.yml new file mode 100644 index 0000000..55ff669 --- /dev/null +++ b/.github/workflows/book.yml @@ -0,0 +1,55 @@ +name: Book + +on: + push: + branches: [main] + paths: + - "docs/**" + - "book.toml" + - ".github/workflows/book.yml" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + with: + key: mdbook + + - name: Install mdbook + run: cargo install mdbook --locked + + - name: Build book + run: mdbook build + + - uses: actions/configure-pages@v5 + + - uses: actions/upload-pages-artifact@v3 + with: + path: ./book + + deploy: + name: Deploy + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 03b54ab..af6c959 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,7 @@ target Cargo.lock !cli/Cargo.lock !examples/**/Cargo.lock + +# mdbook output +/book + diff --git a/README.md b/README.md index c25ca96..0c4e29d 100644 --- a/README.md +++ b/README.md @@ -1,190 +1,110 @@ # tinyboot [![CI](https://github.com/OpenServoCore/tinyboot/actions/workflows/ci.yml/badge.svg)](https://github.com/OpenServoCore/tinyboot/actions/workflows/ci.yml) +[![Docs](https://img.shields.io/badge/docs-handbook-blue)](https://openservocore.github.io/tinyboot/) [![MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE-MIT) [![Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue)](LICENSE-APACHE) -Rust bootloader for resource-constrained microcontrollers. Fits in the CH32V003's 1920-byte system flash with full trial boot, CRC16 app validation, and version reporting — leaving all but one page of user flash for the application (64 bytes reserved for boot metadata on V003; page size varies by chip). +A Rust bootloader for resource-constrained microcontrollers. Fits in the CH32V003's 1920-byte system flash with full trial boot, CRC16 app validation, and version reporting — leaving every byte of user flash free for your application. ![tinyboot demo](docs/demo.gif) -## Chip Compatibility +## Supported chips -tinyboot currently supports **UART / RS-485** transport. Chips with hardware boot pins (e.g. BOOT0/BOOT1) require a small external circuit for firmware-controlled boot mode switching — see [GPIO-Controlled Boot Mode Selection](docs/boot-ctl.md). +| Family | Status | +| ------------ | ------------ | +| **CH32V003** | ✅ Supported | +| **CH32V00x** (V002 / V004 / V005 / V006 / V007) | ✅ Supported | +| **CH32V103** | ✅ Supported (needs a small BOOT0 circuit — see [boot-ctl](https://openservocore.github.io/tinyboot/boot-ctl.html)) | +| **CH32X03x** | 📋 Planned | -| Family | System Flash | Status | -| ----------------------------------------------- | ------------------------- | ------------ | -| **CH32V003** | `0x1FFFF000` (1920B) | ✅ Supported | -| **CH32V00x** (V002 / V004 / V005 / V006 / V007) | `0x1FFF0000` (3KB + 256B) | ✅ Supported | -| **CH32V103** | `0x1FFFF000` (2048B) | ✅ Supported | -| **CH32X03x** (X033 / X034 / X035) | `0x1FFF0000` (3KB + 256B) | 📋 Planned | +Porting to a new MCU family is [a few hundred lines of glue](https://openservocore.github.io/tinyboot/porting.html). -### Supported feature flags +## Quick start (CH32V003) -| Family | Feature flags | -| -------- | ------------------------------------------------------------------------------ | -| CH32V003 | `ch32v003f4p6`, `ch32v003a4m6`, `ch32v003f4u6`, `ch32v003j4m6` | -| CH32V00x | `ch32v002x4x6`, `ch32v004x6x1`, `ch32v005x6x6`, `ch32v006x8x6`, `ch32v007x8x6` | -| CH32V103 | `ch32v103c6t6`, `ch32v103c8t6`, `ch32v103c8u6`, `ch32v103r8t6` | +Five minutes from a blank chip to an app that updates itself over UART. -## Features - -- **Tiny** — Fits in 1920 bytes of CH32V003 system flash, leaving all 16KB user flash for the application -- **CRC16 validation** — Every frame is CRC16-CCITT protected; app image is verified end-to-end after flashing -- **Trial boot** — New firmware gets a limited number of boot attempts; if the app doesn't confirm, the bootloader takes over automatically -- **Boot state machine** — Idle / Updating / Validating lifecycle tracked in a reserved flash page with forward-only bit transitions (no erase needed for state advances) -- **Version reporting** — Boot and app versions packed into flash, queryable over the wire -- **Configurable transport** — The protocol runs over any `embedded_io::Read + Write` stream. The CH32 implementation supports UART with configurable pins, baud rate, and optional TX-enable for RS-485 / DXL TTL, but the core is transport-agnostic — USB, SPI, Bluetooth, or WiFi would work just as well -- **App-side integration** — The app can confirm a successful boot and request bootloader entry over the wire, enabling fully remote firmware updates without physical access -- **Library, not binary** — Build your bootloader by creating a small crate that wires up your specific hardware; the core logic is reusable across chips -- **Modular and portable** — Platform-agnostic core with four traits (`Transport`, `Storage`, `BootMetaStore`, `BootCtl`) that you implement for your MCU; the protocol, state machine, and CLI work unchanged - -## Getting Started - -1. **Build your bootloader** — create a small crate with a `main.rs` that configures your transport and calls `run()`. See the [CH32V003 example](examples/ch32/v003/boot/) — the entire bootloader main is just a `boot_version!()` macro and a `run()` call with USART config. - -2. **Flash the bootloader** to system flash using [wlink](https://github.com/ch32-rs/wlink): - - ```sh - wlink flash --address 0x1FFFF000 target/riscv32ec-unknown-none-elf/release/boot - ``` - -3. **Install the CLI** and flash your app over UART: - - ```sh - cargo install tinyboot - tinyboot flash target/riscv32ec-unknown-none-elf/release/app --reset - ``` - -## Project Structure +**1. Install the tools.** +```sh +rustup toolchain install nightly +rustup component add rust-src --toolchain nightly +cargo install wlink # flash system flash via WCH-LinkE +cargo install tinyboot # host CLI for UART flashing ``` -lib/ Platform-agnostic core - core/ tinyboot-core — boot state machine, protocol dispatcher - protocol/ tinyboot-protocol — wire protocol, frame format, CRC16 - -ch32/ CH32 implementation - src/ tinyboot-ch32 — HAL + platform (boot + app) - rt/ tinyboot-ch32-rt — tiny bootloader runtime -cli/ tinyboot — host CLI flasher +**2. Clone the repo and flash the bootloader.** -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 +```sh +git clone https://github.com/OpenServoCore/tinyboot +cd tinyboot/examples/ch32/v003/boot +cargo build --release +wlink flash --address 0x1FFFF000 target/riscv32ec-unknown-none-elf/release/boot +wlink set-power disable3v3 && wlink set-power enable3v3 # power-cycle ``` -| 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. **Git-only** (see [ch32/README.md](ch32/#installation)) | -| [`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 - -The workspaces use **edition 2024**. - -- **Library crates and CLI** — stable Rust 1.85+ -- **CH32 examples** (bootloader and app binaries) — nightly, for `-Zbuild-std` on `riscv32ec-unknown-none-elf` (V003) or standard `riscv32imc-unknown-none-elf` (V103) - -## Linker Region Contract - -All memory.x files define five standard regions: `CODE`, `BOOT`, `APP`, `META`, `RAM`. The crate linker scripts (`tb-boot.x`, `tb-app.x`) derive all `__tb_*` symbols from these regions — no magic addresses in memory.x. - -| Region | Description | -| ------ | --------------------------------------------------- | -| `CODE` | Execution mirror (VMA) of the binary's flash region | -| `BOOT` | Bootloader physical flash | -| `APP` | Application physical flash | -| `META` | Boot metadata (last flash page) | -| `RAM` | SRAM | - -## Porting to a New MCU Family - -Adding a new chip within an existing family (e.g. another CH32 variant) is straightforward — add the register definitions to the existing HAL module and a feature flag. No new crates needed. - -Porting to an entirely new MCU family (e.g. STM32) requires a parallel crate. The core crates (`tinyboot-core`, `tinyboot-protocol`, `tinyboot`) are platform-agnostic — you implement four traits and provide a minimal HAL. Here's what that looks like: +**3. Build and flash the demo app over UART.** -### 1. Create a `tinyboot-{chip}` crate +Connect a USB-UART adapter (TX ↔ PD6, RX ↔ PD5, GND shared), then: -Mirror the layout of `tinyboot-ch32`: - -- `src/hal/` — low-level register access: flash (unlock/erase/write/lock), GPIO (configure, set level), USART (init, blocking read/write/flush), RCC (enable peripherals), reset (system reset + optional jump). -- `src/platform/` — implementations of the four `tinyboot_core::traits` on top of the HAL. -- `src/boot.rs` and `src/app.rs` — thin bootloader and app entry points exposing the platform to user binaries. +```sh +cd ../app +cargo build --release +tinyboot flash target/riscv32ec-unknown-none-elf/release/app --reset +``` -The four traits from `tinyboot_core::traits`: +**4. Confirm it's running.** -| Trait | What to implement | -| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -| `Transport` | Any `embedded_io::Read + Write` stream — UART, RS-485, USB, SPI, even WiFi or Bluetooth. The protocol doesn't care what carries the bytes | -| `Storage` | `embedded_storage::NorFlash` (erase, write, read), plus `as_slice()` for zero-copy flash reads | -| `BootMetaStore` | Read/write boot state, trial counter, app checksum, and app size from a reserved flash page (address from linker symbol) | -| `BootCtl` | `run_mode()`/`set_run_mode()` for Service/HandOff intent across reset, `reset()` for software reset, `hand_off()` to boot the app | +```sh +tinyboot info +# capacity: 16320 +# erase_size: 64 +# boot_version: 0.4.0 +# app_version: 1.2.3 +# mode: 1 ← 1 means the app is running +``` -### 2. (Optional) Create a `tinyboot-{chip}-rt` crate +LED should be blinking. To kick the device back into bootloader mode at any time: -If your chip needs a custom `_start` + linker script to fit a small bootloader (`tinyboot-ch32-rt` exists for this reason on CH32), ship it alongside; otherwise the regular chip runtime is fine for the app. +```sh +tinyboot reset --bootloader +``` -### 3. Create an example workspace +On CH32V00x or CH32V103, the flow is the same — swap the example directory. See [Getting Started](https://openservocore.github.io/tinyboot/getting-started.html) for chip-specific notes. -Add `examples/{chip}/{variant}/` with boot + app binaries. Each provides a `memory.x` defining the five standard regions (`CODE`, `BOOT`, `APP`, `META`, `RAM`). The core linker scripts (`tb-boot.x`, `tb-app.x`, `tb-run-mode.x`) handle the rest. +## Where to go next -### What you get for free +The full documentation lives at **[openservocore.github.io/tinyboot](https://openservocore.github.io/tinyboot/)**. Highlights: -The entire protocol (frame format, CRC, sync, commands), the boot state machine (Idle/Updating/Validating transitions, trial boot logic, app validation), the CLI, and the host-side flashing workflow all work unchanged. You only write the chip-specific glue. +- [**Getting Started**](https://openservocore.github.io/tinyboot/getting-started.html) — expanded tutorial with more detail and per-chip notes +- [**CLI reference**](cli/README.md) — `tinyboot flash / info / erase / reset / bin` +- [**App integration**](https://openservocore.github.io/tinyboot/app-integration.html) — put `poll()` and `confirm()` into your own firmware +- [**Remote firmware updates**](https://openservocore.github.io/tinyboot/remote-updates.html) — end-to-end OTA flow +- [**Troubleshooting**](https://openservocore.github.io/tinyboot/troubleshooting.html) — things that go wrong and how to fix them +- [**Porting to a new MCU**](https://openservocore.github.io/tinyboot/porting.html) — four traits, one HAL +- [**Design notes**](https://openservocore.github.io/tinyboot/design.html) — motivation, the 1920-byte budget, `unsafe` policy ## Why tinyboot? -I built tinyboot for [OpenServoCore](https://github.com/OpenServoCore), where CH32V006-based servo boards need seamless firmware updates over the existing DXL TTL bus — no opening the shell, no debug probe, just flash over the same wire the servos already talk on. - -The existing options didn't fit: - -- **CH32 factory bootloader** — Fixed to 115200 baud on PD5/PD6 with no way to configure UART pins, baud rate, or TX-enable for RS-485. Uses a sum-mod-256 checksum that silently drops bad commands with no error response. No CRC verification, no trial boot, no boot state machine. See [ch32v003-bootloader-docs](https://github.com/basilhussain/ch32v003-bootloader-docs) for the reverse-engineered protocol details. - -- **[embassy-boot](https://github.com/embassy-rs/embassy/tree/main/embassy-boot)** — A well-designed bootloader, but requires ~8KB of flash. That's half the V003's 16KB user flash, and doesn't fit in system flash at all. Not practical for MCUs with 16-32KB total. - -I took it as a challenge to fit a proper bootloader — with a real protocol, CRC16 validation, trial boot, and configurable transport — into the CH32V003's 1920-byte system flash. The key inspiration was [rv003usb](https://github.com/cnlohr/rv003usb) by cnlohr, whose software USB implementation includes a 1920-byte bootloader in system flash. That project proved it was possible to fit meaningful code in that space, and showed me that the entire 16KB of user flash could be left free for the application. - -### How it fits in 1920 bytes +The CH32 factory bootloader is fixed to 115200 baud on PD5/PD6 with a sum-mod-256 checksum and no error reporting. [embassy-boot](https://github.com/embassy-rs/embassy/tree/main/embassy-boot) is a well-designed bootloader but needs ~8 KB of flash — half the V003's total. tinyboot fits a real protocol (CRC16, trial boot, configurable transport) into 1920 bytes so every byte of user flash is yours. -Beyond the usual Cargo profile tricks (`opt-level = "z"`, LTO, `codegen-units = 1`, `panic = "abort"`), fitting a real bootloader in 1920 bytes required some more deliberate choices: +For the full story and how it fits in 1920 bytes, see the [design notes](https://openservocore.github.io/tinyboot/design.html). -- **No HAL crates** — bare metal register access via PAC crates only; HAL abstractions are too expensive for this budget -- **Custom runtime** — `tinyboot-ch32-rt` replaces qingke-rt in the bootloader; its startup is just GP/SP init and a jump to main (20 bytes of assembly instead of ~1.4KB of full runtime) -- **Symmetric frame format** — the same `Frame` struct is used for both requests and responses with one shared parse and format path, eliminating code duplication -- **`repr(C)` frame with union data** — CRC is computed directly over the struct memory via pointer cast; no serialization step, no intermediate buffer -- **`MaybeUninit` frame buffer** — the 76-byte `Frame` struct is reused every iteration without zero-initialization -- **Bit-bang CRC16** — no lookup table, trades speed for ~512 bytes of flash savings -- **Bit-clear state transitions** — forward state changes (Idle→Updating, trial consumption) flip 1→0 bits without erasing, avoiding the cost of a full erase+rewrite cycle -- **Avoid `memset`/`memcpy`** — these pull in expensive core routines; manual byte loops and volatile writes keep the linker from dragging in library code -- **`.write()` over `.modify()`** — register writes use direct writes instead of read-modify-write, saving the read and mask operations -- **Aggressive code deduplication** — shared flash operation primitives across erase and write (see the flash HAL) +## Project structure -### Design approach - -tinyboot is structured as a library, not a monolithic binary. The core logic and protocol live in platform-agnostic crates; chip-specific details live in a single `tinyboot-{chip}` crate (`tinyboot-ch32` for CH32) with a `boot` module for bootloader binaries and an `app` module for applications. To build your bootloader, you create a small crate with a `main.rs` that wires up your transport and calls `boot::run()` — see the [examples](examples/ch32/v003/) for exactly this. The app module plugs into your application so it can confirm a successful boot and reboot into the bootloader on command, enabling fully remote firmware updates without physical access. - -## Safety - -The crates use `unsafe` in targeted places, primarily to meet the extreme size constraints of system flash (1920 bytes): - -- **`repr(C)` unions and `MaybeUninit`** — zero-copy frame access and avoiding zero-initialization overhead -- **`read_volatile` / `write_volatile`** — direct flash reads/writes, version reads, and boot request flag access -- **`transmute`** — enum conversions (boot state) and function pointer cast for jump-to-address -- **`from_raw_parts`** — zero-copy flash slice access in the storage layer -- **Linker section attributes** — placing version data and boot metadata at fixed flash addresses -- **`export_name` / `extern "C"`** — runtime entry points and linker symbol access for chip runtime integration - -These are deliberate trade-offs — safe alternatives would pull in extra code that doesn't fit. The unsafe is confined to data layout, memory access, and hardware boundaries; the bootloader state machine and protocol logic are safe Rust. +``` +lib/core/ tinyboot-core — boot state machine, protocol dispatcher +lib/protocol/ tinyboot-protocol — wire protocol, frame format, CRC16 +ch32/ tinyboot-ch32 — HAL + platform +ch32/rt/ tinyboot-ch32-rt — minimal bootloader runtime +cli/ tinyboot — host CLI flasher +examples/ch32/ per-chip boot + app examples (also CI test targets) +docs/ user guide +``` ## Contributing -Contributions are welcome — especially new chip ports and transport implementations. If you're thinking about adding support for a new MCU family, the [Porting to a New MCU Family](#porting-to-a-new-mcu-family) section above covers the trait surface you'd need to implement. - -Please [open an issue](https://github.com/OpenServoCore/tinyboot/issues) before starting a large PR so we can discuss the approach. +Contributions are very welcome — especially new chip ports and transports. See the [contributing guide](https://openservocore.github.io/tinyboot/contributing.html) and please [open an issue](https://github.com/OpenServoCore/tinyboot/issues) before starting anything big. ## License diff --git a/book.toml b/book.toml new file mode 100644 index 0000000..5a774d0 --- /dev/null +++ b/book.toml @@ -0,0 +1,13 @@ +[book] +title = "tinyboot" +description = "A Rust bootloader for resource-constrained microcontrollers" +authors = ["Aaron Qian"] +src = "docs" +language = "en" + +[output.html] +default-theme = "navy" +preferred-dark-theme = "navy" +git-repository-url = "https://github.com/OpenServoCore/tinyboot" +edit-url-template = "https://github.com/OpenServoCore/tinyboot/edit/main/{path}" +no-section-label = true diff --git a/ch32/README.md b/ch32/README.md index fc8b92a..6a0f134 100644 --- a/ch32/README.md +++ b/ch32/README.md @@ -1,12 +1,12 @@ # tinyboot-ch32 -Part of the [tinyboot](https://github.com/OpenServoCore/tinyboot) project — see the main README to get started. +Part of the [tinyboot](https://github.com/OpenServoCore/tinyboot) project — start with the [top-level README](https://github.com/OpenServoCore/tinyboot#quick-start-ch32v003) and the [handbook](https://openservocore.github.io/tinyboot/). -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`]). +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`]). ## Installation -As of v0.4.0, `tinyboot-ch32` is **consumed from git**, not crates.io. It depends on [`ch32-metapac`](https://github.com/ch32-rs/ch32-metapac) as a git-only dependency for CH32V00x flash support, which crates.io does not allow. Add it to your `Cargo.toml` like so: +As of v0.4.0, `tinyboot-ch32` is **consumed from git**, not crates.io. It depends on [`ch32-metapac`](https://github.com/ch32-rs/ch32-metapac) as a git-only dependency for CH32V00x flash support, which crates.io does not allow. ```toml [dependencies] @@ -25,7 +25,7 @@ tinyboot-ch32-rt = "0.4" # optional, bootloader-only; on crates.io | `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 +## Minimal bootloader ```rust use panic_halt as _; @@ -51,23 +51,9 @@ fn main() -> ! { `Storage` and `BootMetaStore` are initialized from linker symbols automatically. `boot_version!()` places the crate's `Cargo.toml` version into the `.tb_version` section; the core reads it via `__tb_version`. -For CH32V103 in `system-flash` mode, `BootCtl::new` takes a GPIO pin driving the external BOOT0 circuit, the level that selects system flash, and a reset-delay cycle count (RC settle time): +For configuring RS-485 half-duplex, DXL TTL, or alternate pin remaps, see the [transports guide](https://openservocore.github.io/tinyboot/transports.html). For CH32V103 in system-flash mode, `BootCtl::new` takes additional arguments for the external BOOT_CTL circuit — see [boot-ctl](https://openservocore.github.io/tinyboot/boot-ctl.html). -```rust -BootCtl::new(Pin::PB1, Level::High, 8000) -``` - -For RS-485 half-duplex with a DE/RE pin: - -```rust -Usart::new(&UsartConfig { - duplex: Duplex::Half, - tx_en: Some(TxEnConfig { pin: Pin::PC2, tx_level: Level::High }), - .. -}) -``` - -## App example +## Minimal app ```rust tinyboot_ch32::app::app_version!(); @@ -80,14 +66,9 @@ loop { } ``` -`app::poll` handles Info and Reset: - -- **Info** — responds with capacity, erase size, versions, and `mode=1`. -- **Reset** — resets the device; `addr=1` reboots into the bootloader, `addr=0` reboots into the app. +`app::poll` handles `Info` (responds with capacity, erase size, versions, `mode = 1`) and `Reset` (resets the device; `addr = 1` reboots into the bootloader, `addr = 0` reboots into the app). All other commands return `Status::Unsupported`. -All other commands return `Status::Unsupported`. - -For CH32V103 `system-flash` apps, pass the same `BootCtl::new(pin, level, delay)` as the bootloader. Apps on V003 or in `user-flash` mode use the unit-arg form `BootCtl::new()`. +See the [app integration guide](https://openservocore.github.io/tinyboot/app-integration.html) for a complete app including `embedded_io` transport wrapping and peripheral ownership. ## Features @@ -98,7 +79,9 @@ For CH32V103 `system-flash` apps, pass the same `BootCtl::new(pin, level, delay) | `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/), [`examples/ch32/v00x`](../examples/ch32/v00x/), and [`examples/ch32/v103`](../examples/ch32/v103/). +See the [flash modes guide](https://openservocore.github.io/tinyboot/flash-modes.html) for when to pick `system-flash` vs user-flash. + +Complete boot + app examples live in [`examples/ch32/v003`](https://github.com/OpenServoCore/tinyboot/tree/main/examples/ch32/v003), [`examples/ch32/v00x`](https://github.com/OpenServoCore/tinyboot/tree/main/examples/ch32/v00x), and [`examples/ch32/v103`](https://github.com/OpenServoCore/tinyboot/tree/main/examples/ch32/v103). ## Linker scripts @@ -109,4 +92,4 @@ cargo:rustc-link-arg=-Ttb-boot.x cargo:rustc-link-arg=-Ttb-run-mode.x ``` -The core linker scripts (`tb-boot.x`, `tb-app.x`) are shipped by `tinyboot-core`. +The core linker scripts (`tb-boot.x`, `tb-app.x`) are shipped by `tinyboot-core`. For the linker region contract every `memory.x` must satisfy, see the [porting guide](https://openservocore.github.io/tinyboot/porting.html#linker-region-contract). diff --git a/cli/README.md b/cli/README.md index a123903..628a930 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,8 +1,8 @@ # tinyboot -Part of the [tinyboot](https://github.com/OpenServoCore/tinyboot) project — see the main README to get started. +Part of the [tinyboot](https://github.com/OpenServoCore/tinyboot) project — start with the [top-level README](https://github.com/OpenServoCore/tinyboot#quick-start-ch32v003) and the [handbook](https://openservocore.github.io/tinyboot/). -Host-side CLI for flashing firmware to tinyboot devices over UART/RS-485. +Host-side CLI for flashing firmware to tinyboot devices over UART / RS-485. ## Install diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..9de04f6 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,35 @@ +# tinyboot docs + +Documentation for using, integrating, and extending tinyboot. + +If you're new here, start with the [top-level README](https://github.com/OpenServoCore/tinyboot#quick-start-ch32v003) quick start, then come back for deeper topics. + +## Tutorial + +- [Getting Started](getting-started.md) — toolchain, tools, and your first successful flash + +## Guides + +- [Flash modes: system-flash vs user-flash](flash-modes.md) +- [Transports: UART, RS-485, DXL TTL](transports.md) +- [GPIO-controlled boot mode selection](boot-ctl.md) — BOOT0 circuits for chips with hardware boot pins +- [App integration](app-integration.md) — wire the tinyboot app side into your firmware +- [Remote firmware updates](remote-updates.md) — the end-to-end OTA flow +- [Building your bootloader from an example](examples.md) + +## Reference + +- [Porting to a new MCU family](porting.md) +- [Design notes](design.md) — motivation, the 1920-byte budget, unsafe policy +- [Protocol reference](https://github.com/OpenServoCore/tinyboot/tree/main/lib/protocol) — wire format, frames, commands +- [Boot state machine](https://github.com/OpenServoCore/tinyboot/tree/main/lib/core) — state transitions, metadata layout +- [CLI reference](https://github.com/OpenServoCore/tinyboot/tree/main/cli) +- [`tinyboot-ch32` reference](https://github.com/OpenServoCore/tinyboot/tree/main/ch32) + +## Troubleshooting + +- [Troubleshooting guide](troubleshooting.md) — symptoms, likely causes, fixes + +## Contributing + +- [Contributing](contributing.md) — dev setup, tests, hardware validation diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md new file mode 100644 index 0000000..56eaea1 --- /dev/null +++ b/docs/SUMMARY.md @@ -0,0 +1,29 @@ +# Summary + +[Introduction](README.md) + +# Tutorial + +- [Getting Started](getting-started.md) + +# Guides + +- [Flash modes](flash-modes.md) +- [Transports](transports.md) +- [GPIO-controlled boot mode selection](boot-ctl.md) +- [App integration](app-integration.md) +- [Remote firmware updates](remote-updates.md) +- [Building your bootloader from an example](examples.md) + +# Reference + +- [Porting to a new MCU](porting.md) +- [Design notes](design.md) + +# Troubleshooting + +- [Troubleshooting](troubleshooting.md) + +# Contributing + +- [Contributing](contributing.md) diff --git a/docs/app-integration.md b/docs/app-integration.md new file mode 100644 index 0000000..1e35f86 --- /dev/null +++ b/docs/app-integration.md @@ -0,0 +1,85 @@ +# App integration + +The tinyboot bootloader is only half the story — to support remote firmware updates, your app has to cooperate with it. This guide walks through what the app needs to do and shows a minimal integration. + +## What the app is responsible for + +1. **Declare its version** so the host can see it via `tinyboot info`. +2. **Confirm successful boot** so the bootloader stops retrying. +3. **Poll the transport** for `Info` and `Reset` requests. + +That's it. The bootloader handles everything else — flashing, verification, state transitions, trial boot. + +## Minimal app + +```rust +#![no_std] +#![no_main] + +// Embed version into the app's binary so tinyboot can find it. +tinyboot_ch32::app::app_version!(); + +#[qingke_rt::entry] +fn main() -> ! { + // Your usual peripheral setup. + let p = ch32_hal::init(Default::default()); + + // UART wired the same way as the bootloader's. + let uart = Uart::new_blocking::<0>(p.USART1, p.PD6, p.PD5, uart_config).unwrap(); + let (tx, rx) = uart.split(); + + // Adapt your tx/rx to embedded_io::Read + Write (see examples/ for a sample). + let mut rx = /* wrap rx */; + let mut tx = /* wrap tx */; + + // Create the app handle and confirm that this boot succeeded. + let mut app = tinyboot_ch32::app::new_app(tinyboot_ch32::app::BootCtl::new()); + app.confirm(); + + loop { + // Your app's real work goes here, alongside polling. + app.poll(&mut rx, &mut tx); + } +} +``` + +See [`examples/ch32/v003/app/`](https://github.com/OpenServoCore/tinyboot/tree/main/examples/ch32/v003/app) for a complete example including a `transport.rs` module that wraps ch32-hal's `Uart` in the `embedded_io` traits plus optional RS-485 DE-pin handling. + +## `app::confirm()` — trial boot handshake + +The bootloader tracks newly-flashed firmware in a trial state. Every boot in `Validating` state consumes one trial; when trials run out, the bootloader assumes the app is broken and takes over on the next reset. + +`app::confirm()` tells the bootloader the new firmware is alive. Call it **after** your app is initialized to the point where you're confident it's running correctly — early enough that it always runs on a successful boot, but late enough to catch major initialization failures. + +Once called, the app is considered confirmed and will boot normally on every subsequent reset (until the next firmware update starts the cycle again). + +If `confirm()` is never reached (panic, watchdog, init deadlock), the trials get consumed across resets and the bootloader eventually takes back control. + +## `app::poll()` — handling bootloader commands + +`poll()` reads a single frame from your transport and handles it. In the app, two commands do something; the rest are rejected with `Status::Unsupported`: + +| Command | Behavior in app | +| -------- | -------------------------------------------------------------------------------- | +| `Info` | Responds with capacity, erase size, boot + app versions, `mode = 1` (app mode). | +| `Reset` | Resets the device. `addr = 1` reboots into the bootloader; `addr = 0` reboots into the app. | + +This is enough for the host CLI to do `tinyboot info` and `tinyboot reset --bootloader` while the app is running, which is how remote updates get kicked off — see the [remote updates guide](remote-updates.md). + +Because `poll()` is blocking on a read, a typical app runs it in a dedicated task or a loop iteration alongside its other work. For timing-sensitive apps, consider running the transport on an interrupt-driven reader and feeding `poll()` asynchronously; `poll()` itself is CPU-cheap. + +## UART sharing notes + +The bootloader and app normally share the same USART. A few gotchas: + +- **Matching config** — the app's baud rate, pins, and DE polarity must match the bootloader's. See [transports.md](transports.md). +- **DE pin polarity** — on boards where RS-485 transceiver contention is possible (e.g. some OpenServoCore V006 layouts), use a `tx_level` that leaves the bus driver **disabled** when idle, so the host's TX line can reach MCU_RX. +- **Half-duplex flush** — when sending multi-byte responses on half-duplex, make sure your `embedded_io::Write` implementation flushes the UART before releasing DE. + +## Passing peripherals to `poll()` + +`poll()` takes your transport as split rx/tx types implementing `embedded_io::Read` and `embedded_io::Write`. This lets your app keep full ownership of peripheral initialization — tinyboot doesn't take over USART registers, and you can layer extra features (logging, RTU framing) on top of the same UART if you adapt them correctly. + +## BootCtl in the app + +`BootCtl::new()` takes the same arguments in the app as it does in the bootloader — for CH32V003 / V00x that's `BootCtl::new()`, for CH32V103 in system-flash mode it's `BootCtl::new(pin, level, delay)`. The app needs this so that `Reset` with the `BOOTLOADER` flag can set the run-mode marker before resetting. diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..d846d61 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,98 @@ +# Contributing + +Thanks for your interest in tinyboot. This page covers the dev setup, test procedures, and workflow conventions. + +## Before starting + +- For anything bigger than a typo fix, please [open an issue](https://github.com/OpenServoCore/tinyboot/issues) first so we can discuss the approach. +- New chip ports are especially welcome — see the [porting guide](porting.md) for the trait surface you'd need to implement. + +## Workspace layout + +``` +lib/ platform-agnostic core + core/ tinyboot-core + protocol/ tinyboot-protocol +ch32/ CH32 HAL + platform + rt/ tinyboot-ch32-rt (minimal bootloader runtime) +cli/ tinyboot host CLI +examples/ch32/v003/ V003 boot + app (CI testbed) +examples/ch32/v00x/ V00x boot + app +examples/ch32/v103/ V103 boot + app +docs/ user-facing documentation +``` + +Each directory is its own Cargo workspace with an independent `Cargo.lock`. A "clean compile" of the project therefore means wiping `target/` under every workspace — see below. + +## Rust toolchain + +- **Library crates (`lib/`, `cli/`)** — stable Rust 1.85+, edition 2024. +- **CH32 example binaries** — nightly, for `-Zbuild-std` on `riscv32ec-unknown-none-elf` (V003 / V00x) or the stable `riscv32imc-unknown-none-elf` target (V103). + +Each example workspace pins its toolchain via `rust-toolchain.toml`. + +## Running tests + +```sh +# Unit tests for the platform-agnostic crates +cd lib && cargo test + +# Build every example to make sure all feature combinations compile +cd examples/ch32/v003 && cargo build --release +cd examples/ch32/v00x && cargo build --release +cd examples/ch32/v103 && cargo build --release + +# Host CLI +cd cli && cargo test +``` + +CI runs a matrix across chip variants and flash modes. Match that before opening a PR. + +## Clean compile + +When hunting size regressions or build issues, a "clean compile" means removing `target/` from **every** workspace. A leftover `target/` in one workspace can mask issues in another: + +```sh +find . -type d -name target -prune -exec rm -rf {} + +``` + +Then rebuild the affected workspaces. + +## Hardware validation + +Some changes (particularly to flash, BootCtl, or the RS-485 transport) can't be caught by unit tests and need on-hardware validation. + +### Integration test checklist (user-flash mode) + +This is the acceptance test we run before merging flash-touching changes on CH32V003 / V103 in user-flash mode: + +1. Erase user flash via `wlink`. +2. Build and flash the bootloader to the `BOOT` region. +3. Power-cycle the board. +4. Confirm `tinyboot info` reports `mode = 0` and the expected `boot_version`. +5. Build the app. +6. Flash the app via `tinyboot flash --reset`. +7. Confirm the app is running (LED blinks, `tinyboot info` reports `mode = 1`). +8. Re-flash a different app version to exercise the update flow end-to-end. +9. Trigger bootloader re-entry via `tinyboot reset --bootloader`. +10. Re-flash the original app, confirm it still runs. +11. Simulate an app that never confirms — boot should fall back to the bootloader after trials run out. +12. Simulate a power loss mid-flash (disconnect power during a write); confirm recovery. +13. Confirm `META` is in the expected location post-update. + +> [!NOTE] +> On CH32V103 in user-flash mode with the BOOT_CTL RC network installed, temporarily disconnect the PB1 → BOOT0 trace before running this procedure. A soft reset can otherwise latch BOOT0 HIGH and route you into system flash. + +### System-flash mode + +System-flash validation follows the same shape but uses `wlink` to write to the system flash address (`0x1FFFF000` on V003 / V103, `0x1FFF0000` on V00x). After writing system flash, always power-cycle before testing. + +## Commit and PR conventions + +- Small, focused commits. Commit messages in imperative mood (e.g. "Add V00x feature flag", "Fix FTPG partial page write"). +- A PR covering a behavior change should note how you validated it (unit test added, hardware procedure run, both). +- Keep docs changes in the same PR as the code change they describe, unless the docs are big enough to deserve their own review. + +## Releases + +Releases are tagged `vX.Y.Z` across the whole repo — all crates share a version. `tinyboot-ch32` stays git-only (not published to crates.io) while it depends on an unreleased `ch32-metapac`. The rest (`tinyboot-core`, `tinyboot-protocol`, `tinyboot`, `tinyboot-ch32-rt`) publish to crates.io. diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 0000000..d36c39c --- /dev/null +++ b/docs/design.md @@ -0,0 +1,48 @@ +# Design notes + +Why tinyboot exists, how it fits in the CH32V003's 1920-byte system flash, and what `unsafe` it uses. + +## Motivation + +tinyboot was built for [OpenServoCore](https://github.com/OpenServoCore), where CH32V006-based servo boards need seamless firmware updates over the existing DXL TTL bus — no opening the shell, no debug probe, just flash over the same wire the servos already talk on. + +The existing options didn't fit: + +- **CH32 factory bootloader** — Fixed to 115200 baud on PD5/PD6 with no way to configure UART pins, baud rate, or TX-enable for RS-485. Uses a sum-mod-256 checksum that silently drops bad commands with no error response. No CRC verification, no trial boot, no boot state machine. See [ch32v003-bootloader-docs](https://github.com/basilhussain/ch32v003-bootloader-docs) for the reverse-engineered protocol details. +- **[embassy-boot](https://github.com/embassy-rs/embassy/tree/main/embassy-boot)** — A well-designed bootloader, but requires ~8KB of flash. That's half the V003's 16KB user flash, and doesn't fit in system flash at all. Not practical for MCUs with 16-32KB total. + +I took it as a challenge to fit a proper bootloader — with a real protocol, CRC16 validation, trial boot, and configurable transport — into the CH32V003's 1920-byte system flash. The key inspiration was [rv003usb](https://github.com/cnlohr/rv003usb) by cnlohr, whose software USB implementation includes a 1920-byte bootloader in system flash. That project proved it was possible to fit meaningful code in that space, and showed me that the entire 16KB of user flash could be left free for the application. + +## Design approach + +tinyboot is structured as a library, not a monolithic binary. The core logic and protocol live in platform-agnostic crates; chip-specific details live in a single `tinyboot-{chip}` crate (`tinyboot-ch32` for CH32) with a `boot` module for bootloader binaries and an `app` module for applications. + +To build your bootloader, you create a small crate with a `main.rs` that wires up your transport and calls `boot::run()` — see the [examples](https://github.com/OpenServoCore/tinyboot/tree/main/examples/ch32/v003) for exactly this. The app module plugs into your application so it can confirm a successful boot and reboot into the bootloader on command, enabling fully remote firmware updates without physical access. + +## How it fits in 1920 bytes + +Beyond the usual Cargo profile tricks (`opt-level = "z"`, LTO, `codegen-units = 1`, `panic = "abort"`), fitting a real bootloader in 1920 bytes required more deliberate choices: + +- **No HAL crates** — bare metal register access via PAC crates only; HAL abstractions are too expensive for this budget. +- **Custom runtime** — `tinyboot-ch32-rt` replaces `qingke-rt` in the bootloader; its startup is just GP/SP init and a jump to main (20 bytes of assembly instead of ~1.4KB of full runtime). +- **Symmetric frame format** — the same `Frame` struct is used for both requests and responses with one shared parse and format path, eliminating code duplication. +- **`repr(C)` frame with union data** — CRC is computed directly over the struct memory via pointer cast; no serialization step, no intermediate buffer. +- **`MaybeUninit` frame buffer** — the 76-byte `Frame` struct is reused every iteration without zero-initialization. +- **Bit-bang CRC16** — no lookup table, trades speed for ~512 bytes of flash savings. +- **Bit-clear state transitions** — forward state changes (Idle → Updating, trial consumption) flip 1→0 bits without erasing, avoiding a full erase + rewrite cycle. +- **Avoid `memset` / `memcpy`** — these pull in expensive `core` routines; manual byte loops and volatile writes keep the linker from dragging in library code. +- **`.write()` over `.modify()`** — register writes use direct writes instead of read-modify-write, saving the read and mask operations. +- **Aggressive code deduplication** — shared flash operation primitives across erase and write (see the flash HAL). + +## Safety + +The crates use `unsafe` in targeted places, primarily to meet the extreme size constraints of system flash (1920 bytes): + +- **`repr(C)` unions and `MaybeUninit`** — zero-copy frame access and avoiding zero-initialization overhead. +- **`read_volatile` / `write_volatile`** — direct flash reads / writes, version reads, and boot request flag access. +- **`transmute`** — enum conversions (boot state) and function pointer cast for jump-to-address. +- **`from_raw_parts`** — zero-copy flash slice access in the storage layer. +- **Linker section attributes** — placing version data and boot metadata at fixed flash addresses. +- **`export_name` / `extern "C"`** — runtime entry points and linker symbol access for chip runtime integration. + +These are deliberate trade-offs — safe alternatives would pull in extra code that doesn't fit. The `unsafe` is confined to data layout, memory access, and hardware boundaries; the bootloader state machine and protocol logic are safe Rust. diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..3b8a17d --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,102 @@ +# Building your bootloader from an example + +The `examples/` directory holds complete, buildable boot + app projects for each supported chip family. They double as CI test targets, which is why they look more structured than a typical example — but they're also the fastest way to start your own project: copy the one that matches your chip and trim it down. + +This page walks through what's in an example, what you need to change, and what you can delete. + +## What's in `examples/` + +``` +examples/ch32/ + v003/ CH32V003 (1920 B system flash, 16 KB user flash) + v00x/ CH32V00x (V002 / V004 / V005 / V006 / V007) + v103/ CH32V103 (needs BOOT_CTL circuit for system-flash mode) +``` + +Each chip directory is a Cargo workspace with two members: + +``` +v003/ + Cargo.toml workspace + boot/ bootloader binary + Cargo.toml + build.rs picks the right memory.x for the selected flash mode + memory_x/ + system-flash.x + user-flash.x + src/main.rs + app/ demo app binary + Cargo.toml + build.rs + memory_x/ + src/main.rs + rust-toolchain.toml + riscv32ec-unknown-none-elf.json (V003 / V00x only — custom target) +``` + +## Why are there so many feature flags? + +The example workspaces are built across a CI matrix: multiple chip variants × system-flash / user-flash modes. Features like `ch32v003f4p6`, `system-flash`, `user-flash` exist so CI can re-use the same source tree for every combination. + +**For your own project, you don't need any of that.** Pick one chip variant and one flash mode; pin them as defaults in your boot crate's `Cargo.toml`; delete the rest. + +## Starting your own project from an example + +1. Copy the example that matches your chip (e.g. `examples/ch32/v003/`) to a new directory. +2. In the `boot/Cargo.toml`, remove the extra chip-variant features you don't need. Leave one, set as the default. +3. Pick a flash mode. Delete the `memory_x/` file you don't need, and simplify `build.rs` to just copy the remaining one. +4. In `src/main.rs`, change the UART config (pins, baud, duplex, tx_en) to match your board. +5. Do the same for `app/` — match the UART config, adjust your pins. + +That gives you a minimal, single-purpose workspace with none of the CI scaffolding. + +## Using tinyboot-ch32 from crates.io + +The examples depend on `tinyboot-ch32` via a path reference (`path = "../../../../ch32"`) because they live in this repo. For an external project, switch to a git dependency: + +```toml +[dependencies] +tinyboot-ch32 = { git = "https://github.com/OpenServoCore/tinyboot", tag = "v0.4.0", default-features = false, features = ["ch32v003f4p6", "system-flash"] } +tinyboot-ch32-rt = "0.4" +``` + +`tinyboot-ch32` is git-only until upstream `ch32-metapac` publishes the flash driver it depends on. See the [`tinyboot-ch32` README](https://github.com/OpenServoCore/tinyboot/tree/main/ch32#installation) for details. + +## `memory.x` and the linker region contract + +Every tinyboot `memory.x` defines the same five regions: `CODE`, `BOOT`, `APP`, `META`, `RAM`. The linker scripts shipped by `tinyboot-core` derive all the chip-agnostic symbols (`__tb_*`) from those regions — you don't need to poke at magic addresses. See the [porting guide](porting.md#linker-region-contract) for the contract. + +If you change chip variants (e.g. V003F4P6 → V003A4M6), the defaults in `memory.x` are usually fine — you only need to adjust if your part has non-standard RAM / flash sizes. + +## `build.rs` and linker scripts + +The build.rs job is to make your `memory.x` discoverable to the linker and to pull in the tinyboot linker fragments via `-T` flags. A minimal single-mode bootloader `build.rs` looks like this: + +```rust +fn main() { + let out_dir = std::env::var("OUT_DIR").unwrap(); + std::fs::copy("memory.x", format!("{out_dir}/memory.x")).unwrap(); + + println!("cargo:rustc-link-search={out_dir}"); + println!("cargo:rerun-if-changed=memory.x"); + + println!("cargo:rustc-link-arg=-Ttb-boot.x"); + println!("cargo:rustc-link-arg=-Ttb-run-mode.x"); +} +``` + +For the **app** crate, swap `-Ttb-boot.x` for `-Ttb-app.x`. The rest is identical. + +The example `build.rs` files in this repo look more involved because they read `CARGO_FEATURE_SYSTEM_FLASH` / `CARGO_FEATURE_USER_FLASH` to pick between `memory_x/system-flash.x` and `memory_x/user-flash.x` — that's only needed if you want a single crate that builds both modes. A user project typically picks one mode and keeps a flat `memory.x` at the crate root. + +### Linker scripts + +| Script | Shipped by | For | When to include | +| ------------------- | ------------------ | ------------- | ---------------------------------------------------------------------------- | +| `memory.x` | you | Both | Always. Defines the five regions (`CODE`, `BOOT`, `APP`, `META`, `RAM`). | +| `tb-boot.x` | `tinyboot-core` | Bootloader | Always, in the bootloader binary. Derives `__tb_*` symbols from `memory.x` and places the boot version tag. | +| `tb-app.x` | `tinyboot-core` | App | Always, in the app binary. Derives `__tb_*` symbols and places the app version tag last in flash. | +| `tb-run-mode.x` | `tinyboot-ch32` | Both | When the platform uses a RAM magic word for run-mode persistence (the default on V003 / V00x / V103). Reserves `__tb_run_mode` at `ORIGIN(RAM) + LENGTH(RAM)` — your `memory.x` must size `RAM` to leave 4 bytes free at the top (`LENGTH = - 4`). | +| `split-sysflash.x` | `tinyboot-ch32` | Bootloader | Only on V103 in system-flash mode. Places `.text2` overflow code into the secondary system-flash region (`CODE2` / `BOOT2`). See [flash modes](flash-modes.md) for the V103 split layout. | + +All `tb-*.x` scripts are added via `cargo:rustc-link-arg=-T.x` in `build.rs`. The shipping crates put them on the linker search path automatically as part of their own build scripts, so you only need the `-T` flags. diff --git a/docs/flash-modes.md b/docs/flash-modes.md new file mode 100644 index 0000000..17069d8 --- /dev/null +++ b/docs/flash-modes.md @@ -0,0 +1,88 @@ +# Flash modes: system-flash vs user-flash + +CH32 parts have two regions of on-chip flash: + +- **System flash** — a small region at `0x1FFF_xxxx`, normally containing the factory ISP bootloader. On CH32, this region is writable via `wlink` and can host tinyboot. +- **User flash** — the main application flash starting at `0x0800_0000`, mapped to `0x0000_0000` at execution. + +tinyboot supports running from either region on every supported chip. This page explains the tradeoffs. + +## Quick recommendation + +- **Default to `system-flash`** when your chip can switch boot source in software. The entire user flash stays available for your application. +- **Use `user-flash`** if you'd rather avoid the BOOT_CTL circuit some chips need for system-flash, or keep the factory ISP in place for easier recovery. + +Picking a mode is controlled by a Cargo feature on your **boot** crate — the app crate doesn't need to know. + +## Mode capacities + +| Chip family | System flash size | User flash size | System-flash mode | User-flash mode | +| ----------- | ----------------- | --------------- | ----------------- | --------------- | +| CH32V003 | 1920 B | 16 KB | ✅ Supported | ✅ Supported | +| CH32V00x | 3 KB + 256 B | 16–64 KB | ✅ Supported | ✅ Supported | +| CH32V103 | 2 KB (+ 1.75 KB split) | 32–64 KB | ✅ Supported | ✅ Supported | + +> [!NOTE] +> Chips that can't switch boot source via a software MODE register need an external BOOT_CTL circuit for system-flash mode — see [boot-ctl.md](boot-ctl.md). Among supported chips, this currently applies to **CH32V103**. V003 / V00x switch boot source in software and need no extra hardware. + +> [!NOTE] +> **CH32V103 split system flash**: option bytes sit in the middle of system flash, splitting it into a 2 KB primary region at `0x1FFFF000` and a 1.75 KB secondary region at `0x1FFFF900`. V103 system-flash `memory.x` declares both as `BOOT` / `BOOT2` (with matching `CODE` / `CODE2` execution mirrors) so the linker can spill overflow into the secondary region if needed. + +## What's in each region + +Regardless of mode, every tinyboot layout reserves four regions, named in [`memory.x`](porting.md#linker-region-contract): + +| Region | Role | Where it lives | +| ------ | ------------------------------------- | -------------------------------------------------- | +| `BOOT` | Bootloader code | System flash (system-flash mode) or top/bottom of user flash (user-flash mode) | +| `APP` | Application code | User flash | +| `META` | Boot metadata (state, trials, CRC) | Last page of user flash | +| `CODE` | Execution mirror (VMA) of `BOOT` | Usually `0x0000_0000` for boot, `0x0000_0000` + offset for app | + +The `META` page is always in user flash, even in system-flash mode — it needs to be on a page boundary that matches the chip's erase granularity, and user flash is always present. + +## Choosing a mode + +**Choose `system-flash` if:** + +- You want all of user flash available for your app. +- You're OK with the small extra wiring on chips that need a BOOT_CTL circuit (V103). +- You don't mind that recovering from a bad bootloader requires `wlink` + a power cycle. + +**Choose `user-flash` if:** + +- You want the factory ISP left intact in system flash for easy recovery via `wchisp`. +- You want a uniform layout across a fleet where some chips don't have BOOT_CTL wired. +- You want the bootloader recoverable the same way as the app (any probe, any SWD/JTAG). + +## Turning on a mode + +The boot crate picks the mode via a Cargo feature: + +```toml +[dependencies] +tinyboot-ch32 = { version = "0.4", features = ["ch32v003f4p6", "system-flash"] } +``` + +Drop `system-flash` to run the bootloader from user flash. The example boot crates expose `system-flash` and `user-flash` as mutually-exclusive features and pick the matching linker script at build time — copy that pattern if you want a single crate that builds both. + +See [`examples/ch32/v003/boot/Cargo.toml`](https://github.com/OpenServoCore/tinyboot/blob/main/examples/ch32/v003/boot/Cargo.toml) and [`examples/ch32/v003/boot/memory_x/`](https://github.com/OpenServoCore/tinyboot/tree/main/examples/ch32/v003/boot/memory_x) for the full wiring. + +## Reverting to the factory bootloader + +**In user-flash mode** — the factory ISP in system flash was never touched. Enter it the normal way for your chip (e.g. on V103, hold BOOT0 HIGH and BOOT1 LOW at reset, then use `wchisp` over UART). + +**In system-flash mode** — tinyboot has overwritten the factory ISP. Factory images for the supported chips live in [`vendor/`](https://github.com/OpenServoCore/tinyboot/tree/main/vendor) in this repo; reflash them to system flash to restore: + +```sh +wlink flash vendor/ch32v003-system-flash.bin --address 0x1FFFF000 +wlink flash vendor/ch32v006-system-flash.bin --address 0x1FFF0000 +# CH32V103 has split system flash — flash each region separately: +wlink flash vendor/ch32v103-system-flash-1.bin --address 0x1FFFF000 +wlink flash vendor/ch32v103-system-flash-2.bin --address 0x1FFFF900 +wlink set-power disable3v3 && wlink set-power enable3v3 +``` + +See [`vendor/README.md`](https://github.com/OpenServoCore/tinyboot/blob/main/vendor/README.md) for the file / address table. + +In practice `wlink` over SWIO is the universal recovery tool — as long as the debug interface is reachable, you can reflash anything in either region regardless of mode. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..958b6af --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,119 @@ +# Getting started + +A walkthrough for flashing the tinyboot bootloader + a demo app onto a CH32V003, then updating the app over UART. This is the expanded version of the [top-level README](https://github.com/OpenServoCore/tinyboot#quick-start-ch32v003) quick start, with more detail on each step and notes for the other supported chips. + +## What you'll need + +- A CH32V003 board (e.g. the CH32V003F4P6-R0 dev board, or a custom board). This guide uses PD5/PD6 for UART, which is the factory default. +- A WCH-LinkE programmer (for SWIO / one-wire debug) or equivalent wlink-supported probe. +- A USB-UART adapter wired to the MCU's UART pins (TX → RX, RX → TX, GND shared). For RS-485 / DXL TTL, see the [transports guide](transports.md). + +If you're on a different chip (CH32V00x or CH32V103), the steps are the same — just swap the example directory. See the [chip notes](#chip-notes) below. + +## 1. Install tools + +```sh +# Rust nightly with the RISC-V target the examples use +rustup toolchain install nightly +rustup component add rust-src --toolchain nightly + +# WCH programming tool (flashes over WCH-LinkE) +cargo install wlink + +# The tinyboot host CLI +cargo install tinyboot +``` + +The CH32V003 examples use `riscv32ec-unknown-none-elf`, which is a Tier 3 target that `-Zbuild-std` builds on the fly — nightly is required. CH32V103 examples use the stable `riscv32imc-unknown-none-elf` target. + +## 2. Clone the repo + +```sh +git clone https://github.com/OpenServoCore/tinyboot +cd tinyboot +``` + +## 3. Build and flash the bootloader + +```sh +cd examples/ch32/v003/boot +cargo build --release +wlink flash --address 0x1FFFF000 target/riscv32ec-unknown-none-elf/release/boot +``` + +> [!IMPORTANT] +> After flashing system flash, power-cycle the board before continuing — a software reset is not sufficient to switch the CPU over to the new bootloader on some chips. The easiest way is `wlink set-power disable3v3 && wlink set-power enable3v3`. + +At this point the chip runs the bootloader at power-on. With no valid app in user flash, it will sit and wait for a host. + +## 4. Build the demo app + +```sh +cd ../app +cargo build --release +``` + +This produces an ELF at `target/riscv32ec-unknown-none-elf/release/app`. + +## 5. Flash the app over UART + +Connect your USB-UART adapter to the MCU's UART pins, then: + +```sh +tinyboot flash target/riscv32ec-unknown-none-elf/release/app --reset +``` + +`--reset` tells the bootloader to jump into the app once the flash and verify steps succeed. You should see the LED blink (TIM2-driven, ~1 Hz) — that's the app running. + +If `--port` is omitted, the CLI probes USB serial ports by sending an Info request to each. If auto-detection fails, pass `--port /dev/ttyUSB0` (or the equivalent on your OS). + +## 6. Verify it worked + +```sh +tinyboot info +``` + +You should see something like: + +``` +capacity: 16320 +erase_size: 64 +boot_version: 0.4.0 +app_version: 1.2.3 +mode: 1 +``` + +`mode: 1` means the app is running. `mode: 0` would mean the bootloader is running — you can switch the device into bootloader mode at any time: + +```sh +tinyboot reset --bootloader +``` + +## Next steps + +- [App integration](app-integration.md) — how to wire `poll()` and `confirm()` into your own app +- [Remote firmware updates](remote-updates.md) — the end-to-end OTA flow +- [Flash modes](flash-modes.md) — system-flash vs user-flash tradeoffs +- [Transports](transports.md) — RS-485, DXL TTL, alternate pins and baud rates +- [Troubleshooting](troubleshooting.md) — if something above didn't work + +## Chip notes + +### CH32V003 (this guide) + +- System flash: `0x1FFFF000`, 1920 bytes. +- User flash: 16 KB, minus the last 64-byte META page. +- No BOOT_CTL circuit needed. + +### CH32V00x (V002 / V004 / V005 / V006 / V007) + +- System flash: `0x1FFF0000`, 3 KB + 256 B. +- Example directory: `examples/ch32/v00x/`. +- `wlink` auto-detects V006 / V007 together — this is expected. + +### CH32V103 + +- System flash: `0x1FFFF000`, 2048 bytes. +- Example directory: `examples/ch32/v103/`. +- **Requires** an external BOOT_CTL circuit to switch between system flash (bootloader) and user flash (app) across a reset. See [GPIO-controlled boot mode selection](boot-ctl.md). +- The example uses `BootCtl::new(Pin::PB1, Level::High, 8000)` — adjust to your pin and RC timing. diff --git a/docs/porting.md b/docs/porting.md new file mode 100644 index 0000000..a3d1c3c --- /dev/null +++ b/docs/porting.md @@ -0,0 +1,50 @@ +# Porting to a new MCU family + +Adding a new chip within an existing family (e.g. another CH32 variant) is straightforward — add the register definitions to the existing HAL module and a feature flag. No new crates needed. + +Porting to an entirely new MCU family (e.g. STM32) requires a parallel crate. The core crates (`tinyboot-core`, `tinyboot-protocol`, `tinyboot`) are platform-agnostic — you implement four traits and provide a minimal HAL. Here's what that looks like. + +## 1. Create a `tinyboot-{chip}` crate + +Mirror the layout of [`tinyboot-ch32`](https://github.com/OpenServoCore/tinyboot/tree/main/ch32): + +- `src/hal/` — low-level register access: flash (unlock/erase/write/lock), GPIO (configure, set level), USART (init, blocking read/write/flush), RCC (enable peripherals), reset (system reset + optional jump). +- `src/platform/` — implementations of the four `tinyboot_core::traits` on top of the HAL. +- `src/boot.rs` and `src/app.rs` — thin bootloader and app entry points exposing the platform to user binaries. + +### The four traits + +| Trait | What to implement | +| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `Transport` | Any `embedded_io::Read + Write` stream — UART, RS-485, USB, SPI, even WiFi or Bluetooth. The protocol doesn't care what carries the bytes | +| `Storage` | `embedded_storage::NorFlash` (erase, write, read), plus `as_slice()` for zero-copy flash reads | +| `BootMetaStore` | Read/write boot state, trial counter, app checksum, and app size from a reserved flash page (address from linker symbol) | +| `BootCtl` | `run_mode()`/`set_run_mode()` for Service/HandOff intent across reset, `reset()` for software reset, `hand_off()` to boot the app | + +## 2. (Optional) Create a `tinyboot-{chip}-rt` crate + +If your chip needs a custom `_start` + linker script to fit a small bootloader — [`tinyboot-ch32-rt`](https://github.com/OpenServoCore/tinyboot/tree/main/ch32/rt) exists for this reason on CH32 — ship one alongside. Otherwise the regular chip runtime is fine for the app. + +## 3. Create an example workspace + +Add `examples/{chip}/{variant}/` with boot + app binaries. Each provides a `memory.x` defining the five standard regions (`CODE`, `BOOT`, `APP`, `META`, `RAM`). The core linker scripts (`tb-boot.x`, `tb-app.x`, `tb-run-mode.x`) handle the rest. + +### Linker region contract + +All `memory.x` files define five standard regions. The crate linker scripts (`tb-boot.x`, `tb-app.x`) derive all `__tb_*` symbols from these regions — no magic addresses in `memory.x`. + +| Region | Description | +| ------ | --------------------------------------------------- | +| `CODE` | Execution mirror (VMA) of the binary's flash region | +| `BOOT` | Bootloader physical flash | +| `APP` | Application physical flash | +| `META` | Boot metadata (last flash page) | +| `RAM` | SRAM | + +## What you get for free + +The entire protocol (frame format, CRC, sync, commands), the boot state machine (Idle / Updating / Validating transitions, trial boot logic, app validation), the CLI, and the host-side flashing workflow all work unchanged. You only write the chip-specific glue. + +## Before starting a port + +Please [open an issue](https://github.com/OpenServoCore/tinyboot/issues) so we can discuss the approach. Some chip families have surprises (boot-pin muxing, flash write granularity, clock domain quirks) that we've already run into on CH32 and can share context on. diff --git a/docs/remote-updates.md b/docs/remote-updates.md new file mode 100644 index 0000000..810ba57 --- /dev/null +++ b/docs/remote-updates.md @@ -0,0 +1,111 @@ +# Remote firmware updates + +Once the bootloader is in system flash and your app calls `poll()` + `confirm()`, you can update the firmware over the same UART / RS-485 bus the device uses for normal operation. No probe, no reset button, no shell to open. + +This guide covers the end-to-end flow. + +## The short version + +```sh +# Ask the running app to reboot into the bootloader. +tinyboot reset --bootloader + +# Flash the new app and jump straight into it after verify. +tinyboot flash new-firmware.elf --reset +``` + +That's it. Everything below is what's happening under the hood. + +## The lifecycle + +``` +power-on + │ + ▼ +bootloader starts META.state + │ │ + ├─ META.state == Idle ──► validate app image ──► hand off to app + │ │ + │ └─► (CRC mismatch) stay in bootloader + │ + ├─ META.state == Validating ──► consume 1 trial, boot app + │ │ + │ └─► (no trials left) stay in bootloader + │ + └─ META.state == Updating ──► stay in bootloader (prior update interrupted) + +app starts + │ + ├─ app::confirm() ──► META.state → Idle (keeps current checksum) + │ + └─ app::poll(): + ├─ Info ──► respond with capacity, versions, mode=1 + └─ Reset ──► if flag & BOOTLOADER: mark run_mode = Service, reset +``` + + +## Step 1: enter service mode + +When the user kicks off an update, the host sends `Reset` with the `BOOTLOADER` flag set. The app sees it through `poll()`, writes the "enter bootloader" intent to the BootCtl marker (either a RAM word, BKP register, or BOOT_CTL GPIO depending on the chip), and issues a software reset. + +The bootloader starts, reads the marker, and — instead of handing off to the app — goes into service mode and listens on the transport. + +```sh +tinyboot reset --bootloader +# device reboots +tinyboot info # now reports mode=0 (bootloader) +``` + +## Step 2: flash + +`tinyboot flash` drives the four-phase protocol: + +1. **Erase** the app region (`META.state` → `Updating`). +2. **Write** the image in 64-byte pages, with `WriteFlags::FLUSH` on the final write. +3. **Verify** — the device CRC16s the image, stores the checksum and size in META, and transitions to `Validating`. +4. **Reset** (if `--reset` was passed) — boot the new app for the first time. + +On the first boot under `Validating`, the bootloader consumes one trial and hands off. If the app reaches `confirm()`, META transitions to `Idle` — the update is complete. If the app never confirms (panic, deadlock, bad init), trials get consumed until the bootloader takes back control. + +## Trial boot behavior + +The trial counter is stored as a byte in META. Each power-on in `Validating` state clears one bit of that byte (a forward-only operation — no erase needed), then boots the app. If the byte reaches zero before `confirm()` lands, the bootloader treats the app as broken and stays in service mode. + +This gives you a safety net: a firmware that hangs during init won't brick the device, because the bootloader will reclaim control after the trials run out. + +## Probe-flashed apps (development escape hatch) + +The "validate app image" step in the lifecycle has two paths. When a CRC is stored in META (the normal post-Verify case), validation runs the CRC. When META is virgin — no update has ever completed through tinyboot — it falls back to a simpler check so an app flashed directly via SWD / JTAG still boots. This lets you iterate on app firmware with a probe without invoking the protocol every time. + +All of the following must be true for this path to trigger: + +- **Run mode is not Service.** No pending "reboot into bootloader" request from the app. +- **META state is Idle** (`0xFF`). No update is in progress and none has gone through Verify. +- **META checksum is `0xFFFF`.** No CRC has ever been written (freshly-erased META, or a chip that hasn't seen a full tinyboot update yet). +- **App region is non-empty.** The first 32-bit word of the app region is **not** `0xFFFF_FFFF`. + +When all four hold, the bootloader hands off to the app. As soon as you run a real tinyboot update cycle (Erase / Write / Verify), META gets a stored checksum and subsequent boots fall back to the normal CRC-validation path. + +## What happens if something goes wrong + +| Scenario | Outcome | +| ------------------------------------------------ | ----------------------------------------------------------------------------------- | +| Power lost during erase or write | `META.state = Updating`, app is invalid. Bootloader stays in service mode on restart. | +| Verify returns `CrcMismatch` | META stays in `Updating`. Retry or check [troubleshooting](troubleshooting.md). | +| App panics during init after flash | Trials run out across reboots; bootloader reclaims control. | +| App's `confirm()` never reaches due to bug | Same as above — trials run out, bootloader wins. | +| Host crashes mid-flash | Same as "power lost during erase or write" — safe, just re-flash. | + +## Making it automatic + +Any host logic that can speak the tinyboot CLI can drive updates: + +```sh +# from a script +tinyboot reset --bootloader +sleep 0.5 +tinyboot flash "$FIRMWARE" --reset +tinyboot info +``` + +If you're embedding the update flow into a bigger tool, look at the `tinyboot` crate (the CLI) as a starting point — the flash logic there calls into `tinyboot-protocol` directly and can be reused as a library. diff --git a/docs/transports.md b/docs/transports.md new file mode 100644 index 0000000..6a4fcd3 --- /dev/null +++ b/docs/transports.md @@ -0,0 +1,99 @@ +# Transports + +The tinyboot protocol runs over any `embedded_io::Read + Write` stream. The CH32 implementation ships a USART transport configured via two **independent** axes: + +- **`duplex`** — controls the **MCU's** pin arrangement. + - `Full` — separate RX and TX pins. + - `Half` — RX is muxed onto the TX pin; the MCU uses a single wire. +- **`tx_en`** — optional direction pin for an **external** buffer (RS-485 transceiver, etc.). Independent of `duplex`. Driven to the configured `tx_level` around writes, to the inverse while idle / reading. + +Combining these gives four useful setups. Pick whichever matches your board. + +## Setup 1: full-duplex UART (two wires) + +Regular UART — separate TX and RX to the host. + +```rust +Usart::new(&UsartConfig { + duplex: Duplex::Full, + tx_en: None, + .. +}); +``` + +`rx_pull: Pull::Up` if the RX line can float when the host is disconnected; `Pull::None` if an external pull-up is already present. + +## Setup 2: single-wire UART (MCU-level half duplex) + +The MCU muxes RX onto the TX pin — one wire to the host, no external buffer. Useful when both ends speak half duplex directly (some probes, some DXL servo chains where the MCU is the sole driver on its segment). + +```rust +Usart::new(&UsartConfig { + duplex: Duplex::Half, + tx_en: None, + .. +}); +``` + +## Setup 3: full-duplex UART + external half-duplex buffer (RS-485 / DXL TTL) + +This is the OpenServoCore hardware style: the MCU runs regular full-duplex UART to a hardware transceiver (MAX485, 74LVC2G241, etc.), and `tx_en` drives the transceiver's direction pin so its output stage only drives the bus while the MCU is transmitting. + +```rust +Usart::new(&UsartConfig { + duplex: Duplex::Full, + tx_en: Some(TxEnConfig { + pin: Pin::PC2, + tx_level: Level::High, // level that puts the transceiver in TX mode + }), + .. +}); +``` + +`tx_level` matches the transceiver's direction-pin polarity: + +- **MAX485-style** (DE active high, /RE active low, tied together): `tx_level: Level::High`. +- **Inverted driver** (e.g. some 74LVC2G241 layouts where the enable is active low): `tx_level: Level::Low`. + +## Setup 4: single-wire UART + external buffer + +MCU half-duplex (muxed RX/TX) **and** a direction-controlled external buffer. Valid if your board puts a buffer in front of the MCU's single wire and still needs direction switching. + +```rust +Usart::new(&UsartConfig { + duplex: Duplex::Half, + tx_en: Some(TxEnConfig { pin: Pin::PC2, tx_level: Level::High }), + .. +}); +``` + +## What `tx_en` actually does + +When configured, the driver toggles the direction pin around every frame: + +- Before the first byte of a write, the pin goes to `tx_level`. +- After the UART has finished transmitting (USART TC flag asserted — the driver calls `usart::flush` before releasing), the pin returns to the inverse of `tx_level`. + +This keeps the transceiver in RX the rest of the time, so host bytes can reach the MCU's RX pin without contention. + +## Pin remaps + +`UsartMapping` picks the AFIO remap and selects which physical pins carry TX / RX. Available mappings are codegen'd per chip — check the generated `UsartMapping` enum in `tinyboot-ch32`, and cross-reference against the USART / AFIO sections of your chip's datasheet for the pin assignments. + +In `Duplex::Half`, only the TX pin is used; the RX pin of the mapping is unused. + +## Matching the app side + +The app's USART configuration must match the bootloader's: + +- Same USART instance (e.g. USART1). +- Same pins / remap. +- Same baud rate. +- Same `duplex` mode. +- Same `tx_en` pin and `tx_level` (if used). + +If any of these differ, the app can still run — but it won't be able to receive `Reset` or `Info` over the bus, so remote bootloader entry won't work. See the [app integration guide](app-integration.md) for the app-side wiring. + +## Custom transports + +The protocol is transport-agnostic — it just needs a byte-oriented duplex stream. To implement your own (USB CDC, SPI, even a radio link), implement `tinyboot_core::traits::Transport`, which is just `embedded_io::Read + Write`. See the [porting guide](porting.md) for the trait surface. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..ba7d8c5 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,94 @@ +# Troubleshooting + +Common symptoms and their usual causes. If you hit something not covered here, please [open an issue](https://github.com/OpenServoCore/tinyboot/issues). + +> [!NOTE] +> This page is a skeleton. Each section lists the likely causes and fixes at a high level; the detailed step-by-step is being re-validated and will land in a follow-up pass. + +--- + +## `tinyboot flash` fails at Verify with `CrcMismatch` + +The image reached the device but the CRC over flash didn't match. Common causes: + +- `WriteFlags::FLUSH` was not set on the final write of a contiguous region. +- A write payload was not padded to a 4-byte boundary. +- The host skipped an address gap without flushing the previous region first. + +If you're using the shipped `tinyboot` CLI, the above are handled for you. If you've written a custom host tool, re-check those three rules against the [protocol reference](https://github.com/OpenServoCore/tinyboot/tree/main/lib/protocol). + +--- + +## Device does not respond to `tinyboot info` + +Something in the UART chain isn't right. Work through these in order: + +- **Baud rate** — host and device must match. CLI default is 115200. +- **Pins** — the bootloader `UsartMapping` must match how your UART is wired (and, for the app, the pins passed to `Uart::new_blocking`). +- **`rx_pull`** — floating RX lines need `Pull::Up`; externally pulled-up lines should use `Pull::None`. +- **RS-485 / DXL TTL** — a DE/RE pin must be configured via `TxEnConfig`, with `tx_level` matching the transceiver's DE polarity. +- **Half-duplex contention** — on some boards the programmer's TX driver and the MCU's TX driver both reach MCU_RX. Flipping `tx_level` (so the MCU's side is tri-stated while idle) often resolves it. See the [transports guide](transports.md). + +--- + +## Bootloader changes don't take effect after `wlink flash` + +After writing to system flash on some chips, a full power cycle is required before the new bootloader runs. A software reset or re-attach is not enough. + +- Toggle VCC, or use `wlink set-power disable3v3` followed by `wlink set-power enable3v3`. + +--- + +## CH32V103 won't boot / the debugger can't attach + +The option bytes may be corrupted or the debug interface may be held off by the running firmware. Recovery path: + +1. Hold BOOT0 and BOOT1 both HIGH during a reset to force SRAM boot. +2. With the chip still held in SRAM boot, run `wlink unprotect` to clear the read / write protection on the option bytes. +3. Release BOOT0 / BOOT1 and power-cycle. `wlink` can then reflash normally. + +--- + +## `wchisp` can't enter boot mode + +`wchisp` drives the factory UART ISP, which lives in system flash. A few reasons it might fail: + +- **You're in system-flash mode** — tinyboot has overwritten the factory ISP, so `wchisp` has nothing to talk to. Use `wlink` over SWIO instead, or reflash a factory image to system flash from [`vendor/`](https://github.com/OpenServoCore/tinyboot/tree/main/vendor) first. +- **CH32V103** — entry needs BOOT0 HIGH plus BOOT1 LOW (PB2 LOW) at reset. +- **CH32V003 / V00x** — no hardware entry condition; the factory ISP is normally reached via a software stub that jumps to it. In practice, use `wlink` for anything outside of a fresh-chip ISP workflow. + +--- + +## `wlink erase` didn't wipe the bootloader + +`wlink erase` only targets the **user flash** region. Bootloaders living in system flash (CH32V003 / V00x / V103 default in this repo) are untouched. + +- To fully reset: `wlink erase` to wipe user flash, then re-flash the bootloader to system flash (either your tinyboot build or a factory image from [`vendor/`](https://github.com/OpenServoCore/tinyboot/tree/main/vendor)), then power-cycle. + +--- + +## App crashes immediately on hand-off from the bootloader (user-flash mode) + +This is specific to **user-flash mode**, where the app lives behind the bootloader in user flash (e.g. at `0x08000800`) rather than at the start of the mapped execution region. In system-flash mode the app starts at `0x08000000` which is already mapped to `0x0` at reset, so this doesn't apply. + +When the app links against `qingke-rt`, the runtime hardcodes the trap vector base (`mtvec`) to `0x0`. Behind-a-bootloader apps need the vector to point to their own `.trap` section instead. + +- The example apps work around this via a linker `--wrap` flag applied to the relevant `qingke-rt` symbol. Copy the wiring from [`examples/ch32/v003/app/`](https://github.com/OpenServoCore/tinyboot/tree/main/examples/ch32/v003/app) when starting your own user-flash-mode app crate. + +--- + +## BOOT0 stays HIGH after a soft reset during a user-flash test + +On CH32V103 with the BOOT_CTL RC circuit attached, a soft reset can leave BOOT0 latched HIGH long enough that the chip boots into system flash even in user-flash test mode. + +- When testing user-flash-mode tinyboot on a V103 board that has the RC network, temporarily disconnect the PB1 → BOOT0 network. + +--- + +## I bricked my V103 and can't recover + +See the SRAM-boot recovery procedure above — BOOT0 HIGH + BOOT1 HIGH on reset puts the chip in a state where debuggers can attach even if option bytes are corrupted. + +--- + +Contributions to this page are especially welcome. If you've hit and fixed something that isn't covered, please open a PR. diff --git a/lib/core/README.md b/lib/core/README.md index bedd1e5..71764b9 100644 --- a/lib/core/README.md +++ b/lib/core/README.md @@ -1,8 +1,8 @@ # tinyboot-core -Part of the [tinyboot](https://github.com/OpenServoCore/tinyboot) project — see the main README to get started. +Part of the [tinyboot](https://github.com/OpenServoCore/tinyboot) project — start with the [top-level README](https://github.com/OpenServoCore/tinyboot#quick-start-ch32v003) and the [handbook](https://openservocore.github.io/tinyboot/). -Platform-agnostic bootloader core: protocol dispatcher, boot state machine, and app validation. +Platform-agnostic bootloader core: protocol dispatcher, boot state machine, and app validation. This page is the authoritative reference for the boot state machine; most readers will want the [handbook](https://openservocore.github.io/tinyboot/) first. ## Boot State Machine diff --git a/lib/protocol/README.md b/lib/protocol/README.md index 8f91657..0bf2bdb 100644 --- a/lib/protocol/README.md +++ b/lib/protocol/README.md @@ -1,8 +1,8 @@ # tinyboot-protocol -Part of the [tinyboot](https://github.com/OpenServoCore/tinyboot) project — see the main README to get started. +Part of the [tinyboot](https://github.com/OpenServoCore/tinyboot) project — start with the [top-level README](https://github.com/OpenServoCore/tinyboot#quick-start-ch32v003) and the [handbook](https://openservocore.github.io/tinyboot/). -Wire protocol for tinyboot. Defines the frame format used between host and device over UART/RS-485. +Wire protocol for tinyboot. Defines the frame format used between host and device over UART / RS-485. This page is the authoritative wire-format reference; for end-user guides see the [handbook](https://openservocore.github.io/tinyboot/). ## Frame format