Previous - Up - Next

11.2   Writing a PCI Device in DML

11.2.1   Overview

Note: This chapter assumes a basic knowledge of the PCI standard, how PCI is modelled in Simics and how to program in DML. Refer to the Simics User Guide for Simics PCI modelling information.

Creating a minimal PCI device is very easy: you just need to import the file pci-device.dml in your device. It will define the following:

In this section, we will use as an example the model of a DEC21140A network card. The source code of the model is available in the src/devices/DEC21140A-dml/ directory and section 10.3 contains a more complete description. Let us now create a minimal DEC21140A:

dml 1.0;

device DEC21140A_dml;

import "utility.dml";
import "io-memory.dml";
import "pci-device.dml";

We now have a working—although not very useful—PCI device. Let us have a closer look at what is provided by pci-device.dml.

11.2.2   pci_config Bank

The pci_config bank defines the standard PCI configuration registers for a type 0 PCI header. Following the PCI standard, it interprets all accesses as little-endian.

Some of the registers defined by pci_config have a default value. Others need to be customized before the device can be used. The vendor_id, device_id, revision_id, class_code, subsystem_vendor_id and subsystem_id registers are handled in a special way to cover the different possible implementations. They are declared as maybe_constant registers, which gives them the following behavior: if you set the parameter value in one of them, this register will act as a constant. If you do not set value, it will act as a read-write register, and take hard_reset_value as initial value.

In the DEC21140A device, pci_config is extended to give some of these registers a constant value:

bank pci_config {
    register revision_id { parameter value = 0x21; }
    register class_code { parameter value = 0x020000; }
    register subsystem_vendor_id { parameter value = 0x0E11; }
    register subsystem_id { parameter value = 0xB0BB; }

Base address registers are not defined by default, but a set of templates is provided for you to customize them. Refer to section 11.2.3 for more information.

The pci_config bank defines a number of functions that can be called to let the PCI device perform various actions:

pci_raise_interrupt() and pci_lower_interrupt()
Respectively raise and lower the interrupt pin defined by the value of the register interrupt_pin. A call to one of these functions has no effect if the interrupt is already in the desired status.
Send a SERR# signal to the PCI bus.
This function is described in section Template, Parameters, and Functions.

As an example, the DEC21140A model handles interrupt in the following way (from DEC21140A-eth.dml):

// Update interrupt summary and PCI interrupt pin 
// according to current status
method update_interrupts()
    // update interrupt summary

    // set PCI interrupt pin
    if (($csr.csr5.nis | $csr.csr5.ais) != 0)
        call $pci_config.pci_raise_interrupt();
        call $pci_config.pci_lower_interrupt();

The pci_config bank also provides a number of functions that you can override with your own implementation to react to some PCI events:

Called when an Interrupt Acknowledge cycle has been sent to the bus. The default function only prints a warning. Unless your system uses Interrupt Acknowledge cycle, which is pretty uncommon, you can leave this function as it is.
Called when the bus is reset. The default function performs a soft-reset on the device and remove all BAR mappings. You should override this function if you need to perform some special action at reset that is can not be handled by the soft_reset() functions of the registers.

The parameter busmaster can be overridden in the pci_config bank to prevent the device from doing DMA accesses. It defaults to true.

11.2.3   Base Address Registers


The base address registers (BARs) are not defined by pci_config, in order to leave you complete freedom to organize them. By default, they will map the PCI device itself in either the PCI memory or I/O space, depending on the type of BAR defined. To each BAR is associated a function number (controlled by the map_func parameter) that will be used to identify which mapping of the PCI device is accessed. To let some automatic functions handle the base address registers, you will need to provide a list of active BAR by overriding the base_address_registers parameter.

Let us take an example. The following device defines one I/O BAR and two memory BARs:

bank pci_config {
    parameter base_address_registers = ["base_address_0",

    register base_address_0 @ 0x10 is (io_base_address) {
        parameter size_bits = 7;
        parameter map_func = 2;
    register base_address_1 @ 0x14 is (memory_base_address) {
        parameter size_bits = 12;
        parameter map_func = 2;
    register base_address_2 @ 0x18 is (memory_base_address) {
        parameter size_bits = 14;
        parameter map_func = 6;
    // Other base address registers are not used
    register base_address_3 @ 0x1C is (no_base_address);
    register base_address_4 @ 0x20 is (no_base_address);
    register base_address_5 @ 0x24 is (no_base_address);

The BAR base_address_0 is defined as mapping into PCI I/O space. The map_func parameter indicates that our PCI device will be mapped with the function number 2. The size_bits parameter determines how many address bits will be used in the BAR, and thus how big the mapping is. A size_bits of 7 means that the bits [31:7] will be used, thus the mapping will be 128 bytes long (2 << 7).

The BAR base_address_1 is defined as mapping into PCI memory space. The PCI device will be mapped in a 4096 bytes space. The mapping will use the function number 2 as well, thus the PCI device won't see any difference between an I/O access via the BAR0 space and a memory access via the BAR1 space.

The BAR base_address_2 is defined as mapping into PCI memory space. The PCI device will be mapped in a 16384 bytes space. The mapping will use the function number 6.

In DML, the function number associated to the mapping is usually used to determine which register bank should be accessed (each register bank has to have a function number). This makes it very easy to map a given register bank via the PCI BAR mechanism. If we add the following code to our example:

bank first_bank {
    parameter function = 2;

bank second_bank {
    parameter function = 6;

The bank first_bank will be mapped by BAR0 in I/O space and BAR1 in memory-space. The bank second_bank will be accessible via the space define by BAR2. No additional code is necessary for the device to automatically receive accesses to the right register bank.

Note: The pci_config bank is defined as having a function number of 255. This is a Simics convention that allows the PCI bus object to automatically map the configuration registers of PCI devices in the configuration space and ensure that the right register bank is handling the access.

Template, Parameters, and Functions

The following templates are provided to define base address registers:

Defines a 32-bit memory base address register.
Defines a 64-bit memory base address register. Note that the register will be 8 bytes wide.
Defines a 32-bit I/O base address register.
Defines a 4-bytes long non-implemented base address register.

The following parameters can be customized in a given base address register:

Function number associated to the mapping.
Least significant bit for the base address, which indicates the size of the mapping.

A base address register is composed of the following fields:

Note that the three first fields have no functional meaning in Simics, i.e., changing s will not change in which space the mapping is done. The only field having side-effects is the base field.

A base register address gives you access to the following functions:

Triggers a de-mapping and remapping of the space controlled by the BAR. This is a useful function if the base is changed by some other means than a memory transaction.
pci_mapping_enabled() -> (bool enabled)
This function is called to check whether a mapping is enabled or not. By default, it returns always true, but you can override it to check, for example, another register before enabling the mapping. Note that the standard control provided by the bits io and mem in the command register in pci_config are taken into account independently of the value returned by pci_mapping_enabled().
pci_mapping_object() -> (conf_object_t *obj)
This function is called to obtain the object that should be mapped by the BAR. By default, the PCI object itself is returned, but this can be customized to be any object. Note that this function is called when removing and when adding the mapping to the corresponding PCI space, so it is responsible for returning the same value for the automatic mapping mechanism to work properly.
pci_mapping_target() -> (conf_object_t *target)
This function is called to obtain the target that should be mapped by the BAR. By default, no target is used. This function allows a PCI device to create a translator or bridge mapping for another object. It is only called when mapping in the PCI space.
pci_mapping_customize(map_info_t *info)
This function is called after the map_info_t information for the mapping has been set up but before the mapping is done. It allows you to customize the map_info_t structure (changing the base address, the size, the priority, etc.) in any way you want, except for the function number associated with the mapping, as this would break the automatic mapping mechanism. The device will print out a warning if you attempt to do that and ignore the new value.
pci_bar_size_bits() -> (int bits)
This function is called when reading the BAR value and when creating the mapping. By default, size_bits is returned. By overriding this function it is possible to create dynamically resizable BARs, where the size is configured by e.g. another configuration register.
pci_bar_is_64() -> (bool is_bar_64)
This function is called when reading the BAR value and when creating the mapping, but only for 64-bit memory BARs. By default, true is returned. If false is returned, the base address will be limited to 32 bits. In other words, it allows you to dynamically reconfigure a 64-bit BAR to 32 bits.

The pci_config bank also gives you access to the update_all_mappings() function that calls update_mapping() on all active base address registers (defined by the base_address_registers parameter).

Expansion ROM

The expansion_rom_base register is an already customized base address register that will map the ROM object provided by the attribute expansion_rom. To enable this register, you need to set the enabled parameter to true, and specify the map_func and size_bits parameters.

11.2.4   Functions

A number of functions are provided to help writing PCI devices:

pci_data_from_memory() and pci_value_from_memory()
pci_data_from_memory(addr_space_t space, void *buffer,
                     uint64 address, uint64 size)
    -> (exception_type_t ex)

pci_value_from_memory(addr_space_t space, uint64 address,
                      uint8 size) 
    -> (uint64 value, exception_type_t ex)

Perform a DMA read.

pci_data_to_memory() and pci_value_to_memory()
pci_data_to_memory(addr_space_t space, void *buffer, 
                   uint64 address, uint64 size)
    -> (exception_type_t ex)

method pci_value_from_memory(addr_space_t space, uint64 address, 
                             uint8 size)
    -> (uint64 value, exception_type_t ex)

Perform a DMA write.

For example, the DEC21140A model reads its descriptors with the following function:

// Read a descriptor from memory at address 'addr' to fill in 'desc'
method read_descriptor(descriptor_t *desc, uint32 addr) 
    -> (exception_type_t ex)
    local int i;
    local uint32 *desc_data = cast(desc, uint32 *);

    log "info", 4, 0: "Fetching a descriptor at address 0x%x", addr;
    call $pci_data_from_memory(Sim_Addr_Space_Memory, 
                               cast(desc, void *), addr,
        -> (ex);

11.2.5   PCI and PCIe Capabilities

The command DML PCI code contains templates for a number of capabilities. These templates do not provide any side-effects. Registers are behaving as they are expected to by software (read_only, read_write, write_1_clears, etc.) but all other side-effects should be customized.

Each template provides two parameters that should be overridden: an offset parameter that defines where the capability begins in the configuration registers space, and a next pointer parameter that defines where the next capability will be.

A device with the Power Management capability defined by the PCI standard could be implemented this way:

bank pci_config {
    // Power Management
    parameter pm_offset = 0x50;
    parameter pm_next_ptr = 0x58;
    is defining_pci_power_management_capability;

    // Customize the Power Management capability
    register pm_capabilities {
        field version  { parameter hard_reset_value = 0x2; }
        field ds_init  { parameter hard_reset_value = 0x1; }
        field pme_supp { parameter hard_reset_value = 0x19; }

These are the available capabilities with their related parameters:

Power Management
template defining_pci_power_management_capability
    parameter pm_offset
    parameter pm_next_ptr
MSI (32 bits)
template defining_msi_capability
    parameter msi_offset
    parameter msi_next_ptr
MSI (64 bits)
template defining_msi64_capability
    parameter msi_offset
    parameter msi_next_ptr
template defining_pcix_capability
    parameter pcix_offset
    parameter pcix_next_ptr
PCI Express
template defining_pci_express_capability
    parameter exp_offset
    parameter exp_next_ptr
PCI Express Advanced Error Reporting Capability
template defining_pcie_advanced_error_reporting_capability
    parameter aer_offset
    parameter aer_next_ptr
Device Serial Number Extended Capability
template defining_pci_device_serial_number_extended_capability
    parameter dsn_offset
    parameter dsn_next_ptr
Device Power Budgeting Extended Capability
template defining_pci_device_power_budgeting_extended_capability
    parameter dpb_offset
    parameter dpb_next_ptr
Virtual Channel Extended Capability
template defining_pci_virtual_channel_extended_capability
    parameter vce_offset
    parameter vce_next_ptr

11.2.6   Source Code

For more detailed information, the source code of the PCI templates and functions is available in your installation in the directory [simics]/host/lib/dml/1.0/, in the files pci-device.dml and pci-common.dml.

Previous - Up - Next