做龙芯K上的VLLX驱动移植,不是那种把源码拿过来改个地址就完事的活。我们走马观碑组这次接到的任务,是把一套原本跑在其它硬件架构上的VLLX设备驱动完整挪到龙芯K平台上,让它能被系统正常枚举、申请中断、搬运数据,并且在长时间运行里不出怪问题。整个过程花了大概三周,中间踩了不少坑,也把很多“文档里不会写”的细节摸清楚了。如果你也准备在龙芯K这类平台上做类似外设驱动的迁移,或者只是想了解驱动跨架构移植到底要动哪些代码,这篇文章应该能帮你省下不少时间。
1. 龙芯K平台接盘VLLX驱动移植:先摸清外设是什么
1.1 拿到板子那天的现场情况
驱动移植这件事,最怕的不是代码难写,而是上来就动手。我们组第一天把板子通电,串口能进系统,内核版本、设备树都能看到,但VLLX对应的驱动模块根本没有可用的内核版本包。外设厂家给了一份能够在原平台上编译通过的驱动源码,目录结构倒挺完整,Makefile也写得像模像样,可真要交叉编译到龙芯K上就会发现,很多实现都是基于x86那套裸地址操作来做的。
所以我的第一个建议是,接到这种“驱动移植”任务时,首先整理三样东西:
- 目标板卡的系统环境:龙芯K内核版本、设备树源文件、是否启用了PCIe或平台设备框架。
- VLLX外设的硬件接口定义:它到底挂在什么总线上、寄存器基地址从哪里来、中断号怎么分配。
- 原有驱动源码的依赖清单:驱动里除了标准内核API,是否有厂商私有的库、是否有依赖特定CPU架构的汇编代码。
如果这三样拿不齐,后面每一步都会像踩在棉花上。
1.2 VLLX到底是哪种外设
先说清楚“VLLX”这个称呼。其实它是我们组内部对这个设备驱动的代号,主要是方便口头沟通,原文全称太长,而且在不同的项目里大家叫法不完全一致,所以我后面统一叫它VLLX。
从驱动开发角度,需要先判断它属于哪种设备模型:
第一种是PCIe设备。如果VLLX是一块PCIe板卡,在龙芯K系统里大概率能被lspci -nn识别出来,那驱动框架继续用struct pci_driver就行,改动重点是总线访问函数和DMA映射部分。
第二种是平台设备。很多龙芯K开发板上的外设并不走PCIe,而是直接挂在CPU的局部总线上,通过设备树描述地址和中断。VLLX如果属于这种,就需要把驱动从pci_driver改成platform_driver,并且用device_node里的compatible字段去匹配。
第三种是像FPGA逻辑扩展出来的自定义设备。这种最麻烦,它在Linux里没有现成的总线模型,常常是通过自定义IO窗口或寄存器桥映射到内存空间里。VLLX原始驱动里如果大量出现“直接写死物理地址”的代码,多半就是这种情况。
判断方法也很简单:进系统后先看/proc/iomem,确认有没有VLLX相关的地址区间;再看设备树文件里面有没有它的节点;最后看原驱动模块加载时靠什么函数完成设备注册。这三个信息能排除掉绝大多数不确定性。
1.3 移植工作量的真实分布
很多人以为驱动移植就是把代码里某个#include换掉,再把编译工具链换一换。实际上你把源码放进龙芯K的内核树里编第一次就会发现,报错的地方远不止这些。
以我个人经验,工作量大致是:
- 设备模型适配,大约占30%。这一步解决“驱动怎么被系统找到、probe什么时候执行”的问题。
- 寄存器访问和内存屏障修正,大约占20%。原驱动里可能大量使用
__raw_readl、ioread32等函数,甚至直接*(volatile u32 *)addr,这些在龙芯K上并非完全不能用,但正确性很依赖平台内存模型。 - DMA一致性与缓存同步修正,大约占30%。这是最隐蔽的部分,代码往往能编过,运行到传输大数据时才会随机丢数据或死锁。
- 中断处理和并发控制,大约占15%。
- 编译选项与内核配置对齐,大约是5%,但处理起来很烦。
想明白这个分布之后,我们就决定不直接改原驱动,而是先在龙芯K上搭好一套可以验证的最小环境,再一步一步把小模块换成真正驱动内容。这个顺序在后面帮了大忙。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 搭建龙芯K交叉编译环境,先编译一个最小驱动模块
2.1 工具链选择与交叉编译参数
龙芯K平台的CPU采用LoongArch指令集,交叉编译器并不是随便拿一个ARM或者x86工具链就能用的。建议直接用开发板配套的SDK工具链,或者从内核源码里看arch/loongarch/Makefile中推荐的交叉编译前缀。通常前缀是loongarch64-linux-gnu-,但不同版本可能有差异,所以最稳的方法是先检查工具链本身。
在SDK环境里执行:
bash复制source /opt/loongson-sdk/environment-setup-loongarch64
echo $CC
$CC -v
如果Target一栏里能看到类似loongarch64-unknown-linux-gnu或者loongarch64-linux-gnu字样,那这条工具链大概率是对的。后面编译内核模块时,我们不再依赖环境变量猜来猜去,直接把参数写进Makefile外部传参即可。
我用的编译命令一般是:
bash复制export ARCH=loongarch
export CROSS_COMPILE=loongarch64-linux-gnu-
make -C /path/to/loongson-kernel M=$(pwd) modules
这里-C后面必须是龙芯K目标机上同一版本的内核源码目录,而不是随便一个Linux内核源码包。因为VLLX驱动模块要跟目标内核的符号版本保持一致,如果内核源码版本对不上,模块可以编出来,但insmod的时候几乎一定会报版本不匹配。
2.2 KDIR指向目标机器内核,避免“模块格式无效”
交叉编译模块最容易踩的坑就是把PC本机的/lib/modules/$(uname -r)/build当成内核目录。这么编出来的模块虽然指令集可能是LoongArch,但内核版本、配置文件、导出符号的CRC校验值全都对不上,拿到龙芯K上执行insmod会直接报“Invalid module format”。
正确做法有两类:
第一类,在龙芯K开发板上直接放一份同版本的内核源码,并在板上编译模块。这种方式不涉及交叉编译,模块格式几乎不会出问题,缺点是板子上编译速度慢、需要提前安装好内核头文件。
第二类,在PC上做交叉编译,但KDIR明确指向和目标板完全一致的内核源码目录。我们采用这种方式时写了一个简单的Makefile:
makefile复制obj-m := vllx_host.o
KDIR := /work/loongson-kernel
CROSS := loongarch64-linux-gnu-
all:
$(MAKE) ARCH=loongarch CROSS_COMPILE=$(CROSS) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) ARCH=loongarch CROSS_COMPILE=$(CROSS) -C $(KDIR) M=$(PWD) clean
注意KDIR必须指向已经配置过、并且包含Module.symvers和编译产物.o文件的内核目录。如果龙芯K板子运行的内核是厂家预编译的,最好重新完整编译一次这个内核,并确保模块目录里生成的modules.order和板子上uname -r一致。
2.3 最小驱动模块当“试车台”
在做真正的VLLX驱动之前,我们先写了一个只有几十行的最小模块,名字叫vllx_demo,目的是验证工具链、内核目录和加载流程是否畅通。
c复制#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init vllx_demo_init(void)
{
pr_info("vllx_demo: loaded on loongarch64\n");
return 0;
}
static void __exit vllx_demo_exit(void)
{
pr_info("vllx_demo: unloaded\n");
}
module_init(vllx_demo_init);
module_exit(vllx_demo_exit);
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("VLLX minimal driver test");
把这个文件放进去再把Makefile里的obj-m改成vllx_demo.o,编译后得到一个vllx_demo.ko。拷贝到龙芯K板上执行:
bash复制insmod vllx_demo.ko
dmesg | tail -20
rmmod vllx_demo
如果能看到loaded on loongarch64和unloaded,说明工具链与内核源码环境已经通了。后面改VLLX正式驱动时,可以随时回退到这个最小模块来确认问题到底出在编译环境还是驱动代码本身。
一个小提醒:模块里的MODULE_LICENSE("GPL v2")不是可有可无的。如果原VLLX驱动使用了一些未被EXPORT到非GPL模块的内核符号,许可证声明写得不对会直接编译报错,说符号不可用。
3. VLLX驱动移植真正要改的代码:总线探测、寄存器、DMA、中断
3.1 先把总线模型改对:PCI驱动还是平台驱动
VLLX原驱动如果在大系统上是PCIe设备,驱动结构通常长这样:
c复制static struct pci_driver vllx_pci_driver = {
.name = "vllx",
.id_table = vllx_pci_id_table,
.probe = vllx_pci_probe,
.remove = vllx_pci_remove,
};
这种驱动在x86平台上很自然,因为PCI子系统会主动扫描总线,找到匹配的vendor/device ID就调用probe。但到了龙芯K的某些开发板上,VLLX外设并不是作为标准PCIe设备被枚举的,它可能挂在低速局部总线上,也可能作为设备树节点出现。这时候继续用pci_driver是不会有任何反应的。
我们实际采取的参考做法是:先看龙芯K的官方设备树里有没有一个compatible = "vendor,vllx"的节点。如果存在,就把驱动改写成platform_driver:
c复制static const struct of_device_id vllx_of_match[] = {
{ .compatible = "vendor,vllx", },
{},
};
static struct platform_driver vllx_platform_driver = {
.probe = vllx_probe,
.remove = vllx_remove,
.driver = {
.name = "vllx",
.of_match_table = vllx_of_match,
},
};
module_platform_driver(vllx_platform_driver);
在这个场景下,寄存器地址不再从一个struct pci_dev的BAR区读取,而是通过platform_get_resource和devm_ioremap_resource获取。很多VLLX原代码里类似pci_resource_start(pdev, 0)的写法全部要同步改掉。
3.2 把地址访问统一封装成readl/writel
驱动里对寄存器的读写是最危险的地方。原开发者如果图快,可能直接写这样的代码:
c复制volatile u32 *ctrl = (volatile u32 *)0xfe00a000;
*ctrl = 0x1; // 错误示范
这在x86上可能能跑,不代表在龙芯K上也能跑。先不说物理地址转换成虚拟地址的问题,单说CPU对Device Memory的访问方式就可能不一样。正确做法是先通过ioremap建立映射,然后统一用readl/writel访问。
如果使用平台驱动框架,最简洁的方式是:
c复制struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
void __iomem *base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(base)) {
return PTR_ERR(base);
}
writel(0x1, base + VLLX_CTRL_OFFSET);
u32 status = readl(base + VLLX_STATUS_OFFSET);
为什么要用readl和writel而不是自己定义宏?因为它们不仅做了指针解引用,还在编译器层面限制了优化顺序,并且在不同架构上会插入必要的内存屏障。比如writel在部分平台上写完后要确保总线事务已经提交,你在代码里看到的是寄存器写成功了,实际上外设可能还没收到信号。
有一个经验:VLLX原代码里如果出现大量iowrite32(reg, base + offset)而参数顺序跟writel相反,移植时不要一个个去“翻译”,最好定义一个中间层,把所有寄存器操作统一转换为如下形式:
c复制static inline void vllx_reg_write(void __iomem *base, u32 off, u32 val)
{
writel(val, base + off);
}
这样至少能保证后面审计时不会漏掉某个寄存器写漏屏障。
3.3 DMA缓冲区分配与缓存一致性处理
VLLX这个外设既然要做数据搬运,就离不开DMA。龙芯K平台和x86平台最大的差异之一体现在缓存一致性和DMA地址的获取方式上。
原来的驱动里如果写过类似__pa(buffer)或virt_to_phys这种代码,请立即更换成Linux标准DMA接口。在x86上,很多地址转换因为IOMMU缺省没有启用,可以侥幸用物理地址直通,但龙芯K平台的内存映射并不保证一块直接用kmalloc分配的内存在物理上是连续的,也不能保证CPU缓存一定不会把DMA写入的数据“藏起来”。
我们公共代码里主要用了三类接口:
用于固定缓冲区分配,使用dma_alloc_coherent:
c复制dma_addr_t dma_handle;
void *cpu_addr;
cpu_addr = dma_alloc_coherent(dev, buf_size, &dma_handle, GFP_KERNEL);
if (!cpu_addr) {
dev_err(dev, "dma_alloc_coherent failed\n");
return -ENOMEM;
}
这个接口分配的内存,在CPU和外设访问之间是确保一致性的,不需要手动做缓存clean或invalid。适合DMA环形描述符这种 CPU 和外设都会频繁访问的内存。
如果只是临时把一块数据交给设备去读或写,用dma_map_single更合适:
c复制dma_addr_t bus_addr = dma_map_single(dev, buffer, len, DMA_FROM_DEVICE);
if (dma_mapping_error(dev, bus_addr)) {
dev_err(dev, "dma_map_single failed\n");
return -EIO;
}
// 触发VLLX硬件,在这里搬运数据
dma_unmap_single(dev, bus_addr, len, DMA_FROM_DEVICE);
方向参数要特别小心。VLLX驱动如果是从外设往内存搬运采集数据,方向必须是DMA_FROM_DEVICE;如果是CPU把数据下发到外设,方向就是DMA_TO_DEVICE。如果代码里两个方向搞混,驱动往往不是立刻崩,而是跑几百帧数据后出现偶发错误。
3.4 中断处理的调整:从request_irq到中断线程化
VLLX原始驱动如果工作量大,中断底半部可能用的是tasklet,而任务队列和硬中断处理在龙芯K上并没有本质差别,真正需要改的是中断号和触发方式的获取。
PCI驱动可以直接用pdev->irq,平台驱动则通常要这样拿:
c复制int irq = platform_get_irq(pdev, 0);
if (irq < 0) {
return irq;
}
如果原驱动里写死了某个中断号,比如request_irq(11, ...),到龙芯K上一定要改成动态获取。不同板子的中断路由不同,写死中断号等于自找麻烦。
我们最后采用了request_threaded_irq,把耗时操作放到线程上下文:
c复制ret = request_threaded_irq(irq, vllx_irq_handler, vllx_irq_thread_fn,
IRQF_TRIGGER_HIGH, "vllx", dev);
vllx_irq_handler里面只做最必要的寄存器读取,清中断标记,然后返回IRQ_WAKE_THREAD;真正耗时的是vllx_irq_thread_fn,比如把数据交给上层协议栈。这样能避免在硬中断上下文做太多工作导致系统卡顿。
还有一点,龙芯K平台上某些中断控制器对触发方式很敏感,如果注册中断时没有指定IRQF_TRIGGER_*,硬件可能出现中断丢失或中断风暴。这时候要从设备树里确认触发标号,比如设备树写的是interrupts = <&intc 4 IRQ_TYPE_LEVEL_HIGH>,那代码里就注册成IRQF_TRIGGER_HIGH,不要图省事填0。
4. 走马观碑组实测排坑:龙芯K上VLLX调试实录
4.1 insmod成功但probe不执行,问题出在设备树匹配
我们第一次把VLLX模块编出来加载到龙芯K系统时,insmod返回0,dmesg里只看到模块加载成功,但probe函数从头到尾没有被调用。刚开始怀疑是平台驱动注册函数写得不对,后来又猜是不是模块加载顺序问题,反复折腾了两个小时。
后来用ls /sys/devices/platform/vllx*看了一眼,发现根本没有对应设备节点。再看设备树,里面压根没有VLLX的节点,当然不会触发platform_driver的probe。
这时有两种办法。如果VLLX硬件确实挂在局部总线上,就需要修改设备树,在对应总线节点下增加:
dts复制vllx@1c000000 {
compatible = "vendor,vllx";
reg = <0x0 0x1c000000 0x0 0x1000>;
interrupts = <4 IRQ_TYPE_LEVEL_HIGH>;
};
改完设备树后重新编译,刷新到开发板启动分区。如果不想频繁刷机,也可以先用/sys/bus/platform/devices或device_create_file做临时验证,但正式交付时还是要让设备树齐全。
这里踩坑的深层原因是,PCI时代设备是“可枚举”的,而平台设备是“被描述”的。你驱动写得再好,设备树里没有节点,内核就不会知道有硬件存在。只要记住这一条,之后遇到类似问题会节省很多时间。
4.2 probe执行了但寄存器读回全是0xFF
VLLX驱动probe在龙芯K上终于执行后,我们立刻尝试读取版本寄存器,本来预期能读到某个魔数,结果读回来的数值是0xFFFFFFFF。同事第一句话是“地址错了吧”,第二句话是“FPGA没起来”。
这两个原因都很常见。先排除地址是否越界,再去排查电源和时钟。VLLX如果由FPGA逻辑实现,那么FPGA固件必须在Linux启动前加载完毕,否则映射出来的寄存器地址区域读回全是1。我们在板子上手动触发一次FPGA重配置后,寄存器就能读到正常值了。
另外还有一种可能:base地址映射的窗口大小不对。在设备树里如果用reg = <0x0 0x1c000000 0x0 0x1000>,而VLLX内部寄存器偏移地址超过0x1000,访问到窗品之外就会返回0xFF或触发异常。所以一定要仔细对照寄存器手册,确认BAR窗口覆盖了所有要访问的偏移量。
4.3 数据传输偶发错帧,DMA方向没写对
寄存器通了、中断也正常触发了,VLLX开始搬运数据之后,我们发现一个很恼火的问题:大块数据传100帧,大概有2到3帧内容是错的,但硬件又没有报错。一开始怀疑硬件时序不稳,后来用逻辑分析仪对比原始信号,发现硬件发送的数据本身没错,问题出在CPU读到DMA缓冲区时数据不同步。
原驱动在数据到达后直接用CPU遍历缓冲区,却少了一次DMA同步。正确逻辑应当是:在触发外设开始DMA之前,调用一次dma_sync_single_for_device;在DMA完成后,CPU读取数据前调用一次dma_sync_single_for_cpu。如果缓冲区是每次新分配的,也可以直接调用dma_map_single和dma_unmap_single完成隐式同步。
那时把代码改成这个样子就恢复了稳定:
c复制dma_sync_single_for_cpu(dev, dma_addr, len, DMA_FROM_DEVICE);
// 现在可以安全读取VLLX DMA缓冲区里的数据
process_data(buffer, len);
dma_sync_single_for_device(dev, dma_addr, len, DMA_FROM_DEVICE);
但如果你用的是dma_alloc_coherent,就不需要这些同步操作,因为这块内存在硬件和软件之间是分配时约定好一致性的。混用两套API才是问题根源。
4.4 中断风暴直接把系统卡成“PPT”
VLLX在跑大数据量时,系统响应突然变得极其缓慢,串口敲一个命令要等半天。用top看CPU占用不高,但中断相关的进程栈频繁触发。cat /proc/interrupts里面VLLX对应的中断计数在飞速增长,说明驱动陷入了“中断-处理-又中断”的死循环。
检查后发现是中断处理函数只做了数据搬运,却没有准确清除硬件中断源寄存器。VLLX的中断状态寄存器里可能有多个中断标志位,如果只清除一个,而设备只要有任何其它标志未清除就会持续拉高中断线,系统就会被无限中断淹没。
解决办法是在处理函数开头把中断状态寄存器完整读回来,记录下来,然后在处理结束时写入状态寄存器中所有已经被读取到的位来清中断。要注意有些硬件清除中断的方式是“写1清除”,有些则是写0,不能照抄其它驱动的写法。
5. VLLX驱动移植验收清单与交付前的个人经验
5.1 一张能直接拿去自测的验收表
驱动移植完成不等于可以交付。至少在龙芯K上把下面这些项目逐条过一遍,才算“基本能看”。
| 验收项 | 验证方法 | 常见失败信号 |
|---|---|---|
| 模块加载卸载 | 连续insmod/rmmod执行20次 | 第二次加载失败,说明资源没有释放干净 |
| 设备匹配 | 执行probe并看到打印,/sys/bus/platform/devices下出现设备目录 |
模块加载成功但没有probe日志 |
| 寄存器读写 | 读取VLLX版本号寄存器,回读预期值 | 读回0xFFFFFFFF |
| 中断触发 | 人为产生一次硬件事件,观察/proc/interrupts计数增加 |
计数不增加或增加后系统卡顿 |
| DMA传输 | 连续传输1000帧,校验内容一致 | 中间偶发错帧、超时 |
| 热拔插或复位 | 系统重启后自动加载,dmesg无错误 |
依赖手动加载,开机不能自动工作 |
| 长时间运行 | 压测8小时,观察内存、中断、DMA计数 | 内存只增不减,中断溢出 |
| 与上层对接 | 用实际业务工具跑完整流程 | 应用层读不到数据或返回段错误 |
注意里面对“模块加载卸载连续20次”这个要求,不是制造焦虑。很多驱动在第一次加载时能正常申请中断、注册字符设备,但在卸载时漏掉了一个free_irq或dma_free_coherent,第二次加载就会在insmod阶段直接崩溃或者分配资源失败。走马观碑组每次提交前都会做这种重复动作,这不难,但特别见功力。
5.2 关于长时间压测,真实体会
VLLX驱动在龙芯K上跑通单次功能后,我们进行了8小时压力测试。第一个版本跑到第三个多小时,内存占用比初始值多了几十MB,排查后确认是DMA缓冲区在每次数据传输后只被重新映射,却没有在错误路径里释放。这属于典型的错误路径资源泄漏。
这个问题在普通功能测试里根本不会发现,因为正常路径会释放缓冲区,只有硬件超时或DMA映射失败时才走到泄漏分支。后来我们把所有错误分支都整理了一遍,尤其在vllx_irq_thread_fn里如果遇到VLLX硬件状态异常,必须像正常路径一样调用dma_unmap_single,才彻底解决内存增长问题。
压测期间我们还同时监控了中断号是否溢出。龙芯K上某些中断控制器对中断计数有上限,如果中断频率过高且驱动处理太慢,中断号可能被重复分配或触发丢失。用cat /proc/interrupts间隔两小时对比,能直观看到是否有异常增长。
5.3 别把驱动写死成“只认龙芯K”
这次VLLX驱动移植过程中,我们始终保留了一个公共硬件抽象层文件,原本驱动里所有与平台相关的访问都尽量收拢到vllx_reg_read、vllx_reg_write、vllx_dma_alloc这几个函数后面。这样有个好处——以后再从龙芯K换到其它平台时,不需要再把散落在各个文件里的readl和dma_map_single翻出来逐行改。
驱动代码里最好用#ifdef区分架构敏感区域,但不要把所有代码都塞进#ifdef CONFIG_LOONGARCH里。更合理的方式是保持控制流不变,把差异隔离在几个static inline函数中。比如对VLLX来说,它内部寄存器布局在哪个平台都一样,那我们共享的就是寄存器偏移宏和状态机;真正能变的只有获得寄存器基地址、申请中断、做DMA映射这几个小函数。
实际做的时候,很多驱动代码里写满了arch/arm或arch/x86相关条件编译,看着头大。我个人的习惯是宁可在公共层多写两个函数,也不允许驱动核心代码里到处飘着平台判断。这样的代码不仅好移植,也好审查。等你在龙芯K上调试到凌晨三点,再回头看这个决定,会觉得当时那半小时抽象功夫非常划算。
最后再分享一个很小的经验:驱动移植做完了,记得在源码根目录留一份“如何在龙芯K上编译本模块”的README,把工具链路径、KDIR位置、内核版本号写清楚。不要高看三个月后的自己,那时候真不一定记得住现在是怎么交叉编译的。走马观碑组这次就是靠着这份笔记,在换了一块新修订板子以后,只花半天就把环境重新搭好,VLLX驱动重新跑起来了。移植本身有终点,但把经验沉淀下来的过程没有终点,这才是团队做这类任务最有价值的部分。
