风扇代码调试(2)
220.3.19 风扇代码调试(2)
#include "dht11.h" // 引入 DHT11 驱动声明,使 main.c 可以调用 DHT_Read() 并访问 Data 数组
uint8_t buff[4]={0}; // 在 main() 的 USER CODE BEGIN 1 区域申请 4 字节缓冲区,用于保存 0~255 的十进制字符串及结尾的 '\0'
DHT_Read(); // 发起一次 DHT11 单总线读取;驱动把收到的 5 个字节写入全局 Data 数组
lv_arc_set_value(ui_Arc1, Data[2]); // 用 Data[2] 的温度整数更新左侧温度圆弧的当前值
sprintf(buff,"%u",Data[2]); // 把无符号温度整数转换为以 '\0' 结尾的十进制字符串并写入 buff
lv_label_set_text(ui_Label2, buff); // 把温度字符串复制给温度数值标签 Label2
lv_arc_set_value(ui_Arc2, Data[0]); // 用 Data[0] 的湿度整数更新右侧湿度圆弧的当前值
sprintf(buff,"%u",Data[0]); // 复用 buff,把无符号湿度整数转换为十进制字符串
lv_label_set_text(ui_Label4, buff); // 把湿度字符串复制给湿度数值标签 Label4
上述代码按本节视频最终画面与课程配套工程核对,只保留本节实际加入的业务代码,并逐行补充注释。视频中间曾把第二个
sprintf暂时写成Data[2],最终已改为Data[0];临时测试标题所用的lv_label_set_text(ui_Label1, "Temperature & Humility Control System666")也已注释,因此不列入最终源码。TIM1 初始化代码由 CubeMX 生成,DHT11 驱动则从已有例程复制,本节只修改了数据引脚,均在正文说明而不重复粘贴整份生成代码或驱动代码。
一、温湿度代码调试
本节在第 219 节已经能够显示界面、切换 LED 和调节风扇 PWM 的基础上,补齐温湿度采集与显示链路:
DHT11 → PA8 单总线 → DHT_Read() → Data[5] → Arc/Label → LCD
课堂完成的主要工作是:
- 把现成的
dht11.c、dht11.h驱动复制到 22 号工程的BSP/DHT11目录; - 把
dht11.c加入 Keil 工程,并添加BSP/DHT11头文件搜索路径; - 用 CubeMX 配置 TIM1,为 DHT11 驱动提供微秒级计时基准;
- 因 PA5 已被 LCD 的 SPI1 时钟占用,把驱动内的数据引脚从 PA5 改为 PA8;
- 在
main.c中调用DHT_Read(),把温度、湿度分别写入两个 Arc 和两个 Label; - 编译、烧写,并用手握传感器的方式观察温湿度数值及圆弧变化。
这里的重点不是重新编写一套 DHT11 协议,而是把“已有硬件驱动”“CubeMX 外设配置”和“SquareLine 生成的 LVGL 对象”接成一条完整的数据通路。
二、任务信息资源导图
2.1 任务信息资源导图概述
本节涉及三类资源,它们各自承担不同职责:
| 层次 | 代表文件 | 职责 |
|---|---|---|
| 硬件驱动层 | BSP/DHT11/dht11.c、dht11.h | 控制 PA8 时序、读取 DHT11 的 40 位数据 |
| 芯片外设层 | Core/Src/tim.c、tim.h | 配置并提供 TIM1 句柄,支撑微秒延时 |
| 界面业务层 | Core/Src/main.c、midware/ui/screens/ui_Screen1.c | 读取 Data,更新 Arc 和 Label |
从磁盘复制源文件只是第一步。Keil 工程还必须知道“哪些 .c 参与编译”和“到哪里找 .h”:
dht11.c要加入工程分组,否则其中的函数和全局变量不会参与编译、链接;BSP/DHT11要加入 Include Paths,否则#include "dht11.h"会报找不到头文件;- 两者缺一不可。原笔记中的“DSP/PSP 路径”“d7 幺幺”等均是转录错误,正确名称是 BSP/DHT11。
课堂把驱动文件加入工程后没有立即编译,是因为随后还要用 CubeMX 生成 TIM1 初始化代码。应注意:再次生成 CubeMX 工程时,手工加入 Keil 分组的文件存在被工程配置覆盖的风险,所以老师强调配置完成后要重新确认、重新编译。
2.2 定时器与PWM控制
本节新增的 TIM1 不是用来输出 PWM,也没有使用通道或中断。它作为硬件计数器,被 DHT11 驱动轮询读取,用来形成微秒级阻塞延时。
CubeMX 中的配置为:
| 项目 | 设置 | 含义 |
|---|---|---|
| Clock Source | Internal Clock | TIM1 使用内部时钟计数 |
| Prescaler | 72-1 | 72 MHz 定时器时钟分频 72 倍 |
| Counter Mode | Up | 从小到大向上计数 |
| Counter Period | 65535 | 16 位计数器的最大自动重装值 0xFFFF |
| Channel 1~4 | Disable | 不输出 PWM,也不做输入捕获 |
| NVIC | 未启用 | 不依赖更新中断 |
本工程 TIM1 输入时钟为 72 MHz。STM32 定时器预分频公式是:
计数频率 = 定时器输入时钟 ÷ (PSC + 1)
因此:
72 MHz ÷ (71 + 1) = 1 MHz
1 MHz 的计数周期正好是 1 μs,驱动便可以通过读取 TIM1->CNT 的变化估算微秒延时。65535 是 16 位计数器可表示的最大值,并不是 DHT11 的某个协议参数。
原记录写“未使用硬件定时器”并不准确。准确说法是:使用了 TIM1 硬件计数器,但没有使用定时器通道、PWM 或中断;CPU 在 Delay_us() 中轮询等待。
驱动中的 Delay_us() 每次都会设置计数值、启动 TIM1、忙等到目标值,再停止 TIM1。其 25 个计数的补偿量是这份例程写死的经验值,并非换一颗 MCU、改变主频或优化等级后仍然通用。微秒协议如果出现偶发读错,应使用逻辑分析仪实测 PA8 波形,而不能只凭源码中的数字判断时序一定准确。
本工程现在同时使用三只定时器,职责必须区分:
| 定时器 | 用途 |
|---|---|
| TIM1 | DHT11 驱动的微秒级轮询延时 |
| TIM3_CH2 | 在 PB5 输出 PWM,控制风扇 |
| TIM6 | 通过周期中断给 LVGL 提供 1 ms Tick |
2.3 温湿度传感器DHT11的使用
DHT11 使用一根数据线完成主机请求和传感器应答。课堂复制的驱动大致执行以下过程:
- 把 PA8 配置为输出并拉低至少 18 ms,发出开始信号;
- 主机释放总线并把 PA8 改为输入;
- 等待 DHT11 的低电平、高电平应答;
- 连续读取 40 位数据,即 5 个字节;
- 对前 4 个字节求和,与第 5 个校验字节比较;
- 校验一致时返回
1,否则返回0。
Data[5] 的经典 DHT11 数据布局为:
| 数组元素 | 含义 | 本节是否显示 |
|---|---|---|
Data[0] | 湿度整数部分 | 是,Arc2 和 Label4 |
Data[1] | 湿度小数部分 | 否 |
Data[2] | 温度整数部分 | 是,Arc1 和 Label2 |
Data[3] | 温度小数部分 | 否 |
Data[4] | 校验字节 | 仅由驱动校验 |
所以本节只显示整数温度和整数湿度,没有显示小数。原笔记把它记成“delta 数组”是语音识别错误,正确变量名是 Data;dht11.h 使用 extern uint8_t Data[5]; 声明它,真正的存储空间定义在 dht11.c 中,因此 main.c 包含头文件后可以访问同一数组。
40 位数据按脉宽区分 0 和 1。该驱动在每一位高电平开始约 40 μs 后读取引脚:短高电平已经结束时读到 0,长高电平仍持续时读到 1。代码中的重试计数用于避免信号异常时永久卡在等待循环,但超时后没有向上层报告具体错误。
还要明确三个课程代码的边界:
DHT_Read()有成功/失败返回值,但main.c没有检查它;读失败时,界面仍会使用数组中原有或不完整的数据。- 驱动以 32 位整数计算前 4 字节之和,却没有显式只取低 8 位。协议校验字节本应是和的低 8 位;当前数据总和超过 255 时可能把有效帧误判为失败。
while(1)中只有HAL_Delay(5),然后每轮都调用DHT_Read()。DHT11 官方资料要求相邻读取留出约 2 s,课堂实现的轮询频率明显过高,可能反复取得旧值或导致通信不稳定。本节只忠实记录并分析现状,不擅自改写源码。
2.4 引脚更改与初始化
驱动原来使用 PA5,而本工程 PA5 已配置为 SPI1_SCK,负责给 LCD 提供串行时钟。同一个引脚不能同时稳定承担 LCD 时钟输出和 DHT11 单总线数据,因此课堂把 DHT11 数据线改到未占用的 PA8。
老师在 dht11.c 中选中 GPIO_PIN_5,使用 Ctrl+H 替换为 GPIO_PIN_8,并将范围限制为 Current Document。视频显示本文件共有 10 处匹配,涉及:
- GPIO 输出模式初始化;
- GPIO 输入模式初始化;
- 拉低、拉高数据线;
- 等待应答电平;
- 逐位读取数据电平。
只改初始化中的一处是不够的;所有读写操作必须指向同一根物理数据线。另一方面,也不能对整个工程做无范围替换,否则会把 LCD 使用的 PA5 一并改坏。
DHT11 的 DATA 线需要上拉。若使用三针 DHT11 模块,板上通常已经带上拉电阻;若使用裸四针器件,则要按器件手册接外部上拉。课程驱动通过“推挽输出 → 输入”切换方向,但两个 GPIO_InitTypeDef 局部变量没有清零,也没有明确给 Pull 字段赋值;在严格工程中这可能带来不确定配置,应结合 HAL 结构体要求和实测波形检查。这里属于驱动例程本身的隐患,本节没有修改,因此不加入顶部课程源码。
CubeMX 生成后,main.c 自动出现 MX_TIM1_Init(),tim.c 中出现 htim1 和 TIM1 初始化代码。DHT 驱动的头文件通过 extern TIM_HandleTypeDef htim1; 引用同一句柄,Delay_us() 才能启动、读取并停止 TIM1。
2.5 LVGL代码分析与修改
老师通过 ui_Screen1.c 反查 SquareLine 生成界面时,重点展示了控件的创建顺序和父子关系。例如:
lv_obj_create(NULL)创建顶层 Screen;lv_obj_create(ui_Screen1)创建 Screen 的子面板;lv_arc_create(ui_Panel2)创建面板内的 Arc;lv_label_create(ui_Panel3)创建面板内的 Label。
LVGL 是 C 语言库,但通过 lv_obj_t *、父对象、子对象、样式和事件提供“对象式”的接口。把它描述为“C++ 面向对象编程”并不准确。
生成代码中的几个常见参数需要正确理解:
lv_color_hex(0x4B4B55):把十六进制 RGB 颜色转为 LVGL 颜色,0x4B4B55是深灰色;lv_obj_set_style_bg_opa(..., 255, ...):255表示最大不透明度,也就是完全覆盖,并非“透明”;lv_arc_set_value(ui_Arc1, 50):把 Arc 的当前数值设为 50。它控制指示弧长度,不是把圆弧放到屏幕中间;- Arc 默认数值范围通常为 0~100,本界面生成代码没有另设范围,温度和相对湿度的整数值可直接写入。
SquareLine 生成的 UI 对象在 ui.h 等头文件中以 extern lv_obj_t * 声明,在相应 .c 文件中定义。main.c 已包含 ui.h,因此可以直接使用 ui_Arc1、ui_Label2 等对象指针。这不是“对象自动变成全局变量”,而是 C 语言的外部变量声明与链接机制。
为了证明对象能够在 main.c 中修改,课堂曾临时执行:
lv_label_set_text(ui_Label1, "Temperature & Humility Control System666");
烧写后标题确实多出 666,说明 ui_Label1 指向屏幕上的标题 Label。验证完成后该行被注释,所以它不是最终功能代码。标题里的 Humility 也是界面原文;表达“湿度”的正确英文应为 Humidity,但本节没有在源码中正式改名。
2.6 数据显示与界面更新
最终界面映射关系如下:
| 传感器数据 | LVGL 对象 | 对象作用 |
|---|---|---|
Data[2] 温度整数 | ui_Arc1 | 温度圆弧进度 |
Data[2] 温度整数 | ui_Label2 | 温度数字文本 |
Data[0] 湿度整数 | ui_Arc2 | 湿度圆弧进度 |
Data[0] 湿度整数 | ui_Label4 | 湿度数字文本 |
Arc 接收整数值,可以直接调用 lv_arc_set_value();Label 显示的是字符串,所以要先用 sprintf() 转换:
传感器字节 → sprintf(buff, "%u", value) → 以 '\0' 结尾的文本 → lv_label_set_text()
buff[4] 最多保存 3 位十进制数和 1 个字符串结束符。由于 Data[x] 是 uint8_t,范围是 0~255,所以空间刚好足够。%u 表示按无符号十进制格式输出。课程把它声明为 uint8_t buff[4],而标准字符串接口通常使用 char *;Keil 可能给出指针类型警告,但本笔记不把课堂代码改成后续优化版本。
两次格式化复用同一个 buff 是可行的,因为 lv_label_set_text() 会为 Label 保存一份文本:温度文本设置完成后,覆盖 buff 写入湿度不会把温度 Label 同步改掉。它不同于只保存调用者指针的 lv_label_set_text_static()。
主循环本节最终顺序为:
lv_timer_handler()处理 LVGL 定时任务与刷新;HAL_Delay(5)短暂延时;DHT_Read()阻塞读取传感器;- 更新两个 Arc 和两个 Label;
- 回到下一轮,再由 LVGL 处理新状态并刷新显示。
这样可以工作,但 DHT11 的 18 ms 起始低电平和整帧通信会长时间阻塞主循环,再叠加远高于器件建议的读取频率,会挤占 LVGL 的处理时间。更完善的项目应把传感器读取做成约 2 s 的周期任务,并且只在校验成功后更新 UI;这些是对现有实现的分析,不属于本节视频已经完成的代码。
课堂烧写后的首次画面显示温度约 15、湿度约 19,两个圆弧也分别移动到对应位置。老师用手握住传感器后,湿度变化较快,温度变化相对慢。这是合理现象:手掌周围的湿气会快速影响相对湿度,而传感器温度需要经过热传递才逐步变化。
常见排查顺序如下:
| 现象 | 优先检查 |
|---|---|
dht11.h 找不到 | Keil Include Paths 是否加入 BSP/DHT11 |
DHT_Read 链接失败 | dht11.c 是否加入 Keil 工程并参与构建 |
htim1 未定义 | CubeMX 是否生成 TIM1,tim.c/tim.h 是否在工程中 |
| 一直显示 0 或旧值 | PA8 接线、上拉、供电与共地,DHT 读取返回值是否成功 |
| 数值偶发跳变 | 读取间隔是否过短、TIM1 是否为 1 MHz、微秒时序是否准确 |
| Arc 变化但数字不变 | 对应 Label 名称和 lv_label_set_text() 参数是否正确 |
| 温湿度左右颠倒 | 温度应使用 Data[2],湿度应使用 Data[0] |
| LCD 异常或无显示 | 是否误把整个工程中的 PA5 都替换成 PA8 |
2.7 任务信息资源导图总结
本节完成了一条从传感器到图形界面的闭环:
PA8 采样 DHT11 → TIM1 提供微秒计时 → Data 保存温湿度 → sprintf 转为文本 → LVGL 更新 Arc/Label → LCD 显示
需要牢牢记住的结论是:
- TIM1 是 DHT11 微秒延时的硬件计数基础,不是本节的 PWM 定时器;
- PA5 已被 LCD 的 SPI1_SCK 占用,所以 DHT11 改用 PA8;
Data[2]是温度整数,Data[0]是湿度整数;lv_arc_set_value()设置的是数值,不是控件坐标;- Label 需要字符串,因此要先格式化数字;
- 课程程序能演示功能,但读取间隔、错误处理和驱动健壮性仍有改进空间。
本节只完成 DHT11 数据在当前温湿度界面中的显示,没有引入后续课程的其他功能。