24 Linux PWM 驱动

发布时间 2023-09-14 14:35:15作者: 烟儿公主

一、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 测试的时候需要注意到先设置 频率和占空比 后才能使能。