回顾下之前的章节:
本文我们将介绍通过PWM控制无源蜂鸣器播放音乐的方法
关键字:STM32,GD32,PWM,无源蜂鸣器
蜂鸣器有2种:无源蜂鸣器和有源蜂鸣器,所谓有源,是指蜂鸣器内部带有震荡源,通电就可以响;所谓无源,是指蜂鸣器内部不带震荡源,需要频率信号驱动。
有源蜂鸣器 | 无源蜂鸣器 |
---|---|
有震荡源-频率固定 | 无震荡源-频率可控 |
管脚有方向 | 管脚无方向 |
单向有内阻,一般超过1KΩ | 双向有内阻,一般几百Ω |
有电路板 | 无电路板 |
通电即发声 | 频率信号驱动 |
贵 | 便宜 |
由于无源蜂鸣器由频率信号控制,我们可以通过调整控制频率的方式来播放音乐。
人耳能够感受到的声音频率范围为20HZ ~ 20KHZ,音乐的频率一般是几百HZ,不超过2KHZ。
假设我们的定时器频率是100MHZ,我们需要通过分频的方式把PWM的频率降到音乐的频率范围之内:把prescaler值设置为999,频率降为100KHZ,通过调整period使输出频率在声音的范围内。
例如,C调音阶1的频率是262HZ,那么period值就是:100K/262≈382
我们把接口进行封装(代码GD32):
void timer3_freq_change(uint32_t freq_base, uint16_t freq)
{
uint16_t period = freq_base / freq;
timer_autoreload_value_config(TIMER2, period);
}
其中,freq_base是prescaler之后的值,freq是声音的频率值,我们把这个接口再封装一把。
#define TIMER_FREQ_BASE 100000 // 100K
#define TIMER_PRESCALER 999 // 分频系数 * 100K = 100M
#define PLAY_SOUND(note) timer3_freq_change(TIMER_FREQ_BASE, note)
到这里,播放声音的接口就搞定了。
上面我们已经搞定了播放声音的方式,那么在乐理中,我们还需要知道音乐的调子和节拍。
看一段简谱。
祝你生日快乐
简谱的左上角一般标示了这首歌的调子,节拍数和BPM。
《祝你生日快乐》是C调,3/4表示4分音符为1拍,每小节3拍,小蝌蚪=100表示每分钟100拍。
有了这几个数据,我们就可以算出来每拍音符的时长=60s/100=600ms。
关于简谱中的记法:
音符的下点表示低音,上点表示高音,-表示延长音,一个下划线表示时长减半,N个下划线表示时长缩小2^N倍。
()表示间奏或者过门,‖::‖表示重复,上括号表示连音。
如上划横线部分,表示:
前一节:7播放600ms,-表示7继续播放600ms,5下划线播放300ms,5下划线播放300ms,共播放1800ms。
后一节:6播放600ms,5播放600ms,2上加点播放600ms,共播放1800ms。
我们把音符的频率用数组枚举出来备用:
const uint16_t freq_A[] = {
0,
221, 248, 278, 294, 330, 371, 416, ///< 低音1-7
441, 495, 556, 589, 661, 742, 833, ///< 普通音1-7
882, 990, 1112, 1178, 1322, 1484, 1665 ///< 高音1-7
};
const uint16_t freq_B[] = {
0,
248, 278, 294, 330, 371, 416, 467, ///< 低音1-7
495, 556, 589, 661, 742, 833, 935, ///< 普通音1-7
990, 1112, 1178, 1322, 1484, 1665, 1869 ///< 高音1-7
};
const uint16_t freq_C[] = {
0,
131, 147, 165, 175, 196, 221, 248, ///< 低音1-7
262, 294, 330, 350, 393, 441, 495, ///< 普通音1-7
525, 589, 661, 700, 786, 882, 990 ///< 高音1-7
};
const uint16_t freq_D[] = {
0,
147, 165, 175, 196, 221, 248, 278, ///< 低音1-7
294, 330, 350, 393, 441, 495, 556, ///< 普通音1-7
589, 661, 700, 786, 882, 990, 1112 ///< 高音1-7
};
const uint16_t freq_E[] = {
0,
165, 175, 196, 221, 248, 278, 312, ///< 低音1-7
330, 350, 393, 441, 495, 556, 624, ///< 普通音1-7
661, 700, 786, 882, 990, 1112, 1248 ///< 高音1-7
};
const uint16_t freq_F[] = {
0,
175, 196, 221, 234, 262, 294, 330, ///< 低音1-7
350, 393, 441, 495, 556, 624, 661, ///< 普通音1-7
700, 786, 882, 935, 1049, 1178, 1322 ///< 高音1-7
};
const uint16_t freq_G[] = {
0,
196, 221, 234, 262, 294, 330, 371, ///< 低音1-7
393, 441, 495, 556, 624, 661, 742, ///< 普通音1-7
786, 882, 935, 1049, 1178, 1322, 1484 ///< 高音1-7
};
这里用一个数组把它们组织起来方便调用:
const uint16_t *FREQS[] = {freq_A, freq_B, freq_C, freq_E, freq_F, freq_G};
同时对外开放一个枚举量方便外部调用(外部只需要知道调号就可以用,并不需要关心具体的频率值):
typedef enum
{
TONE_A,
TONE_B,
TONE_C,
TONE_D,
TONE_E,
TONE_F,
TONE_G,
}tone_e;
从上面的推断来看,实际上单个音符的播放时间只跟bpm有关系,在编程上我们需要关心的只有一个点:
如何表示不同播放时长的音符?
这里,我们需要用点数学或者编程的技巧。
我们用有规律的数字来表示不同的播放时长,用一个表来说明一下:
音符 | 数学表示方法 |
---|---|
0(空) | 0 |
低音1-7 | 1-7 |
普通1-7 | 8-14 |
高音1-7 | 15-21 |
--- | --- |
低音1-7(1个下划线) | 31-37 |
普通1-7(1个下划线) | 38-44 |
高音1-7(1个下划线) | 45-51 |
--- | --- |
低音1-7(2个下划线) | 61-67 |
普通1-7(2个下划线) | 68-74 |
高音1-7(2个下划线) | 75-81 |
--- | --- |
低音1-7(3个下划线) | 91-97 |
普通1-7(3个下划线) | 98-104 |
高音1-7(3个下划线) | 105-111 |
--- | --- |
低音1-7(4个下划线) | 121-127 |
普通1-7(4个下划线) | 128-134 |
高音1-7(4个下划线) | 135-141 |
这里用到了线性的表示方法,我们在写程序的时候,就可以很愉快的用if else搞定了。
void play_music(uint16_t bpm, tone_e tone, uint16_t *data, uint16_t len)
{
uint16_t delay = 60000/bpm;
uint8_t index;
const uint16_t *freq = FREQS[tone];
for (index = 0; index < len; index++)
{
if (data[index] <= HIGH7)
{
/* 基本音阶 */
PLAY_SOUND(freq[data[index]]);
delay_ms(delay);
}
else if (data[index] <= HIGH7_)
{
/* 1个_ */
PLAY_SOUND(freq[data[index]-30]);
delay_ms(delay/2);
}
else if (data[index] <= HIGH7_2)
{
/* 2个_ */
PLAY_SOUND(freq[data[index]-60]);
delay_ms(delay/4);
}
else if (data[index] <= HIGH7_3)
{
/* 3个_ */
PLAY_SOUND(freq[data[index]-90]);
delay_ms(delay/8);
}
else if (data[index] <= HIGH7_4)
{
/* 4个_ */
PLAY_SOUND(freq[data[index]-120]);
delay_ms(delay/16);
}
}
}
在上面的程序中,我们再次用到了编程技巧,用简单的宏定义替换数字,方便我们制谱。
0 = BASE0
1低音 = LOW1
1 = BASE1
1高音 = HIGH1
1一个下划线 = BASE1_
1两个下划线 = BASE1_2
1三个下划线 = BASE1_3
1四个下划线 = BASE1_4
有了这个关系,我们就可以很轻松的制谱了。
例如《祝你生日快乐》中的划横线部分,表示出来就是:
BASE7, BASE7, BASE5_, BASE5_
BASE6, BASE5, HIGH2
到这里,我们就可以很轻松的翻译一首歌了。
我们把宏定义也贴一把:
#define BASE0 0
#define LOW1 1
#define LOW2 2
#define LOW3 3
#define LOW4 4
#define LOW5 5
#define LOW6 6
#define LOW7 7
#define BASE1 8
#define BASE2 9
#define BASE3 10
#define BASE4 11
#define BASE5 12
#define BASE6 13
#define BASE7 14
#define HIGH1 15
#define HIGH2 16
#define HIGH3 17
#define HIGH4 18
#define HIGH5 19
#define HIGH6 20
#define HIGH7 21
#define LOW1_ (30+LOW1)
#define LOW2_ (30+LOW2)
#define LOW3_ (30+LOW3)
#define LOW4_ (30+LOW4)
#define LOW5_ (30+LOW5)
#define LOW6_ (30+LOW6)
#define LOW7_ (30+LOW7)
#define BASE1_ (30+BASE1)
#define BASE2_ (30+BASE2)
#define BASE3_ (30+BASE3)
#define BASE4_ (30+BASE4)
#define BASE5_ (30+BASE5)
#define BASE6_ (30+BASE6)
#define BASE7_ (30+BASE7)
#define HIGH1_ (30+HIGH1)
#define HIGH2_ (30+HIGH2)
#define HIGH3_ (30+HIGH3)
#define HIGH4_ (30+HIGH4)
#define HIGH5_ (30+HIGH5)
#define HIGH6_ (30+HIGH6)
#define HIGH7_ (30+HIGH7)
#define LOW1_2 (60+LOW1)
#define LOW2_2 (60+LOW2)
#define LOW3_2 (60+LOW3)
#define LOW4_2 (60+LOW4)
#define LOW5_2 (60+LOW5)
#define LOW6_2 (60+LOW6)
#define LOW7_2 (60+LOW7)
#define BASE1_2 (60+BASE1)
#define BASE2_2 (60+BASE2)
#define BASE3_2 (60+BASE3)
#define BASE4_2 (60+BASE4)
#define BASE5_2 (60+BASE5)
#define BASE6_2 (60+BASE6)
#define BASE7_2 (60+BASE7)
#define HIGH1_2 (60+HIGH1)
#define HIGH2_2 (60+HIGH2)
#define HIGH3_2 (60+HIGH3)
#define HIGH4_2 (60+HIGH4)
#define HIGH5_2 (60+HIGH5)
#define HIGH6_2 (60+HIGH6)
#define HIGH7_2 (60+HIGH7)
#define LOW1_3 (90+LOW1)
#define LOW2_3 (90+LOW2)
#define LOW3_3 (90+LOW3)
#define LOW4_3 (90+LOW4)
#define LOW5_3 (90+LOW5)
#define LOW6_3 (90+LOW6)
#define LOW7_3 (90+LOW7)
#define BASE1_3 (90+BASE1)
#define BASE2_3 (90+BASE2)
#define BASE3_3 (90+BASE3)
#define BASE4_3 (90+BASE4)
#define BASE5_3 (90+BASE5)
#define BASE6_3 (90+BASE6)
#define BASE7_3 (90+BASE7)
#define HIGH1_3 (90+HIGH1)
#define HIGH2_3 (90+HIGH2)
#define HIGH3_3 (90+HIGH3)
#define HIGH4_3 (90+HIGH4)
#define HIGH5_3 (90+HIGH5)
#define HIGH6_3 (90+HIGH6)
#define HIGH7_3 (90+HIGH7)
#define LOW1_4 (120+LOW1)
#define LOW2_4 (120+LOW2)
#define LOW3_4 (120+LOW3)
#define LOW4_4 (120+LOW4)
#define LOW5_4 (120+LOW5)
#define LOW6_4 (120+LOW6)
#define LOW7_4 (120+LOW7)
#define BASE1_4 (120+BASE1)
#define BASE2_4 (120+BASE2)
#define BASE3_4 (120+BASE3)
#define BASE4_4 (120+BASE4)
#define BASE5_4 (120+BASE5)
#define BASE6_4 (120+BASE6)
#define BASE7_4 (120+BASE7)
#define HIGH1_4 (120+HIGH1)
#define HIGH2_4 (120+HIGH2)
#define HIGH3_4 (120+HIGH3)
#define HIGH4_4 (120+HIGH4)
#define HIGH5_4 (120+HIGH5)
#define HIGH6_4 (120+HIGH6)
#define HIGH7_4 (120+HIGH7)
代码:
void play_sheng_ri_kuai_le(void)
{
uint16_t bpm = 100;
tone_e tone = TONE_C;
uint16_t gc[] = {
/* 间奏 */
BASE5, BASE5,
HIGH5, HIGH3, HIGH1,
BASE7, BASE6, BASE6,
BASE0, HIGH4_, HIGH4_,
HIGH3, HIGH1, HIGH2,
HIGH1, HIGH1,
/* 第一段 */
BASE5_, BASE5_, // - 祝你
BASE6, BASE5, HIGH1,
BASE7, BASE7, BASE5_, BASE5_, // 乐,祝你
BASE6, BASE5, HIGH2,
HIGH1, HIGH1, BASE5_, BASE5_, // - 祝你
HIGH5, HIGH3, HIGH1,
BASE7, BASE6, BASE6,
BASE0, HIGH4_, HIGH4_, // - 祝你
HIGH3, HIGH1, HIGH2,
HIGH1, HIGH1,
/* 第二段 */
BASE5_, BASE5_, // - 祝你
BASE6, BASE5, HIGH1,
BASE7, BASE7, BASE5_, BASE5_, // 乐,祝你
BASE6, BASE5, HIGH2,
HIGH1, HIGH1, BASE5_, BASE5_, // - 祝你
HIGH5, HIGH3, HIGH1,
BASE7, BASE6, BASE6,
BASE0, HIGH4_, HIGH4_, // - 祝你
HIGH3, HIGH1, HIGH2,
HIGH1, HIGH1,
/* 结束 */
BASE5_, BASE5_, // - 祝你
HIGH5, HIGH3, HIGH1,
BASE7, BASE6, BASE6,
BASE0, HIGH4_, HIGH4_, // - 祝你
HIGH3, HIGH1, HIGH2,
HIGH1, HIGH1, HIGH1
};
play_music(bpm, tone, gc, ARRAY_NUM(gc));
}
代码:
void play_cang_hai_yi_sheng_xiao(void)
{
uint16_t bpm = 66;
tone_e tone = TONE_A;
uint16_t gz0[] = {
/* 间奏 */
BASE5_2, BASE6_2, HIGH1_2, HIGH2_2, HIGH5_, HIGH2_,
BASE5_2, BASE6_2, HIGH1_2, HIGH2_2, HIGH5_, HIGH2_
};
uint16_t gc123[] = {
/* 第一 二 三段 沧海笑 苍天笑 江山笑 */
BASE6_, BASE6_2, BASE5_2, BASE3_, BASE2_2, BASE1, BASE1,
BASE3_, BASE2_2, BASE1_, LOW6_2, LOW5_2, LOW5, LOW5,
LOW5_, LOW6_2, LOW5_, LOW6_2, LOW1_, LOW2_2, BASE3_, BASE5_,
LOW6_, LOW5_2, BASE3_2, BASE2_2, BASE1_, BASE2
};
uint16_t gc45[] = {
/* 第四段 清风笑 苍生笑 */
BASE6_, BASE6_2, BASE5_2, BASE3_, BASE2_2, BASE1, BASE1,
BASE3_, BASE2_2, BASE1_, LOW6_2, LOW5_2, LOW5, LOW5,
LOW5_, LOW6_2, LOW5_, LOW6_2, LOW1_, LOW2_2, BASE3_, BASE5_,
LOW6_, LOW5_2, BASE3_2, BASE2_2, BASE1_, BASE1, BASE1,
};
uint16_t gz1[] = {
/* 间奏 */
LOW6_2, BASE1_2, BASE2_2, BASE3_2,
LOW6_2, BASE1_2, BASE2_2, BASE3_2,
LOW6_2, BASE1_2, BASE2_2, BASE3_2,
LOW6_2, BASE1_2, BASE2_2, BASE3_2,
LOW6_2, BASE1_2, BASE2_2, BASE3_2,
LOW6_2, BASE1_2, BASE2_2, BASE3_2,
LOW6_2, BASE1_2, BASE2_2, BASE3_2,
LOW6_2, BASE1_2, BASE2_2, BASE3_2,
BASE6_, BASE6_2, BASE5_2, BASE3_, BASE2_, BASE1, BASE1,
BASE3_2, BASE5_2, BASE3_2, BASE2_2, BASE1_, LOW6_, LOW5, LOW5,
LOW5_, LOW6_2, LOW5_, LOW6_2, LOW1_, LOW2_2, BASE3_, BASE5_2,
BASE6_, BASE5_2, BASE5_2, BASE5_2, BASE3_2, BASE2_2, BASE1_2, BASE2, BASE2
};
uint16_t last[] = {
/* 结束 */
BASE6_, BASE6_2, BASE5_2, BASE3_, BASE2_2, BASE1, BASE1,
BASE3_, BASE2_2, BASE1_, LOW6_2, LOW5, LOW5,
LOW5_, LOW6_2, LOW5_, LOW6_2, BASE1_, BASE2_2, BASE3_, BASE5_2,
BASE6_, BASE5_2, BASE5_2, BASE5_2, BASE3_2, BASE2_2, BASE1_2, BASE2, BASE2
};
play_music(bpm, tone, gz0, ARRAY_NUM(gz0));
play_music(bpm, tone, gc123, ARRAY_NUM(gc123));
play_music(bpm, tone, gc123, ARRAY_NUM(gc123));
play_music(bpm, tone, gc123, ARRAY_NUM(gc123));
play_music(bpm, tone, gc45, ARRAY_NUM(gc45));
play_music(bpm, tone, gz1, ARRAY_NUM(gz1));
play_music(bpm, tone, gc45, ARRAY_NUM(gc45));
play_music(bpm, tone, last, ARRAY_NUM(last));
play_music(bpm, tone, last, ARRAY_NUM(last));
}