ThinkNotes

Simple is not easy | 化繁为简,知易行难

0%

电量计 -- 77561(77226)的Firmware Architecture

电量计–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/ # 引导加载程序

启动和运行主流程

启动流程

  1. 上电后从o2bootloader引导区启动

  2. 初始化MCU系统时钟和基本外设

  3. 初始化电量计参数和查找表

  4. 调用fg_init函数初始化电量计算法库

  5. 进入主循环

主循环流程

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. 库伦积分法

    :根据电流积分计算电量变化

    1
    2
    3
    4
    // 电流积分计算
    delta_capacity = (current * delta_time) / 3600; // 单位:mAh
    rca = rca - delta_capacity; // 更新剩余容量
    soc = (rca * 10000) / fcc; // 计算SOC(0-10000)
  2. 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);
    }
  3. 混合算法

    :结合库伦积分和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

低功耗设计

该电量计实现了以下低功耗策略:

  1. 工作周期设计:系统大部分时间处于深度睡眠状态,定时唤醒采集数据和处理

  2. 外设优化:只在需要时启用ADC、I2C等外设

  3. 代码执行效率:优化算法减少指令执行次数

  4. 数据存储优化:使用查找表减少计算量

调试和校准工具

根据Cobra工具文档,系统支持以下调试和校准功能:

  1. 电流校准:通过Cobra工具校准0电流、正向电流和负向电流
  2. 数据采集:通过SBS协议读取电量计内部状态
  3. 内存调试:通过GGMEM0-GGMEM8访问电量计内部RAM
  4. 参数配置:通过Cobra工具配置电量计算法参数

Part2 Firmware主要功能模块分析

通信接口(SBS命令流程)

以SBS命令0x0D(RSOC - 相对电量)为例,详细介绍整个处理流程,包含main.c、sbs.c和i2c.c三个文件在这个流程中的角色和作用

初始化阶段

(1)main.c中的初始化:

在main.c中:

  1. 调用i2cif_init初始化I2C接口,设置从机地址和回调函数sbs_callback_i2c_slave
  2. 调用sbsif_init初始化SBS接口
  3. 调用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中的初始化

  1. 初始化SBS接口数据结构
  2. 初始化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中的初始化

  1. 初始化I2C接口数据结构

  2. 配置I2C硬件参数

  3. 设置回调函数,该函数将在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);

注意:

  • default隐式支持的命令必须在sbsd_cmd_def数组中注册,否则I2C接口无法识别。

  • 注册的数据大小要和传入值一致(0x30的SBSD_LEN_Pos处定义数据是4byte)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const unsigned int sbsd_cmd_def[SBSD_CMD_MAX] = {
    (0x03UL << SBSD_CMD_Pos) | (4UL << SBSD_LEN_Pos) | (SBSD_PRO_DIR_RW << SBSD_PRO_DIR_Pos) | (SBSD_PRO_TYP_WWD), //0x03000434UL, /* BattMode */
    (0x09UL << SBSD_CMD_Pos) | (4UL << SBSD_LEN_Pos) | (SBSD_PRO_DIR_RD << SBSD_PRO_DIR_Pos) | (SBSD_PRO_TYP_SWD), //0x09000415UL, /* Voltage */
    (0x0AUL << SBSD_CMD_Pos) | (4UL << SBSD_LEN_Pos) | (SBSD_PRO_DIR_RD << SBSD_PRO_DIR_Pos) | (SBSD_PRO_TYP_SWD), //0x0A000415UL, /* Current */
    (0x0BUL << SBSD_CMD_Pos) | (4UL << SBSD_LEN_Pos) | (SBSD_PRO_DIR_RD << SBSD_PRO_DIR_Pos) | (SBSD_PRO_TYP_SWD), //0x0B000415UL, /* AvgCurrent */
    (0x0DUL << SBSD_CMD_Pos) | (4UL << SBSD_LEN_Pos) | (SBSD_PRO_DIR_RD << SBSD_PRO_DIR_Pos) | (SBSD_PRO_TYP_SWD), //0x0D000415UL, /* RSOC */
    (0x0EUL << SBSD_CMD_Pos) | (4UL << SBSD_LEN_Pos) | (SBSD_PRO_DIR_RD << SBSD_PRO_DIR_Pos) | (SBSD_PRO_TYP_SWD), //0x0E000415UL, /* ASOC */

    ...
    (0x30UL << SBSD_CMD_Pos) | (4UL << SBSD_LEN_Pos) | (SBSD_PRO_DIR_RD << SBSD_PRO_DIR_Pos) | (SBSD_PRO_TYP_WWD), /* CHGVOLT */
    (0x32UL << SBSD_CMD_Pos) | (4UL << SBSD_LEN_Pos) | (SBSD_PRO_DIR_RD << SBSD_PRO_DIR_Pos) | (SBSD_PRO_TYP_WWD), /* CHGCURRENT */

参数初始化

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个已知点的值来估计任意位置的值。整个过程分三步:

  1. X方向插值:对Z和Y固定,在X方向上插值得到4个点

  2. Y方向插值:对这4个点在Y方向上插值得到2个点

  3. 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表的离散数据点之间生成平滑的过渡值,这带来以下优势:

  1. 平滑过渡:避免在临界点附近出现电量跳变

  2. 精细粒度:无需为每个可能的条件定义离散值,大幅减少表的大小

  3. 处理边界条件:即使输入参数超出表定义范围,也能提供合理的边界值

  4. 适用性广:同一表格可用于各种电池和工作条件

一个插值示例:

场景:电池在-5°C环境下以1500mA电流放电,SOC为25%。

插值过程:

  1. 温度在-10°C(DFCC_ZDATA[0]=-100)和0°C(DFCC_ZDATA[1]=0)之间
  • 温度插值权重:fz[0]=0.5, fz[1]=0.5
  1. 电流在1000mA(DFCC_YDATA[1])和2000mA(DFCC_YDATA[2])之间
  • 电流插值权重:fy[0]=0.5, fy[1]=0.5
  1. 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 + 900.25 = 88.5

  • tmp_xy[1] = 600.75 + 750.25 = 63.75

  • tmp_xy[2] = 850.75 + 850.25 = 85

  • tmp_xy[3] = 750.75 + 750.25 = 75

  • Y方向插值(电流):

  • tmp_x[0] = 88.50.5 + 63.750.5 = 76.125

  • tmp_x[1] = 850.5 + 750.5 = 80

  • Z方向插值(温度):

  • result = 76.1250.5 + 800.5 = 78.06

最终校正因子约为78.06%,表示在-5°C、1500mA放电电流和25% SOC条件下,电池实际可用容量约为标称容量的78.06%。