PCI BAR寄存器详解(二 实例讲解)

前言

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


一、驱动程序加载与卸载

static const struct pci_device_id   pci_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_t  dma_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_t  dma_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 总线地址的转换

在 Linux 系统中,支持一系列 API 实现存储器地址到 PCI 总线地址的转换。下面仅以 pci_map_single 函数为例说明。

static inline dma_addr_t  pci_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- <TAbort- <MAbort- >SERR- <PERR- INTx-
        Latency: 0, Cache Line Size: 64 bytes
        Interrupt: pin A routed to IRQ 16
        Region 0: Memory at f0000000 (32-bit, non-prefetchable) [size=64M]
        Region 1: Memory at f4000000 (32-bit, non-prefetchable) [size=64K]
        Capabilities: [40] Power Management version 3
                Flags: PMEClk- DSI- D1- D2- AuxCurrent=0mA PME(D0-,D1-,D2-,D3hot-,D3cold-)
                Status: D0 NoSoftRst+ PME-Enable- DSel=0 DScale=0 PME-
        Capabilities: [48] MSI: Enable- Count=1/32 Maskable- 64bit+
                Address: 0000000000000000  Data: 0000
        Capabilities: [60] MSI-X: Enable+ Count=33 Masked-
                Vector table: BAR=1 offset=00008000
                PBA: BAR=1 offset=00008fe0
[root@localhost ~]# 

可以看到,
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 信息。

你可能感兴趣的:(pci/pcie,linux,内核驱动,驱动开发,linux,运维)