
简介这套基于STM32的温湿度传感器与OLED屏显示项目文件专为嵌入式开发者和电子爱好者准备适合课程设计、毕业设计以及日常入门实践。项目以STM32F103C8为主控通过DHT11采集环境温湿度并实时显示在OLED屏上适合仓库、机房、家居等需要实时监测温度湿度的场景。压缩包共包含75个文件其中32个头文件、30个C源文件辅以STM32多型号启动文件、Keil工程文件以及清理脚本整体体积仅306KB结构十分紧凑。代码目录按Hardware、System、User、Library、Start等模块划分DHT11驱动、OLED驱动、延时函数一应俱全并包含完整标准外设库便于直接调用和二次开发。已有7934人浏览学习对于想快速掌握STM32传感器驱动或需要OLED显示方案参考的开发者这套资源提供了可直接编译运行的完整工程模板尤其适合基于Keil uVision5的嵌入式工程实践。 做课设或者刚接触嵌入式开发的朋友一定都经历过这种窘境开发板买回来跑个流水灯、点个按键都还好但一旦要把传感器数据读出来、再显示到屏幕上就感觉知识体系突然断了层。GPIO会了、串口会了、定时器也会了可真要凑一个采集-处理-显示的完整链路却不知从哪下手。这次我分享的这个STM32温湿度传感器OLED屏显示项目就是专门解决这个问题的。它以STM32F103系列为核心外接DHT11温湿度传感器采集环境数据通过0.96寸OLED屏实时显示温度和湿度数值。项目麻雀虽小五脏俱全单总线协议解析、I2C通信、SSD1306驱动、系统时钟配置、代码模块化设计全都涉及特别适合拿来打通从零搭建一个完整STM32小项目的任督二脉。先说结论这个项目做完你对STM32的理解会上一个台阶因为你不再只是调用库函数点亮LED而是真正在跟芯片外设打交道理解数据是怎么从传感器经过引脚、寄存器、协议解析最终变成屏幕上的一串字符。1. 硬件选型背后的逻辑为什么是F103、DHT11和0.96寸OLED做嵌入式项目选型永远是第一步也最容易被新手忽略。很多人是手里有什么板子就用什么板子然后发现例程对不上、引脚对不上、库函数版本对不上各种折腾。我这里说说为什么这套组合是入门的神级搭配。1.1 STM32F103C8T6为什么是它STM32F103C8T6这颗芯片在开发板市场几乎是人手一片的存在它属于STM32F1系列Cortex-M3内核主频72MHzFlash 64KBSRAM 20KB。从资源上看并不算充裕但对于温湿度采集OLED显示这种应用场景性能绰绰有余。选择它还有两个实际考虑。第一资料极其丰富。市面上关于F103的教程、例程、答疑帖多到看不完哪怕你用的是国产替代芯片也几乎100%兼容标准库和HAL库的用法。第二这颗芯片的引脚足够布局合理PA、PB、PC端口都有引出方便外接各种传感器模块不用像某些小封装芯片那样为了引脚复用绞尽脑汁。1.2 DHT11与DHT22的选择题DHT11的精度是±2℃、±5%RH测量范围0-50℃、20-90%RH。说实话这个精度在工业场景确实不够看但在室内环境监测、智能家居演示、课设展示这些场景下完全够用。DHT22也叫AM2302则是进阶选择精度±0.5℃、±2%RH分辨率能到0.1价格大概是DHT11的三到五倍。我的建议是如果只是做演示项目、验证流程就用DHT11因为它的时序要求宽泛一些调试起来容错率高如果是要做数据采集记录这种稍微正经一点的应用可以直接上DHT22代码改动量其实很小因为两者都是单总线协议只是时序参数略有差异。1.3 OLED屏I2C版本还是SPI版本0.96寸OLED屏有两种接口版本I2C和SPI。I2C版只需要4根线VCC、GND、SCL、SDA接线简单到不能再简单SPI版需要7根线但刷新速度更快。我强烈建议入门阶段选I2C版本理由就一条少接线就少出错。你在面包板上插错一根线的代价可能是一个下午的排查时间。而且I2C的驱动代码写起来更简洁逻辑更清晰方便理解协议本身的工作方式。至于刷新率对于显示温湿度这种低频变化的数据I2C的那点速率绰绰有余。另外注意一个坑很多OLED模块板上已经有了上拉电阻不需要你额外再挂上拉。但如果你用的是那种纯裸屏自己搭电路的玩法SCL和SDA线上必须加上4.7kΩ左右的上拉电阻到VCC否则I2C通信会出现随机性失败后面我会具体讲。2. 从DHT11时序图开始把读温湿度拆解成代码2.1 单总线协议到底是什么DHT11用的是单总线One-Wire协议名字叫单总线是因为它数据通信只靠一根线完成双向传输都在DQ引脚上进行主机和传感器之间的数据交换通过特定的时序信号完成。你可以把单总线想象成一条只有一根车道的公路双向通行靠的是约定好的规则——什么时候谁发、发多快、发多久都有一套严格的时间窗口。DHT11的数据格式是40位8位湿度整数部分、8位湿度小数部分、8位温度整数部分、8位温度小数部分、8位校验和。以00110101 00000000 00110000 00000000 01000101为例湿度整数0x3553%温度整数0x3048℃校验和前四个字节的和等于0x45说明数据有效。2.2 起始信号主机的敲门砖通信的发起方永远是主机STM32传感器不会主动说话。主机要先把数据引脚拉低至少18ms再释放并拉高20-40μs这个过程就是起始信号相当于告诉DHT11我要读数据了你准备好。DHT11收到起始信号后会先拉低80μs响应再拉高80μs然后开始逐位输出40位数据。主机的代码逻辑是拉低引脚→延时20ms→拉高引脚→等待DHT11响应拉低→等待DHT11拉高→开始读位。// DHT11起始信号 DHT11_GPIO_Mode_Output(); // 设置为输出模式 DHT11_DQ_LOW(); delay_ms(20); // 拉低至少18ms DHT11_DQ_HIGH(); delay_us(30); // 拉高20-40us DHT11_GPIO_Mode_Input(); // 切换为输入模式准备接收数据这里有个非常关键的细节引脚方向切换。因为DHT11是单总线同一根线既要做输出又要做输入所以必须在使用前切换GPIO模式。在标准库中用GPIO_InitTypeDef重新配置即可在HAL库中则要调用HAL_GPIO_Init或直接操作寄存器。2.3 读0和读1一切都在时间窗口里DHT11输出每一位数据时都会先拉低50μs然后拉高。关键在于拉高的持续时间如果是0拉高持续26-28μs如果是1拉高持续70μs左右。所以读位的代码思路就变成了等引脚变高→开始计时→读取引脚状态超过某个阈值就判为1否则判为0。常见的做法是用循环等待加计数也可以用定时器输入捕获或者外部中断来实现但入门阶段最直接的方式就是延时采样uint8_t DHT11_ReadByte(void) { uint8_t byte 0; for (uint8_t i 0; i 8; i) { while (DHT11_DQ_READ() 0); // 等待50us低电平结束 delay_us(40); // 延时到高电平中间位置 byte 1; if (DHT11_DQ_READ() 1) // 此时引脚仍为高 - 是1 { byte | 0x01; } while (DHT11_DQ_READ() 1); // 等待高电平结束进入下一位 } return byte; }这个延时40μs后再采样的思路是关键的时序技巧因为低电平总是50μs高电平在26-70μs之间从低电平结束开始往后数40μs如果是1那么此刻引脚还是高电平如果是0引脚已经变低了完美区分。2.4 校验和别偷懒一定要写读完40位数据后把前四个字节相加与第五个字节比较相等说明传输正确。这项工作看似多写几行代码但能帮你省下大量调试时间。我在实际测试中就遇到过传感器数据偶尔跳变的情况没有校验和的保护屏幕上直接显示-999或者几百度的离谱数值有了校验和就能直接丢弃错误帧显示数据错误或保持上一次的正确值。3. OLED屏的I2C驱动把数据变成能看懂的字符3.1 I2C通信在OLED场景下的简化理解I2C是两根线的通信协议SCL时钟线和SDA数据线。OLED屏幕上的SSD1306驱动芯片扮演的是从机角色MCU是主机。主机通过产生时钟信号在SCL为高电平时把SDA上的数据打入从机。数据是一帧一帧的起始条件SCL高时SDA拉低、从机地址字节0x78写/0x7A读7位地址0x3C左移一位、控制字节告诉SSD1306接下来是命令还是数据、然后才是真正要发送的内容。3.2 SSD1306的初始化序列SSD1306芯片上电后需要发送一串初始化命令才能进入可显示状态。这一串命令包括关闭显示、设置显示时钟分频、设置multiplex ratio、设置显示偏移、启动内部DC-DC升压、设置列地址和页地址、反色控制等。static void OLED_Init_Commands(void) { // 初始化命令序列按顺序发送 static const uint8_t init_cmds[] { 0xAE, // 关闭显示 0xD5, 0x80, // 显示时钟分频 0xA8, 0x3F, // 驱动路数128x64对应1/64 0xD3, 0x00, // 显示偏移无偏移 0x40, // 起始行0 0x8D, 0x14, // 开启电荷泵升压 0x20, 0x00, // 内存寻址模式水平寻址 0xA1, // 段重映射 0xC8, // 扫描方向 0xDA, 0x12, // COM引脚配置 0x81, 0xCF, // 显示对比度 0xD9, 0xF1, // 预充电周期 0xDB, 0x40, // VCOMH电压 0xA4, // 整体显示跟随RAM 0xA6, // 正常显示非反显 0xAF // 开启显示 }; // 逐条发送... }这里有个重要概念SSD1306按照页来组织显示内存0.96寸OLED分辨率128x64它被分成8页Page每页是128列x8行像素。每页的每一列对应一个字节字节的8个bit对应这一列在页内从上到下的8个像素点。这就是为什么你写入一字节数据屏幕上会点亮一列8个点。3.3 从点阵到字符显示Hello World的完整链路要在OLED上显示字符需要把字符转换成点阵数据。英文字符常用的取模方式是8x16或者6x8先按列扫描、每列一字节。比如字符A的8x16点阵就是16个字节每两字节对应一列的上8点和下8点。我在工程里维护一张OLED_F8x16[][16]字库表用于存放ASCII码从0x20到0x7F的点阵数据。显示函数的核心逻辑是void OLED_ShowChar(uint8_t page, uint8_t col, uint8_t ch) { // 计算字库偏移 const uint8_t* font OLED_F8x16[(ch - 0x20) * 2]; // 先写字符上半部分到当前页 OLED_SetPos(page, col); for (uint8_t i 0; i 8; i) { OLED_WriteData(font[i]); } // 再写下半部分到下一页 OLED_SetPos(page 1, col); font 8; for (uint8_t i 0; i 8; i) { OLED_WriteData(font[i]); } }行坐标page的取值范围是0-7列坐标col的取值范围是0-127。8x16字体下一屏最多显示8行16列字符128列/8像素宽16字符8页/2页高4行。这个布局关系一定要心里有数不然你怎么调都显示不全。3.4 浮点数显示printf改造的小技巧温湿度是带小数的直接在OLED上显示浮点数最省事的方式是改造fputc函数让printf重定向到OLED。但更稳定的做法是自己写格式化函数把浮点拆成整数部分和小数部分分别显示void OLED_ShowFloat(uint8_t page, uint8_t col, float value, uint8_t decimal_places) { int int_part (int)value; int dec_part (int)((value - int_part) * pow(10, decimal_places)); // 处理负数、整数部分、小数点、小数部分... OLED_ShowInt(page, col, int_part); OLED_ShowChar(page, col 8 * LenOfInt, .); OLED_ShowInt(page, col 8 * (LenOfInt 1), dec_part); }注意pow函数在嵌入式环境里可能引入较大代码体积实际用循环乘10即可。另外显示数值前一定要清掉上一次显示留下的残影可以调OLED_ClearArea(page, col, len)把指定区域清零后再显示新值。4. 工程搭建步骤从新建工程到点亮屏幕的完整过程4.1 库函数选择标准库还是HAL库这是每个STM32新手都会纠结的问题。我的建议很直接如果你用的是F103且以学习原理为目标选标准库。标准库的代码直接反映了寄存器的操作方式比如GPIO_InitTypeDef里的每个成员对应一个寄存器字段看代码就知道硬件是怎么工作的。而且目前网上的老教程、课程比如大家熟知的江协科技系列大多基于标准库跟着学不容易卡壳。HAL库则更适合做产品开发或者用CubeMX做图形化配置但抽象层较多一旦出问题底层逻辑不好排查。等你能用标准库做几个项目、对寄存器有感觉之后再切HAL库会顺畅很多。4.2 新建工程的完整步骤工程模板搭建看起来枯燥但非常重要。一个干净的工程结构能让你少走很多弯路。推荐的文件组织结构如下Project/ ├── Core/ // 存放主函数、中断处理 │ └── main.c ├── Hardware/ // 外设驱动自己写的 │ ├── dht11.c/h │ ├── oled.c/h │ └── delay.c/h ├── Libraries/ // 固件库文件 │ ├── CMSIS/ // 内核相关 │ └── STM32F10x_StdPeriph_Driver/ // 标准外设库 └── System/ // 系统配置 └── stm32f10x_it.c新建工程的步骤可以精简为下面六步从ST官网或GitHub下载STM32F10x标准外设库解压后copy出CMSIS和STM32F10x_StdPeriph_Driver两个文件夹。在Keil MDK中新建工程选择芯片型号为STM32F103C8。添加启动文件startup_stm32f10x_md.s中容量Flash选择md到工程中。添加stm32f10x_gpio.c、stm32f10x_rcc.c、stm32f10x_i2c.c等需要的库源文件和自定义的dht11.c、oled.c。在C/C选项卡中定义STM32F10X_MD和USE_STDPERIPH_DRIVER宏并设置包含路径。配置调试器为ST-Link设置Flash Download选项。4.3 时钟树配置别让延时尺寸跑偏STM32F103默认使用HSI内部时钟频率8MHz如果不配置PLL系统主频只有8MHz所有基于SysTick的延时函数都会按照8MHz来计算而外设库初始化代码里默认的是72MHz这会导致延时严重偏短——DHT11时序要求延时20ms实际只等了2.2ms读取必然失败。主程序开头必须调用SystemInit()startup文件里已经调用了并把系统时钟切换到HSEPLL到72MHzvoid SystemClock_Config(void) { ErrorStatus HSEStartUpStatus; RCC_DeInit(); RCC_HSEConfig(RCC_HSE_ON); // 开启外部晶振 HSEStartUpStatus RCC_WaitForHSEStartUp(); if (HSEStartUpStatus SUCCESS) { RCC_HCLKConfig(RCC_SYSCLK_Div1); // AHB SYSCLK RCC_PCLK1Config(RCC_HCLK_Div2); // APB1 36MHz RCC_PCLK2Config(RCC_HCLK_Div1); // APB2 72MHz FLASH_SetLatency(FLASH_Latency_2); // Flash 2等待周期 RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); // 8MHz*972MHz RCC_PLLCmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) RESET); RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); } }这里一个典型误区很多人把delay_ms(20)当成绝对精确的定时器来用在DHT11场景里其实不必纠结微小的误差。DHT11协议本身对时序的容错范围很宽起始信号18ms多几ms也行位采样40μs加减几μs也能正确读关键是不能差一个数量级。时钟配置错、延时函数基准错才是读不到数据的真正原因。4.4 主程序框架一个典型的采集-显示循环主程序的逻辑非常简洁真正复杂的东西都在驱动层。核心代码如下int main(void) { SystemClock_Config(); // 系统时钟72MHz Delay_Init(); // 延时函数初始化 OLED_Init(); // OLED初始化 DHT11_Init(); // DHT11引脚配置 OLED_ShowString(0, 0, TEMP: --.-C); OLED_ShowString(2, 0, HUMI: --.-%); while (1) { if (DHT11_ReadData(temperature, humidity) SUCCESS) { OLED_ShowFloat(0, 5, temperature, 1); OLED_ShowFloat(2, 5, humidity, 1); } else { OLED_ShowString(1, 0, Sensor Error!); } delay_ms(1000); // 每1秒刷新一次 } }这里要注意一个隐藏问题DHT11的采样间隔不能太频繁数据手册建议两次读取间隔不小于1-2秒。实际测试中如果连续快速读取DHT11偶尔会返回全高或全低电平导致数据校验失败。官方给出的原因是传感器内部的测量周期就是1秒左右你读得太勤只能拿到上一次缓存的数据甚至触发内部状态机混乱。所以主循环里的delay_ms(1000)不只是一个显示刷新间隔更是DHT11正常工作的必要条件。5. 实测环节这五个坑我调试到怀疑人生过代码写好了工程编译通过烧录也没报错但屏幕上的数据就是不对——要么是温度显示-999要么是湿度恒定不变要么OLED直接白屏。如果你也遇到这些问题往下看我踩过的坑帮你排掉。5.1 白屏的罪魁祸首I2C地址和上拉电阻OLED完全不亮先别急着怀疑初始化代码。我遇到的第一种情况是I2C从机地址写错——SSD1306的7位地址是0x3C左移一位后0x78很多人的代码里写的是直接发送0x3C作为8位地址少了移位操作从机根本不会应答。第二种情况是SCL、SDA的上拉电阻缺失。市面上的OLED模块一般已经板载上拉但如果是自己焊的屏或者杜邦线接触不良I2C总线上没有上拉波形会变成满是毛刺的锯齿状。解决方法是加两个4.7kΩ电阻到3.3V或者检查模块背面的电阻是否虚焊。5.2 温度始终显示16℃DHT11初始化时序出错这是一个非常容易误导人的现象数据能读出来校验也通过了但温度永远是16℃左右湿度永远是20%左右。排查了很久才发现是起始信号的问题——delay_ms(20)实际只有2ms时钟未配置好DHT11根本没有完成内部测量回传的是上一次上电的默认值。另一个可能性是传感器型号搞混了。市面上有些模块直接标着DHT11但实际用的传感器引脚时序和DHT11有细微差别导致读到的数据在半固定值附近抖动。解决办法是换一个正规渠道的模块或者用示波器抓一下传感器回传的波形看位宽度是否符合数据手册。5.3 OLED显示数字毛边显示前没清残影使用8x16字体显示温湿度时如果温度从25.4℃变成5.2℃原先25.4中的5和4对应的像素点不会自动消失屏幕会残留上一帧的笔画。解决方式有两种每次更新前调用OLED_Clear()全屏清屏简单但会闪烁更优雅的做法是计算好数字占用的列数只清除那一区域void OLED_ClearArea(uint8_t page, uint8_t col_start, uint8_t col_len) { for (uint8_t col col_start; col col_start col_len; col) { OLED_SetPos(page, col); OLED_WriteData(0x00); } }5.4 DHT11引脚和调试器冲突这也是新手经常踩的坑ST-Link的SWDIO和SWCLK默认占用PA13和PA14如果你把DHT11的DQ引脚接在PA13或PA14上程序一跑起来调试器就掉线。另外I2C的PB6、PB7和USART1的PA9、PA10在部分开发板引脚排列上挨得很近杜邦线稍不注意就会短接。选引脚之前一定对照开发板的原理图避开调试口和下载口。一般建议DHT11接PA0、PA1或PB0、PB1这种安全引脚OLED的SCL/SDA接PB6/PB7I2C1的默认引脚互不干扰。5.5 值偶尔跳变到异常范围先查电源再看时序数据偶尔跳变比如温度突然变成89℃又恢复最大的嫌疑是供电不稳定。DHT11的工作电压是3.3V-5.5V如果模块用的是开发板的3.3V供电而开发板的3.3V LDO本身带载能力一般再加上OLED模块、传感器同时工作电压纹波一大传感器内部的ADC采样就会出现毛刺。处理办法是在VCC和GND之间并联一个10μF电解电容和100nF陶瓷电容做去耦别小看这两个电容它们的作用是给瞬间电流提供低阻抗通路让传感器供电电压更稳定。如果还不行就换成独立稳压模块给传感器供电让主控板和传感器各自吃一路电源。6. 再进一步这个项目还能往哪些方向扩展做完这个基础项目后如果你想往深处走一步不用急着换芯片、换平台基于现有硬件做下面几个方向的扩展性价比是最高的。6.1 加入掉电保存功能把数据记到Flash里STM32F103的Flash是64KB除了代码占用的空间还剩下不少页可以用。你可以把每次采集到的温湿度按照固定格式写入Flash的某个扇区掉电不丢失重启后可以从Flash里加载历史数据。用标准库的FLASH_ProgramHalfWord函数即可实现注意Flash写之前要先擦除扇区而且擦除操作会把整个扇区清零。6.2 用定时器外部中断替代延时采样解放CPU目前的DHT11读取方式是用delay_us()死等这在读取过程中CPU完全被占用。如果你想让系统同时处理其他任务比如按键扫描、OLED动画可以改用定时器输入捕获或者外部中断定时器配合的方式通过DHT11上升沿和下降沿的中断来记录时间戳在中断回调中解析数据位。这种方式写起来要复杂不少但如果你以后要跑RTOS比如FreeRTOS就必须要掌握非阻塞读取传感器的思想——把传感器读取做成事件驱动而不是轮询死等。6.3 接线改成ESP8266/ESP32变成物联网设备温湿度采集和OLED显示只是第一步加上ESP8266或者直接用ESP32数据就能通过WiFi上报到MQTT服务器手机端做个小程序或者用现成的APP比如点灯科技就能远程查看温湿度。STM32通过串口把格式化好的数据包发给ESP8266ESP8266作为透传模块转发到云平台。这个扩展思路非常成熟网上资料也很多适合想往物联网方向走的人。6.4 别忽略人机交互的细节菜单与按键OLED屏显示数据只是最基本的用法。你可以加两个按键按下按键切换显示界面——一页显示当前温湿度一页显示历史最高/最低温度一页显示设备运行时间。这就会用到状态机编程的思路也是嵌入式开发中很重要的编程范式。这个项目做完可以说你已经在STM32的门口探进半个身子了。我自己回头看觉得这个组合最妙的地方在于它逼着你去理解时序、理解协议、理解硬件和软件的边界。那些用delay_us数出来的微妙延时、那些核对了一遍又一遍的引脚初始化都在帮你建立对微控制器更扎实的直觉。现在动手建工程吧等你把OLED点亮、把DHT11的数据稳稳地显示在屏上那一刻你能感受到一种非常纯粹的成就感。本文还有配套的精品资源点击获取