本文介绍如何在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;
音符-频率对应关系
这个表中有一个规律——音高升高一个八度,频率升高一倍。
表格来自:https://liam.page/2018/04/09/pitch-interval-and-harmonic/
开发板可以输出的最低频率
通过前面的公式,我们知道:
- PWM输出的方波频率和freq成反比,freq越大,输出的方波频率越小;
- 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;
那么问题来了——晶体时钟频率到底是多少?
晶体时钟频率是多少?
可以通过实验测算出晶体时钟频率,具体步骤如下:
- 使用 hi_pwm_set_clock(PWM_CLK_XTAL); 设置时钟源为晶体时钟;
- 使用PwmStart(WIFI_IOT_PWM_PORT_PWM0, 20*1000, 40*1000);输出方波信号;
- 使用示波器测量方波频率,根据测量的频率计算时钟源频率;
经实际测量,方波频率为1000Hz,
因此,时钟频率为 1000 * 40 * 1000,即 40 MHz;
可以输出的方波最低频率
因此,方波最低频率就是 40M / 65535 ,也就是:
>>> 40 * 1000 * 1000 / 65535
610.3608758678569
对照上面的频率表,可以知道,能够输出E5及以上的所有音符;
准备曲谱
为了代码实现起来简单,我选择了《两只老虎》的曲谱作为素材,在简谱网找到了简谱:
简谱说明
简谱上的一些记号,有的同学可能不太清楚是什么意思,这里简单说明一下:
- 左上角的1=C是表示调式(可以不用关心),1是唱名,C是音名,1=C是正调(就是常规的对应关系: 1-C,2-D, 3-E, 4-F, 5-G, 6-A, 7-B);
- 左上角的 4/4 是四四拍,是指 四分音符为一拍, 每小节有四拍;
- 下面谱子上的竖线就是每个小节分隔符,和4/4对应;
- “跑得快”上面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下面应该打一个点;我修改了代码,让整个曲子听起来更自然;