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

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

Время на прочтение35 мин
Количество просмотров12K


Как часто, при разработке прошивки для микроконтроллера, во время отладки, когда байтики не бегают по 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
Теги:
Хабы:
Всего голосов 25: ↑23 и ↓2+32
Комментарии41

Публикации

Истории

Работа

QT разработчик
3 вакансии
Программист C++
103 вакансии
Программист С
37 вакансий

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань