跳到主内容

技术Wiki详情

RTOS任务间通信架构规范

RTOS任务间通信架构规范

山东芯睿智控电子有限公司-嵌入式固件任务间通信架构规范 v4.1.2

山东芯睿智控电子有限公司 · 嵌入式固件任务间通信架构规范 v4.1.2

版本: v4.1.2
目标读者: 嵌入式固件开发工程师(含新入职员工)
平台: Cortex-M + FreeRTOS 单核 MCU
任务规模: 5 ~ 20 个功能任务
适用场景: 大结构体、少数字段跨任务路由、SRAM 敏感
指针让权模型
公司级规范
新员工友好

设计精髓 — 抢占、让权与即时通知

本架构的精髓只有一句话:功能任务把指针发到队列后立刻阻塞等待通知——这等价于同时让出了数据成员的使用权和 CPU 使用权;路由内核以系统最高优先级迅速抢占,完成纯数据拷贝后立即发通知,功能任务随即被唤醒继续运行。

① 内核最高优先级 路由内核 = configMAX_PRIORITIES - 1。队列一有数据,内核立刻抢占所有功能任务。
② 发队列 = 让权 xQueueSend 递交指针集合,交出成员读写权;ulTaskNotifyTake 阻塞,交出 CPU。
③ 内核 = 软件 DMA 仅按地址表做字段级 memcpy,不关心数据是什么——温度、按键、参数对内核都是字节块。正常路径耗时远小于一个时间片。
④ 处理完即通知 xTaskNotifyGive 唤醒功能任务,同时归还 CPU 与数据成员使用权。任务几乎无感延迟。
功能任务 路由内核 T0 T1 T2 T3 T4 写数据 构造指针集合 xQueueSend 递交指针集合 ulTaskNotifyTake 阻塞 挂起 · 让出 CPU(不占处理器) 收到 TaskNotify · 立即唤醒 使用权归还 · 继续业务 等待 / 运行其他低优先级任务 (功能任务阻塞期间不占 CPU) 最高优先级抢占 软件 DMA:按地址表搬运 · 不看数据含义 xTaskNotifyGive 发通知 ① 指针(任务→内核) ② 通知(内核→任务) 单核 MCU:任务阻塞 = 让出 CPU;内核抢占后独占处理器直到本轮拷贝结束;正常全程 < 1 个时间片 异常兜底:等待通知超时 100ms → fault_handler
功能任务 ↔ 路由内核:永远只有两类线
方向载体谁发送内容
任务 → 内核队列 xQueueSend功能任务指针集合(上行或下行描述符)
内核 → 任务xTaskNotifyGive路由内核处理完毕通知(镜像已吸收 / 下行已刷新)

任务若兼有产出与消费身份,与内核之间共 4 条线:上行指针、上行通知、下行指针、下行通知。纯消费任务为 2 条线:下行指针 + 下行通知。

记住这个等式: xQueueSend(指针集合) + ulTaskNotifyTake() = 让出数据成员使用权 + 让出CPU使用权。 内核处理完毕的 xTaskNotifyGive() 同时归还二者:任务被唤醒(CPU 使用权回来),且本轮指针操作已结束(数据成员使用权回来)。
心智模型 — 路由内核 ≈ 软件 DMA: 硬件 DMA 只根据源地址、目标地址、长度搬运内存,不解析载荷是温度还是图像。 路由内核同理:根据指针集与写出映射表做 memcpy,仅作数据搬运,不关心数据语义。 业务含义、有效性判断、是否可覆盖——全部留在功能任务侧。

1. 设计动机 — 解决什么问题?

1.1 从一个具体场景说起

假设你在写一个温度控制设备。整机业务数据碎片化分布在多个功能任务里,而不是一张集中的「整机大结构体」:

  • 传感器任务:ADC 原始值、滤波状态、temperature / ambient
  • 菜单任务:设定值、模式字;
  • 显示任务:界面用的显示缓存;
  • 控制任务:要拼温度 + 设定值做 PID,却不拥有这些数据的生产权。

若按习惯「谁需要谁就连一条队列」,通信关系一多,队列、缓冲、中间副本会散落在各个 *_task.c 之间:传感器→显示一条、传感器→控制一条、菜单→控制又一条……同一温度可能被拷贝多份,每个文件各自维护队列深度、超时与本地快照。任务越多,RAM 占用与端到端延迟往往比业务本身膨胀得更快。

不规范互联的典型后果
后果表现
SRAM 浪费每对任务关系一份队列缓冲 + 值拷贝;关系数随任务数增长,缓冲与副本在工程里重复堆积
数据延迟多级队列串行转发,显示/控制读到的温度比采样时刻晚好几跳,难以做时序分析。
不可审计改一个字段要搜遍全工程,不清楚经几条队列、几份快照,数据流无单一真相来源
竞态与补丁缺少统一让权协议,后期用 Mutex、全局变量或「再拷贝一份 local」打补丁,隐患难查。

落到单个传感器任务:其自有结构体约 48 字节,其中只有 temperatureambient 需要跨任务;其余 raw_adc[]、滤波状态等完全是自用。类似地,每个功能任务往往产出多、路由少——症结不仅是「结构体里无效字段多」,更是数据碎片 + 队列遍地分布

1.2 常见做法为什么不行?

  • 任务间点对点队列:队列句柄与缓冲分散在各源文件,关系网复杂,SRAM 随通信边数膨胀;
  • 全局变量:看似省队列,实则多任务读写无保护,撕裂读,且数据流仍不可追踪;
  • 每字段 / 每关系一条队列:队列爆炸,同一温度被值拷贝多遍,延迟与内存双高;
  • 整结构体多次拷贝:任务栈一份、中转缓冲一份、消费者再快照一份——路由 2 个 float,却拷贝 48 字节 × N 次。

1.3 本架构要解决的矛盾

  • 业务数据碎片化在多个任务,真正需要跨任务路由的只是各任务结构体中的少数成员;
  • 多个消费方各取所需,且字段来源不同(温度来自传感器,设定值来自菜单);
  • 必须把队列与中转收拢到路由内核,在省 SRAM、低延迟、零锁、可审计前提下完成字段级多对多分发。
传感器任务 自有结构体 SensorCtx_t (48B) temperature ← 路由 ambient ← 路由 raw_adc[8] 自用 filter_state 自用 cal_offset 自用 sample_cnt 自用 ... 其余自用 零散队列的代价 队列分散在各 task.c 多份缓冲 + 多跳拷贝 RAM 涨、延迟叠加 路由 2 字段,却维护 N 条链路 本架构: 只搬 2 个 float 中间仅 1 份 RouteCache 显示 (Consumer) 只要 temperature, ambient 2 × float = 8B 控制 (Consumer) temperature + 菜单参数 来自多个产出方

1.4 设计目标

本架构对任务间通信的核心要求如下。第一列特性表示该条所属维度,便于新人建立整体印象。

理想方案的要求
特性设计目标说明
成员级路由 按成员路由,不按整包 只交出需要跨任务的成员指针;自用成员永不进入内核。
单份缓存 中间只有一份路由缓存 内核 SRAM = 所有路由字段之和,不按任务各备一份完整结构体。
星型可审计 多对多、字段级 星型拓扑 + 写出映射表;任意字段的读写路径可搜索、可追踪。
无锁让权 零锁、无竞态 靠「指针使用权让渡 + 内核最高优先级」保证,不靠 Mutex。
星型解耦 任务解耦 功能任务之间无直连,一切经路由内核中转。

2. 核心思想 — 指针让权 + 单份路由缓存

功能任务把「指针集合」发到队列 = 主动让出这些成员的读写使用权,并请求内核服务。 内核相当于软件 DMA:只按地址表搬运字节,不关心数据是什么。 操作完毕后 xTaskNotifyGive = 同时归还 CPU 与数据成员使用权。 上行时内核读到路由缓存;下行时内核从路由缓存写出。 生产者指针绝不直连消费者指针——中间永远经过唯一一份路由缓存

2.0 架构全景

路由内核 软件 DMA · 仅搬运 · 两步循环 路由缓存 唯一跨任务副本 仅含路由字段 最新值覆盖 双向队列 上行: 指针集合(内核读) 下行: 指针集合(内核写) 归还: TaskNotify 写出映射 缓存 → 消费者指针 下行一次性直写 编译期静态 两步循环 ① 扫上行 → 读入缓存 ② 扫下行 → 写出缓存 无独立合并阶段
① 指针集(功能任务 → 内核) 经上行/下行队列递交成员地址,让出使用权。对 DMA 而言只是源地址表——一律当指针集处理
② 通知(内核 → 功能任务) xTaskNotifyGive 唤醒任务并归还数据成员使用权。内核不解释业务语义,只表示「本轮指针操作已完成」。

2.1 三个角色

角色职责架构要点
路由内核 两步循环:读上行指针集合进缓存,从缓存写下行指针集合。不执行业务逻辑。 仅 RouteCache + 写出映射表,内核不设第二份中间合并区。
功能任务(产出方) 数据保存在任务自有静态结构体。交出需路由的成员指针,等「镜像已吸收」通知后再写。 不交出自用成员;不整包拷贝进内核。
功能任务(消费方) 递交接收缓冲区成员指针,等「下行已刷新」通知后读取。 同一任务可同时是产出方和消费方。

2.2 数据通路一览

跨任务数据的唯一通路

产出方:任务 ─指针·上行队列→ 内核 ─读→ RouteCacheTaskNotify→ 任务
消费方:任务 ─指针·下行队列→ 内核 ─写← RouteCacheTaskNotify→ 任务
全程中间只有 RouteCache 一份副本;指针永远任务→内核,通知永远内核→任务。

3. 架构拓扑 — 星型结构与数据通路

3.1 功能任务与内核之间的连线规则

功能任务与路由内核之间只有两类载体、两个方向,不存在内核向任务发指针、也不存在任务向内核发通知:

线方向发送方接收方载体含义
指针线任务 → 内核功能任务路由内核队列 递交指针集合,让出成员使用权
通知线内核 → 任务路由内核功能任务TaskNotify 拷贝完成,归还成员使用权
双身份任务(产出+消费)与内核:4 条线 Sensor 产出 + 消费 路由内核 RouteCache 上行指针 镜像已吸收 下行指针 下行已刷新 粉色 = 指针(任务→内核) 绿色 = 通知(内核→任务) 实线 = 上行通道 虚线 = 下行通道 星型总览(任务间无直连) 内核 RouteCache Display Control Actuator Comm 纯消费 2 条线 · 双身份 4 条线 · 仅指针+通知 任务之间无连线,全部经内核中转
内核可见的数据通路(仅两条载体)
载体方向路径
指针集 功能任务 → 内核 上行队列:内核按描述符 任务 pub → RouteCache
下行队列:内核从 RouteCache 任务 input
通知 内核 → 功能任务 xTaskNotifyGive — 上行 ACK_ABSORBED;下行 ACK_REFRESHED(文档/注释用语)
核心约束: 相对内核只有上述两种数据;内核 ≈ 软件 DMA,仅搬运、不看内容。 不区分「事件」「状态」——那是功能任务自己的业务分类。任意两个功能任务之间无直连;跨任务数据经 RouteCache。指针集永远任务发,通知永远内核发。

4. 指针集合 — 产出类型与消费类型

功能任务与内核之间,队列里传递的永远是指针集合(Pointer Descriptor),由功能任务发出;通知永远由内核发出。队列句柄决定内核对这些指针执行读还是写。

4.1 任务自有数据 vs 路由数据

/* ================================================================
   文件: sensor_task.c — 传感器任务内部(示例)
   数据全部在任务静态区,内核永远拿不到完整结构体
   ================================================================ */

/* 任务自有业务上下文 — 不路由,不交出指针 */
typedef struct {
    int16_t  raw_adc[8];      /* ADC 原始采样,自用 */
    float    filter_state[4]; /* 滤波器状态,自用 */
    float    cal_offset;      /* 校准偏移,自用 */
    uint32_t sample_cnt;      /* 采样计数,自用 */
} SensorPrivate_t;

/* 需要跨任务发布的成员 — 嵌入在任务上下文中 */
typedef struct {
    float temperature;   /* 当前温度,路由字段 */
    float ambient;       /* 环境温度,路由字段 */
    float humidity;      /* 湿度,仅 Storage 需要时可路由 */
} SensorPublish_t;

/* 任务完整上下文 */
typedef struct {
    SensorPrivate_t  priv;     /* 自用,永不出任务 */
    SensorPublish_t  pub;      /* 可路由成员 */
} SensorCtx_t;

static SensorCtx_t g_sensor_ctx;  /* 必须静态分配,禁止用栈上结构体 */

4.2 上行指针集合(产出描述符)

上行描述符只包含需要内核读走的成员地址。编译期与 SensorPublish_t 一一对应。

/* 上行描述符 — 发到 upstream 队列,内核按指针读到 RouteCache */
typedef struct {
    float *p_temperature;   /* 指向 g_sensor_ctx.pub.temperature */
    float *p_ambient;       /* 指向 g_sensor_ctx.pub.ambient */
} SensorUpstreamDesc_t;

/* 构造描述符(每次上行前填充,指针目标不变) */
static SensorUpstreamDesc_t make_sensor_upstream_desc(void)
{
    SensorUpstreamDesc_t desc;
    desc.p_temperature = &g_sensor_ctx.pub.temperature;
    desc.p_ambient     = &g_sensor_ctx.pub.ambient;
    return desc;
}

4.3 下行指针集合(消费描述符)

下行描述符包含希望内核写入的成员地址。内核收到后,从 RouteCache 一次性直写到这些指针——全部映射字段都写,不做「本轮是否更新」的判断。

/* 下行描述符 — 发到 downstream 队列,内核从 RouteCache 写到这些地址 */
typedef struct {
    float *p_temperature;   /* 指向 g_display_ctx.input.temperature */
    float *p_ambient;       /* 指向 g_display_ctx.input.ambient */
} DisplayDownstreamDesc_t;
命名约定: 上行描述符后缀 UpstreamDesc_t,下行描述符后缀 DownstreamDesc_t。 同一任务可同时拥有两种描述符类型(双重身份)。

4.4 指针合法性硬约束

规则原因
指针必须指向任务静态区static 全局或任务私有静态结构体)栈上局部变量在函数返回后失效,内核异步访问会踩内存。
交出指针后至收到通知前,禁止读写这些成员否则与内核拷贝并发,产生撕裂写。
描述符本身可以栈上构造,但指针目标必须稳定队列传递的是描述符副本(值拷贝),内含的指针值不变。
自用成员不得出现在描述符中内核不应感知任务内部实现细节。

5. 队列语义 — 上行读 / 下行写

本架构中,队列句柄决定操作语义,而非两套完全不同的数据类型。同一类指针描述符格式,挂到不同队列上含义不同:

队列方向功能任务动作内核动作数据源 / 去向
queue_upstream_[task] 任务 → 内核 xQueueSend(desc) 让出读权 按 desc 中的指针读取字段值 写入 RouteCache
queue_downstream_[task] 任务 → 内核 xQueueSend(desc) 让出写权 从 RouteCache 一次性直写到 desc 全部指针 源自 RouteCache
功能任务 自有静态数据 pub / input 区 RouteCache 内核唯一副本 .sensor.temperature .sensor.ambient .param.setpoint ... 功能任务 input 接收区 等待内核写入 上行: 内核读指针 下行: 内核写指针 禁止: 生产者指针 ──直连──▶ 消费者指针

5.1 队列深度与元素大小

队列元素建议深度SRAM
上行 queue_upstream_*对应 UpstreamDesc_t(仅指针,无数据值)2深度 × sizeof(desc),通常 < 32B
下行 queue_downstream_*对应 DownstreamDesc_t2同上

深度设为 2 的原因:任务阻塞等待通知期间不应再次 Send;若超时重试,留 1 个槽位容错。

6. 使用权握手 — 让出、归还、超时

握手协议是设计精髓在代码层面的落地。功能任务侧只有两步:发队列等通知。阻塞等待不是空转——在 FreeRTOS 中任务进入 Blocked 状态,主动让出 CPU;内核以最高优先级被唤醒后抢占处理器,拷贝结束立即 xTaskNotifyGive,功能任务随即回到 Ready 并被调度运行。

6.1 统一握手协议

不论产出方还是消费方,任务侧操作模式完全一致:按时间从左到右执行 ①→⑤;指针竖直向下(任务交给内核),通知竖直向上(内核归还任务)。

功能任务 路由内核 ① 准备数据 ② xQueueSend 让出数据使用权 ③ ulTaskNotifyTake 阻塞 · 让出 CPU ⑤ 收到 TaskNotify 归还使用权 · 继续 ④ 内核处理 读/写指针 · 更新 RouteCache 指针 ↓ 通知 ↑ 时间 → 粉色 ↓ 指针(任务→内核) 绿色 ↑ 通知(内核→任务) 蓝色 → 任务侧步骤顺序

6.2 通知语义命名

方向任务动作内核动作通知名(推荐)任务收到后可做
上行 Send UpstreamDesc 读到 RouteCache ACK_ABSORBED 镜像已吸收 再次写入 pub 成员
下行 Send DownstreamDesc 从 RouteCache 写出 ACK_REFRESHED 下行已刷新 读取 input 成员

6.3 阻塞等待与 100ms 超时

所用 API 详见 附录 B: FreeRTOS API 参考

/* 任务侧等待内核归还 — 直接使用 FreeRTOS 原生 API */
#define KERNEL_ACK_TIMEOUT_MS   100U

xQueueSend(queue_upstream_sensor, &desc, portMAX_DELAY);
if (ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(KERNEL_ACK_TIMEOUT_MS)) == 0U) {
    fault_handler(FAULT_KERNEL_ACK_TIMEOUT, TASK_ID_SENSOR);
}
/* pdTRUE: 取走后清零通知计数;超时 100ms 进 fault */
只用原生 API:握手等待直接调用 ulTaskNotifyTake,不要另写易被误认为 FreeRTOS 官方 API 的项目层 wait 包装。附录 B 只收录 FreeRTOS 原生 API。
「死等」= 挂起让出 CPU,不是 while(1) 空转。 单核 MCU 上,功能任务在 ulTaskNotifyTake 中进入 Blocked 态,处理器空闲出来给调度器; 队列消息唤醒路由内核后,内核以最高优先级立即抢占,完成纯拷贝后 xTaskNotifyGive, 功能任务第一时间收到通知。正常路径全程远小于 1 个时间片。
架构铁律: 路由内核优先级 = configMAX_PRIORITIES - 1高于系统中所有功能任务。 编译期必须用 STATIC_ASSERT 校验。降低内核优先级等于摧毁整个无锁让权模型——此类配置错误不得在合入前存在。

7. 路由内核 — 两步循环

路由内核每轮循环只有两个步骤:先摄入上行、再刷新下行。无额外中间缓冲、无脏标志扫表。

把内核想成软件 DMA:它只执行「从 A 地址读到 RouteCache」「从 RouteCache 写到 B 地址」两类搬运,不解析、不判断、不解释载荷内容。写出映射表就是 DMA 的通道描述符。

第一步: 遍历所有上行队列 while (xQueueReceive(upstream[i], &desc, 0)) 按 desc 指针读取 → 写入 RouteCache xTaskNotifyGive(产出任务) /* 镜像已吸收 */ RouteCache 覆盖写,始终保持最新值 必须先完成全部上行,再进入第二步 第二步: 遍历所有下行队列 while (xQueueReceive(downstream[j], &desc, 0)) RouteCache → *desc 映射字段一次性直写 xTaskNotifyGive(消费任务) /* 下行已刷新 */ 无脏判断 · 无分支 · 全部映射字段都写 未本轮上行的缓存字段 = 上次最新值,照样写出

7.1 下行为什么「直接写」而不是「判断后分别写」?

RouteCache 的设计是最新值缓存:本轮没走上的行字段,里面仍然是历史最新值,天然有效。因此下行阶段不需要

  • 脏标志(dirty)判断「这个字段本轮有没有更新」;
  • 按字段 if/else 分支决定写或不写;
  • 先写入第二份中间缓冲再二次写出。

消费者递交 DownstreamDesc 后,内核调用对应的 refresh_*(),把映射表里的字段从上到下顺序赋值——一条直线写完,CPU 分支预测友好,比「判断分别写」更快。

效率原则: 上行按产出方摄入(读到缓存);下行按消费方直写(从缓存写出)。 缓存里永远是各字段的最新值,下行无脑全写即可。多写几个未变化的 float,远比分支判断便宜。

7.2 为什么没有独立的「合并阶段」?

  • 上行阶段已经把各产出方的字段收进了 RouteCache
  • 下行阶段消费者递交目标指针,内核按写出映射一次性直写——路由在写出时完成,无需中间缓冲槽。
时序保证: 每一轮内核循环严格「先全部上行、后全部下行」。 下行写出时,RouteCache 中所有字段均为可用最新值(本轮更新的 + 历史保留的),直接全部写出。

7.3 内核等待策略 — QueueSet 阻塞(推荐)

内核空闲时应零 CPU 占用。推荐用 FreeRTOS QueueSet(队列集) 聚合所有上行、下行队列,在 xQueueSelectFromSet 上阻塞等待——功能任务递交指针集后自动唤醒内核,无需 vTaskResume,也无需循环末尾 taskYIELD() / vTaskDelay()

为何不用「内核自挂起 + 任务 Resume」? 与 QueueSet 空闲效果相同,但 Send 漏配 Resume 会导致整机死锁。 QueueSet 由 RTOS 保证「入队即唤醒」,更简单、更安全。
/* QueueSet 聚合: 全部 upstream + downstream 队列(指针集入口) */
static QueueSetHandle_t s_queue_set = NULL;

static void kernel_v4_init(void)
{
    s_queue_set = xQueueCreateSet(QUEUE_SET_LENGTH);
    /* 每个队列创建后: xQueueAddToSet(queue_upstream_*, s_queue_set); */
    /* 每个队列创建后: xQueueAddToSet(queue_downstream_*, s_queue_set); */
}

/* 内核主循环骨架 */
for (;;) {
    /* 无指针集时内核挂起;任一任务 Send 指针集即唤醒 */
    (void)xQueueSelectFromSet(s_queue_set, portMAX_DELAY);

    kernel_drain_all_upstream();     /* 读指针集 → RouteCache → 通知 */
    kernel_drain_all_downstream();   /* RouteCache → 写指针集 → 通知 */

    /* 禁止 taskYIELD() / vTaskDelay():直接回到 QueueSelect */
}
做法空闲占 CPU唤醒方式评价
taskYIELD() 循环尾立即自抢占禁止
busy-poll 空队列100%禁止
自挂起 + 任务 Resume0手动,易漏不推荐
QueueSet + portMAX_DELAY0Send 自动唤醒推荐

8. 写出映射 — RouteCache 一次性直写

写出映射表写在路由内核中,是编译期静态的字段对应关系:RouteCache 的哪个成员 → DownstreamDesc 的哪个指针。下行时不做运行时判断,按映射顺序全部写出。

/* ================================================================
   写出映射 — 写在 kernel_route.c,编译后静态不可改
   上行: ingest_*  读到 RouteCache
   下行: refresh_* 从 RouteCache 一次性直写(无 if(dirty),无跳过)
   ================================================================ */

typedef struct {
    struct { float temperature; float ambient; float humidity; } sensor;
    struct { float setpoint; float kp, ki, kd; float limit_high, limit_low; } param;
    struct { float duty; uint8_t status; } control;
} RouteCache_t;

static RouteCache_t g_route_cache;

/* 上行摄入 — 按指针读到缓存 */
static void ingest_sensor_upstream(const SensorUpstreamDesc_t *p_desc)
{
    g_route_cache.sensor.temperature = *p_desc->p_temperature;
    g_route_cache.sensor.ambient     = *p_desc->p_ambient;
}

/* 下行直写 — 映射字段全部写出,不判断本轮是否更新
   未本轮上行的字段在缓存中仍是最新值,写出无害且更快 */
static void refresh_display_downstream(const DisplayDownstreamDesc_t *p_desc)
{
    *p_desc->p_temperature = g_route_cache.sensor.temperature;
    *p_desc->p_ambient     = g_route_cache.sensor.ambient;
}

static void refresh_control_downstream(const ControlDownstreamDesc_t *p_desc)
{
    *p_desc->p_temperature = g_route_cache.sensor.temperature;
    *p_desc->p_ambient     = g_route_cache.sensor.ambient;
    *p_desc->p_setpoint    = g_route_cache.param.setpoint;
    *p_desc->p_kp          = g_route_cache.param.kp;
    *p_desc->p_ki          = g_route_cache.param.ki;
    *p_desc->p_kd          = g_route_cache.param.kd;
}
禁止在 refresh_* 里写业务判断,例如 if (sensor_updated) *p_temperature = ...。 这违背「缓存即最新值」前提,且引入分支降低内核快进快出效率。

新同事想看「温度去哪了」?搜索 g_route_cache.sensor.temperature,即可找到所有 refresh_* 直写点。

9. 完整实现 — 伪代码从头写

9.1 路由内核头文件(kernel_v4.h)

/* ================================================================
   文件: kernel_v4.h — 路由内核对外 API(项目层:取队列句柄 / 启动,非握手封装)
   ================================================================ */

typedef enum {
    TASK_ID_SENSOR   = 0,
    TASK_ID_DISPLAY  = 1,
    TASK_ID_ACTUATOR = 2,
    TASK_ID_CONTROL  = 3,
    TASK_ID_STORAGE  = 4,
    TASK_ID_COMM     = 5,
    TASK_ID_MAX
} task_id_t;

/* 获取本任务的上行/下行队列句柄(初始化后有效) */
QueueHandle_t kernel_get_upstream_queue(task_id_t id);
QueueHandle_t kernel_get_downstream_queue(task_id_t id);

/* 握手等待: ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(KERNEL_ACK_TIMEOUT_MS)) */

/* 启动内核任务(内部校验优先级断言) */
void kernel_v4_start(void);

9.2 传感器任务 — 产出方示例

/* ================================================================
   文件: sensor_task.c — 产出方完整流程
   ================================================================ */
static QueueHandle_t s_upstream_q = NULL;

void sensor_task_main(void *arg)
{
    SensorUpstreamDesc_t desc;
    (void)arg;

    s_upstream_q = kernel_get_upstream_queue(TASK_ID_SENSOR);

    for (;;) {
        /* ---- 采集,写入任务自有 pub 区(filter_state 等自用区随意写)---- */
        g_sensor_ctx.pub.temperature = read_temperature();
        g_sensor_ctx.pub.ambient     = read_ambient();
        run_filter_on_private_data();  /* 只碰 priv 区,无需等通知 */

        /* ---- 构造上行描述符 ---- */
        desc.p_temperature = &g_sensor_ctx.pub.temperature;
        desc.p_ambient     = &g_sensor_ctx.pub.ambient;

        /* ---- 让出 pub 成员读权,请求内核摄入 ---- */
        xQueueSend(s_upstream_q, &desc, portMAX_DELAY);

        /* ---- 阻塞等镜像已吸收(正常 << 1 tick;异常 100ms 超时)---- */
        if (ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(KERNEL_ACK_TIMEOUT_MS)) == 0U) {
            fault_handler(FAULT_KERNEL_ACK_TIMEOUT, TASK_ID_SENSOR);
            continue;
        }

        /* 收到通知: pub 成员可再次写入 */
        vTaskDelay(pdMS_TO_TICKS(50));
    }
}

9.3 显示任务 — 消费方示例

/* ================================================================
   文件: display_task.c — 消费方完整流程
   ================================================================ */
static QueueHandle_t s_downstream_q = NULL;

void display_task_main(void *arg)
{
    DisplayDownstreamDesc_t desc;
    (void)arg;

    s_downstream_q = kernel_get_downstream_queue(TASK_ID_DISPLAY);

    for (;;) {
        /* ---- 递交希望内核写入的目标指针 ---- */
        desc.p_temperature = &g_display_ctx.input.temperature;
        desc.p_ambient     = &g_display_ctx.input.ambient;

        xQueueSend(s_downstream_q, &desc, portMAX_DELAY);

        /* ---- 让出 input 成员写权,等内核从 RouteCache 刷新 ---- */
        if (ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(KERNEL_ACK_TIMEOUT_MS)) == 0U) {
            fault_handler(FAULT_KERNEL_ACK_TIMEOUT, TASK_ID_DISPLAY);
            continue;
        }

        /* 收到通知: 可安全读取 input 成员 */
        update_lcd(g_display_ctx.input.temperature,
                   g_display_ctx.input.ambient);

        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

9.4 路由内核主循环(kernel_v4.c 核心)

/* ================================================================
   文件: kernel_v4.c — 两步循环 + 优先级断言
   ================================================================ */
#include "kernel_v4.h"
#include "kernel_route.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"

#define KERNEL_PRIORITY  (configMAX_PRIORITIES - 1)

/* 编译期铁律: 内核必须最高优先级 */
_Static_assert(KERNEL_PRIORITY > tskIDLE_PRIORITY,
    "kernel priority must be highest");

static RouteCache_t g_route_cache;
static QueueSetHandle_t s_queue_set;
static QueueHandle_t s_upstream[TASK_ID_MAX];
static QueueHandle_t s_downstream[TASK_ID_MAX];
static TaskHandle_t  s_task_handle[TASK_ID_MAX];

static void kernel_drain_all_upstream(void)
{
    for (int i = 0; i < TASK_ID_MAX; i++) {
        if (s_upstream[i] == NULL) continue;
        SensorUpstreamDesc_t desc;  /* 实际按任务类型分支,此处简化 */
        while (xQueueReceive(s_upstream[i], &desc, 0) == pdTRUE) {
            switch (i) {
            case TASK_ID_SENSOR:
                ingest_sensor_upstream((const SensorUpstreamDesc_t *)&desc);
                break;
            /* ... 其他产出方 ... */
            default: break;
            }
            xTaskNotifyGive(s_task_handle[i]);  /* ACK_ABSORBED */
        }
    }
}

static void kernel_drain_all_downstream(void)
{
    for (int j = 0; j < TASK_ID_MAX; j++) {
        if (s_downstream[j] == NULL) continue;
        DisplayDownstreamDesc_t desc;
        while (xQueueReceive(s_downstream[j], &desc, 0) == pdTRUE) {
            switch (j) {
            case TASK_ID_DISPLAY:
                refresh_display_downstream((const DisplayDownstreamDesc_t *)&desc);
                break;
            case TASK_ID_CONTROL:
                refresh_control_downstream((const ControlDownstreamDesc_t *)&desc);
                break;
            /* ... */
            default: break;
            }
            xTaskNotifyGive(s_task_handle[j]);  /* ACK_REFRESHED */
        }
    }
}

static void kernel_entry(void *arg)
{
    (void)arg;
    kernel_v4_init();  /* 创建 QueueSet、队列、注册句柄 */

    for (;;) {
        (void)xQueueSelectFromSet(s_queue_set, portMAX_DELAY);
        kernel_drain_all_upstream();    /* 第一步:读指针集 */
        kernel_drain_all_downstream();  /* 第二步:写指针集 */
        /* 回到 QueueSelect 阻塞,禁止 taskYIELD / vTaskDelay */
    }
}

10. 数据流演练 — 用具体数字走一遍

10.1 场景设定

时刻 T0传感器采集完成: temperature=36.5, ambient=28.0。显示任务需要刷新 LCD。

10.2 时间线

T做了什么RouteCache / 任务数据
T1 Sensor 写入 g_sensor_ctx.pub,Send UpstreamDesc 任务挂起,让出 pub 读权
T2 Display Send DownstreamDesc(递交 input 区写指针) 任务挂起,让出 input 写权
T3 内核 QueueSet 唤醒,进入本轮循环
T4 内核 ① 收到 Sensor 上行,摄入缓存 cache.sensor = {36.5, 28.0}
Notify Sensor (ACK_ABSORBED)
T5 内核 ② 收到 Display 下行,RouteCache 映射字段一次性直写 display.input = {36.5, 28.0}
Notify Display (ACK_REFRESHED)
T6 Sensor ulTaskNotifyTake 返回 >0(ACK_ABSORBED 语义),可写 pub 开始下一轮采集
T7 Display ulTaskNotifyTake 返回 >0(ACK_REFRESHED 语义),读 input 刷新 LCD: 36.5℃, 28.0℃
观察要点
  • Sensor 的 priv 区(滤波状态等)全程不需握手,随时可写。
  • 内核本轮先上行后下行,Display 读到的温度与 Sensor 本轮提交一致。
  • 全程无 Mutex;RouteCache 仅内核访问,pub/input 区靠让权协议保护。
  • 跨任务拷贝次数: 2 个 float 进缓存 + 2 个 float 出缓存 = 16 字节(仅搬路由字段)。
  • Sensor / Display 在 T1~T2 阻塞期间不占 CPU;内核在 T3~T5 抢占处理;T6/T7 收到通知后几乎同时唤醒。

11. 内核协议 — 只有指针集与通知

路由内核在心智模型上相当于软件 DMA:根据源/目标地址表搬运内存,仅作数据搬运,不关心数据是什么。 功能任务在业务层可以区分「温度」「按键」「参数」等语义,但内核不解析这些含义——就像硬件 DMA 不会区分搬运的是 ADC 采样值还是 UART 帧。

对内核而言,系统里只有两种载荷:

① 指针集 发送方:功能任务
载体:上行/下行队列中的描述符(内含若干成员地址)。
内核按写出映射读/写 RouteCache,完成后发通知。
② 通知 发送方:路由内核
载体:xTaskNotifyGive
语义:本轮指针操作结束,使用权已归还。

一句话概括:指针集由功能任务递交,通知由内核归还——不存在第三条「事件专用通道」或「值拷贝业务队列」。

视角内核(≈ 软件 DMA)功能任务(业务层)
关心什么源/目标地址、字段长度、写出映射对应关系温度是状态还是按键是事件、是否可覆盖、队列深度策略
不关心什么数据语义、单位、阈值、边沿/电平——DMA 不看内容RouteCache 内部布局(由映射表配置)
离散消息怎么办仍走指针集:任务在静态区写好 KeyEvent_t,把成员地址填入 UpstreamDesc 递交内核;不可丢、须排队等策略由任务侧队列深度与提交节奏保证,而非内核另开通道
/* 按键:ISR 只置位,任务写静态缓冲后以指针集上行 */
static KeyEvent_t     s_pending_key;   /* 静态区,地址稳定 */
static volatile bool  s_key_pending;   /* ISR 与任务间标志 */

void on_key_isr(uint8_t key, uint8_t type)
{
    s_pending_key.key_code   = key;
    s_pending_key.event_type = type;
    s_pending_key.timestamp  = xTaskGetTickCountFromISR();
    s_key_pending = true;  /* 不在 ISR 里向内核 Send 指针集 */
}

void key_task_loop(void)
{
    if (!s_key_pending) {
        return;
    }
    s_key_pending = false;

    KeyUpstreamDesc_t desc = {
        .p_key_code   = &s_pending_key.key_code,
        .p_event_type = &s_pending_key.event_type,
        .p_timestamp  = &s_pending_key.timestamp,
    };
    xQueueSend(queue_upstream_key, &desc, portMAX_DELAY);
    if (ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(KERNEL_ACK_TIMEOUT_MS)) == 0U) {
        fault_handler(FAULT_KERNEL_ACK_TIMEOUT, TASK_ID_KEY);
    }
}

12. 新手常见错误与 FAQ

(1) 指针指向栈上局部变量
SensorUpstreamDesc_t desc; 描述符本身可在栈上构造,但 desc.p_temperature = &local_temp; 绝对禁止——函数返回后内核读到垃圾值。
(2) 交出指针后继续写 pub/input 成员
Send 之后、ulTaskNotifyTake 返回之前修改了 g_sensor_ctx.pub.temperature,与内核摄入并发,数据撕裂。
(3) 把自用成员放进 UpstreamDesc
filter_state 不应暴露给内核——违反封装,且浪费队列/缓存空间。
(4) 内核 busy-poll 空队列
不用 QueueSet 阻塞等待,CPU 空转会吃掉「少拷贝」带来的收益。
(5) 内核循环末尾 taskYIELD / vTaskDelay / busy-poll
最高优先级下 taskYIELD() 会立即自抢占;vTaskDelay() 也多余。 应使用 QueueSet + xQueueSelectFromSet(..., portMAX_DELAY) 阻塞,有 Send 自动唤醒,无消息零 CPU。

12.0 术语易混点(速查)

说法易误解为正确理解
队列空位内核里的「缓冲槽」FreeRTOS 队列剩余容量(§5.1);内核没有第二份待合并中间缓冲
值拷贝一律禁止禁止业务数据多遍拷贝;描述符入队的小结构体拷贝是 RTOS 机制,允许
两种数据温度/按键等业务类型内核仅见指针集 + 通知
镜像已吸收任务侧又多一份副本RouteCache 已写入上行字段的通知语义名
特性(§1.4)版本来源或评审分内存/拓扑/并发等维度标签
ACK / ACK_*FreeRTOS 或项目里的 API 名注释/文档语义名;握手实现是原生 ulTaskNotifyTake / xTaskNotifyGive

FAQ

Q: 消费方还需要做本地快照吗?
A: ulTaskNotifyTake 返回后(下行语义名 ACK_REFRESHED),input 区在下次 Send 之前不会被内核改写,通常可直接使用。若本帧计算耗时超过下游刷新周期,应在读完后立即拷贝到局部变量或尽快再次 Send 交出写权。

Q: 一个任务既有上行又有下行,先等哪个?
A: 按业务顺序:通常先递交上行(产出)、再递交下行(消费)(或反之),两次独立握手。同一轮内核循环会统一处理。

Q: 100ms 超时到了怎么办?
A: 说明内核未在时限内归还使用权——属于严重故障。记录 task_id、队列积压、内核栈水位,进入 fault_handler(复位或安全态),禁止静默继续。

Q: 阻塞等通知会不会浪费 CPU?
A: 不会。ulTaskNotifyTake 使任务进入 Blocked 态,CPU 让给调度器。内核被队列唤醒后以最高优先级抢占,拷贝完成后立即通知——功能任务几乎无感。这是设计精髓,不是性能缺陷。

Q: 能把内核优先级调低吗?
A: 不能。这是架构前提,不是性能调优旋钮。降低内核优先级将破坏抢占与让权模型,导致数据竞态。

Q: 内核能不能在搬运时做阈值判断或单位换算?
A: 不能。内核是软件 DMA,只做 memcpy。换算、滤波、告警逻辑必须在功能任务完成,结果写入静态区后再以指针集递交。

13. 附录 A: 命名规范

统一命名降低 Code Review 成本,也使 grep / 静态分析可脚本化。以下规则强制,合入前须逐项对照。

13.1 总则

规则说明
全小写 + 下划线C 标识符使用 snake_case;类型名使用 PascalCase_t 后缀 _t
任务名英文小写与队列、上下文后缀一致:sensordisplaycontrol
前缀表归属g_ 全局 · s_ 文件内静态 · 无下划线局部变量
内核私有路由缓存、QueueSet、任务句柄表等仅在 kernel_v4.cstatic,不写入 kernel_v4.h
禁止缩写歧义q/que 不混用;统一 queue_ 前缀表示 FreeRTOS 队列句柄

13.2 文件与模块

文件职责
kernel_v4.h / kernel_v4.c路由内核任务、QueueSet、两步循环、对外 API
kernel_route.h / kernel_route.cRouteCache_t、写出映射表、ingest_* / refresh_*
task_[name].c单个功能任务:g_[name]_ctx、业务循环、递交指针集
task_ids.htask_id_t 枚举、TASK_ID_* 常量

13.3 任务上下文与描述符

对象命名模式示例说明
任务上下文结构体[Task]Ctx_tSensorCtx_t含 pub / priv / input 分区
全局上下文实例g_[task]_ctxg_sensor_ctx静态存储,指针目标须稳定
产出区(上行源).pubg_sensor_ctx.pub.temperature仅路由字段指针可填入 UpstreamDesc
自用区.privg_sensor_ctx.priv.filter_state永不进入描述符
消费区(下行目标).inputg_display_ctx.input.temperatureDownstreamDesc 指向此区成员
上行描述符类型[Task]UpstreamDesc_tSensorUpstreamDesc_t仅含 float * 等指针字段
下行描述符类型[Task]DownstreamDesc_tDisplayDownstreamDesc_t消费方递交的写目标指针集
描述符指针字段p_[field]p_temperature与 RouteCache / input 字段名对应

13.4 队列、任务 ID 与内核对象

对象命名模式示例
任务 ID 枚举TASK_ID_[TASK]TASK_ID_SENSORTASK_ID_DISPLAY
上行队列(内核创建)queue_upstream_[task]queue_upstream_sensor
下行队列queue_downstream_[task]queue_downstream_display
任务侧队列句柄缓存s_upstream_q / s_downstream_q启动时 kernel_get_*_queue() 取得
路由缓存g_route_cachekernel_route.c 内 static,不导出
QueueSets_queue_set内核私有
内核任务入口kernel_entryxTaskCreate(kernel_entry, ...)
内核优先级宏KERNEL_PRIORITY固定 configMAX_PRIORITIES - 1

13.5 函数、宏与通知语义

对象命名模式示例
上行摄入ingest_[task]_upstream()按 UpstreamDesc 指针读入 RouteCache
下行刷新refresh_[task]_downstream()RouteCache 一次性直写 DownstreamDesc 目标
握手等待直接 ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(ms))原生 API;返回 0 表示超时
ACK 超时常量KERNEL_ACK_TIMEOUT_MS推荐 100U(毫秒)
上行完成语义ACK_ABSORBED注释:镜像已吸收
下行完成语义ACK_REFRESHED注释:下行已刷新
故障处理fault_handler()ACK 超时、队列异常等统一入口
功能任务周期延时vTaskDelay(pdMS_TO_TICKS(n))仅业务节拍;禁止用于内核主循环

13.6 命名反例(禁止)

反例问题应改为
sensorQueue驼峰 + 未标明上下行queue_upstream_sensor
g_data无任务归属g_sensor_ctx
send_to_kernel()未区分上下行语义内联 xQueueSend(s_upstream_q, ...) + ulTaskNotifyTake(...)(勿再包一层 wait 函数)
event_queue_keyv4 无独立事件队列queue_upstream_key
ISR 内 xQueueSend 指针集破坏让权模型ISR 置位 → 任务写静态区 → Send

14. 附录 B: FreeRTOS API 参考

本架构直接依赖以下 FreeRTOS API。表中「本架构用法」标明在 v4 模型中的固定角色;参数说明基于 FreeRTOS V10.x / V11.x 任务上下文 API,ISR 上下文另注。

API 角色总览
角色API调用方
递交指针集xQueueSend功能任务
取出指针集xQueueReceive路由内核
等待搬运完成ulTaskNotifyTake功能任务
通知搬运完成xTaskNotifyGive路由内核
空闲阻塞xQueueSelectFromSet路由内核
队列创建与入集xQueueCreatexQueueCreateSetxQueueAddToSet内核初始化

14.1 队列 — 指针集载体

xQueueCreate

说明
功能创建 FreeRTOS 队列,分配队列控制块与存储区
原型QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize);
uxQueueLength队列深度,取值范围 ≥1;本架构上行/下行推荐 4~8,须覆盖「任务连续递交指针集而内核尚未 drain」的峰值
uxItemSize单条消息字节数 = sizeof(UpstreamDesc_t)sizeof(DownstreamDesc_t);队列做描述符值拷贝,内含指针值不变
返回值队列句柄;失败返回 NULL(堆不足),启动阶段须断言非空
本架构用法内核 kernel_v4_init() 为每个任务创建一对 queue_upstream_* / queue_downstream_*
queue_upstream_sensor = xQueueCreate(4, sizeof(SensorUpstreamDesc_t));

xQueueSend

说明
功能将一条消息拷贝入队;若队列满则按超时阻塞或立即返回
原型BaseType_t xQueueSend(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait);
pvItemToQueue指向栈上或静态的 UpstreamDesc_t / DownstreamDesc_t;拷贝的是描述符本身,非指针目标数据
xTicksToWait阻塞 tick 数;功能任务递交指针集常用 portMAX_DELAY(必须送出)或 0(非阻塞探测);禁止在 ISR 中调用(除 FromISR 变体,且本架构 ISR 不 Send 指针集)
返回值pdPASS 入队成功;errQUEUE_FULL 超时或队列满
本架构用法功能任务递交指针集 = 让出 pub/input 成员使用权;Send 后须立即 ulTaskNotifyTake,Send 与 Take 之间禁止写已交出成员
SensorUpstreamDesc_t desc = { .p_temperature = &g_sensor_ctx.pub.temperature, ... };
xQueueSend(s_upstream_q, &desc, portMAX_DELAY);
if (ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(KERNEL_ACK_TIMEOUT_MS)) == 0U) {
    fault_handler(FAULT_KERNEL_ACK_TIMEOUT, TASK_ID_SENSOR);
}

xQueueReceive

说明
功能从队列取出一条消息拷贝到缓冲区;队列为空时阻塞或立即返回
原型BaseType_t xQueueReceive(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait);
pvBuffer接收描述符的缓冲区,大小 ≥ uxItemSize
xTicksToWait内核 drain 循环内使用 0(非阻塞):while (xQueueReceive(q, &desc, 0) == pdTRUE),一次清空当前积压
返回值pdTRUE 取到消息;pdFALSE 超时或队列空
本架构用法仅路由内核调用;取到后按描述符做 memcpy(软件 DMA),完毕 xTaskNotifyGive

14.2 任务通知 — 搬运完成握手

ulTaskNotifyTake

说明
功能当前任务等待任务通知;可减计数或清零计数后返回
原型uint32_t ulTaskNotifyTake(BaseType_t xClearCountOnExit, TickType_t xTicksToWait);
xClearCountOnExitpdTRUE:返回时将通知计数清零(本架构必须pdTRUE,避免残留通知导致下次误唤醒)
xTicksToWait阻塞 tick;示例 ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(KERNEL_ACK_TIMEOUT_MS))portMAX_DELAY 仅用于 xQueueSend
返回值收到通知前的计数值;超时返回 0
本架构用法功能任务在 xQueueSend 之后调用;返回 >0 表示内核已通知(数据成员可安全访问且任务已唤醒);超时进入 fault_handler
#define KERNEL_ACK_TIMEOUT_MS  100U

if (ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(KERNEL_ACK_TIMEOUT_MS)) == 0U) {
    fault_handler(FAULT_KERNEL_ACK_TIMEOUT, TASK_ID_SENSOR);
}

xTaskNotifyGive

说明
功能向指定任务发送通知,通知计数 +1;若目标任务阻塞在 Take 上则唤醒
原型void xTaskNotifyGive(TaskHandle_t xTaskToNotify);
xTaskToNotify递交指针集的功能任务句柄;内核在 kernel_v4_init 注册 s_task_handle[task_id]
本架构用法内核在每次 ingest_* / refresh_* 完成后立即调用;语义为 ACK_ABSORBED 或 ACK_REFRESHED,不携带额外数据
ingest_sensor_upstream(&desc);
xTaskNotifyGive(s_task_handle[TASK_ID_SENSOR]);
为何用 TaskNotify 而非二值信号量? 每任务已有独立通知通道,零额外 RAM;Give/Take 与「单任务单次握手」语义一致。禁止用全局信号量代替 per-task 通知。

14.3 QueueSet — 内核零 CPU 等待

xQueueCreateSet

说明
功能创建队列集,用于聚合多个队列上的入队事件
原型QueueSetHandle_t xQueueCreateSet(const UBaseType_t uxEventQueueLength);
uxEventQueueLength须 ≥ 所有成员队列深度之和(各 upstream + downstream 深度累加),否则高负载时可能丢唤醒事件
本架构用法内核持有一个 s_queue_set;所有 queue_upstream_*queue_downstream_* 创建后 xQueueAddToSet

xQueueAddToSet

说明
功能将已创建的队列加入队列集;入队时队列集可被选中有数据
原型BaseType_t xQueueAddToSet(QueueHandle_t xQueueOrSemaphore, QueueSetHandle_t xQueueSet);
注意队列加入 Set 后,不可再单独对该队列阻塞 Receive(须通过 Select 后再 Receive 成员队列)

xQueueSelectFromSet

说明
功能阻塞直到队列集中任一队列入队,返回该队列句柄
原型QueueHandle_t xQueueSelectFromSet(QueueSetHandle_t xQueueSet, TickType_t xTicksToWait);
xTicksToWait内核主循环使用 portMAX_DELAY:无指针集时任务阻塞,零 CPU 占用
返回值有数据的成员队列句柄;超时返回 NULL
本架构用法唤醒后仍须 kernel_drain_all_upstream/downstream 遍历全部成员队列(非只处理返回的那一个),保证每轮「先上行后下行」完整执行
for (;;) {
    (void)xQueueSelectFromSet(s_queue_set, portMAX_DELAY);
    kernel_drain_all_upstream();
    kernel_drain_all_downstream();
}

14.4 任务创建与优先级

xTaskCreate

说明
功能创建任务并加入就绪列表
原型BaseType_t xTaskCreate(TaskFunction_t pxTaskCode, const char *pcName, uint16_t usStackDepth, void *pvParameters, UBaseType_t uxPriority, TaskHandle_t *pxCreatedTask);
uxPriority路由内核 必须configMAX_PRIORITIES - 1;功能任务 < 该值
pxCreatedTask内核须保存各功能任务句柄,供 xTaskNotifyGive 使用
本架构用法kernel_v4_start() 内创建 kernel_entry;功能任务在各自模块创建,并向内核注册句柄(或内核创建全部任务,按项目约定二选一且须统一)

configMAX_PRIORITIES

FreeRTOSConfig.h 中配置的最大优先级级数(不含 IDLE)。有效优先级为 0 … configMAX_PRIORITIES-1,数值越大越高。路由内核占用最高档,编译期 STATIC_ASSERT(KERNEL_PRIORITY > TASK_PRIO_SENSOR) 等逐任务校验。

14.5 时间与常量

符号含义本架构用法
portMAX_DELAY阻塞等待的最大 tick(通常 0xFFFFFFFF)xQueueSend(..., portMAX_DELAY)xQueueSelectFromSet(..., portMAX_DELAY)
pdMS_TO_TICKS(ms)毫秒转 tick,受 configTICK_RATE_HZ 影响ulTaskNotifyTake 超时、功能任务 vTaskDelay
pdTRUE / pdFALSE布尔常量ulTaskNotifyTake(pdTRUE, ...);API 返回值判断
pdPASS操作成功xQueueSend / xQueueReceive 成功

14.6 ISR 相关 API(不递交指针集)

API功能本架构用法
xTaskGetTickCountFromISR()ISR 内安全读取系统 tick按键等 ISR 打时间戳写入静态 s_pending_key
portYIELD_FROM_ISR(x)ISR 末尾请求上下文切换仅当 ISR 唤醒了更高优先级任务时使用;指针集递交在任务上下文完成

14.7 本架构禁止或慎用的 API

API原因
taskYIELD()(内核循环内)最高优先级任务 yield 会立即再被调度,无意义且破坏时序
vTaskDelay()(内核循环内)人为延迟内核响应;应使用 QueueSet 阻塞
空转 while (xQueueReceive(q,0)) 无 Select无消息时 busy-poll,浪费 CPU
Mutex 保护 RouteCache / pub / input与让权模型重复且易死锁;靠 Send+Notify 序列化访问
ISR 中 xQueueSend 递交 UpstreamDesc指针目标可能不稳定;ISR 仅置位,任务递交指针集

14.8 项目封装 API(kernel_v4.h)

功能任务通过原生 xQueueSend + ulTaskNotifyTake 与内核握手;队列句柄由下列项目 API 取得(非 FreeRTOS API):

函数功能参数 / 返回值
kernel_get_upstream_queue(task_id_t id)获取指定任务的上行队列句柄id:TASK_ID_*;返回 QueueHandle_t,生命周期至复位
kernel_get_downstream_queue(task_id_t id)获取指定任务的下行队列句柄同上
kernel_v4_start(void)创建 QueueSet、队列、内核任务无参;须在功能任务启动前或按文档约定顺序调用

15. 附录 C: Code Review 与修订记录

15.1 Code Review 清单

检查项
[ ]与内核交互是否仅有指针集(队列)+ 通知(TaskNotify),无第三条业务队列?
[ ]ISR 是否未直接向内核队列 Send(离散数据由任务写静态区后递交指针集)?
[ ]指针是否全部指向任务静态区?
[ ]Send 后、ulTaskNotifyTake 返回前是否未触碰已交出成员?
[ ]Send 后是否立即 ulTaskNotifyTake 阻塞(让出 CPU)?
[ ]自用成员是否未出现在 UpstreamDesc 中?
[ ]内核循环是否严格先上行、后下行?
[ ]上行/下行队列是否全部加入 QueueSet?
[ ]内核是否在 xQueueSelectFromSet(portMAX_DELAY) 上阻塞(非 busy-poll)?
[ ]内核循环末尾是否无 taskYIELD() / vTaskDelay()
[ ]内核优先级是否为 configMAX_PRIORITIES - 1
[ ]内核处理完是否立即 xTaskNotifyGive 归还使用权?
[ ]Send 后是否直接 ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(100)),超时进 fault?
[ ]下行 refresh_* 是否一次性直写全部映射字段(无 dirty 判断)?
[ ]写出映射表是否只含字段对应关系、无业务判断?
[ ]内核 ingest/refresh 是否仅为 memcpy(软件 DMA),无阈值/换算/分支业务?
[ ]命名是否符合附录 A(队列 queue_upstream_*、上下文 g_*_ctx、描述符 p_*)?
[ ]ulTaskNotifyTake 是否使用 pdTRUE 清零计数?xQueueReceive drain 是否用超时 0

15.2 修订记录

版本日期说明
v4.02026-06-10首版:指针让权、RouteCache 单副本、内核两步循环、最高优先级抢占、100ms ACK 超时
v4.0.12026-06-10全屏布局;去除外部版本引用;新增「设计精髓」专章
v4.0.22026-06-10修正时序图排版;星型图改为指针(任务→内核)+通知(内核→任务);图例移出 SVG
v4.0.32026-06-10明确下行 RouteCache 一次性直写,禁止 dirty 分支判断
v4.0.42026-06-10内核主循环末尾 taskYIELD 改为 vTaskDelay(1ms)
v4.0.52026-06-10代码块语法高亮;HTML 转义修正;术语统一;图表与文案自查
v4.0.62026-06-10内核空闲策略定为 QueueSet+portMAX_DELAY;去掉循环尾 vTaskDelay
v4.0.72026-06-10代码配色改 VS Code Dark+;双通道 banner;补 kernel_drain_all_events
v4.0.82026-06-10废除事件/状态双通道;内核仅指针集+通知;删除 event_queue 与值拷贝业务队列
v4.0.92026-06-10确立「内核 ≈ 软件 DMA」心智模型:仅搬运数据,不关心数据语义
v4.1.02026-06-10扩充附录:命名规范细则 + FreeRTOS API 功能/参数/用法参考
v4.1.12026-06-10§1.1 碎片化动机;§1.4 特性列;让权等式修正;原生 ulTaskNotifyTake;清理 hljs 污染;恢复 CDN
v4.1.22026-06-10术语扫尾:ACK 与 Take 区分;去除 Submit/旧版 v4.0 表述;统一让权归还 CPU+成员

山东芯睿智控电子有限公司 · 嵌入式固件任务间通信架构规范 v4.1.2

评论留言(论坛样式,需管理员审核后显示)

当前身份:游客(将显示IP)

已审核评论

暂无已审核评论。