
搞嵌入式这几年DHT11 应该是我用过最“亲民”的传感器了——成本低、接线少、代码量也不大STM32 教学里十个项目有八个都从它开始。但说它“简单”也不完全对真正上手你会发现时序、上拉、延时、GPIO 模式切换任何一个环节没处理好读回来的数据就是一团乱麻。这篇教程我就把这颗传感器的完整玩法拆开讲清楚从硬件选型、电路设计到标准库和 HAL 库两套代码实现再到实际项目中常见的坑和排查思路希望能帮你少走点弯路。DHT11 属于单总线数字温湿度传感器一条数据线既能发命令又能收数据省引脚是它最大的优势但代价就是时序要求比较严格对 MCU 端微秒级延时和 GPIO 方向切换的功底是个考验。适合 STM32 初学者学习时序协议也适合做智能家居、鱼缸温控、环境监控这类不需要太高精度的项目。1. 项目概述与选型思路1.1 DHT11 到底是什么能做什么DHT11 是一颗集成了温湿度传感元件和数字信号处理电路的复合传感器内部由电容式湿度感应元件、NTC 热敏电阻测温元件以及一个 8 位单片机组成。它输出的不是模拟电压而是经过校准的串行数字信号MCU 直接读数据线就能拿到温度和湿度值省掉了 ADC 采样和线性化换算的麻烦。它的核心参数我列一下温度测量范围 0℃ ~ 50℃精度 ±2℃湿度测量范围 20%RH ~ 90%RH精度 ±5%RH分辨率 1℃ 和 1%RH小数位实际意义不大单总线协议一次传输 40 位数据供电 3.3V ~ 5V典型工作电流约 0.5mA ~ 1mA从参数能看出来DHT11 的定位是“环境级监测”不是“工业级测量”。你拿它判断房间里要不要开加湿器、鱼缸水温是否超标、机房有没有异常升温这些场景足够用了。但如果你想做高精度气象站、培养箱恒温控制这类对误差敏感的项目DHT11 的 ±2℃ 和 ±5%RH 就不太够看了建议直接上 DHT22也叫 AM2302或者 SHT30。1.2 为什么教程普遍选 DHT11什么时候该换 DHT22初学者容易陷入一个误区传感器越便宜越简单越好或者越贵越高级越好。其实选型看的是“够用就行”和“学习价值”。DHT11 之所以成为 STM32 入门标配不只是因为便宜更重要的是它的单总线协议很能锻炼人对时序的敏感度——什么时候拉低、拉低多久、什么时候采样电平、怎么在输入输出模式之间切换这些操作几乎是所有单总线器件的通用套路弄懂了 DHT11以后玩 DS18B20、DHT22 都是同一个思路。那什么时候该换 DHT22很简单当你发现 DHT11 的精度和量程成为项目瓶颈的时候。DHT22 的温度精度是 ±0.5℃湿度精度 ±2%RH量程也大不少价格大概是 DHT11 的三倍左右。不过 DHT22 的时序和 DHT11 是兼容的换传感器只需要改一下初始化参数和解析代码里的数值范围。还有一种思路是用 I2C 接口的 SHT30精度更高、带报警中断适合跟低功耗唤醒逻辑配合但它的驱动难度比 DHT11 高一些。实操阶段我的建议很简单学习用 DHT11项目如果对精度要求明确高于 DHT11 能力直接换 DHT22不要在 DHT11 上花太多时间做补偿校准那不是这颗芯片擅长的事。2. 硬件接线与电路设计2.1 引脚定义与接线方式DHT11 常见封装有两种四针直插裸传感器和四针模块板。裸传感器引脚定义是 VCC、DATA、NC、GND买回来得自己在 DATA 线上加上拉电阻模块板一般已经把上拉电阻和滤波电容做好了还加了电平转换逻辑接起来更方便。以 STM32F103C8T6 最小系统板为例我的惯用接法VCC 接 3.3V不要接 5V原因下面细说GND 接 GNDDATA 接 PB8这个引脚空闲、没有默认复用冲突调试方便DATA 到 VCC 之间接一个 4.7kΩ ~ 10kΩ 上拉电阻接线长度尽量控制在 20cm 以内超过这个距离线间电容和干扰会让单总线的高电平波形变缓读出来的数据就开始出错。我实测过 50cm 杜邦线现象是 10 次读取里偶尔成功一两次一开始还以为是代码问题后来换了短线立刻就好。2.2 上拉电阻和供电几个容易踩的坑关于上拉电阻很多模块板默认已经焊好了你直接用就行。如果是裸传感器DATA 线上必须加上拉电阻否则数据线悬空时电平不确定DHT11 根本无法正常通信。阻值选 4.7kΩ 到 10kΩ 都可以STM32 的 GPIO 内部上拉阻值偏大约 30kΩ ~ 50kΩ直接依赖内部上拉读取 DHT11 通常不太稳定所以尽量还是用外部上拉。供电电压是另一个大头。DHT11 手册标称 3.3V ~ 5V 都能工作但如果你用 STM32 的 3.3V 系统强烈建议给 DHT11 也供 3.3V。原因在于如果 DHT11 用 5V 供电它的数据线高电平接近 5V而 STM32 的 GPIO 多数不能容忍 5V直接接会加大 IO 损坏风险。除非你确认模块板上有电平转换电路或者 STM32 引脚标了 FTFive-volt Tolerant否则别图省事。还要提醒一点DHT11 的采样周期慢手册建议两次读取间隔至少 1 秒最好 2 秒。如果你在主循环里疯狂读取DHT11 会直接不响应。很多人写代码时没注意这个结果就是第一次能读到后面全部超时失败这个坑在下面章节详细展开。3. 单总线协议与时序拆解3.1 通信模型主机发起从机应答DHT11 的通信是典型的半双工单总线模式总线空闲时处于高电平由主机STM32发起一次完整的数据交互。整体流程分四步主机将总线拉低至少 18ms作为起始信号让 DHT11 从待机状态唤醒主机释放总线并拉高 20us ~ 40us然后切换到输入模式等待 DHT11 响应DHT11 检测到起始信号后拉低总线约 80us 作为响应信号再拉高约 80us响应结束后DHT11 连续输出 40 位数据每发送完一位总线会被拉低 50us 进行位间隔同步整个交互过程大约需要 3ms ~ 5ms。理解这个流程很关键因为所有驱动代码都是围绕这个状态机展开的先输出再切换输入然后按时序采样。很多人第一次写 DHT11 驱动失败不是因为协议看不懂而是忽略了一个细节主机发送完起始信号后必须把 GPIO 从推挽输出模式切换成输入模式或者开漏带上拉否则数据线一直被主机驱动着DHT11 根本拉不动总线。这个模式切换是 DHT11 驱动最容易翻车的地方。3.2 每位数据的时序判断DHT11 输出的 40 位数据格式是固定的8 位湿度整数 8 位湿度小数 8 位温度整数 8 位温度小数 8 位校验和。其中湿度小数、温度小数在 DHT11 上通常读出 0但代码里还是要按格式解析不能只读整数位。每一位数据的传输方式是50us 低电平位起始 同步然后是高电平。高电平持续 26us ~ 28us 表示逻辑 0高电平持续约 70us 表示逻辑 1。接收方要做的就是在低电平结束后延时 30us 左右采样总线电平采样到时仍为高说明这一位是 1如果采样到低说明这一位已经结束是 0。这里有三个经验值很重要低电平固定约 50us这是位同步信号逻辑 0 的总周期约 76us ~ 78us逻辑 1 的总周期约 120us判断 0 和 1 的常见方式有两种一种是“延时采样法”在低电平结束后延时 30us 读电平高就是 1低就是 0另一种是“超时测量法”用定时器测量高电平持续时间超过 40us 判为 1否则判为 0。第一种代码简单省资源第二种抗干扰更强适合对稳定性要求高的项目。4. 标准库驱动实现STM32F103 实战4.1 工程准备与延时方案我用 STM32F103C8T6 标准外设库StdPeriph跑通了一套最简驱动下面把完整思路和代码都放上来你可以直接照着搭。工程准备三件事先初始化系统时钟外部 8MHz 晶振倍频到 72MHz再初始化延时函数最后配置 GPIO。这里最大的坑在于延时。DHT11 需要微秒级延时而标准库例程里最常用的Delay_ms()只能做毫秒延时没法处理 30us、50us 这种短延时。所以我的做法是写一个基于 SysTick 的微秒延时函数精确度足够也不会卡住 CPU。SysTick 微秒延时的思路不复杂SysTick 时钟源配置为 HCLK 9 分频72MHz 下就是 8MHz一个 tick 是 0.125us延时 us 个微秒就是us * 8个 tick。但要注意SysTick 的重装载值是 24 位最大 16777215直接延时长导致装载值溢出会有问题。DHT11 时序里最长的是起始信号拉低 20ms算下来需要 160000 个 tick没超上限所以直接用没问题。void delay_init(void) { // SysTick 时钟源HCLK 9 分频即 8MHz SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); } void delay_us(uint32_t us) { uint32_t ticks us * 8; // 8MHz 下每个 tick 0.125us uint32_t start SysTick-VAL; uint32_t current; SysTick-LOAD ticks - 1; SysTick-VAL 0; SysTick-CTRL | SysTick_CTRL_ENABLE_Msk; do { current SysTick-VAL; } while ((start - current) ticks); SysTick-CTRL ~SysTick_CTRL_ENABLE_Msk; } void delay_ms(uint32_t ms) { while (ms--) { delay_us(1000); } }这里有个小细节要提醒SysTick-VAL是递减计数器计算时要注意方向。我习惯用“计数器从 LOAD 开始递减读到当前 VAL 后和起始值比较”的方式循环里必须保证每次读取都新鲜否则延时误差会叠加。4.2 GPIO 配置和输入输出切换DHT11 数据线接在 PB8主机阶段需要输出模式发送起始信号接收阶段需要输入模式读取电平所以代码里要反复切换 GPIO 模式。标准库的 GPIO 配置用GPIO_InitTypeDef我封装了两个宏分别切到推挽输出和浮空输入注意加上拉输入也可以但外部已经有上拉电阻浮空输入就够了。#define DHT11_PORT GPIOB #define DHT11_PIN GPIO_Pin_8 #define DHT11_DQ_H() GPIO_SetBits(DHT11_PORT, DHT11_PIN) #define DHT11_DQ_L() GPIO_ResetBits(DHT11_PORT, DHT11_PIN) void DHT11_GPIO_OUT(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin DHT11_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(DHT11_PORT, GPIO_InitStructure); } void DHT11_GPIO_IN(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin DHT11_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(DHT11_PORT, GPIO_InitStructure); } uint8_t DHT11_DQ_READ(void) { return GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN); }GPIO 速度一定记得设置为 50MHz。如果图省事用默认低速配置输出边沿会变缓起始信号和位信号的电平跳变不够利落读取出错的概率会明显上升。这是我在实际调试中踩过的坑所以特别写出来。MCU 上电后先调用DHT11_GPIO_OUT()把总线置高维持总线空闲状态。初始化阶段不要一上来就发起始信号给传感器留点稳定时间一般延时 100ms 再说。4.3 完整驱动代码核心读取流程分三件事发送起始信号读取 DHT11 响应逐位接收 40 位数据。完整代码我贴出来每一段都有注释方便照着移植。uint8_t DHT11_ReadData(uint8_t *humi, uint8_t *temp) { uint8_t buf[5] {0, 0, 0, 0, 0}; uint8_t i 0; // 主机拉低总线起始信号至少 18ms DHT11_GPIO_OUT(); DHT11_DQ_L(); delay_ms(20); DHT11_DQ_H(); delay_us(30); // 拉高 20us~40us // 切换输入模式准备接收 DHT11 响应 DHT11_GPIO_IN(); // 等待 DHT11 拉低响应起始如果一直高说明没应答 if (DHT11_DQ_READ() 1) { return 1; // 无响应 } // 跳过 80us 响应低电平 while (DHT11_DQ_READ() 0); // 跳过 80us 响应高电平 while (DHT11_DQ_READ() 1); // 接收 40 位数据5 字节 for (i 0; i 5; i) { buf[i] DHT11_ReadByte(); } // 释放总线切回输出拉高 DHT11_GPIO_OUT(); DHT11_DQ_H(); // 校验前 4 字节之和等于第 5 字节 if ((uint8_t)(buf[0] buf[1] buf[2] buf[3]) buf[4]) { *humi buf[0]; *temp buf[2]; return 0; // 成功 } return 2; // 校验失败 } uint8_t DHT11_ReadByte(void) { uint8_t data 0; uint8_t i 0; for (i 0; i 8; i) { // 跳过 50us 低电平 while (DHT11_DQ_READ() 0); // 延时 30us采样高电平中段 delay_us(30); if (DHT11_DQ_READ() 1) { data | (0x80 i); // 高位在前 // 等待高电平结束 while (DHT11_DQ_READ() 1); } } return data; }有一点必须提醒while (DHT11_DQ_READ() 0)这类等待循环如果遇到传感器损坏或者接触不良会变成死循环。实际项目中建议给这些等待加上超时保护比如用一个计数器统计循环次数超过某个阈值就返回错误。教学代码可以简单点但做产品的同学一定要加超时。主函数调用也很简单每 2 秒读一次结果通过串口打印int main(void) { uint8_t humi 0, temp 0; uint8_t ret 0; delay_init(); Serial_Init(); // 串口初始化波特率 115200 DHT11_GPIO_OUT(); DHT11_DQ_H(); delay_ms(100); while (1) { ret DHT11_ReadData(humi, temp); if (ret 0) { printf(温度: %d℃ 湿度: %d%%RH\n, temp, humi); } else { printf(读取失败错误码: %d\n, ret); } delay_ms(2000); // 采样间隔 2s } }5. HAL 库版本读不到数据时先查这里5.1 HAL 库 GPIO 配置差异HAL 库的写法跟标准库不同但核心逻辑一样区别主要在 GPIO 初始化的结构体和 API 名字上。HAL 库的GPIO_InitTypeDef配置时钟用__HAL_RCC_GPIOB_CLK_ENABLE()初始化用HAL_GPIO_Init()读取引脚用HAL_GPIO_ReadPin()。需要注意的是HAL 库切换输入输出模式时要频繁调用HAL_GPIO_Init()而这个函数内部会检查参数合法性、重新配置开销比标准库的GPIO_Init()大不少。DHT11 的位传输是 us 级操作模式切换的开销虽然不至于让时序崩掉但在追求稳定性的场景下建议只在发送起始信号前切换一次输出在开始读数据前切换一次输入不要每一位都来回切换。void DHT11_Pin_Init(GPIO_InitTypeDef *gpio_init) { __HAL_RCC_GPIOB_CLK_ENABLE(); gpio_init-Pin GPIO_PIN_8; gpio_init-Mode GPIO_MODE_OUTPUT_PP; gpio_init-Pull GPIO_NOPULL; gpio_init-Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, gpio_init); } uint8_t DHT11_ReadPin(void) { return HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_8); }HAL 库的GPIO_SPEED_FREQ_HIGH对应高速度初始化后 PMOD 配置变化写代码时不要漏了这一项不然照样会出现信号边沿过缓的问题。5.2 us 级延时的三种实现HAL 库最常见的坑是HAL_Delay()只支持毫秒级延时。DHT11 需要的 30us、50us 毫秒函数根本不满足。三个替代方案方案一DWT 数据观察点延时。DWT 是 Cortex-M3 内核自带的调试组件用 CPU 周期计数做高精度延时不占用定时器精度高代码也短。配置CoreDebug-DEMCR开启 TRCENA然后使能DWT-CTRL的 CYCCNTENA之后DWT-CYCCNT就是 CPU 周期计数器了。72MHz 主频下延时 1us 就是 72 个周期。void DWT_Delay_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; DWT-CYCCNT 0; } void DWT_Delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000); while ((DWT-CYCCNT - start) ticks); }方案二基本定时器延时。用 TIM6 或者 TIM7 做微秒时基初始化时把定时器配成 1MHz 计数频率每次延时只操作 CNT 计数器和标志位不重新配置定时器中断也不用开。这个方案更“正统”适合严格要求不阻塞 CPU 的场景。方案三循环空转。for (i 0; i n; i);这种简单粗暴调试时可以用但依赖编译器优化级别换一个优化选项时序就变了不建议在生产代码里用。我自己的选择是教学代码用 DWT因为代码量最小做复杂项目或需要并行任务时用定时器。5.3 HAL 库驱动要点HAL 库版本的读取流程跟标准库完全一致唯一需要小心的是模式切换 API 的调用频率和超时保护。另外HAL 库项目里SystemCoreClock这个变量如果配置不正确DWT 延时算出来的 ticks 就是错的读时序会整体偏移。遇到过很多人移植代码后发现读取全 0排查半天才发现是系统时钟初始化改成外部高速时钟HSE后SystemCoreClock宏还停留在默认值。一个比较容易被忽略的工作是HAL 库的HAL_GPIO_Init()次数如果太多会影响后续引脚复用。比如 PB8 之前被 I2C 复用切换成 GPIO 之后又切回 I2C 时要重新初始化结构体。建议在程序里把 PB8 注册成专用 DHT11 引脚避免跟其他外设来回抢。6. 数据验证、显示与项目扩展6.1 串口打印和校验代码写完后最直接的验证手段是串口打印。但如果配置不正确串口输出全乱码你很难判断是串口问题还是传感器问题。我的排查顺序是先单独跑一个 GPIO 翻转测试确认主频、延时正常再用示波器或者逻辑分析仪抓 DHT11 数据线波形最后才看串口数值。DHT11 输出的温度范围是 0~50℃如果你在室内读出来 80℃那基本是代码解析错误不用怀疑传感器坏了。校验和失败时错误码为 2这时先查接线和延时一般就能定位。有些人习惯把小数位也显示出来buf[1]是湿度小数buf[3]是温度小数。但 DHT11 的分辨率只有 1小数位长期是 0直接忽略问题不大。如果想显示 0.1 分辨率必须换 DHT22。6.2 接 OLED 显示项目里只用串口打印不够直观很多同学会接一块 0.96 寸 OLED 屏把温度和湿度直接显示出来。OLED 走 I2C 协议用 PB8 和 PB9 吗别急这里有个冲突点很多开发板 PB8/PB9 是 I2C1 的默认引脚如果你 DHT11 用了 PB8OLED 又用 I2C1就会撞车。最简单的办法是 DHT11 换到 PA0 或者 PC13 这类不常用的引脚OLED 的 I2C 占用 PB8/PB9。OLED 显示温度湿度的逻辑很简单读取 DHT11 成功后格式化字符串调用 OLED 显示函数即可。可以把 DHT11 读取和 OLED 刷新放在同一个定时器或者主循环的 1s 周期里注意 DHT11 采样间隔 2sOLED 刷新频率不用跟着它走1s 刷一次就行。6.3 结合智能家居/鱼缸场景扩展DHT11 最常见的项目落地场景是环境监控智能鱼缸需要水温报警和自动加热控制智能台灯可以根据环境温度调整色温宿舍智能控制器用温湿度数据控制加湿器和风扇。一个比较经典的扩展方式是加继电器和阈值逻辑温度超过上限继电器闭合启动风扇湿度低于下限继电器闭合启动加湿器同时加蜂鸣器做超限报警硬件上就是 STM32 驱动继电器模块用 GPIO 输出控制继电器通断主循环里每 2 秒读一次 DHT11判断阈值后执行对应动作。这里有一个容易忽略的点继电器线圈是感性负载如果驱动电路没有加续流二极管关断瞬间会产生反向电动势轻则干扰 DHT11 通信重则烧坏 GPIO 口。买继电器模块时看一眼电路板基本成熟的模块都带了续流保护电路但自己搭分立电路时一定要记得加。7. 常见问题与排查技巧实录7.1 读回来全是 0xFF 或者全是 0全 0xFF 的典型原因是数据线一直保持高电平DHT11 根本没把总线拉低。重点排查四点接线是否接反、上拉电阻是否缺失、GPIO 是否成功切换成输入模式、DHT11 引脚是否被复用到其他外设。全 0 的话基本都是起始信号没发出去检查发送起始信号时 GPIO 是不是推挽输出模式以及拉低时间是否够 18ms。如果代码没问题拿万用表量一下 VCC 和 GND 之间有没有 3.3V 电压很多模块是坏的或者虚焊上电后一点反应都没有。7.2 有时能读有时卡死“偶尔读成功一次”这个问题90% 是时序不稳定造成的。先从波形入手用逻辑分析仪抓 DHT11 数据线的波形如果低电平不是标准的 50us或者高电平时间明显偏短偏长说明延时函数不准。我用过的开发板里因为主频晶振配置错误导致延时偏差的情况不少见检查一下SystemCoreClock是否和实际主频一致。另外要检查读取间隔。DHT11 对采样间隔有硬性要求连续读取间隔不能小于 1 秒否则传感器内部还在处理上一次数据根本不响应。这种问题最隐蔽——第一次能读到循环里第二次、第三次就失败。解决办法读取一次之后至少delay_ms(2000)再尝试下一次。7.3 湿度数值明显偏大或温度不对湿度偏大通常不是传感器问题而是探头被放在密闭小空间、靠近加湿器出风口、或者手触摸了感应区域。DHT11 自身不带通风结构周围空气流通差时湿度读数会明显偏高。温度读数偏差大先检查 DHT11 是否离 STM32 或电源模块太近——板载 LDO、主控芯片工作发热会直接影响附近测温元件我做过一个项目DHT11 放在 STM32 旁边温度读数持续比室温高 3℃~4℃后来把探头延长线拉出来才解决。如果温度读取值忽高忽低还有一种可能是代码里把 buf[2] 当成了有符号数处理。DHT11 温度值只有正值负数场景一般用 DHT22如果强行按int8_t解析本来就超过 0~50℃ 范围的数据会变成负值看起来像“乱跳”。7.4 常见问题速查表现象可能原因解决办法读回全 0xFF数据线一直为高DHT11 未拉低查接线、上拉、GPIO 输入模式和引脚复用读回全 0起始信号未发出或 GPIO 模式不对检查推挽输出、拉低时间 ≥18ms第一次读到后面全失败采样间隔不足 1s主循环加 2s 延时偶尔读错、校验失败延时不准、线太长、供电不稳逻辑分析仪抓波形换短线3.3V 供电温度始终偏高靠近发热源或主板探头远离 MCU/LDO湿度明显偏大通风不畅或探头受潮改善探头位置程序卡死在 while 等待传感器损坏或接触不良加超时保护检测硬件再分享一个排错小技巧换传感器的时候如果新传感器插上去还是同样的问题先别急着怀疑芯片拿万用表打一下数据线对地电压正常空闲状态应该在 3.3V 左右。如果测量出来是 1.5V 这种中间电平说明上拉电阻阻值选小了或者模块板上的电阻坏了换一个 4.7kΩ 的电阻基本能解决。8. 从 DHT11 出发下一步还能玩什么学会了 DHT11 的驱动你会发现单总线协议并不是这个项目最难的部分难的是“如何让一套驱动在各种环境下稳定运行”。我个人的经验是把 DHT11 的读取函数封装成独立模块接口只暴露DHT11_ReadData()这个函数内部逻辑不要跟主业务耦合这样后续换上 DHT22甚至换成 I2C 接口的 SHT30上层代码都不需要改。接下来可以尝试的方向很明确一是把现有驱动改成支持多传感器比如同时监控室内和室外的温湿度二是接入 RTOS把 DHT11 读取放到独立任务里用消息队列把数据传给显示任务和报警任务三是把 DHT11 数据通过 ESP8266 或者 ESP32 上传到云平台做成手机端可查看的物联网节点。最后分享一个我在实际项目中的心得DHT11 这类低精度传感器不要在算法上过度纠结精度补偿做到“稳定读取 及时报警”就够了。真到了需要高精度数据的环节换传感器比写补偿算法划算得多。这篇教程基于 STM32F103 标准库和 HAL 库分别展开重点都在时序和实操细节上你照着搭完一遍后续再去玩 DHT22、DS18B20 这些传感器上手会快很多。