1)实验平台:正点原子ATK-DLRK3568开发板
2)平台购买地址:https://detail.tmall.com/item.htm?id=731866264428
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/docs/boards/xiaoxitongban
第二十三章 Linux PWM驱动实验
PWM是很常用到功能,我们可以通过PWM来控制电机速度,也可以使用PWM来控制LCD的背光亮度。本章我们就来学习一下如何在Linux下进行PWM驱动开发。
23.1 PWM驱动简析
PWM全称是Pulse Width Modulation,也就是脉冲宽度调制,PWM信号如图23.1.1所示:
图23.1.1 PWM信号
PWM信号有两个关键的术语:频率和占空比,频率就是开关速度,把一次开关算作一个周期,那么频率就是1秒内进行了多少次开关。占空比就是一个周期内高电平时间和低电平时间的比例,一个周期内高电平时间越长占空比就越大,反之占空比就越小。占空比用百分之表示,如果一个周期内全是低电平那么占空比就是0%,如果一个周期内全是高电平那么占空比就是100%。
我们给LCD的背光引脚输入一个PWM信号,这样就可以通过调整占空比的方式来调整LCD背光亮度了。提高占空比就会提高背光亮度,降低占空比就会降低背光亮度,重点就在于PWM信号的产生和占空比的控制。
23.1.1 设备树下的PWM控制器节点
1、PWM通道与引脚
RK3568有4个PWM模块,每个PWM模块有4个通道,因此一共有16路PWM:PWM0~PWM15,PWM模块与PWM通道以及对应的引脚关系如表23.1.1.1所示:
图23.1.1.1 RK3568 PWM通道与引脚
其中PWM3,PWM7,PWM11和PWM15可用用于IR。
2、PWM简介
RK3568有4个PWM模块,每个PWM模块的功能基本相同,这些PWM通道的特性如下:
①、每个PWM模块有4个通道。
②、支持捕获模式:
—测量输入波形高低电平的有效周期。
—输入波形极性变化的时候产生中断信号。
—32位高电平捕获寄存器。
—32位低电平捕获寄存器。
—32位当前值寄存器。
—捕获结果可以保存到FIFO中,FIFO深度为8,FIFO中的数据可以通过CPU或DMA读取。
③、支持连续以及单次模式:
— 32位的周期计数寄存器。
— 32位的占空比寄存器。
— 32位当前值寄存器。
— 输出PWM极性以及占空比可调。
— 可配置中央对齐或左对齐模式。
3、PWM设备节点
接下来看一下PWM的设备树,RK3568的PWM设备树绑定信息文档为:Documentation/devicetree/bindings/pwm/pwm-rockchip.txt,我们简单总结一下PWM节点信息。
①、必须的参数:
compatible:必须是“rockchip,-pwm”形式的,比如:rockchip,rk3288-pwm、rockchip,rk3568-pwm等。对于RK3568而言,有效的是rockchip,rk3288-pwm。
reg:PWM控制器物理寄存器基地址,比如对于PWM0来说,这个地址为0XFDD70000,这个可以在RK3568的数据手册上找到。
clocks:时钟源。
#pwm-cells:瑞芯微的芯片必须是2或者3,对于RK3568来说是3。
了解完PWM的绑定文档以后,我们来看一下RK3568实际的定时器节点,打开rk3568.dtsi,找到名为“pwm15”的设备节点,内容如下:
示例代码23.1.1.1 PWM15节点
1 pwm15: pwm@fe700030 {
2 compatible = "rockchip,rk3568-pwm", "rockchip,rk3328-pwm";
3 reg = <0x0 0xfe700030 0x0 0x10>;
4 interrupts = <GIC_SPI 85 IRQ_TYPE_LEVEL_HIGH>,
5 <GIC_SPI 89 IRQ_TYPE_LEVEL_HIGH>;
6 #pwm-cells = <3>;
7 pinctrl-names = "active";
8 pinctrl-0 = <&pwm15m0_pins>;
9 clocks = <&cru CLK_PWM3>, <&cru PCLK_PWM3>;
10 clock-names = "pwm", "pclk";
11 status = "disabled";
12};
RK3568的PWM节点的compatible属性为“rockchip,rk3568-pwm”和“”rockchip,rk3328-pwm,我们可以在linux内核源码中搜索这两个字符串就可以找到PWM驱动文件,这个文件为:drivers/pwm/pwm-rockchip.c。
23.1.2 PWM子系统
Linux内核提供了个PWM子系统框架,编写PWM驱动的时候一定要符合这个框架。PWM子系统的核心是pwm_chip结构体,定义在文件include/linux/pwm.h中,定义如下:
示例代码23.1.2.1 pwm_chip结构体
1 struct pwm_chip {
2 struct device *dev;
3 struct list_head list;
4 const struct pwm_ops *ops;
5 int base;
6 unsigned int npwm;
7
8 struct pwm_device *pwms;
9
10 struct pwm_device * (*of_xlate)(struct pwm_chip *pc,
11 const struct of_phandle_args *args);
12 unsigned int of_pwm_n_cells;
13 bool can_sleep;
14 };
第4行,pwm_ops结构体就是PWM外设的各种操作函数集合,编写PWM外设驱动的时候需要开发人员实现。pwm_ops结构体也定义在pwm.h头文件中,定义如下:
示例代码23.1.2.2 pwm_ops结构体
1 struct pwm_ops {
2 int (*request)(struct pwm_chip *chip, //请求PWM
3 struct pwm_device *pwm);
4 void (*free)(struct pwm_chip *chip, //释放PWM
5 struct pwm_device *pwm);
6 int (*config)(struct pwm_chip *chip, //配置PWM周期和占空比
7 struct pwm_device *pwm,
8 int duty_ns, int period_ns);
9 int (*set_polarity)(struct pwm_chip *chip, //设置PWM极性
10 struct pwm_device *pwm,
11 enum pwm_polarity polarity);
12 int (*enable)(struct pwm_chip *chip, //使能PWM
13 struct pwm_device *pwm);
14 void (*disable)(struct pwm_chip *chip, //关闭PWM
15 struct pwm_device *pwm);
16 #ifdef CONFIG_DEBUG_FS
17 void (*dbg_show)(struct pwm_chip *chip,
18 struct seq_file *s);
19 #endif
20 struct module *owner;
21 };
pwm_ops中的这些函数不一定全部实现,但是像config、enable和disable这些肯定是需要实现的,否则的话打开/关闭PWM,设置PWM的占空比这些就没操作了。
PWM子系统驱动的核心初始化pwm_chip结构体,然后向内核注册初始化完成以后的pwm_chip。这里就要用到pwmchip_add函数,此函数定义在drivers/pwm/core.c文件中,函数原型如下:
int pwmchip_add(struct pwm_chip *chip)
函数参数和返回值含义如下:
chip:要向内核注册的pwm_chip。
返回值:0 成功;负数 失败。
卸载PWM驱动的时候需要将前面注册的pwm_chip从内核移除掉,这里要用到pwmchip_remove函数,函数原型如下:
int pwmchip_remove(struct pwm_chip *chip)
函数参数和返回值含义如下:
chip:要移除的pwm_chip。
返回值:0 成功;负数 失败。
23.1.3 PWM驱动源码分析
我们简单分析一下Linux内核自带的RK3568 PWM驱动,驱动文件前面都说了,是pwm-rockchip.c这个文件。打开这个文件,可以看到,这是一个标准的平台设备驱动文件,有如下所示:
示例代码23.1.3.1 RK3568 PWM平台驱动
1 static const struct of_device_id rockchip_pwm_dt_ids[] = {
2 { .compatible = "rockchip,rk2928-pwm", .data = &pwm_data_v1},
3 { .compatible = "rockchip,rk3288-pwm", .data = &pwm_data_v2},
4 { .compatible = "rockchip,vop-pwm", .data = &pwm_data_vop},
5 { /* sentinel */ }
6 };
7 ......
8 static struct platform_driver rockchip_pwm_driver = {
9 .driver = {
10 .name = "rockchip-pwm",
11 .of_match_table = rockchip_pwm_dt_ids,
12 },
13 .probe = rockchip_pwm_probe,
14 .remove = rockchip_pwm_remove,
15 };
16 module_platform_driver(rockchip_pwm_driver);
第2~4行,当设备树PWM节点的compatible属性值为“rockchip,rk2928-pwm”、“rockchip,rk3288-pwm”和“rockchip,vop-pwm”中的某一个的话就会匹配此驱动。
第13行,当设备树节点和驱动匹配以后rockchip_pwm_probe函数就会执行。
在看rockchip_pwm_probe函数之前先来看下rockchip_pwm_chip stm32_pwm结构体,这个结构体是瑞芯微官方创建的针对瑞芯微芯片的PWM结构体,这个结构体会贯穿整个PWM驱动,起到灵魂的作用。rockchip_pwm_chip结构体内容如下:
示例代码23.1.3.2 rockchip_pwm_chip结构体
1 struct rockchip_pwm_chip {
2 struct pwm_chip chip;
3 struct clk *clk;
4 const struct rockchip_pwm_data *data;
5 void __iomem *base;
6 };
重点看一下第2行,这是一个pwm_chip结构体成员变量chip,前面说了,PWM子系统的核心就是pwm_chip。
rockchip_pwm_probe函数如下(有缩减):
示例代码23.1.3.3 rockchip_pwm_probe函数
1 static int rockchip_pwm_probe(struct platform_device *pdev)
2 {
3 const struct of_device_id *id;
4 struct rockchip_pwm_chip *pc;
5 struct resource *r;
6 int ret;
7
8 id = of_match_device(rockchip_pwm_dt_ids, &pdev->dev);
9 if (!id)
10 return -EINVAL;
11
12 pc = devm_kzalloc(&pdev->dev, sizeof(*pc), GFP_KERNEL);
13 if (!pc)
14 return -ENOMEM;
15
16 r = platform_get_resource(pdev, IORESOURCE_MEM, 0);
17 pc->base = devm_ioremap_resource(&pdev->dev, r);
18 if (IS_ERR(pc->base))
19 return PTR_ERR(pc->base);
20
21 pc->clk = devm_clk_get(&pdev->dev, NULL);
22 if (IS_ERR(pc->clk))
23 return PTR_ERR(pc->clk);
24
25 ret = clk_prepare(pc->clk);
26 if (ret)
27 return ret;
28
29 platform_set_drvdata(pdev, pc);
30
31 pc->data = id->data;
32 pc->chip.dev = &pdev->dev;
33 pc->chip.ops = pc->data->ops;
34 pc->chip.base = -1;
35 pc->chip.npwm = 1;
36
37 if (pc->data->ops->set_polarity) {
38 pc->chip.of_xlate = of_pwm_xlate_with_flags;
39 pc->chip.of_pwm_n_cells = 3;
40 }
41
42 ret = pwmchip_add(&pc->chip);
43 if (ret < 0) {
44 clk_unprepare(pc->clk);
45 dev_err(&pdev->dev, "pwmchip_add() failed: %d\n", ret);
46 }
47
48 return ret;
49 }
第12行,pc是一个rockchip_pwm_chip类型的结构体指针变量,这里为其申请内存。rockchip_pwm_chip结构体有个重要的成员变量chip,chip是pwm_chip类型的。所以这一行就引出了PWM子系统核心部件pwm_chip,稍后的重点就是初始化chip。
第32~35行,初始化pc的chip成员变量,也就是初始化pwm_chip!第33行设置pwm_chip的ops操作集为pc->data->ops。根据示例代码23.1.3.1中的compatible属性可以,RK3568对应的data是pwm_data_v2,因此pwm_data_v2源码如下:
示例代码23.1.3.4 pwm_data_v2源码
1 static const struct rockchip_pwm_data pwm_data_v2 = {
2 .regs = {
3 .duty = 0x08,
4 .period = 0x04,
5 .cntr = 0x00,
6 .ctrl = 0x0c,
7 },
8 .prescaler = 1,
9 .ops = &rockchip_pwm_ops_v2,
10 .set_enable = rockchip_pwm_set_enable_v2,
11 };
第9行,rockchip_pwm_ops_v2就是pwm_chip的ops函数,也就是RK3568的ops函数。
继续回到示例代码23.1.3.3中,第49行使用pwmchip_add函数向内核添加pwm_chip,这个就是rockchip_pwm_probe函数的主要工作。
我们重点来看一下rockchip_pwm_ops_v2,定义如下:
示例代码23.1.3.5 rockchip_pwm_ops_v2操作集合
1 static const struct pwm_ops rockchip_pwm_ops_v2 = {
2 .config = rockchip_pwm_config,
3 .set_polarity = rockchip_pwm_set_polarity,
4 .enable = rockchip_pwm_enable,
5 .disable = rockchip_pwm_disable,
6 .owner = THIS_MODULE,
7 };
第2行rockchip_pwm_config就是最终的PWM设置函数,我们在应用中设置的PWM频率和占空比最终就是由rockchip_pwm_config函数来完成的,此函数会最终操作RK3568相关的寄存器。
rockchip_pwm_config函数源码如下:
示例代码23.1.3.6 rockchip_pwm_config函数
1 static int rockchip_pwm_config(struct pwm_chip *chip,
2 struct pwm_device *pwm,int duty_ns, int period_ns)
3 {
4 struct rockchip_pwm_chip *pc = to_rockchip_pwm_chip(chip);
5 unsigned long period, duty;
6 u64 clk_rate, div;
7 int ret;
8
9 clk_rate = clk_get_rate(pc->clk);
10
11 /*
12 * Since period and duty cycle registers have a width of 32
13 * bits, every possible input period can be obtained using the
14 * default prescaler value for all practical clock rate values.
15 */
16 div = clk_rate * period_ns;
17 do_div(div, pc->data->prescaler * NSEC_PER_SEC);
18 period = div;
19
20 div = clk_rate * duty_ns;
21 do_div(div, pc->data->prescaler * NSEC_PER_SEC);
22 duty = div;
23
24 ret = clk_enable(pc->clk);
25 if (ret)
26 return ret;
27
28 writel(period, pc->base + pc->data->regs.period);
29 writel(duty, pc->base + pc->data->regs.duty);
30 writel(0, pc->base + pc->data->regs.cntr);
31
32 clk_disable(pc->clk);
33
34 return 0;
35 }
第16~18行,根据设置的频率,计算出RK3568 PWM的PERIOD寄存器。
第20~22行,根据设置的占空比,计算出DUTY寄存器的值。
第28~30行,设置PWM外设的PERIOD、DUTY和CNT寄存器。
至此,RK3568的PWM驱动基本分析到这里。
23.2 PWM驱动编写
23.2.1 修改设备树
PWM驱动就不需要我们再编写了,瑞芯微已经写好了,前面我们也已经详细的分析过这个驱动源码了。我们在实际使用的时候只需要修改设备树即可,ATK-DLRK3568开发板上的JP11排针引出了GPIO3_C5这个引脚,这个引脚可以用作PWM15_IR_M0,如图23.2.1.1所示:
图23.2.1.1 GPIO3_C5引脚
GPIO3_C5可以作为PWM3的通道3的PWM输出引脚,也就是PWM15_IR_M0,所以我们需要在设备树里面添加GPIO3_C5的引脚信息以及PWM15外设信息。
1、添加GPIO3_C5引脚信息
PWM的引脚配置瑞芯微已经帮我们做好了,打开rk3568-pinctrl.dtsi文件,找到如下所示内容:
示例代码23.2.1.1 TIM1 PWM引脚信息
1 pwm15 {
2 /omit-if-no-ref/
3 pwm15m0_pins: pwm15m0-pins {
4 rockchip,pins =
5 /* pwm15_irm0 */
6 <3 RK_PC5 1 &pcfg_pull_down>;
7 };
8
9 /omit-if-no-ref/
10 pwm15m1_pins: pwm15m1-pins {
11 rockchip,pins =
12 /* pwm15_irm1 */
13 <4 RK_PC3 1 &pcfg_pull_none>;
14 };
15};
可以看出瑞芯微官方对GPIO3_C5写了两个配置,第6行是把GPIO3_C5配置为PWM15_IR_M0引脚,但是默认没有上下拉!大家根据实际情况修改为上下拉配置即可。本章实验,我们选择pcfg_pull_down,也就是将GPIO3_C5配置为PWM15_IR_M0,默认下拉。
2、向pwm15节点追加信息
前面已经讲过了,rk3568.dtsi文件中已经有了“pwm15”节点,但是这个节点默认是disable的,还不能直接使用,所以需要在rk3568-atk-evb1-ddr4-v10.dtsi文件中向打开pwm15节点。
示例代码23.2.1.3 向timers1添加的内容
1&pwm15 {
2 status = "okay";
3}
第2行,status改为okay,也就是使能PWM15。
23.2.2 使能PWM驱动
瑞芯微官方的Linux内核已经默认使能了PWM驱动,所以不需要我们修改,但是为了学习,我们还是需要知道怎么使能。打开Linux内核配置界面,按照如下路径找到配置项:
-> Device Drivers │
-> Pulse-Width Modulation (PWM) Support (PWM [=y])
-> <*> Rockchip PWM support //选中
配置如图23.2.2.1所示:
图23.2.2.1 PWM配置项
23.3 PWM驱动测试
1、确定PWM15对应的pwmchipX文件
使用新的设备树启动系统,然后将开发板上的GPIO3_C5引脚连接到示波器上,通过示波器来查看PWM波形图。我们可以直接在用户层来配置PWM,进入目录/sys/class/pwm中,如图23.3.1所示:
图23.3.1 当前系统下的PWM外设
注意!图23.3.1中有个pwmchip0~ pwmchip3,但是我们并不知道哪个是PWM15的。我们可以通过查看pwmchip0~pwmchip3的外设基地址哪个和PWM15一样,那么哪个就是PWM15的。
ls -l
进入到pwm执行上面的ls -l指令,以后会打印出其链接的路径,如图23.3.2所示:
图23.3.2 pwmchip1路径名称
从图23.3.2可以看出pwmchip3对应的定时器寄存器起始地址为0XFE700030,根据示例代码23.1.1.1中的pwm15节点,可以知道PWM15这个定时器的寄存器起始地址就是0XFE700030。因此,pwmchip1就是PWM15对应的文件。
为什么要用这么复杂的方式来确定定时器对应的pwmchip文件呢?因为当RK3568开启多个PWM以后,其pwmchip文件就会变!
2、调出pwmchip15的pwm0子目录
输入如下命令打开pwmchip15的pwm0子目录
echo 0 > /sys/class/pwm/pwmchip3/export
执行完成会在pwmchip3目录下生成一个名为“pwm0”的子目录,如图23.3.3所示:
图23.3.3 新生成的pwm0子目录
2、设置PWM的频率
注意,这里设置的是周期值,单位为ns,比如20KHz频率的周期就是50000ns,输入如下命令:
echo 50000 > /sys/class/pwm/pwmchip3/pwm0/period
3、设置PWM的占空比
这里不能直接设置占空比,而是设置的一个周期的ON时间,也就是高电平时间,比如20KHz频率下20%占空比的ON时间就是10000,输入如下命令:
echo 10000 > /sys/class/pwm/pwmchip3/pwm0/duty_cycle
4、设置PWM极性
设置一下PWM波形的极性,输入如下命令:
echo normal > /sys/class/pwm/pwmchip3/pwm0/polarity
极性设置为normal,也就是duty_cycle为高电平时间。如果要将极性反过来,可以设置为inversed。
5、使能PWM
一定要先设置频率和波特率,最后在开启PWM,否则会提示参数错误!输入如下命令使能PWM:
echo 1 > /sys/class/pwm/pwmchip3/pwm0/enable
设置完成使用示波器查看波形是否正确,正确的话如图23.3.4所示:
图23.3.4 PWM波形图
从图23.3.4可以看出,此时PWM频率为20KHz,占空比为20%,与我们设置的一致。如果要修改频率或者占空比的话一定要注意这两者时间值,比如20KHz频率的周期值为50000ns,那么你在调整占空比的时候ON时间就不能设置大于50000,否则就会提示你参数无效。