基于立创梁山派GD32F4高性能开发板制作自己的游戏机,这游戏机移植NES模拟器,目前一般NES游戏都可以玩,目前有上百款游戏下载。在这项目当中,我们可以很好地学习LCD屏的移植,SD卡的文件系统的使用,简单UI界面制作,音乐输出的功能,等等。在这项目当中我们可以回忆童年时光。
(图1 PCB正面)
(图2 PCB反面)
1、文件系统,Fats读写操作(SD卡与Flash 的文件系统)。
2、EXMC的LCD驱动(8080LCD接口)
3、移植NES的显示240*240改成480*480
4、液晶屏触摸控制及玩游戏
5、DAC音乐输出
6、文字库使用
7、SDRAM的内存管理
8、Bmp图片文件读取存储显示
1、电源管理部分:
(图3)TYPE-C 充电接口,二极管可以防反灌输入。
(图4)电源管理模块
图4是充电与升压管理部分。U10为充电IC,充电电流最大1A,U9为升压IC(ME2159)一款比较优秀的锂电池升压IC,2.5V-4.2V宽电压转换效率可以达到90%,
如下图5(官方手册测试参数):
(图5)ME2159 性能图
图4中CHG1、CHG2为充电IC的正在充电、充电完成状态信号。R27电阻是插入充电电源后停止升压,程序要拨充电电源后延时重启否则白屏。右侧R16,R19电池电压采样电流,计算电池电压公式:
faValue = adcValue*(3.368/4096)*fadcoffset;
A. favalue:电池电压
B. adcValue:ADC读取的值
C. fadcoffset:矫正值
2、语音放大电路:
(图6)DAC输出信号放大电路
图6中改变R10与R8 的比例可以实现改变音频放大的倍数,可以让喇叭声音变大。
(图7)兼容两款遥杆的电路
实物展示:
图8
型号:RKJXV1220001
立创商城购买:RKJXV1220001_(ALPSALPINE(阿尔卑斯阿尔派))RKJXV1220001中文资料_价格_PDF手册-立创电子商城 (szlcsc.com)
图9
型号:无
淘宝购买:https://item.taobao.com/item.htm?spm=a1z09.2.0.0.38cf2e8dqWGxGP&id=642782754225&_u=jak0o1p43a3
图10、系统图
1、显示屏选用:购买的立创`梁山派-屏幕扩展板 10件套(详情请点击以下)
立创`梁山派-屏幕扩展板_(立创开发板)立创`梁山派-屏幕扩展板中文资料_价格_PDF手册-立创电子商城 (szlcsc.com)
2、梁山派开发板:立创·梁山派 GD32F470ZGT6开发板 五件套(详情请点击以下)
立创·梁山派 GD32F470ZGT6开发板_(立创开发板)立创·梁山派 GD32F470ZGT6开发板中文资料_价格_PDF手册-立创电子商城 (szlcsc.com)
3、组合后(接好后):
这是完整接好的样子。
这是开机后的样子。
这个是游戏界面
初次调试,EXMC的LCD驱动时发现SDRAM存储异常,运行游戏失败,LCD显示正常,如果屏蔽LCD驱动,SDRAM就正常运行游戏,因为游戏文件要加载到SDRAM上运行。问题就是SDRAM和TFT-LCD共用就程序异常跑飞,最终解决问题,优化了SDRAM的时序延时。
代码如下:
ErrStatus exmc_synchronous_dynamic_ram_init(uint32_t sdram_device)
{
exmc_sdram_parameter_struct sdram_init_struct;
exmc_sdram_timing_parameter_struct sdram_timing_init_struct;
exmc_sdram_command_parameter_struct sdram_command_init_struct;
uint32_t command_content = 0, bank_select;
uint32_t timeout = SDRAM_TIMEOUT;
/* enable EXMC clock*/
rcu_periph_clock_enable(RCU_EXMC);
rcu_periph_clock_enable(RCU_GPIOB);
rcu_periph_clock_enable(RCU_GPIOC);
rcu_periph_clock_enable(RCU_GPIOD);
rcu_periph_clock_enable(RCU_GPIOE);
rcu_periph_clock_enable(RCU_GPIOF);
rcu_periph_clock_enable(RCU_GPIOG);
rcu_periph_clock_enable(RCU_GPIOH);
/* common GPIO configuration */
/* SDNWE(PC0),SDNE0(PC2),SDCKE0(PC3) pin configuration */
gpio_af_set(GPIOC, GPIO_AF_12, GPIO_PIN_0 | GPIO_PIN_2 | GPIO_PIN_3);
gpio_mode_set(GPIOC, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_0 | GPIO_PIN_2 | GPIO_PIN_3);
gpio_output_options_set(GPIOC, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_0 | GPIO_PIN_2 | GPIO_PIN_3);
/* D2(PD0),D3(PD1),D13(PD8),D14(PD9),D15(PD10),D0(PD14),D1(PD15) pin configuration */
gpio_af_set(GPIOD, GPIO_AF_12, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_8 | GPIO_PIN_9 |
GPIO_PIN_10 | GPIO_PIN_14 | GPIO_PIN_15);
gpio_mode_set(GPIOD, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_8 | GPIO_PIN_9 |
GPIO_PIN_10 | GPIO_PIN_14 | GPIO_PIN_15);
gpio_output_options_set(GPIOD, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_8 | GPIO_PIN_9 |
GPIO_PIN_10 | GPIO_PIN_14 | GPIO_PIN_15);
/* NBL0(PE0),NBL1(PE1),D4(PE7),D5(PE8),D6(PE9),D7(PE10),D8(PE11),D9(PE12),D10(PE13),D11(PE14),D12(PE15) pin configuration */
gpio_af_set(GPIOE, GPIO_AF_12, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_7 | GPIO_PIN_8 |
GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 |
GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15);
gpio_mode_set(GPIOE, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_7 | GPIO_PIN_8 |
GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 |
GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15);
gpio_output_options_set(GPIOE, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_7 | GPIO_PIN_8 |
GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 |
GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15);
/* A0(PF0),A1(PF1),A2(PF2),A3(PF3),A4(PF4),A5(PF5),NRAS(PF11),A6(PF12),A7(PF13),A8(PF14),A9(PF15) pin configuration */
gpio_af_set(GPIOF, GPIO_AF_12, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 |
GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_11 | GPIO_PIN_12 |
GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15);
gpio_mode_set(GPIOF, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 |
GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_11 | GPIO_PIN_12 |
GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15);
gpio_output_options_set(GPIOF, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 |
GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_11 | GPIO_PIN_12 |
GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15);
/* A10(PG0),A11(PG1),A12(PG2),A14(PG4),A15(PG5),SDCLK(PG8),NCAS(PG15) pin configuration */
gpio_af_set(GPIOG, GPIO_AF_12, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_4 |
GPIO_PIN_5 | GPIO_PIN_8 | GPIO_PIN_15);
gpio_mode_set(GPIOG, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_4 |
GPIO_PIN_5 | GPIO_PIN_8 | GPIO_PIN_15);
gpio_output_options_set(GPIOG, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_4 |
GPIO_PIN_5 | GPIO_PIN_8 | GPIO_PIN_15);
/* specify which SDRAM to read and write */
if(EXMC_SDRAM_DEVICE0 == sdram_device){
bank_select = EXMC_SDRAM_DEVICE0_SELECT;
}else{
bank_select = EXMC_SDRAM_DEVICE1_SELECT;
}
/* EXMC SDRAM device initialization sequence --------------------------------*/
/* Step 1 : configure SDRAM timing registers --------------------------------*/
/* LMRD: 2 clock cycles */
sdram_timing_init_struct.load_mode_register_delay = 2;
/* XSRD: min = 75ns */
sdram_timing_init_struct.exit_selfrefresh_delay = 9;
/* RASD: min=44ns , max=120k (ns) */
sdram_timing_init_struct.row_address_select_delay = 5;
/* ARFD: min=66ns */
sdram_timing_init_struct.auto_refresh_delay = 8;
/* WRD: min=1 Clock cycles +7.5ns */
sdram_timing_init_struct.write_recovery_delay = 3;
/* RPD: min=20ns */
sdram_timing_init_struct.row_precharge_delay = 3;
/* RCD: min=20ns */
sdram_timing_init_struct.row_to_column_delay = 3;
/* step 2 : configure SDRAM control registers ---------------------------------*/
sdram_init_struct.sdram_device = sdram_device;
sdram_init_struct.column_address_width = EXMC_SDRAM_COW_ADDRESS_9;
sdram_init_struct.row_address_width = EXMC_SDRAM_ROW_ADDRESS_13;
sdram_init_struct.data_width = EXMC_SDRAM_DATABUS_WIDTH_16B;
sdram_init_struct.internal_bank_number = EXMC_SDRAM_4_INTER_BANK;
sdram_init_struct.cas_latency = EXMC_CAS_LATENCY_3_SDCLK;
sdram_init_struct.write_protection = DISABLE;
sdram_init_struct.sdclock_config = EXMC_SDCLK_PERIODS_2_HCLK;
sdram_init_struct.brust_read_switch = ENABLE;
sdram_init_struct.pipeline_read_delay = EXMC_PIPELINE_DELAY_2_HCLK;
sdram_init_struct.timing = &sdram_timing_init_struct;
/* EXMC SDRAM bank initialization */
exmc_sdram_init(&sdram_init_struct);
/* step 3 : configure CKE high command---------------------------------------*/
sdram_command_init_struct.command = EXMC_SDRAM_CLOCK_ENABLE;
sdram_command_init_struct.bank_select = bank_select;
sdram_command_init_struct.auto_refresh_number = EXMC_SDRAM_AUTO_REFLESH_2_SDCLK;
sdram_command_init_struct.mode_register_content = 0;
/* wait until the SDRAM controller is ready */
while((exmc_flag_get(sdram_device, EXMC_SDRAM_FLAG_NREADY) != RESET) && (timeout > 0)){
timeout--;
}
if(0 == timeout){
return ERROR;
}
/* send the command */
exmc_sdram_command_config(&sdram_command_init_struct);
/* step 4 : insert 10ms delay----------------------------------------------*/
delay_1ms(10);
/* step 5 : configure precharge all command----------------------------------*/
sdram_command_init_struct.command = EXMC_SDRAM_PRECHARGE_ALL;
sdram_command_init_struct.bank_select = bank_select;
sdram_command_init_struct.auto_refresh_number = EXMC_SDRAM_AUTO_REFLESH_1_SDCLK;
sdram_command_init_struct.mode_register_content = 0;
/* wait until the SDRAM controller is ready */
timeout = SDRAM_TIMEOUT;
while((exmc_flag_get(sdram_device, EXMC_SDRAM_FLAG_NREADY) != RESET) && (timeout > 0)){
timeout--;
}
if(0 == timeout){
return ERROR;
}
/* send the command */
exmc_sdram_command_config(&sdram_command_init_struct);
/* step 6 : configure Auto-Refresh command-----------------------------------*/
sdram_command_init_struct.command = EXMC_SDRAM_AUTO_REFRESH;
sdram_command_init_struct.bank_select = bank_select;
sdram_command_init_struct.auto_refresh_number = EXMC_SDRAM_AUTO_REFLESH_8_SDCLK;
sdram_command_init_struct.mode_register_content = 0;
/* wait until the SDRAM controller is ready */
timeout = SDRAM_TIMEOUT;
while((exmc_flag_get(sdram_device, EXMC_SDRAM_FLAG_NREADY) != RESET) && (timeout > 0)){
timeout--;
}
if(0 == timeout){
return ERROR;
}
/* send the command */
exmc_sdram_command_config(&sdram_command_init_struct);
/* step 7 : configure load mode register command-----------------------------*/
/* program mode register */
command_content = (uint32_t)SDRAM_MODEREG_BURST_LENGTH_1 |
SDRAM_MODEREG_BURST_TYPE_SEQUENTIAL |
SDRAM_MODEREG_CAS_LATENCY_3 |
SDRAM_MODEREG_OPERATING_MODE_STANDARD |
SDRAM_MODEREG_WRITEBURST_MODE_SINGLE;
sdram_command_init_struct.command = EXMC_SDRAM_LOAD_MODE_REGISTER;
sdram_command_init_struct.bank_select = bank_select;
sdram_command_init_struct.auto_refresh_number = EXMC_SDRAM_AUTO_REFLESH_2_SDCLK;
sdram_command_init_struct.mode_register_content = command_content;
/* wait until the SDRAM controller is ready */
timeout = SDRAM_TIMEOUT;
while((exmc_flag_get(sdram_device, EXMC_SDRAM_FLAG_NREADY) != RESET) && (timeout > 0)){
timeout--;
}
if(0 == timeout){
return ERROR;
}
/* send the command */
exmc_sdram_command_config(&sdram_command_init_struct);
/* step 8 : set the auto-refresh rate counter--------------------------------*/
/* 64ms, 8192-cycle refresh, 64ms/8192=7.81us */
/* SDCLK_Freq = SYS_Freq/2 */
/* (7.81 us * SDCLK_Freq) - 20 */
exmc_sdram_refresh_count_set(761);
/* wait until the SDRAM controller is ready */
timeout = SDRAM_TIMEOUT;
while((exmc_flag_get(sdram_device, EXMC_SDRAM_FLAG_NREADY) != RESET) && (timeout > 0)){
timeout--;
}
if(0 == timeout){
return ERROR;
}
return SUCCESS;
}
修改完代码后,程序正常运行游戏。
原代码是这样的:
源代码显示图片是直接使用SPI对显示屏写入数据,没有直接调用LCD驱动的方法,若是不留意就程序一开机会挂在这里
8080显示驱动 修改为:
字库更新:(需要的字库文件:UNIGBK.BIN,GBK12.BIN,GBK16.BIN,GBK24.BIN)我在正点原子找了字库文件,拿来用,发现出现以下的显示问题:
找到问题所在:text.c ---> Show_Font 函数如下:
//************************************************************************
//***************************************************************************
把函数里的X和Y改改就可以了,改过如下:
//**********************************************************
//**************************************************
其次游戏的显示驱动:240*240分辨率改成480*480分辨率,不需要增加显存的情况下:
nes_ppu.c ----> void scanline_draw(int LineNo)函数
//***********************************************************************************
extern uint8_t nes_xoff; //显示在x轴方向的偏移量(实际显示宽度=256-2*nes_xoff)
void scanline_draw(int LineNo)
{
uint16 i;
uint16_t sx,ex;
do_scanline_and_draw(ppu->dummy_buffer);
#if 0
sx=nes_xoff+8;
ex=256+8-nes_xoff;
//printf("LineNo:%d sx:%d ex:%d ",LineNo,sx,ex);
LCD_Address_Set(LineNo,sx,LineNo,ex);//设置显示范围 横着屏幕
for(i=sx;i<ex;i++){
//LCD_DrawPoint(LineNo,i,NES_Palette[ppu->dummy_buffer[i]]);
//printf("i:%d color:%d ",i,NES_Palette[ppu->dummy_buffer[i]]);
LCD_WR_DATA(NES_Palette[ppu->dummy_buffer[i]]);
}
#else
sx = 17;
ex = 240+17;
LCD_Address_Set(0,(LineNo*2)+40,480,(LineNo*2)+40);//设置显示范围 横着屏显示
for(i=sx;i<ex;i++){
LCD_WR_DATAs(NES_Palette[ppu->dummy_buffer[i]]);
LCD_WR_DATAs(NES_Palette[ppu->dummy_buffer[i]]);
}
LCD_Address_Set(0,(LineNo*2+1)+40,480,(LineNo*2+1)+40);//设置显示范围 横着屏显示
//LCD_DC_Set();//写数据
// LCD_CS_Clr();
for(i=sx;i<ex;i++){
LCD_WR_DATAs(NES_Palette[ppu->dummy_buffer[i]]);
LCD_WR_DATAs(NES_Palette[ppu->dummy_buffer[i]]);
}
}
函数里红色部分:大概原理就是x坐标数据写入两次,Y坐标的一行写两行,等于 一个点放大加1倍,240*240的内存显示放大成 480*480,节省RAM显存。
游戏显示部分好了.
bmp的函数代码复制于教材的源码改的,
重要文件:file_opera.c 、 bmp_opera.c
修改了屏蔽了这两个程序里大多数的LCD驱动代码,只留下bmp文件处理解析的内容。
fats读bmp文件,bmp文件信息的主意格式结构体(bmp_opera.h):
//BMP文件主要信息结构体
typedef struct
{
//1. 文件信息头
uint16 tbfType;//开头2个字节,文件类型,必须是0x4D42
uint32 tbfSize;//文件大小,字节为单位
uint32_tbfOffset; //数据起始偏移地址,单位:字节
//2. 位图信息头
uint32 tbiSize;//信息头字节数,一般就是40
uint32 tbiWidth;//BMP图宽度,像素
uint32 tbiHeight;//BMP图高度,像素
uint16 tbiBitCount;//每个像素的比特数
} BMPFileInfoDef;
bmp文件在SD卡内存储,文件信息数据排列顺序就根据这个结构图排列:
//调用以下函数读取BMP文件信息, 文件信息保存到指针BmpFileInfo指向的变量
uint8_t ReadBmpFileInfo(uint8_t *Filename, BMPFileInfoDef *BmpFileInfo)
{
FIL file;
FRESULT res=f_open(&file, (char *)Filename, FA_READ); //fats 读取bmp文件信息
if(res != FR_OK)
{
f_close(&file);
return 0; //失败返回0
}
ReadBmpHeader(&file, BmpFileInfo);//读取文件头信息
f_close(&file); //成功后关闭清除file句柄
return 1; //成功返回1
}
然后,可以用void ShowBmpFileInfo(const BMPFileInfoDef *BmpFileInfo) 把bmp文件的信息通过printf打印出来
再然后,bmp文件图片数据显示到LCD上,图片是24位RGB888, 一个像素3个字节,排列顺序BGR,
BMP图片从下开始往上逐行存储, 在电脑上对图片进行了上下翻转,所以直接顺序读取,
需要转换为RGB565表示的像素点数据,才能画到LCD上
边读边显示的函数:
void DrawBmp(const uint16_t PosX, const uint16_t PosY,
FIL *file, BMPFileInfoDef *fileInfo)
在LCD上绘制整个BMP图片,返回值为0表示绘图成功
uint8_t DrawBmpFile(const uint16_t PosX, const uint16_t PosY, uint8_t *Filename)
以上是Bmp图片直接显示LCD屏上,后面我自己改了个直接读数据保存在SDRAM里的
uint32_t BmpRead(uint16_t *buf ,FIL *file, BMPFileInfoDef *fileInfo)
//读BMP图片把数据写buffer里,返回值为0表示绘图成功
uint8_t ReadBmpFile(uint16_t *buf , uint8_t *Filename,BMPFileInfoDef *fileInfo)
后面调用程序,使用方法(NES_GUI.c里):
先分配图片内存:
然后读取bmp数据到RAM里:
这些函数把图片显示内容,读到RAM里,再显示
LCD_DrawBmp 因为LCD屏定了是自上往下显示的,bmp文件的内容是自下往上的,所以改了这个函数。
!注意:请使用浏览器自带下载,迅雷等下载软件可能无法下载到有效资源。
欢迎加入EEWorld参考设计群,也许能碰到搞同一个设计的小伙伴,群聊设计经验和难点。 入群方式:微信搜索“helloeeworld”或者扫描二维码,备注:参考设计,即可被拉入群。 另外,如您在下载此设计遇到问题,也可以微信添加“helloeeworld”及时沟通。
EEWorld Datasheet 技术支持