第四章 电压采集与AFE驱动:从芯片到数据的完整链路
各位同学,今天我们进入BMS系统最核心的环节——电压采集。说白了,电池管理系统的所有决策,都建立在准确的电压数据之上。如果电压都测不准,那后面的SOC估算、均衡控制全是空中楼阁。
这一章,我会以ADI的AD7280A为例,带大家走通从AFE芯片工作原理、SPI通信、滤波算法到阈值设定的完整链路。嗯,这些都是我当年踩过坑的地方,希望能帮你们少走弯路。
4.1 AFE芯片工作原理——以AD7280A为例
AFE芯片,全称是模拟前端。它的任务很简单:把电池的模拟电压,变成数字信号送给MCU。但实际做起来,坑不少。
AD7280A是一款6通道的电池监控芯片,支持最多6节串联电池的电压采集。我最早用这颗芯片时,觉得它挺简单的,结果第一次调试就遇到了通信失败的问题——后来发现是菊花链的配置没搞对。
4.1.1 芯片内部架构
AD7280A内部主要包含这几个模块:
- 多路复用器(MUX):轮流选通6个电池通道
- Σ-Δ ADC:14位分辨率,转换时间约7μs
- 内部基准:2.5V参考电压,精度±0.1%
- SPI接口:支持菊花链拓扑,最多可级联32颗芯片
- 辅助ADC输入:可测量温度或电流
这里有个关键点:AD7280A的ADC是逐次逼近型的,不是积分型。这意味着它对噪声比较敏感。我在项目中遇到过,如果PCB布局不好,采集到的电压会跳来跳去,根本没法用。
4.1.2 电压采集流程
一次完整的电压采集,大致分三步:
- 通道选择:通过SPI写入控制寄存器,选择要采集的电池通道
- 启动转换:设置转换控制位,ADC开始工作
- 读取结果:等待转换完成(约7μs),通过SPI读取数据寄存器
你可能会问:为什么要分三步?不能一次搞定吗?
其实有些AFE芯片确实支持自动扫描模式,但AD7280A需要手动触发。我个人习惯用定时器中断,每10ms触发一次转换,这样时序更可控。
4.2 SPI通信时序——别让时序成为你的噩梦
SPI通信,看起来简单,但实际调试时最容易出问题。我见过太多工程师,代码写好了,逻辑也对,就是通信不通——最后发现是时序参数没配对。
4.2.1 AD7280A的SPI时序要求
先看关键参数:
| 参数 | 最小值 | 典型值 | 最大值 | 单位 |
|---|---|---|---|---|
| SCLK频率 | - | 1 | 10 | MHz |
| CS低电平时间 | 50 | - | - | ns |
| 数据建立时间 | 20 | - | - | ns |
| 数据保持时间 | 20 | - | - | ns |
| SCLK高电平时间 | 40 | - | - | ns |
| SCLK低电平时间 | 40 | - | - | ns |
注意看,SCLK频率最大10MHz。我曾经为了追求速度,把SPI时钟设到12MHz,结果数据全是错的。后来老老实实降到8MHz,一切正常。嗯,有时候慢就是快。
4.2.2 通信协议细节
AD7280A的SPI帧格式是固定的:
- 每次传输32位数据
- 高8位是命令/地址
- 低24位是数据
- MSB优先传输
这里有个容易忽略的点:AD7280A的SPI模式是Mode 1(CPOL=0, CPHA=1)。什么意思?就是SCLK空闲时为低电平,数据在SCLK的下降沿采样。我见过有人配成Mode 0,结果死活读不到正确数据。
下面是一个典型的写寄存器代码:
// AD7280A写寄存器函数
// reg_addr: 寄存器地址 (8位)
// data: 要写入的数据 (24位)
void AD7280A_WriteReg(uint8_t reg_addr, uint32_t data)
{
uint32_t tx_data = 0;
// 构建32位数据帧
// 高8位: 0x80 | reg_addr (写命令)
// 低24位: data
tx_data = ((0x80 | reg_addr) << 24) | (data & 0xFFFFFF);
// 拉低CS
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET);
// 发送32位数据
HAL_SPI_Transmit(&hspi1, (uint8_t*)&tx_data, 4, HAL_MAX_DELAY);
// 拉高CS
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);
// 等待至少50ns
delay_ns(100);
}
4.3 电压采集滤波算法——让数据不再跳变
原始电压数据,直接拿来用?别闹。电池电压本身就有噪声,加上ADC的量化误差,不滤波根本没法用。
我常用的滤波方法有三种,各有适用场景:
4.3.1 中值滤波
连续采集5次,去掉最大值和最小值,取中间3个的平均。这个方法对脉冲噪声特别有效。我在项目中遇到过,电池连接器接触不良时,偶尔会出现一个离谱的电压值,中值滤波直接把它干掉了。
4.3.2 滑动平均滤波
维护一个长度为N的队列,每次新数据入队,计算队列平均值。N一般取4-16。这个方法平滑效果好,但会有延迟。N越大,延迟越大。
// 滑动平均滤波
#define FILTER_LEN 8
static uint16_t filter_buf[FILTER_LEN] = {0};
static uint8_t filter_index = 0;
uint16_t MovingAverageFilter(uint16_t new_value)
{
uint32_t sum = 0;
uint8_t i;
// 更新缓冲区
filter_buf[filter_index] = new_value;
filter_index = (filter_index + 1) % FILTER_LEN;
// 计算平均值
for(i = 0; i < FILTER_LEN; i++)
{
sum += filter_buf[i];
}
return (uint16_t)(sum / FILTER_LEN);
}
4.3.3 一阶低通滤波
公式很简单:output = α * input + (1-α) * last_output。α一般取0.1-0.3。这个方法计算量小,适合实时性要求高的场景。
我个人习惯:在BMS中,电压采集用滑动平均滤波(N=8),温度采集用一阶低通滤波(α=0.2)。为什么?因为电压变化快,需要快速响应;温度变化慢,平滑更重要。
4.4 过压/欠压阈值设定——保护电池的第一道防线
阈值设定,说白了就是告诉AFE芯片:电压超过多少算过压,低于多少算欠压。这个值设错了,轻则保护失效,重则电池报废。
4.4.1 阈值计算
以磷酸铁锂电池为例:
- 标称电压:3.2V
- 充电截止电压:3.65V
- 放电截止电压:2.5V
但实际设定时,不能直接用这些值。为什么?因为电池有内阻,大电流充放电时会有压降。我曾经遇到过,客户说电池过压保护了,但实测电压才3.6V——后来发现是充电电流太大,电池端电压虚高。
我的经验是:
| 参数 | 理论值 | 实际设定值 | 说明 |
|---|---|---|---|
| 过压阈值 | 3.65V | 3.70V | 留0.05V余量,防止误触发 |
| 过压恢复 | 3.55V | 3.50V | 回滞0.2V,防止反复触发 |
| 欠压阈值 | 2.50V | 2.45V | 留0.05V余量 |
| 欠压恢复 | 2.60V | 2.65V | 回滞0.2V |
4.4.2 AD7280A的阈值配置
AD7280A内部有专门的比较器,可以硬件实现过压/欠压检测。配置方法很简单:
// 配置过压阈值 (以3.70V为例)
// AD7280A的阈值寄存器格式:
// 高8位: 地址
// 低24位: 阈值值 (单位: 19.5mV/LSB)
uint32_t ov_threshold = (uint32_t)(3.70 / 0.0195); // 约190
// 写入过压阈值寄存器 (地址0x04)
AD7280A_WriteReg(0x04, ov_threshold);
// 配置欠压阈值 (以2.45V为例)
uint32_t uv_threshold = (uint32_t)(2.45 / 0.0195); // 约126
// 写入欠压阈值寄存器 (地址0x05)
AD7280A_WriteReg(0x05, uv_threshold);
4.5 实战经验总结
好了,这一章的内容就到这里。最后分享几个我踩过的坑:
- SPI通信不稳定:检查CS引脚的时序,确保每次传输后CS拉高时间足够长
- 电压采集跳变:先检查电源噪声,再检查滤波算法是否生效
- 阈值误触发:确认是否做了回滞处理,以及阈值是否考虑了电池内阻
下一章,我们会讲电流采集与库仑计。嗯,那又是一个容易踩坑的地方。各位先把这一章消化好,有问题随时交流。