本课程我们讲解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。
-
参数灵活配置
初始化时可设置关键参数:
pythoni2s = 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配置示例:
python
from sdcard import SDCard
import os, time, gc
spi = 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
基础文件读写
使用标准文件操作接口:
python
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录制音频
通过前面的讲解,这一小节的内容需要掌握的知识点我们都已经掌握,直接上代码:
python
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 = 0
buffer = bytearray(2048) # 小缓冲区减少内存压力
while bytes_written < total_bytes:
read = i2s.readinto(buffer)
if read == 0:
print("警告:未读取到数据")
continue
f.write(buffer[:read])
bytes_written += read
gc.collect()
# 实时进度
elapsed = time.ticks_diff(time.ticks_ms(), start_time) / 1000
print(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音频播放
这个也没什么好讲,直接上代码吧
python
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 DAC
while True:
try:
num_read = f.readinto(wav_samples_mv)
# WAV文件结束
if num_read == 0:
break
# 直到所有样本都写入I2S外围设备
num_written = 0
while num_written < num_read:
num_written += audio_out.write(wav_samples_mv[num_written:num_read])
except Exception as ret:
print("产生异常...", ret)
六、完整代码
该代码简单修改可保存为WAV格式文件,可以用我们常见的音频播放软件播放。
python
from machine import I2S, Pin,SPI
from sdcard import SDCard
import os, time, gc
spi = 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 header
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 = 0
buffer = bytearray(1024) # 小缓冲区减少内存压力
while bytes_written < total_bytes:
read = i2s.readinto(buffer)
if read == 0:
print("警告:未读取到数据")
continue
f.write(buffer[:read])
bytes_written += read
gc.collect()
# 实时进度
elapsed = time.ticks_diff(time.ticks_ms(), start_time) / 1000
print(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 DAC
while True:
try:
num_read = f.readinto(wav_samples_mv)
# WAV文件结束
if num_read == 0:
break
# 直到所有样本都写入I2S外围设备
num_written = 0
while 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 = 0
for 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("检测到持续静默")
# 触发后续处理
'''