Commit Graph

5 Commits

Author SHA1 Message Date
Oliver Walter 25dba97c06 Stop miscounting SHT31 bring-up and status-read failures
Two ways the error counters lied about sensor health.

The first measurement failed on every boot, leaving a permanent
i2c_errors=1 that a master polling READ_STATUS could not tell apart from
a real fault. The sensor NACKs a command arriving while it is still busy
with the previous one, and the bring-up sequence issued a measurement
immediately after clear_status with no quiet time. Commands are now
spaced by sht3x::COMMAND_SETTLE_MS. Bring-up failures stay uncounted on
purpose: the counters describe operational health, and an unreadable
serial already surfaces as a zero in READ_INFO.

read_status() failures were discarded via `if let Ok`, which made a
persistently unreadable status register indistinguishable from a
genuinely clean one — both leave sensor_status at zero. They are now
counted, but kept out of the consecutive-failure tally so only the
measurement path drives the fault and soft-reset logic.

Confirmed on hardware: a fresh boot now holds err[i2c=0 sht_crc=0
frame=0 bus_crc=0] indefinitely, where it previously showed i2c=1 from
the first poll onwards. After a full bus test run the counters read
exactly frame=2 bus_crc=2, accounting for the four faults the suite
injects deliberately and nothing else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 20:41:07 +02:00
Oliver Walter aa48d8784f Fix silent truncation of frames longer than the RX FIFO watermark
Every frame of 16 bytes or more was rejected as LengthMismatch and
silently dropped. Found by the new end-to-end suite; neither the host
tests nor the sensor verification could reach it, because it needs real
bus timing and a frame past the watermark.

The watermark interrupt fires mid-frame once 16 bytes are in the FIFO.
The ISR drained those, stamped last_byte_at, and started frame_gap. The
remaining bytes then sat *below* the watermark and raised no interrupt at
all until the PL011's receive timeout eventually fired, so frame_gap
measured its 1822 us gap from a mid-frame timestamp, concluded the frame
had ended while its tail was still arriving, and parsed a prefix whose
LEN field disagreed with its length.

Frames under 16 bytes never trip the watermark, so their only interrupt
is the receive timeout, by which point every byte is drained and the
timestamp is honest. That is why all five real commands worked and only
an oversized PING exposed it.

frame_gap now drains the FIFO itself before each decision rather than
trusting a timestamp the ISR left behind, so a byte that has landed can
always push the deadline back. The gap is then measured from when the
byte was observed rather than when it arrived, making the reply up to one
gap period later than strictly necessary; 1.8 ms is a fair price.

Verified on hardware across every payload size from 0 to 59 — frames of
5 to 64 bytes — with the node's error counters accounting exactly for the
faults the suite injects deliberately and nothing else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 20:32:23 +02:00
Oliver Walter ddfda14085 Add USB-RS485 bridge and PC-side test suite
Makes end-to-end verification of the bus possible with a spare RP2040 and
no dedicated RS485 dongle.

bridge/ turns a second Pico into a transparent USB-to-RS485 bridge. It is
deliberately protocol-agnostic: bytes from USB go out on the pair, bytes
from the pair go back up USB, and it knows nothing about frames,
addresses or CRCs. A protocol-aware bridge could have a bug that happens
to agree with the node's own, which would defeat the point of using it as
a test instrument. It also needs no register-level code — it never has to
delimit a frame, and the one thing it does need, knowing that the final
stop bit has cleared the shifter before releasing DE, rp2040-hal exposes
as uart_is_busy().

tools/wiredsensor.py is an independent implementation of the wire format,
written from the specification in README.md rather than sharing code with
wiredsensor-core. Shared code would let a framing or CRC bug cancel out
and every test pass regardless. Cross-checked: it reproduces the
CRC-16/MODBUS catalogue vector and builds byte-identical frames to the
Rust side for all five documented examples.

Its `test` subcommand covers what host tests structurally cannot, all of
it timing-dependent: that malformed and mis-addressed frames leave the
node silent rather than colliding with whoever was actually addressed,
that the driver-enable turnaround leaves replies intact, and that no
state leaks between back-to-back frames.

Uses one wiring diagram for both boards, since the bridge mirrors the
node's GPIO0/1/2 assignment. Frames must fit one 64-byte USB packet, as
each host write becomes exactly one DE-bracketed transmission; every real
command is at most 21 bytes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 20:31:30 +02:00
Oliver Walter bcc6869965 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>
2026-07-28 18:32:52 +02:00
Oliver Walter 93b4e2c4f8 Initial commit: RP2040 RS485 temperature/humidity node
RTIC 2 firmware for an RP2040 acting as an RS485 slave, reading a
Sensirion SHT31 over I2C and answering a custom binary protocol.

Split into two crates so the protocol is testable without hardware:

- wiredsensor-core: framing, CRC-16/MODBUS, command dispatch and SHT3x
  data-sheet math. Pure computation, no I/O, no peripherals. Covered by
  42 host tests including the CRC-16/MODBUS catalogue vector, the SHT3x
  data-sheet CRC-8 example, and an exhaustive single-bit-corruption
  sweep over every byte of a frame.
- wiredsensor-fw: the RTIC application, PL011 register driver and SHT31
  I2C driver.

Requests are answered entirely from a cached reading. An SHT31
high-repeatability conversion takes up to 15 ms, well past the
turnaround a master expects, so the sensor is polled at the lowest
priority and the bus path never touches I2C. RTIC's priority ceilings
make "a conversion cannot delay a reply" a checked property rather than
an argument.

Two PL011 details drive the low-level approach, since rp2040-hal exposes
neither: the receive-timeout interrupt for prompt frame delimiting, and
the BUSY flag, which is the only way to know the final stop bit has left
the shift register before releasing DE. The HAL performs the fiddly
baud-divisor setup once, then the raw peripheral is reclaimed via free().

Note that RTIM cannot be the sole frame delimiter: it only fires while
the RX FIFO is non-empty, so a frame landing exactly on the FIFO
watermark is drained by the watermark interrupt and never times out. The
authoritative delimiter is a monotonic timer measured from the last
received byte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 17:17:44 +02:00