Как стать автором
Поиск
Написать публикацию
Обновить
4
0

Пользователь

Отправить сообщение

Я взял функцию из статьи и по-быстрому переделал, естественно имена старался не менять, потому если анализатору не нравится сбивающее с толку имя, в этом не моя вина :) Далее, все предупреждения где встречаются PERIPH_BASE и т.д., которых нет в моем коде, а так же все ворнинги с приведением кроме одного моего "old style cast" — это все предупреждения для стандартных хедеров. Причем местами не понятно что оно в принципе хочет, например, у меня FLASH_ACR_LATENCY_Msk задефайнен как (0x7UL << FLASH_ACR_LATENCY_Pos), а в предупреждении речь идет про underlying type unsigned 8-bit int… И где оно 8 бит увидело? Ну ладно, я то как-то переживу, а многие использовали CMSIS даже не подозревая, что в продакшен с ним код не пустят :)

Это написано на C++, bool неявно приводится к int, приводить его к uint32_t в данном случае не обязательно, т.к. даже несмотря на возможное смешение знаковых и беззнаковых типов бинарно результат все равно будет одинаковым. У меня обычно параноидальные настройки ворнингов не выставлены, но сейчас добавил для Gcc -Wall -Wextra -Wpedantic и получил только предупреждение касательно скобок, причем если их добавить, то их выводит серым цветом, как необязательные :) VS и Resharper тоже молчат, хотя там часть предупреждений заблокирована, так что даже не знаю что нужно сделать чтобы на одной строке получить 25 предупреждений...

Какая тут ошибка? С приоритетами операций все нормально...

Автор занимается бессмысленной работой, сначала для каждого мк объявляет кучу структур, потом прячет код внутри setLatency(), хотя можно было просто написать:


INLINE static void setLatency(Flash::Latency latency, bool prefetchBufferEnable = false)
{
    FLASH->ACR = FLASH->ACR & ~(FLASH_ACR_LATENCY | FLASH_ACR_PRFTEN) | 
        uint32_t(latency) | prefetchBufferEnable << FLASH_ACR_PRFTEN_Pos;
}

В итоге весь код будет завернут в классы или просто функции, пользователь либы с регистрами напрямую все равно не пересекается, какая разница что там внутри...

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

Будет не айс, потому что такая реализация, из-за этого и 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(), но это в самом худшем случае.

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


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()?

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


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

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


ldr r3, [r3, #16] 

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

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

С нуля никто компиляторы не пишет, если, допустим, в GCC нужно добавить поддержку новой серии мк, то пишут для него Backend и он одинаковый для С и С++.

Списки пинов — это достаточно продвинутая тема, не каждый такое может написать, а использовать чужое не каждый хочет… А так да, я в класс SPI передаю 4 пина, сам класс SPI передается в какой-нибудь класс дисплея или sd-карты, итого 2 строки, при этом сами пины существуют можно сказать виртуально, а не в массиве указывающем на реальные объекты. Как-то так:


using spi1 = Spi1<PA7, PA6, PA5, PA4>;
using lcd = LcdSpi<ST7735, LcdOrient::Landscape, spi1, PA8, PA10>;

lcd::init(SpiBaudRate::Presc_2);

Внутри класса SPI пины передаются в другой класс который проверяет их допустимость и если что получаем ошибку компиляции с конкретным указанием какая именно нога задана неверно, а возвращается оттуда уже список пинов с добавленными AF. Далее всему списку задается режим, в данном случае 3 разных режима и т.к. все 4 пина относятся к одному порту, то запись в регистры GPIOA будет объединена. Собственно запись констант в известный набор регистров GPIO — это первая и единственная рантайм операция с пинами, все остальное делается на этапе компиляции, так что бесплатные абстракции очень даже бывают.

Есть у меня класс для работы с дисплеями, дисплеи могут быть с разными контролерами и работает это все через SPI, FMC или ногодрыг. В последнем случае я передают в шаблон отдельные управляющие пины(RS, WR...) и список пинов для данных, там они могут идти в любом порядке. Можно взять плату на которой дисплей с 16-ти битной шиной подключен к FMC и заставить ее работать через ногодрыг задав для данных такой список пинов:


PinList<PD10, PD9, PD8, PE15, PE14, PE13, PE12, PE11, PE10, PE9, PE8, PE7, PD1, PD0, PD15, PD14> LcdData;

Все, теперь внутри либы будет вызваться LcdData::write(...) и все разлирутся самом собой, пины автоматически разобьются на 4 группы и данные будут записаны в 2 порта, причем так же работает чтение, установка режима, быстрая смена направления и т.д… С точки зрения пользователя все предельно просто, эффективность на самом высоком уровне… С семисегментником будет то же самое, не важно к каким ногам он подключен, достаточно в списке расставить пины в правильном порядке.

В статье довольно простой поиск групп пинов, там ищутся только идущие подряд, только в одном направлении и не должно быть других пинов относящихся к тому-же порту, т.е. список из PA8, PA7, PA6, PA3 — это 4 отдельных пина, из-за последнего. У меня ищутся цепочки в обоих направлениях и порядок не имеет значения, для списка PA8, PA7, PA3, PA5 будет найдена цепочка PA8, PA7, PA5, потому что если биты 3, 2 и 0 входных данных сдвинуть на 5 влево, то они станут как раз битами 8, 7 и 5. И в статье старенькая Loki используется, я брал за основу списки типов от Олега Фатхиева, на вариадиках, есть видео на youtube .

Я не писал функции конкретно для портов, у меня есть функции для периферии в целом:


enablePeriphClock(AhbPeriph::GpioA | AhbPeriph::GpioB | AhbPeriph::Dma1);
enablePeriphClock(Apb1Periph::Power | Apb1Periph::Tim2);

А вот реализация, при условии того, что операция '|' и прочие для перечислений уже присутствуют:


void enablePeriphClock(AhbPeriph periph)  { RCC->AHBENR |= uint32_t(periph); }
void enablePeriphClock(Apb1Periph periph) { RCC->APB1ENR |= uint32_t(periph); }

Просто и безопасно, не говоря уже о том, что '|' для перечислений — это еще и более естественно, чем перечисление через запятую.

Зачем здесь шаблоны с переменным количеством аргументов, если можно просто использовать перечисления?


Rcc.PortOn(GPort::A | GPort::B);

Правда придется перегрузить операцию '|', но это в современном С++ не проблема, можно перегрузить хоть для всех перечислений сразу:


template<typename E, typename = std::enable_if_t<std::is_enum_v<E>>>
constexpr E operator|(E lhs, E rhs)
{
    using T = std::underlying_type_t<E> ;
    return static_cast<E>(static_cast<T>(lhs) | static_cast<T>(rhs)) ;
}

Информация

В рейтинге
Не участвует
Зарегистрирован
Активность