動SHT30溫濕度傳感器:從I2C通信到實戰(zhàn)排坑)
簡介面向STM32開發(fā)者的SHT30溫濕度傳感器HAL庫例程包基于STM32F130ZET6平臺適合需要快速實現(xiàn)I2C溫濕度采集的中初級嵌入式開發(fā)者也可作為智能家居、環(huán)境監(jiān)測等應(yīng)用場景的參考方案。SHT30具備±2%濕度與±0.3°C溫度精度搭配主頻72MHz、512KB閃存的STM32F130ZET6可應(yīng)對多數(shù)環(huán)境監(jiān)測需求。例程完整演示了SHT30的驅(qū)動配置、數(shù)據(jù)讀取與解析流程涵蓋HAL庫I2C初始化、主從收發(fā)函數(shù)調(diào)用、溫濕度換算等關(guān)鍵環(huán)節(jié)可直接編譯運行并觀察實時數(shù)據(jù)。資源包共231個文件、10.22MB以I2C驅(qū)動、HAL庫源文件、定時器及串口模塊為主同時包含工程配置文件如uvprojx、ewp與編譯中間文件o、axf等便于在Keil或IAR環(huán)境下打開調(diào)試。除核心源碼外包內(nèi)還提供初始化配置和完整參考工程有助于理解I2C通信時序、HAL庫API調(diào)用方式以及如何根據(jù)傳感器數(shù)據(jù)驅(qū)動后續(xù)控制邏輯。同時代碼結(jié)構(gòu)清晰、注釋完整方便直接復(fù)用或二次開發(fā)。已有1977人下載學(xué)習(xí)。 講實話我以前做環(huán)境監(jiān)測項目最早用的是DHT11后來換成了SHT30中間還折騰過一陣子AHT20?,F(xiàn)在手頭固定板子型號是STM32F103ZET6用的是ST官方HAL庫。如果你正好在找一個能直接跑的SHT30例程又不想被網(wǎng)上那些標(biāo)準(zhǔn)外設(shè)庫辦法繞暈?zāi)沁@篇就是給你寫的。我會把SHT30的I2C通信細(xì)節(jié)、HAL庫工程怎么配、驅(qū)動代碼怎么拆解還有我實際調(diào)板子時踩過的坑一次性說完。1. 先說說為什么棄用DHT11換到SHT30DHT11在很多入門教程里出現(xiàn)率極高因為它便宜、接線簡單、代碼也好寫。但用過的都知道它的精度實在拿不出手——溫度精度正負(fù)2℃濕度精度正負(fù)5%有些批次甚至更離譜。放室內(nèi)看個大概還行一旦要做數(shù)據(jù)記錄、恒溫控制或者產(chǎn)品和儀器放一起DHT11的數(shù)據(jù)出來根本不敢用來做決策。SHT30是Sensirion的新一代數(shù)字溫濕度傳感器溫度精度能做到正負(fù)0.2℃典型值濕度精度正負(fù)2%注意是典型值。更關(guān)鍵的SHT30出廠就做了個體校準(zhǔn)不需要你手里攢一堆溫濕度計去逐點修正。單次測量的平均電流約15μA周期測量模式下平均電流甚至可以到1μA左右電池方案完全扛得住。這個精度和功耗差異就是我換到SHT30的根本原因。另外一個實際考量是通信接口。DHT11用單總線時序要求苛刻一旦中斷被嵌套或者系統(tǒng)忙時序就容易飄讀出來的數(shù)據(jù)是錯的你都不知道錯在哪只能一遍遍調(diào)延時。SHT30用的是標(biāo)準(zhǔn)I2C接口地址固定為0x44或0x45主機只要按I2C協(xié)議發(fā)命令、收數(shù)據(jù)就行時序由I2C控制器和內(nèi)部的晶振保證代碼上不需要精確卡微秒級延時。這在HAL庫環(huán)境下尤其省心——ST官方庫把I2C時序細(xì)節(jié)全封裝好了你只需要調(diào)用HAL_I2C_Master_Transmit和HAL_I2C_Master_Receive兩個函數(shù)。SHT30的封裝也友好有DFN-8小封裝也有帶PCB焊盤的貼片版本手工焊完全能做。如果你像我一樣用核心板擴展找個SHT30模塊也行模塊上一般自帶上拉電阻和穩(wěn)壓電路接線就四根VCC、GND、SCL、SDA。板上沒有硬件I2C上拉的話我這里提醒一句SCL和SDA一定要各接一個10kΩ上拉到3.3V否則通信會間歇性失敗后面細(xì)講。2. SHT30不能只調(diào)包命令、時序與數(shù)據(jù)格式很多新手拿到SHT30第一反應(yīng)是找個現(xiàn)成例程復(fù)制粘貼。但驅(qū)動這類數(shù)字傳感器光會調(diào)庫函數(shù)是不夠的。SHT30手冊里其實就干了幾件事發(fā)命令、等測量、讀數(shù)據(jù)、解算。但每一環(huán)都有門道。先說話命令。SHT30的單次測量命令是兩字節(jié)0x2C是高字節(jié)低字節(jié)決定重復(fù)性和時鐘拉伸行為。常用低字節(jié)有這么幾個命令低字節(jié)重復(fù)性時鐘拉伸測量時間上限0x06高使能15.5ms0x10高禁用15.5ms0x0D中使能6.5ms0x1E中禁用6.5ms0x13低使能2.5ms0x27低禁用2.5ms時鐘拉伸這里要多解釋一句。所謂時鐘拉伸就是從設(shè)備在測量期間主動拉低SCL告訴主機我還沒準(zhǔn)備好你別往下走。這在單次測量、主機不等延時直接讀取的場景下很有用。但不巧STM32F103的HAL庫I2C在阻塞模式下對時鐘拉伸的支持并不順暢實際測下來用0x2C 0x06配合HAL_I2C_Master_Receive有時候會卡在等待SCL釋放的地方我后文會專門講這個坑。所以我做例程時默認(rèn)用0x2C 0x10——禁用時鐘拉伸高重復(fù)性然后在發(fā)完命令后主動HAL_Delay(20)等它測量完再讀。20ms比15.5ms的上限多了余量哪怕芯片略有批次差異也足夠。你要是做低功耗可以把重復(fù)性降到中或低延時隨之縮短。接下來是讀取數(shù)據(jù)。SHT30會返回6字節(jié)結(jié)構(gòu)固定成字節(jié)索引內(nèi)容0溫度數(shù)據(jù)高位1溫度數(shù)據(jù)低位2溫度CRC8校驗值3濕度數(shù)據(jù)高位4濕度數(shù)據(jù)低位5濕度CRC8校驗值溫度和濕度原始值都是16位無符號數(shù)。溫度轉(zhuǎn)換公式是-45 175 * rawT / 65535濕度轉(zhuǎn)換公式是100 * rawH / 65535。這個公式手冊上有但你真寫代碼時很容易把除數(shù)和被除數(shù)寫反我犯過這毛病結(jié)果溫度直接飆到一百多度。注意rawT和rawH都是0到65535的整數(shù)65535就是滿量程。重點說一下CRC8校驗。兩個數(shù)據(jù)各跟一個校驗字節(jié)已經(jīng)是很業(yè)界良心的做法了。多項式是0x31初始值0xFF和很多I2C傳感器用的CRC算法一致。比如SHT31、SGP30都是這套寫一遍以后還能復(fù)用到別的傳感器上。千萬別偷懶跳過校驗數(shù)據(jù)線稍微長一點或者供電紋波大偶發(fā)一個字節(jié)錯誤是會很折磨人的有校驗?zāi)苤苯咏o你報錯重讀比你在串口上看到濕度98%但實際濕度60%強一百倍。還有一個容易忽略的細(xì)節(jié)SHT30上電后不要立刻發(fā)命令建議等100ms以上因為它內(nèi)部的穩(wěn)壓器和晶振需要啟動穩(wěn)定時間。我自己例程里在MCU初始化后先做了一次空讀操作權(quán)當(dāng)喚醒復(fù)位避免上電毛刺導(dǎo)致首幀數(shù)據(jù)異常。3. 搭建STM32F103ZET6的HAL庫工程標(biāo)題里的F130是筆誤先糾正一個事STM32家族里其實沒有F130ZET6這個型號ST的F1系列里是STM32F103ZET6。舍得查資料就會發(fā)現(xiàn)這個F130大概率是筆誤或者輸入法聯(lián)想出來的。我這邊板子用的就是F103ZET6動不動512KB Flash、64KB RAM的資源做傳感器采集綽綽有余。下面所有操作按F103ZET6進(jìn)行。我建工程用的是STM32CubeMX圖形化配置它生成HAL庫框架然后我再往里加驅(qū)動。CubeMX版本不用太糾結(jié)我用的是6.x系列。打開CubeMX新建工程時芯片型號選STM32F103ZET6調(diào)試接口要記得在SYS里勾上Serial Wire不然板子第一次下載完還能跑第二次就提示找不到芯片了這個坑不知道坑過多少人了。時鐘樹部分F103ZET6最高主頻72MHz。我外部晶振用的8MHz無源晶振配置HSE并倍頻到72MHzAPB1總線注意要二分頻到36MHz這不光是為了系統(tǒng)穩(wěn)定更關(guān)鍵的是I2C外設(shè)掛在APB1上而I2C想要400kHz快速模式前提就是APB1時鐘36MHz。你要是用默認(rèn)的72MHz給APB1I2C時序會完全不對通信基本廢掉。外設(shè)勾選這邊主要開三樣I2C1我實際用PB6和PB7對應(yīng)I2C1的SCL和SDA外設(shè)使能后選Standard Mode也就是100kHz。實際測試100kHz最穩(wěn)SHT30數(shù)據(jù)手冊雖然支持400kHz但線路長了之后400kHz很容易出現(xiàn)CRC校驗失敗。USART1也就是PA9的TX和PA10的RX波特率115200用來把溫濕度打印到串口助手。GPIO留一個LED引腳做運行指示我用的PD2你也可以用板載LED對應(yīng)的引腳。CubeMX生成完代碼后別忘了檢查一下I2C初始化函數(shù)中Timing參數(shù)。HAL庫在F1上I2C初始化時不需要像F4那樣手動算I2C_TIMINGR寄存器而是由HAL庫根據(jù)APB1時鐘自動計算這算是F1的一個優(yōu)勢。你只要保證I2C1的Clock Speed填100000就行。工程搭建完成后目錄里會有Inc/Src兩個主要文件夾。驅(qū)動文件我會新建sht30.h和sht30.c放在Src和Inc目錄下。這里面有一個隱藏注意點CubeMX生成的代碼不吃手動往main.c里塞上千行工程式代碼尤其每次你重新生成工程main.c會被覆蓋重寫所以我的習(xí)慣是全部驅(qū)動獨立成文件main.c里只留調(diào)用入口這樣重新生成工程時不需要心疼自己寫的代碼丟失。4. 驅(qū)動代碼逐段解析從單次測量到CRC校驗下面這段驅(qū)動就是我最常用、實測最穩(wěn)的版本。先說清楚這個驅(qū)動只做單次測量模式周期測量我放到最后擴展里講。代碼思路很簡單先發(fā)命令再延時再讀6字節(jié)最后解算。在sht30.h里這樣聲明#ifndef __SHT30_H #define __SHT30_H #include main.h uint8_t SHT30_Read_TH(float *temperature, float *humidity); #endifsht30.c的完整實現(xiàn)#include sht30.h #define SHT30_ADDR_W (0x44 1) // I2C寫地址7位地址0x44左移 #define SHT30_ADDR_R (0x44 1) | 0x01 // I2C讀地址 #define SHT30_CMD_MS_H 0x2C // 單次測量命令高字節(jié) #define SHT30_CMD_MS_L 0x10 // 高重復(fù)性禁用時鐘拉伸 // CRC8校驗多項式0x31初始值0xFF static uint8_t SHT30_CRC8(uint8_t *data, uint8_t len) { uint8_t crc 0xFF; while (len--) { crc ^ *data; for (uint8_t i 0; i 8; i) { crc (crc 0x80) ? (uint8_t)((crc 1) ^ 0x31) : (uint8_t)(crc 1); } } return crc; } uint8_t SHT30_Read_TH(float *temperature, float *humidity) { uint8_t cmd[2] {SHT30_CMD_MS_H, SHT30_CMD_MS_L}; uint8_t buf[6] {0}; if (HAL_I2C_Master_Transmit(hi2c1, SHT30_ADDR_W, cmd, 2, 100) ! HAL_OK) { return 1; } // 高重復(fù)性測量上限15.5ms給20ms余量 HAL_Delay(20); if (HAL_I2C_Master_Receive(hi2c1, SHT30_ADDR_R, buf, 6, 100) ! HAL_OK) { return 2; } // 分別校驗溫度和濕度的CRC if (SHT30_CRC8(buf[0], 2) ! buf[2]) { return 3; } if (SHT30_CRC8(buf[3], 2) ! buf[5]) { return 4; } uint16_t rawT ((uint16_t)buf[0] 8) | buf[1]; uint16_t rawH ((uint16_t)buf[3] 8) | buf[4]; *temperature -45.0f 175.0f * (float)rawT / 65535.0f; *humidity 100.0f * (float)rawH / 65535.0f; return 0; }這段代碼有幾個關(guān)鍵點要展開講。第一I2C地址為什么要左移。SHT30的7位地址是0x44焊盤ADDR接GND時為0x45。但在HAL庫的HAL_I2C_Master_Transmit里傳入的地址必須包含讀寫位也就是8位地址所以得把0x44左移1位得到0x88作為寫地址或最低位置1得到0x89作為讀地址。很多人剛開始會直接把0x44填進(jìn)去然后發(fā)現(xiàn)I2C總線根本沒有ACK就是卡在這。第二為什么我不把HAL_Delay換成查詢狀態(tài)。SHT30支持一種變通發(fā)完命令直接進(jìn)入讀流程依靠時鐘拉伸等傳感器完成。前面提過這個思路在STM32F103 HAL庫下容易踩坑。所以我寧愿犧牲20ms延時換取確定不卡死的操作。在大多數(shù)采集場景20ms間隔完全夠用就算你要一秒采一次也沒影響。第三返回錯誤碼代替直接停機。驅(qū)動不管讀成功與否都返回一個狀態(tài)碼主程序可以根據(jù)狀態(tài)碼決定是打印串口信息還是亮LED報警。我習(xí)慣用1表示發(fā)送失敗2表示接收失敗3表示溫度CRC錯誤4表示濕度CRC錯誤。這樣一旦現(xiàn)場出了問題看一眼錯誤碼就能定位到通信的哪個環(huán)節(jié)。主程序調(diào)用就簡單了我在while循環(huán)里每秒讀一次float temp 0.0f, humi 0.0f; uint8_t ret 0; char msg[64]; while (1) { ret SHT30_Read_TH(temp, humi); if (ret 0) { sprintf(msg, Temp: %.2f C, Humi: %.2f %%\r\n, temp, humi); HAL_UART_Transmit(huart1, (uint8_t *)msg, strlen(msg), 100); } else { sprintf(msg, SHT30 Error: %d\r\n, ret); HAL_UART_Transmit(huart1, (uint8_t *)msg, strlen(msg), 100); HAL_Delay(300); } HAL_Delay(1000); }這里注意sprintf格式化濕度百分比時我在格式串中用的是兩個百分號%因為sprintf里單個百分號會被識別成格式符不加兩個百分號打印出來數(shù)據(jù)就亂套了。這種細(xì)節(jié)調(diào)試時最浪費時間。5. 燒進(jìn)去之后三個實測坑與排查方法代碼寫完了上電實測階段往往才是真正耗時間的地方。我在三輪不同項目的調(diào)試中先后遇到這么幾個典型問題每一個都值得拿出來復(fù)盤。第一個坑也是最常見的串口一直打Error Code 2或者3接收失敗或者CRC過不去。先別急著懷疑代碼拿出示波器或者邏輯分析儀抓I2C波形。如果沒有示波器就直接看SDA和SCL的對地電壓用萬用表量正??臻e狀態(tài)都應(yīng)該是3.3V。如果是0V說明總線被拉死大概率是模塊上拉電阻缺失。我之前用過一塊很便宜的SHT30模塊板子上沒焊上拉量下來SCL一直低電平。補兩個10k電阻到3.3V問題立刻消失。如果你手頭正好沒有10k4.7k到47k的也可以用只是上拉太小會加重主機負(fù)載上拉太大則翻轉(zhuǎn)速度變慢別太離譜就行。第二個坑發(fā)完0x2C 0x06后HAL_I2C_Master_Receive卡死。這個坑我印象極深。我用0x2C 0x06配合HAL庫直接讀取結(jié)果程序死在接收函數(shù)里HardFault前反復(fù)卡在上面的超時查詢。仔細(xì)看SHT30手冊才發(fā)現(xiàn)0x06是使能時鐘拉伸傳感器測量期間會把SCL拉低HAL庫在阻塞模式下一直等待SCL釋放但SHT30既要拉低SCL又要回數(shù)據(jù)兩者配合在F1的I2C模塊上就產(chǎn)生了死鎖現(xiàn)象。原理層面的東西不好深究但解決起來很簡單命令改成0x10禁用時鐘拉伸然后自己延時20ms。改完之后這個問題再沒出現(xiàn)過。這也讓我徹底認(rèn)清硬件I2C固然穩(wěn)定但必須匹配從設(shè)備的時序特性不是所有命令都合適。第三個坑是數(shù)據(jù)偶發(fā)跳變溫度濕度一會兒正常一會兒離譜地大。排查下來發(fā)現(xiàn)是我在測試時杜邦線飛了大概20厘米供電又從另一塊板子上取導(dǎo)致地回路不穩(wěn)。SHT30這類高精度傳感器對供電很敏感我后來在VCC和GND之間加了一個100nF去耦電容并且把I2C線縮短到10厘米以內(nèi)跳變就消失了。這個現(xiàn)象在DHT11上不會那么明顯因為DHT11本身精度就差跳變兩三個點你根本不在乎但SHT30精度高偶發(fā)偏差就會直接暴露出來。除了這三個坑還有幾個邊界情況要處理。硬件I2C偶爾會因從機異常進(jìn)入總線鎖死狀態(tài)表現(xiàn)為SCL/SDA長期為低。我在工程里加了一個I2C總線恢復(fù)函數(shù)原理是把SCL對應(yīng)的引腳臨時配置為GPIO輸出手動翻轉(zhuǎn)至少9個時鐘同時把SDA釋放為高電平這樣可以讓卡在錯誤狀態(tài)的從機復(fù)位。這個函數(shù)平時用不上但傳感器熱插拔或者供電抖動之后它能救你一把。另外關(guān)于讀取頻率SHT30單次測量模式下最小間隔可以做到幾十毫秒但我不建議無腦快速循環(huán)讀。一方面頻繁讀會增加功耗另一方面?zhèn)鞲衅鞅旧碛幸欢ǖ捻憫?yīng)時間讀數(shù)變化不會瞬時發(fā)生你要做數(shù)據(jù)平滑不如直接讀取間隔設(shè)置到500ms或1s以上。做產(chǎn)品尤其要注意這個別把CPU和I2C總線無限壓榨。6. 再往前一步周期測量模式與低功耗設(shè)計建議單次測量完全可以覆蓋大多數(shù)應(yīng)用但如果你做的是電池供電設(shè)備比如溫濕度記錄儀或者智能家居里的傳感器節(jié)點那SHT30的周期測量模式更值得研究。周期測量模式下傳感器內(nèi)部會以固定頻率自己完成測量兩種常見配置是每秒2次命令0x20 0x32和每秒10次命令0x20 0x22都是高重復(fù)性。設(shè)置好之后主機不用再發(fā)測量命令只要定時去讀6字節(jié)數(shù)據(jù)就行。這樣主控可以在兩次讀取之間進(jìn)入睡眠由傳感器自己維持測量節(jié)奏功耗優(yōu)勢非常明顯。實測下來2mps配置下SHT30的平均電流在數(shù)十微安級別一顆小鋰電池足夠撐很久。但周期模式也有個容易誤解的地方命令發(fā)一次后傳感器會一直按這個頻率測量如果你不切換到其他模式它永遠(yuǎn)不會自己停。所以代碼里的初始化邏輯要做好管理比如設(shè)備上電后發(fā)一次0x20 0x32后續(xù)循環(huán)里直接HAL_I2C_Master_Receive讀數(shù)據(jù)不要再重復(fù)發(fā)命令否則每次都會疊加一次配置重置影響讀時序。想退出周期模式時可以發(fā)送單次測量命令比如0x2C 0x10傳感器就會回到單次模式。還要留意周期模式下讀到的值并不一定每次都是新的。因為數(shù)據(jù)緩存更新頻率和讀取頻率可能存在相位差連續(xù)讀兩次可能拿到同一個數(shù)據(jù)。這不算故障做數(shù)據(jù)記錄時去重或做一下時間戳即可。如果讀數(shù)長期不變才需要懷疑傳感器卡在舊狀態(tài)可以用讀取狀態(tài)寄存器命令0xF3 0x2D檢查Bit15是否為零零表示正常狀態(tài)。低功耗設(shè)計的另一個細(xì)節(jié)是SHT30在每次測量完成后需要至少一個I2C停止條件才能進(jìn)入內(nèi)部空閑狀態(tài)。所以無論用HAL庫還是軟件模擬I2C讀取結(jié)束一定要讓總線產(chǎn)生STOP這也是HAL_I2C_Master_Receive天然做好的事情不需要額外處理。還有一個容易被忽略的小功能SHT30內(nèi)部帶加熱器可以通過命令0x30 0x0D開啟0x30 0x00關(guān)閉。這個加熱器不是用來給環(huán)境加溫的而是用來在潮濕或者結(jié)露環(huán)境下烘干傳感器表面保證濕度測量不會因為傳感器罩內(nèi)積水而失真。如果你把設(shè)備用在戶外或者冷庫門口這類濕度高的地方可以在初始化后周期性開啟加熱器十幾秒然后關(guān)閉等穩(wěn)定再讀值。我自己很少用但在一個冷鏈記錄項目里測試過確實能把高濕工況下的數(shù)據(jù)漂移拉回來。7. 寫在最后的一點實戰(zhàn)體會如果把整個SHT30驅(qū)動過程濃縮成一條經(jīng)驗?zāi)蔷褪菙?shù)字傳感器調(diào)試的核心不是把代碼寫出來而是把通信鏈路上每一段電氣行為和時序行為都搞明白。DHT11的軟件時序讓你學(xué)會摳延時SHT30的HAL庫例程則讓你學(xué)會信協(xié)議、查CRC、管理總線狀態(tài)。這兩個方向并不沖突經(jīng)歷過一個再上手另一個上手速度會快很多。我個人在實際項目中的最終配置就是STM32F103ZET6作為主控硬件I2C1速率100kHzSHT30地址0x44命令固定0x2C 0x10加20ms延時讀取后帶CRC校驗串口打印。這套組合我在至少三個不同環(huán)境下的設(shè)備上連續(xù)運行過數(shù)月數(shù)據(jù)穩(wěn)定沒有一次總線鎖死或者臟數(shù)據(jù)。你照著搭大概率也能一次跑通。如果后續(xù)想擴展建議去看Sensirion官方的嵌入式驅(qū)動包里面還有狀態(tài)寄存器解析、加熱器控制、傳感器復(fù)位等接口邏輯清晰拿過來改改就能嵌套進(jìn)你的HAL工程。不過官方的抽象層次較高不建議第一版例程就引進(jìn)去先把單次測量跑通、把原始數(shù)據(jù)打印出來再逐步增加復(fù)雜度這才是最穩(wěn)妥的路徑。本文還有配套的精品資源點擊獲取