Das Problem der Verwendung von C ++ in Mikrocontrollern plagt mich schon seit einiger Zeit. Der Punkt war, ich habe ehrlich gesagt nicht verstanden, wie diese objektorientierte Sprache auf eingebettete Systeme angewendet werden kann. Ich meine, wie man Klassen auswählt und auf welcher Basis Objekte erstellt werden, dh wie genau diese Sprache richtig verwendet wird. Nach einiger Zeit und dem Lesen der n-ten Menge an Literatur kam ich zu einigen Ergebnissen, über die ich Ihnen in diesem Artikel erzählen möchte. Ob diese Ergebnisse von Wert sind oder nicht, liegt beim Leser. Es wird für mich sehr interessant sein, die Kritik an meinem Ansatz zu lesen, um mir endlich die Frage zu beantworten: "Wie verwende ich C ++ richtig, wenn ich Mikrocontroller programmiere?"
Seien Sie gewarnt, dieser Artikel enthält viel Quellcode.
In diesem Artikel werde ich am Beispiel der Verwendung von USART in MK stm32 für die Kommunikation mit esp8266 versuchen, meinen Ansatz und seine Hauptvorteile zu skizzieren. Beginnen wir mit der Tatsache, dass der Hauptvorteil der Verwendung von C ++ für mich die Fähigkeit zur Hardware-Entkopplung ist, d. H. Machen Sie die Verwendung von Top-Level-Modulen unabhängig von der Hardwareplattform. Dies führt dazu, dass das System im Falle von Änderungen leicht geändert werden kann. Dafür habe ich drei Ebenen der Systemabstraktion identifiziert:
- HW_USART - Hardwareebene, plattformabhängig
- MW_USART - mittlere Ebene, dient zum Entkoppeln der ersten und dritten Ebene
- APP_ESP8266 - Anwendungsebene, weiß nichts über MK
HW_USART
Die primitivste Ebene. Ich habe stm32f411 gem, USART # 2, verwendet und auch DMA-Unterstützung implementiert. Die Schnittstelle ist nur in Form von drei Funktionen implementiert: Initialisieren, Senden, Empfangen.
Die Initialisierungsfunktion sieht folgendermaßen aus:
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;
}
Die Funktion enthält nichts Besonderes, außer dass ich möglicherweise Bitmasken verwende, um den resultierenden Code zu reduzieren.
Dann sieht die Sendefunktion folgendermaßen aus:
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;
}
Die Funktion verfügt über eine Krücke in Form der Variablen first_attempt, mit deren Hilfe festgestellt werden kann, ob es sich um den allerersten Versand per DMA handelt oder nicht. Warum wird das benötigt? Tatsache ist, dass ich vor dem Senden überprüft habe, ob der vorherige Sendevorgang an DMA erfolgreich war oder nicht, nicht NACHHER. Ich habe es so gemacht, dass es nach dem Senden der Daten nicht dumm ist, auf den Abschluss zu warten, sondern zu diesem Zeitpunkt nützlichen Code auszuführen.
Dann sieht die Empfangsfunktion folgendermaßen aus:
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;
}
Die Besonderheit dieser Funktion ist, dass ich nicht im Voraus weiß, wie viele Bytes ich empfangen soll. Um die empfangenen Daten anzuzeigen, überprüfe ich das IDLE-Flag. Wenn der IDLE-Status festgelegt ist, lösche ich das Flag und lese die Daten aus dem Puffer. Wenn der IDLE-Status nicht festgelegt ist, gibt die Funktion einfach Null zurück, dh keine Daten.
An dieser Stelle schlage ich vor, mit einem niedrigen Level zu enden und direkt zu C ++ und Mustern zu gehen.
MW_USART
Hier habe ich die abstrakte USART-Basisklasse implementiert und das "Prototyp" -Muster angewendet, um Nachkommen zu erstellen (die konkreten USART1- und USART2-Klassen). Ich werde die Implementierung des Prototypmusters nicht beschreiben, da es unter dem ersten Link in Google zu finden ist, aber ich werde sofort den Quellcode angeben und unten erklären.
#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;
};
Erstens wird die Datei mit allen verfügbaren USARTs in der Aufzählungsklasse USART_NUMBER aufgelistet, für meinen Stein gibt es nur zwei davon. Dann kommt die Vorwärtsdeklaration der Basisklassenklasse USART . Als nächstes folgt die Deklaration des Containers und aller Prototypen std :: map <USART_NUMBER, USART *> und seiner Registrierung, die von Mayers als Singleton implementiert wird.
Hier bin ich auf eine Funktion von IAR ARM gestoßen, nämlich die Tatsache, dass statische Variablen zweimal initialisiert werden, zu Beginn des Programms und unmittelbar nach Eingabe von main. Daher habe ich den Singleton etwas umgeschrieben und die statische Variable _instance durch eine globale Variable ersetzt. Wie es aussieht, wird im Idealfall im Kommentar beschrieben.
Als nächstes wird die Basisklasse USART deklariert , in der Methoden zum Hinzufügen eines Prototyps, Löschen eines Prototyps und Erstellen eines Objekts definiert werden (da der Konstruktor der geerbten Klassen als privat deklariert wird, um den Zugriff einzuschränken).
Es wird auch eine rein virtuelle Klonmethode deklariert und rein virtuelle Methoden zum Initialisieren, Senden und Empfangen.
Schließlich erben wir konkrete Klassen, in denen wir die oben beschriebenen rein virtuellen Methoden definieren.
Ich zitiere den Code zur Definition der folgenden Methoden:
#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;
}
Hier sind NICHT Dummy-Methoden nur für USART2 implementiert, da ich sie zur Kommunikation mit esp8266 verwende. Dementsprechend kann die Füllung beliebig sein, sie kann auch unter Verwendung von Zeigern auf Funktionen implementiert werden, deren Wert basierend auf dem aktuellen Chip angenommen wird.
Jetzt schlage ich vor, auf die APP-Ebene zu gehen und zu sehen, warum dies alles benötigt wurde.
APP_ESP8266
Ich definiere die Basisklasse für den ESP8266 nach dem "Singleton" -Muster. Darin definiere ich einen Zeiger auf die Basisklasse 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;
}
};
Es gibt auch eine constexpr-Variable, die die Nummer des verwendeten USART speichert. Um die USART-Nummer zu ändern, müssen wir nur ihren Wert ändern! Die Bindung erfolgt in der Initialisierungsfunktion:
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;
}
Zeile usart = USART :: create_USART (ESP8266_USART_NUMBER); ordnet unsere Anwendungsschicht einem bestimmten USART-Modul zu.
Anstelle von Schlussfolgerungen drücke ich nur die Hoffnung aus, dass das Material für jemanden nützlich sein wird. Danke fürs Lesen!