
# PWM 驱动调试指南

## 概述

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

参数规格：

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

## 特点

芯片中 PWM 具有以下特点：

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

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

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

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

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

## 功能描述

### 典型应用

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](_static/_images/29-PWM_Driver_Debug_Guide/pwm_led.png)

PWM 信号的基本结构：

- 周期：从上升沿到下一个上升沿的时间 ,100HZ 表示 1 秒钟内有 100 个时钟周期。
- 占空比（ Duty Cycle）：占空比是指 PWM 信号在一个周期内“高电平”持续的时间比例。占空比通常以百分比表示。例如，占空比为 50% 时，信号的一半时间为高电平，另一半时间为低电平。
- 频率（ Frequency）：频率是指 PWM 信号每秒钟重复的次数，频率和周期成反比，频率 = 1 / 周期。

### 功能原理

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

![pwm_framework](_static/_images/29-PWM_Driver_Debug_Guide/pwm_framework.png)

### 工作方式

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

- 设备初始化：驱动程序通过设备树或者平台代码初始化 PWM 硬件，并配置硬件寄存器。
- PWM 配置：通过 `drobot_pwm_apply` 或用户空间接口，设置 PWM 的周期（频率）和占空比。
- 启用 PWM ：通过调用 `pwm_enable()` 来启动 PWM 输出。当 PWM 被启用时，硬件控制器将按指定的频率和占空比生成 PWM 波形。
- 运行期间调节：可以通过系统接口实时调整 PWM 的占空比和频率。
- 关闭 PWM：当不再需要 PWM 输出时，可以通过 `pwm_disable()` 来停止 PWM 输出。

具体流程如下：

![pwm_flowchart](_static/_images/29-PWM_Driver_Debug_Guide/pwm_flowchart.png)

## 驱动代码

### PWM 代码说明

```shell
drivers/pwm/pwm-drobot.c
```

### 内核配置

```shell
/* 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`。

<font color=red> 备注：</font>
x5.dtsi 中的节点主要声明 SoC 共有特性，和具体电路板无关，一般情况下不用修改。

```shell
/* 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 信号在物理引脚上的行为，例如引脚号、复用功能、电源域配置等。

## 功能使用

### kernel space

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

```c
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 操作接口，

```c
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。

```shell
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 控制器节点 如下：

```shell
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 。

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

以上设备分别对应 pwmchip0 -- PWM0 ， pwmchip2 -- lPWM0, 进入 pwmchip0 设备，有以下节点：

```shell
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 通道：

```shell
root@buildroot:/sys/class/pwm/pwmchip0# echo 0 > export
root@buildroot:/sys/class/pwm/pwmchip0# cd pwm0
```

- 设置 周期 为 100us：

```shell
root@buildroot:/sys/class/pwm/pwmchip0/pwm0# echo 100000 > period
```

- 设置 占空比 为 50%：

```shell
root@buildroot:/sys/class/pwm/pwmchip0/pwm0# echo 50000 > duty_cycle
```

- 使能 PWM 输出或关闭

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

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

```shell
#!/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`"
```
