マイクロコントローラープログラミングに最新のC ++とデザインパターンを使用しようとしています

こんにちは!



マイクロコントローラーでC ++を使用することの問題は、かなり長い間私を悩ませてきました。重要なのは、このオブジェクト指向言語を組み込みシステムにどのように適用できるかを正直に理解していなかったということです。つまり、クラスを選択する方法と、オブジェクトを構成するための基準、つまり、この言語を正しく使用する方法です。しばらくして、n番目の量の文献を読んだ後、私はいくつかの結果に到達しました。それについては、この記事で説明したいと思います。これらの結果に価値があるかどうかは、読者次第です。「マイクロコントローラーをプログラミングするときにC ++を正しく使用するにはどうすればよいか」という質問に最終的に答えるために、私のアプローチに対する批判を読むことは非常に興味深いことです。



この記事には多くのソースコードが含まれていることに注意してください。



この記事では、MK stm32でUSARTを使用してesp8266と通信する例を使用して、私のアプローチとその主な利点の概要を説明します。私にとってC ++を使用する主な利点は、ハードウェアのデカップリングを実行できることから始めましょう。ハードウェアプラットフォームに依存しないトップレベルモジュールを使用します。これにより、変更があった場合にシステムを簡単に変更できるようになります。このために、私はシステム抽象化の3つのレベルを特定しました。



  1. HW_USART-ハードウェアレベル、プラットフォームに依存
  2. MW_USART-中間レベル、第1レベルと第3レベルを分離するのに役立ちます
  3. APP_ESP8266-アプリケーションレベル、MKについて何も知りません


HW_USART



最も原始的なレベル。stm32f411 gem、USART#2を使用し、DMAサポートも実装しました。インターフェイスは、初期化、送信、受信の3つの関数のみの形式で実装されます。



初期化関数は次のようになります。



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


      
      





まず、ファイルは、使用可能なすべてのUSARTを含む列挙クラスUSART_NUMBER列挙され ます。私の石では、それらは2つしかありません。次に、基本クラスクラスUSARTの前方宣言が行われ ます。次に、コンテナとすべてのプロトタイプstd :: map <USART_NUMBER、USART *>とそのレジストリの宣言があり ます。これは、Mayersによってシングルトンとして実装されています。



ここで、IAR ARMの機能、つまり、プログラムの開始時とmainに入るとすぐに静的変数を2回初期化するという事実に遭遇しました。したがって、静的_instance変数グローバル変数に置き換えて、シングルトンをいくらか書き直しました 。理想的には、それがどのように見えるかはコメントで説明されています。



次に、基本クラスUSARTが宣言され 、プロトタイプの追加、プロトタイプの削除、およびオブジェクトの作成のメソッドが定義されます(継承されたクラスのコンストラクターは、アクセスを制限するためにプライベートとして宣言されているため)。



純粋仮想クローンメソッドも宣言されており、初期化、送信、受信の純粋仮想メソッドも宣言され ています。



結局のところ、上記の純粋仮想メソッドを定義する具象クラスを継承します。



以下のメソッドを定義するためのコードを引用します。



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

      
      





ここでは、esp8266との通信に使用するため、USART2専用のダミーメソッドではなく実装されています。したがって、充填は任意であり、現在のチップに基づいて値をとる関数へのポインタを使用して実装することもできます。



ここで、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;
  }
};

      
      





使用されるUSARTの数を格納するconstexpr変数もあります。ここで、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モジュールに関連付けます。



結論ではなく、この資料が誰かに役立つことを願っています。読んでくれてありがとう!



All Articles