RTT_PWM

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

271.3.7RTT_PWM

#include <rtthread.h>                                      // 引入 RT-Thread 内核接口,本例使用整数类型、错误码、延时函数、打印函数和 MSH 导出宏。
#include <rtdevice.h>                                      // 引入 RT-Thread 设备接口,本例使用 PWM 设备类型及操作函数。

#define PWM_DEV_NAME        "pwm3"                        // 指定要查找的 PWM 设备注册名为 pwm3。
#define PWM_DEV_CHANNEL     4                             // 指定使用 PWM3 的通道 4,本板上对应 TIM3_CH4/PB1。

struct rt_device_pwm *pwm_dev;                            // 定义全局 PWM 设备句柄,用于保存查找到的 pwm3 设备。

static int pwm_led_sample(int argc, char *argv[])          // 定义 MSH 命令函数;最终舵机版本没有使用 argc 和 argv。
{                                                         // pwm_led_sample 函数体开始。
    rt_uint32_t period, pulse, dir;                       // 定义周期、基础脉宽和方向变量;最终舵机逻辑不再使用 dir。

    period = 500000*2*20;                                 // 按原式得到 20000000 ns,即 20 ms、50 Hz 的 PWM 周期。
    dir = 1;                                              // 保留配套源码赋值;呼吸灯增减逻辑被注释后,该变量不再参与运算。
    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 的设备。
    {                                                     // 设备查找失败分支开始。
        rt_kprintf("pwm sample run failed! can't find %s device!\n", PWM_DEV_NAME); // 打印找不到目标 PWM 设备的错误信息。
        return RT_ERROR;                                  // 立即返回错误,避免继续访问空设备指针。
    }                                                     // 设备查找失败分支结束。

    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 输出波形。

    while (1)                                             // 无限循环,依次输出五档脉宽控制舵机位置。
    {                                                     // 无限循环体开始。

        rt_pwm_set(pwm_dev, PWM_DEV_CHANNEL, period, pulse); // 输出 0.5 ms 脉宽。
        rt_thread_mdelay(1000);                            // 保持当前控制信号约 1 秒。
        rt_pwm_set(pwm_dev, PWM_DEV_CHANNEL, period, pulse*2); // 输出 1.0 ms 脉宽。
        rt_thread_mdelay(1000);                            // 保持当前控制信号约 1 秒。
        rt_pwm_set(pwm_dev, PWM_DEV_CHANNEL, period, pulse*3); // 输出 1.5 ms 脉宽。
        rt_thread_mdelay(1000);                            // 保持当前控制信号约 1 秒。
        rt_pwm_set(pwm_dev, PWM_DEV_CHANNEL, period, pulse*4); // 输出 2.0 ms 脉宽。
        rt_thread_mdelay(1000);                            // 保持当前控制信号约 1 秒。
        rt_pwm_set(pwm_dev, PWM_DEV_CHANNEL, period, pulse*5); // 输出 2.5 ms 脉宽。
        rt_thread_mdelay(1000);                            // 保持当前控制信号约 1 秒,然后从 0.5 ms 重新循环。
    }                                                     // 无限循环体结束;正常执行时不会离开这里。
}                                                         // pwm_led_sample 函数体结束;因前面是无限循环,没有可达的正常返回语句。
MSH_CMD_EXPORT(pwm_led_sample, pwm sample);                // 导出可在 MSH 中执行的 pwm_led_sample 命令。

上方代码来自课程配套工程 14IO_DEVICE2/applications/pwm_test.c 的最终保存状态。它已经由最初的呼吸灯示例改为舵机测试:有效语句、表达式、名称和字符串均保持原样,仅移除源码旧注释及已经注释掉的呼吸灯语句,再补充逐行说明。源码中“周期为 0.5 ms”的旧注释与表达式 500000*2*20 不符;实际周期是 20 ms。

一、PWM设备

PWM(Pulse Width Modulation,脉宽调制)让数字引脚周期性输出高、低电平。它仍是数字波形,并不会把 PB1 变成真正的模拟电压输出。常用参数之间的关系是:

  • 周期 T:一帧波形经历的时间;RT-Thread PWM 接口用纳秒表示。
  • 频率 f:f = 1/T,若周期以纳秒给出,则 f = 10^9 / period。
  • 脉宽 pulse:一个周期内有效电平保持的时间。
  • 占空比:对本节的高电平有效 PWM,可写成 pulse / period × 100%。

本节先使用 0.5 ms 周期和渐变脉宽观察呼吸灯,再把周期改为 20 ms、脉宽改为 0.5~2.5 ms驱动舵机。两段实验复用 PWM3 通道 4,但参数含义和负载完全不同。

1.1 访问PWM设备

RT-Thread 把 PWM 控制器注册为设备,应用层不直接操作 TIM3 寄存器。课程代码的调用路径如下:

  1. rt_device_find("pwm3") 按名称取得设备对象。
  2. rt_pwm_set(pwm_dev, 4, period, pulse) 设置通道 4 的周期和脉宽,两个时间参数的单位都是纳秒。
  3. rt_pwm_enable(pwm_dev, 4) 使能输出;不再使用时可调用 rt_pwm_disable()。

其中,设备名和通道号来自两层配置:BSP_USING_PWM3 让 STM32 BSP 创建并注册 pwm3,BSP_USING_PWM3_CH4 让驱动包含第 4 通道。只打开 RT-Thread 的通用 PWM 框架,并不会自动生成具体的 pwm3 设备。

1.2 PWM设备使用示例

1.2.1 创建PWM项目

课程在 applications 目录中新建 pwm_test.c,再粘贴 RT-Thread PWM 示例。完整配置顺序如下:

  1. 在 RT-Thread Settings 中搜索并启用 PWM 设备驱动,保存配置,使工程生成 RT_USING_PWM。

  2. 在 drivers/board.h 中启用 PWM3 及其第 4 通道:

    #define BSP_USING_PWM3
    #define BSP_USING_PWM3_CH4
    
  3. 在 CubeMX 中把 TIM3 的通道 4 设为 PWM Generation CH4。STM32F103ZE 默认映射下,该输出位于 PB1,即 TIM3_CH4。

  4. 生成 CubeMX 参考代码,将本节所需的 MSP 初始化回调复制到 drivers/board.c。

  5. 确认活动工程的 HAL 配置已启用 TIM 模块,然后重新编译。

视频开头提到的 adc.c“看得见或看不见”,取决于 RT-Thread Studio 工程树当前选择的是 C/C++ Projects 还是 Project Explorer 视图。这属于 IDE 显示方式,不是创建 PWM 工程的必要步骤。

1.2.2 复制定时器代码

CubeMX 生成文件是 cubemx/Src/tim.c,不是 OCR 原稿中的 time.c。本节从中复制的是以下两个 HAL 回调,而不是整个 MX_TIM3_Init():

  • HAL_TIM_PWM_MspInit():当句柄实例为 TIM3 时,打开 TIM3 外设时钟。
  • HAL_TIM_MspPostInit():打开 GPIOB 时钟,并将 PB1 配为复用推挽输出。

粘贴目标是 drivers/board.c 的文件作用域,不能把函数嵌进 rt_hw_board_init() 或其他函数体中。课程工程的 cubemx/Src/tim.c 被排除出实际构建;它在这里是 CubeMX 生成硬件配置的参考,而 STM32 BSP 的 drivers/drv_pwm.c 才负责运行时的 PWM 初始化与设备注册。

1.2.3 复制定时器初始化函数

HAL_TIM_PWM_MspInit() 解决“TIM3 能否得到外设时钟”,HAL_TIM_MspPostInit() 解决“TIM3_CH4 能否从 PB1 引出”。本节对应的关键硬件动作是:

动作对应资源缺失时的典型结果
打开 TIM3 时钟__HAL_RCC_TIM3_CLK_ENABLE()定时器计数器不能正常工作
打开 GPIOB 时钟__HAL_RCC_GPIOB_CLK_ENABLE()无法正确配置 PB1
配置 PB1GPIO_MODE_AF_PP,即复用推挽输出TIM3_CH4 波形不能从引脚输出

这里的“初始化函数”特指这两个 MSP 回调。最终工程没有照搬 CubeMX 的 MX_TIM3_Init();实际的计数模式、极性、预分频、自动重装值和比较值由 RT-Thread STM32 PWM 驱动结合 period、pulse 动态设置。

1.2.4 确认定时器打开

需要检查的是活动工程中的 cubemx/Inc/stm32f1xx_hal_conf.h,其中应存在:

#define HAL_TIM_MODULE_ENABLED

它决定 HAL TIM 驱动是否参与编译,但它不等于“TIM3 已经开始输出”。要形成完整链路,还必须同时满足:

  • RT-Thread Settings 生成 RT_USING_PWM;
  • board.h 启用 BSP_USING_PWM3 与 BSP_USING_PWM3_CH4;
  • board.c 中存在正确的 TIM3/PB1 MSP 初始化;
  • 应用找到 pwm3、调用 rt_pwm_set(),并执行 rt_pwm_enable()。

若编译通过但运行时提示 can't find pwm3 device,应优先检查 PWM 框架和 BSP 宏;若能找到设备但 PB1 没有波形,则继续检查通道宏、PB1 复用配置、接线以及示波器地线。

1.2.5 应用案例
1.2.5.1 例题: 呼吸灯测试

课程最初粘贴的官方示例是呼吸灯版本,其核心参数为:

  • period = 500000 ns = 0.5 ms,频率为 2 kHz;
  • pulse 从 0 开始,每轮增加或减少 5000 ns;
  • 当 pulse 到达 period 时改为递减,到达 0 时再改为递增。

因此脉宽的实际变化范围是 0~500000 ns,不是 OCR 原稿中的“0~5000”。每次变化 5000 ns,相当于占空比变化 1 个百分点,一程共有约 100 步。PWM 频率足够高时,人眼主要感受到平均亮度的平滑变化;但 LED 的亮度与占空比通常不是严格线性关系,还会受到 LED 驱动电路及人眼感知曲线影响。

1.2.5.2 改变灯光方法

CubeMX 为 TIM3_CH4 选出的引脚是 PB1,而视频所用开发板上的目标 LED 接在 PB5,因此直接输出 PB1 不会自动点亮该 LED。课程给出了两种思路:

  1. 根据开发板原理图,重新选择能复用到目标 LED 引脚的定时器通道,并同步修改 BSP 实例、通道和 MSP 配置。
  2. 演示时临时用跳线把 PB1 的 PWM 信号送到 PB5 所在的 LED 网络,以验证呼吸效果。

第二种方法必须先确认 PB5 没有被软件配置为与 PB1 相反电平的推挽输出,否则两个输出端直接相连可能产生大电流甚至损坏器件。较稳妥的做法是让 PB5 保持输入/高阻态,并依据板载 LED 原理图确认限流电阻与有效电平。若 LED 是低电平点亮,占空比增大与视觉亮度的方向会和高电平点亮电路相反。

1.2.5.3 调整呼吸灯代码

原示例每步延时 50 ms,约 100 步完成一次由暗到亮或由亮到暗的变化,因此单程约需 5 s。视频先尝试把延时改为 8 ms,随后使用 10 ms;在不计函数执行时间时,单程约为 1 s,完整呼吸周期约为 2 s。

延时只改变“多久调整一次占空比”,不改变 PWM 载波本身的 2 kHz 频率。若要让渐变更平滑,可减小每次的脉宽步进;若要改变整体节奏,可调整步进、延时或两者,但始终应保证 0 ≤ pulse ≤ period。

1.2.5.4 呼吸灯代码分析

呼吸灯的循环逻辑可拆成四步:根据 dir 增减 pulse,在上下边界反转方向,调用 rt_pwm_set() 把新脉宽写入通道,最后延时后再进行下一步。rt_pwm_set() 负责更新硬件比较值,rt_thread_mdelay() 只负责控制渐变速度。

本工程的 STM32 PWM 驱动会根据纳秒单位的周期计算定时器预分频和自动重装值,再根据脉宽计算捕获比较值;若请求脉宽大于周期,驱动还会把它限制到周期以内。应用仍应主动传入合理参数,不能依赖钳位来掩盖单位或算法错误。

配套工程最终保存时,整段 dir 增减逻辑已经被注释,延时改为 1000 ms,并依次设置五档脉宽,所以文件开头展示的是舵机版本。变量 dir 仍被声明和赋值,但在最终有效代码中没有用途,这是课程源码的真实状态。

1.2.5.5 改变舵机控制代码

舵机版保留 pulse = 500000 ns 作为基数,并把周期写成 500000*2*20:

表达式实际时间20 ms 周期内的占空比
period20 ms100%
pulse0.5 ms2.5%
pulse*21.0 ms5%
pulse*31.5 ms7.5%
pulse*42.0 ms10%
pulse*52.5 ms12.5%

20 ms 对应 50 Hz。舵机读取的是每帧中控制脉冲的宽度,而不是把占空比当成普通“模拟电压”。课程用五档脉宽让舵机逐档转动,每档保持 1 秒,随后从第一档重新循环。

“0.5~2.5 ms 一定对应 0~180°”不能当作通用标准。不同型号常见范围可能约为 1~2 ms,中心位置常在 1.5 ms 附近,也有舵机支持更宽范围。接线和测试前应以具体舵机的数据手册为准,从中心脉宽开始小范围试验;若舵机已到机械限位仍发出更极端脉宽,可能出现堵转、过流、发热或齿轮损伤。

硬件连接应遵循以下原则:

  • PB1 只连接舵机控制信号线;它输出的是 3.3 V 逻辑 PWM,不负责给舵机供电。
  • 舵机电源接符合该型号额定范围且电流能力足够的电源,常见小型舵机可能使用约 4.8~6 V,但必须以其规格为准。
  • 舵机地与开发板 GND 必须共地,否则 PB1 信号缺少共同电平参考。
  • 线色并非绝对标准,应按舵机说明确认信号、正电源和地线,不能只凭“红、灰”等颜色猜测。
  • 不宜从 MCU 的 3.3 V 引脚或能力不足的板载稳压器给舵机供电;启动和堵转电流可能导致复位、压降或损坏。接线完成并确认极性后再上电,尽量避免带电插拔。

最终代码还有三个运行特征需要注意:第一,它没有检查 rt_pwm_set() 与 rt_pwm_enable() 的返回值;第二,while (1) 会一直占用执行该 MSH 命令的线程,命令行不会正常返回;第三,代码没有可达的 rt_pwm_disable(),通常只能通过复位或另行设计退出条件来停止。它适合课堂验证,不是完整的可停止舵机控制任务。

二、设备模型框架复盘

本节 PWM 实验体现了 RT-Thread I/O 设备模型的分层关系:

层级本节对象主要职责
应用层pwm_test.c按名称取得设备,设置周期/脉宽并使能通道
PWM 设备接口层rt_pwm_set()、rt_pwm_enable()将统一 API 转成 PWM 控制命令
STM32 BSP 驱动层drivers/drv_pwm.c把纳秒参数换算为 TIM3 的预分频、ARR 和 CCR,并注册 pwm3
HAL/MSP 层HAL TIM 接口及 board.c 回调打开 TIM3、GPIOB 时钟并配置 PB1 复用输出
硬件层TIM3_CH4/PB1产生并输出实际 PWM 波形

这里的 rt_device_find() 负责统一的设备发现,但设置 PWM 参数使用的是 PWM 类设备接口 rt_pwm_set(),并非所有外设都用同一个设置函数。统一的是设备对象、注册/查找思想和框架组织方式;ADC、PWM 等不同设备类仍各自提供符合其功能的数据与控制 API。

调试时可以沿层级逐层定位:找不到 pwm3,检查框架与 BSP 注册;能找到设备但设置失败,检查通道和参数;API 成功但引脚无波形,检查 HAL 模块、MSP、复用映射和硬件接线。这比只用“编译是否通过”判断配置是否正确更可靠。

参考资料

Prev: RTT_ADC Next: RTT_IIC