Как стать автором
Обновить

Комментарии 88

А пользоваться приходится, потому что существующие фирменные решения, известные как CMSIS и HAL слишком сложны, чтобы использовать их в любительских проектах.
Извините, но мне кажется сложным ваш пример. По сути, вы сейчас начали писать альтернативу вышеназванным библиотекам, но только на C++. К тому же, в статье не хватает объяснения для закоренелых сишников, как включить, например, C++ в своем проекте.
Сложные не по освоению, а по поведению.
Описанный тут вариант использует всю мощь компил-тайма, и после компиляции не вызывает неконтролируемого программистом поведения.
А закоренелым Сшникам ничего не объяснить, увы. Для них есть статьи как перейти на плюсы.

По, моему, Пример хороший, пользоваться им очень просто…
Да и класс получился простым...

фирменные решения, известные как CMSIS и HAL
При чем никто не поправил, что CMSIS это как раз то, что в начале статьи автор использовал. И вообще это стандартная либа на все cortex, а не поделка ST. Вероятно имелось ввиду «SPL и HAL».
Да, точно, SPL и HAL. CMSIS – это файлы работы с ядром ARM Cortex, core_cm3.h, cmsis_armcc.h и другие – это оттуда.
Да, вы правы. Но я не планирую останавливаться на одной этой заметке и постараюсь рассказать «закоренелым сишникам», как включить C++ в проекте. В принципе, в связке Eclipse + GCC, которой я пользуюсь, это происходит автоматически. Настроек, конечно, там многовато.
Вот что я для себя запилил на С++ для STM32: github.com/andreili/STM32FX_FM
Уже есть очень многое, включая полную поддержку USB HOST (MSC + HID), scmRTOS, FATfs.
Проект активно развивается, есть поддержка и STM32F103, но частичная — я в основном сосредоточился на STM32F407, для которого и начинал писать данный проект.
Но подход с шаблонами возьму на заметку — может и обновлю свои классы в репе ;)
Для USB не хватает генерации дескрипторов в компил-тайме.
Очень много препроцессора.
Нет поддержки векторов прерываний в памяти с динамической сменой вектора.
1) У меня нет поддержки USB Device как бы ;) Я пока что только Host делал, поскольку он был мне нужен.
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

Опять-таки — данным функционалом не пользовался, потому не проверял пока…
Да, надо популяризировать С++ среди С программистов, некоторые думают, что С++ генерит более тяжелый код
для этого надо написать опус про С++ без стандартной библиотеки.
А еще без исключений, без RTTI (dynamic_cast) и т.д.
На stack overflow множество раз обсуждали почему плохо писать на C++ ядерные драйвера, это же относится и к МК. Вывод во всех эти обсуждениях простой: писать можно, но приходится быть очень осторожным и надо неплохо понимать, что же генерирует компилятор при использовании тех или иных конструкций C++.
А при написание проекта на С ничего понимать не надо?
Конечно надо. Просто количество конструкций языка C кардинально меньше :) Поэтому требования к уровню компетентности программиста тоже будут ниже
Зато код на С++ понятнее на бизнес логике. Конечно внутренности страшные, но пользоваться и понимать, как в целом работает программа на много проще.
А еще код на С++ типобезопасный, более реюзабельный, проще масштабируется… да и просто писать его приятней. Просто надо понимать цену :)
Доля истины в ваших словах конечно есть, но никто не заставляет от плюсов использовать все синтаксические прелести. В МК реально нужно 20...30% от возможностей С++, остальное просто не использовать и тогда уже сверхквалификации не нужна, как мне кажется.

Дак на оборот, я бы сказал, приходится изголятся, и юзать практически все возможности языка, чтобы было экономнее...

Исключения как раз редко меняют в новых стандартах, как и RTTI. А вот описание изменений в новых стандартах почти с первого предложения тонет в std::, а в пространстве ядра нету этой вашей std. И в результате у пользователей возникает ощущение, что С++ это про контейнеры, итераторы и многопоточность, а std является неотъемлемой частью языка. Что не верно.
Тут скорее проблема в том, что включенные исключения, даже если их никто не кидает, порождают лэндинг пады, т.е. избыточный код. RTTI порождает кучу дополнительной информации. Все это кардинально усложняет linker-скрипты и т.д.
Я видал несколько реализаций std под МК. Конечно, они не 100% соответствуют стандарту.
std действительно является неотъемлемой частью языка (она описана в стандарте языка).

Но вот std::tuple, std::invoke, std::integer_sequence, std::strong_order и прочие подобные вещи — это совсем не про контейнеры, итераторы и многопоточность (или динамическую память).

Std это неотъемлемая часть стандарта и поставщик компилятора, должен ее поставлять… Например IAR поставляет свою реализацию, соответствующую стандарту, там есть вещи не реализованные до конца, например thread, или atomic, потому что их реализации зависят от того какую РТОС, например вы используете, но есть заготовку пустые, для того, чтобы могли реализовать это… И пользоваться как обычным std..

Так думают только деды по моему, которым лень переучиваться :)) На хабре вроде даже было тестовое сравнение размеров финальных бинарников на С и С++, там даже плюсы были чуть впереди в ряде задач.
Спасибо за статью, с интересом посмотрел Ваш код. Но все-таки согласен с предыдущим комментарием. HAL, быть может, не так эстетичен, в нем есть баги, но я бы тоже не стал говорить о его сложности. В Вашем варианте тоже есть неприятная особенность — у Вас используемые порты и аппаратно-зависимые константы «размазаны» по всему коду как и в НAL. На малых проектах не проблема. По вот на больших… Основная фишка STM32 — фактическая многозадачность на операциях ввода-вывода за счёт DMA. Вот и представьте читабельность Вашего кода на проекте, где 4-5 разных интерфейсов (типа SDIO для карты памяти, I2S для звука, SPI для дисплея, USB и USART для чего-нибудь еще) работают асинхронно с задействованием 10 или более прерываний. В этом случае имеет смысл полностью отделять слой аппаратной конфигурации (порты, устройства ввода-вывода, прерывания, регистры) от бизнес-логики. И вот здесь С++ поверх HAL или CMSIS, а не вместо их — очень мощный инструмент. Посмотрите, если интересно, наш проект github.com/den-makarov/stm32async.
В нем мы пытаемся на С++ разработать слой аппаратного описания ресурсов контроллера и слой асинхронного управления этими ресурсами.
Вы на свой проект другую лицензию прикрутили бы.

Охохо… Посмотрел немного, как это будет работать с РТОС? SPI если без DMA брать, я бы сделал через интерфейс, сделал бы пару виртуальных функций, типа ReadAndWrite, открыть сессию, чтобы ресурс захватить, закрыть сессию. И работал бы так, например драйвер лсд открывает сессию, конфигурит AЦП под себя, работает с SPI, пока он работает, другой потребитель, например драйвер АЦП курит, как только сессия закрылась, АЦП открывает сессию, конфигурит этот же SPI под себя, работает с ним, закрывает сессию.
Для UART бы подписчиков сделал на обработку прерываний, так как через UART в основном только один потребитель работает, например Modbus, подписался бы на обработку по приему и передаче…… В общем непонятно, зачем глючный HAL оборачивать и ограничивать себя в дизайне, можно изначально продумать все хорошо, отвязать абстрактную аппаратную часть от логики по другому.

Вместо #define MASK 0xF, используйте static constexpr uint32_t Mask = 0xF;

чем static constexprt uint32_t Mask = 0xF; лучше enum { Mask=0xF };?

На уровне языка — тип разный. У Mask в первом варианте тип — uint32_t, во втором — некоторый безымянный анонимный енам. Может неправильно себя вести с шаблонами, ожидающими прямо uint32_t, а не некий конвертируемый в него тип, может просто приводить к некоему раздуванию кода (особенно когда таких енамов у вас будет несколько).


На уровне семантики кода — более ясно передаётся намерение. У вас нет перечисления с единственным элементом. У вас просто есть известная на этапе компиляции целочисленная константа.

К раздуванию из-за типа целочисленной константы? Пример можно.

Чем длиньше выражение тем более ясно намерение? 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);
К раздуванию из-за типа целочисленной константы? Пример можно.

Из-за лишних инстанциирований шаблона.


Если у вас есть


template<typename T> void foo(T) {}

то для


enum { Mask1 = 0xff };
enum { Mask2 = 0xcc };

foo(Mask1);
foo(Mask2);

у вас будет два инстанциирования шаблона, тогда как для варианта с constexpr он будет один.

Это только если не использовать оптимизацию.

Какая оптимизация заставит линкер слить две функции с разными именами, пусть даже и одинаковым телом?


Да и в рамках одного TU она тоже не поможет:


template<typename T>
__attribute__((noinline)) int foo(T t)
{
    return static_cast<int>(t);
}

int main()
{
    enum { Mask1 = 0xff };
    enum { Mask2 = 0xcc };

    return foo(Mask1) + foo(Mask2);
}

gcc 8.3, -O3:


_Z3fooIZ4mainEUt_EiT_.constprop.0:
        movl    $255, %eax
        ret
_Z3fooIZ4mainEUt0_EiT_.constprop.1:
        movl    $204, %eax
        ret
main:
        call    _Z3fooIZ4mainEUt_EiT_.constprop.0
        movl    %eax, %edx
        call    _Z3fooIZ4mainEUt0_EiT_.constprop.1
        addl    %edx, %eax
        ret
LTO?

Это уже тогда не совсем линкер в классическом понимании, но да, засчитывается.


Впрочем, следующий за этим пример показывает, что LTO не поможет: даже в рамках одного TU там компилятор ничего не объединяет.

Я уже почти поверил, но потом вспомнил, что у меня есть WSL…
мой gcc функции объединяет

Хм, интересно. Я не ожидал, что LTO повлияет на поведение даже в рамках одного TU, но вообще это имеет смысл, да.

Спасибо, что попробовали.
Вам спасибо, интересная дискуссия :)
На уровне языка — тип разный. У Mask в первом варианте тип — uint32_t, во втором — некоторый безымянный анонимный енам.

И что, кроме незнания, мешает написать так
enum
{
  Mask = 0x0FUL,
}

?
На самом деле GCC сам решает, размещать константу в памяти или нет, когда оптимизирует использование регистров и инструкций.
По идее const uint32_t Mask, если не брать от него адрес, должен просто в код подставляться как число и всё, не отличаясь в этом плане от #define. Но GCC иногда делает не так, даже с дефайном.
А ваш вариант самый правильный.
Да вы правы что GCC сам решает. Но…
По идее const uint32_t Mask не должен подставлять число, так как это переменная, на которую может ссылаться указатель и она отличается от #define, тем что к ней можно адресоваться, поэтому GCC решает так как ему вздумается исходя из алгоритма вашего кода, и на это надеяться не надо… А вот constexpr по идее должен.
В C++ это скорее всего константа, а в С — переменная только для чтения. В смысле — есть отличия в семантике.
главное отличие в том, что при const переменная не может меняться после объявления и инициализации. Но вот объявляться и инициализироваться она может в ран тайме.
А у constexpr переменной значение должно быть известно на этапе компиляции.
Но вот объявляться и инициализироваться она может в ран тайме.

Приведите пример. Я не могу представить, как это сделать без неопределенного поведения.
Квалификация объекта const требует его немедленной инициализации и делает попытку его прямого изменения ошибкой, а непрямое изменения — неопределенным поведением (его может просто «не быть»). Кроме того, в C++ const (в отличие от C) подразумевает static, если явно не указать иначе. Где тут возможность объявлять что-либо во время выполнения я не понимаю.
Что то у вас в кучу, static и const вообще две разных эпостаси, первый спецификатор класса хранения/размещения, второй спецификатор типа.
Пример, да пожалуйста:
int test(int j)
{
   const int i = j;
   return i + 1;
}

Мне кажется, вы запутались…
Почитайте хотя бы en.cppreference.com/w/cpp/language/cv, может и поймете, причем здесь static.
В Вашем коде инициализация происходит во время выполнения, но Вы же хотели и объявлять и инициализровать в runtime?
Runtime это что по вашему?
Выполнение скомпилированной программы на целевой платформе. А что Вы называете этим словом?
Переменная i создалась на стеке, проинициализировалась j и умерла после выхода из функции test. Хотя она константа… И она не статическая, она на стеке…
А Вы правда уверены, что она создалась? Я бы даже при -O0 не был бы уверен на 100%…

Конечно! Это локальная переменная для конкретной функции, просто внутри функции она константа и не меняется, вот в примере с constexpr ничего не создалось...

Почитайте стандарт, уже не интересно становится. Что такое в C и C++ объект, что с ним делает квалификатор const, что и с чем в C++ делает спецификатор constexpr. Мне кажется, у Вас нет ясности в этих моментах.

Автор использует фишки C++11 и при этом пишет void для функций без аргументов — это какой-то современный стиль?

Ещё автору следует static_cast использовать.

Да, в общем случае это так, но для enum вполне достаточно С++ приведения типов uint32_t(..). — static_cast тут будет загромождать полезную информацию.
Ныне GCC ругается на такое старое приведение типов, требуя как раз таки cast'оов ;)

GCC ругается на Си приведение, а на С++ не может ругаться…
Может вы путаете (uint8_t)7 и uint8_t(7). Первое Сишное, второе С++

Хм, не знал. Спасибо за подсказку ;)

Лучше uint8_t { 7 }, на narrowing ругнется.

Но, непонятно зачем вообще делать наследование от структуры RCC, да еще и публичное? В итоге, у наследника, кроме методов PortOn еще и куча ненужных регистров, и вообще наследование такой стурктуры тут не к месту. А потом объявлять глобальный указатель на структуру.
Ну и 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;
}
Ну, с моей точки зрения здесь наследование от RCC_Typedef – это плюс. Ведь нам нужен именно легкий, с точки зрения синтаксиса, доступ к регистрам. Лучше писать AHB1ENR чем rcc.AHB1ENR, согласитесь. К тому же несомненный плюс в том, что не надо создавать экземпляр класса.
Не понял насчет экземпляра класса. У статического класса тоже не надо экземпляр создавать. В любом случае вы держите ссылку
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 лет с ОЗУ, что то да случится, и никто об этом не узнает. А поведение устройства может стать непредсказуемым. А вот если все будет в ПЗУ, то во первых там надежнее, а во вторых легко проверяется, считая контрольную сумму программы. По крайней мере можно просто обнаружить сбой.

НЛО прилетело и опубликовало эту надпись здесь
Да, можно держать constexpr указатели или ссылки, как в примере выше,
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)) ;
}
Вот хотелось как раз уйти от использования операций в вызове функции. Перечислил через запятую – так красивее. Но ваш подход тоже интересный с точки зрения C++

Я не писал функции конкретно для портов, у меня есть функции для периферии в целом:


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

Просто и безопасно, не говоря уже о том, что '|' для перечислений — это еще и более естественно, чем перечисление через запятую.

Да, тоже красиво. )
С++ не используют(используют) не потому что он медленней С(не медленней) а из-за того что для большинства проектов на МК все ваши плюшки от плюсов как пятое колесо, которое ещё к тому же и крутится в обратную сторону, я уже не говорю про ексепшены которые вообще сродни выстрела себе в ногу.
А писать на плюсах в стиле С-с-классами, это как минимум глупо, а когда появится необходимость глобальных переменных(а она появится, уж поверьте мне) так вообще на ваш код будут все плеваться.
Плюсы надо использовать там где они уместны, на МК это бывает далеко не всегда.
Ну, глобальные переменные при программировании микроконтроллеров и так используются повсеместно. Прямо в заголовке описания контроллера объявлены все: и UART1 и остальные и I2Cn и SPIn. Это доступ к общей для всей программы периферии. А поскольку это объекты класса, а не простые типы, то добавить сериализацию, разделение доступа по времени и прочее вполне возможно. Получатся этакие наивные синглетончики. )
Так это как раз «правильное использование» глобальных переменных — они глобальные т.к. являются отображением глобальных аппаратных синглетонов.
Но лучше их их не юзать, потому что это неправильно.
Троллите? Глобальные переменные не являются проблемой сами по себе, проблемой будет их бездумное и неограниченное использование.
В программировании совсем не стоит использовать разве что нелокальный goto, да и то потому, что его в большинстве языков нет. А для осмысленного нелокального goto используются исключения, setjump/longjump и (в особо запущенных случаях) ассемблерные вставки)
Являются проблемой, если программа больше чем «Hello world». Вы сегодня есть, а завтра вас нет, и ваш код с глобальными переменными можно будет выкинуть, потому что никто, кроме вас не знает, где и как они используются. Если для диплома или курсового студента это, согласен, не проблема, то для проекта, который потом поддерживать или использовать в реальных устройствах, а еще пуще сертифицировать, это проблема, да еще какая. От глобальных переменных надо держаться подальше.
А еще лучше вообще пользователю запрещать делать, все что он сделать не должен. Например, есть регистры только для чтения и вот тут кто-нить берет и пишет такое 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); 
   } // удалили
}

Ну ладно, геттеры-сеттеры — в конце концов вопрос личного дурного вкуса) А на кой ляд тут создавать инстанс класса с ненулевым размером? Особенно доставляет «мэджик» в статическом ассерте.
Меджик в статическом ассерте, конечно он заменится на что-то более читабельное, я тут его для наглядности привел, что это размер элементов структуры и у нас есть возможность проверить, что она выровнена, а то во многих заголовочниках, есть такие проблемы, например идут подряд 2 регистра размером 16 битом и оказывается, что между ними по 2 байта дырок наставлялось… На эти грабли пару раз наступал.
Насчет инстанса, а чем он вам помешал? Это же инлайны, и поэтому конструктор будет делать ровным счетом тоже самое, что и делает #define RCC (RCC_TypeDef*)(RCC_BASE) в заголовочнике сишного файла с описанием структуры RCC.
Вы выравнивание со slope не путаете?
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.