1)实验平台:正点原子阿尔法Linux开发板
2)平台购买地址:https://item.taobao.com/item.htm?id=603672744434
2)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-300792-1-1.html
3)对正点原子Linux感兴趣的同学可以加群讨论:935446741
4)关注正点原子公众号,获取最新资料更新
第七十三章 Linux PWM驱动实验
在裸机篇我们已经学习过了如何使用I.MX6ULL的PWM外设来实现LCD的背光调节,其实在Linux的LCD驱动实验我们也提到过I.MX6ULL的PWM背光调节,但是并没有专门的去讲解PWM部分,本章我们就来学习一下Linux下的PWM驱动开发。
73.1 PWM驱动简析
关于PWM原理以及I.MX6ULL的PWM外设已经在裸机篇进行了详细的讲解,这里就不再赘述了,我们重点来看一下NXP原厂提供的Linux内核自带的PWM驱动。
73.1.1 设备树下的PWM控制器节点
I.MX6ULL有8路PWM输出,因此对应8个PWM控制器,所有在设备树下就有8个PWM控制器节点。这8路PWM都属于I.MX6ULL的AIPS-1域,但是在设备树imx6ull.dtsi中分为了两部分,PWM1PWM4在一起,PWM5PWM8在一起,这8路PWM并没有全部放到一起,这一点一定要注意,不要以为imx6ull.dtsi没有写完整。这8路PWM的设备树节点内容都是一样的,除了reg属性不同(毕竟不同的控制器,其地址范围不同)。本章实验我们使用GPIO1_IO04这个引脚来完成PWM实验,而GPIO1_IO04就是PWM3的输出引脚,所以这里我们就以PWM3为例进行讲解,imx6ull.dtsi文件中的pwm3节点信息如下:
示例代码73.1.1.1 pwm3节点内容
1 pwm3: pwm@02088000 {
2 compatible = "fsl,imx6ul-pwm", "fsl,imx27-pwm";
3 reg = <0x02088000 0x4000>;
4 interrupts = <GIC_SPI 85 IRQ_TYPE_LEVEL_HIGH>;
5 clocks = <&clks IMX6UL_CLK_PWM3>,
6 <&clks IMX6UL_CLK_PWM3>;
7 clock-names = "ipg", "per";
8 #pwm-cells = <2>;
9 };
第2行,compatible属性值有两个“fsl,imx6ul-pwm”和“fsl,imx27-pwm”,所以在整个Linux源码里面搜索这两个字符窜即可找到I.MX6ULL的PWM驱动文件,这个文件就是drivers/pwm/pwm-imx.c。
关于I.MX6ULL的PWM节点更为详细的信息请参考对应的绑定文档:Documentation/devicetree/bindings/pwm/ imx-pwm.txt,这里就不去分析了。
73.1.2 PWM子系统
Linux内核提供了个PWM子系统框架,编写PWM驱动的时候一定要符合这个框架。PWM子系统的核心是pwm_chip结构体,定义在文件include/linux/pwm.h中,定义如下:
示例代码73.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 struct pwm_device *pwms;
8 struct pwm_device * (*of_xlate)(struct pwm_chip *pc,
9 const struct of_phandle_args *args);
10 unsigned int of_pwm_n_cells;
11 bool can_sleep;
12 };
第4行,pwm_ops结构体就是PWM外设的各种操作函数集合,编写PWM外设驱动的时候需要开发人员实现。pwm_ops结构体也定义在pwm.h头文件中,定义如下:
示例代码73.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 struct module *owner;
17 };
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 成功;负数 失败。
73.1.3 PWM驱动源码分析
我们简单分析一下Linux内核自带的I.MX6ULL PWM驱动,驱动文件前面都说了,是pwm-imx.c这个文件。打开这个文件,可以看到,这是一个标准的平台设备驱动文件,如下所示:
示例代码73.1.3.1 I.MX6ULL PWM平台驱动
1 static const struct of_device_id imx_pwm_dt_ids[] = {
2 { .compatible = "fsl,imx1-pwm", .data = &imx_pwm_data_v1, },
3 { .compatible = "fsl,imx27-pwm", .data = &imx_pwm_data_v2, },
4 { /* sentinel */ }
5 };
6
7 ......
8
9 static struct platform_driver imx_pwm_driver = {
10 .driver = {
11 .name = "imx-pwm",
12 .of_match_table = imx_pwm_dt_ids,
13 },
14 .probe = imx_pwm_probe,
15 .remove = imx_pwm_remove,
16 };
17
18 module_platform_driver(imx_pwm_driver);
第3行,当设备树PWM节点的compatible属性值为“fsl,imx27-pwm”的话就会匹配此驱动,注意后面的.data为imx_pwm_data_v2,这是一个imx_pwm_data类型的结构体变量,内容如下:
示例代码73.1.3.2 imx_pwm_data_v2结构体变量
1 static struct imx_pwm_data imx_pwm_data_v2 = {
2 .config = imx_pwm_config_v2,
3 .set_enable = imx_pwm_set_enable_v2,
4 };
imx_pwm_config_v2函数就是最终操作I.MX6ULL的PWM外设寄存器,进行实际配置的函数。imx_pwm_set_enable_v2就是具体使能PWM的函数。
第14行,当设备树节点和驱动匹配以后imx_pwm_probe函数就会执行。
imx_pwm_probe函数如下(有缩减):
示例代码73.1.3.3 imx_pwm_probe函数
1 static int imx_pwm_probe(struct platform_device *pdev)
2 {
3 const struct of_device_id *of_id =
4 of_match_device(imx_pwm_dt_ids, &pdev->dev);
5 const struct imx_pwm_data *data;
6 struct imx_chip *imx;
7 struct resource *r;
8 int ret = 0;
9
10 if (!of_id)
11 return -ENODEV;
12
13 imx = devm_kzalloc(&pdev->dev, sizeof(*imx), GFP_KERNEL);
14 if (imx == NULL)
15 return -ENOMEM;
......
31 imx->chip.ops = &imx_pwm_ops;
32 imx->chip.dev = &pdev->dev;
33 imx->chip.base = -1;
34 imx->chip.npwm = 1;
35 imx->chip.can_sleep = true;
36
37 r = platform_get_resource(pdev, IORESOURCE_MEM, 0);
38 imx->mmio_base = devm_ioremap_resource(&pdev->dev, r);
39 if (IS_ERR(imx->mmio_base))
40 return PTR_ERR(imx->mmio_base);
41
42 data = of_id->data;
43 imx->config = data->config;
44 imx->set_enable = data->set_enable;
45
46 ret = pwmchip_add(&imx->chip);
47 if (ret < 0)
48 return ret;
49
50 platform_set_drvdata(pdev, imx);
51 return 0;
52 }
第13行,imx是一个imx_chip类型的结构体指针变量,这里为其申请内存。imx_chip结构体有个重要的成员变量chip,chip是pwm_chip类型的。所以这一行就引出了PWM子系统核心部件pwm_chip,稍后的重点就是初始化chip。
第31~35行,初始化imx的chip成员变量,也就是初始化pwm_chip!第31行设置pwm_chip的ops操作集为imx_pwm_ops,imx_pwm_ops定义如下:
示例代码73.1.3.4 imx_pwm_ops操作集合
1 static struct pwm_ops imx_pwm_ops = {
2 .enable = imx_pwm_enable,
3 .disable = imx_pwm_disable,
4 .config = imx_pwm_config,
5 .owner = THIS_MODULE,
6 };
imx_pwm_enable、imx_pwm_disable和imx_pwm_config这三个函数就是使能、关闭和配置PWM的函数。
继续回到示例代码73.1.3.3中的37和38行,从设备树中获取PWM节点中关于PWM控制器的地址信息,然后在进行内存映射,这样我们就得到了PWM控制器的基地址。
第43和44行,这两行设置imx的config和set_enable这两个成员变量为data->config和data->set_enable,也就是示例代码73.1.3.2中的imx_pwm_config_v2和imx_pwm_set_enable_v2这两个函数。imx_pwm_enable、imx_pwm_disable和imx_pwm_config这三个函数最终调用就是imx_pwm_config_v2和imx_pwm_set_enable_v2。
整个pwm-imx.c文件里面,最终和I.MX6ULL的PWM寄存器打交道的就是imx_pwm_config_v2和imx_pwm_set_enable_v2这两个函数,我们先来看一下imx_pwm_set_enable_v2函数,此函数用于打开或关闭对应的PWM,函数内容如下:
示例代码73.1.3.5 imx_pwm_set_enable_v2函数
1 static void imx_pwm_set_enable_v2(struct pwm_chip *chip, bool enable)
2 {
3 struct imx_chip *imx = to_imx_chip(chip);
4 u32 val;
5
6 val = readl(imx->mmio_base + MX3_PWMCR);
7
8 if (enable)
9 val |= MX3_PWMCR_EN;
10 else
11 val &= ~MX3_PWMCR_EN;
12
13 writel(val, imx->mmio_base + MX3_PWMCR);
14 }
第6行,读取PWMCR寄存器的值。
第9行,如果enable为真,表示使能PWM,将PWMCR寄存器的bit0置1即可,宏MX3_PWMCR_EN为(1<<0)。
第11行,如果enable不为真,表示关闭PWM,将PWMCR寄存器的bit0清0即可。
第13行,将新的val值写入到PWMCR寄存器中。
imx_pwm_config_v2函数用于设置PWM的频率和占空比,相关操作如下:
示例代码73.1.3.6 imx_pwm_config_v2函数
1 static int imx_pwm_config_v2(struct pwm_chip *chip,
2 struct pwm_device *pwm, int duty_ns, int period_ns)
3 {
4 struct imx_chip *imx = to_imx_chip(chip);
5 struct device *dev = chip->dev;
6 unsigned long long c;
7 unsigned long period_cycles, duty_cycles, prescale;
8 unsigned int period_ms;
9 bool enable = test_bit(PWMF_ENABLED, &pwm->flags);
10 int wait_count = 0, fifoav;
11 u32 cr, sr;
12
......
42
43 c = clk_get_rate(imx->clk_per);
44 c = c * period_ns;
45 do_div(c, 1000000000);
46 period_cycles = c;
47
48 prescale = period_cycles / 0x10000 + 1;
49
50 period_cycles /= prescale;
51 c = (unsigned long long)period_cycles * duty_ns;
52 do_div(c, period_ns);
53 duty_cycles = c;
54
55 /*
56 * according to imx pwm RM, the real period value should be
57 * PERIOD value in PWMPR plus 2.
58 */
59 if (period_cycles > 2)
60 period_cycles -= 2;
61 else
62 period_cycles = 0;
63
64 writel(duty_cycles, imx->mmio_base + MX3_PWMSAR);
65 writel(period_cycles, imx->mmio_base + MX3_PWMPR);
66
67 cr = MX3_PWMCR_PRESCALER(prescale) |
68 MX3_PWMCR_DOZEEN | MX3_PWMCR_WAITEN |
69 MX3_PWMCR_DBGEN | MX3_PWMCR_CLKSRC_IPG_HIGH;
70
71 if (enable)
72 cr |= MX3_PWMCR_EN;
73
74 writel(cr, imx->mmio_base + MX3_PWMCR);
75
76 return 0;
77 }
第43~62行,根据参数duty_ns和period_ns来计算出应该写入到寄存器里面的值duty_cycles和period_cycles。
第64行,将计算得到的duty_cycles写入到PWMSAR寄存器中,设置PWM的占空比
第65行,将计算得到的period_cycles写入到PWMPR寄存器中,设置PWM的频率。
至此,I.MX6ULL的PWM驱动我们就分析完了。
73.2 PWM驱动编写
73.2.1 修改设备树
PWM驱动就不需要我们再编写了,NXP已经写好了,前面我们也已经详细的分析过这个驱动源码了。我们在实际使用的时候只需要修改设备树即可,ALPHA开发板上的JP2排针引出了GPIO1_IO04这个引脚,如图73.2.1.1所示:
图73.2.1.1 GPIO1_IO4引脚
GPIO1_IO04可以作为PWM3的输出引脚,所以我们需要在设备树里面添加GPIO1_IO04的引脚信息以及PWM3控制器对应的节点信息。
1、添加GPIO1_IO04引脚信息
打开imx6ull-alientek-emmc.dts文件,在iomuxc节点下添加GPIO1_IO04的引脚信息,如下所示:
示例代码73.2.1.1 GPIO1_IO04引脚信息
1 pinctrl_pwm3: pwm3grp {
2 fsl,pins = <
3 MX6UL_PAD_GPIO1_IO04__PWM3_OUT 0x110b0
4 >;
5 };
2、向pwm3节点追加信息
前面已经讲过了,imx6ull.dtsi文件中已经有了“pwm3”节点,但是还不能直接使用,需要在imx6ull-alientek-emmc.dts文件中向pwm3节点追加一些内容,在imx6ull-alientek-emmc.dts文件中加入如下所示内容:
示例代码73.2.1.2 向pwm3添加的内容
1 &pwm3 {
2 pinctrl-names = "default";
3 pinctrl-0 = <&pinctrl_pwm3>;
4 clocks = <&clks IMX6UL_CLK_PWM3>,
5 <&clks IMX6UL_CLK_PWM3>;
6 status = "okay";
7 };
第3行,pinctrl-0属性指定PWM3所使用的输出引脚对应的pinctrl节点,这里设置为示例代码73.2.1中的pinctrl_pwm3。
第4和5行,设置时钟,第4行设置ipg时钟,第5行设置per时钟。有些pwm节点默认时钟源是IMX6UL_CLK_DUMMY,这里我们需要将其改为对应的时钟,比如这里设置为IMX6UL_CLK_PWM3。PWM1~PWM8分别对应IMX6UL_CLK_PWM1~ IMX6UL_CLK_PWM8。
3、屏蔽掉其他复用的IO
检查一下设备树中有没有其他外设用到GPIO1_IO04,如果有的话需要屏蔽掉!注意,不能只屏蔽掉GPIO1_IO04的pinctrl配置信息,也要搜索一下“gpio1 4”,看看有没有哪里用到,用到的话也要屏蔽掉。
设备树修改完成以后重新编译设备树,然后使用新的设备树启动系统。
73.2.2 使能PWM驱动
NXP官方的Linux内核已经默认使能了PWM驱动,所以不需要我们修改,但是为了学习,我们还是需要知道怎么使能。打开Linux内核配置界面,按照如下路径找到配置项:
-> Device Drivers
-> Pulse-Width Modulation (PWM) Support
-> <*> i.MX PWM support
配置如图73.2.2.1所示:
图73.2.2.1 PWM配置项
73.3 PWM驱动测试
使用新的设备树启动系统,然后将开发板JP2排针上的GPIO_4(GPIO1_IO04)引脚连接到示波器上,通过示波器来查看PWM波形图。我们可以直接在用户层来配置PWM,进入目录/sys/class/pwm中,如图73.3.1所示:
图73.3.1各路PWM
图73.3.1中pwmchip0pwmchip7对应I.MX6ULL的PWM1PWM8,所以我们需要用到pwmchip2。
1、调出pwmchip2的pwm0子目录
首先需要调出pwmchip2下的pwm0目录,否则后续就没法操作了,输入如下命令:
echo 0 > /sys/class/pwm/pwmchip2/export
执行完成会在pwmchip2目录下生成一个名为“pwm0”的子目录,如图73.3.2所示:
图73.3.2 新生成的pwm0子目录
2、使能PWM3
输入如下命令使能PWM3:
echo 1 > /sys/class/pwm/pwmchip2/pwm0/enable
3、设置PWM3的频率
注意,这里设置的是周期值,单位为ns,比如20KHz频率的周期就是50000ns,输入如下命令:
echo 50000 > /sys/class/pwm/pwmchip2/pwm0/period
4、设置PWM3的占空比
这里不能直接设置占空比,而是设置的一个周期的ON时间,也就是高电平时间,比如20KHz频率下20%占空比的ON时间就是10000,输入如下命令:
echo 10000 > /sys/class/pwm/pwmchip2/pwm0/duty_cycle
设置完成使用示波器查看波形是否正确,正确的话如图73.3.3所示:
图73.3.3 PWM波形图
从图73.3.3可以看出,此时PWM频率为20KHz,占空比为20%,与我们设置的一致。如果要修改频率或者占空比的话一定要注意这两者时间值,比如20KHz频率的周期值为50000ns,那么你在调整占空比的时候ON时间就不能设置大于50000,否则就会提示你参数无效。
73.4 PWM背光设置
有时候我们需要在某个外设上添加PWM功能,比如,LCD的背光控制就是PWM来完成的,本小节我们就以PWM背光控制为例,学习一下如何在其他外设上添加PWM功能。首先肯定是设备树描述,直接看linux内核里面关于backlight(背光)的绑定文档,路径为Documentation/devicetree/bindings/video/backlight/pwm-backlight.txt,此文档描述了如何创建backlight节点来使用linux内核自带的pwm背光驱动。
必要的属性如下:
compatible:内容必须为“pwm-backlight”,通过这个可以匹配到内核自带的PWM背光驱动,驱动文件为drivers/video/backlight/pwm_bl.c,这里就不去分析驱动源码了。
pwms:此属性指定背光使用哪一路PWM,以及PWM相关的属性。
brightness-levels:背光等级数组,范围0255,对应占空比为0%100%。数组内的值必须从0开始,也就是0%占空比,最后一个值必须是255,也就是100%占空比。数组中间值的个数以及值大小可以自行定义。
default-brightness-level:默认的背光等级,也就是brightness-levels属性中第几个值,注意这里是数索引编号,不是具体的数值!
power-supply:支持的电压,此属性可以不需要。
以正点原子ALPHA开发板为例,看一下PWM背光节点是如何设置的,打开imx6ull-alientek-emmc.dts,找到如下所示节点内容:
示例代码73.4.1.1 pwm背光节点
1 backlight {
2 compatible = "pwm-backlight";
3 pwms = <&pwm1 0 5000000>;
4 brightness-levels = <0 4 8 16 32 64 128 255>;
5 default-brightness-level = <7>;
6 status = "okay";
7 };
第2行,compatible属性必须为“pwm-backlight”。
第3行,pwms属性指定背光所使用的pwm通道,第一个参数指定使用pwm1,由于I.MX6ULL的PWM只有一个通道,因此这里为0。最后一个参数是PWM周期,单位为ns,这里PWM周期为5000000ns,频率为200Hz。
第4行,背光等级数组,一共8个等级,索引编号从0到7。
第5行,背光默认处于第7等级,也就是255,为100%占空比。
关于pwm做背光控制就讲解到这里,pwm做其他外设某个功能的时候要具体问题具体分析。