风扇代码调试(1)

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

219.3.18 风扇代码调试(1)

HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2); // 在 main.c 的 USER CODE BEGIN 2 区域启动 TIM3 通道 2 的 PWM 输出
#include "ui.h"   // 引入 SquareLine 生成的 UI 对象、LVGL 类型和事件声明
#include "main.h" // 引入 LED_GPIO_Port、LED_Pin 等 CubeMX 生成的 GPIO 宏
#include "tim.h"  // 引入 htim3 定时器句柄和 TIM3 相关声明

void LED_Switch(lv_event_t * e) // 定义 LED 按钮的回调函数,e 指向本次 LVGL 事件对象
{ // LED 回调函数体开始
    HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); // 翻转 PE5 的输出电平,使低电平有效的板载 LED 在亮、灭之间切换
} // LED 回调函数体结束

void Fan_PWM_Control(lv_event_t * e) // 定义风扇 Slider 的回调函数,e 携带触发事件的控件信息
{ // 风扇 PWM 回调函数体开始
    lv_obj_t * slider = lv_event_get_target(e); // 取得触发本次事件的 Slider 对象指针
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, 20+0.8*lv_slider_get_value(slider)); // 把 Slider 的 0~100 映射到约 20~100,并写入 TIM3_CH2 比较寄存器以改变 PWM 占空比
} // 风扇 PWM 回调函数体结束

上述代码按本节视频最终画面抄录,只删除 SquareLine 自动生成的 // Your code here,并增加逐行注释。视频最终使用 20+0.8*lv_slider_get_value(slider);当前课程配套工程中的同一行后来变成了 10+0.9*lv_slider_get_value(slider),应视为课后或后续调整,不能用它覆盖本节实际展示的版本。本节还没有添加 DHT11 驱动和温湿度刷新代码,那是下一节内容。

一、风扇代码调试

本节把第 218 节从 SquareLine Studio 导出的界面放入 STM32 工程,并完成两条最基本的交互链路:

触摸 LED 按钮 → LVGL 产生 CLICKED 事件 → LED_Switch() → 翻转 PE5 → 板载 LED 改变亮灭状态

操作 Slider → LVGL 产生 CLICKED 事件 → Fan_PWM_Control() → 读取 Slider 值 → 修改 TIM3_CH2 比较值 → PB5 PWM 占空比改变 → 风扇转速改变

其中 PE5 和 PB5 的用途不同:

引脚本节功能控制来源
PE5普通 GPIO 输出,控制另一只板载 LED界面上的 LED Button
PB5TIM3_CH2 PWM 输出,同时连到电机驱动输入和板载 LED0界面上的 FAN PWM Slider

不要把二者混为同一个引脚。原始转录中的 P15、思维导图里 PWM 分支下的 PE5 都不准确:LED 按钮使用 PE5,风扇 PWM 使用 PB5。

1.1 配置流程

本节在上一课准备的 22STM32_Lvgl_DHT11 工程上继续操作。完整流程为:

  1. 用 21 号 SquareLine 工程最新导出的 UI 文件替换 22 号 Keil 工程中的旧 UI;
  2. 确认所有新生成的 .c 文件已经加入 Keil 工程;
  3. 启用界面实际使用的 Montserrat 字体并完成编译、烧写测试;
  4. 在 CubeMX 中把 PE5 配置为 LED 输出;
  5. 在 ui_events.c 的 LED_Switch() 中加入 LED 翻转代码;
  6. 在 CubeMX 中配置 TIM3_CH2,并把 PWM 输出映射到 PB5;
  7. 在 main.c 中启动 TIM3_CH2 PWM;
  8. 在 Fan_PWM_Control() 中读取 Slider 数值并修改 PWM 比较值;
  9. 编译、烧写,通过开发板 LED 和风扇验证结果。

本节只做到 LED 和 PWM 控制。配套工程当前还能看到 DHT11、TIM1 和温湿度显示代码,但它们来自下一节,不能提前并入本节源码。

1.2 打开工程

1.2.1 先验证旧工程

打开 22STM32_Lvgl_DHT11/MDK-ARM/12LCD_Display.uvprojx 后,老师先烧写旧程序,确认 LCD、下载器和原工程仍可工作。此时屏幕显示的是之前的小项目,而不是刚设计的温湿度界面。

这一检查可以把问题分开:

  • 若旧程序也无法下载,应先检查 ST-Link、供电、SWD 接线和接触情况;
  • 若旧程序能运行、替换 UI 后才失败,再检查文件替换、工程分组、头文件路径和 LVGL 配置。

视频中第一次下载异常来自移动连接线后 ST-Link 接触不良,并不是程序容量过大或 UI 代码错误。

1.2.2 替换 UI 文件

文件来源与目标位置如下:

操作目录
复制来源21SQL_DHT11/ui
替换目标22STM32_Lvgl_DHT11/midware/ui

操作步骤:

  1. 关闭或暂时切回 Keil 之外的文件管理器;
  2. 选中 21 号工程 ui 目录下本节导出的全部文件和子目录;
  3. 删除 22 号工程 midware/ui 中的旧 UI 文件;
  4. 把新文件完整粘贴到 midware/ui;
  5. 回到 Keil,按提示重新载入被外部修改的文件;
  6. 检查 Keil 的 midware/ui 分组和文件路径。

替换前最好先保留旧 UI 备份。这里删除的范围只能是明确的 22STM32_Lvgl_DHT11/midware/ui 目录内容,不能误删整个 midware,因为其中还包含 LVGL 本体及其他中间件文件。

1.2.3 把全部 UI 源文件加入 Keil

SquareLine 1.5.0 的导出结果带有子目录,至少要确认以下业务相关 .c 文件已经加入 Keil 工程:

  • midware/ui/ui.c
  • midware/ui/ui_helpers.c
  • midware/ui/ui_events.c
  • midware/ui/components/ui_comp_hook.c
  • midware/ui/screens/ui_Screen1.c

仅把文件复制到磁盘目录,不等于 Keil 会自动编译它们。Keil 工程文件必须记录这些源文件;否则可能出现对象未定义、函数未实现或链接失败。

需要区分两类问题:

  • 头文件找不到:通常是 Include Paths 没有包含 midware/ui 等目录;
  • 函数或变量链接不到:通常是相应 .c 文件没有加入工程,或者被排除在构建之外。

1.3 编译并烧写测试

1.3.1 界面制作
1.3.1.1 字号错误处理

替换 UI 后首次编译出现 4 个字体未定义错误:

  • lv_font_montserrat_18:顶部标题使用;
  • lv_font_montserrat_36:温湿度数值使用;
  • lv_font_montserrat_16:temp、humi 使用;
  • lv_font_montserrat_20:LED、FAN PWM 使用。

SquareLine 导出的 ui_Screen1.c 已经引用这些字体,但本 STM32 工程的 lv_conf.h 默认没有全部启用。打开 midware/lvgl/lv_conf.h,将对应宏设为 1:

配置宏设置值
LV_FONT_MONTSERRAT_161
LV_FONT_MONTSERRAT_181
LV_FONT_MONTSERRAT_201
LV_FONT_MONTSERRAT_361

1 表示把对应字体数据编译进固件,0 表示禁用。原始笔记所说“将报错的字号数值改为 1”实际是修改这些字体功能宏,不是把界面字号改成 1。

如果宏已经设为 1,仍然在链接阶段提示字体符号不存在,还要检查 lv_font_montserrat_16.c、18.c、20.c、36.c 是否随 LVGL 源码一起参加编译。启用更多字号会增加 Flash 占用,这是因为每个字号都有独立的字形位图和描述数据。

字体启用并补齐 UI 源文件后,课堂工程编译通过。视频中的容量工具在初次成功构建后显示 Flash 约占 58%,烧写后 LCD 正确显示上一节制作的温湿度界面。

1.3.2 添加硬件
1.3.2.1 配置 CubeMX

本节添加两部分硬件配置:PE5 普通 GPIO 输出,以及 PB5 上的 TIM3_CH2 PWM 输出。

1.3.2.1.1 添加 LED 控制

在 CubeMX 中选择 PE5,并设置:

配置项设置值
引脚PE5
模式GPIO_Output
GPIO output levelHigh
GPIO modeOutput Push Pull
Pull-up/Pull-downPull-up
Maximum output speedLow
User LabelLED

生成代码后,CubeMX 在 main.h 中创建 LED_Pin 和 LED_GPIO_Port,分别代表 PE5 的引脚号与端口;初始化阶段先输出高电平。该板载 LED 为低电平有效,所以初始高电平对应 LED 熄灭。

在 SquareLine 已生成的 ui_events.c 中填写:

HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);

Toggle 表示翻转:当前为高电平时变低,当前为低电平时变高。每次界面按钮的 CLICKED 事件调用 LED_Switch(),LED 状态便切换一次。

最初编译提示 LED_GPIO_Port、LED_Pin 未定义,是因为 ui_events.c 只包含了 ui.h。加入 #include "main.h" 后,编译器才能看到 CubeMX 生成的两个宏。

1.3.2.1.2 配置 PWM 输出参数

本项目选择 TIM3 的通道 2:

  1. 打开 Timers → TIM3;
  2. Clock Source 选择 Internal Clock;
  3. Channel 2 选择 PWM Generation CH2;
  4. 在引脚图上确认通道输出到 PB5,不要保留 CubeMX 可能自动选择的 PC7;
  5. Prescaler 设置为 7200-1;
  6. Counter Period(ARR)设置为 100-1;
  7. Auto-reload preload 选择 Enable;
  8. 初始 Pulse/CCR 设为 0;
  9. 重新生成工程代码。

STM32F103 的 TIM3_CH2 可以通过部分重映射输出到 PB5。配套工程生成的 tim.c 会自动配置 PB5 为复用推挽输出,并调用 TIM3 partial remap。这里不应手工把 PB5 再配置成普通 GPIO,否则会破坏定时器复用输出。

视频说 Prescaler “随便来一个”不够严谨。分频值和周期值共同决定 PWM 频率,不能在对频率有要求时任意填写。本工程的 APB1 定时器时钟为 72 MHz,因此:

计数时钟 = 72 MHz ÷ (7199 + 1) = 10 kHz

PWM 频率 = 10 kHz ÷ (99 + 1) = 100 Hz

寄存器中写 7200-1 和 100-1,是因为硬件实际除数和计数长度都比寄存器值多 1。计数器从 0 数到 99,共 100 个计数,因此比较值从 0 到 100 可近似对应 0% 到 100% 占空比。

Auto-reload preload 启用后,新的 ARR 周期值先进入预装载寄存器,在更新事件到来时再生效,可以避免在一个 PWM 周期中途直接改变周期造成不完整波形。本节运行时只改 CCR 比较值,没有动态修改 ARR,但保留该配置有利于输出稳定。

1.3.2.1.3 启动 TIM3 PWM

CubeMX 生成 MX_TIM3_Init() 只完成外设、通道和 GPIO 配置,并不会自动开始输出 PWM。在 main.c 中,必须等 MX_TIM3_Init() 执行完成后,再在 USER CODE BEGIN 2 区域调用:

HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);

参数含义:

  • &htim3:TIM3 的 HAL 句柄地址;
  • TIM_CHANNEL_2:启动通道 2;
  • 返回值为 HAL_StatusTypeDef,课堂没有检查返回值。

把代码放在 USER CODE 区域很重要。以后再次从 CubeMX 生成代码时,用户区内的内容会被保留;随意写在自动生成区中的代码可能被覆盖。

本工程同时启动两个定时器,但用途不同:

  • TIM6 通过中断为 LVGL 提供 1 ms 时基;
  • TIM3_CH2 在 PB5 输出控制风扇的 PWM。
1.3.2.2 事件回调函数介绍

SquareLine 在上一节已经建立两条事件转接:

  • Button1 收到 CLICKED 后调用 LED_Switch(e);
  • Slider1 收到 CLICKED 后调用 Fan_PWM_Control(e)。

lv_event_t * e 是 LVGL 事件对象指针,里面包含事件代码、触发对象、用户数据等信息。它不是“定时器事件”,而是 LVGL 图形界面的输入事件。

风扇回调第一行:

lv_obj_t * slider = lv_event_get_target(e);

含义是从事件对象中取得最初触发事件的 LVGL 控件。这里触发者是 Slider1,所以返回值可以交给 lv_slider_get_value()。LVGL 使用 C 语言实现了一套对象式 API;它具有对象、父子关系和事件等概念,但不能简单等同于 C++ 的类和面向对象语法。

风扇回调第二行最初直接使用:

__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, lv_slider_get_value(slider));

数据流为:

Slider 当前值 → lv_slider_get_value() → TIM3_CH2 的 CCR2 → PWM 占空比 → 电机平均驱动功率

__HAL_TIM_SET_COMPARE 修改指定通道的捕获/比较寄存器。PWM1 模式下,在周期固定时,CCR 越大,高电平所占时间通常越长;本项目的电机驱动板以该 PWM 信号调节风扇功率。

由于 TIM3 和 htim3 的声明位于 tim.h,若 ui_events.c 中未包含该头文件,htim3 会显示未定义。加入 #include "tim.h" 后才能正常编译。

老师从 LVGL 官方 Slider 示例中学习了这种写法:先用 lv_event_get_target(e) 取得控件对象,再用 lv_slider_get_value(slider) 读取值。官方示例的其他缓冲区、格式化和 Label 更新代码与本项目需求无关,因此课堂没有整段照搬。

1.3.2.3 低占空比启动问题与课堂调整

直接把 Slider 的 0~100 映射为 CCR 的 0~100 后,低值区域会出现电机发声但不转的现象。原因是低占空比提供的平均电压和启动转矩不足以克服静摩擦、负载及驱动损耗。电机能够维持转动的占空比,也可能低于它从静止状态启动所需的占空比。

本节最后把比较值改为:

比较值 = 20 + 0.8 × Slider值

映射关系如下:

Slider 值计算出的比较值理论占空比约为
02020%
254040%
506060%
758080%
100100100%

这样把原来的 0~100 区间压缩并平移到 20~100,减少无效的低占空比区间。视频中的电机在 Slider 接近最低端时仍能转动,而到 0 时实测停止。

这里有三个必须说明的边界:

  1. 公式在 Slider=0 时实际仍写入比较值 20,并没有在代码中明确关闭 PWM;视频里的“0 时停止”只是该电机在当时电源、驱动板和负载下的实测结果,不能保证换一台电机也停止。
  2. 20 不是通用启动阈值。不同电机、电源电压、扇叶负载和驱动板压降都会改变临界值,必须在硬件上测试。
  3. 常量 0.8 在 C 中默认是 double,计算结果最终会被隐式转换成定时器比较寄存器使用的整数,小数部分不会保留。STM32F103 的 Cortex-M3 没有硬件浮点单元,这种浮点表达式会引入软件浮点运算代码。视频中最终构建的 Flash 占用由约 58.56% 增至约 58.79%,与新增浮点运算支持代码相符;本笔记只解释影响,不擅自改写课堂代码。

若产品要求 Slider=0 时可靠停机,业务逻辑应当单独处理零值并写入 CCR=0,而不能依赖电机恰好在 20% 占空比下堵转。本节没有实现这项改动,所以不加入顶部源码。

还要注意,本节 SquareLine 工程为 Slider 选择的是 CLICKED 事件。因此回调通常在释放操作后执行一次,而不是拖动过程中连续更新。LVGL 为 Slider 提供的 VALUE_CHANGED 才会在数值变化时连续触发;这是理解现象时需要知道的差别,但本节仍严格保留课程的 CLICKED 配置。

1.4 结束

本节完成后的验证结果:

  • 新 SquareLine 界面能够在 LCD 上显示;
  • 点击界面 LED 按钮,PE5 板载 LED 能够切换亮灭;
  • 操作 FAN PWM Slider,PB5 的 PWM 占空比变化,风扇转速随之变化;
  • 与 PB5 相连的低电平有效 LED0 会呈现与风扇速度相反的亮度趋势:PWM 高电平占比越大,风扇越快,而 LED0 越暗;
  • 工程可以编译和烧写,课堂最终容量窗口显示 RAM 约 96.39%、Flash 约 58.79%。RAM 余量已经很小,后续继续加入缓冲区或大对象时要关注链接结果。

常见排查顺序如下:

现象优先检查
ST-Link 无法下载供电、SWD 线序、接口接触、下载器连接
lv_font_montserrat_xx 未定义lv_conf.h 对应字体宏是否为 1
UI 函数或对象链接失败SquareLine 导出的 .c 是否全部加入 Keil
LED_Pin 未定义ui_events.c 是否包含 main.h,CubeMX Label 是否为 LED
htim3 未定义是否包含 tim.h,TIM3 是否已经由 CubeMX 生成
PB5 没有 PWM是否选择 TIM3_CH2、PB5 重映射、调用 HAL_TIM_PWM_Start()
Slider 改变但风扇不转事件是否触发、CCR 是否变化、驱动板是否供电并与 STM32 共地、占空比是否低于启动阈值
风扇只在松手后改变本课事件为 CLICKED,不是连续触发的 VALUE_CHANGED

本节到此只完成界面、LED 与风扇 PWM 的联动。下一节才复制 DHT11 驱动、配置 TIM1 延时并更新温湿度圆弧与 Label。

1.4.1 参考资料