| 本文作者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效果


[实战画面: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不能是宏(会破坏
· 给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
