Включаем периферию контроллера за 1 такт или магия 500 строк кода



Как часто, при разработке прошивки для микроконтроллера, во время отладки, когда байтики не бегают по UART, вы восклицаете: «Ааа, точно! Не включил тактирование!». Или, при смене ножки светодиода, забывали «подать питание» на новый порт? Думаю, что довольно часто. Я, по крайней мере, — уж точно.

На первый взгляд может показаться, что управление тактированием периферии тривиально: записал 1 — включил, 0 — выключил.

Но «просто», — не всегда оказывается эффективно…

Постановка задачи


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

  • Во встраиваемых системах, один из самых главных критериев — это минимально-возможный результирующий код, исполняемый за минимальное время
  • Легкая масштабируемость. Добавление или изменение в проекте какой-либо периферии не должно сопровождаться code review всех исходников, чтобы удалить строчки включения/отключения тактирования
  • Пользователь должен быть лишен возможности совершить ошибку, либо, по крайней мере эта возможность должна быть сведена к минимуму
  • Нет необходимости работы с отдельными битами и регистрами
  • Удобство и однообразность использования независимо от микроконтроллера
  • Помимо основных возможностей включения и выключения тактирования периферии необходим расширенный функционал (о нем речь пойдет далее)

После выяснения критериев оценки, поставим конкретную задачу, попутно определив условия и «окружение» для реализации:

Компилятор: GCC 10.1.1 + Make
Язык: C++17
Среда: Visual Studio Code
Контроллер: stm32f103c8t6 (cortex-m3)
Задача: включение тактирования SPI2, USART1 (оба интерфейса с использованием DMA)

Выбор данного контроллера обусловлен, естественно, его распространённостью, особенно, благодаря одному из китайских народных промыслов – производству плат Blue Pill.



С точки зрения идеологии, совершенно неважно, какой именно контроллер выбран: stmf1, stmf4 или lpc, т.к. работа с системой тактирования периферии сводится лишь к записи в определенный бит либо 0 для выключения, либо 1 для включения.

В stm32f103c8t6 имеется 3 регистра, которые ответственны за включение тактирования периферии: AHBENR, APB1ENR, APB2ENR.

Аппаратные интерфейсы передачи данных SPI2 и USART1 выбраны неслучайно, потому что для их полноценного функционирования необходимо включить биты тактирования, расположенные во всех перечисленных регистрах – биты самих интерфейсов, DMA1, а также биты портов ввода-вывода (GPIOB для SPI2 и GPIOA для USART1).




Следует отметить, что для оптимальной работы с тактированием, необходимо учитывать – AHBENR содержит разделяемый ресурс, используемые для функционирования как SPI2, так и USART1. То есть, отключение DMA сразу приведет к неработоспособности обоих интерфейсов, вместе с тем, КПД повторного включения будет даже не нулевым, а отрицательным, ведь эта операция займет память программ и приведет к дополнительному расходу тактов на чтение-модификацию-запись volatile регистра.

Разобравшись с целями, условиями и особенностями задачи, перейдем к поиску решений.

Основные подходы


В этом разделе собраны типовые способы включения тактирования периферии, которые мне встречались и, наверняка, Вы их также видели и/или используете. От более простых, — реализуемых на C, до fold expression из C++17. Рассмотрены присущие им достоинства и недостатки.

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

Прямая запись в регистры


Классический способ, «доступный из коробки» и для С и для C++. Вендор, чаще всего, представляет заголовочные файлы для контроллера, в которых задефайнены все регистры и их биты, что дает возможность сразу начать работу с периферией:

int main(){
  RCC->AHBENR  |= RCC_AHBENR_DMA1EN;
  RCC->APB2ENR |= RCC_APB2ENR_IOPAEN
               |  RCC_APB2ENR_IOPBEN
               |  RCC_APB2ENR_USART1EN;
  RCC->APB2ENR |= RCC_APB1ENR_SPI2EN;
…
}

Листинг
    // AHBENR(Включение DMA1)
  ldr     r3, .L3
  ldr     r2, [r3, #20]
  orr     r2, r2, #1
  str     r2, [r3, #20]
    // APB2ENR(Включение GPIOA, GPIOB, USART1)
  ldr     r2, [r3, #24]
  orr     r2, r2, #16384
  orr     r2, r2, #12
  str     r2, [r3, #24]
    // APB1ENR(Включение SPI2)
  ldr     r2, [r3, #28]
  orr     r2, r2, #16384
  str     r2, [r3, #28]


Размер кода: 36 байт. Посмотреть

Плюсы:

  • Минимальный размер кода и скорость выполнения
  • Самый простой и очевидный способ

Минусы:

  • Необходимо помнить и названия регистров и названия битов, либо постоянно обращаться к мануалу
  • Легко допустить ошибку в коде. Читатель, наверняка, заметил, что вместо SPI2 был повторно включен USART1
  • Для работы некоторых периферийных блоков требуется также включать другую периферию, например, GPIO и DMA для интерфейсов
  • Полное отсутствие переносимости. При выборе другого контроллера этот код теряет смысл

При всех недостатках, этот способ остается весьма востребованным, по крайней мере тогда, когда нужно «пощупать» новый контроллер, написав очередной «Hello, World!» мигнув светодиодом.

Функции инициализации


Давайте попробуем абстрагироваться и спрятать работу с регистрами от пользователя. И в этом нам поможет обыкновенная C-функция:

void UART1_Init(){
 RCC->AHBENR  |= RCC_AHBENR_DMA1EN;
 RCC->APB2ENR |= RCC_APB2ENR_IOPAEN
              |  RCC_APB2ENR_USART1EN;
  // Остальная инициализация
}

void SPI2_Init(){
 RCC->AHBENR  |= RCC_AHBENR_DMA1EN;
 RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
 RCC->APB1ENR |= RCC_APB1ENR_SPI2EN;
  // Остальная инициализация
}

int main(){
  UART1_Init();
  SPI2_Init();
…
}

Размер кода: 72 байта. Посмотреть

Листинг
UART1_Init():
    // AHBENR(Включение DMA1)
  ldr     r2, .L2
  ldr     r3, [r2, #20]
  orr     r3, r3, #1
  str     r3, [r2, #20]
    // APB2ENR(Включение GPIOA, USART1)
  ldr     r3, [r2, #24]
  orr     r3, r3, #16384
  orr     r3, r3, #4
  str     r3, [r2, #24]
  bx      lr
SPI2_Init():
    //Повторно (!) AHBENR(Включение DMA1)
  ldr     r3, .L5
  ldr     r2, [r3, #20]
  orr     r2, r2, #1
  str     r2, [r3, #20]
    //Повторно (!) APB2ENR(Включение GPIOB)
  ldr     r2, [r3, #24]
  orr     r2, r2, #8
  str     r2, [r3, #24]
    //Запись в APB1ENR(Включение SPI2)
  ldr     r2, [r3, #28]
  orr     r2, r2, #16384
  str     r2, [r3, #28]
  bx      lr
main:
   push    {r3, lr}
   bl      UART1_Init()
   bl      SPI2_Init()


Плюсы:

  • Можно не заглядывать в мануал по каждому поводу
  • Ошибки локализованы на этапе написания драйвера периферии
  • Пользовательский код легко воспринимать

Минусы:

  • Количество необходимых инструкций возросло кратно количеству задействованной периферии
  • Очень много дублирования кода — для каждого номера UART и SPI он будет фактически идентичен

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

Функция включения тактирования


Обернем модификацию регистров тактирования в отдельную функцию, предполагая, что это снизит количество необходимой памяти. Вместе с этим, введем параметр-идентификатор для периферии – для уменьшения кода драйверов:

void PowerEnable(uint32_t ahb, uint32_t apb2, uint32_t apb1){
    RCC->AHBENR  |= ahb;
    RCC->APB2ENR |= apb2;
    RCC->APB1ENR |= apb1;
}

void UART_Init(int identifier){
    uint32_t ahb = RCC_AHBENR_DMA1EN, apb1 = 0U, apb2 = 0U;
    if (identifier == 1){
      apb2 = RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN;
    } 
    else if (identifier == 2){…}
    PowerEnable(ahb, apb2, apb1);
  // Остальная инициализация
}

void SPI_Init(int identifier){
    uint32_t ahb = RCC_AHBENR_DMA1EN, apb1 = 0U, apb2 = 0U;
    if (identifier == 1){…} 
    else if (identifier == 2){
      apb2 = RCC_APB2ENR_IOPBEN;
      apb1 = RCC_APB1ENR_SPI2EN;
    }
    PowerEnable(ahb, apb2, apb1);
  // Остальная инициализация
}

int main(){
  UART_Init(1);
  SPI_Init(2);
…
}

Размер кода: 92 байта. Посмотреть

Листинг
PowerEnable(unsigned long, unsigned long, unsigned long):
  push    {r4}
  ldr     r3, .L3
  ldr     r4, [r3, #20]
  orrs    r4, r4, r0
  str     r4, [r3, #20]
  ldr     r0, [r3, #24]
  orrs    r0, r0, r1
  str     r0, [r3, #24]
  ldr     r1, [r3, #28]
  orrs    r1, r1, r2
  str     r1, [r3, #28]
  pop     {r4}
  bx      lr
UART_Init(int):
  push    {r3, lr}
  cmp     r0, #1
  mov     r2, #0
  movw    r1, #16388
  it      ne
  movne   r1, r2
  movs    r0, #1
  bl      PowerEnable(unsigned long, unsigned long, unsigned long)
  pop     {r3, pc}
SPI_Init(int):
  push    {r3, lr}
  cmp     r0, #2
  ittee   eq
  moveq   r1, #8
  moveq   r1, #16384
  movne   r1, #0
  movne   r2, r1
  movs    r0, #1
  bl      PowerEnable(unsigned long, unsigned long, unsigned long)
  pop     {r3, pc}
main:
   push    {r3, lr}
   movs    r0, #1
   bl      UART_Init(int)
   movs    r0, #2
   bl      SPI_Init(int)


Плюсы:
  • Удалось сократить код описания драйверов микроконтроллера
  • Результирующее количество инструкций сократилось*

Минусы:

  • Увеличилось время выполнения

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

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

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

Свойства-значения и шаблоны


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

Поскольку на этапе компиляции нам известны все значения, которые нужно записать в регистры, то избавимся от операций со стеком. Для этого создадим отдельный класс с шаблонным методом, а классы периферии наделим свойствами (value trait), которые будут хранить значения для соответствующих регистров.

struct Power{
template< uint32_t valueAHBENR, uint32_t valueAPB2ENR, uint32_t valueAPB1ENR>
    static void Enable(){
// Если значение = 0, то в результирующем коде операций с регистром не будет
        if constexpr (valueAHBENR)
            RCC->AHBENR |= valueAHBENR;
        if constexpr (valueAPB2ENR)
            RCC->APB2ENR |= valueAPB2ENR;
        if constexpr (valueAPB1ENR)
            RCC->APB1ENR |= valueAPB1ENR;
    };

};

template<auto identifier>
struct UART{
// С помощью identifier на этапе компиляции можно выбрать значения для периферии
  static constexpr auto valueAHBENR = RCC_AHBENR_DMA1EN;
  static constexpr auto valueAPB1ENR = identifier == 1 ? 0U : RCC_APB1ENR_USART2EN;
  static constexpr auto valueAPB2ENR = RCC_APB2ENR_IOPAEN
                                    |  (identifier == 1 ? RCC_APB2ENR_USART1EN : 0U);
    // Остальная реализация
};

template<auto identifier>
struct SPI{
  static constexpr auto valueAHBENR = RCC_AHBENR_DMA1EN;
  static constexpr auto valueAPB1ENR = identifier == 1 ? 0U : RCC_APB1ENR_SPI2EN;
  static constexpr auto valueAPB2ENR = RCC_APB2ENR_IOPBEN
                                    |  (identifier == 1 ? RCC_APB2ENR_SPI1EN : 0U);
    // Остальная реализация
};

int main(){
    // Необязательные псевдонимы для используемой периферии
  using uart = UART<1>;
  using spi = SPI<2>;

  Power::Enable<
                uart::valueAHBENR  | spi::valueAHBENR,
                uart::valueAPB2ENR | spi::valueAPB2ENR,
                uart::valueAPB1ENR | spi::valueAPB1ENR
                >();
…
}

Размер кода: 36 байт. Посмотреть

Листинг
main:
    // AHBENR(Включение DMA1)
  ldr     r3, .L3
  ldr     r2, [r3, #20]
  orr     r2, r2, #1
  str     r2, [r3, #20]
    // APB2ENR(Включение GPIOA, GPIOB, USART1)
  ldr     r2, [r3, #24]
  orr     r2, r2, #16384
  orr     r2, r2, #12
  str     r2, [r3, #24]
    // APB1ENR(Включение SPI2)
  ldr     r2, [r3, #28]
  orr     r2, r2, #16384
  str     r2, [r3, #28]


Плюсы:

  • Размер и время выполнения получились такими же, как и в эталонном варианте с прямой записью в регистры
  • Довольно просто масштабировать проект – достаточно добавить воды соответствующее свойство-значение периферии

Минусы:

  • Можно совершить ошибку, поставив свойство-значение не в тот параметр
  • Как и в случае с прямой записью в регистры – страдает переносимость
  • «Перегруженность» конструкции

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

Идеальный вариант… почти


Чтобы уменьшить количество пользовательского кода и возможностей для ошибок воспользуемся parameter pack, который уберет обращение к свойствам классов периферии в пользовательском коде. При этом изменится только метод включения тактирования:

struct Power{
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;
  };
};
    …
int main(){
    // Необязательные псевдонимы для используемой периферии
  using uart = UART<1>;
  using spi = SPI<2>;

  Power::Enable<uart, spi>();
…
}

Размер кода: 36 байт. Посмотреть

Листинг
main:
    // AHBENR(Включение DMA1)
  ldr     r3, .L3
  ldr     r2, [r3, #20]
  orr     r2, r2, #1
  str     r2, [r3, #20]
    // APB2ENR(Включение GPIOA, GPIOB, USART1)
  ldr     r2, [r3, #24]
  orr     r2, r2, #16384
  orr     r2, r2, #12
  str     r2, [r3, #24]
    // APB1ENR(Включение SPI2)
  ldr     r2, [r3, #28]
  orr     r2, r2, #16384
  str     r2, [r3, #28]


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

И, вроде бы, можно на этом остановиться, но…


Расширяем функционал


Обратимся к одной из поставленных целей:
Помимо основных возможностей включения и выключения тактирования периферии необходим расширенный функционал

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

В контексте условий, озвученных в начале статьи, будем считать, что генератором события пробуждения будет USART1, а SPI2 и соответствующий ему порт GPIOB — необходимо отключать. При этом, общиий ресурс DMA1 должен оставаться включенным.

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

int main(){
  using uart = UART<1>;
  using spi = SPI<2>;
…
    // Включаем USART, SPI, DMA, GPIOA, GPIOB
  Power::Enable<uart, spi>();

    // Some code

    // Выключаем SPI и GPIOB вместе (!) с DMA
  Power::Disable<spi>();
    
    // Включаем обратно DMA вместе(!) с USART и GPIOA
  Power::Enable<uart>();
    
    // Sleep();

    // Включаем SPI и GPIOB вместе(!) с DMA
  Power::Enable<spi>();
…
}

Размер кода: 100 байт. Посмотреть

Листинг
main:
        // AHBENR(Включение DMA1)
        ldr     r3, .L3
        ldr     r2, [r3, #20]
        orr     r2, r2, #1
        str     r2, [r3, #20]
       // APB2ENR(Включение GPIOA, GPIOB, USART1)
        ldr     r2, [r3, #24]
        orr     r2, r2, #16384
        orr     r2, r2, #12
        str     r2, [r3, #24]
       // APB1ENR(Включение SPI2)
        ldr     r2, [r3, #28]
        orr     r2, r2, #16384
        str     r2, [r3, #28]
        // Выключение SPI2
       // AHBENR(Выключение DMA1)
        ldr     r2, [r3, #20]
        bic     r2, r2, #1
        str     r2, [r3, #20]
       // APB2ENR(Выключение GPIOB)
        ldr     r2, [r3, #24]
        bic     r2, r2, #8
        str     r2, [r3, #24]
       // APB1ENR(Выключение SPI2)
        ldr     r2, [r3, #28]
        bic     r2, r2, #16384
        str     r2, [r3, #28]
        // Повторное (!) включение USART1
        // AHBENR(Включение DMA1)
        ldr     r2, [r3, #20]
        orr     r2, r2, #1
        str     r2, [r3, #20]
       // APB2ENR(Включение GPIOA, USART1)
        ldr     r2, [r3, #24]
        orr     r2, r2, #16384
        orr     r2, r2, #4
        str     r2, [r3, #24]
        // Sleep();
        // AHBENR(Включение DMA1)
        ldr     r2, [r3, #20]
        orr     r2, r2, #1
        str     r2, [r3, #20]
       // APB2ENR(Включение GPIOB)
        ldr     r2, [r3, #24]
        orr     r2, r2, #8
        str     r2, [r3, #24]
       // APB1ENR(Включение SPI2)
        ldr     r2, [r3, #28]
        orr     r2, r2, #16384
        str     r2, [r3, #28]



В это же время эталонный код на регистрах занял 68 байт. Посмотреть

Очевидно, что для подобных задач, камнем преткновения станут разделяемые ресурсы, такие как DMA. Кроме того, конкретно в этом случае возникнет момент, когда оба интерфейса станут неработоспособными и, фактически, произойдет аварийная ситуация.

Давайте попробуем найти решение…

Структура


Для упрощения понимания и разработки — изобразим общую структуру тактирования, какой мы ее хотим видеть:



Она состоит всего из четырех блоков:

Независимые:

  • IPower – интерфейс взаимодействия с пользователем, подготавливающий данные для записи в регистры
  • Hardware– запись значений в регистры контроллера

Аппаратно-зависимые:
  • Peripherals – периферия, которая используется в проекте и сообщает интерфейсу, какие устройства надо включить или выключить
  • Adapter – передает значения для записи в Hardware, указывая в какие именно регистры их следует записать

Интерфейс IPower


С учетом всех требований, определим методы, необходимые в интерфейсе:

template<typename… Peripherals>
Enable();

template<typename EnableList, typename ExceptList>
EnableExcept();

template<typename EnableList, typename DisableList>
Keep();

Enable — включение периферии, указанной в параметре шаблона.

EnableExcept – включение периферии, указанной в параметре EnableList, за исключением той, что указана в ExceptList.

Пояснение
Таблица истинности
Бит включения Бит исключения Результат включение Результат выключение
0 0 0 0
0 1 0 0
1 0 1 0
1 1 0 0

Например, вызов:
EnableExcept<spi, uart>();

должен установить бит SPI2EN и бит IOPBEN. В то время, как общий DMA1EN, а также USART1EN и IOPAEN останутся в исходном состоянии.

Чтобы получить соответствующую таблицу истинности, необходимо произвести следующие операции:

resultEnable = (enable ^ except) & enable


К ним в дополнение также идут комплементарные методы Disable, выполняющие противоположные действия.

Keep – включение периферии из EnableList, выключение периферии из DisableList, при этом, если периферия присутствует в обоих списках, то она сохраняет свое состояние.

Пояснение
Таблица истинности
Бит включения Бит выключения Результат включение Результат выключение
0 0 0 0
0 1 0 1
1 0 1 0
1 1 0 0

Например, при вызове:
Keep<spi, uart>();

установятся SPI2EN и IOPBEN, при этом USART1EN и IOPAEN сбросятся, а DMA1EN останется неизменным.

Чтобы получить соответствующую таблицу истинности, необходимо произвести следующие операции:

resultEnable = (enable ^ disable) & enable
resultDisable = (enable ^ disable) & disable


Методы включения/выключения уже реализованы довольно неплохо с помощью fold expression, но как быть с остальными?

Если ограничиться использованием 2 видов периферии, как это сделано в пояснении, то никаких сложностей не возникнет. Однако, когда в проекте используется много различных периферийных устройств, появляется проблема – в шаблоне нельзя явно использовать более одного parameter pack, т.к. компилятор не сможет определить где заканчивается один и начинается второй:

template<typename… EnableList, typename… ExceptList>
EnableExcept(){…};
  // Невозможно определить где заканчивается EnableList и начинается ExceptList
EnableExcept<spi2, pin3, uart1, pin1, i2c3>();

Можно было бы создать отдельный класс-обертку для периферии и передавать его в метод:

template<typename… Peripherals>
PowerWrap{
  static constexpr auto valueAHBENR = (Peripherals::valueAHBENR | …);
  static constexpr auto valueAPB1ENR = (Peripherals:: valueAPB1ENR | …);
  static constexpr auto valueAPB2ENR = (Peripherals:: valueAPB2ENR | …);
};

using EnableList = PowerWrap<spi2, uart1>;
using ExceptList = PowerWrap<pin1, i2c1>;

EnableExcept<EnableList, ExceptList>();

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

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

Метапрограммирование


Из-за того, что в основе метапрограммирования лежит работа не с обычными типами, а с их списками, то определим две сущности, которые будут оперировать типовыми и нетиповыми параметрами:

template<typename... Types>
struct Typelist{};

template<auto... Values>
struct Valuelist{};
…
using listT = Typelist<char, int> ;// Список из последовательности типов char и int
…
using listV = Valuelist<8,9,5,11> ;// Список из 4 нетиповых параметров

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

1. Извлечение первого элемента из списка

front
  // Прототип функции
template<typename List>
struct front;

  // Специализация для списка типов
  // Разделение списка в пакете параметров на заглавный и оставшиеся
template<typename Head, typename... Tail>
struct front<Typelist<Head, Tail...>>{ 
    // Возвращение заглавного типа
  using type = Head; 
};

 // Специализация для списка нетиповых параметров
template<auto Head, auto... Tail>
struct front<Valuelist<Head, Tail...>> {
  // Возвращение заглавного значения
  static constexpr auto value = Head;
};

  // Псевдонимы для простоты использования
template<typename List>
using front_t = typename front<List>::type;

template<typename List>
static constexpr auto front_v = front<List>::value;

  // Примеры
using listT = Typelist<char, bool, int>;
using type = front_t<listT>; // type = char

using listV = Valuelist<9,8,7>;
constexpr auto value = front_v<listV>; //value = 9


2. Удаление первого элемента из списка

pop_front
template<typename List>
struct pop_front;

  // Специализация для списка типов
  // Разделение списка в пакете параметров на заглавный и оставшиеся
template<typename Head, typename... Tail>
struct pop_front<Typelist<Head, Tail...>> {
  // Возвращение списка, содержащего оставшиеся типы
  using type = Typelist<Tail...>;
};

template<auto Head, auto... Tail>
struct pop_front<Valuelist<Head, Tail...>> {
  using type = Valuelist<Tail...>;
};

template<typename List>
using pop_front_t = typename pop_front<List>::type;

 // Примеры
using listT = Typelist<char, bool, int>;
using typeT = pop_front_t<listT>; // type = Typelist<bool, int>

using listV = Valuelist<9,8,7>;
using typeV = pop_front_t<listV>; // type = Valuelist<8,7>


3. Добавление элемента в начало списка
push_front
template<typename List, typename NewElement>
struct push_front;

template<typename... List, typename NewElement>
struct push_front<Typelist<List...>, NewElement> {
  using type = Typelist<NewElement, List...>;
};

template<typename List, typename NewElement>
using push_front_t = typename push_front<List, NewElement>::type;

  // Пример
using listT = Typelist<char, bool, int>;
using typeT = push_front_t<listT, long >; // type = Typelist<long, char, bool, int>



4. Добавление нетипового параметра в конец списка

push_back_value
template<typename List, auto NewElement>
struct push_back;

template<auto... List, auto NewElement>
struct push_back<Valuelist<List...>, NewElement>{
  using type = Valuelist<List..., NewElement>;
};

template<typename List, auto NewElement>
using push_back_t = typename push_back<List, NewElement>::type;

  // Пример
using listV = Valuelist<9,8,7>;
using typeV = push_back_t<listV, 6>; // typeV = Valuelist<9,8,7,6>



5. Проверка списка на пустоту

is_empty
template<typename List>
struct is_empty{
    static constexpr auto value = false;
};

 // Специализация для базового случая, когда список пуст
template<>
struct is_empty<Typelist<>>{
    static constexpr auto value = true;
};

template<typename List>
static constexpr auto is_empty_v = is_empty<List>::value;

 // Пример
using listT = Typelist<char, bool, int>;
constexpr auto value = is_empty_v<listT>; // value = false


6. Нахождение количества элементов в списке

size_of_list
  // Функция рекурсивно извлекает по одному элементу из списка,
  // инкрементируя счетчик count, пока не дойдет до одного из 2 базовых случаев
template<typename List, std::size_t count = 0>
struct size_of_list : public size_of_list<pop_front_t<List>, count + 1>{};

  // Базовый случай для пустого списка типов
template<std::size_t count>
struct size_of_list<Typelist<>, count>{
  static constexpr std::size_t value = count;
};

  // Базовый случай для пустого списка нетиповых параметров 
template<std::size_t count>
struct size_of_list<Valuelist<>, count>{
  static constexpr std::size_t value = count;
};

template<typename List>
static constexpr std::size_t size_of_list_v = size_of_list<List>::value;

  // Пример
using listT = Typelist<char, bool, int>;
constexpr auto value = size_of_list_v <listT>; // value = 3


Теперь, когда определены все базовые действия, можно переходить к написанию метафункций для битовых операций: or, and, xor, необходимых для методов интерфейса.

Поскольку эти битовые преобразования однотипны, то попытаемся сделать реализацию максимально обобщенно, чтобы избежать дублирования кода.

Функция, выполняющая абстрактную операцию над списком

lists_operation
template<template<typename first, typename second> class operation,
         typename Lists, bool isEnd = size_of_list_v<Lists> == 1>
class lists_operation{

  using first = front_t<Lists>; // (3)
  using second = front_t<pop_front_t<Lists>>; // (4)
  using next = pop_front_t<pop_front_t<Lists>>; // (5)
  using result = operation<first, second>; // (6)

public:

  using type = typename 
      lists_operation<operation, push_front_t<next, result>>::type; // (7)

};

template<template<typename first, typename second> class operation, typename List>
class lists_operation<operation, List, true>{ // (1)
public:
  using type = front_t<List>; // (2)
};

Lists – список, состоящий из типов или списков, над которым необходимо провести некоторое действие.
operation – функциональный адаптер, который принимает 2 первых элемента Lists и возвращает результирующий тип после операции.
isEnd – граничное условие метафункции, которое проверяет количество типов в Lists.

В базовом случае (1) Lists состоит из 1 элемента, поэтому результатом работы функции станет его извлечение(2).

Для остальных случаев – определяют первый (3) и второй (4) элементы из Lists, к которым применяется операция (6). Для получения результирующего типа (7) происходит рекурсивный вызов метафункции с новым списком типов, на первом месте которого стоит (6), за которым следуют оставшиеся типы (5) исходного Lists. Окончанием рекурсии становиться вызов специализации (1).

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

valuelists_operation
template<template <auto value1, auto value2> typename operation, 
         typename List1, typename List2, typename Result = Valuelist<>>
struct operation_2_termwise_valuelists{
  constexpr static auto newValue = 
      operation<front_v<List1>, front_v<List2>>::value; // (2)
  
  using nextList1 = pop_front_t<List1>;
  using nextList2 = pop_front_t<List2>;
    
  using result = push_back_value_t<Result, newValue>; // (3)
  using type = typename 
      operation_2_termwise_valuelists <operation, nextList1, nextList2, result>::type; // (4)
};

template<template <auto value1, auto value2> typename operation, typename Result>
struct operation_2_termwise_valuelists <operation, Valuelist<>, Valuelist<>, Result>{ // (1)
  using type = Result;
};

List1 и List2 – списки нетиповых параметров, над которыми необходимо произвести действие.
operation – операция, производимая над нетиповыми параметрами.
Result – тип, используемый для накопления промежуточных результатов.

Базовый случай (1), когда оба списка пусты, возвращает Result.

Для остальных случаев происходит вычисление значения операции (2) и занесение его в результирующий список Result (3). Далее рекурсивно вызывается метафункция (4) до того момента, пока оба списка не станут пустыми.

Функции битовых операций:

bitwise_operation
template<auto value1, auto value2>
struct and_operation{ static constexpr auto value = value1 & value2;};

template<auto value1, auto value2>
struct or_operation{ static constexpr auto value = value1 | value2;};

template<auto value1, auto value2>
struct xor_operation{ static constexpr auto value = value1 ^ value2;};


Осталось создать псевдонимы для более простого использования:
псевдонимы
  // Псевдонимы для битовых почленных операций над 2 списками
template<typename List1, typename List2>
using operation_and_termwise_t = typename 
          operation_2_termwise_valuelists<and_operation, List1, List2>::type;

template<typename List1, typename List2>
using operation_or_termwise_t = typename 
          operation_2_termwise_valuelists<or_operation, List1, List2>::type;

template<typename List1, typename List2>
using operation_xor_termwise_t = typename 
          operation_2_termwise_valuelists<xor_operation, List1, List2>::type;

  // Псевдонимы почленных битовых операций для произвольного количества списков
template<typename... Lists>
using lists_termwise_and_t = typename 
          lists_operation<operation_and_termwise_t, Typelist<Lists...>>::type;

template<typename... Lists>
using lists_termwise_or_t= typename 
          lists_operation<operation_or_termwise_t, Typelist<Lists...>>::type;

template<typename... Lists>
using lists_termwise_xor_t = typename 
          lists_operation<operation_xor_termwise_t, Typelist<Lists...>>::type;

Пример использования (обратите внимание на вывод ошибок).

Возвращаясь к имплементации интерфейса


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

template<typename adapter>  
struct IPower{

  template<typename... Peripherals>
  static void Enable(){
     
      // Раскрытие пакета параметров периферии, содержащей свойство ‘power’
      // и применение побитового или к значениям
    using tEnableList = lists_termwise_or_t<typename Peripherals::power...>;

      // Псевдоним Valuelist<…>, содержащий только 0, 
      // количество которых равно количеству регистров
    using tDisableList = typename adapter::template fromValues<>::power;
   
      // Передача списков включения/отключения адаптеру 
  adapter:: template _Set<tEnableList , tDisableList>();
  }

  template<typename EnableList, typename ExceptList>
  static void EnableExcept(){

    using tXORedList = lists_termwise_xor_t <
        typename EnableList::power, typename ExceptList::power>;

    using tEnableList = lists_termwise_and_t <
        typename EnableList::power, tXORedList>;

    using tDisableList = typename adapter::template fromValues<>::power;

    adapter:: template _Set<tEnableList , tDisableList>();
  }

  template<typename EnableList, typename DisableList>
    static void Keep(){

    using tXORedList = lists_termwise_xor_t <
        typename EnableList::power, typename DisableList::power>;

    using tEnableList = lists_termwise_and_t <
        typename EnableList::power, tXORedList>;

    using tDisableList = lists_termwise_and_t <
        typename DisableList::power, tXORedList>;

    adapter:: template _Set<tEnableList , tDisableList>();
  }

  template<typename... PeripheralsList>
  struct fromPeripherals{
    using power = lists_termwise_or_t<typename PeripheralsList::power...>;
  };

};

Также, интерфейс содержит встроенный класс fromPeripherals, позволяющий объединять периферию в один список, который, затем, можно использовать в методах:

  using listPower = Power::fromPeripherals<spi, uart>;

  Power::Enable<listPower>();

Методы Disable реализуются аналогично.

Адаптер контроллера


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

struct Power: public IPower<Power>{

  static constexpr uint32_t 
    _addressAHBENR  = 0x40021014,
    _addressAPB2ENR = 0x40021018,
    _addressAPB1ENR = 0x4002101C;
  
  using AddressesList = Valuelist<
      _addressAHBENR, _addressAPB1ENR, _addressAPB2ENR>;

  template<typename EnableList, typename DisableList>
  static void _Set(){
    // Вызов метода класса, осуществляющий запись в регистры
    HPower:: template ModifyRegisters<EnableList, DisableList, AddressesList>();
  }
    
  template<uint32_t valueAHBENR = 0, uint32_t valueAPB1ENR = 0, uint32_t valueAPB2ENR = 0>
  struct fromValues{
    using power = Valuelist<valueAHBENR, valueAPB1ENR, valueAPB2ENR>;
  };

};

Периферия


Наделяем периферию свойством power, используя структуру fromValues адаптера:

template<int identifier>
struct SPI{
  // С помощью identifier можно выбирать необходимые биты на этапе компиляции
  using power = Power::fromValues<
      RCC_AHBENR_DMA1EN, // Значения для соответствующих регистров,
      RCC_APB1ENR_SPI2EN, // последовательность которых определена в адаптере
      RCC_APB2ENR_IOPBEN>::power;
};

template<int identifier>
struct UART{
  using power = Power::fromValues<
      RCC_AHBENR_DMA1EN,
      0U, 
      RCC_APB2ENR_USART1EN | RCC_APB2ENR_IOPAEN>::power;
};

Запись в регистры


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

В качестве параметров, метод принимает 3 списка нетиповых параметров Valuelist<…>:

  • SetList и ResetList – списки из последовательностей значений битов, которые необходимо установить/сбросить в регистре
  • AddressesList – список адресов регистров, в которые будет производится запись значений из предыдущих параметров

struct HPower{

  template<typename SetList, typename ResetList, typename AddressesList>
    static void ModifyRegisters(){
    if constexpr (!is_empty_v<SetList> && !is_empty_v<ResetList> && 
		  !is_empty_v<AddressesList>){

        // Получаем первые значения списков
      constexpr auto valueSet = front_v<SetList>;
      constexpr auto valueReset = front_v<ResetList>;

      if constexpr(valueSet || valueReset){

        constexpr auto address = front_v<AddressesList>;
        using pRegister_t = volatile std::remove_const_t<decltype(address)>* const;
        auto& reg = *reinterpret_cast<pRegister_t>(address);

        // (!)Единственная строчка кода, которая может попасть в ассемблерный листинг
        reg = (reg &(~valueReset)) | valueSet;
      }

        // Убираем первые значения из всех списков            
      using tRestSet = pop_front_t<SetList>;
      using tRestReset = pop_front_t<ResetList>;
      using tRestAddress = pop_front_t<AddressesList>;
      
        // Вызывается до тех пор, пока списки не станут пустыми
      ModifyRegisters<tRestSet, tRestReset, tRestAddress>();
    }
  };

};

В классе присутствует единственная строчка кода, которая попадет в ассемблерный листинг.

Теперь, когда все блоки структуры готовы, перейдём к тестированию.

Тестируем код


Вспомним условия последней задачи:

  • Включение SPI2 и USART1
  • Выключение SPI2 перед входом в «режим энергосбережения»
  • Включение SPI2 после выхода из «режима энергосбережения»

// Необязательные псевдонимы для периферии
using spi = SPI<2>;
using uart = UART<1>;

// Задаем списки управления тактированием (для удобства)
using listPowerInit = Power::fromPeripherals<spi, uart>;
using listPowerDown = Power::fromPeripherals<spi>;
using listPowerWake = Power::fromPeripherals<uart>;

int main() {

   // Включение SPI2, UASRT1, DMA1, GPIOA, GPIOB
    Power::Enable<listPowerInit>();

    // Some code
    
    // Выключение только SPI2 и GPIOB
    Power::DisableExcept<listPowerDown, listPowerWake>();

    //Sleep();

    // Включение только SPI2 и GPIOB
    Power::EnableExcept<listPowerDown, listPowerWake>();
…
}


Размер кода: 68 байт*, как и в случае с прямой записью в регистры.

Листинг
main:
  // AHBENR(Включение DMA1)
  ldr     r3, .L3
  ldr     r2, [r3, #20]
  orr     r2, r2, #1
  str     r2, [r3, #20]
  // APB1ENR(Включение SPI2
  ldr     r2, [r3, #28]
  orr     r2, r2, #16384
  str     r2, [r3, #28]
  // APB2ENR(Включение GPIOA, GPIOB, USART1)
  ldr     r2, [r3, #24]
  orr     r2, r2, #16384
  orr     r2, r2, #12
  str     r2, [r3, #24]
  // APB1ENR(Выключение SPI2)
  ldr     r2, [r3, #28]
  bic     r2, r2, #16384
  str     r2, [r3, #28]
  // APB2ENR(Выключение GPIOB)
  ldr     r2, [r3, #24]
  bic     r2, r2, #8
  str     r2, [r3, #24]
  // APB1ENR(Включение SPI2
  ldr     r2, [r3, #28]
  orr     r2, r2, #16384
  str     r2, [r3, #28]
  // APB2ENR(Выключение GPIOB)
  ldr     r2, [r3, #24]
  orr     r2, r2, #8
  str     r2, [r3, #24]


*При использовании GCC 9.2.1 получается на 8 байт больше, чем в версии GCC 10.1.1. Как видно из листинга — добавляются несколько ненужных инструкций, например, перед чтением по адресу (ldr) есть инструкция добавления (adds), хотя эти инструкции можно заменить на чтение со смещением. Новая версия оптимизирует эти операции. При этом clang генерирует одинаковые листинги.

Итоги


Поставленные в начале статьи цели достигнуты – скорость выполнения и эффективность сохранились на уровне прямой записи в регистр, вероятность ошибки в пользовательском коде сведена к минимуму.

Возможно, объем исходного кода и сложность разработки покажутся избыточными, однако, благодаря такому количеству абстракций, переход к новому контроллеру займет минимум усилий: 30 строчек понятного кода адаптера + по 5 строк на периферийный блок.

Полный код
type_traits_custom.hpp
#ifndef _TYPE_TRAITS_CUSTOM_HPP
#define _TYPE_TRAITS_CUSTOM_HPP

#include <type_traits>

/*!
  @file
  @brief Traits for metaprogramming
*/

/*!
  @brief Namespace for utils.
*/
namespace utils{

/*-----------------------------------Basic----------------------------------------*/

/*!
  @brief Basic list of types
  @tparam Types parameter pack
*/
template<typename... Types>
struct Typelist{};

/*!
  @brief Basic list of values
  @tparam Values parameter pack
*/
template<auto... Values>
struct Valuelist{};

/*------------------------------End of Basic--------------------------------------*/

/*----------------------------------Front-------------------------------------------
  Description:  Pop front type or value from list

  using listOfTypes = Typelist<int, short, bool, unsigned>;
  using listOfValues = Valuelist<1,2,3,4,5,6,1>;

  |-----------------|--------------------|----------|
  |      Trait      |    Parameters      |  Result  |
  |-----------------|--------------------|----------|
  |     front_t     |   <listOfTypes>    |    int   |
  |-----------------|--------------------|----------|
  |     front_v     |   <listOfValues>   |     1    |
  |-----------------|--------------------|----------| */

namespace{

template<typename List>
struct front;

template<typename Head, typename... Tail>
struct front<Typelist<Head, Tail...>>{ 
  using type = Head; 
};

template<auto Head, auto... Tail>
struct front<Valuelist<Head, Tail...>> {
  static constexpr auto value = Head;
};

}

template<typename List>
using front_t = typename front<List>::type;

template<typename List>
static constexpr auto front_v = front<List>::value;

/*----------------------------------End of Front----------------------------------*/

/*----------------------------------Pop_Front---------------------------------------
  Description:  Pop front type or value from list and return rest of the list

  using listOfTypes = Typelist<int, short, bool>;
  using listOfValues = Valuelist<1,2,3,4,5,6,1>;

  |-----------------|--------------------|------------------------|
  |      Trait      |    Parameters      |         Result         |
  |-----------------|--------------------|------------------------|
  |   pop_front_t   |    <listOfTypes>   | Typelist<short, bool>  |
  |-----------------|--------------------|------------------------|
  |   pop_front_t   |   <listOfValues>   | Valuelist<2,3,4,5,6,1> |
  |-----------------|--------------------|------------------------| */

namespace{

template<typename List>
struct pop_front;

template<typename Head, typename... Tail>
struct pop_front<Typelist<Head, Tail...>> {
  using type = Typelist<Tail...>;
};

template<auto Head, auto... Tail>
struct pop_front<Valuelist<Head, Tail...>> {
  using type = Valuelist<Tail...>;
};

}

template<typename List>
using pop_front_t = typename pop_front<List>::type;

/*------------------------------End of Pop_Front----------------------------------*/

/*----------------------------------Push_Front--------------------------------------
  Description:  Push new element to front of the list

  using listOfTypes = Typelist<short, bool>;

  |-----------------------|--------------------------|-------------------------------|
  |      Trait            |        Parameters        |             Result            |
  |-----------------------|--------------------------|-------------------------------|
  |      push_front_t     |   <listOfTypes, float>   | Typelist<float, short, bool>  |
  |-----------------------|--------------------------|-------------------------------| */

namespace{

template<typename List, typename NewElement>
struct push_front;

template<typename... List, typename NewElement>
struct push_front<Typelist<List...>, NewElement> {
  using type = Typelist<NewElement, List...>;
};

}

template<typename List, typename NewElement>
using push_front_t = typename push_front<List, NewElement>::type;

/*------------------------------End of Push_Front---------------------------------*/

/*----------------------------------Push_Back---------------------------------------
  Description:  Push new value to back of the list

  using listOfValues = Valuelist<1,2,3,4,5,6>;

  |-----------------------|--------------------------|-------------------------------|
  |      Trait            |        Parameters        |             Result            |
  |-----------------------|--------------------------|-------------------------------|
  |   push_back_value_t   |     <listOfValues, 0>    |    Valuelist<1,2,3,4,5,6,0>   |
  |-----------------------|--------------------------|-------------------------------| */

namespace{

template<typename List, auto NewElement>
struct push_back_value;

template<auto... List, auto NewElement>
struct push_back_value<Valuelist<List...>, NewElement>{
  using type = Valuelist<List..., NewElement>;
};

}

template<typename List, auto NewElement>
using push_back_value_t = typename push_back_value<List, NewElement>::type;

/*----------------------------------End of Push_Back------------------------------*/

/*-----------------------------------Is_Empty---------------------------------------
  Description:  Check parameters list for empty and return bool value

  using listOfTypes = Typelist<int, short, bool, unsigned>;
  using listOfValues = Valuelist<>;

  |-------------------------|--------------------|----------|
  |          Trait          |     Parameters     |  Result  |
  |-------------------------|--------------------|----------|
  |        is_empty_v       |    <listOfTypes>   |  false   |
  |-------------------------|--------------------|----------|
  |        is_empty_v       |   <listOfValues>   |   true   |
  |-------------------------|--------------------|----------| */

namespace{
/*!
  @brief Check the emptiness of the types in parameters.   \n 
    E.g.: is_empty<int, short, bool>::value;
*/ 
template<typename List>
struct is_empty{
    static constexpr auto value = false;
};

/*!
  @brief Check the emptiness of the types in parameter. Specializatio for empty parameters   \n 
    E.g.: is_empty<>::value;
*/ 
template<>
struct is_empty<Typelist<>>{
    static constexpr auto value = true;
};

template<>
struct is_empty<Valuelist<>>{
    static constexpr auto value = true;
};

}

/*!
  @brief Check the emptiness of the types-list in parameter.   \n 
    E.g.: using list = Typelist<int, short, bool>; is_empty_v<list>;
*/ 
template<typename List>
static constexpr auto is_empty_v = is_empty<List>::value;

/*--------------------------------End of Is_Empty---------------------------------*/

/*---------------------------------Size_Of_List-------------------------------------
  Description:  Return number of elements in list

  using listOfTypes = Typelist<int, float, double, bool>;

  |------------------|--------------------|----------|
  |       Trait      |     Parameters     |  Result  |
  |------------------|--------------------|----------|
  |  size_of_list_v  |     listOfTypes    |    4     |
  |------------------|--------------------|----------| */

namespace{

template<typename List, std::size_t count = 0U>
struct size_of_list : public size_of_list<pop_front_t<List>, count + 1>{};

template<std::size_t count>
struct size_of_list<Typelist<>, count>{
  static constexpr std::size_t value = count;
};

template<std::size_t count>
struct size_of_list<Valuelist<>, count>{
  static constexpr std::size_t value = count;
};

}

template<typename List>
static constexpr std::size_t size_of_list_v = size_of_list<List>::value;

/*-------------------------------End Size_Of_List---------------------------------*/

/*---------------------------------Lists Operation--------------------------------*/

  /*Description: Operations with lists of values

  using list1 = Valuelist<1, 4, 8, 16>;
  using list2 = Valuelist<1, 5, 96, 17>;

  |------------------------------|-------------------|---------------------------|
  |               Trait          |    Parameters     |           Result          |
  |------------------------------|-------------------|---------------------------|
  |     lists_termwise_and_t     |  <list1, list2>   |  Valuelist<1, 4, 0, 16>   |
  |------------------------------|-------------------|---------------------------|
  |     lists_termwise_or_t      |  <list1, list2>   |  Valuelist<1, 5, 104, 17> |
  |---------------------------- -|-------------------|---------------------------|
  |     lists_termwise_xor_t     |  <list1, list2>   |  Valuelist<0, 1, 104, 1>  |
  |------------------------------|-------------------|---------------------------| */

namespace{

template<template <auto value1, auto value2> typename operation, 
         typename List1, typename List2, typename Result = Valuelist<>>
struct operation_2_termwise_valuelists{
  constexpr static auto newValue = operation<front_v<List1>, front_v<List2>>::value;
  using nextList1 = pop_front_t<List1>;
  using nextList2 = pop_front_t<List2>;
    
  using result = push_back_value_t<Result, newValue>;
  using type = typename 
      operation_2_termwise_valuelists<operation, nextList1, nextList2, result>::type;
};

template<template <auto value1, auto value2> typename operation, typename Result>
struct operation_2_termwise_valuelists<operation, Valuelist<>, Valuelist<>, Result>{
  using type = Result;
};

template<template <auto value1, auto value2> typename operation, 
         typename List2, typename Result>
struct operation_2_termwise_valuelists<operation, Valuelist<>, List2, Result>{
  using type = typename 
      operation_2_termwise_valuelists<operation, Valuelist<0>, List2, Result>::type;
};

template<template <auto value1, auto value2> typename operation, 
         typename List1, typename Result>
struct operation_2_termwise_valuelists<operation, List1, Valuelist<>, Result>{
  using type = typename 
      operation_2_termwise_valuelists<operation, List1, Valuelist<0>, Result>::type;
};

template<template<typename first, typename second> class operation,
         typename Lists, bool isEnd = size_of_list_v<Lists> == 1>
class lists_operation{

  using first = front_t<Lists>;
  using second = front_t<pop_front_t<Lists>>;
  using next = pop_front_t<pop_front_t<Lists>>;
  using result = operation<first, second>;

public:

  using type = typename lists_operation<operation, push_front_t<next, result>>::type;

};

template<template<typename first, typename second> class operation,
         typename Lists>
class lists_operation<operation, Lists, true>{
public:
  using type = front_t<Lists>;
};

template<auto value1, auto value2>
struct and_operation{ static constexpr auto value = value1 & value2;};

template<auto value1, auto value2>
struct or_operation{ static constexpr auto value = value1 | value2;};

template<auto value1, auto value2>
struct xor_operation{ static constexpr auto value = value1 ^ value2;};

template<typename List1, typename List2>
using operation_and_termwise_t = typename 
    operation_2_termwise_valuelists<and_operation, List1, List2>::type;

template<typename List1, typename List2>
using operation_or_termwise_t = typename 
    operation_2_termwise_valuelists<or_operation, List1, List2>::type;

template<typename List1, typename List2>
using operation_xor_termwise_t = typename 
    operation_2_termwise_valuelists<xor_operation, List1, List2>::type;

}

template<typename... Lists>
using lists_termwise_and_t = 
    typename lists_operation<operation_and_termwise_t, Typelist<Lists...>>::type;

template<typename... Lists>
using lists_termwise_or_t = typename 
    lists_operation<operation_or_termwise_t, Typelist<Lists...>>::type;

template<typename... Lists>
using lists_termwise_xor_t = typename 
    lists_operation<operation_xor_termwise_t, Typelist<Lists...>>::type;

/*--------------------------------End of Lists Operation----------------------------*/

} // !namespace utils

#endif //!_TYPE_TRAITS_CUSTOM_HPP




IPower.hpp
#ifndef _IPOWER_HPP
#define _IPOWER_HPP

#include "type_traits_custom.hpp"

#define __FORCE_INLINE __attribute__((always_inline)) inline

/*!
  @brief Controller's peripherals interfaces
*/
namespace controller::interfaces{

/*!
  @brief Interface for Power(Clock control). Static class. CRT pattern
  @tparam <adapter> class of specific controller
*/
template<typename adapter>  
class IPower{

  IPower() = delete;

public:

  /*!
    @brief Enables peripherals Power(Clock)
    @tparam <Peripherals> list of peripherals with trait 'power'
  */
  template<typename... Peripherals>
  __FORCE_INLINE static void Enable(){
    using tEnableList = utils::lists_termwise_or_t<typename Peripherals::power...>;
    using tDisableList = typename adapter::template fromValues<>::power;
   adapter:: template _Set<tEnableList, tDisableList>();
  }

  /*!
    @brief Enables Power(Clock) except listed peripherals in 'ExceptList'. 
      If Enable = Exception = 1, then Enable = 0, otherwise depends on Enable.
    @tparam <EnableList> list to enable, with trait 'power'
    @tparam <ExceptList> list of exception, with trait 'power'
  */
  template<typename EnableList, typename ExceptList>
  __FORCE_INLINE static void EnableExcept(){
    using tXORedList = utils::lists_termwise_xor_t<typename EnableList::power, typename ExceptList::power>;
    using tEnableList = utils::lists_termwise_and_t<typename EnableList::power, tXORedList>;
    using tDisableList = typename adapter::template fromValues<>::power;
    adapter:: template _Set<tEnableList, tDisableList>();
  }

  /*!
    @brief Disables peripherals Power(Clock)
    @tparam <Peripherals> list of peripherals with trait 'power'
  */
  template<typename... Peripherals>
  __FORCE_INLINE static void Disable(){
    using tDisableList = utils::lists_termwise_or_t<typename Peripherals::power...>;
    using tEnableList = typename adapter::template fromValues<>::power;
    adapter:: template _Set<tEnableList, tDisableList>();
  }

  /*!
    @brief Disables Power(Clock) except listed peripherals in 'ExceptList'. 
      If Disable = Exception = 1, then Disable = 0, otherwise depends on Disable.
    @tparam <DisableList> list to disable, with trait 'power'
    @tparam <ExceptList> list of exception, with trait 'power'
  */
  template<typename DisableList, typename ExceptList>
  __FORCE_INLINE static void DisableExcept(){
    using tXORedList = utils::lists_termwise_xor_t<typename DisableList::power, typename ExceptList::power>;
    using tDisableList = utils::lists_termwise_and_t<typename DisableList::power, tXORedList>;
    using tEnableList = typename adapter::template fromValues<>::power;
    adapter:: template _Set<tEnableList, tDisableList>();
  }

  /*!
    @brief Disable and Enables Power(Clock) depends on values. 
      If Enable = Disable = 1, then Enable = Disable = 0, otherwise depends on values
    @tparam <EnableList> list to enable, with trait 'power'
    @tparam <DisableList> list to disable, with trait 'power'
  */
  template<typename EnableList, typename DisableList>
  __FORCE_INLINE static void Keep(){
    using tXORedList = utils::lists_termwise_xor_t<typename EnableList::power, typename DisableList::power>;
    using tEnableList = utils::lists_termwise_and_t<typename EnableList::power, tXORedList>;
    using tDisableList = utils::lists_termwise_and_t<typename DisableList::power, tXORedList>;
    adapter:: template _Set<tEnableList, tDisableList>();
  }

  /*!
    @brief Creates custom 'power' list from peripherals. Peripheral driver should implement 'power' trait.
      E.g.: using power = Power::makeFromValues<1, 512, 8>::power; 
    @tparam <PeripheralsList> list of peripherals with trait 'power'
  */
 template<typename... PeripheralsList>
  class fromPeripherals{
    fromPeripherals() = delete;
    using power = utils::lists_termwise_or_t<typename PeripheralsList::power...>;
    friend class IPower<adapter>;
  };

};

} // !namespace controller::interfaces

#undef   __FORCE_INLINE

#endif // !_IPOWER_HPP




HPower.hpp
#ifndef _HPOWER_HPP
#define _HPOWER_HPP

#include "type_traits_custom.hpp"

#define __FORCE_INLINE __attribute__((always_inline)) inline

/*!
  @brief Hardware operations
*/
namespace controller::hardware{

/*!
  @brief Implements hardware operations with Power(Clock) registers
*/
class HPower{

  HPower() = delete;

protected:

/*!
  @brief Set or Reset bits in the registers
  @tparam <SetList> list of values to set 
  @tparam <ResetList> list of values to reset
  @tparam <AddressesList> list of registers addresses to operate
*/
  template<typename SetList, typename ResetList, typename AddressesList>
  __FORCE_INLINE static void ModifyRegisters(){
    using namespace utils;

    if constexpr (!is_empty_v<SetList> && !is_empty_v<ResetList> && 
		  !is_empty_v<AddressesList>){

      constexpr auto valueSet = front_v<SetList>;
      constexpr auto valueReset = front_v<ResetList>;

      if constexpr(valueSet || valueReset){
        constexpr auto address = front_v<AddressesList>;
          
        using pRegister_t = volatile std::remove_const_t<decltype(address)>* const;
        auto& reg = *reinterpret_cast<pRegister_t>(address);

        reg = (reg &(~valueReset)) | valueSet;
      }
        
      using tRestSet = pop_front_t<SetList>;
      using tRestReset = pop_front_t<ResetList>;
      using tRestAddress = pop_front_t<AddressesList>;
      
      ModifyRegisters<tRestSet, tRestReset, tRestAddress>();
    }
  };

};

} // !namespace controller::hardware

#undef __FORCE_INLINE

#endif // !_HPOWER_HPP




stm32f1_Power.hpp
#ifndef _STM32F1_POWER_HPP
#define _STM32F1_POWER_HPP

#include <cstdint>
#include "IPower.hpp"
#include "HPower.hpp"
#include "type_traits_custom.hpp"

#define __FORCE_INLINE __attribute__((always_inline)) inline

/*!
  @brief Controller's peripherals
*/
namespace controller{

/*!
  @brief Power managment for controller
*/
class Power: public interfaces::IPower<Power>, public hardware::HPower{

  Power() = delete;

public:

  /*!
    @brief Creates custom 'power' list from values. Peripheral driver should implement 'power' trait.
      E.g.: using power = Power::fromValues<1, 512, 8>::power; 
    @tparam <valueAHB=0> value for AHBENR register
    @tparam <valueAPB1=0> value for APB1ENR register
    @tparam <valueAPB2=0> value for APB1ENR register
  */
  template<uint32_t valueAHBENR = 0, uint32_t valueAPB1ENR = 0, uint32_t valueAPB2ENR = 0>
  struct fromValues{
    fromValues() = delete;
    using power = utils::Valuelist<valueAHBENR, valueAPB1ENR, valueAPB2ENR>;
  };

private: 

  static constexpr uint32_t 
    _addressAHBENR  = 0x40021014,
    _addressAPB2ENR = 0x40021018,
    _addressAPB1ENR = 0x4002101C;
  
  using AddressesList = utils::Valuelist<_addressAHBENR, _addressAPB1ENR, _addressAPB2ENR>;

  template<typename EnableList, typename DisableList>
  __FORCE_INLINE static void _Set(){
    HPower:: template ModifyRegisters<EnableList, DisableList, AddressesList>();
  }

  friend class IPower<Power>;

};

} // !namespace controller

#undef __FORCE_INLINE

#endif // !_STM32F1_POWER_HPP




stm32f1_SPI.hpp
#ifndef _STM32F1_SPI_HPP
#define _STM32F1_SPI_HPP

#include "stm32f1_Power.hpp"

namespace controller{

template<auto baseAddress>
class SPI{

  static const uint32_t RCC_AHBENR_DMA1EN = 1;
  static const uint32_t RCC_APB2ENR_IOPBEN = 8;
  static const uint32_t RCC_APB1ENR_SPI2EN = 0x4000;

  /*!
    @brief Trait for using in Power class. Consists of Valueslist with
      values for AHBENR, APB1ENR, APB2ENR registers 
  */
  using power = Power::fromValues<
           RCC_AHBENR_DMA1EN,
           RCC_APB1ENR_SPI2EN, 
           RCC_APB2ENR_IOPBEN>::power;

  template<typename>
  friend class interfaces::IPower;
};

}

#endif // !_STM32F1_SPI_HPP




stm32f1_UART.hpp
#ifndef _STM32F1_UART_HPP
#define _STM32F1_UART_HPP

#include "stm32f1_Power.hpp"

namespace controller{

template<auto baseAddress>
class UART{

  static const uint32_t RCC_AHBENR_DMA1EN = 1;
  static const uint32_t RCC_APB2ENR_IOPAEN = 4;
  static const uint32_t RCC_APB2ENR_USART1EN = 0x4000;

  /*!
    @brief Trait for using in Power class. Consists of Valueslist with
      values for AHBENR, APB1ENR, APB2ENR registers 
  */
  using power = Power::fromValues<
           RCC_AHBENR_DMA1EN,
           0U, 
           RCC_APB2ENR_USART1EN | RCC_APB2ENR_IOPAEN>::power;

  template<typename>
  friend class interfaces::IPower;
};

}

#endif // !_STM32F1_UART_HPP




main.cpp
#include "stm32f1_Power.hpp"
#include "stm32f1_UART.hpp"
#include "stm32f1_SPI.hpp"

using namespace controller;

using spi = SPI<2>;
using uart = UART<1>;

using listPowerInit = Power::fromPeripherals<spi, uart>;
using listPowerDown = Power::fromPeripherals<spi>;
using listPowerWake = Power::fromPeripherals<uart>;

int main(){

  Power::Enable<listPowerInit>();

  //Some code

  Power::DisableExcept<listPowerDown, listPowerWake>();

  //Sleep();

  Power::EnableExcept<listPowerDown, listPowerWake>();

  while(1);
  return 1;
};




Github

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

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

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

          +1

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

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

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

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

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

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

              В данный момент я использую примерно такой концепт для периферии:
              Периферия
              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();
              }
              
                0

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

                  0
                  Для этого варианта можно ввести структуру, в которой, естественно, можно задавать все руками:
                  Структура
                  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();
                  }
                  
                    0

                    Да, только теперь у пользователя есть список с номерами портов(возможно ошибочный) из которого непонятно какие это пины, потому ему придется добавить соответствующий комментарий(возможно ошибочный), а сами пины инитить в другом месте, а тут можно перепутать как сами пины, так и режимы с 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КБ флеша :)

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

                      Рассмотрим, для упрощения 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();
                      
                      }
                      
                        –1

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

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

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

                  Ремап также можно победить, если использовать static_assert времени компиляции. А условия для периферии и пинов придется прописывать либо с мануалом в руках, либо парсить документы производителя.
                  +2
                  Читал внимательно, знакомился с кодом по ссылке и всё равно в голове каша.
                  Открываем какие то порты делаем сопутствующие включения и отключения, для них производим сопутствующие действия и в остатке получается, что не знамо чего натворили, слава богу ошибок не было…

                  Думаю, что начинать нужно было предметно:
                  Есть МК, у него столько то ног.
                  Вот эти ноги используются для управления периферией, управление ими мы и будем программировать.
                  Для управления этими выводами, перечисляем, используются такие-то команды: для подачи управляющего сигнала; для считывания значений. Получение обратной связи для дальнейшего выполнения программы;

                  Допустим.
                  Мы имеем МК, который должен управлять одноразрядным ЖК дисплеем для отображения значений температуры поступающих от датчика, обработать и показать в диапазоне от 0 до 9.
                  image
                  Наши действия: С чего начинаем инициализацию и далее поехали.

                  Тогда и будет понятно что мы программируем и что за команды отрабатываем.

                    0
                    Цель, как раз, была противоположна — максимально, по возможности, абстрагироваться от контроллера, его периферии и внешних устройств.

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

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

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

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

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

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

                    Если вопрос именно в метапрограммировании, то вот несколько очень полезных ссылок: раз, два, три.
                      0
                      Спасибо за ссылки. Помогли значительно расширить мой кругозор.
                        0
                        Реально ли в этой среде программирования объявлять массивы?
                        Я когда-то программировал в СИ но это было давно и вот столкнулся с проблемой ограничения объёмом памяти. Было подобное при написании программ на платформе exel, на бейсике 64кб ограничение, а тут 640 байт (если я правильно понял). Получается, что ни для чего лишнего здесь места нет только логика для выполнения простых команд, а программирование позволяет гибко использовать один инструмент под разные задачи.

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

                        Я правильно улавливаю мысль?
                          0
                          Да, в контроллерах ресурсы довольно ограничены и конечны, но 640 байт — это действительно мало. Сейчас есть экземпляры с 1мб+.

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


                          Если я правильно понял, то да, можно так. Создать массив -> передать его адрес функции для приема данных с датчика -> обработать данные, если нужно -> передать адрес для функции вывода на дисплей.
                            0
                            Спасибо, потихоньку вникаю.
                            ссылка, по которой можно скачать специализированную среду программирования ST Visual develop IDE — среда для программирования STM8.
                            и видео о самом процессе создания проекта с нуля в этой среде до готового изделия.
                      0

                      Если использовать, обертку над регистрами, то можно сделать более универсальный код — не привязанный к периферии типа 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(); 

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

                        0
                        Да, все было примерно так, как Вы описываете. Пока, действительно, в одном случае стало необходимо что-то выключать, что-то оставлять(энергосбережение), в другом — просто нехватка памяти. А, поскольку, класс 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 на просто кнопку. В самом пользовательском коде ничего не поменяется.
                          0
                          То потом можно вызывать просто:
                          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]
                          
                            0

                            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) Да..

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

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

                            0

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


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

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

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

                              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>;
                                ...
                              }
                              
                                0

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

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

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

                                Самое читаемое