说实话,接这个任务之前我也觉得,驱动移植也就是“把代码换个平台编一遍、有问题再改一改”的事。真正入了“龙芯k——走马观碑组VLLX驱动移植”这个项目才发现,最花时间的不是改代码,而是把平台上那些“隐含假设”一条条捞出来。VLLX原本是跑在x86侧的专用处理卡,整个驱动栈从PCI枚举到中断处理再到DMA路径,全是按x86的习惯写的。我们要做的不是简单交叉编译,而是先判断哪些代码是PCIe设备自身的逻辑、哪些是架构相关的地基。
这篇文章不讨论项目里不能展开的部分,只聊聊VLLX驱动移植时踩过的那些架构深水区,以及我自己实测后觉得能复用的排查套路。如果你也是第一次把某个x86外设驱动搬到龙芯这类非x86平台,希望这些文字能帮你少走一段弯路。内容从背景拆解、代码改造点讲起,中间穿插可落地的操作流程和现场问题记录,基本按我实际推进的先后顺序来。
1. 项目背景与整体思路
1.1 VLLX是什么任务
VLLX在我们项目里是一个内部代号,对应一块安装在PCIe插槽上的专用数据处理卡。它的上游厂商只提供x86 Linux驱动,而我们跑的平台是龙芯,属于LoongArch指令集体系。用户的诉求很明确:在这块板子上把VLLX识别出来、驱动正常加载、中断按时触发,数据面吞吐虽然暂时没做极限调优,但基本读写链路必须稳定。
从驱动结构看,VLLX的软件栈并不复杂:内核模块负责PCI设备枚举、BAR空间映射、中断申请、DMA描述符管理,用户态工具再通过字符设备接口下发任务。问题在于这个驱动写得太“x86本地化”了,里面藏着不少只在Intel/AMD环境里碰巧成立的写法。移植前我先把代码分成两层来看:
- 设备逻辑层:硬件寄存器布局、固件交互协议、数据收发流程。这些和CPU架构无关,原则上应该原样保留。
- 平台适配层:IO空间访问方式、中断向量分配、DMA一致性映射、内存屏障。这一层才是移植真正要动刀的地方。
这种分发很有必要,因为很多老驱动把平台相关代码和设备流程揉在一个函数里。看到满屏#ifdef CONFIG_X86的时候,最忌讳的就是跟着宏去改分支,而是先识别出哪些功能块是“为特定CPU服务的”,然后重新组织代码,让设备逻辑调用一套抽象的访问接口。
1.2 移植前应该先做架构差异评估
动手之前,我花了半天把VLLX驱动在x86环境里完整编了一遍,同时把龙芯平台的内核配置打开对照。这个步骤不能省,它直接决定了后续所有改造的工作量。我列过一张表,把常见的差异点分成必改、可能要改、大概率不用改三类:
| 差异维度 | x86常见情况 | LoongArch/龙芯平台常见情况 | 改造难度 |
|---|---|---|---|
| PCI配置空间访问 | BIOS/UEFI初始化完备 | ACPI/平台固件初始化,同样需要CONFIG_PCI支持 | 低,通常只是内核配置 |
| BAR类型 | 支持IO BAR和MEM BAR | MEM BAR最常用,IO BAR范围有限 | 需要按资源类型判断 |
| 中断模型 | INTx与MSI/MSI-X并存 | 同样支持INTx与MSI/MSI-X,但中断域注册方式不同 | 中,建议用通用API |
| DMA掩码 | 老驱动有时直接赋32位 | 新平台常见64位,但有IOMMU时需正确设置dma_mask | 中 |
| cache一致性 | PCIe设备通常snoop CPU cache | 多数平台一致,部分老桥片或透传场景例外 | 可能出现偶发数据错位 |
| 内存屏障 | x86强内存序掩盖了很多问题 | LoongArch对驱动要求更严谨 | 隐蔽,必须主动梳理 |
| 字节序 | 小端 | 小端 | 不用改 |
| 原子操作 | 老代码可能用x86专属原子xxx | 通用API可用 | 低,但需替换废弃接口 |
一眼看去好像没多少东西,但这些差异叠加在一起,就会出现“编译通过、加载成功、一跑就挂”的诡异现象。所以我的原则是:每个和平台相关的调用点单独过一遍,不要用“原来在x86上没出过事”作为理由放过。
1.3 推进路线怎么设计
整个移植我拆成了五个阶段,每个阶段有明确的结束标志:
- 环境准备与编译适配。拿到LoongArch工具链和对应内核源码,先把VLLX驱动编成内核模块,先不管运行效果,至少要能insmod。
- 枚举与资源确认。在龙芯平台上确认PCIe设备能被系统识别,BAR和MSI-X能力表读出来,和x86侧对比。
- 基础IO读写验证。通过驱动映射BAR,读取设备固件版本这类只读寄存器,验证IO通路。
- 中断与数据面打通。先跑通INTx,再切到MSI/MSI-X;准备一块DMA缓冲区做回环测试。
- 稳定性与异常排查。长时间跑压力,观察有误报错或设备失联。
每个阶段之间留了足够缓冲,因为平台异常往往不是单点原因。比如设备枚举不出来,可能是主板PCIe链路问题,也可能是内核配置缺了PCIEASPM相关内容;不能一上来就怀疑是驱动代码。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 驱动代码中绕不开的架构差异
2.1 BAR映射和寄存器访问,没那么简单
VLLX驱动在x86侧是靠pci_iomap把BAR映射成内核虚拟地址,然后用readl/writel操作寄存器。表面上这些API在龙芯内核里都有,但有几个地方必须重看。
首先是BAR类型的判断。x86主机上BIOS往往会为设备自动分配IO和MEM资源,很多老驱动图省事直接ioremap(pci_resource_start(pdev, 0), size)。这段代码如果遇到类型为IORESOURCE_IO的BAR0,在x86上通常也“碰巧能用”,因为x86 IO空间本质是独立地址空间。但换到龙芯平台,IO空间策略不同,直接把IO资源当内存来映射就会出问题。更稳妥的写法是先看标志再进分支:
c复制if (pci_resource_flags(pdev, bar) & IORESOURCE_MEM) {
regs = pci_ioremap_bar(pdev, bar);
} else {
/* IO BAR,考虑用request_region + inl/outl,或干脆让硬件使用MEM BAR */
dev_err(&pdev->dev, "io bar is not supported now\n");
}
其次是pci_request_region的顺序。有人喜欢在pci_enable_device之前就调,结果在龙芯上偶尔会报资源忙。我实际测试下来,稳妥顺序是:先pci_enable_device,再pci_request_mem_regions,然后才做映射。因为pci_enable_device可能触发固件重新分配资源,太早请求反而拦下了后续更新。
还有一点很隐蔽:有些设备BAR0映射的寄存器区域很长,而设备实际只实现了一部分,读未实现地址可能返回全F或者触发总线错误。x86下总线错误被MCE机制吞掉或容错,表现不明显;换到龙芯平台上,内核会直接报异常甚至死机。所以测试驱动时,我尽量先通过在x86侧用lspci -vvv记录有效寄存器长度,再在龙芯驱动里按需分小段映射,不要贪图省事整段映射后随便读偏移。
2.2 中断处理:不要直接写死irq号
VLLX老驱动的中断部分属于典型的“历史包袱”。它最早是用pci_enable_device之后顺手request_irq(pdev->irq, handler, ...),用的是传统INTx中断。这在旧平台没问题,但设备本身支持MSI-X多队列,x86用户跑在新内核上往往通过某种方式启用了MSI,因此驱动里实际上混杂了多处中断号赋值逻辑。
移植到龙芯平台时,我的建议是彻底改成通用中断向量API:
c复制int nr_vecs = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSIX | PCI_IRQ_MSI | PCI_IRQ_INTX);
if (nr_vecs < 0)
return nr_vecs;
irq = pci_irq_vector(pdev, 0);
ret = request_irq(irq, vllx_irq_handler, 0, "vllx", dev);
这种写法不关心底层是INTx、MSI还是MSI-X,由PCI子系统和中断域去分配。老驱动里那些手动设置MSI地址和数据寄存器的代码一律不要保留,否则到龙芯平台会因为中断控制器不同而失效。
还有一个容易忽视的点:使能MSI-X之前,最好把设备驱动里可能残留的pci_enable_msix旧函数替换成pci_alloc_irq_vectors。新旧API混用虽然短时间能跑,但遇到中断域重新配置或设备复位时,清理顺序会出问题,表现为第二次加载模块后中断注册失败。我们测试时就碰到过“rmmod后立刻insmod,request_irq报-ENOSPC”的现象,最后定位是中断向量没被统一释放。
2.3 DMA路径需要按平台真实能力来
VLLX设备不是简单把数据放在寄存器里让CPU读,而是需要驱动分配一片内存,把要处理的数据放进去,再告诉设备这块内存的DMA地址。老驱动最原始的版本直接用了__get_free_pages拿到物理地址,然后强制转成dma_addr_t给设备用。
这种写法在x86老平台上能跑,原因是很多x86 PCIe Host能直接访问全部物理内存。但换到龙芯平台上不能这么玩,因为从CPU角度看物理地址和从设备角度看DMA地址,中间可能隔着IOMMU或地址窗口。正确做法是使用标准的DMA映射接口:
c复制dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
ring->desc = dma_alloc_coherent(&pdev->dev, ring_size, &ring->dma, GFP_KERNEL);
if (!ring->desc)
goto err;
这里特别要注意:调用dma_set_mask_and_coherent的时机要放在第一次分配DMA内存之前,并且最好在probe的开头就做。有的驱动只在某个配置分支里设置了dma_mask,一旦用户没设置相应模块参数,分配到32位以下的低地址里,碰到VLLX某些固件版本只支持低32位DMA时没问题,但一旦设备开了64位寻址,描述符地址被截断就会导致数据完全错乱。
我建议在初始化函数里统一加上一句:
c复制err = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
if (err)
err = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));
这样在极端情况下至少能自动降级。VLLX的寄存器字段里如果有“DMA地址高位”和“DMA地址低位”两个字段,还要确认设置顺序。我在实际调试时遇到过一次“DMA方向正确、buffer内容却是旧的”,查到最后是外设先读到了低位再读高位,而驱动是反着写,属于时序匹配问题,和CPU架构无关,但只在压力测试时偶然暴露。
2.4 内存屏障与cache一致性,是移植最多暗坑的地方
x86处理器因为长期保持较强的存储模型,很多驱动工程师对内存屏障基本无感。代码里写了writel后马上再写另一个寄存器,x86上大概率没问题,因为CPU会保证对外设的写入顺序。龙芯使用的LoongArch在这方面更接近典型的精简指令集体系,驱动里不能假设“写就是按顺序的”。
移植时我重点检查了三类场景:
- 描述符写入后通知设备:更新完ring tail寄存器前,要保证描述符内容真正可见。需要在写tail前用
dma_wmb()或等效屏障。 - 中断处理中读取设备状态:驱动在中断处理函数中先读状态寄存器,再读DMA数据。要用
ioremap的readl本身的顺序性加上必要的rmb。 - 发送完成回收:CPU读取描述符的owner位之前,确保设备写入已经可见,有时需要
dma_rmb()。
不要一看到“上屏障”就到处加,加多了性能会下降且掩盖问题。更实用的方式是把寄存器访问封装成几个小函数,比如vllx_write_reg64、vllx_post_descriptor,在这些函数里统一加屏障。这样既有理有据,也方便后续排查。我们这次能比较快地定位中断风暴和DMA超时,很大程度归功于这种封装——每个调用点打开dev_dbg就能看到完整序列,而不是在巨型函数里大海捞针。
2.5 编译期差异:那些隐蔽的废弃接口
改完以上逻辑,编译阶段照样会卡住。VLLX驱动是个有年份的项目,里面用了不少旧内核API,比如pci_find_device这种早被扫进历史垃圾堆的函数。虽然这些和“龙芯移植”无关,但在换内核版本时集中爆发。为了不干扰主线,我直接在源码里做兼容处理,按当前内核版本统一整改:
pci_find_device替换为pci_get_device,用完记得pci_dev_put。create_proc_entry这类老proc接口换成proc_create配合proc_ops。- 原子操作尽量使用
atomic_t和atomic_set,避免直接对裸int做原子加减。 struct pci_device_id尽量用PCI_DEVICE宏补上vendor和device号。
工具链方面,龙芯平台现在有完整的loongarch64-linux-gnu-交叉工具链,但内核模块更建议直接在目标板上用同版本内核头文件就地编译。因为外部模块编出来以后还要依赖Module.symvers里的符号版本,交叉环境和板子内核不一致时,最常见的报错就是Unknown symbol或者version magic mismatch。第一版我建议用完全相同的内核源码树,在板子上用Kbuild按外部模块方式编,省掉一堆无谓的兼容问题。
3. 移植实操过程与关键步骤
3.1 先把驱动编成模块:环境与Makefile适配
我采用的方案不是在x86主机上交叉编译完再拷进板子,而是在龙芯板上放一份完整的内核源码树,用板子自身的编译环境直接编。先确认内核配置开了PCI和MSI支持。可在板子上查看:
bash复制zcat /proc/config.gz | grep CONFIG_PCI
zcat /proc/config.gz | grep CONFIG_PCI_MSI
如果输出里没有CONFIG_PCI_MSI=y,后续中断部分基本走不通,优先在内核配置里补上并重新编译安装内核。
VLLX驱动的Makefile原本写死了把目标编进内核,我改成外部模块模式,结构大致如下:
makefile复制obj-m := vllx.o
vllx-objs := main.o dma.o ring.o eth.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
编出来的vllx.ko直接insmod,用dmesg观察probe是否被调用。我在这里碰到最多的并不是语法错误,而是代码里用了某个头文件在LoongArch上不存在或名称不同。例如有些老驱动直接#include <asm/io.h>,其实更应该写成#include <linux/io.h>,让架构头文件去选择正确实现。类似的还有#include <asm/pci.h>,除非真有架构特定结构需要,否则统一改成#include <linux/pci.h>就好。
3.2 通过lspci确认设备枚举和BAR空间
模块能编过,不代表板子能看见卡。第一次在龙芯板上测试时,我先在插入VLLX设备前后分别跑了lspci -nn。输出里如果根本看不到设备,说明问题发生在枚举或链路训练之前,还轮不到驱动代码。
设备正常识别后的表现是lspci -vvv里能看到完整的BAR地址、MSI-X能力、厂商ID和设备ID。我把龙芯平台的输出和x86平台的输出放在一起对比,重点关注:
- BAR0/BAR2地址范围是否一致
- 是否启用了64位BAR
- MSI-X Table offset与BAR位置
- 当前链路速度和宽度是否异常降级
这种对比能帮你过滤掉不少硬件配置问题。例如VLLX在x86主板上BAR2映射了8MB,但在龙芯平台上看到BAR2变成了256B,这多半不是驱动问题,而是PCIe桥窗口或固件分配策略导致。我遇到这种差异时先不强行去驱动里适配,而是检查内核启动参数里有没有pci=resource_alignment或ACPI引起的窗口限制。
如果BAR资源看起来没问题,就顺手用setpci按x86侧记录的能力偏移读几次,确认CPU能通过配置空间访问到设备固件寄存器。这样到驱动阶段再操作MMIO时,已经有了“硬件本身工作正常”的结论。
3.3 最小化验证:先让驱动完成probe和寄存器读取
不要急于把VLLX的完整收发路径打开。我把VLLX驱动拆成最小模块,probe函数只做这几步:
pci_enable_devicepci_request_mem_regions- BAR0映射
- 读取设备固件版本寄存器并打印
对应代码大致是:
c复制static int vllx_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct vllx_dev *dev;
int err;
err = pci_enable_device(pdev);
if (err)
return err;
err = pci_request_mem_regions(pdev, "vllx");
if (err)
goto err_disable;
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
if (!dev) {
err = -ENOMEM;
goto err_release;
}
pci_set_drvdata(pdev, dev);
dev->pdev = pdev;
dev->bar0 = pci_ioremap_bar(pdev, 0);
if (!dev->bar0) {
err = -ENOMEM;
goto err_free;
}
dev_info(&pdev->dev, "chip version: 0x%08x\n", readl(dev->bar0 + VLLX_CHIP_VER));
return 0;
err_free:
kfree(dev);
err_release:
pci_release_mem_regions(pdev);
err_disable:
pci_disable_device(pdev);
return err;
}
这一步如果能在dmesg里看到版本号,就说明PCI枚举、BAR映射、MMIO读写通道全部正常。我遇到过加载直接触发内核异常的情况,原因大多是BAR0映射长度大于设备实际解码范围,导致读取未实现偏移触发了总线错误。解决办法是先看该设备数据手册确定寄存器空间长度,或者用x86侧记录值先硬编码一个较小映射试。
3.4 从INTx到MSI-X:中断一步步拉通
第一次验证中断时,我先不开MSI-X,而是在probe里使用pci_alloc_irq_vectors时只指定PCI_IRQ_INTx,强制回到传统中断。这样可以先排除中断控制器路由问题,再切高级模式。
c复制err = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_INTX);
if (err < 0)
goto err_free;
irq = pci_irq_vector(pdev, 0);
err = request_irq(irq, vllx_irq_handler, IRQF_SHARED, "vllx", dev);
为什么要加IRQF_SHARED?因为INTx线在PCIe上经常被多个设备共享。如果驱动不声明共享,request_irq会返回-EBUSY。VLLX老驱动在x86上跑的时候没暴露这个问题,多半因为那个槽位没有其他INTx设备。换到龙芯测试机的PCIe拓扑后,同一个中断号确实和另一个设备共享了,没加这个标志就直接失败。
INTx能触发后,再改成MSI-X:
c复制err = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSIX);
验证中断是否真的进入处理器,最简单的方法是在handler里加计数器,然后让设备主动产生一个“测试中断”,通过/sys或debugfs读取计数。如果计数器不增长,优先看/proc/interrupts里这个IRQ是否被分配给了离线CPU,或者被irq affinity屏蔽。龙芯平台对中断亲和的管理和x86略有差异,有些CPU核默认不接收PCIe中断时,要用/proc/irq/xxx/smp_affinity把中断绑到正确的核上再测。
3.5 DMA数据通路:描述符环和一致性命中
VLLX的处理流程是驱动把任务描述符放进环形缓冲区,设备通过PCIe DMA读取描述符再去搬数据。这些描述符必须放在一个外设和CPU都能访问的内存里,最适合用dma_alloc_coherent分配。但在老代码里,我看它用的是kmalloc加virt_to_phys,这个组合在龙芯上风险很高。
我改成下面的结构:
c复制struct vllx_ring {
struct vllx_desc *desc; /* CPU虚拟地址 */
dma_addr_t desc_dma; /* 设备DMA地址 */
u32 head;
u32 tail;
};
ring->desc = dma_alloc_coherent(&pdev->dev,
ring_bytes,
&ring->desc_dma,
GFP_KERNEL);
分配之后,驱动每放一个描述符到ring里,执行顺序是:
- 填写描述符字段
dma_wmb(),确保描述符内容不再停留在CPU写入粒度上- 更新head寄存器,告诉设备有新描述符
如果省略第二部,设备可能拿到旧描述符,表现是发送数据偶发不对,或者某个描述符被执行两次。这种问题在x86上极难复现,换到龙芯后概率提高不少,我一度以为是DMA地址算错,浪费了不少时间。
数据缓冲区侧,我统一用dma_map_single管理,而不是在probe时把所有缓冲区预分配并当作物理连续内存用。若VLLX的每次处理粒度很小、且调用频繁,也可以考虑dma_pool。是否预分配和平台的IOMMU配置有关,没有固定的答案,关键是不能绕过DMA API。
初始化的完整probe流程可以整理为:
| 步骤 | 操作 | 失败处理 |
|---|---|---|
| 1 | pci_enable_device | 返回错误 |
| 2 | pci_request_mem_regions | 释放并disable |
| 3 | dma_set_mask_and_coherent | 回退到32位尝试 |
| 4 | pci_ioremap_bar映射BAR | 按序释放前面资源 |
| 5 | pci_alloc_irq_vectors申请中断向量 | 释放BAR映射 |
| 6 | dma_alloc_coherent分配描述符环 | 释放中断向量 |
| 7 | 注册misc设备或字符设备 | 释放描述符环 |
每个失败分支我都写了对应清理函数,确保驱动可以正常rmmod再重新加载。这一点很重要,因为移植阶段要反复insmod/rmmod,任何资源没释放干净都会让第二次加载时行为异常,进而混淆真正的问题。
4. 常见问题与排查技巧实录
4.1 编译错误速查表
| 报错信息 | 可能原因 | 解决办法 |
|---|---|---|
| unknown field ‘xxx’ specified in initializer | 内核API结构体版本旧 | 查当前内核头文件里的结构体定义,更新字段 |
| implicit declaration of function ‘pci_find_device’ | 使用了废弃接口 | 换成pci_get_device/for_each_pci_dev |
| version magic mismatch | 编译工具链与目标板内核不一致 | 在目标板上用相同内核源码树build |
| Unknown symbol pci_xxx | 内核配置/模块依赖缺失 | 检查config并确认相关模块已先行加载 |
undefined reference to ____atomic_add_return |
原子操作用法错误 | 使用linux/atomic.h标准API |
| readl undefined | 缺少linux/io.h | 不要直接包含asm/io.h |
4.2 设备枚举阶段
问题现象:插上VLLX之后lspci看不到设备,但x86同型号卡是好的。
排查思路:
- 换一个不同的PCIe插槽测试,排除机械接触不良,顺带确认插槽是否有独立供电。
- 看BIOS/固件里PCIe端口是否被设置为关闭状态。龙芯平台的BIOS设置项命名可能和各厂商不同,要找到“PCIe Slot”或“PCIe Port”相关菜单。
- 检查内核启动日志里有没有关于PCIe链路训练的警告,比如
link down,training failed。如果看到类似日志,需要检查插槽协商速率是否超出主板支持能力。 - 用
setpci在设备root port上读取link status寄存器,确认链路有电气连接。
龙芯平台上有部分老主板存在PCIe链路速率协商兼容问题,尤其是在连接一些较早设计的PCIe 1.0/2.0设备时。遇到这种情况时可以尝试在内核命令行里加pcie_aspm=off或pci=realloc,再重启观察是否枚举成功。我们当时是用后一个参数解决了BAR空间不足导致设备被跳过的问题。
4.3 中断不触发或触发异常
中断问题占了我整个移植周期的一半。
第一种表现:模块加载成功,用户态下发任务后,驱动一直等不到中断。
先看/proc/interrupts里对应IRQ有没有计数。如果计数一直是0,大概率是任务没有送到设备,或设备认为没有新描述符。检查驱动是不是在更新head寄存器前已经把描述符可见性同步好。我加上dma_wmb()之后,这类问题明显减少。
如果计数有增加但业务逻辑没反应,可能是中断处理中读了错误的状态寄存器。VLLX有两个中断状态寄存器,一个对应“描述符发送完成”,一个对应“硬件错误”。老代码在x86上偶尔跑正常是因为两种中断共享同一事件,但在龙芯上由于时序变化,错误中断先到了,handler只处理“发送完成”分支就退出,导致队列卡死。我在handler里把所有中断状态位都打印一遍再逐个处理,很快就找到了遗漏的第二路事件。
第二种表现:卸载后加载中断注册失败,报-EBUSY或-ENOSPC。
原因大多是中断向量没有释放彻底。确认remove函数里对称调用free_irq和pci_free_irq_vectors,并且要放在pci_disable_device之前。如果设备和驱动之间还有异步任务没停掉,中断handler可能在free_irq后还在跑,那就会更惨。解决方法是先停掉驱动的工作队列和tasklet,再释放中断。
4.4 DMA传送数据错位,最后一个字节不对
我们在VLLX上遇到的DMA数据问题非常有代表性:回环测试时,前4KB数据完全正确,末尾偶尔多出几个字节或少几个字节。
起初怀疑DMA地址对齐有问题,于是把缓冲区和长度都按64字节对齐,问题依然偶发。后来在发缓冲区处加了dma_sync_single_for_device并配合dma_wmb(),读缓冲区前加dma_sync_single_for_cpu,再跑问题就消失了。
老驱动在x86上没做这些同步也能跑,根源是x86平台和PCIe桥的缓存一致性做得强,很多情况下能掩盖驱动漏同步。移植后不能沿用“试出来的正确”,而是要按DMA API规则把每一步都补齐。这也是我在整个项目里最大的体会:不能因为“以前没出事”就断定某个代码段没毛病,很多隐患只是平台太宽容。
4.5 模块加载后系统CPU卡死,控制台死掉
试过几次比较严重的内核异常,最典型的是读取了未实现的BAR偏移。比如BAR0只映射了256字节,但驱动代码读到了偏移0x1000。x86上这类错误不一定立刻触发MCE,但在龙芯上直接导致CPU异常。
处理这些崩溃,除了细读设备手册确认寄存器范围,还有一个技巧:把驱动里所有register访问先间接化,用真值表管理偏移。定义寄存器偏移时用#define常量,访问时打开宏开关做边界检查。虽然后期为了性能可以把检查关掉,但移植阶段这套机制能帮你挡掉大量低级错误。
我还在驱动里加了一个debugfs节点,可以手动读写指定偏移的寄存器,这样不需要反复加载不同版本模块就能验证硬件行为。调试效率提升非常明显,如果再遇到设备莫名挂死,可以直接通过debugfs把关键寄存器状态导出来对比,省下很多插桩重新编译的时间。
5. 移植完成之后,我后悔的几个决定
这个项目如果让我重做一遍,有几个地方我会调整策略。
第一个是把驱动代码改造做一次再“冻结”再验证的节奏更重要。老驱动中大量逻辑是历史原因叠加出来的,里面有很多没有实际作用但看着很吓人的分支。我一开始不敢删,怕影响设备行为,于是把所有分支都保留着,跑出问题时反而无法定位到底是哪个分支在工作。后面我狠下心把VLLX收发主路径重写成一个清晰的状态机,把不相关的兼容代码全部注释掉,验证通过后再逐个恢复。实践证明,设备并没有那么脆弱,干净代码比“看起来全面”的代码更容易定位异常。
第二个是在一开始就应该确认龙芯平台的DMA API和设备的缓存一致性模型,而不是等测试不过才回头查。这个信息不会自己跳出来,需要看内核配置和桥片手册。也建议把测试代码里的每次DMA映射都打点记录,包括虚拟地址、DMA地址、长度、方向。别嫌日志量大,等出问题时这批记录就是最直接的证据链。
第三个是跟硬件同事并行核对“寄存器写入顺序”和“中断状态位行为”这类文档不明确的细节。VLLX固件某些版本在龙芯平台上表现出跟x86不一样的状态位置位时序,代码改了半天最后发现是固件判断条件太苛刻。遇到类似现象,不要只盯着Linux驱动层,有机会就给设备侧打补丁或者调整配置。
最后再分享一个小技巧:移植过程中我一直保留一份“在x86虚拟机里能编译能insmod的最小版驱动”。每次改动后先在x86环境跑一遍基础加载卸载,确认没有引入通用性问题,再拿到龙芯平台测。这看起来多了一步但能有效分离“设备逻辑错误”和“LoongArch平台适配错误”。尤其是后来频繁改DMA和中断代码时,如果没有这个安全网,很多bug根本分不清是哪一层引入的。希望这些经验能帮你在下次外设驱动跨平台移植时,少翻几回车。
