详解树莓派Model B+控制蜂鸣器演奏乐曲

步进电机以及无源蜂鸣器这些都需要脉冲信号才能够驱动,这里将用GPIO的PWM接口驱动无源蜂鸣器弹奏乐曲,本文基于树莓派Mode B+,其他版本树莓派实现时需参照相关资料进行修改!

1 预备知识

1.1 无源蜂鸣器和有源蜂鸣器

无源蜂鸣器:内部没有震荡源,直流信号无法让它鸣叫。必须用去震荡的电流驱动它,2K-5KHZ的方波PWM (Pulse Width Modulation脉冲宽度调制)。5KHZ的电流方波就是每秒震动5K次,每一个完整的周期占用200us的时间,高点平占一部分时间,低电平占一部分时间。声音频率可控,可以做出不同的音效。

有源蜂鸣器:内部带震荡电路,一通电就鸣叫,所以可以跟前面LED一样,给个高电平就能响,编程比无源的更方便。

本文利用无源蜂鸣器弹奏乐曲,用的就是淘宝上普通的电磁式阻抗16欧交流/2KHz 3V 5V 12V通用无源蜂鸣器,如果手边没有无源蜂鸣器,用普通的耳机也可以来代替无源蜂鸣器。

1.2 PWM

PWM(Pulse Width Modulation)即脉冲宽度调制,是一种利用微处理器的数字输出来控制模拟电路的控制技术。可以用下面的一幅图来形象地说明PWM:

详解树莓派Model B+控制蜂鸣器演奏乐曲

图中tpwm就是一个周期的时间长度。对于2KHz频率来说,那么周期就是1s/2K=500us。图中的D叫做占空比,指的是高电平的时间占用整个周期时间的百分比。第一个周期D=50%,那么就是高电平低电平的时间各占一半。接下来的D为33%,那就是通电时间为33%,剩余的不通电时间占用67%。树莓派Model B+有4个PIN脚支持PWM输出,如下图最右侧:

详解树莓派Model B+控制蜂鸣器演奏乐曲

但是,需要注意的是BCM2835芯片只支持两路PWM输出,所以以上12 Pin脚和32 Pin脚对应的都是channel 1的PWM输出,即如果这两个Pin的功能都选择的是PWM输出,则它们输出的PWM是完全相同的,同理33 Pin脚和35 Pin脚对应芯片channel 2的PWM输出。

博通公司公布的BCM2835芯片资料BCM2835 ARM Peripherals中第9章比较详细的介绍了PWM相关内容,此外还可参考网上整理好的寄存器介绍资料rip-registers,通过阅读可以得知树莓派Model B+支持两种模式的PWM输出:一种是Balanced mode(平衡模式),一种是Mark-Space mode(MS模式)。另外树莓派的PWM输出基础频率是19.2MHz,PWM输出频率受这个基础频率的限制。

1.3 树莓派PWM分析

进行分析前先看一下实验的物理电路连接:

详解树莓派Model B+控制蜂鸣器演奏乐曲

图中,红色杜邦线一头连接树莓派的32 Pin脚(PWM0),一头连接示波器的探针;绿色杜邦线一头连接树莓派的12 Pin脚(PWM0),一头连接无源蜂鸣器的正极;黄色杜邦线一头连接树莓派的6 Pin脚(ground),一头连接无源蜂鸣器的负极,此外示波器探针的ground也连接到黄色杜邦线,结合bcm2835 C library来进行分析:

(1)下载bcm2835库:wget http://www.airspayce.com/mikem/bcm2835/bcm2835-1.50.tar.gz

(2)解压:tar -zxvf bcm2835-1.50.tar.gz

(3)进入目录:cd bcm2835-1.35

(4)编译:./configure && make

(5)安装:sudo make install

修改examples/pwm/pwm.c的内容如下:

详解树莓派Model B+控制蜂鸣器演奏乐曲详解树莓派Model B+控制蜂鸣器演奏乐曲

1 // pwm.c
 2 //
 3 // Example program for bcm2835 library
 4 // Shows how to use PWM to control GPIO pins
 5 //
 6 // After installing bcm2835, you can build this 
 7 // with something like:
 8 // gcc -o pwm pwm.c -l bcm2835 
 9 // sudo ./pwm
10 //
11 // Or you can test it before installing with:
12 // gcc -o pwm -I ../../src ../../src/bcm2835.c pwm.c
13 // sudo ./pwm
14 //
15 // Author: Mike McCauley
16 // Copyright (C) 2013 Mike McCauley
17 // $Id: RF22.h,v 1.21 2012/05/30 01:51:25 mikem Exp $
18 
19 #include <bcm2835.h>
20 #include <stdio.h>
21 
22 // PWM output on RPi Plug P1 pin 12 (which is GPIO pin 18)
23 // in alt fun 5.
24 // Note that this is the _only_ PWM pin available on the RPi IO headers
25 #define PIN RPI_GPIO_P1_12
26 // and it is controlled by PWM channel 0
27 #define PWM_CHANNEL 0
28 // This controls the max range of the PWM signal
29 #define RANGE 1024
30 
31 #define PIN2 RPI_BPLUS_GPIO_J8_32
32 
33 int main(int argc, char **argv)
34 {
35     if (!bcm2835_init())
36         return 1;
37 
38     // Set the output pin to Alt Fun 5, to allow PWM channel 0 to be output there
39     bcm2835_gpio_fsel(PIN, BCM2835_GPIO_FSEL_ALT5);
40     
41     bcm2835_gpio_fsel(PIN2, BCM2835_GPIO_FSEL_ALT0);    // 打开PI 32 Pin脚的PWM0输出功能
42 
43     // Clock divider is set to 16.
44     // With a divider of 16 and a RANGE of 1024, in MARKSPACE mode,
45     // the pulse repetition frequency will be
46     // 1.2MHz/1024 = 1171.875Hz, suitable for driving a DC motor with PWM
47     bcm2835_pwm_set_clock(BCM2835_PWM_CLOCK_DIVIDER_16);
48     bcm2835_pwm_set_mode(PWM_CHANNEL, 0, 1);
49     bcm2835_pwm_set_range(PWM_CHANNEL, RANGE);
50     
51     printf("this is banlance mode, anykey will change to markspace mode\n");
52     bcm2835_pwm_set_data(PWM_CHANNEL, RANGE/4);
53     getchar();
54     
55     printf("change to markspace mode, anykey to exit\n");
56     bcm2835_pwm_set_mode(PWM_CHANNEL, 1, 1);
57     bcm2835_pwm_set_range(PWM_CHANNEL, RANGE);
58     bcm2835_pwm_set_data(PWM_CHANNEL, RANGE/4);
59     getchar();
60 
61     bcm2835_close();
62     return 0;
63 }
pwm.c

 代码中首先设置PWM输出为平衡模式,之后按任意键切换为MS模式,编译:gcc -o pwm pwm.c -lbcm2835,运行:sudo ./pwm,示波器分别捕获到如下波形图:

详解树莓派Model B+控制蜂鸣器演奏乐曲

代码第47行用divider=16对19.2MHz的基础频率进行调整,调整后的pwm频率为19.2MHz/16=1.2MHz,根据BCM2835芯片资料及代码49行和52行内容可知占空比应为N/M=(RANGE/4)/RANGE=256/1024,平衡模式力求任意一段时间占空比都最接近N/M=1/4,即把256个高电平时钟周期平均的分配到1024个之中周期中,可以这样进行处理,每4个时钟周期为一组,其中的一个周期内为高电平,这样即可实现”平衡,这时真实的PWM输出帧率为1.2MHz/4=300KHz,如以上左图所示;对于MarkSpace模式来说,占空比为M/S=(RANGE/4)/RANGE=256/1024,这种模式不需要进行平衡,即可以认为1024个时钟周期的前256个为高电平,其余的为低电平,这时真实的PWM输出帧率为1.2MHz/1024=1171.875Hz,如以上右图所示。

2 树莓派播放音乐

2.1 乐理知识

一首乐曲有若干音符组成,每个音符由音调和演奏时间组成。不同的音调在物理上就对应不同频率的音波。所以我们只要控制输出的频率和时长就能输出一首音乐了。当然实际的音乐很复杂,又有连接,还有重音什么的,这个就先不在讨论范围内了。

每个音符都会播放一定的时间,这样就能构成一首歌曲。在音乐上,音符节奏分为1拍、1/2拍、1/4拍、1/8拍,假设一拍音符的时间为1;半拍为0.5;1/4拍为0.25;1/8拍为0.125……,所以我们可以为每个音符赋予这样的拍子播放出来,音乐就成了。

Arduino官方网站给出了不同音符对应的不同频率的头文件pitches.h,相关内容可以参考博文,在本文我们把pitches.h文件直接应用到树莓派,该文件内容如下:

详解树莓派Model B+控制蜂鸣器演奏乐曲详解树莓派Model B+控制蜂鸣器演奏乐曲

/*************************************************
  * Public Constants
  *************************************************/
 
 #define NOTE_B0  31
 #define NOTE_C1  33
 #define NOTE_CS1 35
 #define NOTE_D1  37
 #define NOTE_DS1 39
 #define NOTE_E1  41
 #define NOTE_F1  44
 #define NOTE_FS1 46
 #define NOTE_G1  49
 #define NOTE_GS1 52
 #define NOTE_A1  55
 #define NOTE_AS1 58
 #define NOTE_B1  62
 #define NOTE_C2  65
 #define NOTE_CS2 69
 #define NOTE_D2  73
 #define NOTE_DS2 78
 #define NOTE_E2  82
 #define NOTE_F2  87
 #define NOTE_FS2 93
 #define NOTE_G2  98
 #define NOTE_GS2 104
 #define NOTE_A2  110
 #define NOTE_AS2 117
 #define NOTE_B2  123
 #define NOTE_C3  131
 #define NOTE_CS3 139
 #define NOTE_D3  147
 #define NOTE_DS3 156
 #define NOTE_E3  165
 #define NOTE_F3  175
 #define NOTE_FS3 185
 #define NOTE_G3  196
 #define NOTE_GS3 208
 #define NOTE_A3  220
 #define NOTE_AS3 233
 #define NOTE_B3  247
 #define NOTE_C4  262
 #define NOTE_CS4 277
 #define NOTE_D4  294
 #define NOTE_DS4 311
 #define NOTE_E4  330
 #define NOTE_F4  349
 #define NOTE_FS4 370
 #define NOTE_G4  392
 #define NOTE_GS4 415
 #define NOTE_A4  440
 #define NOTE_AS4 466
 #define NOTE_B4  494
 #define NOTE_C5  523
 #define NOTE_CS5 554
 #define NOTE_D5  587
 #define NOTE_DS5 622
 #define NOTE_E5  659
 #define NOTE_F5  698
 #define NOTE_FS5 740
 #define NOTE_G5  784
 #define NOTE_GS5 831
 #define NOTE_A5  880
 #define NOTE_AS5 932
 #define NOTE_B5  988
 #define NOTE_C6  1047
 #define NOTE_CS6 1109
 #define NOTE_D6  1175
 #define NOTE_DS6 1245
 #define NOTE_E6  1319
 #define NOTE_F6  1397
 #define NOTE_FS6 1480
 #define NOTE_G6  1568
 #define NOTE_GS6 1661
 #define NOTE_A6  1760
 #define NOTE_AS6 1865
 #define NOTE_B6  1976
 #define NOTE_C7  2093
 #define NOTE_CS7 2217
 #define NOTE_D7  2349
 #define NOTE_DS7 2489
 #define NOTE_E7  2637
 #define NOTE_F7  2794
 #define NOTE_FS7 2960
 #define NOTE_G7  3136
 #define NOTE_GS7 3322
 #define NOTE_A7  3520
 #define NOTE_AS7 3729
 #define NOTE_B7  3951
 #define NOTE_C8  4186
 #define NOTE_CS8 4435
 #define NOTE_D8  4699
 #define NOTE_DS8 4978
pitches.h

可以看到,这是一张类似表格的东西,里面是定义的大量的宏,即用宏名代替了频率名,对应到键盘的各个按键上。我们需要对应相应的音符到宏名上,为了实现这个首先看看钢琴大谱表与钢琴琴键的对照表:

详解树莓派Model B+控制蜂鸣器演奏乐曲

为了将个音符的音名直观的看出来,给出以下表格:

详解树莓派Model B+控制蜂鸣器演奏乐曲

2.2 播放音乐

对照以上表格及射雕英雄传主题曲铁血丹心简谱实现树莓派播放,铁血丹心简谱如下:

详解树莓派Model B+控制蜂鸣器演奏乐曲

上面的简谱中缺少前奏,程序中增加了从其他版本中摘录的前奏部分,主程序tiexuedanxin.c代码如下:

详解树莓派Model B+控制蜂鸣器演奏乐曲详解树莓派Model B+控制蜂鸣器演奏乐曲

1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <stdint.h>
  4 
  5 #include <bcm2835.h>
  6 #include "pitches.h"
  7 
  8 #define PWM_CHANNEL 0
  9 typedef struct _TONE{
 10   int freq;
 11   int t_ms;
 12 } TONE,*PTONE;
 13 
 14 int pin = RPI_GPIO_P1_12;
 15 int baseFreq = 600000;          // BCM2835_PWM_CLOCK_DIVIDER_32 对应600KHz
 16 
 17 typedef struct _melodyNode{ 
 18   int note;
 19   float fDuration;
 20 }melodyNode;
 21 
 22 melodyNode melody[]= {
 23   // 1
 24   {NOTE_A4, 1.5},      // 6
 25   {NOTE_G4, 0.5},      // 5
 26   {NOTE_A4, 1},        // 6
 27   {NOTE_G4, 0.5},      // 5
 28   {NOTE_E4, 0.5},      // 3
 29   
 30   // 2
 31   {NOTE_G4, 1},        // 5
 32   {NOTE_D4, 3},        // 2
 33   
 34   // 3
 35   {NOTE_C4, 1.5},      // 1
 36   {NOTE_A3, 0.5},      // .6
 37   {NOTE_D4, 0.5},      // 2
 38   {NOTE_E4, 0.5},      // 3
 39   {NOTE_G4, 0.5},      // 5
 40   {NOTE_F4, 0.5},      // 4
 41   
 42   // 4
 43   {NOTE_E4, 3},        // 3
 44   {NOTE_E4, 0.5},      // 3
 45   {NOTE_G4, 0.5},      // 5
 46   
 47   // 5
 48   {NOTE_A4, 1.5},      // 6
 49   {NOTE_G4, 0.5},      // 5
 50   {NOTE_A4, 1},        // 6
 51   {NOTE_G4, 0.5},      // 5
 52   {NOTE_E4, 0.5},      // 5
 53   
 54   // 6
 55   {NOTE_G4, 1},        // 5
 56   {NOTE_D4, 3},        // 2
 57   
 58   // 7
 59   {NOTE_C4, 1.5},      // 1
 60   {NOTE_A3, 0.5},      // .6
 61   {NOTE_D4, 0.5},      // 2
 62   {NOTE_E4, 0.5},      // 3
 63   {NOTE_G3, 0.5},      // .5
 64   {NOTE_B3, 0.5},      // .7
 65   
 66   // 8
 67   {NOTE_A3, 4},        // .6
 68   
 69   {0, 1},              // 0
 70   {NOTE_E4, 0.5},      // 3
 71   {NOTE_D4, 0.5},      // 2
 72   {NOTE_C4, 1.5},      // 1
 73   {NOTE_B3, 0.5},      // .7
 74   
 75   //
 76   {NOTE_A3, 1.5},      // .6
 77   {NOTE_E3, 0.5},      // .3
 78   {NOTE_A3, 2},        // .6
 79   
 80   //{NOTE_A3, 1},        // .6
 81   {NOTE_A4, 0.5},      // 6
 82   {NOTE_G4, 0.5},      // 5
 83   {NOTE_E4, 1},        // 3
 84   {NOTE_G4, 0.5},      // 5
 85   {NOTE_D4, 0.5},      // 2
 86   
 87   {NOTE_E4, 3},        // 3
 88   
 89   {NOTE_E4, 0.5},      // 3
 90   {NOTE_D4, 0.5},      // 2
 91   {NOTE_C4, 1.5},      // 1
 92   {NOTE_B3, 0.5},      // .7
 93   
 94   {NOTE_A3, 1.5},        // .6
 95   {NOTE_E3, 0.5},        // .6
 96   {NOTE_A3, 2},          // .6
 97   
 98   {0, 1},              // 0
 99   {NOTE_D4, 0.5},      // 2
100   {NOTE_C4, 0.5},      // 1
101   {NOTE_A3, 1},        // .6
102   {NOTE_C4, 0.5},      // 1
103   {NOTE_D4, 0.5},      // 1
104   
105   {NOTE_E4, 3},        // 3*/
106   {NOTE_E4, 1},        // 3 逐草四方
107   
108   {NOTE_A4, 1.5},      // 6
109   {NOTE_G4, 0.5},      // 5
110   {NOTE_A4, 1},        // 6
111   {NOTE_G4, 0.5},      // 5
112   {NOTE_E4, 0.5},      // 3
113   
114   {NOTE_G4, 1},        // 5
115   {NOTE_D4, 3},        // 2
116   
117   {NOTE_C4, 1.5},      // 1
118   {NOTE_A3, 0.5},      // .6
119   {NOTE_D4, 0.5},      // 2
120   {NOTE_E4, 0.5},      // 3
121   {NOTE_G4, 0.5},      // 5
122   {NOTE_FS4, 0.5},     // #4
123   
124   {NOTE_E4, 3},        // 3
125   {NOTE_E4, 0.5},      // 3
126   {NOTE_G4, 0.5},      // 5
127   
128   {NOTE_A4, 1.5},      // 6
129   {NOTE_G4, 0.5},      // 5
130   {NOTE_A4, 1.0},      // 6
131   {NOTE_G4, 0.5},      // 5
132   {NOTE_E4, 0.5},      // 3
133   
134   {NOTE_G4, 1.0},      // 5
135   {NOTE_D4, 3},        // 2
136   
137   {NOTE_C4, 1.5},      // 1
138   {NOTE_A3, 0.5},      // .6
139   {NOTE_D4, 0.5},      // 2
140   {NOTE_E4, 0.5},      // 3
141   {NOTE_G3, 0.5},      // .5
142   {NOTE_B3, 0.5},      // .7
143   
144   {NOTE_A3, 3},         // .6
145   
146   {0, 1},              // 0
147   {NOTE_E4, 0.5},      // 3 应知爱意似
148   {NOTE_D4, 0.5},      // 2
149   {NOTE_C4, 1.0},      // 1
150   {NOTE_C4, 0.5},      // 1
151   {NOTE_B3, 0.5},      // .7
152   
153   {NOTE_A3, 1.5},      // .6
154   {NOTE_E3, 0.5},      // .3
155   {NOTE_A3, 2.0},      // .6
156   
157   {0, 1},              // 0
158   {NOTE_A3, 0.5},      // .6
159   {NOTE_G3, 0.5},      // .5
160   {NOTE_E3, 1.0},      // .3
161   {NOTE_G3, 0.5},      // .5
162   {NOTE_D3, 0.5},      // .2
163   
164   {NOTE_E3, 3.0},      // .3
165   
166   {0, 1},              // 0
167   {NOTE_E4, 0.5},      // 3 身经百劫也
168   {NOTE_D4, 0.5},      // 2
169   {NOTE_C4, 1.0},      // 1
170   {NOTE_C4, 0.5},      // 1
171   {NOTE_B3, 0.5},      // .7
172   
173   {NOTE_A3, 1.5},      // .6
174   {NOTE_E4, 0.5},      // 3
175   {NOTE_D4, 2.0},      // 2
176   
177   {0, 1},              // 0
178   {NOTE_D4, 0.5},      // 2
179   {NOTE_C4, 0.5},      // 1
180   {NOTE_A3, 1.0},      // .6
181   {NOTE_B3, 0.5},      // .7
182   {NOTE_G3, 0.5},      // .5
183   
184   {NOTE_A3, 3.0},      // .6
185 };
186 
187 void beep(int freq, int t_ms)
188 {
189     int range;
190     /*if(freq<2000||freq>5000)
191     {
192         printf("invalid freq\n");
193         return;
194     }*/
195     if(freq == 0)
196         range=1;
197     else
198         range=baseFreq/freq;
199     printf("will call bcm2835_pwm_set_range freq: %d range: %d\n", freq, range);
200     bcm2835_pwm_set_range(PWM_CHANNEL, range);
201     bcm2835_pwm_set_data(PWM_CHANNEL, range/2);
202     if(t_ms>0)
203     {
204         delay(t_ms);
205     }
206 }
207 
208 void init()
209 {
210     if (!bcm2835_init())
211         exit (1) ;
212     
213     printf("will init pin %d\n", pin);
214     // Set the output pin to Alt Fun 5, to allow PWM channel 0 to be output there
215     bcm2835_gpio_fsel(pin, BCM2835_GPIO_FSEL_ALT5);
216     bcm2835_pwm_set_clock(BCM2835_PWM_CLOCK_DIVIDER_32);
217     bcm2835_pwm_set_mode(PWM_CHANNEL, 1, 1);
218 }
219 
220 int main (void)
221 {
222     int index=0;
223     int nLen = sizeof(melody)/sizeof(melody[0]);
224     init();
225 
226     for ( ; index<nLen; index++) 
227     {
228         int noteDuration = 600*melody[index].fDuration;
229         beep(melody[index].note, noteDuration);
230         printf("will call bcm2835_pwm_set_data 0 after beep\n");
231         bcm2835_pwm_set_data(PWM_CHANNEL, 0);
232         printf("index: %d nLen: %d@@@@@@@@@@@@\n", index, nLen);
233         //delay(100);
234     }
235 
236     bcm2835_pwm_set_data(PWM_CHANNEL, 0);
237     bcm2835_close();
238     
239     return 0 ;
240 }
tiexuedanxin

注意代码中195行做了特殊处理,这时候频率并不是为0,只是让树莓派不再发声。