一、硬件介绍
Tab5 集成了双芯片架构和丰富的硬件资源,其主控采用基于 RISC‑V 架构的 ESP32‑P4 SoC,并配备 16MB Flash 与 32MB PSRAM,无线模块则选用 ESP32-C6-MINI-1U,支持 Wi-Fi 6;
还配备5英寸(1280×720 IPS)触控屏幕,以及2MP摄像头(1600×1200)、双麦克风阵列,3.5mm耳机孔与扬声器;
内置BMI270六轴传感器、实时时钟,板载HY2.0-4P,M5-Bus,GPIO_EXT排母和microSD卡槽等;
底部兼容NP‑F550可拆卸锂电池(具备充放电与实时监测电路);


二、功能实现
1、硬件介绍
麦克风 / 扬声器
Tab5的麦克风采用双麦克风系统(AEC 回声消除)、扬声器由1W 8Ω NS4150B驱动;
双麦克风阵列由两个芯片协作控制:
ES7210:作为AEC回声消除前端芯片,负责麦克风音频的采集与预处理,通过I2C总线(地址0x40)与ESP32-P4主控通信;
ES8388:作为音频编解码芯片,负责对麦克风采集的信号进行编码处理,通过I2C总线(地址0x10)与ESP32-P4主控通信;
NS4150B 功率放大器:将ES8388输出的模拟音频信号进行功率放大,驱动板载扬声器发声,最大输出功率1W;
硬件引脚连接



原理图


2、功能效果
实现效果:主要通过LCD屏幕、板载麦克风 / 扬声器实现,通过触摸屏幕的UI按钮实现音频录制 / 播放的录音机功能;
默认为60s缓存录音,重新录音会清空;
底部两个UI按钮(录制 / 停止、播放 / 停止播放),在录制 / 播放状态时按钮颜色与文本会相应变化;
录音时每秒更新状态 “录制中: _s / 60s”,播放时显示 “播放中: _s / 总秒数”;
(播放时可点击停止播放,播放完成后恢复界面)

三、代码编写
通过修改相关宏定义参数,可配置不同录制的音频效果;
1、板载PSRAM为32MB;
当前音频内存占用:RECORD_SIZE * 2【16bit】 = 1.95 MB;
最长录制时间:(32 × 1024 × 1024 / 2) / RECORD_SAMPLERATE = 16min27s ;
2、若需更长录制的时间,可将音频存储到SD卡中,再进行读取等操作实现;
#include <M5Unified.h>
// 音量播放大小
#define VOLUME 255
// 音频参数
#define RECORD_LENGTH 720 // 每块采样数
#define RECORD_SAMPLERATE 17000 // 采样率:17kHz
#define MAX_RECORD_SEC 60 // 最大录制时长:60秒 (可根据PSRAM调整 / 存储到SD卡)
#define MAX_SAMPLES (RECORD_SAMPLERATE * MAX_RECORD_SEC)
#define MAX_CHUNKS ((MAX_SAMPLES + RECORD_LENGTH - 1) / RECORD_LENGTH)
#define RECORD_SIZE (MAX_CHUNKS * RECORD_LENGTH)
static int16_t *rec_data;
// 录制状态
static bool is_recording = false; // 是否正在录制
static size_t rec_chunks = 0; // 已录制块数
static size_t rec_write_idx = 0; // 写入索引
// UI参数
struct Rect
{
int32_t x, y, w, h;
};
static Rect btn_rec, btn_play;
static int32_t w;
static uint32_t last_display_sec = 0; // 记录上次显示的秒数
static uint32_t last_play_display_sec = 0; // 记录上次显示的播放秒数
// 绘制按钮
static void drawButton(const Rect &r, const char *label, uint16_t fillColor, uint16_t textColor)
{
M5.Display.fillRoundRect(r.x, r.y, r.w, r.h, 8, fillColor);
M5.Display.drawRoundRect(r.x, r.y, r.w, r.h, 8, TFT_WHITE);
M5.Display.setTextDatum(middle_center);
M5.Display.setTextColor(textColor);
M5.Display.setFont(&fonts::efontCN_24);
M5.Display.drawString(label, r.x + r.w / 2, r.y + r.h / 2);
}
// 触摸判断
static bool hitRect(const Rect &r, int32_t px, int32_t py)
{
return (px >= r.x && px < (r.x + r.w) && py >= r.y && py < (r.y + r.h));
}
// UI布局
static void layoutUI()
{
const int32_t sh = M5.Display.height();
const int32_t sw = M5.Display.width();
w = sw;
// 底部两个按钮,各占据一半宽度
const int32_t btn_w = sw / 2;
const int32_t btn_h = sh / 4;
const int32_t btn_y = sh - btn_h;
btn_rec = {0, btn_y, btn_w, btn_h};
btn_play = {btn_w, btn_y, btn_w, btn_h};
}
static void drawStaticUI()
{
M5.Display.fillScreen(TFT_BLACK);
// 标题
M5.Display.setTextDatum(top_center);
M5.Display.setTextColor(TFT_WHITE);
M5.Display.setFont(&fonts::efontCN_24);
M5.Display.drawString("语音录制", w / 2, 50);
// 绘制按钮
drawButton(btn_rec, is_recording ? "停止" : "录制", is_recording ? TFT_RED : TFT_DARKGREY, TFT_WHITE);
drawButton(btn_play, "播放", TFT_GREEN, TFT_WHITE);
M5.Display.display();
}
// 开始录制
static void startRecording()
{
if (M5.Speaker.isEnabled())
M5.Speaker.end();
if (!M5.Mic.isEnabled())
M5.Mic.begin();
is_recording = true;
rec_chunks = 0;
rec_write_idx = 0;
last_display_sec = 0; // 重置显示秒数计数器
// 清空缓冲区
memset(rec_data, 0, RECORD_SIZE * sizeof(int16_t));
// 清空显示区域
M5.Display.fillRect(0, 50, w, 150, TFT_BLACK);
// 更新按钮显示
drawButton(btn_rec, "停止", TFT_RED, TFT_WHITE);
M5.Display.display();
}
// 停止录制
static void stopRecording()
{
is_recording = false;
drawButton(btn_rec, "录制", TFT_DARKGREY, TFT_WHITE);
// 录制结束
M5.Display.fillRect(0, 100, w, 60, TFT_BLACK);
M5.Display.setTextDatum(top_center);
M5.Display.setTextColor(TFT_CYAN);
M5.Display.setFont(&fonts::efontCN_24);
M5.Display.drawString("录制结束", w / 2, 110);
M5.Display.display();
delay(800);
}
// 播放录制
static void playRecording()
{
if (is_recording)
return;
if (rec_chunks == 0)
{
M5.Display.setTextDatum(top_center);
M5.Display.setTextColor(TFT_RED);
M5.Display.setFont(&fonts::efontCN_24);
M5.Display.drawString("暂无录音!", w / 2, 150);
delay(1500);
drawStaticUI();
return;
}
while (M5.Mic.isRecording())
{
delay(1);
}
if (M5.Mic.isEnabled())
M5.Mic.end();
if (!M5.Speaker.isEnabled())
M5.Speaker.begin();
// 显示"正在播放"
const size_t total_samples = rec_chunks * RECORD_LENGTH;
const uint32_t total_duration = total_samples / RECORD_SAMPLERATE; // 总时长
uint32_t play_start_time = millis();
last_play_display_sec = 0; // 重置播放秒数
M5.Speaker.playRaw(rec_data, total_samples, RECORD_SAMPLERATE, false, 1, 0);
// 显示停止播放按钮
drawButton(btn_play, "停止播放", TFT_RED, TFT_WHITE);
// 播放时显示进度
while (M5.Speaker.isPlaying())
{
M5.update();
uint32_t elapsed_ms = millis() - play_start_time;
uint32_t elapsed_sec = elapsed_ms / 1000;
if (elapsed_sec != last_play_display_sec)
{
last_play_display_sec = elapsed_sec;
M5.Display.fillRect(0, 80, w, 100, TFT_BLACK);
M5.Display.setTextDatum(top_center);
M5.Display.setTextColor(TFT_GREEN);
M5.Display.setFont(&fonts::efontCN_24);
char playStr[48];
snprintf(playStr, sizeof(playStr), "播放中: %us / %us", elapsed_sec, total_duration);
M5.Display.drawString(playStr, w / 2, 120);
M5.Display.display();
}
if (M5.Touch.getCount())
{
auto d = M5.Touch.getDetail(0);
if (d.wasClicked() && hitRect(btn_play, d.x, d.y))
{
M5.Speaker.stop();
delay(100);
break;
}
}
delay(50);
}
// 重新回到录制模式
if (M5.Speaker.isEnabled())
M5.Speaker.end();
if (!M5.Mic.isEnabled())
M5.Mic.begin();
// 显示播放完成
M5.Display.fillRect(0, 80, w, 100, TFT_BLACK);
M5.Display.setTextDatum(top_center);
M5.Display.setTextColor(TFT_CYAN);
M5.Display.setFont(&fonts::efontCN_24);
M5.Display.drawString("播放完成", w / 2, 120);
M5.Display.display();
delay(1000);
// 恢复界面
drawStaticUI();
}
void setup(void)
{
M5.begin();
w = M5.Display.width();
// 设置屏幕显示参数
M5.Display.setRotation(1);
M5.Display.fillScreen(TFT_BLACK);
M5.Display.setTextDatum(top_center);
M5.Display.setTextColor(WHITE);
M5.Display.setFont(&fonts::efontCN_24);
// 分配音频缓冲区
rec_data = (typeof(rec_data))heap_caps_malloc(RECORD_SIZE * sizeof(int16_t), MALLOC_CAP_SPIRAM);
if (rec_data == nullptr)
{
rec_data = (typeof(rec_data))heap_caps_malloc(RECORD_SIZE * sizeof(int16_t), MALLOC_CAP_8BIT);
}
memset(rec_data, 0, RECORD_SIZE * sizeof(int16_t));
M5.Speaker.setVolume(VOLUME);
// 麦克风和扬声器不能同时使用
M5.Speaker.end();
M5.Mic.begin();
// 初始化UI布局
layoutUI();
drawStaticUI();
M5.Display.display();
}
void loop(void)
{
M5.update();
// 按钮触摸处理
if (M5.Touch.getCount() && M5.Touch.getDetail(0).wasClicked())
{
auto d = M5.Touch.getDetail(0);
const int32_t tx = d.x;
const int32_t ty = d.y;
// 录制按钮
if (hitRect(btn_rec, tx, ty))
{
if (!is_recording)
{
startRecording();
}
else
{
stopRecording();
}
}
// 播放按钮
else if (hitRect(btn_play, tx, ty))
{
playRecording();
}
}
// 录制循环
if (is_recording && M5.Mic.isEnabled())
{
// 检查是否达到60秒
if (rec_write_idx >= MAX_CHUNKS)
{
stopRecording();
M5.Display.setTextDatum(top_center);
M5.Display.setTextColor(TFT_ORANGE);
M5.Display.setFont(&fonts::efontCN_24);
M5.Display.drawString("已达60秒上限", w / 2, 60);
delay(1000);
drawStaticUI();
return;
}
// 录制一块数据
auto write_ptr = &rec_data[rec_write_idx * RECORD_LENGTH];
if (M5.Mic.record(write_ptr, RECORD_LENGTH, RECORD_SAMPLERATE))
{
// 更新块计数
++rec_write_idx;
if (rec_chunks < rec_write_idx)
{
rec_chunks = rec_write_idx;
}
// 显示录制时间
const uint32_t recorded_sec = (uint32_t)((rec_chunks * RECORD_LENGTH) / RECORD_SAMPLERATE);
if (recorded_sec != last_display_sec)
{
last_display_sec = recorded_sec;
M5.Display.fillRect(0, 100, w, 60, TFT_BLACK);
M5.Display.setTextDatum(top_center);
M5.Display.setTextColor(TFT_YELLOW);
M5.Display.setFont(&fonts::efontCN_24);
char timeStr[32];
snprintf(timeStr, sizeof(timeStr), "录制中: %us / 60s", recorded_sec);
M5.Display.drawString(timeStr, w / 2, 110);
M5.Display.display();
}
if (rec_write_idx >= MAX_CHUNKS)
{
stopRecording();
M5.Display.setTextDatum(top_center);
M5.Display.setTextColor(TFT_ORANGE);
M5.Display.setFont(&fonts::efontCN_24);
M5.Display.drawString("已达60秒上限", w / 2, 150);
delay(1000);
drawStaticUI();
}
}
}
}四、程序烧录
1、连接USB数据线至开发板;
2、选择端口号对应的开发板;
3、点击 上传 烧录程序到开发板上;


五、效果演示
录制音频,以及播放效果;
我要赚赏金
