news 2026/10/7 7:49:33

STM32F1与DHT11实战:从底层原理到温湿度监测项目

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F1与DHT11实战:从底层原理到温湿度监测项目

1. 为什么STM32F1到今天还值得花时间学

1.1 一颗“老芯片”的生存逻辑

如果你最近在选型或者准备入门嵌入式,大概率会刷到一堆推荐:什么国产替代、什么Cortex-M4、什么RTOS加WiFi6。但只要你翻一翻淘宝销量、看看各大论坛的新手提问区,就会发现一个反常识的现象——STM32F1系列依然是出货量最大、教程最多、踩坑记录最全的32位MCU之一。尤其是搭配DHT11温湿度传感器做环境监测这个组合,几乎成了电子类专业学生和转行嵌入式的人绕不开的“新手村任务”。

我自己第一次接触STM32F1是在一块最小系统板上,芯片型号是STM32F103C8T6,48个引脚,64KB Flash,20KB RAM,主频72MHz。当时觉得这参数放到今天简直寒酸,但真正用起来才发现,对于绝大多数控制类、采集类、显示类的项目,它完全够用,而且便宜、好买、资料多到令人发指。一块核心板不到十块钱,DHT11模块两三块,加起来一顿早餐钱就能跑通一个完整的“采集-处理-显示”链路。

这篇文章不打算写成数据手册的复读机,而是想从一个实际做过多个F1项目的人的角度,把STM32F1的核心架构、开发方式选择、DHT11驱动细节、常见翻车现场这几个事情讲透。不管你是刚学完51单片机想往上走一步,还是已经工作但一直用库函数“糊代码”想补补底层,应该都能找到对你有用的东西。

1.2 谁适合看这篇,能解决什么问题

先说清楚受众。如果你是完全零基础,连C语言指针和结构体都还没搞明白,那建议先补一下C语言,否则看寄存器操作会很痛苦。但如果你满足下面任意一条,这篇内容就是为你准备的:

  • 学过51或者Arduino,想系统性地掌握一款32位MCU;
  • 工作中需要用到STM32F1做产品,但一直用CubeMX生成代码,想搞清楚底层到底发生了什么;
  • 做课设或者项目要用DHT11采集温湿度,结果时序调不通、数据一直显示85或者0;
  • 想从标准库迁移到HAL库,或者反过来,不知道两者到底差在哪。

我会从芯片选型、开发环境搭建、GPIO与时钟体系、DHT11单总线时序、串口打印调试、常见故障排查这几个维度展开。每个部分都会给出可以直接复现的操作步骤和参数计算过程,而不是只丢一句“配置一下就行”。尤其是DHT11的时序部分,我会把微秒级延时的计算方法、中断干扰的处理、校验失败的排查思路全部写出来,这些都是我在实际项目里踩过坑之后总结的。

2. STM32F1的芯片家族与选型思路

2.1 型号命名规则拆解

STM32F1不是一个芯片,而是一个系列。拿最常见的型号STM32F103C8T6来拆:

字段含义具体到本例
STM32产品家族意法半导体32位MCU
F产品类型基础型
103子系列增强型,带USB和CAN
C引脚数48引脚
8Flash容量64KB
T封装LQFP
6温度范围-40到85摄氏度

同系列里还有STM32F103RCT6(64引脚、256KB Flash)、STM32F103ZET6(144引脚、512KB Flash)等等。对于DHT11这种单总线传感器项目,C8T6完全足够,甚至用STM32F101这种基础型也行,只是101没有USB和CAN,主频也低一些。

选型的时候有个经验:先看引脚数够不够,再看Flash和RAM,最后看外设。很多人一上来就买ZET6这种大板子,结果焊起来麻烦、体积大、成本高,实际项目里根本用不到那么多IO。我个人的建议是,新手入门买C8T6最小系统板,做产品再根据实际外设需求往上选。

2.2 核心资源与时钟树

STM32F103C8T6的核心资源如下:

  • 内核:ARM Cortex-M3,最高72MHz;
  • Flash:64KB;
  • SRAM:20KB;
  • GPIO:37个可用IO;
  • 定时器:3个通用定时器(TIM2/3/4)、1个高级定时器(TIM1);
  • 通信接口:2个SPI、2个I2C、3个USART、1个USB、1个CAN;
  • ADC:2个12位ADC,共10个通道。

时钟树是F1的精华也是难点。简单说,外部晶振一般是8MHz,经过PLL倍频到72MHz作为系统时钟。具体路径是:8MHz → 除以1 → 乘以9 → 72MHz。这个72MHz再分频给AHB、APB1、APB2。APB1最高36MHz,APB2最高72MHz,所以挂载在APB1上的外设(如TIM2/3/4、USART2/3)时钟频率要注意,配置波特率的时候如果算错了,串口就会出乱码。

我见过太多人串口打印乱码,最后发现是把APB1的时钟当成72MHz去算波特率了。记住一句话:USART1挂在APB2上,USART2和USART3挂在APB1上,前者72MHz,后者36MHz。

3. 开发方式的选择:标准库、HAL库还是寄存器

3.1 三种方式的真实对比

这是每个学F1的人都会纠结的问题。我用一个表格把三种方式说清楚:

对比项寄存器标准库HAL库
代码量最少中等最多
可读性差好好
移植性差差好
学习曲线陡平缓平缓
调试难度高中低
官方支持无已停止持续更新

标准库(SPL)是意法半导体早期推出的,代码直观,比如GPIO_SetBits(GPIOA, GPIO_Pin_0)一眼就能看懂。但官方从2015年左右就停止更新了,新芯片不再支持。HAL库是现在的主流,配合CubeMX可以图形化配置,生成代码快,但封装层次多,出了问题不好查。

我的建议是:新手先用标准库把GPIO、定时器、串口、中断这几个基础外设摸一遍,理解寄存器是怎么被操作的,然后再转HAL库做项目。如果一上来就用CubeMX点几下生成代码,很容易变成“只会点鼠标”的状态,遇到时序问题就懵了。

3.2 开发环境搭建的实操步骤

以Keil MDK为例,搭建一个F1工程需要这几步:

  1. 安装Keil MDK,版本建议5.30以上,太老的版本对Win11兼容性差;
  2. 安装器件支持包,去Keil官网下载Keil.STM32F1xx_DFP,双击安装;
  3. 下载标准库,从ST官网找STM32F10x_StdPeriph_Lib_V3.5.0,这是最后一个标准库版本;
  4. 新建工程,选择芯片型号STM32F103C8;
  5. 添加启动文件,从库里的startup/arm目录复制startup_stm32f10x_md.s(md表示中等容量);
  6. 配置头文件路径,把Library和User目录加进去;
  7. 定义宏,在C/C++选项卡里加上USE_STDPERIPH_DRIVER, STM32F10X_MD。

这七步里最容易出错的是第5步和第7步。启动文件选错了(比如选了hd高容量版本),编译能过但运行会跑飞;宏定义没加,会报一堆assert_param相关的错误。我建议把这几步截图保存,下次建工程直接照着做,能省很多时间。

提示:如果你用的是CubeMX加HAL库,步骤会简化很多,但生成的工程默认用内部晶振,需要手动在Clock Configuration里改成外部晶振并配置PLL,否则主频只有8MHz,DHT11的微秒延时算出来会差很多。

4. DHT11温湿度传感器的驱动细节

4.1 单总线协议的时序本质

DHT11用的是单根数据线完成双向通信,这种协议叫单总线。它的核心难点在于所有时序都是微秒级的,而且对时间精度要求比较高。一次完整的读取流程是这样的:

  1. 主机拉低总线至少18ms,然后拉高20-40us,作为起始信号;
  2. DHT11检测到起始信号后,拉低总线80us,再拉高80us,作为响应;
  3. 之后DHT11连续发送40位数据,每一位以50us低电平开始,高电平的持续时间决定是0还是1;
  4. 高电平持续26-28us表示0,持续70us表示1;
  5. 最后一位数据发完后,DHT11拉低总线50us,然后释放。

这里的关键是区分0和1的阈值。理论上26-28us是0,70us是1,但实际测量会有波动。我一般用40us作为分界:高电平持续时间大于40us判为1,小于40us判为0。这个阈值在大多数DHT11上都能稳定工作。

4.2 微秒延时的实现与误差控制

在72MHz的F1上,用SysTick或者定时器做微秒延时都可以。用SysTick的话,配置成1us中断一次,但中断本身有开销,实际延时会有偏差。更可靠的做法是用定时器计数或者空循环。

空循环的写法需要根据主频计算循环次数。比如72MHz下,一个指令周期约13.9ns,但C语言一条语句编译后可能对应多条指令。我一般用示波器实测校准,先写一个大概的循环,然后测量实际延时,再调整循环次数。下面是一个经过校准的微秒延时函数示例:

void delay_us(uint32_t us) { uint32_t i; while(us--) { for(i = 0; i < 8; i++) // 72MHz下约1us,需实测校准 { __NOP(); } } }

注意这个8不是固定的,不同编译器优化等级下结果不同。建议用示波器或者逻辑分析仪测一下实际波形,把循环次数调到延时准确为止。如果没有仪器,可以用串口打印时间戳来粗略验证。

4.3 完整的读取函数与校验逻辑

DHT11一次返回40位数据,格式是:湿度整数8位 + 湿度小数8位 + 温度整数8位 + 温度小数8位 + 校验和8位。校验和等于前四个字节相加的低8位。下面是一个完整的读取函数框架:

uint8_t DHT11_Read(uint8_t *temp, uint8_t *humi) { uint8_t buf[5]; uint8_t i, j; // 主机发送起始信号 DHT11_Out(); DHT11_Low(); delay_ms(20); DHT11_High(); delay_us(30); DHT11_In(); // 等待DHT11响应 if(DHT11_WaitLow(100) != 0) return 1; if(DHT11_WaitHigh(100) != 0) return 2; // 读取40位数据 for(i = 0; i < 5; i++) { buf[i] = 0; for(j = 0; j < 8; j++) { if(DHT11_WaitHigh(100) != 0) return 3; delay_us(40); buf[i] <<= 1; if(DHT11_ReadPin() == 1) { buf[i] |= 1; if(DHT11_WaitLow(100) != 0) return 4; } } } // 校验 if(buf[4] != (buf[0] + buf[1] + buf[2] + buf[3])) return 5; *humi = buf[0]; *temp = buf[2]; return 0; }

这个函数里每个return对应一种错误:1是响应超时,2是响应高电平超时,3是数据位高电平超时,4是数据位低电平超时,5是校验失败。实际调试时把返回值通过串口打印出来,能快速定位问题出在哪一步。

5. 串口调试与数据可视化

5.1 串口配置的关键参数

用USART1做调试输出,配置如下:

  • 波特率:115200;
  • 数据位:8;
  • 停止位:1;
  • 校验位:无;
  • 硬件流控:无。

在标准库里的配置代码:

USART_InitTypeDef USART_InitStructure; USART_InitStructure.USART_BaudRate = 115200; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, &USART_InitStructure); USART_Cmd(USART1, ENABLE);

波特率的计算依赖于APB2的时钟频率。如果系统时钟是72MHz,USART1挂在APB2上,那么USARTDIV = 72000000 / (16 * 115200) = 39.0625,实际寄存器值取39,小数部分0.0625乘以16等于1,所以BRR寄存器写入0x271。这些计算标准库的USART_Init函数会自动完成,但你要确保RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE)被调用了,否则串口没时钟,什么也发不出来。

5.2 用printf重定向到串口

标准库工程里用printf需要重定向fputc函数:

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

然后在Keil的Target选项里勾选Use MicroLIB。如果不勾选,需要自己实现_sys_exit等底层函数,比较麻烦。MicroLIB是Keil提供的一个精简C库,专门用于嵌入式,勾上就行。

重定向之后,就可以用printf("Temp: %d C, Humi: %d %%\r\n", temp, humi)这样的语句输出数据了。注意DHT11的湿度是整数,温度也是整数,小数部分在DHT11上通常是0,所以直接用%d就行。

5.3 数据上云与本地显示的扩展思路

串口打印只是最基础的调试手段。实际项目里,你可能需要把数据传到上位机或者显示在屏幕上。几个常见的扩展方向:

  • OLED显示:用I2C接口的0.96寸OLED,把温湿度实时显示出来,适合做桌面气象站;
  • 上位机曲线:用Python的pyserial库读取串口数据,配合matplotlib画实时曲线;
  • 数据记录:加一个SD卡模块,用SPI接口把数据按时间戳写入CSV文件;
  • 无线传输:加一个蓝牙或者WiFi模块,把数据发到手机或者云平台。

这些扩展里,OLED显示是最适合练手的,因为I2C协议本身也是F1的必修课。我建议先把DHT11读通,再加OLED,一步一步来,不要一上来就搞无线传输,否则出了问题不知道是传感器的问题还是通信模块的问题。

6. 常见问题与排查技巧实录

6.1 DHT11数据一直显示85或0

这是最经典的问题。85这个值其实是DHT11上电后的默认值,说明根本没有读到有效数据。排查顺序如下:

  1. 检查供电:DHT11的工作电压是3.3V到5.5V,但很多模块是5V供电的,如果MCU是3.3V,数据线电平可能不匹配。建议用3.3V给DHT11供电,或者在数据线上加电平转换;
  2. 检查上拉电阻:DHT11的数据线需要4.7K到10K的上拉电阻,很多模块自带,但如果自己接线,一定要加上;
  3. 检查延时精度:用示波器看起始信号的低电平时间是不是18ms以上,响应信号的80us低和80us高是不是准确;
  4. 检查引脚配置:读取数据前要把引脚从输出模式切换到输入模式,而且输入模式要配置成浮空输入或者上拉输入,不能是模拟输入。

我遇到过最坑的一次是上拉电阻没接,数据线一直悬空,读出来全是0。后来加了一个4.7K电阻到3.3V,立刻正常。

6.2 串口打印乱码

乱码的原因基本只有一个:波特率不对。排查步骤:

  • 确认系统时钟是不是72MHz,用RCC_GetClocksFreq函数打印出来看看;
  • 确认USART挂在哪个总线上,USART1是APB2,USART2/3是APB1;
  • 确认外部晶振是不是8MHz,有些板子用的是12MHz晶振,PLL倍频参数要改;
  • 确认串口助手的波特率和代码里一致。

还有一种情况是晶振没起振,MCU自动切换到内部8MHz RC振荡器,这时候系统时钟不是72MHz,波特率自然就错了。用示波器测一下晶振引脚有没有波形,或者读RCC_GetFlagStatus(RCC_FLAG_HSERDY)看看高速外部晶振有没有就绪。

6.3 程序跑飞或进入HardFault

HardFault是Cortex-M3的异常,通常由以下几种原因引起:

原因排查方法
数组越界检查数组下标,尤其是字符串操作
空指针解引用检查指针是否初始化
堆栈溢出增大启动文件里的Stack_Size
中断优先级配置错误检查NVIC_PriorityGroupConfig
访问了未使能时钟的外设检查RCC_APBxPeriphClockCmd

我调试HardFault的常用方法是在HardFault_Handler里加一个死循环,然后看LR寄存器的值,通过LR判断是从哪个函数跳进来的。Keil的调试模式下,可以在Registers窗口看到LR和PC的值,结合反汇编定位问题代码。

6.4 常见问题速查表

现象可能原因解决方法
DHT11返回85未读到数据检查上拉电阻和延时
DHT11返回0时序错误用示波器校准延时
校验失败数据位读取错误调整0/1判定阈值
串口乱码波特率错误检查时钟配置
程序不运行启动文件选错换成md版本
编译报错assert_param宏定义缺失加USE_STDPERIPH_DRIVER
中断不触发NVIC未配置检查优先级和使能
定时器不准时钟频率算错确认APB1/APB2频率

提示:DHT11的采样率不能太高,官方建议间隔不小于1秒。如果连续快速读取,传感器可能不响应或者返回上一次的数据。我在代码里一般加一个delay_ms(1500),确保两次读取之间有足够的间隔。

7. 从F1出发的进阶路线

7.1 什么时候该换芯片

STM32F1虽然够用,但有些场景确实力不从心:

  • 需要浮点运算:F1没有FPU,做PID或者滤波算法时浮点计算很慢,这时候可以考虑F4系列;
  • 需要更大RAM:F1最多20KB RAM,跑RTOS加网络协议栈会很吃力;
  • 需要高速ADC或DAC:F1的ADC采样率1MHz,F4可以到2.4MHz;
  • 需要以太网:F1没有内置MAC,F4和F7有。

但如果你只是做温湿度采集、继电器控制、串口通信这类任务,F1完全够用,而且成本优势明显。不要为了“先进”而换芯片,要根据实际需求来。

7.2 从裸机到RTOS的过渡

裸机程序写多了,会遇到一个问题:多个任务互相阻塞。比如DHT11读取需要20ms,这期间如果还要处理按键和显示,就会卡顿。这时候可以引入RTOS,比如FreeRTOS。

在F1上跑FreeRTOS是可行的,20KB RAM够创建几个任务。移植步骤:

  1. 下载FreeRTOS源码,把Source目录下的文件加入工程;
  2. 复制FreeRTOSConfig.h到User目录,根据F1的资源修改配置;
  3. 实现vApplicationStackOverflowHook和vApplicationMallocFailedHook;
  4. 配置SysTick和PendSV中断优先级为最低;
  5. 创建任务,启动调度器。

我建议先把裸机程序写稳定,再上RTOS。如果裸机都调不通,上RTOS只会让问题更复杂。而且F1的资源有限,任务不要建太多,一般3到5个就够了。

7.3 项目实战:桌面温湿度监测站

最后给一个完整的项目思路,把前面讲的东西串起来:

  • 硬件:STM32F103C8T6最小系统板 + DHT11 + 0.96寸OLED + 蜂鸣器;
  • 功能:每2秒读取一次温湿度,显示在OLED上,温度超过30度蜂鸣器报警;
  • 软件架构:主循环轮询DHT11,定时器中断刷新OLED,蜂鸣器用GPIO控制;
  • 调试手段:串口打印每次读取的原始数据和校验结果。

这个项目麻雀虽小五脏俱全,涵盖了GPIO、定时器、中断、I2C、单总线、串口这几个核心外设。做完之后,你对F1的掌握程度会有一个质的飞跃。我在带新人的时候,基本都用这个项目作为考核,能独立做出来并且稳定运行一周不出问题的,就可以放手去做更复杂的任务了。

我个人在实际操作中的体会是,STM32F1就像一把用了很多年的螺丝刀,看起来不起眼,但顺手、可靠、坏了不心疼。DHT11虽然精度一般,但胜在便宜和简单,适合用来验证想法。真正把这两个东西吃透,比盲目追新芯片要有价值得多。

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

ESP32芯片与模组怎么选?从SoC概念到选型实战全解析

做硬件这么久&#xff0c;我经常被问到同一个基础得不行但又特别容易绕晕的问题&#xff1a;ESP32 到底是买芯片还是买模组&#xff1f;有人拿着淘宝买回来的 ESP-WROOM-32 模组&#xff0c;以为这就是 ESP32 芯片&#xff1b;也有人图省事直接买了裸芯片回来自己画板&#xff…

作者头像 李华
网站建设 2026/10/7 7:48:39

Loop Engineering 的代价:LLM 可用性靠 TaoToken 统一 Key 买出来

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

作者头像 李华
网站建设 2026/10/7 7:48:30

mongoose 中文排序问题:用 collation 与 locale 让 sort 按拼音生效

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

作者头像 李华
网站建设 2026/10/7 7:48:22

裸机与RTOS之争:单片机多任务处理如何选型与迁移实践

“单片机不搞RTOS&#xff1f;你他妈怎么跟人拼多任务处理&#xff1f;”——这话火药味十足&#xff0c;但放到嵌入式圈子里&#xff0c;其实就是老鸟们隔三差五就要吵一架的核心问题。一边是“我裸机状态机照样跑十个功能”的实战派&#xff0c;一边是“你东西一复杂就等着屎…

作者头像 李华
网站建设 2026/10/7 7:48:21

Linux 一键安装 Hermes Agent:用 TaoToken 统一 Key 打通本地 Agent 调用链

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

作者头像 李华