Комментарии 47
- Нам надо связаться с каким-то скоростным внешним устройством, скорость обмена настолько важна, что никакие SPI нас не устраивают
- Нам надо связаться с каким-то уникальным внешним внешним устройством, которое существует только с параллельным интерфейсом (да еще и шире байта), при этом скорость не критична
Во втором случае, действительно, можно набрать шину требуемой ширины из отдельных бит разных портов, но, положа руку на сердце, насколько часто в реальной практике встречается такая ситуация?
__forceinline static void Write(std::size_t mask)
{
WritePorts(Ports(), mask) ;
}
Вы связываете однокристаллку по параллельной шине шириной более 16 бит?
Я ни с какими, студенты светодиодами моргают, там их 32 штуки :) я просто ради интереса.
Как пример неудачного излишнего абстрагирования от железа можно привести библиотеки Arduino, где дошли до того, что для установки значения одного пина затрачивается несколько десятков тактов процессора, зато
Вообще если используется не ассемблер, то часть знаний вы уже как бы автоматом компиялтору доверяете, ведь ваш код, написанный на Си или С++, компилятор при разных условиях оптимизации превратит в разный ассемблерный. Хотя вроде как у вас код как был, так и остался. Мы же приняли, что пишем на более высоком языке, а не ассемблере в угоду простоты и читаемости, но при этом теряем немного в возможности управления самим микроконтроллером.
Это как машина можно рулем прямо колесами управлять, а можно через усилитель и кучу электроники, думаю в разных ситуациях можно подходить по разному, нужна точная настройка, не уверен, что там компилятор сделает — пишем на ассмеблере. Не нужна такая точная настройка можно и 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));
У вас пара сотен линий кода на шаблонах и трудно предсказуемого шаманства компилятора, которое у любого ревьювера вызовет легкий тремор левого века, создаст многократный разрыв производительности в дебажной и релизной сборках, а так же при поиске багов будет как магнит притягивать к себе внимание всей команды инженеров.
Куда то мы не туда сворачиваем…
Ну не все же сразу :), я же для студентов, пока самое большое только Spi и драйвер для Еpaper… они делали и Modbus протокол. Пока времени нет описать все…
Всё таки для секаса с пинами есть vhdl и verilog, или встроенные аппаратные модули, а тут всё таки более высокоуровневые конструкции обычно.
Просто возмите любой поект и посмотрите на процент кода который работает непосредственно с портами.
Я же не говорю, что надо бросаться делать так, это просто пример, что в принципе на С++17 это сделать уже проще, чем было 10 лет назад и доступно каждому, ведь не многие понимают как Loki работает, а с constexpr функция это намного проще, уже ближе к нормальному программированию.
Если без дебаггера ни как — мы в таких случая деоптимизируем только функции выборочно.
Другой важный момент — производительность, функции на шаблонах и других расширенных возможностях компилятора очень хорошо оптимизируются, стэк схлопывается и все превращается в несколько инструкций, но стоит только эту функцию деоптимизировать и у вас может быть 70-80 кратный разрыв в производительности с ее оптимизированой версией. Если такая функция вызывается из прерывания и занимает 30 микросекунд — то после деоптимизации один только ее вызов превратится в 2 миллисекунды, а если их несколько…
>>вы вместо того, чтобы доверять себе, обзаводитесь достаточно умным компилятором с достаточно умной системой типов и начинаете доверять ему
Эх, это даже не смешно, тонны примеров из прошлого можно даже не приводить, есть свежачок :) Работаю на Мико32, есть GCC и тул чэйн и вот мы недавно выясняем что эти замечательные тулзы компилируют С++ с ошибками в случае виртуальных функций, если в С функции параметр 16 битный то стек едет, есть и другие косяки.
Увы, доверять это не про нас.
функции на шаблонах и других расширенных возможностях компилятора очень хорошо оптимизируются, стэк схлопывается и все превращается в несколько инструкций, но стоит только эту функцию деоптимизировать и у вас может быть 70-80 кратный разрыв в производительности с ее оптимизированой версией.
Функция что на шаблонах, что не на шаблонах оптимизируются одинаково, поэтому если он 70-80 кратный разрыв производительности на шаблонах, то такой же будет и на обычной функции.
Там другое, там кода генерится много, если оптимизацию не включить, потому что получается много разных функции. Но в итоге в прерывании все равно вызовется какая-то одна…
А вот проблема с быстродействием может случиться при использовании constexpr функций, когда она вдруг становится не такой и все начинает вызываться в run time.
>>вы вместо того, чтобы доверять себе, обзаводитесь достаточно умным компилятором с достаточно умной системой типов и начинаете доверять ему
Эх, это даже не смешно, тонны примеров из прошлого можно даже не приводить, есть свежачок :)
Для этого и надо использовать сертифицированный на безопасность компилятор, например IAR имеет такой сертификат
А если серьезно, я не говорил что крив сам GCC, кривой тулчейн, а конкретно backend под мико32.
Увеличение сложности системы (да, увеличение: больше кода — больше точек отказа, спросите ребят из бэк-енда современных веб технологий считают ли они что все сделано правильно?) должно быть оправдано.
Возьмем копилятор С++ и написание на асме, выгода на лицо. Поэтому за компилятор мы будем бороться до последнего :)
А вот эта библиотека какой выигрыш нам даст? Сколько мы ее будем интегрировать, отлаживать и какой выигрыш получим в конце? Если баланс сходится в плюс то используем, а если нет — то зачем?
Не поймите меня не правильно, я не против конкретно этого примера из статьи, но в масштабах индустрии вижу как целое поколение инженеров выбирает усложнение из соображений «это прикольно» нежели из соображений «это выгодно».
Недавний пример, пишем под код SoftCore NIOS II, один ведуший (!!) инженер решает завернуть все вызовы к APB регистрам в классы на шаблонах, вместо классических функций Write & Read уже протестированных, отлаженных и надежных как кувалда.
По итогу за каждую запись в регистр мы платили +22% (по сравнению с простой Write) в текстовом сегменте программы и -30% от производительности того же сишного кода и где то через пол года нашли баг в разыменовании 0 указателя, а через год разработки уткнулись в предел памяти. Все это нам стоило еще пары месяцев работы только чтоб все это разгрести, а выгода от С++ классов для доступа к регистрам была 0, я не шучу, все та же запись, но по другому выглядела.
Гениальность в простоте… но уходят десятилетия чтоб научиться эту простоту создавать.
В данном примере, полностью согласен с вами.
На то, чтобы сделать хорошую библиотеку нужно время и ресурсы. Мысль просто обернуть регистры в классы ради хайпа во время проекта у которого есть дедлайн не очень хорошая.
Другое дело, если это отдельный проект и бизнес выделил на это средства, немного потратим сейчас, зато получим бонусы в будущем. В таком случае это имеет смысл.
В конечном счёте простота должна быть в использовании, а желательно ещё, чтобы было надёжно и эффективно. Но повторюсь, для того, чтобы это продумать нужно время и ресурсы и не у всех компаний и проектов они есть.
Возможно, но если это написано один раз и оформлено в виде библиотеки, то почему нет… Чем каждый раз писать одно и тоже и иметь потенциальный риск ошибки, один раз написали такой "мостроузный" код, отладили и все, тем более, что по факту тут, особо ничего не делается.
Макросы же люди пишут, их отлаживать, например ещё труднее, и ловить в них ошибки, тут то проще все.
Во-первых, это не Write, в Write должны передаваться данные, а не просто маска. Маска — это константа, а данные обычно нет, их нельзя пропустить через constexpr функцию и получить на выходе константу, вместо этого будет генериться огромное количество кода… Во-вторых, у меня даже Write не работает. Например, пишем:
PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5>::Write(7);
И на gcc 9.2 получаем одну инструкцию, хотя пины в списке для трех портов
ldr r3, [r3, #16]
Ага, там вроде просто заглушки в некоторых местах, а на git Write нет…
А так 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 и добавить маску очистки всех пинов данного порта. Это основа, опционально можно искать реверсные цепочки и т.д....
Спасибо, как говорится век живи век учись. В
IAR C++ 17 появился только 6 месяцев назад, полноценный, начиная с версии 8.40.2, поэтому опыта использования его было не много. С fold expression только только начал пользоваться и уже понял, что они существенно могут сократить гемор.
И момент такой, что хоть IAR и поддерживает синтаксис C++17, библиотечные функции в нем не все реализованы, надо проверить, есть ли там all_unique. Подозреваю, что нет.
Ещё раз спасибо, как всегда, очень полезное замечание.
Я еще повнимательнее посмотрел, и подумал, что от дубликатов то все равно надо будет избавляться… Кроме проверки на уникальность, нужно формировать список портов, по которым бегать. (Не очень эффективно для компилятора, но зато кода не так много, так используется 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 для установки портов — замечательная идея, немного подправлю и если время будет новую статью забабахаю :)
Зачем? Если есть дубликаты, то срабатывает ассерт, и компиляция прекращается.
Чтобы список портов сделать…
Вначале берем 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ов не имеет дубликатов.
Работа со списком Pinов, на С++ для микроконтроллеров (на примере CortexM)