6. PWM子系统

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

pwm子系统功能单一,很少单独使用,我们这章通过一个简单的PWM子系统驱动来简单认识一下PWM子系统,并测试led灯光实验(或者pwm波形输出实验)。

6.1. PWM子系统简介

PWM(Pulse width modulation),脉冲宽度调制。在内核中PWM驱动较简单,但是麻雀虽小,五脏俱全,基本的框架是完整的。pwm框架简单参考下:

broken

内核的PWM core,向下对实际pwm控制器驱动,提供了pwm_chip,soc厂商编程控制器驱动,只需注册结构体,配置好private_data,实例化pwm_ops操作,编写具体函数即可。 向上为其他驱动调用提供了统一的接口,通过pwm_device,关联pwm_chip,其他驱动或者用户程序通过接口来操作pwm_device结构体。

pwm控制器驱动soc厂商已经写好,我们要做的是在设备树(或者是设备树插件)中开启控制器节点, 描述pwm设备节点,然后驱动中调用内核PWM提供的接口,来实现pwm驱动控制。完整程序参考配套源码“linux_driver/pwm_sub_system”目录下文件。

6.1.1. PWM设备结构体

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

pwm相关结构体(内核源码/include/linux/pwm.h)
 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
struct pwm_device {
    const char *label;
    unsigned long flags;
    unsigned int hwpwm;
    unsigned int pwm;
    struct pwm_chip *chip;
    void *chip_data;

    struct pwm_args args;
    struct pwm_state state;
};

struct pwm_args {
    u64 period;
    enum pwm_polarity polarity;
};

struct pwm_state {
    u64 period;
    u64 duty_cycle;
    enum pwm_polarity polarity;
    enum pwm_output_type output_type;
    struct pwm_output_pattern *output_pattern;
#ifdef CONFIG_PWM_ROCKCHIP_ONESHOT
    u64 oneshot_count;
#endif /* CONFIG_PWM_ROCKCHIP_ONESHOT */
    bool enabled;
};

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

  • label: pwm设备名称

  • flags:相关标志位

  • pwm :全局的pwm设备索引

  • chip : 一个pwm控制器的抽象

  • state :当前PWM通道的状态

  • 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子系统的使用。下面是以lubancat2板卡为例, 使用了心跳灯引脚GPIO0_C7作为PWM输出(也可以使用其他引出IO),改变驱动中周期和占空比的设置可看到不同的灯光亮度,也可以使用示波器查看波形。 lubancat系列板卡类似,需要确认具体可用的pwm输出引脚。

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

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

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

pwm0节点
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    pwm0: pwm@fdd70000 {
            compatible = "rockchip,rk3568-pwm", "rockchip,rk3328-pwm";
            reg = <0x0 0xfdd70000 0x0 0x10>;
            #pwm-cells = <3>;
            pinctrl-names = "active";
            pinctrl-0 = <&pwm0m0_pins>;
            clocks = <&pmucru CLK_PWM0>, <&pmucru PCLK_PWM0>;
            clock-names = "pwm", "pclk";
            status = "disabled";
    };
rk3568-pinctrl.dtsi
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    pwm0 {
            pwm0m0_pins: pwm0m0-pins {
                    rockchip,pins =
                            /* pwm0_m0 */
                            <0 RK_PB7 1 &pcfg_pull_none>;
            };

            pwm0m1_pins: pwm0m1-pins {
                    rockchip,pins =
                            /* pwm0_m1 */
                            <0 RK_PC7 2 &pcfg_pull_none>;
            };
    };

这里就是PWM驱动对应的设备树节点,这是pwm子系统的控制节点,下面的“rk3568-pinctrl.dtsi”中,描述了pwm0m0_pins。 这次我们要使用的是pwm0m1_pins,因此需要在设备树创建中修改。

使用pwm时,只需要引用该设备树节点,并添加一些属性信息,如下所示:

pwm属性信息
1
2
pwms = <&PWMn id period_ns PWM_POLARITY_INVERTED>;
pwm-names = "name";
  • pwms :属性是必须的

  • &PWMn :指定使用哪个pwm,rk3568.dtsi文件中有定义,在引用时可以起别名如pwm1、pwm2等

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

  • PWM_POLARITY_INVERTED :是可选的,,表示pwm的极性,为0正常极性或者1反转极性。

  • period_ns :用于设置pwm信号周期,单位是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
/*
* Copyright (C) 2022 - All Rights Reserved by
* EmbedFire LubanCat
*/
/dts-v1/;
/plugin/;

#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>
#include <dt-bindings/clock/rk3568-cru.h>
#include <dt-bindings/pwm/pwm.h>

&pwm0 {
    status = "okay";
    pinctrl-names = "active";
    pinctrl-0 = <&pwm0m1_pins>;
};

&{/} {
    pwm_demo: pwm_demo {
        status = "okay";
        compatible = "pwm_demo";

        back {
            pwm-names = "pwm-demo";
            pwms = <&pwm0 0 10000 1>;
            duty_ns = <5000>;
        };
    };
};
  • 第13行,设置pwm0的status = “okay”,即开启该pwm0控制器。

  • 第15行,设置pinctrl-0,使用GPIO0_C7引脚,复用为pwm功能。

  • 第18-28行,此节内容插入到根节点中,定义了一个pwm_demo节点,compatible为”pwm_demo”,用于匹配我们自己写的驱动出现。 此节点包含一个“back”子节点,子节点内定义了pwm属性信息, 这里我们使用PWM0的通道,占空比设置为5000,周期为10000,反相极性。

注意,此设备树插件部分内容与lubancat2板卡设备树leds节点冲突,实验前注意取消设备树中leds的使用,即status = “disabled”。

6.2.2. 驱动程序实现

驱动程序具体代码如下:

注册平台设备
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 static const struct of_device_id of_pwm_leds_match[] = {
    {.compatible = "pwm_demo"},
    {},
};

static struct platform_driver pwm_demo_driver = {
    .probe          = pwm_demo_probe_new,
    .remove         = pwm_demo_remove,
    .driver         = {
            .name   = "pwm_demo",
            .of_match_table = of_pwm_leds_match,
    },
};

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

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

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

平台设备与设备节点匹配成功后我们就可以很容易从设备树中获取信息, 而不必使用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
static int pwm_demo_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)
    {
            /*--------------第二部分-----------------*/
            pwm_test = devm_of_pwm_get(dev, child, NULL);
            if (IS_ERR(pwm_test))
            {
                    printk(KERN_ERR" pwm_test,get pwm  error!!\n");
                    return -1;
            }
    }
    else
    {
            printk(KERN_ERR" pwm_test of_get_next_child  error!!\n");
            return -1;
    }

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

    return ret;
}

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

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

  • 第27-29行,依次调用pwm_config、pwm_set_polarity、pwm_enable函数,配置PWM,设置输出极性、 使能PWM输出,需要注意的是这里设置的极性为负极性, 这样pwm_config函数其中频率是以周期(period_ns)的形式配置为5000,占空比是以有效时间(duty_ns)的形式配置为1000。

6.2.3. 实验准备

在板卡上的部分GPIO可能会被系统占用,引脚被占用后,设备树可能无法再加载或驱动中无法再申请对应的资源, 比如运行代码时出现“Device or resource busy”或者运行代码卡死等等现象,要确保所用的GPIO是没有被其他驱动占用的。

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

6.2.3.1. 通过内核工具编译设备树插件

设备树插件与设备树一样都是使用DTC工具编译,只不过设备树编译为.dtb。而设备树插件需要编译为.dtbo。 我们可以使用DTC编译命令编译生成.dtbo,但是这样比较繁琐、容易出错。

我们可以修改内核目录/arch/arm/boot/dts/overlays下的Makefile文件, 添加我们编辑好的设备树插件。并把设备树插件文件放在和Makefile文件同级目录下。 以进行设备树插件的编译,详细看下 环境搭建章节

在内核的根目录下执行如下命令即可:

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat2_defconfig  //以lubancat2为例

make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs

生成的.dtbo位于内核根目录下的“arch/arm64/boot/dts/rockchip/overlays”目录下。

本章的PWM子系统的设备树插件为“lubancat-pwm0-m1-demo-overlay.dts”, 编译之后就会在arch/arm64/boot/dts/rockchip/overlays目录下生成同名的lubancat-pwm0-m1-demo-overlay.dtbo文件。得到.dtbo后,下一步就是将其加载到系统中。

6.2.3.2. 添加设备树插件文件

上一小节我们编译生成了 lubancat-pwm0-m1-demo-overlay.dtbo ,该文件可以被动态的加载到系统,以lubancat2板卡uboot加载设备树插件为例,详细看下 环境搭建章节

首先我们把编译好的设备树插件文件,上传到我们开发板中。 我们可以使用uboot加载编写好的设备树插件,只需完成简单的两个步骤:

  • 1、将需要加载的.dtbo文件放入板卡 /boot/dtb/overlays/ 目录下。

  • 2、将对应的设备树插件加载配置,写入uEnv.txt配置文件,系统启动过程中会自动从uEnv.txt读取要加载的设备树插件。 以lubancat2为例,打开位于“/boot/uEnv/”目录下的uEnvLubanCat2.txt文件,要将设备树插件写入uEnvLubanCat2.txt也,使用vim或者nano编辑器打开文件,书写格式为“dtoverlay=<设备树插件路径>”。

添加好后,我们重启开发板,使用命令ls /proc/device-tree/ 查看 是否有pwm_demo目录,有就说明加载成功。

6.2.3.3. 编译驱动程序及测试程序

本节实验使用的Makefile如下所示:

Makefile(位于linux_driver/pwm_sub_system)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
KERNEL_DIR=../../kernel/

ARCH=arm64
CROSS_COMPILE=aarch64-linux-gnu-
export  ARCH  CROSS_COMPIL

obj-m := pwm_sub_system.o

all:
    $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules

.PHONY:clean
clean:
    $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean

将配套的驱动代码如:linux_driver/放置在与内核同级目录下,并到pwm_sub_system驱动目录中输入如下命令来编译驱动模块及测试程序:

make

6.2.4. 下载验证

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

重启后,直接使用insmod命令加载驱动。

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

cat /sys/kernel/debug/pwm
broken

使用示波器可以看到设定的PWM波(如果不更改例程配置,pwm频率为200KHz,占空比80%)。