嵌入式 Linux 系统开发入门指南,兼论十种常见 CPU 的软硬件生态
在我发布了关于 1 美元微控制器(MCU)的文章后,有几位读者建议我研究应用处理器 —— 这类配备内存管理单元(MMU)的芯片是运行 Linux 等真正操作系统的必要条件。过去几年,行业发生了巨大变革(安全领域尤为显著),我发现 Linux 的应用场景正变得越来越广泛。
在初级工程师眼中,应用处理器始终带着一层敬畏感:一个微小的 PCB 设计缺陷,就可能让价值 1 万美元的原型机变成废铁。而在资深工程师圈子里,却有一群 “技术老手” 能胸有成竹地将这些芯片融入设计,反观新手则只能依赖树莓派(Raspberry Pi)或价格高昂的工业级系统级模块(SOM)。
本文面向熟悉微控制器但对微处理器或 Linux 不甚了解的嵌入式工程师。我将先简要说明为何要使用嵌入式 Linux,概述应用处理器相关设计的核心要点,再深入分析适合入门级嵌入式 Linux 系统的热门芯片 —— 以及需要避开的型号。
与我之前的微控制器文章一样,本文涵盖的芯片既有过去十年广泛应用的 “经典款”,也有具备新奇功能、值得关注的 “新秀”。如果说微控制器文章的核心观点是 “按需选型,不惧学习新软件生态”,那么本文的主张更简单:只要能在这些芯片上启动 Linux,它们就会成为完全一致的开发环境。
这使得运行嵌入式 Linux 的芯片几乎成为 “大宗商品”:只要处理器满足基本要求,应用代码根本无需关心运行在 ST 还是微芯(Microchip)的芯片上 —— 哪怕其中一款是全新的双核 Cortex-A7,另一款是老旧的 ARM9。I2C 驱动、GPIO 调用,甚至基于 V4L 的图像处理代码,都能无缝运行。
至少,宣传话术是这么说的。但实际启动芯片的过程则完全是另一番光景 —— 这正是本文的核心重点。除了文末的少量基准测试,只要能成功进入 shell 命令行,我们就认为任务完成。
与微控制器评测不同,本次我将重点关注硬件设计:这些芯片的 PCB 设计难度差异极大,这部分内容绝不能省略。为此,我为每款评测的应用处理器都从零设计了开发板 —— 实际上,每款处理器都设计了多款开发板,总计约 25 种不同设计。这让我能够测试不同的 DDR 布局和电源管理方案,同时在过程中修复了一些漏洞。
我特意选择从零设计而非复用他人的 CAD 文件,这样能发现每款 CPU 的潜在 “陷阱”,并优化设计成本和手工焊接流程。每块开发板的设计仅需 1-2 天,采用嘉立创(JLC)的低成本 4 层 PCB 制造服务。
此外,我将板载外设精简到仅满足启动需求的最低限度,因此这些板卡上没有音频编解码器、I2C 传感器或以太网物理层芯片(PHY)。
因此,本次评测使用的开发板,就像高中历史课堂笔记或练习乐器时的录音 —— 仅供后续研究参考。虽然我会展示板卡图片和布局截图以说明特定要点,但这些并非正式的参考设计;评测的核心目的是让你具备独立设计 Linux 开发板的能力。授人以鱼,不如授人以渔,对吧?
微控制器与微处理器的区别
来自微控制器领域的工程师首先会发现:Linux 通常无法运行在 Cortex-M、8051、AVR 等主流微控制器架构上。我们需要使用应用处理器,常见的有 ARM Cortex-A、ARM926EJ-S 以及多款 MIPS 架构衍生产品。
应用处理器与微控制器最核心的区别非常简单:微处理器具备内存管理单元(MMU),而微控制器没有。诚然,无 MMU 也能运行 Linux,但通常不推荐这么做:主频勉强达到 500MHz 的 Cortex-M7 芯片,价格往往是更快的 Cortex-A7 的 2-4 倍,且功耗极高 —— 微控制器采用更大的工艺节点以降低漏电电流。缺乏 MMU 且主频偏低的特性,导致其运行速度十分缓慢。
除 MMU 外,微控制器(MCU)和微处理器(MPU)的界限正逐渐模糊。现代应用处理器通常配备与微控制器类似的外设,而高端 Cortex-M7 微控制器的主频也能达到入门级应用处理器的水平。
为何选择 Linux?
当你的微控制器项目超出 “超级循环 + 中断服务程序(ISR)” 的承载能力时,有许多裸机任务调度内核可供选择 ——FreeRTOS、ThreadX(现为 Azure RTOS)、RT-Thread、μC/OS 等。从学术定义来看,这些都属于操作系统,但与 Linux 相比,更适合将它们视为编写裸机应用的框架。它们提供操作系统的核心组件:线程(及调度器)、信号量、消息传递和事件机制,部分还支持网络、文件系统和其他库。
但运行在应用处理器上的 Linux,与运行实时操作系统(RTOS)的微控制器表现截然不同。(先声明:RTOS 的忠实拥护者请勿反驳 —— 确实存在一些经过大规模验证的 RTOS,专为配备 MMU 的应用处理器设计,例如 RTEMS。它们没有下文讨论的部分限制,在安全关键型实时应用中比 Linux 更具优势。)
动态内存分配
小型微控制器应用通常可通过静态分配满足所有内存需求,但随着应用规模扩大,你会越来越频繁地使用 malloc (),此时各种诡异的漏洞会开始出现。在复杂的长期运行系统中,可能出现 95% 的时间正常运行、却在随机(且通常是关键)时刻崩溃的情况。这些漏洞让最严谨的开发者也束手无策,根据我的经验,它们几乎都源于内存分配问题:要么是内存泄漏(可通过合理调用 free () 修复),要么是更严重的内存碎片(分配器无法找到合适大小的空闲块)。
而支持 Linux 的应用处理器因具备 MMU,*alloc () 调用执行迅速且可靠。物理内存仅在实际访问时才会被预留(缺页中断),Linux 会在后台自动释放和重组内存页,大幅减少内存碎片问题。此外,Linux 提供了更易用的调试工具(如 valgrind),可从源头捕获应用代码中的漏洞。最后,由于应用程序运行在虚拟内存中,即便存在内存漏洞,Linux 也会终止该应用,确保系统其他部分正常运行。(作为最后的权宜之计,可通过超级循环脚本调用应用程序,使其崩溃后自动重启,无需重启整个系统。)
网络与互操作性
在裸机微控制器上,通过 FreeRTOS 运行 lwIP 等协议栈可满足许多简单应用的需求,但实现 HTTP 等应用层网络服务的可靠性颇具挑战。对于桌面程序员来说看似简单的功能 —— 例如支持多并发连接的 WebSocket 服务器 —— 在裸机网络栈中实现却十分棘手。由于 C 语言缺乏对异步调用或异常的良好支持,代码往往充斥着复杂的状态机或多层嵌套分支,调试难度极大。
而 Linux 提供了一流的网络栈,以及大量基于该栈的稳定用户态库,可直接支持应用层网络连接。此外,你还能使用多种高级编程语言,更轻松地处理网络的异步特性。
高速接口(如 USB)在 Linux 环境下的开发效率也高得多,其中最突出的是 USB 主机功能 —— 在 Linux 中,USB 设备通常 “即插即用”。例如,若触摸屏驱动出现故障而半小时后就要演示,你可以临时接入 USB 鼠标应急(我曾遇到过这种情况);若产品需求变更需要添加音频功能,可先使用 20 美元的 USB 声卡,后续再重新设计板卡集成专用音频编解码器。许多无以太网接口的开发板,我也会通过 USB 转网口适配器实现远程文件传输和 GDB 调试。归根结底,嵌入式 Linux 系统与普通电脑的相似度远超你的想象。
安全性
嵌入式设备的安全性通常涉及两个方面:设备安全(确保仅能从验证过的固件启动)和网络安全(身份验证、入侵防御、数据完整性校验等)。
设备安全的核心是 “信任链”:需要引导加载程序(bootloader)读取加密镜像,解密并验证后再执行。引导加载程序和密钥需存储在 ROM 中以防篡改。加密镜像可防止恶意第三方在克隆硬件上安装固件,而 ROM 对镜像的验证机制则能阻止在硬件上运行自定义固件。
网络安全则侧重于减少软件漏洞,并创建可安全执行加密操作的可信执行环境(TEE)。典型场景是使用客户端证书向服务器验证设备身份 —— 若加密哈希运算在安全环境中执行,即便攻击者完全控制了正常执行环境,也无法获取私钥。
在微控制器领域,除非使用较新的 Cortex-M23/M33 内核,否则芯片的安全特性往往杂乱无章,包括硬件加密支持、(众所周知不安全的)闪存读取保护、仅执行内存、写保护、真随机数生成器(TRNG),或许还有内存保护单元(MPU)。尽管厂商可能提供应用笔记或简单示例,但通常需要你自行启用并正确配置这些功能,建立可靠的信任链难度极大,且几乎无法在与系统其他部分隔离的环境中执行加密操作。
本文评测的应用处理器并非都支持安全启动,但该功能更为常见。尽管偶尔仍会披露漏洞,但据我(非专家)观察,其实现比 Cortex-M 系列更为可靠:启动配置数据和密钥存储在一次性可编程(OTP)内存中,非特权代码无法访问。借助 Linux 网络栈和加密支持,网络安全的实现也更成熟、更简便。
文件系统与数据库
若需在重启后保留配置数据,微控制器通常需通过结构体和底层闪存编程代码实现,但如果数据需要追加或任意修改,代码会变得极其繁琐 —— 这正是文件系统(和数据库)存在的意义。虽然有适用于微控制器的嵌入式文件系统库,但它们比 Linux 的文件系统功能简陋得多,且稳定性欠佳,而 Linux 仅需在 menuconfig 中勾选对应选项即可启用完整功能。至于数据库,我从未见过有人在微控制器上真正成功部署数据库,但 Linux 平台上的数据库选择却琳琅满目。
多进程支持
在裸机环境中,系统仅限于单个应用镜像。当应用需要同时执行多项完全不同的任务时,代码会变得十分臃肿。而在 Linux 中,你可以将功能拆分为独立进程,分别开发、调试和部署为单独的二进制镜像。
典型示例是主应用与更新程序的分离:主应用负责设备核心功能,后台服务则每天连接服务器获取主应用的最新版本。这两个程序无需交互,且执行完全不同的任务,拆分为独立进程更为合理。
语言与库支持
裸机微控制器开发主要使用 C 和 C++。虽然有一些有趣的项目尝试在裸机上运行 Python、JavaScript、C#/.NET 等语言,但它们通常仅实现核心语言特性,缺乏与 PC 一致的运行时环境,且语言实现往往不兼容。这意味着你的代码(及所使用的库)必须专门为这些微型实现编写。例如,即便 ESP32 能运行 MicroPython,也无法直接部署 Flask 框架构建 Web 应用服务器。
而切换到嵌入式 Linux 后,你可以使用与 PC 完全相同的编程语言和软件库。
与硬件的严格隔离
在微控制器开发中,你可以在任意位置调用 I2C_SendReceive () 等硬件操作函数。而 Linux 中,用户态调用与底层硬件驱动代码存在严格隔离。这一特性的核心优势是硬件平台迁移极为简便 —— 移植代码时,通常只需修改几行指定新设备名称的代码。
当然,Linux 也支持从用户态操作 GPIO、执行 I2C 事务或发送 SPI 消息,这些工具在诊断和调试时非常有用。此外,若你在微控制器上实现自定义 I2C 外设且配置极少,编写仅用于暴露字符设备并直接转发数据的内核驱动可能显得多余。
但对于市面上常见的显示器、加速度计、惯性测量单元(IMU)、光传感器、压力传感器、温度传感器、模数转换器(ADC)、数模转换器(DAC)等 I2C 或 SPI 外设,Linux 内核已内置相关支持 —— 构建内核时启用对应驱动,并在设备树(DTS)中配置即可。
开发人员可用性与成本
综合上述挑战,编写裸机 C 代码的难度极大(因此成本高昂)。若你希望聘用经验较少的开发人员(例如来自编程培训学校、仅具备基础计算机科学背景的人员),则需要一种更易于开发的架构 —— 尤其是当项目大部分为硬件无关的应用代码,仅小部分涉及底层硬件交互时。
为何不选择 Linux?
当然,构建嵌入式系统时也有充分理由不使用 Linux:
休眠模式功耗
先说说好消息:应用处理器的活跃模式功耗与微控制器相比相当出色。这些芯片通常采用更小的工艺节点,因此每安培电流能提供更高的主频(相比微控制器使用的大工艺节点 Cortex-M 芯片)。但遗憾的是,嵌入式 Linux 设备的电池续航通常以小时或天计算,而非数月或数年。
现代低功耗微控制器的休眠模式电流仅约 1μA—— 这一数值已包含 SRAM 数据保持和低功耗实时时钟(RTC)振荡器运行的功耗,部分产品甚至能维持这种低功耗状态长达十年。
而应用处理器的休眠功耗是微控制器的 300 倍(40nm 工艺的漏电问题终究无法回避),但这与同步动态随机存取存储器(SDRAM)相比仍相形见绌 ——SDRAM 在自刷新模式下的功耗可达 10mA(没错,是毫安,而非微安)甚至更高。当然,你可以采用休眠到闪存(休眠模式),但这仅适用于无需快速唤醒的场景。
即便是苹果这样的公司也无法突破这些基本限制:对比苹果手表(采用应用处理器)18 小时的续航,以及 Pebble 手表(采用 STM32 微控制器,电池容量仅为苹果手表的一半)10 天的续航,差距显而易见。
启动时间
嵌入式 Linux 系统的启动时间通常需要数秒,这与微控制器的启动时间相差一个数量级。公平地说,这一对比并非完全对等:若在微控制器的 RTOS 中初始化大量外部外设、挂载文件系统并启动大型应用,启动时间也可能达到数秒。尽管启动时间受多个组件影响且可优化,但核心限制在于应用处理器无法直接从外部闪存执行代码 —— 必须先将代码复制到 RAM 中(除非运行 XIP 内核)。
响应性
默认情况下,Linux 的调度器和资源管理系统存在无界延迟,在某些极端场景下可能需要很长时间才能响应(甚至完全无响应)。你是否遇到过鼠标随机卡顿 3 秒的情况?嵌入式 Linux 也可能出现类似问题。因此,若使用 Linux 开发呼吸机等设备,需格外谨慎。
为解决这一问题,PREEMPT_RT 补丁已存在多年,它能将 Linux 转变为实时操作系统,调度器可抢占几乎所有任务,确保硬实时任务获得执行机会。
此外,许多人认为需要硬实时内核时,实际上只是希望代码的抖动(jitter)较低。来自微控制器领域的开发者可能认为,1GHz 的处理器应该能稳定生成 50kHz 的方波,但事实并非如此。Linux 调度器的中断抖动约为 ±10μs,而微控制器通常仅为 ±10ns。不过这一问题也有解决方案:Linux 会占用所有常规 ARM 中断向量,但不会触碰快速中断请求(FIQ),因此你可以编写完全运行在内核空间之外的自定义 FIQ 处理程序。
实际上,更常见的做法是将这些实时任务委托给独立的微控制器。本文评测的部分芯片甚至内置了专为控制类任务设计的微控制器协处理器,也可以直接焊接一块 1 美元的微控制器,通过 SPI 或 I2C 与其通信。