2026 ADI机器控制设计竞赛 - 用ADMT4000模块多圈转盘功能游戏机
该项目使用了ADMT4000,实现了多圈转盘功能游戏机的设计,它的主要功能为:ADMT4000模块多圈转盘功能游戏机。
标签
PCB
游戏机
ADMT4000
teensy 4.0
冲向天空的猪
更新2026-05-11
6


项目背景ADMT4000 是一款磁悬浮演示装置,默认仅支持 Type-C 供电。为了摆脱额外电源线束缚,并增加更多交互反馈功能,我基于 Teensy 4.0 设计了一块扩展控制板,实现旋钮状态可视化、震动反馈和菜单交互。
在多圈密码转盘锁中提取其中的想法,扩展了一些相关游戏的设置玩法,增加了一些娱乐性。


项目概述

项目名称

ADMT4000 扩展控制器

核心功能

供电改造 + 旋钮状态可视化 + 震动反馈 + OLED 菜单

主控芯片

Teensy 4.0

关键模块

OLED 显示、WS2812B 彩色灯、震动马达、按键输入

一、需求分析

1.1 原始问题

        ADMT4000 默认只有 Type-C 供电,额外接线麻烦

        旋钮使用磁感应,没有任何触觉反馈,或者声音反馈

        旋钮状态无法直观查看

1.2 需求解决

需求

描述

供电改造

解决额外 Type-C 供电问题

震动反馈

旋钮转动时提供触觉反馈

状态显示

OLED 屏幕显示当前菜单/状态

彩色灯指示

用颜色区分不同状态

按键交互

支持菜单选择和确认操作

二、方案设计

2.1 供电方案解决

基于原理图分析,发现其中有多余 Pin 预留,通过万用表的蜂鸣档测量其中导线是连接的,则采取飞线的方式进行供电实现。

image.png

ADI ADMT4000


根据分析我们可以直接飞线实现供电的操作。

image.png

飞线供电实现

2.2 原理图设计

image.png

原理图实现

2.3 关键器件选型

模块

选型

选型理由

主控

Teensy 4.0

支持 USB HID,可直接作为键盘使用

显示

SSD1306 OLED 128x64

I2C 接口,库支持完善

灯珠

WS2812B x4

单线控制,视觉反馈

马达

震动马达 + 二极管保护

防止电流倒灌损坏芯片

稳压

AP2112K-3.3

给马达和 WS2812B 提供稳定的输出

按键

BUTTON

进行按键对应菜单操作

三、硬件实现

3.1 主控板设计

PCB 布局思路

        1.25mm 排座连接 ADMT4000,减少杜邦线使用

        背面丝印活动相关信息

        元器件紧凑排列

        标注相关丝印注释

image.png

PCB 正面图

image.png

PCB 背面图

引脚分配

功能

引脚

马达 PWM

GPIO 5

彩色灯数据

GPIO 6

左按键

GPIO 7

右按键

GPIO 8

OLED SDA

GPIO 18

OLED SCL

GPIO 19

3.2 焊接组装

注意:这里的排线连接线需要购买其中反向的,否则其中的引脚则对应不上

马达我直接贴在板子的 typec 口上进行震动反馈。

image.png

焊接完成连接实物图

四、软件实现

4.1 开发环境

        核心库WireAdafruit_GFXAdafruit_SSD1306FastLED

        开发板Teensy 4.0

        串口速率115200 baud

4.2 测试软件初始化

void setup() {
Serial.begin(115200);
delay(1000);

// --- 按键初始化 ---
// 硬件已加电容到 GND,开启内部上拉即可
pinMode(SW_LEFT_PIN, INPUT_PULLUP);
pinMode(SW_RIGHT_PIN, INPUT_PULLUP);

// --- 马达初始化 ---
pinMode(MOTOR_PWM_PIN, OUTPUT);
digitalWrite(MOTOR_PWM_PIN, LOW);

// --- WS2812B 初始化 ---
FastLED.addLeds<WS2812B, LED_DATA_PIN, GRB>(leds, NUM_LEDS);
// ⚠️ 重要:限制最大亮度保护 USB 供电
FastLED.setBrightness(30);
fill_solid(leds, NUM_LEDS, CRGB::Black);
FastLED.show();

// --- OLED 初始化 ---
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
// 屏幕连接异常,红灯狂闪报警
while(true) {
fill_solid(leds, NUM_LEDS, CRGB::Red); FastLED.show(); delay(100);
fill_solid(leds, NUM_LEDS, CRGB::Black); FastLED.show(); delay(100);
}
}

Serial.println("✅ 所有外设初始化成功!");
}


4.3 测试主循环逻辑

void loop() {
bool leftPressed = (digitalRead(SW_LEFT_PIN) == LOW);
bool rightPressed = (digitalRead(SW_RIGHT_PIN) == LOW);

if (leftPressed) {
// 左键:轻震 + 绿灯 + 屏幕提示
Serial.println("<- 左键被按下!");
display.clearDisplay();
display.setCursor(0, 25);
display.println("LEFT BTN!");
display.display();

fill_solid(leds, NUM_LEDS, CRGB::Green);
FastLED.show();

analogWrite(MOTOR_PWM_PIN, 80); // 轻微震动

while(digitalRead(SW_LEFT_PIN) == LOW) { delay(10); }
analogWrite(MOTOR_PWM_PIN, 0);
}
else if (rightPressed) {
// 右键:狂震 + 蓝灯 + 屏幕提示
Serial.println("-> 右键被按下!");
display.clearDisplay();
display.setCursor(0, 25);
display.println("RIGHT BTN!");
display.display();

fill_solid(leds, NUM_LEDS, CRGB::Blue);
FastLED.show();

analogWrite(MOTOR_PWM_PIN, 200); // 强力震动

while(digitalRead(SW_RIGHT_PIN) == LOW) { delay(10); }
analogWrite(MOTOR_PWM_PIN, 0);
}
else {
// 待机:呼吸灯 + 屏幕显示
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.println("CYBER-VAULT OS v1.0");
display.setTextSize(2);
display.setCursor(10, 30);
display.println("WAITING...");
display.display();

// 紫色呼吸灯效果
fill_solid(leds, NUM_LEDS, CHSV(190, 255, quadwave8(breath)));
FastLED.show();
breath += 2;

delay(20);
}
}

如果板子的效果有正确的反馈说明板子焊接没有问题--- 这个是测试我设计的焊接小板的测试软件!

 

五、芯片分析

https://www.analog.com/media/en/technical-documentation/data-sheets/admt4000.pdf

我们在官网中可以获得其中的相关资料,我们一步步的查看其中的相关信息,通过数据手册来了解其中要如何驱动。

5.1 通讯协议 SPI

image.png

我们可以知道其中 SPI 的模式是 0,然后其中的传输数据是 32 位。

5.2 校验 CRC

image.png

读写寄存器的 CRC 校验是不同的,需要区分判断

uint8_t calculateCRC5(uint8_t is_read, uint8_t address, uint16_t data, uint8_t status_bits) {
uint32_t data_in = 0;
// is_read: 0代表读,1代表写
data_in |= ((uint32_t)(is_read & 0x01) << 25);
data_in |= ((uint32_t)(address & 0x3F) << 19);
data_in |= ((uint32_t)(data & 0xFFFF) << 3);
data_in |= (status_bits & 0x07);

uint8_t shft[5] = {1, 1, 1, 1, 1};

for (int i = 25; i >= 0; i--) { // 26 次循环
uint8_t bit_val = (data_in >> i) & 0x01;
uint8_t xor_0 = bit_val ^ shft[4];
shft[4] = shft[3];
shft[3] = shft[2];
shft[2] = shft[1] ^ xor_0;
shft[1] = shft[0];
shft[0] = xor_0;
}

uint8_t crc = (shft[4] << 4) | (shft[3] << 3) | (shft[2] << 2) | (shft[1] << 1) | shft[0];
return crc;
}

5.3 读写寄存器

image.pngimage.png


uint16_t readSPIRegister32(uint8_t address) {

digitalWrite(CS_PIN, LOW);

uint8_t cmd_byte = address & 0x3F;
uint8_t rx0 = SPI.transfer(cmd_byte);

uint8_t rx1 = SPI.transfer(0x00);

uint8_t rx2 = SPI.transfer(0x00);

uint8_t rx3 = SPI.transfer(0x00);

digitalWrite(CS_PIN, HIGH);
SPI.endTransaction();

uint16_t data = (rx1 << 8) | rx2;
uint8_t status_bits = (rx3 >> 5) & 0x07;
uint8_t received_crc = rx3 & 0x1F;
uint8_t calculated_crc = calculateCRC5(0, cmd_byte, data, status_bits);
if (calculated_crc == received_crc) {
return data;
} else {

return 0xFFFF; // 返回错误代码
}
}

void writeSPIRegister32(uint8_t address, uint16_t data) {
uint8_t crc = calculateCRC5(1, address, data, 0);
uint8_t byte1 = 0x40 | (address & 0x3F);
uint8_t byte2 = (data >> 8) & 0xFF;
uint8_t byte3 = data & 0xFF;
uint8_t byte4 = crc & 0x1F;
digitalWrite(CS_PIN, LOW);

SPI.transfer(byte1);
SPI.transfer(byte2);
SPI.transfer(byte3);
SPI.transfer(byte4);

digitalWrite(CS_PIN, HIGH); // 通信结束
SPI.endTransaction();
}

5.4 寄存器查看

其中最主要的功能,就是查看其中的相关寄存器地址和是否读写数据位,来实现我们的操作,具体内容可以到对应的寄存器的解释查看。

image.png

 

六、完整模块详解

6.1 系统总体架构方案框图

💡 以下流程图展示了整个软件系统的运行逻辑

image.png

6.2 主菜单(STATE_MENU

case STATE_MENU:
{
u8g2_prep();

// 菜单项列表
const char* ms[] = {
"1. 圈数密码锁",
"2. 街机打砖块",
"3. 电脑调音",
"4. 系统监控"
};

for(int i=0; i<4; i++){
int y_pos = 15 + i*15;
if(i==menuIndex) u8g2.drawUTF8(5, y_pos, "> "); // 选中标记
u8g2.drawUTF8(20, y_pos, ms[i]);
}
u8g2.sendBuffer();

// 💡 旋钮旋转切换菜单(每 15° 切换一项)
float menuDiff = currentAngle - lastMenuAngle;
if (menuDiff > 180) menuDiff -= 360;
if (menuDiff < -180) menuDiff += 360;

if (abs(menuDiff) >= 15.0) {
if (menuDiff > 0) menuIndex = (menuIndex + 1) % 4;
else menuIndex = (menuIndex - 1 + 4) % 4;
hapticTick(); // 菜单切换震动反馈
lastMenuAngle = currentAngle;
}

// 右键进入选中功能
if(right) {
hapticTick();
if(menuIndex==0) {
currentState = STATE_PWD_INPUT;
inputStep = 0;
stepStartTurns = hardwareTurns;
}
if(menuIndex==1) { /* 初始化游戏参数 */ }
if(menuIndex==2) { currentState = STATE_VOLUME_CTRL; lastTickAngle = currentAngle; }
if(menuIndex==3) { currentState = STATE_SYS_INFO; }
while(digitalRead(SW_RIGHT_PIN)==LOW);
}
break;
}

image.png

主菜单界面

6.3 圈数密码锁(STATE_PWD_INPUT

🎯 核心功能:通过旋转旋钮输入圈数,模仿机械密码锁的解锁体验

核心机制悬停确认

case STATE_PWD_INPUT:
{
int currentDiffTurns = hardwareTurns - stepStartTurns;

u8g2_prep();
u8g2.drawUTF8(15, 15, "--- 机械金库锁 ---");

// 显示目标圈数
u8g2.setCursor(5, 35);
u8g2.print("目标: ");
if (pwdTurns[inputStep] > 0) { u8g2.print("右转 "); u8g2.print(pwdTurns[inputStep]); u8g2.print(" 圈"); }
else { u8g2.print("左转 "); u8g2.print(-pwdTurns[inputStep]); u8g2.print(" 圈"); }

// 显示当前圈数
u8g2.setCursor(5, 50);
u8g2.print("当前: ");
if (currentDiffTurns > 0) { u8g2.print("右转 "); u8g2.print(currentDiffTurns); u8g2.print(" 圈"); }
else if (currentDiffTurns < 0) { u8g2.print("左转 "); u8g2.print(-currentDiffTurns); u8g2.print(" 圈"); }
else { u8g2.print("0 圈"); }

// 🔑 悬停确认逻辑:停在目标圈数 1 秒后自动进入下一步
static unsigned long dwellTime = 0;
static bool isDwelling = false;

if (currentDiffTurns == pwdTurns[inputStep]) {
if (!isDwelling) {
isDwelling = true;
dwellTime = millis();
} else {
unsigned long holdingTime = millis() - dwellTime;

// 绘制确认进度条(1 秒读满)
int barWidth = map(holdingTime, 0, 1000, 0, 118);
if (barWidth > 118) barWidth = 118;
u8g2.drawFrame(4, 57, 120, 6);
u8g2.drawBox(5, 58, barWidth, 4);

// 悬停超过 1 秒,自动确认
if (holdingTime >= 1000) {
inputStep++;
stepStartTurns = hardwareTurns;
isDwelling = false;

if (inputStep >= 3) {
// 全部密码输入完成,解锁!
currentState = STATE_UNLOCKED;
analogWrite(MOTOR_PWM_PIN, 255);
delay(500);
analogWrite(MOTOR_PWM_PIN, 0);
resetHardwareTurns();
} else {
hapticConfirm(); // 阶段成功震动
}
}
}
} else {
isDwelling = false; // 偏离目标,打断确认
}

u8g2.sendBuffer();

// 每当物理圈数变化时,震动提示
static int8_t lastVibrationTurn = hardwareTurns;
if (hardwareTurns != lastVibrationTurn) {
analogWrite(MOTOR_PWM_PIN, 255);
delay(60);
analogWrite(MOTOR_PWM_PIN, 0);
lastVibrationTurn = hardwareTurns;
}

// 右键重置当前步骤
if (right) {
inputStep = 0;
stepStartTurns = hardwareTurns;
isDwelling = false;
hapticBoom();
while(digitalRead(SW_RIGHT_PIN) == LOW);
}

if(left) { currentState = STATE_MENU; lastMenuAngle = currentAngle; }
break;
}

image.png

密码锁界面

6.4 音量控制(STATE_VOLUME_CTRL

💡 功能:旋钮旋转调节电脑系统音量,右键一键静音

case STATE_VOLUME_CTRL:
{
u8g2_prep();
u8g2.drawUTF8(20, 20, "[[ 桌面调音台 ]]");
u8g2.drawUTF8(10, 45, "旋钮:调音 | 右键:静音");
u8g2.sendBuffer();

float diff = currentAngle - lastTickAngle;
if (diff > 180) diff -= 360;
if (diff < -180) diff += 360;

// 🔑 每旋转 5° 发送一次音量调节信号
if (abs(diff) >= 5.0) {
hapticTick();
if (diff > 0) {
Keyboard.press(KEY_MEDIA_VOLUME_INC);
Keyboard.release(KEY_MEDIA_VOLUME_INC);
}
else {
Keyboard.press(KEY_MEDIA_VOLUME_DEC);
Keyboard.release(KEY_MEDIA_VOLUME_DEC);
}
lastTickAngle = currentAngle;
}

// 右键静音
if (right) {
analogWrite(MOTOR_PWM_PIN, 255);
delay(80);
analogWrite(MOTOR_PWM_PIN, 0);
Keyboard.press(KEY_MEDIA_MUTE);
Keyboard.release(KEY_MEDIA_MUTE);
while(digitalRead(SW_RIGHT_PIN) == LOW);
}

if(left) { currentState = STATE_MENU; lastMenuAngle = currentAngle; }
break;
}

image.png

音量控制界面

6.5 系统监控(STATE_SYS_INFO

case STATE_SYS_INFO:
{
u8g2_prep();
u8g2.drawUTF8(30, 15, "[ 硬件监控 ]");
u8g2.setCursor(15, 33);
u8g2.print("核心温度: ");
u8g2.print(currentTemp, 1);
u8g2.print(" C");

u8g2.setCursor(15, 48);
u8g2.print("拨盘角度: ");
u8g2.print(currentAngle, 1);
u8g2.print("°");

u8g2.setCursor(15, 63);
u8g2.print("绝对圈数: ");
u8g2.print(hardwareTurns);
u8g2.print(" 圈");

u8g2.sendBuffer();

if(left) { currentState = STATE_MENU; lastMenuAngle = currentAngle; }
break;
}

image.png

系统监控界面

6.6 打砖块游戏(STATE_BALL_GAME

🎮 玩法:旋钮控制底部挡板,让小球消除所有砖块

case STATE_BALL_GAME:
{
u8g2_prep();

if (gameState == 0) {
// ========== 游戏进行中 ==========

// 1. 旋钮控制挡板(带灵敏度调整)
float diff = currentAngle - lastTickAngle;
if (diff > 180) diff -= 360;
if (diff < -180) diff += 360;
if (abs(diff) > 1.0) {
paddleX += diff * 0.6; // 灵敏度系数
if (paddleX < 0) paddleX = 0;
if (paddleX > 128 - paddleWidth) paddleX = 128 - paddleWidth;
lastTickAngle = currentAngle;
}

// 2. 小球运动
ballX += ballVX;
ballY += ballVY;

// 边界反弹
if (ballX <= 0 || ballX >= 126) ballVX = -ballVX;
if (ballY <= 0) ballVY = -ballVY;

// 3. 挡板碰撞检测
if (ballY >= 56 && ballY <= 60
&& ballX >= paddleX - 2
&& ballX <= paddleX + paddleWidth + 2
&& ballVY > 0) {
ballVY = -ballVY;
hapticTick();
}

// 4. 砖块碰撞检测(16 宫格砖块)
for (int i = 0; i < 16; i++) {
if (bricks & (1 << i)) {
int bx = (i % 8) * 16;
int by = (i / 8) * 8 + 10;
if (ballX >= bx && ballX <= bx + 14
&& ballY >= by && ballY <= by + 6) {
bricks &= ~(1 << i); // 消除砖块
ballVY = -ballVY;
hapticTick();
break;
}
}
}

// 5. 游戏结束判断
if (bricks == 0) gameState = 1; // 通关
if (ballY >= 64) { gameState = 2; hapticBoom(); } // 失败

// 6. 绘制画面
u8g2.drawBox(paddleX, 58, paddleWidth, 4);
u8g2.drawDisc(ballX, ballY, 2);
for (int i = 0; i < 16; i++) {
if (bricks & (1 << i)) {
u8g2.drawBox((i % 8) * 16, (i / 8) * 8 + 10, 14, 6);
}
}
}
else if (gameState == 1) {
// ========== 通关画面 ==========
u8g2.drawUTF8(35, 30, "通关成功!");
u8g2.drawUTF8(15, 50, "左:返回 | 右:重试");
if (right) {
ballX = 64.0; ballY = 40.0;
ballVX = 1.5; ballVY = -1.5;
paddleX = 52.0;
bricks = 0xFFFF;
gameState = 0;
lastTickAngle = currentAngle;
while(digitalRead(SW_RIGHT_PIN)==LOW);
}
}
else if (gameState == 2) {
// ========== 失败画面 ==========
u8g2.drawUTF8(35, 30, "Game Over");
u8g2.drawUTF8(15, 50, "左:返回 | 右:重试");
if (right) { /* 同上,重置游戏 */ }
}

u8g2.sendBuffer();

if(left) { currentState = STATE_MENU; lastMenuAngle = currentAngle; }
break;
}

image.png

打砖块游戏界面

6.7 解锁成功(STATE_UNLOCKED

case STATE_UNLOCKED:
{
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_wqy16_t_chinese3);
u8g2.drawUTF8(20, 40, "You Win!");
u8g2.sendBuffer();

// 💡 彩虹灯效果庆祝
static uint8_t hue = 0;
fill_rainbow(leds, NUM_LEDS, hue++, 30);
FastLED.show();

// 任意按键返回菜单
if(left || right) {
currentState = STATE_MENU;
lastMenuAngle = currentAngle;
while(digitalRead(SW_LEFT_PIN)==LOW || digitalRead(SW_RIGHT_PIN)==LOW);
}
break;
}

七、调试记录

7.1 问题汇总表

#

问题现象

原因分析

解决方案

预防措施

1

稳压芯片发烫

彩色灯焊接方向反了,导致电流倒灌短路

拆除灯珠重新焊接,注意方向

焊接前用万用表确认 PCB 丝印方向

2

部分彩色灯不亮

灯珠虚焊或方向错误

检查不亮灯珠的焊接

上电前目视检查焊点

3

马达震动时系统重启

瞬时电流过大

添加二极管保护电路

马达使用独立 PWM 控制

7.2 调试心得

关于 WS2812B 灯珠

        ⚠️ 正负极不能接反,接反会直接烧毁灯珠

        灯珠串联连接,数据信号从 DIN 流入,DOUT 流出

        如果只有前半段亮,检查后半段灯珠的焊接

关于 PWM 马达

        建议加一个反向二极管,防止关闭时产生反向电动势

        震动强度不要调太高,避免瞬时电流过大

关于 OLED 屏幕

        I2C 地址固定为 0x3C

        SDA GPIO 18SCL GPIO 19Teensy 4.0 Wire 接口)

八、总结与改进

8.1 成果总结

完成项

状态

供电改造(飞线供电)

完成

主控板 PCB 设计

完成

OLED 状态显示

完成

WS2812B 彩色灯控制

完成

震动马达反馈

完成

按键交互

完成

8.2 经验沉淀

1.      供电设计优先:项目开始前先确认供电方案,避免后期被动

2.      灯珠注意方向WS2812B 焊接前务必确认 PCB 丝印方向

3.      亮度限制WS2812B 最大亮度会拉垮 USB 供电

附件下载
ADMT4000.ino
ProPrj_密码锁_2026-04-14.epro
团队介绍
认真对待,详细且全面
团队成员
冲向天空的猪
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号