范围与系统分工
跳转到“范围与系统分工”BQ27427 是单节锂电池电量计,主机通过 I²C 读取电压、温度、电流、剩余容量和 SOC,并按电池与系统参数配置其估算模型。它与充电器、电池保护、模拟增益控制分属不同职责;读出 75% 并不证明充电器正在工作,也不代表已完成参数配置。
本页给出经过编译和模拟总线测试的 C 读数层,以及配置流程的检查点。具体 STM32 引脚、I²C 外设、RTOS、超时和电池参数由板级适配层提供。串口协议和 Python 界面见电量与增益控制协议。
flowchart LR B[电池与采样电路] --> G[BQ27427] G <-->|I²C| D[单一电量计任务] D --> S[带时间与有效状态的读数] S --> P[串口协议任务] P <-->|UART| H[上位机] P --> A[增益硬件控制]地址、寄存器与数值类型
跳转到“地址、寄存器与数值类型”七位 I²C 地址是 0x55;在线路上,包含读写位的地址字节为写 0xAA、读 0xAB。本页适配函数接收七位地址。若底层 STM32 HAL 接口要求左移地址,应只在适配层转换一次,不能在调用链中重复左移。
以下为本应用相关命令。普通双字节读数按低字节在前组装;Data Memory 配置中的双字节参数则按其另行规定的高字节在前格式处理。
| 命令 | 起始地址 | 长度 | 含义或单位 |
|---|---|---|---|
| Control | 0x00 | 2 | 子命令及其结果,例如 DEVICE_TYPE |
| Temperature | 0x02 | 2 | 无符号,0.1 K |
| Voltage | 0x04 | 2 | 无符号,mV |
| Flags | 0x06 | 2 | 状态位 |
| NominalAvailableCapacity | 0x08 | 2 | 无符号,mAh,标称可用容量 |
| FullAvailableCapacity | 0x0A | 2 | 无符号,mAh,可用满容量 |
| RemainingCapacity | 0x0C | 2 | 无符号,mAh,当前条件下剩余容量 |
| FullChargeCapacity | 0x0E | 2 | 无符号,mAh,当前条件下满充容量 |
| AverageCurrent | 0x10 | 2 | 有符号,mA;正值为净充电、负值为净放电 |
| AveragePower | 0x18 | 2 | 有符号,mW |
| StateOfCharge | 0x1C | 2 | 无符号百分比,正常范围 0~100 |
| InternalTemperature | 0x1E | 2 | 无符号,0.1 K,内部温度 |
| StateOfHealth | 0x20 | 1 | 百分比,按 Rev. B 第 5.13 节读取低字节 |
| FullChargeCapacityFiltered | 0x2E | 2 | 无符号,mAh,滤波后的满充容量 |
原寄存器表中的 StandbyCurrent(0x12)、MaxLoadCurrent(0x14) 不能作为 BQ27427 的标准命令使用;0x20 不是循环次数,0x2E 不是健康度。当前 TRM 总表遗漏了 SOH 项,但第 5.13 节明确给出 0x20 和单字节返回。应同时核对命令章节,不能从相近型号搬运地址。TI BQ27427 技术参考手册
温度的精确换算为:
采用有符号 32 位百分之一摄氏度可保留换算精度,例如原始值 2731 对应 −0.05 ℃,不能简单减去整数 2731 并声称精确到 0.1 ℃。协议需要 0.1 ℃ 时,再由发送端约定舍入和溢出处理。
状态位要表达什么
跳转到“状态位要表达什么”| Flags 位 | 名称 | 应用解释 |
|---|---|---|
| 0 | DSG | 电量计放电状态;其模式说明中,放电及 RELAX 均可置位,不能取反后直接当成充电器连接状态 |
| 1 / 2 | SOCF / SOC1 | 两级低电量警告,阈值由配置决定 |
| 3 | BAT_DET | 检测到电池;受 BIN/BIE 或主机电池插入命令的工作方式影响 |
| 4 | CFGUPMODE | 正处于配置更新模式 |
| 5 | ITPOR | 配置 RAM 已复位,需要按应用重新装入配置 |
| 8 | CHG | 允许快速充电;不是“当前正在充电” |
| 9 | FC | 电量计满充判定 |
| 14 / 15 | UT / OT | 低温/过温状态;原文 OTD、OTC 命名不适用于此处 |
原始标志位和通信成功应分别保存。BAT_DET 未建立、配置丢失或尚处于配置模式时,读操作可以成功,但不能因此把 SOC 当成已正确初始化的有效估计。充电状态若面向用户,应按充电器状态、净电流阈值与系统工作模式共同定义;零电流还可能意味着静置。SOC 的 0%/100% 是算法和系统截止条件下的估计端点,不等于物理电荷绝对为零或必然已触发充电器满充。
总线时序与所有权
跳转到“总线时序与所有权”同一个任务或受互斥保护的事务负责访问电量计,尤其要保护“写 Control 子命令—等待—读结果”的整个序列。仅给每个底层 I²C 调用加锁,会允许其他任务在间隙覆盖子命令。
当前手册要求包间总线空闲至少 66 µs;超过 100 kHz 时写入采用单字节写,读取仍可多字节。读写型命令结果保守等待至少 2 s,普通只读命令无需这种结果等待,但同一标准命令不应每秒访问超过两次。因此本例每轮结束后至少间隔 1 s 才允许下一轮,失败也限速。66 µs 来自手册图示,PDF 文本提取有时会把 µs 错读成 ms。
底层还需支持时钟延展,并设置有限超时;手册给出的延展上限随运行模式不同。不要把一次 100 ms 超时乘上多次访问后仍称为“5 ms 完成”。长等待在 RTOS 中应让出处理器;总线失败后的重探测也应由任务退避安排,不能在中断里循环阻塞。
C 参考读数层
跳转到“C 参考读数层”将下列代码保存为 bq27427_read.c,或拆成头文件与实现文件。bq_bind 绑定同步适配函数,bq_probe 检查精确型号 0x0427;它们没有执行电池参数配置。bq_read_sample 先读局部变量,只有全部读数及范围检查成功才覆盖输出,失败时保留上次成功数据。调用者必须另存有效状态和时间,避免把旧值当成新读数。
/* BQ27427 readout example. Single owner; the caller owns bus serialization. */#include <stdint.h>#include <stddef.h>#include <string.h>
enum { BQ_OK = 0, BQ_PARAM = -1, BQ_IO = -2, BQ_WRONG_DEVICE = -3, BQ_NOT_PROBED = -4, BQ_RANGE = -5, BQ_NOT_DUE = -6};enum { BQ_DSG = 1u << 0, BQ_SOCF = 1u << 1, BQ_SOC1 = 1u << 2, BQ_BAT_DET = 1u << 3, BQ_CFGUPMODE = 1u << 4, BQ_ITPOR = 1u << 5, BQ_CHG_ALLOWED = 1u << 8, BQ_FC = 1u << 9, BQ_UT = 1u << 14, BQ_OT = 1u << 15};
typedef struct { void *user; /* Synchronous transfers; return 0 only after all requested bytes complete. addr7 is 0x55, reg is an 8-bit command address. Use finite bus timeouts. */ int (*read)(void *, uint8_t addr7, uint8_t reg, uint8_t *, size_t); int (*write_byte)(void *, uint8_t addr7, uint8_t reg, uint8_t); /* Wait at least this duration; an RTOS port should yield for long waits. */ void (*wait_us)(void *, uint32_t); uint32_t (*now_ms)(void *);} BqBus;
typedef struct { uint8_t soc_pct, soh_pct; uint16_t voltage_mv, remaining_mah, full_mah, flags; int16_t current_ma, power_mw; int32_t temperature_centi_c; uint32_t started_ms, completed_ms;} BqSample;
typedef struct { BqBus bus; uint32_t last_attempt_end_ms; uint8_t probed, attempted;} BqGauge;
int bq_bind(BqGauge *g, const BqBus *bus){ if (!g || !bus || !bus->read || !bus->write_byte || !bus->wait_us || !bus->now_ms) return BQ_PARAM; BqGauge fresh = {0}; fresh.bus = *bus; *g = fresh; return BQ_OK;}
static int bq_read_bytes(BqGauge *g, uint8_t reg, uint8_t *p, size_t n){ int result = g->bus.read(g->bus.user, 0x55, reg, p, n); g->bus.wait_us(g->bus.user, 66); /* Packet-to-packet bus-free interval. */ return result == 0 ? BQ_OK : BQ_IO;}
static int bq_read_u16(BqGauge *g, uint8_t reg, uint16_t *value){ uint8_t p[2]; int result = bq_read_bytes(g, reg, p, sizeof p); if (result == BQ_OK) *value = (uint16_t)((uint16_t)p[1] << 8 | p[0]); return result;}
static int16_t bq_signed_u16(uint16_t value){ /* Keep the cast in range, including on pre-C23 implementations. */ int32_t signed_value = (value & 0x8000u) ? (int32_t)value - 65536 : value; return (int16_t)signed_value;}
int bq_probe(BqGauge *g){ if (!g || !g->bus.read || !g->bus.write_byte || !g->bus.wait_us || !g->bus.now_ms) return BQ_PARAM; g->probed = 0; /* DEVICE_TYPE=0x0001. Separate one-byte writes also support >100 kHz. No other owner may write Control() until this transaction finishes. */ const uint8_t bytes[2] = {0x01, 0x00}; for (uint8_t reg = 0; reg < 2; ++reg) { int result = g->bus.write_byte(g->bus.user, 0x55, reg, bytes[reg]); g->bus.wait_us(g->bus.user, 66); if (result != 0) return BQ_IO; } /* Conservative read/write-command result wait from TRM 3.3. */ g->bus.wait_us(g->bus.user, 2000000); uint16_t id; if (bq_read_u16(g, 0x00, &id) != BQ_OK) return BQ_IO; if (id != 0x0427) return BQ_WRONG_DEVICE; g->probed = 1; return BQ_OK;}
static int bq_sample_attempt(BqGauge *g, BqSample *out){ BqSample sample = {0}; uint16_t soc, current, temperature, power; sample.started_ms = g->bus.now_ms(g->bus.user); if (bq_read_u16(g, 0x1C, &soc) != BQ_OK || bq_read_u16(g, 0x04, &sample.voltage_mv) != BQ_OK || bq_read_u16(g, 0x10, ¤t) != BQ_OK || bq_read_u16(g, 0x02, &temperature) != BQ_OK || bq_read_u16(g, 0x0C, &sample.remaining_mah) != BQ_OK || bq_read_u16(g, 0x0E, &sample.full_mah) != BQ_OK || bq_read_u16(g, 0x18, &power) != BQ_OK || bq_read_bytes(g, 0x20, &sample.soh_pct, 1) != BQ_OK || bq_read_u16(g, 0x06, &sample.flags) != BQ_OK) return BQ_IO; if (soc > 100 || sample.soh_pct > 100) return BQ_RANGE; sample.soc_pct = (uint8_t)soc; sample.current_ma = bq_signed_u16(current); sample.power_mw = bq_signed_u16(power); sample.temperature_centi_c = (int32_t)temperature * 10 - 27315; sample.completed_ms = g->bus.now_ms(g->bus.user); *out = sample; /* Publish only after every read and validation succeeds. */ return BQ_OK;}
int bq_read_sample(BqGauge *g, BqSample *out){ if (!g || !out) return BQ_PARAM; if (!g->probed) return BQ_NOT_PROBED; uint32_t now = g->bus.now_ms(g->bus.user); if (g->attempted && (uint32_t)(now - g->last_attempt_end_ms) < 1000u) return BQ_NOT_DUE; int result = bq_sample_attempt(g, out); /* Failed attempts are also rate-limited; the caller retains prior data. */ g->last_attempt_end_ms = g->bus.now_ms(g->bus.user); g->attempted = 1; return result;}
void bq_forget_local_state(BqGauge *g){ if (g) g->probed = 0; /* No bus command: this does not reset the gauge. */}这里的九次寄存器读取不是芯片原子快照,started_ms 与 completed_ms 只描述这一轮主机访问区间。调用层也不能让其他任务同时读写同一个 BqSample;可以使用队列按值发布、受锁保护地复制,或采用经过论证的快照机制。32 位毫秒计数的差值运算允许一次正常回绕,但系统不能在长期停调度后把多次完整回绕当作可测时间。
本例不把读取 SOH 的成功误称为健康诊断,不捏造循环次数,也不提供只修改本地变量却名为芯片“软复位”的函数。bq_forget_local_state 仅让主机下次重新探测;真实 RESET(0x0041) 与 SOFT_RESET(0x0042) 的作用和访问条件不同,必须在独立配置/恢复流程中使用。
配置不是“连通即可”
跳转到“配置不是“连通即可””量产配置至少要核对实际电池的化学模型、设计容量、设计能量及缩放、系统截止电压、充电终止条件、温度来源和电池插入检测方式。芯片提供有限的预设化学模型;不能只按标称电压猜测,也不能声称可以随意下载任意电芯模型。参数应来自电池资料、充电策略和实际验证。TI BQ27427 数据手册
下面是配置事务的顺序检查表;它没有替实际电池选择数值,也不是可以直接盲写到任何板卡的初始化脚本。
- 确认设备型号、密封状态及访问权限;需要时按设备既定解封流程进入 UNSEALED。暂停正常读数发布,并独占该器件的总线事务。
- 发送
SET_CFGUPDATE(0x0013),至少等待 1100 ms,然后以有限期限轮询 CFGUPMODE;未进入就退出并记录失败。 - 通过
BlockDataControl(0x61)、DataClass(0x3E)和DataBlock(0x3F)选择所需配置块。选择后至少等待 5 ms,再读取0x40~0x5F的完整 32 字节及旧校验,先确认旧块自洽。 - 在内存副本中修改已确认的字段,并保留其他字节。例如 State 类 ID 为十进制 82(
0x52),块 0 中 Design Capacity、Design Energy、Terminate Voltage、Taper Rate 的偏移分别为 6、8、10、21,均需按该字段规定处理高低字节与单位。Taper Rate 是比率参数,不能直接填充终止电流的 mA 数值。 - 按允许的写包方式写入更新字节,计算整个 32 字节块的校验:
checksum = 255 - (sum(block) & 0xFF),写0x60才使块内容提交到配置 RAM。随后至少等待 5 ms,重新选择同块并等待,回读内容和校验逐项验证。 - 发送
SOFT_RESET(0x0042)退出配置,等待并检查 CFGUPMODE、ITPOR 清除以及读数重新有效;按产品策略重新密封。失败不能简单跳到“初始化成功”。
这些参数保存在 RAM,电量计掉电后必须重新装入。CONFIG UPDATE 大约 240 s 后自动退出也不等于事务已经完整提交;主机应有更短的明确期限。上述 1100 ms 和数据块等待按当前手册第 5.1.9、8.5 节检查,旧示例中的约 1 s 不能覆盖新版明确下限。
集成与验证
跳转到“集成与验证”板级移植依次完成总线适配、型号探测、配置与有效性管理、定期读数、串口发布,最后再连接界面与增益硬件。增益命令只在硬件执行成功后返回成功 ACK;未知电量、失败状态和周期计数来源要显式表达,不能用零值代替有效数据。
| 现象 | 优先检查 |
|---|---|
| 无应答或型号不符 | 供电、共地、上拉、电平、七位/左移地址约定、器件实际型号 |
| 电流符号或温度异常 | 小端组装、有符号解码、温度单位、采样与温度源配置 |
| SOC 长期不变或跳变 | BAT_DET、ITPOR、CFGUPMODE、电池参数、负载条件和算法更新节奏 |
| 偶发错误或任务阻塞 | 包间隔、命令访问频率、时钟延展、有限超时、事务锁及中断中阻塞 |
| 上位机显示不可信 | 数据有效状态、采样/接收时间、协议字节序和充电语义 |
本例已通过 C11 严格警告编译、Cortex-M3 独立编译和主机 AddressSanitizer/UndefinedBehaviorSanitizer 检查。八组模拟总线测试覆盖精确型号、访问时序、每个读取步骤失败时输出不变、SOH 单字节地址、符号边界、温度精度、范围拒绝和限速回绕。它们不替代真实 I²C 波形、芯片配置、电池充放电曲线与 SOC 精度测试;原文“±1%”“初始化 20 ms”“读取 5 ms”等数字缺少对应测量条件,不作为本实现的实测结论。