文章目录
-
- 一、项目概述
-
- 1.1 硬件准备
- 1.2 软件环境
- 二、硬件电路连接
-
- 2.1 TFT LCD(ILI9341)与STM32F407引脚映射
- 2.2 按键引脚映射
- 三、STM32CubeMX初始化配置
-
- 3.1 新建工程
- 3.2 系统配置
- 3.3 生成代码
- 四、TFT LCD驱动实现
-
- 4.1 创建驱动文件
-
- 文件名:tft_ili9341.h
- 文件名:tft_ili9341.c
- 4.2 驱动代码说明
- 五、按键驱动实现
-
- 5.1 创建按键驱动文件
-
- 文件名:key.h
- 文件名:key.c
- 5.2 按键驱动说明
- 六、贪吃蛇游戏实现
-
- 6.1 游戏逻辑流程图
- 6.2 创建贪吃蛇游戏文件
-
- 文件名:snake.h
- 文件名:snake.c
- 6.3 贪吃蛇代码说明
- 七、俄罗斯方块游戏实现
-
- 7.1 游戏逻辑流程图
- 7.2 创建俄罗斯方块游戏文件
-
- 文件名:tetris.h
- 文件名:tetris.c
- 7.3 俄罗斯方块代码说明
- 八、主函数整合
-
- 8.1 修改main.c文件
- 九、工程编译与下载
-
- 9.1 添加文件到工程
- 9.2 编译工程
- 9.3 下载到 STM32F407
- 十、运行与调试说明
-
- 10.1 开机效果
- 10.2 按键功能
- 10.3 贪吃蛇规则
- 10.4 俄罗斯方块规则
- 十一、常见问题与解决
-
- 11.1 TFT 不亮
- 11.2 TFT 花屏/白屏
- 11.3 按键无反应
- 11.4 游戏不移动/卡顿
- 十二、项目扩展方向(进阶)
- 十三、总结
一、项目概述
你想要基于STM32F407开发板实现一个怀旧游戏机,核心功能是驱动TFT液晶显示屏,并在屏幕上运行贪吃蛇和俄罗斯方块两款经典游戏。本教程从环境搭建到代码实现,全程面向零基础小白,每一步都提供详细操作和完整代码,确保你能一步步落地实现。
1.1 硬件准备
- 核心板:STM32F407ZGT6开发板(带最小系统)
- 显示屏:2.4寸/3.2寸TFT LCD(ILI9341驱动芯片,SPI/I8080并行接口,本教程用SPI接口)
- 外设:4个独立按键(上下左右)、5V电源、杜邦线若干
- 工具:ST-Link下载器、Keil5 MDK开发环境、USB转串口线(可选,用于调试)
1.2 软件环境
- Keil MDK-ARM V5.36及以上
- STM32CubeMX 6.0及以上(用于生成初始化代码)
- ST-Link驱动(用于下载程序)
- 串口调试助手(可选,如SSCOM)
二、硬件电路连接
2.1 TFT LCD(ILI9341)与STM32F407引脚映射
| VCC | 电源(3.3V) | 3.3V |
| GND | 地 | GND |
| SCL | SPI时钟 | PA5(SPI1_SCK) |
| SDA | SPI数据 | PA7(SPI1_MOSI) |
| RES | 复位 | PB0 |
| DC | 数据/命令 | PB1 |
| CS | 片选 | PA4 |
| BL | 背光 | PB2(PWM可选) |
2.2 按键引脚映射
| KEY1 | 上 | PC0 | 下拉(按下接3.3V) |
| KEY2 | 下 | PC1 | 下拉 |
| KEY3 | 左 | PC2 | 下拉 |
| KEY4 | 右 | PC3 | 下拉 |
| KEY5 | 确认/变换 | PC4 | 下拉 |
三、STM32CubeMX初始化配置
3.1 新建工程
3.2 系统配置
- 按键引脚(PC0-PC4):模式设为“Input Pull-Down”(下拉输入);
- TFT复位(PB0)、DC(PB1)、CS(PA4)、背光(PB2):模式设为“Output Push Pull”(推挽输出),初始电平设为高;
- 模式:Full-Duplex Master(全双工主机);
- 时钟分频:Prescaler设为2(时钟频率84MHz);
- 极性(CPOL):Low;相位(CPHA):1 Edge;
- 数据长度:8 Bits;
- NSS:Software(软件片选)。
3.3 生成代码
四、TFT LCD驱动实现
4.1 创建驱动文件
文件名:tft_ili9341.h
#ifndef __TFT_ILI9341_H
#define __TFT_ILI9341_H
#include "stm32f4xx_hal.h"
// 引脚定义
#define TFT_CS_PIN GPIO_PIN_4
#define TFT_CS_PORT GPIOA
#define TFT_DC_PIN GPIO_PIN_1
#define TFT_DC_PORT GPIOB
#define TFT_RST_PIN GPIO_PIN_0
#define TFT_RST_PORT GPIOB
#define TFT_BL_PIN GPIO_PIN_2
#define TFT_BL_PORT GPIOB
// 屏幕参数
#define TFT_WIDTH 240
#define TFT_HEIGHT 320
#define TFT_PIXEL_MAX (TFT_WIDTH * TFT_HEIGHT)
// 颜色定义(RGB565格式)
#define COLOR_BLACK 0x0000
#define COLOR_WHITE 0xFFFF
#define COLOR_RED 0xF800
#define COLOR_GREEN 0x07E0
#define COLOR_BLUE 0x001F
#define COLOR_YELLOW 0xFFE0
#define COLOR_GRAY 0x8410
// 函数声明
void TFT_Init(void); // TFT初始化
void TFT_Write_Cmd(uint8_t cmd); // 写命令
void TFT_Write_Data(uint8_t data); // 写数据
void TFT_Set_Window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1); // 设置显示窗口
void TFT_Draw_Pixel(uint16_t x, uint16_t y, uint16_t color); // 画点
void TFT_Fill_Rect(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color); // 填充矩形
void TFT_Clear_Screen(uint16_t color); // 清屏
void TFT_Draw_Line(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color); // 画线
void TFT_Draw_Rect(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color); // 画矩形
#endif
文件名:tft_ili9341.c
#include "tft_ili9341.h"
#include "spi.h"
// 延时函数(简易版,实际可改用HAL延时)
static void TFT_Delay_ms(uint32_t ms)
{
HAL_Delay(ms);
}
static void TFT_Delay_us(uint32_t us)
{
uint32_t delay = (HAL_RCC_GetHCLKFreq() / 8000000) * us;
while (delay—) {}
}
// 片选使能
static void TFT_CS_Enable(void)
{
HAL_GPIO_WritePin(TFT_CS_PORT, TFT_CS_PIN, GPIO_PIN_RESET);
}
// 片选失能
static void TFT_CS_Disable(void)
{
HAL_GPIO_WritePin(TFT_CS_PORT, TFT_CS_PIN, GPIO_PIN_SET);
}
// 设置DC为命令模式
static void TFT_DC_Cmd(void)
{
HAL_GPIO_WritePin(TFT_DC_PORT, TFT_DC_PIN, GPIO_PIN_RESET);
}
// 设置DC为数据模式
static void TFT_DC_Data(void)
{
HAL_GPIO_WritePin(TFT_DC_PORT, TFT_DC_PIN, GPIO_PIN_SET);
}
// SPI写字节
static void TFT_SPI_Write(uint8_t data)
{
HAL_SPI_Transmit(&hspi1, &data, 1, 100);
}
// 写命令
void TFT_Write_Cmd(uint8_t cmd)
{
TFT_CS_Enable();
TFT_DC_Cmd();
TFT_SPI_Write(cmd);
TFT_CS_Disable();
}
// 写数据
void TFT_Write_Data(uint8_t data)
{
TFT_CS_Enable();
TFT_DC_Data();
TFT_SPI_Write(data);
TFT_CS_Disable();
}
// 初始化TFT
void TFT_Init(void)
{
// 复位屏幕
HAL_GPIO_WritePin(TFT_RST_PORT, TFT_RST_PIN, GPIO_PIN_RESET);
TFT_Delay_ms(100);
HAL_GPIO_WritePin(TFT_RST_PORT, TFT_RST_PIN, GPIO_PIN_SET);
TFT_Delay_ms(100);
// 背光开启
HAL_GPIO_WritePin(TFT_BL_PORT, TFT_BL_PIN, GPIO_PIN_SET);
// ILI9341初始化序列
TFT_Write_Cmd(0xCF);
TFT_Write_Data(0x00);
TFT_Write_Data(0xC1);
TFT_Write_Data(0X30);
TFT_Write_Cmd(0xED);
TFT_Write_Data(0x64);
TFT_Write_Data(0x03);
TFT_Write_Data(0X12);
TFT_Write_Data(0X81);
TFT_Write_Cmd(0xE8);
TFT_Write_Data(0x85);
TFT_Write_Data(0x00);
TFT_Write_Data(0x78);
TFT_Write_Cmd(0xCB);
TFT_Write_Data(0x39);
TFT_Write_Data(0x2C);
TFT_Write_Data(0x00);
TFT_Write_Data(0x34);
TFT_Write_Data(0x02);
TFT_Write_Cmd(0xF7);
TFT_Write_Data(0x20);
TFT_Write_Cmd(0xEA);
TFT_Write_Data(0x00);
TFT_Write_Data(0x00);
TFT_Write_Cmd(0xC0); // Power control
TFT_Write_Data(0x23); // VRH[5:0]
TFT_Write_Cmd(0xC1); // Power control
TFT_Write_Data(0x10); // SAP[2:0];BT[3:0]
TFT_Write_Cmd(0xC5); // VCM control
TFT_Write_Data(0x3e); // Contrast
TFT_Write_Data(0x28);
TFT_Write_Cmd(0xC7); // VCM control2
TFT_Write_Data(0x86);
TFT_Write_Cmd(0x36); // Memory Access Control
TFT_Write_Data(0x48); // 屏幕旋转配置(竖屏)
TFT_Write_Cmd(0x3A);
TFT_Write_Data(0x55);
TFT_Write_Cmd(0xB1);
TFT_Write_Data(0x00);
TFT_Write_Data(0x18);
TFT_Write_Cmd(0xB6); // Display Function Control
TFT_Write_Data(0x08);
TFT_Write_Data(0x82);
TFT_Write_Data(0x27);
TFT_Write_Cmd(0xF2); // 3Gamma Function Disable
TFT_Write_Data(0x00);
TFT_Write_Cmd(0x26); // Gamma curve selected
TFT_Write_Data(0x01);
TFT_Write_Cmd(0xE0); // Set Gamma
TFT_Write_Data(0x0F);
TFT_Write_Data(0x31);
TFT_Write_Data(0x2B);
TFT_Write_Data(0x0C);
TFT_Write_Data(0x0E);
TFT_Write_Data(0x08);
TFT_Write_Data(0x4E);
TFT_Write_Data(0xF1);
TFT_Write_Data(0x37);
TFT_Write_Data(0x07);
TFT_Write_Data(0x10);
TFT_Write_Data(0x03);
TFT_Write_Data(0x0E);
TFT_Write_Data(0x09);
TFT_Write_Data(0x00);
TFT_Write_Cmd(0xE1); // Set Gamma
TFT_Write_Data(0x00);
TFT_Write_Data(0x0E);
TFT_Write_Data(0x14);
TFT_Write_Data(0x03);
TFT_Write_Data(0x11);
TFT_Write_Data(0x07);
TFT_Write_Data(0x31);
TFT_Write_Data(0xC1);
TFT_Write_Data(0x48);
TFT_Write_Data(0x08);
TFT_Write_Data(0x0F);
TFT_Write_Data(0x0C);
TFT_Write_Data(0x31);
TFT_Write_Data(0x36);
TFT_Write_Data(0x0F);
TFT_Write_Cmd(0x11); // Exit Sleep
TFT_Delay_ms(120);
TFT_Write_Cmd(0x29); // Display on
}
// 设置显示窗口
void TFT_Set_Window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1)
{
TFT_Write_Cmd(0x2A); // 列地址设置
TFT_Write_Data(x0 >> 8);
TFT_Write_Data(x0 & 0xFF);
TFT_Write_Data(x1 >> 8);
TFT_Write_Data(x1 & 0xFF);
TFT_Write_Cmd(0x2B); // 行地址设置
TFT_Write_Data(y0 >> 8);
TFT_Write_Data(y0 & 0xFF);
TFT_Write_Data(y1 >> 8);
TFT_Write_Data(y1 & 0xFF);
TFT_Write_Cmd(0x2C); // 写显存
}
// 画点
void TFT_Draw_Pixel(uint16_t x, uint16_t y, uint16_t color)
{
if(x >= TFT_WIDTH || y >= TFT_HEIGHT) return; // 超出范围则退出
TFT_Set_Window(x, y, x, y);
TFT_DC_Data();
TFT_CS_Enable();
TFT_SPI_Write(color >> 8);
TFT_SPI_Write(color & 0xFF);
TFT_CS_Disable();
}
// 填充矩形
void TFT_Fill_Rect(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color)
{
uint32_t i, area;
if(x0 > x1 || y0 > y1) return;
if(x1 >= TFT_WIDTH) x1 = TFT_WIDTH – 1;
if(y1 >= TFT_HEIGHT) y1 = TFT_HEIGHT – 1;
TFT_Set_Window(x0, y0, x1, y1);
TFT_DC_Data();
TFT_CS_Enable();
area = (x1 – x0 + 1) * (y1 – y0 + 1);
for(i = 0; i < area; i++)
{
TFT_SPI_Write(color >> 8);
TFT_SPI_Write(color & 0xFF);
}
TFT_CS_Disable();
}
// 清屏
void TFT_Clear_Screen(uint16_t color)
{
TFT_Fill_Rect(0, 0, TFT_WIDTH – 1, TFT_HEIGHT – 1, color);
}
// 画线(Bresenham算法)
void TFT_Draw_Line(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color)
{
int16_t dx = abs(x1 – x0), sx = x0 < x1 ? 1 : –1;
int16_t dy = –abs(y1 – y0), sy = y0 < y1 ? 1 : –1;
int16_t err = dx + dy, e2;
while(1)
{
TFT_Draw_Pixel(x0, y0, color);
if(x0 == x1 && y0 == y1) break;
e2 = 2 * err;
if(e2 >= dy) { err += dy; x0 += sx; }
if(e2 <= dx) { err += dx; y0 += sy; }
}
}
// 画矩形(仅边框)
void TFT_Draw_Rect(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color)
{
TFT_Draw_Line(x0, y0, x1, y0, color); // 上边框
TFT_Draw_Line(x0, y1, x1, y1, color); // 下边框
TFT_Draw_Line(x0, y0, x0, y1, color); // 左边框
TFT_Draw_Line(x1, y0, x1, y1, color); // 右边框
}
4.2 驱动代码说明
- TFT_Init():执行ILI9341芯片的初始化序列,这是屏幕能正常显示的核心步骤,包含电源、伽马、显示模式等配置;
- TFT_Set_Window():指定屏幕的显示区域,后续写数据只会在该区域生效;
- TFT_Draw_Pixel():最基础的画点函数,所有图形绘制都基于此;
- 填充、画线、画矩形等函数是在画点基础上的封装,方便游戏界面绘制。
五、按键驱动实现
5.1 创建按键驱动文件
文件名:key.h
#ifndef __KEY_H
#define __KEY_H
#include "stm32f4xx_hal.h"
// 按键定义
#define KEY_UP_PIN GPIO_PIN_0
#define KEY_UP_PORT GPIOC
#define KEY_DOWN_PIN GPIO_PIN_1
#define KEY_DOWN_PORT GPIOC
#define KEY_LEFT_PIN GPIO_PIN_2
#define KEY_LEFT_PORT GPIOC
#define KEY_RIGHT_PIN GPIO_PIN_3
#define KEY_RIGHT_PORT GPIOC
#define KEY_CONFIRM_PIN GPIO_PIN_4
#define KEY_CONFIRM_PORT GPIOC
// 按键状态
#define KEY_RELEASED 0
#define KEY_PRESSED 1
// 函数声明
uint8_t KEY_Read(uint8_t key); // 读取单个按键状态
uint8_t KEY_Scan(uint8_t mode); // 按键扫描(消抖)
void KEY_Init(void); // 按键初始化(实际已在CubeMX配置,此处仅占位)
#endif
文件名:key.c
#include "key.h"
// 读取单个按键状态
uint8_t KEY_Read(uint8_t key)
{
switch(key)
{
case 0: return HAL_GPIO_ReadPin(KEY_UP_PORT, KEY_UP_PIN);
case 1: return HAL_GPIO_ReadPin(KEY_DOWN_PORT, KEY_DOWN_PIN);
case 2: return HAL_GPIO_ReadPin(KEY_LEFT_PORT, KEY_LEFT_PIN);
case 3: return HAL_GPIO_ReadPin(KEY_RIGHT_PORT, KEY_RIGHT_PIN);
case 4: return HAL_GPIO_ReadPin(KEY_CONFIRM_PORT, KEY_CONFIRM_PIN);
default: return KEY_RELEASED;
}
}
// 按键扫描(mode=0:单次触发;mode=1:连续触发)
uint8_t KEY_Scan(uint8_t mode)
{
static uint8_t key_up = 1; // 按键松开标志
uint8_t key_val = 0;
if(mode) key_up = 1; // 连续触发模式,重置标志
if(key_up && (KEY_Read(0) || KEY_Read(1) || KEY_Read(2) || KEY_Read(3) || KEY_Read(4)))
{
HAL_Delay(20); // 消抖延时
key_up = 0;
if(KEY_Read(0)) key_val = 1; // 上
else if(KEY_Read(1)) key_val = 2; // 下
else if(KEY_Read(2)) key_val = 3; // 左
else if(KEY_Read(3)) key_val = 4; // 右
else if(KEY_Read(4)) key_val = 5; // 确认
}
else if(!KEY_Read(0) && !KEY_Read(1) && !KEY_Read(2) && !KEY_Read(3) && !KEY_Read(4))
{
key_up = 1;
}
return key_val;
}
// 按键初始化(CubeMX已配置GPIO,此处仅做初始化标记)
void KEY_Init(void)
{
// 空函数,GPIO初始化在main.c的MX_GPIO_Init()中
}
5.2 按键驱动说明
- KEY_Read():直接读取引脚电平,按下时引脚为高(因为下拉连接);
- KEY_Scan():加入20ms消抖延时,避免机械按键的抖动导致误触发;
- 支持“单次触发”和“连续触发”两种模式,游戏中移动操作适合连续触发。
六、贪吃蛇游戏实现
6.1 游戏逻辑流程图
#mermaid-svg-cxB7KNRXwZuKa3EO{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-cxB7KNRXwZuKa3EO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cxB7KNRXwZuKa3EO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cxB7KNRXwZuKa3EO .error-icon{fill:#552222;}#mermaid-svg-cxB7KNRXwZuKa3EO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cxB7KNRXwZuKa3EO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cxB7KNRXwZuKa3EO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cxB7KNRXwZuKa3EO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cxB7KNRXwZuKa3EO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cxB7KNRXwZuKa3EO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cxB7KNRXwZuKa3EO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cxB7KNRXwZuKa3EO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cxB7KNRXwZuKa3EO .marker.cross{stroke:#333333;}#mermaid-svg-cxB7KNRXwZuKa3EO svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cxB7KNRXwZuKa3EO p{margin:0;}#mermaid-svg-cxB7KNRXwZuKa3EO .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-cxB7KNRXwZuKa3EO .cluster-label text{fill:#333;}#mermaid-svg-cxB7KNRXwZuKa3EO .cluster-label span{color:#333;}#mermaid-svg-cxB7KNRXwZuKa3EO .cluster-label span p{background-color:transparent;}#mermaid-svg-cxB7KNRXwZuKa3EO .label text,#mermaid-svg-cxB7KNRXwZuKa3EO span{fill:#333;color:#333;}#mermaid-svg-cxB7KNRXwZuKa3EO .node rect,#mermaid-svg-cxB7KNRXwZuKa3EO .node circle,#mermaid-svg-cxB7KNRXwZuKa3EO .node ellipse,#mermaid-svg-cxB7KNRXwZuKa3EO .node polygon,#mermaid-svg-cxB7KNRXwZuKa3EO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-cxB7KNRXwZuKa3EO .rough-node .label text,#mermaid-svg-cxB7KNRXwZuKa3EO .node .label text,#mermaid-svg-cxB7KNRXwZuKa3EO .image-shape .label,#mermaid-svg-cxB7KNRXwZuKa3EO .icon-shape .label{text-anchor:middle;}#mermaid-svg-cxB7KNRXwZuKa3EO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-cxB7KNRXwZuKa3EO .rough-node .label,#mermaid-svg-cxB7KNRXwZuKa3EO .node .label,#mermaid-svg-cxB7KNRXwZuKa3EO .image-shape .label,#mermaid-svg-cxB7KNRXwZuKa3EO .icon-shape .label{text-align:center;}#mermaid-svg-cxB7KNRXwZuKa3EO .node.clickable{cursor:pointer;}#mermaid-svg-cxB7KNRXwZuKa3EO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-cxB7KNRXwZuKa3EO .arrowheadPath{fill:#333333;}#mermaid-svg-cxB7KNRXwZuKa3EO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-cxB7KNRXwZuKa3EO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-cxB7KNRXwZuKa3EO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cxB7KNRXwZuKa3EO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-cxB7KNRXwZuKa3EO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cxB7KNRXwZuKa3EO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-cxB7KNRXwZuKa3EO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-cxB7KNRXwZuKa3EO .cluster text{fill:#333;}#mermaid-svg-cxB7KNRXwZuKa3EO .cluster span{color:#333;}#mermaid-svg-cxB7KNRXwZuKa3EO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-cxB7KNRXwZuKa3EO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-cxB7KNRXwZuKa3EO rect.text{fill:none;stroke-width:0;}#mermaid-svg-cxB7KNRXwZuKa3EO .icon-shape,#mermaid-svg-cxB7KNRXwZuKa3EO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cxB7KNRXwZuKa3EO .icon-shape p,#mermaid-svg-cxB7KNRXwZuKa3EO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-cxB7KNRXwZuKa3EO .icon-shape rect,#mermaid-svg-cxB7KNRXwZuKa3EO .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cxB7KNRXwZuKa3EO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-cxB7KNRXwZuKa3EO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-cxB7KNRXwZuKa3EO :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
否
是
是
否
是
否
是
否
游戏初始化
清屏并绘制游戏边界
生成初始蛇身和食物
读取按键输入
按键是否有效?
更新蛇的移动方向
移动蛇头
是否吃到食物?
蛇身加长+生成新食物
移动蛇尾
是否撞墙/撞自身?
游戏结束,显示得分
延时控制游戏速度
是否重新开始?
返回游戏选择界面
6.2 创建贪吃蛇游戏文件
文件名:snake.h
#ifndef __SNAKE_H
#define __SNAKE_H
#include "tft_ili9341.h"
#include "key.h"
#include "stdlib.h"
#include "time.h"
// 游戏参数
#define SNAKE_BLOCK_SIZE 10 // 蛇块大小(像素)
#define SNAKE_MAX_LENGTH 100 // 蛇最大长度
#define SNAKE_X_MIN 10 // 游戏区域左边界
#define SNAKE_X_MAX 230 // 游戏区域右边界
#define SNAKE_Y_MIN 10 // 游戏区域上边界
#define SNAKE_Y_MAX 310 // 游戏区域下边界
// 方向定义
typedef enum
{
DIR_UP = 1,
DIR_DOWN,
DIR_LEFT,
DIR_RIGHT
} Snake_Dir;
// 蛇身结构体
typedef struct
{
uint16_t x;
uint16_t y;
} Snake_Body;
// 游戏数据结构体
typedef struct
{
Snake_Body body[SNAKE_MAX_LENGTH]; // 蛇身数组
uint8_t length; // 蛇当前长度
Snake_Dir dir; // 移动方向
uint16_t food_x; // 食物x坐标
uint16_t food_y; // 食物y坐标
uint16_t score; // 得分
uint8_t game_over; // 游戏结束标志
} Snake_Game;
// 函数声明
void Snake_Init(void); // 游戏初始化
void Snake_Draw_Block(uint16_t x, uint16_t y, uint16_t color); // 画蛇块/食物
void Snake_Generate_Food(void); // 生成食物
void Snake_Move(void); // 蛇移动
void Snake_Check_Collision(void); // 碰撞检测
void Snake_Game_Loop(void); // 游戏主循环
void Snake_Show_Score(void); // 显示得分
void Snake_Game_Over(void); // 游戏结束处理
#endif
文件名:snake.c
#include "snake.h"
Snake_Game snake_game; // 游戏全局变量
// 初始化游戏
void Snake_Init(void)
{
// 重置游戏状态
snake_game.length = 3;
snake_game.dir = DIR_RIGHT;
snake_game.score = 0;
snake_game.game_over = 0;
// 初始化蛇身(居中)
snake_game.body[0].x = TFT_WIDTH / 2;
snake_game.body[0].y = TFT_HEIGHT / 2;
snake_game.body[1].x = TFT_WIDTH / 2 – SNAKE_BLOCK_SIZE;
snake_game.body[1].y = TFT_HEIGHT / 2;
snake_game.body[2].x = TFT_WIDTH / 2 – 2 * SNAKE_BLOCK_SIZE;
snake_game.body[2].y = TFT_HEIGHT / 2;
// 清屏(黑色背景)
TFT_Clear_Screen(COLOR_BLACK);
// 绘制游戏边界(白色)
TFT_Draw_Rect(SNAKE_X_MIN – 1, SNAKE_Y_MIN – 1, SNAKE_X_MAX + 1, SNAKE_Y_MAX + 1, COLOR_WHITE);
// 生成初始食物
Snake_Generate_Food();
// 绘制初始蛇身(绿色)
for(uint8_t i = 0; i < snake_game.length; i++)
{
Snake_Draw_Block(snake_game.body[i].x, snake_game.body[i].y, COLOR_GREEN);
}
// 绘制初始食物(红色)
Snake_Draw_Block(snake_game.food_x, snake_game.food_y, COLOR_RED);
// 初始化随机数种子(基于系统时间,简易版)
srand(HAL_GetTick());
}
// 画蛇块/食物(填充矩形)
void Snake_Draw_Block(uint16_t x, uint16_t y, uint16_t color)
{
TFT_Fill_Rect(x, y, x + SNAKE_BLOCK_SIZE – 1, y + SNAKE_BLOCK_SIZE – 1, color);
}
// 生成食物(避免与蛇身重叠)
void Snake_Generate_Food(void)
{
uint8_t flag; // 食物是否与蛇身重叠的标志
do
{
flag = 0;
// 生成食物坐标(对齐块大小,且在游戏区域内)
snake_game.food_x = SNAKE_X_MIN + (rand() % ((SNAKE_X_MAX – SNAKE_X_MIN) / SNAKE_BLOCK_SIZE)) * SNAKE_BLOCK_SIZE;
snake_game.food_y = SNAKE_Y_MIN + (rand() % ((SNAKE_Y_MAX – SNAKE_Y_MIN) / SNAKE_BLOCK_SIZE)) * SNAKE_BLOCK_SIZE;
// 检查是否与蛇身重叠
for(uint8_t i = 0; i < snake_game.length; i++)
{
if(snake_game.food_x == snake_game.body[i].x && snake_game.food_y == snake_game.body[i].y)
{
flag = 1;
break;
}
}
} while(flag); // 重叠则重新生成
}
// 蛇移动逻辑
void Snake_Move(void)
{
// 保存蛇尾坐标(用于擦除)
uint16_t tail_x = snake_game.body[snake_game.length – 1].x;
uint16_t tail_y = snake_game.body[snake_game.length – 1].y;
// 蛇身跟随移动(从尾到头)
for(uint8_t i = snake_game.length – 1; i > 0; i—)
{
snake_game.body[i].x = snake_game.body[i – 1].x;
snake_game.body[i].y = snake_game.body[i – 1].y;
}
// 移动蛇头(根据方向)
switch(snake_game.dir)
{
case DIR_UP:
snake_game.body[0].y -= SNAKE_BLOCK_SIZE;
break;
case DIR_DOWN:
snake_game.body[0].y += SNAKE_BLOCK_SIZE;
break;
case DIR_LEFT:
snake_game.body[0].x -= SNAKE_BLOCK_SIZE;
break;
case DIR_RIGHT:
snake_game.body[0].x += SNAKE_BLOCK_SIZE;
break;
default:
break;
}
// 检查是否吃到食物
if(snake_game.body[0].x == snake_game.food_x && snake_game.body[0].y == snake_game.food_y)
{
// 蛇身加长(长度+1)
snake_game.length++;
if(snake_game.length > SNAKE_MAX_LENGTH) snake_game.length = SNAKE_MAX_LENGTH; // 限制最大长度
// 得分增加
snake_game.score += 10;
// 重新生成食物
Snake_Generate_Food();
// 绘制新食物
Snake_Draw_Block(snake_game.food_x, snake_game.food_y, COLOR_RED);
// 绘制新蛇头(无需擦除尾,因为长度增加)
Snake_Draw_Block(snake_game.body[0].x, snake_game.body[0].y, COLOR_GREEN);
}
else
{
// 未吃到食物,擦除蛇尾
Snake_Draw_Block(tail_x, tail_y, COLOR_BLACK);
// 绘制新蛇头
Snake_Draw_Block(snake_game.body[0].x, snake_game.body[0].y, COLOR_GREEN);
}
}
// 碰撞检测(撞墙/撞自身)
void Snake_Check_Collision(void)
{
// 撞墙检测
if(snake_game.body[0].x < SNAKE_X_MIN || snake_game.body[0].x > SNAKE_X_MAX ||
snake_game.body[0].y < SNAKE_Y_MIN || snake_game.body[0].y > SNAKE_Y_MAX)
{
snake_game.game_over = 1;
return;
}
// 撞自身检测
for(uint8_t i = 1; i < snake_game.length; i++)
{
if(snake_game.body[0].x == snake_game.body[i].x && snake_game.body[0].y == snake_game.body[i].y)
{
snake_game.game_over = 1;
return;
}
}
}
// 显示得分(简易版,固定位置显示)
void Snake_Show_Score(void)
{
// 清屏原有得分区域
TFT_Fill_Rect(10, 0, 100, 8, COLOR_BLACK);
// 此处简化,实际可实现字符显示函数,这里仅用颜色块表示得分(每10分画一个块)
uint8_t score_blocks = snake_game.score / 10;
for(uint8_t i = 0; i < score_blocks; i++)
{
TFT_Fill_Rect(10 + i * 8, 0, 17 + i * 8, 8, COLOR_YELLOW);
}
}
// 游戏结束处理
void Snake_Game_Over(void)
{
// 绘制游戏结束背景(半透明红,简易版用纯色)
TFT_Fill_Rect(50, 120, 190, 200, COLOR_RED);
// 绘制游戏结束文字区域(白色背景)
TFT_Fill_Rect(60, 130, 180, 190, COLOR_WHITE);
// 绘制结束提示(简易版,用矩形模拟文字,实际可加字库)
TFT_Fill_Rect(70, 140, 170, 150, COLOR_RED); // GAME OVER 背景
TFT_Fill_Rect(75, 142, 165, 148, COLOR_BLACK); // 文字区域
// 显示最终得分
TFT_Fill_Rect(70, 160, 170, 170, COLOR_YELLOW);
TFT_Fill_Rect(75, 162, 165, 168, COLOR_BLACK);
Snake_Show_Score();
// 等待按键确认
while(KEY_Scan(0) != 5); // 按确认键退出
}
// 游戏主循环
void Snake_Game_Loop(void)
{
uint8_t key_val;
Snake_Init(); // 初始化游戏
while(1)
{
if(snake_game.game_over)
{
Snake_Game_Over();
break;
}
// 读取按键
key_val = KEY_Scan(1); // 连续触发模式
switch(key_val)
{
case 1: // 上
if(snake_game.dir != DIR_DOWN) snake_game.dir = DIR_UP;
break;
case 2: // 下
if(snake_game.dir != DIR_UP) snake_game.dir = DIR_DOWN;
break;
case 3: // 左
if(snake_game.dir != DIR_RIGHT) snake_game.dir = DIR_LEFT;
break;
case 4: // 右
if(snake_game.dir != DIR_LEFT) snake_game.dir = DIR_RIGHT;
break;
case 5: // 确认(暂停/继续,简易版暂不实现)
break;
default:
break;
}
// 移动蛇
Snake_Move();
// 碰撞检测
Snake_Check_Collision();
// 显示得分
Snake_Show_Score();
// 控制游戏速度(数值越小速度越快)
HAL_Delay(200);
}
}
6.3 贪吃蛇代码说明
- 蛇的移动逻辑:通过数组保存蛇身坐标,移动时从尾到头依次复制前一个坐标,再更新蛇头坐标;
- 食物生成:使用随机数,且会检查是否与蛇身重叠,避免食物出现在蛇身上;
- 碰撞检测:分为撞墙(边界检测)和撞自身(坐标对比);
- 得分显示:简易版用颜色块表示,实际项目中可加入字库实现字符显示。
七、俄罗斯方块游戏实现
7.1 游戏逻辑流程图
#mermaid-svg-19IFFjGqSLINr4fS{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-19IFFjGqSLINr4fS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-19IFFjGqSLINr4fS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-19IFFjGqSLINr4fS .error-icon{fill:#552222;}#mermaid-svg-19IFFjGqSLINr4fS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-19IFFjGqSLINr4fS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-19IFFjGqSLINr4fS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-19IFFjGqSLINr4fS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-19IFFjGqSLINr4fS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-19IFFjGqSLINr4fS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-19IFFjGqSLINr4fS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-19IFFjGqSLINr4fS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-19IFFjGqSLINr4fS .marker.cross{stroke:#333333;}#mermaid-svg-19IFFjGqSLINr4fS svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-19IFFjGqSLINr4fS p{margin:0;}#mermaid-svg-19IFFjGqSLINr4fS .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-19IFFjGqSLINr4fS .cluster-label text{fill:#333;}#mermaid-svg-19IFFjGqSLINr4fS .cluster-label span{color:#333;}#mermaid-svg-19IFFjGqSLINr4fS .cluster-label span p{background-color:transparent;}#mermaid-svg-19IFFjGqSLINr4fS .label text,#mermaid-svg-19IFFjGqSLINr4fS span{fill:#333;color:#333;}#mermaid-svg-19IFFjGqSLINr4fS .node rect,#mermaid-svg-19IFFjGqSLINr4fS .node circle,#mermaid-svg-19IFFjGqSLINr4fS .node ellipse,#mermaid-svg-19IFFjGqSLINr4fS .node polygon,#mermaid-svg-19IFFjGqSLINr4fS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-19IFFjGqSLINr4fS .rough-node .label text,#mermaid-svg-19IFFjGqSLINr4fS .node .label text,#mermaid-svg-19IFFjGqSLINr4fS .image-shape .label,#mermaid-svg-19IFFjGqSLINr4fS .icon-shape .label{text-anchor:middle;}#mermaid-svg-19IFFjGqSLINr4fS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-19IFFjGqSLINr4fS .rough-node .label,#mermaid-svg-19IFFjGqSLINr4fS .node .label,#mermaid-svg-19IFFjGqSLINr4fS .image-shape .label,#mermaid-svg-19IFFjGqSLINr4fS .icon-shape .label{text-align:center;}#mermaid-svg-19IFFjGqSLINr4fS .node.clickable{cursor:pointer;}#mermaid-svg-19IFFjGqSLINr4fS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-19IFFjGqSLINr4fS .arrowheadPath{fill:#333333;}#mermaid-svg-19IFFjGqSLINr4fS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-19IFFjGqSLINr4fS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-19IFFjGqSLINr4fS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-19IFFjGqSLINr4fS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-19IFFjGqSLINr4fS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-19IFFjGqSLINr4fS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-19IFFjGqSLINr4fS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-19IFFjGqSLINr4fS .cluster text{fill:#333;}#mermaid-svg-19IFFjGqSLINr4fS .cluster span{color:#333;}#mermaid-svg-19IFFjGqSLINr4fS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-19IFFjGqSLINr4fS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-19IFFjGqSLINr4fS rect.text{fill:none;stroke-width:0;}#mermaid-svg-19IFFjGqSLINr4fS .icon-shape,#mermaid-svg-19IFFjGqSLINr4fS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-19IFFjGqSLINr4fS .icon-shape p,#mermaid-svg-19IFFjGqSLINr4fS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-19IFFjGqSLINr4fS .icon-shape rect,#mermaid-svg-19IFFjGqSLINr4fS .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-19IFFjGqSLINr4fS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-19IFFjGqSLINr4fS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-19IFFjGqSLINr4fS :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
左右
下
确认
无
否
是
有
无
是
否
是
否
游戏初始化
清屏并绘制游戏区域
生成随机方块形状和位置
绘制当前方块
读取按键输入
按键类型?
移动方块(检测边界/碰撞)
快速下落
旋转方块(检测边界/碰撞)
方块自动下落
是否落地/碰撞?
固定当前方块到背景
检测是否有满行
消除满行+得分+下落上方方块
检查是否游戏结束(方块顶到上边界)
游戏结束,显示得分
是否重新开始?
返回游戏选择界面
7.2 创建俄罗斯方块游戏文件
文件名:tetris.h
#ifndef __TETRIS_H
#define __TETRIS_H
#include "tft_ili9341.h"
#include "key.h"
#include "stdlib.h"
#include "time.h"
// 游戏参数
#define TETRIS_BLOCK_SIZE 10 // 方块大小(像素)
#define TETRIS_ROWS 20 // 游戏区域行数
#define TETRIS_COLS 10 // 游戏区域列数
#define TETRIS_X_OFFSET 20 // 游戏区域X偏移
#define TETRIS_Y_OFFSET 20 // 游戏区域Y偏移
// 方块形状定义(7种基础形状,每个形状4个旋转状态)
typedef enum
{
SHAPE_I,
SHAPE_O,
SHAPE_T,
SHAPE_L,
SHAPE_J,
SHAPE_S,
SHAPE_Z
} Tetris_Shape_Type;
// 方块颜色(对应7种形状)
#define SHAPE_COLORS {COLOR_CYAN, COLOR_YELLOW, COLOR_PURPLE, COLOR_ORANGE, COLOR_BLUE, COLOR_GREEN, COLOR_RED}
// 方块结构体
typedef struct
{
Tetris_Shape_Type type; // 形状类型
uint8_t rotation; // 旋转状态(0-3)
int8_t x; // 方块中心X坐标(列)
int8_t y; // 方块中心Y坐标(行)
uint16_t color; // 方块颜色
} Tetris_Block;
// 游戏数据结构体
typedef struct
{
uint16_t bg[TETRIS_ROWS][TETRIS_COLS]; // 游戏背景(保存固定的方块)
Tetris_Block current_block; // 当前下落的方块
Tetris_Block next_block; // 下一个方块
uint16_t score; // 得分
uint8_t game_over; // 游戏结束标志
uint16_t fall_speed; // 下落速度(ms)
} Tetris_Game;
// 函数声明
void Tetris_Init(void); // 游戏初始化
void Tetris_Init_Block(Tetris_Block *block); // 初始化方块
void Tetris_Draw_Block(Tetris_Block *block, uint8_t erase); // 绘制/擦除方块
uint8_t Tetris_Check_Collision(Tetris_Block *block, int8_t dx, int8_t dy, uint8_t new_rotation); // 碰撞检测
void Tetris_Rotate_Block(void); // 旋转方块
void Tetris_Move_Block(int8_t dx, int8_t dy); // 移动方块
void Tetris_Fall_Block(void); // 方块下落
void Tetris_Fix_Block(void); // 固定方块到背景
void Tetris_Check_Line(void); // 检测并消除满行
void Tetris_Show_Score(void); // 显示得分
void Tetris_Game_Over(void); // 游戏结束处理
void Tetris_Game_Loop(void); // 游戏主循环
#endif
文件名:tetris.c
#include "tetris.h"
// 7种方块的形状定义(每个形状4个旋转状态,1表示有块,0表示无)
const uint8_t shape_defs[7][4][4][4] = {
// I型
{
{{0,0,0,0},{1,1,1,1},{0,0,0,0},{0,0,0,0}},
{{0,0,1,0},{0,0,1,0},{0,0,1,0},{0,0,1,0}},
{{0,0,0,0},{1,1,1,1},{0,0,0,0},{0,0,0,0}},
{{0,0,1,0},{0,0,1,0},{0,0,1,0},{0,0,1,0}}
},
// O型(无旋转)
{
{{0,0,0,0},{0,1,1,0},{0,1,1,0},{0,0,0,0}},
{{0,0,0,0},{0,1,1,0},{0,1,1,0},{0,0,0,0}},
{{0,0,0,0},{0,1,1,0},{0,1,1,0},{0,0,0,0}},
{{0,0,0,0},{0,1,1,0},{0,1,1,0},{0,0,0,0}}
},
// T型
{
{{0,0,0,0},{1,1,1,0},{0,1,0,0},{0,0,0,0}},
{{0,1,0,0},{1,1,0,0},{0,1,0,0},{0,0,0,0}},
{{0,1,0,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}},
{{0,1,0,0},{0,1,1,0},{0,1,0,0},{0,0,0,0}}
},
// L型
{
{{0,0,0,0},{1,1,1,0},{1,0,0,0},{0,0,0,0}},
{{1,1,0,0},{0,1,0,0},{0,1,0,0},{0,0,0,0}},
{{0,0,1,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}},
{{0,1,0,0},{0,1,0,0},{0,1,1,0},{0,0,0,0}}
},
// J型
{
{{0,0,0,0},{1,1,1,0},{0,0,1,0},{0,0,0,0}},
{{0,1,0,0},{0,1,0,0},{1,1,0,0},{0,0,0,0}},
{{1,0,0,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}},
{{0,1,1,0},{0,1,0,0},{0,1,0,0},{0,0,0,0}}
},
// S型
{
{{0,0,0,0},{0,1,1,0},{1,1,0,0},{0,0,0,0}},
{{1,0,0,0},{1,1,0,0},{0,1,0,0},{0,0,0,0}},
{{0,0,0,0},{0,1,1,0},{1,1,0,0},{0,0,0,0}},
{{1,0,0,0},{1,1,0,0},{0,1,0,0},{0,0,0,0}}
},
// Z型
{
{{0,0,0,0},{1,1,0,0},{0,1,1,0},{0,0,0,0}},
{{0,1,0,0},{1,1,0,0},{1,0,0,0},{0,0,0,0}},
{{0,0,0,0},{1,1,0,0},{0,1,1,0},{0,0,0,0}},
{{0,1,0,0},{1,1,0,0},{1,0,0,0},{0,0,0,0}}
}
};
// 方块颜色数组
const uint16_t shape_colors[] = SHAPE_COLORS;
Tetris_Game tetris_game; // 游戏全局变量
// 初始化游戏
void Tetris_Init(void)
{
// 重置游戏状态
tetris_game.score = 0;
tetris_game.game_over = 0;
tetris_game.fall_speed = 800; // 初始下落速度(800ms)
// 清空背景数组
for(uint8_t i = 0; i < TETRIS_ROWS; i++)
{
for(uint8_t j = 0; j < TETRIS_COLS; j++)
{
tetris_game.bg[i][j] = COLOR_BLACK;
}
}
// 清屏(黑色背景)
TFT_Clear_Screen(COLOR_BLACK);
// 绘制游戏区域边界(白色)
TFT_Draw_Rect(TETRIS_X_OFFSET – 1, TETRIS_Y_OFFSET – 1,
TETRIS_X_OFFSET + TETRIS_COLS * TETRIS_BLOCK_SIZE,
TETRIS_Y_OFFSET + TETRIS_ROWS * TETRIS_BLOCK_SIZE, COLOR_WHITE);
// 初始化随机数种子
srand(HAL_GetTick());
// 生成当前方块和下一个方块
Tetris_Init_Block(&tetris_game.current_block);
Tetris_Init_Block(&tetris_game.next_block);
// 绘制初始方块
Tetris_Draw_Block(&tetris_game.current_block, 0);
// 显示初始得分
Tetris_Show_Score();
}
// 初始化方块(随机形状、初始位置)
void Tetris_Init_Block(Tetris_Block *block)
{
// 随机选择形状
block->type = (Tetris_Shape_Type)(rand() % 7);
block->rotation = 0;
block->color = shape_colors[block->type];
// 初始位置(游戏区域居中,顶部)
block->x = TETRIS_COLS / 2 – 2;
block->y = 0;
// 检查游戏结束(初始方块就碰撞)
if(Tetris_Check_Collision(block, 0, 0, block->rotation))
{
tetris_game.game_over = 1;
}
}
// 绘制/擦除方块(erase=1擦除,0绘制)
void Tetris_Draw_Block(Tetris_Block *block, uint8_t erase)
{
uint16_t color = erase ? COLOR_BLACK : block->color;
const uint8_t (*shape)[4][4] = shape_defs[block->type][block->rotation];
// 遍历方块的4×4矩阵,绘制有效块
for(int8_t i = 0; i < 4; i++)
{
for(int8_t j = 0; j < 4; j++)
{
if(shape[i][j])
{
uint16_t x = TETRIS_X_OFFSET + (block->x + j) * TETRIS_BLOCK_SIZE;
uint16_t y = TETRIS_Y_OFFSET + (block->y + i) * TETRIS_BLOCK_SIZE;
// 仅绘制游戏区域内的块
if((block->x + j) >= 0 && (block->x + j) < TETRIS_COLS &&
(block->y + i) >= 0 && (block->y + i) < TETRIS_ROWS)
{
TFT_Fill_Rect(x, y, x + TETRIS_BLOCK_SIZE – 1, y + TETRIS_BLOCK_SIZE – 1, color);
}
}
}
}
}
// 碰撞检测(dx/dy:移动偏移;new_rotation:新旋转状态)
uint8_t Tetris_Check_Collision(Tetris_Block *block, int8_t dx, int8_t dy, uint8_t new_rotation)
{
const uint8_t (*shape)[4][4] = shape_defs[block->type][new_rotation];
// 遍历方块矩阵,检查每个有效块的碰撞
for(int8_t i = 0; i < 4; i++)
{
for(int8_t j = 0; j < 4; j++)
{
if(shape[i][j])
{
int8_t x = block->x + j + dx;
int8_t y = block->y + i + dy;
// 边界检测
if(x < 0 || x >= TETRIS_COLS || y >= TETRIS_ROWS)
{
return 1; // 碰撞
}
// 背景碰撞检测(y<0时不检测)
if(y >= 0 && tetris_game.bg[y][x] != COLOR_BLACK)
{
return 1; // 碰撞
}
}
}
}
return 0; // 无碰撞
}
// 旋转方块
void Tetris_Rotate_Block(void)
{
uint8_t new_rotation = (tetris_game.current_block.rotation + 1) % 4;
// 检测旋转后的碰撞
if(!Tetris_Check_Collision(&tetris_game.current_block, 0, 0, new_rotation))
{
// 擦除原方块
Tetris_Draw_Block(&tetris_game.current_block, 1);
// 更新旋转状态
tetris_game.current_block.rotation = new_rotation;
// 绘制新方块
Tetris_Draw_Block(&tetris_game.current_block, 0);
}
}
// 移动方块(dx:左右偏移,dy:上下偏移)
void Tetris_Move_Block(int8_t dx, int8_t dy)
{
// 检测移动后的碰撞
if(!Tetris_Check_Collision(&tetris_game.current_block, dx, dy, tetris_game.current_block.rotation))
{
// 擦除原方块
Tetris_Draw_Block(&tetris_game.current_block, 1);
// 更新位置
tetris_game.current_block.x += dx;
tetris_game.current_block.y += dy;
// 绘制新方块
Tetris_Draw_Block(&tetris_game.current_block, 0);
}
}
// 方块下落
void Tetris_Fall_Block(void)
{
// 尝试向下移动
if(!Tetris_Check_Collision(&tetris_game.current_block, 0, 1, tetris_game.current_block.rotation))
{
Tetris_Move_Block(0, 1);
}
else
{
// 落地,固定方块到背景
Tetris_Fix_Block();
// 检测满行
Tetris_Check_Line();
// 生成新方块
Tetris_Draw_Block(&tetris_game.next_block, 1); // 擦除预览的下一个方块
tetris_game.current_block = tetris_game.next_block;
Tetris_Init_Block(&tetris_game.next_block);
// 绘制新方块
Tetris_Draw_Block(&tetris_game.current_block, 0);
// 显示得分
Tetris_Show_Score();
}
}
// 固定方块到背景
void Tetris_Fix_Block(void)
{
const uint8_t (*shape)[4][4] = shape_defs[tetris_game.current_block.type][tetris_game.current_block.rotation];
// 将方块的每个有效块写入背景数组
for(int8_t i = 0; i < 4; i++)
{
for(int8_t j = 0; j < 4; j++)
{
if(shape[i][j])
{
int8_t x = tetris_game.current_block.x + j;
int8_t y = tetris_game.current_block.y + i;
if(x >= 0 && x < TETRIS_COLS && y >= 0 && y < TETRIS_ROWS)
{
tetris_game.bg[y][x] = tetris_game.current_block.color;
}
}
}
}
}
// 检测并消除满行
void Tetris_Check_Line(void)
{
uint8_t line_count = 0; // 消除的行数
// 遍历每一行
for(uint8_t i = 0; i < TETRIS_ROWS; i++)
{
uint8_t full = 1;
// 检查该行是否满
for(uint8_t j = 0; j < TETRIS_COLS; j++)
{
if(tetris_game.bg[i][j] == COLOR_BLACK)
{
full = 0;
break;
}
}
// 满行则消除
if(full)
{
line_count++;
// 擦除该行
for(uint8_t j = 0; j < TETRIS_COLS; j++)
{
uint16_t x = TETRIS_X_OFFSET + j * TETRIS_BLOCK_SIZE;
uint16_t y = TETRIS_Y_OFFSET + i * TETRIS_BLOCK_SIZE;
TFT_Fill_Rect(x, y, x + TETRIS_BLOCK_SIZE – 1, y + TETRIS_BLOCK_SIZE – 1, COLOR_BLACK);
tetris_game.bg[i][j] = COLOR_BLACK;
}
// 上方方块下落
for(uint8_t k = i; k > 0; k—)
{
for(uint8_t j = 0; j < TETRIS_COLS; j++)
{
// 复制上一行数据到当前行
tetris_game.bg[k][j] = tetris_game.bg[k–1][j];
// 绘制下落的块
uint16_t x = TETRIS_X_OFFSET + j * TETRIS_BLOCK_SIZE;
uint16_t y = TETRIS_Y_OFFSET + k * TETRIS_BLOCK_SIZE;
TFT_Fill_Rect(x, y, x + TETRIS_BLOCK_SIZE – 1, y + TETRIS_BLOCK_SIZE – 1, tetris_game.bg[k][j]);
// 擦除上一行的块
y = TETRIS_Y_OFFSET + (k–1) * TETRIS_BLOCK_SIZE;
TFT_Fill_Rect(x, y, x + TETRIS_BLOCK_SIZE – 1, y + TETRIS_BLOCK_SIZE – 1, COLOR_BLACK);
}
}
}
}
// 根据消除的行数加分
switch(line_count)
{
case 1: tetris_game.score += 100; break;
case 2: tetris_game.score += 300; break;
case 3: tetris_game.score += 500; break;
case 4: tetris_game.score += 800; break;
default: break;
}
// 加快下落速度(每消除1000分提速)
if(tetris_game.score % 1000 == 0 && tetris_game.fall_speed > 200)
{
tetris_game.fall_speed -= 100;
}
}
// 显示得分(简易版)
void Tetris_Show_Score(void)
{
// 清屏得分区域
TFT_Fill_Rect(TETRIS_X_OFFSET + TETRIS_COLS * TETRIS_BLOCK_SIZE + 10, TETRIS_Y_OFFSET,
TETRIS_X_OFFSET + TETRIS_COLS * TETRIS_BLOCK_SIZE + 100, TETRIS_Y_OFFSET + 30, COLOR_BLACK);
// 绘制得分背景
TFT_Draw_Rect(TETRIS_X_OFFSET + TETRIS_COLS * TETRIS_BLOCK_SIZE + 10, TETRIS_Y_OFFSET,
TETRIS_X_OFFSET + TETRIS_COLS * TETRIS_BLOCK_SIZE + 100, TETRIS_Y_OFFSET + 30, COLOR_WHITE);
// 简易得分显示(每100分画一个黄色块)
uint8_t score_blocks = tetris_game.score / 100;
for(uint8_t i = 0; i < score_blocks && i < 9; i++)
{
TFT_Fill_Rect(TETRIS_X_OFFSET + TETRIS_COLS * TETRIS_BLOCK_SIZE + 15 + i * 9, TETRIS_Y_OFFSET + 5,
TETRIS_X_OFFSET + TETRIS_COLS * TETRIS_BLOCK_SIZE + 23 + i * 9, TETRIS_Y_OFFSET + 25, COLOR_YELLOW);
}
}
// 游戏结束处理
void Tetris_Game_Over(void)
{
// 绘制结束背景
TFT_Fill_Rect(TETRIS_X_OFFSET + 10, TETRIS_Y_OFFSET + 50,
TETRIS_X_OFFSET + TETRIS_COLS * TETRIS_BLOCK_SIZE – 10,
TETRIS_Y_OFFSET + TETRIS_ROWS * TETRIS_BLOCK_SIZE – 50, COLOR_RED);
// 绘制结束提示区域
TFT_Fill_Rect(TETRIS_X_OFFSET + 20, TETRIS_Y_OFFSET + 60,
TETRIS_X_OFFSET + TETRIS_COLS * TETRIS_BLOCK_SIZE – 20,
TETRIS_Y_OFFSET + TETRIS_ROWS * TETRIS_BLOCK_SIZE – 60, COLOR_WHITE);
// 简易结束文字(矩形模拟)
TFT_Fill_Rect(TETRIS_X_OFFSET + 30, TETRIS_Y_OFFSET + 70,
TETRIS_X_OFFSET + TETRIS_COLS * TETRIS_BLOCK_SIZE – 30,
TETRIS_Y_OFFSET + 90, COLOR_RED);
TFT_Fill_Rect(TETRIS_X_OFFSET + 35, TETRIS_Y_OFFSET + 75,
TETRIS_X_OFFSET + TETRIS_COLS * TETRIS_BLOCK_SIZE – 35,
TETRIS_Y_OFFSET + 85, COLOR_BLACK);
// 显示最终得分
TFT_Fill_Rect(TETRIS_X_OFFSET + 30, TETRIS_Y_OFFSET + 100,
TETRIS_X_OFFSET + TETRIS_COLS * TETRIS_BLOCK_SIZE – 30,
TETRIS_Y_OFFSET + 120, COLOR_YELLOW);
TFT_Fill_Rect(TETRIS_X_OFFSET + 35, TETRIS_Y_OFFSET + 105,
TETRIS_X_OFFSET + TETRIS_COLS * TETRIS_BLOCK_SIZE – 35,
TETRIS_Y_OFFSET + 115, COLOR_BLACK);
Tetris_Show_Score();
// 等待确认键退出
while(KEY_Scan(0) != 5);
}
// 游戏主循环
void Tetris_Game_Loop(void)
{
uint8_t key_val;
uint32_t last_fall_time = 0;
Tetris_Init(); // 初始化游戏
while(1)
{
if(tetris_game.game_over)
{
Tetris_Game_Over();
break;
}
// 读取按键
key_val = KEY_Scan(1);
switch(key_val)
{
case 1: // 上(旋转)
Tetris_Rotate_Block();
break;
case 2: // 下(快速下落)
Tetris_Fall_Block();
break;
case 3: // 左
Tetris_Move_Block(–1, 0);
break;
case 4: // 右
Tetris_Move_Block(1, 0);
break;
case 5: // 确认(暂停,简易版暂不实现)
break;
default:
break;
}
// 自动下落(基于时间控制)
if(HAL_GetTick() – last_fall_time > tetris_game.fall_speed)
{
Tetris_Fall_Block();
last_fall_time = HAL_GetTick();
}
// 短延时,降低CPU占用
HAL_Delay(10);
}
}
7.3 俄罗斯方块代码说明
- 方块形状:用三维数组定义7种基础形状的4个旋转状态,每个状态是4×4的矩阵;
- 碰撞检测:检测方块移动/旋转后是否超出边界,或与背景中已固定的方块重叠;
- 消行逻辑:遍历每一行,检测满行后消除,并让上方方块依次下落;
- 速度控制:随着得分增加,下落速度加快,提升游戏难度。
八、主函数整合
8.1 修改main.c文件
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2024 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ——————————————————————*/
#include "main.h"
#include "gpio.h"
#include "spi.h"
/* Private includes ———————————————————-*/
/* USER CODE BEGIN Includes */
#include "tft_ili9341.h"
#include "key.h"
#include "snake.h"
#include "tetris.h"
/* USER CODE END Includes */
/* Private typedef ———————————————————–*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ————————————————————*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro ————————————————————-*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ———————————————————*/
/* USER CODE BEGIN PV */
/* USER CODE END PV */
/* Private function prototypes ———————————————–*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
void Game_Menu(void); // 游戏选择菜单
/* USER CODE END PFP */
/* Private user code ———————————————————*/
/* USER CODE BEGIN 0 */
void Game_Menu(void)
{
uint8_t key_val;
// 清屏
TFT_Clear_Screen(COLOR_BLACK);
// 绘制菜单标题
TFT_Fill_Rect(50, 50, 190, 80, COLOR_WHITE);
TFT_Fill_Rect(55, 55, 185, 75, COLOR_BLUE);
// 绘制游戏选项1:贪吃蛇
TFT_Fill_Rect(30, 100, 210, 130, COLOR_GREEN);
TFT_Fill_Rect(35, 105, 205, 125, COLOR_BLACK);
// 绘制游戏选项2:俄罗斯方块
TFT_Fill_Rect(30, 150, 210, 180, COLOR_RED);
TFT_Fill_Rect(35, 155, 205, 175, COLOR_BLACK);
// 菜单循环
while(1)
{
key_val = KEY_Scan(0);
switch(key_val)
{
case 1: // 上,选择贪吃蛇
case 5: // 确认,启动贪吃蛇
Snake_Game_Loop();
Game_Menu(); // 返回菜单
break;
case 2: // 下,选择俄罗斯方块
Tetris_Game_Loop();
Game_Menu(); // 返回菜单
break;
default:
break;
}
}
}
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration——————————————————–*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_SPI1_Init();
/* USER CODE BEGIN 2 */
// 初始化TFT
TFT_Init();
// 初始化按键
KEY_Init();
// 启动游戏菜单
Game_Menu();
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Configure the main internal regulator output voltage
*/
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 8;
RCC_OscInitStruct.PLL.PLLN = 336;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 7;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
{
Error_Handler();
}
}
/* USER CODE BEGIN 4 */
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
__disable_irq();
while (1)
{
}
}
#ifdef USE_FULL_ASSERT
void assert_failed(uint8_t *file, uint32_t line)
{
}
#endif
九、工程编译与下载
9.1 添加文件到工程
- tft_ili9341.c
- key.c
- snake.c
- tetris.c
9.2 编译工程
9.3 下载到 STM32F407
十、运行与调试说明
10.1 开机效果
- 绿色框:贪吃蛇
- 红色框:俄罗斯方块
10.2 按键功能
- 上键:选择贪吃蛇 / 蛇向上移动
- 下键:选择俄罗斯方块 / 蛇向下移动
- 左键:左移 / 蛇左移
- 右键:右移 / 蛇右移
- 确认键(中键):进入游戏 / 俄罗斯方块旋转
10.3 贪吃蛇规则
10.4 俄罗斯方块规则
十一、常见问题与解决
11.1 TFT 不亮
- 检查 BL 引脚是否拉高
- 检查 3.3V 供电
- 检查复位引脚电平
11.2 TFT 花屏/白屏
- SPI 接线错误(SCL、SDA)
- 初始化指令不对
- 片选 CS、DC 引脚配置错误
11.3 按键无反应
- 上下拉配置错误
- 引脚定义错误
- 按键消抖时间不合理
11.4 游戏不移动/卡顿
- 延时函数错误
- 按键扫描模式错误
- 速度设置过快/过慢
十二、项目扩展方向(进阶)
十三、总结
本项目完整实现了:
- STM32F407 初始化
- SPI 驱动 TFT LCD(ILI9341)
- 按键输入与消抖
- 贪吃蛇完整逻辑
- 俄罗斯方块完整逻辑
- 游戏菜单与切换


