
簡介本資源是一套基于STM32F10x系列微控制器的嵌入式綜合實踐項目面向嵌入式初學者與課程設計者解決溫濕度數據采集與本地可視化顯示的核心需求。項目完整實現DHT11/DHT22傳感器數據讀取、STM32 ADC或GPIO單總線驅動、7腳SH1106 OLEDI2C/SPI接口動態刷新顯示等功能覆蓋硬件連接、外設配置GPIO/ADC/I2C/USART/TIM、固件庫調用及Keil uVision5工程構建全流程。壓縮包共141個文件含37個頭文件h、34個源碼文件c——涵蓋STM32標準外設庫如stm32f10x_adc.c、stm32f10x_i2c.c、OLED驅動、傳感器通信協議解析及主應用邏輯另有編譯中間文件o/d/crf、調試配置dbgconf、工程配置uvprojx/uvoptx及自動化腳本keilkilll.bat結構清晰便于理解工程組織與編譯機制。已有3195人學習下載提供可直接編譯運行的完整代碼框架、README使用說明及典型硬件連接參考助讀者快速掌握傳感器MCU顯示終端的協同開發能力。1. 項目概述一個經典的嵌入式入門實戰如果你剛開始接觸STM32或者想找一個能串聯起GPIO、I2C通信、傳感器數據采集和顯示的綜合小項目來練手那么這個“STM32 溫濕度傳感器 OLED顯示”的組合絕對是你的不二之選。它不像點個LED燈那么簡單也不至于復雜到讓人望而卻步正好卡在能讓你充分動手、又能看到直觀成果的那個甜點上。簡單來說這個項目的核心就是讓STM32微控制器讀取溫濕度傳感器比如常見的DHT11的數據然后通過一個0.96寸的7腳OLED屏幕把這些數據實時、美觀地顯示出來。整個過程涉及了硬件引腳連接、通信協議驅動I2C或SPI取決于你的OLED模塊、傳感器數據協議解析以及最終的屏幕繪圖邏輯。完成它你不僅能獲得一個可以放在桌頭顯示環境溫濕度的小玩意兒更重要的是你能系統地走一遍嵌入式開發中“感知-處理-顯示”的完整鏈路這對理解任何更復雜的物聯網IoT設備都大有裨益。我之所以推薦這個項目是因為它的要素非常典型一個主控STM32一個傳感器輸入一個人機交互輸出。市面上大量的開發板教程和開源庫都支持這些模塊意味著你踩坑時能找到豐富的解決方案。接下來我會基于最常見的硬件組合STM32F103C8T6核心板、DHT11、SSD1306驅動的0.96寸OLED和HAL庫帶你從硬件連接到軟件實現完整地走一遍流程并分享那些教程里通常不會細說的“坑點”和調試技巧。2. 硬件選型與核心思路解析2.1 為什么是這些硬件STM32F103C8T6Blue Pill核心板這是性價比之王也是STM32生態中資源最豐富的一款。它基于ARM Cortex-M3內核擁有足夠的GPIO、定時器和通信接口USART、I2C、SPI來完成我們這個項目。其HAL庫和標準庫資料浩如煙海社區支持極好非常適合學習和原型開發。DHT11溫濕度傳感器這是一個單總線1-Wire數字傳感器。選擇它主要是因為其簡單、廉價且足夠用于室內環境監測。它直接輸出數字信號省去了STM32內部ADC模數轉換器的配置對于初學者更友好。不過要注意它的精度濕度±5%RH溫度±2℃和響應速度對于高要求場合可能不夠但對于學習項目完全足夠。0.96寸OLEDSSD1306驅動7腳OLED是“有機發光二極管”的簡稱。這種屏幕自發光對比度高顯示黑色時幾乎不耗電視覺效果好。7腳模塊通常支持I2C和SPI兩種通信模式通過模塊背面的電阻焊接選擇。I2C只需2根數據線SCL SDA節省IO口SPI速度更快。對于顯示溫濕度這種刷新率要求不高的場景I2C是更簡單、更通用的選擇也是本項目的推薦方式。2.2 整體通信架構設計項目的核心是通信協議。STM32作為大腦需要與兩個外設“對話”。與DHT11的“單線對話”DHT11采用單總線協議。這意味著數據發送和接收都通過同一根數據線DQ依靠嚴格的高低壓電平持續時間來區分數據位0或1和通信階段開始信號、響應信號。這要求STM32必須能夠精確地控制引腳輸出高低電平并更精確地讀取輸入電平的持續時間。通常我們會用一個通用GPIO引腳并配合微秒級延時函數來實現時序模擬。因為時序要求嚴格微秒級直接使用HAL庫的HAL_Delay()毫秒級是不行的需要自己實現或使用定時器。與OLED的“I2C對話”我們選擇I2C模式驅動OLED。I2C是一種同步、串行、多主從的通信總線只需要兩根線串行時鐘線SCL和串行數據線SDA。STM32作為主機MasterOLED屏幕上的SSD1306驅動芯片作為從機Slave。STM32通過I2C總線向SSD1306發送命令和數據控制其點亮哪些像素。HAL庫提供了完善的I2C發送/接收函數我們只需要關注如何組織這些命令和數據來顯示我們想要的字符或圖形。注意在連接硬件前務必確認你的OLED模塊是I2C模式。檢查模塊背面是否有標識或電阻焊點。通常將電阻焊接到標有“I2C”的焊盤上或者斷開“CS”引腳如果是SPI的片選信號的連接即可設置為I2C模式。2.3 硬件連接清單與原理下面是最簡潔的接線表。假設使用STM32F103C8T6的默認引腳功能STM32引腳連接至功能說明備注3.3VOLED VCC, DHT11 VCC電源正極務必接3.3V接5V可能燒毀OLEDGNDOLED GND, DHT11 GND電源地共地至關重要PB6OLED SCLI2C1時鐘線需在CubeMX中配置為I2C1_SCLPB7OLED SDAI2C1數據線需在CubeMX中配置為I2C1_SDAPA1DHT11 DATA單總線數據線可任選一個GPIO需配置為上拉輸入/推挽輸出接線原理與避坑指南電源OLED和DHT11都工作于3.3V。千萬不要將OLED的VCC接到開發板的5V引腳上極大概率會損壞屏幕。上拉電阻I2C總線SCL和SDA需要上拉電阻通常4.7kΩ或10kΩ到3.3V以確保總線空閑時為高電平。好消息是很多OLED模塊已經內置了這些上拉電阻。如果你的模塊沒有或者通信不穩定需要在STM32的PB6和PB7引腳上分別外接上拉電阻到3.3V。DHT11數據線DHT11的數據線DQ在空閑時需要保持高電平。雖然STM32的GPIO可以配置為內部上拉但為了信號穩定建議在PA1和3.3V之間外接一個4.7kΩ~10kΩ的上拉電阻。這是很多初學者忽略導致讀取失敗的原因。3. 軟件工程搭建與驅動層實現3.1 使用STM32CubeMX進行基礎配置STM32CubeMX是ST官方提供的圖形化配置工具能極大簡化引腳、時鐘和外設的初始化工作。創建項目與選型打開CubeMX選擇STM32F103C8Tx創建工程。系統核心SYS在“SYS”選項卡中將“Debug”改為“Serial Wire”。這非常重要否則燒錄一次程序后SWD調試接口可能被禁用導致無法再次下載程序。時鐘RCC在“RCC”選項卡中將高速外部時鐘HSE設置為“Crystal/Ceramic Resonator”。我們的核心板通常搭載了8MHz的外部晶振。時鐘樹配置進入“Clock Configuration”標簽頁。將輸入頻率設為8MHz然后通過PLL倍頻將系統時鐘SYSCLK設置為72MHz。這是F103系列在3.3V下的典型最高工作頻率性能足夠。GPIO配置找到PA1將其設置為“GPIO_Output”。我們先將其用作輸出發送開始信號給DHT11。實際上PA1需要在輸出和輸入模式間動態切換。這部分更精細的控制我們留在代碼中實現這里先設為輸出。I2C配置找到I2C1將模式設置為“I2C”。將PB6和PB7自動映射為I2C1_SCL和I2C1_SDA。在參數設置中I2C速度模式選擇“Standard Mode”100kHz即可。對于OLED顯示這個速度綽綽有余。生成代碼在“Project Manager”中設置好工程名稱、路徑、IDE如MDK-ARM V5然后生成代碼。CubeMX會生成一個完整的Keil工程或其他IDE工程其中main.c、i2c.c、gpio.c等文件的初始化代碼都已就緒。3.2 實現微秒級延時函數由于DHT11的時序要求精確到微秒級別而HAL庫的HAL_Delay()是基于系統滴答定時器Systick的毫秒級延時我們需要一個更精確的延時。通常有兩種方法方法一使用定時器精準但占用硬件資源配置一個基本定時器如TIM2使其每1微秒產生一次更新中斷或計數。但為了一個延時函數占用一個定時器對于簡單項目有些浪費。方法二使用空指令循環NOP進行近似延時簡單常用這是更常見的做法。通過分析在72MHz系統時鐘下執行一條空指令__NOP()所需的時間約1/72MHz ≈ 13.9納秒我們可以編寫一個循環來“消耗”時間。// 在 main.c 或 自定義的 delay.c 文件中 /** * brief 微秒級延時函數近似值 * param us: 需要延時的微秒數 * note 基于72MHz系統時鐘通過循環空指令實現。不同優化等級會影響精度僅適用于時序要求不極端嚴格的場合如DHT11。 */ void delay_us(uint32_t us) { // 此參數需要根據實際芯片主頻進行校準 // 72MHz下粗略經驗值循環一次約消耗 0.125us (8個NOP/us) // 因此 us * 8 作為循環次數。你需要通過示波器或邏輯分析儀精確校準這個系數。 uint32_t delay us * 8; while(delay--) { __NOP(); // 執行一條空操作指令 } }實操心得這個delay_us函數的參數us * 8中的8是一個經驗系數它會因編譯器優化等級-O0, -O1, -O2的不同而產生巨大差異。最靠譜的校準方法是寫一段代碼讓一個GPIO引腳每隔100us翻轉一次然后用邏輯分析儀或示波器測量實際周期反過來調整這個系數。對于DHT11誤差在幾十微秒內通常可以接受。3.3 編寫DHT11單總線驅動DHT11的通信流程分為三步主機STM32發送開始信號 - DHT11響應 - DHT11發送40位數據。我們需要一個GPIO引腳在輸出模式發送開始信號和輸入模式讀取響應和數據之間切換。// dht11.h #ifndef __DHT11_H #define __DHT11_H #include main.h // 定義DHT11數據線連接的GPIO端口和引腳 #define DHT11_GPIO_PORT GPIOA #define DHT11_GPIO_PIN GPIO_PIN_1 // 函數聲明 void DHT11_GPIO_Init(void); uint8_t DHT11_Read_Data(uint8_t *temperature, uint8_t *humidity); #endif// dht11.c #include dht11.h #include delay.h // 包含你自己實現的 delay_us 函數 extern TIM_HandleTypeDef htim2; // 如果你用定時器實現延時需要聲明 /** * brief 設置DHT11數據線引腳為輸出模式推挽輸出 */ static void DHT11_GPIO_SetOutput(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DHT11_GPIO_PIN; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; // 推挽輸出 GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStruct); } /** * brief 設置DHT11數據線引腳為輸入模式上拉輸入 */ static void DHT11_GPIO_SetInput(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DHT11_GPIO_PIN; GPIO_InitStruct.Mode GPIO_MODE_INPUT; // 輸入模式 GPIO_InitStruct.Pull GPIO_PULLUP; // 內部上拉替代外部電阻 HAL_GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStruct); } /** * brief 主機發送開始信號 * 拉低至少18ms然后拉高20-40us等待DHT11響應。 */ static void DHT11_Start(void) { DHT11_GPIO_SetOutput(); HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_RESET); // 拉低 delay_us(18000); // 拉低18ms DHT11手冊要求至少18ms HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_SET); // 拉高 delay_us(30); // 拉高20-40us DHT11_GPIO_SetInput(); // 切換為輸入模式準備讀取響應 } /** * brief 等待DHT11的響應信號 * retval 0: 響應成功 1: 響應超時失敗 */ static uint8_t DHT11_Wait_Response(void) { uint32_t timeout 10000; // 超時計數器防止死循環 // 等待DHT11將總線拉低響應信號開始 while(HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) { if(--timeout 0) return 1; delay_us(1); } timeout 10000; // 等待DHT11拉低結束約80us while(HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_RESET) { if(--timeout 0) return 1; delay_us(1); } timeout 10000; // 等待DHT11拉高結束約80us while(HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) { if(--timeout 0) return 1; delay_us(1); } return 0; // 響應成功 } /** * brief 從單總線上讀取一個比特bit * retval 讀取到的比特值0或1 * note DHT11發送0拉低50us后拉高26-28us * DHT11發送1拉低50us后拉高70us * 通過判斷高電平持續時間來區分0和1。 */ static uint8_t DHT11_Read_Bit(void) { uint32_t timeout 10000; uint8_t bitval 0; // 等待低電平開始每個比特都以50us低電平開始 while(HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) { if(--timeout 0) return 0xFF; // 錯誤 delay_us(1); } delay_us(40); // 跳過開始的50us低電平等待到高電平階段 // 檢測高電平持續時間 if(HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) { // 如果是高電平延時30us后再檢測 delay_us(30); // 如果30us后還是高電平說明高電平持續時間長是比特1 if(HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) bitval 1; else bitval 0; // 否則是比特0 } // 等待這個比特的高電平結束 while(HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) { delay_us(1); } return bitval; } /** * brief 從單總線上讀取一個字節byte * retval 讀取到的字節數據 */ static uint8_t DHT11_Read_Byte(void) { uint8_t i, data 0; for(i0; i8; i) { data 1; // 左移一位為新的比特騰出位置 data | DHT11_Read_Bit(); // 讀取比特并拼接到data上 } return data; } /** * brief 讀取DHT11的溫濕度數據主函數 * param temperature: 指向存儲溫度值的變量整數部分單位℃ * param humidity: 指向存儲濕度值的變量整數部分單位%RH * retval 0: 讀取成功 1: 讀取失敗校驗和錯誤或超時 */ uint8_t DHT11_Read_Data(uint8_t *temperature, uint8_t *humidity) { uint8_t buf[5] {0}; uint8_t i, checksum; DHT11_Start(); if(DHT11_Wait_Response() ! 0) return 1; // 響應失敗 // 連續讀取5個字節濕度整數、濕度小數、溫度整數、溫度小數、校驗和 for(i0; i5; i) { buf[i] DHT11_Read_Byte(); } // 切換回輸出模式釋放總線拉高 DHT11_GPIO_SetOutput(); HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_SET); // 計算校驗和前四個字節相加的低8位 checksum buf[0] buf[1] buf[2] buf[3]; if(checksum ! buf[4]) return 1; // 校驗和錯誤 // DHT11小數部分通常為0我們只取整數部分 *humidity buf[0]; *temperature buf[2]; return 0; // 讀取成功 }關鍵點解析與避坑模式切換DHT11_GPIO_SetOutput和DHT11_GPIO_SetInput是關鍵。必須在發送開始信號前設為輸出在等待響應和讀取數據前設為輸入。時序精度delay_us的準確性直接影響讀取成功率。如果讀取經常失敗第一個要懷疑的就是延時函數。超時處理DHT11_Wait_Response和DHT11_Read_Bit函數中的while循環都加入了超時判斷timeout。這是極其重要的防卡死機制。沒有它一旦DHT11損壞或接觸不良程序就會永遠卡在等待循環里。校驗和DHT11發送的第5個字節是前4個字節的和。每次讀取后必須驗證這是判斷數據是否被干擾或讀取過程是否出錯的重要依據。3.4 編寫OLEDSSD1306I2C驅動OLED驅動本質上就是通過I2C總線向SSD1306芯片發送兩類東西命令控制屏幕開關、對比度、掃描方式等和數據要顯示的像素點陣信息。SSD1306的I2C地址通常是0x78寫地址或0x7A讀地址很少用。發送一幀數據的格式是[I2C地址字節] [控制字節] [數據字節1] [數據字節2] ...。其中控制字節決定了后續字節是命令還是數據。// oled.h #ifndef __OLED_H #define __OLED_H #include main.h #include i2c.h // 使用HAL I2C函數 #define OLED_I2C_ADDR 0x78 // SSD1306的I2C寫地址 #define OLED_WIDTH 128 #define OLED_HEIGHT 64 // 函數聲明 void OLED_Init(void); void OLED_Clear(void); void OLED_ShowChar(uint8_t x, uint8_t y, char chr, uint8_t size); void OLED_ShowString(uint8_t x, uint8_t y, char *str, uint8_t size); void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size); void OLED_Refresh(void); // 刷新整個顯存到屏幕 #endif// oled.c #include oled.h #include font.h // 需要包含字庫數組例如ASCII字符的8x16點陣 // OLED顯存共128*64/8 1024字節。每一位代表一個像素點1亮0滅。 static uint8_t OLED_GRAM[128][8]; // 二維數組方便按頁8像素高為一頁操作 /** * brief 通過I2C向OLED發送一個命令 * param cmd: 要發送的命令字節 */ static void OLED_Write_Cmd(uint8_t cmd) { uint8_t buf[2] {0x00, cmd}; // 控制字節0x00表示后續是命令 HAL_I2C_Master_Transmit(hi2c1, OLED_I2C_ADDR, buf, 2, 100); } /** * brief 通過I2C向OLED發送數據 * param data: 要發送的數據字節數組 * param len: 數據長度 */ static void OLED_Write_Data(uint8_t *data, uint16_t len) { // 方法先發送一個控制字節0x40然后連續發送數據 HAL_I2C_Mem_Write(hi2c1, OLED_I2C_ADDR, 0x40, I2C_MEMADD_SIZE_8BIT, data, len, 100); } /** * brief OLED初始化序列 * 按照SSD1306數據手冊的推薦初始化流程設置顯示開關、對比度、掃描方向等。 */ void OLED_Init(void) { HAL_Delay(100); // 上電后等待OLED穩定 OLED_Write_Cmd(0xAE); // 關閉顯示 OLED_Write_Cmd(0xD5); // 設置顯示時鐘分頻比/振蕩器頻率 OLED_Write_Cmd(0x80); OLED_Write_Cmd(0xA8); // 設置多路復用率 (64-1) OLED_Write_Cmd(0x3F); OLED_Write_Cmd(0xD3); // 設置顯示偏移 OLED_Write_Cmd(0x00); OLED_Write_Cmd(0x40); // 設置顯示起始行 OLED_Write_Cmd(0x8D); // 電荷泵設置 OLED_Write_Cmd(0x14); // 開啟電荷泵 OLED_Write_Cmd(0x20); // 設置內存地址模式 OLED_Write_Cmd(0x00); // 水平地址模式 OLED_Write_Cmd(0xA1); // 段重映射設置 (0xA1左右反置0xA0正常) OLED_Write_Cmd(0xC8); // 掃描方向設置 (0xC8上下反置0xC0正常) OLED_Write_Cmd(0xDA); // 設置COM引腳硬件配置 OLED_Write_Cmd(0x12); OLED_Write_Cmd(0x81); // 設置對比度控制 OLED_Write_Cmd(0xCF); OLED_Write_Cmd(0xD9); // 設置預充電周期 OLED_Write_Cmd(0xF1); OLED_Write_Cmd(0xDB); // 設置VCOMH電壓倍率 OLED_Write_Cmd(0x40); OLED_Write_Cmd(0xA4); // 設置整個顯示開啟/關閉 OLED_Write_Cmd(0xA6); // 設置正常/反色顯示 (A6正常A7反色) OLED_Write_Cmd(0xAF); // 開啟顯示 OLED_Clear(); // 清屏 OLED_Refresh(); // 將清空后的顯存刷新到屏幕 } /** * brief 清空OLED顯存將所有像素點置0 */ void OLED_Clear(void) { uint8_t i, j; for(i0; i128; i) for(j0; j8; j) OLED_GRAM[i][j] 0x00; } /** * brief 設置光標位置像素級 * param x: 列坐標范圍0~127 * param y: 頁坐標每頁8行范圍0~7 */ static void OLED_Set_Pos(uint8_t x, uint8_t y) { OLED_Write_Cmd(0xB0 y); // 設置頁地址 OLED_Write_Cmd(((x 0xF0) 4) | 0x10); // 設置列地址高4位 OLED_Write_Cmd(x 0x0F); // 設置列地址低4位 } /** * brief 將顯存OLED_GRAM的內容全部刷新到OLED屏幕 */ void OLED_Refresh(void) { uint8_t i, j; for(j0; j8; j) // 遍歷8頁 { OLED_Set_Pos(0, j); // 設置到第j頁第0列 OLED_Write_Data(OLED_GRAM[0] j * 128, 128); // 發送該頁的128字節數據 } } /** * brief 在指定位置顯示一個ASCII字符 * param x: 起始列 (0~127) * param y: 起始頁 (0~7每頁8行) * param chr: 要顯示的字符 * param size: 字體大小 (1: 8x16, 2: 16x32需要對應字庫) */ void OLED_ShowChar(uint8_t x, uint8_t y, char chr, uint8_t size) { uint8_t i, j, temp; uint8_t *pfont; // 指向字庫數據的指針 // 根據字符和字體大小從字庫數組中獲取點陣數據首地址 // 假設字庫數組名為 Font8x16每個字符16字節 if(size 1) { pfont (uint8_t*)Font8x16[(chr - ) * 16]; // ASCII字庫從空格開始 for(i0; i16; i) // 字符高度16像素 { temp pfont[i]; for(j0; j8; j) // 字符寬度8像素 { if(temp 0x80) // 判斷最高位是否為1 OLED_GRAM[x j][y i/8] | (1 (i%8)); // 置1點亮像素 else OLED_GRAM[x j][y i/8] ~(1 (i%8)); // 置0熄滅像素 temp 1; } } } // 可以擴展其他字體大小... } /** * brief 在指定位置顯示字符串 */ void OLED_ShowString(uint8_t x, uint8_t y, char *str, uint8_t size) { while(*str ! \0) { OLED_ShowChar(x, y, *str, size); x 8; // 8x16字體每個字符占8列 str; if(x 120) // 換行判斷 { x 0; y; } } } /** * brief 在指定位置顯示數字 */ void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size) { char str[12]; sprintf(str, %*lu, len, num); // 格式化數字為固定長度字符串 OLED_ShowString(x, y, str, size); }驅動層核心邏輯顯存GRAM我們在STM32的內存中開辟一個二維數組OLED_GRAM[128][8]用來模擬OLED屏幕的顯存。這個數組的每一個bit對應屏幕上的一個像素點。我們所有畫點、畫線、顯示字符的操作都是先修改這個數組。局部刷新 vs 全局刷新OLED_ShowChar等函數只修改OLED_GRAM并不立即發送給OLED。只有調用OLED_Refresh()時才會將整個OLED_GRAM通過I2C發送到屏幕。這種方式避免了頻繁的I2C通信提高了效率。在溫濕度顯示中我們可以每秒調用一次OLED_Refresh來更新數據。字庫你需要一個font.h文件里面包含字模數組。例如Font8x16[]它是一個二維數組存儲了從空格開始的所有ASCII字符的8x16點陣數據。你可以用PCtoLCD2002等取模軟件生成。4. 應用層邏輯整合與主程序實現驅動層完成后應用層邏輯就變得非常清晰了。我們只需要在主循環中定期讀取傳感器數據然后格式化并顯示到OLED上。4.1 主程序流程圖與邏輯初始化系統時鐘、GPIO、I2C、OLED、DHT11GPIO。清屏并顯示初始界面例如顯示“Temp:”和“Humi:”的標簽。進入主循環延時為了避免頻繁讀取導致DHT11發熱或響應不及時每次讀取間隔至少2秒。使用HAL_Delay(2000)。讀取數據調用DHT11_Read_Data(temp, humi)。錯誤處理如果返回值為1失敗可以在OLED上顯示“Error”或保留上一次數據。更新顯示將讀取到的temp和humi整數轉換成字符串調用OLED_ShowNum或OLED_ShowString函數在指定位置刷新數值。刷新屏幕調用OLED_Refresh()將更新后的顯存發送到OLED。4.2 主函數代碼示例// main.c #include main.h #include i2c.h #include gpio.h #include dht11.h #include oled.h I2C_HandleTypeDef hi2c1; int main(void) { uint8_t temperature 0; uint8_t humidity 0; uint8_t dht11_status 0; char disp_str[20]; HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_I2C1_Init(); // 初始化外設 OLED_Init(); DHT11_GPIO_Init(); // 初始化DHT11的GPIO設置為輸出模式并拉高 // 顯示靜態標題 OLED_Clear(); OLED_ShowString(0, 0, STM32 DHT11 Demo, 1); OLED_ShowString(0, 2, Temp:, 1); // 第2頁行16-31像素顯示 OLED_ShowString(0, 4, Humi:, 1); // 第4頁行32-47像素顯示 OLED_Refresh(); while (1) { HAL_Delay(2000); // 每2秒讀取一次 dht11_status DHT11_Read_Data(temperature, humidity); if(dht11_status 0) { // 讀取成功更新溫度顯示 sprintf(disp_str, %d C, temperature); OLED_ShowString(40, 2, disp_str, 1); // 在“Temp:”后面顯示 // 更新濕度顯示 sprintf(disp_str, %d %%, humidity); // %% 表示一個%字符 OLED_ShowString(40, 4, disp_str, 1); // 在屏幕底部顯示狀態 OLED_ShowString(0, 6, Status: OK , 1); } else { // 讀取失敗 OLED_ShowString(0, 6, Status: ERROR, 1); // 可以選擇不清除之前的溫濕度數據 } // 刷新整個屏幕將GRAM數據發送到OLED OLED_Refresh(); } }4.3 顯示效果優化與擴展基礎的數值顯示可能有些單調我們可以進行一些優化添加小數顯示DHT11雖然小數位通常是0但我們可以預留位置。或者使用DHT22AM2302傳感器它直接輸出帶一位小數的數據。繪制圖標在“Temp:”和“Humi:”文字前可以繪制一個溫度計和小水滴的圖標增加美觀度。這需要你事先制作好圖標的點陣數組取模。顯示歷史曲線利用OLED的128x64像素可以繪制一個簡單的溫濕度歷史曲線圖。在GRAM中開辟一個區域作為曲線繪制區每次讀取新數據后將舊數據點向左移動并在最右側繪制新的數據點。添加單位如上述代碼所示顯示“C”和“%”符號。低功耗優化如果不要求實時顯示可以讓STM32進入休眠模式Stop Mode定時喚醒讀取傳感器并刷新屏幕可以極大降低功耗。5. 調試技巧與常見問題排查實錄即使按照步驟操作第一次成功也往往伴隨著一些調試過程。以下是幾個最常見的“坑”和解決方法。5.1 DHT11讀取始終失敗這是最常見的問題癥狀是DHT11_Read_Data總是返回1超時或校驗和錯誤。檢查1電源和接線電壓確保DHT11和OLED的VCC接的是3.3V不是5V。上拉電阻DHT11的數據線DQ必須接一個4.7kΩ - 10kΩ的上拉電阻到3.3V。STM32的內部上拉電阻約40kΩ可能強度不夠導致高電平不穩定。共地確保STM32、DHT11、OLED三者的GND引腳連接在一起。檢查2延時函數精度這是頭號嫌疑犯。用邏輯分析儀或示波器檢查DHT11_Start()函數中產生的開始信號低電平18ms高電平30us是否準確。如果沒有儀器可以嘗試調整delay_us函數中的系數。將us * 8改為us * 6或us * 10試試。不同主頻和優化等級下這個系數差異很大。嘗試關閉編譯器的優化在Keil中設置為-O0看看是否能成功。如果關閉優化后成功說明就是延時精度問題。檢查3GPIO模式切換時序在DHT11_Start()函數末尾切換到輸入模式后是否立即進入了等待響應的循環中間不應有大的延時。確保在讀取完數據后將GPIO設置回輸出模式并拉高釋放總線。檢查4傳感器本身DHT11是3.3V器件但有些模塊兼容5V。如果接5V數據線輸出高電平可能是5V長時間可能損壞STM32的3.3V GPIO。建議始終在3.3V下工作。換個新的DHT11試試。5.2 OLED屏幕不亮或顯示亂碼檢查1電源和I2C地址電源再次確認VCC接3.3V。I2C地址嘗試將OLED_I2C_ADDR從0x78改為0x7A。有些模塊的地址可能是0x7A。上拉電阻如果屏幕完全無反應檢查SCL和SDA線上是否有上拉電阻4.7kΩ到3.3V。如果沒有請加上。檢查2初始化序列OLED初始化命令序列很長且順序有要求。確保OLED_Init()函數中的命令一個不少且0xAE關顯示在開頭0xAF開顯示在最后。初始化后務必調用OLED_Clear()和OLED_Refresh()。檢查3I2C通信在OLED_Write_Cmd和OLED_Write_Data函數中檢查HAL_I2C_Master_Transmit的返回值。如果不是HAL_OK說明I2C通信失敗。用邏輯分析儀連接SCL和SDA看初始化階段是否有波形產生。如果沒有檢查CubeMX中I2C的配置是否正確引腳是否被其他功能占用。檢查4顯存與刷新顯示亂碼通常是GRAM數據錯誤或刷新位置不對。確保OLED_GRAM數組大小正確定義為[128][8]。在OLED_Refresh()函數中OLED_Set_Pos(0, j)和OLED_Write_Data(OLED_GRAM[0] j * 128, 128)這兩行是關鍵它確保了將GRAM中每一頁的128字節數據發送到屏幕對應的頁地址。5.3 顯示內容錯位或重疊坐標計算OLED的坐標體系是以“頁”為單位。一頁是8行像素。OLED_ShowChar(x, y, chr, size)中的y是頁坐標0~7x是列坐標0~127。字庫匹配你使用的Font8x16字庫必須是8像素寬、16像素高的。如果你顯示函數里按8x16處理但字庫數據是6x8的肯定會亂。刷新前未清除舊內容如果你在同一個位置顯示變化的數字如從“25”變成“26”新數字“26”只有2個字符如果上次的“25”也是2個字符那沒問題。但如果從“9”變成“10”新內容“10”會覆蓋掉“9”和它后面一個字符的位置。如果后面那個位置原本有內容比如單位“C”就會被覆蓋。解決方法有兩種1) 在顯示新數字前先用空格覆蓋舊數字的整個區域2) 每次更新都重繪整個顯示區域清屏再畫雖然效率低但簡單可靠。5.4 項目穩定性與抗干擾建議增加軟件濾波對于溫濕度數據可以連續讀取3-5次然后取中值或平均值能有效濾除偶然的跳動。完善錯誤處理主循環中如果DHT11連續多次讀取失敗可以嘗試重新初始化其GPIO或者讓系統軟復位。使用看門狗啟用STM32的獨立看門狗IWDG防止程序跑飛。在主循環中定期“喂狗”。如果因DHT11時序卡死等原因導致程序死循環看門狗會復位系統。電源去耦在STM32的3.3V和GND之間靠近芯片引腳處并聯一個100nF和一個10uF的電容可以有效濾除電源噪聲提高系統穩定性。通過以上步驟你應該能夠成功搭建并運行這個STM32溫濕度監測顯示系統。這個項目雖然小但“麻雀雖小五臟俱全”它涵蓋的硬件接口、通信協議、驅動編寫和系統整合思想是嵌入式開發中最核心的部分。掌握了它你就為后續學習更復雜的傳感器、顯示屏、無線通信等打下了堅實的基礎。本文還有配套的精品資源點擊獲取