这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 活动中心 » 有奖活动 » 【分享开发笔记,赚取电动螺丝刀】摇杆模块控制实现LED贪吃蛇游戏

共9条 1/1 1 跳转至

【分享开发笔记,赚取电动螺丝刀】摇杆模块控制实现LED贪吃蛇游戏

助工
2025-12-23 21:36:41     打赏

一、硬件介绍

1、产品特点

XIAO ESP32C6基于ESP32-C6芯片(两个32位RISC-V 处理器)构建,其中包含一个运行频率高达160 MHz的高性能处理器;

以及一个时钟频率高达20 MHz的低功耗32 位 RISC-V处理器,同时具有512KB SRAM 和 4 MB Flash等;


image-20251230144940209.png



image-20251230144940209


2、功能引脚示意图 / 原理图

板载LED灯:GPIO15 引脚控制(黄色LED)

image-20251230145808421.png

充电指示灯

XIAO ESP32C6 具有电池充电红色指示灯


当未连接电池时:连接Type-C线缆时红灯亮起,30秒后熄灭;

当连接电池并插入Type-C线缆充电时:红灯闪烁;

当电池通过Type-C连接完全充满时:红灯熄灭;

板载按键

BOOT按键:当程序无法烧录时,按住该按键重新上电时,即可进入BootLoader模式,重新进行烧录;

RESET按键:用于开发板复位;


image-20251230150009143.png

image-20251230150009143


主要引脚图


image-20251230144827796.png


image-20251230144827796

主要原理图

image-20251230145133008.png



image-202512301451330083、外部硬件

1、摇杆模块

摇杆模块,随着摇杆方向的变化,阻值也会随着变化,从而改变输出的值;

初始状态下X,Y读出电压为供电电压的1/2左右( 1.65V / 2.5V );

当随着X、Y方向移动时,读出电压值减小(0V) / 增大(VCC);


其中X、Y方向输出的数据为模拟量,而Z轴输出的为数字量 (0 / 1);


image-20251230151132425.png


模块引脚介绍:

image.png

2、8*8LED模块【WS2812B】

WS2812B是一个集控制电路与发光电路于一体的智能外控LED光源其外型与一个5050LED灯珠相同, 每个元件即为一个像素点;

数据协议采用单线归零码的通讯方式, 像素点在上电复位以后, DIN端接受从控制器传输过来的数据;


image-20250626181934299.png

image-20250626181934299

主要特点:

● IC控制电路与LED点光源共用一个电源。

● 控制电路与RGB芯片集成在一个5050封装的元器件中, 构成一个完整的外控像素点。

● 每个像素点的三基色颜色可实现256级亮度显示, 完成16777216种颜色的全真色彩显示。

● 端口扫描频率2KHz/s。

● 当刷新速率30帧/秒时, 级联数不小于1024点。

● 数据发送速度可达800Kbps。



3、喇叭模块【GSPK2307P-8R1W】

image-20250626165019254.png


image.png



二、硬件连接


模块与开发板引脚之间的连接方式如下:


image.png


实物效果搭建如下

image-20251230181725294.png


三、项目实现思想

【摇杆控制LED贪吃蛇游戏】

实现效果:通过摇杆模块控制8*8 LED模块以实现贪吃蛇游戏;

1、通过控制摇杆上、下、左、右分别控制LED移动方向;

2、按下按钮时,实现LED清屏复位(既游戏重启);

3、游玩过程中,游戏运行速度会随着蛇身长度逐渐加速;

4、当蛇碰到墙壁或自身时,喇叭模块发生声音,提示游戏结束(此时需按下按钮,重新开启);

通过ADC功能获取摇杆模块数据;

通过PWM功能控制喇叭模块、LED模块;





四、功能实现




安装FastLED 库;


image-20251230155501724.png



主要相关代码


#include <FastLED.h>

//#define DEBUG   //调试 获取摇杆数值

#ifdef DEBUG
unsigned long lastDebugPrint = 0;
#endif

//喇叭引脚
#define Sound_Pin D7

//  摇杆引脚
#define VRx A0
#define VRy A1
#define SW  D9

//摇杆XY初始值
#define X_POS 408
#define Y_POS 416

#define LED_PIN D8  //  LED_IN 引脚
#define LED_COLS 8  // 列
#define LED_ROWS 8  // 行
#define NUM_LEDS (LED_COLS * LED_ROWS)  // 8*8
#define LED_TYPE WS2812B   
#define COLOR_ORDER GRB

#define UP 0
#define DOWN 1
#define LEFT 2
#define RIGHT 3

CRGB HeadColor(255,0,0);  //蛇头颜色(R)
CRGB BodyColor(0,255,0);  //蛇身颜色(G)
CRGB FoodColor(0,0,255);  //食物颜色(B)
CRGB leds[NUM_LEDS];

unsigned long lastUpdate = 0;
int gameSpeed = 900;  // 游戏运行初始速度ms

static byte lastDir = UP; //默认方向向上
bool buttonPressed = false;
unsigned long lastTime = 0;
const int Delay = 50;
bool collisionsState = false;

// 蛇结构体
struct {
  int x, y;           // 蛇头坐标
  int tail[65][2];   // 蛇身坐标数组 (x,y)
  int length = 3;     // 初始长度
  int dir = 0;       // 运动方向(0上,1下,2左,3右) 
} snake;

// 食物结构体
struct {
  int x, y;
  bool eaten = true;  //食物状态
} food;

//移动蛇身体
void moveSnake() {
  for(int i=snake.length-1; i>0; i--){
    snake.tail[i][0] = snake.tail[i-1][0];  //x
    snake.tail[i][1] = snake.tail[i-1][1];  //y
  }
  //更新蛇头位置
  snake.tail[0][0] = snake.x;
  snake.tail[0][1] = snake.y;
  
	// 移动蛇头	(坐标系)
  switch(snake.dir){
    case UP: snake.y++; break; // 上
    case DOWN: snake.y--; break; // 下
    case LEFT: snake.x++; break; // 左
    case RIGHT: snake.x--; break; // 右
  }
}
//生成食物
void spawnFood() {
  if(food.eaten){
    bool valid = false;
    while(!valid){
      //生成食物位置
      valid = true;
      food.x = random(LED_COLS);
      food.y = random(LED_ROWS);
      // 检查是否与蛇重叠
      if(food.x == snake.x && food.y == snake.y) valid = false;
      for(int i=0; i<snake.length; i++){
        if(food.x == snake.tail[i][0] && food.y == snake.tail[i][1]){
          valid = false;
          break;
        }
      }
    }
    food.eaten = false;
  }
}
//蛇碰撞检测
void checkCollisions() {
  // 吃食物检测
  if(snake.x == food.x && snake.y == food.y){
    snake.length++;
    food.eaten = true;
    gameSpeed = max(100, gameSpeed-50); // 游戏加速
    spawnFood();
  }
  // 撞墙检测
  if(snake.x<0 || snake.x>=LED_COLS || snake.y<0 || snake.y>=LED_ROWS){
    playSound();
    collisionsState = true; 
  }
  // 撞自身检测
  for(int i=0; i<snake.length; i++){
    if(snake.x == snake.tail[i][0] && snake.y == snake.tail[i][1]){
      playSound();
      collisionsState = true;
    }
  }
}
//刷新显示
void updateDisplay() {
  if(!collisionsState){ //发生碰撞 暂停显示
  FastLED.clear();
  // 绘制食物
  leds[food.y * LED_COLS + food.x] = FoodColor;
  // 绘制蛇头
  leds[snake.y * LED_COLS + snake.x] = HeadColor;
  // 绘制蛇身
  for(int i=0; i<snake.length; i++){
    int index = snake.tail[i][1] * LED_COLS + snake.tail[i][0];
    leds[index] = BodyColor;
  }
  FastLED.show();
  }
}
//重新开始
void resetGame(){
  collisionsState = false; 
  gameSpeed = 500;
  snake.x = LED_COLS/2;
  snake.y = LED_ROWS/2;
  snake.length = 3;
  
  // 用当前时间重置随机种子 重新生成食物
  randomSeed(millis());
  food.eaten = true;
  spawnFood();
  updateDisplay();
}

//摇杆按下
void buttonSet() {
  int buttonState = digitalRead(SW);
  if (buttonState == LOW && !buttonPressed && (millis() - lastTime) > Delay) {
    buttonPressed = true;
    lastTime = millis();
    resetGame();
  }else if (buttonState == HIGH && buttonPressed) {
    buttonPressed = false;
    lastTime = millis();
  }
}
//摇杆XY数据读取
void controllerRead() {
  byte newDir = lastDir;  
  int xVal = analogRead(VRx) - X_POS;
  int yVal = analogRead(VRy) - Y_POS;
  //方向保持上一次
  if(abs(xVal)< 100 && abs(yVal)<100) 
  {
    xVal = 0;
    yVal = 0;
    snake.dir = lastDir;
  }else{

    // 计算角度方向 (对应摇杆X / Y)
  float angle = atan2(yVal, xVal) * 180/PI;
  if (angle >= -45 && angle < 45) {
    newDir = LEFT;      // 左
  } else if (angle >= 45 && angle < 135) {
    newDir = UP;         // 上
  } else if (angle >= 135 || angle < -135) {
    newDir = RIGHT;       // 右
  } else if (angle >= -135 && angle < -45) {
    newDir = DOWN;       // 下
  	}
  }
  // 防止直接反向180°移动
  if (!(snake.dir == RIGHT && newDir == LEFT) &&
      !(snake.dir == LEFT && newDir == RIGHT) &&
      !(snake.dir == UP && newDir == DOWN) &&
      !(snake.dir == DOWN && newDir == UP)) {
    snake.dir = newDir;   //更新方向
    lastDir = newDir;
  }
} 
//喇叭播放声音
void playSound(){
  tone(Sound_Pin, 3000, 1000);  //3Khz 1s
}

void setup() {
  //串口初始化
  Serial.begin(115200);
  while (!Serial);

  analogReadResolution(10); // 10位ADC
  //摇杆初始化
  pinMode(VRx, INPUT);
  pinMode(VRy, INPUT);
  pinMode(SW, INPUT_PULLUP);
  // 初始化snake LED
  FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS);
  FastLED.setBrightness(15);
  snake.x = LED_COLS/2;
  snake.y = LED_ROWS/2;
  for(int i=0; i<snake.length; i++){
    snake.tail[i][0] = snake.x - i;
    snake.tail[i][1] = snake.y;
  }
    randomSeed(millis());  // 食物随机数种子
}


void loop() {
  //控制游戏速度
  if(millis() - lastUpdate > gameSpeed){
    lastUpdate = millis();
    //摇杆数据读取
    controllerRead();
    //摇杆按下
    buttonSet();
    //移动蛇位置
    moveSnake();
    //检测碰撞
    checkCollisions();
    //更新显示
    updateDisplay();
  }

#ifdef DEBUG    // 调试(获取实际摇杆X / Y值)
  if (millis() - lastDebugPrint > 500) {
    lastDebugPrint = millis();
    int xRaw = analogRead(VRx); //X_POS
    int yRaw = analogRead(VRy); //Y_POS
    int xVal = xRaw - X_POS;
    int yVal = yRaw - Y_POS;
    Serial.print("X="); Serial.print(xRaw);
    Serial.print(", Y="); Serial.print(yRaw);
    Serial.print(" | Δx="); Serial.print(xVal);
    Serial.print(", Δy="); Serial.println(yVal);
  }
#endif

}




五、效果演示

1、通过控制摇杆上、下、左、右分别控制LED移动方向;

2、按下按钮时,实现LED清屏复位(既游戏重启);

3、游玩过程中,游戏运行速度会随着蛇身长度逐渐加速;

4、当蛇碰到墙壁或自身时,喇叭模块发生声音,提示游戏结束(此时需按下按钮,重新开启);


LED_贪吃蛇.gif






六、总结



相关注意事项




1、关于角度方向

  float angle = atan2(yVal, xVal) * 180/PI;
  if (angle >= -45 && angle < 45) {
    newDir = LEFT;      // 左
  } else if (angle >= 45 && angle < 135) {
    newDir = UP;         // 上
  } else if (angle >= 135 || angle < -135) {
    newDir = RIGHT;       // 右
  } else if (angle >= -135 && angle < -45) {
    newDir = DOWN;       // 下
  	}

摇杆实际的XY坐标系与常规的XY坐标系是镜像关系;


因此左右方向是相反的;


image-20251230185853205.png



2、每次重新开始游戏时,需要重新生成食物位置,不然食物开始时永远都会在同一地方;


void resetGame(){
		...
 
  // 用当前时间重置随机种子 重新生成食物
  randomSeed(millis());
  food.eaten = true;
  spawnFood();
  updateDisplay();
}




3、开启#define DEBUG  用于调试以及获取实际摇杆的X / Y初始值变化等,以适配各种硬件不同的摇杆;



















关键词: WS2812B    

院士
2025-12-24 09:46:00     打赏
2楼

真棒,真棒。

话说这么多的接线,楼主也是厉害了啊


专家
2025-12-27 15:51:14     打赏
3楼

楼主,这个左右是不是标注反了

   // 计算角度方向  

  float angle = atan2(yVal, xVal) * 180/PI;  

  if (angle >= -45 && angle < 45) {  

    newDir = LEFT;      // 左  

  } else if (angle >= 45 && angle < 135) {  

    newDir = UP;         // 上  

  } else if (angle >= 135 || angle < -135) {  

    newDir = RIGHT;       // 右  

  } else if (angle >= -135 && angle < -45) {  

    newDir = DOWN;       // 下  

      }  


高工
2025-12-28 09:19:19     打赏
4楼

如果能这蛇能长长,那就牛B了,AI再加工一下。


高工
2025-12-29 00:00:08     打赏
5楼

有没有查过为何吃了三个点后,每次出新点时,右下角就会有个点闪一下?


专家
2025-12-29 11:15:05     打赏
6楼

效果挺棒!有没有考虑使用中断方式处理?


专家
2025-12-29 11:27:07     打赏
7楼

编程方式非常赞!注释到位,逻辑清晰,方便他人学习!


高工
2025-12-30 07:54:32     打赏
8楼

这个红色点出现的是随机得嘛?


高工
2025-12-30 11:34:56     打赏
9楼

效果不错 


共9条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]