make switching on and off on battery possible

This commit is contained in:
2026-02-04 14:33:10 +01:00
parent 43490bf3a7
commit 752287a1df

View File

@@ -8,6 +8,9 @@
#include <zephyr/drivers/uart.h> #include <zephyr/drivers/uart.h>
#include <zephyr/drivers/sensor.h> #include <zephyr/drivers/sensor.h>
#include <zephyr/drivers/sensor/npm1300_charger.h> #include <zephyr/drivers/sensor/npm1300_charger.h>
#include <zephyr/drivers/mfd/npm1300.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/regulator.h>
#include <nrf_fuel_gauge.h> #include <nrf_fuel_gauge.h>
#include <string.h> #include <string.h>
@@ -17,8 +20,28 @@
static const struct device *const uart_dev = DEVICE_DT_GET(UART_DEVICE_NODE); static const struct device *const uart_dev = DEVICE_DT_GET(UART_DEVICE_NODE);
static const struct device *charger_dev; static const struct device *charger_dev;
static const struct device *pmic_dev;
static const struct device *regulators_dev;
static int64_t ref_time; 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 */ /* Battery model from SDK sample */
static const struct battery_model battery_model = { static const struct battery_model battery_model = {
#include "battery_model.inc" #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); sensor_channel_get(charger_dev, SENSOR_CHAN_GAUGE_VOLTAGE, &value);
*voltage = (float)value.val1 + ((float)value.val2 / 1000000); *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); sensor_channel_get(charger_dev, SENSOR_CHAN_DIE_TEMP, &value);
*temp = (float)value.val1 + ((float)value.val2 / 1000000); *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); sensor_channel_get(charger_dev, SENSOR_CHAN_GAUGE_AVG_CURRENT, &value);
*current = (float)value.val1 + ((float)value.val2 / 1000000); *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"; 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) static void print_init_config(void)
{ {
char buf[MSG_SIZE]; char buf[MSG_SIZE];
@@ -82,8 +307,7 @@ static void print_init_config(void)
print_uart("\r\n=== nPM1300 Configuration ===\r\n"); print_uart("\r\n=== nPM1300 Configuration ===\r\n");
/* DTS Config */ snprintk(buf, sizeof(buf), "Battery capacity: %.0f mAh\r\n",
snprintk(buf, sizeof(buf), "Battery capacity: %.0f mAh (configured)\r\n",
(double)BATTERY_CAPACITY_MAH); (double)BATTERY_CAPACITY_MAH);
print_uart(buf); print_uart(buf);
@@ -95,22 +319,9 @@ static void print_init_config(void)
DT_PROP(DT_NODELABEL(charger), current_microamp) / 1000); DT_PROP(DT_NODELABEL(charger), current_microamp) / 1000);
print_uart(buf); 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) { if (sensor_sample_fetch(charger_dev) == 0) {
sensor_channel_get(charger_dev, SENSOR_CHAN_GAUGE_DESIRED_CHARGING_CURRENT, &val); 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); val.val1 * 1000 + val.val2 / 1000);
print_uart(buf); 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); snprintk(buf, sizeof(buf), "nRF Fuel Gauge version: %s\r\n", nrf_fuel_gauge_version);
print_uart(buf); print_uart(buf);
/* Read initial sensor values */
ret = read_sensors(&parameters.v0, &parameters.i0, &parameters.t0, &chg_status); ret = read_sensors(&parameters.v0, &parameters.i0, &parameters.t0, &chg_status);
if (ret < 0) { if (ret < 0) {
snprintk(buf, sizeof(buf), "Error reading sensors: %d\r\n", ret); 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)); charge_status_str(chg_status));
print_uart(buf); print_uart(buf);
/* Initialize fuel gauge */
ret = nrf_fuel_gauge_init(&parameters, NULL); ret = nrf_fuel_gauge_init(&parameters, NULL);
if (ret < 0) { if (ret < 0) {
snprintk(buf, sizeof(buf), "Fuel gauge init error: %d\r\n", ret); 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; return ret;
} }
/* Set charge current limit info */
struct sensor_value val; struct sensor_value val;
sensor_channel_get(charger_dev, SENSOR_CHAN_GAUGE_DESIRED_CHARGING_CURRENT, &val); sensor_channel_get(charger_dev, SENSOR_CHAN_GAUGE_DESIRED_CHARGING_CURRENT, &val);
max_charge_current = (float)val.val1 + ((float)val.val2 / 1000000); 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, 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}); &(union nrf_fuel_gauge_ext_state_info_data){.charge_term_current = term_charge_current});
print_uart("Fuel gauge initialized OK\r\n"); print_uart("Fuel gauge: OK\r\n");
snprintk(buf, sizeof(buf), "NOTE: Using SDK example model, capacity=%.0f mAh\r\n",
(double)BATTERY_CAPACITY_MAH);
print_uart(buf);
ref_time = k_uptime_get(); ref_time = k_uptime_get();
return 0; return 0;
@@ -192,7 +397,7 @@ int main(void)
return 0; 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)); charger_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(charger));
if (charger_dev == NULL || !device_is_ready(charger_dev)) { if (charger_dev == NULL || !device_is_ready(charger_dev)) {
@@ -201,6 +406,11 @@ int main(void)
} }
print_uart("Charger device: OK\r\n"); 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(); print_init_config();
ret = fuel_gauge_init_custom(); 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("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) { while (1) {
float voltage, current, temp; float voltage, current, temp;
int32_t chg_status; 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++; tick++;
snprintk(buf, sizeof(buf), "[%d] ", tick);
print_uart(buf);
ret = read_sensors(&voltage, &current, &temp, &chg_status); ret = read_sensors(&voltage, &current, &temp, &chg_status);
if (ret < 0) { 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); print_uart(buf);
} else { } else {
/* Get die temperature */ int soc_int = 0, soc_frac = 0;
sensor_channel_get(charger_dev, SENSOR_CHAN_DIE_TEMP, &dietemp);
/* Update fuel gauge */
float delta = (float)k_uptime_delta(&ref_time) / 1000.0f; 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 soc = nrf_fuel_gauge_process(voltage, current, temp, delta, NULL);
float tte = nrf_fuel_gauge_tte_get(); soc_int = (int)soc;
soc_frac = (int)((soc - soc_int) * 10) % 10;
/* Convert floats to integers for printing (avoid float printf issues) */
int soc_int = (int)soc;
int soc_frac = (int)((soc - soc_int) * 10) % 10;
if (soc_frac < 0) soc_frac = -soc_frac; if (soc_frac < 0) soc_frac = -soc_frac;
int v_int = (int)voltage; int v_int = (int)voltage;
int v_frac = (int)((voltage - v_int) * 1000); int v_frac = (int)((voltage - v_int) * 1000);
int i_ma = (int)(current * 1000); int i_ma = (int)(current * 1000);
int t_int = (int)temp; int t_int = (int)temp;
snprintk(buf, sizeof(buf), const char *state_str = "?";
"SoC=%d.%d%% V=%d.%03dV I=%dmA T=%dC die=%dC %s", switch (app_state) {
soc_int, soc_frac, v_int, v_frac, i_ma, t_int, case POWER_STATE_ON_BATTERY: state_str = "ON_BAT"; break;
dietemp.val1, charge_status_str(chg_status)); case POWER_STATE_CHARGING_ON: state_str = "CHG_ON"; break;
print_uart(buf); case POWER_STATE_CHARGING_ONLY: state_str = "CHG_ONLY"; break;
if (tte > 0 && tte < 1000000) {
snprintk(buf, sizeof(buf), " TTE=%ds", (int)tte);
print_uart(buf);
} }
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)); k_sleep(K_SECONDS(3));
} }