insmod跑完的瞬间屏幕上其实没有任何多余输出。真正让我确认移植成功了的是后面敲的那条cat /dev/vllx_info——用户态程序一口气读出了设备ID和驱动版本号每个字段都正确。走马观碑组的VLLX驱动在龙芯K平台上的移植从立项到跑通刚好一个半月。这期间我们换了三种工具链、看了四天芯片手册、改了十几版设备树最后跑通的时候反而很平静就是那种终于可以睡个整觉了的感觉。这篇移植手记写给准备把Linux驱动迁移到龙芯平台的开发者。VLLX原本是我们自己一块高速数据采集卡的驱动跑在x86平台负责把采集到的图像数据通过DMA搬运到主机内存。现在要在基于龙芯2K1000的龙芯K开发板上继续用这套采集方案驱动移植就是绕不过去的一关。文章会从项目背景讲到环境搭建再到驱动里必须改的四个核心位置最后把我们踩过的坑和验证方法全部摊开。希望能帮你在自己的移植项目里少熬夜。1. 项目背景好端端的驱动为什么要搬到龙芯K上1.1 VLLX驱动是干什么的VLLX这个名字我们组里全称是Virtual Low-Latency eXtension说白了就是一套图像采集系统的内核驱动代号。它管理一块自研采集卡卡上一颗FPGA加一个图像传感器。驱动在内核里承担四件事初始化采集卡寄存器包括设置采集分辨率、帧率、触发模式建立DMA路径把采集卡内存里的图像帧持续搬运到主机内存里的环形缓冲区响应采集完成中断把最新帧的序号、时间戳和缓冲区地址通知用户态对外提供字符设备节点/dev/vllx用户态程序通过read/mmap/ioctl访问数据流。这个架构本身不复杂但它跨了PCI设备模型、DMA、中断、字符设备、内存映射这几个内核驱动里最容易出问题的地方。当初在x86上从零写它前前后后也折腾了小一个月但写完以后跑得很稳。也正因为很稳我们才隐约意识到这份稳定有一部分是x86平台替我们兜底的换到龙芯上能不能继续站稳是个未知数。1.2 为什么选中龙芯KVLLX原先跑在x86平台上主要给组内算法验证用。后来客户项目的目标运行环境明确要求换成龙芯平台算法层和用户态的库我们之前已经适配过龙芯剩下最大的不确定性就是驱动。当时摆面前的有龙芯3A系列、2K系列好几块板子最终选了基于2K1000的龙芯K开发板。理由很直白2K1000是双核1GHz带本地总线、PCIe、千兆网口VLLX图像采集的数据量它完全扛得住社区资料相对丰富遇到问题能找到人问板子功耗也不高适合客户那边的整机集成。还有一个现实原因我们组里之前的移植项目都是在2K系列这套软件栈上趟出来的内核版本的 patch、交叉编译工具链的适配、根文件系统的制作都有现成经验可以复用。驱动移植这种事最怕的就是目标平台完全陌生连个能问的人都没有。1.3 龙芯K开发板的基本形态我们手里的板子是龙芯2K1000官方评估板后来客户方案里换过自研核心板但驱动移植层面关注的东西是一样的处理器是龙芯2K1000双核GS264兼容MIPS64指令集统一编址小端模式内存板载2GB DDR3启动方式是U-Boot引导内核通过设备树描述板载硬件系统跑Loongnix龙芯官方基于Debian的发行版内核版本4.19也可以装麒麟系统的龙芯版后续的SSH、scp开发流程基本一致日常开发是宿主机Ubuntu交叉编译把内核模块拷到板子上远程加载。这里有一个很实在的建议开发板到手后别急着编译驱动先花半天把板子自带的系统备份好确认SSH、串口、网络、文件系统都正常。后面频繁insmod/rmmod、改设备树重编内核很容易把系统搞坏有备份能省下整整一天的恢复时间。2. 移植前的摸底先分清驱动里哪些代码是平台相关的2.1 平台相关代码清单拿到VLLX源码后我们没有急着开改而是花了两天把代码里所有依赖具体平台的逻辑梳理了一遍。这一步的价值在后面的整个移植过程中被反复验证驱动移植最大的坑不是代码量而是你以为通用的代码里其实藏着一堆平台假设。当时列出来的清单大致是这样设备模型x86下用的是pci_driver框架到了龙芯K要改成platform_driver因为采集卡挂在本地总线上需要由设备树告诉内核设备在哪、地址空间多大、用哪个中断地址资源x86下寄存器基地址来自PCI BAR0由BIOS负责分配龙芯K上来自设备树的reg属性probe函数里改从platform资源里取DMA缓冲原先用了pci_alloc_consistent和pci_map_single这套老接口目标平台要换成dma_alloc_coherent和dma_map_single并且要特别关注cache一致性中断机制x86下是PCIe INTx或MSI边沿触发中断号由内核分配龙芯K上要查2K1000中断控制器里对应的中断号和触发方式内存屏障x86上wmb/rmb很多时候是编译器屏障级别MIPS上是有实际作用的sync指令必须显式补上字节序两边都是小端这块不用动。这份清单后来直接成了组里其他驱动移植的参考模板。看完清单就能预判这次移植真正困难的不是代码量而是对MIPS体系这些底层机制的理解。2.2 设备树告诉内核硬件在这x86的PCI设备是内核自动枚举的驱动根本不用关心设备接在哪个地址上。龙芯K不一样采集卡挂在2K1000的本地总线接口上硬件工程师把片选信号接到了某个地址窗口。驱动就算想自己探测也未必安全所以标准做法是用设备树显式描述。我们在板级DTS里新增了一个节点localbus { vllx1c000000 { compatible zmgb,vllx; reg 0x1c000000 0x10000; interrupts 27 IRQ_TYPE_LEVEL_LOW; }; };reg表示寄存器窗口的起始物理地址和长度interrupts里的中断号必须对着2K1000芯片手册核对。我们一开始想当然填了个中断号结果设备根本不触发中断后面查了两天才定位到是中断号映射错了。这个具体的数值每个板子不一样别照抄一定要翻板级DTS里已有的中断定义做对照。2.3 probe函数的改造改驱动的第一步就是把pci_driver替换成platform_driver。看起来是大工程实际上核心逻辑基本不动只有probe函数里拿资源的方式变了static int vllx_platform_probe(struct platform_device *pdev) { struct resource *res; void __iomem *base; int irq; res platform_get_resource(pdev, IORESOURCE_MEM, 0); base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(base)) return PTR_ERR(base); irq platform_get_irq(pdev, 0); ... }pci_iomap换成了devm_ioremap_resource硬编码的中断号换成了platform_get_irq同时devm_系列的接口会在驱动卸载时自动释放资源省掉不少手动清理代码。整个函数改下来大概半小时但这一步是后面所有适配的基础probe拿不到正确的资源后面全是白搭。3. 环境搭建工具链版本和内核一致性决定成败3.1 交叉编译工具链龙芯官方社区提供2K1000的交叉编译工具链下载解压后配置几个环境变量就能用。VLLX是内核模块不是独立应用程序编译时走的是内核的构建系统所以还需要额外指定ARCH和CROSS_COMPILEexport PATH/opt/loongson/mips64el-loongson-linux-gnu/bin:$PATH export ARCHmips export CROSS_COMPILEmips64el-loongson-linux-gnu-这里有个很容易忽略的点CROSS_COMPILE的字符串要和工具链实际的前缀完全一致。我们组里有人图省事直接复制网上教程里的前缀结果工具链版本和板子内核编译工具链对不上编出来的模块加载时各种诡异行为。建议在动正式代码之前先在板子上用uname -a确认内核版本再交叉编译一个hello模块跑通加载流程确认工具链没问题再往下走。3.2 内核源码和模块编译内核模块不是独立程序它必须和运行中的内核咬合在一起。最稳妥的做法是从龙芯代码仓库拉取和板子内核同版本的源码并且把板子上/boot/config-$(uname -r)这个配置文件拷回来覆盖到源码树里的.config然后执行make ARCHmips CROSS_COMPILEmips64el-loongson-linux-gnu- modules_prepare make ARCHmips CROSS_COMPILEmips64el-loongson-linux-gnu- Mdrivers/vllx modules这里有几个翻车点提醒一下。第一别用发行版自带的linux-headers包去编外部模块版本字符串经常和正在运行的内核对不上insmod时报version magic mismatch是轻的严重点的直接Oops。第二modules_prepare这步不能省它负责生成模块编译需要的头文件和符号表。第三编译完的ko文件通过scp传到板子就行别把整个内核源码树都拷上去板子的存储空间和性能都很有限。3.3 SSH远程开发流程开发过程中的工作流是这样的宿主机Ubuntu上交叉编译scp把ko文件传到板子SSH登到板子上insmod/rmmod再用dmesg看日志。板子上如果跑的是麒麟系统SSH和scp的用法完全一样唯一的区别是麒麟默认可能不给root直接SSH需要提前配置好用户权限或者用sudo执行模块操作。还有个小细节板子没带系统或者想换个系统的时候龙芯官网可以下载Loongnix的镜像文件麒麟系统的龙芯版本也能找到对应的安装包。下载完的镜像通常要配合U-Boot的引导参数使用具体路径每个板子不太一样建议直接参照板子厂家提供的快速上手指南。4. 核心移植步骤四个不改必挂的位置4.1 IO地址映射x86上VLLX的寄存器映射走PCI BAR驱动里用pci_iomap拿到映射后的虚拟地址换到龙芯K映射来源变成了设备树的reg属性用devm_ioremap_resource返回虚拟地址。两者最终都是void __iomem *指针读写寄存器统一用readl/writel这部分API是跨架构通用的不用改。需要注意的反而是ioremap的cache属性。VLLX的寄存器访问必须是非cache的Linux的ioremap默认就是Non-Cacheable所以常规写法没问题。但如果你的设备挂在带cache属性的地址窗口寄存器读写很容易读到过期数据。排查的时候如果发现寄存器读出来偶发错误先查地址窗口的cache配置不要一上来就怀疑是设备坏了。4.2 DMA缓冲区从kzalloc换成dma_alloc_coherent第一个版本我们偷懒DMA缓冲区用kzalloc分配物理地址用virt_to_phys拿x86上跑得好好的搬到龙芯上立刻出现数据时而正确时而乱码。这个问题的根因是x86平台的DMA默认coherent硬件会维护cache一致性而2K1000这类MIPS芯片默认不具备高效的硬件一致性CPU缓存不会自动感知外设写入内存的数据。kzalloc分配的缓冲区是write-back属性设备DMA写完以后CPU读到的可能还是cache里的旧数据。正确的写法是用DMA API分配一致性内存static int vllx_setup_dma(struct vllx_dev *dev, size_t size) { dev-ring dma_alloc_coherent(dev-pdev-dev, size, dev-ring_dma, GFP_KERNEL); if (!dev-ring) return -ENOMEM; memset(dev-ring, 0, size); return 0; }dma_alloc_coherent在MIPS上要么把缓冲区映射成uncached要么在API内部处理好cache操作保证CPU和设备看到的数据一致。如果驱动里需要小粒度、频繁的DMA数据交换也可以用dma_map_single配合dma_sync_single_for_cpu、dma_sync_single_for_device来手动同步cache但同步的方向和时机都要严格控制非常容易写错没有足够把握就用一致性分配最稳。4.3 中断适配触发类型和清中断时机VLLX采集卡在x86上用PCIe中断中断控制器自动处理边沿触发ISR里只需要读数据。搬到龙芯K以后设备挂在本地总线上中断线是电平触发的问题立刻就来了ISR返回IRQ_HANDLED之后设备的中断状态寄存器没有清电平一直有效中断控制器不断上报系统很快就soft lockup。修复的核心是ISR里必须先清设备中断状态再去处理数据static irqreturn_t vllx_isr(int irq, void *dev_id) { struct vllx_dev *dev dev_id; u32 status; status readl(dev-base VLLX_IRQ_STATUS); if (!(status VLLX_IRQ_PENDING)) return IRQ_NONE; writel(status, dev-base VLLX_IRQ_CLEAR); rmb(); /* 唤醒工作队列或通知等待队列把耗时处理放到下半部 */ ... return IRQ_HANDLED; }同时把设备树里的interrupts属性显式写成IRQ_TYPE_LEVEL_LOW确保中断控制器按电平方式处理。另外如果用了request_threaded_irq要注意中断线程里不能长时间占用CPU否则其他中断也会跟着遭殃。4.4 内存屏障x86上不存在的问题x86的内存模型非常强CPU会尽量保证内存访问顺序所以很多x86驱动从来不写wmb/rmb也能碰巧工作。到了MIPS上CPU和设备之间的交互顺序必须显式声明。MIPS的wmb/rmb会展开成sync指令作用是保证之前的内存访问先于之后的内存访问完成。VLLX驱动里需要加屏障的位置有两处一是在DMA描述符写完之后、触发设备读取之前用wmb()二是在ISR里读完成状态之后、访问DMA缓冲区数据之前用rmb()。漏掉第一个设备可能读到半写状态的描述符漏掉第二个CPU可能读到旧的缓冲区数据。这两个API是内核提供的直接调用即可不需要自己去写平台相关的汇编。这也再次说明驱动移植的时候能编译过和能跑对是两回事很多问题只有在特定平台的运行时机上才会暴露。5. 踩坑实录龙芯K上我们栽过的跟头5.1 第一次insmod就报version magic不匹配第一次把编译好的vllx.ko传到板子上insmod直接弹了一行vllx: version magic 4.19.0-1-loongson-2k should be 4.19.0-1-loongson-2k排查链路并不复杂用modinfo vllx.ko查看vermagic字段再对比板子内核的version字符串发现差了一个加号。这个加号来自内核配置里的CONFIG_LOCALVERSION也就是版本号后面追加的字符串。原因是我们编译模块时用的内核源码.config和板子上运行内核的.config不一致。修复办法是把板子/boot下的config文件拷回源码树覆盖.config重新make modules_prepare再编模块。这个坑很典型几乎每个从零开始做驱动移植的人都会踩。记住一个原则模块必须和运行内核严格同源、同配置不要用发行版headers凑合。5.2 采集数据时而对时而错cache一致性排查全记录这是整个移植过程最折磨人的问题。现象是采集的图像帧数据大多数时候正常但隔一段时间就出现一帧错乱的错的字节看起来完全没有规律像极了玄学bug。我们的排查过程是第一步排除DMA地址问题。把缓冲区物理地址打出来固定采集发现错误数据的地址其实一直是同一个缓冲区没有跑偏。第二步观察错误的分布。把错误的帧头、帧尾打到日志里发现错的字节总是落在缓冲区里某个固定偏移附近那个偏移正好对应cache行的边界。到这一步才敢确定是cache一致性问题不是设备或FPGA逻辑的锅。第三步修复改用dma_alloc_coherent分配DMA缓冲区不再手动处理物理地址。这个排查过程中的关键点是控制变量。我们没有一边猜一边改而是先把现象观察透把范围收敛到最小再动手改代码。数据错乱类问题最忌讳瞎猜然后改代码验证那样一天能试出十几种猜测但永远找不到根因。5.3 中断风暴板子几秒钟失去响应那个晚上印象太深了。驱动加载正常但一旦启动采集板子几秒钟失去响应串口刷出soft lockup。排查路径是先在ISR入口加临时printk发现打印疯狂刷屏判断中断确实在持续触发再看/proc/interruptsVLLX对应中断号的计数暴涨然后不启动采集直接在用户态手动触发一次中断问题照样复现——说明和数据路径无关问题就出在中断处理本身。仔细检查代码发现ISR里只读了状态寄存器做判断却没有写清除寄存器把pending位清掉。x86上PCIe中断的中断控制器加设备行为把这个问题掩盖了。修复就是在ISR里先writel(status, VLLX_IRQ_CLEAR)清掉中断源再rmb()最后返回IRQ_HANDLED。这个坑提醒我们换平台不只是换编译目标更要重新审视驱动对中断控制器行为的所有隐式假设。x86上碰巧能用的代码到了MIPS上可能就是定时炸弹。5.4 性能劣化从30fps掉到20fps功能全通之后我们做了性能对比结果很难看。x86上VLLX能稳定跑满1080p30fps采集龙芯K上只有不到20fpsCPU占用还特别高。先用perf定位热点发现大部分时间花在驱动读寄存器上。我们的驱动处理每一帧时要操作十几个寄存器每个寄存器用一次独立的readl/writel在x86上总线事务开销不大但在MIPS这边单次IO访问的代价高得多。优化方向很明确把连续地址的寄存器组织成结构体用memcpy_fromio一次性读回减少总线事务次数DMA描述符从每帧提交一个改成批量提交多个描述符再统一触发环形缓冲区从2MB扩大到16MB并按cache line对齐分配减少cache miss。改完以后帧率回到接近30fpsCPU占用也从70%降到40%左右。性能优化没有银弹路径就是先量化、再定位、再改一次只动一个变量。6. 验证与稳定性测试怎么算真正移植完成6.1 三层验证模块层、设备层、数据层移植完成不是能insmod就算成功我们设计了三个层级的验证。模块层insmod/rmmod连续执行20次确认内存没有泄漏、内核没有warning、设备节点反复创建删除都正常。设备层/dev/vllx节点存在open/read/write/ioctl等基本调用返回预期结果或预期错误码。数据层用户态测试程序连续采集1000帧逐帧校验帧头魔数、帧序号递增、CRC校验任何一帧出错立即标记失败。6.2 压力测试和稳定性测试通过基本功能测试后还要做持续的稳定性验证。我们安排了24小时连续采集统计总帧数和丢帧率要求丢帧率低于0.1%监控/proc/interrupts的中断计数变化确认没有异常突刺采集过程中同时跑内存压力工具确认驱动在内存紧张时还能正常分配DMA缓冲区另外还模拟了设备不在位的情况确认probe函数能干净返回错误驱动卸载后不留半初始化状态。这些测试听起来繁琐但真的能筛出不少问题。我们做24小时连续采集时就抓到一个工作队列偶发未释放的bug头两个小时看不出来跑到第八个小时才复现。如果没有长时稳定性测试这种问题进了客户现场就是灾难。6.3 可复用的移植检查清单这次移植做完我们把经验总结成一张检查清单组里后面做其他驱动移植时直接照着勾检查项说明风险等级内核版本与模块版本一致用同源码同.config编译高IO资源获取方式PCI BAR vs 设备树reg高DMA分配API用dma_alloc_coherent等标准API高cache同步非coherent平台必查高中断触发类型电平/边沿是否和设备匹配中中断源清除ISR返回前必须清pending中内存屏障wmb/rmb是否齐全中字节序小端/大端确认低这张表也适用于其他看起来很小的驱动。比如之前在社区里看到有人移植MPU6050这类I2C传感器驱动到另一个平台表面上和平台无关但I2C控制器基地址、中断引脚映射、寄存器访问方式都要重新确认背后的道理和VLLX这次折腾是完全一样的。最后说句实在的这次移植真正改动的地方不多代码量加起来可能就几百行但花掉的时间八成在排查环境问题、cache一致性、中断行为这些看不见的地方。搞驱动移植最大的敌人不是代码量而是那些你以为是通用但其实隐含了平台假设的细节。后来我们组里整理文档的时候把那几次踩坑经历写成了前面那张检查清单后面的项目直接用这张表当起点确实少走了很多弯路。
