M-Arch(12)第十一个示例:如何用无源蜂鸣器播放音乐

2021-11-19 14:44:06 浏览数 (1)

前言

回顾下之前的章节:

  • 第一章节中我们描述了整个框架的核心设计思路以及主要的文件架构
  • 第二章节中我们基于一个简单的定时器OS实现了串口的数据打印,并完成了通用crc模块的设计和测试
  • 第三章节中我们给出了真随机数和伪随机数的概念和代码示例,并在架构上对接口进行了重构
  • 第四章节中我们回顾了FMC的基本知识,并给出了示例,后面我们将在设计IAP的时候再次使用到FMC
  • 第五章节中我们使用ADC和DMA搭建了一个通用的采样框架,并通过串口给出了采样的数据示例
  • 第六章节中我们总结了DAC的基本使用方法,并通过DAC生成了任意频率的正弦波,三角波和方波
  • 第七章节中我们总结下时钟的概念,并给出了获取系统中各模块的时钟频率的代码
  • 第八章节中我们介绍了如何通过串口的DMA来实现串口数据的收发
  • 第九章节中我们介绍了定时器的使用,以及如何产生普通占空比PWM以及互补带死区的PWM,这在控制中十分重要
  • 第十章节中我们介绍了SPI的概念,以及用三线SPI的时序驱动DS1302时钟芯片的方法,进一步掌握了SPI的使用
  • 第十一章节中我们介绍了通过PWM控制无源蜂鸣器的方法

本文我们将介绍通过PWM控制无源蜂鸣器播放音乐的方法

关键字:STM32,GD32,PWM,无源蜂鸣器

蜂鸣器

蜂鸣器有2种:无源蜂鸣器和有源蜂鸣器,所谓有源,是指蜂鸣器内部带有震荡源,通电就可以响;所谓无源,是指蜂鸣器内部不带震荡源,需要频率信号驱动。

有源蜂鸣器

无源蜂鸣器

有震荡源-频率固定

无震荡源-频率可控

管脚有方向

管脚无方向

单向有内阻,一般超过1KΩ

双向有内阻,一般几百Ω

有电路板

无电路板

通电即发声

频率信号驱动

便宜

由于无源蜂鸣器由频率信号控制,我们可以通过调整控制频率的方式来播放音乐。

人耳能够感受到的声音频率范围为20HZ ~ 20KHZ,音乐的频率一般是几百HZ,不超过2KHZ。

假设我们的定时器频率是100MHZ,我们需要通过分频的方式把PWM的频率降到音乐的频率范围之内:把prescaler值设置为999,频率降为100KHZ,通过调整period使输出频率在声音的范围内。

例如,C调音阶1的频率是262HZ,那么period值就是:100K/262≈382

我们把接口进行封装(代码GD32):

代码语言:javascript复制
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是声音的频率值,我们把这个接口再封装一把。

代码语言:javascript复制
#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。

代码部分

音符

我们把音符的频率用数组枚举出来备用:

代码语言:javascript复制
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
};

这里用一个数组把它们组织起来方便调用:

代码语言:javascript复制
const uint16_t *FREQS[] = {freq_A, freq_B, freq_C, freq_E, freq_F, freq_G};

同时对外开放一个枚举量方便外部调用(外部只需要知道调号就可以用,并不需要关心具体的频率值):

代码语言:javascript复制
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搞定了。

代码语言:javascript复制
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);
        }
    }
}

在上面的程序中,我们再次用到了编程技巧,用简单的宏定义替换数字,方便我们制谱。

代码语言:javascript复制
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

到这里,我们就可以很轻松的翻译一首歌了。

我们把宏定义也贴一把:

代码语言:javascript复制
#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)

SHOW TIME

祝你生日快乐

http://mpvideo.qpic.cn/0b2e6aaamaaahaamrier4nqvb4gda3yaabqa.f10002.mp4?dis_k=67b37e2d1520ce67e59e8fa6aff0e9de&dis_t=1637303932&vid=wxv_2136634837200322561&format_id=10002&support_redirect=0&mmversion=false

代码:

代码语言:javascript复制
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));
}

沧海一声笑

沧海一声笑简谱沧海一声笑简谱

http://mpvideo.qpic.cn/0bc3aaacwaaaqiaprwmr7nqvaagdfmaaakya.f10002.mp4?dis_k=8a0e0dc633bf332946e8a2778c3c6ffe&dis_t=1637303932&vid=wxv_2136654759825555457&format_id=10002&support_redirect=0&mmversion=false

代码:

代码语言:javascript复制
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));
}
spi

0 人点赞