嵌入式Linux厂商内核与驱动定制实战经验总结

发布时间:2026/10/11 11:39:29
嵌入式Linux厂商内核与驱动定制实战经验总结 做嵌入式Linux底层开发的朋友对厂商内核这四个字应该都不陌生。我们常说的厂商内核指的是某芯片或板卡厂商在其硬件平台上维护的一整套内核源码、设备树、驱动和构建脚本也就是大家口里的BSP的一部分。围绕它做的驱动定制也远不只是改几行代码那么轻巧——从配置筛选、设备树适配、模块裁剪到补丁管理、镜像验证每一步都可能踩进厂商封装带来的坑。这篇文章我就把自己在项目实战里折腾厂商内核与驱动定制的一些经验和教训梳理出来给刚接触这块、或者正在被厂商源码折磨的兄弟一个参考。1. 拿到厂商内核后别急着编译先摸清这套东西的家底1.1 厂商内核包里到底装了些什么很多新同事拿到厂商释放的源码包后第一反应就是找README然后敲make。我不太建议这样做。厂商包往往不是一个干净的内核源码树而是一个大杂烩里面可能同时包含内核源码、预编译的启动镜像、设备树源文件、驱动模块、第三方闭源库、打包工具和一大堆版本说明文档。这些文件之间的依赖关系比源码本身更容易坑到你。以我接触过的某嵌入式平台为例厂商的发布包里真正用于内核构建的是一个特定的子目录外层的脚本负责下载工具链、初始化环境、拼接配置。如果你不理解这套结构直接在某个看起来像内核根的目录里编译大概率会碰到缺头文件、缺脚本甚至缺配置的问题。正确做法是先把整个包解压用tree或者find把目录结构拉出来重点找几个东西内核主目录、设备树源文件目录、模块编译目录、以及构建脚本。摸清这套家底后再动手不迟。还有一个细节很容易被忽略厂商包里通常会有一些预生成的产物比如已经编译好的模块、已经生成的Module.symvers文件。如果直接基于这些旧产物做增量编译很容易出现配置漂移。我的习惯是把预生成产物单独归档自己从头走一遍干净构建确保手里的产物和源码是严格对应的。1.2 版本对齐关系内核、SDK与工具链厂商内核不同于主干内核它是在某个标准内核版本之上叠加上厂商私有的补丁和驱动形成的。所以它有自己的版本号、补丁级别和SDK匹配关系。很多问题的根源不是代码有问题而是内核版本、SDK版本、工具链版本三者没有对齐。举一个常见的例子厂商的SDK说明书里写着支持某个版本的GCC结果你机器上默认的编译器版本偏新。编译老内核时新的GCC对代码的检查更严格历史代码里一些能用但不太规范的写法就会变成错误你会看到一堆莫名其妙的报错。这时候千万不要去改厂商源码迁就编译器而是应该回到SDK指定的工具链版本。我的习惯是拿到包后先建一个干净的构建环境用厂商提供的工具链和官方脚本完整编译一次记录下编译耗时、生成物清单和镜像的绝对路径。这个基线构建就是后面所有定制工作的参照物出了问题可以和它对比快速判断是自己的改动破坏了什么还是环境本来就有问题。版本对齐这件事可以类比成盖房子时的地基内核基线是地基SDK版本决定你手里这堆混凝土的配方工具链则是浇筑时用的模板。任何一层对不齐后面砌的砖都可能歪掉。1.3 构建环境的隔离与备份关于环境多强调一句尽可能用容器化或者独立虚拟机来构建厂商内核。内核编译对系统库有依赖本机的环境会被各种项目改得乱七八糟某一回装了个新库内核编译可能就挂了。容器方案可以固定一套依赖团队协作时也能保证大家产出一致。还要养成两个习惯一是编译产物分离尽量用O参数指定独立输出目录不要往源码目录里写太多obj文件。二是源码备份解压后的厂商原始源码保留一份只读副本后面所有操作都在工作副本里进行。这样打乱了补丁、删错了文件随时能从原始副本恢复不至于重头再解压一遍。具体操作上我通常会这样组织目录workdir/ ├── vendor-src/ # 厂商原始源码只读 ├── my-patches/ # 自己的补丁目录 ├── build-output/ # 编译输出目录 └── scripts/ # 自己的构建脚本源码和输出分离、补丁和源码分离这两个分离坚持下来后面排查问题会轻松非常多。很多项目做到后期代码本身没多复杂复杂的是这个文件我到底改过没有这个镜像是什么时候编的这类追溯问题。2. 内核定制的三种层次以及它们该怎么取舍从厂商内核到产品镜像定制工作大致可以拆成三个层次配置层、设备树层、源码层。理解这三个层次的区别和适用场景能帮你在动手之前就选对路线避免一上来就改源码把自己拖进补丁地狱。2.1 配置层定制defconfig与碎片化配置配置层定制是最安全、最基础的定制方式。厂商默认的defconfig为了覆盖产品线下的多款硬件通常会把很多用不到的驱动和子系统编进去镜像臃肿、启动变慢甚至引发一些功能冲突。做产品定制时第一件事往往就是裁剪配置。但我不建议直接去改厂商提供的defconfig文件。更稳妥的做法是先用厂商defconfig生成.config然后通过menuconfig做交互式裁剪。裁剪完成后用savedefconfig把当前有效配置整理成精简的defconfig保存成自己产品的配置文件。savedefconfig的好处在于它会省略所有与默认值相同的选项只保留被改动过的项这样配置差异一眼就能看出来评审和回溯都很方便。裁剪配置时要注意保留哪些基础项呢我一般会先确认这几点目标CPU架构和对应平台、存储介质与根文件系统类型、文件系统格式、网络支持、以及产品真正用到的外设驱动。调试阶段建议保留内核日志和动态调试功能量产时再关掉。这里有个反直觉的经验配置裁剪的收益往往不是线性的有时候砍掉一个大驱动镜像尺寸没小多少却可能连带破坏某个看似无关的功能。所以每做一次裁剪都应该完整构建并启动验证一次而不是攒一堆改动到最后再验证。2.2 设备树层定制硬件差异的落脚点设备树在这套体系里的作用可以理解成一份描述硬件的剧本内核启动时照着它决定加载哪些驱动、配置哪些引脚、映射哪些中断和地址。厂商内核和标准内核最大的差异之一就是厂商会维护一套针对自家SoC的设备树描述。做板级定制时最频繁的操作就是新增一个设备树节点来描述自己板子上有、而厂商默认描述没有的外设。这里要理解两个层级SoC级描述通常叫.dtsi定义芯片内部通用的控制器和外设接口板级描述.dts定义具体某块板子的引脚、时钟和外部设备。定制时尽量新建板级dts复用SoC级dtsi而不是修改dtsi本身。这样厂商后续更新SoC描述时你的板级文件还能继续用。设备树定制里的常见错误是直接在厂商提供的原始dts上改。我们之前就有同事图省事直接在原始文件上加了几个节点后来厂商发新版本两边一合并冲突搞得人头疼。正确姿势是自己建一个独立的板级dts把新增外设的节点写进去编译时单独产出该板子的dtb文件。另外设备树里的status属性是个容易忽视的细节。有些外设节点在SoC级dtsi里是status disabled板级dts里要显式改成status okay才会生效。很多外设莫名其妙不工作的问题追查到最后就是这个属性没改。2.3 源码层定制补丁管理与模块化思路如果需要改驱动逻辑、调内核行为那就进入源码层定制了。这里最容易犯的错是直接改厂商源码树里的文件。改完能跑一时但后续跟踪困难厂商更新时你根本不知道哪些改动是自己做的。我个人的做法是补丁化。用git format-patch或者其他补丁工具把每一次改动做成独立的补丁文件放到一个专门的overlay目录构建脚本里在厂商源码上自动打补丁。补丁的命名尽量能表达意图比如fix-i2c-timeout.patch。这样做有几个好处改动可追踪某次编译出问题能快速排查是哪个补丁导致的可回退不需要clean代码树可复用新来的同事只需要拉取补丁目录就能复现整个定制过程。需要说明的是这套补丁化做法不仅适用于小改动对几十上百个文件的定制同样适用。关键是要保持每个补丁的独立性不要把所有改动揉进一个巨型补丁里。我见过最痛苦的项目就是一个补丁改了三十多个文件里面混杂了三四个完全不相干的功能改动出了问题根本没法排查。补丁切得越小越细后面的调试成本就越低。3. 实操全过程从配置裁剪到驱动编译加载理论说再多不如完整过一遍流程。下面我以一块典型的厂商开发板为例带大家走完从拿到源码配置到驱动编译加载的全过程。实际的命令和路径在不同的平台上会有差异但思路是通用的。3.1 基于厂商默认配置生成并裁剪内核配置拿到源码后进入内核根目录先看arch/下针对你目标架构的configs目录里有哪些defconfig。假设厂商为开发板提供了配套的配置文件第一步就是基于它生成基本配置make ARCHarm64 xxx_defconfig执行完成后会在当前目录生成一个.config文件。此时不要着急编译先看看这个配置开了哪些大块内容。用交互式配置界面来做可视化裁剪make ARCHarm64 menuconfig在界面里可以逐项勾选或取消驱动、文件系统、网络协议栈等。对于不确认是否需要的选项建议先查一下它依赖什么、被什么依赖盲目关闭可能会连坐关闭一些必要选项导致后续启动出问题。裁剪时我习惯聚焦几个区域取消无关的外部设备驱动、去掉不需要的调试框架、精简内核内置的rootfs相关选项。裁剪完成后保存配置并生成精简的defconfigmake ARCHarm64 savedefconfig此时目录下会生成一个精简后的defconfig文件。把它改名为myboard_defconfig复制到arch/arm64/configs/下以后每次构建只要执行make ARCHarm64 myboard_defconfig就能复现当前定制后的配置状态。这一步藏着一个小技巧savedefconfig生成的文件只包含非默认选项所以在代码评审时打开它一眼就能看到你产品定制与厂商默认配置的全部差异这个可读性是原始.config完全不具备的。3.2 手写一个最小内核模块并编译加载配置搞定后我们来写一个最简单的字符设备驱动模块通过这个例子说明驱动定制的最小闭环。先准备驱动代码假设文件名是demo_dev.c#include linux/module.h #include linux/init.h #include linux/fs.h #include linux/miscdevice.h static ssize_t demo_dev_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { return 0; } static const struct file_operations demo_dev_fops { .read demo_dev_read, }; static struct miscdevice demo_dev_device { .minor MISC_DYNAMIC_MINOR, .name demo_dev, .fops demo_dev_fops, }; static int __init demo_dev_init(void) { return misc_register(demo_dev_device); } static void __exit demo_dev_exit(void) { misc_deregister(demo_dev_device); } module_init(demo_dev_init); module_exit(demo_dev_exit); MODULE_LICENSE(GPL);这个模块用miscdevice框架注册一个叫做demo_dev的设备节点属于最简单但结构完整的驱动例子。编译它需要一份配套的Makefileobj-m demo_dev.o KDIR : /path/to/kernel/source PWD : $(shell pwd) all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean注意KDIR要指向你正在构建的厂商内核源码根目录而不是系统的/lib/modules。编译时执行make成功后目录里会出现demo_dev.ko。把它拷贝到目标板某个可读目录用insmod加载insmod demo_dev.ko然后执行dmesg | tail看是否有模块初始化日志同时检查/sys/class/misc/demo_dev目录是否存在。存在就说明模块已经成功注册。卸载则用rmmod demo_dev。这样一个最小驱动定制的闭环就走通了。3.3 驱动与设备树的匹配机制模块能加载只是第一步真实的驱动定制里驱动需要和设备树节点建立绑定关系设备插入或者系统启动时驱动才会被自动加载。这里的核心就是compatible字符串匹配。举个例子。假设板子上有一颗I2C温度传感器在设备树里新增一个节点i2c0 { temp_sensor: temp-sensor48 { compatible vendor,temp-sensor; reg 0x48; interrupt-parent gpio1; interrupts 3 IRQ_TYPE_LEVEL_LOW; }; };驱动侧则在of_match_table里声明自己支持的设备static const struct of_device_id demo_temp_of_match[] { { .compatible vendor,temp-sensor, }, { } }; MODULE_DEVICE_TABLE(of, demo_temp_of_match); static struct i2c_driver demo_temp_driver { .probe demo_temp_probe, .id_table demo_temp_id_table, .driver { .name demo_temp, .of_match_table demo_temp_of_match, }, }; module_i2c_driver(demo_temp_driver);内核在设备树解析阶段发现compatible为vendor,temp-sensor的节点后会在已注册的驱动列表里寻找能匹配的设备找到后触发驱动的probe回调传入设备信息驱动拿到reg、interrupt等属性完成初始化。整个设备树描述硬件、驱动描述能力、compatible完成握手的机制是定制驱动时一定要理解的核心。很多驱动加载不成功的问题最后排查下来都是compatible字符串没对齐或者设备树节点没有真正编入最终dtb。3.4 构建完整镜像并与厂商默认镜像对比验证驱动调试通过后需要回归到完整镜像构建。通常厂商构建脚本会帮你把内核、dtb、模块全部打包。如果没有现成脚本自己维护一个简单的打包流程也可以make ARCHarm64 Image dtbs modules -j$(nproc) make ARCHarm64 INSTALL_MOD_PATH./rootfs modules_install生成三条主要产物内核镜像Image、设备树dtb文件、以及安装到rootfs的模块目录。把它们按厂商要求的结构放进固件分区或根文件系统。这里我强烈建议做一个操作构建完成后分别保存厂商默认镜像和定制镜像的启动日志自己给自己建立一个验收基线。对比的方法很简单把两次启动的串口日志各存一份用diff对比关键阶段。重点关注内核版本字符串是否包含你的额外标识、挂载的根文件系统是否正确、设备树是否显示你新增节点的probe成功消息、加载的模块列表是否符合预期。日志里出现Failed to probe或no matching driver一类的关键字要回到前面的匹配机制里排查。这一步虽然看起来花时间但能避免很多改了配置后功能悄悄消失的隐性回归。4. 厂商驱动定制中的典型问题与排查技巧实录定制做得多了慢慢会发现所有坑都集中在几个固定的类别里。下面这些是我在实际项目里反复踩过、也帮同事排查过的问题整理出来给大家做个速查。4.1 编译阶段报错头文件、配置生成顺序与工具链编译阶段最常见的报错是源码里明明有某个头文件编译器却提示找不到或者某些结构体、宏未定义。这种情况通常不是因为代码缺失而是内核源码树还没有生成编译所需的中间产物。内核在编译前会先根据.config生成一系列头文件比如autoconf.h、version.h外部模块编译依赖这些文件。解决方案是先完整编译一次内核或至少执行make modules_prepare把基础产物生成出来。另一个高发问题是工具链版本。厂商SDK里通常会附带一个scripts目录或者环境变量配置指定GCC的版本。不要绕过它更不要用发行版自带的现代编译器去编译厂商老内核。遇到某种类型定义冲突builtin函数不匹配这类编译错误第一反应应该是查工具链版本而不是查源码。有些时候我在做完配置裁剪后还会专门确认一下CONFIG_LOCALVERSION有没有设置。如果不同机器上构建的内核版本字符串不一致后面排查问题时容易分不清跑的内核到底是哪个编译出来的这里花费的几分钟是值得的。4.2 模块加载失败未知符号与版本校验insmod时最常见的报错有两类。第一类是Unknown symbol in module。这说明模块里引用了内核符号但该符号没有被导出或者依赖的其他模块没有先加载。先用modinfo demo_dev.ko查看模块依赖列表再用nm查看未定义符号确认它们属于哪些模块按依赖顺序加载即可。如果符号确实需要在厂商内核里导出也可以在源码里通过EXPORT_SYMBOL补齐后重新编译内核。第二类是version magic不匹配。内核开启了CONFIG_MODVERSIONS或CONFIG_MODULE_SIG时会对模块做严格的版本校验模块编译时的内核配置、头文件与当前运行内核不一致就会拒绝加载。遇到这种问题几乎不用查别的直接删掉模块在目标内核源码树里重新编译。这里想多说一句厂商SDK里给的预编译模块通常只能和配套的内核配合使用。如果你想对内核做了自定义配置最好把厂商提供的一整套模块也放进新内核树里重新编译一遍否则很容易出现部分模块能加载、部分模块版本magic报错的混合状态排查起来更加痛苦。4.3 设备绑定失败与运行时崩溃排查模块加载成功、但/sys底下没有对应设备节点时多半是设备树匹配没有生效。我会先检查最终烧录到板子上的dtb是不是最新编译出来的——这种问题我遇到过好多次代码改了半天发现烧录的还是旧dtb。然后再确认设备树的compatible与驱动of_match_table完全一致注意大小写和厂商前缀。驱动能加载但一访问设备就崩溃通常检查几个方向ioremap返回的虚拟地址是否为空却没做检查、中断申请的标志与设备树描述是否一致、并发访问共享资源时有没有加锁。定位时用dmesg看异常栈栈顶函数往往就是问题所在。还可以打开内核的CONFIG_DYNAMIC_DEBUG在驱动里加dev_dbg配合echo file xxx.c p /sys/kernel/debug/dynamic_debug/control动态打开日志比起来回改代码加打印要高效得多。# 启用某个文件的所有动态调试日志 echo file demo_dev.c p /sys/kernel/debug/dynamic_debug/control # 确认当前已开启的调试点 cat /sys/kernel/debug/dynamic_debug/control | grep demo_dev这种做法的好处是不用重新编译和烧录内核运行时开关日志特别适合定位那些只在特定访问序列下才会触发的崩溃。4.4 定制踩坑速查表我把上面这些问题整理成一个速查表方便放到手边随时翻。典型现象背后原因快速处理方向编译报缺少头文件、类型未定义未生成内核中间产物或工具链版本不符先执行make modules_prepare核对SDK工具链版本模块加载报Unknown symbol依赖模块未加载或符号未导出modinfo查依赖按序加载必要时EXPORT_SYMBOL模块加载报version magic不符模块与内核配置不一致模块校验失败在目标内核树内重新编译模块设备节点不出现dts未更新或compatible不匹配确认烧录dtb为最新核对compatible字符串设备访问崩溃地址映射、中断、并发保护问题查dmesg异常栈用dynamic debug定位内核启动慢、镜像过大默认配置开具了过多子系统按产品需求裁剪用savedefconfig固化配置厂商更新后定制改动丢失直接修改了厂商源码未补丁化将改动迁移为独立补丁构建脚本自动应用这张表覆盖了我实战中遇到的大多数问题。如果看完还不能定位下一个动作就是把问题拆小用最小复现的方式一步步验证而不是继续在原环境里试探。比如驱动有问题就先在设备树里注释掉无关节点配置有问题就先回到厂商默认配置确认基线这样能快速圈定问题范围。最后再分享一点我个人对厂商内核与驱动定制这事的体会。做这块工作真正的难点从来不是某个驱动的算法或者某个寄存器怎么配置而是如何在一个随时可能变化的厂商代码基础上稳定地维护自己的改动。我们团队后来定了一条规矩厂商原始代码、自己的补丁、构建产物三类东西严格分离所有定制都以补丁形式存在独立目录构建脚本自动完成打补丁、编译、打包的全过程。坚持这个习惯之后绝大多数问题都能在半小时内定位因为任何一步的产物都可以和基线对比。如果你正准备开始接触厂商内核我建议你从完整编译一遍厂商默认镜像起步先别急着提需求。只有当你亲眼看过一份完全没被改动过的镜像能正常工作后续每一次定制你才会清楚地知道究竟是哪个改动改变了系统的行为。这条路没有捷径但环境干净、版本清晰、补丁规范就是最大的捷径。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询