【问题标题】:Hardware-Independent C++ HAL for Embedded Systems用于嵌入式系统的独立于硬件的 C++ HAL
【发布时间】:2019-03-04 12:18:33
【问题描述】:

我正在研究如何实现一个自定义 C++ 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;
};   
// 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;

};
// 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);
}
// -----------------------------
// 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 {

};    

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

进一步了解上述内容,我可以看到上述实现对于 I2cPortSpiPortUsbSerialPort 等类运行良好,但对于时钟、硬件定时器等其他非端口相关的外围设备集。

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

【问题讨论】:

  • 你不是在重新实现 mbed-os 吗?和其他试图做你想做的事情的多个操作系统?
  • @KamilCuk 我完全不知道。我是吗?我没有考虑为这些项目采用 RTOS 路线,但如果我几乎是从头开始,也许我应该...
  • 我认为这个问题太宽泛了。前任。 mbed-os 支持 nrf52 stm32 kinetis。我认为 atmel 不见了。并且 PIC(我的天哪!)不见了。但是您可以采用他们提供的抽象并为平台实现它。但是有几个这样的项目可以选择,在github上搜索就可以了。 mbed-os 是用 C++ 编写的,所以我提到了它。集成许多硬件真的非常非常困难,因为每一个都是独一无二的。还有很多(大)项目,都在尝试做和你一样的事情。
  • @Lundin,我必须承认,我用于嵌入式系统的任何 C++ 编译器都没有遇到过问题。我继承的所有代码库都在 C++ 中并且工作正常,它们只是一团糟 - 回到 C 是一大堆其他工作,我什至不会考虑(而且我不明白为什么我真的需要)。我也没有意识到new 关键字仅适用于PC!我正在使用具有 64/128/256kB SRAM 的设备,而不是小型 8k 设备。
  • 当然你有确定性的用例。如果有 5 个外部外围设备,那么您有一个程序应该处理的指定最大值。它不应该处理更多,它需要在启用 5 个时起作用,因此处理这个所需的必要 RAM 正好是 5,这是最坏的情况。它不是在 1 到 5 之间,因为您必须始终为 5 保留空间。如果用户请求 1,则只为 1 分配空间是没有意义的,因为您的程序没有任何意义处理其他 4 个的内存。它必须为最坏的情况保留。

标签: c++ embedded hal


【解决方案1】:

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

首先,假设一个平台上的外围设备具有一些配置选项,而其他平台上的等效外围设备根本不存在这些配置选项。有一些如何处理的选项,例如:

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

前两个不是很灵活(不能在运行时改变),第三个破坏抽象——平台必须提供函数来配置可能不存在的选项,或者SerialPort用户必须知道底层平台的细节。在我看来,所有这些都是混乱代码库的组成部分。

其次,假设一个平台有多个不同的外围设备可以提供相同的功能。例如,我目前正在使用具有USARTLPUART 外设的STM32,它们都可以提供UART 功能。要处理这个问题,您需要在运行时根据端口实例化不同的 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 */
    };
}

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

/* 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 */
    };
}

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

/* elsewhere */
struct MaestroA5135Driver : public GPSDriver
{
    MaestroA5135Driver( hal::Uart& uart );
}
struct MicrochipRN4871Driver : public BluetoothDriver
{
    MicrochipRN4871Driver( hal::Uart& uart );
}
struct ContrivedPositionAdvertiser
{
     ContrivedPositionAdvertiser( GPSDriver& gps, BluetoothDriver& bluetooth );
}

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

/* 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 */
    }
}

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

【讨论】:

  • Sigve,非常感谢您的全面回答——这绝对是我所希望的那种建议。我喜欢你的示例方法,就像你说的那样,它消除了添加额外平台、MCU 变体等的#ifdef 要求。使用你示例中的命名空间,我想using namespace hal::avr 没有理由不能' t 添加在源文件的开头以显式使用该体系结构的类和结构。我会进一步消化这个答案,但感谢您抽出宝贵时间发表评论。
  • 不客气 - 你对 using namespace 的看法是对的,尽管我会小心你暴露平台细节的地方。我将其限制为代码库的两部分。 1) HAL 本身(并且只有平台特定的实现,而不是通用接口),作为底层架构层,以及 2) 包含主要功能的顶层。 (我在我的架构中将此层标记为“运行时”)。在这之间,还有其他层,每个层都可以使用来自较低层的抽象接口 - 但是只有运行时层可以实例化具体实例。
【解决方案2】:

为了提供跨平台接口,我喜欢使用“platform.h”文件,它将所有#defines 保留在源代码之外,同时也避免了大继承树可能产生的代码膨胀。有关详细信息,请参阅 this answerthis one

就接口实际上是什么而言,我同意@Sigve 的观点,即查看用例是最好的设计工具。许多低级外设接口可以简化为init \ read\ write,只需要暴露几个参数。许多更高级别的“HAL”任务通常可以完全与硬件分离,并且只操作数据流。

【讨论】:

    猜你喜欢
    • 2010-10-08
    • 2013-06-14
    • 1970-01-01
    • 2018-04-06
    • 1970-01-01
    • 1970-01-01
    • 2010-09-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多