Supporting Subsystems Guide
A guide to creating component types to interact with hardware subsystems.
1. Overview
This guide provides best practices for developing component types which interact with hardware subsystems. Subsystems are semi-autonomous systems which a deployment instance must connect to and interact with. Typically these are hardware peripherals such as temperature sensors, GPS receivers or radios which flight software will interact with using hardware communications buses like I2C or CAN.
In the following section we discuss some best practice guidelines which we recommend you follow when writing flight software to interact with hardware subsystems. In particular we discuss how to outline the functionality of the component, and the best way to break down that functionality into helper functions.
We will then introduce three varieties of hardware subsystems which you are likely to encounter when writing flight software, and provide design patterns for interacting with them both. These varieties of subsystems are:
-
Subsystems requiring register-based access.
-
Subsystems which send data asynchronously to a listener.
-
Subsystems which send data as a response to commands.
To use register-based subsystems, your flight software must write to and read from the subsystem’s registers over a channel such as I2C. There may be timing constraints around which registers may be accessed, but interacting with this kind of subsystem is fairly simple using Flightkit. We will use the Texas Instruments TMP10x series of temperature sensors as an example to illustrate the register-based access pattern.
Receiving data asynchronously from a subsystem introduces some complexity into your flight software. After startup, the flight software must be ready to handle the incoming data as it arrives. This means resources must be available in a timely manner. The approach is fundamentally different to the controller-driven approach used to interact with a register-based subsystem. We will use the Novatel OEM6 as an example to illustrate the asynchronous listener subsystem pattern.
Finally, we will look at Command Response subsystems. These are similar to Listener subsystems in that they send data over packets which requires a sporadic task to handle, but they do so in response to a command packet. This means that there usually has to be some synchronisation mechanism between the sending task and the receiving task. In this case the example is a smaller section of a hypothetical generic component, as a lot of the implementation is the same as with Listener components.
2. Best Practice
Subsystem component types work best when they expose their functionality in a predictable, consistent way. This section will examine some best practices that should be followed when writing a subsystem component type. Following these guidelines is not mandatory, but is recommended.
Writing component types for each subsystem allows them to be neatly integrated with
the rest of a deployment. For example, we could set up cdh.tm.DataPool and
cdh.tm.Sampler component instances in the deployment to periodically sample
the temperature parameter exposed by a temperature sensor component type. This
can then raise events which other components can subscribe to, triggering actions
in response to a temperature outside of the suitable range.
2.1. Outline Component Functionality
One of the first things that should be done is establishing exactly how much
functionality the component should provide. Ideally a subsystem component should
be designed to provide access to the subsystem in the simplest way possible,
without compromising that functionality. Using the TMP100 as an example, we
want to expose parameters for accessing the current temperature reading and any
other settings that the hardware provides. In general we recommend against including
extra functionality, such as calculating a running average temperature, because
this is often better handled by a separate component type.
This does not mean subsystem component types should never add any functionality
to what the hardware provides. The TMP100 hardware will periodically track the
temperature by itself when not in one-shot mode, but other sensors explicitly
require a command before sampling. We could expose both the action to trigger a
conversion and a parameter to sample the temperature after this, but for periodically
sampling the temperature with something like the Sampler component it would
make more sense to have the conversion triggered automatically when getting the
temperature` parameter.
There is rarely an objectively correct way to do things: when in doubt, document
what you have done in the componentType.xml!
2.2. Write Helper Functions
Every subsystem is going to require some form of communication between the deployment instance and the peripheral hardware. Separate out a few helper functions for dealing with these. When implementing the register-based access pattern, there will be a few kinds of command bytes you can send and a few registers that you can read back, so a function to send a command with these as parameters will be helpful. Depending on how long the peripheral takes to process a command, the write and read functions may be wrapped together into one function.
You should also separate out defines for the various commands, required bit masks and so on. A good place for these is a private header file, to avoid proliferating the defines into other parts of the build that do not require them.
3. Common Patterns
As outlined in the Overview, two of the most common patterns that apply to subsystem components are the "register-access", and "listener" patterns.
3.1. Register-Based Access
3.1.1. Basic Explanation
A very common way for hardware subsystems to operate is via data registers. Commands and configuration are written to registers on the subsystem, and the result can be read back from a data register at will.
A component type using the register-based access pattern has the following structure:
-
A connection initialisation phase in which configuration registers may be written to. This puts the subsystem in the required state.
-
An
io.MASservice requirement used to access the hardware bus connected to the peripheral. MAS naturally supports accessing register-based subsystems (see the hardware interaction tutorial for more details.) -
Actions and parameters which write to and read from registers as required to fulfil their function.
-
Typically writing to a register involves a single MAS
writecontaining the register address and the data to write. -
Typically reading from a register involves a MAS
writecontaining the register address, following by a MASreadto read the contents of that register.
Additionally, there are several other things that are not explicitly required for a register-based subsystem, but can be done:
-
Parameters may be cached, allowing for a get operation to return something without immediately triggering a write/read to the hardware. This can be useful in cases where updating the parameters takes a long time.
-
Periodic or sporadic tasks can be used to update parameters in a non-blocking way.
A typical setup for a component like this would be to write any needed configuration
to the hardware during the connection initialisation phase, then handling any actions
with a register write, followed by a delay, followed by a register read. The component
will usually define an io.MAS requirement to allow another component to handle
whatever bus is being used to connect to the peripheral (I2C, SPI, etc), which
allows the subsystem component to mostly ignore timing requirements and just used
a fixed (or configurable) delay.
Some components cache values retrieved from the peripheral registers and return these when getting a parameter. This might be worth considering if the delay needed for a certain operation is quite long, on the order of milliseconds, since parameter access is blocking. For components like these you should expose the command needed to trigger the operation as an action, which can then triggered periodically from another component as needed. It is possible to build this functionality into the component itself, but should generally be avoided.
If the subsystem has different operating modes then these can be also exposed via parameters to allow for their use. If this changes what parameters or actions are valid to use then the component implementation can be changed to catch these by throwing errors or changing behaviour, but it is usually best to avoid adding too much complexity. It’s also quite common for a component implementation to just skip parameters or modes that are not needed for a particular use case to avoid the issue entirely. Functionality can always be added later.
3.1.2. Implementation
This section will go over the steps needed to implement a register-based subsystem
for the TMP100 temperature sensor. We can tell that this peripheral is a good
fit for a register-based component from the data sheet, which shows us
that we can connect to it over I2C and control it with register reads and writes.
3.1.2.1. componentType.xml
First we need to create the component and lay out the XML. Start by creating the
component with hfk so that you have a componentType.xml template to work with.
If you need to refresh yourself on how to do this, please see the Hardware Interaction
tutorials).
There are a few things that should be added to the xml before generating the component:
-
Required services. According to section
7.3.2.2in the data sheet,TMP100can be connected to via I2C with a default address of 0x48, so add anio.MASrequirement. -
Actions and parameters. The temperature reading from the subsystem can be accessed via two-byte temperature register, so a parameter that returns the reading would be ideal here.
-
Documentation is not strictly required, but it’s useful to at least have the default I2C address and a link to the data sheet in the documentation tags.
Let’s take a look at the parameter. Obviously it should not be possible to set the temperature, so declare it as read only. You might want to do some conversion on the value from the subsystem to convert it into a regular number before returning it, but in this case it is assumed that the value is passed back without any significant alteration.
Note that while the type is a 12 bit integer, internally Flightkit will treat this as a 16 bit number, so it should be sign extended before being returned.
Add a parameter for the temperature.
<!-- Parameters -->
<Parameters>
<Parameter name="temperature" readOnly="true">
<Description>
The current temperature in sixteenth-degree increments
</Description>
<Documentation><Markdown>
The temperature reported here is a signed value measured in
sixteenth-degree increments such that a value of 1 is equivalent to 0.0625
degrees C and a value of 16 is equivalent to 1 degree C.
</Markdown></Documentation>
<Type>
<Integer signed="true" bits="12"/>
</Type>
</Parameter>
</Parameters>
Now the component can be generated so that the actual implementation can be done.
3.1.2.2. Register Defines
Components handling subsystems often have to deal with a lot of register addresses
and flags, so it’s good practice to pull these out into a private header for the
component to use. These details are internal to the component and not something
that other components should have access to, so create this header under the
same directory as the main TMP100.c file.
This is not something that will be generated by tooling, so do it manually and add some commenting, dates or defines as you need.
Section 7.5 of the TMP100 data sheet details that the pointer register needs to be programmed with one of four addresses to access the other registers, so let’s define those in the new private header.
We will also define the number of bits in a temperature reading, since is is a twelve bit value stored in the top most significant bytes of a two byte register and we will need to shift it later.
Add register definitions.
#define __TMP100_PRIVATE_H__
/*---------------------------------------------------------------------------*
* Defines
*---------------------------------------------------------------------------*/
/*
* Register definitions
*/
/** The address of the temperature register (two bytes) */
#define TMP100_REG_TEMP (0)
/** The address of the configuration register (one byte) */
#define TMP100_REG_CONFIG (1)
/** The address of the low temperature register (two bytes) */
#define TMP100_REG_TLOW (2)
/** The address of the high temperature register (two bytes) */
#define TMP100_REG_THIGH (3)
/** The maximum length of a write is the address plus a two-byte value */
#define TMP100_MAX_WRITE_LEN (3)
/*
* Temperature register layout
*/
/** The number of bits in the temperature measurement */
#define TMP100_TEMP_BITS (12)
Register-based subsystems will also need a fixed delay for the I/O operations they require. If this never changes then it can also be included in the private header, but if it needs to be configurable then put it in the generated configuration header instead. Being able to configure the length of the delay can be useful for reducing the time it takes for unit tests to run on components that spend a lot of time delaying.
Add configuration variables.
/*---------------------------------------------------------------------------*
* Build configuration defines
*---------------------------------------------------------------------------*/
#ifndef TMP100_CONFIG_IO_TIMEOUT
/** The timeout to use for I/O operations, in microseconds. */
#define TMP100_CONFIG_IO_TIMEOUT (5000000)
#endif
#ifndef TMP100_CONFIG_READ_DELAY
/** The delay to apply between writing a command and reading the result.
* Measured in microseconds. */
#define TMP100_CONFIG_READ_DELAY (2500)
#endif
3.1.2.3. Register Access Functions
Now we can implement the actual functions for accessing the registers. We will also need a timeout object for the 'MAS' operations.
Add private functions for accessing registers.
/*---------------------------------------------------------------------------*
* Global variables
*---------------------------------------------------------------------------*/
/** The lock access timeout */
static const ShortTime_t gt_LockTimeout = TMP100_CONFIG_LOCK_TIMEOUT;
/** The I/O access timeout */
static const ShortTime_t gt_IOTimeout = TMP100_CONFIG_IO_TIMEOUT;
/*---------------------------------------------------------------------------*
* Private functions
*---------------------------------------------------------------------------*/
/**
* Read the specified number of bytes from a TMP100 device register
* @param pt_TMP100 The TMP100 component
* @param u8_Address The register address to read from
* @param u8_Length The number of bytes to read
* @param pu8_Buffer On successful exit, will be populated with the bytes read
* @return The success of the read
*/
static status_t readReg
(
TMP100_t *pt_TMP100,
uint8_t u8_Address,
uint8_t u8_Length,
uint8_t *pu8_Buffer
)
{
status_t t_Status; /* The current status */
uint32_t u32_Length; /* The length of the read */
/* Write the address */
t_Status = TMP100_MAS_writeBus(
pt_TMP100, >_IOTimeout, 0, 0, NULL, &u8_Address, 1);
/* If the write did not succeed return an I/O error */
if (t_Status == STATUS_SUCCESS)
{
/* Short delay for the device to process the read data */
t_Status = Task_Time_delayShortTime(TMP100_CONFIG_READ_DELAY);
}
/* If the delay succeeded, do a read */
if (t_Status == STATUS_SUCCESS)
{
/* Set up the length and do the read */
u32_Length = u8_Length;
t_Status = TMP100_MAS_readBus(
pt_TMP100, >_IOTimeout, 0, 0, NULL, pu8_Buffer, &u32_Length);
}
return t_Status;
}
This basic function can be used to read any of the registers using 'MAS' operations.
We initially start by writing to the control register to set the TMP100 read
pointer to the desired register.
TMP100_MAS_writeBus(pt_TMP100, >_IOTimeout, 0, 0, NULL, &u8_Address, 1)
The command written here is a byte corresponding to the addresses for the other registers defined in the data sheet. The memory address, ID and sequence parameters are used for different kinds of MAS operations and are not relevant here, so set them to 0 and NULL.
We have assumed that there will be a fixed delay between the write and read. The
delay needed can usually be determined from the data sheet- if there is no delay,
or it’s short enough, one call to writeReadBus can be made instead of two calls
with a delay between them.
We can also implement a similar function for writing to a register:
Write register helper.
/**
* write the specified number of bytes to a TMP100 device register
* @param pt_TMP100 The TMP100 component
* @param u8_Address The register address to write to
* @param u8_Length The number of bytes to write
* @param pu8_Buffer The bytes to write
* @return The success of the write
*/
static status_t writeReg
(
TMP100_t *pt_TMP100,
uint8_t u8_Address,
uint8_t u8_Length,
uint8_t *pu8_Buffer
)
{
status_t t_Status; /* The current status */
uint8_t ru8_Data[TMP100_MAX_WRITE_LEN]; /* The data to write */
/* Check the length is OK, must leave space for one address byte */
if (u8_Length >= TMP100_MAX_WRITE_LEN)
{
/* Error: invalid data length */
t_Status = STATUS_INVALID_PARAM;
}
else
{
/* Create the packet to write */
ru8_Data[0] = u8_Address;
memcpy(&ru8_Data[1], pu8_Buffer, u8_Length);
/* Do the write */
t_Status = TMP100_MAS_writeBus(
pt_TMP100, >_IOTimeout, 0, 0, NULL, &ru8_Data[0], u8_Length 1);
}
return t_Status;
}
In this case the address value is just included in the same packet as the data. There is no wait in this case as we do not read back a value here.
3.1.2.4. Implementing Parameter Accessors
Now that the functions for accessing the registers are in place we can move on
to providing access to the actual data. This is done by filling out the generated
getter functions that have been produced by HFK tooling.
Details
status_t TPM100_getTemperature
(
TPM100_t *pt_TPM100,
int16_t *ps16_Value
)
{
status_t t_Status; /* Current status */
uint8_t ru8_Raw[2]; /* The two-byte temperature */
uint16_t u16_Raw; /* The raw measurement value */
TASK_PROTECTED_START(&pt_TMP100->t_Lock, >_LockTimeout, t_Status)
{
/* Attempt to read the temperature register value */
t_Status = readReg(pt_TMP100, TMP100_REG_TEMP, sizeof(ru8_Raw), &ru8_Raw[0]);
}
TASK_PROTECTED_END(&pt_TMP100->t_Lock, >_LockTimeout, t_Status);
/* If the read was successful, convert the value */
if (t_Status == STATUS_SUCCESS)
{
/*
* The measurement value is a 2s complement signed number in the
* top TMP100_TEMP_BITS of the 16-bit value. To make it a valid
* signed 16-bit number we need to shift it right and sign extend.
*/
/* Get the value and shift it */
Util_Endian_u16FromNetwork(&ru8_Raw[0], &u16_Raw);
u16_Raw >>= (16 - TMP100_TEMP_BITS);
/* Sign extend the raw value */
if ((u16_Raw & (1 << (TMP100_TEMP_BITS - 1))) != 0)
{
u16_Raw |= ~((1 << TMP100_TEMP_BITS) - 1);
}
/* Pass the value back casting to a signed 16-bit number as we have
* properly sign-extended the raw value */
*ps16_Value = (int16_t)u16_Raw;
}
return t_Status;
}
Note that we have to sign-extend the the value, as it’s being put into a 16 bit
variable. The call to the readReg function is also wrapped in a task protection
block to prevent multiple read operations from interrupting each other.
The data returned from the raw read function is also in network endian order, so
we need to use the Util_Endian functions to convert it to local endianness.
That should be the bare essentials required for a register-based subsystem
component. We’ve defined the scope of the component, written the componentType.xml,
created handler functions to access the registers and then put them to use in the
parameter accessor functions. From here the component should build when run in a
deployment, or when running unit tests for it.
3.2. Listener Pattern
3.2.1. Basic Explanation
Some subsystems operate by periodically sending packets of data as they become available. This is significantly different from register based access as the subsystem is not passive, and the component will need to be able to handle data as it arrives at intervals that may not be fixed. For this it makes sense to employ the listener pattern.
A typical Listener component will have the following:
-
An
io.PSservice requirement for incoming packets from the subsystem. -
A sporadic task to handle this incoming data.
-
A receive handling function called from the receive task. This can include state tracking, depending on the complexity of the data.
-
Cached values that keep the data from incoming packets and are accessed via the component parameters.
They may also need a time.TAS provision for logging, or for timeouts during
receive handling.
It’s also quite common for Listener components to declare events which are raised when data arrives. This allows other components to subscribe to these events and update automatically when the cached parameters are updated.
3.2.2. Implementation
Before we start, this is a fairly broad overview of a component requiring io.PS.
If you need more in-depth instruction on how to handle PS transactions, see
hardware
interaction 2.
This section will go through the steps needed to implement a listener pattern
subsystem for the OEM6 GPS receiver. This piece of hardware uses a serial
interface and can periodically log data based on a configurable delay, making it
ideal for the listener design pattern.
We will be ignoring any commands needed to set up and configure the OEM6, and
just focus on the parts of the component that are relevant to the listener pattern.
3.2.2.1. componentType.xml
Start by creating the component and laying out the XML.
The scope of this guide is not to walk through the full process of creating an entire component type, so we will omit most of the documentation, exceptions and so on. We will be looking at the basic steps needed to create the backbone of a command and response component.
To implement this listener component the following will need to be added to the xml:
-
An 'io.PS' requirement for incoming data.
-
A sporadic task requirement to handle that data.
-
Parameters to access the cached data from incoming packets.
Additionally, we’ll add some elements to allow the component to raise events when new data comes in:
-
A declaration for the event that will be raised when cached parameters are updated.
-
A published service that will allow other components to subscribe and receive this event.
Hardware Interaction 2 has already covered how to declare sporadic tasks, so
we will not go into detail on this. Just add the io.PS and sporadic task
requirements.
There are plenty of parameters that could be logged from the 'OEM6'. For this example we will look at the longitude, latitude and altitude values, which are packed together in the same packet. Each value is a 64 bit real number, and each row in the parameter corresponds to latitude, longitude and altitude respectively.
LLA parameter.
<!-- Parameters -->
<Parameters>
<Parameter name="lla" readOnly="true">
<Description>
The Latitude, Longitude and Altitude value from the GPS
</Description>
<Documentation><Markdown>
Each row is a IEEE double precision floating point value
- Row 0: Latitude
- Row 1: Longitude
- Row 2: Altitude
</Markdown></Documentation>
<Type>
<List fixed="true" maxLength="3">
<Type>
<Real bits="64"/>
</Type>
</List>
</Type>
</Parameter>
</Parameters>
If a component declares events then it also needs to declare a default base ID. These are used to determine the individual IDs of the declared events. This component is only declaring one event, which will have an ID of 300, but if it declared more then they would end up with IDs of 301, 302, and so on.
Severity can range from info to warning to error. As this event is just
informational, it has info level severity.
Events.
<!-- Events -->
<Events defaultBaseId="300">
<Event severity="info" name="newData">
<Description>
New latitude/longitude/altitude data.
</Description>
</Event>
</Events>
It’s not enough to just declare the events. For other components to subscribe to
and receive them this component also needs to publish event messages using the
component.EDS service.
Published Services
<!-- Services (Provided, Required, Subscribed, Brokered and Published) -->
<Services>
<Published>
<Service name="GPSInfo" type="component.EDS">
<Description>
Publishes EDS.Event messages relating to incoming GPS packets.
</Description>
</Service>
</Published>
</Services>
Now the component can be generated to produce the source files.
3.2.2.2. Packet Defines
Before diving into the implementation of the component, it’s a good idea to check
the data sheet to see what the structure of the packets that the subsystem sends
will look like. Like with the register-based pattern, relevant constants such as
the position of various bytes in the packet should be pulled out and put into a
private header. This is not produced automatically by the HFK tooling, so create
the file yourself in the same directory as the main c file for the component.
Private command defines
/*
* Sizes
*/
/** The size of the binary packet header */
#define OEM6_HEADER_SIZE 28
/** The size of the data (plus the CRC) for RxStatusEvent */
#define OEM6_DATA_RXSTATUSEVENT_SIZE 48
/** The size of the data (plus the CRC) for BESTPOS */
#define OEM6_DATA_BESTPOS_SIZE 72
/** The maximum size of a received packet (+header) */
#define OEM6_RX_MAX_PACKET_SIZE \
(OEM6_DATA_BESTPOS_SIZE + OEM6_HEADER_SIZE)
/*
* Indices
*/
/** Index for Sync 1 */
#define OEM6_INDEX_HDR_SYNC1 0
/** Index for Sync 2 */
#define OEM6_INDEX_HDR_SYNC2 1
/** Index for Sync 3 */
#define OEM6_INDEX_HDR_SYNC3 2
/*
* Byte defines
*/
#define OEM6_STATE_POS_GG_MASK
/** Sync byte 1 */
#define OEM6_SYNC1 0xAA
/** Sync byte 2 */
#define OEM6_SYNC2 0x44
/** Sync byte 3 */
#define OEM6_SYNC3 0x12
/** Message ID BestPos */
#define OEM6_MSG_ID_BESTPOS 42
/** Message ID RxStatusEvent */
#define OEM6_MSG_ID_RXSTATUSEVENT 94
/** Message ID BestPos */
#define OEM6_MSG_ID_BESTPOS 241
3.2.2.3. Receive Transaction Queues and Receive Handling
The first part of handling incoming data is to fill in the receive task. This should have been automatically generated as an empty function.
The important thing is to make sure that the data buffer from the transaction
queue is passed to the custom handleRxData function that we’re about to cover.
Most of this code is going to be header parsing: see section 1.1.3 in the data
sheet for the message format.
As the receive buffer provided by the 'io.PS' provider is not guaranteed to always contain a full or error free packet, the receive handling will need to be able to build up a packet over multiple calls. There are two main approaches to this: try to parse an entire packet first using the packet length before evaluating it with the CRC, or deal with each byte as they come and track the current index or state.
The latter tends to be best for cases where the possible length and types of packets are quite constrained, which is often the case with simpler subsystem peripherals. If a byte comes in at an unexpected time, or indicates a type of packet that we simply do not care about, the receive handler can immediately discard it and begin searching for the start of the next packet. The trade off is that this can become unwieldy as the size and complexity of incoming packets increases, leading to large amounts of states to check.
For this example the byte by byte approach will be used. To do this the component needs enumeration states for each byte in the header. There can be a lot of these for more complex headers, so most of the details here will be omitted.
State enumeration
/** The different states for receiving a packet */
typedef enum
{
/* Binary Header */
EM6_STATE_HDR_SYNC1,
EM6_STATE_HDR_SYNC2,
EM6_STATE_HDR_SYNC3,
EM6_STATE_XYZ_VEL_XD,
EM6_STATE_XYZ_VEL_YD,
[...]
EM6_STATE_XYZ_SAT_SOL,
EM6_STATE_XYZ_SAT_L1,
EM6_STATE_XYZ_SAT_MULTI,
EM6_STATE_XYZ_EXT_SOL,
EM6_STATE_XYZ_GBD_MASK,
EM6_STATE_XYZ_GG_MASK,
/* CRC */
EM6_STATE_CRC
}
OEM6_RxPacket_State_t;
The actual state of the parsed message needs to be held in the component struct. Here you can see that there is a variable that keeps track of the current state using the state enums, as well as the current length of the parsed message and the current offset of the parser. The component also holds the parsed LLA data here until the full packet comes in, at which point it can be made available to the parameter getters.
State variables.
/** The OEM6 component */
typedef struct
{
/** Initialisation data */
const OEM6_Init_t *pt_Init;
/** A protection lock to protect the OEM6 state */
Task_ProtectionLock_t t_Lock;
/** Keeps track of the current state */
OEM6_RxPacket_State_t t_RxState;
/** Length of parsed message */
uint16_t u16_ParsedLength;
/** ID of parsed message */
uint16_t u16_ParsedMsgId;
/** The parser offset */
uint8_t u8_RxOffset;
/** Untouched LLA data, built up as data comes in */
uint8_t rru8_ParsedLLA[GPSOEM6_LLA_ROWS][sizeof(float64_t)];
}
OEM6_t;
3.2.3. Parsing Helper Functions
Now comes the function for handling the actual receive data. This should be called from the sporadic task function when incoming data arrives.
The handler function decides what to do with the byte based on the current state of the component. The header parsing function will step through the header bytes and, if the header is valid and something that we are interested in checking for, update the state of the function accordingly.
Receive handler function.
/**
* Handles data received over serial
* @param pt_OEM6 The component
* @param pu8_RxBuffer The buffer containing the received bytes
* @param u32_RxSize The size of the buffer
* @return Whether the data was successfully handled
*/
static status_t handleRxData
(
OEM6_t *pt_OEM6,
uint8_t *pu8_RxBuffer, /* The receive buffer */
uint32_t u32_RxSize /* The receive data size, in bytes */
)
{
status_t t_Status; /* Current status */
uint8_t u8_Data; /* The current data byte */
uint32_t u32_CurrByte;
/* No errors have occurred yet */
t_Status = STATUS_SUCCESS;
/* Go through all bytes in the receive buffer, stop when we run out of data
* or we have successfully extracted a packet */
u32_CurrByte = 0;
while (u32_CurrByte != u32_RxSize)
{
/* Extract the current data byte */
u8_Data = pu8_RxBuffer[u32_CurrByte];
if (pt_OEM6->t_RxState <= OEM6_STATE_HDR_SW)
{
handleRxHeader(pt_OEM6, u8_Data);
}
else if (pt_OEM6->t_RxState <= OEM6_STATE_POS_GG_MASK)
{
handleRxBestPos(pt_OEM6, u8_Data);
}
else
{
UTIL_LOG_ERROR("Unknown state: %u", pt_OEM6->t_RxState);
pt_OEM6->t_RxState = OEM6_STATE_HDR_SYNC1;
pt_OEM6->u8_RxOffset = 0;
break;
}
/* Move on to the next byte in the receive buffer */
u32_CurrByte = 1;
}
return t_Status;
}
When the header is done parsing it sets the next state based on the ID of the incoming message. Since we’re ignoring anything other than the positional data there is only one option here- any other message will result in the packet being discarded.
Header parsing function.
static void handleRxHeader
(
OEM6_t *pt_OEM6,
uint8_t u8_Data
)
{
status_t t_ObtStatus;
uint8_t * const pu8_LogTime =
&pt_OEM6->ru8_Log[OEM6_LOG_INDEX_TIME];
switch(pt_OEM6->t_RxState)
{
case OEM6_STATE_HDR_SYNC1:
/* Make sure the CRC is clear and start building it up */
pt_OEM6->u32_ComputedCrc = 0;
pt_OEM6->u8_RxOffset = 0;
expectByte(pt_OEM6, u8_Data, OEM6_SYNC1, OEM6_STATE_HDR_SYNC2);
break;
[...]
case OEM6_STATE_HDR_ID:
parseU16(pt_OEM6, u8_Data, &pt_OEM6->u16_ParsedMsgId, OEM6_STATE_HDR_TYPE);
break;
[...]
case OEM6_STATE_HDR_SW:
if (pt_OEM6->u16_ParsedMsgId == OEM6_MSG_ID_BESTXYZ)
{
skipField(pt_OEM6, u8_Data, 4, OEM6_STATE_XYZ_POS_SOL);
}
else
{
/* Unknown packet, go back to sync */
skipField(pt_OEM6, u8_Data, 4, OEM6_STATE_HDR_SYNC1);
}
break;
default:
pt_OEM6->t_RxState = OEM6_STATE_HDR_SYNC1;
pt_OEM6->u8_RxOffset = 0;
break;
}
}
Now we put together the handling for the positional data packets. We are only interested in the position data here. Normally we would want to check the CRC value before accepting the packet and copying the received data into the cached parameter, but to keep things simple it is just done as soon as the data has arrived here.
BestPos function.
static void handleRxBestPos
(
OEM6_t *pt_OEM6,
uint8_t u8_Data
)
{
uint32_t u32_SolState = 0;
switch(pt_OEM6->t_RxState){
[...]
case OEM6_STATE_POS_LAT:
parseRaw(pt_OEM6, u8_Data, 8, pt_OEM6->rru8_ParsedLLA[0], OEM6_STATE_POS_LONG);
break;
case OEM6_STATE_POS_LONG:
parseRaw(pt_OEM6, u8_Data, 8, pt_OEM6->rru8_ParsedLLA[1], OEM6_STATE_POS_ALT);
break;
case OEM6_STATE_POS_ALT:
parseRaw(pt_OEM6, u8_Data, 8, pt_OEM6->rru8_ParsedLLA[2], OEM6_STATE_POS_UND);
break;
[...]
case OEM6_STATE_POS_GG_MASK:
skipField(pt_OEM6, u8_Data, 1, OEM6_STATE_CRC);
/* Usually the parsing of the LLA values and raising of the event should
* happen after confirming that the CRC value matches, but we will skip
* this for the sake of brevity and just do that now. */
/* Extract the parsed LLA from its little endian raw form */
for (u8_Index = 0; u8_Index < OEM6_LLA_ROWS; u8_Index += 1)
{
Util_Endian_f64FromLittle(
pt_OEM6->rru8_ParsedLLA[u8_Index],
&pt_OEM6->rf64_LLA[u8_Index]);
}
/* Raise an event to indicate that the cached LLA parameter has been
* updated. We'll ignore the return code, again to keep things short.
* We haven't included any data in the actual message itself. */
(void)OEM6_EDSPublisher_publishEventGPSInfo(
pt_OEM6,
OEM6_EVENTID_NEWDATA,
STATUS_SUCCESS,
NULL,
0);
break;
default:
/* Reset state */
pt_OEM6->t_RxState = OEM6_STATE_HDR_SYNC1;
pt_OEM6->u8_RxOffset = 0;
break;
}
}
That’s the core of a listener pattern component. To recap, this component has
an io.PS requirement for incoming data, a sporadic task to handle it, and
stateful receive handling to update cached parameter values with incoming data as
it arrives.
3.3. Command Response Pattern
3.3.1. Basic Explanation
Some systems operate by sending data in response to commands. These responses can be something simple, like an acknowledgement, or something more complex, like the value of some parameter.
Command response pattern subsystems tend to have a lot of overlap with Listener subsystems, as they will require a receive handler to listen for incoming responses. However, component types expecting a response need to be able to actively listen for one and pair it to the initiating command, rather than passively listening for anything coming from the subsystem.
A typical Command Response component will have the following:
-
An
io.PSservice requirement for incoming packets from the subsystem. -
A sporadic task requirement to handle this incoming data.
-
A receive handling function to be called from the task handler.
-
A command function that blocks while waiting for a response.
-
A mechanism for synchronising the command function and the receive handler.
When sending a command, the command function should send the command and any data
via the io.PS service provision, and then block while waiting on a transaction
from whichever task synchronisation method is being used. When the receive
task receives a response packet, it unblock the command function, which can
then evaluate the response.
Not every command in a Command Response pattern component necessarily needs to expect a response, so in some cases the whole process of blocking and waiting can be skipped. Additionally, the receive handling task does not have to exclusively listen for responses: components can combine elements from this and the Listener pattern, as long as the receive handler can differentiate between command packets and unprompted data packets.
The blocking elements of the command functions should make use of configurable timeouts as well, since it’s possible for packets to be lost or corrupted.
3.3.2. Implementation
The Command Response pattern is a superset of the functionality in the Listener Pattern. The first step in implementing a Command Response component type is therefore to implement a Listener component type with receive parsing functionality to handle responses from the subsystem.
This section then describes implementation of the command helper functions, and the required additions to a Listener component type to synchronise the command sending function with the response handling function.
We make the following assumptions about the subsystem:
-
Command codes are 1-byte.
-
Commands have the same timing behaviour.
-
All data received from the subsystem is sent in response to a command.
-
The length of the received data is predictable and well bounded.
3.3.2.1. Required Component State
A Command Response component needs some extra state tracking to keep the sending and receiving tasks working together.
Here we declare a sized response buffer to hold incoming data from a command response.
Response type structure
/** Sized buffer to contain command responses. */
typedef struct
{
/** The buffer to put the data into */
uint8_t *pu8_Buffer;
/** The current size of the buffer (as input and output) */
uint16_t u16_BufferSize;
}
GenericComponent_ResponseBuffer_t;
The response request struct can then be included in the structure for the full component. Depending on your preference you could also just directly include the buffer within the component struct directly, but it helps keep things simple when there are a lot of things to keep track of.
The component struct also includes a task synchronisation object for synchronisation between the command sending task and the receive handling task.
There are a pair of locks for protecting parts of the command-response process. The buffer lock prevents the response buffer from being accessed by the sending task and the receiving task at the same time: the lock will be held in the send function while the buffer is cleared, released while it blocks so that the receive task can acquire it, and then re-acquired so that the contents can be read.
The other lock is to protect the end to end command response process as a whole. Without this, separate send commands could effectively interrupt each other and contest the receive buffer.
Component structure
typedef struct
{
/** Initialisation data */
const GenericComponent_Init_t *pt_Init;
/** A protection lock to protect the command response process */
Task_ProtectionLock_t t_CommandResponseLock;
/** A protection lock to protect response buffer access */
Task_ProtectionLock_t t_BufferLock;
/** The current response request */
GenericComponent_ResponseBuffer_t t_ResponseRequest;
/** Task synchronisation object for the command response task */
Task_Synchronisation_t t_CommandResponseSync;
}
GenericComponent_t
The synchronisation object behaves like a binary semaphore and is sufficient for this example. More complex Command Response component types may need to pass response information back from the listening context to the sending context. If so, using a task queue or other data structure may be more appropriate.
With the component type structure updated the command sending functions can be implemented.
3.3.2.2. Command Sending Helper Functions
Command Response component types should define helper functions for sending
commands and expecting a response. The helper for sending commands is fairly
simple and just involves sending a packet via the io.PS provision. If no
response is expected then that’s it, it’s done.
Send command example
static status_t sendCommand
(
GenericComponent_t *pt_GenericComponent,
uint8_t u8_Command,
const uint8_t *pu8_CmdData,
uint32_t u32_CmdDataSize
)
{
status_t t_Status = STATUS_SUCCESS;
const ShortTime_t t_SendTimeout = GENERIC_CONFIG_SHORT_TIME;
if (u32_CmdDataSize + 1 > GENERIC_PACKET_SIZE_MAX_REQUEST)
{
t_Status = STATUS_INSUFFICIENT_RESOURCES;
}
if (t_Status == STATUS_SUCCESS)
{
/** The current response request */
uint8_t ru8_CommandPacket[GENERIC_PACKET_SIZE_MAX_REQUEST];
/* Create the packet */
ru8_CommandPacket[0] = u8_Command;
memcpy(
ru8_CommandPacket[1],
pu8_CmdData,
u32_CmdDataSize);
/* Send the command */
t_Status = GenericComponent_PS_sendBus(
pt_GenericComponent,
&ru8_CommandPacket[0],
u32_CmdDataSize + 1,
&t_SendTimeout);
}
return t_Status;
}
For commands that do provide a response, we can reuse the basic sending function,
but we need to block until the reply arrives. After sending the command, this
task needs to use a synchronisation mechanism to wait for the receive handler:
in this case Task_Synchronisation_wait can be used with the synchronisation
object to block until the receive handler signals that a reply packet has arrived.
After receiving the response the buffer should be checked to see that it matches the expected output for the command.
Note that if you use timeouts with your synchronisation mechanism then it should be cleared before blocking with it. This will prevent any signals from previous late responses interfering with the current command.
Send command with response example
static status_t sendCommandWithResponse
(
GenericComponent_t *pt_GenericComponent,
uint8_t u8_Command,
const uint8_t *pu8_CmdData,
uint32_t u32_CmdDataSize,
uint8_t *pu8_RxBuffer,
uint16_t u16_RxBufferSize
)
{
status_t t_Status = STATUS_SUCCESS; /* The current status */
if ((pu8_RxBuffer == NULL))
{
/* Need to have a buffer to write into. */
t_Status = STATUS_INVALID_PARAM;
}
if (t_Status == STATUS_SUCCESS)
{
ShortTime_t t_Timeout = 0;
/* Clear any signals from late responses before trying to send a
* command. A timeout of 0 will cause it to immediately return even if
* there is no pending signal. Since we are only interested in clearing
* signals we just discard the return value */
(void)Task_Synchronisation_wait(
&pt_GenericComponent->t_CommandResponseSync, &t_Timeout);
}
/* Acquire the lock while we clear the buffer */
TASK_PROTECTED_START(
&pt_GenericComponent->t_BufferLock, >_LockTimeout, t_Status)
{
if (t_Status == STATUS_SUCCESS)
{
/* Set up the buffer to be ready for the response. */
pt_GenericComponent->t_ResponseRequest.u16_BufferSize = u16_RxBufferSize;
pt_GenericComponent->t_ResponseRequest.pu8_Buffer = pu8_RxBuffer;
}
if (t_Status == STATUS_SUCCESS)
{
/* Pass through the data for the send command. */
t_Status = sendCommand(
pt_GenericComponent, u8_Command, pu8_CmdData, u32_CmdDataSize);
}
}
TASK_PROTECTED_END(
&pt_GenericComponent->t_BufferLock, >_LockTimeout, t_Status);
if (t_Status == STATUS_SUCCESS)
{
/* Wait for the receive task to get a response. This will be triggered
* by the rx handling task. */
t_Status = Task_Synchronisation_wait(
&pt_GenericComponent->t_CommandResponseSync, >_ResponseTimeout);
if (t_Status == STATUS_TIMEOUT)
{
GENERIC_COMPONENT_CONTAINER_LOG_ERROR(
&pt_GenericComponent,
"No response received before timeout");
}
}
if (t_Status == STATUS_SUCCESS)
{
/* Reacquire the lock and handle the response */
TASK_PROTECTED_START(
&pt_GenericComponent->t_BufferLock, >_LockTimeout, t_Status)
{
/* Now do whatever needs to be done with the packet- check that the
* packet is valid, that the data in the buffer is appropriate to the
* command, and so on. */
t_Status = handleIncomingResponse(
pt_GenericComponent, &pt_GenericComponent->t_ResponseRequest);
}
TASK_PROTECTED_END(
&pt_GenericComponent->t_BufferLock, >_LockTimeout, t_Status);
}
return t_Status;
}
Note that these two helper functions are stateful and will not function correctly with multiple calls interrupting each other, so be sure to wrap them with the other lock when they are called.
Locking section example
status_t GenericComponent_setEnabled
(
GenericComponent_t *pt_GenericComponent,
const bool b_Enabled
)
{
status_t t_Status;
uint8_t u8_Cmd = CMD_ENABLE;
uint8_t ru8_ResponseBuffer[1];
uint16_t u16_ResponseBufferSize = 1;
TASK_PROTECTED_START(
&pt_GenericComponent->t_CommandResponseLock, >_LockTimeout, t_Status)
{
t_Status = sendCommandWithResponse(
pt_GenericComponent,
u8_Cmd,
1,
ru8_ResponseBuffer,
&u16_ResponseBufferSize);
}
TASK_PROTECTED_END(
&pt_GenericComponent->t_CommandResponseLock, >_LockTimeout, t_Status);
/* Not going to do anything with the actual response value, but here you could
* raise an event, set a return value, etc */
return t_Status;
}
3.3.2.3. Receive Handler
Most of the implementation details of the receive handling are the same as for the Listener Pattern, so this section will just focus on the main differences.
The most important thing, after verifying that the incoming data is a valid
response packet, is to use the task synchronisation mechanism to unblock the
task that sent the command in the first place. In this case
Task_Synchronisation_signal allows the original task to unblock and use the
contents of the filled receive buffer.
Locking section example
static void processRxPacket
(
GenericComponent_t *pt_GenericComponent,
uint8_t *pu8_ResponsePacket,
uint16_t u16_ResponseSize
)
{
status_t t_Status;
/* Acquire the lock while filling the receive buffer */
TASK_PROTECTED_START(
&pt_GenericComponent->t_BufferLock, >_LockTimeout, t_Status)
{
/* Errors here need to be propagated back to the command task,
* usually with a state variable or similar. In this case we are
* not too bothered about these details and will just truncate the
* response so it can't overflow the buffer */
if (u16_ResponseSize <
pt_GenericComponent->t_ResponseRequest.u16_BufferSize)
{
u16_ResponseSize =
pt_GenericComponent->t_ResponseRequest.u16_BufferSize;
GENERIC_COMPONENT_CONTAINER_LOG_ERROR(
&pt_GenericComponent, "Response too large, truncating");
}
/* Copy the received data into the responseRequest buffer */
memcpy(
&pt_GenericComponent->t_ResponseRequest.pu8_Buffer[0],
pu8_ResponsePacket,
u16_ResponseSize);
pt_GenericComponent->t_ResponseRequest.u16_BufferSize =
u16_ResponseSize;
}
TASK_PROTECTED_END(
&pt_GenericComponent->t_BufferLock, >_LockTimeout, t_Status);
/* Unblock the original sending task */
Task_Synchronisation_signal(
&pt_GenericComponent->t_CommandResponseSync, >_ResponseTimeout);
return t_Status;
}
Note that this implementation assumes that all incoming packets will be some kind of response. For a mixed Command Response and Listener component type there should be some kind of check for what kind of packet has been received, to avoid unblocking the command task for non-response packets.
That should be the extent of what’s needed for a command response pattern component.
To recap:
-
Our component sends commands over an
io.PSprovision. -
It blocks until a response packet comes in.
-
The handling of collected response data comes from a sporadic task.
-
The sporadic task unblocks the command function once a full response is received.
-
The data from the response can then be used to provide data for whatever action or parameter request triggered the operation.