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 初始化、异常向量、内存管理单元配置;往上是核心子系统,包括进程调度、内存管理、虚拟文件系统、网络协议栈;再往上是各种设备驱动框架,比如字符设备、块设备、网络设备、平台设备;最上面是系统调用接口,给用户空间程序用。
BSP(Board 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 或者 completion;FreeRTOS 里用临界区保护共享资源,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 内核经验不多的工程师,用你能理解的语言把问题讲清楚。如果某篇内容你觉得太浅或者太深,欢迎反馈,我会根据实际情况调整后续的深度和节奏。
最后说一句,内核这东西,看再多不如动手跑一遍。哪怕只是编译一个最小系统、改一行设备树、加一条打印,实际做过之后的理解深度和纯看文档完全不是一个量级。这个总目录先把框架搭起来,后面的路咱们一篇一篇走。