STM32G474 + 1.32 寸 OLED(128×96)俄罗斯方块游戏实现指南

一、项目概述

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.holed.ctetris.htetris.cmain.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) {}
}

四、调试与常见问题

  1. 屏幕不亮?

    • 检查电源是否是 3.3V,接线是否正确。
    • 检查初始化代码里的 0x8D0x14(电荷泵必须开)。
    • 尝试调换 0xA1/0xA00xC8/0xC0(屏幕方向可能反了)。
  2. 按键没反应?

    • 检查 GPIO 是否配置为 下拉输入
    • 按下按键时,用万用表测引脚是否变为高电平(或低电平,根据接线)。
  3. 方块显示乱码?

    • 检查 g_tetrominoes 数组定义是否正确。
    • 检查 OLED_DrawBlock 里的坐标计算是否正确。

五、总结与扩展

恭喜你!现在你已经有了一个能跑的俄罗斯方块了。你可以继续扩展:

  • 加上分数显示(需要取模软件做字库)。
  • 加上 "下一个方块" 预览。
  • 加上游戏结束画面和重新开始功能。
相关推荐
三佛科技-134163842121 小时前
SM2850P无电感离线稳压器 5V输出 典型应用电路分析(管脚、关键设计要点)
单片机·嵌入式硬件·物联网·智能家居·pcb工艺
潜创微科技2 小时前
IT6636+USB 协同芯片 3 进 1 出 HDMI2.1 KVM 切换器一体化方案
嵌入式硬件·音视频
dqsh063 小时前
关于STM32G474芯片有规律的自动重启的问题
stm32·单片机·嵌入式硬件·系统重启·原因解析
yong99903 小时前
基于 STM32 的 4×4 矩阵键盘源码
stm32·矩阵·计算机外设
JSMSEMI114 小时前
JSM63006 5A 28V三相无刷电机驱动电路
单片机·嵌入式硬件
国产芯片设计4 小时前
【LCD驱动实战】单颗YL1621脚位不足?双芯片联动驱动方案详解
stm32·单片机·mcu·51单片机·硬件工程
不怕犯错,就怕不做4 小时前
RK3562的CPU如何降频及关闭硬件编解码
linux·驱动开发·嵌入式硬件
Hical_W5 小时前
Hical 踩坑实录五部曲(二):MSVC / GCC / Clang 三平台 C++20 编译差异
linux·windows·经验分享·嵌入式硬件·macos·开源·c++20
bubiyoushang8886 小时前
基于 Freescale S12 单片机的 Bootloader 开发
单片机·嵌入式硬件·mongodb