零知开源——基于STM32F103RBT6与ADXL362三轴加速度计的体感迷宫游戏设计与实现

✔零知IDE 是一个真正属于国人自己的开源软件平台,在开发效率上超越了Arduino平台并且更加容易上手,大大降低了开发难度。零知开源在软件方面提供了完整的学习教程和丰富示例代码,让不懂程序的工程师也能非常轻而易举的搭建电路来创作产品,测试产品。快来动手试试吧!

✔访问零知实验室,获取更多实战项目和教程资源吧!

www.lingzhilab.com

目录

一、硬件系统设计

[1.1 硬件清单](#1.1 硬件清单)

[1.2 接线方案](#1.2 接线方案)

[1.3 硬件连接图](#1.3 硬件连接图)

[1.4 接线实物图](#1.4 接线实物图)

二、软件系统设计

[2.1 头文件与引脚定义](#2.1 头文件与引脚定义)

[2.2 对象初始化与全局变量](#2.2 对象初始化与全局变量)

[2.3 核心函数详解](#2.3 核心函数详解)

[2.4 完整代码](#2.4 完整代码)

三、操作结果展示

[3.1 操作流程](#3.1 操作流程)

[3.2 显示屏界面分布](#3.2 显示屏界面分布)

[3.3 视频演示](#3.3 视频演示)

四、ADXL362的SPI接口技术

[4.1 SPI通信方式](#4.1 SPI通信方式)

[4.2 SPI数据传输和接收机制](#4.2 SPI数据传输和接收机制)

[4.3 加速度数据格式和解析](#4.3 加速度数据格式和解析)

五、常见问题解答 (FAQ)

[Q1: 为什么我的串口打印是乱码?](#Q1: 为什么我的串口打印是乱码?)

[Q2: 小球控制不灵敏或反向?](#Q2: 小球控制不灵敏或反向?)

[Q3: 小球穿墙bug如何处理?](#Q3: 小球穿墙bug如何处理?)


(1)项目概述

本项目是一个有趣的嵌入式体感交互游戏。核心功能是通过一个三维加速度传感器ADXL362来检测开发板的倾斜姿态,从而控制屏幕上的小球在迷宫内移动,躲避墙壁,最终抵达随机生成的目标点。项目综合了传感器数据采集、数据处理、图形显示、碰撞检测等多个嵌入式开发的关键知识点

(2)项目亮点

>通过倾斜ADXL362传感器进行体感交互操作游戏

>实时绘制迷宫、小球和目标点,视觉效果清晰

>系统启动时自动校准加速度计,消除静态误差

>采用局部刷新策略,避免整屏刷新带来的闪烁

(3)项目难点及解决方案

问题1描述:简单的中心点检测会导致小球"嵌"入墙壁。

解决方案:

采用多点检测法,同时检测小球的上、下、左、右四个边缘点以及四个角点,确保任何部位触墙都能被准确识别。

问题2描述:传感器原始数据存在噪声和偏移,导致小球无故移动。

解决方案:

软件死区: 设置一个阈值 (deadZone),忽略微小的加速度值。

开机校准: 在 setup() 阶段读取多次数据求平均值,将后续读数减去此平均值以消除零偏。

一、硬件系统设计

1.1 硬件清单

组件 数量 说明
零知标准板 1 主控制器,基于STM32F103RBT6。
ST7789 TFT屏 1 1.3英寸,240x240分辨率,SPI接口。
ADXL362加速度计 1 超低功耗,SPI/I2C接口,本项目使用SPI。
杜邦线 若干 用于连接各模块。
Micro USB线 1 为开发板供电和程序下载。

1.2 接线方案

请严格按照代码中的引脚定义进行连接:

(1)ADXL362 传感器接线

ADXL362引脚 零知标准板引脚 功能
VCC 3.3V 电源
GND GND 地
CS 10 片选(SPI)
MOSI 11 (硬件SPI) SPI主出从入
MISO 12 (硬件SPI) SPI主入从出
SCLK 13 (硬件SPI) SPI时钟

(2)ST7789显示屏接线

ST7789引脚 零知标准板引脚 功能
VCC 3.3V 或 5V* 电源 (*视屏幕型号而定)
GND GND 地
CS 6 片选(软件SPI)
DC 2 数据/命令控制
RST 4 复位
MOSI 8 软件SPI数据线
SCK 7 软件SPI时钟线
BL 3.3V 背光(可选)

PS:代码中TFT屏使用了软件SPI (引脚7, 8),而ADXL362使用了硬件SPI(引脚10,11,12,13)

1.3 硬件连接图

1.4 接线实物图

二、软件系统设计

2.1 头文件与引脚定义

BALL_RADIUS: 决定了小球的大小和碰撞检测范围。

MAZE_CELL_SIZE: 决定了迷宫的精细度。增大它会使迷宫更简单,小球移动空间更大。

MAZE_WIDTH & MAZE_HEIGHT: 由屏幕分辨率和单元格大小自动计算得出。

cpp 复制代码
#include <SPI.h>
#include <Wire.h> // 本项目未使用I2C,可移除
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <ADXL362.h>

// ST7789 显示屏引脚定义 (使用软件SPI)
#define TFT_CS   6
#define TFT_RST  4
#define TFT_DC   2
#define TFT_MOSI 8
#define TFT_SCLK 7

// 定义显示屏参数
#define SCREEN_WIDTH  240
#define SCREEN_HEIGHT 240

// 颜色定义 (ST77XX_ 是Adafruit库预定义的颜色)
#define BACKGROUND  ST77XX_BLACK
#define TEXT_COLOR  ST77XX_WHITE
#define WALL_COLOR  ST77XX_BLUE
#define BALL_COLOR  ST77XX_RED
#define TARGET_COLOR ST77XX_GREEN
#define PATH_COLOR ST77XX_WHITE // 路径颜色,通常为背景色或浅色

// 游戏参数
#define BALL_RADIUS 8 // 小球像素半径
#define MAZE_CELL_SIZE 20 // 每个迷宫单元格的像素大小
#define MAZE_WIDTH (SCREEN_WIDTH / MAZE_CELL_SIZE) // 迷宫宽度(单元格数)
#define MAZE_HEIGHT (SCREEN_HEIGHT / MAZE_CELL_SIZE) // 迷宫高度(单元格数)

2.2 对象初始化与全局变量

maze数组: 这是一个二维数组,完全定义了迷宫的布局。修改这个数组就可以创造全新的关卡。1代表墙,0代表可通行的路径。

cpp 复制代码
Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK, TFT_RST);
ADXL362 xl;

// 小球和目标的初始位置(位于迷宫左上角和右下角路径的中央)
int ballX = MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2;
int ballY = MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2;
int targetX = (MAZE_WIDTH - 2) * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2;
int targetY = (MAZE_HEIGHT - 2) * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2;

// 迷宫地图 (0=路径, 1=墙)
byte maze[MAZE_HEIGHT][MAZE_WIDTH] = { ... }; // 此处省略数组内容

// 加速度校准值
int16_t calibX = 0;
int16_t calibY = 0;
int16_t calibZ = 0;

2.3 核心函数详解

(1)calibrateAccelerometer() - 传感器校准

cpp 复制代码
void calibrateAccelerometer() {
  Serial.println("Calibrating accelerometer...");
  
  // 读取多次样本并取平均值
  long sumX = 0, sumY = 0, sumZ = 0;
  int samples = 20;
  
  for (int i = 0; i < samples; i++) {
    xl.readXYZTData(accelX, accelY, accelZ, temp);
    sumX += accelX;
    sumY += accelY;
    sumZ += accelZ;
    delay(10);
  }
  
  calibX = sumX / samples;
  calibY = sumY / samples;
  calibZ = sumZ / samples;
}

(2)clearBall() - 优化的小球清除

避免了重绘整个屏幕或整个单元格,极大减少了刷新时间,消除了画面闪烁,精确地恢复小球所占区域的原始迷宫图案。

cpp 复制代码
void clearBall(int x, int y) {
  // 只清除小球区域,不重新绘制整个迷宫
  // 创建一个临时缓冲区来存储小球区域的背景
  uint16_t bgBuffer[(BALL_RADIUS*2+2) * (BALL_RADIUS*2+2)];
  
  // 确定要清除的区域
  int clearX1 = max(0, x - BALL_RADIUS - 1);
  int clearY1 = max(0, y - BALL_RADIUS - 1);
  int clearX2 = min(SCREEN_WIDTH - 1, x + BALL_RADIUS + 1);
  int clearY2 = min(SCREEN_HEIGHT - 1, y + BALL_RADIUS + 1);
  int width = clearX2 - clearX1 + 1;
  int height = clearY2 - clearY1 + 1;
  
  // 绘制正确的背景
  for (int py = clearY1; py <= clearY2; py++) {
    for (int px = clearX1; px <= clearX2; px++) {
      // 计算像素到小球中心的距离
      int dx = px - x;
      int dy = py - y;
      int distance = dx*dx + dy*dy;
      
      // 如果在小球半径范围内,则绘制正确的背景
      if (distance <= (BALL_RADIUS+1)*(BALL_RADIUS+1)) {
        // 获取像素所在的迷宫单元格
        int cellX = px / MAZE_CELL_SIZE;
        int cellY = py / MAZE_CELL_SIZE;
        
        // 根据单元格类型选择颜色
        uint16_t color = (maze[cellY][cellX] == 1) ? WALL_COLOR : PATH_COLOR;
        
        // 绘制像素
        tft.drawPixel(px, py, color);
      }
    }
  }
}

(3)updateBallPosition() - 控制逻辑

deadZone: 死区范围。绝对值小于50的加速度读数将被视为噪声并忽略,防止小球轻微抖动。

cpp 复制代码
void updateBallPosition() {
  const int deadZone = 50; // 死区阈值,过滤微小抖动

  xl.readXYZTData(accelX, accelY, accelZ, temp); // 读取数据
  accelX -= calibX; // 应用校准
  accelY -= calibY;

  int moveX = 0;
  int moveY = 0;

  // 映射加速度值到移动速度
  if (abs(accelX) > deadZone) {
    moveX = map(accelX, -200, 200, -5, 5); // 映射范围可调整
  }
  if (abs(accelY) > deadZone) {
    moveY = map(accelY, -200, 200, -5, 5);
  }

  ballX = constrain(ballX + moveX, BALL_RADIUS, SCREEN_WIDTH - BALL_RADIUS);
  ballY = constrain(ballY - moveY, BALL_RADIUS, SCREEN_HEIGHT - BALL_RADIUS); // 注意Y轴是减
}

(4)checkCollision() 与 checkPointCollision() - 碰撞检测

综合检查四边中点 和四个角点共8个点是否进入墙壁单元格。这种多点检测法比只检查中心点要准确得多,能有效防止卡墙和穿墙的Bug

cpp 复制代码
bool checkCollision() {
  // 检查小球边缘的四个中点
  int top = ballY - BALL_RADIUS;
  int cellYTop = top / MAZE_CELL_SIZE;
  if (maze[cellYTop][ballX/MAZE_CELL_SIZE] == 1) return true;
  // ... 检查bottom, left, right ...

  // 检查四个角点
  if (checkPointCollision(ballX - BALL_RADIUS, ballY - BALL_RADIUS)) return true; // 左上角
  // ... 检查其他三个角 ...
  return false;
}

bool checkPointCollision(int x, int y) {
  int cellX = x / MAZE_CELL_SIZE;
  int cellY = y / MAZE_CELL_SIZE;
  // 检查是否撞墙
  return (maze[cellY][cellX] == 1);
}

2.4 完整代码

cpp 复制代码
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <ADXL362.h>

// ST7789 显示屏引脚定义
#define TFT_CS   6   // 设置软件SPI的片选引脚
#define TFT_RST  4   // 显示屏复位引脚
#define TFT_DC   2   // 显示屏数据/控制命令引脚
#define TFT_MOSI 8   // 软件SPI的MOSI引脚
#define TFT_SCLK 7   // 软件SPI的SCK引脚

Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK, TFT_RST);

// 定义显示屏参数
#define SCREEN_WIDTH  240
#define SCREEN_HEIGHT 240

// 颜色定义
#define BACKGROUND  ST77XX_BLACK
#define TEXT_COLOR  ST77XX_WHITE
#define WALL_COLOR  ST77XX_BLUE
#define BALL_COLOR  ST77XX_RED
#define TARGET_COLOR ST77XX_GREEN
#define PATH_COLOR ST77XX_WHITE

// ADXL362对象
ADXL362 xl;

// 游戏参数
#define BALL_RADIUS 8
#define MAZE_CELL_SIZE 20
#define MAZE_WIDTH (SCREEN_WIDTH / MAZE_CELL_SIZE)
#define MAZE_HEIGHT (SCREEN_HEIGHT / MAZE_CELL_SIZE)

// 小球位置
int ballX = MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2;
int ballY = MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2;

// 目标位置
int targetX = (MAZE_WIDTH - 2) * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2;
int targetY = (MAZE_HEIGHT - 2) * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2;

// 加速度数据
int16_t accelX, accelY, accelZ, temp;

// 迷宫地图 (0=路径, 1=墙)
byte maze[MAZE_HEIGHT][MAZE_WIDTH] = {
  {1,1,1,1,1,1,1,1,1,1,1,1},
  {1,0,0,0,1,0,0,0,0,0,0,1},
  {1,0,1,0,1,0,1,1,1,1,0,1},
  {1,0,1,0,0,0,0,0,0,1,0,1},
  {1,0,1,1,1,1,1,1,0,1,0,1},
  {1,0,0,0,0,0,0,1,0,1,0,1},
  {1,0,1,1,1,1,0,1,0,1,0,1},
  {1,0,1,0,0,1,0,1,0,0,0,1},
  {1,0,1,0,1,1,0,1,1,1,0,1},
  {1,0,1,0,0,0,0,0,0,1,0,1},
  {1,0,0,0,1,1,1,1,0,0,0,1},
  {1,1,1,1,1,1,1,1,1,1,1,1}
};

// 游戏状态
bool gameActive = true;
int score = 0;

// 加速度校准值
int16_t calibX = 0;
int16_t calibY = 0;
int16_t calibZ = 0;

// 计时器变量
unsigned long lastDebugUpdate = 0;
const unsigned long DEBUG_UPDATE_INTERVAL = 10; // 每500ms更新一次调试信息

void setup() {
  Serial.begin(9600);
  
  // 初始化显示屏
  tft.init(SCREEN_WIDTH, SCREEN_HEIGHT);
  tft.setRotation(1);
  tft.fillScreen(BACKGROUND);
  tft.setTextColor(TEXT_COLOR);
  tft.setTextSize(1);
  
  // 显示初始化信息
  tft.setCursor(50, 100);
  tft.print("Initializing...");
  
  // 初始化加速度计
  xl.begin(10);        // CS引脚连接D10
  xl.beginMeasure();   // 进入测量模式
  
  // 校准加速度计 - 读取初始值作为偏移
  calibrateAccelerometer();
  
  // 绘制迷宫
  drawMaze();
  
  // 绘制目标
  drawTarget();
  
  // 绘制初始小球
  drawBall();
  
  // 显示游戏信息
  displayGameInfo();
  
  delay(1000);
}

void loop() {
  if (!gameActive) {
    delay(100);
    return;
  }
  
  // 读取加速度数据
  xl.readXYZTData(accelX, accelY, accelZ, temp);
  
  // 应用校准
  accelX -= calibX;
  accelY -= calibY;
  accelZ -= calibZ;
  
  // 保存旧位置
  int oldBallX = ballX;
  int oldBallY = ballY;
  
  // 根据加速度更新小球位置
  updateBallPosition();
  
  // 检查碰撞
  if (checkCollision()) {
    // 如果碰撞,恢复旧位置
    ballX = oldBallX;
    ballY = oldBallY;
  } else {
    // 清除旧位置的小球
    clearBall(oldBallX, oldBallY);
    
    // 绘制新位置的小球
    drawBall();
  }
  
  // 检查是否到达目标
  if (checkTargetReached()) {
    score++;
    displayGameInfo();
    
    // 生成新目标
    generateNewTarget();
    drawTarget();
    
    // 短暂暂停
    delay(500);
  }
  
  // 控制调试信息刷新率
  unsigned long currentTime = millis();
  if (currentTime - lastDebugUpdate >= DEBUG_UPDATE_INTERVAL) {
    displayDebugInfo();
    lastDebugUpdate = currentTime;
  }
  
  // 控制游戏速度
  delay(50);
}

// 校准加速度计
void calibrateAccelerometer() {
  Serial.println("Calibrating accelerometer...");
  
  // 读取多次样本并取平均值
  long sumX = 0, sumY = 0, sumZ = 0;
  int samples = 20;
  
  for (int i = 0; i < samples; i++) {
    xl.readXYZTData(accelX, accelY, accelZ, temp);
    sumX += accelX;
    sumY += accelY;
    sumZ += accelZ;
    delay(10);
  }
  
  calibX = sumX / samples;
  calibY = sumY / samples;
  calibZ = sumZ / samples;
  
  Serial.print("Calibration values - X: ");
  Serial.print(calibX);
  Serial.print(" Y: ");
  Serial.print(calibY);
  Serial.print(" Z: ");
  Serial.println(calibZ);
}

// 绘制迷宫
void drawMaze() {
  for (int y = 0; y < MAZE_HEIGHT; y++) {
    for (int x = 0; x < MAZE_WIDTH; x++) {
      if (maze[y][x] == 1) {
        tft.fillRect(x * MAZE_CELL_SIZE, y * MAZE_CELL_SIZE, 
                    MAZE_CELL_SIZE, MAZE_CELL_SIZE, WALL_COLOR);
      } else {
        tft.fillRect(x * MAZE_CELL_SIZE, y * MAZE_CELL_SIZE, 
                    MAZE_CELL_SIZE, MAZE_CELL_SIZE, PATH_COLOR);
      }
    }
  }
}

// 绘制小球
void drawBall() {
  tft.fillCircle(ballX, ballY, BALL_RADIUS, BALL_COLOR);
  // 绘制小球轮廓
  tft.drawCircle(ballX, ballY, BALL_RADIUS, BACKGROUND);
}

// 清除小球
void clearBall(int x, int y) {
  // 只清除小球区域,不重新绘制整个迷宫
  // 创建一个临时缓冲区来存储小球区域的背景
  uint16_t bgBuffer[(BALL_RADIUS*2+2) * (BALL_RADIUS*2+2)];
  
  // 确定要清除的区域
  int clearX1 = max(0, x - BALL_RADIUS - 1);
  int clearY1 = max(0, y - BALL_RADIUS - 1);
  int clearX2 = min(SCREEN_WIDTH - 1, x + BALL_RADIUS + 1);
  int clearY2 = min(SCREEN_HEIGHT - 1, y + BALL_RADIUS + 1);
  int width = clearX2 - clearX1 + 1;
  int height = clearY2 - clearY1 + 1;
  
  // 绘制正确的背景
  for (int py = clearY1; py <= clearY2; py++) {
    for (int px = clearX1; px <= clearX2; px++) {
      // 计算像素到小球中心的距离
      int dx = px - x;
      int dy = py - y;
      int distance = dx*dx + dy*dy;
      
      // 如果在小球半径范围内,则绘制正确的背景
      if (distance <= (BALL_RADIUS+1)*(BALL_RADIUS+1)) {
        // 获取像素所在的迷宫单元格
        int cellX = px / MAZE_CELL_SIZE;
        int cellY = py / MAZE_CELL_SIZE;
        
        // 根据单元格类型选择颜色
        uint16_t color = (maze[cellY][cellX] == 1) ? WALL_COLOR : PATH_COLOR;
        
        // 绘制像素
        tft.drawPixel(px, py, color);
      }
    }
  }
}

// 绘制目标
void drawTarget() {
  tft.fillCircle(targetX, targetY, BALL_RADIUS, TARGET_COLOR);
  // 绘制目标轮廓
  tft.drawCircle(targetX, targetY, BALL_RADIUS, BACKGROUND);
}

// 更新小球位置
void updateBallPosition() {
  // 应用死区过滤微小移动
  const int deadZone = 50;
  
  // 映射加速度到移动速度
  int moveX = 0;
  int moveY = 0;
  
  // 根据您的传感器数据调整映射范围
  if (abs(accelX) > deadZone) {
    moveX = map(accelX, -200, 200, -5, 5);
  }
  
  if (abs(accelY) > deadZone) {
    moveY = map(accelY, -200, 200, -5, 5);
  }
  
  // 更新位置
  ballX = constrain(ballX + moveX, BALL_RADIUS, SCREEN_WIDTH - BALL_RADIUS);
  ballY = constrain(ballY - moveY, BALL_RADIUS, SCREEN_HEIGHT - BALL_RADIUS); // 注意Y轴方向
  
  // 调试输出
  Serial.print("Accel - X: ");
  Serial.print(accelX);
  Serial.print(" Y: ");
  Serial.print(accelY);
  Serial.print(" Move - X: ");
  Serial.print(moveX);
  Serial.print(" Y: ");
  Serial.print(moveY);
  Serial.print(" Ball - X: ");
  Serial.print(ballX);
  Serial.print(" Y: ");
  Serial.println(ballY);
}

// 检查碰撞 - 使用小球边缘进行检测
bool checkCollision() {
  // 检查小球边缘的四个点
  int top = ballY - BALL_RADIUS;
  int bottom = ballY + BALL_RADIUS;
  int left = ballX - BALL_RADIUS;
  int right = ballX + BALL_RADIUS;
  
  // 检查上边缘
  int cellXTop = ballX / MAZE_CELL_SIZE;
  int cellYTop = top / MAZE_CELL_SIZE;
  if (cellYTop >= 0 && cellYTop < MAZE_HEIGHT && 
      cellXTop >= 0 && cellXTop < MAZE_WIDTH &&
      maze[cellYTop][cellXTop] == 1) {
    return true;
  }
  
  // 检查下边缘
  int cellXBottom = ballX / MAZE_CELL_SIZE;
  int cellYBottom = bottom / MAZE_CELL_SIZE;
  if (cellYBottom >= 0 && cellYBottom < MAZE_HEIGHT && 
      cellXBottom >= 0 && cellXBottom < MAZE_WIDTH &&
      maze[cellYBottom][cellXBottom] == 1) {
    return true;
  }
  
  // 检查左边缘
  int cellXLeft = left / MAZE_CELL_SIZE;
  int cellYLeft = ballY / MAZE_CELL_SIZE;
  if (cellYLeft >= 0 && cellYLeft < MAZE_HEIGHT && 
      cellXLeft >= 0 && cellXLeft < MAZE_WIDTH &&
      maze[cellYLeft][cellXLeft] == 1) {
    return true;
  }
  
  // 检查右边缘
  int cellXRight = right / MAZE_CELL_SIZE;
  int cellYRight = ballY / MAZE_CELL_SIZE;
  if (cellYRight >= 0 && cellYRight < MAZE_HEIGHT && 
      cellXRight >= 0 && cellXRight < MAZE_WIDTH &&
      maze[cellYRight][cellXRight] == 1) {
    return true;
  }
  
  // 检查四个角点
  if (checkPointCollision(left, top) || checkPointCollision(right, top) ||
      checkPointCollision(left, bottom) || checkPointCollision(right, bottom)) {
    return true;
  }
  
  return false;
}

// 检查单个点是否碰撞
bool checkPointCollision(int x, int y) {
  int cellX = x / MAZE_CELL_SIZE;
  int cellY = y / MAZE_CELL_SIZE;
  
  // 检查是否超出边界
  if (cellX < 0 || cellX >= MAZE_WIDTH || cellY < 0 || cellY >= MAZE_HEIGHT) {
    return true;
  }
  
  // 检查是否撞墙
  if (maze[cellY][cellX] == 1) {
    return true;
  }
  
  return false;
}

// 检查是否到达目标
bool checkTargetReached() {
  // 计算小球和目标之间的距离
  int dx = ballX - targetX;
  int dy = ballY - targetY;
  int distance = dx*dx + dy*dy;
  
  // 如果距离小于两者半径之和,则认为到达目标
  return distance < (BALL_RADIUS * 2);
}

// 生成新目标
void generateNewTarget() {
  int newX, newY;
  bool validPosition = false;
  int attempts = 0;
  
  // 尝试找到有效位置
  while (!validPosition && attempts < 50) {
    newX = random(1, MAZE_WIDTH - 1) * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2;
    newY = random(1, MAZE_HEIGHT - 1) * MAZE_CELL_SIZE + MAZE_CELL_SIZE / 2;
    
    // 检查是否在墙上
    int cellX = newX / MAZE_CELL_SIZE;
    int cellY = newY / MAZE_CELL_SIZE;
    
    if (maze[cellY][cellX] == 0) {
      validPosition = true;
    }
    
    attempts++;
  }
  
  // 清除旧目标
  clearBall(targetX, targetY);
  
  // 设置新目标
  targetX = newX;
  targetY = newY;
}

// 显示游戏信息
void displayGameInfo() {
  // 在屏幕顶部显示分数
  tft.fillRect(0, 0, SCREEN_WIDTH, 20, BACKGROUND);
  tft.setCursor(10, 5);
  tft.setTextColor(TEXT_COLOR);
  tft.setTextSize(2);
  tft.print("Score: ");
  tft.print(score);
}

// 显示调试信息
void displayDebugInfo() {
  // 在屏幕底部显示加速度数据
  tft.fillRect(0, SCREEN_HEIGHT - 20, SCREEN_WIDTH, 20, BACKGROUND);
  tft.setCursor(10, SCREEN_HEIGHT - 15);
  tft.setTextColor(TEXT_COLOR);
  tft.setTextSize(1);
  tft.print("X:");
  tft.print(accelX);
  tft.print(" Y:");
  tft.print(accelY);
  tft.print(" Z:");
  tft.print(accelZ);
}

三、操作结果展示

3.1 操作流程

  1. 按照接线图连接硬件。
  2. 用零知IDE打开并上传代码至零知标准板。
  3. 将开发板平放在桌面上,系统自动进行加速度计校准。
  4. 游戏开始后,倾斜传感器。向前倾斜小球向下移动,向左倾斜小球向左移动,以此类推。倾斜角度越大,小球移动速度越快。

3.2 显示屏界面分布

顶部区域(约20像素高): 显示当前得分(Score)。

主体区域: 显示迷宫、红色小球和绿色目标点。

底部区域(约20像素高): 调试信息区,实时滚动显示原始的加速度值(X, Y, Z)。

3.3 视频演示

ADXL362三轴加速度计的体感迷宫游戏设计

实际游戏运行效果,包括倾斜控制、碰撞、得分

四、ADXL362的SPI接口技术

4.1 SPI通信方式

ADXL362支持SPI和I2C两种通信协议。本项目采用SPI协议,因其速度更快,时序更稳

4.2 SPI数据传输和接收机制

在传输数据前,通过设置GPIO引脚的电平来激活CS信号,开始通信。

通过SPI的数据寄存器发送数据到SPI总线上。

发送数据的同时,通过读取SPI的数据寄存器来接收数据。

4.3 加速度数据格式和解析

加速度数据以数字值的形式存储在特定的寄存器中,每个轴的数据通常由两个连续的寄存器组成,一个用于高字节,一个用于低字节。代码中使用 xl.readXYZTData(...) 函数一次性读取X, Y, Z三轴的12位数字输出和温度值

XDATA_L包含8个最低有效位(LSBs), XDATA_H包含4个最高有效位(MSBs)

五、常见问题解答 (FAQ)

Q1: 为什么我的串口打印是乱码?

A:请排查:

确保零知IDE的串口监视器的波特率设置为9600,与代码中的 Serial.begin(9600) 一致。

Q2: 小球控制不灵敏或反向?

A:请根据以下提示操作:

调整 updateBallPosition() 函数中的 map 函数的参数,例如将 map(accelY, -200, 200, -5, 5) 改为 map(accelY, -200, 200, 5, -5) 可以反转Y轴方向。

增大 deadZone 值可以减少抖动,增大 map 的输出范围(如-8~8)可以增加灵敏度。

Q3: 小球穿墙bug如何处理?

A:可能的原因:

小球移动速度(map输出的值)设置过快,单帧移动距离超过了墙壁的厚度。尝试减小 map 的输出范围(如改为-2~2),让小球每帧移动慢一些。


项目资源:

零知IDE:零知实验室官网下载

ADXL362加速度计库:ADXL362-master

ADXL362数据手册:ADXL362 (Rev.G)

通过实践,我们掌握了如何驱动SPI显示屏、读取和处理加速度传感器数据、设计高效的图形刷新算法以及实现复杂的游戏碰撞逻辑

相关推荐
JQweryuiop6 天前
中小型游戏陪练工作室数字化管理实践:基于七彩屋电竞护航小程序订单、履约与绩效系统的落地复盘
大数据·游戏·小程序·架构
乌萨达7 天前
低配电脑玩手游怎么设置?4GB、8GB内存与模拟器选择指南
游戏·电脑·安卓模拟器·雷电模拟器
林川~018 天前
Unity 万能物理检测工具:射线检测 / 范围检测 / 层级过滤 / 编辑器可视化(可直接拿去用)
游戏·unity·c#·射线检测·通用工具
一孤程9 天前
游戏测试专题第七篇:游戏自动化测试实战
游戏·测试
一孤程10 天前
游戏测试专题第八篇:游戏安全测试与反外挂
游戏·测试
河南花仙子科技11 天前
小游戏社交裂变功能的高阶设计思路
大数据·科技·游戏
一孤程11 天前
游戏测试专题第五篇:游戏兼容性测试与多端适配
游戏·测试
狂人开飞机11 天前
23、实战平台跳跃游戏
游戏·游戏引擎·godot
甲维斯11 天前
0代码,0建模,3句话开发一个3D游戏!
前端·游戏·游戏开发
笨鸟先飞的橘猫12 天前
树结构在游戏行业中的应用
学习·游戏