简介:PLX SDK for Linux V7.24 是面向 Linux 平台 PCIe 驱动与应用开发者的开发工具包,由 PLX Technology 发布,也是其并入 Broadcom 前较具节点意义的版本。它主要用于配置 PLX PCIe 控制器、管理总线设备、处理中断与 DMA 传输,适合服务器、存储、网络设备及工业自动化等高性能 I/O 场景。压缩包共 8 个文件,含 4 个 htm 指南、3 个 pdf 文档和 1 个 tar 归档,整体约 3.61MB;htm 与 pdf 侧重用户手册、API 说明、常见问题及版本记录,tar 归档便于获取 SDK 主体内容。已有 412 人学习下载。读者可据此梳理 PCIe 设备初始化、驱动调试与性能优化思路,参考 API 资料搭建用户空间程序;虽不再持续更新,仍适合查阅旧版接口与兼容性信息。
1. PLX SDK for Linux V7.24 能解决什么:从 lspci 看不到的那半边说起
板子插进主机,lspci老老实实列出桥设备,链路训练完成,一切看起来正常;可一旦要读交换芯片内部的端口寄存器、配置端口到端口的转发、跑一次 DMA、抓每个端口的实时吞吐,命令行工具立刻失效——因为 PCIe 交换芯片真正有价值的那部分寄存器,根本不在标准配置空间里。PLX SDK for Linux V7.24 补的就是这一段:它由内核驱动模块、用户态 API 库(PlxApi)、一组示例工程和命令行工具构成,把「按偏移读写配置空间、按 BAR 读写内存映射寄存器、开 DMA 通道搬数据、读性能计数器」这些动作封装成稳定的函数调用。适合三类人:做 PCIe 交换板卡驱动适配的、做多卡多盘背板带宽验证的、以及需要在 Linux 下把交换芯片当黑匣子反复读写做产线测试的。它不替代lspci/setpci,而是把那些工具够不到的地方补齐。
2. 装与加载:把安装包变成能调用的 libPlxApi 和内核模块
安装这一步最容易出问题:很多人卡在insmod报错,却以为是 SDK 的问题。实际上 PLX SDK 在 Linux 上属于典型的“驱动 + 用户态库”两层结构:内核模块负责在系统里建立一条访问通道,并把设备节点暴露出来;用户态libPlxApi.so则通过 ioctl 把 API 请求下发给驱动。两层版本必须一致,内核模块必须针对当前运行的内核重新编译。理解了这一点,后面的报错基本都有解释。
2.1 安装前的内核侧准备
先不要急着运行安装脚本。确认三样东西:当前内核版本、对应的内核头文件、以及编译工具链。驱动模块会使用/lib/modules/$(uname -r)/build下的头文件进行编译,如果这个目录不存在或者指向一个早已升级掉的内核,编出来的模块就带不上,加载时必然失败。
# 1) 确认当前内核与头文件是否匹配 uname -r ls -d /lib/modules/$(uname -r)/build # 不存在就先装 kernel-devel / linux-headers cat /lib/modules/$(uname -r)/build/include/config/kernel.release 2>/dev/null # 2) 确认 gcc 与当前内核的编译器主版本不要差太远 gcc --version | head -1 cat /proc/version # 3) 确认系统里没有残留的旧版 SDK(很多人机器上装过 7.20) lsmod | grep -i plx find / -name "libPlxApi*" 2>/dev/null find / -name "plx_drv*" 2>/dev/null逻辑很直白:第一步防止驱动编译出来后 vermagic 不一致;第二步防止内核是用较新的 gcc 编译,而你用较旧的 gcc 编译模块,导致结构体布局告警;第三步是清理现场,旧版库通过LD_LIBRARY_PATH或ldconfig缓存仍然会被优先加载,从而出现“明明装了新版,调用的却是旧符号”的问题。参数上唯一的坑在路径:有些发行版把它放在/usr/src/kernels/$(uname -r),有些放在/usr/src/linux-headers-$(uname -r),用ls -d那条命令确认实际路径后再创建软链接。
提示:如果你打算把这套环境做成可复现的镜像,先固定内核版本并锁定
kernel-devel版本,不要开启自动升级,否则下次重启后驱动就需要重新编译。
2.2 跑安装脚本与目录结构确认
安装包通常是一个自解压的.bin,支持--console走纯文本交互模式,默认安装路径一般是/opt/PlxSdk。
chmod +x PlxSdkLinux_*_x86_64.bin sudo ./PlxSdkLinux_*_x86_64.bin --console # 交互里记下三件事:安装路径、库目录名(x86_64 / aarch64)、是否顺带编译驱动 # 安装完先看目录骨架 tree -L 2 /opt/PlxSdk # 典型结果: # Sdk/Include -> PlxApi.h / PlxTypes.h / PlxDataTypes.h # Sdk/Lib/x86_64 -> libPlxApi.so / libPlxApi.a # Driver/ -> 驱动源码与编译脚本 # Samples/ -> ApiTest / DmaTest / EepromTest / PerfMon 等示例 # Tools/ -> 读写配置空间、查版本的命令行小工具目录确认这一步非常重要。后面写 Makefile 时,-I指向Sdk/Include,-L指向Sdk/Lib/<arch>;这两条路径写错,编译器会报出一堆“未知类型 PLX_DEVICE_KEY”,让人误以为是头文件缺失。常见做法是在项目里定义一个变量,不要硬编码:
# 建议在 ~/.bashrc 或构建脚本里统一出口 export PLX_SDK=/opt/PlxSdk export CPLUS_INCLUDE_PATH=$PLX_SDK/Sdk/Include:$CPLUS_INCLUDE_PATH export LD_LIBRARY_PATH=$PLX_SDK/Sdk/Lib/x86_64:$LD_LIBRARY_PATHLD_LIBRARY_PATH的优先级高于ldconfig缓存,调试阶段用它最方便;等到定版时,再把它写进/etc/ld.so.conf.d/plx.conf,然后执行ldconfig,避免依赖 shell 环境。这两种方式不要混用,否则会出现“我这边能跑、换个人就报找不到符号”的情况。
2.3 驱动加载与设备可见性验证
驱动加载脚本一般叫Plx_load.sh或放在Driver/下的start_plx.sh,做的事情无非是编译、insmod、然后创建设备节点。
cd /opt/PlxSdk/Driver sudo ./Plx_load.sh # 内部通常是 make + insmod plx_drv.ko dmesg | tail -30 # 看驱动打印了多少个设备、BAR 是否映射成功 lsmod | grep -i plx ls -l /dev | grep -i plx # 设备节点名随版本不同,用这条确认验证顺序建议固定成三级:先看lsmod有没有模块、再看dmesg里驱动是否扫描到预期数量的设备、最后用 SDK 自带的命令行工具做一次配置空间读。第三级能通过,才说明“驱动 + 库 + 权限”这条链路是通的;如果前两级正常、第三级报权限错误,那就是设备节点权限问题,加一条 udev 规则或用 root 运行都能验证出来。
注意:不要在没卸载旧模块的情况下重复
insmod,dmesg里只会看到一句含义模糊的“File exists”,很容易被忽略。
3. 用 API 摸到芯片:设备枚举、配置空间与 BAR 内存
驱动跑通只是拿到入口,真正干活的接口都在PlxApi.h里。这套 API 的设计思路是“先全局初始化,再按设备索引查找,拿句柄打开,之后所有读写都挂在句柄上”。顺序不能颠倒,尤其是设备索引:它是驱动枚举的顺序,不是 PCI 总线号,重启后顺序可能变,所以生产脚本里不要用索引硬编码,要用PLX_DEVICE_KEY里的总线/设备/功能号去比对。
3.1 枚举与打开的调用顺序
下面这段代码是几乎所有示例工程的骨架,抄下来改一改就能当自己的工具使用。
#include <stdio.h> #include <string.h> #include "PlxApi.h" int main(void) { PLX_DEVICE_KEY key; PLX_DEVICE_OBJ devObj; PLX_STATUS rc; U16 index = 0; /* 1) 全局初始化:加载库、连上驱动,只需一次 */ rc = PlxPci_Initialize(); if (rc != ApiSuccess) { printf("init fail 0x%x\n", rc); return 1; } /* 2) 按索引逐个查找,查到失败为止;索引 = 驱动枚举顺序 */ while (1) { memset(&key, 0, sizeof(key)); rc = PlxPci_DeviceFind(&key, index); if (rc != ApiSuccess) break; /* 没更多设备了 */ /* 3) 打开设备,拿到后续所有 API 用的句柄 */ rc = PlxPci_DeviceOpen(&key, &devObj); if (rc != ApiSuccess) { index++; continue; } printf("idx=%u bus=%02x dev=%02x fn=%x ven=%04x devid=%04x\n", index, key.bus, key.slot, key.function, key.VendorId, key.DeviceId); /* 4) 这里做真正的读写,见 3.2 */ PlxPci_DeviceClose(&devObj); index++; } PlxPci_Initialize(); /* 收尾,和 Initialize 成对;不同小版本函数名略有差异 */ return 0; }编译时把两个路径带上就够了,注意-lPlxApi要放在源文件后面,否则链接器会因为顺序问题报未定义符号:
gcc -O2 -o enum_dev enum_dev.c \ -I$PLX_SDK/Sdk/Include \ -L$PLX_SDK/Sdk/Lib/x86_64 \ -lPlxApi -lpthread -ldl参数说明:PlxPci_DeviceFind的第二个参数是 0 起始的索引,返回失败即表示枚举结束,不要用固定的数量做循环上界;PLX_DEVICE_KEY里的VendorId常见为0x10b5,但下游板卡可能被改成别的值,所以别拿它做过滤条件,用它做打印对照更合适。打开之后所有 API 的第一个参数都是&devObj,句柄失效(比如热插拔后)继续用会返回错误码而不是崩溃,这点比直接mmap安全得多。
3.2 配置空间与 BAR 内存读写
两类访问要分清:配置空间(0x00–0xFF 或扩展到 0x1000)用PciRegisterRead/Write;BAR 里的内存映射寄存器用MemoryRead/Write。前者改的是设备身份和使能位,后者改的是芯片行为,混用会出严重问题。
U32 val32 = 0; /* 读配置空间 0x00:前两字节是 VendorId,后两字节是 DeviceId */ PlxPci_PciRegisterRead(&devObj, 0x00, &val32, sizeof(val32)); printf("cfg[0x00] = 0x%08x\n", val32); /* 读 BAR0 偏移 0x100 处的 32 位寄存器(示例偏移,按芯片手册改) */ PlxPci_MemoryRead(&devObj, 0 /*BAR*/, 0x100, &val32, sizeof(val32)); /* 写之前先读回来做读改写,避免把相邻控制位清掉 */ U32 old = 0; PlxPci_MemoryRead(&devObj, 0, 0x100, &old, sizeof(old)); PlxPci_MemoryWrite(&devObj, 0, 0x100, old | (1u << 3), sizeof(old));逻辑说明:PlxPci_PciRegisterRead的第三个参数是偏移而不是地址,宽度用sizeof传,不要再额外乘字节数;PlxPci_MemoryRead的第二个参数是 BAR 号(0–5),第三个是该 BAR 内的字节偏移。这两组函数的原型在不同小版本间参数顺序和宽度类型可能微调,第一次使用前务必打开PlxApi.h对照声明,不要照抄网上片段。
提示:写寄存器前先做一次读回,确认读到的不是
0xFFFFFFFF。全 F 通常意味着 BAR 没使能、链路掉了、或者设备已经被热移除,这时候写进去的值会静默丢失,排查起来很耗费时间。
下表是我自己项目里常备的对照表,出现在工单里时直接查它,比翻 README 快:
| 操作 | 推荐接口 | 关键参数 | 典型出错原因 |
|---|---|---|---|
| 查设备 | PlxPci_DeviceFind | 0 起始索引 | 用 PCI 总线号当索引 |
| 打开/关闭 | PlxPci_DeviceOpen/Close | 设备键、句柄 | 忘记 Close 导致句柄泄漏 |
| 配置空间 | PlxPci_PciRegisterRead/Write | 偏移、宽度 | 把偏移当绝对地址 |
| BAR 寄存器 | PlxPci_MemoryRead/Write | BAR 号、BAR 内偏移 | 越过 BAR 实际大小 |
| 复位 | PlxPci_DeviceReset | 复位类型 | 复位后不重新 Open |
4. DMA 与性能计数:把带宽和吞吐量化出来
交换芯片的价值在于转发,光能读寄存器没法证明板子合格。这一章做两件事:用 DMA 通道在主机内存和芯片之间做一次真实的批量搬运,量出有效带宽;用性能计数器读取每个端口的收发速率和错误计数,做长时间采样。这两件事都会遇到“偶发失败”问题,所以要顺手把超时处理和重试写进代码里。
4.1 DMA 通道与用户缓冲区模型
用户态直接做 DMA,核心约束只有一个:缓冲区必须页对齐并且被锁定在物理内存里,否则内核可能在传输途中把它换出,DMA 引擎就会读到失效地址。分配方式用posix_memalign加mlock,比malloc稳得多。
#include <stdlib.h> #include <sys/mman.h> #include "PlxApi.h" #define BUF_SIZE (4u * 1024 * 1024) /* 4MB,按需改 */ #define PAGE_ALIGN 4096 void *buf = NULL; if (posix_memalign(&buf, PAGE_ALIGN, BUF_SIZE) != 0) return -1; mlock(buf, BUF_SIZE); /* 锁页,防止换出 */ memset(buf, 0xA5, BUF_SIZE); PLX_DMA_CHANNEL_OBJ dmaObj; PlxPci_DmaChannelOpen(&devObj, 0 /*通道号*/, &dmaObj); /* 通道号按芯片手册 */ PLX_U64 bytesDone = 0; PLX_STATUS rc = PlxPci_DmaTransferUserBuffer(&devObj, &dmaObj, buf, BUF_SIZE, PLX_DMA_TO_DEVICE, /* 方向 */ 5000 /* 超时 ms */); if (rc != ApiSuccess) { printf("dma fail 0x%x, done=%llu\n", rc, (unsigned long long)bytesDone); PlxPci_DmaChannelClose(&devObj, &dmaObj); /* 失败也要关通道 */ } munlock(buf, BUF_SIZE); free(buf);参数说明:BUF_SIZE不要超过芯片侧描述符能覆盖的范围,小页(4KB)设备描述符数量有限,块越大效率越高,一般 1–4MB 比较合适;超时参数不是越大越好,长时间挂住说明描述符没回写,等到 10 秒只会让问题定位更晚;方向枚举在头文件里叫法不一(PLX_DMA_TO_DEVICE/PLX_DMA_LOCAL_TO_PCI之类),以本地头文件为准。测带宽的做法是发 100 次、去掉前 10 次预热、取中位数,单次测量容易遇到 GIC 中断合并导致的异常偏高值。
注意:复用通道号前必须先 Close 再 Open。我见过直接在同一个通道上连续下发传输的写法,前几十次都正常,后面开始随机超时,原因就是上一条描述符链还没回收。
4.2 性能计数器怎么读
交换芯片的 performance monitor 一般按端口组织:先选择端口和计量类型,再使能,最后读取累计值。读取节奏建议用固定周期(比如 1 秒一次)计算两次差值,而不是直接读数除以运行时间,后者在采样间隔不均匀时误差很大。
/* 使能某个端口的收发计数(接口名以本地 PlxApi.h 为准) */ PlxPci_PerfMonControl(&devObj, port /*端口号*/, PLX_PERFMON_ENABLE); U32 rxBytes = 0, txBytes = 0; PlxPci_PerfMonGet(&devObj, port, PLX_PERFMON_RX_BYTES, &rxBytes); PlxPci_PerfMonGet(&devObj, port, PLX_PERFMON_TX_BYTES, &txBytes); printf("port %d: rx=%u tx=%u\n", port, rxBytes, txBytes);采样脚本层面我一般用 Python 包一层,把周期、端口列表、输出 CSV 都参数化,方便画图:
import subprocess, time, csv, sys PORTS = [0, 4, 8, 12] INTERVAL = 1.0 with open("perf.csv", "w", newline="") as f: w = csv.writer(f) w.writerow(["ts"] + [f"p{p}_{k}" for p in PORTS for k in ("rx", "tx")]) t0 = time.time() while time.time() - t0 < 60: # 跑 60 秒 row = [round(time.time() - t0, 3)] for p in PORTS: out = subprocess.check_output(["./perf_get", str(p)]).decode() rx, tx = [int(x) for x in out.split()] # 你的小工具输出两列 row += [rx, tx] w.writerow(row) time.sleep(INTERVAL)这里的关键不是脚本本身,而是采样窗口。计数器是 32 位的,高速链路跑满几秒就会回绕,所以单次读数没有意义,必须靠差值;差值又要求两次间隔足够短且稳定,1 秒是比较稳妥的折中。窗口拉长到 10 秒以上,就会开始看到锯齿,这是回绕导致的,不是链路抖动。
4.3 把两个指标对起来看
单看 DMA 带宽或者单看端口计数都不够。我在验证背板时会同时跑:主机到芯片的 DMA 写入、芯片内部端口转发,然后对比 DMA 侧的有效字节数和端口侧的计数字节数。两者差值如果稳定在协议开销范围内(包头、TLP 开销),说明转发路径正常;如果 DMA 侧明显偏低,问题一般出在主机侧缓冲区或描述符回收;如果端口侧偏低,则要去查链路宽度和误码计数。
5. 避坑与排查:五个让板子无响应的现场
5.1 安装与加载期的三类翻车
现象一:insmod报 “Invalid module format” 或 “Unknown symbol”。原因基本只有一个:模块是用另一套内核头文件编出来的,vermagic和当前运行内核不匹配,换了内核没重编、或者系统开了模块签名校验都会这样。解决方式是用modinfo plx_drv.ko | grep vermagic和uname -r对一下,不一致就进Driver/目录make clean && make,用当前内核重新产出;签名问题可以用dmesg里的关键字确认,再决定是给自己的模块签名还是关闭校验。
现象二:命令行工具报Device not found,但lspci明明有设备。原因有两个层面:驱动没加载(设备节点不存在),或者库和驱动版本不一致(库是 7.24、驱动还是旧的 7.20,通过 ioctl 号对不上)。解决方法是按 2.3 的三级验证逐层确认,先lsmod、再dmesg、最后跑一次配置空间读;版本不一致就彻底卸掉旧模块和旧库,重新安装。
现象三:root 能跑,普通用户报权限错误。原因是设备节点默认只有 root 可访问。解决方法是写一条 udev 规则,把节点属主改到固定组,再把使用账号加入该组:
# /etc/udev/rules.d/99-plx.rules SUBSYSTEM=="plx", KERNEL=="plx*", MODE="0660", GROUP="plxuser" # 之后 sudo udevadm control --reload && sudo udevadm trigger5.2 运行时与数据面的两类翻车
现象四:BAR 内存读回来全是0xFFFFFFFF。原因通常是配置空间里 Memory Space Enable 位没有打开,或者 BAR 地址被系统重分配过而程序缓存了旧地址。解决方法是先读配置空间0x04,确认 bit1 为 1;再确认自己用的 BAR 号和芯片手册里寄存器所在的 BAR 一致——很多板卡把控制寄存器放在 BAR0、把窗口映射放在 BAR2,混用会读到一块未映射的区域。
现象五:DMA 偶发超时,重启进程就能好。原因有三类:用户缓冲区没有页对齐或者被换出;通道关闭不彻底导致描述符链没回收;中断丢失后退化成轮询也没有加超时。解决方法按顺序走一遍:posix_memalign+mlock固定缓冲区、任何失败路径都DmaChannelClose、把超时设成 3–5 秒并在超时后做一次通道复位。这套处理写进封装函数后,线上再没出现过“跑久了就不响应”的情况。
6. 进阶验证:用一条自检脚本固化基线
调通之后的习惯是:每次换机器、换内核、换板子,都跑一遍自检脚本,把输出存档当成基线。脚本要做的事不复杂,但顺序不能乱——驱动没加载就查设备纯属浪费。
#!/usr/bin/env bash set -euo pipefail OUT=baseline_$(date +%Y%m%d_%H%M%S).log exec > >(tee "$OUT") 2>&1 echo "== kernel =="; uname -r echo "== module =="; lsmod | grep -i plx || echo "driver NOT loaded" echo "== device node =="; ls -l /dev | grep -i plx || true echo "== lspci =="; lspci -nn | grep -i 10b5 || true echo "== sdk version =="; $PLX_SDK/Tools/* --version 2>/dev/null || true echo "== config snap =="; ./dump_cfg 0 0x00 0x40 # 前 0x40 字节存档 echo "== bar crc =="; ./bar_crc 0 # 对 BAR0 只读区域做 CRC echo "== dma bw =="; ./dma_bench 0 100 # 100 次取中位数 echo "== perf 10s =="; ./perf_sample 10 echo "done -> $OUT"脚本里bar_crc那一步是很有效的低成本手段:对 BAR 里只读的版本号、能力寄存器区域算 CRC,前后两次跑出的值一致就说明映射没被重分配、驱动读路径没串位。有一次板子偶发读错,就是靠 CRC 每次都不一样定位到 BAR 映射被重新分配的。dma_bench一定要取中位数并扔掉预热次数,第一次传输通常包含页错误和映射开销,直接平均会让基线虚高。采样时长固定成 10 秒,别贪长——计数器回绕会让数据变得无法比较。
从那以后我每次上机第一件事就是把这条脚本跑一遍,把 log 丢进版本库,跟上次的 diff 一眼就能看出是内核变了、驱动变了还是板子真的坏了。希望帮到你。
本文还有配套的精品资源,点击获取