18、嵌入式软件开发的最佳实践与示例

嵌入式软件开发的最佳实践与示例

1. CI 流程中的 Doxygen

在持续集成(CI)流程中包含 Doxygen 是很有必要的。要求 Doxygen 成功运行且无错误是“构建通过”的一部分。若存在 Doxygen 警告,建议“构建失败”。就像编译器警告一样,大多数 Doxygen 警告是良性的,但并非全部如此。而且,在编写了数千行代码后再去解决这些警告会很痛苦。

2. 抽象接口

抽象接口是实现解耦模块的重要手段,它不一定要是 C++ 中纯虚函数或虚函数的集合。在相关概念中,抽象接口是指定义了一种行为,其实现具有延迟绑定的接口,即不是源代码时间绑定。C++ 中的纯虚函数或虚函数是延迟绑定的一种机制,但也存在其他延迟绑定时间。

2.1 编译时绑定

编译时绑定的一个例子是 Late Header 模式(LHeader),它使用预处理器将绑定延迟到编译时。以下是使用 LHeader 模式定义访问 UART 寄存器的 HAL 接口的示例代码:

src/Driver/Uart/Hal.h
/** @file
    This file defines a hardware abstraction layer (HAL) for accessing a
    hardware register based UART.

    NOTE: NO 'Initialization' method is provided/defined in this interface,
          this is intentional! The initialization of the baud rate, number
          of start/stop bits, etc. is VERY platform specific - which
          translate to very cumbersome/inefficiencies in trying to make a
          generic one size-fits-all init() method.
          What does this mean? Well first the application is RESPONSIBLE
          for making sure that the platform specific initialization happens
          BEFORE any of the Cpl drivers are started. Second, this interface
          ASSUMES that the required 'Uart Handle' is returned/created (at
          least conceptually) from this platform specific init() routine.
 */
#include <stdint.h>
#include "colony_map.h"
/*-------------- PUBLIC API ---------------------------------------------*/
/** This data type defines the platform specific 'handle' of a UART.  The
    'handle' is used to unique identify a specific UART instance.
 */
#define Driver_Uart_Hal_T               Driver_Uart_Hal_T_MAP
/*-------------- PUBLIC API ---------------------------------------------*/
/** This method places the specified byte in to the transmit data register
    and initiates a transmit sequence.
    Prototype:
        void Driver_Uart_Hal_transmitByte( Driver_Uart_Hal_T hdl,
                                           uint8_t byteToTransmit );
 */
#define  Driver_Uart_Hal_transmitByte   Driver_Uart_Hal_transmitByte_MAP
/** This method enables the UART transmitter
    Prototype:
        void Driver_Uart_Hal_enableTx( Driver_Uart_Hal_T hdl );
 */
#define  Driver_Uart_Hal_enableTx       Driver_Uart_Hal_enableTx_MAP
/** This method returns the last received byte, independent of any Rx
    errors
    Prototype:
        uint8_t Driver_Uart_Hal_getRxByte( Driver_Uart_Hal_T hdl );
 */
#define  Driver_Uart_Hal_getRxByte      Driver_Uart_Hal_getRxByte_MAP
/** This method enables the UART receiver
    Prototype:
        void Driver_Uart_Hal_enableRx( Driver_Uart_Hal_T hdl );
 */
#define  Driver_Uart_Hal_enableRx       Driver_Uart_Hal_enableRx_MAP
src/Bsp/Renesas/Rx/u62n/Yrdkr62n/uarts.h
/** This method requests that the specified byte be transmitted (i.e.
    it loads the TX data register and starts the TX process)
 */
#define Bsp_uartTransmitByte(p,b)   \
    BSP_SCI_INDEX_TO_INSTANCE(p).TDR=(b)
/** This method enables RX & TX at the same time. NOTE: The 62N
    effectively does not allow enabling RX & TX at different times.
 */
#define Bsp_uartEnableRX_TX(p)      \
    BSP_SCI_INDEX_TO_INSTANCE(p).SCR.BYTE |= 0x30
/** This method returns the last byte received
 */
#define Bsp_uartGetReceivedByte(p)  \
    BSP_SCI_INDEX_TO_INSTANCE(p).RDR
src/Bsp/Renesas/Rx/u62n/Yrdkr62n/hal_mappings_.h
// Driver::Uart
#include “Bsp/Renesas/Rx/u62n/Yrdkr62n/uarts.h”
///
#define Driver_Uart_Hal_T_MAP                   uint8_t
///
#define Driver_Uart_Hal_transmitByte_MAP        Bsp_uartTransmitByte
/// Enable BOTH RX & TX because that is the way the chip works
#define Driver_Uart_Hal_enableTx_MAP            Bsp_uartEnableRX_TX
///
#define Driver_Uart_Hal_getRxByte_MAP           Bsp_uartGetReceivedByte
/// Enable BOTH (see above)
#define Driver_Uart_Hal_enableRx_MAP            Bsp_uartEnableRX_TX

2.2 链接时绑定

链接时绑定是最容易创建的抽象接口。只需定义一组函数或类,然后为该接口提供单独的实现。通过设置构建脚本,为应用程序构建和链接所需的实现。以下是 Colony.core C++ 库中的致命错误接口示例:

src/Cpl/System/FatalError.h
/** This class defines methods for handling fatal errors encountered by
    an application.  The implementation of the methods is platform
    dependent.
 */
class FatalError
{
public:
    /** This function is used to process/log a FATAL error.  The supplied
        error message will be logged to a "storage media" along with other
        useful info such as the current task, stack dump, etc. In addition,
        THE APPLICATION AND/OR SYSTEM WILL BE "STOPPED".  Stopped can mean
        the application/system is exited, restarted, paused forever, etc.
        The type of "storage media", additional info, stopped behavior,
        etc. is defined by the selected/linked implementation.
        NOTE: Applications, in general should NOT call this method - the
        application should be DESIGNED to handle and recover from errors
        that it encounters/detects.
     */
    static void log( const char* message );
    /** Same as above, but "value" is also logged.  This method allows
        additional information to be logged without resulting to a string
        formatting call (which may not work since something really bad just
        happen).
     */
    static void log( const char* message, size_t value );
    /// Printf style formatted message
    static void logf( const char* format, ... );
public:
    /** Same as log(..) method, except NO "...other useful info
        such as current task,..." is logged, AND the "storage media" is
        restricted to 'media' that is ALWAYS available.
        This allows routines that are supplying the extra info OR routines
        that write to media to be able to log fatal errors WITHOUT creating
        a recursive death loop.
     */
    static void logRaw( const char* message );
    /// Same as log(..) method, except NO 'extra info' and restricted media
    static void logRaw( const char* message, size_t value );
};
不同平台的实现示例
  • src/Cpl/System/Win32/_fatalerror/FatalError.cpp
void FatalError::log( const char* message )
{
    fprintf( stderr, "\n%s%s\n", EXTRA_INFO, message );
    Shutdown::failure( OPTION_CPL_SYSTEM_FATAL_ERROR_EXIT_CODE );
}
void FatalError::log( const char* message, size_t value )
{
    fprintf( stderr, "\n%s%s [%p]\n", EXTRA_INFO, message, (void*) value );
    Shutdown::failure( OPTION_CPL_SYSTEM_FATAL_ERROR_EXIT_CODE );
}
void FatalError::logf( const char* format, ... )
{
    va_list ap;
    va_start( ap, format );
    fprintf( stderr, "\n%s", EXTRA_INFO );
    vfprintf( stderr, format, ap );
    fprintf( stderr, "\n" );
    Shutdown::failure( OPTION_CPL_SYSTEM_FATAL_ERROR_EXIT_CODE );
}
void FatalError::logRaw( const char* message )
{
    log( message );
}
void FatalError::logRaw( const char* message, size_t value )
{
    log( message, value );
}
  • src/Cpl/System/FreeRTOS/_fatalerror/FatalError.cpp
void FatalError::log( const char* message )
{
    if ( xTaskGetSchedulerState() == taskSCHEDULER_RUNNING )
    {
        Cpl::Io::Output* ptr =
            Cpl::System::Trace::getDefaultOutputStream_();
        ptr->write( EXTRA_INFO );
        ptr->write( message );
        ptr->write( "\n" );
        // Allow time for the error message to be outputted
        Cpl::System::Api::sleep( 250 );
    }
    Shutdown::failure( OPTION_CPL_SYSTEM_FATAL_ERROR_EXIT_CODE );
}
void FatalError::log( const char* message, size_t value )
{
    if ( xTaskGetSchedulerState() == taskSCHEDULER_RUNNING )
    {
        int              dummy = 0;
        Cpl::Io::Output* ptr   =
            Cpl::System::Trace::getDefaultOutputStream_();
        ptr->write( EXTRA_INFO );
        ptr->write( message );
        ptr->write( ". v:= " );
        ptr->write( Cpl::Text::sizetToStr( value,
                                           buffer_.getBuffer( dummy ),
                                           SIZET_SIZE, 16 ) );
        ptr->write( "\n" );
        // Allow time for the error message to be outputted
        Cpl::System::Api::sleep( 150 );
    }
    Shutdown::failure( OPTION_CPL_SYSTEM_FATAL_ERROR_EXIT_CODE );
}
void FatalError::logf( const char* format, ... )
{
    va_list ap;
    va_start( ap, format );
    if ( xTaskGetSchedulerState() == taskSCHEDULER_RUNNING )
    {
        buffer_ = EXTRA_INFO;
        buffer_.vformatAppend( format, ap );
        Cpl::System::Trace::getDefaultOutputStream_()->write( buffer_ );
    }
    Shutdown::failure( OPTION_CPL_SYSTEM_FATAL_ERROR_EXIT_CODE );
}
void FatalError::logRaw( const char* message )
{
    log( message );
}
void FatalError::logRaw( const char* message, size_t value )
{
    log( message, value );
}

3. C++ 纯虚和虚构造

如果项目允许使用 C++,应充分利用该语言创建具有运行时绑定的抽象接口和虚方法的能力。

4. 数据模型

数据模型虽不是传统意义上的抽象接口,但它能提供类似的解耦效果。以下是数据模型模式的一些语义和最佳实践总结:
- 模型点包含数据而非对象,且除了基本的值范围检查规则外,不包含业务规则或执行策略。
- 模型点应根据其包含的数据值和使用语义进行唯一类型化。
- 模型点之间没有同步,单个模型点实例的操作是原子的。若需要跨模型点同步,需由应用程序提供。
- 不要跨函数调用缓存模型点值。可以在方法中读取模型点值并使用局部值,但不要将其存储在类成员或全局变量中供后续使用。
- 在对读取的值进行操作之前,始终检查模型点的有效状态。
- 模型点的更改通知语义保证客户端在模型点的值或状态发生更改时会收到通知,但不保证对所有更改都进行通知,只保证最后一次更改。
- 建议所有模型点静态实例化,以避免动态内存问题,并确保所有模型点实例始终在作用域内。
- 使用命名约定或 C++ 命名空间,避免模型点实例名称污染全局命名空间。
- 建议应用程序将所有模型点初始化为无效状态,以确保没有从有效到无效的错误转换,并强化先检查模型状态的行为。
- 对于非特定于应用程序或领域的类和模块,始终提供模型点实例的引用或指针,避免硬编码模型点实例名称。
- 对于特定于应用程序和领域的类,可以使用硬编码的模型点实例名称,以减少传递引用和指针的构造和初始化开销。
- 建议数据模型框架支持模型点的基于文本的序列化,且所有模型点类型都具有此功能。
- 建议数据模型框架支持符号命名的模型点实例。
- 建议数据模型框架支持锁定模型点值的概念,这在测试和调试场景中很有用。

5. 构建系统

相关项目使用 NQBP 构建系统,但本身与构建系统无关。不过,对构建系统和构建脚本有以下要求和最佳实践:
- 构建应基于命令行以支持自动化。虽然这不是绝对要求,但从开发者的 IDE 构建和从完全集成的 CI 工具集构建产生相同结果的情况很少。
- 持续集成构建机器和开发者使用相同的构建脚本。
- 构建应创建应用程序(包括所有变体)并支持无限数量的单元测试。构建脚本必须具有可扩展性,因为单元测试或应用程序变体的数量会随时间增加。
- 开发者向构建脚本添加新文件或新目录的工作量应较低。
- 位于源代码同一目录中的构建脚本文件不应包含“特定于最终输出”的详细信息,因为同一目录可能用于不同的输出映像。
- 构建脚本应能够选择性地构建目录,不应假设递归构建目录及其所有子目录是可行的。
- 构建脚本应确保在生成和引用派生对象时不丢失源代码文件的路径信息。
- 生成的对象文件不应放在与源代码相同的目录中,因为不同的构建目标(如应用程序、功能模拟器、单元测试)可能会产生不同的对象文件。
- 构建引擎应独立于主机、编译器和目标平台。

6. 恒温器示例

6.1 功能概述

恒温器示例应用程序展示了相关原则的实际应用,虽然不是一个完整的应用程序,但提供了一个有意义的示例。其功能包括:
- 可控制单级空调和最多三级加热的熔炉。
- 可控制独立的最多三级加热的熔炉。
- 有 PID 控制来调节温度。
- 有“自动模式”来确定加热或冷却操作。
- 有基本的配置错误警报。
- 有空气过滤器监测和更换警报。
- 可持久存储恒温器配置和用户设置。
- 有在 Windows 和 Linux 下运行的功能模拟器。
- 有一个简单的房屋模拟器为 PID 控制提供温度反馈。
- 使用 Adafruit Grand Central M4 Arduino 板(带有 Atmel SAMD51 微控制器)作为目标硬件。
- 有命令行界面(适用于目标平台和 PC 平台)。

6.2 正式要求

需求编号 需求描述
E001 应能够控制单级冷却单元。
E002 应能够控制最多三级的熔炉。
E003 当室内单元配备变速电机时,应能够提供可变的室内风扇速度信号。
E004 应能够控制固定速度的室内风扇电机。
E005 应能够连接远程室内温度传感器,用于温度控制(代替板载温度传感器)。如果检测到远程传感器故障,温度控制应恢复到板载传感器。

6.3 恒温器示例的工作流程

下面是恒温器示例应用程序的工作流程 mermaid 流程图:

graph TD
    A[启动应用程序] --> B[初始化硬件和配置]
    B --> C{选择操作模式}
    C -->|自动模式| D[PID 控制温度调节]
    C -->|手动模式| E[用户手动控制]
    D --> F{温度是否达标}
    F -->|是| G[维持当前状态]
    F -->|否| H[调整加热或冷却设备]
    E --> I[根据用户输入控制设备]
    G --> J[监测空气过滤器状态]
    H --> J
    I --> J
    J --> K{空气过滤器是否需要更换}
    K -->|是| L[发出更换警报]
    K -->|否| M[继续监测]
    L --> M
    M --> N[检查配置和用户设置]
    N --> O{是否有更改}
    O -->|是| P[保存更改到持久存储]
    O -->|否| Q[继续运行]
    P --> Q

6.4 恒温器示例的代码结构

恒温器示例的代码结构遵循了前面提到的最佳实践,如抽象接口、数据模型等。以下是一个简化的代码结构列表:
- 驱动层 :包含硬件抽象层(HAL)接口的定义和实现,如 UART 驱动等。
- src/Driver/Uart/Hal.h :定义 UART 的 HAL 接口。
- src/Bsp/Renesas/Rx/u62n/Yrdkr62n/uarts.h :提供 UART HAL 接口的具体实现。
- src/Bsp/Renesas/Rx/u62n/Yrdkr62n/hal_mappings_.h :进行编译时绑定,将抽象 HAL 接口符号映射到具体函数。
- 系统层 :处理系统级功能,如致命错误处理。
- src/Cpl/System/FatalError.h :定义致命错误处理的抽象接口。
- src/Cpl/System/Win32/_fatalerror/FatalError.cpp :Windows 平台下致命错误处理的具体实现。
- src/Cpl/System/FreeRTOS/_fatalerror/FatalError.cpp :FreeRTOS 平台下致命错误处理的具体实现。
- 应用层 :实现恒温器的具体功能。
- 包含 PID 控制算法的实现。
- 空气过滤器监测和警报功能的实现。
- 恒温器配置和用户设置的持久存储功能的实现。

6.5 恒温器示例的开发步骤

开发恒温器示例应用程序可以按照以下步骤进行:
1. 需求分析 :明确恒温器的功能需求,如控制空调、熔炉、温度调节、警报等。参考前面提到的正式需求表格。
2. 设计架构 :根据需求设计代码架构,采用抽象接口、数据模型等最佳实践,确保代码的可维护性和可扩展性。
3. 实现驱动层 :编写硬件抽象层(HAL)接口和具体实现,如 UART 驱动。
4. 实现系统层 :实现系统级功能,如致命错误处理。
5. 实现应用层 :实现恒温器的具体功能,如 PID 控制、空气过滤器监测等。
6. 测试和调试 :使用单元测试和功能模拟器进行测试,确保应用程序的正确性。
7. 优化和部署 :根据测试结果进行优化,然后部署到目标硬件上。

6.6 总结

通过恒温器示例应用程序,我们可以看到如何将抽象接口、数据模型、构建系统等最佳实践应用到实际项目中。这些最佳实践可以提高代码的可维护性、可扩展性和可测试性,使嵌入式软件开发更加高效和成功。在开发过程中,遵循这些最佳实践可以避免许多常见的问题,如代码耦合、内存管理问题等。同时,使用命令行构建和持续集成可以提高开发效率,确保代码质量。

希望这些信息对嵌入式软件开发人员有所帮助,能够在实际项目中应用这些最佳实践,开发出高质量的嵌入式软件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值