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`"