Skip to main content

Scanning the PCI bus

Scanning the PCI bus allows you to find your device.

note

If you don't know what PCI is, then feel free to do some extra googling.

Quick summary: PCI is a system that can let you locate devices like USB controllers, video cards, etc. easily.
Each PCI device has a vendor and device ID. It also has a class code (so you can identify generic devices, extremely helpful).

Each PCI device has configuration space as well, which allows you to toggle features of the device and read information about it.

For more information, see the OSDev wiki page

Start

Let's look for our PCI device. Hexahedron provides many kernel APIs for this.

First, include the kernel PCI header:

drivers/example_driver/main.c
#include <kernel/drivers/pci.h>

We now have access to the entire kernel PCI API. Hexahedron provides a vast API, so let's go through how to use the most common for a driver.

Generic PCI device

First let's go over the PCI device structure - pci_device_t

hexahedron/include/kernel/drivers/pci.h
typedef struct pci_device {
uint8_t bus; // Bus
uint8_t slot; // Slot
uint8_t function; // Function
uint8_t class_code; // Class code
uint8_t subclass_code; // Subclass code
uint16_t vid; // Vendor ID
uint16_t pid; // Product ID
void *driver; // Driver-specific field

uint32_t msi_offset; // MSI offset in configuration space (or 0xFF if not found)
uint32_t msix_offset; // MSI-X offset in configuration space (or 0xFF if not found)

int valid; // !!!: Valid device hack
uint32_t msix_index; // MSIX index
} pci_device_t;

The only fields you want are at the top. These PCI device structures are all arranged in an array so updating one will update it for everyhting else.

bus, slot, and function are all specific to the location of the PCI device on the system.

class_code and subclass_code give details on the function of the device

vid and pid provide the aforementioned vendor and device IDs

caution

Hexahedron's PCI API tends to flip flop between using pci_device_t and accepting bus, slot, func manually.

Gradually, the goal is to switch over to pci_device_t fully.

Scanning the PCI bus

The API to scan the PCI bus is pci_scanDevice, declared as so:

int pci_scanDevice(pci_scan_callback_t callback, pci_scan_parameters_t *parameters, void *data); 

callback contains the scan callback. For each device that matches parameters, callback is called.

parameters will be covered later (note that it is optional, you can leave it as NULL to enumerate every PCI device)

Callback have the following signature:

/**
* @brief PCI scan callback function
* @param device The device currently being checked
* @param data Driver-specific
* @returns 1 on an error, 0 on success
*/
typedef int (*pci_scan_callback_t)(pci_device_t *device, void *data);

data is passed directly as it was in pci_scanDevice.

Returning 1 means that an error occurred, and to stop the scan and return an error from pci_scanDevice

Scan parameters

Scan parameters are the most complicated part of this interface. Let's look at an example from the UHCI driver.

drivers/usb/uhci/uhci.c
pci_scan_parameters_t params = {
.class_code = 0x0C, // Serial bus controller
.subclass_code = 0x03, // Universal Serial Bus
.id_list = NULL, // Optional ID list
}

This means that the scan callback will only be called if a device with that class and subclass code is found.

But sometimes, you need to do some VID/PID identification. That's fine, let's go over how to do that. This sample is taken from the RTL8169 driver.

drivers/net/rtl8169/rtl8169.c
pci_id_mapping_t id_list[] = {
{ .vid = 0x10ec, .pid = { 0x8161, 0x8168, 0x8169, 0x2600, PCI_NONE } },
{ .vid = 0x1259, .pid = { 0xc107, PCI_NONE } },
{ .vid = 0x1737, .pid = { 0x1032, PCI_NONE } },
{ .vid = 0x16ec, .pid = { 0x0116, PCI_NONE } },
PCI_ID_MAPPING_END
};

pci_scan_parameters_t params = {
.class_code = 0,
.subclass_code = 0,
.id_list = id_list
};

Ignore the specific hexadecimals. They are not important.

The important part is that the id_list is an array of pci_id_mapping_t structures, finished with PCI_ID_MAPPING_END
The PID list must also be finished with PCI_NONE , else the kernel will most likely crash

There also exists a PCI_PID_ACCEPT_ALL, which can be set as { .vid = VID, .pid = PCI_PID_ACCEPT_ALL },. It means that all PIDs will be accepted.

Reading and writing to the PCI namespace

You can read/write to the PCI namespace with the following functions:

/**
* @brief Read a specific offset from the PCI configuration space
*
* Uses configuration space access mechanism #1.
* List of offsets is header-specific except for general header layout, see pci.h
*
* @param bus The bus of the PCI device to read from
* @param slot The slot of the PCI device to read from
* @param func The function of the PCI device to read (if the device supports multiple functions)
* @param offset The offset to read from
* @param size The size of the value you want to read from. Do note that you'll have to typecast to this (max uint32_t).
*
* @returns Either PCI_NONE if an invalid size was specified, or a value according to @c size
*/
uint32_t pci_readConfigOffset(uint8_t bus, uint8_t slot, uint8_t func, uint8_t offset, int size);


/**
* @brief Write to a specific offset in the PCI configuration space
*
* @param bus The bus of the PCI device to write to
* @param slot The slot of the PCI device to write to
* @param func The function of the PCI device to write (if the device supports multiple functions)
* @param offset The offset to write to
* @param value The value to write
* @param size How big of a value to write
*
* @returns 0 on success
*/
int pci_writeConfigOffset(uint8_t bus, uint8_t slot, uint8_t func, uint8_t offset, uint32_t value, int size)

These functions are self explanatory.

Getting BARs

Getting BARs of PCI devices is easily done with pci_readBAR:

/**
* @brief Auto-determine a BAR type and read it using the configuration space
*
* Returns a pointer to an ALLOCATED @c pci_bar_t structure. You MUST free this structure
* when you're finished with it!
*
* @param bus Bus of the PCI device
* @param slot Slot of the PCI device
* @param func Function of the PCI device
* @param bar The number of the BAR to read
*
* @returns A @c pci_bar_t structure or NULL
*/
pci_bar_t *pci_readBAR(uint8_t bus, uint8_t slot, uint8_t func, uint8_t bar);
danger

The structure returned by pci_readBAR is allocated and you must free it.

The PCI bar structure returned looks like this:

hexahedron/include/kernel/drivers/pci.h
/* BAR types */
#define PCI_BAR_MEMORY32 ... // 32-bit memory space BAR (physical RAM)
#define PCI_BAR_IO_SPACE ... // I/O space BAR, can reside at any memory address
#define PCI_BAR_MEMORY16 ... // 16-bit memory space BAR (reserved nowadays)
#define PCI_BAR_MEMORY64 ... // 64-bit memory space BAR


typedef struct pci_bar {
int type; // Type of the BAR
uint64_t size; // Size of the BAR
int prefetchable; // Whether the BAR is prefetchable (it does not read side effects)
uint64_t address; // Address of the BAR. Note that it doesn't take up all of this space.s
} pci_bar_t;

Interrupts

Interrupts are complicated in PCI, and Ethereal's interfaces can be convoluted.

There are three types of PCI interrupts:

  • MSIX interrupt (recommended, Ethereal allows you to get multiple)
  • MSI interrupt (good)
  • Pin interrupt (bad, may be incompatible)
danger

"May be incompatible" is an understatement. Because of modern hardware and bad design, ACPI AML parsing is required to parse the GSI of the PCI pin interrupt

This interface is not yet implemented in Ethereal (the code is prototyped), so for all intensive purposes assume pin interrupts will not work, period

danger

Interrupts are the worst part of the Ethereal PCI API

Ethereal has two interfaces for all three interrupt types:

uint8_t pci_enableMSI(uint8_t bus, uint8_t slot, uint8_t func);
uint8_t pci_getInterrupt(uint8_t bus, uint8_t slot, uint8_t func);

pci_enableMSI will also attempt to enable MSI-X if it is available (and it will be preferred).

pci_getInterrupt is for legacy pin interrupts and again, should not be used

Putting it all together

Putting all of this together, let's expand our example driver.

/**
* @brief PCI example
*/

#include <kernel/loader/driver.h>
#include <kernel/debug.h>
#include <kernel/drivers/pci.h>


int driver_scanCallback(pci_device_t *device, void *context) {
dprintf(INFO, "Found matching device at bus %d slot %d func %d\n", device->bus, device->slot, device->function);

// Get BAR0
pci_bar_t *b = pci_getBAR(device->bus, device->slot, device->function, 0);
assert(b);

// Now what???
// We've got to map the BAR in!

return 0;
}

int driver_init(int argc, char *argv[]) {
dprintf(DEBUG, "Scanning for example PCI device with VID 1234 and PID 1111...\n");

pci_id_mapping_t id_list[] = {
{ .vid = 0x1234, .pid = { 0x1111, PCI_NONE }},
PCI_ID_MAPPING_END
};

pci_scan_parameters_t params = {
.class_code = 0, // No class code
.subclass_code = 0, // No class code
.id_list = id_list
};

return pci_scan(driver_scanCallback, &params, NULL);
}

int driver_deinit() { return DRIVER_STATUS_SUCCESS; }

struct driver_metadata driver_metadata = {
.name = "Example Driver",
.author = "Your Name Here",
.init = driver_init,
.deinit = driver_deinit
};

This updated driver scans for a PCI device matching 1234:1111.

tip

This is the PCI VID:PID of the Bochs Graphics Adapter, so QEMU will in fact pick it up.

We will not be creating a BGA driver, as an implementation is already available in drivers/graphics/bga

Instead this driver creation tutorial will cover a fake magic PCI device.

Continue on 3 - allocating memory.