FastBond4挑战部分-基于M5StickC PLUS2的手腕运动状态检测控制器
该项目使用了M5Stack M5StickC PLUS2智能手表开发套件(K016-H2),实现了手腕运动状态检测控制器的设计,它的主要功能为:姿态检测:通过六轴IMU传感器(MPU6886)实时检测手腕倾斜角度;状态识别:识别前进、后退、左转、右转、停止五种手势状态;实时显示:在1.14寸TFT屏幕上实时显示当前状态、方向箭头和传感器数据。
标签
IMU
M5StickC
手势控制
FastBond4
智能穿戴
林守护
更新2026-07-06
6

一、所选主题和项目介绍

1.1 项目背景

在智能穿戴和物联网应用场景中,手势控制是一种直观、自然的交互方式。传统的遥控器需要双手操作,不够便捷。本项目使用M5Stack M5StickC PLUS2智能手表开发套件(K016-H2),实现一款手腕运动状态检测控制器,通过检测手腕的倾斜姿态来识别运动意图,为后续遥控小车控制提供手势输入接口,实现单手便捷控制。

1.2 项目目标

本项目使用M5Stack M5StickC PLUS2 (K016-H2) 智能手表套装(含表带配件),实现一款手腕运动状态检测控制器,主要功能如下:

  1. 姿态检测:通过六轴IMU传感器(MPU6886)实时检测手腕倾斜角度
  2. 状态识别:识别前进、后退、左转、右转、停止五种手势状态
  3. 实时显示:在1.14寸TFT屏幕上实时显示当前状态、方向箭头和传感器数据
  4. 方向指示:使用三角形箭头图形直观指示当前运动方向
  5. 命令输出:为后续遥控小车控制提供运动命令接口
  6. 可穿戴设计:配合表带配件,实现便携式可穿戴控制

1.3 创新点

  1. 手势控制:通过手腕姿态实现直观的运动控制,无需按键操作
  2. 实时反馈:屏幕实时显示姿态角度、运动状态和方向箭头
  3. 可调节阈值:支持调节检测灵敏度,适应不同用户习惯
  4. 方向指示器:使用三角形箭头图形,直观显示当前运动方向
  5. 模块化设计:代码采用类封装,便于扩展到其他控制场景
  6. 可穿戴形态:配合表带配件,实现真正的可穿戴控制器

二、硬件介绍

2.1 主控设备

M5StickC PLUS2 (SKU: K016-H2)

M5StickC PLUS2是M5StickC Plus的迭代版本,采用ESP32-PICO-V3-02芯片作为主控。在小巧的机身内部,集成了丰富多样的硬件资源,例如红外、RTC、麦克风、LED、IMU、按键、蜂鸣器等。它采用由ST7789V2驱动的1.14寸TFT屏幕,分辨率为135×240。电池容量增加到了200mAh,接口方面同样支持HAT与Unit系列产品。

核心参数

参数

规格

型号

K016-H2 (M5StickC PLUS2 Watch Kit)

主控芯片

ESP32-PICO-V3-02

CPU

双核 Xtensa LX7,240MHz

Flash

8MB

PSRAM

2MB

Wi-Fi

2.4 GHz Wi-Fi

USB

USB Type-C

人机交互

参数

规格

显示屏

ST7789V2,1.14英寸,135×240像素

按键

自定义按键×3 (A/B/C)

LED

绿色LED×1 (休眠指示),红色LED×1 (与红外共用)

蜂鸣器

板载无源蜂鸣器

麦克风

MEMS麦克风 SPM1423

传感器

参数

规格

IMU

MPU6886 (六轴姿态传感器)

RTC

BM8563 (实时时钟)

扩展接口

参数

规格

Grove接口

1× HY2.0-4P (I2C+I/O+UART)

外接引脚

G0,G25/G26,G36,G32,G33

电源参数

参数

规格

电池

200mAh@3.7V

输入电压

5V@500mA

工作温度

0~40°C

物理参数

参数

规格

产品尺寸

48.0 × 24.0 × 13.5 mm

产品重量

16.7 g

2.2 手表配件

M5StickC Plus2 Watch Kit (SKU: K016-H2)

手表套装包含M5StickC PLUS2开发板和一系列表带配件,可将设备佩戴在手腕上,实现可穿戴应用。

配件

数量

StickC-Plus2

1

LEGO适配件

1

Wall/1515

1

表带

1

Type-C USB(50cm)

1

2.3 IMU传感器

MPU6886 六轴姿态传感器

参数

规格

传感器类型

3轴加速度计 + 3轴陀螺仪

通信接口

I2C

I2C地址

0x68

加速度量程

±2g / ±4g / ±8g / ±16g

陀螺仪量程

±250 / ±500 / ±1000 / ±2000 °/s

分辨率

16位ADC

2.4 硬件框图

硬件框图

三、方案框图和项目设计思路

3.1 系统架构

系统架构

3.2 手腕姿态检测原理

通过加速度计数据计算手腕倾斜角度:

  1. Pitch角 (俯仰角):前后倾斜角度
    • 正值:手腕向上抬起(手背向上) → 后退 (BACKWARD)
    • 负值:手腕向下压(手心向下) → 前进 (FORWARD)
  2. Roll角 (横滚角):左右倾斜角度
    • 正值:手腕向右倾斜 → 右转 (RIGHT)
    • 负值:手腕向左倾斜 → 左转 (LEFT)

注意:由于设备佩戴在右手上,且屏幕朝外,当手腕自然水平时屏幕几乎朝天。因此抬腕(Pitch增大)对应的是手腕向后翻转,映射为后退;压腕(Pitch减小)对应手腕向前翻转,映射为前进。这与左手佩戴或桌面放置时的方向定义相反。

  1. 角度计算公式
Pitch = atan2(ay, sqrt(ax² + az²))
Roll = atan2(-ax, az)

3.3 状态检测流程

状态检测流程

3.4 运动控制映射

手腕姿态

Pitch角度

Roll角度

控制命令

小车动作

向下压

< -15°

-

FORWARD

前进

向上抬起

> 15°

-

BACKWARD

后退

向左倾斜

-

< -15°

LEFT

左转

向右倾斜

-

> 15°

RIGHT

右转

水平

±15°内

±15°内

STOP

停止

四、原理图和PCB展示及介绍

4.1 原理图展示

I2C适配板原理图

4.2 PCB布局展示

PCB

设计过程

电路设计采用KiCad完成,包括原理图设计和PCB布线两个环节。在具体实现上,在指导老师的帮助下完成了电路设计的审阅和优化,方便用于增加外设控制。

五、关键代码介绍

5.1 IMU数据读取API

MPU6886通过内部I2C接口连接。在UiFlow2 MicroPython固件中,IMU通过全局Imu对象访问,无需手动初始化I2C:

# 通过全局Imu对象读取6轴数据
accel = Imu.getAccel() # 返回 (ax, ay, az),单位G
gyro = Imu.getGyro() # 返回 (gx, gy, gz),单位°/s

注意Imu是全局单例对象,由from M5 import *自动导入,不需要创建实例。不支持单轴读取方法,必须通过getAccel()一次读取三轴数据。

5.2 主程序结构

"""
文件名: wrist_motion_controller.py
功能: 手腕状态检测与显示控制器
硬件: M5StickC PLUS2 (K016-H2)
用途: 检测手腕姿态运动,用于控制遥控小车前进后退
"""

5.2.1 import导入

import M5
from M5 import *
import time
import math

关键点:

  • from M5 import * 会导入全局Imu对象(IMU传感器)和Widgets(屏幕控件)、Lcd(底层绘图)等模块
  • 不需要额外导入hardware模块或手动初始化I2C

5.3 手腕运动检测类

class WristMotionDetector:
"""
手腕运动检测器类

基于MPU6886六轴传感器检测手腕姿态:
- Pitch > +15: 抬腕 -> BACKWARD (后退)
- Pitch < -15: 压腕 -> FORWARD (前进)
- Roll > +15: 右倾 -> RIGHT (右转)
- Roll < -15: 左倾 -> LEFT (左转)
- 其他: STOP (停止)
"""

def __init__(self):
self.ax = 0.0
self.ay = 0.0
self.az = 0.0
self.pitch = 0.0
self.roll = 0.0
self.state = "STOP"
self.threshold_pitch = 15.0
self.threshold_roll = 15.0

def read_imu(self):
"""通过全局Imu对象读取6轴数据"""
accel = Imu.getAccel() # 返回三轴加速度
gyro = Imu.getGyro() # 返回三轴角速度
self.ax, self.ay, self.az = accel
self.gx, self.gy, self.gz = gyro
return (self.ax, self.ay, self.az,
self.gx, self.gy, self.gz)

def calculate_angles(self):
"""计算俯仰角(pitch)和横滚角(roll)"""
self.read_imu()
ax, ay, az = self.ax, self.ay, self.az
norm = math.sqrt(ax*ax + ay*ay + az*az)
if norm > 0:
ax, ay, az = ax/norm, ay/norm, az/norm
self.pitch = math.degrees(
math.atan2(ay, math.sqrt(ax*ax + az*az)))
self.roll = math.degrees(
math.atan2(-ax, az))
return (self.pitch, self.roll)

def detect_state(self):
"""检测手腕状态(右手佩戴,抬腕=后退)"""
self.calculate_angles()
if self.pitch > self.threshold_pitch:
self.state = "BACKWARD" # 抬腕 = 后退
elif self.pitch < -self.threshold_pitch:
self.state = "FORWARD" # 压腕 = 前进
elif self.roll > self.threshold_roll:
self.state = "RIGHT"
elif self.roll < -self.threshold_roll:
self.state = "LEFT"
else:
self.state = "STOP"
return self.state

def get_motion_command(self):
"""将手腕状态转换为电机控制命令"""
self.detect_state()
commands = {}
commands["FORWARD"] = {"motor_left": 100, "motor_right": 100, "direction": "Forward"}
commands["BACKWARD"] = {"motor_left": -100, "motor_right": -100, "direction": "Backward"}
commands["LEFT"] = {"motor_left": -50, "motor_right": 50, "direction": "Left"}
commands["RIGHT"] = {"motor_left": 50, "motor_right": -50, "direction": "Right"}
commands["STOP"] = {"motor_left": 0, "motor_right": 0, "direction": "Stop"}
return commands.get(self.state, commands["STOP"])

IMU API说明

  • Imu是UiFlow2固件提供的全局单例对象,由from M5 import *自动创建
  • Imu.getAccel() 返回三元组(ax, ay, az),单位G
  • Imu.getGyro() 返回三元组(gx, gy, gz),单位度/秒
  • 不支持和 Imu.getAccX() 等单轴方法,也不支持构造 IMU()M5.IMU 访问
  • 数据无需转换,可直接用于角度计算

方向说明:设备戴在右手上、屏幕朝外时,抬腕(手背向上)对应Pitch正值,此时手腕实际姿势是向后翻转,因此映射为BACKWARD(后退);压腕(手心向下)映射为FORWARD(前进)。

5.4 显示管理类

class DisplayManager:
"""
显示管理器类

管理M5StickC PLUS2TFT屏幕(135x240竖屏)显示内容
- 状态文字 + 方向箭头 + 传感器数据
"""

def __init__(self):
Widgets.fillScreen(0x000000)
# 标题 (DejaVu12, 白色)
self.title_label = Widgets.Label("Wrist Ctrl", 35, 3, 1.0,
0xFFFFFF, 0x000000, Widgets.FONTS.DejaVu12)
# 状态 (DejaVu18, 黄色)
self.state_label = Widgets.Label("STOP", 5, 26, 1.0,
0xFFFF00, 0x000000, Widgets.FONTS.DejaVu18)
# 传感器数据 (DejaVu12)
self.pitch_label = Widgets.Label("Pitch: 0.0", 5, 74, 1.0,
0xFFFF00, 0x000000, Widgets.FONTS.DejaVu12)
self.roll_label = Widgets.Label("Roll: 0.0", 5, 92, 1.0,
0xFFFF00, 0x000000, Widgets.FONTS.DejaVu12)
self.ax_label = Widgets.Label("AX: 0.00", 5, 116, 1.0,
0x00FFFF, 0x000000, Widgets.FONTS.DejaVu12)
self.ay_label = Widgets.Label("AY: 0.00", 5, 134, 1.0,
0x00FFFF, 0x000000, Widgets.FONTS.DejaVu12)
self.az_label = Widgets.Label("AZ: 0.00", 5, 152, 1.0,
0x00FFFF, 0x000000, Widgets.FONTS.DejaVu12)
self.last_direction = "STOP"

def _draw_arrow(self, cx, cy, direction, color):
"""在指定位置绘制方向箭头"""
s = 12 # 三角形半边长
if direction == "FORWARD":
# 向上三角形
Lcd.fillTriangle(cx, cy-s, cx-s, cy+s, cx+s, cy+s, color)
elif direction == "BACKWARD":
# 向下三角形
Lcd.fillTriangle(cx, cy+s, cx-s, cy-s, cx+s, cy-s, color)
elif direction == "LEFT":
# 向左三角形
Lcd.fillTriangle(cx-s, cy, cx+s, cy-s, cx+s, cy+s, color)
elif direction == "RIGHT":
# 向右三角形
Lcd.fillTriangle(cx+s, cy, cx-s, cy-s, cx-s, cy+s, color)
else: # STOP
Lcd.fillRect(cx-s, cy-s, s*2, s*2, color)

def update_display(self, detector):
"""更新显示内容和方向箭头"""
state = detector.state
# 各状态对应颜色: 绿色/红色/蓝色/紫色/黄色
state_colors = {}
state_colors["FORWARD"] = 0x00FF00
state_colors["BACKWARD"] = 0xFF0000
state_colors["LEFT"] = 0x0000FF
state_colors["RIGHT"] = 0xFF00FF
state_colors["STOP"] = 0xFFFF00
color = state_colors.get(state, 0xFFFFFF)

self.state_label.setColor(color, 0x000000)
self.state_label.setText(state)
self.pitch_label.setText("Pitch: {:.1f}".format(detector.pitch))
self.roll_label.setText("Roll: {:.1f}".format(detector.roll))
self.ax_label.setText("AX: {:.2f}".format(detector.ax))
self.ay_label.setText("AY: {:.2f}".format(detector.ay))
self.az_label.setText("AZ: {:.2f}".format(detector.az))

# 方向变化时清除旧箭头并绘制新箭头
if state != self.last_direction:
clear_x = 56 # 箭头区域居中
clear_y = 46 # 状态文字正下方
clear_size = 24
Lcd.fillRect(clear_x, clear_y, clear_size, clear_size, 0x000000)
arrow_cx = clear_x + clear_size // 2
arrow_cy = clear_y + clear_size // 2
self._draw_arrow(arrow_cx, arrow_cy, state, color)
self.last_direction = state

显示API说明

  • Widgets.Label() 是文本标签控件,构造参数为:(文字, x, y, 缩放, 前景色, 背景色, 字体)
  • Widgets.FONTS.DejaVu12DejaVu18 是可用字体,不支持中文字符
  • Lcd.fillTriangle() 用于绘制方向箭头三角形
  • 不支持 M5TextBoxM5.Lcd.FONT_DejaVu 等旧API
  • 屏幕分辨率为135x240竖屏,需使用 Widgets.setRotation(0) 设置正常方向

5.5 主循环逻辑

def main():
"""主程序入口"""
M5.begin()
Widgets.setRotation(0) # 竖屏正常方向

print("=" * 40)
print("Wrist Motion Controller")
print("M5StickC PLUS2 (K016-H2)")
print("=" * 40)

# 初始化IMU
try:
accel = Imu.getAccel()
gyro = Imu.getGyro()
print("[OK] IMU init success")
except Exception as e:
print("[ERROR] IMU init failed:", e)
return

detector = WristMotionDetector()
display = DisplayManager()

last_state = ""
update_counter = 0
paused = False

while True:
M5.update()

# 按键A: 暂停/恢复检测
if M5.BtnA.wasPressed():
paused = not paused
time.sleep_ms(200)

if paused:
time.sleep_ms(100)
continue

# 检测手腕状态
current_state = detector.detect_state()

# 每5帧更新一次显示(减少刷新频率)
if update_counter % 5 == 0:
display.update_display(detector)

# 状态变化时输出命令
if current_state != last_state:
cmd = detector.get_motion_command()
print("[STATE] {} -> {}".format(last_state, current_state))
last_state = current_state

update_counter += 1
time.sleep_ms(50)

主循环说明

  • 使用 update_counter % 5 控制屏幕刷新频率为10Hz(主循环50ms×5=250ms)
  • BtnA实现暂停/恢复功能,而非切换阈值
  • M5.update() 必须在每次循环中调用,用于处理按键事件和系统任务

六、硬件功能展示图及说明

6.1 显示界面

6.1.1 显示布局

屏幕为1.14寸ST7789V2竖屏显示,分辨率135x240像素。使用Widgets.setRotation(0)设置正常方向。实物展示硬件实物图实物展示2

6.1.2 各状态显示

状态

颜色

手势

小车动作

箭头方向

FORWARD

绿色

手腕下压

前进

向上

BACKWARD

红色

手腕上抬

后退

向下

LEFT

蓝色

手腕左倾

左转

向左

RIGHT

紫色

手腕右倾

右转

向右

STOP

黄色

手腕水平

停止

方形

6.2 系统运行日志

========================================
Wrist Motion Controller
M5StickC PLUS2 (K016-H2)
========================================
[OK] IMU init success
[OK] Accel: 0.02, -0.01, 1.01
[OK] Gyro: 0.00, 0.00, 0.00
[OK] Starting main loop...
----------------------------------------
[STATE] STOP -> BACKWARD
[CMD] Direction: Backward, Motors: L:-100 R:-100
[STATE] BACKWARD -> STOP
[CMD] Direction: Stop, Motors: L:0 R:0
[STATE] STOP -> FORWARD
[CMD] Direction: Forward, Motors: L:100 R:100
[PAUSE] Detection paused
[RESUME] Detection resumed

七、设计中遇到的难题和解决方法

7.1 IMU API差异(全局单例 vs 构造函数)

问题描述:初次编写代码时使用了from hardware import IMUM5.IMU.getAccX()方式访问IMU,导致运行时报错AttributeErrorImportError

解决方案: UiFlow2 MicroPython固件中,IMU通过from M5 import *导入的全局单例Imu对象访问,而非通过构造函数创建。正确的API为:

from M5 import *
accel = Imu.getAccel() # 返回 (ax, ay, az)
gyro = Imu.getGyro() # 返回 (gx, gy, gz)

不支持Imu.getAccX()单轴方法或IMU()构造函数。

7.2 字典字面量初始化导致SyntaxError

问题描述:直接使用字典字面量state_colors = {"FORWARD": 0x00FF00, ...}会在设备上报错SyntaxError

根本原因:某些版本的MicroPython对字典字面量语法解析有限制,特别是混合使用字符串键和非数字值时。

解决方案:改用空字典逐步赋值:

state_colors = {}
state_colors["FORWARD"] = 0x00FF00
state_colors["BACKWARD"] = 0xFF0000
# ...

同理,commands字典也使用相同的增量方式构建。

7.3 屏幕旋转方向校准

问题描述:屏幕初始显示方向错误,内容上下颠倒、左右反置。

解决方案: 使用Widgets.setRotation(n)进行校准,其中n为:

  • 0:正常方向(USB口朝下,内容可读)
  • 2:180度翻转

由于M5StickC戴在右手上且屏幕朝外,最佳方向为setRotation(0)(竖屏模式)。经过多次测试确定该方向在手腕自然水平时显示内容可正常阅读。

7.4 前后方向映射反转(右手佩戴)

问题描述:初始逻辑将Pitch正值(抬腕)映射为"前进",但实际测试时抬腕感觉更像"后退"动作。

解决方案: 由于M5StickC戴在右手上、屏幕朝外:

  • 抬腕(Pitch > 15度)= 手腕向后翻转 = BACKWARD(后退)
  • 压腕(Pitch < -15度)= 手腕向前翻转 = FORWARD(前进)

这与桌面放置或左手佩戴时的方向感知相反,需要根据实际佩戴习惯调整映射。

7.5 方向箭头被状态文字遮挡

问题描述:在屏幕右侧绘制的方向箭头与状态文字"BACKWARD"/"FORWARD"在垂直方向重叠,导致箭头不可见或被文字覆盖。

解决方案

  1. 将箭头从屏幕右侧(x=106)移至状态文字正下方并居中(x=56,y=46)
  2. 缩小箭头三角形尺寸(半边长s从14减小到12)
  3. 传感器数据标签整体下移4px,避免与箭头区域重叠
  4. 最终布局:状态文字 y=26,箭头 y=46,Pitch y=74,依次间隔4px

八、心得体会

8.1 项目收获

  1. IMU传感器应用:深入理解了六轴传感器的工作原理和姿态解算方法,掌握了通过加速度计计算Pitch/Roll角度的公式和归一化处理
  2. UiFlow2 MicroPython开发:掌握了ESP32平台上MicroPython固件的开发流程,包括设备连接、代码上传、串口调试等
  3. 显示API适配:实际验证了UiFlow2固件的Widgets模块和Lcd绘图API,了解了与官方文档示例的差异
  4. 可穿戴方向映射:实践了右手佩戴场景下的方向映射逻辑(抬腕=后退、压腕=前进),理解了坐标系变换的重要性
  5. MicroPython陷阱排查:积累了MicroPython语法限制(字典字面量、UTF-8编码)的排查经验
  6. 迭代调试技巧:通过屏幕显示+串口输出双重反馈进行状态调试,大幅缩短了问题定位时间

8.2 技术亮点

  1. 直观交互:通过手腕姿态实现自然的运动控制,无需按键操作
  2. 实时反馈:屏幕实时显示姿态角度、运动状态和方向箭头
  3. 方向指示器:使用三角形箭头图形直观显示当前运动方向
  4. 模块化设计:采用WristMotionDetector(检测)+ DisplayManager(显示)双层架构,职责清晰
  5. 刷新优化:使用计数器控制屏幕刷新频率,降低CPU占用

8.3 改进建议

  1. 无线通信:添加Wi-Fi/蓝牙通信(如UDP或蓝牙SPP),实现真正的遥控功能
  2. 滤波算法:实现互补滤波或卡尔曼滤波,融合加速度计和陀螺仪数据提高角度精度
  3. 手势扩展:增加更多手势识别,如摇动(用于鸣笛或切换模式)、旋转等
  4. 低功耗设计:实现休眠唤醒机制,利用RTC定时唤醒,延长续航时间
  5. 阈值自适应:根据用户手腕抖动幅度自动调整检测阈值,减少误触发
  6. 振动反馈:利用板载蜂鸣器或外接振动马达,提供状态变化触觉反馈
  7. OTA升级:支持通过Wi-Fi远程固件升级
  8. 数据记录:记录运动轨迹到SD卡或通过Wi-Fi上传,用于后续数据分析

创意方向关联

本项目的技术探索为以下创意方向奠定了基础:

1. 人工智能在嵌入式系统中的应用

本项目的IMU姿态检测技术为AI应用提供了基础:

  • 故障保护:人工智能判断:运动状态异常检测可发展为AI驱动的健康预警系统
  • 计算机视觉:IMU数据与视觉融合实现更精确的姿态识别
  • 图像识别:手势识别技术可扩展为基于视觉的手势交互系统

2. 机械臂 / 工业自动化 / 元宇宙

手势控制技术在工业领域有重要应用:

  • 机械臂控制:手势控制可作为机械臂的示教输入方式
  • 工业自动化:手势控制实现无接触设备操作
  • 元宇宙:手势识别是元宇宙人机交互的核心技术

3. 楼宇自动化

手势控制技术在楼宇自动化中有广泛应用:

  • 无接触开关:手势控制灯光、窗帘等设备
  • 智能家电控制:手势控制电视、空调等家电
  • 无障碍设施:为残障人士提供便捷控制方式

技术迁移价值

本项目开发的技术方案可迁移到:

  • 人工智能应用:手势识别、运动健康分析
  • 机械臂/自动化:手势示教、无接触控制
  • 楼宇自动化:智能开关、无障碍控制

致谢

感谢 DigiKey电子森林 提供的FastBond4活动支持,本次活动链接:https://www.eetree.cn/page/digikey-fastbond


附件下载
adapter.epro
adapter.zip
wrist_motion_controller.py
wrist_motion_controller.m5f2
团队介绍
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号