6. PWM子系统–pwm波形输出实验

PWM子系统用于管理PWM波的输出,与我们之前学习的其他子系统类似,PWM具体实现代码由芯片厂商提供并默认编译进内核, 而我们可以使用内核(pwm子系统)提供的一些接口函数来实现具体的功能,例如使用PWM波控制显示屏的背光、控制无源蜂鸣器等等。

pwm子系统功能单一,很少单独使用。PWM子系统的使用也很简单,我们这 章通过一个极简的PWM子系统驱动来简单认识一下PWM子系统。其中讲解的一些接口函数后 面的复杂驱动可能会用到。

本章配套源码和设备树插件位于 ~/linux_driver/pwm_sub_system 目录下。

6.1. PWM子系统简介

在i.mx6中pwm子系统使用的是PWM外设,共有8个。 有关PWM外设的介绍可以参考imx6ull参考手册的Chapter 38 Pulse Width Modulation(PWM)章节, 这里不再介绍。使用了PWM子系统后和具体硬件相关的内容芯片厂商已经写好了, 我们唯一要做的就是在设备树(或者是设备树插件)中声明使用的引脚。

6.1.1. PWM设备结构体

在驱动中使用pwm_device结构体代表一个PWM设备。结构体原型如下所示:

pwm_device结构体
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
struct pwm_device {
    const char              *label;
    unsigned long           flags;
    unsigned int            hwpwm;
    unsigned int            pwm;
    struct pwm_chip         *chip;
    void                    *chip_data;

    unsigned int            period;         ---------①
    unsigned int            duty_cycle;     ---------②
    enum pwm_polarity       polarity;   ---------③
};

pwm_device结构体中几个重要的参数介绍如下,

  • period:设置PWM的周期,这里的单位是纳秒(ns)。 例如我们要输出一个1MHz的PWM波,那么period需要设置为1000。

  • duty_cycle :设置占空比,如果是正常的输出极性, 这个参数指定PWM波一个周期内高电平持续时间,单位还是ns,很明显duty_cycle不能大于period。 如果设置非输出反相,则该参数用于指定一个周期内低电平持续时间。

  • polarity :参数用于指定输出极性,即PWM输出是否反相。它是一个枚举类型,如下所示。

pwm_polarity枚举类型
1
2
3
4
enum pwm_polarity {
    PWM_POLARITY_NORMAL,
    PWM_POLARITY_INVERSED,
};
  • PWM_POLARITY_NORMAL :表示正常模式,不反相。

  • PWM_POLARITY_INVERSED :表示输出反相。

6.1.2. pwm的申请和释放函数

PWM使用之前要申请,不用时及时释放。 申请和释放函数很多,共分为四组,介绍如下:

第一组pwm的申请和释放函数
1
2
struct pwm_device *pwm_request(int pwm, const char *label);
void pwm_free(struct pwm_device *pwm);

这是旧的系统使用的pwm申请和释放函数,现在已经弃用,看到之后认识即可。这里不做介绍。

第二组pwm的申请和释放函数
1
2
struct pwm_device *pwm_get(struct device *dev, const char *con_id)
void pwm_put(struct pwm_device *pwm)
  • pwm_get :PWM申请函数

  • pwm_put :PWM释放函数

  • 参数dev :从哪个设备获取PWM,内核会在dev设备的设备树节点中根据参数“con_id”查找, 判断依据是con_id与设备树节点的”pwm-names”相同。

  • 参数con_id :如果设备中只用了一个PWM则可以将**参数con_id**设置为NULL,并且在设备树节点中不用设置“pwm-names”属性。

  • 返回值 :获取成功后返回得到的pwm。失败返回NULL。

  • 在不使用pwm设备时使用pwm_put释放pwm。参数为pwm_get得到的pwm_device结构体类型指针。

第三组pwm的申请和释放函数
1
2
struct pwm_device *devm_pwm_get(struct device *dev, const char *con_id)
void devm_pwm_put(struct device *dev, struct pwm_device *pwm)

这一组函数是对上一组函数的封装,使用方法和第二组相同,优点是当驱动移除时自动注销申请的pwm。

第四组pwm的申请和释放函数
1
2
3
4
/*---------------第四组---------------*/
struct pwm_device *of_pwm_get(struct device_node *np, const char *con_id)
struct pwm_device *devm_of_pwm_get(struct device *dev, struct device_node *np,
                               const char *con_id)
  • of_pwm_get 函数:从指定的设备树节点获取PWM。

  • 参数np 指定从哪个设备节点获取PWM。

  • 参数con_id 作用和前几组函数一样。

  • 返回值 是获取得到的PWM,失败则返回NULL。

函数devm_of_pwm_get是对of_pwm_get函数的封装,区别是它有三个参数, 参数dev指定那个设备要获取PWM ,其他两个与of_pwm_get函数相同, 它的优点是在驱动移除之前自动注销申请的pwm。

6.1.3. pwm配置函数和使能/停用函数

申请成功后只需使用函数配置pwm的频率和占空比然后使能输出即可在设定的引脚上输出PWM波。函数很简单,如下所示。

pwm配置函数和启动/停用函数
1
2
3
4
int pwm_config(struct pwm_device *pwm, int duty_ns, int period_ns)
int pwm_set_polarity(struct pwm_device *pwm, enum pwm_polarity polarity)
int pwm_enable(struct pwm_device *pwm)
void pwm_disable(struct pwm_device *pwm)

函数 pwm_config 用于配置PWM的频率和占空比, 需要注意的是这里是通过设置PWM一个周期的时间和高电平时间来设置PWM的频率和占空比,单位都是ns。 函数int pwm_set_polarity (struct pwm_device *pwm, enum pwm_polarity polarity)用于设置PWM极性, 需要注意的是如果这里设置PWM为负极性则函数pwm_config中的参数duty_ns设置的是一个周期内低电平时间。

使用 pwm_enablepwm_disable 函数使能和停用pwm。

6.2. pwm输出实验

由于PWM子系统很少单独使用,这里仅仅用一个极简的示例驱动程序介绍PWM子系统的使用。 我们把RGB灯的红灯引脚复用为PWM3的输出,在驱动程序中通过设置占空比调整红灯亮度, 同样也可以使用示波器观察、验证输出是否正确。

示例程序主要包含两部分内容,第一,添加相应的设备树节点(这里使用设备树插件)。第二,编写测试驱动程序。

6.2.1. 添加pwm相关设备树插件

首先简单介绍一下设备树中的PWM相关内容。打开“imx6ull.dtsi”文件,直接搜索“pwm”在文件中找到如下内容。

pwm节点
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
pwm1: pwm@2080000 {
    compatible = "fsl,imx6ul-pwm", "fsl,imx27-pwm";
    reg = <0x2080000 0x4000>;
    interrupts = <GIC_SPI 83 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clks IMX6UL_CLK_PWM1>,
             <&clks IMX6UL_CLK_PWM1>;
    clock-names = "ipg", "per";
    # = <2>;

pwm2: pwm@2084000 {
    compatible = "fsl,imx6ul-pwm", "fsl,imx27-pwm";
    reg = <0x2084000 0x4000>;
    interrupts = <GIC_SPI 84 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clks IMX6UL_CLK_DUMMY>,
             <&clks IMX6UL_CLK_DUMMY>;
    clock-names = "ipg", "per";
    #pwm-cells = <2>;
};

这里就是PWM驱动对应的设备树节点,这是pwm子系统的控制节点, 可以看到它设置了imx6ull芯片pwm外设的时钟、中断、寄存器地址等等。这样的节点共有8个分别对应pwm1~pwm8。 简单了解即可,我们不会去修改它。

使用pwm 只需要在设备树节点中添加两条属性信息,如下所示

pwm属性信息
1
2
pwms = <“&PWMn id period_ns>;
pwm-names = "name";
  • pwms :属性是必须的,它共有三个属性值

    &PWMn 指定使用哪个pwm,在imx6ull.dtsi文件中定义,总共有8个可选;

    id :pwm的id通常设置为0。

    period_ns :用于设置周期。单位是ns。

  • pwm-names :定义pwm设备名字。

本实验只使用了一个gpio 设备树插件源码如下所示。

设备树插件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 / {
     fragment@0 {
          target-path = "/";
         __overlay__ {
                    /*----------------第一部分-------------*/
            red_led_pwm {
                    compatible = "red_led_pwm";
                    pinctrl-names = "default";
                    pinctrl-0 = <&red_led_pwm>;

                    front {
                                    pwm-names = "red_led_pwm3"
                            pwms = <&pwm3 0 50000>;
                    };
            };
         };
     };

     fragment@1 {
         target = <&iomuxc>;
         __overlay__ {
                     /*----------------第二部分-------------*/
            red_led_pwm: ledsgrp {
                    fsl,pins = <
                            MX6UL_PAD_GPIO1_IO04__PWM3_OUT 0x1b0b0
                    >;
                    };
         };
     };
 };
  • 第6-15行,添加的red_led_pwm节点,red_led_pwm节点包含一个“front”子节点, red_led_pwm节点包含一个“front”子节点,子节点内定义了pwm属性信息, 这里我们使用PWM3,频率设置为20KHz(周期为50000ns,计算得到频率为20KHz)

  • 第23-27行,将红灯所在引脚复用为PWM3的输出引脚。

注意,如果之前做过RGB灯的其他实验,要检查下RGB红灯引脚是否被重复使用。

6.2.2. 驱动程序实现

驱动程序具体代码如下:

注册平台设备
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
 static const struct of_device_id of_pwm_leds_match[] = {
    {.compatible = "red_led_pwm"},
    {},
};

static struct platform_driver led_pwm_driver = {
    .probe          = led_pwm_probe_new,
    .remove         = led_pwm_remove,
    .driver         = {
            .name   = "led_pwm",
            .of_match_table = of_pwm_leds_match,
    },
};

/*
*驱动初始化函数
*/
static int __init pwm_leds_platform_driver_init(void)
{
    int DriverState;
    DriverState = platform_driver_register(&led_pwm_driver);
    return 0;
}

/*
*驱动注销函数
*/
static void __exit pwm_leds_platform_driver_exit(void)
{
    printk(KERN_ERR " pwm_leds_exit\n");
    /*注销平台设备*/
    platform_driver_unregister(&led_pwm_driver);
}

module_init(pwm_leds_platform_driver_init);
module_exit(pwm_leds_platform_driver_exit);

MODULE_LICENSE("GPL");
  • 第1-4行,设置设备树节点的匹配信息。

  • 第6-13行,填充platform_driver结构体。

  • 第18-23行,采用了注册平台设备的方式来注册我们的驱动程序。

  • 第28-33行,注销平台设备驱动。

平台设备与设备节点匹配成功后我们就可以很容易从设备树中获取信息, 而不必使用of函数直接从设备树节点中获取,当然获取设备树节点的方法有很多种。

我们在.prob函数中申请、设置、使能PWM,具体代码如下:

prob函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
static int led_pwm_probe(struct platform_device *pdev)
{
    int ret = 0;
    struct device_node *child; // 保存子节点
    struct device *dev = &pdev->dev;
    printk("match success \n");

    /*--------------第一部分-----------------*/
    child = of_get_next_child(dev->of_node, NULL);
    if (child)
    {
            /*--------------第二部分-----------------*/
            red_led_pwm = devm_of_pwm_get(dev, child, NULL);
            if (IS_ERR(red_led_pwm))
            {
                    printk(KERN_ERR" red_led_pwm,get pwm  error!!\n");
                    return -1;
            }
    }
    else
    {
            printk(KERN_ERR" red_led_pwm of_get_next_child  error!!\n");
            return -1;
    }



    /*--------------第三部分-----------------*/
    pwm_config(red_led_pwm, 1000, 5000);
    pwm_set_polarity(red_led_pwm, PWM_POLARITY_INVERSED);
    pwm_enable(red_led_pwm);

    return ret;
}

static int led_pwm_remove(struct platform_device *pdev)
{
    pwm_config(red_led_pwm, 0, 5000);
    pwm_free(red_led_pwm);
    return 0;
}
  • 第9行,获取子节点,在设备树插件中,我们把PWM相关信息保存在red_led_pwm的子节点中, 所以这里首先获取子节点。

  • 第13行,在子节点获取成功后我们使用devm_of_pwm_get函数获取pwm, 由于节点内只有一个PWM 这里将最后一个参数直接设置为NULL,这样它将获取第一个PWM。

  • 第29-31行,依次调用pwm_config、pwm_set_polarity、pwm_enable函数配置PWM、设置输出极性、 使能PWM输出,需要注意的是这里设置的极性为负极性, 这样pwm_config函数第二个参数设置的就pwm波的一个周期内低电平事件, 数值越大RGB红灯越亮。

6.2.3. 实验准备

在板卡上的部分GPIO可能会被系统占用,在使用前请根据需要修改 /boot/uEnv.txt 文件, 可注释掉某些设备树插件的加载,重启系统,释放相应的GPIO引脚。

如本节实验中,可能在鲁班猫系统中默认使能了 LED 的设备功能。 引脚被占用后,设备树可能无法再加载或驱动中无法再申请对应的资源。

方法参考如下:

broken

取消 KEY 设备树插件,以释放系统对应KEY资源,操作如下:

broken
dtoverlay=/usr/lib/linux-image-4.19.35-imx6/overlays/imx-fire-pwm-sub-system.dtbo

如若运行代码时出现“Device or resource busy”或者运行代码卡死等等现象, 请按上述情况检查并按上述步骤操作。

如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。

6.2.3.1. 编译设备树插件

linux_driver/pwm_sub_system/imx-fire-pwm-sub-system-overlay.dts 拷贝到 内核源码/arch/arm/boot/dts/overlays 目录下, 并修改同级目录下的Makefile,追加 imx-fire-pwm-sub-system.dtbo 编译选项。然后执行如下命令编译设备树插件:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- npi_v7_defconfig

make ARCH=arm -j4 CROSS_COMPILE=arm-linux-gnueabihf- dtbs

编译成功后生成同名的设备树插件文件(imx-fire-pwm-sub-system.dtbo)位于 内核源码/arch/arm/boot/dts/overlays 目录下。

6.2.3.2. 编译驱动程序

linux_driver/pwm_sub_system 拷贝到内核源码同级目录,执行里面的MakeFile,生成pwm_sub_system.ko。

5|

6.2.4. 下载验证

将编译好的驱动、设备树插件并拷贝到开发板,这里就不再赘述这一部分内容了,前面的章节中都有详细介绍。

在加载模块之前,先查看 /boot/uEnv.txt 文件是否加载了板子上原有的与rgb_led相关设备树插件。 如果之前开启了rgb_led相关的设备树插件,记得先屏蔽掉。并添加pwm子系统的设备树插件。

1

使用如下命令,可以查看系统当前的PWM状态:

cat /sys/kernel/debug/pwm
1

正常情况下可以看到RGB红灯亮度较低,使用示波器也可以看到设定的PWM波(如果不更改例程配置,pwm频率为200kHz,占空比20%)。