5.12 PWM设备
5.12 PWM设备
📚 本节导读
学习时长: 约 25 分钟
难度级别: ⭐⭐⭐☆☆
前置知识: C 语言基础、MCU 定时器/PWM 基础知识、5.1 设备驱动框架
🎯 学习目标
- 理解 OneOS PWM 设备驱动框架的设计(框架层
pwm.c/ HAL 层drv_pwm.c) - 掌握
os_pwm_device_t结构体(parent继承机制、period缓存字段)与os_pwm_ops操作接口 - 掌握 PWM 周期和占空比的配置方法(
os_pwm_set_period/set_pulse) - 掌握 PWM 通道的使能与禁用
- 掌握占空比、PWM 频率、舵机脉宽的数值换算
- 能够实现 LED 呼吸灯、舵机控制等 PWM 应用
- 掌握更换 MCU 时 PWM 的移植/适配流程(纳秒→预分频/重载值换算)
一、概述
PWM(Pulse Width Modulation)是利用定时器产生可调占空比方波的技术,广泛应用于 LED 调光、电机调速、舵机控制等场景。OneOS 将 PWM 抽象为设备,提供统一的周期/占空比配置接口。PWM 设备驱动位于 drivers/misc/ 目录下,核心头文件为 pwm.h,实现文件为 pwm.c。
PWM 设备框架的特点:
- 多通道支持:每个 PWM 设备可管理多个定时器通道
- 独立配置:每个通道可独立设置周期和占空比
- 控制命令:通过
os_device_control()发送配置命令 - 简洁 API:提供
os_pwm_enable/disable/set_period/set_pulse专用接口
1.1 一句话核心
PWM 设备适用于输出可调频率、可调占空比的方波的场景(LED 调光、电机调速、舵机控制、蜂鸣器)。它让你"指定周期和脉宽,输出一个占空比可控的方波"。
1.2 典型应用场景
场景一:LED 调光 / 呼吸灯——通过连续改变占空比控制 LED 亮度:
os_pwm_set_period(pwm_dev, 1, 1000); /* 周期 1000(定时器 tick) */
os_pwm_set_pulse(pwm_dev, 1, 500); /* 脉宽 500 → 50% 占空比 */
os_pwm_enable(pwm_dev, 1); /* 使能通道输出 */场景二:舵机控制——舵机角度由脉宽决定(通常 0.5~2.5ms 对应 0~180°),周期固定 20ms(50Hz):
os_pwm_set_period(pwm_dev, 2, 20000); /* 20ms 周期 */
os_pwm_set_pulse(pwm_dev, 2, 1500); /* 1.5ms 脉宽 → 中位 90° */
os_pwm_enable(pwm_dev, 2);为什么用 PWM:PWM 是硬件定时器产生方波,不占 CPU——设好周期/脉宽后硬件自动翻转引脚,CPU 可以去做别的事。比软件延时翻转(CPU 忙等)高效得多。
数值演算 —— 占空比与频率换算(假设定时器时钟频率 1 MHz,period/pulse 单位为定时器 tick):
| 计算项 | 公式 | 代入 | 结果 |
|---|---|---|---|
| 实际周期 | period / 时钟频率 | 1000 / 1MHz | 1 ms |
| PWM 频率 | 时钟频率 / period | 1MHz / 1000 | 1 kHz |
| 占空比 | pulse / period × 100% | 500 / 1000 | 50% |
| 舵机脉宽 | pulse / 时钟频率 | 1500 / 1MHz | 1.5 ms |
注意:若时钟频率是
1 MHz,则period=1000→ 周期1ms、频率1kHz;若时钟频率是100 MHz,则period=1000→ 周期10µs、频率100kHz。同样的period值在不同时钟频率下对应不同时间,所以"周期是多少秒"必须结合 HAL 层的定时器时钟频率才知道。
数值演算 —— 舵机角度↔脉宽换算(典型舵机:0.5ms~2.5ms 对应 0°~180°):
| 目标角度 | 脉宽 | 计算 |
|---|---|---|
| 0° | 0.5 ms | 0.5 + 角度 × (2.0/180) |
| 90° | 1.5 ms | 0.5 + 90 × (2.0/180) = 1.5 ms |
| 180° | 2.5 ms | 0.5 + 180 × (2.0/180) = 2.5 ms |
公式:
脉宽(ms) = 0.5 + 角度 × 2.0 / 180。不同舵机规格区间可能不同(如 0.5~2.5ms),配置前需查实际舵机数据手册。
1.3 选型边界表(何时不该用它)
| 场景 | 该用什么 | 为什么 |
|---|---|---|
| 需要精确测脉宽/输入捕获 | 5.15 硬件定时器输入捕获 | PWM 是输出,捕获是输入,方向相反 |
| 需要 DAC 输出模拟电压 | DAC 设备 | PWM 是数字方波,不能当模拟电压用(除非加 RC 滤波) |
| 需要 PWM 检测输出频率 | 定时器计数 | PWM 只负责产生,不检测自身输出 |
| 需要纳秒级精确单次脉冲 | 硬件定时器单次模式 | PWM 是连续方波,不适合单次精确脉冲 |
二、核心数据结构
2.1 PWM 配置结构体 os_pwm_configuration
来源:drivers/misc/pwm.h
struct os_pwm_configuration
{
uint32_t channel;
uint32_t period;
uint32_t pulse;
};字段详解:
| 字段 | 类型 | 含义 |
|---|---|---|
channel | uint32_t | PWM 通道号(0~N),逻辑通道号,HAL 层映射到芯片定时器通道 |
period | uint32_t | PWM 周期(单位:定时器时钟周期,即计数值),实际时间 = period / 定时器时钟频率 |
pulse | uint32_t | PWM 脉宽(单位:定时器时钟周期),占空比 = pulse / period,须满足 pulse <= period |
说明:这个结构体是
os_device_control控制命令(OS_PWM_CMD_*)的args载体,把"通道/周期/脉宽"三个参数打包成一个结构体传给 HAL(见 4.4 的_pwm_control)。注意源码中三个字段没有注释,上表为文档补充说明,不属于源码。
2.2 PWM 设备结构体 os_pwm_device_t
来源:drivers/misc/pwm.h
struct os_pwm_device
{
struct os_device parent;
const struct os_pwm_ops *ops;
uint32_t period;
uint32_t max_value;
};
typedef struct os_pwm_device os_pwm_device_t;字段详解:
| 字段 | 类型 | 含义 |
|---|---|---|
parent | struct os_device(内嵌在开头) | 标准设备基类,让这个结构体"是一个设备",可被 os_device_register 注册、被 os_device_open 打开 |
ops | const struct os_pwm_ops * | PWM 操作函数表,指向 HAL 层实现 |
period | uint32_t | 当前周期(缓存),os_pwm_set_period/_pwm_control 设周期时同步更新,供 set_pulse 校验 pulse <= period 等使用 |
max_value | uint32_t | 最大计数值(HAL 层使用,如 16 位定时器为 0xFFFF),用于周期/脉宽的 tick 换算 |
关键理解 —— 为什么 parent(os_device)内嵌在最开头:与 5.8 RTC、5.9 Watchdog、5.11 ADC 同理,parent 是第一个成员,所以"设备指针 os_device_t *dev"和"PWM 指针 os_pwm_device_t *pwm"数值相等。_pwm_control 里 (struct os_pwm_device *)dev、_pwm_set 里 (struct os_pwm_device *)dev 都是直接强转 dev,因为 dev 指向的就是 parent,数值相同。这是 OneOS 用"C 语言继承"实现设备多态的标准手法。
关键理解 —— period 缓存字段的配合机制:period 不只是配置用,还被 _pwm_control(OS_PWM_CMD_SET_PERIOD 时 pwm->period = config->period)和 os_pwm_set_period(pwm->period = period)两条路径同步维护。而 HAL 层 set_pulse(如 ST 的 stm32_pwm_set_pulse)会用 buffer > dev->period 校验脉宽不超周期。所以应用用 os_device_control 和 os_pwm_set_period 混合配置时,period 缓存保持一致。
2.3 PWM 操作函数表 struct os_pwm_ops
来源:drivers/misc/pwm.h
struct os_pwm_ops
{
os_err_t (*enabled)(struct os_pwm_device *dev, uint32_t channel, os_bool_t enable);
os_err_t (*set_period)(struct os_pwm_device *dev, uint32_t channel, uint32_t period);
os_err_t (*set_pulse)(struct os_pwm_device *dev, uint32_t channel, uint32_t pulse);
os_err_t (*control)(struct os_pwm_device *dev, int cmd, void *args);
};| 函数指针 | 说明 | 典型实现(见六、移植章节) |
|---|---|---|
enabled | 使能/禁用指定通道的 PWM 输出 | stm32_pwm_enabled:HAL_TIM_PWM_Start/Stop |
set_period | 设置指定通道的 PWM 周期 | stm32_pwm_set_period:纳秒→tick→算预分频/重载值 |
set_pulse | 设置指定通道的 PWM 脉宽(占空比) | stm32_pwm_set_pulse:校验后 __HAL_TIM_SET_COMPARE |
control | 扩展控制命令(HAL 层自定义) | 可留 OS_NULL(ST 就是 NULL) |
说明:
ops四个函数指针中,control是可选的(ST 的stm32_pwm_ops.control = OS_NULL),enabled/set_period/set_pulse三个必须有。源码本身无注释,上表为文档补充说明,不属于源码。
三、API 详解
3.1 使能与禁用
os_err_t os_pwm_enable(struct os_pwm_device *pwm, uint32_t channel);
os_err_t os_pwm_disable(struct os_pwm_device *pwm, uint32_t channel);3.2 设置周期
os_err_t os_pwm_set_period(struct os_pwm_device *pwm, uint32_t channel, uint32_t period);| 参数 | 说明 |
|---|---|
pwm | PWM 设备指针 |
channel | PWM 通道号 |
period | 周期值(单位:定时器时钟周期) |
注意:
period的单位取决于 HAL 层定时器的时钟频率。例如时钟为 1MHz,period=1000表示周期为 1ms(频率 1kHz)。
3.3 设置脉宽(占空比)
os_err_t os_pwm_set_pulse(struct os_pwm_device *pwm, uint32_t channel, uint32_t pulse);| 参数 | 说明 |
|---|---|
pulse | 脉宽值(单位:定时器时钟周期),pulse <= period |
占空比 =
pulse / period× 100%。例如period=1000, pulse=500表示 50% 占空比。
3.4 控制命令
#define OS_PWM_CMD_ENABLE IOC_PWM(0x00) /* 使能 PWM */
#define OS_PWM_CMD_DISABLE IOC_PWM(0x01) /* 禁用 PWM */
#define OS_PWM_CMD_SET_PERIOD IOC_PWM(0x02) /* 设置周期 */
#define OS_PWM_CMD_SET_PULSE IOC_PWM(0x03) /* 设置脉宽 */通过 os_device_control() 发送,需传入 os_pwm_configuration 结构体。
3.5 注册 PWM 设备
os_err_t os_device_pwm_register(struct os_pwm_device *device, const char *name);四、核心源码详解
说明:以下源码均取自
drivers/misc/pwm.c,与项目源码逐字一致(含原始英文注释)。
4.1 os_pwm_enable / os_pwm_disable —— 使能/禁用通道
一句话定位:把使能/禁用操作透传给 HAL 层 ops->enabled,用 enable 参数区分。
完整源码(drivers/misc/pwm.c,与项目一致):
os_err_t os_pwm_enable(struct os_pwm_device *pwm, uint32_t channel)
{
os_err_t result = OS_SUCCESS;
OS_ASSERT(pwm != OS_NULL);
result = pwm->ops->enabled(pwm, channel, OS_TRUE);
return result;
}
os_err_t os_pwm_disable(struct os_pwm_device *pwm, uint32_t channel)
{
os_err_t result = OS_SUCCESS;
OS_ASSERT(pwm != OS_NULL);
result = pwm->ops->enabled(pwm, channel, OS_FALSE);
return result;
}分步详解:
第 1 步:透传使能/禁用(无条件执行)
result = pwm->ops->enabled(pwm, channel, OS_TRUE); /* enable */
result = pwm->ops->enabled(pwm, channel, OS_FALSE); /* disable */详细解释:两个 API 都走同一个 HAL 函数 ops->enabled(pwm, channel, enable),用第三个参数 OS_TRUE/OS_FALSE 区分使能与禁用。HAL 层据此打开/关闭对应通道的 PWM 输出。
4.2 os_pwm_set_period / os_pwm_set_pulse —— 设置周期/脉宽
完整源码(drivers/misc/pwm.c,与项目一致):
os_err_t os_pwm_set_period(struct os_pwm_device *pwm, uint32_t channel, uint32_t period)
{
pwm->period = period;
return pwm->ops->set_period(pwm, channel, period);
}
os_err_t os_pwm_set_pulse(struct os_pwm_device *pwm, uint32_t channel, uint32_t pulse)
{
return pwm->ops->set_pulse(pwm, channel, pulse);
}分步详解:
第 1 步:设置周期并缓存(set_period)
pwm->period = period;
return pwm->ops->set_period(pwm, channel, period);详细解释:关键点:os_pwm_set_period 先把 period 缓存到 pwm->period(供其他逻辑读取当前周期),再调 HAL 的 ops->set_period 真正配置定时器周期。
第 2 步:透传脉宽(set_pulse)
return pwm->ops->set_pulse(pwm, channel, pulse);详细解释:os_pwm_set_pulse 直接把 pulse 透传给 HAL 层 ops->set_pulse 设置脉宽。占空比 = pulse / period,由 HAL 层根据两者配置定时器比较值。
4.3 标准设备写 _pwm_set(按脉宽写)
一句话定位:设备框架的 write 操作,把一次 os_device_write 转成"设脉宽"。
完整源码(drivers/misc/pwm.c,与项目一致):
os_ssize_t _pwm_set(struct os_device *dev, os_off_t pos, const void *buffer, os_size_t size)
{
return os_pwm_set_pulse((struct os_pwm_device *)dev, pos, *(int32_t *)buffer);
}分步详解:
第 1 步:把 pos 当通道、buffer 当脉宽(无条件执行)
return os_pwm_set_pulse((struct os_pwm_device *)dev, pos, *(int32_t *)buffer);详细解释:关键理解:os_device_write(dev, channel, &pulse, 4) 到达这里后,pos 是通道号,buffer 里存的 int32_t 是脉宽。直接调 os_pwm_set_pulse(dev, pos, *(int32_t *)buffer) 设置该通道脉宽。这是用标准 write 接口设脉宽的快捷方式。
4.4 标准设备控制 _pwm_control
一句话定位:设备框架的 control 操作,把 OS_PWM_CMD_* 命令转成 HAL 调用。
完整源码(drivers/misc/pwm.c,与项目一致):
static os_err_t _pwm_control(struct os_device *dev, int cmd, void *args)
{
struct os_pwm_device *pwm = (struct os_pwm_device *)dev;
struct os_pwm_configuration *config = (struct os_pwm_configuration *)args;
switch (cmd)
{
case OS_PWM_CMD_ENABLE:
return pwm->ops->enabled(pwm, config->channel, OS_TRUE);
case OS_PWM_CMD_DISABLE:
return pwm->ops->enabled(pwm, config->channel, OS_FALSE);
case OS_PWM_CMD_SET_PERIOD:
pwm->period = config->period;
return pwm->ops->set_period(pwm, config->channel, config->period);
case OS_PWM_CMD_SET_PULSE:
return pwm->ops->set_pulse(pwm, config->channel, config->pulse);
default:
return OS_NOSYS;
}
}分步详解:
第 1 步:按命令分发(条件 cmd 值)
switch (cmd)
{
case OS_PWM_CMD_ENABLE:
return pwm->ops->enabled(pwm, config->channel, OS_TRUE);
case OS_PWM_CMD_DISABLE:
return pwm->ops->enabled(pwm, config->channel, OS_FALSE);
case OS_PWM_CMD_SET_PERIOD:
pwm->period = config->period;
return pwm->ops->set_period(pwm, config->channel, config->period);
case OS_PWM_CMD_SET_PULSE:
return pwm->ops->set_pulse(pwm, config->channel, config->pulse);
default:
return OS_NOSYS;
}
}详细解释:关键理解:os_device_control(dev, cmd, &config) 中 args 是 os_pwm_configuration(含 channel/period/pulse)。按命令分别调 HAL 的 enabled/set_period/set_pulse,SET_PERIOD 时同步缓存 pwm->period。未知命令返回 OS_NOSYS。
_pwm_control 与 os_pwm_* 的关系:两者最终都调 HAL 的 ops->enabled/ops->set_period/ops->set_pulse,殊途同归。
_pwm_control() 条件走向总结:
_pwm_control(dev, cmd, args) 条件走向总结:
├── pwm = (struct os_pwm_device *)dev
├── config = (struct os_pwm_configuration *)args
├── cmd == OS_PWM_CMD_ENABLE
│ └── return ops->enabled(pwm, config->channel, OS_TRUE)
├── cmd == OS_PWM_CMD_DISABLE
│ └── return ops->enabled(pwm, config->channel, OS_FALSE)
├── cmd == OS_PWM_CMD_SET_PERIOD
│ ├── pwm->period = config->period(同步缓存)
│ └── return ops->set_period(pwm, config->channel, config->period)
├── cmd == OS_PWM_CMD_SET_PULSE
│ └── return ops->set_pulse(pwm, config->channel, config->pulse)
└── 其他 cmd(未知命令)→ return OS_NOSYS4.5 注册 os_device_pwm_register
完整源码(drivers/misc/pwm.c,与项目一致):
os_err_t os_device_pwm_register(struct os_pwm_device *device, const char *name)
{
os_err_t result = OS_SUCCESS;
device->parent.ops = &pwm_device_ops;
device->parent.type = OS_DEVICE_TYPE_MISCELLANEOUS;
result = os_device_register(&device->parent, name);
return result;
}分步详解:
第 1 步:设置操作表与类型并注册(无条件执行)
device->parent.ops = &pwm_device_ops;
device->parent.type = OS_DEVICE_TYPE_MISCELLANEOUS;
result = os_device_register(&device->parent, name);详细解释:操作表 pwm_device_ops 含 write = _pwm_set 和 control = _pwm_control。类型 OS_DEVICE_TYPE_MISCELLANEOUS(杂项),用 name 注册。注册后应用用 os_device_open(name) 打开,可用 write/control 或 os_pwm_* 访问。
五、代码示例
5.1 基本使用:LED 调光
#include <oneos_config.h>
#include <device.h>
#include <misc/pwm.h>
#include <os_util.h>
void pwm_basic_example(void)
{
os_pwm_device_t *pwm_dev;
os_err_t ret;
/* 查找 PWM 设备 */
pwm_dev = (os_pwm_device_t *)os_device_open_s("pwm1");
if (pwm_dev == OS_NULL)
{
os_kprintf("PWM device not found!\r\n");
return;
}
/* 配置 PWM 通道 1:周期 1000,占空比 50%(每步检查返回值) */
ret = os_pwm_set_period(pwm_dev, 1, 1000); /* 周期 = 1000 */
if (ret != OS_SUCCESS)
{
os_kprintf("set period failed, ret=%d\r\n", ret);
return;
}
ret = os_pwm_set_pulse(pwm_dev, 1, 500); /* 脉宽 = 500(50% 占空比) */
if (ret != OS_SUCCESS)
{
os_kprintf("set pulse failed, ret=%d\r\n", ret);
return;
}
ret = os_pwm_enable(pwm_dev, 1); /* 使能 PWM 输出 */
if (ret != OS_SUCCESS)
{
os_kprintf("enable pwm failed, ret=%d\r\n", ret);
return;
}
os_kprintf("PWM CH1: period=1000, pulse=500, duty=50%%\r\n");
}5.2 LED 呼吸灯效果
void pwm_breathing_led(void)
{
os_pwm_device_t *pwm_dev;
uint32_t period = 1000;
int32_t pulse;
int32_t step = 10;
pwm_dev = (os_pwm_device_t *)os_device_open_s("pwm1");
if (pwm_dev == OS_NULL)
return;
os_pwm_set_period(pwm_dev, 1, period);
os_pwm_enable(pwm_dev, 1);
pulse = 0;
while (1)
{
/* 渐亮:占空比从 0% 到 100% */
for (pulse = 0; pulse <= period; pulse += step)
{
os_pwm_set_pulse(pwm_dev, 1, pulse);
os_task_msleep(10);
}
/* 渐暗:占空比从 100% 到 0% */
for (pulse = period; pulse >= 0; pulse -= step)
{
os_pwm_set_pulse(pwm_dev, 1, pulse);
os_task_msleep(10);
}
}
}5.3 使用控制命令接口
void pwm_control_example(void)
{
os_device_t *dev;
struct os_pwm_configuration config;
dev = os_device_open_s("pwm1");
if (dev == OS_NULL)
return;
/* 配置 PWM 通道 2 */
config.channel = 2;
config.period = 20000; /* 20ms 周期(50Hz,舵机常用频率) */
config.pulse = 1500; /* 1.5ms 脉宽(舵机中位) */
os_device_control(dev, OS_PWM_CMD_SET_PERIOD, &config);
os_device_control(dev, OS_PWM_CMD_SET_PULSE, &config);
os_device_control(dev, OS_PWM_CMD_ENABLE, &config);
os_kprintf("PWM CH2: period=20000, pulse=1500\r\n");
}5.4 多通道独立控制
void pwm_multi_channel(void)
{
os_pwm_device_t *pwm_dev;
pwm_dev = (os_pwm_device_t *)os_device_open_s("pwm1");
if (pwm_dev == OS_NULL)
return;
/* 通道 1:LED 调光,1kHz,30% 占空比 */
os_pwm_set_period(pwm_dev, 1, 1000);
os_pwm_set_pulse(pwm_dev, 1, 300);
os_pwm_enable(pwm_dev, 1);
/* 通道 2:舵机控制,50Hz,1.5ms 中位 */
os_pwm_set_period(pwm_dev, 2, 20000);
os_pwm_set_pulse(pwm_dev, 2, 1500);
os_pwm_enable(pwm_dev, 2);
/* 通道 3:蜂鸣器,4kHz,50% 占空比 */
os_pwm_set_period(pwm_dev, 3, 250);
os_pwm_set_pulse(pwm_dev, 3, 125);
os_pwm_enable(pwm_dev, 3);
os_kprintf("PWM 3 channels configured\r\n");
}六、PWM 的移植/适配(更换 MCU)
6.1 先认识 HAL 层:谁在真正操作 PWM 硬件
回顾 1.1 节,框架把差异封装在 HAL 层。HAL 层就是 drivers/hal/<厂商>/drivers/drv_pwm.c,它为这个 MCU 实现 os_pwm_ops 的四个函数,并注册为 "pwmX" 设备。
关键认知:drivers/misc/pwm.c 是跨平台统一的框架层,只负责透传命令、缓存周期、注册,不碰任何硬件寄存器。换 MCU 只需换 drv_pwm.c。项目 drivers/hal/ 下每个厂商都有 drv_pwm.c,并列平级,一个工程只编译当前所选 MCU 的那一份:
| 厂商 | 文件 |
|---|---|
| ST | drivers/hal/st/drivers/drv_pwm.c |
| GD | drivers/hal/gd/drivers/{f10x,f20x,f4xx,f403,c10x}/drv_pwm.c |
| nRF | drivers/hal/nrf/drivers/nrf52/drv_pwm.c |
| 全志/博通/高云/FM | drivers/hal/{apollo,beken,levetop,fm}/drivers/drv_pwm.c |
6.2 移植的全景图:只有 HAL 层与 MCU 相关
┌──────────────────────────────────────────────────────────┐
│ 应用层(Application) │
│ os_pwm_set_period/pulse/enable/disable / os_device_* │
└──────────────────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ PWM 框架层(drivers/misc/pwm.c) │
│ os_pwm_set_period/pulse/enable/disable/_pwm_control │
│ (换 MCU 不用改) │
└──────────────────────────────┬───────────────────────────┘
│ os_pwm_ops(enabled/set_period/set_pulse/control)
▼
┌──────────────────────────────────────────────────────────┐
│ HAL 层(drivers/hal/<厂商>/drivers/drv_pwm.c) ★需适配★ │
│ 实现 os_pwm_ops + 通道映射 + 纳秒→tick→预分频/重载换算 │
└──────────────────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 硬件(MCU 的定时器 PWM 通道) │
└──────────────────────────────────────────────────────────┘移植的关键洞察:换 MCU,应用层与框架层一行都不用改,只需在 HAL 层提供 os_pwm_ops 并注册为 "pwmX" 设备。
6.3 移植需要提供的"接口契约"
HAL 层 PWM 驱动要能用,必须实现 os_pwm_ops(enabled/set_period/set_pulse 必填,control 可 NULL),并做通道映射与 tick 换算。
源码参考——ST 的 struct stm32_pwm 与 ops(drivers/hal/st/drivers/drv_pwm.c,与项目逐字一致):
struct stm32_pwm
{
struct os_pwm_device pwm;
struct stm32_timer *tim;
uint64_t freq;
uint64_t tim_mult;
uint64_t tim_shift;
};static const struct os_pwm_ops stm32_pwm_ops = {
.enabled = stm32_pwm_enabled,
.set_period = stm32_pwm_set_period,
.set_pulse = stm32_pwm_set_pulse,
.control = OS_NULL,
};接口契约要点:
ops 函数 | 职责 | 关键点 |
|---|---|---|
enabled | 使能/禁用通道输出 | HAL_TIM_PWM_Start/Stop;先做通道映射校验 |
set_period | 设置周期 | 把 nsec(纳秒周期)换算成定时器 tick,再算预分频 prescaler 和重载值 Period,写入 Init.Prescaler/Init.Period |
set_pulse | 设置脉宽 | 校验 pulse <= dev->period;把脉宽 tick 换算后 __HAL_TIM_SET_COMPARE 写比较寄存器 |
control | 扩展命令 | 可留 OS_NULL(ST 未用) |
关键理解 —— 周期/脉宽的换算机制(为什么 period 是"纳秒"):
ST 的 stm32_pwm_set_period 接收的 nsec 参数实际上是纳秒(应用传的 period 值 × 定时器时钟 tick 对应纳秒,通过 calc_mult_shift 得到 tim_mult/tim_shift 换算)。换算流程:
tim_tick = nsec × tim_mult >> tim_shift (纳秒 → 定时器计数 tick)
prescaler = tim_tick / max_value (定时器计数 / 最大重载,得到预分频)
Period = tim_tick / (prescaler + 1) - 1 (重载值,即 ARR)其中 tim_mult/tim_shift 在注册时用 calc_mult_shift(NSEC_PER_SEC, tim->freq, max_value/freq) 算出(与 5.11 ADC 的电压换算同一套机制)。
数值演算 —— 纳秒周期 → 定时器预分频/重载值(假设定时器时钟 freq = 72 MHz,定时器 16 位 max_value = 0xFFFF,tim_mult/tim_shift 已按 72MHz 算出),要输出周期 nsec = 20000(20µs,即 50kHz):
| 计算项 | 公式 | 代入 | 结果 |
|---|---|---|---|
tim_tick | nsec × tim_mult >> tim_shift(把 20µs 换成 72MHz 计数) | 20000 × mult >> shift | 1440(tick) |
prescaler | tim_tick / max_value | 1440 / 65535 | 0 |
Period | tim_tick / (prescaler+1) - 1 | 1440 / 1 - 1 | 1439 |
| 实际频率 | freq / ((prescaler+1) × (Period+1)) | 72MHz / (1 × 1440) | 50 kHz ✓ |
为什么这么算:16 位定时器重载值最大 65535,若周期太大(如
tim_tick = 720000)就超了,必须用预分频prescaler先分频、再用重载值装剩余部分。prescaler = tim_tick / max_value估算需要的分频数,Period = tim_tick/(prescaler+1) - 1精确到重载值(-1是因为 ARR 从 0 计数)。算错预分频/重载值,实际 PWM 频率就与设定不符——这是移植最易错的地方。
6.4 移植的标准流程(5 步)
第 1 步:确认硬件能力——新 MCU 定时器个数、位宽(16/32 位)、每个定时器的 PWM 通道数、时钟频率。
第 2 步:定义设备控制块——内嵌 os_pwm_device + 定时器句柄 + 换算系数:
struct my_pwm
{
struct os_pwm_device pwm; /* 内嵌 PWM 设备(首成员) */
struct my_timer *tim; /* MCU 定时器句柄 */
uint64_t freq; /* 定时器时钟频率 */
uint64_t tim_mult; /* 纳秒→tick 换算系数(注册时算) */
uint64_t tim_shift;
};第 3 步:实现通道映射——把逻辑通道号(1~N)映射到芯片定时器通道宏(如 TIM_CHANNEL_x),非法通道返回 OS_NOSYS:
static os_err_t get_pwm_channel(uint16_t channel, uint32_t *mcu_channel)
{
switch (channel)
{
case 1: *mcu_channel = TIM_CHANNEL_1; break;
case 2: *mcu_channel = TIM_CHANNEL_2; break;
/* ... 各芯片 TIM_CHANNEL_3/4/5/6 ... */
default: return OS_NOSYS;
}
return OS_SUCCESS;
}第 4 步:实现 os_pwm_ops 三函数——enabled(Start/Stop)、set_period(纳秒→预分频/重载)、set_pulse(校验+写比较寄存器)。
第 5 步:注册——os_device_pwm_register(&st_pwm->pwm, pwm_name),名字自动带 pwm_ 前缀:
os_err_t my_pwm_register(const char *name, struct my_timer *tim)
{
struct my_pwm *st_pwm = os_calloc(1, sizeof(struct my_pwm));
OS_ASSERT(st_pwm);
st_pwm->tim = tim;
st_pwm->freq = tim->freq;
st_pwm->pwm.max_value = 0xFFFF; /* 16 位定时器 */
calc_mult_shift(&st_pwm->tim_mult, &st_pwm->tim_shift,
NSEC_PER_SEC, tim->freq, st_pwm->pwm.max_value / tim->freq);
st_pwm->pwm.ops = &my_pwm_ops;
return os_device_pwm_register(&st_pwm->pwm, pwm_name);
}6.5 移植检查清单(快速自查)
| 检查项 | 说明 |
|---|---|
| 通道映射完整? | 逻辑通道 1~N 都映射到芯片 TIM_CHANNEL_x,非法通道返回 OS_NOSYS |
set_period 的预分频/重载算对? | prescaler = tim_tick/max_value、Period = tim_tick/(prescaler+1)-1,算错频率不对 |
set_pulse 校验 pulse <= period? | 不校验则脉宽超周期行为未定义(ST 返回 OS_FAILURE) |
calc_mult_shift 系数正确? | 纳秒→tick 换算依赖 tim_mult/tim_shift,用 NSEC_PER_SEC 与定时器频率算 |
名字带 pwm_ 前缀? | 应用用 os_device_open("pwm1") 打开,注册名需匹配 |
七、注意事项
- 周期和脉宽单位:
period和pulse的单位是定时器时钟周期,实际时间取决于 HAL 层时钟频率 - 脉宽不大于周期:
pulse <= period,否则 PWM 行为未定义 - 先配置后使能:建议先设置周期和脉宽,最后使能输出,避免产生意外的波形
- 通道独立性:同一 PWM 设备的不同通道共享定时器时钟,但周期和脉宽可独立设置
- 占空比分辨率:
period越大,占空比可调分辨率越高,但 PWM 频率越低
📝 本节小结
核心要点回顾
本节围绕 PWM 设备展开,核心要点如下:
- 统一抽象:PWM 设备通过
os_pwm_device_t统一抽象,HAL 层实现os_pwm_ops接口。 - 核心操作:设置周期(
os_pwm_set_period)和脉宽(os_pwm_set_pulse),占空比 = 脉宽 / 周期。 - 多通道支持:支持多通道独立配置,每个通道可设置不同的占空比。
- 典型应用:LED 呼吸灯、舵机控制、电机调速、蜂鸣器驱动。
- 关键结构体:
os_pwm_device_t内嵌parent(os_device,首成员继承)+ops+period(缓存)+max_value;os_pwm_configuration是控制命令的 args 载体。 - 更换 MCU 的移植要点:框架层
pwm.c与 HAL 层drv_pwm.c解耦,换 MCU 只换drv_pwm.c;HAL 需实现os_pwm_ops(enabled/set_period/set_pulse)+ 通道映射 + 纳秒→预分频/重载换算。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
| 脉宽超过周期 | 占空比不能超过 100%,set_pulse 的脉宽应小于等于周期 |
| 忘记使能通道 | 部分芯片配置 PWM 后还需 os_pwm_enable 使能对应通道才输出波形 |
| 周期单位混淆 | 周期/脉宽的时间单位(ns/us)由驱动约定,配置前需确认芯片实现 |
| 舵机角度与脉宽换算错误 | 舵机通常 0.5~2.5ms 脉宽对应 0~180°,需按实际舵机规格换算 |
| 周期频率换算错 | 实际时间 = period / 定时器时钟频率,同样 period 值在不同时钟频率下对应不同时间 |
| 换 MCU 改框架层 | 只改 HAL 层 drv_pwm.c,pwm.c 框架与应用层不动 |
| 预分频/重载值算错 | prescaler = tim_tick/max_value、Period = tim_tick/(prescaler+1)-1,算错 PWM 频率不对 |
set_pulse 未校验脉宽超周期 | HAL 应校验 pulse <= dev->period,超周期返回错误,避免波形异常 |
下一步
掌握了 PWM 设备后,接下来学习 DMA 设备:
- 5.13 DMA设备:了解 OneOS Soft DMA 抽象层、环形缓冲区与半满/全满/超时中断回调。