Вот некоторые из опасений, которые у меня возникли по поводу вашего подхода:
Во-первых, допустим, что периферийное устройство на одной платформе имеет некоторую опцию конфигурации, которой просто не существует для эквивалентных периферийных устройств на других платформах.Есть несколько вариантов, как это сделать, например:
- Жесткое кодирование определенного значения для этого параметра
- Включите файл, который предоставляет значения конфигурации для этого параметра, но непредоставить файл с хал.Каждый проект, использующий 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 с использованием удвоений теста.