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/TIM532 位定时器。每个定时器支持 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 测试的时候需要注意到先设置 频率和占空比 后才能使能。

本文作者:烟儿公主

本文链接:https://www.cnblogs.com/toutiegongzhu/p/17697360.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   烟儿公主  阅读(681)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起
  1. 1 夏日大冒险 暴躁的兔子
夏日大冒险 - 暴躁的兔子
00:00 / 00:00
An audio error has occurred.

作词 : 暴躁的兔子

作曲 : 暴躁的兔子

编曲 : IOF

混音:Gfanfan

出品:网易飓风

夏天 不要再浪费时间

实现 你承诺过的改变

别再 找一堆借口拖延

现在就和我一起飞向海边

人生苦短 你应该学会如何作乐

低着头还怎么应对挫折

人应该为自己活着

不用去迎合

要去寻欢作乐

撮合我的浪漫和悲欢

把这荒诞人生都塞满

生活难免磕磕绊绊

对抗生活的平庸就是浪漫

学会取悦自己逆风翻盘

去反抗变态的三观

把条条框框都砸烂

建立新的规则推翻谈判

无可救药的人呐

和我一起去海边

看那日出和晚霞 海天一线

看阳光穿越地平线

现实交织的明天

就在这个夏天

为自己改变

别怕山高路远

去冒险

我真的不care你是否会喜欢我

不跟风被定义的美 全都是灾祸

我才不讨好大多数绝不与示弱

过好你的生活

你管我应该怎么快活

没有人能有资格审判

别人的生活和牵绊

快闭上你的高谈阔论

乘风破浪吧 理想的风帆

我就是肆意张扬又如何

我就是锋芒毕露又如何

我就是离经叛道又如何

我就是要出格 你管我要如何

我就是与众不同又如何

我就是特立独行又如何

我就是不知好歹又如何

你管我怎样出格 你管我如何

无可救药的人呐

和我一起去海边

看那日出和晚霞 海天一线

看阳光穿越地平线

现实交织的明天

就在这个夏天

为自己改变

别怕山高路远

不知进退的人呐

和我一起去海边

聊聊曾经的理想 一起想当年

那曾想改变世界的人

是否还满腔热忱

不羁的我们放肆着

反抗那命运的指针

解放灵魂

推广:网易飓风

企划:贾焱祺

监制:徐思灵

出品人:谢奇笛