舵机代码完善

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

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 BSPBSP_USING_PWM3、BSP_USING_PWM3_CH4注册 pwm3 并启用通道 4
STM32 HALHAL_TIM_MODULE_ENABLED编译 HAL 定时器代码
MSP/GPIOboard.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 工程中的板级配置。手工移植时必须自己确认:

  1. board.h 已启用 PWM3 和通道 4;
  2. board.c 已包含 TIM3 时钟与 PB1 后初始化回调;
  3. 活动 HAL 配置已定义 HAL_TIM_MODULE_ENABLED;
  4. 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 deviceBSP_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() 的顺序是:

  1. 计算周期 500000*2*20 = 20000000 ns;
  2. 设置基础脉宽 500000 ns;
  3. 用 rt_device_find("pwm3") 查找设备;
  4. 找不到设备则返回错误;
  5. 找到后调用 rt_pwm_set();
  6. 调用 rt_pwm_enable() 开始输出。

原稿与视频口述把初始值说成“5 毫秒”或“五个毫米”,实际是 0.5 ms。参数换算如下:

表达式纳秒值实际时间20 ms 周期内占空比
500000*2*2020,000,000 ns20 ms100% 周期基准
500000500,000 ns0.5 ms2.5%
500000*52,500,000 ns2.5 ms12.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.c MSP 回调都要同时到位。

编译器若提示 control reaches end of non-void function,对应的是课程 pwm_init() 成功路径缺少返回值,不应把这个警告当作无关信息。日志若提示找不到 pwm3,则是运行时设备注册问题,不是再补一个函数声明就能解决。

1.11 测试舵机

视频因手机电量不足没有录下最终舵机动作,只口述“已自行测试可用”。因此学习笔记不能把它写成画面中已经完整验证。建议按以下顺序验证:

  1. 舵机先不连接门锁机械负载,使用独立且限流的合适电源,并与 STM32 共地;
  2. 复位后用示波器或逻辑分析仪检查 PB1 是否为约 20 ms 周期、0.5 ms 高电平;
  3. 人脸识别且温度低于 37 ℃时,确认脉宽变为约 2.5 ms;
  4. 停止产生新识别事件,确认约 7 秒后恢复 0.5 ms;
  5. 再连接机械结构,小范围校准开门、关门位置并检查堵转电流;
  6. 测试温度大于、等于和小于 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 次并不断打断播报;应采用越阈值事件、状态变化检测、滞回或节拍限流。

参考资料