IDynChannel — SIL / HIL 信道抽象
> Status: NEW · 已对齐 PCR Master Blueprint v1.0 §1.3 / §2.10 > 范畴: runtime/{IDynChannel, InMemoryDynChannel, UdpDynChannel}.{h,cpp} > 依赖: contracts(DynInFrame/DynOutFrame) > 被依赖: runtime/Runner
1. 问题陈述
仿真有四种部署模式(Blueprint §1.3):
| 模式 | AvionicsSystem 位置 | PlantPhysics 位置 | DynChannel 类型 |
|---|---|---|---|
avionics_dry | 本进程 | 冻结 | InMemory(plant 不演化,FCC 走时序) |
sil_monolithic | 本进程 | 本进程 | InMemory |
hil_dyn | 本进程 | 远端(UDP server) | Udp Client |
hil_fcc | 远端 RTOS | 本进程(UDP server) | Udp Server |
关键洞察(Blueprint §1.3):HIL 的本质不是重新设计架构,而是在两个数据包边界上换信道:
DynChannel跨 AvionicsSystem ↔ PlantPhysics(payload =DynInFrame/DynOutFrame)Bus跨 FCC ↔ Devices(payload =BusMessage)
反例:HIL 需要重写 world_tick、新写一套通信状态机、双份控制律代码。 正解:BodyTick / WorldTick 骨架不变,仅替换 IDynChannel::send/recv 与 IBus 的具体实现。
2. 接口
// runtime/IDynChannel.h
namespace runtime {
class IDynChannel {
public:
virtual ~IDynChannel() = default;
// Plant → Avionics(plant 把 DynOut 发出)
virtual void send(const std::vector<contracts::DynOutFrame>& frames) = 0;
// Avionics → Plant(plant 收回 DynIn / 命令)
virtual std::vector<contracts::DynInFrame> recv() = 0;
// 反方向(对称:FCC 节点视角)
virtual void send(const std::vector<contracts::DynInFrame>& frames) = 0;
virtual std::vector<contracts::DynOutFrame> recv_out() = 0;
// 状态
virtual bool is_connected() const = 0;
virtual void close() = 0;
};
} // namespace runtime> 签名设计:单一类同时支持两种方向;具体实现根据 PlantScope 选择性 use 一半。 > 例如 SIL 同进程时,InMemoryDynChannel 维护两个队列(DynIn / DynOut),plant 调用 send(DynOut),FCC 调用 recv() → DynOut。 > HIL UDP 时只用各自方向的 UDP socket。
3. InMemoryDynChannel(SIL / avionics_dry)
// runtime/InMemoryDynChannel.h
namespace runtime {
class InMemoryDynChannel : public IDynChannel {
public:
void send(const std::vector<contracts::DynOutFrame>& fs) override {
std::scoped_lock g(mu_);
for (auto& f : fs) out_queue_.push_back(f);
}
std::vector<contracts::DynInFrame> recv() override {
std::scoped_lock g(mu_);
auto v = std::move(in_queue_);
in_queue_.clear();
return v;
}
void send(const std::vector<contracts::DynInFrame>& fs) override {
std::scoped_lock g(mu_);
for (auto& f : fs) in_queue_.push_back(f);
}
std::vector<contracts::DynOutFrame> recv_out() override {
std::scoped_lock g(mu_);
auto v = std::move(out_queue_);
out_queue_.clear();
return v;
}
bool is_connected() const override { return true; }
void close() override {}
private:
mutable std::mutex mu_;
std::deque<contracts::DynOutFrame> out_queue_;
std::deque<contracts::DynInFrame> in_queue_;
};
} // namespace runtime性质:
- 零拷贝(move into queue),零序列化
- 与 SIL 部署等价于"FCC ↔ Plant 完全可见对方内存"
avionics_dry模式下 plant 不演化,recv_out总返回空 → FCC 看到全零 DynOutFrame(或保留上一帧)
4. UdpDynChannel(HIL)
// runtime/UdpDynChannel.h
namespace runtime {
struct UdpConfig {
std::string remote_host; // "192.168.1.100"
uint16_t local_port; // 31000
uint16_t remote_port; // 31001
Time timeout; // 接收超时
};
class UdpDynChannel : public IDynChannel {
public:
explicit UdpDynChannel(const UdpConfig& cfg);
~UdpDynChannel() override; // 关闭 socket
void send(const std::vector<contracts::DynOutFrame>& fs) override;
std::vector<contracts::DynInFrame> recv() override;
// ... 对称方向 ...
private:
int sock_fd_;
UdpConfig cfg_;
// 协议:[u32 magic][u32 frame_count][frames...]
std::vector<uint8_t> serialize_(const std::vector<contracts::DynOutFrame>&);
std::vector<contracts::DynInFrame> deserialize_(const std::vector<uint8_t>&);
};
} // namespace runtime性质:
- 单 UDP socket(可换成 TCP,签名不变)
- 序列化用紧凑二进制(禁止 JSON / Protobuf,HIL 节点要求低延迟)
recv()阻塞至超时;超时返回空 vector(Runner 视为"上一帧无更新")
> HIL 时序契约: > - phys 节点(plant)每 phys_dt(1ms)调用 dyn_channel_->send(out_frames) > - fcc 节点每 fcc_dt(20ms)调用 dyn_channel_->send(in_frames) > - 不同步包丢弃 vs 重传策略由 PlantScope 决定
5. Bus 信道对称
对称地,bus::IBus(详见 03_Avionics_and_Bus/Semantic_Bus_Pattern.md)也支持多种部署:
| 部署 | bus 实现 |
|---|---|
sil_monolithic | InMemoryBus(同进程,零拷贝 std::variant 投递) |
hil_fcc(FCC RTOS) | Bus1553B / BusTTE(真实总线驱动) |
hil_dyn(plant 本机,FCC 远端) | BusBridge(UDP 桥接到远端 InMemoryBus) |
bus::IBus 的接口与 IDynChannel 互不重叠:
- IDynChannel 传飞控仿真协议帧(DynIn/DynOut,物理真值+总输出指令)
- IBus 传设备级语义消息(BusMessage = ImuPayload | GpsPayload | ScuPayload | IcuPayload)
> 不要混用:DynIn ≠ Bus payload。前者是 FCC↔仿真器协议,后者是 FCC↔设备协议。
6. 工厂分发
// runtime/ChannelFactory.cpp
namespace runtime {
std::unique_ptr<IDynChannel> make_dyn_channel(const PlantScope& scope) {
using Kind = PlantScope::TransportKind;
switch (scope.dyn_transport) {
case Kind::InMemory:
return std::make_unique<InMemoryDynChannel>();
case Kind::UdpServer:
case Kind::UdpClient:
return std::make_unique<UdpDynChannel>(scope.udp_config);
}
throw std::runtime_error("unknown dyn_transport");
}
std::unique_ptr<bus::IBus> make_bus(const PlantScope& scope) {
using Kind = PlantScope::BusKind;
switch (scope.bus_kind) {
case Kind::InMemory: return std::make_unique<bus::InMemoryBus>();
case Kind::Bridge1553B: return std::make_unique<bus::Bus1553Bridge>(scope.bus_bridge_config);
case Kind::RealtimeBus: return std::make_unique<bus::BusRealtime>(scope.bus_rt_config);
}
throw std::runtime_error("unknown bus_kind");
}
} // namespace runtime关键:工厂函数只在 Runner 构造时调用一次。整个仿真生命周期内 dyn_channel / bus 对象稳定。
7. Runner 的部署适配
Runner 不需要为不同部署写多个 main loop;它只依赖 scope 决定信道:
// runtime/Runner.cpp 的简化版(部署无关)
Runner::Runner(SimulationInstance inst) : instance_(std::move(inst)) {
bus_ = make_bus(instance_.scope);
dyn_channel_ = make_dyn_channel(instance_.scope);
// ... rest is identical across all deployments
}
int Runner::run() {
while (state.current_time < end_time) {
auto [wout, new_state, wlog] = sim::world_tick(dt).run(env, std::move(state));
state = std::move(new_state);
log_writer_.write(wlog);
// 部署透明:dyn_channel 是 InMemory / Udp 都不影响
if (dyn_channel_) {
dyn_channel_->send(make_dyn_out_frames(wout));
// hil_fcc 模式:fcc 在远端,plant 把控制命令收回
// sil_monolithic 模式:DynInFrame 通过 InMemoryDynChannel 即时回环
auto cmds = dyn_channel_->recv();
apply_dyn_in_frames(state, cmds);
}
}
}胜利条件:换部署模式只需换 YAML,不改一行 main loop 代码。
8. 序列化契约
HIL 模式下 DynInFrame / DynOutFrame 必须可序列化:
// contracts/DynFrames.h
namespace contracts {
struct DynOutFrame {
BodyId body_id;
Time timestamp;
Vec3_T<frames::ECF> pos_ecf;
Vec3_T<frames::ECF> vel_ecf;
Quat att_q;
Vec3_T<frames::BODY> omega_b;
Vec3_T<frames::BODY> spec_force_b;
double total_mass;
Vec3 centroid_b;
};
struct DynInFrame {
BodyId body_id;
Time timestamp;
// 控制命令(从 FCC 来)
std::vector<EngineCommand> engine_commands;
std::vector<ServoCommand> servo_commands;
std::vector<FinCommand> fin_commands;
// 事件
std::vector<DiscreteEvent> events;
};
} // namespace contracts字段约束:
- 全部 POD-ish(无
std::function、无指针、无 std::any) - 浮点数布局:所有
double8 字节小端(HIL 节点统一约定) - 字符串(debug_id)禁止——用
enum classID 或固定长度 char 数组
> 理由:HIL 节点可能是 RTOS(无 std::string heap),必须 POD 友好。
9. 反模式
| 反模式 | 后果 | 正确做法 |
|---|---|---|
world_tick 内部直接判断 if (scope.dyn_transport == Udp) { ... } | 部署逻辑泄漏进 simulation/ | 部署是 runtime 的事;simulation 只看 IBus / IDynChannel 接口 |
DynInFrame 持有 std::function<void()> 回调 | HIL 不可序列化 | DynFrame 是纯数据;回调走 Bus / 信道 |
把 BusMessage 通过 IDynChannel 转发 | 信道职责混淆 | BusMessage 走 IBus;DynFrame 走 IDynChannel |
每个 tick 内 make_unique<UdpDynChannel>() | 端口反复绑定;崩溃 | 构造一次,整生命周期复用 |
| HIL 用 JSON / Protobuf 序列化 DynFrame | 延迟 + 内存开销不可控 | 紧凑二进制(手写或 flatbuffers) |
| UDP socket 阻塞主 loop | 整个仿真挂起 | 用 timeout + 非阻塞 recv |
把 LogConfig 通过 IDynChannel 发出 | 业务策略漂出仿真 | LogConfig 是本地的,每个节点自己持有 |
10. C-Distillation 路径
| C++ 抽象 | C 蜕化 |
|---|---|
IDynChannel 虚类 | 函数指针表:struct DynChannelOps { send_fn, recv_fn, ... } |
std::unique_ptr<IDynChannel> | DynChannelOps* 全局静态 |
InMemoryDynChannel | 静态环形缓冲区 + 内存屏障 |
UdpDynChannel | LwIP / RTOS socket API |
std::vector<DynOutFrame> 序列化 | 固定大小 struct 数组 + memcpy |
serialize_ / deserialize_ | 编译期 layout struct(不需要 codegen) |
make_dyn_channel(scope) | 编译期 #ifdef DEPLOYMENT_SIL_MONOLITHIC 选择 |
参见 Blueprint §7.12。
11. 测试策略
11.1 单元层
InMemoryDynChannel::send + recv回环:发 N 帧,收 N 帧,bit-identicalUdpDynChannel在 localhost 上回环:send/recv 数据完整性
11.2 组件层
ChannelFactory× 三种 scope:返回正确具体类型- 序列化往返:
serialize(frame) → deserialize → frame',所有字段 bit-equal
11.3 集成层
- 1s SIL 跑通(InMemoryDynChannel)
- 1s HIL 模拟(两个进程 UdpDynChannel 互连,验证 tick 数对齐)
12. 引用
- Blueprint §1.3(三种部署模式 + 信道本质)、§2.10(信道抽象与 Bus)
07_Runtime/PlantScope.md(scope 决定信道选择)07_Runtime/Assembler_and_Runner.md(Runner 构造时调 make_dyn_channel/make_bus)03_Avionics_and_Bus/Semantic_Bus_Pattern.md(bus::IBus 对称设计)01_Foundation/Contracts_PerBodyFrames.md(DynInFrame / DynOutFrame 类型)