目标平台: M5Stack PaperS3 (ESP32-S3R8) 显示器: ED047TC1 4.7" EPD 墨水屏 (960×540, 16级灰度) 框架: UIFlow MicroPython (基于 M5Unified + M5GFX) 源码: m5stack/uiflow-micropython + lovyan03/M5GFX
- 硬件规格与引脚映射
- M5 模块总览
- M5.Display — M5GFX 图形库
- M5.Touch — 触摸系统
- M5.Btn — 物理按键
- M5.Power — 电源管理
- M5.Imu — 陀螺仪 (BMI270)
- M5.Spk — 蜂鸣器
- M5.Mic — 麦克风 (如已启用)
- M5.Board — 板级识别
- WiFi — 网络
- machine.SDCard — SD 卡
- UIFlow 启动框架 (PaperS3)
- 完整示例
| 项目 | 规格 |
|---|---|
| SKU | C139 |
| SoC | ESP32-S3R8 (Xtensa LX7 双核, 240MHz) |
| PSRAM | 8MB Octal (OPI) |
| Flash | 16MB (外部 SPI, Quad I/O) |
| 显示屏 | 4.7" EPD_ED047TC1, 960×540, 16级灰度 |
| 触控 | GT911 电容触摸 (两指, 多种手势) |
| 传感器 | BMI270 陀螺仪 (I2C 0x68) |
| RTC | BM8563 (I2C 0x51) |
| 电池 | 1800mAh 锂电池 |
| 蜂鸣器 | 无源蜂鸣器 (PWM) |
| microSD | SPI 模式 (20MHz) |
| USB | Type-C (OTG/CDC/MSC/烧录) |
| EPD 信号 | ESP32-S3 GPIO |
|---|---|
| DB0~DB7 | GPIO6,14,7,12,9,11,8,10 |
| XSTL | GPIO13 |
| XLE | GPIO15 |
| SPV | GPIO17 |
| CKV | GPIO18 |
| PWR | GPIO45 |
| 外设 | I2C 地址 | 用途 |
|---|---|---|
| BM8563 RTC | 0x51 | 时钟、休眠唤醒 |
| GT911 触摸 | — | 电容触摸面板 |
| BMI270 陀螺仪 | 0x68 | 运动检测 |
| SD 信号 | ESP32-S3 GPIO |
|---|---|
| CS | GPIO47 |
| SCK | GPIO39 |
| MOSI | GPIO38 |
| MISO | GPIO40 |
| 功能 | GPIO | 说明 |
|---|---|---|
| BUZ_PWM | GPIO21 | 无源蜂鸣器 |
| USB_DET | GPIO5 | USB 供电检测 (>0.2V) |
| 外扩接口 | GPIO1, GPIO2 | HC1.25-4PLT |
所有外设通过 M5 单例对象访问(M5.begin() 初始化后可用):
| 模块 | 访问路径 | 用途 |
|---|---|---|
| 图形 | M5.Display.* |
M5GFX 绘图库 |
| 触摸 | M5.Touch.* |
GT911 电容触摸 |
| 按键 | M5.BtnA/BtnB/BtnC/* |
(PaperS3 无独立按键) |
| 电源 | M5.Power.* |
电池、充电、休眠 |
| IMU | M5.Imu.* |
BMI270 陀螺仪 |
| 喇叭 | M5.Spk.* |
蜂鸣器 |
| 麦克风 | M5.Mic.* |
(视固件版本) |
| 板级 | M5.Board |
主板识别常量 |
M5GFX 是 M5Stack 的底层图形库,基于 LovyanGFX。所有颜色使用 24-bit RGB 格式 (
0xRRGGBB),墨水屏自动映射到 16 级灰度。
成对使用的批量绘制函数。startWrite() 后所有绘图操作被缓冲,调用 endWrite() 才刷入显示缓冲区并触发 EPD 刷新。对墨水屏至关重要,能减少闪烁和刷新次数。
M5.Display.startWrite()
M5.Display.fillRect(0, 0, 960, 540, 0xFFFFFF)
M5.Display.drawString("Hello", 10, 10)
M5.Display.endWrite()将帧缓冲区内容刷新到实际显示屏幕。某些设备需要显式调用。
清除整个屏幕内容,填充默认背景色(白色)。
用指定颜色填充整个屏幕。
关闭显示资源,程序结束时调用。
返回屏幕的宽度和高度(像素)。
- PaperS3 默认: 960 × 540 (ESPDIY 驱动)
- 注意: UIFlow 启动应用是竖屏设计 (旋转后 540 × 960)
获取/设置屏幕旋转角度。参数范围 0~7:
- 0 = 270°
- 1 = 正常 (默认)
- 2 = 90°
- 3 = 180°
- 4~7 = 上下颠倒变体
获取/设置颜色深度。常见值: 1 (单色), 8 (256色), 16 (65K色), 24 (1677万色)。
获取/设置亮度(墨水屏通常不支持调亮,函数返回固定值)。
返回 True(PaperS3 是电子墨水屏设备)。
获取当前 EPD 刷新模式。
设置墨水屏刷新模式。
| 模式 | 值 | 说明 |
|---|---|---|
EPD_QUALITY |
1 | 最高质量,刷新最慢 (~2-3s) |
EPD_TEXT |
2 | 文本模式,兼顾速度与质量 |
EPD_FAST |
3 | 快速刷新 |
EPD_FASTEST |
4 | 最快刷新,质量最低 |
建议: 开发阶段用 EPD_FASTEST(4) 减少闪烁;最终显示用 EPD_QUALITY(1)。
M5.Display.drawPixel(x, y, color) # 单像素
M5.Display.drawLine(x0, y0, x1, y1, color) # 直线M5.Display.drawRect(x, y, w, h, color) # 空心矩形
M5.Display.fillRect(x, y, w, h, color) # 实心矩形
M5.Display.drawRoundRect(x, y, w, h, r, color) # 圆角空心
M5.Display.fillRoundRect(x, y, w, h, r, color) # 圆角实心r: 圆角半径(像素)
M5.Display.drawCircle(x, y, r, color) # 空心圆
M5.Display.fillCircle(x, y, r, color) # 实心圆
M5.Display.drawEllipse(x0, y0, x1, y1, color) # 空心椭圆
M5.Display.fillEllipse(x0, y0, x1, y1, color) # 实心椭圆
M5.Display.drawEllipseArc(x0, y0, x1, y1, start, end, color)
M5.Display.fillEllipseArc(x0, y0, x1, y1, start, end, color)start,end: 角度(度)
M5.Display.drawTriangle(x0, y0, x1, y1, x2, y2, color)
M5.Display.fillTriangle(x0, y0, x1, y1, x2, y2, color)M5.Display.drawArc(x, y, r, start_angle, end_angle, color)
M5.Display.fillArc(x, y, r, start_angle, end_angle, color)M5.Display.drawQR(text, x, y, size, color)M5.Display.drawString(text, x, y) # 左对齐
M5.Display.drawCenterString(text, x, y) # 居中
M5.Display.drawRightString(text, x, y) # 右对齐- 对齐以
(x, y)为参考点
M5.Display.setTextColor(fg_color) # 仅前景色
M5.Display.setTextColor(fg_color, bg_color) # 前景+背景(不透明)
M5.Display.setTextSize(size) # 缩放倍数 (1=原生大小)
M5.Display.setFont(font) # 设置字体(内置或自定义)
M5.Display.setCursor(x, y) # 设置光标位置
M5.Display.getCursor() → (x, y) # 获取光标
# .vlw 字体加载
M5.Display.loadFont(path) # 从文件加载 .vlw 字体
M5.Display.setFont(path) # 另一种设置方式
M5.Display.unloadFont() # 卸载当前字体关于 textSize: textSize(1) 对应 .vlw 字体的原生像素大小(例如 HarmonyOS_Sans_SC_Regular_28.vlw 就是 28px)。如果有 _28.vlw,默认应该用 textSize(1) 而非 2。
M5.Display.textWidth(text) → int # 文字像素宽度
M5.Display.fontHeight() → int # 当前字体高度M5.Display.print(text) # 在光标位置输出
M5.Display.printf(format, *args) # 格式化输出
M5.Display.setTextScroll(enable) # 文字滚动模式M5.Display.drawPng(filename, x, y)M5.Display.drawJpg(filename, x, y)M5.Display.drawBmp(filename, x, y)M5.Display.drawImage(data, x, y, w, h) # 像素数据
M5.Display.drawRawBuf(x, y, data, w, h) # 原始缓冲区canvas = M5.Display.newCanvas(width, height) # 创建离屏画布
# 在画布上绘图(与 Display 方法相同)
canvas.fillRect(0, 0, 100, 50, 0x000000)
canvas.drawString("Sprite", 10, 10)
# 渲染到屏幕
M5.Display.drawImage(canvas, x, y, w, h)适用场景: 预先渲染复杂图形,减少屏幕刷新次数,提升动画流畅度。
M5.Display.readData(x, y) → bytes
M5.Display.readData16(x, y) → int # RGB565 格式 (0~65535)
M5.Display.readData32(x, y) → int # 32-bit 格式
M5.Display.writeData(data) # 写入原始数据
M5.Display.writeData16(value) # 写入16位颜色
M5.Display.writeData32(value) # 写入32位颜色M5.Display.writeCommand(cmd) # 发送命令字节
M5.Display.writeCommand16(cmd) # 发送16位命令普通应用不需要直接使用,用于访问显示控制器的特殊功能。
基于 GT911 电容触摸控制器。触摸数据通过
M5.Touch读取,在M5.update()中自动轮询。
返回当前触摸点数(0 = 无触摸,≥1 = 有触摸)。
获取最新触摸点的 X 坐标。
获取最新触摸点的 Y 坐标。
获取指定触摸点的详细状态。detail 值的含义:
| 值 | 含义 | 说明 |
|---|---|---|
| 0 | 按下 | 手指首次触摸屏幕 |
| 1 | 释放 | 手指离开屏幕 |
| 2 | 长按 | 持续按住超过阈值时间 |
注意: getDetail(0) 返回的 detail 值的[9]位用于判断 isHolding(UIFlow framework 中实现)。
获取原始触摸坐标(可能未经校准)。
def handle_touch():
if M5.Touch.getCount() == 0:
return
x = M5.Touch.getX()
y = M5.Touch.getY()
detail = M5.Touch.getDetail(0)
if detail == 0: # 按下
print(f"Touch down at ({x}, {y})")
elif detail == 1: # 释放
print(f"Touch up at ({x}, {y})")
elif detail == 2: # 长按
print(f"Long press at ({x}, {y})")PaperS3 只有一个物理按键(电源/复位/下载),没有独立的 A/B/C 按键。
M5.BtnA.isPressed() → bool
M5.BtnA.isReleased() → bool
M5.BtnA.isHolding() → bool
M5.BtnA.wasClicked() → bool
M5.BtnA.wasPressed() → bool
M5.BtnA.wasReleased() → bool
M5.BtnA.wasHold() → bool
M5.BtnA.wasSingleClicked() → bool
M5.BtnA.wasChangePressed() → bool
M5.BtnA.lastChange() → int # 毫秒时间戳实际: PaperS3 上的物理按键主要用于电源/固件下载模式切换。UIFlow 启动框架中通过触摸侧边栏操作,不依赖物理按键。
PaperS3 没有传统电源管理芯片 (AXP192/AXP2101),电池管理由 PMS150G + LGS4056H 独立处理。
M5.Power.getBatteryLevel() → int # 电池电量百分比 (0~100)
M5.Power.getBatteryVoltage() → float # 电池电压 (mV)
M5.Power.getBatteryCurrent() → float # 电池电流 (mA)
M5.Power.isCharging() → bool # 是否正在充电M5.Power.powerOff() # 关机
M5.Power.deepSleep() # 深度休眠
M5.Power.lightSleep() # 浅度休眠
M5.Power.timerSleep(ms) # 定时休眠M5.Power.setBatteryCharge(enable) # 启用/禁用充电
M5.Power.setChargeCurrent(ma) # 设置充电电流 (mA)
M5.Power.setChargeVoltage(mv) # 设置充电电压 (mV)M5.Power.setExtOutput(enable) # 外部引脚输出
M5.Power.getExtOutput() → bool
M5.Power.setUsbOutput(enable) # USB 输出
M5.Power.getUsbOutput() → boolM5.Power.setLed(brightness) # 设置 LED 亮度 (0~255)
M5.Power.setVibration(enable) # 振动马达 (如支持)M5.Power.getKeyState() → int # 按键状态
M5.Power.getType() → int # 电源芯片类型
M5.Power.getPortVbus() → int # VBUS 电压
M5.Power.getPortCurrent() → int # 端口电流BMI270 6-axis IMU (加速度计 + 陀螺仪),I2C 地址 0x68。
M5.Imu.getAccel() → (x, y, z) # 加速度 (m/s²)
M5.Imu.getGyro() → (x, y, z) # 角速度 (°/s)
M5.Imu.getMag() → (x, y, z) # 地磁 (如支持)
M5.Imu.isEnabled() → bool # 是否已启用
M5.Imu.getType() → int # IMU 芯片型号accel = M5.Imu.getAccel()
print(f"Accel: x={accel[0]:.2f}, y={accel[1]:.2f}, z={accel[2]:.2f}")PaperS3 的无源蜂鸣器使用 PWM 驱动 (GPIO21)。
M5.Spk.begin() # 初始化
M5.Spk.end() # 关闭
M5.Spk.config(sample_rate, stereo, ...) # 配置
M5.Spk.isRunning() → bool
M5.Spk.isEnabled() → bool
M5.Spk.isPlaying() → bool
M5.Spk.getPlayingChannels() → int
M5.Spk.stop() # 停止播放M5.Spk.setVolume(volume) # 0~255
M5.Spk.getVolume() → int
M5.Spk.getVolumePercentage() → float # 0.0~1.0
M5.Spk.setAllChannelVolume(volume) # 所有通道音量
M5.Spk.setChannelVolume(ch, volume) # 指定通道
M5.Spk.getChannelVolume(ch) → intM5.Spk.tone(frequency, duration_ms) # 单音
M5.Spk.playRaw(data, len, ...) # 播放原始 PCM 数据
M5.Spk.playWav(data, ...) # 播放 WAV 数据(内存)
M5.Spk.playWavFile(path) # 播放 WAV 文件M5.Spk.tone(440, 200) # 播放 A4 音 200ms
time.sleep_ms(100)
M5.Spk.tone(880, 200) # 播放 A5 音 200ms取决于固件是否启用。PaperS3 硬件上无内置麦克风,此模块仅供兼容。
M5.Mic.config(sample_rate, ...) # 配置
M5.Mic.begin() # 开始录音
M5.Mic.end() # 停止
M5.Mic.isRunning() → bool
M5.Mic.isEnabled() → boolM5.getBoard() # 返回 board ID
M5.Board.M5PaperS3 # = 19 (常量)board = M5.getBoard()
if board == M5.Board.M5PaperS3:
print("Running on M5PaperS3!")
# 或直接:
import M5
board_id = M5.Board.M5PaperS3 # 值为 19from startup import startup
startup(boot_opt=2, timeout=60) # 2 = BOOT_OPT_MENU_NETPaperS3 使用 ESP32-S3 内置 WiFi (2.4GHz, 802.11 b/g/n)。
import network
wlan = network.WLAN(network.STA_IF) # Station 模式
wlan.active(True)
wlan.connect("SSID", "password")
while not wlan.isconnected():
pass
print("IP:", wlan.ifconfig()[0])UIFlow 有内建的 Startup 类封装网络连接:
from startup import Startup
wlan = Startup()
wlan.connect_network(ssid, pswd, timeout=60)SPI 模式,20MHz,引脚 GPIO47/39/38/40。
import machine, os
sd = machine.SDCard(
slot=3, # SPI 模式
sck=39, miso=40,
mosi=38, cs=47,
freq=20000000
)
os.mount(sd, "/sd") # 挂载到 /sd
print(os.listdir("/sd")) # 列出文件
os.umount("/sd") # 卸载注意: UIFlow 的 machine.SDCard 使用自定义实现,slot=3 表示 SPI 模式,slot=1 表示 SDMMC 1-bit 模式。
UIFlow 固件内置的启动应用框架,使用
papers3/目录下的模块。
M5.begin()
→ startup() 调度到 PaperS3_Startup
→ WiFi 连接
→ 显示 startup.png 启动画面
→ Framework 启动:
- StatusBarApp (状态栏: WiFi/云连接)
- DevApp (开发者信息)
- SettingsApp (WiFi 配置)
- ListApp (文件浏览器/启动器)
install → launch → view → ready (async run loop) → hide → exit → uninstall
| 阶段 | 回调 | 说明 |
|---|---|---|
| 安装 | on_install() |
设置触摸热区描述符 |
| 启动 | on_launch() |
初始化数据 |
| 显示 | on_view() |
绘制界面 |
| 就绪 | on_ready() |
创建 async task |
| 运行 | on_run() |
async 循环 (每 500ms) |
| 隐藏 | on_hide() |
取消 async task |
from modules.startup.papers3 import framework
fw = framework.Framework()
fw.install(app) # 安装应用
fw.install_bar(app) # 安装状态栏
fw.install_launcher(app) # 安装启动器
fw.start() # 启动框架主循环触摸分发: Framework 检测触摸 → 判断是否在应用热区内 → 切换到对应应用 → 派发 _click_event_handler(x, y, fw)。
触摸消抖: 150ms。
键盘支持: CardKB (I2C 0x5F) 左右键切换应用。
import M5
import time
def setup():
M5.begin()
M5.Display.startWrite()
# 清屏
M5.Display.fillScreen(0xFFFFFF)
# 绘制矩形
M5.Display.fillRect(20, 20, 200, 100, 0x000000)
M5.Display.drawRect(20, 140, 200, 100, 0x000000)
# 圆角矩形
M5.Display.fillRoundRect(260, 20, 200, 100, 20, 0x333333)
M5.Display.drawRoundRect(260, 140, 200, 100, 20, 0x000000)
# 圆形
M5.Display.fillCircle(120, 340, 60, 0x000000)
M5.Display.drawCircle(120, 380, 60, 0x666666)
# 文字
M5.Display.setTextColor(0x000000)
M5.Display.setTextSize(2)
M5.Display.drawString("Hello PaperS3!", 100, 480)
# 线条
M5.Display.drawLine(0, 530, 540, 530, 0x000000)
M5.Display.endWrite()
def loop():
M5.update()
time.sleep_ms(100)import M5
# 设置快速刷新(开发调试用)
M5.Display.setEpdMode(4) # EPD_FASTEST
# 检查是否为 EPD 屏
if M5.Display.isEPD():
print("EPD display detected")
print(f"Current mode: {M5.Display.getEpdMode()}")import M5
import time
def setup():
M5.begin()
M5.Display.setEpdMode(4)
def loop():
M5.update()
if M5.Touch.getCount() > 0:
x = M5.Touch.getX()
y = M5.Touch.getY()
detail = M5.Touch.getDetail(0)
if detail == 0: # 按下
# 蜂鸣器反馈
M5.Spk.tone(880, 50)
# 绘制触摸点
M5.Display.fillCircle(x, y, 10, 0x000000)
time.sleep_ms(20)import machine
import os
import network
# 挂载 SD 卡
sd = machine.SDCard(slot=3, sck=39, miso=40,
mosi=38, cs=47, freq=20000000)
os.mount(sd, "/sd")
# 读取文件
with open("/sd/config.txt", "r") as f:
print(f.read())
# 连接 WiFi
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect("SSID", "password")文档生成: 基于 m5stack/uiflow-micropython 源码中 cmodules/m5unified/ 和 components/M5Unified/ 的 C 绑定代码, 以及 M5GFX 库源码分析。最后更新: 2026-05-23