search
HomeBackend DevelopmentC++Reusable Component Libraries: Simplifying Migration Between Targets

Reusable Component Libraries: Simplifying Migration Between Targets

Operating with components external to the microcontroller or target itself is the norm in firmware development. Therefore, knowing how to develop libraries for them is essential. These libraries allow us to interact with them and exchange information or commands. However, it is not uncommon to find, in legacy code or code from students (or not-so-students), that these interactions with components are done directly in the application code or, even when placed in separate files, these interactions are intrinsically tied to the target.

Let’s look at a poor example of library development for a Bosch BME280 temperature, humidity, and pressure sensor in an application for an STMicroelectronics STM32F401RE. In the example, we want to initialize the component and read the temperature every 1 second. (In the example code, we will omit all the "noise" generated by STM32CubeMX/IDE, such as the initialization of various clocks and peripherals, or comments like USER CODE BEGIN or USER CODE END.)

#include "i2c.h"
#include <stdint.h>

int main(void)
{
    uint8_t  idx           = 0U;
    uint8_t  tx_buffer[64] = {0};
    uint8_t  rx_buffer[64] = {0};
    uint16_t dig_temp1     = 0U;
    int16_t  dig_temp2     = 0;
    int16_t  dig_temp3     = 0;

    MX_I2C1_Init();

    tx_buffer[idx++] = 0b10100011;

    HAL_I2C_Mem_Write(&hi2c1, 0x77U > 4U));

        var1 = (((float)adc_temp) / 16384.0f -
                ((float)dig_temp1) / 1024.0f) *
               ((float)dig_temp2);
        var2 = ((((float)adc_temp) / 131072.0f -
                 ((float)dig_temp1) / 8192.0f) *
                (((float)adc_temp) / 131072.0f -
                 ((float)dig_temp1) / 8192.0f)) *
               ((float)dig_temp3);

        t_fine = (int32_t)(var1 + var2);

        temperature = ((float)t_fine) / 5129.0f;

        // Temperature available for the application.
    }
}
</stdint.h>

Based on this example, we can raise a series of questions: what happens if I need to change the target (whether due to stock shortages, wanting to reduce costs, or simply working on another product that uses the same component)? What happens if I have more than one component of the same type in the system? What happens if another product uses the same component? How can I test my development if I don't have the hardware yet (a very common situation in the professional world where firmware and hardware development phases often overlap at certain points in the process)?

For the first three questions, the answer is to edit the code, whether to completely change it when switching targets, to duplicate the existing code to operate with an additional component of the same type, or to implement the same code for the other project/product. In the last question, there is no way to test the code without having the hardware to execute it. This means that only after the hardware is finished could we begin testing our code and start fixing errors inherent to firmware development itself, thus prolonging the product development time. This raises the question that gives rise to this post: is it possible to develop libraries for components that are independent of the target and allow for reuse? The answer is yes, and this is what we will see in this post.

Isolating the Library from the Target

To isolate libraries from a target, we will follow two rules: 1) we will implement the library in its own compilation unit, meaning its own file, and 2) there will be no references to any target-specific headers or functions. We will demonstrate this by implementing a simple library for the BME280. To start, we will create a folder called bme280 within our project. Inside the bme280 folder, we will create the following files: bme280.c, bme280.h, and bme280_interface.h. To clarify, no, I haven’t forgotten to name the file bme280_interface.c. This file will not be part of the library.

I usually place the library folders inside Application/lib/.

The bme280.h file will declare all the functions available in our library to be called by our application. On the other hand, the bme280.c file will implement the definitions of those functions, along with any auxiliary and private functions that the library may contain. So, what does the bme280_interface.h file contain? Well, our target, whatever it may be, will need to communicate with the BME280 component in one way or another. In this case, the BME280 supports either SPI or I2C communication. In both cases, the target must be able to read and write bytes to the component. The bme280_interface.h file will declare those functions so they can be called from the library. The definition of these functions will be the only part tied to the specific target, and it will be the only thing we need to edit if we migrate the library to another target.

Declaring the Library API

We begin by declaring the available functions in the library within the bme280.h file.

#include "i2c.h"
#include <stdint.h>

int main(void)
{
    uint8_t  idx           = 0U;
    uint8_t  tx_buffer[64] = {0};
    uint8_t  rx_buffer[64] = {0};
    uint16_t dig_temp1     = 0U;
    int16_t  dig_temp2     = 0;
    int16_t  dig_temp3     = 0;

    MX_I2C1_Init();

    tx_buffer[idx++] = 0b10100011;

    HAL_I2C_Mem_Write(&hi2c1, 0x77U > 4U));

        var1 = (((float)adc_temp) / 16384.0f -
                ((float)dig_temp1) / 1024.0f) *
               ((float)dig_temp2);
        var2 = ((((float)adc_temp) / 131072.0f -
                 ((float)dig_temp1) / 8192.0f) *
                (((float)adc_temp) / 131072.0f -
                 ((float)dig_temp1) / 8192.0f)) *
               ((float)dig_temp3);

        t_fine = (int32_t)(var1 + var2);

        temperature = ((float)t_fine) / 5129.0f;

        // Temperature available for the application.
    }
}
</stdint.h>

The library we are creating will be very simple, and we will only implement a basic initialization function and another to obtain a temperature measurement. Now, let’s implement the functions in the bme280.c file.

To avoid making the post too verbose, I am skipping the comments that would document the functions. This is the file where those comments would go. With so many AI tools available today, there’s no excuse for not documenting your code.

Implementation of the Driver API

The skeleton of the bme280.c file would be as follows:

#ifndef BME280_H_
#define BME280_H_

void  BME280_init(void);
float BME280_get_temperature(void);

#endif // BME280_H_

Let’s focus on initialization. As mentioned earlier, the BME280 supports both I2C and SPI communication. In both cases, we need to initialize the appropriate peripheral of the target (I2C or SPI), and then we need to be able to send and receive bytes through them. Assuming we are using I2C communication, in the STM32F401RE it would be:

#include "i2c.h"
#include <stdint.h>

int main(void)
{
    uint8_t  idx           = 0U;
    uint8_t  tx_buffer[64] = {0};
    uint8_t  rx_buffer[64] = {0};
    uint16_t dig_temp1     = 0U;
    int16_t  dig_temp2     = 0;
    int16_t  dig_temp3     = 0;

    MX_I2C1_Init();

    tx_buffer[idx++] = 0b10100011;

    HAL_I2C_Mem_Write(&hi2c1, 0x77U > 4U));

        var1 = (((float)adc_temp) / 16384.0f -
                ((float)dig_temp1) / 1024.0f) *
               ((float)dig_temp2);
        var2 = ((((float)adc_temp) / 131072.0f -
                 ((float)dig_temp1) / 8192.0f) *
                (((float)adc_temp) / 131072.0f -
                 ((float)dig_temp1) / 8192.0f)) *
               ((float)dig_temp3);

        t_fine = (int32_t)(var1 + var2);

        temperature = ((float)t_fine) / 5129.0f;

        // Temperature available for the application.
    }
}
</stdint.h>

Once the peripheral is initialized, we need to initialize the component. Here, we must use the information provided by the manufacturer in its datasheet. Here’s a quick summary: we need to start the temperature sampling channel (which is in sleep mode by default) and read some calibration constants stored in the component's ROM, which we will need later to calculate the temperature.

The goal of this post is not to learn how to use the BME280, so I will skip details of its usage, which you can find in its datasheet.

The initialization would look like this:

#ifndef BME280_H_
#define BME280_H_

void  BME280_init(void);
float BME280_get_temperature(void);

#endif // BME280_H_

Details to comment on. The calibration values that we read are stored in variables called dig_temp1, dig_temp2, and dig_temp3. These variables are declared as global so they are available for the rest of the functions in the library. However, they are declared as static so that they are only accessible within the library. No one outside the library needs to access or modify these values.

We also see that the return value from the I2C instructions is checked, and in case of a failure, the function execution is halted. This is fine, but it can be improved. Wouldn't it be better to notify the caller of the BME280_init function that something went wrong, if that was the case? To do this, we define the following enum in the bme280.h file.

I use typedef for them. There is debate about the use of typedef because they improve code readability at the cost of hiding details. It’s a matter of personal preference and making sure all members of the development team are on the same page.

void BME280_init(void)
{
}

float BME280_get_temperature(void)
{
}

Two notes: I usually add the _t suffix to typedefs to indicate that they are typedefs, and I add the typedef prefix to the values or members of the typedef, in this case BME280_Status_. The latter is to avoid collisions between enums from different libraries. If everyone used OK as an enum, we’d be in trouble.

Now we can modify both the declaration (bme280.h) and the definition (bme280.c) of the BME280_init function to return a status. The final version of our function would be:

void BME280_init(void)
{
    MX_I2C1_Init();
}
#include "i2c.h"
#include <stdint.h>

#define BME280_TX_BUFFER_SIZE 32U
#define BME280_RX_BUFFER_SIZE 32U
#define BME280_TIMEOUT        200U
#define BME280_ADDRESS        0x77U
#define BME280_REG_CTRL_MEAS  0xF4U
#define BME280_REG_DIG_T      0x88U

static uint16_t dig_temp1 = 0U;
static int16_t  dig_temp2 = 0;
static int16_t  dig_temp3 = 0;

void BME280_init(void)
{
    uint8_t idx                              = 0U;
    uint8_t tx_buffer[BME280_TX_BUFFER_SIZE] = {0};
    uint8_t rx_buffer[BME280_RX_BUFFER_SIZE] = {0};

    HAL_StatusTypeDef status = HAL_ERROR;

    MX_I2C1_Init();

    tx_buffer[idx++] = 0b10100011;

    status = HAL_I2C_Mem_Write(
        &hi2c1, BME280_ADDRESS 



<p>Since we are using the status enum, we must include the bme280.h file in the bme280.c file. We have already initialized the library. Now, let's create the function to retrieve the temperature. It would look like this:<br>
</p>

<pre class="brush:php;toolbar:false">typedef enum
{
    BME280_Status_Ok,
    BME280_Status_Status_Err,
} BME280_Status_t;

You’ve noticed, right? We’ve modified the function signature so that it returns a status to indicate whether there were communication issues with the component or not, and the result is returned through the pointer passed as a parameter to the function. If you’re following the example, remember to modify the function declaration in the bme280.h file so that they match.

BME280_Status_t BME280_init(void);

Great! At this point, in the application we can have:

#include "i2c.h"
#include <stdint.h>

int main(void)
{
    uint8_t  idx           = 0U;
    uint8_t  tx_buffer[64] = {0};
    uint8_t  rx_buffer[64] = {0};
    uint16_t dig_temp1     = 0U;
    int16_t  dig_temp2     = 0;
    int16_t  dig_temp3     = 0;

    MX_I2C1_Init();

    tx_buffer[idx++] = 0b10100011;

    HAL_I2C_Mem_Write(&hi2c1, 0x77U > 4U));

        var1 = (((float)adc_temp) / 16384.0f -
                ((float)dig_temp1) / 1024.0f) *
               ((float)dig_temp2);
        var2 = ((((float)adc_temp) / 131072.0f -
                 ((float)dig_temp1) / 8192.0f) *
                (((float)adc_temp) / 131072.0f -
                 ((float)dig_temp1) / 8192.0f)) *
               ((float)dig_temp3);

        t_fine = (int32_t)(var1 + var2);

        temperature = ((float)t_fine) / 5129.0f;

        // Temperature available for the application.
    }
}
</stdint.h>

Super clean! This is readable. Ignore the use of the Error_Handler function from STM32CubeMX/IDE. It’s generally not recommended to use it, but for the example, it works for us. So, is it done?

Well, no! We’ve encapsulated our interactions with the component into its own files. But its code is still calling target functions (HAL functions)! If we change the target, we’ll have to rewrite the library! Hint: we haven’t written anything in the bme280_interface.h file yet. Let’s tackle that now.

Interface Declaration

If we look at the bme280.c file, our interactions with the target are threefold: to initialize peripherals, to write/send bytes, and to read/receive bytes. So, what we’ll do is declare those three interactions in the bme280_interface.h file.

#ifndef BME280_H_
#define BME280_H_

void  BME280_init(void);
float BME280_get_temperature(void);

#endif // BME280_H_

If you notice, we’ve also defined a new type for the interface status. Now, instead of calling the target functions directly, we will call these functions from the bme280.c file.

void BME280_init(void)
{
}

float BME280_get_temperature(void)
{
}

Et voilà! The target dependencies have disappeared from the library. We now have a library that works for STM32, MSP430, PIC32, etc. In the three library files, nothing specific to any target should appear. What's the only thing left? Well, defining the interface functions. This is the only part that needs to be migrated/adapted for each target.

I usually do it inside the folder Application/bsp/components/.

We create a file called bme280_implementation.c with the following content:

void BME280_init(void)
{
    MX_I2C1_Init();
}

This way, if we want to use the library in another project or on another target, we only need to adapt the bme280_implementation.c file. The rest remains exactly the same.

Other aspects to consider

With this, we have seen a basic example of a library. This implementation is the simplest, safest, and most common. However, there are different variants depending on the characteristics of our project. In this example, we have seen how to perform a selection of the implementation at link time. That is, we have the bme280_implementation.c file, which provides the definitions of the interface functions during the compilation/linking process. What would happen if we wanted to have two implementations? One for I2C communication and another for SPI communication. In that case, we would need to specify the implementations at run time using function pointers.

Another aspect is that in this example, we assume there is only one BME280 in the system. What would happen if we had more than one? Should we copy/paste code and add prefixes to functions like BME280_1 and BME280_2? No. That’s not ideal. What we would do is use handlers to allow us to operate with the same library on different instances of a component.

These aspects and how to test our library before even having our hardware available is a topic for another post, which we will cover in future articles. For now, we have no excuse not to implement libraries properly. However, my first recommendation (and paradoxically, the one I’ve left for the end) is that, first and foremost, make sure the manufacturer doesn’t already provide an official library for their component. This is the fastest way to get a library up and running. Rest assured that the library provided by the manufacturer will likely follow a similar implementation to the one we have seen today, and our job will be to adapt the interface implementation part to our target or product.


If you're interested in this topic, you can find this post and others related to embedded systems development on my blog! ?

The above is the detailed content of Reusable Component Libraries: Simplifying Migration Between Targets. For more information, please follow other related articles on the PHP Chinese website!

Statement
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
C# and C  : Exploring the Different ParadigmsC# and C : Exploring the Different ParadigmsMay 08, 2025 am 12:06 AM

The main differences between C# and C are memory management, polymorphism implementation and performance optimization. 1) C# uses a garbage collector to automatically manage memory, while C needs to be managed manually. 2) C# realizes polymorphism through interfaces and virtual methods, and C uses virtual functions and pure virtual functions. 3) The performance optimization of C# depends on structure and parallel programming, while C is implemented through inline functions and multithreading.

C   XML Parsing: Techniques and Best PracticesC XML Parsing: Techniques and Best PracticesMay 07, 2025 am 12:06 AM

The DOM and SAX methods can be used to parse XML data in C. 1) DOM parsing loads XML into memory, suitable for small files, but may take up a lot of memory. 2) SAX parsing is event-driven and is suitable for large files, but cannot be accessed randomly. Choosing the right method and optimizing the code can improve efficiency.

C   in Specific Domains: Exploring Its StrongholdsC in Specific Domains: Exploring Its StrongholdsMay 06, 2025 am 12:08 AM

C is widely used in the fields of game development, embedded systems, financial transactions and scientific computing, due to its high performance and flexibility. 1) In game development, C is used for efficient graphics rendering and real-time computing. 2) In embedded systems, C's memory management and hardware control capabilities make it the first choice. 3) In the field of financial transactions, C's high performance meets the needs of real-time computing. 4) In scientific computing, C's efficient algorithm implementation and data processing capabilities are fully reflected.

Debunking the Myths: Is C   Really a Dead Language?Debunking the Myths: Is C Really a Dead Language?May 05, 2025 am 12:11 AM

C is not dead, but has flourished in many key areas: 1) game development, 2) system programming, 3) high-performance computing, 4) browsers and network applications, C is still the mainstream choice, showing its strong vitality and application scenarios.

C# vs. C  : A Comparative Analysis of Programming LanguagesC# vs. C : A Comparative Analysis of Programming LanguagesMay 04, 2025 am 12:03 AM

The main differences between C# and C are syntax, memory management and performance: 1) C# syntax is modern, supports lambda and LINQ, and C retains C features and supports templates. 2) C# automatically manages memory, C needs to be managed manually. 3) C performance is better than C#, but C# performance is also being optimized.

Building XML Applications with C  : Practical ExamplesBuilding XML Applications with C : Practical ExamplesMay 03, 2025 am 12:16 AM

You can use the TinyXML, Pugixml, or libxml2 libraries to process XML data in C. 1) Parse XML files: Use DOM or SAX methods, DOM is suitable for small files, and SAX is suitable for large files. 2) Generate XML file: convert the data structure into XML format and write to the file. Through these steps, XML data can be effectively managed and manipulated.

XML in C  : Handling Complex Data StructuresXML in C : Handling Complex Data StructuresMay 02, 2025 am 12:04 AM

Working with XML data structures in C can use the TinyXML or pugixml library. 1) Use the pugixml library to parse and generate XML files. 2) Handle complex nested XML elements, such as book information. 3) Optimize XML processing code, and it is recommended to use efficient libraries and streaming parsing. Through these steps, XML data can be processed efficiently.

C   and Performance: Where It Still DominatesC and Performance: Where It Still DominatesMay 01, 2025 am 12:14 AM

C still dominates performance optimization because its low-level memory management and efficient execution capabilities make it indispensable in game development, financial transaction systems and embedded systems. Specifically, it is manifested as: 1) In game development, C's low-level memory management and efficient execution capabilities make it the preferred language for game engine development; 2) In financial transaction systems, C's performance advantages ensure extremely low latency and high throughput; 3) In embedded systems, C's low-level memory management and efficient execution capabilities make it very popular in resource-constrained environments.

See all articles

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

Video Face Swap

Video Face Swap

Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Tools

SecLists

SecLists

SecLists is the ultimate security tester's companion. It is a collection of various types of lists that are frequently used during security assessments, all in one place. SecLists helps make security testing more efficient and productive by conveniently providing all the lists a security tester might need. List types include usernames, passwords, URLs, fuzzing payloads, sensitive data patterns, web shells, and more. The tester can simply pull this repository onto a new test machine and he will have access to every type of list he needs.

SAP NetWeaver Server Adapter for Eclipse

SAP NetWeaver Server Adapter for Eclipse

Integrate Eclipse with SAP NetWeaver application server.

Atom editor mac version download

Atom editor mac version download

The most popular open source editor

Dreamweaver CS6

Dreamweaver CS6

Visual web development tools

WebStorm Mac version

WebStorm Mac version

Useful JavaScript development tools