1 项目要求
可识别挥动,接近/远离等手部动作,并使用手势动作控制屏幕上的菜单功能
2 项目简介
本项目想要使用嵌入式平台+dtof激光测距模块实现一个类似于华为手机隔空手势滑动屏幕的效果,效果参照链接。使用的平台为eetree的RP2040游戏机,传感器为艾迈斯欧司朗dToF传感器型号为tmf8821。实现了在手距离传感器9-30cm之间,上下挥动可控制菜单上下移动选项的功能。在本项目中,自行移植了传感器的驱动程序,使用树莓派pico的C-SDK开发包,用CMake管理工程。在算法方面使用了DTW算法进行上下手势的判断,判断准确率基本可达95%以上。
3 项目实现细节
3.1 项目实现思路
3.2 tmf8821驱动实现
3.2.1 初始化传感器参数及启动传感器
3.2.1.1 初始化传感器参数
初始化传感器的i2C地址等
tmf882xInitialise(&tmf8821); // 初始化传感器的参数
3.2.1.2 启动传感器
tmf882xEnable(); // 拉高传感器上的使能引脚,以及中断引脚(如果使用中断的情况下)
值得注意的是,这里在拉高后,需要等待3ms,才能进行后续操作
3.2.1.3 唤醒传感器
void tmf882xWakeup(tmf882xdriver *driver)
- 检查
ENABLE
(0xE0)寄存器的cpu_ready
位(Bit 7) 是否为 0。如果cpu_ready == 0
,说明设备仍处于低功耗状态,需要手动唤醒。 - 设置最低位最低位 Bit 0,即
pon
置为1=Activate oscillator激活晶振。
3.2.1.4 检查传感器cpu ready情况
检查 cpu_ready
(Bit 7)是否为 1,确保 CPU 可以正常运行并执行后续命令
轮询读取读取 ENABLE
(0xE0)寄存器,如果 cpu_ready
(Bit 7)为 1,则证明成功。设置一个超时时间,若超时则返回失败,避免CPU卡死。
3.2.2 进入bootloader流程并下载固件
int8_t tmf882xDownloadFirmware(tmf882xdriver *driver, uint32_t imageStartAddress, const uint8_t *image, int32_t imageSizeInBytes)
-> static int8_t tmf882xBootloaderSetRamAddr (tmf882xdriver * driver, uint16_t addr)
-> static int8_t tmf882xBootloaderWriteRam (tmf882xdriver * driver, uint8_t len )
-> static int8_t tmf882xBootloaderRamRemap (tmf882xdriver * driver, uint8_t appId )
上电(Power-On Reset,POR)后,TMF8821 默认进入 Bootloader 模式。设备在上电时需要将固件加载到 RAM 中。如果RAM 为空或固件无效,设备将保持在 Bootloader 模式,并且 APP_ID = 0x80
。
从单片机的指定地址读取RAM中的数据即image文件中的16进制数据。
查看手册8.9.5bootloader命令。按照手册给出的命令从下往上依次写入BL_CMD_STAT
寄存器,当执行当前命令成功后再次读取BL_CMD_STAT
寄存器,如果返回0x01
则证明该命令执行成功,如果返回其他值,查看BL_CMD_STAT
寄存器说明以确定错误类型。
3.2.2.1 设置固件的初始地址
static int8_t tmf882xBootloaderSetRamAddr (tmf882xdriver * driver, uint16_t addr)
这里总共写入五部分数据(命令 0x43
+ 长度 2
+ 目标地址 addr
+ 校验和)。
注:这里的校验和算法见手册8.9.4,确保传输数据正确。
寄存器名称 | 寄存器地址 | 写入命令 | 含义 |
---|---|---|---|
BL_CMD_STAT | 0x08 | 0x43 | 设置 RAM 访问地址 |
BL_SIZE | 0x09 | 0x02 | 命令数据长度 |
BL_DATA | 0x0A-0x8A |
| 目标地址 addr,由固件内给出 |
3.2.2.2 向 TMF8821 传输固件数据并写入 RAM
寄存器名称 | 寄存器地址 | 写入命令 | 含义 |
---|---|---|---|
BL_CMD_STAT | 0x08 | 0x41 | 发送 |
BL_SIZE | 0x09 | 24 | 设定数据长度 |
BL_DATA | 0x0A-0x8A |
| 需要写入 RAM 的固件数据 |
注:其实0x0A-0x8A
有128个寄存器,可以一次性写入固件内的128个十六进制数据,但是实际调试过程中一次性最多写入24个数据,当大于这个数据量时会写入失败。
3.2.2.3 执行固件
执行 RAM 重映射(RAM Remap),用于启动下载到 RAM 的固件。这一步骤相当于让 Bootloader 交出控制权,转而执行新下载的应用程序。
static int8_t tmf882xBootloaderRamRemap (tmf882xdriver * driver, uint8_t appId )
寄存器名称 | 寄存器地址 | 写入命令 | 含义 |
---|---|---|---|
BL_CMD_STAT | 0x08 | 0x11 | 发送 |
BL_SIZE | 0x09 | 0 | 无额外参数 |
BL_CSUM | 0x0A | 校验和 | 确保数据完整性 |
等待应用程序启动。由于 RAMREMAP_RESET
命令不会返回响应,因此无法直接确认执行结果。通过检查 APP_ID
寄存器如果为0x03确认启动成功。
3.2.3 设置硬件模式
int8_t tmf882xSwitchToLegacyMode (tmf882xdriver * driver )
-> static int8_t tmf882xSwitchToMode (tmf882xdriver * driver, uint8_t modeCmd, uint8_t mode )
根据手册中寄存器overview中的描述cid_rid
的值代表不同的模式。
当bootloader成功后,appid=0x03,默认CID_RID 为 "Main Application Register" 模式
寄存器 0x08
(cmd_stat
)是命令寄存器,负责接收指令。为了提高设计的通用性(个人猜测,这是器件厂家的考虑),需要向 0x08
写入 0x65
,以强制设备进入 TMF8820/TMF8821 模式。切换完成后,应检查 0x10
(mode
寄存器),该寄存器存储当前设备模式。
- 如果
0x10 == 0x00
,表示设备已成功进入 TMF8820/TMF8821 兼容模式。 - 如果
0x10 != 0x00
,说明模式切换失败或设备仍处于应用模式。
3.2.4 配置 TMF882X 传感器
int8_t tmf882xConfigure (tmf882xdriver * driver, uint16_t periodInMs, uint16_t kiloIterations, uint8_t spadMapId, uint16_t lowThreshold, uint16_t highThreshold, uint8_t persistence, uint32_t intMask, uint8_t dumpHistogram )
-> static int8_t tmf882xConfigInternal(tmf882xdriver * driver, uint16_t periodInMs, uint16_t kiloIterations, uint8_t spadMapId, uint16_t lowThreshold, uint16_t highThreshold, uint8_t persistence, uint32_t intMask, uint8_t dumpHistogram )
-> int8_t tmf882xLoadConfigPageCommon (tmf882xdriver * driver ) // 给0x08寄存器写入0x16,加载默认配置页
-> static int8_t tmf882xLoadConfigPage (tmf882xdriver * driver, uint8_t pageCmd )
-> int8_t tmf882xWriteConfigPage (tmf882xdriver * driver ) // 给0x08写入0x15,写入配置
通过tmf882xConfigure()
函数配置传感器的测量参数
参数 | 取值 | 作用 |
---|---|---|
configPeriod | [33, 500, 100] 或 [132, 264, 528] | 测距周期(单位 ms) |
configKiloIter | [537, 550, 900] 或 [250, 500, 1000] | 迭代次数(Kilo Iterations) |
configSpadId | [map_no_1, map_no_2, map_no_7] 或 [map_no_15] | SPAD 映射 |
configLowThreshold | 100 | 最小测距阈值(10cm) |
configHighThreshold | 500 | 最大测距阈值(50cm) |
configPersistance | [0, 1, 3] | 测量结果的稳定性要求 |
configInterruptMask | 0x3FFFF | 中断触发掩码 |
dumpHistogramOn | 0 | 是否输出直方图数据 |
参数名称 | 寄存器名称 | 寄存器地址(HEX) | 写入值(示例) | 作用 |
测距周期(Period) | TMF8828_COM_PERIOD_MS_LSB | 0x20 (LSB) | 0x21, 0x00 (示例:33ms) | 设置测量间隔 |
0x21 (MSB) | ||||
迭代次数(Kilo Iter) | TMF8828_COM_KILO_ITERATIONS_LSB | 0x22 (LSB) | 0x19, 0x02 (示例:537 Kilo) | 设置测量精度 |
0x23 (MSB) | ||||
SPAD 映射 ID | TMF8828_COM_SPAD_MAP_ID | 0x24 | 0x01 (示例:SPAD Map No.1) | 选择光电探测单元 |
最小测距阈值 | TMF8828_COM_LOW_THRESHOLD_LSB | 0x25 (LSB) | 0x64, 0x00 (示例:100mm = 10cm) | 设置检测下限 |
0x26 (MSB) | ||||
最大测距阈值 | TMF8828_COM_HIGH_THRESHOLD_LSB | 0x27 (LSB) | 0xF4, 0x01 (示例:500mm = 50cm) | 设置检测上限 |
0x28 (MSB) | ||||
测量持久性 | TMF8828_COM_PERSISTENCE | 0x29 | 0x01 (示例:报告所有有效数据) | 过滤测量数据 |
中断掩码 | TMF8828_COM_INT_MASK_0 | 0x2A - 0x2C | 0xFF, 0xFF, 0x03 (示例:18-bit Mask) | 选择触发中断的测量区域 |
TMF8828_COM_INT_MASK_1 | ||||
TMF8828_COM_INT_MASK_2 | ||||
直方图数据控制 | TMF8X2X_COM_HIST_DUMP | 0x30 | 0x01 (示例:仅启用原始直方图) | 选择直方图类型 |
算法设置 | TMF8X2X_COM_ALG_SETTING_0 | 0x31 | 0x10 (示例:启用对数置信度) | 调整测量算法 |
写入方法:
步骤 | 寄存器 | 写入值 | 作用 |
---|---|---|---|
| 0x08 | 0x16 | 加载 Page 0 配置到 RAM |
| 0x20 - 0x31 | 用户定义的参数 | 主机修改 0x20 - 0x31 |
| 0x08 | 0x15 | 将 0x20 - 0x31 写入设备 |
0x08
(cmd_stat
)寄存器中的 0x16
(CMD_LOAD_CONFIG_PAGE_COMMON) 命令用于加载“配置页 0”(Page 0),将默认配置加载到 0x20
及后续寄存器(相当于 I²C RAM)。主机(MCU)可在 RAM 中修改这些寄存器的值,然后再写回设备。由于 TMF882X 不能直接修改 0x20 - 0x31
,必须先执行 0x16
命令,以确保所有参数先加载到 RAM,从而支持批量修改。
完成参数修改后,需要执行 0x15
(CMD_WRITE_CONFIG_PAGE) 命令,将 0x20
及后续寄存器中的数据正式写入传感器存储单元,使其生效,并应用于测距计算。这一步骤确保参数不会丢失,并减少 I²C 传输错误的风险,主机可在确认所有配置正确后再一次性写入。
采用 0x16 -> 修改 -> 0x15
这一流程的主要目的是:
- 确保配置完整性:只有完整的配置数据写入后,传感器才会应用新设置。
- 防止中途修改数据:避免数据未完全写入时,设备进入错误状态。
- 适用于批量写入:支持一次性修改多个参数,而非逐个寄存器更新,提高配置效率。
3.2.5 测量并读取数据
3.2.5.1 启动测量模式
int8_t tmf8828StartMeasurement ( tmf882xdriver * driver )
给0x08寄存器写入0x10命令开启测量
3.2.5.2 读取数据
int8_t tmf882xOutputDistanceResults(tmf882xdriver *driver, measurementoutput *output)
具体读取的寄存器见3.3.1部分。
3.3 手势采集与识别算法
3.3.1 数据采集
本项目使用的是"3x3 Normal mode",如图所示。即有9个采样区域,可以分别读出每个区域的测距结果。以便于后续的数据处理。
根据手册8.4节可知,在传感器进入测量状态下,读取寄存器0x24-0xa4即可读取到所有需要的信息。这里根据我后方的算法需要,读取寄存器号及其说明如下:
寄存器号 | 寄存器名称 | 寄存器说明 |
0x26 | NUMBER_VALID_RESULTS | 读取有效数据个数 |
0x39,0x3A | RES_DISTANCE_0_LSB, RES_DISTANCE_0_MSB | zone1的距离数据,低八位和高八位。实际一个zone的距离数据需要把两个寄存器值进行拼接。 |
0x3B-0xA3 | 剩余zone的距离和置信度,这里只取距离数据,不读取置信度的寄存器。 |
最后将数据存储到一个数组中,如下所示(由于默认的区域1-9并不是我们传感器实际的方向,这里还需要重新排列每个区域号):
173 171 469 159 469 469 487 488 473
注:这里其实有一个小bug,如果根据寄存器NUMBER_VALID_RESULTS读取到的数量来决定实际数据的数量的话,会出现当距离很近时NUMBER_VALID_RESULTS<9,当距离很大时NUMBER_VALID_RESULTS>9的情况。所以为了保证最后数据的一致性,在读出数据后,如果数据量小于9,缺失的区域用0补上;如果数据量大于9则丢弃多余区域数据。
3.3.2 数据预处理
为了方便后续算法在算力有限的嵌入式芯片上实现,将数据进行适当处理可大幅优化计算效率。
这里为了便于说明,将数据重新排列,以便于算法的讲解.
// 合并数据并二值化:当一排数据中有两个数据满足30 < x < 300, 则该排取1,否则取0,
// 再将该二值化序列看作一个三位二进制数并转化为10进制数
173 171 469 1
159 469 469 → 0 → 1 0 0 → 4
487 488 473 0
3.3.3 算法实现
DTW算法简介
In time series analysis, dynamic time warping (DTW) is an algorithm for measuring similarity between two temporal sequences, which may vary in speed.
https://en.wikipedia.org/wiki/Dynamic_time_warping
在本项目中,我的实现思路为:当手进入到采集区域内,触发采集,采集105ms数据(反复试验得来,人手正常速度通过传感器的时间,既可以保证较好的识别率,又不会采集过多多余帧数,造成肉眼可见的显示上下翻页效果延迟,徒增设备功耗与内存压力)。采样间隔3ms,采样次数即为105 / 3 = 35。
最后得到形如下方的序列:
// 标准从上到下挥动手臂时间序列的模板
{0,4,4,4,4,6,6,6,6,6,6,7,7,7,7,7,7,3,3,3,3,3,3,1,1,1,1,1,1,1,1,1,1,1,0}
// 标准从下到上挥动手臂时间序列的模板
{0,1,1,1,1,1,3,3,3,3,3,3,7,7,7,7,7,7,6,6,6,6,6,6,4,4,4,4,4,0,0,0,0,0,0}
可以发现上下运动分别有特别明确的变化特征,DTW算法对其有特别好的识别效果。
DTW 计算过程
- 定义两条序列
- 设定参考序列(模板序列):
- 设定测试序列(需要比对的序列):
- 构建距离矩阵
- 计算两个序列的两两欧式距离,形成矩阵 ,其大小为 ,定义为:
- 这里通常使用欧式距离,也可以使用其他距离度量(如绝对差值)。
- 构建累积代价矩阵
- 定义累积代价矩阵 ,其计算方式如下:
- 其中 作为初始条件。
- 计算最短路径
- 从 处开始回溯,找到从 到 之间的最优匹配路径(即最小累积代价路径)。
- 允许非线性匹配,如跳过某些点以获得最优路径。
- DTW 距离
- 最终的 DTW 距离为:
- 该值表示两序列的最优对齐代价,数值越小表明两序列越相似。
图为与序列T = {0,4,5,6,6,7,3,3,1,1,0},的对齐过程路径图,在DTW算法中,并不要求两序列长度必须相等,但在我的项目中所有采集数据序列长度均相等。
最后将输入序列分别与两个模板计算其DTW距离,并比较取DTW距离较小的那个分类作为当前输入序列的分类标签。
4 效果展示
由于图片并不能直观展示功能,见上方视频中1:38处的视频内容演示。
5 总结与反思
第一次从零开始对着手册独立写一个传感器的驱动,而且还是一个启动流程与功能要比一般传感器复杂得多的驱动。对自己本身来说是一个挑战,经过一个月的研究,最终还是成功驱动起该传感器,对我自身也是一个巨大提升,是我也更加深刻的理解传感器的启动及其他驱动流程。
在手势识别算法方面,阅读诸多论文与材料,最终决定使用DTW算法作为识别算法,在先使用python在电脑上运行尝试之后发现该算法效果出奇的好。于是将算法代码移植到嵌入式平台,并且为了避免嵌入式平台处理器算力有限造成的计算延迟,对数据与算法做了诸多优化。
此外,在编程的多个细节上也融入了一些巧妙的设计。例如,在将采集到的各区域距离数据转换到目标方向时,并未采用简单的数组遍历方式逐个赋值,而是通过指针结合指定数组索引的方式进行映射。这种方法能够显著减少内存地址的频繁跳转,从而优化 CPU 运行时的指令效率,提高执行速度。此外,在数据采集的时间间隔设计上,也进行了优化。由于单片机计算效率较低,在执行复杂算法时,其运行时间不可忽略。因此,我先对代码块的运行时间进行粗略估算,然后在此基础上加入适当的等待时间作为采样周期,以确保数据采集的稳定性和有效性。
因为读书繁忙,还有许多地方我认为可以优化,例如:增加模板个数以提升识别准确率;增加数据预处理和判断流程可以增加左右挥动与靠近远离手势;传感器采集到的其他寄存器中的数据例如置信度等是否可以提高算法效率和准确度。这些都有待探索。
总的来说参加本次活动对编程和阅读技术手册能力方面都有巨大提升。