[原创] MAME中的地址映射(Address Mapping)与内存库切换(Bank Switching):基础篇

在本文中,我将简单介绍一下MAME中Memory Mapping和Bank Switching的基本概念,其具体内部实现将在下一篇文档中叙述。

1.MAME驱动中的内存映射定义

先简单介绍一下MAME中内存映射的基本概念。在模拟过程中,给定CPU寻址空间当中的某一合法地址,CPU模拟核心必须知道自己该到哪里去获取其地址所对应的数据(这里的数据可以是指令、操作数、也可以是统一编址的I/O”端口”)。一般而言,在最简单的情况下,我们可以在模拟器中直接开辟一个庞大的数组来模拟整个CPU的寻址空间,这个数组中的每一个字直接对应着寻址空间中相应地址位置的数据;不难看出,这种做法是低效的,且不论CPU寻址空间有可能非常大(32位地址位宽的话,便是4个G的寻址空间),而且在多数场合下,这个寻址空间中所存放的数据有可能是稀疏的,更为特殊的是,在嵌入式系统中,对于某地址的读写操作将有可能触发除读写之外的其他操作,如触发中断、ROM切换和控制视频/音频硬件等,这些多种多样的额外操作显然比我们所想象的单纯读写要复杂得多。

为提高存储效率,同时提供足够的灵活性,MAME采用了内存地址映射(Address Mapping)的方式来实现上述寻址-读写过程。首先,可以将被模拟CPU的整个寻址空间划分为若干地址范围,其中有一些是在程序执行的过程中要实际用到的,而另一些地址范围则有可能从未使用过;然后,将其中可能用到的地址范围统统挑选出来,假设为{Ai},为每一个地址范围Ai分别指定一个访存函数Fi,当CPU核心需要读、写某个地址范围Ai中的数据时,其所对应的Fi将会被CPU核心调用,而由Fi来完成具体的读取或写入操作。这种做法避免了寻址空间的稀疏问题,同时也非常灵活:如果某读写操作还将同时触发其他的操作,那么便把那些其他操作都写在其地址所对应的访存函数里面好了,最后,其还为我们的实现提供了方便:没有必要一次性分配一个超级大数组来模拟整个寻址空间了,我们完全可以将其中的某一个地址范围用数组来模拟,而其他的地址范围则用链表或哈希表方式来模拟,甚至直接使用变量来存储都行,其内存数据的存储方式完全取决于访存函数的具体实现。不难发现,地址映射方式显然比分配大数组方式要优雅的多,但是,这种方式也有其固有的缺陷,那便是模拟寻址操作(即匹配地址范围)和数据读写操作(即调用访存函数)的速度,如果处理不当,低效的寻址和高频率的访存很容易拖慢CPU核心的运行速度,成为整个模拟系统的瓶颈。由于这一问题涉及到MAME中内存映射机制的内部实现,因此它不是本文的重点,我将在下一篇文章中对其进行详细介绍。在本文中,我将主要从模拟驱动定义的角度、结合具体的驱动实例来简要阐述一下地址映射机制和内存库切换的基本概念。

仍以经典游戏双截龙(Double Dragon 2)为例,让我们来看一个具体的地址映射实例。在drivers/ddragon.c中,查找启动代码MACHINE_DRIVER_START( ddragon2 ),其片段如下:

static MACHINE_DRIVER_START( ddragon2 )
    
/* basic machine hardware */
    MDRV_CPU_ADD(HD6309, 
3579545)   /* CPU1: 3.579545 MHz */
    MDRV_CPU_PROGRAM_MAP(dd2_readmem,dd2_writemem)
    MDRV_CPU_VBLANK_INT(ddragon_interrupt,272)

    MDRV_CPU_ADD(Z80,
12000000 / 3)  /* CPU2: 4 MHz */
    MDRV_CPU_PROGRAM_MAP(dd2_sub_readmem,dd2_sub_writemem)

    
/* audio CPU */
    MDRV_CPU_ADD(Z80, 
3579545)      /* CPU3: 3.579545 MHz */
    MDRV_CPU_PROGRAM_MAP(dd2_sound_readmem,dd2_sound_writemem)

    ......
MACHINE_DRIVER_END

上诉代码片段说明,ddragon2的游戏基板一共有3个CPU需要模拟,其中的HD6309为主CPU;与此同时,dd2_readmem和dd2_writemem这两个内存映射表将与主CPU相绑定,前者记录了读操作中地址与访问函数的关系,而后者则保存了写操作中地址与存储函数之间的映射关系(访存函数是访问、存储函数的通称)。下面来具体看看这两张表,查找dd2_readmem和dd2_writemem,得到下列代码:

static ADDRESS_MAP_START( dd2_readmem, ADDRESS_SPACE_PROGRAM, 8 )
    AM_RANGE(
0x00000x1fff) AM_READ(MRA8_RAM)
    AM_RANGE(
0x20000x2fff) AM_READ(ddragon_spriteram_r)
    AM_RANGE(
0x30000x37ff) AM_READ(MRA8_RAM)
    AM_RANGE(
0x38000x3800) AM_READ(input_port_0_r)
    AM_RANGE(
0x38010x3801) AM_READ(input_port_1_r)
    AM_RANGE(
0x38020x3802) AM_READ(port4_r)
    AM_RANGE(
0x38030x3803) AM_READ(input_port_2_r)
    AM_RANGE(
0x38040x3804) AM_READ(input_port_3_r)
    AM_RANGE(
0x3c000x3fff) AM_READ(MRA8_RAM)
    AM_RANGE(
0x40000x7fff) AM_READ(MRA8_BANK1)
    AM_RANGE(
0x80000xffff) AM_READ(MRA8_ROM)
ADDRESS_MAP_END

static ADDRESS_MAP_START( dd2_writemem, ADDRESS_SPACE_PROGRAM, 8 )
    AM_RANGE(
0x00000x17ff) AM_WRITE(MWA8_RAM)
    AM_RANGE(
0x18000x1fff) AM_WRITE(ddragon_fgvideoram_w) AM_BASE(&ddragon_fgvideoram)
    AM_RANGE(
0x20000x2fff) AM_WRITE(ddragon_spriteram_w) AM_BASE(&ddragon_spriteram)
    AM_RANGE(
0x30000x37ff) AM_WRITE(ddragon_bgvideoram_w) AM_BASE(&ddragon_bgvideoram)
    AM_RANGE(
0x38080x3808) AM_WRITE(ddragon_bankswitch_w)
    AM_RANGE(
0x38090x3809) AM_WRITE(MWA8_RAM) AM_BASE(&
ddragon_scrollx_lo)
    AM_RANGE(
0x380a0x380a) AM_WRITE(MWA8_RAM) AM_BASE(&ddragon_scrolly_lo)
    AM_RANGE(
0x380b0x380f) AM_WRITE(ddragon_interrupt_w)
    AM_RANGE(
0x3c000x3dff) AM_WRITE(paletteram_xxxxBBBBGGGGRRRR_split1_w) AM_BASE(&paletteram)
    AM_RANGE(
0x3e000x3fff) AM_WRITE(paletteram_xxxxBBBBGGGGRRRR_split2_w) AM_BASE(&paletteram_2)
    AM_RANGE(
0x40000xffff) AM_WRITE(MWA8_ROM)
ADDRESS_MAP_END

注意,ADDRESS_MAP_START、AM_RANGE等宏其实是一个地址映射表构造函数中若干代码片段的缩写形式,它们主要是为了方便驱动开发者的使用而定义的。其中,ADDRESS_MAP_START宏中的3个参数分别给出了所定义的内存映射表的名称、地址映射类型和数据访问位长;地址映射类型一共可以分为代码地址空间(ADDRESS_SPACE_PROGRAM)、数据地址空间(ADDRESS_SPACE_DATA)和IO地址空间(ADDRESS_SPACE_IO)3种;而数据访问位长则可为8、16、32和64等值。比如,上述代码定义了名为dd2_readmem、dd2_writemem的两张地址映射表,二者的类型皆为代码地址空间类型,且其中的数据均必须按照8位字节方式访存。

我们可以将一张地址映射表看成由若干映射表项构成,其中的每一项则记录了地址映射的范围、及该地址范围所对应的访存函数(accessor/handler)。比如,在dd2_readmem中,AM_RANGE(0x2000,0x2fff)一行表明,读地址0x2000-0x2ffff所对应的访问函数为ddragon_spriteram_r;当HD6309 CPU核心需要读0x2000-0x2ffff地址范围之间的某字节时,假定该字节地址为x(0x2000≤x≤0x2fff),则ddragon_spriteram_r函数将会被CPU核心所(间接地)调用,同时该字节地址的相对偏移址(x-0x2000)将作为调用参数传递给访问函数ddragon_spriteram_r,而由ddragon_spriteram_r的内部实现来决定究竟该到何处去获取该字节所对应的内容,并最终返回给CPU核心。同理,从表dd2_writemem中,我们可以看到存在着AM_RANGE(0x3808,0x3808)这么一项,其说明,当CPU core需要在地址0x3808处写入一字节时,它将调用存储函数 ddragon_bankswitch_w,并且把要写入的值传递给ddragon_bankswitch_w。

在AM_WRITE宏中,也可以直接以MWA8_RAM/ MWA8_ROM等专门定义的标志位来替代访存函数,并利用AM_BASE宏,将某个数组或者变量与地址映射范围直接绑定,如dd2_writemem表中的AM_RANGE(0x3809, 0x3809),后继的AM_READ和AM_BASE宏调用表明,所有写往地址0x3809的8位字节均将被直接写入变量ddragon_scrollx_lo中;也就是说,在无需额外操作的情况下,存储函数是可以省略的。


2.  MAME驱动中的内存库切换(Bank Switching)

先谈谈什么是Banking操作。一般而言,游戏基板上的ROM数据可以是固化的可执行代码、图像数据或者是音频数据,我们必须首先将执行所需的代码和数据加载到地址空间中来,然后方可让CPU开始执行;但不幸的是,嵌入式CPU的寻址空间是有限的,我们也许并不能够将所有ROM数据一次性地全部加载进来,因此,必须引入相应的动态ROM加载机制,即Bank Switching。以ddragon2为例,在这个游戏基板中,主CPU的可执行代码总共包括了4个ROM,其中的1个ROM为常驻代码,即它必须一直保留在对应CPU寻址空间中,映射在固定的地址范围处;而另外3个ROM则为Bank Roms,只有在需要执行它们的时候,这些Rom才会被映射至适当的寻址空间处。当然,动态地加载代码ROM只是ddragon2的做法,实际上,Bank Roms也并不一定都必须是代码,它们也可以是声音、图像等数据ROM,这取决于游戏本身的实现。即,Bank Switching是一种将ROM动态加载至CPU寻址空间的通用机制。

下面来看看Double Dragon 2模拟中Bank Switching的具体过程。首先,我们可以从drivers/ddragon.c中查找到ddragon2的Romset加载列表,如下列代码片段所示。由其中可见,26a9-04.bin、26aa-03.bin、26ab-0.bin和26ac-0e.63等4个rom是用于主CPU执行的ROM代码,即,在长度为0x28000且名为REGION_CPU1的一个数组中,依次存放了上述4个rom的内容,这些是ROM_REGION宏所给出的信息;另一方面,再来看看ROM_LOAD宏,为便于理解,这里我们必须结合前文的读地址映射表dd2_readmem一起来看,比如26a9-04.bin这个文件,第一个0x08000表明是主CPU中的所对应的加载地址(亦即在REGION_CPU1中的偏移位置),而第二个0x8000则为文件的长度,即这个长达0x8000的文件将被映射至主CPU寻址空间的0x8000-0xffff处,同时我们注意到,该段地址范围在表dd2_readmem中被标记为MRA8_ROM;继续往下看,你将会很惊讶地发现,26aa-03.bin等后面3个rom在主CPU中的映射地址居然是超出了其16位寻址空间的(HD6308的地址总线位宽为16位,换而言之,寻址空间应该是0x0-0xffff)!不必惊慌,其实这里只要结合Bank-Switching的基本概念稍微思考一下便很容易理解:实际上,26a9-04.bin中的代码内容便是上文所说的常驻代码,而另外3个.bin文件则是所谓的Bank Roms,其只有在运行时才会被映射至真实寻址空间中合适的位置上来;那么,这些Bank Rom究竟会被加载到哪里呢?再回过头来看一下表dd2_readmem,从中不难发现从0x4000到0x7fff的地址范围被标记为MRA8_BANK1,这便是我们要找的位置了,其长度正好跟26aa-03.bin等3个Bank rom的长度一致,均分别为0x8000字节,也就是说,当被模拟的程序运行时,26aa-03.bin等3个Bank rom将被有选择性地映射至地址空间0x4000-0x7fff处。

ROM_START( ddragon2 )
    ROM_REGION( 
0x28000, REGION_CPU1, 0 )    /* 64k for code + bankswitched memory */
    ROM_LOAD( 
"26a9-04.bin",  0x080000x8000, .... )
    ROM_LOAD( 
"26aa-03.bin",  0x100000x8000, .... ) /* banked at 0x4000-0x8000 */
    ROM_LOAD( 
"26ab-0.bin",   0x180000x8000, .... ) /* banked at 0x4000-0x8000 */
    ROM_LOAD( 
"26ac-0e.63",   0x200000x8000, .... ) /* banked at 0x4000-0x8000 */

    ROM_REGION( 
0x10000, REGION_CPU2, 0 ) /* sprite CPU 64kb (Upper 16kb = 0) */
    ......

    ROM_REGION( 
0x10000, REGION_CPU3, 0 ) /* music CPU, 64kb */
    ......
ROM_END

那么,Bank Rom又是如何被加载至寻址空间中来的呢?再一次的,我们又得在地址映射表中来寻找答案了,答案就在写地址映射表dd2_writemem中,注意到其中有一个存储函数名为ddragon_bankswitch_w 的映射表项,当主CPU所运行的程序在地址0x3808处写入一字节(data)时,该函数将被调用,继而触发Bank-Switching操作。下面具体来看看ddragon_bankswitch_w究竟是怎么写的:

static WRITE8_HANDLER( ddragon_bankswitch_w )
{
    UINT8 
*RAM = memory_region(REGION_CPU1);
    
    ddragon_scrolly_hi 
= ( ( data & 0x02 ) << 7 );
    ddragon_scrollx_hi 
= ( ( data & 0x01 ) << 8 );
    flip_screen_set(
~data & 0x04);
    
    /* bit 3 unknown */
    
if (data & 0x10)
        dd_sub_cpu_busy 
= 0x00;
    
else if (dd_sub_cpu_busy == 0x00)
        cpunum_set_input_line(
1,sprite_irq,(sprite_irq==INPUT_LINE_NMI)?PULSE_LINE:HOLD_LINE);
    
    
//将dd2_readmem中的MRA8_BANK1区域动态地映射到REGION_CPU1块中的对应位置
    
//具体映射到REGION_CPU1的哪个位置,取决于运行时的CPU核心在0x3808地址处写入的是何值(即data)
    //第一个参数”1”对应着dd2_readmem中的MRA8_BANK1标志
    memory_set_bankptr(1 , &RAM[ 0x10000 + ( 0x4000 * ( ( data & 0xe0>> 5 ) ) ] ); 

    bank_data
=data;
}

这个函数应该不难看懂。往地址0x3808处写入的这一个字节(data)是非常有讲究的,它的最低两位(第0位,第1位)分别代表着游戏画面在x、y轴两个方向上的滚动值,第2位是个屏幕翻转绘制的开关,而第4位则用于CPU1、CPU2之间的同步!最后,其最高3位(data & 0xe0)是我们最关心的,它直接决定了该将那一个Bank Rom切换至标志为MRA8_BANK1的地址空间中。至此,整个ddragon2中的Bank Switching过程便很清楚了。

最后,简要总结一下ddragon2中的内存库切换过程:

  1. 首先,主CPU中的程序开始运行,其初始的地址位置肯定是在0x8000-0xffff之间,因为只有这里才是常驻的ROM代码
  2. 当程序向地址0x3808写入一个字节(data)时,将触发一次Bank Switching操作
  3. 系统根据data的最高3位来决定将哪一个Bank Roms切换至地址空间0x4000-0x7ffff处
  4. 接下来,程序便可以访问或执行0x4000-0x7fff之间的代码了
posted @ 2007-05-10 21:18  neoragex2002  阅读(3463)  评论(32编辑  收藏  举报