Skip to content
Closed
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
108 changes: 92 additions & 16 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
name: Libraries
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- run: rustup toolchain install stable --profile minimal --component rustfmt,clippy
- run: cargo fmt --all -- --check
- run: cargo clippy -p tinyboot -p tinyboot-protocol --all-targets -- -D warnings
Expand All @@ -29,7 +29,7 @@ jobs:
run:
working-directory: tinyboot-cli
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- run: sudo apt-get update && sudo apt-get install -y libudev-dev
- run: rustup toolchain install stable --profile minimal --component rustfmt,clippy
- run: cargo fmt --all -- --check
Expand All @@ -43,34 +43,110 @@ jobs:
strategy:
fail-fast: false
matrix:
chip: [ch32v003f4p6, ch32v003a4m6, ch32v003f4u6, ch32v003j4m6]
include:
- chip: ch32v003f4p6
target: examples/ch32/boot/riscv32ec-unknown-none-elf.json
target-flags: -Zbuild-std=core -Zjson-target-spec
- chip: ch32v003a4m6
target: examples/ch32/boot/riscv32ec-unknown-none-elf.json
target-flags: -Zbuild-std=core -Zjson-target-spec
- chip: ch32v003f4u6
target: examples/ch32/boot/riscv32ec-unknown-none-elf.json
target-flags: -Zbuild-std=core -Zjson-target-spec
- chip: ch32v003j4m6
target: examples/ch32/boot/riscv32ec-unknown-none-elf.json
target-flags: -Zbuild-std=core -Zjson-target-spec
- chip: ch32v103c6t6
target: riscv32imc-unknown-none-elf
target-flags: ""
- chip: ch32v103c8t6
target: riscv32imc-unknown-none-elf
target-flags: ""
- chip: ch32v103c8u6
target: riscv32imc-unknown-none-elf
target-flags: ""
- chip: ch32v103r8t6
target: riscv32imc-unknown-none-elf
target-flags: ""
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install toolchain
run: rustup toolchain install nightly --profile minimal --component rust-src,rustfmt,clippy
run: |
rustup toolchain install nightly --profile minimal --component rust-src,rustfmt,clippy,llvm-tools
rustup +nightly target add ${{ matrix.target }} 2>/dev/null || true
cargo install cargo-binutils
mkdir -p /tmp/sizes
- name: Clippy libraries
run: cargo +nightly clippy -p tinyboot-ch32-hal -p tinyboot-ch32-boot -p tinyboot-ch32-app --features ${{ matrix.chip }} --target examples/ch32/boot/riscv32ec-unknown-none-elf.json -Zbuild-std=core -Zjson-target-spec -- -D warnings
run: cargo +nightly clippy -p tinyboot-ch32-hal -p tinyboot-ch32-boot -p tinyboot-ch32-app --features ${{ matrix.chip }} --target ${{ matrix.target }} ${{ matrix.target-flags }} -- -D warnings
- name: Build libraries
run: cargo +nightly build -p tinyboot-ch32-hal -p tinyboot-ch32-boot -p tinyboot-ch32-app --features ${{ matrix.chip }} --target examples/ch32/boot/riscv32ec-unknown-none-elf.json -Zbuild-std=core -Zjson-target-spec --release
run: cargo +nightly build -p tinyboot-ch32-hal -p tinyboot-ch32-boot -p tinyboot-ch32-app --features ${{ matrix.chip }} --target ${{ matrix.target }} ${{ matrix.target-flags }} --release
- name: Doc libraries
run: cargo +nightly doc -p tinyboot-ch32-hal -p tinyboot-ch32-boot -p tinyboot-ch32-app --features ${{ matrix.chip }} --target examples/ch32/boot/riscv32ec-unknown-none-elf.json -Zbuild-std=core -Zjson-target-spec --no-deps
run: cargo +nightly doc -p tinyboot-ch32-hal -p tinyboot-ch32-boot -p tinyboot-ch32-app --features ${{ matrix.chip }} --target ${{ matrix.target }} ${{ matrix.target-flags }} --no-deps
- name: Fmt boot
run: cd examples/ch32/boot && cargo +nightly fmt --all -- --check
- name: Clippy boot (system-flash)
run: cd examples/ch32/boot && cargo +nightly clippy --features "${{ matrix.chip }},system-flash" -- -D warnings
run: cd examples/ch32/boot && cargo +nightly clippy --features "${{ matrix.chip }},system-flash" --target $(basename "${{ matrix.target }}") ${{ matrix.target-flags }} -- -D warnings
- name: Build boot (system-flash)
run: cd examples/ch32/boot && cargo +nightly build --features "${{ matrix.chip }},system-flash" --release
run: |
cd examples/ch32/boot && cargo +nightly build --features "${{ matrix.chip }},system-flash" --target $(basename "${{ matrix.target }}") ${{ matrix.target-flags }} --release
TARGET_DIR=$(basename "${{ matrix.target }}" .json)
rust-size "target/$TARGET_DIR/release/boot" | tail -1 | awk '{print "${{ matrix.chip }},boot,system-flash," $4}' >> /tmp/sizes/sizes.csv
- name: Clippy boot (user-flash)
run: cd examples/ch32/boot && cargo +nightly clippy --features "${{ matrix.chip }},user-flash" -- -D warnings
run: cd examples/ch32/boot && cargo +nightly clippy --features "${{ matrix.chip }},user-flash" --target $(basename "${{ matrix.target }}") ${{ matrix.target-flags }} -- -D warnings
- name: Build boot (user-flash)
run: cd examples/ch32/boot && cargo +nightly build --features "${{ matrix.chip }},user-flash" --release
run: |
cd examples/ch32/boot && cargo +nightly build --features "${{ matrix.chip }},user-flash" --target $(basename "${{ matrix.target }}") ${{ matrix.target-flags }} --release
TARGET_DIR=$(basename "${{ matrix.target }}" .json)
rust-size "target/$TARGET_DIR/release/boot" | tail -1 | awk '{print "${{ matrix.chip }},boot,user-flash," $4}' >> /tmp/sizes/sizes.csv
- name: Fmt app
run: cd examples/ch32/app && cargo +nightly fmt --all -- --check
- name: Clippy app (system-flash)
run: cd examples/ch32/app && cargo +nightly clippy --features "${{ matrix.chip }},system-flash" -- -D warnings
run: cd examples/ch32/app && cargo +nightly clippy --features "${{ matrix.chip }},system-flash" --target $(basename "${{ matrix.target }}") ${{ matrix.target-flags }} -- -D warnings
- name: Build app (system-flash)
run: cd examples/ch32/app && cargo +nightly build --features "${{ matrix.chip }},system-flash" --release
run: |
cd examples/ch32/app && cargo +nightly build --features "${{ matrix.chip }},system-flash" --target $(basename "${{ matrix.target }}") ${{ matrix.target-flags }} --release
TARGET_DIR=$(basename "${{ matrix.target }}" .json)
rust-size "target/$TARGET_DIR/release/app" | tail -1 | awk '{print "${{ matrix.chip }},app,system-flash," $4}' >> /tmp/sizes/sizes.csv
- name: Clippy app (user-flash)
run: cd examples/ch32/app && cargo +nightly clippy --features "${{ matrix.chip }},user-flash" -- -D warnings
run: cd examples/ch32/app && cargo +nightly clippy --features "${{ matrix.chip }},user-flash" --target $(basename "${{ matrix.target }}") ${{ matrix.target-flags }} -- -D warnings
- name: Build app (user-flash)
run: cd examples/ch32/app && cargo +nightly build --features "${{ matrix.chip }},user-flash" --release
run: |
cd examples/ch32/app && cargo +nightly build --features "${{ matrix.chip }},user-flash" --target $(basename "${{ matrix.target }}") ${{ matrix.target-flags }} --release
TARGET_DIR=$(basename "${{ matrix.target }}" .json)
rust-size "target/$TARGET_DIR/release/app" | tail -1 | awk '{print "${{ matrix.chip }},app,user-flash," $4}' >> /tmp/sizes/sizes.csv
- name: Upload sizes
uses: actions/upload-artifact@v6
with:
name: sizes-${{ matrix.chip }}
path: /tmp/sizes/sizes.csv

size-report:
name: Size Report
if: github.event_name == 'pull_request'
needs: ch32
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Download all size artifacts
uses: actions/download-artifact@v8
with:
pattern: sizes-*
path: /tmp/sizes
- name: Generate size table
run: |
cat /tmp/sizes/*/sizes.csv | sort -t, -k3,3 -k2,2r -k1,1 > /tmp/all-sizes.csv
CSV=/tmp/all-sizes.csv
{
echo "## Binary Sizes"
echo ""
echo "| Chip | Example | Mode | Size |"
echo "|------|---------|------|-----:|"
while IFS=, read -r chip crate flash size; do
echo "| \`$chip\` | $crate | $flash | $size |"
done < "$CSV"
} > /tmp/sizes/comment.md
- name: Post PR comment
uses: marocchino/sticky-pull-request-comment@v3
with:
path: /tmp/sizes/comment.md
29 changes: 20 additions & 9 deletions .zed/settings.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
{
"languages": {
"Rust": {
"format_on_save": "on"
}
"format_on_save": "on",
},
},
"lsp": {
"rust-analyzer": {
"initialization_options": {
"cargo": {
"target": "riscv32imac-unknown-none-elf",
"features": ["ch32v003f4p6", "system-flash"]
"target": "riscv32imc-unknown-none-elf",
"features": [
// "ch32v003f4u6"
"ch32v103c8u6",
// "user-flash",
"system-flash",
],
},
"check": {
"command": "clippy",
"allTargets": false
}
}
}
}
"allTargets": false,
},
"linkedProjects": [
"Cargo.toml",
// "tinyboot-cli/Cargo.toml",
"examples/ch32/boot/Cargo.toml",
"examples/ch32/app/Cargo.toml",
],
},
},
},
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Rust bootloader for resource-constrained microcontrollers. Fits in the CH32V003'

## Chip Compatibility

tinyboot currently supports **UART / RS-485** transport. The table below tracks chip support status.
tinyboot currently supports **UART / RS-485** transport. The table below tracks chip support status. Chips with hardware boot pins (e.g. BOOT0/BOOT1) require a small external circuit to allow firmware-controlled boot mode switching. Please see [GPIO-Controlled Boot Mode Selection](docs/boot-ctl.md) document for more information.

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

Expand Down
Binary file added docs/boot-ctl-flip-flop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/boot-ctl-rc.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
73 changes: 73 additions & 0 deletions docs/boot-ctl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# GPIO-Controlled Boot Mode Selection

Some MCUs (e.g. CH32V103) use hardware boot pins (BOOT0/BOOT1) to select the boot
source at reset. If you choose to run tinyboot from system flash, you need BOOT0
to default HIGH on power-on (system flash), and provide a way for firmware to
switch to user flash (BOOT0 LOW) across a reset.

This document describes two circuits for controlling BOOT0 from a single GPIO pin
(BOOT_CTL). Both share the same firmware interface.

Note: BOOT1 is tied to GND in both circuits.

## BOOT_CTL GPIO Truth Table

| BOOT_CTL | Effect |
| ------------------ | ----------------------------- |
| HIGH | Boots system flash (tinyboot) |
| LOW | Boots user flash (app) |

## Circuit Option 1: RC (Recommended)

![RC Circuit](boot-ctl-rc.png)

**How it works:**

- R1 (10K) pulls BOOT0 HIGH by default (system flash).
- When BOOT_CTL drives LOW, it overpowers the pull-up through R2 (1K), pulling
BOOT0 LOW and discharging C1.
- On reset, the GPIO goes Hi-Z. The discharged capacitor holds BOOT0 LOW long
enough for the chip to sample it (RC time constant = 10K x 100nF = 1ms).
- The capacitor then recharges through R1, returning BOOT0 to HIGH. On any
subsequent reset, the chip boots back into system flash automatically.

**Tested:** Yes, verified on CH32V103C8T6. The RC time constant of 1ms provides
sufficient hold time across a software-triggered system reset. The capacitor
charges fast enough during power-on reset (POR) that BOOT0 reads HIGH on first
boot.

## Circuit Option 2: Flip-Flop

![Flip-Flop Circuit](boot-ctl-flip-flop.png)

**How it works:**

- The NPN transistor inverts BOOT_CTL to drive /PRE (active-low preset).
- BOOT_CTL drives /CLR (active-low clear) directly through a 1K series resistor.
- When BOOT_CTL = HIGH: transistor ON, /PRE = LOW (active), Q = HIGH (system flash).
- When BOOT_CTL = LOW: transistor OFF, /CLR = LOW (active), Q = LOW (user flash).
- When BOOT_CTL = Hi-Z (during reset): transistor OFF (/PRE inactive via pull-up),
/CLR inactive (via pull-up). The flip-flop holds its previous state.
- The two 1K series resistors isolate the transistor base from /CLR, preventing
the /CLR pull-up from turning on the transistor during Hi-Z.

**Power-on state:** The 74AUC1G74 does not have a guaranteed power-on state. An
optional 100nF capacitor from /PRE to GND can be added to force Q HIGH on first
power-up.

**Tested:** Not yet verified on hardware. The design is sound in theory but has not
been tested. Let me know if you can confirm that it works.

## Which to Choose

The RC circuit is simpler (3 passive components, no IC) and has been validated on
hardware. It relies on the capacitor holding BOOT0 LOW for ~1ms across a reset,
which is well within the margin for software-triggered resets.

The flip-flop circuit is deterministic (no timing dependency) and holds state
indefinitely, but requires more components and has not been tested. It may be
preferable in designs if you need reliable state retention, e.g. a real product
that requires high reliability.

For most use cases though, my personal opinion is the RC circuit is probably reliable
enough and simpler / lower cost.
7 changes: 7 additions & 0 deletions examples/ch32/app/.cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ rustflags = [
]
runner = "probe-rs run --chip CH32V003"

[target.riscv32imc-unknown-none-elf]
rustflags = [
"-C", "link-arg=-Tlink.x",
"-C", "link-arg=--nmagic",
]
runner = "probe-rs run --chip CH32V103"

[env]
DEFMT_LOG = "trace"
DEFMT_RTT_BUFFER_SIZE = "128"
14 changes: 9 additions & 5 deletions examples/ch32/app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,21 @@ tinyboot-ch32-app = { path = "../../../tinyboot-ch32-app", default-features = fa
embedded-io = "0.7"
critical-section = "1"
ch32-hal = { git = "https://github.com/ch32-rs/ch32-hal", default-features = false }
qingke-rt = { version = "0.6", features = ["v2"] }
qingke-rt = { version = "0.6", default-features = false }
defmt = { version = "1.0", optional = true }
defmt-rtt = { version = "1.1", optional = true }

[features]
system-flash = ["tinyboot-ch32-app/system-flash"]
user-flash = ["dep:defmt", "dep:defmt-rtt", "tinyboot-ch32-app/defmt"]
ch32v003f4p6 = ["tinyboot-ch32-app/ch32v003f4p6", "ch32-hal/ch32v003f4p6", "ch32-hal/rt"]
ch32v003a4m6 = ["tinyboot-ch32-app/ch32v003a4m6", "ch32-hal/ch32v003a4m6", "ch32-hal/rt"]
ch32v003f4u6 = ["tinyboot-ch32-app/ch32v003f4u6", "ch32-hal/ch32v003f4u6", "ch32-hal/rt"]
ch32v003j4m6 = ["tinyboot-ch32-app/ch32v003j4m6", "ch32-hal/ch32v003j4m6", "ch32-hal/rt"]
ch32v003f4p6 = ["tinyboot-ch32-app/ch32v003f4p6", "ch32-hal/ch32v003f4p6", "ch32-hal/rt", "qingke-rt/v2"]
ch32v003a4m6 = ["tinyboot-ch32-app/ch32v003a4m6", "ch32-hal/ch32v003a4m6", "ch32-hal/rt", "qingke-rt/v2"]
ch32v003f4u6 = ["tinyboot-ch32-app/ch32v003f4u6", "ch32-hal/ch32v003f4u6", "ch32-hal/rt", "qingke-rt/v2"]
ch32v003j4m6 = ["tinyboot-ch32-app/ch32v003j4m6", "ch32-hal/ch32v003j4m6", "ch32-hal/rt", "qingke-rt/v2"]
ch32v103c6t6 = ["tinyboot-ch32-app/ch32v103c6t6", "ch32-hal/ch32v103c6t6", "ch32-hal/rt", "qingke-rt/v3"]
ch32v103c8t6 = ["tinyboot-ch32-app/ch32v103c8t6", "ch32-hal/ch32v103c8t6", "ch32-hal/rt", "qingke-rt/v3"]
ch32v103c8u6 = ["tinyboot-ch32-app/ch32v103c8u6", "ch32-hal/ch32v103c8u6", "ch32-hal/rt", "qingke-rt/v3"]
ch32v103r8t6 = ["tinyboot-ch32-app/ch32v103r8t6", "ch32-hal/ch32v103r8t6", "ch32-hal/rt", "qingke-rt/v3"]

[profile.dev]
opt-level = "z"
Expand Down
6 changes: 5 additions & 1 deletion examples/ch32/app/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ fn main() {
"CARGO_FEATURE_CH32V003A4M6",
"CARGO_FEATURE_CH32V003F4U6",
"CARGO_FEATURE_CH32V003J4M6",
"CARGO_FEATURE_CH32V103C6T6",
"CARGO_FEATURE_CH32V103C8T6",
"CARGO_FEATURE_CH32V103C8U6",
"CARGO_FEATURE_CH32V103R8T6",
];
let selected: Vec<&str> = chips.iter().filter(|c| cfg_has(c)).copied().collect();
if selected.len() != 1 {
Expand All @@ -37,9 +41,9 @@ fn main() {
println!("cargo:rustc-link-search={out_dir}");
println!("cargo:rerun-if-changed=memory_x");
println!("cargo:rustc-link-arg=-Ttb-app.x");
println!("cargo:rustc-link-arg=-Ttb-boot-req.x");

if user_flash {
println!("cargo:rustc-link-arg=-Ttb-user-flash.x");
println!("cargo:rustc-link-arg=--wrap=_setup_interrupts");
println!("cargo:rustc-link-arg=-Tdefmt.x");
}
Expand Down
21 changes: 21 additions & 0 deletions examples/ch32/app/memory_x/system-flash/ch32v103c6t6.x
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* CH32V103 application memory layout for system-flash bootloader.
*
* The application occupies the full 32KB user flash. The bootloader lives
* in the separate system flash region, so no space is reserved here.
*
* Addresses use the code-execution alias (0x0000_0000 base).
* The FPEC programming offset (0x0800_0000) is handled by the HAL.
*/
MEMORY
{
FLASH : ORIGIN = 0x00000000, LENGTH = 32K
RAM : ORIGIN = 0x20000000, LENGTH = 10K
}

/* Region aliases required by qingke-rt's linker script. */
REGION_ALIAS("REGION_TEXT", FLASH);
REGION_ALIAS("REGION_RODATA", FLASH);
REGION_ALIAS("REGION_DATA", RAM);
REGION_ALIAS("REGION_BSS", RAM);
REGION_ALIAS("REGION_HEAP", RAM);
REGION_ALIAS("REGION_STACK", RAM);
21 changes: 21 additions & 0 deletions examples/ch32/app/memory_x/system-flash/ch32v103c8t6.x
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* CH32V103 application memory layout for system-flash bootloader.
*
* The application occupies the full 64KB user flash. The bootloader lives
* in the separate system flash region, so no space is reserved here.
*
* Addresses use the code-execution alias (0x0000_0000 base).
* The FPEC programming offset (0x0800_0000) is handled by the HAL.
*/
MEMORY
{
FLASH : ORIGIN = 0x00000000, LENGTH = 64K
RAM : ORIGIN = 0x20000000, LENGTH = 20K
}

/* Region aliases required by qingke-rt's linker script. */
REGION_ALIAS("REGION_TEXT", FLASH);
REGION_ALIAS("REGION_RODATA", FLASH);
REGION_ALIAS("REGION_DATA", RAM);
REGION_ALIAS("REGION_BSS", RAM);
REGION_ALIAS("REGION_HEAP", RAM);
REGION_ALIAS("REGION_STACK", RAM);
21 changes: 21 additions & 0 deletions examples/ch32/app/memory_x/system-flash/ch32v103c8u6.x
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* CH32V103 application memory layout for system-flash bootloader.
*
* The application occupies the full 64KB user flash. The bootloader lives
* in the separate system flash region, so no space is reserved here.
*
* Addresses use the code-execution alias (0x0000_0000 base).
* The FPEC programming offset (0x0800_0000) is handled by the HAL.
*/
MEMORY
{
FLASH : ORIGIN = 0x00000000, LENGTH = 64K
RAM : ORIGIN = 0x20000000, LENGTH = 20K
}

/* Region aliases required by qingke-rt's linker script. */
REGION_ALIAS("REGION_TEXT", FLASH);
REGION_ALIAS("REGION_RODATA", FLASH);
REGION_ALIAS("REGION_DATA", RAM);
REGION_ALIAS("REGION_BSS", RAM);
REGION_ALIAS("REGION_HEAP", RAM);
REGION_ALIAS("REGION_STACK", RAM);
Loading
Loading