Pull to refresh

Comments 34

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

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


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


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'ы писали на плюсах. Однако этот подход требует довольно глубокого понимания компилятора и линкера.
А так да, инициализация и работа со стеком и таблицей прерываний на плюсах очень приятная.

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


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

UFO just landed and posted this here

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

UFO just landed and posted this here
Дело в том, что метопрограммирование оно предполагает, что вы пишите код не для микроконтроллера, а для компилятора. Т.е. Весь этот код, ну кроме функций 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>{}
    


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

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

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

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

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

И кресты тоже продвинулись в 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;
}
UFO just landed and posted this here
Я не знаю, мне и то и то понятно :), но точно будет занимать меньше кода в ОЗУ, так как никакого массива нет вообще в этом решении, и нет никаких временных переменных, типа i для обхода массива.

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

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

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

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

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

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

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

Articles