教學目的 - - 做出對講機 - 通廣知識,進而嘗試激發對這方面自主學習的動力 - 宣傳ttussc 需要教的背景知識的大約內容 - - **C++ Arduino** :::spoiler | 功能分類 | 用法(語法) | 中文說明 | 範例程式碼 | | ------ | --------------------------- | -------------- | ------------------------------ | | 宣告常數 | `#define 名稱 數值` | 設定不會變的數值 | `#define SAMPLE_RATE 16000` | | 宣告變數 | `int 變數名稱 = 值;` | 存放一個整數 | `int level = 5;` | | 如果判斷 | `if (條件) { ... }` | 判斷條件成立才執行裡面的內容 | `if (x > 10) { ... }` | | 重複執行 | `for (...) { ... }` | 重複做某件事 | `for (int i = 0; i < 10; i++)` | | 註解說明 | `//` | 給人看的備註,不會執行 | `// 這是麥克風設定` | | 函式寫法 | `void 名稱() { ... }` | 將重複的程式整理成小段 | `void playSound() { ... }` | | 顯示訊息 | `Serial.print()` | 印出文字或數字到電腦畫面 | `Serial.println("Hi!");` | | 顯示的頻段 |`Serial.begin()`|選擇要印出訊息的序耊副頻段|`Serial.begin(115200);`| | 延遲 | `delay(毫秒);` | 暫停一下(會卡住整個程式) | `delay(1000); // 等 1 秒` | | 控制腳位 | `digitalWrite()` | 控制 LED、喇叭等開關 | `digitalWrite(2, HIGH);` | | 設定腳位模式 | `pinMode(腳位, INPUT/OUTPUT)` | 設定腳位用途(輸入還是輸出) | `pinMode(2, OUTPUT);` | ::: - **esp32 by espressif systems** :::spoiler | 功能分類 | 用法(語法) | 中文說明 | 範例程式碼 | | ------ | -------------------------------- | ----------------- | ------------------------------------ | | 多核心任務(不一定要) | `xTaskCreatePinnedToCore(...)` | 同時跑兩件事,分配到 CPU 核心 | 建立背景任務來處理麥克風、喇叭等 | | 非阻塞延遲 | `vTaskDelay(ticks);` | 不會卡住整個程式的暫停 | `vTaskDelay(pdMS_TO_TICKS(10));` | | 毫秒轉換 | `pdMS_TO_TICKS(毫秒);` | 把毫秒轉成任務專用的時間格式 | `pdMS_TO_TICKS(100);` | | 無線溝通 | `esp_now_send(...)` | 使用 ESP-NOW 傳資料 | 把聲音資料用無線傳給另一台 ESP32 | | 音訊設定 | `i2s_driver_install(...)` | 啟用 I2S 麥克風或喇叭 | 設定音訊輸入輸出 | | 指定音訊腳位 | `i2s_set_pin(...)` | 告訴 ESP32 哪些腳位接麥克風 | `i2s_set_pin(I2S_NUM_0, &myPins);` | | 音訊讀寫 | `i2s_read(...) / i2s_write(...)` | 收麥克風或播音訊 | `i2s_read(...);` / `i2s_write(...);` | | 名稱 | 所屬 | 說明 | | ------------------------------- | --------- | ------------------------ | | I2S\_MODE\_MASTER | ESP32 | 設為 I2S 主裝(產生時脈) | | I2S\_MODE\_TX / I2S\_MODE\_RX | ESP32 | 開啟發送/接收音訊功能 | | I2S\_BITS\_PER\_SAMPLE\_16BIT | ESP32 | 每個樣本使用 16 位元 | | I2S\_CHANNEL\_FMT\_ONLY\_LEFT | ESP32 | 只使用左聲道資料 | | I2S\_COMM\_FORMAT\_I2S\_MSB | ESP32 | 使用標準 I2S 通訊協定(MSB first) | | ESP\_INTR\_FLAG\_LEVEL1 | ESP32 | 中斷優先順序(中等等級) | | I2S\_PIN\_NO\_CHANGE | ESP32 | 保持腳位設定不變 | | use\_apll | ESP32 | 是否使用 APLL 高精度時脈 | | tx\_desc\_auto\_clear | ESP32 | 自動清除傳送描述子 | | dma\_buf\_count / dma\_buf\_len | ESP32 | DMA buffer 數量與長度設定 | ::: - **其他** - 數位類比 - Mac地址 - ESP-NOW:I2S協議 - 硬體input/output的方式(?) - mclk和bclk的關係 - 時脈 esp32程式 - :::spoiler I am your father (1hr30min) ``` #include <WiFi.h> #include <esp_now.h> // 對方的 MAC 地址 (您需要根據實際情況修改) uint8_t peerAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // 廣播地址作為範例 char user_name[20] = "I am your father"; // 資料結構 typedef struct struct_message { char msg[32]; } struct_message; struct_message myData; struct_message receivedData; // ESP-NOW 發送成功回呼函數 void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) { Serial.print("Last Packet Send Status: "); Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail"); } // ESP-NOW 接收資料回呼函數 void OnDataRecv(const esp_now_recv_info_t *info, const uint8_t *incomingDataBytes, int len) { memcpy(&receivedData, incomingDataBytes, sizeof(receivedData)); Serial.print("Received from: "); Serial.print(info->src_addr[0], HEX); Serial.print(":"); Serial.print(info->src_addr[1], HEX); Serial.print(":"); Serial.print(info->src_addr[2], HEX); Serial.print(":"); Serial.print(info->src_addr[3], HEX); Serial.print(":"); Serial.print(info->src_addr[4], HEX); Serial.print(":"); Serial.print(info->src_addr[5], HEX); Serial.print(" Message: "); Serial.println(receivedData.msg); } void setup() { Serial.begin(115200); WiFi.mode(WIFI_STA); Serial.println("ESP-NOW Peer-to-Peer (Send & Receive)"); Serial.print("My MAC address: "); Serial.println(WiFi.macAddress()); if (esp_now_init() != ESP_OK) { Serial.println("[-]Error initializing ESP-NOW"); return; } esp_now_register_send_cb(OnDataSent); esp_now_register_recv_cb(OnDataRecv); // 添加對方節點資訊 esp_now_peer_info_t peerInfo = {}; memcpy(peerInfo.peer_addr, peerAddress, 6); peerInfo.channel = 0; peerInfo.encrypt = false; if (esp_now_add_peer(&peerInfo) != ESP_OK){ Serial.println("[-]Failed to add peer"); return; } } void loop() { if (Serial.available() > 0) { String inputString = Serial.readStringUntil('\n'); inputString.trim(); if (inputString.length() > 0 && inputString.length() < 32) { std::string combinedString = std::string(user_name) + ": " + inputString.c_str(); strcpy(myData.msg, combinedString.c_str()); esp_now_send(peerAddress, (uint8_t *) &myData, sizeof(myData)); Serial.printf("%s Sent: ", user_name); Serial.println(myData.msg); } else if (inputString.length() >= 32) { Serial.println("[-]Input too long (max 31 characters)."); } delay(100); } // 接收端的回呼函數 OnDataRecv 會在收到訊息時自動執行, // loop 函數本身不需要額外處理接收邏輯。 } ``` ::: :::spoiler 單向INMP441 ``` #include <WiFi.h> #include <esp_now.h> #include <driver/i2s.h> // INMP441 I2S 腳位 #define I2S_WS 25 #define I2S_BCLK 26 #define I2S_SD 32 #define I2S_PORT I2S_NUM_0 #define SAMPLE_RATE 16000 #define BUFFER_LEN 64 // 按鈕腳位(例如 GPIO33) #define BUTTON_PIN 33 uint8_t receiverMac[] = {0xA4, 0xE5, 0x7C, 0xD3, 0xB4, 0x5C}; void setup() { Serial.begin(115200); delay(1000); // 設定按鈕腳位 pinMode(BUTTON_PIN, INPUT_PULLUP); // I2S 初始化 (這部分通常不會是問題,但為了完整性保留) const i2s_config_t i2s_config = { .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX), .sample_rate = SAMPLE_RATE, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, .communication_format = I2S_COMM_FORMAT_I2S_MSB, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 8, .dma_buf_len = 64, .use_apll = false, .tx_desc_auto_clear = false, .fixed_mclk = 0 }; const i2s_pin_config_t pin_config = { .bck_io_num = I2S_BCLK, .ws_io_num = I2S_WS, .data_out_num = I2S_PIN_NO_CHANGE, .data_in_num = I2S_SD }; i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL); i2s_set_pin(I2S_PORT, &pin_config); i2s_zero_dma_buffer(I2S_PORT); // ESP-NOW 初始化 WiFi.mode(WIFI_STA); // <<=== 在這裡加入延遲或等待 WiFi 啟動 ===>> // 方法一:簡單延遲 (通常有效) delay(100); if (esp_now_init() != ESP_OK) { Serial.println("❌ ESP-NOW 初始化失敗"); while (true); } esp_now_peer_info_t peerInfo = {}; memcpy(peerInfo.peer_addr, receiverMac, 6); peerInfo.channel = 0; peerInfo.encrypt = false; if (esp_now_add_peer(&peerInfo) != ESP_OK) { Serial.println("❌ 加入 ESP-NOW 對等端失敗"); while (true); } // 在這裡獲取並列印 MAC 地址 Serial.print("My MAC address: "); // 加上標籤更清晰 Serial.println(WiFi.macAddress()); Serial.println("🎙 麥克風傳送端啟動"); } void loop() { int16_t buffer[BUFFER_LEN]; size_t bytesRead; // 收音 i2s_read(I2S_PORT, &buffer, sizeof(buffer), &bytesRead, portMAX_DELAY); // 音量計算 uint32_t sum = 0; for (int i = 0; i < BUFFER_LEN; i++) { sum += abs(buffer[i]); } int avgVolume = sum / BUFFER_LEN; Serial.print("🎚 音量:"); Serial.print(avgVolume); // 若按鈕按下(LOW)就傳送 if (digitalRead(BUTTON_PIN) == LOW) { esp_err_t result = esp_now_send(receiverMac, (uint8_t*)buffer, sizeof(buffer)); if (result == ESP_OK) { Serial.println(" → 傳送成功"); } else { Serial.println(" → 傳送失敗"); } } else { Serial.println("(未傳送)"); } delay(50); } ``` ::: :::spoiler 單向MAX98357A ``` #include <WiFi.h> #include <esp_now.h> #include <driver/i2s.h> // I2S 設定 #define I2S_DOUT 22 // DIN #define I2S_BCLK 26 // BCLK #define I2S_LRC 25 // LRC #define SAMPLE_RATE 16000 // ==== 新增濾波器設定 ==== #define FILTER_TAP_COUNT 2 // 移動平均濾波器的「抽頭」數量 (N值),越大濾波越平滑,但延遲和模糊越大 // 建議值:2-5,過大會導致聲音模糊不清 #define NOISE_THRESHOLD 30 // 噪音閾值,當音量低於此值時,強制靜音。需要根據實際環境調整。 // 範例值,請根據實際測試調整 (範圍約 0-32767) // 緩衝區用於移動平均濾波器 // 因為 incomingData 會不斷進來,所以不需要一個靜態的 filter_buffer, // 而是對 incomingData 進行操作。 // 但我們可以保留上一次的樣本,用於更複雜的濾波,這裡先用簡單的。 void setupI2S() { const i2s_config_t i2s_config = { .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX), .sample_rate = SAMPLE_RATE, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, .communication_format = I2S_COMM_FORMAT_I2S_MSB, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 8, .dma_buf_len = 64, .use_apll = false, .tx_desc_auto_clear = true, .fixed_mclk = 0 }; const i2s_pin_config_t pin_config = { .bck_io_num = I2S_BCLK, .ws_io_num = I2S_LRC, .data_out_num = I2S_DOUT, .data_in_num = I2S_PIN_NO_CHANGE }; i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL); i2s_set_pin(I2S_NUM_0, &pin_config); i2s_zero_dma_buffer(I2S_NUM_0); } // 接收音訊資料後播放 void onDataReceive(const esp_now_recv_info_t *recv_info, const uint8_t *incomingData, int len) { size_t bytesWritten; int16_t *samples = (int16_t *)incomingData; // 將 incomingData 轉換為 int16_t 指針 int sampleCount = len / sizeof(int16_t); // 計算音量 (平均絕對值) - 用於閾值判斷和除錯 uint32_t current_sum_abs = 0; for (int i = 0; i < sampleCount; i++) { current_sum_abs += abs(samples[i]); } int avgVolume = current_sum_abs / sampleCount; // ==== 雜訊過濾處理 ==== // 1. 閾值降噪 (Noise Gating) if (avgVolume < NOISE_THRESHOLD) { // 如果平均音量低於閾值,則將所有樣本設為 0 (靜音) for (int i = 0; i < sampleCount; i++) { samples[i] = 0; } Serial.println("📥 接收音訊:靜音中 (低於閾值)"); } else { // 2. 移動平均濾波器 // 這裡實現一個簡單的窗口移動平均濾波 // 為了實現移動平均,需要一些過去的樣本。 // 最簡單的方式是在每個處理塊內應用,效果會受限於塊邊界。 // 更複雜的需要一個歷史緩衝區來維持濾波上下文。 // 對於實時系統,我們可以簡化為對當前數據塊內的局部平滑。 // 這裡我們直接對當前的 samples 數據進行移動平均處理 // 注意:這會改變原始的 incomingData 內容 for (int i = 0; i < sampleCount; i++) { long sum_filter = 0; int count_filter = 0; for (int j = 0; j < FILTER_TAP_COUNT; j++) { if (i - j >= 0) { // 確保不越界 sum_filter += samples[i - j]; count_filter++; } } samples[i] = (int16_t)(sum_filter / count_filter); } Serial.print("📥 接收音訊:"); Serial.print(len); Serial.print(" bytes,音量:"); Serial.println(avgVolume); } // 將處理後的音訊資料寫入 I2S 輸出 i2s_write(I2S_NUM_0, samples, len, &bytesWritten, portMAX_DELAY); } void setup() { Serial.begin(115200); WiFi.mode(WIFI_STA); WiFi.disconnect(); delay(100); Serial.print("🔊 接收端 MAC 地址:"); Serial.println(WiFi.macAddress()); if (esp_now_init() != ESP_OK) { Serial.println("❌ ESP-NOW 初始化失敗"); return; } esp_now_register_recv_cb(onDataReceive); setupI2S(); Serial.println("🎧 接收端已啟動,等待音訊傳入..."); } void loop() { // 無需做其他事,音訊由 callback 處理 } ``` ::: :::spoiler 最終的對講機程式 ``` #include <WiFi.h> #include <esp_now.h> #include <driver/i2s.h> // ==== 設定 ==== #define SAMPLE_RATE 16000 #define BUFFER_LEN 64 #define NOISE_THRESHOLD 30 #define FILTER_TAP_COUNT 2 // ==== I2S 腳位(單一 I2S)==== #define I2S_PORT I2S_NUM_0 #define I2S_BCLK 26 #define I2S_WS 25 #define I2S_DATA_IN 32 // 麥克風 SD #define I2S_DATA_OUT 27 // 喇叭 DIN // ==== 控制 ==== #define BUTTON_PIN 33 // ==== 對方 MAC 地址(請自行修改)==== uint8_t peerMac[] = { 0xA8, 0x48, 0xFA, 0x0B, 0x89, 0x3C }; //uint8_t peerMac[] = { 0xA4, 0xE5, 0x7C, 0xD3, 0xB4, 0x5C }; // ==== 佇列 ==== QueueHandle_t sendQueue; QueueHandle_t playbackQueue; // ==== 初始化 I2S ==== void setupI2S() { i2s_config_t config = { .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_RX), .sample_rate = SAMPLE_RATE, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, .communication_format = I2S_COMM_FORMAT_I2S_MSB, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 8, .dma_buf_len = BUFFER_LEN, .use_apll = true, .tx_desc_auto_clear = true, .fixed_mclk = I2S_PIN_NO_CHANGE }; i2s_pin_config_t pins = { .bck_io_num = I2S_BCLK, .ws_io_num = I2S_WS, .data_out_num = I2S_DATA_OUT, .data_in_num = I2S_DATA_IN }; i2s_driver_install(I2S_PORT, &config, 0, NULL); i2s_set_pin(I2S_PORT, &pins); i2s_zero_dma_buffer(I2S_PORT); } // ==== 音訊接收處理 ==== void onDataReceive(const esp_now_recv_info_t *info, const uint8_t *data, int len) { int16_t *samples = (int16_t *)data; int count = len / sizeof(int16_t); // 降噪與平均值 uint32_t sum = 0; for (int i = 0; i < count; i++) sum += abs(samples[i]); int avg = sum / count; Serial.print("📥 收到音訊平均值:"); Serial.print(avg); if (avg < NOISE_THRESHOLD) { memset(samples, 0, len); Serial.println("(靜音處理)"); } else { Serial.println(); for (int i = 0; i < count; i++) { long s = 0; int tap = 0; for (int j = 0; j < FILTER_TAP_COUNT; j++) { if (i - j >= 0) { s += samples[i - j]; tap++; } } samples[i] = s / tap; } } int16_t *copy = (int16_t *)malloc(len); if (copy) { memcpy(copy, data, len); if (xQueueSend(playbackQueue, &copy, 0) != pdPASS) free(copy); } } // ==== 播放任務 ==== void playbackTask(void *param) { int16_t *buffer; size_t written; while (true) { if (xQueueReceive(playbackQueue, &buffer, portMAX_DELAY) == pdPASS) { i2s_write(I2S_PORT, buffer, BUFFER_LEN * sizeof(int16_t), &written, portMAX_DELAY); free(buffer); } } } // ==== 麥克風讀取任務 ==== void micReadTask(void *param) { int16_t buffer[BUFFER_LEN]; size_t read; while (true) { i2s_read(I2S_PORT, buffer, sizeof(buffer), &read, portMAX_DELAY); // 計算平均音量 uint32_t sum = 0; int count = read / sizeof(int16_t); for (int i = 0; i < count; i++) sum += abs(buffer[i]); int avg = sum / count; if (digitalRead(BUTTON_PIN) == LOW) { Serial.print("🎤 錄音中,平均音量:"); Serial.println(avg); xQueueSend(sendQueue, buffer, 0); } vTaskDelay(pdMS_TO_TICKS(4)); } } // ==== 傳送任務 ==== void micSendTask(void *param) { int16_t buffer[BUFFER_LEN]; int counter = 0; while (true) { if (xQueueReceive(sendQueue, buffer, pdMS_TO_TICKS(20)) == pdPASS) { esp_err_t result = esp_now_send(peerMac, (uint8_t *)buffer, sizeof(buffer)); if (result == ESP_OK) { Serial.print("📤 已傳送封包 #"); Serial.println(++counter); } else { Serial.print("❌ 傳送失敗,錯誤碼:"); Serial.println(result); } } } } // ==== 主程式 ==== void setup() { Serial.begin(115200); pinMode(BUTTON_PIN, INPUT_PULLUP); setupI2S(); sendQueue = xQueueCreate(10, sizeof(int16_t) * BUFFER_LEN); playbackQueue = xQueueCreate(10, sizeof(int16_t *)); if (!sendQueue || !playbackQueue) { Serial.println("❌ 佇列建立失敗!"); while (true); } WiFi.mode(WIFI_STA); delay(100); Serial.print("📡 本機 MAC 地址:"); Serial.println(WiFi.macAddress()); if (esp_now_init() != ESP_OK) { Serial.println("❌ ESP-NOW 初始化失敗!"); while (true); } esp_now_peer_info_t peerInfo = {}; memcpy(peerInfo.peer_addr, peerMac, 6); peerInfo.channel = 0; peerInfo.encrypt = false; if (!esp_now_is_peer_exist(peerMac)) { if (esp_now_add_peer(&peerInfo) != ESP_OK) { Serial.println("❌ 加入對等端失敗!"); while (true); } } esp_now_register_recv_cb(onDataReceive); xTaskCreatePinnedToCore(micReadTask, "MicReadTask", 4096, NULL, 1, NULL, 0); xTaskCreatePinnedToCore(micSendTask, "MicSendTask", 4096, NULL, 1, NULL, 0); xTaskCreatePinnedToCore(playbackTask, "PlaybackTask", 4096, NULL, 1, NULL, 1); Serial.println("✅ 雙向音訊對講機已啟動!"); } void loop() { vTaskDelay(pdMS_TO_TICKS(1000)); } ``` ::: | 公公線 | 麵包板 | 按鈕 | micro-usb | NodeMCU32S | INMP441 | MAX98357A | | -------- | -------- | -------- | -------- | -------- | -------- | -------- | | 28 | 4 | 2 | 2 | 2 | 2 | 2 |