[Из песочницы] Включаем периферию контроллера за 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
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
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
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();
…
}
Размер кода: 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();
// Some code
// Выключаем SPI и GPIOB вместе (!) с DMA
Power::Disable();
// Включаем обратно DMA вместе(!) с USART и GPIOA
Power::Enable();
// Sleep();
// Включаем SPI и GPIOB вместе(!) с DMA
Power::Enable();
…
}
Размер кода: 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
Enable();
template
EnableExcept();
template
Keep();
Enable — включение периферии, указанной в параметре шаблона.
EnableExcept — включение периферии, указанной в параметре EnableList, за исключением той, что указана в ExceptList.
Например, вызов:
EnableExcept();
должен установить бит SPI2EN и бит IOPBEN. В то время, как общий DMA1EN, а также USART1EN и IOPAEN останутся в исходном состоянии.
Чтобы получить соответствующую таблицу истинности, необходимо произвести следующие операции:
resultEnable = (enable ^ except) & enable
К ним в дополнение также идут комплементарные методы Disable, выполняющие противоположные действия.
Keep — включение периферии из EnableList, выключение периферии из DisableList, при этом, если периферия присутствует в обоих списках, то она сохраняет свое состояние.
Например, при вызове:
Keep();
установятся SPI2EN и IOPBEN, при этом USART1EN и IOPAEN сбросятся, а DMA1EN останется неизменным.
Чтобы получить соответствующую таблицу истинности, необходимо произвести следующие операции:
resultEnable = (enable ^ disable) & enable
resultDisable = (enable ^ disable) & disable
Методы включения/выключения уже реализованы довольно неплохо с помощью fold expression, но как быть с остальными?
Если ограничиться использованием 2 видов периферии, как это сделано в пояснении, то никаких сложностей не возникнет. Однако, когда в проекте используется много различных периферийных устройств, появляется проблема — в шаблоне нельзя явно использовать более одного parameter pack, т.к. компилятор не сможет определить где заканчивается один и начинается второй:
template
EnableExcept(){…};
// Невозможно определить где заканчивается EnableList и начинается ExceptList
EnableExcept();
Можно было бы создать отдельный класс-обертку для периферии и передавать его в метод:
template
PowerWrap{
static constexpr auto valueAHBENR = (Peripherals::valueAHBENR | …);
static constexpr auto valueAPB1ENR = (Peripherals:: valueAPB1ENR | …);
static constexpr auto valueAPB2ENR = (Peripherals:: valueAPB2ENR | …);
};
using EnableList = PowerWrap;
using ExceptList = PowerWrap;
EnableExcept();
Но и в этом случае, интерфейс станет жестко завязан на количестве регистров, следовательно, для каждого типа контроллера станет необходимо писать свой отдельный класс, с множеством однотипных операций и без возможности разделения на абстрактные слои.
Поскольку вся используемая периферия и регистры тактирования известны на этапе компиляции, то поставленную задачу возможно решить с использованием метапрограммирования.
Метапрограммирование
Из-за того, что в основе метапрограммирования лежит работа не с обычными типами, а с их списками, то определим две сущности, которые будут оперировать типовыми и нетиповыми параметрами:
template
struct Typelist{};
template
struct Valuelist{};
…
using listT = Typelist ;// Список из последовательности типов char и int
…
using listV = Valuelist<8,9,5,11> ;// Список из 4 нетиповых параметров
Прежде чем делать что-либо полезное с этими списками, нам потребуется реализовать некоторые базовые операции, на основе которых станет возможно совершать более сложные действия.
1. Извлечение первого элемента из списка
// Прототип функции
template
struct front;
// Специализация для списка типов
// Разделение списка в пакете параметров на заглавный и оставшиеся
template
struct front>{
// Возвращение заглавного типа
using type = Head;
};
// Специализация для списка нетиповых параметров
template
struct front> {
// Возвращение заглавного значения
static constexpr auto value = Head;
};
// Псевдонимы для простоты использования
template
using front_t = typename front::type;
template
static constexpr auto front_v = front::value;
// Примеры
using listT = Typelist;
using type = front_t; // type = char
using listV = Valuelist<9,8,7>;
constexpr auto value = front_v; //value = 9
2. Удаление первого элемента из списка
template
struct pop_front;
// Специализация для списка типов
// Разделение списка в пакете параметров на заглавный и оставшиеся
template
struct pop_front> {
// Возвращение списка, содержащего оставшиеся типы
using type = Typelist;
};
template
struct pop_front> {
using type = Valuelist;
};
template
using pop_front_t = typename pop_front::type;
// Примеры
using listT = Typelist;
using typeT = pop_front_t; // type = Typelist
using listV = Valuelist<9,8,7>;
using typeV = pop_front_t; // type = Valuelist<8,7>
3. Добавление элемента в начало списка
template
struct push_front;
template
struct push_front, NewElement> {
using type = Typelist;
};
template
using push_front_t = typename push_front::type;
// Пример
using listT = Typelist;
using typeT = push_front_t; // type = Typelist
4. Добавление нетипового параметра в конец списка
template
struct push_back;
template
struct push_back, NewElement>{
using type = Valuelist;
};
template
using push_back_t = typename push_back::type;
// Пример
using listV = Valuelist<9,8,7>;
using typeV = push_back_t; // typeV = Valuelist<9,8,7,6>
5. Проверка списка на пустоту
template
struct is_empty{
static constexpr auto value = false;
};
// Специализация для базового случая, когда список пуст
template<>
struct is_empty>{
static constexpr auto value = true;
};
template
static constexpr auto is_empty_v = is_empty::value;
// Пример
using listT = Typelist;
constexpr auto value = is_empty_v; // value = false
6. Нахождение количества элементов в списке
// Функция рекурсивно извлекает по одному элементу из списка,
// инкрементируя счетчик count, пока не дойдет до одного из 2 базовых случаев
template
struct size_of_list : public size_of_list, count + 1>{};
// Базовый случай для пустого списка типов
template
struct size_of_list, count>{
static constexpr std::size_t value = count;
};
// Базовый случай для пустого списка нетиповых параметров
template
struct size_of_list, count>{
static constexpr std::size_t value = count;
};
template
static constexpr std::size_t size_of_list_v = size_of_list::value;
// Пример
using listT = Typelist;
constexpr auto value = size_of_list_v ; // value = 3
Теперь, когда определены все базовые действия, можно переходить к написанию метафункций для битовых операций: or, and, xor, необходимых для методов интерфейса.
Поскольку эти битовые преобразования однотипны, то попытаемся сделать реализацию максимально обобщенно, чтобы избежать дублирования кода.
Функция, выполняющая абстрактную операцию над списком
template class operation,
typename Lists, bool isEnd = size_of_list_v == 1>
class lists_operation{
using first = front_t; // (3)
using second = front_t>; // (4)
using next = pop_front_t>; // (5)
using result = operation; // (6)
public:
using type = typename
lists_operation>::type; // (7)
};
template class operation, typename List>
class lists_operation{ // (1)
public:
using type = front_t; // (2)
};
Lists — список, состоящий из типов или списков, над которым необходимо провести некоторое действие.
operation — функциональный адаптер, который принимает 2 первых элемента Lists и возвращает результирующий тип после операции.
isEnd — граничное условие метафункции, которое проверяет количество типов в Lists.
В базовом случае (1) Lists состоит из 1 элемента, поэтому результатом работы функции станет его извлечение (2).
Для остальных случаев — определяют первый (3) и второй (4) элементы из Lists, к которым применяется операция (6). Для получения результирующего типа (7) происходит рекурсивный вызов метафункции с новым списком типов, на первом месте которого стоит (6), за которым следуют оставшиеся типы (5) исходного Lists. Окончанием рекурсии становиться вызов специализации (1).
Далее, реализуем операцию для предыдущей метафункции, которая почленно будет производить абстрактные действия над нетиповыми параметрами из двух списков:
template typename operation,
typename List1, typename List2, typename Result = Valuelist<>>
struct operation_2_termwise_valuelists{
constexpr static auto newValue =
operation, front_v>::value; // (2)
using nextList1 = pop_front_t;
using nextList2 = pop_front_t;
using result = push_back_value_t; // (3)
using type = typename
operation_2_termwise_valuelists ::type; // (4)
};
template typename operation, typename Result>
struct operation_2_termwise_valuelists , Valuelist<>, Result>{ // (1)
using type = Result;
};
List1 и List2 — списки нетиповых параметров, над которыми необходимо произвести действие.
operation — операция, производимая над нетиповыми параметрами.
Result — тип, используемый для накопления промежуточных результатов.
Базовый случай (1), когда оба списка пусты, возвращает Result.
Для остальных случаев происходит вычисление значения операции (2) и занесение его в результирующий список Result (3). Далее рекурсивно вызывается метафункция (4) до того момента, пока оба списка не станут пустыми.
Функции битовых операций:
template
struct and_operation{ static constexpr auto value = value1 & value2;};
template
struct or_operation{ static constexpr auto value = value1 | value2;};
template
struct xor_operation{ static constexpr auto value = value1 ^ value2;};
Осталось создать псевдонимы для более простого использования:
// Псевдонимы для битовых почленных операций над 2 списками
template
using operation_and_termwise_t = typename
operation_2_termwise_valuelists::type;
template
using operation_or_termwise_t = typename
operation_2_termwise_valuelists::type;
template
using operation_xor_termwise_t = typename
operation_2_termwise_valuelists::type;
// Псевдонимы почленных битовых операций для произвольного количества списков
template
using lists_termwise_and_t = typename
lists_operation>::type;
template
using lists_termwise_or_t= typename
lists_operation>::type;
template
using lists_termwise_xor_t = typename
lists_operation>::type;
Пример использования (обратите внимание на вывод ошибок).
Возвращаясь к имплементации интерфейса
Поскольку на этапе компиляции известен и контроллер, и используемая периферия, то логичный выбор для реализации интерфейса — статический полиморфизм с идиомой CRTP. В качестве шаблонного параметра, интерфейс принимает класс-адаптер конкретного контроллера, который, в свою очередь, является наследником этого интерфейса.
template
struct IPower{
template
static void Enable(){
// Раскрытие пакета параметров периферии, содержащей свойство ‘power’
// и применение побитового или к значениям
using tEnableList = lists_termwise_or_t;
// Псевдоним Valuelist<…>, содержащий только 0,
// количество которых равно количеству регистров
using tDisableList = typename adapter::template fromValues<>::power;
// Передача списков включения/отключения адаптеру
adapter:: template _Set();
}
template
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();
}
template
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();
}
template
struct fromPeripherals{
using power = lists_termwise_or_t;
};
};
Также, интерфейс содержит встроенный класс fromPeripherals, позволяющий объединять периферию в один список, который, затем, можно использовать в методах:
using listPower = Power::fromPeripherals;
Power::Enable();
Методы Disable реализуются аналогично.
Адаптер контроллера
В классе адаптера необходимо задать адреса регистров тактирования и определить последовательность, в которой будет производиться запись в них, а затем передать управление непосредственно классу, который установит или сбросит биты указанных регистров.
struct Power: public IPower{
static constexpr uint32_t
_addressAHBENR = 0x40021014,
_addressAPB2ENR = 0x40021018,
_addressAPB1ENR = 0x4002101C;
using AddressesList = Valuelist<
_addressAHBENR, _addressAPB1ENR, _addressAPB2ENR>;
template
static void _Set(){
// Вызов метода класса, осуществляющий запись в регистры
HPower:: template ModifyRegisters();
}
template
struct fromValues{
using power = Valuelist;
};
};
Периферия
Наделяем периферию свойством power, используя структуру fromValues адаптера:
template
struct SPI{
// С помощью identifier можно выбирать необходимые биты на этапе компиляции
using power = Power::fromValues<
RCC_AHBENR_DMA1EN, // Значения для соответствующих регистров,
RCC_APB1ENR_SPI2EN, // последовательность которых определена в адаптере
RCC_APB2ENR_IOPBEN>::power;
};
template
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
static void ModifyRegisters(){
if constexpr (!is_empty_v && !is_empty_v &&
!is_empty_v){
// Получаем первые значения списков
constexpr auto valueSet = front_v;
constexpr auto valueReset = front_v;
if constexpr(valueSet || valueReset){
constexpr auto address = front_v;
using pRegister_t = volatile std::remove_const_t* const;
auto& reg = *reinterpret_cast(address);
// (!)Единственная строчка кода, которая может попасть в ассемблерный листинг
reg = (reg &(~valueReset)) | valueSet;
}
// Убираем первые значения из всех списков
using tRestSet = pop_front_t;
using tRestReset = pop_front_t;
using tRestAddress = pop_front_t;
// Вызывается до тех пор, пока списки не станут пустыми
ModifyRegisters();
}
};
};
В классе присутствует единственная строчка кода, которая попадет в ассемблерный листинг.
Теперь, когда все блоки структуры готовы, перейдём к тестированию.
Тестируем код
Вспомним условия последней задачи:
- Включение SPI2 и USART1
- Выключение SPI2 перед входом в «режим энергосбережения»
- Включение SPI2 после выхода из «режима энергосбережения»
// Необязательные псевдонимы для периферии
using spi = SPI<2>;
using uart = UART<1>;
// Задаем списки управления тактированием (для удобства)
using listPowerInit = Power::fromPeripherals;
using listPowerDown = Power::fromPeripherals;
using listPowerWake = Power::fromPeripherals;
int main() {
// Включение SPI2, UASRT1, DMA1, GPIOA, GPIOB
Power::Enable();
// Some code
// Выключение только SPI2 и GPIOB
Power::DisableExcept();
//Sleep();
// Включение только SPI2 и GPIOB
Power::EnableExcept();
…
}
Размер кода: 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 строк на периферийный блок.
#ifndef _TYPE_TRAITS_CUSTOM_HPP
#define _TYPE_TRAITS_CUSTOM_HPP
#include
/*!
@file
@brief Traits for metaprogramming
*/
/*!
@brief Namespace for utils.
*/
namespace utils{
/*-----------------------------------Basic----------------------------------------*/
/*!
@brief Basic list of types
@tparam Types parameter pack
*/
template
struct Typelist{};
/*!
@brief Basic list of values
@tparam Values parameter pack
*/
template
struct Valuelist{};
/*------------------------------End of Basic--------------------------------------*/
/*----------------------------------Front-------------------------------------------
Description: Pop front type or value from list
using listOfTypes = Typelist;
using listOfValues = Valuelist<1,2,3,4,5,6,1>;
|-----------------|--------------------|----------|
| Trait | Parameters | Result |
|-----------------|--------------------|----------|
| front_t | | int |
|-----------------|--------------------|----------|
| front_v | | 1 |
|-----------------|--------------------|----------| */
namespace{
template
struct front;
template
struct front>{
using type = Head;
};
template
struct front> {
static constexpr auto value = Head;
};
}
template
using front_t = typename front::type;
template
static constexpr auto front_v = front::value;
/*----------------------------------End of Front----------------------------------*/
/*----------------------------------Pop_Front---------------------------------------
Description: Pop front type or value from list and return rest of the list
using listOfTypes = Typelist;
using listOfValues = Valuelist<1,2,3,4,5,6,1>;
|-----------------|--------------------|------------------------|
| Trait | Parameters | Result |
|-----------------|--------------------|------------------------|
| pop_front_t | | Typelist |
|-----------------|--------------------|------------------------|
| pop_front_t | | Valuelist<2,3,4,5,6,1> |
|-----------------|--------------------|------------------------| */
namespace{
template
struct pop_front;
template
struct pop_front> {
using type = Typelist;
};
template
struct pop_front> {
using type = Valuelist;
};
}
template
using pop_front_t = typename pop_front::type;
/*------------------------------End of Pop_Front----------------------------------*/
/*----------------------------------Push_Front--------------------------------------
Description: Push new element to front of the list
using listOfTypes = Typel