Guided tour through MESS
MESS is lacking what many software projects, in particular open-source projects, also fail to give: a good introduction to find into the concepts of what you can see there as program code. This is rarely a malevolence of the implemententors; it is usually caused by one or more of these reasons:
- lack of time: We have to fix bug#18274 and have no more time to write the docs
- outdated: Docs have been written with quite some efforts, but since version x.y most of them do not apply any longer, and no one really has the energy to make them catch up (Sisyphus)
- different level of understanding: What may be plain to see for the developer is often cryptic to the beginner.
- hidden knowledge: Similar to the last point, the developer does not even know that he assumes knowledge which the reader need not have.
I'll try to fight the last two points with this guided tour through MESS. We will discuss on the example of the TI-99/4A emulation how MESS works and how to implement new parts.
Terminology
MESS is a sister project of MAME, the Multi Arcade Machine Emulator. It uses the same software core but adds new structures and support for typical devices used with computers. MAME introduced some terms which are also used inside MESS (which is reasonable, or we would need a dictionary all the time) but which can be confusing or misleading sometimes. Accordingly, I will present some of the more important terms here.
MAME/MESS term | Meaning |
---|---|
Game | Refers to the emulated system (consider the viewpoint of MAME); GAME_NOT_WORKING is a flag to mark a system as non-functional |
Driver | The complete system; ti99_4a is a driver, as is geneve. |
Device | An instance of the device_t class. Managed by the core. |
Image | Representation of memory contents stored in a file, usually referring to media like disks, cartridges |
Dump | File of ROM contents. Good dump = verified to be correct; overdump = dump that is longer than the original contents |
Cartridges | Representation of pluggable software components, like the TI-99/4A command modules. |
CHD | Compressed Hunk of Data, storage format for mass storage devices, allows for uncompressed or compressed and checksummed content. |
Input ports | Any kind of system component that changes state on external action, like a dip switch or a key on the keyboard. |
... | ... |
Declarative concept
One of the most important things to keep in mind is that MAME and MESS follow a declarative concept. This means that we want to tell the system what its components are, instead of implementing their behavior. Of course, in some situations we need to really implement behavior by code lines, but I'll give you an example for understanding.
Suppose you have a circuit like a 74LS138. This is a component appearing quite frequently in many systems; it translates a three-bit number into one of several active lines. With a 000 at its inputs, line 1 gets active; a 001 turns on line 2, and so on, until 111 which turns on line 8.
You may now decide to implement the behavior of this circuit. This is quite easy, considering the features of the C and C++ languages; maybe you could choose a switch statement. When your code is complete you do not see anything like a 74LS138 anymore but only its functionality.
MESS encourages a different strategy. The 74LS138 can be emulated as a single circuit. Whereever you need it, you can include this as a black box, just connecting the incoming and outgoing lines. The implementation work is only done once. Of course, for a simple 74LS138 this may seem like nonsense, but think of disk controller chips or complete CPUs.
Therefore, what you will repeatedly see in all the MESS emulations is the following:
- Specify which parts are used.
- Specify how the parts are connected with each other.
You will often encounter structures of configuration data instead of program code. MESS and MAME are, in fact, electronic circuit emulators, and if these circuit emulations are properly done, the complete machine will run correctly. Even the main structure of the emulated machine is just a dataset of components that will be used.
Eventually, the emulators will reach a state where you can define a system in a completely declarative way, for example, by encoding schematics into XML. But at this time (in 2012) this is still in far future.
Starting point
MESS can be started from the command line or from a frontend like QMC2. The frontend supplies the same parameters to the emulator as would be required on a command line. A simple command line could look like this:
mess ti99_4ae
The executable program file is mess (in Windows: mess.exe), sometimes also mess64 (for 64 bit systems) or messd (special version containing symbols for debugging) or mess64d. If you want to find bugs in the emulator (not in your TI programs running on the emulator!) you should use a debug build. For normal usage the standard build is recommended which is also much smaller.
Within the whole TI emulation part you will not find any int main(...). The main function belongs to the core, and this is a area where I would recommended to go not before you finished our tour and completely understood the concepts. For now, just keep in mind that the program starts somewhere in the core and will later reach the TI emulation.
The system
The first thing to do for the system is to process the argument ti99_4ae. This is a name of a driver which it must look up in its tables. These tables were created during build time, so this is not of interest for you. The file where we should look at next is ti99_4x.c. (It could have been named in any way - the important point are the data which it registers in the core.)
At the end of ti99_4x we can find the following lines:
COMP( 1979, ti99_4, 0, 0, ti99_4_60hz, ti99_4, 0, "Texas Instruments", "TI99/4 Home Computer (US)" , 0) COMP( 1980, ti99_4e, ti99_4, 0, ti99_4_50hz, ti99_4, 0, "Texas Instruments", "TI99/4 Home Computer (Europe)" , 0) COMP( 1981, ti99_4a, 0, 0, ti99_4a_60hz, ti99_4a, 0, "Texas Instruments", "TI99/4A Home Computer (US)" , 0) COMP( 1981, ti99_4ae, ti99_4a, 0, ti99_4a_50hz, ti99_4a, 0, "Texas Instruments", "TI99/4A Home Computer (Europe)" , 0) COMP( 1994, ti99_4ev, ti99_4a, 0, ti99_4ev_60hz,ti99_4a, 0, "Texas Instruments", "TI99/4A Home Computer with EVPC" , 0)
COMP is not a command, but it is a macro which is transformed to a structure that is loaded into the core at startup time. In the fourth line you can find the character string that we provided on the command line.
The components of the lines have the following meaning:
- Year of the release of the system; just an information item
- The driver name
- The parent driver name.
- Compatibility information (not used here)
- Machine name - this one points to configuration data
- A pointer to the configuration of input ports
- Initialization information (not used here)
- Company; just information
- System name; used in frontends
There is already one important thing to note. Drivers (see above for the meaning - it is the complete system) may have children. Sometimes you have systems that closely resemble each other, except for some small differences. If they share the same ROM dumps you can avoid to provide the dumps for each system again. In our case, the ti99_4ae driver is a child of ti99_4a; it only differs in the video refresh rate. Therefore we do not need to provide the emulator with the dumps for ti99_4a and ti99_4ae. If the emulator cannot find a ROM dump for a driver, it checks whether there is a parent which can deliver the missing dumps. As all dumps are the same, it suffices to provide the ROMs for ti99_4a only.
ROMs
The next thing the core has to find out is whether it must load any ROM dumps, and how they are named. For a TI-99/4A we need dumps for the console ROMs and GROMs. This is also declared inside ti99_4x.c:
ROM_START(ti99_4a) ROM_REGION16_BE(0x2000, "maincpu", 0) ROM_LOAD16_WORD("994arom.bin", 0x0000, 0x2000, CRC(db8f33e5) SHA1(6541705116598ab462ea9403c00656d6353ceb85)) ROM_REGION(0x10000, region_grom, 0) ROM_LOAD("994agrom.bin", 0x0000, 0x6000, CRC(af5c2449) SHA1(0c5eaad0093ed89e9562a2c0ee6a370bdc9df439)) ROM_END
As just explained, we do not have separate ROMs for ti99_4ae, so the core looks up the parent name and finds this structure. Again, ROM_START is a macro, like all the other capitalized terms. It does not execute anything but transforms the data as shown here into a structure that is registered in the core. The CRC and SHA1 declarations are used to verify that the ROMs are unchanged.
What if I want to use a modified ROM? - Well, this is not directly possible. You would need to calculate the new CRC and SHA1 values (MESS prints them on startup when they differ) and recompile the emulator. If it were allowed to modify the ROMs, debugging would become a nightmare. Imagine your TI emulation locks up - we could not be sure whether the bug is in the emulation or whether the dump has been modified. Sorry ... you're on your own here.
Both ROM files must be stored in a ZIP file named after the driver name, in this case ti99_4a.zip. The lines should be read in this way:
- This is ROM content for the memory region "maincpu" (see later).
- It is 16 bit (our ROMs are connected to the 16 bit bus), and the byte order is big-endian (in the TI, the most significant byte has a lower address than the least significant byte; if you store >1234 at >A000, then >12 is at >A000 and >34 is at >A001). PCs use little-endian, in contrast.
- Load the contents of the file "994arom.bin" into the region, >2000 bytes (8192).
- Another region we use is region_grom (somewhere else declared as a string "console_groms").
- Its size is 64 KiB (yes, each GROMs only has 6 KiB contents, but they nevertheless occupy the whole address space) and it is 8-bit memory.
- We load the contents of "994agrom.bin" into the region, size is >6000 bytes (24576; the first three GROMs). The remainder of the region is filled with zeros.
We could also clip the GROM region, since we put cartridge GROMs in their own regions. (Maybe I'll do that in a later release.) For now it does not hurt to waste 40960 bytes.
Main part
static MACHINE_CONFIG_START( ti99_4a_50hz, ti99_4x_state ) /* CPU */ MCFG_TMS99xx_ADD("maincpu", TMS9900, 3000000, memmap, cru_map, ti99_cpuconf) MCFG_MACHINE_START_OVERRIDE(ti99_4x_state, ti99_4a ) MCFG_MACHINE_RESET_OVERRIDE(ti99_4x_state, ti99_4a ) /* Video hardware */ MCFG_TI_TMS991x_ADD_PAL(VIDEO_SYSTEM_TAG, TMS9929A, ti99_4_tms9928a_interface) /* Main board */ MCFG_TMS9901_ADD(TMS9901_TAG, tms9901_wiring_ti99_4a, 3000000) MCFG_DMUX_ADD( DATAMUX_TAG, datamux_conf ) MCFG_TI99_GROMPORT_ADD( GROMPORT_TAG, console_cartslot ) /* Software list */ MCFG_SOFTWARE_LIST_ADD("cart_list_ti99", "ti99_cart") /* Peripheral expansion box */ MCFG_PERIBOX_ADD( PERIBOX_TAG, peribox_conf ) /* sound hardware */ MCFG_TI_SOUND_94624_ADD( TISOUND_TAG, sound_conf ) /* Cassette drives */ MCFG_SPEAKER_STANDARD_MONO("cass_out") MCFG_CASSETTE_ADD( "cassette", default_cassette_interface ) MCFG_CASSETTE_ADD( "cassette2", default_cassette_interface ) MCFG_SOUND_WAVE_ADD(WAVE_TAG, "cassette") MCFG_SOUND_ROUTE(ALL_OUTPUTS, "cass_out", 0.25) /* GROM devices */ MCFG_GROM_ADD( GROM0_TAG, grom0_config ) MCFG_GROM_ADD( GROM1_TAG, grom1_config ) MCFG_GROM_ADD( GROM2_TAG, grom2_config ) /* Joystick port */ MCFG_TI_JOYPORT4A_ADD( JOYPORT_TAG, joyport4a_50 ) MACHINE_CONFIG_END
This is a complete system description. The lines are not executed one after the other, although the order does matter for some lines. Actually, each line is a macro that is translated to a operation to put a value into the system description object which you won't see anywhere; it is part of the core.
MCFG_TMS99xx_ADD is a specialized version of MCFG_DEVICE_ADD which add a device; here, in particular, a device that can be scheduled. It defines a clock frequency as 3000000 Hz, creates an instance of the device (class) TMS9900 which is defined in tms9900.h.
Note that MESS has an own model of component management - the devices like TMS9900 should not be understood as objects but as components. Instances of these components are automatically created at runtime, and they are referenced by a name given as a string. In this case it is "maincpu". Whenever the string "maincpu" appears, the system will follow a reference to the concrete instance of the component. Accordingly you do not instantiate a component in the same way as an object (by new), but by registering the component class with the core. Components are usually not created on demand but all at startup time.
Hence, in order to get a running system, you have to instantiate a set of schedulable objects, and these objects are then run by the MAME core at their defined frequencies. What follows now is to connect the signal lines of these components to other components in the system.
Any schedulable components may have up to three address spaces: a PROGRAM space, a DATA space, and an IO space. For the TI-99/4A we define a PROGRAM space to encompass the whole 64 KiB address space, while the IO space corresponds to the CRU lines. We do not define a DATA space as the TI console is a standard Von-Neumann architecture. (Note that you could maybe create a Harvard architecture with separate program and data space by making use of the IAQ output line of the processor. This line is asserted when the processor attempts to get a command word over the bus.)
The idea is now that a map be declared for each address space. This is contained within the ti99_4x.c file.
static ADDRESS_MAP_START(memmap, AS_PROGRAM, 16, ti99_4x_state) ADDRESS_MAP_GLOBAL_MASK(0xffff) AM_RANGE(0x0000, 0x1fff) AM_ROM AM_RANGE(0x8000, 0x80ff) AM_MIRROR(0x0300) AM_RAM AM_RANGE(0x0000, 0xffff) AM_DEVREADWRITE(DATAMUX_TAG, ti99_datamux_device, read, write) ADDRESS_MAP_END
While the processor runs, it performs accesses into the address space. The map as given here declares what happens in specific sections of the address space.
- In the area from 0000 - 1fff (hex), the ROM is accessed. This means that write accesses are ignored, and that the contents can be found in the appropriate ROM section.
- In the area of 8000 to 80ff, we have a 256 byte of RAM memory (Scratch pad) which is mirrored at addresses that are defined by the mirror mask 0x0300. The mirror mask tells us which bits may be set to define a mirror. In this case, the main address area is 8000-80ff, and the three other options are 8100-81ff, 8200-82ff, and 8300-83ff. Indeed, if you check the schematics, you will see that these lines are not decoded, so the scratch pad RAM is mirrored at these addresses.
- The last line covers the whole address space. It will only be reached when no other option before applies. What it does is that it calls the READ and WRITE handlers of a component identified by DATAMUX_TAG, instantiated from ti99_datamux_device, with a method called read for reading and a method write for writing.
The databus multiplexer is a collection of circuits which converts the 16-bit data bus of the TMS9900 to an 8-bit time-multiplexed bus. This means that each word access is split into two byte accesses. We do not see this here, but in the implementation of the datamux; see datamux.c.
class ti99_datamux_device : public device_t { public: ti99_datamux_device(const machine_config &mconfig, const char *tag, device_t *owner, UINT32 clock); DECLARE_READ16_MEMBER( read ); DECLARE_WRITE16_MEMBER( write ); ...
This is the start of the declaration of the component in datamux.h. Besides the constructor which is called form the MAME core, the two access methods read and write are declared. They carry a set of parameters which are required for the component handling, and by using the macro DECLARE_READ16_MEMBER we can hide the details. Correspondingly, in the file datamux.c we have
READ16_MEMBER( ti99_datamux_device::read )
which is the implementation of this method.
Summarizing: When the CPU reads, for example, from address at location >6000, the core will consult the address map, searches through the map, finds the entry for the datamux, and invokes the read method on the instance identified by DATAMUX_TAG.
To be continued