diff --git a/.gitignore b/.gitignore index a5309e6..04ea576 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ build*/ +external*/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 81e8c06..8969bc3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,9 @@ cmake_minimum_required(VERSION 3.20.0) + +if(EXISTS "E:/extra/modules/ip5305") + list(APPEND ZEPHYR_EXTRA_MODULES "E:/extra/modules/ip5305") +endif() + find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(new_kbd) @@ -8,7 +13,9 @@ zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/events) target_sources(app PRIVATE src/main.c + src/events/battery_status_event.c src/events/mode_event.c + src/modules/battery_module.c src/modules/button_map_module.c src/modules/mode_switch_module.c ) diff --git a/app.overlay b/app.overlay index 21872e9..f57dfdb 100644 --- a/app.overlay +++ b/app.overlay @@ -1,5 +1,8 @@ +#include + / { zephyr,user { + vbat-en-gpios = <&gpio0 9 GPIO_ACTIVE_HIGH>; io-channels = <&adc 5>, <&adc 7>; }; }; @@ -24,3 +27,13 @@ &adc { status = "okay"; }; + +&i2c1 { + status = "okay"; + + ip5305: pmic@75 { + status = "okay"; + /* 试验项:调整 IP5305 KEY 保活周期,观察 I2C 失败窗口是否随周期移动。 */ + keepalive-interval-ms = <10000>; + }; +}; diff --git a/prj.conf b/prj.conf index eef80bc..94bc21f 100644 --- a/prj.conf +++ b/prj.conf @@ -12,6 +12,7 @@ CONFIG_CAF_POWER_MANAGER=y CONFIG_CAF_POWER_MANAGER_TIMEOUT=20 CONFIG_CAF_POWER_MANAGER_ERROR_TIMEOUT=10 CONFIG_REBOOT=y +CONFIG_CAF_KEEP_ALIVE_EVENTS=y CONFIG_CAF_BUTTONS=y CONFIG_CAF_BUTTONS_DEF_PATH="buttons_def.h" @@ -19,3 +20,5 @@ CONFIG_CAF_BUTTONS_SCAN_INTERVAL=5 CONFIG_CAF_BUTTONS_DEBOUNCE_INTERVAL=10 CONFIG_ADC=y +CONFIG_I2C=y +CONFIG_IP5305=y diff --git a/src/events/battery_status_event.c b/src/events/battery_status_event.c new file mode 100644 index 0000000..984a169 --- /dev/null +++ b/src/events/battery_status_event.c @@ -0,0 +1,34 @@ +#include "battery_status_event.h" + +static void log_battery_status_event(const struct app_event_header *aeh) +{ + const struct battery_status_event *event = cast_battery_status_event(aeh); + + APP_EVENT_MANAGER_LOG(aeh, + "charging=%u full=%u soc=%u", + event->charging, + event->full, + event->soc); +} + +static void profile_battery_status_event(struct log_event_buf *buf, + const struct app_event_header *aeh) +{ + const struct battery_status_event *event = cast_battery_status_event(aeh); + + nrf_profiler_log_encode_uint8(buf, event->charging ? 1U : 0U); + nrf_profiler_log_encode_uint8(buf, event->full ? 1U : 0U); + nrf_profiler_log_encode_uint8(buf, event->soc); +} + +APP_EVENT_INFO_DEFINE(battery_status_event, + ENCODE(NRF_PROFILER_ARG_U8, + NRF_PROFILER_ARG_U8, + NRF_PROFILER_ARG_U8), + ENCODE("charging", "full", "soc"), + profile_battery_status_event); + +APP_EVENT_TYPE_DEFINE(battery_status_event, + log_battery_status_event, + &battery_status_event_info, + APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE)); diff --git a/src/events/battery_status_event.h b/src/events/battery_status_event.h new file mode 100644 index 0000000..b41174e --- /dev/null +++ b/src/events/battery_status_event.h @@ -0,0 +1,20 @@ +#ifndef BATTERY_STATUS_EVENT_H +#define BATTERY_STATUS_EVENT_H + +#include +#include + +#include +#include + +struct battery_status_event +{ + struct app_event_header header; + bool charging; + bool full; + uint8_t soc; +}; + +APP_EVENT_TYPE_DECLARE(battery_status_event); + +#endif diff --git a/src/modules/battery_module.c b/src/modules/battery_module.c new file mode 100644 index 0000000..be287e4 --- /dev/null +++ b/src/modules/battery_module.c @@ -0,0 +1,345 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define MODULE battery +#include + +#include "battery_status_event.h" + +#include +LOG_MODULE_REGISTER(MODULE); + +#define BATTERY_USER_NODE DT_PATH(zephyr_user) +#define BATTERY_ADC_IO_CH_IDX 1 +#define BATTERY_SAMPLE_INTERVAL_MS 1000 +#define BATTERY_VDIV_NUM 2 +#define BATTERY_VDIV_DEN 1 +#define BATTERY_MV_WINDOW_SIZE 10 + +/* + * SOC 估算策略: + * 这里采用线性估算,默认把 3.30V 映射为 0%,4.20V 映射为 100%。 + * 若后续实测曲线和电压分压比例不同,只需调整两个阈值即可。 + */ +#define BATTERY_EMPTY_MV 3300 +#define BATTERY_FULL_MV 4100 + +static const struct adc_dt_spec battery_adc = + ADC_DT_SPEC_GET_BY_IDX(BATTERY_USER_NODE, BATTERY_ADC_IO_CH_IDX); + +/* + * 电池采样使能脚从 zephyr,user/vbat-en-gpios 读取,避免把引脚号硬编码在 C 里。 + * 后续板级改脚位只需改 DTS,不需要改固件代码。 + */ +static const struct gpio_dt_spec battery_en_gpio = + GPIO_DT_SPEC_GET(BATTERY_USER_NODE, vbat_en_gpios); + +static const struct device *const ip5305_dev = DEVICE_DT_GET(DT_NODELABEL(ip5305)); + +struct battery_status +{ + bool charging; + bool full; + uint8_t soc; +}; + +static struct k_work_delayable battery_sample_work; +static int16_t battery_adc_sample_buffer; +static atomic_t active; +static struct battery_status last_status; +static bool has_last_status; +static int32_t battery_mv_window[BATTERY_MV_WINDOW_SIZE]; +static int64_t battery_mv_sum; +static size_t battery_mv_count; +static size_t battery_mv_index; + +static void battery_module_resume(void); + +static uint8_t soc_from_mv(int32_t mv) +{ + if (mv <= BATTERY_EMPTY_MV) + { + return 0; + } + + if (mv >= BATTERY_FULL_MV) + { + return 100; + } + + int32_t soc = ((mv - BATTERY_EMPTY_MV) * 100) / (BATTERY_FULL_MV - BATTERY_EMPTY_MV); + return (uint8_t)CLAMP(soc, 0, 100); +} + +static int adc_sample_once_mv(int32_t *mv) +{ + struct adc_sequence sequence = {0}; + int err = adc_sequence_init_dt(&battery_adc, &sequence); + if (err) + { + return err; + } + + sequence.buffer = &battery_adc_sample_buffer; + sequence.buffer_size = sizeof(battery_adc_sample_buffer); + + err = adc_read_dt(&battery_adc, &sequence); + if (err) + { + LOG_WRN("adc_read_dt failed (err=%d)", err); + return err; + } + + *mv = battery_adc_sample_buffer; + err = adc_raw_to_millivolts_dt(&battery_adc, mv); + if (err) + { + LOG_WRN("adc_raw_to_millivolts_dt failed (err=%d raw=%d)", + err, battery_adc_sample_buffer); + } + + return err; +} + +static int read_battery_mv(int32_t *mv) +{ + int err; + int32_t sensed_mv; + + err = adc_sample_once_mv(&sensed_mv); + if (err) + { + return err; + } + + /* + * 板级电池检测存在分压,ADC 读到的是分压后电压。 + * 这里按分压比还原电池端真实电压,供 SOC 估算与事件上报使用。 + */ + int32_t battery_mv = (sensed_mv * BATTERY_VDIV_NUM) / BATTERY_VDIV_DEN; + + /* + * 使用固定窗口平均抑制采样抖动。 + * 窗口未填满前按当前样本数求平均,填满后使用环形缓冲滚动更新。 + */ + if (battery_mv_count < BATTERY_MV_WINDOW_SIZE) + { + battery_mv_window[battery_mv_index] = battery_mv; + battery_mv_sum += battery_mv; + battery_mv_count++; + } + else + { + battery_mv_sum -= battery_mv_window[battery_mv_index]; + battery_mv_window[battery_mv_index] = battery_mv; + battery_mv_sum += battery_mv; + } + + battery_mv_index = (battery_mv_index + 1U) % BATTERY_MV_WINDOW_SIZE; + *mv = (int32_t)(battery_mv_sum / (int64_t)battery_mv_count); + + return 0; +} + +static int read_battery_status(struct battery_status *status) +{ + int err; + int32_t mv; + + err = ip5305_is_charging(ip5305_dev, &status->charging); + if (err) + { + return err; + } + + err = ip5305_is_charge_full(ip5305_dev, &status->full); + if (err) + { + return err; + } + + err = read_battery_mv(&mv); + if (err) + { + LOG_WRN("read_battery_mv failed (err=%d)", err); + return err; + } + + status->soc = soc_from_mv(mv); + return 0; +} + +static void publish_battery_status_event(const struct battery_status *status) +{ + struct battery_status_event *event = new_battery_status_event(); + + event->charging = status->charging; + event->full = status->full; + event->soc = status->soc; + + APP_EVENT_SUBMIT(event); +} + +static void battery_sample_fn(struct k_work *work) +{ + ARG_UNUSED(work); + + if (!atomic_get(&active)) + { + return; + } + + + struct battery_status sampled; + int err = read_battery_status(&sampled); + if (err) + { + goto out_reschedule; + } + + /* + * 仅在状态发生变化时上报,避免重复事件淹没总线。 + * 变化条件:充电标志、满电标志、SOC 任意一个变化。 + */ + if (!has_last_status || + (sampled.charging != last_status.charging) || + (sampled.full != last_status.full) || + (sampled.soc != last_status.soc)) + { + last_status = sampled; + has_last_status = true; + publish_battery_status_event(&sampled); + } + +out_reschedule: + k_work_reschedule(&battery_sample_work, K_MSEC(BATTERY_SAMPLE_INTERVAL_MS)); +} + +static int battery_module_init(void) +{ + if (!device_is_ready(ip5305_dev)) + { + LOG_ERR("IP5305 device not ready"); + return -ENODEV; + } + + if (!adc_is_ready_dt(&battery_adc)) + { + LOG_ERR("Battery ADC device not ready"); + return -ENODEV; + } + + if (!device_is_ready(battery_en_gpio.port)) + { + LOG_ERR("Battery EN GPIO device not ready"); + return -ENODEV; + } + + int err = gpio_pin_configure_dt(&battery_en_gpio, GPIO_OUTPUT_INACTIVE); + if (err) + { + LOG_ERR("Battery EN GPIO configure failed (err=%d)", err); + return err; + } + + err = adc_channel_setup_dt(&battery_adc); + if (err) + { + LOG_ERR("Battery ADC channel setup failed (err=%d)", err); + return err; + } + + /* + * 按需求将最深电源级别限制在 SUSPENDED,禁止进入 OFF。 + * 这样即使 CPU 进入挂起,IP5305 的硬件保活后端仍可持续输出保活脉冲。 + */ + power_manager_restrict(MODULE_IDX(MODULE), POWER_MANAGER_LEVEL_SUSPENDED); + + k_work_init_delayable(&battery_sample_work, battery_sample_fn); + has_last_status = false; + battery_mv_sum = 0; + battery_mv_count = 0; + battery_mv_index = 0; + atomic_set(&active, false); + + atomic_set(&active, true); + (void)gpio_pin_set_dt(&battery_en_gpio, 1); + k_work_reschedule(&battery_sample_work, K_NO_WAIT); + + return 0; +} + +static void battery_module_suspend(void) +{ + atomic_set(&active, false); + (void)k_work_cancel_delayable(&battery_sample_work); + (void)gpio_pin_set_dt(&battery_en_gpio, 0); + module_set_state(MODULE_STATE_STANDBY); +} + +static void battery_module_resume(void) +{ + if (atomic_get(&active)) + { + return; + } + + atomic_set(&active, true); + (void)gpio_pin_set_dt(&battery_en_gpio, 1); + k_work_reschedule(&battery_sample_work, K_NO_WAIT); + module_set_state(MODULE_STATE_READY); +} + +static bool app_event_handler(const struct app_event_header *aeh) +{ + if (is_module_state_event(aeh)) + { + const struct module_state_event *event = cast_module_state_event(aeh); + + if (check_state(event, MODULE_ID(main), MODULE_STATE_READY)) + { + int err = battery_module_init(); + if (err) + { + module_set_state(MODULE_STATE_ERROR); + } + else + { + module_set_state(MODULE_STATE_READY); + } + } + + return false; + } + + if (is_power_down_event(aeh)) + { + battery_module_suspend(); + return false; + } + + if (is_wake_up_event(aeh)) + { + battery_module_resume(); + return false; + } + + __ASSERT_NO_MSG(false); + return false; +} + +APP_EVENT_LISTENER(MODULE, app_event_handler); +APP_EVENT_SUBSCRIBE(MODULE, module_state_event); +APP_EVENT_SUBSCRIBE_EARLY(MODULE, power_down_event); +APP_EVENT_SUBSCRIBE(MODULE, wake_up_event);