嵌入式Linux C应用编程——输出PWM
PWM 应用编程
概述
PWM(Pulse Width Modulation,脉冲宽度调制)是一种通过调节脉冲的宽度(占空比)来实现模拟信号控制的技术,常用于 LED 调光、电机调速、蜂鸣器控制等场景。
本文学习如何在应用层通过 sysfs 文件系统 操控 I.MX6U 开发板上的 PWM 外设。
应用层如何操控 PWM
与 LED、GPIO 设备一样,PWM 也是通过 sysfs 方式 进行操控,对应的路径为 /sys/class/pwm。
/sys/class/pwm 目录结构
进入 /sys/class/pwm 目录,可以看到 8 个以 pwmchipX 命名的文件夹(X 为数字 0~7):
/sys/class/pwm/
├── pwmchip0/ → PWM1
├── pwmchip1/ → PWM2
├── pwmchip2/ → PWM3
├── pwmchip3/ → PWM4
├── pwmchip4/ → PWM5
├── pwmchip5/ → PWM6
├── pwmchip6/ → PWM7
└── pwmchip7/ → PWM8
I.MX6U 共有 8 个 PWM 控制器,每个控制器只有 1 路 PWM 输出。
注意: 开发板出厂系统中,PWM1(pwmchip0)已被用作 LCD 背光控制,应用层不能直接对它进行控制。其它 PWM 因 I/O 资源不足,出厂系统中均不能使用。如需测试 PWM,需要先禁用 LCD 和 backlight 设备(见下文测试章节)。
pwmchipX 目录下的属性文件
以 pwmchip0 为例,其目录下包含三个关键属性文件:
/sys/class/pwm/pwmchip0/
├── export # 只写:导出 PWM(写入编号 0)
├── npwm # 只读:读取该控制器下有多少路 PWM 输出
└── unexport # 只写:删除已导出的 PWM(写入编号 0)
npwm(只读)
读取可得知该 PWM 控制器下共有几路 PWM 输出:
cat /sys/class/pwm/pwmchip0/npwm
# 输出: 1
I.MX6U 每个 PWM 控制器只有 1 路,所以读取结果均为 1。
export(只写,无读权限)
在使用 PWM 之前,需要先将其导出:
# 导出 PWM1(pwmchip0 下的编号 0)
echo 0 > /sys/class/pwm/pwmchip0/export
# 导出 PWM2
echo 0 > /sys/class/pwm/pwmchip1/export
# 导出 PWM3
echo 0 > /sys/class/pwm/pwmchip2/export
注意: 每个
pwmchipX下的编号都从 0 开始。因为 I.MX6U 每个控制器只有一路 PWM,所以都只能使用编号 0。
导出成功后,会在 pwmchipX 目录下生成一个名为 pwm0 的子目录。
unexport(只写,无读权限)
使用完 PWM 后,需要将导出的 PWM 删除:
echo 0 > /sys/class/pwm/pwmchip0/unexport
写入 unexport 的编号必须与导出时写入 export 的编号对应。因为 I.MX6U 每个控制器只有一路 PWM,所以编号只能是 0。
pwm0 目录下的属性文件
导出后生成的 pwm0 目录包含以下属性文件:
/sys/class/pwm/pwmchipX/pwm0/
├── enable # 可读可写:使能/禁止 PWM
├── period # 可读可写:配置 PWM 周期(单位:ns)
├── duty_cycle # 可读可写:配置 PWM 占空比(单位:ns)
└── polarity # 可读可写:配置 PWM 极性(可选,部分 SoC 不支持)
enable
写入 "0" 禁止 PWM 输出,写入 "1" 使能 PWM 输出。通常先配置好 period 和 duty_cycle,最后再使能 PWM。
echo 0 > enable # 禁止 PWM 输出
echo 1 > enable # 使能 PWM 输出
period
配置 PWM 周期,以 纳秒(ns) 为单位。
# 配置 PWM 周期为 10us(10 × 1000ns)
echo 10000 > period
duty_cycle
配置 PWM 占空比,以 纳秒(ns) 为单位。
# 配置占空比为 5us(占空比 50%,周期 10us)
echo 5000 > duty_cycle
polarity
设置 PWM 极性,可写入的值:
| 值 | 说明 |
|---|---|
"normal" |
普通极性(默认) |
"inversed" |
极性反转 |
echo normal > polarity # 默认极性
echo inversed > polarity # 极性反转
注意: 很多 SoC 的 PWM 外设在硬件上并不支持极性配置,驱动中未实现此接口,因此应用层无法通过
polarity属性文件配置极性。ALPHA/Mini I.MX6U 开发板出厂系统便是如此。
编写应用程序
程序功能
通过命令行传参接收 PWM 编号、周期和占空比,然后:
- 导出 PWM
- 配置周期
- 配置占空比
- 使能 PWM
完整代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
static char pwm_path[100];
static int pwm_config(const char *attr, const char *val)
{
char file_path[100];
int len;
int fd;
sprintf(file_path, "%s/%s", pwm_path, attr);
if (0 > (fd = open(file_path, O_WRONLY))) {
perror("open error");
return fd;
}
len = strlen(val);
if (len != write(fd, val, len)) {
perror("write error");
close(fd);
return -1;
}
close(fd); // 关闭文件
return 0;
}
int main(int argc, char *argv[])
{
/* 校验传参 */
if (4 != argc) {
fprintf(stderr, "usage: %s <id> <period> <duty>\n",
argv[0]);
exit(-1);
}
/* 打印配置信息 */
printf("PWM config: id<%s>, period<%s>, duty<%s>\n",
argv[1], argv[2],
argv[3]);
/* 导出 pwm */
sprintf(pwm_path, "/sys/class/pwm/pwmchip%s/pwm0", argv[1]);
if (access(pwm_path, F_OK)) { // 如果 pwm0 目录不存在,则导出
char temp[100];
int fd;
sprintf(temp, "/sys/class/pwm/pwmchip%s/export", argv[1]);
if (0 > (fd = open(temp, O_WRONLY))) {
perror("open error");
exit(-1);
}
if (1 != write(fd, "0", 1)) { // 导出 pwm
perror("write error");
close(fd);
exit(-1);
}
close(fd); // 关闭文件
}
/* 配置 PWM 周期 */
if (pwm_config("period", argv[2]))
exit(-1);
/* 配置占空比 */
if (pwm_config("duty_cycle", argv[3]))
exit(-1);
/* 使能 pwm */
pwm_config("enable", "1");
/* 退出程序 */
exit(0);
}
代码解析
-
pwm_config()辅助函数- 接收属性名和值,拼装完整路径
- 以只写方式打开属性文件
- 写入配置值后关闭文件
- 简化了多次配置的重复操作
-
main()函数流程- 校验传参:需要 3 个参数——
<id>:PWM 控制器编号(0、1、2、3…,分别对应 PWM1、PWM2、PWM3…)<period>:PWM 周期(单位 ns)<duty>:PWM 占空比(单位 ns)
- 导出 PWM:通过
access()判断pwm0目录是否存在- 存在 → 已导出,跳过
- 不存在 → 向
export文件写入"0"完成导出
- 配置周期:调用
pwm_config("period", argv[2]) - 配置占空比:调用
pwm_config("duty_cycle", argv[3]) - 使能 PWM:调用
pwm_config("enable", "1")
- 校验传参:需要 3 个参数——
编译
# 交叉编译(ARM 架构)
arm-linux-gnueabihf-gcc -o testApp pwm.c
运行
# 配置 PWM1,周期 500us(500000ns),占空比 250us(50%)
./testApp 0 500000 250000
在开发板上测试
开发板出厂系统中 PWM1 被 LCD 背光占用,需要先将其释放。有两种方式:
- 修改内核源码:配置内核、修改设备树,禁用 LCD 和 backlight 设备后重新编译
- u-boot 临时修改(推荐):在 u-boot 命令行中修改内存中的设备树,重启后失效
u-boot 临时修改方法
步骤一:重启开发板,进入 u-boot 命令行模式
开机时按下任意键进入 u-boot 命令行。

步骤二:设置环境变量(禁用 LCD 和 backlight)
SD/eMMC 启动方式:
# 定义禁用 LCD 和 backlight 的命令
setenv disable_lcd 'fdt addr ${fdt_addr}; fdt set /backlight status disable; fdt set /soc/aips-bus@02100000/lcdif@021c8000 status disable'
# 修改 mmcboot 命令,在启动内核前先执行 disable_lcd
setenv mmcboot 'echo Booting from mmc ...; run mmcargs; if test ${boot_fdt} = yes || test ${boot_fdt} = try; then if run loadfdt; then run disable_lcd; bootz ${loadaddr} - ${fdt_addr}; else if test ${boot_fdt} = try; then bootz; else echo WARN: Cannot load the DT; fi; fi; else bootz; fi;'
NAND 启动方式:
setenv disable_lcd 'fdt addr ${fdt_addr}; fdt set /backlight status disable; fdt set /soc/aips-bus@02100000/lcdif@021c8000 status disable'
setenv bootcmd 'nand read ${loadaddr} 0x620000 0x800000;nand read ${fdt_addr} ${fdt_offset} 0x20000; run disable_lcd; bootz ${loadaddr} - ${fdt_addr}'

步骤三:启动内核
boot

这种修改方式只对 本次启动 生效,因为修改的是内存中的设备树文件。重启开发板后会恢复到未修改状态。
步骤四:拷贝并运行测试程序
系统启动后,LCD 已被禁用,PWM1 已腾出。将编译好的可执行文件拷贝到开发板并运行:
# 运行测试程序
./testApp 0 500000 250000
观察 PWM 波形
PWM1 的输出引脚绑定到 GPIO1_IO08(即 LCD 背光引脚),该引脚已通过开发板扩展口引出。使用示波器检测 GPIO1_IO08 引脚,可观察到 PWM 波形。
总结
PWM 应用编程的核心思路与 GPIO 类似——通过 sysfs 文件系统接口,以读写文件的方式操控硬件外设。

浙公网安备 33010602011771号