电量计–77561(77226)的Firmware Architecture Part1 总体结构概述 Keil MDK项目结构 项目基于ARM Cortex-M0 MCU,使用Keil MDK开发环境构建。核心代码分为底层驱动、算法库和应用三层结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 - newton.uvprojx # Keil项目主文件 - newton.sct # 链接脚本 - Device/ # ARM M0 MCU设备相关文件 - RTE/ # 运行时环境配置 - lib/ # 核心算法库文件 - lib_fg.c/h # 电量计核心算法库(Fuel Gauge) - lib_pg.c/h # 电池包电量计算法库(Pack Gauge) - user/ # 用户应用代码 - main.c # 主程序入口 - parameter.c/h # 参数配置 - sbsd.c/h # SBS通信协议实现 - filter.c/h # 数据滤波处理 - db_print.c/h # 调试打印功能 - flash/ # Flash操作相关 - chip/ # 芯片驱动层 - table/ # 查找表数据 - o2bootloader/ # 引导加载程序
启动和运行主流程 启动流程
上电后从o2bootloader引导区启动
初始化MCU系统时钟和基本外设
初始化电量计参数和查找表
调用fg_init函数初始化电量计算法库
进入主循环
主循环流程 1 2 3 4 5 6 7 [上电] → [初始化] → [主循环{ 读取电池数据(电压/电流/温度) 处理电量计算法 更新SOC和其他电池状态 处理SBS通信请求 进入低功耗状态 }]
主程序采用状态机设计,根据不同的电池状态(充电/放电/空闲)调用不同的处理函数。
模块关系和流程图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 [硬件层] │ ├── MCU外设(ADC/I2C/Flash) │ │ │ ▼ ├── 驱动层(chip/) │ │ │ ▼ [软件层] │ ├── 电量计算法核心(lib_fg) ◄────┐ │ │ │ │ ▼ │ ├── 电池包管理(lib_pg) │ │ │ │ │ ▼ │ ├── 应用层(user/main) │ │ │ │ │ ▼ │ ├── SBS通信层(sbsd) │ │ │ │ │ ▼ │ [数据层] │ │ │ └── 查找表(table/) ─────────────┘
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 读取原始数据(电压/电流/温度) │ ▼ 数据过滤(filter.c) │ ▼ 状态判断(充电/放电/空闲) │ ┌───────────┬────────────┐ ▼ ▼ ▼ 充电状态 放电状态 空闲状态 处理函数 处理函数 处理函数 │ │ │ └───────────┼────────────┘ │ ▼ SOC计算(库伦积分+OCV校正) │ ▼ 电量计状态更新 │ ▼ SBS数据更新
I2C SBS通信命令 SBS(Smart Battery System)通信是电量计与主机通信的标准协议。该项目中在sbsd.c/h文件实现了SBS标准通信命令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 命令码 | 命令名称 | 描述 | |-------|---------|------| | 0x00 | ManufacturerAccess | 制造商访问 | | 0x01 | RemainingCapacityAlarm | 剩余容量警报 | | 0x02 | RemainingTimeAlarm | 剩余时间警报 | | 0x03 | BatteryMode | 电池模式 | | 0x04 | Temperature | 温度 | | 0x05 | Voltage | 电压 | | 0x06 | Current | 电流 | | 0x07 | AverageCurrent | 平均电流 | | 0x08 | MaxError | 最大误差 | | 0x09 | RelativeStateOfCharge | 相对电量百分比 | | 0x0A | AbsoluteStateOfCharge | 绝对电量百分比 | | 0x0B | RemainingCapacity | 剩余容量 | | 0x0C | FullChargeCapacity | 满充容量 | | 0x0D | RunTimeToEmpty | 运行剩余时间 | | 0x0E | AverageTimeToEmpty | 平均剩余时间 | | 0x0F | AverageTimeToFull | 平均充满时间 | | 0x10 | ChargingCurrent | 充电电流 | | 0x11 | ChargingVoltage | 充电电压 | | 0x12 | BatteryStatus | 电池状态 | | 0x13 | CycleCount | 循环次数 | | 0x14 | DesignCapacity | 设计容量 | | 0x15 | DesignVoltage | 设计电压 | | 0x16 | SpecificationInfo | 规格信息 | | 0x17 | ManufactureDate | 生产日期 | | 0x18 | SerialNumber | 序列号 | | 0x19 | ManufacturerName | 制造商名称 | | 0x1A | DeviceName | 设备名称 | | 0x1B | DeviceChemistry | 电池化学成分 | | 0x1C | ManufacturerData | 制造商数据 | | 0x3C-0x3F | GGMEM0-GGMEM8 | 调试内存区域 |
此外,项目还实现了一些扩展命令,用于调试和配置电量计算法参数。
电量计算法fg_lib分析 核心数据结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 typedef *struct* { // 电量计状态 *int* status; // 电量计状态标志位 // 容量相关 *int* fcc; // 满充容量(Full Charge Capacity) *int* facc; // 满充绝对容量(Full Absolute Charge Capacity) *int* rca; // 剩余容量(Remaining Capacity) *int* chgcap; // 充电容量 // SOC相关 *int* soc; // 当前SOC百分比(0-10000,对应0-100%) *int* soc_raw; // 原始SOC *int* soc_now; // 实时SOC *int* rsoc_now; // 相对SOC // 电池参数 *int* cell_temp; // 电池温度 *int* cell_volt; // 电池电压 *int* cell_curr; // 电池电流 // 算法参数 *int* parm_eocmv; // 充电截止电压 *int* parm_eocma; // 充电截止电流 *int* parm_eodmv; // 放电截止电压 // 时间记录 *unsigned* *int* chgtime; // 充电时间 *unsigned* *int* dsgtime; // 放电时间 *unsigned* *int* idletime; // 空闲时间 // 表格数据 FG_LUT_T fg_tbls; // 查找表结构 // 滤波相关 *int* cavgmov; // 移动平均电流 *int* cavgcntr[4]; // 电流滤波器 } LIB_FG_TYPE_T;
算法核心模块 电量计状态机 1 2 3 4 5 6 7 8 9 10 11 12 电量计使用状态机管理不同的工作状态: \#define FG_STAT_INIT 0x00000001 // 初始化状态 \#define FG_STAT_CHG 0x00000002 // 充电状态 \#define FG_STAT_DSG 0x00000004 // 放电状态 \#define FG_STAT_IDLE 0x00000008 // 空闲状态 \#define FG_STAT_CHG_CC 0x00000010 // 恒流充电 \#define FG_STAT_CHG_CV 0x00000020 // 恒压充电 \#define FG_STAT_CHG_EOC 0x00000040 // 充电截止 \#define FG_STAT_DSG_EOD 0x00000080 // 放电截止 \#define FG_STAT_DSG_FST1 0x00000100 // 快速放电1 \#define FG_STAT_DSG_FST2 0x00000200 // 快速放电2 \#define FG_STAT_DSG_LT 0x00000400 // 低温放电
SOC计算核心 SOC (State of Charge) 计算采用混合算法:
库伦积分法
:根据电流积分计算电量变化
1 2 3 4 // 电流积分计算 delta_capacity = (current * delta_time) / 3600; // 单位:mAh rca = rca - delta_capacity; // 更新剩余容量 soc = (rca * 10000) / fcc; // 计算SOC(0-10000)
OCV校准
:通过电压查表获取SOC
1 2 3 4 5 6 // 通过OCV查表获取SOC FG_ERROR_T fg_get_soc_by_ocv(FG_HANDLE_T **handle*, *short* *ocv_mv*, *short* **soc*) { // 通过一维表查找获取SOC return lut_one_axis_r(handle, &(((LIB_FG_TYPE_T *)handle)->fg_tbls.ocv_tbl), ocv_mv, soc); }
混合算法
:结合库伦积分和OCV校准
1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 空闲状态下的OCV校准 if (idle_time > OCV_RELAX_TIME) { // 获取OCV对应的SOC fg_get_soc_by_ocv(handle, ocv, &ocv_soc); // 计算偏差 delta = ocv_soc - current_soc; // 如果偏差超过阈值,进行校准 if (abs(delta) > OCV_DELTA_THRESHOLD) { // 使用滤波平滑过渡 current_soc = current_soc + (delta * OCV_FILTER_FACTOR) / 100; } }
温度补偿 温度对电池容量和电压有显著影响,算法中实现了温度补偿:
1 2 3 4 5 6 7 8 9 // 温度补偿 FG_ERROR_T fg_get_temp_factor(FG_HANDLE_T **handle*, *short* *temp*, *short* **factor*) { // 查表获取温度补偿系数 return lut_one_axis_f(handle, &(((LIB_FG_TYPE_T *)handle)->fg_tbls.temp_factor_tbl), temp, factor); } // 应用温度补偿 fcc_temp = (fcc * temp_factor) / 1000; // 温度修正后的满充容量
老化补偿 电池循环次数增加会导致容量衰减,算法实现了老化补偿:
1 2 3 4 5 6 7 8 9 10 11 // 老化因子计算 *int* fg_check_agefactor(FG_HANDLE_T **handle*) { *int* temp = fg_idiv_rounddown(handle, ((LIB_FG_TYPE_T *)handle)->chgcap, ((LIB_FG_TYPE_T *)handle)->facc); temp *= FG_AGING_RATIO_MIN; // 最小损耗 * 循环次数 temp = (10000 - temp) / 100; return temp; } // 应用老化补偿 fcc_aged = (fcc * (10000 - aging_factor)) / 10000; // 老化修正后的满充容量
快速放电处理 在大电流放电或低电压情况下,电池容量会急剧下降,算法实现了快速放电处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static *int* fg_fast_dsg(FG_HANDLE_T **handle*, FG_PARAM_T **param*) { // 根据不同电压档位加速SOC下降速率 if (param->volt_lo_mv < (((LIB_FG_TYPE_T *)handle)->parm_eodmv - ((LIB_FG_TYPE_T *)handle)->parm_ddv - 200)) { // 每秒减少1%容量 ((LIB_FG_TYPE_T *)handle)->deltauah -= fg_idiv_roundup(handle, ((LIB_FG_TYPE_T *)handle)->facc * param->delta_ms, 100); } else if (param->volt_lo_mv < (((LIB_FG_TYPE_T *)handle)->parm_eodmv - ((LIB_FG_TYPE_T *)handle)->parm_ddv - 100)) { // 每5秒减少1%容量 ((LIB_FG_TYPE_T *)handle)->deltauah -= fg_idiv_roundup(handle, ((LIB_FG_TYPE_T *)handle)->facc * param->delta_ms, 500); } else { // 每10秒减少1%容量 ((LIB_FG_TYPE_T *)handle)->deltauah -= fg_idiv_roundup(handle, ((LIB_FG_TYPE_T *)handle)->facc * param->delta_ms, 1000); } // 更新状态 ((LIB_FG_TYPE_T *)handle)->status |= FG_STAT_DSG_FST1; return ERR_FG_NO_ERROR; }
查表算法 算法使用多种查表算法进行插值计算:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // 一维表正向查找 static *int* lut_one_axis_f(FG_HANDLE_T **handle*, *tbl_one_t* **table*, *short* *input*, *short* **output*) { // 通过输入值在表中查找并插值计算输出值 // ... } // 一维表反向查找 static *int* lut_one_axis_r(FG_HANDLE_T **handle*, *tbl_one_t* **table*, *short* *value*, *short* **output*) { // 通过目标值反向查找输入值 // ... } // 三维表查找 static *int* lut_three_axis(FG_HANDLE_T **handle*, *tbl_three_t* **table*, *short* *x_value*, *short* *y_value*, *short* *z_value*, *short* **output*) { // 三维插值计算 // ... }
算法流程概述 初始化流程 fg_init() → 初始化电量计参数和状态 → 加载查找表 → OCV初始化SOC
充电处理流程 检测充电状态 → 更新充电时间 → 库伦积分计算SOC → 判断CC/CV模式 → 检查充电截止条件 → 更新SOC
放电处理流程 检测放电状态 → 更新放电时间 → 库伦积分计算SOC → 温度补偿 → 低电压检测 → 快速放电处理 → 更新SOC
空闲处理流程 检测空闲状态 → 更新空闲时间 → 检查OCV稳定时间 → OCV校准SOC → 更新SOC
低功耗设计 该电量计实现了以下低功耗策略:
工作周期设计:系统大部分时间处于深度睡眠状态,定时唤醒采集数据和处理
外设优化:只在需要时启用ADC、I2C等外设
代码执行效率:优化算法减少指令执行次数
数据存储优化:使用查找表减少计算量
调试和校准工具 根据Cobra工具文档,系统支持以下调试和校准功能:
电流校准:通过Cobra工具校准0电流、正向电流和负向电流
数据采集:通过SBS协议读取电量计内部状态
内存调试:通过GGMEM0-GGMEM8访问电量计内部RAM
参数配置:通过Cobra工具配置电量计算法参数
Part2 Firmware主要功能模块分析 通信接口(SBS命令流程) 以SBS命令0x0D(RSOC - 相对电量)为例,详细介绍整个处理流程,包含main.c、sbs.c和i2c.c三个文件在这个流程中的角色和作用
初始化阶段 (1)main.c中的初始化:
在main.c中:
调用i2cif_init初始化I2C接口,设置从机地址和回调函数sbs_callback_i2c_slave
调用sbsif_init初始化SBS接口
调用gg_lib_init初始化电量计算库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int* main(*void*) { // ...系统初始化 // 初始化I2C接口,设置从机地址和回调函数 i2cif_init(param_board_cfg[PARM_BCFG_I2CADDR], tx_buff, sbs_callback_i2c_slave); // 初始化SBS接口 sbsif_init(param_board_cfg[PARM_BCFG_I2CADDR]); // 初始化电量计模块 gg_lib_init(); // ...其他初始化 // 进入主循环 main_loop(); }
(2)sbs.c中的初始化
初始化SBS接口数据结构
初始化SBS数据缓冲区,包括SBS0D_RSOC(相对电量)的初始值
1 2 3 4 5 6 7 8 9 10 11 12 void* sbsif_init(*uint32_t* *slv_addr*) { psbsifHandle = (SBS_HANDLE_T *)sbs_mem; ((SBSIF_T *)psbsifHandle)->sbs_slv_addr = slv_addr; ((SBSIF_T *)psbsifHandle)->sbs_buff = sbs_data_buff; // ...初始化各种SBS数据 // 初始相对电量为0 ((SBSIF_T *)psbsifHandle)->sbs_buff[SBS0D_RSOC] = 0; }
(2)i2c.c中的初始化
初始化I2C接口数据结构
配置I2C硬件参数
设置回调函数,该函数将在I2C通信事件发生时被调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void* i2cif_init(*uint32_t* *slv_addr*, *uint8_t* **t_buff*, i2c_callback *p_callback*) { i2cif_p = (I2CIF_T *)(i2c_mem); i2cif_p->pI2C_Base = I2C; // I2C模块基地址 i2cif_p->slv_addr = slv_addr; // 从机地址 i2cif_p->tx_buffer = t_buff; // 发送缓冲区 i2cif_p->i2c_callback_f = p_callback; // 回调函数 // ...配置I2C硬件 // 启用中断 Chip_I2C_EnableInts(i2cif_p->pI2C_Base, (...)); Chip_I2C_Enable(i2cif_p->pI2C_Base); }
电量数据更新阶段 (1)main.c中的电量数据更新:
主循环调用gg_step计算最新的电量数据 调用gg_sync_result将电量计算结果同步到SBS数据 调用sbsif_update和sbsif_update_time更新SBS数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void* main_loop() { while (1) { // ...其他循环处理 // 更新电量计算 gg_step(passSec); // 同步电量计结果到SBS gg_sync_result(); // 更新SBS数据 sbsif_update(1); sbsif_update_time(); // ...其他处理 } }
1 2 3 4 5 6 7 static *void* gg_sync_result(*void*) { // 将电量计计算结果同步到SBS数据 sbsif_set_data(SBS0D_RSOC, gg_result.rsoc); // ...更新其他SBS数据 }
(2)sbs.c中的电量数据更新
sbsif_set_data函数允许外部模块(如电量计)直接设置SBS数据 sbsif_update函数基于库仑计数据和满充容量计算相对电量 将计算结果存储在SBS0D_RSOC(相对电量)中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void sbsif_update(uint8_t updata_cc) { // ...更新其他数据 // 更新相对电量 ((SBSIF_T *)psbsifHandle)->sbs_val = ((SBSIF_T *)psbsifHandle)->sbs_buff[SBS5F_CCACCMAH]; ((SBSIF_T *)psbsifHandle)->sbs_val *= 100; ((SBSIF_T *)psbsifHandle)->sbs_val /= ((SBSIF_T *)psbsifHandle)->sbs_buff[SBS10_FCC]; temp1 = ((SBSIF_T *)psbsifHandle)->sbs_val; if (temp1 > 100) temp1 = 100; ((SBSIF_T *)psbsifHandle)->sbs_buff[SBS0E_ASOC] = temp1; ((SBSIF_T *)psbsifHandle)->sbs_buff[SBS0D_RSOC] = ((SBSIF_T *)psbsifHandle)->sbs_buff[SBS0E_ASOC]; }
1 2 3 4 5 6 void sbsif_set_data(SBS_DATA_T index, int32_t ivalue) { if (index < SBSD_CMD_MAX) { sbs_data_buff[index] = ivalue; } }
I2C通信阶段 - 处理SBS 0x0D命令 当上位机发送SBS命令0x0D(读取相对电量)时:
(1)i2c.c中的命令接收
首先接收SBS命令字节(0x0D)并识别它 当上位机发送读请求时,调用回调函数准备要发送的数据 当I2C发送缓冲区需要数据时,发送预先准备好的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 I2C_STATUS_T i2cif_slave_handle(void) { // 地址匹配检测 if (i2cif_p->isr2stat & I2C_ISR2_SLV_ADDR) { // 检查I2C方向(读/写) if (i2cif_p->intstat & I2C_ISR1_SLV_ADDR_DIR) { // 读操作 - 准备发送数据 i2cif_p->i2c_state = I2C_XFER_SLVSND; // ... i2cif_p->i2c_status = I2C_STATUS_TXDATA; if (i2cif_p->i2c_callback_f) { i2cif_p->i2c_callback_f(i2cif_p->i2c_status, i2cif_p->sbd_idx); } } else { // 写操作 - 准备接收命令 i2cif_p->i2c_state = I2C_XFER_SLVRCV; i2cif_p->rx_idx = 0; // ... } } // 接收数据 if (i2cif_p->isr2stat & I2C_ISR2_SLV_RXNE) { i2cif_p->data = Chip_I2C_ReadRXData(i2cif_p->pI2C_Base); // 第一个字节是SBS命令 if (i2cif_p->rx_idx == 0) { i2cif_p->i2c_cmd = (uint8_t)i2cif_p->data; // 0x0D // 查找命令定义 do { if (((sbsd_cmd_def[i2cif_p->sbd_idx] & SBSD_CMD_Msk) >> SBSD_CMD_Pos) == i2cif_p->i2c_cmd) { break; } } while ((i2cif_p->sbd_idx++) < SBSD_CMD_MAX); // ...处理命令 } // ... } // 发送数据 if (i2cif_p->isr2stat & I2C_ISR2_SLV_TXIS && i2cif_p->i2c_state == I2C_XFER_SLVSND) { if (i2cif_p->tx_idx < i2cif_p->tx_size) { if (i2cif_p->tx_buffer) { i2cif_p->data = *i2cif_p->tx_buffer; i2cif_p->tx_buffer++; } } // ... i2cif_p->tx_idx++; Chip_I2C_WriteTXData(i2cif_p->pI2C_Base, i2cif_p->data); } // ... }
(2)main.c中的I2C回调处理
回调函数sbs_callback_i2c_slave处理I2C事件 当接收到状态I2C_STATUS_TXDATA时,准备发送数据 对于命令SBS0D_RSOC,调用sbsif_get_data获取当前相对电量 将数据填充到发送缓冲区 调用i2cif_set_tx通知I2C模块准备发送这些数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 static void sbs_callback_i2c_slave(uint32_t status, uint32_t n) { switch (status) { case I2C_STATUS_TXDATA: // 准备发送数据 switch (n) { case SBS0D_RSOC: // 准备发送相对电量数据 value = sbsif_get_data(SBS0D_RSOC); // 设置发送缓冲区 *tx_ptr++ = (uint8_t)value; *tx_ptr++ = (uint8_t)(value >> 8); // 设置要发送的数据大小 i2cif_set_tx(tx_cnt, tx_buff); break; // ...处理其他SBS命令 } break; // ...处理其他I2C状态 } }
(3)sbs.c中的数据获取
sbsif_get_data函数返回指定SBS命令的当前值 对于SBS0D_RSOC,返回当前存储的相对电量值
1 2 3 4 5 6 7 int32_t sbsif_get_data(SBS_DATA_T index) { if (index >= SBSD_CMD_MAX) return sbs_data_buff[SBS03_BATTMODE]; else return sbs_data_buff[index]; // 返回SBS0D_RSOC的值 }
完整流程图解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 【初始化阶段】 main.c:main() ↓ ├── i2c.c:i2cif_init() → 初始化I2C接口 ├── sbs.c:sbsif_init() → 初始化SBS数据接口 └── main.c:gg_lib_init() → 初始化电量计算库 【电量数据更新阶段 - 周期性执行】 main.c:main_loop() ↓ ├── main.c:gg_step() → 电量计算 ├── main.c:gg_sync_result() → 同步电量计结果 │ └── sbs.c:sbsif_set_data(SBS0D_RSOC, ...) → 更新SBS数据 ├── sbs.c:sbsif_update() → 更新SBS接口数据 └── sbs.c:sbsif_update_time() → 更新时间预测数据 【I2C通信阶段 - 上位机请求时】 上位机发送SBS命令0x0D ↓ i2c.c:I2C_IRQHandler() → I2C中断处理 ↓ i2c.c:i2cif_slave_handle() → 识别命令0x0D ↓ main.c:sbs_callback_i2c_slave() → 处理I2C回调 ↓ sbs.c:sbsif_get_data(SBS0D_RSOC) → 获取当前相对电量值 ↓ main.c:sbs_callback_i2c_slave() → 准备发送数据 ↓ i2c.c:i2cif_set_tx() → 设置发送缓冲区 ↓ i2c.c:i2cif_slave_handle() → 通过I2C发送数据 ↓ 上位机接收相对电量数据
SBS命令的隐式支持 注意sbs_callback_i2c_slave中的default流程,可能隐藏支持一些SBS命令,例如SBS30_CV
每秒轮询更新eocmv之后,存储到sbs buffer的SBS30_CV:
smart_charge_func:
((SBSIF_T *)psbsifHandle)->sbs_buff[SBS30_CV] = eocmv;
((SBSIF_T *)psbsifHandle)->sbs_buff[SBS34_EOC] = eocma;
((SBSIF_T *)psbsifHandle)->sbs_buff和sbs_data_buff指向同一个内存区域:
1 2 3 4 5 6 7 void sbsif_init(uint32_t slv_addr) { psbsifHandle = (SBS_HANDLE_T *)sbs_mem; ((SBSIF_T *)psbsifHandle)->sbs_slv_addr = slv_addr; ((SBSIF_T *)psbsifHandle)->sbs_buff = sbs_data_buff; ... }
Host发SBS30命令查询时,进到sbs_callback_i2c_slave的I2C_STATUS_TXDATA default,拿到sbs_data_buff数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 sbs_callback_i2c_slave: //命令返回数据大小 size = ((sbsd_cmd_def[n] & SBSD_LEN_Msk) >> SBSD_LEN_Pos); switch (n){ ... //显式定义的命令处理 //隐式处理命令:只要定义过的命令数据写到了sbs_data_buff就能获取到 default: pBuff = (uint8_t*)(&sbs_data_buff[n]); break; } i2cif_set_tx(size, pBuff);
注意:
参数初始化 FW参数有两个来源:
代码的默认配置;
Cobra写入到PRJ文件的Flash参数区域(GDM)
代码默认配置参数 Cobra写入Flash参数 以param_board_cfg[PARM_BCFG_DESIGNCAPACITY]为例:
(1)定义:
param_board_cfg[PARM_BCFG_DESIGNCAPACITY] 是从 Flash 中读取的设计容量参数
在 parameter.h 中定义了参数的枚举类型:
1 2 3 4 5 6 typedef enum { // ... PARM_BCFG_DESIGNCAPACITY, //=14 // ... PARM_BCFG_MAX } PARM_TYPE_BCFG_T;
parameter.c 中定义了参数数组及其默认值(实际不是值,是Flash offset)
1 2 3 4 5 volatile signed int param_board_cfg[(PARM_BCFG_MAX)] = { // ... 0x1388, //0x0D48, PARM_BCFG_DESIGNCAPACITY, 5000mAh // ... };
(2)参数加载过程:
在 gdm_init() 函数中,位于 flash/gdm.c 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void gdm_init() { // ... if (M32(GDM_FLASH_TABLECHECKSUM) != NEWTON_FLASH_EMPTY_DWORD) { // copy Board configure from flash ptrmemu32 = (uint32_t *)param_board_cfg; for (i = 0; i < PARM_BCFG_MAX; i++) { flashoffset = GDM_FLASH_BOARD_CONF + i * 4; *(ptrmemu32 + i) = M32(flashoffset); } // ... gdm_load_sw_cali(); // ... } // ... }
这段代码: 首先检查 Flash 中的校验和是否有效 如果有效,则从 Flash 地址 GDM_FLASH_BOARD_CONF 开始,按顺序读取所有配置参数 每个参数占用 4 字节,按照枚举顺序依次读取 将读取的值存储到 param_board_cfg 数组中
(3)参数使用
设计容量参数在多处被使用,例如在 user/main.c 中初始化电量计算配置时:
1 2 FG_CONFIG_T cfg; cfg.dsncap = (int32_t)(param_board_cfg[PARM_BCFG_DESIGNCAPACITY]);
在 sbs.c 中初始化 SBS 接口时:
1 ((SBSIF_T *)psbsifHandle)->sbs_buff[SBS18_DSNCAP] = param_board_cfg[PARM_BCFG_DESIGNCAPACITY];
数据持久化(Flash log日志) log数据结构 log.c实现了电池管理系统的日志记录和数据持久化功能,主要包括: 历史数据记录:记录电池使用过程中的极值数据,如最高/最低电压、最大充放电电流、最高/最低温度等 参数持久化:存储电量计算所需的自学习参数,确保系统断电后能恢复重要数据 Flash管理:管理Flash存储空间,实现数据的读写和擦除功能 CRC校验:通过CRC校验确保持久化数据的完整性
日志模块使用LOG_T结构体存储数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 typedef struct { uint16_t checksum; // CRC校验值 uint16_t logindex; // 日志索引 uint32_t historiage; // 历史容量老化累计 uint16_t rc; // 当前剩余容量 uint8_t packinfo[64]; // 电池包信息 // 极值记录 uint16_t vmaxcell1; // 最高单体电压 uint16_t vmincell1; // 最低单体电压 uint16_t cmaxchg; // 最大充电电流 int16_t cmaxdsg; // 最大放电电流 uint16_t maxtempcell; // 最高温度 uint8_t maxtempcnt; // 高温事件计数 uint16_t mintempcell; // 最低温度 // 时间统计 uint16_t timefw; // 固件运行时间 uint16_t timeUT; // 极低温时间 uint16_t timeELT; // 超低温时间 uint16_t timeLTL; // 低温下限时间 uint16_t timeLT; // 低温时间 uint16_t timeLTH; // 低温上限时间 uint16_t timeSTL; // 标准温度下限时间 uint16_t timeRT; // 参考温度时间 uint16_t timeSTH; // 标准温度上限时间 uint16_t timeHT; // 高温时间 uint16_t timeOT; // 过温时间 } LOG_T;
日志数据初始化和更新 (1)初始化流程:初始化过程中,系统会从Flash中读取最新的日志记录,校验其完整性,并将数据同步到内存中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 void log_init(void) { // 初始化Flash接口 eflash_init(); // 查找最新的日志 ret = find_latest_log(); if (-1 == ret) { // 没有找到日志,初始化缓冲区 log_init_buff(); } else { // 读取日志数据 for (i = 0; i < sizeof(LOG_T); i++) log_mem[i] = Chip_Flash_Read_Main_Byte(...); // 校验CRC for (i = LOG_DATA_START; i < sizeof(LOG_T); i += 2) wval = math_calc_crc16(*(uint16_t *)(log_mem + i), wval); if (wval == log_p->checksum) { // CRC校验通过,同步数据到内存 ((SBSIF_T *)psbsifHandle)->sbs_historiage = log_p->historiage; vmaxcell1_now = log_p->vmaxcell1; vmincell1_now = log_p->vmincell1; // ...其他数据同步 } // 日志索引自增 log_p->logindex++; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void recover_dfcc_data() { // 从Flash读取DFCC数据 log_crc = Chip_Flash_Read_Main_DWord(PAGE_DEFCC_START << PAGE_NO_SHIFT_TO_OFFSET); // 计算数据的CRC校验值 for (i = 0; i < DFCC_TABLE_LEN; i += 2) { value = Chip_Flash_Read_Main_Word((PAGE_DEFCC_START << PAGE_NO_SHIFT_TO_OFFSET) + 4 + i); wval = math_calc_crc16(value, wval); } // 校验CRC if ((log_crc == wval) && (wval != 0)) { // 复制数据到内存 fn_memcpy(DFCC_table, (uint8_t *)(PAGE_DEFCC_START << PAGE_NO_SHIFT_TO_OFFSET) + 4, DFCC_TABLE_LEN); } // 类似地恢复充电FCC表和放电偏移量表... }
(2)数据更新流程
数据更新流程中,系统会持续监控电池参数的极值,并在特定条件下(如极值变化、定时更新、强制更新)将数据写入Flash。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 void update_log_data(uint8_t right_now) { // 更新电压极值 if (((SBSIF_T *)psbsifHandle)->sbs_buff[SBS3C_CELLMV01] > vmaxcell1_now) vmaxcell1_now = ((SBSIF_T *)psbsifHandle)->sbs_buff[SBS3C_CELLMV01]; if (((SBSIF_T *)psbsifHandle)->sbs_buff[SBS3C_CELLMV01] < vmincell1_now) vmincell1_now = ((SBSIF_T *)psbsifHandle)->sbs_buff[SBS3C_CELLMV01]; // 判断是否需要更新日志 if ((log_check_ultimate_value() && lifetime_en) || right_now || (time_flag[10] && lifetime_en)) { // 更新日志数据 log_p->historiage = ((SBSIF_T *)psbsifHandle)->sbs_agemah; log_p->rc = sbsif_get_data(SBS0F_RC); if (lifetime_en) { log_p->vmaxcell1 = vmaxcell1_now; log_p->vmincell1 = vmincell1_now; log_p->cmaxchg = cmaxchg_now; log_p->cmaxdsg = cmaxdsg_now; log_p->maxtempcell = (maxtempcell_now - DK_BASE) / 10; log_p->mintempcell = (mintempcell_now - DK_BASE) / 10; log_p->maxtempcnt = maxtempcnt; log_update_time(); } // 计算CRC校验值 for (i = LOG_DATA_START; i < sizeof(LOG_T); i += 2) { wval = math_calc_crc16(*(uint16_t *)(log_mem + i), wval); } log_p->checksum = wval; // 写入Flash write_log_into_flash(); log_p->logindex++; } // 充电完成后更新自学习参数 if ((!charge_full_flag && (sbsif_get_data(SBS16_BATTSTATUS) & SBS16_FULLY_CHARGED)) || update_dfcc_flag) { if (log_check_update_condition()) { log_update_dfcc_data(); log_update_charge_fcc_data(); log_update_discharge_offset(); } update_dfcc_flag = 0; } }
持久化数据类型 (1)基本电池信息日志
由LOG_T结构体存储,包含: 电池容量老化历史 电压电流温度极值记录 各温度区间使用时间统计 这些数据用于追踪电池使用历史和健康状态。
(2)放电容量动态学习表(DFCC)
DFCC表存储了不同温度和电流条件下的放电容量校正因子,用于提高电量计算精度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void log_update_dfcc_data() { // 检查是否启用动态放电自学习 if (!plib_en_cfg->l_fg_dync_dsg_enable) return; // 擦除Flash页 eflash_erase_page(PAGE_DEFCC_START); // 计算CRC校验值 wval = log_calculate_crc((uint8_t *)DFCC_table, sizeof(DFCC_table)); // 写入CRC校验值 eflash_mem_to_flash(PAGE_DEFCC_START, 0, 4, (uint8_t *)&wval); // 写入DFCC表 eflash_mem_to_flashEX(PAGE_DEFCC_START, 4, sizeof(DFCC_table), (uint8_t *)DFCC_table); }
(3)充电容量动态学习表(CFCC)
充电FCC表记录了不同温度和电流条件下的充电容量校正数据,用于优化充电过程中的电量计算。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void log_update_charge_fcc_data() { // 检查是否启用动态充电自学习 if (!plib_en_cfg->l_fg_dync_chg_enable) return; // 计算CRC校验值 wval = log_calculate_crc((uint8_t *)charge_fcc_data, sizeof(charge_fcc_data)); // 写入CRC校验值 eflash_mem_to_flash(PAGE_DEFCC_START, LOG_CHARGE_FCC_OFFSET, 4, (uint8_t *)&wval); // 写入充电FCC表 eflash_mem_to_flash(PAGE_DEFCC_START, LOG_CHARGE_FCC_OFFSET + 4, sizeof(charge_fcc_data), (uint8_t *)charge_fcc_data); }
(4)放电偏移量表
放电偏移量表存储了不同条件下的放电偏移校正值,用于补偿放电过程中的误差。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void log_update_discharge_offset(void) { // 检查是否启用动态放电自学习 if (!plib_en_cfg->l_fg_dync_dsg_enable) return; // 计算CRC校验值 wval = log_calculate_crc((uint8_t *)discharge_offset, sizeof(discharge_offset)); // 组装数据 fn_memcpy(log_buf, &wval, 4); fn_memcpy(log_buf + 4, discharge_offset, sizeof(discharge_offset)); // 写入放电偏移量表 eflash_mem_to_flash(PAGE_DEFCC_START, LOG_DISCHARGE_OFFSET_OFFSET, 12, (uint8_t *)log_buf); }
持久化的应用 (1)电池电量计算精度提升
问题:电池在不同温度、电流条件下的容量会有显著差异,使用固定参数难以保证准确性。 解决方案:通过日志模块记录DFCC表和充电FCC表,实现动态学习和参数调整。
数据收集: 充放电过程中记录温度、电流等参数 完整放电/充电周期后计算实际容量 参数更新:
1 2 3 4 5 if (log_check_update_condition()) { log_update_dfcc_data(); log_update_charge_fcc_data(); log_update_discharge_offset(); }
应用算法: 电量计根据当前温度和电流查表获取校正因子 应用校正因子调整容量计算
(2)电池健康状态监测
问题:需要监控电池寿命周期内的健康状态变化,为用户提供电池健康程度的指导。 解决方案:利用日志模块的极值记录功能。
极值记录:
1 2 3 4 5 if (((SBSIF_T *)psbsifHandle)->sbs_buff[SBS3C_CELLMV01] > vmaxcell1_now) vmaxcell1_now = ((SBSIF_T *)psbsifHandle)->sbs_buff[SBS3C_CELLMV01]; // 定期持久化 log_p->vmaxcell1 = vmaxcell1_now;
健康评估: 分析电压极值分布判断是否过充/过放 通过温度极值记录判断是否经历过高/低温 基于充放电电流极值评估是否有过大电流冲击
容量衰减追踪:
1 log_p->historiage = ((SBSIF_T *)psbsifHandle)->sbs_agemah;
(3)断电数据保护
问题:电池管理系统断电后会丢失当前的电量计算参数和状态。 解决方案:关键参数的持久化存储与恢复。
状态保存: 定期或在特定事件(如充满电)时保存电量计算状态 包括当前电量(RC)、最大容量(FCC)、学习参数等
上电恢复:
1 2 3 4 5 6 7 8 9 10 // 启动时恢复数据 if (wval == log_p->checksum) { // CRC校验通过,同步数据 ((SBSIF_T *)psbsifHandle)->sbs_historiage = log_p->historiage; vmaxcell1_now = log_p->vmaxcell1; // ...其他数据恢复 } // 恢复学习参数 recover_dfcc_data();
(4)Flash寿命管理
考虑到Flash的有限擦写次数,系统采取了以下策略: 只在必要时(极值变化、定时更新)写入数据 使用日志循环写入机制,平均分布擦写压力
状态机(State Machine) TODO
Part3:电量算法模块 OCV table OCV表在lib_fg.c中的确实应用主要集中在: 系统初始化时的SOC确定 静置状态下的SOC校正 电压与SOC之间的转换计算 反向查询电压
(1)fg_init_soc函数:用于初始化SOC
1 2 3 4 5 6 7 8 static FG_ERROR_T fg_init_soc(FG_HANDLE_T *handle, short volt, short curr, short cellthm) { short soc = 0; // 使用OCV表估算初始SOC if (lut_soc_by_ocv(handle, volt, &soc) != ERR_FG_NO_ERROR) return ERR_FG_LUT_ERROR; // ...设置初始SOC及库仑计数 }
(2)fg_idle_process函数:静置状态下的SOC校正
1 2 3 4 5 6 7 8 9 static FG_ERROR_T fg_idle_process(FG_HANDLE_T *handle, FG_PARAM_T *param) { // ...在静置条件下 // 使用OCV表估算SOC并进行校正 if (lut_soc_by_ocv(handle, tmp_volt, &ocv_soc) == ERR_FG_NO_ERROR) { // ...根据OCV表估算结果校正SOC } }
(3)lut_soc_by_ocv函数本身:OCV查表获取SOC
1 2 3 4 static FG_ERROR_T lut_soc_by_ocv(FG_HANDLE_T *handle, int volt, short *soc) { // 实现查表逻辑,根据电压和温度获取SOC }
(3)使用OCV表反向查询电压
lib_fg.c中的lut_ocv_by_soc函数提供了OCV表的反向查询能力,用于预测特定SOC点的电压:
在以下场景有应用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 static int fg_time_estimation(FG_HANDLE_T *handle, FG_PARAM_T *param) { short eod_ocv = 0; int time_to_empty = 0; // 预测放电终止点的OCV if (lut_ocv_by_soc(handle, 0, &eod_ocv) == ERR_FG_NO_ERROR) { // 根据当前电压与终止电压的差距,以及当前放电速率,估算剩余时间 int voltage_drop = param->volt_lo_mv - eod_ocv; int discharge_rate = voltage_drop * 3600 / ((LIB_FG_TYPE_T*)handle)->bat_avg_rate; time_to_empty = ((LIB_FG_TYPE_T*)handle)->cc_prv * 3600 / (param->current_ma * discharge_rate / 100); ((LIB_FG_TYPE_T*)handle)->time_to_empty = time_to_empty; } return 0; }
RC table RC表是一个三维查找表,存储了电池在不同电压、电流和温度条件下电池内阻相关的容量校正因子。
1 2 3 4 5 6 7 8 9 10 11 12 // RC表的查询实现 // 输入电压、电流、温度,输出RC值 static FG_ERROR_T lut_rc_f(FG_HANDLE_T *handle, short volt, short curr, short temp, short *output) { // 检查输入参数有效性 // 可能会对电流取绝对值 // 使用三维表查询函数获取RC值 // 可能调用三线性插值函数 return ERR_FG_NO_ERROR; // 成功返回 }
RC表的应用 RC表在fg_update中的应用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 FG_ERROR_T fg_update(FG_HANDLE_T *handle, FG_PARAM_T *param, FG_RESULT_T *result) { // 更新电池状态 // ... // 在这里调用lut_rc_f获取RC值 // 将RC值填充到结果结构体中 result->rc = ((LIB_FG_TYPE_T *)handle)->rc; return ERR_FG_NO_ERROR; }
RC表在放电过程中的应用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 在fg_mobile_discharge_process函数中,RC表用于调整放电容量估计 static FG_ERROR_T fg_mobile_discharge_process(FG_HANDLE_T *handle, FG_PARAM_T *param) { // 放电处理逻辑 // 在这里调用lut_rc_f获取放电条件下的RC值 // 使用RC值调整容量估计 // 更新内部状态 return ERR_FG_NO_ERROR; }
RC表在充电过程中的应用
在fg_mobile_charge_process函数中,RC表用于调整充电容量估计:
1 2 3 4 5 6 7 8 9 10 11 12 static FG_ERROR_T fg_mobile_charge_process(FG_HANDLE_T *handle, FG_PARAM_T *param) { // 充电处理逻辑 // 在这里调用lut_rc_f获取充电条件下的RC值 // 使用RC值调整容量估计 // 更新内部状态 return ERR_FG_NO_ERROR; }
RC表在时间预估中的应用:
用于fg_time_estimation函数中,影响剩余时间计算:
1 2 3 4 5 6 7 8 9 static int fg_time_estimation(FG_HANDLE_T *handle, FG_PARAM_T *param) { // 在这里间接调用lut_rc_f获取的RC值 // 使用RC值调整剩余时间计算 // 计算并更新time_to_empty或time_to_full return 0; }
RC表在SOH计算中的应用:
在soh_update函数中用于电池健康状态评估:
1 2 3 4 5 6 7 static void soh_update(FG_HANDLE_T *handle, FG_PARAM_T *param) { // 在这里调用lut_rc_f获取标准条件下的RC值 // 使用RC值参与SOH计算 // 更新SOH值 }
RC表小结 RC与电池内阻:
RC表代表以下含义: 在放电过程中:RC表值较大表示内阻小、可用容量高;RC表值较小表示内阻大、可用容量低 在低温条件下:RC表值会降低,反映低温对容量的不利影响 在大电流条件下:RC表值会降低,反映大电流放电对有效容量的减少 在电池老化后:RC表配合SOH机制,反映容量衰减
因为温度和负载情况(电流大小)都会最终反映到锂电池内阻上,RC表的容量值变化实际是与电池内阻相关: IR压降补偿: 估算负载下的电压损失 计算实际OCV(开路电压) 提高电量估算精度
容量校正: 调整电池在不同条件下的实际可用容量 考虑温度和电流对容量的影响
放电终点预测: 根据内阻预测不同负载下的放电终止点 提高剩余时间估算精度
RC表在实际应用中的一般流程:
获取实时数据:读取电池电压、电流和温度 查询RC表:调用lut_rc_f函数,获取当前条件下的RC值 容量调整:根据RC值调整FCC(满充容量)和CC(当前容量) SOC计算:基于调整后的容量计算SOC 结果输出:将RC值和其他计算结果返回给系统
DFCC table DFCC表的配置与初始化 默认数据结构和初始值:
1 2 3 4 5 6 7 8 9 10 11 12 // 表的维度定义 #define DFCC_X 11 // SOC轴 #define DFCC_Y 3 // 电流轴 #define DFCC_Z 4 // 温度轴 // 各轴的数据范围 const short DFCC_XDATA[DFCC_X] = {500, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000}; // SOC: 5%-100% const short DFCC_YDATA[DFCC_Y] = {600, 1500, 3000}; // 电流: 600mA-3000mA const short DFCC_ZDATA[DFCC_Z] = {-100, 0, 250, 550}; // 温度: -10°C到55°C // DFCC表数据结构 short DFCC_table[DFCC_Y * DFCC_Z][DFCC_X]; // 3D查找表
使用前的初始化:
从main.c中的代码片段可以看到DFCC表是作为查找表(lut)的一部分进行初始化的:
这段代码配置了一个三维查找表结构(tbl_three_t),包含: 轴点数量:x_num, y_num, z_num 轴数据指针:p_x_start, p_y_start, p_z_start 表数据指针:p_data_start
1 2 3 4 5 6 7 lut.dfcc_tbl.x_num = DFCC_X; lut.dfcc_tbl.y_num = DFCC_Y; lut.dfcc_tbl.z_num = DFCC_Z; lut.dfcc_tbl.p_x_start = (short *)DFCC_XDATA; lut.dfcc_tbl.p_y_start = (short *)DFCC_YDATA; lut.dfcc_tbl.p_z_start = (short *)DFCC_ZDATA; lut.dfcc_tbl.p_data_start = (short *)DFCC_table;
查找表结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 typedef struct { int x_num; // X轴点数 int y_num; // Y轴点数 int z_num; // Z轴点数 short* p_x_start; // X轴数据起始指针 short* p_y_start; // Y轴数据起始指针 short* p_z_start; // Z轴数据起始指针 short* p_data_start; // 表数据起始指针 } tbl_three_t; typedef struct { tbl_read_t readfunc; // 表读取函数 tbl_one_t thm_tbl; // 温度表信息 tbl_one_t ocv_tbl; // OCV表信息 tbl_three_t rc_tbl; // RC表信息 tbl_three_t dfcc_tbl; // DFCC表信息 } FG_LUT_T;
lut_dfcc_f查表函数 定义DFCC table为宏DDTABLE
1 #define DDTABLE ((LIB_FG_TYPE_T *)handle)->fg_tbls.dfcc_tbl.p_data_start
在放电处理流程内调用lut_dfcc_f查表,过程:
a) 输入参数限制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // SOC限制 if (soc < XDTABLE[0]) { soc = XDTABLE[0]; } if (soc > XDTABLE[XDNUM - 1]) { soc = XDTABLE[XDNUM - 1]; } // 电流限制 if (curr < YDTABLE[0]) { curr = YDTABLE[0]; } if (curr > YDTABLE[YDNUM - 1]) { curr = YDTABLE[YDNUM - 1]; } // 温度限制 if (temp < ZDTABLE[0]) { temp = ZDTABLE[0]; } if (temp > ZDTABLE[ZDNUM - 1]) { temp = ZDTABLE[ZDNUM - 1]; }
b) 索引查找:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // 查找SOC对应的索引 for (IDX = 1; IDX < XDNUM; IDX++) { if ((XDTABLE[IDX-1] <= soc) && (XDTABLE[IDX] > soc)) { break; } } // 查找电流对应的索引 for (IDY = 1; IDY < YDNUM; IDY++) { if ((YDTABLE[IDY-1] <= curr) && (YDTABLE[IDY] > curr)) { break; } } // 查找温度对应的索引 for (IDZ = 1; IDZ < ZDNUM; IDZ++) { if ((ZDTABLE[IDZ-1] <= temp) && (ZDTABLE[IDZ] > temp)) { break; } }
c) 三线性插值计算:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // X轴(SOC)方向插值 if (SVAL == STMP) { XFRACS = 1 * 100; } else { XFRACS = fg_idiv(handle, (soc - STMP) * 100, (SVAL - STMP)); } // Y轴(电流)方向插值 if (SVAL == STMP) { YFRAC = 1; } else { YFRAC = (SVAL - STMP); } // Z轴(温度)方向插值 if (SVAL == STMP) { ZFRAC = 1; } else { ZFRAC = (SVAL - STMP); }
DFCC与动态容量 放电流程中查找RC table后继续查找DFCC table,动态修改FCC容量值
1 2 3 4 5 error = lut_dfcc_f(*handle*, ((LIB_FG_TYPE_T *)*handle*)->rsoc_now, -curr_now, *param*->cellthm, (*short* *)&dfcc_comp); rca_prv = (fg_idiv(*handle*, tmp_soh_mah, ((LIB_FG_TYPE_T *)*handle*)->rsoc_now) * dfcc_comp) / 10000; ((LIB_FG_TYPE_T *)*handle*)->fcc = rca_prv;
查表函数的插值原理 RC table和DFCC table都是三维查表函数,是离散点。而电量计检测到的温度电流电压soc等数据是连续点。因此需要三维插值去处理连续点无法在离散表中查询到的情况。
三线性插值是一种在三维空间中进行插值的技术,它基于周围8个已知点的值来估计任意位置的值。整个过程分三步:
X方向插值:对Z和Y固定,在X方向上插值得到4个点
Y方向插值:对这4个点在Y方向上插值得到2个点
Z方向插值:对这2个点在Z方向上插值得到最终结果
插值公式:
!V = V_{000}(1-x)(1-y)(1-z) + V_{100}x(1-y)(1-z) + V_{010}(1-x)y(1-z) + V_{110}xy(1-z) + V_{001}(1-x)(1-y)z + V_{101}x(1-y)z + V_{011}(1-x)yz + V_{111}xyz
其中x, y, z是归一化的坐标(范围0到1)。
以下是示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 int lut_three_latitude(tbl_three_t *p_tbl, short x, short y, short z, short *result) { int i, j, k; int x_idx, y_idx, z_idx; float fx[2], fy[2], fz[2]; // 插值权重 float tmp_xyz[8]; // 八个顶点的值 float tmp_xy[4]; // 四条边的插值结果 float tmp_x[2]; // 两个面的插值结果 // 查找X轴索引并计算插值权重 x_idx = find_table_index(p_tbl->p_x_start, p_tbl->x_num, x); if (x_idx < 0) return -1; if (x_idx == p_tbl->x_num - 1) { fx[0] = 1.0f; fx[1] = 0.0f; } else { fx[1] = (float)(x - p_tbl->p_x_start[x_idx]) / (float)(p_tbl->p_x_start[x_idx+1] - p_tbl->p_x_start[x_idx]); fx[0] = 1.0f - fx[1]; } // 查找Y轴索引并计算插值权重 y_idx = find_table_index(p_tbl->p_y_start, p_tbl->y_num, y); if (y_idx < 0) return -1; if (y_idx == p_tbl->y_num - 1) { fy[0] = 1.0f; fy[1] = 0.0f; } else { fy[1] = (float)(y - p_tbl->p_y_start[y_idx]) / (float)(p_tbl->p_y_start[y_idx+1] - p_tbl->p_y_start[y_idx]); fy[0] = 1.0f - fy[1]; } // 查找Z轴索引并计算插值权重 z_idx = find_table_index(p_tbl->p_z_start, p_tbl->z_num, z); if (z_idx < 0) return -1; if (z_idx == p_tbl->z_num - 1) { fz[0] = 1.0f; fz[1] = 0.0f; } else { fz[1] = (float)(z - p_tbl->p_z_start[z_idx]) / (float)(p_tbl->p_z_start[z_idx+1] - p_tbl->p_z_start[z_idx]); fz[0] = 1.0f - fz[1]; } // 获取周围8个点的值 for (k = 0; k < 2 && (z_idx + k) < p_tbl->z_num; k++) { for (j = 0; j < 2 && (y_idx + j) < p_tbl->y_num; j++) { for (i = 0; i < 2 && (x_idx + i) < p_tbl->x_num; i++) { int data_idx = (z_idx + k) * (p_tbl->y_num * p_tbl->x_num) + (y_idx + j) * p_tbl->x_num + (x_idx + i); tmp_xyz[k*4 + j*2 + i] = (float)p_tbl->p_data_start[data_idx]; } } } // 沿X轴插值,得到4个边的值 for (k = 0; k < 2; k++) { for (j = 0; j < 2; j++) { tmp_xy[k*2 + j] = tmp_xyz[k*4 + j*2] * fx[0] + tmp_xyz[k*4 + j*2 + 1] * fx[1]; } } // 沿Y轴插值,得到2个面的值 for (k = 0; k < 2; k++) { tmp_x[k] = tmp_xy[k*2] * fy[0] + tmp_xy[k*2 + 1] * fy[1]; } // 沿Z轴插值,得到最终结果 *result = (short)(tmp_x[0] * fz[0] + tmp_x[1] * fz[1]); return 0; }
通过三线性插值,系统能够在RC表/DFCC表的离散数据点之间生成平滑的过渡值,这带来以下优势:
平滑过渡:避免在临界点附近出现电量跳变
精细粒度:无需为每个可能的条件定义离散值,大幅减少表的大小
处理边界条件:即使输入参数超出表定义范围,也能提供合理的边界值
适用性广:同一表格可用于各种电池和工作条件
一个插值示例:
场景:电池在-5°C环境下以1500mA电流放电,SOC为25%。
插值过程:
温度在-10°C(DFCC_ZDATA[0]=-100)和0°C(DFCC_ZDATA[1]=0)之间
温度插值权重:fz[0]=0.5, fz[1]=0.5
电流在1000mA(DFCC_YDATA[1])和2000mA(DFCC_YDATA[2])之间
电流插值权重:fy[0]=0.5, fy[1]=0.5
SOC在10%(DFCC_XDATA[1]=1000)和30%(DFCC_XDATA[2]=3000)之间
SOC插值权重:fx[0]=0.75, fx[1]=0.25
计算过程:
获取8个顶点值:
V000 = DFCC_table[03+1][1] = 88(-10°C, 1000mA, 10%SOC)
V001 = DFCC_table[13+1][1] = 85(0°C, 1000mA, 10%SOC)
V010 = DFCC_table[03+2][1] = 60(-10°C, 2000mA, 10%SOC)
V011 = DFCC_table[13+2][1] = 75(0°C, 2000mA, 10%SOC)
V100 = DFCC_table[03+1][2] = 90(-10°C, 1000mA, 30%SOC)
V101 = DFCC_table[13+1][2] = 85(0°C, 1000mA, 30%SOC)
V110 = DFCC_table[03+2][2] = 75(-10°C, 2000mA, 30%SOC)
V111 = DFCC_table[13+2][2] = 75(0°C, 2000mA, 30%SOC)
应用三线性插值公式计算最终值(简化计算):
X方向插值(SOC):
tmp_xy[0] = 880.75 + 90 0.25 = 88.5
tmp_xy[1] = 600.75 + 75 0.25 = 63.75
tmp_xy[2] = 850.75 + 85 0.25 = 85
tmp_xy[3] = 750.75 + 75 0.25 = 75
Y方向插值(电流):
tmp_x[0] = 88.50.5 + 63.75 0.5 = 76.125
tmp_x[1] = 850.5 + 75 0.5 = 80
Z方向插值(温度):
result = 76.1250.5 + 80 0.5 = 78.06
最终校正因子约为78.06%,表示在-5°C、1500mA放电电流和25% SOC条件下,电池实际可用容量约为标称容量的78.06%。