MicroPython 开发ESP32应用教程 之 I2S、INMP441音频录制、MAX98357A音频播放、SD卡读写

本课程我们讲解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等主流微控制器平台,通过统一接口简化跨硬件开发‌

三)、核心功能实现
  1. ‌音频输入/输出‌

    • 录音‌:从麦克风模块获取PCM音频数据
    • 播放‌:向DAC或音频解码器发送音频流‌27。
  2. 参数灵活配置‌

    初始化时可设置关键参数:

    python 复制代码
    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"}
  3. ‌中断与DMA支持‌

    支持异步数据读写,通过DMA减少CPU占用率,提升实时性‌

四)、典型应用场景
  1. 音频播放器

    播放WAV/MP3文件(需解码库支持)‌。

  2. 语音采集系统

    连接INMP441等数字麦克风实现环境音录制‌。

  3. 实时语音处理

    结合神经网络进行关键词识别或声纹分析‌

三、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("检测到持续静默")
        # 触发后续处理
'''
相关推荐
Freak嵌入式2 个月前
开源一款I2C电机驱动扩展板-FreakStudio多米诺系列
嵌入式硬件·嵌入式·智能硬件·开源硬件·micropython·电机驱动·电子模块
skywalk81635 个月前
esp32c3开发板通过micropython的mqtt库连MQTT物联网消息服务器
单片机·物联网·mqtt·esp32·micropython
Mr_Chenph6 个月前
MicroPython rp2-LVGL 固件编译记录
lvgl·micropython·pico·固件
哦豁灬7 个月前
树莓派pico上手
单片机·嵌入式硬件·micropython·树莓派pico
dnpao8 个月前
七、ESP32-S3上使用MicroPython点亮WS2812智能LED灯珠并通过web控制和JS颜色选择器改变灯珠颜色
esp32·micropython·ws2812·js颜色选择器
dnpao8 个月前
五、ESP32-S3上使用MicroPython点亮WS2812智能LED灯珠并通过web控制改变灯珠颜色
esp32·micropython·ws2812
张高兴9 个月前
张高兴的 MicroPython 入门指南:(三)使用串口通信
iot·micropython·raspberry pi
张高兴9 个月前
张高兴的 MicroPython 入门指南:(二)GPIO 的使用
iot·micropython·raspberry pi
张高兴9 个月前
张高兴的 MicroPython 入门指南:(一)环境配置、Blink、部署
iot·micropython·raspberry pi