Files
fpp/docs/users-guide/Defining-Topologies.adoc
2026-03-22 18:35:06 -07:00

1819 lines
50 KiB
Plaintext

== Defining Topologies
In F Prime, a *topology* or connection graph is the
highest level of software architecture in a FSW application.
A topology specifies what
<<Defining-Component-Instances,component instances>>
are used in the application and how their
<<Defining-Components_Port-Instances,port instances>>
are connected.
An F Prime FSW application consists of a topology _T_;
all the types, ports, and components used by _T_;
and a small amount of top-level {cpp} code that you write by hand.
In the section on
<<Writing-C-Plus-Plus-Implementations_Implementing-Deployments,
implementing deployments>>, we will explain more about the top-level
{cpp} code.
In this section we explain how to define a topology in FPP.
=== A Simple Example
We begin with a simple example that shows how many of the pieces
fit together.
[source,fpp]
----
port P
passive component C {
sync input port pIn: P
output port pOut: P
}
instance c1: C base id 0x100
instance c2: C base id 0x200
@ A simple topology
topology Simple {
@ This specifier says that instance c1 is part of the topology
instance c1
@ This specifier says that instance c2 is part of the topology
instance c2
@ This code specifies a connection graph C1
connections C1 {
c1.pOut -> c2.pIn
}
@ This code specifies a connection graph C2
connections C2 {
c2.pOut -> c1.pIn
}
}
----
In this example, we define a <<Defining-Ports,port>> `P`.
Then we define a <<Defining-Components,passive component>> `C`
with an input port and an output port, both of type `P`.
We define two <<Defining-Component-Instances,instances>> of
`C`, `c1` and `c2`.
We put these instances into a topology called `Simple`.
As shown, to define a topology, you write the keyword `topology`,
the name of the topology, and the members of the topology
definition enclosed in curly braces.
In this case, the topology has two kinds of members:
. Two *instance specifiers* specifying that instances
`c1` and `c2` are part of the topology.
. Two *graph specifiers* that specify connection graphs
named `C1` and `C2`.
As shown, to write an instance specifier, you write the
keyword `instance` and the name of a component instance
definition.
In general the name may be a qualified name such as `A.B`.
if the instance is defined inside a
<<Defining-Modules,module>>; in this simple
example it is not.
Each instance specifier states that the instance it names
is part of the topology.
The instances appearing in the list must be distinct.
For example, this is not correct:
[source,fpp]
--------
topology T {
instance c1
instance c1 # Error: duplicate instance c1
}
--------
A graph specifier specifies one or more connections
between component instances.
Each graph specifier has a name.
By dividing the connections of a topology into named
graphs, you can organize the connections in a meaningful way.
For example you can have one graph group
for connections that send commands, another one
for connections that send telemetry, and so forth.
We will have more to say about this in a later section.
As shown, to write a graph specifier, you may write the keyword `connections`
followed by the name of the graph; then you may list
the connections inside curly braces.
(In the next section, we will explain another way to write a graph specifier.)
Each connection consists of an endpoint, an arrow `pass:[->]`,
and another endpoint.
An endpoint is the name of a component instance
(which in general may be a qualified name), a dot,
and the name of a port of that component instance.
In this example there are two connection graphs, each containing
one connection:
. A connection graph `C1` containing a connection from `c1.pOut` to `c2.pIn`.
. A connection graph `C2` containing a connection from `c2.pOut` to `c1.pIn`.
As shown, topologies and their members are
<<Writing-Comments-and-Annotations_Annotations,annotatable elements>>.
The topology members form an
<<Defining-Constants_Multiple-Definitions-and-Element-Sequences,
element sequence>> in which the optional
terminating punctuation is a semicolon.
=== Connection Graphs
In general, an FPP topology consists of a list of instances
and a set of named connection graphs.
There are two ways to specify connection graphs:
*direct graph specifiers* and *pattern graph specifiers*.
==== Direct Graph Specifiers
A direct graph specifier provides a name and a list
of connections.
We illustrated direct graph specifiers in the
previous section, where the simple topology example
included direct graph specifiers for graphs named
`C1` and `C2`.
Here are some more details about direct graph specifiers.
As shown in the previous section, each connection consists
of an output port specifier, followed by an arrow, followed
by an input port specifier.
For example:
[source,fpp]
--------
connections C {
a.p -> b.p
}
--------
Each of the two port specifiers consists of a component
instance name, followed by a dot, followed the name of a port instance.
The component instance name must refer to a
<<Defining-Component-Instances,component instance definition>>
and may be qualified by a module name.
For example:
[source,fpp]
--------
connections C {
M.a.p -> N.b.p
}
--------
Here component instance `a` is defined in module `M` and component
instance `b` is defined in module `N`.
In a port specifier `a.p`, the port instance name `p` must refer to a
<<Defining-Components_Port-Instances,port instance>> of the
component definition associated with the component instance `a`.
Each component instance named in a connection must be part of the
instance list in the topology.
For example, if you write a connection `a.b pass:[->] c.d` inside
a topology `T`, and the specifier `instance a` does not
appear inside topology `T`, then you will get an error --
even if `a` is a valid instance name for the FPP model.
The reason for this rule is that in flight code we need
to be very careful about which instances are included
in the application.
Naming all the instances also lets us check for
<<Analyzing-and-Translating-Models_Checking-Models,
unconnected ports>>.
You may use the same name in more than one direct
graph specifier in the same topology.
If you do this, then all specifiers with the same
name are combined into a single graph with that name.
For example, this code
[source,fpp]
--------
connections C {
a.p -> b.p
}
connections C {
c.p -> d.p
}
--------
is equivalent to this code:
[source,fpp]
--------
connections C {
a.p -> b.p
c.p -> d.p
}
--------
The members of a direct graph specifier form an
<<Defining-Constants_Multiple-Definitions-and-Element-Sequences,
element sequence>> in which the optional
terminating punctuation is a comma.
For example, you can write this:
[source,fpp]
--------
connections C { a.p -> b.p, c.p -> d.p }
--------
The connections appearing in direct graph specifiers must obey the
following rules:
* Each connection must go from an output port instance to
an input port instance.
* The types of the ports must match, except that a
<<Defining-Components_Port-Instances_Serial-Port-Instances,
serial port instance>> may be connected to a port of any
type.
In particular, serial to serial connections are allowed.
* If a typed port _P_ is connected to a serial port in either direction,
then the port type of _P_ may not specify a
<<Defining-Ports_Returning-Values,return type>>.
==== Pattern Graph Specifiers
A few connection patterns are so common in F Prime that they
get special treatment in FPP.
For example, an F Prime topology typically includes an
instance of the component `Svc.Time`.
This component has a port `timeGetPort`
of type `Fw.Time` that other components can use to get the system
time.
Any component that gets the system time
(and there are usually several) has a connection to
the `timeGetPort` port of the `Svc.Time` instance.
Suppose you are constructing a topology in which
(1) `sysTime` is an instance of `Svc.Time`; and (2)
each of the instances
`a`, `b`, `c`, etc., has a
<<Defining-Components_Special-Port-Instances_Time-Get-Ports, time get port>>
`timeGetOut` port connected to `sysTime.timeGetPort`,
If you used a direct graph specifier to write all these connections,
the result might look like this:
[source,fpp]
--------
connections Time {
a.timeGetOut -> sysTime.timeGetPort
b.timeGetOut -> sysTime.timeGetPort
c.timeGetOut -> sysTime.timeGetPort
...
}
--------
This works, but it is tedious and repetitive. So FPP provides
a better way: you can use a *pattern graph specifier*
to specify this common pattern.
You can write
[source,fpp]
--------
time connections instance sysTime
--------
This code says the following:
. Use the instance `sysTime` as the instance of `Fw.Time`
for the time connection pattern.
. Automatically construct a direct graph specifier named `Time`.
In this direct graph specifier, include one connection
from each component instance that has a time get port
to the input port of `sysTime` of type `Fw.Time`.
The result is as if you had written the direct graph specifier
yourself.
All the other rules for direct graph specifiers apply: for example,
if you write another direct graph specifier with name `Time`, then
the connections in that specifier are merged with the connections
generated by the pattern specifier.
In the example above, we call `time` the *kind* of the pattern
graph specifier.
We call `sysTime` the *source instance* of the pattern.
It is the source of all the time pattern connections
in the topology.
We call the instances that have time get ports (and so contribute
connections to the pattern) the *target instances*.
They are the instances targeted by the pattern once the source
instance is named.
Table <<pattern-graph-specifiers>> shows the pattern graph
specifiers allowed in FPP.
The columns of the table have the following meanings:
* *Kind:* The keyword or keywords denoting the kind.
When writing the specifier, these appear just before
the keyword `connections`, as shown above for the time example.
* *Source Instance:* The source instance for the pattern.
* *Target Instances:* The target instances for the pattern.
* *Graph Name:* The name of the connection graph
generated by the pattern.
* *Connections:* The connections generated by the pattern.
The command pattern specifier generates three connection graphs:
`Command`, `CommandRegistration`, and `CommandResponse`.
[[pattern-graph-specifiers]]
.Pattern Graph Specifiers
|===
|Kind|Source Instance|Target Instances|Graph Name|Connections
|
|
|
|`Command`
|All connections from the unique output port of type `Fw::Cmd`
of the source instance to the
<<Defining-Components_Special-Port-Instances_Command-Ports,
`command` `recv` port>>
of each target instance.
|`command`
|An instance of `Svc.CommandDispatcher` or a similar component for
dispatching commands.
The instance must have a unique output port of type `Fw.Cmd`,
a unique input port of type `Fw.CmdReg`, and a unique
input port of type `Fw.CmdResponse`.
|Each instance that has
<<Defining-Components_Special-Port-Instances_Command-Ports,
command ports>>.
|`CommandRegistration`
|All connections from the
<<Defining-Components_Special-Port-Instances_Command-Ports,
`command` `reg` port>> of each target instance to the
unique input port of type `Fw.CmdReg` of the source instance.
|
|
|
|`CommandResponse`
|All connections from the
<<Defining-Components_Special-Port-Instances_Command-Ports,
`command` `resp` port>> of each target instance to the
unique input port of type `Fw.CmdResponse` of the source instance.
|`event`
|An instance of `Svc.ActiveLogger` or a similar component for
logging event reports.
The instance must have a unique input port of type
`Fw.Log`.
|Each instance that has an
<<Defining-Components_Special-Port-Instances_Event-Ports,
`event` port>>.
|`Events`
|All connections from the
<<Defining-Components_Special-Port-Instances_Event-Ports,
`event` port>> of each target instance to the unique
input port of type `Fw.Log` of the source instance.
|`health`
|An instance of `Svc.Health` or a similar component for
health monitoring.
The instance must have a unique output port of type
`Svc.Ping` and a unique input port of type `Svc.Ping`.
|Each instance other than the source instance
that has a unique output port of type
`Svc.Ping` and a unique input port of type `Svc.Ping`.
|`Health`
|(1) All connections from the unique output port of type
`Svc.Ping` of each target instance to the unique input
port of type `Svc.Ping` of the source instance.
(2) All connections from the unique output port of type
`Svc.Ping` of the source instance to the unique
input port of type `Svc.Ping` of each target instance.
|`param`
|An instance of `Svc.PrmDb` or a similar component representing
a database of parameters.
The instance must have a unique input port of type `Fw.PrmGet`
and a unique input port of type `Fw.PrmSet`.
|Each instance that has
<<Defining-Components_Special-Port-Instances_Parameter-Ports,
parameter ports>>.
|`Parameters`
|(1) All connections from the
<<Defining-Components_Special-Port-Instances_Parameter-Ports,
`param` `get` port>> of each target instance
to the unique input port of type `Fw.PrmGet` of the source instance.
(2) All connections from the
<<Defining-Components_Special-Port-Instances_Parameter-Ports,
`param` `set` port>> of each target instance
to the unique input port of type `Fw.PrmSet` of the source instance.
|`telemetry`
|An instance of `Svc.TlmChan` or a similar component for
storing channelized telemetry.
The instance must have a unique input port of type `Fw.Tlm`.
|Each instance that has a <<Defining-Components_Special-Port-Instances_Telemetry-Ports,
telemetry port>>.
|`Telemetry`
|All connections from the
<<Defining-Components_Special-Port-Instances_Telemetry-Ports,
`telemetry` port>> of each target instance to the unique input
port of type `Fw.Tlm` of the source instance.
|`text` `event`
|An instance of `Svc.PassiveTextLogger` or a similar component
for logging event reports in textual form.
The instance must have a unique input port of type `Fw.LogText`.
|Each instance that has a <<Defining-Components_Special-Port-Instances_Event-Ports,
`text` `event` port>>.
|`TextEvents`
|All connections from the
<<Defining-Components_Special-Port-Instances_Event-Ports,
`text` `event` port>> of each target instance to the unique
input port of type `Fw.LogText` of the source instance.
|`time`
|An instance of `Svc.Time` or a similar component for providing
the system time.
The instance must have a unique input port of type `Fw.Time`.
|Each instance that has a
<<Defining-Components_Special-Port-Instances_Time-Get-Ports,
`time` `get` port>>.
|`Time`
|All connections from the
<<Defining-Components_Special-Port-Instances_Time-Get-Ports,
`time` `get` port>> of each target instance to the unique
input port of type `Fw.Time` of the source instance.
|===
Here are some rules for writing graph pattern specifiers:
. At most one occurrence of each pattern kind is allowed in
each topology.
. For each pattern, the required ports shown in the table
must exist and must be unambiguous.
For example, if you write a time pattern
+
[source,fpp]
--------
time connections instance sysTime
--------
+
then you will get an error if `sysTime` has no
input ports of type `Fw.Time`,
You will also get an error if `sysTime` has two or more
such ports.
The default behavior for a pattern is
to generate the connections for all target instances
as shown in the table.
If you wish, you may generate connections for a selected
set of target instances.
To do this, you write a list of target instances enclosed in
curly braces after the source instance.
For example, suppose a topology contains instances
`a`, `b`, and `c` each of which has an output port
that satisfies the time pattern.
And suppose that `sysTime` is an instance of `Svc.Time`.
Then if you write this pattern
[source,fpp]
--------
time connections instance sysTime
--------
you will get a connection graph `Time` containing
time connections from each of `a`, `b`, and `c` to `sysTime`.
But if you write this pattern
[source,fpp]
--------
time connections instance sysTime {
a
b
}
--------
then you will just get the connections from `a` and `b`
to `sysTime`.
The instances `a` and `b` must be valid target instances
for the pattern.
As with connections, you can write the instances `a` and `b`
each on its own line, or you can separate them with commas:
[source,fpp]
--------
time connections instance sysTime { a, b }
--------
=== Port Numbering
As discussed in the
<<Defining-Components_Port-Instances_Arrays-of-Port-Instances,
section on defining components>>,
each named port instance is actually an array of
one or more port instances.
When the size of the array exceeds one, you
must specify the port number (i.e., the array index)
of each connection going into or out of the port instance.
In FPP, there are three ways to specify port numbers:
explicit numbering, matched numbering, and general numbering.
==== Explicit Numbering
To use explicit numbering, you provide an explicit port number
for a connection endpoint.
You write the port number as a
<<Defining-Constants_Expressions,numeric expression>>
in square brackets, immediately following the port name.
The port numbers start at zero.
For example, the `RateGroups` graph of the Ref (reference) topology in the F Prime
repository defines the rate group connections.
It contains the following connection:
[source,fpp]
--------
rateGroupDriverComp.CycleOut[Ports.RateGroups.rateGroup1] -> rateGroup1Comp.CycleIn
rateGroup1Comp.RateGroupMemberOut[0] -> SG1.schedIn
rateGroup1Comp.RateGroupMemberOut[1] -> SG2.schedIn
rateGroup1Comp.RateGroupMemberOut[2] -> chanTlm.Run
rateGroup1Comp.RateGroupMemberOut[3] -> fileDownlink.Run
--------
The first line says to connect the port at index
`Ports.RateGroups.rateGroup1` of `rateGroupDriverComp.CycleOut`
to `rateGroup1Comp.CycleIn`.
The symbol `Ports.RateGroups.rateGroup1` is an enumerated constant, defined
like this:
[source,fpp]
----
module Ports {
enum RateGroups {
rateGroup1
rateGroup2
rateGroup3
}
}
----
The second and following lines say to connect the ports of
`rateGroup1Comp.RateGroupMemberOut` at the indices 0, 1, 2, and 3
in the manner shown.
As another example, the `Downlink` graph of the reference topology
contains the following connection:
[source,fpp]
--------
downlink.framedAllocate -> staticMemory.bufferAllocate[Ports.StaticMemory.downlink]
--------
This line says to connect `downlink.framedAllocate` to
`staticMemory.bufferAllocate` at index
`Port.StaticMemory.downlink`.
Again the port index is a symbolic constant.
If you wish, you may write two explicit port numbers,
one at each endpoint.
For example:
[source,fpp]
--------
a.b[0] -> c.d[1]
--------
Here are some rules to keep in mind when using explicit numbering:
. You can write any numeric expression as a port number.
Each port number must be in bounds for the port (greater than
or equal to zero and less than the size of the port array).
If you write a port number that is out of bounds, you will get an error.
. Use symbolic constants judiciously.
Avoid scattering "magic" literal
constants throughout the topology definition.
For example:
.. The Ref topology uses the symbolic constants
`Ports.RateGroups.rateGroup1` and `Ports.StaticMemory.downlink`, as shown
above.
Because these constants appear in several different places, it is
better to use symbolic constants here.
Using literal constants would decrease readability and increase
the chance of using incorrect or inconsistent numbers.
.. The Ref topology uses the literal constants 0, 1, 2, and 3
to connect the ports of `rateGroup1Comp.RateGroupMemberOut`.
Here there are no obvious names to associate with the numbers,
the numbers go in sequence, and all the numbers appear together in one place.
So there is no clear benefit to giving them names.
. Remember that in F Prime, multiple connections can go to the same
input port, but only one connection can go from each output port.
For example, this code is allowed:
+
[source,fpp]
--------
c1.p1 -> c2.p[0]
c1.p2 -> c2.p[0] # OK: Two connections into c2.p[0]
--------
+
But this code is incorrect:
+
[source,fpp]
--------
c1.p[0] -> c2.p1
c1.p[0] -> c2.p2 # Error: Two connections out of c1.p[0]
--------
. Use explicit numbering as little as possible.
Instead, use matched numbering or general numbering
(described in the next sections) and let FPP
do the numbering for you.
In particular, avoid writing zero indices such as
`c.p[0]` except in cases where you need to control the assignment
of numbers, such as in the rate group example shown above.
In other cases, write `c.p` and let FPP infer
the zero index.
For example, this is what we did in the section on
<<Defining-Topologies_Connection-Graphs_Direct-Graph-Specifiers,
direct graph specifiers>>.
==== Matched Numbering
*Automatic matching:*
After resolving
<<Defining-Topologies_Port-Numbering_Explicit-Numbering,
explicit numbering>>, the FPP translator applies
*matched numbering*.
In this step, the translator numbers all pairs of
<<Defining-Components_Matched-Ports,matched ports>>.
Matched numbering is essential for resolving the command and health
<<Defining-Topologies_Connection-Graphs_Pattern-Graph-Specifiers,
patterns>>, each of which has matched ports.
You can also use matched numbering in conjunction with direct
graph specifiers.
For example, the Ref topology contains the following connections:
[source,fpp]
--------
connections Sequencer {
cmdSeq.comCmdOut -> cmdDisp.seqCmdBuff
cmdDisp.seqCmdStatus -> cmdSeq.cmdResponseIn
}
connections Uplink {
...
uplink.comOut -> cmdDisp.seqCmdBuff
cmdDisp.seqCmdStatus -> uplink.cmdResponseIn
...
}
--------
The port `cmdDisp.seqCmdBuff` port of the command dispatcher receives
command input from the command sequencer or from the ground.
The corresponding command response goes out on
port `cmdDisp.seqCmdStatus`.
These two ports are matched in the definition of the Command
Sequencer component.
When you use matched numbering with direct graph specifiers, you
must obey the following rules:
. When a component has the matching specifier
`match p1 with p2`, for every connection between `p1`
and another component, there must be a corresponding
connection between that other component and `p2`.
. You can use explicit numbering, and the automatic matching
will work around the numbers you supply if it can.
However, you may not do this in a way that makes the matching impossible.
For example, you may not connect `p1[0]` to another component
and `p2[1]` to the same component, because this connection
forces a mismatch.
. Duplicate connections at the same port number of `p1` or
`p2` are not allowed, even if `p1` or `p2` are input ports.
If you violate these rules, you will get an error during
analysis.
You can relax these rules by writing unmatched connections,
as described below.
*Unmatched connections:*
Occasionally you may need to relax the rules for using matched ports.
For example, you may need to match pairs of connections that use
the F Prime hub pattern to cross a network boundary.
In this case, although the connections are logically matched
at the endpoints, they all go through a single hub instance
on the side of the boundary that has the matched ports, and so they do not obey the
simple rules for matching given here.
When a connection goes to or from a matched port,
we say that it is *match constrained*.
Ordinarily a match constrained connection must obey the
rules for matching stated above.
To relax the rules, you can write an *unmatched* connection.
To do this, write the keyword `unmatched` at the start of the connection
specifier.
Here is an example:
[source,fpp]
--------
Port P
passive component Source {
sync input port pIn: [2] P
output port pOut: [2] P
match pOut with pIn
}
passive component Target {
sync input port pIn: [2] P
output port pOut: [2] P
}
instance source: Source base id 0x100
instance target: Target base id 0x200
topology T {
instance source
instance target
connections C {
unmatched source.pOut[0] -> target.pIn[0]
unmatched target.pOut[0] -> source.pIn[0]
unmatched source.pOut[1] -> target.pIn[1]
unmatched target.pOut[1] -> source.pIn[1]
}
}
--------
In this example, there are two pairs of connections between the
`pIn` and `pOut` connections of the instances `source` and `target`.
The ports of `source` are match constrained, so ordinarily
the connections would need to obey the matching rules.
The connections do partially obey the rules: for example,
there are no duplicate numbers, and the numbers match.
However, both pairs of connections go to and from the same
instance `target`; ordinarily this is not allowed for
match constrained connections.
To allow it, we need to use unmatched ports as shown.
Note the following about using unmatched ports:
. When connections are marked `unmatched`, the analyzer cannot check that the
port numbers assigned to the connections conform to any particular pattern.
If you need the port numbers to follow a pattern, as in the example shown
above, then you must use explicit numbering.
For a suggestion on how to do this, see the discussion of manual matching
below.
. Unmatched ports must still obey the rule that distinct
connections at a matched port must have distinct port numbers.
. The `unmatched` keyword is allowed only for connections that
are match constrained, i.e., that go to or from a matched port.
If you try to write an unmatched connection and the connection
is not match constrained, then you will get an error.
*Manual matching:*
Port matching specifiers work well when each matched pair of connections
goes between the same two components, one of which
has a matched pair of ports.
If the matching does not follow this pattern, then automatic matched
numbering will not work, and it is usually better not to
use a port matching specifier at all.
Instead, you can use explicit port numbers to express the matching.
For example, the Ref topology contains these connections:
[source,fpp]
--------
comm.allocate -> staticMemory.bufferAllocate[Ports.StaticMemory.uplink]
comm.$recv -> uplink.framedIn
uplink.framedDeallocate -> staticMemory.bufferDeallocate[Ports.StaticMemory.uplink]
--------
In this case the `staticMemory` instance requires that pairs of
allocation and deallocation requests for the same memory
go to the same port.
But the allocation request comes from `comm`,
and the deallocation request comes from `uplink`.
Since the allocation and deallocation connections go to different
component instances, we can't used automatic matched numbering.
Instead we define a symbolic constant `Ports.StaticMemory.uplink`
and use that twice to do the matching by hand.
==== General Numbering
After resolving
<<Defining-Topologies_Port-Numbering_Explicit-Numbering,
explicit numbering>> and
<<Defining-Topologies_Port-Numbering_Matched-Numbering,
matched numbering>>,
the FPP translator applies
*general numbering*.
In this step, the translator uses the following algorithm to
fill in any remaining unassigned
port numbers:
. Traverse the connections in a deterministic order.
The order is fully described in _The FPP Language Specification_.
. For each connection
.. If the output port number is unassigned, then set it to the
lowest available port number.
.. If the input port number is unassigned, then set it to zero.
For example, consider the following connections:
[source,fpp]
--------
a.p -> b.p
a.p -> c.p
--------
After general numbering, the connections could be numbered
as follows:
[source,fpp]
--------
a.p[0] -> b.p[0]
a.p[1] -> c.p[0]
--------
=== Importing Topologies
It is often useful to decompose a flight software project
into several topologies.
For example, a project might have the following topologies:
. A topology for command and data handling (CDH) with
components such as a command dispatcher, an event logger, a telemetry data
base,
a parameter database, and components for managing files.
. Various subsystem topologies, for example power, thermal,
attitude control, etc.
. A release topology.
Each of the subsystem topologies might include the CDH topology.
The release topology might include the CDH topology
and each of the subsystem topologies.
Further, to enable modular testing, it is useful for
each topology to be able to run on its own.
In FPP, the way we accomplish these goals is to *import*
one topology into another one.
In this section of the User Guide, we explain how to do that.
==== Importing Instances and Connections
To import a topology `A` into a topology `B`, you write
`import A` inside topology `B`, like this:
[source,fpp]
--------
topology B {
import A
...
}
--------
You may add instances and connections as usual to `B`, as shown
by the dots.
When you do this, the FPP translator does the following:
. *Resolve `A`:* Resolve all pattern graph
specifiers in `A`, and resolve all explicit port numbers in `A`.
Call the resulting topology `T`.
. *Form the instances of `B`:*
Take the union of the instances specified in `T` and
the instances specified in `B`, counting any duplicates once.
These are the instances of `B`.
. *Form the connections of `B`:*
Take the union of the connection graphs specified in `T` and
the connection graphs specified in `B`.
If each of `T` and `B` has a connection between the same
ports, then each becomes a separate connection in `B`.
. *Resolve `B`:* Resolve the pattern graph specifies of `B`.
Apply matched numbering and general numbering to `B`.
For example, suppose topologies `A` and `B` are defined
as follows:
[source,fpp]
--------
topology A {
instance a
instance b
connections C1 {
a.p1 -> b.p
}
}
topology B {
import A
instance c
connections C1 {
a.p1 -> c.p
}
connections C2 {
a.p2 -> c.p
}
}
--------
After import resolution, `B` is equivalent to this topology:
[source,fpp]
--------
topology B {
instance a
instance b
instance c
connections C1 {
a.p1 -> b.p
a.p1 -> c.p
}
connections C2 {
a.p2 -> c.p
}
}
--------
Notice that the `C1` connections of `A` are merged with the `C1`
connections of `B`.
==== Multiple Imports
Multiple imports are allowed.
For example:
[source,fpp]
--------
topology A {
import B
import C
...
}
--------
This has the obvious meaning: both topology `B` and
topology `C` are imported into topology `A`, according
to the rules described above.
Each topology may appear at most once in the import list.
For example, this is incorrect:
[source,fpp]
--------
topology A {
import B
import B # Error: B imported twice
}
--------
==== Transitive Imports
In general, transitive imports are allowed.
For example, topology `A` may import topology `B`,
and topology `B` may import topology `C`.
Resolution works bottom-up on the import graph:
for example, first we resolve `C`, and then we resolve `B`,
and then we resolve `A`.
Cycles in the import graph are not allowed.
For example, if `A` imports `B` and `B` imports `C`
and `C` imports `A`, you will get an error.
=== Topology Ports
In the previous section, we explained the most basic way to import a topology
`B` into a topology `A`.
This method of importing topologies erases or flattens the structure implied
by the separate topologies `A` and `B`.
In particular, when `B` is imported into `A`,
* All component instances that are part of `B` (directly or by import) are visible in `A`.
* Connections that span `A` and `B` go directly between
component instances that are part of `A` and component instances that are
part of `B`.
This approach is simple and flexible.
It also aligns well with the {cpp} implementation, which is flattened in this
way.
However, when specifying connections in the model,
it is often useful to preserve the separation between
an importing topology `A` and an imported topology `B`.
To do that, FPP has a feature called *topology ports*,
which we explain in this section.
With topology ports, the code generator still flattens the
hierarchy of imported topologies into a single topology, as
described in the previous section.
However, before this flattening occurs, you can specify
connections _to and from subtopologies_.
For example, you can specify a connection from a component instance
`a` of `A` to a _port_ of topology `B`, and this connection is automatically
flattened to a connection between `a` and a component instance `b` of `B`.
In this way, you can think of imported topologies as
units of function that have their own interfaces and connections.
==== Specifying Topology Ports
*Connections to subtopologies:*
Consider the following example:
[source,fpp]
--------
topology A {
instance a
import B
connections C {
a.p -> b.p
}
}
topology B {
instance b
}
--------
As discussed in the previous section, import resolution produces this
flattened topology:
[source,fpp]
--------
topology A {
instance a
instance b
connections C {
a.p -> b.p
}
}
--------
Notice that in both the original topology and the flattened topology,
the connection goes directly from `a.p` to `b.p`.
Now let us rewrite this example using topology ports.
First, we need a way to specify a port of topology `B` and to map it to
a port of an instance of `B`, so the flattening can occur.
To do that, we write `port` followed by a name, and equals sign,
and a port of a component instance, like this:
[source,fpp]
--------
topology B {
instance b
port p = b.p
}
--------
Here the FPP analyzer checks that `b.p` is a port of instance `b`;
if not, an error will occur.
It is also an error to specify multiple ports with
the same name in the same topology.
Now in topology `A`, we can write a connection to port `B.p` instead
of directly to component instance `b.p`, like this:
[source,fpp]
--------
topology A {
instance a
import B
connections C {
a.p -> B.p
}
}
--------
As usual, the FPP analyzer checks that `a.p` is an output port,
that `B.p` is a input port, and that the types of the ports
are compatible.
Here `B.p` has the direction and type of `b.p`, the actual port to which the
topology port `B.p` is mapped.
If these requirements aren't met, then an error occurs.
After import resolution, the result is the same as before:
`A` has two instances `a` and `b` and one connection
from `a.p` to `b.p`.
However, the source model respects the hierarchical structure
of `A` and `B`:
the connection goes to the interface
of `B`, instead of going directly to an instance of `B`.
*Connections from subtopologies:*
Connections from subtopologies work in the same way,
with the arrow reversed.
For example, if we assume that `B.p` is an output port
and `a.p` is an input port, then we could write this:
[source,fpp]
--------
topology A {
instance a
import B
connections C {
B.p -> a.p
}
}
--------
*Connections between subtopologies:*
Another useful pattern is to have a topology `A` import
two subtopologies `B1` and `B2` and to specify a connection
from `B1` to `B2`.
Here is an example:
[source,fpp]
--------
topology B1 {
instance b1
port p = b1.p
}
topology B2 {
instance b2
port p = b2.p
}
topology A {
import B1
import B2
connections C {
B1.p -> B2.p
}
}
--------
After flattening, topology `A` looks like this:
[source,fpp]
--------
topology A {
instance b1
instance b2
connections {
b1.p -> b2.p
}
}
--------
==== Implementing Port Interfaces
A topology is like a component instance, in that it provides
a port interface.
We will have more to say about this similarity in the
<<Defining-Topologies_Topology-Ports_Port-Interface-Instances,
next section>>.
In the case of an instance of a component _C_, we can
<<Defining-and-Using-Port-Interfaces_Defining-Port-Interfaces,
define a port interface _I_>> and make _I_ part of the
interface of _C_.
We do that by
<<Defining-and-Using-Port-Interfaces_Using-Port-Interfaces-in-Component-Definitions,
importing>> _I_ into the definition of _C_.
In the case of a topology _T_, we can't import an interface _I_ in this way,
because we have to specify a mapping for each of the ports
in the interface of _T_ to a port of a component instance or an imported
topology of _T_.
However, we can use one or more port interface definitions to specify and check
that the actual interface of _T_ provides all the ports that it should.
To do this, we write the keyword `implements` followed by list of names
that refer to port interface definitions.
The FPP analyzer will then check that the topology provides
all the ports required by each of the interfaces in the definitions.
Here is an example:
[source,fpp]
----
port P
interface I {
sync input port p: P
}
passive component C {
import I
}
instance a: C base id 0x100
@ Specify and check that topology T provides the ports of interface I
topology T implements I {
instance a
port p = a.p
}
----
This code is correct because topology `T` provides the port `p` of interface `I`,
via the line `port p = a.p`.
An error would result in each of the following cases:
* We delete `port p = a.p` from the model.
In this case, the `implements` clause is violated, because
topology `T` provides no port `p`.
* We remove `import I` from the definition of `C` and replace it with `output port p: P`.
In this case, topology `T` does provide a port `p`, but the port does
not conform to the interface `I`.
Try these examples in `fpp-check`, and make sure you understand what is
happening in each case.
You can also write more than one interface name in an `implements` clause.
In general the interface names form an
<<Defining-Constants_Multiple-Definitions-and-Element-Sequences,element
sequence>>
in which the optional terminating punctuation is a comma.
For example:
[source,fpp]
----
port P
port Q
interface I {
sync input port p: P
}
interface J {
output port q: Q
}
passive component C {
import I
import J
}
instance a: C base id 0x100
@ Specify and check that topology T provides the ports of interfaces I and J
topology T implements I, J {
instance a
port p = a.p
port q = a.q
}
----
Implements clauses are not strictly required: any correct model
containing an implements clause will also be correct if
that clause is deleted.
However, implements clauses are useful for documenting and checking intended
behavior.
For example, suppose the intent of topology `T` in the previous example
is to provide interfaces `I` and `J`.
If there were no `implements` clause,
then a developer could inadvertently change the contract of `T` by removing or
modifying ports `p` or `q` of `T`.
Because of the implements clause, this can't happen.
Instead an error will occur,
alerting the developer to the fact that an interface contract is
being changed.
Finally, an implements clause does not require that a topology implement
_only_ what is in the clause.
For example, in the code shown above, we could add any number
of ports to `T` that are not specified in the interfaces `I` and `J`.
The only requirement is that all the ports specified in `I` and `J`
are present in the interface.
==== Port Interface Instances
As noted in the previous section, both topologies and component
instances provide port interfaces.
Therefore we refer to both topologies and component instances as *port
interface instances*.
The motivation for this name is as follows:
* A port interface instance is a concrete implementation (an instance)
of a unit of function that has a port interface.
* The units of function with port interfaces are component
instances and topologies.
Thus the concept of a port interface instance generalizes
the concept of a component instance in a way that includes
the concept of a topology.
The concept of a port interface instance will become more obviously useful
in a future version of FPP, where we will introduce the following
features:
* *Module templates* that you can expand several times to generate
different sections of of FPP code (for example, different topologies) that are
largely the same, but that differ in particular ways that you specify.
* *Template parameters* that let you bind concrete values to parameters
when instantiating templates.
When instantiating templates, it will be useful to treat component
instances and topologies in the same way, so that a single `instance`
parameter can be bound to either a component instance or a topology,
as long as the constraints of the parameter are satisfied.
For now, we note that the concept of a port interface instance exists.
We also note that imported topologies are port interface instances.
For this reason, you can use the keyword `instance` to import a topology.
For example, instead of writing `import A` to import topology `A`,
you can write `instance A`.
Here is one of the FPP models from
<<Defining-Topologies_Topology-Ports_Specifying-Topology-Ports,the section on
specifying topology ports>>,
written using `instance` instead of `import`:
[source,fpp]
--------
topology B {
instance b
port p = b.p
}
topology A {
instance a
# Here we write instance B to import topology B into A
# Previously we wrote import B
# Both are valid syntax as of FPP v3.2.0
instance B
connections C {
a.p -> B.p
}
}
--------
Notice how this syntax emphasizes that both the component instance `a`
and the imported topology `B` provide port interfaces
that can participate in connections.
The concept of a port interface instance is new as of v3.2.0 of FPP.
In previous versions, there were no topology ports, so topologies
were not port interface instances.
Further, the only way to import a topology was to
use the `import` keyword, as discussed in the previous sections.
For backwards compatibility, we do not require you to use `instance`
when importing a topology; you may still use `import`.
However, it may be useful to use `instance` in cases where you specify
connections to and from the ports of an imported topology `T`, without
specifying any connections directly to or from the instances
of `T`.
In this case, you may wish to think of `T` as a pure port interface,
with the details of its implementation (its component instances,
imported subtopologies, and connections) abstracted away.
=== Include Specifiers
You can include code from another file in a topology definition.
You do this by writing an *include specifier*.
Here is a simple example, in which the body of a topology
definition is specified in a file `T.fppi` and then included
in a topology definition:
[source,fpp]
--------
topology T {
include "T.fppi"
}
--------
We will explain more about this feature in the section on
<<Specifying-Models-as-Files_Include-Specifiers,include specifiers>>
below.
=== Telemetry Packets
When defining a topology, you may (but are not required to)
specify how the <<Defining-Components_Telemetry,telemetry channels>>
defined in the topology are grouped into **telemetry packets**.
If you do this, then you can use an F Prime component called
the Telemetry Packetizer to construct telemetry packets and transmit
them to the ground.
This approach is usually more space efficient than transmitting
individual channels.
In this section we explain how to specify telemetry packets in FPP.
==== Telemetry Packet Sets
To group the channels of a topology _T_ into packets, you write a
**telemetry packet set** as part of the definition of _T_.
Here is a simple example showing a complete topology with a
telemetry packet set:
[source,fpp]
----
passive component C {
telemetry port tlmOut
time get port timeGetOut
telemetry T1: U32
telemetry T2: F32
}
instance c1: C base id 0x100
instance c2: C base id 0x200
topology T {
instance c1
instance c2
@ Telemetry packet set PacketSet
telemetry packets PacketSet {
@ Packet P1
packet P1 group 0 {
c1.T1
c2.T1
c1.T1
}
@ Packet P2
packet P2 group 1 {
c1.T2
c2.T2
}
}
}
----
In this example, we have defined a passive component `C` with two telemetry
channels, `T1` and `T2`.
We have defined two instances of `C`, `c1` and `c2`, so that there
are four telemetry channels overall: `c1.T1`, `c1.T2`, `c2.T1`, and `c2.T2`.
We have defined a topology `T` that uses the instances `c1` and `c2`
and defines a telemetry packet set `PacketSet`.
(In a realistic topology, there would be other instances and connections,
for example the telemetry packetizer instance and the ground interface
component. Here we have omitted these instances
and connections to focus on illustrating the FPP features.)
Notice the following about this example:
. To write a telemetry packet set, you write the keywords `telemetry` `packets`
followed by the name of the packet set, here `PacketSet`.
Then you write the body of the telemetry packet set inside curly braces
`{ ... }`.
. In the body of the packet set, you specify packets.
The packet specifiers are
<<Writing-Comments-and-Annotations_Annotations,annotatable elements>>.
The body of the packet set is an
<<Defining-Constants_Multiple-Definitions-and-Element-Sequences,
element sequence>> in which the optional
terminating punctuation is a comma.
. Each packet specifier consists of the keyword `packet`, the packet
name, the keyword `group`, a group identifier, and the body of the packet.
Within a telemetry packet set, the packet names must be distinct.
The group identifiers must be numeric values.
In particular they can be
<<Defining-Constants_Writing-a-Constant-Definition,defined constants>>,
<<Defining-Enums,enumerated constants>>, or
<<Defining-Constants_Expressions_Value-Arithmetic-Expressions,
arithmetic expressions>>.
The Telemetry Packetizer uses the group identifiers to select
which packets get transmitted.
For example, it is possible to transmit packets from group 0 but not group 1.
More than one packet may have the same group identifier.
. Inside each packet, you specify telemetry channels
enclosed in curly braces `{ ... }`.
The channel specifiers are
<<Writing-Comments-and-Annotations_Annotations,annotatable elements>>.
The body of the packet is an
<<Defining-Constants_Multiple-Definitions-and-Element-Sequences,
element sequence>> in which the optional
terminating punctuation is a comma.
When the packets are serialized and transmitted, each packet with name
_P_ contains data from
the channels listed in the specification for _P_, in the order that
the channels are specified.
As shown for packet `P1`, a single channel may appear more than
once in a packet; that means that data from that channel appears at
two or more offsets in that packet.
The form of a telemetry channel specifier
is an instance name followed by a dot and a channel name.
The instance name must refer to an instance of the topology.
The channel name must refer to a channel of that instance.
For example, in the model above, `c1.T` is a valid channel
for topology `T` because `c1` is an instance of `T`,
`c1` has type `C`, and `T` is a channel of `C`.
The instance name may be a qualified name containing a dot,
as discussed in
the section on <<Defining-Modules,defining modules>>.
For example, if we had written
[source,fpp]
--------
module M {
instance c1: C base id 0x100
}
--------
instead of
[source,fpp]
--------
instance c1: C base id 0x100
--------
to define instance `c1` in the model shown above,
then in the topology `T` we would write
`instance M.c1` instead of `instance c`, and we would write
channel `M.c1.T` instead of channel `c1.T`.
==== Telemetry Packet Identifiers
Within a telemetry packet set, every packet has a unique numeric identifier.
Typically the identifiers start at zero and count up by one.
If you don't specify an identifier, as in the example shown in the previous
section, then FPP assigns a default identifier: zero for the first packet in the set;
otherwise one more than the identifier of the previous packet.
If you wish, you may explicitly specify one or more packet identifiers. To do
this, you write the keyword `id` followed by a numeric expression immediately
after the packet name.
For example, in the model shown in the previous section, we
could have written
[source,fpp]
--------
@ Packet P1
packet P1 id 0 group 0 {
c1.T1
c2.T1
c1.T1
}
@ Packet P2
packet P2 id 1 group 1 {
c1.T2
c2.T2
}
--------
for the packet specifiers, and this would have equivalent behavior.
==== Omitting Channels
By default, every telemetry channel in the topology must appear
in at least one telemetry packet.
For example, in the model shown above, if we comment out the definition
of packet `P2` and run the result through `fpp-check`, then an error will result,
because neither channel
`c1.T2` nor channel `c2.T2` appears in any packet.
The purpose of this behavior is to ensure that you don't omit
any channels from the telemetry by mistake.
If you do want to omit channels from the telemetry packets,
then you can do so explicitly by writing the keyword `omit` and listing
those channels after the main specification of the packet set.
Here is an example:
[source,fpp]
----
passive component C {
telemetry port tlmOut
time get port timeGetOut
telemetry T1: U32
telemetry T2: F32
}
instance c1: C base id 0x100
instance c2: C base id 0x200
topology T {
instance c1
instance c2
@ Telemetry packet set PacketSet
telemetry packets PacketSet {
@ Packet P1
packet P1 group 0 {
c1.T1
c2.T1
c1.T1
}
} omit {
c1.T2
c2.T2
}
}
----
The form of the list of omitted channels is the same as the
form of the list of channels in a telemetry packet.
It is an error for any channel to appear in a telemetry
packet and also to appear in the omitted list.
==== Specifying Multiple Telemetry Packet Sets
You may specify more than one telemetry packet set in a topology.
Each telemetry packet set must have a distinct name.
For example:
[source,fpp]
----
passive component C {
telemetry port tlmOut
time get port timeGetOut
telemetry T: U32
}
instance c1: C base id 0x100
instance c2: C base id 0x200
topology T {
instance c1
instance c2
@ Telemetry packet set PacketSet1
telemetry packets PacketSet1 {
@ Packet P
packet P1 group 0 {
c1.T
}
} omit {
c2.T
}
@ Telemetry packet set PacketSet2
telemetry packets PacketSet2 {
@ Packet P
packet P1 group 0 {
c2.T
}
} omit {
c1.T
}
}
----
When you run the F Prime ground data system, you can tell it which
telemetry packet set to use to decode the packets.
==== Include Specifiers
When writing a telemetry packet set, you can
<<Defining-Topologies_Include-Specifiers,include code
from another file>> in the following places:
. You can include code inside a packet set specifier.
The code must contain packet specifiers.
. You can include code inside a packet specifier.
The code must list telemetry channels.
For example:
[source,fpp]
--------
telemetry packets PacketSet {
@ Include some packets
include "packets.fppi"
packet P group 0 {
c1.T1
# Include some channels
include "channels.fppi"
}
}
--------
The files that you include can themselves include code
from other files, if you wish.