文章目录
- 1- PWM介绍
- 2- PWM使能
- (1)添加配置
- (2)export、unexport与npwm属性文件
- (3)duty_cycle、enable和period属性文件
- 3- PWM测试编程
- (1)PWM8管脚连接
- (2)流程介绍及源码
- (3)运行结果
- 4- pwm编程实现音乐播放
1- PWM介绍
PWM(Pulse Width Modulation),是脉冲宽度调制缩写,它是通过对一系列脉冲的宽度进行调制,等效出所需要的波形(包含形状以及幅值),对模拟信号电平进行数字编码,也就是说通过调节占空比的变化来调节信号、能量等的变化。
高电平占多一点,也是占空比大一点亮度就亮一点,占空比小一点亮度就没有那么亮,前提是PWM的频率要大于我们人眼识别频率(大约80Hz以上最好)。在电机驱动、无源蜂鸣器驱动、LCD屏幕背光调节、逆变电路等都会有应用。
我们来介绍一下PWM中涉及到的参数:
-
PWM的频率:
是指1秒钟内信号从高电平到低电平再回到高电平的次数(如下图一个周期),即一秒钟PWM有多少个周期。50H表示1s有50个周期。 -
PWm周期:
周期=1/频率,50Hz = 1/50 s = 0.02s = 20ms ,如果频率为50Hz ,也就是说一个周期是20ms那么一秒钟就有 50次PWM周期。 -
占空比:
是一个脉冲周期内,高电平(如下图的脉宽时间)的时间与整个周期时间的比例(百分数表示),如下图脉宽时间占总周期时间的比例,即占空比。
2- PWM使能
在开发板原理图我们可以看见40pin扩展口的PWM7和PWM8,实际上开发板上有4路PWM,如下:
PWM1 ---> backlight //LCD背光
PWM2 ---> beep //蜂鸣器
PWM7,PWM8 ---> 40pin扩展 //需要使能开启
(1)添加配置
我们需要修改相关配置文件才能使用40pin扩展口的PWM7和PWM8,如下修改(添加)配置文件,添加两个管脚的PWM支持,然后关机重启即可。
root@igkboard:~# vi /run/media/mmcblk1p1/config.txt
# Enable PWM overlays, PWM8 conflict with UART8(NB-IoT/4G module)
dtoverlay_pwm=7 8
(2)export、unexport与npwm属性文件
PWM 同样也是通过 sysfs 方式进行操控,进入到/sys/class/pwm 目录下,可以看到四个pwmchip?命名的文件夹,这4个文件夹其实就对应了IMX6ULL的4个PWM控制器(分别对应pwm1,pwm2,pwm7,pwm8):
root@igkboard:/sys/class/pwm# ls
pwmchip0 pwmchip1 pwmchip2 pwmchip3
进入pwmchip1查看文件我们发现三个属性文件。
- npwm:这是一个只读属性,读取该文件可以得知该PWM控制器下共有几路PWM输出,如下所示:
root@igkboard:/sys/class/pwm/pwmchip1# cat npwm
1
- export:与GPIO控制一样,在使用PWM之前,也需要将其导出,通过export属性进行导出,以下所示(可以看见多出来了pwm0,注意导出的编号(echo 0)必须小于npwm(1)的值)
root@igkboard:/sys/class/pwm/pwmchip1# echo 0 > export
root@igkboard:/sys/class/pwm/pwmchip1# ls
consumers device export npwm power pwm0 subsystem suppliers uevent
unexport
- uexport:unexport:将导出的PWM删除。当使用完PWM之后,我们需要将导出的PWM删除,如下所示(可以看见pwm0不见了)
root@igkboard:/sys/class/pwm/pwmchip1# echo 0 > unexport
root@igkboard:/sys/class/pwm/pwmchip1# ls
consumers device export npwm power subsystem suppliers uevent unexport
(3)duty_cycle、enable和period属性文件
我们进入pwm0可以看见duty_cycle、enable和period三个文件属性:
- enable:可读可写,写入"0"表示禁止PWM;写入"1"表示使能PWM。读取该文件获取PWM当前是禁止还是使能状态。
echo 0 > enable #禁止PWM输出
echo 1 > enable #使能PWM输出
- period:用于配置PWM周期,可读可写;写入一个字符串数字值,以ns(纳秒)为单位,譬如配
置PWM周期为10us(微秒)。
echo 10000 > period #PWM周期设置为10us(10 * 1000ns)
- duty_cycle:用于配置PWM的占空比,可读可写;写入一个字符串数字值,同样也是以ns为单位。
echo 5000 > duty_cycle #PWM占空比设置为5us
3- PWM测试编程
(1)PWM8管脚连接
(2)流程介绍及源码
我们先来梳理一下流程:
- 获取终端输入的字符串,拼接字符串/sys/class/pwm/pwmchip/pwm0(?就是终端需要输入的访问哪一个PWM控制器)
- 判断上述路径文件是否存在,存在则修改其中的duty_cycle、enable和period文件属性,不存在就需要打开export属性文件写入0,再打开pwm0修改文件属性
- 修改文件属性自定义函数,就是打开文件写入即可
源码:
一些不常见的函数都有注释帮助了解。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
char pwm_path[100];
static int pwm_config(const char *attr, const char *val);
int main(int argc, char *argv[])
{
char temp[100];
int fd = -1;//文件描述符
/*运行的时候需要输入四个参数:执行文件,pwmchip?,周期,占空比*/
if(4 != argc)
{
printf("Please enter four parameters: %s <id> <period> <duty>\n", argv[0]);
exit(-1);//进程正常退出是exit(0),异常退出是exit(非0)
}
printf("Pwm config: id<%s> period<%s> duty<%s>\n", argv[1],argv[2],argv[3]);
/*接下来就是打开文件执行相关命令*/
memset(pwm_path, 0, sizeof(pwm_path));//有数组使用之前都需要memset一下
snprintf(pwm_path, sizeof(pwm_path), "/sys/class/pwm/pwmchip%s/pwm0", argv[1]);
/*如果pwm_path不能打开,export:与GPIO控制一样,在使用PWM之前,也需要将其导出,通过export属性进行导出*/
/*
int access(const char *pathname,int mode)参数:
pathname:表示要测试的文件的路径
mode:表示测试的模式可能的值有:
R_OK:是否具有读权限
W_OK:是否具有可写权限
X_OK:是否具有可执行权限
F_OK:文件是否存在
返回值:若测试成功则返回0,否则返回-1
*/
if(access(pwm_path, F_OK))
{
memset(temp, 0, sizeof(temp));
snprintf(temp, sizeof(temp), "/sys/class/pwm/pwmchip%s/export", argv[1]);
fd = open(temp, O_WRONLY);
if(fd < 0)
{
printf("open pwmchip%s error: %s\n", argv[1], strerror(errno));
exit(-1);
}
/*写入文件0,1个字节*/
if(1 != write(fd, "0", 1))
{
printf("write '0' to pwmchip%s/export error\n", argv[1]);
close(fd);
exit(-1);
}
printf("export Failed to export\n");
close(fd);
}
/*配置pwm周期*/
if(pwm_config("period", argv[2]))
{
//printf("Configuration cycle failure.\n");
exit(-1);
}
/*配置占空比*/
if(pwm_config("duty_cycle", argv[3]))
{
//printf("Failed to configure the duty cycle.\n");
exit(-1);
}
/*使能pwm*/
pwm_config("enable", "1");
return 0;
}
/*pwm配置函数,attr:属性文件名字,val:属性的值*/
static int pwm_config(const char *attr, const char *val)
{
char file_path[100];
int fd = -1;
int len;
/*判断参数*/
if(attr == NULL || val == NULL)
{
printf("[%s] argument error\n", __FUNCTION__);
return -1;
}
memset(file_path, 0, sizeof(file_path));
snprintf(file_path, sizeof(file_path), "%s/%s", pwm_path, attr);
fd = open(file_path, O_WRONLY);
if(fd < 0)
{
printf("[%s] open %s error\n", __FUNCTION__, file_path);
return fd;
}
len = strlen(val);
/*如果顺利write()会返回实际写入的字节数*/
if(len != write(fd, val, len))
{
printf("[%s] write %s to %s error\n", __FUNCTION__, val, file_path);
close(fd);
return -2;
}
close(fd);
return 0;
}
Makefile文件:
CC=arm-linux-gnueabihf-gcc
APP_NAME=pwm_test
all:clean
@${CC} ${APP_NAME}.c -o ${APP_NAME}
clean:
@rm -f ${APP_NAME}
make之后生成了ARM架构上运行的文件了。
wangdengtao@wangdengtao-virtual-machine:~/wangdengtao/tftpboot$ make
wangdengtao@wangdengtao-virtual-machine:~/wangdengtao/tftpboot$ file pwm_test
pwm_test: ELF 32-bit LSB pie executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, BuildID[sha1]=c8161112e18f74d738f5e089f49ad0594037e73f, for GNU/Linux 3.2.0, not stripped
(3)运行结果
tftp服务器不会搭建的可以参考这篇文章:wpa_supplicant无线网络配置imx6ull以及搭建tftp服务器
tftp服务器下载文件到开发板:
root@igkboard:~# tftp -gr pwm_test 192.168.0.134
root@igkboard:~# chmod a+x pwm_test
运行(pwmchip3(PWM8)):
root@igkboard:~# ./pwm_test 3 10000 1000
PWM config: id<3>, period<10000>, duty<1000>
root@igkboard:~# ./pwm_test 3 10000 3000
PWM config: id<3>, period<10000>, duty<3000>
root@igkboard:~# ./pwm_test 3 10000 7000
PWM config: id<3>, period<10000>, duty<7000>
root@igkboard:~# ./pwm_test 3 10000 9000
PWM config: id<3>, period<10000>, duty<9000>
可以发现我们的LED灯越来越亮。
运行(pwmchip1( PWM1蜂鸣器)):
root@igkboard:~# ./pwm_test 1 10000 5000
PWM config: id<1>, period<10000>, duty<5000>
root@igkboard:~# ./pwm_test 1 10000 0
PWM config: id<1>, period<10000>, duty<0>
可以听到蜂鸣器一直在叫,占空比为0的时候就不会叫了,人多的时候还是别放了,挺吵的。
4- pwm编程实现音乐播放
我们先来熟悉一下流程:
- 在打开pwm0的基础上我们继续实现。
- 首先我们需要了解乐谱的相关信息,将低、中、高音频率分组方便调节,相当于不同的音调。如果是高音音调,只需要将上面的频率乘以 2,如果是低音音调,需要将上面的音频除以2。
- 我们想要连续播放,就需要在一个循环中不断改变频率,于是需要数组,保存我们需要演奏的曲子乐谱,乐谱数组中就是我们的音调。当然还需要注意休眠的时间,ms级函数的实现。
源码(在第一个代码的基础上添加修改)
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>
typedef struct pwm_one_s
{
unsigned int msec;//持续的时间,单位毫秒
unsigned int freq;//频率
unsigned char duty;//占空比,百分数*100
}pwm_one_t;
#define CX CM
static const unsigned short CL[8] = {0, 131, 147, 165, 175, 196, 211, 248};
static const unsigned short CM[8] = {0, 262, 294, 330, 350, 393, 441, 495};
static const unsigned short CH[8] = {0, 525, 589, 661, 700, 786, 882, 990};
static unsigned short songP[] =
{
CX[1], CX[5], CX[5], CX[5], CX[6], CX[6], CX[5], CX[0],
CX[4], CX[4], CX[3], CX[3], CX[2], CX[2], CX[1], CX[0],
CX[5], CX[5], CX[4], CX[4], CX[3], CX[3], CX[2], CX[0],
};
static char pwm_path[100];
void mssleep(unsigned long ms);//毫秒级函数的实现
static int pwm_config(const char *attr, const char *val);//pwm配置函数
static int pwm_ring_one(pwm_one_t *pwm_ring);
int main(int argc, char *argv[])
{
char temp[100];
int fd = -1;//文件描述符
pwm_one_t pwm_one_test;
/*运行的时候需要输入四个参数:执行文件,pwmchip?,周期,占空比*/
if(2 != argc)
{
printf("Please enter two parameters: %s <id>\n", argv[0]);
exit(-1);//进程正常退出是exit(0),异常退出是exit(非0)
}
//printf("Pwm config: id<%s> period<%s> duty<%s>\n", argv[1],argv[2],argv[3]);
/*接下来就是打开文件执行相关命令*/
memset(pwm_path, 0, sizeof(pwm_path));//有数组使用之前都需要memset一下
snprintf(pwm_path, sizeof(pwm_path), "/sys/class/pwm/pwmchip%s/pwm0", argv[1]);
/*如果pwm_path不能打开,export:与GPIO控制一样,在使用PWM之前,也需要将其导出,通过export属性进行导出*/
/*
int access(const char *pathname,int mode)参数:
pathname:表示要测试的文件的路径
mode:表示测试的模式可能的值有:
R_OK:是否具有读权限
W_OK:是否具有可写权限
X_OK:是否具有可执行权限
F_OK:文件是否存在
返回值:若测试成功则返回0,否则返回-1
*/
if(access(pwm_path, F_OK))
{
memset(temp, 0, sizeof(temp));
snprintf(temp, sizeof(temp), "/sys/class/pwm/pwmchip%s/export", argv[1]);
fd = open(temp, O_WRONLY);
if(fd < 0)
{
printf("open pwmchip%s error: %s\n", argv[1], strerror(errno));
exit(-1);
}
/*写入文件0,1个字节*/
if(1 != write(fd, "0", 1))
{
printf("write '0' to pwmchip%s/export error\n", argv[1]);
close(fd);
exit(-1);
}
close(fd);
}
pwm_one_test.freq = 1000;
pwm_one_test.duty = 50;
pwm_one_test.msec = 100;
pwm_config("enable","1");
for(int i=0; i<(sizeof(songP)/sizeof(songP[0])); i++)
{
if(songP[i] == 0)
{
pwm_one_test.duty = 0;
}
else
{
pwm_one_test.duty = 15;
pwm_one_test.freq = songP[i];
}
pwm_one_test.msec = 300;
pwm_ring_one(&pwm_one_test);//这才是关键的函数
}
pwm_config("enable", "0");
return 0;
}
static int pwm_ring_one(pwm_one_t *pwm_ring)
{
unsigned long period = 0;
unsigned long duty_cycle = 0;
char period_str[20] = {};
char duty_cycle_str[20] = {};
/*占空比不可能大于100(0.1*100)*/
if(!pwm_ring || pwm_ring->duty > 100)
{
printf("[INFO] %s argument error.\n", __FUNCTION__);
return -1;
}
period = (unsigned long)((1.f / (double)pwm_ring -> freq)*1e9);//周期=1/频率(s)*1000 000 000ns
duty_cycle = (unsigned long) (((double)pwm_ring->duty / 100.f)*(double)period);//15/100=15%,15%*一个周期 = 占空比(ns)
snprintf(period_str, sizeof(period_str), "%ld", period);
snprintf(duty_cycle_str, sizeof(duty_cycle_str), "%ld", duty_cycle);
printf("period: %sns, duty_cycle_str: %sns\n", period_str, duty_cycle_str);
if(pwm_config("period", period_str))
{
printf("pwm_config period failure.\n");
return -1;
}
if(pwm_config("duty_cycle", duty_cycle_str))
{
printf("pwm_config duty_cycle failure.\n");
return -2;
}
mssleep(pwm_ring->msec);
if(pwm_config("duty_cycle", "0"))
{
printf("pwm_config duty_cycle failure.\n");
}
mssleep(20);
}
/*毫秒级函数的实现*/
void mssleep(unsigned long ms)
{
struct timespec ts = {
.tv_sec = (long int) (ms / 1000),
.tv_nsec = (long int) (ms % 1000) * 1000000ul
};
nanosleep(&ts, 0);
}
/*pwm配置函数,attr:属性文件名字,val:属性的值*/
static int pwm_config(const char *attr, const char *val)
{
char file_path[100];
int fd = -1;
int len;
/*判断参数*/
if(attr == NULL || val == NULL)
{
printf("[%s] argument error\n", __FUNCTION__);
return -1;
}
memset(file_path, 0, sizeof(file_path));
snprintf(file_path, sizeof(file_path), "%s/%s", pwm_path, attr);
fd = open(file_path, O_WRONLY);
if(fd < 0)
{
printf("[%s] open %s error\n", __FUNCTION__, file_path);
return fd;
}
len = strlen(val);
/*如果顺利write()会返回实际写入的字节数*/
if(len != write(fd, val, len))
{
printf("[%s] write %s to %s error\n", __FUNCTION__, val, file_path);
close(fd);
return -2;
}
close(fd);
return 0;
}
root@igkboard:~# tftp -gr pwm_music 192.168.0.134
root@igkboard:~# chmod a+x pwm_music
root@igkboard:~# ./pwm_music 1
period: 3816793ns, duty_cycle_str: 572518ns
period: 2544529ns, duty_cycle_str: 381679ns
period: 2544529ns, duty_cycle_str: 381679ns
period: 2544529ns, duty_cycle_str: 381679ns
period: 2267573ns, duty_cycle_str: 340135ns
period: 2267573ns, duty_cycle_str: 340135ns
period: 2544529ns, duty_cycle_str: 381679ns
period: 2544529ns, duty_cycle_str: 0ns
period: 2857142ns, duty_cycle_str: 428571ns
period: 2857142ns, duty_cycle_str: 428571ns
period: 3030303ns, duty_cycle_str: 454545ns
period: 3030303ns, duty_cycle_str: 454545ns
period: 3401360ns, duty_cycle_str: 510204ns
period: 3401360ns, duty_cycle_str: 510204ns
period: 3816793ns, duty_cycle_str: 572518ns
period: 3816793ns, duty_cycle_str: 0ns
period: 2544529ns, duty_cycle_str: 381679ns
period: 2544529ns, duty_cycle_str: 381679ns
period: 2857142ns, duty_cycle_str: 428571ns
period: 2857142ns, duty_cycle_str: 428571ns
period: 3030303ns, duty_cycle_str: 454545ns
period: 3030303ns, duty_cycle_str: 454545ns
period: 3401360ns, duty_cycle_str: 510204ns
period: 3401360ns, duty_cycle_str: 0ns
还是Makefile编写之后编译成ARM架构上的文件,然后tftp服务器下载到开发板,给予可执行权限,运行就可以听见小星星的曲子了。