fpp/docs/users-guide/Defining-Component-Instances.adoc
Rob Bocchino e4ae91672b Revise User's Guide
Scrub references to XML
2025-07-24 21:21:54 -07:00

802 lines
25 KiB
Plaintext

== Defining Component Instances
As discussed in the section on <<Defining-Components,defining-components>>,
in F Prime you define components and instantiate them.
Then you construct a *topology*, which is a graph
that specifies the connections between the components.
This section explains how to define component instances.
In the next section, we will explain how to
construct topologies.
=== Component Instance Definitions
To instantiate a component, you write a *component instance definition*.
The form of a component instance definition depends on the kind
of the component you are instantiating: passive, queued, or active.
==== Passive Components
To instantiate a passive component, you write the following:
* The keyword `instance`.
* The <<Defining-Constants_Names,name>> of the instance.
* A colon `:`.
* The name of a <<Defining-Components_Component-Definitions,component definition>>.
* The keywords `base` `id`.
* An <<Defining-Constants_Expressions,expression>> denoting
the *base identifier* associated with the component instance.
The base identifier must resolve to a number.
The FPP translator adds this number to each of the component-relative
<<Defining-Components_Commands_Opcodes,command opcodes>>,
<<Defining-Components_Events_Identifiers,event identifiers>>,
<<Defining-Components_Telemetry_Identifiers,telemetry channel identifiers>>,
and
<<Defining-Components_Parameters_Identifiers,parameter identifiers>>
specified in the component, as discussed in the previous section.
The base identifier for the instance plus the component-relative
opcode or identifier for the component gives the corresponding
opcode or identifier for the instance.
Here is an example:
[source,fpp]
----
module Sensors {
@ A component for sensing engine temperature
passive component EngineTemp {
@ Schedule input port
sync input port schedIn: Svc.Sched
@ Telemetry port
telemetry port tlmOut
@ Time get port
time get port timeGetOut
@ Impulse engine temperature
telemetry ImpulseTemp: F32
@ Warp core temperature
telemetry WarpTemp: F32
}
}
module FSW {
@ Engine temperature instance
instance engineTemp: Sensors.EngineTemp base id 0x100
}
----
We have defined a passive component `Sensors.EngineTemp` with three ports:
a schedule input port for driving the component periodically on a rate group,
a time get port for getting the time, and a telemetry port
for reporting telemetry.
(For more information on rate groups and the use of `Svc.Sched`
ports, see the
https://fprime.jpl.nasa.gov/devel/docs/user-manual/design-patterns/rate-group/[F
Prime documentation].)
We have given the component two telemetry channels:
`ImpulseTemp` for reporting the temperature of the impulse engine,
and `WarpTemp` for reporting the temperature of the warp core.
Next we have defined an instance `FSW.engineTemp` of component `Sensors.EngineTemp`.
Because the instance definition is in a different module from the
component definition, we must refer to the component by its
qualified name `Sensors.EngineTemp`.
If we wrote
[source,fpp]
--------
instance engineTemp: EngineTemp base id 0x100
--------
the FPP compiler would complain that the symbol `EngineTemp` is undefined
(try it and see).
We have specified that the base identifier of instance `FSW.engineTemp`
is the hexadecimal number 0x100 (256 decimal).
In the component definition, the telemetry channel `ImpulseTemp`
has relative identifier 0, and the telemetry channel `WarpTemp`
has relative identifier 1.
Therefore the corresponding telemetry channels for the instance
`FSW.engineTemp` have identifiers 0x100 and 0x101 (256 and 257)
respectively.
For consistency, the base identifier is required for all component instances,
even instances that define no dictionary elements (commands, events, telemetry,
or parameters).
For each component instance _I_, the range of numbers between the base
identifier and the base identifier plus the largest relative identifier
is called the *identifier range* of _I_.
If a component instance defines no dictionary elements, then the
identifier range is empty.
All the numbers in the identifier range of _I_ are reserved for
instance _I_ (even if they are not all used).
No other component instance may have a base identifier that lies within the
identifier range of _I_.
For example, this code is illegal:
[source,fpp]
-------
module FSW {
@ Temperature sensor for the left engine
instance leftEngineTemp: Sensors.EngineTemp base id 0x100
@ Temperature sensor for the right engine
instance rightEngineTemp: Sensors.EngineTemp base id 0x101
}
-------
The base identifier 0x101 for `rightEngineTemp` is inside the
identifier range for `leftEngineTemp`, which goes from
0x100 to 0x101, inclusive.
==== Queued Components
Instantiating a queued component is just like instantiating
a passive component, except that you must also specify
a queue size for the instance.
You do this by writing the keywords `queue` `size` and
the queue size after the base identifier.
Here is an example:
[source,fpp]
----
module Sensors {
@ A port for calibration input
port Calibration(cal: F32)
@ A component for sensing engine temperature
queued component EngineTemp {
@ Schedule input port
sync input port schedIn: Svc.Sched
@ Calibration input
async input port calibrationIn: Calibration
@ Telemetry port
telemetry port tlmOut
@ Time get port
time get port timeGetOut
@ Impulse engine temperature
telemetry ImpulseTemp: F32
@ Warp core temperature
telemetry WarpTemp: F32
}
}
module FSW {
@ Engine temperature sensor
instance engineTemp: Sensors.EngineTemp base id 0x100 \
queue size 10
}
----
In the component definition, we have revised the example from the previous
section so that
the `EngineTemp` component is queued instead of passive,
and we have added an async input port for calibration input.
In the component instance definition, we have specified a queue size of 10.
==== Active Components
Instantiating an active component is like instantiating a queued
component, except that you may specify additional parameters
that configure the OS thread associated with each component instance.
*Queue size, stack size, and priority:*
When instantiating an active component, you _must_
specify a queue size, and you _may_ specify either or both of
a stack size and priority.
You specify the queue size in the same way as for a queued component.
You specify the stack size by writing the keywords `stack` `size`
and the desired stack size in bytes.
You specify the priority by writing the keyword `priority`
and a numeric priority.
The priority number is passed to the OS operation for creating
the thread, and its meaning is OS-specific.
Here is an example:
[source,fpp]
----
module Utils {
@ A component for compressing data
active component DataCompressor {
@ Uncompressed input data
async input port bufferSendIn: Fw.BufferSend
@ Compressed output data
output port bufferSendOut: Fw.BufferSend
}
}
module FSW {
module Default {
@ Default queue size
constant queueSize = 10
@ Default stack size
constant stackSize = 10 * 1024
}
@ Data compressor instance
instance dataCompressor: Utils.DataCompressor base id 0x100 \
queue size Default.queueSize \
stack size Default.stackSize \
priority 30
}
----
We have defined an active component `Utils.DataCompressor`
for compressing data.
We have defined an instance of this component called
`FSW.dataCompressor`.
Our instance has base identifier 0x100, the default
queue size, the default stack size, and priority 30.
We have used
<<Defining-Constants,constant definitions>> for
the default queue and stack sizes.
We could also have omitted either or both of the stack size and priority
specifiers.
When you omit the stack size or priority from a component instance
definition, F Prime supplies a default value appropriate to the
target platform.
With implicit stack size and priority, the `dataCompressor`
instance looks like this:
[source,fpp]
--------
instance dataCompressor: Utils.DataCompressor base id 0x100 \
queue size Default.queueSize
--------
*CPU affinity:*
When defining an active component, you may specify
a *CPU affinity*.
The CPU affinity is a number whose meaning depends on
the platform.
Usually it is an instruction to the operating system
to run the thread of the active component on a particular
CPU, identified by number.
To specify CPU affinity, you write the keyword `cpu`
and the CPU number after the queue size, the stack size (if any),
and the priority specifier (if any).
For example:
[source,fpp]
--------
instance dataCompressor: Utils.DataCompressor base id 0x100 \
queue size Default.queueSize \
stack size Default.stackSize \
priority 30 \
cpu 0
--------
This example is the same as the previous `dataCompressor`
instance, except that we have specified that the thread
associated with the instance should run on CPU 0.
With implicit stack size and priority, the example looks like this:
[source,fpp]
--------
instance dataCompressor: Utils.DataCompressor base id 0x100 \
queue size Default.queueSize \
cpu 0
--------
=== Specifying the Implementation
When you define a component instance _I_, the FPP translator needs
to know the following information about the {cpp} implementation of _I_:
. The type (i.e., the name of the {cpp} class) that defines the
implementation.
. The location of the {cpp} header file that declares the implementation
class.
In most cases, the translator can infer this information.
However, in some cases you must specify it manually.
*The implementation type:*
The FPP translator can automatically infer the implementation
type if its qualified {cpp} class name matches the qualified
name of the FPP component.
For example, the {cpp} class name `A::B` matches the FPP component
name `A.B`.
More generally, modules in FPP become namespaces in {cpp}, so
dot qualifiers in FPP become double-colon qualifiers in {cpp}.
If the names do not match, then you must provide the type
associated with the implementation.
You do this by writing the keyword `type` after the base identifier,
followed by a <<Defining-Constants_Expressions_String-Values, string>>
specifying the implementation type.
For example, suppose we have a {cpp} class `Utils::SpecialDataCompressor`,
which is a specialized implementation of the FPP component
`Utils.DataCompressor`.
By default, when we specify `Utils.DataCompressor` as the component name, the
translator infers `Utils::DataCompressor` as the implementation type.
Here is how we specify the implementation type `Utils::SpecialDataCompressor`:
[source,fpp]
--------
instance dataCompressor: Utils.DataCompressor base id 0x100 \
type "Utils::SpecialDataCompressor" \
queue size Default.queueSize \
cpu 0
--------
*The header file:*
The FPP translator can automatically locate the header file for _I_
if it conforms to the following rules:
. The name of the header file is `Name.hpp`, where `Name`
is the name of the component in the FPP model, without
any module qualifiers.
. The header file is located in the same directory as the FPP
source file that defines the component.
For example, the F Prime repository contains a reference FSW implementation
with instances defined in the file `Ref/Top/instances.fpp`.
One of the instances is `SG1`.
Its definition reads as follows:
[source,fpp]
--------
instance SG1: Ref.SignalGen base id 0x2100 \
queue size Default.queueSize
--------
The FPP component `Ref.SignalGen` is
defined in the directory `Ref/SignalGen/SignalGen.fpp`,
and the implementation class `Ref::SignalGen` is declared in
the header file `Ref/SignalGen/SignalGen.hpp`.
In this case, the header file follows rules (1) and (2)
stated above, so the FPP translator can automatically locate
the file.
If the implementation header file does not follow
rules (1) and (2) stated above, then you must specify
the name and location of the header file by hand.
You do that by writing the keyword `at` followed by
a <<Defining-Constants_Expressions_String-Values, string>>
specifying the header file path.
The header file path is relative to the directory
containing the source file that defines the component
instance.
For example, the F Prime repository has a directory
`Svc/Time` that contains an FPP model for a component `Svc.Time`.
Because the {cpp} implementation for this component
is platform-specific, the directory `Svc/Time` doesn't
contain any implementation.
Instead, when instantiating the component, you have to
provide the header file to an implementation located
in a different directory.
The F Prime repository also provides a Linux-specific implementation
of the `Time` component in the directory `Svc/LinuxTime`.
The file `Ref/Top/instances.fpp` contains an instance definition
`linuxTime` that reads as follows:
[source,fpp]
----
instance linuxTime: Svc.Time base id 0x4500 \
type "Svc::LinuxTime" \
at "../../Svc/LinuxTime/LinuxTime.hpp"
----
This definition says to use the implementation of the component
`Svc.Time` with {cpp} type name `Svc::LinuxTime` defined in the header
file `../../Svc/LinuxTime/LinuxTime.hpp`.
=== Init Specifiers
In an F Prime FSW application, each component instance _I_
has some associated {cpp} code
for setting up _I_ when FSW starts up
and tearing down _I_ when FSW exits.
Much of this code can be inferred from the FPP model,
but some of it is implementation-specific.
For example, each instance of the standard F Prime command sequencer
component has a method `allocateBuffer` that the FSW must
call during setup to allocate the sequence buffer
for that instance.
The FPP model does not represent this function;
instead, you have to provide
the function call directly in {cpp}.
To do this, you write one or more *init specifiers*
as part of a component instance definition.
An init specifier names a phase
of the setup or teardown process and
provides a snippet of literal {cpp} code.
The FPP translator pastes the snippet into the setup
or teardown code according to the phase named in
the specifier.
(Strictly speaking, the init specifier should be called
a "setup or teardown specifier."
However, most of the code is in fact initialization code,
and so FPP uses "init" as a shorthand name.)
==== Execution Phases
The FPP translator uses init specifiers when it generates
code for an F Prime topology.
We will have more to say about topology generation in the
next section.
For now, you just need to know the following:
. A topology is a unit of an FPP model that specifies the top-level
structure of an F Prime application (the component instances
and their connections).
. Each topology has a name, which we will refer to here generically as _T_.
. When generating {cpp} code for topology _T_, the code generator produces
files _T_ `TopologyAc.hpp` and _T_ `TopologyAc.cpp`.
The generated code in _T_ `TopologyAc.hpp` and _T_ `TopologyAc.cpp`
is divided into several phases of execution.
Table <<execution-phases>> shows the execution phases
recognized by the FPP code generator.
In this table, _T_ is the name of a topology and _I_ is the
name of a component instance.
The columns of the table have the following meanings:
* *Phase:* The symbol denoting the execution phase.
These symbols are the enumerated constants of the
<<Defining-Enums,enum>> `Fpp.ToCpp.Phases` defined in
`Fpp/ToCpp.fpp` in the F Prime repository.
* *Generated File:* The generated file for topology _T_
that contains the definition:
either _T_ `TopologyAc.hpp` (for compile-time symbols)
or _T_ `TopologyAc.cpp` (for link-time symbols).
* *Intended Use:* The intended use of the {cpp} code snippet
associated with the instance _I_ and the phase.
* *Where Placed:* Where FPP places the code snippet
in the generated file.
* *Default Code:* Whether FPP generates default code if
there is no init specifier for instance _I_
and for this phase.
If there is an init specifier, then it replaces any
default code.
[[execution-phases]]
.Execution Phases
|===
|Phase|Generated File|Intended Use|Where Placed|Default Code
|`configConstants`
|_T_ `TopologyAc.hpp`
|{cpp} constants for use in constructing and
initializing an instance _I_.
|In the namespace `ConfigConstants::` _I_.
|None.
|`configObjects`
|_T_ `TopologyAc.cpp`
|Statically declared {cpp} objects for use in
constructing and initializing instance _I_.
|In the namespace `ConfigObjects::` _I_.
|None.
|`instances`
|_T_ `TopologyAc.cpp`
|A constructor for an instance _I_ that has a non-standard
constructor format.
|In an anonymous (file-private) namespace.
|The standard constructor call for _I_.
|`initComponents`
|_T_ `TopologyAc.cpp`
|Initialization code for an instance _I_ that has a non-standard
initialization format.
|In the file-private function `initComponents`.
|The standard call to `init` for _I_.
|`configComponents`
|_T_ `TopologyAc.cpp`
|Implementation-specific configuration code for an instance _I_.
|In the file-private function `configComponents`.
|None.
|`regCommands`
|_T_ `TopologyAc.cpp`
|Code for registering the commands of _I_ (if any)
with the command dispatcher.
Required only if _I_ has a
non-standard command registration format.
|In the file-private function `regCommands`.
|The standard call to `regCommands` if _I_ has commands;
otherwise none.
|`readParameters`
|_T_ `TopologyAc.cpp`
|Code for reading parameters from a file.
Ordinarily used only when _I_ is the parameter database.
|In the file-private function `readParameters`.
|None.
|`loadParameters`
|_T_ `TopologyAc.cpp`
|Code for loading parameter values from the parameter database.
Required only if _I_ has a non-standard parameter-loading
format.
|In the file-private function `loadParameters`.
|The standard call to `loadParameters` if _I_
has parameters; otherwise none.
|`startTasks`
|_T_ `TopologyAc.cpp`
|Code for starting the task (if any) of _I_.
|In the file-private function `startTasks`.
|The standard call to `startTasks` if _I_
is an active component; otherwise none.
|`stopTasks`
|_T_ `TopologyAc.cpp`
|Code for stopping the task (if any) of _I_.
|In the file-private function `stopTasks`.
|The standard call to `exit` if _I_
is an active component; otherwise none.
|`freeThreads`
|_T_ `TopologyAc.cpp`
|Code for freeing the thread associated with _I_.
|In the file-private function `freeThreads`.
|The standard call to `join` if _I_ is an
active component; otherwise none.
|`tearDownComponents`
|_T_ `TopologyAc.cpp`
|Code for deallocating the allocated memory
(if any) associated with _I_.
|In the file-private function `tearDownComponents`.
|None.
|===
You will most often need to write code for `configConstants`,
`configObjects`, and `configComponents`.
These phases often require implementation-specific input that
cannot be provided in any other way, except to write an init specifier.
In theory you should never have to write code for `instances`
or `initComponents` -- this code can be be standardized --
but in practice not all F Prime components conform to the standard,
so you may have to override the default.
You will typically not have to write code for `regCommands`,
`readParameters`, and `loadParameters` -- the framework can generate
this code automatically -- except that the parameter database
instance needs one line of special code for reading its parameters.
Code for `startTasks`, `stopTasks`,
and `freeThreads` is required only if the user-written implementation of
a component instance manages its own F Prime task.
If you use a standard F Prime active component, then the framework
manages the task, and this code is generated automatically.
Code for `tearDownComponents` is required only if a component
instance needs to deallocate memory or release resources on program exit.
==== Writing Init Specifiers
You may write one or more init specifiers as part of a component
instance definition.
The init specifiers, if any, come at the end of the
definition and must be enclosed in curly braces.
The init specifiers form an
<<Defining-Constants_Multiple-Definitions-and-Element-Sequences,element sequence>>
with a semicolon as the optional terminating punctuation.
To write an init specifier, you write the following:
* The keyword `phase`.
* The
<<Defining-Component-Instances_Init-Specifiers_Execution-Phases,
execution phase>>
of the init specifier.
* A
<<Defining-Constants_Expressions_String-Values, string>>
that provides the code snippet.
It is usually convenient, but not required, to use a multiline string
for the code snippet.
As an example, here is the component instance definition for the
command sequencer instance `cmdSeq` from the
https://github.com/fprime-community/fprime-system-reference/blob/main/SystemReference/Top/instances.fpp[F Prime system reference deployment]:
[source,fpp]
--------
instance cmdSeq: Svc.CmdSequencer base id 0x0700 \
queue size Default.queueSize \
stack size Default.stackSize \
priority 100 \
{
phase Fpp.ToCpp.Phases.configConstants """
enum {
BUFFER_SIZE = 5*1024
};
"""
phase Fpp.ToCpp.Phases.configComponents """
cmdSeq.allocateBuffer(
0,
Allocation::mallocator,
ConfigConstants::SystemReference_cmdSeq::BUFFER_SIZE
);
"""
phase Fpp.ToCpp.Phases.tearDownComponents """
cmdSeq.deallocateBuffer(Allocation::mallocator);
"""
}
--------
The code for `configConstants` provides a constant `BUFFER_SIZE`
that is used in the `configComponents` phase.
The code generator places this code snippet in the
namespace `ConfigConstants::SystemReference_cmdSeq`.
Notice that the second part of the namespace uses the
fully qualified name `SystemReference::cmdSeq`, and it replaces
the double colon `::` with an underscore `_` to generate
the name.
We will explain this behavior further in the section on
<<Defining-Component-Instances_Generation-of-Names,generation of names>>.
The code for `configComponents` calls `allocateBuffer`, passing
in an allocator object that is declared elsewhere.
(In the section on
<<Writing-C-Plus-Plus-Implementations_Implementing-Deployments,
implementing deployments>>, we will explain where this allocator
object is declared.)
The code for `tearDownComponents` calls `deallocateBuffer` to
deallocate the sequence buffer, passing in the allocator
object again.
As another example, here is the instance definition for the parameter
database instance `prmDb` from the system reference deployment:
[source,fpp]
--------
instance prmDb: Svc.PrmDb base id 0x0D00 \
queue size Default.queueSize \
stack size Default.stackSize \
priority 96 \
{
phase Fpp.ToCpp.Phases.instances """
Svc::PrmDb prmDb(FW_OPTIONAL_NAME("prmDb"), "PrmDb.dat");
"""
phase Fpp.ToCpp.Phases.readParameters """
prmDb.readParamFile();
"""
}
--------
Here we provide code for the `instances` phase because the constructor
call for this component is nonstandard -- it takes the parameter
file name as an argument.
In the `readParameters` phase, we provide the code for reading the parameters
from the file.
As discussed above, this code is needed only for the parameter database
instance.
When writing init specifiers, you may read (but not modify) a special value
`state` that you define in a handwritten main function.
This value lets you pass application-specific information from the
handwritten code to the auto-generated code.
We will explain the special `state` value further in the
section on <<Writing-C-Plus-Plus-Implementations_Implementing-Deployments,
implementing deployments>>.
For more examples of init specifiers in action, see the rest of
the file `SystemReference/Top/instances.fpp` in the F Prime repository.
In particular, the init specifiers for the `comDriver` instance
use the `state` value that we just mentioned.
=== Generation of Names
FPP uses the following rules to generate the names associated with
component instances.
First, as explained in the section on
<<Defining-Component-Instances_Specifying-the-Implementation,
specifying the implementation>>,
a component type `M.C` in FPP becomes the type `M::C` in {cpp}.
Here `C` is a {cpp} class defined in namespace `M` that
implements the behavior of component `C`.
Second, a component instance _I_ defined in module _N_ becomes
a {cpp} variable _I_ defined in namespace _N_.
For example, this FPP code
[source,fpp]
--------
module N {
instance i: M.C base id 0x100
}
--------
becomes this code in the generated {cpp}:
[source,c++]
----
namespace N {
M::C i;
}
----
So the fully qualified name of the instance is `N.i` in FPP and `N::i`
in {cpp}.
Third, all other code related to instances is generated in the namespace of the
top-level implementation.
For example, in the System Reference example from the previous section,
the top-level implementation is in the namespace `SystemReference`, so
the code for configuring constants is generated in that namespace.
We will have more to say about the top-level implementation in
the section on <<Writing-C-Plus-Plus-Implementations_Implementing-Deployments,
implementing deployments>>.
Fourth, when generating the name of a constant associated with an instance,
FPP uses the fully-qualified name of the instance, and it replaces
the dots (in FPP) or the colons (in {cpp}) with underscores.
For example, as discussed in the previous section, the configuration
constants for the instance `SystemReference::cmdSeq` are placed in
the namespace `ConfigConstants::SystemReference_cmdSeq`.
This namespace, in turn, is placed in the namespace `SystemReference`
according to the previous paragraph.