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 whatever thread you need the data in while the second is the thread who’s 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).
The rest of this page details that general design by referring to this sample.
Sample sensor interface
/** * Altimeter Sensor Functions * Authors: Lucy Gong, Sahil Kale */ #ifndef ALTIMETER_HPP #define ALTIMETER_HPP #include <cstdint> struct AltimeterData_t { float pressure, altitude, temp; bool isDataNew; int status; //TBD but probably 0 = SUCCESS, -1 = FAIL, 1 = BUSY }; class Altimeter{ public: /** * Triggers interrupt for new altimeter measurement - stores raw data in variables and returns right away * */ virtual void Begin_Measuring() = 0; /**GetResult should: * 1. Reset dataIsNew flag * 2. Transfers raw data from variables to struct * 3. Updates utcTime and status values in struct as well * */ virtual void GetResult(AltimeterData_t *Data) = 0; // }; class MS5637 : public Altimeter { public: MS5637(const MS5637*) = delete; //Apparently if you try to copy a singleton this will give you errors? static MS5637* GetInstance(); void Begin_Measuring(); void GetResult(AltimeterData_t *Data); private: MS5637(); //Constructor can never be called muwhahaha static MS5637* s_Instance; void beginCollectingTemperature(); void beginCollectingPressure(); uint32_t getNewestMeasurement(); uint32_t readFromMS5637(uint8_t commandToWrite); void getRawPressureAndTemperature(float *displayPressure, float *displayTemperature, float *displayAltitude); uint32_t getCurrentTime(); uint32_t timeOfResult; bool dataIsNew = false; float altitudeMeasured = 0, pressureMeasured = 0, temperatureMeasured = 0; // various offsets and calibration parameters read from device. Used in internal math but God only knows what each one means. uint16_t c1, c2, c3, c4, c5, c6; }; #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 potetially 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 theMS5637
altimeter available, but there may be more) and assign that result to a pointer of the base class type (in this caseAltimeter
).(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.
Add Comment