Comments 34
Всегда с интересом смотрю на реализацию работы с периферией с помощью шаблонной магии!
Можете пояснить, как работает магия с проверкой?
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 }
}
А так да, инициализация и работа со стеком и таблицей прерываний на плюсах очень приятная.
Угу, вот пример выше как раз показывает, насколько "приятно" и, самое главное, "понятно" выглядят все эти вещи на плюсах.
Хотя я уверен, что плюсы в скором времени победят Си в эмбеде, но очевидно скорость этого процесса прямо зависит от смертности среди Си-программистов. Должно исчезнуть поколение людей, знающих, что можно жить без всех этих "приятных" вещей.
/Этот вызов
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>{}
Ну и все, остальное за вас сделает компилятор.
хотя вас должно это настораживать. Включите чуть более сильную оптимизацию, оптимизацию всей программы целиком (не знаю как в IAR, я про -flto в GCC) и компилятор может вам полностью убрать ваш код, потому что нет volatile.
Но имхо на С++ гораздо проще написать плохой код, чем на С
Именно. Остаётся надеяться на улучшение качества кода анмасс, но это невозможно без более жёсткой специализации в отрасли. Что конечно не лучшим образом скажется на сроках, стоимости и краткосрочном качестве изделий. Так что остаётся только с интересом наблюдать за процессом.э, т.к. лично я для себя вижу мало шансов пересесть с "деревенского Си" на подобные чудеса.
И кресты тоже продвинулись в embed. В 17 году у iar даже c++14 отсутствовал. А gcc-only код традиционно (и, увы, оправдано) не любят в продакшене
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;
}
По ПЗУ вопрос, потому что в вашем коде цикл, скорее всего он сожрет столько же кода, а может чуть больше, сколько и разворачивание в последовательность вызовов 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 не уверен, врать не буду).
Как поморгать 4 светодиодами на CortexM используя С++17, tuple и немного фантазии