多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Linux内核模块展示MediaTek AIoT SoC能力实操指南

Linux内核模块展示MediaTek AIoT SoC能力实操指南 手里的这块板子一到手我就知道接下来的几周都要泡在串口和交叉编译环境里了。这几年做嵌入式Linux接触过不少海外芯片平台的BSP但专门为了给新一代AIoT SoC做Linux驱动模块展示还是头一回。MediaTek这一波AIoT SoC主打的不是单纯堆算力而是把AI加速单元、低功耗多核CPU和丰富的连接外设打包在一起面向智能家居、工业边缘、零售终端这类场景。而把这些特性真正“露出来”给客户看的不是SoC本身而是跑在它上面的Linux驱动模块——毕竟客户拿到的是一套软硬一体的方案不是一颗光秃秃的芯片。这篇文章就围绕“用Linux驱动模块展示MediaTek AIoT SoC”这件事把我从环境搭建、内核配置、设备树编写、驱动框架选型到调试排障的完整过程捋一遍。内容偏实操适合正在做嵌入式Linux驱动开发、或者准备在新SoC平台上做方案验证的同学参考。文章里会涉及工具链选择、模块编译的Makefile写法、常见的内核模块加载失败原因以及一些我从实际调试里磨出来的经验。别小看这些细节很多时候方案演示能不能顺利跑起来差的往往就是这最后一步。1. 项目整体定位与设计拆解1.1 MediaTek AIoT SoC的核心差异化在哪先聊为什么选MediaTek的AIoT SoC来做这次驱动模块展示。业内做AIoT的芯片平台不少但MediaTek这一代SoC的定位很有意思它不追求顶级峰值算力而是强调“在合理功耗下把AI推理、视频编解码、外围接口整合成一套完整方案”。这就意味着上层业务跑什么模型、接什么传感器底层驱动就得相应地把NPU、ISP、I2C、SPI、UART、GPIO这些资源全部打通。我之前在别的平台上调驱动最大的痛点是外设驱动模块化程度低牵一发动全身。而这次MediaTek SoC配套的Linux BSP里设备树Device Tree覆盖已经很完整外设驱动也基本是标准Linux框架像platform driver、i2c driver、spi driver这些都能直接套用。所以“展示SoC能力”这件事本质上不再是到寄存器级别去写驱动而是把这些标准框架用起来再针对AIoT场景做定制外设的驱动扩展。标题里有个关键词是“Linux-Driven Modules”通俗讲就是一系列以内核模块形式存在的驱动程序。它们的作用是让上层应用能控制SoC的各个外设也让客户在评估板上能直观地看到“这个SoC能接什么、能算多快、能跑多稳”。选择模块化而不是全编进内核一是方便迭代二是方便按场景裁剪三是商业交付时既能保密源码又能分发预编译模块。1.2 模块化思路解读为什么非要用内核模块我记得刚入行那会觉得把驱动直接编进内核镜像一了百了启动就加载干嘛还要费劲编成 .ko后来被现实教育了。AIoT项目往往不是一锤子买卖硬件版本会有迭代外设方案会换如果所有驱动都编进内核每次改动都要重新编译整个内核镜像出问题还不好定位。内核模块则可以单独加载、卸载、调试甚至可以在不重启设备的情况下更换驱动逻辑。这次项目里我把SoC的外设能力拆成几个独立模块来展示核心展示模块负责SoC基础信息、CPU频率、温度读取等AI加速模块封装NPU推理的字符设备接口外设交互模块涵盖GPIO、I2C、PWM等外设的子驱动数据采集模块挂接在SPI/I2C总线上的传感器驱动每一类模块都对应一组真实可演示的场景。比如客户想看AI识别速度就加载NPU模块跑一个分类模型想看多路传感器采集就加载相应的总线和传感器驱动直接查看实时数据流。这样整套方案不再是PPT上的框图而是能动手体验的“活Demo”。这么做还有一个好处模块加载的顺序和组合方式本身就反映了SoC的资源调度逻辑。同一套内核既能支持最低配的传感器控制应用也能支持最高配的视频AI分析应用展示了Linux模块化的灵活性。1.3 方案选型时踩过的几个思考点选型阶段其实纠结过一阵是用内核态驱动还是用户态驱动对于NPU这类高数据吞吐设备内核态驱动更稳妥因为它能更好的管理中断、DMA、内存连续分配而且跟MediaTek提供的runtime库对接也更顺畅。对于简单的GPIO点灯用户态操作都不是大问题但为了统一Demo流程我还是尽量走内核模块再用udev规则自动创建设备节点。另一个思考点是设备树的使用方式。BSP里默认设备树已经定义好了大部分外设资源比如NPU的运行基地址、中断号、时钟信息。我在做展示模块时尽量复用这些默认节点只在必要时新增自定义子节点。这样做的好处是后续官方BSP更新时我的设备树改动可以最小化。一句话总结设计思路用最小侵入方式把SoC能力暴露给上层用可插拔方式组织驱动模块用标准Linux框架保持可维护性。2. 开发环境搭建与工具链准备2.1 交叉编译环境是第一个门槛既然是嵌入式驱动逃不掉交叉编译。MediaTek这套SoC是ARM架构所以主机x86上的gcc编出来的模块完全不能用。第一步就是装好交叉编译工具链。不同的SDK版本可能对应不同的工具链这次我用的是ARM 64位的工具链tar解压后直接加进PATH就行。配置好环境变量后我建议立刻做一次自检写一个helloworld.c用交叉编译器编一下再在板子上跑起来。这一步能提前暴露工具链版本不匹配、动态库缺失之类的问题。不要在还没验证工具链的时候就急着编内核模块不然等编译报错的时候你可能分不清是工具链问题还是Makefile问题。交叉编译工具链里还要注意GCC版本与SDK的匹配。有些SDK是在某个特定Ubuntu版本下构建的如果你用的工具链太新兼容性可能出问题。我一般打开SDK自带的build目录看看编译参数不随意换版本。2.2 内核源码与内核头文件的版本对齐编内核模块时最关键的一点模块要在目标板内核源码树里编译而且内核源码的版本必须跟板子上跑的内核版本一致。这里说的“一致”不只是版本号还包括 .config 文件。很多新手上来直接拿一个网上下载的同版本内核源码编模块结果insmod时报“version magic mismatch”原因就是配置不一致。我这次的做法是先查看板子当前内核版本然后去SDK目录找到配套的内核源码用板子上的 /proc/config.gz 或者SDK自带的defconfig 还原出 .config然后再开始编模块。这里多花十分钟后面省一小时。如果板子上跑的是预编译镜像而你手上只有内核源码包可以先交叉编译内核但不烧写只保留编译模块时需要的中间文件。内核模块不依赖于烧写结果只依赖内核源码和编译配置。2.3 设备树的读取与验证在改任何设备树之前先把板子上现有的设备树反编译出来看看。设备树文件在 /sys/firmware/devicetree/base 或 /proc/device-tree 下能看到实际生效的节点。配合主板原理图核对寄存器地址、中断号、GPIO编号确认自家模块要用到的资源是否已经被其他设备占用。设备树是驱动展示脚本里重要的一环因为很多时候你新加一个模块实际上是在描述一个“虚拟平台设备”。比如要给NPU模块暴露一个字符设备你可以在设备树里新增一个名为mediatek,aiot-npu-demo的兼容节点填写中断号等信息然后在驱动里用of_match_table匹配这个兼容字符串。整个流程很标准但前提是你对设备树的语法和中断签名的写法要熟悉。我在搭建环境时先做了一次“最小验证”在设备树里新增一个空的测试节点配置好compatible属性然后写一个只有probe函数的platform驱动看模块加载时probe能不能被调用。这个验证通过后就说明整个驱动框架和资源匹配链路是通的之后写业务逻辑就踏实了。3. 驱动模块的框架选择与核心实现3.1 用platform driver承载大多数外设模块在Linux驱动的世界platform driver是连接设备和驱动最常用的中间层。它的核心在于总线匹配机制驱动的of_match_table里的compatible字符串如果能和设备树节点的compatible属性对上设备就会被匹配到驱动的probe函数就会执行。我这次面向MediaTek AIoT SoC的展示模块大多采用platform driver模型。比如首先写的一个基础信息模块它能从设备树中读取SoC型号、芯片版本、频率等参数再通过miscdevice或字符设备接口把这些信息输出给用户态。用platform driver的好处是不依赖具体硬件具体型号只要是同一系列SoC换板子也能用同一套驱动。static const struct of_device_id aiot_demo_of_match[] { { .compatible mediatek,aiot-demo }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, aiot_demo_of_match);注意这段代码里的MODULE_DEVICE_TABLE这个是让驱动在模块加载时能被系统识别为可匹配of设备的声明。漏掉它就算编译加载成功设备树匹配也会失效。3.2 字符设备接口与用户态沟通Demo的基础驱动只是内核态的存在真正演示要给用户态应用看东西。字符设备是最基础也最通用的接口。为每个展示功能创建一个设备节点通常是 /dev/aiot_info、/dev/aiot_npu、/dev/aiot_gpio。然后上层测试程序通过 open/read/write/ioctl 跟驱动打交道。字符设备有两种注册方式老式的是register_chrdev新式的是alloc_chrdev_region cdev_init cdev_add。我建议用新式虽然代码多一些但可以分配多个设备号也符合现代内核习惯。配上miscdevice的话更简单Minor号自动分配会被自动挂载到/dev下。不过miscdevice适合简单场景像“读取芯片信息”这种就够了如果是需要多个命令交互的场景还是用完整的字符设备框架。static struct cdev aiot_cdev; static struct class *aiot_class; static dev_t aiot_devno; if (alloc_chrdev_region(aiot_devno, 0, 1, aiot_demo) 0) goto err_alloc; cdev_init(aiot_cdev, aiot_fops); if (cdev_add(aiot_cdev, aiot_devno, 1) 0) goto err_add; aiot_class class_create(THIS_MODULE, aiot_class); device_create(aiot_class, NULL, aiot_devno, NULL, aiot_demo);这段代码把设备节点创建放在驱动probe里配合设备树匹配插上模块就能看到 /dev/aiot_demo卸载模块后设备节点也随之消失。对演示来说体验已经很接近“即插即用”了。3.3 IO操作与Demo场景的结合方式驱动模块要展示SoC的IO能力最常见的做法是给上层提供GPIO读写、I2C读写这几类IO控制服务。Linux内核里读写GPIO有标准的gpiod_get/gpiod_set_value接口I2C设备则用i2c_transfer或i2c_smbus_read_byte_data这类接口。我在写驱动时重点不是实现通信协议本身而是把协议封装成“上层一条命令驱动完成一整套时序”的形式。比如I2C读取温湿度传感器上层只要ioctl传入寄存器地址和读取长度驱动内部就负责打开对应的i2c_client、发起传输、把数据拷回用户态。这样Demo脚本只用几十行C代码或者直接用shell配合echo/cat操作sysfs接口就能把传感器数据可视化出来。这里也提醒一下别在原子上下文里直接睡眠等待I2C传输很多初写驱动的朋友在这里踩坑导致系统hung住。3.4 中断、DMA与性能数据留接口展示SoC能力不能光展示“能通”还得展示“性能”。所以驱动里我给中断信息和DMA通道状态留了统计接口。具体来说用cat /proc/interrupts可以看中断分布用/sys/kernel/debug/gpio可以看GPIO状态。但为了让Demo更直观我在驱动里专门做了一个ioctl命令把模块处理过的中断次数、DMA传输字节数、最近一次传输耗时这些信息一次性算出来返回给用户态。request_irq注册中断并记录次数统计用原子变量避免并发问题DMA则用到dma_alloc_coherent分配一致内存记录字节数。这样上层测试程序可以画出每秒中断次数、传输带宽的变化曲线客户看到的不再是“驱动工作正常”一句话而是“这一秒SoC处理了多少中断、带宽多少”非常有说服力。4. 集成MediaTek SoC特有功能4.1 把NPU算力封装成可调用的驱动接口MediaTek AIoT SoC命名里最值钱的就是AI部分也就是NPU。SDK里通常会自带一套NPU运行时库和内核驱动但面向“展示”这个目标我更倾向于在这一层之上再包一层更简单直观的字符设备接口。上层Demo不直接复杂调用runtime API而是通过几个固定格式的命令请求推理剩下的交给我的封装驱动去对接底层NPU驱动。这种封装层的意义在于降低观众和客户的理解门槛。技术交流的时候你可以快速跑通一个图像分类、目标检测的Demo而不用把NPU驱动里的每一个细节都暴露给上层。实际项目里如果性能要求很高这样封装的代价也值得因为API本身可以保持稳定底层NPU驱动升级不影响上层应用。封装NPU时有个细节就是模型加载、输入输出buffer的管理。我建议给每个推理请求预分配好输入输出缓冲区通过mmap映射到用户态减少多次open/read/write的系统调用开销。这样在演示视频流目标检测时帧率才不会因为系统调用太频繁而掉得厉害。4.2 视频、ISP和多媒体链路的驱动协同AIoT场景里视觉能力往往是跟AI推理绑在一起的。MediaTek SoC集成了ISP支持多路摄像头输入。要让摄像头的数据直接送入NPU做推理驱动层面要打通一条链路摄像头采集驱动 - ISP处理 - NPU推理。我在项目里特别验证了这条链路。底层驱动通常由SDK提供但多路视频流的缓冲管管理还是要自己梳理。我的做法是在NPU封装驱动里额外维护一个“当前输入帧”的dma_buf引用当摄像头驱动完成一帧采集后直接把帧数据通过dma_buf传给NPU驱动避免一次不必要的内存拷贝。如果要在Demo中展示这一块需要注意踩雷点不同分辨率下ISP和NPU的buffer对齐要求不同DMA分配内存时要设置正确的对齐参数。我见过很多团队在这里翻车demo上分辨率低的时候没问题一切到1080p就花屏甚至内存报错最后查出来是buffer对齐位数不够。4.3 电源管理低功耗SoC的加分项MediaTek AIoT SoC的低功耗特性很值得在Demo里展示。Linux 内核有标准的runtime PM框架设备不用时自动suspend有请求时自动resume。驱动模块需要实现runtime_suspend和runtime_resume回调并在真正需要的时候调用pm_runtime_get_sync/pm_runtime_put。这块我踩过坑项目里一开始所有设备都设为“一直active”结果整板功耗高得离谱。后来逐个模块接入runtime PM同时配置好设备的wakeup capabilities实测待机功耗降了一大截。对于AIoT场景客户经常问“能不能太阳能供电”“能不能电池撑半年”驱动层的电源管理就是回答这些问题的核心。在演示中我设计了一个脚本实时读取各设备的runtime PM状态和功耗数据动态展示在界面上。比如运行模型推理时NPU进入active状态推理结束后数秒内自动切回suspend整个功耗曲线一目了然。这一套下来客户对“低功耗”的感知就从参数表上的数字变成立刻可以量到的现象。5. 模块编译、加载与调试排查实录5.1 Makefile怎么写才能少踩坑内核模块的Makefile看起来简单但写错一行就能让你折腾半天。最标准的写法是用Kbuild系统指定模块名和源码文件名然后在外层调用内核源码树的make命令。以下是一个可复制的模板obj-m aiot_demo.o aiot_demo-objs : main.o npu_if.o gpio_ctrl.o KDIR : /home/user/mediatek-sdk/kernel-5.15 CROSS_COMPILE : aarch64-linux-gnu- all: $(MAKE) -C $(KDIR) M$(PWD) ARCHarm64 CROSS_COMPILE$(CROSS_COMPILE) modules clean: $(MAKE) -C $(KDIR) M$(PWD) ARCHarm64 CROSS_COMPILE$(CROSS_COMPILE) clean注意几点KDIR一定要指向目标内核源码目录而不是编译工具链目录CROSS_COMPILE变量必须是前缀不能写成完整的gcc路径M$(PWD)表示当前目录是模块源码目录。如果SDK里有make modules_prepare之类的准备步骤最好先执行一遍确认内核头文件完整。5.2 insmod失败的高频原因与判断方法驱动写完之后加载是第一个真正验证的点。我总结了几个高频的insmod失败原因version magic mismatch内核版本或config不一致解决方法是重新编译模块或者换内核源码unknown symbol模块依赖的符号没有被导出或者依赖模块没有先加载permission denied模块签名要求大多数AIoT开发板默认关闭模块签名但如果SDK开了就必须走签名流程no space left / invalid module format也许是文件没拷贝完整或者架构不对判断这些问题的第一现场就是dmesg。内核在加载模块时会打日志不管成功失败都会有记录。加载失败后先别急直接看dmesg尾部输出它往往直接告诉你原因。如果你连dmesg都看不到那可能是log_buf被其他打印刷掉了这时候可以临时加大 log_buf或者在SDK的启动参数里加 loglevel8。实际调试时我习惯用modprobe而不是insmod因为modprobe会自动处理依赖模块的加载也能读取/etc/modprobe.d下的配置。但注意modprobe默认从lib/modules目录查找模块文件需要先把 .ko 拷贝到对应路径或者手动指定目录。5.3 设备树匹配失败与probe不执行模块加载成功但probe不执行也是高频问题。系统日志里可能有以下几个现象提示 “no platform device found”或者干脆没提示。这时候要按顺序排查设备树节点是否添加并生效重新编译设备树并烧写后用/proc/device-tree查看节点是否存在compatible字符串是否一致一个字节都不能差注意大小写驱动里的of_match_table是否被正确注册检查MODULE_DEVICE_TABLE是否漏了设备树节点的status是否被设置为disabled我调试时会在probe函数入口加一段printk(KERN_INFO aiot_demo probe called\n)配合设备树节点的status快速确认匹配链路。如果probe始终不执行可以在内核配置里打开CONFIG_OF_UNITTEST等方式做单元验证但那样太慢直接看of_match逻辑更快。5.4 中断、并发和内存分配的实战教训在驱动里操作真实硬件时中断并发和内存访问的问题会暴露出来。比如同一个GPIO中断频繁触发时如果处理函数里不能快速完成任务就要用workqueue或tasklet把耗时操作挪到“下半部”。我这次在NPU封装模块里用了一个workqueue来异步完成推理准备和耗时统计避免阻塞中断上下文。另一个常见问题是DMA内存分配失败。AIoT场景下连续物理内存可能很紧张尤其在长稳测试后内存碎片化严重。我调试时试过dma_alloc_coherent在连续分配64MB时失败后来在设备树里给NPU保留了一块CMA内存区域问题解决。CMA是连续内存分配器在设备树里用linux,cma-default或者其他自定义节点声明内核启动时会预留区域专门给这类高带宽外设使用。还要提醒一点读取硬件寄存器时要用ioremap映射物理地址然后使用readl/writel访问。不要直接解引用物理地址指针在ARM上会直接触发data abort。这也是新手容易犯的错误看着好像地址对得上其实背后少了iomem转换这一步。5.5 性能瓶颈排查perf和trace工具的使用既然做的是“展示”那性能数据不能只在心里有数得能精确测出来。内核线程和中断处理函数的CPU占用可以用perf top、perf record看热点驱动调用的IO等待和调度延迟用trace-cmd或者ftrace的function_graph跟踪。perf在嵌入式板卡上通常需要内核配置CONFIG_PERF_EVENTS支持如果没开可以看/sys/kernel/debug/tracing下有没有可用的tracepoint。调试一个I2C传感器轮询频率异常的问题时我用ftrace跟踪了i2c驱动的调用路径很快定位到是驱动里一个无意的delay导致阻塞了后续调度。这种问题靠肉眼读代码很难察觉抓trace效率高得多。建议在模块里也埋几个tracepoint或者用trace_printk特别是在入口和出口打点这样事后排查时就能看到“每一次调用发生在什么时候、持续多久”的完整时间线。6. 常见问题速查与避坑心得6.1 模块加载与运行期问题速查表现象可能原因快速排查/解决insmod失败Invalid module format内核版本/架构不匹配用dmesg看具体日志重新编译模块insmod失败Unknown symbol依赖模块未加载或符号未导出dmesg会提示符号来源先加载依赖模块probe不执行设备树compatible不匹配或节点disabled查看/proc/device-tree确认节点核对字符串设备节点未创建udev规则缺失或class/device创建失败dmesg确认有没有create失败日志中断计数不增长中断号配置错误或GPIO管理冲突查看/proc/interrupts确认IRQ注册DMA分配失败CMA区域不足或内存碎片增大CMA区域检查设备树配置系统sleep后无法唤醒电源管理回调没写完整检查wakup source配置核对PM回调这张表是我多个项目里实际踩过的坑精炼出来的比起逐个查内核文档先走一遍这个表能省大量时间。很多问题看着不一样根因其实是同一个版本不一致或者设备树资源冲突。6.2 长稳测试中遇见的隐蔽问题短时间Demo一切正常但跑24小时长稳就会出现问题。我最深的一个教训是I2C传输偶发失败。刚开始只看到上层报错驱动没任何打印后来在驱动里增加了重试机制和错误计数才定位到是总线被某个罕见时序干扰导致偶发NACK。建议正式展示前至少跑48小时长稳把日志和计数器留下来。另一个隐蔽问题是内存泄漏。驱动里每次ioctl调用都分配内存但忘了释放长稳时内存占用缓慢上涨直到最终申请分配失败。现在每次提交驱动前我都会用kmemleak扫一遍或者至少观察 /proc/slabinfo 的变化这能提前揪出大部分泄漏问题。6.3 针对演示场景的特殊调优技巧做演示和做产品有个区别演示要“快、稳、好看”。所以我在驱动和上层脚本上做了一点定制调整。比如NPU推理Demo刚启动时先把模型加载好而不是每次推理都重新加载外设数据采集Demo则是预先把所有传感器初始化然后统一触发一轮采集减少中途等待。另外我建议在演示机上把内核日志级别调高用一条命令dmesg -n 8这样驱动里打印的关键状态信息可以直接显示在console上观众能看到模块加载、推理启动、设备传感器的实时状态变化。再配合一个简单的Web页面或Shell脚本就能把“媒体Tek AIoT SoC能力展示”这个目标呈现得很完整。还要注意板卡散热。长时间满载跑NPU推理核心温度会上升如果温度过高触发降频Demo帧率会忽高忽低。演示前可以先查看 /sys/class/thermal/thermal_zone*/temp确认温度在合理区间必要时加个小风扇。7. 这个项目的后续延展方向补齐了Linux驱动模块这套展示体系之后我不建议就把它当成一个纯内部Demo结束。这个框架本身可以继续生长。比如把每个驱动模块的测试用例用自动化脚本串起来跑一轮就能输出一份“SoC能力报告”包含NPU推理耗时、传感器采样频率、中断吞吐量这些数据。这份报告在客户沟通、方案选型、甚至采购评审时都很有用。另一个方向是把这些驱动模块的接口标准化。如果未来切到其他型号的MediaTek AIoT SoC设备树和外设资源定义会变但驱动接口可以尽量保持一致。提前把接口规范抽象出来后续迁移成本会低很多。再往前一步这些驱动模块和演示程序可以一起打包成一套SDK示例配套详细的注释和文档。客户拿到评估板后不用翻着手册找示例代码直接跑这个展示套件就能快速理解SoC能做什么、怎么集成进自己的产品。不管从技术验证还是商务推广层面这套Linux驱动模块都是值得持续打磨的资产。
返回列表