PCI device enumeration using ports 0xCF8, 0xCFC

written on Wed 02 July 2014

This is how you can enumerate your PCI devices by using ports 0xCF8 and 0xCFC. Please note that this code is intended to run from the privileged mode (ring 0), thus it's compiled as a kernel module for Linux.

The PCI specification can be downloaded from here (or, alternatively, you can search here if the previous link is no longer working).

The example below will read the uint32_t from the address of 0 (which is the last argument of the r_pci_32 function) of this structure:

PCI Configuration Space structure

This is the PCI Configuration Space structure, which holds basic information about the PCI device.

Here is the source code of the module:

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <asm/io.h>

const u32 PCI_ENABLE_BIT     = 0x80000000;
const u32 PCI_CONFIG_ADDRESS = 0xCF8;
const u32 PCI_CONFIG_DATA    = 0xCFC;

// func - 0-7
// slot - 0-31
// bus - 0-255
u32 r_pci_32(u8 bus, u8 device, u8 func, u8 pcireg) {
        // unsigned long flags;
        // local_irq_save(flags)

        outl(PCI_ENABLE_BIT | (bus << 16) | (device << 11) | (func << 8) | (pcireg << 2), PCI_CONFIG_ADDRESS);
        u32 ret = inl(PCI_CONFIG_DATA);

        // local_irq_restore(flags);
        return ret;
}

static __init int init_pcilist(void) {
        u8 bus, device, func;
        u32 data;

        for(bus = 0; bus != 0xff; bus++) {
                for(device = 0; device < 32; device++) {
                        for(func = 0; func < 8; func++) {
                                data = r_pci_32(bus, device, func, 0);

                                if(data != 0xffffffff) {
                                        printk(KERN_INFO "bus %d, device %d, func %d: vendor=0x%08x\n", bus, device, func, data);
                                }
                        }
                }
        }
        return 0;
}

static __exit void exit_pcilist(void) {
        return;
}

module_init(init_pcilist);
module_exit(exit_pcilist);

This code is not adapted for production code, because if suffers from race conditions. It doesn't lock the access to the ports, so the risk of using it is that you will end up with some device in unspecified state. In a controlled, home environment (virtual machine) it works quite nicely though.

The locking part could be probably implemented by using:

raw_spin_lock_irqsave(& pci_config, lock, ...);

It should also temporarily disable interrupts. You could probably also lock the access by using request_region. But, instead of directly reading those ports by outl and inl, it's better to use Linux' mechanism of reading PCI data, which lives insite the pci_bus object in the ops field. More information about this mechanism can be found inside the arch/x86/pci/direct.c file.