diff --git a/boards/teco/openearable_v2/board_init.c b/boards/teco/openearable_v2/board_init.c index a3ce26f5..b3762f87 100644 --- a/boards/teco/openearable_v2/board_init.c +++ b/boards/teco/openearable_v2/board_init.c @@ -1,21 +1,12 @@ #include #include #include -#include // ✅ Correct Power Management API #include LOG_MODULE_REGISTER(board_init, LOG_LEVEL_DBG); -//#include "nrf5340_audio_common.h" - #include -#include -#include - -#define load_switch_sd_id DT_NODELABEL(load_switch_sd) -#define load_switch_1_8_id DT_NODELABEL(load_switch) -#define load_switch_3_3_id DT_CHILD(DT_NODELABEL(bq25120a), load_switch) -//#define load_switch_3_3_id DT_NODELABEL(lsctrl) +#include "openearable_common.h" const struct device *const cons = DEVICE_DT_GET(DT_CHOSEN(zephyr_console)); const struct device *const ls_1_8 = DEVICE_DT_GET(load_switch_1_8_id); diff --git a/dts/bindings/load-switch.yaml b/boards/teco/openearable_v2/dts/bindings/load-switch.yaml similarity index 94% rename from dts/bindings/load-switch.yaml rename to boards/teco/openearable_v2/dts/bindings/load-switch.yaml index 8c4fbd49..3db78f42 100644 --- a/dts/bindings/load-switch.yaml +++ b/boards/teco/openearable_v2/dts/bindings/load-switch.yaml @@ -22,3 +22,5 @@ properties: default: 1000 description: | Delay in microseconds before enabling the load switch. + "#power-domain-cells": + const: 0 diff --git a/boards/teco/openearable_v2/mcuboot_hook.c b/boards/teco/openearable_v2/mcuboot_hook.c index dfee09f5..07f1683c 100644 --- a/boards/teco/openearable_v2/mcuboot_hook.c +++ b/boards/teco/openearable_v2/mcuboot_hook.c @@ -2,7 +2,6 @@ #include #include -//#include "bootutil/bootutil.h" #include "bootutil/boot_hooks.h" #include "bootutil/mcuboot_status.h" @@ -22,44 +21,72 @@ void mcuboot_status_change(mcuboot_status_type_t status) { static bool led_initialized = false; - // Initialisiere LED beim ersten Aufruf + // Initialize LED on first call if (!led_initialized) { if (device_is_ready(led.port)) { gpio_pin_configure_dt(&led, GPIO_OUTPUT_INACTIVE); led_initialized = true; } + k_timer_init(&blink_timer, blink_timer_handler, NULL); } + /* + * The printk() calls below are kept (commented out) for future debugging. + * + * NOTE ON MCUBOOT CONSOLE LOGGING (RTT & USB CDC ACM): + * MCUboot executes extremely fast. By default, you will NOT see printk logs + * in RTT or USB UART, because of a timing race condition with the host PC: + * + * 1. RTT: MCUboot and the App have separate RTT control blocks in RAM. The + * J-Link Viewer searches RAM for the block, but MCUboot usually jumps to + * the App before J-Link can find and connect to MCUboot's RTT block. + * 2. USB UART (CDC ACM): Enumerable USB takes ~1-2 seconds. MCUboot finishes + * executing long before the PC creates /dev/ttyACM0. + * + * HOW TO VIEW THESE PRINTS: + * - RTT / USB CDC ACM: You must configure the backend in sysbuild/mcuboot/prj.conf, + * AND you must add a blocking delay (e.g., k_msleep(3000);) right here at + * the STARTUP case to give your PC/Viewer time to connect before MCUboot exits. + * - Hardware UART: The most reliable method. Route CONFIG_UART_CONSOLE=y to + * physical TTL pins. It is stateless and does not require artificial delays. + */ + // printk("mcuboot_status_change: %d\n", status); + switch (status) { case MCUBOOT_STATUS_STARTUP: - //state_indicator.set_state(STARTUP); + // printk(" STARTUP\n"); + k_timer_start(&blink_timer, K_MSEC(50), K_MSEC(50)); break; case MCUBOOT_STATUS_UPGRADING: - k_timer_init(&blink_timer, blink_timer_handler, NULL); + // printk(" UPGRADING\n"); k_timer_start(&blink_timer, K_MSEC(250), K_MSEC(250)); break; case MCUBOOT_STATUS_BOOTABLE_IMAGE_FOUND: + // printk(" BOOTABLE_IMAGE_FOUND\n"); k_timer_stop(&blink_timer); gpio_pin_set_dt(&led, 0); break; case MCUBOOT_STATUS_NO_BOOTABLE_IMAGE_FOUND: + // printk(" NO_BOOTABLE_IMAGE_FOUND\n"); k_timer_stop(&blink_timer); gpio_pin_set_dt(&led, 1); break; case MCUBOOT_STATUS_BOOT_FAILED: - //state_indicator.set_state(BOOT_FAILED); + // printk(" BOOT_FAILED\n"); + k_timer_stop(&blink_timer); + gpio_pin_set_dt(&led, 1); break; case MCUBOOT_STATUS_USB_DFU_WAITING: - //state_indicator.set_state(USB_DFU_WAITING); + // printk(" USB_DFU_WAITING\n"); break; case MCUBOOT_STATUS_USB_DFU_ENTERED: - //state_indicator.set_state(USB_DFU_ENTERED); + // printk(" USB_DFU_ENTERED\n"); break; case MCUBOOT_STATUS_USB_DFU_TIMED_OUT: - //state_indicator.set_state(USB_DFU_TIMED_OUT); + // printk(" USB_DFU_TIMED_OUT\n"); break; case MCUBOOT_STATUS_SERIAL_DFU_ENTERED: - //state_indicator.set_state(SERIAL_DFU_ENTERED); + // printk(" SERIAL_DFU_ENTERED\n"); break; } } @@ -67,18 +94,21 @@ void mcuboot_status_change(mcuboot_status_type_t status) int init_load_switch() { int ret; + + // V_LS 1.8v is required for flash. static const struct gpio_dt_spec load_switch_pin = { .port = DEVICE_DT_GET(DT_NODELABEL(gpio1)), .pin = 11, .dt_flags = GPIO_ACTIVE_HIGH }; + // LS 3.3v is required for the Error LED. static const struct gpio_dt_spec ls_3_3_pin = { .port = DEVICE_DT_GET(DT_NODELABEL(gpio0)), .pin = 14, .dt_flags = GPIO_ACTIVE_HIGH }; - + ret = device_is_ready(load_switch_pin.port); if (!ret) { printk("Pins not ready.\n"); @@ -87,26 +117,23 @@ int init_load_switch() ret = gpio_pin_configure_dt(&load_switch_pin, GPIO_OUTPUT_ACTIVE); if (ret != 0) { - printk("Failed to setup Load Switch.\n"); + printk("Failed to setup 1.8V load switch.\n"); return ret; } + ret = device_is_ready(ls_3_3_pin.port); + if (!ret) { + printk("Pins not ready.\n"); + return -1; + } + ret = gpio_pin_configure_dt(&ls_3_3_pin, GPIO_OUTPUT_ACTIVE); if (ret != 0) { - printk("Failed to setup 3.3V.\n"); + printk("Failed to setup 3.3V load switch.\n"); return ret; } return 0; } -SYS_INIT(init_load_switch, PRE_KERNEL_2, 80); - -/* -int wait() { - k_msleep(1); - - return 0; -} - -SYS_INIT(wait, POST_KERNEL, 80);*/ \ No newline at end of file +SYS_INIT(init_load_switch, PRE_KERNEL_2, 80); \ No newline at end of file diff --git a/boards/teco/openearable_v2/openearable_v2_nrf5340_cpuapp_common.dts b/boards/teco/openearable_v2/openearable_v2_nrf5340_cpuapp_common.dts index d3796b69..77c97f07 100644 --- a/boards/teco/openearable_v2/openearable_v2_nrf5340_cpuapp_common.dts +++ b/boards/teco/openearable_v2/openearable_v2_nrf5340_cpuapp_common.dts @@ -30,12 +30,13 @@ status = "okay"; default-on; }; - + load_switch_sd: load_switch_sd { compatible = "load-switch"; enable-gpios = <&gpio1 12 GPIO_ACTIVE_HIGH>; power-delay-us = <300>; // 250us per datasheet status = "okay"; + default-on; }; gpio_fwd: nrf-gpio-forwarder { @@ -119,15 +120,15 @@ load-switch { compatible = "load-switch"; enable-gpios = <&gpio0 14 GPIO_ACTIVE_HIGH>; - power-delay-us = <600>; // 250us per datasheet + power-delay-us = <600>; status = "okay"; + zephyr,pm-device-runtime-auto; }; }; - - adau1860: adau1860@64 { - compatible = "analog,adau1860"; - reg = <0x64>; - enable-gpios = <&gpio0 4 GPIO_ACTIVE_HIGH>; + + ktd2026: ktd2026@30 { + compatible = "i2c-device"; + reg = <0x30>; }; }; @@ -146,12 +147,13 @@ reg = <0x62>; }; - mlx90632: mlx90632@3A { - compatible = "i2c-device"; - reg = <0x3A>; - }; + adau1860: adau1860@64 { + compatible = "adi,adau1860"; + reg = <0x64>; + enable-gpios = <&gpio0 4 GPIO_ACTIVE_HIGH>; + }; }; - + &i2c3 { compatible = "nordic,nrf-twim"; status = "okay"; @@ -159,8 +161,8 @@ pinctrl-1 = <&i2c3_sleep>; pinctrl-names = "default", "sleep"; clock-frequency = ; // 1000 kHz - zephyr,concat-buf-size = <512>; - zephyr,flash-buf-max-size = <512>; + zephyr,concat-buf-size = <1088>; + zephyr,flash-buf-max-size = <1088>; bmp388: bmp388@76 { compatible = "i2c-device"; @@ -175,6 +177,11 @@ bmx160: bmx160@68 { compatible = "i2c-device"; reg = <0x68>; + }; + + mlx90632: mlx90632@3a { + compatible = "i2c-device"; + reg = <0x3a>; }; }; diff --git a/src/Battery/BQ25120a.cpp b/src/Battery/BQ25120a.cpp index 71da49d9..b1c4c1c4 100644 --- a/src/Battery/BQ25120a.cpp +++ b/src/Battery/BQ25120a.cpp @@ -7,8 +7,20 @@ LOG_MODULE_REGISTER(bq25120a, LOG_LEVEL_DBG); BQ25120a battery_controller(&I2C1); -BQ25120a::BQ25120a(TWIM * i2c) : _i2c(i2c) { //, load_switch(LoadSwitch(GPIO_DT_SPEC_GET(DT_NODELABEL(bq25120a), lsctrl_gpios))) { +BQ25120a::BQ25120a(TWIM * i2c) : _i2c(i2c) { + k_mutex_init(&active_mutex); +} + +BQ25120a::ActiveScope::ActiveScope(BQ25120a &c) : c_(c) { + k_mutex_lock(&c_.active_mutex, K_FOREVER); + if (c_.active_count++ == 0) c_.exit_high_impedance(); + k_mutex_unlock(&c_.active_mutex); +} +BQ25120a::ActiveScope::~ActiveScope() { + k_mutex_lock(&c_.active_mutex, K_FOREVER); + if (--c_.active_count == 0) c_.enter_high_impedance(); + k_mutex_unlock(&c_.active_mutex); } int BQ25120a::begin() { @@ -68,27 +80,31 @@ int BQ25120a::reset() { } int BQ25120a::set_wakeup_int() { - int ret; - - ret = device_is_ready(pg_pin.port); //bool - if (!ret) { + if (!device_is_ready(pg_pin.port)) { LOG_ERR("BQ25120a pins not ready.\n"); return -1; } - ret = gpio_pin_interrupt_configure_dt(&pg_pin, GPIO_INT_LEVEL_ACTIVE); - if (ret != 0) { - LOG_ERR("Failed to setup interrupt on PG.\n"); - return ret; + // Arm two System-OFF wake sources from the BQ25120A: + // PG — wake-on-plug (VBUS present) + // INT — wake-on-button. The BQ25120A signals a pushbutton WAKE_1/ + // WAKE_2 event via its INT pin; without this armed, button + // hold only wakes the SoC via the BQ25120A's *RESET* output + // (much longer timer, ~12 s), not the fast ~4 s WAKE path. + // INT also pulses on faults (UVLO / OV / TS); those are re-entered + // into power_down() in begin() if the user didn't actually press + // the button, so spurious wakes just return to System OFF. + int pg_ret = gpio_pin_interrupt_configure_dt(&pg_pin, GPIO_INT_LEVEL_ACTIVE); + if (pg_ret != 0) { + LOG_ERR("Failed to setup interrupt on PG: %d.\n", pg_ret); } - ret = gpio_pin_interrupt_configure_dt(&int_pin, GPIO_INT_LEVEL_ACTIVE); - if (ret != 0) { - LOG_ERR("Failed to setup interrupt on INT.\n"); - return ret; + int int_ret = gpio_pin_interrupt_configure_dt(&int_pin, GPIO_INT_LEVEL_ACTIVE); + if (int_ret != 0) { + LOG_ERR("Failed to setup interrupt on INT: %d.\n", int_ret); } - return 0; + return pg_ret | int_ret; } bool BQ25120a::readReg(uint8_t reg, uint8_t * buffer, uint16_t len) { @@ -101,7 +117,7 @@ bool BQ25120a::readReg(uint8_t reg, uint8_t * buffer, uint16_t len) { if (delay > 0) k_usleep(delay); - _i2c->aquire(); + _i2c->acquire(); ret = i2c_burst_read(_i2c->master, address, reg, buffer, len); if (ret) LOG_WRN("I2C read failed: %d\n", ret); @@ -123,7 +139,7 @@ void BQ25120a::writeReg(uint8_t reg, uint8_t *buffer, uint16_t len) { if (delay > 0) k_usleep(delay); //TODO: assert no message ? - _i2c->aquire(); + _i2c->acquire(); ret = i2c_burst_write(_i2c->master, address, reg, buffer, len); if (ret) LOG_WRN("I2C write failed: %d", ret); @@ -138,9 +154,8 @@ void BQ25120a::setup(const battery_settings &_battery_settings) { params.lim_mA = _battery_settings.i_max; params.uvlo_v = _battery_settings.u_vlo; - exit_high_impedance(); + ActiveScope active(*this); - //reset(); setup_ts_control(); write_battery_voltage_control(_battery_settings.u_term); write_charging_control(_battery_settings.i_charge); @@ -148,17 +163,58 @@ void BQ25120a::setup(const battery_settings &_battery_settings) { write_LDO_voltage_control(3.3); write_uvlo_ilim(params); - enter_high_impedance(); + // Push-button Control (register 0x08, datasheet Table 21). Reset + // default is 0110_10xx: MRWAKE1=0 (80 ms), MRWAKE2=1 (1500 ms), + // MRREC=1 (device enters Hi-Z after RESET, not Ship Mode), + // MRRESET=01 (9 s hold-to-reset), PGB_MR=0. + // Only change: MRRESET 01→00 (9 s → 5 s) so the power-on hold time + // feels closer to the old firmware's ~4 s. Bottom two bits are + // read-only WAKE1/WAKE2 status; write 0 there. + uint8_t btn_ctrl = (0 << 7) // MRWAKE1 = 0 (80 ms) + | (1 << 6) // MRWAKE2 = 1 (1500 ms) + | (1 << 5) // MRREC = 1 (Hi-Z after RESET) + | (0 << 4) // MRRESET_1 + | (0 << 3) // MRRESET_0 (MRRESET = 00 → 5 s) + | (0 << 2); // PGB_MR = 0 + writeReg(registers::BTN_CTRL, &btn_ctrl, sizeof(btn_ctrl)); +} + +void BQ25120a::enter_ship_mode() { + // Ship Mode entry per datasheet §9.3.1.1 Figure 15: VIN < VUVLO AND + // CD high AND MR high must all hold through tQUIET for the chip to + // actually latch off. Caller guarantees VIN absent and MR high; + // we drive CD high here and issue the I2C write. + // + // Deliberately does NOT wrap in ActiveScope — ActiveScope's destructor + // would drop CD low after the write, which per Figure 15 can abort + // the transition. After this function, the chip disables its BAT FET + // within tQUIET; SYS collapses and the nRF5340 loses power. Do not + // touch the chip (or expect any code to run reliably) after this. + // + // Register 0x00 bit 5 is EN_SHIPMODE (write-only). Other bits in the + // byte are read-only status (STAT, CD_STAT, VINDPM_STAT, etc.), so + // writing 0x20 is fine — only EN_SHIPMODE is affected. + exit_high_impedance(); + uint8_t val = 1 << 5; // EN_SHIPMODE + writeReg(registers::CTRL, &val, sizeof(val)); } uint8_t BQ25120a::read_charging_state() { + ActiveScope active(*this); + uint8_t status = 0; bool ret = readReg(registers::CTRL, (uint8_t *) &status, sizeof(status)); return status; } +BQ25120a::ChargePhase BQ25120a::read_charge_phase() { + return static_cast(read_charging_state() >> 6); +} + uint8_t BQ25120a::read_fault() { + ActiveScope active(*this); + uint8_t status = 0; bool ret = readReg(registers::FAULT, (uint8_t *) &status, sizeof(status)); @@ -166,6 +222,8 @@ uint8_t BQ25120a::read_fault() { } uint8_t BQ25120a::read_ts_fault() { + ActiveScope active(*this); + uint8_t status = 0; bool ret = readReg(registers::TS_FAULT, (uint8_t *) &status, sizeof(status)); @@ -173,6 +231,8 @@ uint8_t BQ25120a::read_ts_fault() { } chrg_state BQ25120a::read_charging_control() { + ActiveScope active(*this); + uint8_t status = 0; bool ret = readReg(registers::CHARGE_CTRL, (uint8_t *) &status, sizeof(status)); @@ -199,6 +259,8 @@ chrg_state BQ25120a::read_charging_control() { uint8_t BQ25120a::write_charging_control(float mA) { + ActiveScope active(*this); + uint8_t status = 0; bool ret = readReg(registers::CHARGE_CTRL, &status, sizeof(status)); @@ -220,6 +282,8 @@ uint8_t BQ25120a::write_charging_control(float mA) { uint8_t BQ25120a::write_LS_control(bool enable) { + ActiveScope active(*this); + uint8_t status = 0; readReg(registers::LS_LDO_CTRL, &status, sizeof(status)); @@ -241,19 +305,40 @@ uint8_t BQ25120a::write_LDO_voltage_control(float volt) { volt = CLAMP(volt, 0.8f, 3.3f); - readReg(registers::LS_LDO_CTRL, &status, sizeof(status)); + // Per datasheet: "The LS/LDO output can only be changed when the + // EN_LS_LDO and LSCTRL pin has disabled the output." + // Must disable BOTH before changing voltage bits. - //status |= (((uint16_t)((volt - 0.8) * 10)) & 0x1F) << 2; - status &= 1 << 7; - status |= ((uint8_t)((volt - 0.8f) * 10 + EPS)) << 2; - //status |= 1 << 7; + // 1. Disable EN_LS_LDO + status = 0; + readReg(registers::LS_LDO_CTRL, &status, sizeof(status)); + status &= ~(1 << 7); + writeReg(registers::LS_LDO_CTRL, &status, sizeof(status)); + // 2. Pull LSCTRL low + const struct gpio_dt_spec lsctrl = { + .port = DEVICE_DT_GET(DT_NODELABEL(gpio0)), + .pin = 14, + .dt_flags = GPIO_ACTIVE_HIGH + }; + gpio_pin_set_dt(&lsctrl, 0); + k_usleep(100); + + // 3. Now write voltage + EN_LS_LDO + status = ((uint8_t)((volt - 0.8f) * 10 + EPS)) << 2; + status |= 1 << 7; writeReg(registers::LS_LDO_CTRL, &status, sizeof(status)); + // 4. Re-enable LSCTRL + gpio_pin_set_dt(&lsctrl, 1); + k_usleep(600); // power-delay-us from DTS + return status; } float BQ25120a::read_ldo_voltage() { + ActiveScope active(*this); + uint8_t status = 0; bool ret = readReg(registers::LS_LDO_CTRL, (uint8_t *) &status, sizeof(status)); @@ -262,7 +347,17 @@ float BQ25120a::read_ldo_voltage() { return voltage; } +uint8_t BQ25120a::read_ls_ldo_ctrl_raw() { + ActiveScope active(*this); + + uint8_t status = 0; + readReg(registers::LS_LDO_CTRL, &status, sizeof(status)); + return status; +} + float BQ25120a::read_battery_voltage_control() { + ActiveScope active(*this); + uint8_t status = 0; bool ret = readReg(registers::BAT_VOL_CTRL, (uint8_t *) &status, sizeof(status)); @@ -287,17 +382,15 @@ uint8_t BQ25120a::write_battery_voltage_control(float volt) { } chrg_state BQ25120a::read_termination_control() { + ActiveScope active(*this); + uint8_t status = 0; bool ret = readReg(registers::TERM_CTRL, (uint8_t *) &status, sizeof(status)); struct chrg_state chrg; - // if (!ret) printk("failed to read\n"); - chrg.enabled = status & 0x2; - //chrg.high_impedance = status & 0x1; - // charger disabled if (!chrg.enabled) return chrg; float mAh = (status & 0x7F) >> 2; @@ -316,9 +409,6 @@ chrg_state BQ25120a::read_termination_control() { uint8_t BQ25120a::write_termination_control(float mA, bool enable_termination) { uint8_t status = 0; - //bool ret = readReg(registers::TERM_CTRL, &status, sizeof(status)); - //status &= 0x3; - if (mA >= 6) { if (mA > 37) mA = 37; status |= (((uint16_t)(mA - 6)) & 0x1F) << 2; @@ -338,13 +428,13 @@ uint8_t BQ25120a::write_termination_control(float mA, bool enable_termination) { } ilim_uvlo BQ25120a::read_uvlo_ilim() { + ActiveScope active(*this); + struct ilim_uvlo param; uint8_t status = 0; bool ret = readReg(registers::ILIM_UVLO, (uint8_t *) &status, sizeof(status)); - // if (!ret) printk("failed to read\n"); - param.uvlo_v = CLAMP(3.0f- 0.2f * ((status & 0x7) - 2), 2.2, 3.0); param.lim_mA = 50.f + 50.f * ((status >> 3) & 0x7); @@ -372,6 +462,8 @@ void BQ25120a::setup_ts_control() { } void BQ25120a::disable_ts() { + ActiveScope active(*this); + uint8_t ts_fault = read_ts_fault(); ts_fault &= ~(1 << 7); @@ -405,13 +497,13 @@ void BQ25120a::enable_charge() { button_state BQ25120a::read_button_state() { + ActiveScope active(*this); + struct button_state btn; uint8_t status = 0; bool ret = readReg(registers::BTN_CTRL, (uint8_t *) &status, sizeof(status)); - // if (!ret) printk("failed to read\n"); - btn.wake_1 = status & 0x2; btn.wake_2 = status & 0x1; diff --git a/src/Battery/BQ25120a.h b/src/Battery/BQ25120a.h index 38eb75f3..1539aad8 100644 --- a/src/Battery/BQ25120a.h +++ b/src/Battery/BQ25120a.h @@ -5,7 +5,6 @@ #include #include -//#include #include #include "openearable_common.h" @@ -45,6 +44,22 @@ class BQ25120a { ILIM_UVLO = 0x09 }; + enum class ChargePhase : uint8_t { + Discharge = 0, + Charging = 1, + Done = 2, + Fault = 3, + }; + + // Register 0x01 (Faults) bit layout per BQ25120A datasheet Table 13. + struct FaultBit { uint8_t mask; const char *name; }; + static constexpr FaultBit fault_bits[] = { + { 1 << 4, "BAT_OCP: battery over-current (cleared on read)" }, + { 1 << 5, "BAT_UVLO: battery under-voltage lockout (persistent)" }, + { 1 << 6, "VIN_UV: input under-voltage (cleared on read)" }, + { 1 << 7, "VIN_OV: input over-voltage (persistent)" }, + }; + BQ25120a(TWIM * i2c); int begin(); @@ -52,30 +67,29 @@ class BQ25120a { int reset(); - void setup_ts_control(); - void setup(const battery_settings &_battery_settings); bool power_connected(); - void enter_high_impedance(); - void exit_high_impedance(); + // Fire-and-die Ship Mode entry (datasheet §9.3.1.1, Figure 15). Caller + // MUST ensure VIN is absent and MR is high; this drives CD high and + // writes EN_SHIPMODE=1. SYS collapses within tQUIET and the SoC loses + // power — this call does not return to a usable state. + void enter_ship_mode(); void disable_charge(); void enable_charge(); - + uint8_t read_charging_state(); + ChargePhase read_charge_phase(); uint8_t read_fault(); uint8_t read_ts_fault(); chrg_state read_charging_control(); uint8_t write_charging_control(float mA); float read_battery_voltage_control(); - uint8_t write_battery_voltage_control(float volt); struct chrg_state read_termination_control(); - uint8_t write_termination_control(float mA, bool enable_termination = true); ilim_uvlo read_uvlo_ilim(); - uint8_t write_uvlo_ilim(ilim_uvlo param); void disable_ts(); - uint8_t write_LDO_voltage_control(float volt); float read_ldo_voltage(); + uint8_t read_ls_ldo_ctrl_raw(); uint8_t write_LS_control(bool enable); button_state read_button_state(); @@ -83,6 +97,33 @@ class BQ25120a { int set_power_connect_callback(gpio_callback_handler_t handler); int set_int_callback(gpio_callback_handler_t handler); private: + // RAII guard: brackets any I2C access with exit/enter high-impedance. + // Refcounted so nested scopes compose; mutex serialises concurrent + // thread/work-handler entries so the 0↔1 transition is atomic. Every + // public I2C method self-wraps in one of these, so external callers + // never need to touch it directly. + class ActiveScope { + public: + explicit ActiveScope(BQ25120a &c); + ~ActiveScope(); + ActiveScope(const ActiveScope&) = delete; + ActiveScope& operator=(const ActiveScope&) = delete; + private: + BQ25120a &c_; + }; + + // Hi-Z control. Driven exclusively by ActiveScope ctor/dtor. Only + // enter_ship_mode() pokes Hi-Z directly (see its comment for why). + void enter_high_impedance(); + void exit_high_impedance(); + + // Setup-only helpers. setup()'s ActiveScope covers all of them. + void setup_ts_control(); + uint8_t write_battery_voltage_control(float volt); + uint8_t write_termination_control(float mA, bool enable_termination = true); + uint8_t write_uvlo_ilim(ilim_uvlo param); + uint8_t write_LDO_voltage_control(float volt); + bool readReg(uint8_t reg, uint8_t * buffer, uint16_t len); void writeReg(uint8_t reg, uint8_t * buffer, uint16_t len); @@ -91,6 +132,9 @@ class BQ25120a { uint64_t last_i2c; uint64_t last_high_z; + struct k_mutex active_mutex; + uint32_t active_count = 0; + TWIM *_i2c; gpio_callback power_connect_cb_data; @@ -99,10 +143,6 @@ class BQ25120a { const struct gpio_dt_spec pg_pin = GPIO_DT_SPEC_GET(DT_NODELABEL(bq25120a), pg_gpios); const struct gpio_dt_spec cd_pin = GPIO_DT_SPEC_GET(DT_NODELABEL(bq25120a), cd_gpios); const struct gpio_dt_spec int_pin = GPIO_DT_SPEC_GET(DT_NODELABEL(bq25120a), int_gpios); - - /*const struct gpio_dt_spec pg_pin = GPIO_DT_SPEC_GET_OR(DT_NODELABEL(bq25120a), pg_gpios, {0}); - const struct gpio_dt_spec cd_pin = GPIO_DT_SPEC_GET_OR(DT_NODELABEL(bq25120a), cd_gpios, {0}); - const struct gpio_dt_spec int_pin = GPIO_DT_SPEC_GET_OR(DT_NODELABEL(bq25120a), int_gpios, {0});*/ }; extern BQ25120a battery_controller; diff --git a/src/Battery/BQ27220.cpp b/src/Battery/BQ27220.cpp index 715f22c1..38b49041 100644 --- a/src/Battery/BQ27220.cpp +++ b/src/Battery/BQ27220.cpp @@ -64,7 +64,7 @@ bool BQ27220::readReg(uint8_t reg, uint8_t * buffer, uint16_t len) { if (delay > 0) k_usleep(delay); - _i2c->aquire(); + _i2c->acquire(); ret = i2c_burst_read(_i2c->master, address, reg, buffer, len); if (ret) LOG_WRN("I2C read failed: %d\n", ret); @@ -83,7 +83,7 @@ void BQ27220::writeReg(uint8_t reg, uint8_t *buffer, uint16_t len) { if (delay > 0) k_usleep(delay); - _i2c->aquire(); + _i2c->acquire(); ret = i2c_burst_write(_i2c->master, address, reg, buffer, len); if (ret) LOG_WRN("I2C write failed: %d", ret); @@ -182,7 +182,7 @@ float BQ27220::state_of_health() { float BQ27220::current() { int16_t mA = 0; - bool ret = readReg(registers::NAC, (uint8_t *) &mA, sizeof(mA)); + bool ret = readReg(registers::CURR, (uint8_t *) &mA, sizeof(mA)); return mA; } diff --git a/src/Battery/BQ27220.h b/src/Battery/BQ27220.h index 2b97c6a2..fee669c8 100644 --- a/src/Battery/BQ27220.h +++ b/src/Battery/BQ27220.h @@ -5,7 +5,6 @@ #include #include -//#include #include #include "openearable_common.h" @@ -85,32 +84,32 @@ class BQ27220 { SEAL = 0x0030, }; + // Standard commands per BQ27220 TRM (SLUUBD4A) Table 2-1 enum registers : uint8_t { CTRL = 0x00, TEMP = 0x06, - INT_TEMP = 0x28, VOLT = 0x08, - AI = 0x14, FLAGS = 0x0A, + CURR = 0x0C, // Current() — instantaneous current, mA signed + RM = 0x10, // RemainingCapacity(), mAh + FCC = 0x12, // FullChargeCapacity(), mAh + AI = 0x14, // AverageCurrent(), mA signed TTE = 0x16, TTF = 0x18, + SI = 0x1A, // StandbyCurrent(), mA signed TTES = 0x1c, - TTECP= 0x26, - NAC = 0x0C, - FCC = 0x12, + RCC = 0x22, // RawCoulombCount(), mAh + AP = 0x24, // AveragePower(), mW signed + INT_TEMP = 0x28, CYCT = 0x2A, - AE = 0x22, SOC = 0x2C, SOH = 0x2E, + CC = 0x32, // ChargingCurrent(), mA + OP_STAT = 0x3A, DCAP = 0x3C, - AP = 0x24, - CC = 0x32, - SI = 0x1A, - RM = 0x10, DATA = 0x40, CHECK_SUM = 0x60, DATA_LEN = 0x61, - OP_STAT = 0x3A, }; BQ27220(TWIM * i2c); @@ -163,7 +162,6 @@ class BQ27220 { gpio_callback int_cb_data; - //const struct gpio_dt_spec gpout_pin = GPIO_DT_SPEC_GET(DT_NODELABEL(bq27220), gpout_gpios); const struct gpio_dt_spec gpout_pin = GPIO_DT_SPEC_GET_OR(DT_NODELABEL(bq27220), gpout_gpios, {0}); TWIM *_i2c; @@ -171,6 +169,4 @@ class BQ27220 { extern BQ27220 fuel_gauge; -//extern BQ27220 battery_gauge; - #endif \ No newline at end of file diff --git a/src/Battery/CMakeLists.txt b/src/Battery/CMakeLists.txt index a1c2dd11..c9a65efd 100644 --- a/src/Battery/CMakeLists.txt +++ b/src/Battery/CMakeLists.txt @@ -3,6 +3,5 @@ target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/BQ27220.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BQ25120a.cpp ${CMAKE_CURRENT_SOURCE_DIR}/PowerManager.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/PowerManager.c ${CMAKE_CURRENT_SOURCE_DIR}/BootState.c ) \ No newline at end of file diff --git a/src/Battery/PowerManager.c b/src/Battery/PowerManager.c deleted file mode 100644 index 873bc309..00000000 --- a/src/Battery/PowerManager.c +++ /dev/null @@ -1,32 +0,0 @@ -//#include "PowerManager.h" - -#include "openearable_common.h" - -#include -#include - -#include -LOG_MODULE_REGISTER(battery_pub, CONFIG_MODULE_BUTTON_HANDLER_LOG_LEVEL); - -struct load_switch_data { - struct gpio_dt_spec ctrl_pin; - bool default_on; -}; - -/*static int b_init(const struct device *dev) -{ - ARG_UNUSED(dev); - - struct load_switch_data *data_1_8 = dev->data; - - if(data_1_8->default_on) { - int ret = pm_device_action_run(ls_1_8, PM_DEVICE_ACTION_SUSPEND); - if (ret < 0) { - printk("Failed to suspend device: %d", ret); - } - } - - return 0; -} - -SYS_INIT(b_init, APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEVICE);*/ \ No newline at end of file diff --git a/src/Battery/PowerManager.cpp b/src/Battery/PowerManager.cpp index 628739da..1b7cb670 100644 --- a/src/Battery/PowerManager.cpp +++ b/src/Battery/PowerManager.cpp @@ -2,11 +2,13 @@ #include "macros_common.h" +#include #include #include #include #include +#include #include #include #include @@ -19,6 +21,7 @@ #endif #include +#include #include "../drivers/LED_Controller/KTD2026.h" #include "../drivers/ADAU1860.h" @@ -35,24 +38,19 @@ #include LOG_MODULE_REGISTER(power_manager, LOG_LEVEL_DBG); -//K_TIMER_DEFINE(PowerManager::charge_timer, PowerManager::charge_timer_handler, NULL); - K_WORK_DELAYABLE_DEFINE(PowerManager::charge_ctrl_delayable, PowerManager::charge_ctrl_work_handler); - K_WORK_DELAYABLE_DEFINE(PowerManager::power_down_work, PowerManager::power_down_work_handler); +K_WORK_DELAYABLE_DEFINE(PowerManager::power_button_watch_work, PowerManager::power_button_watch_handler); -//K_WORK_DEFINE(PowerManager::power_down_work, PowerManager::power_down_work_handler); -//K_WORK_DEFINE(PowerManager::charge_ctrl_work, PowerManager::charge_ctrl_work_handler); K_WORK_DEFINE(PowerManager::fuel_gauge_work, PowerManager::fuel_gauge_work_handler); K_WORK_DEFINE(PowerManager::battery_controller_work, PowerManager::battery_controller_work_handler); +K_WORK_DEFINE(PowerManager::usb_plug_reboot_work, PowerManager::usb_plug_reboot_handler); ZBUS_CHAN_DEFINE(battery_chan, struct battery_data, NULL, NULL, ZBUS_OBSERVERS_EMPTY, ZBUS_MSG_INIT(0)); static struct battery_data msg; -//LoadSwitch PowerManager::v1_8_switch(GPIO_DT_SPEC_GET(DT_NODELABEL(load_switch), gpios)); - void PowerManager::fuel_gauge_callback(const struct device *dev, struct gpio_callback *cb, uint32_t pins) { LOG_DBG("Fuel Gauge GPOUT Interrupt"); k_work_submit(&fuel_gauge_work); @@ -68,14 +66,23 @@ void PowerManager::power_good_callback(const struct device *dev, struct gpio_cal k_work_submit(&fuel_gauge_work); if (power_good) { - power_manager.last_charging_state = 0; + power_manager.charger_init_pending = true; k_work_schedule(&charge_ctrl_delayable, K_NO_WAIT); + // USB MSC can't come up cleanly against a running system — the SD / + // USB stacks race with whatever the app is doing. Take the reliable + // path: reboot so we come back up through the cold-boot-with-USB + // path, which is known to work end-to-end. + if (power_manager.power_on) k_work_submit(&usb_plug_reboot_work); } else { k_work_cancel_delayable(&charge_ctrl_delayable); if (!power_manager.power_on) k_work_reschedule(&power_manager.power_down_work, K_NO_WAIT); } } +void PowerManager::usb_plug_reboot_handler(struct k_work * work) { + power_manager.reboot(); +} + void PowerManager::power_down_work_handler(struct k_work * work) { power_manager.power_down(); } @@ -87,22 +94,75 @@ void PowerManager::charge_ctrl_work_handler(struct k_work * work) { } void PowerManager::battery_controller_work_handler(struct k_work * work) { - button_state state; + button_state state = battery_controller.read_button_state(); - //uint8_t val = gpio_pin_get_dt(&power_manager.error_led); - //gpio_pin_set_dt(&power_manager.error_led, 1 - val); - - battery_controller.exit_high_impedance(); - state = battery_controller.read_button_state(); - battery_controller.enter_high_impedance(); - - if (state.wake_2) { + // WAKE event = long-press threshold reached. Toggle power immediately so + // the user sees the state change while the button is still held. Events + // that latched from the power-on press itself are suppressed until the + // user has released the button at least once (first_release_seen). + if (state.wake_2 && power_manager.first_release_seen) { power_manager.power_on = !power_manager.power_on; - //LOG_INF("Power on: %i", power_manager.power_on); - if (!power_manager.power_on) power_manager.power_down(); } +} + +void PowerManager::power_button_watch_handler(struct k_work * work) { + if (earable_btn.getState() == BUTTON_RELEASED) { + power_manager.first_release_seen = true; + return; + } + k_work_reschedule(&power_button_watch_work, K_MSEC(100)); +} + +struct charging_snapshot { + BQ25120a::ChargePhase phase; + bat_status bat; + gauge_status gs; + uint8_t fault; + float voltage_v; + float current_ma; + float target_current_ma; + bool power_connected; +}; + +// Pure classifier: hardware snapshot + config → application-level charging_state. +// No I2C, no logging, no side effects. Unit-testable by construction. +static enum charging_state classify_charging(const charging_snapshot &s, + const battery_settings &cfg) { + switch (s.phase) { + case BQ25120a::ChargePhase::Discharge: + if (s.gs.edv1) return BATTERY_CRITICAL; +#ifdef CONFIG_BATTERY_ENABLE_LOW_STATE + if (s.gs.edv2) return BATTERY_LOW; +#endif + return DISCHARGING; + case BQ25120a::ChargePhase::Charging: + if (s.bat.SYSDWN) return PRECHARGING; + if (s.current_ma > 0.8f * s.target_current_ma - 2.0f * cfg.i_term) { + return CHARGING; + } + if (s.voltage_v > cfg.u_term - 0.02f) { +#ifdef CONFIG_BATTERY_ENABLE_TRICKLE_CHARGE + return TRICKLE_CHARGING; +#else + return CHARGING; +#endif + } + return POWER_CONNECTED; + + case BQ25120a::ChargePhase::Done: + return FULLY_CHARGED; + + case BQ25120a::ChargePhase::Fault: + // Undervoltage fault with power connected + current flowing = recovery. + if ((s.fault & (1 << 5)) && s.power_connected && + s.current_ma > 0.5f * cfg.i_term) { + return PRECHARGING; + } + return FAULT; + } + return DISCHARGING; } void PowerManager::fuel_gauge_work_handler(struct k_work * work) { @@ -111,142 +171,78 @@ void PowerManager::fuel_gauge_work_handler(struct k_work * work) { msg.battery_level = fuel_gauge.state_of_charge(); - bat_status bat = fuel_gauge.battery_status(); - power_manager.get_battery_status(status); - // full discharge - //if (bat.FD) k_work_reschedule(&power_manager.power_down_work, K_NO_WAIT); - if (power_manager.power_on && bat.SYSDWN) { + charging_snapshot snap; + snap.bat = fuel_gauge.battery_status(); + snap.gs = fuel_gauge.gauging_state(); + snap.voltage_v = fuel_gauge.voltage(); + snap.current_ma = fuel_gauge.current(); + snap.target_current_ma = fuel_gauge.charge_current(); + + if (power_manager.power_on && snap.bat.SYSDWN) { LOG_WRN("Battery reached system down voltage."); - k_work_reschedule(&power_manager.power_down_work, K_NO_WAIT); + power_manager.power_down(); + return; } - if (bat.CHGINH) { + if (snap.bat.CHGINH) { power_manager.charging_disabled = true; battery_controller.disable_charge(); } else if (power_manager.charging_disabled) { battery_controller.enable_charge(); } - float current; - float target_current; - float voltage; + uint8_t ts_fault = 0; + uint8_t ctrl_raw = battery_controller.read_charging_state(); + snap.phase = static_cast(ctrl_raw >> 6); + snap.power_connected = battery_controller.power_connected(); + if (snap.phase == BQ25120a::ChargePhase::Fault) { + snap.fault = battery_controller.read_fault(); + ts_fault = battery_controller.read_ts_fault(); + } else { + snap.fault = 0; + } - battery_controller.exit_high_impedance(); + msg.charging_state = classify_charging(snap, power_manager._battery_settings); - uint16_t charging_state = battery_controller.read_charging_state() >> 6; - gauge_status gs; - - switch (charging_state) { - case 0: + switch (snap.phase) { + case BQ25120a::ChargePhase::Discharge: LOG_INF("charging state: discharge"); - msg.charging_state = DISCHARGING; - - gs = fuel_gauge.gauging_state(); - - if (gs.edv2) { - #ifdef CONFIG_BATTERY_ENABLE_LOW_STATE - msg.charging_state = BATTERY_LOW; - #endif - } - if (gs.edv1) { - msg.charging_state = BATTERY_CRITICAL; - } break; - case 1: + case BQ25120a::ChargePhase::Charging: LOG_INF("charging state: charging"); - - if (bat.SYSDWN) { - msg.charging_state = PRECHARGING; - break; - } - - current = fuel_gauge.current(); - target_current = fuel_gauge.charge_current(); - voltage = fuel_gauge.voltage(); - - msg.charging_state = POWER_CONNECTED; - - LOG_DBG("Voltage: %.3f V", voltage); - LOG_DBG("Charging current: %.3f mA", current); - LOG_DBG("Target current: %.3f mA", target_current); + LOG_DBG("Voltage: %.3f V", snap.voltage_v); + LOG_DBG("Charging current: %.3f mA", snap.current_ma); + LOG_DBG("Target current: %.3f mA", snap.target_current_ma); LOG_DBG("State of charge: %.3f %%", fuel_gauge.state_of_charge()); - - // check if target current is met (if not tapering) - if (current > 0.8 * target_current - 2 * power_manager._battery_settings.i_term) { - msg.charging_state = CHARGING; - } - else if (voltage > power_manager._battery_settings.u_term - 0.02) { - #ifdef CONFIG_BATTERY_ENABLE_TRICKLE_CHARGE - msg.charging_state = TRICKLE_CHARGING; - #else - msg.charging_state = CHARGING; - #endif - } - break; - case 2: + case BQ25120a::ChargePhase::Done: LOG_INF("charging state: done"); - msg.charging_state = FULLY_CHARGED; break; - case 3: - LOG_WRN("charging state: fault"); - msg.charging_state = FAULT; - - uint8_t fault = battery_controller.read_fault(); - // Battery fuel gauge status - bat_status status = fuel_gauge.battery_status(); - voltage = fuel_gauge.voltage(); - current = fuel_gauge.current(); - - // cleared after read - if (fault & (1 << 4)) { - LOG_WRN("Input over voltage."); + case BQ25120a::ChargePhase::Fault: + LOG_WRN("charging state: fault (CTRL=0x%02x FAULT=0x%02x TS_FAULT=0x%02x)", + ctrl_raw, snap.fault, ts_fault); + for (const auto &b : BQ25120a::fault_bits) { + if (snap.fault & b.mask) LOG_WRN("%s", b.name); } - - // as long as fault exists - if (fault & (1 << 5)) { - bool power_connected = battery_controller.power_connected(); - if (power_connected && current > 0.5 * power_manager._battery_settings.i_term) { - msg.charging_state = PRECHARGING; - } - LOG_WRN("Battery under voltage: %.3f V", voltage); + if (snap.fault & (1 << 5)) { + LOG_WRN("Battery voltage at under-voltage fault: %.3f V", snap.voltage_v); } - - // cleared after read - if (fault & (1 << 6)) { - LOG_WRN("Input under voltage"); - } - - // as long as fault exists - if (fault & (1 << 7)) { - LOG_WRN("Battery over voltage"); - } - - uint8_t ts_fault = battery_controller.read_ts_fault(); - if ((ts_fault >> 5) & 0x7) { LOG_WRN("TS_ENABLED: %i, TS FAULT: %i", ts_fault >> 7, (ts_fault >> 5) & 0x3); - battery_controller.setup(power_manager._battery_settings); + power_manager.setup_pmic(); } - LOG_DBG("------------------ Battery Info ------------------"); LOG_DBG("Battery Status:"); - LOG_DBG(" Present: %d, Full Charge: %d, Full Discharge: %d", - status.BATTPRES, status.FC, status.FD); - - // Basic measurements + LOG_DBG(" Present: %d, Full Charge: %d, Full Discharge: %d", + snap.bat.BATTPRES, snap.bat.FC, snap.bat.FD); LOG_DBG("Basic Measurements:"); - LOG_DBG(" Voltage: %.3f V", voltage); - LOG_DBG(" Current: %.3f mA", current); + LOG_DBG(" Voltage: %.3f V", snap.voltage_v); + LOG_DBG(" Current: %.3f mA", snap.current_ma); break; } - battery_controller.enter_high_impedance(); - - power_manager.last_charging_msg_state = msg.charging_state; - // Adjust interval based on state if (msg.charging_state == FAULT || msg.charging_state == POWER_CONNECTED) { power_manager.chrg_interval = K_SECONDS(CONFIG_BATTERY_CHARGE_CONTROLLER_FAST_INTERVAL_SECONDS); @@ -254,11 +250,10 @@ void PowerManager::fuel_gauge_work_handler(struct k_work * work) { power_manager.chrg_interval = K_SECONDS(CONFIG_BATTERY_CHARGE_CONTROLLER_NORMAL_INTERVAL_SECONDS); } - //ret = k_msgq_put(&battery_queue, &msg, K_NO_WAIT); ret = zbus_chan_pub(&battery_chan, &msg, K_FOREVER); - if (ret) { - LOG_WRN("power manager msg queue full"); - } + if (ret) { + LOG_WRN("power manager msg queue full"); + } } int PowerManager::begin() { @@ -271,140 +266,101 @@ int PowerManager::begin() { fuel_gauge.begin(); earable_btn.begin(); - battery_controller.exit_high_impedance(); - uint8_t bat_state = battery_controller.read_charging_state(); - button_state btn = battery_controller.read_button_state(); - - power_on = btn.wake_2; - // get reset reason uint32_t reset_reas = NRF_RESET->RESETREAS; - // reset the reset reason - NRF_RESET->RESETREAS = 0xFFFFFFFF; - - if (reset_reas & RESET_RESETREAS_RESETPIN_Msk) { - oe_boot_state.timer_reset = bat_state & (1 << 4); - power_on |= oe_boot_state.timer_reset; + // Boot diagnostics: decode RESETREAS and snapshot PMIC fault / fuel-gauge + // status so spurious wakes from System OFF can be traced to a specific + // line (BQ25120A PG/INT or BQ27220 GPOUT). + { + uint8_t bq_fault = battery_controller.read_fault(); + uint8_t bq_ts_fault = battery_controller.read_ts_fault(); + bat_status fg = fuel_gauge.battery_status(); + LOG_WRN("boot: RESETREAS=0x%08x [%s%s%s%s%s%s%s%s%s%s%s]", + reset_reas, + (reset_reas & RESET_RESETREAS_RESETPIN_Msk) ? "PIN " : "", + (reset_reas & RESET_RESETREAS_DOG0_Msk) ? "DOG0 " : "", + (reset_reas & RESET_RESETREAS_CTRLAP_Msk) ? "CAP " : "", + (reset_reas & RESET_RESETREAS_SREQ_Msk) ? "SREQ " : "", + (reset_reas & RESET_RESETREAS_LOCKUP_Msk) ? "LOCK " : "", + (reset_reas & RESET_RESETREAS_OFF_Msk) ? "OFF " : "", + (reset_reas & RESET_RESETREAS_LPCOMP_Msk) ? "LPC " : "", + (reset_reas & RESET_RESETREAS_DIF_Msk) ? "DIF " : "", + (reset_reas & RESET_RESETREAS_NFC_Msk) ? "NFC " : "", + (reset_reas & RESET_RESETREAS_DOG1_Msk) ? "DOG1 " : "", + (reset_reas & RESET_RESETREAS_VBUS_Msk) ? "VBUS " : ""); + LOG_WRN("boot: BQ25120A fault=0x%02x ts_fault=0x%02x " + "BQ27220 DSG=%u SYSDWN=%u BATTPRES=%u FC=%u FD=%u " + "OTC=%u OTD=%u TCA=%u TDA=%u", + bq_fault, bq_ts_fault, + fg.DSG, fg.SYSDWN, fg.BATTPRES, fg.FC, fg.FD, + fg.OTC, fg.OTD, fg.TCA, fg.TDA); } - /*if (reset_reas & RESET_RESETREAS_DOG1_Msk) { - printk("Reset durch Watchdog-Timer\n"); - }*/ - - if (reset_reas & RESET_RESETREAS_SREQ_Msk) { - LOG_INF("Rebooting ..."); - power_on = true; + // Record the BQ25120A timer_reset bit on RESETPIN wake (long-hold button + // vs. programmer-driven nRESET) for downstream policy decisions. + if (reset_reas & RESET_RESETREAS_RESETPIN_Msk) { + oe_boot_state.timer_reset = bat_state & (1 << 4); } - /*if (reset_reas & RESET_RESETREAS_LOCKUP_Msk) { - printk("Reset durch CPU Lockup\n"); - }*/ + // reset the reset reason + NRF_RESET->RESETREAS = 0xFFFFFFFF; - battery_controller.setup(_battery_settings); + setup_pmic(); battery_controller.set_int_callback(battery_controller_callback); - // check setup op_state state = fuel_gauge.operation_state(); if (state.SEC != BQ27220::SEALED) { - //battery_controller.setup(); fuel_gauge.setup(_battery_settings); } - //k_timer_init(&charge_timer, charge_timer_handler, NULL); - - bool battery_condition = check_battery(); - - if (!battery_condition) LOG_WRN("Battery check failed."); - - // check charging state - bool charging = battery_controller.power_connected(); - - if (!battery_condition) { - power_on = false; - // LOG_ERR("Bad battery condition."); - if (!charging){ - //TODO: Flash red LED once + // If we woke from System OFF via a GPIO event (INT or PG), check if it was intended. + // INT also pulses on PMIC faults and timer resets. If the user isn't holding the + // button and power isn't connected, this is an undesired wake. + if (reset_reas & RESET_RESETREAS_OFF_Msk) { + if (!battery_controller.power_connected() && earable_btn.getState() == BUTTON_RELEASED) { + LOG_WRN("Undesired wake from System OFF detected. Powering down."); return power_down(false); } } - if (charging) { - power_manager.last_charging_state = 0; - - int ret = pm_device_runtime_enable(ls_1_8); - if (ret != 0) { - LOG_WRN("Error setting up load switch 1.8V."); - } - - ret = pm_device_runtime_enable(ls_3_3); - if (ret != 0) { - LOG_WRN("Error setting up load switch 3.3V."); - } - - //battery_level_status bat_status; - //get_battery_status(&bat_status); + // Battery health is the only gate on booting: if we can't run safely, + // power down and rely on the charger wake path to bring us back. + if (!check_battery()) { + LOG_WRN("Battery check failed — powering down."); + return power_down(false); + } - oe_state.charging_state = POWER_CONNECTED; + power_on = true; - state_indicator.init(oe_state); + // Start watching the button GPIO so a long-press + release toggles power. + // The first release consumes the power-on press so it doesn't immediately + // toggle us back off. + k_work_reschedule(&power_button_watch_work, K_MSEC(100)); + oe_state.charging_state = battery_controller.power_connected() ? POWER_CONNECTED + : DISCHARGING; + if (oe_state.charging_state == POWER_CONNECTED) { k_work_schedule(&charge_ctrl_delayable, K_NO_WAIT); - - while(!power_on && battery_controller.power_connected()) { - //__WFE(); - k_sleep(K_SECONDS(1)); - } - } else { - oe_state.charging_state = DISCHARGING; } - if (!power_on) return power_down(); - - //TODO: check power on condition - // either not charging and edv1 or charging and edv0 and temperature - battery_controller.set_power_connect_callback(power_good_callback); fuel_gauge.set_int_callback(fuel_gauge_callback); - //battery_controller.set_int_callback(battery_controller_callback); - - //float voltage = battery_controller.read_ldo_voltage(); - //if (voltage != 3.3) battery_controller.write_LDO_voltage_control(3.3); - - battery_controller.enter_high_impedance(); - - int ret = pm_device_runtime_enable(ls_1_8); - if (ret != 0) { - LOG_WRN("Error setting up load switch 1.8V."); - } - - ret = pm_device_runtime_enable(ls_3_3); - if (ret != 0) { - LOG_WRN("Error setting up load switch 3.3V."); - } - ret = pm_device_runtime_enable(ls_sd); - if (ret != 0) { - LOG_WRN("Error setting up load switch SD."); - } - - ret = device_is_ready(error_led.port); //bool + int ret = device_is_ready(error_led.port); if (!ret) { LOG_WRN("Error LED not ready."); - //return -1; } ret = gpio_pin_configure_dt(&error_led, GPIO_OUTPUT_INACTIVE); if (ret != 0) { LOG_INF("Failed to set Error LED as output: ERROR -%i.", ret); - //return ret; } - // check if fuel gauge has wrong value float capacity = fuel_gauge.capacity(); - if (abs(capacity - _battery_settings.capacity) > 1e-4) { + if (fabsf(capacity - _battery_settings.capacity) > 1e-4f) { fuel_gauge.setup(_battery_settings); set_error_led(); } @@ -430,7 +386,7 @@ int PowerManager::begin() { uint32_t device_id[2]; - // Lesen der DEVICEID + // Read the device ID device_id[0] = nrf_ficr_deviceid_get(NRF_FICR, 0); device_id[1] = nrf_ficr_deviceid_get(NRF_FICR, 1); @@ -457,15 +413,12 @@ bool PowerManager::check_battery() { float temp = fuel_gauge.temperature(); if (temp < _battery_settings.temp_min || temp > _battery_settings.temp_max) { - // set params battery_controller.disable_charge(); return false; } else if (temp < _battery_settings.temp_fast_min || temp > _battery_settings.temp_fast_max) { - // set params battery_controller.write_charging_control(_battery_settings.i_charge / 2); battery_controller.enable_charge(); } else { - // normal params battery_controller.write_charging_control(_battery_settings.i_charge); battery_controller.enable_charge(); } @@ -474,15 +427,11 @@ bool PowerManager::check_battery() { bat_status bs = fuel_gauge.battery_status(); if (bs.SYSDWN) return false; - //gauge_status gs = fuel_gauge.gauging_state(); - //if (gs.edv1) return false; // critical battery state - return true; } void PowerManager::get_battery_status(battery_level_status &status) { - battery_controller.exit_high_impedance(); - uint8_t charging_state = battery_controller.read_charging_state() >> 6; + BQ25120a::ChargePhase phase = battery_controller.read_charge_phase(); status.flags = 0; status.power_state = 0x1; // battery_present @@ -490,15 +439,14 @@ void PowerManager::get_battery_status(battery_level_status &status) { // charging state if (battery_controller.power_connected()) { status.power_state |= (0x1 << 1); // external source wired (wireless = 3-4), - if (charging_state == 0x1) { + if (phase == BQ25120a::ChargePhase::Charging) { status.power_state |= (0x1 << 5); // charging status.power_state |= (0x1 << 9); // const current } - else if (charging_state == 0x2) status.power_state |= (0x3 << 5); // inactive discharge + else if (phase == BQ25120a::ChargePhase::Done) status.power_state |= (0x3 << 5); // inactive discharge } else { status.power_state |= (0x2 << 5); // active discharge } - battery_controller.enter_high_impedance(); // battery level gauge_status gs = fuel_gauge.gauging_state(); @@ -507,7 +455,6 @@ void PowerManager::get_battery_status(battery_level_status &status) { if (gs.edv1) status.power_state |= (0x3 << 7); // critical else if (gs.edv2) status.power_state |= (0x2 << 7); // low else status.power_state |= (0x1 << 7); // good - // status.power_state |= (0x1 << 12); // fault reason } void PowerManager::get_energy_status(battery_energy_status &status) { @@ -544,65 +491,49 @@ void bt_disconnect_handler(struct bt_conn *conn, void * data) { } } -void PowerManager::reboot() { +void PowerManager::setup_pmic() { + battery_controller.setup(_battery_settings); +} + +void PowerManager::shutdown_subsystems() { int ret; - - // disconnect devices + uint8_t data = BT_HCI_ERR_REMOTE_USER_TERM_CONN; bt_conn_foreach(BT_CONN_TYPE_ALL, bt_disconnect_handler, &data); - ret = bt_le_adv_stop(); + ret = bt_mgmt_ext_adv_stop(0); + if (ret) LOG_WRN("Failed to stop ext adv: %d", ret); stop_sensor_manager(); ret = bt_mgmt_stop_watchdog(); - ERR_CHK(ret); + if (ret) LOG_WRN("Failed to stop watchdog: %d", ret); dac.end(); +} +void PowerManager::reboot() { + shutdown_subsystems(); sys_reboot(SYS_REBOOT_COLD); } int PowerManager::power_down(bool fault) { int ret; - // disconnect devices - uint8_t data = BT_HCI_ERR_REMOTE_USER_TERM_CONN; - bt_conn_foreach(BT_CONN_TYPE_ALL, bt_disconnect_handler, &data); - - ret = bt_le_adv_stop(); + shutdown_subsystems(); - // power disonnected - // prepare interrupts - - led_controller.begin(); led_controller.power_off(); - stop_sensor_manager(); - bool charging = battery_controller.power_connected(); - if (!charging) { - ret = battery_controller.set_wakeup_int(); - if (ret != 0) return ret; - - ret = fuel_gauge.set_wakeup_int(); - if (ret != 0) return ret; - - // check battery good - //if (!fault) ret = power_switch.set_wakeup_int(); - //if (ret != 0) return ret; - - battery_controller.enter_high_impedance(); - } - - //TODO: prevent crashing with bt_disable (does not wake up) - /*ret = bt_disable(); - - if (ret != 0) { - NVIC_SystemReset(); - sys_reboot(SYS_REBOOT_COLD); - }*/ + // Let BT disconnect events from bt_conn_disconnect above propagate, then + // tear down the host. Side effect: on nRF5340 this also writes + // NETWORK.FORCEOFF=Hold via the bt_hci_transport_teardown → nrf53_cpunet_enable(false) + // path (zephyr/drivers/bluetooth/hci/nrf53_support.c + nrf53_cpunet_mgmt.c), + // so no separate net-core shutdown is needed here. + k_msleep(200); + int bt_ret = bt_disable(); + if (bt_ret) LOG_WRN("bt_disable() failed: %d", bt_ret); if (fault) { LOG_WRN("Power off due to fault"); @@ -611,84 +542,86 @@ int PowerManager::power_down(bool fault) { } LOG_PANIC(); - ret = bt_mgmt_stop_watchdog(); - //ERR_CHK(ret); - - dac.end(); - - // TODO: check states of load switch (should already be suspended - // if all devieses have been terminated correctly) - - // turn off error led - gpio_pin_set_dt(&error_led, 0); + gpio_pin_set_dt(&error_led, 0); if (charging) { - //NVIC_SystemReset(); sys_reboot(SYS_REBOOT_COLD); return 0; } + // Disable EN_LS_LDO in the BQ25120A so the 3.3V LDO is fully off + battery_controller.write_LS_control(false); + ret = pm_device_action_run(ls_sd, PM_DEVICE_ACTION_SUSPEND); ret = pm_device_action_run(ls_3_3, PM_DEVICE_ACTION_SUSPEND); ret = pm_device_action_run(ls_1_8, PM_DEVICE_ACTION_SUSPEND); - ret = pm_device_action_run(cons, PM_DEVICE_ACTION_SUSPEND); - /*const struct device *const i2c = DEVICE_DT_GET(DT_NODELABEL(i2c1)); - ret = pm_device_action_run(i2c, PM_DEVICE_ACTION_SUSPEND); - ERR_CHK(ret);*/ + // Pre-poweroff audit. Expected: load-switch pins LOW, EN_LS_LDO=0. + // pmic_cd reflects the CD line state *before* we drive it high for + // Ship Mode entry below. netcore_off reflects whatever bt_disable() + // left behind. Anything else is a possible leak. + { + uint8_t ls_1_8_pin = nrf_gpio_pin_out_read(NRF_GPIO_PIN_MAP(1, 11)); + uint8_t ls_3_3_pin = nrf_gpio_pin_out_read(NRF_GPIO_PIN_MAP(0, 14)); + uint8_t ls_sd_pin = nrf_gpio_pin_out_read(NRF_GPIO_PIN_MAP(1, 12)); + uint8_t ppg_ldo = nrf_gpio_pin_out_read(NRF_GPIO_PIN_MAP(0, 6)); + uint8_t pmic_cd = nrf_gpio_pin_out_read(NRF_GPIO_PIN_MAP(0, 17)); + uint8_t on_btn = nrf_gpio_pin_read(NRF_GPIO_PIN_MAP(1, 5)); + uint8_t netcore_off = NRF_RESET->NETWORK.FORCEOFF & 1; + uint8_t en_ls_ldo = (battery_controller.read_ls_ldo_ctrl_raw() >> 7) & 1; + LOG_WRN("poweroff audit: ls_1_8=%u ls_3_3=%u ls_sd=%u ppg_ldo=%u " + "EN_LS_LDO=%u pmic_cd=%u on_btn=%u netcore_off=%u", + ls_1_8_pin, ls_3_3_pin, ls_sd_pin, ppg_ldo, + en_ls_ldo, pmic_cd, on_btn, netcore_off); + LOG_PANIC(); + } - /*const struct device *const watch_dog = DEVICE_DT_GET(DT_CHOSEN(zephyr_bt_hci_rpmsg_ipc)); - ret = pm_device_action_run(watch_dog, PM_DEVICE_ACTION_SUSPEND); - ERR_CHK(ret);*/ + ret = pm_device_action_run(cons, PM_DEVICE_ACTION_SUSPEND); - /*const struct device *const watch_dog = DEVICE_DT_GET(DT_ALIAS(watchdog0)); - ret = pm_device_action_run(watch_dog, PM_DEVICE_ACTION_SUSPEND); - ERR_CHK(ret);*/ + // Ship Mode entry (BQ25120A datasheet §9.3.1.1 Figure 15) requires MR + // high. If we got here via the long-press WAKE_2 path, the user is still + // holding the button (MR low); wait for release. Bound the wait so a + // stuck button can't block shutdown forever — after the timeout we fall + // through to sys_poweroff() which gives System OFF (~µA) rather than + // Ship Mode (~nA), but at least we don't spin. + // + // Poll the raw ON+BTN pin (gpio1.5, high when button pressed) directly + // rather than earable_btn.getState(): the Button class updates its + // cached state from a k_work on the system work queue, which is the + // same queue we may be executing on — so getState() can be stale. + const uint32_t BTN_PIN = NRF_GPIO_PIN_MAP(1, 5); + for (int i = 0; i < 50 && nrf_gpio_pin_read(BTN_PIN); i++) { + k_msleep(100); + } + // Fire-and-die: drives CD high and writes EN_SHIPMODE=1. The BQ25120A + // latches BAT FET off within tQUIET (< 1 ms per datasheet timing + // diagrams), SYS collapses, the nRF5340 loses power. Quiescent drain in + // Ship Mode is ~2 nA (datasheet I_BAT_SHIP) vs ~0.7–4 µA in Hi-Z — the + // ~1000× factor is what gives months-of-shelf-life behaviour. Wake path: + // long MR press (>= MRRESET time configured in setup()) asserts the + // BQ25120A RESET output, which is wired to nRF5340 nRESET → cold boot. + battery_controller.enter_ship_mode(); + + // Fallbacks. If Ship Mode entry was declined (e.g. VIN re-appeared in + // the last millisecond, invalidating the VIN> 6; - - //LOG_INF("Charger Watchdog ..................."); - - if (last_charging_state == 0) { + if (charger_init_pending) { + charger_init_pending = false; LOG_INF("Setting up charge controller ........"); - battery_controller.setup(_battery_settings); + setup_pmic(); battery_controller.enable_charge(); } - - //if (last_charging_state != charging_state || ) { - k_work_submit(&fuel_gauge_work); - //state_inidicator.set_state() - /*switch (charging_state) { - case 0: - LOG_INF("charging state: ready"); - break; - case 1: - LOG_INF("charging state: charging"); - break; - case 2: - LOG_INF("charging state: done"); - break; - case 3: - LOG_WRN("charging state: fault"); - - //battery_controller.setup(_battery_settings); - - break; - }*/ - //} - - last_charging_state = charging_state; + k_work_submit(&fuel_gauge_work); } int cmd_setup_fuel_gauge(const struct shell *shell, size_t argc, const char **argv) { @@ -735,18 +668,191 @@ static int cmd_battery_info(const struct shell *shell, size_t argc, const char * shell_print(shell, " Time to Empty: %ih %02dmin", (int)tte / 60, (int)tte % 60); // Battery controller status - battery_controller.exit_high_impedance(); - shell_print(shell, "Charging Information:"); - uint16_t charging_state = battery_controller.read_charging_state() >> 6; - shell_print(shell, " Charging State: %i", charging_state); + BQ25120a::ChargePhase phase = battery_controller.read_charge_phase(); + shell_print(shell, " Charging State: %i", static_cast(phase)); shell_print(shell, " Power Good: %i", battery_controller.power_connected()); - + struct chrg_state charge_ctrl = battery_controller.read_charging_control(); - shell_print(shell, " Charge Control: enabled=%i, current=%.1f mA", + shell_print(shell, " Charge Control: enabled=%i, current=%.1f mA", charge_ctrl.enabled, charge_ctrl.mAh); - battery_controller.enter_high_impedance(); + uint8_t en_ls_ldo = (battery_controller.read_ls_ldo_ctrl_raw() >> 7) & 1; + + // Load switch / LDO rail state + shell_print(shell, "Load Switches:"); + shell_print(shell, " ls_1_8 (P1.11): %d", nrf_gpio_pin_out_read(NRF_GPIO_PIN_MAP(1, 11))); + shell_print(shell, " ls_3_3 (P0.14): %d", nrf_gpio_pin_out_read(NRF_GPIO_PIN_MAP(0, 14))); + shell_print(shell, " ls_sd (P1.12): %d", nrf_gpio_pin_out_read(NRF_GPIO_PIN_MAP(1, 12))); + shell_print(shell, " PPG LDO (P0.06): %d", nrf_gpio_pin_out_read(NRF_GPIO_PIN_MAP(0, 6))); + shell_print(shell, " BQ25120A EN_LS_LDO: %u", en_ls_ldo); + + return 0; +} + +static void i2c_manual_recover(uint32_t pin_scl, uint32_t pin_sda) { + // Configure SCL and SDA as standard push-pull + nrf_gpio_cfg_output(pin_scl); + nrf_gpio_pin_set(pin_scl); + nrf_gpio_cfg_output(pin_sda); + nrf_gpio_pin_set(pin_sda); + k_usleep(100); + + // Generate 9 SCL clock pulses to clock out any stuck transmitting slave + for (int i = 0; i < 10; i++) { + nrf_gpio_pin_clear(pin_scl); + k_usleep(10); + nrf_gpio_pin_set(pin_scl); + k_usleep(10); + } + + // Generate a STOP condition (SDA LOW to HIGH while SCL is HIGH) + nrf_gpio_pin_clear(pin_scl); + k_usleep(10); + nrf_gpio_pin_clear(pin_sda); // SDA LOW + k_usleep(10); + nrf_gpio_pin_set(pin_scl); // SCL HIGH + k_usleep(10); + nrf_gpio_pin_set(pin_sda); // SDA HIGH + k_usleep(10); + + // Restore to Input (High-Impedance) + nrf_gpio_cfg_input(pin_scl, NRF_GPIO_PIN_NOPULL); + nrf_gpio_cfg_input(pin_sda, NRF_GPIO_PIN_NOPULL); +} + +static int cmd_sensor_diag(const struct shell *shell, size_t argc, const char **argv) { + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + shell_print(shell, "=== Sensor Bus Diagnostic ==="); + + // 1. Load switch GPIO states (output pins need nrf_gpio_pin_out_read) + shell_print(shell, "\n-- Load Switch GPIO States --"); + shell_print(shell, " ls_1_8 (P1.11): %d", nrf_gpio_pin_out_read(NRF_GPIO_PIN_MAP(1, 11))); + shell_print(shell, " ls_3_3 (P0.14): %d", nrf_gpio_pin_out_read(NRF_GPIO_PIN_MAP(0, 14))); + shell_print(shell, " ls_sd (P1.12): %d", nrf_gpio_pin_out_read(NRF_GPIO_PIN_MAP(1, 12))); + shell_print(shell, " PPG LDO (P0.06): %d", nrf_gpio_pin_out_read(NRF_GPIO_PIN_MAP(0, 6))); + + // 2. I2C bus line levels (HIGH = idle/OK, LOW = stuck) + shell_print(shell, "\n-- I2C Bus Line Levels --"); + shell_print(shell, " I2C2: SCL(P1.00)=%d SDA(P1.15)=%d", + nrf_gpio_pin_read(NRF_GPIO_PIN_MAP(1, 0)), + nrf_gpio_pin_read(NRF_GPIO_PIN_MAP(1, 15))); + shell_print(shell, " I2C3: SCL(P1.02)=%d SDA(P1.03)=%d", + nrf_gpio_pin_read(NRF_GPIO_PIN_MAP(1, 2)), + nrf_gpio_pin_read(NRF_GPIO_PIN_MAP(1, 3))); + + // 3. BQ25120a PMIC registers + shell_print(shell, "\n-- BQ25120a PMIC --"); + uint8_t charging_state = battery_controller.read_charging_state(); + uint8_t fault = battery_controller.read_fault(); + uint8_t ts_fault = battery_controller.read_ts_fault(); + uint8_t ls_ldo_raw = battery_controller.read_ls_ldo_ctrl_raw(); + float ldo_voltage = battery_controller.read_ldo_voltage(); + + shell_print(shell, " Charging state reg: 0x%02x (state=%d)", charging_state, charging_state >> 6); + shell_print(shell, " Fault reg: 0x%02x", fault); + for (const auto &b : BQ25120a::fault_bits) { + if (fault & b.mask) shell_print(shell, " %s", b.name); + } + shell_print(shell, " TS fault reg: 0x%02x", ts_fault); + shell_print(shell, " LS_LDO_CTRL raw: 0x%02x", ls_ldo_raw); + shell_print(shell, " EN_LS_LDO (bit7): %d", (ls_ldo_raw >> 7) & 1); + shell_print(shell, " LDO voltage: %.1f V", (double)ldo_voltage); + shell_print(shell, " Power Good (PG): %d", battery_controller.power_connected()); + + // 3b. If LDO voltage is wrong, try to fix it and report + if ((ls_ldo_raw >> 7) == 0) { + shell_print(shell, "\n EN_LS_LDO is OFF — re-running PMIC setup..."); + power_manager.setup_pmic(); + uint8_t after = battery_controller.read_ls_ldo_ctrl_raw(); + shell_print(shell, " After setup: LS_LDO_CTRL=0x%02x (EN=%d)", after, (after >> 7) & 1); + } + + // 4. I2C3 sensor probes + shell_print(shell, "\n-- I2C3 Sensor Probes --"); + const struct device *i2c3_dev = DEVICE_DT_GET(DT_NODELABEL(i2c3)); + struct { const char *name; uint8_t addr; } sensors[] = { + {"BMX160 (IMU)", 0x68}, + {"BMP388 (Baro)", 0x76}, + {"BMA580 (Bone)", 0x18}, + {"MLX90632 (Temp)", 0x3A}, + }; + for (int i = 0; i < 4; i++) { + uint8_t dummy; + int ret = i2c_burst_read(i2c3_dev, sensors[i].addr, 0x00, &dummy, 1); + shell_print(shell, " %s @0x%02x: %s (ret=%d)", + sensors[i].name, sensors[i].addr, + ret == 0 ? "OK" : "FAIL", ret); + } + + // 5. I2C2 sensor probe + shell_print(shell, "\n-- I2C2 Sensor Probes --"); + const struct device *i2c2_dev = DEVICE_DT_GET(DT_NODELABEL(i2c2)); + { + uint8_t dummy; + int ret = i2c_burst_read(i2c2_dev, 0x62, 0x00, &dummy, 1); + shell_print(shell, " MAXM86161 (PPG) @0x62: %s (ret=%d)", + ret == 0 ? "OK" : "FAIL", ret); + } + + shell_print(shell, "\n=== Diagnostic Complete ==="); + return 0; +} + +static int cmd_sensor_bus_reset(const struct shell *shell, size_t argc, const char **argv) { + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + // 1. Stop all sensors to halt ongoing I2C traffic + shell_print(shell, "Stopping all sensors..."); + stop_sensor_manager(); + k_msleep(100); + + // 2. Disable EN_LS_LDO in PMIC register BEFORE pulling GPIOs + // With EN_LS_LDO=1 persisting in the PMIC, the 3.3V LDO may leak + // even with LSCTRL low, preventing sensors from fully de-powering. + shell_print(shell, "Disabling EN_LS_LDO in PMIC..."); + battery_controller.write_LS_control(false); + + // 3. Suspend I2C peripherals before GPIO takeover + shell_print(shell, "Suspending I2C peripherals..."); +#if DT_NODE_HAS_STATUS(DT_NODELABEL(i2c2), okay) + const struct device *i2c2_dev = DEVICE_DT_GET(DT_NODELABEL(i2c2)); + pm_device_action_run(i2c2_dev, PM_DEVICE_ACTION_SUSPEND); +#endif +#if DT_NODE_HAS_STATUS(DT_NODELABEL(i2c3), okay) + const struct device *i2c3_dev = DEVICE_DT_GET(DT_NODELABEL(i2c3)); + pm_device_action_run(i2c3_dev, PM_DEVICE_ACTION_SUSPEND); +#endif + + // 4. Turn off ALL load switches via raw GPIO (bypass PM refcounts) + shell_print(shell, "Turning off all load switches..."); + nrf_gpio_cfg_output(NRF_GPIO_PIN_MAP(1, 11)); // ls_1_8 + nrf_gpio_pin_clear(NRF_GPIO_PIN_MAP(1, 11)); + nrf_gpio_cfg_output(NRF_GPIO_PIN_MAP(0, 14)); // ls_3_3 / LSCTRL + nrf_gpio_pin_clear(NRF_GPIO_PIN_MAP(0, 14)); + nrf_gpio_cfg_output(NRF_GPIO_PIN_MAP(1, 12)); // ls_sd + nrf_gpio_pin_clear(NRF_GPIO_PIN_MAP(1, 12)); + + // 5. Force I2C lines LOW to break back-powering through pull-ups/ESD diodes + shell_print(shell, "Forcing I2C lines LOW to break back-power path..."); + nrf_gpio_cfg_output(NRF_GPIO_PIN_MAP(1, 0)); // I2C2 SCL + nrf_gpio_pin_clear(NRF_GPIO_PIN_MAP(1, 0)); + nrf_gpio_cfg_output(NRF_GPIO_PIN_MAP(1, 15)); // I2C2 SDA + nrf_gpio_pin_clear(NRF_GPIO_PIN_MAP(1, 15)); + nrf_gpio_cfg_output(NRF_GPIO_PIN_MAP(1, 2)); // I2C3 SCL + nrf_gpio_pin_clear(NRF_GPIO_PIN_MAP(1, 2)); + nrf_gpio_cfg_output(NRF_GPIO_PIN_MAP(1, 3)); // I2C3 SDA + nrf_gpio_pin_clear(NRF_GPIO_PIN_MAP(1, 3)); + + // 6. Power off so EN_LS_LDO=0 persists and sensors stay de-powered. + // Leave off as long as needed, then press button to restart. + // setup() will re-enable the LDO on boot. + shell_print(shell, "Powering off. Press button to restart."); + k_msleep(20); + sys_poweroff(); return 0; } @@ -754,6 +860,8 @@ static int cmd_battery_info(const struct shell *shell, size_t argc, const char * SHELL_STATIC_SUBCMD_SET_CREATE(battery_cmd, SHELL_COND_CMD(CONFIG_SHELL, info, NULL, "Print battery info", cmd_battery_info), SHELL_COND_CMD(CONFIG_SHELL, setup, NULL, "Setup fuel gauge", cmd_setup_fuel_gauge), + SHELL_COND_CMD(CONFIG_SHELL, sensor_diag, NULL, "Diagnose sensor bus and power rail state", cmd_sensor_diag), + SHELL_COND_CMD(CONFIG_SHELL, sensor_bus_reset, NULL, "Hard reset I2C sensor bus via GPIO sink", cmd_sensor_bus_reset), SHELL_SUBCMD_SET_END); SHELL_CMD_REGISTER(battery, &battery_cmd, "Power Manager Commands", NULL); diff --git a/src/Battery/PowerManager.h b/src/Battery/PowerManager.h index e010e5f6..7457a9dc 100644 --- a/src/Battery/PowerManager.h +++ b/src/Battery/PowerManager.h @@ -18,9 +18,6 @@ class PowerManager { int begin(); int power_down(bool fault = false); - //bool check_boot_condition(); - - //static LoadSwitch v1_8_switch; void reboot(); @@ -29,33 +26,47 @@ class PowerManager { void get_health_status(battery_health_status &status); void set_error_led(int val = 1); + void setup_pmic(); static k_work_delayable power_down_work; private: bool power_on = false; bool charging_disabled = false; - uint16_t last_charging_state = 0; - - enum charging_state last_charging_msg_state = DISCHARGING; + // ISR→work handoff: the USB plug-in ISR sets this; the work handler + // clears it after re-running setup_pmic() (I2C can't run in ISR context). + bool charger_init_pending = false; + // Set the first time the button GPIO reads RELEASED after boot. BQ25120A + // WAKE events are ignored until this flag is set, so the power-on press + // doesn't immediately toggle power back off. + bool first_release_seen = false; void charge_task(); void power_connected(); + // Teardown sequence shared between reboot() and power_down(): disconnect + // BT peers, stop ext adv, stop sensors, stop BT watchdog, stop DAC. Each + // step logs a breadcrumb and failures are warned but not fatal so the + // caller always reaches its final sys_reboot / sys_poweroff. + void shutdown_subsystems(); + bool check_battery(); k_timeout_t chrg_interval = K_SECONDS(CONFIG_BATTERY_CHARGE_CONTROLLER_NORMAL_INTERVAL_SECONDS); static k_work_delayable charge_ctrl_delayable; + static k_work_delayable power_button_watch_work; - //static k_work power_down_work; static k_work fuel_gauge_work; static k_work battery_controller_work; + static k_work usb_plug_reboot_work; static void charge_ctrl_work_handler(struct k_work * work); static void power_down_work_handler(struct k_work * work); static void fuel_gauge_work_handler(struct k_work * work); static void battery_controller_work_handler(struct k_work * work); + static void power_button_watch_handler(struct k_work * work); + static void usb_plug_reboot_handler(struct k_work * work); static void power_good_callback(const struct device *dev, struct gpio_callback *cb, uint32_t pins); static void fuel_gauge_callback(const struct device *dev, struct gpio_callback *cb, uint32_t pins); diff --git a/src/drivers/LED_Controller/KTD2026.cpp b/src/drivers/LED_Controller/KTD2026.cpp index f42e7bac..fb3d263a 100644 --- a/src/drivers/LED_Controller/KTD2026.cpp +++ b/src/drivers/LED_Controller/KTD2026.cpp @@ -9,7 +9,7 @@ LOG_MODULE_REGISTER(LED, CONFIG_MAIN_LOG_LEVEL); bool KTD2026::readReg(uint8_t reg, uint8_t * buffer, uint16_t len) { - _i2c->aquire(); + _i2c->acquire(); int ret = i2c_burst_read(_i2c->master, address, reg, buffer, len); if (ret) LOG_WRN("I2C read failed: %d\n", ret); @@ -20,40 +20,61 @@ bool KTD2026::readReg(uint8_t reg, uint8_t * buffer, uint16_t len) { } void KTD2026::writeReg(uint8_t reg, uint8_t *buffer, uint16_t len) { - _i2c->aquire(); + _i2c->acquire(); int ret = i2c_burst_write(_i2c->master, address, reg, buffer, len); - if (ret) LOG_WRN("I2C write failed: %d", ret); + if (ret) LOG_WRN("I2C write reg 0x%02x failed: %d", reg, ret); _i2c->release(); } void KTD2026::begin() { - int ret; - if (_active) return; _active = true; - - ret = pm_device_runtime_get(ls_1_8); - ret = pm_device_runtime_get(ls_3_3); + /* KTD2026 VIN is on ls_3_3; i2c1 pull-ups are on the permanent supply. */ + pm_device_runtime_get(ls_3_3); + + k_usleep(200); /* datasheet recovery time after reset command */ _i2c->begin(); + k_usleep(200); /* datasheet recovery time after reset command */ reset(); } void KTD2026::reset() { + /* Datasheet page 13: after power-up or VIN dropping below 2.7V, write + * Reg0[2:0]=111 ("Reset Complete Chip") then wait 200 µs. + * + * Empirical observation: on any MCU-only reboot (sys_reboot, incl. + * SYS_REBOOT_COLD), the chip NACKs this reset write, but still ACKs + * writes to other registers (Reg 4/6 etc.), so subsequent LED ops work + * fine. The exact reason is not documented. + * + * We issue the command anyway so true cold power cycles (battery + * disconnect, or sys_poweroff→wake which disables the LS/LDO) still + * get the datasheet-recommended sequence, and log the NACK at DBG. */ + _i2c->acquire(); uint8_t val = 0x7; - writeReg(registers::CTRL, &val, sizeof(val)); - k_usleep(200); + int ret = i2c_burst_write(_i2c->master, address, registers::CTRL, &val, sizeof(val)); + _i2c->release(); + if (ret) { + LOG_DBG("reset NACK (expected when VIN did not drop below UVLO): %d", ret); + } + k_usleep(200); /* datasheet recovery time after reset command */ } void KTD2026::power_off() { + if (!_active) return; + + /* Reg0 = 0x08 → Reg0[4:3]=01 = "Device ON when SCL=H AND SDA toggling; + * shutdown when SCL goes low or SDA stops toggling" (datasheet p.13). + * Bus goes idle right after this write, so the chip drops into + * shutdown mode (<1 µA per datasheet p.1) before we release ls_3_3. */ uint8_t val = 0x8; writeReg(registers::CTRL, &val, sizeof(val)); - int ret = pm_device_runtime_put(ls_1_8); - ret = pm_device_runtime_put(ls_3_3); + pm_device_runtime_put(ls_3_3); _active = false; } @@ -61,7 +82,7 @@ void KTD2026::power_off() { void KTD2026::setColor(const RGBColor& color) { uint8_t channel_enable = 0; RGBColor _color; - memcpy(_color, color, sizeof(RGBColor)); // Korrekte Array-Kopie + memcpy(_color, color, sizeof(RGBColor)); for (int i = 0; i < 3; i++) { if (_color[i] > 0) { diff --git a/src/drivers/LED_Controller/KTD2026.h b/src/drivers/LED_Controller/KTD2026.h index 510415f3..760038ec 100644 --- a/src/drivers/LED_Controller/KTD2026.h +++ b/src/drivers/LED_Controller/KTD2026.h @@ -38,9 +38,8 @@ class KTD2026 { bool readReg(uint8_t reg, uint8_t *buffer, uint16_t len); void writeReg(uint8_t reg, uint8_t *buffer, uint16_t len); - int address = 0x30; + int address = DT_REG_ADDR(DT_NODELABEL(ktd2026)); - //TwoWire * _pWire = &Wire; TWIM * _i2c = &I2C1; bool _active = false; diff --git a/src/utils/StateIndicator.cpp b/src/utils/StateIndicator.cpp index c9fa51a3..86c6e08c 100644 --- a/src/utils/StateIndicator.cpp +++ b/src/utils/StateIndicator.cpp @@ -10,6 +10,8 @@ #include "channel_assignment.h" #ifdef CONFIG_MCUMGR_MGMT_NOTIFICATION_HOOKS +#include +#include #include #include #endif @@ -24,26 +26,30 @@ ZBUS_CHAN_DECLARE(battery_chan); struct mgmt_callback mcu_mgr_cb; +/* Hold the 1.8 V rail (ls_1_8) up for the duration of a DFU upload so the + * MX25R6435F is powered when mcumgr writes to it. */ +static const struct device *const ls_1_8_dev = DEVICE_DT_GET(load_switch_1_8_id); + enum mgmt_cb_return chuck_write_indication(uint32_t event, enum mgmt_cb_return prev_status, int32_t *rc, uint16_t *group, bool *abort_more, void *data, size_t data_size) { - if (event == MGMT_EVT_OP_IMG_MGMT_DFU_CHUNK) { - /* This is the event we registered for */ - led_controller.setColor(LED_ORANGE); + switch (event) { + case MGMT_EVT_OP_IMG_MGMT_DFU_STARTED: + pm_device_runtime_get(ls_1_8_dev); + break; + case MGMT_EVT_OP_IMG_MGMT_DFU_STOPPED: + pm_device_runtime_put(ls_1_8_dev); + break; + case MGMT_EVT_OP_IMG_MGMT_DFU_CHUNK: + led_controller.setColor(LED_ORANGE); k_msleep(10); led_controller.setColor(LED_OFF); + break; + default: + break; } - /*else if (event == MGMT_EVT_OP_IMG_MGMT_DFU_CHUNK_WRITE_COMPLETE) { - led_controller.setColor(LED_OFF); - } - else if (event == MGMT_EVT_OP_OS_MGMT_RESET) { - LOG_INF("RESET received"); - }*/ - - //LOG_DBG("mcu mgr hook called with event: %d", event); - /* Return OK status code to continue with acceptance to underlying handler */ return MGMT_CB_OK; } @@ -79,9 +85,46 @@ static void power_evt_handler(const struct zbus_channel *chan) ZBUS_LISTENER_DEFINE(power_evt_listen, power_evt_handler); //static +// Duration to show charging indication before switching to device status +#define CHARGING_DISPLAY_MS 3000 +// Duration to show device status before switching back to charging +#define STATUS_DISPLAY_MS 2000 + +static struct k_work_delayable alternate_work; +static bool showing_device_status = false; + +static void alternate_work_handler(struct k_work *work) { + showing_device_status = !showing_device_status; + + if (showing_device_status) { + state_indicator.show_device_indication(); + k_work_schedule(&alternate_work, K_MSEC(STATUS_DISPLAY_MS)); + } else { + state_indicator.show_charging_indication(); + k_work_schedule(&alternate_work, K_MSEC(CHARGING_DISPLAY_MS)); + } +} + +static bool is_usb_charging_state(enum charging_state state) { + switch (state) { + case POWER_CONNECTED: + case PRECHARGING: + case SLOW_CHARGING: + case CHARGING: + case TRICKLE_CHARGING: + case FULLY_CHARGED: + case FAULT: + return true; + default: + return false; + } +} + void StateIndicator::init(struct earable_state state) { int ret; + k_work_init_delayable(&alternate_work, alternate_work_handler); + led_controller.begin(); ret = zbus_chan_add_obs(&bt_mgmt_chan, &bt_mgmt_evt_listen3, ZBUS_ADD_OBS_TIMEOUT_MS); @@ -94,9 +137,9 @@ void StateIndicator::init(struct earable_state state) { LOG_ERR("Failed to add battery listener"); } -#ifdef CONFIG_MCUMGR_MGMT_NOTIFICATION_HOOKS - mcu_mgr_cb.callback = chuck_write_indication; - mcu_mgr_cb.event_id = MGMT_EVT_OP_IMG_MGMT_DFU_CHUNK; //MGMT_EVT_OP_IMG_MGMT_ALL +#ifdef CONFIG_MCUMGR_MGMT_NOTIFICATION_HOOKS + mcu_mgr_cb.callback = chuck_write_indication; + mcu_mgr_cb.event_id = MGMT_EVT_OP_IMG_MGMT_ALL; mgmt_callback_register(&mcu_mgr_cb); #endif @@ -129,15 +172,7 @@ void StateIndicator::set_sd_state(enum sd_state state) { set_state(_state); } -void StateIndicator::set_state(struct earable_state state) { - _state = state; - - // do not update the state if set to custom color - if (_state.led_mode == CUSTOM) { - led_controller.setColor(color); - return; - } - +void StateIndicator::show_charging_indication() { switch (_state.charging_state) { case POWER_CONNECTED: led_controller.setColor(LED_ORANGE); @@ -157,6 +192,80 @@ void StateIndicator::set_state(struct earable_state state) { case FAULT: led_controller.setColor(LED_RED); break; + default: + break; + } +} + +void StateIndicator::show_device_indication() { + // Use faster blink patterns so they're visible in the brief status window + switch (_state.sd_state) { + case SD_RECORDING: + if (_state.pairing_state == CONNECTED) { + led_controller.pulse2(LED_MAGENTA, LED_GREEN, 100, 0, 0, 500); + } else { + led_controller.blink(LED_MAGENTA, 200, 500); + } + return; + case SD_FAULT: + led_controller.blink(LED_RED, 100, 200); + return; + default: + break; + } + + switch (_state.pairing_state) { + case SET_PAIRING: { + audio_channel channel; + channel_assignment_get(&channel); + if (channel == AUDIO_CH_L) { + led_controller.blink(LED_BLUE, 200, 500); + } else if (channel == AUDIO_CH_R) { + led_controller.blink(LED_RED, 200, 500); + } + break; + } + case BONDING: + led_controller.blink(LED_BLUE, 200, 500); + break; + case PAIRED: + led_controller.blink(LED_BLUE, 200, 500); + break; + case CONNECTED: + led_controller.blink(LED_GREEN, 200, 500); + break; + } +} + +void StateIndicator::set_state(struct earable_state state) { + _state = state; + + // do not update the state if set to custom color + if (_state.led_mode == CUSTOM) { + led_controller.setColor(color); + k_work_cancel_delayable(&alternate_work); + _alternating = false; + return; + } + + if (is_usb_charging_state(_state.charging_state)) { + // Show charging indication immediately + show_charging_indication(); + // Start or restart alternation cycle so device status is also visible + showing_device_status = false; + _alternating = true; + k_work_cancel_delayable(&alternate_work); + k_work_schedule(&alternate_work, K_MSEC(CHARGING_DISPLAY_MS)); + return; + } + + // Not USB-connected — stop alternation and show normal status + if (_alternating) { + k_work_cancel_delayable(&alternate_work); + _alternating = false; + } + + switch (_state.charging_state) { case BATTERY_CRITICAL: led_controller.blink(LED_RED, 100, 2000); break; @@ -183,7 +292,7 @@ void StateIndicator::set_state(struct earable_state state) { default: // Not recording, show the pairing state switch (_state.pairing_state) { - case SET_PAIRING: + case SET_PAIRING: { audio_channel channel; channel_assignment_get(&channel); if (channel == AUDIO_CH_L) { @@ -192,6 +301,7 @@ void StateIndicator::set_state(struct earable_state state) { led_controller.blink(LED_RED, 100, 200); } break; + } case BONDING: led_controller.blink(LED_BLUE, 100, 500); break; diff --git a/src/utils/StateIndicator.h b/src/utils/StateIndicator.h index 1eabb5ae..aa0842c7 100644 --- a/src/utils/StateIndicator.h +++ b/src/utils/StateIndicator.h @@ -1,6 +1,7 @@ #ifndef _STATE_INDICATOR_H #define _STATE_INDICATOR_H +#include #include "openearable_common.h" static const RGBColor LED_OFF = {0, 0, 0}; @@ -25,9 +26,13 @@ class StateIndicator { void set_indication_mode(enum led_mode state); void set_custom_color(const RGBColor &color); + void show_charging_indication(); + void show_device_indication(); + private: earable_state _state; RGBColor color; + bool _alternating = false; }; extern StateIndicator state_indicator; diff --git a/src/utils/error_handler.c b/src/utils/error_handler.c index 7e5ae97e..b3f2ae89 100644 --- a/src/utils/error_handler.c +++ b/src/utils/error_handler.c @@ -9,6 +9,7 @@ #include #include #include +#include /* Print everything from the error handler */ #include @@ -38,19 +39,34 @@ void error_handler(unsigned int reason, const struct arch_esf *esf) #endif /* defined(CONFIG_BOARD_NRF5340_AUDIO_DK_NRF5340_CPUAPP) */ #if CONFIG_BOARD_OPENEARABLE_V2_NRF5340_CPUAPP irq_lock(); - const struct gpio_dt_spec button_pin = GPIO_DT_SPEC_GET(DT_ALIAS(sw0), gpios); const struct gpio_dt_spec error_led = GPIO_DT_SPEC_GET_OR(DT_NODELABEL(led_error), gpios, {0}); - // turn on error led - gpio_pin_set_dt(&error_led, 1); + // Blink error LED to alert user of the crash + for (int i = 0; i < 250; i++) { + gpio_pin_set_dt(&error_led, 1); + k_busy_wait(100000); + gpio_pin_set_dt(&error_led, 0); + k_busy_wait(300000); + } - // wait for turning of power switch - while (1) { - int button_press = gpio_pin_get_dt(&button_pin); - if (button_press == 1) sys_reboot(SYS_REBOOT_COLD); + // Bit-bang peripheral load switches to LOW to prevent power drain during System OFF. + // This is safe to call from fault handlers (irq_lock held) unlike zephyr PM APIs. + nrf_gpio_pin_clear(NRF_GPIO_PIN_MAP(1, 11)); // ls_1_8 + nrf_gpio_pin_clear(NRF_GPIO_PIN_MAP(0, 14)); // ls_3_3 + nrf_gpio_pin_clear(NRF_GPIO_PIN_MAP(1, 12)); // ls_sd + nrf_gpio_pin_clear(NRF_GPIO_PIN_MAP(0, 6)); // ppg_ldo + + // Drive PMIC CD (Chip Disable) LOW to enter high-impedance mode + nrf_gpio_pin_clear(NRF_GPIO_PIN_MAP(0, 17)); // pmic_cd + + // Force network core off so that the main core can enter System OFF + NRF_RESET->NETWORK.FORCEOFF = 1; - k_busy_wait(10000); - //__asm__ volatile("nop"); + sys_poweroff(); + + // Fallback in case sys_poweroff() returns + while (1) { + k_busy_wait(1000000); } #endif #else