pci/pcie|PCI BAR寄存器详解(二 实例讲解)

前言 下面以一个实际项目,讲解 PCI 驱动程序和 BAR 空间的相关操作函数。
一、驱动程序加载与卸载

static const struct pci_device_idpci_ids[] = { { PCI_DEVICE(0x1DED, 0x1020), }, {0,} }; MODULE_DEVICE_TABLE(pci, pci_ids); static struct pci_driver pci_driver = { .name = DRV_NAME, .id_table = pci_ids, .probe = probe, .remove = remove, }; static int __init xdma_init(void) { rc = pci_register_driver(&pci_driver); ...... }static void __exit xdma_exit(void) { pci_unregister_driver(&pci_driver); }

在上述代码中,pci_register_driver 函数的主要作用是将 pci_driver 结构 与 PCI 设备的 pci_dev 结构进行绑定,并且在初始化时执行 probe 函数,而在结束时执行 remove 函数。在 pci_ids 结构中使用的 id 号,是联系 pci_driver 结构和 pci_device 结构的桥梁。
二、初始化与关闭
// 硬件初始化片段 1 static int probe(struct pci_dev *pdev, const struct pci_device_id *id) { rc = pci_enable_device(pdev); if (rc) { dbg_init("pci_enable_device() failed, rc = %d.\n", rc); goto free_alloc; } pci_set_master(pdev); if (!pci_set_dma_mask(pdev, DMA_BIT_MASK(64))) { pci_set_consistent_dma_mask(pdev, DMA_BIT_MASK(32)); } else if (!pci_set_dma_mask(pdev, DMA_BIT_MASK(32))) { pci_set_consistent_dma_mask(pdev, DMA_BIT_MASK(32)); } else { rc = -1; } ......

首先 prode 函数从 local_pci_probe 函数获得 PCI 设备对应的 pci_dev 描述符,在 Linux 系统中每个 PCI/PCIe 设备都与唯一的 pci_dev 描述符对应。
pci_enable_device 函数的主要作用是修改 PCI 设备 PCI配置空间 Command 寄存器的 I/O Space 位和 Memory Space 位。
pci_enable_device 函数最终调用 pci_enable_resources 函数,并由 pci_enable_resources 函数扫描 PCI 设备的 BAR0 到 BAR5 空间,如果这些 BAR0 到 BAR5 空间 用到了 I/O 或者 Memory 空间,则将 I/O Space 位和 Memory Space 位 置 1。
pci_enable_device 函数最后调用 pcibios_enable_irq 函数分配 PCI 设备使用的中断向量号。
pci_set_master 函数表示 PCI 设备作为 PCI 总线的主设备。
pci_set_dma_mask 函数设置 PCIe 设备使用的 DMA 掩码。 PCI 设备对一段内存进行 DMA 操作时,需要使用这段内存在 PCI 总线域的物理地址 pci_address。如果这段内存在存储器域的物理地址 phy_address & DMA_BIT_MASK(64) = pci_address 时,表示 PCI 设备可以对这段内存进行 DMA 操作。
// 硬件初始化片段 2 rc = pci_request_regions(pdev, DRV_NAME); ...... for (idx = 1; idx < XDMA_BAR_NUM; idx++) { resource_size_t bar_start; resource_size_t bar_len; resource_size_t map_len; bar_start = pci_resource_start(dev, idx); bar_len = pci_resource_len(dev, idx); map_len = bar_len; lro->bar[idx] = NULL; /* do not map BARs with length 0. Note that start MAY be 0! */ if (!bar_len) { NSADRV_DEBUG("BAR #%d is not present - skipping\n", idx); return 0; } /* * map the full device memory or IO region into kernel virtual * address space */ NSADRV_DEBUG("BAR%d: %llu bytes to be mapped.\n", idx, (u64)map_len); lro->bar[idx] = pci_iomap(dev, idx, map_len); NSADRV_DEBUG("BAR%d at 0x%llx mapped at 0x%p, length=%llu(/%llu)\n", idx, (u64)bar_start, lro->bar[idx], (u64)map_len, (u64)bar_len);

这段源代码调用 pci_request_regions 函数使 DRV_NAME 对应的驱动程序成为 pci_dev 存储器资源的管理者。
pci_resource_start 函数从 resources 结构获得 BARX 空间的存储器域的物理基地址。从 resources 结构获得的是该设备 BAR 寄存器在存储器域的物理地址,而使用 pci_read_config_word 函数获得的是 PCI 总线域的物理地址。在 Linux 驱动程序中,需要使用的是存储器域的物理地址。
程序的最后,使用 pci_iomap 将 存储器域的物理地址映射成为 Linux 系统中的虚拟地址。之后 PCI 驱动程序可以使用映射的虚拟地址访问 PCI 设备存储器映射的寄存器。 驱动程序的物理地址必须从 pci_resource_start 函数获得,而不能使用“通过 pci_read_config_xxxx 函数”获得的 BARX 基地址,因为 PCIe 设备的 BARX 基地址空间属于 PCI 总线域,而不是存储器域。
// 硬件初始化片段 3 result = register_chrdev(test_dri_major, DEV_NAME, &xxx_fops); ......result = pci_enable_msi(pdev); if(unlikely(result)) { goto chrdev_unregister; }result = request_irq(pdev->irq, xxx_interrupt, 0, DEV_NAME, NULL); if(unlikely(result)) { goto err_disable_msi; }static const struct file_operations xxx_fops = { .owner= THIS_MODULE, .ioctl= xxx_ioctl, .open= xxx_open, .release= xxx_release, .write= xxx_write, .read= xxx_read, }; ......

在 probe 函数中,剩下的部分便是 PCI 设备对应设备类型的驱动程序,如字符设备、网络设备等的初始化代码。
这段源代码首先使用 register_chrdev 函数注册一个 char 类型的设备驱动程序,包括打开、关闭、读写操作和 ioctl 函数。之后程序调用 pci_enable_msi 函数使能 PCI 设备的 MSI 中断请求机制。
随后这段程序使用 request_irq 函数注册 PCI 设备使用的中断服务例程 xxxx_interrupt,并使用 pdev->irq 作为 irq 入口参数。pdev->irq 参数在 Linux 系统对 PCI 总线进行初始化时分配,在 X86 处理器中,如果一个 PCIe 设备支持 MSI 中断,驱动程序执行完毕 pci_enable_msi 函数后, pdev->irq 参数还会发生变化。因此,request_irq 函数必须在 pci_enable_msi 函数之后运行。
// DMA 写片段1 static ssize_t xxx_read(struct file* file, char __user* buff, size_t count, loff_t* f_pos) { int err = -EINVAL; void *virt_addr = NULL; dma_addr_tdma_write_addr; ......virt_addr = kmalloc(count, GFP_KERNAL); if(unlikely(! virt_addr)) { return -EIO; }dma_write_addr = pci_map_single(adapter->pci_dev, virt_addr, count, PCI_DMA_FROMDEVICE);

这段源代码首先使用 kmalloc 函数分配 DMA 写使用的数据缓存。随后这段代码调用 pci_map_single 函数将存储器域的虚拟地址 virt_addr 转换为 PCI 总线域的物理地址 dma_write_addr ,供 PCI 设备的 DMA 控制器使用。
// DMA 写片段2 xxx_w32(dma_write_addr, WR_DMA_ADR); xxx_w32(count, WR_DMA_ADR); xxx_w32(MWR_START, DCSR2); if ((unlikely(interruptible_sleep_on(adapter->dma_write_wait)))) { goto err_pci_map; }if (unlikely(copy_to_user(buff, virt_addr, count))) { goto err_pci_map; }pci_unmap_single(adapter->pci_dev, dma_write_addr , count, PCI_DMA_FROMDEVICE); kfree(virt_addr); return count; ...... }

这段程序进行特定的寄存器操作后,可以使用轮询方式,或者使用中断方式唤醒这个 DMA 写进程。当进程被唤醒后,表示 DMA 写操作已经完成,此时这段程序使用 copy_to_user 函数将数据复制到用户空间。
// DMA 读片段 static ssize_t xxx_write(struct file* file, const char __user* buff, size_t count, loff_t* f_pos) { int err = -EINVAL; void *virt_addr = NULL; dma_addr_tdma_write_addr; ......virt_addr = kmalloc(count, GFP_KERNAL); if(unlikely(! virt_addr)) { return -EIO; } if (unlikely(copy_from_user(virt_addr, buff, count))) { goto err_pci_map; }dma_write_addr = pci_map_single(adapter->pci_dev, virt_addr, count, PCI_DMA_TODEVICE); xxx_w32(dma_write_addr, RD_DMA_ADR); xxx_w32(count, RD_DMA_ADR); xxx_w32(MWR_START, DCSR2); if ((unlikely(interruptible_sleep_on(adapter->dma_read_wait)))) { goto err_pci_map; }pci_unmap_single(adapter->pci_dev, dma_write_addr , count, PCI_DMA_TODEVICE); kfree(virt_addr); return count; ...... }

读者如果正确理解了上文关于 DMA 写的执行过程,DMA 读 的执行过程并不难理解。
三、存储器地址到 PCI 总线地址的转换 【pci/pcie|PCI BAR寄存器详解(二 实例讲解)】在 Linux 系统中,支持一系列 API 实现存储器地址到 PCI 总线地址的转换。下面仅以 pci_map_single 函数为例说明。
static inline dma_addr_tpci_map_single(struct pci_dev* hwdev, void* ptr, size_t size, int direction) { return dma_map_single(hwdev == NULL ? NULL: &hwdev->dev, ptr, size, (enum dma_data_direction)direction); }

该函数共有 4 个输入参数,其中 hwdev 参数与 PCI 设备的 pci_dev 对应,ptr 参数对应存储器域的虚拟地址, size 字段对应数据区域的大小。
pci_map_single 函数的主要作用是通过 ptr 参数,获得与之对应的 dma_addr,即进行存储器域虚拟地址到 PCI 总线域物理地址的转换。值得注意的是存储器域物理地址与 PCI 总线域物理地址的区别。
在 Linux 系统中,使用 virt_to_phys 函数将 存储器域的虚拟地址转换为存储器域的物理地址,但是通过该函数仅能获得存储器域的物理地址,因此该地址不能填写到 PCI 设备中进行 DMA 操作。值得注意的是,进行 DMA 操作的地址是由 PCI 设备使用的,而且这个地址只能是 PCI 总线域的物理地址。
四、驱动日志比对 下面是加载驱动后 dmesg 的信息:
[ 1547.520031] [DEBUG] probe.699:dev_id(2) BAR 0:length[67108864]virt_addr[0xffffc9001e480000] [ 1547.539921] [DEBUG] probe.758:dev_id(2) lastest DDR ECC state: 0x03 [ 1547.539925] [DEBUG] map_single_bar.1820:BAR1: 65536 bytes to be mapped. [ 1547.539945] [DEBUG] map_single_bar.1829:BAR1 at 0xe4000000 mapped at 0xffffc900049c0000, length=65536(/65536) [ 1547.539948] [DEBUG] is_config_bar.1849:BAR 1 is the XDMA config BAR [ 1547.539949] [DEBUG] map_single_bar.1806:BAR #2 is not present - skipping [ 1547.539950] [DEBUG] map_single_bar.1806:BAR #3 is not present - skipping [ 1547.539951] [DEBUG] map_single_bar.1806:BAR #4 is not present - skipping [ 1547.539952] [DEBUG] map_single_bar.1806:BAR #5 is not present - skipping [ 1547.539954] [DEBUG] msix_irq_setup.4528:write_msix_vectors..1 [ 1547.539955] [DEBUG] msix_irq_setup.4530:write_msix_vectors..2 [ 1547.539973] [DEBUG] msix_irq_setup.4548:Using IRQ#92 with 0xffff8800bdb4aa90 [ 1547.539982] [DEBUG] msix_irq_setup.4548:Using IRQ#93 with 0xffff8800bdb4aab8 [ 1547.539990] [DEBUG] msix_irq_setup.4548:Using IRQ#94 with 0xffff8800bdb4aae0 [ 1547.539998] [DEBUG] msix_irq_setup.4548:Using IRQ#95 with 0xffff8800bdb4ab08 [ 1547.540005] [DEBUG] msix_irq_setup.4548:Using IRQ#96 with 0xffff8800bdb4ab30 [ 1547.540012] [DEBUG] msix_irq_setup.4548:Using IRQ#97 with 0xffff8800bdb4ab58 [ 1547.540018] [DEBUG] msix_irq_setup.4548:Using IRQ#98 with 0xffff8800bdb4ab80 [ 1547.540024] [DEBUG] msix_irq_setup.4548:Using IRQ#99 with 0xffff8800bdb4aba8 [ 1547.540030] [DEBUG] msix_irq_setup.4548:Using IRQ#100 with 0xffff8800bdb4abd0 [ 1547.540037] [DEBUG] msix_irq_setup.4548:Using IRQ#101 with 0xffff8800bdb4abf8 [ 1547.540045] [DEBUG] msix_irq_setup.4548:Using IRQ#102 with 0xffff8800bdb4ac20 [ 1547.540051] [DEBUG] msix_irq_setup.4548:Using IRQ#103 with 0xffff8800bdb4ac48 [ 1547.540058] [DEBUG] msix_irq_setup.4548:Using IRQ#104 with 0xffff8800bdb4ac70 [ 1547.540066] [DEBUG] msix_irq_setup.4548:Using IRQ#105 with 0xffff8800bdb4ac98 [ 1547.540072] [DEBUG] msix_irq_setup.4548:Using IRQ#106 with 0xffff8800bdb4acc0 [ 1547.540078] [DEBUG] msix_irq_setup.4548:Using IRQ#107 with 0xffff8800bdb4ace8 [ 1547.541959] [DEBUG] read_interrupts.525:ioread32(0xffffc900049c2040) returned 0x00000000 (user_int_request). [ 1547.541963] [DEBUG] read_interrupts.528:ioread32(0xffffc900049c2044) returned 0x00000000 (channel_int_request)

可以看到, BAR 0:length[67108864] BAR0 是 64M字节,BAR1: 65536 BAR1是64k字节。BAR2~BAR5 寄存器没有配置地址映射。
下面是 lspci 信息:
[root@localhost ~]# lspci -s 03:00.0 -vv 03:00.0 Serial controller: Device 1ded:1020 (prog-if 01 [16450]) Subsystem: Xilinx Corporation Device 0007 Control: I/O+ Mem+ BusMaster+ SpecCycle- MemWINV- VGASnoop- ParErr- Stepping- SERR- FastB2B- DisINTx+ Status: Cap+ 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast >TAbort- SERR-

可以看到,
Region 0: Memory at f0000000 (32-bit, non-prefetchable) [size=64M]
Region 1: Memory at f4000000 (32-bit, non-prefetchable) [size=64K]
对应的BAR空间大小是正确的。
[root@localhost trunk_345_adapt]# npl_version:0x3e35 npl_run_type:0x504a run_mode:Xdma [root@localhost trunk_345_adapt]# ./devmem 0xf0100008 w /dev/mem opened. Memory mapped at address 0x7f1274ea9000. Value at address 0xF0100008 (0x7f1274ea9008): 0x20170609 [root@localhost trunk_345_adapt]# ./devmem 0xf0100004 w /dev/mem opened. Memory mapped at address 0x7fec81137000. Value at address 0xF0100004 (0x7fec81137004): 0x504A3E35 [root@localhost trunk_345_adapt]#

通过 “BAR寄存器 + 偏移地址” 的方式,使用 ./devmem 0xf0100004 w 读取 0x100004 寄存器,确实可以获取到 npl_version 和 npl_run_type 信息。

    推荐阅读