项目介绍
本次活动的项目是围绕TMF8821的dToF传感器模块,我选择的主控是RP2040,就是电子森林官方提供的基于树莓派RP2040的嵌入式系统学习平台 - 游戏机
实现了以下功能:
- 检测手势的上、下、左、右的运动
- 菜单页面,菜单的选择、进入和退出
硬件介绍
总共有两个主体,后面这个就是RP2040游戏机,前面这块PCB是本次活动的重点-TMF8821芯片制作的拓展板
TMF8821
TMF8821是一款高精度、超小型的时间飞行(dToF)传感器。集成了 VCSEL(垂直腔面发射激光器),SPAD(单光子雪崩二极管)探测器,TDC(时间数字转换器)以及直方图技术。常被用于精确的距离测量和空间感知的相关应用。
具有以下特点:
- TMF8821:多区域,包括3x3、4x4和3x6区域
- 单光子雪崩光电二极管 (SPAD) 直方图架构
- 时间数字转换器 (TDC) 直接测量飞行时间
- 模块化2.0mm x 4.6mm x 1.4mm封装
- 快速VCSEL驱动器,具有保护功能
- 在要求苛刻的用例情况下可靠运行
- 功耗:141mW(30Hz工作频率时)
- 功耗待机电流:8µA(保持内存)
- 关断电流消耗:2µA (EN=0)
- 动态盖玻璃校准
- 63° FoI/FoV
- 基准SPAD
TMF8821的一些关键参数:
在尽可能的功能下,体积还非常的小巧,总共有12个管教,可以分为以下几类:
- 供电
- 通讯(IIC)
- 使能和中断
- 两个独立的GPIO
在硬件布局和设计的时候也十分的简单和方便,仅需三个滤波电容就可以工作了
下面是TMF8821和外部主控连接的示意图,可以看到内部集成了一个Cortex M0+内核的芯片
下面是TMF882x的顶视图,可以直接看到两个最主要的部分:照明器和接收器。这是实现各种测量功能的最主要部分之一了。
RP2040
RP2040 是树莓派首次推出的微控制器,是一款高性能、低成本和易用性的芯片,具有以下特点:
- 双 ARM Cortex-M0+ @ 133MHz
- 264kB 片上 SRAM,分六个独立的存储组
- 通过专用 QSPI 总线支持高达 16MB 的片外闪存
- DMA 控制器
- 完全连接的 AHB 交叉条
- 插值器和整数除法器外设
- 片内可编程 LDO,用于产生内核电压
- 2 个片上 PLL,用于产生 USB 和内核时钟
- 30 个 GPIO 引脚,其中 4 个可用作模拟输入
- 外设
- 2 个 UART
- 2 个 SPI 控制器
- 2 个 I2C 控制器
- 16 个 PWM 通道
- USB 1.1 控制器和 PHY,支持主机和设备
- 8 个 PIO 状态机
- 支持MicroPython、C、C++编程
设计思路
因为使用的套件都是官方的,所以在硬件上面并不需要设计,并且模块都是直插连接,都不需要额外的飞线,下面是连接后的样子:
在软件方面需要考虑的事情就多了,但是大致可以分为以下几个点:
本次使用的编程语言:micropython
本次使用的开发环境:Thonny
在驱动编写方面,明确规定了不能使用艾迈斯欧司朗官方提供的dToF支持的Arduino的库,所以可以从0根据提供的驱动手册来编写,或者使用第三方的库等等
在应用开发方面,主要是验证驱动是否正常,能否正确的识别到手势的移动方向,所以在此基础上写一个简单的菜单Demo
软件流程图
驱动层面关键部分
驱动的内容比较细,但是可以分为3大部分:
- 设备的启动分为冷启动和热启动,流程如下:
- 加载镜像流程:
- 测量流程:
应用层面关键部分
初始化、加载bootloader
tof = Tmf8821Utility(ic_com=I2C_com())
tof.log("Try to open connection")
if Tmf8821App.Status.OK != tof.open():
tof.error("Error open FTDI device")
raise RuntimeError("Error open FTDI device")
else:
tof.log("Opened connection")
tof.init_bootloader_check()
主循环:其中较为关键的是 detect_gesture 函数
while True:
frames = tof.measure_frame() #获取传感器数据
if frames and frames[0].results: # 确保frame不为空并且有结果
distances = frames[0].get_distances() # 获取距离列表
dir = detect_gesture()
def detect_gesture():
global history_distances, first_run, last_gesture_time, gesture_cooldown_ms
if first_run: # 第一次不进行手势检测
history_distances = [distances[:], distances[:], distances[:]]
first_run = False
return ''
# 更新历史数据
history_distances.pop(0)
history_distances.append(distances[:])
# 转化成易读的变量
last_last_TR = history_distances[2][6]
last_TR = history_distances[1][6]
TR = history_distances[0][6]
last_last_TL = history_distances[2][0]
last_TL = history_distances[1][0]
TL = history_distances[0][0]
last_last_BR = history_distances[2][8]
last_BR = history_distances[1][8]
BR = history_distances[0][8]
last_last_BL = history_distances[2][2]
last_BL = history_distances[1][2]
BL = history_distances[0][2]
# 获取当前时间
current_time = time.ticks_ms()
# 检查是否处于冷却期内
if current_time - last_gesture_time < gesture_cooldown_ms:
return ''
# 判断手势
rst_h = is_horizontal_gesture(last_TR, TR, last_TL, TL, last_BR, BR, last_BL, BL)
rst_v = is_vertical_gesture(last_TR, TR, last_TL, TL, last_BR, BR, last_BL, BL)
if rst_h != None:
last_gesture_time = current_time
print(rst_h)
return rst_h
if rst_v != None:
last_gesture_time = current_time
print(rst_v)
return rst_v
函数解析:
- 保存了三次的数据,分别为:上上次、上次、现在的数据。用于更精细的判断,
- 将提取数组中的值,保存到变量中,用于更好的区分哪个是哪个变量
- 设置时间阈值,在一定时间内只会触发一次状态的更新
- 根据数据来判断手势的方向,并且返回
在上面 detect_gesture 函数中调用了检测方向的函数 is_horizontal_gesture 和 is_vertical_gesture,分别用于识别水平运动和垂直运动的方向。下面以识别水平方向运动为例,垂直的同理。
def is_horizontal_gesture(last_TR, TR, last_TL, TL, last_BR, BR, last_BL, BL):
horizontal_change_TR_TL = (TR - TL)
horizontal_change_BR_BL = (BR - BL)
horizontal_change_last_TR_TL = (last_TR - last_TL)
horizontal_change_last_BR_BL = (last_BR - last_BL)
if ((horizontal_change_TR_TL > threshold and horizontal_change_BR_BL > threshold) and
(horizontal_change_last_TR_TL < threshold and horizontal_change_last_BR_BL < threshold)):
return "Right"
elif ((horizontal_change_TR_TL < -threshold and horizontal_change_BR_BL < -threshold) and
(horizontal_change_last_TR_TL < threshold and horizontal_change_last_BR_BL < threshold)):
return "Left"
else:
return None
菜单界面
菜单界面比较简易,仅支持两级菜单
menu_str = ['menu_1','menu_2','menu_3','menu_4','menu_5']
menu_len = len(menu_str)
menusub_str = ['menu_1_ENTRE','menu_2_ENTRE','menu_3_ENTRE','menu_4_ENTRE','menu_5_ENTRE']
menu_index = 0 # 当前菜单选项
menu_Lastindex = -1 # 上次菜单选项
isEntre = False # 是否进入二级菜单
while True:
if isEntre: # 二级菜单
display.text(font2,menusub_str[menu_Lastindex],25,100)
else: # 一级菜单
menu_y = 10
for item in menu_str:
display.text(font2, item, 30, menu_y)
menu_y = menu_y + 40
if menu_Lastindex != menu_index:
display.text(font2, '>', 5, 10+40*menu_index)
display.text(font2, ' ', 5, 10+40*menu_Lastindex)
menu_Lastindex = menu_index
问题总结
1. TMF8821的驱动
官方提供了多种平台的驱动:驱动链接。但是要在RP2040上使用micropython来驱动TMF8821并没有提供。一开始是手撸驱动,写到加载镜像就卡壳了,但是也算熟悉了芯片的加载和使用流程,后面在github上找到了一个使用micropython驱动TMF8821的部分功能的代码:参考链接,所以就在此基础上进行了简单的修改和移植,在RP2040游戏机上使用。
2. 识别准确度低
问题:只使用当前这一组数据,比如获取3*3=9个数据就直接进行判断了
解决:保存了历史数据,上上次、上次和当前的数据。添加了更多判断条件(见上面:应用层面关键部分的is_horizontal_gesture函数)
3. 容易误触
现象:手掌从左划到右,先会触发一次左滑,然后紧接着触发右滑
解决:添加识别冷却时间,在触发一次后的一定时间内不会进行下次一次的判断(见上面:应用层面关键部分的detect_gesture函数)
4. 识别准确度不高
现在:有时候挥动手不是识别到动作
解决:尽量让设备和墙体保持水平且不要前后乱动(算法上应该还可以在优化优化)
活动总结
本次活动的时间非常的重组充足,从去年的11月中旬到今年的2月底。在这段时间里,对官方和网络的资料进行整理汇总,并且选择了难度适中的任务二-手势识别。
可以说驱动编写十分的坎坷,文档都是English的配合上AI和翻译,大致浏览了全部的文档,主要参考AN001015这个文档,一步一步的写到3.2章-Image Download,但是这镜像咋都烧不到,没进应用程序。加上回头看前面写的驱动,发现毫无复用可言,简直就是杂乱无章,这方面技能树还得在加强加强。