Comments 91
А пользоваться приходится, потому что существующие фирменные решения, известные как CMSIS и HAL слишком сложны, чтобы использовать их в любительских проектах.Извините, но мне кажется сложным ваш пример. По сути, вы сейчас начали писать альтернативу вышеназванным библиотекам, но только на C++. К тому же, в статье не хватает объяснения для закоренелых сишников, как включить, например, C++ в своем проекте.
Описанный тут вариант использует всю мощь компил-тайма, и после компиляции не вызывает неконтролируемого программистом поведения.
А закоренелым Сшникам ничего не объяснить, увы. Для них есть статьи как перейти на плюсы.
Увы, да. Зачастую, приходится общаться с коллегами, программирующими МК. Большая часть из них, не любит плюсы от слова совсем. А ведь С++ это не только объекты и STL. Впрочем, многие программирующие на плюсах, против использования части STL, использующей динамическое выделение памяти в МК, и это, на мой взгля нормально. Что же касается шаблонов и строгой типизации,- это здорово.
По, моему, Пример хороший, пользоваться им очень просто…
Да и класс получился простым...
Уже есть очень многое, включая полную поддержку USB HOST (MSC + HID), scmRTOS, FATfs.
Проект активно развивается, есть поддержка и STM32F103, но частичная — я в основном сосредоточился на STM32F407, для которого и начинал писать данный проект.
Но подход с шаблонами возьму на заметку — может и обновлю свои классы в репе ;)
Очень много препроцессора.
Нет поддержки векторов прерываний в памяти с динамической сменой вектора.
2) Постепенно ухожу от него;
3) Есть, ещё как есть:
#ifdef USE_MEMORY_ISR
__attribute__((section(".isr_vector"))) const ISR::ShortVectors interruptsVectorTable =
#else
volatile __attribute__((section(".isr_vector"))) const ISR::Vectors interruptsVectorTable =
#endif
{
#ifdef USE_MEMORY_ISR
(uint32_t)&_estack,
ISR::Reset
};
Ну и кусок из стандартного init'а:
/* Configure the Vector Table location add offset address ------------------*/
#ifdef VECT_TAB_SRAM
SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM */
#else
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH */
#endif
Опять-таки — данным функционалом не пользовался, потому не проверял пока…
Один мой знакомый из компании, где я раньше работал (Antilatency), как раз таки писал подобный код, с генерацией дескрипторов в Compile-Time. Но в Alt-е весь HAL был свой, написанный на C++. Работал и на STM32 и на Nordic.
На stack overflow множество раз обсуждали почему плохо писать на C++ ядерные драйвера, это же относится и к МК. Вывод во всех эти обсуждениях простой: писать можно, но приходится быть очень осторожным и надо неплохо понимать, что же генерирует компилятор при использовании тех или иных конструкций C++.
Не сказал бы что код, после оптимизации получается менее оптимальным. Смотря какие фишки языка использовать. Template + constexpr позволяет считать битовые маски в Compile-Time-режиме. static_assert, не даст скомпилировать код, который не должен компилироваться, что ускоряет разработку. enum class позволяет использовать именованные типы полей, что упрощает читабельность. Строгая типизация, позволяет допустить меньше ошибок. Умные указатели, помогают предотвартить утечки памяти.
А если правилько писать код, то еще и оптимизровать удается серьезно. Например, в операции установки группы полей, можно не читать регистр, если он пишется целиком, а это позволяет пропустить не только команду чтения, но и барьерные инструкции, если они используются. Это самое простое. Если же сравнивать код, скомпилированный, например при помощи GCC (g++), с опцией O0, зачастую, все не очень хорошо. Но как только применяешь оптимизацию O1,O2,O3,Os,- код оптимизируется настолько что не отличается от C, а иногда и превосходит его по оптимизации.
В микроконтроллерах и драйверах, просто не стоит использовать ту часть STL, которая вызывает динамические аллокаторы. Во возможности, лучше вообще без них. Если приперло, то можно использовать свой аллокатор. В драйверах, иногда используются свои пулы памяти. Драйвера под Windows, разделяют память на pageble и non-pagable. Пишем свой аллокатор и все. Если МК, тоже можно использовать аллокатроы в памяти DMA, но обычно, проще статически назначить. RTTI и исключения в этом случае просто отключаются и по старинке передаем Result. Но и разработчики драйверов, это как правило дорогие специалисты, поскольку стек их знаний значительно шире в аппратной части и ядре ОС.
Я видал несколько реализаций std под МК. Конечно, они не 100% соответствуют стандарту.
Std это неотъемлемая часть стандарта и поставщик компилятора, должен ее поставлять… Например IAR поставляет свою реализацию, соответствующую стандарту, там есть вещи не реализованные до конца, например thread, или atomic, потому что их реализации зависят от того какую РТОС, например вы используете, но есть заготовку пустые, для того, чтобы могли реализовать это… И пользоваться как обычным std..
В нем мы пытаемся на С++ разработать слой аппаратного описания ресурсов контроллера и слой асинхронного управления этими ресурсами.
Охохо… Посмотрел немного, как это будет работать с РТОС? SPI если без DMA брать, я бы сделал через интерфейс, сделал бы пару виртуальных функций, типа ReadAndWrite, открыть сессию, чтобы ресурс захватить, закрыть сессию. И работал бы так, например драйвер лсд открывает сессию, конфигурит AЦП под себя, работает с SPI, пока он работает, другой потребитель, например драйвер АЦП курит, как только сессия закрылась, АЦП открывает сессию, конфигурит этот же SPI под себя, работает с ним, закрывает сессию.
Для UART бы подписчиков сделал на обработку прерываний, так как через UART в основном только один потребитель работает, например Modbus, подписался бы на обработку по приему и передаче…… В общем непонятно, зачем глючный HAL оборачивать и ограничивать себя в дизайне, можно изначально продумать все хорошо, отвязать абстрактную аппаратную часть от логики по другому.
Вместо #define MASK 0xF, используйте static constexpr uint32_t Mask = 0xF;
Чем длиньше выражение тем более ясно намерение? ideone.com/4UCivk
static constexpr int fn(int a) { while(a) a=(a*5)&255; return a; }
static constexpr int zero=fn(0);
static constexpr int twix=fn(1);
На уровне языка — тип разный. У Mask в первом варианте тип — uint32_t, во втором — некоторый безымянный анонимный енам.
И что, кроме незнания, мешает написать так
enum
{
Mask = 0x0FUL,
}
?
По идее const uint32_t Mask, если не брать от него адрес, должен просто в код подставляться как число и всё, не отличаясь в этом плане от #define. Но GCC иногда делает не так, даже с дефайном.
А ваш вариант самый правильный.
По идее const uint32_t Mask не должен подставлять число, так как это переменная, на которую может ссылаться указатель и она отличается от #define, тем что к ней можно адресоваться, поэтому GCC решает так как ему вздумается исходя из алгоритма вашего кода, и на это надеяться не надо… А вот constexpr по идее должен.
А у constexpr переменной значение должно быть известно на этапе компиляции.
Но вот объявляться и инициализироваться она может в ран тайме.
Приведите пример. Я не могу представить, как это сделать без неопределенного поведения.
Квалификация объекта const требует его немедленной инициализации и делает попытку его прямого изменения ошибкой, а непрямое изменения — неопределенным поведением (его может просто «не быть»). Кроме того, в C++ const (в отличие от C) подразумевает static, если явно не указать иначе. Где тут возможность объявлять что-либо во время выполнения я не понимаю.
Пример, да пожалуйста:
int test(int j)
{
const int i = j;
return i + 1;
}
Мне кажется, вы запутались…
В Вашем коде инициализация происходит во время выполнения, но Вы же хотели и объявлять и инициализровать в runtime?
int test(int j)
{
static constexpr int i = 10;
return i + 1;
}
Автор использует фишки C++11 и при этом пишет void для функций без аргументов — это какой-то современный стиль?
Ну и void в параметре метода надо конечно убирать. В С++ это никакого смысла не имеет.
Общее правило для дизайна, наследовать только интерфейс, т.е. виртуальные функции, остальное можно сделать агрегацией или композицией.
Наверное лучше бы сделать так?
class Rcc
{
public:
template<GPort... port>
inline static void PortOn()
{
rcc.AHB1ENR |= SetBits17<(std::uint32_t)port...>();
}
private:
static constexpr RCC_TypeDef & rcc = *RCC ;
template<std::uint8_t... bitmask>
inline static constexpr std::uint32_t SetBits17()
{
return (bitmask | ...);
}
};
int main()
{
Rcc::PortOn<GPort::A, GPort::B, GPort::C>();
return 0;
}
TRcc & Rcc = *static_cast<TRcc *>RCC;
И компилятор без оптимизации создаст что-то типа указателя, т.е. будет адрес по которому будет лежать адрес первого регистра модуля RCC. При оптимизации понятно будет только адрес RCC.
Наследование плохо, потому что оно тут не по делу, тем более публичное. Если не хотите через статический класс сделайте оверлей структуры
class TRcc
{
TRcc() : pRCC(reinterpret_cast<tRCC*>(RCC_BASE))
{
}
private:
using tRCC = RCC_TypeDef ;
static_assert(sizeof(tRCC) == sizeof(uint32_t)*36, "Структура не выровнена") ;
volatile tRCC * const pRCC;
};
В таком случае ав сможете скрыть все регистры и сделать для доступа к ним нормальные удобочитаемые методы, но если не хотите скрывать то можете сделать его публичным. Но тогда, чем это отличается от C?
Можно, конструкторы и методы делать constexpr, также, скажем для порта методы должны быть константными.
Это позволит создаваемые объекты располагать в ПЗУ, а не в ОЗУ. А это влияет на надежность.
Скажем, создал я объект порта при запуске, он в ОЗУ, и работает устройство 10 лет без передыха, За 10 лет с ОЗУ, что то да случится, и никто об этом не узнает. А поведение устройства может стать непредсказуемым. А вот если все будет в ПЗУ, то во первых там надежнее, а во вторых легко проверяется, считая контрольную сумму программы. По крайней мере можно просто обнаружить сбой.
static constexpr RCC_TypeDef & rcc = *RCC
Вообще в АРМ положить объект в ПЗУ затруднительно, const этого не гарантирует, может лежать как в ОЗУ так и в ПЗУ. Если вы, например забудете в классе сказать хотя бы одному методу, что он константный, хотя реально он не будет менять данные класса, то константный объект на 90% будет в ОЗУ. В АВР было слово _flash и все ложилось в ПЗУ, в АРМ другая архитектура и const не гарантирует что объект будет в ПЗУ, только constexpr. Но с ним ограничения есть, например нельзя делать constexpr конструктор классу, который наследует виртуальный класс.
Зачем здесь шаблоны с переменным количеством аргументов, если можно просто использовать перечисления?
Rcc.PortOn(GPort::A | GPort::B);
Правда придется перегрузить операцию '|', но это в современном С++ не проблема, можно перегрузить хоть для всех перечислений сразу:
template<typename E, typename = std::enable_if_t<std::is_enum_v<E>>>
constexpr E operator|(E lhs, E rhs)
{
using T = std::underlying_type_t<E> ;
return static_cast<E>(static_cast<T>(lhs) | static_cast<T>(rhs)) ;
}
Я не писал функции конкретно для портов, у меня есть функции для периферии в целом:
enablePeriphClock(AhbPeriph::GpioA | AhbPeriph::GpioB | AhbPeriph::Dma1);
enablePeriphClock(Apb1Periph::Power | Apb1Periph::Tim2);
А вот реализация, при условии того, что операция '|' и прочие для перечислений уже присутствуют:
void enablePeriphClock(AhbPeriph periph) { RCC->AHBENR |= uint32_t(periph); }
void enablePeriphClock(Apb1Periph periph) { RCC->APB1ENR |= uint32_t(periph); }
Просто и безопасно, не говоря уже о том, что '|' для перечислений — это еще и более естественно, чем перечисление через запятую.
А писать на плюсах в стиле С-с-классами, это как минимум глупо, а когда появится необходимость глобальных переменных(а она появится, уж поверьте мне) так вообще на ваш код будут все плеваться.
Плюсы надо использовать там где они уместны, на МК это бывает далеко не всегда.
В программировании совсем не стоит использовать разве что нелокальный goto, да и то потому, что его в большинстве языков нет. А для осмысленного нелокального goto используются исключения, setjump/longjump и (в особо запущенных случаях) ассемблерные вставки)
А еще лучше вообще пользователю запрещать делать, все что он сделать не должен. Например, есть регистры только для чтения и вот тут кто-нить берет и пишет такое USART1->SR = mask;
Никто ничего не сообщит, а ведь можно нормально сделать класс Register и через SNIFAE запретить операции для регистров, которые ReadOnly еще на этапе компиляции.
Вообще тема надежности софта и кода, она отдельная. Но, при любой маломальской сертификации на надежность на глобальные переменные смотрят, как потенциальную ПРОБЛЕМУ и источник ошибок и плюсов за это не добавляют, зато ставят огромный минус.
Для того, чтобы ограничить доступ к регистрам ввода-вывода с помощью SFINAE (да как угодно, на самом деле) не надо создавать инстансы классов и т.д. По большому счету даже метапрограммирование необязательно, достаточно закрыть доступ функциями, а сами регистры использовать внутри соответствующего модуля.
Не спорю, с метапрограммированием получается красиво (но не совсем так, как Вы написали, посмотрите Kvasir, там много статических методов). Я был бы всеми конечностями «за», да вот только новое железо появляется быстрее, чем библиотеки на hana пишутся для старого(
вот например для модуля RCC можно так сделать.
class TRcc
{
TRcc() : pRCC(reinterpret_cast<tRCC*>(RCC_BASE))
{
}
void SetCR(uint32_t mask)
{
pRCC->CR |= mask ;
}
private:
using tRCC = RCC_TypeDef ;
static_assert(sizeof(tRCC) == sizeof(uint32_t)*36, "Структура не выровнена") ;
volatile tRCC * const pRCC;
};
int main()
{
{ //создали
TRcc rcc;
rcc.SetCR(RCC_CR_HSION);
} // удалили
}
Насчет инстанса, а чем он вам помешал? Это же инлайны, и поэтому конструктор будет делать ровным счетом тоже самое, что и делает #define RCC (RCC_TypeDef*)(RCC_BASE) в заголовочнике сишного файла с описанием структуры RCC.
Использование C++ и шаблонов с переменным количеством аргументов при программировании микроконтроллеров