Pipeline Factory and Compilation: 四段式职责分离
> Aligned with PCR Master Blueprint v1.0 — see Blueprint §2.6.4(跨域判据:simulation/ 是唯一允许聚合所有子域的层)、§3.1(FCC 三层架构)、§3.3(FCC 隔离)、§7.3(双代数化)。 > 职责:本文是 FCC 预编译机制的唯一权威文档,规定 GNC pipeline 从工厂构造到运行期 tick 的完整四段式职责分离。 > 要解决的问题:FccEnv heavy(气动表 / 增益矩阵 / 卡尔曼参数 / 调度表)+ 10ms 热点循环 ⇒ 动态调度禁区。同时,必须保证 GNC 算法、pipeline 工厂、mission 映射、跨域 tick 四者互不知情,以满足 layer-agnostic 设计与 HIL 互换能力。 > C-Distillation note:四段在蒸馏后仍然存在,但 std::function 闭包退化为函数指针 + 静态绑定参数表;MissionProfile 是只读 PoD。
1. 问题陈述
如果不做四段式分离,最容易踩的两个反模式:
反模式 A — strategy 内部按相位查表
// ❌ 错:外壳每拍 cache thrashing
auto strategy = read_imu() >>= [](auto imu) {
return get_state() >>= [imu](auto s) {
auto tasks = env.schedule.tasks_for_stage(s.current_stage); // map 查找
for (auto& task : tasks) {
switch (task.module) { /* 函数指针解引用 */ }
}
};
};反模式 B — 把 FccStage→algorithm 映射放进 simulation/
// ❌ 错:违反 Blueprint §2.6.4 跨域判据
namespace sim::pipeline {
InnerPipeline pick_pipeline(FccStage s, const FccEnv& env) {
switch (s) {
case FccStage::Boost1: return /* iterative guidance */;
// ...
}
}
}为什么 B 错:FccStage 是 FCC 自己的概念,FccEnv 是 FCC 自己的环境,"哪个 stage 用哪个制导律"是 firmware mission 知识。simulation/ 是跨域绑定层,不该持有任何子域的内部决策。
2. 四段式职责分离
┌───────────────────────────────────────────────────────────────────┐
│ ① fcc/pipelines/ Pipeline 工厂(层级无知) │
│ make_iter_guidance_pipeline(FccEnv) → InnerPipeline │
│ make_ballistic_pipeline(FccEnv) → InnerPipeline │
│ make_recovery_pipeline(FccEnv) → InnerPipeline │
│ make_standby_pipeline(FccEnv) → InnerPipeline │
│ │
│ 工厂只知道:自己是哪套 GNC 算法、用什么速率 │
│ 工厂不知道:FccStage、Bus、simulation、runtime │
└───────────────────────────────────────────────────────────────────┘
▲
│ 调用工厂、组装 per-stage 表
│
┌───────────────────────────────────────────────────────────────────┐
│ ② fcc/firmware/ MissionProfile + FccCompiler │
│ MissionProfile { stage_to_factory[FccStage] = PipelineFactory }│
│ compile_fcc(profile, env) → CompiledFcc.per_stage[N] │
│ │
│ 这里知道:FccStage → 工厂的映射;这是 firmware mission 知识 │
│ 这里不知道:Bus、simulation、runtime、Assembler │
└───────────────────────────────────────────────────────────────────┘
▲
│ 启动期一次调用
│
┌───────────────────────────────────────────────────────────────────┐
│ ③ runtime::Assembler 通用装配触发 │
│ initialize(): │
│ compiled_fcc_ = fcc::firmware::compile_fcc(mission, env); │
│ fcc_tick_ = sim::pipeline::make_fcc_tick(compiled_fcc_); │
│ │
│ 这里知道:"启动期调一次 fcc::firmware::compile_fcc" │
│ 这里不知道:FCC 内部结构、FccStage 语义、Bus 协议、GNC 算法 │
└───────────────────────────────────────────────────────────────────┘
│
│ const CompiledFcc& 传递
▼
┌───────────────────────────────────────────────────────────────────┐
│ ④ simulation/pipeline/ FccTick(跨域 tick) │
│ run_one_tick(ctx, t): │
│ 1. Bus → FccInFrame │
│ 2. interpreter.run(flight_control_loop(compiled), ...) │
│ 3. FccOutFrame → Bus │
│ │
│ 这里知道:Bus 帧 ↔ FccFrame 转换;持有 const CompiledFcc& │
│ 这里不知道:FccStage→factory 映射、MissionProfile、GNC 算法 │
└───────────────────────────────────────────────────────────────────┘每段的"知道 / 不知道"是强约束,越界等于反模式。
3. 各段详解
3.1 段 ① fcc/pipelines/ —— 层级无知工厂
工厂的核心契约:签名里不能出现 FccStage。这与 plant/physics/Drag 不知道自己装在哪个 body 完全同构。
目录结构:
src/fcc/pipelines/
iter_guidance_pipeline.h
iter_guidance_pipeline.cpp
ballistic_pipeline.h
ballistic_pipeline.cpp
recovery_pipeline.h
recovery_pipeline.cpp
standby_pipeline.h
standby_pipeline.cpp
InnerPipeline.h ← using 别名 + SchedulePolicy 编译期常量Inner pipeline 别名(统一接口):
// fcc/pipelines/InnerPipeline.h
namespace fcc {
using InnerPipeline = std::function<
FccState(const FccState& /*prev*/,
const ImuMsg& /*imu*/,
Time /*now*/,
Time /*dt*/)
>;
// 编译期常量速率(替代 runtime schedule 查表)
namespace SchedulePolicy {
struct IterGuidance {
static constexpr bool nav_due(Time t) { return t.ticks() % 1 == 0; } // 100Hz
static constexpr bool guidance_due(Time t) { return t.ticks() % 10 == 0; } // 10Hz
static constexpr bool control_due(Time t) { return t.ticks() % 2 == 0; } // 50Hz
};
struct Ballistic {
static constexpr bool nav_due(Time t) { return true; }
static constexpr bool guidance_due(Time) { return false; } // 不制导
static constexpr bool control_due(Time t) { return t.ticks() % 2 == 0; } // passive hold
};
// ...
}
}工厂实现示例:
// fcc/pipelines/iter_guidance_pipeline.cpp
InnerPipeline fcc::pipelines::make_iter_guidance_pipeline(const FccEnv& env) {
// 烘焙:Env 切片 const& 捕获,零拷贝
const auto& nav_cfg = env.nav_config;
const auto& guidance_cfg = env.guidance_config_iter;
const auto& control_cfg = env.control_config_att_pid;
using Rates = SchedulePolicy::IterGuidance;
return [&nav_cfg, &guidance_cfg, &control_cfg]
(const FccState& prev, const ImuMsg& imu, Time t, Time dt) -> FccState {
FccState s = prev;
if (Rates::nav_due(t)) s.nav = step_nav(s.nav, imu, nav_cfg, dt);
if (Rates::guidance_due(t)) s.guidance = step_guidance(s.guidance, s.nav, guidance_cfg, dt);
if (Rates::control_due(t)) {
auto [next_c, out] = step_control(s.control, s.guidance, s.nav, control_cfg, dt);
s.control = next_c;
s.pending_output = out;
}
return s;
};
}契约:
- 工厂签名是
InnerPipeline (*)(const FccEnv&)——没有 FccStage 参数 - 工厂不引用
FccStage::*任何枚举值 - 工厂内的速率是编译期常量(
SchedulePolicy::*),不查 schedule map - 工厂内调用的
step_*函数也不知道 FccStage
复用收益:
- 同一个
make_iter_guidance_pipeline可被多个 stage 复用(如 Boost1 / Boost2 都用迭代制导) - HIL 测试中,可以直接用工厂构造单一 pipeline 跑闭环,不需要拉起整套 FccStage 机器
- 单元测试中,工厂构造的 InnerPipeline 是纯函数闭包,喂 (FccState, ImuMsg, Time, Time) 即可验证
3.2 段 ② fcc/firmware/ —— MissionProfile + FccCompiler
fcc/firmware/ 是 FCC 把"工厂集合"+"FccStage 状态机"装配成可运行 artifact 的层。它是 intra-FCC 的——不依赖任何 FCC 之外的代码。
目录结构:
src/fcc/firmware/
MissionProfile.h
MissionProfile.cpp
FccCompiler.h
FccCompiler.cpp
CompiledFcc.h
mission_loader.h ← YAML → MissionProfile 解析器(可选)
mission_loader.cppMissionProfile:
// fcc/firmware/MissionProfile.h
namespace fcc::firmware {
using PipelineFactory = InnerPipeline(*)(const FccEnv&);
constexpr size_t kFccStageCount = static_cast<size_t>(FccStage::_Count);
struct MissionProfile {
std::array<PipelineFactory, kFccStageCount> stage_to_factory;
// 可扩展:mission-level overrides (e.g., abort behavior, transition tables)
};
MissionProfile default_mission();
std::optional<MissionProfile> load_mission_from_yaml(std::string_view path);
}// fcc/firmware/MissionProfile.cpp
MissionProfile fcc::firmware::default_mission() {
using namespace fcc::pipelines;
MissionProfile p{};
p.stage_to_factory[(size_t)FccStage::PreLaunch] = make_standby_pipeline;
p.stage_to_factory[(size_t)FccStage::Boost1] = make_iter_guidance_pipeline;
p.stage_to_factory[(size_t)FccStage::Coast1] = make_ballistic_pipeline;
p.stage_to_factory[(size_t)FccStage::Boost2] = make_iter_guidance_pipeline;
p.stage_to_factory[(size_t)FccStage::Coast2] = make_ballistic_pipeline;
p.stage_to_factory[(size_t)FccStage::Reentry] = make_ballistic_pipeline;
p.stage_to_factory[(size_t)FccStage::TerminalDescent] = make_recovery_pipeline;
p.stage_to_factory[(size_t)FccStage::Landed] = make_standby_pipeline;
p.stage_to_factory[(size_t)FccStage::Aborted] = make_standby_pipeline;
return p;
}CompiledFcc:
// fcc/firmware/CompiledFcc.h
namespace fcc::firmware {
struct CompiledFcc {
std::array<InnerPipeline, kFccStageCount> per_stage;
};
}FccCompiler:
// fcc/firmware/FccCompiler.cpp
CompiledFcc fcc::firmware::compile_fcc(const MissionProfile& profile, const FccEnv& env) {
CompiledFcc out;
for (size_t i = 0; i < kFccStageCount; ++i) {
PipelineFactory f = profile.stage_to_factory[i];
// 缺失工厂回退到 standby,避免 nullptr
out.per_stage[i] = f ? f(env) : fcc::pipelines::make_standby_pipeline(env);
}
return out;
}契约:
- 依赖:
fcc/pipelines/+fcc/state/(FccStage、FccEnv、InnerPipeline);不依赖simulation/、runtime/、bus/ - MissionProfile 是数据;可硬编码、可 YAML 加载、可外部脚本生成
- FccCompiler 是无业务的单一职责:调表 + 调工厂
HIL / Mission 互换收益:
- 换 mission:换
MissionProfile即可;不动工厂、不动外壳、不动 Assembler - 切实飞 FCC:
MissionProfile嵌入芯片 firmware;simulation 完全不知道映射细节
3.3 段 ③ runtime/Assembler —— 通用装配触发
runtime::Assembler 是 Blueprint §3 已经规定的薄装配层。它只负责"启动期调一次 compile",不持有业务定义。
// runtime/Assembler.cpp(概念片段)
void Assembler::initialize() {
// ... 加载 environment / plant assets / 构造 FccEnv / 构造 RocketBody ...
// 拿 mission profile(来源不重要:硬编码、YAML、CLI 参数)
auto mission = fcc::firmware::default_mission();
// 或者 auto mission = fcc::firmware::load_mission_from_yaml(yaml_path).value();
// 启动期调一次——Assembler 不知道编译内部干啥
compiled_fcc_ = fcc::firmware::compile_fcc(mission, fcc_env_);
// 装入 simulation 层的 tick 结构
fcc_tick_ = sim::pipeline::make_fcc_tick(compiled_fcc_,
fcc_interpreter_,
bus_,
fcc_state_,
fcc_env_);
}契约:
- Assembler 不知道
FccStage/FccOp/InnerPipeline的内部结构 - Assembler 不知道
MissionProfile的字段——它只是把对象往下传 - Assembler 是唯一调用
fcc::firmware::compile_fcc的地方(除测试外)
层级薄性收益:runtime/ 永远只做"启动 / 装配 / runtime loop / 退出"四件事;新增 FCC 功能不会污染 runtime/。
3.4 段 ④ simulation/pipeline/FccTick —— 每拍跨域 tick
这是唯一允许同时引用 bus/、fcc/、runtime/ 的代码位置(Blueprint §2.6.4 跨域判据)。
// simulation/pipeline/FccTick.h
namespace sim::pipeline {
struct FccTick {
const fcc::firmware::CompiledFcc& compiled;
fcc::FccInterpreter& interpreter;
bus::IBus& bus;
fcc::FccState& state_ref;
fcc::FccEnv& env_ref;
};
FccTick make_fcc_tick(const fcc::firmware::CompiledFcc& c,
fcc::FccInterpreter& i,
bus::IBus& b,
fcc::FccState& s,
fcc::FccEnv& e);
void run_one_tick(FccTick& ctx, Time t);
}// simulation/pipeline/FccTick.cpp
void sim::pipeline::run_one_tick(FccTick& ctx, Time t) {
// 1. 跨域:Bus → FccInFrame
auto in_frame = bus::decode_to_fcc_in(ctx.bus, t);
// 2. 顶层策略:跨所有 stage 共用同一段 monadic 代码
auto strategy = fcc::flight_control_loop(ctx.compiled);
// 3. 解释执行
ctx.interpreter.run(strategy, ctx.env_ref, ctx.state_ref, in_frame, t);
// 4. 跨域:FccOutFrame → Bus
bus::encode_from_fcc_out(ctx.state_ref.pending_output, ctx.bus, t);
}契约:
- FccTick 持有
const CompiledFcc&——不构造、不修改 - FccTick 内部禁止
switch (state.current_stage)——派发完全由compiled.per_stage[]自动完成 - FccTick 不知道 mission profile、不知道 FccStage→factory 映射
跨域职责:
- Bus 上的字节流 ↔
FccInFrame结构化字段 FccOutFrame.events↔ Bus 上的 DiscreteEvent
4. 依赖图与禁忌
4.1 依赖方向(只允许的箭头)
fcc/algorithms/ ◄── fcc/pipelines/ ◄── fcc/firmware/ ◄── runtime/Assembler
│
▼
simulation/pipeline/FccTick
│
├──► bus/
├──► fcc/free_monad/ (解释器)
└──► fcc/firmware/ (只读 CompiledFcc)4.2 禁忌矩阵
| 越界类型 | 例子 | 违反 | 修正 |
|---|---|---|---|
| 工厂引用 FccStage | make_iter_guidance_pipeline(env, FccStage::Boost1) | 段 ① 层级无知 | 移除 stage 参数;让 fcc/firmware/MissionProfile 决定 |
| firmware 引用 Bus | #include "bus/IBus.h" in fcc/firmware/ | 段 ② intra-FCC | Bus 在段 ④ |
| Assembler 写映射 | compiled.per_stage[Boost1] = ... in runtime/ | 段 ③ 薄性 | 把映射移到 fcc/firmware/MissionProfile |
| simulation 决策 stage | if (stage == Boost1) call X in sim/ | 段 ④ 跨域判据 | 派发用 compiled.per_stage[] 自动选择 |
| GNC 算法读 FccStage | if (stage == Boost) gain *= 2 in step_control | 算法 ↔ 相位解耦 | 把 stage 相关增益写进不同的 control_cfg 切片,由不同工厂选用 |
| 外壳每拍读 Env | strategy 内部 get_env() >>= ... 做查表 | 段 ④ 性能契约 | Env 切片已烘焙进闭包;GetEnv 算子在预编译模式下退化 |
5. 与 dynamics 侧的对比
dynamics 侧采用同构但不完全对称的四段式(合理的非对称,源于 Blueprint §2.6.4 跨域判据):
| 段 | FCC | Dynamics |
|---|---|---|
| ① 工厂 | fcc/pipelines/make_*_pipeline(intra-FCC,层级无知) | simulation/pipeline/factories/make_*_body_pipeline(跨域工厂,已在 simulation/) |
| ② 编译 | fcc/firmware/compile_fcc | simulation/pipeline/compile_dynamics |
| ③ 触发 | runtime::Assembler 启动期 | runtime::Assembler 启动期 |
| ④ tick | simulation::pipeline::FccTick | simulation::pipeline::BodyTick + WorldTick |
| 编译产物 | CompiledFcc.per_stage[FccStage] | CompiledDynamics.per_stage[WorldStage] |
| 派发 | compiled.per_stage[s.current_stage] | compiled.per_stage[body.world_stage] |
> 注 1:为什么 dynamics 的"段 ①"已经在 simulation/,而 FCC 的段 ① 在 fcc/pipelines/? > > FCC 的 inner pipeline 是intra-FCC(只调 fcc/algorithms/ 的 step_nav / step_guidance / step_control),因此整个段①可以保持层级无知地放在 fcc/pipelines/。 > > dynamics 的 body pipeline 工厂内部要调用: > - plant::physics::compute_thrust_contribution (plant/) > - plant::physics::compute_aero_contribution (plant/) > - plant::physics::compute_gravity_contribution(plant/ + environment/) > - dynamics_core::ode::integrate_rk4 (dynamics_core/) > > 这是跨多个子域的组合。Blueprint §2.6.4 判据:跨域 → simulation/。因此 dynamics 没有"intra-domain 工厂"层——它的整个段①从一开始就在 simulation/pipeline/factories/。 > > 这个不对称是正确应用判据的结果,不是架构裂痕。两侧的派发结构(compiled.per_stage[stage])保持对称,仅工厂归属不同。
详见 05_Dynamics_Core/Topology_Algebra.md §6。
6. 测试策略
| 测试目标 | 级别 | 描述 |
|---|---|---|
| 工厂层级无知 | unit | grep fcc/pipelines/ 不得包含 FccStage:: |
| 工厂闭包纯净 | unit | 喂同样 (prev, imu, t, dt),多次调用结果完全一致 |
| MissionProfile 完整性 | unit | 每个 FccStage 必须有非空 factory(或 make_standby_pipeline 兜底) |
| 编译产物可重入 | unit | compile_fcc 输入相同 (profile, env) 输出等价闭包行为 |
| Assembler 薄性 | structural | grep runtime/Assembler.cpp 不得包含 FccStage:: / step_nav |
| FccTick 跨域纯净 | structural | grep simulation/pipeline/FccTick.cpp 不得包含 FccStage::Boost1 字样 |
| Per-stage 派发正确 | integration | 注入 FccState.current_stage = Coast1,验证调用的是 make_ballistic_pipeline 构造的闭包 |
| Mission 互换 | integration | 替换 MissionProfile,相同 FccState 跑出不同 controls |
7. C-Distillation 视角
C++ 阶段:
- 工厂返回
std::function<FccState(...)> InnerPipeline是std::functionCompiledFcc.per_stage是std::array<std::function<...>, N>
C 蒸馏阶段:
- 工厂返回
struct InnerPipeline { fp; bound_env_ptrs[]; }——函数指针 + 静态绑定参数 InnerPipeline是 PoDCompiledFcc.per_stage是static const InnerPipeline per_stage[N],整张表在.rodata- 派发是
per_stage[stage].fp(per_stage[stage].bound, prev, imu, t, dt),零分配
详见 Static_Compilation_FSM.md 与 08_Cross_Cutting/Hardware_Decoupling.md。
8. Wave 落地计划
当前代码状态:
src/fcc/FccCore.h是 7 行 stubsrc/fcc/free_monad/实现了 8 个 op + 解释器 + std::any visitorsrc/fcc/pipelines/、src/fcc/firmware/、src/simulation/pipeline/FccTick.*尚不存在
Wave 1 落地顺序:
- 创建
src/fcc/pipelines/InnerPipeline.h(别名 + SchedulePolicy) - 创建
src/fcc/pipelines/standby_pipeline.{h,cpp}(最小可工作工厂) - 创建
src/fcc/firmware/{MissionProfile,FccCompiler,CompiledFcc}.{h,cpp} - 修改
src/runtime/Assembler.cpp调用fcc::firmware::compile_fcc - 创建
src/simulation/pipeline/FccTick.{h,cpp} - 逐步把
iter_guidance/ballistic/recoverypipeline 落地
每步可独立验证:上一步落地后,可用 make_standby_pipeline 跑通整条数据流,再逐步替换。
9. Cross References
- 跨域判据 → Blueprint §2.6.4
- Strategy 写法 + O(1) 派发 →
Free_Monad_DSL.md§5 / §7 - GNC 三模块 Mealy →
FCC_State_Machine.md§2 - FccStage 代数 →
FCC_State_Machine.md§3 - 静态编译 FSM 总览 →
Static_Compilation_FSM.md - WorldStage 双代数化 + per-WorldStage 预编译 →
05_Dynamics_Core/Topology_Algebra.md§5 / §6 - BodyRWS pipeline 跨域实例化 →
06_Simulation/Body_Tick.md(待写)/ Blueprint §2.6.4 - 解释器 →
Interpreter_and_RWS.md - Bus 装包 →
03_Avionics_and_Bus/Semantic_Bus_Pattern.md§9