STM32F103+DHT11+OLED 温湿度显示教程

最终效果

上电后 OLED 屏幕实时刷新:

TEMP: 25.0 C
HUMI: 60.0 %

实物效果

硬件接线

DHT11 的 DATA 脚接 STM32 的 PA6,记得在 DATA 和 VCC 之间并联一个 4.7kΩ 上拉电阻,不然通信会失败。OLED 是 I2C 接口,SCL 接 PB8,SDA 接 PB9。DHT11 和 OLED 都从 STM32 的 3.3V 取电。

硬件接线图

DHT11 是怎么工作的

DHT11 是一个被动响应的传感器——你不问,它就不说。主机必须主动发一个起始信号,传感器才会应答并回传数据。整个过程分三步走。

第一步,主机发起始信号。

先把 PA6 切成输出模式,拉低总线至少 18 毫秒,再拉高 20 到 40 微秒,最后切回输入模式释放总线。这一步相当于敲了敲传感器的门。

起始信号

第二步,传感器应答。

DHT11 检测到起始信号后,会先拉低总线约 80 微秒表示"我听到了",再拉高 80 微秒表示"准备说了"。代码里用 while 循环死等这两个电平变化,超时 200 个计数就放弃。

第三步,传感器吐 40 位数据。

这是最关键的部分。DHT11 一共吐出 5 个字节共 40 位,前两个字节是湿度(整数 + 小数),中间两个字节是温度(整数 + 小数),最后一个字节是前四个字节之和的低 8 位当校验和。DHT11 的小数位永远为 0,这是硬件决定的,不是代码问题。

每一位怎么区分 0 和 1?

每个 bit 都以传感器拉低 50 微秒开始,然后拉高——高电平持续多久决定了这一位是 0 还是 1。大约 28 微秒就是 0,大约 70 微秒就是 1。

这里就是本文代码和网上大多数例程的区别所在。很多教程用一个固定的 Delay_us(35) 延时后去读引脚电平——但 35 微秒恰好卡在 28 和 70 的正中间,电压波动或者温度变化时就容易读错。

更好的做法是"脉宽测量法":用 while 循环不停读引脚,同时计数器一直加,直到高电平结束。最后看计数器是多少——小于阈值(约 35~40 个计数)就是 0,大于就是 1。思维上就是拿了一把尺子去量脉冲宽度,而不是在某个固定时间点偷瞄一眼。

/* 测量高电平持续宽度,区分 0 和 1 */
pulseWidth = 0;
while (DHT11_DQ_IN()) {              /* 引脚为高电平时一直循环 */
    if (++pulseWidth > 200) goto exit; /* 超时保护 */
    Delay_us(1);
}
dataBuffer[byteIndex] <<= 1;
if (pulseWidth > DHT11_PULSE_THRESHOLD) {
    dataBuffer[byteIndex] |= 0x01;    /* 长脉宽 = 逻辑 1 */
}
/* else: 短脉宽 = 逻辑 0,什么也不做(已左移,最低位就是 0) */

整个过程必须关全局中断 __disable_irq(),因为时序是微秒级的,随便来个 SysTick 中断就会把脉冲计数打乱。读完 5 个字节再 __enable_irq() 恢复。万一中间某个阶段超时了(传感器接触不良或者线断了),用 goto exit 统一跳到函数末尾恢复中断然后返回,输出参数保持上一次的正确值不动,屏幕不会闪。

读完数据后验证校验和,只取低 8 位比对。C 语言里四个 uint8_t 相加会自动提升成 int,超过 255 就会误判,必须显式截断:

if ((uint8_t)(dataBuffer[0] + dataBuffer[1] +
              dataBuffer[2] + dataBuffer[3]) == dataBuffer[4]) {
    /* 校验通过,更新输出参数 */
}

这基本上就是 DHT11 驱动的全部要点了。几句话总结:敲门(18ms 低电平)、等回应(两个 80us)、量脉冲(while 计数)、验校验和。 脉宽测量是灵魂,关中断是保障。

主函数逻辑

整个 main 函数非常直白,四个步骤循环往复:

初始化三个模块(SysTick 延时、OLED 屏幕、DHT11 传感器)→ 画好固定的显示模板 "TEMP: . C""HUMI: . %" → 等一秒 → 读传感器 → 把读到的四个数字覆盖到模板里预留的空位上 → 回到等一秒。

模板只写一次,while 循环里只更新那四个数字位。小数点由模板的字符串固定不动,这样既减少了 I2C 通信量,又不会出现小数点被数字覆盖的低级 bug。

int main(void)
{
	uint8_t humidityInt    = 0;
	uint8_t humidityDec    = 0;
	uint8_t temperatureInt = 0;
	uint8_t temperatureDec = 0;

	Delay_Init();
	OLED_Init();
	DHT11_Init();

	OLED_ShowString(1, 1, "TEMP:  .  C");
	OLED_ShowString(2, 1, "HUMI:  .  %");

	while (1) {
		Delay_ms(1000);

		DHT11_ReadData(&humidityInt, &humidityDec,
		               &temperatureInt, &temperatureDec);

		OLED_ShowNum(1, 6, temperatureInt, 2);
		OLED_ShowNum(1, 9, temperatureDec, 1);
		OLED_ShowNum(2, 6, humidityInt,    2);
		OLED_ShowNum(2, 9, humidityDec,    1);
	}
}

两个可以微调的参数

第一个是 DHT11_PULSE_THRESHOLD,在 DHT11.c 顶部。当前设的 35,它是 while 循环的计数值,不是微秒。实际对应多少微秒取决于你的主频和 Delay_us 的调用开销,72MHz 下大约对应 45 微秒左右,正好在 28us 和 70us 的正中间。如果你有逻辑分析仪,抓一下波形看一下实际的计数值,取个中间数填进去就行。

第二个是采样间隔。手册要求 DHT11 两次采样之间至少隔 1 秒,代码里用 Delay_ms(1000) 足够了。如果发现读数不稳定,可以适当拉长到 1500 毫秒,因为 DHT11 内部有一个温湿度转换过程,刚上电头一两秒的数据可能是错的。

逻辑分析

完整代码获取

B站:https://b23.tv/0Ea2yCh
源码: 置顶文件

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐