STM32F407 驱动 BQ34Z100 电量计:I2C 通信与数据读取实战

发布时间:2026/10/27 18:47:07
STM32F407 驱动 BQ34Z100 电量计:I2C 通信与数据读取实战

简介:本资源面向嵌入式开发工程师与电子类学生,提供STM32F407通过IIC总线读取BQ34Z100电量计芯片的完整工程源码,可用于锂电池电压、电流、温度及SOC等参数的采集与电池管理系统开发。压缩包共218个文件,约12.2MB,以53个h头文件与42个c源文件为核心,涵盖STM32F4标准外设库驱动;另有32个d与32个o编译中间文件、31个crf交叉引用文件、uvprojx与uvoptx工程配置、hex与bin固件、map映射文件及docx说明文档,构成可直接编译下载的MDK工程。资源重点演示IIC初始化、BQ34Z100地址配置、寄存器命令收发、数据解析与错误处理,并涉及中断与DMA传输优化,read_BQ34Z100()等封装函数便于上层调用。目前已有1199人学习下载,适合希望掌握电量计通信与电池管理开发的读者参考实践。

1. STM32F407 读取 BQ34Z100:从“读不到数据”到稳定拿到电量

如果你手上有块 STM32F407 的板子,又刚好在做一个带锂电池的设备,大概率绕不开电量计这颗料——BQ34Z100。它不像普通 ADC 采样电阻那样给你个电压就完事,而是一颗带固件、带算法、带化学 ID 的智能电量计芯片。很多人第一次接上它,用 I2C 一读,要么全是 0xFF,要么读出来一堆看不懂的寄存器值,然后就开始怀疑人生。这个标题讲的就是怎么用 STM32F407 把 BQ34Z100 的数据稳定读出来,包括电压、电流、剩余电量、健康状态这些真正能用的信息。适合正在做 BMS、便携设备、储能监控的嵌入式工程师,也适合刚接触 TI 电量计、想少走弯路的开发者。下面按“先搞懂它在干嘛 → 再动手接线和写代码 → 最后避开那些让人翻车的坑”这条线走一遍。

2. BQ34Z100 到底在算什么:先搞懂它的数据模型再动手

2.1 它不是一个简单的 I2C 从机

BQ34Z100 是 TI 的 Impedance Track 电量计,内部跑着一套算法,靠跟踪电池阻抗和开路电压来估算剩余电量。它对外暴露的是标准 SMBus/I2C 接口,但从机地址、寄存器宽度、数据格式都有自己的一套规矩。你如果拿它当普通 I2C 传感器读,比如直接读某个地址的两个字节就当电压用,那大概率会得到莫名其妙的值。它的数据分两类:一类是命令寄存器,比如 0x08 读电压、0x0C 读电流、0x2C 读 SOC;另一类是扩展命令和 Data Flash,用来配置电池参数和化学 ID。STM32F407 这边要做的就是通过 I2C 正确发起 SMBus 协议里的块读或字读,把这两类数据区分开。

2.2 关键寄存器先认全,不然后面全是玄学

动手前先把最常用的几个命令码记住,这些是后面写代码直接要用的。电压、电流、SOC 这几个是高频读取对象,温度、剩余容量、满充容量、健康状态按需读。注意电流是带符号的,充电为正还是放电为正取决于配置,读出来要按有符号 16 位处理。SOC 是百分比,但它是 0 到 100 的整数,不是浮点。下面这张表是我实际调试时贴在显示器边上的,你可以直接抄。

命令码含义数据长度单位/格式
0x08Voltage2 字节mV,无符号
0x0CCurrent2 字节mA,有符号
0x2CStateOfCharge1 字节%,无符号
0x06Temperature2 字节0.1K,无符号
0x10RemainingCapacity2 字节mAh,无符号
0x0EFullChargeCapacity2 字节mAh,无符号
0x2EStateOfHealth1 字节%,无符号

提示:不同固件版本的 BQ34Z100 在命令码上基本兼容,但 Data Flash 里的参数地址会变,配置前一定先读一遍芯片的固件版本号。

2.3 硬件连接:上拉电阻和地线比你想的重要

BQ34Z100 的 I2C 引脚是开漏的,STM32F407 这边也是开漏,所以 SDA 和 SCL 都必须加上拉电阻。我一般用 4.7k 拉到 3.3V,如果总线走线长或者挂的设备多,降到 2.2k 也能用。但有个血泪经验:很多人只给 SDA 加了上拉,忘了 SCL,结果就是偶尔能读一次,大部分时间总线卡死。另外 BQ34Z100 的 VCC 和 BAT 引脚要分清,BAT 接电池正极做采样,VCC 是芯片供电,别接反。GND 一定要和 STM32F407 共地,不共地的话 I2C 电平没有参考,读出来全是随机数。如果你用的是现成的模块,先拿万用表量一下 SDA 和 SCL 对 VCC 的阻值,正常应该是 4.7k 左右,如果量出来是 0 或者无穷大,先查硬件再写代码。

3. 用 STM32F407 的 HAL 库把 I2C 读通:从初始化到第一个有效值

3.1 CubeMX 里 I2C 参数怎么设才不翻车

在 CubeMX 里选 I2C1 或 I2C2 都行,关键是速度模式。BQ34Z100 支持标准模式 100kHz 和快速模式 400kHz,我建议先用 100kHz 调通,稳定后再试 400kHz。时钟源选 APB1,STM32F407 的 APB1 一般是 42MHz,CubeMX 会自动算分频。有个细节:I2C 的 GPIO 模式要选开漏输出,上拉使能,速度等级选 Very High 或者 High 都行。如果你用中断或 DMA,初期先别开,用阻塞模式把数据读出来再说。下面是我常用的 I2C 初始化片段,用 HAL 库直接生成后改两处。

// I2C1 初始化,100kHz,7 位地址模式 hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed = 100000; hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 = 0; hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; hi2c1.Init.OwnAddress2 = 0; hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(&hi2c1) != HAL_OK) { Error_Handler(); }

这段代码里ClockSpeed设成 100000 就是 100kHz,DutyCycle在标准模式下其实不影响,但 HAL 库要求填。NoStretchMode一定要关,因为 BQ34Z100 在某些操作时会拉低 SCL 做时钟拉伸,如果你禁了,通信直接失败。OwnAddress1填 0 就行,主机模式不需要自己的地址。

3.2 读电压和电流:SMBus 字读的时序要自己拼

BQ34Z100 的命令寄存器读法跟标准 I2C 读寄存器不太一样。标准 I2C 是先写寄存器地址,再 restart 读数据。SMBus 的字读也是这个流程,但 BQ34Z100 要求先写命令码,然后读两个字节,低字节在前。HAL 库的HAL_I2C_Mem_Read可以直接用,把命令码当内存地址传进去。下面是我封装的两个函数,读电压和电流。

#define BQ34Z100_ADDR (0x55 << 1) // 7 位地址 0x55,左移后给 HAL 用 // 读电压,返回 mV uint16_t BQ34Z100_ReadVoltage(I2C_HandleTypeDef *hi2c) { uint8_t buf[2] = {0}; HAL_StatusTypeDef ret; ret = HAL_I2C_Mem_Read(hi2c, BQ34Z100_ADDR, 0x08, I2C_MEMADD_SIZE_8BIT, buf, 2, 100); if (ret != HAL_OK) { return 0xFFFF; // 错误标志 } return (uint16_t)(buf[1] << 8 | buf[0]); // 低字节在前 } // 读电流,返回 mA,有符号 int16_t BQ34Z100_ReadCurrent(I2C_HandleTypeDef *hi2c) { uint8_t buf[2] = {0}; HAL_StatusTypeDef ret; ret = HAL_I2C_Mem_Read(hi2c, BQ34Z100_ADDR, 0x0C, I2C_MEMADD_SIZE_8BIT, buf, 2, 100); if (ret != HAL_OK) { return 0x7FFF; // 错误标志 } return (int16_t)(buf[1] << 8 | buf[0]); }

注意BQ34Z100_ADDR是 0x55 左移一位,因为 HAL 库要求传入的地址已经包含读写位。如果你直接传 0x55,HAL 会当成 7 位地址处理,但实际发送时会出错。I2C_MEMADD_SIZE_8BIT表示命令码是 8 位,BQ34Z100 的命令码确实都是 8 位。超时设 100ms 足够,如果经常超时,先查上拉电阻和总线电容。读回来的两个字节要按低字节在前拼,这是 SMBus 字读的规矩,拼反了电压会变成 0x1234 这种离谱值。

3.3 读 SOC 和健康状态:单字节命令别用错函数

SOC 和健康状态都是单字节,用HAL_I2C_Mem_Read读一个字节就行。但有个坑:有些版本的 HAL 库在读单字节时,如果最后一个字节前发了 NACK,可能会返回 HAL_ERROR。我一般直接用HAL_I2C_Master_Transmit发命令码,再HAL_I2C_Master_Receive读一个字节,这样时序更可控。下面这个写法更稳。

// 读 SOC,返回 0-100 uint8_t BQ34Z100_ReadSOC(I2C_HandleTypeDef *hi2c) { uint8_t cmd = 0x2C; uint8_t soc = 0; if (HAL_I2C_Master_Transmit(hi2c, BQ34Z100_ADDR, &cmd, 1, 100) != HAL_OK) { return 0xFF; } if (HAL_I2C_Master_Receive(hi2c, BQ34Z100_ADDR, &soc, 1, 100) != HAL_OK) { return 0xFF; } return soc; }

这里先发命令码 0x2C,然后直接读一个字节。注意两次操作之间没有 STOP,HAL 库的Master_Transmit和Master_Receive默认会发 STOP,但 BQ34Z100 对 STOP 不敏感,所以能用。如果你追求严格时序,可以用HAL_I2C_Mem_Read配合I2C_MEMADD_SIZE_8BIT,效果一样。SOC 读出来如果是 0xFF,说明通信失败,不是电量 255%。

4. 避坑与排查:BQ34Z100 读数据时最常见的 5 个翻车现场

4.1 现象:读出来全是 0xFF,偶尔能读到一次

原因:I2C 总线上拉电阻没接或者阻值太大,导致上升沿太慢,STM32F407 采样时已经变成低电平。另外 BQ34Z100 的 SCL 时钟拉伸如果被主机忽略,也会让数据错位。解决:先量 SDA 和 SCL 对 VCC 的电阻,正常 4.7k 左右。如果大于 10k,换 2.2k 到 4.7k。然后在 CubeMX 里确认NoStretchMode是 Disable。如果还不行,把 I2C 速度降到 50kHz 试一次,能读就说明是时序余量不够。

4.2 现象:电压读出来是 0xFFFF 或者 0x0000

原因:命令码发错了,或者地址不对。BQ34Z100 的 7 位地址是 0x55,但有些模块出厂时改了地址,比如 0x0B。另外HAL_I2C_Mem_Read的MemAddress参数如果传了 16 位,芯片会解析错。解决:先用逻辑分析仪抓一次波形,看发出的第一个字节是不是 0xAA(0x55 左移一位加写位)。如果不是,检查代码里的地址宏。然后确认I2C_MEMADD_SIZE_8BIT没写错。如果地址不确定,写个扫描程序从 0x08 到 0x77 遍历一遍,能 ACK 的就是它。

4.3 现象:SOC 一直是 0 或者 100,不变化

原因:BQ34Z100 还没有完成学习周期,或者电池没有接上,芯片处于未配置状态。它需要先配置化学 ID、设计容量、终止电压这些参数,然后经过一次完整的充放电学习,才会输出准确的 SOC。解决:先用 TI 的 bqStudio 连上芯片,读一下 Data Flash 里的Design Capacity和Chem ID,如果都是默认值,说明没配置。配置完再跑一次学习周期。如果只是调试通信,可以先读电压和电流,这两个不依赖学习。

4.4 现象:电流值正负反了,充电显示放电

原因:BQ34Z100 的电流方向定义和你的采样电阻接法有关。它默认充电为正还是放电为正在 Data Flash 里有个Current Sign配置位。解决:读一下0x0C的原始值,如果充电时读出来是负数,要么改硬件采样电阻的接法,要么在软件里取反。我一般先在软件里取反,因为改硬件麻烦。但要注意,如果 SOC 也在反向计算,那必须改配置,不能只改显示。

4.5 现象:I2C 总线频繁卡死,需要断电重启

原因:STM32F407 的 I2C 外设在总线被拉低时如果发生错误,可能会锁死 SDA 或 SCL。BQ34Z100 在低功耗模式下偶尔会拉低总线。解决:在 I2C 初始化后加一段总线恢复代码,手动发 9 个时钟脉冲把从机复位。另外在HAL_I2C_ErrorCallback里加重新初始化的逻辑。下面这段恢复代码我放在main循环开头,每次读之前检查一下。

// I2C 总线恢复:发 9 个时钟脉冲 void I2C_BusRecovery(I2C_HandleTypeDef *hi2c) { GPIO_InitTypeDef gpio = {0}; // 切换 SCL 和 SDA 为普通 GPIO 输出 gpio.Pin = GPIO_PIN_6 | GPIO_PIN_7; gpio.Mode = GPIO_MODE_OUTPUT_OD; gpio.Pull = GPIO_PULLUP; gpio.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, &gpio); // 发 9 个时钟 for (int i = 0; i < 9; i++) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); HAL_Delay(1); } // 重新初始化 I2C HAL_I2C_Init(hi2c); }

这段代码把 PB6 和 PB7 临时切成开漏输出,手动翻转 SCL 九次,让从机把剩余的数据位发完,然后重新初始化 I2C 外设。注意HAL_Delay(1)在 1ms 左右,太快了从机可能反应不过来。恢复完再读一次,如果还是失败,检查硬件有没有短路。

5. 进阶:用 STM32F407 的 DMA 和定时器做无阻塞电量采集

5.1 为什么阻塞读在真实产品里不够用

前面用的HAL_I2C_Mem_Read是阻塞模式,读一次电压大概要 1ms 到 2ms,如果主循环里还有屏幕刷新、按键扫描、通信协议要处理,这点时间累积起来就会让系统变卡。更麻烦的是,如果 I2C 总线因为干扰卡住,阻塞读会把整个主循环拖死。我一般会在产品里把电量计读取改成 DMA 加定时器触发,每 500ms 读一次,读的时候 CPU 去干别的,读完了进中断处理数据。这样即使 I2C 出错,也不会影响主循环。

5.2 用 TIM2 触发 DMA 读取的配置步骤

第一步,把 I2C1 的 TX 和 RX 都配上 DMA 通道。STM32F407 的 I2C1_RX 对应 DMA1 Stream0,I2C1_TX 对应 DMA1 Stream6。在 CubeMX 里勾上,模式选 Normal,不要选 Circular,因为每次读的字节数不一样。第二步,配置 TIM2 为 500ms 周期中断,在中断里启动一次 DMA 传输。第三步,在 DMA 完成回调里解析数据。下面是我用的 DMA 启动代码。

// 在 TIM2 中断里调用,启动一次电压读取 void BQ34Z100_StartVoltageDMA(I2C_HandleTypeDef *hi2c) { static uint8_t cmd = 0x08; static uint8_t rx_buf[2]; // 先发命令码,用 DMA 发送 HAL_I2C_Master_Transmit_DMA(hi2c, BQ34Z100_ADDR, &cmd, 1); // 等待发送完成,实际项目中用中断标志,这里简化 while (HAL_I2C_GetState(hi2c) != HAL_I2C_STATE_READY); // 再启动接收 DMA HAL_I2C_Master_Receive_DMA(hi2c, BQ34Z100_ADDR, rx_buf, 2); }

这段代码里HAL_I2C_Master_Transmit_DMA发完命令码后,要等状态回到 READY 再启动接收,否则会冲突。实际项目里我会把接收启动放在发送完成回调里,而不是用while等。rx_buf要定义成全局或者静态,不然 DMA 传输时栈上的变量可能被覆盖。读回来的两个字节在HAL_I2C_MasterRxCpltCallback里拼成电压值,然后更新到一个全局结构体里,主循环直接读结构体就行。

5.3 验证数据稳定性的一个土办法

改完 DMA 之后怎么知道读得对不对?我一般会同时用阻塞读和 DMA 读各读一次,比较两个值。如果差超过 10mV,说明 DMA 时序有问题。另外可以在HAL_I2C_ErrorCallback里加一个计数器,每出错一次加一,跑一晚上看计数器涨不涨。如果涨了,说明总线抗干扰不行,要么加屏蔽,要么降低速度。还有一个更直接的办法:拿一个已知电压的稳压源接在 BAT 引脚上,看读出来的值跟万用表差多少,一般 BQ34Z100 的电压精度在 ±10mV 以内,差太多就是配置或者采样电阻的问题。

5.4 我踩过的一个 DMA 坑

有一次我用 DMA 读电流,发现读出来的值偶尔会跳变到 0x8000,也就是 -32768mA。查了半天发现是 DMA 传输完成中断里,我直接读了rx_buf,但那时候 DMA 可能还没把最后一个字节写完。后来改成在回调里先判断HAL_I2C_GetState是不是 READY,再读数据,问题就没了。另外 DMA 的优先级要设成中或者低,别设成 Very High,不然会打断其他外设。这个坑让我养成了一个习惯:凡是 DMA 读回来的数据,先做一次范围检查,超出合理范围就丢弃,用上一次的值。

希望帮到你。

本文还有配套的精品资源,点击获取