一、所选主题和项目介绍
1.1 项目背景
在视力检测场景中,传统视力表测试需要人工记录和判断,操作繁琐且效率低下。在医院体检、学校筛查等批量测试场景中,医护人员需反复更换视标、记录结果,工作强度大且容易出现记录错误。本项目使用M5StickC PLUS2和M5Paper开发板,构建一套智能视力测试系统,通过手腕动作识别实现远程控制、E-Ink大屏清晰显示视标、自动计算测试结果,大幅提升视力检测的效率和准确性。
1.2 项目目标
本项目使用M5StickC PLUS2 (K016-H2) 和 M5Paper 开发板,依据国家标准GB 11533-2011,实现一款智能视力测试系统,主要功能如下:
- 手腕动作控制:通过MPU6886 IMU检测手腕姿态(上下左右),控制E视标开口方向选择
- E-Ink大屏显示:M5Paper 540×960 E-Ink显示屏清晰呈现E视标,墨水屏无闪烁不伤眼
- 自适应阶梯法:从4.3级开始,每级4个视标,≥3正确升级,<3正确降级
- 左右眼独立测试:支持右眼/左眼分别测试,可切换裸眼和矫正视力模式
- ESPNow无线通信:控制器和显示端之间通过ESPNow协议低延迟通信
- 测试历史记录:自动记录每次测试的五分记录和小数记录结果
- 触摸屏操作:M5Paper触摸屏支持直接点击操作,方便独立使用
1.3 创新点
- 手腕动作控制:利用IMU传感器将手腕姿态映射为E视标方向,实现无接触式操作
- 自适应阶梯算法:根据当前表现自动升降级别,快速收敛到真实视力水平
- 双设备协同架构:控制器(M5StickC)和显示器(M5Paper)分离,受测者可独立操作
- 国标合规:严格遵循GB 11533-2011标准,包括视标尺寸、测试距离(2米)和评分系统
- 双模式数据记录:同时输出五分记录(4.0-5.3)和小数记录(0.1-2.0),兼顾专业和通俗
二、硬件介绍
2.1 主控设备
M5StickC PLUS2 (SKU: K016-H2) — 基于ESP32-PICO-V3-02的迷你开发板,配备MPU6886六轴IMU、0.93英寸TFT屏幕、红外发射器,作为手腕控制器。
参数 | 规格 |
|---|---|
主控芯片 | ESP32-PICO-V3-02,双核240MHz |
Flash/PSRAM | 16MB / 8MB |
IMU传感器 | MPU6886 六轴(加速度+陀螺仪) |
屏幕 | 0.93英寸 TFT,135×240 |
按键 | BtnA (G37)、BtnB (G39) |
电池 | 内置110mAh锂电池 |
无线 | WiFi、ESPNow |
M5Paper (E-Ink显示屏) — 基于ESP32的墨水屏开发板,配备540×960电子墨水屏和触摸面板,作为视标显示器。
参数 | 规格 |
|---|---|
主控芯片 | ESP32,双核240MHz |
显示屏 | 540×960 E-Ink墨水屏,PPI 234 |
触摸面板 | 电容式触摸 |
刷新模式 | QUALITY/FAST/FASTEST |
扩展接口 | MicroSD卡槽、Grove |
电池 | 内置1100mAh锂电池 |
2.2 传感器模块
MPU6886六轴IMU传感器(集成在M5StickC PLUS2内)— 包含3轴加速度计和3轴陀螺仪,用于检测手腕姿态:
参数 | 规格 |
|---|---|
加速度计量程 | ±2/±4/±8/±16g |
陀螺仪量程 | ±250/±500/±1000/±2000°/s |
通信接口 | I2C (0x68) |
方向检测阈值 | 俯仰/横滚 ±15° |
防抖时间 | 300ms去抖,400ms保持确认 |
2.3 硬件连接
本项目的硬件连接为无线通信方式,无需物理连线:
M5StickC PLUS2 (控制器) | M5Paper (显示器) | 功能 |
|---|---|---|
ESPNow (WiFi信道1) | ESPNow (WiFi信道1) | 无线数据通信 |
MPU6886 (I2C内嵌) | - | 手腕姿态检测 |
BtnA (G37) | 触摸屏 | 短按=开始测试/确认,长按2秒=换人 |
BtnB (G39) | 触摸屏 | 切换裸眼/矫正视力模式 |
内置电池 | 内置电池 | 无线供电 |
三、方案框图和项目设计思路
3.1 设备启动流程

3.2 核心算法:自适应阶梯法测试流程
采用自适应阶梯法算法,根据被测者的表现自动调整视标级别:
- 起始级别:从4.3级(较大E视标)开始
- 同级别测试:每级随机呈现4个E视标(上下左右方向)
- 升级条件:同一级别正确回答≥3个 → 升级到下一更细级别
- 降级条件:同一级别错误≥2个 → 降级到上一更粗级别
- 终止条件:最低级别(4.0)失败,或最高级别(5.3)通过
- 结果计算:取最后一个正确回答的级别作为最终视力结果
四、原理图和PCB展示及介绍
4.1 原理图展示

原理图
4.2 PCB布局展示

PCB
4.3 设计过程
使用KiCad EDA工具完成了原理图设计和PCB布局布线。关于设计方案,在指导老师的帮助下完成了电路设计的审阅和优化。
4.4 硬件连接说明
本项目使用自设计的I2C适配板(PCB)连接 M5StickC PLUS2 主控与外设传感器,主控与 M5Paper 之间通过 ESPNow 无线通信。

4.5 E视标设计原理
E视标(视力表"E"字)采用5×5网格点阵绘制,根据GB 11533-2011标准:
- 每个视标由5行5列的方形网格组成
- 开口方向由镂空位置决定(上下左右四个方向)
- 笔画宽度决定了视力等级,计算公式:
视角 = 10^(5-V) 角分 - 在2米测试距离下,笔画宽度(mm)= 2000 × tan(视角/60°)
五、软件流程和关键代码介绍
5.1 主程序结构
本项目包含两个独立固件程序:
文件 | 说明 | 部署位置 |
|---|---|---|
| 手腕控制器主程序,IMU姿态检测+ESPNow通信 | M5StickC PLUS2 |
| 视标显示主程序,E视标渲染+测试引擎+ESPNow通信 | M5Paper |
5.2 关键代码 - IMU姿态检测与手腕方向映射
class WristInputDetector:
"""手腕动作检测器"""
def __init__(self):
self.pitch = 0.0 # 俯仰角 (上翻/下翻)
self.roll = 0.0 # 横滚角 (左转/右转)
def calculate_angles(self):
"""从IMU加速度计计算俯仰和横滚角度"""
ax, ay, az = self._read_accel()
norm = math.sqrt(ax*ax + ay*ay + az*az)
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))
def detect_direction(self):
"""映射手腕姿态到E视标方向 (UP/DOWN/LEFT/RIGHT)"""
self.calculate_angles()
if self.pitch < -15: return "UP" # 手掌朝上翻
elif self.pitch > 15: return "DOWN" # 手掌朝下翻
elif self.roll > 15: return "RIGHT" # 手掌向右转
elif self.roll < -15: return "LEFT" # 手掌向左转
return "NONE"
5.3 关键代码 - E视标渲染
# E视标 5x5 网格定义 (1=黑色方块, 0=白色间隙)
E_GRIDS = {
"RIGHT": [
[1, 1, 1, 1, 1],
[1, 0, 0, 0, 0], # 右侧开口
[1, 1, 1, 1, 1],
[1, 0, 0, 0, 0],
[1, 1, 1, 1, 1]
],
"LEFT": [...], "UP": [...], "DOWN": [...]
}
def calculate_stroke_pixels(level_index):
"""
根据视力等级计算E视标笔画宽度(像素)
基于GB 11533-2011: 视角 = 10^(5-V) 角分
"""
vision_5 = VISION_LEVELS[level_index][0]
visual_angle_arcmin = 10 ** (5 - vision_5)
stroke_mm = 2000 * math.tan(math.radians(visual_angle_arcmin / 60))
stroke_pixels = max(1, round(stroke_mm * 9.23))
return stroke_pixels
def draw_e_optotype(cx, cy, direction, stroke_px):
"""在M5Paper E-Ink屏幕上绘制E视标"""
grid = E_GRIDS[direction]
total_size = stroke_px * 5
start_x = cx - total_size // 2
start_y = cy - total_size // 2
for row in range(5):
for col in range(5):
if grid[row][col]:
M5.Lcd.fillRect(
start_x + col*stroke_px, start_y + row*stroke_px,
stroke_px, stroke_px, COLOR_BLACK)
5.4 关键代码 - ESPNow通信协议
控制端(M5StickC PLUS2)和显示端(M5Paper)之间通过ESPNow传输命令和数据:
# ESPNow通信协议:
#
# 控制端 → 显示端:
# VISION_TEST_HOST - 广播信标,用于发现
# CONTROLLER_READY - 控制器就绪确认
# UP/DOWN/LEFT/RIGHT - 手腕方向(开口方向选择)
# EYE_R / EYE_L - 切换左右眼
# MODE_NAKED / MODE_CORRECTED - 切换裸眼/矫正
# NEW_TEST - 换人开始新测试
# START_TEST - 开始测试
#
# 显示端 → 控制端:
# HOST_READY - 主机就绪确认
# STATUS:R,NAKED,5.0 - 当前状态(眼,模式,级别)
def init_espnow():
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
espnow = espnow.ESPNow()
espnow.active(True)
espnow.add_peer(b'\xff\xff\xff\xff\xff\xff')
return espnow
def broadcast_beacon(espnow):
"""广播VISION_TEST_HOST信标,供控制器发现"""
espnow.send(b'\xff\xff\xff\xff\xff\xff', b"VISION_TEST_HOST")
5.5 关键代码 - 自适应测试引擎
class VisionTestEngine:
"""视力测试引擎 - 管理测试流程和结果计算"""
def __init__(self):
self.current_level_index = 3 # 从4.3级开始
self.level_results = {} # {level_index: {correct:N, total:N}}
self.level_errors = {} # {level_index: error_count}
self.test_direction = "RIGHT"
self.MAX_RESPONSES_PER_EYE = 10
def record_response(self, direction_chosen):
"""记录用户选择,判断正误"""
is_correct = (direction_chosen == self.test_direction)
index = self.current_level_index
if index not in self.level_results:
self.level_results[index] = {"correct": 0, "total": 0}
self.level_results[index]["total"] += 1
if is_correct:
self.level_results[index]["correct"] += 1
return is_correct
def compute_final_result(self):
"""计算最终视力(取最后一个正确级别)"""
vision_val = 4.0
for idx in sorted(self.level_results.keys()):
if self.level_results[idx]["correct"] > 0:
v = VISION_LEVELS[idx]
vision_val = v[0]
return vision_val
# 测试流程:
# 1. 同一级别连续测4个视标
# 2. ≥3个正确 → 升级到下一更细级别
# 3. <3个正确(已错2个) → 降级并结束
# 4. 最后正确级别即为最终视力结果
六、硬件实物展示与功能说明
6.1 硬件整体展示

M5StickC PLUS2手腕控制器与M5Paper E-Ink视标显示器实物展示
6.2 主菜单界面

6.3 测试界面

6.4 结果界面
6.5 系统运行日志
==================================================
视力测试控制器 v2
M5StickC PLUS2 (K016-H2)
BtnA短按=换眼 长按=新人
BtnB=裸眼/矫正切换
==================================================
[OK] IMU: Accel(0.02,0.98,-0.12) Gyro(0.1,-0.2,0.3)
[ESPNow] M5StickC MAC: aabbccddeeff
[ESPNow] 通道: 1, 等待自动发现M5Paper...
[ESPNow] 发现主机, 已发送CONTROLLER_READY
[ESPNow] 主机已确认连接, 模式已同步
==================================================
[TEST] R_裸眼 级别: 4.3, 方向: UP, 笔画: 232px
[RESP] (1/10) 选择: UP, 实际: UP, 正确!
[TEST] R_裸眼 级别: 4.7, 方向: RIGHT, 笔画: 46px
[RESP] (2/10) 选择: RIGHT, 实际: RIGHT, 正确!
[RESULT] 第1人 右眼 裸眼: 5分=5.0, 小数=1.0, 评估=正常

七、设计中遇到的难题和解决方法
7.1 IMU姿态检测的抖动和误触
问题描述:手腕动作检测存在自然抖动,导致误触发方向选择。简单阈值判断会产生大量误判。
解决方案:
- 采用双阈值防抖机制:300ms去抖时间 + 400ms保持确认
- 使用加速度归一化消除姿态影响
- 角度阈值设为15°,预留自然抖动空间
# 防抖机制:需要维持方向400ms才确认
if raw != self.last_raw_direction:
self.last_raw_direction = raw
self.hold_start_time = now
return None
if time.ticks_diff(now, self.hold_start_time) > HOLD_TIME_MS:
if time.ticks_diff(now, self.last_send_time) > DEBOUNCE_MS:
return raw # 确认方向
7.2 E-Ink墨水屏刷新速度
问题描述:E-Ink墨水屏刷新速度慢(全刷可达1-2秒),影响测试体验。E视标切换需要快速响应。
解决方案:
- 使用FASTEST模式(EPD_MODE_FASTEST)进行快速局部刷新
- 仅在菜单和结果显示时使用高质量全刷(EPD_MODE_QUALITY)
- 测试中以局部刷新替换视标区域,不清除全部屏幕
# 测试中使用最快刷新模式
Display.setEpdMode(EPD_MODE_FASTEST)
# 仅清除视标区域,保留状态信息
M5.Lcd.fillRect(e_cx - e_size//2 - margin, e_cy - e_size//2 - margin,
e_size + 2*margin, e_size + 2*margin, COLOR_BG)
7.3 ESPNow通信可靠性
问题描述:ESPNow通信在设备距离变化或信道干扰时可能出现丢包,导致方向选择无响应。
解决方案:
- 控制器端实现重发机制:3秒未收到确认自动重发CONTROLLER_READY
- 显示端每秒广播信标,确保新控制器可发现
- 状态同步机制:每次测试更新时主动推送STATUS消息
# 控制器重发机制
if self.pending_peer_mac and time.ticks_diff(now, self.last_ready_send_time) > 3000:
self.espnow.send(self.m5paper_mac, b"CONTROLLER_READY")
self.last_ready_send_time = time.ticks_ms()
7.4 E视标尺寸计算与显示适配
问题描述:不同视力等级的E视标尺寸差异巨大(4.0级笔画约232px,5.3级仅约3px),需要合理适配屏幕尺寸。
解决方案:
- 根据GB 11533-2011精确计算笔画像素:
stroke_pixels = 2000 × tan(10^(5-V)/60°) × 9.23 - 最小笔画限制为1px,避免小视标消失
- 大视标动态居中,保证不被屏幕边界截断
def calculate_stroke_pixels(level_index):
vision_5 = VISION_LEVELS[level_index][0]
visual_angle_arcmin = 10 ** (5 - vision_5)
stroke_mm = TEST_DISTANCE_MM * math.tan(math.radians(visual_angle_arcmin / 60))
stroke_pixels = max(1, int(round(stroke_mm * PIXELS_PER_MM)))
return stroke_pixels
7.5 按键长短按区分
问题描述:BtnA需要同时支持短按(切换眼睛)和长按(换人测试),需要精确区分两种操作。
解决方案:
- 使用按键按下时间和释放时间双重判断
- 长按阈值设为2秒,配合震动反馈避免误触
- 释放时才触发短按操作,给用户反悔空间
if M5.BtnA.isPressed() and hold_ms >= LONG_PRESS_MS:
result = "LONG_PRESS_A" # 长按2秒:换人测试
if M5.BtnA.wasReleased() and hold_ms < LONG_PRESS_MS:
result = "SHORT_PRESS_A" # 短按:开始测试/切换眼
八、心得体会
8.1 项目收获
- 嵌入式系统设计:深入理解ESP32双设备协同架构的设计和实现
- 传感器数据处理:掌握了IMU姿态解算和防抖算法
- E-Ink屏幕驱动:熟悉了墨水屏的刷新模式和局部刷新优化技巧
- 无线通信协议:实践了ESPNow点对点低延迟通信协议
- 国家标准应用:将GB 11533-2011视力标准转化为精确的数学计算
8.2 技术亮点
- 手腕动作控制:IMU姿态映射为方向选择,实现无接触式操作
- 自适应阶梯算法:根据受测者表现自动调整,快速准确评估视力
- ESPNow无线通信:低延迟、低功耗的双设备协同架构
- E-Ink优化渲染:根据不同场景切换刷新模式,兼顾速度和显示质量
- 国标精确计算:基于GB 11533-2011的精确视标尺寸计算
8.3 改进建议
- 语音引导:增加语音提示功能,指导受测者操作
- 云存储:通过WiFi将测试结果上传到云端,实现数据长期追踪
- 多语言支持:增加英文等界面,适应不同语言用户
- 视力趋势分析:长期记录绘制视力变化曲线
- 儿童模式:增加图形化引导和游戏化测试流程
- 测试报告打印:通过蓝牙连接打印机,即时打印测试报告
- 距离校验:使用超声波传感器验证测试距离是否准确
- 远程监测:医护人员可远程实时查看测试过程
- 批量测试模式:支持学校体检等场景的批量测试流程优化
九、创意方向
本项目的技术创新为以下创意方向提供了新的思路:
9.1 人工智能在嵌入式系统中的应用
本项目的传感器数据处理技术为AI应用提供了基础:
- 自动评估系统:自适应阶梯算法可发展为AI驱动的智能评估系统
- 手势识别:IMU数据可配合机器学习实现更复杂的手势识别
- 异常检测:通过数据分析识别测试过程中的异常操作
9.2 医疗健康物联网
智能视力测试是医疗健康物联网的典型应用:
- 远程医疗:测试结果可上传云端,实现远程视力监测
- 家庭健康管理:设备轻便易用,适合家庭日常视力自检
- 学校集体筛查:批量测试模式适合学校集体视力筛查
9.3 无线通信应用
项目集成的ESPNow通信功能:
- 低功耗传感网络:ESPNow适合建立低功耗传感器网络
- 室内定位:利用ESPNow信号强度实现室内定位
- 多设备协同:一对多通信模式适合多设备协同工作场景
9.4 技术迁移价值
本项目开发的技术方案可迁移到:
- 医疗健康:视力筛查、色盲测试、听力测试
- 人机交互:IMU手势控制、体感交互
- 教育领域:交互式学习设备、课堂互动系统
十、树莓派服务器应用
10.1 树莓派作为MQTT服务器
本项目使用树莓派作为本地MQTT服务器(Broker),运行Mosquitto MQTT服务,实现视力测试数据的实时传输。树莓派3B+(BCM2837B0四核1.4GHz,1GB RAM)作为边缘计算节点,能够同时处理多台M5Stack设备的并发数据上报。
10.2 树莓派作为数据库服务器
测试结果通过MQTT上报到树莓派后,使用SQLite数据库进行持久化存储,记录每次视力测试的详细数据(测试时间、左右眼视力值、测试模式等)。
10.3 MQTT通信架构
10.4 部署说明
# 树莓派端MQTT服务部署
sudo apt-get install -y mosquitto mosquitto-clients
sudo systemctl enable mosquitto
sudo systemctl start mosquitto
# Python数据存储服务
pip3 install paho-mqtt
python3 rpi_mqtt_data_service.py --broker localhost
10.5 数据表设计
CREATE TABLE IF NOT EXISTS vision_test_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
device_id TEXT NOT NULL,
eye_type TEXT NOT NULL,
test_mode TEXT NOT NULL,
naked_vision REAL,
corrected_vision REAL,
raw_payload TEXT
);
致谢
感谢 DigiKey 和 电子森林 提供的FastBond4活动支持,本次活动链接:https://www.eetree.cn/page/digikey-fastbond