Как стать автором
Обновить

Комментарии 41

Еще вариант: DEFINE битовую маску для каждого используемого модуля (0 или значащие биты). Будет работать и на простом С.
Можно уточнить, где тут «за 1 такт»?
Да, естественно, за 1 такт(инструкцию) контроллера включить ничего не получится. В контексте статьи под этим подразумевается — за минимально возможное время и размер кода.
Извините, если на святое покушаюсь: а почему не кодогенерация сишного кода, на питоне, например? Всё-таки есть у меня ощущения, что макросы в си, что шаблоны в плюсах идеологически не бьются с остальным языком (хотя с учётом приобретенной фичерастости и мультипарадигменности, даже Нъярлатхотеп выглядит родным братом плюсов).
По сути, шаблоны и дефайны, — также используются для кодогенерации, но не так явно. Кроме того, скорость и сложность разработки, как мне кажется, будут сопоставимы.
Сейчас я использую стороннюю кодогенерацию, скорее как дополнение к самой логике на шаблонах. Например, если брать систему тактирования, то у меня есть парсер, которому можно скормить svd файл контроллера + несколько уточнений, а на выходе получить «адаптер» для тактирования + периферийные драйверы, в которые допишется свойство power.
Мне такой подход нравится несколько больше (хотя обычно я пишу «по-пионерски»), потому что можно, например, проще, чем lamerok описывал, сделать то же объединение флагов в маску и ловить ошибки и повторы/опечатки.
Если совсем уж писать-писать на макросах, то кажется, что это совсем другой язык, по классификации ближе всего к ФП в терминах рефал-машины, что вывихивает мозг относительно прочего кода :) Даже М4 как-то ближе.
Если брать за основу код из статьи, то до Pinlist' ов lamerka всего где-то 100 строчек.

Но, да, конечно, — трехэтажные макросы и метафункции выглядят монструозно, с этим трудно спорить.

Интересный прием, думал сделать что-то подобное, но потом понял что использовать не стану. Переферию обычно нужно не только включить, но и настроить, а так же включить нужные пины. На фоне полной инциализации избыточное кол-во записей в пару регистров находится на уровне процента, а на фоне общей работы прошивки вовсе теряется.

Да, конкретно в этом случае выигрыш не сильно заметен. Но если добавить около 100 строк кода, то получится примерно Pinlist. Который позволяет скопом инициализировать пины:
int main(){
  Pinlist<spi1, uart1, spi2, pin1, led, i2c2, uart3>::Init();  
}

Будет сформирован список используемых портов, пины будут распределены по этим портам, сформированы значения для записи и будет произведена однократная модификация каждого регистра. Т.е. если вся периферия сидит на 1 порте А, то только в него и произойдет запись, если на 3 — соответственно 3. Плюс включение тактирования, плюс включение прерываний — в сумме может набежать значительно.

Как пример: есть M0 с 16кБ флеша. Из них 6 на бутлоадер. Ну не ровно 6, а на пару десятков байт больше. Соответственно, получается, что занята еще одна страница памяти и на пользовательский код остается уже на целых 10% меньше. Можно было попробовать решить задачу, используя ассемблер, а можно вот так.

Лично меня напрягает, что включаешь SPI2 и UASRT1, а включается также часть портов и DMA… А если DMA не используется? Или если у мк есть DMAMUX и можно для любой периферии выбрать любой канал любого DMA, то написать SPI<2> уже будет недостаточно. А ремап пинов? Есть мк у которых ноги SPI ремапятся на все порты, следовательно чтобы определить эти порты нужно в класс SPI передать его номер, 4 пина и канал DMA… Передав пины логично уже там их и инитить, а это до 5-ти регистров на порт(за исключением F1), при этом не самая эффективная инициализация значительно перекроет оверхед приходящийся на лишнее включение бита периферии, если его делать по отдельности.

Я намеренно упростил реализацию периферии, чтобы не перегружать статью.

В данный момент я использую примерно такой концепт для периферии:
Периферия
template<auto identifier, bool isDMA, bool isRemap>
struct Peripheral{

 // В зависимости от шаблона, сюда подставляются необходимые значения
  using power = Valuelist<
    isDMA ? DMAEN : 0, 
    identifier ==  1 ? PeripheralBitEn1 : PeripheralBitEn2,
    isRemap ? AFIOEN : 0 |  identifier ==  1 ? GPIOAEN : GPIOBEN>;

  // С пинами - аналогичная ситуация. На этапе компиляции известен и порт и номер и настройка
  using pins = Typelist<Pin<GPIO, n, AF>, Pin<GPIO, y, INPUT>>;

  // 
  using interrupts = Valuelist<isDMA ? IRQ1 : IRQ2>;
};


Далее, для всего этого есть три соответствующих класса:
Классы инициализации
template<typename... Peripherals> // Тот, что описан в статье.
struct Power{ 
  
  static void Enable();
};

template<typename... Peripherals> // Похожий класс для включения/выключения прерываний
struct Interrupts{ 
  
  static void Enable();
};

template<typename... Peripherals> // Самый интересный - проверка повторов, распределение по портам и т.д.
struct Pinlist{ 
  
  static void Init();
};




Пример использования:
using peripheral1 = Peripheral<1, true, true>;
using peripheral2 = Peripheral<2, false, false>

using initList = periph<peripheral1 , peripheral2>;

int main(){
  Power<initList>::Enable();
  Pinlist<initList>::Init();
  Interrupts<initList>::Enable();
}

Берем какой-нибудь G071RB, там всего два SPI, но суммарно они ремапяпся на 39 пинов 4-х портов, так что isRemap достаточно только для морально устаревших F1.

Для этого варианта можно ввести структуру, в которой, естественно, можно задавать все руками:
Структура
template<auto _portMosi, auto _pinMosi, auto _portMiso, auto _pinMiso>
struct spiPins{
  static constexpr auto portMosi = _portMosi;
  static constexpr auto pinMosi = _pinMosi;
  static constexpr auto portMiso = _portMiso;
  static constexpr auto pinMiso = _pinMiso;
};


И передавать ее в класс
Класс SPI
template<typename pins>
struct SPI{
  using power = Valuelist<
                          0,
                          pins::portMosi | pins::portMiso,
                          0>;

  using pin = Typelist<
                       Pin<pins::portMosi, pins::pinMosi, AF>, 
                       Pin<pins::portMiso, pins::pinMiso, AF>
                       >;
};


И, собственно, в пользовательском коде особо ничего не изменится:
int main(){
  using pins = spiPins<1,2,1,4>;
  using spi = SPI<pins>;
  
  Power<spi>::Enable();
  Pinlist<spi>::Init();
}

Да, только теперь у пользователя есть список с номерами портов(возможно ошибочный) из которого непонятно какие это пины, потому ему придется добавить соответствующий комментарий(возможно ошибочный), а сами пины инитить в другом месте, а тут можно перепутать как сами пины, так и режимы с AF. Я пошел другим путем, у меня можно сделать так:


using spi2 = Spi2<PA12, PC2, PC3, PB4>;
//using lcd = LcdSpi<ST7735, LcdOrient::LandscapeRev, spi2, PD10, PD9>;

using fmc = FmcBank1<8, PD7, PE6>;
using lcd = LcdFmc<RM68140, LcdOrient::LandscapeRev, fmc, PB13, 1, 1, 1, 3, 3, 3>;

using lcdDataPins = PinList<PE10, PE9, PE8, PE7, PD1, PD0, PD15, PD14>;
//using lcd = Lcd<ILI9481, LcdOrient::LandscapeRev, PB13, lcdDataPins, PD7, PE6, PD5, PD4, 0, 4, 0>;

using spi4 = Spi4<PE4, PE12, PE5, PE14>;
Touch<spi4, PB12, lcd> touch;

Тут пара SPI, в шаблон передаются именно пины, причем для каждого мк только из списка допустимых, если, допустим, SCK нельзя повесить на данный пин, то будет ошибка компиляции говорящая о том какой именно пин ошибочный, при этом для всех пинов автоматически добавляется номер AF. Также здесь есть объявление для трех разных дисплеев подключенных по SPI/FMC или GPIO(к пинам FMC). Помимо пинов для дисплеев также передаются тайминги, а инитятся все три простым вызовом init(), возможно с указанием скорости для SPI, после чего все дисплеи работают одинаково. Пины обрабатываются списками, сразу задаются режимы для всех пинов с одного порта, потому эффективность достаточно высокая Тактирование периферия включает для себя лично, тактирование для "расшаренных" портов или DMA включая я вот таким образом:


Periph::enableClock(Ahb4Periph::Gpios);
Periph::enableClock(Ahb1Periph::Dma1 | Ahb1Periph::Dma2);

Теперь представим что вместо половины моих пинов будут номера портов, а вся инициализация находится где-то еще, ее нужно написать, не ошибиться с пинами/AF/режимами и все это ради экономии самого дешевого ресурса — флеша. Для примеру беру реальный проект, далеко не самый простой, с -O2 размер бинарника 11496 байт, для -Os уже всего 8780, а у мк 512КБ флеша :)

Да, я о том же :)
Только я предлагаю вынести тактирование и пины за пределы периферии, чтобы код стал еще меньше.

Рассмотрим, для упрощения spi и пины, без дма и т.д.
В коде, пин выглядит примерно так:
Пин
template<auto _gpioBase, auto _pinNumber, auto _config = 0>
struct Pin{

  static constexpr auto gpioBase = _gpioBase;
  static constexpr auto pinNumber = _pinNumber;
  static constexpr auto config = _config;

  using power = Valuelist<
    0, // Первый регистр тактирования
    0, // Второй регистр тактирования
    gpioBase == GPIOA_BASE ? 2 : 0 // Выбираем соответствующий порту бит
    >;
  // Задаем новую функцию 
  template<auto configNew>
  using pinFunc = Pin<gpioBase, pinNumber, configNew>;

  // Что-то еще - инициализация, опрос, установка...
};
// И определены, допустим все так
...
using PA12 = Pin<GPIOA_BASE, 12>
...


Далее spi
SPI
template<typename MOSI, typename MISO, typename SCK>
struct SPI{
  using powerSPI = Valuelist<
    0, // Выбираем биты, соответствующие SPI
    16384,
    0
    >;

    // "складываем" значения пинов и собственно SPI
  using power = lists_termwise_or_t<
        typename MOSI::power, 
        typename MISO::power, 
        typename SCK::power, powerSPI>;

   // Задаем функцию пинов
  using pins = Typelist<
        typename MOSI::template pinFunc<AF>, 
        typename MISO::template pinFunc<AF>, 
        typename SCK:: template pinFunc<AF>>;

  //static_assert() - Производим всякие проверки, в т.ч. на корректность пинов 
};


И пинлист
Пинлист
template<typename... Peripherals>
struct Pinlist{

// приводим список <Typelist<Pin1, Pin2...>, ..., Typelist<Pin_n, Pin_m>>
// к виду Typelist<Pin1, Pin2, ..., Pin_n, Pin_m>
  using pins = lists_expand_t<typename Peripherals::pins...>

// Работаем со списком - проверяем уникальность, распределяем пины по портам
  void Init();
};


int main(){
  using spi2 = SPI<PA12, PC2, PC3>;
  using spi4 = SPI<PE4, PE12, PE5>;
 
   // соответственно происходит только reg1 |= GPIOA | GPIOC | GPIOE
  //                                   reg2 |= SPI2 | SPI4
  Power::Enable<spi2, spi4>(); 

// Происходит распределение пинов по всем портам
// В каждый порт будет происходить запись только 1 раз
// Условно 
//GPIOA_CONF = (GPIOA_CONF  & (~maskA)) | conf_Pin12;
//GPIOC_CONF = (GPIOC_CONF  & (~maskC)) | conf_Pin2 | conf_Pin3;
//GPIOE_CONF = (GPIOE_CONF  & (~maskE)) | conf_Pin4 | conf_Pin5 | conf_Pin12;
  Pinlist<spi2, spi4>::Init();

  spi2::Init();
  spi4::Init();

}

Пины, даже если их треть от общего числа используются, с большой вероятностью будут со всех портов, к тому же по моему примеру видно, что используются два SPI и FMC, но и дисплеи и тач задействуют еще вспомогательные пины, которые также нужно учитывать. В итоге проще не заморачиваться и включить тактирование портов самому… Аналогично если мк усыпляли и периферию отключали, то затем помимо включения периферии придется для нее вызывать init(), который может включать ее сам… Кода генерится будет немного больше, но так проще.

Если в варианте 2 снабдить функции префиксом inline, то размер полученного кода становится такой же, как и в первом варианте :).
Godbolt классный.
У меня не получилось, если речь идет про сравнение прямой записи в регистры и «функции инициализации» :) Ссылка
Во втором случае добавляется повторная модификация volatile регистров, что, естественно, увеличит размер листинга.
Да, Вы правы, я снял volatile и не заметил.
Я глубоко уважаю Александреску и его класс Loki (в первую очередь потому, что так и на смог до конца его прочувствовать), но, по моему, в применении к данной конкретной задаче написание кода в виде макросов выглядит понятнее, а ведь можно и condtexpr использовать. Наверное, я не умею готовить шаблоны, поэтому их и не люблю.

А с маппингом пинов все еще сложнее, я видел реализации (вроде как в embedded), где Вы можете замапировать интерфейс на любые ноги, но функция включения просто проверяет по внутренним таблицам, что вы назначили его именно на стандартные (единственно возможные для данного МК) ноги и дает ошибку в противном случае. Наверное, это опять таки для совместимости разных МК, хотя и выглядит странновато.
Уже и не помню, проверялось это на ассертах (еще куда ни шло) или в рантайме (это просто офигеть).
Да, спор на все времена — макросы vs шаблоны.
Возможно, конкретно в этом случае проще было сделать с использованием дефайнов, но все используемые метафункции уже были готовы для класса Pinlist, который значительно сложнее. Думаю, что его также эффективно повторить макросами не получится. Там есть и проверка на уникальность, возможность манипуляции одним пином, разделение списка пинов с обоих сторон и т.д.

Ремап также можно победить, если использовать static_assert времени компиляции. А условия для периферии и пинов придется прописывать либо с мануалом в руках, либо парсить документы производителя.
НЛО прилетело и опубликовало эту надпись здесь
Цель, как раз, была противоположна — максимально, по возможности, абстрагироваться от контроллера, его периферии и внешних устройств.

В контроллерах arm-cortex, прежде, чем использовать периферийный блок, необходимо произвести следующие действия:

  1. Включить тактирование
  2. Произвести настройку

В статье говорится о том, как эффективно и обобщенно реализовать пункт 1, независимо от контроллера.
Вместе с тем, совсем без каких-либо примеров, — текст был бы очень сложен для восприятия, поэтому введено уточнение: stm32f103c8, SPI и USART, при этом данные пересылаются с использованием DMA.

Для функционирования аппаратных интерфейсов требуется устройство ввода-вывода, т.е. пины. Эти пины могут относится к разным портам (Порт А, Порт В, ...), которые также требуется включить(подать тактирование).

Если немного перефразировать исходную задачу под Ваши условия, то:
Требуется выводить температуру от датчика, подключенного по UART, на семисегментный индикатор по SPI через сдвиговый регистр 74HC595.

Разбор этой задачи в статье закончился на этапе включении тактирования UART, SPI и сопутствующей периферии. Показаны различные способы это сделать, в том числе на C++.

Если вопрос именно в метапрограммировании, то вот несколько очень полезных ссылок: раз, два, три.
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
Да, в контроллерах ресурсы довольно ограничены и конечны, но 640 байт — это действительно мало. Сейчас есть экземпляры с 1мб+.

Я правильно улавливаю мысль?


Если я правильно понял, то да, можно так. Создать массив -> передать его адрес функции для приема данных с датчика -> обработать данные, если нужно -> передать адрес для функции вывода на дисплей.
НЛО прилетело и опубликовало эту надпись здесь

Если использовать, обертку над регистрами, то можно сделать более универсальный код — не привязанный к периферии типа RCC->APB2ENR


template<typename... Peripherals>
  static void Enable(){
      // Для всех параметров пакета будет применена операция | 
      // В нашем случае value = uart::valueAHBENR | spi::valueAHBENR и т.д.
    if constexpr (constexpr auto value = (Peripherals::valueAHBENR | ... ); value)
      RCC->AHBENR |= value;
    if constexpr (constexpr auto value = (Peripherals::valueAPB2ENR | ... ); value)
      RCC->APB2ENR |= value;
    if constexpr (constexpr auto value = (Peripherals::valueAPB1ENR | ... ); value)
      RCC->APB1ENR |= value;
  };
};

можно было бы заменить на


template<typename RccRegs, typename ... PeripheralsValues>
struct RccEnableHelper
{
   void Enable ()
   {
       constexpr auto value = (Peripherals::value | ... );
      RccReg::Write<value>();
   }
};

typename<typename ...Rcc>
struct Power
{
   static void Enable()
  {
      (Rcc::Enable(), ...);
   };
};

using AHB1ENRHelper = RccEnableHelper<RCC::AHB1ENR, uart::valueAHBENR, spi::valueAHBENR>;
using APB1ENRHelper = RccEnableHelper<RCC::APB1ENR,uart::valueAPB1ENR,spi::valueAPB1ENR>;
using AppPower = Power<AHB1ENRHelper,APB1ENRHelper>;

int main()
{
   AppPower::Enable();
}

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


А вообще конечно, тактирование, если его не надо выключать, лучше один раз включить где-нибудь при стартапе и все… А вот если надо его отключать (только не пойму зачем), то это имеет смысл. В любом случае вы все равно, явно указываете какие биты надо поставить. И если spi или uart будут на других шинах, придется классы переписывать.


Кроме того, в таком случае, когда установка тактирования периферии вынесена отдельно, можно действительно сделать установку за один такт для одной шины. Т.е. вместо |= писать на прямую значение. Типа такого сделать:


RCC::APB1ENRPack<
                  RCC::APB1ENR::TIM5EN::Enable, 
                  RCC::APB1ENR::TIM2EN::Enable, 
                  RCC::APB1ENR::SPI2EN::Enable, 
                  RCC::APB1ENR::USART2EN::Enable
                 >::Write();

а если это обозвать как-то псевдонимом


using Apb1Enabler = RCC::APB1ENRPack<
                        RCC::APB1ENR::TIM5EN::Enable,
                        RCC::APB1ENR::TIM2EN::Enable,
                        RCC::APB1ENR::SPI2EN::Enable,
                        RCC::APB1ENR::USART2EN::Enable>;

То потом можно вызывать просто:


Apb1Enabler::Write(); 

И вот тут действительно одна ассемблерная команда....

Да, все было примерно так, как Вы описываете. Пока, действительно, в одном случае стало необходимо что-то выключать, что-то оставлять(энергосбережение), в другом — просто нехватка памяти. А, поскольку, класс Pinlist был сделан, то от него до этого кода совсем чуть-чуть :)

По поводу других битов — все ведь известно на этапе, поэтому можно сделать описание периферии один раз и забыть.

Условный пример:
  // Вариантов ремапа может быть много, но это все также можно отобразить в коде
template<auto identifier, bool isDMA, bool isRemap>
class SPI{
   using power = Valuelist<
       isDMA ?  DMAEN : 0, // Значение для 1 регистра тактирования
       identifier == 1 ? SPI1EN : identifier == 2 ? SPI2EN : SPI3EN, // Для 2 регистра
       isRemap ? GPIOAEN : GPIOBEN // Для 3 регистра
>
};


Это можно немного автоматизировать, написав парсер(по крайней мере с stm), который сам будет дописывать это свойство в периферию.

И пример, с допущением, что описана вся периферия в таком стиле:
Малопотребляющее устройство, в котором UART используется только для выхода из сна, в остальное время он отключен

using listInit = powerlist<spi1, led1, led2, btn1, btn2>;
using listWake = powerlist<uart>;

Power::Enable<listInit >();

// User Code

// Произойдет только включение uart, 
//Порт для TX и RX останется включенным, если был до этого включен, в противном - включится
// Так же и для DMA, если оно нужно
Power::Keep<listWake, listInit>();
Sleep();
// Произойдет обратная ситуация - отключится только uart и его пины
// если они не задействованы в другой периферии.
Power::Keep<listInit, listWake>();


При этом, неважно какой задействован uart, пины и т.д. Все решится само на этапе компиляции. Можно даже сменить uart на просто кнопку. В самом пользовательском коде ничего не поменяется.
То потом можно вызывать просто:
Apb1Enabler::Write();

И вот тут действительно одна ассемблерная команда…


Здесь тоже должно быть соблюдено довольно много условий, чтобы действительно была одна команда :)
  1. Должна происходить только запись, без чтения
  2. В одном регистре общего назначения должно содержатся значение для записи, которое туда попало в результате предыдущих операций, и которое подходит для текущей, что маловероятно
  3. В другом регистре общего назначения должен быть адрес целевого регистра, причем с максимальным оффсетом 2 в 12 степени


Если, например:
Apb1Enabler::Write();
// Значение для записи в APB2 совпадает c APB1
// и APB2 смещен на +4, относительно APB1
Apb2Enabler::Write();


То, действительно, в APB2ENR запись произойдет в 1 инструкцию
  movs    r3, value
  ldr     r2, .address
  str     r3, [r2]
  str     r3, [r2, #4]

1) Да если известны вся периферия, которая должна быть включена, то Write без чтения. Для установки, через чтение другой метод Set(), но его можно не использоваться. Например, например, вначале включили всю периферию, потом когда-то надо отключить UART.


using Apb1Enabler = RCC::APB1ENRPack<
                        RCC::APB1ENR::TIM5EN::Enable,
                        RCC::APB1ENR::TIM2EN::Enable,
                        RCC::APB1ENR::SPI2EN::Enable,
                        RCC::APB1ENR::USART2EN::Enable>;

using UartClockDisabler = RCC::APB1ENRPack<
                        RCC::APB1ENR::TIM5EN::Enable,
                        RCC::APB1ENR::TIM2EN::Enable,
                        RCC::APB1ENR::SPI2EN::Enable,
                        RCC::APB1ENR::USART2EN::Disable
>;

то в начале, что-то типа


Apb1Enabler::Write();

потом когда надо отключить UART


UartClockDisabler::Write() ; 

2) Да именно — это пример под пунктом 1 и показывает, так как вы на этапе компиляции знаете, что и когда у вас надо подключать и отключать — это можно разную конфигурацию задать для разных случаев.
Т.е. вам из вне от пользователя не приходит же значение периферии, которую надо отключить верно я понимаю? В таком случае, просто делаете псевдонимы для каждого случая, можно их потом в список запихать и даже использовать, что- типа стейт машины, если известен алгоритм, а можно просто по месту использовать где надо.


3) Да..

Согласен, что с регистрами тактирования, в принципе, можно использовать только запись. Однако, некоторые из них содержат дефолтные значение, но и это, естественно, можно учесть.
Такой глупый вопрос: имена типа RCC->AHBENR это соглашение, зафиксированное в том же cmsis, или внутреннее соглашение stm, которое в принципе может быть изменено?

Да для одной серии микро, но на разных сериях микроконтроллеров названия могут быть другие, даже STM могут быть разные, например, RCC->AHB1ENR, вместо RCC->AHBENR.

Еще такое замечание


template<typename EnableList, typename DisableList>
  static void _Set(){
    // Вызов метода класса, осуществляющий запись в регистры
    HPower:: template ModifyRegisters<EnableList, DisableList, AddressesList>();
  }

Метод _Set торчит наружу, его надо сделать приватным, а адаптер сделать friendом.
HPower — Сильная связанность. Лучше сделать Power шаблонным и HPower передать в шаблон Power. Тогда просто появится интерфейс для работы с регистрами. А как вы там его назовете и сделаете — не важно уже, главное, чтобы у него был метод ModifyRegisters

Это в тексте статьи сделано такое упрощение, в полном коде, в конце, это все учтено :)

class HPower{

protected:

  template<typename SetList, typename ResetList, typename AddressesList>
  static void ModifyRegisters();
}

/**/

class Power: public interfaces::IPower<Power>, public hardware::HPower{
...
friend class IPower<Power>;
  ...
}

А зачем тогда метод от HPower протектед? Может агрегацию использовать, вместо наследования? Хотя Ок — норм, тогда.

Полностью оградить пользователя от взаимодействия с регистрами :)
Только разрешенные методы.
GCC 10.1.1
А какой именно компилятор?
arm-none-eabi-gcc 10.1.1 20200529 (release)
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории