本課程我們講解Micropython for ESP32 的i2s及其應用,比如INMP441音頻錄制、MAX98357A音頻播放等,還有SD卡的讀寫。
一、硬件準備
1、支持micropython的ESP32S3開發板
2、INMP441數字全向麥克風模塊
3、MAX98357A音頻播放模塊
4、SD卡模塊
5、面包板及連接線若干
連接方式:
? ? ? ? ? ? ? ? ?
inmp441 | MAX98357A | ESP32S3 |
SD | IO13 | |
WS | IO12 | |
SCK | IO11 | |
L/R接地 | ||
SD接VCC | ||
GAIN接地 | ||
DIN | IO37 | |
BCLK | IO38 | |
LRC | IO39 |
SD卡模塊 | ESP32S3 |
SCK | IO4 |
MOSI | IO5 |
MISO | IO16 |
CS | IO17 |
? ? ? ?
二、i2s介紹?
一)、I2S協議基礎
I2S(Inter-IC Sound)是一種同步串行通信協議,專為數字音頻設備設計,支持單向/雙向音頻數據傳輸。其物理層包含三條信號線:
- ?SCK?(串行時鐘):同步數據傳輸速率
- ?WS?(字選擇):區分左右聲道或定義采樣率
- ?SD?(串行數據):傳輸實際音頻數據流?
二)、MicroPython I2S類特性
? ? ? ? A.? 僅支持主設備操作模式,可控制SCK和WS信號的生成,適用于連接麥克風、
?????????????DAC等從設備?
? ? ? ? B.?支持ESP32、STM32、RP2等主流微控制器平臺,通過統一接口簡化跨硬件開發?
三)、核心功能實現
-
?音頻輸入/輸出?
- ?錄音?:從麥克風模塊獲取PCM音頻數據
- ?播放?:向DAC或音頻解碼器發送音頻流?27。
-
?參數靈活配置?
初始化時可設置關鍵參數:i2s = I2S(id, # 硬件實例編號(如I2S.NUM0)sck=Pin(11), ws=Pin(12), sd=Pin(13), # 引腳映射mode=I2S.RX, # 模式(RX/TX)bits=16, # 采樣位深format=I2S.MONO, # 聲道格式 MONO為單聲道,STEREO為立體聲rate=16000, # 采樣率ibuf=8092) # 輸入緩沖區大小?:ml-citation{ref="4,7" data="citationList"}
-
?中斷與DMA支持?
支持異步數據讀寫,通過DMA減少CPU占用率,提升實時性?
?
?四)、典型應用場景
-
?音頻播放器?
播放WAV/MP3文件(需解碼庫支持)?。 -
?語音采集系統?
連接INMP441等數字麥克風實現環境音錄制?。 -
?實時語音處理?
結合神經網絡進行關鍵詞識別或聲紋分析?
三、MicroPython SD卡介紹?
一)、SD卡初始化與掛載
硬件接口配置?
使用SPI模式連接SD卡(需4線:CLK/MOSI/MISO/CS),典型ESP32配置示例:
from sdcard import SDCard
import os, time, gcspi = SPI(2,baudrate=80000000,polarity=0,phase=0,sck=Pin(4),mosi=Pin(5),miso=Pin(16))
sd = SDCard(spi,Pin(17,Pin.OUT))
二)、文件操作API
????????基礎文件讀寫?
????????使用標準文件操作接口:
?
def test_sd():os.mount(sd,'/sd')# 重新查詢系統文件目錄print('掛載SD后的系統目錄:{}'.format(os.listdir()))with open("/sd/test.txt", "w") as f:f.write(str("Hello MicroPython!"))# 從sd卡目錄下讀取hello.txt文件內容with open("/sd/test.txt", "r") as f:# 打印讀取的內容data = f.read()print (data)
四、inmp4411錄制音頻
通過前面的講解,這一小節的內容需要掌握的知識點我們都已經掌握,直接上代碼:
audiofilename = '/sd/rec.pcm'
def record_audio(filename=audiofilename, duration=5, sample_rate=16000):
# # 硬件診斷print("初始化I2S...")try:i2s = I2S(0,sck=Pin(11), ws=Pin(12), sd=Pin(13),mode=I2S.RX,bits=16,format=I2S.MONO,rate=sample_rate,ibuf=4096)except Exception as e:print("I2S初始化失敗:", e)return# 計算數據量bytes_per_second = sample_rate * 2 # 16bit=2字節total_bytes = bytes_per_second * duration
# header = createWavHeader(sample_rate, 16, 1, total_bytes)# 錄音循環try:with open(audiofilename, 'wb') as f:
# f.write(header)start_time = time.ticks_ms()bytes_written = 0buffer = bytearray(2048) # 小緩沖區減少內存壓力while bytes_written < total_bytes:read = i2s.readinto(buffer)if read == 0:print("警告:未讀取到數據")continuef.write(buffer[:read])bytes_written += readgc.collect()# 實時進度elapsed = time.ticks_diff(time.ticks_ms(), start_time) / 1000print(f"進度: {bytes_written/total_bytes*100:.1f}%, 時間: {elapsed:.1f}s")except OSError as e:print("文件寫入錯誤:", e)finally:i2s.deinit()
# print("錄音結束,文件大小:", os.stat(audiofilename)[6], "字節")print("錄音結束,文件大小:", bytes_written, "字節")
但這里需要說明一下的是,我們剛開始開發的時候,錄制的音頻文件中的數據全是0,也就是說沒有聲音,噪音都沒有,檢查連接線、換IO口等等,各種折騰,但問題依然存在,后來因為出了其它的錯誤,就暫停了,具體可以參考:MicroPython 開發ESP32應用教程 之 WIFI、BLE共用常見問題處理及中斷處理函數注意事項
上文中提到的問題處理完后,我們繼續折騰音頻錄制及播放的功能,奇怪的事情發生了,連接好各功能模塊后,測試,居然好了,懷疑是上文中提到的電源的問題,但把外接電源移除,測試沒有問題。
也就是說,到現在,我們還是不知道之前為什么有問題?現在為什么好了?只能懷疑電源不穩?
五、MAX98357A音頻播放
這個也沒什么好講,直接上代碼吧
audiofilename = '/sd/rec.pcm'audio_out = I2S(1, sck=Pin(38), ws=Pin(39), sd=Pin(37), mode=I2S.TX, bits=16, format=I2S.MONO, rate=16000, ibuf=20000)
def play_audio(filename='/sd/rec.wav', duration=5, sample_rate=16000): # audio_out.volume(80)with open(audiofilename,'rb') as f:# 跳過文件的開頭的44個字節,直到數據段的第1個字節
# pos = f.seek(44) # 用于減少while循環中堆分配的內存視圖wav_samples = bytearray(1024)wav_samples_mv = memoryview(wav_samples)print("開始播放音頻...")#并將其寫入I2S DACwhile True:try:num_read = f.readinto(wav_samples_mv)# WAV文件結束if num_read == 0: break# 直到所有樣本都寫入I2S外圍設備num_written = 0while num_written < num_read:num_written += audio_out.write(wav_samples_mv[num_written:num_read])except Exception as ret:print("產生異常...", ret)
六、完整代碼
?
該代碼簡單修改可保存為WAV格式文件,可以用我們常見的音頻播放軟件播放。
from machine import I2S, Pin,SPI
from sdcard import SDCard
import os, time, gcspi = SPI(2,baudrate=20000000,polarity=0,phase=0,sck=Pin(4),mosi=Pin(5),miso=Pin(16))
sd = SDCard(spi,Pin(17,Pin.OUT))audiofilename = '/sd/rec.pcm'
def createWavHeader(sampleRate, bitsPerSample, num_channels, datasize): riff_size = datasize + 36 - 8 # 修正RIFF塊大小header = bytes("RIFF", 'ascii')header += riff_size.to_bytes(4, 'little')header += bytes("WAVE", 'ascii')header += bytes("fmt ", 'ascii')header += (16).to_bytes(4, 'little') # fmt塊大小header += (1).to_bytes(2, 'little') # PCM格式header += num_channels.to_bytes(2, 'little') # 聲道數header += sampleRate.to_bytes(4, 'little') # 采樣率header += (sampleRate * num_channels * bitsPerSample // 8).to_bytes(4, 'little') # 字節率header += (num_channels * bitsPerSample // 8).to_bytes(2, 'little') # 塊對齊header += bitsPerSample.to_bytes(2, 'little') # 位深header += bytes("data", 'ascii')header += datasize.to_bytes(4, 'little') # 數據塊大小return headerdef record_audio(filename=audiofilename, duration=5, sample_rate=16000):
# # 硬件診斷print("初始化I2S...")try:i2s = I2S(0,sck=Pin(11), ws=Pin(12), sd=Pin(13),mode=I2S.RX,bits=16,format=I2S.MONO,rate=sample_rate,ibuf=4096)except Exception as e:print("I2S初始化失敗:", e)return# 計算數據量bytes_per_second = sample_rate * 2 # 16bit=2字節total_bytes = bytes_per_second * duration
# header = createWavHeader(sample_rate, 16, 1, total_bytes)# 錄音循環try:with open(audiofilename, 'wb') as f:
# f.write(header)start_time = time.ticks_ms()bytes_written = 0buffer = bytearray(1024) # 小緩沖區減少內存壓力while bytes_written < total_bytes:read = i2s.readinto(buffer)if read == 0:print("警告:未讀取到數據")continuef.write(buffer[:read])bytes_written += readgc.collect()# 實時進度elapsed = time.ticks_diff(time.ticks_ms(), start_time) / 1000print(f"進度: {bytes_written/total_bytes*100:.1f}%, 時間: {elapsed:.1f}s")except OSError as e:print("文件寫入錯誤:", e)finally:i2s.deinit()
# print("錄音結束,文件大小:", os.stat(audiofilename)[6], "字節")print("錄音結束,文件大小:", bytes_written, "字節")audio_out = I2S(1, sck=Pin(38), ws=Pin(39), sd=Pin(37), mode=I2S.TX, bits=16, format=I2S.MONO, rate=16000, ibuf=20000)
def play_audio(filename='/sd/rec.wav', duration=5, sample_rate=16000): # audio_out.volume(80)with open(audiofilename,'rb') as f:# 跳過文件的開頭的44個字節,直到數據段的第1個字節
# pos = f.seek(44) # 用于減少while循環中堆分配的內存視圖wav_samples = bytearray(1024)wav_samples_mv = memoryview(wav_samples)print("開始播放音頻...")#并將其寫入I2S DACwhile True:try:num_read = f.readinto(wav_samples_mv)# WAV文件結束if num_read == 0: break# 直到所有樣本都寫入I2S外圍設備num_written = 0while num_written < num_read:num_written += audio_out.write(wav_samples_mv[num_written:num_read])except Exception as ret:print("產生異常...", ret)if __name__ == "__main__":try:os.mount(sd,'/sd') record_audio(duration=5)play_audio()except Exception as e:print("異常:",e)
# 測試'''import time
from machine import I2S, Pin
import math# I2S配置
i2s = I2S(0,sck=Pin(22), ws=Pin(23), sd=Pin(21),mode=I2S.RX,bits=16,rate=16000,channel_format=I2S.ONLY_LEFT)# 參數配置
SILENCE_THRESHOLD = 0.02 # 需根據環境噪聲校準
CHECK_INTERVAL = 0.1 # 檢測間隔(秒)
SILENCE_DURATION = 1.0 # 目標靜默時長buffer = bytearray(1024) # 512個16位樣本
last_sound_time = time.time()while True:i2s.readinto(buffer) # 讀取I2S數據?:ml-citation{ref="6" data="citationList"}# 計算當前塊RMS值sum_sq = 0for i in range(0, len(buffer), 2):sample = int.from_bytes(buffer[i:i+2], 'little', True)sum_sq += (sample / 32768) ** 2 # 16位有符號轉浮點?:ml-citation{ref="6" data="citationList"}rms = math.sqrt(sum_sq / 512)# 更新最后有聲時間戳if rms > SILENCE_THRESHOLD:last_sound_time = time.time()# 判斷靜默持續時間if (time.time() - last_sound_time) >= SILENCE_DURATION:print("檢測到持續靜默")# 觸發后續處理
'''