Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/display_service.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "buzzer_control.h"
#include "sensor_sht40.h"
#include "sensor_bq27220.h"
#include "sensor_axp2101.h"
#include "communication.h"
#include "encryption.h"
#include "boot_screen.h"
Expand Down Expand Up @@ -1767,6 +1768,10 @@ static float readBatteryVoltageUncached() {
return gaugeV;
}
}
const float pmicV = axp2101BatteryVoltageVolts(globalConfig.sensors, globalConfig.sensor_count);
if (pmicV >= 0.0f) {
return pmicV;
}
if (globalConfig.power_option.battery_sense_pin == 0xFF) return -1.0;
uint8_t sensePin = globalConfig.power_option.battery_sense_pin;
uint8_t enablePin = globalConfig.power_option.battery_sense_enable_pin;
Expand Down
108 changes: 108 additions & 0 deletions src/sensor_axp2101.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#include "sensor_axp2101.h"

#include "structs.h"
#include "display_service.h"
#include "od_log.h"

#include <Arduino.h>
#include <Wire.h>

static const SensorData* axp2101_config(const SensorData* sensors, uint8_t count) {
if (sensors == nullptr) {
return nullptr;
}
for (uint8_t i = 0; i < count; i++) {
if (sensors[i].sensor_type == OD_SENSOR_TYPE_AXP2101) {
return &sensors[i];
}
}
return nullptr;
}

static uint8_t axp2101_bus_id(const SensorData* s) {
uint8_t bid = s->bus_id;
if (bid == 0xFF) {
bid = 0;
}
return bid;
}

// Register block read: write `reg`, then requestFrom `len` bytes into the
// caller-supplied `out`. Returns false on any transaction failure or short read.
// Allocates nothing. Mirrors bq27220_read_block() in sensor_bq27220.cpp.
static bool axp2101_read_block(uint8_t addr, uint8_t reg, uint8_t* out, uint8_t len) {
Wire.beginTransmission(addr);
Wire.write(reg);
if (Wire.endTransmission(false) != 0) {
return false;
}
if (Wire.requestFrom(addr, (size_t)len, true) != len) {
return false;
}
for (uint8_t i = 0; i < len; i++) {
out[i] = Wire.read();
}
return true;
}

static bool axp2101_write_reg(uint8_t addr, uint8_t reg, uint8_t value) {
Wire.beginTransmission(addr);
Wire.write(reg);
Wire.write(value);
return Wire.endTransmission() == 0;
}

float axp2101BatteryVoltageVolts(const SensorData* sensors, uint8_t sensor_count) {
const SensorData* s = axp2101_config(sensors, sensor_count);
if (s == nullptr) {
return -1.0f;
}
const uint8_t bus = axp2101_bus_id(s);
if (!initOrRestoreWireForBus(bus)) {
od_log_warn("AXP2101: bus %u init failed", bus);
return -1.0f;
}
const uint8_t addr = axp2101_resolve_addr(s->i2c_addr_7bit);
od_log_debug("AXP2101: addr=0x%02X bus=%u", addr, bus);

uint8_t status = 0;
if (!axp2101_read_block(addr, AXP2101_REG_POWER_STATUS_ADDR, &status, 1)) {
od_log_warn("AXP2101: power-status read failed @0x%02X", addr);
return -1.0f;
}
const bool batt_present = axp2101_batt_present(status);
const bool vbus_present = axp2101_vbus_present(status);
od_log_debug("AXP2101: power_status=0x%02X batt=%d vbus=%d",
status, (int)batt_present, (int)vbus_present);
if (!batt_present) {
od_log_debug("AXP2101: battery not present, returning -1");
return -1.0f;
}

uint8_t adc_ctrl = 0;
if (!axp2101_read_block(addr, AXP2101_REG_ADC_CHANNEL_CTRL_ADDR, &adc_ctrl, 1)) {
od_log_warn("AXP2101: ADC ctrl read failed @0x%02X", addr);
return -1.0f;
}
bool channel_changed = false;
const uint8_t adc_ctrl_new = axp2101_adc_enable_bit0(adc_ctrl, &channel_changed);
od_log_debug("AXP2101: adc_ctrl=0x%02X -> 0x%02X (changed=%d)",
adc_ctrl, adc_ctrl_new, (int)channel_changed);
if (channel_changed) {
if (!axp2101_write_reg(addr, AXP2101_REG_ADC_CHANNEL_CTRL_ADDR, adc_ctrl_new)) {
od_log_warn("AXP2101: ADC ctrl enable failed @0x%02X", addr);
return -1.0f;
}
delay(AXP2101_ADC_SETTLING_MS);
}

uint8_t vbat_raw[2] = {0, 0};
if (!axp2101_read_block(addr, AXP2101_REG_VBAT_H_ADDR, vbat_raw, 2)) {
od_log_warn("AXP2101: VBAT read failed @0x%02X", addr);
return -1.0f;
}
const uint16_t mv = axp2101_decode_vbat_mv(vbat_raw[0], vbat_raw[1]);
od_log_debug("AXP2101: VBAT raw=[0x%02X,0x%02X] -> %u mV (%.3fV)",
vbat_raw[0], vbat_raw[1], (unsigned)mv, (float)mv / 1000.0f);
return (float)mv / 1000.0f;
}
108 changes: 108 additions & 0 deletions src/sensor_axp2101.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// AXP2101 battery-voltage reader for the normal OpenDisplay battery path.
//
// Sits alongside sensor_bq27220 in shape: the sensor-specific configuration
// lookup, address defaulting, bus restoration, and voltage getter all live
// here. display_service.cpp's readBatteryVoltageUncached() consults this file
// when the configured sensor set contains an AXP2101 (sensor type 3), which is
// the case for the Waveshare ESP32-S3-PhotoPainter esp32-s3-wspp preset.
//
// The register decoding and bit manipulation are kept as pure `static inline`
// helpers here so tools/test_sensor_axp2101.cpp can exercise them without a
// PlatformIO build. See tools/README.md.
#ifndef SENSOR_AXP2101_H
#define SENSOR_AXP2101_H

#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>

// AXP2101 register layout used by the battery-voltage path. display_service.cpp
// carries its own AXP2101_* constants for the PMIC init and shutdown block, and
// four values appear in both places: the 0x34 slave address, 0x00 power status,
// 0x30 ADC channel control, and 0x34 VBAT high. The duplication is deliberate --
// those belong to that block, and the pure helpers below must compile as a
// standalone unit for the host test.
#define AXP2101_DEFAULT_ADDR_7BIT 0x34u
#define AXP2101_REG_POWER_STATUS_ADDR 0x00u
#define AXP2101_REG_ADC_CHANNEL_CTRL_ADDR 0x30u
#define AXP2101_REG_VBAT_H_ADDR 0x34u // VBAT high byte; read as 2-byte block with 0x35

// Datasheet reg 0x00: bit 3 = battery presence, bit 5 = VBUS presence.
// The uncalled readAXP2101Data() diagnostic in display_service.cpp reads these
// the other way round (0x20 for battery, 0x08 for VBUS). That is a pre-existing
// defect in unreachable code, flagged in the pull request and left unchanged.
#define AXP2101_POWER_STATUS_BATT_PRESENT_BIT (1u << 3)
#define AXP2101_POWER_STATUS_VBUS_PRESENT_BIT (1u << 5)

// Datasheet reg 0x30: bit 0 enables the VBAT-voltage ADC channel. Every other
// bit is a different channel or an unrelated ADC control and must be preserved.
#define AXP2101_ADC_ENABLE_VBAT_BIT (1u << 0)

// Post-enable settling window before the first VBAT read is meaningful. Only
// applied when the caller has just flipped bit 0 from 0 to 1.
#define AXP2101_ADC_SETTLING_MS 2u

// Address defaulting: `0` and `0xFF` (SensorData.i2c_addr_7bit sentinel values,
// see include/opendisplay_structs.h) both mean "use the AXP2101 default 0x34".
// Any other value is respected as the configured 7-bit address.
static inline uint8_t axp2101_resolve_addr(uint8_t configured) {
if (configured == 0u || configured == 0xFFu) {
return AXP2101_DEFAULT_ADDR_7BIT;
}
return configured;
}

// Battery-present interpretation of the power-status register.
static inline bool axp2101_batt_present(uint8_t status_reg) {
return (status_reg & AXP2101_POWER_STATUS_BATT_PRESENT_BIT) != 0u;
}

// VBUS-present interpretation of the power-status register. Reported in the
// battery reader's debug line; it does not affect the voltage returned.
static inline bool axp2101_vbus_present(uint8_t status_reg) {
return (status_reg & AXP2101_POWER_STATUS_VBUS_PRESENT_BIT) != 0u;
}

// Returns the value to write back to reg 0x30 so bit 0 is set, with every other
// bit preserved. `out_channel_changed` receives `true` iff the caller has just
// flipped the channel from disabled to enabled -- the only case where the
// caller must allow AXP2101_ADC_SETTLING_MS before the first VBAT sample.
static inline uint8_t axp2101_adc_enable_bit0(uint8_t current, bool* out_channel_changed) {
const bool was_off = (current & AXP2101_ADC_ENABLE_VBAT_BIT) == 0u;
if (out_channel_changed != NULL) {
*out_channel_changed = was_off;
}
return (uint8_t)(current | AXP2101_ADC_ENABLE_VBAT_BIT);
}

// VBAT decoding. Reg 0x34 holds the six valid high bits (bits 7:6 are unused
// and must be masked), reg 0x35 holds the full low byte. Result is millivolts
// at 1 mV per count.
static inline uint16_t axp2101_decode_vbat_mv(uint8_t hi, uint8_t lo) {
const uint16_t high_bits = (uint16_t)(hi & 0x3Fu);
return (uint16_t)((high_bits << 8) | (uint16_t)lo);
}

struct SensorData;

#ifdef __cplusplus
extern "C" {
#endif

// Reads the current battery voltage in volts through the OpenDisplay bus
// abstraction. The caller passes the parsed sensor set rather than this module
// reaching for a global (see the no-extern rule in CLAUDE.md); the AXP2101 entry
// is located here so the lookup stays with the sensor-specific code, matching
// sensor_bq27220.
//
// Returns `-1.0f` on any failure -- no AXP2101 in `sensors`, bus init failed,
// PMIC not answering, short read, or battery absent per reg 0x00 -- so the
// caller can fall through to its next battery source. Never allocates. Emits an
// od_log_warn on transaction failure.
float axp2101BatteryVoltageVolts(const struct SensorData* sensors, uint8_t sensor_count);

#ifdef __cplusplus
}
#endif

#endif // SENSOR_AXP2101_H
4 changes: 4 additions & 0 deletions tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ firmware's config layout.
boards that flash via USB mass storage.
- `test_zlib_stream.c` — standalone test harness for the firmware's streaming
zlib/uzlib decoder (`lib/uzlib`). Not part of the PlatformIO build.
- `test_sensor_axp2101.cpp` — standalone host test for the AXP2101 battery-
voltage helpers in `src/sensor_axp2101.h` (VBAT decoding, targeted ADC-enable,
address defaulting, and I2C-failure fall-through). Not part of the PlatformIO
build. See the header comment for the exact build line.
- `ble_crypto.py` / `config_packet.py` — shared helpers. `ble_crypto.py` is
inlined into `od-device-cli.py` and kept here for reference. `config_packet.py`
is imported by `provision_firmware.py`.
Expand Down
Loading
Loading