4.3.11. PWM 驱动调试指南

4.3.11.1. 概述

PWM(脉宽调制, Pulse Width Modulation)是一种调制信号的方式,通过控制信号的开关时间占比来调节输出的平均电压或功率,通常用于控制电机速度、舵机、 LED 亮度等。

参数规格:

  • PWM 默认支持频率范围为 0.05Hz - 1MHz,占空比寄存器 RATIO 精度为 16bit,周期有效时间为 1us 到 20s,占空比有效时间为 10ns 到 20s。

4.3.11.2. 特点

芯片中 PWM 具有以下特点:

1. 两个独立的 PWM 通道,具有可编程的周期和采样周期。

2. 每个 PWM 通道都有专用计数器。

3. 每个通道的 PWM 可启用/禁用。

4. 每个通道的 PWM 脉冲极性可以通过软件选择。

5. 八位模式下支持 8 个采样,或 16 位模式下支持 4 个采样的 PWM 分辨率。

4.3.11.3. 功能描述

典型应用

PWM 信号通过调节脉冲的占空比( duty cycle)来控制舵机的位置。占空比指脉冲周期内有效电平 ( 通常高电平 ) 在周期信号里的持续时间,通常,舵机的控制信号是 50Hz(即每秒 50 个脉冲),不同的占空比 (0.5ms ~ 2.5ms) 控制舵机的转向角度。

  • 0.5ms: 舵机转动到 0 度

  • 1.0ms: 舵机转动到 45 度

  • 1.5ms: 舵机转动到 90 度

  • 2.0ms: 舵机转动到 135 度

  • 2.5ms: 舵机转动到 180 度

pwm_led

PWM 信号的基本结构:

  • 周期:从上升沿到下一个上升沿的时间 ,100HZ 表示 1 秒钟内有 100 个时钟周期。

  • 占空比( Duty Cycle):占空比是指 PWM 信号在一个周期内“高电平”持续的时间比例。占空比通常以百分比表示。例如,占空比为 50% 时,信号的一半时间为高电平,另一半时间为低电平。

  • 频率( Frequency):频率是指 PWM 信号每秒钟重复的次数,频率和周期成反比,频率 = 1 / 周期。

功能原理

下图为 PWM 子系统框架,其可大致分为三层,即用户层、核心层及硬件层。

pwm_framework

工作方式

PWM 信号的生成与控制流程可以分为几个步骤:

  • 设备初始化:驱动程序通过设备树或者平台代码初始化 PWM 硬件,并配置硬件寄存器。

  • PWM 配置:通过 drobot_pwm_apply 或用户空间接口,设置 PWM 的周期(频率)和占空比。

  • 启用 PWM :通过调用 pwm_enable() 来启动 PWM 输出。当 PWM 被启用时,硬件控制器将按指定的频率和占空比生成 PWM 波形。

  • 运行期间调节:可以通过系统接口实时调整 PWM 的占空比和频率。

  • 关闭 PWM:当不再需要 PWM 输出时,可以通过 pwm_disable() 来停止 PWM 输出。

具体流程如下:

pwm_flowchart

4.3.11.4. 驱动代码

PWM 代码说明

drivers/pwm/pwm-drobot.c

内核配置

/* arch/arm64/configs/hobot_x5_soc_defconfig */
...
CONFIG_PWM_DROBOT=y
...

DTS 节点配置

X5 PWM 控制器设备树定义位于 kernel 文件夹下的 arch/arm64/boot/dts/hobot/x5.dtsi 文件内 , 当需要使能特定 PWM 端口输出的时候,可以到对应的板级文件修改,这里以 x5-evb.dts 为例,使能 pwm3 ch0-1

备注: x5.dtsi 中的节点主要声明 SoC 共有特性,和具体电路板无关,一般情况下不用修改。

/* arch/arm64/boot/dts/hobot/x5-evb.dts */
...
&pwm3 {
	status = "okay";
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_pwm3_0 &pinctrl_pwm3_1>;
};
...

pwm 设备树解析如下:

  • &pwm3:这是另一个 PWM 控制器, PWM3 。

  • status = "okay" 启用 PWM3 控制器。

  • pinctrl-names = "default":指定使用默认的引脚控制设置,这里打开了 PWM3 的两个 channel。

  • pinctrl-0 = <&pinctrl_pwm3_0 &pinctrl_pwm3_1>: 这里打开了 PWM3 两个 channel。

    • 指定了 PWM3 控制器的引脚配置。这是一个指针数组,指向一系列引脚控制配置(通常是由 pinctrl 子系统管理的),每个 pinctrl_pwm3_X 表示一个预定义的引脚配置,用于控制 PWM 信号在物理引脚上的行为,例如引脚号、复用功能、电源域配置等。

4.3.11.5. 功能使用

kernel space

drobot_pwm_probe 函数是一个设备驱动的初始化函数,属于 platform_driver 的 probe 操作。它在设备被检测到并且驱动被加载时执行,用于初始化 PWM 模块硬件资源和配置平台设备。

static int drobot_pwm_probe(struct platform_device *pdev)
{
	struct drobot_pwm_chip *drobot_pwm = NULL;
	struct resource *res = NULL;
	int ret = 0;

	drobot_pwm = devm_kzalloc(&pdev->dev, sizeof(*drobot_pwm), GFP_KERNEL);
	if (!drobot_pwm)
		return -ENOMEM;

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);

	drobot_pwm->base = devm_ioremap_resource(&pdev->dev, res);

	if (IS_ERR(drobot_pwm->base))
		return PTR_ERR(drobot_pwm->base);

	drobot_pwm->clk = devm_clk_get(&pdev->dev, NULL);
	if (IS_ERR(drobot_pwm->clk))
		return PTR_ERR(drobot_pwm->clk);

	ret = clk_prepare_enable(drobot_pwm->clk);
	if (ret < 0) {
		clk_disable_unprepare(drobot_pwm->clk);
		dev_err(&pdev->dev, "failed to enable pwm clock, error %d\n", ret);
		return ret;
	}

	drobot_pwm->reset = devm_reset_control_get_exclusive(&pdev->dev,
						       NULL);
	if (IS_ERR(drobot_pwm->reset))
		return PTR_ERR(drobot_pwm->reset);
	reset_control_assert(drobot_pwm->reset);
	usleep_range(1, 2);
	reset_control_deassert(drobot_pwm->reset);

	drobot_pwm->chip.dev = &pdev->dev;
	drobot_pwm->chip.ops = &drobot_pwm_ops;
	drobot_pwm->chip.npwm = 2;
	drobot_pwm->chip.base = -1;

	ret = pwmchip_add(&drobot_pwm->chip);
	if (ret < 0) {
		dev_err(&pdev->dev, "failed to add PWM chip, error %d\n", ret);
		return ret;
	}

	/* When PWM is disable, configure the output to the default value */
	platform_set_drvdata(pdev, drobot_pwm);
	pm_runtime_enable(&pdev->dev);

	dev_info(&pdev->dev, "D-Robotics PWM register done!\n");

	return 0;
}

驱动代码关键接口

1.devm_kzalloc

  • 作用:为 drobot_pwm_chip 结构体分配内存,并使用内核的内存管理方式 (devm) 进行资源管理。

  • 参数:第一个参数为设备对象 (&pdev->dev),第二个参数是要分配的内存大小,第三个参数为分配标志 (GFP_KERNEL)。

  • 返回值:返回分配的内存地址,若分配失败返回 NULL。

2.platform_get_resource

  • 作用:平台设备中提取指定类型的资源,这里提取的是内存资源 (IORESOURCE_MEM)。

  • 参数:第一个参数为平台设备对象 pdev,第二个参数是资源类型(内存类型),第三个参数是资源的索引(通常为 0 )。

  • 返回值:返回 struct resource 类型的资源结构体,若没有该资源返回 NULL。

3.devm_ioremap_resource

  • 作用:为 drobot_pwm_chip 结构体中的 base 成员赋值,将设备的物理内存地址映射为虚拟地址。

  • 参数:第一个参数为设备对象,第二个参数为要映射的资源。

  • 返回值:返回映射后的虚拟地址,若失败返回 ERR_PTR。

4.devm_clk_get

  • 作用:获取与设备相关的时钟源,并为设备提供时钟支持。

  • 参数:第一个参数为设备对象,第二个参数为时钟名称(此处为 NULL,表示获取默认时钟)。

  • 返回值:返回时钟的句柄,若获取失败返回 ERR_PTR。

5.clk_prepare_enable

  • 作用:在启用设备时钟之前,准备时钟并确保它处于活动状态。

  • 参数:时钟句柄。

  • 返回值:返回时钟启用操作的结果,若失败返回负值。

6.devm_reset_control_get_exclusive

  • 作用:获取并管理与设备相关的复位控制资源。

  • 参数:第一个参数为设备对象,第二个参数为复位控制器的名称(此处为 NULL,表示获取默认复位控制器)。

  • 返回值:返回复位控制器的句柄,若获取失败返回 ERR_PTR。

7.pwmchip_add

  • 作用:将 drobot_pwm 设备的 PWM 配置结构体添加到内核中,供内核管理 PWM 输出。

  • 参数: PWM 配置结构体 (drobot_pwm->chip)。

  • 返回值:返回负值表示添加失败,返回 0 表示成功

8.platform_set_drvdata

  • 作用:将 drobot_pwm 设备的指针保存到平台设备中,以便后续访问。

  • 参数:平台设备对象 pdev,驱动数据(此处是 drobot_pwm)。

  • 返回值:无返回值。

drobot_pwm_probe 函数通过设置 drobot_pwm->chip.opsdrobot_pwm_ops,即该驱动中定义的 PWM 操作接口,

static const struct pwm_ops drobot_pwm_ops = {
	.apply = drobot_pwm_apply,
	.owner = THIS_MODULE,
};

通过 drobot_pwm_apply 来设置占空比、周期、极性等,最后调用 robot_pwm_enable 来使能 pwm 输出。

user space

sysfs 节点调试

在板端操作 PWM 时,需要使用 cat 命令,读取 pwmchip 下的 device/uevent 文件,查看当前 pwmchip 的地址是否与目标 PWM 地址一致,以 pwmchip0 为例,在板端使用以下命令查看 pwmchip0 的 uevent。

cat /sys/class/pwm/pwmchip0/device/uevent
DRIVER=drobot-pwm
OF_NAME=pwm
OF_FULLNAME=/soc/a55_apb0/pwm@34160000
OF_COMPATIBLE_0=d-robotics,pwm
OF_COMPATIBLE_N=1
MODALIAS=of:NpwmT(null)Cd-robotics,pwm

查看 x5.dtsi 中 关于 PWM 控制器节点 如下:

pwm2: pwm@34160000 {
	compatible = "d-robotics,pwm";
	status = "disabled";
	reg = <0x34160000 0x10000>;
	interrupt-parent = <&gic>;
	interrupts = <GIC_SPI 97 IRQ_TYPE_LEVEL_HIGH>;
	clocks = <&hpsclks X5_LSIO_PWM2_PCLK>;
	#pwm-cells = <2>;
	resets = <&socrst LSIO_PWM2_APB_RESET>;
};

可以看出 pwmchip0 的地址为 0x34160000 , DTS 中 PWM2 的地址为 0x34160000 ,因此 PWM2 对应 pwmchip0 。

root@buildroot:/sys/class/pwm# ls
pwmchip0  pwmchip2  pwmchip6

以上设备分别对应 pwmchip0 – PWM0 , pwmchip2 – lPWM0, 进入 pwmchip0 设备,有以下节点:

root@buildroot:/sys/class/pwm# cd pwmchip0
root@buildroot:/sys/class/pwm/pwmchip0# ls
device  export  npwm  power  subsystem  uevent  unexport
  • device: 这是一个指向设备的符号链接,它通常链接到实际的硬件设备节点。通过访问该文件,系统可以获取或修改与该设备相关的属性。

  • npwm: 指当前 PWM 所包含的 channel 数量, pwm 通道数为 2 。

  • export:用户可以通过 echo 向该文件写入 PWM 通道的编号,将通道导出到 /sys/class/pwm/pwmchip0/pwmX 目录(其中 X 为 PWM 通道的编号)。通过这种方式,系统允许用户对该通道进行配置和控制。

  • power:该文件提供了与设备电源管理相关的信息。它可以包含关于设备的电源状态的详细信息,比如是否启用节能模式、是否开启了电源等。

  • subsystem:该目录是指向该 PWM 设备所属的子系统的符号链接。子系统是 Linux 中的设备管理层次结构的一部分,表示该设备所属的类别(如 PWM 设备、 I2C 设备等)。

  • uevent:该文件用于管理 udev(设备管理器)事件,它通常用于向系统通知设备的变化。

  • unexport:该文件允许用户通过写入来取消导出某个 PWM 通道,如果某个 PWM 通道已经被导出(通过 export 文件),则用户可以通过向 unexport 文件写入该通道的编号来取消导出,从而使该 PWM 通道不再可用。

可以通过 echo 的方式,设置 period, duty_cycle 参数。注意, linux PWM 框架参数精度为 1ns,输入参数会经过四舍五入计算出精度为 1us 的参数设置到寄存器,所以 period/duty_cycle 输入值需要 x1000

  • 申请注册 PWM0 通道:

root@buildroot:/sys/class/pwm/pwmchip0# echo 0 > export
root@buildroot:/sys/class/pwm/pwmchip0# cd pwm0
  • 设置 周期 为 100us:

root@buildroot:/sys/class/pwm/pwmchip0/pwm0# echo 100000 > period
  • 设置 占空比 为 50%:

root@buildroot:/sys/class/pwm/pwmchip0/pwm0# echo 50000 > duty_cycle
  • 使能 PWM 输出或关闭

root@buildroot:/sys/class/pwm/pwmchip0/pwm0# echo 1 > enable
root@buildroot:/sys/class/pwm/pwmchip0/pwm0# echo 0 > enable

用户可以参考如下脚本读取 PWM 寄存器来验证 PWM 工作是否正常,以 pwmchip0 ch0 为例:

#!/bin/bash
set -e

target_chip="pwmchip0"
target_ch="0"
chip_sysfs_path="/sys/class/pwm/${target_chip}"
ch_sysfs_path="${chip_sysfs_path}/pwm${target_ch}"

cd "$chip_sysfs_path" || { echo "$chip_sysfs_path not found! Abort!"; exit 1; }
if [ ! -d "$ch_sysfs_path" ];then
	echo "$target_ch" > export
fi
cd "pwm${target_ch}"

# 配置周期为 100us
echo 100000 > period
# 配置占空比为 50% = 100us * 0.5 = 50us
echo 50000 > duty_cycle
# 使能 PWM 输出
echo 1 > enable

# 以下是进行寄存器读取
chip_reg="0x$(cat ${chip_sysfs_path}/device/uevent | grep OF_FULLNAME | awk -F'@' '{print $2}')"
echo "Regs of ${target_chip}:"
echo "PWM_EN       `devmem $(printf "0x%X" $((chip_reg + 0x00))) 32`"
echo "PWM_INT_CTRL `devmem $(printf "0x%X" $((chip_reg + 0x04))) 32`"
echo "PWMCH0_CTRL    `devmem $(printf "0x%X" $((chip_reg + 0x10))) 32`"
echo "PWMCH0_CLK     `devmem $(printf "0x%X" $((chip_reg + 0x14))) 32`"
echo "PWMCH0_PERIOD  `devmem $(printf "0x%X" $((chip_reg + 0x20))) 32`"
echo "PWMCH0_STATUS  `devmem $(printf "0x%X" $((chip_reg + 0x28))) 32`"
echo "PWMCH1_CTRL    `devmem $(printf "0x%X" $((chip_reg + 0x30))) 32`"
echo "PWMCH1_CLK     `devmem $(printf "0x%X" $((chip_reg + 0x34))) 32`"
echo "PWMCH1_PERIOD  `devmem $(printf "0x%X" $((chip_reg + 0x40))) 32`"
echo "PWMCH1_STATUS  `devmem $(printf "0x%X" $((chip_reg + 0x48))) 32`"