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

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

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

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

6.1. PWM子系统简介

在MP157中pwm子系统使用的是TIM外设。 有关TIM外设的介绍可以参考MP157参考手册的40~45章节,全为各种定时器的介绍,这里不再介绍。 使用了PWM子系统后和具体硬件相关的内容芯片厂商已经写好了, 我们唯一要做的就是在设备树(或者是设备树插件)中声明使用的引脚。

PWM子系统相关资料,可在内核源码中查看参考文档:

Documentation/devicetree/bindings/pwm/pwm.txt

Documentation/devicetree/bindings/pwm/pwm-stm32.txt

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子系统的使用。 我们把开发板引出的 UART3_TX 引脚复用为定时器2的PWM输出,在驱动程序中通过设置输出的占空比, 并使用示波器观察、验证输出是否正确。

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

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

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

pwm节点
 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
timers2:timer@40000000 {
    #address-cells = <1>;
    #size-cells = <0>;
    compatible = "st,stm32-timers";
    reg = <0x40000000 0x400>;
    clocks = <&rcc TIM2_K>;
    clock-names = "int";
    dmas = <&dmamux1 18 0x400 0x5>,
        <&dmamux1 19 0x400 0x5>,
        <&dmamux1 20 0x400 0x5>,
        <&dmamux1 21 0x400 0x5>,
        <&dmamux1 22 0x400 0x5>;
    dma-names = "ch1", "ch2", "ch3", "ch4", "up";
    status = "disabled";

    pwm {
        compatible = "st,stm32-pwm";
        #pwm-cells = <3>;
        status = "disabled";
    };

    timer@1 {
        compatible = "st,stm32h7-timer-trigger";
        reg = <1>;
        status = "disabled";
    };
};

这里就是PWM驱动对应的设备树节点,这是pwm子系统的控制节点, 可以看到它设置了MP157芯片定时器外设的时钟、DMA、寄存器地址等等。这样的节点有非常多,对应着MP157片上定时器外设的数量。 简单了解即可,我们不会去修改它。

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

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

    &PWMn :指定使用哪个pwm,stm32mp157c.dtsi文件中的tim节点都有定义,在引用时可以起别名如pwm2

    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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// SPDX-License-Identifier: (GPL-2.0+ OR BSD-3-Clause)
/*
 * Copyright (C) STMicroelectronics 2018 - All Rights Reserved
 * Author: Alexandre Torgue <alexandre.torgue@st.com>.
 */

/dts-v1/;
/plugin/;
#include <dt-bindings/pinctrl/stm32-pinfunc.h>
#include <dt-bindings/input/input.h>
#include <dt-bindings/mfd/st,stpmic1.h>
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>

/ {

    fragment@0 {
        target = <&timers2>;
        __overlay__ {
            status = "okay";
            /delete-property/dmas;
            /delete-property/dma-names;
            pwm2: pwm {
                /* configure PWM pins on TIM2_CH3 */
                pinctrl-0 = <&pwm2_pins_a>;
                pinctrl-1 = <&pwm2_sleep_pins_a>;
                pinctrl-names = "default", "sleep";
                /* enable PWM on TIM2 */
                #pwm-cells = <2>;
                status = "okay";
            };
        };
    };

    fragment@1 {
        target = <&pinctrl>;
        __overlay__ {
            /* select TIM2_CH3 alternate function 1 on 'PB10' */
            pwm2_pins_a: pwm2-0 {
                pins {
                    pinmux = <STM32_PINMUX('B', 10, AF1)>;
                    bias-pull-down;
                    drive-push-pull;
                    slew-rate = <0>;
                };
            };
            /* configure 'PB10' as analog input in low-power mode */
            pwm2_sleep_pins_a: pwm2-sleep-0 {
                pins {
                    pinmux = <STM32_PINMUX('B', 10, ANALOG)>;
                };
            };
        };
    };

    fragment@2 {
        target-path="/";
        __overlay__{
            pwm_test {
                compatible = "pwm_test";
                status = "okay";
                front {
                    pwm-names = "test_tim2_ch3_pwm2";
                    pwms = <&pwm2 2 1000000>;
                };
            };
        };
    };
};
  • 第18-31行,此节内容插入到timers2节点中,由于本节要将使用的引脚 UART3_TX 可被复用tim2的pwm输出功能,那么就要开启timers2功能,status = “okay”。 在timers2节点下包含一个“pwm”子节点,在此子节点中我们添加了 UART3_TX 引脚的复用属性,复用为&pwm2_pins_a节点定义的属性。

  • 第38-51行,此节内容插入到pinctrl节点中,这里定义了 UART3_TX 引脚的两种状态, 一是pwm2_pins_a:将 UART3_TX 引脚也就是PB10的属性复用为TIM2_CH3(查阅MP157的产品手册可得)。 二是pwm2_sleep_pins_a:将 UART3_TX 引脚设置为模拟模式,在低功耗模式会使用。

  • 第59-65行,此节内容插入到根节点中,定义了一个pwm_test节点,compatible为”pwm_test”,用于匹配我们自己写的驱动出现。 此节点包含一个“front”子节点,子节点内定义了pwm属性信息, 这里我们使用PWM2的通道2(TIM2_CH3),频率设置为100KHz(周期为50000ns,计算得到频率为100KHz)

此设备树插件做了三个功能,将TIM2定时器开启,设置 UART3_TX (PB10)的引脚复用,定义一个节点供我们的驱动程序使用PWM功能。

注意,此设备树插件部分内容与LCD的设备树插件冲突,实验前注意取消LCD的设备树插件加载。

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 = "pwm_test"},
    {},
};

static struct platform_driver led_pwm_driver = {
    .probe          = led_pwm_probe_new,
    .remove         = led_pwm_remove,
    .driver         = {
            .name   = "test_tim2_ch3_pwm2",
            .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)
    {
            /*--------------第二部分-----------------*/
            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 led_pwm_remove(struct platform_device *pdev)
{
    pwm_config(pwm_test, 0, 5000);
    pwm_free(pwm_test);
    return 0;
}
  • 第9行,获取子节点,在设备树插件中,我们把PWM相关信息保存在pwm_test的子节点中, 所以这里首先获取子节点。

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

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

6.2.3. 实验准备

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

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

方法参考如下:

broken

取消 LCD 设备树插件,以释放系统对应LCD资源,并添加PWM子系统实验的设备树插件,操作如下:

broken
dtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-pwm-sub-system.dtbo

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

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

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

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

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

broken

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

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

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

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

broken

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

6.2.3.2. 添加设备树插件文件

将上小节中编译出的设备树插件 stm-fire-pwm-sub-system.dtbo 添加到开发板目录 /usr/lib/linux-image-4.19.94-stm-r1/overlays/ 中重启开发板即可。

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

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

Makefile(位于../linux_driver/button_interrupt/interrupt)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
KERNEL_DIR=../ebf_linux_kernel/build_image/build

ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-
export  ARCH  CROSS_COMPILE

obj-m := pwm_sub_system.o

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

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

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

make

6.2.4. 下载验证

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

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

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

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

cat /sys/kernel/debug/pwm
broken

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

broken