電池采集:SPI時序、PEC校驗與數(shù)據(jù)合并)
簡介這套源碼面向STM32單片機開發(fā)者與電池管理系統(tǒng)研究人員完整實現(xiàn)了通過LTC6804-1芯片讀取級聯(lián)電池電壓的功能工程基于STM32F10x系列包含RCC、GPIO、串口、DMA及LTC6804初始化等核心配置可直接在Keil環(huán)境中打開編譯也可作為BMS電壓采集模塊的參考實現(xiàn)。資源包共188個文件壓縮后大小6.35MB以C語言源碼.c、頭文件.h為主并附帶Keil工程文件.uvproj、編譯中間文件.o、.crf及啟動文件.s便于查看完整構建過程與調試入口。已有1156人學習下載適合需要上手LTC6804驅動或了解級聯(lián)電池電壓采集邏輯的嵌入式工程師。通過閱讀main函數(shù)與LTC6804_adc等關鍵代碼可掌握LTC6804的初始化流程、ADC轉換觸發(fā)及電壓數(shù)據(jù)讀取方法節(jié)省從零查閱數(shù)據(jù)手冊的時間。1. 把 LTC6804-1 接到 STM32 上做級聯(lián)電池電壓采集最常被低估的不是 ADC 精度而是軟件棧的層次芯片內部的 ADC、濾波、基準源都已經(jīng)做好了主機要做的只是通過 SPI 把命令發(fā)進去再把一包一包 16 位電壓數(shù)據(jù)原樣取回來。真正難伺候的反而是三件事SPI 時序和片選控制、命令幀自帶的 PEC 校驗、以及菊花鏈回傳時“最遠端的芯片最先回來”這個反直覺的數(shù)據(jù)順序。本文把這些層拆開講給出可以直接照抄的參數(shù)、函數(shù)和工程組織方式適合手頭有一個 LTC6804-1 工程源碼包、正準備移植到自己的 BMS 項目里去的嵌入式開發(fā)。如果你剛拿到一個“軟件程序工程源碼.zip”先別急著編譯讀完第二、三章再動手能少走大半彎路。2. 先打通物理層SPI 速率、片選時序和 PEC 校驗這三件事沒有先后2.1 為什么要先確認 SPI 速率和片選極性再寫任何寄存器LTC6804-1 是一顆電池監(jiān)控前端通信接口是 SPI但它和常見的 Flash、傳感器不一樣對 SCLK 頻率有明確上限。常見做法是把 STM32 的 SPI 預分頻放到 8 分頻或 16 分頻讓實際 SCLK 落在 1 MHz 以內而不是追求滿速。接口速率不是越高越好超過器件規(guī)格后表現(xiàn)為命令偶爾生效、讀回數(shù)據(jù)錯位而且這種故障很難用示波器一次抓準。片選信號同樣有講究。LTC6804-1 要求 CS 在“命令幀 后續(xù)數(shù)據(jù)幀”的整個事務期間保持低電平不能像操作普通 SPI 從機那樣每收發(fā)一個字節(jié)就拉高一次 CS。很多移植失敗的工程癥狀是讀回全是 0xFF 或 0x00根因不是 SPI 沒初始化好而是片選在命令和讀數(shù)據(jù)之間被中斷了器件認為本次事務非法。還有一點容易被忽略LTC6804-1 支持 SPI Mode 0 和 Mode 3具體用哪個以數(shù)據(jù)手冊為準。我一般先按 Mode 0 配置如果讀回數(shù)據(jù)全 FF先把 CPOL/CPHA 切到 Mode 3 試一次比盲目調分頻更有效。2.2 16 位命令幀加 8 位 PEC寫一次命令其實要發(fā) 3 個字節(jié)LTC6804-1 的每條命令是 16 位命令字后面跟著 8 位 PEC 校驗合計 24 位。PEC 是器件自帶的 CRC-15 校驗結果取低 8 位跟在命令后面。命令碼不是隨意編的數(shù)據(jù)手冊命令表里能查到常見幾條如下命令命令字用途ADCV0x03啟動電壓 ADC 轉換RDCVA0x04讀電壓寄存器組 ARDCVB0x06讀電壓寄存器組 BRDCVC0x08讀電壓寄存器組 CRDSTATA0x0A讀狀態(tài)寄存器組 AWRCONF0x1A寫配置寄存器組RDCONF0x2A讀配置寄存器組注意順序命令低字節(jié)先發(fā)高字節(jié)后發(fā)最后發(fā) PEC。如果 PEC 不對器件直接丟棄整條命令不會返回錯誤標志肉眼看到的現(xiàn)象就是“發(fā)了命令沒反應”。這也是讀電壓全 0xFF 的第二個高發(fā)原因。2.2.1 計算 PEC 的 CRC-15 算法PEC 本質上是 CRC-15生成多項式 0x4599初始值為 0。逐字節(jié)輸入命令字最后取結果的高 8 位。參考實現(xiàn)如下#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; }這段代碼直接用位循環(huán)算 CRC-15輸入命令字節(jié)流輸出 8 位 PEC。參數(shù)data是待校驗的字節(jié)數(shù)組len是字節(jié)數(shù)調用時傳入命令字的低字節(jié)和高字節(jié)兩個元素即可。如果要壓榨性能可以改成查表法但在 1 MHz SPI 下位循環(huán)的開銷完全可以忽略。2.3 用 HAL 庫封裝一條“命令加讀回”的通用 SPI 函數(shù)工程里所有 LTC6804 操作都該收斂到一個 SPI 交換函數(shù)里不要在業(yè)務代碼里直接調 HAL_SPI_Transmit。通用函數(shù)長這樣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; // 命令低字節(jié)先發(fā) tx_buf[1] (cmd 8) 0xFF; // 命令高字節(jié) tx_buf[2] ltc6804_pec15(tx_buf, 2); // 前兩字節(jié)算 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 字節(jié)命令幀拉低 CS發(fā)命令然后在不拉高 CS 的前提下繼續(xù)讀回len字節(jié)數(shù)據(jù)。len為 0 時只發(fā)命令例如啟動轉換len為 8 時讀回一個寄存器組6 字節(jié)數(shù)據(jù)加 2 字節(jié) PEC。參數(shù)data指向接收緩沖區(qū)cmd傳上小節(jié)的命令字。片選拉低到發(fā)出第一個字節(jié)之間要留一點延時這個延時在 HAL 庫的 SPI 傳輸內部已經(jīng)有處理但如果換成寄存器版或純 GPIO 模擬 SPI必須在 CS 拉低后加 2~3 個空指令延時。寄存器版和標準外設庫版的移植也是改這個函數(shù)而已上層邏輯完全不用動。3. 單芯片讀電壓ADCV 之后真正花時間是數(shù)據(jù)重組3.1 發(fā)起轉換的 ADCV 命令和它攜帶的量程、模式位讀取電壓不是直接發(fā) RDCVA 就能拿到數(shù)據(jù)。芯片必須先執(zhí)行一次電壓 ADC 轉換轉換完成后再去讀寄存器組。ADCV 命令除了命令字 0x03還攜帶轉換模式、量程、自測模式等配置位。這些位在數(shù)據(jù)手冊 ADCV 命令一節(jié)有明確表格移植時建議先按默認量程和最慢濾波模式跑通再去優(yōu)化速度。轉換需要時間而且級聯(lián)時是多顆芯片同時轉換等待時間按鏈上芯片數(shù)線性增長。常見做法是發(fā)完 ADCV 后延時延時時長按“每片約 2 ms”估算鏈上有 10 片就至少等 20 ms 以上。延時不夠的典型癥狀是讀回電壓里有幾片是 0因為數(shù)據(jù)還沒準備好就被讀走了。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 裕量 }參數(shù)說明chain_len是級聯(lián)芯片數(shù)量單芯片時傳 1。延時時間取“轉換時間上限 裕量”不要取典型值溫度低時晶振和芯片內部 RC 都會漂裕量不足會偶發(fā)性讀到舊數(shù)據(jù)。3.2 電壓寄存器組 A/B/C 的字節(jié)序和拼接順序LTC6804-1 最多測量 12 節(jié)電芯電壓分成三組寄存器每組 4 個電芯。每組數(shù)據(jù)幀是 6 字節(jié)電壓數(shù)據(jù)加 2 字節(jié) PEC總共 8 字節(jié)。讀回后先把 PEC 校驗掉再把 6 字節(jié)按每 2 字節(jié)一個電芯拼成 16 位無符號數(shù)。寄存器組與電芯的對應關系如下寄存器組命令包含電芯數(shù)據(jù)區(qū)字節(jié)數(shù)電壓組 ARDCVACell 1~46電壓組 BRDCVBCell 5~86電壓組 CRDCVCCell 9~126拼接順序是高位在前每節(jié)電芯的電壓占 2 字節(jié)第一個字節(jié)是高 8 位第二個字節(jié)是低 8 位。讀回組 A 的 6 字節(jié)數(shù)據(jù)后Cell 1 電壓就是(data[0] 8) | data[1]Cell 2 是(data[2] 8) | data[3]依此類推。有些工程源碼會把高低字節(jié)順序搞反導致電壓讀數(shù)在 0~255 之間跳那就是拼接時把首字節(jié)當成低位了。3.3 實際工程里的電壓換算和濾波宏、浮點與均值拼出來的 16 位原始值要換算成毫伏或微伏換算系數(shù)來自數(shù)據(jù)手冊“電壓轉換結果”一節(jié)不同配置下系數(shù)不同。代碼里不要散落魔法數(shù)字應該用一個宏定義#define V_LSB_UV 100 // 1 LSB 對應的微伏數(shù)按手冊配置填寫 uint32_t ltc6804_raw_to_uv(uint16_t raw) { return (uint32_t)raw * V_LSB_UV; }參數(shù)說明raw是寄存器拼出來的 16 位原始值返回值是微伏。V_LSB_UV這個宏必須按數(shù)據(jù)手冊核對不同量程配置下 LSB 權重不一樣先用精密電壓源標定一次再固化成常量比直接照抄網(wǎng)上代碼可靠得多。電壓數(shù)據(jù)抖動是正常的尤其是多片級聯(lián)后電源噪聲變大。我一般不會對原始值直接做很強濾波而是先做一次剔除野值再做滑動平均#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 個元素的環(huán)形緩沖每次讀回都覆蓋最舊的值輸出均值。這種濾波對隨機噪聲有效但對真實電壓跌落會有約 8 個周期的延遲做保護邏輯時不要直接用濾波后的值判斷過壓原始值或淺濾波值要另留一路。4. 級聯(lián)讀回鏈上配置“分地址寫入”數(shù)據(jù)合并“反向取值”4.1 菊花鏈的兩種工作相位配置廣播與數(shù)據(jù)回傳級聯(lián)模式下主控 SPI 只接到最底層的那顆 LTC6804-1后續(xù)芯片通過片間接口串聯(lián)。SDO 數(shù)據(jù)是逐級傳回主控的最頂層的芯片數(shù)據(jù)最先被移出來經(jīng)過中間芯片一級一級往下傳最后到達主控的 MISO。也就是說主控讀回數(shù)據(jù)時收到的第一個字節(jié)屬于鏈上地址最大的芯片最后一組數(shù)據(jù)才屬于最底層芯片。這個順序和多數(shù)人的直覺相反是級聯(lián)合并最容易出錯的地方。級聯(lián)工作分兩個相位。配置相位里主機要保證每顆芯片寫進自己的地址和參數(shù)轉換相位則簡單得多ADCV 是廣播命令一顆芯片發(fā)起整條鏈都開始轉換等待時間隨鏈長線性增加。完成轉換后主機逐組讀回電壓寄存器把所有芯片的數(shù)據(jù)拼接成一個完整電池包電壓表。4.2 寫配置時地址挨個遞增讀回時把數(shù)組倒序歸位LTC6804-1 的器件地址寫在配置寄存器組 B 的高 4 位里鏈上從底層到頂層依次是 0x0、0x1、0x2……寫配置時地址不能一樣否則所有芯片都認領同一個地址讀回的數(shù)據(jù)會互相覆蓋。級聯(lián)寫配置的組幀方式和單芯片不同要按數(shù)據(jù)手冊“寫配置寄存器組”一節(jié)的菊花鏈說明來拼接這一點直接對照官方驅動確認比猜更穩(wěn)。讀回合并時應用層要主動反轉順序。我一般這樣處理#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 是最遠端芯片的數(shù)據(jù) 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)); } }參數(shù)說明group_buf是 SPI 讀回的原始數(shù)據(jù)區(qū)按寄存器組順序依次存放下標 0 對應最遠端芯片cell_buf是給應用層用的最終數(shù)組下標 0 對應底層芯片。循環(huán)里把讀回的第i組數(shù)據(jù)放到target_ic的位置完成倒序歸位。不做這一步電池編號和實際電芯位置會整個鏡像翻轉輕則顯示錯位重則均衡策略打錯對象。4.3 一個最小級聯(lián)讀電壓流程完整流程至少分三步啟動轉換、按組讀回、重排合并。偽代碼級別的實現(xiàn)如下#define IC_COUNT 6 #define GROUP_SZ 8 // 6 字節(jié)數(shù)據(jù) 2 字節(jié) 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 字節(jié) 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); }邏輯說明先發(fā) ADCV 讓整條鏈同時轉換再連續(xù)讀回 N 組電壓數(shù)據(jù)。讀回時 CS 保持低電平所有組的讀回在同一個事務里完成更穩(wěn)妥我的示例是按組單獨做實際工程里也可以一次性讀回N * 8字節(jié)效果一樣。PEC 校驗必須做它不僅是校驗傳輸錯誤還能間接反映鏈路里哪顆芯片的通信開始劣化。4.4 源碼包工程組織驅動層和應用層分開才找得到人一個能長期維護的 LTC6804-1 工程文件結構通常長這樣app/ bsp/ bsp_spi.c # STM32 SPI 和片選初始化 bsp_ltc6804.c # 協(xié)議層封裝 bsp_ltc6804.h protocol/ ltc6804_cmd.h # 命令字、寄存器地址定義 ltc6804_pec.c # CRC-15 校驗實現(xiàn) service/ bms_voltage.c # 濾波、換算、保護判斷 bms_voltage.h文件/目錄職責改動頻率bsp_spi.cSPI 引腳、時鐘、DMA 配置換平臺時改bsp_ltc6804.c命令拼裝、數(shù)據(jù)讀回、PEC 組幀協(xié)議調整時改ltc6804_pec.cCRC-15 算法幾乎不改bms_voltage.c換算、濾波、閾值判斷業(yè)務需求變化時改拿到一個現(xiàn)成的 zip 工程包我一般先打開 bsp_ltc6804.c 和 ltc6804_pec.c 兩個文件確認 PEC 算法是不是對 16 位命令生成 8 位校驗值再看 bsp_spi.c 里 SCLK 分頻是否低于 1 MHz。這兩個點沒問題剩下的就是業(yè)務層移植。Keil MDK 或 VSCode 加 Embedded IDE 都行關鍵是芯片包版本要和工程里 CubeMX 生成代碼的 HAL 庫版本一致否則編譯報的錯會讓人誤以為代碼有問題。5. 驗證讀數(shù)和觀察鏈路健康把“能跑”變成“可以上線”5.1 用自測模式代替精密電壓源做初步驗證沒有精密電壓源的時候可以先不接電池組直接用 LTC6804-1 的自測模式驗證鏈路。ADCV 命令里有自測位設置后芯片內部會切換到已知參考電壓去轉換不去測外部電芯。把自測讀回的原始值和手冊給出的自測典型值對比能同時驗證 SPI 通信、PEC 校驗和拼接順序三件事。如果自測值對得上再接電池組就可以把懷疑重點放到外部采樣電阻和濾波電路上。5.2 讓每顆芯片在狀態(tài)寄存器里留下溫度當作鏈路健康位LTC6804-1 內部有溫度傳感器狀態(tài)寄存器組 A 里能讀回芯片溫度。級聯(lián)工程里我習慣把每顆芯片的溫度一起讀回來這不僅是熱管理需要更是一個廉價的鏈路健康探針。溫度讀數(shù)正常說明那顆芯片的通信路徑是通的哪顆芯片溫度讀回 0 或亂碼故障點大概率就在它和上一級之間。這個思路比單純看電壓 CRC 錯誤計數(shù)更直觀因為溫度值本身有合理范圍一眼就能看出異常。5.3 用定時器調度轉換相位鏈越長越需要顯式狀態(tài)機轉換等待時間隨鏈長線性增長在 1 kHz 控制循環(huán)里直接 HAL_Delay 等轉換會卡死整個任務。我一般把讀取流程拆成一個狀態(tài)機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; } }參數(shù)說明phase 0負責啟動轉換phase 1負責等待和讀回。WAIT_TICKS按芯片數(shù)量放大鏈越長等待周期越長。這樣主循環(huán)不會被阻塞看門狗也不用加長超時。鏈路穩(wěn)定后再把 SPI 讀回放到 DMA 里讓狀態(tài)機變得更精簡——這個優(yōu)化的收益在鏈長超過 6 顆時非常明顯。本文還有配套的精品資源點擊獲取