Intentando utilizar C ++ moderno y patrones de diseño para la programación de microcontroladores

¡Hola!



El problema de usar C ++ en microcontroladores me ha estado atormentando durante bastante tiempo. El punto era que, sinceramente, no entendía cómo se podía aplicar este lenguaje orientado a objetos a sistemas integrados. Quiero decir, cómo seleccionar clases y sobre qué base componer objetos, es decir, cómo exactamente usar este lenguaje correctamente. Después de un tiempo y de leer la enésima cantidad de literatura, llegué a algunos resultados, de los que quiero hablarles en este artículo. Si estos resultados tienen algún valor o no, depende del lector. Será muy interesante para mí leer la crítica de mi enfoque para finalmente responderme a la pregunta: "¿Cómo usar C ++ correctamente al programar microcontroladores?"



Tenga cuidado, este artículo contendrá una gran cantidad de código fuente.



En este artículo, yo, usando el ejemplo de uso de USART en MK stm32 para comunicarme con esp8266, intentaré describir mi enfoque y sus principales ventajas. Comencemos con el hecho de que la principal ventaja de usar C ++ para mí es la capacidad de desacoplar hardware, es decir, hacer que el uso de módulos de nivel superior sea independiente de la plataforma de hardware. Esto dará como resultado el hecho de que el sistema se volverá fácilmente modificable en caso de cambios. Para ello, he identificado tres niveles de abstracción del sistema:



  1. HW_USART: nivel de hardware, dependiente de la plataforma
  2. MW_USART - nivel medio, sirve para desacoplar el primer y tercer nivel
  3. APP_ESP8266 - nivel de aplicación, no sabe nada sobre MK


HW_USART



El nivel más primitivo. Usé stm32f411 gem, USART # 2, también implementé soporte DMA. La interfaz se implementa en forma de solo tres funciones: inicializar, enviar, recibir.



La función de inicialización se ve así:



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;
}

      
      





No hay nada especial en la función, excepto quizás que uso máscaras de bits para reducir el código resultante.



Entonces, la función de envío se ve así:



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;
}

      
      





La función tiene una muleta, en forma de la variable first_attempt, que ayuda a determinar si es el primer envío a través de DMA o no. ¿Por qué es necesario? El hecho es que verifiqué si el envío anterior a DMA fue exitoso o no ANTES de enviar, no DESPUÉS. Lo hice para que después de enviar los datos no sea estúpido esperar a que se complete, sino ejecutar código útil en este momento.



Entonces, la función de recepción se ve así:



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;
}

      
      





La peculiaridad de esta función es que no sé de antemano cuántos bytes debo recibir. Para indicar los datos recibidos, verifico la bandera IDLE, luego, si el estado IDLE es fijo, borro la bandera y leo los datos del búfer. Si el estado IDLE no es fijo, la función simplemente devuelve cero, es decir, sin datos.



En este punto, propongo terminar con un nivel bajo e ir directamente a C ++ y patrones.



MW_USART



Aquí implementé la clase USART abstracta base y apliqué el patrón "prototipo" para crear descendientes (las clases concretas USART1 y USART2). No describiré la implementación del patrón prototipo, ya que se puede encontrar en el primer enlace de Google, pero daré inmediatamente el código fuente y lo explicaré a continuación.



#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;
};


      
      





Primero, el archivo se enumera en la clase de enumeración USART_NUMBER con todos los USART disponibles, para mi piedra solo hay dos de ellos. Luego viene la declaración hacia adelante de la clase base USART . Luego viene la declaración del contenedor y de todos los prototipos std :: map <USART_NUMBER, USART *> y su registro, que se implementa como singleton por Mayers.



Aquí me encontré con una característica de IAR ARM, a saber, el hecho de que inicializa las variables estáticas dos veces, al comienzo del programa e inmediatamente después de ingresar a main. Por lo tanto, reescribí un poco el singleton, reemplazando la variable _instance estática por una global. Idealmente, cómo se ve se describe en el comentario.



A continuación, se declara la clase base USART , donde se definen los métodos para agregar un prototipo, eliminar un prototipo y crear un objeto (ya que el constructor de las clases heredadas se declara como privado para restringir el acceso).



También se declara un método de clonación puramente virtual y métodos puramente virtuales de inicialización, envío y recepción.



Después de todo, heredamos clases concretas, donde definimos métodos puramente virtuales descritos anteriormente.



Cito el código para definir los métodos a continuación:



#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;
}

      
      





Aquí NO se implementan métodos ficticios solo para USART2, ya que lo uso para comunicarme con esp8266. En consecuencia, el relleno puede ser cualquiera, también se puede implementar usando punteros a funciones que toman su valor en función del chip actual.



Ahora propongo ir al nivel de la aplicación y ver por qué se necesitaba todo esto.



APP_ESP8266



Defino la clase base para el ESP8266 de acuerdo con el patrón "singleton". En él defino un puntero a la clase base 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;
  }
};

      
      





También hay una variable constexpr que almacena el número de USART utilizado. Ahora, para cambiar el número USART, ¡solo necesitamos cambiar su valor! El enlace tiene lugar en la función de inicialización:



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;
}

      
      





Línea usart = USART :: create_USART (ESP8266_USART_NUMBER); asocia nuestra capa de aplicación con un módulo USART específico.



En lugar de conclusiones, solo expreso la esperanza de que el material sea útil para alguien. ¡Gracias por leer!



All Articles