上一篇看到在insmod时候init调用了probe,probe牵扯到的函数比较多,所以我们先做一下准备:
1,pci_enable_device()
pci_enable_device() 是 Linux 内核中用于启用 PCI 设备的函数。当 PCI 设备被探测到时,内核会自动调用这个函数来初始化设备并使其可以被使用。
下面是一个使用 pci_enable_device() 的示例:
#include <linux/pci.h>
int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
int ret;
/* 启用 PCI 设备 */
ret = pci_enable_device(pdev);
if (ret) {
dev_err(&pdev->dev, "Failed to enable PCI device: %d\n", ret);
return ret;
}
/* 其他设备初始化代码 */
...
return 0;
}
在这个例子中,my_pci_probe()
函数是 PCI 驱动程序的探测回调函数。当内核找到匹配的 PCI 设备时,就会调用这个函数。
首先,我们调用 pci_enable_device()
函数来启用 PCI 设备。这个函数执行以下操作:
- 分配并设置 PCI 设备的 I/O 和内存资源。
- 使能 PCI 设备的总线主模式(bus master mode)。
- 清除 PCI 命令寄存器中的错误标志位。
如果启用设备成功,pci_enable_device()
函数会返回 0。否则,它会返回一个负的错误码。
在成功启用设备后,你就可以继续执行其他的设备初始化操作,例如设置中断处理程序、映射 I/O 内存等。
总的来说,pci_enable_device()
是在使用 PCI 设备之前必须调用的一个重要函数。它负责正确地初始化设备,确保设备能够正常工作。
2,pcie_capability_clear_and_set_word()
pcie_capability_clear_and_set_word()
是 Linux 内核中用于操作 PCI Express 设备寄存器的一个函数。它可以用来清除和设置 PCI Express 寄存器中特定的位。
这个函数通常用于修改 PCI Express 设备的能力寄存器,比如设备的功能、错误处理等。
下面是一个使用 pcie_capability_clear_and_set_word()
的示例:
#include <linux/pci.h>
struct pci_dev *pdev;
/* 清除 device control 寄存器中的 correctable error reporting enable位,
并设置 unsupported request reporting enable位 */
pcie_capability_clear_and_set_word(pdev, PCI_EXP_DEVCTL,
PCI_EXP_DEVCTL_CERE,
PCI_EXP_DEVCTL_URRE);
在这个示例中:
pdev 是一个指向 PCI 设备的指针。
PCI_EXP_DEVCTL 是 PCI Express 设备控制寄存器的偏移量。
PCI_EXP_DEVCTL_CERE 是设备控制寄存器中的"可修正错误报告使能"位。
PCI_EXP_DEVCTL_URRE 是设备控制寄存器中的"不支持请求报告使能"位。
pcie_capability_clear_and_set_word() 函数首先会清除指定的位(在本例中是 PCI_EXP_DEVCTL_CERE),然后设置指定的位(在本例中是 PCI_EXP_DEVCTL_URRE)。这可以确保在设置新的位时不会意外地修改其他位。
该函数返回执行操作之前寄存器的值。如果出现错误,它会返回负的错误码。
在这个驱动中,应该是申请4KB的空间?
/* Reconfigure MaxReadReq to 4KB */
pcie_capability_clear_and_set_word(pdev, PCI_EXP_DEVCTL,
PCI_EXP_DEVCTL_READRQ, PCI_EXP_DEVCTL_READRQ_4096B);
2,pci_set_master()
这个是设置此设备为MASTER 就是允许自主发起DMA操作。就是具备对指定控件的直接读写能力,而不需要CPU的参与。
The pcie_set_master function is a Linux kernel function used to set a PCI Express (PCIe) device as the bus master. This means the device can initiate transactions on the PCIe bus, such as reading from or writing to system memory, without requiring the CPU to perform the data transfer.
3,pci_resource_flags 和 pci_resource_len
if (!(pci_resource_flags(pdev, 0) & IORESOURCE_MEM) ||
pci_resource_len(pdev, 0) < 1 << 12) {
dev_err(&pdev->dev, "Missing UL BAR, aborting.\n");
err = -ENODEV;
goto err_disable_pdev;
}
这里查看是否具备IORESOURCE_MEM属性以及pci_resource_len的长度是否不小于1K。
4,pci_request_regions
在成功启用 PCI 设备之后,它调用 pci_request_regions() 函数来请求 PCI 设备的内存和 I/O 资源。
5,pci_set_consistent_dma_mask
if (pci_set_consistent_dma_mask(pdev, DMA_BIT_MASK(32))) {
dev_err(&pdev->dev,"No suitable consistent DMA available.\n");
goto err_disable_pdev;
}
it’s good practice to call pci_set_consistent_dma_mask() to set the
appropriate mask even if your device only supports 32-bit DMA
(default) and especially if it’s a PCI-X device.
咱们的硬件使用的是32位地址,因此要调用这个函数设置成32位的地址。
在 PCI 设备驱动中,经常需要进行 DMA 传输。DMA 掩码是一个用来表示设备支持的最大 DMA 地址宽度的位掩码。它决定了设备能够访问的物理内存地址的范围。
pci_set_consistent_dma_mask 函数的作用就是设置 PCI 设备的一致性 DMA 掩码。所谓一致性 DMA,是指设备能够直接访问内存中的数据,而不需要额外的拷贝或转换。这种 DMA 方式通常效率更高。
6,pci_iomap
void __iomem *bar0_addr;
void __iomem *bar1_addr;
bar0_addr = pci_iomap(pdev, 0, 1 << 12);
if (!bar0_addr) {
dev_err(&pdev->dev, "Failed to map BAR 0.\n");
goto err_free_res;
}
bar1_addr = pci_iomap(pdev, 1, 1 << 16);
if (!bar0_addr) {
dev_err(&pdev->dev, "Failed to map BAR 1.\n");
goto err_unmap0;
}
这里看到pci_iomap三个参数分别是要映射的pcie设备,bar的索引号,要映射的资源大小(0表示整块)。
我们看到获取到了这bar地址之后,就可以以此为基地址,使用指针对这片映射过的存储空间进行读写,驱动中的代码如下:
static void xtrx_writel(struct xtrx_dev *dev, unsigned int off, unsigned int value)
{
printk(KERN_INFO PFX "liwei xtrx_writel[%d] off=%08x d=%08x \n", __LINE__ ,off,value );
iowrite32(cpu_to_be32(value), (void __iomem *)((unsigned long)dev->bar0_addr + 4*off));
}
static unsigned int xtrx_readl(struct xtrx_dev *dev, unsigned int off)
{
unsigned int r = be32_to_cpu(ioread32((void __iomem *)((unsigned long)dev->bar0_addr + 4*off)));
printk(KERN_INFO PFX "liwei xtrx_readl[%d] off=%08x return=%08x \n", __LINE__ ,off,r );
return r ;
}
上述代码我在FPGA里面对应分析过代码,采用类似axi_lite总线方式(代码中反复出现的ul,可能他们定义为ul总线接口),实现了对内部用户寄存器的读写控制。
这里cpu_to_be32是cpu字端方式转成大字端。基本x86以及arm都是小字端的。而网络传输比如tcp/ip以及pcie多用大字端,所以需要转换。字端其实就是字节组成字的顺序规定。
对bar1_addr的实际操作,除了pci_iounmap,其他的我在代码中没有实际看到,因此实际可以忽略哈。
7,kzalloc
这是内核版本的malloc.申请一块内存并清0.第二参数一般都是GFP_KERNEL。
xtrxdev = kzalloc(sizeof(*xtrxdev), GFP_KERNEL);
if (!xtrxdev) {
dev_err(&pdev->dev, "Failed to allocate memory.\n");
err = -ENOMEM;
goto err_unmap1;
}
pci_set_drvdata(pdev, xtrxdev);
xtrxdev->bar0_addr = bar0_addr;
xtrxdev->bar1_addr = bar1_addr;
xtrxdev->devno = xtrx_no;
xtrxdev->pdev = pdev;
xtrxdev->locked_msk = 0;
xtrxdev->gps_ctrl_state = 0;
xtrxdev->sim_ctrl_state = 0;
xtrxdev->pwr_msk = 0;
xtrxdev->valid = 0;
spin_lock_init(&xtrxdev->slock);
这里看到是申请到了内存并初始化了结构。
8,get_zeroed_page
请求一块内存清零的页,通常是4K大小,下面是来自人工智能的例子。
#include <linux/kernel.h>
#include <linux/mm.h>
int my_function(void)
{
unsigned long *page;
// 使用 get_zeroed_page() 分配并清零一个内存页
page = (unsigned long *)get_zeroed_page(GFP_KERNEL);
if (!page)
return -ENOMEM;
// 现在 page 指向的内存页已经被清零
pr_info("page[0] = %lu\n", page[0]);
// 使用分配的内存页
page[0] = 42;
pr_info("page[0] = %lu\n", page[0]);
// 释放内存页
free_page((unsigned long)page);
return 0;
}
我们看到其实就是申请一块内存而已,没有什么特殊的说法。
xtrxdev->shared_mmap = (char*)get_zeroed_page(GFP_KERNEL);
if (!xtrxdev->shared_mmap) {
dev_err(&pdev->dev, "Failed to allocate kernel mmap memory.\n");
err = -ENOMEM;
goto err_alloc0;
}
我们看到申请的这块空间,实际上用来当做数组用,设置了几个偏移量,就当内存公共区域进行同步机制:
上面看到都是进行atomic操作的对象。
9,spin_lock_init
这个操作是自旋锁的相关操作。
从功能上更类似于互斥的mutex操作。但是比mutex效率搞,但是占用cpu资源大。
static void xtrx_uart_set_termios(struct uart_port *port,
struct ktermios *new,
struct ktermios *old)
{
unsigned long flags;
printk(KERN_INFO PFX "liwei xtrx_uart_set_termios [%d]\n", __LINE__ );
spin_lock_irqsave(&port->lock, flags);
// TODO: Set HW registers
spin_unlock_irqrestore(&port->lock, flags);
}
pin_lock_irqsave 是一个用于获取自旋锁并保存当前中断状态的宏。它的定义如下:
#define spin_lock_irqsave(lock, flags) \
do { \
local_irq_save(flags); \
spin_lock(lock); \
} while (0)
它的作用如下:
首先使用 local_irq_save(flags) 函数保存当前的中断状态,并禁用中断。这个函数会将当前的中断状态存储在 flags 变量中。
然后调用 spin_lock(lock) 函数获取指定的自旋锁。自旋锁是一种轻量级的互斥锁,当锁已被占用时,获取锁的线程会一直循环等待,直到锁被释放。
整个过程被包含在一个 do-while 循环中,确保操作的原子性。
通过使用 spin_lock_irqsave,驱动程序可以在获取自旋锁的同时禁用中断。这样可以防止在持有锁的过程中被其他中断处理程序打断,从而引发竞争条件。
当驱动程序不再需要锁时,需要使用 spin_unlock_irqrestore 宏来释放锁并恢复之前保存的中断状态:
#define spin_unlock_irqrestore(lock, flags) \
do { \
spin_unlock(lock); \
local_irq_restore(flags); \
} while (0)
这个宏首先释放自旋锁,然后使用 local_irq_restore(flags) 函数恢复之前保存的中断状态。
总之, spin_lock_irqsave 和 spin_unlock_irqrestore 是 Linux 内核中用于处理自旋锁和中断状态的重要机制,广泛应用于各种内核驱动和子系统中。