2024艾迈斯欧司朗竞赛 - 基于dToF的手势识别2048游戏机
该项目使用了TMF8821和RP2040游戏机,实现了基于dToF的手势识别2048游戏机的设计,它的主要功能为:通过dToF传感器实时采集手势数据,并通过神经网络模型识别玩家的手势动作,从而实现控制2048游戏的功能。。
标签
嵌入式系统
zxfeng
更新2025-03-07
91

1.项目背景与意义

随着人工智能和物联网技术的不断发展,智能交互设备的应用已经从传统的鼠标、键盘输入,逐步转向更自然、更直观的交互方式。手势识别作为一种新兴的人机交互方式,因其高效性、直观性和非接触性,受到了广泛关注。尤其是在游戏、智能家居、虚拟现实等领域,手势识别技术为用户提供了全新的互动体验。

传统的手势识别设备大多依赖于昂贵的深度摄像头或复杂的传感器阵列,而dToF(直接飞行时间)传感器以其高精度、低功耗和小型化特性,成为手势识别技术的有力支持。结合强大的计算平台,如RP2040微控制器,可以实现高效的实时数据处理和手势识别,带来更精准的交互体验。

本项目利用RP2040游戏机套件与TMF8821 dToF传感器,设计并实现一款基于手势识别的2048游戏机。该系统将通过dToF传感器实时采集手势数据,并通过神经网络模型识别玩家的手势动作,从而实现控制游戏的功能。

2.硬件部分

硬件包括三个部分:RP2040游戏机、dToF传感器模块、dToF模块转接板。硬件部分组成如下图所示:

2.1 RP2040游戏机

RP2040游戏机是基于树莓派RP2040的嵌入式系统学习平台,USB Type-C供电,采用RP2040作为主控,支持MicroPython、C/C++编程,性能强大。RP2040 配备了两个 ARM Cortex-M0+ 核心,主频可达 133 MHz,并且配备了 264KB 的 SRAM,此外板上还外扩2MB Flash,能够为运行 Tiny ML提供足够的资源。

2.2 TMF8821 dToF 传感器

TMF8821是基于dToF技术的传感器,专为距离测量与深度感知应用设计。该传感器采用了高精度的VCSEL激光源,结合Cortex-M0微处理器与高效的TDC(时间到数字转换器)模块,能够实时处理反射回来的光信号并计算距离信息。

TMF8821支持4x4的深度数据采集模式,即传感器能够通过一次激光脉冲收集4x4个不同区域的数据。这些区域以“zone”(区域)为单位,每个zone包含多个SPAD(单光子雪崩二极管),负责接收来自不同方向的光信号。通过多次采集不同区域的数据,TMF8821能够生成详细的深度图,供系统进一步处理。

为了提高测距精度,TMF8821在采集过程中会进行一定的噪声抑制处理,尤其是针对目标物体的散射影响。传感器内置了降噪算法(例如去散射功能),能够减少背景干扰对距离测量的影响。此外,TMF8821支持使用软件门限设置,优化距离测量精度,尤其在复杂的环境中,能够提高数据的可靠性。

2.3 dToF模块转接板

由于TMF8821 dToF传感器模块安装在RP2040游戏机的背部,传感器朝向与屏幕方向相反,无法实现正面的手势采集功能,无法直观地通过手势与屏幕内容进行交互。为了解决这一问题,我们特别设计了一款专用转接板,从而使得dToF传感器的采集方向与屏幕显示方向一致。这样一来,就能够在屏幕正前方轻松完成手势操作,同时实时查看反馈效果,极大提升了用户体验和设备的交互便捷性。硬件如下图所示:

3软件设计

3.1 整体方案与架构设计

软件方案如下图所示:

软件部分开发主要分为以下五个部分:

  • RP2040游戏机适配Arduino
  • RP2040游戏机显示屏驱动
  • dToF传感器驱动
  • 神经网络训练与部署
  • 游戏功能设计与控制

3.2 各部分功能设计

3.2.1 RP2040游戏机适配Arduino

虽然Arduino支持RP2040开发,但是由于RP2040游戏机并没有适配Arduino,不能够直接使用Arduino进行开发。因此我们基于 Arduino Mbed OS RP2040 Boards,过修改其 pins_arduino.h 文件,来适配RP2040游戏机。

3.2.1.1 添加RP2040开发板

首先需要确保 Arduino Mbed OS RP2040 Boards 已正确安装。在 Arduino IDE 左侧边栏中,打开 开发板管理器,搜索 2040,然后找到 Arduino Mbed OS RP2040 Boards 进行安装。

安装完成后,在上侧工具栏中点击选择开发板,就能够搜索到Raspberry Pi Pico开发板了,但此时由于它与RP2040游戏机的管脚定义有区别,因此不能够直接使用。

3.2.1.2 修改引脚配置

由于Game RP2040 Kit开发板的硬件设计与 Raspberry Pi Pico 不同,所以并不能直接选择该开发板进行程序的编译下载,因此我们还需要对其默认的引脚配置进行修改,才能够进行编程和烧录。Arduino中关于引脚定义在 pins_arduino.h 中。

我们前面安装的 RASPBERRY_PI_PICO 开发板文件默认保存在以下路径:

C:\Users\用户名\AppData\Local\Arduino15\packages\arduino\hardware\mbed_rp2040\4.2.1\variants\RASPBERRY_PI_PICO

在其中找到 pins_arduino.h 文件后,使用文本编辑器或 IDE(如 VS Code)打开它。

在 pins_arduino.h 文件中,根据 RP2040游戏机 的硬件原理图,对引脚定义进行如下修改:

  1. LED

开发板带有一个可控绿色 LED,连接至 IO4,外部未接上拉,高电平有效。

// LEDs
// ----
#define PIN_LED (4u)
#define LED_BUILTIN PIN_LED
  1. LCD显示屏

开发板配备 ST7789 驱动的显示屏,分辨率240x240。

//LCD
#define LCD_DC (1u)
#define LCD_RST (0u)
#define LCD_SCK (2u)
#define LCD_SDA (3u)
  1. IMU传感器

开发板上的IMU型号为MMA7660FC,通过I2C接口与单片机进行通信,并且包含一个中断引脚INT。

//IMU
#define IMU_INT (9u)
  1. Button按键

开发板共有四个按键,低电平触发。

//Buttons
#define BUTTON_A (6u)
#define BUTTON_B (5u)
#define BUTTON_START (7u)
#define BUTTON_SELECT (8u)
  1. 红外传感器

开发板还包含一个红外接收头和一个红外发射头,引脚定义如下:

//IR
#define IR_TX (24u)
#define IR_RX (25u)
  1. 蜂鸣器

开发板上的蜂鸣器可通过PWM控制,引脚定义如下:

//Buzz
#define BUZZER (23u)
  1. SPI接口

// SPI
#define PIN_SPI_MISO (12u)
#define PIN_SPI_MOSI (15u)
#define PIN_SPI_SCK (14u)
#define PIN_SPI_SS (13u)
  1. I2C接口

开发板上共有两组I2C,其中I2C0引出到背面排针,I2C1与IMU相连,引脚定义如下:

// Wire
#define PIN_WIRE0_SDA (16u)
#define PIN_WIRE0_SCL (17u)
#define PIN_WIRE1_SDA (10u)
#define PIN_WIRE1_SCL (11u)

#define WIRE_HOWMANY (2)
#define I2C_SDA (digitalPinToPinName(PIN_WIRE0_SDA))
#define I2C_SCL (digitalPinToPinName(PIN_WIRE0_SCL))
#define I2C_SDA1 (digitalPinToPinName(PIN_WIRE1_SDA))
#define I2C_SCL1 (digitalPinToPinName(PIN_WIRE1_SCL))

3.2.2 RP2040游戏机显示屏驱动

TFT_eSPI是一个为Arduino平台设计的高效图形库,专门用于驱动TFT-LCD显示屏。这个库通过SPI协议与显示屏进行通信,并提供丰富的图形操作功能,如绘制基本图形、显示文本、控制颜色等。TFT_eSPI库支持多种常见的显示屏驱动芯片,其中包括ILI9341、ST7735和ST7789等。为了使得RP2040游戏机正确使用TFT_eSPI库,还需要进行一些配置。

3.2.2.1 安装TFT_eSPI库

首先,需要确保在Arduino IDE中安装了TFT_eSPI库。安装方法如下,打开Arduino,在侧边栏的Libraries中搜索TFT_eSPI库,并安装。

安装完成后,TFT_eSPI库将会被下载并自动存储在Arduino库的默认路径下。库文件的路径一般位于:

C:\Users\用户名\Documents\Arduino\libraries\TFT_eSPI

可以在该路径下找到TFT_eSPI库的所有相关文件,特别是User_Setup.h文件,它是配置库的核心文件。

3.2.2.2 配置User_Setup.h文件

TFT_eSPI库的配置主要通过User_Setup.h文件来完成。在这一步中,我们将根据ST7789屏幕的硬件特性对配置文件进行调整。User_Setup.h文件包含了许多重要的设置项,下面将重点介绍如何配置这些选项。

  1. 选择驱动芯片

由于我们的驱动芯片为ST7789,因此我们需要在User_Setup.h中选择正确的驱动芯片。在该文件中,找到ST7789_DRIVER的定义,并取消注释以启用它:

该设置告诉库使用ST7789芯片来驱动显示屏,确保后续的初始化操作可以顺利进行。

  1. 定义屏幕尺寸

接下来,我们还需要设置屏幕的分辨率。ST7789显示屏有两种常见分辨率:240x240和240x320。开发板上使用的是240x240分辨率的屏幕,因此需要配置如下:

  1. 配置SPI引脚

TFT_eSPI库使用SPI协议与显示屏通信。在RP2040 Game Kit开发板上,ST7789显示屏的连接引脚如下:

根据上图引脚接线,在User_Setup.h文件中配置这些引脚如下

其中没有用到MISO和CS片选,因此这两条信号线可以注释掉。

  1. 选择字体及优化存储占用

TFT_eSPI库支持多种字体,但由于字体会占用Flash存储空间,可以根据需要选择加载不同的字体。例如,如果只需要显示较小的字体,可以选择以下配置来节省存储空间:

#define LOAD_FONT2  // 加载16像素的字体
#define LOAD_FONT4 // 加载26像素的字体

也可以根据需要启用更多的字体。

3.2.3 dToF传感器驱动

dToF传感器通过I2C与RP2040游戏机通信。其使用步骤大致如下:

  1. 传感器上电
  2. 下载固件
  3. 传感器配置
  4. 启动测量

传感器上电

首先,拉高传感器的使能脚。

之后,向0xe0寄存器写入0x01。

然后调用tmf8828IsCpuReady函数循环读使能寄存器0xe0,直至返回0x41。

tmf8828Enable( &(tmf8828[0]) );
delayMicroseconds( ENABLE_TIME_MS * 1000 );
tmf8828Wakeup( &(tmf8828[0]) );
tmf8828IsCpuReady( &(tmf8828[0]), CPU_READY_TIME_MS );

下载固件

固件下载流程如下:

int8_t tmf8828DownloadFirmware ( tmf8828Driver * driver, uint32_t imageStartAddress, const uint8_t * image, int32_t imageSizeInBytes ) 
{
int32_t idx = 0;
int8_t stat = BL_SUCCESS_OK;
uint8_t chunkLen;

stat = tmf8828BootloaderSetRamAddr( driver, imageStartAddress );
idx = 0; // start again at the image begin
while ( stat == BL_SUCCESS_OK && idx < imageSizeInBytes )
{
for( chunkLen=0; chunkLen < BL_MAX_DATA_PAYLOAD && idx < imageSizeInBytes; chunkLen++, idx++ )
{
dataBuffer[BL_HEADER + chunkLen] = pgm_read_byte( (uint32_t)(image + idx) ); // read from code memory into local ram buffer
}
stat = tmf8828BootloaderWriteRam( driver, chunkLen );
}
if ( stat == BL_SUCCESS_OK )
{
stat = tmf8828BootloaderRamRemap( driver, TMF8828_COM_APP_ID__application ); // if you load a test-application this may have another ID
if ( stat == BL_SUCCESS_OK )
{
return stat;
}
}
return stat;
}

传感器配置

设定测量周期、SPAD类型等参数

tmf8828Configure( &(tmf8828[0]), configPeriod, configKiloIter, configSpadId, configLowThreshold, configHighThreshold, configPersistance, configInterruptMask, dumpHistogramOn );

设置校准数据

tmf8828SetStoredFactoryCalibration( &(tmf8828[0]), tmf882x_calib );  //cali tmf8821

启动测量

向寄存器0x08写入0x10。

int8_t tmf8828StartMeasurement ( tmf8828Driver * driver ) 
{
tmf8828ResetClockCorrection( driver ); // clock correction only works during measurements
dataBuffer[0] = TMF8828_COM_CMD_STAT__cmd_measure;
i2cTxReg(driver->i2cSlaveAddress, TMF8828_COM_CMD_STAT, 1, dataBuffer ); // instruct device to load page
return tmf8828CheckRegister( driver, TMF8828_COM_CMD_STAT, TMF8828_COM_CMD_STAT__stat_accepted, 1, APP_CMD_MEASURE_TIMEOUT_MS ); // check that measure command is accepted
}

3.2.4 神经网络训练与部署

我们基于Edge Impulse设计了一个神经网络模型,使RP2040游戏机能够通过dToF模块采集到的数据准确实现手势分类。

3.2.4.1 数据集准备

在训练模型前,我们首先需要进行数据集的准备。Edge Impulse提供了非常简单的数据采集方法,可以直接调用RP2040游戏机进行数据采集。

首先在Edge Impulse网站上新建一个工程,之后编写Arduino数据采集代码,相关代码如下:

#define UART_BAUD_RATE              115200
#define I2C_CLK_SPEED 400000

#define FREQUENCY_HZ 50+1
#define INTERVAL_MS (1000 / (FREQUENCY_HZ))

static unsigned long last_interval_ms = 0;

void loop ( )
{
uint8_t intStatus = 0;

if (millis() > last_interval_ms + INTERVAL_MS) {
last_interval_ms = millis();

intStatus = tmf8828GetAndClrInterrupts( &(tmf8828[0]), TMF8828_APP_I2C_RESULT_IRQ_MASK ); // always clear also the ANY interrupt
if ( intStatus & TMF8828_APP_I2C_RESULT_IRQ_MASK ) // check if a result is available (ignore here the any interrupt)
{
tmf8828ReadResults( &(tmf8828[0]) );
}

Serial.print(distance_data[0][0]);
Serial.print('\t');
Serial.print(distance_data[0][1]);
Serial.print('\t');
Serial.print(distance_data[0][2]);
Serial.print('\t');
Serial.print(distance_data[0][3]);
Serial.print('\t');
Serial.print(distance_data[0][4]);
Serial.print('\t');
Serial.print(distance_data[0][5]);
Serial.print('\t');
Serial.print(distance_data[0][6]);
Serial.print('\t');
Serial.print(distance_data[0][7]);
Serial.print('\t');
Serial.println(distance_data[0][8]);
}
}

在程序中我们定义了信号的采样周期,并且按照每次一行,每个数据使用TAB分开的格式向串口发送数据。

在电脑端,我们使用Edge Impulse CLI工具从串口采集信号并上传至Edge Impulse:

edge-impulse-data-forwarder

之后,在项目网页右侧选中Data acquisition,进行数据采集

3.2.4.2 模型训练

当采集够一定数量的样本数据后,我们就能够进行模型训练了。我这里分别采集了四种数据各30个,分别为(down、idle、right、left)。

网络模型结构如下:

当训练完成后,在右侧即可看到模型的准确度、推理速度等相关参数。

3.2.4.3 模型部署

将上述模型编译成Arduino库并添加至Arduino,并编写板端推理程序。

首先需要将库的头文件包含进来,并定义推理结果打印函数

#include <hand_gesture_inferencing.h>

void print_inference_result(ei_impulse_result_t result) {

// Print how long it took to perform inference
ei_printf("Timing: DSP %d ms, inference %d ms, anomaly %d ms\r\n",
result.timing.dsp,
result.timing.classification,
result.timing.anomaly);

ei_printf("Predictions:\r\n");
for (uint16_t i = 0; i < EI_CLASSIFIER_LABEL_COUNT; i++) {
ei_printf(" %s: ", ei_classifier_inferencing_categories[i]);
ei_printf("%.5f\r\n", result.classification[i].value);
}

// Print anomaly result (if it exists)
#if EI_CLASSIFIER_HAS_ANOMALY == 1
ei_printf("Anomaly prediction: %.3f\r\n", result.anomaly);
#endif

}

之后采集数据

signal_t signal;
ei_impulse_result_t result;
int err = numpy::signal_from_buffer(features, EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE, &signal);
if (err != 0) {
ei_printf("Failed to create signal from buffer (%d)\n", err);
return;
}

其中features变量就是传感器采集到的原始数据。

最后执行模型推理,并打印推理结果

EI_IMPULSE_ERROR res = run_classifier(&signal, &result, true);

if (err != EI_IMPULSE_OK) {
ei_printf("ERR:(%d)\r\n", err);
return;
}
print_inference_result(result);

其中run_classifier函数的最后一个参数为debug参数,如果设置为true,则会通过串口打印程序的调试数据。

3.2.5 游戏功能设计与控制

2048游戏的核心逻辑是控制一个4x4的棋盘,玩家通过操作来合并相同数字的方块,最终目标是得到更大的数字。每一步操作,都会生成一个新的数字(2或4),并将其放置在棋盘的空白位置上。

3.2.5.1 棋盘数组和游戏状态

我们用一个4x4的二维数组 game_arr 来表示游戏的棋盘,数组中的每个元素代表一个格子的数字,初始时棋盘为空,所有位置的数字都为0。

int game_arr[4][4] = {
{0,0,0,0},
{0,0,0,0},
{0,0,0,0},
{0,0,0,0}
};

此外,我们定义了 game_failedgame_score 变量来记录游戏状态和分数。

bool game_failed = false;
unsigned int game_score = 0;

3.2.5.2 随机生成数字与游戏结束判断

游戏中的每一步都会生成一个新的数字(2或4),并将其放置到棋盘的一个空位置上。我们通过 game_radom_and_over() 函数来完成此操作。它首先检查是否还有空位,如果没有空位且无法进行合并,则判定游戏失败。

其中,isEmpty() 函数用于检查棋盘上是否还有空位。它会遍历棋盘,检查是否存在 0,如果存在空位返回1,否则返回0。

int game_radom_and_over() {
randomSeed(analogRead(0)); // 使用模拟引脚 A0 的噪声值作为种子
// 没有空位
bool moveable = false;
if (!isEmpty()) {
//从右向左检测是否有可合成的
for (int x = 0; x < 4; x++) {
for (int y = 3; y >= 1; y--) {
if (game_arr[x][y - 1]==game_arr[x][y] ) {
moveable = true;
break;
}
}
}
//从下向上判断是否有可合成的
for (int y = 0; y < 3; y++) {
for (int x = 3; x >= 1; x--) {
if (game_arr[x - 1][y]==game_arr[x][y]) {
moveable = true;
break;
}
}
}
game_failed = !moveable;
return 0;
}
int x, y;
do {
x = random(1, 100) % 4;
y = random(1, 100) % 4;
} while (game_arr[x][y] != 0); // 直到找到一个空位
int newNumber = (random(1, 100) % 10 < 9) ? 2 : 4; // 90% 生成 2,10% 生成 4
game_arr[x][y] = newNumber;
}

static int isEmpty() {
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
if (game_arr[i][j] == 0) {
return 1; // 找到空位
}
}
}
return 0; // 没有空位
}

3.2.5.3 控制逻辑

游戏的核心控制逻辑包括四个方向的移动操作(上、下、左、右)。例如,game_left() 函数实现了棋盘向左移动的操作。每次移动后,棋盘上的相同数字会合并成一个新的数字,并且会更新分数。

game_left() 函数为例,在该函数中,棋盘的每一行都会向左移动。当数字向左移动时,如果发现相邻的两个数字相等,它们会合并成一个数字,合并后的数字会加到当前分数中,并把原来的两个位置清空。

void game_left() {
for (int x = 0; x < 4; x++) {
//移动三次
for (int i = 0; i <= 3; i++) {
//从右向左移动
for (int y = 3; y >= 1; y--) {
if (game_arr[x][y - 1] == 0 && game_arr[x][y] != 0) {
game_arr[x][y - 1] = game_arr[x][y];
game_arr[x][y] = 0;
}
}
}
//从左向右相同的相加并计分
for (int y = 1; y <= 3; y++) {
if (game_arr[x][y - 1] == game_arr[x][y]) {
game_score += (game_arr[x][y - 1] = 2 * game_arr[x][y - 1]);
game_arr[x][y] = 0;
}
}
//移动三次
for (int i = 0; i <= 3; i++) {
//从右向左移动
for (int y = 3; y >= 1; y--) {
if (game_arr[x][y - 1] == 0 && game_arr[x][y] != 0) {
game_arr[x][y - 1] = game_arr[x][y];
game_arr[x][y] = 0;
}
}
}
}
game_radom_and_over();
}

上述 game_left() 函数执行了三个步骤:

  1. 数字移动:先从右向左移动数字,把空位填充起来。
  2. 数字合并:从左向右检查相邻的两个数字是否相等,如果相等则合并,并增加分数。
  3. 再移动:合并后可能会留下空位,因此我们需要再次执行移动,确保所有数字都按预期排列。

game_right()game_up()game_down() 的操作逻辑与此类似

,相关代码如下:

void game_right() {
for (int x = 0; x < 4; x++) {
//移动三次
for (int i = 0; i <= 3; i++) {
//从左向右移动
for (int y = 1; y <= 3; y++) {
if (game_arr[x][y - 1] != 0 && game_arr[x][y] == 0) {
game_arr[x][y] = game_arr[x][y - 1];
game_arr[x][y - 1] = 0;
}
}
}
//从右向左加并计算分数
for (int y = 3; y >= 1; y--) {
if (game_arr[x][y - 1] == game_arr[x][y]) {
game_score += (game_arr[x][y] = 2 * game_arr[x][y]);
game_arr[x][y - 1] = 0;
}
}
//移动三次
for (int i = 0; i <= 3; i++) {
//从左向右移动
for (int y = 1; y <= 3; y++) {
if (game_arr[x][y - 1] != 0 && game_arr[x][y] == 0) {
game_arr[x][y] = game_arr[x][y - 1];
game_arr[x][y - 1] = 0;
}
}
}
}
game_radom_and_over();
}

void game_down() {
for (int y = 0; y <= 3; y++) {
//移动三次
for (int i = 0; i <= 3; i++) {
//从上向下移动
for (int x = 1; x <= 3; x++) {
if (game_arr[x - 1][y] != 0 && game_arr[x][y] == 0) {
game_arr[x][y] = game_arr[x - 1][y];
game_arr[x - 1][y] = 0;
}
}
}
//从下向上加并计算分数
for (int x = 3; x >= 1; x--) {
if (game_arr[x][y]==game_arr[x-1][y]) {
game_score += (game_arr[x][y] = 2*game_arr[x][y]);
game_arr[x-1][y] = 0;
}
}
//移动三次
for (int i = 0; i <= 3; i++) {
//从上向下移动
for (int x = 1; x <= 3; x++) {
if (game_arr[x - 1][y] != 0 && game_arr[x][y] == 0) {
game_arr[x][y] = game_arr[x - 1][y];
game_arr[x - 1][y] = 0;
}
}
}
}
game_radom_and_over();
}

void game_up() {
for (int y = 0; y <= 3;y++) {
//移动三次
for (int i = 0; i <= 3; i++) {
//从下向上移动
for (int x = 3; x >= 1; x--) {
if (game_arr[x - 1][y] == 0 && game_arr[x][y] != 0) {
game_arr[x - 1][y] = game_arr[x][y];
game_arr[x][y] = 0;
}
}
}
//从上向下相加并计算分数
for (int x = 1; x <= 3; x++) {
if (game_arr[x - 1][y] == game_arr[x][y]) {
game_score += (game_arr[x - 1][y] = 2 * game_arr[x - 1][y]);
game_arr[x][y] = 0;
}
}
//移动三次
for (int i = 0; i <= 3; i++) {
//从下向上移动
for (int x = 3; x >= 1; x--) {
if (game_arr[x - 1][y] == 0 && game_arr[x][y] != 0) {
game_arr[x - 1][y] = game_arr[x][y];
game_arr[x][y] = 0;
}
}
}
}
game_radom_and_over();
}

3.2.5.4 游戏重置

此外,还定义了重置函数,可以调用该函数来重新开始游戏。game_restart() 函数会清空棋盘,并重置分数和游戏状态。

void game_restart() {
// 游戏棋盘清零
for(int x = 0; x < 4; x++) {
for(int y = 0; y < 4; y++) {
game_arr[x][y] = 0;
}
}
game_score = 0;//分数统计清零
game_failed = false;//失败状态恢复
}

3.2.5.5 控制函数

之后定义游戏控制函数,通过 game_control() 函数,我们可以根据用户输入控制游戏的移动方向。该函数从串口读取字符并执行相应的操作。

void game_control()
{
switch (currentControl) {
case RIGHT:
game_right();
break;
case LEFT:
game_left();
break;
case UP:
game_up();
break;
case DOWN:
game_down();
break;
default:
break;
}
}

3.2.5.6 屏幕更新

定义屏幕更新函数update_screen() ,用于更新LCD屏幕上的棋盘和分数。每次游戏状态发生变化后,我们调用这个函数来重新绘制棋盘。

void update_screen()
{
tft.fillRect(0, 0, 70, 20, TFT_BLACK);
switch (currentControl) {
case RIGHT:
tft.drawString("right",0,0,4);
break;
case LEFT:
tft.drawString("left",0,0,4);
break;
case UP:
tft.drawString("up",0,0,4);
break;
case DOWN:
tft.drawString("down",0,0,4);
break;
default:
break;
}

tft.drawNumber(game_score,140,0,4);
for(int i=0;i<4;i++)
{
for(int j=0;j<4;j++)
{
tft.fillRect(i*60, 40+50*j+8, 40, 20, TFT_BLACK);
tft.drawNumber(game_arr[j][i] ,i*60,40+50*j+8, 4);
}
}
}

3.2.5.7 手势识别控制

我们使用currentControl变量来指示当前移动的方向,手势 左右移动 会改变移动的方向,手势 按下 则是执行当前方向的移动。

result_maxValue = result.classification[0].value; // 假设第一个元素为最大值
result_maxIndex = 0;
for (int i = 0; i < EI_CLASSIFIER_LABEL_COUNT; i++) {
if (result.classification[i].value > result_maxValue) { // 如果当前元素大于最大值
result_maxValue = result.classification[i].value; // 更新最大值
result_maxIndex = i; // 更新最大值的索引
}
}
ei_printf("currentControl:%d,label:%s,%d,score:%.5f\r\n", currentControl,ei_classifier_inferencing_categories[result_maxIndex],result_maxIndex,result_maxValue);

if(result_maxIndex == 2)
{
currentControl = (currentControl + 4 - 1)%4;
update_screen();
}else if(result_maxIndex == 3)
{
currentControl = (currentControl + 1)%4;
update_screen();
}else if(result_maxIndex == 0)
{
game_control();
update_screen();
}

4 功能展示

4.1 游戏展示

系统上电后,屏幕初始化,在顶端显示初始的移动方向以及当前得分,屏幕中间显示的是当前的棋盘状态。可以看到,初始的移动方向为”右“,初始得分为0,并且当前棋盘上显示的均为0。

当手势输入”down“,即按下时,游戏开始,出现第一个数字,之后就能够通过左右挥动手掌,调整移动的方向,并向下按下执行移动操作。

之后,通过向左两次挥动手掌,可以看到左上角的移动方向由right变成了left。

4.2 串口打印展示

在串口中,能够实时输出当前的识别结果、置信度以及当前的移动方向。

从串口打印数据可以看到,当没有任何动作时,识别结果为idle,并且currentControl(移动方向)不会发生改变。当识别为left时,移动方向会-1,从0变成了3(因为只有4个方向,currentControl限制在0-3),当识别结果为right时,currentControl又从3变为了0。

5 心得体会

非常感谢硬禾提供的支持与资源,这让我有机会深入接触并学习到dToF技术。通过本次项目,我对dToF的工作原理有了更深刻的理解,同时掌握了其使用方法和驱动开发技巧。此外,我还学会了如何在嵌入式设备上部署神经网络,这一过程不仅提升了我的技术能力,也让我对嵌入式人工智能的应用有了全新的认识。这次经历让我深切体会到理论与实践相结合的重要性,为未来在相关领域的探索奠定了坚实的基础。

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