Pull to refresh

Comments 47

Чижов — голова и Александреску — голова.
А хорошо ли позволять программисту включать в PinsPack ноги от разных портов? Ведь не залезая внутрь реализации, создается ложное ощущение одновременности операции установки/сброса группы ног, а на самом деле компилятор ее размазывает в последовательность действий с портами, скрывая эту последовательность от программиста. Все таки, на мой взгляд, эмбеддер не должен настолько далеко дистанцироваться от железа, с которым он работает.
Это и есть HAL, когда мы представляем в виде одной сущности несколько более низкоуровневых, когда делаешь параллельную шину на МК где у каждого порта меньше выводов чем необходимая разрядность это прям существенно помогает. Эмбеддер должен думать о задаче по хорошему а не каждый раз вспоминать путь до неё. Таким образом будет более продуктивным. А оптимизации оставить уже на тот момент когда действительно видны проблемы.
Пример с параллельной шиной не очень удачный применительно к теме статьи, так как на шине обычно необходимо иметь возможность выставлять любое значение, а не только все 0 / все 1. Кстати, ради интереса, а с какими реальными устройствами Вы связываете однокристаллку по параллельной шине шириной более 16 бит?
Так можно собрать конструкцию которая будет выставлять необходимые биты в нужное значение (это не проблема). Порой бывает в stm32 что даже 16 бит собрать по одному порту это достаточно сложно — т.к. ноги заняты другими функциями.
Ну давайте попытаемся представить реальную ситуации, когда нам может потребоваться широкая 16-битная параллельная шина:
  1. Нам надо связаться с каким-то скоростным внешним устройством, скорость обмена настолько важна, что никакие SPI нас не устраивают
  2. Нам надо связаться с каким-то уникальным внешним внешним устройством, которое существует только с параллельным интерфейсом (да еще и шире байта), при этом скорость не критична
В первом случае мы ради скорости все равно будем вынуждены использовать один порт, а не «склеивать» его из отдельных линий разных портов (в крайнем случае, если уложимся в быстродействие, можно разделить 16-битное слово на два байтовых полуслова, но явно не собирать его из россыпи отдельных бит). Если никакими ремапингами альтернативных функций мы не можем для параллельной шины выделить хотя бы два «непрерывных» байта, то придется выбрать контроллер с большим числом ног, так как собрав шину из отдельных битов разных портов мы сильно потеряем в скорости обмена.
Во втором случае, действительно, можно набрать шину требуемой ширины из отдельных бит разных портов, но, положа руку на сердце, насколько часто в реальной практике встречается такая ситуация?

В радиолюбительской практике достаточно часто. Причём вариант 2 преобладает.

Возможно, но по моему в любительской практике более распространен ардуиновский подход, а не использование шаблонной «магии» C++. Впрочем, буду рад, если я в этом ошибаюсь.
Так можно же, для этого метод Write есть:
 __forceinline static void Write(std::size_t mask)
   {
     WritePorts(Ports(), mask) ;
   }

Вы связываете однокристаллку по параллельной шине шириной более 16 бит?

Я ни с какими, студенты светодиодами моргают, там их 32 штуки :) я просто ради интереса.
Согласен с no111u3, добавлю ещё, что даже когда вы работаете с регистрами, не все так очевидно.
Если, к примеру, хотите поставить бит в порт через регистр ODR:
GPIOA->ODR |= 0b010 ;
Это выглядит как одна операция, но на самом деле здесь 3 операции, чтение, установка, запись.
Ваш пример как раз подтверждает, что эмбеддер, хочешь — не хочешь, должен хорошо представлять архитектуру железа с которым работает. Для того, чтобы установить/сбросить бит (или набор бит) порта, в Cortex M существует регистр BSRR (о чем Вы сами же пишете в статье). Использовать для этого регистр ODR можно только если не знать архитектуру железа.

Как пример неудачного излишнего абстрагирования от железа можно привести библиотеки Arduino, где дошли до того, что для установки значения одного пина затрачивается несколько десятков тактов процессора, зато программист«программист» изолирован от того, что GPIO существуют не сами по себе, а организованы в порты, каждый из которых имеет свой набор регистров. Я понимаю, что Вы, используя «магию» шаблонов C++, реализовали все то же самое гораздо изящней и без таких излишних накладных расходов, но все равно сильно сомневаюсь в том, что это действительно надо разработчику.
Отчасти согласен, но тогда лучше писать на ассемблере. Ведь есть и другие регистры, в которых нет атомарного доступа, можно конечно перевести все установки битов на бит бендинг, но это очень специфичная фенечка для архитектуры.

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

Это как машина можно рулем прямо колесами управлять, а можно через усилитель и кучу электроники, думаю в разных ситуациях можно подходить по разному, нужна точная настройка, не уверен, что там компилятор сделает — пишем на ассмеблере. Не нужна такая точная настройка можно и HAL использовать.

Да на Ardunio согласен, там проблема в том, что по быстродействию, как и по объему кода никто не заморачивался, нужна была универсальность и по сути библиотека сделана для быстрого создания прототипов — решение для этого оптимальное.

Тоже самое можно сказать и про HAL, вот в новой статье STM32 fast start. Часть 2 Hello World на HAL, отладка в Atollic TrueSTUDIO же показано, что моргнул светодиодом за 1.54 КБ оперативы и 4.69 — это мне кажется крутовато.

Но опять же HALом же куча народа пользуется и не задумывается о том, что там вообще происходит.
Так я тоже согласен почти со всеми Вашими аргументами из этого комментария. Давайте вернемся к моему первому замечанию:
А хорошо ли позволять программисту включать в PinsPack ноги от разных портов?
Я же вовсе не возражал против Вашего подхода в принципе, а только против неявного для программиста смешивания битов разных портов. Ведь если мы будем вынуждены для битов нескольких портов написать не одну, а две (ну пусть три) операции над разными битсетами, мы же не переломимся от непосильного труда, зато будет явно видно, что это не единовременная операция.

По поводу ассемблера: я застал времена, когда для программирования 8051 это было безальтернативно. Я ни в коем случае не хочу в них возвращаться (чур меня!). Но я не против посмотреть, что нагенерил компилятор в критичных местах, и, при необходимости, переписать их на инлайн ассемблере. Правда, уже затрудняюсь вспомнить, когда мне последний раз потребовались ассемблерные вставки, в основном его приходится только читать. Но это уже зависит от того, у кого какая стоит задача.
Коллеги, а не кажется ли вам, что это ту мач?
Вот пишете Вы под микроконтроллер, памяти в обрез, герцев мало, код хотите видеть простой, понятный и предсказуемый, так как хороший дебаггер в реальном времени это не про нас.
И вот вместо приблизительно такого кода:
GPIO_BSRR_Write(GPIOA, (1<<1) | (1 << 3));
GPIO_BSRR_Write(GPIOB, (1 << 1));
GPIO_BSRR_Write(GPIOC, (1<<1) | (1 << 3));

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

Куда то мы не туда сворачиваем…
По моему это лютый оверкил. Вот нафига все эти пляски с бубнами. Чем все эти удобства помогут при написании например драйвера CAN шины? Или вы всё время с пинами работаете?

Ну не все же сразу :), я же для студентов, пока самое большое только Spi и драйвер для Еpaper… они делали и Modbus протокол. Пока времени нет описать все…

И что в modbus вам понадобилось пины пачками дёргать?
Всё таки для секаса с пинами есть vhdl и verilog, или встроенные аппаратные модули, а тут всё таки более высокоуровневые конструкции обычно.
Просто возмите любой поект и посмотрите на процент кода который работает непосредственно с портами.
Ну с портами фронт работ большой (кнопки, светодиоды, реле, переключатели всякие, однопроводные протоколы, индикаторы с параллельными шинами), но вообще код, который напрямую с аппаратурой работает у меня составляет не более 10%, это же не значит, что аппаратуру не надо описывать как-то.
Я же не говорю, что надо бросаться делать так, это просто пример, что в принципе на С++17 это сделать уже проще, чем было 10 лет назад и доступно каждому, ведь не многие понимают как Loki работает, а с constexpr функция это намного проще, уже ближе к нормальному программированию.
А вы не задумывались что проще сделать описание этой аппаратуры и преобразовывать в код скриптами (такой препроцессор), чем делать костыли на шаблонах и потом делать тоже самое, но только для C++17.
А чем скрипт отличается от С++. Вот и есть тоже самое практически, считайте это скрипт, только сразу лежит с кодом рядом, и запускается одновременно с компиляцией программы.
Просто попытки превратить C++ в perl выглядят довольно забавно.
Но я так понял вы еще этого не осознали.
UFO just landed and posted this here
>>У вас же памяти в обрез, какая ещё дебажная сборка?
Если без дебаггера ни как — мы в таких случая деоптимизируем только функции выборочно.

Другой важный момент — производительность, функции на шаблонах и других расширенных возможностях компилятора очень хорошо оптимизируются, стэк схлопывается и все превращается в несколько инструкций, но стоит только эту функцию деоптимизировать и у вас может быть 70-80 кратный разрыв в производительности с ее оптимизированой версией. Если такая функция вызывается из прерывания и занимает 30 микросекунд — то после деоптимизации один только ее вызов превратится в 2 миллисекунды, а если их несколько…

>>вы вместо того, чтобы доверять себе, обзаводитесь достаточно умным компилятором с достаточно умной системой типов и начинаете доверять ему
Эх, это даже не смешно, тонны примеров из прошлого можно даже не приводить, есть свежачок :) Работаю на Мико32, есть GCC и тул чэйн и вот мы недавно выясняем что эти замечательные тулзы компилируют С++ с ошибками в случае виртуальных функций, если в С функции параметр 16 битный то стек едет, есть и другие косяки.
Увы, доверять это не про нас.
функции на шаблонах и других расширенных возможностях компилятора очень хорошо оптимизируются, стэк схлопывается и все превращается в несколько инструкций, но стоит только эту функцию деоптимизировать и у вас может быть 70-80 кратный разрыв в производительности с ее оптимизированой версией.

Функция что на шаблонах, что не на шаблонах оптимизируются одинаково, поэтому если он 70-80 кратный разрыв производительности на шаблонах, то такой же будет и на обычной функции.
Там другое, там кода генерится много, если оптимизацию не включить, потому что получается много разных функции. Но в итоге в прерывании все равно вызовется какая-то одна…

А вот проблема с быстродействием может случиться при использовании constexpr функций, когда она вдруг становится не такой и все начинает вызываться в run time.

>>вы вместо того, чтобы доверять себе, обзаводитесь достаточно умным компилятором с достаточно умной системой типов и начинаете доверять ему
Эх, это даже не смешно, тонны примеров из прошлого можно даже не приводить, есть свежачок :)

Для этого и надо использовать сертифицированный на безопасность компилятор, например IAR имеет такой сертификат
UFO just landed and posted this here
Не видя кода вы говорите, что вероятней всего есть UB? Как вы это делаете? :)
А если серьезно, я не говорил что крив сам GCC, кривой тулчейн, а конкретно backend под мико32.
Увеличение сложности системы (да, увеличение: больше кода — больше точек отказа, спросите ребят из бэк-енда современных веб технологий считают ли они что все сделано правильно?) должно быть оправдано.
UFO just landed and posted this here
Мы всегда исходили из простого постулата — этот код/утилита сохранит нам время или заберет его?
Возьмем копилятор С++ и написание на асме, выгода на лицо. Поэтому за компилятор мы будем бороться до последнего :)
А вот эта библиотека какой выигрыш нам даст? Сколько мы ее будем интегрировать, отлаживать и какой выигрыш получим в конце? Если баланс сходится в плюс то используем, а если нет — то зачем?
Не поймите меня не правильно, я не против конкретно этого примера из статьи, но в масштабах индустрии вижу как целое поколение инженеров выбирает усложнение из соображений «это прикольно» нежели из соображений «это выгодно».
Недавний пример, пишем под код SoftCore NIOS II, один ведуший (!!) инженер решает завернуть все вызовы к APB регистрам в классы на шаблонах, вместо классических функций Write & Read уже протестированных, отлаженных и надежных как кувалда.
По итогу за каждую запись в регистр мы платили +22% (по сравнению с простой Write) в текстовом сегменте программы и -30% от производительности того же сишного кода и где то через пол года нашли баг в разыменовании 0 указателя, а через год разработки уткнулись в предел памяти. Все это нам стоило еще пары месяцев работы только чтоб все это разгрести, а выгода от С++ классов для доступа к регистрам была 0, я не шучу, все та же запись, но по другому выглядела.
Гениальность в простоте… но уходят десятилетия чтоб научиться эту простоту создавать.

В данном примере, полностью согласен с вами.
На то, чтобы сделать хорошую библиотеку нужно время и ресурсы. Мысль просто обернуть регистры в классы ради хайпа во время проекта у которого есть дедлайн не очень хорошая.


Другое дело, если это отдельный проект и бизнес выделил на это средства, немного потратим сейчас, зато получим бонусы в будущем. В таком случае это имеет смысл.


В конечном счёте простота должна быть в использовании, а желательно ещё, чтобы было надёжно и эффективно. Но повторюсь, для того, чтобы это продумать нужно время и ресурсы и не у всех компаний и проектов они есть.

Возможно, но если это написано один раз и оформлено в виде библиотеки, то почему нет… Чем каждый раз писать одно и тоже и иметь потенциальный риск ошибки, один раз написали такой "мостроузный" код, отладили и все, тем более, что по факту тут, особо ничего не делается.
Макросы же люди пишут, их отлаживать, например ещё труднее, и ловить в них ошибки, тут то проще все.

Set, Reset и Toggle — это востребованные операции для пинов, для списка пинов — это будут Write и Read, которых тут нет и реализовать которые значительно сложнее. Также нельзя объединять списки пинов с другими списками и пинами, лично я такое тоже использую достаточно часто.
Write то есть, onlinegdb.com/r1eoXQBRH, Read в примере нет, но тоже не сложный, а вот объединения списков нет действительно.

Во-первых, это не Write, в Write должны передаваться данные, а не просто маска. Маска — это константа, а данные обычно нет, их нельзя пропустить через constexpr функцию и получить на выходе константу, вместо этого будет генериться огромное количество кода… Во-вторых, у меня даже Write не работает. Например, пишем:


PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5>::Write(7);

И на gcc 9.2 получаем одну инструкцию, хотя пины в списке для трех портов


ldr r3, [r3, #16] 

Ага, там вроде просто заглушки в некоторых местах, а на git Write нет…

Да похоже в Git забыл запушить…
А так Write работает и на GCC 9.2
gcc.godbolt.org/z/JpB_GU — строки 178-189 для двух Write(7) и Write('A')
Он правда оптимизировал их.
А вот в рантайме
gcc.godbolt.org/z/p7ryy6 — строки 263 — 331
Но тоже вроде работает…
Для расчет значения используется вот такая функция:
 template<class Q>
   constexpr static auto GetPortValue(std::size_t mask) 
   {
     std::size_t result = 0;  
     auto rmask = mask ;
     //Для установки нужных битов
     pass{(result |= ((std::is_same<Q, typename Ts::PortType>::value ? 1 : 0)  & 
         mask) * (1 << Ts::pin), mask>>=1)...};
     //Для сброса нужных битов
     pass{(result |= ((std::is_same<Q, typename Ts::PortType>::value ? 1 : 0) & 
         ~rmask) * ((1 << Ts::pin) << 16), rmask>>=1)...};
     return result;
   }      


Если mask будет константа, то функция будет выполнена в компайл тайме, если нет, то в рам тайме, со всеми вытекающими.
Но для определения компайлтайма, и введена функция Write<7>();
А в рантайме использовать можно Write(7);

Возьмем простой пример:


for (uint32_t i = 0; i < 10; ++i)
{
    PinList<PA7, PA6, PA5, PA4, PA3, PA2, PA1, PA0>::write(GPIOA->IDR);
}

У компилятора есть все необходимые данные чтобы на этапе компиляции определить, что write() можно свести к:


GPIOA->BSRR = 0xFF'0000 | (GPIOA->IDR & 0xFF);

Или даже записи в половинку порта, что еще немного эффективнее… А во что это скомпилируется при использовании GetPortValue()?

Да будет не айс, но это из-за того, что по сути вызов
PinList<PA7, PA6, PA5, PA4, PA3, PA2, PA1, PA0>::write(GPIOA->IDR);

вырождается в вызов
PinList<PA7, PA6, PA5, PA4, PA3, PA2, PA1, PA0>::write(*reinterpert_cast<volatile uint32_t*>(IDR_ADDRESS));

А в компайлтайм компилятор С++ (по стандарту) reinterpret_cast делать не умеет. Ну т.е. по стандарту у компилятора нет необходимых данных. Отсюда все вытекающие проблемы.

Будет не айс, потому что такая реализация, из-за этого и Read/Write реализуются элементарно. Даже старенькая либа Чижова находила подобные последовательности пинов и генерила более эффективный код, хотя далеко не всегда… Если я, допустим, пишу класс для дисплея и хочу передавать туда все пины данных в виде списка пинов, то какой смысл это делать если даже для 8-ми подряд идущих пинов получим достаточно медленную реализацию? Сейчас у меня в подобной либе есть строка:


PinList<Pins::RS, Pins::WR, Pins::Data>::write(data);

Т.е. пишем 8 бит данных(PB15..8) и одновременно сбрасываем RS и WR, если все пины на одном порту, то получаем:


ldr r2, [pc, #24]   
ldr r3, [pc, #24]
orr.w r0, r2, r0, lsl #8 
str r0, [r3, #24]

Если же все 10 пинов будут идти вразброс таким образом, что никаких закономерностей обнаружено не будет, тогда получится нечто похожее на результат после GetPortValue(), но это в самом худшем случае.

Если пины на одном порту и идут последовательно, то не париться и сразу маску накладывать, правильно я понял?

Я уже принцип описывал. После сортировки пинов по портам ищутся последовательности пинов, их может быть много и пины не обязательно идут подряд. Допустим есть последовательность PA5, PB3, PA2, PB1, PA1, где самый правый пин проецируется на нулевой бит входных данных. Берем крайний PA1, разница между номером бита пина и данных равна 1 — 0 = 1, для PA2 она 2 — 2 = 0, а для PA5 получим 5 — 4 = 1. Для PA1 и PA5 разница одинаковая, значит можно два бита данных для этих пинов выделить маской, которую посчитать не проблема, сдвинуть на 1 влево, аналогичную операцию проделать для оставшегося PA2 и добавить маску очистки всех пинов данного порта. Это основа, опционально можно искать реверсные цепочки и т.д....

Ага понял, добавлю, спасибо.
UFO just landed and posted this here

Спасибо, как говорится век живи век учись. В
IAR C++ 17 появился только 6 месяцев назад, полноценный, начиная с версии 8.40.2, поэтому опыта использования его было не много. С fold expression только только начал пользоваться и уже понял, что они существенно могут сократить гемор.
И момент такой, что хоть IAR и поддерживает синтаксис C++17, библиотечные функции в нем не все реализованы, надо проверить, есть ли там all_unique. Подозреваю, что нет.
Ещё раз спасибо, как всегда, очень полезное замечание.

UFO just landed and posted this here

Я еще повнимательнее посмотрел, и подумал, что от дубликатов то все равно надо будет избавляться… Кроме проверки на уникальность, нужно формировать список портов, по которым бегать. (Не очень эффективно для компилятора, но зато кода не так много, так используется NoDuplicates из Loki)


// Формируем список пинов без дубликатов
   using  TPins =  typename NoDuplicates<Collection<Ts...>>::Result;
   // Проверяем совпадает ли исходный список пинов со списком без дубликатов
   static_assert(std::is_same<TPins, Collection<Ts...>>::value, 
                 "Беда: Одинаковые пины в списке") ;   
   // Формируем список уникальных портов
   using Ports = typename 
                     NoDuplicates<Collection<typename Ts::PortType...>>::Result;

Можно конечно его формировать по другому, просто идти и смотреть, что таких портов еще нет в списке добавляем, есть не добавляем — не через Loki, но почему бы уже готовым велосипедом не воспользоваться, тем более, что он используется для двух целей: Формирование списка уникальных портов и проверки списка пинов на уникальность


А вызов через fold expression для установки портов — замечательная идея, немного подправлю и если время будет новую статью забабахаю :)

UFO just landed and posted this here
Зачем? Если есть дубликаты, то срабатывает ассерт, и компиляция прекращается.

Чтобы список портов сделать…
Вначале берем Pinы, и вытаскиваем из них все порты на которых они сидят
Например:


using Pin1 = Port<GPIOB, 3>;
using Pin2 = Port<GPIOB, 3>;
using Pin3 = Port<GPIOA, 3>;

Typelist<Pin1, Pin2, Pin3> ;

из него получается список портов:


Typelist<GPIOB, GPIOB, GPIOA> ;

и его нужно сократить до:


Typelist<GPIOB, GPIOA> ;

А потом уже пробежаться по 2 портам только и записать в них рассчитанные значения.
Собственно, чтобы велик не делать, я делаю так:


using Ports = typename 
                     NoDuplicates<Collection<typename Ts::PortType...>>::Result ;

А заодно этот NoDuplicates еще использую для проверки того, что сам список Pinов не имеет дубликатов.

Sign up to leave a comment.

Articles