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