24 Linux PWM 驱动
一、PWM 驱动简介
其实在 stm32 中我们就学过了 PWM,这里就是再复习一下。PWM(Pulse Width Modulation),称为脉宽调制,PWM 信号图如下:
PWM 最关键的两个参数:频率和占空比。
频率是指单位时间内脉冲信号的周期数。比如开关灯,开关一次算一次周期,在 1s 进行多少次开关(开关一次为一个周期)。
占空比是指一个周期内高电平时间和低电平时间的比例。也拿开关当作例子,总共 100s,开了 50s 灯(高电平),关了 50s 灯(低电平),这时候的占空比就为 50%(比例)。
1. 设备树下的 PWM 控制器节点
① 定时器
PWM 其实就是由定时器来产生,STM32MP157总共有很多定时器。
TIM1/TIM8:有两个 16 位高级定时器,主要用于电机控制。每个定时器支持 4 通道 PWM 信号。
TIM2/TIM3/TIM4/TIM5:这四个是通用定时器,TIM3/TIM4 是 16 位定时器,TIM2/TIM5是 32 位定时器。每个定时器支持 4 通道 PWM 信号。
TIM15/TIM16/TIM17: 这 3 个也都是 16 位的通用定时器, TIM15 支持 2 通道的 PWM 信号, TIM16/TIM17 每个定时器支持 1 通道的 PWM 信号。
多通道控制 PWM 好处:
1、独立控制:多通道 PWM 允许每个通道独立地配置和控制,可以针对不同的需求进行个性化设置。
2、同步控制:通过使用多通道PWM,确保各个通道的PWM信号在时间上保持一致,避免信号间的干扰或不匹配。
② TIM1 简介
① 16 位的向上、向下自动加载计数器。
② 16 位可编程的预分频器。
③ 6 个独立的通道,这些通道的功能如下:
— 输入捕获(只有通道 5 和 6 支持)。
— 输出比较
— PWM 波形生成(边缘和中间对齐模式)。
— 单脉冲模式。
④ 带有死区的可编程互补输出。
⑤ 以下事件可以生成中断或者 DMA:
— 更新事件,计数器溢出。
— 触发事件,计数器开始、停止、初始化等。
— 输入捕获。
— 输出比较
③ TIM1 设备节点
在 Documentation/devicetree/bindings/mfd/stm32-timers.txt 文件夹下可以看到 TIM 在设备树中需要注意的事情。
1、必须的参数:
compatible:必须是 "st,stm32-timers"。
reg:定时器物理寄存器地址,对于 TIM1,地址为 0x44000000,这个是在 STM32MP157 数据手册上的。(我找了半天没找到,有大佬说说在哪吗?)
clock-names:时钟源名字,设置为 "int"。
clocks:时钟源。
2、可选参数:
resets:复位句柄,用来复位定时器控制器。
dmas:DMA 通道,最多 7 通道 DMA。
dma-names:DMA 名称列表,必须和 "dmas" 属性匹配,可选的名字有:“ch1”、“ch2”、“ch3”、“ch4”、“up”、“trig”、“com”。
3、可选的子节点:
定时器有很多功能,不同的功能需要不同的子节点表示,可选三种子节点:
pwm: 描述定时器的 PWM 功能。
timer: 描述定时器的定时功能。
counter: 描述定时器的计数功能。
现在来看实际的定时器节点,打开 /home/alientek/linux/atk-mpl/linux/my_linux/linux5.4.31/arch/arm/boot/dts/stm32mp151.dtsi 文件,找到 timers1 设备节点。
timers1: timer@44000000 { // 定义一个timers1的子设备,并且物理地址为44000000
#address-cells = <1>; // 定义该节点子节点地址单元格数量
#size-cells = <0>; // 定义该节点子节点大小单元格数量
compatible = "st,stm32-timers";
reg = <0x44000000 0x400>; // 指定寄存器物理地址(物理地址0x44000000,大小0x400)
clocks = <&rcc TIM1_K>; // 指定时钟源
clock-names = "int"; // 指定时钟源名称
dmas = <&dmamux1 11 0x400 0x80000001>, // 指定定时器使用的DMA控制器和通道号
<&dmamux1 12 0x400 0x80000001>,
<&dmamux1 13 0x400 0x80000001>,
<&dmamux1 14 0x400 0x80000001>,
<&dmamux1 15 0x400 0x80000001>,
<&dmamux1 16 0x400 0x80000001>,
<&dmamux1 17 0x400 0x80000001>;
dma-names = "ch1", "ch2", "ch3", "ch4", // 指定每个DMA通道名字
"up", "trig", "com";
status = "disabled"; // 设备未启用
pwm {
compatible = "st,stm32-pwm";
#pwm-cells = <3>; // 指定PWM单元格数量为3,即占空比、频率和相位角
status = "disabled";
};
timer@0 {
compatible = "st,stm32h7-timer-trigger";
reg = <0>;
status = "disabled";
};
counter {
compatible = "st,stm32-timer-counter";
status = "disabled";
};
};
④ PWM 设备子节点
打开 Documentation/devicetree/bindings/pwm/pwm-stm32.txt 文件,可以看到 PWM 子节点属性信息:
compatible:必须是 “st,stm32-pwm”。
pinctrl-names:设置为 "default",也可以额外添加 "sleep",以在低功率时将引脚设置为睡眠状态。
pinctrl-n:PWM 引脚 pinctrl 句柄,用来指定 PWM 信号输出引脚。
#pwm-cells:设置为 3,即占空比、频率和相位角。
2. PWM 子系统
Linux 内核提供了 PWM 子系统框架,所以编写 PWM 驱动的时候需要符合这个框架。PWM子系统的核心是 pwm_chip
结构体,定义在文件 include/linux/pwm.h 中:
struct pwm_chip {
struct device *dev;
const struct pwm_ops *ops;
int base;
unsigned int npwm;
struct pwm_device * (*of_xlate)(struct pwm_chip *pc, const struct of_phandle_args *args);
unsigned int of_pwm_n_cells;
/* only used internally by the PWM framework */
struct list_head list;
struct pwm_device *pwms;
};
pwm_ops
结构体就是 PWM 外设的各种操作函数集合,编写 PWM 外设驱动的时候必须要实现。pwm_ops
在 pwm.h 头文件中:
struct pwm_ops {
int (*request)(struct pwm_chip *chip, struct pwm_device *pwm); /* 请求 PWM */
void (*free)(struct pwm_chip *chip, struct pwm_device *pwm); /* 释放 PWM */
int (*capture)(struct pwm_chip *chip, struct pwm_device *pwm, struct pwm_capture *result, unsigned long timeout); /* 捕获 PWM 信号 */
int (*apply)(struct pwm_chip *chip, struct pwm_device *pwm, const struct pwm_state *state); /* 新的 PWM 配置方法,配置 PWM 周期和占空比 */
void (*get_state)(struct pwm_chip *chip, struct pwm_device *pwm, struct pwm_state *state);
struct module *owner;
/* Only used by legacy drivers */
int (*config)(struct pwm_chip *chip, struct pwm_device *pwm, int duty_ns, int period_ns); /* 配置 PWM 周期和占空比 */
int (*set_polarity)(struct pwm_chip *chip, struct pwm_device *pwm,enum pwm_polarity polarity);/* 设置 PWM 极性 */
int (*enable)(struct pwm_chip *chip, struct pwm_device *pwm);/* 使能 PWM */
void (*disable)(struct pwm_chip *chip, struct pwm_device *pwm);/* 关闭 PWM */
};
pwm_ops
函数不用全部实现,但是配置 PWM 的函数必须全部实现,比如 apply 或 config。apply 函数是新的配置 PWM 方法,config 和 config 之后的函数都是老版本内核所使用的函数。
PWM 子系统驱动首先得初始化 pwm_chip
,之后向内核注册(pwmchip_add
)初始化好的 pwm_chip,用完后并且要注销(pwmchip_remove
) pwm_chip。
/*
* @description : 向内核注册 pwm_chip
* @param - chip : 要向内核注册的 pwm_chip
* @return : 0 成功;负数 失败
*/
int pwmchip_add(struct pwm_chip *chip);
/*************** 分割线 ***************/
/*
* @description : 向内核注销 pwm_chip
* @param - chip : 要移除的 pwm_chip
* @return : 0 成功;负数 失败
*/
int pwmchip_remove(struct pwm_chip *chip);
PWM 设置就两个方面:频率和占空比。TIM 的 PSC 寄存器是用来设置定时器分频器,当 TIM 时钟源确定以后,设置 PSC 分频值就可以得到 TIM 最终的时钟频率。TIM 的 ARR 寄存器是自动加载寄存器,将 TIM 设置为向下计数器,定时器开启之后每个时钟周期计数器减一,直到计数器减为0。这个时候将 ARR 的值加载到计数器里,计数器会重新倒计时,以此往复。所以 PSC 和 ARR 决定了 PWM 周期值。注意,一个定时器的 PWM 只能设置同一个周期,如果要想多路周期不同的 PWM 信号,那就要使用多个不同的 TIM。
一个定时器下的 4 路 PWM可以设置不同的占空比,相当于一个定时器下的 4 路 PWM 信号,周期是一样的,但是占空比可以不同。
二、PWM 驱动编写
1. 修改设备树
由于使用自带的 PWM 驱动,所以只需要修改设备树即可。这次使用 PA10 引脚,我们需要在设备树里添加 PA10 引脚信息及 TIM1 通道 3 的 PWM 信息。
打开 /home/alientek/linux/atk-mpl/linux/my_linux/linux-5.4.31/arch/arm/boot/dts/stm32mp15-pinctrl.dtsi 文件,找到 pwm1_pins_a: pwm1-0:
修改成:
由于 stm32mp151.dtsi 文件有 "timers1"节点,但这个节点默认为 disable,不能直接使用,所以需要在 stm32mp157d-atk.dts 向 timers 追加一些内容。
打开 /home/alientek/linux/atk-mpl/linux/my_linux/linux-5.4.31/arch/arm/boot/dts/stm32mp157d-atk.dts 文件,加入以下内容:
&timers1 {
status = "okay";
/delete-property/dmas;
/delete-property/dma-names; // 这里是把dma和dma-names属性删除,因为PWM不需要DMA
pwm1: pwm {
pinctrl-0 = <&pwm1_pins_a>;
pinctrl-1 = <&pwm1_sleep_pins_a>;
pinctrl-names = "default", "sleep";
#pwm-cells = <2>; // 现在只有占空比和频率
status = "okay";
};
};
最后需要检查设备树中是否有其他外设用到了 PA10 或者 gpioa 10,如果有那就要屏蔽掉。我觉得直接先拿去编译,然后开启开发板,如果有出错的,那就会出现 gpio-keys gpio-keys: failed to get gpio: -16 类似的情况,就去找。
2. 使能 PWM 驱动
默认是使能的,我们可以看看在哪使能。先进入 linux/atk-mpl/linux/my_linux/linux-5.4.31,输入命令 make menuconfig
进入图形化配置界面。进入以下路径:
-> Device Drivers
-> Pulse-Width Modulation (PWM) Support
-> <*> STMicroelectronics STM32 PWM // 选中
3. PWM 驱动测试
① 确定 TIM 的 pwmchipX 文件
这里因为要使用示波器,我暂时没有所以效果图就没有,但还是看一下流程。在开启开发板之前需要将新编译的设备树文件放在 tftproot 里面。
开启开发板,第一件事情就是确定 pwmchip 是否属于 TIM1,进入目录 /sys/class/pwm,可以看到 pwmchip0,进入这个目录。
进入 pwmchip0 目录后会打印出其路径,我们可以看到寄存器起始地址为 0x44000000,所以 pwmchip0 就是对应的 TIM1。
为什么需要这样复杂的方式来确定 TIM 对应的 pwmchip 文件?原因就是当多个 TIM 的 PWM 功能开启后,pwmchip 文件会发生相应的改变,所以用这种方式来相互对应。
② 调出 pwmchip0 的 pwm2 子目录
pwmchip0 是 TIM1 的总目录,TIM1 有 4 路 PWM,每一路都可以单独打开或者关闭,CH1~CH4 对应的编号为 0~3,所以打开 TIM1 的 CH3 输入命令如下:
echo 2 > /sys/class/pwm/pwmchip0/export
# 如果要打开TIM1_CH4的话,那就是修改 echo 2 为 echo 3
③ 设置 PWM 频率
这里是周期值,单位 ns,假设 20KHz 频率,周期 = 1 / 频率,所以周期 = 50000ns,输入以下命令:
echo 50000 > /sys/class/pwm/pwmchip0/pwm2/period
④ 设置 PWM 占空比
设置占空比不是直接设置占空比,而是需要设置一个高电平时间,那么低电平时间自然而然就出来了。比如 20KHz 频率下的 20% 占空比。高电平时间 = 周期 * 占空比,高电平时间 = 10000ns。
命令如下:
echo 10000 > /sys/class/pwm/pwmchip0/pwm2/duty_cycle
⑤ 使能 TIM1 通道3
注意,一定要先设置了频率和占空比后,才能开启定时器,否则会提示参数出错。命令如下:
echo 1 > /sys/class/pwm/pwmchip0/pwm2/enable
⑥ 极性反转
我们之前设置的 PWM 占空比为 20%,只需要修改极性就可以把占空比设置为 80%。
极性反转:
echo "inversed" > /sys/class/pwm/pwmchip0/pwm2/polarity
恢复极性:
echo "normal" > /sys/class/pwm/pwmchip0/pwm2/polarity
总结
无论是在学习 STM32 的时候还是现在学习 Linux 驱动的时候,都涉及到了 PWM,它最关键的两个参数就是频率和占空比。计算公式也同样重要。
这一章学习了设备树中的 TIM 和 PWM 节点设置,并且这一次也是用自带的定时器来驱动 PWM,最后在 PWM 测试的时候需要注意到先设置 频率和占空比 后才能使能。