
简介面向嵌入式初学者的STM32温湿度与距离测量完整工程源码适合单片机开发者学习如何集成DHT11和HC-SR04传感器实现环境参数采集、LCD显示及超限LED报警。资源共203个文件以C源码、头文件及Keil工程文件为主包含可烧录的hex、调试映射map、配置备份及多版本工程记录整体仅3.37MB便于快速导入并对照源码学习。目前已有526人学习可用于课程设计、毕业设计或实战练手。工程中详细展示了STM32的GPIO、定时器、串行通信及中断配置方法覆盖DHT11单总线时序解析与HC-SR04回波时间测量并包含LCD显示屏驱动和阈值比较后点亮LED的完整逻辑。内容预览中还可见多个.bak备份和调试输出文件能够帮助读者了解开发排错过程提升基于ARM Cortex-M内核的嵌入式系统设计能力。1. 从“测得到”到“测得稳”STM32温湿度距离测量的三个核心问题一块屏幕同时显示温度、湿度和小风扇到水箱液面的距离听起来只是把 DHT11 和 HC-SR04 接上同一块 STM32 而已。实际把两个模块放到同一个工程后你会发现真正的难点不在“读数据”而在“谁先读、读多快、超时怎么办”。温湿度传感器是单总线时序对 GPIO 翻转时机极其敏感超声波模块靠回波脉宽换算距离一旦被中断或调度延迟测出来就是跳变。STM32 在两个模块之间协调时序、处理超时和滤波才是这个选题真正要解决的问题。这篇内容按“硬件选型 → 温湿度采集 → 距离采集 → 融合调度”推进用 HAL 库代码把每一步落到命令行和函数级别新手能照着接线上板熟手可以直接跳到第 4 章的定时器捕获实现和第 5 章的排错表。2. 硬件选型与电路温湿度传感器与超声波模块的搭配逻辑2.1 DHT11 / DHT22 / SHT30 怎么选标题写的是“温湿度”能接的传感器不少但不同传感器的接口决定了你在 STM32 上要写多少代码。DHT11 是 1-Wire 单总线数据脚既当输入又当输出时序在微秒级代码里全是循环翻转 GPIODHT22AM2302同样走单总线数据更准但采样间隔要求不低于 2 秒SHT30 走 I2C硬件时序由外设接管代码简洁缺点是价格是前者的好几倍。传感器接口精度温度/湿度采样间隔要求代码成本典型场景DHT11单总线±2℃ / ±5%RH1s 以上高GPIO 模拟时序低成本环境监测、鱼缸、宿舍小制作DHT22单总线±0.5℃ / ±2%RH2s 以上高时序更严格需要相对准确数据的室内监a测SHT30I2C±0.3℃ / ±2%RH无强制低HAL I2C 直接调项目型产品、批量产线我一般给“毕业设计型”项目推荐 DHT11因为后续章节要讲的单总线时序解析本身就是 STM32 GPIO 操作的重要练习如果标题背后是产品预研直接上 SHT30把 I2C 时序交给硬件外设。判断依据只有一个你愿不愿意为准确度付出把微秒级时序调到稳定的调试时间。2.2 HC-SR04 与更短距离的取舍距离测量这边最常用的是 HC-SR04工作电压 5V测量范围 2cm 到 400cm精度标称 3mm实际上受温度影响明显。它只需要一根 Trigger 输入、一根 Echo 输出原理是测回波脉宽代码量小。缺点是它有一个 2cm 左右的盲区且回波接收角度窄对着曲面物体读数会漂。如果测量距离在 1m 以内且要求连续可调很多人改用激光测距模块如 VL53L0X走 I2C没有盲区问题但成本上了一个台阶。这里我按最常见、最便宜的方案处理HC-SR04 负责距离STM32 的定时器负责精确计时后面第 4 章会展开。2.3 接线与供电先解决电平匹配再写代码接线上容易犯的第一个错是STM32F103C8T6 的 GPIO 容忍 5V 输入部分板子的 ADC 引脚却不耐受HC-SR04 的 Echo 输出高电平是 5V直接进 STM32 引脚多数情况能工作因为很多 STM32 引脚标注为 5V 容忍但这不是所有型号都成立。更稳妥的接法是用两个 10kΩ 电阻分压或者用一个 1kΩ2kΩ 电阻串联把 Echo 拉到 3.3V。后面排错表里我会再强调一次因为这是“时好时坏”的经典来源。信号DHT11HC-SR04STM32 引脚示例电源3.3V-5V5V5V/3.3V 对应接入地GNDGNDGND 共地数据DATA无PB0上拉电阻 4.7k触发无TriggerPB1回波无EchoPB2经分压DHT11 的数据线需要一个 4.7kΩ 上拉电阻到 VCC这几乎是单总线器件的标配。上拉电阻缺失时数据线靠内部弱上拉也能出数但传输距离稍长或线缆有一点容性就会连续丢帧表现为读一次成功、读两次超时。2.3.1 供电与滤波的细节两个模块放在同一块面包板上没问题但如果电机、继电器在同一电源上强烈建议给 STM32 板子单独供电。超声波发射瞬间电流可达几十毫安锂电池供电时电压跌落会导致 STM32 复位。常见做法是在模块电源引脚两侧各加一个 100nF 去耦电容靠近模块放置如果电源线超过 20cm再并一个 100μF 电解电容在板端。3. 基于 STM32 读 DHT11单总线时序的 HAL 实现3.1 单总线时序拆解起始、响应与 40bit 数据DHT11 读时序的关键词是“拉低再拉低然后等释放”。主机先把数据线拉低至少 18ms这是起始信号释放后等待传感器响应DHT11 会先把线拉低 80μs再拉高 80μs这是响应信号。随后每个 bit 的格式都一样先拉低 50μs再拉高高电平持续 26-28μs 表示 0持续 70μs 表示 1。这 40bit 数据分别是8bit 湿度整数、8bit 湿度小数、8bit 温度整数、8bit 温度小数、8bit 校验和。校验和方法是前四个字节相加取低 8 位与第五个字节比较。DHT11 时序参数数值说明起始信号拉低18ms最小 18ms可放宽到 20ms传感器响应低80μs自动产生无需干预Bit 0 高电平26-28μs读回时用于判断 0/1Bit 1 高电平70μs超过 60μs 视为 1读取间隔1s 以上连续读会读到上次缓存数据3.2 用 HAL_GPIO 模拟单总线读取代码下面按 HAL 库写Keil5 里新建标准工程后直接复制调整即可。核心思路GPIO 输出模式发送起始信号立刻切换为输入模式用HAL_GetTick()做毫秒级超时用DWT-CYCCNT或简单for循环做微秒级延时。/* DHT11 引脚定义假设接在 PB0 */ #define DHT11_PORT GPIOB #define DHT11_PIN GPIO_PIN_0 /* 微秒级阻塞延时依赖系统时钟 72MHz */ static void delay_us(uint32_t us) { uint32_t ticks us * 72; while (ticks--) { __NOP(); } } /* 将引脚切换为输出且输出低电平 */ static void dht11_set_output_low(void) { GPIO_InitTypeDef gpio {0}; gpio.Pin DHT11_PIN; gpio.Mode GPIO_MODE_OUTPUT_PP; gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DHT11_PORT, gpio); HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_RESET); } /* 将引脚切换为输入模式 */ static void dht11_set_input(void) { GPIO_InitTypeDef gpio {0}; gpio.Pin DHT11_PIN; gpio.Mode GPIO_MODE_INPUT; gpio.Pull GPIO_PULLUP; HAL_GPIO_Init(DHT11_PORT, gpio); } /* 读取 40bit返回 0 表示成功 */ uint8_t dht11_read(float *humidity, float *temperature) { uint8_t data[5] {0}; uint8_t bit 0, byte_idx 0; uint32_t timeout 0; /* 发送起始信号 */ dht11_set_output_low(); HAL_Delay(20); HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_SET); delay_us(30); dht11_set_input(); /* 等待传感器拉低响应信号超时 100ms */ timeout 100000; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET) { if (--timeout 0) return 1; } /* 等待响应信号结束 */ timeout 100000; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_RESET) { if (--timeout 0) return 2; } /* 读取 40bit 数据 */ for (byte_idx 0; byte_idx 5; byte_idx) { for (bit 0; bit 8; bit) { timeout 100000; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_RESET) { if (--timeout 0) return 3; } delay_us(40); /* 在这个位置判断高低电平宽度 */ if (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET) { data[byte_idx] | (0x80 bit); timeout 100000; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET) { if (--timeout 0) return 4; } } } } if (((data[0] data[1] data[2] data[3]) 0xFF) ! data[4]) { return 5; } *humidity data[0] data[1] / 10.0f; *temperature data[2] data[3] / 10.0f; return 0; }代码里的关键点有两个。第一个是delay_us(40)的位置它在每一位数据的前一个低电平结束时开始计时如果 40μs 后引脚仍为高说明该位高电平宽度超过 60μs判定为 1否则为 0。第二个是每个超时循环都返回不同错误码方便你根据返回值判断是“无响应”还是“数据中断”。实际调试中返回 1 或 2 最常见原因基本都是起始信号拉低时间不足或者传感器供电电压低于 3.3V。3.3 校验、超时与错误重试策略DHT11 的校验和只能发现错误不能纠正。我的处理方式是连续读三次至少两次校验通过才采用否则报错。还有一种常见做法是把读取失败当作“数据无效”在 OLED 或上位机上保留上次正确值并标记刷新时间这样 UI 不会闪。3.3.1 从 DHT11 切换到 DHT22DHT22 的时序和 DHT11 几乎一致区别是数据格式里温度高字节的最高位表示负温且湿度分辨率到 0.1。代码只需要修改数据拼接部分uint8_t sign 0; if (data[2] 0x80) { sign 1; data[2] 0x7F; } *temperature sign ? -(data[2] data[3] / 10.0f) : (data[2] data[3] / 10.0f); *humidity data[0] data[1] / 10.0f;因为 DHT22 要求两次读取间隔至少 2 秒所以第 5 章的调度循环里会把它的采样周期单独设置而不是和超声波一样每 200ms 跑一次。4. 距离测量HC-SR04 回波计时与定时器输入捕获实现4.1 从 Trigger 到 Echo超声波测距的物理基础HC-SR04 的测量流程是STM32 给 Trigger 引脚一个 10μs 以上的高电平模块内部发射 8 个 40kHz 的超声波脉冲同时把 Echo 引脚拉高当模块接收到回波Echo 拉低。Echo 高电平的持续时间就是声波往返的总时间。距离计算公式是距离cm Echo 高电平时间μs / 5858 这个系数的来源是声速 340m/s 时往返 1cm 约需 58μs。但声速会随温度变化公式的精确版本在 4.4 节给出。4.2 阻塞式 delay 测量与它的局限最常见的教学写法是拉高 Trigger 10μs然后用HAL_GetTick()或 micros 函数循环等待 Echo 拉高再等待 Echo 拉低中间用delay_us计时。这个写法在单独测距时没问题一旦和 DHT11 的单总线读取放在同一个主循环问题就暴露了Echo 拉高期间如果来了定时器中断中断服务函数里的代码会推迟你读取电平的时机测出来的时间偏长如果用了HAL_Delay误差更大。/* 阻塞式写法只适合单独测距不推荐用在多传感器场景 */ HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_SET); delay_us(15); HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_RESET); while (HAL_GPIO_ReadPin(ECHO_PORT, ECHO_PIN) GPIO_PIN_RESET); uint32_t start DWT-CYCCNT; while (HAL_GPIO_ReadPin(ECHO_PORT, ECHO_PIN) GPIO_PIN_SET); uint32_t time_us (DWT-CYCCNT - start) / 72;这段代码把时间测量建立在 DWT 计数器上不依赖定时器精度尚可。但阻塞期间任何中断都会造成测量误差且如果超声波模块损坏或没有回波while循环会卡死主线程。4.3 定时器 外部中断的非阻塞实现推荐做法用定时器产生微秒级时间基准例如 TIM2 以 1MHz 计数Echo 引脚配置为外部中断拉高时记录当前计数值拉低时再记录一次差值就是高电平持续时间。这个方案的好处是测量期间 CPU 完全释放可以同时执行 OLED 刷新或其他逻辑。/* TIM2 已初始化为 1MHz 递增计数即每 1μs 加 1 */ volatile uint32_t echo_start_tick 0; volatile uint32_t echo_end_tick 0; volatile uint8_t echo_ready 0; /* 触发一次测量Trigger 高电平持续 15μs */ void hcsr04_trigger(void) { echo_ready 0; HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_SET); delay_us(15); HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_RESET); } /* Echo 引脚的外部中断回调放在 HAL_GPIO_EXTI_Callback 里 */ void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin ECHO_PIN) { if (HAL_GPIO_ReadPin(ECHO_PORT, ECHO_PIN) GPIO_PIN_SET) { echo_start_tick __HAL_TIM_GET_COUNTER(htim2); } else { echo_end_tick __HAL_TIM_GET_COUNTER(htim2); if (echo_end_tick echo_start_tick) { echo_ready 1; } else { /* 计数器回绕TIM2 为 32 位几乎不会发生保留处理逻辑 */ echo_ready 1; } } } } /* 在定时器中断或超时判断中检查回波超时 */ uint8_t hcsr04_get_distance_cm(uint32_t *distance_cm) { hcsr04_trigger(); uint32_t timeout HAL_GetTick() 100; while (!echo_ready HAL_GetTick() timeout); if (!echo_ready) return 1; uint32_t diff_us echo_end_tick - echo_start_tick; *distance_cm diff_us / 58; return 0; }这段代码有几个参数必须说清。TIM2 的计数频率必须是 1MHz也就是 72MHz 时钟下预分频为 71如果预分频不对diff_us算出来的就不是微秒。超时时间设置 100ms对应最大测量距离约 1.7m如果你要测满 4m把超时放到 250ms。还有一个细节多次测量之间要留至少 50ms 的间隔否则超声波余波会干扰下一轮。4.4 声速温度补偿与多探头防串扰声速在空气中随温度变化0℃ 时约 331m/s30℃ 时约 349m/s如果你在恒温室内做测试差异不大但用在户外或鱼缸边误差能到好几厘米。精确的换算公式是距离cm Echo 高电平时间μs×331.45 0.607 × 温度℃/ 10000 / 2也就是说把第 3 章读到的温度直接代进来。我的建议是测距代码里读一个全局温度变量每秒钟更新一次不要每次测距都去读 DHT11因为单总线读取本身耗时。温度声速 m/s无补偿时 1m 处误差0℃331.45约 2.5cm20℃343.46基准40℃355.46约 2.8cm多模块同时测距时两个 HC-SR04 会互相收对方的回波。解决方法是让模块 A 触发后先等 60ms 让余波消失再触发模块 B如果项目要求更高用不同频率的收发一体式换能器但成本翻倍一般不需要。5. 传感器融合调度与几个值得记录的踩坑点5.1 单线程多传感器的调度顺序主循环里常见的错误是“每轮循环把温湿度和距离都读一遍”。DHT11 要求两次读取间隔不小于 1sHC-SR04 测距最好 50ms 以上一次如果把两个读操作叠在同一个周期只会让 DHT11 读到缓存旧值。我一般用一个 100ms 的软件定时器做调度基准每 5 个 tick 读一次 DHT11每 2 个 tick 触发一次超声波每个 tick 刷新一次屏幕。这样所有传感器都有各自的采样节拍互不阻塞。uint32_t tick_10ms HAL_GetTick() / 10; if (tick_10ms % 5 0) { dht11_read(hum, temp); } if (tick_10ms % 2 0) { hcsr04_get_distance_cm(dist); }5.2 数据平滑与异常值丢弃超声波在风扇叶片或水面波纹上会得到跳动很大的回波第 5.1 节的调度配上滤波才能输出稳定数据。最简单有效的是滑动窗口中值滤波保留最近 5 个有效距离值排序后取中间值。uint32_t dist_buf[5] {0}; uint8_t dist_idx 0; dist_buf[dist_idx] dist; if (dist_idx 5) dist_idx 0; /* 排序取中值 */检查异常值的另一个经验如果连续两次测量差超过 50cm大概率是回声缺失或外部干扰直接丢弃本次结果而不是把跳变送进滤波队列。5.3 排查表信号对但数据不对怎么查现象原因处理DHT11 时而返回 1 或 2起始信号时间不足/供电偏低拉低时间改到 30ms3.3V 供电时检查电压距离读数固定为 0Echo 未分压导致引脚损坏换板子加两个电阻分压距离读数周期性漂移声速未做温度补偿代入第 4.4 节公式超声波和 DHT11 同时运行时读数错乱DHT11 的单总线阻塞循环打断了 Echo 中断回调处理用第 4.3 节非阻塞测距缩短 DHT11 超时时间最后一个可以立刻验证的技巧在 OLED 上把声速温度补偿后的距离和未补偿的并排显示比较它们在不同温度下的差值。看到差值随温度变化说明时序和数据链路基本正确剩下的是算法层面的打磨。本文还有配套的精品资源点击获取