本文全部内容均可在https://gitee.com/gaoro-xiao/rv1103-learning-code内查看

产生PWM

任何嵌入式平台,只要学会了点灯,你就已经学会了一半。

——沃兹基·硕德

这一次我们尝试产生PWM信号,并用它来调节LED的亮度。依然是这张引脚图:
在这里插入图片描述

我们发现Luckfox的开发板上没有在**GPIO1_A2(34)**引脚标记PWM功能,但是根据RV1103的datasheet和内核设备树,这个引脚是支持PWM输出的。所以我们就用这个引脚来产生PWM信号。

修改设备树

什么是设备树?

不管是这一期还是上一期,我们都多次提到一个词——设备树(Device Tree)。那这设备树是什么东西呢?

设备树是Linux内核用来描述硬件信息的一种数据结构。它以一种与平台无关的方式描述了系统中的各种硬件设备及其属性,从而使得内核可以根据设备树的信息来初始化和管理这些设备。

简单来说,设备树就像是一张硬件的“地图”,告诉内核系统中有哪些设备,它们的位置在哪里,以及它们是如何连接和配置的。因此,通过查看和修改设备树,我们可以了解和控制系统中的硬件资源。

我们先举一个例子。假设我们有一个简单的设备树片段,描述了一个GPIO控制器:

gpio0: gpio@ff380000 {
    compatible = "rockchip,gpio-bank";
    reg = <0xff380000 0x100>;
    interrupts = <GIC_SPI 5 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&cru PCLK_PMU_GPIO0>, <&cru DBCLK_PMU_GPIO0>;

    gpio-controller;
    #gpio-cells = <2>;
    gpio-ranges = <&pinctrl 0 0 32>;
    interrupt-controller;
    #interrupt-cells = <2>;
};

这个片段定义了一个名为gpio0的GPIO控制器,指定了它的寄存器地址、中断信息、时钟信息等属性。内核在启动时会根据这些信息来初始化这个GPIO控制器。其中,compatible属性用于指定设备的类型,以匹配驱动;reg属性用于指定设备的寄存器地址;interrupts属性用于指定设备的中断信息;clocks属性用于指定设备的时钟信息。这样的操作是不是非常像我们在单片机编程时对寄存器的配置呢?是的,设备树就是对硬件寄存器配置的一种抽象和封装。只不过寄存器配置的细节被隐藏在设备树和驱动程序中,我们只需要关注更高层次的硬件描述。

配置PWM引脚

接下来我们来配置PWM引脚。首先找到我们编译内核的设备树源文件(DTS)。在Luckfox的SDK中,设备树源文件使用了一个符号链接,你可以在config/dts_config文件中查看并修改。

打开config/dts_config文件,你会看到类似下面的内容:

/dts-v1/;

#include "rv1103.dtsi"
#include "rv1106-evb.dtsi"
#include "rv1103-luckfox-pico-ipc.dtsi"

/ {
	model = "Luckfox Pico Mini";
	compatible = "rockchip,rv1103g-38x38-ipc-v10", "rockchip,rv1103";
};

......

上面三行是包含其他设备树片段的指令。它包含了其它的dtsi文件。dtsi文件是设备树的包含文件,通常用于定义通用的硬件信息和配置。通过包含这些文件,我们可以复用已有的设备树定义,避免重复编写相同的硬件描述。由于是通用的硬件配置,我们尽可能不去修改它。但是如果我们需要对里面的内容进行调整怎么办呢?这就需要用到设备树的覆盖机制(Overlay)。设备树覆盖机制允许我们在不修改原始设备树文件的情况下,对其进行扩展和修改。通过创建一个新的设备树片段,我们可以覆盖或添加新的硬件描述,从而实现对原始设备树的定制化配置。

了解完这些,我们可以尝试配置PWM引脚了。根据RV1103的datasheet,GPIO1_A2(34)引脚支持PWM输出功能。我们需要在设备树中为这个引脚添加PWM功能的配置。打开rv1103.dtsi文件,找到PWM控制器的定义部分,通常类似下面这样:

pwm0: pwm@ff350000 {
    compatible = "rockchip,rv1106-pwm", "rockchip,rk3328-pwm";
    reg = <0xff350000 0x10>;
    interrupts = <GIC_SPI 31 IRQ_TYPE_LEVEL_HIGH>;
    #pwm-cells = <3>;
    pinctrl-names = "active";
    pinctrl-0 = <&pwm0m0_pins>;
    clocks = <&cru CLK_PWM0_PERI>, <&cru PCLK_PWM0_PERI>;
    clock-names = "pwm", "pclk";
    status = "disabled";
};

在这里我们只能看见PWM控制器的定义,并不能找到具体的引脚配置。以这一份PWM配置的设备树文件举例,由于引脚配置通常在pinctrl节点中定义,我们需要通过pwm0m0_pins来找到对应的引脚配置。继续在整个工程文件中搜索pwm0m0_pins,你会在rv1106-pinctrl.dtsi中看到类似下面的内容:

pwm0 {
    /omit-if-no-ref/
    pwm0m0_pins: pwm0m0-pins {
        rockchip,pins =
            /* pwm0_m0 */
            <1 RK_PA2 1 &pcfg_pull_none>;
    };

    /omit-if-no-ref/
    pwm0m1_pins: pwm0m1-pins {
        rockchip,pins =
            /* pwm0_m1 */
            <1 RK_PD2 6 &pcfg_pull_none>;
    };
};

正常来说市面上任何一款RV1103的SDK,你都能找到这样的代码段。我们发现,这份代码实际上只描述了引脚,对pwm的其它任何功能都没有进行描述。这不难理解,pwm的其它功能在rv1103.dtsi文件中有描述了,而rv1103.dtsi文件是引用了rv1106-pinctrl.dtsi文件的,因此直接使用在rv1106-pinctrl.dtsi文件内已经定义好的符号pwm0m0_pins。好不容易找到pinctrl文件了,我们如何知道这份文件内那一个引脚连接到了哪一组pwm呢?关键就在<1 RK_PA2 1 &pcfg_pull_none>;这一行代码中。这里的RK_PA2就是我们要找的GPIO1_A2引脚的符号名称,而前面的数字1表示这个引脚属于GPIO1组。通过查阅RV1103的datasheet,我们可以确认GPIO1_A2(34)引脚确实支持PWM输出功能。而它所在的pwm0_m0正是我们需要使用的PWM通道。

启用PWM设备

找到对应的引脚配置后,我们还需要启用PWM设备本身。在rv1103.dtsi文件中,我们能看到已有的pwm0_m0配置:

pwm0: pwm@ff350000 {
    compatible = "rockchip,rv1106-pwm", "rockchip,rk3328-pwm";
    reg = <0xff350000 0x10>;
    interrupts = <GIC_SPI 31 IRQ_TYPE_LEVEL_HIGH>;
    #pwm-cells = <3>;
    pinctrl-names = "active";
    pinctrl-0 = <&pwm0m0_pins>;
    clocks = <&cru CLK_PWM0_PERI>, <&cru PCLK_PWM0_PERI>;
    clock-names = "pwm", "pclk";
    status = "disabled";
};

注意到最后一行的status = "disabled";,这表示PWM设备默认是禁用状态。我们需要将其修改为status = "okay";。但是直接在dtsi文件内修改并不是一个好习惯,最好是通过设备树覆盖机制来实现。我们可以在dts_config文件中添加一个新的片段,覆盖原有的PWM配置。添加如下内容:

&pwm0 {
    status = "okay";
};

这样就启用了PWM设备。随后只需要重新编译内核并烧录到开发板上即可。

简单来说我们就是在dts_config文件中添加了三行代码,但是需要确定是哪一组pwm通道,以及对应的引脚配置。通过查阅设备树源文件,我们成功找到了这些信息。

在控制台操作PWM

修改完设备树并烧录系统后,我们就可以在控制台操作PWM了。 首先我们ssh连接到开发板的控制台,查看一下/sys/class/pwm目录,你应该会看到如下结果:

[root@luckfox pwm]# ls /sys/class/pwm/
pwmchip0

我们再看看pwmchip0目录下的内容。为了方便操作,我们可以直接进入该目录:

cd /sys/class/pwm/pwmchip0
ls

你会看到下面的文件列表:

[root@luckfox pwmchip0]# ls
device     export     npwm       power      subsystem  uevent     unexport

这些文件分别对应着PWM控制器的各种属性。我们需要做的第一件事是导出PWM通道,以便我们可以控制它。根据RV1103的设备树配置,GPIO1_A2(34)引脚对应的是pwm0_m0通道,所以我们需要导出通道0。执行下面的命令:

echo 0 > export

执行完毕后,再次查看/sys/class/pwm/pwmchip0目录:

[root@luckfox pwmchip0]# echo 0 > export
[root@luckfox pwmchip0]# ls
device     export     npwm       power      pwm0       subsystem  uevent     unexport

可以看到,已经多了一个pwm0的文件夹,这就是我们刚才导出的PWM通道对应的文件夹。接下来,我们进入pwm0目录,查看里面的文件:

cd pwm0
ls

你会看到下面的文件列表:

[root@luckfox pwm0]# ls
capture      duty_cycle   enable       output_type  period       polarity     power        uevent

接下来要做的事情就和我们操作GPIO差不多了。我们需要设置PWM的周期(period)和占空比(duty_cycle),然后启用(enable)PWM输出。输出前注意修改极性(polarity)为正常模式(normal),否则可能会出现反转的情况。而周期的单位是ns,数据类型为整形。占空比单位也是ns,是高电平时间。注意占空比不能大于周期。 试试看:

echo "normal" > polarity        # 设置极性为正常模式
echo 1000000 > period        # 设置周期为1ms(1000000ns)
echo 500000 > duty_cycle     # 设置占空比为50%(500000ns)
echo 1 > enable              # 启用PWM输出

执行完毕后,你应该可以看到开发板上的LED灯亮起,并且亮度为50%了!你可以尝试修改占空比的值,来观察LED亮度的变化。例如,将占空比设置为100000(10%):

echo 100000 > duty_cycle     # 设置占空比为10%(100000ns)

LED的亮度应该会变暗。

使用完成后,记得将PWM通道取消导出,以释放资源。执行下面的命令:

echo 0 > /sys/class/pwm/pwmchip0/unexport

这样就完成了通过控制台操作PWM来调节LED亮度的全过程。

将PWM功能封装为库

由于使用C语言驱动PWM的过程和驱动GPIO非常相似,因此直接驱动就不再赘述了。我们可以将PWM的功能封装为一个简单的C语言库,方便后续调用。下面是一个简单的PWM库的示例代码:
pwm_sysfs.h

#ifndef PWM_SYSFS_H
#define PWM_SYSFS_H
#include <stdio.h>

typedef struct {
    int chip; // PWMn
    int channel; // PWMn_m
    char path[64];        // PWM sysfs 绝对路径
    FILE *period_fp;      // 保持打开以避免频繁 fopen/fclose
    FILE *duty_cycle_fp;  // 保持打开以避免频繁 fopen/fclose
    FILE *enable_fp;      // 保持打开以避免频繁 fopen/fclose
    FILE *polarity_fp;    // 保持打开以避免频繁 fopen/fclose
} pwm_t;

typedef enum {
    PWM_POLARITY_NORMAL = 0,
    PWM_POLARITY_INVERSED = 1
} pwm_polarity_t;

// 初始化并导出 PWM
int pwm_open(pwm_t *pwm, int chip, int channel);

// 设置 PWM 周期,单位纳秒
int pwm_set_period(pwm_t *pwm, unsigned int period_ns);

// 设置 PWM 占空比,单位纳秒
int pwm_set_duty_cycle(pwm_t *pwm, unsigned int duty_cycle_ns);

// 使能或禁用 PWM
int pwm_enable(pwm_t *pwm, int enable); // 1 to enable,

// 设置 PWM 极性
int pwnm_set_polarity(pwm_t *pwm, pwm_polarity_t polarity);

// 操作PWM
int pwm_operate(pwm_t *pwm, int period_ns, int duty_cycle_ns, pwm_polarity_t polarity);

// 关闭并 unexport
int pwm_close(pwm_t *pwm);

#endif // PWM_SYSFS_H

pwm_sysfs.c

#include "pwm_sysfs.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 相对路径
#define DUTY_PATH "/duty_cycle"
#define PERIOD_PATH "/period"
#define ENABLE_PATH "/enable"
#define POLARITY_PATH "/polarity"
const char *pwm_polarity_str[2] = {
    "\"normal\"",
    "\"inversed\""
};

static char temp_path[64];
// 打开文件辅助函数
static int open_fp(pwm_t *pwm, const char *relative_path, FILE **fp)
{
    memset(temp_path, 0, sizeof(temp_path));
    snprintf(temp_path, sizeof(temp_path), "%s%s", pwm->path, relative_path);
    *fp = fopen(temp_path, "w");
    if (!*fp)
        return -1;
    return 0;
}

int pwm_open(pwm_t *pwm, int chip, int channel)
{
    pwm->chip = chip;
    pwm->channel = channel;

    // export
    FILE *export_fp = NULL;
    snprintf(temp_path, sizeof(temp_path),
             "/sys/class/pwm/pwmchip%d/export", chip);
    export_fp = fopen(temp_path, "w");
    if (!export_fp)
        return -1;
    fprintf(export_fp, "%d", channel);
    fclose(export_fp);

    // 构建路径
    snprintf(pwm->path, sizeof(pwm->path),
             "/sys/class/pwm/pwmchip%d/pwm%d", chip, channel);
    // 打开各个文件
    if (open_fp(pwm, PERIOD_PATH, &pwm->period_fp) < 0)
        return -1;
    if (open_fp(pwm, DUTY_PATH, &pwm->duty_cycle_fp) < 0)
        return -1;
    if (open_fp(pwm, ENABLE_PATH, &pwm->enable_fp) < 0)
        return -1;
    if (open_fp(pwm, POLARITY_PATH, &pwm->polarity_fp) < 0)
        return -1;
    return 0;
}

int pwm_close(pwm_t *pwm)
{
    if (pwm->period_fp)
        fclose(pwm->period_fp);
    if (pwm->duty_cycle_fp)
        fclose(pwm->duty_cycle_fp);
    if (pwm->enable_fp)
        fclose(pwm->enable_fp);
    if (pwm->polarity_fp)
        fclose(pwm->polarity_fp);

    // unexport
    FILE *unexport_fp = NULL;
    snprintf(temp_path, sizeof(temp_path),
             "/sys/class/pwm/pwmchip%d/unexport", pwm->chip);
    unexport_fp = fopen(temp_path, "w");
    if (!unexport_fp)
        return -1;
    fprintf(unexport_fp, "%d", pwm->channel);
    fclose(unexport_fp);
    return 0;
}

int pwm_set_period(pwm_t *pwm, unsigned int period_ns)
{
    if (!pwm->period_fp)
        return -1;
    fprintf(pwm->period_fp, "%u", period_ns);
    fflush(pwm->period_fp);
    return 0;
}

int pwm_set_duty_cycle(pwm_t *pwm, unsigned int duty_cycle_ns)
{
    if (!pwm->duty_cycle_fp)
        return -1;
    fprintf(pwm->duty_cycle_fp, "%u", duty_cycle_ns);
    fflush(pwm->duty_cycle_fp);
    return 0;
}

int pwm_enable(pwm_t *pwm, int enable)
{
    if (!pwm->enable_fp)
        return -1;
    fprintf(pwm->enable_fp, "%d", !!enable); // 强制 0/1
    fflush(pwm->enable_fp);
    return 0;
}

int pwnm_set_polarity(pwm_t *pwm, pwm_polarity_t polarity)
{
    if (!pwm->polarity_fp)
        return -1;
    fprintf(pwm->polarity_fp, "%s", pwm_polarity_str[polarity]);
    fflush(pwm->polarity_fp);
    return 0;
}

int pwm_operate(pwm_t *pwm, int period_ns, int duty_cycle_ns, pwm_polarity_t polarity)
{
    if (pwm->path[0] == '\0')
        return -1;

    pwm_enable(pwm, 0); // 先禁用
    pwm_set_period(pwm, period_ns);
    pwm_set_duty_cycle(pwm, duty_cycle_ns);
    pwnm_set_polarity(pwm, polarity);
    pwm_enable(pwm, 1); // 最后使能
    return 0;
}

这个库提供了初始化PWM、设置周期和占空比、使能PWM、设置极性以及关闭PWM等功能。你可以根据需要调用这些函数来控制PWM输出。
下面是一个使用这个PWM库的示例程序:
main.c

#include <unistd.h>
#include <math.h>
#include "./lib/pwm_sysfs.h"

int main()
{
    // 这个程序实现按指数级变化的PWM占空比调节LED亮度
    // 因为人的眼睛对亮度的感知是对数关系,而非线性关系
    int pwm_chip = 0;    // PWM芯片号
    int pwm_channel = 0; // PWM通道号
    char input[16];

    printf("Set PWM chip number (default 0): ");
    fgets(input, sizeof(input), stdin);
    if (sscanf(input, "%d", &pwm_chip) != 1)
        return 0; // 输入无效,退出
    printf("Set PWM channel number (default 0): ");
    fgets(input, sizeof(input), stdin);
    if (sscanf(input, "%d", &pwm_channel) != 1)
        return 0; // 输入无效,退出
    printf("Using PWM chip %d, channel %d\n", pwm_chip, pwm_channel);
    pwm_t pwm;
    if (pwm_open(&pwm, pwm_chip, pwm_channel) != 0)
    {
        perror("Failed to open PWM");
        return -1;
    }

    // 设置PWM参数
    long long period_ns = 1000000; // 1ms周期,频率1kHz
    int duty = 0;
    pwm_operate(&pwm, period_ns, duty, PWM_POLARITY_NORMAL);
    pwm_enable(&pwm, 1); // 使能PWM

    // 指数级变化占空比
    while(1)
    {
        for (int i = 0; i <= 0xFF; i++) // 255级调光
        {
            // gamma校正
            double gamma = 2.2;
            double normalized = (double)i / 255.0;
            double corrected = pow(normalized, gamma);
            duty = (int)(corrected * period_ns);
            pwm_set_duty_cycle(&pwm, duty);
            printf("Duty cycle set to %d ns\n", duty);
            usleep(10000); // 10ms延时
        }
        for (int i = 0xFF; i >= 0; i--)
        {
            // gamma校正
            double gamma = 2.2;
            double normalized = (double)i / 255.0;
            double corrected = pow(normalized, gamma);
            duty = (int)(corrected * period_ns);
            pwm_set_duty_cycle(&pwm, duty);
            printf("Duty cycle set to %d ns\n", duty);
            usleep(10000); // 10ms延时
        }
    }

    return 0;
}

这个程序会让LED的亮度按照指数级变化,模拟人眼对亮度的感知。你可以编译并运行这个程序,观察LED亮度的变化效果。随后,你可以按照上一篇的方法交叉编译,并将生成的可执行文件上传到开发板上运行。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐