When working with sensors in a modern development cycle, it’s important that before writing any sensor code, developers take the time to carefully think through their system architecture. A carefully architected application will provide software interfaces that not only provide a common methodology to interact with sensors but will also abstract the hardware details of those sensors. Many embedded developers still write code that tightly couples their sensor code to their application which can make it challenging to reuse, scale and test the software. A great best practice that developers can follow is to take the time to design a proper interface in their architecture which will then yield these benefits. In my previous post, I discussed different types of drivers. In this post, I discuss interface design concepts and how they can be applied to interfacing with sensors.
Benefits to Creating an Interface
Developers who work with object-oriented languages will naturally understand the benefits that an interface can provide to an application, but most embedded systems are still written using C so those benefits might be overlooked by those developers. There are many benefits to creating an interface to interact with an I/O device in an embedded system such as:
- Reversing the code dependency direction
- Enhancing portability
- Abstracted complexity and low-level details
- Increasing reuse and scalability
- Simplifying software maintenance
When using an interface to interact with sensors, developers will find that much of the low-level details are abstracted from the high-level application. This means that the application doesn’t know if the sensor is connected to an ADC, an I2C bus, SPI bus or some other hardware interface.
For example, take a moment to look at the figure below:
Abstraction in the sensor interface (Source: Jacob Beningo)
In this example, the application makes a call through the sensors API and uses one of its operations to interact with the sensor. The application doesn’t know what is happening behind the scenes in Sensor API, which may have function calls such as:
That sensor interface may be making direct calls to an ADC peripheral or it may be creating a message packet that is transmitted out a communication peripheral. The nice thing about the interface is that the application developer doesn’t need to know those details. (In fact, the interface layer may be doing nothing more than dereferencing a function pointer that has been configured to point to the correct module to handle the sensor communication! This provides a form of simple inheritance in our C application.)
Designing a Sensor Interface
There are several steps that a developer who is interested in creating a good, reusable sensor interface should follow. These steps help to ensure that the interface is as usable as possible on the first interaction even though it may take several iterations before the interface fully stabilizes.
The first step is to identify the sensor types that will be used in the embedded systems that you will design and then examining the datasheets. During this step you want to become familiar with the operations and the data that are common between all the different sensor types and which ones are not common. What you will find is that there is always a commonality between the operations and the data even across different types of sensors. We want to build that commonality into the interface. The operations and data that are not common we build into an extension of that interface, which allows us to add and remove those features based on the application we are developing.
Next, once we have identified the operations and the data, we can sketch out an interface in C that could fit the needs for our sensors. The complexity for that interface is completely up to the developer. For example, we could design a simple function call-based interface where the function prototypes might look like:
bool Sensor_Init(const SensorConfig_t * const Config);
bool Sensor_Read(const SensorObj_t * const, SensorData_t * const SensorData);
bool Sensor_Write(const SensorObj_t * const, SensorData_t * const SensorData);
In this case, any call to the interface returns a bool that gives information as to the result of the operation. For example, we might call Sensor_Read and if the underlying implementation is to poll the device for sensor data ready, then we may return false if there is no new data. If the data was available, it might be copied into the SensorData location provided to the interface and return true. (We could certainly get more sophisticated and create error codes and other return values, but we should start simple).
This interface could be used to interact with any number of sensors that we simply pass the SensorObj information to the interface and then let the interface do the work to perform the operation that we need done. We could also use this as a template and rename Sensor for the sensor name, although that starts to minimize the abstraction’s usefulness and reuseability.
One last and interesting way we could design the interface is to be a structure of function pointers. The developer would then instantiate the structure and initialize it with the function specific calls that relate to the sensor that they want to interface to. This implementation might look some like the following:
bool (Init)(const SensorConfig_t * const Config);
bool (Read)(const SensorObj_t * const, SensorData_t * const SensorData);
bool (*Write)(const SensorObj_t * const, SensorData_t * const SensorData);
We could then use this same interface for multiple sensors by simply creating and initializing this structure like:
const Sensor_t Analog =
const Sensor_t Gyro =
Making a call to the interface for the sensor then looks something like:
As you can see this type of interface is quite scalable and reusable. It may make some developers nervous since it does use function pointers. Care must be taken to make sure those function pointers behave.
When interfacing a sensor to an embedded system, the natural instinct is to examine that sensor and then start writing a driver for it. Unfortunately, this results in software that is tightly coupled and does not have the benefits of being scalable or reusable. As we have seen in this post, we should instead first focus on our software architecture and how our sensor fits into that architecture. We can then develop an interface that abstracts out the details of our sensor so that the application is not aware of the complexities or the low-level details. This makes it so that if the sensor is discovered to not fit an application late in the design cycle, the sensor can be easily swapped without any need to modify the core application code.
In the next post, we will examine how we can build on these concepts to write an application that samples a sensor through an interface to a polled ADC peripheral.
Jacob Beningo is an embedded software consultant, advisor and educator who currently works with clients in more than a dozen countries to dramatically transform their software, systems and processes. Feel free to contact him at firstname.lastname@example.org, at his website www.beningo.com, and sign-up for his monthly Embedded Bytes Newsletter.