news 2026/10/30 1:08:26

STM32F1+DHT11温湿度采集实战:从选型到联调排错全记录

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F1+DHT11温湿度采集实战:从选型到联调排错全记录

从纸上画框图,到真正把一块STM32F1芯片焊上板子让外设跑起来,中间隔着的往往是整整一个星期的折腾。我最早接触STM32F1系列是在一个温湿度采集的小项目上,当时用的是最普通的STM32F103C8T6,搭配一颗DHT11温湿度传感器,需求就是把温度和湿度实时读出来送进串口。这个组合如今看起来平平无奇,但对当年刚入门的我来说,踩平的坑比学到的东西还多。STM32F1系列作为ARM Cortex-M3内核的经典单片机,至今仍是很多产品开发、毕业设计、DIY项目的首选,原因不外乎三点:资料多、上手快、价格温和。这篇内容不打算写成芯片手册,而是把我从选型、搭工程、写驱动到联调实测这一整套过程里,觉得最值得说的东西摊开讲一讲,给准备在STM32F1上做类似传感器项目的人当个参考。

1. 型号那么多,选型时真正要看的就这几点

1.1 先看懂F103、F101和互联型到底差在哪

STM32F1系列是个大家族,但市面上见到最多的还是STM32F103,其次是F101,以及带USB主机功能的F105和F107互联型。很多人第一次选型时盯着数据手册上的几十个型号发懵,其实规律很简单:同封装的芯片,F103和F101引脚基本兼容,主要区别在外设资源和主频。F103最高主频72MHz,带USB从机、CAN、ADC、定时器、DMA一套齐全;F101主频只有36MHz,砍掉了USB和部分高级定时器,但价格略低,适合对性能没要求、单纯做控制逻辑的场景。F105和F107则是多了USB OTG和以太网MAC,一般涉及USB高速通信或网络设备才用得上,日常项目不需要纠结。

选型时还有一个容易被忽略的点:Flash容量和封装。同一颗F103C8T6是64引脚、Flash 64KB,F103RCT6则是64引脚、Flash 256KB,F103ZET6是144引脚、Flash 512KB。最好根据实际代码量估算Flash,我见过有人在64KB的芯片上硬塞图形库,最后工程编译都过不去,只能换芯片换板子,PCB都改了,非常痛苦。

1.2 我选芯片时的固定查看清单

结合自己做DHT11温湿度项目的经验,我筛芯片时会按这个顺序看:

  • 主频和外设是否够用:72MHz对大多数单传感器采集、UART传输、小型控制逻辑绰绰有余;如果需要同时跑LCD、FATFS文件系统、多个传感器,考虑Flash不低于256KB的型号。
  • 引脚数量和封装:小项目用LQFP48的C8T6就行,引脚少焊接容易,想加功能也有富余;做产品化设计则要留ISP、SWD调试口,至少留3-5个空闲IO备用。
  • ADC精度和通道数:如果项目里要采集多路模拟量,F103的12位ADC有10个外部通道,基本够用,但注意别把所有模拟输入引脚都用去做数字IO。
  • 供货和价格:这一点越来越重要。哪怕只是做几个样品,也要先确认渠道能不能稳定买到,否则画完板子买不到芯片只能干瞪眼。

1.3 开发板和自制电路板的取舍

入门阶段买一块成品开发板是最省事的,板载DAP或ST-Link下载器、晶振电路、复位电路、LED全都给你接好了,插上USB就能点灯,环境变量少很多。但专心做DHT11这类实际采集项目时,我更建议自己画一块或手焊一块最小系统板:一个8MHz晶振加两个20pF电容、一个复位电路、BOOT0接下拉电阻、3.3V电源滤波电容网络,再接上SWD四线接口,STM32F103C8T6就能稳稳跑起来。只有自己搭过最小系统,才能确认你对时钟电路、复位时序、电源去耦这些基础设计是真的理解了,而不是只知道往开发板上插线。

2. 工程搭建与时钟配置:不理解时钟树,后面全是坑

2.1 HAL库还是标准库,我的真实建议

STM32F1系列的开发库主要有三种选择:寄存器操作、标准外设库、HAL库。寄存器操作适合想彻底搞懂芯片内部结构的人,但不适合实现复杂项目;标准外设库是ST早期主推,代码直接、打包完整,很多老工程师一直用到现在;HAL库则是ST移交给Cube生态后的主力,抽象层次高、跨系列复用性强,但封装层级多,开个外设要配一堆结构体,调试时跳进去能绕晕。

我自己的习惯是:学习阶段用标准外设库,它把寄存器操作封装了一层,跳进去还能看到底层,适合理解原理;做实际产品时也仍然常用标准库,因为F1系列的标准外设库足够稳定,代码体积小,可控性高。HAL库当然也能做,只是要有耐心去啃那一大堆初始化和句柄结构体。如果你手头的参考代码是基于标准外设库的,那就别迷信HAL必须用,先听一下编译器和链接器给你什么反馈。

2.2 时钟树配置是STM32F1最容易出错的地方

时钟配置听起来无聊,但所有外设的波特率、定时器周期、ADC采样频率都从这颗时钟树里长出来。STM32F103复位后默认使用内部8MHz RC振荡器(HSI)的8分频,也就是1MHz作为系统时钟。如果不配置PLL,很多外设直接就算不对。典型的外部8MHz晶振(HSE)加PLL倍频到72MHz的配置是:

  • HSE 8MHz,PLL倍频系数设为9倍,得到SYSCLK = 72MHz
  • AHB预分频1分频,HCLK = 72MHz
  • APB1预分频2分频,PCLK1 = 36MHz,这是低速外设域(USART2/3、SPI2/3、定时器TIM2-7等)
  • APB2预分频1分频,PCLK2 = 72MHz,这是高速外设域(USART1、SPI1、ADC、GPIO等)

早期很多教程只写"倍频9倍"不解释为什么,结果有人改了外部晶振换成12MHz后还是照抄9倍,系统时钟直接变成108MHz,芯片跑飞,串口乱码。这个坑我踩过不止一次,所以每次换板子第一件事就是确认晶振实际频率,然后用示波器或测量一个定时器翻转周期验证系统时钟是否真的到了72MHz。

2.3 用SysTick做延时:DHT11项目的基本功

DHT11这类单总线传感器对时序很敏感,延时函数写不好,读出来的数据就是一堆FF或00。标准做法是初始化SysTick为1ms中断,然后做毫秒级延时;微秒级延时用循环空转。我测试过,在72MHz主频下,用for(i=0; i<72; i++);差不多能延时1微秒,但不同编译优化等级会影响实际循环次数,因此无论如何都推荐用逻辑分析仪或示波器验证一下微秒延时的真实长度。实测DHT11的起始信号要求主机拉低至少18ms,我写20ms,长一点更稳妥,但也不能太长——拉低超过几十毫秒还问题不大,但过长的低电平会让传感器误判总线状态,所以给20ms±3ms留足够余量就没问题。

3. DHT11单总线协议与STM32F1 GPIO模拟:一次完整的时序拆解

3.1 先看明白DHT11的40位数据格式

DHT11温湿度传感器出来的数据格式非常固定:一次完整传输包含40位数据,顺序是湿度整数(8位)、湿度小数(8位)、温度整数(8位)、温度小数(8位)、校验和(8位)。湿度小数和温度小数在DHT11上一般读出为0,但格式里仍然保留,不能省略。校验和是前四个字节的累加取低8位,如果校验失败,这组数据直接丢弃重读,这是单总线传感器最常见的容错手段。

我见过很多人在这个环节出问题:好不容易把40位读出来了,看到数据跳变,第一反应是改延时,其实八成是校验没过。DHT11的位数要数清楚,每一位的判定标准是看高电平持续时间长短:持续26到28微秒左右代表逻辑0,持续70微秒左右代表逻辑1。用STM32F1在72MHz主频下,用GPIO输入捕获或者纯粹的读引脚加计时循环都能做;我建议初学者先用循环读法理解时序,真要做高精度再换外部中断。

3.2 主机起始信号与GPIO方向切换

DHT11的数据引脚是单总线,空闲时是高电平(外部上拉),主机要和它通信时做这样的动作:

  1. 主机把引脚配置为推挽输出,拉低至少18ms
  2. 然后拉高,释放总线,延时20到40微秒
  3. 此刻切换GPIO为输入模式(上拉输入即可,如果有外部上拉就用浮空输入也一样)
  4. DHT11会拉低总线约80微秒作为响应信号,然后再拉高80微秒,之后开始逐位输出40位数据

关键点在于GPIO的方向切换。F103上用标准外设库配置方向挺清晰,示例代码是设置GPIO_Mode_Out_PP和GPIO_Mode_IPU两个模式然后交替;用HAL库则可以用HAL_GPIO_WritePin控制输出,同时调整GPIO初始化结构体里的Mode字段,但不要频繁调用HAL_GPIO_Init,因为它会重新配置整个GPIO端口,耗时较长,可能把时序搞乱。更稳的做法是直接操作寄存器改CRL或CRH里对应引脚的CNF和MODE位,或者用位带操作,速度极快,不会打乱微秒级时序。

一个容易忽略的坑:DHT11的响应低电平之后,会主动把总线拉高再开始发数据,所以主机检测到上升沿之后,就进入按位读取状态。位读取时,DHT11每发一位都会先把总线拉低约50微秒,然后根据数据是0或1决定拉高的时间。很多人误以为是从拉低就开始计时,实际应该从拉高开始计时,数高电平持续时间来判断0或1,这个逻辑搞反了会全乱。

3.3 我自己在标准外设库下写的DHT11读取代码

下面这段是基于STM32F103C8T6和标准外设库的完整读取流程,已验证可以正常读回温湿度数据:

// DHT11 on PB0 #define DHT11_GPIO_PORT GPIOB #define DHT11_GPIO_PIN GPIO_Pin_0 #define DHT11_RCC RCC_APB2Periph_GPIOB static void DHT11_Pin_Output(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = DHT11_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStructure); } static void DHT11_Pin_Input(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = DHT11_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStructure); }

起始信号发送部分:

uint8_t DHT11_Start(void) { uint8_t timeout = 80; DHT11_Pin_Output(); GPIO_WriteBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN, Bit_RESET); // 拉低 delay_ms(20); // 至少18ms GPIO_WriteBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN, Bit_SET); // 拉高 delay_us(30); // 20-40us DHT11_Pin_Input(); // 等待DHT11拉低响应 while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == Bit_SET) { if ((timeout--) == 0) return 1; // 超时 } // 等待拉高 timeout = 80; while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == Bit_RESET) { if ((timeout--) == 0) return 2; // 超时 } // 等待数据开始前的低电平 timeout = 80; while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == Bit_SET) { if ((timeout--) == 0) return 3; // 超时 } return 0; }

逐位读取:

static uint8_t DHT11_ReadBit(void) { uint8_t timeout = 100; // 等待低电平结束 while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == Bit_RESET) { if ((timeout--) == 0) return 0xFF; } // 延时40us后判断电平 delay_us(40); if (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == Bit_SET) { // 高电平持续较长,说明是1 return 1; } else { return 0; } }

读取字节并校验:

uint8_t DHT11_ReadData(uint8_t *humidity, uint8_t *temperature) { uint8_t buf[5]; uint8_t i, j; if (DHT11_Start() != 0) return 1; for (i = 0; i < 5; i++) { buf[i] = 0; for (j = 0; j < 8; j++) { buf[i] <<= 1; buf[i] |= DHT11_ReadBit(); } } // 校验和时间:SHA256 之类的大材小用,累加低8位就够 if (buf[4] != ((buf[0] + buf[1] + buf[2] + buf[3]) & 0xFF)) { return 2; // 校验失败 } *humidity = buf[0]; *temperature = buf[2]; return 0; }

这套代码在PB0上实测,每次读取耗时约50到60毫秒,满足DHT11的1秒采样周期要求。要注意的是读取频率别太快,DHT11手册建议两次读间隔不大于1秒、不小于1秒即可,同一颗传感器至少隔1秒再读取,不然容易读到上一次数据或返回错误。

3.4 延时函数和引脚的时序验证方法

这段代码里delay_ms和delay_us的实现直接决定了DHT11读取稳定性。delay_ms用SysTick中断计数实现,精确度足够;delay_us我用的是简单循环空转,但前面强调过,空转循环在不同编译器优化级别下偏差很大。建议把delay_us套在逻辑分析仪前面实测校准:让一个GPIO翻转一次,测量高电平宽度,再调整循环次数。我实测在Keil -O2优化下,72MHz主频大约2到3个周期可以校准到一个接近1微秒的循环,但不同芯片温度下还有轻微漂移。DHT11的时序容忍范围其实比较宽:低电平信号误差几十微秒问题不大,只要别在一位数据内把采样点卡在临界处就行,因此40微秒的判定延时不要写成精确的40.000us,留点偏差反而没事。

4. 把数据送到PC端:串口发送思路与常见乱码排查

4.1 为什么我坚持用串口而不是屏幕

DHT11项目最常用的数据出口是串口UART,原因很简单:调试直观、接线少、实时性足够。一个USB转TTL模块接USART1的TX、RX和GND,PC端用串口助手或终端工具就能连续查看温湿度值。相比LCD屏幕,串口不用考虑中文编码、刷屏延迟,也不占太多引脚,特别适合快速验证传感器本身有没有工作正常。等传感器验证结束,再把数据换成LCD显示或者报文协议也不迟,串口在整个开发周期里还可以当作日志口继续用。

我习惯把USART1设置在APB2总线上,波特率115200,8位数据,1位停止位,无校验,代码里直接查波特率寄存器分频表算USARTDIV。STM32F103的USART1时钟源是72MHz,要得到115200波特率,USARTDIV大约是72000000 / (16 * 115200) = 39.0625,取整为39,再处理小数部分的0.0625。标准外设库里USART_BaudRate设置会自动帮我们算好这些,但理解分频原理对排查乱码很有帮助。

4.2 用printf重定向输出温湿度数据

标准外设库下,把printf重定向到USART1是最方便的:

int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }

然后主循环里一行输出:

printf("Humidity: %d.%d %% Temperature: %d.%d C\r\n", humidity, 0, temperature, 0);

这样格式化输出确实方便,但有两个注意点:printf默认启用MicroLIB时占用的代码空间更小,缺点是浮点支持有限;如果你不需要浮点,就用整数拼接输出,避免引入float库导致Flash紧张。另外\r\n要一起写,否则部分串口终端会只换行不回车,输出叠在一起。

4.3 串口乱码的排查顺序

串口乱码问题是STM32F1项目里最常被问到的,我按优先级排列排查方向:

  • 波特率不匹配:检查串口助手和代码实际配置是否一致,尤其注意外置晶振频率。如果板子上焊的是12MHz晶振,代码里还按8MHz算PLL,实际系统时钟就是错的,波特率自然乱。
  • 时钟分频配置错误:USART1挂在APB2(72MHz),USART2挂在APB1(36MHz)。很多人把USART2的波特率计算套APB2的时钟,结果波特率偏差翻倍。DHT11项目里如果两个串口都用了,要小心这个。
  • 电平不匹配:STM32F1的IO是3.3V逻辑电平,如果接的是5V的USB转TTL模块,部分模块逻辑电平不兼容会导致通信偶尔出错。稳妥起见选支持3.3V电平的模块,或者加电平转换。
  • GND没共地:USB转TTL和板子不接GND,串口基本必乱。

5. 联调实测:从时序异常到稳定运行的完整排错过程

5.1 第一次读取全0xFF:线索在波形上,不在代码里

我在DHT11项目上第一次调通后,把引脚焊到了另一块板子的PB1,结果读回来全0xFF。一开始以为是代码问题,反复改延时、改GPIO配置,折腾一晚上没有任何进展。后来把逻辑分析仪夹到DATA脚和GND之间采波形,才发现问题根本不是代码:是那根杜邦线接触不良,导致引脚实际是空接状态。STM32F1的GPIO配置成上拉输入时,内部上拉电阻约40kΩ,DHT11和主机之间靠这根线传输电平,接触电阻一大,波形边沿变得很钝,采样点判断逻辑0/1连续出错。换了一根短线,同一套代码立刻正常。

这个经历给我的教训是:DHT11这类低速单总线器件,排错优先查物理连接,波形图比任何打印日志都可靠。逻辑分析仪不贵,哪怕是入门级的24MHz采样率也足够看DHT11时序了,一定要备一个。

5.2 校验和偶尔失败:供电纹波和上拉电阻的取舍

另一个常见问题是校验和偶尔失败,比如连续读100次有3到5次返回错误。代码没问题、连接没问题,就要看电源和上拉。DHT11的DATA引脚建议接4.7kΩ到10kΩ外部上拉到3.3V,只靠单片机内部上拉,边沿在长线场景下不够陡峭。我实测在同一块板上,外部上拉从10kΩ换成4.7kΩ后,错误率明显下降。另外DHT11的供电芯片最好加一个0.1μF去耦电容,如果共用3.3V且电源上有较大纹波,数据位的判断阈值会受到干扰。

5.3 数据更新频率:DHT11不是越快越好

DHT11的温度和湿度采样周期是1Hz,也就是每秒更新一次内部测量值。就算你每秒读10次,读到的也是同一份内部数据。有一次我为了让串口刷新更流畅,把主循环读DHT11的间隔缩短到200ms,结果每三四次读取就有一次校验失败。翻手册才发现,连续两次读取间隔至少要1秒,特别是上一次读完后立刻再发起起始信号,传感器可能还没准备好。改成每1.2秒读一次后,连续跑了48小时,校验失败次数为零。这里千万不要做"读失败就立刻重试"的逻辑,失败后最好等待1秒再发起下一次读取,否则可能反复失败,看起来就像硬件坏了。

5.4 我的最终轮询架构和容错策略

最终我采用的是这样的主循环结构:

  • 主循环里每1秒拉高一个标志位,触发一次DHT11读取
  • 读取函数返回0才解析数据,返回非0则计数并保留上一次有效数据
  • 连续失败超过5次才把数据置为无效,避免单次偶发错误导致界面显示"--"
  • DHT11数据校验失败和超时错误分不同错误码,方便串口打印定位

这样处理下来,运行稳定性比"每次读失败就panic重置传感器"的写法高很多。传感器在恶劣天气下、供电波动时偶尔抽风是正常现象,软件容错能兜住这些偶发问题。

6. 几个值得养成的习惯:写在联调之外

代码能跑通之后,我发现最提升效率的其实是一堆小习惯。比如DHT11的读取代码里所有延时函数、GPIO配置都独立成文件,不改主程序;比如串口日志统一加时间戳,方便事后看数据曲线,我在PC端用的串口工具支持数据记录CSV,直接把温湿度存下来,配合Excel画曲线,能直观看到传感器响应速度是否正常。再比如DHT11引脚尽量选带引出的、远离电源噪声的IO,避免反复重焊。

功耗方面如果做电池供电产品,DHT11和STM32F1都支持低功耗模式:DHT11在测量间隔期间可以暂停供电,读间隔结束再唤醒,但注意从断电状态重新上电后要先等一秒,等传感器内部稳定再发起读取。这一点在参考设计时经常被忽略,实际做低功耗项目时却很关键。

如果你打算长期在这条路上走,我的建议是别只停留在跑通例程。把DHT11的驱动自己按寄存器重写一遍,再试着换成DHT22或DS18B20,感受不同单总线协议的差异;然后给温湿度数据加个简单的Modbus-RTU报文,通过串口接到上位机。STM32F1系列这颗芯片的通用性很强,作为学习平台能把GPIO、定时器、串口、中断、ADC这些外设串起来,以后换STM32F4或G0系列时,很多思路是相通的。

每次重新用回DHT11这颗老传感器,我都会想起第一次用逻辑分析仪看时序波形时的感觉——所有拉高拉低的长短在那里一目了然,之前文档里读不懂的"26微秒表示0,70微秒表示1"变成了屏幕上肉眼可见的方波。从当时反复调延时的懵懂,到现在几分钟定位问题,差的不是别的东西,就是实际做一遍、跑一遍、踩一遍坑的耐力。嵌入式这行没有捷径,但走过的弯路都会变成你下一次调程序时的直觉。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/30 1:08:04

硬件I2C与软件I2C选型实战:从OLED点不亮到稳定通信的避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/30 1:07:46

PyTorch DDP多卡训练实战:从单卡到4卡DDP避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/30 1:06:50

基于STM32F103C8T6的实验室巡检遥控小车设计与实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/30 1:06:40

全人类AI发展路径:技术民主化与公平性建模

1. 项目背景与问题定义2026年美赛F题直指人工智能发展中最具争议性的命题——是否应该以及如何发展"全人类人工智能"(Humanity-wide AI)。这个看似简单的问题背后&#xff0c;涉及技术伦理、社会公平、经济影响和全球治理等多维度复杂因素。作为数学建模竞赛题目&…

作者头像 李华
网站建设 2026/10/30 1:04:44

TIA Portal中LAD/FBD/STL/SCL注释技巧与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/30 1:03:11

被忽略的工业软件:自动控制的核心与工程实践反思

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华