舵机代码完善
281.7舵机代码完善
/* rtconfig.h:在 RT-Thread Settings 中启用通用 PWM 设备框架后生成。 */
#define RT_USING_PWM // 让 RT-Thread 编译 PWM 设备接口与相关驱动代码。
/* drivers/board.h:启用 STM32 的 PWM3 设备和通道 4。 */
#define BSP_USING_PWM3 // 让 STM32 BSP 创建并注册名为 pwm3 的设备。
#define BSP_USING_PWM3_CH4 // 让 PWM3 驱动包含第 4 通道,即本工程使用的 TIM3_CH4。
/* drivers/stm32f1xx_hal_conf.h:手动工程需要启用 HAL TIM 模块。 */
#define HAL_TIM_MODULE_ENABLED // 编译 STM32 HAL 定时器接口,供 PWM BSP 驱动调用。
/* drivers/board.c:从前一工程复制的 TIM3 时钟与 PB1 复用初始化回调。 */
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef* tim_pwmHandle) // 定义 PWM 定时器底层初始化回调。
{ // HAL_TIM_PWM_MspInit() 函数体开始。
if(tim_pwmHandle->Instance==TIM3) // 只在当前句柄对应 TIM3 时执行下方配置。
{ // TIM3 初始化分支开始。
__HAL_RCC_TIM3_CLK_ENABLE(); // 打开 TIM3 外设时钟,使定时器能够运行。
} // TIM3 初始化分支结束。
} // HAL_TIM_PWM_MspInit() 函数体结束。
void HAL_TIM_MspPostInit(TIM_HandleTypeDef* timHandle) // 定义定时器 GPIO 后初始化回调。
{ // HAL_TIM_MspPostInit() 函数体开始。
GPIO_InitTypeDef GPIO_InitStruct = {0}; // 创建 GPIO 配置结构体并全部清零。
if(timHandle->Instance==TIM3) // 只为 TIM3 配置对应的 PWM 输出引脚。
{ // TIM3 GPIO 配置分支开始。
__HAL_RCC_GPIOB_CLK_ENABLE(); // 打开 GPIOB 端口时钟。
GPIO_InitStruct.Pin = GPIO_PIN_1; // 选择 PB1,即默认映射下的 TIM3_CH4。
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 将 PB1 配置为复用推挽输出。
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; // 将 GPIO 输出速度配置为低速档。
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); // 应用上述配置,让 TIM3_CH4 波形从 PB1 输出。
} // TIM3 GPIO 配置分支结束。
} // HAL_TIM_MspPostInit() 函数体结束。
/* applications/pwm_cfg.h:本节新建的 PWM 配置头文件。 */
#ifndef APPLICATIONS_PWM_CFG_H_ // 若尚未包含本头文件,则保留下方声明。
#define APPLICATIONS_PWM_CFG_H_ // 定义头文件保护宏,防止同一编译单元重复包含。
#include <rtthread.h> // 引入 RT-Thread 基本类型、错误码和日志接口。
#include <rtdevice.h> // 引入 PWM 设备类型及操作接口。
#define PWM_DEV_NAME "pwm3" // 指定要查找的 PWM 设备注册名为 pwm3。
#define PWM_DEV_CHANNEL 4 // 指定使用 PWM3 的第 4 通道。
struct rt_device_pwm *pwm_dev; // 定义全局 PWM 设备句柄,保存设备查找结果。
int pwm_init(void); // 声明 PWM 初始化函数,供 main.c 调用。
#endif // 结束 APPLICATIONS_PWM_CFG_H_ 头文件保护范围。
/* applications/pwm_cfg.c:由第 14 个工程的 PWM 测试代码精简而来。 */
#include "pwm_cfg.h" // 引入 PWM 设备名、通道、句柄和初始化函数声明。
int pwm_init(void) // 定义 PWM 初始化函数;课程源码的返回类型是 int。
{ // pwm_init() 函数体开始。
rt_uint32_t period, pulse; // 定义 PWM 周期和有效脉冲宽度,单位均为纳秒。
period = 500000*2*20; // 计算得到 20000000 ns,即 20 ms、50 Hz 的周期。
pulse = 500000; // 设置初始脉宽为 500000 ns,即 0.5 ms。
pwm_dev = (struct rt_device_pwm *)rt_device_find(PWM_DEV_NAME); // 查找 pwm3 并保存其 PWM 设备指针。
if (pwm_dev == RT_NULL) // 判断系统是否没有注册 pwm3 设备。
{ // PWM 设备查找失败分支开始。
rt_kprintf("pwm sample run failed! can't find %s device!\n", PWM_DEV_NAME); // 打印设备查找失败信息。
return RT_ERROR; // 返回错误,避免继续操作空设备指针。
} // PWM 设备查找失败分支结束。
rt_pwm_set(pwm_dev, PWM_DEV_CHANNEL, period, pulse); // 把通道 4 设置为 20 ms 周期、0.5 ms 脉宽。
rt_pwm_enable(pwm_dev, PWM_DEV_CHANNEL); // 使能 PWM3 通道 4,开始从 PB1 输出控制波形。
} // pwm_init() 成功路径到此结束;课程源码漏写了 int 返回值。
/* applications/uart_cfg.h:让 main.c 能访问识别事件标志。 */
extern uint8_t face_check_flag; // 声明标志实体定义在其他源文件中,此处不分配第二份存储空间。
/* applications/uart_cfg.c:新增标志实体及识别成功后的置位操作。 */
uint8_t face_check_flag=0; // 定义识别事件标志;0 表示当前没有待处理的开门事件。
if(rt_tick_get() - k210_last_tick > 5*1000) // 仅在识别成功语音通过 5 秒门控后处理本次事件。
{ // 允许处理本次识别成功事件的分支开始。
rt_device_write(serial3, 0, voice_check_face_succ, 5); // 向语音模块发送“人脸识别成功”命令。
k210_last_tick = rt_tick_get(); // 更新识别语音的 5 秒门控基准时刻。
face_check_flag=1; // 通知 main 线程存在一次已接受的人脸识别事件。
} // 允许处理本次识别成功事件的分支结束。
/* applications/main.c:本节完成后的测温、报警与舵机开关门逻辑。 */
#include <rtthread.h> // 引入 RT-Thread 核心类型、节拍和线程延时接口。
#include "key_cfg.h" // 引入蜂鸣器引脚、按键与 io_init() 声明。
#include "oled_cfg.h" // 引入 OLED 初始化与温度显示函数声明。
#include "GY906.h" // 引入红外温度传感器初始化与读取函数声明。
#include "pwm_cfg.h" // 引入 PWM 设备句柄、通道宏和 pwm_init() 声明。
#include "uart_cfg.h" // 引入串口初始化函数及 face_check_flag 的外部声明。
float temp; // 定义全局浮点变量,保存当前测得的目标温度。
int main(void) // 定义 RT-Thread 用户程序入口。
{ // main() 函数体开始。
static rt_tick_t open_door_tick = 0; // 保存最近一次开门时刻;0 同时表示当前没有待执行的关门计时。
DisplayCfg_Init(); // 初始化 U8g2 和 SSD1306 OLED。
GY906_Init(); // 初始化 GY-906 使用的软件 SMBus 引脚。
io_init(); // 初始化蜂鸣器和人脸录入、删除按键。
uart3_init(); // 初始化向语音模块发送命令的 UART3。
uart2_init(); // 初始化与 K210 通信的 UART2 及其接收线程。
pwm_init(); // 查找、设置并使能 PWM3 通道 4,初始脉宽表示课程设定的关门位置。
while (1) // 永久循环,持续执行测温、报警和门控逻辑。
{ // 主循环开始。
temp = SMBus_ReadTemp(); // 读取 MLX90614 测得的目标温度。
Display_normal(temp); // 把当前温度刷新到 OLED。
if(temp>37) // 判断温度是否严格大于 37 ℃。
{ // 高温报警分支开始。
rt_pin_write(BEEP_PIN_NUM, PIN_HIGH); // 将 PB8 置高,使课程使用的蜂鸣器发声。
}else { // 温度不大于 37 ℃时执行此分支。
rt_pin_write(BEEP_PIN_NUM, PIN_LOW); // 将 PB8 置低,关闭蜂鸣器。
} // 高温报警判断结束。
if(face_check_flag && temp<37) // 同时满足识别事件有效和温度低于 37 ℃时才开门。
{ // 开门处理分支开始。
rt_pwm_set(pwm_dev, PWM_DEV_CHANNEL, 500000*2*20, 500000*5); // 设置 20 ms 周期、2.5 ms 脉宽,转到课程定义的开门位置。
face_check_flag=0; // 消耗本次识别事件,避免主循环反复执行开门设置。
open_door_tick = rt_tick_get(); // 记录本次开门时刻,供非阻塞关门计时使用。
} // 开门处理分支结束。
if((rt_tick_get()-open_door_tick>7*1000) && (open_door_tick != 0)) // 开门后超过 7000 tick 且计时标志有效时关门。
{ // 自动关门处理分支开始。
rt_pwm_set(pwm_dev, PWM_DEV_CHANNEL, 500000*2*20, 500000); // 恢复 20 ms 周期、0.5 ms 脉宽,转到课程定义的关门位置。
open_door_tick=0; // 清除关门计时标志,防止以后每轮重复设置关门脉宽。
} // 自动关门处理分支结束。
rt_thread_mdelay(100); // 让 main 线程休眠 100 ms,再执行下一轮检测。
} // 主循环结束。
return RT_EOK; // 保留课程源码的成功返回语句;正常运行时不会执行到这里。
} // main() 函数体结束。
文首代码来自课程归档
15PRJ的本节最终状态:PWM 配置链、pwm_cfg模块、识别标志与开关门业务均已完成。代码本身、数值和函数签名按课程原样保留,只移除旧注释并增加逐行说明;源码中成功路径漏返回值、头文件定义全局对象以及温度门控的状态漏洞没有被擅自修复,而是在正文中明确指出。
一、舵机代码完善
本节完成最后一条业务链:K210 的有效人脸识别事件经 UART2 接收线程置位标志,main 线程结合当前温度决定是否改变 PWM 脉宽开门,并用系统节拍在约 7 秒后自动关门。
数据流可以概括为:
K210 识别成功 → UART2 解析并通过 5 秒门控 → face_check_flag = 1 → main 检查 temp < 37 → PB1/TIM3_CH4 改变舵机位置 → 7 秒后恢复关门脉宽
1.1 建文件夹和头文件
视频口述的“建两个文件夹”不准确,实际是在 applications 下新建两个文件:pwm_cfg.h 和 pwm_cfg.c。归档中也不存在 pwm_kfig.h、pwm_config.c 或“pwm 肯税格”等名称。
1.1.1 建源文件
pwm_cfg.c 从第 14 个工程的 pwm_test.c 精简而来,删除了 MSH 命令参数、五档循环、方向变量和延时,只留下设备查找、初始 PWM 设置与通道使能。
课程实际把函数从:
static int pwm_led_sample(int argc, char *argv[])
改成:
int pwm_init(void)
这里改为 void 的是参数列表,表示函数不接收参数;返回类型仍然是 int。原稿所说“返回类型改为 void”与归档源码不符。
pwm_cfg.h 保存设备名、通道号、设备句柄和函数声明。当前写法把 pwm_dev 的实体定义放在头文件中;多个 .c 文件包含该头时可能出现重复定义。更规范的方式是头文件写 extern struct rt_device_pwm *pwm_dev;,只在 pwm_cfg.c 定义一次,但本节没有进行这项重构。
归档还有一个真实缺陷:pwm_init() 声明为返回 int,查找失败时返回 RT_ERROR,成功路径却直接运行到右花括号,没有返回 RT_EOK。main() 当前忽略返回值,所以实物可能仍能运行,但编译器可对此发出警告;若调用者读取该返回值,结果不可靠。精校稿不修改课程源码,正式工程应补齐成功返回值并检查它。
1.1.2 建应用
应用层 PWM 能工作,需要同时满足四层配置:
| 层级 | 本节设置 | 作用 |
|---|---|---|
| RT-Thread 设备框架 | RT_USING_PWM | 编译通用 PWM 设备接口 |
| STM32 BSP | BSP_USING_PWM3、BSP_USING_PWM3_CH4 | 注册 pwm3 并启用通道 4 |
| STM32 HAL | HAL_TIM_MODULE_ENABLED | 编译 HAL 定时器代码 |
| MSP/GPIO | board.c 中两个回调 | 打开 TIM3、GPIOB 时钟并把 PB1 配为复用输出 |
只复制 pwm_cfg.c 不足以产生波形。相反,底层配置全部正确但应用没有执行 rt_pwm_set() 和 rt_pwm_enable(),PB1 也不会按本节参数持续输出。
1.2 舵机代码使用
视频中的“两个三角 PWM”指 RT-Thread Settings 中 PWM 设备驱动的配置入口。启用后保存,工程生成 RT_USING_PWM,并可能更新工程索引或依赖文件;应等待保存过程完成再编译。
本节没有重新使用 CubeMX 生成全部工程,而是复用前一个 PWM 工程中的板级配置。手工移植时必须自己确认:
board.h已启用 PWM3 和通道 4;board.c已包含 TIM3 时钟与 PB1 后初始化回调;- 活动 HAL 配置已定义
HAL_TIM_MODULE_ENABLED; rtconfig.h已生成RT_USING_PWM。
所谓“打开 HAL 小开关”就是启用 HAL_TIM_MODULE_ENABLED,不是运行时的舵机开关。它决定相关 HAL 源码是否参与条件编译。
1.3 修改配置
本归档工程实际生效的 HAL 配置文件是:
drivers/stm32f1xx_hal_conf.h
视频所说的 config.h、config.back.h 是对不同 RT-Thread Studio/CubeMX 工程生成方式的口头概括,不能作为固定文件名照抄。判断哪个文件生效,应从源码包含关系、工程构建日志和宏定义跳转结果确认;本节归档没有名为 config.back.h 的活动配置文件。
应在非注释区域确认:
#define HAL_TIM_MODULE_ENABLED
该宏只让 HAL TIM 模块参加编译,不会替代 BSP_USING_PWM3、通道宏或 PB1 的 MSP 配置。四层缺一不可。
1.4 重新编译
本节同时修改 RT-Thread Settings、BSP 宏、HAL 配置、board.c、应用头文件和主函数,必须保存后重新完整编译。诊断顺序可按错误发生的层级展开:
| 现象 | 优先检查 |
|---|---|
找不到 PWM 类型或 rt_pwm_* 声明 | RT_USING_PWM、是否包含 <rtdevice.h> |
| HAL TIM 类型或函数未定义 | HAL_TIM_MODULE_ENABLED |
| 链接时找不到 PWM 驱动符号 | Settings 是否保存、工程是否重新生成/刷新 |
运行时打印 can't find pwm3 device | BSP_USING_PWM3 和通道配置 |
| 找到设备但 PB1 无波形 | board.c MSP 回调、PB1 复用、通道号和测量接地 |
编译通过只证明语法、声明和链接基本成立,不证明设备已经注册,也不证明 PB1 上有正确波形。
1.5 修改头文件
课程遇到的头文件问题最终对应 #include "pwm_cfg.h",不是原稿中的 pwm_ctrl.h 或 pwm_config.h。文件名、包含语句和头文件保护宏必须一致。
main.c 还必须包含 uart_cfg.h,因为 face_check_flag 的 extern 声明位于其中。没有可见声明就使用变量,会产生未声明标识符错误。IDE 自动补全或其他头文件偶然间接包含不能代替明确依赖。
1.6 舵机设备设置
pwm_init() 的顺序是:
- 计算周期
500000*2*20 = 20000000 ns; - 设置基础脉宽
500000 ns; - 用
rt_device_find("pwm3")查找设备; - 找不到设备则返回错误;
- 找到后调用
rt_pwm_set(); - 调用
rt_pwm_enable()开始输出。
原稿与视频口述把初始值说成“5 毫秒”或“五个毫米”,实际是 0.5 ms。参数换算如下:
| 表达式 | 纳秒值 | 实际时间 | 20 ms 周期内占空比 |
|---|---|---|---|
500000*2*20 | 20,000,000 ns | 20 ms | 100% 周期基准 |
500000 | 500,000 ns | 0.5 ms | 2.5% |
500000*5 | 2,500,000 ns | 2.5 ms | 12.5% |
20 ms 周期对应 50 Hz。舵机主要根据每个周期中的有效脉宽决定目标位置,不应把它理解成普通直流模拟电压。
1.7 配置舵机
课程把 0.5 ms 位置约定为“关门”,把 2.5 ms 位置约定为“开门”。这只是当前机械结构和舵机方向的业务映射,不是所有舵机的通用方向:换一侧安装摇臂、改变连杆方向或使用不同型号后,两端可能相反。
0.5~2.5 ms 也不是所有舵机都保证支持的安全范围。初次调试应断开门锁负载,从约 1.5 ms 中位开始小幅改变,观察舵机是否到达机械限位;若到限位后仍嗡鸣、发热或持续大电流,应立即缩小脉宽范围并断电检查。
本节直接在 main.c 中再次写死周期和两个脉宽,没有复用 pwm_init() 的局部变量。这样保持了课程源码的简单写法,但参数分散在多个位置,修改舵机范围时容易漏改。
硬件连接至少要满足:
| 舵机端 | STM32/电源端 | 说明 |
|---|---|---|
| 控制信号 | PB1 / TIM3_CH4 | 只传送 3.3 V 逻辑 PWM |
| 正电源 | 符合具体舵机额定值且电流充足的电源 | 不能从 GPIO 或能力不足的 3.3 V 稳压输出取电 |
| GND | 舵机电源 GND 与 STM32 GND 相连 | 独立供电时仍必须共地 |
舵机启动、快速换向和堵转时电流会明显增大。电源能力不足或地线压降可能导致 STM32、K210 或语音模块复位;应让舵机电源留有余量、靠近负载去耦,并在机械结构装好前验证极性和限位。
1.8 人脸检测标志位
变量的准确名称是 face_check_flag,类型为 uint8_t:
uart_cfg.c中的uint8_t face_check_flag=0;是唯一实体定义并分配存储;uart_cfg.h中的extern uint8_t face_check_flag;只是外部声明;main.c包含uart_cfg.h后才能使用该名称;- 外部声明不应再写初始化值,否则它会成为另一个定义,可能导致重复定义问题。
该标志并不代表 K210 当前摄像头中“持续有人脸”。它只在识别成功帧通过上一节 5 秒语音门控、并发送识别成功语音之后被置为 1,因此更准确的含义是“有一次已接受、尚未被 main 消耗的开门事件”。
这是一位事件锁存器,不是计数队列:在 main 清零之前,即使 UART2 线程又置位多次,仍只能表示 1 次事件。UART2 接收线程写它,main 线程读写它,但课程代码没有使用消息队列、事件对象、临界区或其他同步机制;课堂实物可能正常工作,正式多线程代码更适合用 RT-Thread IPC 传递事件并明确内存同步语义。
1.9 开门关门逻辑
开门需要同时满足:
face_check_flag != 0 && temp < 37
满足后,程序把 PWM 脉宽改为 2.5 ms,立即清除标志,并记录 open_door_tick。这里没有阻塞 7 秒;主循环仍每 100 ms 继续测温和刷新 OLED。
自动关门条件为:
当前 tick - open_door_tick > 7000 && open_door_tick != 0
本工程 RT_TICK_PER_SECOND = 1000,所以 7000 tick 约为 7 秒。由于使用严格大于并且主循环每约 100 ms 才检查一次,实际关门动作会发生在略超过 7 秒后的某次轮询。关门后将 open_door_tick 清零,第二个条件随即失效,避免每一轮都重复下发关门脉宽。
选择 7 秒大于识别事件的 5 秒门控,会产生以下实际行为:如果同一张脸持续被识别,每隔约 5 秒又产生一次有效事件,程序会再次设置开门脉宽并重置 open_door_tick,于是关门倒计时不断后移,门可以保持打开;当不再收到有效识别事件后,最后一次开门时刻再过约 7 秒才关闭。
温度边界必须精确区分:
| 温度 | 蜂鸣器 | 有识别事件时是否开门 |
|---|---|---|
temp > 37 | 响 | 否 |
temp == 37 | 不响 | 否 |
temp < 37 | 不响 | 是 |
这是因为蜂鸣器用 > 37,开门用 < 37,恰好 37 ℃没有报警但也不开门。
当前源码还有一个重要状态漏洞:如果有效识别事件到来时 temp >= 37,开门分支不执行,face_check_flag 也不会被清零。以后温度一旦降到 37 ℃以下,保留的旧标志就会触发开门,即使此时没有再次识别人脸。正式门禁至少应给识别授权设置有效期,并在高温拒绝、超时或状态变化时明确消费/清除事件;本节只按课程源码记录其行为,不在文首擅自修复。
还应注意,0.5 ms 与 2.5 ms 只是发给舵机的目标信号。程序没有限位开关、门位反馈、电流检测或堵转检测,所以“已经设置关门脉宽”不等于机械门一定成功关闭。
1.10 编译问题处理
本节遇到的主要问题来自声明和配置链不完整:
main.c要包含pwm_cfg.h,否则看不到pwm_dev、PWM_DEV_CHANNEL和pwm_init();main.c要包含uart_cfg.h,否则看不到face_check_flag;pwm_cfg.h要包含<rtdevice.h>,否则 PWM 设备类型和接口不可见;pwm_init()必须在头文件声明并在pwm_cfg.c定义;- Settings、BSP 宏、HAL TIM 宏与
board.cMSP 回调都要同时到位。
编译器若提示 control reaches end of non-void function,对应的是课程 pwm_init() 成功路径缺少返回值,不应把这个警告当作无关信息。日志若提示找不到 pwm3,则是运行时设备注册问题,不是再补一个函数声明就能解决。
1.11 测试舵机
视频因手机电量不足没有录下最终舵机动作,只口述“已自行测试可用”。因此学习笔记不能把它写成画面中已经完整验证。建议按以下顺序验证:
- 舵机先不连接门锁机械负载,使用独立且限流的合适电源,并与 STM32 共地;
- 复位后用示波器或逻辑分析仪检查 PB1 是否为约 20 ms 周期、0.5 ms 高电平;
- 人脸识别且温度低于 37 ℃时,确认脉宽变为约 2.5 ms;
- 停止产生新识别事件,确认约 7 秒后恢复 0.5 ms;
- 再连接机械结构,小范围校准开门、关门位置并检查堵转电流;
- 测试温度大于、等于和小于 37 ℃三个边界,并专门验证旧识别标志是否会在降温后误开门。
归档工程的时钟源配置为内部 HSI,经系统时钟配置运行到目标频率;并没有使用外部 HSE 晶振。切换外部晶振不能只改一个宏,还要核对晶振频率、PLL、Flash 等待周期、总线分频以及 UART/PWM 的派生时钟,再重新测量串口波特率和 PWM 周期。
复位日志只有 UART2、UART3 初始化提示,是因为其他初始化函数没有打印成功信息;没有日志不等于没有初始化。若要补日志,应同时检查初始化/API 返回值,避免先打印“成功”再执行可能失败的操作。
本项目显式创建的额外应用线程只有 UART2 接收线程,但 RT-Thread 运行时还存在 main 线程、空闲线程以及按配置启用的系统线程,不能概括为“没有使用多线程”。UART2 接收线程通过信号量等待数据;main 线程负责测温、显示、报警与舵机状态轮询。
温度异常时课程只驱动蜂鸣器,没有发送 AA 55 04 55 AA 语音命令。若自行增加,不能在 100 ms 主循环中只要 temp > 37 就重复发送,否则每秒可能下发约 10 次并不断打断播报;应采用越阈值事件、状态变化检测、滞回或节拍限流。