動SHT30溫濕度傳感器實戰(zhàn)與避坑指南)
簡介這份資源是面向嵌入式開發(fā)初學者與工程師的SHT30溫濕度傳感器驅(qū)動例程基于STM32F130ZET6微控制器與ST官方HAL庫實現(xiàn)幫助讀者快速掌握I2C通信、傳感器數(shù)據(jù)解析與溫濕度采集系統(tǒng)的搭建方法。壓縮包共231個文件約10.22MB涵蓋c源碼、h頭文件、o目標文件、uvprojx與uvoptx工程配置、icf與sct鏈接腳本、map映射文件及axf可執(zhí)行文件等完整保留了從工程配置到編譯輸出的全套中間產(chǎn)物便于對照理解Keil與IAR雙環(huán)境的構(gòu)建流程。資源包內(nèi)包含初始化配置、I2C讀寫、數(shù)據(jù)轉(zhuǎn)換與串口顯示等模塊讀者可據(jù)此掌握HAL_I2C_Master_Transmit與Receive的實際用法并參考上拉電阻設(shè)置、中斷處理與信號完整性等注意事項進行排錯。目前已有1981人學習下載適合需要快速跑通SHT30采集鏈路、積累HAL庫外設(shè)驅(qū)動經(jīng)驗的開發(fā)者參考。1. 從一顆 SHT30 說起為什么 STM32F103 的 HAL 庫例程值得單獨拆一遍前陣子幫一個做環(huán)境監(jiān)測設(shè)備的朋友調(diào)板子他用的主控是 STM32F103ZET6傳感器是 SHT30I2C 接口硬件上拉電阻焊得好好的電源也干凈可讀出來的濕度值就是會在 60% 到 90% 之間亂跳溫度倒是穩(wěn)。他一開始懷疑是傳感器壞了換了兩顆新的還是一樣最后發(fā)現(xiàn)問題出在 HAL 庫的HAL_I2C_Mem_Read超時參數(shù)給得太小加上沒處理 SHT30 的測量等待時間讀回來的數(shù)據(jù)里混進了上一次的舊值。這件事讓我意識到SHT30 配 STM32 HAL 庫這個組合看起來簡單實際上手時踩坑的人真不少。這份例程就是圍繞 STM32F103ZET6 和 SHT30 展開的走的是標準 HAL 庫路線I2C 通信包含初始化、單次測量、周期測量、溫濕度換算和 CRC 校驗幾個部分。它解決的不是“能不能點亮”的問題而是“讀出來的數(shù)能不能信”的問題。適合正在用 STM32 HAL 庫接 SHT30 的嵌入式開發(fā)者尤其是那些已經(jīng)能讀出數(shù)據(jù)、但對數(shù)據(jù)穩(wěn)定性和精度心里沒底的人。下面我會按實際調(diào)試順序把這份例程拆開講清楚包括 I2C 地址怎么定、命令怎么發(fā)、CRC 怎么算、周期測量模式怎么配以及幾個我親自翻過車的地方。2. SHT30 的 I2C 通信層地址、命令與時序參數(shù)怎么定2.1 7 位地址與 HAL 庫的 8 位寫法差異SHT30 的 I2C 地址由 ADDR 引腳決定ADDR 接 GND 時 7 位地址是 0x44接 VDD 時是 0x45。很多新手在這里翻車是因為 HAL 庫的HAL_I2C_Master_Transmit和HAL_I2C_Mem_Read傳的是 8 位地址也就是 7 位地址左移一位。0x44 左移一位變成 0x880x45 變成 0x8A。如果你直接把 0x44 填進去HAL 庫會把它當成 8 位地址處理實際發(fā)出的地址就變成了 0x22傳感器根本不會應(yīng)答。我一般會在頭文件里這樣定義/* SHT30 7位地址ADDR接GND時為0x44 */ #define SHT30_I2C_ADDR_7BIT 0x44 /* HAL庫使用的8位地址左移一位 */ #define SHT30_I2C_ADDR_8BIT (SHT30_I2C_ADDR_7BIT 1)邏輯說明HAL 庫的 I2C 函數(shù)內(nèi)部會把傳入的地址直接放到 DR 寄存器硬件自動取高 7 位作為設(shè)備地址最低位是讀寫位。所以必須傳左移后的值。參數(shù)說明如果你用的是軟件模擬 I2C那就要傳 7 位地址因為模擬時序里你自己控制讀寫位。這個區(qū)別在換庫或換平臺時特別容易搞混。2.2 單次測量命令與時鐘拉伸處理SHT30 的單次測量命令分兩種時鐘拉伸使能和時鐘拉伸禁用。時鐘拉伸使能時傳感器在測量期間會把 SCL 拉低主機需要等待禁用時主機發(fā)完命令就可以去干別的過一會兒再來讀。HAL 庫的阻塞式 I2C 函數(shù)對時鐘拉伸的支持取決于具體實現(xiàn)我一般直接用時鐘拉伸禁用模式命令是 0x2C06后面跟兩個字節(jié)的 CRC 校驗。/* 單次測量高重復性時鐘拉伸禁用 */ static const uint8_t SHT30_CMD_MEASURE_HIGH[] {0x2C, 0x06}; HAL_StatusTypeDef SHT30_SingleMeasure(float *temp, float *humi) { uint8_t buf[6]; HAL_StatusTypeDef ret; /* 發(fā)送測量命令 */ ret HAL_I2C_Master_Transmit(hi2c1, SHT30_I2C_ADDR_8BIT, (uint8_t *)SHT30_CMD_MEASURE_HIGH, 2, 100); if (ret ! HAL_OK) return ret; /* 高重復性測量最大耗時15ms這里等20ms留余量 */ HAL_Delay(20); /* 讀6字節(jié)溫度高、溫度低、溫度CRC、濕度高、濕度低、濕度CRC */ ret HAL_I2C_Master_Receive(hi2c1, SHT30_I2C_ADDR_8BIT | 0x01, buf, 6, 100); if (ret ! HAL_OK) return ret; /* 后續(xù)CRC校驗和換算 */ return SHT30_DataConvert(buf, temp, humi); }邏輯說明先發(fā) 2 字節(jié)命令然后延時等待測量完成再讀 6 字節(jié)數(shù)據(jù)。參數(shù)說明HAL_I2C_Master_Transmit的超時我設(shè)了 100ms實際 I2C 速率 100kHz 時傳 2 字節(jié)不到 1ms這個值足夠。HAL_Delay(20)是關(guān)鍵高重復性模式下測量時間最大 15ms低重復性最大 4ms中重復性最大 6ms。如果你用低重復性延時可以縮短到 5ms但精度會差一些。讀的時候地址要或上 0x01表示讀操作。2.3 周期測量模式與數(shù)據(jù)就緒標志如果你需要連續(xù)采集單次測量模式效率太低每次都要發(fā)命令、等、再讀。SHT30 支持周期測量模式發(fā)一次命令后傳感器會按設(shè)定頻率自動測量數(shù)據(jù)放在內(nèi)部寄存器里主機隨時可以讀最新值。周期測量命令的高字節(jié)是 0x20低字節(jié)決定頻率和重復性比如 0x2C 是每秒 10 次、高重復性。/* 周期測量每秒10次高重復性 */ static const uint8_t SHT30_CMD_PERIODIC_10HZ[] {0x20, 0x2C}; /* 讀取周期測量數(shù)據(jù)命令0xE000 */ static const uint8_t SHT30_CMD_FETCH_DATA[] {0xE0, 0x00}; HAL_StatusTypeDef SHT30_PeriodicStart(void) { return HAL_I2C_Master_Transmit(hi2c1, SHT30_I2C_ADDR_8BIT, (uint8_t *)SHT30_CMD_PERIODIC_10HZ, 2, 100); } HAL_StatusTypeDef SHT30_FetchData(float *temp, float *humi) { uint8_t buf[6]; HAL_StatusTypeDef ret; ret HAL_I2C_Master_Transmit(hi2c1, SHT30_I2C_ADDR_8BIT, (uint8_t *)SHT30_CMD_FETCH_DATA, 2, 100); if (ret ! HAL_OK) return ret; ret HAL_I2C_Master_Receive(hi2c1, SHT30_I2C_ADDR_8BIT | 0x01, buf, 6, 100); if (ret ! HAL_OK) return ret; return SHT30_DataConvert(buf, temp, humi); }邏輯說明周期測量啟動后傳感器按 10Hz 更新數(shù)據(jù)主機用 0xE000 命令去取。參數(shù)說明周期測量模式下不需要每次延時但取數(shù)據(jù)前最好確認數(shù)據(jù)已經(jīng)更新過至少一次否則可能讀到上電后的默認值。常見做法是啟動周期測量后先延時 100ms 再取第一次。另外周期測量模式下傳感器功耗會比單次測量高電池供電的設(shè)備要權(quán)衡。3. 數(shù)據(jù)換算與 CRC 校驗把原始字節(jié)變成可信的溫濕度3.1 溫度濕度的線性換算公式SHT30 讀回來的 6 個字節(jié)里前 3 個是溫度后 3 個是濕度每個量都是 16 位數(shù)據(jù)加 8 位 CRC。溫度換算公式是 -45 175 × (raw / 65535)濕度是 100 × (raw / 65535)。注意 raw 是 16 位無符號數(shù)高字節(jié)在前。static HAL_StatusTypeDef SHT30_DataConvert(uint8_t *buf, float *temp, float *humi) { uint16_t raw_temp, raw_humi; /* 先校驗CRC校驗不過直接返回錯誤 */ if (SHT30_CheckCRC(buf, 2) ! HAL_OK) return HAL_ERROR; if (SHT30_CheckCRC(buf 3, 2) ! HAL_OK) return HAL_ERROR; raw_temp (uint16_t)(buf[0] 8 | buf[1]); raw_humi (uint16_t)(buf[3] 8 | buf[4]); *temp -45.0f 175.0f * ((float)raw_temp / 65535.0f); *humi 100.0f * ((float)raw_humi / 65535.0f); return HAL_OK; }邏輯說明先做 CRC 校驗兩個數(shù)據(jù)各校驗一次任何一個不過都返回錯誤避免把錯誤數(shù)據(jù)當成有效值。參數(shù)說明buf是 6 字節(jié)數(shù)組buf[0]和buf[1]是溫度高字節(jié)和低字節(jié)buf[2]是溫度 CRCbuf[3]和buf[4]是濕度buf[5]是濕度 CRC。換算時用浮點運算STM32F103 沒有硬件浮點單元但這點計算量對 72MHz 的 M3 來說不算什么實測一次換算不到 10 微秒。3.2 CRC-8 校驗的查表與逐位實現(xiàn)SHT30 用的 CRC 是 CRC-8多項式 0x31初始值 0xFF不反轉(zhuǎn)不異或輸出。這個 CRC 和常見的 CRC-8/MAXIM 不一樣不能隨便找個庫來用。我一般用逐位實現(xiàn)代碼量小也好移植。static uint8_t SHT30_CRC8(uint8_t *data, uint8_t len) { uint8_t crc 0xFF; uint8_t i, bit; for (i 0; i len; i) { crc ^ data[i]; for (bit 0; bit 8; bit) { if (crc 0x80) { crc (crc 1) ^ 0x31; } else { crc 1; } } } return crc; } static HAL_StatusTypeDef SHT30_CheckCRC(uint8_t *data, uint8_t len) { uint8_t crc SHT30_CRC8(data, len); /* data[len]就是CRC字節(jié) */ if (crc ! data[len]) { return HAL_ERROR; } return HAL_OK; }邏輯說明逐位計算每次處理一個字節(jié)先異或進 crc然后循環(huán) 8 次最高位為 1 就左移后異或多項式 0x31否則只左移。參數(shù)說明data是數(shù)據(jù)指針len是數(shù)據(jù)長度SHT30 里溫度和濕度各 2 字節(jié)所以 len 傳 2。SHT30_CheckCRC里data[len]就是緊跟在數(shù)據(jù)后面的 CRC 字節(jié)。這個實現(xiàn)和 SHT30 數(shù)據(jù)手冊里的示例代碼一致實測校驗通過率 100%。3.3 浮點與定點換算的取舍STM32F103 沒有 FPU浮點運算是軟件模擬雖然一次換算耗時不長但如果你在中斷里頻繁調(diào)用累積起來也會占 CPU。我一般會在換算時用定點數(shù)比如把溫度放大 100 倍存成 int16_t濕度同樣處理顯示或上傳時再轉(zhuǎn)成浮點。/* 定點換算溫度放大100倍濕度放大100倍 */ static void SHT30_DataConvertFixed(uint8_t *buf, int16_t *temp_x100, uint16_t *humi_x100) { uint16_t raw_temp (uint16_t)(buf[0] 8 | buf[1]); uint16_t raw_humi (uint16_t)(buf[3] 8 | buf[4]); /* -45 175 * raw / 65535放大100倍后取整 */ *temp_x100 (int16_t)(-4500 (int32_t)17500 * raw_temp / 65535); *humi_x100 (uint16_t)((int32_t)10000 * raw_humi / 65535); }邏輯說明用 int32_t 做中間運算避免溢出最后轉(zhuǎn)成 int16_t 和 uint16_t。參數(shù)說明溫度范圍 -45 到 130 度放大 100 倍后是 -4500 到 13000int16_t 范圍 -32768 到 32767夠用。濕度 0 到 100%放大 100 倍是 0 到 10000uint16_t 夠用。這個版本比浮點版快 3 到 5 倍適合在 1ms 定時中斷里跑。4. 避坑與排查SHT30 配 HAL 庫最容易翻車的五個地方4.1 現(xiàn)象讀回來的溫濕度全是 0 或 0xFFFF原因I2C 地址寫錯或者上拉電阻沒接。SHT30 的 SDA 和 SCL 都需要 4.7k 到 10k 的上拉有些模塊自帶有些沒有。如果地址寫成 7 位而不是 8 位HAL 庫會發(fā)出錯誤的地址傳感器不應(yīng)答讀回來的緩沖區(qū)保持初始值。解決先用邏輯分析儀或示波器看 SDA 和 SCL 上有沒有波形地址字節(jié)是不是 0x88 或 0x8A。如果沒有波形檢查 I2C 外設(shè)時鐘和 GPIO 復用配置如果有波形但地址不對檢查地址定義。我一般會在初始化后調(diào)用HAL_I2C_IsDeviceReady確認傳感器在線。4.2 現(xiàn)象溫度正常濕度偶爾跳到 90% 以上原因讀數(shù)據(jù)太快讀到了上一次的舊值或者傳感器還沒完成測量。單次測量模式下發(fā)完命令必須等足夠時間高重復性要等 15ms中重復性 6ms低重復性 4ms。如果你只延時 5ms 就去讀高重復性模式下讀到的可能是上一次的數(shù)據(jù)。解決把延時改成 20ms 留足余量或者改用周期測量模式讓傳感器自己按節(jié)奏更新。另外讀回來的數(shù)據(jù)一定要做 CRC 校驗校驗不過就丟棄重讀不要直接把錯誤數(shù)據(jù)換算后輸出。4.3 現(xiàn)象CRC 校驗一直不過原因CRC 算法用錯或者數(shù)據(jù)字節(jié)順序搞反。SHT30 的 CRC 多項式是 0x31初始值 0xFF不是常見的 0x07 或 0x31 反轉(zhuǎn)。另外溫度和濕度的 CRC 是分別計算的不是一起算。解決用數(shù)據(jù)手冊里的示例數(shù)據(jù)驗證你的 CRC 函數(shù)。比如溫度原始值 0x61 0x9CCRC 應(yīng)該是 0x5E。如果你算出來不是這個值檢查多項式、初始值和字節(jié)順序。我一般會在調(diào)試時把原始字節(jié)和算出來的 CRC 通過串口打印出來和手冊對照。4.4 現(xiàn)象周期測量模式下數(shù)據(jù)不更新原因啟動周期測量后沒有等待第一次測量完成就去取數(shù)據(jù)或者取數(shù)據(jù)命令發(fā)錯了。周期測量啟動后傳感器需要至少一個測量周期才會更新數(shù)據(jù)寄存器10Hz 模式下就是 100ms。如果你啟動后立刻取讀到的可能是上電默認值。解決啟動周期測量后先延時 100ms 到 200ms再開始取數(shù)據(jù)。取數(shù)據(jù)命令是 0xE000不是 0xE0 單字節(jié)。另外周期測量模式下如果主機長時間不取數(shù)據(jù)傳感器會繼續(xù)測量但數(shù)據(jù)寄存器只保留最新值不會溢出。4.5 現(xiàn)象I2C 總線死鎖HAL 庫返回 HAL_BUSY原因SHT30 在測量期間如果主機復位或異??赡馨?SDA 拉低不放導致總線死鎖。HAL 庫的 I2C 函數(shù)在檢測到 BUSY 標志時會一直等超時后返回 HAL_BUSY但總線可能還是死的。解決在 I2C 初始化時加上總線恢復邏輯如果檢測到 SDA 被拉低就手動發(fā)送 9 個時鐘脈沖讓從機釋放總線。另外HAL 庫的 I2C 超時參數(shù)不要設(shè)太大100ms 足夠設(shè)太大反而會在死鎖時卡住整個系統(tǒng)。我一般會在HAL_I2C_Init之后加一段 GPIO 模擬時序的恢復代碼確??偩€干凈。5. 進階技巧用 DMA 加空閑中斷把 SHT30 的讀取徹底非阻塞化前面講的都是阻塞式讀取CPU 要等 I2C 傳輸完成。如果你的系統(tǒng)里 I2C 總線上掛了多個傳感器或者主循環(huán)任務(wù)很重阻塞式讀取會拖慢整體響應(yīng)。我后來在一個低功耗項目里把 SHT30 的讀取改成了 DMA 加空閑中斷的方式CPU 占用幾乎降到零。具體做法是用HAL_I2C_Master_Transmit_DMA發(fā)測量命令發(fā)完后啟動一個定時器延時 20ms 后觸發(fā) DMA 接收。接收完成回調(diào)里做 CRC 校驗和換算然后通過消息隊列把結(jié)果發(fā)給主任務(wù)。這樣主循環(huán)完全不用等 I2C可以去做別的事。/* DMA發(fā)送測量命令 */ HAL_I2C_Master_Transmit_DMA(hi2c1, SHT30_I2C_ADDR_8BIT, (uint8_t *)SHT30_CMD_MEASURE_HIGH, 2); /* 在發(fā)送完成回調(diào)里啟動定時器20ms后觸發(fā)接收 */ void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c) { if (hi2c hi2c1) { __HAL_TIM_SET_COUNTER(htim6, 0); HAL_TIM_Base_Start_IT(htim6); } } /* 定時器中斷里啟動DMA接收 */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim htim6) { HAL_TIM_Base_Stop_IT(htim6); HAL_I2C_Master_Receive_DMA(hi2c1, SHT30_I2C_ADDR_8BIT | 0x01, sht30_buf, 6); } } /* 接收完成回調(diào)里做校驗和換算 */ void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c) { if (hi2c hi2c1) { float temp, humi; if (SHT30_DataConvert(sht30_buf, temp, humi) HAL_OK) { /* 把結(jié)果放入隊列通知主任務(wù) */ SHT30_ResultQueue_Push(temp, humi); } } }邏輯說明發(fā)送用 DMA發(fā)送完成回調(diào)里啟動定時器定時器中斷里啟動接收 DMA接收完成回調(diào)里處理數(shù)據(jù)。整個過程 CPU 只在回調(diào)里執(zhí)行幾個指令其余時間都在休眠或處理其他任務(wù)。參數(shù)說明定時器 6 我配的是 1ms 周期中斷里先停定時器再啟動接收避免重復觸發(fā)。DMA 接收緩沖區(qū)sht30_buf要定義成全局或靜態(tài)不能在回調(diào)里被釋放。消息隊列用 FreeRTOS 的xQueueSendFromISR注意中斷優(yōu)先級要配成 FreeRTOS 可管理的中斷優(yōu)先級。這個方案我在一個電池供電的溫濕度記錄儀上跑過主控也是 STM32F103ZET6SHT30 每秒采集一次整機平均電流從阻塞式的 12mA 降到了 3mA 左右效果很明顯。不過 DMA 方式也有代價代碼復雜度上來了調(diào)試時如果 I2C 出錯錯誤回調(diào)里要記得重啟 DMA 和定時器否則整個采集鏈路就停了。從那以后我每次用 DMA 做 I2C 讀取都會在錯誤回調(diào)里加一個狀態(tài)機復位確保異常后能自動恢復。希望幫到你。本文還有配套的精品資源點擊獲取