这一篇说说跟NES文件格式相关的内容。

NES文件,在真机上相当于就是游戏卡带了。

下面是NES文件格式的说明表。


偏移

字节数

内容

0-3

4

字符串“NES^Z”用来识别.NES文件

4

1

16kB ROM的数目

5

1

8kB VROM的数目

6

1

D0:1=垂直镜像,0=水平镜像



D1:1=有电池记忆,SRAM地址$6000-$7FFF



D2:1=在$7000-$71FF有一个512字节的trainer



D3:1=4屏幕VRAM布局



D4-D7:ROM  Mapper的低4位

7

1

D0-D3:保留,必须是0(准备作为副Mapper号^_^)



D4-D7:ROM  Mapper的高4位

8-F

8

保留,必须是0

16-

16KxM

ROM段升序排列,如果存在trainer,它的512字节摆在ROM段之前

-EOF

8KxN

VROM段, 升序排列


结合代码来看看程序中是如何读取NES文件的。

代码所在的文件:NES\ROM.cpp ROM.h

所在函数:ROM::ROM( constchar* fname )

代码比较长,所以分开来一部分一部分的看,有些无关紧要,或者和NES文件格式主题关系不大的代码,我就略过了。

FILE         *fp = NULL;
LPBYTE  temp = NULL;
LPBYTE  bios = NULL;
LONG       FileSize;
ZEROMEMORY( &header, sizeof(header) );
ZEROMEMORY( path, sizeof(path) );
ZEROMEMORY( name, sizeof(name) );
bPAL = FALSE;
bNSF = FALSE;
NSF_PAGE_SIZE = 0;
lpPRG = lpCHR = lpTrainer = lpDiskBios= lpDisk = NULL;
crc = crcall = 0;
mapper = 0;
diskno = 0;


一些变量的初始化,具体含义用到了再详细研究。


if( !(fp = ::fopen( fname, "rb")) )
{
     LPCSTR   szErrStr = CApp::GetErrorString( IDS_ERROR_OPEN );
     ::wsprintf( szErrorString,szErrStr, fname );
     throw  szErrorString;
}

打开文件,fp是文件句柄


::fseek( fp, 0,SEEK_END );
FileSize = ::ftell(fp );
::fseek( fp, 0,SEEK_SET );


求文件大小,赋值给FileSize


if( FileSize < 17 )
{
throw        CApp::GetErrorString(IDS_ERROR_SMALLFILE );
}

NES文件头大小16,文件中至少得有个文件头才好继续。



if( !(temp = (LPBYTE)::malloc( FileSize )) )
{
throw        CApp::GetErrorString( IDS_ERROR_OUTOFMEMORY);
}
if( ::fread( temp, FileSize, 1, fp ) != 1 )
{
throw        CApp::GetErrorString( IDS_ERROR_READ );
}
FCLOSE( fp );

文件中的所有内容读取出来放到temp缓冲区中,之后关闭文件。


::memcpy(&header, temp, sizeof(NESHEADER) );

读取NES文件头,存储到header中。NESHEADER定义如下:

可以结合文章开头的表格查看。

typedefstruct         tagNESHEADER{
         BYTE       ID[4];  //NES文件标记
         BYTE       PRG_PAGE_SIZE;  //16kB ROM的数目就是ROM大小
         BYTE       CHR_PAGE_SIZE;  //8kB VROM的数目就是VROM大小
         BYTE       control1;      
         BYTE       control2;
         BYTE       reserved[8];   //保留,必须是0
} NESHEADER;




DWORD   PRGoffset, CHRoffset;  //Rom和VRom数据距离文件头的偏移量
LONG       PRGsize, CHRsize;            //Rom和VRom的大小


根据文件类型的不同(NES,FDS等),接下来的代码产生了分支,我就只管NES了(NES还没搞明白,其它就暂时无视吧)。

if( header.ID[0] == 'N'&& header.ID[1] == 'E'
                    && header.ID[2] == 'S' && header.ID[3] == 0x1A )
{
    PRGsize =(LONG)header.PRG_PAGE_SIZE*0x4000;
    CHRsize =(LONG)header.CHR_PAGE_SIZE*0x2000;
    PRGoffset = sizeof(NESHEADER); //ROM数据紧跟在文件头后面
    CHRoffset =PRGoffset + PRGsize; //VROM数据紧跟在ROM数据后面
    if( PRGsize <= 0 || (PRGsize+CHRsize) >FileSize )
    {
        throw CApp::GetErrorString(IDS_ERROR_INVALIDNESHEADER );
    }
    if( !(lpPRG = (LPBYTE)malloc( PRGsize )) )
    {
    throw CApp::GetErrorString(IDS_ERROR_OUTOFMEMORY );
    }
    ::memcpy( lpPRG,temp+PRGoffset, PRGsize );
    if( CHRsize > 0 )
    {
        if( !(lpCHR = (LPBYTE)malloc( CHRsize )) )
        {
            throw CApp::GetErrorString(IDS_ERROR_OUTOFMEMORY );
        }
        if( FileSize >= CHRoffset+CHRsize )
        {
             memcpy(lpCHR, temp+CHRoffset, CHRsize );
        }
        else
        {
             CHRsize-= (CHRoffset+CHRsize - FileSize);
             memcpy(lpCHR, temp+CHRoffset, CHRsize );
        }
    }
    else
    {
        lpCHR =NULL;
    }
}



以上一长串的代码,说白了就是,检查文件数据有没有异常,没有异常就读取ROM和VROM。

string         tempstr;
tempstr =CPathlib::SplitPath( fname );
::strcpy( path,tempstr.c_str() );
tempstr =CPathlib::SplitFname( fname );
::strcpy( name,tempstr.c_str() );
::strcpy( fullpath, fname );


path保存NES文件所在的目录

name保存NES文件名

fullpath 保存NES文件的全路径



接下来的代码处理的是和mapper有关的东西。mapper这玩意儿水比较深,况且和NES文件格式这个主题关系不大,今后再细细研究吧。