Add USB CDC diagnostics, verified against real hardware

Presents a USB CDC serial port alongside the RS485 protocol, so the node
can be verified with nothing but a USB cable — no SWD probe and no RS485
adapter. Motivated by having neither available: probe-rs needs SWD and
defmt logs travel over RTT, so with only the USB bootloader attached
there was no way to observe the sensor at all.

Two tasks, both at the lowest priority so they can never delay a bus
reply:

- usb_irq (binds USBCTRL_IRQ) services the stack and drains the RX
  buffer, which has to happen even though the port is output-only.
- usb_report emits one diagnostic line per second.

The report deliberately exposes raw internal state rather than the
protocol's flag encoding: "is the sensor working and what does it read"
is a different question from what a bus master asks.

Writes discard whatever does not fit rather than blocking, so an
unattended port cannot stall the firmware. The USB bus allocator lives
in an #[init(local = ...)] resource, which supplies the 'static the
device and class borrow from without a static mut.

Verified on hardware. The SHT31 reports serial 0x2d5ac752, read from its
factory serial register and CRC-8 validated, with readings of 27.3 C and
43.5 %RH varying by a few LSB between samples — the noise floor of a live
16-bit conversion rather than a stuck cache.

Costs ~17 KiB of flash and ~1.3 KiB of RAM, taking the image to ~34.5 KiB
of 2 MiB and ~2.5 KiB of 256 KiB.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Oliver Walter
2026-07-28 18:32:52 +02:00
parent 93b4e2c4f8
commit bcc6869965
5 changed files with 280 additions and 3 deletions
+49 -1
View File
@@ -235,7 +235,53 @@ Flash and watch `defmt` logs over SWD:
cargo run --release -p wiredsensor-fw # runner is probe-rs
```
Resource use is ~17 KiB of flash and ~1.2 KiB of RAM, of 2 MiB and 256 KiB.
Readings are logged at `debug` level while `.cargo/config.toml` pins
`DEFMT_LOG=info`, so use `DEFMT_LOG=debug cargo run …` to see them.
Resource use is ~34.5 KiB of flash and ~2.5 KiB of RAM, of 2 MiB and 256 KiB.
Roughly half the flash is the USB stack.
## USB diagnostics
The node also presents a **USB CDC serial port**, so it can be verified with
nothing but a USB cable — no SWD probe and no RS485 adapter. This is a diagnostic
aid, not part of the protocol, and it runs at the lowest priority so it can never
delay a bus reply.
With no debug probe available, flash over the bootloader instead: hold `BOOTSEL`
while plugging in, then
```bash
cp target/thumbv6m-none-eabi/release/wiredsensor-fw /tmp/fw.elf
picotool load -x /tmp/fw.elf # picotool requires a known file extension
```
The board appears as `16c0:27dd` "wiredsensor RS485 node". Read it with:
```bash
stty -F /dev/ttyACM0 raw -echo && cat /dev/ttyACM0
```
```
=== wiredsensor v0.1.0 ===
unit=0x01 baud=19200 gap=1822us
SHT31 on I2C1 SDA=GP14 SCL=GP15 addr=0x44 100000Hz
[ 82s] t=+27.349 C rh=+43.486 % age=985ms | OK fails=0
sht_serial=0x2d5ac752 sht_status=0x0000 err[i2c=1 sht_crc=0 frame=0 bus_crc=0]
```
A non-zero `sht_serial` is the useful signal: it is read from the SHT31's factory
serial register and CRC-8 validated, so a missing or miswired sensor cannot fake
it. Small jitter in the last digits of the readings is the genuine noise floor of
a live 16-bit conversion — an identical repeated value would suggest a stuck
cache instead.
The banner prints once at start-up. Output is dropped when no host is draining
the port, rather than blocking the firmware, so hold the port open across a reset
if you want to see it.
Note `16c0:27dd` is the pid.codes generic CDC-ACM pair, intended for development.
Replace it before shipping.
## Design notes
@@ -264,6 +310,8 @@ drives it from registers thereafter.
| 3 | `uart0_irq` | hardware | Empty the 32-byte RX FIFO, timestamp bytes |
| 2 | `frame_gap` | async | Detect the frame gap, answer the request |
| 1 | `sensor_task` | async | Poll the SHT31 once a second |
| 1 | `usb_irq` | hardware | Service the USB CDC diagnostic port |
| 1 | `usb_report` | async | Emit a diagnostic line once a second |
| 1 | `heartbeat` | async | Blink the status LED |
The central decision is that **requests are answered entirely from a cached