DHT11实验_定时器实现us延时
143.5.15DHT11实验(定时器实现us延时)
/*
* 整体目的:使用 TIM1 构造微秒级阻塞延时,按 DHT11 单总线时序读取 5 字节温湿度数据。
* 时钟依据:TIM1CLK=72 MHz,PSC=71 后计数频率为 1 MHz,因此 CNT 每增加 1 约经过 1 μs。
* 引脚依据:DHT11 DATA 接 PA5;主机发送起始信号时 PA5 为输出,读取应答和数据时切为输入。
* 协议流程:主机拉低至少 18 ms → 释放总线 → 传感器应答 → 读取 40 位
* → 前 4 字节求和的低 8 位与第 5 字节校验。
* 代码范围:摘自 CubeIDE/09DHT11 的 main.c、tim.c 和 dht11.c;
* HAL 定时器/GPIO/串口库内部不逐行展开。
*/
/* ==================== Core/Src/tim.c:1 MHz TIM1 自由计数基础 ==================== */
TIM_HandleTypeDef htim1; /* 定义 TIM1 句柄,供初始化和 Delay_us() 共同访问。 */
void MX_TIM1_Init(void) /* 把 TIM1 配置成每拍约 1 μs 的向上计数器。 */
{ /* TIM1 初始化函数体开始。 */
TIM_ClockConfigTypeDef clock_cfg = {0}; /* 清零时钟源配置结构体。 */
htim1.Instance = TIM1; /* 选择 APB2 上的高级定时器 TIM1。 */
htim1.Init.Prescaler = 71U; /* 实际 72 分频:72 MHz/72=1 MHz。 */
htim1.Init.CounterMode = TIM_COUNTERMODE_UP; /* CNT 向上递增。 */
htim1.Init.Period = 0xFFFFU; /* ARR 设为 16 位最大值,一圈为 65536 μs。 */
htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; /* 不额外分频数字采样时钟。 */
htim1.Init.RepetitionCounter = 0U; /* 每次溢出都产生普通更新事件。 */
htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; /* ARR 写入立即生效。 */
if (HAL_TIM_Base_Init(&htim1) != HAL_OK) /* 把基础配置应用到 TIM1。 */
{ /* 初始化失败分支开始。 */
Error_Handler(); /* 失败时进入统一错误处理。 */
} /* 初始化结果判断结束。 */
clock_cfg.ClockSource = TIM_CLOCKSOURCE_INTERNAL; /* 使用芯片内部定时器时钟。 */
if (HAL_TIM_ConfigClockSource(&htim1, &clock_cfg) != HAL_OK) /* 应用时钟源。 */
{ /* 时钟源配置失败分支开始。 */
Error_Handler(); /* 配置失败时停止正常业务。 */
} /* 时钟源配置结果判断结束。 */
} /* TIM1 初始化函数结束;计数器尚未启动。 */
/* ==================== Core/Src/dht11.c:微秒延时 ==================== */
uint8_t Data[5] = {0U}; /* 依次保存湿度整数/小数、温度整数/小数和校验字节。 */
void Delay_us(uint16_t us) /* 使用 TIM1 轮询产生约 us 微秒的阻塞延时。 */
{ /* 微秒延时函数体开始。 */
uint16_t counter = /* 计算本次计数器的起始值。 */
(uint16_t)(0xFFFFU - us - 25U); /* 起点与终点都避开溢出点 25 拍,差值仍为 us。 */
__HAL_TIM_SET_COUNTER(&htim1, counter); /* 把 TIM1_CNT 写成计算出的起始值。 */
HAL_TIM_Base_Start(&htim1); /* 启动 TIM1 普通计数,不开启中断。 */
while (counter < (uint16_t)(0xFFFFU - 25U)) /* 未到固定终点 65510 就继续等待。 */
{ /* 忙等循环开始,期间 CPU 不能处理其他前台任务。 */
counter = (uint16_t)__HAL_TIM_GET_COUNTER(&htim1); /* 反复读取当前 CNT。 */
} /* 到达目标计数后退出忙等。 */
HAL_TIM_Base_Stop(&htim1); /* 停止 TIM1,避免函数返回后继续计数。 */
} /* 微秒延时函数结束;调用和轮询开销会带来一定误差。 */
/* ==================== Core/Src/dht11.c:PA5 输入/输出切换 ==================== */
void DHT_GPIO_SET_OUTPUT(void) /* 将 PA5 交给主机主动驱动起始信号。 */
{ /* 输出模式切换函数体开始。 */
GPIO_InitTypeDef gpio = {0}; /* 清零 GPIO 配置结构体。 */
gpio.Pin = GPIO_PIN_5; /* 选择连接 DHT11 DATA 的 PA5。 */
gpio.Mode = GPIO_MODE_OUTPUT_PP; /* 按课程工程配置为推挽输出。 */
gpio.Speed = GPIO_SPEED_FREQ_HIGH; /* 选择高速 GPIO 边沿能力。 */
HAL_GPIO_Init(GPIOA, &gpio); /* 把输出模式配置写入 PA5。 */
} /* 输出模式切换函数结束。 */
void DHT_GPIO_SET_INPUT(void) /* 释放 PA5,由传感器驱动总线并供 MCU 读取。 */
{ /* 输入模式切换函数体开始。 */
GPIO_InitTypeDef gpio = {0}; /* 清零 GPIO 配置结构体。 */
gpio.Pin = GPIO_PIN_5; /* 仍选择 DHT11 DATA 所在 PA5。 */
gpio.Mode = GPIO_MODE_INPUT; /* 将 PA5 改为输入,停止主机主动输出。 */
gpio.Pull = GPIO_NOPULL; /* 不启用内部上下拉,外部数据线应有合适上拉。 */
HAL_GPIO_Init(GPIOA, &gpio); /* 把输入模式配置写入 PA5。 */
} /* 输入模式切换函数结束。 */
/* ==================== Core/Src/dht11.c:读取一个字节 ==================== */
uint8_t DHT_Read_Byte(void) /* 按高位先发顺序读取连续 8 个 DHT11 数据位。 */
{ /* 单字节读取函数体开始。 */
uint8_t value = 0U; /* 累积 8 位结果,初始清零。 */
uint8_t retry = 0U; /* 限制等待电平变化的循环次数。 */
uint8_t i = 0U; /* 保存当前正在读取的位序号。 */
for (i = 0U; i < 8U; i++) /* 每个 DHT11 数据字节固定包含 8 位。 */
{ /* 一位数据的读取流程开始。 */
while ((HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5) == GPIO_PIN_RESET) && /* 等待位起始低电平结束。 */
(retry < 100U)) /* 最多等待约 100 次,避免总线异常时永久卡死。 */
{ /* 低电平等待循环开始。 */
Delay_us(1U); /* 每次轮询间隔约 1 μs。 */
retry++; /* 累计等待次数。 */
} /* 低电平等待循环结束。 */
retry = 0U; /* 为本位后续等待重新清零。 */
Delay_us(40U); /* 高电平开始约 40 μs 后采样,区分短 0 与长 1。 */
value <<= 1U; /* 先左移,为当前位腾出最低位。 */
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5) == GPIO_PIN_SET) /* 40 μs 后仍为高则判为 1。 */
{ /* 当前位为 1 的分支开始。 */
value |= 1U; /* 把结果最低位置 1。 */
} /* 当前位为 1 的分支结束;为 0 时无需额外写入。 */
while ((HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5) == GPIO_PIN_SET) && /* 等待当前高电平结束。 */
(retry < 100U)) /* 同样设置等待上限。 */
{ /* 高电平等待循环开始。 */
Delay_us(1U); /* 每次等待约 1 μs。 */
retry++; /* 累计等待次数。 */
} /* 高电平等待循环结束。 */
retry = 0U; /* 为下一位清除等待计数。 */
} /* 8 位循环结束。 */
return value; /* 返回按发送顺序组合完成的字节。 */
} /* 单字节读取函数结束。 */
/* ==================== Core/Src/dht11.c:读取完整 40 位帧 ==================== */
uint8_t DHT_Read(void) /* 发送起始信号、接收 5 字节并校验。 */
{ /* 完整读取函数体开始。 */
uint8_t retry = 0U; /* 限制等待传感器应答的时间。 */
uint8_t i = 0U; /* 五字节读取循环下标。 */
uint8_t checksum = 0U; /* 保存前四字节和的低 8 位。 */
DHT_GPIO_SET_OUTPUT(); /* PA5 切为输出,主机准备发起通信。 */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); /* 主机把 DATA 拉低。 */
HAL_Delay(18U); /* 低电平保持 18 ms,满足课程使用的起始时序。 */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); /* 主机暂时拉高 DATA。 */
Delay_us(20U); /* 等待约 20 μs 后释放并监听传感器。 */
DHT_GPIO_SET_INPUT(); /* PA5 切为输入,把总线控制权交给 DHT11。 */
Delay_us(20U); /* 等待传感器进入应答低电平。 */
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5) != GPIO_PIN_RESET) /* 未检测到预期应答低电平。 */
{ /* 无响应错误分支开始。 */
return 0U; /* 立即报告失败,不把旧 Data 当作新数据。 */
} /* 无响应错误分支结束。 */
while ((HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5) == GPIO_PIN_RESET) && /* 等应答低电平结束。 */
(retry < 100U)) /* 限制最长等待次数。 */
{ /* 应答低电平等待开始。 */
Delay_us(1U); /* 每次轮询约等待 1 μs。 */
retry++; /* 累计低电平等待时间。 */
} /* 应答低电平等待结束。 */
retry = 0U; /* 为应答高电平等待清零。 */
while ((HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5) == GPIO_PIN_SET) && /* 等应答高电平结束。 */
(retry < 100U)) /* 限制最长等待次数。 */
{ /* 应答高电平等待开始。 */
Delay_us(1U); /* 每次轮询约等待 1 μs。 */
retry++; /* 累计高电平等待时间。 */
} /* 应答高电平等待结束。 */
for (i = 0U; i < 5U; i++) /* 顺序读取湿度、温度和校验共 5 字节。 */
{ /* 五字节读取循环开始。 */
Data[i] = DHT_Read_Byte(); /* 调用单字节函数并保存当前字节。 */
} /* 五字节读取循环结束。 */
checksum = (uint8_t)(Data[0] + Data[1] + Data[2] + Data[3]); /* 只保留和的低 8 位。 */
return (checksum == Data[4]) ? 1U : 0U; /* 校验相等返回成功,否则返回失败。 */
} /* 完整 DHT11 读取函数结束。 */
/* ==================== Core/Src/main.c:周期读取与串口输出 ==================== */
int main(void) /* 程序入口,初始化后每秒读取一次 DHT11。 */
{ /* main() 函数体开始。 */
HAL_Init(); /* 初始化 HAL 与 SysTick。 */
SystemClock_Config(); /* 配置 72 MHz 系统时钟。 */
MX_GPIO_Init(); /* 开启 DHT11 所在 GPIOA 等工程 GPIO 时钟。 */
MX_TIM1_Init(); /* 初始化 Delay_us() 使用的 1 MHz TIM1。 */
MX_USART1_UART_Init(); /* 初始化温湿度输出使用的 USART1。 */
while (1) /* 周期性采样循环开始。 */
{ /* 一次 DHT11 采样周期开始。 */
if (DHT_Read() != 0U) /* 只有收到应答且校验正确时才打印。 */
{ /* 有效数据分支开始。 */
printf(" H:%u T:%u\r\n", /* 输出整数湿度和整数温度。 */
Data[0], /* 第 1 字节是湿度整数部分。 */
Data[2]); /* 第 3 字节是温度整数部分。 */
} /* 有效数据分支结束。 */
HAL_Delay(1000U); /* 两次读取之间等待约 1 秒。 */
} /* 返回循环顶部开始下一次读取。 */
} /* main() 语法边界。 */
一、DHT11温湿度传感器实验
1.1 任务资源图
- 本实验使用 STM32F103ZET6 读取 DHT11 温湿度传感器,并经 USART1 输出温度和湿度。
HAL_Delay()的单位是毫秒,而 DHT11 的应答和数据位判别需要几十微秒的时序,因此使用 TIM1 实现Delay_us()。- TIM1 在本实验中只作为由程序启动、读取和停止的计数器,不使用 PWM、输入捕获、外部通道或定时器中断。
- DHT11 的数据线使用
PA5,源码中的GPIOA与GPIO_PIN_5是实际依据。
1.2 实验演示
- DHT11 三针模块与 STM32 的连接如下:
STM32F103ZET6 DHT11 三针模块
------------- ----------------
3.3V ---------------------> VCC
GND ---------------------> GND
PA5 <--------------------> DATA
DATA是双向数据线。STM32 发起通信时将 PA5 配为推挽输出并拉低;随后将 PA5 切为输入,释放总线,由 DHT11 输出应答和 40 位数据。双方不能同时驱动该线。- 视频画面能确认本课使用的是带小电路板的三针 DHT11 模块,但看不清板上 DATA 是否已接上拉电阻,因此不能直接断言“该模块一定自带上拉”。实际接线前应查看所购模块原理图,或断电后测量
DATA与VCC之间的电阻;若没有上拉,再按模块手册连接外部上拉电阻。 - 课程说明该模块的
VCC可接 3.3 V 或 5 V。供电电压和 DATA 上拉电压是两件事:若模块把 DATA 上拉到 VCC,使用 5 V 供电时还必须核对 STM32 对应引脚的输入电压规格;没有模块原理图时,不应凭外观判断数据线电平。 - 串口连接为
PA9 -> USB 转串口模块 RX、PA10 <- USB 转串口模块 TX、GND -> GND,参数为 115200 bit/s、8 位数据位、1 位停止位、无校验。 - 用手靠近传感器或改变周围空气状态时,串口中的温湿度会缓慢变化。DHT11 的采样间隔应不小于 1 s。
1.3 代码分析
1.3.1 系统时钟配置
- 系统时钟为 72 MHz。TIM1 位于 APB2,APB2 未分频时 TIM1 输入时钟也是 72 MHz。
MX_GPIO_Init()只需要开启 GPIOA 时钟;TIM1 没有配置外部通道,因此不需要为 TIM1 分配输入捕获或 PWM 引脚。MX_TIM1_Init()、MX_USART1_UART_Init()必须在第一次调用DHT_Read()前完成。
1.3.2 定时器一初始化
- TIM1 使用内部时钟,向上计数,
Prescaler=71、Period=65535,不启用中断。 - 预分频器的实际分频系数是
71 + 1 = 72,因此计数频率为:
$ f_{CNT}=\frac{72\ \text{MHz}}{71+1}=1\ \text{MHz} $
- 计数器每增加 1 就经过 $1\ \mu s$。16 位计数器的范围是 0 到 65535,单次延时上限约为 65.535 ms。
1.3.3 DHT11分析
DHT_GPIO_SET_OUTPUT()将 PA5 配置为推挽输出,用于发送开始信号;DHT_GPIO_SET_INPUT()将 PA5 配置为输入,用于接收传感器应答和数据。DHT_Read()的流程是:PA5 拉低至少 18 ms,拉高约 20 us,切换为输入,等待 DHT11 的低电平和高电平应答,再读取 5 个字节。- DHT11 发送的数据依次为湿度整数、湿度小数、温度整数、温度小数和校验和。本实验打印
Data[0]与Data[2],即整数湿度和整数温度。
1.3.4 DHT11_Read_Byte分析
DHT_Read_Byte()连续读取 8 位。每一位先等待约 50 us 的低电平结束,再等待 40 us 后读取电平。- 高电平约 26 到 28 us 表示
0,约 70 us 表示1;在高电平开始 40 us 后采样,短高电平已经结束,长高电平仍然存在。 - 每读取一位执行一次左移并写入当前位,最终按照高位在前的顺序组成一个字节。
1.3.5 Delay_us分析
Delay_us()为 DHT11 的应答等待和位判别提供微秒级延时;毫秒级的开始信号仍使用HAL_Delay(18)。- DHT11 协议规定,校验字节等于前四个数据字节之和的低 8 位:
$ \text{checksum}=(Data[0]+Data[1]+Data[2]+Data[3])\ &\ 0xFF $
- 配套源码使用
uint32_t sum并直接比较sum == Data[4],当总和超过 255 时会与协议的“低 8 位校验”不一致。顶部教学代码已显式转换为uint8_t checksum,保证按协议只比较低 8 位。 retry < 100的作用是避免等待电平时永久卡死,但读取函数没有单独返回“位超时”状态,最终仍依靠校验结果判断本帧是否可用。
1.4 Delay_us分析
- 配套源码把起点写成
0xFFFF-us-25,终点写成0xFFFF-25。两端同时减 25 不会改变它们之间的us个计数差,而是让轮询终点避开 16 位计数器即将溢出的0xFFFF,降低程序恰好错过终点后又等待一整圈的风险。
Delay_us(20)
CNT: 65490 -> 65491 -> ... -> 65510
|<--------- 20 个 1 us 计数周期 --------->|
- 该函数是忙等延时,CPU 在等待期间不能处理其他任务,适合 DHT11 所需的几十微秒短时序;长时间延时应使用 SysTick、定时器中断或操作系统延时机制。
- 这种方法的核心是先将定时器配置成已知计数单位,再通过写入计数器起点、启动、读取计数值和停止得到可控短延时。
1.5 配置界面配置定时器
- 在 CubeMX 中将 TIM1 的 Clock Source 设为 Internal Clock,Prescaler 设为 71,Counter Mode 设为 Up,Counter Period 设为 65535。
- 不配置 PWM、输入捕获通道,也不启用 TIM1 中断;Auto-reload preload 可保持 Disable。
- 将
dht11.c、dht11.h加入工程,dht11.h中使用正确的htim1句柄。使用其他定时器时,需要同步修改句柄和初始化函数。
1.6 头文件透析
dht11.h应声明Data[5]、Delay_us()、GPIO 模式切换函数和读取函数;tim.h应声明htim1。- 复用驱动时,将
GPIOA、GPIO_PIN_5改为实际接入的端口和引脚,并保证数据线有可靠上拉、传感器供电与 MCU 数据引脚电平兼容。 - 主循环在每次读取后执行
HAL_Delay(1000)。顶部教学代码在未检测到应答时直接返回失败,并按低 8 位校验,因此不会把“没有新应答”误判为一次成功读取;更完整的驱动还应把各个等待超时分别向上传递。 - 排查顺序:检查 VCC、GND、DATA 与 PA5 的连接;检查上拉与供电;检查 TIM1 的 1 MHz 计数配置;最后检查串口、源文件是否加入工程及初始化顺序。