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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ On CH32V00x or CH32V103, the flow is the same — swap the example directory. Se
The full documentation lives at **[openservocore.github.io/tinyboot](https://openservocore.github.io/tinyboot/)**. Highlights:

- [**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`
- [**CLI reference**](cli/README.md) — `tinyboot flash / info / erase / reset / baud / discover / 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
Expand Down
10 changes: 6 additions & 4 deletions ch32/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ tinyboot-ch32-rt = "0.4" # optional, bootloader-only

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

Expand All @@ -40,10 +40,12 @@ fn main() -> ! {
wire: FullDuplex { rx_pull: Pull::None, drive: PushPull },
tx_en: NoTxEn,
});
tinyboot_ch32::boot::run(transport, BootCtl::new());
tinyboot_ch32::boot::run(transport, BootCtl::new(), Unaddressed);
}
```

`Unaddressed` answers every frame, for a device that has the bus to itself. With several devices on one bus, pass `Addressable::new(chip_uid)` instead: the bootloader then stays silent until a host selects it by the chip's unique id (see the [shared bus guide](https://openservocore.github.io/tinyboot/shared-bus.html)).

`Storage` and `BootMetaStore` are initialized from linker symbols automatically. `boot_version!()` ends the image with its tail: a compatibility key and the crate's `Cargo.toml` version. `boot_version!(compat = "name")` sets the key, and the bootloader then accepts only app images built with the same `app_version!(compat = "name")` (see [app integration](https://openservocore.github.io/tinyboot/app-integration.html)).

`boot::run` reads frames with up to 52 payload bytes (a whole frame fits one 64-byte USB packet); `boot::run_with_payload::<N>` takes `N` bytes per Write (at least 18, not limited to a flash page) for faster updates, at `N` bytes of RAM. Hosts read the capacity from Info.
Expand All @@ -63,7 +65,7 @@ loop {
}
```

`app::poll` handles `Info` (responds with capacity, erase size, versions, `mode = 1`, the stored boot baud and the bootloader's compatibility key) and `Reset` (resets the device; `addr = 1` reboots into the bootloader, `addr = 0` reboots into the app). All other commands return `Status::Unsupported`.
`app::poll` handles `Info` (responds with capacity, erase size, versions, `mode = 1`, the stored boot baud and the bootloader's compatibility key) and `Reset` (resets the device; `ResetFlags::BOOTLOADER` reboots into the bootloader, otherwise into the app). All other commands return `Status::Unsupported`. On a shared bus, `app::new_addressable_app(ctl, wait_bytes)` also handles `Select` and stays silent until selected.

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.

Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ If you're new here, start with the [top-level README](https://github.com/OpenSer
- [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
- [Shared bus (addressable)](shared-bus.md) - several devices on one bus, selected by unique id
- [Building your bootloader from an example](examples.md)

## Reference
Expand Down
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- [GPIO-controlled boot mode selection](boot-ctl.md)
- [App integration](app-integration.md)
- [Remote firmware updates](remote-updates.md)
- [Shared bus (addressable)](shared-bus.md)
- [Building your bootloader from an example](examples.md)

# Reference
Expand Down
9 changes: 7 additions & 2 deletions docs/app-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The tinyboot bootloader is only half the story — to support remote firmware up

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.
3. **Poll the transport** for `Info` and `Reset` requests (and `Select` on a shared bus).

That's it. The bootloader handles everything else — flashing, verification, state transitions, trial boot.

Expand Down Expand Up @@ -57,12 +57,13 @@ If `confirm()` is never reached (panic, watchdog, init deadlock), the trials get

## `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`:
`poll()` reads a single frame from your transport and handles it. In the app, two commands do something (three when addressable), and the rest are rejected with `Status::Unsupported`:

| Command | Behavior in app |
| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Info` | Responds with capacity, erase size, boot + app versions, `mode = 1` (app mode), the stored boot baud and the bootloader's compatibility key. |
| `Reset` | Resets the device. The `BOOTLOADER` flag reboots into the bootloader, otherwise into the app. The `BAUD` flag first stores addr byte 0 as the bootloader's baud. |
| `Select` | Addressable apps only: selects or deselects the device by unique id. An unselected app answers nothing else. |

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).

Expand Down Expand Up @@ -94,6 +95,10 @@ tinyboot_ch32::app::app_version!(compat = "acme-widget-rev-b");

`compat = "name"` hashes the name at compile time (32-bit FNV-1a); `compat_key = 0x1234_5678` takes a raw value. tinyboot only compares keys, so encode whatever has to match: product, board revision, pinout. `Verify` refuses an image with another key, or none, with `Status::Incompatible` and leaves the device in its bootloader waiting for the right image; a mistaken image never starts. A bootloader built without a key accepts any image and carries no check.

## On a shared bus

Several devices on one bus need the addressable app, created with `new_addressable_app(BootCtl::new(), wait_bytes)`. It stays silent until the host selects it by the chip's unique id, like the addressable bootloader. `wait_bytes(n)` waits about `n` byte-times at the bus baud, and the example apps compute it from the USART's baud divisor. Build the bootloader addressable too. See the [shared bus guide](shared-bus.md).

## UART sharing notes

The bootloader and app normally share the same USART. A few gotchas:
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ 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).
If `--port` is omitted, the CLI probes USB serial ports by sending a `Select` request to each. If auto-detection fails, pass `--port /dev/ttyUSB0` (or the equivalent on your OS).

## 6. Verify it worked

Expand Down
2 changes: 2 additions & 0 deletions docs/porting-series.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ mod family;

Repeat for `rcc/` and `afio/` if their versions differ too.

Check that the series keeps its 96-bit unique id at `0x1FFF_F7E8` (the ESIG area, `ch32/src/hal/esig.rs`), which the addressable bootloader reads. Move it behind a `cfg` if not.

### Boot mode selection

The flash controller version also determines how the bootloader selects its flash region and persists run-mode across resets. The `build.rs` derives this automatically from the `boot_pin` flag (which itself comes from the flash version):
Expand Down
6 changes: 5 additions & 1 deletion docs/porting.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@ Mirror the layout of [`tinyboot-ch32`](https://github.com/OpenServoCore/tinyboot

| 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 |
| `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. For a shared bus, also `wait_bytes(n)`, a wait of about `n` byte-times |
| `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 |

### Unique id for a shared bus

To support [addressable devices](shared-bus.md), expose the chip's factory unique id as a function returning `&'static [u8; N]` (1 to 16 bytes, least significant first), like `tinyboot_ch32::boot::chip_uid`. Users pass it to `Addressable::new`. A function item keeps the id's address and length constant, so it costs only the loads.

## 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.
Expand Down
17 changes: 15 additions & 2 deletions docs/remote-updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ 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
├─ Select ──► (addressable) select or deselect by unique id
├─ Info ──► respond with capacity, versions, mode=1
└─ Reset ──► if flag & BOOTLOADER: mark run_mode = Service, reset
```


Expand Down Expand Up @@ -98,6 +99,18 @@ When all four hold, the bootloader hands off to the app. As soon as you run a re
| `Reset` to the app mid-flash | Refused with `Unsupported`; the device stays in the bootloader. Re-flash. |
| Image built for another board | Verify returns `Incompatible` (bootloader has a compatibility key); flash the right one. |

## Several devices on one bus

With addressable devices, add `--uid` to pick the device. Each command selects it again, so a reset in between is fine:

```sh
tinyboot discover
tinyboot reset --bootloader --uid 1032
tinyboot flash new-firmware.elf --reset --uid 1032
```

The device's siblings stay silent the whole time, including any that sit in their bootloader waiting for firmware. See the [shared bus guide](shared-bus.md).

## Making it automatic

Any host logic that can speak the tinyboot CLI can drive updates:
Expand Down
86 changes: 86 additions & 0 deletions docs/shared-bus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Shared bus (addressable)

By default a tinyboot device answers every frame it hears. That is fine with one device on the wire. But with several devices on one bus (a chain of servos, a row of sensor boards on RS-485), they would all answer at once.

An **addressable** device stays silent until the host selects it by its unique id. The bootloader and the app both support it, and the CLI drives it for you.

## The short version

```sh
# List every device on the bus by unique id.
tinyboot discover

# Talk to one of them. A unique prefix of the id is enough.
tinyboot info --uid 1032
tinyboot flash new-firmware.elf --uid 1032 --reset
```

With a single device on the bus you don't need `--uid` at all. The CLI selects the only device by itself, and falls back to the plain flow when the device is not addressable.

## Turning it on

In the bootloader, pass `Addressable::new(chip_uid)` to `run()`:

```rust
tinyboot_ch32::boot::run(transport, BootCtl::new(), Addressable::new(chip_uid));
```

`chip_uid` returns the chip's factory unique id (96 bits on CH32, read from the ESIG area). Pass `Unaddressed` instead for a device that has the bus to itself.

In the app, create the handle with `new_addressable_app`:

```rust
let mut app = tinyboot_ch32::app::new_addressable_app(BootCtl::new(), wait_bytes);
```

`wait_bytes(n)` waits about `n` byte-times at the bus baud. The app uses it to space out its `Select` reply (see the slot below). The examples compute it from the USART's baud divisor, so it needs no timer.

Make sure the bootloader and the app agree. If one is addressable and the other is not, the device answers differently depending on which mode it is in.

## The silence rule

An addressable device only speaks in two cases:

- It answers a `Select` whose prefix matches its id.
- It answers any other command while it is selected.

It never answers a frame it can't read (bad CRC, unknown command, payload too big), and it ignores frames that are replies from other devices. Selection lives in RAM, so every reset starts deselected.

This means a device sitting in its bootloader never disturbs the bus. Several devices waiting for firmware at once is fine too. Each one stays quiet until the host picks it.

## How `Select` works

`Select` carries a seed and a prefix of id bits. Every device whose id starts with the prefix becomes selected and replies with its id. Every other device becomes deselected.

- An empty prefix selects every device.
- A full id selects exactly one device.
- A prefix longer than any id selects none, which is how the host deselects everyone.

The [protocol reference](https://github.com/OpenServoCore/tinyboot/tree/main/lib/protocol#select) has the exact byte layout.

## Finding every device

When two devices reply at the same time, their bytes overlap on the wire and the host reads garble. The host uses that as a signal. `tinyboot discover` walks the id tree one bit at a time:

1. Select a prefix (it starts empty).
2. Silence means no device under this prefix, so this branch is done.
3. Garble means two or more devices, so try the prefix plus `0`, then plus `1`.
4. A clean reply is one device. The host double checks it by probing the next bit both ways. The device has to answer alone under one and nothing under the other.

The double check is there because devices running the same code can answer in exact unison. On some wires that can read back as one clean frame. Each device waits a **slot** before replying, 0 to 15 byte-times picked from a hash of its id and the seed. The host sends a new seed with every probe, so two devices that collided once get a fresh draw next time.

Each probe listens about 50 ms for replies. Devices whose ids part early are found in a few probes each. Two ids that share a long prefix take longer, since the walk goes down that prefix one bit at a time. The worst case, two 96-bit ids that differ only in their last bit, takes about 150 probes, so several seconds.

## Custom transports

The slot wait goes through `Transport::wait_bytes`. The CH32 USART implements it by spinning on the baud divisor. If you write your own transport for a shared bus, implement it too. The default returns right away, which still works, it just gives up the unison protection.

## What it costs

On CH32V003 in system flash, addressable adds about 130 bytes. Every combination of wire setup, DE pin, compatibility key and addressing still fits the 1920-byte system flash. CI builds the heaviest one (`examples/ch32/v003/boot/src/bin/heaviest.rs`) to keep it that way.

## Caveats

- Don't mix addressable and unaddressed devices on one bus. An unaddressed device answers everything, including `Select`, so it garbles every probe.
- Two devices with the same id can't be told apart. The walk stops with an error. Chip unique ids are unique, so this only happens with a custom id source.
- The CLI's `--uid` takes whole bytes (2 hex digits each), in the order `tinyboot discover` prints them.
10 changes: 10 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ Something in the UART chain isn't right. Work through these in order:

---

## `more than one device answered` or `no device answered`

The CLI selects a device before every command (see the [shared bus guide](shared-bus.md)).

- **More than one device** - several addressable devices share the bus. Run `tinyboot discover` and pass `--uid`.
- **No device answered** - the usual wiring and baud causes above apply. An addressable device also stays silent until selected, so a custom host tool has to send `Select` first.
- **Mixed bus** - an unaddressed device answers every frame, `Select` included, and garbles every probe. Build every device on a shared bus addressable.

---

## 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.
Expand Down
Loading
Loading