Русский
Русский
English
Статистика
Реклама

Попытка использовать современный C и паттерны проектирования для программирования микроконтроллеров

Всем привет!

Проблема использования С++ в микроконтроллерах терзала меня довольно долгое время. Дело было в том, что я искренне не понимал, как этот объектно ориентированный язык может быть применим к встраиваем системам. Я имею ввиду, как выделять классы и на базе чего составлять объекты, то есть как именно применять этот язык правильно. Спустя некоторое время и прочтения n-ого количества литературы, я пришёл к кое каким результатам, о чем и хочу поведать в этой статье. Имеют ли какую либо ценность эти результаты или нет остается на суд читателя. Мне будет очень интересно почитать критику к моему подходу, чтобы наконец ответить себе на вопрос: Как же правильно использовать C++ при программировании микроконтроллеров?.

Предупреждаю, в статье будет много исходного кода.

В этой статье, я, на примере использования USART в МК stm32 для связи с esp8266 постараюсь изложить свой подход и его основные преимущества. Начнем с того, что главное преимущество использование C++ для меня это возможность сделать аппаратную развязку, т.е. сделать использование модулей верхнего уровня независимым от аппаратной платформы. Это будет вытекать в то, что система станет легко модифицирована при каких либо изменениях. Для этого я выделил три уровня абстракции системы:

  1. HW_USART аппаратный уровень, зависит от платформы
  2. MW_USART средний уровень, служит для развязки первого и третьего уровней
  3. 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). Я не буду описывать реализацию паттерна прототип, так как его можно найти по первой ссылке в гугле, а сразу приведу исходный код, и пояснения приведу ниже.

#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 classusing 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 class USART_NUMBER со всеми доступными USART, для моего камня их всего два. Затем идёт опережающее объявление базового класса class USART. Далее идёт объявление контейнер а всех прототипов std::map<USART_NUMBER, USART*> и его реестра, который реализован в виде синглтона Мэйерса.

Тут я напоролся на особенность IAR ARM, а именно то, что он инициализирует статические переменные два раза, в начале программы и непосредственно при входе в main. Поэтому я несколько переписал синглтон, заменив статическую переменную _instance на глобальную. То, как это выглядит в идеале, описано в комментарии.

Далее объявлен базовый класс USART, где определены методы добавления прототипа, удаления прототипа, а также создания объекта(так как конструктор классов наследников объявлен как приватный, для ограничения доступа).

Также объявлен чисто виртуальный метод clone, и чисто виртуальные методы инициализации, отправки и получения.

После всего лишь, мы наследуем конкретные классы, где определяем чисто виртуальные методы, описанные выше.

Код определения методов привожу ниже:

#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. Соответственно, наполнение может быть любое, также оно может быть реализовано с помощью указателей на функции, которые принимают свое значение исходя из текущего чипа.

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

APP_ESP8266


Определяю базовый класс для ESP8266 по паттерну одиночка. В нем определяю указатель на базовый класс USART*.

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 модулем.

Вместо выводов, просто выражу надежду, что материал окажется кому-нибудь полезен. Спасибо за прочтение!
Источник: habr.com
К списку статей
Опубликовано: 31.01.2021 18:12:32
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

C++

Программирование микроконтроллеров

С++

C++14

Stm32

Usart

Dma

Паттерны проектирования

Прототип

Одиночка

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru