一、所选主题和项目介绍
1.1 项目背景
伴随物联网领域多终端协作场景的普及,设备间无线联动控制的需求日益迫切。传统方案往往依赖专用遥控硬件或繁琐的网络预配置,增加了部署难度和使用门槛。为解决这一问题,本设计选取M5Stack M5Dial V1.1为主控核心,外接Unit-Joystick2霍尔摇杆作为人机交互入口,借助ESP-NOW广播协议实现两台设备间的自动发现与画面联动。使用者只需按压任意一台Joystick2的摇杆轴按键(LEFT-SW-B),两端连接的M5Dial即可同步完成画面切换,形成"一按双响"的无线协同体验。
1.2 项目目标
本方案基于M5Dial V1.1 (K130-V11) 与 Unit-Joystick2 (U024-V2) 构建,核心功能规划如下:
- 摇杆按键输入:通过Joystick2的Z轴数字按键(LEFT-SW-B)驱动画面切换
- 自动无线组网:上电后借助MASTER_PING / SLAVE_JOIN 协议自主协商主从角色
- 跨端同步切换:LOCAL端按下摇杆按钮后本地立即切换画面,同时通过ESP-NOW广播SYNC消息驱动对端同步切换
- LED多态指示:利用Joystick2板载WS2812 RGB灯珠呈现连接状态与画面状态
- 随机延迟避让:引入随机延时机制,从根本上杜绝双设备同时升格为Master的可能性
1.3 创新点
- 纯广播自动配对:基于ESP-NOW广播信道实现Master/Slave自动协商,无需预配任何地址或网络信息
- 霍尔磁感应触发:选用非接触式霍尔电磁摇杆作为交互元件,具备耐磨、高精度、长寿命优势
- WS2812联动反馈:将Joystick2的RGB灯珠与连接进展、画面状态绑定,提供直观的光效指示
- 毫秒级低延迟:利用ESP-NOW广播特性将同步延迟压缩至毫秒量级
- 开箱即配对:双设备上电后自动完成角色协商与连接,使用者无需任何手动干预
二、硬件介绍
2.1 主控设备
主控型号:M5Stack Dial V1.1 (产品编码 K130-V11)
以ESP32-S3为核心驱动的圆形智能交互终端,集成了1.28吋圆形TFT彩屏、电容触摸层、旋转编码器与Grove扩展座,同时具备WiFi与ESP-NOW无线能力。
- 处理器:ESP32-S3FN8,双核240MHz
- 存储:8MB Flash + 2MB PSRAM
- 显示:1.28" 圆形 TFT,分辨率240×240,驱动IC ST7789V2
- 触控:电容式触摸面板
- 扩充:Grove HY2.0-4P (Port.A,I2C)
- 无线:WiFi 802.11 b/g/n + BLE 5.0
- 编码器:20脉冲/圈,内嵌按压开关
2.2 传感器模块
传感单元:M5Stack Unit-Joystick2 (产品编码 U024-V2)
基于STM32G030F6P6的高精度霍尔磁感摇杆,通过I2C总线与主控通信,提供三轴信号(X/Y模拟位移 + Z轴数字按键),并搭载可编程全彩LED。
- 内嵌MCU:STM32G030F6P6 @ Cortex-M0+,64MHz
- 通信方式:I2C,设备地址 0x63
- X/Y输出:16bit ADC 原始值,范围 0~65535
- Z轴按键:LEFT-SW-B,数字电平 0/1
- 指示灯:WS2812C 全彩LED × 1
- 外形尺寸:40.0 × 24.0 × 23.9 mm
2.3 对外接线
侧(M5Dial Port.A) | 侧(Joystick2) | 信号描述 |
|---|---|---|
GND (黑) | GND | 电源回路地 |
5V (红) | 5V | 电源供电正 |
SDA (白) | SDA | I2C 数据线 |
SCL (黄) | SCL | I2C 时钟线 |
2.4 Joystick2管脚分配
STM32引脚 | 用途 | 说明 |
|---|---|---|
PA1 | LEFT-SW-X | X轴模拟量输入 |
PA2 | LEFT-SW-Y | Y轴模拟量输入 |
PA3 | LEFT-SW-B | Z轴数字按键(本方案使用) |
PA4 | RGB-LED | WS2812C 灯珠控制信号 |
三、方案框图和项目设计思路
3.1 系统架构



3.2 设备启动流程

3.3 Master/Slave角色协商协议
整个自动配对过程由三条消息驱动:
- MASTER_PING(Master → 广播域)
Master 端每 2 s 发出一次 PING 声明自身存在;尚未配对的设备收到后自动降为 Slave 并回复 JOIN。 - SLAVE_JOIN(Slave → 广播域)
Slave 端收到 PING 后立即广播 JOIN 回执;Master 收到后建立 peer 表项并以 SYNC 确认。 - SYNC(双向同步触发)
连接建成后,任一端的摇杆按键按下即广播 SYNC;对端收到后执行与本地相同的画面切换动作。
四、原理图和PCB展示及介绍
4.1 原理图展示

4.2 PCB布局展示

4.3 设计过程
使用KiCad EDA工具完成了原理图设计和PCB布局布线。作为电路设计的新手,这次从零开始画原理图、布PCB的经历让我学到了很多。
4.4 硬件接线图
本项目使用自设计的I2C适配板(PCB)连接 M5Dial 主控与 Joystick2 外设模块。适配板通过自定义 PCB 走线将 M5Dial Port.A 接口的 I2C 信号引出至 Joystick2 的对应引脚,实现了稳定的电气连接与信号传输。
M5Dial Port.A 与 Joystick2 之间通过 Grove 线缆进行连接,具体引脚对应关系如下:
- 红线(VCC):M5Dial Port.A 5V 输出 → Joystick2 5V 输入,为模块提供电源;
- 黑线(GND):M5Dial Port.A GND → Joystick2 GND,构成电源回路;
- 黄线(SCL):M5Dial Port.A SCL → Joystick2 SCL,传输 I2C 时钟信号;
- 白线(SDA):M5Dial Port.A SDA → Joystick2 SDA,传输 I2C 数据信号。
Joystick2 内部采用 STM32G030F6P6 作为从机 MCU,I2C 设备地址为 0x63。其管脚功能分配如下:
- PA1:X 轴模拟量输入(LEFT-SW-X),输出 16bit ADC 值;
- PA2:Y 轴模拟量输入(LEFT-SW-Y),输出 16bit ADC 值;
- PA3:Z 轴数字按键(LEFT-SW-B),为本方案使用的画面切换触发源;
- PA4:WS2812C 全彩 LED 控制信号,用于输出连接状态与画面状态指示。
4.5 ESP-NOW无线配对时序
Master 与 Slave 两端的角色协商与同步过程由以下四个步骤组成:
- Master 声明存在(MASTER_PING):Master 端以 2 秒为周期向广播域发送 MASTER_PING 消息,声明自身已就绪。尚未建立配对的设备收到该消息后,自动检测到已有 Master 存在,随即将自身角色降为 Slave。
- Slave 回执加入(SLAVE_JOIN):Slave 端收到 MASTER_PING 后,立即向广播域回复 SLAVE_JOIN 回执消息。Master 端收到该回执后,在本地建立对端 peer 表项,并以 SYNC 消息确认连接建立。
- Master 确认连接(SYNC):Master 端发送 SYNC 消息确认连接已就绪;Slave 端收到后也在本地建立 peer 表项。至此双方 peer 表项均建立完毕,连接正式就绪。
- 对端同步触发(按键 → SYNC):连接建成后,用户按下任意一端的摇杆按键(LEFT-SW-B)。按下端首先在本地切换当前显示画面,同时立即向广播域发送 SYNC 消息。对端收到该消息后执行与本地相同的画面切换动作,从而实现"一按双响"的无线联动效果。
五、软件流程图和关键代码介绍
代码已全部收录于 joystick2_sync.py,以下仅展示核心流程。
5.1 入口与 setup()
开机 → random_delay(0~5s) → M5.begin() → I2C(SCL=1,SDA=2) → Joystick2Unit(0x63) → load_image → M5ESPNow(callback)
def setup():
time.sleep_ms(random.randint(0, 5000))
M5.begin()
Widgets.fillScreen(0x000000)
i2c = I2C(1, scl=Pin(1), sda=Pin(2), freq=100000)
joystick = Joystick2Unit(i2c, address=0x63)
joystick.fill_color(0x000000) # 初始熄灭灯珠
image0 = Widgets.Image("/flash/res/img/crazy-eyes-open.jpg", 0, 0)
now = M5ESPNow(0)
now.set_irq_callback(espnow_recv_callback)
初始化要点:
- 随机休眠 0~5 s — 错开双设备的启动时间窗口
- I2C 总线选择
I2C(1),Pin(1)=SCL、Pin(2)=SDA,适应 M5Dial Port.A 的物理连接 - Joystick2 的 I2C 地址固定为 0x63
- 灯珠默认熄灭,后续会根据连接进度和画面状态动态变色
5.2 组网回调
收到 SLAVE_JOIN → 升 Master,记录 peer,广播 SYNC 确认
收到 MASTER_PING → 降 Slave,记录 peer,广播 JOIN 回执
收到 SYNC → 直接调用 toggle_image()
def espnow_recv_callback(espnow_obj):
try:
mac, data = espnow_obj.recv_data()
msg = data.decode() if isinstance(data, bytes) else str(data)
mac_hex = mac_bytes_to_hex(mac) if isinstance(mac, bytes) else mac
if mac_hex == mac_bytes_to_hex(now.get_mac(0)): # 过滤自包
return
if msg == "SLAVE_JOIN" and peer_mac is None:
peer_mac, peer_id, is_master, connected = mac_hex, 1, True, True
now.set_add_peer(peer_mac, peer_id, 0, False)
now.broadcast_data("SYNC")
elif msg == "MASTER_PING":
if peer_mac is None:
peer_mac, peer_id, is_master, connected = mac_hex, 1, False, True
now.broadcast_data("SLAVE_JOIN")
now.set_add_peer(peer_mac, peer_id, 0, False)
elif msg == "SYNC":
toggle_image()
except Exception as e:
print("espnow error:", e)
三条消息覆盖完整生命周期:发现(PING)→ 注册(JOIN)→ 同步(SYNC)。
5.3 主循环与按键
def loop():
M5.update()
# ── 未连接:转发 PING 或自升 Master ──
if not connected:
t = time.ticks_ms()
if not is_master:
if t >= listen_timeout:
is_master = True
send_ping_time = t + 100
else:
if t >= send_ping_time:
now.broadcast_data("MASTER_PING")
send_ping_time = t + 2000
joystick.fill_color(0x0000FF) # 蓝灯待连
return
# ── 已连接 ──
joystick.fill_color(0x00FF00) # 绿灯就绪
# 上升沿检测
if joystick.get_button_status() and not prev_button:
toggle_image()
send_sync()
prev_button = joystick.get_button_status() # 保留当前值用于下次比对
上升沿判别:只响应 False→True 的跳变,按住不放不会重复触发。
5.4 画面切换
def toggle_image():
current_image_idx = 1 - current_image_idx
if current_image_idx == 0:
image0.setImage("/flash/res/img/crazy-eyes-open.jpg")
joystick.fill_color(0x00FF00) # 睁眼 → 绿
else:
image0.setImage("/flash/res/img/crazy-eyes-close.jpg")
joystick.fill_color(0xFF0000) # 闭眼 → 红
画面与灯色联动:睁眼=绿灯,闭眼=红灯,双端效果完全一致。
六、硬件功能展示图及说明
6.1 屏幕画面

6.2 LED 灯色含义
灯色 | 阶段 | 含义 |
|---|---|---|
熄灭 | 上电初始化 | Joystick2 尚未就绪 |
蓝色 | 搜索中 | 未找到配对设备,持续监听或广播 PING |
绿色 | 已连接 / 睁眼 | 连接正常,当前画面为睁眼 |
红色 | 已连接 / 闭眼 | 连接正常,当前画面为闭眼 |

6.3 典型启动日志
==================================================
M5Dial + Unit-Joystick2 Wireless Sync
==================================================
[Boot] Random delay: 2341 ms
[I2C] Init OK (SCL=1, SDA=2)
[Joystick2] Init OK (I2C addr 0x63)
[ESP-NOW] Callback registered
[Phase1] Waiting for MASTER_PING...
[Recv] MASTER_PING from d0cf130f446c
[Role] Became Slave
[Send] SLAVE_JOIN (broadcast)
[Peer] Added d0cf130f446c
[Recv] SYNC from d0cf130f446c
[Image] Switched to close
==================================================
[Loop] Sync ready — press LEFT-SW-B to toggle




七、设计中遇到的难题和解决方法
7.1 两端同时升 Master
现象:双设备几乎同时上电时可能各自升为 Master,相互只发 PING 不收 JOIN,无法连通。
对策:
- 启动阶段插入 0~5 s 随机
sleep_ms,破坏时间同步性 - 先监听 5 s,期间仅收不发,收不到 PING 才自升 Master
- "先占"原则:先成为 Master 的一方持续广播,等待对方加入
delay = random.randint(0, 5000)
time.sleep_ms(delay)
# 5 秒后仍未收到 PING → 升 Master
if now_t >= listen_timeout:
is_master = True
7.2 广播自收干扰
现象:ESP-NOW 广播模式下本端也会收到自己发出的消息,导致回调误判。
对策:回调入口处用本机 MAC 过滤,自包直接丢弃。
if mac_hex == mac_bytes_to_hex(now.get_mac(0)):
return
7.3 按键机械抖动
现象:LEFT-SW-B 按下/释放瞬间触点抖动,单次按压可能被识别成多次。
对策:
- 上升沿检测:仅记录从
False变为True的那一次 - loop 以 ~20 ms 周期运行,天然具备低通滤波效果
btn = joystick.get_button_status()
if btn and not prev_button:
toggle_image()
send_sync()
prev_button = btn
7.4 I2C 引脚适配
现象:M5Dial 的 Port.A 引脚编号与其它 M5 设备不同,直接套用通用配置会通信失败。
对策:查阅 M5Dial V1.1 原理图,确认 Port.A 对应 SCL=Pin(1)、SDA=Pin(2),并在代码中显式指定。
i2c = I2C(1, scl=Pin(1), sda=Pin(2), freq=100000)
joystick = Joystick2Unit(i2c, address=0x63)
八、心得体会
8.1 项目收获
- ESP-NOW 协议实践:通过实际编码理解了 ESP-NOW 广播的收发模型与回调机制,掌握了基于消息的 Master/Slave 角色协商方法
- I2C 外设驱动:掌握了 Joystick2Unit 的初始化、按钮读取、RGB LED 控制等 API,加深了对 I2C 总线时序的理解
- M5Dial + UiFlow2 开发:熟悉了 M5Dial 的屏幕驱动、图片加载、ESP-NOW 模块的使用流程
- 双端同步设计:从信号产生、本地响应到无线广播、对端执行,完整实践了一条端到端同步链路
- 轻量协议设计:用 PING/JOIN/SYNC 三条消息完成了自动发现、角色协商、数据同步三大功能
8.2 技术亮点
- 零手工配置:上电即自动配对,无需预存任何 MAC 或 SSID
- 霍尔按键输入:非接触式磁感摇杆,杜绝机械磨损隐患
- 灯色联动:WS2812 灯珠与连接状态、画面状态三者绑定一次到位
- 秒级同步:ESP-NOW 广播延迟在毫秒级,人眼几乎无感知
- 双重防冲突:随机延时 + 超时监听两道防线确保角色分配可靠
8.3 改进方向
- 信道加密:对 ESP-NOW 载荷做 AES 加密,防范空中窃听
- 多端组网:将 1 对 1 拓展为 1 对 N,实现多设备同步切换
- X/Y 轴利用:将摇杆的模拟位移数据纳入交互,支持更多操控维度
- 心跳保活:定期心跳检测,断开后自动重连
- 模式切换:长按按键切换同步模式(主从 / 对等 / 独立)
- OTA 远程升级:通过网络推送固件,免去物理刷机
- 低功耗睡眠:无操作时进入深度睡眠,延长续航
- 配对持久化:将已配对设备信息写入 Flash,下次开机直连
- 多图轮播:利用 X/Y 轴选择图片,扩展内容展示能力
创意方向关联
本设计涉及的关键技术可向以下方向延伸:
无线通信方向
ESP-NOW 的低延迟广播机制可用于智能楼宇内的设备联动场景:
- 局域联动:建筑内多终端通过 ESP-NOW 实现状态同步,无需中心网关
- 广播迁移:ESP-NOW 的组播模型可为 5G 物联网的群发指令提供参考
- 高带宽扩展:将 WiFi 作为骨干网承载大批量设备的数据聚合
嵌入式 AI 方向
摇杆 + 无线同步的交互框架可作为 AI 终端的输入/输出管道:
- 人机接口:摇杆用作 AI 设备的指令输入装置
- 协同推理:AI 推理结果通过 ESP-NOW 分发至多个终端同步呈现
- 场景联动:单次触发驱动多个节点协同响应,适合智能家居场景
楼宇自动化方向
双端同步控制是建筑自动化的基础模块:
- 联动控制:一个触发源驱动多处执行器同时动作
- 状态镜像:分布式节点间的状态实时保持一致
- 应急广播:紧急情况下同步触发全部关联设备
技术迁移清单
- 无线层:ESP-NOW 广播 + 自动发现协议
- 输入层:霍尔摇杆按键检测 + 上升沿去抖
- 硬件层:M5Dial + UiFlow2 MicroPython
- 应用层:多设备无线联动控制模式
致谢
感谢 DigiKey 和 电子森林 提供的FastBond4活动支持,本次活动链接:https://www.eetree.cn/page/digikey-fastbond
