系統(tǒng)設(shè)計(jì)與實(shí)現(xiàn))
簡介面向嵌入式開發(fā)學(xué)習(xí)者與交通監(jiān)測(cè)類課程設(shè)計(jì)需求這套基于STM32的公路四路超重檢測(cè)系統(tǒng)工程資料圍繞多路稱重采集、閾值判斷、聲光報(bào)警及聯(lián)網(wǎng)上傳等環(huán)節(jié)展開可用于畢業(yè)設(shè)計(jì)、課程實(shí)踐或原型驗(yàn)證。壓縮包共306個(gè)文件約10.31MB包含57個(gè)c源文件和63個(gè)h頭文件以及o、crf、d等編譯中間文件另有uvprojx工程、sct分散加載、map與lst映射清單等便于直接打開Keil工程復(fù)現(xiàn)和二次開發(fā)。已有333人學(xué)習(xí)下載。資料保留較完整的工程結(jié)構(gòu)與外設(shè)驅(qū)動(dòng)代碼涵蓋ADC、定時(shí)器、串口通信及物聯(lián)網(wǎng)協(xié)議相關(guān)實(shí)現(xiàn)讀者可據(jù)此梳理四路檢測(cè)的數(shù)據(jù)流、報(bào)警邏輯和聯(lián)網(wǎng)監(jiān)控思路也能參考工程配置與編譯產(chǎn)物進(jìn)行排錯(cuò)和功能擴(kuò)展。1. 去年幫人看一個(gè)公路治超點(diǎn)的老設(shè)備問題不是秤不準(zhǔn)去年幫人看一個(gè)公路治超點(diǎn)的老設(shè)備問題不是秤不準(zhǔn)而是四條車道的讀數(shù)互相干擾一號(hào)道剛過完一輛六軸車三號(hào)道的顯示還沒回零值班的人只能憑經(jīng)驗(yàn)猜哪條是真的。這套基于 STM32 的公路四路超重檢測(cè)系統(tǒng)要解決的就是這種“多路同時(shí)在線”的現(xiàn)場問題——四路稱重通道各自獨(dú)立采集、獨(dú)立標(biāo)定、獨(dú)立判定超限再由主控統(tǒng)一打包通過聯(lián)網(wǎng)模塊把車重、時(shí)間、車道號(hào)一起送到后臺(tái)。它適合正在做公路超重檢測(cè)方向畢設(shè)或課設(shè)、需要一套能從采集跑通到上報(bào)的完整方案的人也適合要把樣機(jī)裝到路邊實(shí)測(cè)的工程場景。下面按需求、選型、架構(gòu)、實(shí)現(xiàn)、避坑、驗(yàn)收的順序把這套系統(tǒng)拆開講。2. 需求拆解與器件選型四路通道、量程和采樣率怎么定2.1 先確認(rèn)“四路”指什么四條車道還是同一秤臺(tái)的四個(gè)角這是動(dòng)手前必須問清楚的一件事。兩種理解對(duì)應(yīng)的軟件結(jié)構(gòu)完全不同四條車道是四個(gè)互不相干的獨(dú)立通道各自有零點(diǎn)、靈敏度系數(shù)和超重閾值同一秤臺(tái)四個(gè)角則是四個(gè)分量需要先求和得到總重再用四角差值判斷車輛是否偏載。我在方案里把兩種模式都留了口子用一個(gè)配置字節(jié)ch_mode切換0 表示獨(dú)立通道1 表示四點(diǎn)合成這樣后面換場景不用改代碼結(jié)構(gòu)只改配置。除了通道定義還要把性能邊界寫死否則答辯現(xiàn)場一問就露怯。公路治超點(diǎn)常見兩種工況一種是引導(dǎo)進(jìn)站的低速通過車速 5 到 15 公里每小時(shí)另一種是完全靜態(tài)停車稱重。低速工況下一輛車壓在 1.5 米長的秤板上有效信號(hào)窗口大概 0.3 到 0.4 秒如果采樣率只有 10Hz窗口里只能拿到三四個(gè)點(diǎn)峰值根本抓不準(zhǔn)。所以這套方案定位在低速與靜態(tài)稱重動(dòng)態(tài)精度按滿量程 ±3% 到 ±5% 驗(yàn)收靜態(tài)精度可以做到 ±0.5%。想做成高速不停車稱重必須上專用的動(dòng)態(tài)稱重傳感器和更高的采樣率那是另一個(gè)量級(jí)的預(yù)算不在本方案范圍內(nèi)。功能清單我一般按下面幾條鎖死后面所有模塊都圍繞它展開四路獨(dú)立采集單路斷線不影響其余三路車輛到位觸發(fā)、離開結(jié)束一次通過生成一條完整記錄按軸重和總重雙閾值判定超限本地聲光報(bào)警并輸出一路繼電器信號(hào)本地至少存一萬條記錄聯(lián)網(wǎng)上報(bào)斷網(wǎng)期間不丟數(shù)據(jù)現(xiàn)場可標(biāo)定標(biāo)定參數(shù)掉電不丟還要能防止誤改。工作溫度按零下 20 到 60 攝氏度設(shè)計(jì)傳感器接線做防雷和屏蔽。2.2 主控與 ADC 選型為什么是四片 HX711而不是片內(nèi) 12 位 ADC先說結(jié)論主控用 STM32F103RCT6稱重前端用四片 HX711每片對(duì)應(yīng)一路。這個(gè)組合不是最先進(jìn)的但在四路低速稱重場景下是最省心、最容易復(fù)現(xiàn)的。對(duì)比項(xiàng)STM32F103RCT6STM32F407VET6主頻72MHz168MHzFlash / RAM256KB / 48KB512KB / 192KB串口數(shù)量56片上 ADC12 位12 位適合場景裸機(jī)時(shí)間片調(diào)度四路 IO 模擬時(shí)序需要網(wǎng)絡(luò)協(xié)議棧、多任務(wù)或更強(qiáng)濾波算力F103 的 48KB 內(nèi)存跑一個(gè)裸機(jī)時(shí)間片調(diào)度器加一個(gè)環(huán)形緩沖隊(duì)列綽綽有余省下來的成本可以貼到傳感器和防護(hù)上。真正要謹(jǐn)慎的是 ADC 這一環(huán)很多同學(xué)圖省事直接用片內(nèi) 12 位 ADC 接應(yīng)變橋結(jié)果量程 30 噸時(shí)最小分辨力大概 7 公斤空載時(shí)噪聲就有十幾公斤零點(diǎn)一直跳。這不是代碼問題是分辨率不夠。方案分辨率有效通道輸出速率MCU 開銷說明HX711 × 424 位410Hz / 80Hz低IO 模擬時(shí)序通道間物理隔離無串?dāng)_成本低ADS125624 位8 路差分 4 路最高 30kSPS中SPI 加 DRDY 中斷速率高但要外加基準(zhǔn)和模擬前端片內(nèi) 12 位 ADC12 位多路快低分辨力不足只適合做粗略檢測(cè)HX711 的短板是速率80Hz 輸出時(shí)內(nèi)部增益和濾波切換會(huì)帶來更大噪聲實(shí)測(cè)零點(diǎn)抖動(dòng)比 10Hz 時(shí)明顯。所以我的做法是靜態(tài)稱重走 10Hz低速動(dòng)態(tài)走 80Hz由配置項(xiàng)決定標(biāo)定時(shí)按實(shí)際工況選不要一套參數(shù)打天下。2.3 觸發(fā)、執(zhí)行與聯(lián)網(wǎng)部件把外圍清單一次列全觸發(fā)用一對(duì)地感線圈加檢測(cè)器檢測(cè)器輸出干接點(diǎn)經(jīng)光耦隔離后進(jìn) GPIO這樣線圈那邊的干擾不會(huì)直接打到主控。線圈負(fù)責(zé)“車來了”紅外對(duì)射負(fù)責(zé)“車走了”兩個(gè)信號(hào)配合比單靠重量閾值判斷車輛離開靠譜得多——重車停在秤上不動(dòng)時(shí)光看重量是判斷不出它走沒走的。執(zhí)行部分包括一路繼電器輸出可接道閘或外部報(bào)警器、一個(gè)有源蜂鳴器和四個(gè)車道狀態(tài)指示燈。本地顯示用 0.96 寸 OLED只顯示四路當(dāng)前重量和狀態(tài)不做過多的界面現(xiàn)場人員看得懂就行。聯(lián)網(wǎng)這塊有兩種常見做法WiFi 透傳模塊走 AT 指令適合有現(xiàn)成熱點(diǎn)的實(shí)驗(yàn)室和校園場景4G Cat.1 模塊走串口 AT 或 PPP適合真正裝在路邊的場景。兩者對(duì)主控來說都是串口所以底層驅(qū)動(dòng)寫成同一套串口收發(fā)加狀態(tài)機(jī)換模塊只改初始化指令表不動(dòng)上層協(xié)議。存儲(chǔ)用 W25Q64 存歷史記錄AT24C02 存標(biāo)定參數(shù)做雙備份多花兩塊錢能省掉很多“參數(shù)丟了要重新標(biāo)定”的麻煩。3. 總體架構(gòu)與通信協(xié)議設(shè)計(jì)四層鏈路和數(shù)據(jù)幀怎么落地3.1 四層架構(gòu)與各模塊的職責(zé)邊界整套系統(tǒng)按感知層、控制層、傳輸層、應(yīng)用層劃分這個(gè)劃分不是為了好看是為了讓每個(gè)人接手時(shí)知道改哪一層不會(huì)牽連別處。層級(jí)主要模塊職責(zé)對(duì)外接口感知層稱重傳感器、地感線圈、紅外對(duì)射把重量和車輛位置變成電信號(hào)模擬毫伏信號(hào)、開關(guān)量控制層STM32 主控、HX711、OLED、報(bào)警輸出采集、濾波、標(biāo)定、判定、本地顯示串口、GPIO、SPI、I2C傳輸層WiFi / 4G 模塊把記錄打包上傳接收下行參數(shù)UART AT 指令應(yīng)用層后臺(tái)服務(wù)與數(shù)據(jù)庫入庫、查詢、統(tǒng)計(jì)、告警推送MQTT / HTTP控制層內(nèi)部再分成驅(qū)動(dòng)、業(yè)務(wù)、應(yīng)用三段。驅(qū)動(dòng)層只管時(shí)序比如 HX711 讀數(shù)、Flash 讀寫、串口收發(fā)業(yè)務(wù)層管濾波、標(biāo)定換算、超重判定狀態(tài)機(jī)應(yīng)用層管調(diào)度用 SysTick 做 1ms 基準(zhǔn)主循環(huán)里按時(shí)間片輪詢各個(gè)任務(wù)。這里沒有上 RTOS原因是任務(wù)數(shù)量少、實(shí)時(shí)性要求不高裸機(jī)時(shí)間片更容易調(diào)試出問題的時(shí)候一眼能看出是哪個(gè)任務(wù)超時(shí)。調(diào)度周期我這樣分配HX711 采集任務(wù)每 10ms 跑一次四路輪詢?yōu)V波與判定任務(wù)每 50ms 跑一次OLED 刷新 200ms聯(lián)網(wǎng)心跳 30 秒記錄落盤在判定完成后立即觸發(fā)。這套節(jié)奏在 F103 上跑下來 CPU 占用不到三成留足了余量。3.2 數(shù)據(jù)幀格式與 CRC16 校驗(yàn)上下行都用同一套殼本地模塊之間、設(shè)備與后臺(tái)之間我習(xí)慣用同一套幀結(jié)構(gòu)調(diào)試時(shí)用串口助手就能看不用兩套解析邏輯。偏移長度字段說明02幀頭固定 0xAA 0x5521協(xié)議版本當(dāng)前 0x01方便以后擴(kuò)展31命令字0x01 實(shí)時(shí)數(shù)據(jù)0x02 記錄補(bǔ)傳0x10 參數(shù)下發(fā)0x11 參數(shù)回讀41通道掩碼bit0 到 bit3 對(duì)應(yīng)第 1 到第 4 路52負(fù)載長度小端最大 512 字節(jié)7N負(fù)載TLV 結(jié)構(gòu)重量用 int32 定點(diǎn)單位 0.01kg7N2CRC16MODBUS 多項(xiàng)式 0xA001低字節(jié)在前重量為什么要用定點(diǎn)而不用 float因?yàn)楦↑c(diǎn)在串口傳輸和存儲(chǔ)時(shí)字節(jié)序容易出岔子int32 定點(diǎn)表示 0.01kg30 噸就是 3000000離溢出還遠(yuǎn)得很解析也簡單。CRC 校驗(yàn)代碼如下初值 0xFFFF多項(xiàng)式 0xA001算完低字節(jié)先發(fā)。/* MODBUS CRC16初值 0xFFFF多項(xiàng)式 0xA0010x8005 反轉(zhuǎn) */ static uint16_t crc16_modbus(const uint8_t *p, uint16_t len) { uint16_t crc 0xFFFF; while (len--) { crc ^ *p; for (uint8_t i 0; i 8; i) { if (crc 0x0001) { crc (crc 1) ^ 0xA001; } else { crc 1; } } } return crc; /* 發(fā)送時(shí)先發(fā)低字節(jié)再發(fā)高字節(jié) */ }這個(gè)函數(shù)在接收端要再算一遍和幀尾的兩個(gè)字節(jié)比對(duì)不一致就整幀丟棄并累加錯(cuò)誤計(jì)數(shù)。錯(cuò)誤計(jì)數(shù)超過閾值時(shí)我一般會(huì)觸發(fā)一次模塊重連因?yàn)檫B續(xù) CRC 錯(cuò)通常意味著波特率漂了或者線路接觸不良而不是偶發(fā)干擾。3.3 本地存儲(chǔ)分區(qū)與掉電保護(hù)參數(shù)雙備份記錄走環(huán)形區(qū)W25Q64 有 8MB按下面這樣分區(qū)邊界留出余量以后加字段不用重排。起始地址大小用途校驗(yàn)方式0x0000004KB參數(shù)備份 A魔數(shù) 0x5A5A CRC160x0010004KB參數(shù)備份 B同上0x002000512KB稱重記錄環(huán)形區(qū)每條 32 字節(jié)共 16384 條每條末尾 CRC80x0820004KB運(yùn)行日志與異常計(jì)數(shù)無參數(shù)區(qū)做雙備份的意義在于寫參數(shù)時(shí)先擦 B 區(qū)寫入新值并校驗(yàn)成功后再寫 A 區(qū)。如果寫到一半掉電下次上電讀到 A 區(qū)校驗(yàn)失敗就回退到 B 區(qū)兩個(gè)都?jí)牟庞媚J(rèn)值。這個(gè)邏輯看著簡單但真到了現(xiàn)場一次意外斷電就能把標(biāo)定系數(shù)寫成 0xFF重新標(biāo)定要封路半小時(shí)代價(jià)比多存 4KB 高得多。記錄區(qū)用環(huán)形寫入寫指針存在參數(shù)區(qū)里每寫一條就更新一次指針。環(huán)形區(qū)滿了就覆蓋最老的記錄同時(shí)置一個(gè)溢出標(biāo)志。記錄結(jié)構(gòu)里帶序號(hào)和時(shí)間戳補(bǔ)傳時(shí)后臺(tái)按序號(hào)去重避免網(wǎng)絡(luò)重發(fā)導(dǎo)致重復(fù)入庫。4. 下位機(jī)實(shí)現(xiàn)四路采集、動(dòng)態(tài)濾波與超重判定的代碼骨架4.1 四路 HX711 的時(shí)序驅(qū)動(dòng)與輪詢調(diào)度HX711 的時(shí)序不復(fù)雜但有幾個(gè)參數(shù)不能錯(cuò)DOUT 變低表示數(shù)據(jù)就緒SCK 需要給 25 個(gè)脈沖前 24 個(gè)移出數(shù)據(jù)補(bǔ)碼形式第 25 個(gè)脈沖決定下一次的通道和增益。四路就是四組 DOUT 和 SCK共占用 8 個(gè) IO。#define HX711_CH_NUM 4 #define HX711_TIMEOUT 30 /* ms超過判定該路斷線 */ typedef struct { GPIO_TypeDef *port_dout; /* 數(shù)據(jù)腳輸入 */ uint16_t pin_dout; GPIO_TypeDef *port_sck; /* 時(shí)鐘腳輸出 */ uint16_t pin_sck; int32_t offset; /* 空載零點(diǎn)原始值 */ float scale; /* 每公斤對(duì)應(yīng)的原始計(jì)數(shù) */ int32_t raw; /* 最近一次原始讀數(shù) */ float weight_kg; /* 換算后的重量 */ uint8_t online; /* 1 在線0 斷線 */ } Hx711Ch_t; /* 讀取一路原始值返回 INT32_MIN 表示超時(shí)斷線 */ static int32_t hx711_read_raw(Hx711Ch_t *ch) { uint32_t t0 HAL_GetTick(); /* 等待 DOUT 拉低說明轉(zhuǎn)換完成 */ while (HAL_GPIO_ReadPin(ch-port_dout, ch-pin_dout) GPIO_PIN_SET) { if (HAL_GetTick() - t0 HX711_TIMEOUT) { ch-online 0; return INT32_MIN; } } int32_t v 0; for (uint8_t i 0; i 24; i) { HAL_GPIO_WritePin(ch-port_sck, ch-pin_sck, GPIO_PIN_SET); delay_us(1); /* SCK 高電平需保持 0.2~0.5us */ v (v 1) | (HAL_GPIO_ReadPin(ch-port_dout, ch-pin_dout) GPIO_PIN_SET); HAL_GPIO_WritePin(ch-port_sck, ch-pin_sck, GPIO_PIN_RESET); delay_us(1); } /* 第 25 個(gè)脈沖選擇下一次為 A 通道增益 128 */ HAL_GPIO_WritePin(ch-port_sck, ch-pin_sck, GPIO_PIN_SET); delay_us(1); HAL_GPIO_WritePin(ch-port_sck, ch-pin_sck, GPIO_PIN_RESET); if (v 0x00800000) { /* 24 位負(fù)數(shù)補(bǔ)碼符號(hào)擴(kuò)展 */ v | 0xFF000000; } ch-online 1; return v; }這段代碼有兩個(gè)地方值得多說一句。一是delay_us(1)不能省成幾個(gè)__NOP()72MHz 下一個(gè) NOP 才十幾納秒湊不夠 HX711 要求的 0.2 微秒高電平讀出來的數(shù)據(jù)會(huì)整體偏移甚至全 0這個(gè)坑我在第一版上踩過示波器一量才發(fā)現(xiàn) SCK 是尖脈沖。二是超時(shí)返回必須區(qū)分于正常讀數(shù)我用 INT32_MIN 做哨兵值上層看到它就跳過這一路絕不能讓斷線的通道參與平均值計(jì)算否則零點(diǎn)會(huì)被拉偏。輪詢調(diào)度放在 10ms 的時(shí)間片里一次只讀一路四路輪完是 40ms。HX711 在 80Hz 模式下單次轉(zhuǎn)換 12.5ms40ms 輪一圈剛好跟得上不會(huì)丟數(shù)據(jù)。如果開 10Hz 模式單次 100ms輪詢周期要相應(yīng)拉長到 400ms 以上這時(shí)候HX711_TIMEOUT也得跟著放大否則會(huì)誤判斷線。4.2 動(dòng)態(tài)稱重的濾波與峰值提取原始讀數(shù)不能直接用空載時(shí)會(huì)有幾十個(gè) counts 的抖動(dòng)重車壓上去又疊加了車輛振動(dòng)。我的處理鏈?zhǔn)侨蜗茸?5 點(diǎn)中值濾波去掉脈沖干擾再做一階低通平滑最后在觸發(fā)窗口內(nèi)取峰值。#define MED_N 5 #define LPF_ALPHA 0.25f /* 越小越平滑但響應(yīng)變慢 */ typedef struct { int32_t med_buf[MED_N]; uint8_t med_idx; float lpf; /* 低通輸出 */ int32_t peak; /* 本次窗口峰值 */ uint16_t peak_hold; /* 峰值附近保持計(jì)數(shù) */ } Filter_t; /* 中值濾波5 點(diǎn)排序取中間值抗脈沖干擾 */ static int32_t median5(int32_t *buf) { int32_t t[MED_N]; for (uint8_t i 0; i MED_N; i) t[i] buf[i]; for (uint8_t i 0; i MED_N - 1; i) { for (uint8_t j i 1; j MED_N; j) { if (t[j] t[i]) { int32_t tmp t[i]; t[i] t[j]; t[j] tmp; } } } return t[MED_N / 2]; } /* 每 50ms 調(diào)用一次輸入換算后的公斤值 */ static void filter_update(Filter_t *f, float kg, uint8_t vehicle_in) { f-med_buf[f-med_idx] (int32_t)(kg * 100.0f); /* 轉(zhuǎn)成 0.01kg 整數(shù) */ f-med_idx (f-med_idx 1) % MED_N; float med median5(f-med_buf) / 100.0f; f-lpf LPF_ALPHA * (med - f-lpf); if (vehicle_in) { if ((int32_t)(f-lpf * 100) f-peak) { f-peak (int32_t)(f-lpf * 100); f-peak_hold 0; } else if (f-peak_hold 20) { f-peak_hold; /* 峰值后 1 秒內(nèi)不更新防止抖動(dòng)誤判 */ } } }中值濾波的窗口取 5 是折中取 3 點(diǎn)去不掉連續(xù)的干擾取 7 點(diǎn)會(huì)把真實(shí)的上升沿削平。低通系數(shù) 0.25 對(duì)應(yīng)的時(shí)間常數(shù)大約 3 個(gè)采樣周期在 50ms 周期下是 150ms跟得上車輛壓上來的速度又不至于把振動(dòng)放大。峰值保持段加了 1 秒的鎖定時(shí)間原因是車輛在秤上輕微晃動(dòng)時(shí)重量會(huì)在峰值附近反復(fù)波動(dòng)不加鎖定的話峰值會(huì)一直往上爬判出來的重量比實(shí)際偏高。峰值取的是“軸重”還是“總重”取決于秤板長度和車輛軸距。秤板短于軸距時(shí)一次只壓一根軸峰值就是單軸重多軸車要按軸依次累加秤板長于整車時(shí)峰值直接就是總重。這兩種情況的判定邏輯不一樣我在配置里加了weigh_mode答辯時(shí)如果老師問“六軸車怎么算總重”這就是回答的落點(diǎn)。4.3 超重判定狀態(tài)機(jī)與報(bào)警輸出判定不能只看一個(gè)瞬時(shí)值要用狀態(tài)機(jī)把“車來了、正在稱、稱完了、判超限、上報(bào)”這幾步串起來否則一輛車會(huì)被重復(fù)判定好幾次后臺(tái)收到的記錄數(shù)對(duì)不上實(shí)際車流量。typedef enum { ST_IDLE 0, /* 等待車輛觸發(fā) */ ST_SAMPLING, /* 車輛在秤采集峰值 */ ST_SETTLE, /* 車輛離開等待重量回落 */ ST_JUDGE, /* 判定超限 */ ST_REPORT /* 生成記錄并入隊(duì) */ } WeighState_e; typedef struct { uint32_t limit_kg; /* 超重閾值單位 kg */ uint16_t deadband_pct; /* 死區(qū)百分比默認(rèn) 5防止臨界抖動(dòng) */ uint8_t confirm_cnt; /* 連續(xù)超限次數(shù)確認(rèn)默認(rèn) 3 */ } Threshold_t; static void judge_task(void) { static WeighState_e st ST_IDLE; static uint8_t over_cnt 0; Threshold_t *th g_threshold; switch (st) { case ST_IDLE: if (g_vehicle_in) { g_filter.peak 0; st ST_SAMPLING; } break; case ST_SAMPLING: if (!g_vehicle_in) { st ST_SETTLE; g_settle_ms 0; } break; case ST_SETTLE: g_settle_ms 50; if (g_settle_ms 500) { st ST_JUDGE; } break; case ST_JUDGE: { uint32_t peak_kg g_filter.peak / 100; /* 只有超過閾值加上死區(qū)才累加超限計(jì)數(shù) */ if (peak_kg th-limit_kg * (100 th-deadband_pct) / 100) { if (over_cnt th-confirm_cnt) { g_alarm_on 1; /* 聲光報(bào)警 繼電器輸出 */ g_record.over_flag 1; } } else { over_cnt 0; g_record.over_flag 0; } st ST_REPORT; break; } case ST_REPORT: record_push(g_record); /* 寫入環(huán)形隊(duì)列 */ over_cnt 0; st ST_IDLE; break; default: st ST_IDLE; break; } }死區(qū)和連續(xù)確認(rèn)這兩層是為了對(duì)付臨界車。一輛限重 30 噸的車實(shí)際稱出來 30.2 噸如果直接判超限司機(jī)和值班員都會(huì)來找你。加 5% 死區(qū)和連續(xù) 3 次確認(rèn)之后判出來的超限基本都在 31.5 噸以上爭議少很多。報(bào)警輸出用g_alarm_on控制持續(xù) 5 秒后自動(dòng)關(guān)閉防止蜂鳴器一直響。4.4 上報(bào)隊(duì)列與斷網(wǎng)補(bǔ)傳上報(bào)隊(duì)列放在 RAM 里做一個(gè) 32 條的環(huán)形緩沖每條記錄生成后先入隊(duì)聯(lián)網(wǎng)任務(wù)從隊(duì)頭取。發(fā)送成功才出隊(duì)失敗就留在隊(duì)里等下一輪。隊(duì)列滿的時(shí)候最老的一條轉(zhuǎn)存到 Flash 記錄區(qū)保證不丟。#define QUEUE_SIZE 32 typedef struct { uint32_t seq; /* 記錄序號(hào)后臺(tái)按此去重 */ uint32_t ts; /* Unix 時(shí)間戳 */ uint8_t ch; /* 通道號(hào) 1~4 */ int32_t weight01kg; /* 重量單位 0.01kg */ uint8_t over_flag; /* 1 超限 */ } Record_t; static Record_t q_buf[QUEUE_SIZE]; static uint8_t q_head, q_tail, q_count; static uint8_t record_push(const Record_t *r) { if (q_count QUEUE_SIZE) { record_save_flash(q_buf[q_tail]); /* 隊(duì)列滿落盤保底 */ q_tail (q_tail 1) % QUEUE_SIZE; q_count--; } q_buf[q_head] *r; q_head (q_head 1) % QUEUE_SIZE; q_count; return 1; }補(bǔ)傳邏輯放在聯(lián)網(wǎng)任務(wù)里連上后臺(tái)之后先讀 Flash 里未確認(rèn)的記錄按序號(hào)從小到大發(fā)每發(fā)一條等一次 ACKACK 里的序號(hào)對(duì)得上才把 Flash 里該條標(biāo)記為已上傳。這里有個(gè)細(xì)節(jié)標(biāo)記已上傳不要擦整塊用一個(gè) 32 位的位圖記錄哪些條已確認(rèn)位圖滿了再統(tǒng)一整理這樣能少擦很多次 Flash。5. 常見問題與避坑五個(gè)最容易在現(xiàn)場翻車的點(diǎn)5.1 零點(diǎn)漂移上電讀數(shù)正常跑兩小時(shí)重量越飄越高現(xiàn)象是空載時(shí)顯示從 0 慢慢漲到幾十公斤重啟后又歸零。原因通常有兩個(gè)一是 HX711 的供電和主控共用一路 3.3V數(shù)字部分電流波動(dòng)通過地線串到模擬端二是軟件上只在開機(jī)時(shí)取了零點(diǎn)之后一直用固定值減。解決的辦法是硬件上給 HX711 的 AVDD 單獨(dú)走 LC 濾波地線單點(diǎn)匯到電源入口軟件上加自動(dòng)零點(diǎn)跟蹤車輛離開后如果連續(xù) 30 秒重量都在滿量程 2% 以內(nèi)就緩慢修正零點(diǎn)每次只修一個(gè)很小的步長避免把真實(shí)的小重量也修掉。跟蹤步長我一般設(shè)成每 10 秒修正 0.5kg慢慢糾不激進(jìn)。5.2 四路串?dāng)_壓一號(hào)道三號(hào)道跟著跳數(shù)現(xiàn)象很嚇人以為是軟件讀了別的通道。實(shí)際排查下來九成是四路傳感器的屏蔽線擰在一起走線或者四片 HX711 共用一根 SCK 時(shí)鐘。SCK 是方波頻率雖然不高但走線長、回路大很容易耦合到相鄰?fù)ǖ赖哪M輸入上。解決分三步每路傳感器用獨(dú)立屏蔽雙絞線屏蔽層單端接地四片 HX711 的 SCK 各走各的絕不在板上并聯(lián)成一根總線在 HX711 的輸入端并 0.1uF 電容做差模濾波。改完之后再壓單路其余三路的跳數(shù)應(yīng)該在滿量程 0.1% 以內(nèi)。5.3 聯(lián)網(wǎng)模塊假在線心跳正常數(shù)據(jù)死活發(fā)不出去現(xiàn)象是串口每 30 秒都能收到模塊返回的“已連接”但上報(bào)的數(shù)據(jù)后臺(tái)一條也收不到。原因是 AT 指令的返回只代表模塊和基站或路由建立了鏈路不代表上層協(xié)議通。這種情況我一般是三層排查先看模塊返回里有沒有分配到 IP再看能不能 ping 通后臺(tái)地址最后看協(xié)議層有沒有收到后臺(tái)的心跳應(yīng)答。軟件上不能只信模塊的“連接成功”提示必須自己發(fā)一條心跳幀并等待后臺(tái)回包連續(xù)三次沒回包就判定鏈路失效主動(dòng)重連。這個(gè)超時(shí)重連邏輯寫進(jìn)代碼里比在現(xiàn)場盯著串口助手看要省事得多。5.4 看門狗復(fù)位正好把剛采到的數(shù)據(jù)寫沒了現(xiàn)象是偶爾丟一條記錄日志里能看到復(fù)位標(biāo)志。原因是寫 Flash 需要幾十毫秒這段時(shí)間喂狗沒跟上。解決不是簡單地把看門狗時(shí)間拉長而是把寫 Flash 的動(dòng)作拆開先寫 RAM 隊(duì)列由低優(yōu)先級(jí)的任務(wù)在空閑時(shí)批量落盤落盤前先喂一次狗參數(shù)區(qū)寫入時(shí)用雙緩沖先寫 B 區(qū)校驗(yàn)通過再寫 A 區(qū)任何一步被復(fù)位打斷下次上電都有可用的一份。另外中斷里不要做 Flash 操作F103 在擦寫 Flash 時(shí)取指會(huì)暫停中斷響應(yīng)會(huì)被拖住反過來又影響喂狗這是個(gè)連環(huán)坑。5.5 標(biāo)定參數(shù)被誤改現(xiàn)場手一抖整段路的數(shù)據(jù)全廢現(xiàn)象是換了個(gè)操作員之后所有重量都偏大一倍或偏小一半。原因是標(biāo)定入口藏在按鍵長按里誤觸就進(jìn)了標(biāo)定模式隨便按兩下把靈敏度系數(shù)改了。解決是三道鎖進(jìn)入標(biāo)定必須同時(shí)按住兩個(gè)指定按鍵并保持 5 秒進(jìn)入后要求輸入一個(gè)四位數(shù)口令口令存在參數(shù)區(qū)里默認(rèn)值在首次上電時(shí)強(qiáng)制修改標(biāo)定完成寫入前回讀一次并算出校驗(yàn)重量和輸入的標(biāo)準(zhǔn)重量偏差超過 2% 就拒絕保存。參數(shù)區(qū)同時(shí)保留上一版參數(shù)我在參數(shù)結(jié)構(gòu)里留了個(gè)rollback標(biāo)志現(xiàn)場出問題時(shí)能一鍵回到上一版。6. 驗(yàn)收與答辯怎么證明這套系統(tǒng)真的能用驗(yàn)收要給出可重復(fù)的測(cè)試方法而不是“我試了一下沒問題”。我一般按下面這張表走一遍每一項(xiàng)都要留下數(shù)據(jù)。測(cè)試項(xiàng)測(cè)試方法判定標(biāo)準(zhǔn)靜態(tài)精度每路單獨(dú)加標(biāo)準(zhǔn)砝碼從 20% 到滿量程取 5 個(gè)點(diǎn)誤差不超過滿量程 ±0.5%四路一致性同一載荷依次壓四路比較讀數(shù)四路之間偏差不超過 ±1%超重判定用可調(diào)載荷逐步逼近閾值死區(qū)內(nèi)不報(bào)警超過死區(qū)連續(xù) 3 次后報(bào)警斷網(wǎng)補(bǔ)傳拔掉聯(lián)網(wǎng)模塊制造 10 條記錄后恢復(fù)恢復(fù)后 60 秒內(nèi)全部補(bǔ)傳序號(hào)無重復(fù)掉電保護(hù)記錄寫入過程中斷電重復(fù) 10 次參數(shù)不丟記錄最多丟最后 1 條長時(shí)間運(yùn)行連續(xù)通電 72 小時(shí)零點(diǎn)漂移不超過滿量程 0.3%答辯現(xiàn)場最容易出問題的環(huán)節(jié)是演示稱重因?yàn)椴豢赡馨秧来a搬到教室里。我的做法是提前準(zhǔn)備一個(gè)數(shù)據(jù)回放模式把現(xiàn)場采到的一路原始數(shù)據(jù)存成數(shù)組通過串口注入系統(tǒng)照常走濾波、判定、上報(bào)的完整流程屏幕上能看到重量曲線跳動(dòng)、超限報(bào)警亮起、后臺(tái)收到記錄。這樣演示不依賴實(shí)物也不怕現(xiàn)場環(huán)境干擾。要強(qiáng)調(diào)的是回放模式只走業(yè)務(wù)層不替代真實(shí)采集的驗(yàn)證驗(yàn)收表里那幾項(xiàng)還是得用砝碼實(shí)測(cè)。還有一個(gè)小習(xí)慣把所有可以調(diào)的量集中到一個(gè)結(jié)構(gòu)體里閾值、死區(qū)、確認(rèn)次數(shù)、上報(bào)周期、標(biāo)定系數(shù)全放進(jìn)去做成一個(gè)參數(shù)頁答辯時(shí)老師問“這個(gè)閾值能不能改”當(dāng)場改給他看比嘴上說“可以改”有說服力得多。從那以后我每次打樣之前都會(huì)先把標(biāo)定流程和斷網(wǎng)補(bǔ)傳這兩條路在臺(tái)面上跑通再焊板子、再裝殼。血淚經(jīng)驗(yàn)是硬件問題能靠換件解決流程沒走通到了現(xiàn)場就是干瞪眼。希望這套結(jié)構(gòu)能幫到你少走幾段彎路。本文還有配套的精品資源點(diǎn)擊獲取