The problem of using C ++ in microcontrollers has been plaguing me for quite some time. The point was, I honestly didn't understand how this object-oriented language could be applied to embedded systems. I mean, how to select classes and on what basis to compose objects, that is, how exactly to use this language correctly. After some time and reading the nth amount of literature, I came to some results, which I want to tell about in this article. Whether these results are of any value or not is up to the reader. It will be very interesting for me to read the criticism of my approach in order to finally answer myself the question: "How to use C ++ correctly when programming microcontrollers?"
Be warned, this article will contain a lot of source code.
In this article, using the example of using USART in MK stm32 to communicate with esp8266, I will try to outline my approach and its main advantages. Let's start with the fact that the main advantage of using C ++ for me is the ability to do hardware decoupling, i.e. make the use of top-level modules independent of the hardware platform. This will result in the fact that the system will become easily modifiable in case of any changes. For this, I have identified three levels of system abstraction:
- HW_USART - hardware level, platform dependent
- MW_USART - middle level, serves to decouple the first and third levels
- APP_ESP8266 - application level, knows nothing about MK
HW_USART
The most primitive level. I used stm32f411 gem, USART # 2, also implemented DMA support. The interface is implemented in the form of only three functions: initialize, send, receive.
The initialization function looks like this:
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;
}
There is nothing special in the function, except perhaps that I use bit masks to reduce the resulting code.
Then the send function looks like this:
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;
}
The function has a crutch, in the form of the first_attempt variable, which helps to determine whether it is the very first send via DMA or not. Why is this needed? The fact is that I checked whether the previous send to DMA was successful or not BEFORE sending, not AFTER. I made it so that after sending the data it is not stupid to wait for it to complete, but to execute useful code at this time.
Then the receive function looks like this:
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;
}
The peculiarity of this function is that I do not know in advance how many bytes I should receive. To indicate the received data, I check the IDLE flag, then, if the IDLE state is fixed, I clear the flag and read the data from the buffer. If the IDLE state is not fixed, then the function simply returns zero, that is, no data.
At this point, I propose to end with a low level and go directly to C ++ and patterns.
MW_USART
Here I have implemented the base abstract USART class and applied the "prototype" pattern to create descendants (the concrete USART1 and USART2 classes). I will not describe the implementation of the prototype pattern, since it can be found at the first link in Google, but I will immediately give the source code, and explain below.
#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;
};
First, the file is enumerated enum class USART_NUMBER with all available USARTs, for my stone there are only two of them. Then comes the forward declaration of the base class class USART . Next comes the declaration of the container and of all prototypes std :: map <USART_NUMBER, USART *> and its registry, which is implemented as a singleton Mayers.
Here I ran into a feature of IAR ARM, namely the fact that it initializes static variables two times, at the beginning of the program and immediately upon entering main. Therefore, I rewrote the singleton somewhat, replacing the static _instance variable with a global one. Ideally, how it looks is described in the comment.
Next, the base class USART is declared , where methods for adding a prototype, removing a prototype, and creating an object are defined (since the constructor of inherited classes is declared as private to restrict access).
A purely virtual clone method is also declared , and purely virtual methods of initialization, sending and receiving.
After all, we inherit concrete classes, where we define purely virtual methods described above.
I cite the code for defining the methods below:
#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;
}
Here are implemented NOT dummy methods only for USART2, as I use it to communicate with esp8266. Accordingly, the filling can be any, it can also be implemented using pointers to functions that take their value based on the current chip.
Now I propose to go to the APP level and see why all this was needed.
APP_ESP8266
I define the base class for the ESP8266 according to the "singleton" pattern. In it I define a pointer to the base USART * class .
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;
}
};
There is also a constexpr variable that stores the number of the USART used. Now, to change the USART number, we just need to change its value! The binding takes place in the initialization function:
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;
}
Line usart = USART :: create_USART (ESP8266_USART_NUMBER); associates our application layer with a specific USART module.
Instead of conclusions, I just express the hope that the material will be useful to someone. Thanks for reading!