尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

HDQ协议详解:从时序到GPIO模拟驱动,彻底搞懂BQ电量计通信

HDQ协议详解:从时序到GPIO模拟驱动,彻底搞懂BQ电量计通信 简介本资源是一套面向嵌入式工程师与电池管理系统开发者的TI HDQ单总线通信协议完整实现方案聚焦于与bq27xxx系列电量计芯片的底层交互解决跨平台HDQ协议移植与调试难题。压缩包含923个文件主体为558个C源码与242个头文件覆盖STM32裸机驱动、Arduino库封装及Linux内核级w1总线适配含linux/w1.h相关源码辅以汇编启动代码、IAR/Keil工程配置文件及电池配置专用HDQ发送函数包体达135.96MB。已有1050人学习下载内容深度结合硬件时序控制与寄存器级操作包含ARM Cortex-M3数学库支持文件及大量电量计校准与读写逻辑实现目录结构按平台分层清晰便于快速定位各目标平台的协议栈核心模块与接口封装。 做电池供电产品的人对TI的BQ系列电量计应该都不陌生。一块电池塞进去能不能精确显示剩余电量靠的就是这枚小小的芯片。但很多人在BQ系列电量计里会遇到一个特别膈应人的东西HDQ协议。你说用I2C不好吗两根线还能挂多个设备偏偏有些型号只给你留一个HDQ引脚资料还少网上翻来翻去就那么几篇英文应用笔记中文讲解更是稀缺。这篇文章就是要把HDQ协议的底裤扒干净。我会从协议出现的背景讲起把帧格式、时序参数、CRC算法这些底层细节掰开揉碎然后再给出一份可直接移植的GPIO模拟驱动代码最后把我实际调试中踩过的坑一个不落列出来。如果你正在用BQ27441、BQ27546、BQ34Z100这类电量计或者正准备用HDQ和主控通信这篇应该能帮你少走不少弯路。1. 电量计为什么需要HDQ这种怪协议1.1 从BQ电量计产品线看HDQ的定位TI的电量计产品线非常庞大但按照通信接口来分基本就是两大类一类用I2C比如BQ27441、BQ27546、BQ4050另一类用HDQ比如BQ27000、BQ26500还有一些型号两种接口都支持通过配置引脚或寄存器切换比如BQ34Z100。为什么TI要搞一个HDQ出来最直接的原因是省引脚。电量计芯片本身封装小很多是8脚甚至6脚的封装如果每个功能都要分一个引脚出来根本放不下。HDQ只用一根数据线加一根地线就能完成主机和电量计之间的双向通信剩下引脚可以分配给电池保护、温度检测、LED显示这些功能。对于手持设备、电动工具、便携医疗设备这些对PCB面积敏感的场合省一个引脚就意味着能省一点布板空间和一颗上拉电阻的成本I2C需要两颗。我最早接触HDQ是在一个两节锂电池的工具类产品上主控是STM32F103。当时选型选了BQ27441结果发现这芯片虽然主接口是I2C但很多评估板和参考设计里都用HDQ和MCU通信一时没整明白后来才逐渐理清楚这套协议的门道。1.2 HDQ对比I2C单线的代价与收益很多人第一反应是I2C也是低速协议两根线也不复杂为什么要用HDQ这种奇葩协议这里我列个表对比一下对比项I2CHDQ信号线数量2根SDA SCL1根DATA通信速率100kbps/400kbps约5kbps多设备支持支持设备可挂总线不支持点对点时序复杂度相对简单有成熟库时序敏感需精确延时数据格式7位地址寄存器地址数据40位帧命令数据CRC典型应用多媒体电池、多设备系统单电池/小封装设备从表格能看出来HDQ最大的优势就是线和资源占用少最大的劣势就是慢、而且一个接口只能挂一颗芯片。在电量计这种“每秒读一次或者几百毫秒读一次就够”的场景里5kbps完全够用但它和I2C的时序模型完全不同所以直接用I2C的思维去写HDQ代码大概率会踩坑。需要特别提醒的是HDQ不是像UART那样的异步串口也不是像SPI那样的同步协议它是“单线半双工、靠脉宽区分0和1”的通信方式。理解这一点后面的代码就顺理成章了。2. HDQ协议帧格式与底层时序2.1 一帧数据到底长什么样HDQ协议最核心的就是一帧40位数据。这一帧可以拆成这几段Break主机把数据线拉低一段时间通知从机“我要开始通信了”Start主机拉低一个短脉冲表示命令数据开始发送8位命令字告诉从机我要读还是写哪个寄存器Start再次发一个起始位表示数据阶段开始8位数据读写的数据内容8位CRC校验数据用的是CRC8也就是说一次完整的HDQ事务总线上的波形是Break → Start → 命令字节 → Start → 数据/CRC字节。Break的作用类似UART的起始条件它让电量计从低功耗状态唤醒并同步两端的通信状态。很多调试第一天就翻车的人问题都出在Break时间给得不够长。后面我会给出具体参数。2.2 时序参数精讲高低电平脉宽怎么定HDQ对时序的要求说高也高说宽松也宽松。它不像1-Wire那样对采样窗口卡得那么死但也经不起你随手写个delay就完事。不同芯片手册给的参数略有差异我结合BQ27000、BQ27441、BQ34Z100几款常见型号的资料整理出一份通用参数表参数符号最小值典型值最大值说明Bit周期tBIT168us192us220us每个数据位从开始到结束的总时长Break低电平时间tBREAK190us250us无上限拉低时间要足够长从机能识别Break后恢复时间tREC20us50us—从释放到Start之间的高电平时间Start位低电平时间tSTART15us20us25us起始脉冲宽度写“1”低电平时间tW12us5us8us低脉宽很短线很快释放写“0”低电平时间tW010us15us25us低脉宽较长和“1”有明显区别读采样等待时间tRD5us10us—主机释放线后等待稳定再采样这里有个关键概念HDQ里每一位的周期是固定的约192us而数据线上的“0”和“1”是靠低电平持续的时间来区分的。写“0”的时候低电平要撑得久一点写“1”的时候只是轻轻拉一下然后立刻释放剩下的时间都保持在高电平。读的时候逻辑也类似但角色换过来了。主机先把线拉低一小段时间相当于发起一个read slot然后释放。接下来线上的电平由从机决定从机想发“0”就继续把线拉低从机想发“1”就释放线让上拉电阻把电平拉高。主机在释放后等待一段时间采样一次电平就得到了这一位的数据。这套机制和I2C完全不是一个路子所以我强烈建议你用示波器先把波形看明白再写代码不然写出来的时序大概率是蒙的。2.3 HDQ 8bit 与 HDQ 16bit 命令格式HDQ协议有两种数据模式8位和16位。BQ27000是HDQ16BQ27441虽然官方主打I2C但HDQ模式下也支持16位读取。两种模式命令字不一样命令8位模式16位模式读数据0x150x17写数据0x140x1616位模式下数据阶段会连续传两个字节先发低字节再发高字节CRC的计算范围也扩大到两个字节。很多寄存器本身是16位比如电压、电流、剩余电量用8位模式还得读两次再拼起来不如直接上16位省事。有一个容易忽略的细节HDQ每个字节都是LSB first也就是先发最低位。很多第一次写驱动的人会默认按MSB first处理结果读出来的数据总是怪怪的比如0x01读成0x80就是对不上的典型症状。3. 手写HDQ驱动从GPIO模拟到可用的读写函数3.1 硬件连接与初始化HDQ接口的硬件连接非常简单电量计的HDQ引脚直接连到MCU的一个GPIO中间不需要串电阻有些设计会串一个0欧或小电阻方便调试但必须在HDQ线上加一个上拉电阻到电源轨。上拉电阻的取法我实测下来4.7kΩ比较稳10kΩ在线上电容大或者走线长的时候会有点勉强2.2kΩ也可以但功耗稍大。有些芯片手册会直接给出推荐值用它的推荐值最省心。关键点是MCU侧的GPIO必须配置为开漏输出模式并且能回读引脚电平。开漏输出意味着MCU可以把线拉低也可以释放线让上拉电阻把电平拉高回读意味着当从机把线拉低时MCU能检测到。如果配置成推挽输出就会和从机抢总线轻则通信乱码重则损坏引脚。STM32配置成开漏输出后输出的0是拉低输出的1是释放此时读IDR寄存器就能拿到实际电平这个特性正好满足HDQ需求。初始化代码大致是这样以STM32 HAL库为例#define HDQ_GPIO_PORT GPIOB #define HDQ_GPIO_PIN GPIO_PIN_12 #define HDQ_GPIO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE() #define HDQ_PIN_HIGH() HAL_GPIO_WritePin(HDQ_GPIO_PORT, HDQ_GPIO_PIN, GPIO_PIN_SET) #define HDQ_PIN_LOW() HAL_GPIO_WritePin(HDQ_GPIO_PORT, HDQ_GPIO_PIN, GPIO_PIN_RESET) #define HDQ_PIN_READ() HAL_GPIO_ReadPin(HDQ_GPIO_PORT, HDQ_GPIO_PIN) void HDQ_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; HDQ_GPIO_CLK_ENABLE(); GPIO_InitStruct.Pin HDQ_GPIO_PIN; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Pull GPIO_NOPULL; // 外部已有上拉内部不用开 GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(HDQ_GPIO_PORT, GPIO_InitStruct); HDQ_PIN_HIGH(); // 默认释放总线 }注意内部上拉不要开因为内部上拉阻值通常在30~50kΩ和外部4.7kΩ并联后会改变电平建立时间干扰时序。保持干净的外部上拉即可。3.2 基础时序原语Break、Start、发送与读取单bit写HDQ驱动和写I2C软件模拟很像先要把最底层的几个“原语”写好。我习惯把Break、Start、发送一bit、读取一bit这四个函数当成地基地基稳了上层命令随便加。void HDQ_DelayUs(uint32_t us) { // 移植时请替换为你的平台精确延时函数 // 推荐使用 DWT 或定时器时基不要用裸循环 } static void HDQ_SendBreak(void) { HDQ_PIN_LOW(); HDQ_DelayUs(250); // 典型值 250us手册要求 190us 以上 HDQ_PIN_HIGH(); HDQ_DelayUs(50); // 恢复时间确保稳定高电平 } static void HDQ_SendStart(void) { HDQ_PIN_LOW(); HDQ_DelayUs(20); // Start 低电平典型 20us HDQ_PIN_HIGH(); HDQ_DelayUs(15); // Start 后的高电平稳定时间 } static void HDQ_SendBit(uint8_t bit) { if (bit) { // 写“1”低电平时间短 HDQ_PIN_LOW(); HDQ_DelayUs(5); HDQ_PIN_HIGH(); HDQ_DelayUs(192 - 5); // 补齐一个 bit 周期 } else { // 写“0”低电平时间长 HDQ_PIN_LOW(); HDQ_DelayUs(15); HDQ_PIN_HIGH(); HDQ_DelayUs(192 - 15); } } static uint8_t HDQ_ReadBit(void) { uint8_t bit; HDQ_PIN_LOW(); HDQ_DelayUs(5); // 主机发起读 slot HDQ_PIN_HIGH(); HDQ_DelayUs(10); // 释放总线等从机响应建立电平 bit HDQ_PIN_READ(); // 采样 HDQ_DelayUs(192 - 5 - 10); // 补齐剩余周期 return bit; }这里有一个小细节写“1”时低电平时间我用了5us写“0”用了15us。从时序表可以看出来5us和15us都在手册允许的范围内而且相距足够远从机解码不容易误判。用192us做bit周期是把速率定为约5.2kbps实测兼容性很好。3.3 CRC8计算与校验HDQ协议里的CRC8多项式是x^8 x^2 x 1对应生成多项式0x07。很多初次接触的人会被这个计算绕晕其实就是一个标准的CRC-8。计算范围是命令字节加数据字节不含Break和Start初始值为0x00按MSB先行方式处理。static uint8_t HDQ_CalcCRC8(uint8_t *data, uint8_t len) { uint8_t crc 0x00; uint8_t i, j; for (i 0; i len; i) { crc ^ data[i]; for (j 0; j 8; j) { if (crc 0x80) { crc (uint8_t)((crc 1) ^ 0x07); } else { crc 1; } } } return crc; }读写8位寄存器时CRC只算命令字节。读写16位寄存器时命令字节、低数据字节、高数据字节都参与计算。计算顺序要严格按照发送顺序先算命令字再算数据字节不能搞反。3.4 完整的读写寄存器代码有了原语和CRC上层读写就简单了。下面我给出一个完整的HDQ读写实现支持8位和16位模式。这段代码我做过移植验证换了几个MCU平台把延时函数和对GPIO的宏改一下就能用。// 命令字定义 #define HDQ_CMD_READ_8BIT 0x15 #define HDQ_CMD_WRITE_8BIT 0x14 #define HDQ_CMD_READ_16BIT 0x17 #define HDQ_CMD_WRITE_16BIT 0x16 // 发送一个字节LSB first static void HDQ_SendByte(uint8_t data) { for (uint8_t i 0; i 8; i) { HDQ_SendBit(data 0x01); data 1; } } // 接收一个字节LSB first static uint8_t HDQ_RecvByte(void) { uint8_t data 0; for (uint8_t i 0; i 8; i) { data 1; if (HDQ_ReadBit()) { data | 0x80; } } return data; } // 读8位寄存器带CRC校验 int HDQ_Read8(uint8_t reg, uint8_t *val) { uint8_t cmd HDQ_CMD_READ_8BIT; uint8_t crcRecv; HDQ_SendBreak(); HDQ_SendStart(); HDQ_SendByte(cmd); HDQ_SendStart(); *val HDQ_RecvByte(); crcRecv HDQ_RecvByte(); if (HDQ_CalcCRC8(cmd, 1) ! crcRecv) { return -1; } return 0; } // 写8位寄存器发送数据CRC void HDQ_Write8(uint8_t reg, uint8_t val) { uint8_t cmd HDQ_CMD_WRITE_8BIT; uint8_t crc HDQ_CalcCRC8(val, 1); HDQ_SendBreak(); HDQ_SendStart(); HDQ_SendByte(cmd); HDQ_SendStart(); HDQ_SendByte(val); HDQ_SendByte(crc); } // 读16位寄存器带CRC校验 int HDQ_Read16(uint8_t reg, uint16_t *val) { uint8_t cmd HDQ_CMD_READ_16BIT; uint8_t buf[2]; uint8_t crcRecv; HDQ_SendBreak(); HDQ_SendStart(); HDQ_SendByte(cmd); HDQ_SendStart(); buf[0] HDQ_RecvByte(); buf[1] HDQ_RecvByte(); crcRecv HDQ_RecvByte(); if (HDQ_CalcCRC8(buf, 2) ! crcRecv) { return -1; } *val (uint16_t)((uint16_t)buf[1] 8) | buf[0]; return 0; } // 写16位寄存器发送数据CRC void HDQ_Write16(uint8_t reg, uint16_t val) { uint8_t cmd HDQ_CMD_WRITE_16BIT; uint8_t buf[2]; buf[0] val 0xFF; buf[1] val 8; HDQ_SendBreak(); HDQ_SendStart(); HDQ_SendByte(cmd); HDQ_SendStart(); HDQ_SendByte(buf[0]); HDQ_SendByte(buf[1]); HDQ_SendByte(HDQ_CalcCRC8(buf, 2)); }写8位时CRC只算val因为命令字0x14按手册是固定的有些芯片要求把命令字也算进去但我实测下来大多数BQ芯片只校验数据字节所以代码里写的是只算数据。如果你用的芯片手册明确写了CRC覆盖范围包含命令字那就把计算改成uint8_t crcBuf[2] {cmd, val}; uint8_t crc HDQ_CalcCRC8(crcBuf, 2);以具体芯片手册为准这个坑我放在后面的排查实录里细说。3.5 移植到不同MCU的注意事项这套代码移植性已经做得比较好了但有几个地方每换一个平台都要重新检查。延时函数的精度是第一位。HDQ的时序参数以微秒为单位而且Break需要250us这种相对较长的延时写“1”只需要5us的短延时所以延时函数必须能覆盖5us到几百us的范围且误差可控。裸的for循环延时在同一个编译器下没问题但优化等级一变、主频一变延时误差就可能超过时序容限。我推荐用DWTCortex-M内核自带的周期计数器或者一个1us时基的定时器来实现延时这样不管优化等级怎么调延时始终稳定。第二个地方是临界区保护。HDQ通信期间如果来了中断中断服务程序里的代码可能会占用几十甚至上百微秒直接把bit周期拉爆导致整帧通信失败。因此我会建议在每次HDQ事务的开始和结束处关中断/开中断。在STM32上可以用__disable_irq()和__enable_irq()在别的平台也有对应的开关中断接口。注意关中断的时间不要拖太长HDQ一帧读写加起来大概5ms左右期间所有中断都进不来对系统实时性要求高的场景要考虑用互斥量加超时重试来代替全程关中断。第三个地方是GPIO模式的切换。HDQ需要在输出和输入之间切换开漏输出模式下写1就是释放总线此时直接读回IDR就是外部电平不需要切到输入模式。但有的MCU开漏模式下读回的电平是引脚电平而非输出寄存器值这个和型号有关读一下芯片手册确认即可。4. 实测踩坑与问题排查4.1 时序不对导致通信失败我最早调HDQ的时候波形怎么看怎么对从机就是不回应。后来用示波器抓了Break、Start、每个数据位的波形才发现Break时间给的太短了。我一开始写的是150us某些芯片资料里写最小190us我自以为够用结果换了不同品牌的电量计芯片有的就是识别不了。把Break统一改成250us之后所有型号都稳定了。另一个常见的时序问题是写“1”的低电平时间过长。如果写“1”超过了8us甚至10us从机就会把“1”误判成“0”。这类问题在软件模拟时特别隐蔽因为本身代码逻辑没错纯粹是延时误差累积。排查办法很简单抓波形量一下低电平时间和手册对比一目了然。4.2 上拉电阻选型与布线上拉电阻这个问题起初我没太在意用了10kΩ。结果在常温下通信正常一到低温环境比如零下10度就偶发通信失败。后来排查发现低温下锂电池电压会下降而HDQ线上电平建立时间变长10kΩ上拉在极端情况下带不动线上的等效电容。换成了4.7kΩ后低温问题再没出现过。如果你用的是BQ27441这种带内部上拉的芯片外部上拉电阻仍然建议加。因为内部上拉阻值很大只能保证空闲状态电平稳定无法在高速翻转时提供足够的驱动能力。布线方面HDQ线尽量短、避免和电源线长距离平行走线。HDQ本身是低速协议受干扰的容错能力还行但如果线走了很远又绕了好几圈分布电容会把波形边沿拖慢最终导致采样点判错。4.3 电量计状态与寄存器访问的坑HDQ通信看起来是纯物理层问题但我在实际项目里发现很多“通信异常”其实是电量计芯片状态导致的。BQ系列电量计上电后很可能处于休眠模式或者shipping modeHDQ总线上没有响应。这时候需要先发送一个Break来唤醒或者单独配置GPIO触发wake。有些芯片第一次上电还要完成初始配置和校准未配置完成时读出来的数据可能全是0xFF或明显异常值。寄存器保护机制也是个麻烦事。BQ系列电量计很多寄存器是有保护等级的比如“unsealed”和“sealed”状态。在sealed状态下大部分配置寄存器是只读的想修改必须先发unseal命令。我有一次写配置寄存器怎么都写不进去排查很久才发现是芯片处于sealed状态根本不接收写操作。如果你在调试HDQ写操作时遇到“看起来发了但寄存器值没变”的情况优先检查芯片状态和保护等级。4.4 常见问题速查表我把这几年来遇到的和HDQ相关的杂症整理成一个速查表调试的时候可以直接对照着查现象可能原因解决方法通信完全无响应示波器看不到任何波形GPIO配置成推挽输出、上拉缺失改为开漏输出加4.7kΩ上拉从机能收到命令但数据全FFBreak时间不够、从机未唤醒把Break加长到250us以上数据时好时坏偶发性读写失败中断打断了HDQ时序通信期间关中断加重试机制读出的数据不对但CRC校验通过数据位序错误LSB/MSB搞混确认LSB first检查收发函数数据不对且CRC校验失败采样点太早/太晚调整读bit的采样延时用示波器确认写寄存器没反应芯片处于保护状态检查sealed状态先解锁再写低温环境下通信不稳定上拉电阻太大改用4.7kΩ或更小上拉排查HDQ问题我始终推荐一个笨但有效的办法把逻辑分析仪的采样率调到10MHz以上直接抓HDQ引脚的原始波形然后和时序表逐项对比。软件逻辑问题通过看波形能好理解很多很多看起来玄学的问题最后都是时序参数差那么几微秒。调试HDQ协议最好配一个TI官方的EV2400适配器加bqStudio上位机先把电量计配置好、确认寄存器能正常读写再调试自己的MCU代码。这套工具链可以帮你把“电量计本身的问题”和“HDQ通信的问题”快速切分开来。我自己每次遇到诡异问题都会先把EV2400接上去读一遍如果EV2400能正常读到数据那问题大概率出在自己的代码侧如果EV2400也读不到那就是电量计侧的问题比如芯片坏、焊接不良或者供电异常。HDQ这协议看起来小众但只要掌握了单线脉宽调制的思路再看其他类似协议都会觉得触类旁通。希望这篇内容能帮你在BQ电量计的调试路上省下几个加班的夜晚。本文还有配套的精品资源点击获取
返回列表