Всем привет! Проблема использования C++ в микроконтроллерах мучает меня уже довольно давно.
Дело было в том, что я искренне не понимал, как этот объектно-ориентированный язык можно применить к встраиваемым системам.
Я имею в виду, как идентифицировать классы и на какой основе создавать объекты, то есть как правильно использовать этот язык.
Спустя некоторое время и прочитав энное количество литературы, я пришел к некоторым результатам, о которых хочу рассказать в этой статье.
Имеют ли эти результаты какую-либо ценность или нет, остается на усмотрение читателя.
Мне будет очень интересно прочитать критику моего подхода, чтобы наконец ответить на вопрос: «Как правильно использовать C++ при программировании микроконтроллеровЭ» Предупреждаю, эта статья будет содержать много исходного кода.
В этой статье на примере использования USART в МК stm32 для связи с esp8266 я попытаюсь изложить свой подход и его основные преимущества.
Начнем с того, что основным преимуществом использования C++ для меня является возможность сделать аппаратную развязку, т.е.
сделать использование модулей верхнего уровня независимым от аппаратной платформы.
Это приведет к тому, что система станет легко модифицируемой при любых изменениях.
Для этого я выделил три уровня системной абстракции:
- HW_USART — аппаратный уровень, зависит от платформы
- MW_USART — средний уровень, используется для разделения первого и третьего уровней.
- APP_ESP8266 — уровень приложения, ничего не знает о МК
HW_USART
Самый примитивный уровень.Я использовал камень stm32f411, USART #2, также поддерживал DMA. Интерфейс реализован в виде всего трёх функций: инициализировать, отправить, получить.
Функция инициализации выглядит следующим образом:
В функции нет ничего особенного, кроме того, что я использую битовые маски для сокращения результирующего кода.bool usart2_init(uint32_t baud_rate) { bool res = false; /*-------------GPIOA Enable, PA2-TX/PA3-RX ------------*/ BIT_BAND_PER(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN) = true; /*----------GPIOA set-------------*/ GPIOA->MODER |= (GPIO_MODER_MODER2_1 | GPIO_MODER_MODER3_1); GPIOA->OSPEEDR |= (GPIO_OSPEEDER_OSPEEDR2 | GPIO_OSPEEDER_OSPEEDR3); constexpr uint32_t USART_AF_TX = (7 << 8); constexpr uint32_t USART_AF_RX = (7 << 12); GPIOA->AFR[0] |= (USART_AF_TX | USART_AF_RX); /*!---------------USART2 Enable------------>!*/ BIT_BAND_PER(RCC->APB1ENR, RCC_APB1ENR_USART2EN) = true; /*-------------USART CONFIG------------*/ USART2->CR3 |= (USART_CR3_DMAT | USART_CR3_DMAR); USART2->CR1 |= (USART_CR1_TE | USART_CR1_RE | USART_CR1_UE); USART2->BRR = (24000000UL + (baud_rate >> 1))/baud_rate; //Current clocking for APB1 /*-------------DMA for USART Enable------------*/ BIT_BAND_PER(RCC->AHB1ENR, RCC_AHB1ENR_DMA1EN) = true; /*-----------------Transmit DMA--------------------*/ DMA1_Stream6->PAR = reinterpret_cast<uint32_t>(&(USART2->DR)); DMA1_Stream6->M0AR = reinterpret_cast<uint32_t>(&(usart2_buf.tx)); DMA1_Stream6->CR = (DMA_SxCR_CHSEL_2| DMA_SxCR_MBURST_0 | DMA_SxCR_PL | DMA_SxCR_MINC | DMA_SxCR_DIR_0); /*-----------------Receive DMA--------------------*/ DMA1_Stream5->PAR = reinterpret_cast<uint32_t>(&(USART2->DR)); DMA1_Stream5->M0AR = reinterpret_cast<uint32_t>(&(usart2_buf.rx)); DMA1_Stream5->CR = (DMA_SxCR_CHSEL_2 | DMA_SxCR_MBURST_0 | DMA_SxCR_PL | DMA_SxCR_MINC); DMA1_Stream5->NDTR = MAX_UINT16_T; BIT_BAND_PER(DMA1_Stream5->CR, DMA_SxCR_EN) = true; return res; }
Тогда функция отправки будет выглядеть так: bool usart2_write(const uint8_t* buf, uint16_t len)
{
bool res = false;
static bool first_attempt = true;
/*!<-----Copy data to DMA USART TX buffer----->!*/
memcpy(usart2_buf.tx, buf, len);
if(!first_attempt)
{
/*!<-----Checking copmletion of previous transfer------->!*/
while(!(DMA1->HISR & DMA_HISR_TCIF6)) continue;
BIT_BAND_PER(DMA1->HIFCR, DMA_HIFCR_CTCIF6) = true;
}
first_attempt = false;
/*!<------Sending data to DMA------->!*/
BIT_BAND_PER(DMA1_Stream6->CR, DMA_SxCR_EN) = false;
DMA1_Stream6->NDTR = len;
BIT_BAND_PER(DMA1_Stream6->CR, DMA_SxCR_EN) = true;
return res;
}
У функции есть костыль в виде переменной first_attempt, которая помогает определить, первая ли это отправка DMA или нет. Почему это необходимо? Все дело в том, что я проверял, была ли предыдущая отправка в DMA успешной или нет ДО отправки, а не ПОСЛЕ.
Я сделал это для того, чтобы после отправки данных было не глупо ждать их завершения, а в это время выполнять полезный код.
Тогда функция приема будет выглядеть так: uint16_t usart2_read(uint8_t* buf)
{
uint16_t len = 0;
constexpr uint16_t BYTES_MAX = MAX_UINT16_T; //MAX Bytes in DMA buffer
/*!<---------Waiting until line become IDLE----------->!*/
if(!(USART2->SR & USART_SR_IDLE)) return len;
/*!<--------Clean the IDLE status bit------->!*/
USART2->DR;
/*!<------Refresh the receive DMA buffer------->!*/
BIT_BAND_PER(DMA1_Stream5->CR, DMA_SxCR_EN) = false;
len = BYTES_MAX - (DMA1_Stream5->NDTR);
memcpy(buf, usart2_buf.rx, len);
DMA1_Stream5->NDTR = BYTES_MAX;
BIT_BAND_PER(DMA1->HIFCR, DMA_HIFCR_CTCIF5) = true;
BIT_BAND_PER(DMA1_Stream5->CR, DMA_SxCR_EN) = true;
return len;
}
Особенность этой функции в том, что я заранее не знаю, сколько байт я должен получить.
Чтобы указать полученные данные, я проверяю флаг IDLE, затем, если состояние IDLE зафиксировано, я сбрасываю флаг и считываю данные из буфера.
Если состояние IDLE не фиксировано, функция просто возвращает ноль, то есть никаких данных.
На этом я предлагаю закончить с низким уровнем и перейти непосредственно к C++ и шаблонам.
MW_USART
Здесь я реализовал базовый абстрактный класс USART и использовал шаблон прототипа для создания потомков (конкретных классов USART1 и USART2).Я не буду описывать реализацию паттерна-прототипа, так как его можно найти по первой ссылке в Google, но сразу приведу исходный код и приведу пояснения ниже.
#pragma once
#include <stdint.h>
#include <vector>
#include <map>
/*!<========Enumeration of USART=======>!*/
enum class USART_NUMBER : uint8_t
{
_1,
_2
};
class USART; //declaration of basic USART class
using usart_registry = std::map<USART_NUMBER, USART*>;
/*!<=========Registry of prototypes=========>!*/
extern usart_registry _instance; //Global variable - IAR Crutch
#pragma inline=forced
static usart_registry& get_registry(void) { return _instance; }
/*!<=======Should be rewritten as========>!*/
/*
static usart_registry& get_registry(void)
{
usart_registry _instance;
return _instance;
}
*/
/*!<=========Basic USART classes==========>!*/
class USART
{
private:
protected:
static void add_prototype(USART_NUMBER num, USART* prot)
{
usart_registry& r = get_registry();
r[num] = prot;
}
static void remove_prototype(USART_NUMBER num)
{
usart_registry& r = get_registry();
r.erase(r.find(num));
}
public:
static USART* create_USART(USART_NUMBER num)
{
usart_registry& r = get_registry();
if(r.find(num) != r.end())
{
return r[num]->clone();
}
return nullptr;
}
virtual USART* clone(void) const = 0;
virtual ~USART(){}
virtual bool init(uint32_t baudrate) const = 0;
virtual bool send(const uint8_t* buf, uint16_t len) const = 0;
virtual uint16_t receive(uint8_t* buf) const = 0;
};
/*!<=======Specific class USART 1==========>!*/
class USART_1 : public USART
{
private:
static USART_1 _prototype;
USART_1()
{
add_prototype( USART_NUMBER::_1, this);
}
public:
virtual USART* clone(void) const override final
{
return new USART_1;
}
virtual bool init(uint32_t baudrate) const override final;
virtual bool send(const uint8_t* buf, uint16_t len) const override final;
virtual uint16_t receive(uint8_t* buf) const override final;
};
/*!<=======Specific class USART 2==========>!*/
class USART_2 : public USART
{
private:
static USART_2 _prototype;
USART_2()
{
add_prototype( USART_NUMBER::_2, this);
}
public:
virtual USART* clone(void) const override final
{
return new USART_2;
}
virtual bool init(uint32_t baudrate) const override final;
virtual bool send(const uint8_t* buf, uint16_t len) const override final;
virtual uint16_t receive(uint8_t* buf) const override final;
};
В начале файла есть перечисление enum класс USART_NUMBER из всех имеющихся USART для моего камня их всего два.
Затем идет предварительное объявление базового класса.
класс УСАРТ .
Далее идет объявление контейнера для всех прототипов.
станд::карта и его реестр, который реализован как синглтон Майера.
Здесь я столкнулся с особенностью IAR ARM, а именно с тем, что он инициализирует статические переменные дважды, в начале программы и сразу при входе в main. Поэтому я несколько переписал синглтон, заменив статическую переменную _пример к глобальному.
Как это выглядит в идеале, описано в комментарии.
Далее объявляется базовый класс УСАРТ , где определены методы добавления прототипа, удаления прототипа, а также создания объекта (поскольку конструктор классов-потомков объявлен закрытым для ограничения доступа).
Также объявлен чисто виртуальный метод клонировать и чисто виртуальная инициализация, методы отправки и получения.
Ведь мы наследуем конкретные классы, в которых определяем описанные выше чисто виртуальные методы.
Код для определения методов приведен ниже: #include "MW_USART.h"
#include "HW_USART.h"
usart_registry _instance; //Crutch for IAR
/*!<========Initialization of global static USART value==========>!*/
USART_1 USART_1::_prototype = USART_1();
USART_2 USART_2::_prototype = USART_2();
/*!<======================UART1 functions========================>!*/
bool USART_1::init(uint32_t baudrate) const
{
bool res = false;
//res = usart_init(USART1, baudrate); //Platform depending function
return res;
}
bool USART_1::send(const uint8_t* buf, uint16_t len) const
{
bool res = false;
return res;
}
uint16_t USART_1::receive(uint8_t* buf) const
{
uint16_t len = 0;
return len;
}
/*!<======================UART2 functions========================>!*/
bool USART_2::init(uint32_t baudrate) const
{
bool res = false;
res = usart2_init(baudrate); //Platform depending function
return res;
}
bool USART_2::send(const uint8_t* buf, const uint16_t len) const
{
bool res = false;
res = usart2_write(buf, len); //Platform depending function
return res;
}
uint16_t USART_2::receive(uint8_t* buf) const
{
uint16_t len = 0;
len = usart2_read(buf); //Platform depending function
return len;
}
Здесь НЕ фиктивные методы реализованы только для USART2, поскольку именно его я использую для связи с esp8266. Соответственно контент может быть каким угодно, также его можно реализовать с помощью указателей на функции, которые принимают свое значение исходя из текущего чипа.
Теперь предлагаю перейти на уровень АПП и посмотреть, зачем все это было нужно.
ПРИЛОЖЕНИЕ_ESP8266
Я определяю базовый класс для ESP8266, используя шаблон «singleton».В нем я определяю указатель на базовый класс УСАРТ* .
class ESP8266
{
private:
ESP8266(){}
ESP8266(const ESP8266& root) = delete;
ESP8266& operator=(const ESP8266&) = delete;
/*!<---------USART settings for ESP8266------->!*/
static constexpr auto USART_BAUDRATE = ESP8266_USART_BAUDRATE;
static constexpr USART_NUMBER ESP8266_USART_NUMBER = USART_NUMBER::_2;
USART* usart;
static constexpr uint8_t LAST_COMMAND_SIZE = 32;
char last_command[LAST_COMMAND_SIZE] = {0};
bool send(uint8_t const *buf, const uint16_t len = 0);
static constexpr uint8_t ANSWER_BUF_SIZE = 32;
uint8_t answer_buf[ANSWER_BUF_SIZE] = {0};
bool receive(uint8_t* buf);
bool waiting_answer(bool (ESP8266::*scan_line)(uint8_t *));
bool scan_ok(uint8_t * buf);
bool if_str_start_with(const char* str, uint8_t *buf);
public:
bool init(void);
static ESP8266& Instance()
{
static ESP8266 esp8266;
return esp8266;
}
};
Здесь также есть переменная constexpr, в которой хранится количество используемых USART. Теперь, чтобы изменить номер USART, нам нужно всего лишь изменить его значение! Связывание происходит в функции инициализации: bool ESP8266::init(void)
{
bool res = false;
usart = USART::create_USART(ESP8266_USART_NUMBER);
usart->init(USART_BAUDRATE);
const uint8_t* init_commands[] =
{
"AT",
"ATE0",
"AT+CWMODE=2",
"AT+CIPMUX=0",
"AT+CWSAP=\"Tortoise_assistant\",\"00000000\",5,0",
"AT+CIPMUX=1",
"AT+CIPSERVER=1,8888"
};
for(const auto &command: init_commands)
{
this->send(command);
while(this->waiting_answer(&ESP8266::scan_ok)) continue;
}
return res;
}
Линия usart = USART::create_USART(ESP8266_USART_NUMBER); подключает наш прикладной уровень к определенному модулю USART.
Вместо того, чтобы делать выводы, я просто выскажу надежду, что материал кому-то будет полезен.
Спасибо за прочтение! Теги: #Программирование микроконтроллеров #stm32 #прототип #c++ #C++ #DMA #c++14 #шаблоны проектирования #usart #singleton
-
Студия Артемия Лебедева Обновила Логотип
19 Oct, 24 -
29 Марта, Москва — Backend Stories 3.0
19 Oct, 24 -
Nevermap «Россия – Родина Слонов»
19 Oct, 24