拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32接LTC6804-1做级联电池采集:SPI时序、PEC校验与数据合并

简介这套源码面向STM32单片机开发者与电池管理系统研究人员完整实现了通过LTC6804-1芯片读取级联电池电压的功能工程基于STM32F10x系列包含RCC、GPIO、串口、DMA及LTC6804初始化等核心配置可直接在Keil环境中打开编译也可作为BMS电压采集模块的参考实现。资源包共188个文件压缩后大小6.35MB以C语言源码.c、头文件.h为主并附带Keil工程文件.uvproj、编译中间文件.o、.crf及启动文件.s便于查看完整构建过程与调试入口。已有1156人学习下载适合需要上手LTC6804驱动或了解级联电池电压采集逻辑的嵌入式工程师。通过阅读main函数与LTC6804_adc等关键代码可掌握LTC6804的初始化流程、ADC转换触发及电压数据读取方法节省从零查阅数据手册的时间。1. 把 LTC6804-1 接到 STM32 上做级联电池电压采集最常被低估的不是 ADC 精度而是软件栈的层次芯片内部的 ADC、滤波、基准源都已经做好了主机要做的只是通过 SPI 把命令发进去再把一包一包 16 位电压数据原样取回来。真正难伺候的反而是三件事SPI 时序和片选控制、命令帧自带的 PEC 校验、以及菊花链回传时“最远端的芯片最先回来”这个反直觉的数据顺序。本文把这些层拆开讲给出可以直接照抄的参数、函数和工程组织方式适合手头有一个 LTC6804-1 工程源码包、正准备移植到自己的 BMS 项目里去的嵌入式开发。如果你刚拿到一个“软件程序工程源码.zip”先别急着编译读完第二、三章再动手能少走大半弯路。2. 先打通物理层SPI 速率、片选时序和 PEC 校验这三件事没有先后2.1 为什么要先确认 SPI 速率和片选极性再写任何寄存器LTC6804-1 是一颗电池监控前端通信接口是 SPI但它和常见的 Flash、传感器不一样对 SCLK 频率有明确上限。常见做法是把 STM32 的 SPI 预分频放到 8 分频或 16 分频让实际 SCLK 落在 1 MHz 以内而不是追求满速。接口速率不是越高越好超过器件规格后表现为命令偶尔生效、读回数据错位而且这种故障很难用示波器一次抓准。片选信号同样有讲究。LTC6804-1 要求 CS 在“命令帧 后续数据帧”的整个事务期间保持低电平不能像操作普通 SPI 从机那样每收发一个字节就拉高一次 CS。很多移植失败的工程症状是读回全是 0xFF 或 0x00根因不是 SPI 没初始化好而是片选在命令和读数据之间被中断了器件认为本次事务非法。还有一点容易被忽略LTC6804-1 支持 SPI Mode 0 和 Mode 3具体用哪个以数据手册为准。我一般先按 Mode 0 配置如果读回数据全 FF先把 CPOL/CPHA 切到 Mode 3 试一次比盲目调分频更有效。2.2 16 位命令帧加 8 位 PEC写一次命令其实要发 3 个字节LTC6804-1 的每条命令是 16 位命令字后面跟着 8 位 PEC 校验合计 24 位。PEC 是器件自带的 CRC-15 校验结果取低 8 位跟在命令后面。命令码不是随意编的数据手册命令表里能查到常见几条如下命令命令字用途ADCV0x03启动电压 ADC 转换RDCVA0x04读电压寄存器组 ARDCVB0x06读电压寄存器组 BRDCVC0x08读电压寄存器组 CRDSTATA0x0A读状态寄存器组 AWRCONF0x1A写配置寄存器组RDCONF0x2A读配置寄存器组注意顺序命令低字节先发高字节后发最后发 PEC。如果 PEC 不对器件直接丢弃整条命令不会返回错误标志肉眼看到的现象就是“发了命令没反应”。这也是读电压全 0xFF 的第二个高发原因。2.2.1 计算 PEC 的 CRC-15 算法PEC 本质上是 CRC-15生成多项式 0x4599初始值为 0。逐字节输入命令字最后取结果的高 8 位。参考实现如下#define CRC15_POLY 0x4599 uint16_t ltc6804_pec15(uint8_t *data, uint8_t len) { uint16_t crc 0x0000; uint8_t i, j; for (i 0; i len; i) { crc ^ data[i]; for (j 0; j 8; j) { if (crc 0x8000) { crc (crc 1) ^ CRC15_POLY; } else { crc 1; } } } return (crc 0x7FFF) 7; }这段代码直接用位循环算 CRC-15输入命令字节流输出 8 位 PEC。参数data是待校验的字节数组len是字节数调用时传入命令字的低字节和高字节两个元素即可。如果要压榨性能可以改成查表法但在 1 MHz SPI 下位循环的开销完全可以忽略。2.3 用 HAL 库封装一条“命令加读回”的通用 SPI 函数工程里所有 LTC6804 操作都该收敛到一个 SPI 交换函数里不要在业务代码里直接调 HAL_SPI_Transmit。通用函数长这样uint8_t ltc6804_spi_transfer(uint16_t cmd, uint8_t *data, uint8_t len) { uint8_t tx_buf[3]; uint8_t rx_dummy[3]; tx_buf[0] cmd 0xFF; // 命令低字节先发 tx_buf[1] (cmd 8) 0xFF; // 命令高字节 tx_buf[2] ltc6804_pec15(tx_buf, 2); // 前两字节算 PEC HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(hspi1, tx_buf, rx_dummy, 3, 100); if (len 0) { HAL_SPI_Receive(hspi1, data, len, 100); } HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); return 0; }逻辑说明先组 3 字节命令帧拉低 CS发命令然后在不拉高 CS 的前提下继续读回len字节数据。len为 0 时只发命令例如启动转换len为 8 时读回一个寄存器组6 字节数据加 2 字节 PEC。参数data指向接收缓冲区cmd传上小节的命令字。片选拉低到发出第一个字节之间要留一点延时这个延时在 HAL 库的 SPI 传输内部已经有处理但如果换成寄存器版或纯 GPIO 模拟 SPI必须在 CS 拉低后加 2~3 个空指令延时。寄存器版和标准外设库版的移植也是改这个函数而已上层逻辑完全不用动。3. 单芯片读电压ADCV 之后真正花时间是数据重组3.1 发起转换的 ADCV 命令和它携带的量程、模式位读取电压不是直接发 RDCVA 就能拿到数据。芯片必须先执行一次电压 ADC 转换转换完成后再去读寄存器组。ADCV 命令除了命令字 0x03还携带转换模式、量程、自测模式等配置位。这些位在数据手册 ADCV 命令一节有明确表格移植时建议先按默认量程和最慢滤波模式跑通再去优化速度。转换需要时间而且级联时是多颗芯片同时转换等待时间按链上芯片数线性增长。常见做法是发完 ADCV 后延时延时时长按“每片约 2 ms”估算链上有 10 片就至少等 20 ms 以上。延时不够的典型症状是读回电压里有几片是 0因为数据还没准备好就被读走了。void ltc6804_adcv_start(uint8_t chain_len) { uint16_t adcv_cmd 0x03; // 默认量程默认滤波模式 ltc6804_spi_transfer(adcv_cmd, NULL, 0); HAL_Delay(2 * chain_len 2); // 每片约 2ms留 2ms 裕量 }参数说明chain_len是级联芯片数量单芯片时传 1。延时时间取“转换时间上限 裕量”不要取典型值温度低时晶振和芯片内部 RC 都会漂裕量不足会偶发性读到旧数据。3.2 电压寄存器组 A/B/C 的字节序和拼接顺序LTC6804-1 最多测量 12 节电芯电压分成三组寄存器每组 4 个电芯。每组数据帧是 6 字节电压数据加 2 字节 PEC总共 8 字节。读回后先把 PEC 校验掉再把 6 字节按每 2 字节一个电芯拼成 16 位无符号数。寄存器组与电芯的对应关系如下寄存器组命令包含电芯数据区字节数电压组 ARDCVACell 1~46电压组 BRDCVBCell 5~86电压组 CRDCVCCell 9~126拼接顺序是高位在前每节电芯的电压占 2 字节第一个字节是高 8 位第二个字节是低 8 位。读回组 A 的 6 字节数据后Cell 1 电压就是(data[0] 8) | data[1]Cell 2 是(data[2] 8) | data[3]依此类推。有些工程源码会把高低字节顺序搞反导致电压读数在 0~255 之间跳那就是拼接时把首字节当成低位了。3.3 实际工程里的电压换算和滤波宏、浮点与均值拼出来的 16 位原始值要换算成毫伏或微伏换算系数来自数据手册“电压转换结果”一节不同配置下系数不同。代码里不要散落魔法数字应该用一个宏定义#define V_LSB_UV 100 // 1 LSB 对应的微伏数按手册配置填写 uint32_t ltc6804_raw_to_uv(uint16_t raw) { return (uint32_t)raw * V_LSB_UV; }参数说明raw是寄存器拼出来的 16 位原始值返回值是微伏。V_LSB_UV这个宏必须按数据手册核对不同量程配置下 LSB 权重不一样先用精密电压源标定一次再固化成常量比直接照抄网上代码可靠得多。电压数据抖动是正常的尤其是多片级联后电源噪声变大。我一般不会对原始值直接做很强滤波而是先做一次剔除野值再做滑动平均#define SAMPLE_BUF_SIZE 8 uint32_t v_filtered[SAMPLE_BUF_SIZE]; uint8_t v_filter_idx 0; uint32_t voltage_filter(uint32_t new_value) { uint32_t sum 0; uint8_t i; v_filtered[v_filter_idx] new_value; v_filter_idx (v_filter_idx 1) % SAMPLE_BUF_SIZE; for (i 0; i SAMPLE_BUF_SIZE; i) { sum v_filtered[i]; } return sum / SAMPLE_BUF_SIZE; }逻辑说明维护一个 8 个元素的环形缓冲每次读回都覆盖最旧的值输出均值。这种滤波对随机噪声有效但对真实电压跌落会有约 8 个周期的延迟做保护逻辑时不要直接用滤波后的值判断过压原始值或浅滤波值要另留一路。4. 级联读回链上配置“分地址写入”数据合并“反向取值”4.1 菊花链的两种工作相位配置广播与数据回传级联模式下主控 SPI 只接到最底层的那颗 LTC6804-1后续芯片通过片间接口串联。SDO 数据是逐级传回主控的最顶层的芯片数据最先被移出来经过中间芯片一级一级往下传最后到达主控的 MISO。也就是说主控读回数据时收到的第一个字节属于链上地址最大的芯片最后一组数据才属于最底层芯片。这个顺序和多数人的直觉相反是级联合并最容易出错的地方。级联工作分两个相位。配置相位里主机要保证每颗芯片写进自己的地址和参数转换相位则简单得多ADCV 是广播命令一颗芯片发起整条链都开始转换等待时间随链长线性增加。完成转换后主机逐组读回电压寄存器把所有芯片的数据拼接成一个完整电池包电压表。4.2 写配置时地址挨个递增读回时把数组倒序归位LTC6804-1 的器件地址写在配置寄存器组 B 的高 4 位里链上从底层到顶层依次是 0x0、0x1、0x2……写配置时地址不能一样否则所有芯片都认领同一个地址读回的数据会互相覆盖。级联写配置的组帧方式和单芯片不同要按数据手册“写配置寄存器组”一节的菊花链说明来拼接这一点直接对照官方驱动确认比猜更稳。读回合并时应用层要主动反转顺序。我一般这样处理#define CELLS_PER_IC 4 // 每组寄存器包含 4 个电芯 void ltc6804_merge_chain_data(uint16_t *group_buf, uint16_t *cell_buf, uint8_t ic_count) { uint8_t i; // group_buf 按读回顺序存放下标 0 是最远端芯片的数据 for (i 0; i ic_count; i) { uint8_t target_ic ic_count - 1 - i; memcpy(cell_buf[target_ic * CELLS_PER_IC], group_buf[i * CELLS_PER_IC], CELLS_PER_IC * sizeof(uint16_t)); } }参数说明group_buf是 SPI 读回的原始数据区按寄存器组顺序依次存放下标 0 对应最远端芯片cell_buf是给应用层用的最终数组下标 0 对应底层芯片。循环里把读回的第i组数据放到target_ic的位置完成倒序归位。不做这一步电池编号和实际电芯位置会整个镜像翻转轻则显示错位重则均衡策略打错对象。4.3 一个最小级联读电压流程完整流程至少分三步启动转换、按组读回、重排合并。伪代码级别的实现如下#define IC_COUNT 6 #define GROUP_SZ 8 // 6 字节数据 2 字节 PEC uint8_t raw_buf[IC_COUNT * GROUP_SZ]; uint16_t cell_uv[IC_COUNT * CELLS_PER_IC]; void bms_read_all_voltage(void) { uint8_t i; ltc6804_adcv_start(IC_COUNT); // 广播 ADC 转换 // 逐组读回这里以电压组 A 为例B/C 逻辑相同 for (i 0; i IC_COUNT; i) { ltc6804_spi_transfer(0x04, raw_buf[i * GROUP_SZ], GROUP_SZ); } // 校验每组末尾 2 字节 PEC for (i 0; i IC_COUNT; i) { if (!ltc6804_check_reg_pec(raw_buf[i * GROUP_SZ], GROUP_SZ)) { // 上报链路异常本帧丢弃 } } ltc6804_merge_chain_data((uint16_t *)raw_buf, cell_uv, IC_COUNT); }逻辑说明先发 ADCV 让整条链同时转换再连续读回 N 组电压数据。读回时 CS 保持低电平所有组的读回在同一个事务里完成更稳妥我的示例是按组单独做实际工程里也可以一次性读回N * 8字节效果一样。PEC 校验必须做它不仅是校验传输错误还能间接反映链路里哪颗芯片的通信开始劣化。4.4 源码包工程组织驱动层和应用层分开才找得到人一个能长期维护的 LTC6804-1 工程文件结构通常长这样app/ bsp/ bsp_spi.c # STM32 SPI 和片选初始化 bsp_ltc6804.c # 协议层封装 bsp_ltc6804.h protocol/ ltc6804_cmd.h # 命令字、寄存器地址定义 ltc6804_pec.c # CRC-15 校验实现 service/ bms_voltage.c # 滤波、换算、保护判断 bms_voltage.h文件/目录职责改动频率bsp_spi.cSPI 引脚、时钟、DMA 配置换平台时改bsp_ltc6804.c命令拼装、数据读回、PEC 组帧协议调整时改ltc6804_pec.cCRC-15 算法几乎不改bms_voltage.c换算、滤波、阈值判断业务需求变化时改拿到一个现成的 zip 工程包我一般先打开 bsp_ltc6804.c 和 ltc6804_pec.c 两个文件确认 PEC 算法是不是对 16 位命令生成 8 位校验值再看 bsp_spi.c 里 SCLK 分频是否低于 1 MHz。这两个点没问题剩下的就是业务层移植。Keil MDK 或 VSCode 加 Embedded IDE 都行关键是芯片包版本要和工程里 CubeMX 生成代码的 HAL 库版本一致否则编译报的错会让人误以为代码有问题。5. 验证读数和观察链路健康把“能跑”变成“可以上线”5.1 用自测模式代替精密电压源做初步验证没有精密电压源的时候可以先不接电池组直接用 LTC6804-1 的自测模式验证链路。ADCV 命令里有自测位设置后芯片内部会切换到已知参考电压去转换不去测外部电芯。把自测读回的原始值和手册给出的自测典型值对比能同时验证 SPI 通信、PEC 校验和拼接顺序三件事。如果自测值对得上再接电池组就可以把怀疑重点放到外部采样电阻和滤波电路上。5.2 让每颗芯片在状态寄存器里留下温度当作链路健康位LTC6804-1 内部有温度传感器状态寄存器组 A 里能读回芯片温度。级联工程里我习惯把每颗芯片的温度一起读回来这不仅是热管理需要更是一个廉价的链路健康探针。温度读数正常说明那颗芯片的通信路径是通的哪颗芯片温度读回 0 或乱码故障点大概率就在它和上一级之间。这个思路比单纯看电压 CRC 错误计数更直观因为温度值本身有合理范围一眼就能看出异常。5.3 用定时器调度转换相位链越长越需要显式状态机转换等待时间随链长线性增长在 1 kHz 控制循环里直接 HAL_Delay 等转换会卡死整个任务。我一般把读取流程拆成一个状态机static uint8_t phase 0; static uint16_t tick_count 0; #define WAIT_TICKS (2 * IC_COUNT 2) void bms_1ms_tick(void) { switch (phase) { case 0: ltc6804_adcv_start(IC_COUNT); tick_count 0; phase 1; break; case 1: if (tick_count WAIT_TICKS) { bms_read_all_voltage(); phase 0; } break; default: phase 0; break; } }参数说明phase 0负责启动转换phase 1负责等待和读回。WAIT_TICKS按芯片数量放大链越长等待周期越长。这样主循环不会被阻塞看门狗也不用加长超时。链路稳定后再把 SPI 读回放到 DMA 里让状态机变得更精简——这个优化的收益在链长超过 6 颗时非常明显。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门