news 2026/9/10 6:21:38

Linux驱动移植到龙芯K平台:DMA、设备树与中断适配全记录

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux驱动移植到龙芯K平台:DMA、设备树与中断适配全记录

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 为什么选中龙芯K

VLLX原先跑在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 { vllx@1c000000 { 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_COMPILE:

export PATH=/opt/loongson/mips64el-loongson-linux-gnu/bin:$PATH export ARCH=mips export CROSS_COMPILE=mips64el-loongson-linux-gnu-

这里有个很容易忽略的点:CROSS_COMPILE的字符串要和工具链实际的前缀完全一致。我们组里有人图省事,直接复制网上教程里的前缀,结果工具链版本和板子内核编译工具链对不上,编出来的模块加载时各种诡异行为。建议在动正式代码之前,先在板子上用uname -a确认内核版本,再交叉编译一个hello模块跑通加载流程,确认工具链没问题再往下走。

3.2 内核源码和模块编译

内核模块不是独立程序,它必须和运行中的内核"咬合"在一起。最稳妥的做法是从龙芯代码仓库拉取和板子内核同版本的源码,并且把板子上/boot/config-$(uname -r)这个配置文件拷回来,覆盖到源码树里的.config,然后执行:

make ARCH=mips CROSS_COMPILE=mips64el-loongson-linux-gnu- modules_prepare make ARCH=mips CROSS_COMPILE=mips64el-loongson-linux-gnu- M=drivers/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/interrupts,VLLX对应中断号的计数暴涨;然后不启动采集,直接在用户态手动触发一次中断,问题照样复现——说明和数据路径无关,问题就出在中断处理本身。

仔细检查代码,发现ISR里只读了状态寄存器做判断,却没有写清除寄存器把pending位清掉。x86上PCIe中断的中断控制器加设备行为,把这个问题掩盖了。修复就是在ISR里先writel(status, VLLX_IRQ_CLEAR)清掉中断源,再rmb(),最后返回IRQ_HANDLED。这个坑提醒我们,换平台不只是换编译目标,更要重新审视驱动对中断控制器行为的所有隐式假设。x86上"碰巧能用"的代码,到了MIPS上可能就是定时炸弹。

5.4 性能劣化:从30fps掉到20fps

功能全通之后,我们做了性能对比,结果很难看。x86上VLLX能稳定跑满1080p@30fps采集,龙芯K上只有不到20fps,CPU占用还特别高。

先用perf定位热点,发现大部分时间花在驱动读寄存器上。我们的驱动处理每一帧时要操作十几个寄存器,每个寄存器用一次独立的readl/writel,在x86上总线事务开销不大,但在MIPS这边单次IO访问的代价高得多。优化方向很明确:把连续地址的寄存器组织成结构体,用memcpy_fromio一次性读回,减少总线事务次数;DMA描述符从"每帧提交一个"改成批量提交多个描述符再统一触发;环形缓冲区从2MB扩大到16MB,并按cache line对齐分配,减少cache miss。改完以后帧率回到接近30fps,CPU占用也从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一致性、中断行为这些"看不见"的地方。搞驱动移植,最大的敌人不是代码量,而是那些你以为是"通用"但其实隐含了平台假设的细节。后来我们组里整理文档的时候,把那几次踩坑经历写成了前面那张检查清单,后面的项目直接用这张表当起点,确实少走了很多弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 6:20:58

Matlab udpport UDP通信实战:字节序、事件回调与跨平台序列化

简介&#xff1a;本资源是一套基于MATLAB实现UDP协议通信的完整源码示例&#xff0c;面向通信、自动化及嵌入式方向的新手开发者与进阶工程师&#xff0c;解决网络编程中端到端报文收发的实际落地问题&#xff0c;适用于仿真测试、设备联调、传感器数据透传等典型工业与教学场景…

作者头像 李华
网站建设 2026/9/10 6:12:14

嵌入式与安卓低功耗开发实战:从芯片手册到用户工单

1. 这不是“省电小技巧”&#xff0c;而是设备工程师的生存基本功你有没有遇到过这样的场景&#xff1a;一台工业传感器节点&#xff0c;标称续航6个月&#xff0c;实测3周就彻底宕机&#xff1b;某款智能手表在实验室跑满电循环测试毫无问题&#xff0c;量产交付后用户投诉“充…

作者头像 李华