项目介绍
此次活动我使用了官方推荐的平台,即基于 TMF8821 的 dToF 传感器模块-搭配RP2040游戏机。
TMF8821 dToF 传感器模块是基于 TMF8821 传感器设计的一款直接飞行时间传感器,与 RP2040 游戏机管脚匹配,插上直接可以使用,模块侧面预留了扩展接口,可以自由焊接/调试/抓取数据。
我实现的是任务2--手势识别,可识别挥动,接近、远离等手部动作,并使用手势动作控制屏幕上的菜单功能。
由于参赛规则限定艾迈斯欧司朗官方提供的 dToF 支持的 Arduino 库和支持 Arduino 的板卡不可同时使用,因此我的实现方式是 RP2040 运行 Pico C SDK,然后移植官方提供的 dToF Arduino 库到 RP2040 C SDK,进而通过 TMF8821 传感器模块读取测量值,实现手势识别算法,最终控制 RP2040 游戏机显示屏 GUI 界面。
简短的使用到的硬件介绍
主控 RP2040 Game Kit
RP2040 Game Kit是基于树莓派RP2040的嵌入式系统学习平台,USB Type-C供电,采用RP2040作为主控,支持MicroPython、C/C++编程,性能强大。
板上功能:
- 240x240 分辨率的彩色IPS LCD,SPI接口,控制器为ST7789
- 四向摇杆 + 2个轻触按键 + 一个三轴姿态传感器MMA7660用做输入控制
- 板上外扩2MB Flash,预刷MicroPython的UF2固件
- 一个红外接收管 + 一个红外发射器
- 一个蜂鸣器
- 双排16Pin连接器,有SPI、I2C以及2路模拟信号输入
游戏机可玩性极高,可移植多款复古游戏,还可作为电赛的控制、显示接口平台,搭配传感器、模拟电路外还可以完成更多创意项目。
TMF8821 模块
dToF模块是基于 TMF8821 设计的直接飞行时间 (dToF) 传感器模块,TMF8821采用单个模块化封装,带有相关的 VCSEL(垂直腔面发射激光器)。dToF 设备基于 SPAD、TDC 和直方图技术,可实现 5000 mm 的检测范围。由于它的镜头位于 SPAD 上,它支持 3x3、4x4 和 3x6 多区域输出数据以及宽广的、动态可调的视野。VCSEL 上方的封装内的多透镜阵列 (MLA) 拓宽了 FoI(照明场)。原始数据的所有处理都在片上进行,TMF8821在其 I2C 接口上提供距离信息和置信度值。
TMF8821 dToF传感器模块与RP2040游戏机管脚匹配,插上直接可以使用,并在侧面预留了扩展接口,可以自由焊接/调试/抓取数据。
硬件连接
dToF 模块和 RP2040 通信接口是IIC,此外还有使能管脚和中断管脚,但我的程序中没有使用中断管脚。
dToF 模块上标注的几个管脚和主控的连接关系如下:
序号 | dToF 丝印 | RP2040 Game Kit 管脚 |
---|---|---|
1 | 5V | 5V |
2 | SCL | IO17 |
3 | SDA | IO16 |
4 | EN | IO22 |
5 | INT | 不适用,无连接 |
6 | GND | GND |
此外用到了按键A和摇杆,作为LVGL输入设备。其中按键A对应 IO6,摇杆X轴对应 ADC2, 摇杆Y轴对应 ADC3。
原理图
从 game2040-V3-20211228.pdf 中找到 J3 扩展口,使用 I2C0 (其中 IO17 是 SCL ,IO16 是 SDA),使用 IO22 作为 TMF8821 的使能管脚。按键A和摇杆的IO也在图中。
从 TMF8821_kicad.pdf 也能看到对应的管脚,符合上面的表格。
方案框图和项目设计思路介绍
系统框图如下:
- 主控是 RP2040,通过各个接口与外设连接,获取dTOF传感器数据,运行手势识别算法,并控制GUI控件;
- LCD 驱动器是 ST7789,通过SPI接口与主控通信,显示基于LVGL的界面;
- 按键、摇杆作为输入设备,接入到LVGL;
- dToF TMF8821 传感器,和主控通过 I2C 通信,下载固件后启动测量,并将结果返回给主控;
我的实现步骤如下:
- 搭建 RP2040 游戏机的 Pico C SDK,点亮 LED;
- 点亮 RP2040 游戏机的 显示屏;
- 移植 LVGL 到 Pico C SDK,主要是移植显示驱动和输入驱动;
- 移植 TMF8821 Arudino 库到 Pico C SDK,主要是对接 IIC 和 UART驱动,并运行 TMF8821 串口测量程序;
- 基于 TMF8821 串口测量程序做修改,提取 dToF 检测数据;
- 基于 dToF 检测数据,实现手势识别算法;
- 将手势识别算法的结果适配到 LVGL 输入设备驱动中,得以控制GUI控件;
- 设计应用界面,手势控制GUI,显示按键导航、点按按键的操作;
其他的步骤如移植LVGL,适配显示驱动和输入驱动,我之前在别的开发板已进行多次适配,不是问题。重点在第4步和第6步。
第4步,熟悉 Pico C SDK,运用它的 IIC 和 UART 驱动适配 TMF8821 Arduino 的官方驱动,并且运行官方串口命令程序。
第6步,实现手势识别算法。此前未曾接触过算法类的代码,卡住了我。
软件流程图和关键代码介绍
主流程
完整的工程代码流程图如下:
stdio_init_all()
是 Pico C SDK 初始化,负责 UART, USB 初始化;setupFn()
是 TMF8821 库初始化,配置UART、IIC和使能管脚,复位 TMF8821 库的状态变量,从串口打印帮助字符串;- LVGL 初始化,包括LVGL核心初始化,显示接口、输入接口初始化,并添加一个定时器维持 LVGL 心跳;
ui_dtof_entry()
创建一个基于 LVGL 的 GUI 应用,界面上显示两个按钮和一个标签;- 函数
cmd_xxx_tmf8828_yyy()
封装了 tmf882x 的库函数,执行一系列操作,例如下载固件、执行校准、启动测量; lv_timer_handler()
每隔一段时间更新 LVGL,根据输入设备驱动或者外部事件触发GUI重新绘制;cmd_loop_fn()
获取测量结果,保存到全局变量中,结果中包含:tmf882x的I2C器件地址,结果编号,芯片温度,此次结果中有效数据个数,时间戳,多个点的信息(距离和置信度);detect_gesture()
运行手势识别算法,输入数据是步骤7中的全局变量,输出是手势类型,如靠近、远离、上下左右挥动等;- 根据手势执行不同的动作,这里把手势融合到LVGL输入设备驱动,即远离是GUI控件焦点转移,靠近是单击控件;
LVGL
移植 LVGL 先点亮屏幕,然后接入按键和摇杆,可以与GUI交互。
ST7789 LCD
当前环境是 Pico C SDK,驱动 ST7789 的显示屏,从头开始比较费时间,幸好从网上找到了一个开源的针对 Pico ST7789 的项目,它的地址是 GitHub - gavinlyonsrepo/ST7789\_TFT\_PICO: TFT SPI LCD, ST7789 Driver, Raspberry pi PICO display library. C++ SDK
把上面开源的项目 ST7789_TFT_PICO 移植到当前工程,遇到几个小问题,但是顺利解决了。
显示驱动适配层
LVGL 驱动适配层很简单,只需要实现两个函数,代码简化如下:
disp_init()
初始化SPI、GPIO管脚,发送屏幕初始化序列,设置屏幕方向,清屏;disp_flush()
把 LVGL 渲染的图像发送到显示屏;
// 全局变量
ST7789_TFT mLvglTFT; // 注意这是一个类实例化
static void disp_init(void)
{
mLvglTFT.TFTInitSPIType(); // SPI 初始化
mLvglTFT.TFTSetupGPIO(); // 初始化 SPI 管脚
mLvglTFT.TFTInitScreenSize(); // 设置屏幕大小
mLvglTFT.TFTST7789Initialize(); // 初始化屏幕,发送初始化序列,设置屏幕方向
}
// 把LVGL渲染的图像绘制到屏幕上
static void disp_flush(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p)
{
if (disp_flush_enabled) {
mLvglTFT.TFTdrawBitmap16Data(area->x1, area->y1, (uint8_t *)color_p, area->x2 - area->x1 + 1, area->y2 - area->y1 + 1);
}
lv_disp_flush_ready(disp_drv);
}
输入设备驱动层
RP2040 Game Kit 有一个摇杆和两个物理按键,这里只使用按键A和摇杆。
- 按键A作为确认按键;
- 摇杆有4个方向,摇杆拨动到左边或者上边,视为后退键;摇杆拨动到右边或者下边,视为前进键;
- 按键A通过 IO6 读取输入,摇杆通过ADC读取采样值(IO28 对应X轴,IO29 对应Y轴),然后判定位置;
按键作为输入设备,读取电平状态即可;但是摇杆通过ADC读取数值,读取数值划分区段,作为3种不同的状态,如下图所示:
- 没有拨动摇杆时,ADC读取的数值范围在 [1700, 2300]
- 摇杆拨动到左边或者上边,ADC读取的数值范围在 [0, 200]
- 摇杆拨动到右边或者下边,ADC读取的数值范围在 [3900, 4096]
- 摇杆拨动到中间一部分,认为是无效状态;
这里把摇杆和按键A的组合模拟旋转编码器,分为三种状态:
- 按键A按下表示确认键;
- 摇杆拨动到左边或者上边,表示后退键;
- 摇杆拨动到右边或者下边,表示前进键;
在 lv_port_indev.c 文件中实现输入设备驱动,相关代码如下:
/*------------------
* Encoder
* -----------------*/
#define BUTTON_A_GPIO 6
#define MIDDLE_DOWN 1700
#define MIDDLE_UP 2300
#define BACK_LIMIT 200
#define FORWARD_LIMIT 3900
/*Initialize your keypad*/
static void encoder_init(void)
{
adc_init();
adc_gpio_init(28); //X , IO28, ADC2 -- left, back
adc_gpio_init(29); //Y , IO29, ADC3 -- right, forward
// Button A, IO6 -- Enter
gpio_init(BUTTON_A_GPIO);
gpio_set_dir(BUTTON_A_GPIO, GPIO_IN);
gpio_pull_up(BUTTON_A_GPIO);
}
bool encoder_back_pressed = false;
bool encoder_forward_pressed = false;
bool encoder_enter_pressed = false;
bool gesture_inject_flag = true;
/**
* @brief 扫描更新按键和 Joystick 的状态
*
*/
static void encoder_button_update(void)
{
static uint32_t pre_update_time_ms = 0;
uint32_t cur_update_time_ms = to_ms_since_boot(get_absolute_time());
if ((cur_update_time_ms - pre_update_time_ms) > 150) { // 间隔 XXXms 才真正扫描一次
// X, IO28, ADC2 -- left, back
adc_select_input(2);
uint16_t adc_x_raw = adc_read();
adc_select_input(3);
uint16_t adc_y_raw = adc_read();
// Joystick 的 x, y 都没有拨动
if (((MIDDLE_DOWN < adc_x_raw) && (adc_x_raw < MIDDLE_UP)) && ((MIDDLE_DOWN < adc_y_raw) && (adc_y_raw < MIDDLE_UP))) { // x middle, y middle --> 没有按下左右键
encoder_back_pressed = false;
encoder_forward_pressed = false;
// printf("back, forward = false\r\n");
}
// Joystick 拨动到左侧 -- left/back
if ((adc_x_raw < BACK_LIMIT) || (adc_y_raw < BACK_LIMIT)) {
encoder_back_pressed = true;
// printf("back = true \r\n");
}
// Joystick 拨动到右侧 -- right/forward
if ((FORWARD_LIMIT < adc_x_raw) || (FORWARD_LIMIT < adc_y_raw)) {
encoder_forward_pressed = true;
// printf("forward = true \r\n");
}
if (gpio_get(BUTTON_A_GPIO) == 0) { // enter pressed
encoder_enter_pressed = true;
// printf("enter = true \r\n");
} else {
encoder_enter_pressed = false;
// printf("enter = false \r\n");
}
// 更新旧的时间戳
pre_update_time_ms = cur_update_time_ms;
} else { // 没有到时间,直接释放所有按键
encoder_back_pressed = false;
encoder_forward_pressed = false;
// encoder_enter_pressed = false;
}
}
/*Will be called by the library to read the encoder*/
static void encoder_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
if (gesture_inject_flag) {
gesture_inject_flag = false;
} else {
// 摇杆识别放在这里
encoder_button_update();
}
if (encoder_enter_pressed) { // 按下确认键
encoder_diff = 0;
encoder_state = LV_INDEV_STATE_PRESSED;
goto flag_update_enc_state;
}
if (encoder_back_pressed) { // Left, Back
encoder_diff -= 1;
encoder_state = LV_INDEV_STATE_RELEASED;
goto flag_update_enc_state;
}
if (encoder_forward_pressed) { // Right, Forward
encoder_diff += 1;
encoder_state = LV_INDEV_STATE_RELEASED;
goto flag_update_enc_state;
}
// 默认情况下,没有按键按下,则释放状态
encoder_diff = 0;
encoder_state = LV_INDEV_STATE_RELEASED;
flag_update_enc_state:
data->enc_diff = encoder_diff;
data->state = encoder_state;
}
GUI 设计
GUI效果图
手势控制或者按键操控达成的效果:
- 按钮 EETREE 可以点击,每次点击会改变自身颜色,红色-->蓝色-->红色;
- 按钮 TMF8821 也可以点击,每次点击啊会改变自身颜色,红色-->蓝色-->红色;除此之外,会改变下面标签的字号大小,依次是 12-->14-->16-->18-->12;
- 手势控制,靠近表示单击按钮;远离表示焦点转移到下一个控件;
GUI 代码
#include "lvgl.h"
#include "ui_dtof_lvgl.h"
// 定义全局变量以存储 signature_label 和当前字体大小
static lv_obj_t* signature_label;
static uint16_t current_font_size = 16;
// 字体数组(假设 LVGL 已经支持这些字体)
static const lv_font_t* font_sizes[] = {
&lv_font_montserrat_12,
&lv_font_montserrat_14,
&lv_font_montserrat_16,
&lv_font_montserrat_18
};
// 事件处理函数
static void event_handler(lv_event_t* e)
{
lv_event_code_t code = lv_event_get_code(e);
lv_obj_t* btn = lv_event_get_target(e);
if (code == LV_EVENT_CLICKED) {
// 判断是否是 TMF8821 按钮被点击
if (btn == lv_event_get_user_data(e)) {
// 遍历字体大小
current_font_size = (current_font_size + 2 - 12) % 8 + 12; // 循环遍历 12, 14, 16, 18
int font_index = (current_font_size - 12) / 2;
lv_obj_set_style_text_font(signature_label, font_sizes[font_index], 0);
LV_LOG_USER("Font size changed to %d", current_font_size);
}
}
}
void ui_dtof_entry(void)
{
lv_obj_t* label;
// 创建第一个按钮 EETREE
lv_obj_t* btn1 = lv_btn_create(lv_scr_act());
lv_obj_add_event_cb(btn1, event_handler, LV_EVENT_ALL, NULL);
lv_obj_align(btn1, LV_ALIGN_CENTER, 0, -80);
lv_obj_set_size(btn1, 120, 50);
lv_obj_add_flag(btn1, LV_OBJ_FLAG_CHECKABLE);
label = lv_label_create(btn1);
lv_label_set_text(label, "EETREE");
lv_obj_set_style_text_font(label, &lv_font_montserrat_18, 0);
lv_obj_center(label);
// 创建第二个按钮 TMF8821
lv_obj_t* btn2 = lv_btn_create(lv_scr_act());
lv_obj_add_event_cb(btn2, event_handler, LV_EVENT_ALL, btn2); // 将 btn2 作为用户数据传递
lv_obj_align(btn2, LV_ALIGN_CENTER, 0, 20);
lv_obj_set_size(btn2, 160, 60);
lv_obj_add_flag(btn2, LV_OBJ_FLAG_CHECKABLE);
label = lv_label_create(btn2);
lv_label_set_text(label, "TMF8821");
lv_obj_set_style_text_font(label, &lv_font_montserrat_18, 0);
lv_obj_center(label);
// 在屏幕右下角创建签名标签
signature_label = lv_label_create(lv_scr_act());
lv_label_set_text(signature_label, "Batman9527 @ 20250227");
lv_obj_set_style_text_color(signature_label, lv_color_black(), 0);
lv_obj_set_style_text_font(signature_label, &lv_font_montserrat_16, 0);
lv_obj_align(signature_label, LV_ALIGN_BOTTOM_RIGHT, -10, -10); // 右下角对齐,留出 10 像素边距
}
TMF882x 适配
代码流程解读
从上面的函数流程图来看,涉及到 TMF882x 的代码仅 setupFn()
和 cmd_loop_fn()
,它是驱动库中偏上层的代码,供用户调用;还有一些是库内部的代码,维护 TMF882x 的状态;还有些底层的代码,于硬件通信,需要适配。这三部分的代码各自的范围和所在的文件如下图所示。
适配层的代码都在文件 tmf882x_shim.c 中,涉及到时间函数、GPIO、UART和 I2C。
时间函数
涉及到两个函数 delayInMicroseconds()
和 getSysTick()
,函数实现太简单,直接贴代码。
/**
* @brief 微秒延时
*
* @param wait
*/
void delayInMicroseconds(uint32_t wait)
{
busy_wait_us_32(wait);
}
/**
* @brief 返回自程序开始运行以来的微秒数
*
* @return uint32_t
*/
uint32_t getSysTick()
{
return to_us_since_boot(get_absolute_time());
// return to_ms_since_boot(get_absolute_time()); // 有问题
}
UART 适配
有一些函数打印字符串通过 printf()
就不需要适配,有一些读取字节则需要适配,例如 inputGetKey()
void printConstStr(const char* str)
{
/* casting back to Arduino specific memory */
// Serial.print(reinterpret_cast<const __FlashStringHelper*>(str));
#if DLIB_PICO_STDIO_USB
printf("%s", str);
#else
uart_puts(uart_default, str);
#endif
}
int8_t inputGetKey(char* c)
{
*c = 0;
#if DLIB_PICO_STDIO_USB
* c = stdio_getchar();
return 1;
#else
if (uart_is_readable(uart_default)) {
uart_read_blocking(uart_default, c, 1);
return 1;
}
#endif
return 0;
}
IIC 适配
IIC初始化和反初始化
对应函数 i2cOpen()
和 i2cClose()
void i2cOpen(void* dptr, uint32_t i2cClockSpeedInHz)
{
(void)dptr; // not used here
i2c_init(i2c_default, i2cClockSpeedInHz);
gpio_set_function(PICO_DEFAULT_I2C_SDA_PIN, GPIO_FUNC_I2C);
gpio_set_function(PICO_DEFAULT_I2C_SCL_PIN, GPIO_FUNC_I2C);
gpio_pull_up(PICO_DEFAULT_I2C_SDA_PIN);
gpio_pull_up(PICO_DEFAULT_I2C_SCL_PIN);
// Make the I2C pins available to picotool
bi_decl(bi_2pins_with_func(PICO_DEFAULT_I2C_SDA_PIN, PICO_DEFAULT_I2C_SCL_PIN, GPIO_FUNC_I2C));
}
/**
* @brief I2C 反初始化
*
* @param dptr
*/
void i2cClose(void* dptr)
{
(void)dptr; // not used here
i2c_deinit(i2c_default);
}
IIC收发数据
这里 i2cTxOnly()
发送数据到从器件, i2cRxOnly()
从从器件读取数据,需要适配。而 i2cTxReg()
, i2cRxReg()
和 i2cTxRx()
是直接调用这两个函数,所以无需再修改。
static int8_t i2cTxOnly(uint8_t logLevel, uint8_t slaveAddr, uint8_t regAddr, uint16_t toTx, const uint8_t* txData)
{ // split long transfers into max of 32-bytes: 1 byte is register address, up to 31 are payload.
int8_t res = I2C_SUCCESS;
uint8_t tempBuffer[ARDUINO_MAX_I2C_TRANSFER] = { 0 };
do {
uint8_t tx;
if (toTx > ARDUINO_MAX_I2C_TRANSFER - 1) {
tx = ARDUINO_MAX_I2C_TRANSFER - 1;
} else {
tx = toTx; // less than 31 bytes
}
if (logLevel & TMF8828_LOG_LEVEL_I2C) {
PRINT_STR("I2C-TX (0x");
PRINT_UINT_HEX(slaveAddr);
PRINT_STR(")");
PRINT_STR(" tx=");
PRINT_INT(tx + 1); // +1 for regAddr
PRINT_STR(" 0x");
PRINT_UINT_HEX(regAddr);
if (logLevel >= TMF8828_LOG_LEVEL_DEBUG) {
uint8_t dumpTx = tx;
const uint8_t* dump = txData;
while (dumpTx--) {
PRINT_STR(" 0x");
PRINT_UINT_HEX(*dump);
dump++;
}
}
PRINT_LN();
}
tempBuffer[0] = regAddr;
if (tx) {
for (int i = 0; i < tx; i++) {
tempBuffer[i + 1] = txData[i];
}
}
int realSend = i2c_write_blocking(i2c_default, slaveAddr, (const uint8_t*)tempBuffer, tx+1, false);
if (realSend == (tx+1)) {
res = I2C_SUCCESS;
} else {
res = I2C_ERR_OTHER;
}
toTx -= tx;
txData += tx;
regAddr += tx;
} while (toTx && res == I2C_SUCCESS);
return I2C_SUCCESS;
}
static int8_t i2cRxOnly(uint8_t logLevel, uint8_t slaveAddr, uint16_t toRx, uint8_t* rxData)
{ // split long transfers into max of 32-bytes
uint8_t expected = 0;
uint8_t rx = 0;
int8_t res = I2C_SUCCESS;
if (i2c_read_blocking(i2c_default, slaveAddr, rxData, toRx, false) != toRx) {
if (logLevel & TMF8828_LOG_LEVEL_I2C) {
PRINT_STR("I2C-RX (0x");
PRINT_UINT_HEX(slaveAddr);
PRINT_STR(")");
PRINT_STR(" toRx=");
PRINT_INT(rxData);
if (logLevel >= TMF8828_LOG_LEVEL_DEBUG) {
uint16_t dumpRx = toRx;
uint8_t* dump = rxData;
while (dumpRx--) {
PRINT_STR(" 0x");
PRINT_UINT_HEX(*dump);
dump++;
}
}
PRINT_LN();
}
return I2C_ERR_OTHER;
}
return I2C_SUCCESS;
}
命令行程序验证
串口命令演示一:下载固件,打印寄存器
串口命令演示二:测量
手势识别算法
数据来源
从主流程中可知 cmd_loop_fn()
函数取出测量结果保存到全局变量 g_tmf8828_result
中,然后喂给算法函数 detect_gestures(&g_tmf8828_result)
.
获取检测结果的流程图如下,cmd_loop_fn()
--> lintex_tmf8828ReadResults()
--> lintex_printResults()
保存结果到 g_tmf8828_result
中
数据格式
调用函数 i2cRxReg(i2cAddr, 0x20, 0xa4-0x20, dataBuffer)
把结果保存到 dataBuffer
,它的格式如下:
// #Obj,<i2c_slave_address>,<result_number>,<temperature>,<number_valid_results>,<systick>,<distance_0_mm>,<confidence_0>,<distance_1_mm>,<distance_1>, ...
我定义了一个结构体 tmf8828_result_t
,把这个缓冲区的数据映射到这个结构体变量 g_tmf8828_result
typedef struct {
uint8_t confidence; // 置信度,0~155
uint16_t distance_mm;// 距离,单位:mm
} point_t;
typedef struct {
uint8_t i2cSlaveAddress; // TMF882x 器件的 I2C 地址
uint8_t resultNumber; // 结果编号
uint8_t temperature; // 芯片温度
uint8_t numberValidResults; // 当前结果有效值个数
uint32_t sysTick; // 系统时间
point_t resultPoints[16]; // 最大的结果数是 16
} tmf8828_result_t;
算法设计
通过计算有效点的平均距离变化来判断手势是靠近还是远离。首先检查所有点的置信度,若无有效点则返回无手势;然后计算平均距离,初始化历史数据后,根据距离变化量判断手势类型。
算法流程图
算法代码
#include "dtof_algo.h"
#define CONFIDENCE_THRESHOLD 80 // 置信度阈值(0~155)
#define DISTANCE_CHANGE_THR 25 // 距离变化阈值(mm)
GestureType detect_gestures(const tmf8828_result_t* result) {
static uint16_t last_avg_distance = 0; // 上一帧平均距离
static bool initialized = false; // 初始化标志
uint32_t sum_distance = 0;
uint8_t valid_count = 0;
// 检查所有有效点的置信度
for (uint8_t i = 0; i < result->numberValidResults; i++) {
if (result->resultPoints[i].confidence < CONFIDENCE_THRESHOLD) {
valid_count = 0;
break;
} else {
sum_distance += result->resultPoints[i].distance_mm;
valid_count++;
}
}
// 若无有效点,返回无手势
if (valid_count == 0) {
return GESTURE_NONE;
}
// 计算有效点的平均距离
uint16_t avg_distance = (uint16_t)(sum_distance / valid_count);
// 初始化历史数据
if (!initialized) {
last_avg_distance = avg_distance;
initialized = true;
return GESTURE_NONE;
}
// 计算距离变化量
int16_t delta = (int16_t)avg_distance - (int16_t)last_avg_distance;
// 更新历史数据
last_avg_distance = avg_distance;
// 判断手势类型
if (delta <= -DISTANCE_CHANGE_THR) {
return GESTURE_CLOSE; // 靠近
} else if (delta >= DISTANCE_CHANGE_THR) {
return GESTURE_FAR; // 远离
} else {
return GESTURE_NONE;
}
}
算法效果
- 能识别到靠近和远离,但是不稳定;
- 有可能是办公室环境狭小,导致校准时有噪声;
- 在10~30厘米之内识别效果还可以,如果有大物体在近距离背景中就识别不出来;
功能展示图及说明
板子实物图
实际上 dToF 模块直接插到 RP2040 背后的J3扩展口即可,但是那样 dToF 就在板卡背面,手势检测和GUI界面观察就不好同时进行,为此我通过面包板把 dToF 模块转接到前面,和 RP2040 Game Kit 在同一个平面。
右侧有一个蓝色盒子 H7-Tool 支持 CMSIS-DAP,我用来调试程序,只需要三根线(GND, SCLK, SWD)即可,调试程序非常方便。
演示视频
见B站:
https://www.bilibili.com/video/BV1MF9VYfEQ6/?vd_source=8f2bbf56b70c541bec2ea0b9f102ebee
项目中遇到的难题和解决方法
Pico C SDK 开发环境搭建,尝试过 Keil MDK,最终使用 VSCode 插件
第一次玩 RP2040 开发板,看到官方和论坛中很多人都是用 micropython 环境,我不习惯也不想用,常使用 C 的方式进行开发。找到别人用 Keil MDK 搭建环境,比较繁琐。
几经查找终于找到了 getting-started-with-pico.pdf
文件,详细描述了 VS Code 搭建 Pico C SDK ,步骤详细,轻松搭建。
在.c文件中使用 C++ 对象或者语法
我移植过很多次 LVGL,习惯性地把文件命令为 lv_port_disp.c
(注意,这是C文件), 然后在文件中调用 ST7789_TFT_PICO
库中的对象,但这个是 C++ 语法。
移植 LVGL 的输入设备驱动时,出现了如下的问题,编译 lv_port_disp.c
时报错,文件 ST7789_TFT_graphics.hpp 中 fatal error: cstring: No such file or directory
最后发现是.c 中使用C++语法或者对象。因为这个源文件中有一个语句 ST7789_TFT mLvglTFT;
而 ST7789_TFT
是类类型,在 .c 文件中是编不过的。
解决办法是修改文件后缀为 .cpp,顺利编译。
移植 TMF882x dToF 的 Arduino 库到 Pico C SDK,下载固件超时,原来是 IIC 驱动适配出现了问题
仔细检查函数 i2cTxOnly()
把长数组拆包分包发送数据第一个字节都是寄存器地址,我忘了修改地址,导致每次发送的数据都是写到 TMF8828 同一个地址上。解决办法是每次分包都修改 regAddr
并填写在 tempBuffer[0]
上。
下图中左边是出问题的代码,右边是修复bug的代码。
针对LVGL移植输入设备,通过ADC读取摇杆值,一直能读到数值,但是总是触发 GUI 控件焦点转移,延时解决
虽然ADC读取摇杆值模拟旋转编码器成功了,某些GUI应用可以用,但是如果摇杆一直在左边或者右边,就一直认为动作有效,导致GUI控件焦点一直在跳。
解决办法是驱动中延时读取 ADC 数值,解决了。
手势识别算法
这个是最难的,向AI提问,修改了多个版本,测试了多个版本,有的版本加了滤波器导致手势识别不出来。最后退化到最简单的情形只识别靠近和远离,勉强有一些效果。
对本次竞赛的心得体会
- 这次竞赛认识到自己的不足,算法能力很弱,只能做一些基础的适配性的工作;
- 学会了把AI应用在工作中,提升工作效率;