C语言如何实现PCI设备编程?

99ANYc3cd6
预计阅读时长 36 分钟
位置: 首页 C语言 正文

对于绝大多数现代操作系统(如 Linux、Windows、macOS),应用程序级别的开发者不应该、也不能直接通过 C 语言操作 PCI 硬件

c 语言 编程 pci
(图片来源网络,侵删)

原因如下:

  1. 权限问题:直接操作硬件需要最高级别的系统权限(内核 Ring 0),而应用程序运行在用户态(Ring 3),没有权限直接读写物理内存和 I/O 端口。
  2. 硬件抽象:操作系统已经提供了标准的、统一的硬件访问接口(API),开发者应该使用这些接口,而不是去绕过它们,这保证了系统的稳定性和安全性。
  3. 设备即插即用:现代操作系统负责设备的发现、配置、加载驱动和资源分配,直接操作会破坏这个流程。

PCI 编程主要分为两个层面:

  1. 内核驱动开发:这是最核心、最底层的 PCI 编程,开发者编写 C 代码,作为操作系统内核的一部分,直接与 PCI 设备交互,这是实现网卡、显卡、声卡等设备驱动的方式。
  2. 用户空间应用程序:开发者通过操作系统提供的库或接口,从用户空间查询和管理 PCI 设备信息,但无法直接驱动硬件。

下面我将分别从这两个层面,并结合具体的 C 语言代码示例来讲解。


内核驱动编程 (以 Linux 为例)

在 Linux 内核中,有一个 PCI 子系统,它提供了一组结构体和函数,用于驱动程序发现、配置和与 PCI 设备通信。

c 语言 编程 pci
(图片来源网络,侵删)

核心概念和 API

  1. struct pci_dev:内核中代表一个 PCI 设备的核心结构体,驱动程序通过它来访问设备的所有信息。
  2. pci_register_driver():向 PCI 子系统注册你的驱动程序,这个函数告诉内核你的驱动支持哪些设备(通过 ID 表)。
  3. pci_match_device():PCI 子系统在发现设备时,会调用此函数来检查你的驱动是否支持该设备。
  4. pci_enable_device():启用 PCI 设备,包括请求内存、I/O 端口等资源。
  5. pci_request_regions():请求设备占用的内存和 I/O 区域,防止其他驱动使用。
  6. pci_map_bar() / ioremap():将设备的 BAR (Base Address Register) 映射到内核的虚拟地址空间,以便 CPU 可以访问。
  7. readb(), readw(), readl(), writeb(), writew(), writel():用于向映射后的内存地址(通常是设备的配置空间或内存 BAR)读写数据的函数。
  8. pci_disable_device() / pci_release_regions():在驱动卸载时,释放之前请求的资源。

简单的 PCI 驱动示例

这个示例将创建一个最简单的 PCI 驱动,它只会在加载时打印出找到的设备信息,然后映射其内存 BAR 并进行一次简单的读写操作。

驱动代码 (pci_simple_driver.c)

#include <linux/module.h>
#include <linux/pci.h>
#include <linux/io.h> // 用于 ioremap
#define PCI_VENDOR_ID_VIRTIO 0x1AF4
#define PCI_DEVICE_ID_VIRTIO_NET 0x1000
// 注意:我们使用一个已知的虚拟设备 ID 作为示例
// probe 函数:当 PCI 子系统找到一个匹配的设备时调用
static int pci_simple_probe(struct pci_dev *dev, const struct pci_device_id *id)
{
    int ret;
    void __iomem *bar0_addr; // 用于存储映射后的 BAR 地址
    resource_size_t bar0_size;
    printk(KERN_INFO "pci_simple_driver: Found device %04x:%04x\n",
           dev->vendor, dev->device);
    // 1. 启用设备
    ret = pci_enable_device(dev);
    if (ret) {
        printk(KERN_ERR "pci_simple_driver: Failed to enable device\n");
        return ret;
    }
    // 2. 请求设备占用的资源
    ret = pci_request_regions(dev, "pci_simple_driver");
    if (ret) {
        printk(KERN_ERR "pci_simple_driver: Failed to request regions\n");
        pci_disable_device(dev);
        return ret;
    }
    // 3. 获取 BAR0 的信息并映射
    bar0_size = pci_resource_len(dev, 0); // BAR0 的大小
    bar0_addr = pci_iomap(dev, 0, 0);    // 映射 BAR0
    if (!bar0_addr) {
        printk(KERN_ERR "pci_simple_driver: Failed to map BAR0\n");
        pci_release_regions(dev);
        pci_disable_device(dev);
        return -ENOMEM;
    }
    printk(KERN_INFO "pci_simple_driver: BAR0 mapped at %p, size %llu\n",
           bar0_addr, (unsigned long long)bar0_size);
    // 4. 读写操作示例 (假设 BAR0 是内存空间)
    // 注意:直接读写硬件很危险,这里仅作演示
    u32 original_value = readl(bar0_addr);
    printk(KERN_INFO "pci_simple_driver: Original value at BAR0: 0x%08x\n", original_value);
    u32 new_value = 0xDEADBEEF;
    writel(new_value, bar0_addr);
    printk(KERN_INFO "pci_simple_driver: Wrote new value 0x%08x to BAR0\n", new_value);
    u32 read_back_value = readl(bar0_addr);
    printk(KERN_INFO "pci_simple_driver: Read back value from BAR0: 0x%08x\n", read_back_value);
    // 5. 清理映射和资源
    pci_iounmap(dev, bar0_addr);
    pci_release_regions(dev);
    pci_disable_device(dev);
    return 0;
}
// remove 函数:当设备被移除时调用
static void pci_simple_remove(struct pci_dev *dev)
{
    printk(KERN_INFO "pci_simple_driver: Removing device %04x:%04x\n",
           dev->vendor, dev->device);
}
// PCI 设备 ID 表,告诉内核我们的驱动支持哪些设备
static const struct pci_device_id pci_simple_ids[] = {
    { PCI_DEVICE(PCI_VENDOR_ID_VIRTIO, PCI_DEVICE_ID_VIRTIO_NET) },
    { 0, } // 结束标记
};
MODULE_DEVICE_TABLE(pci, pci_simple_ids);
// PCI 驱动结构体
static struct pci_driver pci_simple_driver = {
    .name = "pci_simple_driver",
    .id_table = pci_simple_ids,
    .probe = pci_simple_probe,
    .remove = pci_simple_remove,
};
// 模块初始化函数
static int __init pci_simple_init(void)
{
    printk(KERN_INFO "pci_simple_driver: Loading driver\n");
    return pci_register_driver(&pci_simple_driver);
}
// 模块退出函数
static void __exit pci_simple_exit(void)
{
    printk(KERN_INFO "pci_simple_driver: Unloading driver\n");
    pci_unregister_driver(&pci_simple_driver);
}
module_init(pci_simple_init);
module_exit(pci_simple_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple PCI driver example");

编译和加载

你需要一个内核开发环境来编译这个模块。

# 编译成 .ko 内核模块
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
# 加载模块 (需要 root 权限)
sudo insmod pci_simple_driver.ko
# 查看内核日志
dmesg | tail
# 卸载模块
sudo rmmod pci_simple_driver

用户空间应用程序编程

在用户空间,我们不能直接驱动硬件,但可以查询 PCI 设备的详细信息,Linux 提供了两种主要方式:

  1. /proc/bus/pci/:一个虚拟文件系统,直接暴露 PCI 配置空间。
  2. libpci:更现代、更方便的方式,提供了 C API 来访问 PCI 信息。lspci 命令就是用这个库实现的。

使用 libpci 库编程

这是推荐的用户空间编程方式。

安装开发库

# 对于 Debian/Ubuntu
sudo apt-get install libpci-dev
# 对于 Fedora/CentOS
sudo yum install pciutils-devel

C 语言示例代码 (pci_info.c)

这个程序会列出系统中的所有 PCI 设备,并打印出它们的基本信息。

#include <stdio.h>
#include <stdlib.h>
#include <pci/pci.h> // libpci 的头文件
int main()
{
    struct pci_access *pacc; // PCI 访问对象
    struct pci_dev *dev;     // PCI 设备对象
    // 1. 初始化 PCI 访问
    pacc = pci_alloc();
    pci_init(pacc);
    pci_scan_bus(pacc); // 扫描所有总线上的设备
    // 2. 遍历所有找到的设备
    for (dev = pacc->devices; dev; dev = dev->next) {
        // 3. 填充设备信息 (从配置空间读取)
        pci_fill_info(dev, PCI_FILL_IDENT | PCI_FILL_BASES | PCI_FILL_CLASS);
        // 4. 打印设备信息
        printf("Bus: %02d, Device: %02d, Function: %02d\n",
               dev->bus, dev->dev, dev->func);
        printf("  Device ID: %04x:%04x\n", dev->vendor_id, dev->device_id);
        printf("  Class: %s\n", pci_lookup_name(pacc, buf, sizeof(buf),
                                                PCI_LOOKUP_CLASS, dev->class_code));
        printf("  Vendor: %s\n", pci_lookup_name(pacc, buf, sizeof(buf),
                                               PCI_LOOKUP_VENDOR, dev->vendor_id, dev->device_id));
        printf("  Device: %s\n", pci_lookup_name(pacc, buf, sizeof(buf),
                                               PCI_LOOKUP_DEVICE, dev->vendor_id, dev->device_id));
        if (dev->base_addr[0] != 0) {
            printf("  BAR0: 0x%llx\n", (unsigned long long)dev->base_addr[0]);
        }
        printf("  IRQ: %d\n", dev->irq);
        printf("--------------------------------\n");
    }
    // 5. 清理
    pci_cleanup(pacc);
    return 0;
}

编译和运行

# 编译,链接 libpci 库
gcc pci_info.c -o pci_info -lpci
# 运行 (普通用户即可)
./pci_info

输出示例:

Bus: 00, Device: 00, Function: 00
  Device ID: 8086:31e4
  Class: Host bridge
  Vendor: Intel Corporation
  Device: 4th Gen Core Processor DRAM Controller
  BAR0: 0x0
  IRQ: 0
--------------------------------
Bus: 00, Device: 01, Function: 0
  Device ID: 8086:31e6
  Class: PCI bridge
  Vendor: Intel Corporation
  Device: 4th Gen Core Processor PCIe Root Port 1
  BAR0: 0x0
  IRQ: 16
--------------------------------
Bus: 01, Device: 00, Function: 0
  Device ID: 1af4:1000
  Class: Network controller
  Vendor: Red Hat, Inc.
  Device: VirtIO network card
  BAR0: 0xfebfc000
  IRQ: 19
--------------------------------
...

Windows 下的 PCI 编程

在 Windows 下,原理类似,但 API 完全不同。

  • 内核驱动:使用 Windows Driver Model (WDM) 或 Windows Driver Framework (WDF),通过 Zw* 系列函数(如 ZwReadFile, ZwWriteFile)与用户态通信,或者直接使用 WdfIoTargetSendInternalIoctlSynchronously 等函数,访问硬件通常通过 READ_PORT_UCHAR, WRITE_PORT_UCHAR 等函数,但这只能在驱动中完成。
  • 用户态应用程序:使用 SetupAPI 来枚举和获取设备信息(如硬件 ID、实例路径),或者,如果你有一个内核驱动提供了接口,你可以使用 DeviceIoControl 函数与驱动通信,间接控制硬件。

Windows 用户态示例 (使用 SetupAPI)

这个 C 程序会枚举所有 PCI 设备。

#include <windows.h>
#include <setupapi.h>
#include <tchar.h>
#include <stdio.h>
#pragma comment(lib, "setupapi.lib")
int main()
{
    HDEVINFO hDevInfo;
    SP_DEVINFO_DATA DeviceInfoData;
    DWORD i;
    // 1. 获取所有 PCI 设备的设备信息集
    // "PCI" 是一个已知的类名
    hDevInfo = SetupDiGetClassDevs(&GUID_DEVCLASS_PCI, NULL, NULL, DIGCF_PRESENT | DIGCF_ALLCLASSES);
    if (hDevInfo == INVALID_HANDLE_VALUE) {
        printf("SetupDiGetClassDevs failed with error: %d\n", GetLastError());
        return 1;
    }
    // 2. 遍历设备信息集中的每个设备
    DeviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
    for (i = 0; SetupDiEnumDeviceInfo(hDevInfo, i, &DeviceInfoData); i++)
    {
        TCHAR szHardwareID[MAX_PATH];
        DWORD dwSize = MAX_PATH;
        // 3. 获取设备的硬件 ID
        if (SetupDiGetDeviceRegistryProperty(hDevInfo, &DeviceInfoData, SPDRP_HARDWAREID,
                                             NULL, (PBYTE)szHardwareID, dwSize, &dwSize))
        {
            _tprintf(_T("Device %d Hardware ID: %s\n"), i, szHardwareID);
        }
    }
    // 5. 清理
    SetupDiDestroyDeviceInfoList(hDevInfo);
    return 0;
}
层面 操作系统 核心方法 适用场景
内核驱动 Linux struct pci_dev, pci_register_driver, ioremap, readl/writel 编写网卡、显卡、声卡等硬件的驱动程序。
Windows WDF/WDM, READ_PORT_UCHAR, DeviceIoControl 编写 Windows 平台的硬件驱动。
用户空间 Linux /proc/bus/pci (过时), libpci 库 (lspci 的实现) 查询硬件信息、设备监控、诊断工具。
Windows SetupAPI, DeviceIoControl (与通信) 查询硬件信息、开发设备管理软件、与自定义驱动通信。

如果你想在 C 语言中“编程 PCI”,首先要明确你的目标。

  • 如果你想让硬件工作(写一个网卡驱动),你需要编写内核驱动,这非常复杂,需要对操作系统内核和硬件有深入的了解。
  • 如果你想查看或管理 PCI 设备(写一个类似 lspci 的工具),你应该在用户空间使用 libpci (Linux) 或 SetupAPI (Windows) 这样的库,这是相对安全且容易实现的。
-- 展开阅读全文 --
头像
dede会员如何实现QQ登录绑定?
« 上一篇 04-05
dede织梦帝国CMS如何选择?
下一篇 » 04-05

相关文章

取消
微信二维码
支付宝二维码

目录[+]