---
# System prepended metadata

title: 2025 電資創客營 - 對講機製作共筆
tags: [電資創客營]

---

---
title: 2025 電資創客營 - 對講機製作共筆
tags: 電資創客營
---

:::info
# 2025 電資創客營 - 對講機製作共筆
**時間**：2025/08/12 9:00 ~ 2025/08/13 16:55
**地點**：挺生大樓 A3 - 200 教室
:::



## 目錄

**上午**
- ESP32 介紹
- 開發環境介紹
- ESP-IDF
- FreeRTOS
- WiFi 模式
- MAC 位址
- ESP - NOW
- 數位類比
- 時脈
- 主機從機
- 周邊介面介紹
- 元件介紹

**下午**

# 上午

## ESP32 介紹

![NodeMCU-32S-details-3](https://hackmd.io/_uploads/BJSKAZudll.jpg)

- 開發版
- 豐富社群資源
- 適合初學者使用

|功能|Arduino|ESP32|
|-|-|-|
|處理速度|16 MHz|高達 240 MHz|
記憶體|2 kB SRAM|約 520 KB SRAM
WiFi / BLE | :x:|內建
多核心|:x:|雙核心
模擬輸出|:x:|內建 DAC
感應器支援|基本|更強大


## 開發環境介紹

- Arduino IDE
    - 提供更簡化介面
    - 更簡化程式語法

## ESP-IDF

Espressif IoT Development Framework

- ESP32指的是晶片，電路板上的微控制器
- 開發版指的是搭載 ESP32 的電路板
- 工具箱
- 核心引擎
    - FreePTOS：內建即時作業系統，管理 ESP32 上的所有任務的執行、排程、資源分配、同步
- 無線通訊能力
    - WiFi
    - Bluetooth
    - ESP - NOW
- 周邊介面與功能(輸出介面)
    - GPIO
    - ADC/DAC
    - I2C/SPI/UART
    - I2S
    - PWM

## FreeRTOS

- 開源的即時**作業系統**
- 輕量級，專為嵌入式系統設計的，廣泛應用於微控制器
- 沒有像 Linux、Windows 一樣的先進特徵
- 簡潔與速度，因為太過輕量化，有時候被認為是執行緒函式庫而非作業系統

### POTS 即時作業系統

- 是一種能在嚴格時間內完成工作的作業系統，並根據優先順序快速切換，確保關進任務準時執行

### 微控制系統

- 小型電腦，被設計來執行特定任務，常見於嵌入式系統
- 通常整合了處理器、記憶體、與輸出入介面於一塊

### 嵌入式系統

- 一種類似被遷入在其他設備中的電腦系統
- 可以針對電器、被遷入機器的同客製化功能

## WiFi 模式

- STA(station 無線終端)
    - 定位：無限收發器
    - 運作：是一個專心對講機頻道(訊號接收)的機器 
- AP (Access Point)
    - 定位：網路中繼站
    - 運作：專心做網路通訊中繼站
## MAC 位址

- MAC 位址(Media Access Control Address)
    - 定義：裝置在硬體層面的**實體位址**或硬體位址
    - 格式：由**12 個十六進位數字**組成，用冒號分隔
例：00:1A:2B:3C:4D:5E

用途：
主要用於區域網路 (LAN) 內的數據傳輸，確保數據包在同一個網段內能**被正確的裝置接收**，在 Wi-Fi 通訊中，AP 和 STA 之間就是透過 MAC 位址來識別和建立連接的。

> 就像送信要知道對方的地址一樣，才能讓信送到正確的人那裡。

## ESP - NOW

- 專為 ESP32 的發明的通訊協議
- 獨立於 WiFi 網路，可直接透過 WiFi 協議進行通訊，不需要經過路由器
- 基於 MAC 位址即可建立傳輸或連接

## 數位類比
- 類比(Analog)
    類比訊號是==連續==的，可以有很多種值
    例如：自然現象(聲音、光線、溫度)
    類比訊號：麥克風(聲音)、溫度感測器的電壓輸出(溫度->電壓)
    > 圖案看起來像是**溜滑梯(slide)**
- 數位(Digital)
    數位訊號是==離散==的，只有特定幾種值(例如 0 和 1)
    電腦、微控制器只能處理數位訊號(還記得嗎?電腦只認識0101、也就是**二進位**)
    數位訊號：開跟關、二進位資料
    > 圖案看起來像是**階梯(stairs)**

## 時脈

時脈(clock)：電腦的節拍器
- 週期(Period)
    - 一次幾秒
    - 頻率(frequency)是一秒幾次
- 脈波(Clock Pulse)
    - 協調各個元件的動作時機

> 沒有穩定的時脈，數位系統就無法協調運作

## 主機從機
主機（Master）和從機（Slave）的概念
- 核心思想是在描述一種不對等的控制與被控制、主動與被動的關係
簡單來說：
- 主機（Master）：**ESP32**
    - 是系統中的主導者，負責發起命令、控制和管理整個系統的運作
    - 通常是主動的一方，決定什麼時候發起通訊、傳輸什麼資料，並協調從機行為
- 從機（Slave）：**麥克風、音訊模組、按鈕、喇叭**
    - 是系統中的執行者，它負責接收主機的指令並執行
    - 從機通常是被動的一方，等待主機發出命令，然後根據命令進行**資料回傳**或**執行特定操作**

<!-- 應該是 -->
## 周邊介面介紹
<!-- 他目錄是錯的 -->
### GPIO
- General-Purpose Input/Output, 通用輸入/輸出
- 可透過軟體程式來**控制**這些引腳的**行為**
    依現實考量可作為通用輸入（GPI）或通用輸出（GPO）或通用輸入與輸出（GPIO）
### SPI
> 串行：一次只傳輸一個位元
串行外設介面（Serial Peripheral Interface Bus，SPI），是一種用於**晶片通訊**的同步串行通訊介面規範，主要應用於**單晶片系統**中的訊號傳遞
由 Motorola 在 1980 年代提出，廣泛應用於感測器、記憶體、顯示器、音訊晶片、SD 卡等設備與微控制器之間

SPI匯流排規定了4個保留邏輯訊號介面：
- SCLK（Serial Clock）：串行時脈，由主機發出
- MOSI（Master Output, Slave Input）：主機輸出，從機輸入訊號（資料由主機發出）
- MISO（Master Input, Slave Output）：主機輸入，從機輸出訊號（資料由從機發出）
- SS（Slave Select）：片選訊號，由主機發出，一般是低電位有效

> 共用 SCLK 時脈訊號
![image](https://hackmd.io/_uploads/BJKnGN_Oxe.png)

### I2C
I²C 或 I2C（Inter-Integrated Circuit）是IC間**傳輸控制與資料**的一種序列介面標準
由飛利浦半導體在 1982 年設計，廣泛應用於感測器、記憶體、顯示器、實時時鐘（RTC）、音訊編碼器等設備與微控制器之間
特色：兩條訊號線進行通訊
SCL（Serial Clock Line）：由主機產生的時鐘訊號，控制資料傳輸節奏
SDA（Serial Data Line）：雙向資料線，負責傳送與接收資料

### I2S
I²S 或 I2S (Inter-IC Sound)是IC間傳輸數位**音訊**資料的一種介面標準
由飛利浦半導體在1986年提出，廣泛應用於音訊編碼器、數位音訊處理器、微控制器與 DAC/ADC 等設備之間
ESP32 的 ESP-IDF 提供了完整的 I2S 驅動程式，可以設定為：
傳送（TX）或接收（RX）模式
多個時鐘信號： 通常包含時鐘線 (BCLK)、字時鐘線 (WS 或 LRCLK) 和數據線 (SD 或 DATA)，確保數據的精確同步

| 訊號線               | 功能                                    |
| -------------------- | ------------------------------------- |
| SCK（Serial Clock）  | 又稱 BCLK，控制每個 bit 的傳輸時序         |
| WS（Word Select）    | 又稱 LRCLK，決定目前傳輸的是左聲道還是右聲道  |
| SD（Serial Data）    | 傳輸音訊資料的位元流                       |
| MCLK（Master Clock） | 系統中頻率最高的時鐘，保持時脈準確性（非必須) |

## 核心(Core)
什麼是核心？
- 核心（Core） 是 CPU 中能夠獨立執行程式指令的處理單元
- 就像是一間工廠裡的一個工人，有自己的一套工具（ALU、控制單元、暫存器等），能夠自成為一個完整的生產線

那 CPU core 和 CPU 晶片的關係是什麼？
- CPU core 和 CPU 晶片的關係就像是工人和工廠
- 每個工人（核心）都能獨立地接訂單、生產產品
- 工廠內會提供共用的資源（快取記憶體、記憶體控制器等） 

### ESP32 的雙核心架構
ESP32 是一個為物聯網（IoT）設計的微控制器，更強調協作和專門化
- 工人 0 號：通訊與後勤核心（Core 0, Protocol_CPU）
    角色：專注於通訊協議的專家
    例：處理系統底層的任務，像是Wi-Fi、藍牙、FreeRTOS等
- 工人 1 號：應用程式核心（Core 1 或稱為 Application_CPU）
    角色：專注於執行你的應用程式的工程師
    例：負責使用者的應用程式邏輯，處理你的業務邏輯、運算、數據處理等

## 元件介紹

- ESP32開發板
- 麵包板
- INMP441 麥克風
- MAX98357A 音訊模組

# 下午

## 標頭檔
我們這兩天會用到的標頭檔
- <WiFi.h>：處理 WiFi 相關功能，用來抓 MAC 位址
- <esp_now.h>：ESP-NOW 無線通訊協定函式庫
- <driver/i2s.h>：I2S 數位音訊介面函式庫

使用以下兩行得知你的MAC位址
```Ariduino=
WiFi.mode(WIFI_STA) //設定WiFi模式
Serial.println(WiFi.macAddress()) //印出MAC位址
```
## i2s初始化
初始化的東西放在setup裡面。
有兩個**結構**負責定義許多參數：
- i2s_config_t
    例如：工作模式(mode)、取樣率(sample_rate)、位元深度(bits_per_sample)、通道格式(channel_format)
- i2s_pin_config_t
    負責指定 I2S 的訊號線的 GPIO 腳位：
    .bck_io_num：連接 I2S 的 BCLK (位元時脈) 腳位
    .ws_io_num：連接 I2S 的 WS (字元選擇/左右聲道時脈) 腳位
    .data_in_num：連接 I2S 的輸入腳位，連接SD
    .data_out_num：連接 I2S 的輸出腳位
> config_t 是指 configuration type


> 只有mode、usr_apll、tx_desc_auto_clear、fixed_mclk我們會修改到。
### 初始化的最後三劍客
- i2s_driver_install：安裝 I2S 驅動的主要函式
- i2s_set_pin：設定 I2S 所使用的 GPIO 腳位
- i2s_zero_dma_buffer：清空緩衝區，避免殘值

例如：
i2s_driver_install ( I2S_PORT, &i2s_config, 0, NULL );
i2s_set_pin ( I2S_PORT, &pin_config);
i2s_zero_dma_buffer ( I2S_PORT );

### i2s_config_t設定
- .mode： 
    - 讀取：(i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    - 輸入：(i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
- .use_apll：今天都是false
- .tx_desc_auto_clear：
    - 輸入：true
    - 不輸入：false
- .fixed_mclk：今天都是0


### 腳位設定
利用 #define 設定：
使用格式：#define 名稱 值
:::spoiler 詳細設定
#define I2S_WS 25 // I2S Word Select (LRCLK) 腳位 
#define I2S_BCLK 26 // I2S Bit Clock 腳位 
#define I2S_SD 32 // I2S Serial Data (資料輸入) 腳位
#define BUTTON_PIN 33 // 定義按鈕連接的 ESP32 腳位 
#define I2S_PORT I2S_NUM_0 // 使用 I2S 端口 0 
#define SAMPLE_RATE 16000 // 音訊取樣率 (每秒取樣次數)，16kHz 是語音常用頻率 
#define BUFFER_LEN 64 // 每次讀取/傳送的音訊樣本數量 (緩衝區大小)
:::

## i2s read跟write

## esp_now指令

## 濾波處理


## ESP-NOW 接收 (更新版)
```
#include <WiFi.h>
#include <esp_now.h>


/* ==== 音量處理參數 ==== */
#define NOISE_THRESHOLD 30     // 音量低於此值視為靜音
#define FILTER_TAP_COUNT 2     // 移動平均濾波器抽頭數


void filter_data(int16_t *samples, int sampleCount){
 /* 移動平均濾波器（簡單實作）*/
 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);
 }
}

void sound_update(int16_t *samples, int sampleCount){
  float gain = 4.0;  // 設定增益係數，例如 2.0 代表音量加倍
  for (int i = 0; i < sampleCount; i++) {
    // 確保不會超過 16 位元整數的最大值
    long amplified_sample = (long)samples[i] * gain;
    if (amplified_sample > 32767) {
      samples[i] = 32767;
    } else if (amplified_sample < -32768) {
      samples[i] = -32768;
    } else {
      samples[i] = (int16_t)amplified_sample;
    }
  }
}

void wifi_mode_setup(){
 /* 設定為 STA 模式 */
 WiFi.mode(WIFI_STA);
 WiFi.disconnect(); // 確保不連接 AP

 delay(100);
 Serial.print("📡 接收端 MAC 地址：");
 Serial.println(WiFi.macAddress());
}

void espInit_setup(){
 // 初始化 ESP-NOW
 if (esp_now_init() != ESP_OK) {
   Serial.println("❌ ESP-NOW 初始化失敗");
   return;
 }
}

void onDataReceive(const esp_now_recv_info_t *info, const uint8_t *data, int len) {
 
 /* Step 1 */
 int16_t *samples = (int16_t *)data;
 int sampleCount = len / sizeof(int16_t);
 
 /* Step 2 */
 /* 計算平均音量 */
 
 uint32_t sum = 0;
 for (int i = 0; i < sampleCount; i++) {
   sum += abs(samples[i]);
 }
 int avgVolume = sum / sampleCount;
 

 /* Step 3 */
 
 if (avgVolume < NOISE_THRESHOLD) { // 如果平均音量低於閾值，則將所有樣本設為 0 (靜音)
 for (int i = 0; i < sampleCount; i++) {
      samples[i] = 0;
    }
    Serial.println("📥 接收音訊：靜音中 (低於閾值)");
  } else {
  
  /* Step 4 */
  //filter_data(samples, sampleCount);
  
  }

 /* Step 5 */
 /* 印出處理後的音量資訊 */
 
 Serial.print("📥 接收到 ");
 Serial.print(len);
 Serial.print(" bytes，平均音量：");
 Serial.println(avgVolume);
 
}

void setup(){

 /* Step 1 */
 Serial.begin(115200);
 delay(1000);
 
 /* Step 2 */
 wifi_mode_setup();
 
 /* Step 3 */
 espInit_setup();
 
 /* Step 4 */
 esp_now_register_recv_cb(onDataReceive);
 
 /* Step 5 */
 Serial.print("接收端初始化啟動...");

}

void loop(){
  
  }

 
```

// 麥克風測試Day3
```
#include <driver/i2s.h> // 引入 I2S 數位音訊介面函式庫

/* I2S 設定 (喇叭輸出腳位) */
#define I2S_DOUT 27   // I2S Data Out (資料輸出) 腳位，實際音訊資料線
#define I2S_BCLK 26   // I2S Bit Clock 腳位
#define I2S_WS  25   // I2S Left/Right Clock (LRCLK) 腳位

#define I2S_PORT I2S_NUM_0
#define SAMPLE_RATE 16000 // 音訊取樣率，需與傳送端一致
#define BUFFER_LEN 64 // 每次寫入的音訊樣本數量 (緩衝區大小)

// 簡單的正弦波數據，用於測試喇叭播放
int16_t test_tone[BUFFER_LEN]; 

void setupI2S(){
  
  const i2s_config_t i2s_config = {    // 設定為 I2S 主模式 (ESP32 提供時脈) 和 接收模式 (從麥克風接收)
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),                 //填入(I2S_MODE_MASTER | I2S_MODE_RX)或(I2S_MODE_MASTER | I2S_MODE_TX)
    .sample_rate = SAMPLE_RATE,                     // 音訊取樣率，需與麥克風輸出相符
    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,   // 每個音訊樣本使用 16 位元來表示
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,    // 設定為左聲道    
    .communication_format = I2S_COMM_FORMAT_I2S_MSB,// I2S通訊格式，通常是MSB最高有效位元優先
    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,       // 中斷分配旗標，設定中斷優先級
    .dma_buf_count = 8,                             // DMA 緩衝區的數量，DMA 讓音訊資料高效傳輸，不佔用 CPU
    .dma_buf_len = 64,                              // 每個 DMA 緩衝區的長度 (樣本數)
    .use_apll =false , 
    .tx_desc_auto_clear = true, 
    .fixed_mclk = 0 
  };
  
  
  
  const i2s_pin_config_t pin_config = { // 不使用的腳位填I2S_PIN_NO_CHANGE
    .bck_io_num = I2S_BCLK,                       // BCLK 腳位
    .ws_io_num = I2S_WS,                        // WS/LRCLK 腳位
    .data_out_num = I2S_DOUT,                     // 資料輸出腳位
    .data_in_num =  I2S_PIN_NO_CHANGE                      // 腳位，用於接收麥克風的資料
  };
  
  
  i2s_driver_install(I2S_PORT, &i2s_config , 0, NULL); // 安裝 I2S 驅動程式
  i2s_set_pin(I2S_PORT, & pin_config);                 // 設定 I2S 腳位
  i2s_zero_dma_buffer(I2S_PORT);                      // 清空 DMA 緩衝區，準備接收新資料
}

void sin_tone(){
/* 生成一個簡單的正弦波測試音 */
  for (int i = 0; i < BUFFER_LEN; i++) {
    test_tone[i] = (int16_t)(sin(i * 2 * PI / BUFFER_LEN * 5) * 10000); // 5個週期，振幅10000
  }
}

void loop_print(){
    uint32_t sum = 0;
   for(int i=0; i<BUFFER_LEN; i++) sum+=abs(test_tone[i]);
   int avgVolume = sum / BUFFER_LEN;
   Serial.print("音量：");
   Serial.println(avgVolume);
}

void setup() {
  
  /* Step 1 */
  Serial.begin(115200);
  
  /* Step 2 */
  setupI2S();
  
  /* Step 3 */
  sin_tone();

  Serial.println("🔊 喇叭播放測試啟動...");
}

void loop() {
  
  /* Step 1 */
  size_t bytesWritten;
  
  /* Step 2 */
  i2s_write(I2S_NUM_0, test_tone, sizeof(test_tone), &bytesWritten, portMAX_DELAY);
  
  /* Step 3 */
  loop_print();

  /* Step 4 */
  delay(10); // 稍微延遲，避免播放過快
}




```

