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 – С Днем Космонавтики!
В качестве отправной точки использован текст, созданный расширением Eclips'а для работы с микроконтроллерами GNU MCU Eclipse ARM Embedded и STM-ского CubeMX, т. есть файлы стандартных функций C++, _start() и _init(), определения векторов прерываний взяты из MCU Eclipse ARM Embedded, а файлы определения регистров и работы с ядром Cortex M3 – из проекта, сделанного CubeMX.
Комментарии (50)
andreili
16.04.2019 12:38Вот что я для себя запилил на С++ для STM32: github.com/andreili/STM32FX_FM
Уже есть очень многое, включая полную поддержку USB HOST (MSC + HID), scmRTOS, FATfs.
Проект активно развивается, есть поддержка и STM32F103, но частичная — я в основном сосредоточился на STM32F407, для которого и начинал писать данный проект.
Но подход с шаблонами возьму на заметку — может и обновлю свои классы в репе ;)Goron_Dekar
16.04.2019 12:49Для USB не хватает генерации дескрипторов в компил-тайме.
Очень много препроцессора.
Нет поддержки векторов прерываний в памяти с динамической сменой вектора.andreili
16.04.2019 13:201) У меня нет поддержки USB Device как бы ;) Я пока что только Host делал, поскольку он был мне нужен.
2) Постепенно ухожу от него;
3) Есть, ещё как есть:
#ifdef USE_MEMORY_ISR __attribute__((section(".isr_vector"))) const ISR::ShortVectors interruptsVectorTable = #else volatile __attribute__((section(".isr_vector"))) const ISR::Vectors interruptsVectorTable = #endif { #ifdef USE_MEMORY_ISR (uint32_t)&_estack, ISR::Reset };
Ну и кусок из стандартного init'а:
/* Configure the Vector Table location add offset address ------------------*/ #ifdef VECT_TAB_SRAM SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM */ #else SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH */ #endif
Опять-таки — данным функционалом не пользовался, потому не проверял пока…
KonstantinSpb
16.04.2019 12:45Да, надо популяризировать С++ среди С программистов, некоторые думают, что С++ генерит более тяжелый код
8street
Goron_Dekar
Сложные не по освоению, а по поведению.
Описанный тут вариант использует всю мощь компил-тайма, и после компиляции не вызывает неконтролируемого программистом поведения.
А закоренелым Сшникам ничего не объяснить, увы. Для них есть статьи как перейти на плюсы.
lamerok
По, моему, Пример хороший, пользоваться им очень просто…
Да и класс получился простым...
NordicEnergy