资讯动态

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

发布时间:2026/10/27 18:47:08 来源:尧图企业网站定制
简介本资源面向嵌入式开发工程师与电子类学生提供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。时钟源选 APB1STM32F407 的 APB1 一般是 42MHzCubeMX 会自动算分频。有个细节I2C 的 GPIO 模式要选开漏输出上拉使能速度等级选 Very High 或者 High 都行。如果你用中断或 DMA初期先别开用阻塞模式把数据读出来再说。下面是我常用的 I2C 初始化片段用 HAL 库直接生成后改两处。// I2C1 初始化100kHz7 位地址模式 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 就是 100kHzDutyCycle在标准模式下其实不影响但 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 库要求传入的地址已经包含读写位。如果你直接传 0x55HAL 会当成 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然后直接读一个字节。注意两次操作之间没有 STOPHAL 库的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 位芯片会解析错。解决先用逻辑分析仪抓一次波形看发出的第一个字节是不是 0xAA0x55 左移一位加写位。如果不是检查代码里的地址宏。然后确认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 Stream0I2C1_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 读回来的数据先做一次范围检查超出合理范围就丢弃用上一次的值。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑