313 lines
6.5 KiB
C
313 lines
6.5 KiB
C
#include <zephyr/device.h>
|
|
#include <zephyr/drivers/adc.h>
|
|
#include <zephyr/drivers/gpio.h>
|
|
#include <zephyr/init.h>
|
|
#include <zephyr/kernel.h>
|
|
#include <zephyr/logging/log.h>
|
|
|
|
#include <errno.h>
|
|
#include <stdbool.h>
|
|
|
|
#include "batt_mgmt.h"
|
|
#include "usb_mgmt.h"
|
|
|
|
LOG_MODULE_REGISTER(batt_mgmt, CONFIG_BATT_MGMT_LOG_LEVEL);
|
|
|
|
#define BATT_VDDH_DIVIDER_FACTOR 5
|
|
|
|
#define BATT_VDDH_ADC_NODE DT_PATH(zephyr_user)
|
|
static const struct adc_dt_spec batt_vddh_adc = ADC_DT_SPEC_GET_BY_NAME(BATT_VDDH_ADC_NODE, vddh);
|
|
|
|
#define BATT_CHG_STATUS_NODE DT_ALIAS(chg_status)
|
|
static const struct gpio_dt_spec batt_chg_status = GPIO_DT_SPEC_GET(BATT_CHG_STATUS_NODE, gpios);
|
|
|
|
static bool batt_mgmt_ready;
|
|
static int64_t batt_mgmt_last_chg_change_ms;
|
|
static bool batt_mgmt_chg_interrupt_enabled;
|
|
static K_THREAD_STACK_DEFINE(batt_monitor_stack, CONFIG_BATT_MGMT_MONITOR_THREAD_STACK_SIZE);
|
|
static struct k_thread batt_monitor_thread_data;
|
|
|
|
static int batt_mgmt_to_adc_oversampling(uint8_t oversampling, uint8_t *adc_oversampling)
|
|
{
|
|
if (adc_oversampling == NULL)
|
|
{
|
|
return -EINVAL;
|
|
}
|
|
|
|
switch (oversampling)
|
|
{
|
|
case BATT_MGMT_OVERSAMPLING_8X:
|
|
*adc_oversampling = 3U;
|
|
return 0;
|
|
case BATT_MGMT_OVERSAMPLING_16X:
|
|
*adc_oversampling = 4U;
|
|
return 0;
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
|
|
int batt_mgmt_measure_vddh_mv(uint8_t oversampling, int32_t *vddh_mv)
|
|
{
|
|
int rc;
|
|
uint8_t adc_oversampling;
|
|
int16_t raw_sample;
|
|
int32_t input_mv;
|
|
struct adc_sequence sequence = {
|
|
.buffer = &raw_sample,
|
|
.buffer_size = sizeof(raw_sample),
|
|
};
|
|
|
|
if (vddh_mv == NULL)
|
|
{
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (!batt_mgmt_ready)
|
|
{
|
|
return -EAGAIN;
|
|
}
|
|
|
|
rc = batt_mgmt_to_adc_oversampling(oversampling, &adc_oversampling);
|
|
if (rc < 0)
|
|
{
|
|
return rc;
|
|
}
|
|
|
|
rc = adc_sequence_init_dt(&batt_vddh_adc, &sequence);
|
|
if (rc < 0)
|
|
{
|
|
return rc;
|
|
}
|
|
|
|
sequence.oversampling = adc_oversampling;
|
|
|
|
rc = adc_read_dt(&batt_vddh_adc, &sequence);
|
|
if (rc < 0)
|
|
{
|
|
return rc;
|
|
}
|
|
|
|
input_mv = raw_sample;
|
|
rc = adc_raw_to_millivolts_dt(&batt_vddh_adc, &input_mv);
|
|
if (rc < 0)
|
|
{
|
|
return rc;
|
|
}
|
|
|
|
*vddh_mv = input_mv * BATT_VDDH_DIVIDER_FACTOR;
|
|
|
|
LOG_DBG("Battery VDDH: %d mV (raw=%d, os=%ux)", *vddh_mv, raw_sample, oversampling);
|
|
return 0;
|
|
}
|
|
|
|
static void batt_mgmt_chg_interrupt_handler(const struct device *dev,
|
|
struct gpio_callback *cb,
|
|
uint32_t pins)
|
|
{
|
|
int64_t now_ms = k_uptime_get();
|
|
int64_t delta_ms = now_ms - batt_mgmt_last_chg_change_ms;
|
|
|
|
#ifdef CONFIG_BATT_MGMT_BLINK_INTERVAL_LOGGING
|
|
LOG_DBG("CHG interrupt: delta=%lld ms", delta_ms);
|
|
#endif
|
|
|
|
batt_mgmt_last_chg_change_ms = now_ms;
|
|
}
|
|
|
|
static int batt_mgmt_enable_chg_interrupt(void)
|
|
{
|
|
int rc;
|
|
static struct gpio_callback batt_chg_cb;
|
|
|
|
if (batt_mgmt_chg_interrupt_enabled)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if (!gpio_is_ready_dt(&batt_chg_status))
|
|
{
|
|
LOG_ERR("CHG GPIO device not ready");
|
|
return -ENODEV;
|
|
}
|
|
|
|
rc = gpio_pin_configure_dt(&batt_chg_status, GPIO_INPUT);
|
|
if (rc < 0)
|
|
{
|
|
LOG_ERR("Failed to configure CHG GPIO: %d", rc);
|
|
return rc;
|
|
}
|
|
|
|
gpio_init_callback(&batt_chg_cb, batt_mgmt_chg_interrupt_handler, BIT(batt_chg_status.pin));
|
|
rc = gpio_add_callback(batt_chg_status.port, &batt_chg_cb);
|
|
if (rc < 0)
|
|
{
|
|
LOG_ERR("Failed to add CHG callback: %d", rc);
|
|
return rc;
|
|
}
|
|
|
|
rc = gpio_pin_interrupt_configure_dt(&batt_chg_status, GPIO_INT_EDGE_BOTH);
|
|
if (rc < 0)
|
|
{
|
|
LOG_ERR("Failed to configure CHG interrupt: %d", rc);
|
|
return rc;
|
|
}
|
|
|
|
batt_mgmt_chg_interrupt_enabled = true;
|
|
batt_mgmt_last_chg_change_ms = k_uptime_get();
|
|
LOG_INF("CHG interrupt enabled");
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int batt_mgmt_disable_chg_interrupt(void)
|
|
{
|
|
int rc;
|
|
|
|
if (!batt_mgmt_chg_interrupt_enabled)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
rc = gpio_pin_interrupt_configure_dt(&batt_chg_status, GPIO_INT_DISABLE);
|
|
if (rc < 0)
|
|
{
|
|
LOG_WRN("Failed to disable CHG interrupt: %d", rc);
|
|
return rc;
|
|
}
|
|
|
|
rc = gpio_pin_configure(batt_chg_status.port, batt_chg_status.pin, GPIO_DISCONNECTED);
|
|
if (rc < 0)
|
|
{
|
|
LOG_WRN("Failed to disconnect CHG GPIO: %d", rc);
|
|
return rc;
|
|
}
|
|
|
|
batt_mgmt_chg_interrupt_enabled = false;
|
|
LOG_DBG("CHG interrupt disabled");
|
|
|
|
return 0;
|
|
}
|
|
|
|
bool batt_mgmt_get_charger_status(void)
|
|
{
|
|
if (!batt_mgmt_chg_interrupt_enabled)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return gpio_pin_get_dt(&batt_chg_status);
|
|
}
|
|
|
|
batt_mgmt_state_t batt_mgmt_get_battery_state(void)
|
|
{
|
|
int64_t now_ms = k_uptime_get();
|
|
int64_t delta_ms = now_ms - batt_mgmt_last_chg_change_ms;
|
|
bool chg_high = batt_mgmt_get_charger_status();
|
|
bool usb_present = usb_mgmt_is_vbus_present();
|
|
|
|
/* Blinking detection: state change within window */
|
|
if (delta_ms < CONFIG_BATT_MGMT_CHG_BLINKING_WINDOW_MS)
|
|
{
|
|
LOG_DBG("Blinking detected (delta=%lld ms)", delta_ms);
|
|
return BATT_STATE_ERROR;
|
|
}
|
|
|
|
/* Stable states */
|
|
if (chg_high)
|
|
{
|
|
return BATT_STATE_CHARGING;
|
|
}
|
|
|
|
if (usb_present)
|
|
{
|
|
return BATT_STATE_FULL;
|
|
}
|
|
|
|
return BATT_STATE_DISCHARGING;
|
|
}
|
|
|
|
static void batt_mgmt_monitor_thread(void *unused1, void *unused2, void *unused3)
|
|
{
|
|
(void)unused1;
|
|
(void)unused2;
|
|
(void)unused3;
|
|
|
|
if (!usb_mgmt_wait_ready(K_SECONDS(2)))
|
|
{
|
|
LOG_WRN("usb_mgmt not ready after timeout; using current VBUS state");
|
|
}
|
|
|
|
/* Sync initial state in case VBUS was already present before this thread started. */
|
|
bool vbus_present = usb_mgmt_is_vbus_present();
|
|
|
|
if (vbus_present)
|
|
{
|
|
(void)batt_mgmt_enable_chg_interrupt();
|
|
}
|
|
else
|
|
{
|
|
(void)batt_mgmt_disable_chg_interrupt();
|
|
}
|
|
|
|
while (1)
|
|
{
|
|
uint32_t events = k_event_wait(&usb_mgmt_events,
|
|
USB_MGMT_VBUS_CONNECTED | USB_MGMT_VBUS_DISCONNECTED,
|
|
false, K_FOREVER);
|
|
|
|
if (events & USB_MGMT_VBUS_CONNECTED)
|
|
{
|
|
(void)batt_mgmt_enable_chg_interrupt();
|
|
}
|
|
|
|
if (events & USB_MGMT_VBUS_DISCONNECTED)
|
|
{
|
|
(void)batt_mgmt_disable_chg_interrupt();
|
|
}
|
|
|
|
k_event_clear(&usb_mgmt_events, USB_MGMT_VBUS_CONNECTED | USB_MGMT_VBUS_DISCONNECTED);
|
|
}
|
|
}
|
|
|
|
static int batt_mgmt_init(void)
|
|
{
|
|
int32_t boot_vddh_mv;
|
|
int rc;
|
|
|
|
if (!adc_is_ready_dt(&batt_vddh_adc))
|
|
{
|
|
LOG_ERR("VDDH ADC device not ready");
|
|
return -ENODEV;
|
|
}
|
|
|
|
rc = adc_channel_setup_dt(&batt_vddh_adc);
|
|
if (rc < 0)
|
|
{
|
|
LOG_ERR("VDDH ADC channel setup failed: %d", rc);
|
|
return rc;
|
|
}
|
|
|
|
batt_mgmt_ready = true;
|
|
|
|
rc = batt_mgmt_measure_vddh_mv(BATT_MGMT_OVERSAMPLING_16X, &boot_vddh_mv);
|
|
if (rc < 0)
|
|
{
|
|
LOG_WRN("Initial battery measurement failed: %d", rc);
|
|
return 0;
|
|
}
|
|
|
|
LOG_DBG("Initial battery VDDH: %d mV", boot_vddh_mv);
|
|
|
|
k_thread_create(&batt_monitor_thread_data,
|
|
batt_monitor_stack,
|
|
K_THREAD_STACK_SIZEOF(batt_monitor_stack),
|
|
batt_mgmt_monitor_thread,
|
|
NULL, NULL, NULL,
|
|
K_PRIO_COOP(CONFIG_BATT_MGMT_MONITOR_THREAD_PRIORITY),
|
|
0, K_NO_WAIT);
|
|
|
|
return 0;
|
|
}
|
|
|
|
SYS_INIT(batt_mgmt_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); |