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.
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:
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(); else 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:
The parameter busmaster can be overridden in the pci_config bank to prevent the device from doing DMA accesses. It defaults to true.
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", "base_address_1", "base_address_2"]; 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.
The following templates are provided to define base address registers:
The following parameters can be customized in a given base address register:
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:
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).
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.
A number of functions are provided to help writing PCI devices:
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(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, sizeof(descriptor_t)) -> (ex); ... }
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:
template defining_pci_power_management_capability parameter pm_offset parameter pm_next_ptr
template defining_msi_capability parameter msi_offset parameter msi_next_ptr
template defining_msi64_capability parameter msi_offset parameter msi_next_ptr
template defining_pcix_capability parameter pcix_offset parameter pcix_next_ptr
template defining_pci_express_capability parameter exp_offset parameter exp_next_ptr
template defining_pcie_advanced_error_reporting_capability parameter aer_offset parameter aer_next_ptr
template defining_pci_device_serial_number_extended_capability parameter dsn_offset parameter dsn_next_ptr
template defining_pci_device_power_budgeting_extended_capability parameter dpb_offset parameter dpb_next_ptr
template defining_pci_virtual_channel_extended_capability parameter vce_offset parameter vce_next_ptr
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.