
把工业级产品常见的通信健康监测故障自愈思路和多硬件SPI资源合理分配这两个通常只在更复杂产品里才会考虑的工程实践一起落地在了一个教学级小项目上同时对开源的ADXL362 Arduino驱动库做了针对性二次开发让它支持挂载到任意一路硬件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总线的时序隔离六、常见问题解答FAQQ1ADXL362能不能用I2C接Q2ADXL362跟TFT共用SPI不行吗项目概述本项目零知派增强板主控STM32F407VET6实现利用零知派增强板多SPI外设优势用双硬件SPI总线彻底隔离显示刷新与传感器采样。ADXL362以12位分辨率采集三轴加速度数据通过硬件SPI1读取。将X/Y轴加速度值经死区阈值映射为小球在迷宫中的移动步长倾斜越大小球移动越快。实时轮询检测ADXL362的SPI通信是否正常断线自动跳失败页、右上角显示红色状态点恢复后自动软复位重新校准显示绿色状态点项目难点问题描述在不依赖任何额外硬件的前提下让系统知道传感器什么时候掉线、什么时候又恢复了并且自己完成恢复动作解决方案设计独立的SensorHealth健康检测模块连续N次一致才切换状态的去抖动逻辑检测到恢复后自动执行一次软复位重新进入测量模式重新校准的自愈动作一、系统接线部分1.1 硬件清单组件型号/规格数量主控板零知派标准板STM32F103RBT61加速度传感器ADXL362 三轴加速度计SPI 接口1显示屏ST7789 240×240 SPI TFT1EEPROMAT24C02CI2C 接口256 字节板载连接线杜邦线/排线若干1.2 接线方案表这张表严格按config.h里MODE_DUAL_HW分支的宏定义整理TFT和ADXL362分别独立占用两条不同的硬件SPI总线①ST7789 TFT屏SPI2ST7789引脚连接到对应config.h宏说明VCC3.3V—不要接5VGNDGND—与主控共地SCL(SCK)D52板载硬件SPI2软件模拟时钟SDA(MOSI)D51板载硬件SPI2软件模拟数据线RES(RST)D4TFT_RST复位DCD2TFT_DC数据/命令选择CSD53TFT_CS片选② ADXL362加速度计SPI1ADXL362引脚连接到对应config.h宏说明VDD3.3V—ADXL362是3.3V器件不能上5VGNDGND—与主控共地SCLKA5ACCEL_SPI1_SCLKSTM32F103标准SPI1时钟脚SDAA7ACCEL_SPI1_MOSI主控→传感器方向SDOA6ACCEL_SPI1_MISO传感器→主控方向CSA4ACCEL_CS由代码手动控制片选不用SPI外设自带的NSS功能1.3 连接示意图ADXL362的VDD必须接3.3V不能接5V。ADXL362的工作电压范围是1.8V~3.3V1.4 具体接线图TFT和ADXL362务必接到两组独立的信号线需要考虑片选时序二、安装与使用部分2.1 开源平台-输入ADXL362加速度传感器并搜索-代码下载自动打开2.2 连接-验证-上传2.3 调试-串口监视器三、代码讲解部分整个项目采用模块化设计共4个自定义模块ADXL362(改进版驱动)、SensorHealth(健康检测)、MazeGame(游戏逻辑)、DisplayManager(显示管理)加上主程序。下面重点讲解五个最重要的代码部分3.1 双硬件SPI总线分配与实例化/* SPI 驱动模式选择 * MODE_SOFT_HW : TFT走软件SPIADXL362走硬件SPI(全局SPI对象) —— 零知派标准板 * MODE_DUAL_HW : TFT走板载默认硬件SPIADXL362单独走硬件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_MOSI 8 #define TFT_SCLK 7 #else // MODE_DUAL_HW: TFT使用板载默认硬件SPI(实测SPI2CS/SCK/MOSI/MISOD53/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 #endifSPIAdafruit_ST7789的硬件SPI构造函数接受一个SPIClass*指针传入全局默认的SPI对象让TFT走板载默认那条硬件总线spi1另外单独实例化的一个SPIClass对象默认构造后再用setMOSI/setMISO/setSCLK把它的三根信号线重新指定到A7/A6/A5xl.begin(ACCEL_CS, spi1)把这个专属的spi1对象地址传给ADXL362驱动让它内部所有的transfer()调用都走这条总线而不是默认的全局SPI3.2 加速度计校准与倾斜映射X轴倾斜θ角时X轴读数约为g×sin(θ)。对于小角度倾斜sin(θ)≈θ弧度所以加速度值与倾角近似线性/* --------------------------------------------------------------------------- * 现场校准静置状态下采样求平均值作为零位偏移 * ------------------------------------------------------------------------- */ void calibrateAccelerometer() { long sx 0, sy 0, sz 0; const int samples 20; int16_t x, y, z, t; for (int i 0; i samples; i) { xl.readXYZTData(x, y, z, t); sx x; sy y; sz z; delay(10); } calibX sx / samples; calibY sy / samples; calibZ sz / samples; Serial.print(F([CALIB] X:)); Serial.print(calibX); Serial.print(F( Y:)); Serial.print(calibY); Serial.print(F( Z:)); Serial.println(calibZ); } void enterFailPage() { uiState UI_FAIL_PAGE; pageEnterMs millis(); disp.drawFailPage(); } bool MazeGame::update(int16_t accelX, int16_t accelY) { int moveX 0, moveY 0; if (abs(accelX) BALL_MOVE_DEADZONE) moveX map(accelX, -200, 200, -BALL_MOVE_MAXSTEP, BALL_MOVE_MAXSTEP); if (abs(accelY) BALL_MOVE_DEADZONE) moveY map(accelY, -200, 200, -BALL_MOVE_MAXSTEP, BALL_MOVE_MAXSTEP); int oldX _ballX, oldY _ballY; int newX constrain(_ballX moveX, BALL_RADIUS, SCREEN_WIDTH - BALL_RADIUS); int newY constrain(_ballY - moveY, BALL_RADIUS, SCREEN_HEIGHT - BALL_RADIUS); if (checkCollision(newX, newY)) { return false; // 撞墙保持原位 } _ballX newX; _ballY newY; clearBallAt(oldX, oldY); drawBall(); int dx _ballX - _targetX, dy _ballY - _targetY; if (dx * dx dy * dy (BALL_RADIUS * 2)) { _score; generateNewTarget(); // 食物随机刷新到迷宫内任意一个可通行格子 drawTarget(); return true; } return false; }参数值含义samples20校准采样次数取平均值作为零位偏移BALL_MOVE_DEADZONE50死区阈值加速度绝对值小于此值时小球不移动BALL_MOVE_MAXSTEP5单次最大移动步长像素-200/200映射输入范围加速度值的有效范围不校准的话水平放置时X/Y轴可能读到±30的偏移值导致小球“自己往一边跑”。校准后水平放置时X/Y轴读数接近零3.3 非阻塞轮询与去抖动状态机consecutiveOk和_consecutiveFail是两个计数器分别记录连续正常和连续失败的次数只有当连续正常次数达到HEALTH_OK_DEBOUNCE2次时才判定为恢复连续失败达到HEALTH_FAIL_DEBOUNCE2次时才判定为失败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[12][12]144字节,uint16_t按行压缩成位图一行12格墙体信息压进16位整数的低12位1墙0通路12行只需24字节static const uint16_t MAZE_ROWS[MAZE_HEIGHT] { 0xFFF, 0x811, 0xBD5, 0xA05, 0xAFD, 0xA81, 0xABD, 0x8A5, 0xBB5, 0xA05, 0x8F1, 0xFFF, }; bool MazeGame::isWall(uint8_t cellX, uint8_t cellY) { if (cellX MAZE_WIDTH || cellY MAZE_HEIGHT) return true; return (MAZE_ROWS[cellY] cellX) 0x01; }取值(MAZE_ROWS[cellY] cellX) 0x01是标准的取第cellX位位运算写法右移cellX位把目标位移到最低位再与0x01清除其余高位。cellX MAZE_WIDTH这行边界检查把越界统一当墙处理相当于给迷宫套了层看不见的保护墙判断所在格子是否为墙小球被抽象成一个半径为BALL_RADIUS8px的圆bool MazeGame::checkCollision(int x, int y) { int top y - BALL_RADIUS, bottom y BALL_RADIUS; int left x - BALL_RADIUS, right x BALL_RADIUS; if (checkPointCollision(x, top)) return true; if (checkPointCollision(x, bottom)) return true; if (checkPointCollision(left, y)) return true; if (checkPointCollision(right, y)) return true; if (checkPointCollision(left, top)) return true; if (checkPointCollision(right, top)) return true; if (checkPointCollision(left, bottom)) return true; if (checkPointCollision(right, bottom)) return true; return false; }碰撞检测取圆的四条边中点和四个角点共8个采样点每个点调用checkPointCollision()计算量是常量级8次调用比精确的圆-矩形相交检测简单得多对于20px格子的迷宫来说精度完全够用随机尝试法这种方法的优势是不需要预先计算所有可通行格子的列表节省RAM和代码复杂度void MazeGame::generateNewTarget() { int nx _targetX, ny _targetY; for (uint8_t attempt 0; attempt 50; attempt) { int cx random(1, MAZE_WIDTH - 1); int cy random(1, MAZE_HEIGHT - 1); if (!isWall((uint8_t)cx, (uint8_t)cy)) { nx cx * MAZE_CELL_SIZE MAZE_CELL_SIZE / 2; ny cy * MAZE_CELL_SIZE MAZE_CELL_SIZE / 2; break; } } clearBallAt(_targetX, _targetY); _targetX nx; _targetY ny; }在迷宫的内部区域排除边界墙随机取一个格子如果不是墙就放食物最多尝试50次3.5 显示管理器与HUD刷新策略将整行顶部0~20px刷黑这样做的好处是HUD和状态点共享同一个黑色背景条视觉上更统一void DisplayManager::drawStatusDot(CommState state) { // 先刷黑底再画圆点避免迷宫墙壁的蓝色从圆点周围透出来 int16_t bx STATUS_DOT_X - STATUS_DOT_R - 2; int16_t by STATUS_DOT_Y - STATUS_DOT_R - 2; int16_t bs (STATUS_DOT_R 2) * 2; _tft-fillRect(bx, by, bs, bs, COLOR_BG); uint16_t c (state COMM_OK) ? COLOR_DOT_OK : COLOR_DOT_FAIL; _tft-fillCircle(STATUS_DOT_X, STATUS_DOT_Y, STATUS_DOT_R, c); } void DisplayManager::drawScoreHUD(uint16_t score, uint16_t best) { // 整行铺满刷黑不留空档给状态点——本项目里这个函数只在 // enterGameScreen()里被调用后面一定紧跟一次drawStatusDot()。 _tft-fillRect(0, 0, SCREEN_WIDTH, 20, COLOR_BG); _tft-setCursor(6, 4); _tft-setTextColor(COLOR_TEXT); _tft-setTextSize(1); _tft-print(Score:); _tft-print(score); _tft-print( Best:); _tft-print(best); }由于drawScoreHUD()之后一定紧跟drawStatusDot()状态点会画在已经刷黑的区域上系统流程图①ADXL362 库文件多总线改造原版 ADXL362 Arduino 库使用全局 SPI 对象硬编码在 SPI.transfer() 调用中。本项目将 begin() 函数改造为接受第二个 SPIClass *spiPort 参数并新增 _spi 成员变量void ADXL362::begin(int16_t chipSelectPin, SPIClass *spiPort) { _csPin chipSelectPin; _spi (spiPort ! nullptr) ? spiPort : SPI; // 不传就退化成默认全局SPI向下兼容 pinMode(_csPin, OUTPUT); digitalWrite(_csPin, HIGH); _spi-begin(); _spi-setDataMode(SPI_MODE0); // CPHACPOL0对应手册Mode 0 delay(1000); SPIwriteOneRegister(0x1F, 0x52); // 软复位寄存器地址0x1F写入0x52(ASCII R) delay(10); }所有寄存器读写函数内部原本直接写SPI.transfer(...)的地方全部同步改为_spi-transfer(...)。当 ST7789 正在接收像素数据时SPI2 的 MOSI 线被占满但 SPI1 的 MOSI/MISO/SCK 完全独立ADXL362 的采样不受任何影响两条总线并行工作游戏刷新和传感器采样真正做到了互不干扰既支持了 SPI1 独立总线又通过spiPort ! nullptr的判断保持了向下兼容——如果不传第二个参数自动退化为默认全局 SPI。②测量模式切换机制与断电恢复ADXL362 的测量模式切换遵循以下步骤软复位写 0x1F 寄存器为 0x52将所有寄存器恢复到默认值→ 配置参数在待机模式下设置输出数据率、量程、噪声模式等 → 进入测量模式读 0x2D 的当前值将 bit1 置 1temp | 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屏幕下方显示Calibrating...时请让板子保持水平静止正在采集20个样本做零位校准②进入游戏主界面蓝色为墙体白色为通路红色圆点是小球绿色圆点是目标食物任意方向倾斜板子小球朝对应方向滚动撞墙不可穿越。小球接触绿色目标得1分目标在迷宫内随机挑选新的可通行位置重新刷新③右上角圆形状态点绿色ADXL362(挂载在独立SPI1总线上)通信正常红色通信异常试着拔掉ADXL362在A4~A7这组独立总线上的任意一根信号线观察屏幕先跳转红色警示页、3秒后自动回主界面显示红点重新插回后几百毫秒内跳转COMM RESTORED提示页随后自动回主界面变绿点4.2 演示视频零知派增强板双硬件SPI驱动ADXL362重力迷宫游戏本视频展示了基于ADXL362三轴加速度计的体感迷宫游戏。玩家通过倾斜零知派增强板控制红色小球在12×12迷宫中滚动避开蓝色墙壁撞击绿色目标点得分。视频包含硬件接线展示、上电初始化过程、游戏操作演示、以及通信失败/恢复的自愈过程。重点展示了双硬件SPI架构下显示屏刷新与传感器采样互不干扰的流畅体验五、技术讲解部分5.1 ADXL362的架构与通信方式ADXL362的核心是一个微机械加工的三轴加速度传感结构。当传感器受到加速度时内部的可移动质量块发生偏转差分电容的平衡被打破产生与加速度成正比的电容变化。这个变化经过相敏解调后被转换为数字信号最终以12位精度存储在XDATA/YDATA/ZDATA寄存器中ADXL362的通信接口是4线SPI工作模式为CPOL0、CPHA0推荐的SPI时钟频率为1MHz~5MHz。SPI命令集只有三条命令字节功能说明0x0A写寄存器后跟地址字节和数据字节0x0B读寄存器后跟地址字节然后读取数据0x0D读FIFO无需地址字节连续读取FIFO内容SPI传输采用多字节结构第一个字节是命令第二个字节是寄存器地址后续字节是数据。寄存器读写命令支持多字节突发传输地址会自动递增5.2 倾斜检测的物理原理加速度传感器的基本物理模型是加速度a 重力分量 运动加速度。当传感器静止时运动加速度为零此时读到的就是重力在各轴上的投影设传感器坐标系为X-Y-Z重力向量为g指向地心大小为1g。当传感器倾斜时重力在X轴和Y轴上的投影分别为X轴读数 ≈ g×sin(θ_x)Y轴读数 ≈ g×sin(θ_y)。对于小角度倾斜θ30°sin(θ)≈θ所以加速度值与倾角近似线性这也是用map()函数做线性映射的理论基础5.3 双SPI总线的时序隔离STM32F407VET6有两个独立的SPI外设SPI1挂载在APB2总线上SPI2挂载在APB1总线上本项目中的分配方案Adafruit_ST7789 tft(SPI, TFT_CS, TFT_DC, TFT_RST); // TFT 走默认 SPISPI2 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 对象传给 ADXL362TFT传入全局 SPI 对象走板载默认硬件 SPISTM32F407VET6 上实测为 SPI2对应 D53/52/51/50ADXL362创建独立的 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写寄存器CS down 0x0A 地址 数据 ... CS up0x0B读寄存器CS down 0x0B 地址 数据 ... CS up0x0D读 FIFOCS down 0x0D 数据 数据... CS up多字节突发传输读写寄存器命令支持多字节突发访问地址会自动递增。地址递增在无效寄存器地址 630x3F处停止避免地址回绕寄存器映射ADXL362 的寄存器映射覆盖了从设备 ID 到加速度数据、FIFO 控制、运动检测配置等完整功能。本项目重点涉及以下几个关键寄存器。设备标识寄存器用于通信自检 寄存器名地址固定值说明DEVID_AD0x000xADAnalog Devices 设备 IDDEVID_MST0x010x1D模拟器件 MEMS IDPARTID0x020xF2器件型号 ID362 的八进制表示项目 SensorHealth::poll() 中调用的 _accel-checkCommunication() 就是通过读取并比对 DEVID_AD 和 PARTID 来判断 SPI 链路是否正常POWER_CTL 寄存器地址 0x2DPOWER_CTL 寄存器是 ADXL362 最核心的控制寄存器复位默认值为 0x00待机模式位定义如下位名称设置值描述7Reserved—保留6EXT_CLK0/1外部时钟使能由 INT1 引脚输入[5:4]LOW_NOISE00/01/1000普通模式 01低噪声 10超低噪声3WAKEUP0/1唤醒模式使能2AUTOSLEEP0/1自动休眠使能需配合活动/非活动检测[1:0]MEASURE00/1000待机 10测量模式MEASURE 位的操作要点将 POWER_CTL 的 bit[1:0] 写入10即可进入测量模式。对 POWER_CTL 之前的所有寄存器0x00~0x2C的修改应在待机模式下进行如果在测量模式下修改可能只在部分测量周期内生效六、常见问题解答FAQQ1ADXL362能不能用I2C接A不能。手册明确只支持4线SPI引脚定义里也没有SDA/SCL这是芯片本身的硬件限制如果一定要I2C接口的加速度计需要换成ADXL345/ADXL343等支持I2C/SPI双模的型号Q2ADXL362跟TFT共用SPI不行吗A共用一条总线也能工作(只是每次收发前要额外切换片选和SPI参数)但增强板既然有多余的硬件SPI资源独立分配一条给ADXL362可以避免任何总线仲裁开销代码逻辑也更清晰如果换到只有一路好用硬件SPI的板子本项目也保留了MODE_SOFT_HW模式(TFT走软件SPI)作为兼容方案项目资源整合ADXL362 库文件 ADXL362 (Rev.G)ADXL362 数据手册 annem/ADXL362