最近看到Arduino可以利用pwm及蜂鳴器播放音樂,想到樹莓派上也有類似的pwm輸出,所以決定把相應的內容移植到樹莓派上,並給出了樹莓派播放鐵血丹心的例子! ...
步進電機以及無源蜂鳴器這些都需要脈衝信號才能夠驅動,這裡將用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:
圖中tpwm就是一個周期的時間長度。對於2KHz頻率來說,那麼周期就是1s/2K=500us。圖中的D叫做占空比,指的是高電平的時間占用整個周期時間的百分比。第一個周期D=50%,那麼就是高電平低電平的時間各占一半。接下來的D為33%,那就是通電時間為33%,剩餘的不通電時間占用67%。樹莓派Model B+有4個PIN腳支持PWM輸出,如下圖最右側:
但是,需要註意的是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分析
進行分析前先看一下實驗的物理電路連接:
圖中,紅色杜邦線一頭連接樹莓派的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的內容如下:
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,示波器分別捕獲到如下波形圖:
代碼第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文件直接應用到樹莓派,該文件內容如下:
1 /************************************************* 2 * Public Constants 3 *************************************************/ 4 5 #define NOTE_B0 31 6 #define NOTE_C1 33 7 #define NOTE_CS1 35 8 #define NOTE_D1 37 9 #define NOTE_DS1 39 10 #define NOTE_E1 41 11 #define NOTE_F1 44 12 #define NOTE_FS1 46 13 #define NOTE_G1 49 14 #define NOTE_GS1 52 15 #define NOTE_A1 55 16 #define NOTE_AS1 58 17 #define NOTE_B1 62 18 #define NOTE_C2 65 19 #define NOTE_CS2 69 20 #define NOTE_D2 73 21 #define NOTE_DS2 78 22 #define NOTE_E2 82 23 #define NOTE_F2 87 24 #define NOTE_FS2 93 25 #define NOTE_G2 98 26 #define NOTE_GS2 104 27 #define NOTE_A2 110 28 #define NOTE_AS2 117 29 #define NOTE_B2 123 30 #define NOTE_C3 131 31 #define NOTE_CS3 139 32 #define NOTE_D3 147 33 #define NOTE_DS3 156 34 #define NOTE_E3 165 35 #define NOTE_F3 175 36 #define NOTE_FS3 185 37 #define NOTE_G3 196 38 #define NOTE_GS3 208 39 #define NOTE_A3 220 40 #define NOTE_AS3 233 41 #define NOTE_B3 247 42 #define NOTE_C4 262 43 #define NOTE_CS4 277 44 #define NOTE_D4 294 45 #define NOTE_DS4 311 46 #define NOTE_E4 330 47 #define NOTE_F4 349 48 #define NOTE_FS4 370 49 #define NOTE_G4 392 50 #define NOTE_GS4 415 51 #define NOTE_A4 440 52 #define NOTE_AS4 466 53 #define NOTE_B4 494 54 #define NOTE_C5 523 55 #define NOTE_CS5 554 56 #define NOTE_D5 587 57 #define NOTE_DS5 622 58 #define NOTE_E5 659 59 #define NOTE_F5 698 60 #define NOTE_FS5 740 61 #define NOTE_G5 784 62 #define NOTE_GS5 831 63 #define NOTE_A5 880 64 #define NOTE_AS5 932 65 #define NOTE_B5 988 66 #define NOTE_C6 1047 67 #define NOTE_CS6 1109 68 #define NOTE_D6 1175 69 #define NOTE_DS6 1245 70 #define NOTE_E6 1319 71 #define NOTE_F6 1397 72 #define NOTE_FS6 1480 73 #define NOTE_G6 1568 74 #define NOTE_GS6 1661 75 #define NOTE_A6 1760 76 #define NOTE_AS6 1865 77 #define NOTE_B6 1976 78 #define NOTE_C7 2093 79 #define NOTE_CS7 2217 80 #define NOTE_D7 2349 81 #define NOTE_DS7 2489 82 #define NOTE_E7 2637 83 #define NOTE_F7 2794 84 #define NOTE_FS7 2960 85 #define NOTE_G7 3136 86 #define NOTE_GS7 3322 87 #define NOTE_A7 3520 88 #define NOTE_AS7 3729 89 #define NOTE_B7 3951 90 #define NOTE_C8 4186 91 #define NOTE_CS8 4435 92 #define NOTE_D8 4699 93 #define NOTE_DS8 4978pitches.h
可以看到,這是一張類似表格的東西,裡面是定義的大量的巨集,即用巨集名代替了頻率名,對應到鍵盤的各個按鍵上。我們需要對應相應的音符到巨集名上,為了實現這個首先看看鋼琴大譜表與鋼琴琴鍵的對照表:
為了將個音符的音名直觀的看出來,給出以下表格:
2.2 播放音樂
對照以上表格及射雕英雄傳主題曲鐵血丹心簡譜實現樹莓派播放,鐵血丹心簡譜如下:
上面的簡譜中缺少前奏,程式中增加了從其他版本中摘錄的前奏部分,主程式tiexuedanxin.c代碼如下:
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,只是讓樹莓派不再發聲。