Использование C++ и шаблонов с переменным количеством аргументов при программировании микроконтроллеров

в 8:21, , рубрики: c++, diy или сделай сам, stm32, STM32F103C8T6, микроконтроллеры arm, переменное количество аргументов, программирование микроконтроллеров, шаблоны c++

ARM с ядром Cortex Mx (на примере STM32F10x)

КДПВ Микроконтроллер ARM Cortex M3 STM32F103c8t6 широко распространен как 32-х битный микроконтроллер для любительских проектов. Как для практически любого микроконтроллера, для него существует SDK, включающая, в том числе и заголовочные файлы C++ определения периферии контроллера.

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

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

Эта структура и её экземпляр описаны вот так:

/* =========================================================================*/
typedef struct {
    __IO uint32_t CR1;  /*!< USART Control register 1, Address offset: 0x00 */ 
     .
     .
     .
    __IO uint32_t ISR;  /*!< USART Interrupt and status register, ... */ 
} USART_TypeDef; // USART_Type было бы достаточно.

/* =========================================================================*/
#define USART1_BASE    (APBPERIPH_BASE + 0x00013800)
     .
     .
     .
#define USART1         ((USART_TypeDef *) USART1_BASE)
#define USART1_BASE    0x400xx000U

Подробнее можно посмотреть здесь stm32f103xb.h ≈ 800 кБайт

И если пользоваться только только определениями в этом файле, приходится писать вот так (пример использования регистра состояний последовательного порта):

// ----------------------------------------------------------------------------
if (USART1->ISR & (ONE_ISR_FLAG & OTHER_ISR_FLAG))
{

}

А пользоваться приходится, потому что существующие фирменные решения, известные как CMSIS и HAL слишком сложны, чтобы использовать их в любительских проектах.

Но если писать на C++, то можно написать так:

// ----------------------------------------------------------------------------
USART_TypeDef & Usart1 = *USART1;

// ----------------------------------------------------------------------------
if (Usart1.ISR & (ONE_ISR_FLAG & OTHER_ISR_FLAG))
{

}

Изменяемая ссылка инициализируется указателем. Это небольшое облегчение, но приятное. Ещё лучше, конечно, написать небольшой класс-оберточку над этим, при этом такой прием всё равно пригодится.

Конечно, хотелось бы сразу написать этот класс-оберточку над последовательным портом (EUSART – extended universal serial asinhronous reseiver-transmitter), таким заманчивым, с расширенными возможностями, последовательным асинхронным приемопередатчиком и иметь возможность связать наш маленький микроконтроллер с настольной системой или ноутбуком, но микроконтроллеры Cortex отличаются развитой системой тактирования и начать придется с неё, а потом ещё сконфигурировать соответствующие выводы портов ввода-вывода для работы с периферией, потому что в серии STM32F1xx, как и во многих других микроконтроллерах ARM Cortex нельзя просто так сконфигурировать выводы порта на ввод или вывод и работать при этом с периферией.

Что же, начнем с включения тактирования. Система тактирования называется RCC регистры управления тактированием (registers for clock control) и тоже представляет из себя структуру данных, объявленному указателю на которую присвоено конкретное значение адреса.

/* =========================================================================*/
typedef struct
{
    .
    .
    .
} RCC_TypeDef;

Поля этой структуры, объявленные вот так, где __IO определяет volatile:

/* =========================================================================*/
__IO uint32_t CR;

соответствуют регистрам из RCC, а отдельные биты этих регистров включению или функции тактирования периферии микроконтроллера. Всё это хорошо описано в документации (pdf).

Указатель на структуру определен как

/* =========================================================================*/
#define RCC          ((RCC_TypeDef *)RCC_BASE)

Работа с битами регистров без использования SDK обычно выглядит таким образом:

Вот включение тактирования порта A.

// ----------------------------------------------------------------------------
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;

Можно включить два и более бита сразу

// ----------------------------------------------------------------------------
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN;

Выглядит для C++ немного, что ли, непривычно. Лучше было бы написать по другому, вот так, например, используя ООП.

// ----------------------------------------------------------------------------
Rcc.PortOn(Port::A);

Выглядит лучше, но в XXI веке мы пойдем немного дальше, воспользуемся C++ 17 и напишем с использованием шаблонов с переменным количеством параметров ещё красивее:

// ----------------------------------------------------------------------------
 Rcc.PortOn<Port::A, Port::B>();

Где Rcc определена вот таким образом:

// ----------------------------------------------------------------------------
TRcc & Rcc = *static_cast<TRcc *>RCC; 

От этого и начнем строить обертку над регистрами тактирования. Для начала определим класс и указатель (ссылку) на него.

Сначала хотелось написать в стандарте C++ 11/14 с использованием рекурсивной распаковки параметров шаблона функции. Хорошая статья об этом приведена в конце заметки, в разделе ссылки.

// ============================================================================
enum class GPort : uint32_t
{
    A = RCC_APB2ENR_IOPAEN,
    B = RCC_APB2ENR_IOPBEN,
    C = RCC_APB2ENR_IOPCEN,

};

// ----------------------------------------------------------------------------
class TRcc: public ::RCC_TypeDef
{
    private:
        TRcc() = delete;
        ~TRcc() = delete;


    // ========================================================================
    public:
        template<GPort... port>
        inline void PortOn(void) // Без явного разворачивания (inline)
        {                         // не развернется при -Og или -O0
            APB2ENR |= SetBits<(uint32_t)port...>();
        }

    // ------------------------------------------------------------------------
#define BITMASK 0x01    // Макроопределение здесь гарантирует нам, что константа
#define MASKWIDTH 1     // не будет перенесена компилятором в память. Брать от
                        // неё указатель мы не собираемся и у нас есть #undef.
    private:
        // Функциональное пролистывание (fold) пакета параметров рекурсией.
        template<uint8_t bitmask>
        inline constexpr uint32_t SetBits(void)
        {
            // Немного избыточная проверка, ведь GPort это enum
 	    //  (а, кстати, bitmask это и не бит).
            // static_assert(bitmask < 16, "Превышена разрядность.");
            return bitmask;
        }

        template<uint8_t bit1, uint8_t bit2, uint8_t... bit>
        inline constexpr uint32_t SetBits(void)
        {
            return SetBits<bit1>() | SetBits<bit2, bit...>();
        }

};

#undef BITMASK
#undef MASKWIDTH

    // ------------------------------------------------------------------------
    TRcc & Rcc = *static_cast<TRcc *>RCC;

Рассмотрим вызов функции включения тактирования порта:

    Rcc.PortOn<GPort::A>();

GCC развернет его вот в такой набор команд:

   ldr     r3, [pc, #376]  ; (0x8000608 <main()+392>)
   ldr     r0, [r3, #24]
   orr.w   r0, r0, #4
   str     r0, [r3, #24]

Получилось? Проверим дальше

    Rcc.PortOn<GPort::A, GPort::B, GPort::C>();

Увы, не совсем, наивный GCC развернул замыкающий вызов рекурсии отдельно:

    ldr     r3, [pc, #380]  ; (0x8000614 <main()+404>) 
    ldr     r0, [r3, #24]
    orr.w   r0, r0, #4      ; APB2ENR |= GPort::A
    str     r0, [r3, #24]
    ldr     r0, [r3, #24]
    orr.w   r0, r0, #28     ; APB2ENR |= Gport::B | GPort::C
    str     r0, [r3, #24] #24]

В защиту GCC нужно сказать, что вот так разворачивается не всегда, а только в более сложных случаях, что будет видно при реализации класса порта ввода-вывода. Что же, тут на помощь спешит C++ 17. Перепишем класс TRCC, используя возможности встроенного пролистывания.

// ----------------------------------------------------------------------------
class TRcc: public ::RCC_TypeDef
{
    private:
        TRcc() = delete;      // Мы не создаем экземпляр класса, а
        ~TRcc() = delete;    // используем для инициализации указатель.


    // ========================================================================
    public:
        template<GPort... port>
        inline void PortOn(void) // Без явного разворачивания (inline)
        {                         // не развернется при -Og или -O0
            APB2ENR |= SetBits17<(uint32_t)port...>();
        }

    // ------------------------------------------------------------------------
#define BITMASK 0x01    // Макроопределение здесь гарантирует нам, что константа
#define MASKWIDTH 1     // не будет перенесена компилятором в память. Брать от
                        // неё указатель мы не собираемся и у нас есть #undef.
    private:
        // Функциональное пролистывание (fold) пакета параметров рекурсией. С++ 17.
        template<uint8_t... bitmask>
        inline constexpr uint32_t SetBits17(void)
        {
            return (bitmask | ...); // Можно и справа налево ... | bit
        }
};

#undef BITMASK
#undef MASKWIDTH

Вот теперь получилось:

ldr     r2, [pc, #372]  ; (0x800060c <main()+396>)
ldr     r0, [r2, #24]
orr.w   r0, r0, #28     ; APB2ENR |= Gport::A | Gport::B | GPort::C
str     r0, [r3, #24]

И код класса стал проще.

Вывод: C++ 17 позволяет нам с помощью шаблонов с переменным числом параметров получить такой же минимальный набор инструкций (даже при выключенной оптимизации), какой получается при использовании классической работы с микроконтроллером через определения регистров, но при этом мы получаем все преимущества сильной типизации C++, проверок во время компиляции, переиспользуемого через структуру базовых классов кода и так далее.

Вот как-то так записанное на C++

Rcc.PortOn<Port::A, Port::B, Port::C>();

И классический текст на регистрах:

RCC->APB2 |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN;

разворачиваются в оптимальный набор инструкций. Вот код, сгенерированный GCC (оптимизация выключена -Og):

    ldr     r2, [pc, #372]  ; (0x800060c <main()+396>) [Адрес структуры RCC]
    ldr     r0, [r2, #0]    ; r0 = RCC->APB2 // [Адрес регистра APB2]
    orr.w   r0, r0, #160    ; r0 |= 0x10100000
    str     r0, [r2, #0]    ; RCC->APB2 = r0

Теперь следует продолжить работу и написать класс порта ввода-вывода. Работа с битами портов ввода-вывода осложняется тем, что на конфигурацию одной ножки порта отводится четыре бита и, таким образом, на 16-ти битный порт требуется 64 бита конфигурации, которые разделены на два 32-х битные регистра CRL и CRH. Плюс ещё ширина битовой маски становится больше 1. Но и тут пролистывание C++ 17 показывает свои возможности.

image

Далее будет написан класс TGPIO, а также классы для работы с другой периферией, последовательного порта, I2C, SPI, ПДП, таймеров и многого другого, что обычно присутствует в микроконтроллерах ARM Cortex и тогда можно будет помигать вот такими светодиодиками.

Но об этом в следующей заметке. Исходники проекта на гитхабе.

Интернет статьи, использованные при написании заметки

Шаблоны с переменным количеством аргументов в C++11.
Нововведения в шаблонах.
Языковые новшества C++17. Часть 1. Свёртка и выведение.
Список ссылок на документацию по микроконтроллерам STM.
Макросы с переменным числом параметров

Статьи на Xабре, побудившие меня всё-таки написать эту заметку

Светофорчик на Attiny13.

Джулиан Ассанж арестован полицией Великобритании
Космос как смутное воспоминание

Написано 12.04.2019 – С Днем Космонавтики!

P. S.

STM32F103c8t6 в Stm CubeMx Картинка STM32F103c8t6 из CubeMX.

В качестве отправной точки использован текст, созданный расширением Eclips'а для работы с микроконтроллерами GNU MCU Eclipse ARM Embedded и STM-ского CubeMX, т. есть файлы стандартных функций C++, _start() и _init(), определения векторов прерываний взяты из MCU Eclipse ARM Embedded, а файлы определения регистров и работы с ядром Cortex M3 – из проекта, сделанного CubeMX.

P. P. S.

На КДПВ изображена отладка с контроллером STM32F103c8t6. Далеко не у всех есть такая плата, но приобрести её несложно, правда, это выходит за рамки данной заметки.

Автор: Ardt2

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js