嵌入式系统选型
裸机前后台 / FreeRTOS / RT-Thread / Zephyr / 云端 IoT OS / 嵌入式 Linux 六类系统的横向对照 —— 从资源档位、实时性等级、认证周期到授权与工具链的一份选型方法论参考 | 编制日期 2026-10-15 | 版本号 Rev 1.0
做嵌入式方案时,「用哪个操作系统」几乎总是第一个被问出口的问题,也是最容易问错的问题。因为它把一个多变量的决策压缩成了单变量:仿佛只要选定内核,剩下的适配、驱动、中间件、构建、升级、长期维护都是自动发生的。实际上这些环节各自都有独立的决策空间,而且其中大多数比「用哪个内核」更影响项目成败。
这份文档给出的是一条固定的提问顺序:内存档位 → 实时性等级 → 存储与启动方式 → 认证与生命周期 → 生态与团队 → 授权与商业条款。顺序不能颠倒,因为后一问永远依赖前一问的答案:内存档位只有 20 KB 时,讨论 Linux 的驱动模型没有意义;实时性等级还没定义时,比较两个 RTOS 的调度器只是在比运气;授权条款往往是最后一步才生效的那一刀,却常常砍掉一半候选。
围绕这六问,本文把六类系统逐一拆开:它们为哪种资源量级设计、把实时性保证到什么程度、依赖什么样的启动链与驱动模型、生命周期与认证假设是什么。结论不落在「谁最好」,而落在「在什么约束下谁是对的」。
一句话结论: 嵌入式系统选型不是挑「最好的操作系统」,而是按顺序回答六个问题——内存档位、实时性等级、存储与启动方式、认证与生命周期、生态与团队、授权与商业条款。 内存档位是唯一不可协商的第一筛子:它由芯片选型阶段就已经确定,先于任何生态偏好与个人熟悉度,先把装不下的系统全部排除,而不是留给「后面再优化」。 实时性等级必须先定义再选系统:软实时产品被硬实时方案绑死是最贵的一种误判——多付出资源与复杂度,换回的是自己根本不需要的确定性保证。 授权与工具链不是软肋而是硬约束:它与技术能力无关,却常常在最后一步砍掉一半候选,让前面所有技术比较变成背景资料。 而答案本身正在变化:MCU 管实时 + MPU 管连接与界面 的混合架构正在成为主流回答,而不是过去的妥协方案。
读者路径
- 刚开始做方案,还没定内核:先看第 2 章确认候选集合,再看第 10 章(横向对照 + 决策树 + 检查清单)。
- 手上只有几十 KB RAM:第 2.1 节定档位,然后在第 3 章(裸机)与第 4 章(FreeRTOS)之间二选一,重点是实时性等级怎么定。
- 要过车规、医疗或工业认证:第 2.4 节先确认认证与生命周期要求,它会直接改变技术路线的判断顺序。
- 做的是联网设备,要接云:第 7 章单独看安全启动、设备身份与批量升级这三条。
- 在用 RTOS,想评估要不要上 Linux:第 8 章到第 9 章,很多产品的答案是「两颗芯片各管一段」,而不是「换掉原来那颗」。
- 只想要速查:附录 B + 第 12 章就够了;受授权条款限制的直接看第 10.4 节。
1. 嵌入式系统选型的坐标系
这一章不比较任何具体系统,只先把坐标系立起来:嵌入式系统到底由哪些环节组成、每个环节各自有哪些可选空间、以及为什么「选操作系统」只是其中一环。很多选型争论谈不拢,是因为双方对「选型对象」的边界理解不同——一方认为选定内核就定了一半,另一方知道真正的工作量分布在哪里。先对齐边界,后面的比较才有意义。
1.1 什么才算「嵌入式系统」:从复位到应用运行的完整链路
「嵌入式系统」常被用得很随意:有人把「没有操作系统的单片机固件」叫嵌入式系统,有人把跑在工业网关上的完整 Linux 也叫嵌入式系统。这些说法混淆了规模与责任两件事。更有用的定义是:嵌入式系统是一条从复位到应用运行的完整链路,这条链路上每一个环节都由设计者负责,没有哪一段是「免费的」。一块只有 8 KB Flash 的小单片机同样要面对完整的七段链路,只是每一段的实现都可能极其简单。
- 复位与启动源:取指入口在哪里、上电初始时钟与外设状态是什么。基本由硬件决定,但决定了后面所有环节的起点约束——串口启动、调试口启动、独立看门狗启动会带来完全不同的软件结构。
- 启动介质:镜像放在片内 NOR、外部 SPI NOR、外部 SPI NAND、eMMC 还是 SD 卡。它在硬件方案阶段就已定型,却对系统的裁剪方式影响极大。
- 引导阶段:自举、时钟树就绪、镜像搬运与校验、多镜像选择与回滚。它在裸机产品里是手写代码,在嵌入式 Linux 里是引导程序。它常被当成「不需要设计」的一段,却往往是启动时间与升级可靠性的大头。
- 内核与 RTOS:调度模型、内存管理、同步原语、中间件的组织方式。这是通常所说的「操作系统」,也是第 3 章到第 8 章逐个展开的对象。
- 驱动层:外设怎么访问、驱动怎么组织、与硬件抽象的边界画在哪里。这一段的差异经常比内核之间的差异更大——同一个 RTOS 配上不同的驱动模型,工程量可以差出好几倍。
- 中间件:协议栈、文件系统、图形、加密、OTA 框架、日志。在 MCU 产品上,中间件的代码量常常超过内核本身;内核上省下的每一 KB RAM,往往在这里被某个协议栈吃回去。
- 应用与工具链:业务逻辑、调试手段、构建与烧录流程。工具链不产生功能,但它决定了这套系统能不能被团队高效地修改、复现问题与批量生产。
把七段串起来看,选型的真实形状就清楚了:它是一条链路,不是一次选择。任何一环的选择都会改变其余各环的可行方案,而大部分人只对其中一环(通常是第四环)有完整的直觉。
| 环节 | 这一段做什么 | 通常由谁决定 | 选型时最常见的误判 |
|---|---|---|---|
| 复位与启动源 | 取指入口与上电初始状态 | 硬件方案 | 以为上电即可运行,忽略启动模式与看门狗 |
| 启动介质 | 存放固件镜像并提供读取路径 | 硬件与 BOM | 只按容量选型,忽略读取方式与启动时延 |
| 引导阶段 | 自举、时钟就绪、镜像搬运与校验 | BSP 与工具链 | 当成不需设计的一段,忽略启动耗时与回滚 |
| 内核与 RTOS | 调度、内存管理、同步原语 | 操作系统选型 | 把整条链路简化为一个变量 |
| 驱动层 | 外设访问方式与驱动组织 | BSP 与驱动团队 | 默认换内核就能复用原有驱动 |
| 中间件 | 协议栈、文件系统、加密、升级 | 组件选型 | 只算内核 RAM,不算中间件占用 |
| 应用与工具链 | 业务逻辑、调试、构建与烧录 | 团队与流程 | 忽视可复现性与产线一致性 |
1.2 选型要回答的六个问题与它们的回答顺序
把上面七个环节收敛一下,一次完整的系统选型只需要回答六个问题。它们不是六个并列的评分项,而是一条单向收敛的链路。
第一问:内存档位。这块板子能给到多少 RAM?几 KB、十几 KB、几十 KB,还是几百 KB 以上?这是唯一一道不可协商的筛子——它由芯片选型阶段确定,不受偏好影响,而且能一次性排除掉大批候选。
第二问:实时性等级。需要软实时(响应时间有统计意义上的上界,超了不会造成损失)、硬实时(每个截止期必须满足,超了就是故障),还是微秒级的超低延迟回路?这一问必须在选系统之前回答,因为它是需求,不是结果。
第三问:存储与启动方式。镜像放在片内 NOR 还是外部介质?原地执行还是先拷贝到 RAM 再执行?需要多大可写空间?这一问决定了系统的最小配置,也决定了很多功能的可行性。
第四问:认证与生命周期要求。要不要过车规、医疗或工业认证?要在现场运行多少年?它决定了一个系统能不能在十年后仍然拿到缺陷修复,而这件事在立项时几乎总被跳过。
第五问:生态与团队。现有团队已经掌握什么?谁来实现驱动、中间件与升级框架?第六问:商业条款。授权形态能否接受?长期维护承诺由谁承担?后两问与技术能力关联较弱,却常常是最后生效的约束。
顺序的价值在于收敛速度。内存档位一问就能砍掉大半候选;反过来,先挑「我们熟悉的那个内核」,再去论证它能装进 20 KB RAM,成功率远低于先把 20 KB 这个事实确定下来。
三处依赖关系是「顺序不可颠倒」的全部理由:
- 资源档位先于生态。生态的价值是「让合适的人做合适的事」,前提是系统能被装进去。装不进去的系统,生态再好也是零。
- 实时性等级先于系统。它是产品需求,不是系统属性。先选系统再补实时性需求,等于用「有没有这个特性」代替「要不要这个特性」,最后往往是买了一个更贵更复杂的方案去满足一个本来不必满足的指标。
- 认证周期先于技术偏好。社区驱动的项目可能功能最强,但拿不出十年支持承诺;有商业支持的项目功能平庸,却能陪产品走完全生命周期。这不构成优劣之分,只构成匹配与不匹配。
1.3 本文覆盖的六类系统与各自定位
本文横向对照六类系统。它们不是同一类东西的排名:有的是一套编程模型,有的是一个小内核,有的是把连接协议栈与云接入做成基础设施的平台。把它们放在一起对照之前,先明确各自站在什么位置、由谁在什么约束下使用。
裸机前后台(第 3 章)是一套编程模型而不是操作系统:中断负责采集与置位标志,主循环负责处理。它的全部复杂度被压缩在「中断与主循环怎么分」这一个设计决策上,资源占用最小,可预测性上限最高,但并发结构要自己设计。
FreeRTOS(第 4 章)是通用轻量实时内核,以极小的资源占用提供抢占式多任务、队列、信号量与软件定时器,是闭源 MCU 产品最常见的默认候选。它的取向是「提供最小内核,把设备模型留给上层」。
RT-Thread(第 5 章)在轻量内核之上提供了较完整的对象模型与设备框架,并配有本土化的软件包管理与配置工具。它的价值在于「组件齐全」,代价是占用比纯内核大。
Zephyr(第 6 章)代表另一种路线:用配置驱动开发方式(Kconfig、devicetree、west 三件套)与分层驱动模型换取大规模工程的可重复性,适合团队大、外设多、产品线长的场景。
AliOS Things 与云端 IoT 操作系统(第 7 章)把连接、设备身份、安全启动与批量升级当成一等公民,操作系统只是这套云端管理体系的一层,服务的是「设备要被远程管起来」的场景。
嵌入式 Linux(第 8 章)是网络、存储、多进程与人机界面的默认答案,它的问题永远是资源与启动时间。第 9 章的MCU 与 MPU 混合架构回答了更实用的问题:一颗芯片装不下全部需求时,是不是必须做减法——答案是可以分工,不必取舍。
这六类系统之间不存在普遍意义上的优劣排序。资源占用最小的不等于最可靠的,功能最强的也不等于最省心的。只有在给定约束之后,「哪一个装得下并且撑得住」这个问题才有答案——图中按资源档位排列,正是因为资源档位是唯一可以从硬件方案直接读出的客观量。
1.4 常见误判:把「操作系统」当成唯一变量
下面四个误判在现场出现的频率最高,它们的共同点是都用一个简化模型替代了真实链路。
误判一:选定内核等于完成选型。项目初期花两周时间论证内核选型,然后在接下来的半年里被启动代码、驱动适配、中间件移植、工具链搭建、产线烧录流程一件一件卡住。内核只占七段链路里的一段,而且往往不是工作量最大的一段。
误判二:换个内核,原有驱动就能复用。驱动能不能复用,取决于驱动依赖的硬件抽象层而不是内核本身。同一个外设寄存器,在两套不同的驱动模型下代码几乎要重写。如果「原有驱动能复用」是选型理由之一,这条理由必须在实验室里被验证,而不是在会议室里被假设。
误判三:上了操作系统就一定更省事。很多人是从裸机转过来的,觉得加了内核就不用再操心并发与时序。实际上操作系统只是把并发结构显式化了:裸机下的隐式约定(谁在什么时候改哪个变量)变成了任务与临界区,而临界区写错的后果比裸机更严重,因为多了一个抢占点。
误判四:功能最强的系统最省事。在资源足够的前提下这通常成立,但在资源紧张时恰好相反——一个完整系统需要更多内存来支撑自身的通用机制,从而挤压业务代码的空间。在 20 KB RAM 的约束下,最合适的系统往往不是功能最多的那个,而是刚好够用的那个。
| 选型变量 | 由谁决定 | 换内核时是否要重做 | 最常见的错误假设 |
|---|---|---|---|
| 内存档位 | 芯片选型 | 不用重做,但无法绕过 | 以为可以在驱动开发阶段再挤出来 |
| 启动链与镜像格式 | BSP 与工具链 | 基本要重做 | 以为引导代码可以照搬 |
| 驱动模型与外设抽象 | 驱动团队 | 基本要重做 | 以为寄存器访问代码可以复用 |
| 中间件组合 | 组件选型 | 部分可复用 | 只算内核占用,漏算协议栈与加密 |
| 构建与配置体系 | 工具链 | 要重新学 | 以为换系统只是换个编译选项 |
| 调试与量产流程 | 团队与产线 | 要重新建 | 忽略批量生产的一致性要求 |
假设只回答「用哪个内核」,那么下面这些问题的答案仍然是空的:引导代码谁写、多镜像选择怎么做、外设抽象层按什么模型组织、协议栈从哪来、加密模块放在哪个安全边界内、产线怎么批量烧写与追溯、三年后谁来修缺陷。
这些问题里,任何一项处理不当都足以让项目失败,而且失败时通常已经被归因成「操作系统选错了」——真正的根因在别处。所以正确做法是把六个问题都问完再定内核;反过来,如果把启动链、驱动模型、中间件与工具链都当成显式的选型对象,很多问题会在方案阶段就暴露出来,成本远低于在样机阶段发现。
1.5 本文主线与读者路径
全文的组织方式就是那六问的顺序。第 1 章立坐标系;第 2 章把前四问讲完并给出候选矩阵;第 3 章到第 8 章逐个展开六类系统;第 9 章处理单颗芯片装不下时的分工问题;第 10 章做横向对照与决策;第 11 章讲从选型到落地的移植与验证;第 12 章给结论与速查。
需要强调的是:第 3 章到第 8 章应当被当作参考资料,而不是推荐序列。正确用法是先用第 2 章把候选收窄到两三个,再到对应章节读那几个系统的细节;把六章都读完再做决定,只会让每个系统都形成一点印象,然后回到起点重新纠结。
| 顺序 | 要回答的问题 | 判定内容 | 答错的后果 | 对应章节 |
|---|---|---|---|---|
| 第一问 | 内存档位 | 可用 RAM 落在哪一档 | 在装不下的系统里做优化 | 第 2.1 节 |
| 第二问 | 实时性等级 | 软实时、硬实时或微秒级回路 | 买来用不上的确定性保证 | 第 2.3 节 |
| 第三问 | 存储与启动方式 | 启动介质与执行方式 | 启动时间与可维护性失控 | 第 2.2 节 |
| 第四问 | 认证与生命周期 | 认证种类与支持年限 | 产品无法交付或无法维护 | 第 2.4 节 |
| 第五问 | 生态与团队 | 团队能力与工具链可得性 | 理论最优,工程最差 | 第 10.5 节 |
| 第六问 | 商业条款 | 授权形态与支持周期 | 最后一步砍掉一半候选 | 第 10.4 节 |
结论:嵌入式系统是一条从复位到应用运行的链路,选型对象是这条链路上的七个环节,而不是其中一个。把链路简化成「选内核」会让大量决策变成隐式的,而这些隐式决策往往要到项目后期才被迫显式化,此时改动成本最高。
这一章没有回答「到底该用几 KB 的内存」与「实时性该定在哪一级」,那取决于业务逻辑规模、协议栈选择与产品定义,属于项目输入而不是方法论输出;也没有回答「生态与团队」「商业条款」两问,它们与技术能力关联较弱,同样放在第 10 章展开。
2. 第一性问题:资源与实时性决定候选集合
这一章确立全文前四道筛子:内存档位、存储与启动方式、实时性等级、认证与生命周期要求。它们的共同特点是——都在选系统之前就已经由硬件方案或产品定义确定,因此属于「约束」而不是「偏好」。这一章要说明的不是「哪个系统更强」,而是这四类约束各自排除了什么,以及为什么顺序错了会导致整个方案返工。
2.1 内存档位与候选集合
内存是嵌入式系统里最硬的约束。它由芯片选型阶段就已经确定,而且几乎不可能靠软件优化来翻盘——一块已经流出去的板子,RAM 容量就固定在那里。经验上可以分成五档,分档的意义不在于精确到 KB,而在于每档大致只能装下哪一类系统。
- 几 KB 到十几 KB 档:能装的只有裸机前后台,以及裁剪到极限的最小内核。关键动作是控制栈深度与所有缓冲区,因为在没有堆内存管理的前提下,一个深一点的调用链就会吃掉相当可观的空间。
- 十几 KB 到几十 KB 档:轻量 RTOS 的舒适区。内核、几个任务栈与一个精简协议栈可以共存,但图形界面与复杂中间件放不下。这是闭源 MCU 产品最常见的位置,也是第 4 章讨论的主要场景。
- 几十 KB 到几百 KB 档:可以容纳完整 RTOS 加协议栈、文件系统、加密组件,或者裁剪过的嵌入式 Linux 加基础网络栈。这个档位开始出现「系统选得对但配置没做对」的问题——功能都能编译进去,运行时却不够用。
- 几百 KB 到几 MB 档:嵌入式 Linux 与云端 IoT 系统的入口区间。开始需要考虑存储布局、根文件系统与镜像压缩,而不是只考虑裁剪代码。
- 十 MB 量级以上:完整 Linux 加图形界面与多进程应用,通常伴随 MPU 而非 MCU。
除了总量,内存的形态也很重要:是 SRAM 还是 DRAM,是否需要外部刷新,缓存有多大,是否有 DMA 可用区段。这些决定了某些架构(例如需要大块连续内存做帧缓冲的图形方案)是否可行。可用 RAM 与可用堆上限以芯片手册与系统配置为准。
最后提醒一个反复出现的错误:按内核的标称占用做估算。真正吃掉内存的通常是任务栈集合、协议栈收发缓冲、加密与证书缓冲、日志缓冲、升级用的镜像缓冲区。把这些算进预算之后,原本「刚好够用」的方案往往就变成不够用了。
| RAM 档位 | 典型可用量级 | 能装下的系统 | 这个档位的真正瓶颈 | 设计动作重点 |
|---|---|---|---|---|
| 极小档 | 几 KB 到十几 KB | 裸机前后台、极限裁剪内核 | 栈深度与静态缓冲区 | 不引入动态分配,静态规划内存 |
| 小档 | 十几 KB 到几十 KB | 轻量 RTOS 加精简协议栈 | 任务栈总量与并发数量 | 控制任务数,协议栈按需裁剪 |
| 中档 | 几十 KB 到几百 KB | 完整 RTOS 加中间件组合 | 缓冲与碎片,而非内核 | 统一内存池,避免堆碎片 |
| 大档 | 几百 KB 到几 MB | 嵌入式 Linux、云端 IoT 系统 | 镜像体积与启动时间 | 裁剪内核与根文件系统,规划存储布局 |
| 完整档 | 十 MB 量级以上 | 完整 Linux 加图形与多进程 | 启动时间与热设计 | 分层启动、镜像压缩、界面规划 |
这个顺序的诱惑在于它符合直觉:内核名单比硬件 BOM 更容易讨论。但它的失败模式是系统性的。
第一,缺口会在多个环节同时出现。内核省下的内存往往被协议栈缓冲、任务栈、升级镜像缓冲吃掉,最后表现为「明明每项都不大,运行时就是不够」。第二,内存不足的表现通常不是崩溃,而是行为退化:动态分配失败被忽略、缓冲区静默截断、任务栈溢出导致难以复现的随机故障,排查成本远高于启动即失败。
第三,约束一旦变化,代价极高。产品后期要升级协议、增加功能或延长生命周期时,内存档位不能改,只能回头削减已经写好的业务逻辑。正确顺序是:先确定 RAM 总量,把中间件与业务预算一起算进去,再在这个预算里挑系统。内存档位是筛选器,不是优化项。
2.2 存储介质与启动方式
第二道筛子是存储与启动方式。它常被当成「BOM 上的一个容量参数」,实际上它决定了系统架构中相当一部分结构性选择。
第一条分野是原地执行与拷贝执行。片内 NOR 与外部 NOR 支持按地址直接读取指令,代码可以原地执行;外部 SPI NAND、SD 卡与 eMMC 虽然也能寻址读取,但直接执行时的取指效率通常不足以支撑正常运行,因此普遍采用引导程序把内核与关键数据拷贝到 RAM 再执行。拷贝执行需要为镜像额外预留一块内存,并把启动时间拉长到与镜像大小成正比。
第二条分野是介质的读写特性。片内 NOR 容量小但读取快、寿命高、坏块少,适合放代码与关键数据;SPI NAND 容量大、单位成本低,但有坏块管理与磨损均衡的需求;eMMC 与 SD 卡在接口与可靠性上更接近工业级存储,但引脚与功耗成本更高。选介质不只是选容量,它决定了你需不需要自己处理坏块、需不需要做磨损均衡、需不需要设计双备份。
第三条分野是可写空间与只读内容的比例。如果镜像可以整体放在 NOR 上并只读挂载,升级只需替换一块区域;如果运行时需要大量可写空间,就必须规划可写分区,而可写分区的介质特性又会反过来影响应用层怎么用存储。
结论是:介质与启动方式在很多情况下直接决定系统形态。只有 512 KB 外部 SPI NAND 的板子,引导程序、固件、可写数据三者的分配比例就要反复权衡;只有片内 NOR 的板子,升级往往必须依赖外部存储或双镜像机制。
| 启动方式 | 典型介质 | 资源代价 | 主要收益 | 带来的结构性问题 |
|---|---|---|---|---|
| 片内原地执行 | 片内 NOR | 启动最快,RAM 占用最低 | 启动确定、代码无需搬移 | 容量受限,升级需额外机制 |
| 外部 NOR 原地执行 | 外部 SPI NOR | 启动快,RAM 占用低 | 容量可扩展,调试方便 | 需处理总线占用与驱动就绪 |
| 引导后拷贝到 RAM | SPI NAND、SD 卡 | 启动时间随镜像增大 | 取指效率高,介质选择自由 | 需预留镜像大小的 RAM |
| 完整 Linux 引导链 | eMMC、SD 卡或 NAND 加逻辑卷 | 引导链最复杂 | 文件系统灵活、可动态加载 | 引导程序、内核、根文件系统三段配置 |
2.3 实时性等级与调度模型要求
第三道筛子往往是文档里写得最少、在现场出问题最多的:实时性等级。它的核心要求是先定义再选型——它属于产品需求,不属于系统属性。
- 软实时:响应时间有统计意义上的上界,偶尔超出不会造成功能失效或安全问题。人机交互、网络收发、采样上报大多属于这一档。绝大多数 RTOS 与 Linux 都能满足,因此它不能用来区分系统,只能用来排除明显不合适的方案。
- 硬实时:每个截止期都必须满足,超出即视为故障。电机控制、功率电子、安全联锁属于这一档。它要求调度模型本身提供确定性保证,并且要求应用不引入不可控的长临界区。
- 微秒级超低延迟回路:延迟低到需要绕过内核与驱动栈直接处理,通常配合专用硬件或可编程逻辑。这一档在很多情况下已经不属于操作系统选型范畴,而属于「哪些部分交给硬件、哪些交给软件」的分工决策。
这一问最容易出的错,是把需求当成结果来验证。典型流程是:先选定功能最强的系统,然后在项目后期被问到「能不能保证这个周期」,团队再去补实时性配置甚至补实时补丁。这个流程的问题是它把「有没有这个特性」当成了「要不要这个特性」的答案。正确做法是在选型前先问:产品的截止期是多少、超时的后果是什么、有没有哪一部分绝对不能被调度抖动影响。
另一个常见方向性错误是往高了买:软实时产品选了硬实时方案。多付出的资源与复杂度换回来的是用不上的确定性保证,而这份开销会一直体现在 RAM、代码量与移植难度上。软实时产品被硬实时方案绑死,是嵌入式选型里最贵的一种误判。
还有一个结构性问题是调度模型是否匹配产品结构:如果有明确的周期性任务与偶发任务之分,固定优先级抢占通常够用;如果大量任务共享同等重要度,优先级反转与抖动会明显得多。这些结构特征在选型之前就应当从产品功能列表里读出来。
| 实时性等级 | 典型截止期量级 | 典型应用 | 对调度模型的要求 | 选型上的含义 |
|---|---|---|---|---|
| 软实时 | 毫秒到几十毫秒 | 交互、网络、采样上报 | 抢占式优先级即可满足 | 不能用来区分系统,只能排除不合适的 |
| 硬实时 | 百微秒到毫秒 | 电机控制、功率电子、安全联锁 | 确定性调度、抖动上界可分析 | 需评估调度模型而非只看重功能 |
| 超低延迟 | 微秒及以下 | 高速采样、直通回路 | 常需绕过内核与驱动栈 | 很多情况下已不是操作系统选型问题 |
实时性问题之所以难挽回,是因为它的根因通常不在「系统慢」,而在于系统架构允许了一条不守规矩的路径:一个在软实时假设下写成的临界区、一段没有超时机制的总线等待、一个在中断里做了太多事的外设驱动。这些在软实时产品里完全正常,放在硬实时要求下就是缺陷。
等到发现问题时,改动通常需要重构任务划分、调整驱动模型、甚至重新划分硬件职责。可操作的做法是:在选型阶段就用一句话写出实时性等级与最紧的那条截止期,并把它作为淘汰候选的硬条件,而不是等到性能测试阶段才发现不满足。
2.4 认证与生命周期要求
第四道筛子与前面三道性质不同:它不影响系统能不能跑起来,只影响系统能不能被交付和被维护下去。因此它经常被推迟到立项之后、认证之前才被提起,而那时能改的余地已经很小。生命周期要求的强度大致分成两档:
- 长周期支持档:车规、工业、医疗类产品往往要在现场运行十年以上,并要求在停产之后仍能获得备件与安全修复。对应的要求包括:组件有明确的维护承诺与版本支持策略、缺陷响应有约定、关键设计决策可追溯。这一档会直接把一批社区驱动、无长期维护承诺的项目排除。
- 快速迭代档:面向互联网的物联产品迭代周期以周或月计,希望每次发版都能带上新功能与新协议支持。对应的偏好是生态活跃、组件更新快。这一档同样会把那些为长周期设计、变更流程严格的项目排除——不是因为它们不好,而是因为速度不匹配。
这两个档位在技术上并不互斥,但在工程实践上很难由同一个选择同时满足:一个为十年生命周期设计的系统,变更流程通常意味着更长的验证周期;而一个为快速迭代设计的系统,很难承诺十年期的缺陷修复与安全更新。
认证还会影响另外两件事。一是可验证性:认证要求关键模块有明确边界、可独立测试、冻结后不再随意变更,这意味着架构上要预留隔离层。二是安全边界的划分:密钥存储、可信启动、调试口管控往往需要硬件与系统能力配合,选型阶段就要确认,而不是认证阶段再补。
需要明确的是:认证门槛不构成对系统的优劣评价,它只回答「能不能进入这个市场」。一个系统在某个维度上完全满足要求但拿不到某项认证,仍然可能是该项目的正确选择。
| 约束类型 | 典型时长 | 对系统的要求 | 会排除掉的方向 | 必须在选型时确认 |
|---|---|---|---|---|
| 长周期工业 | 十年量级 | 维护承诺、版本支持、缺陷响应约定 | 无长期维护承诺的社区项目 | 维护策略与可获得的时间跨度 |
| 车规 | 十年量级加安全要求 | 可验证边界、变更控制、可追溯性 | 变更流程过于随意的方案 | 关键模块的隔离与冻结策略 |
| 医疗 | 十年量级加合规要求 | 可审计、可追溯、配置受控 | 配置项过多的通用系统 | 配置与版本的可追溯手段 |
| 快速迭代 | 周或月级 | 生态活跃、组件更新快、协议跟进及时 | 流程严格、变更缓慢的系统 | 新协议与新硬件的支持节奏 |
2.5 按约束收窄后的候选矩阵
把前四道筛子依次应用到六类系统上,可以得到一张候选矩阵。矩阵里的字不是分数,而是「在给定约束下是否结构上可行」:填「可行」表示这一组合不违反约束但不表示最优;填「不可行」表示必须排除;填「需评估」表示该项需要看具体应用才能判断。
| 约束组合 | 裸机前后台 | 轻量 RTOS | 组件化 RTOS | 云端 IoT OS | 嵌入式 Linux |
|---|---|---|---|---|---|
| RAM 几 KB 到十几 KB | 可行 | 极限裁剪 | 不可行 | 不可行 | 不可行 |
| RAM 十几 KB 到几十 KB | 可行 | 可行 | 可行 | 不可行 | 不可行 |
| RAM 几百 KB 以上 | 可行 | 可行 | 可行 | 可行 | 可行 |
| 软实时要求 | 可行 | 可行 | 可行 | 可行 | 需评估 |
| 硬实时要求 | 可行 | 需评估 | 需评估 | 需评估 | 需补实时扩展 |
| 微秒级超低延迟回路 | 可行 | 需评估 | 需评估 | 不可行 | 不可行 |
| 片内 NOR 启动 | 可行 | 可行 | 可行 | 需确认 | 不可行 |
| 外部存储拷贝执行 | 可行 | 可行 | 可行 | 可行 | 可行 |
| 需要图形界面 | 不可行 | 需外部库 | 需外部库 | 需外部库 | 可行 |
| 长周期支持要求 | 自主可控 | 看维护方 | 看维护方 | 看维护方 | 看发行版 |
| 快速迭代要求 | 自主可控 | 生态活跃 | 生态活跃 | 生态活跃 | 生态活跃 |
| 多进程与隔离要求 | 不可行 | 不可行 | 不可行 | 不可行 | 可行 |
这张矩阵的用法是三步:先按 RAM 行删行(把不符合的行整行丢掉),再按认证行确认产品归属,最后在剩下的两到三个系统之间做技术对照。如果做完这三步,剩下的候选仍多于三个,说明约束登记得不够细——多半是「实时性等级」那一栏还写着模糊的描述,而不是写着明确的截止期。矩阵里大量「需评估」也是有意保留的:这些格子的判断依赖具体应用,硬实时是否被满足取决于任务划分与驱动设计,长周期支持能否满足取决于具体维护方的承诺。遇到这类格子,正确做法是带着明确的问题去读对应章节与官方文档。
结论:四道筛子把六类系统收敛到等量候选。内存档位是第一筛子且不可协商;实时性等级必须先定义再选系统;存储与启动方式在很多情况下直接决定系统形态;认证与生命周期决定系统能不能被交付与维护。
本章没有回答「生态与团队」与「商业条款」两问,它们与技术能力关联较弱却常常是最终生效的约束,放在第 10 章第 10.4 与 10.5 节;也没有回答「选定了之后怎么落地」,那集中在第 11 章。
最后一件事没回答:单颗芯片装不下时怎么办。本章的思路是「收窄候选」,但如果所有筛子都过完仍然装不下,说明需要改变分工方式而不是继续收窄——这正是第 9 章 MCU 与 MPU 混合架构要处理的问题。
3. 裸机与前后台系统
裸机与前后台不是「没赶上时代」的写法,而是资源占用与行为确定性被同时放到最大化时的唯一形态。本章把超级循环与时间触发两条实现路线讲透,并说明为什么它的扩展性上限由最长一次循环决定、而不是由平均负载决定。本章的核心判断是:裸机的可预测性不是免费的,它是用「扩展性」换来的;换得值不值,取决于模块数量与截止期分布,而不是取决于「裸机落后」这个印象。
3.1 设计目标与适用边界
裸机把两件事同时做到最大:资源占用最小与行为最可预测。没有调度就没有调度开销,没有任务就没有任务栈,栈的峰值可以在编译期精确算出;控制流完全由代码顺序决定,运行期不存在任何「谁来决定下一步」的分支。这三件事同时成立的系统形态只有一种,它是资源紧张、启动时间敏感、需要给出确定性论证这几类约束叠加时的天然解。
但裸机的可预测性有隐含前提,而且这三个前提必须同时成立,否则这套写法会立刻崩掉:
- 模块数量足够少(几十个量级以内),且循环周期短到可以整体估算出。
- 实时要求可以用「最坏一轮」来表达,而不是用「截止期分布」来表达;如果不同模块的截止期相差一个数量级且都要求准点,保守上界会大到无法接受。
- 团队愿意用编码规范与评审替代工具提供的隔离。裸机没有强制隔离,一个模块越界写全局就会破坏别的模块,全靠人守。
据此可以把适用边界说得更具体。适合裸机的典型场景是:主控上留给系统的 RAM 只有几 KB、Flash 里业务代码已经占满、需求在两三年内不会大改、模块之间本来就是单向的数据流(采集 → 计算 → 输出),以及需要通过功能性安全审查的场合。不适合的典型场景是:模块持续增加、每个模块截止期差异大、需要动态装载或用户态隔离、团队规模大到无法靠评审保证规范。
因此本文不把裸机放在「先进 / 落后」的轴上讨论。第 2 章按六个问题收窄出的候选集合里,裸机通常落在「资源档位最低 + 确定性要求最高 + 认证要求严」这一格上;只要这三个条件同时成立,裸机就是这一格里的最优解,而不是权衡之后的选择。
| 资源与实时档位 | 主要约束 | 候选形态 | 选它的决定性理由 |
|---|---|---|---|
| 内存极紧,Flash 也要留给业务 | 系统自身只能占极小份额 | 裸机超级循环 | 无内核镜像、无任务栈、无堆,占用是唯一可选的最小值 |
| 需要确定性上界,且要写进论证材料 | 上界必须能被静态算出并复核 | 裸机或前后台 | 没有调度决策,上界等于各模块最坏耗时之和 |
| 模块并发增加,截止期差异明显 | 保守上界已超出可接受范围 | 前后台加时间触发 | 把「谁在什么时候跑」从运行时决策改为静态表 |
| 需要任务隔离与明确的时序关系 | 一个模块不能拖垮另一个模块 | 极小内核 RTOS | 队列与优先级把隐式耦合变成显式、可检查的对象 |
| 需要协议栈、文件系统与多组件复用 | 自研组件成本高于系统占用成本 | 组件丰富的 RTOS | 复用现成组件比压缩占用更划算 |
| 需要用户态隔离、动态装载与复杂应用 | 进程隔离的收益超过内存代价 | 嵌入式 Linux 一类 | 地址空间隔离与生态是这类需求的前置条件 |
3.2 超级循环模型与时间触发
超级循环(superloop)的结构非常朴素:while(1) 里依次跑各模块,中断负责采样并置标志,主循环负责按标志处理。模块之间靠共享变量与标志位耦合——A 模块写一个全局标志,B 模块读到它并据此更新输出。它的可扩展性上限由最长一次循环决定,而不是由平均负载决定,这是本章最重要的一条。
设第 i 个模块的最坏耗时为 Ti,那么循环周期上界就是这些 Ti 的和再加上轮询本身的开销。注意这里取的是最坏值:所有标志同时置位、每个模块都走最长的那个分支、缓存未命中、Flash 正在忙、串口发送队列正好是满的。只要这个和超过了最短的那个截止期,实时性就已经破了——而此时 CPU 占用率可能只有三成。这就是「平均负载明明很低,却仍然声称不满足实时性」的根因,也是裸机项目最常见的验收争议。
更麻烦的是这个和会随迭代单调增长:日志模块加了几行格式化、滤波从一阶改成八阶、协议解析多了一个分支,每次迭代都在把 Ti 往上推,而截止期是由产品需求定死的常量。于是扩展性天花板变成一个可以提前算出来的数字:在项目的某个迭代上,各模块最坏耗时之和必然越过最短截止期,那时唯一的出路是重构,不能靠调参解决。
时间触发是针对这一问题的直接修正:用一张静态调度表替代动态轮询,表里每格写清「哪个任务、跑多长时间」,到点由一个定时器中断把它拉起来。它的全部收益与全部代价都来自同一个改变——「谁在什么时候跑」从运行时的决策变成了编译期的决定。
- 收益一:可预测性提升。每个任务的时间窗是固定的,冲突(任务窗重叠、共享资源被两个窗同时占用)可以在设计阶段静态检出,而不是等到现场偶发。
- 收益二:模块增删变成设计动作。新模块要占一个时间窗,加不进去就是明确的「没有余量了」,而不是「响应好像变慢了」。
- 代价一:灵活性下降。临时插入一段耗时逻辑必须改表并重新分析,表也从此成为一份必须与代码同步维护的资产。
- 代价二:设计成本上移。需要有人先把各任务的时间预算分配出来,这件事在项目早期往往拿不到可靠数据。
实践中最常见的是混用形态:主循环仍按固定顺序跑常规模块以保持简单,同时用定时器中断在固定节拍上抢修那一两件必须准点的事。这个折中保留了大部分可预测性,又不必为全部模块做时间预算。
| 对比项 | 动态轮询(超级循环) | 时间触发(静态调度表) |
|---|---|---|
| 谁在什么时候跑 | 运行时由代码顺序决定 | 编译期由调度表决定 |
| 时序可预测性 | 上界等于各模块最坏耗时之和 | 每格时间固定,冲突可静态检出 |
| 可扩展性上限 | 最长一轮的耗时,受模块复杂度推高 | 时间预算的剩余量,冲突即超支 |
| 新增一个模块 | 加进循环即可,但要重估总耗时 | 改调度表并重新做冲突分析 |
| 删掉一个模块 | 从循环里去掉,顺序自动前移 | 从调度表里删格,注意余量的重新分配 |
| 灵活性 | 高,临时插逻辑代价小 | 低,临时插逻辑要改表并复核 |
| 维护成本落在哪 | 落在代码本身 | 落在代码与调度表的一致性上 |
不要写「循环周期约 5 ms」这样的结论。把它拆成可以复核的三步:
- 第一步,逐模块取最坏路径的耗时,而不是典型路径。测法是让所有标志同时置位、每个模块都触发它最长的那个分支(错误重试、超长报文、缓冲区满)。
- 第二步,加上轮询开销:把「判断标志是否置位」这件事本身的开销也计入。模块多时这项不可忽略。
- 第三步,把和与最短截止期比,并留出余量。余量系数需要在评审里被明确同意,不能由开发口头给出。
这套估算同时也是一道很有价值的体检题:如果某个模块的最坏耗时本身就接近整个周期,说明它应该被拆出去,而不是继续优化。这类模块通常是通信与存储,它们各有更好的归属形态。
3.3 中断与主循环的职责划分
前后台系统的分工原则可以压缩成一句话:中断只做「采样 + 置标志」,重活回主循环。这不是风格偏好,而是有四条硬理由:
- 中断期间同优先级与低优先级中断被屏蔽,中断体越长,屏蔽窗口越长;窗口内到达的次级事件只有被丢掉或退化成标志溢出两种下场。
- 中断里不能阻塞。没有让出处理器的机制,任何等待都会退化成忙等,而忙等期间中断仍然是开着的,回路会变成中断套中断。
- 中断栈通常比主栈小得多。在中断里调深层调用链是栈溢出的高发点,而溢出的现场表现往往不是立刻崩,而是行为变得莫名其妙。
- 打断主循环会让主循环状态复杂化。「重活放主循环」本身就是一种状态保护:主循环只在模块边界被改变状态,不会在半途被换掉。
在中断里做长耗时处理是嵌入式最经典的架构错误,它的典型现场表现有四种:在中断里解析协议报文、在中断里打印日志、在中断里擦写 Flash、在中断里等一个从机应答。前三种把中断服务时间拉长,第四种最糟——它把一个「等设备」的时长直接变成了「所有中断都被屏蔽」的时长。
顺着分工往下看,会看到一条清晰的响应链:外设产生事件 → 中断控制器按优先级排队与仲裁 → 中断服务程序响应 → 主循环在轮到该模块时响应。四段延迟的量级差得非常远,硬件那几段通常只有几十个时钟周期,而最后一段可能是零、也可能是一整个循环周期。最不可控、最容易被低估的永远是最后一段,因为它取决于循环里排在后面的模块有多慢,而不取决于中断本身有多快。
压缩这条链的做法要分清哪一段压哪一段:第 3 段靠中断分层——短中断只置标志,长处理降到低优先级或直接回主循环,这样高优先级中断不必等长中断跑完;第 4 段与中断体量成正比,是最值得优化的一段,能不做的判断就不要在中断里做;第 5 段靠把等待型模块移出主循环,或者在循环开头先扫一遍全部标志再决定处理哪些,把响应提前。三个手段互不替代,各自对应链上的一环。
| 环节 | 发生什么 | 延迟量级(定性) | 能不能压缩 |
|---|---|---|---|
| 外设事件到感知 | 外设从低功耗唤醒、信号到达接口 | 由外设与接口时序决定 | 不能,由硬件决定 |
| 进入中断入口 | 压栈、跳转中断向量 | 个位数到几十个时钟周期 | 只能略微,精简入口序言 |
| 控制器排队与仲裁 | 同级与低级中断被屏蔽,高优先级插队 | 取决于当前屏蔽窗口的长短 | 能,靠中断分层缩短长中断 |
| 中断服务程序执行 | 读外设、判条件、置标志 | 与中断体量成正比 | 能,把不必要的事移出中断 |
| 主循环响应 | 等循环轮询到对应模块 | 从零到一整个循环周期 | 能,把等待型模块移出主循环 |
3.4 资源占用与可预测性
裸机的资源优势可以用四个维度说清楚,而且这四项都是「零」而不是「小」:
- Flash:没有内核镜像。链接进来的只有业务代码与一个很小的启动入口。
- RAM:没有任务栈。只有主栈、中断栈与全局变量三块。
- 堆:可以完全不用。没有动态分配,就没有碎片,也就没有「运行了三个月之后分配失败」这类问题。
- 启动时间:复位到业务第一行之间没有多余环节。对于电机、继电器这类动作本身要求确定时刻的产品,这一条常常是决定性的。
其中「栈可静态精确计算」这一条在安全相关场景里最有说服力。任务栈要用运行时统计才知道够不够,而主循环的栈占用可以在编译期就算出来:沿每条路径累加局部变量、参数与被保存的寄存器,再取最大值即可。不需要任何运行时数据就能给出一个可复核的数字,这在需要提交论证材料的场合价值很高。
可预测性上,裸机是唯一能给出确定性上界的形态,原因也是三个「没有」:没有调度决策,所以没有「谁在什么时候跑」的不确定性;没有优先级继承,所以没有优先级反转带来的不可预期阻塞;没有动态内存,所以没有碎片导致的不可预期失败。三个不可预期源同时消失之后,剩下的就只是逐模块测出的耗时之和。
但这个上界的代价必须说清楚,否则容易把它当成免费的午餐。它是「所有模块最坏情况之和」,是一个保守值,并且随模块数量与模块复杂度线性膨胀。膨胀到一定程度后会出现两个后果:一是明明 CPU 还有大量空闲却不敢声称满足实时性,二是为了压这个上界开始写「本轮来不及就跳过」的分支——此时代码里已经出现了时序相关的隐式状态,可预测性实际上被悄悄破坏了,而且是以一种很难在评审里被看出来的方式。
| 对比项 | 裸机超级循环 | 前后台(中断加主循环) | 极小内核 RTOS |
|---|---|---|---|
| 上下文切换开销 | 无 | 只有中断进出的开销 | 有固定量,与内核配置有关 |
| 栈空间 | 主栈与中断栈,可在编译期算出 | 主栈与中断栈,可在编译期算出 | 每任务一个栈,按最坏路径估算 |
| 堆 | 可以完全不用 | 可以完全不用 | 默认可用,需要时可显式关闭 |
| 内核镜像占用 | 无 | 无 | 有,随配置裁剪而变 |
| 优先级反转风险 | 无优先级继承,无从产生 | 无优先级继承,无从产生 | 有,靠优先级继承缓解 |
| 可预测性上界的来源 | 各模块最坏耗时之和 | 各模块最坏耗时之和再加最坏屏蔽窗口 | 需按优先级与阻塞关系做响应时间分析 |
| 模块耦合方式 | 共享变量与标志位 | 共享变量与标志位 | 队列、信号量、事件等显式原语 |
3.5 优势、限制与典型陷阱
把这一章的结论收成三段:优势集中在占用最小、启动最快、行为最可预测这三件事上,而且三者互为因果;限制集中在扩展性有硬上限、缺少强制隔离、没有资源仲裁这三件事上;陷阱则集中在中断、估算与看门狗这三处。
其中「没有资源仲裁」是最容易被低估的限制。裸机里谁先谁后完全由代码顺序决定,一旦某个需求变得更紧急,能做的只有把它挪到循环前面——这是一个纯手工的、随迭代不断推翻的操作,也是裸机项目后期维护成本的主要来源。
| 类别 | 条目 | 现场表现 | 该怎么处理 |
|---|---|---|---|
| 优势 | 启动时间最短 | 复位到业务第一行之间没有多余环节 | 有确定动作时刻要求时优先 |
| 优势 | 资源占用最小 | 无内核镜像、无任务栈、无堆 | 内存与 Flash 都紧张时的唯一解 |
| 优势 | 行为完全可预测 | 路径由代码唯一确定,没有运行时调度 | 需要安全论证或严格验收时最省事 |
| 限制 | 扩展性有硬上限 | 加模块必然推高循环上界 | 到点必须重构,不能靠调参 |
| 限制 | 缺少强制隔离 | 一个模块越界写会破坏全局状态 | 靠编码规范与评审约束 |
| 限制 | 没有资源仲裁 | 优先级靠代码顺序硬凑 | 紧急需求只能靠挪位置解决 |
| 陷阱 | 中断里做长耗时处理 | 中断里解析协议、打印、写 Flash | 中断只留采样与置标志 |
| 陷阱 | 用平均耗时估算周期 | 只测过空闲场景就算通过 | 按最坏路径测,按和判定 |
| 陷阱 | 看门狗失效后行为不确定 | 喂狗路径与业务耦合,复位后半初始化 | 喂狗路径独立且最短 |
陷阱一:在中断里做长耗时处理。中断体每延长一段,同级与低优先级中断就多屏蔽一段。表现是平时完全正常,只在某个特定事件组合下丢数据或响应超时,而且极难复现。修法只有一条:中断里只留采样与置标志。
陷阱二:用平均耗时估算循环周期。平均负载三成看起来非常安全,但平均值掩盖了罕见路径——一条错误重试分支或一个超长报文就能吃掉三倍耗时。验收必须按「所有标志同时置位、所有模块走最长分支」这一组条件测。
陷阱三:看门狗失效后的行为不确定。喂狗路径与业务耦合时,业务一卡住就看门狗复位,而复位后往往只恢复了一部分状态,表现为设备「活着但不工作」,现场最难查。修法是让喂狗走一条最短且不依赖业务状态的路径,并把状态恢复做成幂等的。
4. FreeRTOS
FreeRTOS 是 MCU 侧最常见的小内核实时内核。本章按「内核结构 → 调度 → 同步 → 内存 → 中断边界」的顺序把它拆开。本章要传达的核心判断只有一条:实时内核的价值不在于多了一个调度器,而在于它把「模块之间怎么互相影响」从隐式的代码约定变成了显式的、可被检查的对象(优先级、原语、栈深度)。配套的一条实操判断是:优先级应跟截止期走,不是跟重要性走。
4.1 设计目标与内核结构
FreeRTOS 的三个设计目标彼此支撑,缺一个它就变成了另一种东西。
- 极小内核。内核只做两件事:任务调度与任务间同步。它不做内存管理(可选)、不做设备抽象、不做网络、不做文件系统。凡是不属于这两件事的能力,都被推给应用或第三方组件——这也是它能塞进几 KB 内存的原因。
- 可裁剪。不用的原语不参与链接。这条在 MCU 上是硬指标而不是优化项:内核占用直接与业务代码抢同一块 Flash。
- 移植层薄。架构差异——上下文保存与恢复、中断嵌套与临界区实现、存储模型——全部收敛到一个 port 层,应用代码与内核代码不含架构特定内容。这一条决定了「换一个 MCU 型号要改多少东西」,也是评估移植成本时唯一真正该问的问题。
需要建立的一个判断是:实时内核与裸机不是替代关系,而是把耦合显式化的关系。在裸机里,「模块 A 改了一个全局变量,模块 B 下一轮就会看到」这件事是隐式的,只存在于某个人脑子里;在实时内核里,「A 通过队列把一份数据交给 B」是显式的,可以在代码里看到、可以单独测试、可以在评审里追问。对于模块数已经越过超级循环舒适区的产品,这个显式化带来的收益,通常比省下来的那几 KB 内存更值钱。
| 内核组成部分 | 作用 | 不用时能否裁掉 | 裁掉的影响 |
|---|---|---|---|
| 调度器 | 优先级调度与同优先级时间片 | 不能 | 这是内核本身 |
| 任务与上下文切换 | 切换任务栈与控制块 | 不能 | 这是内核本身 |
| 端口层(移植层) | 隔离架构差异 | 不能 | 每个架构一份实现 |
| 队列 | 任务之间传递数据 | 能 | 失去数据传递手段,只能靠共享变量 |
| 信号量 | 事件通知与资源计数 | 能 | 失去无数据的同步手段 |
| 互斥量与优先级继承 | 共享资源的排他访问 | 能 | 临界区只能靠自己关中断保护 |
| 事件组 | 多位条件标志 | 能 | 多条件判定要拆成多个单比特标志 |
| 任务通知 | 一对一的轻量通知 | 能 | 跨任务通知只能退回信号量 |
| 软件定时器 | 回调式的周期任务 | 能 | 周期逻辑要放进任务里自己实现 |
| 栈溢出检测钩子 | 运行时校验栈边界 | 能但不建议关 | 失去栈溢出最重要的兜底手段 |
4.2 任务状态、调度与优先级
任务状态的关键是区分阻塞态与就绪态:阻塞态是「在等某个事件,事件没来之前无论优先级多高都不会被调度」,就绪态是「随时可以跑,等处理器」。把一个任务误设成长期阻塞态,等于把它的优先级降到了零;把一个本该阻塞的任务写成忙等,等于把处理器丢给了它。这两类错误在代码里都看不出来,只能靠想清楚「这个任务此刻在等什么」来避免。
调度是抢占式的优先级调度,同优先级之间按时间片轮转。这里有一个容易被误解的点:时间片轮转不构成实时性保证,它只保证同优先级的任务不会饿死。想要确定性就得靠优先级区分,把关键任务放到更高的级别上。
本章最关键的一条判断是:优先级分配应当跟截止期走,不是跟重要性走。优先级决定的是「谁先拿到处理器」,而实时性关心的是「谁不能迟到」——这两件事只有在「越重要越紧急」的简单产品里才恰好一致。一旦出现「重要但不紧急」的任务(统计、诊断上报、非实时日志),按重要性分配就会立刻出错:
- 典型错配一:把日志与调试输出设成高优先级(因为感觉它重要),把控制回路设成低优先级(因为它代码短)。结果是控制回路被日志 I/O 挤掉,实时性彻底崩掉,而代码评审时没有人会认为这是错的。
- 典型错配二:把控制回路设成最高优先级,但让它在里面做串口打印。结果控制回路的执行时间被输出耗时支配,而输出耗时随缓冲区状态波动,形成难以定位的抖动。
正确做法是机械的:把每个任务的截止期列出来,按截止期从近到远分配优先级,重要但不紧急的一律给最低级,靠系统空闲时间跑。这条规则的好处是它可以被复核——把优先级表和截止期表并排放在一起,任何一条对不上都会立刻暴露。
最后一条边界:优先级级数不要超过实际需要。每一级都要独立验证最坏路径,级数越多,分析成本越高,出错面也越大。把五个优先级用足,通常好过把二十五级用一半。
把项目的优先级表拿出来,按下面四条逐条过一遍。这四条任何一条不过关,实时性问题就会在集成阶段才暴露出来。
- 对照截止期,不是对照重要性。每个任务旁边标出截止期与最坏执行时间,检查「优先级顺序是否与截止期顺序一致」。
- 关键任务里不许有阻塞操作。最高优先级的那一两个任务里,不得出现串口打印、大块数据拷贝、Flash 擦写、等待从机应答。
- 每级的最坏路径要独立算过。不能因为「这一级只有两个任务」就跳过估算。
- 想清楚被饿死的那个是谁。低优先级任务如果永远等不到空闲时间,要么给它周期性阻塞点,要么承认它可以不被执行——假装它会跑,实际就是没有跑。
4.3 同步与通信原语的选择
同步原语的选择看起来是背口诀,实际上只需要先问三个问题:要传的是数据还是只是一个「发生过」的信号;是传一份数据、一个计数,还是一组位条件;是任务之间还是任务与中断之间。三个问题的答案基本唯一地指向某个原语。
逐项来看每一类的判据与代价:
- 队列传数据,会发生一次拷贝。拷贝换来的是数据所有权清晰——发送后缓冲区立刻可复用,这是它最容易被忽略的收益。代价是内存与时间开销,大消息尤其明显。池化形式可固定元素大小、避免运行期动态分配。
- 二值信号量只表达「发生过」,不携带数据,且没有优先级继承,因此不能用来做互斥,这是最需要记住的一条。计数信号量表达「有几件」,适合管理数量有限的空闲资源。
- 互斥量是唯一为排他保护设计的原语,带优先级继承以避免优先级反转。代价是继承本身带来调度抖动,且不允许在中断里加锁。
- 事件组是多位位标志,可等任意一位或全部位,只提供条件等待、不携带数据,开销极小。代价是位操作要自己保证原子性。
- 任务通知是最轻量的一对,没有对象也没有分配,代价是只有等待者、无法多播。
选择原则因此可以写成一句可执行的规则:能用最轻的就不要用重的。默认从任务通知开始,需要传数据才上队列,需要排他才上互斥量,多条件判定留给事件组。反过来也要成立:不要用一个轻量原语去承担它承担不了的语义——用二值信号量当锁用,是实时内核里最常见也最昂贵的错误。
| 原语 | 传什么 | 什么时候用它 | 主要代价 | 典型误用 |
|---|---|---|---|---|
| 队列 | 数据拷贝 | 把一份数据交给另一个任务 | 有内存与时间开销,大消息更明显 | 用它传大块数据,拷贝挤占高优先级任务 |
| 队列(池化形式) | 定长元素 | 消息长度或数量不确定 | 池要静态预留,分档不当会浪费 | 元素槽太小,消息被截断却看不出 |
| 二值信号量 | 一个信号 | 只关心事件是否发生过 | 不传数据,且没有优先级继承 | 拿它做互斥,亲手制造优先级反转 |
| 计数信号量 | 一个计数 | 管理若干个同类空闲资源 | 初值与语义容易搞反,计数会溢出 | 把「有几件」当成「是不是有」用 |
| 互斥量 | 一把锁 | 保护共享资源的排他访问 | 优先级继承带来调度抖动 | 在中断里加锁,持锁时间不可控 |
| 事件组 | 多个位标志 | 等多个位条件同时成立 | 位操作要自己保证原子性 | 把事件组当成队列传数据 |
| 任务通知 | 一个通知值 | 一对一通知另一个任务 | 只有等待者,不能多播 | 用它指望存下多条待处理消息 |
4.4 内存与栈的分配策略
内存问题在实时内核里被拆成两个独立的决策:任务栈给多少,以及运行期对象在哪里分配。前者是最容易翻车的地方,而且翻车时通常不报错。
栈深度必须按最坏路径算,不是按平均值。算法是沿最深的调用链,把每层的局部变量、参数、被保存的寄存器累加,再取所有路径的最大值;还要额外加上中断嵌套那一层。平均值是最危险的依据——一条平时几乎不走的错误处理路径,可能瞬间吃掉平时三倍的栈,而它平时不出现,所以测试时看不到。
验证要用两个手段并且两个都开,因为它们的盲区正好互补:
- 栈填充法:启动时把栈区域填成已知模式,运行时不断检查有没有被踩过。它能抓到突发的越界写入,但只看「有没有被踩」,不告诉你实际用了多少。
- 运行时水位统计:周期性记录栈指针与栈底之间的距离,得到连续观测下的最高水位。它能告诉你该给多少,但采样周期不够长时会漏掉罕见路径。
最终给的栈深度应当是实测最高水位再乘一个余量系数,且该系数须在评审中被明确同意。
分配策略上有三条可选:
- 动态分配灵活,但碎片不可预测,分配失败点要到运行期才暴露出来,且失败时往往已经晚了。
- 静态分配在编译期确定全部对象,可预测性最好,代价是对象数量一变就要改配置重来。
- 内存池是折中:按大小分档预留固定的小块,运行时从池里取,保留了大部分确定性又不至于完全不能变。
一条实用的默认建议是:栈用静态分配,运行期对象优先走内存池而不是直接堆分配。这样把「最难预测的两类分配」都收进了可统计的范围,剩下的堆使用才可以用总量上界的方式兜底。
| 方法或策略 | 怎么做 | 能得到什么 | 局限 |
|---|---|---|---|
| 最坏路径静态分析 | 沿最深调用链累加局部变量、参数与保存的寄存器 | 一个可复核的理论下界 | 难以覆盖隐式调用与库函数实际用量 |
| 栈填充法 | 启动时填充已知模式,运行时检查是否被踩 | 突发越界写入的确凿证据 | 只能判断有与无,给不出占用量 |
| 运行时水位统计 | 周期记录栈指针与栈底的距离 | 连续观测下的最高水位 | 采样周期不足时会漏掉罕见路径 |
| 栈溢出检测钩子 | 在内核侧做边界检查 | 越界时立刻定位到任务 | 带来固定开销,不能替代前面的手段 |
| 动态分配 | 运行期从堆申请 | 灵活,对象数量可变 | 碎片不可预测,失败点在运行期才暴露 |
| 静态分配 | 编译期确定全部对象 | 可预测性最好 | 对象数量一变就要改配置 |
| 内存池 | 按大小分档预留固定小块,运行时从池取 | 兼顾确定性与灵活性 | 分档不当会浪费或不足 |
4.5 中断与任务的边界
任务上下文里可以随便调用的 API,在中断上下文里有一半是不能用的。这条边界不是风格建议,而是内核的执行模型决定的。
- 只能调用带 FromISR 后缀的版本。这不是命名习惯,而是语义差别:任务上下文里的阻塞类调用会触发一次调度,而中断期间不允许发生任务切换,所以中断版本把这些动作推迟到中断返回时由调度器完成。
- 只能「通知」,不能「等待」。中断里可以发通知、发队列、发信号量,但任何会阻塞当前执行体的调用都是非法的——中断里没有可以让出执行权的机制。
- 关中断与临界区都有代价。关得越久,最坏中断延迟越长,次级事件被硬丢掉的概率越大。真正需要关中断的通常只有两类:改一组共享变量的极短片段、访问没有硬件锁的外设寄存器。其余场合优先用原子的位操作代替关中断,这类指令本身就是原子的,比关中断便宜得多。
据此可以得到一条很实用的分工原则:中断里只做「读寄存器 + 置标志」,连队列发送也尽量留在任务里。如果确实需要在中断里发队列,可以用中断版本,但要意识到唤醒一个高优先级任务会在中断退出时立刻触发一次调度,这次调度的时机与开销都要计入实时性分析。
最后补一条与看门狗相关的边界:喂狗路径必须短、确定、且不依赖需要排队的资源。如果喂狗被放在一个会阻塞的任务里,一旦那个任务被高优先级任务长期占用,看门狗就会在系统明明还活着的情况下复位——这是实时内核项目里「莫名其妙重启」的一类常见根因。
把下面四条写进项目的编码规范,并在评审时逐条对照,它们覆盖了绝大多数与中断相关的问题:
- 只用中断版本的 API。调错版本的后果通常不是报错,而是行为变得难以预测,且不一定会崩。
- 中断里不做阻塞、不做等待、不做长耗时计算。包括等串口、等从机、等 DMA 完成标志。
- 共享数据的读改写必须是原子操作。要么用原子的位操作,要么进临界区;先判断再赋值的写法在有并发时是错的。
- 中断体量与它的屏蔽窗口成正比地重要。每多十行中断代码,就多十行的屏蔽窗口,这件事在评审时应当像看栈深度一样被认真估算。
4.6 优势、限制与典型陷阱
把这一章收成三段。优势集中在三件事:内核占用小因而可以塞进小内存 MCU、任务隔离把隐式耦合变成显式对象、抢占式调度配合正确的优先级划分能给出可分析的上界。第三点尤其重要——它意味着实时内核不是「更高级的写法」,而是「能从形式上论证实时性」的写法,这在需要出具材料的场合是本质差别。
限制同样明确:没有地址空间隔离,一个任务越界写会破坏另一个任务,隔离只到「逻辑上不该互相越界」这一层;同步原语全部建立在共享内核数据结构之上,用错一次的代价是难以复现的竞态,而不是可捕获的错误;动态分配默认是开启的,不显式配置就会用堆,碎片问题因此会以「跑了几个月才出问题」的形式出现。
陷阱集中在四个位置,其中栈溢出与优先级分配这两条是重灾区:栈溢出往往不立刻触发任何异常,系统继续跑但行为异常,现场几乎无法定位;优先级按重要性分配,则在评审时看起来完全合理,直到集成阶段才暴露。
| 类别 | 条目 | 现场表现 | 该怎么处理 |
|---|---|---|---|
| 优势 | 内核极小且可裁剪 | 小内存 MCU 上也能跑起实时内核 | 内存紧张但需要任务隔离时的首选 |
| 优势 | 耦合关系被显式化 | 数据流写在代码里,可单独测试 | 模块数越过超级循环舒适区时收益最大 |
| 优势 | 可做实时性论证 | 配合优先级划分能算出响应上界 | 需要出具论证材料时价值最高 |
| 限制 | 没有地址空间隔离 | 一个任务越界写会破坏别的任务 | 靠编码规范与代码审查约束 |
| 限制 | 原语基于共享内核结构 | 用错一次是难以复现的竞态 | 统一封装,禁止裸用 |
| 限制 | 动态分配默认开启 | 碎片问题以「跑了几个月才出问题」出现 | 需要确定性时显式改用静态或内存池 |
| 陷阱 | 栈溢出无声崩溃 | 不报错,系统继续跑但行为异常 | 同时开栈填充与水位统计并留足余量 |
| 陷阱 | 优先级按重要性分配 | 重要但不紧急的任务挤掉了关键任务 | 按截止期排序重排并逐条复核 |
| 陷阱 | 耗时操作塞进高优先级任务 | 打印、Flash 擦写拖住关键任务 | 耗时操作一律下沉到低优先级 |
| 陷阱 | 喂狗任务自身会阻塞 | 系统没死却被看门狗复位 | 喂狗路径短且不依赖排队资源 |