Files
ch32-bms/firmware/HARDWARE.md

152 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Hardware implementor's notes
Reference for anyone touching `firmware/src/*.rs`: what's wired to what, which
peripherals/pins are used and why, and the non-obvious dependency-version
constraints this firmware relies on. Written against the real, currently
maintained schematic at `/home/empire/workspace/PlantCtrl/Hardware/open-bms/bms/bms.kicad_sch`.
**The KiCad project checked into *this* repo (`board/bms/`) is a stale, older
board revision.** Its component references and pin assignments do not match
the hardware this firmware actually targets — do not use it as a source of
truth. Re-derive facts from the real schematic (e.g. via
`kicad-cli sch export netlist`) if anything below is in doubt.
Target chip: **CH32V203C8T6** (RISC-V, `riscv32imc-unknown-none-elf`), no
external crystal populated — clocked from HSI only (see RCC note below).
## Pin map
| Pin | Function | Notes |
|---|---|---|
| PA8 | D7 LED | Active-high, 2.2kΩ (R13) to GND on the cathode side |
| PB12 | D3 LED | Active-high, 2.2kΩ (R14) to GND |
| PB13 | D4 LED | Active-high, 2.2kΩ (R15) to GND |
| PB14 | D5 LED | Active-high, 2.2kΩ (R16) to GND |
| PB15 | D6 LED | Active-high, 2.2kΩ (R17) to GND |
| PA11 | USB D | Fixed-function USB pin on this chip, not remappable |
| PA12 | USB D+ | Fixed-function USB pin on this chip, not remappable |
| PA4 | SPI1 NSS (flash CS) | Driven manually as a GPIO `Output``ch32-hal`'s `Spi` has no CS management. Idle-high, matches the board's 10kΩ pull-up (R7) |
| PA5 | SPI1 SCK | Default/non-remapped SPI1 pin |
| PA6 | SPI1 MISO | Default/non-remapped SPI1 pin |
| PA7 | SPI1 MOSI | Default/non-remapped SPI1 pin |
| PB9 | INA219 SDA | **Bit-banged**, not hardware I2C — see gotcha below |
| PB10 | INA219 SCL | **Bit-banged**, not hardware I2C — see gotcha below |
| PB2 | Spare GPIO input | Pulled low via 10kΩ (R8), nothing else attached. Only real "input" available on this board |
| PB6 | I2C1 SDA (`S_SDA`) | Battery-pack-facing bus (XT30 connector pins 3/4, BAV99 protection diodes). Currently unused — see I2C1 note below |
| PB7 | I2C1 SCL (`S_SCL`) | Battery-pack-facing bus. Currently unused — see I2C1 note below |
Not wired to anything: PA0-3, PA9-10, PA15, PB0-1, PB3-5, PB8, PB11, PC13-15.
`Boot1`/`Reset1` are the BOOT0 strap and NRST reset buttons respectively —
not runtime-readable GPIOs, don't try to poll them as inputs.
## Peripherals in use
- **USBD** — USB CDC-ACM virtual serial console. `hal::usbd::Driver::new(p.USBD, Irqs, p.PA12 /*dp*/, p.PA11 /*dm*/)`. Interrupt vector is shared with CAN1: `bind_interrupts!` needs `USB_LP_CAN1_RX0 => hal::usbd::InterruptHandler<hal::peripherals::USBD>`. Max packet size is 64 bytes — `write_packet()` on a single call **fails** (`EndpointError::BufferOverflow`, does not panic) for anything longer; chunk before sending (see `usb_writer` in `main.rs`).
- **SPI1** — blocking, talks to the W25Q128JVE NOR flash (U5). `hal::spi::Spi::new_blocking::<0>(p.SPI1, sck, mosi, miso, config)` — note the argument order is `(sck, mosi, miso)`, not alphabetical. No hardware CS support in this driver; drive it yourself.
- **I2C1** — hardware peripheral, PB6/PB7 (remap 0). Intended for the battery-pack-facing slave bus (device acts as an I2C *slave*, address `0x55` in the existing but currently-commented-out code). **Not currently enabled**`I2c::listen_blocking()` is a genuine busy-spin with no `.await`, which starves every other embassy task (USB included) on this single-threaded executor the instant it's called. Don't re-enable it without either moving it off the main task's critical path or replacing it with a non-blocking variant.
- **I2C2** — hardware peripheral, fixed to PB10 (SCL) / PB11 (SDA), no remap. **Do not use for the INA219.** PB11 is completely unconnected on this board. See the bit-bang gotcha below.
- **TIM2** — claimed by `ch32-hal`'s `time-driver-tim2` feature as the embassy time driver's tick source. Don't reuse TIM2 directly for anything else.
## Hardware gotcha: the INA219 bus needs software (bit-banged) I2C
The schematic labels a bus "SCL"/"SDA" going to the INA219 (U3), wired to
PB10 and PB9. This looks like it should be a normal hardware I2C bus, but
**it isn't reachable by any single hardware I2C peripheral on this chip**:
- I2C2 is fixed to PB10=SCL, **PB11**=SDA (no remap available). PB11 is
unconnected on this board.
- I2C1 remap 1 is **PB8**=SCL, PB9=SDA. PB8 is unconnected on this board.
PB10 only pairs (in hardware) with PB11; PB9 only pairs with PB8. Neither
partner pin is wired to the sensor. This is a board wiring quirk, not
something fixable via `ch32-hal` configuration. `firmware/src/selfcheck.rs`
therefore implements a minimal bit-banged I2C driver (`BitbangI2c`) over
plain open-drain GPIOs (`hal::gpio::Flex`) on PB9/PB10, implementing
`embedded_hal::i2c::I2c` via its single required `transaction()` method (the
`read`/`write`/`write_read` provided methods come for free from that). It
does **not** support clock stretching — fine for the INA219, which doesn't
stretch, but keep that in mind if another device ever goes on this bus.
INA219 address: **0x48**`A0`→GND, `A1`→SDA on this board, i.e.
`ina219::address::Address::from_pins(Pin::Gnd, Pin::Sda)`. Not the more
common default addresses seen in INA219 examples/datasheet defaults.
Current-sense shunt: R4 ‖ R5, 100mΩ each → **50mΩ** combined. Current is
derived in software from `shunt_voltage_uv() / 50`, not from the INA219's
own calibration registers (`IntCalibration`) — this board's real max current
draw isn't documented anywhere, so guessing a `current_lsb` for hardware
calibration seemed worse than a simple, honest Ohm's-law calculation using
the known shunt value.
## RCC / clock configuration
`hal::init` is called with `rcc: hal::rcc::Config::SYSCLK_FREQ_144MHZ_HSI`,
**not** the HAL's default config. This is required for USB to get a valid
48MHz derived clock, and it's HSI-based because there's no external crystal
on this board (`OSC_IN`/`OSC_OUT` are unconnected). If you ever add code
that assumes a different clock tree, check this first.
## Dependency versions that matter (not arbitrary pins in `Cargo.toml`)
These aren't just "whatever version worked" — each one fixes a real,
previously-hit build or runtime failure. Don't casually bump/change them
without re-verifying:
- **`embassy-executor` features = `["arch-spin", "executor-thread"]`** — NOT
`arch-riscv32`. `arch-riscv32`'s WFI-based sleep races with the USB
interrupt's wake signal: if the interrupt fires right as the core is about
to sleep, the wake can be missed and anything `.await`ing on it (e.g.
`wait_connection()`/`write_packet()`) hangs forever, even though the
device still enumerates fine. `arch-spin` busy-polls instead of sleeping,
sidestepping the race. (Symptom if this regresses: USB enumerates, but no
console output ever appears.) Confirmed the same fix is needed in the
sibling `PlantCtrl/Software/CAN_Sensor` project on the same HAL fork.
- **`embassy-time` features = `["generic-queue-8"]`, version `"0.5.0"`** —
needs to match the `embassy-time` version `ch32-hal` itself pulls in
transitively (currently 0.5.x); a direct-dependency version mismatch
causes a Cargo `links` conflict (`embassy-time-queue-utils` can't resolve
two different versions simultaneously). The `generic-queue-8` feature
makes `embassy-time` use its own self-contained timer queue instead of
requiring `embassy-executor` to implement an "integrated timer queue"
symbol that our pinned `embassy-executor 0.7.0` doesn't provide (that
support landed in later `embassy-executor` versions).
- **`embassy-usb = "0.5.1"`** — NOT 0.3.0/0.4.0. Those depend on
`embassy-usb-driver ^0.1.0`; `ch32-hal`'s `hal::usbd::Driver` implements
the `embassy-usb-driver 0.2.0` traits. `embassy-usb` versions ≥0.5.0 are
the first to depend on `embassy-usb-driver ^0.2.0`. Using an older
`embassy-usb` here is a hard type-check failure, not a subtle bug.
- **`heapless` feature = `"portable-atomic-critical-section"`** — needed for
`static_cell` (used for the `'static` USB buffers/class/device via the
`mk_static!` macro in `main.rs`) to build at all. `riscv32imc` has no
native atomic compare-and-swap instruction; without this feature,
`static_cell`'s internal `AtomicBool::compare_exchange` fails to compile
for this target. This feature makes `portable-atomic` (a shared transitive
dependency) emulate CAS via critical sections instead.
- **`ch32-hal`** — git dependency on `ju6ge/ch32-hal`, branch
`feature/i2c-slave-api` (upstream `ch32-rs/ch32-hal` does **not** have the
I2C slave-mode API this firmware's (currently disabled) I2C1 code depends
on — checked directly, no `SlaveConfig`/`listen_blocking` there). This
branch has been force-pushed/rebased before (the previously-locked commit
disappeared from GitHub entirely) — if `cargo build` ever fails to fetch
the pinned commit in `Cargo.lock`, that's almost certainly why; re-run
`cargo update -p ch32-hal` to pick up the branch's current tip.
- Entry point is `qingke_rt::entry` (via
`#[embassy_executor::main(entry = "qingke_rt::entry")]`), matching
`ch32-hal`'s own examples for this chip family.
## Logging
`println!` in `main.rs` is a **local macro**, not `ch32_hal::println!` — the
latter writes over WCH's SDI single-wire debug protocol (needs a WCH-Link
probe attached, see `hal`'s `debug.rs`). The local one formats into a
`heapless::String<128>` and pushes it onto a channel (`LOG_CH`) that a
spawned `usb_writer` task drains and writes out over the USB CDC-ACM
console. It's `#[macro_export]`ed with `$crate::LOG_CH` internally so other
modules (e.g. `selfcheck.rs`) can call `crate::println!(...)` and have it
land in the same place. Sends are non-blocking (`try_send`) and silently
drop the line if the channel (capacity 8) is full — there's no backpressure,
so a burst of many `println!` calls in a tight loop with no `.await` in
between can lose messages if nothing's draining the channel yet (e.g. no
terminal connected). USB CDC devices also re-enumerate on every reflash;
most terminal programs need to be manually reconnected afterward.