嵌入式软件架构的六个步骤(二)软件基础设施

本文围绕嵌入式软件架构展开,重点讨论统一的软件基础设施。介绍了其包含基础数据结构、底层库、操作系统等内容,阐述统一的必要性及优点。还分别从基础类型与宏定义、操作系统、中间件、框架与机制方面说明统一方法,强调其与硬件的关系及重要性。

软件架构这东西,众说纷纭,各有观点。在我看来,软件架构是软件系统的基本结构,包含其组件、组件之间的关系、组件设计与演进的规则,以及体现这些规则的基础设施。软件架构,从来不是一件容易事,它贯穿在产品的整个生命周期,需要所有团队成员遵守并自律,才能将架构思想在软件中体现。新手工程师,由于经历的项目太少,看不到项目全貌,很难从全局理解软件架构。但软件架构真的只是资深工程师的专利吗?这个也不见得。古人作文,讲究立意为先。今天工程师做项目和产品,也应该先立意。这个意,就是指要有高度。工程师入门能从软件架构的高度出发,看待软件问题,相信对软件的理解,会更加深刻一些。因此,我总结了软件架构的六个步骤,供嵌入式工程师参考。

上次谈到了嵌入式软件架构的第一个步骤,抽象层。建立抽象层(HAL或者DAL)的目的,是为了隔离硬件,让代码与硬件无关。即使整个项目的代码,由某工程师一个人完成,抽象层仍是是有必要的。但这次我们要聊的是,统一的基础设施,这是针对多人合作一个项目,或者多个项目共享同一个系统架构的情况。如果说,你手头的项目,没有与他人合作,也不会有后续的相关项目,软件基础设施这一步,可以直接跳过。

基础设施,分为硬件基础设施和软件基础设施。硬件基础设施,包含常用器件库、封装库、原理图库和硬件参考设计等等;而今天我们讨论的重点,主要在于软件基础设施。软件基础设施包含以下内容:

  • 基础数据结构。
  • 底层库。比如C标准库、加密库、校验库、工具库等等。
  • 操作系统/调度机制。包含操作系统或及调度相关服务。
  • 中间件。比如文件系统、协议栈、数据库等。
  • 框架与机制。比如消息通信机制、事件驱动机制、状态机框架、行为树框架。
  • 工具支持。
  • 统一的编程工具链。
  • 统一的代码风格和编程规范。

在一些小公司粗放的开发模式中,并不规定工程师所依赖的软件平台、硬件平台和工具,而是由工程师自己决定。很多工程师,也喜欢这种自由奔放的开发模式,认为只有在这种环境中,才能发挥自身创造力。这种认知是有偏差的,这个我们后续找机会详细讨论。随着小公司研发能力的提升,对软件基础设施进行约束和规定,几乎是注定的事情。因为软件区别于其他技术的本质,是在于其复用性。复用程度越高的软件,质量越高,对开发效率和开发质量的提升,就越大。无论从公司的降本增效还是科学管理的角度,都有着强大的动机,去统一软件基础设施。当软件的基础设施统一之后,会产生如下优点:

  • 软件质量提升,风格的高度一致性
  • 软件复用性,会提升至一个新的水平
  • 将可复用的功能,尽量抽象到基础设施层,减少软件冗余,提升开发效率。
  • 为高层模块提供约束和纪律
  • 有利于团队的技术积累和技术传承
  • 有利于团队的技术培训
  • 是团队进行单元测试和测试驱动开发,以及跨平台开发的前提。

因此,是否统一,根本不是一个有争议的问题;如何统一,才是今天我们的重点。

一、基础类型与宏定义

统一的软件基础设施的前提,就是声明统一的基础数据类型和宏,以克服不同的硬件平台和编译器的差异性。比如下面是我从自己的开源项目EventOS中摘录出来的代码,不见得很完整,只能代表在我在项目里需求。

#include <stdbool.h>

typedef unsigned int                    eos_u32_t;
typedef signed int                      eos_s32_t;
typedef unsigned short                  eos_u16_t;
typedef signed short                    eos_s16_t;
typedef unsigned char                   eos_u8_t;
typedef signed char                     eos_s8_t;
typedef bool                            eos_bool_t;

#define EOS_NULL                        ((void *)0)

#define EOS_U32_MAX                     (0xffffffffU)
#define EOS_U32_MIN                     (0U)
#define EOS_U16_MAX                     (0xffffU)
#define EOS_U16_MIN                     (0U)
#define EOS_U8_MAX                      (0xffU)
#define EOS_U8_MIN                      (0U)

编译器相关的宏定义。使用宏,屏蔽掉编译器的差异,会

/* Compiler Related Definitions */
#if defined(__ARMCC_VERSION)           /* ARM Compiler */

    #define eos_section(x)              __attribute__((section(x)))
    #define eos_used                    __attribute__((used))
    #define eos_align(n)                __attribute__((aligned(n)))
    #define eos_weak                    __attribute__((weak))
    #define eos_inline                  static __inline

#elif defined (__GNUC__)                /* GNU GCC Compiler */

    #define eos_section(x)              __attribute__((section(x)))
    #define eos_used                    __attribute__((used))
    #define eos_align(n)                __attribute__((aligned(n)))
    #define eos_weak                    __attribute__((weak))
    #define eos_inline                  static __inline

#elif defined (__IAR_SYSTEMS_ICC__)     /* for IAR Compiler */

    #define eos_section(x)              @ x
    #define eos_used                    __root
    #define eos_align(n)                PRAGMA(data_alignment=n)
    #define eos_weak                    __weak
    #define eos_inline                  static inline

#else
    #error "The current compiler is not supported. "
#endif

一些常用的数据结构。这些数据结构,与硬件和编译器无关,是在代码中频繁使用,并在多个模块间共享的数据结构,有必要将其提升至基础设施的层面进行支持,以避免各个模块,对同一个数据类型,进行不同的定义带来的数据转换问题。这些数据结构,与产品紧密相关,不同的产品类型之间,各自是不同的。比如下面的定义。

typedef struct eos_date
{
    eos_u32_t year               : 16;
    eos_u32_t month              : 8;
    eos_u32_t day                : 8;
} eos_date_t;

typedef struct eos_time
{
    eos_u32_t hour               : 8;
    eos_u32_t minute             : 8;
    eos_u32_t second             : 6;
    eos_u32_t ms                 : 10;
} eos_time_t;

typedef struct eos_imu_data
{
    float acc[3];
    float gyr[3];
    float mag[3];
} eos_imu_data_t;

二、操作系统

有些芯片的资源太小,是不能运行操作系统的。这些芯片,一般而言,也没有办法建立严谨的嵌入式软件架构,我们会在后续《小资源芯片的软件开发平台》中,单独进行讨论。这里只讨论芯片的。

不同的芯片,所能跑的操作系统是不同的。但如果要建立软件基础设施,应该尽量选取同一个操作系统。在现存的操作系统中,FreeRTOS和国产RT-Thread对各种不同的硬件架构的芯片,支持比较广泛,可以作为RTOS的首选。而当产品线异常丰富时,特别是使用了某些小众芯片,或者使用芯片商提供的操作系统时,就没有办法建立统一的软件基础设施。这时,有两个办法解决这一问题:

  • 编写高层模块时,使用宏定义和条件编译,选择对应的RTOS API。这种一般用于所使用的操作系统较少的情况,比如说只有两三种。

static void *task_handler = NULL;

static void task_func_module_one(void *parameter);

void module_one_init(void)
{
    /* Newly creating a task to run the module. */
#if (EOS_RTOS_NAME == EOS_RTOS_NAME_FREERTOS)
    xTaskCreate(task_func_module_one,
                "TaskModule", 2048, NULL, 2,
                (TaskHandle_t *)&task_handler);
#elif (EOS_RTOS_NAME == EOS_RTOS_NAME_RTTHREAD)
    task_handler = rt_thread_create("led1", task_func_module_one, NULL,
                                    2048, 2, 20);
#else
    eos_assert(false);
#endif

    eos_assert(task_handler != NULL);
}

/* The task function of the module one. */
static void task_func_module_one(void *parameter)
{
    (void)parameter;

    /* Initialization. */

    while (1)
    {
        /* Add the task function. */
    }
}

  • 建立操作系统抽象层(OSAL,Operating System Abstraction Layer),以屏蔽操作系统的差异,使高层模块依赖于OSAL。这种情况适合于资源丰富的情况。著名的POSIX标准,就是为了建立OSAL的,FreeRTOS和RT-Thread都在不同程度上对POSIX标准进行了支持;在 v嵌入式领域,CMSIS_OS也是为了建立操作系统的统一接口;但无论是POSIX和CMSIS_OS,被各RTOS支持的力度是不同,因此如果我们产品中需要建立严谨的嵌入式软件架构,还是要建立属于自己的OSAL,以便屏蔽掉操作系统的不同带来的差异。

三、中间件

中间件有很多类型,文件系统、各种协议栈、数据库、日志模块、Shell模块,都属于中间件的范畴。但在大部分情况下,这些也都属于软件基础设施的范畴。因为我们一旦选择某个中间件,一般来说,是没有必要更换的,正是由于这种稳定性,中间件也可以纳入软件基础设施的范畴。以下是我经常使用的开源中间件:

  • FatFS
  • LwIP
  • FlashDB
  • uC/Modbus
  • CAN Festival
  • letter-shell

开源中间件,只占据了一小部分。实际产品中,中间件的大部分,都是产品或者项目私有的代码。我日常所使用的主要有:

  • 日志模块
  • 数据采集模块
  • 通讯传输层协议
  • 通讯应用层协议
  • 文件传输协议
  • OTA功能
  • 时间同步

中间件,占据了软件基础设施的大部分内容。在产品开发中,之所以软件复用性能够做到越来越高,中间件的积累,是一个很重要的原因。

四、框架与机制

在不同的产品上,开发嵌入式软件,除了RTOS之外,很多产品还需要一些框架的支持。常见的框架包括:

  • 外设与驱动框架
  • 设备框架
  • 消息框架
  • 状态机框架
  • 行为树框架
  • 事件驱动框架

这些框架的使用,与产品的特性相关,由产品和需求所决定。比如家庭服务机器人中,需要应用状态机框架和行为树框架,来应对复杂的应用层逻辑。而某些应用层逻辑比较简单的产品,就不需要使用状态机和行为树。

软件基础设施与硬件的关系

嵌入式软件有一个区别于其他软件领域的重要特性,那就是直接依赖于硬件。软件基础设施,有很多也是需要硬件去体现和承载。比如文件系统,在规定某个源代码比如FatFS作为其文件系统解决方案的同时,所伴随的硬件驱动程序和硬件推荐设计,也往往被固化,以便在下一个项目中进行复用,并节约时间。

对于一些重要的且复杂的软件基础设施,如文件系统、网络等,由于调试和测试都比较耗时,一般推荐固化硬件设计的方式。硬件工程师,应优先对这些重要且复杂的软件基础设置,分配硬件资源,而硬件的其他工程,比如IO、ADC等,再行分配。

结论

嵌入式软件基础设施,非常重要,根据项目与产品的不同,他所包含的内容也不尽相同。一般在项目启动时,就会初步选定一些软件基础设施的内容,比如RTOS、协议栈、文件系统等等。

需要指出的是,软件基础设施,也不是不变的,而是随着产品开发发展,不断会有新的组件和元素,加入到软件基础设施中去,也有可能会剥离掉旧的组件,就像生物的新陈代谢。只是,软件基础设施的新陈代谢,要温和,要相对稳定,添加和删除,都要执行谨慎原则。
施,非常重要,根据项目与产品的不同,他所包含的内容也不尽相同。一般在项目启动时,就会初步选定一些软件基础设施的内容,比如RTOS、协议栈、文件系统等等。

需要指出的是,软件基础设施,也不是不变的,而是随着产品开发发展,不断会有新的组件和元素,加入到软件基础设施中去,也有可能会剥离掉旧的组件,就像生物的新陈代谢。只是,软件基础设施的新陈代谢,要温和,要相对稳定,添加和删除,都要执行谨慎原则。

### TC387嵌入式软件开发架构分析 TC387是一款基于ARM Cortex-M系列的微控制器,广泛应用于工业控制、消费电子和物联网等领域。在设计其嵌入式软件架构时,需要综合考虑硬件特性、实时性要求以及软件可维护性和扩展性。以下是对TC387嵌入式软件开发架构的详细分析。 #### 1. 抽象层与硬件隔离 为了提高软件的可移植性和可维护性,嵌入式软件架构中通常会引入抽象层[^1]。对于TC387,可以通过定义硬件抽象层(HAL)来隔离底层硬件细节。HAL的主要功能包括: - 提供统一的API接口,屏蔽不同硬件平台之间的差异。 - 简化上层应用对硬件资源的访问,例如GPIO、UART、SPI等外设的初始化和操作。 通过这种方式,即使硬件平台发生变化,只需修改HAL部分即可,而无需改动上层应用逻辑。 #### 2. 数据问题的解决 尽管硬件抽象层可以解决部分问题,但数据处理仍然是嵌入式软件开发中的核心挑战[^2]。在TC387的软件架构中,可以通过以下方法解决数据相关的问题: - **软件基础设施**:构建合理的中间件或框架,用于数据的采集、存储和传输。 - **数据机制**:制定清晰的数据流规则,确保数据的一致性和完整性。 例如,在设计按键防抖逻辑时,可以采用状态机的方式替代传统的延时方法,从而减少CPU资源的浪费[^2]。 #### 3. 嵌入式软件架构的选择 根据TC387的应用场景,可以选择适合的嵌入式软件架构[^3]。以下是几种常见的架构方案及其适用条件: - **前后台系统**:适用于任务简单且实时性要求较低的系统。所有任务在一个无限循环中顺序执行。 - **操作系统调度**:适用于复杂系统,尤其是需要多任务并发处理的情况。使用RTOS(如FreeRTOS)可以有效管理任务优先级和资源分配。 - **混合架构**:介于前后台系统和操作系统之间的一种设计方案。适用于程序较复杂但实时性要求不高的嵌入式系统[^3]。 对于TC387,如果任务数量较多且存在实时性需求,则建议采用混合架构或引入RTOS。这样可以在保证实时性的同时,充分利用CPU资源。 #### 4. 六步设计法的应用 参考嵌入式软件架构的六步设计法[^1],可以为TC387的开发提供明确的方向。以下是六个关键步骤的简要说明: 1. **需求分析**:明确系统的功能需求和性能指标。 2. **模块划分**:将系统划分为若干独立的功能模块。 3. **抽象层设计**:实现硬件与软件的隔离。 4. **数据流设计**:定义数据的采集、处理和传输路径。 5. **任务调度**:选择合适的任务调度机制,确保实时性要求。 6. **测试与优化**:通过测试验证系统功能,并进行必要的性能优化。 #### 示例代码:按键防抖逻辑 以下是一个基于状态机的按键防抖示例代码,展示了如何避免传统延时方法带来的资源浪费。 ```c typedef enum { STATE_IDLE, STATE_PRESSED, STATE_RELEASED } ButtonState; ButtonState currentState = STATE_IDLE; uint32_t lastDebounceTime = 0; void buttonCheck(void) { uint32_t currentTime = HAL_GetTick(); if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0)) { // 按键按下 if (currentState == STATE_IDLE && (currentTime - lastDebounceTime) > 10) { currentState = STATE_PRESSED; lastDebounceTime = currentTime; // 执行按键按下动作 } } else { // 按键释放 if (currentState == STATE_PRESSED && (currentTime - lastDebounceTime) > 10) { currentState = STATE_RELEASED; lastDebounceTime = currentTime; // 执行按键释放动作 } } } ``` ### 总结 TC387嵌入式软件开发架构的设计需要综合考虑硬件特性、实时性要求以及软件的可维护性和扩展性。通过引入抽象层、合理解决数据问题并选择适当的架构方案,可以有效提升系统的性能和可靠性[^1]。
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

狗哥嵌入式

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值