这是课程视频的简化版文字稿,你可以在这里找到完整的课程视频
T型板的不足
之前T型开发板性能尚可,价格实惠,通用型强,适合基础课程的学习,但是它有以下制约:
- 整体处理能力有限,难以处理复杂任务或多任务情景
- 缺乏对
USB-OTG的支持,难以进行鼠标,键盘等外设的扩展 - 芯片
IO接口较少,难以同时连接多种外设 - 对复杂电路案例来说,搭建成本和可靠性均不能满足要求
USB-OTG允许 USB 设备充当主机或周边设备。这意味着USB-OTG设备可以连接到其他 USB 设备,而无需计算机或其他主机设备。
认识本课程所需的设备
- 基于ESP32-S3的多媒体开发平台
- 图片展示

- 采用
ESP32-S3-WROOM模块,内含8MB闪存与8MB内存 - 配备8键无冲数字键盘,方便支持组合键的开发
- 配有TF卡槽,方便存储空间拓展
- 采用160x128
ST7735彩屏进行显示,具有屏幕触控功能 - 采用两个USB-C接口,分别用与连接上位机和实现USB-OTG
- 采用I2S实现音频输出,配备3.5mm音频输出接口(新版本的开发平台内置了扬声器)
- 板载
MPU-6050传感器,支持体感开发
ESP32是乐鑫科技开发的一系列低成本,低功耗的单片机微控制器,集成了Wi-Fi和双模蓝牙.它采用Tensilica Xtensa LX6双核和单核微处理器,内置无线开关,RF换衡器,功率放大器,低噪声接收放大器,滤波器和电源管理模块
ST7735是一款具有SPI接口的彩色TFT液晶显示驱动芯片,具有 128 x 160 像素的显示分辨率,可显示 16 位RGB颜色
MPU6050是一种集成了 3 轴加速度计和 3 轴陀螺仪的微型运动处理单元(IMU).它是一种小型,低功耗的传感器,能方便的采集加速度和角速度数据,用于测量物体的运动
I2S是用于数字音频传输的串行总线标准,由三条线组成:数据线,时钟线和选择线.其是一种面向主从架构的总线:主设备负责产生时钟信号并将数据传输到从设备,从设备负责接收数据并将其转换为模拟音频信号
- 基于ESP32-S3的智能摄像头
- 图片展示

- 采用
ESP32-S3模块,并配备ST7735彩屏 - 内置
OV2640摄像头,便于开发CV,图传类应用 - 内置TF卡槽和
MPU6050六轴传感器 - 基于拓展手柄可支持
I2S音频输出,5D按键,INMP441数字麦克风 - 配有
7Pin拓展接口便于连接其他外设,用USB-C接口连接上位机
OV2640是一款 1632 x 1232 像素,1/4 英寸,低功耗 CMOS 图像传感器,可用于各种应用.包括机器视觉,物联网,无人机和智能家居,具有 50 帧/秒的视频帧率和 0.008 lux 的低光性能
INMP441是 TDK InvenSense 制造的一种高性能,低功耗,数字输出,全向 MEMS 麦克风.它采用 4.72 x 3.76 x 1 毫米的薄型表面贴装封装,具有可回流焊接兼容性,无灵敏度下降
配套软件系统的重要性
工作室定制了高度客制化的MicroPython固件,继承官方MicroPython特性的同时,还添加了以下支持:
- MP3解码
- FFT计算辅助
- 摄像头支持(
OV2640) - TFLite Runtime
- 声音池
- 软管线3D
- 2D图像仿射变换
- 帧缓冲缩放辅助
- I2S音频录放
- 数字图像处理
- 颜色识别
- USB键鼠和摄像头
以上功能模块可以极大方便人工智能,2D/3D动画和音频等复杂功能的开发
FFT 是快速傅里叶变换的缩写,它是一种计算离散傅里叶变换 (DFT) 的有效算法.FFT有许多应用:包括信号处理,图像处理和信号合成
TensorFlow Lite 是 TensorFlow 的轻量级版本,用于在移动设备,嵌入式设备和物联网设备上运行机器学习模型
运行时(Runtime)是计算机程序运行生命周期内的一个重要阶段,它负责提供程序运行所需的各种服务,并确保程序能够正确执行
管线通常用于处理数据:例如一个图像处理管线可以将图像从原始格式转换为压缩格式,然后进行滤波和增强
背景知识
相较于WAV音频,MP3音频在同等条件下占用空间更小.但由于这种类型的文件存储的是编码压缩之后的音频数据,所以播放前需要进行解码.ESP32-S3的性能相较于T型板有了显著提升,可以进行音频解码操作
hzmp3 模块包含的工具方法
我们将用几个例子引入它们:
- 获取MP3文件信息
import hzmp3
data = open("test.mp3", "rb").read(2048) #by default #as an array
file_size = os.path.getsize("test.mp3")
info = hzmp3.getMp3Info(data, len(data), file_size)
print("总时间长度:", info[0])
print("第一帧数据字节数:", info[1])
print("普通帧数据的字节数:", info[2])
print("采样率:", info[3])
print("声道型:", info[4]) #1 for stereo, 2 for mono
- 初始化
import hzmp3
bck, ws, sd = "I2S的三个GPIO引脚编号"
samplerRate = "待播放MP3的采样率"
channels = "声道数"
hzmp3.initMp3(bck, ws, sd, samplerRate, channels)
#初始化解码器,I2S和工作在另一个核心的播放任务
#本模块占用#0 I2S
- 将一定帧数的数据送入队列等待解码
import hzmp3
inputData = open("test.mp3", "rb").read() #readbinary
volumeFactor = "音量等级[0,5]" #0为原音量,其余为原音量/2^volumeFactor
frameCount = "送入的数据帧数"
bytesPerFrame = "每帧数据字节数"
if hzmp3.playMp3(inputData, volumeFactor, frameCount, bytesPerFrame):
print("Success!") #返回bool
else:
print("Fail!")
#如果系统处于暂停状态,调用此方法会切换成播放状态
#开发者需要确定第一帧和普通帧数据的长度
- 快进快退
import hzmp3
timeS = "快进/快退秒数,复数为快退"
frameSizeCommon = "音频文件普通帧字节数"
frameSizeFirst = "第一帧字节数"
hzmp3.progressSeek(timeS, frameSizeCommon, frameSizeFirst)
- 一些简单方法
import hzmp3
hzmp3.getTime() #获取已播放时间
hzmp3.pauseOrContinue() #切换播放/暂停
hzmp3.isFinish() #判断队列是否播放完毕
hzmp3.mp3DeInit() #释放MP3解码器和I2S资源,并销毁另一个核心的播放任务
案例
import hzmp3
from machine import SoftSPI
from machine import Pin
from BN165DKBDriver import keyCode
import machine
import utime,time
#I2S
I2Sbck=14
I2Sws=38
I2Ssd=12
#将秒数转化为 分钟:秒数 格式字符串的方法
def MS(I):
#秒数除以60取整得到分钟数
M=int(I/60)
#秒数对60取模得到余数秒数
S=I%60
#结果字符串中的分钟子串
strM=str(M)
#结果字符串中的秒数子串
strS=str(S)
#若分钟数小于10则在分钟子串前面补0
if(M<10):
strM="0"+str(M)
#若秒数小于10则在秒数子串前面补0
if(S<10):
strS="0"+str(S)
return strM+":"+strS
#主程序开始
#设置当前CPU的工作频率为240MHz(默认160MHz)
machine.freq(240000000)
#无冲数字键盘对应的IO口
CP=Pin(41,Pin.OUT)
CE=Pin(39,Pin.OUT)
PL=Pin(40,Pin.OUT)
Q7=Pin(21,Pin.IN)
adcIn=(CP,CE,PL,Q7)
#要播放的MP3文件名称
mp3Name="xxdd.mp3"
#音量等级(可选0-5整数)
volume=0
#单次送入队列解码的数据帧数
frameNum=-1
#最大读取字节数,这里为8KB,大小要合适
maxReadBytes=1024*8
#打印信息
print("playing ",mp3Name)
#闪存中的MP3文件
mp3File = open(mp3Name,'rb')
#若文件不存在则打印提示信息
if(not mp3File):
print(f"filename:{mp3Name} not found!")
#分4次读取总数为2048字节(2KB)的数据,服务于获取待播放MP3的信息
tempMp3Data=mp3File.read(512)
tempMp3Data=tempMp3Data+mp3File.read(512)
tempMp3Data=tempMp3Data+mp3File.read(512)
tempMp3Data=tempMp3Data+mp3File.read(512)
#将文件指针跳转到文件末尾
mp3File.seek(0,2)
#获取文件总字节数
FileSize=mp3File.tell()
#获取待播放MP3的信息
mp3Info=hzmp3.getMp3Info(tempMp3Data,2048,FileSize)
#得到总时长
totalTime=mp3Info[0]
#得到第一帧的数据字节数
frameSizeFirst=mp3Info[1]
#得到普通帧的数据字节数
frameSizeCommon=mp3Info[2]
#计算出单次送入队列解码的数据帧数(以普通帧计)
frameNum=maxReadBytes//frameSizeCommon #int div
#得到采样率
samplerRate=mp3Info[3]
#得到通道数
channels=mp3Info[4]
#初始化MP3及I2S
hzmp3.initMp3(I2Sbck,I2Sws,I2Ssd,samplerRate,channels)
#将文件指针跳转到文件开始
mp3File.seek(0)
#是否为第一帧数据标志
isFirst=True
#待解码的一批数据(可以包含一帧或多帧数据),初始时为空
inputData=bytearray([])
#是否读取下一帧数据标志
readNext=True
#已播放时间打印时间戳
printTimeStamp=0
#播放循环
while True:
#获取按键编号
kc=keyCode(adcIn)
#本次需要读取的字节数=一帧数据长度*送入的帧数
needBytes=frameSizeCommon*frameNum
#一轮需要的数据如果超过512字节则需要分批读取
#获取当前的已播放时间(以秒计)
currTime=hzmp3.getTime()
#若距离上一次打印已播放时间信息已经超过了2秒(即2000ms)
if(utime.ticks_ms()-printTimeStamp>2000):
#更新时间戳
printTimeStamp=utime.ticks_ms()
#打印当前的已播放时间
print(MS(currTime))
#若按下键盘左侧右键
if(kc==3):
#执行快进,并获取快进后的文件字节索引
seek=hzmp3.progressSeek(5,frameSizeCommon,frameSizeFirst)
#将文件指针跳转到对应字节处
mp3File.seek(seek)
#设置需要读取下一帧数据
readNext=True
#打印当前的已播放时间
print(MS(currTime))
#休眠一段时间,防止按键连按过快
time.sleep(0.2)
#若按下键盘左侧左键
elif(kc==1):
#执行快退,并获取快退后的文件字节索引
seek=hzmp3.progressSeek(-5,frameSizeCommon,frameSizeFirst)
#如果快退到文件开始,则设置需要读取第一帧数据
if(seek==0):
isFirst=True
#将文件指针跳转到对应字节处
mp3File.seek(seek)
#设置需要读取下一帧数据
readNext=True
#打印当前的已播放时间
print(MS(currTime))
#休眠一段时间,防止按键连按过快
time.sleep(0.2)
#当前需要解码的一批帧数据的帧字节数
currFrameByteCount=frameSizeCommon
#如果需要解码的是第一帧
if(isFirst):
#设置为不需要读取第一帧数据
isFirst=False
#设置本次需要读取的字节数为frameSizeFirst
#否则保持needBytes为frameNum个普通帧所需的字节数
needBytes=frameSizeFirst
#当前需要解码的一批帧数据的帧字节数
currFrameByteCount=frameSizeFirst
#如果数据送入成功
if(readNext):
#清空当前数据,以备读取下一帧数据
inputData=bytearray([])
#分步读取needBytes字节的数据,每步最多读取512字节
while(needBytes>0):
if needBytes>512:
inputData=inputData+mp3File.read(512)
needBytes=needBytes-512
else:
inputData=inputData+mp3File.read(needBytes)
needBytes=0
#将当前待解码的一批数据送入解码模块解码并播放
# 一批帧数据 音量控制参数 帧数 每帧字节数
readNext=hzmp3.playMp3(inputData,volume,len(inputData)//currFrameByteCount,currFrameByteCount)
#若播放队列中没有数据且文件已经读取完毕
if(hzmp3.isFinish() and len(inputData)==0):
#打印结束信息
print("play finished.....")
break
#释放MP3播放相关资源
hzmp3.mp3DeInit()
#关闭当前歌曲文件
mp3File.close()