零知派——基于STM32F407VET6与ADXL362加速度传感器的双硬件SPI重力感应迷宫小球游戏
把工业级产品常见的"通信健康监测+故障自愈"思路和"多硬件SPI资源合理分配"这两个通常只在更复杂产品里才会考虑的工程实践,一起落地在了一个教学级小项目上,同时对开源的ADXL362 驱动库做了针对性二次开发,让它支持挂载到任意一路硬件SPI总线目录
一、系统接线部分
1.1 硬件清单1.2 接线方案表1.3 连接示意图1.4 具体接线图二、安装与使用部分
三、代码讲解部分
3.1 双硬件SPI总线分配与实例化3.2 加速度计校准与倾斜映射3.3 非阻塞轮询与去抖动状态机3.4 迷宫碰撞检测与食物刷新3.5 显示管理器与HUD刷新策略四、项目结果演示
4.1 操作流程4.2 演示视频五、技术讲解部分
5.1 ADXL362的架构与通信方式5.2 倾斜检测的物理原理5.3 双SPI总线的时序隔离六、常见问题解答(FAQ)
Q1:ADXL362能不能用I2C接?Q2:ADXL362跟TFT共用SPI不行吗?项目概述
本项目零知派增强板(主控STM32F407VET6)实现,利用零知派增强板多SPI外设优势,用双硬件SPI总线彻底隔离显示刷新与传感器采样。ADXL362以12位分辨率采集三轴加速度数据,通过硬件SPI1读取。将X/Y轴加速度值经死区阈值映射为小球在迷宫中的移动步长,倾斜越大,小球移动越快。实时轮询检测ADXL362的SPI通信是否正常,断线自动跳失败页、右上角显示红色状态点,恢复后自动软复位+重新校准+显示绿色状态点
项目难点
问题描述:在不依赖任何额外硬件的前提下,让系统知道传感器什么时候掉线、什么时候又恢复了,并且自己完成恢复动作
解决方案:设计独立的SensorHealth健康检测模块,连续N次一致才切换状态的去抖动逻辑,检测到恢复后自动执行一次软复位+重新进入测量模式+重新校准的自愈动作
一、系统接线部分1.1 硬件清单
组件型号/规格数量
主控板零知派标准板(STM32F103RBT6)1
加速度传感器 ADXL362 三轴加速度计(SPI 接口) 1
显示屏ST7789 240×240 SPI TFT1
EEPROMAT24C02C(I2C 接口,256 字节)板载
连接线杜邦线/排线若干
1.2 接线方案表 这张表严格按config.h里MODE_DUAL_HW分支的宏定义整理,TFT和ADXL362分别独立占用两条不同的硬件SPI总线
①ST7789 TFT屏(SPI2)
ST7789引脚连接到对应config.h宏说明
VCC3.3V—不要接5V
GNDGND—与主控共地
SCL(SCK)D52板载硬件SPI2 软件模拟时钟
SDA(MOSI) D51板载硬件SPI2软件模拟数据线
RES(RST)D4TFT_RST复位
DCD2TFT_DC数据/命令选择
CSD53 TFT_CS片选
② ADXL362加速度计(SPI1)
ADXL362引脚连接到对应config.h宏说明
VDD 3.3V—ADXL362是3.3V器件,不能上5V
GNDGND —与主控共地
SCLKA5ACCEL_SPI1_SCLK STM32F103标准SPI1时钟脚
SDAA7ACCEL_SPI1_MOSI 主控→传感器方向
SDOA6ACCEL_SPI1_MISO 传感器→主控方向
CSA4ACCEL_CS由代码手动控制片选,不用SPI外设自带的NSS功能
1.3 连接示意图ADXL362的VDD必须接3.3V,不能接5V。ADXL362的工作电压范围是1.8V~3.3V
https://file1.elecfans.com/web3/M00/7A/66/wKgZPGqvcLKAPcLuAAj3Id0l8Dw202.png
1.4 具体接线图https://file1.elecfans.com/web3/M00/7A/6B/wKgZO2qvcL2AMQSHABn4iADidIE008.pngTFT和ADXL362务必接到两组独立的信号线,需要考虑片选时序
二、安装与使用部分2.1 开源平台-输入"ADXL362加速度传感器"并搜索-代码下载自动打开
https://file1.elecfans.com/web3/M00/7A/6B/wKgZO2qvcMWAR-3UAAeaHWF-eo4605.png2.2 连接-验证-上传
https://file1.elecfans.com/web3/M00/7A/6B/wKgZO2qvcMWAR-3UAAeaHWF-eo4605.png2.3 调试-串口监视器
https://file1.elecfans.com/web3/M00/7A/66/wKgZPGqvcN-AMOFVAA8YC5ezeiU850.png
三、代码讲解部分 整个项目采用模块化设计,共4个自定义模块:ADXL362(改进版驱动)、SensorHealth(健康检测)、MazeGame(游戏逻辑)、DisplayManager(显示管理),加上主程序。下面重点讲解五个最重要的代码部分
3.1 双硬件SPI总线分配与实例化
/* ============================ SPI 驱动模式选择 ============================
* MODE_SOFT_HW: TFT走软件SPI,ADXL362走硬件SPI(全局SPI对象) —— 零知派标准板
* MODE_DUAL_HW: TFT走板载默认硬件SPI,ADXL362单独走硬件SPI1(A4~A7) —— 增强板
* ========================================================================= */
#define MODE_SOFT_HW 0
#define MODE_DUAL_HW 1
#define ACTIVE_SPI_MODE MODE_DUAL_HW // < -- 零知派增强板(STM32F407VET6)用这个模式
/* ============================ ST7789 显示屏引脚 ============================ */
#define TFT_RST 4 // 复位
#define TFT_DC 2 // 数据/命令
#if (ACTIVE_SPI_MODE == MODE_SOFT_HW)
#define TFT_CS 6
#define TFT_MOSI8
#define TFT_SCLK7
#else // MODE_DUAL_HW: TFT使用板载默认硬件SPI(实测SPI2,CS/SCK/MOSI/MISO=D53/52/51/50)
#define TFT_CS 53
#endif
/* ============================ ADXL362 引脚 / SPI总线 ============================ */
#if (ACTIVE_SPI_MODE == MODE_SOFT_HW)
#define ACCEL_CS A4
#else
#define ACCEL_CS A4 // SPI1软件片选,手动控制
#define ACCEL_SPI1_MOSI A7
#define ACCEL_SPI1_MISO A6
#define ACCEL_SPI1_SCLK A5
#endif
[*]&SPI:Adafruit_ST7789的硬件SPI构造函数接受一个SPIClass*指针,传入全局默认的SPI对象,让TFT走板载默认那条硬件总线;
[*]spi1:另外单独实例化的一个SPIClass对象,默认构造后再用setMOSI/setMISO/setSCLK把它的三根信号线重新指定到A7/A6/A5
[*]xl.begin(ACCEL_CS, &spi1):把这个专属的spi1对象地址传给ADXL362驱动,让它内部所有的transfer()调用都走这条总线,而不是默认的全局SPI
3.2 加速度计校准与倾斜映射 X轴倾斜θ角时,X轴读数约为g×sin(θ)。对于小角度倾斜,sin(θ)≈θ(弧度),所以加速度值与倾角近似线性
<blockquote><p>/* ---------------------------------------------------------------------------</p>
参数值含义
samples20校准采样次数,取平均值作为零位偏移
BALL_MOVE_DEADZONE50死区阈值,加速度绝对值小于此值时小球不移动
BALL_MOVE_MAXSTEP5单次最大移动步长(像素)
-200 / 200映射输入范围加速度值的有效范围
不校准的话水平放置时X/Y轴可能读到±30的偏移值,导致小球“自己往一边跑”。校准后,水平放置时X/Y轴读数接近零
3.3 非阻塞轮询与去抖动状态机
consecutiveOk和_consecutiveFail是两个计数器,分别记录连续正常和连续失败的次数
[*]只有当连续正常次数达到HEALTH_OK_DEBOUNCE(2次)时,才判定为恢复
[*]连续失败达到HEALTH_FAIL_DEBOUNCE(2次)时,才判定为失败
HealthEvent SensorHealth::poll() {
uint32_t now = millis();
if (now - _lastPollMs < HEALTH_POLL_INTERVAL_MS) {
return HEALTH_EVENT_NONE; // 还没到轮询节拍,直接跳过(非阻塞)
}
_lastPollMs = now;
// 读 DEVID_AD(0x00,应为0xAD) 和 PARTID(0x02,应为0xF2) 两个只读寄存器,
// 判断SPI链路是否正常——即使传感器已经在测量模式下运行,这两个寄存器
// 的值也不会变,用它们做"心跳检测"不影响正常的姿态数据采集。
bool ok = (_accel != nullptr) && _accel- >checkCommunication();
if (ok) {
_consecutiveFail = 0;
if (_consecutiveOk < 255) _consecutiveOk++;
} else {
_consecutiveOk = 0;
if (_consecutiveFail < 255) _consecutiveFail++;
}
if (_state != COMM_OK && _consecutiveOk >= HEALTH_OK_DEBOUNCE) {
bool wasFail = (_state == COMM_FAIL);
_state = COMM_OK;
if (wasFail) {
return HEALTH_EVENT_RECOVERED; // 只有"曾经失败过"才算恢复事件
}
} else if (_state != COMM_FAIL && _consecutiveFail >= HEALTH_FAIL_DEBOUNCE) {
_state = COMM_FAIL;
_failEventCount++;
return HEALTH_EVENT_NEW_FAIL;
}
return HEALTH_EVENT_NONE;
}用millis()时间戳判断代替delay(),是嵌入式非阻塞编程最基础也最重要的模式,健康检测不会拖慢游戏画面刷新
恢复自愈逻辑:
} else if (ev == HEALTH_EVENT_RECOVERED) {
xl.SPIwriteOneRegister(0x1F, 0x52); // 软复位
delay(10);
xl.beginMeasure(); // 重新进入测量模式
calibrateAccelerometer(); // 重新校准
record.calibX = calibX; ...
eeprom.save(record);
enterOkPage();
}关键的是软复位 + 重新进入测量模式:
[*]ADXL362断电重连后,POWER_CTL寄存器(0x2D)的测量模式位会被硬件复位清零
[*]但SPI协议层面通信其实已经恢复了,如果不处理这一步,游戏会显示"通信正常"却读到无效数据
[*]检测到RECOVERED事件的瞬间,自动补一次软复位+重新进入测量模式+重新校准,相当于让代码自己完成了原本需要人工按复位键才能做的事
3.4 迷宫碰撞检测与食物刷新迷宫地图的位压缩存储
存储方式是byte maze,144字节,uint16_t按行压缩成位图,一行12格墙体信息压进16位整数的低12位(1=墙,0=通路),12行只需24字节
<blockquote><p>static const uint16_t MAZE_ROWS = {</p>取值(MAZE_ROWS >> cellX) & 0x01是标准的"取第cellX位"位运算写法:右移cellX位把目标位移到最低位,再与0x01清除其余高位。cellX >= MAZE_WIDTH这行边界检查把越界统一当墙处理,相当于给迷宫套了层看不见的保护墙
判断所在格子是否为墙
小球被抽象成一个半径为BALL_RADIUS(8px)的圆
<blockquote><p>bool MazeGame::checkCollision(int x, int y) {</p>碰撞检测取圆的四条边中点和四个角点共8个采样点,每个点调用checkPointCollision()计算量是常量级(8次调用),比精确的圆-矩形相交检测简单得多,对于20px格子的迷宫来说精度完全够用
随机尝试法
这种方法的优势是不需要预先计算所有可通行格子的列表,节省RAM和代码复杂度
<blockquote><p>void MazeGame::generateNewTarget() {</p>在迷宫的内部区域(排除边界墙)随机取一个格子,如果不是墙就放食物;最多尝试50次
3.5 显示管理器与HUD刷新策略 将整行顶部(0~20px)刷黑,这样做的好处是HUD和状态点共享同一个黑色背景条,视觉上更统一
<blockquote><p>void DisplayManager::drawStatusDot(CommState state) {</p>由于drawScoreHUD()之后一定紧跟drawStatusDot(),状态点会画在已经刷黑的区域上
系统流程图
https://file1.elecfans.com/web3/M00/7A/6B/wKgZO2qvcSOAT-wkABsldh5Cddk810.png①ADXL362 库文件多总线改造
原版 ADXL362 Arduino 库使用全局 SPI 对象,硬编码在 SPI.transfer() 调用中。本项目将 begin() 函数改造为接受第二个 SPIClass *spiPort 参数,并新增 _spi 成员变量
<blockquote><p>void ADXL362::begin(int16_t chipSelectPin, SPIClass *spiPort) {</p>
所有寄存器读写函数内部原本直接写 SPI.transfer(...) 的地方,全部同步改为 _spi->transfer(...)。
[*]当 ST7789 正在接收像素数据时,SPI2 的 MOSI 线被占满,但 SPI1 的 MOSI/MISO/SCK 完全独立,ADXL362 的采样不受任何影响
[*]两条总线并行工作,游戏刷新和传感器采样真正做到了互不干扰
既支持了 SPI1 独立总线,又通过 spiPort != nullptr 的判断保持了向下兼容——如果不传第二个参数,自动退化为默认全局 SPI。
②测量模式切换机制与断电恢复
ADXL362 的测量模式切换遵循以下步骤:
软复位:写 0x1F 寄存器为 0x52,将所有寄存器恢复到默认值 → 配置参数:在待机模式下设置输出数据率、量程、噪声模式等 → 进入测量模式:读 0x2D 的当前值,将 bit1 置 1(temp | 0x02),写回 0x2D
在 HEALTH_EVENT_RECOVERED 分支中,自动执行三步自愈动作:
xl.SPIwriteOneRegister(0x1F, 0x52); // 软复位:寄存器全部恢复默认值
delay(10);
xl.beginMeasure(); // 重新进入测量模式
calibrateAccelerometer(); // 重新校准零位偏移
[*]软复位确保芯片回到已知状态,清除可能存在的寄存器异常值
[*]beginMeasure() 重新将 POWER_CTL 的 MEASURE 位写入测量模式
[*]重新校准是因为断电重连后芯片的零位偏移可能发生变化
这三步相当于让代码自动完成了原本需要人工按复位键才能做的事,实现了“通信中断→自动恢复→无需手动干预”的闭环。
四、项目结果演示4.1 操作流程①系统上电
屏幕显示开机Logo和固件版本号,随后进入ADXL362初始化,此时spi1总线正在被配置到A5/A6/A7
https://file1.elecfans.com/web3/M00/7A/6B/wKgZO2qvcT-AcINyACYyXtORXv8182.png屏幕下方显示"Calibrating..."时,请让板子保持水平静止,正在采集20个样本做零位校准
②进入游戏主界面
蓝色为墙体,白色为通路,红色圆点是小球,绿色圆点是目标(食物)
https://file1.elecfans.com/web3/M00/7A/66/wKgZPGqvcW6ASqrlACEoWrXoYTw999.png任意方向倾斜板子,小球朝对应方向滚动,撞墙不可穿越。小球接触绿色目标得1分,目标在迷宫内随机挑选新的可通行位置重新刷新
③右上角圆形状态点
绿色=ADXL362(挂载在独立SPI1总线上)通信正常,红色=通信异常
https://file1.elecfans.com/web3/M00/7A/6B/wKgZO2qvcXaAWoHyABvr6rLFJzs978.png
[*]试着拔掉ADXL362在A4~A7这组独立总线上的任意一根信号线,观察屏幕先跳转红色警示页、3秒后自动回主界面显示红点
[*]重新插回后几百毫秒内跳转"COMM RESTORED"提示页,随后自动回主界面变绿点
4.2 演示视频https://live.csdn.net/v/544350?spm=1001.2014.3001.5501
本视频展示了基于ADXL362三轴加速度计的体感迷宫游戏。玩家通过倾斜零知派增强板,控制红色小球在12×12迷宫中滚动,避开蓝色墙壁,撞击绿色目标点得分。视频包含硬件接线展示、上电初始化过程、游戏操作演示、以及通信失败/恢复的自愈过程。重点展示了双硬件SPI架构下,显示屏刷新与传感器采样互不干扰的流畅体验
五、技术讲解部分5.1 ADXL362的架构与通信方式 ADXL362的核心是一个微机械加工的三轴加速度传感结构。当传感器受到加速度时,内部的可移动质量块发生偏转,差分电容的平衡被打破,产生与加速度成正比的电容变化。这个变化经过相敏解调后被转换为数字信号,最终以12位精度存储在XDATA/YDATA/ZDATA寄存器中
https://file1.elecfans.com/web3/M00/7A/67/wKgZPGqvciWAHp-lAACWVqjmcAQ495.pngADXL362的通信接口是4线SPI,工作模式为CPOL=0、CPHA=0,推荐的SPI时钟频率为1MHz~5MHz。SPI命令集只有三条:
命令字节功能说明
0x0A 写寄存器 后跟地址字节和数据字节
0x0B读寄存器后跟地址字节,然后读取数据
0x0D读FIFO无需地址字节,连续读取FIFO内容
SPI传输采用多字节结构:第一个字节是命令,第二个字节是寄存器地址,后续字节是数据。寄存器读写命令支持多字节(突发)传输,地址会自动递增
5.2 倾斜检测的物理原理 加速度传感器的基本物理模型是:加速度a = 重力分量 + 运动加速度。当传感器静止时,运动加速度为零,此时读到的就是重力在各轴上的投影
设传感器坐标系为X-Y-Z,重力向量为g(指向地心,大小为1g)。
https://file1.elecfans.com/web3/M00/7A/6B/wKgZO2qvcjOAaIv1AABsgq8lRwQ509.png
[*]当传感器倾斜时,重力在X轴和Y轴上的投影分别为:X轴读数 ≈ g×sin(θ_x),Y轴读数 ≈ g×sin(θ_y)。
[*]对于小角度倾斜(θ<30°),sin(θ)≈θ,所以加速度值与倾角近似线性,这也是用map()函数做线性映射的理论基础
5.3 双SPI总线的时序隔离 STM32F407VET6有两个独立的SPI外设:SPI1挂载在APB2总线上,SPI2挂载在APB1总线上
https://file1.elecfans.com/web3/M00/7A/67/wKgZPGqvckSAYyDYAABMOkbsYsU523.png本项目中的分配方案:
Adafruit_ST7789 tft(&SPI, TFT_CS, TFT_DC, TFT_RST);// TFT 走默认 SPI(SPI2)
SPIClass spi1; // 独立创建 SPI1 对象
spi1.setMOSI(ACCEL_SPI1_MOSI); // SPI1_MOSI → A7
spi1.setMISO(ACCEL_SPI1_MISO); // SPI1_MISO → A6
spi1.setSCLK(ACCEL_SPI1_SCLK); // SPI1_SCLK → A5
xl.begin(ACCEL_CS, &spi1); // 将 SPI1 对象传给 ADXL362
[*]TFT:传入全局 &SPI 对象,走板载默认硬件 SPI(STM32F407VET6 上实测为 SPI2,对应 D53/52/51/50)
[*]ADXL362:创建独立的 spi1 对象,通过 setMOSI/setMISO/setSCLK 将三根信号线映射到 A7/A6/A5,再将该对象的地址传给 xl.begin()。ADXL362 库内部所有 _spi->transfer() 调用都会走这条总线
SPI通信是全双工同步串行:主设备产生时钟(SCK),数据在MOSI和MISO上同时传输。当ST7789正在接收像素数据时,SPI2的MOSI线被占满,但SPI1的MOSI/MISO/SCK完全独立,ADXL362的采样不受任何影响
SPI 命令集
ADXL362 的 SPI 端口采用多字节结构,第一个字节是命令字节,命令集只有三条:
命令字节功能命令结构
0x0A写寄存器<0x0A> <地址> <数据> <...>
0x0B 读寄存器 <0x0B> <地址> <数据> <...>
0x0D读 FIFO<0x0D> <数据> <数据>...
多字节(突发)传输:读写寄存器命令支持多字节突发访问,地址会自动递增。地址递增在无效寄存器地址 63(0x3F)处停止,避免地址回绕
寄存器映射
ADXL362 的寄存器映射覆盖了从设备 ID 到加速度数据、FIFO 控制、运动检测配置等完整功能。本项目重点涉及以下几个关键寄存器。设备标识寄存器(用于通信自检) :
寄存器名地址固定值说明
DEVID_AD0x000xADAnalog Devices 设备 ID
DEVID_MST0x010x1D模拟器件 MEMS ID
PARTID0x020xF2器件型号 ID(362 的八进制表示)
项目 SensorHealth::poll() 中调用的 _accel->checkCommunication() 就是通过读取并比对 DEVID_AD 和 PARTID 来判断 SPI 链路是否正常
POWER_CTL 寄存器(地址 0x2D) :
POWER_CTL 寄存器是 ADXL362 最核心的控制寄存器
https://file1.elecfans.com/web3/M00/7A/67/wKgZPGqvcluAFHgrAAEtqArMkAI866.png复位默认值为 0x00(待机模式),位定义如下:
位名称设置值描述
7Reserved—保留
6EXT_CLK0/1外部时钟使能,由 INT1 引脚输入
LOW_NOISE00/01/1000=普通模式 01=低噪声 10=超低噪声
3WAKEUP0/1唤醒模式使能
2AUTOSLEEP0/1自动休眠使能(需配合活动/非活动检测)
MEASURE00/1000=待机 10=测量模式
MEASURE 位的操作要点:将 POWER_CTL 的 bit 写入 10 即可进入测量模式。对 POWER_CTL 之前的所有寄存器(0x00~0x2C)的修改应在待机模式下进行,如果在测量模式下修改,可能只在部分测量周期内生效
六、常见问题解答(FAQ)Q1:ADXL362能不能用I2C接? A:不能。手册明确只支持4线SPI,引脚定义里也没有SDA/SCL,这是芯片本身的硬件限制,如果一定要I2C接口的加速度计,需要换成ADXL345/ADXL343等支持I2C/SPI双模的型号
Q2:ADXL362跟TFT共用SPI不行吗?
A:共用一条总线也能工作(只是每次收发前要额外切换片选和SPI参数),但增强板既然有多余的硬件SPI资源,独立分配一条给ADXL362可以避免任何总线仲裁开销,代码逻辑也更清晰,如果换到只有一路好用硬件SPI的板子,本项目也保留了MODE_SOFT_HW模式(TFT走软件SPI)作为兼容方案项目资源整合
ADXL362 库文件: ADXL362 (Rev.G)
ADXL362 数据手册: annem/ADXL362
页:
[1]