干驱动移植这行,最怕听到的就是“保进度”三个字,但真正让人成长的,往往是那些不熟悉的平台和说不清的硬件差异。我们“走马观碑组”接手的“秦”项目,核心任务是把一套原本跑在ARM Linux上的MPU(Memory Protection Unit,内存保护单元)驱动,完整迁移到国产龙芯LS2K1000平台上。第一次拿到这块板子和一堆协议文档的时候,说实话心里没底,但走完整个流程,最大的感受是:国产化平台没有想象中那么难,难的是对硬件细节的敬畏和耐心。这篇文章就把这次移植的完整思路、踩过的坑、验证方法原原本本写出来,希望对同样在做龙芯、飞腾、兆芯这类平台外设驱动适配的同行有点用。
1. 项目背景与整体设计思路
1.1 龙芯2K1000与MPU模块的来龙去脉
龙芯2K1000是龙芯中科推出的一款工业级双核处理器,核心基于MIPS指令集架构,主频最高1GHz,片内集成了显示控制器、DDR3内存控制器、GMAC网络控制器、USB、PCIe等常用接口。它最大的特点是“接口齐全、工控友好”,在很多电力、交通、轨交AFC项目中都能看到它的身影。我们这次用的板卡就是一颗LS2K1000,外加一片国产自研的MPU内存保护协处理器。
这里说的MPU,不是某些MCU片内的内存保护单元,而是一块板级外设芯片。它的作用是给主CPU访问的外部内存区域做权限控制和越界监控:当DMA、GPU或者其他总线主机访问到已经被MPU标记为受保护的地址范围时,MPU会拦截访问并产生一个中断信号上报给CPU,同时记录下发生违规的源地址和错误类型。简单说,它像给内存区域装了一圈“电子围栏”。这套机制在军工、金融、轨交这类对数据安全有要求的场景里很常见。
原有的驱动跑在ARM平台上,用的是通用的Linux platform驱动框架。由于项目需要将整块板卡国产化替代,硬件平台从ARM换到了龙芯MPU,而外设MPU芯片没有变,主控SoC变了,挂在SoC上的总线接口也从ARM内存映射空间换到了龙芯的Local Bus空间。因此,驱动的移植本质上不是把代码复制过来就行,而是要让驱动适应新平台的地址映射方式、中断控制器行为、总线时序特征,以及Linux内核版本差异。
1.2 这次移植要解决什么问题
很多非内核开发者会问:驱动不都是Linux统一的吗,换个CPU改改编译试试不就行了?实际上没那么轻巧。虽然Linux抽象了大部分硬件差异,但平台相关的部分永远绕不开。我们这次遇到的难点可以拆成三层来讲。
第一层是地址映射。ARM平台下,外设寄存器通常通过固定的物理地址映射到内核虚拟地址空间,驱动里一个ioremap就能搞定。到了龙芯MIPS架构,地址空间划分和ARM完全不同,虽然Linux驱动最终用的也是ioremap,但物理地址的来源、总线窗口的配置方式都需要重新核对。稍不注意,驱动读到的就是一片空白或者错误数据。
第二层是中断。ARM的GIC中断控制器和龙芯的中断控制器在中断号分配、触发方式、中断共享行为上差异很大。MPU模块虽然只有一路中断输出,但在ARM上挂的是GPIO中断还是SPI中断,到了龙芯上要挂到哪个中断引脚、怎么申请IRQ号,这些都要详细查板级原理图。
第三层是时序和缓存一致性。MIPS架构的Cache特性和ARM不太一样,对外设寄存器访问一般要通过ioremap得到的非缓存地址来做,但涉及DMA缓冲区、内存保护表项更新时,如果没有刷Cache或者加内存屏障,轻则驱动工作不稳定,重则直接死机。这些坑,只有跑起来才能发现。
1.3 移植路线的三种方案与最终选择
启动之前,我们内部讨论过三条路线。
第一条是“最小改动路线”,直接用原版的ARM驱动,把里面的平台相关宏换成龙芯的,比如修改地址、中断号,然后重新交叉编译。这个方案在理论上可行,但实际执行时很容易踩到架构相关的代码,比如ARM的wmb()、mb()屏障在MIPS上语义相同但编译器处理可能不同,如果原驱动里有readl/writel还好,如果用了__raw_readl就比较危险。
第二条是“重新设计驱动骨架”,完全按照龙芯平台的标准来写一套新驱动,移植只保留MPU芯片的操作时序和状态机。这样最干净,但工作量大,需要重新验证的功能点太多,项目周期不允许。
第三条是“以原驱动逻辑为主体,做平台适配层”,也就是把地址获取、中断申请、寄存器访问这些操作封装成小函数,区分ARM和MIPS两套实现,再通过#ifdef和devicetree驱动数据来隔离差异。我们最终选了第三条,因为MPU芯片本身的操作逻辑是跨平台通用的,真正的差异只集中在少数几个接口点上。把这个思路落实到代码里,后续维护起来也轻松得多。
2. 动手之前先把两个平台的差异摸透
2.1 寄存器寻址和地址映射的差异
ARM平台下,外设寄存器一般被编址在高位物理地址区间,比如0x10000000以上,很多SoC的片上外设都在0xF0000000附近。驱动代码里常见的写法是:
static void __iomem *base; base = ioremap(MPU_PHYS_BASE, MPU_REG_SIZE); u32 val = readl(base + MPU_CTRL_OFFSET);这套逻辑在龙芯上同样适用,但关键问题是MPU_PHYS_BASE从哪来。龙芯LS2K1000内部有一个Local Bus控制器,它负责把总线上挂载的外部设备映射到处理器的物理地址空间。这个窗口的基地址、大小,需要通过Local Bus控制寄存器进行设置。在整套板卡上,MPU芯片就是挂在Local Bus片选0上,对应的物理基地址在原理图上写的是0x10000000(这个值是实际板卡配置出来的,不代表所有板子都一样)。原ARM驱动里写死的是0x30000000,这个必须改。
改动并不只是替换一个宏。地址映射时还要考虑地址总线宽度。原ARM设备是32位地址总线,MPU芯片的寄存器偏移最小步进是4字节,但在龙芯Local Bus上,如果片选配置了“地址不按4字节对齐”,读取时就会出现地址错位。后来我们重新配置了Local Bus的地址掩码寄存器,把片选0的地址模式设成按4字节对齐,这才确保base + offset的计算结果和芯片内部寄存器偏移一致。
2.2 中断和异常处理模型
ARM平台如果用GIC,驱动申请中断通常写:
irq = platform_get_irq(pdev, 0); ret = request_irq(irq, mpu_isr, IRQF_TRIGGER_RISING, "mpu", priv);龙芯平台的中断控制器虽然Linux内核已经封装好了,但中断号的获取方式略有差异。在设备树中,中断源不仅要有interrupts属性,还要指定中断控制器phandle。更重要的是,龙芯的一些中断控制器默认不支持电平触发,或者需要先把对应的中断使能位打开。如果原驱动使用共享中断标志IRQF_SHARED,在龙芯上申请共享中断时,要求驱动在中断处理函数里检查设备是否真的产生了中断,否则会死循环。MPU芯片只有一个中断脚,中断状态寄存器只有低两位有效,所以我们在ISR里第一件事就是读状态寄存器,确认bit0的故障标志置位了才继续处理。
还有一个隐藏问题:MIPS处理器在中断返回时会自动恢复interrupt enable状态,但如果ISR里访问MPU寄存器耗时过长,会导致中断堆积。尤其当MPU检测到连续多次违例访问时,芯片会一直拉高中断线,如果驱动不在ISR里把芯片的中断源使能位清掉,就会反复进中断。这个行为ARM和龙芯是类似的,但表现更剧烈,后来我们改为在ISR里彻底关掉MPU中断源,然后通过tasklet_work做后续处理,才稳定下来。
2.3 总线接口与时序参数
MPU芯片在ARM板卡上走的是SoC的静态存储控制器,在龙芯板卡上走的是Local Bus。两者的物理层虽然都是并行的地址数据总线,但时序参数要求不一定一样。例如,片选到读有效的延迟、写信号脉冲宽度、数据采样点位置等,这些参数都需要在龙芯的Local Bus控制器里重新配置。
第一版我们偷懒,直接用了龙芯BSP默认的Local Bus时序,MPU芯片初始化时读ID和版本号都正常,但执行正式的配置命令时,偶尔会出现读回的状态和写入的不一致。用示波器抓了波形,发现写信号的有效宽度只有大约30ns,而MPU芯片手册要求最小值50ns。后来在设备树里给Local Bus节点增加了lb-rd-wait、lb-wr-wait等时序参数,把等待周期加上去,果然就好了。时序问题是最容易忽视的,因为寄存器读写往往前几次成功,一旦批量或者高速访问就出问题。
3. 驱动移植实操一步步说清
3.1 交叉编译环境和内核准备
龙芯LS2K1000有专门的老内核BSP,我们基于官方提供的Linux 4.19内核作为基线。交叉编译工具链用的是gcc-linaro-7.5.0-2019.12-x86_64_mips-linux-gnu,虽然版本不是最新,但配合4.19内核很稳定。如果你也在搞龙芯平台,建议优先使用官方BSP里的工具链,不要自己去下载一个最新版,否则很容易在标准库头文件、内核头文件匹配上出问题。
配置内核之前,需要确保内核已经支持Local Bus上的MTD或simple-bus设备。我们这颗MPU芯片不是MTD设备,所以直接在设备树里把它挂到了Local Bus节点下,用simple-bus兼容。编译前要打开CONFIG_OF、CONFIG_USE_OF,以及字符设备驱动需要的CONFIG_DEVKMEM。调试阶段还把CONFIG_DEVTMPFS打开,方便/dev自动创建设备节点。
编译命令不多,但有一个细节很容易踩坑。原ARM驱动使用的是arm-linux-gnueabihf-前缀,换成交叉编译链之后,必须完全清理一遍旧的编译产物,包括驱动模块的.ko和Module.symvers。如果只是简单make,很可能出现“符号版本不匹配”或者“Unknown symbol”的错误。我们吃过这个亏,浪费了半天时间,后来统一用make clean && make modules才恢复正常。
3.2 设备树节点的添加
龙芯平台在DTS里描述设备的方式和ARM类似,但要注意中断父节点的引用。以我们最终的设备树节点为例:
&localbus { status = "okay"; #address-cells = <1>; #size-cells = <1>; mpu@10000000 { compatible = "loongson,mpu-ace"; reg = <0x10000000 0x1000>; interrupts = <12 IRQ_TYPE_LEVEL_LOW>; interrupt-parent = <&icu0>; loongson,lb-cs = <0>; loongson,lb-rd-wait = <5>; loongson,lb-wr-wait = <5>; status = "okay"; }; };这里reg描述了物理基地址和映射长度,范围可以比芯片实际寄存器空间略大。interrupts里的12对应龙芯内部中断控制器的一路中断输入,具体编号务必查BSP里的中断分配表。interrupt-parent要指向正确的中断控制器节点,龙芯2K1000上通常是icu0。如果不写interrupt-parent,驱动会去中断控制器数组中找第一个,结果可能绑定到错误的中断控制器上,现象就是request_irq返回失败或者中断永远不来。
还有一个容易忽略的点:设备树中额外添加了loongson,lb-cs和等待周期参数,这是龙芯pinctrl/syscon平台特有的自定义属性。驱动代码里用of_property_read_u32读取,用来在probe阶段对Local Bus控制器做配置。如果板级硬件有其他CS占用了地址窗口,必须调整窗口大小,防止地址冲突。
3.3 驱动代码的核心适配点
MPU驱动的整体结构沿用platform driver模型。我们保留了原驱动中的协议处理模块,也就是负责解析MPU芯片内部控制块的函数,这部分完全没动。主要改动集中在mpu_probe、mpu_remove和中断申请部分。
以probe为例,新版关键代码如下:
static int mpu_probe(struct platform_device *pdev) { struct mpu_priv *priv; struct resource *res; void __iomem *base; priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); base = devm_ioremap_resource(&pdev->dev, res); if (IS_ERR(base)) return PTR_ERR(base); priv->base = base; priv->irq = platform_get_irq(pdev, 0); if (priv->irq < 0) return priv->irq; /* original ARM code: ioremap + private config */ mpu_chip_init(priv); ret = request_threaded_irq(priv->irq, NULL, mpu_thread_isr, IRQF_TRIGGER_LOW | IRQF_ONESHOT, "mpu_ace", priv); ... }注意这里用了devm_ioremap_resource而不是原来的ioremap,好处是资源管理和错误路径都被内核框架接管了,内存泄漏风险小很多。另外,我们把原来在probe里直接request_irq改成request_threaded_irq,并把中断线程化。这是因为MPU故障处理中要读取多个寄存器、记录日志、甚至向应用层发送通知,如果都在硬中断里做,会明显影响系统实时性。中断线程化之后,硬中断只做寄存器状态确认和事件标记,重活放到线程里。
寄存器读写方面,统一使用readl/writel,并且在实际访问MPU控制寄存器前加了一条rmb()。为什么要加屏障?因为MPU配置完一个保护区域后,CPU需要等待状态寄存器更新才能继续操作。在ARM上,有些SoC会隐式保证次序,但在MIPS上,外设寄存器访问和内存访问的次序不能随便假定,加屏障是最稳妥的做法。
3.4 编译、部署与加载验证
驱动编译成内核模块后,用insmod或者modprobe加载。加载前请确认设备树已经生效:看/sys/firmware/devicetree/base/localbus/mpu@10000000目录是否存在。如果不存在,检查DTS有没有编译进内核,启动时有没有打印“MPU DT node not found”之类。
第一次加载后,我们用dmesg看到了mpu_probe ok,但紧接着就遇到了中断异常。测试脚本很简单:向MPU配置一个保护区域,然后故意用devmem去访问被保护的内存地址,看MPU中断是否会被触发,同时读取MPU的故障状态寄存器。
验证命令大概是这样:
devmem 0x10000000 32 0x00010001 # 使能MPU并配置保护区域 devmem 0x20000000 32 0xdeadbeef # 故意访问受保护区域 cat /proc/interrupts | grep mpu # 查看中断计数 dmesg | grep mpu # 查看异常上报信息我特意把被保护区域设成0x20000000到0x2000FFFF一段连续空间,然后让CPU直接去访问这个地址。如果驱动移植正确,应该能看到mpu_ace: access violation from 0x20000000之类的日志。这个操作很危险,建议在测试内核里进行,或者至少先备份好数据,因为访问非法地址可能导致总线错误或者系统异常。
4. 实际踩过的坑与排查技巧
4.1 中断申请返回-EBUSY
第一次在龙芯平台上加载驱动时,request_threaded_irq一直返回-EBUSY,说明中断号已经被占用。检查设备树后,发现我们用的interrupts = <12 IRQ_TYPE_LEVEL_LOW>和板载UART的中断号冲突了。龙芯2K1000的中断号分配和ARM不完全一样,有些中断号是固定的,有些是内部共享的。
后来查了BSP中的arch/mips/loongson32/irq.c(不同内核目录名略有差异)才发现,12号中断已经分配给了某个定时器。最后把MPU设备的中断改成另一个空闲的GPIO中断,通过GPIO中断控制器统一上报,才避开冲突。这个问题的教训是:拿到板子第一件事,一定要把/proc/interrupts里的中断号列表导出来,核对一遍再定设备树。
4.2 寄存器读回全是0xFF
调试MPU ID寄存器时,无论读什么偏移地址,返回的都是0xFFFFFFFF。这个现象在ARM板上从未出现过,一度让我怀疑芯片坏了。后来排查了三个点,终于定位到问题。
第一,确认reg属性中的物理地址指向的是Local Bus片选0,并且片选0已经通过loongson,lb-cs属性正确配置。第二,确认Local Bus控制器时钟是使能的,如果时钟关闭,读回全部为高电平。第三,确认地址线没有接反。龙芯Local Bus的地址线宽度是24位,但漏掉了最重要的字节地址线LA0,导致寄存器偏移计算整体偏了4字节,读出来的自然全是错的值。
解决方法是把DTS中reg = <0x10000000 0x1000>改成首地址+0x1000,同时确保Local Bus地址线从LA2开始对应芯片的A0。这些细节只能靠对照原理图和万用表一点点确认。
4.3 配置保护区域后系统死机
这是整个项目里最惊险的坑。我们把MPU保护区域配置好,然后触发一次非法访问,原本期望驱动能上报中断,结果整个系统直接挂起,控制台没有任何输出。
排查后发现,问题出在Cache一致性上。MPU芯片负责拦截总线访问,但CPU访问某个物理地址时,可能命中了Cache,数据根本没走到Local Bus总线去,MPU自然监控不到。即使关掉Cache,MIPS平台下还存在写缓冲区(Write Buffer),写入操作可能暂存在CPU内部,不会立刻推送到外部总线。
解决办法是在驱动里对受保护的内存区域做一次整体flush_dcache_range操作,并且在访问MPU寄存器之前加上wmb()屏障。这个细节在原ARM平台上是不需要的,因为ARM的CP15可以配置成设备内存属性,ioremap默认会映射成Device还是Normal Memory要看平台配置。MIPS下则要明确设置_CACHE_UNCACHED属性。我们最终把MPU的寄存器区域映射方式从默认值改成了_CACHE_UNCACHED,同时把需要监控的内存缓冲区用dma_alloc_coherent分配,才彻底稳定。
4.4 异常上报延迟与性能调优
驱动基本稳定后,我们用压力测试工具模拟高频DMA访问,发现MPU中断上报存在较大延迟,有时候甚至快1s才打印日志。一开始怀疑是中断优先级太低,后来检查发现是ISR里每次读取MPU状态寄存器时加了太保守的延时循环。
原驱动为了等待MPU芯片内部状态机稳定,在每个寄存器操作后都加了udelay(100)。这条代码在ARM平台上碰巧可行,但在龙芯平台因为主频更高,MPU芯片的响应速度没有那么慢,实际延时1us就足够。我们把不必要的延时去掉,同时把中断处理中大量的日志输出改成ratelimited打印或只在调试模式打开,延迟大幅下降。
另外,龙芯的中断控制器支持对中断源设置优先级,我们把MPU中断优先级调到较高档位,紧急故障时能更快得到响应。修改之后,中断上报延迟从几百毫秒降到了十几微秒,满足项目指标。
4.5 排查工具与方法总结
移植期间最常用的工具是devmem和ftrace。devmem可以直接读写物理地址,非常适合验证寄存器映射。但注意devmem访问的是模拟的未缓存地址,和驱动内ioremap的行为可能不完全一致,所以只能用来确认电路连接和寄存器基本状态,不能替代驱动级测试。
内核启动参数中加ftrace=function_graphftrace_filter=mpu_*,可以跟踪到MPU驱动的调用流程,尤其是看到哪个地方卡住了。还有一个容易被忽略的工具是/proc/iomem,它能看出ioremap之后地址是否正确。我们在排查“读回全是0xFF”时,就是在/proc/iomem里发现MPU区域被某个已经在使用的驱动占用了,才导致访问冲突。
5. 一点收尾心得
5.1 移植不是重写,而是翻译
整个“秦”项目做下来,我最大的体会是:驱动移植更像翻译工作,而不是创作。MPU芯片的核心逻辑、状态机、协议解析这些跨平台通用的部分,原样保留就好。真正要翻译的是平台相关的“方言”:地址空间、中断模型、Cache策略、总线时序。只要把这几个点对着龙芯BSP逐个核对,移植工作就会变成查表和验证的体力活。
翻译也不意味着机械替换。要理解每句代码为什么会这么写。比如原驱动里的mb(),在ARM上是全屏障,到了MIPS可能需要具体区分是读屏障还是写屏障。如果不加思考直接照搬,功能上未必错,但肯定会影响性能和稳定性。
5.2 文档与硬件人员的沟通很重要
这次能比较顺利推进,离不开一个习惯:每次遇到疑似接线或芯片问题,先别急着改代码,把板级原理图、芯片手册、Local Bus控制器手册并排摊开,逐项核对地址、中断号、时序等待周期。很多“bug”其实不是代码逻辑问题,而是平台手册上的某个参数没对上。
建议在项目一开始,就把龙芯BSP里的设备树文件全部过一遍,标记出已被占用的资源。尤其是中断号和地址窗口,这两个资源最稀缺,也最容易冲突。准备好一张资源占用表,后续加任何外设驱动,都先查表再动手。
5.3 后续维护的注意事项
项目结束前,我们把驱动代码中的平台差异用宏做了隔离,在mpu_platform.h里分别定义了MPU_PHYS_BASE、MPU_IRQ_ID、MPU_CACHE_POLICY等宏,方便以后切换到其他龙芯型号或者回到ARM平台时一键切换。
另外,MPU驱动涉及安全监控功能,建议在运行时开启看门狗机制:驱动加载后,周期性读取MPU心跳寄存器,如果连续几次读不到,就触发系统告警,防止MPU芯片故障导致安全功能静默失效。我们这次就遇到过MPU芯片在长时间跑压力后自动进入休眠模式,但没有任何中断上报,最后是通过监视心跳寄存器发现问题的。
最后再分享一个小技巧:越是底层的东西,越要用最简单的例子先打通。我们第一次调通MPU驱动,只做了两件事——读ID成功、触发一次非法访问成功上报中断。这两点通了,后续所有复杂功能都有了信任基础。做驱动移植,慢慢来,反而最快。