本文作者:烟火之旅

基于安信可Ai-M61-32S-Kit的肉鸽生存游戏实现方案

烟火之旅 2026-09-21 1634
本文作者robot-cnGitHub 主页 github.com/robot-cn

原帖 github.com/Ai-Thinker-Open/.github/discussions/39来自安信可开源社区

本项目基于开源社区的 ESP32/Arduino/TFT_eSPI 肉鸽工程("Last 7 Days")移植到安信可 Ai-M61-32S-Kit(BL616/BL618),本帖重点分享从"游戏源码"到"C++ 工程在博流 SDK 上跑起来"的全过程实现与优化思路。

01效果

dc81e74a-b026-11f1-90a1-92fbcf53809c.jpgdd3b5fa4-b026-11f1-90a1-92fbcf53809c.jpg

[实战画面:Day 1 草地场景,Lv2、HP/EXP 条、伤害数字飘字]

一句话玩法:类"吸血鬼幸存者"自动攻击,攒经验升级弹三选一卡牌,撑过 7 天(现实 11 分钟)算胜利。

游戏内按 GPIO 24 确认键呼出属性面板(Level/HP/Weapon/Crit/DPS),同时世界暂停,再按一次恢复。

02硬件清单

器件 型号 说明
主控 安信可 Ai-M61-32S-Kit BL616/BL618, RISC-V, 480KB SRAM
屏幕 ST7789 SPI LCD 320×240 RGB565,游戏实际绘制 240×240 居中
按键 轻触按键 ×5 上/下/左/右 + 确认,公共端接 GND
杜邦线 若干

软件:bouffalo_sdk(官方 SDK,含 RISC-V GCC 工具链+ BL616 全套驱动)。

03接线(照抄即可)

3.1 屏幕(ST7789,SPI0)

LCD 引脚 接 M61-32S-Kit 备注
VCC 3V3
GND GND
CS GPIO12 软件控制
CLK(SCL) GPIO33 SPI0 硬件时钟
MOSI(SDA) GPIO19 SPI0 硬件
MISO GPIO18 屏不用,可不接
RES GPIO26
DC(RS) GPIO27
BLK GPIO28 背光
注意:Ai-M61-32S-Kit 上GPIO4~9 是 NC(与模组内部 Flash 共用引脚,接不通),方向键特意避开了这一段。

3.2 按键(5 个,active-low)

按键一端接 GPIO,另一端统一接GND。按下读 0(LOW),松开被芯片内部上拉读 1(HIGH)。

操作 GPIO
GPIO10
GPIO11
GPIO13
GPIO14
确认 / 交互 GPIO24

04博流 SDK 上的工程亮点

这部分是本帖最想分享的:博流 SDK 官方 examples 里几乎没有 C++ 工程,而这个游戏的源码是 Arduino C++ 写的,几乎没法像 ESP-IDF 那样直接有现成模板套用。整套移植 = "C++ 工具链打通 + 图形加速 + 输入重构 + 代码分层",下面是几个关键点。

4.1 在博流 SDK 上从零打通 C++ 工具链

SDK 启用 CXX 后能直接拉riscv64-unknown-elf-g++,但有几个"没有文档提"的坑需要自己补:

·CMakeLists.txt必须project(dappfife3)触发 SDK 的_project(... C CXX)宏,自动把 g++ 工具链链入;默认-std=c++11 -fno-rtti -fno-exceptions -nostdlib;

·C++ 全局构造不会自动跑:博流start.S不调__libc_init_array(),所以 C++ 写的全局对象(TFT_eSprite screenBuffer;、全局tft;等)如果不手动构造,就是野指针。

在main.c的board_init()后必须显式extern void __libc_init_array(void); __libc_init_array();,并补一个__dso_handle.c定义void *__dso_handle = 0;否则链接期直接报undefined reference;

·Arduino.h的min/max/abs不能是宏(会破坏 / / 的std::numeric_limits<>::min()等),改写为 C++ 模板;random重载不能放进extern "C"块(newlib 已有long random(void)会冲突);String operator+自由函数需要friend声明访问私有_append;

· 给st7789.h的 C 函数声明加extern "C"包裹,否则 C++ 端st7789_init会被 name-mangling,与 C 编译的st7789.c符号名对不上 → 链接期undefined reference。

这套 C++ 启动修复是博流 SDK 上跑 C++ 工程必备的 4 步。

4.2 SPI + DMA 异步双缓冲送帧(关键性能优化)

游戏画面 =240×240 RGB565 软件帧缓冲(malloc 115200 字节在堆上),全部图元在内存里画完再一次性推给屏幕。这里的"一次性推送"用到了博流 SDK 的SPI0 + DMA 双缓冲:

·st7789_draw_picture_dma()内部维护两片 buffer:

1. 等待上一帧 DMA 全部完成(CPU 不会空等太久);

2. 把当前帧拷贝到"背 buffer";

3. 配置 SPI0 RAMWR 窗口、bflb_dma启动 DMA、函数立即返回;

4. CPU 在 DMA 传输过程中继续算下一帧 → 渲染与传输并行。

· SPI 时钟拉到160MHz(ST7789_SPI_FREQ),整帧 115200 字节理论传输 5.75ms,实测比 80MHz 明显更稳(轮询开销被 DMA 吃掉)。

· 帧率门控FRAME_INTERVAL_MS = 10,串口看 FPS 稳定在 60+:

[FPS] 64 fps, ~15 ms/frame

DMA 送帧是这块设计的关键——没有 DMA 时一帧 ~20~30ms,且主循环被 SPI 阻塞;上 DMA 后 CPU 与 SPI 平行,整帧延迟接近纯渲染时间。

4.3 输入层 0 个 ADC / 0 个 PWM

原游戏是"模拟摇杆 + 1 按钮",但 BL616 ADC 通道被屏幕占了、且游戏玩法对 ADC 精度要求不高,直接砍掉:

· 5 个 GPIO 全部用bflb_gpio_init(key_gpio, pin, GPIO_INPUT | GPIO_PULLUP | GPIO_SMT_EN | GPIO_DRV_1)配成上拉输入;

·digitalRead()内部 lazy init:第一次调用才把 5 个脚一起配成输入(避免在 setup 阶段多耗一个外设依赖);

· 按下 = LOW = 摇杆"满偏方向",readJoystickX/Y把方向键映射回摇杆语义,game/Utils.cpp的核心输入 API 零改动——这也是上一条 C++ 兼容层真正发挥作用的地方。

4.4 代码分区与资源占用

资源 占用 说明
固件 bin ~226 KB 优化-O3,Flash 占用约 5%
静态 RAM 几十 KB 业务代码 + 兼容层
游戏帧缓冲 112 KB 240×240×2 malloc 在堆,BL616 480KB SRAM 完全够
代码目录 shim / game / 驱动 三层 见下

Dappfife3/

├── main.c # C 入口:board_init + __libc_init_array + game_setup/loop

├── app.cpp # 状态机(菜单/游戏/结算/卡牌)

├── st7789.c / st7789.h # ST7789 驱动(SPI0 + DMA 双缓冲)

├── __dso_handle.c # C++ 全局对象需要

├── CMakeLists.txt / Makefile / defconfig ...

├── game/ # 业务逻辑(Config/Utils/Entity/Render/game.cpp)

├── shim/ # Arduino/TFT_eSPI API 兼容层(移植关键)

│ ├── Arduino.h/.cpp # millis/String/GPIO → bflb_*

│ ├── TFT_eSPI.h/.cpp # TFT_eSprite 软件帧缓冲 + 推屏

│ └── font5x7.h

└── img/ # 帖子里这 3 张实拍图

shim 层的可复用性:把shim/整个目录视为"Arduino on BL616"的移植层。以后再移植别的 TFT_eSPI / Arduino 小游戏,只需在game/替换业务 .cpp/.h,shim/与st7789.c不用动。

4.5 博流 SDK 特性一览

SDK 能力 用途
board_init() 一键初始化外设时钟/IO/Console,0 配置启动
bflb_mtimer_get_time_ms/us() 实现 Arduinomillis()/micros()(64-bit cycle counter 高精度
bflb_gpio_init / bflb_gpio_read 实现pinMode/digitalRead(无 ADC/PWM)
bflb_spi + bflb_dma SPI0 异步双缓冲送帧(4.2 节)
CMake + project(dappfife3) 启用 C/CXX/ASM 工具链 + 链接 SDK 一键产出 bin
printf + 串口 调试日志(固件已加每 5 秒[FPS]诊断输出)

05编译 + 烧录(Windows)

工程目录开一个cmd窗口:

cd bouffalo_sdkexamplesDappfife3

:: 1) 编译

make CHIP=bl616 BOARD=bl616dk

:: 2) 烧录(换成你自己的串口号)

make flash CHIP=bl616 BOARD=bl616dk COMX=COM4

产物在build/build_out/dappfife3_bl616.bin(约 226KB)。

踩坑提示:

·make flash只烧录、不编译!改完代码务必先单独执行make再烧录,否则烧的是旧固件(判断方法:看两次烧录日志里的 SHA 是否变化);

· 如果是在 Git Bash / WSL 等非 cmd 环境跑,必须显式make OS=Windows_NT SHELL=cmd.exe,否则 make 会把 Windows 误判成 Linux、去找不存在的BLFlashCommand-ubuntu烧录工具。

烧录后打开串口监视器(115200),固件每 5 秒打印一条真实帧率日志:

[FPS] 64 fps, ~15 ms/frame

06想改哪里?

6.1 换按键 —— 改两处,必须同步

①game/Config.h的按键宏:

// 方向键 + 确认键(active-low,内部上拉)

#defineKEY_UP_GPIO 10

#defineKEY_DOWN_GPIO 11

#defineKEY_LEFT_GPIO 13

#defineKEY_RIGHT_GPIO 14

#defineKEY_A_GPIO 24 // 确认 / 交互

②shim/Arduino.cpp的初始化数组(顺序 = 上/下/左/右/确认):

static const uint8_t KEY_PINS[] = { 10, 11, 13, 14, 24 };

血泪教训:这两处必须一起改。只改数组不改宏,会出现"上电直接自动进游戏 + 按键没反应"的诡异现象——因为游戏逻辑查的是KEY_A_GPIO宏指向的脚,而底层初始化的是KEY_PINS[]数组里的脚,两套引脚对不上,悬空脚读到低电平就把菜单自动确认了。

6.2 换屏幕引脚 ——st7789.h顶部

#defineST7789_CS_PIN GPIO_PIN_12

#defineST7789_CLK_PIN GPIO_PIN_33

#defineST7789_MOSI_PIN GPIO_PIN_19

#defineST7789_RES_PIN GPIO_PIN_26

#defineST7789_DC_PIN GPIO_PIN_27

#defineST7789_BLK_PIN GPIO_PIN_28

#defineST7789_SPI_FREQ (160 * 1000 * 1000)

换分辨率或接非居中屏幕时,找shim/TFT_eSPI.cpp的pushSprite(),里面(ST7789_WIDTH - _w) / 2就是水平居中偏移。

07小结

· 一块Ai-M61-32S-Kit+ 一块ST7789 屏+ 5 个轻触按键,不接任何 ADC/PWM,纯 GPIO + SPI 就能跑起一个带图形菜单、卡牌选择、满屏伤害数字的肉鸽生存游戏;

· 工程上在博流 SDK 上从零搭了一套 C++ 工具链(4 步必要修复:项目启用 CXX / 手动__libc_init_array/__dso_handle/ 头文件extern "C"),用 SPI0 + DMA 双缓冲实现非阻塞送帧,把整帧 ~30ms 的瓶颈压到 ~15ms;

· 代码按shim / game / 驱动三层切分,shim/整个目录是"Arduino on BL616"兼容层,下次移植别的 TFT_eSPI 项目可以直接复用。

想要更多玩法(手动重击、真·暂停菜单、音效),评论区见,代码改动都集中在app.cpp的状态机里,随时可以加。

08下载

游戏帖子源码.zip