风扇代码调试(1)
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 |
| PB5 | TIM3_CH2 PWM 输出,同时连到电机驱动输入和板载 LED0 | 界面上的 FAN PWM Slider |
不要把二者混为同一个引脚。原始转录中的 P15、思维导图里 PWM 分支下的 PE5 都不准确:LED 按钮使用 PE5,风扇 PWM 使用 PB5。
1.1 配置流程
本节在上一课准备的 22STM32_Lvgl_DHT11 工程上继续操作。完整流程为:
- 用 21 号 SquareLine 工程最新导出的 UI 文件替换 22 号 Keil 工程中的旧 UI;
- 确认所有新生成的
.c文件已经加入 Keil 工程; - 启用界面实际使用的 Montserrat 字体并完成编译、烧写测试;
- 在 CubeMX 中把 PE5 配置为 LED 输出;
- 在
ui_events.c的LED_Switch()中加入 LED 翻转代码; - 在 CubeMX 中配置 TIM3_CH2,并把 PWM 输出映射到 PB5;
- 在
main.c中启动 TIM3_CH2 PWM; - 在
Fan_PWM_Control()中读取 Slider 数值并修改 PWM 比较值; - 编译、烧写,通过开发板 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 |
操作步骤:
- 关闭或暂时切回 Keil 之外的文件管理器;
- 选中 21 号工程
ui目录下本节导出的全部文件和子目录; - 删除 22 号工程
midware/ui中的旧 UI 文件; - 把新文件完整粘贴到
midware/ui; - 回到 Keil,按提示重新载入被外部修改的文件;
- 检查 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.cmidware/ui/ui_helpers.cmidware/ui/ui_events.cmidware/ui/components/ui_comp_hook.cmidware/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_16 | 1 |
LV_FONT_MONTSERRAT_18 | 1 |
LV_FONT_MONTSERRAT_20 | 1 |
LV_FONT_MONTSERRAT_36 | 1 |
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 level | High |
| GPIO mode | Output Push Pull |
| Pull-up/Pull-down | Pull-up |
| Maximum output speed | Low |
| User Label | LED |
生成代码后,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:
- 打开 Timers → TIM3;
- Clock Source 选择 Internal Clock;
- Channel 2 选择 PWM Generation CH2;
- 在引脚图上确认通道输出到 PB5,不要保留 CubeMX 可能自动选择的 PC7;
- Prescaler 设置为
7200-1; - Counter Period(ARR)设置为
100-1; - Auto-reload preload 选择 Enable;
- 初始 Pulse/CCR 设为 0;
- 重新生成工程代码。
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 值 | 计算出的比较值 | 理论占空比约为 |
|---|---|---|
| 0 | 20 | 20% |
| 25 | 40 | 40% |
| 50 | 60 | 60% |
| 75 | 80 | 80% |
| 100 | 100 | 100% |
这样把原来的 0~100 区间压缩并平移到 20~100,减少无效的低占空比区间。视频中的电机在 Slider 接近最低端时仍能转动,而到 0 时实测停止。
这里有三个必须说明的边界:
- 公式在 Slider=0 时实际仍写入比较值 20,并没有在代码中明确关闭 PWM;视频里的“0 时停止”只是该电机在当时电源、驱动板和负载下的实测结果,不能保证换一台电机也停止。
20不是通用启动阈值。不同电机、电源电压、扇叶负载和驱动板压降都会改变临界值,必须在硬件上测试。- 常量
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。