【游戏机扩展板】资料
游戏机扩展板资料下载
资料下载链接
一、SPI驱动屏幕显示图像文字
在这里,我们先了解一下嵌入式开发常用的显示屏幕种类,最后再介绍游戏机扩展板使用的显示屏类型,然后使用它显示画面。
1.常见的屏幕种类
显示屏是大多数嵌入式开发必备的外部器件,相比于其他数码管/LED等,它可以显示更加丰富的类容(文字,图片,线条等)。
常见的显示屏从色彩上分为单色和彩色屏幕。接口上又可以分为并口,RGB,SPI,IIC,MIPI等接口。屏幕材质也可以分为OLED屏,TFT屏,墨水屏。显示 大小上面可以分为0.96寸,2.4寸,7寸等大小的屏幕。还有像素区分为240320,320480,480*800等屏幕。
2.游戏扩展板使用的SPI屏幕
游戏机扩展板屏幕采用了SPI通讯的240*280像素的1.69寸IPS高清圆角屏幕。
该屏幕因为单位像素密度更高(总像素/尺寸),所以显示画质更精细,并且控制IO少,外围电路也相对简单。
3.屏幕的使用
一般来说,我们使用屏幕需要了解该屏幕的控制芯片以及该芯片的控制寄存器再进行相应的初始化后才能正常使用,但是,大多数的屏幕售卖厂商已经提供了完整的初始化代码,以及相应的简单显示字符串以及图形,图片的代码。所以我们大多数情况下只需要进行代码的移植即可。
3.1.屏幕的初始化
因为屏幕像素为240*280,且颜色数据为565(2字节),显示的数据量非常的庞大,单纯用IO模拟spi显示非常的慢,这里我们使用硬件SPI进行数据传输,以提高通信效率。
uint8_t Spi4_Read(void)
{
while (RESET == spi_i2s_flag_get(SPI4, SPI_FLAG_RBNE));
return spi_i2s_data_receive(SPI4);
while (RESET == spi_i2s_flag_get(SPI4, SPI_FLAG_RBNE));
}
void Spi4_Write(uint8_t dat)
{
while (RESET == spi_i2s_flag_get(SPI4, SPI_FLAG_TBE)) ;
spi_i2s_data_transmit(SPI4, dat);
while (RESET == spi_i2s_flag_get(SPI4, SPI_FLAG_TBE)) ;
}
#define Lcd_SpiRead Spi4_Read
#define Lcd_SpiWrite Spi4_Write
void LCD_Writ_Bus(uint8_t dat)
{
LCD_CS_L;
Lcd_SpiWrite(dat);
delay(4);
LCD_CS_H;
}
void Lcd_WriteData(uint8_t data)
{
LCD_Writ_Bus(data);
}
void Lcd_WriteData16(uint16_t data)
{
LCD_Writ_Bus(data >> 8);
LCD_Writ_Bus(data);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
3.2.字体的取模
在屏幕上面为像素点显示,如果想显示相应的字体文字,在没有使用字库芯片的情况下,只有进行软件取模为数组数据后,在进行相应的显示。不同的显示方式对应不同的取模设置。
const unsigned char ascii_1206[][12]={
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*" ",0*/
{0x00,0x00,0x04,0x04,0x04,0x04,0x04,0x00,0x00,0x04,0x00,0x00},/*"!",1*/
{0x14,0x14,0x0A,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*""",2*/
{0x00,0x00,0x0A,0x0A,0x1F,0x0A,0x0A,0x1F,0x0A,0x0A,0x00,0x00},/*"#",3*/
{0x00,0x04,0x0E,0x15,0x05,0x06,0x0C,0x14,0x15,0x0E,0x04,0x00},/*"$",4*/
{0x00,0x00,0x12,0x15,0x0D,0x15,0x2E,0x2C,0x2A,0x12,0x00,0x00},/*"%",5*/
{0x00,0x00,0x04,0x0A,0x0A,0x36,0x15,0x15,0x29,0x16,0x00,0x00},/*"&",6*/
{0x02,0x02,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*"'",7*/
{0x10,0x08,0x08,0x04,0x04,0x04,0x04,0x04,0x08,0x08,0x10,0x00},/*"(",8*/
{0x02,0x04,0x04,0x08,0x08,0x08,0x08,0x08,0x04,0x04,0x02,0x00},/*")",9*/
{0x00,0x00,0x00,0x04,0x15,0x0E,0x0E,0x15,0x04,0x00,0x00,0x00},/*"*",10*/
{0x00,0x00,0x00,0x08,0x08,0x3E,0x08,0x08,0x00,0x00,0x00,0x00},/*"+",11*/
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x02,0x01,0x00},/*",",12*/
}
typedef struct
{
unsigned char Index[2];
unsigned char Msk[32];
}typFNT_GB16;
const typFNT_GB16 tfont16[]={
"始",
0x08,0x04,0x08,0x04,0x08,0x04,0x08,0x02,
0x3F,0x12,0x24,0x21,0xA4,0x7F,0x24,0x41,
0x24,0x00,0x12,0x3F,0x14,0x21,0x08,0x21,
0x14,0x21,0x22,0x21,0x01,0x3F,0x00,0x21,/*"始",0*/
"开",
//0x00,0x00,0xFE,0x3F,0x10,0x04,0x10,0x04,
//0x10,0x04,0x10,0x04,0x10,0x04,0xFF,0x7F,
//0x10,0x04,0x10,0x04,0x10,0x04,0x10,0x04,
//0x08,0x04,0x08,0x04,0x04,0x04,0x02,0x04,/*"开",1*/
0x00,0x00,0xFE,0x3F,0x10,0x04,0x10,0x04,0x10,0x04,0x10,0x04,0x10,0x04,0xFF,0x7F,
0x10,0x04,0x10,0x04,0x10,0x04,0x10,0x04,0x08,0x04,0x08,0x04,0x04,0x04,0x02,0x04,/*"开",0*/
"游",
0x40,0x08,0x84,0x08,0x88,0x08,0xE8,0x7D,
0x41,0x04,0x42,0x02,0xC2,0x3D,0x48,0x21,
0x48,0x11,0x44,0x11,0x47,0x7D,0x44,0x11,
0x24,0x11,0x24,0x11,0x94,0x15,0x08,0x08,/*"游",2*/
"戏",
0x00,0x04,0x00,0x14,0x00,0x24,0x7E,0x24,
0x40,0x04,0x40,0x7C,0xA4,0x07,0x28,0x24,
0x10,0x24,0x10,0x14,0x28,0x14,0x48,0x08,
0x44,0x4C,0x02,0x52,0x00,0x61,0x80,0x40,/*"戏",3*/
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
英文字符的取模保存相对简单,英文字符为ASCLL码依次排列对应。而中文受编码方式影响,所以需要对数组的字符(汉字)进行检索判定。
3.3.图像的取模
屏幕的图像显示在没有文件系统的情况下,显示的图像同样需要先进行取模后显示,并且因为屏幕的单色/彩色不同,显示设置不同,取模的设置也有不同。
void LCD_ShowPicture(uint16_t x,uint16_t y,uint16_t length,uint16_t width,const uint8_t pic[])
{
uint16_t i,j;
uint32_t k=0;
Lcd_PushStart(x,y,x+length-1,y+width-1);
for(i=0;i<length;i++)
{
for(j=0;j<width;j++)
{
Lcd_WriteData(pic[k*2]);
Lcd_WriteData(pic[k*2+1]);
k++;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
4.功能演示
本功能中,初始化屏幕后,依次显示图片和不同大小字体的文字。
int main(void)
{
nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); // 优先级分组
/* 核心板 */
systick_config(); // 滴答定时器初始化
led_gpio_config(); // led初始化
key_gpio_config(); // key初始化
usart_gpio_config(115200U); // 串口0初始化
/* 扩展板 */
Lcd_Init(); // spi lcd初始化
/* SPI LCD */
LCD_Fill(0,0,LCD_W,LCD_H,COLOR_BLUE); // 深蓝色 背景
LCD_ShowPicture((LCD_W-222) / 2,0,222,60,gImage_lCKFB); // 显示图片
LCD_DrawRectangle(10,60 + 10,LCD_W -10,LCD_H -10,COLOR_BLACK); // 绘制一个矩形
LCD_ShowChinese(15,60 + 10 + 5,"开始游戏",COLOR_WHITE,COLOR_BLUE,12,0); // 显示12汉字字符串
LCD_ShowChinese(15,60 + 10 + 5 + 12,"开始游戏",COLOR_WHITE,COLOR_BLUE,16,0); // 显示16汉字字符串
LCD_ShowChinese(15,60 + 10 + 5 + 12 + 16,"开始游戏",COLOR_WHITE,COLOR_BLUE,24,0); // 显示24汉字字符串
LCD_ShowChinese(15,60 + 10 + 5 + 12 + 16 + 24 ,"开始游戏",COLOR_WHITE,COLOR_BLUE,32,0); // 显示32汉字字符串
LCD_ShowString(15,60 + 10 + 5 + 12 + 16 + 24 + 32 ,"123abc",COLOR_WHITE,COLOR_BLUE,16,0); // 显示16字符串
LCD_ShowString(15,60 + 10 + 5 + 12 + 16 + 24 + 32 +16,"123abc",COLOR_WHITE,COLOR_BLUE,24,0); // 显示24字符串
LCD_ShowString(15,60 + 10 + 5 + 12 + 16 + 24 + 32 + 16 + 24,"123abc",COLOR_WHITE,COLOR_BLUE,32,0); // 显示32字符串
//LCD_ShowPicture(0,0,240,280,gImage_aa);
while(1);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
5.示例代码工程
👉文件下载
二、DAC外设播放音频的实现
1.数字化声音的记录以及播放的大致原理
在这里,我们先了解一下数字化声音记录以及播放的原理,最后再结合单片机的外设来实现相应的功能。
1.1. 声音的采集
麦克风是一种能检测声音的元件,根据声音的强弱产生不同强弱的电信号,再经过运放的放大处理,最后通过固定周期模拟信号数字采集(ADC)处理,将声音处理成了原始的音频数字信号。
模拟信号数字采集器(ADC)的常见量化(bit)单位分为8bit,16bit,24bit。一般来说比特越高声音细节保留越完整。
信号的采集周期常见的分为:8000,11025,22500,44100。一般来说采集周期越高音频细节保留越完整,但是考虑到人耳的听觉范围为20-2000Hz,过高的采样周期并没有太多意义。
一般声音的采集还分为单声道和多声道,就是一个ADC或者多个ADC的区别了。
1.2. 声音的存储
原始的数字音频信号的保存常用的有原始信号保存和压缩信号保存。 原始音频信号常见的保存格式为WAV。压缩音频常见的各位MP3。
其实音频的播放最终还是使用的原始的音频(ADC)数据,所以我们主要了解wav格式存储就行。在这里我们不使用wav音乐文件播放,而是将wav转换成我们可以使用的音乐数据(数组)进行DAC播放就行。
1.3. 声音的播放
数字音乐的播放无论是有无压缩,最终都是使用的原始的模拟信号数字采集器(ADC)采集得到的数据通过数字信号模拟输出(DAC)输出,在经过运放(功放)电路驱动喇叭形成的声音。
数字功放即为集成了DAC和功放一体的电路模块。
因此,使用单片机可以实现完整的音频记录以及播放,在游戏机扩展项目中,我们主要学习的为音频的播放。
2.单片机DAC外设及音频电路
前面提到,音频的播放,需要数字信号模拟输出(DAC)以及喇叭驱动功放电路,所以我们也需要单片机的DAC外设以及外围的功放电路。
2.1.单片机DAC外设
梁山派开发板主控GD32F450芯片带有DAC外设,支持8/12位的数据转换。相应初始化后,将数据传输给DAC,相应DAC输出IO上面就会形成不同的电压值。
2.2.功放电路
本电路采用了一个8002a音频功放芯片,并且经过一个220nF的电容耦合DAC输出的声音信号,输出驱动喇叭发出声音。
这里我们只是用单声道音频播放。
3.单片机所需音频的转换
单片机的DAC只支持8/12位的数据输出。因为该寄存器仍为16位,也可以使用16为原始音频数据进行输出(DAC数据左对齐,只取16位数据的高12位),但是这样会加大音频文件的存储空间。16位(2字节)比8位(1字节)的音频数据量多了一倍。 所以我们这里采用8位采样的音频数据,所以我们需要将电脑上面的音频文件转换一下。
3.1.使用WAV音乐文件
因为WAV的存储为原始音频数据,故我们能只能处理WAV文件,其他音频文件需要格式转换为wav格式才能使用。
3.2.使用XP系统的录音机修改音频
sndrec32.exe 为xp系统自带的录音机软件,也可以在win7/win10上面使用,是笔者找到的现有的可以将音频转换为8bit的软件。
使用该录音机打开需要的音频文件,另存为8bit + 采样率 + 单声道的WAV音频即可。
3.3.音频文件转换成单片机数组
存储在电脑上的文件是基于文件系统的文件数据,在单片机上面想直接使用就必须移植文件系统,但是在我们的游戏机扩展项目中,我们只需要使用基本的音频数据即可,所以需要将音频文件转换成可以直接使用的数组即可。
c2b转换助手.exe 这里我们使用这个正点原子开发的转换软件,将音频WAV格式手动修改为bin格式,最后在用这个软件生产对应的数组即可,但是这样还不能直接使用。
前面提到过,wav文件格式的音频数据会有特定的文件存储方式,这里我们需要将数组的前面部分删除(文件信息头)即可(单声道datak开始音频数据)。
4.DAC+定时器实现音频播放
原始数据处理好了之后,只需要将原始音频数据发送给数字信号模拟输出(DAC)即可,DAC生成的电压信号经过功放就可以驱动喇叭发出声音了。
但是我们前面有提到过,声音的采集时有固定的周期的,所以播放的时候也必须严格按照该周期将数据输出到DAC,否则声音输出时会因为周期的不同而变音。
dac_audio_init(22000); // dac音频初始化
void dac_audio_init(uint16_t samplingRate)
{
dac_audio_gpio_init(); // gpio初始化
Timer3_config(samplingRate); // 配置音频采样率
}
2
3
4
5
6
音频数据的播放使用的定时器中断内输出。这里需要注意的是音频数据播放完了是否循环。
void TIMER3_IRQHandler(void)
{
if (timer_interrupt_flag_get(TIMER3, TIMER_INT_FLAG_UP))
{
/* clear TIMER interrupt flag */
timer_interrupt_flag_clear(TIMER3, TIMER_INT_FLAG_UP);
dac_data_set(DAC1, DAC_ALIGN_8B_R, currentAudio.audioCurrentPlayP[currentAudio.audioIndex]); // 8位数据右对齐
dac_software_trigger_enable(DAC1); // 软件触发使能 输出数据
currentAudio.audioIndex ++; // 等待下一次数据
if(currentAudio.audioLength == currentAudio.audioIndex){
/* 判断,播放模式 */
if(currentAudio.audio_play_mode == 1) // 循环播放
{
currentAudio.audioIndex = 0; // 从头开始
}else // 单次播放
{
/* 播放完毕 停止播放 */
timer_disable(TIMER3); // 关闭定时器
audio_status = 2; // 显示停止
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
5.功能演示
本功能中,将音频涉及的DAC和定时器初始化后,在主函数中按下按键KEY-B后(中断处理)播放音频,再次按下停止播放音频。之前有学习SPI屏幕,所以这里进行了一个屏幕显示。
int main(void)
{
uint8_t keyTimes = 0; // 开机次数
uint16_t temp; // 临时变量
nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); // 优先级分组
/* 核心板 */
systick_config(); // 滴答定时器初始化
led_gpio_config(); // led初始化
key_gpio_config(); // key初始化
usart_gpio_config(115200U); // 串口0初始化
/* 扩展板 */
adc_config(); // 摇杆初始化
Lcd_Init(); // spi lcd初始化
dac_audio_init(22000); // dac音频初始化
/* SPI LCD */
LCD_Fill(0,0,LCD_W,LCD_H,COLOR_BLUE); // 深蓝色 背景
LCD_ShowPicture((LCD_W-222) / 2,0,222,60,gImage_lCKFB); // 显示图片
LCD_DrawRectangle(10,60 + 10,LCD_W -10,LCD_H -10,COLOR_BLACK); // 绘制一个矩形
/* 背景音乐:*/
temp = 75;
LCD_ShowChinese(15,temp,"背景音乐:",COLOR_WHITE,COLOR_BLUE,16,0); // 显示16汉字字符串
Draw_Solid_Circle(15 + 100,temp + 8,8,COLOR_WHITE); // 显示实心圆
while(1)
{
/* 音频 */
if(audio_status == 1) // 开启音乐
{
audio_status = 3;
Draw_Solid_Circle(15 + 100,temp + 8,8,COLOR_GREEN); // 显示实心圆
start_play_audio(audioSouce_Background,sizeof(audioSouce_Background),1); // 播放音乐 单次播放
}else if(audio_status == 2) // 震动马达状态关闭
{
audio_status = 0;
Draw_Solid_Circle(15 + 100,temp + 8,8,COLOR_WHITE); // 显示实心圆
stop_play_audio();
}
delay_1ms(20);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
6.示例代码工程
👉文件下载
三、ADC读取摇杆数据
在这里,我们先了解一下常见的摇杆种类及其工作原理,最后再使用梁山派MCU的ADC来进行摇杆变化值得读取。
1. 常见的摇杆种类
摇杆是一种可以左右上下移动的控制杆元件,常出现在遥控器,游戏机等设备上面,其内部主要元件为两个滑动变阻器,根据旋转/位移的不同滑动变阻器的位置相对变化,阻值也相应变化,通过ADC即可采集到的当前位置的分压值,进行估算旋转/位移的控制量大小了。
上图均为市面常见的滑动变阻器摇杆,其价格低廉普及度更高,但是滑动变阻器存在使用寿命等限制。还有一种霍尔摇杆,采用的无接触式霍尔或者磁编码器作为角度检测,有兴趣的自行了解。
2.游戏扩展板使用的摇杆
游戏机扩展板整体尺寸相对小巧,故采用了小巧的滑动摇杆。
3.摇杆的使用
在使用摇杆功能时,需要对ADC进行初始化,然后开始ADC检测摇杆输出变量就行。
void adc_config(void)
{
/* enable ADC0 clock */
rcu_periph_clock_enable(RCU_ADC0);
/* config ADC clock */
adc_clock_config(ADC_ADCCK_PCLK2_DIV8);
/* reset ADC */
adc_deinit();
/* configure the ADC mode */
adc_sync_mode_config(ADC_SYNC_MODE_INDEPENDENT); // 所有ADC都工作在独立模式
/* ADC contineous function disable */
adc_special_function_config(ADC0, ADC_CONTINUOUS_MODE, DISABLE); // 关闭连续模式
/* ADC scan mode disable */
adc_special_function_config(ADC0, ADC_SCAN_MODE, DISABLE); // 关闭扫描模式
/* ADC data alignment config */
adc_data_alignment_config(ADC0,ADC_DATAALIGN_RIGHT); // LSB对齐,低位对齐
/* ADC channel length config */
adc_channel_length_config(ADC0,ADC_REGULAR_CHANNEL,1U); // ADC规则通道 长度为1
/* enable ADC interface */
adc_enable(ADC0);
/* wait for ADC stability */
delay_1ms(1);
/* ADC calibration and reset calibration */
adc_calibration_enable(ADC0); // ADC校准
/* wait for ADC stability */
delay_1ms(1);
/* adc 引脚初始化 */
adc_gpio_init();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
4.功能演示
本功能中,初始化屏幕后,控制摇杆移动,屏幕上面的滚动条会相应变化且显示当前的ADC值。
int main(void)
{
uint8_t keyTimes = 0; // 开机次数
uint16_t temp; // 临时变量
nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); // 优先级分组
/* 核心板 */
systick_config(); // 滴答定时器初始化
led_gpio_config(); // led初始化
key_gpio_config(); // key初始化
usart_gpio_config(115200U); // 串口0初始化
/* 扩展板 */
adc_config(); // 摇杆初始化
Lcd_Init(); // spi lcd初始化
/* SPI LCD */
LCD_Fill(0,0,LCD_W,LCD_H,COLOR_BLUE); // 深蓝色 背景
LCD_ShowPicture((LCD_W-222) / 2,0,222,60,gImage_lCKFB); // 显示图片
/* 先来获取一下遥感的位置 */
adc_key_scan();
/* x轴 进度条的长度最大为220 - 50 = 170, 然后值是从0-4095 共4096 平均一个像素点是24 */
temp = 75;
LCD_ShowString(15,temp,"X:",COLOR_WHITE,COLOR_BLUE,24,0); // 显示16字符串
RockerProgressBar(50,temp,adcXValue);
/* y轴 */
temp = temp + 32 + 5;
LCD_ShowString(15,temp,"Y:",COLOR_WHITE,COLOR_BLUE,24,0); // 显示16字符串
RockerProgressBar(50,temp,adcYValue);
while(1)
{
/* 摇杆 */
adc_key_scan(); // 摇杆扫描
RockerProgressBar(50,75,adcXValue); // x轴
RockerProgressBar(50,75 + 16 + 16 + 5,adcYValue); // y轴
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
5.示例代码工程
👉文件下载
四、震动电机的控制
在这里,我们先了解一下常见的震动电机种类及其工作原理,最后再使用梁山派MCU的来控制震动的强弱控制。
1. 常见的震动电机种类
震动电机是一种产生震动的元件,常见的是通过旋转偏心配重产生震动感,一般震动电机大功率就大,震动电机小功率也会相对小一下。通常使用于按摩椅,震动抛光机,游戏机手柄,手机内部。
2.游戏扩展板使用的震动电机
游戏机扩展板配置了一个贴片的震动电机,该震动电机电流较小(85mA),可以通过一个三极管来控制震动电机。
3.震动电机的使用
通过控制电路可以发现,我们可以用IO控制震动开关,也可以用PWM控制震动强弱。这里我们使用PWM控制。
static void pwm_gpio_config(void)
{
/* 使能时钟 */
rcu_periph_clock_enable(BSP_PWM_RCU);
/* 配置GPIO的模式 */
gpio_mode_set(BSP_PWM_PORT,GPIO_MODE_AF,GPIO_PUPD_NONE,BSP_PWM_PIN);
/* 配置GPIO的输出 */
gpio_output_options_set(BSP_PWM_PORT,GPIO_OTYPE_PP,GPIO_OSPEED_50MHZ,BSP_PWM_PIN);
/* 配置GPIO的复用 */
gpio_af_set(BSP_PWM_PORT,BSP_PWM_AF,BSP_PWM_PIN);
}
void pwm_config(uint16_t pre,uint16_t per)
{
timer_parameter_struct timere_initpara; // 定义定时器结构体
timer_oc_parameter_struct timer_ocintpara; //定时器比较输出结构体
pwm_gpio_config(); // 使能GPIO
rcu_periph_clock_enable(BSP_PWM_TIMER_RCU); // 开启定时器时钟
rcu_timer_clock_prescaler_config(RCU_TIMER_PSC_MUL4); // 配置定时器时钟
/* 配置定时器参数 */
timer_deinit(BSP_PWM_TIMER); // 复位定时器
timere_initpara.prescaler = pre-1; // 时钟预分频值 PSC_CLK= 200MHZ / 200 = 1MHZ
timere_initpara.alignedmode = TIMER_COUNTER_EDGE; // 边缘对齐
timere_initpara.counterdirection = TIMER_COUNTER_UP; // 向上计数
timere_initpara.period = per-1; // 周期 T = 10000 1MHZ = 10ms f = 100HZ
/* 在输入捕获的时候使用 数字滤波器使用的采样频率之间的分频比例 */
timere_initpara.clockdivision = TIMER_CKDIV_DIV1; // 分频因子
/* 只有高级定时器才有 配置为x,就重复x+1次进入中断 */
timere_initpara.repetitioncounter = 0; // 重复计数器 0-255
timer_init(BSP_PWM_TIMER,&timere_initpara); // 初始化定时器
/* 配置输出结构体 */
timer_ocintpara.ocpolarity = TIMER_OC_POLARITY_HIGH; // 有效电平的极性
timer_ocintpara.outputstate = TIMER_CCX_ENABLE; // 配置比较输出模式状态 也就是使能PWM输出到端口
/* 配置定时器输出功能 */
timer_channel_output_config(BSP_PWM_TIMER,BSP_PWM_CHANNEL,&timer_ocintpara);
/* 配置占空比 */
timer_channel_output_pulse_value_config(BSP_PWM_TIMER,BSP_PWM_CHANNEL,0); // 配置定时器通道输出脉冲值
timer_channel_output_mode_config(BSP_PWM_TIMER,BSP_PWM_CHANNEL,TIMER_OC_MODE_PWM0); // 配置定时器通道输出比较模式
timer_channel_output_shadow_config(BSP_PWM_TIMER,BSP_PWM_CHANNEL,TIMER_OC_SHADOW_DISABLE); // 配置定时器通道输出影子寄存器
/* 只有高级定时器使用 */
// timer_primary_output_config(TIMER0,ENABLE);
timer_auto_reload_shadow_enable(BSP_PWM_TIMER);
/* 使能定时器 */
timer_enable(BSP_PWM_TIMER);
}
void pwmMotorSetValue(uint16_t value)
{
timer_channel_output_pulse_value_config(BSP_PWM_TIMER,BSP_PWM_CHANNEL,value); // 配置定时器通道输出脉冲值
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
4.功能演示
本功能中,我们控制震动电机的强弱产生不同的震动效果。按下KEY-A开始震动,再次按下即关闭震动,屏幕进行相应的显示。
int main(void)
{
uint8_t keyTimes = 0; // 开机次数
uint16_t temp; // 临时变量
nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); // 优先级分组
/* 核心板 */
systick_config(); // 滴答定时器初始化
led_gpio_config(); // led初始化
key_gpio_config(); // key初始化
usart_gpio_config(115200U); // 串口0初始化
/* 扩展板 */
adc_config(); // 摇杆初始化
Lcd_Init(); // spi lcd初始化
pwm_config(200,1000); // 震动马达
/* SPI LCD */
LCD_Fill(0,0,LCD_W,LCD_H,COLOR_BLUE); // 深蓝色 背景
LCD_ShowPicture((LCD_W-222) / 2,0,222,60,gImage_lCKFB); // 显示图片
LCD_DrawRectangle(10,60 + 10,LCD_W -10,LCD_H -10,COLOR_BLACK); // 绘制一个矩形
/* 背景音乐:*/
temp = 75;
LCD_ShowString(15,temp,"Press KEY-A Motor Toggle!!",COLOR_WHITE,COLOR_BLUE,16,0);
temp = temp+20;
LCD_ShowChinese(15,temp,"震动马达:",COLOR_WHITE,COLOR_BLUE,16,0); // 显示16汉字字符串
Draw_Solid_Circle(15 + 100,temp + 8,8,COLOR_WHITE); // 显示实心圆
pwmMotorSetValue(1000); // 打开震动马达
delay_1ms(500); // 延迟3s
pwmMotorSetValue(0); // 关震动马达
while(1)
{
/* 震动马达 如果用摇杆控制的话 就是1:4*/
if(pwm_motor_status) // 震动马达状态开启
{
for(temp=200;temp<1000;temp++)
{
pwmMotorSetValue(temp);
delay_1ms(1);
}
for(;temp>200;temp--)
{
pwmMotorSetValue(temp);
delay_1ms(1);
}
pwmMotorSetValue(0);
delay_1ms(50);
}else // 震动马达状态关闭
{
pwmMotorSetValue(0);
}
delay_1ms(20);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
5.示例代码工程
👉文件下载
五、EEPROM芯片的使用
eeprom芯片用于掉电记录数据,做到了掉电不丢失的特点,虽说目前大量的MCU都内部集成了一定容量的eeprom,但是为了安全性以及无限制等原因,eeprom芯片仍广泛应用于各种电子产品中。
1.EEPROM芯片的电路
游戏机扩展板配置了24c02,该电路非常简单,并且使用iic通讯,只需要两个io进行通讯,并且支持多地址设置。这里只使用给一个eeprom,所以我们将地址设置IO全部拉低接地处理。
2.EEPROM芯片的使用
使用eeprom芯片前,需要对iic进行初始化,再按照eeprom控制命令进行iic通讯读写操作。 一般iic协议的通讯可以使用io软件模拟,也可以使用硬件iic协议,软件io模拟无io限制,但占用mcu处理能力,硬件iic通讯控制简单,但是需按外设对应io进行连接,通讯控制也需按照寄存器进行相应操作(原厂一般也写好)。
static void gpio_config(void)
{
/* enable GPIOB clock */
rcu_periph_clock_enable(RCU_GPIO_I2C);
/* connect I2C_SCL_PIN to I2C_SCL */
gpio_af_set(I2C_SCL_PORT, I2C_GPIO_AF, I2C_SCL_PIN);
/* connect I2C_SDA_PIN to I2C_SDA */
gpio_af_set(I2C_SDA_PORT, I2C_GPIO_AF, I2C_SDA_PIN);
/* configure GPIO pins of I2C */
gpio_mode_set(I2C_SCL_PORT, GPIO_MODE_AF, GPIO_PUPD_PULLUP, I2C_SCL_PIN);
gpio_output_options_set(I2C_SCL_PORT, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, I2C_SCL_PIN);
gpio_mode_set(I2C_SDA_PORT, GPIO_MODE_AF, GPIO_PUPD_PULLUP, I2C_SDA_PIN);
gpio_output_options_set(I2C_SDA_PORT, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, I2C_SDA_PIN);
}
void i2c_config(void)
{
gpio_config();
/* enable I2C clock */
rcu_periph_clock_enable(RCU_I2C);
/* configure I2C clock */
i2c_clock_config(I2CX, I2C_SPEED, I2C_DTCY_2);
/* configure I2C address */
i2c_mode_addr_config(I2CX, I2C_I2CMODE_ENABLE, I2C_ADDFORMAT_7BITS, I2CX_SLAVE_ADDRESS7);
/* enable I2CX */
i2c_enable(I2CX);
/* enable acknowledge */
i2c_ack_config(I2CX, I2C_ACK_ENABLE);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
3.功能演示
本功能中,我们将上电次数记录在eeprom中,每上电一次即计数加一并且保存,并且在屏幕上面显示当前次数。
int main(void)
{
uint8_t keyTimes = 0; // 开机次数
uint16_t temp; // 临时变量
nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); // 优先级分组
/* 核心板 */
systick_config(); // 滴答定时器初始化
led_gpio_config(); // led初始化
key_gpio_config(); // key初始化
usart_gpio_config(115200U); // 串口0初始化
/* 扩展板 */
adc_config(); // 摇杆初始化
Lcd_Init(); // spi lcd初始化
//pwm_config(200,1000); // 震动马达
i2c_eeprom_init(); // eeprom初始化
//dac_audio_init(22000); // dac音频初始化
/* EEPROM */
eeprom_buffer_read_timeout(&keyTimes,0x00,1); // 先读出来keyTimes
keyTimes ++;
eeprom_buffer_write_timeout(&keyTimes,0x00,1); // 再写入keyTimes
/* SPI LCD */
LCD_Fill(0,0,LCD_W,LCD_H,COLOR_BLUE); // 深蓝色 背景
LCD_ShowPicture((LCD_W-222) / 2,0,222,60,gImage_lCKFB); // 显示图片
LCD_DrawRectangle(10,60 + 10,LCD_W -10,LCD_H -10,COLOR_BLACK); // 绘制一个矩形
LCD_ShowString(15,75,"Power On Number:",COLOR_WHITE,COLOR_BLUE,16,0);
LCD_ShowString(15,95,"EEPROM:",COLOR_WHITE,COLOR_BLUE,32,0); // 显示32字符串
LCD_ShowIntNum(15 + 7*16,95,keyTimes,Num_count(keyTimes),COLOR_RED,COLOR_BLUE,32); // 显示EEPROM保存的数据 记录开机的次数
while(1);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
4.示例代码工程
👉文件下载
六、电池管理模块的使用
作为一个游戏机,肯定是需要移动使用的,所以就需要配备电池以随时随地玩游戏。 电池管理模块的开源工程
1. 电池管理模块
使用电池时就需要用到电池充电管理电路以及电池稳压(升/降压)电路,电池休眠电路,这里呢,我们已经设计了一个电池管理模块,将这些电路集成在了一个模块上面,方便大家使用。(该电路也已经开源,感兴趣的自行查看)
2.电池管理模块的电路
在使用电池管理模块的时候,我们仅需要少量的元器件和一个电池端子就行,锂电池的电压一般为2.4-4.2V,单片机工作电压为3.3V,ADC采集电池电压时需要分压处理即ADC的测量范围为0-6.6V。这里采用的两个100K电阻1/2分压.[]
电池的管理[BAT-CTRL]为电池休眠开机IO,通过按键接地触发,休眠状态下轻按开机供电,工作状态下长按休眠关机。这里通过一个二极管隔离复位电路的电压信号[BAT-CTRL]电压为:电池电压。
电池的管理[BAT-EN]为软件休眠控制io。在游戏机长时间未操作的情况下,将[]电压拉低接地即可休眠电池管理模块。
3.电池管理模块的使用
使用时需要将adc采集的io初始化为浮空模拟输入,控制EN脚的IO设置为推挽浮空输出即可。 采集电压为分压电压,并且adc基准为大概值3.3V,故需要将ADC的值(0-4096)转换为(0-6.6V)。
void bat_io_init(void)
{
// /* enable the clock */
rcu_periph_clock_enable(ADC_BAT_RCU);
/* configure GPIO port 附加功能需要配置为 GPIO_MODE_ANALOG */
gpio_mode_set(ADC_BAT_PORT, GPIO_MODE_ANALOG, GPIO_PUPD_NONE,ADC_BAT_PIN);
rcu_periph_clock_enable(BAT_EN_RCU);
gpio_bit_set(BAT_EN_PORT,BAT_EN_PIN);
gpio_mode_set(BAT_EN_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE,BAT_EN_PIN);
gpio_output_options_set(BAT_EN_PORT,GPIO_OTYPE_PP,GPIO_OSPEED_50MHZ,BAT_EN_PIN);
}
uint16_t adc_bat_get_value(void)
{
uint8_t i;
uint16_t adcValue=0;
for(i=0;i<16;i++)
{
adcValue += adc_channel_sample(ADC_CHANNEL_9); // 采样15通道
}
adcValue = (adcValue>>4)*3300/2048; //分压电阻/2
return adcValue;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
4.功能演示
本功能中,我们将屏幕上面显示电池的电压,并且不接入USB供电的情况下,工作10秒钟后即进入休眠状态,再次轻按复位按键唤醒电池管理模块,开始供电工作。
int main(void)
{
uint16_t Times = 0; // 次数
uint16_t temp; // 临时变量
nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); // 优先级分组
/* 核心板 */
systick_config(); // 滴答定时器初始化
led_gpio_config(); // led初始化
key_gpio_config(); // key初始化
usart_gpio_config(115200U); // 串口0初始化
/* 扩展板 */
adc_bat_init();
//BAT_EN_ONFF();
adc_config(); // 摇杆初始化
Lcd_Init(); // spi lcd初始化
/* SPI LCD */
LCD_Fill(0,0,LCD_W,LCD_H,COLOR_BLUE); // 深蓝色 背景
LCD_ShowPicture((LCD_W-222) / 2,0,222,60,gImage_lCKFB); // 显示图片
LCD_DrawRectangle(10,60 + 10,LCD_W -10,LCD_H -10,COLOR_BLACK); // 绘制一个矩形
/* x轴 进度条的长度最大为220 - 20 = 200, 然后值是从2.4V-4.2V 共1800 平均一个像素点是9 */
LCD_ShowString(20,75,"BAT VCC: 2.4-4.2V",COLOR_WHITE,COLOR_BLUE,24,0); // 显示16字符串
while(1)
{
temp = adc_bat_get_value();
RockerProgressBar(20,95,temp);
delay_1ms(100);
if(++Times> 100)
{
Times=0;
BAT_EN_ONFF();//电池休眠
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
5.示例代码工程
👉文件下载
七、综合基础功能演示
这里我们先讲解整体的基础功能如何整合,将前面讲解的各个功能都综合起来。让大家了解学习制作游戏机需要哪些方面的知识。(具体的游戏制作将在后面章节进行讲解)
1. 游戏机扩展板的原理图
前面已经将各个电路进行了说明,这里只展示整体的原理图效果。有需要的可以自行查看前面的单个功能章节。
2.游戏机扩展板的PCB
3.游戏机扩展板的基础功能源码
上电以后,将各个功能初始化,和前面的单个功能类似。在主循环里面,会依次刷新显示摇杆的移动ADC值,电池的电压值,按键控制震动和音乐。 总的来说就是将前面的单个功能综合到了一起。
int main(void)
{
uint16_t Times = 0; // 次数
uint8_t keyTimes = 0; // 开机次数
uint16_t temp; // 临时变量
nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); // 优先级分组
/* 核心板 */
systick_config(); // 滴答定时器初始化
led_gpio_config(); // led初始化
key_gpio_config(); // key初始化
usart_gpio_config(115200U); // 串口0初始化
/* 扩展板 */
bat_io_init(); //电池管理
adc_config(); // 摇杆初始化
Lcd_Init(); // spi lcd初始化
pwm_config(200,1000); // 震动马达
i2c_eeprom_init(); // eeprom初始化
dac_audio_init(22000); // dac音频初始化
/* EEPROM */
eeprom_buffer_read_timeout(&keyTimes,0x00,1); // 先读出来keyTimes
keyTimes ++;
eeprom_buffer_write_timeout(&keyTimes,0x00,1); // 再写入keyTimes
/* SPI LCD */
LCD_Fill(0,0,LCD_W,LCD_H,COLOR_BLUE); // 深蓝色 背景
LCD_ShowPicture((LCD_W-222) / 2,0,222,60,gImage_lCKFB); // 显示图片
LCD_DrawRectangle(10,60 + 10,LCD_W -10,LCD_H -10,COLOR_BLACK); // 绘制一个矩形
LCD_ShowChinese(15,60 + 10 + 5,"开始游戏",COLOR_WHITE,COLOR_BLUE,12,0); // 显示12汉字字符串
LCD_ShowChinese(15,60 + 10 + 5 + 12,"开始游戏",COLOR_WHITE,COLOR_BLUE,16,0); // 显示16汉字字符串
LCD_ShowChinese(15,60 + 10 + 5 + 12 + 16,"开始游戏",COLOR_WHITE,COLOR_BLUE,24,0); // 显示24汉字字符串
LCD_ShowChinese(15,60 + 10 + 5 + 12 + 16 + 24 ,"开始游戏",COLOR_WHITE,COLOR_BLUE,32,0); // 显示32汉字字符串
LCD_ShowString(15,60 + 10 + 5 + 12 + 16 + 24 + 32 ,"123abc",COLOR_WHITE,COLOR_BLUE,16,0); // 显示16字符串
LCD_ShowString(15,60 + 10 + 5 + 12 + 16 + 24 + 32 +16,"123abc",COLOR_WHITE,COLOR_BLUE,24,0); // 显示24字符串
LCD_ShowString(15,60 + 10 + 5 + 12 + 16 + 24 + 32 + 16 + 24,"123abc",COLOR_WHITE,COLOR_BLUE,32,0); // 显示32字符串
LCD_ShowString(15,60 + 10 + 5 + 12 + 16 + 24 + 32 + 16 + 24 + 32,"EEPROM:",COLOR_RED,COLOR_BLUE,32,0); // 显示32字符串
LCD_ShowIntNum(15 + 7*16,60 + 10 + 5 + 12 + 16 + 24 + 32 + 16 + 24 + 32,keyTimes,Num_count(keyTimes),COLOR_RED,COLOR_BLUE,32); // 显示EEPROM保存的数据 记录开机的次数
pwmMotorSetValue(1000); // 打开震动马达
delay_1ms(500); // 延迟3s
pwmMotorSetValue(0); // 关震动马达
LCD_Fill(15,75,LCD_W -10,LCD_H -10,COLOR_BLUE); // 蓝色 背景
/* 先来获取一下遥感的位置 */
adc_key_scan();
/* x轴 进度条的长度最大为220 - 50 = 170, 然后值是从0-4095 共4096 平均一个像素点是24 */
temp = 75;
LCD_ShowString(15,temp,"X:",COLOR_WHITE,COLOR_BLUE,24,0); // 显示16字符串
RockerProgressBar(50,temp,adcXValue);
/* y轴 */
temp = temp + 32 + 5;
LCD_ShowString(15,temp,"Y:",COLOR_WHITE,COLOR_BLUE,24,0); // 显示16字符串
RockerProgressBar(50,temp,adcYValue);
//电池电压
temp = temp + 32 + 5;
LCD_ShowString(15,temp+4,"VCC:",COLOR_WHITE,COLOR_BLUE,16,0); // 显示16字符串
RockerProgressBar1(50,temp,adc_bat_get_value());
/* 震动马达 */
temp = temp + 32 + 20;
LCD_ShowChinese(15,temp,"震动马达:",COLOR_WHITE,COLOR_BLUE,16,0); // 显示16汉字字符串
/* 背景音乐:*/
temp = temp + 16 + 20;
LCD_ShowChinese(15,temp,"背景音乐:",COLOR_WHITE,COLOR_BLUE,16,0); // 显示16汉字字符串
Draw_Solid_Circle(15 + 100,temp + 8,8,COLOR_WHITE); // 显示实心圆
while(1)
{
/* 摇杆 */
adc_key_scan(); // 摇杆扫描
RockerProgressBar(50,75,adcXValue); // x轴
RockerProgressBar(50,112,adcYValue); // y轴
RockerProgressBar1(50,149,adc_bat_get_value()); //电池电压
/* 震动马达 如果用摇杆控制的话 就是1:4*/
if(pwm_motor_status) // 震动马达状态开启
{
Draw_Solid_Circle(15 + 100,temp - 16 - 20 + 8,8,COLOR_GREEN); // 显示实心圆
pwmMotorSetValue(adcYValue / 4); // 设置震动马达
}else // 震动马达状态关闭
{
Draw_Solid_Circle(15 + 100,temp - 16 - 20 + 8,8,COLOR_WHITE); // 显示实心圆
pwmMotorSetValue(0); // 关闭震动马达
}
/* 音频 */
if(audio_status == 1) // 开启音乐
{
audio_status = 3;
Draw_Solid_Circle(15 + 100,temp + 8,8,COLOR_GREEN); // 显示实心圆
start_play_audio(audioSouce_Background,sizeof(audioSouce_Background),1); // 播放音乐 单次播放
}else if(audio_status == 2) // 震动马达状态关闭
{
audio_status = 0;
Draw_Solid_Circle(15 + 100,temp + 8,8,COLOR_WHITE); // 显示实心圆
stop_play_audio();
}
delay_1ms(20);
if(++Times> 500)
{
Times=0;
BAT_EN_ONFF();//电池休眠
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
4.示例代码工程
👉文件下载
八、NES模拟器代码移植
NES游戏为任天堂公司红白机上面运行的一种游戏,CPU为6502芯片。若想在其他环境使用NES游戏则需要相应的NES游戏模拟器(电脑端,手机端,单片机端)。这里我们着重介绍单片机端的NES模拟器。 网上开源的单片机端的NES模拟器主要有:info nes(龙元)stm32 nes(ye781205),stm32 nes(正点原子)。正点原子的nes是在ye781205开源的基础上进行优化处理的,这里我们就讲解如何移植正点原子的nes模拟器。
1. NES模拟器源码大致说明
移植现有的NES模拟器代码,主要需要修改的地方为音频的播放(nes_apu.c),画面的显示(nes_ppu.c),按键的输入以及其他(nes_main.c)。
完整的NES游戏机是支持文件系统的,参考案例:NES游戏机功能说明。这里我们只需要将NES模拟先单独移植好再引入文件系统即可。所以这里我们是使用的文件转数组的方式(将nes文件转换成数组文件直接在代码里面使用)。以减少移植的难度。
1. 画面的显示(nes_ppu.c)修改
这个是NES模拟器的画面底层划线更新函数,每次更新一行的屏幕像素。正点原子采用的FSMC驱动的16位并口屏,更新速度相对SPI 1位数据更新屏幕会快一些,所以这里我们将SPI发送数据的代码整合到该函数下以提高刷新速度,让NES游戏的画面运行更加流畅。(还有DMA+SPI的方法,有兴趣自行尝试。)
//(nes_ppu.c)
void scanline_draw(int LineNo)
{
uint16 i;
uint16_t sx=17,ex=257;
do_scanline_and_draw(ppu->dummy_buffer);
// sx = 17;
// ex = 240+17;
LCD_CS_Clr();
LCD_DC_Clr();//写命令
spi_i2s_data_transmit(SPI4, 0x2a);
while(RESET == spi_i2s_flag_get(SPI4, SPI_FLAG_TBE));
LCD_DC_Set();//写数据
spi_i2s_data_transmit(SPI4, 0);
while(RESET == spi_i2s_flag_get(SPI4, SPI_FLAG_TBE));
spi_i2s_data_transmit(SPI4, 0);
while(RESET == spi_i2s_flag_get(SPI4, SPI_FLAG_TBE));
spi_i2s_data_transmit(SPI4, 0);
while(RESET == spi_i2s_flag_get(SPI4, SPI_FLAG_TBE));
spi_i2s_data_transmit(SPI4, 240);
while(RESET == spi_i2s_flag_get(SPI4, SPI_FLAG_TBE));
LCD_DC_Clr();//写命令
spi_i2s_data_transmit(SPI4, 0x2b);
while(RESET == spi_i2s_flag_get(SPI4, SPI_FLAG_TBE));
LCD_DC_Set();//写数据
spi_i2s_data_transmit(SPI4, (LineNo+20)>>8);
while(RESET == spi_i2s_flag_get(SPI4, SPI_FLAG_TBE));
spi_i2s_data_transmit(SPI4, LineNo+20);
while(RESET == spi_i2s_flag_get(SPI4, SPI_FLAG_TBE));
spi_i2s_data_transmit(SPI4, (LineNo+20)>>8);
while(RESET == spi_i2s_flag_get(SPI4, SPI_FLAG_TBE));
spi_i2s_data_transmit(SPI4, LineNo+20);
while(RESET == spi_i2s_flag_get(SPI4, SPI_FLAG_TBE));
LCD_DC_Clr();//写命令
spi_i2s_data_transmit(SPI4, 0x2c);
while(RESET == spi_i2s_flag_get(SPI4, SPI_FLAG_TBE));
LCD_DC_Set();//写数据
for(i=sx;i<ex;i++)
{
while(RESET == spi_i2s_flag_get(SPI4, SPI_FLAG_TBE));
spi_i2s_data_transmit(SPI4, NES_Palette[ppu->dummy_buffer[i]]>>8);
while(RESET == spi_i2s_flag_get(SPI4, SPI_FLAG_TBE));
spi_i2s_data_transmit(SPI4, NES_Palette[ppu->dummy_buffer[i]]);
}
while(RESET == spi_i2s_flag_get(SPI4, SPI_FLAG_TBE));
LCD_CS_Set();
// LCD_Address_Set(0,LineNo+20,240,LineNo+20);//设置显示范围 横着屏显示
// LCD_Address_Set(x,y,x,y);
// for(i=sx;i<ex;i++){
// //LCD_DrawPoint(LineNo,i,NES_Palette[ppu->dummy_buffer[i]]);
// //printf("i:%d color:%d \r\n",i,NES_Palette[ppu->dummy_buffer[i]]);
// LCD_WR_DATA(NES_Palette[ppu->dummy_buffer[i]]);
// }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
3. 音频的播放代码修改
(nes_apu.c)为NES模拟的声音处理代码,但是实际的音频播放完整的流程涉及到了(nes_apu.c)音频的生成,(nes_main.c)缓冲区的处理,(DacAudio.c)音频的定时器播放。
//(nes_apu.c)apu声音输出
void apu_soundoutput(void)
{
u16 i;
apu_process(wave_buffers,APU_PCMBUF_SIZE);
for(i=0;i<30;i++)if(wave_buffers[i]!=wave_buffers[i+1])break;//判断前30个数据,是不是都相等?
if(i==30)//都相等,且不等于0
{
if(wave_buffers[i])
for(i=0;i<APU_PCMBUF_SIZE;i++)wave_buffers[i]=0;//是暂停状态输出的重复无效数据,直接修改为0.从而不输出杂音.
else{
clocks=0;
return;
}
}
clocks=0;
nes_apu_fill_buffer(0,wave_buffers);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//(nes_main.c)NES音频输出到缓存
void nes_apu_fill_buffer(int samples,u16* wavebuf)
{
int ret = 0;
int i,j;
uint8_t data = 0;
for(i = 0; i<APU_PCMBUF_SIZE; i++){data = wavebuf[i]>>8;
if(data > 0x80) data = 0x80 - ((0xff - data)>>3);//
else data = (data>>3) + 0x80;//
ret = cw_queue_write(&t_queue,&data,1,10);
if(!ret)
{
//if write ok start next Rx
GWDBG("queue write faild");
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//(DacAudio.c)
void TIMER4_IRQHandler(void)
{
int ret = 0;
uint8_t data = 0;
ret = cw_queue_read(&t_queue,&data,1,0);
if(ret)
{
dac_data_set(DAC1, DAC_ALIGN_8B_R,data);
dac_software_trigger_enable(DAC1);
}
//printf("irq:%d\r\n",ret);
/* clear TIMER interrupt flag */
timer_interrupt_flag_clear(TIMER4, TIMER_INT_FLAG_UP);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
4. 按键代码修改
输入分为摇杆控制和普通按键控制。在使用时不考虑摇杆按键的模拟量,只考虑上下左右控制,所以直接用一个阈值处理即可。
//adc_Key.c
/* 五向按键扫描函数 */
five_key_enum five_way_key_scan(void)
{
uint16_t adcXValue,adcYValue;
adcXValue = adc_channel_sample(ADC_CHANNEL_1); // 采样
adcYValue = adc_channel_sample(ADC_CHANNEL_11); // 采样
if (adcYValue >= 3000 )
{ //
return five_key_down;
}
if ( adcYValue <= 1000)
{ //
return five_key_up;
}
if (adcXValue >= 3000 )
{ //
return five_key_left;
}
if ( adcXValue <= 1000)
{ //
return five_key_right;
}
return five_key_null;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//key.c
one_key_enum get_key_val()
{
if( gpio_input_bit_get(KEYL_PORT, KEYL_PIN) == SET ||
gpio_input_bit_get(KEYR_PORT, KEYR_PIN) == SET ||
gpio_input_bit_get(KEYA_PORT, KEYA_PIN) == SET ||
gpio_input_bit_get(KEYB_PORT, KEYB_PIN) == SET )
{
//delay_1ms(10); // 延迟消抖
if (gpio_input_bit_get(KEYL_PORT, KEYL_PIN) == SET)
{ // 上
return one_key_left;
}
else if (gpio_input_bit_get(KEYR_PORT, KEYR_PIN) == SET)
{ // 左
return one_key_right;
}
else if (gpio_input_bit_get(KEYA_PORT, KEYA_PIN) == SET)
{ // 下
return one_key_a;
}
else if (gpio_input_bit_get(KEYB_PORT, KEYB_PIN) == SET)
{ //
return one_key_b;
}
else
{ // 默认 没有操作
return one_key_null;
}
}
else
{
return one_key_null;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
按键数据的处理在(nes_main.c)中进行统一处理。
//读取游戏手柄数据
void nes_get_gamepadval(void)
{
//手柄1键值 [7:0]右7 左6 下5 上4 Start3 Select2 B1 A0
// uint16_t key_val[]={0,4,5,6,7,0xB1,0xA0};
//右128 左64 下32 上16 Start 8 Select 4 B 2 A0
uint16_t five_key_Val[]={0,16,32,64,128};
uint16_t one_key_Val[]={0,8,4,2,1};
five_key_enum five_key = five_key_null;
one_key_enum one_key = one_key_null;
volatile static uint32_t time = 0;
if (g_systick-10 > time)
{
PADdata = 0;
PADdata1=0;
//GWDBG("time:%d tick:%d",time,g_systick);
five_key = five_way_key_scan();
if(five_key!=five_key_null)
{
time = g_systick;
PADdata = five_key_Val[five_key];
PADdata1=0;
GWDBG("five_key:%d",PADdata);
}
one_key = get_key_val();
if(one_key!=one_key_null)
{
time = g_systick;
PADdata |= one_key_Val[one_key];
PADdata1=0;
GWDBG("one_key:%d",PADdata);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
5. 游戏文件加载代码修改
移植的游戏加载方式为数据加载,故只需要修改读取数组到SRAM立马即可。
//炸弹人:(bomb.h)unsigned char g_bomb_data[24592] =
#define G_NES_GAM_RES g_bomb_data
//开始nes游戏
u8 nes_load(u8* pname)
{
// FIL *file;
// UINT br;
u8 res=0;
GWDBG("");
res=nes_sram_malloc(sizeof(G_NES_GAM_RES)); //申请内存
cw_queue_init(&t_queue,i2sbuf1,APU_PCMBUF_SIZE*8,delay_1ms);
cw_queue_clean(&t_queue);
if(res==0)
{
GWDBG("");
// f_read(file,romfile,file->fsize,&br); //读取nes文件
memcpy(romfile,G_NES_GAM_RES,sizeof(G_NES_GAM_RES));
res=nes_load_rom(); //加载ROM
if(res==0)
{
GWDBG("");
Mapper_Init(); //map初始化
GWDBG("");
cpu6502_init(); //初始化6502,并复位
GWDBG("");
PPU_reset(); //ppu复位
GWDBG("");
apu_init(); //apu初始化
GWDBG("");
nes_sound_open(0,APU_SAMPLE_RATE); //初始化播放设备
GWDBG("");
nes_emulate_frame(); //进入NES模拟器主循环
GWDBG("");
nes_sound_close(); //关闭声音输出
GWDBG("");
}
GWDBG("");
}
// f_close(file);
// myfree(SRAMIN,file);//释放内存
nes_sram_free(); //释放内存
GWDBG("res:%d",res);
return res;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
6. 主函数mian代码
这里我们只简单的移植nes游戏机代码,所以主函数里面只需要实现基本的NES功能即可,详细的NES游戏机设计请看下一个章节。
int main(void)
{
ErrStatus init_state;
/* 核心板 */
systick_config();
usart0_init();
/* 扩展板 */
adc_config();
key_init();
Dac_Init();
DacAudio_Init();
// DacAudio_Play(dacAudio_AliPay,sizeof(dacAudio_AliPay),9000);
GWDBG("EXMC");
/* config the EXMC access mode */
init_state = exmc_synchronous_dynamic_ram_init(EXMC_SDRAM_DEVICE0);
GWDBG("");
if (ERROR == init_state)
{
GWDBG("\r\n\r\nSDRAM initialize fail!");
while (1);
}
my_mem_init(SRAMIN);
my_mem_init(SRAMEX);
my_mem_init(SRAMCCM);
delay_1ms(50);
#ifdef USE_HARDWARESPI
spi_config();
#endif
LCD_Init();//LCD初始化
LCD_Fill(0,0,LCD_W,LCD_H,BLACK);
GWDBG("nes_load start\r\n");
nes_load(NULL);//NES游戏循环
GWDBG("nes_load end\r\n");
while (1);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
7.示例代码工程
👉文暂时只支持git仓库下载工程源码
九、NES游戏机代码说明
1.NES游戏机代码设计总览
NES游戏机实现了打开文件系统内的nes游戏,删除/复制文件系统内的nes游戏,设置显示功能。底层部分采用了网上开源的文件系统/malloc/中文字库/nes游戏驱动。经过大量底层适配,加入自己编写的UI界面以及文件检索/复制等代码。实现了大致4个功能:打开游戏,历史游戏,游戏管理,设置。
int main(void)
{
//初始化
systick_config();
usart0_init();
adc_config();
adc_bat_init();
key_init();
led_init();
ui_main_set_init();////开机掉电设置
ui_main_lcd_init();//开机logo显示
keywav_init(ui_set.volume_class);
Dac_Init();
motro_gpio_config();
key_timeinit(600*50); //按键音频率
rtctime_init();
//sram初始化
exmc_synchronous_dynamic_ram_init(EXMC_SDRAM_DEVICE0);
my_mem_init(SRAMIN);
my_mem_init(SRAMEX);
my_mem_init(SRAMCCM);
//文件系统
exfuns_init(); //为fatfs相关变量申请内存
sd_fatfs_init();//sd fatfs初始化
spi_fatfs_init();//spi flash fatfs+字库 初始化
//无字库显示英文
if(ui_ram.language==0 && fat_ram.word_stock==0) ui_ram.language=1;
///主界面小图标显示
rtc_init_show();
///主界面大图标全部显示
ui_main_init_show();
while(1)
{
//主界面选择
ui_adckey.now_adckey = five_way_key_scan();
if( ui_adckey.last_adckey != ui_adckey.now_adckey)
{
ui_adckey.last_adckey = ui_adckey.now_adckey;
if(ui_adckey.last_adckey == adckey_null) continue;
key_wav_motor();//按键声音震动
//循环切换
//if(ui_ram.now_app & 0xf0) ui_ram.now_app &= 0x0f;
//else ui_ram.now_app |= 0x10;
//if(ui_ram.now_app & 0x0f) ui_ram.now_app &= 0xf0;
//else ui_ram.now_app |= 0x01;
//不可循环切换 上下同时选中
if(ui_adckey.last_adckey & adckey_up) ui_ram.now_app &= 0x0f;
else if(ui_adckey.last_adckey & adckey_down) ui_ram.now_app |= 0x10;
else;
if(ui_adckey.last_adckey & adckey_left) ui_ram.now_app &= 0xf0;
else if(ui_adckey.last_adckey & adckey_right) ui_ram.now_app |= 0x01;
else;
}
//选择变化显示
ui_main_last_show();
///确定进入功能
ui_key.now_key = get_key_val();
if(ui_key.last_key != ui_key.now_key)
{
ui_key.last_key = ui_key.now_key;
if(ui_key.last_key == key_null) continue;
if(ui_key.last_key & key_exit) continue;
key_wav_motor();//按键声音震动
//进入功能
switch(ui_ram.last_app)
{
case game_start:
game_start_main();//开始游戏
break;
case game_last:
game_last_main();//上次游戏
break;
case game_manage:
game_manage_main();//管理游戏
break;
case game_set:
game_set_main();//游戏设置
break;
default:
break;
}
}
//小图标显示
rtc_last_show();
delay_1ms(10);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
2.NES游戏机UI代码说明
UI代码实现分为两部分:1初始化功能界面,2操作变化更新界面。初始化界面时会将整个屏幕内容刷新,操作变化更新界面则只需要更新变化的部分即可,缩短刷新时间,提高显示效果,降低操作延时。
图2-2 主界面操作演示
///初始化显示主界面
void ui_main_init_show(void)
{
//清空
LCD_Fill(0,uitela_show_start,LCD_W,LCD_H,WHITE);
//显示
switch(ui_ram.now_app)
{
case game_start:
LCD_ShowPicture1((uimain_bmp_paly_x-(uimain_bmp_szie>>1)),(uimain_bmp_paly_y-(uimain_bmp_szie>>1)),uimain_bmp_szie,uimain_bmp_szie,UIMAIN_BMP[onplay_number]);
LCD_ShowPicture1((uimain_bmp_last_x-(uimain_bmp_szie>>1)),(uimain_bmp_last_y-(uimain_bmp_szie>>1)),uimain_bmp_szie,uimain_bmp_szie,UIMAIN_BMP[offlast_number]);
LCD_ShowPicture1((uimain_bmp_copy_x-(uimain_bmp_szie>>1)),(uimain_bmp_copy_y-(uimain_bmp_szie>>1)),uimain_bmp_szie,uimain_bmp_szie,UIMAIN_BMP[offcopy_number]);
LCD_ShowPicture1((uimain_bmp_set_x-(uimain_bmp_szie>>1)),(uimain_bmp_set_y-(uimain_bmp_szie>>1)),uimain_bmp_szie,uimain_bmp_szie,UIMAIN_BMP[offset_number]);
Show_Str(((240-strlen((char*)ui_show[ui_set.language][uimain_playh_show])*8)/2),uihint_show_line,(u8*)ui_show[ui_set.language][uimain_playh_show],LCEDA,WHITE,16,0);
break;
case game_last:
LCD_ShowPicture1((uimain_bmp_paly_x-(uimain_bmp_szie>>1)),(uimain_bmp_paly_y-(uimain_bmp_szie>>1)),uimain_bmp_szie,uimain_bmp_szie,UIMAIN_BMP[offplay_number]);
LCD_ShowPicture1((uimain_bmp_last_x-(uimain_bmp_szie>>1)),(uimain_bmp_last_y-(uimain_bmp_szie>>1)),uimain_bmp_szie,uimain_bmp_szie,UIMAIN_BMP[onlast_number]);
LCD_ShowPicture1((uimain_bmp_copy_x-(uimain_bmp_szie>>1)),(uimain_bmp_copy_y-(uimain_bmp_szie>>1)),uimain_bmp_szie,uimain_bmp_szie,UIMAIN_BMP[offcopy_number]);
LCD_ShowPicture1((uimain_bmp_set_x-(uimain_bmp_szie>>1)),(uimain_bmp_set_y-(uimain_bmp_szie>>1)),uimain_bmp_szie,uimain_bmp_szie,UIMAIN_BMP[offset_number]);
Show_Str(((240-strlen((char*)ui_show[ui_set.language][uimain_last_show])*8)/2),uihint_show_line,(u8*)ui_show[ui_set.language][uimain_last_show],LCEDA,WHITE,16,0);
break;
case game_manage:
LCD_ShowPicture1((uimain_bmp_paly_x-(uimain_bmp_szie>>1)),(uimain_bmp_paly_y-(uimain_bmp_szie>>1)),uimain_bmp_szie,uimain_bmp_szie,UIMAIN_BMP[offplay_number]);
LCD_ShowPicture1((uimain_bmp_last_x-(uimain_bmp_szie>>1)),(uimain_bmp_last_y-(uimain_bmp_szie>>1)),uimain_bmp_szie,uimain_bmp_szie,UIMAIN_BMP[offlast_number]);
LCD_ShowPicture1((uimain_bmp_copy_x-(uimain_bmp_szie>>1)),(uimain_bmp_copy_y-(uimain_bmp_szie>>1)),uimain_bmp_szie,uimain_bmp_szie,UIMAIN_BMP[oncopy_number]);
LCD_ShowPicture1((uimain_bmp_set_x-(uimain_bmp_szie>>1)),(uimain_bmp_set_y-(uimain_bmp_szie>>1)),uimain_bmp_szie,uimain_bmp_szie,UIMAIN_BMP[offset_number]);
Show_Str(((240-strlen((char*)ui_show[ui_set.language][uimain_copy_show])*8)/2),uihint_show_line,(u8*)ui_show[ui_set.language][uimain_copy_show],LCEDA,WHITE,16,0);
break;
case game_set:
LCD_ShowPicture1((uimain_bmp_paly_x-(uimain_bmp_szie>>1)),(uimain_bmp_paly_y-(uimain_bmp_szie>>1)),uimain_bmp_szie,uimain_bmp_szie,UIMAIN_BMP[offplay_number]);
LCD_ShowPicture1((uimain_bmp_last_x-(uimain_bmp_szie>>1)),(uimain_bmp_last_y-(uimain_bmp_szie>>1)),uimain_bmp_szie,uimain_bmp_szie,UIMAIN_BMP[offlast_number]);
LCD_ShowPicture1((uimain_bmp_copy_x-(uimain_bmp_szie>>1)),(uimain_bmp_copy_y-(uimain_bmp_szie>>1)),uimain_bmp_szie,uimain_bmp_szie,UIMAIN_BMP[offcopy_number]);
LCD_ShowPicture1((uimain_bmp_set_x-(uimain_bmp_szie>>1)),(uimain_bmp_set_y-(uimain_bmp_szie>>1)),uimain_bmp_szie,uimain_bmp_szie,UIMAIN_BMP[onset_number]);
Show_Str(((240-strlen((char*)ui_show[ui_set.language][uimain_set_show])*8)/2),uihint_show_line,(u8*)ui_show[ui_set.language][uimain_set_show],LCEDA,WHITE,16,0);
break;
default:
break;
}
}
////操作有变化显示
void ui_main_last_show(void)
{
if(ui_ram.now_app != ui_ram.last_app)
{
//消除
switch(ui_ram.last_app)
{
case game_start:
LCD_ShowPicture1((uimain_bmp_paly_x-(uimain_bmp_szie>>1)),(uimain_bmp_paly_y-(uimain_bmp_szie>>1)),uimain_bmp_szie,uimain_bmp_szie,UIMAIN_BMP[offplay_number]);
break;
case game_last:
LCD_ShowPicture1((uimain_bmp_last_x-(uimain_bmp_szie>>1)),(uimain_bmp_last_y-(uimain_bmp_szie>>1)),uimain_bmp_szie,uimain_bmp_szie,UIMAIN_BMP[offlast_number]);
break;
case game_manage:
LCD_ShowPicture1((uimain_bmp_copy_x-(uimain_bmp_szie>>1)),(uimain_bmp_copy_y-(uimain_bmp_szie>>1)),uimain_bmp_szie,uimain_bmp_szie,UIMAIN_BMP[offcopy_number]);
break;
case game_set:
LCD_ShowPicture1((uimain_bmp_set_x-(uimain_bmp_szie>>1)),(uimain_bmp_set_y-(uimain_bmp_szie>>1)),uimain_bmp_szie,uimain_bmp_szie,UIMAIN_BMP[offset_number]);
break;
default:
break;
}
//显示
LCD_Fill(0,uihint_show_start,240,uihint_show_stop,WHITE);
switch(ui_ram.now_app)
{
case game_start:
LCD_ShowPicture1((uimain_bmp_paly_x-(uimain_bmp_szie>>1)),(uimain_bmp_paly_y-(uimain_bmp_szie>>1)),uimain_bmp_szie,uimain_bmp_szie,UIMAIN_BMP[onplay_number]);
Show_Str(((240-strlen((char*)ui_show[ui_set.language][uimain_playh_show])*8)/2),uihint_show_line,(u8*)ui_show[ui_set.language][uimain_playh_show],LCEDA,WHITE,16,0);
break;
case game_last:
LCD_ShowPicture1((uimain_bmp_last_x-(uimain_bmp_szie>>1)),(uimain_bmp_last_y-(uimain_bmp_szie>>1)),uimain_bmp_szie,uimain_bmp_szie,UIMAIN_BMP[onlast_number]);
Show_Str(((240-strlen((char*)ui_show[ui_set.language][uimain_last_show])*8)/2),uihint_show_line,(u8*)ui_show[ui_set.language][uimain_last_show],LCEDA,WHITE,16,0);
break;
case game_manage:
LCD_ShowPicture1((uimain_bmp_copy_x-(uimain_bmp_szie>>1)),(uimain_bmp_copy_y-(uimain_bmp_szie>>1)),uimain_bmp_szie,uimain_bmp_szie,UIMAIN_BMP[oncopy_number]);
Show_Str(((240-strlen((char*)ui_show[ui_set.language][uimain_copy_show])*8)/2),uihint_show_line,(u8*)ui_show[ui_set.language][uimain_copy_show],LCEDA,WHITE,16,0);
break;
case game_set:
LCD_ShowPicture1((uimain_bmp_set_x-(uimain_bmp_szie>>1)),(uimain_bmp_set_y-(uimain_bmp_szie>>1)),uimain_bmp_szie,uimain_bmp_szie,UIMAIN_BMP[onset_number]);
Show_Str(((240-strlen((char*)ui_show[ui_set.language][uimain_set_show])*8)/2),uihint_show_line,(u8*)ui_show[ui_set.language][uimain_set_show],LCEDA,WHITE,16,0);
break;
default:
break;
}
ui_ram.last_app = ui_ram.now_app;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
3.NES游戏机FAT文件操作代码说明
文件系统的操作代码主要分为检索文件列表和复制文件。检索部分包含了完整路径以及文件字节两种列表,用于不同的使用场景,打开游戏使用完整路径列表,管理游戏使用文件字节列表。
//指定检索sd卡游戏 带字节数
uint8_t sd_file_search(void)
{
uint8_t temp;
uint8_t len;
temp=f_opendir(picdir,(const TCHAR*)"0:/nes"); //打开目录
if(temp != FR_OK)
{
fat_ram.sd_nesnumber = 0;
fat_ram.sd_nesname[0] = (uint8_t *)ui_show[ui_set.language][uigame_no_found];//提示未找到
fat_ram.sd_sizename[0] = (uint8_t *)ui_show[ui_set.language][uigame_no_found];//提示未找到
return temp;
}
fat_ram.sd_nesnumber=0;
while(temp==FR_OK)
{
temp=f_readdir(picdir,tfileinfo);
if(temp!=FR_OK||tfileinfo->fname[0]==0)break;
strcpy((char*)&pname,"0:/nes/");
strcat((char*)&pname,(const char*)tfileinfo->fname);//将文件名接在后面
len=strlen((const char*)tfileinfo->fname);
if(tfileinfo->fname[len-4] == '.' && len < (max_name_number-8))
{
if((tfileinfo->fname[len-3] == 'n') || (tfileinfo->fname[len-3] == 'N'))
{
if((tfileinfo->fname[len-2] == 'e') || (tfileinfo->fname[len-2] == 'E'))
{
if((tfileinfo->fname[len-1] == 's') || (tfileinfo->fname[len-1] == 'S'))
{
//带路径记录
strcpy((char*)fat_ram.sd_nesname[fat_ram.sd_nesnumber],(char*)pname);
//带容量路径记录
if((tfileinfo->fsize >> 10) < 4096) sprintf((char*)pname,"%4dKB ",(unsigned int)(tfileinfo->fsize >> 10)+1);
else sprintf((char*)pname,"%4dMB ",(unsigned int)(tfileinfo->fsize >> 20));
strcat((char*)&pname,(const char*)tfileinfo->fname);//将文件名接在后面
strcpy((char*)fat_ram.sd_sizename[fat_ram.sd_nesnumber],(char*)pname);
if(++fat_ram.sd_nesnumber >= max_nes_number) break;
}
}
}
}
}
//没找到文件
if(fat_ram.sd_nesnumber == 0)
{
fat_ram.sd_nesname[0] = (uint8_t *)ui_show[ui_set.language][uigame_no_found];//提示未找到
fat_ram.sd_sizename[0] = (uint8_t *)ui_show[ui_set.language][uigame_no_found];//提示未找到
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
4.NES游戏机功能代码【开始游戏】说明
开始游戏功能主要为打开SD卡或者SPI flash内的nes游戏,开始游戏前记录打开的游戏路径。方便【上次游戏】功能的实现。
///开始游戏
void game_start_main(void)
{
u8 res;
///选择界面大图标全部显示
ui_select_init_show();
while(1)
{
//选择
ui_adckey.now_adckey = five_way_key_scan();
if( ui_adckey.last_adckey != ui_adckey.now_adckey)
{
ui_adckey.last_adckey = ui_adckey.now_adckey;
if(ui_adckey.last_adckey == adckey_null) continue;
if(ui_adckey.last_adckey & adckey_up_down) continue;
key_wav_motor();
if(ui_adckey.last_adckey & adckey_left_right)
{
//不可循环
if(ui_adckey.last_adckey & adckey_left) ui_ram.now_sdspi = 0;
else if(ui_adckey.last_adckey & adckey_right) ui_ram.now_sdspi = 1;
else;
}
}
//选择变化界面
ui_select_last_show();
//进入游戏
ui_key.now_key = get_key_val();
if( ui_key.last_key != ui_key.now_key)
{
ui_key.last_key = ui_key.now_key;
if(ui_key.last_key == key_null) continue;
key_wav_motor();
if(ui_key.last_key & key_exit)
{
///主界面大图标全部显示
ui_main_init_show();
//while(get_key_val());
break;
}
///目录游戏打开 maxnes fat_ram.sd_nesname
if(ui_ram.now_sdspi) res = spiflash_file_search();
else res = sd_file_search();
if(res)
{
//错误提示
ui_error_show((u8 *)ui_show[ui_ram.language][uierror_dir_show]);
///选择界面大图标全部显示
ui_select_init_show();
while(get_key_val());
continue;
}
//初始化变量
ui_ram.sd_show_number=0; //显示位置
ui_ram.spi_show_number=0; //显示位置
ui_ram.sd_make_number=0;//选择游戏
ui_ram.spi_make_number=0;//选择游戏
///初始化游戏选择界面
ui_selectgame_init_show();
//选择游戏
while(1)
{
//摇杆方向
ui_adckey.now_adckey = five_way_key_scan();
if( ui_adckey.last_adckey != ui_adckey.now_adckey)
{
ui_adckey.last_adckey = ui_adckey.now_adckey;
if(ui_adckey.last_adckey == adckey_null) continue;
if(ui_adckey.last_adckey & adckey_left_right) continue;
key_wav_motor();
if(ui_adckey.last_adckey & adckey_up_down)
{
ui_ram.now_game=0xff; //变化
}
}
///选择游戏变化界面
ui_selectgame_last_show();
//进入游戏
ui_key.now_key = get_key_val();
if( ui_key.last_key != ui_key.now_key)
{
ui_key.last_key = ui_key.now_key;
if(ui_key.last_key == key_null) continue;
key_wav_motor();
if(ui_key.last_key & key_exit)
{
///选择界面大图标全部显示
ui_select_init_show();
//while(get_key_val());
break;
}
if(ui_key.last_key & key_enter)
{
//保存历史游戏记录
printf("//////////\r\n");
res = f_open(filin,"1:/LastGame.txt",FA_CREATE_ALWAYS|FA_WRITE);//打开文件 记录
if(res)
{
printf("1:/LastGame.txt FA_WRITE OFF! %d \r\n",res);
LCD_Fill(0,uihint_show_start,240,280,WHITE);
Show_Str(((240-strlen("[记录失败]")*8)/2),uihint_show_line,"[记录失败]",LCEDA,WHITE,16,0);
}
if(ui_ram.now_sdspi) strcpy((char*)&buf_r,(char*)fat_ram.spi_nesname[ui_ram.spi_make_number]);
else strcpy((char*)&buf_r,(char*)fat_ram.sd_nesname[ui_ram.sd_make_number]);
f_write(filin,buf_r,512,&bw);
f_close(filin);//关闭文件
/////开始打开游戏
LCD_Fill(0,uihint_show_start,240,280,WHITE);
Show_Str(0,uihint_show_line,buf_r,LCEDA,WHITE,16,0);
res = f_open(filout,(char *)buf_r,FA_READ);//打开文件
if(res == FR_OK)
{
printf("nes_load start\r\n");
nes_load1(buf_r);
printf("nes_load end\r\n");
}
else
{
//错误提示
printf("//ui_error_show: %s error: %d \r\n",buf_r , res);
ui_error_show((u8 *)ui_show[ui_ram.language][uierror_file_show]);
while(get_key_val());
}
///初始化游戏选择界面
ui_selectgame_init_show();
while(get_key_val());
}
}
//小图标显示
rtc_last_show();
delay_1ms(10);
}
}
//小图标显示
rtc_last_show();
delay_1ms(10);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
5.NES游戏机功能代码【上次游戏】说明
上次游戏功能主要为打开之前记录的游戏路径,快速开始游戏。
///上次游戏
void game_last_main(void)
{
u8 res;
//打开文件
res = f_open(filout,"1:/LastGame.txt",FA_READ);//打开文件
if(res)
{
printf("1:/LastGame.txt FA_READ OFF! %d \r\n",res);
//错误提示
ui_error_show((u8 *)ui_show[ui_ram.language][uierror_file_show]);
///主界面大图标全部显示
ui_main_init_show();
while(get_key_val());
}
else
{
//读取文件
f_read(filout,buf_r,512,&br);
LCD_Fill(0,uihint_show_start,240,280,WHITE);
Show_Str(0,uihint_show_line,buf_r,LCEDA,WHITE,16,0);
printf("f_read %s \r\n",buf_r);
//开始游戏
res = f_open(filout,(char *)buf_r,FA_READ);//打开文件
if(res == FR_OK)
{
printf("nes_load start\r\n");
nes_load1(buf_r);
printf("nes_load end\r\n");
}
else
{
//错误提示
ui_error_show((u8 *)ui_show[ui_ram.language][uierror_file_show]);
while(get_key_val());
}
///主界面大图标全部显示
ui_main_init_show();
//while(get_key_val());
}
f_close(filout);//关闭文件
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
6.NES游戏机功能代码【管理游戏】说明
管理游戏功能主要为管理游戏机内的nes游戏,分为复制SD卡游戏到spi flash内和删除spi flash内的nes游戏。
///管理游戏
void game_manage_main(void)
{
u8 res;
//必须SPI flash 文件系统存在
if((fat_ram.spi_flash == 0x81))
{
//检索SD卡游戏
sd_file_search();
//检索SPI游戏
spiflash_file_search();
//初始化变量
ui_ram.sd_show_number=0; //显示位置
ui_ram.spi_show_number=0; //显示位置
ui_ram.sd_make_number=0;//选择游戏
ui_ram.spi_make_number=0;//选择游戏
ui_ram.last_nes_sdspi=0;//选择
ui_ram.now_nes_sdspi=0;//选择
//复制删除游戏文件 初始化显示
ui_copygame_init_show();
while(1)
{
//操作按键 选择文件和切换功能
ui_adckey.now_adckey = five_way_key_scan();
if( ui_adckey.last_adckey != ui_adckey.now_adckey)
{
ui_adckey.last_adckey = ui_adckey.now_adckey;
if(ui_adckey.last_adckey == adckey_null) continue;
key_wav_motor();
if(ui_adckey.last_adckey & adckey_up_down)
{
ui_ram.now_game=0xff;
}
else if(ui_adckey.last_adckey == adckey_left) //左
{
if(ui_ram.now_nes_sdspi) ui_ram.now_nes_sdspi=0;
}
else if(ui_adckey.last_adckey == adckey_right) //右
{
if(ui_ram.now_nes_sdspi == 0) ui_ram.now_nes_sdspi=0x01;
}
else;
}
//复制删除游戏文件 切换显示
ui_copygame_last_show();
//进入游戏
ui_key.now_key = get_key_val();
if( ui_key.last_key != ui_key.now_key)
{
ui_key.last_key = ui_key.now_key;
if(ui_key.last_key == key_null) continue;
key_wav_motor();
if(ui_key.last_key & key_exit) //返回
{
///主界面大图标全部显示
ui_main_init_show();
//while(get_key_val());
break;
}
if(ui_key.last_key & key_enter)
{
if(ui_ram.now_nes_sdspi)
{//删除文件
if(fat_ram.spi_flash == 0x81)
{
f_unlink((const char*)fat_ram.spi_nesname[ui_ram.spi_make_number]);
while(exf_getfree("1:",&fat_ram.spi_total,&fat_ram.spi_free))delay_1ms(10);
//检索SPI游戏
spiflash_file_search();
//位置归零
//ui_ram.spi_show_number=0;
//ui_ram.spi_make_number=0;
//位置继承
if(ui_ram.spi_show_number) ui_ram.spi_show_number--;
if(ui_ram.spi_make_number) ui_ram.spi_make_number--;
//变化spi文件后刷新
ui_spigame_show();
}
}
else
{//复制文件
if((fat_ram.sd_card == 0x81) || (fat_ram.spi_flash == 0x81))
{
//文件复制
strcpy((char*)&pname,"1:/nes/");
strcat((char*)&pname,(const char*)fat_ram.sd_nesname[ui_ram.sd_make_number]+7);//将文件名接在后面
res=f_open(filout,(char*)pname,FA_READ);//打开文件
if(res)
{
//复制文件
copy_file_file(fat_ram.sd_nesname[ui_ram.sd_make_number],pname);
///////列表更新
while(exf_getfree("1:",&fat_ram.spi_total,&fat_ram.spi_free))delay_1ms(10);
//检索SPI游戏
spiflash_file_search();
//位置归零
//ui_ram.spi_show_number=0;
//ui_ram.spi_make_number=0;
//位置继承
//变化spi文件后刷新
ui_spigame_show();
}
}
}
}
else;
}
//小图标显示
rtc_last_show();
delay_1ms(10);
}
}
else
{
//错误提示
ui_error_show((u8 *)ui_show[ui_ram.language][uierror_file_show]);
///主界面大图标全部显示
ui_main_init_show();
while(get_key_val());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
7.NES游戏机功能代码【设置】说明
设置功能主要为设置语言,音量,亮度,震动,计时等功能的设置。
//游戏设置
void game_set_main(void)
{
//变量初始化
ui_ram.now_set=0;
ui_ram.last_set=0;
//设置界面全刷新显示
ui_set_init_show();
while(1)
{
//主界面选择
ui_adckey.now_adckey = five_way_key_scan();
if( ui_adckey.last_adckey != ui_adckey.now_adckey)
{
ui_adckey.last_adckey = ui_adckey.now_adckey;
if(ui_adckey.last_adckey == adckey_null) continue;
if(ui_adckey.last_adckey & adckey_left_right) continue;
key_wav_motor();
if(ui_adckey.last_adckey & adckey_up_down)
{
if(ui_adckey.last_adckey & 0x10)
{
if(ui_ram.now_set) ui_ram.now_set--;
}
else if(ui_adckey.last_adckey & 0x20)
{
if(ui_ram.now_set < (uiset_number-1)) ui_ram.now_set++;
}
else;
}
}
///确定进入功能
ui_key.now_key = get_key_val();
if(ui_key.last_key != ui_key.now_key)
{
ui_key.last_key = ui_key.now_key;
if(ui_key.last_key == key_null) continue;
key_wav_motor();
if(ui_key.last_key & key_exit)
{
///主界面大图标全部显示
ui_main_init_show();
//while(get_key_val());
break;
}
else
{
ui_ram.now_makeset=0xff;//变化标志
}
}
///变化显示
ui_set_last_show();
//小图标显示
rtc_last_show();
delay_1ms(10);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
8.示例代码工程
👉暂时只支持git仓库下载工程源码