При обучении студентов разработке встроенного программного обеспечения для микроконтроллеров в университете я использую С++ и иногда даю особо интересующимся студентам всякие задачки для определения особо
В очередной раз таким студентам была дана задача поморгать 4 светодиодами, используя язык С++ 17 и стандартную библиотеку С++, без подключения дополнительных библиотек, типа CMSIS и их заголовочных файлов с описанием структур регистров и так далее… Побеждает тот, у кого код в ROM будет занимать наименьший размер и меньше всего затрачено ОЗУ. Оптимизация компилятора при этом не должна быть выше Medium. Компилятор IAR 8.40.1.
Победитель
Сам я до этого тоже эту задачу не решал, поэтому расскажу как её решили студенты и что получилось у меня. Предупреждаю сразу, навряд ли такой код можно будет использовать в реальных приложениях, потому и разместил публикацию в раздел «Ненормальное программирование», хотя кто знает.
Условия задачи
Есть 4 светодиода на портах GPIOA.5, GPIOC.5, GPIOC.8, GPIOC.9. Ими нужно поморгать. Чтобы было с чем сравнивать мы взяли код написанный на Си:
void delay() {
for (int i = 0; i < 1000000; ++i){
}
}
int main() {
for(;;) {
GPIOA->ODR ^= (1 << 5);
GPIOC->ODR ^= (1 << 5);
GPIOC->ODR ^= (1 << 8);
GPIOC->ODR ^= (1 << 9);
delay();
}
return 0 ;
}Функция
delay() здесь чисто формальная, обычный цикл, её оптимизировать нельзя. Предполагается, что порты уже настроены на выход и на них подано тактирование.
А также сразу скажу, что bitbanging не использовался, чтобы код был переносимым.
Этот код занимает 8 байт на стеке и 256 байт в ROM на Medium оптимизации
255 bytes of readonly code memory
1 byte of readonly data memory
8 bytes of readwrite data memory
255 байт из-за того, что часть памяти ушла под таблицу векторов прерывания, вызовы функций IAR для инициализации блока с плавающей точкой, всякие отладочные функции и функция __low_level_init, где собственно порты настроились.
Итак, полные требования:
- Функция main() должна содержать как можно меньше кода
- Нельзя использовать макросы
- Компилятор IAR 8.40.1 поддерживающий С++17
- Нельзя использовать заголовочные файлы CMSIS, типа "#include «stm32f411xe.h»
- Можно использовать директиву __forceinline для встраиваемых функций
- Оптимизация компилятора Medium
Решение студентов
Вообще решений было несколько, я покажу только одно… оно не оптимальное, но мне понравилось.
Так как нельзя использовать заголовочные файлы, студенты первым делом сделали класс
Gpio, который должен хранить ссылку на регистры порта по их адресам. Для этого они используют оверлей структуры, скорее всего идею взяли отсюда: Structure overlay:class Gpio {
public:
__forceinline inline void Toggle(const std::uint8_t bitNum) volatile {
Odr ^= bitNum ;
}
private:
volatile std::uint32_t Moder;
volatile std::uint32_t Otyper;
volatile std::uint32_t Ospeedr;
volatile std::uint32_t Pupdr;
volatile std::uint32_t Idr;
volatile std::uint32_t Odr;
//Проверка что структура выравнена
static_assert(sizeof(Gpio) == sizeof(std::uint32_t) * 6);
} ;Как видно они сразу определили класс
Gpio с атрибутами, которые должны быть расположены по адресам соответствующих регистров и метод для переключения состояния по номеру ножки:Затем определили структуру для
GpioPin, содержащую указатель на Gpio и номер ножки:struct GpioPin
{
volatile Gpio* port ;
std::uint32_t pinNum ;
} ;Затем они сделали массив светодиодов, которые сидят на конкретных ножках порта и пробежались по нему вызвав метод
Toggle() каждого светодиода:const GpioPin leds[] = {{reinterpret_cast<volatile Gpio*>(GpioaBaseAddr), 5},
{reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 5},
{reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 9},
{reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 9}
} ;
struct LedsDriver {
__forceinline static inline void ToggelAll() {
for (auto& it: leds) {
it.port->Toggle(it.pinNum);
}
}
} ;
constexpr std::uint32_t GpioaBaseAddr = 0x4002'0000 ;
constexpr std::uint32_t GpiocBaseAddr = 0x4002'0800 ;
class Gpio {
public:
__forceinline inline void Toggle(const std::uint8_t bitNum) volatile {
Odr ^= bitNum ;
}
private:
volatile std::uint32_t Moder;
volatile std::uint32_t Otyper;
volatile std::uint32_t Ospeedr;
volatile std::uint32_t Pupdr;
volatile std::uint32_t Idr;
volatile std::uint32_t Odr;
} ;
//Проверка что структура выравнена
static_assert(sizeof(Gpio) == sizeof(std::uint32_t) * 6);
struct GpioPin {
volatile Gpio* port ;
std::uint32_t pinNum ;
} ;
const GpioPin leds[] = {{reinterpret_cast<volatile Gpio*>(GpioaBaseAddr), 5},
{reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 5},
{reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 9},
{reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 9}
} ;
struct LedsDriver {
__forceinline static inline void ToggelAll() {
for (auto& it: leds) {
it.port->Toggle(it.pinNum);
}
}
} ;
int main() {
for(;;) {
LedsContainer::ToggleAll() ;
delay();
}
return 0 ;
}Статистика их кода на Medium оптимизации:
275 bytes of readonly code memory
1 byte of readonly data memory
8 bytes of readwrite data memory
Хорошее решение, но памяти занимает много :)
Решение мое
Я конечно решил не искать простых путей и решил действовать по серьезному :).
Светодиоды находятся на разных портах и разных ножках. Первое что необходимо, это сделать класс
Port, но чтобы избавиться от указателей и переменных, которые занимают ОЗУ, нужно использовать статические методы. Класс порт может выглядеть так:template <std::uint32_t addr>
struct Port {
//здесь скоро что-то будет
};В качестве параметра шаблона у него будет адрес порта. В заголовочнике
"#include "stm32f411xe.h", например для порта А, он определен как GPIOA_BASE. Но заголовочники нам использовать запрещено, поэтому просто нужно сделать свою константу. В итоге класс можно будет использовать так:constexpr std::uint32_t GpioaBaseAddr = 0x4002'0000 ;
constexpr std::uint32_t GpiocBaseAddr = 0x4002'0800 ;
using PortA = Port<GpioaBaseAddr> ;
using PortC = Port<GpiocBaseAddr> ;
Чтобы поморгать нужен метод Toggle(const std::uint8_t bit), который будет переключать необходимый бит с помощью операции исключающее ИЛИ. Метод должен быть статическим, добавляем его в класс:
template <std::uint32_t addr>
struct Port {
//сразу применяем директиву __forceinline, чтобы компилятор воспринимал эту функцию как встроенную
__forceinline inline static void Toggle(const std::uint8_t bitNum) {
*reinterpret_cast<std::uint32_t*>(addr+20) ^= (1 << bitNum) ; //addr + 20 адрес ODR регистра
}
};Отлично
Port<> есть, он может переключать состояние ножки. Светодиод сидит на конкретной ножке, поэтому логично сделать класс Pin, у которого в качестве параметров шаблона будет Port<> и номер ножки. Поскольку тип Port<> у нас шаблонный, т.е. разный для разного порта, то передавать мы можем только универсальный тип T.template <typename T, std::uint8_t pinNum>
struct Pin {
__forceinline inline static void Toggle() {
T::Toggle(pinNum) ;
}
} ;Плохо, что мы можем передать любую чепуху типа
T у которой есть метод Toggle() и это будет работать, хотя предполагается что передавать мы должны только тип Port<>. Чтобы от этого защититься, сделаем так, чтобы Port<> наследовался от базового класса PortBase, а в шаблоне будем проверять, что наш переданный тип действительно базируется на PortBase. Получаем следующее:constexpr std::uint32_t OdrAddrShift = 20U;
struct PortBase {
};
template <std::uint32_t addr>
struct Port: PortBase {
__forceinline inline static void Toggle(const std::uint8_t bit) {
*reinterpret_cast<std::uint32_t*>(addr ) ^= (1 << bit) ;
}
};
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) ;
}
} ;Теперь шаблон инстанциируется, только если наш класс имеет базовый класс
PortBase.По идее уже можно использовать эти классы, давайте посмотрим, что получится без оптимизации:
using PortA = Port<GpioaBaseAddr> ;
using PortC = Port<GpiocBaseAddr> ;
using Led1 = Pin<PortA, 5> ;
using Led2 = Pin<PortC, 5> ;
using Led3 = Pin<PortC, 8> ;
using Led4 = Pin<PortC, 9> ;
int main() {
for(;;) {
Led1::Toggle();
Led2::Toggle();
Led3::Toggle();
Led4::Toggle();
delay();
}
return 0 ;
}271 bytes of readonly code memory
1 byte of readonly data memory
24 bytes of readwrite data memory
Откуда взялись эти дополнительные 16 байт в ОЗУ и 16 байт в ROM. Они взялись из того, факта, что мы передаем в функцию Toggle(const std::uint8_t bit) класса Port параметр bit, и компилятор, при входе в функцию main сохраняет на стеке 4 дополнительных регистра, через которые передает этот параметр, потом использует эти регистры в которых сохраняется значения номера ножки для каждого Pin и при выходе из main восстанавливает эти регистры из стека. И хотя по сути это какая-то полностью бесполезная работа, так как функции встроенные, но компилятор действует в полном соответствии со стандартом.
От этого можно избавиться убрав класс порт вообще, передать адрес порта в качестве параметра шаблона для класса
Pin, а внутри метода Toggle() высчитывать адрес регистра ODR:constexpr std::uint32_t OdrAddrShift = 20U;
template <std::uint32_t addr, std::uint8_t pinNum,
struct Pin {
__forceinline inline static void Toggle() {
*reinterpret_cast<std::uint32_t*>(addr + OdrAddrShift ) ^= (1 << bit) ;
}
} ;
using Led1 = Pin<GpioaBaseAddr, 5> ; Но это выглядит не совсем хорошо и удобно для пользователя. Поэтому будем надеяться, что компилятор уберет это ненужное сохранение регистров при небольшой оптимизации.
Ставим оптимизацию на Medium и смотрим результат:
251 bytes of readonly code memory
1 byte of readonly data memory
8 bytes of readwrite data memory
Вау вау вау… у нас на 4 байта меньше
255 bytes of readonly code memory
1 byte of readonly data memory
8 bytes of readwrite data memory
Как такое может быть? Давайте взглянем на ассемблер в отладчике для С++ кода(слева) и Си кода(справа):

Видно, что во-первых, компилятор все функции сделал встроенные, теперь нет никаких вызовов вообще, а во вторых, он оптимизировал использование регистров. Видно, в случае с Си кодом, для хранения адресов портов компилятор использует то регистр R1, то R2 и делает дополнительную операции каждый раз после переключения бита (сохранить адрес в регистре то в R1, то в R2). Во втором же случае он использует только регистр R1, а поскольку 3 последних вызова на переключение всегда с порта C, то надобности сохранять тот же самый адрес порта С в регистре уже нет. В итоге экономится 2 команды и 4 байта.
Вот оно чудо современных компиляторов :) Ну да ладно. В принципе можно было на этом остановится, но пойдем дальше. Оптимизировать еще что-то, думаю, уже не выйдет, хотя возможно не прав, если есть идеи, пишите в комментариях. А вот с количеством кода в main() можно поработать.
Теперь хочется, чтобы все светодиоды были бы где нибудь в контейнере, и можно было бы вызывать метод, переключить все… Вот как-то так:
int main() {
for(;;) {
LedsContainer::ToggleAll() ;
delay();
}
return 0 ;
}Мы не будем тупо вставлять переключение 4 светодиодов в функцию LedsContainer::ToggleAll, потому что это неинтересно :). Мы хотим светодиоды положить в контейнер и потом пройтись по ним и вызывать у каждого метод Toggle().
Студенты использовали массив для того, чтобы хранить указатели на светодиоды. Но у меня разные типы, например:
Pin<PortA, 5>, Pin<PortC, 5>, и указатели на разные типы я хранить в массиве не могу. Можно сделать виртуальный базовый класс, для всех Pin, но тогда появится таблица виртуальных функций и Поэтому будем использовать кортеж. Он позволяет хранить у себя объекты разных типов. Выглядеть это дело будет выглядеть так:
class LedsContainer {
private:
constexpr static auto records = std::make_tuple (
Pin<PortA, 5>{},
Pin<PortC, 5>{},
Pin<PortC, 8>{},
Pin<PortC, 9>{}
) ;
using tRecordsTuple = decltype(records) ;
}Отлично есть контейнер, он хранит все светодиоды. Теперь добавим в него метод
ToggleAll():class LedsContainer {
public:
__forceinline static inline void ToggleAll() {
//сейчас придумаем как тут перебрать все элементы кортежа
}
private:
constexpr static auto records = std::make_tuple (
Pin<PortA, 5>{},
Pin<PortC, 5>{},
Pin<PortC, 8>{},
Pin<PortC, 9>{}
) ;
using tRecordsTuple = decltype(records) ;
}Просто так пройтись по элементам кортежа нельзя, так как получение элемента кортежа должно происходить только на этапе компиляции. Для доступа к элементам кортежа есть темплейтный метод get. Ну т.е. если напишем так
std::get<0>(records).Toggle(), то вызовется метод Toggle() для объекта класса Pin<PortA, 5>, если std::get<1>(records).Toggle(), то вызовется метод Toggle() для объекта класса Pin<PortС, 5> и так далее…Можно было
__forceinline static inline void ToggleAll() {
std::get<0>(records).Toggle();
std::get<1>(records).Toggle();
std::get<2>(records).Toggle();
std::get<3>(records).Toggle();
} Но мы не хотим напрягать программиста, который будет поддерживать этот код и позволять делать ему дополнительную работу, тратя ресурсы его компании, скажем в случае, если появится еще один светодиод. Придется добавлять код в двух местах, в кортеж и в этот метод — а это нехорошо и владелец компании будет не очень доволен. Поэтому обходим кортеж с помощью методов помощников:
class class LedsContainer {
friend int main() ;
public:
__forceinline static inline void ToggleAll() {
// создаем последовательность индексов 3,2,1,0 и вызываем соответствующий метод, куда передается эта последовательность
visit(std::make_index_sequence<std::tuple_size<tRecordsTuple>::value>());
}
private:
__forceinline template<std::size_t... index>
static inline void visit(std::index_sequence<index...>) {
Pass((std::get<index>(records).Toggle(), true)...); // распаковываем в последовательность get<3>(records).Toggle(), get<2>(records).Toggle(), get<1>(records).Toggle(), get<0>(records).Toggle()
}
__forceinline template<typename... Args>
static void inline Pass(Args... ) {//Вспомогательный метод для распаковки вариативного шаблона
}
constexpr static auto records = std::make_tuple (
Pin<PortA, 5>{},
Pin<PortC, 5>{},
Pin<PortC, 8>{},
Pin<PortC, 9>{}
) ;
using tRecordsTuple = decltype(records) ;
} Выглядит страшновато, но я предупреждал в начале статьи, что способ
Вся эта магия сверху на этапе компиляции делает буквально следующее:
//Это вызов
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) ;Вперед компилировать и проверять размер кода без оптимизации:
#include <cstddef>
#include <tuple>
#include <utility>
#include <cstdint>
#include <type_traits>
//#include "stm32f411xe.h"
#define __forceinline _Pragma("inline=forced")
constexpr std::uint32_t GpioaBaseAddr = 0x4002'0000 ;
constexpr std::uint32_t GpiocBaseAddr = 0x4002'0800 ;
constexpr std::uint32_t OdrAddrShift = 20U;
struct PortBase
{
};
template <std::uint32_t addr>
struct Port: PortBase
{
__forceinline inline static void Toggle(const std::uint8_t bit)
{
*reinterpret_cast<std::uint32_t*>(addr + OdrAddrShift) ^= (1 << bit) ;
}
};
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) ;
}
} ;
using PortA = Port<GpioaBaseAddr> ;
using PortC = Port<GpiocBaseAddr> ;
//using Led1 = Pin<PortA, 5> ;
//using Led2 = Pin<PortC, 5> ;
//using Led3 = Pin<PortC, 8> ;
//using Led4 = Pin<PortC, 9> ;
class LedsContainer {
friend int main() ;
public:
__forceinline static inline void ToggleAll() {
// создаем последовательность индексов 3,2,1,0 и вызываем соответствующий метод, куда передается эта последовательность
visit(std::make_index_sequence<std::tuple_size<tRecordsTuple>::value>());
}
private:
__forceinline template<std::size_t... index>
static inline void visit(std::index_sequence<index...>) {
Pass((std::get<index>(records).Toggle(), true)...);
}
__forceinline template<typename... Args>
static void inline Pass(Args... ) {
}
constexpr static auto records = std::make_tuple (
Pin<PortA, 5>{},
Pin<PortC, 5>{},
Pin<PortC, 8>{},
Pin<PortC, 9>{}
) ;
using tRecordsTuple = decltype(records) ;
} ;
void delay() {
for (int i = 0; i < 1000000; ++i){
}
}
int main() {
for(;;) {
LedsContainer::ToggleAll() ;
//GPIOA->ODR ^= 1 << 5;
//GPIOC->ODR ^= 1 << 5;
//GPIOC->ODR ^= 1 << 8;
//GPIOC->ODR ^= 1 << 9;
delay();
}
return 0 ;
}
Видим, что по памяти перебор, на 18 байтов больше. Проблемы все те же, плюсом еще 12 байт. Не стал разбираться откуда они… может кто пояснит.
283 bytes of readonly code memory
1 byte of readonly data memory
24 bytes of readwrite data memory
Теперь тоже самое на Medium оптимизации и о чудо… получили код идентичный С++ реализации в лоб и оптимальнее Си кода.
251 bytes of readonly code memory
1 byte of readonly data memory
8 bytes of readwrite data memory

Как видите победил я, и
Кому интересно, код тут
Где можно такое использовать, ну я придумал, например такое, у нас есть параметры в EEPROM памяти и класс описывающий эти параметры (Читать, писать, инициализировать в начальное значение). Класс шаблонный, типа
Param<float<>>, Param<int<>> и нужно, например, все параметры сбросить в default значения. Как раз тут и можно все их положить в кортеж, так как тип разный и вызвать у каждого параметра метод SetToDefault(). Правда, если таких параметров будет 100, то ПЗУ отъестся много, зато ОЗУ не пострадает.P.S. Надо признаться, что на максимальной оптимизации этот код по размеру получается такой же как на Си и на моем решении. И все потуги программиста по улучшению кода сводятся к одному и тому же коду на ассемблере.
P.S1 Спасибо 0xd34df00d за дельный совет. Можно упростить распаковку кортежа с помощью
std::apply(). Код функции ToggleAll() тогда упроститься до такого: __forceinline static inline void ToggleAll()
{
std::apply([](auto... args) { (args.Toggle(), ...); }, records);
} К сожалению в IAR std::apply в текущей версии еще не реализован, но работать будет также, см на реализацию с std::apply
Комментарии (28)

Alex_ME
23.06.2019 23:52Всегда с интересом смотрю на реализацию работы с периферией с помощью шаблонной магии!
Можете пояснить, как работает магия с проверкой?
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) ; } } ;
lamerok Автор
24.06.2019 09:33Работает это дело примерно так:
Чтобы было проще понять на пальцах, считайте, что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 и Т не определен.

Goron_Dekar
24.06.2019 08:14С++17 и без bitbanding. Ну как же так?
А линкер скрипт, инициацию стека, вектор прерываний копирование памяти взяли из библиотеки. А зря, плюсы и тут бы помогли.
Современные плюсы должны делать С из-за огромных компилтайм возможностей.

lamerok Автор
24.06.2019 08:46Инициализацию стека и векторов прерываний на С++ сделал, правда это больше на Си смахивает.
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 } }

evgeniy1294
24.06.2019 12:51+1Встречал статьи, в том числе и на хабре, где программисты startup'ы писали на плюсах. Однако этот подход требует довольно глубокого понимания компилятора и линкера.
А так да, инициализация и работа со стеком и таблицей прерываний на плюсах очень приятная.
hhba
24.06.2019 17:39Угу, вот пример выше как раз показывает, насколько "приятно" и, самое главное, "понятно" выглядят все эти вещи на плюсах.
Хотя я уверен, что плюсы в скором времени победят Си в эмбеде, но очевидно скорость этого процесса прямо зависит от смертности среди Си-программистов. Должно исчезнуть поколение людей, знающих, что можно жить без всех этих "приятных" вещей.

0xd34df00d
24.06.2019 19:07Жить-то можно, понятное дело. Можно и без С жить, в конце концов. Но нужно ли?

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

staticmain
24.06.2019 10:01class = typename std::enable_if_t<std::is_base_of<PortBase, T>::value>>
std::apply(records, [](auto... args) { (args.Toggle(), ...); });
visit(std::make_index_sequence<std::tuple_size<tRecordsTuple>::value>());
Я, возможно, чего-то не понимаю, но разве не должен код, максимально приближенный к железу быть максимально понятен и упрощен для надежности? С для микроконтроллеров используют не просто так, а по причине того, что он, обладая достаточно простым синтаксисом может читаться как последовательные инструкции, близкие по уровню к ассемблерному коду. В любой момент можно сделать cc -S и увидеть ассемблерный листинг для того, чтобы убедиться, что сгенерированный код максимально соответствует задуманному.
Можете ли вы гарантировать, что вышеприведенный код не обращается к памяти за пределами списка или не выкидывается компилятором (как, например, memset в конце блока) по какой-либо причине? Можете ли вы без подготовки изобразить на псевдо-ассемблере как должен выглядеть листинг вот этой строчки?
visit(std::make_index_sequence<std::tuple_size<tRecordsTuple>::value>());
Я не имею целью сказать, что автор статьи выбрал не тот инструмент. Проблема в том, что культура писать абстракционный негарантируемый код для микроконтроллеров прививается студентам.
lamerok Автор
24.06.2019 10:54Дело в том, что метопрограммирование оно предполагает, что вы пишите код не для микроконтроллера, а для компилятора. Т.е. Весь этот код, ну кроме функций 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) ;
hhba
24.06.2019 17:45Да, с точки зрения результата должно быть не хуже, а даже лучше, чем на си (и очевидно, что выход за пределы массива был притянут комментатором за уши). Однако вопрос понятности кода остаётся.

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

0xd34df00d
24.06.2019 19:09Я, возможно, чего-то не понимаю, но разве не должен код, максимально приближенный к железу быть максимально понятен и упрощен для надежности?
Для какого рода надёжности? Для гарантии отсутствия аллокаций? Для гарантии максимальной глубины рекурсии и потребного стека? Для гарантии соответствия семантики программы требуемой спецификации?
Что характерно, ответ во всех этих случаях «нет», кстати.

mpa4b
24.06.2019 10:50+1В инклудах например на stm32 все без исключения регистры, в т.ч. и GPIO, объявлены как volatile. У вас — нет. Как следствие, вы гордитесь вот этим:
хотя вас должно это настораживать. Включите чуть более сильную оптимизацию, оптимизацию всей программы целиком (не знаю как в IAR, я про -flto в GCC) и компилятор может вам полностью убрать ваш код, потому что нет volatile.
lamerok Автор
24.06.2019 10:56Да согласен… В этом и есть причина оптимизации, в реальности код получится один в Си и С++. Хорошее замечание.

olekl
24.06.2019 11:19Ух ты, а неплохо С++ продвинулся на микроконтроллеры… По сравнению с тем, как на Embed переключение пина из In в Out занимало 600+ тактов :)

NordicEnergy
24.06.2019 12:17600 тактов это не проблема С++, это проблема рукожопа, который написал библиотеку. На С++11 можно так же писать вполне хороший код, который не будет уступать по результату привычному С.
Тут скорее не С++ продвинулся в развитии, а культура писать на нем для МК развилась. Еще 5 лет назад за плюсы в реал-таймовых железках создателя бы обмазали фекалиями, а сейчас это уже норма.
Но имхо на С++ гораздо проще написать плохой код, чем на С. Да и те же шаблоны выглядят слишком монструозно, код на С обернутый юнионы выглядит читабельнее и такой же шустрый.
hhba
24.06.2019 17:50Но имхо на С++ гораздо проще написать плохой код, чем на С
Именно. Остаётся надеяться на улучшение качества кода анмасс, но это невозможно без более жёсткой специализации в отрасли. Что конечно не лучшим образом скажется на сроках, стоимости и краткосрочном качестве изделий. Так что остаётся только с интересом наблюдать за процессом.э, т.к. лично я для себя вижу мало шансов пересесть с "деревенского Си" на подобные чудеса.

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

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

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

Orange11Sky
26.06.2019 02:56Замечательная статья.
Мне показалось интересным и возможно более наглядным решение с использованием шаблонной специализации функции. Не претендуя на минимум памяти и скурпулезность, привожу свой вариант:
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(); } }

lamerok Автор
27.06.2019 08:37Ну да, я как раз там писал:
Можно сделать виртуальный базовый класс, для всех 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; }
staticmain
27.06.2019 12:45И это считается более читаемым, чем это:?
#define LEDS_COUNT (4) static lep32_led_rgb_a_t leds[LEDS_COUNT] = { {{ LEP32_GPIOA, 9}, { LEP32_GPIOA, 10}, { LEP32_GPIOA, 11}, { LEP32_GPIOA, 12}}, {{ LEP32_GPIOA, 10}, { LEP32_GPIOA, 11}, { LEP32_GPIOA, 12}, { LEP32_GPIOA, 9}}, {{ LEP32_GPIOA, 11}, { LEP32_GPIOA, 12}, { LEP32_GPIOA, 9}, { LEP32_GPIOA, 10}}, {{ LEP32_GPIOA, 12}, { LEP32_GPIOA, 9}, { LEP32_GPIOA, 10}, { LEP32_GPIOA, 11}} }; void main(void) { LEP32_RCC->apb2enr |= LEP32_RCC_APB2ENR_PORTA_ENABLE | LEP32_RCC_APB2ENR_PORTB_ENABLE | LEP32_RCC_APB2ENR_PORTC_ENABLE; // можно выкинуть в отдельную функцию u32 i; for (i = 0; i < LEDS_COUNT; i++) { // можно посворачивать в отдельные функции типа set red/blue lep32_gpio_setvcc((&leds[0])->vcc.io, 1 << (&leds[0])->vcc.pin); lep32_gpio_setgnd((&leds[0])->red.io, 1 << (&leds[0])->red.pin); lep32_gpio_sethiz((&leds[0])->green.io, 1 << (&leds[0])->green.pin); lep32_gpio_sethiz((&leds[0])->blue.io, 1 << (&leds[0])->blue.pin); } }
0xd34df00d
Непонятно, зачем в 2019 году вся эта ерунда с ручной рекурсией по списку индексов, когда давно есть std::apply и folding expressions. Можно ж в полторы строчки с C++17-то.
lamerok Автор
Тут нет рекурсии. Про apply посмотрю, спасибо за наводку. Но вообще компиляторы для микроконтроллеров не все библиотечные функции из С++17 поддерживают. Например, конкретно std::apply там и не реализован. И кстати, не могли бы показать, как в полторы строчки это сделать, я не совсем уловил, как кортеж распаковать в последовательность вызовов методов элементов кортежа…
0xd34df00d
А, тьфу. Я сначала слишком бегло прочитал, сорри. Это лучше, да :)
Но вообще тогда
Passне нужен, можно завернуть вinitializer_list<bool>или типа того. У которого, кстати, порядок вычисления аргументов (если через{}) полностью специфицирован, в отличие от. Не то, чтобы это было важно в этом случае, но привычка хорошая, ИМХО.Ну, значит, IAR этот C++17 не поддерживает, увы.
Что-то типа
В реальном коде выглядит как-то так, например.
lamerok Автор
Спасибо, добавил в конец статьи.