最近在极客圈掀起了一股用微控制器复刻复古游戏机的热潮。作为性价比极高的开发板,Raspberry Pi Pico凭借其双核处理器和丰富的外设接口,成为DIY游戏机的理想选择。本文将手把手教你如何用最常见的硬件配置——Raspberry Pi Pico搭配国产ST7789 SPI屏幕,从零开始构建一个能流畅运行FC游戏的便携设备。
不同于市面上现成的开发套件,我们的方案更注重硬件适配的灵活性和代码修改的实操性。很多爱好者在使用开源项目时,常遇到硬件不匹配导致的显示异常、按键失灵等问题。本文将特别针对这些痛点,提供详细的解决方案。
在开始项目前,请确保你已准备好以下硬件:
提示:国产ST7789屏幕通常价格在30-50元之间,购买时注意确认是否支持SPI接口。部分屏幕可能标注为"ST7789V"或"ST7789VW"。
将各组件按以下方式连接:
| Pico引脚 | ST7789引脚 | 功能说明 |
|---|---|---|
| GP0 | SCL | SPI时钟信号 |
| GP1 | SDA | SPI数据信号 |
| GP2 | RES | 屏幕复位信号 |
| GP3 | DC | 数据/命令选择 |
| GP4 | CS | 片选信号 |
| GP5 | BLK | 背光控制(可选) |
| 3V3 | VCC | 电源正极 |
| GND | GND | 电源地 |
按键部分连接:
code复制UP -> GP6
DOWN -> GP7
LEFT -> GP8
RIGHT -> GP9
A/B -> GP10/GP11
bash复制git clone https://github.com/fhoedemakers/PicoSystem_InfoNes
原项目代码假设屏幕带有VSYNC(垂直同步)信号引脚,但大多数国产ST7789屏幕并未引出此功能。我们需要修改硬件抽象层代码:
hardware.cpp文件cpp复制// 等待VSYNC信号
while(gpio_get(8) == 1);
while(gpio_get(8) == 0);
cpp复制// while(gpio_get(8) == 1);
// while(gpio_get(8) == 0);
注意:移除VSYNC同步后,理论上可能出现画面撕裂,但在FC游戏这种帧率不高的应用中几乎不可察觉。
国产ST7789屏幕常存在颜色反转问题,表现为显示色彩异常。修改方法:
hardware.cpp文件中,找到约384行处的显示初始化代码cpp复制// write_cmd(0x21); // 关闭颜色反转
write_cmd(0x20); // 部分屏幕需要使用此命令
不同屏幕可能需要尝试以下组合:
0x200x210x36+参数原项目的按键检测逻辑较为复杂,我们可以简化为直接读取GPIO状态:
打开hardware.hpp,修改按键引脚定义:
cpp复制#define PIN_UP 6
#define PIN_DOWN 7
#define PIN_LEFT 8
#define PIN_RIGHT 9
#define PIN_A 10
#define PIN_B 11
在hardware.cpp中重写按键检测函数:
cpp复制uint8_t get_input() {
uint8_t ret = 0;
if(!gpio_get(PIN_UP)) ret |= UP_MASK;
if(!gpio_get(PIN_DOWN)) ret |= DOWN_MASK;
if(!gpio_get(PIN_LEFT)) ret |= LEFT_MASK;
if(!gpio_get(PIN_RIGHT)) ret |= RIGHT_MASK;
if(!gpio_get(PIN_A)) ret |= A_MASK;
if(!gpio_get(PIN_B)) ret |= B_MASK;
return ret;
}
默认项目可能只包含演示游戏,我们需要添加从SD卡加载ROM的功能:
在Pico上连接SPI接口的Micro SD卡模块
添加以下库到项目中:
pico-sdk/lib/tinyusb/src/class/mscFatFs文件系统库实现ROM加载函数示例:
cpp复制void load_rom(const char* filename) {
FIL file;
if(f_open(&file, filename, FA_READ) == FR_OK) {
UINT bytes_read;
f_read(&file, rom_buffer, ROM_SIZE, &bytes_read);
f_close(&file);
}
}
创建一个基于文本的游戏选择菜单:
cpp复制void show_menu() {
clear_screen();
draw_text("FC Game Console", 40, 20, WHITE);
DIR dir;
FILINFO fno;
if(f_opendir(&dir, "/") == FR_OK) {
int y = 50;
while(f_readdir(&dir, &fno) == FR_OK && fno.fname[0]) {
if(strstr(fno.fname, ".nes")) {
draw_text(fno.fname, 30, y, GREEN);
y += 20;
}
}
f_closedir(&dir);
}
}
超频设置:
cpp复制// 在main.cpp中添加
set_sys_clock_khz(250000, true); // 超频至250MHz
双核利用:
内存优化:
cpp复制// 修改CMakeLists.txt增加内存分配
target_link_libraries(your_project pico_stdlib hardware_spi hardware_dma)
检查背光是否开启:
cpp复制gpio_init(PIN_BLK);
gpio_set_dir(PIN_BLK, GPIO_OUT);
gpio_put(PIN_BLK, 1); // 开启背光
验证SPI通信:
python复制# 使用MicroPython快速测试
from machine import Pin, SPI
spi = SPI(0, baudrate=40000000, polarity=1, phase=1)
检查复位时序:
cpp复制gpio_put(PIN_RES, 0);
sleep_ms(100);
gpio_put(PIN_RES, 1);
sleep_ms(100);
添加软件去抖:
cpp复制bool debounced_read(uint pin) {
if(!gpio_get(pin)) {
sleep_ms(20); // 去抖延时
return !gpio_get(pin);
}
return false;
}
调整内部上拉电阻:
cpp复制gpio_pull_up(PIN_UP);
gpio_pull_up(PIN_DOWN);
// ...其他按键同理
降低显示刷新率:
cpp复制#define FRAME_RATE 30 // 从60调整为30
简化画面渲染:
cpp复制// 在nes_emu.c中
void render_frame() {
// 跳过不必要的背景层渲染
if(skip_background) return;
}
使用DMA加速SPI传输:
cpp复制spi_init(spi0, 62500000); // 提高SPI时钟
dma_channel_configure(dma_chan, &c, ...);
测量元件尺寸:
按键布局建议:
散热考虑:
选用3.7V锂电池(500-1000mAh)
添加TP4056充电模块
电压转换电路:
code复制锂电池 -> TP4056 -> Pico VSYS
|
Micro USB(充电)
低功耗优化代码:
cpp复制// 在无操作时进入睡眠
if(last_input == 0 && sleep_timer++ > 300) {
sleep_ms(1000);
sleep_timer = 0;
}
硬件连接:
简单音频实现:
cpp复制void play_sound(int freq) {
gpio_set_function(PIN_AUDIO, GPIO_FUNC_PWM);
pwm_set_wrap(pwm_gpio_to_slice_num(PIN_AUDIO), 125000/freq);
pwm_set_chan_level(pwm_gpio_to_slice_num(PIN_AUDIO),
pwm_gpio_to_channel(PIN_AUDIO), 50);
}
完成所有修改后,重新编译项目并将生成的UF2文件拖入Pico即可体验自制的复古游戏机。这个项目不仅能让您重温经典FC游戏,更能深入理解嵌入式系统开发的全流程。