跳到主要内容

PWM

在本章中,我们将学习如何在应用层中使用 PWM。

示例程序:Code.zip

1.PWM

PWM,全称为脉冲宽度调制(Pulse Width Modulation),是一种通过控制信号的脉冲宽度来实现模拟信号输出的技术。它常用于嵌入式系统和电子设备中,用于控制电机速度、LED 亮度、音频信号生成等应用。

在 Linux 系统中,PWM 设备通常通过 sysfs 文件系统进行管理和配置,其设备目录通常位于 /sys/class/pwm/ 目录下。

2.控制PWM(Shell)

2.1 引脚分布

由于在 LuckFox PicoLuckFox Pico Mini A/BLuckFox Pico Plus/Pro/Max 引脚复用,会有引脚功能冲突,所以各型号开发板默认开启的PWM如下:

  • LuckFox Pico:
    • PWM0_0 - 34 号引脚
    • PWM1_0 - 4 号引脚
    • PWM10_1 - 54 号引脚
    • PWM11_1 - 55 号引脚
  • LuckFox Pico Mini A/B:
    • PWM10_1 - 54 号引脚
    • PWM11_1 - 55 号引脚
  • LuckFox Pico Plus:
    • PWM0_0 - 34 号引脚
    • PWM1_0 - 4 号引脚
    • PWM10_1 - 54 号引脚
    • PWM11_1 - 55 号引脚
  • LuckFox Pico Pro/Max:
    • PWM5_1 - 72 号引脚
    • PWM6_1 - 73 号引脚
    • PWM10_1 - 54 号引脚
    • PWM11_1 - 55 号引脚
  1. LuckFox Pico 引脚图:

  2. LuckFox Pico Mini A/B 引脚图:

  3. LuckFox Pico Plus 引脚图:

  4. LuckFox Pico Pro/Max 引脚图:

  5. LuckFox Pico Ultra/Ultra W 引脚图:

2.2 设备目录

/sys/class/pwm/ 目录中,每个 PWM 设备都有一个独立的子目录,其名称通常以 pwmchipX 的形式命名,其中 X 是 PWM 设备的编号。您可使用如下命令查看:

# ls -l /sys/class/pwm
lrwxrwxrwx 1 root root 0 pwmchip10 -> ../../devices/platform/ff490020.pwm/pwm/pwmchip10
lrwxrwxrwx 1 root root 0 pwmchip1 -> ../../devices/platform/ff350010.pwm/pwm/pwmchip1
lrwxrwxrwx 1 root root 0 pwmchip11 -> ../../devices/platform/ff490030.pwm/pwm/pwmchip11
lrwxrwxrwx 1 root root 0 pwmchip0 -> ../../devices/platform/ff350000.pwm/pwm/pwmchip0

2.3 设备属性

在成功导出 PWM 的一个通道后,会在该 PWM 的设备目录下生成单个通道的目录,其中有这个通道对应的属性文件,用户可以通过修改这些文件的内容来配置和控制特定 PWM 通道的各个参数,以实现对 PWM 信号的精确控制。这提供了一个方便的接口,使用户能够与 PWM 硬件进行交互并调整 PWM 信号的特性。

  1. 查看PWM10单个通道的属性文件

    # ls /sys/class/pwm/pwmchip10/pwm0/
    uevent capture duty_cycle output_type
    polarity enable power period
  2. 属性文件

    通过使用这些属性文件,您可以轻松地配置和控制 PWM 通道,使其适应不同的应用场景和需求。其中一些关键的属性如下:

    • enable:用于启用或禁用PWM通道。
    • period:用于设置PWM信号的周期时间。
    • duty_cycle:用于设置PWM信号的占空比。
    • polarity:用于配置PWM信号的极性。
    • power/control:用于启用或禁用PWM通道的电源管理(通常用于省电模式)。

2.4 控制PWM

  1. 测试 PWM。将PWM10_M1(GPIO1_C6_d)导出到用户空间

    echo 0 > /sys/class/pwm/pwmchip10/export
  2. 设置PWM周期单位为ns,比如 1KHz 频率的周期就是 1000000ns(注意,在任何的情况下都得保证 period 的值大于等于 duty_cycle 的值)

    echo 1000000 > /sys/class/pwm/pwmchip10/pwm0/period
  3. 设置占空比(小于等于 period )

    echo 100000 > /sys/class/pwm/pwmchip10/pwm0/duty_cycle
    echo 200000 > /sys/class/pwm/pwmchip10/pwm0/duty_cycle
    echo 300000 > /sys/class/pwm/pwmchip10/pwm0/duty_cycle
    echo 400000 > /sys/class/pwm/pwmchip10/pwm0/duty_cycle
    echo 500000 > /sys/class/pwm/pwmchip10/pwm0/duty_cycle
    echo 600000 > /sys/class/pwm/pwmchip10/pwm0/duty_cycle
    echo 700000 > /sys/class/pwm/pwmchip10/pwm0/duty_cycle
    echo 800000 > /sys/class/pwm/pwmchip10/pwm0/duty_cycle
    echo 900000 > /sys/class/pwm/pwmchip10/pwm0/duty_cycle
    echo 1000000 > /sys/class/pwm/pwmchip10/pwm0/duty_cycle
    echo 0 > /sys/class/pwm/pwmchip10/pwm0/duty_cycle
  4. 设置PWM极性正常或翻转

    echo "normal" > /sys/class/pwm/pwmchip1/pwm0/polarity
    echo "inversed" > /sys/class/pwm/pwmchip1/pwm0/polarity
  5. 使能和关闭PWM

    echo 1 > /sys/class/pwm/pwmchip1/pwm0/enable
    echo 0 > /sys/class/pwm/pwmchip1/pwm0/enable
  6. 取消导出PWM到用户空间

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

3.控制PWM(Python程序)

在前文中,我们演示了使用 echo 命令修改设备文件,以达到控制设备的目的。接下来,我们将使用 Python 程序实现 PWM 的控制。

3.1 示例程序

通过以下程序,可以实现控制PWM10输出。

from periphery import PWM
import time

pwm = PWM(10, 0)

try:
pwm.frequency = 1000
pwm.duty_cycle = 0
pwm.polarity = "normal"
pwm.enable()

direction = 1

while True:
pwm.duty_cycle += 0.01 * direction
pwm.duty_cycle = round(pwm.duty_cycle, 2)
if pwm.duty_cycle == 1.0:
direction = -1
elif pwm.duty_cycle == 0.0:
direction = 1

time.sleep(0.05)

except KeyboardInterrupt:
pass

finally:
pwm.close()

3.2 打开PWM设备

此代码段使用 periphery 库中的 PWM 类创建了一个 PWM 对象。在这个例子中,PWM 对象被初始化为 PWM10 。

pwm = PWM(10, 0)

3.3 配置PWM

在这个代码段中,配置了先前创建的 PWM 对象的一些属性。设置了 PWM 的频率为 1000 Hz,极性为正常("normal"),并将占空比初始化为 0。

pwm.frequency = 1000
pwm.duty_cycle = 0
pwm.polarity = "normal"

3.4 PWM输出

这个代码段实现了一个简单的 PWM 输出。通过循环不断调整 PWM 的占空比,以实现呼吸灯效果。pwm.duty_cycle 每次增加 0.01(可以根据需要调整),并使用 round 函数将其保留两位小数。在达到最大或最小值时,反转方向。并在每次更新 PWM 值后,通过 time.sleep 控制呼吸灯的速度。

while True:
pwm.duty_cycle += 0.01 * direction
pwm.duty_cycle = round(pwm.duty_cycle, 2)
if pwm.duty_cycle == 1.0:
direction = -1
elif pwm.duty_cycle == 0.0:
direction = 1

print(pwm.duty_cycle)
time.sleep(0.05)

3.5 运行程序

  1. 使用 nano 工具在终端创建 py 文件,粘贴并保存 python 程序

    # nano pwm.py
  2. 运行程序

    # python3 pwm.py
  3. 实验现象

    控制PWM10输出,实现呼吸灯
    image

4.控制PWM(C程序)

在前文中,我们演示了如何使用 echo 命令和 Python 程序控制 PWM。实际上也可以使用 vi 编辑器对文件进行修改,修改时需注意用户权限。此外,我们还可以使用C库函数或系统调用来读写设备文件,以达到控制设备的目的。请注意,为了在特定的嵌入式系统上运行程序,通常需要使用交叉编译工具来编译代码,以生成可在目标开发板上执行的可执行文件。接下来,让我们一起探讨具体的实施步骤。

4.1 示例程序

通过以下程序,可以实现控制PWM10输出。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define PWM_PATH "/sys/class/pwm/pwmchip10"
#define PERIOD_NS 1000000
#define MIN_DUTY_CYCLE_NS 0
#define MAX_DUTY_CYCLE_NS 1000000

int main() {
FILE *pwm_export = fopen(PWM_PATH "/export", "w");
if (!pwm_export) {
perror("Failed to open PWM export");
return 1;
}
fprintf(pwm_export, "0");
fclose(pwm_export);

FILE *period_file = fopen(PWM_PATH "/pwm0/period", "w");
if (!period_file) {
perror("Failed to open PWM period");
return 1;
}
fprintf(period_file, "%d", PERIOD_NS);
fclose(period_file);

FILE *enable_file = fopen(PWM_PATH "/pwm0/enable", "w");
if (!enable_file) {
perror("Failed to open PWM enable");
return 1;
}
fprintf(enable_file, "1");
fclose(enable_file);

int direction = 1;
int duty_cycle_ns = 0;

while (1) {
duty_cycle_ns += 10000 * direction;
if(duty_cycle_ns == MAX_DUTY_CYCLE_NS)
direction = -1;
else if(duty_cycle_ns == MIN_DUTY_CYCLE_NS)
direction = 1;

FILE *duty_cycle_file = fopen(PWM_PATH "/pwm0/duty_cycle", "w");
if (!duty_cycle_file) {
perror("Failed to open PWM duty cycle");
return 1;
}
fprintf(duty_cycle_file, "%d", duty_cycle_ns);
fclose(duty_cycle_file);

usleep(50000);
}

FILE *pwm_unexport = fopen(PWM_PATH "/unexport", "w");
if (!pwm_unexport) {
perror("Failed to open PWM unexport");
return 1;
}
fprintf(pwm_unexport, "0");
fclose(pwm_unexport);

return 0;
}

4.2 导出PWM通道到用户空间

这段代码通过向 /sys/class/pwm/pwmchip10/export 写入设备索引(通常为0),以导出指定的PWM通道供用户空间控制。

FILE *pwm_export = fopen(PWM_PATH "/export", "w");
if (!pwm_export) {
perror("Failed to open PWM export");
return 1;
}
fprintf(pwm_export, "0");
fclose(pwm_export);

4.3 设置PWM周期

这段代码通过向 /sys/class/pwm/pwmchip10/pwm0/period 写入周期值 “1000000”(以纳秒为单位),以设置PWM的周期为1KHZ。

FILE *period_file = fopen(PWM_PATH "/pwm0/period", "w");
if (!period_file) {
perror("Failed to open PWM period");
return 1;
}
fprintf(period_file, "%d", PERIOD_NS);
fclose(period_file);

4.4 使能PWM

这段代码通过向 /sys/class/pwm/pwmchip10/pwm0/enable 文件写入值"1",以启用PWM信号的输出。

FILE *enable_file = fopen(PWM_PATH "/pwm0/enable", "w");
if (!enable_file) {
perror("Failed to open PWM enable");
return 1;
}
fprintf(enable_file, "1");
fclose(enable_file);

4.5 使用PWM实现呼吸灯

这段代码打开 /sys/class/pwm/pwmchip10/pwm0/duty_cycle 文件,并且通过不断改变 duty_cycle_ns 变量的值,实现了PWM占空比从0%逐渐增加到100%,然后再从100%逐渐减小到0%的呼吸灯效果。duty_cycle_ns 的变化在0到1000000之间,以控制占空比的变化。当占空比达到最小值或最大值时,它会反转方向,以实现往复运动。此外,usleep(50000) 函数用于控制呼吸灯的速度,您可以根据需要调整此值以改变呼吸速度。

int direction = 1; 
int duty_cycle_ns = 0;

while (1) {
duty_cycle_ns += 10000 * direction;
if(duty_cycle_ns == MAX_DUTY_CYCLE_NS)
direction = -1;
else if(duty_cycle_ns == MIN_DUTY_CYCLE_NS)
direction = 1;

FILE *duty_cycle_file = fopen(PWM_PATH "/pwm0/duty_cycle", "w");
if (!duty_cycle_file) {
perror("Failed to open PWM duty cycle");
return 1;
}
fprintf(duty_cycle_file, "%d", duty_cycle_ns);
fclose(duty_cycle_file);

usleep(50000);
}

4.6 取消导出PWM通道到用户空间

这段代码通过向 /sys/class/pwm/pwmchip10/unexport 写入设备索引,取消导出指定的PWM通道到用户空间。

FILE *pwm_unexport = fopen(PWM_PATH "/unexport", "w");
if (!pwm_unexport) {
perror("Failed to open PWM unexport");
return 1;
}
fprintf(pwm_unexport, "0");
fclose(pwm_unexport);

4.7 交叉编译

  1. 指定交叉编译工具

    首先,我们要将交叉编译工具的路径添加到系统的 PATH 环境变量中,以便可以在任何地方使用交叉编译工具,您可以在shell配置文件中添加以下行(通常是 ~/.bashrc~/.bash_profile~/.zshrc,具体取决于您使用的shell),注意 PATH= 后的路径为交叉编译工具所在的目录。

    • gcc路径

      <SDK Directory>/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin/arm-rockchip830-linux-uclibcgnueabihf-gcc
    • 打开shell配置文件

      vi ~/.bashrc 
    • 将交叉编译工具的路径添加到系统的PATH环境变量中,将 <SDK Directory> 修改为自己的 SDK 路径,如 /home/luckfox/luckfox-pico/

      export PATH=<SDK Directory>/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin:$PATH
    • 重新加载shell配置文件,使更改生效

      source ~/.bashrc  
  2. 使用交叉编译工具编译程序

    arm-rockchip830-linux-uclibcgnueabihf-gcc pwm.c -o pwm
  3. 交叉编译成功后,将在当前目录下生成可在开发板运行的可执行文件

    #ls
    pwm pwm.c

4.8 运行程序

  1. 文件传输

    先将 pwm 从虚拟机传输到 Windows,再通过 TFTP 或 ADB 传输到开发板,将文件从 Windows 通过 ADB 将文件传输到开发板的步骤如下:

    adb push 文件所在路径 开发板存储路径

    eg:(将当前目录下pwm文件传输到开发板的根目录)
    adb push pwm /
  2. 运行程序

    修改 pwm 文件的操作权限后运行程序

    # chmod 777 pwm
    # ./pwm
  3. 实验现象

    控制PWM10输出,实现呼吸灯
    image

5.修改设备树

程序中的 PWM10_M1 在所有型号的开发板都是默认开启的,可以直接使用。接下来,我们一起看看如果需要开启其他的 PWM 该如何操作。

5.1 修改设备树文件

  1. 设备树文件路径

    • 开发板配置文件

      <SDK目录>/project/cfg/BoardConfig_IPC/ 目录下有不同型号开发板的配置文件,这些文件主要包含了针对 Luckfox Pico 不同型号开发板的配置参数,涵盖了目标架构、启动介质、Uboot和内核配置、分区设置等多方面的内容。SDK目录结构如下:

      ├── build.sh -> project/build.sh ---- SDK编译脚本
      ├── media --------------------------- 多媒体编解码、ISP等算法相关(可独立SDK编译)
      ├── sysdrv -------------------------- U-Boot、kernel、rootfs目录(可独立SDK编译)
      ├── project ------------------------- 参考应用、编译配置以及脚本目录
      │ ├── cfg
      │ ├── BoardConfig_IPC
      │ ├── BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico-IPC.mk
      │ ├── BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico_Mini_A-IPC.mk
      │ ├── BoardConfig-SPI_NAND-NONE-RV1103_Luckfox_Pico_Mini_B-IPC.mk
      │ ├── BoardConfig-SPI_NAND-NONE-RV1103_Luckfox_Pico_Plus-IPC.mk
      │ ├── BoardConfig-SPI_NAND-NONE-RV1106_Luckfox_Pico_Pro_Max-IPC.mk
      │ └── ...
      ├── output -------------------------- SDK编译后镜像文件存放目录
      ├── docs ---------------------------- SDK文档目录
      └── tools --------------------------- 烧录镜像打包工具以及烧录工具

      其中,RK_KERNEL_DTS 指定了内核的设备树文件。我们以 Luckfox Pico 为例,打开 BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico-IPC.mk 文件,可以看到 RK_KERNEL_DTS 指向的文件为 rv1103g-luckfox-pico.dts

    • 设备树文件路径

      根据RK_KERNEL_DTS,可以确定 Luckfox Pico 的设备树文件路径如下:

      <SDK目录>/sysdrv/source/kernel/arch/arm/boot/dts/rv1103g-luckfox-pico.dts
  2. 开启PWM0_M0

    在设备树中,开启PWM0_M0的代码片段如下所示
    image

  3. 开启PWM0_M1

    如果我们想要使用 PWM0_M1 进行输出,首先我们需要取消注释 PWM0_M1 的配置语句(请查看第201行代码)
    image

    根据接口图可知,PWM0_M1(GPIO1_D2_d) 默认功能为 I2C3 功能,所以我们需要在设备树中注释 I2C3 的代码片段
    image

5.2 编译内核

  1. 编译选择分支,分别是指定 LuckFox Pico 、LuckFox Pico Mini A 、LuckFox Pico Mini B、LuckFox Pico Plus 和 LuckFox Pico Pro/Max

    luckfox@luckfox:~/luckfox-pico$ ./build.sh lunch
    ls: cannot access 'BoardConfig*.mk': No such file or directory

    You're building on Linux
    Lunch menu...pick a combo:

    BoardConfig-*.mk naming rules:
    BoardConfig-"启动介质"-"电源方案"-"硬件版本"-"应用场景".mk
    BoardConfig-"boot medium"-"power solution"-"hardware version"-"applicaton".mk

    ----------------------------------------------------------------
    0. BoardConfig_IPC/BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico-IPC.mk
    boot medium(启动介质): EMMC
    power solution(电源方案): NONE
    hardware version(硬件版本): RV1103_Luckfox_Pico
    applicaton(应用场景): IPC
    ----------------------------------------------------------------

    ----------------------------------------------------------------
    1. BoardConfig_IPC/BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico_Mini_A-IPC.mk
    boot medium(启动介质): EMMC
    power solution(电源方案): NONE
    hardware version(硬件版本): RV1103_Luckfox_Pico_Mini_A
    applicaton(应用场景): IPC
    ----------------------------------------------------------------

    ----------------------------------------------------------------
    2. BoardConfig_IPC/BoardConfig-SPI_NAND-NONE-RV1103_Luckfox_Pico_Mini_B-IPC.mk
    boot medium(启动介质): SPI_NAND
    power solution(电源方案): NONE
    hardware version(硬件版本): RV1103_Luckfox_Pico_Mini_B
    applicaton(应用场景): IPC
    ----------------------------------------------------------------

    ----------------------------------------------------------------
    3. BoardConfig_IPC/BoardConfig-SPI_NAND-NONE-RV1103_Luckfox_Pico_Plus-IPC.mk
    boot medium(启动介质): SPI_NAND
    power solution(电源方案): NONE
    hardware version(硬件版本): RV1103_Luckfox_Pico_Plus
    applicaton(应用场景): IPC
    ----------------------------------------------------------------

    ----------------------------------------------------------------
    4. BoardConfig_IPC/BoardConfig-SPI_NAND-NONE-RV1106_Luckfox_Pico_Pro_Max-IPC.mk
    boot medium(启动介质): SPI_NAND
    power solution(电源方案): NONE
    hardware version(硬件版本): RV1106_Luckfox_Pico_Pro_Max
    applicaton(应用场景): IPC
    ----------------------------------------------------------------

    Which would you like? [0]: 0
    [build.sh:info] switching to board: /home/luckfox/luckfox-pico/project/cfg/BoardConfig_IPC/BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico-IPC.mk
    [build.sh:info] Running build_select_board succeeded.
  2. 重新编译内核

    luckfox@luckfox:~/Luckfox-Pico/luckfox-pico$ ./build.sh kernel

5.3 重新烧录固件

  1. 内核编译成功后生成的文件在 <SDK目录>output/image 目录下
    image
  2. 替换原固件中的 boot.imageenv.txt 文件
    image
  3. 重新创建SD(Luckfox Pico Plus 可只修改相应的分区)
    image