简介:这份资源是面向嵌入式开发者的BQ79616电压采集芯片串口通讯程序,基于STM32F429平台构建,适合正在做BMS电池管理、多节电池电压采样或电源模式控制相关项目的工程师与学习者参考。压缩包共201个文件,以99个.h头文件和89个.c源文件为主体,涵盖STM32F4xx HAL库驱动、RS485通讯及BQ79616寄存器操作等模块,另含少量txt说明、Keil工程文件与批处理脚本,整体约1.07MB,结构完整便于直接导入工程。程序重点实现了电源模式切换与电压采集两条主线,可帮助读者理解BQ79616的串口协议时序、命令帧组织方式以及STM32端收发状态机的搭建思路,对调试采样精度、排查通讯异常也有实际参考价值。目前已有1739人学习下载,适合具备一定STM32与串口通讯基础、希望快速上手BQ79616芯片的开发者。
1. 拿到 BQ79616.zip 之后:这块 16 串电池监控板到底能跑出什么数据
如果你手上正好有一块 STM32F429 的开发板和一套 BQ79616 的电池监控板,打开这个压缩包的第一反应大概率是——里面到底有没有能直接烧进去的工程。答案是有的,而且它解决的是一个很具体的问题:让 STM32F429 通过 SPI 把 BQ79616 采集到的 16 串电芯电压、温度、电流数据读出来,并且能配置过压欠压保护阈值。这个项目适合两类人:一类是做储能 BMS 或者电动车电池包的嵌入式工程师,需要快速验证 BQ79616 的通信链路;另一类是在校学生做电池管理系统课题,需要一个能跑通的 STM32F429 + BQ79616 最小系统参考。它不涉及复杂的 SOC 估算算法,核心就是底层驱动和寄存器配置,把通信这条链路打通,后面加什么算法都是你自己的事。
2. 架构拆解:从 STM32F429 到 BQ79616 的 SPI 链路是怎么搭的
2.1 硬件连接与通信拓扑
BQ79616 是 TI 的 16 串电池监控 AFE,支持菊花链和 SPI 两种主机接口。这个项目用的是 SPI 模式,STM32F429 作为 SPI 主机,BQ79616 作为从机。硬件上需要关注几个关键引脚:SPI 的 SCK、MISO、MOSI、CS 不用多说,BQ79616 这边还有几个容易翻车的控制脚——NFAULT 用来报故障,如果悬空不处理,读寄存器时可能一直返回异常值;BQ79616 的 WAKE 引脚需要主机拉高来唤醒,很多新手直接上电就发 SPI 命令,结果一个字节都读不回来,就是因为芯片还在休眠。常见做法是在初始化阶段先把 WAKE 拉高至少 1ms,再拉低,然后再发通信命令。
STM32F429 的 SPI 配置要注意时钟极性和相位。BQ79616 的 SPI 模式要求 CPOL=0、CPHA=1,也就是空闲时 SCK 为低电平,数据在第二个边沿采样。这个参数在 STM32CubeMX 里对应的是 SPI_POLARITY_LOW 和 SPI_PHASE_2EDGE。如果配成 CPHA=0,读回来的数据会整体偏移一位,现象就是电压值看起来像随机数。SPI 时钟频率建议先降到 1MHz 以下调试,稳定后再往上提,BQ79616 手册标称支持到 4MHz,但实际布线长了之后 2MHz 以上就容易出错。
2.2 工程目录结构与模块划分
解压后的工程目录大致是这样的结构,我按功能模块拆开说:
BQ79616_Project/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── bq79616.h # BQ79616 驱动头文件 │ │ └── stm32f4xx_hal_conf.h │ └── Src/ │ ├── main.c # 主循环和初始化 │ ├── bq79616.c # BQ79616 驱动实现 │ └── stm32f4xx_it.c # 中断服务函数 ├── Drivers/ │ ├── STM32F4xx_HAL_Driver/ # HAL 库 │ └── CMSIS/ # 内核支持 ├── BQ79616.ioc # CubeMX 配置文件 └── MDK-ARM/ # Keil 工程目录这个划分方式很典型,bq79616.c 和 bq79616.h 是核心,所有跟 AFE 相关的寄存器操作、CRC 校验、命令封装都在这两个文件里。main.c 只负责调用初始化和周期性读取。如果你用 Keil 打开,注意 MDK-ARM 目录下的 .uvprojx 文件,里面已经配好了 STM32F429 的器件型号和下载算法。用 IAR 的话需要自己新建工程,把 Core 和 Drivers 下的源文件加进去,头文件路径也要手动配。
2.3 寄存器映射与数据帧格式
BQ79616 的通信协议是命令加数据的格式。主机发一个命令字节,后面跟数据字节,从机返回响应。命令字节的最高位表示读还是写,低 7 位是寄存器地址。比如读电芯电压的寄存器起始地址是 0x0500,那么命令字节就是 0x80 | 0x00 的高位组合,实际发送时是分两个字节传地址的。这个项目里封装了一个BQ79616_ReadReg函数,把地址拆成高低字节,加上 CRC 校验后通过 SPI 发出去。
数据帧里 CRC 是必须的,BQ79616 默认启用 CRC 校验,如果主机发的 CRC 不对,从机直接丢弃命令,你读回来的全是 0xFF。CRC 的计算多项式是 x^8 + x^2 + x + 1,初始值 0x00。项目里有一个查表法的 CRC 实现,速度比逐位计算快,在 1MHz SPI 下差别不大,但养成习惯没坏处。读回来的数据是 16 位,高字节在前,低字节在后,电芯电压的换算公式是:电压值 = 原始值 × 190.73uV。这个系数是 BQ79616 的 ADC 分辨率决定的,不同批次的芯片可能有微小偏差,量产时建议用高精度源校准一次。
3. 跑起来:从零编译到读出第一组电压值的完整步骤
3.1 开发环境准备与工程导入
你需要准备的东西不多:Keil MDK 5 或者 STM32CubeIDE,STM32F429 的板子一块,BQ79616 评估板或者自己画的板子一块,再加上一根 SPI 连接线。如果用的是官方评估板,注意跳线帽的位置,有些板子默认是菊花链模式,要改成 SPI 模式需要动几个电阻。这个坑我踩过,调了一下午发现模式不对,血泪经验就是先看板子原理图再上电。
用 Keil 的话,直接双击 MDK-ARM 目录下的 .uvprojx 文件,然后检查 Options for Target 里的器件型号是不是 STM32F429IGTx,下载器选 ST-Link 或者 J-Link。用 CubeIDE 的话,需要新建一个空工程,把 Core 和 Drivers 下的文件复制进去,然后在项目属性里添加头文件路径:Core/Inc、Drivers/STM32F4xx_HAL_Driver/Inc、Drivers/CMSIS/Device/ST/STM32F4xx/Include、Drivers/CMSIS/Include。少一个路径就会报一堆 undefined reference。
3.2 关键初始化代码逐段解析
初始化分两步:先初始化 STM32F429 的 SPI 外设和 GPIO,再初始化 BQ79616 芯片本身。先看 SPI 和 GPIO 的配置:
// SPI1 初始化,用于与 BQ79616 通信 void MX_SPI1_Init(void) { hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; // 主机模式 hspi1.Init.Direction = SPI_DIRECTION_2LINES; // 全双工 hspi1.Init.DataSize = SPI_DATASIZE_8BIT; // 8位数据 hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0 hspi1.Init.CLKPhase = SPI_PHASE_2EDGE; // CPHA=1 hspi1.Init.NSS = SPI_NSS_SOFT; // 软件控制CS hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_64; // 约 1.3MHz hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; // 高位先发 HAL_SPI_Init(&hspi1); }这段代码里最关键的三个参数:CLKPolarity 和 CLKPhase 决定了采样边沿,配错了数据就是乱的;BaudRatePrescaler 先设大一点,STM32F429 的 SPI1 挂在 APB2 上,时钟 84MHz,64 分频后约 1.3MHz,调试稳定后再改成 16 分频到 5MHz 左右;NSS 用软件控制,因为 BQ79616 的 CS 需要手动拉低拉高来框定帧。
然后是 BQ79616 的初始化序列,这个顺序不能乱:
// BQ79616 初始化流程 void BQ79616_Init(void) { // 1. 唤醒芯片:WAKE 拉高至少 1ms HAL_GPIO_WritePin(BQ_WAKE_GPIO_Port, BQ_WAKE_Pin, GPIO_PIN_SET); HAL_Delay(2); HAL_GPIO_WritePin(BQ_WAKE_GPIO_Port, BQ_WAKE_Pin, GPIO_PIN_RESET); HAL_Delay(10); // 等待芯片内部上电稳定 // 2. 发送复位命令 BQ79616_WriteReg(0x0306, 0x01); // 软件复位 HAL_Delay(5); // 3. 配置 ADC 采样模式 BQ79616_WriteReg(0x0300, 0x03); // 连续采样模式 // 4. 使能电芯电压和温度采集 BQ79616_WriteReg(0x0302, 0xFF); // 使能全部16串 BQ79616_WriteReg(0x0303, 0x0F); // 使能温度通道 }唤醒那一步的延时不能省,BQ79616 从休眠到 SPI 就绪需要几百微秒,加上电源建立时间,给 10ms 比较保险。复位命令写 0x0306 寄存器,这个地址是 BQ79616 的 CONTROL 寄存器,写 1 触发软复位。复位后所有寄存器回到默认值,所以后面的配置必须重新写。ADC 模式选连续采样,这样读电压的时候不需要每次触发转换,直接读结果寄存器就行。
3.3 读取电压和温度的完整调用链
初始化完成后,主循环里周期性调用读取函数。读电压的调用链是这样的:
// 读取16串电芯电压,结果存入 voltage_mv 数组 void BQ79616_ReadCellVoltages(uint16_t *voltage_mv) { uint8_t raw_data[32]; uint16_t raw_value; // 从 0x0500 开始连续读 32 字节,覆盖 16 串电压 BQ79616_ReadReg(0x0500, raw_data, 32); for (int i = 0; i < 16; i++) { // 高字节在前,低字节在后 raw_value = (raw_data[i*2] << 8) | raw_data[i*2 + 1]; // 换算公式:190.73uV/LSB,结果转成 mV voltage_mv[i] = (uint16_t)((raw_value * 190.73) / 1000.0); } }这里有个细节:BQ79616 的电压寄存器是 16 位,但实际有效位是 14 位,高两位是状态标志。如果你发现读出来的电压值偶尔跳变很大,先检查是不是把状态位当成数据位算了。正确做法是 raw_value & 0x3FFF 之后再乘系数。温度读取类似,地址从 0x0510 开始,换算公式不同,NTC 的阻值到温度需要查表或者用 Steinhart-Hart 公式算,项目里给了一个简化的线性查表法,精度要求不高的话够用。
4. 避坑与排查:调 BQ79616 时最容易翻车的五个地方
4.1 读回数据全是 0xFF 或 0x00
现象是 SPI 能发出去,但读回来的每个字节都是 0xFF 或者 0x00。原因通常有三个:一是芯片没唤醒,WAKE 引脚时序不对,解决方法是示波器抓一下 WAKE 波形,确认高电平持续时间大于 1ms;二是 SPI 模式配错,CPHA 设成了 1EDGE,改成 2EDGE 后正常;三是 CS 信号没有正确拉低,BQ79616 要求 CS 在整帧传输期间保持低电平,如果你用硬件 NSS 模式,STM32 会在每个字节后自动拉高 CS,导致帧被截断。改成软件 NSS,手动控制 CS 引脚。
4.2 CRC 校验一直失败
现象是发出去的命令没有响应,或者读回来的数据 CRC 对不上。BQ79616 的 CRC 是逐字节计算的,初始值 0x00,多项式 0x07。常见错误是 CRC 计算范围搞错了——只算了地址字节没算数据字节,或者把 CRC 本身也算进去了。正确做法是:命令字节 + 地址高字节 + 地址低字节 + 数据字节,全部算完之后的 CRC 放在最后发送。读操作时,从机返回的数据帧里也带 CRC,主机要校验后再使用数据。
4.3 电压读数跳变超过 ±50mV
现象是同一串电芯的电压读数在几十毫伏范围内跳动。原因可能是 ADC 参考电压不稳,BQ79616 内部有基准,但电源纹波太大会影响精度。解决方法是检查 VREF 引脚的去耦电容,通常需要 1uF 和 100nF 并联。另一个原因是采样速率太快,连续采样模式下 ADC 一直在转换,如果 SPI 读取频率跟不上,会读到正在更新的寄存器。改成单次触发模式,读之前发一个转换命令,延时 1ms 再读,数据就稳了。
4.4 多串电压读出来只有前几串有值
现象是 16 串里只有前 4 串或者前 8 串有电压值,后面的全是 0。这个通常是因为 BQ79616 的通道使能寄存器没配对。0x0302 寄存器控制电芯通道使能,默认值可能只开了前几串。写 0xFF 使能全部 16 串。另外注意,如果你用的不是 16 串的板子,比如只接了 8 串,那没接的通道读出来是悬空值,不是 0,需要在软件里屏蔽掉。
4.5 通信一段时间后死机
现象是跑了几分钟或者几十分钟后,SPI 读不到数据,STM32 卡在 HAL_SPI_TransmitReceive 里出不来。原因是 SPI 传输没有加超时,BQ79616 如果因为干扰丢了一帧,STM32 会一直等从机的响应。解决方法是把 HAL 的 SPI 函数换成带超时参数的版本,或者在调用前加一个看门狗。我一般会在 SPI 读写外面包一层超时判断,超过 10ms 没完成就重新初始化 SPI 外设,这样即使偶发干扰也能自恢复。
5. 二次开发:从能读到能用,我改过的那几个地方
拿到这个工程之后,直接烧进去能看到电压值,但离产品化还有距离。我自己的习惯是先做三件事:加滤波、加保护、加日志。滤波方面,原始电压值跳动是正常的,我一般用滑动平均或者中值滤波,窗口大小取 8 或者 16,在 10ms 的采样周期下响应速度够用。代码改动很小,在BQ79616_ReadCellVoltages返回之前加一个滤波数组就行。保护方面,BQ79616 本身支持过压欠压阈值配置,写 0x0308 到 0x030F 这几个寄存器,但项目里没启用,需要自己加上。我通常会把阈值设成电芯规格的 1.1 倍和 0.9 倍,然后使能 NFAULT 中断,这样出问题时 STM32 能第一时间知道。
日志这块,调试阶段我建议把每次读到的原始数据和换算后的值都通过串口打出来,格式用 CSV,方便导入 Excel 画曲线。STM32F429 的串口资源多,用 UART1 就行,波特率 115200。打印的时候注意别在 SPI 传输中间打断,否则时序会乱。我一般是在主循环里读完之后统一打印,一次打印 16 串电压加 4 路温度,大概 200 字节,115200 下 20ms 内能发完,不影响 10ms 的采样周期。
还有一个我踩过的坑:BQ79616 的寄存器写入之后需要延时才能生效,特别是配置 ADC 模式和通道使能,写完立刻读会读到旧值。我一般写完延时 5ms 再读回来验证,确认写进去了再继续。这个习惯是从调其他 AFE 芯片时养成的,后来发现 BQ79616 也吃这一套。从那以后我每次改完寄存器配置都强制走一遍「写-延时-回读」的流程,虽然多花几毫秒,但省去了很多玄学问题。希望帮到你。
本文还有配套的精品资源,点击获取