--- 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); // 稍微延遲,避免播放過快 } ```