山东芯睿智控电子有限公司 · 嵌入式固件任务间通信架构规范 v4.1.2
目录
设计精髓 — 抢占、让权与即时通知
本架构的精髓只有一句话:功能任务把指针发到队列后立刻阻塞等待通知——这等价于同时让出了数据成员的使用权和 CPU 使用权;路由内核以系统最高优先级迅速抢占,完成纯数据拷贝后立即发通知,功能任务随即被唤醒继续运行。
configMAX_PRIORITIES - 1。队列一有数据,内核立刻抢占所有功能任务。
xQueueSend 递交指针集合,交出成员读写权;ulTaskNotifyTake 阻塞,交出 CPU。
xTaskNotifyGive 唤醒功能任务,同时归还 CPU 与数据成员使用权。任务几乎无感延迟。
| 方向 | 载体 | 谁发送 | 内容 |
|---|---|---|---|
| 任务 → 内核 | 队列 xQueueSend | 功能任务 | 指针集合(上行或下行描述符) |
| 内核 → 任务 | xTaskNotifyGive | 路由内核 | 处理完毕通知(镜像已吸收 / 下行已刷新) |
任务若兼有产出与消费身份,与内核之间共 4 条线:上行指针、上行通知、下行指针、下行通知。纯消费任务为 2 条线:下行指针 + 下行通知。
记住这个等式:xQueueSend(指针集合)+ulTaskNotifyTake()= 让出数据成员使用权 + 让出CPU使用权。 内核处理完毕的xTaskNotifyGive()同时归还二者:任务被唤醒(CPU 使用权回来),且本轮指针操作已结束(数据成员使用权回来)。
1. 设计动机 — 解决什么问题?
1.1 从一个具体场景说起
假设你在写一个温度控制设备。整机业务数据碎片化分布在多个功能任务里,而不是一张集中的「整机大结构体」:
- 传感器任务:ADC 原始值、滤波状态、
temperature/ambient; - 菜单任务:设定值、模式字;
- 显示任务:界面用的显示缓存;
- 控制任务:要拼温度 + 设定值做 PID,却不拥有这些数据的生产权。
若按习惯「谁需要谁就连一条队列」,通信关系一多,队列、缓冲、中间副本会散落在各个 *_task.c 之间:传感器→显示一条、传感器→控制一条、菜单→控制又一条……同一温度可能被拷贝多份,每个文件各自维护队列深度、超时与本地快照。任务越多,RAM 占用与端到端延迟往往比业务本身膨胀得更快。
| 后果 | 表现 |
|---|---|
| SRAM 浪费 | 每对任务关系一份队列缓冲 + 值拷贝;关系数随任务数增长,缓冲与副本在工程里重复堆积。 |
| 数据延迟 | 多级队列串行转发,显示/控制读到的温度比采样时刻晚好几跳,难以做时序分析。 |
| 不可审计 | 改一个字段要搜遍全工程,不清楚经几条队列、几份快照,数据流无单一真相来源。 |
| 竞态与补丁 | 缺少统一让权协议,后期用 Mutex、全局变量或「再拷贝一份 local」打补丁,隐患难查。 |
落到单个传感器任务:其自有结构体约 48 字节,其中只有 temperature、ambient 需要跨任务;其余 raw_adc[]、滤波状态等完全是自用。类似地,每个功能任务往往产出多、路由少——症结不仅是「结构体里无效字段多」,更是数据碎片 + 队列遍地分布。
1.2 常见做法为什么不行?
- 任务间点对点队列:队列句柄与缓冲分散在各源文件,关系网复杂,SRAM 随通信边数膨胀;
- 全局变量:看似省队列,实则多任务读写无保护,撕裂读,且数据流仍不可追踪;
- 每字段 / 每关系一条队列:队列爆炸,同一温度被值拷贝多遍,延迟与内存双高;
- 整结构体多次拷贝:任务栈一份、中转缓冲一份、消费者再快照一份——路由 2 个 float,却拷贝 48 字节 × N 次。
1.3 本架构要解决的矛盾
- 业务数据碎片化在多个任务,真正需要跨任务路由的只是各任务结构体中的少数成员;
- 多个消费方各取所需,且字段来源不同(温度来自传感器,设定值来自菜单);
- 必须把队列与中转收拢到路由内核,在省 SRAM、低延迟、零锁、可审计前提下完成字段级多对多分发。
1.4 设计目标
本架构对任务间通信的核心要求如下。第一列特性表示该条所属维度,便于新人建立整体印象。
| 特性 | 设计目标 | 说明 |
|---|---|---|
| 成员级路由 | 按成员路由,不按整包 | 只交出需要跨任务的成员指针;自用成员永不进入内核。 |
| 单份缓存 | 中间只有一份路由缓存 | 内核 SRAM = 所有路由字段之和,不按任务各备一份完整结构体。 |
| 星型可审计 | 多对多、字段级 | 星型拓扑 + 写出映射表;任意字段的读写路径可搜索、可追踪。 |
| 无锁让权 | 零锁、无竞态 | 靠「指针使用权让渡 + 内核最高优先级」保证,不靠 Mutex。 |
| 星型解耦 | 任务解耦 | 功能任务之间无直连,一切经路由内核中转。 |
2. 核心思想 — 指针让权 + 单份路由缓存
功能任务把「指针集合」发到队列 = 主动让出这些成员的读写使用权,并请求内核服务。
内核相当于软件 DMA:只按地址表搬运字节,不关心数据是什么。
操作完毕后 xTaskNotifyGive = 同时归还 CPU 与数据成员使用权。
上行时内核读到路由缓存;下行时内核从路由缓存写出。
生产者指针绝不直连消费者指针——中间永远经过唯一一份路由缓存。
2.0 架构全景
xTaskNotifyGive 唤醒任务并归还数据成员使用权。内核不解释业务语义,只表示「本轮指针操作已完成」。
2.1 三个角色
| 角色 | 职责 | 架构要点 |
|---|---|---|
| 路由内核 | 两步循环:读上行指针集合进缓存,从缓存写下行指针集合。不执行业务逻辑。 | 仅 RouteCache + 写出映射表,内核不设第二份中间合并区。 |
| 功能任务(产出方) | 数据保存在任务自有静态结构体。交出需路由的成员指针,等「镜像已吸收」通知后再写。 | 不交出自用成员;不整包拷贝进内核。 |
| 功能任务(消费方) | 递交接收缓冲区成员指针,等「下行已刷新」通知后读取。 | 同一任务可同时是产出方和消费方。 |
2.2 数据通路一览
产出方:任务 ─指针·上行队列→ 内核 ─读→ RouteCache ─TaskNotify→ 任务
消费方:任务 ─指针·下行队列→ 内核 ─写← RouteCache ─TaskNotify→ 任务
全程中间只有 RouteCache 一份副本;指针永远任务→内核,通知永远内核→任务。
3. 架构拓扑 — 星型结构与数据通路
3.1 功能任务与内核之间的连线规则
功能任务与路由内核之间只有两类载体、两个方向,不存在内核向任务发指针、也不存在任务向内核发通知:
| 线 | 方向 | 发送方 | 接收方 | 载体 | 含义 |
|---|---|---|---|---|---|
| 指针线 | 任务 → 内核 | 功能任务 | 路由内核 | 队列 | 递交指针集合,让出成员使用权 |
| 通知线 | 内核 → 任务 | 路由内核 | 功能任务 | TaskNotify | 拷贝完成,归还成员使用权 |
| 载体 | 方向 | 路径 |
|---|---|---|
| 指针集 | 功能任务 → 内核 | 上行队列:内核按描述符 读 任务 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 |
5.1 队列深度与元素大小
| 队列 | 元素 | 建议深度 | SRAM |
|---|---|---|---|
上行 queue_upstream_* | 对应 UpstreamDesc_t(仅指针,无数据值) | 2 | 深度 × sizeof(desc),通常 < 32B |
下行 queue_downstream_* | 对应 DownstreamDesc_t | 2 | 同上 |
深度设为 2 的原因:任务阻塞等待通知期间不应再次 Send;若超时重试,留 1 个槽位容错。
6. 使用权握手 — 让出、归还、超时
握手协议是设计精髓在代码层面的落地。功能任务侧只有两步:发队列、等通知。阻塞等待不是空转——在 FreeRTOS 中任务进入 Blocked 状态,主动让出 CPU;内核以最高优先级被唤醒后抢占处理器,拷贝结束立即 xTaskNotifyGive,功能任务随即回到 Ready 并被调度运行。
6.1 统一握手协议
不论产出方还是消费方,任务侧操作模式完全一致:按时间从左到右执行 ①→⑤;指针竖直向下(任务交给内核),通知竖直向上(内核归还任务)。
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 */
ulTaskNotifyTake,不要另写易被误认为 FreeRTOS 官方 API 的项目层 wait 包装。附录 B 只收录 FreeRTOS 原生 API。
ulTaskNotifyTake 中进入 Blocked 态,处理器空闲出来给调度器;
队列消息唤醒路由内核后,内核以最高优先级立即抢占,完成纯拷贝后 xTaskNotifyGive,
功能任务第一时间收到通知。正常路径全程远小于 1 个时间片。
configMAX_PRIORITIES - 1,高于系统中所有功能任务。
编译期必须用 STATIC_ASSERT 校验。降低内核优先级等于摧毁整个无锁让权模型——此类配置错误不得在合入前存在。
7. 路由内核 — 两步循环
路由内核每轮循环只有两个步骤:先摄入上行、再刷新下行。无额外中间缓冲、无脏标志扫表。
把内核想成软件 DMA:它只执行「从 A 地址读到 RouteCache」「从 RouteCache 写到 B 地址」两类搬运,不解析、不判断、不解释载荷内容。写出映射表就是 DMA 的通道描述符。
7.1 下行为什么「直接写」而不是「判断后分别写」?
RouteCache 的设计是最新值缓存:本轮没走上的行字段,里面仍然是历史最新值,天然有效。因此下行阶段不需要:
- 脏标志(dirty)判断「这个字段本轮有没有更新」;
- 按字段 if/else 分支决定写或不写;
- 先写入第二份中间缓冲再二次写出。
消费者递交 DownstreamDesc 后,内核调用对应的 refresh_*(),把映射表里的字段从上到下顺序赋值——一条直线写完,CPU 分支预测友好,比「判断分别写」更快。
7.2 为什么没有独立的「合并阶段」?
- 上行阶段已经把各产出方的字段收进了 RouteCache;
- 下行阶段消费者递交目标指针,内核按写出映射一次性直写——路由在写出时完成,无需中间缓冲槽。
时序保证: 每一轮内核循环严格「先全部上行、后全部下行」。 下行写出时,RouteCache 中所有字段均为可用最新值(本轮更新的 + 历史保留的),直接全部写出。
7.3 内核等待策略 — QueueSet 阻塞(推荐)
内核空闲时应零 CPU 占用。推荐用 FreeRTOS QueueSet(队列集) 聚合所有上行、下行队列,在 xQueueSelectFromSet 上阻塞等待——功能任务递交指针集后自动唤醒内核,无需 vTaskResume,也无需循环末尾 taskYIELD() / vTaskDelay()。
/* 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% | — | 禁止 |
| 自挂起 + 任务 Resume | 0 | 手动,易漏 | 不推荐 |
| QueueSet + portMAX_DELAY | 0 | Send 自动唤醒 | 推荐 |
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;
}
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
SensorUpstreamDesc_t desc; 描述符本身可在栈上构造,但 desc.p_temperature = &local_temp; 绝对禁止——函数返回后内核读到垃圾值。
Send 之后、
ulTaskNotifyTake 返回之前修改了 g_sensor_ctx.pub.temperature,与内核摄入并发,数据撕裂。
filter_state 不应暴露给内核——违反封装,且浪费队列/缓存空间。
不用 QueueSet 阻塞等待,CPU 空转会吃掉「少拷贝」带来的收益。
最高优先级下
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 |
| 任务名英文小写 | 与队列、上下文后缀一致:sensor、display、control |
| 前缀表归属 | g_ 全局 · s_ 文件内静态 · 无下划线局部变量 |
| 内核私有 | 路由缓存、QueueSet、任务句柄表等仅在 kernel_v4.c 内 static,不写入 kernel_v4.h |
| 禁止缩写歧义 | q/que 不混用;统一 queue_ 前缀表示 FreeRTOS 队列句柄 |
13.2 文件与模块
| 文件 | 职责 |
|---|---|
kernel_v4.h / kernel_v4.c | 路由内核任务、QueueSet、两步循环、对外 API |
kernel_route.h / kernel_route.c | RouteCache_t、写出映射表、ingest_* / refresh_* |
task_[name].c | 单个功能任务:g_[name]_ctx、业务循环、递交指针集 |
task_ids.h | task_id_t 枚举、TASK_ID_* 常量 |
13.3 任务上下文与描述符
| 对象 | 命名模式 | 示例 | 说明 |
|---|---|---|---|
| 任务上下文结构体 | [Task]Ctx_t | SensorCtx_t | 含 pub / priv / input 分区 |
| 全局上下文实例 | g_[task]_ctx | g_sensor_ctx | 静态存储,指针目标须稳定 |
| 产出区(上行源) | .pub | g_sensor_ctx.pub.temperature | 仅路由字段指针可填入 UpstreamDesc |
| 自用区 | .priv | g_sensor_ctx.priv.filter_state | 永不进入描述符 |
| 消费区(下行目标) | .input | g_display_ctx.input.temperature | DownstreamDesc 指向此区成员 |
| 上行描述符类型 | [Task]UpstreamDesc_t | SensorUpstreamDesc_t | 仅含 float * 等指针字段 |
| 下行描述符类型 | [Task]DownstreamDesc_t | DisplayDownstreamDesc_t | 消费方递交的写目标指针集 |
| 描述符指针字段 | p_[field] | p_temperature | 与 RouteCache / input 字段名对应 |
13.4 队列、任务 ID 与内核对象
| 对象 | 命名模式 | 示例 |
|---|---|---|
| 任务 ID 枚举 | TASK_ID_[TASK] | TASK_ID_SENSOR、TASK_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_cache | kernel_route.c 内 static,不导出 |
| QueueSet | s_queue_set | 内核私有 |
| 内核任务入口 | kernel_entry | xTaskCreate(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_key | v4 无独立事件队列 | queue_upstream_key |
ISR 内 xQueueSend 指针集 | 破坏让权模型 | ISR 置位 → 任务写静态区 → Send |
14. 附录 B: FreeRTOS API 参考
本架构直接依赖以下 FreeRTOS API。表中「本架构用法」标明在 v4 模型中的固定角色;参数说明基于 FreeRTOS V10.x / V11.x 任务上下文 API,ISR 上下文另注。
| 角色 | API | 调用方 |
|---|---|---|
| 递交指针集 | xQueueSend | 功能任务 |
| 取出指针集 | xQueueReceive | 路由内核 |
| 等待搬运完成 | ulTaskNotifyTake | 功能任务 |
| 通知搬运完成 | xTaskNotifyGive | 路由内核 |
| 空闲阻塞 | xQueueSelectFromSet | 路由内核 |
| 队列创建与入集 | xQueueCreate、xQueueCreateSet、xQueueAddToSet | 内核初始化 |
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); |
| xClearCountOnExit | pdTRUE:返回时将通知计数清零(本架构必须用 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]);
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.0 | 2026-06-10 | 首版:指针让权、RouteCache 单副本、内核两步循环、最高优先级抢占、100ms ACK 超时 |
| v4.0.1 | 2026-06-10 | 全屏布局;去除外部版本引用;新增「设计精髓」专章 |
| v4.0.2 | 2026-06-10 | 修正时序图排版;星型图改为指针(任务→内核)+通知(内核→任务);图例移出 SVG |
| v4.0.3 | 2026-06-10 | 明确下行 RouteCache 一次性直写,禁止 dirty 分支判断 |
| v4.0.4 | 2026-06-10 | 内核主循环末尾 taskYIELD 改为 vTaskDelay(1ms) |
| v4.0.5 | 2026-06-10 | 代码块语法高亮;HTML 转义修正;术语统一;图表与文案自查 |
| v4.0.6 | 2026-06-10 | 内核空闲策略定为 QueueSet+portMAX_DELAY;去掉循环尾 vTaskDelay |
| v4.0.7 | 2026-06-10 | 代码配色改 VS Code Dark+;双通道 banner;补 kernel_drain_all_events |
| v4.0.8 | 2026-06-10 | 废除事件/状态双通道;内核仅指针集+通知;删除 event_queue 与值拷贝业务队列 |
| v4.0.9 | 2026-06-10 | 确立「内核 ≈ 软件 DMA」心智模型:仅搬运数据,不关心数据语义 |
| v4.1.0 | 2026-06-10 | 扩充附录:命名规范细则 + FreeRTOS API 功能/参数/用法参考 |
| v4.1.1 | 2026-06-10 | §1.1 碎片化动机;§1.4 特性列;让权等式修正;原生 ulTaskNotifyTake;清理 hljs 污染;恢复 CDN |
| v4.1.2 | 2026-06-10 | 术语扫尾:ACK 与 Take 区分;去除 Submit/旧版 v4.0 表述;统一让权归还 CPU+成员 |
山东芯睿智控电子有限公司 · 嵌入式固件任务间通信架构规范 v4.1.2