版权声明

本文档所有内容文字资料由凌云实验室郭工编著,主要用于凌云嵌入式Linux教学内部使用,版权归属作者个人所有。任何媒体、网站、或个人未经本人协议授权不得转载、链接、转帖或以其他方式复制发布/发表。已经授权的媒体、网站,在下载使用时必须注明来源,违者本人将依法追究责任。

  • Copyright (C) 2022 凌云物网智科实验室·郭工

  • Author: Guo Wenxue Email: guowenxue@gmail.com QQ: 281143292

wechat_pub

2.5 PWM编程蜂鸣器编程控制

2.5.1 PWM概述

PWM(Pulse Width Modulation),是脉冲宽度调制缩写,它是通过对一系列脉冲的宽度进行调制,等效出所需要的波形(包含形状以及幅值),对模拟信号电平进行数字编码,也就是说通过调节占空比的变化来调节信号、能量等的变化,占空比就是指在一个周期内,信号处于高电平的时间占据整个信号周期的百分比,例如方波的占空比就是50%。PWM是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。PWM信号一般下图所示

PWM_example_1

​ PWM常见的应用场合

经常见到的就是交流调光电路(手机充电的呼吸灯),也可以说是无级调速,高电平占多一点,也就是占空比大一点亮度就亮一点,占空比小一点亮度就没有那么亮,前提是PWM的频率要大于我们人眼识别频率(大约80Hz以上最好)。在电机驱动、无源蜂鸣器驱动、LCD屏幕背光调节、逆变电路等都会有应用.

2.5.2 PWM的参数说明

  • PWM的频率

是指1秒钟内信号从高电平到低电平再回到高电平的次数(一个周期),即一秒钟PWM有多少个周期。 单位: Hz 表示方式: 50Hz 100Hz

  • PWM的周期:

周期=1/频率 50Hz = 1/50 s = 0.02s = 20ms ,如果频率为50Hz ,也就是说一个周期是20ms 那么一秒钟就有 50次PWM周期。

  • 占空比

是一个脉冲周期内,高电平的时间与整个周期时间的比例 单位: % (0%-100%) 表示方式:20%

使用下面示例图进行参考,知晓各个参数在实际PWM信号中表达的含义

PWM_example_2

周期: 一个脉冲信号的时间 1s内测周期次数等于频率

脉宽时间: 高电平时间

脉宽时间占总周期时间的比例,即占空比

2.5.3 应用层操控PWM

2.5.3.1 驱动配置使用说明

查看开发板底板原理图和其40pin扩展口可以知道,开发板上可以使用的有4路PWM,其中

PWM1 ---> backlight //LCD背光
PWM2 ---> beep 		//蜂鸣器
PWM7,PWM8 ---> 40pin扩展 //需要使能开启

想要使能40pin扩展口的PWM7和8的话,需要修改开发板上的DTOverlay配置文件,添加两个管脚的PWM支持。

root@igkboard:~# vi /run/media/mmcblk1p1/config.txt 

# Enable PWM overlays, PWM8 conflict with UART8(NB-IoT/4G module)
dtoverlay_pwm=7 8

修改完成后重启系统,和之前sysfs操控gpio的方式一样,PWM 同样也是通过 sysfs 方式进行操控,进入到/sys/class/pwm 目录下,可以看到四个以pwmchipX(X表示数字0~3)命名的文件夹,这4个文件夹其实就对应了I.MX6U的4个PWM控制器,,其实I.MX6U总共有8个PWM控制器,大家可以通过查询I.MX6U参考手册得知。由于我们只使能了四个,所以只看到四个控制器文件夹。

root@igkboard:/sys/class/pwm# ls
pwmchip0  pwmchip1  pwmchip2  pwmchip3

易知其对应方式是如下所示

pwmchip0  --->  pwm1
pwmchip1  --->  pwm2
pwmchip2  --->  pwm7
pwmchip3  --->  pwm8

2.5.3.2 pwmchip属性简介

由于 pwm1 被LCD背光驱动占用,我们以另外三个中的一个作为示例测试,进入pwmchip1(pwm2 物理连接板载蜂鸣器)文件夹下

root@igkboard:/sys/class/pwm# cd pwmchip1/
root@igkboard:/sys/class/pwm/pwmchip1# ls
consumers  device  export  npwm  power  subsystem  suppliers  uevent  unexport

只需要重点关注三个属性文件,export、npwm以及unexport ,下面一一介绍:

  • npwm:这是一个只读属性,读取该文件可以得知该PWM控制器下共有几路PWM输出,如下所示:

root@igkboard:/sys/class/pwm/pwmchip1# cat npwm 
1
  • export:与GPIO控制一样,在使用PWM之前,也需要将其导出,通过export属性进行导出,以下所示:

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

导出之后,pwmchip1文件夹下生成一个名为pwm0的文件夹,稍后介绍。注意导出的编号(echo 0)必须小于npwm(1)的值

  • unexport:将导出的PWM删除。当使用完PWM之后,我们需要将导出的PWM删除,如下所示

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

写入到unexport文件中的编号与写入到export文件中的编号是相对应的;需要注意的是,export文件和unexport文件都是只写的、没有读权限。

2.5.3.3 如何控制PWM

上文成功在pwmchip1中导出pwm0文件夹后,我们进入pwm0文件夹下后看看:

root@igkboard:/sys/class/pwm/pwmchip1/pwm0# ls
capture  consumers  duty_cycle  enable  period  polarity  power  suppliers  uevent

该目录下也有一些属性文件,我们重点关注duty_cycle、enable、period以及polarity这四个属性文件,接下来一一进行介绍。

  • enable:可读可写,写入”0”表示禁止PWM;写入”1”表示使能PWM。读取该文件获取PWM当前是禁止还是使能状态。

echo 0 > enable #禁止PWM输出 
echo 1 > enable #使能PWM输出
  • polarity:用于设置极性,可读可写,可写入的值如下。

echo normal > polarity #默认极性 
echo inversed > polarity #极性反转

很多SoC的PWM外设其硬件上并不支持极性配置,所以对应的驱动程序中并未实现这个接口,应用层自然也就无法通过polarity属性文件对PWM极性进行配置,IGKBoard开发板系统便是如此。

  • period:用于配置PWM周期,可读可写;写入一个字符串数字值,以**ns(纳秒)**为单位,譬如配置PWM周期为10us(微秒)。

echo 10000 > period #PWM周期设置为10us(10 * 1000ns)
  • duty_cycle:用于配置PWM的占空比,可读可写;写入一个字符串数字值,同样也是以ns为单位.

echo 5000 > duty_cycle #PWM占空比设置为5us

2.5.3.4 PWM测试程序应用编程

下面是PWM测试程序的源码。

guowenxue@ubuntu20:~/igkboard/apps$ vim pwm_test.c
/*********************************************************************************
 *      Copyright:  (C) 2021 Guo Wenxue<Email:guowenxue@gmail.com QQ:281143292>
 *                  All rights reserved.
 *
 *       Filename:  pwm_test.c
 *    Description:  This file used to test PWM 
 *                 
 *        Version:  1.0.0(10/1/2022~)
 *         Author:  Guo Wenxue <guowenxue@gmail.com>
 *      ChangeLog:  1, Release initial version on "10/1/2022 17:46:18 PM"
 *                 
 ********************************************************************************/
#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];

/*pwm 配置函数 attr:属性文件名字 
 *  val:属性的值(字符串)
*/
static int pwm_config(const char *attr, const char *val)
{
    char file_path[100];
    int len;
    int fd =  -1;

    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);
    if (0 > (fd = open(file_path, O_WRONLY))) {
        printf("[%s] open %s error\n", __FUNCTION__, file_path);
        return fd;
    }

    len = strlen(val);
    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;
}

int main(int argc, char *argv[])
{
    char temp[100];
    int fd = -1;

    /* 校验传参 */
    if (4 != argc) {
        printf("usage: %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]);

    /* 导出pwm 首先确定最终导出的文件夹路径*/
    memset(pwm_path, 0, sizeof(pwm_path));
    snprintf(pwm_path, sizeof(pwm_path), "/sys/class/pwm/pwmchip%s/pwm0", argv[1]);
    
    //如果pwm0目录不存在, 则导出
    memset(temp, 0, sizeof(temp));
    if (access(pwm_path, F_OK)) {
        snprintf(temp, sizeof(temp) , "/sys/class/pwm/pwmchip%s/export", argv[1]);
        if (0 > (fd = open(temp, O_WRONLY))) {
            printf("open pwmchip%s error\n", argv[1]);
            exit(-1);
        }
        //导出pwm0文件夹
        if (1 != write(fd, "0", 1)) {
            printf("write '0' to  pwmchip%s/export error\n", argv[1]);
            close(fd);
            exit(-2);
        }

        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");

    return 0;
}

编写Makefile如下

guowenxue@ubuntu20:~/igkboard/apps$ vim Makefile
CC=arm-linux-gnueabihf-gcc
APP_NAME=pwm_test

all:clean
	@${CC} ${APP_NAME}.c -o ${APP_NAME}

clean:
	@rm -f ${APP_NAME}

2.5.3.5 交叉编译测试运行

在ubuntu下的相关源码路径下执行make命令将会编译源码生成ARM开发板上的可执行文件。

guowenxue@ubuntu20:~/igkboard/apps$ make
guowenxue@ubuntu20:~/igkboard/apps$ ls
Makefile  pwm_test  pwm_test.c

现在我们在开发板上通过 tftp 命令 或其它方式将编译生成的测试程序下载到开发板上。

root@igkboard:~# tftp -gr pwm_test 192.168.2.2

将led阳极连接开发板的PWM8管脚上,阴极连接GND。如下图所示连接

igkboard_pwm8_led

实物连接图如下。

igkboard_pwm8_led_real_connect

接下来,我们给该程序加上执行权限并运行。由源码可知,该执行程序需要三个参数,第一个是pwmchip的编号,第二个是周期(单位ns),第三个是占空比(即高电平时间,单位ns)。依次输入以下命令,可以看到LED灯亮度由暗至亮,证明PWM8成功使用。

root@igkboard:~# chmod a+x pwm_test 
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> 

2.5.4 板载蜂鸣器测试

由于蜂鸣器开启后,会产生较大噪声,注意周围环境,以免打扰别人。在开发板依次输入以下命令即可测试。

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>

第一行命令,蜂鸣器会响起,第二行命令,蜂鸣器会关闭,增大或缩小第三个参数,可以让蜂鸣器响起不同频率的声音。

2.5.4.1 板载蜂鸣器播放音乐实战

关于音调基础乐理知识

简单的物理学知识告诉我们,声音的声调高低是有声波的频率决定的,而声音的大小是有声波的振幅决定的。人耳可以听到的声音频率在 20Hz-20000Hz 之间。频率低于 20Hz 的声波被称为次声波,我们听不到。同样,频率高于 20000Hz 的声波称为超声波,我们也听不到。生活中,我们都喜欢听美妙的音乐,音乐是由很多不同的音调和参差的拍子组合而成的。小时候音乐课上常说的 Do,Re,Mi,Fa,So,La,Xi 就是最常见的音调。我们以 C 大调为例,其 7 个音阶与对应的频率如下表所示:

Do

Re

Mi

Fa

So

La

Xi

262

294

330

350

393

441

495

与上表对应,如果是高音音调,只需要将上面的频率乘以 2,如果是低音音调,需要将上面的音频除以 2。

要演奏出美妙的旋律,仅仅只控制音调是不够的,我们还需要把握每个音调演奏的节奏,即每个音调播放的时长。在乐曲中,节奏的把控是通过节拍来定义的,例如常见的四分之四节拍,指的是四分音符为一拍,每小节有四拍,这样,每遇到一个四分音符我们就播放这个音调一拍的单位时间,如果遇到八分音符,我们就播放二分之一拍的单位时间,如果遇到二分音符,我们就播放两拍的单位时间。

播放音乐《小星星》代码如下

guowenxue@ubuntu20:~/igkboard/apps$ vim pwm_music.c
/*********************************************************************************
 *      Copyright:  (C) 2021 Guo Wenxue<Email:guowenxue@gmail.com QQ:281143292>
 *                  All rights reserved.
 *
 *       Filename:  pwm_music.c
 *    Description:  This file used to test PWM music
 *                 
 *        Version:  1.0.0(10/21/2022~)
 *         Author:  Guo Wenxue <guowenxue@gmail.com>
 *      ChangeLog:  1, Release initial version on "10/21/2022 17:46:18 PM"
 *                 
 ********************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>
#include <unistd.h>
#include <string.h>

typedef struct pwm_one_s
{
    unsigned int    msec; //持续时间,毫秒
    unsigned int    freq;//频率
    unsigned char   duty;//相对占空比,百分比 * 100
}pwm_one_t;

/* 使用宏确定使用中音还是高低音 */
#define CX                      CM
static char pwm_path[100];

/* 低、中、高音频率*/
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 int pwm_config(const char *attr, const char *val);
static inline void msleep(unsigned long ms);
static int pwm_ring_one(pwm_one_t *pwm_ring);

int main(int argc, char *argv[])
{
    char temp[100];
    int fd = -1;
    int  i = 0;
    pwm_one_t pwm_one_test;

    /* 校验传参 */
    if (2 != argc) {
        printf("usage: %s <id> \n", argv[0]);
        exit(-1);  /* exit(0) 表示进程正常退出 exit(非0)表示异常退出*/
    }

    /* 打印配置信息 */
    printf("PWM config: id<%s>\n", argv[1]);

    /* 导出pwm 首先确定最终导出的文件夹路径*/
    memset(pwm_path, 0, sizeof(pwm_path));
    snprintf(pwm_path, sizeof(pwm_path), "/sys/class/pwm/pwmchip%s/pwm0", argv[1]);
    
    //如果pwm0目录不存在, 则导出
    memset(temp, 0, sizeof(temp));
    if (access(pwm_path, F_OK)) {
        snprintf(temp, sizeof(temp) , "/sys/class/pwm/pwmchip%s/export", argv[1]);
        if (0 > (fd = open(temp, O_WRONLY))) {
            printf("open pwmchip%s error\n", argv[1]);
            exit(-1);
        }
        //导出pwm0文件夹
        if (1 != write(fd, "0", 1)) {
            printf("write '0' to  pwmchip%s/export error\n", argv[1]);
            close(fd);
            exit(-2);
        }
        close(fd); 
    }

    pwm_one_test.freq = 1000; //1Khz
    pwm_one_test.duty = 50; //50%
    pwm_one_test.msec = 100;
    pwm_config("enable", "1");
    for(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;
}

/*pwm 配置函数 attr:属性文件名字 
 *  val:属性的值(字符串)
*/
static int pwm_config(const char *attr, const char *val)
{
    char file_path[100];
    int len;
    int fd =  -1;

    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);
    if (0 > (fd = open(file_path, O_WRONLY))) {
        printf("[%s] open %s error\n", __FUNCTION__, file_path);
        return fd;
    }

    len = strlen(val);
    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;
}

/* ms级休眠函数 */
static inline void msleep(unsigned long ms)
{
    struct timespec cSleep;
    unsigned long ulTmp;

    cSleep.tv_sec = ms / 1000;
    if (cSleep.tv_sec == 0)
    {
        ulTmp = ms * 10000;
        cSleep.tv_nsec = ulTmp * 100;
    }
    else
    {
        cSleep.tv_nsec = 0;
    }

    nanosleep(&cSleep, 0);
}

/* pwm蜂鸣器响一次声音 */
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] = {}; 
    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);//ns单位
    duty_cycle = (unsigned long)(((double)pwm_ring->duty / 100.f) * (double)period);//ns单位

    snprintf(period_str, sizeof(period_str), "%d", period);
    snprintf(duty_cycle_str, sizeof(duty_cycle_str), "%d", duty_cycle);

    // printf("period = %sns, duty_cycle_str = %sns\n", period_str, duty_cycle_str);

    //设置pwm频率和周期
    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;
    }

    msleep(pwm_ring->msec);
    /* 设置占空比为0 蜂鸣器无声 */
    if (pwm_config("duty_cycle", "0"))
    {
        printf("pwm_config duty_cycle failure.\n");
        return -3;
    }
    msleep(20);

    return 0;
}

编写Makefile如下

guowenxue@ubuntu20:~/igkboard/apps$ vim Makefile
CC=arm-linux-gnueabihf-gcc
APP_NAME=pwm_music

all:clean
	@${CC} ${APP_NAME}.c -o ${APP_NAME}

clean:
	@rm -f ${APP_NAME}

2.5.4.2 交叉编译测试运行

在ubuntu下的相关源码路径下执行make命令将会编译源码生成ARM开发板上的可执行文件。

guowenxue@ubuntu20:~/igkboard/apps$ make
guowenxue@ubuntu20:~/igkboard/apps$ ls
Makefile  pwm_music  pwm_music.c

现在我们在开发板上通过 tftp 命令 或其它方式将编译生成的测试程序下载到开发板上。

root@igkboard:~# tftp -gr pwm_test 192.168.2.2

运行测试示例,命令行参数给1即板载蜂鸣器对应的设备标识。

root@igkboard:~# ./pwm_music 1            
PWM config: id<1>

可以听到板载蜂鸣器播放小星星曲子,测试通过。