一、项目概述
1.1 项目背景与意义
1.2 核心技术栈
1.3 最终效果展示
二、硬件准备与接线
2.1 完整硬件清单
2.2 详细引脚连接表
2.3 硬件连接注意事项
三、整体实现思路
3.1 模块化架构设计
3.2 屏幕分辨率适配方案
3.3 游戏核心流程
四、底层驱动开发
4.1 SPI 接口配置
4.2 OLED 屏初始化(128×96 专属配置)
4.3 基础绘图函数实现
五、游戏逻辑开发
5.1 游戏数据结构定义
5.2 7 种方块形状与旋转实现
5.3 核心碰撞检测算法
5.4 方块固定与消行机制
5.5 按键控制与游戏速度调节
六、完整代码实现
6.1 工程文件结构
6.2 OLED 驱动代码(oled.h/oled.c)
6.3 游戏逻辑代码(tetris.h/tetris.c)
6.4 主程序代码(main.c)
七、调试与常见问题排查
7.1 屏幕显示问题排查
7.2 按键响应问题排查
7.3 游戏逻辑问题排查
八、性能优化与功能扩展
8.1 画面渲染效率优化
8.2 分数显示功能实现
8.3 下一个方块预览功能
8.4 游戏结束与重新开始
九、总结与展望

本文将手把手教你用 STM32G474 单片机,驱动一块 1.32 寸、128×96 分辨率的 SPI 接口 OLED 屏,完成一个经典的俄罗斯方块游戏。全文采用保证通俗易懂,跟着做就能跑通!
一、硬件准备:清单与接线
先把硬件搭起来,这里用表格整理最清晰。
1.1 硬件清单
| 组件名称 | 规格说明 | 数量 | 用途 |
|---|---|---|---|
| 开发板 | STM32G474RET6(核心板) | 1 | 主控芯片 |
| OLED 屏 | 1.32 寸,128×96,SPI 接口 | 1 | 游戏画面显示 |
| 轻触按键 | 6×6mm | 3 | 控制:左移、右移、旋转 |
| 杜邦线 | 公对母、公对公 | 若干 | 电路连接 |
| 面包板(可选) | 830 孔 | 1 | 方便按键接线 |
1.2 引脚连接表
我们用 SPI1 驱动 OLED,按键用普通 GPIO 输入,接线如下:
| OLED 屏引脚 | STM32G474 引脚 | 功能说明 |
|---|---|---|
| VCC | 3.3V | 电源(千万别接 5V!) |
| GND | GND | 地线 |
| SCK | PA5 | SPI 时钟线 |
| MOSI | PA7 | SPI 数据线(主机发从机) |
| CS | PA4 | 片选(低电平有效) |
| DC | PA6 | 数据 / 命令选择(高 = 数据) |
| RST | PB0 | 复位(低电平复位) |
| 按键功能 | STM32G474 引脚 | GPIO 配置模式 |
|---|---|---|
| 左移 | PC0 | 下拉输入(GPIO_PULLDOWN) |
| 右移 | PC1 | 下拉输入 |
| 旋转 | PC2 | 下拉输入 |
二、实现思路:模块化拆解
我们把项目拆成 "底层驱动→游戏逻辑→画面渲染" 三个模块,逐个攻破。
2.1 模块划分与职责
| 模块名称 | 核心职责 |
|---|---|
| OLED 驱动层 | 初始化 SPI、发送命令 / 数据、清屏、绘制 8×8 像素的 "方块格子" |
| 游戏逻辑层 | 定义 7 种方块、处理移动 / 旋转、碰撞检测、消行、生成新方块 |
| 主循环层 | 扫描按键、控制方块下落速度、调用渲染函数更新画面 |
2.2 关键参数定义
为了适配 128×96 的屏幕,我们把游戏里的 "一个格子" 对应 8×8 像素,这样:
- 游戏区域宽度:
128 ÷ 8 = 16列 - 游戏区域高度:
96 ÷ 8 = 12行
三、代码实现:分文件带注释
我们用 STM32 HAL 库 开发,代码分为 oled.h、oled.c、tetris.h、tetris.c 和 main.c。
3.1 OLED 驱动代码 (oled.h + oled.c)
这部分负责和硬件打交道,驱动屏幕亮灭。
oled.h(头文件)
c
运行
#ifndef __OLED_H
#define __OLED_H
#include "stm32g4xx_hal.h"
#include <stdint.h>
// 引脚定义(对应接线表)
#define OLED_CS_Pin GPIO_PIN_4
#define OLED_CS_GPIO_Port GPIOA
#define OLED_DC_Pin GPIO_PIN_6
#define OLED_DC_GPIO_Port GPIOA
#define OLED_RST_Pin GPIO_PIN_0
#define OLED_RST_GPIO_Port GPIOB
// 函数声明
void OLED_Init(void); // 初始化屏幕
void OLED_Clear(void); // 清屏
void OLED_DrawBlock(uint8_t x, uint8_t y, uint8_t color); // 画一个8×8的格子
#endif
oled.c(驱动实现)
c
运行
#include "oled.h"
// SPI句柄(在main.c中初始化,这里声明为extern)
extern SPI_HandleTypeDef hspi1;
// 发送命令给OLED
static void OLED_SendCmd(uint8_t cmd) {
HAL_GPIO_WritePin(OLED_DC_GPIO_Port, OLED_DC_Pin, GPIO_PIN_RESET); // DC拉低=命令
HAL_GPIO_WritePin(OLED_CS_GPIO_Port, OLED_CS_Pin, GPIO_PIN_RESET); // CS拉低=选中
HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); // 发送1字节
HAL_GPIO_WritePin(OLED_CS_GPIO_Port, OLED_CS_Pin, GPIO_PIN_SET); // CS拉高=释放
}
// 发送数据给OLED
static void OLED_SendData(uint8_t data) {
HAL_GPIO_WritePin(OLED_DC_GPIO_Port, OLED_DC_Pin, GPIO_PIN_SET); // DC拉高=数据
HAL_GPIO_WritePin(OLED_CS_GPIO_Port, OLED_CS_Pin, GPIO_PIN_RESET);
HAL_SPI_Transmit(&hspi1, &data, 1, HAL_MAX_DELAY);
HAL_GPIO_WritePin(OLED_CS_GPIO_Port, OLED_CS_Pin, GPIO_PIN_SET);
}
// 初始化OLED(针对128×96分辨率的关键配置!)
void OLED_Init(void) {
// 1. 硬件复位
HAL_GPIO_WritePin(OLED_RST_GPIO_Port, OLED_RST_Pin, GPIO_PIN_SET);
HAL_Delay(10);
HAL_GPIO_WritePin(OLED_RST_GPIO_Port, OLED_RST_Pin, GPIO_PIN_RESET);
HAL_Delay(10);
HAL_GPIO_WritePin(OLED_RST_GPIO_Port, OLED_RST_Pin, GPIO_PIN_SET);
HAL_Delay(10);
// 2. 发送初始化命令序列(SSD1306芯片,适配128×96)
OLED_SendCmd(0xAE); // 关闭显示(先关屏,配置完再开)
OLED_SendCmd(0xD5); // 设置显示时钟分频
OLED_SendCmd(0x80); // 建议值
OLED_SendCmd(0xA8); // 设置多路复用率(关键!对应96行)
OLED_SendCmd(0x5F); // 96-1=95=0x5F
OLED_SendCmd(0xD3); // 设置显示偏移
OLED_SendCmd(0x00); // 无偏移
OLED_SendCmd(0x40); // 设置显示起始行
OLED_SendCmd(0x8D); // 电荷泵设置(必须开,否则屏幕不亮)
OLED_SendCmd(0x14); // 开启电荷泵
OLED_SendCmd(0x20); // 设置内存地址模式
OLED_SendCmd(0x00); // 水平地址模式
OLED_SendCmd(0xA1); // 段重映射(左右翻转,根据实际屏幕调整)
OLED_SendCmd(0xC8); // COM扫描方向(上下翻转,根据实际屏幕调整)
OLED_SendCmd(0xDA); // COM引脚配置(关键!适配128×96)
OLED_SendCmd(0x12); // 适合128×96的配置
OLED_SendCmd(0x81); // 对比度
OLED_SendCmd(0xCF); // 建议值
OLED_SendCmd(0xD9); // 预充电周期
OLED_SendCmd(0xF1); // 建议值
OLED_SendCmd(0xDB); // VCOMH电压
OLED_SendCmd(0x30); // 建议值
OLED_SendCmd(0xA4); // 全局显示开启
OLED_SendCmd(0xA6); // 正常显示(非反色)
OLED_SendCmd(0xAF); // 打开显示!
OLED_Clear(); // 初始化完清屏
}
// 清屏函数
void OLED_Clear(void) {
for (uint8_t page = 0; page < 12; page++) { // 128×96对应12个页(每页8行)
OLED_SendCmd(0xB0 + page); // 设置页地址
OLED_SendCmd(0x00); // 设置列地址低4位
OLED_SendCmd(0x10); // 设置列地址高4位
for (uint8_t col = 0; col < 128; col++) {
OLED_SendData(0x00); // 写入0(熄灭)
}
}
}
// 画一个8×8的"游戏格子"
// x: 0-15(列),y: 0-11(行),color: 1=亮,0=灭
void OLED_DrawBlock(uint8_t x, uint8_t y, uint8_t color) {
uint8_t page = y; // 1页=8行,y直接对应页
uint8_t startCol = x * 8; // 1格=8列,计算起始列
// 设置要写的位置
OLED_SendCmd(0xB0 + page);
OLED_SendCmd(0x00 + (startCol & 0x0F));
OLED_SendCmd(0x10 + ((startCol >> 4) & 0x0F));
// 连续写8个字节(8行)
for (uint8_t i = 0; i < 8; i++) {
OLED_SendData(color ? 0xFF : 0x00); // 0xFF=全亮,0x00=全灭
}
}
3.2 游戏逻辑代码 (tetris.h + tetris.c)
这部分是游戏的 "大脑",处理方块的各种行为。
tetris.h(头文件)
c
运行
#ifndef __TETRIS_H
#define __TETRIS_H
#include <stdint.h>
// 游戏区域尺寸
#define GAME_WIDTH 16 // 128/8
#define GAME_HEIGHT 12 // 96/8
// 方块结构体
typedef struct {
uint8_t type; // 方块类型(0-6,对应7种形状)
uint8_t rotation; // 旋转状态(0-3,对应4个方向)
int8_t x; // X坐标(列)
int8_t y; // Y坐标(行)
} Tetromino;
// 全局变量声明
extern Tetromino g_currentTet; // 当前正在下落的方块
extern uint8_t g_gameBoard[GAME_HEIGHT][GAME_WIDTH]; // 游戏面板(1=有方块)
// 函数声明
void Tetris_Init(void); // 初始化游戏
void Tetris_Spawn(void); // 生成新方块
uint8_t Tetris_CheckCollision(void); // 碰撞检测
void Tetris_FixBlock(void); // 固定方块到面板
void Tetris_ClearLines(void); // 消行处理
void Tetris_MoveLeft(void); // 左移
void Tetris_MoveRight(void); // 右移
void Tetris_Rotate(void); // 旋转
#endif
tetris.c(逻辑实现)
c
运行
#include "tetris.h"
#include <stdlib.h>
#include <string.h>
// 全局变量定义
Tetromino g_currentTet;
uint8_t g_gameBoard[GAME_HEIGHT][GAME_WIDTH];
// 7种方块的定义!每种方块4种旋转状态,用4x4位图表示
// 每一行是一个字节,bit为1表示有方块
const uint8_t g_tetrominoes[7][4][4] = {
// 0: I型
{
{0x00, 0xF0, 0x00, 0x00}, // 旋转0
{0x20, 0x20, 0x20, 0x20}, // 旋转1
{0x00, 0x00, 0xF0, 0x00}, // 旋转2
{0x40, 0x40, 0x40, 0x40} // 旋转3
},
// 1: O型(正方形,旋转不变)
{
{0x60, 0x60, 0x00, 0x00},
{0x60, 0x60, 0x00, 0x00},
{0x60, 0x60, 0x00, 0x00},
{0x60, 0x60, 0x00, 0x00}
},
// 2: T型
{
{0x00, 0xE0, 0x40, 0x00},
{0x40, 0xC0, 0x40, 0x00},
{0x40, 0xE0, 0x00, 0x00},
{0x40, 0x60, 0x40, 0x00}
},
// 3: S型
{
{0x00, 0x60, 0xC0, 0x00},
{0x40, 0xC0, 0x80, 0x00},
{0x00, 0x60, 0xC0, 0x00},
{0x40, 0xC0, 0x80, 0x00}
},
// 4: Z型
{
{0x00, 0xC0, 0x60, 0x00},
{0x80, 0xC0, 0x40, 0x00},
{0x00, 0xC0, 0x60, 0x00},
{0x80, 0xC0, 0x40, 0x00}
},
// 5: J型
{
{0x40, 0xE0, 0x00, 0x00},
{0x40, 0x40, 0xC0, 0x00},
{0x00, 0xE0, 0x20, 0x00},
{0x60, 0x40, 0x40, 0x00}
},
// 6: L型
{
{0x20, 0xE0, 0x00, 0x00},
{0xC0, 0x40, 0x40, 0x00},
{0x00, 0xE0, 0x40, 0x00},
{0x40, 0x40, 0x60, 0x00}
}
};
// 初始化游戏
void Tetris_Init(void) {
memset(g_gameBoard, 0, sizeof(g_gameBoard)); // 清空面板
Tetris_Spawn(); // 生成第一个方块
}
// 生成新方块
void Tetris_Spawn(void) {
g_currentTet.type = rand() % 7; // 随机类型
g_currentTet.rotation = 0; // 初始旋转0
g_currentTet.x = GAME_WIDTH / 2 - 2; // 初始X:中间偏左
g_currentTet.y = -2; // 初始Y:屏幕上方(慢慢下来)
}
// 碰撞检测(核心函数!)
// 返回1表示碰撞,0表示安全
uint8_t Tetris_CheckCollision(void) {
for (uint8_t row = 0; row < 4; row++) {
for (uint8_t col = 0; col < 4; col++) {
// 检查当前方块的这个位置是否有像素
if (g_tetrominoes[g_currentTet.type][g_currentTet.rotation][row] & (0x80 >> col)) {
int8_t boardX = g_currentTet.x + col;
int8_t boardY = g_currentTet.y + row;
// 1. 检查是否撞左右墙或撞底
if (boardX < 0 || boardX >= GAME_WIDTH || boardY >= GAME_HEIGHT) {
return 1;
}
// 2. 检查是否和已固定的方块重叠(且在屏幕内)
if (boardY >= 0 && g_gameBoard[boardY][boardX]) {
return 1;
}
}
}
}
return 0;
}
// 把当前方块固定到游戏面板上
void Tetris_FixBlock(void) {
for (uint8_t row = 0; row < 4; row++) {
for (uint8_t col = 0; col < 4; col++) {
if (g_tetrominoes[g_currentTet.type][g_currentTet.rotation][row] & (0x80 >> col)) {
int8_t boardX = g_currentTet.x + col;
int8_t boardY = g_currentTet.y + row;
if (boardY >= 0) {
g_gameBoard[boardY][boardX] = 1; // 标记为有方块
}
}
}
}
}
// 消行处理
void Tetris_ClearLines(void) {
for (int8_t row = GAME_HEIGHT - 1; row >= 0; row--) {
// 检查这一行是否满了
uint8_t isFull = 1;
for (uint8_t col = 0; col < GAME_WIDTH; col++) {
if (!g_gameBoard[row][col]) {
isFull = 0;
break;
}
}
if (isFull) {
// 满了!把上面的行整体下移
for (int8_t r = row; r > 0; r--) {
memcpy(g_gameBoard[r], g_gameBoard[r-1], GAME_WIDTH);
}
// 清空最上面一行
memset(g_gameBoard[0], 0, GAME_WIDTH);
row++; // 重新检查当前行(因为上面的移下来了)
}
}
}
// 左移
void Tetris_MoveLeft(void) {
g_currentTet.x--;
if (Tetris_CheckCollision()) {
g_currentTet.x++; // 撞了,移回去
}
}
// 右移
void Tetris_MoveRight(void) {
g_currentTet.x++;
if (Tetris_CheckCollision()) {
g_currentTet.x--;
}
}
// 旋转
void Tetris_Rotate(void) {
uint8_t prevRot = g_currentTet.rotation;
g_currentTet.rotation = (g_currentTet.rotation + 1) % 4;
if (Tetris_CheckCollision()) {
g_currentTet.rotation = prevRot; // 撞了,转回去
}
}
3.3 主程序代码 (main.c)
这部分把所有模块串起来,处理按键和游戏循环。
c
运行
#include "stm32g4xx_hal.h"
#include "oled.h"
#include "tetris.h"
#include <stdlib.h>
// 函数声明
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_SPI1_Init(void);
// 全局变量
SPI_HandleTypeDef hspi1;
uint32_t g_fallTimer = 0; // 下落计时器
uint32_t g_fallInterval = 800; // 下落间隔(ms),越小越快
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_SPI1_Init();
OLED_Init(); // 初始化屏幕
Tetris_Init(); // 初始化游戏
srand(HAL_GetTick()); // 用系统时间做随机种子
while (1) {
// 1. 按键扫描(左移)
if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_0) == GPIO_PIN_RESET) {
HAL_Delay(20); // 消抖
if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_0) == GPIO_PIN_RESET) {
Tetris_MoveLeft();
while (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_0) == GPIO_PIN_RESET); // 等待松手
}
}
// 2. 按键扫描(右移)
if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_1) == GPIO_PIN_RESET) {
HAL_Delay(20);
if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_1) == GPIO_PIN_RESET) {
Tetris_MoveRight();
while (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_1) == GPIO_PIN_RESET);
}
}
// 3. 按键扫描(旋转)
if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_2) == GPIO_PIN_RESET) {
HAL_Delay(20);
if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_2) == GPIO_PIN_RESET) {
Tetris_Rotate();
while (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_2) == GPIO_PIN_RESET);
}
}
// 4. 自动下落处理
if (HAL_GetTick() - g_fallTimer >= g_fallInterval) {
g_fallTimer = HAL_GetTick();
g_currentTet.y++; // 下落一格
if (Tetris_CheckCollision()) {
// 撞到底了!
g_currentTet.y--; // 移回上一格
Tetris_FixBlock(); // 固定到面板
Tetris_ClearLines(); // 检查消行
Tetris_Spawn(); // 生成新方块
// 简单的加速机制:每固定一个方块,速度稍微变快
if (g_fallInterval > 200) {
g_fallInterval -= 10;
}
}
}
// 5. 渲染画面!
// 先画面板上固定的方块
for (uint8_t row = 0; row < GAME_HEIGHT; row++) {
for (uint8_t col = 0; col < GAME_WIDTH; col++) {
OLED_DrawBlock(col, row, g_gameBoard[row][col]);
}
}
// 再画当前正在下落的方块
for (uint8_t row = 0; row < 4; row++) {
for (uint8_t col = 0; col < 4; col++) {
if (g_tetrominoes[g_currentTet.type][g_currentTet.rotation][row] & (0x80 >> col)) {
int8_t x = g_currentTet.x + col;
int8_t y = g_currentTet.y + row;
if (y >= 0) { // 只画屏幕内的部分
OLED_DrawBlock(x, y, 1);
}
}
}
}
}
}
// 系统时钟配置(STM32G474,主频170MHz,由STM32CubeMX生成)
void SystemClock_Config(void) {
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
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 = 6;
RCC_OscInitStruct.PLL.PLLN = 85;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;
RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;
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_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) {
Error_Handler();
}
}
// SPI1初始化(由STM32CubeMX生成)
static void MX_SPI1_Init(void) {
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER;
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8;
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
if (HAL_SPI_Init(&hspi1) != HAL_OK) {
Error_Handler();
}
}
// GPIO初始化(由STM32CubeMX生成)
static void MX_GPIO_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
// OLED控制引脚:CS, DC, RST 推挽输出
HAL_GPIO_WritePin(GPIOA, OLED_CS_Pin|OLED_DC_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB, OLED_RST_Pin, GPIO_PIN_SET);
GPIO_InitStruct.Pin = OLED_CS_Pin|OLED_DC_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = OLED_RST_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// 按键引脚:PC0, PC1, PC2 下拉输入
GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLDOWN;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
}
// 错误处理(死循环)
void Error_Handler(void) {
while (1) {}
}
四、调试与常见问题
-
屏幕不亮?
- 检查电源是否是 3.3V,接线是否正确。
- 检查初始化代码里的
0x8D和0x14(电荷泵必须开)。 - 尝试调换
0xA1/0xA0和0xC8/0xC0(屏幕方向可能反了)。
-
按键没反应?
- 检查 GPIO 是否配置为 下拉输入。
- 按下按键时,用万用表测引脚是否变为高电平(或低电平,根据接线)。
-
方块显示乱码?
- 检查
g_tetrominoes数组定义是否正确。 - 检查
OLED_DrawBlock里的坐标计算是否正确。
- 检查
五、总结与扩展
恭喜你!现在你已经有了一个能跑的俄罗斯方块了。你可以继续扩展:
- 加上分数显示(需要取模软件做字库)。
- 加上 "下一个方块" 预览。
- 加上游戏结束画面和重新开始功能。