Попытка использовать современный 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 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 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 модулем.

Вместо выводов, просто выражу надежду, что материал окажется кому-нибудь полезен. Спасибо за прочтение!
Источник: https://habr.com/ru/post/540148/


Интересные статьи

Интересные статьи

Apache Wicket - это фреймворк для веб-разработки на Java. Я чувствую, что ему не уделяют столько внимания, сколько он того заслуживает. Я профессионально использую Wicket для реальных про...
Меня зовут Назим Гафаров, я разработчик интерфейсов в Mail.ru Cloud Solutions. На дворе 2020 год, а мы продолжаем обсуждать «нововведения» ES6-синтаксиса и преимущества MobX над Redu...
Холивар о том, нужно ли использовать для создания игр движки, начался сразу после появления первых игровых движков. Этот пост на reddit не является идеальным примером разумных контраргументов про...
Какой маршрут будет самым безопасным, где больше всего врагов и где ближайшая аптечка? Все эти часто встречающиеся задачи о пространственных связях можно эффективно решать при помощи математиче...
Автор материала, перевод которого мы сегодня публикуем, говорит, что одна из проблем, с которыми приходится сталкиваться программистам, заключается в том, что у них их код работает, а у кого-то д...