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

Как поморгать 4 светодиодами на CortexM используя С++17, tuple и немного фантазии

Время на прочтение12 мин
Количество просмотров9.4K
Всего голосов 23: ↑22 и ↓1+21
Комментарии34

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

НЛО прилетело и опубликовало эту надпись здесь
Тут нет рекурсии. Про apply посмотрю, спасибо за наводку. Но вообще компиляторы для микроконтроллеров не все библиотечные функции из С++17 поддерживают. Например, конкретно std::apply там и не реализован. И кстати, не могли бы показать, как в полторы строчки это сделать, я не совсем уловил, как кортеж распаковать в последовательность вызовов методов элементов кортежа…
НЛО прилетело и опубликовало эту надпись здесь
Спасибо, добавил в конец статьи.

Всегда с интересом смотрю на реализацию работы с периферией с помощью шаблонной магии!


Можете пояснить, как работает магия с проверкой?


template <typename T, std::uint8_t pinNum, 
class = typename std::enable_if_t<std::is_base_of<PortBase, T>::value>> //Вот и защита
struct Pin {
  __forceinline inline static void Toggle()  {
    T::Toggle(pinNum) ;
  }
} ;
Работает это дело примерно так:
Чтобы было проще понять на пальцах, считайте, что std::enable_if_t<> это функция
T enabled_t(bool), на входе она получает, либо true, либо false. На выходе либо тип T, если передали true, либо ничего и тогда T не определен. Т.е:
std::enabled_t(true) возвратит тип T и будет T = T,
std::enabled_t(false) возвратит ничего и T= ничего.

Если T не определен, то компилятор не сможет выполнить T::Toggle(pinNum); Так как T не существует. И собственно выдаст вам ошибку, что нельзя передать такой T.

Упрощенно запишем так:

если Т является подтипом PortBase, то функция std::enable_if_t<> возвратит Т и T =T и шаблон будет таким
template <T, pinNum>

если Т не является подтипом PortBase, то то функция std::enable_if_t<> ничего не возвратит и T = ничего и шаблон будет таким
template <,pinNum>
и наш класс не соберется

Собственно std::is_base_of<PortBase, T>::value>, как раз и проверят, является ли T подтипом PortBase. Если да, то возвратит true, и T=T, если нет то false и Т не определен.

С++17 и без bitbanding. Ну как же так?
А линкер скрипт, инициацию стека, вектор прерываний копирование памяти взяли из библиотеки. А зря, плюсы и тут бы помогли.


Современные плюсы должны делать С из-за огромных компилтайм возможностей.

Инициализацию стека и векторов прерываний на С++ сделал, правда это больше на Си смахивает.
class DummyModule {
  public:
    static void HandleInterrupt() {};
} ;
#define __vectortable _Pragma("location=\".intvec\"") 
using tInterruptFunction = void (*)() ;
using tInterruptVectorItem = union 

__vectortable const tInterruptVectorItem __vector_table[] = {  
  {     .pPtr = __sfe( "CSTACK" )   }, 
  {     __iar_program_start //Reset  }, 
    // Non maskable interrupt, Clock Security System
  {   DummyModule::HandleInterrupt }, 
  {   DummyModule::HandleInterrupt },    // All class of fault
  {   DummyModule::HandleInterrupt  },  // Memory management
  {   DummyModule::HandleInterrupt },   // Pre-fetch fault, memory access fault
  {   DummyModule::HandleInterrupt },   // Undefined instruction or illegal state
  {   0  },      //Reserved
  {   0  },      //Reserved
  {   0  },      //Reserved
  {   0  },      //Reserved
  {   OsWrapper::Rtos::HandleSvcInterrupt },   
  {   DummyModule::HandleInterrupt  },     // Debug Monitor
  {    0  },     // Reserved 
  {  OsWrapper::Rtos::HandlePendSvInterrupt  },    
  {  OsWrapper::Rtos::HandleSysTickInterrupt  }      
}
Встречал статьи, в том числе и на хабре, где программисты startup'ы писали на плюсах. Однако этот подход требует довольно глубокого понимания компилятора и линкера.
А так да, инициализация и работа со стеком и таблицей прерываний на плюсах очень приятная.

Угу, вот пример выше как раз показывает, насколько "приятно" и, самое главное, "понятно" выглядят все эти вещи на плюсах.


Хотя я уверен, что плюсы в скором времени победят Си в эмбеде, но очевидно скорость этого процесса прямо зависит от смертности среди Си-программистов. Должно исчезнуть поколение людей, знающих, что можно жить без всех этих "приятных" вещей.

НЛО прилетело и опубликовало эту надпись здесь

Никто и не утверждает, что все продемонстрированное выше не нужно. Наверное нужно же...

НЛО прилетело и опубликовало эту надпись здесь
Дело в том, что метопрограммирование оно предполагает, что вы пишите код не для микроконтроллера, а для компилятора. Т.е. Весь этот код, ну кроме функций Toggle() был написан для компилятора, который преобразовал этот код в последовательные вызовы Toggle() каждого светодиода.
Вот так
/Этот вызов 
LedsContainer::ToggleAll() ;
//Преобразуется в эти 4 вызова:
Pin<PortС, 9>().Toggle() ;
Pin<PortС, 8>().Toggle() ;
Pin<PortC, 5>().Toggle() ;
Pin<PortA, 5>().Toggle() ;
//А поскольку у нас метод Toggle() inline, то в это:
 *reinterpret_cast<std::uint32_t*>(0x40020814 ) ^= (1 << 9) ;
 *reinterpret_cast<std::uint32_t*>(0x40020814 ) ^= (1 << 8) ;
 *reinterpret_cast<std::uint32_t*>(0x40020814 ) ^= (1 << 5) ;
 *reinterpret_cast<std::uint32_t*>(0x40020014 ) ^= (1 << 5) ;


и собственно поэтому и вопрос типа:
Можете ли вы гарантировать, что вышеприведенный код не обращается к памяти за пределами списка или не выкидывается компилятором (как, например, memset в конце блока) по какой-либо причине? Можете ли вы без подготовки изобразить на псевдо-ассемблере как должен выглядеть листинг вот этой строчки?
visit(std::make_index_sequence<std::tuple_size<tRecordsTuple>::value>());


смысла не имеет, потому что этого кода нет в коде микроконтроллера. Он был выполнен на этапе компиляции и стандарт гарантирует что никаких выходов за границу списка не будет, так как функции библиотечные.
Вопрос тут в конечном счет в удобстве использования в дальнейшем… Теперь чтобы новый светодиод добавить нужно просто его добавить в список (в кортеж) в данном случае…
Добавляем новые светодиоды в список
constexpr static auto records = std::make_tuple (
                                                   Pin<PortA, 5>{},
                                                   Pin<PortC, 5>{},
                                                   Pin<PortC, 8>{},
                                                   Pin<PortC, 9>{},
                                                   Pin<PortC, 2>{},
                                                   Pin<PortC, 6>{}
    


Ну и все, остальное за вас сделает компилятор.

Да, с точки зрения результата должно быть не хуже, а даже лучше, чем на си (и очевидно, что выход за пределы массива был притянут комментатором за уши). Однако вопрос понятности кода остаётся.

Пардон, не массив конечно, а список. С телефона даже поправить теперь не могу...

НЛО прилетело и опубликовало эту надпись здесь
В инклудах например на stm32 все без исключения регистры, в т.ч. и GPIO, объявлены как volatile. У вас — нет. Как следствие, вы гордитесь вот этим: вот этим
хотя вас должно это настораживать. Включите чуть более сильную оптимизацию, оптимизацию всей программы целиком (не знаю как в IAR, я про -flto в GCC) и компилятор может вам полностью убрать ваш код, потому что нет volatile.
Да согласен… В этом и есть причина оптимизации, в реальности код получится один в Си и С++. Хорошее замечание.
Ух ты, а неплохо С++ продвинулся на микроконтроллеры… По сравнению с тем, как на Embed переключение пина из In в Out занимало 600+ тактов :)
НЛО прилетело и опубликовало эту надпись здесь
Но имхо на С++ гораздо проще написать плохой код, чем на С

Именно. Остаётся надеяться на улучшение качества кода анмасс, но это невозможно без более жёсткой специализации в отрасли. Что конечно не лучшим образом скажется на сроках, стоимости и краткосрочном качестве изделий. Так что остаётся только с интересом наблюдать за процессом.э, т.к. лично я для себя вижу мало шансов пересесть с "деревенского Си" на подобные чудеса.

И кресты тоже продвинулись в embed. В 17 году у iar даже c++14 отсутствовал. А gcc-only код традиционно (и, увы, оправдано) не любят в продакшене

Боюсь, iar-only код не любят ещё больше, у них довольно много специфики. Для меня стандартом является возможность сборки проекта gcc и clang — я и мои коллеги должны иметь возможность выкачать исходники из git, поставить подходящий компилятор из открытых репозиториев и собрать его, позвав cmake.

Iar опция есть strict standard и код будет полностью соответствовать стандарту С++, благо за последние 4 года они сдели правильные шаги и даже получили сертификат на соо вествие стандарту надежности. Т. Е. можно быть уверенным, что std библиотеки, да и вообще компилятор полгость следует стандарту и ошибок там не много. Чего не скажешь про gcc. Поэтому его в продакшене и недолюбливают. А вот GreenHills и IAR юзают вплоть до космоса и военки.

Замечательная статья.
Мне показалось интересным и возможно более наглядным решение с использованием шаблонной специализации функции. Не претендуя на минимум памяти и скурпулезность, привожу свой вариант:
template<std::uint32_t, std::uint32_t> class Led
{
public:
	void Toggle();
};
template<std::uint32_t addr, std::uint32_t bitNum > void Led<addr, bitNum>::Toggle() {
	*reinterpret_cast<std::uint32_t*>(addr + 20) ^= (1 << bitNum);
}
int main()
{
	Led<GpioaBaseAddr, 5> Led1;
	Led<GpiocBaseAddr, 5> Led2;
	Led<GpiocBaseAddr, 8> Led3;
	Led<GpiocBaseAddr, 9> Led4;
	for (;;) {
		Led1.Toggle();
		Led2.Toggle();
		Led3.Toggle();
		Led4.Toggle();
		delay();
	}
}


Если чуток пожертвовать памятью то решению можно придать еще больше элегантности, добавив возможность использовать контейнеры.

class LedBase
{
public:
	void virtual Toggle() = 0;
};

template<std::uint32_t, std::uint32_t> class Led : public LedBase
{
public:
	void virtual Toggle();
};
template<std::uint32_t addr, std::uint32_t bitNum > void Led<addr, bitNum>::Toggle() {
	*reinterpret_cast<std::uint32_t*>(addr + 20) ^= (1 << bitNum);
}
int main()
{
	............
        LedBase* leds[] = { &Led1, &Led2, &Led3,  &Led4 };
	for (LedBase* led : leds) { led->Toggle(); }
}


Ну да, я как раз там писал:
Можно сделать виртуальный базовый класс, для всех Pin, но тогда появится таблица виртуальных функций и уделать выиграть студентов мне не удастся.

Проблема в том, что у нас Led это объекты разных классов, и хранить указатели разных классов в массиве нельзя, надо будет делать, как вы сделали базовый класс с виртуальной функцией, а вот в кортеже можно хранить объекты разных типов.

И задача была была памяти не кушать, чтобы студентов обыграть :)
Поэтому создавать объекты было неправильно… Можно было бы еще сделать вот так:
template <typename... Types>
class Container {};

using Led1 = Pin<PortA, 5> ;
using Led2 = Pin<PortC, 5> ;
using Led3 = Pin<PortC, 8> ;

class LedsController {
  public:
    __forceinline template<typename... args>
     inline constexpr static void ToggleAll()    {
      toggleAll(tLedsController()) ;
    }
  
  private:       
    using tLedsController = Container<Led1, Led2, Led3> ;    // вот тут делаем шаблонный тип с разными классами на входе
  
  __forceinline template<typename ...Args> 
   constexpr inline void static toggleAll(Container<Args...> obj)  {
      pass((Args::Toggle(), true)...) ;  // проходим по каждому типу в списке и вызываем у него Toggle()
  }    
  
  __forceinline  template<typename... Args> 
  inline constexpr static void pass(Args&&...) {}
} ;

int main() { 
  LedsController::ToggleAll() ;
  return 0;
}

Это вырождается в ту же самую последовательность

int main() {  
  Led1::Toggle() ;
  Led2::Toggle() ;
  Led3::Toggle() ;
  return 0;
}
НЛО прилетело и опубликовало эту надпись здесь
Я не знаю, мне и то и то понятно :), но точно будет занимать меньше кода в ОЗУ, так как никакого массива нет вообще в этом решении, и нет никаких временных переменных, типа i для обхода массива.

По ПЗУ вопрос, потому что в вашем коде цикл, скорее всего он сожрет столько же кода, а может чуть больше, сколько и разворачивание в последовательность вызовов Toggle()

Но вот с точки зрения удобства поддержки и расширения, возможно тоже все просто… потому что, все что надо будет добавить это новый светодиод/ножку (тип) в список:
using tLedsController = Container<Led1, Led2, Led3, Pin<PortC, 9>> ;

Хотя в вашем решении тоже только в массив надо добавить…

В общем основное преимущество тут: компактность кода ны выходе, при простоте и понятности использования… Один раз написал страшный код, который реализует распаковку списка и потом юзай очень просто и понятно…

int main() { 
  LedsController::ToggleAll() ;
  return 0;
}

Кроме того, и глобальных переменных тут нет и пользователь кода, не сможет сделать ничего плохого, так как кроме как ToggleAll() ему ничего недоступно, даже обратиться к элементу списка у него возможности нет, ну потому что его нет :). А в вашем коде, любой программист в любом месте может обратиться к любому элементу массива, а так как он не const у вас, то еще и поменять, а это уже нехорошо, потенциально небезопасный код.
НЛО прилетело и опубликовало эту надпись здесь
При оптимизации? цикл то есть
for (i = 0; i < LEDS_COUNT; i++)
куда он денется? Или я чего-то не понял?
НЛО прилетело и опубликовало эту надпись здесь
Ок, понял, т.е при оптимизации. Но я там в статье писал, что при оптимизации все потуги приведутся к одному ассемблерному коду.
P.S. Надо признаться, что на максимальной оптимизации этот код по размеру получается такой же как на Си и на моем решении. И все потуги программиста по улучшению кода сводятся к одному и тому же коду на ассемблере.

Распаковка списка, делает это без оптимизации, иногда требуется без оптимизации программы поставлять для сертификации, правда в последнее время, если пользуешь сертифицированный по IEC 61508 компилятор, то оптимизацию разрешают (могу ошибся, но все равно не всю, насчет code motion не уверен, врать не буду).
А вы уверены, что `-O3` — это то, что надо в embedded пихать везде? И мне кажется, что мигание светодиодами это точно то место, где `-Os` и никак иначе.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории