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 показывает свои возможности.

Далее будет написан класс TGPIO, а также классы для работы с другой периферией, последовательного порта, I2C, SPI, ПДП, таймеров и многого другого, что обычно присутствует в микроконтроллерах ARM Cortex и тогда можно будет помигать вот такими светодиодиками.
Но об этом в следующей заметке. Исходники проекта на гитхабе.
Интернет статьи, использованные при написании заметки
Шаблоны с переменным количеством аргументов в C++11.
Нововведения в шаблонах.
Языковые новшества C++17. Часть 1. Свёртка и выведение.
Список ссылок на документацию по микроконтроллерам STM.
Макросы с переменным числом параметров
Статьи на Xабре, побудившие меня всё-таки написать эту заметку
Светофорчик на Attiny13.
Джулиан Ассанж арестован полицией Великобритании
Космос как смутное воспоминание
Написано 12.04.2019 – С Днем Космонавтики!
P. S.
Картинка STM32F103c8t6 из CubeMX.
В качестве отправной точки использован текст, созданный расширением Eclips'а для работы с микроконтроллерами GNU MCU Eclipse ARM Embedded и STM-ского CubeMX, т. есть файлы стандартных функций C++, _start() и _init(), определения векторов прерываний взяты из MCU Eclipse ARM Embedded, а файлы определения регистров и работы с ядром Cortex M3 – из проекта, сделанного 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. Далеко не у всех есть такая плата, но приобрести её несложно, правда, это выходит за рамки данной заметки.
