【项目思维】通过编写一个贪吃蛇小程序,并移植到嵌入式设备上,解析编程思维的本质

这篇文章是一个 专门服务于新手向 的博客,通过编写一个贪吃蛇小程序,并将这个小游戏移植到嵌入式设备上(运行在 STM32 上、通过 OLED 显示、GPIO 按键控制的贪吃蛇小游戏)。

最重要的是,通过经历这个过程 讲述编程思维的本质。 在我们初学技术时,很多时候会被海洋一般的技术所围绕,这个时候需要通过一个小项目将此前学过的所有技术栈融合贯通的运用起来。而这个过程中,不仅是技术实现的展示,更是一次对 编程思维本质的深度剖析。

技术无远弗届,而这其中所蕴含的 编程思维制作一个完整项目的思维方式,才是更为关键的能力。往后的博客更新中,我会通过大型项目陆续介绍 项目运作能力。现在这篇文章解释 “什么是编程的思维”。


一、第一阶段,项目的初始版本

首先,我们需要用原生代码的方式编写一个小项目的初始模型:

下面是对 贪吃蛇小游戏实现(C语言 + 控制台)的流程步骤分解:

✅ 控制台显示界面✅ 键盘控制(上下左右)✅ 食物生成、得分、撞墙/自咬结束✅ 清晰的函数组织结构✅ 每行代码都有注释✅ 提供伪代码 + 流程图式结构说明

项目结构与设计

✅ 1. 游戏流程伪代码


开始游戏:
    初始化地图和蛇
    生成食物
    显示初始界面

进入主循环:
    如果有键盘输入,更新方向
    移动蛇
    判断是否吃到食物
    判断是否撞墙或撞自己
    如果游戏结束,跳出循环
    重新绘制地图
    延时控制速度

✅ 2. 游戏核心模块划分(函数设计)(基于伪代码)

函数名 功能

init_game()
初始化地图、蛇、食物

draw_map()
绘制游戏地图

draw_snake()
绘制蛇身

generate_food()
随机生成食物

move_snake()
控制蛇运动

check_collision()
检查是否撞墙或自撞

process_input()
获取用户按键

game_loop()
主循环

✅ 3. 每一步功能汇总

步骤 功能
1️⃣ 初始化游戏 设置蛇初始位置、长度、方向
2️⃣ 生成食物 随机不重复位置
3️⃣ 用户输入 通过
w a s d
改变方向
4️⃣ 移动蛇身 改变头部,移动身体,判断是否吃食物
5️⃣ 碰撞检测 判断撞墙或自撞
6️⃣ 绘制界面 显示蛇、食物、边框、分数
7️⃣ 游戏结束 打印 Game Over 和分数
完整 C 语言代码(Windows 控制台)[第一版]

📌 当前编译环境:Windows + gcc(支持
<conio.h>

由于内容较为简答,目前不需要模块化。


#include <stdio.h>
#include <conio.h>     // 用于 _kbhit() 和 _getch()
#include <windows.h>   // 用于 Sleep() 和清屏
#include <stdlib.h>    // 用于 rand()

// 地图大小
#define WIDTH  40
#define HEIGHT 20

// 方向定义
#define UP    1
#define DOWN  2
#define LEFT  3
#define RIGHT 4

// 蛇身最大长度
#define MAX_SNAKE_LENGTH 100

// 蛇结构
typedef struct {
    int x;
    int y;
} Point;

// 游戏数据
Point snake[MAX_SNAKE_LENGTH]; // 蛇身数组
int snake_length = 3;          // 初始长度为3
int direction = RIGHT;         // 初始方向向右
Point food;                    // 食物位置
int score = 0;                 // 游戏得分
int game_over = 0;             // 游戏结束标志

// 函数声明
void init_game();
void draw_map();
void draw_snake();
void generate_food();
void move_snake();
void check_collision();
void process_input();
void goto_xy(int x, int y);
int is_snake_body(int x, int y);
void game_loop();

// 主函数
int main() {
    init_game();     // 初始化游戏
    game_loop();     // 开始游戏循环
    return 0;
}

// 初始化游戏
void init_game() {
    // 初始蛇身
    snake[0].x = 10;
    snake[0].y = 5;
    snake[1].x = 9;
    snake[1].y = 5;
    snake[2].x = 8;
    snake[2].y = 5;

    generate_food(); // 生成第一个食物
    draw_map();      // 绘制初始地图
}

// 绘制地图边框
void draw_map() {
    system("cls"); // 清屏
    for (int y = 0; y <= HEIGHT; y++) {
        for (int x = 0; x <= WIDTH; x++) {
            if (x == 0 || x == WIDTH || y == 0 || y == HEIGHT)
                printf("#"); // 边界
            else if (x == food.x && y == food.y)
                printf("@"); // 食物
            else if (is_snake_body(x, y))
                printf("O"); // 蛇身
            else
                printf(" "); // 空白
        }
        printf("
");
    }
    printf("Score: %d
", score); // 显示分数
}

// 判断坐标是否在蛇身上
int is_snake_body(int x, int y) {
    for (int i = 0; i < snake_length; i++) {
        if (snake[i].x == x && snake[i].y == y)
            return 1;
    }
    return 0;
}

// 随机生成食物,不能生成在蛇身上
void generate_food() {
    do {
        food.x = rand() % (WIDTH - 2) + 1;
        food.y = rand() % (HEIGHT - 2) + 1;
    } while (is_snake_body(food.x, food.y));
}

// 移动蛇身
void move_snake() {
    // 保存头部位置
    Point new_head = snake[0];

    // 根据方向更新头部位置
    switch (direction) {
        case UP:    new_head.y--; break;
        case DOWN:  new_head.y++; break;
        case LEFT:  new_head.x--; break;
        case RIGHT: new_head.x++; break;
    }

    // 吃到食物
    if (new_head.x == food.x && new_head.y == food.y) {
        snake_length++;      // 蛇身变长
        score += 10;         // 得分增加
        generate_food();     // 生成新食物
    }

    // 蛇身移动:从尾到头依次移动位置
    for (int i = snake_length - 1; i > 0; i--) {
        snake[i] = snake[i - 1];
    }

    // 更新头部
    snake[0] = new_head;

    // 检查是否撞墙或自咬
    check_collision();
}

// 检查碰撞
void check_collision() {
    // 撞墙
    if (snake[0].x <= 0 || snake[0].x >= WIDTH || snake[0].y <= 0 || snake[0].y >= HEIGHT) {
        game_over = 1;
    }

    // 撞自己
    for (int i = 1; i < snake_length; i++) {
        if (snake[0].x == snake[i].x && snake[0].y == snake[i].y) {
            game_over = 1;
        }
    }
}

// 处理方向输入
void process_input() {
    if (_kbhit()) { // 如果有键盘输入
        char key = _getch();
        switch (key) {
            case 'w': if (direction != DOWN)  direction = UP; break;
            case 's': if (direction != UP)    direction = DOWN; break;
            case 'a': if (direction != RIGHT) direction = LEFT; break;
            case 'd': if (direction != LEFT)  direction = RIGHT; break;
        }
    }
}

// 主游戏循环
void game_loop() {
    while (!game_over) {
        process_input();  // 处理键盘输入
        move_snake();     // 移动蛇
        draw_map();       // 重新绘制地图
        Sleep(100);       // 延时控制速度
    }

    // 游戏结束提示
    printf("Game Over! Your score: %d
", score);
}
运行效果预览(文本界面)

########################################
#                                      #
#   OOO                                #
#                                      #
#      @                               #
#                                      #
########################################
Score: 10

二、第二阶段,接下来,需要在此基础上进一步优化代码:

例如:新增需求:

① 需要图形化版本(基于 SDL 或
ncurses
)或 Linux 可移植版本。
② 增加暂停(如按
p
);
③ 支持多种速度或难度;④ 存档功能(使用文件保存 score);⑤ 用户 账号 切换:用户一和用户二登入玩游戏,分别含有的是各自不同的存档。…等等更多

接下来,优化代码 并 新增需求:

基于 ncurses 实现的 Linux 终端版贪吃蛇游戏(C语言)
同理:首先是:✅ 流程图式伪代码(简化)

		+-------------------------+
        | 用户登录(用户一/用户二)|
        +-----------+-------------+
                    |
          +---------v---------+
          | 选择难度(速度)   |
          +---------+---------+
                    |
          +---------v---------+
          | 初始化游戏数据     |
          +---------+---------+
                    |
          +---------v---------+
          | 主循环            |
          |  - 读取输入       |
          |  - 更新蛇位置     |
          |  - 检查碰撞       |
          |  - 生成食物       |
          |  - 绘制界面       |
          +-----+---+---+-----+
                |   |   |
          +-----v   v   v-----+
    撞墙/自咬?      暂停?     继续游戏
                    |
	         +------v-----+
	         |  保存得分   |
	         +------------+
📦 步骤列表汇总

【1】用户系统

登录界面,输入用户名(user1/user2)加载对应
score_user1.txt

score_user2.txt

【2】选择难度

1(easy):蛇移动慢2(medium):中速3(hard):移动快

【3】初始化游戏状态

初始蛇身、方向、地图边界生成首个食物

【4】主游戏循环

用户方向键控制移动吃到食物增加长度撞墙或自咬游戏结束
p
键暂停切换

【5】游戏结束

显示得分写入用户得分文件

基于流程式伪代码:然后是:✅ 实现功能总览
功能项 说明
✅ 控制台图形界面 使用
ncurses
实现字符图形界面
✅ 键盘控制 实时响应方向键,支持
WASD
✅ 食物生成 食物随机生成,不与蛇重叠
✅ 碰撞检测 撞墙或自咬即 Game Over
✅ 分数管理 实时显示得分
✅ 暂停功能
p
键暂停/继续游戏
✅ 多种速度 启动时选择难度(1-简单,2-中等,3-困难)
✅ 存档功能 每个用户的分数保存在对应文件
✅ 用户系统 支持“用户一”和“用户二”登录、切换存档
具体代码实现(Linux + ncurses):先基于函数结构设计将函数大体框架 和 参数接口 写好,然后根据业务的实现逻辑 逐个补全 函数体的内部实现。

由于环境需要:⚠️ 需先安装
ncurses


sudo apt install libncurses5-dev
gcc snake_ncurses.c -lncurses -o snake
./snake

#include <ncurses.h>      // 图形库
#include <stdlib.h>       // rand()
#include <unistd.h>       // usleep()
#include <string.h>       // strcmp()
#include <time.h>         // time()

#define WIDTH  40         // 地图宽度
#define HEIGHT 20         // 地图高度
#define MAX_LEN 100       // 蛇最大长度

typedef struct {
    int x, y;
} Point;

// 全局变量
Point snake[MAX_LEN];     // 蛇身体
int snake_len = 3;        // 蛇初始长度
int direction = KEY_RIGHT;// 起始方向
Point food;               // 食物坐标
int score = 0;            // 得分
int paused = 0;           // 是否暂停
int game_over = 0;        // 游戏结束
char username[20];        // 当前用户
int speed = 200000;       // 默认速度(us)

// 函数声明
void init_game();
void draw_game();
void move_snake();
void generate_food();
void check_collision();
void save_score();
void load_score();
int is_snake_body(int x, int y);
void login_user();
void select_difficulty();

int main() {
    initscr();              // 初始化 ncurses
    curs_set(0);            // 隐藏光标
    keypad(stdscr, TRUE);   // 开启方向键
    noecho();               // 不显示输入
    nodelay(stdscr, TRUE);  // 非阻塞输入
    srand(time(0));         // 随机种子

    login_user();           // 用户登录
    load_score();           // 读取存档
    select_difficulty();    // 设置速度
    init_game();            // 初始化游戏

    while (!game_over) {
        int ch = getch();
        if (ch == 'p' || ch == 'P') paused = !paused;
        if (!paused) {
            if (ch == KEY_UP && direction != KEY_DOWN) direction = KEY_UP;
            if (ch == KEY_DOWN && direction != KEY_UP) direction = KEY_DOWN;
            if (ch == KEY_LEFT && direction != KEY_RIGHT) direction = KEY_LEFT;
            if (ch == KEY_RIGHT && direction != KEY_LEFT) direction = KEY_RIGHT;
            move_snake();
            check_collision();
            draw_game();
            usleep(speed);
        } else {
            mvprintw(HEIGHT + 2, 0, "Paused. Press 'p' to resume.");
            refresh();
        }
    }

    save_score(); // 游戏结束保存分数
    nodelay(stdscr, FALSE);
    mvprintw(HEIGHT + 4, 0, "Game Over! Final Score: %d", score);
    mvprintw(HEIGHT + 5, 0, "Press any key to exit...");
    getch();
    endwin(); // 退出 ncurses
    return 0;
}

// 登录用户
void login_user() {
    echo();
    mvprintw(0, 0, "Enter username (user1/user2): ");
    getstr(username);
    noecho();
}

// 选择难度
void select_difficulty() {
    mvprintw(2, 0, "Select difficulty (1-Easy, 2-Medium, 3-Hard): ");
    int level = getch() - '0';
    switch (level) {
        case 1: speed = 300000; break;
        case 2: speed = 200000; break;
        case 3: speed = 100000; break;
        default: speed = 200000;
    }
    clear();
}

// 初始化游戏
void init_game() {
    snake[0] = (Point){10, 5};
    snake[1] = (Point){9, 5};
    snake[2] = (Point){8, 5};
    generate_food();
    draw_game();
}

// 绘图函数
void draw_game() {
    clear();
    // 边界
    for (int x = 0; x <= WIDTH; x++) {
        mvprintw(0, x, "#");
        mvprintw(HEIGHT, x, "#");
    }
    for (int y = 1; y < HEIGHT; y++) {
        mvprintw(y, 0, "#");
        mvprintw(y, WIDTH, "#");
    }

    // 食物
    mvprintw(food.y, food.x, "@");

    // 蛇
    for (int i = 0; i < snake_len; i++) {
        mvprintw(snake[i].y, snake[i].x, i == 0 ? "O" : "o");
    }

    mvprintw(HEIGHT + 1, 0, "User: %s | Score: %d", username, score);
    refresh();
}

// 判断是否在蛇身上
int is_snake_body(int x, int y) {
    for (int i = 0; i < snake_len; i++) {
        if (snake[i].x == x && snake[i].y == y)
            return 1;
    }
    return 0;
}

// 生成食物
void generate_food() {
    do {
        food.x = rand() % (WIDTH - 2) + 1;
        food.y = rand() % (HEIGHT - 2) + 1;
    } while (is_snake_body(food.x, food.y));
}

// 移动蛇
void move_snake() {
    Point new_head = snake[0];
    switch (direction) {
        case KEY_UP:    new_head.y--; break;
        case KEY_DOWN:  new_head.y++; break;
        case KEY_LEFT:  new_head.x--; break;
        case KEY_RIGHT: new_head.x++; break;
    }

    // 吃到食物
    if (new_head.x == food.x && new_head.y == food.y) {
        snake_len++;
        score += 10;
        generate_food();
    }

    // 移动身体
    for (int i = snake_len - 1; i > 0; i--) {
        snake[i] = snake[i - 1];
    }
    snake[0] = new_head;
}

// 碰撞检测
void check_collision() {
    if (snake[0].x <= 0 || snake[0].x >= WIDTH || snake[0].y <= 0 || snake[0].y >= HEIGHT)
        game_over = 1;

    for (int i = 1; i < snake_len; i++) {
        if (snake[0].x == snake[i].x && snake[0].y == snake[i].y)
            game_over = 1;
    }
}

// 存档保存
void save_score() {
    char filename[30];
    sprintf(filename, "score_%s.txt", username);
    FILE *fp = fopen(filename, "w");
    if (fp) {
        fprintf(fp, "%d
", score);
        fclose(fp);
    }
}

// 加载分数
void load_score() {
    char filename[30];
    sprintf(filename, "score_%s.txt", username);
    FILE *fp = fopen(filename, "r");
    if (fp) {
        fscanf(fp, "%d", &score);
        fclose(fp);
    }
}

如若你还需要新增新功能,在此基础之上进行版本迭代即可,但实现的思路方式与上述第二阶段完全一致。当情况逐渐变得复杂的时候,代码量逐渐因需求的增多而增多,这时候我们就需要进行 代码优化、模块化划分、文件划分等工作,后面会介绍走到这一步时具体该怎么做。


第三阶段,将这个贪吃蛇小游戏移植到嵌入式设备

✅ 1、移植目标与环境

🎯 目标:将文字界面的贪吃蛇移植到 STM32 单片机 + OLED(如 128×64 SSD1306)上运行,使用按键控制方向,OLED 显示蛇、食物等游戏内容。

✅ 2、硬件要求

此处 针对硬件的需求和条件:

硬件 说明
STM32 MCU 此处使用 STM32F103C8T6(或任意 STM32 系列)
OLED 屏 SSD1306 驱动 128×64 OLED(I2C 或 SPI)
按键 上下左右 + 暂停(可用 GPIO)
开发环境 STM32CubeMX + Keil / STM32CubeIDE
🧠 三、游戏逻辑简化(适合嵌入式)

原PC版功能						嵌入式优化版
控制台打印						OLED 显示点阵
键盘 WASD						GPIO 按键控制
文件存档							Flash/Eeprom 存储(可选)
ncurses 控制台					OLED 点阵图形
延时 usleep						HAL_Delay 或 SysTick
✅ 四、系统架构设计
模块 功能

snake.c
蛇体逻辑、碰撞检测、生成食物

oled.c
OLED 显示驱动函数(画点、画矩形)

input.c
按键扫描,方向控制

main.c
初始化 + 主循环

storage.c
(可选)

EEPROM/Flash
存档管理
📐 五、OLED 分辨率适配

如果 OLED 是
128x64
点阵,可以将整个游戏划分成网格:


单位格大小:8x8 像素
地图大小:宽 16 × 高 8 格(128 ÷ 8,64 ÷ 8)
步骤列表

步骤流程:
1️⃣ 初始化 OLED、按键、时钟
2️⃣ 初始化蛇身和食物
3️⃣ 主循环中读取按键方向
4️⃣ 更新蛇身位置
5️⃣ 判断是否吃到食物或撞墙
6️⃣ 在 OLED 上绘制地图、蛇、食物
7️⃣ 游戏结束后显示提示
8️⃣ (可选)将分数保存到 Flash


💾 移植版贪吃蛇核心代码区域

下面是移植版核心逻辑(适配 STM32 + SSD1306 OLED):

1️⃣
snake.h


#ifndef __SNAKE_H__
#define __SNAKE_H__

#define MAP_WIDTH  16
#define MAP_HEIGHT 8
#define MAX_LEN    128

typedef enum {UP, DOWN, LEFT, RIGHT} Direction;

typedef struct {
    uint8_t x, y;
} Point;

extern void Snake_Init(void);
extern void Snake_Update(void);
extern void Snake_Draw(void);
extern void Snake_ChangeDir(Direction dir);
extern uint8_t Snake_IsGameOver(void);

#endif

2️⃣
snake.c


#include "snake.h"
#include "oled.h"
#include "stdlib.h"

static Point snake[MAX_LEN];
static uint8_t length = 3;
static Direction dir = RIGHT;
static Point food;
static uint8_t game_over = 0;

static void GenerateFood(void) {
    do {
        food.x = rand() % MAP_WIDTH;
        food.y = rand() % MAP_HEIGHT;
    } while (Snake_IsBody(food.x, food.y));
}

uint8_t Snake_IsBody(uint8_t x, uint8_t y) {
    for (uint8_t i = 0; i < length; i++) {
        if (snake[i].x == x && snake[i].y == y) return 1;
    }
    return 0;
}

void Snake_Init(void) {
    length = 3;
    dir = RIGHT;
    snake[0] = (Point){5, 4};
    snake[1] = (Point){4, 4};
    snake[2] = (Point){3, 4};
    GenerateFood();
    game_over = 0;
}

void Snake_ChangeDir(Direction new_dir) {
    if ((dir == UP && new_dir == DOWN) ||
        (dir == DOWN && new_dir == UP) ||
        (dir == LEFT && new_dir == RIGHT) ||
        (dir == RIGHT && new_dir == LEFT)) return;
    dir = new_dir;
}

void Snake_Update(void) {
    if (game_over) return;

    Point new_head = snake[0];
    switch (dir) {
        case UP:    new_head.y--; break;
        case DOWN:  new_head.y++; break;
        case LEFT:  new_head.x--; break;
        case RIGHT: new_head.x++; break;
    }

    if (new_head.x >= MAP_WIDTH || new_head.y >= MAP_HEIGHT) {
        game_over = 1;
        return;
    }

    for (uint8_t i = 0; i < length; i++) {
        if (snake[i].x == new_head.x && snake[i].y == new_head.y) {
            game_over = 1;
            return;
        }
    }

    for (int i = length; i > 0; i--) {
        snake[i] = snake[i - 1];
    }
    snake[0] = new_head;

    if (new_head.x == food.x && new_head.y == food.y) {
        length++;
        GenerateFood();
    } else {
        // 不吃食物则蛇尾自动消失
    }
}

void Snake_Draw(void) {
    OLED_Clear();

    // 画蛇身
    for (uint8_t i = 0; i < length; i++) {
        OLED_DrawRect(snake[i].x * 8, snake[i].y * 8, 8, 8, 1);
    }

    // 画食物
    OLED_DrawFilledRect(food.x * 8, food.y * 8, 8, 8, 1);

    // 显示分数
    OLED_ShowNum(0, 0, length - 3, 2);

    if (game_over) {
        OLED_ShowString(30, 30, "GAME OVER", 16);
    }

    OLED_Refresh();
}

uint8_t Snake_IsGameOver(void) {
    return game_over;
}

3️⃣
main.c


#include "snake.h"
#include "oled.h"
#include "key.h"
#include "stm32f1xx_hal.h"

int main(void) {
    HAL_Init();
    SystemClock_Config();
    OLED_Init();
    KEY_Init();

    Snake_Init();

    while (1) {
        if (KEY_UP_PRESSED())    Snake_ChangeDir(UP);
        if (KEY_DOWN_PRESSED())  Snake_ChangeDir(DOWN);
        if (KEY_LEFT_PRESSED())  Snake_ChangeDir(LEFT);
        if (KEY_RIGHT_PRESSED()) Snake_ChangeDir(RIGHT);

        Snake_Update();
        Snake_Draw();

        HAL_Delay(300); // 控制蛇的速度
    }
}

4️⃣
oled.c
(因为代码量过大,而且此处OLED驱动代码在网上四处可见,找AI问一下就行了,所以这里我简略给个函数结构)

使用已有的
SSD1306
驱动库,如
u8g2 / Adafruit SSD1306 /
自写库,需提供:


OLED_Init()

OLED_DrawRect(x, y, w, h, color)

OLED_DrawFilledRect(...)

OLED_ShowString(x, y, str, size)

OLED_ShowNum(x, y, num, len)

OLED_Refresh()

OLED_Clear()

5️⃣
key.c

读取 GPIO 按键输入,返回是否按下:


uint8_t KEY_UP_PRESSED(void)    { return HAL_GPIO_ReadPin(GPIOx, GPIO_PIN_X) == 0; }
uint8_t KEY_DOWN_PRESSED(void)  { return HAL_GPIO_ReadPin(GPIOx, GPIO_PIN_Y) == 0; }
...

✅ 存档功能(可选)

使用 Flash 或 EEPROM 保存得分(可使用 STM32 HAL Flash 写入 API):


#define SAVE_ADDR 0x0800FC00 // 最后一个扇区

void SaveScore(uint32_t score) {
    HAL_FLASH_Unlock();
    HAL_FLASH_Program(TYPEPROGRAM_WORD, SAVE_ADDR, score);
    HAL_FLASH_Lock();
}

uint32_t LoadScore(void) {
    return *(uint32_t*)SAVE_ADDR;
}

效果展示(示意图)


OLED 屏幕 128x64:

+------------------------+
|  OOO         @         |
|                        |
|                        |
|                        |
|      GAME OVER         |
+------------------------+
Score: 10

第四阶段,在此基础之上继续扩展贪吃蛇游戏的嵌入式版本

模块 功能说明
工程模板 提供
Keil

STM32CubeIDE
工程结构
显示支持 兼容
SSD1306 OLED、TFT、LCD
声音音效 蜂鸣器发声,吃食物/失败提示
动画过渡 游戏开始/结束动画
菜单系统 用户选择:开始游戏 / 难度 / 存档 / 音效开关
📁 1、更新完整工程模板结构(CubeIDE 或 Keil)(此处可以用CMake文件管理工具)

SnakeGame_STM32/
├── Core/
│   ├── Inc/
│   │   ├── main.h
│   │   ├── snake.h
│   │   ├── menu.h
│   │   ├── display.h
│   │   ├── sound.h
│   │   └── key.h
│   ├── Src/
│   │   ├── main.c
│   │   ├── snake.c
│   │   ├── menu.c
│   │   ├── display.c
│   │   ├── sound.c
│   │   └── key.c
├── Drivers/
│   ├── OLED/                // OLED 驱动(SSD1306)
│   ├── TFT_LCD/             // 可选:TFT 显示驱动(ILI9341)
│   ├── Buzzer/              // 蜂鸣器控制
├── Middlewares/
│   └── u8g2/                // 图形库(可选)
├── SnakeGame.ioc           // STM32CubeMX 工程文件
├── KeilProject.uvprojx     // Keil 工程(可选)
└── README.md
🖥️ 2、显示屏移植说明

✅ OLED(SSD1306)

I2C 驱动;建议使用已有库:
ssd1306.h/c
;单位格建议
8x8
像素 →
16x8
网格;

✅ TFT LCD(如 ILI9341)

SPI 接口,分辨率 240×320;使用库:
Adafruit_GFX

LCD_ILI9341.c
;单位格建议 16×16 像素 → 15×10 网格;

示例绘制蛇块(TFT):


LCD_DrawFilledRect(snake[i].x * 16, snake[i].y * 16, 16, 16, COLOR_GREEN);
LCD_DrawFilledRect(food.x * 16, food.y * 16, 16, 16, COLOR_RED);
🔊 3、声音控制模块(
sound.c

连接方式:

蜂鸣器接 GPIO(PWM 推荐)使用
TIMx
+
PWM
控制发声频率

函数:


void Sound_PlayEat() {
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, GPIO_PIN_SET);
    HAL_Delay(100);
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, GPIO_PIN_RESET);
}

void Sound_PlayGameOver() {
    for (int i = 0; i < 3; i++) {
        Sound_PlayEat();
        HAL_Delay(200);
    }
}
🧩 4、菜单系统设计(
menu.c

OLED菜单界面功能:

按钮 功能

UP / DOWN
菜单项选择

A / OK
进入菜单

B / BACK
返回上级菜单

示例界面:


> Start Game
  Difficulty: Easy
  Sound: ON
  Load Save

函数接口:


void Menu_Draw();                 // 显示菜单
void Menu_ProcessKey(uint8_t k);  // 响应按键
uint8_t Menu_IsInGame();          // 是否进入游戏
🎞️ 5、动画系统设计

开场动画:


void Animation_Opening() {
    for (int i = 0; i < 16; i++) {
        OLED_DrawRect(i * 8, 0, 8, 64, 1);
        OLED_Refresh();
        HAL_Delay(30);
    }
    OLED_Clear();
}

游戏结束动画:


void Animation_GameOver() {
    OLED_ShowString(20, 30, "GAME OVER", 16);
    for (int i = 0; i < 3; i++) {
        OLED_InvertDisplay(1);
        HAL_Delay(150);
        OLED_InvertDisplay(0);
        HAL_Delay(150);
    }
}
💾 6、存档功能

使用 Flash 存储(推荐)


#define FLASH_ADDR 0x0800FC00

void SaveScore(uint32_t score) {
    HAL_FLASH_Unlock();
    HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, FLASH_ADDR, score);
    HAL_FLASH_Lock();
}

uint32_t LoadScore(void) {
    return *(uint32_t*)FLASH_ADDR;
}

多用户存档(
user1/user2

使用 EEPROM 或 Flash 区段保存结构体:


typedef struct {
    char name[8];
    uint32_t score;
    uint8_t difficulty;
} SaveData;
✅ 7、最终运行流程

显示菜单界面(选择开始游戏、难度、音效)初始化蛇、食物、地图按键控制蛇移动吃到食物变长,播放音效撞墙或自咬,播放 Game Over 动画 & 音效存档分数到 Flash回到菜单界面

如果你使用 CubeIDE 生成代码模版,然后写入贪吃蛇的代码逻辑:
代码逻辑同上完全一致。只是在形式上还需要注意一些问题:

生成的工程文件夹要 包含全部
.c/.h
源码OLED 或 TFT 驱动STM32CubeMX
.ioc
Makefile 或 Keil 工程文件注意自己在项目中使用到的硬件型号和开发环境,工程源码要包含完整工程的代码结构和打包说明(最好可以自动生成
.ioc
配置内容)


回归至文章标题:基于上述案例的过程解析编程思维的本质。

🎯五、什么是编程思维的本质?(☆☆☆本文核心☆☆☆)

探讨 贪吃蛇小游戏在 STM32 嵌入式环境中的完整实现过程,不仅是技术实现的展示,更是一次对 编程思维本质的深度剖析。

编程思维本质就是:

把现实世界的问题进行抽象建模,通过逻辑结构与算法构建可控、可扩展的系统,最终转化为可执行的程序流程。

从贪吃蛇案例来拆析着看编程思维:
🧩 1. 抽象建模:将现实问题数字化
要实现的对象 抽象成程序结构

Point[]
数组(坐标队列)
食物
Point
坐标
地图边界 宽高限制常量
用户输入 按键中断 / GPIO 读取方向
碰撞 判断蛇头与身体/边界是否重合
分数 整型计数器,吃食物 +10

📌 本质:将“视觉图形”抽象为“数据状态”,让程序“认知到要实现的对象”


🔄 2. 状态驱动:构建系统逻辑

一定要深刻理解这句话:

程序不是一成不变的,而是随着状态变化而驱动行为。

程序是运动的过程。 举个例子:

状态枚举(类似生命周期):
INIT

RUNNING

PAUSED

GAME_OVER
状态机模型驱动流程:


switch (game_state) {
    case INIT:     Snake_Init(); break;
    case RUNNING:  Snake_Update(); Snake_Draw(); break;
    case PAUSED:   OLED_Show("Paused"); break;
    case GAME_OVER: OLED_Show("Game Over"); break;
}

📌 本质:程序 = 状态 + 转移 + 响应


🔀 3. 模块分离:控制复杂度

复杂系统需要拆解为模块,每个模块职责单一(模块化:将程序代码切分成一个一个对应的模块,类似乐高积木那样的最小单元):

我们这个例子中的模块:

模块 职责

snake.c
蛇的逻辑、运动、碰撞

oled.c
显示驱动

menu.c
菜单导航与状态机

sound.c
音效控制

key.c
按键输入

模块化编程是一种将程序 拆解为若干独立的模块(Module) 的设计方法。
每个模块完成特定的功能,内部封装实现细节,对外暴露接口。

模块化的编程思维本质:

把一个大问题,拆解成多个小问题,每个模块专注处理一部分逻辑,组合起来解决整体问题。

📌 本质:高内聚、低耦合,分而治之


🧮 4. 算法设计:解决核心问题

在程序实现的过程中,有些部分内容相对简单,但有些地方是复杂、需要精确算法计算才能够实现的部分:

示例:蛇身移动算法:(此处拿简单的内容举例,是为了直观一目了然)


for (i = length; i > 0; i--) {
    snake[i] = snake[i - 1];
}
snake[0] = new_head;

采用 队列头插尾删 模式,模拟蛇移动吃食物则不删尾,自动增长

📌 本质:用最优的逻辑结构解决问题


🎯 5. 用户体验:输入+反馈闭环

程序不是一成不变的,需要根据用户体验和反馈不断进行优化迭代升级的过程。

按键方向输入 → 实时响应撞墙 → 游戏结束提示吃到食物 → 播放音效 + 分数+1

想一想,如果你玩自己设计的程序,还需要具备哪些别的东西呢?

📌 本质:程序的设计要围绕“人机交互”构建闭环反馈系统


💾 6. 数据持久化:状态跨时保存(嵌入式方向)

存档功能:
Flash
中保存
score
用户管理:根据用户名选择存档区段

📌 本质:程序的“记忆”能力 = 状态持久化,当下次用户登录进来的时候数据不会丢失。


🔁 7. 可扩展性:让程序可进化

从最初的:

✅ 控制台贪吃蛇
→ 移植到 OLED
→ 加入菜单、音效、动画、存档

📌 本质:代码架构应支持未来不断增长的需求,从刚开始设计程序的时候就要考虑到后期的迭代、需求变化、可移植性、模块化、函数封装等等问题。

综上。整合编程思维的七个核心关键词:


编号		核心思维			实践体现
1️⃣		抽象				将蛇、食物、地图抽象成数据结构
2️⃣		分解				拆分为 snake、oled、input 等模块
3️⃣		状态管理			游戏状态机:运行、暂停、结束
4️⃣		控制流程			主循环 + 输入判断 + 状态跳转
5️⃣		数据结构			队列(蛇身)、结构体(点)
6️⃣		反馈机制			得分、动画、声音、界面
7️⃣		可扩展性			支持分辨率变化、平台移植

编程不是“写代码”,而是将一个系统问题结构化解决的完整过程。 你的目的是要成为一个能结构化思考系统化建模逻辑化设计 问题的开发者。

一个真正的程序员,一定是系统建构师 + 逻辑设计师 + 用户体验设计者的复合体。

当然,掌握编程思维也并非一朝一夕可以实现的。所以下一篇文章在此基础之上,我将为你介绍系统性、分阶段的 编程思维学习路线,从零基础小白到系统架构级别,帮助你全面建立和提升编程思维能力。

以上,欢迎有从事同行业的电子信息工程、互联网通信、嵌入式开发的朋友共同探讨与提问,我可以提供实战演示或模板库。希望内容能够对你产生帮助!

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容