1. 项目背景与需求拆解
1.1 为什么龙芯平台需要重新移植驱动
老周大概率和我一样,是在某次国产化替代项目里被龙芯"教育"过的人。拿到的机器是龙芯k系列处理器,板卡厂商给了个底包,外设厂商甩过来一份VLLX驱动的源码包,说"你自己改改就行"。到手一看,源码基于x86的PCI枚举流程写的,中断处理走的是IOAPIC,寄存器访问用了outl/inl,整个驱动跟龙芯平台的适配层基本是两条平行线。
这就是驱动移植最典型的场景:不是你的驱动写错了,而是它假设的硬件环境不成立。龙芯处理器的架构虽然是LoongArch,但外设控制器本身大多是通用IP,真正变的是CPU侧怎么访问这些外设、怎么分发中断、怎么建立DMA映射。VLLX驱动移植的实质,就是在保留原有驱动核心算法和协议栈的前提下,把与CPU架构强相关的底层接口全部换成适配龙芯的实现。
我这次要分享的“走马观碑组VLLX驱动移植”项目,做的就是这件事:把一个原本面向x86平台的VLLX设备驱动,完整地移植到龙芯k系列处理器上,实现在LoongArch平台下的稳定运行。整个过程从环境搭建、源码分析、接口改写,到中断改造、DMA适配、稳定性调试,前后折腾了小一个月。这篇文章会把移植过程中所有能公开的核心步骤、决策逻辑、踩坑记录都整理出来,给正被同样问题磨的兄弟们一条可抄的作业。
1.2 先搞清楚VLLX到底是什么
VLLX这个命名在这类外设驱动里很常见,一般是某个专用控制器芯片的缩写代号,可能是视频链路处理、逻辑控制、或某种行业专用I/O设备。这里不对具体硬件型号展开,只把它当作"需要由CPU通过总线访问、依赖中断和DMA、有寄存器操作时序"的典型外设来看待,这也是驱动移植工作量最大的设备类型。
这类驱动在x86平台上通常依赖三个硬件基础:PCI/PCIE配置空间枚举、IOAPIC或MSI中断、I/O端口或MMIO寄存器访问。龙芯平台对应的则是:PCI/PCIE枚举逻辑不变但需适配LoongArch的PCI实现、中断控制器从IOAPIC换成了龙芯的HT中断控制器或APIC兼容模式、寄存器访问从outl/inl变成内存映射后用readl/writel。搞清楚这三个对应关系,移植任务的核心轮廓就出来了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 驱动移植前的环境准备与评估
2.1 交叉编译工具链与目标内核版本匹配
移植前第一件必须做对的事,是准备LoongArch交叉编译工具链。龙芯官方目前推荐的交叉编译器是gcc 13以上的loongarch64-linux-gnu-系列,不同内核版本对编译器版本也有要求,太老的编译器编新内核会直接报错。这里建议直接使用龙芯开源社区提供的交叉工具链压缩包,解压后配置环境变量:
bash复制export PATH=/opt/loongarch64-toolchain/bin:$PATH
export CROSS_COMPILE=loongarch64-linux-gnu-
export ARCH=loongarch
你目标板卡如果用的是SDK自带内核,那内核版本基本锁死,交叉工具链也要和SDK对齐。这里有一个非常容易踩的坑:用高版本编译器去编老内核,或者反过来,都会在链接阶段出现莫名其妙的错误,比如__divdi3符号缺失、__multi3符号未定义这类,本质就是编译器特性与内核内置库函数不匹配。遇到这类报错先别急着改代码,优先检查工具链版本。
2.2 拿到板卡现有内核的配置与设备树
龙芯平台目前的设备树使用非常普遍,大多数板卡的硬件拓扑、内存布局、中断控制器类型都是由设备树描述的。拿到板卡第一步,确认三件事:
- 内核版本是多少,用的是官方主线内核还是板卡SDK定制内核;
- 设备树源文件dts是否完整,PCIe控制器节点、中断控制器节点是否可见;
- 内核是否已经打开了CONFIG_PCI、CONFIG_PCIEPORTBUS、CONFIG_PCI_MSI等选项。
如果板卡SDK内核里PCIe控制器工作正常,能看到lspci输出,那说明基础总线枚举没问题。VLLX设备驱动移植主要工作在驱动自身,设备树层面通常不需要改,只要确认设备能够被PCI核心枚举到就行。
我实际操作中遇到过一个情况:板卡自带的dts里PCIe控制器只有一个根端口,没有把RC的ECAM空间完整描述,导致PCI设备扫描时resource分配异常,VLLX设备虽然能出现在lspci列表里,但BAR地址全是0。这就是设备树描述不全导致的经典问题,排查了一天才定位到。如果你遇到设备枚举正常但BAR读不到的情况,优先去查dts里PCIe节点的ranges属性。
2.3 评估VLLX驱动源码的可移植性
在动手改代码之前,先把源码完整读一遍,评估工作量。这里有个实用方法:用grep统计驱动源码中与架构强相关的函数调用次数,主要包括:
bash复制grep -rn "outb\|outw\|outl\|inb\|inw\|inl" vllx_driver/
grep -rn "request_irq\|free_irq\|irq_set_chip\|handle_level_irq" vllx_driver/
grep -rn "dma_alloc_coherent\|dma_map_single\|dma_map_sg" vllx_driver/
grep -rn "pci_alloc_consistent\|virt_to_phys\|ioremap" vllx_driver/
如果前两项数量多,移植工作量集中在中断和I/O访问;如果第三项多,则DMA适配是重头;如果都多,那就准备打硬仗。我经手的VLLX驱动源码属于第三种,既有大量inl/outl操作寄存器,又把中断处理绑定在MSI上,还用了多个DMA环形队列,三座大山一个没落。
这一阶段的产出应该是一份端口映射表,把源码中每个架构相关调用点列出来,标明原始实现、需要改成什么、涉及哪些函数。后面实际改代码就是照表施工,避免改到一半思路中断。
3. 核心移植过程详解
3.1 PCI枚举与BAR映射的正确处理方式
VLLX驱动在x86平台上的初始化流程一般是:pci_register_driver注册驱动,probe函数里用pci_resource_start获取BAR地址,然后ioremap映射,再用readl/writel访问寄存器。这套流程在龙芯平台上基本可以复用,唯一需要注意的地方是LoongArch下的PCI域和总线号分配可能与x86不同。
在龙芯平台上,PCIe控制器通常挂在一个域下,设备BDF号与x86的枚举结果不一定相同。如果驱动代码里硬编码了PCI_SLOT或PCI_FUNC的判,会导致设备识别失败。正确做法是全部通过pci_dev结构体获取BDF信息,不要自己做任何假设。
BAR映射部分,x86驱动里常见的ioremap(pci_resource_start(pdev, bar), size)写法在龙芯下直接可用,不需要改。但iomem资源是否被PCI核心正确分配,取决于前面的dts配置。所以这一步的检查顺序应该是:先看lspci输出确认BAR已分配,再看驱动probe里读到的pci_resource_start返回值是否正常,最后才是检查读写寄存器能否生效。
3.2 寄存器操作从I/O端口到内存映射的改写
VLLX驱动源码里如果有大量outl/inl调用,到了龙芯平台需要判断这些I/O端口访问是真实I/O地址还是PCI BAR映射的虚拟端口。绝大多数PCIe设备其实已经不支持独立的I/O空间,x86上的outl/inl大多是ACPI或遗留设备用的。PCIE设备的I/O BAR在龙芯平台默认不分配。
如果你的VLLX驱动在probe阶段访问的是PCI Configuration Space,那已经通过pci_read_config_*系列接口实现了,这部分是跨架构的,不用改。真正需要改的是bar资源访问代码,也就是把:
c复制outl(reg_val, port_base + offset);
改成:
c复制writel(reg_val, reg_base + offset);
同理inl改成readl。这里有个需要注意的细节:写入寄存器之前,必须保证先有读操作或write barrier,部分PCIe控制器对写后读的一致性有要求。LoongArch使用的是弱内存模型,CPU在执行普通内存操作时可能乱序,所以驱动初始化序列里,如果原来x86下靠IO操作天然带barrier,现在换成MMIO后需要在关键位置补充wmb()或mb()屏障。
我当时给VLLX驱动写了一套内存屏障维护清单,凡是涉及"写寄存器A,等待状态寄存器B变为X,再写寄存器C"这类操作序列,都会在写和读之间加readl_sync或mb()。改动之后,原来在x86下偶发的寄存器操作超时几乎绝迹,这也是整个移植中收益最明显的优化之一。
3.3 中断处理从IOAPIC/MSI到龙芯中断控制器的适配
这个部分是整个移植工程的硬骨头。x86平台下,PCIe设备的中断要么走INTx,通过IOAPIC路由到CPU,要么走MSI/MSI-X,直接写内存消息。龙芯平台的情况有所不同,LoongArch对中断的支持非常完整,但存在两种模式:一种是通过HT总线的中断控制器,另一种是APIC兼容模式。
VLLX驱动如果原来用了MSI中断,在龙芯平台上大概率可以继续用,但前提是内核使能了CONFIG_PCI_MSI,并且PCIe控制器的MSI支持正确配置。检测方法很简单:驱动加载后,看/sys/bus/pci/devices/0000:XX:XX.X/msi_irqs目录下有没有中断号。
在龙芯平台上,MSI中断的申请接口pci_alloc_irq_vectors和request_irq这套API是通用的,但实际分配的中断号范围和x86不同,驱动程序不应该假设中断号的含义。另外,龙芯的线程中断控制器(IRQCHIP)在处理MSI中断时,配合的亲和性设置方式也不太一样,如果驱动里调用了irq_set_affinity,需要确认传的CPU mask是合法的NUMA节点CPU。
如果VLLX设备不支持MSI,只支持INTx,移植过程会麻烦一些。龙芯平台在PCIe INTx路由上支持传统中断共享,设备树里需要确认INTx映射是否与PCIe控制器的中断父节点正确关联。我建议是:只要硬件支持,优先启用MSI,无论是从CPU中断分布效率还是驱动代码修改量来看,MSI都是更优选择。
我实际在走马观碑组这个项目里,VLLX设备同时支持MSI和INTx,但固件默认只开了INTx。驱动在x86下依赖MSI,到了龙芯板卡上一开始跑不起来,是因为设备树里pci节点没有开启msi-parent属性。补上msi-parent指向的中断控制器后,MSI功能才真正工作起来。排查过程花了整整两天,最后用cat /proc/interrupts对比发现INTx和MSI的区别才定位到。这个坑值得记。
3.4 DMA缓冲区的分配与一致性处理
DMA这一层是驱动移植中真正考验内功的地方。x86平台常见的老式驱动会用pci_alloc_consistent分配DMA缓冲区,这个API在龙芯平台的内核里依然存在,但它最终走的是dma_alloc_coherent的统一框架。这里有一个隐藏的arch特性差异:LoongArch的DMA默认是cache-coherent(硬件一致性),因此dma_alloc_coherent返回的地址和dma_addr之间的映射关系与x86一致,不需要做额外的cache维护。
但如果VLLX驱动原来的代码里用了dma_map_single、dma_unmap_single这套流式映射API,并且没有正确调用dma_sync_single_for_cpu/device,在x86的写回式cache下可能碰巧能工作,到了龙芯的硬件一致性模型下反而可能出现数据"看不到"的问题。这只是表象,实际原因是驱动对DMA操作的时序假设不成立。
我的建议是:在龙芯平台统一走dma_alloc_coherent分配DMA环形队列和描述符,不要再用pci_alloc_consistent这种旧接口。同时检查驱动里所有的dma_map_single调用,确认映射方向(DMA_TO_DEVICE/DMA_FROM_DEVICE)是否与设备实际读写行为一致。VLLX驱动的DMA环形队列就是被这个坑害惨了,x86上跑得好好的,一上龙芯就偶发数据错位,最后定位到是驱动里用DMA_FROM_DEVICE方向去映射了一个既是设备写入又是CPU读写的共享描述符区域。
3.5 现代内核API的兼容性调整
VLLX源码如果是比较老旧的项目,大概率还会用一些已经被内核移除的接口。最常见的有:
pci_alloc_consistent被dma_alloc_coherent替代;irq_set_affinity_hint在新内核中签名变化;pci_enable_msi被pci_alloc_irq_vectors替代;create_proc_entry被proc_create替代;init_timer系列被timer_setup替代。
这里最重要的一条经验是:不要为了保留老API而去给新内核打补丁。维护一套老API兼容层的成本远高于直接改驱动调用新API。新版内核在2024年开始已经彻底移除了大量旧接口,靠编译期适配宏维持兼容的做法早晚崩。
我在移植VLLX驱动时,把5.10内核API一次性适配到目标板卡使用的6.1内核,包括pci_enable_msi_range改pci_alloc_irq_vectors、timer接口改timer_setup、proc接口改proc_create。改动量不大,但需要细心把每个调用点的参数对应好。这里给一个实用技巧:直接搜索内核源码include目录中同名函数的定义,确认参数列表,再对驱动代码进行替换,不要凭记忆写。
4. 调试方法与典型问题排查实录
4.1 让printk成为最可靠的调试手段
驱动移植过程中,调试手段的优先级应当是:printk > dev_dbg > ftrace > JTAG。千万不要一上来就上大型调试工具,大部分问题用printk就能定位。
在龙芯平台上使用printk有两点特殊要求:第一是内核命令行要配console=ttyS0,115200,串口输出是最稳定的观察通道;第二是LoongArch的早期调试如果boot阶段就中断,需要加earlycon参数。VLLX驱动在probe阶段就直接panic的话,可能连串口都无法起来,这时优先用earlycon。
一个非常实用的调试技巧:在驱动probe函数的入口、BAR映射完成后、中断申请后、DMA分配后、设备启动命令发出后,分阶段打印指针和状态值。然后通过二分法确定卡在哪一步。我当时就是靠五条printk,定位到设备初始化卡在"等待固件就绪"的循环里,因为驱动等待的状态寄存器在龙芯平台上出现了总线响应超时。
4.2 总线错误与Synchronous External Abort的排查
龙芯平台上访问未正确映射的物理地址,会触发总线错误,打印信息类似Oops - Bus error。出现这个提示,基本可以断定是访问了非法的物理地址或已释放的映射空间。注意它和x86的Page Fault不同,x86下访问非法地址很大概率只是进程被杀掉,而LoongArch的总线错误有时会直接挂起整个串口,系统失去响应。
这种情况的排查思路:
- 检查pci_resource_start返回值是否为0或已经超出实际BAR范围;
- 检查ioremap返回值是否非NULL;
- 检查是否有驱动在release固件固件之后仍访问设备寄存器;
- 检查DMA buffer的物理地址是否真的在设备可访问的地址范围内。
我曾经遇到过一个很隐蔽的问题:VLLX驱动从固件A.BB.CC.DD开始启用DMA功能,固件版本从v1.2升到v1.3后DMA地址高位被改写,驱动没有重新读取新地址,导致访问到了非法区域,整个内核直接hang死。排查的最后手段是把DMA描述符的物理地址完整打了出来,才发现高位被设备固件重置了。
4.3 缓存一致性问题:数据读出来的全是旧值
这是龙芯平台移植驱动时绕不开的问题。虽然LoArch架构的DMA是cache-coherent的,但内核配置会影响这一机制的生效方式。如果重新编译内核时开了CONFIG_DMA_COHERENT_POOL或者修改了内存属性,设备的DMA访问路径就会变化。
VLLX驱动的DMA环形队列出现"CPU写入描述符后设备看不到更新"的问题,一般有两个方向排查:
- 检查是否需要对描述符内存执行dma_sync_single_for_device;
- 检查描述符内存是否用了
dma_alloc_coherent而不是kmalloc后的虚拟地址映射。
大多数老驱动移植后遇到的cache问题,都出在使用了普通kmalloc内存当DMA缓冲区。正确方案是使用dma_alloc_coherent,并且注意它返回的虚拟地址是可以直接读写的,其对应的物理地址通过dma_to_phys确认。有人图省事用virt_to_phys换算普通内存地址当DMA缓冲区,这在x86下往往碰巧能用,但到了龙芯平台几乎必然出问题,因为这个转换绕过了DMA层对内存属性的管理。
4.4 性能摸底:吞吐量和CPU占用是否达标
移植跑通只是第一步,性能达标才是交付标准。VLLX设备是个典型的高吞吐外设,驱动性能主要受两个因素制约:中断处理效率和DMA队列深度。
在龙芯平台上做性能摸底,我用的是如下方法:
bash复制# 看设备中断分布
cat /proc/interrupts | grep vllx
# 看CPU软中断占用
top -d 1 | grep si
# 查看DMA队列状态
cat /sys/kernel/debug/vllx/queue_stats
如果发现中断都打在一个CPU核上,可以考虑设置IRQ affinity进行负载均衡。LoongArch的中断亲和性写在/proc/irq/<irq_number>/smp_affinity,与x86区别不大。需要注意的是龙芯大小核架构下,E核心和S核心的中断处理能力不同,亲和性尽量设置在E核上。
另一个性能问题是DMA队列深度。VLLX驱动默认的环形队列深度如果在x86下是128,在龙芯平台上因为cacheline对齐要求不同,可能需要调整描述符大小和对齐方式,否则会出现PCIe传输性能折半的情况。这个问题的发现过程很有代表性:性能测试只有理论值的一半,各种参数都快调烂了,最后把DMA描述符的cacheline对齐从32字节改成64字节,性能立刻翻倍。
5. 常见问题速查表与避坑清单
5.1 移植高频问题一览
把这次VLLX驱动移植中遇到的高频问题整理成一张表,后续再移植其他驱动的兄弟可以直接对照排查:
| 现象 | 可能原因 | 排查命令/方法 | 解决方案 |
|---|---|---|---|
| 设备枚举不到 | dts中PCIe节点不完整 | lspci、dmesg | 补全dts的ranges、msi-parent |
| BAR地址为0 | PCIe ECAM资源未分配 | cat /sys/bus/pci/devices/.../resource | 检查dts中的PCIe控制器节点 |
| 寄存器读写触发Bus error | ioremap失败或硬件地址错误 | dmesg、printk | 确认pci_resource_start非0 |
| 中断不触发 | MSI未打开/设备树缺msi-parent | cat /proc/interrupts | 补dts属性或改用INTx |
| 数据错位/乱码 | DMA映射方向错误或cache不一致 | printk描述符关键字段 | 统一使用dma_alloc_coherent |
| 初始化时死循环等待 | 状态寄存器总线无响应 | printk等待变量值 | 确认寄存器偏移和内存屏障 |
| 性能只有一半 | DMA描述符对齐不达标 | dd或iperf测速 | cacheline按64字节对齐 |
5.2 我在移植中坚持的五条避坑纪律
第一,改动一个文件就编译一次,不要攒一堆改动再集中编译。交叉编译虽然慢,但出错排查成本更高。我见过同行一次改了十几个文件再编译,结果几百个报错根本不知道从哪定位。
第二,每条printk都加模块前缀和函数名,例如vllx_probe: BAR0 mapped at ...。事后看日志时能快速定位是哪个函数在什么阶段打印的,比满屏"info: xxx"强太多了。
第三,DMA缓冲区统一用dma_alloc_coherent分配,禁止用kmalloc代替。这条纪律我在团队里强制执行,因为它能规避至少一半以上的偶发数据问题。
第四,中断处理函数里不打印任何信息。VLLX设备中断频率一高,中断上下文里的printk会让系统直接卡死,这类问题极难排查。
第五,每次遇到不可解释的问题,先怀疑是硬件时序,再怀疑驱动逻辑。因为平台换了,时序假设变了,很多x86下固有的时序窗口在龙芯平台根本不存在,这个调整成本必须纳入计划。
6. 绕不开的几个地方:LoongArch平台独有的特性
6.1 内存模型带来的差异
LoongArch和x86最大的差异之一就是内存模型。x86有TSO(Total Store Order)内存一致性保证,程序员几乎不需要考虑内存屏障,而LoongArch采用的是弱内存模型,CPU和DMA之间的读写顺序如果没有屏障保护,就可能出现驱动代码逻辑上没问题但实际执行结果异常的情况。
VLLX驱动里曾经有一处简化写法,在x86下完全没问题:
c复制writel(desc->addr, vllx->dma_ring_base + RING_REG);
writel(desc->len, vllx->dma_ring_base + RING_LEN);
这段代码在x86下意味着先写地址再写长度,设备固件按顺序读取就能看到正确值。但到了LoongArch弱内存模型下,第二次writel可能先于第一次writel对设备可见,设备读取RING_LEN时拿到的是新长度,但读RING_REG还是旧地址,导致DMA描述符错位。
解决方案是两次写之间加wmb(),强制写操作顺序:
c复制writel(desc->addr, vllx->dma_ring_base + RING_REG);
wmb();
writel(desc->len, vllx->dma_ring_base + RING_LEN);
这种细节不实际踩坑很难意识到,但处理完之后设备DMA出错率直线下降。
6.2 固件和BIOS层面的差异
还有一个容易被忽视的点:BIOS/UEFI在不同平台上的PCI资源预分配策略不同。x86的BIOS通常会在启动阶段把PCIe设备的BAR全部初始化好,甚至MMIO地址都和Linux分配的完全一致。但龙芯平台的固件更多依赖内核来重新配置PCI资源,所以有时冷启动后第一次进入Linux,PCIe枚举时设备BAR还处于未初始化状态,或者固件分配的地址和内核期望的完全不同。
这个现象最典型的体现是:连续reset重启,或者从grub直接引导内核,VLLX设备的BAR会随机变化或为0,但进入系统后再reboot一次就正常了。如果遇到这种情况,不要怀疑驱动,先确认固件对PCIe初始化所承担的角色,必要时在内核rootwait阶段手动pci=realloc或通过pci_rescan_bus触发重新枚举。
7. 更多拓展方向:VLLX驱动后续的优化与维护
7.1 支持内核热插拔与自动加载
驱动移植完成之后,杂散设备插拔和启动时自动加载是交付过程中时间最长的部分。建议把驱动构建为.ko模块,配套一个systemd alias或modprobe配置,并确认设备匹配ID正确。对VLLX这种专用设备,在probe里做好pci_device_id匹配,避免同厂商ID不同设备型号被错误加载。
7.2 从字符设备到内核态数据面
如果VLLX驱动只是做一个简单的字符设备供用户态读写,那当前移植已经到了交付终点。但如果需要进一步追求数据路径性能,建议在内核态直接实现数据面处理,配合io_uring或AF_XDP套接字,将DMA数据直接送进网络协议栈,绕过用户态拷贝。这也是龙芯平台后续高性能外设驱动的一个通用优化方向,我在走马观碑组的下一步规划就是把这套VLLX驱动从字符设备模式升级到内核态数据面模式。
7.3 与龙芯SDK版本的长期耦合管理
龙芯SDK更新频繁,内核版本升级后部分驱动API会有变化。如果一个驱动由于历史原因停在5.10,后续升级到6.1又要重做适配,那前期的移植工作就浪费了。建议在驱动编写之初就尽量避免使用老接口,跟进内核主流API,降低未来升级成本。这也是做国产化平台驱动的长期经验——平台本身还在快速演进,驱动的可移植性设计要比业务功能更优先。
