Address Maps

From MAMEDEV Wiki

Address maps define how the address space of a CPU is layed out. This article aims to explain how address maps are declared and modified. Before reading this article, you might also want to check out CPUs and Address Spaces.

Address Map Structure

A typical address map looks like this (this example is taken from the qix.c driver):

static ADDRESS_MAP_START( main_map, ADDRESS_SPACE_PROGRAM, 8 )
    AM_RANGE(0x8000, 0x83ff) AM_RAM AM_SHARE(1)
    AM_RANGE(0x8400, 0x87ff) AM_RAM
    AM_RANGE(0x8800, 0x8bff) AM_READNOP   /* 6850 ACIA */
    AM_RANGE(0x8c00, 0x8c00) AM_MIRROR(0x3fe) AM_READWRITE(qix_video_firq_r, qix_video_firq_w)
    AM_RANGE(0x8c01, 0x8c01) AM_MIRROR(0x3fe) AM_READWRITE(qix_data_firq_ack_r, qix_data_firq_ack_w)
    AM_RANGE(0x9000, 0x93ff) AM_READWRITE(pia_3_r, pia_3_w)
    AM_RANGE(0x9400, 0x97ff) AM_READWRITE(pia_0_r, qix_pia_0_w)
    AM_RANGE(0x9800, 0x9bff) AM_READWRITE(pia_1_r, pia_1_w)
    AM_RANGE(0x9c00, 0x9fff) AM_READWRITE(pia_2_r, pia_2_w)
    AM_RANGE(0xa000, 0xffff) AM_ROM
ADDRESS_MAP_END

As you can see, it relies strongly on macros to do the heavy lifting. In the current implementation, the macros define a series of tokens which are decoded at runtime into the final structure. This allows for efficient packing of the data.

Each address map starts with an ADDRESS_MAP_START declaration. This declaration takes 3 parameters. The first parameter (main_map) is the name of the variable you are defining. Each memory map is associated with a variable name so that you can reference it in your machine configuration. The second parameter (ADDRESS_SPACE_PROGRAM) simply specifies which address space the memory map is intended for. This helps MAME ensure that you don't mix memory maps inappropriately. The final parameter (8) is the data bus width, which again is used as a cross-check against the CPU's defined data bus width for the address space you are working with.

Following the ADDRESS_MAP_START declaration is a list of address ranges. Each range starts with a begin/end address pair wrapped in an AM_RANGE macro, followed by a series of macros that describe how to handle memory accesses within that range. The details of each macro will be described in detail below.

Finally, there is an ADDRESS_MAP_END macro which ties everything up.

A few general comments about the address map above:

  • First, note that this address map has everything listed in nice ascending order. This is not required, though it is usually recommended for readability.
  • Second, note that there are no overlapping ranges. This is also not a requirement. Entries in the address map are always processed in reverse order, starting from the bottom and working up to the top. So any overlapping ranges which appear earlier in the list will take precedence over ranges which appear later. In general, however, using overlapping ranges is discouraged because it can be confusing to parse without understanding the details of how address maps are built up.

In general, each address space associated with a CPU is allowed to specify two address maps. The reason for this is historical: in older versions of MAME, there were separate address maps for reads and writes. When this distinction was removed, there was no coordinated effort to merge all memory maps into a single structure. Over time, however, this is happening and someday you may only be allowed to specify a single address map per CPU address space.

Address Map Macros

Below is a comprehensive list of the supported macros and what they mean.

AM_RANGE

AM_RANGE(start, end) 

The primary purpose of this macro is to declare a memory range. Any AM_* macros which follow implicitly apply to the most recently declared range. The AM_RANGE macro takes two parameters which specify an inclusive range of consecutive addresses beginning with start and ending with end (that is, an address hits in this bucket if the address >= start and address <= end).

AM_READ, AM_WRITE, AM_READWRITE

AM_READ(readhandler)
AM_WRITE(writehandler)
AM_READWRITE(readhandler, writehandler)

These macros provide pointers to functions that will be called whenever a read or write within the current range is detected. The actual prototypes and behaviors of the readhandler and writehandler functions will be described later. However, it is important to note that there is strict typechecking on the function pointers, especially in terms of data bus width, to prevent you from specifying a 16-bit readhandler in an 8-bit address map (recall that the data bus width of the address map was specified in the ADDRESS_MAP_START macro).

Instead of passing the raw address to the read/write handlers, the memory system actually passes an offset relative to the start address provided in the AM_RANGE macro. This allows for common handlers regardless of where the component is actually mapped in the address space.

In addition to regular function pointers, a small number of static identifiers are also permitted. For example, in an 8-bit address map, you can specify a readhandler of SMH_RAM to specify a dynamically allocated region of RAM, or a writehandler of SMH_UNMAP to specify that the current address range is unmapped for writes. More information on the supported static handler types is provided later.

The AM_READWRITE macro is really just a shortcut for AM_READ followed by AM_WRITE.

AM_READn, AM_WRITEn, AM_READWRITEn

AM_READ8(readhandler, shift)
AM_WRITE8(writehandler, shift)
AM_READWRITE8(readhandler, writehandler, shift)
AM_READ16(readhandler, shift)
AM_WRITE16(writehandler, shift)
AM_READWRITE16(readhandler, writehandler, shift)
AM_READ32(readhandler, shift)
AM_WRITE32(writehandler, shift)
AM_READWRITE32(readhandler, writehandler, shift)

These variants of the standard AM_READ and AM_WRITE macros allow you to specify a read or write handler for a data bus width smaller than the data bus width of the current address space. By default, read and write handlers must be sized equal to the data bus width; for example, on a 32-bit wide data bus, you must specify a READ32_HANDLER function. However, it is common to use peripheral devices with smaller data bus widths on systems that have a wide data bus. In general, when this is done, one of two approaches is used.

The simpler approach is simply to map only a subset of the bits to that peripheral. This is most commonly done on arcade hardware where backwards compatibility is not important. So if you had, for example, an 8-bit device on a 32-bit data bus, the hardware designer could choose any of four possible byte lanes to position those 8 bits on, shifted either 0, 8, 16, or 24 bits. Whenever a read or write to that address occurs, only 8 of the 32 bits are provided by the device and all other bits are effectively unmapped. In this case, you would use the AM_READ8 macro and provide a shift value, indiciating how much to shift the 8-bit value to locate it in the right bits.

The second approach is more complicated, and involves "packing" the address space of the device into the data bus width. This is far more common on computers where backwards compatbility is important. In the example above, instead of each 32-bit access only generating a single 8-bit access to the device, it could be packed such that a single 32-bit read actually generates 4 separate 8-bit accesses and packs the results into a single 32-bit result. To describe this situation, you could use the AM_READ8 macro with a special shift value of SHIFT_PACKED.

AM_READ_PORT

AM_READ_PORT(tag)

This macro is an alternate way of specifying a read handler for an input port. Since it is preferred that input ports are referenced by tag, you can use this macro to have MAME automatically look up the tagged port and substitute the correct input port handler.

AM_DEVREAD, AM_DEVWRITE, AM_DEVREADWRITE

AM_DEVREAD(type, tag, readhandler)
AM_DEVWRITE(type, tag, writehandler)
AM_DEVREADWRITE(type, tag, readhandler, writehandler)

These three macros follow the same pattern as the previous set of macros, except that they are used for device-specific read/write handlers. The only difference between a regular read/write handler and a device read/write handler is that the former is passed a pointer to the currently live running_machine, while the latter is passed a pointer to a specific device. The intention here is that if you have allocated a device in your machine's configuration (via MDRV_DEVICE_ADD), then the read/write handlers appropriate for that device should be invoked with a reference to that devices rather than a global pointer to the machine.

To specify which device you wish to pass to the read/write handler, you provide the device's type and tag, which is used to look up the device.

AM_DEVREADn, AM_DEVWRITEn, AM_DEVREADWRITEn

AM_DEVREAD8(type, tag, readhandler, shift)
AM_DEVWRITE8(type, tag, writehandler, shift)
AM_DEVREADWRITE8(type, tag, readhandler, writehandler, shift)
AM_DEVREAD16(type, tag, readhandler, shift)
AM_DEVWRITE16(type, tag, writehandler, shift)
AM_DEVREADWRITE16(type, tag, readhandler, writehandler, shift)
AM_DEVREAD32(type, tag, readhandler, shift)
AM_DEVWRITE32(type, tag, writehandler, shift)
AM_DEVREADWRITE32(type, tag, readhandler, writehandler, shift)

These macros are perfectly analagous to the AM_READn, AM_WRITEn and AM_READWRITEn macros, except that they allow you to specify a device.

AM_MASK

AM_MASK(mask)

Specifies a bitmask which applies to the offset that is passed to the read/write handlers. By default, there is no mask, and the read/write handlers are passed in the raw address minus the start address of the current address range. If a mask is provided, this bitmask is applied in an AND operation after subtracting the start address. Thus, the value passed to the read/write handlers is really ((address - start) & mask).

AM_MIRROR

AM_MIRROR(mirror)

This macro specifies the "mirror mask" for the current address range. There are two ways to understand a mirror mask; hopefully at least one of them makes sense!

  • A hardware-centric interpretation would describe a mirror mask as essentially a bitmask consisting of all bits that are ignored when the address is decoded by the hardware. Most arcade hardware does not fully decode each address; rather, in order to save on chip counts, the hardware is set up to do the minimum necessary work to separate accesses to different components in the system, and many bits are ignored. For example, in Pac-Man, bits 13 and 15 are not used at all when deciding whether an access should be directed to spriteram. Thus, the mirror mask is set as $A000.
  • A software-centric interpretation would be that each bit in the mirror mask describes a "mirror" of the address range at a different address in the system. Looking again at the Pac-Man example, spriteram is traditionally thought of as existing at address $4FF0. But it turns out that you can also access it at $6FF0, $CFF0, and $EFF0, due to the fact that the hardware does not care whether bits 13 and 15 are 0 or 1. So the mirror mask of $A000 means that the memory system will replicate this address range to automatically create these mirrors by going through each bit of the mirror mask and mapping the range with that bit set to 0 and then to 1.

Note that the mirroring is by default completely hidden to the read/write handlers. This is done by making the default AM_MASK value for a mirrored range equal to the logic NOT of the mirror. In the case of Pac-Man above, for example, the mask would be ~$A000 = $5FFF. Looking at an example access to $CFF7, we would subtract the base address of $4FF0, giving an offset of $8007. Then we apply the mask of $5FFF to get the final offset of $0007.

If you want your read/write handler to see the full address with no masking, you can provide an explicit AM_MASK which will override the default value and enable you to specify which bits you wish to see.

AM_REGION

AM_REGION(region, offset)

This macro is only useful if you used AM_READ or AM_WRITE and specified a reference to RAM, ROM, or a BANK. By default, in these cases memory is either allocated (RAM and BANK) or assumed to point to the memory region corresponding to the relevant CPU (ROM). When you use the AM_REGION macro, you are overriding this default behavior, specifying instead a particular memory region and an offset within that region which corresponds to the start address of the memory range.

AM_SHARE

AM_SHARE(index)

This macro has limitations similar to AM_REGION, in that it only makes sense when used with RAM, ROM, or a BANK. However, instead of specifying an explicit memory region and offset, you instead specify a non-zero index. The first memory range that is encountered with an AM_SHARE allocates its memory in the default fashion. However, subsequent ranges which also use AM_SHARE and which reference the same index override this default behavior and point to the exact same memory that the first instance referenced.

This is primarily used to map shared memory between multiple CPUs. For example, if CPU #1 has RAM in the region $4000-$4fff, and CPU #2 has that same RAM mapped in the region $8000-$8fff, you can specify AM_SHARE(1) next to each one. When building up the memory system, AM_SHARE(1) is seen first for CPU #1, and it is allocated as normal RAM. Shortly afterwards, AM_SHARE(1) is see a second time for CPU #2, but instead of allocating memory, we simply point back to the same RAM that was allocated for CPU #1.

Note that multiple independent shared regions can be managed this way, by using a different value for index. Also note that this sharing technique only works between CPUs with the same data bus width (e.g., 8-bit to 8-bit, or 16-bit to 16-bit). If there is shared RAM between, say, an 8-bit CPU and a 16-bit CPU, then you need to write your own handlers to manage that RAM.

AM_BASE, AM_SIZE

AM_BASE(base)
AM_SIZE(size)

These macros are a convenience for driver writers. Since the memory system will allocate memory automatically for certain types of address ranges, you need a way to get ahold of a pointer to that memory so you can examine it. The AM_BASE macro takes a pointer to an appropriately-sized pointer (e.g., a pointer to a UINT8 * for 8-bit data bus), and fills it in after the memory system is initialized with the address of the memory that was allocated. In a similar fashion, the AM_SIZE macro takes a pointer to a size_t and returns in it the size (end + 1 - start) of the range referenced.

AM_BASE_MEMBER, AM_SIZE_MEMBER

AM_BASE_MEMBER(struct, member)
AM_SIZE_MEMBER(struct, member)

These two macros are variants of the standard AM_BASE and AM_SIZE macros which are designed to work in a newer more object-oriented style. Drivers now have a pointer in the running_machine object which contains driver-specific data. But since the memory for this data is allocated dynamically, you cannot use the regular AM_BASE and AM_SIZE macros to point to an address where the pointer or size should be stored. To make this work, you instead use the AM_BASE_MEMBER macro to specify the type of struct that will be allocated and the name of the struct member where you want the data to be stored.

Address Map Shortcuts

To improve readability and also as a programming shortcut, there are several common contracted forms of the above macros.

AM_UNMAP

AM_UNMAP is short for AM_READWRITE(SMH_UNMAP, SMH_UNMAP), which specifies that the given range should be treated as unmapped. This means calling the static unmapped memory handler (SMH is short for Static Memory Handler), which by default logs each unmapped access to the error.log file.

Note that by default the entire address space is considered unmapped, so the ability to explicitly specify an unmapped range is rarely needed except as an explicit override of another range.

AM_RAM

AM_RAM is short for AM_READWRITE(SMH_RAM, SMH_RAM), specifying that the given range should be treated as RAM. It is vital to understand that SMH_RAM and SMH_ROM are special static memory handlers which only have meaning when specified in an address map structure. They cannot be used to dynamically install RAM or ROM; see the section on Runtime Modifications below for more details.

AM_ROM

AM_ROM is short for AM_READ(SMH_ROM), which states that the current range should be treated as ROM for reads. Note that there is no explicit definition of what happens on a write here. There are two reasons for this. First, since all address space is unmapped by default, any writes will by default be processed as unmapped memory and logged to the error.log, which is an appropriate behavior. Second, some systems have write-only devices mapped in the same space as ROM, and leaving the writes undefined here allows this to be easily described.

AM_WRITEONLY

AM_WRITEONLY is a shortcut for AM_WRITE(SMH_RAM). Of course, "write-only" RAM may seem like a silly concept, but in fact there are many cases where only the write logic is connected for a given block of RAM. In general, it is the rest of the system hardware which performs all of the reads of such memory. AM_WRITEONLY is also useful for specifying a register that has no immediate side-effects, and thus doesn't need to call a function. In almost all cases, you will follow AM_WRITEONLY with an AM_BASE or AM_BASE_MEMBER so that you can access the RAM elsewhere in the code.

AM_RAMBANK, AM_ROMBANK

AM_RAMBANK(n) is short for AM_READWRITE(SMH_BANK(n), SMH_BANK(n)). AM_ROMBANK(n) is short for AM_WRITE(SMH_BANK(n)). These macros are used to specify that the current address range refers to a specific bank of memory. MAME supports up to 32 explicitly-specified banks globally within a system. Banked memory can be dynamically toggled at runtime to point to different regions of memory on the hosting system.

AM_NOP, AM_READNOP, AM_WRITENOP

AM_NOP is short for AM_READWRITE(SMH_NOP, SMH_NOP). AM_READNOP is short for AM_READ(SMH_NOP), and AM_WRITENOP is short for AM_WRITE(SMH_NOP). The static "nop" memory handler operates exactly like the unmapped memory handler, except that it does not log to the error.log file. This is handy if you have a particular address range you wish to explicitly ignore and don't want each access to that range to clog up the log file.

Address Map Global Controls

There are two global control values that can be specified in an address map. Generally, these controls are placed at the top of the address map. Regardless of where they are located, however, their effects apply to the entire address space.

ADDRESS_MAP_GLOBAL_MASK

Specifying ADDRESS_MAP_GLOBAL_MASK(maskval) applies the given maskval as a bitmask to all addresses in the range. CPUs generally have many address lines and for some systems, it isn't worth the effort to decode each and every address line in order to determine whether or not a given memory access corresponds to a particular device. In this case, the address line from the CPU is completely ignored, and ADDRESS_MAP_GLOBAL_MASK allows you to specify a bitmask of only those bits which are actually decoded.

A famous example of this is the Z80 I/O address space. In reality, this is a 16-bit address bus, but most systems that used it only decoded the lower 8 bits. So in many cases in MAME you will see ADDRESS_MAP_GLOBAL_MASK(0xff) in the I/O address map.

ADDRESS_MAP_UNMAP_LOW, ADDRESS_MAP_UNMAP_HIGH

These two macros specify explicitly how unmapped (and "nop") reads from the address space are handled. By default, an unmapped read returns 0 (this is actually usually not the case, though the default is left this way for historical reasons). However, on many systems, an unmapped read should return all 1 bits (for example, $FF on an 8-bit data bus). To enable this, simply include an ADDRESS_MAP_UNMAP_HIGH in your address map and the static unmapped memory handler will return all 1's instead of all 0's (which is what you get by default, or if you specify ADDRESS_MAP_UNMAP_LOW).

Runtime Modifications

Debugging Helpers