下面以一个实际项目,讲解 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 读 的执行过程并不难理解。
在 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 信息。