Device Interfaces

From MAMEDEV Wiki

Device interfaces are separate classes that enable devices to participate in more areas of the overall system. Each interface is a sort of contract between the device that inherits from it and related other parts of the system. For example, a device inherits from the execute interface if it wishes to be scheduled and called regularly to execute. Simiarly, a device inherits from the NVRAM interface if it wishes to participate in saving/loading data to the .nv files.

To add an interface to a device, both the device class and device's configuration class must inherit from the interface's class and the interface's configuration class, respectively. Some interfaces are more about configuration than runtime, and others are more about runtime than configuration, but in all cases both interface classes are required.

Since a device is already required to inherit from device_t, this means that you must leverage C++'s multiple inheritance support in order to add an interface. For example, our configuration class might look like this:

class example1_device_config : public device_config,
                               public device_config_memory_interface,
                               public device_config_nvram_interface
{
};

and our device class like this:

class example1_device : public device_t,
                        public device_memory_interface,
                        public device_nvram_interface
{
};

Notice first that the device_config and device_t classes remain the first classes listed. This is important as they are the real base classes of our device. The interface classes should always be listed afterwards. Also notice that our configuration class and our device class inherit from matching configuration and interface classes.

Each interface has its own demands on the device. Some interfaces, like the sound interface, really don't require any significant changes. Others, like the NVRAM interface, are pure virtual classes, requiring implementation of one or more methods.

Currently the MAME core defines six standard interfaces:

  • The execute interface connects the device to the internal scheduler, and requires implementation of a device_run() method.
  • The memory interface connects the device to the memory system, allowing it to specify one or more address spaces.
  • The state interface connects the device to the debugger, enabling display and editing of state during execution from within the register view. It also provides simple indexed accessors for reading/writing state in a standard fashion.
  • The NVRAM interface connects the device to the NVRAM read/write process.
  • The disassembly interface connects the device to the debugger, enabling disassembly views from within the disassembly window.
  • The sound interface connects the device to the sound network, enabling routing of sound from the device to/from other devices.

Details on the interfaces and their requirements are given in the sections below.

Standard Interfaces

This section has descriptions of each of the standard device interfaces.

Execute

The execute interface enables a device to participate in the standard device scheduling. The scheduler makes a list of all devices with execute interfaces, and then iterates through them, assigning each a timeslice and requesting the device to execute.

The execute interface also provides mechanisms for interrupt signalling and synchronization, allowing the device to participate properly in the execution of the aggregate system.

Finally, the configuration portion of the execute interface automatically supports the MDRV_DEVICE_DISABLE() configuration tokens, which allow an executable device to exist in a configuration but prevents it from event being scheduled.

A device configuration class that inherits from device_config_execute_interface can optionally override several methods that describe how the device executes:

class example2_device_config : public device_config,
                               public device_config_execute_interface
{
    // normal device stuff here

protected:
    // device_config_execute_interface overrides
    virtual UINT32 execute_clocks_to_cycles(UINT32 clocks) const;
    virtual UINT32 execute_cycles_to_clocks(UINT32 cycles) const;
    virtual UINT32 execute_min_cycles() const;
    virtual UINT32 execute_max_cycles() const;
    virtual UINT32 execute_input_lines() const;
    virtual UINT32 execute_default_irq_vector() const;
};

The first two overrides provide methods that translate from clocks to cycles and vice-versa. A clock is defined as a single cycle of the input clock to the device. A cycle is defined as an internal unit used by the device when executing. By default, it is assumed that 1 clock == 1 cycle. However, if your device has an internal clock divider or multiplier, these methods should be overridden to do the math and return the proper numbers.

The middle two overrides should simply return the minimum and maximum number of cycles that any given execution step in your device may take. For example, if you are describing a CPU whose fastest instruction takes 2 cycles and whose slowest instruction takes 10 cycles, you would override these two methods to return the values 2 and 10, respectively. Note that these numbers are in cycles, not clocks. If not provided, these values both default to 1 cycle.

The execute_input_lines() override should simply return the number of synchronized input lines attached to the device. By default, this is 0.

Finally, the execute_default_irq_vector() override should return the default integer vector that is implicitly specified when signalling an input line without explicitly providing a vector.

Moving on to the device class:

class example2_device : public device_t,
                        public device_execute_interface
{
    // normal device stuff here

protected:
    // device_execute_interface overrides
    virtual INT32 execute_run(INT32 cycles);
    virtual void execute_burn(INT32 cycles);
    virtual void execute_set_input(int linenum, int state);
};

The most important (and only required) override here is execute_run() which is called whenever the device is scheduled for execution. It is passed the number of cycles (not clocks) to execute, and when it is finished, it should return the number of cycles that were executed.

The optional override execute_burn() is called if the device execution is suspended for any reason, but cycles are consumed. This is generally used to support internal running clocks or other things which need to keep track of how many cycles executed.

Finally, the execute_set_input() override is called whenever a synchronized input is changed. Internally, the execute interface accepts synchronized input change requests, queues them, and then calls this override at the appropriate time to ensure the state is updated in sync with all other executing devices.

Memory

The memory interface allows to a device to own one or more address spaces. These address spaces are detected by the memory system and automatically allocated at startup time. Memory interfaces are most commonly used for CPU cores, though any device that accesses memory out over a bus should ideally use an address space for those accesses so that bank switching or other mechanisms can be implemented to act upon reads/writes to that address space.

The configuration portion of the memory interface automatically supports the MDRV_DEVICE_ADDRESS_MAP() configuration tokens. These allow an address map to be expressed in a driver, which the memory system turns into an address space at startup.

A device configuration class that inherits from device_config_memory_interface is required to override a method that returns an adress_space_config pointer for a given address space index:

class example3_device_config : public device_config,
                               public device_config_memory_interface
{
    // normal device stuff here

protected:
    // device_config_memory_interface overrides
    const address_space_config *space_config(int spacenum = 0) const;
    // address space configurations
    address_space_config   m_program_space;
    address_space_config   m_io_space;
};

The space_config' override accepts a space index and returns a pointer to an adress_space_config object. In general, the configuration of the address space is fixed for a device, so it is typical to just embed these objects and initialize them in the constructor. For example:

example3_device_config::example3_device_config(const machine_config &mconfig, const char *tag, 
        const device_config *owner, UINT32 clock)
    : device_config(mconfig, static_alloc_device_config, tag, owner, clock),
      device_config_memory_interface(mconfig, *this),
      m_program_space("program", ENDIANNESS_LITTLE, 32, 32, 0, 32, 12),
      m_io_space("data", ENDIANNESS_LITTLE, 32, 16)
{
}

This creates a "program" memory space that is 32-bit little-endian with a 32-bit address bus, a 32-bit logical address space, and 12-bit pages; and a "data" memory space that is also 32-bit little-endian with a 16-bit address bus.

Given this, then the space_config looks like:

const address_space_config *space_config(int spacenum) const
{
    if (spacenum == AS_PROGRAM)
        return &m_program_space;
    else if (spacenum == AS_IO)
        return &m_io_space;
    else
        return NULL;
}

Moving on to the device class:

class example2_device : public device_t,
                        public device_memory_interface
{
    // normal device stuff here

protected:
    // device_execute_interface overrides
    virtual bool memory_translate(int spacenum, int intention, offs_t &address);
    virtual bool memory_read(int spacenum, offs_t offset, int size, UINT64 &value);
    virtual bool memory_write(int spacenum, offs_t offset, int size, UINT64 value);
    virtual bool memory_readop(offs_t offset, int size, UINT64 &value);
};

All of these methods are optional. For devices that support virtual address spaces (e.g., address translation), you will want to override the memory_translate() method which allows you to transform logical addresses to physical addresses, or return false if there is no mapping for the provided address.

The remaining three overrides are primarily for debugging, and allow the device a chance to act on reads, writes, and opcode reads before they are passed to the memory system. Return true if your method handled the operation to prevent it from being passed on.

State

NVRAM

Disassembly

Sound

Custom Interface