Skip to content

Commit 9e462fc

Browse files
committed
Fix raw PPG startup, FIFO recovery, and hub firmware flashing
Bring-up on 40.2.2 and 40.6.0 boards surfaced several driver bugs and made the firmware flasher usable. Application driver: - startRaw() sent a 5-byte LED-current frame (0x40 0x03 <reg> <val> 0x00); the hub expects exactly 4 bytes (UG6921 Table 6 1.8/1.9) and rejects the padded form. - startRaw() treated "enable BPT in estimation mode" as fatal. That command is rejected on 40.2.2+ until a calibration vector, date/time and SpO2 coefficients are loaded, so a freshly booted board could never stream raw PPG. The algorithm contributes nothing in sensor-only output mode, so the step is now best-effort. Command order now follows UG6921 Table 6. - enterAppMode() unconditionally wrote 0x01 0x00 0x00 and waited 20 ms. That restarts the hub application, leaving later commands to land mid-init. UG6921's flows only ever read the mode, so it now reads first and switches only when needed, with a full settling delay. - enableAfe(true) retries a 0xFF up to 3 times at 250 ms spacing and, on final failure, traces the AFE PART_ID read that UG6921 Table 1 prescribes. - readRaw()/readSamples() flush the FIFO when a sample read fails and report the partial count. An overflowed output FIFO rejects every 0x12 0x01 read until emptied, which previously wedged streaming permanently. - Report the algorithm version as unavailable when the hub does not answer 0xFF/0x07 (status 0x02) instead of a fabricated 0.0.0. - Added readAfePartId(), flushFifo(), algoVersionValid(). Firmware flashing: - writePage() split the 8208-byte payload across 30-byte I2C frames; the bootloader scores a page as one command and answered 0x02 (incorrect byte count). It now sends the whole page in a single transaction, with the two chunked strategies kept selectable for A/B testing. - Page size comes from the image header (uint16 LE at 0x46 plus 16 CRC bytes) rather than a hard-coded 8208. The bootloader's own 0x81 0x01 answer is not a byte count: BL 8.0.0 reports 2048 words for the same 8192-byte page. - configureForPageSize() grows the Wire buffer where the core allows it and returns PageTooLarge otherwise, before the erase step, so an unsuitable host leaves the hub's firmware intact. The UNO R4 cannot flash the hub -- its Renesas Wire caps a transaction at 255 bytes; use an ESP32 or RP2040. - Driver traces are recorded even with no debug stream and served over a new 'D' command, so the host script can explain a failure. - New flash-firmware.sh drives both stages, and extras/flash_tool/README.md documents the tool, the host-board constraint and troubleshooting. 10.DeviceInfoAndDiagnostics now enables tracing, prints the AFE PART_ID, sweeps sensor indices when it does not answer, and drains the FIFO fully. All 11 examples compile for Uno R4 Minima; the flasher also builds for ESP32, RP2040 and the AVR stub. The bootloader page write remains unverified on hardware.
1 parent 97a99ef commit 9e462fc

12 files changed

Lines changed: 1675 additions & 89 deletions

CHANGELOG.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,114 @@
22

33
All notable changes to the ProtoCentral Pulse Express library.
44

5+
## [Unreleased]
6+
7+
### Fixed
8+
- **`startRaw()` failed on 40.2.2 hubs, leaving the hub idle** (`dataReady` never
9+
asserting). Two causes:
10+
- The MAX30101 LED-current writes sent a 5-byte frame
11+
(`0x40 0x03 <reg> <val> 0x00`); the hub expects exactly 4 bytes and rejects
12+
the padded form.
13+
- Raw sensor-only mode required "enable BPT in estimation mode"
14+
(`0x52 0x04 0x02`) to succeed. That command is rejected on 40.2.2+ until a
15+
calibration vector / date-time / SpO2 coefficients have been loaded, so a
16+
freshly-booted board could never stream raw PPG. The algorithm contributes
17+
nothing in sensor-only output mode, so the step is now best-effort: its
18+
status is traced and raw streaming continues.
19+
- **`begin()` restarted the hub application unnecessarily.** `enterAppMode()`
20+
unconditionally wrote `0x01 0x00 0x00` (set operating mode) and waited only
21+
20 ms. That command restarts the hub's application; the following commands
22+
then landed while its sensor stack was still initialising and the AFE enable
23+
answered `0xFF`. UG6921's application-mode flows never write the mode byte —
24+
the hub boots into application mode when MFIO is high at reset — so
25+
`enterAppMode()` now *reads* the mode first (`0x02 0x00`) and only switches,
26+
with a full 1.4 s settling delay, if the hub reports something other than
27+
application mode.
28+
- `enableAfe(true)` retries a `0xFF` up to 3 times at 250 ms spacing (UG6921
29+
Table 1: "insert delay and resend"), and on final failure traces the AFE
30+
`PART_ID` read so the log distinguishes a sequencing fault from a hub that
31+
genuinely cannot reach the sensor.
32+
- `startRaw()` traces the status of every sub-step so a failure names the
33+
offending command, and now follows UG6921 Table 6's documented ordering
34+
(AFE -> algorithm -> AGC off).
35+
36+
- **Misleading "Algorithm firmware: 0.0.0".** UG6921 documents `AA FF 03` (hub
37+
version) but not `AA FF 07`; some 40.x builds reject the latter with status
38+
0x02 (incorrect byte count for the family). `begin()` ignored that failure and
39+
left the version at 0.0.0, which reads like a broken hub in a diagnostic log.
40+
The result is now tracked via `algoVersionValid()`.
41+
42+
- **Hub output-FIFO overflow wedged raw streaming.** `readRaw()`/`readSamples()`
43+
return at most `cap` samples per call; a caller that polls slowly (as
44+
`10.DeviceInfoAndDiagnostics` did, one 16-sample call per 500 ms) lets the
45+
backlog grow until the hub's output FIFO overflows, after which every
46+
`0x12 0x01` read is rejected with `0xFF` and streaming never recovers. Both
47+
readers now flush the FIFO on a failed sample read, report how many samples
48+
they did retrieve, and trace a warning when the overflow flag is set. The
49+
example drains the FIFO to empty each pass and polls at 100 ms.
50+
51+
### Added
52+
- **`flash-firmware.sh`** — one-command hub firmware flashing: compiles and
53+
uploads `11.FirmwareFlash` to the Arduino host, then runs
54+
`extras/flash_tool/flash_msbl.py` against it. Auto-detects the board's serial
55+
port (preferring one matching the target FQBN), with `--dry-run`,
56+
`--sketch-only`, `--skip-upload`, `--port`, `--fqbn`/`--wifi`/`--esp32`.
57+
- **`extras/flash_tool/README.md`** — full `flash_msbl.py` usage: how the sketch
58+
and host script split the work, options, a sample run, troubleshooting table,
59+
and the `.msbl` layout the parser expects.
60+
- `flash_msbl.py` reports malformed images, missing files, serial errors and
61+
Ctrl-C as one-line messages with distinct exit codes instead of tracebacks,
62+
validates the `msbl` magic, and prints the target part and crypto from the
63+
header. Bootloader status bytes come with a plain-English hint.
64+
65+
### Fixed (firmware flashing)
66+
- **Page size was hard-coded to 8208 bytes.** It now comes from the image header
67+
(uint16 LE at offset `0x46`, plus 16 CRC bytes) and is sent to the sketch,
68+
which sizes each `P` command from it. Overridable with `--page-size`.
69+
The bootloader's own `0x81 0x01` answer must NOT be used as a byte count:
70+
UG6806's trace (BL 3.0.0) returns 8192 there, but **BL 8.0.0 returns 2048 for
71+
the same 8192-byte page** — 32-bit words, matching the `4` at `.msbl` offset
72+
`0x48`. The sketch now prints that value as information only.
73+
- **Page writes were split across I2C transactions; the bootloader requires one.**
74+
`writePage()` sent the 8208-byte payload in 30-byte `Wire` frames with a STOP
75+
after each, and the MAX32664D answered `0x02` (incorrect byte count) because it
76+
scores each frame as a separate command. It now sends `0x80 0x04` + the whole
77+
page in a **single transaction** (`ChunkMode::SingleTransaction`, the default).
78+
The two chunked strategies remain selectable via `setChunkMode()` /
79+
`--chunk-mode {repeated-start,stop-each}` for A/B testing.
80+
- **The host board must be able to buffer a whole page.** New
81+
`configureForPageSize()` grows the Wire buffer on cores that support it
82+
(`WIRE_HAS_BUFFER_SIZE`: ESP32, RP2040 — the sketch pre-sizes before
83+
`Wire.begin()`, which RP2040 requires) and returns the new
84+
`Status::PageTooLarge` (0xE4) when the host cannot. The check runs **before**
85+
the erase step, so an unsuitable host leaves the hub's firmware intact.
86+
**The Arduino UNO R4 cannot flash the hub** — its Renesas Wire caps a
87+
transaction at 255 bytes. The sketch prints `maxTxn=` and the host script
88+
refuses to start when it is too small.
89+
- `writePage()` checks every `Wire.write()` return value; on a platform whose TX
90+
buffer is smaller than the chunk size it now reports a comm error instead of
91+
silently dropping bytes and sending an under-length page.
92+
- **Flash failures were undiagnosable.** The sketch runs with debug output off
93+
(the serial link carries a binary protocol), so driver traces went nowhere.
94+
`trace()` now always records into a buffer exposed by `lastError()`, the sketch
95+
serves it via a new `D` command, and `flash_msbl.py` prints it under any error
96+
(`board says: ...`).
97+
- `flushFifo()` — discard the hub's output FIFO (bounded), the documented
98+
recovery from an overflow.
99+
- `algoVersionValid()` — false when the hub did not answer the algorithm-version
100+
read, so callers can print "unavailable" instead of a fabricated 0.0.0.
101+
- `readAfePartId(uint8_t&, uint8_t sensorIdx = kAfeSensorIndex)` — reads a
102+
sensor's `PART_ID` register (`0x41 0x03 0xFF`, expect `0x15`), the diagnostic
103+
UG6921 Table 1 prescribes for `0xFF` responses. The index argument lets a
104+
caller sweep the sensor slots to find where a given firmware image maps the
105+
AFE; `10.DeviceInfoAndDiagnostics` does this automatically when the default
106+
index does not answer.
107+
108+
### Changed
109+
- `10.DeviceInfoAndDiagnostics` enables `setDebug(&Serial)`, prints the AFE
110+
`PART_ID` and the `startRaw()` status code, and reports FIFO sample count plus
111+
the first IR/Red counters each poll.
112+
5113
## [2.1.0] - 2026-06-17
6114

7115
### Changed

0 commit comments

Comments
 (0)