Essayer d'utiliser des modèles de conception et C ++ modernes pour la programmation de microcontrôleurs

salut!



Le problème de l'utilisation de C ++ dans les microcontrôleurs me tourmente depuis un certain temps. Le fait était que je ne comprenais honnêtement pas comment ce langage orienté objet pouvait être appliqué aux systèmes embarqués. Je veux dire, comment sélectionner des classes et sur quelle base composer des objets, c'est-à-dire comment utiliser correctement ce langage. Après un certain temps et après avoir lu la n-ième quantité de littérature, je suis arrivé à quelques résultats, dont je veux vous parler dans cet article. Que ces résultats aient une quelconque valeur ou non, c'est au lecteur. Il sera très intéressant pour moi de lire la critique de ma démarche afin de me répondre enfin à la question: "Comment utiliser correctement C ++ lors de la programmation de microcontrôleurs?"



Attention, cet article contiendra beaucoup de code source.



Dans cet article, j'essaierai, en utilisant l'exemple d'utilisation d'USART dans MK stm32 de communiquer avec esp8266, de décrire mon approche et ses principaux avantages. Commençons par le fait que le principal avantage de l'utilisation de C ++ pour moi est la possibilité de faire un découplage matériel, c'est-à-dire utiliser des modules de premier niveau indépendants de la plate-forme matérielle. Cela se traduira par le fait que le système deviendra facilement modifiable en cas de changement. Pour cela, j'ai identifié trois niveaux d'abstraction système:



  1. HW_USART - niveau matériel, dépendant de la plate-forme
  2. MW_USART - niveau intermédiaire, sert à découpler les premier et troisième niveaux
  3. APP_ESP8266 - niveau application, ne sait rien sur MK


HW_USART



Le niveau le plus primitif. J'ai utilisé le gem stm32f411, USART # 2, également implémenté le support DMA. L'interface est implémentée sous la forme de seulement trois fonctions: initialiser, envoyer, recevoir.



La fonction d'initialisation ressemble à ceci:



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

      
      





Il n'y a rien de spécial dans la fonction, sauf peut-être que j'utilise des masques de bits pour réduire le code résultant.



Ensuite, la fonction d'envoi ressemble à ceci:



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 fonction a une béquille, sous la forme de la variable first_attempt, qui permet de déterminer s'il s'agit du tout premier envoi via DMA ou non. Pourquoi est-ce nécessaire? Le fait est que j'ai vérifié si l'envoi précédent à DMA a réussi ou non AVANT l'envoi, pas APRÈS. J'ai fait en sorte qu'après l'envoi des données, il ne soit pas stupide d'attendre la fin, mais d'exécuter du code utile à ce moment.



Ensuite, la fonction de réception ressemble à ceci:



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 particularité de cette fonction est que je ne sais pas à l'avance combien d'octets je devrais recevoir. Pour indiquer les données reçues, je vérifie le drapeau IDLE, puis, si l'état IDLE est fixe, j'efface le drapeau et lis les données du tampon. Si l'état IDLE n'est pas fixe, la fonction renvoie simplement zéro, c'est-à-dire aucune donnée.



À ce stade, je propose de terminer par un bas niveau et de passer directement au C ++ et aux modèles.



MW_USART



Ici, j'ai implémenté la classe USART abstraite de base et appliqué le modèle "prototype" pour créer des descendants (les classes USART1 et USART2 concrètes). Je ne décrirai pas l'implémentation du modèle prototype, car il peut être trouvé au premier lien dans Google, mais je vais immédiatement donner le code source et expliquer ci-dessous.



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


      
      





Tout d'abord, le fichier est énuméré enum class USART_NUMBER avec tous les USART disponibles, pour ma pierre, il n'y en a que deux. Vient ensuite la déclaration directe de la classe de base USART . Vient ensuite la déclaration du conteneur et de tous les prototypes std :: map <USART_NUMBER, USART *> et son registre, qui est implémenté comme un singleton par Mayers.



Ici, je suis tombé sur une fonctionnalité d'IAR ARM, à savoir le fait qu'il initialise deux fois les variables statiques, au début du programme et immédiatement en entrant dans main. Par conséquent, j'ai quelque peu réécrit le singleton, en remplaçant la variable statique _instance par une variable globale. Idéalement, son apparence est décrite dans le commentaire.



Ensuite, la classe de base USART est déclarée , où les méthodes pour ajouter un prototype, supprimer un prototype et créer un objet sont définies (puisque le constructeur des classes héritées est déclaré privé pour restreindre l'accès).



Une méthode de clonage purement virtuelle est également déclarée , et des méthodes purement virtuelles d'initialisation, d'envoi et de réception.



Après tout, nous héritons de classes concrètes, où nous définissons des méthodes purement virtuelles décrites ci-dessus.



Je cite le code pour définir les méthodes ci-dessous:



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

      
      





Ici sont implémentées des méthodes PAS factices uniquement pour USART2, car je l'utilise pour communiquer avec esp8266. En conséquence, le remplissage peut être quelconque, il peut également être mis en œuvre à l'aide de pointeurs vers des fonctions qui prennent leur valeur en fonction de la puce actuelle.



Maintenant, je propose d'aller au niveau de l'APP et de voir pourquoi tout cela était nécessaire.



APP_ESP8266



Je définis la classe de base pour l'ESP8266 selon le modèle "singleton". J'y définis un pointeur vers la classe USART * de base .



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

      
      





Il existe également une variable constexpr qui stocke le numéro de l'USART utilisé. Maintenant, pour changer le numéro USART, il suffit de changer sa valeur! La liaison a lieu dans la fonction d'initialisation:



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

      
      





Ligne usart = USART :: create_USART (ESP8266_USART_NUMBER); associe notre couche applicative à un module USART spécifique.



Au lieu de conclusions, j'exprime simplement l'espoir que le matériel sera utile à quelqu'un. Merci d'avoir lu!



All Articles