
1. 这个专栏到底想解决什么问题搞嵌入式的人大概都有过这种体验MCU 上的裸机代码写得飞起中断、DMA、状态机玩得明明白白可一旦项目升级到带 Linux 的应用处理器整个人就像被扔进了一片没有地图的森林。启动流程看不懂、设备树不知道从哪下手、驱动 probe 不成功只能靠打印大法硬猜、内核裁剪完发现某个功能莫名其妙没了。更让人头疼的是网上关于 Linux 内核的资料要么是面向服务器运维的要么是面向纯软件开发的真正从 MCU 工程师视角切入、把 BSP 和内核串起来讲的内容少得可怜。这个专栏就是冲着这个断层来的。它不打算写成一本面面俱到的内核教科书而是想做成一条从 MCU 思维平滑过渡到 Linux 内核与 BSP 开发的路径。关键词里出现的 Linux、内核、MCU、FreeRTOS、BSP 这几个词其实已经勾勒出了整条主线从实时操作系统的调度思维出发理解 Linux 内核的分层设计再落到板级支持包的实际开发上。适合的读者是那些有单片机基础、做过 FreeRTOS 或裸机项目、现在需要接手嵌入式 Linux 项目的工程师也包括想系统梳理内核知识但被大部头劝退的自学者。我自己的经历是先在 STM32 上跑了两年 FreeRTOS后来转去做基于 ARM Cortex-A 的平台开发。最开始那段时间最大的认知冲击不是语法或者 API而是整个系统的组织方式变了。MCU 上你几乎掌控一切内存布局、中断向量、时钟树都是你亲手配的到了 Linux 这边内核帮你接管了绝大部分底层你要做的是通过一套约定好的接口去描述硬件、注册驱动、和内核子系统打交道。这个思维转换如果没人点破靠自己摸索会走很多弯路。所以这个总目录的设计思路就是先把地图画出来让你知道每一块知识在整个体系里的位置再逐个击破。2. 从 MCU 到 Linux 内核的认知地图2.1 为什么 MCU 工程师转 Linux 会觉得别扭先说清楚别扭在哪。MCU 开发的核心模型是我写代码直接操作寄存器程序是顺序执行加中断打断资源是确定的、可预测的。FreeRTOS 引入任务调度之后你开始有了优先级、任务栈、信号量这些概念但整个系统的规模仍然可控一个工程几十个文件编译出来几百 KB跑在几十 MHz 的核上。Linux 完全是另一个量级。内核本身几百万行代码编译出来的镜像几 MB 到几十 MB运行在几百 MHz 到几 GHz 的核上还带 MMU、带多级缓存、带完整的内存管理。你写的驱动代码不再直接跑在硬件上而是作为内核模块或内置代码通过内核提供的框架和硬件交互。这种隔了一层的感觉是很多人初期最不适应的地方。但换个角度看这层隔离恰恰是 Linux 能支撑复杂系统的原因。MCU 上你要自己管理内存、自己处理并发、自己保证实时性Linux 把这些都抽象成了子系统你只要按规矩接入就行。理解这个抽象层次是跨过门槛的第一步。2.2 内核分层与 BSP 的边界在哪里Linux 内核从下到上大致可以分成几层最底下是体系结构相关代码arch负责 CPU 初始化、异常向量、内存管理单元配置往上是核心子系统包括进程调度、内存管理、虚拟文件系统、网络协议栈再往上是各种设备驱动框架比如字符设备、块设备、网络设备、平台设备最上面是系统调用接口给用户空间程序用。BSPBoard Support Package的位置比较特殊它横跨了 arch 和驱动两层。具体来说BSP 要负责的是板子启动时的最早那段初始化代码、设备树的描述、各个外设的驱动适配、以及和具体硬件相关的配置。换句话说内核提供了通用的机制BSP 负责把这块具体板子的硬件翻译给内核听。这个边界感很重要。很多初学者会把本该放在 BSP 里的板级配置硬塞进通用驱动或者反过来把通用逻辑写死在板级代码里结果就是代码没法复用、换个板子就得大改。搞清楚哪些属于这块板子特有、哪些属于这个芯片通用、哪些属于内核框架是写出可维护 BSP 的前提。2.3 设备树硬件描述与代码解耦的关键设备树是嵌入式 Linux 里绕不开的一个东西也是 MCU 背景的人最容易困惑的地方。在 MCU 上硬件信息是直接写在代码里的比如GPIOA-ODR | (1 5)这种。到了 Linux硬件信息被抽出来放进了设备树文件.dts/.dtsi内核启动时解析这棵树根据里面的描述去匹配和初始化驱动。为什么要这么设计核心目的是解耦。同一份内核镜像配上不同的设备树就能跑在不同的板子上。芯片厂商提供 SoC 级别的 .dtsi板级厂商在此基础上写板级的 .dts描述这块板子具体接了哪些外设、用了哪些引脚、中断怎么连。驱动代码只关心有一个符合某 compatible 属性的设备不关心它具体在哪个地址、接的哪个引脚。理解设备树的关键是抓住三个东西节点node描述一个设备或总线属性property描述这个设备的参数compatible 属性是驱动匹配的钥匙。内核启动时平台设备会根据 compatible 去和驱动表比对匹配上了就调用驱动的 probe 函数。这个机制搞懂了后面看任何驱动的设备树绑定文档都不会发怵。3. 专栏内容规划与学习路径3.1 模块划分从启动到驱动再到调试整个专栏的内容我打算按启动链路—内核机制—驱动开发—调试排错这条主线来组织而不是按教科书那种先讲进程管理再讲内存管理的顺序。原因很简单对嵌入式工程师来说最有代入感的切入点是板子怎么跑起来的从这里往下挖每一步都能对应到实际会碰到的问题。启动链路部分会从芯片上电后的第一段代码讲起包括 BootROM、SPL、U-Boot、内核解压、start_kernel 一直到 init 进程。这条链路走通了你就能回答为什么我的板子卡在某个阶段不动了这类问题。内核机制部分会挑和驱动开发最相关的几个子系统重点讲比如内存管理里的虚拟地址映射、中断子系统、时钟框架、电源管理框架不会去深挖调度器算法这种偏理论的内容。驱动开发部分覆盖字符设备、平台设备、I2C/SPI 子系统、GPIO 和 pinctrl这些都是实际项目里天天打交道的。调试排错部分会专门讲 printk 和动态调试、ftrace、内核崩溃日志分析、以及常见的 probe 失败排查思路。3.2 和 FreeRTOS 经验的对照学习法有 FreeRTOS 基础的人学 Linux 内核其实有一个很大的优势就是你已经理解了任务调度优先级同步互斥这些概念。Linux 里对应的东西是进程/线程、调度策略、自旋锁和信号量概念上能对上号只是实现复杂度和使用场景不同。我建议的学习方法是做对照。比如 FreeRTOS 里任务间通信用队列Linux 里内核线程间通信用 kfifo 或者 completionFreeRTOS 里用临界区保护共享资源Linux 里根据上下文选择自旋锁或互斥锁FreeRTOS 的 tick 中断对应 Linux 的定时器子系统。把已知的概念映射到新体系里比从零建立认知要快得多。但也要注意别硬套。FreeRTOS 是硬实时内核任务切换时间可预测Linux 是通用内核虽然也有实时补丁但默认配置下不保证硬实时。这个差异决定了你不能把 MCU 上那套精确到微秒的时序控制思路直接搬到 Linux 应用层该用硬件外设完成的精确时序还是得交给硬件。3.3 每篇文章的定位与前置知识这个总目录本身是第 00 篇作用是给整个专栏定框架。后续每篇文章我会尽量做到自包含但有些确实需要前置知识。比如讲设备树之前最好先了解平台设备和驱动匹配机制讲驱动 probe 之前最好先知道内核模块的加载流程。我会在每篇开头标注建议的前置阅读方便你按顺序学或者按需跳读。如果你是完全零基础建议从启动链路开始顺着看如果你已经有项目经验、只是想补某个具体知识点直接跳到对应章节也没问题。每篇的结尾我会尽量留一个可以动手验证的小实验哪怕只是改一行设备树看打印变化动手做过一遍比看十遍都管用。4. 动手之前的环境与工具准备4.1 开发环境搭建的几种选择学内核开发环境是第一道坎。常见的选择有这么几种一是用 QEMU 模拟好处是不需要真实硬件一台电脑就能跑坏处是和外设打交道的内容没法完整验证二是用开发板比如各种基于 ARM 的开发板好处是能接触真实硬件坏处是要花钱、要接线、要处理各种硬件问题三是用现成的虚拟机镜像好处是开箱即用坏处是内核版本可能比较旧、和实际项目脱节。我的建议是组合使用。前期学启动流程、内核机制这些偏软件的内容用 QEMU 就够了编译快、调试方便、不怕把系统搞崩。到了驱动开发阶段尤其是涉及 GPIO、I2C、SPI 这些外设的还是得有块真板子因为很多问题只有在真实硬件上才会暴露比如时序、电平、上拉电阻这些。工具链方面交叉编译工具链是必须的。现在主流的是各种 GNU 工具链选的时候注意版本要和内核版本匹配太新的编译器编老内核可能报错太老的编译器编新内核也可能有问题。我一般会优先用芯片厂商或板卡厂商推荐的版本省得踩兼容性的坑。4.2 源码获取与版本选择的坑内核源码从官方仓库拿是最稳妥的但要注意选对版本。主线内核mainline最新但可能缺少某些芯片的完整支持芯片厂商的内核比如各家 SoC 厂商维护的对自家芯片支持最好但可能落后主线好几个版本板卡厂商的内核又在此基础上做了板级适配。选哪个取决于你的目的。如果是学习内核机制用主线稳定版就行代码干净、文档全。如果是做实际项目基本得用芯片或板卡厂商的版本因为主线可能还没合并你需要的驱动。这里有个坑要提醒厂商内核往往打了大量补丁和主线差异很大你在网上看到的教程可能对不上遇到这种情况要以厂商的文档和代码为准。源码目录结构也值得先花时间熟悉一下。arch 放体系结构相关代码drivers 放驱动kernel 放核心代码include 放头文件Documentation 放文档。其中 Documentation 目录经常被忽略但它里面的设备树绑定文档devicetree/bindings是写设备树时最权威的参考比网上任何教程都靠谱。4.3 调试手段的提前储备在 MCU 上调试你可能有 SWD 或者 JTAG能单步、能看寄存器。到了 Linux调试手段更丰富但也更需要提前准备。最基础的当然是 printk但要注意日志级别和输出时机早期启动阶段的打印需要特殊配置才能看到。再往上是动态调试dynamic debug可以运行时开关某条打印不用重新编译内核。ftrace 是内核自带的跟踪框架能跟踪函数调用、中断、调度等事件排查性能问题和理解代码流程都很有用。还有内核崩溃时的 oops 日志里面包含调用栈和寄存器状态学会读这个日志能省下大量猜测时间。这些工具不需要一开始就精通但至少要知道它们存在、大概怎么用遇到问题时才不会抓瞎。5. 常见误区与经验提醒5.1 别把内核当 MCU 程序来写最常见的误区就是把 MCU 的编程习惯带进内核。比如在内核代码里做长时间的忙等待、在中断上下文里调用可能睡眠的函数、不考虑并发就直接访问共享数据。这些在 MCU 上可能没事在内核里轻则性能下降重则直接死机。内核代码运行在各种上下文里进程上下文可以睡眠中断上下文不行有些锁在中断里能用有些不能内存分配有原子和非原子之分。这些约束不是故意为难人而是内核为了保证整体稳定性和实时性必须遵守的规则。写驱动之前先把这些基本约束搞清楚能避免很多低级错误。5.2 设备树不是万能配置表设备树能描述硬件但它不是万能的。有些东西设备树表达不了比如复杂的初始化时序、需要软件参与的状态机有些东西虽然能写进设备树但写进去反而不如放在驱动里清晰。判断标准是这个信息是这块板子特有的硬件描述还是这个驱动的行为逻辑。前者放设备树后者放驱动。还有一个常见问题是设备树写错了但内核不报错只是设备不工作。因为设备树解析是宽松的属性名拼错、节点放错位置很多时候不会直接报错只是匹配不上或者用了默认值。所以写完设备树一定要对照绑定文档检查别凭感觉写。5.3 遇到问题先缩小范围再动手改内核出问题的时候最忌讳的就是一通乱改。正确的做法是先缩小范围是启动阶段的问题还是运行阶段的问题是某个驱动的问题还是整个子系统的问题是配置问题还是代码问题把范围缩小到一定程度问题往往就自己浮现出来了。具体手段包括看启动日志定位卡在哪一步、用动态调试打开相关模块的打印、用 ftrace 看函数调用流程、对比能工作的配置和不能工作的配置差异。这些方法听起来简单但真正遇到问题时能冷静按这个思路走的人不多。我自己的经验是越是着急改代码越容易把问题搞复杂先花十分钟理清思路往往比盲目改一小时更有效。6. 写在专栏开篇的一些个人体会做嵌入式这行从 MCU 到 Linux 几乎是一条必经的升级路径。这个过程里最难的从来不是某个具体的 API 或者某段代码而是整个知识体系的重构。你在 MCU 上积累的硬件直觉、时序意识、资源约束感到了 Linux 这边依然有价值只是需要换一种方式表达。这个专栏我会尽量写得实在不堆砌概念不回避难点遇到容易踩坑的地方会明确标出来。每篇内容我都会假设你是一个有单片机基础、但 Linux 内核经验不多的工程师用你能理解的语言把问题讲清楚。如果某篇内容你觉得太浅或者太深欢迎反馈我会根据实际情况调整后续的深度和节奏。最后说一句内核这东西看再多不如动手跑一遍。哪怕只是编译一个最小系统、改一行设备树、加一条打印实际做过之后的理解深度和纯看文档完全不是一个量级。这个总目录先把框架搭起来后面的路咱们一篇一篇走。