用鸿蒙OS在蜂鸣器上播放一曲《两只老虎》 原创 精华

许思维
发布于 2020-10-30 22:21
浏览
14收藏

本文介绍如何在HiSpark Wi-Fi IoT套件上,使用Harmony OS IoT硬件子系统的PWM接口 驱动蜂鸣器 播放音乐。

用PWM输出方波的API

鸿蒙系统IoT硬件子系统提供了PWM相关接口,接口头文件为wifiiot_pwm.h,其中开始输出方波的接口为:

/**
 * @brief Outputs PWM signals based on the input parameters.
 *
 * This function outputs PWM signals from a specified port based on
 * the configured frequency division multiple and duty cycle.
 *
 * @param port Indicates the PWM port number.
 * @param duty Indicates the PWM duty cycle.
 * @param freq Indicates the frequency-division multiple.
 * @return Returns {@link WIFI_IOT_SUCCESS} if the operation is successful;
 * returns an error code defined in {@link wifiiot_errno.h} otherwise.
 * @since 1.0
 * @version 1.0
 */
unsigned int PwmStart(WifiIotPwmPort port, unsigned short duty, unsigned short freq);

PWM输出的方波频率
通过PwmStart接口的注释,可以知道freq参数是分频倍数,PWM实际输出的方波频率等于 PWM时钟源频率 除以 分频倍数,即

f = Fcs / freq

其中,Fcs是PWM时钟源频率;

 

PWM输出方波的占空比
通过PwmStart接口的duty参数可以控制输出方波的占空比,占空比是指PWM输出的方波波形的高电平时间占整个方波周期的比例,具体占空比值是 duty 和 freq的比值,例如想要输出占空比 50%的方波信号,那么duty填的值就要是 freq/2;

 

音符-频率对应关系
用鸿蒙OS在蜂鸣器上播放一曲《两只老虎》-鸿蒙开发者社区

这个表中有一个规律——音高升高一个八度,频率升高一倍。

表格来自:https://liam.page/2018/04/09/pitch-interval-and-harmonic/

 

开发板可以输出的最低频率
通过前面的公式,我们知道:

  1. PWM输出的方波频率和freq成反比,freq越大,输出的方波频率越小;
  2. freq是unsinged short类型,最大值为65535;
    因此,输出频率的最小值取决于时钟源,而PWM的默认时钟源为160M:
unsigned int HalPwmInit(HalWifiIotPwmPort port)
{
    if (hi_pwm_set_clock(PWM_CLK_160M) != HI_ERR_SUCCESS) {
        return (unsigned int)HAL_WIFI_IOT_FAILURE;
    }
    return hi_pwm_init((hi_pwm_port)port);
}

160M时钟源条件下,输出方波的最低频率是:160M/65535=2441.44...,这个频率还是略高,在上面的表格中没有找到音名。但是我可以用上面表格值继续往后推算两个八度,就能够覆盖这个频率(不过通常只使用7个八度,所以还是有点高)。

 

如果时钟源频率可以更低,那么输出频率也可以更低!
幸运的是,通过调用hi_pwm_set_clock接口,可以修改时钟源:

/**
 * @ingroup iot_pwm
 *
 * Enumerates the PWM clock sources.CNcomment:PWM时钟源枚举。CNend
 */
typedef enum {
    PWM_CLK_160M, /**< 160M APB clock.CNcomment:160M 工作时钟 CNend */
    PWM_CLK_XTAL, /**< 24M/40M crystal clock.CNcomment:24M或40M 晶体时钟 CNend */
    PWM_CLK_MAX   /**< Maximum value, which cannot be used.CNcomment:最大值,不可使用CNend */
} hi_pwm_clk_source;

hi_u32 hi_pwm_set_clock(hi_pwm_clk_source clk_type);


通过注释我们知道hi_pwm_set_clock(PWM_CLK_XTAL);可以将时钟源设置为晶体时钟,晶体时钟可能为24M或40M;
那么问题来了——晶体时钟频率到底是多少?

 

晶体时钟频率是多少?
可以通过实验测算出晶体时钟频率,具体步骤如下:

  1. 使用 hi_pwm_set_clock(PWM_CLK_XTAL); 设置时钟源为晶体时钟;
  2. 使用PwmStart(WIFI_IOT_PWM_PORT_PWM0, 20*1000, 40*1000);输出方波信号;
  3. 使用示波器测量方波频率,根据测量的频率计算时钟源频率;

经实际测量,方波频率为1000Hz,

因此,时钟频率为 1000 * 40 * 1000,即 40 MHz;


可以输出的方波最低频率
因此,方波最低频率就是 40M / 65535 ,也就是:

>>> 40 * 1000 * 1000 / 65535
610.3608758678569
对照上面的频率表,可以知道,能够输出E5及以上的所有音符;

 

准备曲谱
为了代码实现起来简单,我选择了《两只老虎》的曲谱作为素材,在简谱网找到了简谱:
 用鸿蒙OS在蜂鸣器上播放一曲《两只老虎》-鸿蒙开发者社区

 

简谱说明
简谱上的一些记号,有的同学可能不太清楚是什么意思,这里简单说明一下:

  1. 左上角的1=C是表示调式(可以不用关心),1是唱名,C是音名,1=C是正调(就是常规的对应关系: 1-C,2-D, 3-E, 4-F, 5-G, 6-A, 7-B);
  2. 左上角的 4/4 是四四拍,是指 四分音符为一拍, 每小节有四拍;
  3. 下面谱子上的竖线就是每个小节分隔符,和4/4对应;
  4. “跑得快”上面5后面的横线表示延时一拍;
  5. “一只没有眼睛”一句,5后面的点表示顺延半拍,一条下划线表示二分之一时间,两条下划线表示四分之一时间;


编写代码
有了以上知识,我们就可以编写代码了,关键代码如下:

static const uint16_t g_tuneFreqs[] = { // 音符对应的分频系数
    0, // 40M Hz 时钟源,C6 ~ B6:
    38223, // 1 1046.5
    34052, // 2 1174.7
    30338, // 3 1318.5
    28635, // 4 1396.9
    25511, // 5 1568
    22728, // 6 1760
    20249, // 7 1975.5
    51021 // 5_ 783.99 // 低一个八度的 5
};

// 曲谱音符
static const uint8_t g_scoreNotes[] = {
    // 《两只老虎》简谱:http://www.jianpu.cn/pu/33/33945.htm
    1, 2, 3, 1,        1, 2, 3, 1,        3, 4, 5,  3, 4, 5,
    5, 6, 5, 4, 3, 1,  5, 6, 5, 4, 3, 1,  1, 8, 1,  1, 8, 1, // 最后两个 5 应该是低八度的,链接图片中的曲谱不对,声音到最后听起来不太对劲
};

// 曲谱时值,根据简谱记谱方法转写
static const uint8_t g_scoreDurations[] = {
    4, 4, 4, 4,        4, 4, 4, 4,        4, 4, 8,  4, 4, 8,
    3, 1, 3, 1, 4, 4,  3, 1, 3, 1, 4, 4,  4, 4, 8,  4, 4, 8,
};

static void *BeeperMusicTask(const char *arg)
{
    (void)arg;

    printf("BeeperMusicTask start!\r\n");

    hi_pwm_set_clock(PWM_CLK_XTAL); // 设置时钟源为晶体时钟(40MHz,默认时钟源160MHz)

    for (size_t i = 0; i < sizeof(g_scoreNotes)/sizeof(g_scoreNotes[0]); i++) {
        uint32_t tune = g_scoreNotes[i]; // 音符
        uint16_t freqDivisor = g_tuneFreqs[tune];
        uint32_t tuneInterval = g_scoreDurations[i] * (125*1000); // 音符时间
        printf("%d %d %d %d\r\n", tune, (40*1000*1000) / freqDivisor, freqDivisor, tuneInterval);
        PwmStart(WIFI_IOT_PWM_PORT_PWM0, freqDivisor/2, freqDivisor);
        usleep(tuneInterval);
        PwmStop(WIFI_IOT_PWM_PORT_PWM0);
    }

    return NULL;
}


谱子中最后两个5是错误的,应该是低八度的5,也就是5下面应该打一个点;我修改了代码,让整个曲子听起来更自然;

完整代码:https://gitee.com/hihopeorg/HarmonyOS-IoT-Application-Development/blob/master/02_device_control/beeper_music_demo.c

 

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
beeper_music_demo.zip 3.22K 278次下载
已于2020-12-26 10:27:36修改
14
收藏 14
回复
举报
11条回复
按时间正序
/
按时间倒序
gordonlonglong
gordonlonglong

厉害,谢谢分享!!!

1
回复
2020-10-31 08:52:33
鲜橙加冰
鲜橙加冰

前排留名。。。。。。。。。。。。。许老师你继续加油。

1
回复
2020-10-31 11:07:56
SummerRic
SummerRic

有童鞋贴编译过程中报错问题的解决方案哦,有同遇到的,可以看过来https://harmonyos.51cto.com/answer/209

回复
2020-11-1 00:48:30
tinghuadeguai
tinghuadeguai

插眼,静静的观察

回复
2020-11-4 09:21:19
Copy攻城狮
Copy攻城狮

老师 请教一下 1上有个点 是哪个音? 是1108.7吗?

回复
2020-12-13 11:43:43
许思维
许思维 回复了 Copy攻城狮
老师 请教一下 1上有个点 是哪个音? 是1108.7吗?

简谱上加一个点就是高一个八度,

比如代码注释里面的1的频率是 1046.5,高一个八度就是频率乘以2

// 1 1046.5  => 2093.0

回复
2020-12-14 11:43:37
..._...
..._...

你好,请问hi_u32 hi_pwm_set_clock这个函数在哪里?我从hpm拉取的源码中的wifiiot_pwm.h文件里没有找到。

而且现在下载的鸿蒙源码头文件都改变了,内容也有些不同,好多之前课程的代码都用不了了,请问应该怎么继续学习(哪里更全面系统)?

回复
2021-7-7 18:12:46
拓维信息Abin
拓维信息Abin

请问一下,我需要周期为20ms的PWM输出,应该怎么做?因为想驱动SG90 Servo,一直驱动不起来。

回复
2021-7-29 14:22:33
许思维
许思维 回复了 拓维信息Abin
请问一下,我需要周期为20ms的PWM输出,应该怎么做?因为想驱动SG90 Servo,一直驱动不起来。

两个思路:

1. 关闭内核调度器,然后循环延时20ms切换一次gpio输出高低电平,操作完了之后在开启调度器;

    这种方法需要用到CMSIS v2几个接口:

        osKernelLock和osKernelUnlock关闭、开启调度器;

        osKernelGetSysTimerCount 获取CPU硬件cycle数,

        osKernelGetSysTimerFreq 获取CPU工作频率,这两个接口可以用来实现短时间的忙等延时;

     这种方式的好处是时间控制精度比较高,缺点是忙等过程中CPU会一直被占用,不能用来处理其他任务;

2.  使用定时器,定时20ms切换一次gpio输出高低电平;

    这种方式的好处是不占用CPU,缺点是由调度器切换实现,控制精度为调度单位(当前为10ms);

 

回复
2021-8-31 14:54:20
许思维
许思维 回复了 ..._...
你好,请问hi_u32 hi_pwm_set_clock这个函数在哪里?我从hpm拉取的源码中的wifiiot_pwm.h文件里没有找到。 而且现在下载的鸿蒙源码头文件都改变了,内容也有些不同,好多之前课程的代码都用不了了,请问应该怎么继续学习(哪里更全面系统)?

这个接口是海思 hi3861 SDK里面的接口,可以在vendor下面搜索到:

grep -nr hi_pwm_set_clock vendor/

回复
2021-8-31 14:55:34
许思维
许思维 回复了 ..._...
你好,请问hi_u32 hi_pwm_set_clock这个函数在哪里?我从hpm拉取的源码中的wifiiot_pwm.h文件里没有找到。 而且现在下载的鸿蒙源码头文件都改变了,内容也有些不同,好多之前课程的代码都用不了了,请问应该怎么继续学习(哪里更全面系统)?

> 下载的鸿蒙源码头文件都改变了,内容也有些不同,好多之前课程的代码都用不了了,请问应该怎么继续学习(哪里更全面系统)?

这个确实是很大的问题,OpenHarmony IoT硬件的API接口一直在变化,这一块来目前说只有直接用海思SDK接口是比较稳定的;

IoT方向的话,可以继续学习一下:

1. 内核接口CMSIS-RTOS:https://arm-software.github.io/CMSIS_5/RTOS2/html/index.html

2. 海思的SDK文档;

3. 物联网相关协议应用和原理;

以及更深入的,可以学习:

4. liteos_m的相关实现原理、移植;

5. 分布式软总线的应用和原理(目前需要HarmonyOS手机作为主控端);

6. 其他RTOS,例如RT-Thread、Zephyr等;

回复
2021-8-31 15:12:13
回复
    相关推荐