---
# System prepended metadata

title: August-2025_電資創客營課綱

---

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

