2016年9月17日 星期六

Arduino 的聲音測試 (一)

很早就想做這個實驗, 因為只需要 Arduino NANO 加上一個蜂鳴器就可以進行了, 頂多加一顆按鈕開關, 這跟物聯網無關, 因此不需要用到 ESP8266 上網. 趁著中秋連假, 就把這個簡單的小實驗做完吧!

以下實驗所用的程式是參考下列幾本書裡面的範例加以修改來的 :
  1. Arduino 輕鬆入門, 葉難, 博碩出版 (第 6 章)
  2. Arduino 互動設計入門 2, 趙英傑, 旗標 (第 13-1, 13-2 節)
  3. Arduino 最佳入門與應用第二版, 楊明峰, 碁峰 (第 11 章)
  4. Arduino Cookbook 錦囊妙計, 歐萊禮 (第 9 節)
我使用之前製作的 Arduino NANO+ESP8266 IOT 模組來進行實驗, 在設計此模組時有考量到如果偵測到異常狀況可能需要發出警告音, 所以有加裝了一個無源蜂鳴器 (Passive piezo speaker), 透過一個跳線針腳連接到 Arduino 的 D9 腳, 另一端接地即可. 由於無源蜂鳴器為高阻抗, 因此不需要串聯限流電阻. 如果專案中不需要用到蜂鳴器, 且又必須用到 D9 腳時, 可將跳線帽拔掉, 讓蜂鳴器與 D9 脫鉤即可, 如下圖左半部所示 (左上是跳線帽, 左下圓形物為蜂鳴器, 此為第一版的 IOT 模組) :



關於此模組佈線圖與製作方式, 參考 :

# 製作 Arduino Nano + ESP8266 物聯網模組 (四) : 完結篇

注意, 購買蜂鳴器要指名無源蜂鳴器, 這樣才能透過 PWM 方式控制其發聲頻率 (又稱為它激式), 不要買到有源蜂鳴器, 這種蜂鳴器內建震盪電路 (又稱為自激式), 只要一通電就會發出固定頻率的聲音, 無法利用 PWM 對其音頻進行控制. 另外還要注意其電壓, 要配合 Arduino 板是跑 3.3V 還是 5V 而定, 我都使用 5V 系統, 需要 3.3V 時再用 AMS1117 來降壓. 我買的無源蜂鳴器長相如下 :



剛買來上面有一張貼紙, 標示其接腳有分正負極, 長腳為正, 短腳為負.

首先說明一下蜂鳴器的發聲原理, 一般的無源蜂鳴器主要是由一片金屬片 (銅片) 與壓電感應材料構成, 通電後金屬片會移動 (稱為壓電效應), 無電時金屬片復位, 亦即壓電效應可將電能轉換成機械能, 跟一般喇叭的音盆震動發聲原理相似.

原本聲音是連續的類比信號, 數位系統無法輸出類比信號, 但是可以利用不同頻率的方波來發聲, 因為如果對蜂鳴器施予週期性方波, 金屬片就會來回震動, 只要方波週期在 20Hz 到20KHz 之間, 就會發出人耳聽得到的聲音.


利用數位方波來模擬類比效果的方法稱為脈寬調變 PWM (Pulse Width Modulation), 參考 :

[Arduino] 脈衝寬度調變 (PWM) 與 Arduino – Pulse Width Modulation
Make: Projects|技能培養系列:進階Arduino聲響合成技術
[Arduino] 脈衝寬度調變 (PWM) 與 Arduino – Pulse Width Modulation

PWM 有兩個參數, 一個是單位為 Hz 的頻率, 即週期的倒數, 它決定了聲音的音高 (Pitch); 另一個參數是 Duty Cycle (工作週期), 方波一個週期有 HIGH (峰值) 與 LOW (谷值) 兩個狀態, 峰值時間佔整個週期的百分比稱為 Duty Cycle, 它決定了聲音的音色.

Duty cycle=峰值時間/週期

Arduino 內建了 tone() 與 noTone() 兩個函式來對數位接腳輸出 Duty Cycle 為 50% 的方波 (即 HIGH, LOW 各佔週期之一半) :

 函式 說明
 tone(pin, frequency, duration) 對指定數位接腳 pin 發出 duration 毫秒, 頻率為 frequency 之方波
 tone(pin, frequency)  對指定數位接腳 pin 持續發出頻率為 frequency 之方波
 noTone(pin) 停止對指定數位接腳 pin 發出方波

其中 tone() 有三個參數 tone(pin, frequency, duration) 與兩個參數 tone(pin, frequency) 兩種, 前者有指定發音期間的時間到時就會停止; 而後者會一直發音直到呼叫 noTone(pin) 才會停止.

注意, Arduino 同時只能對一個數位接腳輸出一個音調, 它既不能同時讓兩個數位接腳發聲, 也不能對同一接腳發出兩種頻率的聲音. 這是因為 ATmega328 處理器只有三個硬體計時器, 其中 timer0 被 millis() 函式用掉了 (它也負責 D5 與 D6 的 PWM 輸出); 而 timer1 則被 Servo.h 函式庫用掉了 (它也負責 D9 與 D10 的 PWM 輸出); 所以 tone() 只能用剩下的 timer2 來產生音調. 注意, 因為 timer2 負責 D3 與 D11 的 PWM, 所以使用 tone() 函數時會干擾這兩個接腳的 PWM 輸出功能. ATmega328 硬體計時器使用情形摘要如下 :

 硬體計時器 控制 PWM 接腳 預設使用者
 timer0 D5, D6 millis() 
 timer1 D9, D10 Servo.h
 timer2 D3, D11 tone()

參考 "Arduino 輕鬆入門" 第 6-4 節說明以及下列文章 :

Questions about millis()
# http://playground.arduino.cc/ComponentLib/Servotimer1

下面我們就來測試一下 tone() 函數, 首先測試 1 是讓 Arduino 發出 1 KHz 的告警音, 這可以用在偵測到異常時發出訊號用 :

測試 1 :

int buzzerPin=9;  //D9 conectted to a buzzer

void setup() {
 pinMode(buzzerPin, OUTPUT);
 }

void loop() {
  alarmBeep(buzzerPin);
  }

void alarmBeep(int pin) {
  tone(pin, 1000, 1000);
  delay(2000);
  }

此程式會先發出 1 秒 Bee 後停兩秒, 如此周而復始.

下面測試 2 則是模擬鬧鐘的鬧鈴音 :

測試 2  :

int buzzerPin=9;

void setup() {
 pinMode(buzzerPin, OUTPUT);
 }

void loop() {
  alarmClockBeep(buzzerPin);  
  }

void alarmClockBeep(int pin) {
  tone(pin, 1000, 100);
  delay(200);
  tone(pin, 1000, 100);
  delay(200);
  tone(pin, 1000, 100);
  delay(200);
  tone(pin, 1000, 100);
  delay(1000);
  }

此程式是發出 4 聲短促的 Bee 聲後暫停 1 秒, 周而復始.

另外我在 "Arduino 最佳入門與應用" 11-3 節看到一個模擬電話鈴聲的範例, 我將其改寫為如下測試 3 :

測試 3 :

int buzzerPin=9;

void setup() {
 pinMode(buzzerPin, OUTPUT);
 }

void loop() {
  ringTone(buzzerPin);  
  }

void ringTone(int pin) {
  for (int i=0; i<10; i++) { //repeat 10 times
    tone(pin, 1000);
    delay(50);
    tone(pin, 500);
    delay(50);
    }
  noTone(pin);
  delay(2000);
  }

電話振鈴音是以 1000 Hz 與 500 Hz 間隔 50 毫秒重複 10 次模擬出來的, 然後用 noTone() 來暫停 2 秒. 我覺得這電話鈴聲跟真的一樣耶! 還蠻有趣的.

還有一個常作為告警鈴聲的是警車的笛聲, 我參考了下面這篇文章加以改編成測試 4 :

# Arduino Police Light and Sound

測試 4 :

int buzzerPin=9;

void setup() {
 pinMode(buzzerPin, OUTPUT);
 }

void loop() {
  policeSiren(buzzerPin);  
  }

void policeSiren(int pin) {
  for (int i=150; i<1800; i++) { //upward tone
    tone(pin, i, 10);
    delay(1);
    }
  for (int i=1800; i>150; i--) { //downward tone
    tone(pin, i, 10);  
    delay(1);
    }
  }

還有一個可以表示系統有問題的音訊是救護車 "歐伊歐伊" 的笛聲, 我參考下面這篇文章裡的範例, 將其修改為測試 5  :

Arduino Fun – Door Entry Alarm

測試 5 :

int buzzerPin=9;

void setup() {
 pinMode(buzzerPin, OUTPUT);
 }

void loop() {
  ambulenceSiren(buzzerPin);  
  }

void ambulenceSiren(int pin) {
  tone(pin, 400);        
  delay(500);
  tone(pin, 800);        
  delay(500);
  noTone(pin);
  }

其他參考 :

# Arduino Siren Sound Generator Circuit - Customizable

除了產生機械音外, Arduino 也可以產生樂音, 即依據樂理發出特定頻率與節拍的聲音. 頻率部分, 聲音的頻率有時又被稱為音高 (Pitch), 但所謂的音高其實是指我們的聽覺對物理世界中的聲音頻率之主觀認知, 與機械性客觀的頻率意涵不同, 但在指涉頻率高低上兩者意思一樣, 所以兩個詞也就經常被混用了.

樂理上把一個音階分成了 8 個音度, 首尾兩個 Do 在頻率上剛好相差 2 倍. 而這 8 個音度加上半音又分成 12 個音符, 音符有唱名 (Do, Re, Mi, Fa, So, La, Si, Do) 與相對之音名 (A, B, C, D, E, F, G, A) 兩種標示. 其中的 C 大調低音 La 為 440 Hz, 被用來作為樂器調校的標準音. 一個音階的 12 個音符要差兩倍頻率的話, 每一個頻率之間的乘數就是 2^(1/12), 即 2 開 12 次方, 大約是 1.05946 倍. 根據此倍數可計算出各音符的近似頻率如下表 :

 1 2 3 4 5 6 7 8 9 10 11 12
音名 C C# D D# E F F# G G# A A# B
 0 16 17 18 19 21 22 23 25 26 28 29 31
 1 33 35 37 39 41 44 46 49 52 55 58 62
 2 65 69 73 78  82  87  93  98  104 110 117 123
 3 131 139 147 156 165 175 185 196 208 220 233 247
 4 262 277 294 311 330 349 370 392 415 440 466 493
 5 523 554 587 622 659 698 740 784 831 880 932 988
 6 1046 1109 1175 1245 1319 1397 1480 1568 1661 1760 1864 1976
 7 2093 2217 2349 2489 2637 2794 2960 3136 3322 3520 3729 3951
 8 4186 4435 4699 4978 5274 5588 5920 6272 6645 7040 7459 7902

其中藍色字體部分為標準 88 鍵樂器的音域範圍, 而 C4 (262 Hz) 即為鋼琴鍵盤的中央 C (C 大調低音 Do). 依據這張樂音頻率分配表就可以利用 tone() 函數讓蜂鳴器發出各種音符了.

除了頻率之外還需要節拍 (tempo) 才能構成音樂的旋律, 這就需要利用 tone() 函數的持續時間以及 delay() 函數來模擬停頓效果了, 關於節拍參考 :

# 音符時值

下列測試 6 是我參考 "Arduino 互動設計入門 2" 13-1 節瑪莉歐旋律的範例加以改編的 :

測試 6 :

int buzzerPin=9;

void setup() {
 pinMode(buzzerPin, OUTPUT);
 }

void loop() {
  mario(buzzerPin);
  }

void mario(int pin) {
  tone(pin, 659, 150);  //E5
  delay(150);  //pause for key transition
  tone(pin, 659, 150);  //E5
  delay(150);  //pause for key transition
  tone(pin, 659, 150);  //E5
  delay(150);  //pause for key transition
  delay(150);  //pause for a Quarter rest (1/4)
  tone(pin, 523, 150);  //C5
  delay(150);  //pause for key transition
  tone(pin, 659, 150);
  delay(150);  //pause for key transition
  delay(150);  //pause for a Quarter rest (1/4)
  tone(pin, 784, 150);  //G5
  delay(3000);
  }

這裡每個音符之間要用 delay(150) 來模擬每個琴鍵轉換之空檔, 否則所有的音符就會黏在一起. 另外, 四分休止符的時間長度也剛好是停頓 150 毫秒. 如果要像上面的程式一樣, 每次都要查頻率表實在太麻煩了, 而且一堆數字看起來令人眼花撩亂, 其實每個音符可以用 define 或 const 將頻率以音名來代換, 這樣程式可讀性比較高, 如下測試 7 所示 :

測試 7 :

#define E5 659
#define C5 523
#define G5 784

int buzzerPin=9;

void setup() {
 pinMode(buzzerPin, OUTPUT);
 }

void loop() {
  mario(buzzerPin);
  }

void mario(int pin) {
  tone(pin, E5, 150);  //E5
  delay(150);  //pause for key transition
  tone(pin, E5, 150);  //E5
  delay(150);  //pause for key transition
  tone(pin, E5, 150);  //E5
  delay(150);  //pause for key transition
  delay(150);  //pause for a Quarter rest (1/4)
  tone(pin, C5, 150);  //C5
  delay(150);  //pause for key transition
  tone(pin, E5, 150);
  delay(150);  //pause for key transition
  delay(150);  //pause for a Quarter rest (1/4)
  tone(pin, G5, 150);  //G5
  delay(3000);
  }

如果換另一首樂曲就要 define 所用到的音符的話很麻煩, 乾脆將上面的頻率分配表另外打成一個 notes.h 檔, 然後用 include 匯入即可. 此檔內容如下 :

#define C0  18
#define CS0 17
#define D0  18
#define DS0 19
#define E0  21
#define F0  22
#define FS0 23
#define G0  25
#define GS0 26
#define A0  28
#define AS0 29
#define B0  31
#define C1  33
#define CS1 35
#define D1  37
#define DS1 39
#define B0  31
#define C1  33
#define CS1 35
#define D1  37
#define DS1 39
#define E1  41
#define F1  44
#define FS1 46
#define G1  49
#define GS1 52
#define A1  55
#define AS1 58
#define B1  62
#define C2  65
#define CS2 69
#define D2  73
#define DS2 78
#define E2  82
#define F2  87
#define FS2 93
#define G2  98
#define GS2 104
#define A2  110
#define AS2 117
#define B2  123
#define C3  131
#define CS3 139
#define D3  147
#define DS3 156
#define E3  165
#define F3  175
#define FS3 185
#define G3  196
#define GS3 208
#define A3  220
#define AS3 233
#define B3  247
#define C4  262
#define CS4 277
#define D4  294
#define DS4 311
#define E4  330
#define F4  349
#define FS4 370
#define G4  392
#define GS4 415
#define A4  440
#define AS4 466
#define B4  494
#define C5  523
#define CS5 554
#define D5  587
#define DS5 622
#define E5  659
#define F5  698
#define FS5 740
#define G5  784
#define GS5 831
#define A5  880
#define AS5 932
#define B5  988
#define C6  1047
#define CS6 1109
#define D6  1175
#define DS6 1245
#define E6  1319
#define F6  1397
#define FS6 1480
#define G6  1568
#define GS6 1661
#define A6  1760
#define AS6 1865
#define B6  1976
#define C7  2093
#define CS7 2217
#define D7  2349
#define DS7 2489
#define E7  2637
#define F7  2794
#define FS7 2960
#define G7  3136
#define GS7 3322
#define A7  3520
#define AS7 3729
#define B7  3951
#define C8  4186
#define CS8 4435
#define D8  4699
#define DS8 4978
#define E8  5274
#define F8  5588
#define FS8 5920
#define G8  6272
#define GS8 6645
#define A8  7040
#define AS8 7459
#define B8  7902

由於係自訂的標頭檔, 因此要放在主檔案目錄內, 如下圖所示 :



而且要用雙引號匯入 (內建函式庫才是用角括號匯入) :

# include "notes.h"

使用 notes.h 將上面程式改為如下測試 8 :

測試 8 :

#include "notes.h"

int buzzerPin=9;

void setup() {
 pinMode(buzzerPin, OUTPUT);
 }

void loop() {
  mario(buzzerPin);
  }

void mario(int pin) {
  tone(pin, E5, 150);
  delay(150);
  tone(pin, E5, 150);
  delay(150);
  tone(pin, E5, 150);
  delay(300);
  tone(pin, C5, 150);
  delay(150);
  tone(pin, E5, 150);
  delay(300);
  tone(pin, G5, 150);
  delay(3000);
  }

效果是一樣的. 在 Arduino IDE 中也有內建一個 toneMelody 範例, 可從 "檔案/範例/02.Digital/toneMelody" 開啟, 我將其改編為如下測試 9 :

測試 9 :

#include "notes.h"

int buzzerPin=9;
int note[]={C4, G3, G3, A3, G3, 0, B3, C4};  //note name
int duration[]={4, 8, 8, 4, 4, 4, 4, 4};  //note type (tempo)

void setup() {
 pinMode(buzzerPin, OUTPUT);
 }

void loop() {
  melody(buzzerPin);
  }

void melody(int pin) {
  for (int i=0; i<8; i++) { //visit 8 notes
    int d=1000/duration[i]; //transform tempo to ms
    tone(pin, note[i], d);
    int p=d * 1.3; //suitable pause=plus 30% duration
    delay(p); //pause between notes
    noTone(8);
    }
  delay(2000);
  }

在這個程式中, 我分別為音符與節拍定義了 note 與 duration 兩個陣列, 其中 note 第 6 個音符 0 表示頻率為 0, 也就是休止符的意思. 而 duration 陣列之值, 4 表示 四分音符; 8 表示八分音符, 這樣可讀性比較高, 但在 melody() 函數中, 我們必須將其轉換成毫秒數, 在此以全音符為 1 秒, 則四分音符為 1000/4=250 毫秒, 亦即用 1000 毫秒除以 duration 陣列的元素值即可. 另外每個音符之間必須模擬一個琴鍵轉換的停頓, 否則每一個音符會黏在一起, 這裡以節拍的 1.3 倍時間為最適宜.

在 "Arduino 最佳入門與應用" 的 11-3-4 節有一個演奏小蜜蜂的範例, 我把它改編成下列測試 10, 小蜜蜂的簡譜如下 :

|5 3 3 - |4 2 2 - |1 2 3 4 |5 5 5 - |

|5 3 3 - |4 2 2 - |1 3 5 5 |3 - - - |

|2 2 2 2 |2 3 4 - |3 3 3 3 |3 4 5 –|

|5 3 3 - |4 2 2 - |1 3 5 5 |1 - - - |

參考 :

# 小蜜蜂簡譜

其中 1, 2, 3, 4, 5 分別對應到 C5, D5, E5, F5, G5 五個音符, 將其轉換成 note[] 陣列, 每個音節有 4 拍, 以四分音符當作一拍.

測試 10 :

#include "notes.h"

int buzzerPin=9;

int note[]={G5, E5, E5, 0, F5, D5, D5, 0, C5, D5, E5, F5, G5, G5, G5, 0,
            G5, E5, E5, 0, F5, D5, D5, 0, C5, E5, G5, G5, E5, 0, 0, 0,
            D5, D5, D5, D5, D5, E5, F5, 0, E5, E5, E5, E5, E5, F5, G5, 0,
            G5, E5, E5, 0, F5, D5, D5, 0, C5, E5, G5, G5, C5, 0, 0, 0};
         
int duration[]={4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
                4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
                4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
                4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4};

void setup() {
 pinMode(buzzerPin, OUTPUT);
 }

void loop() {
  littleBee(buzzerPin, sizeof(note)/sizeof(int));
  }

void littleBee(int pin, int count) {
 for (int i=0; i<count; i++) {
    int d=1000/duration[i];
    tone(pin, note[i], d);
    int p=d * 1.3;
    delay(p);
    noTone(pin);
    }
  delay(2000);
  }

這裡比較特別的地方是要把 note[] 或 duration[] 陣列的長度傳進函數中, 以便 for 迴圈擷取樂譜中的全部音符與音長. 由於 sizeof() 事實上是傳回這個陣列所占的 byte 數, 並非元素個數, 因此必須除以此陣列元素之資料型別長度, 才會得到元素個數, 即陣列長度.

所以只要有簡譜, 我們可以很快地將其轉換成音符與音長陣列, 套用上面的程式架構讓 Arduino 透過蜂鳴器演奏音樂.

下面是小星星的簡譜 :

|1 1 5 5|6 6 5 -|4 4 3 3|2 2 1 -|

|5 5 4 4|3 3 2 -|5 5 4 4|3 3 2 -|

|1 1 5 5|6 6 5 -|4 4 3 3|2 2 1 -|

這裡簡譜的 1, 2, 3, 4, 5, 6 分別對應到 
C5, D5, E5, F5, G5, A5 六個音符, 將其轉換成 note[] 陣列, 每個音節有 4 拍, 以四分音符當作一拍. 其演奏程式如下 :

測試 11 :

#include "notes.h"

int buzzerPin=9;

int note[]={C5, C5, G5, G5, A5, A5, G5, 0, F5, F5, E5, E5, D5, D5, C5, 0,
            G5, G5, F5, F5, E5, E5, D5, 0, G5, G5, F5, F5, E5, E5, D5, 0,
            C5, C5, G5, G5, A5, A5, G5, 0, F5, F5, E5, E5, D5, D5, C5, 0};
         
int duration[]={4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
                4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
                4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4};

void setup() {
 pinMode(buzzerPin, OUTPUT);
 }

void loop() {
  littleStar(buzzerPin, sizeof(note)/sizeof(int));
  }

void littleStar(int pin, int count) {
  for (int i=0; i<count; i++) {
    int d=1000/duration[i];
    tone(pin, note[i], d);
    int p=d * 1.3;
    delay(p);
    noTone(pin);
    }
  delay(2000);
  }

很簡單齁! 其實凡事只要抓住了 pattern 或架構, 應用就很簡單, 就是把內容餵進去而已, 玩久了就是所謂 "熟能生巧" 的老狗把戲罷了. 


最後我想把上面小蜜蜂與小星星兩首曲子做成音樂盒, 透過按鈕控制播放不同歌曲, 開機時預設靜音, 按一下播放小蜜蜂, 按一下播放小星星, 再按一下又變靜音, 如此周而復始. 此實驗參考 "Arduino 最佳入門與應用 (第二版)" 的 11-3-5 節範例修改.

測試 12 :

#include "notes.h"

int buzzerPin=9;
int littleStarNote[]={C5, C5, G5, G5, A5, A5, G5, 0, F5, F5, E5, E5, D5, D5, C5, 0,
                      G5, G5, F5, F5, E5, E5, D5, 0, G5, G5, F5, F5, E5, E5, D5, 0,
                      C5, C5, G5, G5, A5, A5, G5, 0, F5, F5, E5, E5, D5, D5, C5, 0};
int littleStarDuration[]={4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
                          4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
                          4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4};
int littleBeeNote[]={G5, E5, E5, 0, F5, D5, D5, 0, C5, D5, E5, F5, G5, G5, G5, 0,
                     G5, E5, E5, 0, F5, D5, D5, 0, C5, E5, G5, G5, E5, 0, 0, 0,
                     D5, D5, D5, D5, D5, E5, F5, 0, E5, E5, E5, E5, E5, F5, G5, 0,
                     G5, E5, E5, 0, F5, D5, D5, 0, C5, E5, G5, G5, C5, 0, 0, 0};
int littleBeeDuration[]={4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
                         4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
                         4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
                         4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4};

const byte swPin=2;  //switch pin for interrupt
volatile int swValue=0;  //initial swValue:0=silent
int debounceDelay=200; //debounce delay (ms)

void setup() {
  pinMode(buzzerPin, OUTPUT);
  pinMode(swPin, INPUT_PULLUP); //enable pull-up resistor of input pin
  attachInterrupt(0, int0, LOW); //assign int0
  }

void loop() {
  switch (swValue) {
    case 1 :
      playLittleBee(buzzerPin, sizeof(littleBeeNote)/sizeof(int));
      break;
    case 2 :
      playLittleStar(buzzerPin, sizeof(littleStarNote)/sizeof(int));
      break;
    default :
      noTone(buzzerPin);  //silent buzzer
      break;  
    }
  }

void playLittleBee(int pin, int count) {
  for (int i=0; i<count; i++) {
    int d=1000/littleBeeDuration[i];
    tone(pin, littleBeeNote[i], d);
    int p=d * 1.3;
    delay(p);
    noTone(pin);
    }
  delay(2000);
  }

void playLittleStar(int pin, int count) {
  for (int i=0; i<count; i++) {
    int d=1000/littleStarDuration[i];
    tone(pin, littleStarNote[i], d);
    int p=d * 1.3;
    delay(p);
    noTone(pin);
    }
  delay(2000);
  }

void int0() { //interrupt handler
  if (debounced()) { //debounced:
    ++swValue; //increment swValue
    if (swValue > 2) {swValue=0;} //reset swValue
    }
  }

boolean debounced() { //check if debounced
  static unsigned long lastMillis=0; //record last millis
  unsigned long currentMillis=millis(); //get current elapsed time
  if ((currentMillis-lastMillis) > debounceDelay) {
    lastMillis=currentMillis; //update lastMillis with currentMillis
    return true; //debounced
    }
  else {return false;} //not debounced

  }

這個程式我使用了 D2 (中斷 0) 的硬體中斷來更改一個紀錄按鍵狀態的全域變數 swValue 之值, 每按一次就會將原本上拉至 HIGH 的 D2 準位拉到 LOW 而觸發中斷, 在中斷處理函數 int0() 裡面會將 swValue 增量, 但若超過 2 又會重置為 0, 藉此來讓 loop() 裡的 switch-case 判斷要執行哪一個演奏函數, 若為 1 執行 playLittleBee(); 若為 2 執行 playLittleStar(), 否則就靜音. 此程式使用了之前按鈕測試中所使用的去彈跳函數, 參考 :

# Arduino 按鈕開關測試 (二) : 硬體中斷法 (Interrupt)

注意, 按鈕按下去只是改變 swValue 之值, 並非馬上切換所播放之歌曲, 要等到目前播放的曲子結束, 從函式回到 loop() 迴圈時才會判斷 swValue 之值決定接下來要播放哪一曲或靜音. 

參考 :

# Use tone() with Arduino for an Easy Way to Make Noise
# Arduino Based Digital Clock with Alarm
Ardunio+蜂鳴器(Buzzer)播音樂
# [Arduino] 會唱歌的蜂鳴器 – Controlling Piezo
# Arduino 筆記 – Lab6 控制蜂鳴器發聲
# Brett Hagman 的 Tone 函式庫 (播放多個頻率)


12 則留言:

  1. 作者已經移除這則留言。

    回覆刪除
  2. 不好意思 小弟剛接觸這一塊 有很多東西不懂 想請教一下
    請問測試1中alarmBeep(buzzerPin)這句是甚麼意思呢?為什麼只把它放進void loop裡 而其他指令要另外宣告呢?
    如果要將測試一中的程式與while的語法搭配做開關控制又該如何寫呢?

    回覆刪除
  3. alarmBeep(buzzerPin) 是呼叫定義在後面的函數 alarmBeep(), 並且將全域變數 buzzerPin=9 傳遞給它. Arduino 程式主要架構由引入函式庫+巨集+全域變數, 初始設定函數 setup() 與無窮迴圈 loop() 函數構成, 只需要執行一次的例如初始值放在 setup() 裡, 需重複執行的邏輯放在 loop() 中. 有 loop 就不需要用 while 了, 只要在 loop() 中用 DigitalWrite() 函數輸出 HIGH/LOW 即可 :

    void loop() {
    digitalWrite(swPin, HIGH);
    delay(5000);
    digitalWrite(swPin, LOW);
    delay(5000);
    }

    回覆刪除
  4. 請問使用nodemcu接蜂鳴器,因為nodemcu工作電壓3.3V。那麼蜂鳴器要選用哪種電壓呢?3V?還是5V?

    回覆刪除
  5. Hi! 5V 蜂鳴器也可以給 3.3V 用喔! 只是聲音較小, 因為驅動電流變小了.

    回覆刪除
  6. 老師晚安:
    我是一名小六生,想請教您我初學習Arduinod課程,所使用的語言是C語言,您可推薦有關C語言的書嗎?謝謝您

    回覆刪除
  7. 您好, 小六就對這有興趣, 太好了! 其實網路上有系統的 C 語言教學資料很多, 不一定要買書, 我大部分的程式能力都是上網學來的, 只有找不到時才買書. 像下面這個網站就很好 :

    https://openhome.cc/Gossip/CGossip/

    買書的話, 下面這本應該適合你 :

    1.全民學程式設計:從插畫學C語言 (旗標, 林克鴻)
    https://www.books.com.tw/products/0010777399?sloc=main

    2. 最新C語言程式設計實例入門(第四版) (博碩, 高橋麻奈)
    https://www.books.com.tw/products/0010576829?sloc=main

    圖書館借得到就不用買了, 我大部分的技術書籍都是從圖書館借來的.

    回覆刪除
  8. 我有整理一篇 Arduino 速成的 C 語言語法 :

    https://yhhuang1966.blogspot.com/2015/09/arduino_14.html

    我的 Arduino 筆記總索引 :

    https://yhhuang1966.blogspot.com/2020/05/blog-post_15.html

    回覆刪除
  9. 為甚麼我用tinkercad上的模擬電路裡的蜂鳴器,再加上鮑率後就不會響了

    回覆刪除
  10. Hi, 線上模擬器可能有一些限制吧? 何不買實體來測試比較有臨場感?

    回覆刪除
  11. 請問要如何使一個開發版同時控制兩個蜂鳴器,並且兩個蜂鳴器要演奏出不同的旋律,我試過使用millis(),但只能做到讓兩個蜂鳴器同時發出同樣旋律,並且兩首旋律輪流撥放,另外我使用的開發版為ESP32不是UNO所以裡面的城市也會稍微跟UNO用的有點不同,試問有沒辦法修改,以下為程式碼:
    #include "notes.h"


    int note[]={0,0,0,0,0,0,F6,G6,F6,0,0,0,0,0,0,F6,G6,F6};

    int duration[]={4,4,4,4,4,4,12,12,12,4,4,4,4,4,4,12,12,12};

    int dutyCycle[]= {0,0,0,0,0,0,300,300,300,0,0,0,0,0,0,300,300,300};

    int notes[]={G4,D4,G4,D4,G4,D4,G5,A5,G5,D5,G4,D4,G4,D4,G4,D4,G5,A5,G5,D5};

    int durations[]={4,4,4,4,4,4,16,16,16,16,4,4,4,4,4,4,16,16,16,16,};

    int dutyCycles[]= {300,300,300,300,300,300,100,100,100,100,300,300,300,300,300,300,300,300,300,300,100,100,100,100};
    //宣告 Declare 區塊-----------------
    unsigned long intervals[] = {250,2000}; //this defines the interval for each task in milliseconds
    unsigned long last[] = {0,0}; //this records the last executed time for each task
    int buzzerPin=14;
    int Pin=27;//蜂鳴器控制腳位
    int freq = 2000;
    int Buzzer_Channel = 0; //channel提供0-15共16個通道。
    int resolution = 10; //duty cycle解析度(2^10=1024解析度,變數dutyCycle最大值為1024)

    //控制PWM脈寬,即可控制音量大小。脈寬越大、音量越大。

    void setup() {
    Serial.begin(115200);
    pinMode(buzzerPin, OUTPUT);
    pinMode(Pin, OUTPUT);
    ledcSetup(Buzzer_Channel, freq, resolution); //設定通道0的頻率與duty cycle解析度。
    ledcAttachPin(buzzerPin, Buzzer_Channel);
    ledcAttachPin(Pin, Buzzer_Channel);//控制蜂鳴器的G15腳位綁定於通道0
    }

    void loop() {
    unsigned long now = millis();
    if(now-last[0]>=intervals[0]){ last[0]=now; firstTask(buzzerPin, sizeof(note)/sizeof(int)); }
    if(now-last[1]>=intervals[1]){ last[1]=now; secondTask(Pin, sizeof(notes)/sizeof(int)); }

    //do other things here
    }

    void firstTask(int pin, int count) {
    for (int i=0; i<count; i++) {
    int d=1000/duration[i];
    ledcWriteTone(Buzzer_Channel, note[i]);
    ledcWrite(Buzzer_Channel, dutyCycle[i]);
    int p=d * 2.08;
    delay(p);
    }
    delay(1);
    }

    void secondTask(int pin, int count) {
    for (int i=0; i<count; i++) {
    int d=1000/durations[i];
    ledcWriteTone(Buzzer_Channel, notes[i]);
    ledcWrite(Buzzer_Channel, dutyCycles[i]);
    int p=d * 2.08;
    delay(p);
    }
    delay(1);
    }

    回覆刪除
  12. 抱歉, 我之前 ESP32 都以 MicroPython 做實驗, 很少用 Arduino C. 同時播放兩個蜂鳴器似乎要用多執行緒, 且 ESP32 有雙核, 這方面我沒試過, 您可參考這篇做法 :
    https://randomnerdtutorials.com/esp32-dual-core-arduino-ide/

    回覆刪除