F´ 框架 Fw::Port 端口体系深度解析:PortBase 基类、端口连接与对象注册机制
【免费下载链接】fprimeF´ - A flight software and embedded systems framework项目地址: https://gitcode.com/GitHub_Trending/fpr/fprime
F´(F Prime)是一个面向飞行软件与嵌入式系统的组件化框架,而端口(Port)是连接各组件、传递调用与数据的关键设施。本文以Fw::Port模块的软件设计文档(Fw/Port/docs/sdd.md)为核心,结合仓库源码深入剖析端口基类Fw::PortBase、输入/输出端口的继承体系、端口连接与调用链路,以及由FW_OBJECT_REGISTRATION宏控制的Fw::ObjRegistry对象注册机制。读完本文,你将掌握 F´ 端口体系的分层结构、每个类的实际职责与源码实现、端口追踪与序列化调用等可选特性,以及如何通过配置文件开关裁剪这些能力。
1. Fw::Port 模块概览:端口在组件架构中的位置
按照官方设计文档的定义:Fw::Port模块包含架构中输入端口(Input Port)与输出端口(Output Port)的基类。
在 F´ 的组件化模型中,组件(Component)之间不直接互相调用,而是通过"输出端口 → 输入端口"的连接关系进行通信:
- 输出端口(Output Port)挂在调用方组件上,相当于"客户端",由调用方主动发起调用;
- 输入端口(Input Port)挂在被调用方组件上,相当于"服务端",负责接收并处理调用。
输出端口与输入端口之间的连接在拓扑(Topology)中建立。框架本身并不关心具体的业务逻辑,Fw::Port只提供支撑这一切的抽象基类——它们是 F´ 组件间通信机制的底层地基。
从源码目录结构看,该模块包含以下文件(见 Fw/Port/CMakeLists.txt):
PortBase.{hpp,cpp}:所有端口的公共基类;InputPortBase.{hpp,cpp}:输入端口基类;OutputPortBase.{hpp,cpp}:输出端口基类;InputSerializePort.{hpp,cpp}/OutputSerializePort.{hpp,cpp}:支持序列化调用的端口实现;- 以及
PortBase、InputSerializePort、OutputSerializePort等公共端口基础设施。
此外,模块文档还一并描述了Fw::ObjRegistry(对象注册表)这一可选基础设施,它虽然定义在 Fw/Obj/ObjBase.hpp 中,但与端口/组件对象的初始化流程紧密相关,下文第 6 节会详细展开。
2. Fw::PortBase:端口体系的总基类
2.1 类层次与设计定位
官方文档明确指出:Fw::PortBase是 ISF(Integrated Software Framework)类层次结构中的端口基类,是所有端口的基类。
在源码中(Fw/Port/PortBase.hpp),它继承自Fw::ObjBase:
class PortBase : public Fw::ObjBase { public: bool isConnected() const; // ... };Fw::ObjBase是框架对象体系的根类(Fw/Obj/ObjBase.hpp),提供对象命名(FW_OBJECT_NAMES)、对象描述(toString,受FW_OBJECT_TO_STRING控制)以及对象注册(FW_OBJECT_REGISTRATION)等基础设施。因此端口对象天然具备名称、可注册、可被追踪的能力。
从类层次结构看,PortBase位于最顶层,其下派生:
InputPortBase(输入端口基类)→ 具体的输入端口实例;OutputPortBase(输出端口基类)→ 具体的输出端口实例;InputSerializePort/OutputSerializePort(序列化端口)。
值得注意的设计约束:PortBase的构造函数与析构函数是protected的,并且拷贝构造、拷贝赋值均被显式禁用(Fw/Port/PortBase.hpp)。这意味着端口对象不允许被复制,只能通过派生类实例化——这保证了每个端口在连接图中的身份唯一性。
2.2 连接状态与连接对象
PortBase的核心成员是连接对象指针m_connObj(Fw/Port/PortBase.hpp):
Fw::ObjBase* m_connObj; // !< object port is connected to该指针指向端口连接的另一端对象。isConnected()是端口最常用的状态查询接口,其实现极为直接(Fw/Port/PortBase.cpp):
bool PortBase::isConnected() const { return m_connObj == nullptr ? false : true; }连接建立与断开的具体逻辑不在PortBase中,而由派生类与自动生成的连接代码(通常在拓扑的connect调用中完成)负责填充m_connObj。例如输出端口的registerSerialPort()就会同时设置m_connObj与m_serPort(见第 4 节)。这种"基类持有连接指针、派生类维护具体连接语义"的分工,是端口体系保持灵活的关键。
2.3 端口调用追踪:FW_PORT_TRACING
PortBase提供可选的**端口追踪(tracing)**能力,用于在调试时打印端口调用轨迹。该特性由宏FW_PORT_TRACING控制,默认开启(见 default/config/FpConfig.h):
#ifndef FW_PORT_TRACING #define FW_PORT_TRACING (1) //!< Indicates whether port calls are traced (more code, more visibility into execution) #endif当FW_PORT_TRACING == 1时,PortBase提供以下追踪接口(Fw/Port/PortBase.hpp):
static void setTrace(bool trace); // !< turn tracing on or off void ovrTrace(bool ovr, bool trace); // !< override tracing for a particular portsetTrace(bool):全局开关,设置静态成员s_trace;ovrTrace(ovr, trace):对单个端口覆盖全局设置,分别设置局部标志m_ovr_trace与m_trace。
trace()方法决定是否打印(Fw/Port/PortBase.cpp):
void PortBase::trace() const { FW_ASSERT(this->isConnected()); // Ports are only traced as part of invocation, which requires a connection bool do_trace = false; if (this->m_ovr_trace) { if (this->m_trace) { do_trace = true; } } else if (PortBase::s_trace) { do_trace = true; } if (do_trace) { #if FW_OBJECT_NAMES == 1 Fw::Logger::log("Trace: %s\n", this->m_objName.toChar()); #else Fw::Logger::log("Trace: %p\n", this); #endif } }从实现可推断出三点细节:
trace()断言端口必须已连接——追踪只发生在端口调用过程中;- 追踪优先级是"单端口覆盖(
m_ovr_trace)优先于全局开关(s_trace)"; - 输出内容优先打印对象名(
FW_OBJECT_NAMES == 1时),否则退化为打印对象地址。
此外,PortBase.cpp还导出了一个 C 语言接口setConnTrace(bool trace)(Fw/Port/PortBase.cpp),方便非 C++ 调用路径(如 C 接口适配层)打开/关闭连接追踪。
2.4 端口描述:toString 与 FW_OBJECT_TO_STRING
当FW_OBJECT_TO_STRING == 1时(该宏默认由 default/config/FpConfig.h 根据FW_OBJECT_NAMES自动开启),PortBase重写了ObjBase的toString()方法,用于生成端口的可读描述字符串(Fw/Port/PortBase.cpp):
void PortBase::toString(char* buffer, FwSizeType size) { const char* formatString = this->getToStringFormatString(); // 端口对象名(无命名时输出 "UNKNOWN") // 连接状态:"C"(connected)或 "NC"(not connected) // 连接目标:已连接时输出对方对象名,否则输出 "None" const Fw::FormatStatus formatStatus = Fw::ExternalString(buffer, ...).format(formatString, object_name, this_is_connected, connected_to); // ... }输出格式由虚函数getToStringFormatString()提供,各派生类可覆盖:
| 类 | 格式串 | 来源 |
|---|---|---|
PortBase | "Port: %s %s->(%s)" | Fw/Port/PortBase.cpp |
InputPortBase | "Input Port: %s %s->(%s)" | Fw/Port/InputPortBase.cpp |
OutputPortBase | "Output Port: %s %s->(%s)" | Fw/Port/OutputPortBase.cpp |
InputSerializePort | "Input Serial Port: %s %s->(%s)" | Fw/Port/InputSerializePort.cpp |
OutputSerializePort | "Output Serial Port: %s %s->(%s)" | Fw/Port/OutputSerializePort.cpp |
这样的 toString 输出(例如Output Port: rateGroup.RateGroupOut[0] C->(cmdDisp.CmdIn))在调试拓扑连接关系时非常实用,可以直接判断某个端口是否连接、连接到哪个对象。
3. InputPortBase:输入端口基类
InputPortBase是被调用侧端口的基类,继承自PortBase(Fw/Port/InputPortBase.hpp)。它补全了输入端口的两项核心信息:
PassiveComponentBase* m_comp; // !< pointer to containing component FwIndexType m_portNum; // !< port number in containing objectm_comp:指向包含该端口的组件(PassiveComponentBase),端口回调由此找到正确的组件实例;m_portNum:端口在组件内的序号,默认初始化为-1(Fw/Port/InputPortBase.cpp),由setPortNum()在初始化阶段设置:
void InputPortBase::setPortNum(FwIndexType portNum) { FW_ASSERT(portNum >= 0, static_cast<FwAssertArgType>(portNum)); this->m_portNum = portNum; }setPortNum通过FW_ASSERT拒绝负数端口号,即端口序号必须从 0 开始。
当FW_PORT_SERIALIZATION == 1时,InputPortBase还声明了纯虚函数invokeSerial()(Fw/Port/InputPortBase.hpp),为输入端口定义"以序列化形式被调用"的统一入口,具体实现由派生类提供(见第 5 节)。
与PortBase一样,InputPortBase的构造函数、析构函数是protected,拷贝构造与拷贝赋值被禁用(Fw/Port/InputPortBase.hpp)。
4. OutputPortBase:输出端口基类
OutputPortBase是发起调用侧端口的基类,继承自PortBase(Fw/Port/OutputPortBase.hpp)。它最关键的职责是管理序列化调用的转发目标:
#if FW_PORT_SERIALIZATION == 1 void registerSerialPort(InputPortBase* port); // !< register a port for serialized calls SerializeStatus invokeSerial(LinearBufferBase& buffer); // !< invoke the port with a serialized version of the call #endifregisterSerialPort()的实现(Fw/Port/OutputPortBase.cpp)同时完成两件事:
void OutputPortBase::registerSerialPort(InputPortBase* port) { FW_ASSERT(port != nullptr); this->m_connObj = port; // 记录连接对象(PortBase 的连接状态随之变为已连接) this->m_serPort = port; // 记录序列化调用目标 }也就是说,注册序列化端口的同时也就建立了连接——m_connObj被置为该输入端口,isConnected()随即返回true。
而invokeSerial()则把序列化缓冲直接转发给对端输入端口(Fw/Port/OutputPortBase.cpp):
SerializeStatus OutputPortBase::invokeSerial(LinearBufferBase& buffer) { FW_ASSERT(this->m_serPort != nullptr); return this->m_serPort->invokeSerial(buffer); }这里形成了一条完整的调用链:
输出端口 OutputPortBase::invokeSerial(buffer) → 已注册的输入端口 InputPortBase::invokeSerial(buffer) → 输入端口内部回调组件(m_comp + m_portNum)普通的强类型端口(由 FPP 模型自动生成的具体端口类)在编译期就确定了调用函数签名,直接通过函数指针完成调用;而序列化端口则把参数打包进LinearBufferBase缓冲,走运行时序列化路径——这正是 F´ 支持跨节点(多处理器)通信的基础设施之一。
5. 序列化端口:InputSerializePort 与 OutputSerializePort
在FW_PORT_SERIALIZATION == 1时,模块提供一对可直接使用的序列化端口实现,它们属于"透传序列化数据"的端口(源码注释:"SerializePort has no call interface. It is to pass through serialized data",见 Fw/Port/InputSerializePort.cpp)。
InputSerializePort(Fw/Port/InputSerializePort.hpp):
- 继承
InputPortBase,实现invokeSerial(); - 通过
addCallComp(callComp, funcPtr)注册"组件 + 回调函数指针":
typedef void (*CompFuncPtr)(Fw::PassiveComponentBase* callComp, FwIndexType portNum, LinearBufferBase& arg); // !< port callback definition void addCallComp(Fw::PassiveComponentBase* callComp, CompFuncPtr funcPtr);invokeSerial()断言组件与回调已注册后,直接调用回调函数(Fw/Port/InputSerializePort.cpp),并恒返回FW_SERIALIZE_OK——注释解释:普通输入端口会对缓冲执行deserialize()并以其结果为状态,而InputSerializePort不反序列化,只是透传,因此直接返回 OK。
OutputSerializePort(Fw/Port/OutputSerializePort.hpp):
- 继承
OutputPortBase,是一个"空实现"的输出端口,本身不保存额外的调用目标,序列化调用能力全部来自基类OutputPortBase::invokeSerial()(它会把缓冲转发给m_serPort)。
从源码结构可以推断:这一对序列化端口通常配合使用,用于构建"数据只透传、不做类型化拆包"的调用通道,例如 F´ 中负责在帧(frame)与上层协议之间搬运原始字节流的场景,需要把任意端口的调用以序列化形式桥接到另一侧。
6. Fw::ObjRegistry:对象注册机制
6.1 设计文档中的定位
官方设计文档(Fw/Port/docs/sdd.md)将Fw::ObjRegistry列为该模块的重要组成部分,原文要点如下:
Fw::ObjRegistry是对象注册表的虚基类(virtual base class)。这是一个可选特性,通过FW_OBJECT_REGISTRATION宏开启或关闭,该宏位于FpConfig.hpp中。其思想是:对象注册表提供一种方式,用于追踪系统中已实例化的所有对象。注册表可以查询Fw::Object中的任意公开函数,以获取实例信息。而"如何存储Fw::Object指针、如何产生实例数据"的具体方法,则留给派生类实现。
需要说明的是:从当前仓库源码看,该注册表相关实现实际位于 Fw/Obj/ObjBase.hpp 与 Fw/Obj/ObjBase.cpp 中,Fw::Port的文档将其一并纳入介绍,是因为端口与组件对象都继承自Fw::ObjBase,天然参与对象注册流程——这正是注册表能"追踪系统中所有对象"的原因。
6.2 宏开关:FW_OBJECT_REGISTRATION
FW_OBJECT_REGISTRATION在 default/config/FpConfig.h 中定义,默认开启:
// Adds the ability for all component related objects to register centrally. #ifndef FW_OBJECT_REGISTRATION #define FW_OBJECT_REGISTRATION \ (1) //!< Indicates whether or not objects can register themselves (more code, more object tracking) #endif注释清晰地说明了取舍:开启会增加代码量,但获得集中的对象追踪能力;对内存与二进制体积敏感的场景(如小容量嵌入式平台)可以将其置 0 裁剪掉。
6.3 实现机制:从 setObjRegistry 到 regObject
对象注册的完整流程由三部分协作完成:
(1)注册表基类ObjRegistry(Fw/Obj/ObjBase.hpp)定义唯一纯虚接口:
class ObjRegistry { public: virtual void regObject(ObjBase* obj) = 0; // !< 注册一个对象实例 virtual ~ObjRegistry(); };(2)全局注册表指针与设置接口。ObjBase内部持有一个静态指针s_objRegistry(Fw/Obj/ObjBase.hpp),通过静态方法setObjRegistry()安装:
static void setObjRegistry(ObjRegistry* reg); // 调用后,后续所有 ObjBase::init() 都会调用注册表的 regObject()(3)对象初始化时自动注册。ObjBase::init()是触发点(Fw/Obj/ObjBase.cpp):
void ObjBase::init() { #if FW_OBJECT_REGISTRATION if (ObjBase::s_objRegistry) { ObjBase::s_objRegistry->regObject(this); } #endif }由于PortBase::init()、InputPortBase::init()、OutputPortBase::init()都会逐级调用ObjBase::init()(见 Fw/Port/PortBase.cpp、Fw/Port/InputPortBase.cpp、Fw/Port/OutputPortBase.cpp),因此只要系统安装了注册表,所有组件与端口在 init 阶段就会被自动登记。
头文件注释中还有一个重要提醒:setObjRegistry调用可能不可重入、非线程安全(Fw/Obj/ObjBase.hpp),仓库自带的SimpleObjRegistry即非可重入实现——这意味着注册表安装应在单线程的启动初始化阶段完成。
6.4 派生实现示例:SimpleObjRegistry
仓库提供了一个最小的派生实现SimpleObjRegistry(Fw/Obj/SimpleObjRegistry.hpp、Fw/Obj/SimpleObjRegistry.cpp),演示了"如何存储对象指针、如何产生实例数据"由派生类自行决定的设计思想。它内部维护一个对象指针列表,在regObject()中登记对象,并提供查询接口遍历已注册对象、读取每个对象的公开信息(如对象名、toString描述等)。
使用模式可以概括为:
// 1. 实例化一个注册表(如 SimpleObjRegistry) SimpleObjRegistry registry; // 2. 安装为全局注册表(启动阶段,单线程) Fw::ObjBase::setObjRegistry(®istry); // 3. 之后所有组件/端口的 init() 都会自动调用 registry.regObject(this) // 4. 随时查询系统内已实例化的对象列表这一机制对于系统自检、运行时对象清单导出、调试诊断等场景非常实用。
7. 相关配置开关速查
端口体系相关的所有可裁剪特性均集中在 default/config/FpConfig.h 中,汇总如下(均为当前仓库的默认值):
| 宏 | 默认值 | 作用 | 影响 |
|---|---|---|---|
FW_OBJECT_REGISTRATION | 1 | 对象注册表开关 | 开启增加代码量,获得集中对象追踪(FpConfig.h) |
FW_PORT_TRACING | 1 | 端口调用追踪开关 | 开启增加代码量,获得执行可见性(FpConfig.h) |
FW_PORT_SERIALIZATION | 1 | 序列化端口连接代码 | 开启增加代码量,获得调用序列化能力(跨节点系统需要)(FpConfig.h) |
FW_OBJECT_NAMES | 1 | 是否存储对象名 | 开启消耗更多内存,便于追踪对象(FpConfig.h) |
FW_OBJECT_TO_STRING | 1 | 生成对象的 toString() 方法 | 开启增加代码量,可导出对象内部状态(FpConfig.h) |
注意FW_OBJECT_TO_STRING依赖FW_OBJECT_NAMES:当对象名被关闭时,它会被强制置 0(FpConfig.h)。这些开关共同决定了端口体系是"完整调试能力"还是"极致精简"形态,开发者可按平台资源在工程配置中覆盖这些默认值。
8. 端口体系与周边模块的协作
为了更完整地理解Fw::Port的位置,可以结合仓库中的周边设施:
- 组件基类:输入端口通过
m_comp指向PassiveComponentBase(Fw/Port/InputPortBase.hpp),组件侧还有ActiveComponentBase、QueuedComponentBase等派生形态(见 Fw/Comp/),端口回调据此分发到正确的组件实例; - 端口接口定义(FPP):具体端口的签名、方向与类型由 FPP 模型文件描述,例如 Fw/Interfaces/Command.fpp、Fw/Interfaces/Event.fpp、Fw/Interfaces/Channel.fpp 定义了命令、事件、遥测等系统级端口,由代码生成器产出继承
InputPortBase/OutputPortBase的具体端口类; - 公共端口类型:Fw/Ports/ 目录还提供
Signal、Ready、CompletionStatus、SuccessCondition等基础端口定义,供各模块复用; - 序列化缓冲:序列化调用链路依赖
LinearBufferBase承载参数数据(见 Fw/Types/SerialBuffer.hpp 所在类型目录的线性缓冲实现)。
这一协作关系说明:Fw::Port提供的基类是一个"骨架",具体端口的类型化调用由代码生成器在编译期填充,而序列化端口则为运行时动态调用保留了通用通道。
9. 总结
Fw::Port模块是 F´ 组件间通信机制的基石,其设计呈现清晰的层次:
Fw::PortBase(Fw/Port/PortBase.hpp)提供所有端口的公共能力:连接状态查询(isConnected)、调用追踪(FW_PORT_TRACING)、对象描述(FW_OBJECT_TO_STRING);InputPortBase/OutputPortBase分别承载被调用侧与发起调用侧的核心状态(所属组件m_comp、端口序号m_portNum、序列化目标m_serPort),并共同构成invokeSerial()的转发调用链;InputSerializePort/OutputSerializePort提供"数据透传"式的序列化端口实现;Fw::ObjRegistry通过FW_OBJECT_REGISTRATION宏与ObjBase::init()挂钩,在系统启动时自动登记所有组件与端口对象,注册表的具体存储与查询策略由派生类(如SimpleObjRegistry)自行决定。
所有这些能力都可以通过 default/config/FpConfig.h 中的配置宏按需裁剪,在"调试可见性"与"资源占用"之间灵活取舍。理解这一体系,是深入阅读 F´ 组件实现、理解 FPP 生成代码以及排查组件连接问题的前提。
【免费下载链接】fprimeF´ - A flight software and embedded systems framework项目地址: https://gitcode.com/GitHub_Trending/fpr/fprime
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考