Hardware Interaction 1: MAS
Introduction
In this tutorial we introduce the Memory Access Service (MAS), and show how it is used within Flightkit to access hardware.
To demonstrate this you will develop a Component Type to run on a Raspberry Pi Zero W which communicates with an attached light sensor using MAS.
|
While this tutorial produces a Component Type for running on a Raspberry Pi Zero W, the ideas and techniques introduced are useful for developing Component Types which interact with subsystems on any of the platforms which Flightkit supports. |
For simplicity, whenever we reference the Raspberry Pi Zero W in this tutorial, we will refer to it as the "Pi".
|
In this tutorial, you will:
|
Before You Begin
Guidance on the styles and conventions used in these tutorials is available here.
Unless otherwise directed, all commands in this tutorial must be run in the developing_components/HardwareInteraction1 workspace.
You must have Flightkit installed as described in the Getting Started Guide.
Navigate to the developing_components/HardwareInteraction1 workspace in a terminal and run hfk workspace prepare to ensure it is ready to use.
If you haven’t already, follow the steps in our Raspberry Pi Zero How-to Guide to set up your Pi and install the correct toolchain in order to complete this tutorial.
Memory Access Service (MAS)
In space systems it is common for different functionality to be divided between independent subsystems. Typically these subsystems will be connected to onboard computers (OBCs) which monitor and control them.
There are a variety of interaction patterns which might be used to monitor and control subsystems. A common one is a "request-response" or "controller-peripheral" pattern.
In this interaction pattern:
-
The controller (OBC) initiates an interaction by issuing a request to the peripheral (the subsystem).
-
The request is either a request to write data to the subsystem (a command or register value, for example) or read data from the subsystem (some telemetry values, perhaps).
-
The peripheral never sends unprompted data to the controller. It is only permitted to send data in response to requests received from the controller.
MAS is a Service which supports this interaction pattern, and specifically contains Service Operations for:
-
Writing data to a peripheral.
-
Reading data from a peripheral.
-
Sequentially or simultaneously writing and reading data to and from a peripheral.
Typically Component Types which represent the high-level capabilities of this kind of subsystem will consume MAS. Component Types supporting interactions with hardware which follow this pattern (like I2C or SPI drivers) will provide it.
|
In the earlier Using a Service tutorial, you learned how the File Store Service (FSS) is a contract which defines Operations for writing to and reading from files in a POSIX-like filesystem. MAS defines Operations for writing to and reading from hardware, rather than files, but can be thought of in very similar terms. |
Analysis and Design
In the following sections this tutorial will guide you through implementing a new Component Type to interact with the Pi’s light sensor.
This Component Type will:
-
Use Actions and Parameters to represent the high-level functionality of the sensor,
-
Consume MAS, and use it to make requests of the hardware.
-
Need to be connected to a MAS Provider once deployed. This MAS Provider will send hardware requests to the sensor.
This leaves some questions to answer. What kind of functionality does the sensor have? What requests can we make of it, and how? What MAS Provider should we use to issue those requests?
To answer these questions you will need to understand how the hardware operates. Without this understanding, designing and developing a Component Type like this is impossible.
The Peripheral
First consider the peripheral itself.
In this tutorial the aim is to interact with a specific light sensor connected to the Pi - an M5 DLight Unit. This is an integrated light sensor built around the BH1750FVI ambient light sensor IC.
Technical detail about this device can be found in its datasheet, available online here.
The datasheet includes a lot of information. Much of it concerns the physical and electrical properties of the sensor. These are not relevant to your task of interacting with it from software.
Of more interest to you are the details of its data interface, and how to use it to measure light levels.
The datasheet states that the sensor uses an I2C interface. I2C is a physical layer protocol which is a common choice for communicating with sensors like this one. Importantly, it closely matches the "request-response" pattern supported by MAS.
The data gives further information on the interface which you’ll need to implement your Component Type:
-
Page 4 includes a state diagram showing that "one time" or "continuous" measurement can be carried out.
-
Page 5 provides a table of instructions which can be sent to the sensor.
-
Page 7 gives step-by-step procedures for requesting a measurement and reading it back from the sensor.
-
Page 10 gives more detail on the I2C interface, including the 2 addresses the sensor uses.
Assuming a one time measurement is sufficient, use the datasheet to plan what steps your Component Implementation will need to carry out.
Show the procedure which your Component Implementation will use.
-
Write the "one time H-resolution mode" command via I2C to the sensor.
-
Wait 180ms to allow the measurement to complete.
-
Read the measurement from the sensor.
The simplicity of this procedure means you will be able to give your component type a single read-only Parameter for getting the light level.
The Controller
The other piece of hardware you need to consider is the controller. In this case the controller is the Broadcom Serial Controller (BSC) onboard the Pi, which is designed for writing and reading data to and from an I2C bus.
As with the peripheral in the previous section, you can find out more about how this controller works by referring to its datasheet.
If you were starting from scratch, you would need to write a Component Type which provided MAS. It would need to provide MAS Operations by manipulating the BSC such that a MAS write would trigger an I2C write, for example.
To save time here you will use our I2CController Component Type provided in
the FlightkitFoundation bundle. This Component Type Provides MAS in
exactly the way you require.
The Connected System
The following sketch shows how our Component Types connect with the hardware on the board to enable interaction with the BH1750FVI subsystem.
Notice how the I2CController Component Type uses MAS to represent the
general capabilities of the Broadcomm Serial Controller.
In a similar way, notice how the BH1750FVI Component Type represents the
capabilities of the BH1750FVI sensor with Actions and Parameters.
In constructing the system like this, and using MAS between the Component Types,
they are decoupled. Importantly, the BH1750FVI Component Type you are about
to implement can be instantiated on any platform with a Component Type which
Provides MAS and uses it to give access to an I2C bus.
Now that the key pieces of hardware in the system have been identified, you can
move on to implementing the new BH1750FVI component type.
The BH1750FVI Component Type
The outcome of the last section was that, in order to access the Pi’s light sensor, you will need to create a new Component Type.
As you have learned from previous tutorials, the first step in producing a new
Component Type is to define its interfaces and characteristics in its
componentType.xml.
|
When developing new Component Types it is often best to think carefully about how to define them before thinking too hard about how to implement them. This approach ensures the Component Type you produce will meet your needs, and fit into the system neatly. While some iteration between definition and implementation is usually needed, more time spent at the definition stage tends to reduce the time spent at the implementation stage. |
We have suggested you name it BH1750FVI after the specific device it
supports. Naming it LightSensor might be easier on the eye, but it would be
unclear exactly which light sensor it can communicate with.
Definition
Create the Component Type using the following command:
hfk component-type create BH1750FVI
This creates a componentType.xml for your new Component Type in the
library/component_types/BH1750FVI directory.
Open the componentType.xml file.
Update the description and documentation elements to give more detail about what the Component Type does:
<Description>
This component provides an interface to the BH1750FVI digital ambient light sensor.
</Description>
<Documentation><Markdown>
Datasheet available from:
https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/docs/datasheet/hat/BH1750FVI.pdf
This component supports one time H-Resolution mode.
</Markdown></Documentation>
|
Most of the elements in They make sharing Component Types between developers much easier, and give developers a way to communicate important implementation details to systems engineers and spacecraft operators. Remember that comparatively few end-users of your Component Types will have access to the C code you write. |
You will use MAS to issue commands to the light sensor. To do this, you will
need to declare that BH1750FVI requires MAS.
Add the <Services> element just below the <!-- Services (Provided, Required,
Subscribed, Brokered and Published) --> comment:
<Services>
<Required><Service name="bus" type="io.MAS">
<Description>
Provides access to the I2C bus connected to the sensor.
</Description>
</Service></Required>
</Services>
As you learned in the earlier Using a Service
tutorial, adding this Service requirement makes BH1750FVI a MAS
Consumer, which, when instantiated, must be connected to a MAS Provider.
The BH1750FVI implementation can safely assume this MAS requirement will be
fulfilled.
Finally, your Component Type needs to expose the functionality it has to end users. We assumed above that the only required functionality is to get one time light readings. This fits well with implementing a single Read-Only Parameter.
Add a <Parameters> element just below the <!-- Parameters -->
comment, and define a light parameter which will return the current light
reading:
<Parameters>
<Parameter name="light" readOnly="true">
<Description>
The current ambient light reading, in lux.
</Description>
<Type>
<Integer signed="false" bits="16"/>
</Type>
</Parameter>
</Parameters>
The type for the Parameter is set to a 16-bit unsigned integer because the sensor returns 16-bit values which are then scaled down by a calibration factor to give a reading in lux.
Generate the BH1750FVI Component Type using the usual command:
hfk component-type generate BH1750FVI
Implementation
Having generated the Component Type, the Flightkit Tooling has produced the
BH1750FVI.c file which you need to modify to complete the implementation.
Open library/component_types/BH1750FVI/BH1750FVI.c.
This Component Type's implementation is relatively simple. The majority of
your effort will go into writing the BH1750FVI_getLight() Parameter getter
function.
The procedure this function must implement was outlined above:
-
Write the "one time H-resolution mode" command via I2C to the sensor.
-
Wait 180ms to allow the measurement to complete.
-
Read the measurement from the sensor.
As shown in the table on page 5 of the BH1750FVI datasheet, the "one time
H-resolution mode" command code is a single byte: 0010 0000 in binary, or
0x20 in hexadecimal.
In addition to this command code, notice that the examples on page 7 of the
datasheet show a variety of other fields that need to be set in the I2C
transaction. One of the benefits of using MAS is that these fields are all the
responsibility of the MAS Provider. The BH1750FVI Component Type, as
the Consumer, only needs to provide the data to write.
In BH1750FVI_getLight(), add a call to BH1750FVI_MAS_write() to
write the "one time H-resolution mode" command to the sensor.
You can find the signature of BH1750FVI_MAS_write() in the
BH1750FVI_MAS_package.h header file.
Show the MAS write added to BH1750FVI_getLight().
status_t BH1750FVI_getLight
(
BH1750FVI_t *pt_BH1750FVI,
uint16_t *pu16_Value
)
{
status_t t_Status; /* The current status */
TASK_PROTECTED_START(&pt_BH1750FVI->t_Lock, >_LockTimeout, t_Status)
{
- /* TODO: Implement light get accessor */
- t_Status = STATUS_NOT_IMPLEMENTED;
+ uint8_t u8_WriteData;
+
+ /* Send the "one time H-resolution mode" command */
+ u8_WriteData = 0x20;
+
+ /* Write the data to the device */
+ t_Status = BH1750FVI_MAS_writeBus(
+ pt_BH1750FVI,
+ >_LockTimeout,
+ 0,
+ 0,
+ NULL,
+ &u8_WriteData,
+ 1);
}
TASK_PROTECTED_END(&pt_BH1750FVI->t_Lock, >_LockTimeout, t_Status);
return t_Status;
}
Add a call to Task_Time_delayShortTime() to BH1750FVI_getLight() to
wait for the required 180ms measurement time.
Show the delay added to BH1750FVI_getLight().
status_t BH1750FVI_getLight
(
BH1750FVI_t *pt_BH1750FVI,
uint16_t *pu16_Value
)
{
status_t t_Status; /* The current status */
TASK_PROTECTED_START(&pt_BH1750FVI->t_Lock, >_LockTimeout, t_Status)
{
uint8_t u8_WriteData;
/* Send the "one time H-resolution mode" command */
u8_WriteData = 0x20;
/* Write the data to the device */
t_Status = BH1750FVI_MAS_writeBus(
pt_BH1750FVI,
>_LockTimeout,
0,
0,
NULL,
&u8_WriteData,
1);
+
+ if (t_Status == STATUS_SUCCESS)
+ {
+ /* Wait 180ms time for the measurement to be taken */
+ t_Status = Task_Time_delayShortTime(180uL * 1000);
+ }
}
TASK_PROTECTED_END(&pt_BH1750FVI->t_Lock, >_LockTimeout, t_Status);
return t_Status;
}
After waiting for the appropriate length of time, the final step is to read the measurement result. To do this, all you need to do is issue a 2-byte MAS read.
In BH1750FVI_getLight(), add a call to BH1750FVI_MAS_read() to
read the 2-byte measured value from the sensor.
Show the MAS read added to BH1750FVI_getLight().
status_t BH1750FVI_getLight
(
BH1750FVI_t *pt_BH1750FVI,
uint16_t *pu16_Value
)
{
status_t t_Status; /* The current status */
TASK_PROTECTED_START(&pt_BH1750FVI->t_Lock, >_LockTimeout, t_Status)
{
uint8_t u8_WriteData;
/* Send the "one time H-resolution mode" command */
u8_WriteData = 0x20;
/* Write the data to the device */
t_Status = BH1750FVI_MAS_writeBus(
pt_BH1750FVI,
>_LockTimeout,
0,
0,
NULL,
&u8_WriteData,
1);
if (t_Status == STATUS_SUCCESS)
{
/* Wait 180ms time for the measurement to be taken */
t_Status = Task_Time_delayShortTime(180uL * 1000);
}
+
+ if (t_Status == STATUS_SUCCESS)
+ {
+ /* A buffer to read into, and its length. */
+ uint8_t ru8_ReadData[2];
+ uint32_t u32_ReadDataLength = sizeof(ru8_ReadData);
+
+ /* Read the measurement from the device */
+ t_Status = BH1750FVI_MAS_readBus(
+ pt_BH1750FVI,
+ >_LockTimeout,
+ 0,
+ 0,
+ NULL,
+ &ru8_ReadData[0],
+ &u32_ReadDataLength);
+ }
}
TASK_PROTECTED_END(&pt_BH1750FVI->t_Lock, >_LockTimeout, t_Status);
return t_Status;
}
Finally, you need to add code to convert the retrieved sample from raw counts to lux. The examples on page 7 of the BH1750FVI datasheet use a nominal conversion factor of 1.2.
Convert the contents of ru8_ReadData to a uint16_t representing light level
in lux, and return it to the user.
This will require you to convert the network-endian byte buffer to a uint16_t,
promote it to a float32_t, divide by 1.2f, then convert back to a uint16_t.
Show the value conversion code added to BH1750FVI_getLight().
if (t_Status == STATUS_SUCCESS)
{
/* A buffer to read into, and its length. */
uint8_t ru8_ReadData[2];
uint32_t u32_ReadDataLength = sizeof(ru8_ReadData);
/* Read the measurement from the device */
t_Status = BH1750FVI_MAS_readBus(
pt_BH1750FVI,
>_LockTimeout,
0,
0,
NULL,
&ru8_ReadData[0],
&u32_ReadDataLength);
+
+ if (t_Status == STATUS_SUCCESS)
+ {
+ uint16_t u16_RawValue;
+ float32_t f32_LuxValue;
+
+ Util_Endian_u16FromNetwork(&ru8_ReadData[0], &u16_RawValue);
+
+ /* Convert the sample to lux by dividing by 1.2 */
+ f32_LuxValue = (float32_t)u16_RawValue / 1.2f;
+
+ *pu16_Value = (uint16_t)f32_LuxValue;
+ }
}
}
TASK_PROTECTED_END(&pt_BH1750FVI->t_Lock, >_LockTimeout, t_Status);
return t_Status;
}
In order for the provided implementation to work, you also need to include header files for functions you’re using to the top of the implementation file.
Include the following additional files so that you can access the relevant endian handling functions:
#include "Util_Endian.h"
|
It is good practice to now address the remaining |
Open BH1750FVI.h and update the TODO in the BH1750FVI_Init_t data
structure so that it informs the user that no Initialisation Data is
required.
Remove the TODO comment in the BH1750FVI_t Component Type Structure, as
the implementation requires no additional state.
|
It would be preferable to use
If you choose to do this you will need to |
The LightSensorDemo Deployment Type
Before using the BH1750FVI Component Type to interact with the light
sensor, you must instantiate it in a Deployment Type, and instantiate that
Deployment Type in a Mission.
To help with this, a Deployment Type and Mission have been provided. The Deployment Type contains all the necessary Component Instances needed to communicate with the Deployment Type via Lab.
|
You’ll now update the provided |
New Component Instances
Open the deploymentType.xml file which can be found in the
library/deployment_types/LightSensorDemo directory.
Add the following two Component Instances to the bottom of the
<Implementation> element:
Show the Component Instances added to the deploymentType.xml file.
<Component name="comms.services.PASTarget" type="core.component.pas.PASTarget"/>
<Component name="comms.services.PASMessaging" type="io.ams.AMSTargetPS">
<Connections>
<Services>
<Service name="initiator" component="comms.SpacePacket" service="dataPacket" channel="0" />
<Service name="serviceTarget" component="comms.services.PASTarget" service="remote" />
</Services>
</Connections>
<Tasks>
<SporadicTask name="receive" priority="2" />
<SporadicTask name="waitForReplyComplete" priority="2" />
<SporadicTask name="sendCompleteServiceTarget" priority="2" />
<SporadicTask name="requestCompleteServiceTarget" priority="2" />
</Tasks>
</Component>
+ <Component name="BSC" type="drivers.linux.io.bus.I2CController">
+ <Description>
+ The Broadcomm Serial Controller which controls the platform I2C bus.
+ </Description>
+ </Component>
+ <Component name="M5DLightUnit" type="BH1750FVI">
+ <Description>
+ Provides access to the M5 DLight Unit sensor connected via I2C.
+ </Description>
+ <Connections>
+ <Services>
+ <Service name="bus" component="BSC" service="data" channel="0" />
+ </Services>
+ </Connections>
+ </Component>
</Implementation>
</DeploymentType>
</ModelElement>
BSC is an instance of our drivers.linux.io.bus.I2CController
Component Type. It is a MAS Provider and is the controller on the I2C
bus.
M5DLightUnit is an instance of your new BH1750FVI Component Type. It is a
MAS Consumer which issues MAS Operations to control the peripheral -
the M5 DLight Unit.
As you would expect, this addition to the deploymentType.xml expresses the
design we discussed above, but in XML instead of as a
block diagram.
Notice how the io.MAS requirement which you previously gave your BH1750FVI
Component Type has been satisfied in the connections section of your
M5DLightUnit Component Instance. The <Service> element can be read as
follows: "the Service requirement named bus has been fulfilled by the BSC
Component Instance's data Service Provision".
Notice also that we have specified channel="0". The I2CController
Component Type Provides multiple channels of io.MAS. This
lets it fulfil the Service requirements of multiple Consumers. It offers
multiple channels by accessing a different I2C address on each channel. You
only require access to a single I2C peripheral, so below you’ll set up your
BSC Component Instance to only provide a single channel - channel 0.
Now that you have updated the Deployment Type, you can generate the Mission in which it is instantiated. This will verify that you have connected the various Component Instances in the Deployment Type correctly.
Generate the HardwareInteraction1 Mission using the following command:
hfk mission generate HardwareInteraction1
This produces a Mission Database which will allow you to interact with the Deployment Instance via Lab. It also produces empty Initialisation Data for the new Component Instances which you will populate later.
Setting the Initialisation Data
The final step before building the HardwareInteraction1 Mission is to
populate the Initialisation Data for the two new Component Instances.
Open missions/HardwareInteraction1/LightSensorDemo/Default/init/BSC_Init.c.
Here you must specify the Initialisation Data for the BSC
Component Instance using the definition of the I2CController_Init_t
structure defined in I2CController.h.
You will need to use values in this structure as follows:
-
u8_BusIndexneeds to be set to1, because the light sensor is connected to the Pi’s I2C bus 1. -
pt_Channelsneeds to be a valid pointer to an array ofI2CController_Channel_tstructures. -
u32_NumOfChannelsneeds to be set to the length of the array pointed to bypt_Channels. -
b_Enabledshould be set totrueso that the BSC peripheral is enabled at start up.
The number of Service channels which BSC Provides is determined by the
u32_NumOfChannels Initialisation Data member.
As discussed above, only one channel is required, because you only require access to the single M5 DLight Unit on the I2C bus.
This means the array pointed to by pt_Channels only needs to contain a single
I2CController_Channel_t structure. That structure has only a single member,
u8_PeripheralAddress, which should be set to the peripheral address of the
connected peripheral. This is documented in the BH1750FVI datasheet as 0x23.
Set the Initialisation Data for the BSC Component Instance using the
values given above.
Show the new Initialisation Data
+/** The list of channels for BSC */
+static const I2CController_Channel_t grt_Channels[] =
+ {
+ {
+ /* Unshifted peripheral address of the connected BH1750FVI */
+ .u8_PeripheralAddress = 0x23
+ }
+ };
/**The BSC initialisation data */
const I2CController_Init_t gt_BSCInit =
{
- /* TODO: Add initialisation data for the BSC component */
+ /** The index of the I2C bus */
+ .u8_BusIndex = 1,
+ /** Channel list */
+ .pt_Channels = &grt_Channels[0],
+ /** Number of available channels */
+ .u32_NumOfChannels = ARRAY_COUNT(grt_Channels),
+ /** Whether the I2C should be enabled by default */
+ .b_Enabled = true
};
With the BSC Component Instance set up properly, the same must be done for
the M5DLightUnit.
Open missions/HardwareInteraction1/LightSensorDemo/Default/init/M5DLightUnit_Init.c.
Recall that you updated the definition of BH1750FVI_Init_t in BH1750FVI.h
with a comment indicating that no Initialisation Data is required. The
Initialisation Data in the M5DLightUnit instance should therefore be empty,
but with a similar comment.
Update the comment in the Initialisation Data structure to state that no Initialisation Data is required.
With the Initialisation Data complete, you can now move on to testing your flight software on the target hardware.
Testing the Light Sensor
|
In order to build and run Flightkit flight software on your Pi you will need to follow the steps in our Raspberry Pi Zero How-to Guide! The steps below will fail if your machine or the Pi are not correctly configured. |
If you haven’t already, connect and power up the Pi.
Ensure that the light sensor is properly connected to I2C bus 1 of the Pi.
Build HardwareInteraction1 using the following command:
hfk mission build HardwareInteraction1
This builds a binary for the Deployment Instance that can now be run on the Pi. The binary is found here:
output/missions/HardwareInteraction1/LightSensorDemo/Default/Default
To run this binary on the Pi, you first need to copy it over to the device.
Copy the binary to the platform using the following command and entering your password when prompted.
If you’ve changed your username or hostname from the default values, be sure to use your own.
scp output/missions/HardwareInteraction1/LightSensorDemo/Default/Default pi@raspberrypi:
With the binary on the Pi filesystem, you can now log in to the Pi and run it.
Using another terminal, SSH into the platform using the following command, again ensuring to use your own username and hostname if required:
ssh pi@raspberrypi
Once connected, you can run the binary like you would any other executable.
Using the same terminal, run the binary using the following command:
./Default
If successful, the following output should be printed to the terminal:
INF: Deployment.c:311 Running deployment:
INF: Deployment.c:312 - Deployment instance: Default
INF: Deployment.c:313 - Deployment type: LightSensorDemo
INF: Deployment.c:314 - Target: LightSensorDemo
INF: Deployment.c:315 - Mission: HardwareInteraction1
INF: Deployment.c:455 Deployment initialisation successful
|
For more in depth instructions on how to run Deployment Instances on the Pi, refer to the remaining sections of the Raspberry Pi Zero How-to Guide. |
Now that the Deployment Instance is running on the platform, you can connect to it using Lab and test out the new light sensor functionality.
Open Lab, load the MDB and connect to the Deployment Instance.
Once connected, use the Commanding view to get the current value of the
light Parameter.
Experiment with shining different amounts of light on the sensor and requesting updated readings, and verify that the values returned are as expected.
Wrap Up
In this tutorial you have:
-
Learned how to interact with subsystems using the Memory Access Service (MAS).
-
Gained a deeper understanding of how Services are used in Flightkit and how they can be used to connect Component Instances together.
-
Learned about how the Flightkit development workflow can be used to produce Component Types which interact with hardware subsystems.
In the next tutorial, we’ll introduce a new Service for interacting with hardware in a "peer-to-peer" fashion, and you’ll create a Component Type which Consumes this Service.
Click here to move on to that now.