feat: battery measurement and SHT40/BQ27220 sensor telemetry#7
Merged
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012g2e8mr132vcizx92WsgiR
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012g2e8mr132vcizx92WsgiR
CMSIS 6 (framework-zephyr 3.40400 for the LM20A) gates DSP intrinsic wrappers on __ARM_FEATURE_DSP; forcing the macro exposes __sxtb16/__sxtab16 which g++ never declares as builtins, failing every C++ TU. No code in this tree uses DSP intrinsics, and the compiler predefines the macro itself when the target arch actually supports DSP. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012g2e8mr132vcizx92WsgiR
…reakage Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012g2e8mr132vcizx92WsgiR
Port the nRF52840 reference telemetry path to the nRF54 firmware: - Battery voltage via SAADC (opendisplay_battery.c). Selects the AIN channel at runtime from the configured battery_sense_pin, honors the optional enable pin, 10-sample average, 30 s TTL cache. Scales the reading into the reference's 10-bit / 3.6 V count space so existing voltage_scaling_factor calibrations work unchanged. - Bit-banged I2C master over the nRF54 GPIO helpers (opendisplay_i2c.c), bounded clock-stretch timeouts, never hangs the main loop. - SHT40 driver (opendisplay_sensor_sht40.c): measurement, CRC, and the exact 3-byte MSD dynamic-slot encoding. - BQ27220 driver (opendisplay_sensor_bq27220.c): voltage register as the preferred battery source, SOC + charging dynamic byte. - MSD payload now feeds real battery 10 mV (9th bit in status bit0) and ticks the sensors before each update, mirroring updatemsdata(). - Boot screen battery read wired to the new module. AXP2101 skipped (PMIC power-mgmt, not an MSD telemetry source on the reference). Chip-temp byte 13 keeps the boot-once cache (no safe periodic die-temp path under the BLE controller). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012g2e8mr132vcizx92WsgiR
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Brings the nRF54 port to telemetry parity with the nRF52840 reference firmware: real battery voltage, SHT40 temperature/humidity, and BQ27220 fuel-gauge state, all fed into the advertisement MSD payload. Before this,
battery_voltage_10mvwas hardcoded to 0 and the parsedsensor_data(0x23) packets were never consumed.New modules (mirroring
Firmware/src/display_service.cpp+sensor_sht40.cpp+sensor_bq27220.cpp):opendisplay_battery.c/.h— SAADC battery read with a 30 s TTL cache, source priority BQ27220 then ADC (matchesreadBatteryVoltageUncached).opendisplay_i2c.c/.h— a minimal bit-banged I2C master over thenrf54_gpiohelpers (runtime pins from thedata_bus0x24 config), with bounded clock-stretch timeouts so a stuck line fails the transfer instead of hanging the main loop.opendisplay_sensor_sht40.c/.h— measurement command, CRC-8, and the exact 3-byte MSD dynamic-slot encoding.opendisplay_sensor_bq27220.c/.h— voltage register (preferred battery source), SOC + charging dynamic byte.Wiring:
update_msd_payload()now ticks the sensors and reads the battery source before packing the frame (mirrorsupdatemsdata()); the boot screen'sboot_read_battery_voltage()is wired to the new module.SensorData/PowerOptiongain named fields (i2c_addr_7bit,msd_data_start_byte,charge_*) over previously-reserved bytes — struct sizes are unchanged (30 B / 30 B), so the config wire format andmemcpy-based parser are untouched.Verification
CI (PlatformIO CI, this branch): both environments green —
build (seeed-xiao-nrf54l15): success,build (seeed-xiao-nrf54lm20a): success. Cannot be built locally (no arm-none-eabi + Zephyr SDK), verified only via Actions on the fork.ADC transfer-function math (parity with the reference)
The reference reads the sense pin with Arduino
analogRead(sensePin)on the Adafruit nRF52 core and never callsanalogReadResolution/analogReference, so it uses the core defaults: 10-bit resolution withAR_DEFAULT(0.6 V internal ref, 1/6 gain, i.e. 3.6 V full scale). Existing device configs calibratedvoltage_scaling_factorfor:On nRF54L the SAADC channels come from the board DTS (
channel@0..7->AIN0..AIN7, gain 1/4,ADC_REF_INTERNAL, 12-bit, 8x oversample).adc_raw_to_millivolts_dt()gives the pin voltage in millivolts, which I convert back into the reference's 10-bit / 3.6 V count space before applying the unchanged formula:Worked example — 4.2 V battery through a 2:1 divider (2.1 V at pin),
voltage_scaling_factor = 703:So configs calibrated on the reference read the same voltage here without recalibration. The 10-sample average and 30 s TTL are preserved; the optional enable pin is asserted HIGH / settled 10 ms / released exactly as the reference (note:
battery_sense_flagsENABLE_INVERTEDis intentionally not honored — the reference ignores it too).Analog-pin constraint: on nRF54L only P1.00–P1.07 are AIN-capable (P1.0n -> AINn). The compact pin byte
(port<<4)|pinis mapped to the channel index; a non-analog pin is rejected with a log and battery reads as absent (0), never a hang.SHT40 3-byte MSD dynamic-slot encoding
Little-endian 24-bit value written at the sensor's
msd_data_start_byte(default 7):v = (rh_deci & 0x3FF) | (tu << 10), wheretu = temp_deci + 400(temp in 0.1 C, clamped -40.0..125.0 C) andrh_deciis 0.1 %RH steps (0..1000). All-FFis the invalid marker.Verified against both the reference encoder (
Firmware/src/sensor_sht40.cpp:193-213) and the browser client decoder (opendisplay.org httpdocs/js/opendisplay-msd.js:19-34decodeSht40Three). The battery/temp/status bytes round-trip through the Python client decoderpy-opendisplay src/opendisplay/models/advertisement.py:380-391.F4 29 0AE8 03 0000 40 06FF FF FFThe default start byte is 7 (never 0), so the 0.0 C/0.0 % case encodes as
00 40 06— not all-zero — sidestepping the known website gotcha where a literal0start byte decodes as -40 C.BQ27220 dynamic byte
One byte at
msd_data_start_byte(0xFF= skip):bit7 = charging,bits0-6 = SOC(0-100 %);0xFFwhen the gauge is unreadable or SOC is out of range. Voltage comes from register0x08(mV, LE) and, when the gauge reads a nonzero voltage, becomes the preferred battery-voltage source over the ADC (asdisplay_service.cpp:1218-1223). SOC is register0x2C. Charging state is read from the optionalcharge_state_pinGPIO withcharger_flagspolarity, ported verbatim fromcharger_gpio_charging().Decisions / deviations
main.cppinit/power-down of rails); it is not a battery/SOC MSD source —readBatteryVoltageUncachedonly consults BQ27220 then the ADC. So there is nothing telemetry-shaped to port. Left out deliberately.readChipTemperature,sd_temp_geton nRF52) and keeps SHT40 in the dynamic slot — it does not prefer SHT40 for byte 13, so this port doesn't either (parity). The existing boot-once cache is preserved because the nRF54L TEMP peripheral IRQ stops arriving once the Zephyr BLE controller is up and would hang the caller withK_FOREVER. The clean analog tosd_temp_getwould be MPSL's timeslot-mediatedmpsl_temperature_get(), but MPSL availability on this Seeed Zephyr build is unconfirmed and cannot be validated without hardware, so a periodic die-temp refresh is deliberately deferred rather than risk regressing the documented boot hang.{configured, 0x44, 0x45}(the reference's 2-pass Wire re-init dance is an Arduino-Wire quirk; the bit-bang bus is re-initialized per transfer).Self-review
Notes for reviewers
feat/ci, so until ci: build both nRF54 environments on push and PR #1 merges this PR's diff includes the CI workflow. Review the sensor commit on top.opendisplay_ble.c(and feat: add GT911 touch controller support (config packet 0x28) #5 adds its own bit-banged I2C master). Expect textual conflicts on those lines on rebase; the I2C helper here is standalone so a later rebase can unify it with the touch branch's master.🤖 Generated with Claude Code
https://claude.ai/code/session_012g2e8mr132vcizx92WsgiR