搜索
首页后端开发C++可重用组件库:简化目标之间的迁移

Reusable Component Libraries: Simplifying Migration Between Targets

使用微控制器或目标本身外部的组件进行操作是固件开发的常态。因此,了解如何为他们开发库至关重要。这些库允许我们与它们交互并交换信息或命令。然而,在遗留代码或学生(或非学生)的代码中,经常会发现这些与组件的交互是直接在应用程序代码中完成的,或者即使放在单独的文件中,这些交互本质上也是如此与目标绑定。

让我们看一下 STMicroelectronics STM32F401RE 应用程序中 Bosch BME280 温度、湿度和压力传感器的库开发的一个糟糕示例。在示例中,我们要初始化组件并每 1 秒读取一次温度。 (在示例代码中,我们将省略STM32CubeMX/IDE产生的所有“噪音”,例如各种时钟和外设的初始化,或者诸如USER CODE BEGIN或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>

基于这个例子,我们可以提出一系列问题:如果我需要更改目标(无论是由于库存短缺、想要降低成本,还是只是开发使用相同组件的另一个产品),会发生什么?如果系统中有多个相同类型的组件,会发生什么情况?如果另一个产品使用相同的组件会发生什么?如果我还没有硬件,如何测试我的开发(这是专业领域中非常常见的情况,固件和硬件开发阶段经常在过程中的某些点重叠)?

对于前三个问题,答案是编辑代码,是否在切换目标时完全更改它,复制现有代码以与相同类型的附加组件一起操作,或者为目标实现相同的代码其他项目/产品。在最后一个问题中,如果没有硬件执行代码,就无法测试代码。这意味着只有硬件完成后我们才能开始测试代码并开始修复固件开发本身固有的错误,从而延长产品开发时间。这就提出了引发这篇文章的问题:是否可以为独立于目标并允许重用的组件开发库?答案是肯定的,这就是我们将在这篇文章中看到的内容。

将库与目标隔离

为了将库与目标隔离,我们将遵循两条规则:1)我们将在其自己的编译单元中实现库,即它自己的文件,2)不会引用任何特定于目标的标头或函数。我们将通过为 BME280 实现一个简单的库来演示这一点。首先,我们将在项目中创建一个名为 bme280 的文件夹。在 bme280 文件夹中,我们将创建以下文件:bme280.c、bme280.h 和 bme280_interface.h。澄清一下,不,我没有忘记将文件命名为 bme280_interface.c。该文件不会成为库的一部分。

我通常将库文件夹放在 Application/lib/ 中。

bme280.h 文件将声明我们的库中可用的所有函数,供我们的应用程序调用。另一方面,bme280.c 文件将实现这些函数的定义,以及库可能包含的任何辅助函数和私有函数。那么,bme280_interface.h 文件包含什么内容?好吧,无论我们的目标是什么,都需要以一种或另一种方式与 BME280 组件进行通信。在这种情况下,BME280 支持 SPI 或 I2C 通信。在这两种情况下,目标必须能够读取组件字节并将其写入组件。 bme280_interface.h 文件将声明这些函数,以便可以从库中调用它们。这些函数的定义将是与特定目标相关的唯一部分,如果我们将库迁移到另一个目标,这将是我们唯一需要编辑的内容。

声明库 API

我们首先在 bme280.h 文件中声明库中的可用函数。

#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>

我们创建的库将非常简单,我们只会实现一个基本的初始化函数和另一个来获取温度测量值。现在,我们来实现bme280.c文件中的函数。

为了避免帖子过于冗长,我跳过了记录功能的注释。这是这些评论所在的文件。如今有如此多的人工智能工具可用,没有理由不记录您的代码。

驱动程序API的实现

bme280.c 文件的骨架如下:

#ifndef BME280_H_
#define BME280_H_

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

#endif // BME280_H_

让我们重点关注初始化。如前所述,BME280 支持 I2C 和 SPI 通信。在这两种情况下,我们都需要初始化目标的适当外设(I2C 或 SPI),然后我们需要能够通过它们发送和接收字节。假设我们使用 I2C 通信,在 STM32F401RE 中它将是:

#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>

外围设备初始化后,我们需要初始化组件。在这里,我们必须使用制造商在其数据表中提供的信息。简单总结一下:我们需要启动温度采样通道(默认处于睡眠模式)并读取存储在组件 ROM 中的一些校准常数,稍后我们将需要这些校准常数来计算温度。

这篇文章的目的不是学习如何使用 BME280,因此我将跳过其使用详细信息,您可以在其数据表中找到这些详细信息。

初始化看起来像这样:

#ifndef BME280_H_
#define BME280_H_

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

#endif // BME280_H_

详情可评论。我们读取的校准值存储在名为 dig_temp1、dig_temp2 和 dig_temp3 的变量中。这些变量被声明为全局变量,因此它们可用于库中的其余函数。但是,它们被声明为静态,因此只能在库内访问。图书馆外的任何人都不需要访问或修改这些值。

我们还看到对 I2C 指令的返回值进行检查,如果失败,函数执行将停止。这很好,但还可以改进。如果是这种情况,通知 BME280_init 函数的调用者出现问题不是更好吗?为此,我们在 bme280.h 文件中定义以下枚举。

我对它们使用 typedef。关于 typedef 的使用存在争议,因为它们以隐藏细节为代价提高了代码的可读性。这是个人喜好的问题,并确保开发团队的所有成员都在同一页面上。

void BME280_init(void)
{
}

float BME280_get_temperature(void)
{
}

两个注意事项:我通常在 typedef 中添加 _t 后缀以表明它们是 typedef,并且在 typedef 的值或成员中添加 typedef 前缀,在本例中为 BME280_Status_。后者是为了避免来自不同库的枚举之间的冲突。如果每个人都使用 OK 作为枚举,我们就会遇到麻烦。

现在我们可以修改 BME280_init 函数的声明(bme280.h)和定义(bme280.c)以返回状态。我们函数的最终版本将是:

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>由于我们使用状态枚举,因此我们必须在 bme280.c 文件中包含 bme280.h 文件。我们已经初始化了该库。现在,让我们创建一个函数来检索温度。它看起来像这样:<br>
</p>

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

你已经注意到了,对吧?我们修改了函数签名,以便它返回一个状态来指示组件是否存在通信问题,并且结果通过作为参数传递给函数的指针返回。如果您遵循该示例,请记住修改 bme280.h 文件中的函数声明以使它们匹配。

BME280_Status_t BME280_init(void);

太棒了!此时,在应用程序中我们可以有:

#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>

超级干净!这是可读的。忽略 STM32CubeMX/IDE 中 Error_Handler 函数的使用。一般不建议使用它,但对于我们来说,它是有效的。那么,完成了吗?

嗯,不!我们已将与组件的交互封装到其自己的文件中。但它的代码仍然在调用目标函数(HAL函数)!如果我们改变目标,我们就必须重写库!提示:我们还没有在 bme280_interface.h 文件中编写任何内容。现在让我们解决这个问题。

接口声明

如果我们查看 bme280.c 文件,我们与目标的交互有三重:初始化外设、写入/发送字节以及读取/接收字节。因此,我们要做的就是在 bme280_interface.h 文件中声明这三个交互。

#ifndef BME280_H_
#define BME280_H_

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

#endif // BME280_H_

如果您注意到的话,我们还为界面状态定义了一种新类型。现在,我们不再直接调用目标函数,而是从 bme280.c 文件中调用这些函数。

void BME280_init(void)
{
}

float BME280_get_temperature(void)
{
}

Et voilà! 目标依赖项已从库中消失。我们现在有了一个适用于 STM32、MSP430、PIC32 等的库。在这三个库文件中,不应出现任何特定于任何目标的内容。唯一剩下的是什么?好了,定义接口函数。这是唯一需要针对每个目标迁移/调整的部分。

我通常在文件夹Application/bsp/components/中进行。

我们创建一个名为 bme280_implementation.c 的文件,其中包含以下内容:

void BME280_init(void)
{
    MX_I2C1_Init();
}

这样,如果我们想在另一个项目或另一个目标上使用该库,我们只需要修改 bme280_implementation.c 文件即可。其余部分保持完全相同。

其他需要考虑的方面

至此,我们已经看到了一个库的基本示例。这种实现是最简单、最安全、也是最常见的。然而,根据我们项目的特点,有不同的变体。在此示例中,我们了解了如何在链接时执行实现选择。也就是说,我们有 bme280_implementation.c 文件,它提供了编译/链接过程中接口函数的定义。如果我们想要有两个实现会怎样?一个用于 I2C 通信,另一个用于 SPI 通信。在这种情况下,我们需要使用函数指针在运行时指定实现。

另一方面是,在这个例子中,我们假设系统中只有一个 BME280。如果我们有多个的话会发生什么?我们是否应该复制/粘贴代码并向 BME280_1 和 BME280_2 等函数添加前缀?不,这并不理想。我们要做的是使用处理程序来允许我们在组件的不同实例上使用相同的库。

这些方面以及如何在硬件可用之前测试我们的库是另一篇文章的主题,我们将在以后的文章中介绍。目前,我们没有理由不正确实现库。然而,我的第一个建议(矛盾的是,我留到最后的建议)是,首先也是最重要的,确保制造商尚未为其组件提供官方库。这是启动和运行库的最快方法。请放心,制造商提供的库可能会遵循与我们今天看到的类似的实现,我们的工作将是使接口实现部分适应我们的目标或产品。


如果您对这个主题感兴趣,您可以在我的博客上找到这篇文章以及其他与嵌入式系统开发相关的文章! ?

以上是可重用组件库:简化目标之间的迁移的详细内容。更多信息请关注PHP中文网其他相关文章!

声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
C XML解析:技术和最佳实践C XML解析:技术和最佳实践May 07, 2025 am 12:06 AM

C 中解析XML数据可以使用DOM和SAX方法。1)DOM解析将XML加载到内存,适合小文件,但可能占用大量内存。2)SAX解析基于事件驱动,适用于大文件,但无法随机访问。选择合适的方法并优化代码可提高效率。

c在特定领域:探索其据点c在特定领域:探索其据点May 06, 2025 am 12:08 AM

C 在游戏开发、嵌入式系统、金融交易和科学计算等领域中的应用广泛,原因在于其高性能和灵活性。1)在游戏开发中,C 用于高效图形渲染和实时计算。2)嵌入式系统中,C 的内存管理和硬件控制能力使其成为首选。3)金融交易领域,C 的高性能满足实时计算需求。4)科学计算中,C 的高效算法实现和数据处理能力得到充分体现。

揭穿神话:C真的是一种死语吗?揭穿神话:C真的是一种死语吗?May 05, 2025 am 12:11 AM

C 没有死,反而在许多关键领域蓬勃发展:1)游戏开发,2)系统编程,3)高性能计算,4)浏览器和网络应用,C 依然是主流选择,展现了其强大的生命力和应用场景。

C#vs. C:编程语言的比较分析C#vs. C:编程语言的比较分析May 04, 2025 am 12:03 AM

C#和C 的主要区别在于语法、内存管理和性能:1)C#语法现代,支持lambda和LINQ,C 保留C特性并支持模板。2)C#自动内存管理,C 需要手动管理。3)C 性能优于C#,但C#性能也在优化中。

用C构建XML应用程序:实例用C构建XML应用程序:实例May 03, 2025 am 12:16 AM

在C 中处理XML数据可以使用TinyXML、Pugixml或libxml2库。1)解析XML文件:使用DOM或SAX方法,DOM适合小文件,SAX适合大文件。2)生成XML文件:将数据结构转换为XML格式并写入文件。通过这些步骤,可以有效地管理和操作XML数据。

C中的XML:处理复杂的数据结构C中的XML:处理复杂的数据结构May 02, 2025 am 12:04 AM

在C 中处理XML数据结构可以使用TinyXML或pugixml库。1)使用pugixml库解析和生成XML文件。2)处理复杂的嵌套XML元素,如书籍信息。3)优化XML处理代码,建议使用高效库和流式解析。通过这些步骤,可以高效处理XML数据。

C和性能:它仍然主导C和性能:它仍然主导May 01, 2025 am 12:14 AM

C 在性能优化方面仍然占据主导地位,因为其低级内存管理和高效执行能力使其在游戏开发、金融交易系统和嵌入式系统中不可或缺。具体表现为:1)在游戏开发中,C 的低级内存管理和高效执行能力使得它成为游戏引擎开发的首选语言;2)在金融交易系统中,C 的性能优势确保了极低的延迟和高吞吐量;3)在嵌入式系统中,C 的低级内存管理和高效执行能力使得它在资源有限的环境中非常受欢迎。

C XML框架:为您选择合适的一个C XML框架:为您选择合适的一个Apr 30, 2025 am 12:01 AM

C XML框架的选择应基于项目需求。1)TinyXML适合资源受限环境,2)pugixml适用于高性能需求,3)Xerces-C 支持复杂的XMLSchema验证,选择时需考虑性能、易用性和许可证。

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

DVWA

DVWA

Damn Vulnerable Web App (DVWA) 是一个PHP/MySQL的Web应用程序,非常容易受到攻击。它的主要目标是成为安全专业人员在合法环境中测试自己的技能和工具的辅助工具,帮助Web开发人员更好地理解保护Web应用程序的过程,并帮助教师/学生在课堂环境中教授/学习Web应用程序安全。DVWA的目标是通过简单直接的界面练习一些最常见的Web漏洞,难度各不相同。请注意,该软件中

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

安全考试浏览器

安全考试浏览器

Safe Exam Browser是一个安全的浏览器环境,用于安全地进行在线考试。该软件将任何计算机变成一个安全的工作站。它控制对任何实用工具的访问,并防止学生使用未经授权的资源。

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SecLists

SecLists

SecLists是最终安全测试人员的伙伴。它是一个包含各种类型列表的集合,这些列表在安全评估过程中经常使用,都在一个地方。SecLists通过方便地提供安全测试人员可能需要的所有列表,帮助提高安全测试的效率和生产力。列表类型包括用户名、密码、URL、模糊测试有效载荷、敏感数据模式、Web shell等等。测试人员只需将此存储库拉到新的测试机上,他就可以访问到所需的每种类型的列表。