← 返回嵌入式学习资料第 10 章 · FreeRTOS 与实时设计

CHAPTER 10 · REAL-TIME SYSTEMS

FreeRTOS 与实时设计
让时间成为合同

从 deadline、WCET 和任务状态开始,建立调度、同步、内存、看门狗与过载恢复的共同语言;把主机模型、目标机事实、官方资料和可回滚交付放进同一条证据链。

6 个一级单元51 个递进小节主机路径可复现

TEXTBOOK MAP

CHAPTER 10 · MAP

从时间合同到可回滚实时系统

六个一级单元对应六种能力:建立实时模型、安排任务调度、选择同步原语、预算内存、处理过载、完成时间证据交付。

先把时间与任务写清楚
  • 01 deadline、WCET 与抖动
  • 02 任务状态与调度选择
再把资源与压力变成合同
  • 03 队列、通知与互斥
  • 04 栈、堆与缓冲所有权
最后让失败也能恢复
  • 05 看门狗、过载与安全态
  • 06 时间线、版本与回滚

小节数量按认知任务变化:调度、同步和交付需要更多递进步骤;每个单元都保留无目标机主机路径,并标出目标机停止条件。

使用方式:先从左侧选择一个实时问题,再按时间模型、任务、同步、内存、恢复和交付推进。每个单元底部有前后翻页;小节标题直接说明正在追踪的 deadline、状态、容量、进展或回滚。
主机边界:默认使用标准库模型、任务时间线、队列容量、内存预算和故障分类;没有目标板也能完成公式、状态、背压、超时和清单测试。
目标机边界:真实 FreeRTOS port、tick、时钟、上下文切换、IRQ、缓存/DMA、栈/堆、看门狗、输出安全态、负载、仪器和复位路径必须回到当前芯片、板卡和官方资料。

01 · REAL-TIME MODEL

实时模型:deadline、WCET 与抖动

实时系统不是把平均速度做快,而是在指定输入、资源和负载条件下,让每个重要动作在 deadline 之前完成。先用 release、period、deadline、WCET 和 jitter 描述任务,再讨论调度器、时钟和测量工具,才能把‘实时’从宣传词变成可复查的时间合同。

看懂读懂 deadline、WCET、period 和 response time 的关系。
说清会做利用率、裕量与抖动的初步预算。
开始做把时间模型写成主机可运行、目标机可测量的记录。
官方基线FreeRTOS Kernel本课程固件实际编译的内核版本;不得与最新 API 文档混用。锁定:V10.3.1 (课程工程运行版本) · 核对日期:2026-09-05

10.1.1 任务三元组:WCET、D 与 P

实时合同先写对象和时基。一个任务至少要有 name、release 条件、period 或事件来源、relative deadline、WCET 假设、优先级、资源依赖和失败动作。‘10 ms 任务’可能指每 10 ms 释放一次,也可能指必须在 10 ms 内响应;若不写清楚,调度、测试和现场日志无法对齐。

10.1.2 硬实时、软实时与价值曲线

FreeRTOS 的任务 API、Zephyr 的线程/调度服务以及 Linux 的调度策略都提供了不同抽象,但抽象名称不会自动生成 deadline。调度器只能依据可见的就绪、阻塞和优先级状态做选择;系统设计者仍要定义哪些工作必须完成、哪些可以丢弃、哪些应降级,以及错过之后如何保存证据。

C · DEADLINE BUDGET
typedef struct { const char *name; uint32_t wcet_us; uint32_t period_us; uint32_t deadline_us; } task_budget_t; static int due(uint32_t now, uint32_t deadline) { return (int32_t)(now - deadline) >= 0; }

模型展示预算和回绕安全比较;实际 tick、时钟、端口和 WCET 仍需目标机证据。

PYTHON · UTILIZATION
def utilization(wcet, period): if wcet <= 0 or period <= 0: raise ValueError("positive budget required") return wcet / period def schedulable(tasks, reserve=0.2): return sum(utilization(t[0], t[1]) for t in tasks) <= 1 - reserve

主机计算是准入检查,不替代 ISR、阻塞、缓存和真实最坏执行路径。

10.1.3 release、finish 与 response time

release 是工作可被执行的时刻,start 是实际获得处理器的时刻,finish 是本次工作完成的时刻,response time 通常是 finish-release。等待队列、互斥锁、禁中断、缓存未命中、总线争用和高优先级抢占都可能拉长 start 或 finish;只测函数内部 elapsed 会漏掉排队和阻塞。

表 1 · 实时模型:deadline、WCET 与抖动的证据判断表
对象应留下的证据不能直接推出
时间release/deadline/finish平均值就是 WCET
预算WCET/period/reserve利用率低就必然按时
证据trace/时基/窗口一次样本证明上界

10.1.4 WCET 测量不是平均值

WCET 不是在某台机器上跑几次得到的最大值,而是针对给定代码、编译选项、输入集合、硬件、时钟和干扰模型所声称的上界。主机可以做路径枚举、压力和统计,目标机还要覆盖缓存、Flash wait state、DMA、IRQ、温度、电源和编译器生成代码;没有这些条件时只能称为测得样本。

10.1.5 利用率、裕量与准入

利用率 U=C/T 是准入预算的起点,不是所有调度算法都适用的充分条件。C 要包含真正占用 CPU 的执行,T 要与 release 语义一致,还要为 ISR、内核 tick、上下文切换、日志、故障恢复和突发任务保留 reserve。若总和接近 1,任何测量误差和优先级反转都会变成 deadline 失败。

图 1.1 · 实时模型:deadline、WCET 与抖动的判断路径每一格都要有自己的条件。
输入周期/事件
预算WCET/deadline
执行调度/阻塞
证据trace/最坏

从左到右是思考顺序,不是把时间、调度、资源和恢复证据压缩成一个“通过”。

10.1.6 抖动、延迟与观测窗口

抖动应说明测量哪两个事件以及窗口多长。输入边沿本身可能不均匀,任务 release 可能受 tick 量化,开始时间可能受抢占影响,完成时间还会叠加锁和总线等待。把 min、max、平均、p95 与样本数量一起保存,才能区分偶发异常、系统性相位漂移和观测工具引入的延迟。

10.1.7 tick、单调时钟与时间回绕

时间比较应使用单调时基,并明确 tick 宽度、单位、换算和回绕窗口。对有限宽度计数器,直接用 now > deadline 可能在回绕后错误;常见做法是用有符号差值并限制最大等待窗口。墙上时钟适合显示日期,不适合判断任务是否过期,尤其在校时、休眠唤醒或时间源切换时。

10.1.8 本节交付:实时预算卡

实时预算卡建议包含 task_id、release_source、period、deadline、wcet_sample、wcet_claim、priority、blocking_bound、isr_share、memory、clock、trace_fields、failure_action、host_result、target_result 和 unverified_scope。它把‘实时’变成读者可运行、工程师可测量、审查者可质疑的一组字段,而不是一句性能承诺。

CHECKPOINT 10.1把“一个任务什么时候算按时完成,怎样把‘快’写成可以验收的合同?”写成一份可复查记录

请围绕“实时模型:deadline、WCET 与抖动”写出对象、输入/释放条件、调度或同步机制、输出与完成语义、一个时间或内存约束、一个失败路径和对应证据。没有目标板时,运行主机路径并把未执行字段标为未执行;拥有目标机时,还要补齐芯片、端口、时钟、FreeRTOSConfig、工具链、仪器、负载和观察条件。不要用一次平均延迟、一个 heartbeat、一个队列发送成功或一张截图覆盖没有做过的验证。

合格记录要让没有现场上下文的读者知道先查什么、先运行什么、看到什么才算通过,以及哪一句结论仍然不能推出。未知项、停止条件和回滚动作都是交付的一部分。

02 · TASKS AND SCHEDULING

任务与调度:优先级不是时间保证

任务是可调度的工作单元,但任务状态、优先级、抢占、时间片、阻塞和唤醒之间有明确规则。把一个函数包进 task 入口并不会自动得到实时性;只有在释放、阻塞、预算、优先级和过期策略都被写出后,调度器的行为才可以被观察和解释。

看懂用状态机描述任务生命周期。
说清区分优先级、抢占与真正的时间保证。
开始做设计不会漂移、饿死或无限追赶的周期任务。
官方基线FreeRTOS Kernel本课程固件实际编译的内核版本;不得与最新 API 文档混用。锁定:V10.3.1 (课程工程运行版本) · 核对日期:2026-09-05

10.2.1 任务状态与生命周期

任务状态是解释调度的第一张地图。READY 表示具备运行条件,RUNNING 表示当前占用某个 CPU,BLOCKED 表示等待时间、队列、通知或同步对象,SUSPENDED 表示被显式挂起,DELETED 表示生命周期结束。不同端口可能还有等待超时或临界区细节;记录状态时应同时保存原因和时间。

10.2.2 优先级、抢占与合作式执行

优先级决定多个就绪任务的选择顺序,不等于任务在 deadline 前一定完成。高优先级任务可能被中断、禁调度、互斥锁或更高优先级任务延迟;低优先级任务也可能持锁阻塞高优先级等待者。调度合同必须把 blocking bound、ISR 预算和最坏执行时间一起算,而不是只比较 priority 数字。

C · PERIODIC TASK
void sample_task(void *arg) { TickType_t next = xTaskGetTickCount(); for (;;) { sample_once(); vTaskDelayUntil(&next, pdMS_TO_TICKS(10)); record_progress(); } }

绝对节奏示例;任务状态、tick 换算、过期策略和端口行为必须按配置核对。

PYTHON · RELEASE TRACE
def release_times(start, period, count): if period <= 0 or count < 0: raise ValueError("invalid schedule") return [start + i * period for i in range(count)] def response(release, finish): if finish < release: raise ValueError("finish precedes release") return finish - release

确定性时间线用于主机断言;它不包含上下文切换、ISR 或目标时钟误差。

10.2.3 时间片与同优先级公平

抢占式调度在更高优先级任务就绪时切换,合作式或调度锁则要求当前代码主动让出。临界区太长会把‘可抢占’变成假设;在任务里连续计算、轮询外设或关闭中断,都会把其他任务的 response time 拉长。trace 中要标出切换原因和临界区边界。

表 2 · 任务与调度:优先级不是时间保证的证据判断表
对象应留下的证据不能直接推出
状态ready/block/wake任务只有运行和停止
选择priority/preempt/time slice高优先级等于零延迟
节奏absolute release/overrun相对 delay 不漂移

10.2.4 阻塞、饥饿与活锁

同优先级时间片改善公平性,但 tick 粒度、端口配置和阻塞行为会影响实际份额。若两个任务都不阻塞且每片都做大量工作,系统可能看似公平却整体错过 deadline;若高优先级任务不断唤醒,低优先级任务会饥饿。要保存每个任务获得 CPU 的区间和被推迟的次数。

10.2.5 周期任务的绝对节奏

相对 delay 是‘从现在再等一段时间’,执行时间和抢占会逐次累积相位误差;绝对节奏则保存 next_release,每次按 period 推进。若任务已经过期,需要明确跳过、合并、限次追赶、降级或复位。无限追赶历史工作会形成过载正反馈,让系统越来越晚。

图 2.1 · 任务与调度:优先级不是时间保证的判断路径每一格都要有自己的条件。
状态ready/running
选择priority/preempt
等待block/wake
节奏period/deadline

从左到右是思考顺序,不是把时间、调度、资源和恢复证据压缩成一个“通过”。

10.2.6 空闲任务、CPU 预算与低功耗

空闲任务的时间可以用于低功耗、后台清理或统计,但不能被误认为可随时借用的实时预算。进入睡眠会改变 tick、时钟和唤醒延迟,Flash/外设重新上电也有成本。CPU 使用率报告应分任务、ISR、内核和 idle,并说明采样窗口、计时来源和睡眠状态。

10.2.7 工作队列与延后处理

工作队列把短 ISR 或调用者动作转成后台 handler,减少中断上下文中的复杂操作;但 handler 若阻塞,会卡住同一队列后续项目。Zephyr 文档也强调工作队列 handler 的同步和阻塞边界。为每个项目写最大执行、是否可睡眠、失败重试和队列拥塞行为,才能判断这种延后是否安全。

10.2.8 通知任务与交接点

任务交接的证据不应只有‘通知已发送’。需要记录事件来源、通知/队列序号、产生时间、唤醒时间、开始时间、消费结果、剩余容量和 deadline。这样可以分开定位事件没产生、唤醒晚、被高优先级抢占、处理慢和结果丢弃,而不是把所有问题归因于调度器。

10.2.9 本节交付:任务调度卡

任务调度卡包括 task_id、state_machine、priority、preemption、period、release_source、deadline、wcet、blocking、critical_sections、cpu_share、idle_policy、workqueue_policy、overrun_action、trace_schema 和 target_limits。它是后续同步、内存、看门狗与交付单元共同使用的事实表。

CHECKPOINT 10.2把“FreeRTOS 任务何时运行、何时阻塞,怎样证明调度选择没有破坏时间合同?”写成一份可复查记录

请围绕“任务与调度:优先级不是时间保证”写出对象、输入/释放条件、调度或同步机制、输出与完成语义、一个时间或内存约束、一个失败路径和对应证据。没有目标板时,运行主机路径并把未执行字段标为未执行;拥有目标机时,还要补齐芯片、端口、时钟、FreeRTOSConfig、工具链、仪器、负载和观察条件。不要用一次平均延迟、一个 heartbeat、一个队列发送成功或一张截图覆盖没有做过的验证。

合格记录要让没有现场上下文的读者知道先查什么、先运行什么、看到什么才算通过,以及哪一句结论仍然不能推出。未知项、停止条件和回滚动作都是交付的一部分。

03 · SYNCHRONIZATION AND IPC

同步与通信:队列、通知与互斥

同步原语不是可互换的积木。队列传数据,通知传事件或计数,信号量表达可用资源,互斥锁保护共享所有权,事件组表达多个条件;从 ISR 调用的 API、阻塞方式、唤醒对象、容量和溢出策略都必须写进合同。

看懂按语义选择队列、通知、信号量和互斥锁。
说清识别优先级反转、背压与丢失事件。
开始做给一条 ISR→任务路径写出容量与完成证据。
官方基线FreeRTOS Kernel本课程固件实际编译的内核版本;不得与最新 API 文档混用。锁定:V10.3.1 (课程工程运行版本) · 核对日期:2026-09-05

10.3.1 队列是数据合同

队列同时定义元素格式、容量、顺序和所有权。发送成功只说明元素进入了队列,不说明消费者已经处理;接收成功才说明某个消费者取走了元素。固定长度、指针和零拷贝队列的内存归属不同,必须写清谁分配、谁释放、谁可以修改以及队列满、空和关闭时返回什么。

10.3.2 任务通知的定向语义

FreeRTOS 任务通知通常直接附着在一个任务上,因此比通用事件组或信号量更轻量、更快,但它不是广播总线。通知值可以表达计数、位或简单状态,却只有一个被通知对象;多个消费者、数据载荷、持久事件和取消语义仍可能需要队列、事件组或专用任务。选择原语时先写接收者和证据。

C · ISR TO TASK
BaseType_t higher = pdFALSE; void ADC_IRQHandler(void) { sample_t sample = adc_read_sample(); xQueueSendFromISR(sample_queue, &sample, &higher); portYIELD_FROM_ISR(higher); }

ISR 只做最小采样和 ISR-safe 发送;队列满、缓存和中断优先级仍需目标配置。

PYTHON · BOUNDED QUEUE
def offer(queue, item, capacity, policy="reject"): if capacity <= 0: raise ValueError("capacity must be positive") q = list(queue) if len(q) >= capacity: if policy == "reject": return q, False, "full" if policy == "overwrite_oldest": q.pop(0) else: raise ValueError("unknown policy") q.append(item) return q, True, "accepted"

主机模型显式记录拒绝或覆盖;目标机还需证明所有权、DMA 和真实生产速率。

10.3.3 二值与计数信号量

二值信号量适合同步一次发生的事件,计数信号量适合表示多个同类令牌,互斥锁则表达‘谁拥有共享资源’。把信号量当互斥锁会丢掉所有权与优先级继承语义;把互斥锁当事件通知会让持锁时间、递归和 ISR 使用变得不可解释。名称相似不能替代语义。

表 3 · 同步与通信:队列、通知与互斥的证据判断表
对象应留下的证据不能直接推出
原语queue/notify/mutex所有同步对象等价
边界ISR-safe/ownership发送成功等于消费
压力capacity/backpressure队列满可静默覆盖

10.3.4 互斥锁与所有权

互斥锁的持有范围应尽量短,并且只保护真实共享不变量。若持锁期间做日志、等待 DMA、访问慢外设或调用未知回调,高优先级任务的阻塞上界会膨胀。FreeRTOS mutex 文档说明优先级继承可以降低部分反转风险,但不能治愈所有优先级反转;还要限制锁链、嵌套顺序和最长持有时间。

10.3.5 优先级反转与继承

优先级反转通常表现为高优先级任务等待低优先级持锁者,而中优先级任务不断运行。继承机制可能临时提升持锁者,但不会消除死锁、长临界区、多个锁链或中断禁用造成的延迟。证据应包含 holder、waiter、priority、lock_acquire/release、blocked_duration 和最终 deadline,而不只是‘开启了继承’。

图 3.1 · 同步与通信:队列、通知与互斥的判断路径每一格都要有自己的条件。
事件ISR/notification
数据queue/buffer
资源mutex/semaphore
压力capacity/backpressure

从左到右是思考顺序,不是把时间、调度、资源和恢复证据压缩成一个“通过”。

10.3.6 事件组与多条件启动

事件组适合把多个独立条件组合成 wait-any 或 wait-all,例如网络、传感器和校准都 ready;它不携带数据,也不自动说明每一位由谁清除。若清位时机不明确,事件可能丢失或被重复消费。把事件位和数据队列配对,记录快照、等待条件和清除动作,才能复查启动路径。

10.3.7 ISR-safe API 与中断边界

ISR 不能调用会阻塞、调度不安全或依赖任务上下文的接口。正确模式通常是读取最小硬件状态、把样本写入已分配缓冲、调用 ISR-safe 发送/通知并请求一次上下文切换;复杂解析、内存释放和重试留给任务。目标机还要核对中断优先级是否满足端口限制、缓存/DMA 是否同步。

10.3.8 背压、容量与溢出

背压是系统在生产速度超过消费速度时的明确选择:拒绝新数据、覆盖最旧、降采样、暂停采集、提高批量或进入故障。静默覆盖会让数据质量无法解释;无限阻塞会把 ISR 或高优先级任务拖死。记录 capacity、high_watermark、accepted、dropped、overwritten 和 recovery_action,才能把过载变成可诊断事实。

10.3.9 本节交付:通信同步卡

通信同步卡包括 producer、consumer、primitive、payload、ownership、capacity、ordering、isr_api、wait_semantics、timeout、overflow、backpressure、priority_bound、trace_fields 和 target_assumptions。它将同步选择与后续内存、看门狗和发布证据连接起来,避免‘换一个 API 只是实现细节’的误判。

CHECKPOINT 10.3把“多个任务和中断怎样交换数据,又怎样避免丢失、重复和优先级反转?”写成一份可复查记录

请围绕“同步与通信:队列、通知与互斥”写出对象、输入/释放条件、调度或同步机制、输出与完成语义、一个时间或内存约束、一个失败路径和对应证据。没有目标板时,运行主机路径并把未执行字段标为未执行;拥有目标机时,还要补齐芯片、端口、时钟、FreeRTOSConfig、工具链、仪器、负载和观察条件。不要用一次平均延迟、一个 heartbeat、一个队列发送成功或一张截图覆盖没有做过的验证。

合格记录要让没有现场上下文的读者知道先查什么、先运行什么、看到什么才算通过,以及哪一句结论仍然不能推出。未知项、停止条件和回滚动作都是交付的一部分。

04 · MEMORY AND STACK

内存与栈:确定性从哪里来

内存策略决定了系统是否会在运行中遇到不可预测的等待、碎片、失败和栈溢出。FreeRTOS 同时支持静态与动态创建对象;工程上要把选择、生命周期、对齐、DMA、所有权、栈水位和失败动作写进预算,而不是只看链接器的剩余数字。

看懂区分静态、动态、栈和共享缓冲的风险。
说清会读栈高水位、堆失败与容量预算。
开始做为一个可复现配置写出内存上限和停止条件。
官方基线FreeRTOS Kernel本课程固件实际编译的内核版本;不得与最新 API 文档混用。锁定:V10.3.1 (课程工程运行版本) · 核对日期:2026-09-05

10.4.1 静态与动态对象

静态分配把对象存储和生命周期放进编译期或显式缓冲,通常更容易审查上限;动态分配则更灵活,但需要失败路径、碎片分析和运行期证据。现代 FreeRTOS 可以为任务、队列、定时器、事件组、信号量和互斥锁使用静态内存,但‘可静态创建’不代表对象自动初始化、并发和释放语义已经正确。

10.4.2 heap scheme 与碎片

堆的剩余字节不等于下一次分配一定成功。不同 heap 实现的合并、拆分、对齐和碎片行为不同;长期运行中交替申请不同尺寸对象可能留下无法利用的小块。记录 free、minimum-ever-free、largest-free-block、失败次数和分配调用点,比单一 RAM 百分比更能说明风险。

C · STATIC OBJECTS
static StackType_t worker_stack[512]; static StaticTask_t worker_tcb; static uint8_t queue_storage[8 * sizeof(frame_t)]; static StaticQueue_t queue_tcb; /* create with xTaskCreateStatic/xQueueCreateStatic */

静态数组让上限可见;大小、对齐、section、栈水位和具体 API 仍需当前版本核对。

PYTHON · MEMORY BUDGET
def memory_budget(stack, queue, heap, limit): if min(stack, queue, heap, limit) < 0: raise ValueError("memory values must be non-negative") total = stack + queue + heap return {"total": total, "within_limit": total <= limit}

预算模型检查加总,不会测量栈峰值、碎片、cache、DMA 可见性或 linker 段属性。

10.4.3 任务栈预算

任务栈要覆盖最深调用链、局部数组、库函数、异常路径、格式化日志、FPU 上下文和中断嵌套带来的额外需求。high-water mark 只说明在已执行路径上留下的最小余量,不能证明未走到的路径安全。把栈预算、编译选项、优化级别、输入集合和测量窗口一起版本化。

表 4 · 内存与栈:确定性从哪里来的证据判断表
对象应留下的证据不能直接推出
对象static/dynamic/lifetime剩余 RAM 足够
栈high-water/overflow一次运行无溢出就是安全
缓冲owner/cache/DMA指针入队无需归属

10.4.4 栈溢出检测与恢复

栈溢出检测可以用哨兵、边界检查、MPU、端口 hook 或 fault 现场,但每种方法覆盖的时机不同。检测到溢出后继续运行通常会扩大数据破坏,安全动作应是记录任务名、栈指针、调用现场、复位原因并进入受控停止或恢复。不要把‘没有触发 hook’写成‘绝不会溢出’。

10.4.5 缓冲区所有权与生命周期

零拷贝和 DMA 让缓冲所有权更复杂:生产者可能只拥有填充阶段,DMA 控制器拥有传输阶段,消费者拥有解析阶段,回收者拥有释放阶段。每次转移都要有状态、屏障/缓存动作和失败回收。如果队列只传指针而没有长度、容量和 owner,最容易出现覆盖、双重释放和旧数据重用。

图 4.1 · 内存与栈:确定性从哪里来的判断路径每一格都要有自己的条件。
布局Flash/RAM/stack
分配static/dynamic
使用queue/buffer
证据map/watermark

从左到右是思考顺序,不是把时间、调度、资源和恢复证据压缩成一个“通过”。

10.4.6 ISR、DMA 与分配禁区

ISR 中不能依赖可能阻塞或不可重入的分配器;实时路径也不应把堆碎片和锁竞争引入每次事件。预分配固定大小块、无锁环形缓冲或生产者专属池可以减少不确定性,但会把容量和耗尽策略前置。目标机还要核对 DMA 能否访问该区域以及 cache clean/invalidate 的时机。

10.4.7 确定性、缓存与对齐

对齐、缓存属性、内存总线和 Flash wait state 都会影响执行时间和数据一致性。一个主机 Python 列表没有 cache miss、总线仲裁或非对齐陷阱;目标机应记录地址、段属性、DMA 可见性、cache 操作、测量工具和最坏路径。将内存放入某个 section 也不能自动证明 linker、MPU 和外设配置一致。

10.4.8 本节交付:内存预算卡

内存预算卡包括 object、count、size、alignment、section、owner、lifetime、peak、stack_high_water、heap_floor、largest_block、dma_cache_rule、failure_hook、reset_action、host_result、target_result 和 stop_conditions。它让调度和看门狗能引用明确的内存边界,而不是在故障后才猜‘是不是 RAM 不够’。

CHECKPOINT 10.4把“任务栈、堆、队列和缓冲怎样预算,才能让实时性不被分配和溢出破坏?”写成一份可复查记录

请围绕“内存与栈:确定性从哪里来”写出对象、输入/释放条件、调度或同步机制、输出与完成语义、一个时间或内存约束、一个失败路径和对应证据。没有目标板时,运行主机路径并把未执行字段标为未执行;拥有目标机时,还要补齐芯片、端口、时钟、FreeRTOSConfig、工具链、仪器、负载和观察条件。不要用一次平均延迟、一个 heartbeat、一个队列发送成功或一张截图覆盖没有做过的验证。

合格记录要让没有现场上下文的读者知道先查什么、先运行什么、看到什么才算通过,以及哪一句结论仍然不能推出。未知项、停止条件和回滚动作都是交付的一部分。

05 · WATCHDOG AND OVERLOAD

看门狗与过载:失败时怎样活下来

看门狗是最后一道时间边界,不是把系统永远保持运行的按钮。健康监视器要检查关键任务是否真正完成、队列是否有进展、内存是否越界、通信是否恢复,并在超时前保存原因、进入安全态或执行有界复位。喂狗应证明进展,而不是证明某个线程还活着。

看懂区分硬件看门狗、任务看门狗和健康检查。
说清建立过载、死锁、活锁和超时分类。
开始做设计有证据的降级、复位和恢复路径。
官方基线FreeRTOS Long Term Support用于迁移与维护对照,不改变课程工程实际锁定的 V10.3.1。锁定:202604.00-LTS / Kernel V11.3.0 (对照资料) · 核对日期:2026-09-05

10.5.1 看门狗的职责边界

硬件看门狗通常由独立或不同于主 CPU 的时钟计时,软件看门狗则由任务或监视器收集健康证据。两者职责不同:软件层可以解释哪个任务、队列或锁没有进展,硬件层可以在 CPU 卡死时复位。若软件监视器无条件调用 refresh,硬件狗只证明监视器线程还在运行,不能证明控制回路完成。

10.5.2 健康监视器看什么

健康检查应选择可代表系统进展的信号,例如任务完成序号、消费计数、最近一次有效通信、传感器样本时间和输出更新版本。单纯 heartbeat 可能在死循环中继续翻转;把 progress、last_success、queue_age、lock_wait 和 error_state 组合起来,才能区分‘线程活着’与‘系统做了正确的事’。

C · PROGRESS WATCHDOG
typedef struct { uint32_t last_progress; TickType_t last_kick; } health_t; static bool healthy(const health_t *h, TickType_t now, TickType_t timeout) { return (TickType_t)(now - h->last_kick) <= timeout && h->last_progress != 0; }

监视进展而非线程存活;硬件窗口、复位原因和安全输出仍需目标机验证。

PYTHON · TIMEOUT CLASSIFIER
def classify_timeout(stage, elapsed, budget): if elapsed < 0 or budget <= 0: raise ValueError("invalid timing") if elapsed <= budget: return {"status": "within_budget", "stage": stage} return {"status": "timeout", "stage": stage, "overrun": elapsed - budget}

分类器保留阶段和超时量;它不判断 brownout、硬件复位或真实执行器安全。

10.5.3 超时的分类与证据

超时记录要说明哪个时钟、哪个预算、从哪个事件开始计时以及最后一个已确认的完成点。deadline miss、锁等待、队列拥塞、DMA 不完成、时钟停摆、brownout 和外部设备失联的恢复动作不同。只写‘watchdog reset’会丢失最有价值的阶段信息,也无法判断是否应自动重试。

表 5 · 看门狗与过载:失败时怎样活下来的证据判断表
对象应留下的证据不能直接推出
健康progress/last success线程活着就是健康
故障timeout/overload/reset所有超时都重试
恢复snapshot/safe/rollback喂狗能修复死锁

10.5.4 过载、背压与降级

过载策略要在资源耗尽前选择优先级:先限制生产速率,再丢弃可重建数据、合并重复工作、降低采样、暂停非关键日志,最后进入安全态或复位。每一步都要有上限和恢复条件。无限重试、无限扩容或继续接收超出协议窗口的数据,会把暂时拥塞放大成永久失控。

10.5.5 死锁、活锁与饥饿

死锁是相互等待且没有转移,活锁是不断动作但状态不前进,饥饿是某个工作长期得不到资源。CPU 使用率高不代表有进展,队列计数和 heartbeat 也可能在活锁中变化。把进度序号、状态转移、锁图、队列年龄和有效输出一起观察,比只看任务是否运行更可靠。

图 5.1 · 看门狗与过载:失败时怎样活下来的判断路径每一格都要有自己的条件。
进展完成/消费
压力queue/CPU
判断timeout/fault
恢复safe/reset

从左到右是思考顺序,不是把时间、调度、资源和恢复证据压缩成一个“通过”。

10.5.6 故障隔离与安全态

故障隔离的目标是让一个任务的坏指针、超时或协议异常不会直接驱动危险输出。可以使用分层服务、独立任务、边界检查、消息复制、超时断路器和硬件安全输入;但隔离机制本身也消耗时间和内存。安全态要写成可执行动作,例如关闭 PWM、释放继电器、限制速度或停止发送。

10.5.7 复位原因与恢复序列

复位恢复应先读取并保存复位原因、故障快照、配置版本和最后进度,再清理旧状态、初始化时钟/内存/外设、验证关键资源,最后逐步恢复服务。不要在恢复早期立即喂狗或启动全部任务;若同一条件再次失败,要限制重启次数并进入维护或安全模式,避免复位风暴。

10.5.8 本节交付:恢复卡

恢复卡包括 monitor_id、health_signals、period、timeout、window、last_progress、fault_class、snapshot_fields、safe_outputs、reset_cause、retry_limit、backoff、rollback_image、maintenance_mode、host_replay、target_injection 和 stop_conditions。它将看门狗从‘最后一行代码’提升为可审查的故障处理合同。

CHECKPOINT 10.5把“任务失去进展、队列爆满或系统超时后,怎样避免‘继续喂狗’掩盖故障?”写成一份可复查记录

请围绕“看门狗与过载:失败时怎样活下来”写出对象、输入/释放条件、调度或同步机制、输出与完成语义、一个时间或内存约束、一个失败路径和对应证据。没有目标板时,运行主机路径并把未执行字段标为未执行;拥有目标机时,还要补齐芯片、端口、时钟、FreeRTOSConfig、工具链、仪器、负载和观察条件。不要用一次平均延迟、一个 heartbeat、一个队列发送成功或一张截图覆盖没有做过的验证。

合格记录要让没有现场上下文的读者知道先查什么、先运行什么、看到什么才算通过,以及哪一句结论仍然不能推出。未知项、停止条件和回滚动作都是交付的一部分。

06 · REAL-TIME DELIVERY

实时交付:从时间线到可回滚证据

实时交付要把配置、任务预算、同步选择、内存峰值、看门狗策略、trace、测试命令和目标机边界绑在同一个版本上。主机包应让读者在干净 Python 3.11/3.12 环境运行模型和测试;目标机记录则要补齐芯片、端口、时钟、仪器、负载、故障注入、升级和回滚证据。

看懂构建可重放的时间线和配置快照。
说清区分主机证据、目标机证据和未验证项。
开始做写出升级、回滚、维护和停止条件。
官方基线FreeRTOS Long Term Support用于迁移与维护对照,不改变课程工程实际锁定的 V10.3.1。锁定:202604.00-LTS / Kernel V11.3.0 (对照资料) · 核对日期:2026-09-05

10.6.1 时间线与最小 trace

最小实时 trace 需要同时记录 release、ready、start、block、wake、finish、deadline、task_id、priority 和 source。若只在任务末尾打印 elapsed,无法知道延迟来自事件产生、排队、抢占、锁、执行还是日志本身。时间戳要使用已声明的单调时基,并记录 tick/频率、单位、溢出处理和采样开销。

10.6.2 运行时统计与取样偏差

运行时统计有观察成本和偏差。周期采样可能漏掉很短的高峰,事件追踪可能改变缓存和调度,printf 可能阻塞或抢占关键任务。CPU 使用率、栈 high-water、队列水位和 p95/p99 都应说明采样窗口、工具版本、是否包含 ISR/idle、是否启用优化以及统计开销,不能把一个百分比当成完整证明。

PYTHON · TRACE SUMMARY
from collections import defaultdict def summarize(records): by_task = defaultdict(list) for row in records: by_task[row["task"]].append(row["finish"] - row["release"]) return {name: {"count": len(values), "max": max(values)} for name, values in by_task.items()}

主机脚本汇总时间线;目标机仍要绑定时钟、端口、测量开销和输入条件。

SHELL · PROVENANCE
python run_all_tests.py python -c "import hashlib, pathlib; p=pathlib.Path('src/realtime_model.py'); print(hashlib.sha256(p.read_bytes()).hexdigest())" # record FreeRTOSConfig, port, tick, compiler, map/ELF and target scope

命令、版本、哈希、测试输出和未验证范围共同组成实时交付证据。

10.6.3 主机测试与虚拟调度器

主机路径可以用标准库实现 utilization、deadline、队列背压、状态机、内存预算、看门狗和超时分类。读者应能在干净 Python 3.11/3.12 环境运行 `run_all_tests.py`,看到确定性断言和失败样例;这证明软件模型和交付步骤可复现,不证明具体 MCU 的 tick、IRQ、电气或温度行为。

表 6 · 实时交付:从时间线到可回滚证据的证据判断表
对象应留下的证据不能直接推出
时间线trace/release/finish一条平均曲线足够
来源config/toolchain/hash二进制名字可追溯
边界host/target/stop主机通过就是实机保证

10.6.4 目标机测量与工具配置

目标机测量必须写明芯片、板卡、FreeRTOS port、tick rate、时钟源、编译器、优化、探针、GPIO 标记、触发、负载和环境。示波器上的一条边沿只覆盖标记点之间的路径;它不能自动证明整个任务链、所有输入集合或长期稳定性。遇到过热、输出异常、复位风暴或未知电气状态时应立即停止并保存现场。

10.6.5 FreeRTOSConfig、版本与哈希

FreeRTOSConfig、链接脚本、编译器版本、源码提交、依赖锁和生成配置共同决定调度、内存和时间行为。一次构建应保存 source_hash、config_hash、toolchain、port、tick、optimization、map/ELF、测试输出和目标板修订。二进制文件名或构建时间不能代替 provenance,否则无法解释同一源码为何产生不同 deadline 结果。

图 6.1 · 实时交付:从时间线到可回滚证据的判断路径每一格都要有自己的条件。
重放输入/时间线
测量任务/资源
审查版本/边界
恢复升级/回滚

从左到右是思考顺序,不是把时间、调度、资源和恢复证据压缩成一个“通过”。

10.6.6 实时安全、权限与敏感日志

实时证据可能包含设备序列号、网络令牌、内存地址或内部故障现场。发布包要区分公开样例、调试日志和生产日志,必要时哈希/截断敏感字段,同时保留错误阶段、计数和时间关系。权限也属于合同:谁可以改变优先级、触发 reset、下载镜像、读取 raw trace,都要明确并可审计。

10.6.7 升级、回滚与不可逆动作

升级和回滚要优先保证可启动和安全态。写 Flash、切换镜像、改变时钟或启动执行器都是不可逆或高风险动作,不能靠无限重试。保存旧镜像、版本签名、更新阶段和复位原因;启动后先验证完整性、配置兼容和健康信号,失败时回到已知镜像或维护模式,而不是继续运行半更新系统。

10.6.8 评审、维护与已知限制

评审记录应列出已经运行的命令、输入、输出、最坏样本、目标机测量、已知限制、开放问题和下一步单变量实验。‘通过’要限定条件,例如‘在 host 模型、配置 v1、10 个任务和 reserve=20% 下可调度’,不能升级成‘所有板卡都满足实时性’。边界写得越清楚,维护者越能安全扩展。

10.6.9 本节交付:实时发布卡

实时发布卡包括 manifest、source_hash、config_hash、toolchain、port、tick、task_budgets、sync_policy、memory_budget、watchdog_policy、trace_schema、host_result、target_result、compatibility、rollback、security_scope、known_limits 和 stop_conditions。它把本章所有模型汇总为读者可运行、工程师可测量、产品可回滚的交付物。

CHECKPOINT 10.6把“怎样把实时系统交给读者和下一位工程师,而不是只留下一个能启动的固件?”写成一份可复查记录

请围绕“实时交付:从时间线到可回滚证据”写出对象、输入/释放条件、调度或同步机制、输出与完成语义、一个时间或内存约束、一个失败路径和对应证据。没有目标板时,运行主机路径并把未执行字段标为未执行;拥有目标机时,还要补齐芯片、端口、时钟、FreeRTOSConfig、工具链、仪器、负载和观察条件。不要用一次平均延迟、一个 heartbeat、一个队列发送成功或一张截图覆盖没有做过的验证。

合格记录要让没有现场上下文的读者知道先查什么、先运行什么、看到什么才算通过,以及哪一句结论仍然不能推出。未知项、停止条件和回滚动作都是交付的一部分。

资料卡(访问日期:2026-09-04):FreeRTOS 官方文档支撑任务、通知、互斥、事件组和静态/动态分配;Zephyr 官方文档提供调度、线程与工作队列的对照;Linux SCHED_DEADLINE 以 runtime/deadline/period 表达时间预算。通用模型不能替代当前 MCU、FreeRTOS port、时钟、FreeRTOSConfig、板卡、负载、测量工具和真实复位条件。