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 信号的基本结构:
周期:从上升沿到下一个上升沿的时间 ,100HZ 表示 1 秒钟内有 100 个时钟周期。
占空比( Duty Cycle):占空比是指 PWM 信号在一个周期内“高电平”持续的时间比例。占空比通常以百分比表示。例如,占空比为 50% 时,信号的一半时间为高电平,另一半时间为低电平。
频率( Frequency):频率是指 PWM 信号每秒钟重复的次数,频率和周期成反比,频率 = 1 / 周期。
功能原理
下图为 PWM 子系统框架,其可大致分为三层,即用户层、核心层及硬件层。

工作方式
PWM 信号的生成与控制流程可以分为几个步骤:
设备初始化:驱动程序通过设备树或者平台代码初始化 PWM 硬件,并配置硬件寄存器。
PWM 配置:通过
drobot_pwm_apply或用户空间接口,设置 PWM 的周期(频率)和占空比。启用 PWM :通过调用
pwm_enable()来启动 PWM 输出。当 PWM 被启用时,硬件控制器将按指定的频率和占空比生成 PWM 波形。运行期间调节:可以通过系统接口实时调整 PWM 的占空比和频率。
关闭 PWM:当不再需要 PWM 输出时,可以通过
pwm_disable()来停止 PWM 输出。
具体流程如下:

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.ops 为 drobot_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`"