✔零知IDE 是一个真正属于国人自己的开源软件平台,在开发效率上超越了Arduino平台并且更加容易上手,大大降低了开发难度。零知开源在软件方面提供了完整的学习教程和丰富示例代码,让不懂程序的工程师也能非常轻而易举的搭建电路来创作产品,测试产品。快来动手试试吧!
✔访问零知实验室,获取更多实战项目和教程资源吧!
目录
[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 视频演示)
[4.1 SPI通信方式](#4.1 SPI通信方式)
[4.2 SPI数据传输和接收机制](#4.2 SPI数据传输和接收机制)
[4.3 加速度数据格式和解析](#4.3 加速度数据格式和解析)
[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 操作流程
- 按照接线图连接硬件。
- 用零知IDE打开并上传代码至零知标准板。
- 将开发板平放在桌面上,系统自动进行加速度计校准。
- 游戏开始后,倾斜传感器。向前倾斜小球向下移动,向左倾斜小球向左移动,以此类推。倾斜角度越大,小球移动速度越快。

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显示屏、读取和处理加速度传感器数据、设计高效的图形刷新算法以及实现复杂的游戏碰撞逻辑