给Linux驱动开发者的PCI配置空间Header实战指南:手把手教你读懂BAR、中断与命令寄存器
Linux驱动开发者实战指南:深度解析PCI配置空间关键寄存器
在Linux内核开发领域,PCI/PCIe设备的驱动编写一直是系统级编程的核心技能之一。不同于应用层开发,驱动开发者需要直接与硬件寄存器打交道,而PCI配置空间就是这场"硬件对话"的第一现场。本文将聚焦struct pci_dev背后的寄存器世界,特别是BAR、中断和命令寄存器这些驱动开发中的高频操作对象。
1. PCI配置空间基础与内核访问机制
PCI配置空间是PCI/PCIe设备的"身份证"和"控制面板",它包含了设备的所有基础信息和运行时控制接口。在Linux内核中,我们通过一系列API与这个空间交互:
#include <linux/pci.h> // 读取配置空间的基本函数 int pci_read_config_byte(struct pci_dev *dev, int where, u8 *val); int pci_read_config_word(struct pci_dev *dev, int where, u16 *val); int pci_read_config_dword(struct pci_dev *dev, int where, u32 *val); // 写入配置空间的对应函数 int pci_write_config_byte(struct pci_dev *dev, int where, u8 val); int pci_write_config_word(struct pci_dev *dev, int where, u16 val); int pci_write_config_dword(struct pci_dev *dev, int where, u32 val);这些函数中的where参数就是寄存器在配置空间中的偏移地址。例如,要读取设备的Vendor ID(位于偏移0x00处),可以这样操作:
u16 vendor_id; pci_read_config_word(dev, 0x00, &vendor_id);提示:在实际驱动开发中,更常见的做法是使用
pci_dev结构体已经缓存的部分字段,如dev->vendor和dev->device,而不是每次都去读取配置空间。
配置空间的标准布局如下图所示(以Type 0 Header为例):
| 偏移量 | 寄存器名称 | 大小 | 访问权限 |
|---|---|---|---|
| 0x00 | Vendor ID | 16位 | 只读 |
| 0x02 | Device ID | 16位 | 只读 |
| 0x04 | Command | 16位 | 读写 |
| 0x06 | Status | 16位 | 读写 |
| 0x08 | Revision ID / Class Code | 32位 | 只读 |
| 0x0C | Header Type | 8位 | 只读 |
| 0x10 | BAR0 | 32位 | 读写 |
| ... | ... | ... | ... |
| 0x3C | Interrupt Line | 8位 | 读写 |
| 0x3D | Interrupt Pin | 8位 | 只读 |
2. BAR寄存器:地址空间映射的艺术
Base Address Register(BAR)是PCI设备与系统内存或I/O空间交互的桥梁。每个BAR对应设备的一段地址空间,驱动需要正确配置这些寄存器才能使设备正常工作。
2.1 BAR寄存器解析
BAR寄存器的结构取决于它映射的是内存空间还是I/O空间:
内存空间BAR(bit 0 = 0):
| 3 | 2 | 1 | 0 | 0 | 0 | 0 | 0 | |---|---|---|---|---|---|---|---| | Prefetchable | Type | 总是0 | 地址位[31:4] |- Type字段:00表示32位地址,10表示64位地址
I/O空间BAR(bit 0 = 1):
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | |---|---|---|---|---|---|---|---| | 保留 | 地址位[31:2] |
探测BAR空间大小的标准方法是:
u32 orig, size; pci_read_config_dword(dev, BAR_OFFSET, &orig); pci_write_config_dword(dev, BAR_OFFSET, 0xFFFFFFFF); pci_read_config_dword(dev, BAR_OFFSET, &size); pci_write_config_dword(dev, BAR_OFFSET, orig); size = ~(size & 0xFFFFFFF0) + 1; // 对于内存空间2.2 内核中的BAR操作
在实际驱动中,我们通常使用内核提供的更高级接口:
// 启用设备并分配BAR资源 int pci_enable_device(struct pci_dev *dev); // 请求BAR对应的内存区域 void __iomem *pci_iomap(struct pci_dev *dev, int bar, unsigned long maxlen); // 释放映射的资源 void pci_iounmap(struct pci_dev *dev, void __iomem *addr);典型的使用模式如下:
struct my_dev { void __iomem *regs; ... }; static int my_probe(struct pci_dev *dev, const struct pci_device_id *id) { struct my_dev *mydev; int err; err = pci_enable_device(dev); if (err) return err; mydev = devm_kzalloc(&dev->dev, sizeof(*mydev), GFP_KERNEL); mydev->regs = pci_iomap(dev, 0, 0); if (!mydev->regs) { dev_err(&dev->dev, "Cannot map BAR0\n"); return -ENOMEM; } pci_set_drvdata(dev, mydev); ... }注意:64位BAR需要两个连续的32位寄存器空间。在访问这类BAR时,需要特别处理高低32位。
3. 中断配置:从硬件引脚到软件处理
PCI设备的中断配置涉及三个关键寄存器:Interrupt Pin(只读)、Interrupt Line(读写)和Command寄存器中的中断禁用位。
3.1 中断寄存器详解
Interrupt Pin(0x3D):
- 1 = INTA#
- 2 = INTB#
- 3 = INTC#
- 4 = INTD#
- 0 = 不使用引脚中断
Interrupt Line(0x3C):
- 传统上用于x86系统的8259A中断控制器
- 在现代系统中通常由操作系统动态分配
Command Register(bit 2):
- 中断禁用控制位(1=禁用)
3.2 Linux中的中断处理
现代Linux PCI驱动通常使用pci_alloc_irq_vectors和pci_request_irq等API:
int pci_alloc_irq_vectors(struct pci_dev *dev, unsigned int min_vecs, unsigned int max_vecs, unsigned int flags); int pci_request_irq(struct pci_dev *dev, unsigned int nr, irq_handler_t handler, irq_handler_t thread_fn, void *dev_id, const char *fmt, ...); void pci_free_irq(struct pci_dev *dev, unsigned int nr, void *dev_id);一个完整的中断初始化流程示例:
static irqreturn_t my_interrupt(int irq, void *dev_id) { struct my_dev *mydev = dev_id; // 处理中断 ... return IRQ_HANDLED; } static int my_probe(struct pci_dev *dev, const struct pci_device_id *id) { int ret; // 启用设备 ret = pci_enable_device(dev); if (ret) return ret; // 分配中断向量 ret = pci_alloc_irq_vectors(dev, 1, 1, PCI_IRQ_LEGACY); if (ret < 0) return ret; // 请求中断处理程序 ret = pci_request_irq(dev, 0, my_interrupt, NULL, dev, "mydev"); if (ret) { pci_free_irq_vectors(dev); return ret; } // 启用PCI设备中断 pci_write_config_word(dev, PCI_COMMAND, PCI_COMMAND_INTX_DISABLE & ~PCI_COMMAND_INTX_DISABLE); ... }4. Command寄存器:设备控制的核心
Command寄存器(偏移0x04)是PCI设备的"总开关",控制着设备的基本行为。这个16位寄存器的主要控制位包括:
| 位 | 名称 | 功能描述 |
|---|---|---|
| 0 | IO Space | 1=启用I/O空间访问 |
| 1 | Memory Space | 1=启用内存空间访问 |
| 2 | Bus Master | 1=允许设备作为总线主设备 |
| 3 | Special Cycles | 1=允许特殊周期 |
| 4 | Memory Write & Invalidate | 1=允许MWI命令 |
| 6 | Parity Error Response | 1=启用奇偶错误响应 |
| 8 | SERR# Enable | 1=启用SERR#信号 |
| 10 | Interrupt Disable | 1=禁用中断 |
在驱动中,我们通常会这样初始化和修改Command寄存器:
u16 cmd; // 读取当前命令寄存器值 pci_read_config_word(dev, PCI_COMMAND, &cmd); // 启用内存空间和总线主控 cmd |= PCI_COMMAND_MEMORY | PCI_COMMAND_MASTER; // 禁用中断 cmd |= PCI_COMMAND_INTX_DISABLE; // 写回新值 pci_write_config_word(dev, PCI_COMMAND, cmd);重要提示:
pci_enable_device()函数内部已经处理了基本的Command寄存器设置(启用I/O和内存空间),但在需要更精细控制时,仍需直接操作该寄存器。
5. 实战案例:编写一个PCI设备驱动
让我们通过一个简化的PCI网卡驱动示例,整合前面讨论的所有概念:
#include <linux/module.h> #include <linux/pci.h> #include <linux/interrupt.h> #define DRV_NAME "mypci" struct mypci_dev { void __iomem *bar0; struct pci_dev *pdev; int irq; }; static irqreturn_t mypci_interrupt(int irq, void *dev_id) { struct mypci_dev *dev = dev_id; // 处理中断 ... return IRQ_HANDLED; } static int mypci_probe(struct pci_dev *pdev, const struct pci_device_id *ent) { struct mypci_dev *dev; int err; // 启用PCI设备 if ((err = pci_enable_device(pdev))) return err; // 请求内存区域 if ((err = pci_request_regions(pdev, DRV_NAME))) { pci_disable_device(pdev); return err; } // 映射BAR0 dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL); dev->bar0 = pci_iomap(pdev, 0, pci_resource_len(pdev, 0)); if (!dev->bar0) { err = -ENOMEM; goto err_release; } // 设置DMA掩码 if ((err = pci_set_dma_mask(pdev, DMA_BIT_MASK(64)))) { if ((err = pci_set_dma_mask(pdev, DMA_BIT_MASK(32)))) { dev_err(&pdev->dev, "No suitable DMA available\n"); goto err_unmap; } } // 分配中断 if ((err = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_ALL_TYPES)) < 0) goto err_unmap; // 请求中断 if ((err = request_irq(pci_irq_vector(pdev, 0), mypci_interrupt, IRQF_SHARED, DRV_NAME, dev))) { dev_err(&pdev->dev, "Cannot request IRQ\n"); goto err_irq; } dev->pdev = pdev; pci_set_drvdata(pdev, dev); return 0; err_irq: pci_free_irq_vectors(pdev); err_unmap: pci_iounmap(pdev, dev->bar0); err_release: pci_release_regions(pdev); pci_disable_device(pdev); return err; } static void mypci_remove(struct pci_dev *pdev) { struct mypci_dev *dev = pci_get_drvdata(pdev); free_irq(pci_irq_vector(pdev, 0), dev); pci_free_irq_vectors(pdev); pci_iounmap(pdev, dev->bar0); pci_release_regions(pdev); pci_disable_device(pdev); } static const struct pci_device_id mypci_ids[] = { { PCI_DEVICE(0x10ec, 0x8168) }, // Realtek RTL8168 { 0, } }; MODULE_DEVICE_TABLE(pci, mypci_ids); static struct pci_driver mypci_driver = { .name = DRV_NAME, .id_table = mypci_ids, .probe = mypci_probe, .remove = mypci_remove, }; module_pci_driver(mypci_driver);这个示例展示了PCI驱动开发中的关键环节:
- 设备启用和资源分配
- BAR空间映射
- 中断处理设置
- 内存管理和DMA配置
- 完整的初始化和清理流程
