嵌入式系统的硬件独立 C++ HAL

web*_*r83 2 c++ embedded hal

我正在研究如何实现自定义 C++ HAL,该 HAL 面向多个微控制器,可能具有不同的架构(ARM、AVR、PIC 等),同时保持一切正常。

我继承了几个大型的、凌乱的代码库,它们在当前状态下无法维护,因此需要更结构化的东西。

在挑选了许多优秀的文章和设计指南后,我正在考虑PIMPL实施。

考虑以下 UART/串行端口示例:

// -----------------------------
// High-level HAL
// -----------------------------

// serialport.h
class SerialPortPrivate;

class SerialPort {

public:
    SerialPort(uint8_t portNumber);
    ~SerialPort();

    bool open();
    void close();

    void setBaudRate(uint32_t baudRate = 115200);

private:
    SerialPortPrivate *_impl;
};   
Run Code Online (Sandbox Code Playgroud)
// serialport_p.h
class SerialPort;

class SerialPortPrivate {

public:
    SerialPortPrivate(uint8_t portNumber, SerialPort *parent) {
        // Store the parent (q_ptr)
        _parent = parent;

        // Store the port number, this is used to access UART
        // specific registers UART->D[portNumber] = 0x10;
        _portNumber = portNumber;
    }
    ~SerialPortPrivate();

    bool open() = 0;
    void close() = 0;

    void setBaudRate(uint32_t baudRate) = 0;

protected:
    uint8_t _portNumber;

private:
    SerialPort *_parent;

};
Run Code Online (Sandbox Code Playgroud)
// serialport.cpp
#include "serialport.h"
#include "serialport_p.h"    

#include "stm32serialport_p.h"
#include "avr32serialport_p.h"
#include "nrf52serialport_p.h"
#include "kinetisserialport_p.h"

SerialPort::SerialPort(uint8_t portNumber) {
#if MCU_STM32
    _impl = new Stm32SerialPortPrivate(portNumber, this);
#elif MCU_AVR32
    _impl = new Avr32SerialPortPrivate(portNumber, this);
#elif MCU_NRF52
    _impl = new Nrf52SerialPortPrivate(portNumber, this);
#elif MCU_KINETIS
    _impl = new KinetisSerialPortPrivate(portNumber, this);
#endif
}

void SerialPort::setBaudRate(uint32_t baudRate) {
    _impl->setBaudRate(baudRate);
}
Run Code Online (Sandbox Code Playgroud)
// -----------------------------
// Low-level BSP
// Hardware-specific overrides
// -----------------------------

// stm32serialport_p.h
class Stm32SerialPortPrivate : public SerialPortPrivate {

};

// nrf52serialport_p.h
class Nrf52SerialPortPrivate : public SerialPortPrivate {

};

// kinetisserialport_p.h
class KinetisSerialPortPrivate : public SerialPortPrivate {

};    
Run Code Online (Sandbox Code Playgroud)

上面的代码#if/#endif在高级接口 ( SerialPort)的构造函数中只有一组语句,并且硬件特定的代码(寄存器访问等)是在私有实现中完成的。

综合以上。此外,我可以看到上面的实施工作以及像类I2cPortSpiPortUsbSerialPort但对于其他非港口相关的外设集像时钟,硬件定时器。

我确定上述概念中存在一些漏洞,任何人都可以从经验中建议要避免的事情,或者是否有更好的抽象方法?

小智 6

以下是我对您的方法的一些担忧:

首先,假设一个平台上的外设具有某些配置选项,而其他平台上的等效外设根本不存在这些配置选项。有一些选项可以解决这个问题,例如:

  • 为该选项硬编码特定值
  • 包含一个提供该选项配置值的文件,但不要为该文件提供 hal.txt 文件。每个使用 hal 的项目也必须提供此文件。
  • 扩展SerialPort以配置选项(额外功能?某种回调?)。

前两个不是很灵活(不能在运行时更改),第三个打破了抽象 - 平台必须提供功能来配置可能不存在的选项,或者SerialPort用户必须了解底层平台的详细信息。在我看来,所有这些都是混乱代码库的组成部分。

其次,假设一个平台具有多个可以提供相同功能的不同外设。例如,我目前正在使用 STM32,它具有USARTLPUART外围设备,两者都可以提供 UART 功能。为了处理这个问题,您需要根据端口在运行时实例化不同的 pimpl,或者为可以处理的平台创建一个 pimpl。可行,但可能会变得混乱。

第三,为了添加对另一个平台的支持,您现在需要修改大量其他代码以添加新的#elif子句。而且#if- #elif-#endif使代码的可读性较差,尽管良好的语法突出显示会遮蔽代码的非活动部分。

至于我的建议:

找到正确的接口。人们很想尝试为硬件的功能创建一个接口 - 这是一个硬件抽象层,对吧?然而,我发现最好从接口客户端的角度来看待它——HAL 的用例是什么。如果您发现一个简单的界面可以满足您的大多数或所有用例,那么它可能是一个不错的界面。

(我认为这可能与您关于时钟和硬件定时器的观点最相关。问问自己:您的用例是什么?)

I2C 就是一个很好的例子。根据我的经验,大多数时候特定的 I2C 外设永远是主机或永远是从机。我并不经常遇到需要在运行时在主从之间进行交换的情况。考虑到这一点,最好是提供一个I2CDriver试图封装任何平台上“典型”I2C 外设功能的接口,或者提供一对接口I2CMasterDriverI2CSlaveDriver,每个接口仅提供该接口一端的用例。 I2C 事务。

我认为后者是最好的起点。典型的用例是主或从,并且用例在编译时是已知的。

将接口限制为“普遍通用”的接口。某些平台可能提供执行 SPI/I2C 功能的单个外设,而其他平台则提供单独的外设。如上所述,相同的外设在平台之间可能具有不同的配置选项。

为“通用”功能提供抽象接口。

提供该接口的特定于平台的实现。这些还可以提供任何所需的特定于平台的配置。

我认为这样做——将“普遍通用”和特定硬件分开——可以使接口更小、更简单。这使得当它开始变得混乱时更容易发现。

这是我将如何解决这个问题的一个例子。首先,为普遍通用的功能定义一个抽象接口。

/* hal/uart.h */
namespace hal
{
    struct Uart
    {
        virtual ~Uart() {};
        virtual void configure( baud_rate, framing_spec ) = 0;
        /* further universally common functions */
    };
}
Run Code Online (Sandbox Code Playgroud)

接下来,创建此接口的实现,其中可以包括特定于平台的详细信息 - 配置选项、资源管理。配置您的工具链以仅包含针对特定平台的工具链

/* hal/avr32/uart.h */
namespace hal::avr
{
    struct Uart : public hal::Uart
    {
        Uart( port_id );
        ~Uart();
        void configure( /*platform-specific options */ );
        virtual void configure( baud_rate, framing_spec );
        /* the rest of the pure virtual functions required by hal::Uart */
    };
}
Run Code Online (Sandbox Code Playgroud)

为了完整起见,让我们添加上面接口的一些更高级别的“客户端”。请注意,它们通过引用获取抽象接口(可以是指针,但不能通过值,因为这会分割对象)。我在这里省略了名称空间和基类,因为我认为没有它们可以更好地说明。

/* hal/uart.h */
namespace hal
{
    struct Uart
    {
        virtual ~Uart() {};
        virtual void configure( baud_rate, framing_spec ) = 0;
        /* further universally common functions */
    };
}
Run Code Online (Sandbox Code Playgroud)

最后让我们将它们放在一个人为的示例中。请注意,特定于硬件的配置是特别完成的,因为客户端无法访问它。

/* main.cpp */
void main()
{
    hal::avr::Uart gps_uart( Uart1 );
    gps_uart.configure(); /* do the hardware-specific config here */
    MaestroA5135Driver gps( gps_uart ); /* can do the generic UART config */

    hal::avr::Uart bluetooth_uart( Uart2 );
    bluetooth_uart.configure(); /* do the hardware-specific config here */
    MicrochipRN4871Driver bluetooth( bluetooth_uart ); /* can do the generic UART config */

    ContrivedPositionAdvertiser cpa( gps, bluetooth );
    for(;;)
    {
        /* do something */
    }
}
Run Code Online (Sandbox Code Playgroud)

这种方法也有一些缺点。例如,将实例传递给更高级别类的构造函数可能会快速增长。所以所有的实例都需要管理。但总的来说,我认为优点超过了缺点 - 例如,易于添加另一个平台,易于使用测试替身对 hal 客户端进行单元测试。