KT25-1015_AC695x_SDK310/apps/kaotings/kt_battery.c
2026-05-08 22:12:19 +08:00

323 lines
12 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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"
#include "user_cfg_id.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))
/* VM 写入策略: 百分比变化至少 2%,且间隔至少 10 分钟 */
#define KT_BAT_VM_SAVE_DELTA_PERCENT 2u
#define KT_BAT_VM_SAVE_MIN_SEC 600u
#define KT_BAT_VM_SAVE_MIN_TICKS ((u16)(KT_BAT_VM_SAVE_MIN_SEC * 1000u / KT_BAT_SAMPLE_MS))
/* 首次无历史记录时,插电开机随机起点 65~75% */
#define KT_BAT_BOOT_FALLBACK_MIN 65u
#define KT_BAT_BOOT_FALLBACK_MAX 75u
/* VM 数据校验 */
#define KT_BAT_VM_TAG 0xA5u
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_vm_save_ticks;
static u8 vbat_vm_last_saved_percent = 0xFFu;
static u16 vbat_timer_id;
struct kt_bat_vm_record {
u8 tag;
u8 percent;
u8 checksum;
};
static u8 kt_battery_vm_checksum(u8 tag, u8 percent)
{
return (u8)(tag ^ percent ^ 0x5Au);
}
static u8 kt_battery_random_percent_fallback(void)
{
u8 rnd = 0;
get_random_number(&rnd, 1);
return (u8)(KT_BAT_BOOT_FALLBACK_MIN
+ (rnd % (KT_BAT_BOOT_FALLBACK_MAX - KT_BAT_BOOT_FALLBACK_MIN + 1u)));
}
static u8 kt_battery_vm_load_percent(u8 *percent)
{
struct kt_bat_vm_record rec;
if (!percent) {
return 0u;
}
if (syscfg_read(VM_KT_BAT_LAST_PERCENT, &rec, sizeof(rec)) != sizeof(rec)) {
return 0u;
}
if (rec.tag != KT_BAT_VM_TAG) {
return 0u;
}
if (rec.checksum != kt_battery_vm_checksum(rec.tag, rec.percent)) {
return 0u;
}
if ((rec.percent == 0u) || (rec.percent >= 100u)) {
return 0u;
}
*percent = rec.percent;
return 1u;
}
static void kt_battery_vm_save_percent(u8 percent)
{
struct kt_bat_vm_record rec;
if ((percent == 0u) || (percent >= 100u)) {
return;
}
rec.tag = KT_BAT_VM_TAG;
rec.percent = percent;
rec.checksum = kt_battery_vm_checksum(rec.tag, rec.percent);
if (syscfg_write(VM_KT_BAT_LAST_PERCENT, &rec, sizeof(rec)) == sizeof(rec)) {
vbat_vm_last_saved_percent = percent;
vbat_vm_save_ticks = 0;
printf("kt_battery: vm save percent=%d\n", percent);
}
}
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);
/* 恢复期结束后从 0 重新计时,避免立刻写 VM */
vbat_vm_save_ticks = 0;
}
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;
}
if (vbat_vm_save_ticks < 0xFFFFu) {
vbat_vm_save_ticks++;
}
if ((vbat_percent_cached > 0u)
&& (vbat_percent_cached < 100u)
&& (vbat_vm_last_saved_percent != 0xFFu)
&& ((u8)((vbat_vm_last_saved_percent > vbat_percent_cached)
? (vbat_vm_last_saved_percent - vbat_percent_cached)
: (vbat_percent_cached - vbat_vm_last_saved_percent)) >= KT_BAT_VM_SAVE_DELTA_PERCENT)
&& (vbat_vm_save_ticks >= KT_BAT_VM_SAVE_MIN_TICKS)) {
kt_battery_vm_save_percent(vbat_percent_cached);
}
}
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)
{
u8 vm_percent = 0;
u8 have_vm_percent = kt_battery_vm_load_percent(&vm_percent);
/* 用一次即时采样把环形缓冲全部种子化,避免开机瞬间百分比从 100% 跳到真实值 */
u16 seed = kt_battery_read_raw_mv();
kt_battery_reseed(seed);
vbat_recovery_ticks = 0;
vbat_charge_bump_cnt = 0;
vbat_vm_save_ticks = 0;
if (gpio_read(KT_CFG_USB_PLUG_DET_PIN)) {
vbat_charging = 1u;
if (have_vm_percent) {
vbat_percent_cached = vm_percent;
printf("kt_battery_init: USB inserted, use vm percent=%d%%, seed_mv=%d\n",
vbat_percent_cached, vbat_avg_mv);
} else {
vbat_percent_cached = kt_battery_random_percent_fallback();
printf("kt_battery_init: USB inserted, vm empty, random percent=%d%%, seed_mv=%d\n",
vbat_percent_cached, vbat_avg_mv);
}
vbat_vm_last_saved_percent = vbat_percent_cached;
printf("kt_battery_init: charging from %d%%\n", vbat_percent_cached);
} else {
vbat_charging = 0u;
vbat_percent_cached = kt_battery_mv_to_percent(seed);
vbat_vm_last_saved_percent = vbat_percent_cached;
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);
}
}