2017年10月18日 星期三

Arduino 聲音感測模組測試

以前在露天買過一個麥克風模組, 也是放在零件箱中不見天日.

►267◄聲音感測器 聲音檢測模組 咪頭模組 聲控口哨開關 聲音模組 Arduino
向 allen_6833 採購電子零件模組一批

上週從母校高應大圖書館借到下面這本 Arduino 的書, 裡面12-1 節介紹麥克風模組, 可以用來偵測環境音量遂行控制, 例如聲控燈或聲控開關等.

跨入Maker物聯網時代 : 誰都可以用Arduino / 楊佩璐, 任昱衡編著

以下測試還參考了下面幾本書 :

# 超圖解 Arduino 互動設計入門 2, 趙英傑 (旗標)
# 輕鬆入門-Arduino 範例分析與實作設計, 葉難 (博碩)

麥克風模組 (或稱聲音感測模組, 麥克風放大器模組) 有兩種, 我買的這組是常見的三針腳式的模組, 只有 VCC, GND, 以及 DO (Digital Ooutput) 三隻腳, 運作電壓 3V~5V, 因此 Arduino 與 ESP8266 均可使用, 一般價位 17~40 元之間 :

T電子 現貨 Arduino模組 Arduino UNO R3 聲音模組 口哨模組 電子積木 聲音感測器 $30
Arduino 麥克風 放大器 聲音傳感器 模組 聲控 開關 數位輸出 01高低電平(附範例) $35

這種模組板上有一顆可變電阻來設定聲音感測的靈敏度, 當電容式麥克風感測到的聲音所產生的電壓高於靈敏度電阻所設之門檻電壓時, 板上的運算放大器 LM358 會驅動 OUT 針腳輸出 LOW 位準, 否則輸出 HIGH 位準. 缺點很明顯, 就是無法透過程式去控制門檻電壓, 所以不要買這種三腳貓的模組.

另外一種是四支接腳的, 它多了一個 AO 輸出 (Ananlog Output), 透過 Arduino 的類比輸入腳讀取經 A/D 轉換後, 呼叫 AnalogRead() 的會傳回值域 0~1023, 使用這樣的模組才能根據環境與應用來調整採取動作的門檻值 :

麥克風放大器模組 聲音模組MIC模組麥克風模組語音模組 $40
KY-037 高感度麥克風/聲音感測器模組 for Arduino / 附 範例 $40
廠家熱賣 麥克風放大器模組 聲音模組MIC模組麥克風模組語音模組 $40

也可以自己兜模組, 需要一個電容式麥克風, 一個 LM358 運算放大器, 電阻 1K (棕黑紅), 2.2K (紅紅紅), 68K (藍灰橙), 100K (棕黑黃) 各一個, 0.1uF (104) 電容一個, 參考趙英傑寫的 "超圖解 Arduino 互動設計入門 2" 這本書的 6-3 節, 電路圖如下 :




這裡主要的元件是 LM358 運算放大器與電容式麥克風, 可在露天購得 :

帶引腳 咪頭 6*5mm 電容式 駐極體話筒 拾音器 麥克風靈敏度52D $6
直插 LM358P 晶片 運算放大器 雙路 DIP-8 $3

LM358 內含兩組運算放大器, 這裡只用到其中一組而已. 上圖中麥克風的輸出會經過一個高通濾波器穿送至運算放大器的 + 腳, 然後與 1K+100K 分壓電阻進行差分放大, 其中 100K 電阻可改用 100K ~ 200K 可變電阻來調整信號放大倍率. 當然, 如果買有四隻腳的模組就比較方便, 不需要自己兜啦. 不過, 若要安裝在機箱中, 麥克風須拉到機殼外拾音, 或許自己兜會比較好安排元件配置.

下面測試 1 是從接到 Arduino A0 輸入的聲音感測模組的 AO 輸出讀取音量資料, 如果超過門檻值就點亮內建 LED, 否則就熄滅 :


測試 1 :  當環境聲音超過門檻值時點亮 LED

int MIC=A0;  //聲音感測模組 AO 輸出接至 A0 腳
int LED=13;  //Arduino 板上內建 LED
boolean toggle=false; //紀錄 LED 狀態,預設為熄滅
int micVal;  //紀錄偵測到的音量

void setup() {
  pinMode(LED, OUTPUT);
  Serial.begin(9600);
  }

void loop() {
  micVal=analogRead(MIC);  //讀取感測器輸出
  Serial.println(micVal);
  if (micVal > 200) {  //若超過門檻值
    Serial.println(micVal);
    toggle = !toggle;  //反轉 LED 狀態
    if (toggle) {digitalWrite(LED, HIGH);}  //狀態為 ON 就點亮 LED
    else {digitalWrite(LED, LOW);}  //狀態為 OFF 就點亮 LED
    }
  delay(1000);
  }


上面測試 1 只要拍手的音量超過門檻值, 板上 LED 就會切換狀態, 如果要連續拍兩次才動作的話該怎麼做呢? 下面測試 2 參考趙英傑寫的 "超圖解 Arduino 互動設計入門 2" 這本書的 6-4 節改寫 :


測試 2 : 連續拍手 2 次控制 LED 明滅

int MIC=A0;  //聲音感測模組 AO 輸出接至 A0 腳
int LED=13;  //Arduino 板上內建 LED
boolean toggle=false; //紀錄 LED 狀態,預設為熄滅
int micVal;  //紀錄偵測到的音量

unsigned long current=0;  //紀錄目前過門檻時戳
unsigned long last=0;  //紀錄上次過門檻時戳
unsigned long diff=0;  //紀錄前後兩次時間差
unsigned int count=0;  //紀錄已偵測到的次數

void setup() {
  pinMode(LED, OUTPUT);
  Serial.begin(9600);
  }

void loop() {
  micVal=analogRead(MIC);  //讀取感測器輸出
  if (micVal > 200) {  //若超過門檻值
    current=millis();  //紀錄目前時戳
    ++count;  //增量偵測次數
    Serial.print("count=");  //輸出偵測次數
    Serial.println(count);
    if (count >= 2) {  //若次數已達 2 次, 判斷間隔時間是否在 0.3~1.5 秒內
      diff=current-last;  //計算前後兩次時間差
      if (diff > 300 && diff < 1500) {  //判斷間隔時間是否在 0.3~1.5 秒內
        toggle = !toggle;  //反轉 LED 狀態
        count=0;  //計數器歸零
        }
      else {count=1;}  //間隔太短或太長則第二次不算, 計數器還原為 1
      }
    last=current;  //以目前時戳更新上次時戳, 以便下一次偵測時比較之用
    if (toggle) {digitalWrite(LED, HIGH);}  //狀態為 ON 就點亮 LED
    else {digitalWrite(LED, LOW);}  //狀態為 OFF 就點亮 LED
    }
  }

當然也可以改為連續拍兩次手觸發, 那就需要多一個變數來儲存時戳了, 不過, 或許用有限狀態機 (FSM) 會比較容易. 如下列測試 3 所示 :


測試 3 : 連續拍手 3 次控制 LED 明滅 (使用有限狀態機)

int MIC=A0;  //聲音感測模組 AO 輸出接至 A0 腳
int LED=13;  //Arduino 板上內建 LED
boolean toggle=false; //紀錄 LED 狀態,預設為熄滅
int micVal;  //紀錄偵測到的音量

unsigned long current=0;  //紀錄目前過門檻時戳
unsigned long last=0;  //紀錄上次過門檻時戳
unsigned long diff=0;  //紀錄前後兩次時間差

typedef enum {  //定義有限狀態機之狀態
  S_START,
  S_FIRST,
  S_SECOND,
  S_THIRD
  } State;

State state;  //建立 State 類型變數
boolean detect();  //函數 detect 原型宣告

void setup() {
  pinMode(LED, OUTPUT);
  Serial.begin(9600);
  state=S_START;  //起始狀態
  }

void loop() {
  switch(state) {  //判斷目前狀態
    case S_START:
      if (detect()) {  //偵測到第1個信號:進入狀態1
        state=S_FIRST;
        Serial.println("count=1");
        }
    break;
    case S_FIRST:
      if (detect()) {  //偵測到第2個信號:進入狀態2
        state=S_SECOND;
        Serial.println("count=2");
        }
    break;
    case S_SECOND:
      if (detect()) {  //偵測到第3個信號:進入狀態3
        state=S_THIRD;
        Serial.println("count=3");
        } 
    break;
    case S_THIRD:
      toggle = !toggle;  //反轉 LED 狀態
      state=S_START;  //回到起始狀態
    break;
    }
  if (toggle) {digitalWrite(LED, HIGH);}  //狀態為 ON 就點亮 LED
  else {digitalWrite(LED, LOW);}  //狀態為 OFF 就點亮 LED
  }

boolean detect() {  //if volume reaches threshold return true
  micVal=analogRead(MIC);  //讀取感測器輸出
  boolean ret=false;  //預設傳回值 false
  if (micVal > 200) {  //若超過門檻值
    current=millis();  //紀錄目前時戳
    diff=current-last;  //計算前後兩次時間差 
    if (diff > 300 && diff < 1500) {  //間隔時間在 0.3~1.5 秒內
      ret=true;  //有效信號,傳回值改為 true
      last=current;  //更新上次時戳為目前時戳
      }
    }
  return ret;  //傳回偵測結果
  }

上面程式中使用 typedef 定義了一個 enum 的有限狀態機, 開機起始狀態是 S_START, 在 loop() 迴圈中使用 switch case 來判斷目前狀態, 然後依據全域變數 toggle 之值變更 LED 狀態. 偵測音量訊號的部分被寫成了 detect() 函數, 當音量超過門檻值, 且與上一次偵測到訊號之間隔在 0.3~1.5 秒內表示訊號有效, 就會更新上一次時戳 last 並傳回 true.

在每一個 case 中會先呼叫 detect() 函數, 若傳回 true 表示偵測到有效的信號, 就進入下一個狀態, 狀態機依序從 S_START 走到 S_FIRST, S_SECOND, S_THIRD, S_START, ... , 周而復始. 當進入第三狀態 S_THIRD 時, LED 狀態變數會被反轉, 使 LED 明滅, 同時狀態機也會回到初始的 S_START 狀態, 狀態遷移圖如下所示 :


使用有限狀態機技巧可以讓我們將一個複雜的邏輯運算分解為多個獨立的作業, 使運作邏輯的實作簡化, 有利於軟體後續的維護與擴充. 對於像 Arduino 這樣無作業系統的嵌入式設備, 有限狀態機似乎就是一個超迷你作業系統. 關於有限狀態機的用法, 可參考葉難寫的 "輕鬆入門-Arduino 範例分析與實作設計" 第 3-5 節與 6-5 節或下列文章 :

Arduino練習:Simon Says請你跟我這樣做
有限状态机在单片机编程中的应用
Arduino编程之----如何让你Arduino以状态机方式运行

在上面的範例中使用的音量偵測門檻值都是武斷的, 在實際應用中都需要根據環境噪音的強弱加以調整, 程式需重新編譯上傳非常麻煩, 實用性不高. 在楊佩璐寫的 "跨入Maker物聯網時代 : 誰都可以用Arduino" 第 12.1.3 節介紹了自動調整門檻值的方法, 我參考這個方法將上面測試 1 改寫為如下測試 4 :


測試 4 :  當環境聲音超過門檻值時點亮 LED (可偵測背景音量)

int MIC=A0;  //聲音感測模組 AO 輸出接至 A0 腳
int LED=13;  //Arduino 板上內建 LED
boolean toggle=false; //紀錄 LED 狀態,預設為熄滅
int micVal;  //紀錄偵測到的音量
int background=0;  //紀錄環境音量最大值

void setup() {
  pinMode(LED, OUTPUT);
  Serial.begin(9600);
  while (millis() < 3000) {  //以3秒時間偵測環境音量
    micVal=analogRead(MIC);  //讀取麥克風音量 
    if (micVal > background) {  //若大於前一次音量就更新為目前音量
      background=micVal;
      }
    }
  }

void loop() {
  micVal=analogRead(MIC);  //讀取感測器輸出
  Serial.println(micVal);
  if (micVal-background > 10) {  //若超過背景音量 10
    Serial.println(micVal);
    toggle = !toggle;  //反轉 LED 狀態
    if (toggle) {digitalWrite(LED, HIGH);}  //狀態為 ON 就點亮 LED
    else {digitalWrite(LED, LOW);}  //狀態為 OFF 就點亮 LED
    }
  delay(1000);
  }

此程式中添加了一個全域變數 background 來記錄背景音量的最大值, 然後在 setup() 中以一個 3 秒的迴圈來記錄背景雜音的最高值, 記錄在 background 中. 在 loop() 中的音量偵測就改為比 background 高出 10 視為有效之信號而使 LED 改變狀態.

不過上面測試 4 程式有個缺點, 它只在一開機或 reset 時會偵測一次背景音量, 之後若背景噪音變化就失靈了. 解決辦法就是週期性去偵測環境音量, 這就要用到計時器了, 參考之前做 NTP 測試時所用的 Time 與 TimeAlarm 這兩個函式庫 :

利用 NTP 伺服器來同步 Arduino 系統時鐘 (三)

可從 GitHub 下載 TimeTimeAlarm 函式庫 :

https://github.com/PaulStoffregen/Time
https://github.com/PaulStoffregen/TimeAlarms

解壓縮 zip 檔後將 Time 與 TimeAlarm 兩個子目錄複製到 Arduino IDE 安裝目錄下的 libraries 下即可.


測試 5 :  當環境聲音超過門檻值時點亮 LED (可定時偵測背景音量)

#include <Time.h>
#include <TimeAlarms.h>

int MIC=A0;  //聲音感測模組 AO 輸出接至 A0 腳
int LED=13;  //Arduino 板上內建 LED
boolean toggle=false; //紀錄 LED 狀態,預設為熄滅
int micVal;  //紀錄偵測到的音量
int background=0;  //紀錄環境音量
void update_background();  //函數 update_background() 原型宣告

void setup() {
  pinMode(LED, OUTPUT);
  Serial.begin(9600);
  update_background();  //更新環境音量最高值
  Alarm.timerRepeat(60, update_background);  //設定計時器每 60 秒更新環境音量最高值
  }

void loop() {
  micVal=analogRead(MIC);  //讀取感測器輸出
  Serial.println(micVal);
  if (micVal-background > 10) {  //若超過背景音量 10
    Serial.println(micVal);
    toggle = !toggle;  //反轉 LED 狀態
    if (toggle) {digitalWrite(LED, HIGH);}  //狀態為 ON 就點亮 LED
    else {digitalWrite(LED, LOW);}  //狀態為 OFF 就點亮 LED
    }
  delay(1000);
  }

void update_background() {  //更新背景環境音量最大值
  while (millis() < 3000) {  //以3秒時間偵測環境音量
    micVal=analogRead(MIC);  //讀取麥克風音量 
    if (micVal > background) {  //若大於前一次音量就更新為目前音量
      background=micVal;
      }
    }
  }

上面程式在 setup() 中先呼叫 update_background() 做 background 的初始更新, 然後呼叫 Alarm.timerRepeat() 設定計時器, 每 60 秒去呼叫 update_background() 更新背景環境音量最大值.

參考 :

Arduino : 聲控開關
Arduino 聲控開關

9 則留言:

  1. 您好 我是剛玩Arduino的新手
    我最近都在使用您的Blogger學習Arduino
    此篇測試 5 : 當環境聲音超過門檻值時點亮 LED (可定時偵測背景音量)
    將兩個 Time 與 TimeAlarm 函式庫灌入後,
    出現以下的錯誤碼
    This report would have more information with
    "Show verbose output during compilation"
    enabled in File > Preferences.
    Arduino: 1.0.6 (Windows NT (unknown)), Board: "Arduino Uno"
    NEW_lesson15_3.ino: In function 'void setup()':
    NEW_lesson15_3:16: error: 'Alarms' was not declared in this scope


    請問這個問題要怎麼解??
    謝謝

    回覆刪除
  2. Time 與 TimeAlarm 函式庫解壓縮到 Arduino IDE 安裝目錄下的 libraries 後, IDE 要關掉重開喔!

    回覆刪除
  3. 最近看到這一篇文章,我跟著圖上的接線,使用arduino nano,接上A0後,序列阜監控顯示當我拍手時數值沒有顯改變,請問是哪裡的問題

    回覆刪除
  4. 若電路接線沒問題的話, 請用一字起子調整麥克風模組上的可變電阻試試看.

    回覆刪除
  5. 您好 打擾請教一下版主
    關於第五個 定時偵測背景音量的部分

    void update_background() { //更新背景環境音量最大值
    while (millis() < 3000) { //以3秒時間偵測環境音量
    micVal=analogRead(MIC); //讀取麥克風音量
    if (micVal > background) { //若大於前一次音量就更新為目前音量
    background=micVal;
    }
    }
    }


    millis() 我對這個的見解是 開機後的時間
    如果沒有清除 時間就會一直計算上去
    那他就只有第一次 會偵測背景音量(少於3000的條件達成)

    定時60秒再跳進來這段程式碼
    millis()的時間 是不是大於3000ms 所以才沒有成功的進入while 回圈
    讓背景音量沒有辦法順利更新??


    新手提問 還請版主給點指教><
    謝謝您的耐心觀看我的問題
    (發現背景音一直沒再更新)

    回覆刪除
  6. Dear 緯熊: 您的看法是對的, 此程式僅在 setup() 時呼叫 update_background(), 因此只在開機時偵測而已, 應該改成固定間隔時間去更新背景音較好, 找時間來改看看, 作法可參考 :
    https://www.baldengineer.com/arduino-how-do-you-reset-millis.html
    您也可以先試試, 因我目前在忙 Python 機器學習.

    回覆刪除
  7. 謝謝版大的資訊
    我先研究看看

    回覆刪除