1. Introduction
This document describes F Prime Prime, also known as FPP or F Double Prime. FPP is a modeling language for the F Prime flight software framework. A paper presented at SmallSat 2018 provides an overview of F Prime. For more detailed information about F Prime, see the F Prime User’s Guide.
The goals of FPP are as follows:
-
To provide a modeling language for F Prime that is simple, easy to use, and well-tailored to its purpose.
-
To provide semantic checking and error reporting for F Prime models.
-
To generate code in the various languages that F Prime uses, e.g., C++, Python, XML. In this document, we will call these languages the target languages.
Developers may combine code generated from FPP with code written by hand to create, e.g., deployable FSW programs and ground data environments.
The name "F Double Prime" (or F'') deliberately suggests the idea of a "derivative" of F Prime (or F'). By "integrating" an FPP model (i.e., running the tools) you get a partial FSW implementation in the F Prime framework; and then by "integrating" again (i.e., providing the project-specific C++ implementation) you get a FSW application.
Purpose: The purpose of this document is to describe FPP in a way that is accessible to users, including beginning users. A more detailed and precise description is available in The FPP Language Specification. We recommend that you read this document before consulting that one.
XML limitations: Currently we translate FPP to F Prime XML and then to C++. Some features of FPP are not supported in the XML representation. Where a feature is not supported in XML, we will call out this fact in a note marked XML limitation. As we continue to develop FPP, we will phase in support for all the features described here.
GDS limitations: It is currently possible to construct FPP models that are not fully supported by the F Prime Ground Data System (GDS). Where an FPP feature is not supported in the GDS, we will call out this fact in a note marked GDS limitation.
Overview: The rest of this document proceeds as follows. Section 2 explains how to get up and running with FPP. Sections 3 through 11 describe the elements of an FPP model, starting with the simplest elements (constants and types) and working towards the most complex (components and topologies). Section 12 explains how to specify a model as a collection of files: for example, it covers the management of dependencies between files. Section 13 explains how to analyze FPP models and how to translate FPP models to XML, to C++, and from XML. Section 14 explains how to write a C++ implementation against the code generated from an FPP model.
2. Installing FPP
Before reading the rest of this document, you should install the latest version of FPP. The installation instructions are available here:
Make sure that the FPP command-line tools are in your shell path.
For example, running fpp-check
on the command line should succeed and should
prompt for standard input. You can type control-C to end
the program:
% fpp-check ^C %
fpp-check
is the tool for checking that an FPP model is valid.
Like most FPP tools (except the ones that operate on XML files — more on this
below),
fpp-check
reads either from named files or from standard input.
If one or more files are named on the command line, fpp-check
reads those;
otherwise it reads from standard input.
As an example, the following two operations are equivalent:
% fpp-check < file.fpp % fpp-check file.fpp
The first operation redirects file.fpp
into the standard input of
fpp-check
.
The second operation names file.fpp
as an input file of fpp-check
.
Most of the examples in the following sections are complete FPP models.
You can run the models through
fpp-check
by typing or pasting them into a file or into standard input.
We recommend that you to this for at least a few of the examples,
to get a feel for how FPP works.
3. Defining Constants
The simplest FPP model consists of one or more constant definitions. A constant definition associates a name with a value, so that elsewhere you can use the name instead of re-computing or restating the value. Using named constants makes the model easier to understand (the name says what the value means) and to maintain (changing a constant definition is easy; changing all and only the relevant uses of a repeated value is not).
This section covers the following topics:
-
Writing an FPP constant definition.
-
Writing an expression, which is the source text that defines the value associated with the constant definition.
-
Writing multiple constant definitions.
-
Writing a constant definition that spans two or more lines of source text.
3.1. Writing a Constant Definition
To write a constant definition, you write the keyword constant
,
an equals sign, and an expression.
A later section
describes all the expressions you can write.
Here is an example that uses an integer literal expression representing
the value 42:
constant ultimateAnswer = 42
This definition associates the name ultimateAnswer
with the value 42.
Elsewhere in the FPP model you can use the name ultimateAnswer
to represent
the value.
You can also generate a C++ header file that defines the C++ constant
ultimateAnswer
and gives it the value 42.
As an example, do the following:
-
On the command line, run
fpp-check
. -
When the prompt appears, type the text shown above, type return, type control-D, and type return.
You should see something like the following on your console:
% fpp-check constant ultimateAnswer = 42 ^D %
As an example of an incorrect model that produces an error message, repeat the exercise, but omit the value 42. You should see something like this:
% fpp-check constant ultimateAnswer = ^D fpp-check stdin: end of input error: expression expected
Here the fpp-check
tool is telling you that it could not parse the input:
the input ended where it expected an expression.
3.2. Names
Names in FPP follow the usual rules for identifiers in a programming language:
-
A name must contain at least one character.
-
A name must start with a letter or underscore character.
-
The characters after the first may be letters, numbers, or underscores.
For example:
-
name
,Name
,_name
, andname1
are valid names. -
1invalid
is not a valid name, because names may not start with digits.
3.2.1. Reserved Words
Certain sequences of letters such as constant
are called out as reserved
words (also called keywords) in FPP.
Each reserved word has a special meaning, such as introducing a constant
declaration.
The FPP Language Specification has a complete list of reserved words.
In this document, we will introduce reserved words as needed to explain
the language features.
Using a reserved word as a name in the ordinary way causes a parsing error. For example, this code is incorrect:
constant constant = 0
To use a reserved word as a name, you must put the character $
in
front of it with no space.
For example, this code is legal:
constant $constant = 0
The character sequence $constant
represents the name constant
,
as opposed to the keyword constant
.
You can put the character $
in front of any identifier,
not just a keyword.
If the identifier is not a keyword, then the $
has no effect.
For example, $name
has the same meaning as name
.
3.2.2. Name Clashes
FPP will not let you define two different symbols of the same kind with the same name. For example, this code will produce an error:
constant c = 0
constant c = 1
Two symbols can have the same unqualified name if they reside in different modules or enums; these concepts are explained below. Two symbols can also have the same name if the analyzer can distinguish them based on their kinds. For example, an array type (described below) and a constant can have the same name, but an array type and a struct type may not. The FPP Language Specification has all the details.
3.3. Expressions
This section describes the expressions that you can write as part of a constant definition. Expressions appear in other FPP elements as well, so we will refer back to this section in later sections of the manual.
3.3.1. Primitive Values
A primitive value expression represents a primitive machine value, such as an integer. It is one of the following:
-
A decimal integer literal value such as
1234
. -
A hexadecimal integer literal value such as
0xABCD
or0xabcd
. -
A floating-point literal value such as
12.34
or1234e-2
. -
A Boolean literal expression
true
orfalse
.
As an exercise, construct some constant definitions with primitive values as their
expressions, and
feed the results to fpp-check
.
For example:
constant a = 1234
constant b = 0xABCD
If you get an error, make sure you understand why.
3.3.2. String Values
A string value represents a string of characters. There are two kinds of string values: single-line strings and multiline strings.
Single-line strings:
A single-line string represents a string of characters
that does not contain a newline character.
It is written as a string of characters enclosed in double quotation
marks "
.
For example:
constant s = "This is a string."
To put the double-quote character in a string, write the double quote
character as \"
, like this:
constant s = "\"This is a quotation within a string,\" he said."
To encode the character \
followed by the character "
, write
the backslash character as \\
, like this:
constant s = "\\\""
This string represents the literal character sequence \
, "
.
In general, the sequence \
followed by a character c
is translated to c.
This sequence is called an escape sequence.
Multiline strings:
A multiline string represents a string of characters
that may contain a newline character.
It is enclosed in a pair of sequences of three double quotation
marks """
.
For example:
constant s = """
This is a multiline string.
It has three lines.
"""
When interpreting a multiline string, FPP ignores any newline
characters at the start and end of the string.
FPP also ignores any blanks to the left of the column where
the first """
appears.
For example, the string shown above consists of three lines and starts
with This
.
Literal quotation marks are allowed inside a multiline string:
constant s = """
"This is a quotation within a string," he said.
"""
Escape sequences work as for single-line strings. For example:
constant s = """
Here are three double-quote characters in a row: \"\"\"
"""
3.3.3. Array Values
An array value expression represents a fixed-size array of values. To write an array value expression, you write a comma-separated list of one or more values (the array elements) enclosed in square brackets. Here is an example:
constant a = [ 1, 2, 3 ]
This code associates the name a
with the array of
integers
[ 1, 2, 3 ]
.
As mentioned in the introduction, an FPP model describes the structure of a FSW application; the computations are specified in a target language such as C++. As a result, FPP does not provide an array indexing operation. In particular, it does not specify the index of the leftmost array element; that is up to the target language. For example, if the target language is C++, then array indices start at zero.
Here are some rules for writing array values:
-
An array value must have at least one element. That is,
[]
is not a valid array value. -
An array value may have at most 256 elements.
-
The types of the elements must match. For example, the following code is illegal, because the value
1
(which has typeInteger
) and the value"abcd"
(which has typestring
) are incompatible:constant mismatch = [ 1, "abcd" ]
Try entering this example into
fpp-check
and see what happens.
What does it mean for types to match? The FPP Specification has all the details, and we won’t attempt to repeat them here. In general, things work as you would expect: for example, we can convert an integer value to a floating-point value, so the following code is allowed:
constant a = [ 1, 2.0 ]
It evaluates to an array of two floating-point values.
If you are not sure whether a type conversion is allowed, you can
ask fpp-check
.
For example: can we convert a Boolean value to an integer value?
In older languages like C and C++ we can, but in many newer languages
we can’t. Here is the answer in FPP:
% fpp-check constant a = [ 1, true ] ^D fpp-check stdin: 1.16 constant a = [ 1, true ] ^ error: cannot compute common type of Integer and bool
So no, we can’t.
Here are two more points about array values:
-
Any legal value can be an element of an array value, so in particular arrays of arrays are allowed. For example, this code is allowed:
constant a = [ [ 1, 2 ], [ 3, 4 ] ]
It represents an array with two elements: the array
[ 1, 2 ]
and the array[ 3, 4 ]
. -
To avoid repeating values, a numeric, string, or Boolean value is automatically promoted to an array of appropriate size whenever necessary to make the types work. For example, this code is allowed:
constant a = [ [ 1, 2, 3 ], 0 ]
It is equivalent to this:
constant a = [ [ 1, 2, 3 ], [ 0, 0, 0 ] ]
3.3.4. Struct Values
A struct value expression represents a C- or C++-style structure, i.e., a mapping of names to values. To write a struct value expression, you write a comma-separated list of zero or more struct members enclosed in curly braces. A struct member consists of a name, an equals sign, and a value.
Here is an example:
constant s = { x = 1, y = "abc" }
This code associates the name s
with a struct value.
The struct value has two members x
and y
.
Member x
has the integer value 1, and member y
has the string value "abc"
.
The order of members: When writing a struct value, the order in which the
members appear does not matter.
For example, in the following code, constants s1
and s2
denote the same
value:
constant s1 = { x = 1, y = "abc" }
constant s2 = { y = "abc", x = 1 }
The empty struct: The empty struct is allowed:
constant s = {}
Arrays in structs: You can write an array value as a member of a struct value. For example, this code is allowed:
constant s = { x = 1, y = [ 2, 3 ] }
Structs in arrays: You can write a struct value as a member of an array value. For example, this code is allowed:
constant a = [ { x = 1, y = 2 }, { x = 3, y = 4 } ]
This code is not allowed, because the element types don’t match — an array is not compatible with a struct.
constant a = [ { x = 1, y = 2 }, [ 3, 4 ] ]
However, this code is allowed:
constant a = [ { x = 1, y = 2 }, { x = 3 } ]
Notice that the first member of a
is a struct with two members x
and y
.
The second member of a
is also a struct, but it has only one member x
.
When the FPP analyzer detects that a struct type is missing a member,
it automatically adds the member, giving it a default value.
The default values are the ones you would expect: zero for numeric members, the empty
string for string members, and false
for Boolean members.
So the code above is equivalent to the following:
constant a = [ { x = 1, y = 2 }, { x = 3, y = 0 } ]
3.3.5. Name Expressions
A name expression is a use of a name appearing in a constant definition. It stands for the associated constant value. For example:
constant a = 1
constant b = a
In this code, constant b
has the value 1.
The order of definitions does not matter, so this code is equivalent:
constant b = a
constant a = 1
The only requirement is that there may not be any cycles in the graph
consisting of constant definitions and their uses.
For example, this code is illegal, because there is a cycle from a
to b
to
c
and back to a
:
constant a = c
constant b = a
constant c = b
Try submitting this code to fpp-check
, to see what happens.
Names like a
, b
, and c
are simple or unqualified names.
Names can also be qualified: for example A.a
is allowed.
We will discuss qualified names further when we introduce
module definitions and enum definitions below.
3.3.6. Value Arithmetic Expressions
A value arithmetic expression performs arithmetic on values. It is one of the following:
-
A negation expression, for example:
constant a = -1
-
A binary operation expression, where the binary operation is one of
+
(addition),-
(subtraction),*
(multiplication), and/
(division). For example:constant a = 1 + 2
-
A parenthesis expression, for example:
constant a = (1)
The following rules apply to arithmetic expressions:
-
The subexpressions must be integer or floating-point values.
-
If there are any floating-point subexpressions, then the entire expression is evaluated using 64-bit floating-point arithmetic.
-
Otherwise the expression is evaluated using arbitrary-precision integer arithmetic.
-
In a division operation, the second operand may not be zero or (for floating-point values) very close to zero.
3.3.7. Compound Expressions
Wherever you can write a value inside an expression, you can write a more complex expression there, so long as the types work out. For example, these expressions are valid:
constant a = (1 + 2) * 3
constant b = [ 1 + 2, 3 ]
The first example is a binary expression whose first operand is a parentheses expression; that parentheses expression in turn has a binary expression as its subexpression. The second example is an array expression whose first element is a binary expression.
This expression is invalid, because 1 + 2.0
evaluates to a floating-point
value, which is incompatible with type string
:
constant a = [ 1 + 2.0, "abc" ]
Compound expressions are evaluated in the obvious way. For example, the constant definitions above are equivalent to the following:
constant a = 9
constant b = [ 3, 3 ]
For compound arithmetic expressions, the precedence and associativity rules are the usual ones (evaluate parentheses first, then multiplication, and so forth).
3.4. Multiple Definitions and Element Sequences
Typically you want to specify several definitions in a model source file, not just one. There are two ways to do this:
-
You can separate the definitions by one or more newlines, as shown in the examples above.
-
You can put the definitions on the same line, separated by a semicolon.
For example, the following two code excerpts are equivalent:
constant a = 1
constant b = 2
constant a = 1; constant b = 2
More generally, a collection of several constant definitions is an example of an element sequence, i.e., a sequence of similar syntactic elements. Here are the rules for writing an element sequence:
-
Every kind of element sequence has optional terminating punctuation. The terminating punctuation is either a semicolon or a comma, depending on the kind of element sequence. For constant definitions, it is a semicolon.
-
When writing elements on separate lines, the terminating punctuation is optional.
-
When writing two or more elements on the same line, the terminating punctuation is required between the elements and optional after the last element.
3.5. Multiline Definitions
Sometimes, especially for long definitions, it is useful to split a definition across two or more lines. In FPP there are several ways to do this.
First, FPP ignores newlines that follow opening symbols like [
and precede
closing symbols like ]
.
For example, this code is allowed:
constant a = [
1, 2, 3
]
Second, the elements of an array or struct form an element sequence (see the previous section), so you can write each element on its own line, omitting the commas if you wish:
constant s = {
x = 1
y = 2
z = 3
}
This is a clean way to write arrays and structs.
The assignment of each element to its own line and the lack of
terminating punctuation
make it easy to rearrange the elements.
In particular, one can do a line-by-line sort on the elements (for example, to
sort struct members alphabetically by name) without concern for messing up the
arrangement of commas.
If we assume that the example represents the first five lines of a source file,
then in vi this is easily done as :2-4!sort
.
Third, FPP ignores newlines that follow connecting symbols such as =
and +
For example, this code is allowed;
constant a =
1
constant b = 1 +
2
Finally, you can always create an explicit line continuation by escaping
one or more newline characters with \
:
constant \
a = 1
Note that in this example you need the explicit continuation, i.e., this code is not legal:
constant
a = 1
4. Writing Comments and Annotations
In FPP, you can write comments that are ignored by the parser. These are just like comments in most programming languages. You can also write annotations that have no meaning in the FPP model but are attached to model elements and may be carried through to translation — for example, they may become comments in generated C++ code.
4.1. Comments
A comment starts with the character #
and goes to the end of the line.
For example:
# This is a comment
To write a comment that spans multiple lines, start each line with #
:
# This is a comment.
# It spans two lines.
4.2. Annotations
Annotations are attached to elements of a model, such as constant definitions. A model element that may have an annotation attached to it is called an annotatable element. Any constant definition is an annotatable element. Other annotatable elements will be called out in future sections of this document.
There are two kinds of annotations: pre annotations and post annotations:
-
A pre annotation starts with the character
@
and is attached to the annotatable element that follows it. -
A post annotation starts with the characters
@<
and is attached to the annotatable element that precedes it.
In either case
-
Any whitespace immediately following the
@
or@<
characters is ignored. -
The annotation goes to the end of the line.
For example:
@ This is a pre annotation
constant c = 0 @< This is a post annotation
Multiline annotations are allowed. For example:
@ This is a pre annotation.
@ It has two lines.
constant c = 0 @< This is a post annotation.
@< It also has two lines.
The meaning of the annotations is tool-specific. A typical use is to
concatenate the pre and post annotations into a list of lines and emit them as
a comment. For example, if you send the code immediately above through the
tool fpp-to-cpp
,
it should generate a file FppConstantsAc.hpp
. If you examine that file,
you should see, in relevant part, the following code:
//! This is a pre annotation.
//! It has two lines.
//! This is a post annotation.
//! It also has two lines.
enum FppConstant_c {
c = 0
};
The two lines of the pre annotation and the two lines of the post annotation have been concatenated and written out as a Doxygen comment attached to the constant definition, represented as a C++ enum.
In the future, annotations may be used to provide additional capabilities, for example timing analysis, that are not part of the FPP language specification.
5. Defining Modules
In an FPP model, a module is a group of model elements that are all qualified with a name, called the module name. An FPP module corresponds to a namespace in C++ and a module in Python. Modules are useful for (1) organizing a large model into a hierarchy of smaller units and (2) avoiding name clashes between different units.
To define a module, you write the keyword module
followed by one
or more definitions enclosed in curly braces.
For example:
module M {
constant a = 1
}
The name of a module qualifies the names of all the definitions that the module
encloses.
To write the qualified name, you write the qualifier, a dot, and the base name:
for example M.a
. (This is also the way that
name qualification works in Python, Java, and Scala.)
Inside the module, you can use the qualified name or the unqualified
name.
Outside the module, you must use the qualified name.
For example:
module M {
constant a = 1
constant b = a # OK: refers to M.a
constant c = M.b
}
constant a = M.a
constant c = b # Error: b is not in scope here
As with namespaces in C++, you can close a module definition and reopen it later. All the definitions enclosed by the same name go in the module with that name. For example, the following code is allowed:
module M {
constant a = 0
}
module M {
constant b = 1
}
It is equivalent to this code:
module M {
constant a = 0
constant b = 1
}
You can define modules inside other modules. When you do that, the name qualification works in the obvious way. For example:
module A {
module B {
constant c = 0
}
}
constant c = A.B.c
The inside of a module definition is an element sequence with a semicolon as the optional terminating punctuation. For example, you can write this:
module M { constant a = 0; constant b = 1 }; constant c = M.a
A module definition is an annotatable element, so you can attach annotations to it, like this:
@ This is module M
module M {
constant a = 0
}
XML limitation: The F Prime XML format supports only one level of C++ namespaces. Therefore nested modules are not translated properly to C++. As a workaround, you can use an underscore prefix. For example, instead of writing
module A {
module B {
}
}
you can write
module A_B {
}
6. Defining Types
An FPP model may include one or more type definitions. These definitions describe named types that may be used elsewhere in the model and that may generate code in the target language. For example, an FPP type definition may become a class definition in C++ or Python.
There are three kinds of type definitions:
-
Array type definitions
-
Struct type definitions
-
Abstract type definitions
Type definitions may appear at the top level or inside a module definition. A type definition is an annotatable element.
6.1. Array Type Definitions
An array type definition associates a name with an array type. An array type describes the shape of an array value. It specifies an element type and a size.
6.1.1. Writing an Array Type Definition
As an example, here is an array type definition that associates
the name A
with an array of three values, each of which is a 32-bit unsigned
integer:
array A = [3] U32
In general, to write an array type definition, you write the following:
-
The keyword
array
. -
The name of the array type.
-
An equals sign
=
. -
An expression enclosed in square brackets
[
…]
denoting the size (number of elements) of the array. -
A type name denoting the element type. The available type names are discussed below.
Notice that the size expression precedes the element type, and the whole
type reads left to right.
For example, you may read the type [3] U32
as "array of 3 U32
."
The size may be any legal expression. It doesn’t have to be a literal integer. For example:
constant numElements = 10
array A = [numElements] U32
As for array values, an array type must have size greater than zero and less than or equal to 256.
6.1.2. Type Names
The following type names are available for the element types:
-
The type names
U8
,U16
,U32
, andU64
, denoting the type of unsigned integers of width 8, 16, 32, and 64 bits. -
The type names
I8
,I16
,I32
, andI64
, denoting the type of signed integers of width 8, 16, 32, and 64 bits. -
The type names
F32
andF64
, denoting the type of floating-point values of width 32 and 64 bits. -
The type name
bool
, denoting the type of Boolean values (true
andfalse
). -
The type name
string
, denoting the type of string values. This type has a default maximum size. For example:# A is an array of 3 strings with default maximum size array A = [3] string
-
The type name
string size
e, where e is a numeric expression specifying a maximum string size.# A is an array of 3 strings with maximum size 40 array A = [3] string size 40
-
A name associated with another type definition. In particular, an array definition may have another array definition as its element type; this situation is discussed further below.
An array type definition may not refer to itself (array type definitions are not recursive). For example, this definition is illegal:
array A = [3] A # Illegal: the definition of A may not refer to itself
6.1.3. Default Values
Optionally, you can specify a default value for an array type.
To do this, you write the keyword default
and an expression
that evaluates to an array value.
For example, here is an array type A
with default value [ 1, 2, 3 ]
:
array A = [3] U32 default [ 1, 2, 3 ]
A default value expression need not be a literal array value; it can be any expression with the correct type. For example, you can create a named constant with an array value and use it multiple times, like this:
constant a = [ 1, 2, 3 ]
array A = [3] U8 default a # default value is [ 1, 2, 3 ]
array B = [3] U16 default a # default value is [ 1, 2, 3 ]
If you don’t specify a default value, then the type gets an automatic default value,
consisting of the default value for each element.
The default numeric value is zero, the default Boolean value is false
,
the default string value is ""
, and the default value of an array type
is specified in the type definition.
The type of the default expression must match the size and element type of the array, with type conversions allowed as discussed for array values. For example, this default expression is allowed, because we can convert integer values to floating-point values, and we can promote a single value to an array of three values:
array A = [3] F32 default 1 # default value is [ 1.0, 1.0, 1.0 ]
However, these default expressions are not allowed:
array A = [3] U32 default [ 1, 2 ] # Error: size does not match
array B = [3] U32 default [ "a", "b", "c" ] # Error: element type does not match
6.1.4. Format Strings
You can specify an optional format string which says how to display each element value and optionally provides some surrounding text. For example, here is an array definition that interprets three integer values as wheel speeds measured in RPMs:
array WheelSpeeds = [3] U32 format "{} RPM"
Then an element with value 100 would have the format 100 RPM
.
Note that the format string specifies the format for an element, not the
entire array.
The way an entire array is displayed is implementation-specific.
A standard way is a comma-separated list enclosed in square brackets.
For example, a value [ 100, 200, 300 ]
of type WheelSpeeds
might
be displayed as [ 100 RPM, 200 RPM, 300 RPM ]
.
Or, since the format is the same for all elements, the implementation could
display the array as [ 100, 200, 300 ] RPM
.
The special character sequence {}
is called a replacement field; it says
where to put the value in the format text.
Each format string must have exactly one replacement field.
The following replacement fields are allowed:
-
The field
{}
for displaying element values in their default format. -
The field
{c}
for displaying a character value -
The field
{d}
for displaying a decimal value -
The field
{x}
for displaying a hexadecimal value -
The field
{o}
for displaying an octal value -
The field
{e}
for displaying a rational value in exponent notation, e.g.,1.234e2
. -
The field
{f}
for displaying a rational value in fixed-point notation, e.g.,123.4
. -
The field
{g}
for displaying a rational value in general format (fixed-point notation up to an implementation-dependent size and exponent notation for larger sizes).
For field types c
, d
, x
, and o
, the element type must be an integer
type.
For field types e
, f
, and g
, the element type must be a floating-point
type.
For example, the following format string is illegal, because
type string
is not an integer type:
array A = [3] string format "{d}" # Illegal: string is not an integer type
For field types e
, f
, and g
, you can optionally specify a precision
by writing a decimal point and an integer before the field type. For example,
the replacement field {.3f}
, specifies fixed-point notation with a precision
of 3.
To include the literal character {
in the formatted output, you can write
{{
, and similarly for }
and }}
. For example, the following definition
array A = [3] U32 format "{{element {}}}"
specifies a format string element {0}
for element value 0.
No other use of {
or }
in a format string is allowed. For example, this is illegal:
array A = [3] U32 format "{" # Illegal use of { character
You can include both a default value and a format; in this case, the default value must come first. For example:
array WheelSpeeds = [3] U32 default 100 format "{} RPM"
If you don’t specify an element format, then each element is displayed
using the default format for its type.
Therefore, omitting the format string is equivalent to writing the format
string "{}"
.
6.1.5. Arrays of Arrays
An array type may have another array type as its element type. In this way you can construct an array of arrays. For example:
array A = [3] U32
array B = [3] A # An array of 3 A, which is an array of 3 U32
When constructing an array of arrays, you may provide any legal default expression, so long as the types are compatible. For example:
array A = [2] U32 default 10 # default value is [ 10, 10 ]
array B1 = [2] A # default value is [ [ 10, 10 ], [ 10, 10 ] ]
array B2 = [2] A default 1 # default value is [ [ 1, 1 ], [ 1, 1 ] ]
array B3 = [2] A default [ 1, 2 ] # default value is [ [ 1, 1 ], [ 2, 2 ] ]
array B4 = [2] A default [ [ 1, 2 ], [ 3, 4 ] ]
6.2. Struct Type Definitions
A struct type definition associates a name with a struct type. A struct type describes the shape of a struct value. It specifies a mapping from element names to their types. As discussed below, it also specifies a serialization order for the struct elements.
6.2.1. Writing a Struct Type Definition
As an example, here is a struct type definition that associates the name S
with
a struct type containing two members: x
of type U32
, and y
of type string
:
struct S { x: U32, y: string }
In general, to write a struct type definition, you write the following:
-
The keyword
struct
. -
The name of the struct type.
-
A sequence of struct type members enclosed in curly braces
{
…}
.
A struct type member consists of a name, a colon, and a
type name,
for example x: U32
.
The struct type members form an element sequence in which the optional terminating punctuation is a comma. As usual for element sequences, you can omit the comma and use a newline instead. So, for example, we can write the definition shown above in this alternate way:
struct S {
x: U32
y: string
}
6.2.2. Annotating a Struct Type Definition
As noted in the beginning of this section, a type definition is an annotatable element, so you can attach pre and post annotations to it. A struct type member is also an annotatable element, so any struct type member can have pre and post annotations as well. Here is an example:
@ This is a pre annotation for struct S
struct S {
@ This is a pre annotation for member x
x: U32 @< This is a post annotation for member x
@ This is a pre annotation for member y
y: string @< This is a post annotation for member y
} @< This is a post annotation for struct S
6.2.3. Default Values
You can specify an optional default value for a struct definition.
To do this, you write the keyword default
and an expression
that evaluates to a struct
value.
For example, here is a struct type S
with default value { x = 1, y = "abc"
}
:
struct S { x: U32, y: string } default { x = 1, y = "abc" }
A default value expression need not be a literal struct value; it can be any expression with the correct type. For example, you can create a named constant with a struct value and use it multiple times, like this:
constant s = { x = 1, y = "abc" }
struct S1 { x: U8, y: string } default s
struct S2 { x: U32, y: string } default s
If you don’t specify a default value, then the struct type gets an automatic default value, consisting of the default value for each member.
The type of the default expression must match the type of the struct, with type conversions allowed as discussed for struct values. For example, this default expression is allowed, because we can convert integer values to floating-point values, and we can promote a single value to a struct with numeric members:
struct S { x: F32, y: F32 } default 1 # default value is { x = 1.0, y = 1.0 }
And this default expression is allowed, because if we omit a member of a struct, then FPP will fill in the member and give it the default value:
struct S { x: F32, y: F32 } default { x = 1 } # default value is { x = 1.0, y = 0.0 }
However, these default expressions are not allowed:
struct S1 { x: U32, y: string } default { z = 1 } # Error: member z does not match
struct S2 { x: U32, y: string } default { x = "abc" } # Error: type of member x does not match
6.2.4. Member Arrays
For any struct member, you can specify that the member is an array of elements. To do this you, write an array the size enclosed in square brackets before the member type. For example:
struct S {
x: [3] U32
}
This definition says that struct S
has one element x
,
which is an array consisting of three U32
values.
We call this array a member array.
Member arrays vs. array types: Member arrays let you include an array of elements as a member of a struct type, without defining a separate named array type. Also:
-
Member arrays generate less code than named arrays. Whereas a member size array is a native C++ array, each named array is a C++ class.
-
The size of a member array is not limited to 256 elements.
On the other hand, defining a named array is usually a good choice when
-
You want to define a small reusable array.
-
You want to use the array outside of any structure.
-
You want the convenience of a generated array class, which has a richer interface than the bare C++ array.
In particular, the generated array class provides bounds-checked access operations: it causes a runtime failure if an out-of-bounds access occurs. The bounds checking provides an additional degree of memory safety when accessing array elements.
Member arrays and default values: FPP ignores member array sizes when checking the types of default values. For example, this code is accepted:
struct S {
x: [3] U32
} default { x = 10 }
The member x
of the struct S
gets three copies of the value
10 specified for x
in the default value expression.
GDS limitation: The F Prime GDS does not properly handle member arrays. Therefore, if you plan to use the F Prime GDS, then you should avoid using a struct type with a member array in the flight-ground interface (i.e., commands, events, telemetry, and parameters).
6.2.5. Member Format Strings
For any struct member, you can include an optional format.
To do this, write the keyword format
and a format string.
The format string for a struct member has the same form as for an
array member.
For example, the following struct definition specifies
that member x
should be displayed as a hexadecimal value:
struct Channel {
name: string
offset: U32 format "offset 0x{x}"
}
How the entire struct is displayed depends on the implementation.
As an example, the value of S
with name = "momentum"
and offset = 1024
might look like this when displayed:
Channel { name = "momentum", offset = 0x400 }
If you don’t specify a format for a struct member, then the system uses the default format for the type of that member.
If the member has a size greater than one, then the format is applied to each element. For example:
struct Telemetry {
velocity: [3] F32 format "{} m/s"
}
The format string is applied to each of the three
elements of the member velocity
.
6.2.6. Struct Types Containing Named Types
A struct type may have an array or struct type as a member type. In this way you can define a struct that has arrays or structs as members. For example:
array Speeds = [3] U32
# Member speeds has type Speeds, which is an array of 3 U32 values
struct Wheel { name: string, speeds: Speeds }
When initializing a struct, you may provide any legal default expression, so long as the types are compatible. For example:
array A = [2] U32
struct S1 { x: U32, y: string }
# default value is { s1 = { x = 0, y = "" }, a = [ 0, 0 ] }
struct S2 { s1: S1, a: A }
# default value is { s1 = { x = 0, y = "abc" }, a = [ 5, 5 ] }
struct S3 { s1: S1, a: A } default { s1 = { y = "abc" }, a = 5 }
6.2.7. The Order of Members
For struct values,
we said that the order in which the members appear in the value is not
significant.
For example, the expressions { x = 1, y = 2 }
and { y = 2, x = 1 }
denote
the same value.
For struct types, the rule is different.
The order in which the members appear is significant, because
it governs the order in which the members appear in the generated
code.
For example, the type struct S1 { x: U32, y : string }
might generate a C++
class S1
with members x
and y
laid out with x
first; while struct S2
{ y : string, x : U32 }
might generate a C++ class S2
with members x
and y
laid out with y
first.
Since class members are generally serialized in the order in which they appear in
the class,
the members of S1
would be serialized with x
first, and the members of
S2
would be serialized with y
first.
Serializing S1
to data and then trying to deserialize it to S2
would
produce garbage.
The order matters only for purposes of defining the type, not for assigning default values to it. For example, this code is legal:
struct S { x: U32, y: string } default { y = "abc", x = 5 }
FPP struct values have no inherent order associated with their members. However, once those values are assigned to a named struct type, the order becomes fixed.
6.3. Abstract Type Definitions
An array or struct type definition specifies a complete type: in addition to the name of the type, it provides the names and types of all the members. An abstract type, by contrast, has an incomplete or opaque definition. It provides only a name N. Its purpose is to tell the analyzer that a type with name N exists and will be defined elsewhere. For example, if the target language is C++, then the type is a C++ class.
To define an abstract type, you write the keyword type
followed
by the name of the type.
For example, you can define an abstract type T
; then you can construct
an array A
with member type T
:
type T # T is an abstract type
array A = [3] T # A is an array of 3 values of type T
This code says the following:
-
A type
T
exists. It is defined in the implementation, but not in the model. -
A
is an array of three values, each of typeT
.
Now suppose that the target language is C++. Then the following happens when generating code:
-
The definition
type T
does not cause any code to be generated. -
The definition
array A =
… causes a C++ classA
to be generated. By F Prime convention, the generated files areAArrayAc.hpp
andAArrayAc.cpp
. -
File
AArrayAc.hpp
includes a header fileT.hpp
.
It is up to the user to implement a C++ class T
with
a header file T.hpp
.
This header file must define T
in a way that is compatible
with the way that T
is used in A
.
We will have more to say about this topic in the section on
implementing abstract types.
In general, an abstract type T
is opaque in the FPP model
and has no values that are expressible in the model.
Thus, every use of an abstract type T
represents the default value
for T
.
The implementation of T
in the target language
provides the default value.
In particular, when the target language is C++, the default
value is the zero-argument constructor T()
.
Built-in types: When translating to XML, there are a few special types that are abstract in the model, but that are known to the translator. You don’t have to define C++ classes for these types. We will discuss these types further in the section on implementing abstract types.
7. Defining Enums
An FPP model may contain one or more enum definitions. Enum is short for enumeration. An FPP enum is similar to an enum in C or C++. It defines a named type called an enum type and a set of named constants called enumerated constants. The enumerated constants are the values associated with the type.
An enum definition may appear at the top level or inside a module definition. An enum definition is an annotatable element.
7.1. Writing an Enum Definition
Here is an example:
enum Decision {
YES
NO
MAYBE
}
This code defines an enum type Decision
with three
enumerated constants: YES
, NO
, and MAYBE
.
In general, to write an enum definition, you write the following:
-
The keyword
enum
. -
The name of the enum.
-
A sequence of enumerated constants enclosed in curly braces
{
…}
.
The enumerated constants form an element sequence in which the optional terminating punctuation is a comma. For example, this definition is equivalent to the one above:
enum Decision { YES, NO, MAYBE }
There must be at least one enumerated constant.
7.2. Using an Enum Definition
Once you have defined an enum, you can use the enum as a type and the enumerated constants as constants of that type. The name of each enumerated constant is qualified by the enum name. Here is an example:
enum State { ON, OFF }
constant initialState = State.OFF
The constant s
has type State
and value State.ON
.
Here is another example:
enum Decision { YES, NO, MAYBE }
array Decisions = [3] Decision default Decision.MAYBE
Here we have used the enum type as the type of the array member,
and we have used the value Decision.MAYBE
as the default
value of an array member.
7.3. Numeric Values
As in C and C++, each enumerated constant has an associated
numeric value.
By default, the values start at zero and go up by one.
For example, in the enum Decision
defined above,
YES
has value 0, NO
has value 1, and MAYBE
has value 2.
You can optionally assign explicit values to the enumerated constants. To do this, you write an equals sign and an expression after each of the constant definitions. Here is an example:
enum E { A = 1, B = 2, C = 3 }
This definition creates an enum type E
with three enumerated constants E.A
,
E.B
, and E.C
. The constants have 1, 2, and 3 as their associated numeric
values.
If you provide an explicit numeric value for any of the enumerated constants, then you must do so for all of them. For example, this code is not allowed:
# Error: cannot provide a value for just one enumerated constant
enum E { A = 1, B, C }
Further, the values must be distinct.
For example, this code is not allowed, because
the enumerated constants A
and B
both have the value 2:
# Error: enumerated constant values must be distinct
enum E { A = 2, B = 1 + 1 }
You may convert an enumerated constant to its associated numeric value. For example, this code is allowed:
enum E { A = 5 }
constant c = E.A + 1
The constant c
has the value 6.
However, you may not convert a numeric value to an enumerated constant. This is for type safety reasons: a value of enumeration type should have one of the numeric values specified in the type. Assigning an arbitrary number to an enum type would violate this rule.
For example, this code is not allowed:
enum E { A, B, C }
# Error: cannot assign integer 10 to type E
array A = [3] E default 10
7.4. The Representation Type
Each enum definition has an associated representation type. This is the primitive integer type used to represent the numeric values associated with the enumerated constants when generating code.
If you don’t specify a representation type, then the default
type is I32
.
For example, in the enumerations defined in the previous sections,
the representation type is I32
.
To specify an explicit representation type, you write it after
the enum name, separated from the name by a colon, like this:
enum Small : U8 { A, B, C }
This code defines an enum Small
with three enumerated constants
Small.A
, Small.B
, and Small.C
.
Each of the enumerated constants is represented as a U8
value
in C++.
GDS limitation:
The F Prime GDS currently assumes that all enums use the default representation
size of I32
.
Therefore, if you plan to use the F Prime GDS, then
you should not use any enum with a specified representation
type other than I32
in the flight-ground interface (i.e., commands, events, telemetry,
and parameters).
7.5. The Default Value
Every type in FPP has an associated default value. For enum types, if you don’t specify a default value explicitly, then the default value is the first enumerated constant in the definition. For example, given this definition
enum Decision { YES, NO, MAYBE }
the default value for the type Decision
is Decision.YES
.
That may be too permissive, say if Decision
represents
a decision on a bank loan.
Perhaps the default value should be Decision.MAYBE
.
To specify an explicit default value, write the keyword default
and the enumerated constant after the enumerated constant
definitions, like this:
enum Decision { YES, NO, MAYBE } default MAYBE
Notice that when using the constant MAYBE
as a default value, we
don’t need to qualify it with the enum name, because the
use appears inside the enum where it is defined.
8. Defining Ports
A port definition defines an F Prime port. In F Prime, a port specifies the endpoint of a connection between two component instances. Components are the basic units of FSW function in F Prime and are described in the next section. A port definition specifies (1) the name of the port, (2) the type of the data carried on the port, and (3) an optional return type.
8.1. Port Names
The simplest port definition consists of the keyword port
followed
by a name.
For example:
port P
This code defines a port named P
that carries no data and returns
no data.
This kind of port can be useful for sending or receiving a triggering event.
8.2. Formal Parameters
More often, a port will carry data. To specify the data, you write formal parameters enclosed in parentheses. The formal parameters of a port definition are similar to the formal parameters of a function in a programming language: each one has a name and a type, and you may write zero or more of them. For example:
port P1() # Zero parameters; equivalent to port P1
port P2(a: U32) # One parameter
port P3(a: I32, b: F32, c: string) # Three parameters
The type of a formal parameter may be any valid type, including an
array type, a struct type, an enum type, or an abstract type.
For example, here is some code that defines an enum type E
and
and abstract type T
, and then uses those types in the
formal parameters of a port:
enum E { A, B }
type T
port P(e: E, t: T)
The formal parameters form an element sequence in which the optional terminating punctuation is a comma. As usual for element sequences, you can omit the comma and use a newline instead. So, for example, we can write the definition shown above in this alternate way:
enum E { A, B }
type T
port P(
e: E
t: T
)
8.3. Handler Functions
As discussed further in the sections on
defining components
and
instantiating components,
when constructing an F Prime application, you
instantiate port definitions as output ports and
input ports of component instances.
Output ports are connected to input ports.
For each output port pOut
of a component instance c1
,
there is a corresponding auto-generated function that the
implementation of c1
can call in order to invoke pOut
.
If pOut
is connected to an input
port pIn
of component instance c2
, then invoking pOut
runs a
handler function pIn_handler
associated with pIn
.
The handler function is part of the implementation of the component
C2
that c2
instantiates.
In this way c1
can send data to c2
or request
that c2
take some action.
Each input port may be synchronous or asynchronous.
A synchronous invocation directly calls a handler function.
An asynchronous invocation calls a short function that puts
a message on a queue for later dispatch.
Dispatching the message calls the handler function.
Translating handler functions:
In FPP, each output port pOut
or input port pIn
has a port type.
This port type refers to an FPP port definition P
.
In the C++ translation, the signature of a handler function
pIn_handler
for pIn
is derived from P
.
In particular, the C++ formal parameters of pIn_handler
correspond to the
FPP formal parameters of P
.
When generating the handler function pIn_handler
, F
Prime translates each formal parameter p of P
in the following way:
-
If p carries a primitive value, then p is translated to a C++ value parameter.
-
Otherwise p is translated to a C++
const
reference parameter.
As an example, suppose that P
looks like this:
type T
port P(a: U32, b: T)
Then the signature of pIn_handler
might look like this:
virtual void pIn_handler(U32 a, const T& b);
Calling handler functions:
Suppose again that output port pOut
of component instance c1
is connected to input port pIn
of component instance c2
.
Suppose that the implementation of c1
invokes pOut
.
What happens next depends on whether pIn
is synchronous
or asynchronous.
If pIn
is synchronous, then the invocation is a direct
call of the pIn
handler function.
Any value parameter is passed by copying the value on
the stack.
Any const
reference parameter provides a reference to
the data passed in by c1
at the point of invocation.
For example, if pIn
has the port type P
shown above,
then the implementation of pIn_handler
might look like this:
// Assume pIn is a synchronous input port
void C2::pIn_handler(U32 a, const T& b) {
// a is a local copy of a U32 value
// b is a const reference to T data passed in by c1
}
Usually the const
reference is what you want, for efficiency reasons.
If you want a local copy of the data, you can make one.
For example:
// Copy b into b1
auto b1 = b
Now b1
has the same data that the parameter b
would have
if it were passed by value.
If pIn
is asynchronous, then the invocation does not
call the handler directly. Instead, it calls
a function that puts a message on a queue.
The handler is called when the message is dispatched.
At this point, any value parameter is passed by
copying the value out of the queue and onto the stack.
Any const
reference parameter is passed by
(1) copying data out of the queue and onto the stack and
(2) then providing a const
reference to the data on the stack.
For example:
// Assume pIn is an asynchronous input port
void C2::pIn_handler(U32 a, const T& b) {
// a is a local copy of a U32 value
// b is a const reference to T data copied across the queue
// and owned by this component
}
Note that unlike in the synchronous case, const references in parameters refer to data owned by the handler (residing on the handler stack), not data owned by the invoking component. Note also that the values must be small enough to permit placement on the queue and on the stack.
If you want the handler and the invoking component to share data
passed in as a parameter, or if the data values are too large
for the queue and the stack, then you can use a data structure
that contains a pointer or a reference as a member.
For example, T
could have a member that stores a reference
or a pointer to shared data.
F Prime provides a type Fw::Buffer
that stores a
pointer to a shared data buffer.
8.4. Reference Parameters
You may write the keyword ref
in front of any formal parameter p
of a port definition.
Doing this specifies that p is a reference parameter.
Each reference parameter in an FPP port becomes a mutable
C++ reference at the corresponding place in the
handler function signature.
For example, suppose this port definition
type T
port P(a: U32, b: T, ref c: T)
appears as the type of an input port pIn
of component C
.
The generated code for C
might contain a handler function with a
signature like this:
virtual void pIn_handler(U32 a, const T& b, T& c);
Notice that parameter b
is not marked ref
, so it is
translated to const T& b
, as discussed in the previous section.
On the other hand, parameter c
is marked ref, so it
is translated to T& c
.
Apart from the mutability, a reference parameter has the same
behavior as a const
reference parameter, as described in
the previous section.
In particular:
-
When
pIn
is synchronous, a reference parameter p ofpIn_handler
refers to the data passed in by the invoking component. -
When
pIn
is asynchronous, a reference parameter p ofpIn_handler
refers to data copied out of the queue and placed on the local stack.
The main reason to use a reference parameter is to return a value to the sender by storing it through the reference. We discuss this pattern in the section on returning values.
8.5.
To do this you write an arrow ->
and a type
after the name and the formal parameters, if any.
For example:
type T
port P1 -> U32 # No parameters, returns U32
port P2(a: U32, b: F32) -> T # Two parameters, returns T
Invoking a port with a return type is like calling a function with a return value. Such a port may be used only in a synchronous context (i.e., as a direct function call, not as a message placed on a concurrent queue).
In a synchronous context only, ref
parameters provide another way to return
values on the port,
by assigning to the reference, instead of executing a C++ return
statement.
As an example, consider the following two port definitions:
type T
port P1 -> T
port P2(ref t: T)
The similarities and differences are as follows:
-
Both
P1
andP2
must be used in a synchronous context, because each returns aT
value. -
In the generated C++ code,
-
The function for invoking
P1
has no arguments and returns aT
value. A handler associated withP1
returns a value of typeT
via the C++return
statement. For example:T C::p1In_handler() { ... return T(1, 2, 3); }
-
The function for invoking
P1
has one argumentt
of typeT&
. A handler associated withP2
returns a value of typeT
by updating the referencet
(assigning to it, or updating its fields). For example:void C::p2In_handler(T& t) { ... t = T(1, 2, 3); }
-
The second way may involve less copying of data.
Finally, there can be any number of reference parameters,
but at most one return value.
So if you need to return multiple values on a port, then reference
parameters can be useful.
As an example, the following port attempts to update a result
value of type U32
.
It does this via reference parameter.
It also returns a status value indicating whether the update
was successful.
enum Status { SUCCEED, FAIL }
port P(ref result: U32) -> Status
A handler for P
might look like this:
Status C::pIn_handler(U32& result) { Status status = Status::FAIL; if (...) { ... result = ... status = Status::SUCCEED; } return status; }
8.6. Pass-by-Reference Semantics
Whenever a C++ formal parameter p enables sharing of data between
an invoking component and a handler function pIn_handler
,
we say that p has pass-by-reference semantics.
Pass-by-reference semantics occurs in the following cases:
-
p has reference or
const
reference type, and the portpIn
is synchronous. -
p has a type T that contains a pointer or a reference as a member.
When using pass-by-reference semantics, you must carefully manage the use of the data to avoid concurrency bugs such as data races. This is especially true for references that can modify shared data.
Except in special cases that require special expertise (e.g.,
the implementation of highly concurrent data structures),
you should enforce the rule that at most
one component may use any piece of data at any time.
In particular, if component A
passes a reference to component B
,
then component A
should not use the reference while
component B
is using it, and vice versa.
For example:
-
Suppose component
A
owns some dataD
and passes a reference toD
via a synchronous port call to componentB
. Suppose the port handler in componentB
uses the data but does not store the reference, so that when the handler exits, the reference is lost. This is a good pattern. In this case, we may say that ownership ofD
resides inA
, temporarily goes toB
for the life of the handler, and goes back toA
when the handler exits. Because the port call is synchronous, the handler inB
never runs concurrently with any code inA
that usesD
. So at most one ofA
orB
usesD
at any time. -
Suppose instead that the handler in
B
stores the reference into a member variable, so that the reference persists after the handler exits. If this happens, then you should make sure thatA
cannot useD
unless and untilB
passes ownership ofD
toA
and vice versa. For example, you could use state variables of enum type inA
and inB
to track ownership, and you could have a port invocation fromA
toB
pass the reference and transfer ownership fromA
toB
and vice versa.
8.7. Annotating a Port Definition
A port definition is an annotatable element. Each formal parameter is also an annotatable element. Here is an example:
@ Pre annotation for port P
port P(
@ Pre annotation for parameter a
a: U32
@ Pre annotation for parameter b
b: F32
)
9. Defining Components
In F Prime, the component is the basic unit of FSW function. An F Prime component is similar to a class in an object-oriented language. An F Prime FSW application is divided into several component instances, each of which instantiates a component. The component instances communicate by sending and receiving invocations on their ports.
In F Prime, there are three kinds of components: active, queued, and passive. An active component has a thread of control and a message queue. A queued component has a message queue, but no thread of control; control runs on another thread, such as a rate group thread. A passive component has no thread of control and no message queue; it is like a non-threaded function library.
9.1. Component Definitions
An FPP component definition defines an F Prime component. To write a component definition, you write the following:
-
The component kind: one of
active
,passive
, orqueued
. -
The keyword
component
. -
The name of the component.
-
A sequence of component members enclosed in curly braces
{
…}
.
As an example, here is a passive component C
with no members:
@ An empty passive component
passive component C {
}
A component definition and each of its members is an annotatable element. For example, you can annotate the component as shown above. The members of a component form an element sequence with a semicolon as the optional terminating punctuation. The following sections describe the available component members.
9.2. Port Instances
A port instance is a component member that specifies an instance of an FPP port used by the instances of the component. Component instances use their port instances to communicate with other component instances.
A port instance instantiates a port. The port definition provides information common to all uses of the port, such as the kind of data carried on the port. The port instance provides use-specific information, such as the name of the instance and the direction of invocation (input or output).
9.2.1. Basic Port Instances
The simplest port instance specifies a kind, a name, and a type. The kind is one of the following:
-
async
input
: Input to this component that arrives on a message queue, to be dispatched on this component’s thread (if this component is active) or on the thread of another port invocation (if this component is queued). -
sync
input
: Input that invokes a handler defined in this component, and run on the thread of the caller. -
guarded
input
: Similar to sync input, but the handler is guarded by a mutual exclusion lock. -
output
: Output transmitted by this component.
The name is the name of the port instance. The type refers to a port definition.
As an example, here is a passive component F32Adder
that
adds two F32
values and produces an F32
value.
@ A port for carrying an F32 value
port F32Value(value: F32)
@ A passive component for adding two F32 values
passive component F32Adder {
@ Input 1
sync input port f32ValueIn1: F32Value
@ Input 2
sync input port f32ValueIn2: F32Value
@ Output
output port f32ValueOut: F32Value
}
There are two sync input port instances and one output port
instance.
The kind appears first, followed by the keyword port
, the port instance
name, a colon, and the type.
Each port instance is an
annotatable element,
so you can annotate the instances as shown.
As another example, here is an active version of F32Adder
with async
input ports:
@ A port for carrying an F32 value
port F32Value(value: F32)
@ An active component for adding two F32 values
active component ActiveF32Adder {
@ Input 1
async input port f32ValueIn1: F32Value
@ Input 2
async input port f32ValueIn2: F32Value
@ Output
output port f32ValueOut: F32Value
}
In each case, the adding is done in the target language. For example, in the C++ implementation, you would generate a base class with a virtual handler function, and then override that virtual function in a derived class that you write. For further details about implementing F Prime components, see the F Prime User’s Guide.
Note on terminology: As explained above, there is a technical
distinction between a port type (defined outside any component, and providing
the type of a port instance)
and a port instance (specified inside a component and instantiating
a port type).
However, it is sometimes useful to refer to a port instance with
the shorter term "port" when there is no danger of confusion.
We will do that in this manual.
For example, we will say that the F32Adder
component has three
ports: two async input ports of type F32Value
and one output port
of type F32Value
.
9.2.2. Rules for Port Instances
The port instances appearing in a component definition must satisfy certain rules. These rules ensure that the FPP model makes sense.
First, no passive component may have an async
input
port.
This is because a passive component has no message queue,
so asynchronous input is not possible.
As an example, if we modify the input ports of our F32Adder
to make them async
, we get an error.
port F32Value(value: F32)
# Error: Passive component may not have async input
passive component ErroneousF32Adder {
async input port f32ValueIn1: F32Value
async input port f32ValueIn2: F32Value
output port f32ValueOut: F32Value
}
Try presenting this code to fpp-check
and observe what happens.
Second, an active or queued component must have asynchronous input.
That means it must have at least one async input port;
or it must have an internal port (described below);
or it must have at least one async command (also described below).
As an example, if we modify the input ports of our ActiveF32Adder
to make them sync
, we get an error, because
there is no async input.
port F32Value(value: F32)
# Error: Active component must have async input
active component ErroneousActiveF32Adder {
sync input port f32ValueIn1: F32Value
sync input port f32ValueIn2: F32Value
output port f32ValueOut: F32Value
}
Third, a port type appearing in an async
input
port
may not have a return type.
This is because returning a value
makes sense only for synchronous input.
As an example, this component definition is illegal:
port P -> U32
active component Error {
# Error: port instance p: P is async input and
# port P has a return type
async input port p: P
}
9.2.3. Arrays of Port Instances
When you specify a port instance as part of an FPP component, you are actually specifying an array of port instances. Each instance has a port number, where the port numbers start at zero and go up by one at each successive element. (Another way to say this is that the port numbers are the array indices, and the indices start at zero.)
If you don’t specify a size for the array, as shown in
the previous sections, then the array has size one, and there is a single port
instance with port number zero.

Alternatively, you can specify an explicit array size.
You do that by writing an expression
enclosed in square brackets [
… ]
denoting the size (number of elements)
of the array.
The size expression must evaluate to a numeric value.
As with
array type definitions,
the size goes before the element type.
As an example, here is another version of the F32Adder
component, this time
using a single array of two input ports instead of two named ports.
@ A port for carrying an F32 value
port F32Value(value: F32)
@ A passive component for adding two F32 values
passive component F32Adder {
@ Inputs 0 and 1
sync input port f32ValueIn: [2] F32Value
@ Output
output port f32ValueOut: F32Value
}
9.2.4. Priority
For async
input
ports, you may specify a priority.
The priority specification is not allowed for other kinds of ports.
To specify a priority, you write the keyword priority
and an
expression that evaluates to a numeric value after the port type.
As an example, here is a modified version of the ActiveF32Adder
with specified priorities:
@ A port for carrying an F32 value
port F32Value(value: F32)
@ An active component for adding two F32 values
@ Uses specified priorities
active component ActiveF32Adder {
@ Input 1 at priority 10
async input port f32ValueIn1: F32Value priority 10
@ Input 2 at priority 20
async input port f32ValueIn2: F32Value priority 20
@ Output
output port f32ValueOut: F32Value
}
If an async
input
port has no specified priority, then the
translator uses a default priority.
The precise meaning of the default priority and of the numeric priorities is
implementation-specific.
In general the priorities regulate the order in which elements are dispatched
from the message queue.
9.2.5. Queue Full Behavior
By default, if an invocation of an async
input
port causes
a message queue to overflow, then a FSW assertion fails.
A FSW assertion is a condition that must be true in order
for FSW execution to proceed safely.
The behavior of a FSW assertion failure is configurable in the C++
implementation of the F Prime framework; typically it causes a FSW
abort and system reset.
Optionally, you can specify the behavior when a message
received on an async
input
port causes a queue overflow.
There are three possible behaviors:
-
assert
: Fail a FSW assertion (the default behavior). -
block
: Block the sender until the queue is available. -
drop
: Drop the incoming message and proceed.
To specify queue full behavior, you write one of the keywords assert
,
block
, or drop
after the port type and after the priority
(if any).
As an example, here is the ActiveF32Adder
updated with explicit
queue full behavior.
@ A port for carrying an F32 value
port F32Value(value: F32)
@ An active component for adding two F32 values
@ Uses specified priorities
active component ActiveF32Adder {
@ Input 1 at priority 10: Block on queue full
async input port f32ValueIn1: F32Value priority 10 block
@ Input 2: Drop on queue full
async input port f32ValueIn2: F32Value drop
@ Output
output port f32ValueOut: F32Value
}
As for priority specifiers, queue full specifiers are allowed only
for async
input
ports.
9.2.6. Serial Port Instances
When writing a port instance, instead of specifying a named port type,
you may write the keyword serial
.
Doing this specifies a serial port instance.
A serial port instance does not specify the type of data that it carries.
It may be connected to a port of any type.
Serial data passes through the port; the data may be converted to or from a
specific type at the other end of the connection.
As an example, here is a passive component for taking a stream of serial data and splitting it (i.e., repeating it by copy) onto several streams:
@ Split factor
constant splitFactor = 10
@ Component for splitting a serial data stream
passive component SerialSplitter {
@ Input
sync input port serialIn: serial
@ Output
output port serialOut: [splitFactor] serial
}
By using serial ports, you can send several unrelated types of data over the same port connection. This technique is useful when communicating across a network: on each side of the network connection, a single component can act as a hub that routs all data to and from components on that side. This flexibility comes at the cost that you lose the type compile-time type checking provided by port connections with named types. For more information about serial ports and their use, see the F Prime User’s Guide.
9.3. Special Port Instances
A special port instance is a port instance that has a special behavior in F Prime. As discussed above, when writing a general port instance, you specify a port kind, a port type, and possibly other information such as array size and priority. Writing a special port instance is a bit different. In this case you specify a predefined behavior provided by the F Prime framework. The special port behaviors fall into five groups: commands, events, telemetry, parameters, and time.
9.3.1. Command Ports
A command is an instruction to the spacecraft to perform an action. Each component instance C that specifies commands has the following high-level behaviors:
-
At FSW startup time, C registers its commands with a component instance called the command dispatcher.
-
During FSW execution, C receives commands from the command dispatcher. For each command received, C executes the command and sends a response back to the command dispatcher.
In FPP, the keywords for the special command behaviors are as follows:
-
command
reg
: A port for sending command registration requests. -
command
recv
: A port for receiving commands. -
command
resp
: A port for sending command responses.
Collectively, these ports are known as command ports.
To specify a command port, you write one of the keyword pairs
shown above followed by the keyword port
and the port name.
As an example, here is a passive component CommandPorts
with each
of the command ports:
@ A component for illustrating command ports
passive component CommandPorts {
@ A port for receiving commands
command recv port cmdIn
@ A port for sending command registration requests
command reg port cmdRegOut
@ A port for sending command responses
command resp port cmdResponseOut
}
Any component may have at most one of each kind of command port. If a component receives commands (more on this below), then all three ports are required. The port names shown in the example above are standard but not required; you can use any names you wish.
During translation, each command port is converted into a typed port instance with a predefined port type, as follows:
-
command
recv
uses the portFw.Cmd
-
command
reg
uses the portFw.CmdReg
-
command
resp
uses the portFw.CmdResponse
The F Prime framework provides definitions for these ports
in the directory Fw/Cmd
.
For checking simple examples, you can use the following
simplified definitions of these ports:
module Fw {
port Cmd
port CmdReg
port CmdResponse
}
For example, to check the CommandPorts
component, you can
add these lines before the component definition.
If you don’t do this, or something similar, then the component
definition won’t pass through fpp-check
because of the missing ports.
(Try it and see.)
Note that the port definitions shown above are for conveniently checking simple examples only. They are not correct for the F Prime framework and will not work properly with F Prime C++ code generation.
For further information about command registration, receipt, and response, and implementing command handlers, see the F Prime User’s Guide.
9.3.2. Event Ports
An event is a report that something happened, for example, that a file was successfully uplinked. The special event behaviors, and their keywords, are as follows:
-
event
: A port for emitting events as serialized bytes. -
text
event
: A port for emitting events as human-readable text (usually used for testing and debugging on the ground).
Collectively, these ports are known as event ports.
To specify an event port, you write one of the keyword groups
shown above followed by the keyword port
and the port name.
As an example, here is a passive component EventPorts
with each
of the event ports:
@ A component for illustrating event ports
passive component EventPorts {
@ A port for emitting events
event port eventOut
@ A port for emitting text events
text event port textEventOut
}
Any component may have at most one of each kind of event port. If a component emits events (more on this below), then both event ports are required.
During translation, each event port is converted into a typed port instance with a predefined port type, as follows:
-
event
uses the portFw.Log
-
text
event
uses the portFw.LogText
The name Log
refers to an event log.
The F Prime framework provides definitions for these ports
in the directory Fw/Log
.
For checking simple examples, you can use the following
simplified definitions of these ports:
module Fw {
port Log
port LogText
}
For further information about events in F Prime, see the F Prime User’s Guide.
9.3.3. Telemetry Ports
Telemetry is data regarding the state of the system.
A telemetry port allows a component to emit telemetry.
To specify a telemetry port, you write the keyword telemetry
,
the keyword port
, and the port name.
As an example, here is a passive component TelemetryPorts
with
a telemetry port:
@ A component for illustrating telemetry ports
passive component TelemetryPorts {
@ A port for emitting telemetry
telemetry port tlmOut
}
Any component may have at most one telemetry port. If a component emits telemetry (more on this below), then a telemetry port is required.
During translation, each telemetry port is converted into
a typed port instance with the predefined port type
Fw.Tlm
.
The F Prime framework provides a definition for this port
in the directory Fw/Tlm
.
For checking simple examples, you can use the following
simplified definition of this port:
module Fw {
port Tlm
}
For further information about telemetry in F Prime, see the F Prime User’s Guide.
9.3.4. Parameter Ports
A parameter is a configurable constant that may be updated from the ground. The current parameter values are stored in an F Prime component called the parameter database.
The special parameter behaviors, and their keywords, are as follows:
-
param
get
: A port for getting the current value of a parameter from the parameter database. -
param
set
: A port for setting the current value of a parameter in the parameter database.
Collectively, these ports are known as parameter ports.
To specify a parameter port, you write one of the keyword groups
shown above followed by the keyword port
and the port name.
As an example, here is a passive component ParamPorts
with each
of the parameter ports:
@ A component for illustrating parameter ports
passive component ParamPorts {
@ A port for getting parameter values
param get port prmGetOut
@ A port for setting parameter values
param set port prmSetOut
}
Any component may have at most one of each kind of parameter port. If a component has parameters (more on this below), then both parameter ports are required.
During translation, each parameter port is converted into a typed port instance with a predefined port type, as follows:
-
param
get
uses the portFw.PrmGet
-
param
set
uses the portFw.PrmSet
The F Prime framework provides definitions for these ports
in the directory Fw/Prm
.
For checking simple examples, you can use the following
simplified definitions of these ports:
module Fw {
port PrmGet
port PrmSet
}
For further information about parameters in F Prime, see the F Prime User’s Guide.
9.3.5. Time Get Ports
A time get port allows a component to get the system time from a
time component.
To specify a time get port, you write the keywords time
get
,
the keyword port
, and the port name.
As an example, here is a passive component TimeGetPorts
with
a time get port:
@ A component for illustrating time get ports
passive component TimeGetPorts {
@ A port for getting the time
time get port timeGetOut
}
Any component may have at most one time get port. If a component emits events or telemetry (more on this below), then a time get port is required, so that the events and telemetry points can be time stamped.
During translation, each time get port is converted into
a typed port instance with the predefined port type
Fw.Time
.
The F Prime framework provides a definition for this port
in the directory Fw/Time
.
For checking simple examples, you can use the following
simplified definition of this port:
module Fw {
port Time
}
For further information about time in F Prime, see the F Prime User’s Guide.
9.4. Internal Ports
An internal port is a port that a component can use to send a message to itself. In the ordinary case, when a component sends a message, it invokes an output port that is connected to an async input port. When the output port and input port reside in the same component, it is simpler to use an internal port.
As an example, suppose we have a component that needs to send a message to itself. We could construct such a component in the following way:
@ A data type T
type T
@ A port for sending data of type T
port P(t: T)
@ A component that sends data to itself on an async input port
active component ExternalSelfMessage {
@ A port for sending data of type T
async input port pIn: P
@ A port for receiving data of type T
output port pOut: P
}
This works, but if the only user of pIn
is
ExternalSelfMessage
, it is cumbersome.
We need to declare two ports and connect them.
Instead, we can use an internal port, like this:
@ A data type T
type T
@ A component that sends data to itself on an internal port
active component InternalSelfMessage {
@ An internal port for sending data of type T
internal port pInternal(t: T)
}
When the implementation of InternalSelfMessage
invokes
the port pInternal
, a message goes on its queue.
This corresponds to the behavior of pOut
in
ExternalSelfMessage
.
Later, when the framework dispatches the message, it
calls a handler function associated with the port.
This corresponds to the behavior of pIn
in
ExternalSelfMessage
.
So an internal port is like two ports (an output port
and an async input port) fused into one.
When writing an internal port, you do not use a named
port definition.
Instead, you provide the formal parameters directly.
Notice that when defining ExternalSelfMessage
we
defined and used the port P
, but when defining
InternalSelfMessage
we did not.
The formal parameters of an internal port work in the same way
as for a port definition,
except that none of the parameters may be a
reference parameter.
When specifying an internal port, you may specify
priority and
queue full behavior
as for an async input port.
For example, we can add priority and queue full behavior
to pInternal
as follows:
@ A data type T
type T
@ A component that sends data to itself on an internal port,
@ with priority and queue full behavior
active component InternalSelfMessage {
@ An internal port for sending data of type T
internal port pInternal(t: T) priority 10 drop
}
Internal ports generate async input, so they make sense
only for active
and queued
components.
As an example, consider the following component
definition:
type T
passive component PassiveInternalPort {
# Internal ports don't make sense for passive components
internal port pInternal(t: T)
}
What do you think will happen if you run fpp-check
on this code?
Try it and see.
9.5. Commands
When defining an F Prime component, you may specify one or more commands. When you are operating the FSW, you use the F Prime Ground Data System or another ground data system to send commands to the FSW. On receipt of a command C, a Command Dispatcher component instance dispatches C to the component instance where that command is implemented. The command is handled in a C++ command handler that you write as part of the component implementation.
For complete information about F Prime command dispatch and handling, see the F Prime User’s Guide. Here we concentrate on how to specify commands in FPP.
9.5.1. Basic Commands
The simplest command consists of a kind followed by the keyword
command
and a name.
The kind is one of the following:
-
async
: The command arrives on a message queue, to be dispatched on this component’s thread (if this component is active) or on the thread of a port invocation (if this component is queued). -
sync
: The command invokes a handler defined in this component, and run on the thread of the caller. -
guarded
: Similar to sync input, but the handler is guarded by a mutual exclusion lock.
Notice that the kinds of commands are similar to the kinds of input ports. The name is the name of the command.
As an example, here is an active component called Action
with two commands: an async command START
and a sync
command STOP
.

That means that when a START
command is dispatched
to an instance of this component, it arrives on a queue.
Later, the F Prime framework takes the message off the queue
and calls the corresponding handler on the thread
of the component.
Command STOP
is declared sync
.
That means that the command runs immediately on the
thread of the invoking component (for example,
a command dispatcher component).
Because the command runs immediately, its handler
should be very short.
For example, it could set a stop flag and then exit.
Notice that we defined the three
command ports
for this component.
All three ports are required for any component that has commands.
As an example, try deleting one or more of the command ports from the
code above and running the result through fpp-check
.
async
commands require a message queue, so
they are allowed only for active and queued
components.
As an example, try making the Action
component passive and
running the result through fpp-check
.
9.5.2. Formal Parameters
When specifying a command, you may specify one or more formal parameters. The parameters are bound to arguments when the command is sent to the spacecraft. Different uses of the same command can have different argument values.
The formal parameters of a command are the same
as for a port definition, except
that none of the parameters may be a
reference parameter.
As an example, here is a Switch
component that has
two states, ON
and OFF
.
The component has a SET_STATE
command for
setting the state.
The command has a single argument state
that specifies the new state.
@ The state enumeration
enum State {
OFF @< The off state
ON @< The on state
}
@ A switch with on and off state
active component Switch {
# ----------------------------------------------------------------------
# Ports
# ----------------------------------------------------------------------
@ Command input
command recv port cmdIn
@ Command registration
command reg port cmdRegOut
@ Command response
command resp port cmdResponseOut
# ----------------------------------------------------------------------
# Commands
# ----------------------------------------------------------------------
@ Set the state
async command SET_STATE(
state: State @< The new state
)
}
9.5.3. Opcodes
Every command in an F Prime FSW application has an opcode. The opcode is a number that uniquely identifies the command. The F Prime framework uses the opcode when dispatching commands because it is a more compact identifier than the name. The name is mainly for human interaction on the ground.
The opcodes associated with each component C are relative to the component. Typically the opcodes start at zero: that is, the opcodes are 0, 1, 2, etc. When constructing an instance I of component C, the framework adds a base opcode for I to each relative opcode associated with C to form the global opcodes associated with I. That way different instances of C can have different opcodes for the same commands defined in C. We will have more to say about base and relative opcodes when we describe component instances and topologies.
If you specify a command c with no explicit opcode, as in the examples shown in the previous sections, then FPP assigns a default opcode to c. The default opcode for the first command in a component is zero. Otherwise the default opcode for any command is one more than the opcode of the previous command.
It is usually convenient to rely on the default opcodes.
However, you may wish to specify one or more opcodes explicitly.
To do this, you write the keyword opcode
followed
by a numeric expression after the command name and after the
formal parameters, if any.
Here is an example:
@ Component for illustrating command opcodes
active component CommandOpcodes {
# ----------------------------------------------------------------------
# Ports
# ----------------------------------------------------------------------
@ Command input
command recv port cmdIn
@ Command registration
command reg port cmdRegOut
@ Command response
command resp port cmdResponseOut
# ----------------------------------------------------------------------
# Commands
# ----------------------------------------------------------------------
@ This command has default opcode 0x0
async command COMMAND_1
@ This command has explicit opcode 0x10
async command COMMAND_2(a: F32, b: U32) opcode 0x10
@ This command has default opcode 0x11
sync command COMMAND_3
}
Within a component, the command opcodes must be unique. For example, this component is incorrect because the opcode zero appears twice:
@ Component for illustrating a duplicate opcode
active component DuplicateOpcode {
# ----------------------------------------------------------------------
# Ports
# ----------------------------------------------------------------------
@ Command input
command recv port cmdIn
@ Command registration
command reg port cmdRegOut
@ Command response
command resp port cmdResponseOut
# ----------------------------------------------------------------------
# Commands
# ----------------------------------------------------------------------
@ This command has opcode 0x0
async command COMMAND_1
@ Oops! This command also has opcode 0x0
async command COMMAND_2 opcode 0x0
}
9.5.4. Priority and Queue Full Behavior
When specifying an async command, you may specify priority and queue full behavior as for an async input port. You put the priority and queue full information after the command name and after the formal parameters and opcode, if any.
Try changing one of the commands in the previous example
to sync
and see what fpp-check
has to say about it.
9.6. Events
When defining an F Prime component, you may specify one or more events. The F Prime framework converts each event into a C++ function that you can call from the component implementation. Calling the function emits a serialized event report that you can store in an on-board file system or send to the ground.
For complete information about F Prime event handling, see the F Prime User’s Guide. Here we concentrate on how to specify events in FPP.
9.6.1. Basic Events
The simplest event consists of the keyword event
, a name, a severity,
and a format string.
The name is the name of the event.
A severity is the keyword severity
and one of the following:
-
activity
high
: Spacecraft activity of greater importance. -
activity
low
: Spacecraft activity of lesser importance. -
command
: An event related to commanding. Primarily used by the command dispatcher. -
diagnostic
: An event relating to system diagnosis and debugging. -
fatal
: An event that causes the system to abort. -
warning
high
: A warning of greater importance. -
warning
low
: A warning of lesser importance.
A format is the keyword format
and a literal string for
use in a formatted real-time display or event log.
As an example, here is an active component called BasicEvents
with a few basic events.
@ A component for illustrating basic events
passive component BasicEvents {
# ----------------------------------------------------------------------
# Ports
# ----------------------------------------------------------------------
@ Event port
event port eventOut
@ Text event port
text event port textEventOut
@ Time get port
time get port timeGetOut
# ----------------------------------------------------------------------
# Events
# ----------------------------------------------------------------------
@ Activity low event
event Event1 severity activity low format "Event 1 occurred"
@ Warning low event
event Event2 severity warning low format "Event 2 occurred"
@ Warning high event
event Event3 severity warning high format "Event 3 occurred"
}
Notice that we defined the two
event ports
and a
time get port
for this component.
All three ports are required for any component that has events.
As an example, try deleting one or more of these ports from the
code above and running the result through fpp-check
.
9.6.2. Formal Parameters
When specifying an event, you may specify one or more formal parameters. The parameters are bound to arguments when the component instance emits the event. The argument values appear in the formatted text that describes the event.
The formal parameters of an event are the same as for a port definition, except that none of the parameters may be a reference parameter. For each formal parameter, there must be a corresponding replacement field in the format string. The replacement fields for event format strings are the same as for format strings in type definitions. The replacement fields in the format string match the event parameters, one for one and in the same order.
As an example, here is a component with two events, each of which has formal parameters. Notice how the replacement fields in the event format strings correspond to the formal parameters.
@ An enumeration of cases
enum Case { A, B, C }
@ An array of 3 F64 values
array F64x3 = [3] F64
@ A component for illustrating event formal parameters
passive component EventParameters {
# ----------------------------------------------------------------------
# Ports
# ----------------------------------------------------------------------
@ Event port
event port eventOut
@ Text event port
text event port textEventOut
@ Time get port
time get port timeGetOut
# ----------------------------------------------------------------------
# Events
# ----------------------------------------------------------------------
@ Event 1
@ Sample output: "Event 1 occurred with argument 42"
event Event1(
arg1: U32 @< Argument 1
) \
severity activity high \
format "Event 1 occurred with argument {}"
@ Event 2
@ Sample output: "Saw value [ 0.001, 0.002, 0.003 ] for case A"
event Event2(
case: Case @< The case
value: F64x3 @< The value
) \
severity warning low \
format "Saw value {} for case {}"
}
9.6.3. Identifiers
Every event in an F Prime FSW application has a unique numeric identifier. As for command opcodes, the event identifiers for a component are specified relative to the component, usually starting from zero and counting up by one. If you omit the identifier, then FPP assigns a default identifier: zero for the first event in the component; otherwise one more than the identifier of the previous event.
If you wish, you may explicitly specify one or more event
identifiers.
To do this, you write the keyword id
followed
by a numeric expression immediately before the keyword format
.
Here is an example:
@ Component for illustrating event identifiers
passive component EventIdentifiers {
# ----------------------------------------------------------------------
# Ports
# ----------------------------------------------------------------------
@ Event port
event port eventOut
@ Text event port
text event port textEventOut
@ Time get port
time get port timeGetOut
# ----------------------------------------------------------------------
# Events
# ----------------------------------------------------------------------
@ Event 1
@ Its identifier is 0x00
event Event1 severity activity low \
id 0x10 \
format "Event 1 occurred"
@ Event 2
@ Its identifier is 0x10
event Event2(
count: U32 @< The count
) \
severity activity high \
id 0x11 \
format "The count is {}"
@ Event 3
@ Its identifier is 0x11
event Event3 severity activity high \
format "Event 3 occurred"
}
Within a component, the event identifiers must be unique.
9.6.4. Throttling
Sometimes it is necessary to throttle events, to ensure that they do not flood the system. For example, suppose that the FSW requests some resource R at a rate r of several times per second. Suppose further that if R is unavailable, then the FSW emits a warning event. In this case, we typically do not want the FSW to emit an unbounded number of warnings at rate r; instead, we want it to emit a single warning or a few warnings.
To achieve this behavior, you can write the keyword throttle
and a
numeric expression after the format string.
The expression must evaluate to a constant value n.
After an instance of the component has emitted the event n times, it will
stop emitting the event.
Here is an example:
@ Component for illustrating event throttling
passive component EventThrottling {
# ----------------------------------------------------------------------
# Ports
# ----------------------------------------------------------------------
@ Event port
event port eventOut
@ Text event port
text event port textEventOut
@ Time get port
time get port timeGetOut
# ----------------------------------------------------------------------
# Events
# ----------------------------------------------------------------------
@ Event 1
event Event1 severity warning high \
format "Event 1 occurred" \
throttle 10
}
In this example, event E
will be throttled after the component
instance has emitted it ten times.
Once an event is throttled, the component instance will no longer emit the event until the throttling is canceled. Typically, the canceling happens via a FSW command. For details, see the F Prime User’s Guide.
9.7. Telemetry
When defining an F Prime component, you may specify one or more telemetry channels. A telemetry channel consists of a data type and an identifier. The F Prime framework converts each telemetry into a C++ function that you can call from the component implementation. Calling the function emits a value on the channel. Each emitted value is called a telemetry point. You can store the telemetry points in an on-board file system or send them the ground.
For complete information about F Prime telemetry handling, see the F Prime User’s Guide. Here we concentrate on how to specify telemetry channels in FPP.
9.7.1. Basic Telemetry
The simplest telemetry channel consists of the keyword telemetry
,
a name, and a data type.
The name is the name of the channel.
The data type is the type of data carried on the channel.
As an example, here is an active component called BasicTelemetry
with a few basic events.
@ An array of 3 F64 values
array F64x3 = [3] F64
@ A component for illustrating basic telemetry channels
passive component BasicTelemetry {
# ----------------------------------------------------------------------
# Ports
# ----------------------------------------------------------------------
@ Telemetry port
telemetry port tlmOut
@ Time get port
time get port timeGetOut
# ----------------------------------------------------------------------
# Telemetry
# ----------------------------------------------------------------------
@ Telemetry channel 1
telemetry Channel1: U32
@ Telemetry channel 2
telemetry Channel2: F64
@ Telemetry channel 3
telemetry Channel3: F64x3
}
Notice that we defined a telemetry port and a time get port for this component. Both ports are required for any component that has telemetry.
9.7.2. Identifiers
Every telemetry channel in an F Prime FSW application has a unique numeric identifier. As for command opcodes and event identifiers, the telemetry channel identifiers for a component are specified relative to the component, usually starting from zero and counting up by one. If you omit the identifier, then FPP assigns a default identifier: zero for the first event in the component; otherwise one more than the identifier of the previous channel.
If you wish, you may explicitly specify one or more
telemetry channel identifiers.
To do this, you write the keyword id
followed
by a numeric expression immediately after the data type.
 Update Frequency
You can specify how often the telemetry is emitted on a channel C. There are two possibilities:
-
always
: Emit a telemetry point on C whenever the component implementation calls the auto-generated function F that emits telemetry on C. -
on
change
: Emit a telemetry point whenever (1) the implementation calls F and (2) either (a) F has not been called before or (b) the last time that F was called, the argument to F had a different value.
Emitting telemetry on change can reduce unnecessary
activity in the system.
For example, suppose a telemetry channel C counts
the number of times that some event E occurs
in a periodic task,
and suppose that E does not occur on every cycle.
If you declare channel C on
change
, then your implementation
can call the telemetry emit function for C on every
cycle, and telemetry will be emitted only when
E occurs.
To specify an update frequency, you write the keyword update
and one of the frequency selectors shown above.
The update specifier goes after
the type name and after the channel identifier, if any.
If you don’t specify an update frequency, then the default
value is always
.
Here is an example:
@ An array of 3 F64 values
array F64x3 = [3] F64
@ Component for illustrating telemetry channel update specifiers
passive component TlmUpdate {
# ----------------------------------------------------------------------
# Ports
# ----------------------------------------------------------------------
@ Telemetry port
telemetry port tlmOut
@ Time get port
time get port timeGetOut
# ----------------------------------------------------------------------
# Telemetry
# ----------------------------------------------------------------------
@ Telemetry channel 1
@ Always emitted
telemetry Channel1: U32
@ Telemetry channel 2
@ Emitted on change
telemetry Channel2: F64 id 0x10 update on change
@ Telemetry channel 3
@ Always emitted
telemetry Channel3: F64x3 update always
}
9.7.4. Format Strings
You may specify how a telemetry channel is formatted in the
ground display.
To do this, you write the keyword format
and a format string
with one
replacement field.
The replacement field must match the type of the telemetry
channel.
The format specifier comes after the type name, after the
channel identifier, and after the update specifier.
Here is an example:
@ Component for illustrating telemetry channel format specifiers
passive component TlmFormat {
# ----------------------------------------------------------------------
# Ports
# ----------------------------------------------------------------------
@ Telemetry port
telemetry port tlmOut
@ Time get port
time get port timeGetOut
# ----------------------------------------------------------------------
# Telemetry
# ----------------------------------------------------------------------
@ Telemetry channel 1
telemetry Channel1: U32 format "{x}"
@ Telemetry channel 2
telemetry Channel2: F64 id 0x10 \
update on change \
format "{.3f}"
@ Telemetry channel 3
telemetry Channel3: F64\
update always \
format "{e}"
}
9.7.5. Limits
You may specify limits, or bounds, on the expected values
carried on a telemetry channel.
There are two kinds of limits: low
(meaning that the
values on the channel should stay above the limit) and high
(meaning that the values should stay below the limit).
Within each kind, there are three levels of severity:
-
yellow
: Crossing the limit is of low concern. -
orange
: Crossing the limit is of medium concern. -
red
: Crossing the limit is of high concern.
The F Prime ground data system displays an appropriate warning when a telemetry point crosses a limit.
The limit specifiers come after the type name, identifier,
update specifier, and format string.
You specify the low limits (if any) first, and then the high limits.
For the low limits, you write the keyword low
followed by a
list of limits in curly braces { … }
.
For the high limits, you do the same thing but use the keyword
high
.
Each limit is a severity keyword followed by a numeric expression.
Here are some examples:
@ Component for illustrating telemetry channel limits
passive component TlmLimits {
# ----------------------------------------------------------------------
# Ports
# ----------------------------------------------------------------------
@ Telemetry port
telemetry port tlmOut
@ Time get port
time get port timeGetOut
# ----------------------------------------------------------------------
# Telemetry
# ----------------------------------------------------------------------
@ Telemetry channel 1
telemetry Channel1: U32 \
low { red 0, orange 1, yellow 2 }
@ Telemetry channel 2
telemetry Channel2: F64 id 0x10 \
update on change \
format "{.3f}" \
low { red -3, orange -2, yellow -1 } \
high { red 3, orange 2, yellow 1 }
@ Telemetry channel 3
telemetry Channel3: F64 \
update always \
format "{e}" \
high { red 3, orange 2, yellow 1 }
}
Each limit must be a numeric value. The type of the telemetry channel must be (1) a numeric type; or (2) an array or struct type each of whose members has a numeric type; or (3) an array or struct type each of whose members satisfies condition (1) or condition (2).
XML limitation: The F Prime XML representation does not allow limits for telemetry channels of array or struct type.
9.8. Parameters
When defining an F Prime component, you may specify one or more parameters. A parameter is a typed constant value that you can update by command. For example, it could be a configuration constant for a hardware device or a software algorithm.
F Prime has special support for parameters, including a parameter database component for storing parameters in a non-volatile manner (e.g., on a file system). For complete information about F Prime parameters, see the F Prime User’s Guide. Here we concentrate on how to specify parameters in FPP.
9.8.1. Basic Parameters
The simplest parameter consists of the keyword param
,
a name, and a data type.
The name is the name of the parameter.
The data type is the type of data stored in the parameter.
As an example, here is an active component called BasicParams
with a few basic parameters.
 Both ports are required for any component that has parameters.
Notice also that we defined the
command ports
for this component.
When you add one or more parameters to a component,
F Prime automatically generates commands for (1)
setting the local parameter in the component and (2) saving
the local parameter to a system-wide parameter database.
Therefore, any component that has parameters must have
the command ports.
Try deleting one or more of the command ports from the example
above and see what fpp-check
does.
9.8.2. Default Values
You can specify a default value for any parameter. This is the value that F Prime will use if no value is available in the parameter database. If you don’t specify a default value, and no value is available in the database, then attempting to get the parameter produces an invalid value. What happens then is up to the FSW implementation. By providing default values for your parameters, you can avoid handling this case.
Here is the example from the previous section, updated to include default values for the parameters:
@ An array of 3 F64 values
array F64x3 = [3] F64
@ A component for illustrating default parameter values
passive component ParamDefaults {
# ----------------------------------------------------------------------
# Ports
# ----------------------------------------------------------------------
@ Command receive port
command recv port cmdIn
@ Command registration port
command reg port cmdRegOut
@ Command response port
command resp port cmdResponseOut
@ Parameter get port
param get port prmGetOut
@ Parameter set port
param set port prmSetOut
# ----------------------------------------------------------------------
# Parameters
# ----------------------------------------------------------------------
@ Parameter 1
param Param1: U32 default 1
@ Parameter 2
param Param2: F64 default 2.0
@ Parameter 3
param Param3: F64x3 default [ 1.0, 2.0, 3.0 ]
}
9.8.3. Identifiers
Every parameter in an F Prime FSW application has a unique numeric identifier. As for command opcodes, event identifiers, and telemetry channel identifiers, the parameter identifiers for a component are specified relative to the component, usually starting from zero and counting up by one. If you omit the identifier, then FPP assigns a default identifier: zero for the first event in the component; otherwise one more than the identifier of the previous parameter.
If you wish, you may explicitly specify one or more
parameter identifiers.
To do this, you write the keyword id
followed
by a numeric expression after the data type
and after the default value, if any.
 Set and Save Opcodes
Each parameter that you specify has two implied commands: one for setting the value bound to the parameter locally in the component, and one for saving the current local value to the system-wide parameter database. The opcodes for these implied commands are called the set and save opcodes for the parameter.
By default, FPP generates set and save opcodes for a parameter P according to the following rules:
-
If no command or parameter appears before P in the component, then the set opcode is 0, and the save opcode is 1.
-
Otherwise, let o be the previous opcode defined in the component (either a command opcode or a parameter save opcode). Then the set opcode is o + 1, and the save opcode is o + 2.
If you wish, you may specify either or both of the set and
save opcodes explicitly.
To specify the set opcode, you write the keywords set
opcode
and a numeric expression.
To specify the save opcode, you write the keywords save
opcode
and a numeric expression.
The set and save opcodes come after the type name, default
parameter value, and parameter identifier.
If both are present, the set opcode comes first.
When you specify an explicit set or save opcode o, the default value for the next opcode is o + 1.  Constants and Types
You can write a constant definition
or type definition
as a component member.
When you do this, the component qualifies
the name of the constant or type, similarly to the way that a
module qualifies the names of the
definitions it contains.
For example, if you define a type T
inside a component
C
, then
-
Inside the definition of
C
, you can refer to the type asT
. -
Outside the definition of
C
, you must refer to the type asC.T
.
As an example, here is the SerialSplitter
component
from the section on
serial port instances, where we have moved the
definition of the constant splitFactor
into the definition of the component.
@ Component for splitting a serial data stream
passive component SerialSplitter {
# ----------------------------------------------------------------------
# Constants
# ----------------------------------------------------------------------
@ Split factor
constant splitFactor = 10
# ----------------------------------------------------------------------
# Ports
# ----------------------------------------------------------------------
@ Input
sync input port serialIn: serial
@ Output
output port serialOut: [splitFactor] serial
}
As another example, here is the Switch
component from the section on
command formal parameters, where we have moved the definition of
the enum State
into the component:
@ A switch with on and off state
active component Switch {
# ----------------------------------------------------------------------
# Types
# ----------------------------------------------------------------------
@ The state enumeration
enum State {
OFF @< The off state
ON @< The on state
}
# ----------------------------------------------------------------------
# Ports
# ----------------------------------------------------------------------
@ Command input
command recv port cmdIn
@ Command registration
command reg port cmdRegOut
@ Command response
command resp port cmdResponseOut
# ----------------------------------------------------------------------
# Commands
# ----------------------------------------------------------------------
@ Set the state
async command SET_STATE(
state: State @< The new state
)
}
In general, it is a good idea to state a definition inside a component when the definition logically belongs to the component. The name scoping mechanism emphasizes the hierarchical relationship and prevents name clashes.
XML limitation: In most cases, a qualified name such as Switch.State
in FPP becomes a qualified name such as Switch::State
when translating
to C++.
However, the F Prime XML format does not support the definition
of constants and types as members of C++ components.
Therefore, when translating the previous example through XML to C++,
the following occurs:
-
The component
Switch
becomes an auto-generated C++ classSwitchComponentBase
. -
The type
State
becomes a C++ classSwitch_State
.
Similarly, the FPP constant SerialSplitter.splitFactor
becomes a C++ constant SerialSplitter_SplitFactor
.
We will have more to say about this issue in the sections on
generating XML
and
generating C++.
9.10. Include Specifiers
Component definitions can become long, especially when there are many commands, events, telemetry channels, and parameters. In this case it is useful to break up the component definition into several files.
For example, suppose you are defining a component with
many commands, and you wish to place the commands in a
separate file Commands.fppi
.
The suffix .fppi
is conventional for included FPP files.
Inside the component definition, you can write the
following component member:
include "Commands.fppi"
This construct is called an include specifier.
During analysis and translation, the include specifier
is replaced with the commands specified
in Commands.fppi
, just as if you had written them
at the point where you wrote the include specifier.
This replacement is called expanding or resolving the
include specifier.
You can use the same technique for events, telemetry,
parameters, or any other component members.
The text enclosed in quotation marks after the keyword
include
is a path name relative to the directory of the
file in which the include specifier appears.
The file must exist and must contain component members
that can validly appear at the point where the include
specifier appears.
For example, if Commands.fppi
contains invalid syntax
or syntax that may not appear inside a component,
or if the file Commands.fppi
does not exist, then
the specifier include "Commands.fppi"
is not valid.
Include specifiers are perhaps most useful when defining components, but they can also appear at the top level of a model, inside a module definition, or inside a topology definition. We discuss include specifiers further in the section on specifying models as files.
9.11. Matched Ports
Some F Prime components employ the following pattern:
-
The component has a pair of port arrays, say
p1
andp2
. The two arrays have the same number of ports. -
For every connection between
p1
and another component instance, there must be a matching connection between that component instance andp2
. -
The matched pairs in item 2 must be connected to the same port numbers at
p1
andp2
.
In this case we call p1
and p2
a pair of
matched ports.
For example:
-
The standard Command Dispatcher component has matched ports
compCmdReg
for receiving command registration andcompCmdSend
for sending commands. -
The standard Health component has matched ports
PingSend
for sending health ping messages andPingReturn
for receiving responses to the ping messages.
FPP provides special support for matched ports.
Inside a component definition, you can write
match p1 with p2
, where p1
and p2
are the names of
port instances
defined in the component.
When you do this, the following occurs:
-
The FPP translator checks that
p1
andp2
have the same number of ports. If not, an error occurs. -
When automatically numbering a topology, the translator ensures that the port numbers match in the manner described above.
For example, here is a simplified version of the Health component:
@ Number of health ping ports
constant numPingPorts = 10
queued component Health {
@ Ping output port
output port pingOut: [numPingPorts] Svc.Ping
@ Ping input port
async input port pingIn: [numPingPorts] Svc.Ping
@ Corresponding port numbers of pingOut and pingIn must match
match pingOut with pingIn
}
This component defines a pair of matched ports
pingOut
and pingIn
.
10. Defining Component Instances
As discussed in the previous section, 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.
10.1. 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.
10.1.1. Passive Components
To instantiate a passive component, you write the following:
-
The keyword
instance
. -
The name of the instance.
-
A colon
:
. -
The name of a component definition.
-
The keywords
base
id
. -
An 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 command opcodes, event identifiers, telemetry channel identifiers, and 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:
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 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
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:
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.
XML limitation: The tool that generates the XML dictionary requires that each component instance I have a distinct base ID, even if I defines no dictionary elements.
10.1.2. 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:
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.
10.1.3. 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:
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
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:
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:
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:
instance dataCompressor: Utils.DataCompressor base id 0x100 \
queue size Default.queueSize \
cpu 0
10.2. Specifying the Implementation
When you define a component instance I, the FPP translator needs to know the following information about the C++ implementation of I:
-
The type (i.e., the name of the C++ class) that defines the implementation.
-
The location of the C++ 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 C++ class name matches the qualified
name of the FPP component.
For example, the C++ class name A::B
matches the FPP component
name A.B
.
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 string
specifying the implementation type.
For example, suppose we have a C++ 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
:
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
, whereName
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:
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 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 C++ 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:
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 C++ type name Svc::LinuxTime
defined in the header
file ../../Svc/LinuxTime/LinuxTime.hpp
.
10.3. Init Specifiers
In an F Prime FSW application, each component instance I
has some associated C++ 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 C++.