ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32+DHT11温湿度监测系统设计:嵌入式开发全流程实战

2026/9/9 3:11:30 拓冰建站 浏览量
STM32+DHT11温湿度监测系统设计:嵌入式开发全流程实战 “第五次作业”这几个字看起来平平无奇很多人在拿到题目时甚至不知道该从哪儿下手。我这次做的是一个嵌入式方向的课程作业基于 STM32 的智能仓库温湿度监测系统。东西不复杂但整个流程走下来从方案设计、硬件选型、代码调试到上位机数据记录踩坑和收获都不少。这篇就把整个思路和实操过程掰开揉碎了讲一遍适合正在做类似嵌入式课程设计、或者第一次接触单片机项目的人参考。得先说明一点作业要求本身通常不会写得太细很多细节是基于常见课程实践的合理补齐。我做的方案是用 STM32F103C8T6 最小系统板作为主控接一个 DHT11 温湿度传感器采集环境数据数据通过 I2C 接口显示在 0.96 寸 OLED 屏幕上同时 STM32 通过串口把数据发给电脑端的上位机程序上位机负责实时显示并存储成 CSV 文件。这套方案覆盖了传感器采集、主控处理、本地显示、通信传输、上位机解析这几个嵌入式开发的核心环节作为“第五次作业”来说工作量适中技术点也足够丰富。1. 整体设计与思路拆解1.1 为什么选这个题目方向“第五次作业”这类课程任务通常出现在嵌入式、物联网或者单片机相关课程的中后期前四次作业大概率已经覆盖了 GPIO 控制、定时器、中断、串口通信这些基础知识点。第五次作业的价值在于把这些零散的知识点串成一个相对完整的系统。我选择温湿度监测这个方向核心原因有三个。第一DHT11 传感器足够便宜模块化程度高几块钱就能买到坏了也不心疼。第二这个题目能把单总线协议、I2C 显示、串口通信、数据解析这几个嵌入式常用技能全部用上覆盖率很高。第三应用场景非常好解释仓库监测、机房环境监控、智能农业大棚这些都是刚需场景写实验报告和答辩的时候很好展开。如果你们作业没有指定方向我建议优先选这种带传感器、带显示、带通信的题目。纯点灯太单薄纯算法又偏离硬件课程的核心目标。温湿度加上一个 OLED 显示和串口上报整体难度曲线对大多数人是友好的。1.2 硬件方案选型和成本控制硬件选型是很多第一次做项目的人最容易纠结的地方。我的建议是在满足作业要求的前提下优先用熟悉、资料多、调试方便的芯片。主控选了 STM32F103C8T6也就是俗称的“蓝丸”最小系统板。这颗芯片是 Cortex-M3 内核主频 72MHzFlash 64KBRAM 20KB做温湿度采集这种轻量级任务绰绰有余。更重要的是它的生态太成熟了不管是标准外设库、HAL 库还是寄存器操作网上资料一抓一大把遇到问题随便搜就有答案。传感器选了 DHT11虽然它的精度和采样频率跟 SHT30、DHT22 这些传感器没法比但作为课程作业来说完全够了。DHT11 的湿度测量范围是 20% 到 90% RH精度 ±5% RH温度测量范围 0℃ 到 50℃精度 ±2℃。这些参数对仓库环境监测这种场景来说是合理的而且它的数字单总线协议很有代表性理解了这个协议以后用其他单总线传感器基本是相通的。显示部分用了 0.96 寸 SSD1306 驱动的 OLEDI2C 接口四根线搞定。I2C 总线只需要两根信号线SCL 和 SDA一根电源一根地接线非常简洁。相比 1602 液晶屏OLED 不需要背光显示效果好而且不需要占一堆 GPIO 口。时间基准用了 TIM2 定时器做 1s 周期的数据采集触发通过串口 1 的 TX 引脚发送数据到 USB 转 TTL 模块再转发到电脑的串口调试助手或者自写的 Python 上位机。整块板子的物料成本大概在 40 块钱左右对学生党很友好。1.3 软件框架和模块划分写嵌入式程序最怕的就是把所有代码堆在一个 main.c 里第二次看就不知道哪儿是哪儿了。我这次把程序按功能划分成了几个模块dht11.cDHT11 传感器驱动负责初始化引脚、发起读取时序、解析 40bit 数据oled.cSSD1306 驱动负责初始化显示屏、清屏、显示字符串和数字usart.c串口驱动负责初始化串口、重定向 printf 实现格式化输出main.c主逻辑负责初始化各模块、启动定时器、在定时器中断里触发采样和显示每个模块单独成文件头文件里只暴露必要的函数接口。这样做的好处很明显调试某个模块的时候不需要牵扯其他代码而且如果后面要把 DHT11 换成 DHT22只需要改 dht11.c 里的底层解析逻辑上层不用动。数据流的设计也很直白传感器数据 → 单片机内存 → OLED 显示 / 串口发送 → 上位机接收解析 → 存储。这个链路短期内看起来是单向的但实际上留了一个扩展口如果后续要加上下位机控制功能比如超温报警风扇只需要在上位机软件里加一个命令发送接口把数据流反向打通就行。2. 核心细节解析与实操要点2.1 DHT11 单总线通信协议深入理解DHT11 用一根数据线完成双向通信这条线是开漏结构所以需要一个 4.7kΩ 到 10kΩ 的上拉电阻。很多现成模块上已经集成好了上拉电阻用模块的话就不用操心这件事但如果自己用裸传感器焊电路板这个电阻必须有否则通信绝对不稳定。单总线通信的起始时序是这样主机先把数据线拉低至少 18ms然后释放这个低电平时间告诉传感器“准备开始传输”。紧接着传感器拉低数据线 80us再释放 80us这算是传感器的应答信号。应答之后传感器开始连续发送 40bit 数据。这 40bit 数据的读取方式有个非常容易踩坑的细节。DHT11 并没有真正的时钟信号它靠高电平的持续时间来表示 0 或 150us 左右的高电平表示 070us 左右的高电平表示 1。所以读取时需要在每次电平跳变后开启一个精确的计时窗口等高电平结束后通过记录的高电平时间判断当前位是 0 还是 1。DHT11 传输的 40bit 数据中前 8 位是湿度整数部分第 9 到 16 位是湿度小数部分第 17 到 24 位是温度整数部分第 25 到 32 位是温度小数部分最后 8 位是校验和。校验方法非常简单把前 4 个字节相加如果低位字节等于第 5 个字节数据有效。我实际测试的时候发现一个问题直接从 DHT11 数据手册拷贝的时序代码在某些 GPIO 配置下读出来的数据偶尔会全 0 或者全 1。排查到最后发现是 GPIO 的输出速度和上下拉配置不对数据线模式切换的时候 GPIO 必须设置成开漏输出、带上拉的模式才能正常读否则信号质量不好。这个经验在后面的常见问题部分会详细展开。2.2 SSD1306 OLED 显示要点OLED 用的是 SSD1306 控制器I2C 接口最常用的地址是 0x3C也有少部分是 0x3D。初始化的时候如果屏幕不亮第一件事就是确认地址对不对。可以用 I2C 扫描程序把总线上所有设备的地址扫一遍一劳永逸。SSD1306 内部有一块 1KB 的 GDDRAM 显存对应 128×64 像素。控制方式是通过写命令和写数据来刷新显存内容。比较推荐的做法是先在内存里维护一个 1024 字节的显存缓冲数组所有绘制操作都对这个数组进行需要更新显示的时候一次性把整个数组通过 I2C 总线刷到屏幕的 GDDRAM 里。这样做能避免频繁调用 I2C 通信导致屏幕闪烁而且修改代码逻辑也更清晰。显示中文字符这件事如果做作业的时候没有特殊要求我建议直接用原生的英文字库通过取模软件生成 ASCII 字符点阵。中文需要用到 GB2312 编码的 16×16 或 12×12 点阵字库虽然也不难但每个汉字要自己取模工作量会多不少。我这里直接用了 6×8 和 8×16 两套 ASCII 点阵显示“Temp” “Humi” 这类英文标签完全够了。2.3 串口发送与上位机对接串口部分作为一种非常成熟的技术方案配置比较标准波特率设置为 9600 或 115200 都可以。要注意的是 DHT11 的采样周期最短是 1s所以串口数据不需要发太快否则会导致上位机收到大量重复数据。我设置的是每 2s 发送一帧帧格式是固定长度的字符串例如TEMP:25.3,HUMI:60.5这个格式简单、可靠、便于解析。如果你想做得更严谨可以加上帧头帧尾和校验比如$TEMP:25.3,HUMI:60.5*3F其中 3F 是前面所有字符的异或校验值。但课程作业阶段固定字符串配合换行符已经足够。上位机我写了一个简单的 Python 脚本用 pyserial 库读取串口数据正则表达式提取温度和湿度数值然后实时显示在控制台同时追加写入 CSV 文件。这个脚本大概 60 行配合 matplotlib 还能画实时曲线。2.4 定时器中断设计思路采集和显示逻辑放在中断里还是主循环里是很多初学嵌入式的人纠结的问题。我这次把定时器中断用于产生 2s 的采样节拍中断服务函数里只置一个标志位真正的采集和显示逻辑放在主循环里判断这个标志位。这是嵌入式开发中一个非常经典的做法中断服务函数保持短小精简复杂逻辑放在主循环避免在中断里耗时太长影响实时性。具体的定时器配置使用的是 TIM2在 APB1 总线上时钟源是 72MHz 经过分频后的 36MHz如果 APB1 的预分频系数不等于 1。我设置预分频值 35999重装载值 3999算下来定时周期就是 (36000/36000000) × 4000 秒 4 秒不对我实际使用的是预分频 7199、重装载 9999这样定时器计数频率是 72MHz/(71991) 10kHz周期为 0.1ms重装载 9999 后就是 1s 中断一次。在中断里再计数两次得到 2s 的采集周期。这个参数计算的实际过程书里写得很简单但自己算的时候容易搞混时钟树的分配关系。3. 实操过程与核心环节实现3.1 硬件连接与接线检查我用的 STM32F103C8T6 最小系统板的引脚定义和常见的 Arduino Nano 有所不同接线之前一定先查板子的原理图不要凭感觉接。核心接线如下表模块信号线STM32 引脚DHT11DATAPA0OLEDSCLPB6 (I2C1_SCL)OLEDSDAPB7 (I2C1_SDA)串口模块RXPA9 (USART1_TX)串口模块TXPA10 (USART1_RX)DHT11 模块的 VCC 接 3.3V 还是 5V要看模块上有没有电平转换电路。市售很多 DHT11 模块是客服说支持 3.3V 到 5V 宽电压但保险起见我直接接 3.3V和 STM32 的电平匹配最稳。OLED 的 VCC 也是接 3.3V不接 5V免得时间长了把 SSD1306 烧了。串口模块要接成交叉线单片机的 TX 接 USB 转 TTL 模块的 RX单片机的 RX 接模块的 TX接线错误会导致数据出不来。我刚开始就接反了串口助手什么都收不到。另外USB 转 TTL 模块和最小系统板最好共地否则通信会漂移数据时通时断。这个共地问题在 3.3V 和 5V 混合供电的系统里很重要缺了地线电平参考点不一致串口信号根本没有意义。3.2 代码实现与关键配置主程序框架用标准外设库实现这里挑几个关键的部分说一下。DHT11 的读取函数是最容易出问题的。核心流程可以抽象成拉低 18ms → 释放并等待传感器响应 → 读取 40bit 数据 → 校验。读取每一位的代码逻辑如下uint8_t dht11_read_byte(void) { uint8_t i, byte 0; for (i 0; i 8; i) { while (DHT11_DATA_IN() 0); // 等待低电平结束 delay_us(40); // 在40us时采样 if (DHT11_DATA_IN() 1) // 仍然为高说明是1 { byte | (1 (7 - i)); while (DHT11_DATA_IN() 1); // 等待高电平结束 } } return byte; }这里的判断逻辑是DHT11 的 0 信号高电平持续 26-28us1 信号高电平持续 70us。从高电平开始计时40us 时采样如果此时还是高电平那说明这是一位 1否则就是 0。这个方法简单可靠比单纯测量高电平宽度然后判断长于/短于某个阈值更容易理解。HAL 库用户需要特别注意一点HAL 库的延时函数在系统时钟配置不当时误差会变大。如果发现读出来的数据老是校验失败先检查系统时钟是否真的跑在手册标称的频率上可以翻转一个 GPIO 然后拿示波器看实际频率或者用逻辑分析仪配合测量高电平宽度。OLED 显示部分的初始化序列比较长一般不用自己从头调直接参考厂商提供的初始化代码即可。关键是设置合适的内存访问模式和段重映射否则屏幕上显示的字符可能是镜像的。3.3 上位机代码和数据处理上位机用 Python 实现依赖 pyserial 库。安装命令很简单pip install pyserial核心读取循环如下import serial import csv import re import time ser serial.Serial(COM7, 115200, timeout1) csv_file open(env_data.csv, a, newline) writer csv.writer(csv_file) while True: line ser.readline().decode(utf-8, errorsignore).strip() if line: match re.match(rTEMP:([\d.]),HUMI:([\d.]), line) if match: temp float(match.group(1)) humi float(match.group(2)) print(f{time.strftime(%Y-%m-%d %H:%M:%S)} - {temp} C, {humi} %) writer.writerow([time.strftime(%Y-%m-%d %H:%M:%S), temp, humi]) csv_file.flush()串口号 COM7 是自己机器上识别出来的需要去设备管理器里看。这个脚本有几个值得注意的设计一是 CSV 文件用追加模式写入即使程序中途崩溃之前的数据也不会丢二是每次写入都调 flush保证数据即时落盘避免程序异常退出时缓冲区里的数据丢失三是正则表达式匹配可以过滤掉无效行不会因为串口收到的噪声数据导致程序崩溃。这里我遇到过一个小坑pyserial 的 readline 在读不到换行符时会一直阻塞如果单片机侧发的字符串没有加 \r\n 结尾那 Python 端永远等不到完整的一行数据。所以单片机端发送数据时字符串末尾要加上 \r\n。3.4 系统联调的完整流程我联调的时候分了三个阶段进行。第一阶段先让 OLED 屏亮起来随便显示一些静态文字确认I2C通信正常。第二阶段让 DHT11 的数据成功显示在屏幕上通过对着传感器吹气、用手握住排气等方式观察数值变化。第三阶段才接通串口让数据同时送到上位机验证存储文件。分阶段调的好处是出现问题时能快速定位。如果一开始就把所有设备接好然后写完全部代码再上电一旦屏幕不亮你根本没法确定是硬件问题还是代码问题调试效率极低。先跑通最底层的显示链路再往上加传感器数据最终再连电脑每一步都有确定的验证标准。4. 常见问题与排查技巧实录4.1 OLED 屏幕不显示的排查思路我遇到的第一个大坑是 OLED 怎么都不亮排查下来分了几个层次。先是测供电确认 VCC 和 GND 之间有 3.3V 电压这个测量是基础但必须做。然后测 SCL 和 SDA 引脚发生通信时应该有波动的电平这就说明硬件通路是通畅的。然后怀疑地址问题我写了段 I2C 扫描代码把总线上探测到的所有设备地址打印出来很快就确认了地址是 0x3C 没错。最后发现问题是 I2C 引脚配置错了。我用的标准外设库里PB6 和 PB7 复用为 I2C1 引脚时需要先把引脚配置为复用开漏输出模式并且打开 I2C1 的时钟。这个配置很容易漏漏了之后 I2C 总线根本没有波形输出屏幕自然黑屏。另外我犯过的错误是在初始化 I2C 前先调用了 OLED 初始化函数导致在时钟没有开启的状态下就去操作寄存器程序卡死在等待标志位的地方。4.2 DHT11 数据一直为 0 或校验失败DHT11 数据异常是我这次工程里最费时间的问题。从显示结果看温度和湿度一直是 0校验和过不了。我首先想到的是时序问题就开始调延时函数的准确性后来发现自己手写的延时函数在开启优化编译选项后循环变量被编译器优化掉了延时时间直接变成 0。解决办法是使用 DWT 周期计数器或者改用定时器来保证微秒级延时的准确性。还有一个让数据变 0 的原因是引脚模式配置。DHT11 的数据线在输出低电平拉低总线之后必须立即切换成输入模式来读取传感器反馈的响应信号。很多第一次做这个模块的人会在初始化的最后设置 GPIO 为推挽输出然后读取的时候忘记切换导致读到的电平始终是引脚输出状态而不是总线状态。我改成了开漏输出带上拉这样输出时整个总线是定的输入时也不用切换模式逻辑更稳定。校验失败的问题主要集中在读取过程中如果某一位因为时序抖动读错校验和必然对不上。我的处理是超过三次读取失败就丢弃当前数据下次定时器中断再重新读取绝不把错误数据上传给上位机。这一条对后面做数据清洗和显示逻辑来说非常重要宁可少显示一帧也不要显示错误数据。4.3 串口数据乱码的处理串口收到的数据是乱码通常只有两个原因波特率不匹配或者是时钟配置不对导致波特率实际值和预期值差太多。我刚开始用的 9600 波特率上位机收到的全是乱码。一查芯片时钟树发现内部 RC 振荡器误差太大而我没有切换到外部晶振。解决方法是主板上有 8MHz 晶振的话在 SystemInit 函数里把系统时钟来源切换到 HSE同时配置好 PLL 倍频系数。时钟配置好之后9600 和 115200 都试过数据都正常。如果用的开发板没有外部晶振建议换用 9600 或者 4800 波特率内部 RC 在低频时误差相对更小容错性更好。还有一种乱码是电平不匹配造成的。有些 USB 转 TTL 模块是 5V 电平的直接接 STM32 的 3.3V 引脚时间长了会把芯片的引脚拉坏。最好是用带电平转换功能的模块或者加一级 3.3V 稳压和电平转换。4.4 定时器触发时间不准确定时器的时间参数看起来很好算实际上容易出问题的点有两个。一个是分频系数和重装载值到底要不要加 1很多人在算的时候会漏掉这个“加 1”。第二个是 APB1 的分频系数对定时器时钟的影响当 APB1 预分频不是 1 时定时器的时钟是 APB1 的两倍这个规则在参考手册里写得很隐蔽但如果你忽视了它算出来的定时时间会差一倍。我建议的检验方法是把计算出来的定时周期和实际观察到的现象做交叉验证。比如给 LED 翻转电平观察闪烁周期然后用手表计时 60 秒数 LED 翻转了多少次就知道定时是否准确。放在本次项目里每 2s 一帧的数据用手机秒表数 10 帧数据是不是恰好 20 秒这个验证简单直接。4.5 常见问题速查表现象可能原因排查步骤与解决办法OLED 不亮I2C 地址错误、引脚复用配置缺失、供电不足先扫 I2C 地址确认设备在线检查 PB6/PB7 复用开漏配置测 VCC 电压OLED 显示乱码或镜像SSD1306 初始化参数不对、滑移设置错误检查段重映射和 COM 扫描方向命令初学时直接用厂家初始化序列DHT11 数据全 0GPIO 模式配置错误、延时函数不准确、未等应答数据脚用开漏输出带上拉用 DWT 实现微秒延时检查响应时序DHT11 校验失败引脚上拉了没有、总线干扰、读取时序太快加上拉电阻线材缩短降低主频或优化延时精度串口乱码波特率不匹配、时钟源不准、电平不匹配确认上位机和下位机波特率一致切换外部晶振用 3.3V 电平的串口模块定时不准确分频系数加 1 漏掉、APB1 时钟倍率忽略跑测试程序实测翻转频率查参考手册时钟树5. 项目之外可从这次作业延伸的方向做完这套温湿度采集系统之后站在课程设计的角度其实已经合格了但如果你想让这个“第五次作业”拿高分或者真的想把它往实际应用方向推进还有很多值得顺手扩展的地方。硬件层面DHT11 的精度确实一般换成 SHT30 之后I2C 通信方式不变精度和稳定性都会上一个大台阶。OLED 可以升级成 TFT 彩屏甚至加一个按键实现菜单切换、历史数据回看。如果想要脱离电脑独立工作加一个 SD 卡模块把数据直接记录到本地 CSV 文件那就成了一个小型的数据记录仪仓库巡检这种场景也能实际用上。软件层面上位机可以从命令行版升级成带界面的工具用 PyQt 或 Tkinter 写一个小窗口显示实时数值曲线数据存到 SQLite 数据库。如果再进一步用 MQTT 协议把这套数据上报到本地服务器通过浏览器远程查看那就是一个标准的物联网项目雏形了。通信方式上把有线串口换成 ESP8266 模块连 WiFi上位机逻辑不变采集端变成无线传输灵活性会高很多。虽然 ESP8266 自身的 AT 固件配置会带来新的复杂度但这个过程对理解物联网通信链路非常有帮助。如果后续作业刚好是“做一个远程监控系统”这个扩展点可以直接复用。回到这次作业本身我做完之后最大的感受是嵌入式项目的难点不在于单个知识点有多难而在于把多个知识点串起来的时候任何一环出了问题都会导致整体失败。硬件的问题可能隐藏在你没有怀疑到的那根跳线上软件的问题可能出在你觉得“肯定没问题”的延时函数里。这种系统性排查和解决问题的能力才是“第五次作业”真正让你练的东西。如果你们课程进度正常可能很快会迎来期末考试或者课程设计答辩建议把这次作业的过程数据保留好包括硬件接线图、调试过程记录、上位机脚本和几次失败的截图。写报告的时候这些素材非常加分答辩时也可以讲清楚“我遇到过什么问题、怎么定位的、最后怎么解决的”这比只说“全部一次跑通”要有说服力得多。