9 Advanced Programming with DML 11 Defining New Interface Types
Model Builder User's Guide  /  II Device Modeling  / 

10 Example Models

Three real device models are included in the Simics Base package; an AM79C960 (ISA) Ethernet controller, a DS12887 real-time clock and a DEC21140A (PCI) Ethernet controller. They contain full source code and some tests. You can use these models and tests as examples when developing your own device models. This chapter serves as an orientation about how those devices are modeled and shows how you can test them if you have access to the Enterprise machine or Firststeps for the DEC21140A.

Source packages also contain devices which can be used as examples. If you develop a different kind of model than the ones in Model Builder, you may find a device in one of the source packages a better example.

10.1 AM79C960

AM79C960 was a rather common ISA Ethernet card, used mostly in PCs. It is a little dated now, but still serves as a good example of how to implement Ethernet.

The workable source code for the sample device can also be found in the directory [simics]/src/devices/AM79C960. If you want to try modifying the AM79C960 module yourself, we recommend that you set up a user project and copy the source code there, as described in section 3.4.

Please do read previous sections in part II to know how to compose and build an empty device, and we will ignore those general parts in this section and highlight the methodologies for how to efficiently model a specific device.

Typical work flow for Simics modeling includes below four stages:

As the first stage, modeling materials preparation should be done before the modeling of the device, the necessary materials include hardware spec, target OS driver, and test applications, and optional software manuals. Hardware spec is the most important document, it also serves as the root spec of the model and the bridge between modeling team and the software team. Do make sure you get the right version of them.

We will elaborate the other three stages by tearing down the modeling methdology of an AM79C960 device.

10.1.1 Architecture Design

There is always a system block diagram in the hardware spec, which perfectly shows the system level functional blocks, AM79C960 does as well, it has even two: one is bus master mode and the other is shared memory mode, there are some difference on ISA bus interface unit, since Simics will not simulate the details of a bus arbitration, we could regard them as one in the modeling.

From the diagram we know that AM79C960 has the major functional blocks like ISA/EISA bus, IEEE 802.3 Ethernet port, transmit and receive FIFOs, etc. It receives frames from ISA bus and sends them to Ethernet port, and vice versa. Our model should focus on those data handling, implement the actions the software calls for the device.

As Simics is a functional simulator, we need not model details how the device works internally, for example, the Ethernet PHY block actually has line encoder/decoder (MENDEC), frame preamble detector, etc. sub functional modules, yet they are not visible to the software, so we will hide those details and handle the data octets directly.

Simics itself is built up by several fundentmental modules, we also suggest the user to divide the complex device module into several sub modules and develop them separately. In each simple module, user always can utilize provided well-defined Simics libraries and templates to facilitate the design, and through interfaces, each module can be connected with each other efficiently.

It's a good practice to try to reuse existing modules and build modular system. The AM79C960 module simplified the design and implemented all functionalities in one module for it's simple enough, while if you're modeling a more complex Ethernet controller, it's better at least divide it into PHY and MAC modules, then you could reuse generic_eth_phy module and get some helpful references from the implementation of ich10_lan_v2 module, avoid duplicating the common PHY and MAC functionalities, and could be able to improve them separately and facilitate their debugging.

Once the sub modules are defined, we should then begin the interfaces design. In Simics, interface is the primary way to transfer data between modules, see section 5.5 for more details.

10.1.2 Interfaces Design

According to the device block diagram, there are several interfaces, means connections to outside of the module, should be includes in this model:

The outgoing Ethernet link could be implemented as:

connect link {
    param documentation = "The ethernet-link the device is connected to.";
    interface ethernet_common;
}

The incoming Ethernet link could be implemented as:

implement ethernet_common {
    method frame(const frags_t *frame, eth_frame_crc_status_t crc_status) {
        receive_packet(frame);
    }
}

When another device is trying to send data to this device, the interface method frame is called, the user is expected to handle the incoming data within the method.

Interrupt is a very common interface for the device to inform some internal events, ex. one frame is received or transmitted, error condition is detected, etc., we can use simple_interrupt interface to do that:

connect irq_dev {
    param documentation = "The device that interrupts are sent to.";
    param configuration = "required";

    interface simple_interrupt;
}

The simple_interrupt interface is deprecated, user should use signal interface in the new design.

To record current level of interrupt line and avoid to raise or lower the interrupt line twice, we need an attribute somewhat like irq_raised. This is also one suggested design practice in Simics to support checkpointing, when Simics restores the running from a checkpoint, the current runtime interrupt status should be correctly restored as well, this is supported by utilizing an attribute, whose value will be automatically saved and restored when the checkpoint is written and read.

To support DMA and share memory with the target CPU, we use memory_space interface to simulate the bus master function. Through this interface, the device is able to access the initialization data and Ethernet descriptor rings which are mapped on the system bus:

connect memory {
    param documentation = "The memory space the device uses to access the "
        + "initialization data and descriptor rings";
    param configuration = required;
    interface memory_space;
}

AM79C960 supports both shared memory operations mode and bus master mode. In the first mode, AM79C960 is visible as one shared memory space to the master CPU, all registers are mapped on the memory space. In the second mode, AM79C960 has the ability to perform DMA operation, access data from system bus directly. In Simics, there needs no special design for DMA controller, the device can use interface call to access the memory space at any time.

ISA bus logically is seen as an range of linear address memory space from the viewpoint of software, but there is no need to implement bus details, for example, bus arbitration, IOR/IOW signals, etc., instead, we implement this bus functionality as a register bank. DML bank implicitly implements the io_memory interface to make it accessible from the CPU, so the user needs only declare the registers offset, name, size etc. parameters, and focus on its registers logic design, which in many cases is the most important and time-consuming part in a device modeling.

Don't forget to check the interfaces configuration, some of them are optional and may be left unconnected during initialization or even all the runtime. So, in case the interface is optional, user needs to check the connection before calling its methods, just like what AM79C960 module does in send_packet() method:

method send_packet() {
    local physical_address_t txd_addr;
    // ... other preparation statements
    if (link.obj != NULL) {
        // ... prepare frame buffer
        link.ethernet_common.frame(&frame, Eth_CRC_Match);
    } else {
        log info, 2: "not connected, not packet sent";
    }
    // ... post handling of sending
}

10.1.3 Registers Design

Most of the device logics are triggered by register accessing, we could design those logics into the registers' side effect or some global functions. It's a good style to design (declare) the registers bank in one place and implement them at another place, it makes the implementation clear.

Let's take mac_address as an example, it's a primary attribute for an Ethernet controller. The mac_address locates at I/O registers address 0 ~ 5, which are set by the CPU. By utilizing attr_accessor template, any writing to register aprom_0 ~ aprom_5 is also updating the mac_address attribute, which is then be used to check destination address when a frame is received.

Another example is the package transmitting, which is triggered by CSR0 writing. This occurs when the driver prepared the data to be transmitted, configured the descriptor ring accordingly, and then writes the CSR0 to start transmitting. To implement this behavior, we add side effect to the CSR0 writing:

    register csr0 {
        // will csr0.init() after csr0 written?
        session uint1 do_init;
        // will csr0.start() after csr0 written?
        session uint1 do_start;
        // will csr0.transmit_demand() after csr0 written?
        session uint1 do_transmit_demand;

        method write_register(uint64 value, uint64 enabled_bytes, void *aux) {
            default(value, enabled_bytes, aux);
            if (do_init == 1) {
                do_init = 0;
                initialize();
            }
            if (do_start == 1) {
                do_start = 0;
                start();
            }
            if (do_transmit_demand == 1) {
                do_transmit_demand = 0;
                transmit_demand();
            }
        }
        field TDMD  @ [3] "Transmit Demand" {
            is write;
            method write(uint64 value) {
                if (value == 1) {
                    this.val = 1;
                    do_transmit_demand = 1;
                }
                // ignore write 0; cleared by send_packet()
            }
        }

We update the demand bit when writing, as well as other bits in case more than one bits are written, then check the demand bit after writing. It's better to implement the packet sending outside of the register definition, for it could be quite complex.

From related register bits definition and explanation of descriptor ring buffer management, we can conclude below procedure when transmitting one packet frame (Refer to send_packet() method for the implementation):

  1. Check if the previous transmitting is finished, quit immediately if it was not, the new transmitting will be scheduled by the event queue.
  2. Clear the TDMD flag, to let other blocks and software know the status.
  3. Read the transmit descriptor, which should have been prepared by the driver.
  4. Check if it is connected to a link, it may be left unconnected.
  5. If it's connected, read the actual frame data and send them by calling the interface method.
  6. Update the registers and write back the transmit descriptor.
  7. Check if the event queue has pending packets to be sent.

Let's also take a look at receiving a packet, this occurs when external device call the ethernet_common interface method (Refer to receive_packet() method for the implementation):

  1. Buffer the incoming frame.
  2. Check RXON to see if the device is ready to receive.
  3. Read the receive descriptor and check related length.
  4. Check the MAC address to determine whether the device should handle this packet.
  5. If yes, write the frame data to the receive buffer.
  6. Update related flags and raise interrupt to inform the driver to read.

The descriptor ring used by transmitting and receiving block is a very common data structure for the Ethernet controller, let's take a look at the transmit descriptor ring implementation in this device as an example.

typedef struct {
    physical_address_t addr;
    uint1 OWN;
    uint1 STP;
    uint1 ENP;
    uint16 size;
} txd_t;

method txd_from_buf(uint8* buf) -> (txd_t) {
    local txd_t txd;
    txd.addr = buf[0] | buf[1] << 8 | buf[2] << 16;
    txd.OWN = buf[3][7];
    txd.STP = buf[3][1];
    txd.ENP = buf[3][0];
    txd.size = - (buf[4] | buf[5] << 8);
    return txd;
}

method txd_to_buf(txd_t txd, uint8* buf) {
    local int neg_size = - txd.size;
    buf[0] = txd.addr[7:0];
    buf[1] = txd.addr[15:8];
    buf[2] = txd.addr[23:16];
    buf[3] = (txd.OWN << 7) | (txd.STP << 1) | (txd.ENP << 0);
    buf[4] = neg_size[7:0];
    buf[5] = neg_size[15:8];
    buf[6] = 0;
    buf[7] = 0;
}

The descriptor ring buffer is built up by numbers of descriptors, each descriptor entry has the same 8 bytes structure, when reading out one transmit descriptor, AM79C960 module extracts its inner data and assigns them to a txd_t type value, for later on convenient access. At writing back, the value is mapped back to the buffer data structure accordingly.

Indexing of the descriptor ring buffer is somewhat tricky, it originally locates at the transmit ring counter register at CSR74, yet this device uses an attribute curr_txd to simulate its behavior, it works well from the software point of view and is quite simple to be implemented:

attribute curr_txd is uint64_attr {
    param documentation = "Index of the current transmit descriptor";
    param configuration = "optional";
    method addr() -> (physical_address_t) {
        return xmt_descr_tbl_addr.val + (this.val * TXD_SIZE);
    }
    method next() {
        this.val = (this.val + 1) % xmt_descr_tbl_length.val;
    }
}

The AM79C960 model is far from complete, it implements just enough functionalities that the device can be used with Linux 2.4. There always are too many registers to be implemented, in practice, we only implement the smallest set of registers required for the software to work correctly, and leave those not necessary as unimplemented (could use unimpl type logging to record). This completeness could be verified by target software, so when the software is upgraded, the model probably will have to be updated as well.

10.1.4 Other Elements Design

Attributes are also widely used in Simics to record internal state or be exposed to user for configuration and debugging, for example, the 64-bit Logical Address Filter that resides in CSR8 - CSR11 is stored in the logical_address_filter attribute. Some of the registers, for example, CSR24 and CSR25 that contain Base Address of Receive Ring, are not even implemented and the values are only stored in separate attributes.

Another example is poll_interval, it originally locates in CSR47, but we could use an attribute to simply the implementation, while keep the ability to configure that at runtime.

In case the device needs some asynchronous handling, such as deferred transmitting process, Simics provides event to support this behavior:

event poll_txd is simple_time_event {
    method event() {
        send_packet();
        poll_txd.post(poll_interval.val);
    }
}

This event periodically polls the transmit descriptor ring buffer, if there are any descriptors previously pending to be transmitted, they'll be checked again by the event handler. The polling is then rescheduled for the next round.

There is no need to design the event in receiving block, every time the packet is received, it can be handled immediately.

Simics provides template to avoid duplicating the same codes, one good example for this common practice is data_accessor:

template attr_accessor {
    param attr;
    param attr_msb;
    param attr_lsb;
    param ignore_write default false;
    param mac_register default false;

    method get() -> (uint64) {
        return read();
    }
    method set(uint64 value) {
        write(value);
    }
    is read;
    method read() -> (uint64) {
        return attr.val[attr_msb:attr_lsb];
    }
    is write;
    method write(uint64 value) {
        if (!ignore_write) {
            attr.val[attr_msb:attr_lsb] = value;
        }
    }
}

This template implements register default get/set and read/write behaviors, and the user could customize this kind of register by setting parameters accordingly. Remember that, the user can always find good templates from the utility.dml library and define their own templates as they like.

Any device needs some methods to be reset to known states. By instantiating the sreset utility template, our Simics device will be provided with soft_reset semantics, this reset will reset all banks and registers recursively by default, unless those explicitly ignored:

    // should be read/write accessible only when STOP is set
    register csr1 is sticky;

    // should be read/write accessible only when STOP is set
    register csr2 is sticky;

These use the utility template sticky which overrides the default soft_reset behavior, and prevents them to be reset to the default value during soft reset.

In case there are some other elements are to be initialized, such as attributes, user can place them in init() or post_init() methods, see section 4.2.7.3 for more details.

10.1.5 Run the AM79C960 model tests

Functional tests and integration tests are both heavily used in Simics device modeling, normally functional tests locates at the test subdirectory in the device module, user could execute make test to perform them at any time. Integration tests are much more complex, user needs to connect devices and components in the Simics configuration and run the target OS, generally use target application to test the device models.

AM79C960 does not provide functional tests, so we will not explain that here. Yet if your distribution contains the simulated machine enterprise, you can find the Simics script enterprise-common.simics in the directory [simics]/targets/440bx. Let's see how to utilize this script to verify the functionalities of this model. This file creates an enterprise machine using the AM79C960 module. The AM79C960 object is called enterprise.motherboard.lance.lance.

To do something interesting with the AM79C960 model it needs to be connected to something that it can talk to. The default setup for the enterprise system has the AM79C960 controller connected to a service-node via an Ethernet switch.

Start the simulation and let the machine boot and login as the user root. No password is required. Stop the simulation and set the log level of the lance object to 2:

simics> enterprise.motherboard.lance.lance.log-level 2
[enterprise.motherboard.lance.lance] Changing log level: 1 -> 2

You can now start the simulation again and send a ping packet to the service-node by entering ping -c 1 10.10.0.1 in the console on the simulated machine. The lance object will log what happens:

[enterprise.motherboard.lance.lance info] Packet sent, dst 20:20:20:20:20:00, src 10:10:10:10:10:30, length 102 bytes
[enterprise.motherboard.lance.lance info] packet received, dst 10:10:10:10:10:30, src 20:20:20:20:20:00, length 102 bytes
[enterprise.motherboard.lance.lance info] MAC address matches, packet accepted

The enterprise machines sends a 102-byte packet to the service-node and receives a 102-byte reply. These are the actual ping request and ping reply.

If you want more detailed logs you can change the log level to 3 or 4. At log level 3 a lot more information about what is going on in the device will be logged. The device polls for packets to transmit regularly, so this will cause a lot of output. At log level 4 all accesses that the processor does to the device will also be logged.

By the test applications we make sure the basic transmitting and receiving work well and the driver can access the device as expected. As a good practice, we should document those key unimplemented features as limitations at the top of the model, just like AM79C960 module does:

param limitations = ("<ul>"
    + "..."
    + "<li>The ISA bus configuration registers are not implemented</li>"
    + "..."
    + "</ul>");

In case new features are required, we could define the new test scenarios and develop that add-on features based on this ready model, continuously improve the model's completeness.

10.2 DS12887

DS12887 is a very common real-time clock device. It is used, among other places, in common PCs. There are also many other devices that are extensions of the DS12887, for example, DS17485 and M5823.

It may be good to have the documentation for the DS12887 chip when looking at the sample code, so that you can compare the code to the specification. The documentation can be found on the Internet, search for ds12887.pdf on www.google.com and you will find several links to it.

The source code for the sample device can be found in the directory [simics]/src/devices/DS12887. If you want to try modifying the DS12887 yourself, we recommend that you set up a user project and copy the source code there, as described in section 3.4.

The source code of the DS12887 module is quite richly commented, so if you have the documentation for the DS12887 chip you should hopefully be able to understand most of the code without too much problem.

10.2.1 Architecture Design

Refer to the address map of DS12887 from the spec, the major function blocks of the DS12887 block diagram is clearly the calendar and alarm ram (block "CLOCK, CALENDAR, AND ALARM RAM") and the control registers (block "REGISTERS A,B,C,D"). The output signals of this model are also simple, the main output signal is the interrupt (signal "IRQ"). We can abstract DS12887 with a real time clock device, it increases its internal time counter per second. The date and time are stored in registers. Alarm interrupts can be controlled by registers if clock match the values in alarm registers. As Simics is a functional simulator, we do not need to exactly model such details. Instead we design an attribute base_rtc_time to store absolute time, so that we only update RTC time when time registers are really accessed. Also when registers are accessed we compute and schedule the alarm interrupt rather than compare and decide if the alarm interrupt needs to be triggered. This is also known as common practice to improve the simulation performance.

10.2.2 Registers Design

As registers are the main function blocks of DS12887. Firstly we write the layout of the register banks. The address and size of registers can be found in the documentation.

bank registers is function_mapped_bank {
    param register_size = 1;
    param function = 1;

    register seconds            @ 0;
    ...
    register nvram[i < 114]  @ 14 + i;
}

As we introduced previously, we implement the model to update the time registers "lazily". The handling logic looks more complicated than absolutely necessary, but does help simulation performance. A simple implementation could post an event that raises the UIP flag and then an event that lowers the UIP flag, updates the time registers and compares them with the alarm registers each simulated second. Such simple implementation actually would lead to many regular Simics events just for idle loop, in case if the time registers and alarm registers are not really accessed. To avoid having to frequently post these events, the model instead saves the simulated time that the real-time clock time was last set, and the time it was set to. From this information the current real-time clock time can be calculated at any time, and the time registers are only updated when they are read. We actually develop template time_register to include such time updating methods as update_time_registers_conditional() and writeback_time_registers() so time registers can adopt such functionalities. There is a comment above the base_time attribute in the source code that describes more detailedly about the time representation.

Similarly, events for the alarm interrupt, periodic interrupt and update-ended interrupt are only posted if the corresponding interrupt flag is not already raised. We also develop template interrupt_event to implement such interrupt scheduling functionalities, mostly by the update_time() method in it. The interrupt related registers or register fields, for example, the alarm registers, can adopt the template. This implementation means that, if Linux does not use the device after the boot, as we generally observed in our simulated scenarios, the model will have good simulation performance since it does not need to post any more events once Linux has booted.

DS12887 spec well describes the control registers. The 3 DV bits in register a enable the oscillator and interrupt posting. The 4 rate-selection bits decide how to generates the periodic interrupt. Register b include the interrupt enable bits, and the interrupt flag bits are in register c. To better maintain such interrupt functionalities we implement common method update_IRQF() to check, update those fields, and raise or lower the interrupt. This is also a common practice eventually seen in interrupt processing of every DML devices. Interrupt related events and register fields can do individual update, then call the common method to sync the interrupt state, without spreading similar or even inconsistent code in different DML code pieces.

A difference between the documentation of the DS12887 and the model is that the model has three register banks, while the documentation only describes one. This is because of the way the device is used in PCs. The registers described in the documentation correspond to the registers bank. When the device is used in a PC a small translation device with two registers that forwards accesses to the registers of the DS12887 is mapped in the port space. This translation device corresponds to the port_registers bank. In addition we define a bank partially_persistent_registers which serves as persistent backing storage for registers where not all fields are persistent. See the section 7.2 of the second chapter of the Simics User's Guide and the DML 1.4 Reference Manual for details. If you want to use the model as a pure DS12887, just ignore the port_registers bank.

10.2.3 Interfaces Design

According to the functions described in the spec, there are several interfaces should be included in this model:

We can use the interrupt interface simple_interrupt to really trigger the interrupt from DS12887 internal events:

connect irq_dev {
    param documentation = "The device that interrupts are sent to,"
        + " or Nil if the interrupt line is not connected to anything.";
    param configuration = "optional";

    interface simple_interrupt;
}

The simple_interrupt is obsoleted by the signal interface.

We connect to an outside timer to get the current virtual time. We need it to calculate the real time.

    local conf_object_t *clock = SIM_object_clock(dev.obj);
    if (clock == NULL) {
        log error: "device does not have a time queue/clock";
    } else {
        SIM_require_object(clock);
    }

One amazing capacity of Simics environment is checkpointing and reverse execution. So Simics environment might automatically store or restore DS12887 states to or from a Simics checkpoint. This actually leads to an extra step in Simics modeling to see if the model is checkpointing safe. For a Simics timer model as DS12887, reschedule its time events or time interrupts after restore is generally needed. Aware of that, we also implement the temporal_state interface of such computing and rescheduling.

10.2.4 Other Elements Design

We mainly picked alarm interrupts as interrupt example in previous sections. Actually DS12887 also support periodic interrupts and update-ended interrupts. The implementing experience of alarm interrupts can be easily used in other interrupts according to the details described in the spec.

Templates facilitate Simics model code consistency, debugging and device hierarchy. For example, we also implement template rtc_register to be used by all time and alarm registers, which contains methods for register value range checking and converting between BCD format and binary format. DML enables template parameters so that the template method just implement the value boundary checking logic and leave the boundary value setting to each individual registers. We also implement other templates as hour_rtc_register, time_alarm_register, irq_flag, irq_enable_flag accordingly.

There are some DS12887 hardware functionalities are not needed in Simics functional modeling. For example, we do not need the details of oscillator but just need to monitor if it is enabled or not, to trigger the interrupts, from the DV bits of register a. We do not implement the power switch because this is generally not needed in a functional simulation. We also do not implement the daylight saving time and square wave output functionalities since we do not observe software requiring it. We record such non-implemented functionalities with limitations parameter for future enhancement.

10.2.5 Running the DS12887 model

If your distribution contains the simulated machine enterprise, you can find the Simics script enterprise-common.simics in the directory [simics]/targets/440bx. This file creates an enterprise machine using the DS12887 module. The DS12887 object is called rtc0.

You can, for example, log what is happening to the device during the boot by setting the log level of the rtc0 object to 3:

simics> rtc0.log-level 3
[rtc0] Changing log level: 1 -> 3
simics> c
[rtc0 info] Update-ended interrupt triggered, raising UF.
Pressing return
[rtc0 info] Periodic interrupt frequency set to 1024.000000 Hz.
[rtc0 info] Periodic interrupt triggered, raising PF.
[rtc0 info] UF lowered.
[rtc0 info] PF lowered.
[rtc0 info] UIE set.
[rtc0 info] Periodic interrupt triggered, raising PF.
[rtc0 info] Update-ended interrupt triggered, raising UF.
[rtc0 info] Raising interrupt.
[rtc0 info] UF lowered.
[rtc0 info] PF lowered.
[rtc0 info] Lowering interrupt.
[rtc0 info] UIE cleared.
[rtc0 info] Periodic interrupt triggered, raising PF.
[rtc0 info] Update-ended interrupt triggered, raising UF.

If you raise the log level to 4 all access the processor does to the device will be logged. The rtc0 object is accessed a lot during the boot, so you probably do not want to run the entire boot with log level 4.

Note that Linux only uses the real-time clock while booting and shutting down. Once it has booted it uses other timers to keep the time, so to get Linux to access the D12887 again once it has booted, you have to reboot the system.

10.3 DEC21140A

The DEC21140A is a PCI Ethernet card. As the AM79C960, it is obsolete today but it provides a good example of a PCI device written in DML. Its specification can be found on the Internet, for example by looking for ec-qn7nc-te.ps.gz with Google.

The source code for the sample device can be found in the directory [simics]/src/devices/DEC21140A-dml. If you want to modify the DEC21140A-dml module yourself, we recommend that you set up a user project and copy the source code there, as described in section 3.4.

10.3.1 Architecture Design

Module DEC21140A-dml is a DML-implemented DEC21140A device, the -dml suffix is to differentiate with the old C-implemented one. The DEC21140A-dml implementation is more readable and modern by utilizing DML powerful modeling features, for example, by importing Simics PCI library, the PCI bus implementation is pretty simple and the latest PCI bus features are well supported.

Both AM79C960 and DEC21140 are Ethernet controllers, they have many common parts in Ethernet functionalities, for example, descriptors list (or ring), frame transmit and receive handling, etc., the biggest difference is that, AM79C960 uses ISA bus, yet DEC21140A uses PCI bus to connect to the host.

Another point that should be highlighted here is that, module DEC21140A-dml does not implement the PHY layer functionalities itself, instead, it reuses generic_eth_phy when building the Simics component, this kinds of design practics is highly recommended in Simics modeling.

With the help of PCI standard library and reusing existing PHY module, there are only few necessary device specific implementations to be done, including device status transaction, MAC layer packets handling, interrupt reporting, etc..

For better readability, the source code has been divided into two files:

The two files also indicate the two key parts for this device in modeling, the host connections which includes PCI bus and CSR registers, and the Ethernet connection.

Similar with other Simics modules, the DEC21140A-dml module is far from complete, it implements just enough functionalities that the device model can be used with Linux.

10.3.2 Interfaces Design

As described in the specification, the DEC21140A and the host driver communicate through two data structures: control and status registers (CSRs), descriptor lists and data buffers.

CSR registers are implemented in a Simics bank, its base address will be assigned by BIOS through PCI configuration space BAR register when booting the system. The descriptors lists locate in the host memory and are composed by pointers to the host memory, the DEC21140A-dml module reads and writes those descriptors and the real data through DMA function of the PCI bus.

With the help of Simics PCI library (by importing the file pci/common.dml), module DEC21140A-dml only needs to fill the necessary fields in the PCI configuration space to implement the PCI bus, such as vendor_id, device_id, BARs, etc..

For more info about how to model a PCI device in general, please refer to the Technology Guide "PCIe Modeling Library"

Ethernet connection is implemented by below interfaces:

Interface microwire is to connect to a serial EEPROM, where some customizable data, ex. MAC address, are stored in.

10.3.3 Registers Design

As a good practice, the CSR bank declares those registers and only implements small logic size registers in original place, all others logics, ex. interrupting and Ethernet frame transferring, are put to another place.

The CSR registers should be designed in the bank which function number is equal to the map_func assigned in PCI config bank pci_config BARs, so the CSR bank can be accessed by the software through PCI bus.

Register CSR0 contains the bus mode configuration, but at normal condition, we need not implement them, so leave most of them as dummy bits, for example the software reset, it's just logged without any real actions, this is good enough for the device module to work in the target linux version.

To avoid duplicating the same kind of register behavior, ex. checking device status before any writes to the register, three templates are designed, named as rw_tx_stopped, rw_rx_stopped and rw_rx_tx_stopped, this kind of design pattern is suggested in Simics modeling. For example:

template rw_tx_stopped {
    is write;
    method write(uint64 value) {
        if (csr.csr5.ts.val != TX_STOPPED && value != this.val)
            log spec_viol, 3:
                "writing 0x%x to the %s.%s.%s field with running Tx process",
                value, bank.name, reg.name, this.name;
        this.val = value;
    }
}

There are two registers that are implemented as unmapped registers, current_rx_address and current_tx_address, they are updated every time the CSR3 and CSR4 registers are written separately, and updated accordingly when the descriptors are processed. By doing like this, those two registers are kept as internal variables and not able to be accessed from the software directly, yet they are still automatically checkpointed by Simics and can be accessed by fields, like an ordinary register does.

10.3.4 Other Elements Design

Interrupt reporting usually needs some pre-checking for the current level, mask bits setting, enablement, etc. conditions, so in most cases we implement the interrupt raise and lower operation in a dedicated method, just like what DEC21140A-dml does to raise an interrupt bit:

method raise_interrupt(int bit) {
    if (csr.csr5.itr.val[bit] == 0)
        log info, 4: "raise interrupt %d", bit;
    csr.csr5.itr.val[bit] = 1;
    update_interrupts();
}
method update_interrupts() {
    if ((csr.csr5.itr.val & INT_ABNORMAL_MASK & csr.csr7.itr_mask.val) != 0)
        csr.csr5.ais.val = 1 & csr.csr7.ais_mask.val;
    else
        csr.csr5.ais.val = 0;
    if ((csr.csr5.itr.val & INT_NORMAL_MASK & csr.csr7.itr_mask.val) != 0)
        csr.csr5.nis.val = 1 & csr.csr7.nis_mask.val;
    else
        csr.csr5.nis.val = 0;

    if ((csr.csr5.nis.val | csr.csr5.ais.val) != 0)
        pci_config.pci_raise_interrupt();
    else
        pci_config.pci_lower_interrupt();
}

10.3.5 Run the DEC21140A-dml Module

In the QSP-x86 package you can find the script qsp-linux-dec21140.simics in the directory [simics]/targets/qsp-x86. This file creates a QSP-x86 based machine and attaches the DEC21140A-dml device to its PCI bus and the Ethernet link. It also detaches the internal network device of the QSP-x86 from the Ethernet link, so the DEC21140A is the only network device connected to the link.

Start the simulation and let the machine boot. Now you can see the network device and use it to ping the service node which is connected to the Ethernet link:

# ifconfig eth0
eth0      Link encap:Ethernet  HWaddr 10:10:10:10:26
          inet addr:10.10.0.100  Bcast:10.10.0.255  Mask:255.255.255.0
          ...
# ping 10.10.0.1
PING 10.10.0.1 (10.10.0.1): 56(84) bytes of data.
64 bytes from 10.10.0.1: icmp_seq=1 ttl=31 time=0.063 ms
64 bytes from 10.10.0.1: icmp_seq=2 ttl=31 time=0.062 ms
64 bytes from 10.10.0.1: icmp_seq=3 ttl=31 time=0.063 ms

10.4 Ethernet PHY chip

The previous section described how to use a DEC21140 device to communicate with an Ethernet network from Firststeps machine. In the setup, all Ethernet communication goes via a PHY chip, represented by the generic_eth_phy device. A PHY chip takes care of the physical layer of the Ethernet protocol; its primary task is to redirect traffic between a media access controller (MAC) device and an Ethernet network. The DEC21140A is one example of a MAC device; the Ethernet network is represented by an Ethernet link in Simics. Below interfaces are used to support those behaviors:

The generic_eth_phy device can be configured by target software via the Media Independent Interface (MII), which in Simics is represented by the mii_management interface.

The source code of the generic_eth_phy module is sparsely commented but rather simple. Most of the relevant documentation can be found in the IEEE 802.3 standard—in particular chapter 22, where the MII registers are specified.

The generic_eth_phy model is complete enough to be detected and configured correctly by most operating systems. Much of the configuration of a PHY controls which speed to use. The model ignores these settings; instead, the attribute tx_bandwidth can be used to manually configure the maximum transmission speed.

The PHY functionality has been divided between two files:

These files cannot be used by themselves; they need to be imported by a DML file that defines the device. The files have some additional requirements, which are documented in comments. The file generic_eth_phy.dml combines the two DML files into the generic_eth_phy device.

This model provides some functional tests in test sub-directory, which are helpful to verify the functionalities implementation, each test, as we suggest, only tests one functionality and is named on its testing purpose. For more details about how to write tests, please refer to chapter 16.

9 Advanced Programming with DML 11 Defining New Interface Types