Creation of hardware-independent libraries for microcontrollers
Contents
Content
-
Introduction
-
Description of the problem
-
Implementation of the basic driver
-
Implementation of the library for the LED matrix module
-
Examples of use
-
Demonstration of results
-
Conclusion
1. Introduction
In this article, I would like to show you how you can create your own hardware-independent microcontroller libraries to work with digital chips.
The point of creating a hardware-independent library is to turn away from the level of abstraction (libraries and frameworks) provided by the microcontroller manufacturer, inside the implemented library. For example, for STM32 – HAL, ESP32 – ESP-IDF or Arduino, for AVR, Arduino is most often used. This will allow using the same library on different microcontrollers (and not only) without changing the library code for each stone.
Most chips work through digital interfaces (UART, SPI, I2C, etc.). With the help of these interfaces, we interact with the registers of the chip and get a certain result. To do this, it is enough to describe several functions of working with the interface and transfer pointers to these functions to our library. This means that in the implementation of the library itself, you can describe only the logic of work and provide an interface for working with almost any microcontroller at the output.
How this can be implemented, I will explain using the example of a fairly simple MAX7219 microcircuit and an LED matrix module based on it. I think many people are familiar with this microcircuit and have seen LED matrix modules and seven-segment indicators based on it. In the course of implementation, I will not go into detail about how the microcircuit works, you either already know all this, or you can find it in the documentation.
2. Description of the problem
Note: All code in this section is for STM32 and was found in GitHub repositories.
When I search the Internet for ready-made libraries, basically everything I find looks like this:
The .h files of the library define and external HAL handlers, pins and ports.
#define NUMBER_OF_DIGITS 8
#define SPI_PORT hspi1
extern SPI_HandleTypeDef SPI_PORT;
#define MAX7219_CS_Pin GPIO_PIN_6
#define MAX7219_CS_GPIO_Port GPIOA
…
void max7219_Init();
void max7219_SetIntensivity(uint8_t intensivity);
…
void max7219_SendData(uint8_t addr, uint8_t data);
…
In .c files, HAL headers for a specific stone are included, and accordingly the entire library works on HAL.
#include "stm32f1xx_hal.h"
#include
…
void max7219_Init() {
max7219_TurnOff();
max7219_SendData(REG_SCAN_LIMIT, NUMBER_OF_DIGITS - 1);
max7219_SendData(REG_DECODE_MODE, 0x00);
max7219_Clean();
}
void max7219_SetIntensivity(uint8_t intensivity) {
if (intensivity > 0x0F)
return;
max7219_SendData(REG_INTENSITY, intensivity);
}
…
void max7219_SendData(uint8_t addr, uint8_t data) {
HAL_GPIO_WritePin(MAX7219_CS_GPIO_Port, MAX7219_CS_Pin, GPIO_PIN_RESET);
HAL_SPI_Transmit(&hspi1, &addr, 1, HAL_MAX_DELAY);
HAL_SPI_Transmit(&hspi1, &data, 1, HAL_MAX_DELAY);
HAL_GPIO_WritePin(MAX7219_CS_GPIO_Port, MAX7219_CS_Pin, GPIO_PIN_SET);
}
Cons of this approach:
-
The library can only work with one connected module.
-
Dependency on HAL.
-
When connecting the library to your project, you need to go into the library files and configure them for yourself.
-
It will be problematic to transfer the library to microcontrollers of other manufacturers.
What I suggest to do:
First, to describe a framework that will allow us to create multiple instances of pluggable modules. This approach will solve the first problem.
Second, to avoid using the HAL inside the library, we can create function pointers to work with the interface and ports we need. It will be necessary to describe the implementation of the function yourself in the main program and transfer a pointer to it to the library. This will solve three problems.
All this applies not only to STM32 and HAL, but also to all other implementations of libraries for a specific microcontroller, created according to this principle.
3. Implementation of the basic driver
First, let’s create a small driver for working with the registers of the microcircuit. In the main application, the developer should not use this driver. This driver will describe the lowest level of abstraction, and then a library will be written based on this driver, which the developer can use in the project.
The first place to start is to create a pair of files max7219.h and max7219.c.
In the file max7219.h, we define the registers of the microcircuit:
#define REG_NOOP 0x00
#define REG_DIGIT_0 0x01
#define REG_DIGIT_1 0x02
#define REG_DIGIT_2 0x03
#define REG_DIGIT_3 0x04
#define REG_DIGIT_4 0x05
#define REG_DIGIT_5 0x06
#define REG_DIGIT_6 0x07
#define REG_DIGIT_7 0x08
#define REG_DECODE_MODE 0x09
#define REG_INTENSITY 0x0A
#define REG_SCAN_LIMIT 0x0B
#define REG_SHUTDOWN 0x0C
#define REG_DISPLAY_TEST 0x0F
Next, we will create pointers to functions for working with SPI:
typedef void (*SPI_Transmit)(uint8_t* data, size_t size);
typedef void (*SPI_ChipSelect)(uint8_t level);
In the main program, you need to describe these functions yourself. The SPI_Transmit function will have to transmit an array of data bytes of size according to our chosen SPI. SPI_ChipSelect should toggle the state of the CS pin according to the passed level parameter.
Next, we define the structure that will describe our microcircuit.
typedef struct{
SPI_Transmit spiTransmit;
SPI_ChipSelect spiChipSelect;
}MAX7219_st;
In this case, there will be enough fields that accept pointers to SPI functions. The matrix data buffer will be described at the next level of abstraction.
Finally, let’s define the main functions:
void MAX7219_Init(MAX7219_st* max7219, SPI_Transmit spiTransmit,
SPI_ChipSelect spiChipSelect);
void MAX7219_WriteReg(MAX7219_st* max7219, MAX7219_Register_t reg, uint8_t data);
In the MAX7219_Init initialization function, we will pass a pointer to the MAX7219_st structure and pointers to SPI functions, which we will describe in the main program.
Let’s go to the max7219.c file.
#include "max7219.h"
void MAX7219_Init(MAX7219_st* max7219, SPI_Transmit spiTransmit,
SPI_ChipSelect spiChipSelect){
max7219->spiTransmit = spiTransmit;
max7219->spiChipSelect = spiChipSelect;
}
void MAX7219_WriteReg(MAX7219_st* max7219, MAX7219_Register_t reg, uint8_t data){
if(max7219->spiChipSelect != NULL){
max7219->spiChipSelect(0);
}
max7219->spiTransmit(®, 1);
max7219->spiTransmit(&data, 1);
if(max7219->spiChipSelect != NULL){
max7219->spiChipSelect(1);
}
}
In MAX7219_Init, we simply assign pointers to the field functions of the transferred structure. In MAX7219_WriteReg, we call the functions of sending data via SPI. There is one nuance with SPI_ChipSelect. The fact is that in some microcontrollers it is possible to configure automatic switching of the CS pin, in this case there is no need to programmatically switch this pin. If you configure your SPI like this, you can simply pass NULL to the spiChipSelect parameter during initialization.
This completes the implementation of the basic driver. In the next section, we implement a higher-level logic for working with an LED matrix using a driver.
4. Implementation of the library for the LED matrix module
At this stage, we will create a library that will provide a convenient interface for the developer to work with the LED matrix.
Let’s create a pair of MatrixLed.h and MatrixLed.c files.
In MatrixLed.h, we will connect the previously created max7219 driver and describe the structure of the matrix module.
#include "max7219.h"
#define MATRIX_SIZE 8
typedef struct{
MAX7219_st max7219;
uint8_t displayBuffer[MATRIX_SIZE];
}MatrixLed_st;
The MatrixLed_st structure contains an instance of the MAX7219_st driver and an image buffer on the matrix.
Next, we will announce the following functions:
void MatrixLed_Init(MatrixLed_st* matrixLed, SPI_Transmit spiTransmit,
SPI_ChipSelect spiChipSelect);
void MatrixLed_SetPixel(MatrixLed_st* matrixLed, uint8_t x, uint8_t y,
uint8_t state);
void MatrixLed_DrawDisplay(MatrixLed_st* matrixLed);
In MatrixLed_Init, we pass a pointer to the MatrixLed_st structure and pointers to SPI functions.
Using MatrixLed_SetPixel, we will set the state of the pixel by coordinates. This function does not toggle the state of the LEDs immediately, there will be a separate function for that.
MatrixLed_DrawDisplay is needed to update the state of the LEDs.
Let’s go to MatrixLed.c.
void MatrixLed_Init(MatrixLed_st* matrixLed, SPI_Transmit spiTransmit,
SPI_ChipSelect spiChipSelect){
matrixLed->max7219.spiTransmit = spiTransmit;
matrixLed->max7219.spiChipSelect = spiChipSelect;
MAX7219_WriteReg(&matrixLed->max7219, REG_NOOP, 0x00);
MAX7219_WriteReg(&matrixLed->max7219, REG_SHUTDOWN, 0x01);
MAX7219_WriteReg(&matrixLed->max7219, REG_DISPLAY_TEST, 0x00);
MAX7219_WriteReg(&matrixLed->max7219, REG_DECODE_MODE, 0x00);
MAX7219_WriteReg(&matrixLed->max7219, REG_INTENSITY, 0x00);
MAX7219_WriteReg(&matrixLed->max7219, REG_SCAN_LIMIT, 0x07);
MAX7219_WriteReg(&matrixLed->max7219, REG_DIGIT_0, 0x00);
MAX7219_WriteReg(&matrixLed->max7219, REG_DIGIT_1, 0x00);
MAX7219_WriteReg(&matrixLed->max7219, REG_DIGIT_2, 0x00);
MAX7219_WriteReg(&matrixLed->max7219, REG_DIGIT_3, 0x00);
MAX7219_WriteReg(&matrixLed->max7219, REG_DIGIT_4, 0x00);
MAX7219_WriteReg(&matrixLed->max7219, REG_DIGIT_5, 0x00);
MAX7219_WriteReg(&matrixLed->max7219, REG_DIGIT_6, 0x00);
MAX7219_WriteReg(&matrixLed->max7219, REG_DIGIT_7, 0x00);
}
In the initialization function, we accept the pointers of the SPI operation functions, configure the module and turn off all the LEDs on the matrix.
void MatrixLed_SetPixel(MatrixLed_st* matrixLed, uint8_t x, uint8_t y,
uint8_t state){
if(state){
matrixLed->displayBuffer[y] |= (0x80 >> x);
}
else{
matrixLed->displayBuffer[y] &= ~(0x80 >> x);
}
}
MatrixLed_SetPixel sets the necessary bits in the matrix image buffer according to the transferred coordinates.
void MatrixLed_DrawDisplay(MatrixLed_st* matrixLed){
MAX7219_WriteReg(&matrixLed->max7219, REG_DIGIT_0,
matrixLed->displayBuffer[0]);
MAX7219_WriteReg(&matrixLed->max7219, REG_DIGIT_1,
matrixLed->displayBuffer[1]);
MAX7219_WriteReg(&matrixLed->max7219, REG_DIGIT_2,
matrixLed->displayBuffer[2]);
MAX7219_WriteReg(&matrixLed->max7219, REG_DIGIT_3,
matrixLed->displayBuffer[3]);
MAX7219_WriteReg(&matrixLed->max7219, REG_DIGIT_4,
matrixLed->displayBuffer[4]);
MAX7219_WriteReg(&matrixLed->max7219, REG_DIGIT_5,
matrixLed->displayBuffer[5]);
MAX7219_WriteReg(&matrixLed->max7219, REG_DIGIT_6,
matrixLed->displayBuffer[6]);
MAX7219_WriteReg(&matrixLed->max7219, REG_DIGIT_7,
matrixLed->displayBuffer[7]);
}
MatrixLed_DrawDisplay writes data from the buffer to the microcircuit registers.
5. Examples of use
For example, we will implement the same algorithm on different microcontrollers.
Task: cyclically turn on the LEDs diagonally starting from the lower left corner to the upper right with a period of 1 second.
In all examples, the code will be practically the same. The main differences will be only in the implementation of SPI functions for a specific microcontroller. The demonstration of the results is shown in clause 6. Demonstration of results.
5.1. An example of use on an STM32 microcontroller
For an example, we will use a debugging board based on STM32F401. Let’s create a new project in CubeIDE and configure SPI.
Crucifixion:
MAX7219 |
STM32 |
---|---|
VCC |
3V3 |
GND |
GND |
DIN |
PA7 |
CS |
PA4 |
CLK |
PA5 |
Next, in main.c, we will describe the following code fragment:
#include "main.h"
#include "MatrixLed.h"
SPI_HandleTypeDef hspi1;
MatrixLed_st matrixLed;
void MatrixLed_SPI_ChipSelect (uint8_t level){
HAL_GPIO_WritePin(SPI1_CS1_GPIO_Port, SPI1_CS1_Pin, level);
}
void MatrixLed_SPI_Transmit (uint8_t* data, size_t size){
HAL_SPI_Transmit(&hspi1, data, size, 10);
}
int main(void)
{
MatrixLed_Init(&matrixLed, MatrixLed_SPI_Transmit, MatrixLed_SPI_ChipSelect);
while (1)
{
uint8_t x = 0;
uint8_t y = 0;
while(x
MatrixLed_SPI_ChipSelect sets the desired level on the CS pin according to the passed parameter. MatrixLed_SPI_Transmit sends the transmitted SPI buffer. Pointers to these functions are passed to MatrixLed_Init. In the cycle, the LEDs are lit according to the problem set in the example.
5.2 Example of using the ESP32 microcontroller
For an example, we will use a debugging board based on ESP32C3. Let’s create a new project in ESP-IDE, and configure SPI.
Crucifixion:
MAX7219 |
ESP32 |
---|---|
VCC |
3V3 |
GND |
GND |
DIN |
GPIO4 |
CS |
GPIO3 |
CLK |
GPIO2 |
Main.c code:
#include
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "MatrixLed.h"
#define MOSI_PIN GPIO_NUM_4
#define CS_PIN GPIO_NUM_3
#define CLK_PIN GPIO_NUM_2
spi_device_handle_t spi2;
MatrixLed_st matrixLed;
void MatrixLed_SPI_Transmit(uint8_t* data, size_t size){
spi_transaction_t transaction = {
.tx_buffer = data,
.length = size * 8
};
spi_device_polling_transmit(spi2, &transaction);
}
static void SPI_Init() {
spi_bus_config_t buscfg={
.miso_io_num = -1,
.mosi_io_num = MOSI_PIN,
.sclk_io_num = CLK_PIN,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 8,
};
spi_device_interface_config_t devcfg={
.clock_speed_hz = 1000000,
.mode = 0,
.spics_io_num = CS_PIN,
.queue_size = 1,
.flags = SPI_DEVICE_HALFDUPLEX,
.pre_cb = NULL,
.post_cb = NULL,
};
spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO);
spi_bus_add_device(SPI2_HOST, &devcfg, &spi2);
};
void app_main(void)
{
SPI_Init();
MatrixLed_Init(&matrixLed, MatrixLed_SPI_Transmit, NULL);
while (1) {
uint8_t x = 0;
uint8_t y = 0;
while(x
The implementation of the MatrixLed_SPI_Transmit function is similar to the STM32 example, but in this case you can not implement the MatrixLed_SPI_ChipSelect function because SPI is configured to automatically drive the CS pin. The task implementation code has changed, except for the delay function.
5.3 Example of use on an AVR microcontroller
For an example, we will use a debugging board based on Atmega328. Let’s create a new project in PlatformIO and configure SPI. The project was created on the basis of Arduino, but the implementation will not use Arduino functions except delay().
Crucifixion:
MAX7219 |
Atmega328 |
---|---|
VCC |
3V3 |
GND |
GND |
DIN |
PB3 |
CS |
PB2 |
CLK |
PB5 |
Main.c code:
#include "MatrixLed.h"
MatrixLed_st matrixLed;
void MatrixLed_SPI_ChipSelect(uint8_t level){
if(!level){
PORTB &= ~(0x04);
}
else{
PORTB |= 0x04;
}
}
void MatrixLed_SPI_Transmit(uint8_t* data, size_t size){
for(size_t i = 0; i
The implementation of the MatrixLed_SPI_ChipSelect and MatrixLed_SPI_Transmit functions is similar to the STM32 example. The task implementation code has changed, except for the delay function.
6. Demonstration of results
Since the results on all three boards are the same, I will attach only one gif with the implementation on STM32. The result is identical on the rest of the boards.
7. Conclusion
This approach allows you to easily use the same library on different hardware platforms without directly interfering with the library. All that needs to be done in this case is to describe several functions for working with the interface in the project itself, taking into account the features of the platform used. Link to the driver repository https://github.com/krllplotnikov/MAX7219.