ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

PLX SDK Linux V7.24 驱动编译、内核适配与排错

PLX SDK Linux V7.24 驱动编译、内核适配与排错

简介:PLX SDK for Linux V7.24 是面向Linux平台PCIe设备驱动与应用开发的官方工具包,适合对接PLX PCIe控制器的驱动工程师、嵌入式开发者及高性能I/O系统研发人员。它提供内核驱动、用户态库、API说明与开发辅助资料,可用于设备初始化、寄存器配置、中断处理、DMA传输和性能调试,也适合旧项目维护与硬件适配参考。压缩包共8个文件,以4个htm网页文档、3个pdf手册和1个tar源码包为主,整体约3.61MB;网页与PDF侧重API、用户手册、常见问题及发行说明,tar包便于查看驱动与示例工程结构。已有412人学习下载。作为PLX并入Broadcom前发布的最终版本,其文档体系较完整,读者可据此梳理PCIe控制器开发流程、排查兼容性问题,并借助示例代码与接口说明搭建测试程序,对理解早期PLX SDK的驱动架构与Linux高速I/O开发具有参考价值。

1. PLX SDK Linux V7.24 装之前:三个必须先对齐的版本前提

PLX SDK Linux 这套环境,最容易出错的地方不在编译本身,而在编译之前。板卡插好了,lspci也能看到那个 PCIe 交换芯片,可/dev下干净得像什么都没装过,应用层调 API 第一句就返回设备不存在。V7.24 这一年头对内核前提比较挑,它默认的内核区间和现在主流发行版隔了好几年,直接make大概率在第一屏就停住。做板卡适配、交换芯片功能验证、产线老化测试的人都绕不开它,手上有一块回板等着点亮的硬件工程师也一样。装之前先把内核、编译器、芯片三条线对齐,后面能少掉一半时间。到现在我还记得某次对着一行Unknown symbol翻了大半夜内核 config,那种感觉再也不想经历第二遍。

2. 解包与编译:从 PlxSdk 目录树到第一颗 plx.ko

2.1 目录结构:先分清驱动、API 库和用户态服务

把 V7.24 的包解开之后,别急着敲make,先在顶层ls一遍。这套 SDK 的目录划分一直比较固定,理解了三层结构,后面每一条编译命令你就知道它在动哪一块。最常见的布局是这样:

PlxSdk/ ├── Driver/ │ └── PlxDriver/ # 内核模块源码,最终产出 plx 驱动 .ko ├── Api/ │ └── Source/ # 用户态 API 库,编出 libPlxApi.a / .so ├── Include/ # PlxApi.h、PlxTypes.h 等公共头文件 ├── Samples/ # 示例程序,验证链路最方便的入口 ├── Svc/ # PlxSvc 用户态服务,管设备热插拔与枚举 └── Install/ # 打包脚本与 udev 规则

这三层的关系要理清:内核模块负责把 PCIe 设备的 BAR 空间映射出来、处理中断和 DMA,只提供最底层的出入口;API 库把这些出入口包成PlxPci_*、Plx_dma_*这类函数;PlxSvc跑在用户态,负责打开设备、维护设备列表,样例程序通过它间接访问硬件。所以只编驱动、不装 API 库,你会得到一个能加载但没法用的模块;反过来只装库不加载驱动,所有 API 调用统一返回失败。

提示:不同小版本的目录名会有细微差别,比如驱动目录有的叫PlxDriver,有的直接是Driver。以解包后的实际结构为准,下面所有路径都按上面这套布局写。

2.2 依赖与内核头文件:编译前把 gcc、make、headers 对齐

内核模块编译对工具链的敏感度远高于普通用户态程序。编译器版本太新会对老代码报一堆警告甚至错误,内核头文件和当前运行内核不匹配则会直接编译失败。装之前先把这几样点一遍:

# 1. 确认当前内核版本,后面所有 headers 都要跟它一致 uname -r # 2. 编译器与构建工具 gcc --version make --version # 3. 内核头文件必须是运行内核对应的那一份 ls -l /lib/modules/$(uname -r)/build # 4. 确认 PCI 与 MSI 相关配置没被裁掉 grep -E "CONFIG_PCI_MSI|CONFIG_PCI_MSI_IRQ_DOMAIN" /boot/config-$(uname -r)

/lib/modules/$(uname -r)/build这个软链接必须存在且指向真实内核源码树,没有的话用发行版的包管理器装linux-headers-$(uname -r)或kernel-devel。这一步是最常见的失败源头:很多人编译时用的是 A 内核的头文件,装完之后在内核 B 上insmod,报Invalid module format。CONFIG_PCI_MSI这条也要看,有些精简过的嵌入式内核把 MSI 关掉了,SDK 里默认按 MSI 中断写的那部分逻辑会走不通,需要退回 INTx 模式,后面第 3 章会讲到。

我一般还会额外确认一下CONFIG_MODULE_SIG的状态。开了强制签名而模块没签,insmod会直接被拒,报错信息又只有一行,很容易被误判成驱动本身的问题。

grep CONFIG_MODULE_SIG_FORCE /boot/config-$(uname -r) # 输出 y 的话,要么在 BIOS 里关 Secure Boot,要么给模块签名

2.3 编译驱动模块:PLX_ARCH 与 KERNELDIR 两个变量决定成败

进入驱动目录先别急着make,把 Makefile 里接受的外部变量扫一遍,这是省时间的关键动作:

cd PlxSdk/Driver/PlxDriver grep -nE "PLX_ARCH|KERNELDIR|KDIR|CROSS_COMPILE" Makefile

常见的做法是显式传这两个变量,别指望 Makefile 的默认推导:

# PLX_ARCH 决定指针位宽、寄存器访问宏和部分汇编片段 # KERNELDIR 必须指向运行内核的 build 目录 make PLX_ARCH=amd64 KERNELDIR=/lib/modules/$(uname -r)/build

PLX_ARCH取x86、amd64、arm、arm64之类的值,写错的话编译阶段可能只是几条警告,加载后才会以数据错位的形式暴露出来,排查成本极高。KERNELDIR不传也能编过,但会用 Makefile 里写死的路径,一旦和你实际内核不一致,insmod阶段就会报版本魔法数不匹配。ARM 平台还要补CROSS_COMPILE,否则默认调本机 gcc,链接出来的 .ko 架构对不上。

编译成功后在当前目录找.ko:

ls -lh *.ko modinfo ./plx*.ko | head -20

modinfo的输出重点看vermagic,它应该和你uname -r完全一致,包括内核版本号后的编译标识。不一致就回头检查KERNELDIR,不用再往下走。模块名在不同分支里可能是plx_drv.ko,也可能是PlxDriver.ko,用ls结果为准,后面加载时替换成实际文件名。

2.4 装上模块和节点:insmod 之后 dmesg 要看哪几行

驱动编出来先别急着写进开机脚本,手动加载一遍看行为:

# 先清 dmesg 环形缓冲,方便只看这次加载的输出 sudo dmesg -C # 加载模块 sudo insmod ./plx_drv.ko # 立刻看内核说了什么 sudo dmesg | tail -40

这段输出是整篇操作里信息密度最高的地方。正常的加载日志里应该能看到驱动版本号、扫描到的 PCIe 设备 BDF 号、BAR 映射结果,以及类似probe of 0000:xx:00.0 succeeded的确认行。出现Unknown symbol说明依赖的符号在当前内核里不存在,通常是内核版本超出了 SDK 的支持区间,属于第 3 章要处理的问题;出现cannot reserve BAR说明 BAR 空间被别的驱动或固件占着,第 4 章会单独讲。

设备节点一般由 udev 根据模块创建的 class 自动生成:

ls -l /dev/plx* /dev/Plx* 2>/dev/null

两个大小写都试一下,不同版本命名不一致。节点没出来但驱动已经加载,先看 SDK 的Install/下有没有 udev 规则文件,常见做法是拷到/etc/udev/rules.d/再udevadm control --reload-rules。还不行就手动建一个mknod节点应急,确认权限是 666 或至少对业务用户可读写。

2.5 安装 API 库与样例:让用户态 Demo 能链上

驱动层通了以后装用户态部分。API 库在Api/Source下,编译产物是静态库和动态库两种形态:

cd PlxSdk/Api/Source make PLX_ARCH=amd64 # 头文件和库文件分别落到系统路径,方便后续编译样例 sudo cp ../../Include/*.h /usr/local/include/ sudo cp libPlxApi.so* /usr/local/lib/ sudo ldconfig

ldconfig这步别省,否则样例程序运行时找不到libPlxApi.so。如果你不想动系统路径,也可以在编译样例时用-I和-L指定相对路径,效果一样,适合只做一次性验证的场景。

样例程序在Samples/下,先编两个最实用的:

cd PlxSdk/Samples make PLX_ARCH=amd64 # 典型产物:Plx_Device 列出设备,Plx_Reg 读写寄存器

Plx_Device只做枚举,不碰硬件寄存器,是判断「驱动 + 库 + 服务」这条链路通没通的最轻量工具。Plx_Reg会真正读写 BAR 空间,跑之前确认你没有在跟别的测试程序抢同一块板卡。

用户态服务PlxSvc一般要单独跑起来,它的作用是维护设备列表并处理热插拔事件:

cd PlxSdk/Svc sudo ./PlxSvc -i # -i 表示前台交互模式,调试阶段别用 -d

前台模式能看到它枚举到几个设备、每个设备的 BDF 和厂商/设备 ID。确认无误后再改成后台守护方式。

3. 内核适配:V7.24 在 5.x/6.x 上的六处编译改动

3.1 版本宏是好东西:先看一眼 SDK 里有没有做条件编译

V7.24 的源码里其实已经带了一部分兼容代码,很多地方用#if LINUX_VERSION_CODE包着,只是覆盖的内核区间不够新。动手改之前先做个统计,能省掉一堆重复劳动:

cd PlxSdk/Driver/PlxDriver grep -rn "LINUX_VERSION_CODE" . | head -30 grep -rn "KERNEL_VERSION(" . | wc -l

如果数量不少,说明作者是按「打补丁」的思路写的,你只需要在已有分支上补新条件,而不是从头改。补的时候统一用这个格式,便于以后回溯:

#include <linux/version.h> #if (LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)) /* 6.4 起 class_create 去掉 owner 参数 */ plxClass = class_create(PLX_DRIVER_NAME); #else plxClass = class_create(THIS_MODULE, PLX_DRIVER_NAME); #endif

LINUX_VERSION_CODE在编译期展开成整数,KERNEL_VERSION(a,b,c)把它编码成同一套表示,两者比较就能在编译期确定走哪个分支。这种写法比直接改源码安全得多——同一份代码可以同时编给不同内核,出问题也能一眼看出是哪条分支引入的。

3.2 class_create 与 proc_ops:模块初始化路径上的两处硬改

第一处是class_create。老内核是class_create(owner, name)两个参数,6.4 之后只剩class_create(name),多传一个THIS_MODULE会直接报too few arguments to function 'class_create'或者类型不匹配。按 3.1 的条件编译包起来即可。

第二处是proc_create的第四个参数。5.6 之后struct file_operations不能再直接传给 proc 接口,要用struct proc_ops:

#if (LINUX_VERSION_CODE >= KERNEL_VERSION(5, 6, 0)) static const struct proc_ops plx_proc_ops = { .proc_open = plx_proc_open, .proc_read = plx_proc_read, .proc_write = plx_proc_write, .proc_release = plx_proc_release, }; #else static const struct file_operations plx_proc_ops = { .owner = THIS_MODULE, .open = plx_proc_open, .read = plx_proc_read, .write = plx_proc_write, .release = plx_proc_release, }; #endif

注意函数指针的成员名也变了,open变proc_open、read变proc_read,这个不是包一层宏能解决的,必须逐个改名。改漏的典型症状是编译能过,但/proc/plx*打不开或者读出来是空的。

第三处是定时器。init_timer在 4.15 之后被timer_setup取代,老代码里如果用的是setup_timer或者直接给timer.function赋值,在 6.x 上会报passing argument 2 of 'timer_setup' from incompatible pointer type,因为回调原型多了个struct timer_list *参数:

static void plx_timeout_handler(struct timer_list *t) { struct plx_dev *dev = from_timer(dev, t, timeout); /* 原来的业务逻辑,dev 从 timer 反查出来 */ }

from_timer这个宏是配套的,用容器反查的方式拿回自己的设备结构体指针,比老代码里手工container_of干净。

3.3 get_user_pages 与 access_ok:DMA 路径上的签名漂移

DMA 相关代码是改动最集中的一块,因为涉及用户态缓冲区的锁定。get_user_pages这个函数在 4.6、4.9、5.8 三个节点上都动过签名,到 5.8 之后稳定成get_user_pages(start, nr_pages, gup_flags, pages),current、mm、force全部去掉。

#if (LINUX_VERSION_CODE >= KERNEL_VERSION(5, 8, 0)) ret = get_user_pages((unsigned long)userBuf, pageCount, write ? FOLL_WRITE : 0, pages); #else down_read(&current->mm->mmap_sem); ret = get_user_pages(current, current->mm, (unsigned long)userBuf, pageCount, write, 0, pages, NULL); up_read(&current->mm->mmap_sem); #endif

这里的FOLL_WRITE对应老版本里的write布尔参数,语义一样,写法变了。漏掉它会得到一个只读映射,写操作时内核报页错误,DMA 传回来的数据全花。5.8 之前的版本还要自己加mmap_sem读写锁,新版本内部已经处理。

access_ok是另一处:5.0 之后第一个type参数被删掉,只剩access_ok(addr, size)。这类改动不会引起编译错误,只会引起警告,非常容易被忽略——而它的后果是用户态地址校验失效,越界访问在内核里静默发生,症状表现为随机崩溃,典型的黑匣子问题。

注意:这一节的改动只针对 DMA 直线路径,如果板卡上还挂了链式描述符或者 Scatter-Gather 表,dma_map_sg那一侧的 API 在 5.x 里也有调整,需要单独核对,不要一次性全改完再编译。

3.4 编译通过不等于能加载:modpost 与 vermagic 检查

改完源码重新编,编译过了先别急着装。用modpost阶段的警告当筛选条件,很多符号问题在这一步就有提示:

make PLX_ARCH=amd64 KERNELDIR=/lib/modules/$(uname -r)/build 2>&1 | tee build.log grep -iE "warning|undefined" build.log | grep -v "^make"

重点关注两类:一类是implicit declaration of function,说明某个头文件没包含,函数被当成返回 int 处理,运行时指针会被截断;另一类是undefined!或Unknown symbol,说明内核里没有这个符号,要么是配置项没开,要么是 API 已经彻底移除。前一类看着像警告,实际上比错误更危险。

modinfo再确认一次vermagic和depends:

modinfo ./plx_drv.ko | grep -E "vermagic|depends|name"

depends为空说明没有外部模块依赖,加载时不需要先modprobe别的模块。有依赖的话按顺序加载,顺序错了同样报Unknown symbol。

3.5 改动要能回滚:打补丁前先留一份原始树

改之前先cp -a PlxDriver PlxDriver.orig,这个习惯救过我两次。V7.24 的代码量不算小,改到第三四处的时候已经记不清哪几行是自己动的,回头跟原始版本diff一下,清清楚楚:

diff -ruN PlxDriver.orig PlxDriver > plx-v724-kernel6.patch

这个 patch 文件有两个用处:一是换机器时直接patch -p0 < plx-v724-kernel6.patch复现,不用重新改一遍;二是升级到下一个 SDK 版本时,拿来对比官方有没有把这部分合进去。-ruN里的N很关键,它把新增文件也纳入 diff,否则新加的头文件会漏掉。

回滚就是反过来:

patch -R -p0 < plx-v724-kernel6.patch

补丁冲突时别硬patch,先看.rej文件里哪一段没合上,通常是因为官方在新版本里已经改过同一行。

4. 排错:装 PLX SDK 时最常翻车的五处现场

4.1 现象:lspci 有设备,/dev 下没有节点

lspci -nn | grep -i plx能看到那个交换芯片,lsmod里模块也在,但/dev/plx*始终不出现,API 调用返回设备不存在。

原因通常有三层:模块加载了但没有 probe 到设备,可能是设备被固件或 BMC 提前接管;probe 成功但 class 创建失败,udev 拿不到通知;或者模块压根没加载,lsmod看到的是上一次残留的旧版本。先看dmesg | grep -i plx里有没有probe字样,有 probe 无节点就查 udev,无 probe 就查设备归属。

解决按顺序来:先dmesg -C清空再重新insmod,确保看到的是本次日志;确认class_create那段按 3.2 改对了;把 SDK 自带的 udev 规则装到/etc/udev/rules.d/并udevadm trigger;实在不行mknod /dev/plx0 c <major> <minor>手工兜底,major 号从/proc/devices里查。

4.2 现象:insmod 报 Invalid argument 或 Unknown symbol

insmod直接失败,日志里只有一行invalid module format或者一串Unknown symbol in module,看不出具体是哪个符号。

前者的原因是vermagic不匹配,编译时用的内核头文件和运行内核不是同一份,或者架构变量写错了。后者是依赖的导出符号在当前内核不存在,往往是内核配置裁剪掉了某个子系统,或者 API 在新内核里已经不导出。

解决:modinfo对比vermagic和uname -r,不一致就重编;Unknown symbol用grep -w <符号名> /proc/kallsyms确认内核里到底有没有,没有的话要么改代码绕开,要么回头开内核配置项。别用modprobe --force硬塞,vermagic不对的模块即使塞进去,后面访问结构体也会错位。

4.3 现象:驱动装上了但 BAR 申请失败

dmesg里出现cannot reserve BAR或address space collision,模块加载了一半就退出。

原因是那块 PCIe 设备的 BAR 空间已经被别人占了。常见占用方是内核自带的pcieport服务驱动、板卡厂商预装的另一个驱动,或者 BIOS/UEFI 把资源分配给了别的东西。两个 PLX 驱动同时加载也会互相抢。

解决:cat /proc/iomem | grep -i "plx\|pex"看是谁占着;lspci -vv -s <BDF>看Kernel driver in use那一行写的是谁;把冲突的驱动rmmod掉再加载自己的。开机自动加载的要在/etc/modprobe.d/里加黑名单,否则重启又回来。

4.4 现象:DMA 传输超时或数据错位

寄存器读写都正常,一跑到 DMA 就超时或者收上来的数据是乱的,前几个字节对得上,后面全花。

原因多半在地址映射这一层:IOMMU 开着但驱动没走dma_map_*接口,直接把物理地址塞进描述符;或者 3.3 节里get_user_pages的FOLL_WRITE漏了,缓冲区是只读映射;再或者缓冲区没有按页对齐,跨越了不连续的物理页。

解决:先dmesg | grep -i iommu确认 IOMMU 状态,开着的话所有 DMA 地址必须走映射接口拿;用getconf PAGE_SIZE确认对齐粒度,用户态缓冲区按页对齐分配;在描述符提交前后各打一条日志,确认写进去的是总线地址而不是虚拟地址。这类问题没有捷径,只能靠日志一点点缩小范围。

4.5 现象:ARM64 平台编译第一步就挂

在 x86 上编得好好的源码,挪到 ARM64 板子上第一条gcc命令就报错,提示不认识-m64或者链接阶段架构不匹配。

原因是 Makefile 里根据PLX_ARCH拼编译参数,变量没传或者传了 x86 的值,交叉编译工具链也没指定。

解决:make PLX_ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-,三个变量一起给;确认工具链在PATH里;编完用file plx_drv.ko看架构,输出里应该是ARM aarch64,不是x86-64。这一步经常被跳过,结果把 x86 的模块拷到板子上加载,报错信息完全对不上。

5. 验证跑通:用寄存器读写和 DMA 自检确认链路真的通了

验证不能只看「模块加载成功」这一条,那是三层里最外面的一层。我一般按驱动层、服务层、API 层这个顺序往下压,任何一层不过就停在那里,不往下走。

5.1 三层验证的检查顺序

# 第一层:驱动层,看设备和节点 lspci -nn | grep -i plx lsmod | grep -i plx ls -l /dev/plx* # 第二层:服务层,看枚举结果 sudo ./PlxSvc -i # 输出里应该有设备数量、每个设备的 BDF 和 ID # 第三层:API 层,读一个只读寄存器 sudo ./Plx_Reg -d 0 -b 0 -o 0x0 -r 4 # 偏移 0x0 是 Vendor ID / Device ID 所在位置,读出来应该和 lspci 一致

最后一行的返回值是这套环境最硬的证据:寄存器读出来的值和lspci显示的 ID 对得上,说明 BAR 映射、字节序、位宽全部正确。对不上但又不报错,八成是PLX_ARCH传错了导致位宽解释错误。

5.2 一张对照表收口

验证项命令期望结果不通过先查
设备可见lspci -nn | grep -i plx列出交换芯片 BDF插槽、供电、BIOS 分配
模块加载lsmod | grep -i plx模块名带引用计数dmesg里的加载错误
节点生成ls /dev/plx*至少一个字符设备udev 规则、class_create
服务枚举PlxSvc -i设备数与实际一致驱动是否 probe 成功
寄存器读Plx_Reg -r 4与 lspci 的 ID 一致PLX_ARCH、BAR 偏移
DMA 自检Samples/里的 DMA 例子回环数据比对通过IOMMU、页对齐、FOLL_WRITE

DMA 那一项放在最后,是因为它同时依赖前面所有环节,任何一层有问题都会在这里表现成数据错乱,反而不好定位。把前五项跑绿了再碰它,排查范围会小很多。

从那以后我每次装这套 SDK,不管多急,都强制走一遍「清 dmesg → insmod → 看 probe 日志 → 读 Vendor ID」这四步,宁可多花五分钟,也不想再经历一次数据错位查到凌晨。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进