/* * kt_battery.c — 自采集电池电量 * * SDK 的 get_vbat_percent() 在本项目实测不准,改用 KT_CFG_VBAT_DET_PIN(PA12) * 经 100K/100K 分压采集 → ADC → 平均 → 线性映射 4.2V/3.2V → 滞回输出百分比。 * * 锂电池物理特性带来的算法约束: * 1) 充电中,充电 IC 在 CV 阶段会把电池端电压强制维持在 4.2V,无论电池真实 SOC 多少 * 电压计都会读到 ~4.2V,因此充电时**电压完全无法用于估算 SOC**; * 2) 充电刚结束的瞬间,电池存在"极化电压"(电极内离子不平衡导致端电压比 OCV 高 * 0.1~0.3V),需要几秒~几十秒通过自身内阻消退到真实 OCV; * 3) 正常放电时,负载突变(如 PA 工作)瞬时拉低端电压并不代表 SOC 真的下降, * 松开负载后电压会回升,因此需要平均 + 单向滞回避免百分比抖动。 * * 对应的算法处理: * 1) 充电中(vbat_charging=1):锁住电压不参与 SOC 计算,改用"时间法"模拟爬升 * (每 KT_BAT_CHARGE_BUMP_SEC 秒 +1%,封顶 100%),给用户充电进度反馈; * 2) 拔下充电器瞬间:启动 KT_BAT_RECOVERY_MS 的极化消退期,期间继续冻结 %, * 让端电压自然回落到 OCV; * 3) 消退期结束:用此刻电压一次性吸附 %,清空缓冲重新种子化,进入正常滑窗采样; * 4) 正常放电期:16 点滑动平均 + 单向"只许降"滞回 + 1% 门槛,防抖。 * * 串口仍然每次打印瞬时电压,便于观察电压回落情况。 */ #include "kt_battery.h" #include "asm/adc_api.h" #include "system/timer.h" /* 采样周期 ms;周期 × 缓冲长度 = 平滑窗口 */ #define KT_BAT_SAMPLE_MS 200u /* 滑动平均缓冲长度,值越大越稳越慢 */ #define KT_BAT_FILTER_N 16u /* 滞回门槛(百分点),新值与旧值差值需 ≥ 该门槛才采纳 */ #define KT_BAT_HYSTERESIS 1u /* 拔下充电器后的极化消退期(ms):锂电池端电压从充电"极化高电压"回落到真实 OCV * 所需时间,典型几秒~几十秒。实测拔出 8s 左右从 4217 → 3924,用 12s 留余量。 */ #define KT_BAT_RECOVERY_MS 12000u #define KT_BAT_RECOVERY_TICKS ((u8)(KT_BAT_RECOVERY_MS / KT_BAT_SAMPLE_MS)) /* 充电模拟爬升:分两段,贴合锂电池 CC/CV 真实充电速率。 * CC 阶段(电池端电压 < 4.2V,SOC 大致 < 80%):充电 IC 维持设定电流,充得快; * CV 阶段(端电压 = 4.2V,SOC 大致 80~100%):电流随 SOC 上升而下降,充得慢。 * * 本项目 4000mAh / IP5306 默认 0.8A: * CC: (40 mAh/1%) / 800 mA × 3600 ≈ 180 秒/1% → 0~80% 约 4 小时 * CV: 平均电流跌到 ~0.4A,(40 mAh/1%) / 400 mA × 3600 ≈ 360 秒/1% → 80~100% 约 2 小时 * 全程约 6 小时充满 * * 切换阈值取 80%(锂电池典型 CC→CV 切换点)。换电池/换电流时改这三个宏。 */ #define KT_BAT_CC_BUMP_SEC 180u #define KT_BAT_CV_BUMP_SEC 360u #define KT_BAT_CC_CV_THRESHOLD 80u #define KT_BAT_CC_BUMP_TICKS ((u16)(KT_BAT_CC_BUMP_SEC * 1000u / KT_BAT_SAMPLE_MS)) #define KT_BAT_CV_BUMP_TICKS ((u16)(KT_BAT_CV_BUMP_SEC * 1000u / KT_BAT_SAMPLE_MS)) static u16 vbat_buf[KT_BAT_FILTER_N]; static u8 vbat_buf_idx; static u8 vbat_buf_filled; static u16 vbat_avg_mv; static u8 vbat_percent_cached = 100u; static u8 vbat_charging; static u8 vbat_recovery_ticks; /* > 0 表示处于"极化消退期",期间不更新 % */ static u16 vbat_charge_bump_cnt; /* 充电中累计的 200ms tick 数,达到阈值就 +1% */ static u16 vbat_timer_id; static u16 kt_battery_read_raw_mv(void) { /* SDK adc_get_voltage 返回引脚电压 (mV),× 分压系数 = 电池电压 (mV) */ u32 pin_mv = adc_get_voltage(AD_CH_PA12); u32 bat_mv = pin_mv * KT_BAT_DIVIDER_NUM; if (bat_mv > 0xFFFFu) { bat_mv = 0xFFFFu; } return (u16)bat_mv; } static u8 kt_battery_mv_to_percent(u16 mv) { if (mv >= KT_BAT_FULL_MV) { return 100u; } if (mv <= KT_BAT_EMPTY_MV) { return 0u; } return (u8)(((u32)(mv - KT_BAT_EMPTY_MV) * 100u) / (KT_BAT_FULL_MV - KT_BAT_EMPTY_MV)); } /* 用即时电压重新种子化整个滑窗缓冲,后续采样从这个值开始平滑 */ static void kt_battery_reseed(u16 mv) { for (u8 i = 0; i < KT_BAT_FILTER_N; i++) { vbat_buf[i] = mv; } vbat_buf_idx = 0; vbat_buf_filled = 1; vbat_avg_mv = mv; } static void kt_battery_sample_cb(void *priv) { (void)priv; u16 raw = kt_battery_read_raw_mv(); /* 1) 充电中:充电 IC CV 阶段把端电压维持在 4.2V,与真实 SOC 无关。 * 电压不参与计算,改用"时间法"模拟爬升给用户充电进度反馈。 * CC/CV 两段不同速率,贴合锂电池真实充电曲线。 */ if (vbat_charging) { vbat_avg_mv = raw; if (vbat_percent_cached < 100u) { u16 bump_ticks = (vbat_percent_cached < KT_BAT_CC_CV_THRESHOLD) ? KT_BAT_CC_BUMP_TICKS /* CC 阶段:快 */ : KT_BAT_CV_BUMP_TICKS; /* CV 阶段:慢 */ vbat_charge_bump_cnt++; if (vbat_charge_bump_cnt >= bump_ticks) { vbat_charge_bump_cnt = 0; vbat_percent_cached++; printf("kt_battery: charge bump -> %d%% (%s)\n", vbat_percent_cached, (vbat_percent_cached <= KT_BAT_CC_CV_THRESHOLD) ? "CC" : "CV"); } } return; } /* 2) 刚拔下充电器:端电压还在"极化电压"阶段(高于真实 OCV),继续锁住 %, * 等极化消退期结束再用此刻电压一次性吸附到真实 OCV 对应的 SOC */ if (vbat_recovery_ticks > 0) { vbat_recovery_ticks--; vbat_avg_mv = raw; if (vbat_recovery_ticks == 0) { kt_battery_reseed(raw); vbat_percent_cached = kt_battery_mv_to_percent(raw); } return; } /* 3) 正常放电:滑窗平均 + 单向下降滞回 */ vbat_buf[vbat_buf_idx++] = raw; if (vbat_buf_idx >= KT_BAT_FILTER_N) { vbat_buf_idx = 0; vbat_buf_filled = 1; } u8 n = vbat_buf_filled ? (u8)KT_BAT_FILTER_N : vbat_buf_idx; if (n == 0) { return; } u32 sum = 0; for (u8 i = 0; i < n; i++) { sum += vbat_buf[i]; } vbat_avg_mv = (u16)(sum / n); u8 new_p = kt_battery_mv_to_percent(vbat_avg_mv); /* 放电只允许 % 下降:负载突变(如 PA 工作)瞬时电压回弹不会让 % 反弹, * 边界 0% 允许直接吸附,避免卡在 1% */ if (new_p == 0u) { vbat_percent_cached = 0u; } else if ((u16)new_p + KT_BAT_HYSTERESIS <= vbat_percent_cached) { vbat_percent_cached = new_p; } } u8 kt_get_vbat_percent(void) { return vbat_percent_cached; } u16 kt_get_vbat_mv(void) { return vbat_avg_mv; } void kt_set_charging(u8 charging) { u8 was_charging = vbat_charging; vbat_charging = charging ? 1u : 0u; /* 充电 → 不充电 的下降沿:启动极化消退期 */ if (was_charging && !vbat_charging) { vbat_recovery_ticks = KT_BAT_RECOVERY_TICKS; /* 消退期内不参与平均,先把缓冲清掉,消退期结束时重新种子化 */ vbat_buf_idx = 0; vbat_buf_filled = 0; printf("kt_battery: charging->discharging, polarization recovery %dms\n", KT_BAT_RECOVERY_MS); } else if (!was_charging && vbat_charging) { /* 从插上充电那一刻重新计时,而非累计上一次充电的剩余 tick */ vbat_charge_bump_cnt = 0; printf("kt_battery: discharging->charging, freeze at %d%%, bump CC=%ds CV=%ds\n", vbat_percent_cached, KT_BAT_CC_BUMP_SEC, KT_BAT_CV_BUMP_SEC); } } void kt_battery_init(void) { /* 用一次即时采样把环形缓冲全部种子化,避免开机瞬间百分比从 100% 跳到真实值 */ u16 seed = kt_battery_read_raw_mv(); kt_battery_reseed(seed); vbat_recovery_ticks = 0; vbat_charge_bump_cnt = 0; /* 开机就用电压算一个起点 %。开机即在充电时,这个值会偏高(电池端电压被充电 IC * 拉到 CC/CV 阶段的水平),但作为"起点"展示无伤大雅——后续充电模拟爬升会让它 * 单调向 100% 增长,用户看到的就是"开机当前 N%、慢慢涨到 100%"的自然反馈。 * 拔掉充电器后,极化消退期结束会用真实 OCV 重新校准 %。 */ vbat_percent_cached = kt_battery_mv_to_percent(seed); if (gpio_read(KT_CFG_USB_PLUG_DET_PIN)) { vbat_charging = 1u; printf("kt_battery_init: USB inserted at boot, charging from %d%%, seed_mv=%d\n", vbat_percent_cached, vbat_avg_mv); } else { vbat_charging = 0u; printf("kt_battery_init: seed_mv=%d init_percent=%d\n", vbat_avg_mv, vbat_percent_cached); } if (!vbat_timer_id) { vbat_timer_id = sys_timer_add(NULL, kt_battery_sample_cb, KT_BAT_SAMPLE_MS); } }