PWM子系统基础学习与应用笔记

一、PWM基础知识回顾

脉宽调制(Pulse-Width Modulation,PWM)是利用微处理器的数字输出,来对模拟电路进行控制的一种非常有效的技术,通过对一系列脉冲的宽度进行调制,来等效的获得所需要的波形(含形状和幅值),即通过改变导通时间占总时间的比例,也就是占空比,达到调整电压和频率的目的。

PWM中两个比较重要的参数:

  • 周期(T):指一个完整的高低电平循环所需要的时间,而频率为周期的倒数,指在 1 秒钟有多少个周期, 单位为 Hz,例如一个周期是 20ms,那么一秒钟就有 50 次 PWM 周期。
  • 占空比(Duty Cycle):是指高电平时间与周期的比例,通常以百分比表示,例如周期为 20ms, 高电平所占的时间为 10ms,那占空比就是 50%。

PWM除了能够对电压的控制之外,PWM 还有着以下的工作场景:

  • 电机控制:控制电机转速和转矩。
  • 照明控制:控制 LED 亮度。
  • 信号调制:用于无线电通讯中的调制。
    至此,对于 PWM 的基本介绍就完成了,在下个小节中将对瑞芯微开发板的PWM接口进行介绍。

二、PWM子系统框架

整个PWM子系统的图如下所示:
Pasted image 20260204160521.png

在上图中 PWM 子系统被划分为了三个层次,分别为用户空间、内核空间和硬件层,内核空间包括 PWM 设备驱动层、 PWM 核心层和 PWM 控制器驱动层。也可以看见从核心层到用户空间层有两种方式选择,第一就是通过设备驱动程序,第二就是直接通过sysfs文件系统,这恰好对应了两种操作PWM的方式,下面对不同层的作用进行简要阐述:

2.1 设备驱动层

PWM的设备驱动层,主要是为了给上层应用程序提供PWM设备的访问接口,调用核心层提供的一系列接口函数,提高系统的可扩展性和可维护性。
主要包含以下几个关键的部分:
(1)pwm_device表示一个连接到PWM总线上的从设备
(2)/dev/xxx设备节点主要是为上层应用程序提供pwm设备访问接口,通过打开/读写/控制设备节点,应用程序可以与 PWM 设备进行交互

2.2 PWM核心层

通过 struct pwm_device 统一描述 PWM 设备的信息,如设备名称、所属控制器等。为上层应用程序提供标准化的 PWM 设备访问 API。
(1)PWM 设备管理
通过struct pwm_device 统一描述 PWM 设备的信息,如设备名称、所属控制器等。为上层应用程序提供标准化的 PWM 设备访问 API,例如通过devm_pwm_get函数获取一个struct pwm_device
(2)PWM 参数配置
提供 pwm_config 接口,用于配置 PWM 设备的工作参数,如周期、占空比、极性等。将上层配置参数转换为底层硬件可以理解的形式,并通过适配器驱动进行设置。
(3)PWM 输出控制
提供 pwm_enable pwm_disable 接口,用于控制 PWM 输出的开启和关闭。负责调用适配器驱动程序的相应函数来执行实际的输出控制。
(4)PWM sysfs 接口
PWM 核心层还提供了一组基于 sysfs 的接口,用于上层应用程序对 PWM 设备进行配置和控制。

2.3 PWM控制器驱动层

PWM 控制器驱动层是连接 PWM 硬件和上层 PWM 核心层的关键组件,它负责对底层 PWM控制器硬件进行抽象化封装,并适配不同类型的 PWM 设备,为上层提供标准化的硬件控制接口,同时还需要处理一些硬件相关的初始化、状态管理和电源管理等任务。PWM 控制器驱动层的具体作用如下所示:
(1)硬件抽象化
PWM 控制器驱动层负责对底层 PWM 硬件设备进行抽象化封装。它定义了一组标准化的 pwm_chip 接口,包括设备初始化、参数配置、输出控制等功能。上层 PWM 核心层可以通过调用这些标准接口来间接控制底层 PWM 硬件,而不需要关注具体的硬件实现细节。
(2)硬件适配
PWM 控制器驱动程序需要实现 pwm_chip 接口中定义的各个函数,以适配底层 PWM 硬件的具体工作方式,这部分主要是原厂工程师编写。这些适配函数将上层 PWM 核心层的标准化请求,转换为底层硬件可以理解的操作指令。通过这种适配层,PWM 核心层可以无缝地支持不同类型的 PWM 硬件设备。
(3)硬件初始化
PWM 控制器驱动程序的probe函数中,主要负责完成 PWM 硬件设备的初始化工作,包括 GPIO、时钟、电源等相关资源的配置和初始化。在 PWM 设备注册时,控制器驱动会执行初始化操作,确保硬件设备处于可用状态。
(4)状态管理
适配器驱动程序需要维护 PWM 硬件设备的当前状态,如设备是否已经被使能、当前的工作参数等。这些状态信息会反馈给上层 PWM 核心层,以确保核心层能够正确地控制和管理 PWM设备。

至此对于 PWM 子系统框架的讲解就完成了,可能大家对PWM子系统的框架还比较模糊,具体之后还会详细讲解

三、设备树中PWM设备节点配置

在使用PWM外设之前,先需要在设备树中配置PWM节点,配置PWM节点主要有以下三个地方需要注意

1、通用PWM节点配置
在瑞芯微提供的设备树头文件kernel/arch/arm64/boot/dts/rockchip/rk3576.dtsi,已经默认写好了各个pwm通道的设备树配置,下面举一个例子

pwm1_6ch_2: pwm@2add2000 {
	compatible = "rockchip,rk3576-pwm";
	reg = <0x0 0x2add2000 0x0 0x1000>;
	interrupts = <GIC_SPI 104 IRQ_TYPE_LEVEL_HIGH>;
	#pwm-cells = <3>;
	pinctrl-names = "active";
	pinctrl-0 = <&pwm1m0_ch2>;
	clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>, <&cru CLK_OSC_PWM1>;
	clock-names = "pwm", "pclk", "osc";
	status = "disabled";
};

可以看见瑞芯微官方对这个pwm通道的配置,其中需要注意的是默认的pinctrl-0属性,和status属性,后续需要根据自己需求进行自定义修改。还有就是节点名称的意义,pwm1_6ch_2表示pwm控制器1,一共有6个通道,该节点对应是通道2。

2、PWM引脚配置
在上面我们看见了瑞芯微默认的pinctrl-0属性,它具体表示什么意思呢?可以看见该PWM通道,被设置在了pwm1m0_ch2对应的引脚上,这个引脚名字pwm1表示PWM控制器1,ch2表示通道2。重点讲一下m0,在瑞芯微平台对于一个PWM通道,可以在多个引脚上进行复用,这里就以pwm1的ch2为例

打开kernel/arch/arm64/boot/dts/rockchip/rk3576-pinctrl.dtsi设备树头文件,这个文件是瑞芯微对rk3576平台所有外设的引脚定义文件,找到pwm1节点,下面已经列出对应设备树代码

pwm1 {
/omit-if-no-ref/

	pwm1m0_ch0: pwm1m0-ch0 {
		rockchip,pins =
			/* pwm1_ch0_m0 */
			<0 RK_PB4 12 &pcfg_pull_none>;
	};
	
	/omit-if-no-ref/
	pwm1m0_ch1: pwm1m0-ch1 {
		rockchip,pins =
			/* pwm1_ch1_m0 */
			<0 RK_PB5 12 &pcfg_pull_none>;
	};
	
	/omit-if-no-ref/
	pwm1m0_ch2: pwm1m0-ch2 {
		rockchip,pins =
			/* pwm1_ch2_m0 */
			<0 RK_PB6 12 &pcfg_pull_none>;
	};
	
	/omit-if-no-ref/
	pwm1m0_ch3: pwm1m0-ch3 {
		rockchip,pins =
			/* pwm1_ch3_m0 */
			<0 RK_PC0 12 &pcfg_pull_none>;
	};
	
	/omit-if-no-ref/
	pwm1m0_ch4: pwm1m0-ch4 {
		rockchip,pins =
			/* pwm1_ch4_m0 */
			<0 RK_PB7 12 &pcfg_pull_none>;
	};
	
	/omit-if-no-ref/
	pwm1m0_ch5: pwm1m0-ch5 {
		rockchip,pins =
			/* pwm1_ch5_m0 */
			<0 RK_PD2 12 &pcfg_pull_none>;
	};
	
	/omit-if-no-ref/
	pwm1m1_ch0: pwm1m1-ch0 {
		rockchip,pins =
			/* pwm1_ch0_m1 */
			<1 RK_PB4 13 &pcfg_pull_none>;
	};
	
	/omit-if-no-ref/
	pwm1m1_ch1: pwm1m1-ch1 {
		rockchip,pins =
			/* pwm1_ch1_m1 */
			<1 RK_PB5 13 &pcfg_pull_none>;
	};
	
	/omit-if-no-ref/
	pwm1m1_ch2: pwm1m1-ch2 {
		rockchip,pins =
			/* pwm1_ch2_m1 */
			<1 RK_PC2 13 &pcfg_pull_none>;
	};
	
	/omit-if-no-ref/
	pwm1m1_ch3: pwm1m1-ch3 {
		rockchip,pins =
			/* pwm1_ch3_m1 */
			<1 RK_PD2 13 &pcfg_pull_none>;
	};
	
	/omit-if-no-ref/
	pwm1m1_ch4: pwm1m1-ch4 {
		rockchip,pins =
			/* pwm1_ch4_m1 */
			<1 RK_PD3 13 &pcfg_pull_none>;
	};
	
	/omit-if-no-ref/
	pwm1m1_ch5: pwm1m1-ch5 {
		rockchip,pins =
			/* pwm1_ch5_m1 */
			<4 RK_PC0 14 &pcfg_pull_none>;
	};
	
	/omit-if-no-ref/
	pwm1m2_ch0: pwm1m2-ch0 {
		rockchip,pins =
			/* pwm1_ch0_m2 */
			<2 RK_PC0 13 &pcfg_pull_none>;
	};
	
	..........
};

可以看见,pwm1的ch2被定义到了多个引脚上,<0 RK_PB6 12 &pcfg_pull_none><1 RK_PC2 13 &pcfg_pull_none>,唯一就是通道节点名称有所不同,所以m0表示的是该通道的复用功能编号,同一通道,同一时刻只能复用在一个引脚上,也就是不能同时使能pwm1m0_ch2pwm1m1_ch2节点,他们都表示pwm1的ch2,只是复用的引脚不同罢了,所以m[编号]表示的是pwm的通道复用编号,具体引脚,更具自己的需求进行选择,选择合理的复用引脚以免和其他外设冲突

3、板级PWM节点配置
在板级dts文件中,会include这个通用rk3576.dtsi头文件,在自己的板级dts配置中引用对应的PWM通道节点,对默认设备树配置进行修改,例如

&pwm1_6ch_2 {
	status = "okay";
	pinctrl-0 = <&pwm1m1_ch2>;
};

在自定义的板级dts中,将引脚信息更具自己需求进行修改,并将status状态修改为okey使能该通道

4、PWM通道节点使用
在 DTS 中,PWM 节点通常被别的驱动所引用,在其中通过 PWM 框架提供的各接口来配置和使用 PWM,本节以常见的背光驱动为例。当然除了作为背光,也可以用作其他用途,这个更具用户需求进行自定义,需要注意的是想要使用该PWM通道节点,必须按照对应的设备树规则来填写属性。

backlight: backlight {
	compatible = "pwm-backlight";
	pwms = <&pwm1_6ch_2 0 25000 0>;
	......
};

在其他节点中,使用pwms属性来指定所用的pwm通道节点,需要指定三个参数分别为

参数 1:表示 index (per-chip index of the PWM to request),值固定为 0。Rockchip 平台的每个 PWM channel 对应一个
PWM device,且每个 device 只有一个 chip。
参数 2:表示 PWM 输出波形的 period,单位为 ns。示例中的 25000 ns 换算为频率即为 40KHz。
参数 3:表示可选参数 polarity,默认为 0,若要翻转极性则配为 PWM_POLARITY_INVERTED。

其中pwms属性为必填项,在对应的驱动程序中,会使用pwm_get()函数来获取pwms对应的属性,pwm-names属性可选,若未指明该属性,默认使用节点名称。更具体的设备树参数规则可以查看`kernel/Documentation/devicetree/bindings/pwm/pwm.txt

需要注意的是,如果这个PWM节点被其他驱动使用的话,例如上面所提出的背光驱动,就无法在sysfs系统中使用echo命令操作这个pwm通道了

PWM 框架在 /sys/class/pwm/ 目录下提供了用户层接口,详见 drivers/pwm/sysfs.c,重新编译内核,将内核重新烧录到开发板中,使用如下命令,查看pwm对应通道是否被打开,当PWM 驱动加载成功后,会在其下生成pwmchipX 目录,如 pwmchip0pwmchip1 等,需要特别注意的是此处的 X 与 PWM 的控制器或通道 id 无关,仅与 PWM 设备的 probe 顺序有关。

wzy@wzy-JiaoLong:~$ adb shell
root@linaro-alip:/# ls /sys/class/pwm/  
pwmchip0
root@linaro-alip:/# cat /sys/kernel/debug/pwm    
platform/2ade5000.pwm, 1 PWM device  
pwm-0   (backlight           ): requested period: 25000 ns duty: 0 ns polarity: normal  
root@linaro-alip:/# cd /sys/class/pwm/  
root@linaro-alip:/sys/class/pwm# cd pwmchip0  
root@linaro-alip:/sys/class/pwm/pwmchip0# ls  
device  export  npwm  power  subsystem  uevent  unexport  
root@linaro-alip:/sys/class/pwm/pwmchip0# ls -ahl  
总计 0  
drwxr-xr-x 3 root root    0  2月 4日 10:42 .  
drwxr-xr-x 3 root root    0  2月 4日 10:42 ..  
lrwxrwxrwx 1 root root    0  2月 4日 10:58 device -> ../../../2ade5000.pwm  
--w------- 1 root root 4.0K  2月 4日 10:58 export  
-r--r--r-- 1 root root 4.0K  2月 4日 10:58 npwm  
drwxr-xr-x 2 root root    0  2月 4日 10:58 power  
lrwxrwxrwx 1 root root    0  2月 4日 10:42 subsystem -> ../../../../../class/pwm  
-rw-r--r-- 1 root root 4.0K  2月 4日 10:42 uevent  
--w------- 1 root root 4.0K  2月 4日 10:58 unexport  
root@linaro-alip:/sys/class/pwm/pwmchip0#

又结果可以看,在/sys/class/pwm目录下已经使能了一个pwm控制器,并且在/sys/kernel/debug/pwm调试信息中,也有pwm启动的内容,并且对应的pwm外设地址与设备树中使能的相同,进入pwmchip0控制器对应的目录下,可以看见已经开启了一个通道pwm0(这里多个通道没有解释清楚)

PWM设备配置完成之后,接下来就是如何使用它了

四、sysfs文件系统驱动PWM

当我们在用户态想要使用PWM功能时,有两种方法,第一种方法是将PWM进行sysfs系统注册,直接向PWM对应的sysfs系统的相应控制文件中写入对应的值就能够控制PWM实现相关功能。第二种是通过编写PWM驱动程序,加载驱动后,在用户态通过系统调用接口来控制底层PWM。这里先讲解如何使用sysfs文件文件系统来控制PWM

当PWM通过内核框架注册好之后,可以直接通过echo命令向对应文件中写入配置参数来控制PWM外设。在对应的/sys/class/pwm/pwmchip0/pwmY下会出现这几个可操作的节点:

enable:写入 1 使能 PWM,写入 0 关闭 PWM;
polarity:有 normalinversed 两个参数选择,对应 PWM 的极性配PWM_POLARITY_NORMAL/PWM_POLARITY_INVERSED;
duty_cycle:在 normal 模式下,表示一个周期内高电平持续的时间(单位:ns),在 reversed 模式下,表示一个周期中低电平持续的
时间(单位:ns);
period:表示 PWM 波形的周期(单位:ns);
oneshot_count:需开启 CONFIG_PWM_ROCKCHIP_ONESHOT,表示 oneshot 模式的 PWM 波形个数;
oneshot_repeat:需开启 CONFIG_PWM_ROCKCHIP_ONESHOT 且仅 PWM v4 支持,表示 oneshot 模式重复的次数,最后输出的波形个数为 oneshot_repeat * oneshot_count;
duty_offset:需开启 CONFIG_PWM_ROCKCHIP_ONESHOT,表示 PWM 输出波形偏移的时间(单位:ns);
capture:使能 capture 模式,获取输入波形高低电平的时长(单位:ns)。

操作步骤:

# 切换到pwmchip0控制器路径
cd /sys/class/pwm/pwmchip0/
# 导出pwm
echo 0 > export
# 切换换到对应的pwm文件夹下
cd pwm0
# 设置周期
echo 10000 > period
# 设置高电平时间
echo 5000 > duty_cycle
# 设置极性
echo normal > polarity
# 使能PWM
echo 1 > enable

通过sysfs文件系统可以很方便的对我们的PWM进行调试,下一小节将讲解如何自己编写驱动程序来使用PWM功能

五、编写驱动程序来驱动PWM

5.1 PWM控制器注册流程分析

上面只是简单介绍了如何配置pwm设备树,如何通过sysfs文件系统操作对应的pwm设备,这一小结先对PWM控制器注册流程进行分析,PWM控制器的驱动程序一般是原厂工程师来完成,这里只是进行简单分析,了解其过程。

首先打开瑞芯微通用设备树rk3576.dtsi文件,找到对应的pwm节点以及其compatible属性

Pasted image 20260205094731.png

根据 PWM 节点的 compatible 属性进行查找,可以找到瑞芯微的 PWM 驱动路径为内核目录下的kernel/drivers/pwm/pwm-rockchip.c,打开源文件,首先可以看见驱动的入口函数

Pasted image 20260205095311.png
可以看见瑞芯微的PWM控制器是使用的platform总线框架,在驱动的入口init函数中,注册了一个pwm控制器的platform_driver用来与设备进行匹配,device在设备树中实现,同时找到对应的of_match_table表,与设备树中的compatible相匹配,之后会调用对应的probe函数进行初始化与注册,这里还有一个值得注意的点,就是.data成员变量,瑞芯微平台的pwm驱动经过版本迭代有v1,v2,v3,v4多个版本,通过.data成员变量来用同一个驱动,适配多代 SoC,但每一代的硬件细节不同,通过.data 把差异传给驱动。

Pasted image 20260205095458.png

具体的probe函数如下,对关键步骤进行讲解

static int rockchip_pwm_probe(struct platform_device *pdev)
{
	const struct of_device_id *id;
	struct rockchip_pwm_chip *pc;
	struct resource *r;
	u32 enable_conf, ctrl, version;
	bool enabled;
	int ret, count;
	
	// 检查设备树匹配
	id = of_match_device(rockchip_pwm_dt_ids, &pdev->dev);
	if (!id)
		return -EINVAL;

	// 分配PWM chip结构体
	pc = devm_kzalloc(&pdev->dev, sizeof(*pc), GFP_KERNEL);
	if (!pc)
		return -ENOMEM;
	
	// 获取资源并映射IO地址
	r = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (!r) {
		dev_err(&pdev->dev, "Failed to get pwm register\n");
		return -EINVAL;
	}
	pc->res = r;

	pc->base = devm_ioremap(&pdev->dev, pc->res->start,
	resource_size(pc->res));

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

	pc->clk = devm_clk_get(&pdev->dev, "pwm");

	if (IS_ERR(pc->clk)) {
		pc->clk = devm_clk_get(&pdev->dev, NULL);
		if (IS_ERR(pc->clk))
			return dev_err_probe(&pdev->dev, PTR_ERR(pc->clk),
			"Can't get PWM clk\n");
	}
	
	// 获取设备树中的总线时钟信息
	count = of_count_phandle_with_args(pdev->dev.of_node,
	"clocks", "#clock-cells");

	if (count >= 2) {
		pc->pclk = devm_clk_get(&pdev->dev, "pclk");
		pc->clk_osc = devm_clk_get_optional(&pdev->dev, "osc");
	} else {
		pc->pclk = pc->clk;
	}
	
	if (IS_ERR(pc->pclk))
		return dev_err_probe(&pdev->dev, PTR_ERR(pc->pclk), "Can't get APB clk\n");
	
	// 使能总线时钟
	ret = clk_prepare_enable(pc->clk);

	if (ret)
		return dev_err_probe(&pdev->dev, ret, "Can't prepare enable PWM clk\n");

	ret = clk_prepare_enable(pc->pclk);

	if (ret) {
		dev_err_probe(&pdev->dev, ret, "Can't prepare enable APB clk\n");
		goto err_clk;
	}
	
	// 获取引脚控制器
	pc->pinctrl = devm_pinctrl_get(&pdev->dev);

	if (IS_ERR(pc->pinctrl)) {
		dev_err(&pdev->dev, "Get pinctrl failed!\n");
		ret = PTR_ERR(pc->pinctrl);
		goto err_pclk;
	}
	
	pc->active_state = pinctrl_lookup_state(pc->pinctrl, "active");

	if (IS_ERR(pc->active_state)) {
		dev_err(&pdev->dev, "No active pinctrl state\n");
		ret = PTR_ERR(pc->active_state);
		goto err_pclk;
	}
	
	// 设置驱动数据
	platform_set_drvdata(pdev, pc);
	
	// 初始化PWM chip结构体
	pc->data = id->data;
	pc->chip.dev = &pdev->dev;
	pc->chip.ops = &rockchip_pwm_ops;
	pc->chip.base = of_alias_get_id(pdev->dev.of_node, "pwm");
	pc->chip.npwm = 1;
	pc->clk_rate = clk_get_rate(pc->clk);
	pc->main_version = pc->data->main_version;
	if (pc->main_version >= 4) {
		version = readl_relaxed(pc->base + pc->data->regs.version);
		pc->channel_id = (version & CHANNLE_INDEX_MASK) >> CHANNLE_INDEX_SHIFT;
		pc->ir_trans_support = !!(version & IR_TRANS_SUPPORT);
		pc->freq_meter_support = !!(version & FREQ_METER_SUPPORT);
		pc->counter_support = !!(version & COUNTER_SUPPORT);
		pc->wave_support = !!(version & WAVE_SUPPORT);
		pc->biphasic_support = !!(version & BIPHASIC_SUPPORT);
	} else {
		pc->channel_id = rockchip_pwm_get_channel_id(pdev->dev.of_node->full_name);
	}
	
	if (pc->channel_id < 0 || pc->channel_id >= PWM_MAX_CHANNEL_NUM) {
		dev_err(&pdev->dev, "Channel id is out of range: %d\n", pc->channel_id);
		ret = -EINVAL;
		goto err_pclk;
	}

	if (pc->data->funcs.irq_handler) {
		if (pc->main_version >= 4) {
			pc->irq = platform_get_irq(pdev, 0);
			if (pc->irq < 0) {
				dev_err(&pdev->dev, "Get irq failed\n");
				ret = pc->irq;
				goto err_pclk;
			}
	
			ret = devm_request_irq(&pdev->dev, pc->irq, pc->data->funcs.irq_handler,
			IRQF_NO_SUSPEND, "rk_pwm_irq", pc);
			if (ret) {
				dev_err(&pdev->dev, "Claim IRQ failed\n");
				goto err_pclk;
			}
		} else {
			if (IS_ENABLED(CONFIG_PWM_ROCKCHIP_ONESHOT)) {
				pc->irq = platform_get_irq_optional(pdev, 0);
				if (pc->irq < 0) {
				dev_warn(&pdev->dev,
				"Can't get oneshot mode irq and oneshot interrupt is unsupported\n");
				} else {

					ret = devm_request_irq(&pdev->dev, pc->irq,
					pc->data->funcs.irq_handler,
					IRQF_NO_SUSPEND | IRQF_SHARED,
					"rk_pwm_oneshot_irq", pc);
		
					if (ret) {
					dev_err(&pdev->dev, "Claim oneshot IRQ failed\n");
					goto err_pclk;
					}
				}
			}
		}
	}

	enable_conf = pc->data->enable_conf;
	if (pc->main_version >= 4)
		ctrl = readl_relaxed(pc->base + pc->data->regs.enable);
	else
		ctrl = readl_relaxed(pc->base + pc->data->regs.ctrl);
	enabled = (ctrl & enable_conf) == enable_conf;

	pc->center_aligned =
	device_property_read_bool(&pdev->dev, "center-aligned");

	ret = devm_pwmchip_add(&pdev->dev, &pc->chip);

	if (ret < 0) {
		dev_err_probe(&pdev->dev, ret, "pwmchip_add() failed\n");
		goto err_pclk;
	}

	if (pc->wave_support) {
		if (!pc->clk_osc) {
			dev_err(&pdev->dev, "Can't find OSC clk for wave generator mode\n");
			ret = -EINVAL;
			goto err_pclk;
		}

		ret = clk_prepare(pc->clk_osc);
		if (ret) {
			dev_err(&pdev->dev, "Can't prepare OSC clk for wave generator mode\n");
			goto err_pclk;
		}
	}

#ifdef CONFIG_RC_CORE
	if (pc->ir_trans_support &&
		device_property_present(&pdev->dev, "rockchip,pwm-ir-transmit")) {
		struct rc_dev *rcdev;

		init_completion(&pc->ir_trans_completion);
		rcdev = devm_rc_allocate_device(&pdev->dev, RC_DRIVER_IR_RAW_TX);
		if (!rcdev)
			goto err_pclk;
			
		rcdev->priv = pc;
		rcdev->driver_name = "rockchip-pwm-ir-tx";
		rcdev->device_name = "Rockchip IR TX";
		rcdev->tx_ir = rockchip_pwm_ir_transmit;
		ret = devm_rc_register_device(&pdev->dev, rcdev);
		if (ret < 0)
			goto err_pclk;
	}
#endif

	rockchip_pwm_debugfs_init(pc);
/* Keep the PWM clk enabled if the PWM appears to be up and running. */
	if (!enabled)
		clk_disable(pc->clk);
		
	clk_disable(pc->pclk);
	return 0;
	
err_pclk:
	clk_disable_unprepare(pc->pclk);
err_clk:
	clk_disable_unprepare(pc->clk);
	
	return ret;
}

在第 4 行创建了一个 struct rockchip_pwm_chip 类型的指针变量,它主要用于描述 Rockchip 系列 SoC 上的 PWM 控制器的硬件特性和配置信息,具体内容如下所示:

struct rockchip_pwm_chip {
	struct pwm_chip chip;  // linux提供的通用PWM控制器结构体
	struct clk *clk;  // pwm控制器主时钟
	struct clk *pclk;  // pwm控制器外设时钟
	struct clk *clk_osc;
	struct pinctrl *pinctrl;  // pwm控制器的引脚复用控制器
	struct pinctrl_state *active_state;  // 当前使用引脚的抓功能太
	struct delayed_work pwm_work;
	const struct rockchip_pwm_data *data;
	const struct rockchip_pwm_biphasic_config *biphasic_config;
	struct resource *res;
	struct dentry *debugfs;
	struct completion ir_trans_completion;
	void __iomem *base;
	unsigned long clk_rate;
	unsigned long is_clk_enabled;
	bool vop_pwm_en; /* indicate voppwm mirror register state */
	bool center_aligned;
	bool oneshot_en;  // 是否使能了oneshot模式
	bool capture_en;  // 是否使能了捕获模式
	bool wave_en;
	bool global_ctrl_grant;
	bool ir_trans_support;
	bool freq_meter_support;
	bool counter_support;
	bool wave_support;
	bool biphasic_support;
	bool freq_res_valid;
	bool biphasic_res_valid;
	int channel_id;
	int irq;
	u32 scaler;
	u8 main_version;
	u8 capture_cnt;
};

其中第二行的 struct pwm_chip 结构体是linux内核的 PWM 子系统中对 PWM 控制器的一种抽象和封装。如果其他半导体厂商想要描述自己板卡上的PWM控制器,就需要它将 PWM 控制器的基本信息和操作函数集中到一个结构体中,方便上层 PWM 框架进行管理和调用,该结构体定义在 include/linux/pwm.h 文件中,具体内容如下所示:

struct pwm_chip {
	struct device *dev;  // PWM控制器所属的设备
	struct list_head list;  // PWM 控制器所在的全局链表
    const struct pwm_ops *ops;  // PWM 控制器的操作函数集
    int base;  // number of first PWM controlled by this chip
	unsigned int npwm;   // number of PWMs controlled by this chip
	struct pwm_device *pwms;  // PWM通道设备数组
	struct pwm_device* (*of_xlate)(struct pwm_chip *pc, const struct of_phhandle_args* args);  // request a PWM device given a device tree PWM specifier
	unsigned int of_pwm_n_cells;  // 设备树中PWM属性描述的属性数量
}

pwm_chip这个结构体中,比较重要的就是const struct pwm_ops *ops;该成员主要定义了PWM控制器的各类操作函数,这块主要是原厂进行实现

struct pwm_ops {
	int (*request)(struct pwm_chip *chip, struct pwm_device *pwm);
	void (*free)(struct pwm_chip *chip, struct pwm_device *pwm);
	int (*capture)(struct pwm_chip *chip, struct pwm_device *pwm,
			struct pwm_capture *result, unsigned long timeout);
	int (*apply)(struct pwm_chip *chip, struct pwm_device *pwm,
			const struct pwm_state *state);
	int (*get_state)(struct pwm_chip *chip, struct pwm_device *pwm,
	struct pwm_state *state);
	struct module *owner;

};

可以看见在pwm-rockchip.c源文件中瑞芯微主要实现了其中的三个操作函数

static const struct pwm_ops rockchip_pwm_ops = {
	.capture = rockchip_pwm_capture,
	.apply = rockchip_pwm_apply,
	.get_state = rockchip_pwm_get_state,
	.owner = THIS_MODULE,
};

第二个比较重要的就是struct pwm_device *pwms;表示的是在这个pwm_chip下的pwm_device数组,具体结构体如下所示,其中struct pwm_chip *chip;成员用来表示当前这个pwm_device属于哪一个pwm_chip

struct pwm_device {
	const char *label;  // name of the PWM device
	unsigned long flags;  // flags associated with the PWM device
	unsigned int hwpwm;  // per-chip relative index of the PWM device
	unsigned int pwm;  // global index of the PWM device
	struct pwm_chip *chip;  // PWM chip providing this PWM device
	void *chip_data;  // chip-private data associated with the PWM device
	
	struct pwm_args args;  // PWM arguments
	struct pwm_state state;
	struct pwm_state last;
};

总结下来,probe函数的流程主要包含以下几步:

  1. 定义rockchip_pwm_chip结构体,对结构体及其成员变量动态内存分配
  2. 解析设备树中对应的pwm节点,获取内存资源并映射IO地址
  3. 获取PWM总线时钟和APB时钟,并使能这些时钟
  4. 获取引脚控制器并获取活动状态
  5. rockchip_pwm_chip结构体中的成员进行赋值
  6. 通过pwmchip_add函数将填充好的结构体注册到内核中去

pwmchip_add注册函数的代码如下所示,最关键的就是在第62行通过调用list_add(&chip->list, &pwm_chips);将其添加到内核的PWM子系统中去管理

int pwmchip_add(struct pwm_chip *chip)
{
	struct pwm_device *pwm;
	unsigned int i;
	int ret;
	
	// 检查chip结构体有效性
	if (!chip || !chip->dev || !chip->ops || !chip->npwm)
		return -EINVAL;
	
	// 检查pwm_ops结构体有效性
	if (!pwm_ops_check(chip))
		return -EINVAL;
	
	// 获取全局pwm_lock互斥锁
	mutex_lock(&pwm_lock);
	
	// 分配pwm设备索引号
	ret = alloc_pwms(chip->npwm);
	if (ret < 0)
		goto out;
	
	// 保存第一个索引号
	chip->base = ret;
	
	// 更具`chip->npwm`数量分配对应个pwm_device结构体
	chip->pwms = kcalloc(chip->npwm, sizeof(*pwm), GFP_KERNEL);

	if (!chip->pwms) {
		ret = -ENOMEM;
		goto out;
	}
	
	// 开始初始化每个PWM设备
	for (i = 0; i < chip->npwm; i++) {
		pwm = &chip->pwms[i];
		pwm->chip = chip;
		pwm->pwm = chip->base + i;
		pwm->hwpwm = i;
		// 将PWM设备添加到全局PWM设备树中
		radix_tree_insert(&pwm_tree, pwm->pwm, pwm);
	}

	bitmap_set(allocated_pwms, chip->base, chip->npwm);
	
	// 将PWM控制器添加到全局PWM控制器链表中
	INIT_LIST_HEAD(&chip->list);
	list_add(&chip->list, &pwm_chips);
	
	ret = 0;

	if (IS_ENABLED(CONFIG_OF))
		of_pwmchip_add(chip);

out:
	mutex_unlock(&pwm_lock);
	
	// 如果添加成功,在sysfs中导出pwm控制器
	if (!ret)
		pwmchip_sysfs_export(chip);
	return ret;
}
EXPORT_SYMBOL_GPL(pwmchip_add);

至此,对于 PWM 控制器的注册流程就介绍完成了。这部分PWM控制器的驱动主要是原厂工程师进行编写,这里只是简单分析其加载过程,如果需要更加深入了解其底层的话,请读者自行阅读相关代码。在实际开发中,我们的重点还是落在PWM设备的驱动开发上,只需要调用原厂提供的PWM控制器的一些API接口函数来操作PWM实现相应功能即可,下面将讲解具体的API接口函数,以及以sg90舵机为例,编写一个具体的PWM设备驱动

5.2 PWM具体设备驱动编写

PWM子系统API接口函数简单介绍

前面介绍了直接使用sysfs文件系统来操作pwm,这里讲解第二种方法,通过驱动层的pwm操作API接口函数对pwm进行操作,例如我们现在要写一个SG90舵机的驱动程序。

第一步先在自己的板级设备树中添加对应的设备节点,这里添加的设备节点直接引用前面配置好的PWM节点,并设置对应的compatible属性方便device与driver匹配

sg90 {
	compatible="sg90";
	pwms=<&pwm1_6ch_2 0 20000000 1>;
};

修改好设备树之后,就可以重新编译内核并烧录了,接下来就是对具体设备的驱动程序进行编写了,在编写之前,介绍几个比较常用的PWM相关的API接口函数,这些函数的实现都在kernel/drivers/pwm/core.c,对应的声明都在kernel-6.1/include/linux/pwm.h

1、devm_pwm_get 函数
该函数用来从设备树节点获取pwm_device,与pwm_get函数类似,但是不同的是,使用devm_pwm_get获取的pwm设备会在驱动程序分离时自动释放,而pwm_get不能释放

struct pwm_device *devm_pwm_get(struct device *dev, const char *con_id);

函数参数:

  • struct device *dev: 设备结构体。这个结构体主要是通过platform设备匹配成功之后probe函数的参数struct platform_device中的device成员
  • const char *con_id: 设备名字。
    返回值:
  • 成功时返回指向 PWM 设备结构体的指针。失败时返回错误指针(例如 ERR_PTR 宏)

2、pwm_config函数
该函数主要用来修改PWM的配置,包括占空比和周期。

int pwm_config(struct pwm_device *pwm, int duty_ns, int period_ns);

函数参数:

  • struct pwm_device *pwm: 指向 PWM 设备结构体的指针。
  • int duty_ns :占空比,单位是纳秒,表示高电平持续的时间。
  • int period_ns: 周期,单位是纳秒,表示 PWM 信号的总周期。
    返回值:
  • 成功返回0,失败返回负数

3、 pwm_set_polarity函数
该函数主要主要用来设置PWM信号的极性,可以是正极性也可以是负极性

 int pwm_set_polarity(struct pwm_device *pwm, enum pwm_polarity polarity);

函数参数:

  • struct pwm_device *pwm: 指向 PWM 设备结构体的指针。
  • enum pwm_polarity polarity: 极性参数,可以是 PWM_POLARITY_NORMAL(正极性)或PWM_POLARITY_INVERSED(负极性)。
    返回值:
  • 成功返回0,失败返回负数

4、pwm_enable函数
该函数主要用来使能PWM信号,开始输出PWM信号

 int pwm_enable(struct pwm_device *pwm);

函数参数:

  • struct pwm_device *pwm: 指向 PWM 设备结构体的指针。
    返回值:
  • 成功返回0,失败返回负数

5、pwm_disable函数
该函数主要用来使能PWM信号,开始输出PWM信号

 int pwm_disable(struct pwm_device *pwm);

函数参数:

  • struct pwm_device *pwm: 指向 PWM 设备结构体的指针。
    返回值:
  • 成功返回0,失败返回负数

还有其他一些列接口函数,这里不再一一例举,如果读者感兴趣可以查看kernel/drivers/pwm/core.c源码,需要注意的是,这些API接口函数,底层都会去调用PWM控制器的ops结构体成员中的操作函数,主要调用的都是pwm_apply_state函数,在这个函数中会调用ops操作函数集中的apply函数chip->ops->apply(chip, pwm, state);rockchip_pwm_apply函数中对state变量进行判断在调用对应的接口函数

SG90舵机驱动程序编写

最后编写基于platform总线驱动程序,这里就不再对platform总线进行阐述了,可以查看我之前的platform总线的博客,最后编写SG90舵机驱动程序如下

#include <linux/module.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/uaccess.h>
#include <linux/pwm.h>

dev_t dev_num;
struct cdev cdev_test;

struct class *class;
struct device *device;
struct pwm_device *sg90_pwm_device;

// 打开设备时的回调函数

static int cdev_test_open(struct inode *inode, struct file*file) {
	printk("This is cdev test open\n");
	pwm_config(sg90_pwm_device, 500000, 20000000) // 配置 参数:脉冲宽度为 5000000纳秒, 周期为 20000000纳秒
	pwm_set_polarity(sg90_pwm_device, PWM_POLARITY_NORMAL); // 设置PWM极性为正常极性
	pwm_enable(sg90_pwm_device); // 启动PWM
	return 0;
}

// 写入设备时的回调函数
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off) {
	int ret;
	unsigned char data[1];
	printk("This is cdev test write\n");
	// 从用户空间拷贝数据到内核空间
	ret = copy_from_user(data, buf, size);
	if (ret) {
		printk("copy_from_user failed\n");
		return -EFAULT;
	}
	// 更新 PWM 参数:脉冲宽度根据用户输入的数据调整
	pwm_config(sg90_pwm_device, 500000 + data[0] * 100000 / 9, 20000000);
	return size;
}
 
// 释放设备时的回调函数
static int cdev_test_release(struct inode *inode, struct file *file) {
	printk("This is cdev test release\n");
	pwm_config(sg90_pwm_device, 500000, 20000000); // 回到初始的 PWM 参数配置
	pwm_disable(sg90_pwm_device); // 停止 PWM
	return 0;
}

// 定义字符设备操作函数集合
static struct file_operations cdev_test_ops = {
	.owner = THIS_MODULE,
	.open = cdev_test_open,
	.write = cdev_test_write,
	.release = cdev_test_release,
};

// 设备探测函数
static int sg90_probe(struct platform_device *pdev) {
	int ret;
	// 获取 PWM 设备
	sg90_pwm_device = devm_pwm_get(&pdev->dev, NULL);
	if (IS_ERR(sg90_pwm_device)) {
		printk("Failed to get PWM device\n");
		return PTR_ERR(sg90_pwm_device);
	}
	// 申请设备号
	ret = alloc_chrdev_region(&dev_num, 0, 1, "alloc_name");
	if (ret < 0) {
		printk("alloc_chrdev_region is error\n");
		return ret;
	}
	
	printk("alloc_chrdev_region is ok\n");
	// 初始化字符设备
	cdev_init(&cdev_test, &cdev_test_ops);
	cdev_test.owner = THIS_MODULE;
	// 添加字符设备
	ret = cdev_add(&cdev_test, dev_num, 1);
	// 创建设备类
	class = class_create(THIS_MODULE, "test"); 
	// 创建设备
	device = device_create(class, NULL, dev_num, NULL, "sg90"); 
	printk("sg90_probe successful\n");
	return 0;
}

// 设备移除函数
static int sg90_remove(struct platform_device *pdev) {
	device_destroy(class, dev_num); // 删除设备
	class_destroy(class); // 删除设备类
	cdev_del(&cdev_test); // 删除字符设备
	unregister_chrdev_region(dev_num, 1); // 释放设备号
	printk("sg90_remove successful\n");
	return 0;
}

// 设备树匹配表
static const struct of_device_id sg90_of_device_id[] = {
	{.compatible = "sg90"},
	{},
};
MODULE_DEVICE_TABLE(of, sg90_of_device_id);

// 定义平台驱动
static struct platform_driver sg90_platform_driver = {
	.driver = {
		.name = "sg90",
		.of_match_table = sg90_of_device_id,
	},
	.probe = sg90_probe,
	.remove = sg90_remove,
};

// 模块初始化函数
static int __init modulecdev_init(void) {
	int ret;
	// 注册平台驱动
	ret = platform_driver_register(&sg90_platform_driver);
	if (ret) {
		printk("platform_driver_register is error\n");
		return ret;
	}
	printk("platform_driver_register is ok\n");
	return 0;
}

// 模块退出函数
static void __exit modulecdev_exit(void) {
	// 注销平台驱动
	platform_driver_unregister(&sg90_platform_driver);
	printk("bye bye\n");
}

// 声明模块许可证和作者
module_init(modulecdev_init);
module_exit(modulecdev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

最后可以编写一个简单的应用程序来测试该驱动程序,应用程序这里就不列出来了,在应用程序中调用write系统调用,写入对应的角度值,内陷到内核之后通过角度计算设置对应的pwm参数,就实现了简单的pwm外设使用了。

posted @ 2026-02-06 17:26  ttkwzyttk  阅读(5)  评论(0)    收藏  举报