风扇代码调试(2)

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 0 Likes: 0 Comments: 0

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

课堂完成的主要工作是:

  1. 把现成的 dht11.c、dht11.h 驱动复制到 22 号工程的 BSP/DHT11 目录;
  2. 把 dht11.c 加入 Keil 工程,并添加 BSP/DHT11 头文件搜索路径;
  3. 用 CubeMX 配置 TIM1,为 DHT11 驱动提供微秒级计时基准;
  4. 因 PA5 已被 LCD 的 SPI1 时钟占用,把驱动内的数据引脚从 PA5 改为 PA8;
  5. 在 main.c 中调用 DHT_Read(),把温度、湿度分别写入两个 Arc 和两个 Label;
  6. 编译、烧写,并用手握传感器的方式观察温湿度数值及圆弧变化。

这里的重点不是重新编写一套 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 SourceInternal ClockTIM1 使用内部时钟计数
Prescaler72-172 MHz 定时器时钟分频 72 倍
Counter ModeUp从小到大向上计数
Counter Period6553516 位计数器的最大自动重装值 0xFFFF
Channel 1~4Disable不输出 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 波形,而不能只凭源码中的数字判断时序一定准确。

本工程现在同时使用三只定时器,职责必须区分:

定时器用途
TIM1DHT11 驱动的微秒级轮询延时
TIM3_CH2在 PB5 输出 PWM,控制风扇
TIM6通过周期中断给 LVGL 提供 1 ms Tick

2.3 温湿度传感器DHT11的使用

DHT11 使用一根数据线完成主机请求和传感器应答。课堂复制的驱动大致执行以下过程:

  1. 把 PA8 配置为输出并拉低至少 18 ms,发出开始信号;
  2. 主机释放总线并把 PA8 改为输入;
  3. 等待 DHT11 的低电平、高电平应答;
  4. 连续读取 40 位数据,即 5 个字节;
  5. 对前 4 个字节求和,与第 5 个校验字节比较;
  6. 校验一致时返回 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()。

主循环本节最终顺序为:

  1. lv_timer_handler() 处理 LVGL 定时任务与刷新;
  2. HAL_Delay(5) 短暂延时;
  3. DHT_Read() 阻塞读取传感器;
  4. 更新两个 Arc 和两个 Label;
  5. 回到下一轮,再由 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 数据在当前温湿度界面中的显示,没有引入后续课程的其他功能。

2.7.1 参考资料