Sensors
Overview
Of course, our aircraft need a lot of information about what’s going on in the world around them in order to respond correctly and do what they need to do. That information comes from multiple different sensors that for the most part, operate in very different ways and require multiple different communication protocols to speak to.
As a result, there is no standard for the way the “guts” of each sensor driver is written, that is entirely up to the developer. You may find some docs about particular drivers in the children of this page, but for the most part, the best way to understand a driver’s implementation is to dig through its source file.
There is, however, a design for the way interactions with the sensor drivers are to occur, and as a result, the interfaces of each driver (their header files) look similar.
From the highest level, most sensor drivers (though not all), will belong to 2 independent threads. One is the thread you need the data in, which is, by design, exclusively SensorFusion, while the second is the thread whose exclusive job is to sample the sensor at a regular interval. The reason that second thread is required is that the process of collecting data from the sensor via SPI, I2C or whatever else, takes time. If we were only using a single thread, that thread would have to wait for that transfer to occur before it could move on which is unacceptable for, for example, the state machines, which need to run at a very strict rate. The second thread ensures we can begin the transaction with the sensor so that by the time the data is needed by the first thread, it can be directly picked up without having to wait. The reason this isn’t the case with all sensors is that some sensors are always spewing data regardless of whether of whether we’re asking for it (gps for example) and others might be analog, meaning their data is always available to read at our ADCs (airspeed for example).
Sample sensor interface
/**
* IMU Sensor Functions and Part Number Selection.
* Available IMU driver(s): BMX160
* Author(s): Lucy Gong, Dhruv Rawat, Anthony Berbari
*/
#ifndef IMU_HPP
#define IMU_HPP
#include <cstdint>
/***********************************************************************************************************************
* Definitions
**********************************************************************************************************************/
struct IMUData_t {
float magx, magy, magz; // TODO although the BMX 160 has a magnetometer, it seems to produce bizarre results.More investigation needs to be done. Figuring out what the RHall register does is likely a part of that.
float accx, accy, accz;
float gyrx, gyry, gyrz;
float temp;
bool isDataNew;
int sensorStatus; // 0 = SUCCESS, -1 = FAIL, 1 = BUSY
};
/***********************************************************************************************************************
* Prototypes
*********************************************************************************************************************/
class IMU {
public:
/**
* Begins a transaction with the IMU.
* This function is non blocking and returns right away, data will be stored inside the module as it arrives.
* To achieve synchronous data, this function must be called synchronously.
* */
virtual void Begin_Measuring() = 0;
/**
* Retrieves any data already received by the imu.
* If no new data is available, the appropriate flag will be set in the return struct.
* All contents of the return struct, apart from the isDataNew flag, are undefined unless isDataNew is set to 1.
* This function is non blocking and returns right away.
* @param[in] Data reference to the results struct.
* */
virtual void GetResult(IMUData_t &Data) = 0;
};
class BMX160: public IMU{
public:
/**
* This module is built as a singleton. Thus to access a BMX160 object, this function must be called.
* Only a single BMX160 object will ever be created and will be shared by all callers of this function.
* @return IMU reference to the singleton object.
* */
static IMU& getInstance();
/**
* Deletes the constructor to disallow users to instantiate objects.
* */
BMX160(const BMX160*) = delete;
void Begin_Measuring();
void GetResult(IMUData_t &Data);
private:
BMX160();
void SetAllPowerModesToNormal(void);
void ConfigAcc(void);
void ConfigGyro(void);
void ConfigMag(void);
void SetMagConfig(uint8_t regAddr, uint8_t data);
void PrepareMagForDataMode(void);
void Bmx160WriteReg(uint8_t reg, uint8_t val);
void Bmx160ReadReg(uint8_t const regAddr, uint8_t *pData, uint8_t len);
void Calibrate(void);
//Variables
uint8_t rawImuData[21];
IMUData_t ImuCalibration;
IMUData_t ImuCalibrationFinal;
};
#endif
User’s perspective
If you're the user of a certain sensor driver, there are at most 3 methods of interest to you (and potentially a 4th calibration method). Here is what you have to do:
Instantiate the sensor object in the thread you wish to have the data in. If the particular sensor has a
Begin_Measuring
method, you’ll need to instantiate a second object in the same way in the second thread. Do this by calling the staticGetInstance
method of the sensor that interests you (in the sample, there is only thebmx160
imu available, but there may be more) and assign that result to a pointer of the base class type (in this caseIMU
).(Only if the sensor has a
Begin_Measuring
method). In the second thread mentioned in (1), call theBegin_Measuring
method at a regular interval.In the data thread, you are free to call
GetResult
as frequently as you want (it’s non blocking). The contents of the returned struct will not only include the latest available data but will also include an indication of whether the data has been refreshed since last timeGetResult
was called.
Design
Ok so what’s actually going on here.
Abstract class
First thing to explain is the existence of abstract and derived classes. There are a couple reasons things are designed this way. First reason is that if we have multiple different sensors available (multiple gps sensors, for example), they will be derived from the same abstract class. That means the caller needs to do very little work to swap between them; all that needs to be done is to call the GetInstance
method of the appropriate sensor. Other than that, the caller can interact with the drivers through the base class in the same way. Second reason is unit testing. When we do unit tests, we are doing things off the hardware, and our unit testing framework (google tests) requires that we have a “mock class” derive from the abstract class in order to test against the drivers. Third reason is Simulation. When we run our simulation, the data our software reads from the sensor drivers comes from a Matlab model rather than the actual sensor. That is easily done by using another class that inherits from the base class and ensures minimum modification in the calling code to execute a simulation.
Singletons
So what’s this business of calling a GetResult
method rather than just instantiating an object. Things were actually designed this way to accommodate the fact that Begin_Measuring
and GetResult
are called from different threads. GetResult
Actually returns a singleton. That means it always returns a pointer to the same object and at most 1 object can ever exist. In this way, both threads are actually calling methods from the same object. That makes the process of initializing the sensor much easier (since it’s only done once, when the constructor of the first and only created object is called) and it also means that we don’t need to worry about exchanging data between the threads since 1 object means we are are reading the same memory from either thread.
Should be noted that for consistency, even if a sensor does not need a Begin_Measuring
method, it should still be a singleton.