From 752287a1dfbafa07fd6ddf8c7955e065665af0ba Mon Sep 17 00:00:00 2001 From: pstruebi Date: Wed, 4 Feb 2026 14:33:10 +0100 Subject: [PATCH] make switching on and off on battery possible --- src/main.c | 371 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 318 insertions(+), 53 deletions(-) diff --git a/src/main.c b/src/main.c index 2cdcc3c..a2b313c 100644 --- a/src/main.c +++ b/src/main.c @@ -8,6 +8,9 @@ #include #include #include +#include +#include +#include #include #include @@ -17,8 +20,28 @@ static const struct device *const uart_dev = DEVICE_DT_GET(UART_DEVICE_NODE); static const struct device *charger_dev; +static const struct device *pmic_dev; +static const struct device *regulators_dev; static int64_t ref_time; +/* Power state machine (matches diagram) + * OFF_HIBERNATE: Device off/ship mode (not represented in code - device is off) + * ON_BATTERY: Running on battery, no USB connected + * CHARGING_ON: Running with USB connected (SW=ON) + * CHARGING_ONLY: Soft sleep with USB connected (SW=OFF) + */ +typedef enum { + POWER_STATE_ON_BATTERY, + POWER_STATE_CHARGING_ON, + POWER_STATE_CHARGING_ONLY +} power_state_t; + +static power_state_t app_state = POWER_STATE_ON_BATTERY; +static struct gpio_callback pmic_cb; +static volatile bool shphld_pressed = false; +static volatile bool vbus_changed = false; +static volatile bool vbus_detected = false; + /* Battery model from SDK sample */ static const struct battery_model battery_model = { #include "battery_model.inc" @@ -51,11 +74,9 @@ static int read_sensors(float *voltage, float *current, float *temp, int32_t *ch sensor_channel_get(charger_dev, SENSOR_CHAN_GAUGE_VOLTAGE, &value); *voltage = (float)value.val1 + ((float)value.val2 / 1000000); - /* Use die temp since no thermistor connected, clamp to model range 5-45C */ + /* Use die temp since no thermistor connected (no clamping) */ sensor_channel_get(charger_dev, SENSOR_CHAN_DIE_TEMP, &value); *temp = (float)value.val1 + ((float)value.val2 / 1000000); - if (*temp < 5.0f) *temp = 5.0f; - if (*temp > 45.0f) *temp = 45.0f; sensor_channel_get(charger_dev, SENSOR_CHAN_GAUGE_AVG_CURRENT, &value); *current = (float)value.val1 + ((float)value.val2 / 1000000); @@ -75,6 +96,210 @@ static const char *charge_status_str(int32_t chg_status) return "NOT_CHARGING"; } +/* ============== Power Management State Machine ============== */ + +static bool is_vbus_present(void) +{ + struct sensor_value val; + int ret; + + ret = sensor_attr_get(charger_dev, SENSOR_CHAN_NPM1300_CHARGER_VBUS_STATUS, + SENSOR_ATTR_NPM1300_CHARGER_VBUS_PRESENT, &val); + if (ret < 0) { + return false; + } + return val.val1 != 0; +} + +static void enter_ship_mode(void) +{ + print_uart(">>> Entering SHIP MODE (full power off)...\r\n"); + k_sleep(K_MSEC(100)); /* Allow logs to flush */ + + /* Use regulator_parent_ship_mode() - this is the correct API for full shutdown */ + int ret = regulator_parent_ship_mode(regulators_dev); + if (ret < 0) { + char buf[64]; + snprintk(buf, sizeof(buf), "ERROR: Ship mode failed! ret=%d\r\n", ret); + print_uart(buf); + } + /* If successful, device will power off immediately and won't reach here */ +} + +static void enter_soft_sleep_mode(void) +{ + print_uart(">>> Entering CHARGING_ONLY (powering down nRF53, charging continues)...\r\n"); + k_sleep(K_MSEC(100)); /* Allow logs to flush */ + + /* Actually power down the nRF53 - nPM1300 will continue charging battery */ + /* On SHPHLD press, device wakes and will detect VBUS -> CHARGING_ON */ + int ret = regulator_parent_ship_mode(regulators_dev); + if (ret < 0) { + char buf[64]; + snprintk(buf, sizeof(buf), "ERROR: Ship mode failed! ret=%d\r\n", ret); + print_uart(buf); + /* Fall back to just state change if ship mode fails */ + app_state = POWER_STATE_CHARGING_ONLY; + } + /* If successful, device will power off immediately and won't reach here */ +} + +static void pmic_event_handler(const struct device *dev, struct gpio_callback *cb, + uint32_t pins) +{ + if (pins & BIT(NPM1300_EVENT_SHIPHOLD_PRESS)) { + printk("SHPHLD PRESS\n"); + shphld_pressed = true; + } + if (pins & BIT(NPM1300_EVENT_SHIPHOLD_RELEASE)) { + printk("SHPHLD RELEASE\n"); + } + if (pins & BIT(NPM1300_EVENT_VBUS_DETECTED)) { + printk("VBUS DETECTED\n"); + vbus_changed = true; + vbus_detected = true; + } + if (pins & BIT(NPM1300_EVENT_VBUS_REMOVED)) { + printk("VBUS REMOVED\n"); + vbus_changed = true; + vbus_detected = false; + } +} + +static void handle_power_button(void) +{ + char buf[MSG_SIZE]; + bool vbus_connected = is_vbus_present(); + + const char *state_str = "UNKNOWN"; + switch (app_state) { + case POWER_STATE_ON_BATTERY: state_str = "ON_BATTERY"; break; + case POWER_STATE_CHARGING_ON: state_str = "CHARGING_ON"; break; + case POWER_STATE_CHARGING_ONLY: state_str = "CHARGING_ONLY"; break; + } + snprintk(buf, sizeof(buf), "Power button pressed. VBUS=%s, State=%s\r\n", + vbus_connected ? "present" : "absent", state_str); + print_uart(buf); + + if (!vbus_connected) { + /* No USB: button press -> SHIP MODE (full power off) */ + print_uart("No USB power - entering ship mode...\r\n"); + enter_ship_mode(); + } else { + /* USB present: button press -> CHARGING_ONLY (power down nRF53, keep charging) + * Note: On wake via SHPHLD, device restarts and goes to CHARGING_ON + */ + enter_soft_sleep_mode(); + } +} + +static void handle_vbus_change(bool vbus_present) +{ + char buf[MSG_SIZE]; + const char *old_state = "UNKNOWN"; + switch (app_state) { + case POWER_STATE_ON_BATTERY: old_state = "ON_BATTERY"; break; + case POWER_STATE_CHARGING_ON: old_state = "CHARGING_ON"; break; + case POWER_STATE_CHARGING_ONLY: old_state = "CHARGING_ONLY"; break; + } + + if (vbus_present) { + /* USB plugged in: ON_BATTERY -> CHARGING_ON */ + if (app_state == POWER_STATE_ON_BATTERY) { + snprintk(buf, sizeof(buf), ">>> VBUS detected: %s -> CHARGING_ON\r\n", old_state); + print_uart(buf); + app_state = POWER_STATE_CHARGING_ON; + } + } else { + /* USB removed */ + if (app_state == POWER_STATE_CHARGING_ON) { + /* CHARGING_ON + VBUS removed -> ON_BATTERY */ + snprintk(buf, sizeof(buf), ">>> VBUS removed: %s -> ON_BATTERY\r\n", old_state); + print_uart(buf); + app_state = POWER_STATE_ON_BATTERY; + } else if (app_state == POWER_STATE_CHARGING_ONLY) { + /* CHARGING_ONLY + VBUS removed -> SHIP MODE */ + print_uart(">>> VBUS removed in CHARGING_ONLY -> entering ship mode...\r\n"); + enter_ship_mode(); + } + } +} + +static int power_mgmt_init(void) +{ + char buf[MSG_SIZE]; + int ret; + uint8_t pending; + + /* Get PMIC device */ + pmic_dev = DEVICE_DT_GET(DT_NODELABEL(npm1300)); + if (!device_is_ready(pmic_dev)) { + print_uart("ERROR: PMIC device not ready\r\n"); + return -ENODEV; + } + print_uart("PMIC device: OK\r\n"); + + /* Get regulators device (needed for ship mode) */ + regulators_dev = DEVICE_DT_GET(DT_CHILD(DT_NODELABEL(npm1300), regulators)); + if (!device_is_ready(regulators_dev)) { + print_uart("ERROR: Regulators device not ready\r\n"); + return -ENODEV; + } + print_uart("Regulators device: OK\r\n"); + + /* IMPORTANT: Clear pending SHPHLD events BEFORE registering callback + * This prevents the wake-up button press from immediately triggering ship mode again + */ + if (mfd_npm1300_reg_read(pmic_dev, 0x00, 0x12, &pending) == 0 && pending != 0) { + snprintk(buf, sizeof(buf), "Clearing wake-up SHPHLD events: 0x%02x\r\n", pending); + print_uart(buf); + mfd_npm1300_reg_write(pmic_dev, 0x00, 0x13, pending); + } + /* Clear VBUS events too */ + if (mfd_npm1300_reg_read(pmic_dev, 0x00, 0x16, &pending) == 0 && pending != 0) { + snprintk(buf, sizeof(buf), "Clearing pending VBUS events: 0x%02x\r\n", pending); + print_uart(buf); + mfd_npm1300_reg_write(pmic_dev, 0x00, 0x17, pending); + } + + /* Small delay to ensure button is released after wake-up */ + k_sleep(K_MSEC(500)); + + /* Now register callback for PMIC events */ + gpio_init_callback(&pmic_cb, pmic_event_handler, + BIT(NPM1300_EVENT_SHIPHOLD_PRESS) | + BIT(NPM1300_EVENT_SHIPHOLD_RELEASE) | + BIT(NPM1300_EVENT_VBUS_DETECTED) | + BIT(NPM1300_EVENT_VBUS_REMOVED)); + + ret = mfd_npm1300_add_callback(pmic_dev, &pmic_cb); + if (ret < 0) { + snprintk(buf, sizeof(buf), "ERROR: Failed to add PMIC callback: %d\r\n", ret); + print_uart(buf); + return ret; + } + print_uart("PMIC callbacks registered\r\n"); + + /* Clear any events that occurred during the delay */ + mfd_npm1300_reg_read(pmic_dev, 0x00, 0x12, &pending); + if (pending != 0) { + mfd_npm1300_reg_write(pmic_dev, 0x00, 0x13, pending); + } + + /* Set initial state based on VBUS presence */ + if (is_vbus_present()) { + app_state = POWER_STATE_CHARGING_ON; + print_uart("Initial state: CHARGING_ON (VBUS present)\r\n"); + } else { + app_state = POWER_STATE_ON_BATTERY; + print_uart("Initial state: ON_BATTERY (VBUS absent)\r\n"); + } + + return 0; +} + +/* ============================================================ */ + static void print_init_config(void) { char buf[MSG_SIZE]; @@ -82,8 +307,7 @@ static void print_init_config(void) print_uart("\r\n=== nPM1300 Configuration ===\r\n"); - /* DTS Config */ - snprintk(buf, sizeof(buf), "Battery capacity: %.0f mAh (configured)\r\n", + snprintk(buf, sizeof(buf), "Battery capacity: %.0f mAh\r\n", (double)BATTERY_CAPACITY_MAH); print_uart(buf); @@ -95,22 +319,9 @@ static void print_init_config(void) DT_PROP(DT_NODELABEL(charger), current_microamp) / 1000); print_uart(buf); - snprintk(buf, sizeof(buf), "Discharge limit: %d mA\r\n", - DT_PROP(DT_NODELABEL(charger), dischg_limit_microamp) / 1000); - print_uart(buf); - - snprintk(buf, sizeof(buf), "VBUS limit: %d mA\r\n", - DT_PROP(DT_NODELABEL(charger), vbus_limit_microamp) / 1000); - print_uart(buf); - - snprintk(buf, sizeof(buf), "Thermistor: %d ohms (0=disabled)\r\n", - DT_PROP(DT_NODELABEL(charger), thermistor_ohms)); - print_uart(buf); - - /* Runtime config from sensor - read after fetch */ if (sensor_sample_fetch(charger_dev) == 0) { sensor_channel_get(charger_dev, SENSOR_CHAN_GAUGE_DESIRED_CHARGING_CURRENT, &val); - snprintk(buf, sizeof(buf), "Actual charge current limit: %d mA\r\n", + snprintk(buf, sizeof(buf), "Actual charge current: %d mA\r\n", val.val1 * 1000 + val.val2 / 1000); print_uart(buf); } @@ -140,7 +351,6 @@ static int fuel_gauge_init_custom(void) snprintk(buf, sizeof(buf), "nRF Fuel Gauge version: %s\r\n", nrf_fuel_gauge_version); print_uart(buf); - /* Read initial sensor values */ ret = read_sensors(¶meters.v0, ¶meters.i0, ¶meters.t0, &chg_status); if (ret < 0) { snprintk(buf, sizeof(buf), "Error reading sensors: %d\r\n", ret); @@ -153,7 +363,6 @@ static int fuel_gauge_init_custom(void) charge_status_str(chg_status)); print_uart(buf); - /* Initialize fuel gauge */ ret = nrf_fuel_gauge_init(¶meters, NULL); if (ret < 0) { snprintk(buf, sizeof(buf), "Fuel gauge init error: %d\r\n", ret); @@ -161,7 +370,6 @@ static int fuel_gauge_init_custom(void) return ret; } - /* Set charge current limit info */ struct sensor_value val; sensor_channel_get(charger_dev, SENSOR_CHAN_GAUGE_DESIRED_CHARGING_CURRENT, &val); max_charge_current = (float)val.val1 + ((float)val.val2 / 1000000); @@ -173,10 +381,7 @@ static int fuel_gauge_init_custom(void) nrf_fuel_gauge_ext_state_update(NRF_FUEL_GAUGE_EXT_STATE_INFO_TERM_CURRENT, &(union nrf_fuel_gauge_ext_state_info_data){.charge_term_current = term_charge_current}); - print_uart("Fuel gauge initialized OK\r\n"); - snprintk(buf, sizeof(buf), "NOTE: Using SDK example model, capacity=%.0f mAh\r\n", - (double)BATTERY_CAPACITY_MAH); - print_uart(buf); + print_uart("Fuel gauge: OK\r\n"); ref_time = k_uptime_get(); return 0; @@ -192,7 +397,7 @@ int main(void) return 0; } - print_uart("\r\n*** Scout Battery Test - Fuel Gauge ***\r\n"); + print_uart("\r\n*** Scout Battery Test ***\r\n"); charger_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(charger)); if (charger_dev == NULL || !device_is_ready(charger_dev)) { @@ -201,6 +406,11 @@ int main(void) } print_uart("Charger device: OK\r\n"); + ret = power_mgmt_init(); + if (ret < 0) { + print_uart("Power management init failed\r\n"); + } + print_init_config(); ret = fuel_gauge_init_custom(); @@ -208,53 +418,108 @@ int main(void) print_uart("Fuel gauge init failed, continuing without SoC\r\n"); } - print_uart("\r\nStarting monitoring (3s interval)...\r\n\r\n"); + print_uart("\r\nStarting monitoring (3s interval)...\r\n"); + print_uart("Press SHPHLD button to test power state transitions\r\n\r\n"); while (1) { float voltage, current, temp; int32_t chg_status; - struct sensor_value dietemp; + + /* Polling fallback: check SHPHLD event register if interrupt didn't fire */ + uint8_t shphld_event = 0; + if (mfd_npm1300_reg_read(pmic_dev, 0x00, 0x12, &shphld_event) == 0) { + if (shphld_event & 0x01) { /* SHPHLD press event */ + if (!shphld_pressed) { + printk("SHPHLD PRESS (polled)\n"); + shphld_pressed = true; + } + /* Clear the press event */ + mfd_npm1300_reg_write(pmic_dev, 0x00, 0x13, 0x01); + } + if (shphld_event & 0x02) { /* SHPHLD release event - just clear it */ + mfd_npm1300_reg_write(pmic_dev, 0x00, 0x13, 0x02); + } + } + + /* Polling fallback: check VBUS event register if interrupt didn't fire */ + uint8_t vbus_event = 0; + if (mfd_npm1300_reg_read(pmic_dev, 0x00, 0x16, &vbus_event) == 0) { + if (vbus_event & 0x01) { /* VBUS detected event */ + printk("VBUS DETECTED (polled)\n"); + mfd_npm1300_reg_write(pmic_dev, 0x00, 0x17, 0x01); + if (!vbus_changed) { + vbus_changed = true; + vbus_detected = true; + } + } + if (vbus_event & 0x02) { /* VBUS removed event */ + printk("VBUS REMOVED (polled)\n"); + mfd_npm1300_reg_write(pmic_dev, 0x00, 0x17, 0x02); + if (!vbus_changed) { + vbus_changed = true; + vbus_detected = false; + } + } + } + + /* Also check actual VBUS state and correct app_state if mismatched */ + bool actual_vbus = is_vbus_present(); + if (actual_vbus && app_state == POWER_STATE_ON_BATTERY) { + print_uart(">>> State correction: VBUS present but ON_BATTERY -> CHARGING_ON\r\n"); + app_state = POWER_STATE_CHARGING_ON; + } else if (!actual_vbus && app_state == POWER_STATE_CHARGING_ON) { + print_uart(">>> State correction: VBUS absent but CHARGING_ON -> ON_BATTERY\r\n"); + app_state = POWER_STATE_ON_BATTERY; + } + + /* Handle VBUS change detected via polling */ + if (vbus_changed) { + vbus_changed = false; + handle_vbus_change(vbus_detected); + } + + /* Handle button press detected via polling */ + if (shphld_pressed) { + shphld_pressed = false; + handle_power_button(); + } tick++; - snprintk(buf, sizeof(buf), "[%d] ", tick); - print_uart(buf); - ret = read_sensors(&voltage, ¤t, &temp, &chg_status); if (ret < 0) { - snprintk(buf, sizeof(buf), "sensor err=%d\r\n", ret); + snprintk(buf, sizeof(buf), "[%d] sensor err=%d\r\n", tick, ret); print_uart(buf); } else { - /* Get die temperature */ - sensor_channel_get(charger_dev, SENSOR_CHAN_DIE_TEMP, &dietemp); - - /* Update fuel gauge */ + int soc_int = 0, soc_frac = 0; float delta = (float)k_uptime_delta(&ref_time) / 1000.0f; - if (delta < 0.1f) delta = 3.0f; /* Ensure minimum delta on first call */ + if (delta < 0.1f) delta = 3.0f; float soc = nrf_fuel_gauge_process(voltage, current, temp, delta, NULL); - float tte = nrf_fuel_gauge_tte_get(); - - /* Convert floats to integers for printing (avoid float printf issues) */ - int soc_int = (int)soc; - int soc_frac = (int)((soc - soc_int) * 10) % 10; + soc_int = (int)soc; + soc_frac = (int)((soc - soc_int) * 10) % 10; if (soc_frac < 0) soc_frac = -soc_frac; + int v_int = (int)voltage; int v_frac = (int)((voltage - v_int) * 1000); int i_ma = (int)(current * 1000); int t_int = (int)temp; - snprintk(buf, sizeof(buf), - "SoC=%d.%d%% V=%d.%03dV I=%dmA T=%dC die=%dC %s", - soc_int, soc_frac, v_int, v_frac, i_ma, t_int, - dietemp.val1, charge_status_str(chg_status)); - print_uart(buf); - - if (tte > 0 && tte < 1000000) { - snprintk(buf, sizeof(buf), " TTE=%ds", (int)tte); - print_uart(buf); + const char *state_str = "?"; + switch (app_state) { + case POWER_STATE_ON_BATTERY: state_str = "ON_BAT"; break; + case POWER_STATE_CHARGING_ON: state_str = "CHG_ON"; break; + case POWER_STATE_CHARGING_ONLY: state_str = "CHG_ONLY"; break; } - print_uart("\r\n"); + + snprintk(buf, sizeof(buf), + "[%d] %d.%d%% %d.%03dV %dmA %dC %s [%s]\r\n", + tick, soc_int, soc_frac, v_int, v_frac, i_ma, t_int, + charge_status_str(chg_status), state_str); + print_uart(buf); } + /* Normal 3 second polling interval + * Note: CHARGING_ONLY state powers down the device, so we won't reach here in that state + */ k_sleep(K_SECONDS(3)); }