教學目的
-
- 做出對講機
- 通廣知識,進而嘗試激發對這方面自主學習的動力
- 宣傳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, ©, 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 |