ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux PCIe设备驱动开发实战:从BDF到中断处理与调试

Linux PCIe设备驱动开发实战:从BDF到中断处理与调试 1. PCIe 驱动开发到底在搞什么Linux 下的 PCI 设备驱动说白了就是让内核认识一块插在主板上的板卡并且给它分配好资源、挂上中断、暴露接口最后让用户态程序能读写它。听起来简单但真正动手写的时候你会发现从设备枚举到 BAR 空间映射从 MSI 中断到 DMA 一致性每一步都有坑。我做了十多年底层开发经手的 PCIe 设备从网卡、采集卡到自定义加速器都有踩过的坑比写过的驱动还多。这篇文章就把这些经验整理出来给正在做或者准备做 PCIe 驱动的朋友一个参考。先明确一下适用范围。如果你用的是标准外设比如常见的 Realtek PCIe GbE 网卡内核里已经有现成驱动你只需要确认设备 ID 匹配、固件加载正常就行。但如果你面对的是自定义 FPGA 板卡、专用采集设备或者老设备移植那就得自己写驱动。本文主要面向后者同时也会覆盖调试和排查的通用方法因为不管写不写驱动PCIe 设备出问题时排查思路是一样的。核心关键词先摆出来PCIe、Linux、PCI、设备驱动、BDF。BDF 是 Bus-Device-Function 的缩写这是 PCIe 设备的身份证后面会反复用到。整个驱动开发的主线就是找到设备、分配资源、注册驱动、处理中断、实现数据通路。下面按这个顺序展开。2. 从 BDF 到驱动匹配设备是怎么被内核认出来的2.1 BDF 编号与配置空间每个 PCIe 设备在系统里都有一个唯一地址格式是domain:bus:device.function通常 domain 是 0000所以简写成bus:device.function比如03:00.0。这个编号不是随便定的是硬件拓扑决定的。CPU 通过 Host Bridge 发起配置周期内核枚举时从 bus 0 开始扫描发现桥就继续往下扫最终给每个设备分配一个 BDF。配置空间是每个 PCIe 设备都有的 256 字节PCIe 扩展后是 4KB寄存器区域前 64 字节是标准头部里面有关键字段Vendor ID、Device ID、Class Code、BARBase Address Register、中断引脚等。内核就是靠 Vendor ID 和 Device ID 来匹配驱动的。你可以用lspci -nn看到这些信息lspci -nn 03:00.0 Ethernet controller [0200]: Realtek Semiconductor Co., Ltd. RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller [10ec:8168] (rev 15)方括号里的10ec:8168就是 Vendor ID 和 Device ID。驱动里用pci_device_id结构体声明支持的设备static const struct pci_device_id my_pci_ids[] { { PCI_DEVICE(0x10ec, 0x8168) }, { 0, } }; MODULE_DEVICE_TABLE(pci, my_pci_ids);PCI_DEVICE宏会自动填充 Vendor 和 DeviceMODULE_DEVICE_TABLE则是让用户态的udev或depmod知道这个驱动支持哪些设备方便自动加载。这里有个细节如果你只填 Vendor ID 不填 Device ID会匹配该厂商所有设备一般不建议这么干容易误匹配。2.2 驱动注册与 probe 流程驱动注册用pci_register_driver核心是提供一个pci_driver结构体static struct pci_driver my_pci_driver { .name my_pci_drv, .id_table my_pci_ids, .probe my_pci_probe, .remove my_pci_remove, }; module_pci_driver(my_pci_driver);当内核枚举到一个设备并且它的 Vendor/Device ID 跟某个驱动的 id_table 匹配上就会调用该驱动的probe函数。probe 是驱动初始化的主战场你要在这里做几件事使能设备、申请 BAR 资源、映射寄存器、注册中断、初始化硬件、创建字符设备或 sysfs 节点。顺序很重要后面会细说。remove函数则在设备被移除或驱动卸载时调用负责释放资源。很多人写驱动时 probe 写得很仔细remove 随便糊弄结果热插拔或者模块卸载时各种 oops。记住一句话probe 里申请了什么remove 里就要按相反顺序释放什么。2.3 枚举过程与热插拔PCIe 枚举是内核在启动时或者热插拔时触发的。启动时的枚举由 BIOS/UEFI 先做一遍内核再重新扫描一遍确保资源分配合理。热插拔则是通过 PCIe 的 Hot-Plug 信号或者用户手动触发echo 1 /sys/bus/pci/rescan来重新扫描总线。热插拔功能在实际项目中非常有用尤其是服务器场景可以在不关机的情况下更换板卡。但热插拔对驱动的要求更高remove 必须能干净地释放所有资源probe 必须能处理设备重新出现的情况。我遇到过不少驱动在热插拔后第二次 probe 失败原因通常是第一次 remove 时没释放干净比如中断没注销、DMA 缓冲区没释放、sysfs 节点没删除。排查热插拔问题的一个实用技巧是看内核日志dmesg | grep -i pci热插拔时会有pci 0000:03:00.0: [10ec:8168] type 00 class 0x020000这样的日志如果 remove 或 probe 出错也会有相应的报错信息。另外/sys/bus/pci/devices/下每个设备目录里都有remove和rescan文件可以手动触发测试。3. 资源分配与 BAR 空间映射驱动的地基3.1 BAR 是什么为什么重要BAR 是 PCIe 设备向系统申请的地址窗口。一个设备最多有 6 个 BAR32 位系统或更多64 位系统每个 BAR 可以映射到内存空间或者 I/O 空间。设备内部的寄存器、FIFO、DMA 描述符等都会映射到某个 BAR 里驱动通过读写这些地址来控制硬件。BAR 的大小和类型由硬件设计决定内核在枚举时会读取 BAR 寄存器确定它需要多大空间然后分配一段物理地址给它。驱动在 probe 里要用pci_request_regions申请这段资源再用pci_iomap映射到内核虚拟地址ret pci_request_regions(pdev, my_pci_drv); if (ret) { dev_err(pdev-dev, Failed to request regions\n); return ret; } bar0 pci_iomap(pdev, 0, 0); if (!bar0) { dev_err(pdev-dev, Failed to map BAR0\n); pci_release_regions(pdev); return -ENOMEM; }pci_iomap的第三个参数是映射长度传 0 表示映射整个 BAR。映射完成后你就可以用ioread32/iowrite32等函数读写寄存器了。注意不要直接用指针解引用因为 BAR 空间可能是 I/O 映射的必须用专门的 I/O 访问函数。3.2 资源分配的常见坑第一个坑是 BAR 大小不匹配。有些 FPGA 板卡在设计时 BAR 大小没对齐比如声明需要 4KB 但实际只用了 1KB内核分配时可能因为对齐要求分配失败。解决办法是在硬件设计阶段就确保 BAR 大小是 2 的幂并且跟实际使用量匹配。第二个坑是 64 位 BAR 的处理。64 位 BAR 占用两个 BAR 寄存器低 32 位在前高 32 位在后。驱动里用pci_resource_start和pci_resource_len获取资源时内核已经帮你处理好了但如果你自己解析配置空间就要注意这个细节。第三个坑是 I/O 空间和内存空间的区分。现在大多数 PCIe 设备都用内存空间I/O 空间基本淘汰了。但如果你遇到老设备可能还是 I/O 空间这时候要用inb/outb系列函数而不是ioread32/iowrite32。判断方法是看 BAR 的最低位0 表示内存空间1 表示 I/O 空间。3.3 DMA 与一致性PCIe 设备做数据传输通常用 DMA驱动需要分配 DMA 缓冲区并且保证 CPU 和设备的视图一致。Linux 提供了两类 DMA API一致性 DMA 和流式 DMA。一致性 DMA 用dma_alloc_coherent分配返回的缓冲区在 CPU 和设备侧都能直接访问不需要额外的同步操作适合长期存在的描述符环。流式 DMA 用dma_map_single或dma_map_sg映射适合一次性传输传输完成后要dma_unmap解除映射。dma_addr_t dma_handle; void *cpu_addr dma_alloc_coherent(pdev-dev, size, dma_handle, GFP_KERNEL); if (!cpu_addr) { dev_err(pdev-dev, Failed to allocate DMA buffer\n); return -ENOMEM; }这里有个关键点dma_alloc_coherent分配的地址是设备可见的物理地址驱动要把dma_handle写到设备的 DMA 寄存器里设备才能正确访问。如果写错了设备会 DMA 到错误的内存区域轻则数据错乱重则系统崩溃。另外要注意 DMA 掩码。有些设备只能访问 32 位地址空间这时候要用dma_set_mask_and_coherent设置掩码ret dma_set_mask_and_coherent(pdev-dev, DMA_BIT_MASK(32)); if (ret) { dev_err(pdev-dev, Failed to set DMA mask\n); return ret; }如果不设置内核可能分配 64 位地址设备访问不了就会出现 DMA 失败。这个问题在嵌入式平台上特别常见因为很多 SoC 的 PCIe 控制器只支持 32 位地址。4. 中断处理与数据通路让设备真正跑起来4.1 中断类型与注册PCIe 设备支持三种中断方式INTx、MSI、MSI-X。INTx 是传统的中断引脚通过 IOAPIC 路由共享中断线效率低。MSI 是消息信号中断设备直接写一个特定地址触发中断不共享。MSI-X 是 MSI 的扩展支持更多中断向量并且每个向量可以独立配置。现代 PCIe 设备基本都用 MSI-X驱动里要优先尝试 MSI-X失败再退到 MSI最后才用 INTxret pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSIX | PCI_IRQ_MSI | PCI_IRQ_LEGACY); if (ret 0) { dev_err(pdev-dev, Failed to allocate IRQ vectors\n); return ret; } ret request_irq(pci_irq_vector(pdev, 0), my_irq_handler, 0, my_pci_drv, priv); if (ret) { dev_err(pdev-dev, Failed to request IRQ\n); pci_free_irq_vectors(pdev); return ret; }pci_alloc_irq_vectors的第二个和第三个参数是最小和最大向量数这里只申请一个。如果设备需要多个中断向量比如多队列网卡就要申请多个并且每个向量单独注册处理函数。中断处理函数要做的事情通常很简单读取设备的中断状态寄存器判断是不是自己的中断清除中断标志然后唤醒等待队列或者调度下半部。不要在中断上下文里做耗时操作比如大量内存拷贝或者睡眠这些要放到 tasklet 或工作队列里。4.2 数据通路的实现模式PCIe 设备的数据通路一般有两种模式PIO 和 DMA。PIO 是 CPU 直接读写 BAR 空间适合小数据量、低频率的场景。DMA 是设备直接访问内存适合大数据量、高频率的场景。以字符设备为例用户态通过read/write系统调用触发数据传输。驱动里要实现file_operations的read和write函数。对于 DMA 传输典型流程是用户态提供缓冲区驱动用copy_from_user拷贝到内核缓冲区或者直接用用户态缓冲区做 DMA需要get_user_pages固定页面。驱动把缓冲区物理地址写到设备的 DMA 描述符里。驱动写设备寄存器启动 DMA。设备完成传输后触发中断中断处理函数唤醒等待队列。驱动检查传输状态返回结果给用户态。这里有个性能优化点如果数据量大可以用mmap把 DMA 缓冲区映射到用户态避免拷贝。但要注意缓存一致性问题mmap映射的内存要设置为非缓存或者写合并否则 CPU 缓存和 DMA 数据可能不一致。4.3 中断与轮询的取舍有些场景下中断开销太大比如高速采集卡每秒产生几十万次中断CPU 全耗在中断处理上了。这时候可以用轮询模式驱动主动查询设备状态而不是等中断。Linux 的 NAPI 机制就是中断和轮询的结合中断触发后关闭中断用轮询处理一批数据处理完再打开中断。对于自定义设备你可以实现类似的机制在中断处理函数里调度一个工作队列工作队列里循环读取设备 FIFO 直到为空然后再重新使能中断。这样既保证了低延迟又避免了中断风暴。5. 调试与问题排查掉卡、降速、AER 怎么破5.1 常见问题速查表问题现象可能原因排查方法设备不识别供电不足、链路训练失败、驱动未加载lspci看是否枚举到dmesg看链路状态掉卡链路不稳定、电源波动、散热问题dmesg看 AER 报错检查电源和散热降速链路训练到低速率、信号完整性差lspci -vv看 LnkSta检查走线和连接器降 lane链路宽度协商失败、金手指脏污lspci -vv看 LnkSta清洁金手指AER 报错传输错误、ECC 错误、超时dmesg看 AER 详情检查硬件和驱动DMA 失败地址掩码不匹配、IOMMU 配置问题检查dma_set_mask看 IOMMU 日志中断不触发MSI-X 配置错误、中断路由问题cat /proc/interrupts看中断计数5.2 链路问题排查实战掉卡、降速、降 lane 是 PCIe 最让人头疼的问题因为它们往往是硬件和软件交织在一起的。我遇到过一个案例一块 FPGA 板卡在实验室跑得好好的到了现场就频繁掉卡。用dmesg看日志发现有大量 AER 报错pcieport 0000:00:1c.0: AER: Corrected error received: 0000:03:00.0 pcieport 0000:00:1c.0: AER: PCIe Bus Error: severityCorrected, typePhysical LayerCorrected error 说明链路有错误但被纠正了还不致命。但如果出现 Uncorrected error设备就会掉。排查步骤是用lspci -vv看链路状态lspci -vv -s 03:00.0 | grep -A 10 LnkSta LnkSta: Speed 8GT/s (downgraded), Width x4 (downgraded)如果显示 downgraded说明链路没有协商到最高速率或最大宽度。检查信号完整性。用示波器看差分信号的眼图如果眼图闭合说明信号质量差。常见原因是走线太长、阻抗不匹配、连接器接触不良。检查电源。PCIe 设备对电源纹波很敏感尤其是高速率下。用万用表测 12V 和 3.3V 电源纹波要控制在几十毫伏以内。检查散热。有些 FPGA 或高速网卡发热量大温度过高会导致链路不稳定。摸一下散热片如果烫手就要加风扇。软件侧可以尝试限制链路速率setpci -s 03:00.0 CAP_EXP10.w0002:000f这个命令把目标链路速率设为 5GT/sGen2如果降速后稳定了说明是信号完整性问题。5.3 AER 机制与错误处理AERAdvanced Error Reporting是 PCIe 的错误报告机制能报告 Corrected、Uncorrected Non-Fatal、Uncorrected Fatal 三类错误。驱动里可以注册 AER 处理函数在错误发生时做恢复static pci_ers_result_t my_error_detected(struct pci_dev *pdev, pci_channel_state_t state) { switch (state) { case pci_channel_io_normal: return PCI_ERS_RESULT_CAN_RECOVER; case pci_channel_io_frozen: return PCI_ERS_RESULT_NEED_RESET; case pci_channel_io_perm_failure: return PCI_ERS_RESULT_DISCONNECT; } return PCI_ERS_RESULT_NONE; }然后在pci_driver里注册err_handlerstatic const struct pci_error_handlers my_err_handler { .error_detected my_error_detected, .slot_reset my_slot_reset, .resume my_resume, };AER 处理的关键是区分可恢复和不可恢复错误。Corrected error 一般不用管内核会记录但不会影响设备。Uncorrected Non-Fatal 可以尝试 reset 恢复Uncorrected Fatal 通常需要重新枚举设备。5.4 实操心得与避坑技巧第一个心得probe 里一定要检查返回值。我见过太多驱动在pci_request_regions失败后继续往下走结果访问了未映射的地址直接 oops。每个可能失败的调用都要检查失败就按相反顺序清理已申请的资源。第二个心得中断处理函数要快进快出。不要在中断里做printk因为printk可能睡眠而且大量打印会拖慢系统。调试时可以用pr_debug或者 tracepoint生产环境一定要关掉。第三个心得DMA 缓冲区要用dma_alloc_coherent分配不要用kmalloc然后手动转换物理地址。kmalloc返回的地址在开启 IOMMU 后可能不是设备可见的物理地址会导致 DMA 失败。第四个心得热插拔测试一定要做。很多驱动在冷启动时正常热插拔后就出问题。测试方法是反复echo 1 /sys/bus/pci/devices/0000:03:00.0/remove和echo 1 /sys/bus/pci/rescan看驱动是否能正常 probe 和 remove。第五个心得用dev_err/dev_info而不是printk。dev_*系列函数会自动带上设备名称和 BDF排查问题时能快速定位是哪个设备出的错。6. 从零写一个 PCIe 驱动的完整流程6.1 环境准备与骨架搭建先确认内核头文件已安装apt install linux-headers-$(uname -r)然后创建驱动骨架#include linux/module.h #include linux/pci.h #include linux/fs.h #include linux/cdev.h #define DRV_NAME my_pci_drv static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) { dev_info(pdev-dev, Probe called\n); return 0; } static void my_pci_remove(struct pci_dev *pdev) { dev_info(pdev-dev, Remove called\n); } static const struct pci_device_id my_pci_ids[] { { PCI_DEVICE(0x1234, 0x5678) }, { 0, } }; MODULE_DEVICE_TABLE(pci, my_pci_ids); static struct pci_driver my_pci_driver { .name DRV_NAME, .id_table my_pci_ids, .probe my_pci_probe, .remove my_pci_remove, }; module_pci_driver(my_pci_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(My PCIe Driver);Makefileobj-m my_pci_drv.o KDIR : /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean编译加载make insmod my_pci_drv.ko dmesg | tail如果设备 ID 匹配应该能看到 probe 被调用。6.2 完善 probe 与资源管理在 probe 里按顺序做以下事情static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) { int ret; void __iomem *bar0; dma_addr_t dma_handle; void *dma_buf; ret pci_enable_device(pdev); if (ret) { dev_err(pdev-dev, Failed to enable device\n); return ret; } ret pci_request_regions(pdev, DRV_NAME); if (ret) { dev_err(pdev-dev, Failed to request regions\n); goto err_disable; } bar0 pci_iomap(pdev, 0, 0); if (!bar0) { dev_err(pdev-dev, Failed to map BAR0\n); ret -ENOMEM; goto err_release; } ret dma_set_mask_and_coherent(pdev-dev, DMA_BIT_MASK(64)); if (ret) { dev_err(pdev-dev, Failed to set DMA mask\n); goto err_unmap; } dma_buf dma_alloc_coherent(pdev-dev, 4096, dma_handle, GFP_KERNEL); if (!dma_buf) { dev_err(pdev-dev, Failed to allocate DMA buffer\n); ret -ENOMEM; goto err_unmap; } ret pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSIX | PCI_IRQ_MSI | PCI_IRQ_LEGACY); if (ret 0) { dev_err(pdev-dev, Failed to allocate IRQ vectors\n); goto err_free_dma; } ret request_irq(pci_irq_vector(pdev, 0), my_irq_handler, 0, DRV_NAME, pdev); if (ret) { dev_err(pdev-dev, Failed to request IRQ\n); goto err_free_irq; } pci_set_drvdata(pdev, bar0); dev_info(pdev-dev, Probe successful\n); return 0; err_free_irq: pci_free_irq_vectors(pdev); err_free_dma: dma_free_coherent(pdev-dev, 4096, dma_buf, dma_handle); err_unmap: pci_iounmap(pdev, bar0); err_release: pci_release_regions(pdev); err_disable: pci_disable_device(pdev); return ret; }remove 函数按相反顺序清理static void my_pci_remove(struct pci_dev *pdev) { void __iomem *bar0 pci_get_drvdata(pdev); free_irq(pci_irq_vector(pdev, 0), pdev); pci_free_irq_vectors(pdev); pci_iounmap(pdev, bar0); pci_release_regions(pdev); pci_disable_device(pdev); dev_info(pdev-dev, Remove done\n); }6.3 中断处理与数据读写中断处理函数static irqreturn_t my_irq_handler(int irq, void *dev_id) { struct pci_dev *pdev dev_id; void __iomem *bar0 pci_get_drvdata(pdev); u32 status; status ioread32(bar0 REG_INT_STATUS); if (!(status INT_MASK)) { return IRQ_NONE; } iowrite32(status, bar0 REG_INT_CLEAR); /* 唤醒等待队列或调度下半部 */ return IRQ_HANDLED; }数据读写通过字符设备接口暴露给用户态这里不展开完整代码核心是copy_from_user/copy_to_user和 DMA 传输的配合。6.4 测试与验证加载驱动后用lspci -vv确认设备状态用cat /proc/interrupts确认中断注册成功用dmesg看驱动日志。如果设备有回环测试模式可以先在驱动里做自测确认寄存器读写和 DMA 通路正常。性能测试可以用dd或者自己写测试程序测量吞吐量和延迟。如果吞吐量不达标检查 DMA 描述符环大小、中断合并配置、PCIe 链路速率和宽度。7. 几个容易被忽略的细节7.1 电源管理与运行时 PMPCIe 设备支持多种电源状态D0全开、D1/D2中间态、D3hot/D3cold低功耗。驱动里可以实现suspend/resume回调在系统休眠时保存设备状态唤醒时恢复。如果设备支持运行时 PM还可以在空闲时自动进入低功耗状态。static int my_pci_suspend(struct device *dev) { struct pci_dev *pdev to_pci_dev(dev); /* 保存寄存器状态 */ return 0; } static int my_pci_resume(struct device *dev) { struct pci_dev *pdev to_pci_dev(dev); /* 恢复寄存器状态 */ return 0; } static const struct dev_pm_ops my_pm_ops { .suspend my_pci_suspend, .resume my_pci_resume, };然后在pci_driver里设置.driver.pm my_pm_ops。注意 suspend 时要确保没有正在进行的 DMA 传输否则恢复后数据会错乱。7.2 多设备与多实例如果系统里有多个相同设备驱动要支持多实例。每个设备对应一个私有数据结构通过pci_set_drvdata保存中断处理函数通过dev_id区分是哪个设备。字符设备可以用主设备号加次设备号区分或者用cdev_add动态分配。7.3 国产平台与嵌入式适配在国产 Linux 平台或者嵌入式 SoC 上做 PCIe 驱动要注意几点一是 PCIe 控制器可能只支持 32 位 DMA 地址必须设置 DMA 掩码二是中断控制器可能不支持 MSI-X要退到 MSI 或 INTx三是设备树里要正确配置 PCIe 控制器的 ranges 和 interrupt-map否则枚举会失败。我在全志平台上移植过一个 PCIe 采集卡驱动踩的最大的坑就是 DMA 掩码没设导致设备 DMA 到高地址内存失败。后来加上dma_set_mask_and_coherent(pdev-dev, DMA_BIT_MASK(32))就正常了。另外国产平台的 PCIe 时钟和复位配置也跟标准 x86 不同需要仔细看 SoC 手册。7.4 调试工具与技巧除了lspci和dmesg还有几个工具很有用setpci直接读写配置空间调试链路速率、BAR 大小很方便。pcimem读写 BAR 空间验证寄存器读写是否正常。perf分析中断和 DMA 的性能瓶颈。ftrace跟踪内核函数调用定位驱动里的耗时操作。我习惯在 probe 和 remove 里加dev_dbg调试时打开动态调试echo file my_pci_drv.c p /sys/kernel/debug/dynamic_debug/control这样只有这个文件的调试信息会打印不会刷屏。8. 最后分享几个实战技巧第一个技巧probe 里加一个自检步骤读写几个已知寄存器确认硬件真的在工作。我遇到过设备枚举成功但寄存器读写全是 0xFFFFFFFF 的情况原因是 FPGA 没加载比特流。自检能提前发现这类问题。第二个技巧中断处理函数里加一个计数器通过 sysfs 暴露出来。调试时看计数是否增长能快速判断中断有没有触发。如果计数不增长检查 MSI-X 配置和中断路由。第三个技巧DMA 传输加超时机制。设备可能因为各种原因不触发完成中断如果没有超时用户态会一直阻塞。用wait_event_interruptible_timeout代替wait_event_interruptible超时后返回错误并复位设备。第四个技巧热插拔测试用脚本自动化反复插拔几百次看有没有内存泄漏或者资源未释放。用slabtop看内核内存变化用ls /sys/bus/pci/devices/看设备目录是否残留。第五个技巧如果设备支持 AER一定要注册错误处理函数。即使不做复杂恢复至少把错误信息打印出来方便定位问题。AER 报错里会包含错误类型、错误位置、建议的恢复动作是排查链路问题的第一手资料。这些经验都是我在实际项目中一点点积累的有些是踩坑之后才明白的。PCIe 驱动开发没有捷径多写多调多测遇到问题看日志、查手册、做实验慢慢就有感觉了。
返回列表