Добрый день, жители Хабра. Данный пост будет посвящен программированию на C++, и использованию constexpr объектов с целью повышения уровня удобства и одновременно оптимизации кода с точки зрения размера и производительности.
В процессе работы над одним из проектов, задумался: "нельзя ли сделать удобный доступ к GPIO портам на STM32, и при этом сделать его оптимальным по размеру кода и производительности". Что я хотел получить:
Использования контекстных подсказок и автодополнения при работе с GPIO.
Получение максимально оптимального кода. 1-2 ассемблерных инструкции.
Потенциальная возможность добавить проверки на уровне компиляции, которые не будут влиять на производительность.
Изначально я посмотрел как доступ к портам организован на платформе Arduino, и конечно данный способ далеко не оптимален с точки зрения производительности. Сначала происходит поиск порта по индексу, и только потом обращение. Тут мне в голову пришла мысль о использовании constexpr выражений и классов для реализации обоих пунктов сразу. Итак приступим. В моем случае код не будет кросплатформенным, т.к. можно считать что это часть HAL (Hardware Abstraction Layer). Код был написан для микроконтроллера STM32F103xxx.
Для начала определим адреса портов.
static constexpr const uint32_t GPIOA_BASE = 0x40010800;
static constexpr const uint32_t GPIOB_BASE = 0x40010C00;
static constexpr const uint32_t GPIOC_BASE = 0x40011000;
Теперь определим настройки порты, которые будут нам доступны.
enum class GpioMode : uint8_t {
InputAnalog = 0x00,
InputFloating,
InputWithPullup,
OutputPushPull = 0x04,
OutputOpenDrain,
AlternatePushPull,
AlternateOpenDrain
};
enum class GpioOutputSpeed : uint8_t {
Input,
Max10Mhz,
Max2Mhz,
Max50Mhz
};
Напишем класс порта. Класс оформим в виде шаблона. Все функции класса определим как static inline. Это делается для оптимизации кода. Шаблонный класс в данном случае используется для группировки функций, хранения параметорв в виде constexpr значений. Т.е. данные параметры будут доступны только на этапе компиляции, а после компиляции, код будет оптимизирован до минимального количества инструкции. В идеале до одной-двух ассемблерных инструкций при доступе к порту, даже при компиляции с опцией "-O0". В вункции доступа к портам добавим барьерные инструкции dsb.
Небольшое отступление. На собеседованиях часто задают вопрос про volatile, которым мягко говоря задолбали уже. У меня большая просьба к тем кто проводит собеседования в сфере embedded: "не могли бы вы с ходу, не подглядывая, своими словами рассказать для чего нужны инструкции dmb, dsb и isb в системе команд arm"? Полагаю, вопрос про volatile отпадет сам собой.
template<uint32_t GpioAddr, uint8_t pinNo>
struct GpioPin {
static constexpr const uint32_t GpioAddress = GpioAddr;
static constexpr const uint8_t GpioPinNo = pinNo;
static constexpr const uint32_t GpioPinMask = (1 << pinNo);
static constexpr const uint32_t GpioConfPerReg = 8;
static inline void mode(const GpioMode mode, const GpioOutputSpeed oSpeed = GpioOutputSpeed::Input){
if constexpr (GpioPinNo < GpioConfPerReg){
static constexpr const uint32_t maskBitCount = 4;
static constexpr const uint32_t maskOffset = (pinNo * maskBitCount) & 0x1F;
static constexpr const uint32_t mask = (1 << (maskBitCount + 1)) - 1;
reinterpret_cast<volatile GPIO* const>(GpioAddress)->CRL &= ~(mask << maskOffset);
reinterpret_cast<volatile GPIO* const>(GpioAddress)->CRL |= ((static_cast<uint32_t>(mode) & 0x03) << maskOffset);
reinterpret_cast<volatile GPIO* const>(GpioAddress)->CRL |= ((static_cast<uint32_t>(oSpeed) & 0x03) << (maskOffset + 2));
} else {
// Error
// TO DO: add error compile time error message.
}
}
static inline bool get() {
return (reinterpret_cast<volatile GPIO* const>(GpioAddress)->IDR & GpioPinMask);
}
static inline void set() {
reinterpret_cast<volatile GPIO* const>(GpioAddress)->BSRR = GpioPinMask;
asm volatile("dsb;");
}
static inline void reset(){
reinterpret_cast<volatile GPIO* const>(GpioAddress)->BRR = GpioPinMask;
asm volatile("dsb;");
}
static inline void invert(){
reinterpret_cast<volatile GPIO* const>(GpioAddress)->ODR ^= GpioPinMask;
asm volatile("dsb;");
}
}
Теперь объявим определения портов в виде прведенных к типу адресов.
#define GPIOA (reinterpret_cast<volatile GPIO* const>(GPIOA_BASE))
#define GPIOB (reinterpret_cast<volatile GPIO* const>(GPIOB_BASE))
#define GPIOC (reinterpret_cast<volatile GPIO* const>(GPIOC_BASE))
Осталось только объявить constexpr классы:
constexpr const GpioPin<GPIOA_BASE, 0> PA0;
constexpr const GpioPin<GPIOA_BASE, 1> PA1;
constexpr const GpioPin<GPIOA_BASE, 2> PA2;
constexpr const GpioPin<GPIOB_BASE, 0> PB0;
constexpr const GpioPin<GPIOB_BASE, 1> PB1;
constexpr const GpioPin<GPIOB_BASE, 2> PB2;
constexpr const GpioPin<GPIOB_BASE, 3> PB3;
.....
constexpr const GpioPin<GPIOC_BASE, 0> PC0;
constexpr const GpioPin<GPIOC_BASE, 1> PC1;
constexpr const GpioPin<GPIOC_BASE, 2> PC2;
Что можно было усовершенстворовать? Внутри класса можно добавить различные проверки, которые будут выполнятся на этапе компиляции. Например проверки адресов.
И наконец пример использования. Выглядит как обычный класс, но компилпируетсся в 2-3 иассемблерных инструкции. При этом работает автодополнение, покрайне мере в eclipse.
PA0.set();
PA0.invert();
PA0.set();
PA0.reset();
PA0.invert();
Код доступен по ссылке: |
https://github.com/hwswdevelop/BluePillUsbDebugPrototyping/blob/main/Template/System/Core/gpio.h |
Комментарии (43)
oleg-m1973
19.09.2021 19:43+4А зачем здесь все эти constexpr, какой в них смысл? По-моему, они здесь не нужны вообще. Этот класс и без них скомпилируется в 2-3 инструкции.
И зачем такое количество reinterpret_cast, почему бы сразу не объявить константу нужного типа?
EvgenySbl Автор
20.09.2021 06:39Добрый день!
Возможно, я не совсем правильно понял вопрос. Дело в том, что любая константа будет занимать место в памяти, не важно, будет ли это оперативная память или сегмент ".text", который, как правило, располагается во flash памяти микроконтроллера. Если скомпилировать данный код без оптимизации, т.е. с опцией "-O0", то, он реально скомпилируется в 2-3 ассемблерных инструкции, чего нельзя сказать об обычном классе. Касательно, "-Os" или "-O3", вероятно разница будет небольшой. Если Вы можете показать как это сделать другим способом, я с удовольсивием приму Ваши предложения. Можно поэксперементировать с различными online компиляторами для проверки результата.
oleg-m1973
20.09.2021 13:13+1Насчёт того, почему постоянно вызывается reinterpret_cast, понял - его нельзя использовать в compile-time, поэтому нельзя объявить constexpr-указатель (не нашёл сходу, как это обойти).
Насчёт static const - непонятно, зачем вы вообще компилируете без оптимизации.
И ещё небольшое замечание - в методе mode() условие if constexpr (GpioPinNo < GpioConfPerReg){ можно заменить на static_assert(GpioPinNo < GpioConfPerReg)
mmatrosov
20.09.2021 14:27любая константа будет занимать место в памяти
Если нет ODR-использования константы (как и любой другой переменной), то компилятор постарается её выкинуть. Если он не может избавиться от её значения - то да, придётся сохранить в сегменте .text.
mctMaks
20.09.2021 10:55зачем такое количество reinterpret_cast
Для того чтобы использовать структуру, определенную в хедере на контроллер.
Тут либо делать каст, либо как написал @juramehanik ниже воспользоваться парсером и получить уже готовые структуры для работы с периферией.
juramehanik
19.09.2021 20:50+1Для начала определим адреса портов.
ох не так надо людей завлекать плюсами в микроконтроллерах=)
Что мешает подключать cmsis и быть более кросплатформенным, ну хотя бы не использовать магические числа?
Есть готовые плюсовые проекты которые парсят SVD файлы камней (и не только одного конкретного производителя) и на основе этого создают классы работы с периферией, почему бы там не применить свои идеи, чтоб не строить весь этот велосипед с нуля, а добавить к существующему свою блестящую звездочку? (здесь точно про это статьи две было, но пока не нашел с наскоку).EvgenySbl Автор
20.09.2021 06:56Доброе утро.
Я и не пытаюсь кого либо завлекать плюсами (C++) таким образом. Изначально, я написал прототип загрузчика для STM32, который работает по USB, и позволяет практически полноценно отлаживать код в отладчике среды разработки. Это вещь не кросплатформенная, от слова совсем. Затем понял что получилась неплохая альтернатива Arduino, но еще и с отладкой. Многие привыкли использовать эту среду разработки, но... Ее библиотеки мне не нравились ввиду неоптимальности. Ничего не имею против, но хотелось сделать это более оптимально. Так пришел к разработке собственной недорогой платформы для обучения программированию МК. (Откровенно говоря, даже 9 летнего племянника научил ей пользоваться). В итоге, решил доработать загрузчик, и написать базовый набор библиотек, которые будут с одной стороны просты для использования детьми, а с другой максимально оптимизированы вплоть до уровня ассемблерных инструкций.
Delsian
20.09.2021 14:29Кстати да. Мне нравится, как сделано в Zephyr - стандартный DTS на этапе компиляции превращается в вызовы CMSIS, соответствующего объявленному камню. И переход с STM32, например, на Nordic происходит максимально безболезненно.
lamerok
19.09.2021 21:01+1Можно не делать constexpr const объекты, они не нужны.
using PA0 = GpioPin<GPIOA_BASE, 2> PA2;; PAO::set();
Далее пины можно опять в шаблон запихать и управлять и настраивать их пачками, также без издержек, все в одну ассемблерную команду переведется.
Вообще тема норм, я что то подобное писал тут:
,https://habr.com/ru/post/459204/
И тут
EvgenySbl Автор
20.09.2021 07:00Спасибо. Полностью согласен с использованием "using", но мне хотелось получить Arduino-like код, и при этом оптимизированный с точки зрения машинных инструкций. Не хочется на начальном этапе объяснять детям чем отличается "::" от "->" или ".". За ссылки еще раз спасибо, обязательно посмотрю.
EvgenySbl Автор
20.09.2021 07:14Касательно настройки пачками, думал, как это правилно сделать. Обязательно воспользуюсь способом. Спасибо.
abutorin
19.09.2021 22:38На библиотеку Константина Чижова не смотрели? http://easyelectronics.ru/rabota-s-portami-vvoda-vyvoda-mikrokontrollerov-na-si.html
EasyLy
20.09.2021 00:34+1Дополню, что в той статье скорее рассказывается про GPIO и группы контактов, но по факту, эта библиотека связывает эти самые GPIO с классами прочих устройств (таймеров, UART, SPI, I2C...). Потому что в наше время линии этих устройств могут быть выведены на произвольные ноги. И вот GPIO этой библиотеки прекрасно всё связывает. Плюс драйверы устройств написаны весьма и весьма компактно. В общем, mcupp охватывает так много всего, что я сам ею пользуюсь, и информирую о её мощи при любом удобном случае. Так что после прочтения статьи по ссылке, надо понимать, что статья показывает только часть возможностей.
Недавно я общался с @DSarovsky , он делает библиотеку, похожую по идеологии, но базирующуюся на более современных стандартах C++. Я там смотрел GPIO и USB. GPIO реализовано в соответствии с заветами товарища Чижова, а USB у Чижова нет, не сравнить. Формирование дескрипторов - замечательное и простое. Работа с конечными точками - понятная. Скорость - предельно возможная, за счёт использования двойного буфера. Про драйвера других устройств пока не скажу, не довелось изучить в бою.
mctMaks
20.09.2021 11:04эта библиотека связывает эти самые GPIO с классами прочих устройств
а вот связывания действительно не хватает. Прокидывание GPIO в шаблон того же I2C/SPI/USART описано, да и самому написать не сложно. А вот например, привязку DMA канала через шаблон с проверкой, что этот канал не был настроен раньше, я пока не встречал. Сам пробую сделать, но пока особо не получается.
Я там смотрел GPIO и USB
я б тоже глянул с целью повышения образованности
Ardt2
19.09.2021 23:18+1Я как-то вот так недавно написал:
// enum class PinFunct : uint32_t { // 0x03(11) - Резерв. ODR - 1/0 Подтяжка верх/вниз. AnalogInput = 0x00UL, FloatInput = 0x01UL << 2, PullInput = 0x02UL << 2, PushPull = 0x00UL << 2, OpenDrain = 0x01UL << 2, AF_PushPull = 0x02UL << 2, AF_OpenDrain = 0x03UL << 2, }; enum class PinMode : uint32_t { Input = 0x00UL, Out2MHz = 0x02UL, Out10Mhz = 0x01UL, Out50Mhz = 0x03UL, }; // inline uint32_t operator|(PinFunct funct, PinMode mode) { return (uint32_t)funct | (uint32_t)mode; } // template<PinFunct Funct, PinMode Mode> class TPin : public TReg { private: TPin() = delete; protected: GPIO_TypeDef &Gpio; uint8_t Pin; public: TPin(GPIO_TypeDef &gpio, uint8_t pin) : Gpio(gpio), Pin(pin) { // ASSERT(Pin <= 15); // ASSERT(apb2enr(Gpio) != 0); if((Rcc.APB2ENR & apb2enr(Gpio)) == 0) { Rcc.APB2ENR |= apb2enr(Gpio); } if(Pin < 8) { SetCRL(); } else { SetCRH(); } } void __attribute__((always_inline)) On() { Gpio.BSRR = Mask(); } void __attribute__((always_inline)) Off() { Gpio.BRR = Mask(); } bool __attribute__((always_inline)) IsOn() { return Gpio.IDR & Mask(); } void SetCRL(PinFunct funct, PinMode mode) { Gpio.CRL &= ~(0x0FUL << Pin * 4); Gpio.CRL |= (Funct | Mode) << Pin * 4; } void SetCRH(PinFunct funct, PinMode mode) { Gpio.CRH &= ~(0x0FUL << (Pin - 8) * 4); Gpio.CRH |= (funct | mode) << (Pin - 8) * 4; } // protected: constexpr uint32_t __attribute__((always_inline)) Mask() { return 0x01UL << Pin; } // private: constexpr uint32_t apb2enr(GPIO_TypeDef &gpio) const { if(&Gpio == &GpioA) { return RCC_APB2ENR_IOPAEN; } else if(&Gpio == &GpioB) { return RCC_APB2ENR_IOPBEN; } else if(&Gpio == &GpioC) { return RCC_APB2ENR_IOPCEN; } else { return 0; } } void SetCRL() { Gpio.CRL &= ~(0x0FUL << Pin * 4); Gpio.CRL |= (Funct | Mode) << Pin * 4; } void SetCRH() { Gpio.CRH &= ~(0x0FUL << (Pin - 8) * 4); Gpio.CRH |= (Funct | Mode) << (Pin - 8) * 4; } }; // inline TPin<PinFunct::, PinMode::Out2MHz> TestPA15pin(GpioA, 15);
Про массовую настройку выводов у меня есть статья: https://habr.com/ru/post/448288/
Nufunello
20.09.2021 07:02static inline void mode(const GpioMode mode, const GpioOutputSpeed oSpeed = GpioOutputSpeed::Input){
if constexpr (GpioPinNo < GpioConfPerReg)
Попробуй static_assert, чтобы не писать "if else и в else ошибка"
EvgenySbl Автор
20.09.2021 07:09Доброе утро!
Касательно применения static_assert, полностью согласен. Но на самом деле это даже не срвсем вопрос отображения ошибок времени компиляции. Это просто пример как можно сделать и как проверить. На самом деле, если я правильно помню, в коныигурационеый регистр помещаются настройки первых 8ми пинов, а вторые 8 пинов 16и битного порта должны конфигурироваться с использованием другого регистра, по этому в коде даже не ошибка должна генерироваться, а использоваться другой регистр.
За комментарий еще раз спасибо.
sami777
20.09.2021 12:20Автор, вы меня заинтриговали! Каким образом инструкции dmb, dsb и isb могут заменить необходимость использования volatile?
EvgenySbl Автор
20.09.2021 14:15Они не могут заменить. Просто часто, на собеседованиях, человеку который знает что: "
Компилятор в процессе оптимизации делает реордеринг инструкций.
В процессе выполнения инструкции конвейерезируются, иногда приостанавливаются, иногда ускорябтся с применением байпасов (не ожидая окнчания предыдущей инструкции).
-
Еще к тому же и выполняются не в том порядке. А для ожидания применяются барьерные инструкции (кода, данных) "
задают вопросы про volatile и непременно хотят услышать тупой книжный ответ про оптимизацию. А ответ своими словами не принимается, что говорит о некомпетентности собеседующего.
EvgenySbl Автор
20.09.2021 14:25Я даже могу назвать одну из компаний в СПБ, где посчитали что у меня недостаточно опыта из за таких вот вопросов. Или еще была ситуация, когда мне задали вопрос о том, сколько можно помледовательно поставить операционных усилителей для того чтобы не было самовозбуждения. Я понял что нажо мной решили постебаться, и спокойно сказал: "главное чтобы полюсов не было", хотя мог бы оиветить "главное чтобы положительная обратная связь была недостаточна или чтобы не соблюдался юаланс фаз и амплитуд". Полагаю не все поняли о каких поюсах шла речь. А я имел ввиду расчет схемы с использованием преобразования Лапласа, когда возникает понятие нулей и полюсов, что определят устойчивость схемы....
mmatrosov
20.09.2021 14:30Небольшое замечание -
constexpr
объекты автоматически будутconst
, так что нет смысла писать для них `constexpr const`. А методы, объявленные в теле класса, автоматическиinline
, так что тоже нет необходимости это указывать ещё раз.
Leopoldius
20.09.2021 15:04@EvgenySbl, Касательно dsb и прочих барьеров, все у них хорошо, если не считать того, что в некоторых случаях можно получить пенальти по тактам. Т.е. процессор будет сидеть и педалить в ожидании завершения транзакций на шине AXI/AHB/whatever через которую он соединен с внешним миром.
В некоторых случаях это может быть критично, особенно при ногодрыге или быстром обмене, когда у тебя Chip select на spi, например с ручным приводом и ты общаешься на частотах 5-10 МГц и выше. Что приведет к задержке включения CS например или отключения его. Как следствие страстная любовь с трудноотлавливаемыми глюками.
Как по мне, каждому решению свое место должно быть. Сие есть мое IMHO :-)
EvgenySbl Автор
20.09.2021 15:13С одной стороны да, с другой стороны нет. В некоторых случаях это как раз помогает. Предположим что мы в ассемблерном коде подготовим две инструкции записи в порт заранее. А затем пропишем две str или им подобные инструкции последовательно. Есть ли гарантия что они выполнятся именно в том порядке? Во первых процессор может их зареордерить. Во вторых нужно учесть является ли данная память cachable, если да, то какой тип кеша и как это отразить на записи данных. Во третьих возникнет вопрос того что реально попадет на gpio порт и будет ли раельно сформирован glitch. В третих скорость gpio порта может быть, а как правило так и есть, значительно ниже скорости процесора....
Leopoldius
20.09.2021 15:24Безусловно. Мой посыл был в том, что нужно понимать когда и для чего барьер следует использовать.
Reflector
20.09.2021 16:40Volatile дает гарантии строгого порядка для volatile объектов, т.е. если дважды что-то записать в BSRR, то генерируемые инструкции будут идти в том же порядке, хотя между ними компилятор может вставить обращение к не volatile объектам. А если у нас есть две инструкции записи в порт, то никакого реордеринга не будет, кортексы так просто не умеют, уж точно не для портов висящих на одной шине, потому DSB тут не нужна, она только тормозов добавит, а у F1 и так порты медленные.
Leopoldius
20.09.2021 17:26Спасибо за дополнение. Об этом собственно и речь, что нужно понимать что ты делаешь и зачем ты это делаешь. )))
EvgenySbl Автор
20.09.2021 18:39На самом деле в психологии есть два хороших правила.
Единственный способ выиграть спор,- это не спорить.
Если Вы (я) ошиблись, сразу признавайте свою ошибку.
Хорошо, пррнято. А что если сначала записать odr, а потом прочитать idr? Транзакция на шине успеет пройти или конвейерезированная инструкция прочитает предыдущее значение? Это не спор, а попытка показать что все зависит от реализации в данном случае.
Reflector
20.09.2021 21:08А реализация от чего зависит? Мы же с регистрами работаем, тип памяти у нас Device и никакого кеширования, строгий порядок записи и чтения гарантируется уже только этим. Есть же периферия которая требует установки одного бита, а следом другого в том же регистре. Не вместе и не в обратном порядке и все это работает без никаких барьеров. А чтение из IDR вообще зависит от многих факторов, если есть быстрый мк, но медленные порты или задан не самый подходящий режим, типа OpenDrain с подтяжкой, то после записи в ODR придется вставлять задержку просто потому, что уровень сигнала на выходе нарастает слишком медленно и одного DSB может не хватить. Toggle() довольно часто пытаются так делать, читают IDR, инвертируют биты и пишут в ODR... Твой invert(), кстати, тоже проблемный, т.к. не атомарный, второй invert() над тем же портом в прерывании может все поломать.
EvgenySbl Автор
20.09.2021 23:20ОК. Еще один психологический прием, полезный руководителям и не только. Стоит выждать пока собеседник остынет и появится возможность возобновить общение. Это все из Карнеги. Но если почитать Э.Берна, то сечас Вы, (заметте, я написал с большой буквы, и не "ты", а "Вы"), играете в игру "Спор". А я не собираюсь спорить, спасибо. Мы похоже уже несколько ушли от обсуждения основной темы, и близки к тому чтобы перейти на личности ).
Если говорить по теме, то .... Конечно есть переферия и конечно работает, но, не всегда. Кто-то программирует простые системы, кто-то системы более сложные. И в сложных системах очень многое зависит от настроек MMU/MPU, от использования кэша. Если работает в одном случае, это не означает что будет работать в другом.
Касательно toggle, то, тут Вы безусловно правы. Правда я не использую IDR в качестве базовой величины, в моем случае это ODR, что вполне логично (ODR = ORD xor Bitmask).
А дальше вопрос процессора, компилятора и уровня оптимизации. К сожалению , в системе команд thumb/tumb2, насколько я помню, не предусмотрены битовые операции с ячейкой памяти, а соответственно операция инвертирования бита порта происходит посредством <load, invert, store> c применением регистра общего назначения, и именно по этому не является атомарной. Если бы stm32, имел регистр аналогичный BSRR, BRRR, но с названием BERR (где E - EOR), вероятно операция была бы атомарной.
PA1.reset(); // Первое обращение идет с подготовкой. Загрузка адреса и битовой маски 2 инструкции + 1 запись в порт + dsb. ldr r3, .L2 // Грузим адрес порта eor r2, r2, #2 // Битовая маска PIN1 r2 = 0; r2 |= 0x02 str r2, [r3, #12] // Запись в порт (сброс бита). dsb // Барьерная инструкция PA1.set(); // Далее одна ассемблерная инструкция + dsb str r2, [r3, #16] // Запись в порт (установка бита) dsb; // Барьерная инструкция PA1.reset(); // Далее одна ассемблерная инструкция + dsb str r2, [r3, #20] // Запись в порт (сброс бита) dsb; // Барьерная инструкция PA1.invert(); // Три ассемблерных инструкции + dsb ldr r2, [r3, #12] // Читаем ODR eor r2, r2, #2 // Делаем OR str r2, [r3, #12] // Записываем в порт dsb // Барьерная инструкция PA1.invert(); // Три ассемблерных инструкции + dsb ldr r2, [r3, #12] eor r2, r2, #2 str r2, [r3, #12] dsb
Если говорить о прерываниях, то смотря о чем речь. Насколько я помню, NVIC, повторно не позволит войти в прерывание с тем же приоритетом. А использование того же пина порта, при обработке в другом приерывании без выхода из текущего будет являться логической ошибкой программирования. Если же такая необходимость сущетвует можно защитить операцию инвертирования с применением запрета вызова прервыаний совсем или только некоторых прерываний. Для этих целей можно использовать, как флаг в регистре процессора, так и basepri, или же запрет прерывания на уровне переферии.
Касательно (PullUP / PullDown c режимами OpenDrain) и PushPull, в целом, это немного из другой оперы. Сигнал на линии не имеет прямого отношения к теме касающейся барьерных инструкций. За комментарий спасибо. "Если хочешь научиться играть, то всегда выбирай сильного противника/союзника",- это про Вас.
Reflector
21.09.2021 00:20Никто в здравом уме не станет для региона памяти в котором расположены регистры периферии менять тип памяти с Device на Normal и т.д., иначе вообще ничего работать не будет, потому рассуждать о подобным гипотетических ситуациях не имеет смысла.
И я говорил не про один и тот же пин порта, а про разные пины одного порта, один можно тоглить в главном цикле, а второй в прерывании и будет глючить. Даже HAL_GPIO_TogglePin() раньше работал чисто с ODR, а теперь тоже переделали через BSRR.
Goron_Dekar
Мне во всех реализациях классов gpio для stm не хватает массовых (желательно ещё и ленивых) настроек нескольких портов. Я понимаю, что можно или писать на регистрах, или делать по одному, благо перенастройка портов явление редкое, но мой внутренний перфекционист ждёт этого решения.
Сам брался писать его, но пока всё придумать элегантно решение не хватает знания плюсов. В общем, планировал сделать это так: метод класса порта при настройке возвращает временный объект, эти объекты присоеденяются друг к другу а вся работа по настройке происходит в их деструкторе.
Пока не знаю как это сделать гарантированно compile-time с выводом ошибки в случае невозможности разрешения.
kovserg
Всегда было интересно: что мешает сделать все настройки в before compile time обычным скриптом (perl,python,lua,php,js,groovy,...), который сгенерирует из входного запроса оптимальный код инициализации, необходимые константы, картинки и документацию. И затем с чистой совестью вызывать инициализацию периферии. Зачем это всё пихать в compile-time, при этом требуя компилятор с поддержкой последних стандартов? Не ужели стоя и в гамаке предпочтительнее.
Goron_Dekar
Поддерживал я как-то проект с таким подходом. Очень сложно при отладке и при мажорных изменениях.
DSarovsky
Это есть у Чижова, список типов, инстанцированный нужным набором портов. При разработке форка его mcucpp я тоже такое добавил, чтобы при настройке PinList-а одной строкой все используемые порты настроить (+ добавил уникальность). Вот так примерно выглядит:
lamerok
Тут _Ports понимается, как список пинов уже? или отдельный пин?
DSarovsky
My bad, не сразу понял, что Goron_Dekar имел ввиду. Перечитал и осознал, что идея именно для списка пинов применять настройки. Мой пример делает это для списка портов, но и для пинов есть соответствующий функционал, который заключается в получении для списка пинов списка портов (причем уникального).
Получаю список портов так:
Ну, собственно, этот список портов уже применим для предложенного выше кода.
mctMaks
Прошу прощения, тут ниже ветка с вашим упоминанием:
подскажите пожалуйста, а все желающие могут посмотреть код? интерес скорее академический.
DSarovsky
Конечно, буду более чем рад. Можно зайти в любую из моих статей, везде внизу есть ссылка на github (здесь не буду оставлять, думаю, это не очень красиво будет, как реклама).
mctMaks
а
слонассылку то я и не заметил, спасибо)sled
такой вариант Вам подойдет? (easyelectronics.ru Удобная настройка GPIO портов)
библиотека на
#define
, пример:GPIO_INIT_PIN(GPIOA,3,GPIO_MODE_INPUT_PULL_DOWN);
GPIO_INIT_PIN(GPIOA,4,GPIO_MODE_OUTPUT2_OPEN_DRAIN_UP);
или мультибит:
GPIO_MULTI_INIT(GPIOA,BIN(1010101010101010),GPIO_MODE_OUTPUT50_OPEN_DRAIN_DOWN);
mctMaks
не самое удачное решение, так как:
нет автоподсказок по параметрам при использовании макроса, что выливается в необходимости поиска места написания макроса каждый раз, когда через полгода приходится возвращаться к коду. Настройка пинов да и в целом периферии это 5%, ну максимум 10% , от объема кода проекта. Поэтому настройка должна быть простой и с подсказками;
макросы это иногда хорошо, если код на чистом Си. Тут же обсуждали С++ и шаблоны, что немного безопасней, так как есть проверка кода на этапе компиляции. Тут же ошибка проявится только после компиляции, и повезет если сразу станет понятно где она;
"BIN(1010101010101010)" - блондинка в красном платье?
Лучше тогда смотреть: http://easyelectronics.ru/rabota-s-portami-vvoda-vyvoda-mikrokontrollerov-na-si.html
Выше как раз упоминали автора сего кода, Константина Чижова. Статья образцовая, так как последовательная и достаточно детальная.
sled
Спасибо за пояснения!
Но для простолюдинов непонятны все эти высокие материи, соответственно никто не пользуется.
Вот еще статья про использование шаблонов и описаний из SVD - Безопасный доступ к полям регистров на С++ без ущерба эффективности (на примере CortexM)
BIN() - функция для препроцессора по конвертации последовательности бит в байты/слова.
mctMaks
это знаю. проблема в том, что в последовательности 0 и 1 очень легко ошибся и весьма не просто сказать какой пин стоит в каком состоянии. Поэтому видя "010010010101" попробуй угадать какой пин в какое состояние встал. особенно если для одних stm32 00 - это режим работы на вход, а для других 00 - режим работы с аналоговым сигналом.
с вами сейчас готова поспорить миллионная армия пользователей arduino. Они пишут на языке, близком по синтаксису к С++. При этом большая часть не задумывается и, наверно, не понимает как это работает. Просто берет и пользует.
Вопрос в другом, что сами вендоры не стремятся внедрять код на С++ к себе. У тех же ST явно хватит ресурсов сделать HAL на шаблонах (учитывая что touchGFX который они купили, как раз плюсовый код и дает на выходе). но почему это не идет в массы, я не понимаю.
да не за что, это просто мнение основанное на опыте чтения своих же проектов спустя 2-3 года после закрытия)
p.s.
это отсылка на фильм матрица. где оператор в бинарном коде на экране видел людей.