動SCD4X讀取CO2和溫濕度:I2C通信與CRC校驗詳解)
簡介面向嵌入式開發(fā)者的STM32F103C8T6與Sensirion SCD4X傳感器I2C通信完整工程包適用于智能家居、空氣監(jiān)測等需要CO?/溫濕度采集的場景。資源包共143個文件約3.86MB以C源文件、頭文件、HAL庫文件及編譯生成文件為主另含項目配置文檔和PDF數(shù)據(jù)手冊可直接在Keil工程中編譯調(diào)試。已有698人學習使用。內(nèi)容覆蓋GPIOPB6/PB7I2C模式初始化、HAL_I2C主從通信、傳感器命令時序及原始數(shù)據(jù)解析并附串口輸出調(diào)試方法與常見坑點說明配合隨包的Sensirion數(shù)據(jù)手冊和示例工程開發(fā)者可對照完成驅(qū)動移植與參數(shù)校準有效縮短開發(fā)周期。 最近幫朋友調(diào)了一個小項目用一塊STM32F103C8T6最小系統(tǒng)板把Sensirion盛思睿的SCD4X二氧化碳傳感器數(shù)據(jù)讀出來實時顯示溫度和濕度。整個過程踩了不少坑從I2C通信不穩(wěn)定、CRC校驗失敗到數(shù)據(jù)讀出來明顯不合理最后總算穩(wěn)定跑起來了。這里把完整過程整理一下包含選型考慮、硬件接線、協(xié)議細節(jié)、HAL庫代碼實現(xiàn)和常見問題排查給后面要做同類項目的人一個能直接抄作業(yè)的參考。SCD4X這個傳感器最吸引人的地方是把CO2、濕度、溫度三個測量集成了在一個超小體積的光學傳感器里精度也不錯功耗還低。配合STM32F103C8T6這種幾塊錢就能買到的最小系統(tǒng)板非常適合做室內(nèi)空氣質(zhì)量監(jiān)測、新風聯(lián)動、溫室大棚環(huán)境采集這類項目。無論你是剛接觸單片機的學生還是已經(jīng)在做產(chǎn)品選型的工程師這篇內(nèi)容都值得看一下。1. 項目整體設(shè)計與選型思路1.1 為什么用STM32F103C8T6做主控STM32F103C8T6可以說是國內(nèi)玩嵌入式最常用的芯片之一了。Cortex-M3內(nèi)核72MHz主頻64KB Flash、20KB RAM雖然參數(shù)放在今天不算亮眼但做傳感器數(shù)據(jù)采集和簡單控制完全夠用。關(guān)鍵是它有兩個硬件I2C外設(shè)支持100kHz和400kHz兩種速率接SCD4X這種I2C接口的傳感器非常方便。更實際的原因是生態(tài)成熟。CubeMX配置外設(shè)、HAL庫調(diào)用接口網(wǎng)上的資料一抓一大把遇到問題很容易找到解決方案。如果是做畢設(shè)或者快速驗證方案用這塊板子能把開發(fā)周期壓縮到最短。而且F103C8T6的國產(chǎn)替代型號很多將來要批量做產(chǎn)品成本也能壓得很低。1.2 SCD4X系列怎么選SCD40、SCD41、SCD42對比Sensirion的SCD4X系列分為三個型號很多人一開始搞不清楚區(qū)別直接買錯了。這里先整理一個對比表型號CO2精度壽命典型用途價格區(qū)間SCD40±(50ppm 5%讀數(shù))40000小時室內(nèi)空氣質(zhì)量監(jiān)測較低SCD41±(40ppm 5%讀數(shù))40000小時高精度環(huán)境監(jiān)測較高SCD42±(50ppm 5%讀數(shù))20000小時低成本消費類產(chǎn)品最低我這次用的是SCD41精度最高適合做數(shù)據(jù)記錄分析。如果你只是做個LED聯(lián)動或者給新風系統(tǒng)一個粗略參考SCD40就夠用了。SCD42價格最低但壽命短了一半工業(yè)場景慎選。另外市場上有些模塊打著SCD41的旗號賣實際是SCD40到手可以讀一下序列號或者對比一下數(shù)據(jù)能看出一些端倪。還有一個關(guān)鍵點SCD4X的工作電壓范圍是2.4V到5.5V但I2C電平會跟隨VDD。如果你給傳感器供5VSCL/SDA的高電平也是5VSTM32F103C8T6的GPIO不一定吃得消。穩(wěn)妥的做法是統(tǒng)一用3.3V供電和STM32邏輯電平一致省掉電平轉(zhuǎn)換電路。2. 硬件準備與接線2.1 材料清單實際用到的東西非常精簡這也是這個項目的優(yōu)點之一STM32F103C8T6最小系統(tǒng)板一塊SCD4X傳感器模塊一個我買的是Sensirion官方評估板接口已經(jīng)引出來ST-Link V2下載器一個USB轉(zhuǎn)串口模塊一個用于查看調(diào)試信息杜邦線若干面包板或洞洞板如果你的SCD4X是純芯片需要自己焊注意它底部有暴露的感光孔焊接時不要堵住也不要讓助焊劑殘留在上面。官方評估板會好很多引腳已經(jīng)做好直接連杜邦線就行。2.2 接線圖和注意事項STM32F103C8T6的I2C1默認在PB6SCL和PB7SDA上這個組合最常用。接線表如下SCD4X模塊引腳連接到說明VDD3.3V供電不要接5VGNDGND共地SCLPB6I2C時鐘線SDAPB7I2C數(shù)據(jù)線注意I2C總線上必須有上拉電阻。SCD4X官方模塊板載了上拉電阻直接能工作。如果你是自己畫板或者買的模塊沒帶上拉需要在SCL和SDA上各接一個4.7kΩ到3.3V的電阻否則I2C通信會時好時壞甚至完全掃描不到設(shè)備。另外實際接線時保持杜邦線盡量短I2C在100kHz下長線也能跑但線太長或者面包板接觸不良時容易出現(xiàn)隨機CRC錯誤或者NACK。我第一次調(diào)試時就是因為杜邦線松動導致偶爾能讀到數(shù)據(jù)偶爾讀不到排查了很久才發(fā)現(xiàn)是物理接觸問題。3. 通信協(xié)議與核心原理拆解3.1 SCD4X的I2C地址和命令體系SCD4X的7位I2C地址固定是0x62換算成8位寫地址是0xC4讀地址是0xC5。這個地址是芯片出廠寫死的不能像EEPROM那樣用引腳配置。在HAL庫中調(diào)用HAL_I2C_Master_Transmit時地址參數(shù)要填0xC4。SCD4X的寄存器不是普通的內(nèi)存地址而是通過命令碼來操作。常用命令碼整理在下表功能命令碼16位說明啟動周期測量0x21B1默認模式約5秒更新一次數(shù)據(jù)啟動低功耗周期測量0x21AC約30秒更新一次數(shù)據(jù)停止周期測量0x3F86進入待機模式讀取測量結(jié)果0xEC05讀取9字節(jié)數(shù)據(jù)讀取數(shù)據(jù)就緒狀態(tài)0xE4B8判斷是否有新數(shù)據(jù)單次測量0x219D測完自動待機適合電池供電讀取序列號0x3690返回芯片序列號這里有個容易搞混的地方執(zhí)行任何命令之前芯片必須處于空閑狀態(tài)。如果你一直在跑周期測量模式想發(fā)送單次測量命令是沒用的必須先發(fā)停止命令0x3F86等待大約500ms后再發(fā)新命令。3.2 數(shù)據(jù)幀格式與CRC校驗SCD4X返回的數(shù)據(jù)幀格式很固定。執(zhí)行讀測量結(jié)果命令后芯片返回9個字節(jié)排列如下字節(jié)索引內(nèi)容0-1CO2濃度16位無符號整數(shù)單位ppm2CO2數(shù)據(jù)CRC校驗3-4溫度原始值16位無符號整數(shù)5溫度數(shù)據(jù)CRC校驗6-7濕度原始值16位無符號整數(shù)8濕度數(shù)據(jù)CRC校驗每兩個數(shù)據(jù)字節(jié)后面跟著一個CRC校驗字節(jié)。這個CRC算法是CRC-8多項式是0x31初始值是0xFF和SHT3x傳感器是一套算法。別小看這個校驗I2C線路上稍有干擾CO2數(shù)據(jù)就可能從600ppm跳到99999ppm如果不做CRC校驗這種錯誤數(shù)據(jù)會直接污染你的統(tǒng)計結(jié)果。CRC代碼實現(xiàn)如下uint8_t SCD4X_CRC8(uint8_t *data, uint8_t len) { uint8_t crc 0xFF; for (uint8_t i 0; i len; i) { crc ^ data[i]; for (uint8_t bit 0; bit 8; bit) { if (crc 0x80) crc (crc 1) ^ 0x31; else crc (crc 1); } } return crc; }3.3 原始數(shù)據(jù)如何換算成物理量SCD4X返回的溫度和濕度都是16位原始值需要經(jīng)過線性變換才能得到實際值。公式如下CO2濃度不需要轉(zhuǎn)換原始值就是ppm。比如返回0x0258就是600ppm。溫度℃ -45 175 × 原始值 / 65535相對濕度%RH 100 × 原始值 / 65535舉個例子如果溫度原始值是0x6666十進制的26214代入公式-45 175 × 26214 / 65535 ≈ 25.0℃正好是室溫。濕度原始值如果接近14745換算出來大約22.5%RH。這幾個公式是Sensor數(shù)據(jù)手冊里明確給的不要自己發(fā)明算法。網(wǎng)上有些改版代碼會先移位再換算結(jié)果差得離譜都是沒看懂數(shù)據(jù)手冊導致的。4. CubeMX配置與HAL庫代碼實現(xiàn)4.1 CubeMX關(guān)鍵配置用STM32CubeMX生成工程時只需要配置兩個外設(shè)第一個是I2C1。在Pinout視圖里把PB6設(shè)為I2C1_SCLPB7設(shè)為I2C1_SDA然后在Parameter Settings里把Speed Mode設(shè)為Standard Mode100kHz其他保持默認。這里千萬不要圖快選Fast ModeSCD4X規(guī)格書上明確寫了最大支持100kHz我之前試過400kHz十次里有兩三次會NACK靠不住。第二個是USART1PB6/PB7沒有沖突USART1引腳是PA9TX、PA10RX。配置成異步模式波特率115200用來輸出調(diào)試日志。如果你有OLED顯示屏可以再開一個軟件I2C或者SPI接口不過那是后話先把串口打通。時鐘這塊直接用HSE 8MHz晶振系統(tǒng)時鐘配到72MHzAPB1總線時鐘36MHz。I2C1掛在APB1上36MHz的頻率分頻到100kHzCubeMX會自動幫你算好不用手工干預。4.2 驅(qū)動代碼實現(xiàn)工程生成后我習慣新建一個scd4x.c和scd4x.h把傳感器相關(guān)的操作封裝起來主程序看起來干凈也方便以后復用。先看發(fā)送命令和讀取數(shù)據(jù)的底層函數(shù)#include scd4x.h #include crc.h extern I2C_HandleTypeDef hi2c1; #define SCD4X_ADDR_WRITE 0xC4 #define SCD4X_ADDR_READ 0xC5 uint8_t SCD4X_SendCommand(uint16_t cmd) { uint8_t buf[2]; buf[0] cmd 8; buf[1] cmd 0xFF; return HAL_I2C_Master_Transmit(hi2c1, SCD4X_ADDR_WRITE, buf, 2, 1000); } uint8_t SCD4X_ReadData(uint8_t *data, uint8_t len) { return HAL_I2C_Master_Receive(hi2c1, SCD4X_ADDR_READ, data, len, 1000); }然后封裝一個讀取測量結(jié)果的函數(shù)讀完直接做CRC校驗和物理量換算uint8_t SCD4X_ReadMeasurement(float *co2, float *temperature, float *humidity) { uint8_t buf[9]; uint16_t co2_raw, temp_raw, hum_raw; if (SCD4X_SendCommand(0xEC05) ! HAL_OK) return 1; HAL_Delay(2); if (SCD4X_ReadData(buf, 9) ! HAL_OK) return 2; if (buf[2] ! SCD4X_CRC8(buf[0], 2) || buf[5] ! SCD4X_CRC8(buf[3], 2) || buf[8] ! SCD4X_CRC8(buf[6], 2)) return 3; co2_raw (buf[0] 8) | buf[1]; temp_raw (buf[3] 8) | buf[4]; hum_raw (buf[6] 8) | buf[7]; *co2 co2_raw; *temperature -45.0f 175.0f * temp_raw / 65535.0f; *humidity 100.0f * hum_raw / 65535.0f; return 0; }注意一個細節(jié)SCD4X執(zhí)行完讀測量結(jié)果命令之后內(nèi)部需要一點時間準備數(shù)據(jù)我實測加2ms延時比較穩(wěn)。你在網(wǎng)上看到的很多代碼不延時就直讀偶爾也行但在芯片剛啟動或者剛從低功耗喚醒時容易讀到0xFF。加上這2ms問題就消失了。4.3 主流程和串口輸出主程序邏輯很清晰上電延時啟動周期測量然后在循環(huán)里等數(shù)據(jù)就緒標志就緒后讀取數(shù)據(jù)并打印。int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_I2C1_Init(); MX_USART1_UART_Init(); float co2, temp, hum; uint8_t data_ready[3]; printf(SCD4X Test Start\r\n); if (SCD4X_SendCommand(0x21B1) ! HAL_OK) { printf(Start periodic measurement failed\r\n); while (1); } while (1) { HAL_Delay(1000); if (SCD4X_SendCommand(0xE4B8) ! HAL_OK) continue; HAL_Delay(2); if (SCD4X_ReadData(data_ready, 3) ! HAL_OK) continue; if (data_ready[0] 0x01) { if (SCD4X_ReadMeasurement(co2, temp, hum) 0) { printf(CO2: %.0f ppm, Temp: %.2f C, Hum: %.2f %%RH\r\n, co2, temp, hum); } } } }串口重定向printf的代碼我用的是標準方式在usart.c里補一個fputc把輸出落到USART1就行這塊比較基礎(chǔ)就不展開說了。實際運行效果符合預期上電約5秒后串口打出第一組數(shù)據(jù)之后每5秒更新一次。CO2數(shù)值穩(wěn)定在450~550ppm左右室內(nèi)開窗通風時溫度和濕度跟家用溫濕度計對比誤差在可接受范圍內(nèi)。5. 數(shù)據(jù)校準與補償實戰(zhàn)5.1 自動自校準和強制校準的選擇SCD4X默認開啟了自動自校準ASC功能。它的原理是傳感器長期運行中會持續(xù)跟蹤一段時間內(nèi)的最低CO2讀數(shù)默認認為這個最低值接近400ppm即室外新鮮空氣背景值然后自動修正零點。這個功能適合長時間運行的室內(nèi)空氣質(zhì)量監(jiān)測儀因為室內(nèi)CO2濃度經(jīng)常波動到室外水平傳感器可以自己慢慢修正。但在溫室大棚、動物養(yǎng)殖場這類CO2濃度長期高于1000ppm的場景最低值也遠高于400ppmASC反而會把零點拉偏這時候建議關(guān)閉自動校準改用強制校準。強制校準的流程是先讓傳感器在400ppm的標定環(huán)境下運行幾分鐘然后發(fā)送強制校準命令芯片內(nèi)部會計算出零點偏移并保存。對普通玩家來說最簡單的方法是把設(shè)備拿到室外通風處讓傳感器吹10分鐘新鮮空氣然后執(zhí)行校準命令效果就很不錯。我沒法在這里寫出絕對可靠的操作步驟因為官方手冊也強調(diào)需要穩(wěn)定的400ppm標準氣體才能做到絕對準確但家用場景用室外空氣替代基本夠用。5.2 溫度偏移修正和海拔設(shè)置SCD4X芯片在工作時自身會發(fā)熱雖然發(fā)熱量很小但放在密封外殼里實測讀數(shù)會比環(huán)境溫度高1到3攝氏度。這個問題在數(shù)據(jù)手冊里有明確說明也提供了溫度偏移修正命令偏移值單位是攝氏度乘以2的16次方再寫進去。如果你只是讀取溫度和濕度做參考這個偏移可以忽略。但如果你用SCD4X算露點或者做環(huán)境溫度記錄那1℃的誤差就很明顯了。我自己的做法是把傳感器放在通風良好的位置和下圖的參考溫度計對比找一個穩(wěn)定的差值作為偏移量寫入。另外SCD4X內(nèi)部使用氣壓來修正CO2讀數(shù)默認值是海拔高度對應的標準大氣壓。如果你在高海拔地區(qū)使用比如海拔2000米以上最好把當?shù)睾0卧O(shè)置進去。這個命令相對冷門很多教程都不提但對測量結(jié)果的準確性影響還是挺明顯的。6. 踩坑實錄與排查速查表6.1 最常見的幾個問題我這次調(diào)試過程中遇到的坑以及網(wǎng)上被問得最多的問題集中匯總?cè)缦卢F(xiàn)象根本原因解決方法I2C掃描不到0x62設(shè)備SDA/SCL接反或上拉電阻缺失核對接線補4.7kΩ上拉上電后一直讀不到數(shù)據(jù)沒有先啟動周期測量或啟動后等待時間不足啟動測量后至少等5秒再讀數(shù)據(jù)全是0xFF讀測量結(jié)果命令后沒有延時發(fā)完命令后加2ms延時CRC校驗頻繁失敗杜邦線接觸不良或I2C速率過高換短線牢固連接確認100kHzCO2讀數(shù)跳動巨大傳感器被誤拾上拉電阻被拔掉或者受風直吹檢查焊接避免放置在風口溫度比室溫高2℃以上傳感器自身發(fā)熱且外殼密封用溫度偏移命令修正6.2 STM32F103硬件I2C的死鎖問題關(guān)于STM32F1系列硬件I2C業(yè)界流傳的說法是“有bug”容易死鎖。我實際體驗下來死鎖確實遇到過但大多發(fā)生在通信出錯后沒有恢復機制的情況下。SCD4X是慢速設(shè)備只要嚴格按照命令時序來HAL庫的I2C基本穩(wěn)定。如果真遇到死鎖最簡單的恢復辦法是把I2C外設(shè)DeInit再Init一次或者直接對GPIO做一次拉高脈沖強制釋放總線。這算是最簡單可靠的兜底方案代碼如下void I2C_Recovery(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_I2C_DISABLE(hi2c1); GPIO_InitStruct.Pin GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); for (int i 0; i 9; i) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); HAL_Delay(1); } MX_I2C1_Init(); }這個方法在從機意外拉低SDA導致總線鎖死時特別管用。不過最好還是在通信錯誤后加錯誤重試邏輯不要每次都靠硬件復位來解決。6.3 快速排查清單我在調(diào)完這個項目后給自己總結(jié)了一套固定的排查順序每次換新板、換新傳感器全部走一遍基本幾分鐘就能定位問題用邏輯分析儀或示波器抓SCL和SDA波形確認有信號。用I2C掃描代碼確認0x62設(shè)備在線。用串口打印啟動周期測量的返回值確認命令成功。等夠5秒后打印數(shù)據(jù)就緒狀態(tài)確認芯片在工作。再讀測量結(jié)果看CRC有沒有報錯。如果數(shù)據(jù)還是異常檢查供電電壓和上拉電阻。這套流程看起來簡單但真的能省下大量瞎猜的時間。個人實際使用體會和擴展建議SCD4X和之前常用的SCD30相比體積小了很多功耗也更低待機時電流幾乎可以忽略非常適合做電池供電的便攜式空氣質(zhì)量記錄儀。但要注意的是SCD4X的通信協(xié)議比SCD30稍繁瑣一些啟動命令、等待就緒、讀數(shù)據(jù)三步缺一不可剛開始接觸時需要耐心讀一下數(shù)據(jù)手冊。我在實際使用中最滿意的場景是用SCD41做了一個桌面空氣監(jiān)測小玩意CO2超過1000ppm時蜂鳴器提醒開窗通風冬天實測下來確實能提醒自己在門窗緊閉的房間里及時換氣。SCD4X的體積和功耗讓我可以把它塞進各種小外殼里再加上OLED顯示屏整機電流也不過幾十毫安用一塊1000mAh鋰電池就能跑很長時間。如果你手頭已經(jīng)有STM32F103C8T6板子不妨直接按本文步驟試一下。順利的話從零到讀到第一組數(shù)據(jù)兩個小時以內(nèi)就能搞定。后面還可以玩出更多花樣比如用FreeRTOS多任務定期采集數(shù)據(jù)、把數(shù)據(jù)通過ESP8266模塊上傳到云平臺做歷史曲線甚至把這個驅(qū)動移植到國產(chǎn)替代的GD32F103上代碼基本上不用改。SCD4X這個傳感器給我的整體印象是性能穩(wěn)定、資料規(guī)范、開發(fā)難度適中是一個非常適合入門到進階的傳感器項目。本文還有配套的精品資源點擊獲取