这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 活动中心 » 板卡试用 » 【M5PAPERESP32EINKDEVKIT评测】M5Paper适配xiaoz

共1条 1/1 1 跳转至

【M5PAPERESP32EINKDEVKIT评测】M5Paper适配xiaozhi-esp32,玩转墨水屏小智

助工
2026-04-11 03:06:12     打赏

前言

M5Paper作为M5Stack家族中唯一配备电子墨水屏的成员,凭借4.7英寸大屏和丰富的扩展接口,如果能够匹配小智AI,将会格外吸睛。

经过一周时间的适配和调试,终于成功的为M5Paper匹配上了小智AI。值得注意的是,M5Paper本身并不带音频编解码器,我在GPIO25/32/26/18/33/34上外接了一块外置音频模块,包含ES8311音频编解码器、NS4150B功放和数字麦克风,实现了完整的音频采集与播放功能。

在实际移植过程中,需要注意准确适用M5Paper的两组I2C总线,不能混淆颠倒!
核心要点包括:M5Unified 库的深度集成双 I2C 总线的严格隔离,以及电子墨水屏与 LVGL 的适配


一、m5stack-m5paper 目录文件结构

完整源码:https://github.com/HonestQiao/xiaozhi-esp32/tree/m5paper

 xiaozhi-esp32/main/boards/m5stack-m5paper 目录下,存放了 M5Paper 的所有板级适配代码:

main/boards/m5stack-m5paper/
├── config.h              # 引脚定义与硬件参数(I2C、I2S、EPD 等)
├── config.json           # 编译目标声明(ESP32)与 sdkconfig 附加项
├── m5paper.cc            # 核心板级逻辑:Board 类实现、GPIO/I2C/音频/电源初始化
├── m5paper_display.h     # M5Paper 显示屏驱动头文件
├── m5paper_display.cc    # IT8951 电子墨水屏 + LVGL 的具体适配实现
├── README.md             # 编译命令速查
└── sdkconfig.m5paper     # PSRAM 预配置模板(编译前必须复制到根目录)

各文件职责说明

文件作用关键内容




config.h硬件宏定义GPIO 引脚分配、EPD 分辨率、音频采样率、EXT_I2C_NUM 
config.json构建配置声明 target: esp32,追加 PSRAM、8MB 分区表等 sdkconfig 项
m5paper.ccBoard 主入口继承 WifiBoard,实现 InitializeI2c()、GetAudioCodec()、GetBatteryLevel() 
m5paper_display.h/.cc显示驱动封装 M5PaperDisplay 类,负责 M5Unified 初始化、LVGL flush、睡眠/唤醒
sdkconfig.m5paper内存配置CONFIG_SPIRAM=y、CONFIG_SPIRAM_SPEED=40 等关键选项
README.md编译指引idf.py set-target esp32、cp sdkconfig.m5paper sdkconfig 等命令

二、项目中引入的 M5 模块

为了让 M5Paper 的硬件能力被充分利用,本项目引入了以下核心库:

模块作用说明




M5Unified硬件抽象层自动识别 M5Paper,初始化 IT8951 电子墨水屏、AXP192 电源管理、RTC、IMU 等
M5GFX图形驱动M5Unified 内部依赖,处理 IT8951 的刷屏、灰度转换、pushImage 等

关键代码体现

  • m5paper.cc  #include <M5Unified.h>

  • m5paper_display.cc  #include <esp_lvgl_port.h>

  • 通过 M5.begin(cfg) 一次性完成主板、屏幕、电源、I2C 的自动初始化

m5paper_xiaozhiai_layer.png


三、I2C 总线的严格区分(⚠️ 核心要点)

M5Paper 存在两组物理独立的 I2C 总线,这是最容易踩坑的地方。代码中必须严格隔离,否则会出现地址冲突或设备初始化失败。

3.1 I2C_NUM_0:外接音频专用

仅用于连接 外接音频模块(ES8311 编解码器)。

  • SDA:GPIO 25(Port A 黄线)

  • SCL:GPIO 32(Port A 白线)

  • 设备:ES8311(地址 0x18)

  • 管理方:由 m5paper.cc 中的 InitializeI2c() 手动创建并独占

// m5paper.ccvoid InitializeI2c() {
    i2c_master_bus_config_t i2c_bus_cfg = {
        .i2c_port = (i2c_port_t)EXT_I2C_NUM,  // = I2C_NUM_0
        .sda_io_num = AUDIO_CODEC_I2C_SDA_PIN, // GPIO 25
        .scl_io_num = AUDIO_CODEC_I2C_SCL_PIN, // GPIO 32
        // ...
    };
    ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_));}

3.2 I2C_NUM_1:板载传感器总线

 M5Unified 内部自动初始化,用户无需手动创建。

  • SDA:GPIO 21

  • SCL:GPIO 22

  • 设备:GT911 触摸屏、BM8563 RTC、SHT30 温湿度传感器(如外接)等

  • 管理方:通过 M5.In_I2C 访问

// 读取 SHT30 温度(使用 M5Unified 内部 I2C_NUM_1)
M5.In_I2C.start(0x44, false, 400000);
M5.In_I2C.write(0x2C);
M5.In_I2C.write(0x06);
M5.In_I2C.stop();

【踩坑记录】:误将 ES8311 配到 I2C_NUM_1

  • 现象:i2c_master_probe 扫描不到 0x18

  • 原因:ES8311 物理上只接到了 GPIO 25/32,对应 I2C_NUM_0

  • 解决:在 config.h 中显式定义 EXT_I2C_NUM = I2C_NUM_0,audio I2C 与 sensor I2C 彻底分离


实际的I2C扫描输出日志:

     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                         -- -- -- -- -- -- -- --
10:                         18 -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

说明:只有 0x18(ES8311)出现在 I2C_NUM_0 的总线上,证明了总线隔离的正确性。


四、外接音频模块的具体连线

本项目使用的外接音频模块基于 ES8311 + NS4150B方案,通过 M5Paper 的 Port A/B/C 引出。

4.1 引脚定义(来自 config.h)

/*
引脚定义说明(原子echo风格接线):
PORT_A_G25_黄 : SDA  -> ES8311 SDA
PORT_A_G32_白 : SCL  -> ES8311 SCL

PORT_B_GND_黑 : MLCK -> NC(未连接)
PORT_B_G26_黄 : SCLK -> BCLK
PORT_B_G33_白 : DIN  -> DOUT(ESP32 DIN 接 ES8311 的 DOUT)

PORT_C_G18_黄 : WS   -> LRCK
PORT_C_G19_白 : DOUT -> DIN(ESP32 DOUT 接 ES8311 的 DIN)
PORT_B_5V_红  : 5V   -> 模块供电
PORT_B_GND_黑 : GND  -> 模块地
*/
#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_NC
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_26
#define AUDIO_I2S_GPIO_DIN  GPIO_NUM_33   

// 从 ES8311 接收音频数据
#define AUDIO_I2S_GPIO_WS   GPIO_NUM_18
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_19   

// 向 ES8311 发送音频数据
#define AUDIO_CODEC_I2C_SDA_PIN  GPIO_NUM_25
#define AUDIO_CODEC_I2C_SCL_PIN  GPIO_NUM_32
#define AUDIO_CODEC_ES8311_ADDR  ES8311_CODEC_DEFAULT_ADDR  // 0x18
#define AUDIO_CODEC_GPIO_PA     GPIO_NUM_NC  // 无独立功放使能GPIO

4.2 接线示意图

m5paper_audio_link.png

9ce50159c4eed0033d5f251735b8d809.jpg




五、音频模块的处理5.1 ES8311 的初始化

 m5paper.cc 中,GetAudioCodec() 创建了一个 Es8311AudioCodec 实例,所有参数都来源于 config.h:

virtual AudioCodec* GetAudioCodec() override {
    if (audio_codec_ == nullptr) {
        audio_codec_ = new Es8311AudioCodec(
            i2c_bus_,         // 手动创建的 I2C_NUM_0
            EXT_I2C_NUM,      // I2C_NUM_0
            AUDIO_INPUT_SAMPLE_RATE,   // 24000
            AUDIO_OUTPUT_SAMPLE_RATE,  // 24000
            AUDIO_I2S_GPIO_MCLK,       // NC
            AUDIO_I2S_GPIO_BCLK,       // GPIO 26
            AUDIO_I2S_GPIO_WS,         // GPIO 18
            AUDIO_I2S_GPIO_DOUT,       // GPIO 19
            AUDIO_I2S_GPIO_DIN,        // GPIO 33
            AUDIO_CODEC_GPIO_PA,       // NC
            AUDIO_CODEC_ES8311_ADDR,   // 0x18
            false);
    }
    return audio_codec_;}

注意:采样率统一为 24kHz,输入输出同频,简化了时钟配置。

5.2 按键的特殊处理:DisablePullButton

M5Paper 的板载按键(A/B/C)已经带有外部上拉电阻(10kΩ)。如果直接使用 xiaozhi-esp32 的默认 Button 类,它会启用 ESP32 的内部上拉,与外部上拉并联后导致电平判断异常(按键灵敏度变低或无法触发)。

因此代码中自定义了 DisablePullButton:

class DisablePullButton : public Button {public:
    DisablePullButton(gpio_num_t gpio_num) : Button(...) {
        // ...
        button_gpio_config_t gpio_config = {
            .gpio_num = gpio_num,
            .active_level = 0,
            .enable_power_save = false,
            .disable_pull = true  // 禁用内部 pull-up
        };
        iot_button_new_gpio_device(&button_config, &gpio_config, &button_handle_);
    }};

按键功能映射:

  • GPIO 38(中间键 / Boot):单击切换对话状态,长按进入配网,启动时长按进入 WiFi 配网

  • GPIO 37(左键 / Vol+):单击音量 +10,长按音量 MAX

  • GPIO 39(右键 / Vol-):单击音量 -10,长按静音

m5paper_button.png

5.3 睡眠模式与音频联动

为了省电,代码使用了 PowerSaveTimer:

power_save_timer_ = new PowerSaveTimer(240, 60, -1);// CPU max=240MHz, 60秒无操作进入睡眠, -1表示不自动关机
power_save_timer_->OnEnterSleepMode([this]() {
    if (audio_codec_) {
        audio_codec_->EnableInput(false);  // 关闭麦克风省电
    }});

六、电子墨水屏的适配

M5Paper 的屏幕由 IT8951 驱动控制器控制,分辨率为 960×540,支持 16 级灰度。本项目的显示适配核心在 m5paper_display.cc中完成。

6.1 初始化策略

不直接操作 IT8951 的 SPI 寄存器,而是通过 M5Unified + M5GFX 进行高层抽象:

void M5PaperDisplay::InitializeM5Unified() {
    auto cfg = M5.config();
    cfg.clear_display = true;
    cfg.output_power  = true;
    cfg.internal_imu  = true;
    cfg.internal_rtc  = true;
    cfg.internal_spk  = true;  // 虽是板载speaker标记,但不影响外接模块
    cfg.internal_mic  = true;

    M5.begin(cfg);  // 自动检测并初始化 M5Paper 的 IT8951

    M5.Display.setEpdMode(m5gfx::epd_mode_t::epd_fastest);
    M5.Display.setRotation(0);  // 竖屏:540x960}

6.2 LVGL 与电子墨水屏的集成

电子墨水屏的特殊性在于:局部刷新容易留下残影。因此本项目采用了 FULL 全屏刷新模式

// 分配一整屏的 buffer(存放在 PSRAM 中)
size_t buf_size = width * height * sizeof(lv_color_t);  // RGB565
void* buf1 = heap_caps_malloc(buf_size, MALLOC_CAP_SPIRAM);
lv_display_set_buffers(lv_display_, buf1, nullptr, buf_size, LV_DISPLAY_RENDER_MODE_FULL);
lv_display_set_flush_cb(lv_display_, lvgl_flush_cb);

Flush 回调负责将 LVGL 渲染好的 RGB565 图像推送到电子墨水屏:

void M5PaperDisplay::FlushToEPD(const lv_area_t* area, const uint8_t* px_map) {
    // M5GFX 自动将 RGB565 转换为 IT8951 支持的 4-bit 灰度
    M5.Display.pushImage(x1, y1, w, h, (const uint16_t*)px_map);

    // 触发 IT8951 刷新(关键:不调用 display() 则只写入内存,不会显示)
    M5.Display.display();

    lv_disp_flush_ready(lv_display_);
 }

6.3 电源与屏幕控制

M5Paper 的屏幕电源由两颗 GPIO 控制:

// 在 InitializeGpio() 中
#define EPD_MAIN_PWR_PIN  GPIO_NUM_2   // 电子墨水屏主电源
#define EXT_PWR_EN_GPIO   GPIO_NUM_5   // 外部电源使能
gpio_set_direction(EPD_MAIN_PWR_PIN, GPIO_MODE_OUTPUT);
gpio_set_level(EPD_MAIN_PWR_PIN, true);
gpio_set_direction(EXT_PWR_EN_GPIO, GPIO_MODE_OUTPUT);
gpio_set_level(EXT_PWR_EN_GPIO, true);

同时,为了避免 M5Unified 对屏幕和音频引脚的预占用,代码在初始化时显式 reset 了相关 GPIO

// 释放 GPIO 25/32,确保 ES8311 的 I2C 能正常接管
gpio_reset_pin(AUDIO_CODEC_I2C_SDA_PIN);
gpio_reset_pin(AUDIO_CODEC_I2C_SCL_PIN);// 释放 I2S 引脚
gpio_reset_pin(AUDIO_I2S_GPIO_BCLK);
gpio_reset_pin(AUDIO_I2S_GPIO_DOUT);
gpio_reset_pin(AUDIO_I2S_GPIO_DIN);// 释放 EPD 的 SPI 数据引脚
gpio_reset_pin(EPD_GPIO_MOSI);
gpio_reset_pin(EPD_GPIO_MISO);
gpio_reset_pin(EPD_GPIO_SCK);// 注意:不 reset CS/RST/BUSY,因为它们由 IT8951 驱动自行管理

七、电量、温度等传感器适配7.1 电池电量读取

M5Paper 使用 AXP192/AXP2101 管理电源,M5Unified 已经封装好了接口。在 m5paper.cc 中重写了 GetBatteryLevel:

virtual bool GetBatteryLevel(int& level, bool& charging, bool& discharging) override {
    level = M5.Power.getBatteryLevel();

    auto charge_status = M5.Power.isCharging();
    charging = (charge_status == m5::Power_Class::is_charging_t::is_charging);
    discharging = (charge_status == m5::Power_Class::is_charging_t::is_discharging);

    return true;
}

该接口会被 xiaozhi-esp32 的 UI 层自动调用,在屏幕顶部显示电量百分比和充电状态。

7.2 环境温度与湿度(SHT30)

通过 M5Unified 的 M5.In_I2C(即 I2C_NUM_1,GPIO 21/22)读取板载 SHT30 温湿度传感器(I2C 地址 0x44)。

代码中将其注册为了一个 MCP 工具,小智 AI 可以直接调用:

void InitializeMcpTools() {
    auto& mcp_server = McpServer::GetInstance();
    mcp_server.AddTool("self.environment.get_temperature",
        "获取环境温度和湿度...",
        PropertyList(),
        [this](const PropertyList&) -> ReturnValue {
            float temp = ReadSht30Temperature();
            
            // 读取湿度
            M5.In_I2C.start(0x44, false, 400000);
            M5.In_I2C.write(0x2C);
            M5.In_I2C.write(0x06);
            M5.In_I2C.stop();
            vTaskDelay(pdMS_TO_TICKS(20));
            
            M5.In_I2C.start(0x44, true, 400000);
            M5.In_I2C.read(data, 6, true);
            M5.In_I2C.stop();
            
            // CRC 校验后组装 JSON 返回
            cJSON* result = cJSON_CreateObject();
            cJSON_AddNumberToObject(result, "temperature", temp);
            cJSON_AddNumberToObject(result, "humidity", humidity);
            return result;
        });}



八、编译与运行8.1 编译步骤

该项目基于 ESP32(带 4MB PSRAM),必须使用特定的 sdkconfig:

# 1. 设置目标为 ESP32idf.py set-target esp32
# 2. 复制 M5Paper 的 PSRAM 配置(必须!)cp main/boards/m5stack-m5paper/sdkconfig.m5paper sdkconfig
# 3. 打开 menuconfig 选择 Boardidf.py menuconfig
# 路径:Xiaozhi Assistant -> Board Type -> M5Stack M5Paper
# 4. 编译idf.py build# 5. 烧录并监视idf.py flash monitor

8.2 关键编译配置

config.json 中的预定义:

  • CONFIG_SPIRAM=y:启用 PSRAM(LVGL 全屏缓冲必需)

  • CONFIG_SPIRAM_SPEED=40:PSRAM 40MHz

  • CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/v2/8m.csv":适配 8MB Flash

  • CONFIG_USE_MULTILINE_CHAT_MESSAGE=n:不开启多行聊天气泡

sdkconfig.m5paper 中的内存配置:

CONFIG_SPIRAM=y
CONFIG_SPIRAM_BOOT_INIT=y
CONFIG_SPIRAM_USE_MALLOC=y
CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=4096
CONFIG_SPIRAM_SPEED=40



九、最终效果展示9.1 功能清单

 已验证功能

  •  外接 ES8311 音频编解码器驱动(I2C_NUM_0 隔离)

  •  24kHz 音频输入/输出(GPIO 26/18/19/33)

  •  IT8951 电子墨水屏 LVGL 显示(FULL 刷新无残影)

  •  3 个实体按键完整映射(Boot / Vol+ / Vol-)

  •  电池电量与充电状态实时显示

  •  SHT30 温湿度读取(通过小智 AI 语音查询)

  •  60 秒自动睡眠、任意操作唤醒

  •  Wi-Fi 配网与 WebSocket 语音对话

9.2 实际效果

m5paper_xiaozhiai.png

视频:


十、总结

本次适配的核心在于:正确使用 M5Unified 管理板载资源,同时在 I2C_NUM_0 上独立管理外接 ES8311 音频模块。通过将传感器总线与音频总线物理隔离,避免了地址冲突;通过 M5GFX + LVGL 的全屏缓冲策略,让电子墨水屏能够流畅显示语音助手的对话界面。




关键词: M5paper     小智AI     墨水屏    

共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]