Аппаратно-независимый C ++ HAL для встраиваемых систем - PullRequest
0 голосов
/ 04 марта 2019

Я исследую, как я могу реализовать собственный 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 {

};    

В приведенном выше коде только один набор операторов #if/#endif в конструкторевысокоуровневого интерфейса (SerialPort) и аппаратно-зависимого кода (доступ к реестру и т. д.) выполняется в рамках частной реализации.

Принимая вышеизложенное, я вижу, что приведенная выше реализация хорошо работает дляклассы, такие как I2cPort, SpiPort, UsbSerialPort, но для других периферийных наборов, не связанных с портами, таких как часы, аппаратные таймеры.

Я уверен, что в вышеупомянутой концепции есть некоторые дыры, может кто-нибудь предложитьиз опыта чего следует избегать или есть ли лучший способ абстрагирования?

Ответы [ 2 ]

0 голосов
/ 06 марта 2019

Для обеспечения кроссплатформенных интерфейсов мне нравится использовать файл "platform.h", который хранит все #defines из исходного кода, а также избегает раздувания кода, которое может генерировать большое дерево наследования.См. этот ответ или этот для деталей.

Что касается интерфейсов, я согласен с @Sigve в том, что рассмотрение вариантов использования - лучший инструмент для проектирования.Многие низкоуровневые периферийные интерфейсы могут быть уменьшены до init \ read\ write с использованием всего нескольких параметров.Многие задачи «HAL» более высокого уровня часто могут быть полностью отделены от оборудования и работают только с потоком данных.

0 голосов
/ 06 марта 2019

Вот некоторые из опасений, которые у меня возникли по поводу вашего подхода:

Во-первых, допустим, что периферийное устройство на одной платформе имеет некоторую опцию конфигурации, которой просто не существует для эквивалентных периферийных устройств на других платформах.Есть несколько вариантов, как это сделать, например:

  • Жесткое кодирование определенного значения для этого параметра
  • Включите файл, который предоставляет значения конфигурации для этого параметра, но непредоставить файл с хал.Каждый проект, использующий hal, должен также предоставить этот файл.
  • Расширить SerialPort для настройки опции (дополнительная функция? Какой-то обратный вызов?).

Первые дване очень гибки (не могут меняться во время выполнения), а третье нарушает абстракцию - платформы должны предоставлять функции для настройки потенциально несуществующих опций, или SerialPort пользователи должны знать детали базовой платформы.Все это, на мой взгляд, является компонентом для грязной кодовой базы.

Во-вторых, скажем, у платформы есть несколько различных периферийных устройств, которые могут обеспечивать одинаковую функциональность.Например, в настоящее время я работаю с STM32, имеющим периферийные устройства USART и LPUART, которые могут обеспечивать функциональность UART.Чтобы справиться с этим, вам нужно либо создавать экземпляры различных pimpl во время выполнения в зависимости от порта, либо иметь один для платформы, которая может обрабатывать.Выполнимо, но может запутаться.

В-третьих, чтобы добавить поддержку другой платформы, теперь вам нужно изменить много другого кода, чтобы добавить новые предложения #elif.Кроме того, #if - #elif - #endif делает код менее читаемым, хотя хорошая подсветка синтаксиса будет затенять неактивные части кода.

Что касается моего совета:

Найдите подходящие интерфейсы.Существует соблазн попытаться создать интерфейс для того, что может делать аппаратное обеспечение - это ведь уровень аппаратной абстракции, верно?Однако я считаю, что лучше взглянуть на это с точки зрения клиентов интерфейса - каковы варианты использования HAL.Если вы найдете простой интерфейс, который может удовлетворить большинство или все ваши варианты использования, он, вероятно, хороший.

(Это, я думаю, вероятно, наиболее актуально для вашей точки зрения о часах и аппаратных таймерах. Задайте себе вопрос: каковы ваши варианты использования?)

Хороший пример здесь - I2C.По моему опыту, большую часть времени конкретное периферийное устройство I2C постоянно является ведущим или постоянно ведомым.Я не часто сталкивался с необходимостью менять во время выполнения между ведущим и ведомым.Имея это в виду, лучше предоставить I2CDriver, который пытается инкапсулировать то, на что способен «типичный» периферийный модуль I2C на любой платформе, или предоставить пару интерфейсов I2CMasterDriver и I2CSlaveDriver, каждый из которых обеспечиваеттолько варианты использования для одного конца транзакций 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 с использованием удвоений теста.

...