Дізнайтеся, як створювати апаратно-незалежні бібліотеки для мікроконтролерів, щоб максимально спростити їх повторне використання на різних платформах без потреби у глибоких змінах коду. У цій статті наведені конкретні приклади, зокрема робота з STM32 та MAX7219, які допоможуть краще зрозуміти підхід до створення універсальних рішень для мікроконтролерів.
У статті пояснюється, як розробити апаратно-незалежні бібліотеки для мікроконтролерів, щоб використовувати одну бібліотеку на різних платформах без необхідності модифікувати код під конкретний мікроконтролер. Описується процес взаємодії з цифровими мікросхемами через інтерфейси UART, SPI, I2C та інших. Створення таких бібліотек полягає в абстрагуванні від специфічних фреймворків (STM32-HAL, ESP-IDF, Arduino тощо), що дозволяє використовувати код на різних мікроконтролерах. Розглядається приклад роботи з мікросхемою MAX7219 і світлодіодними матрицями на її основі.

Примітка: код у розділі подано для STM32 і взятий з репозиторіїв GitHub.
Часто під час пошуку готових бібліотек в інтернеті можна зустріти такі приклади, де у файлах заголовків (.h) використовуються директиви #define для HAL-хендлерів, пінів і портів мікроконтролера.
#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); …
У .c файлах інклюдяться HAL-івські хедери під конкретний камінь і відповідно вся бібліотека працює на HAL.
#include "stm32f1xx_hal.h"
#include <max7219.h>
…
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);
}
Мінуси цього підходу:
Бібліотека може працювати лише з одним підключеним модулем.
Залежність від HAL.
При підключенні бібліотеки у свій проект необхідно залазити у файли бібліотеки та конфігурувати під себе.
Перенести бібліотеку на мікроконтролери інших виробників буде проблематично.
#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
Далі створимо покажчики на функції для роботи з SPI:
typedef void (*SPI_Transmit)(uint8_t* data, size_t size); typedef void (*SPI_ChipSelect)(uint8_t level);
В основній програмі необхідно самостійно реалізувати такі функції, як SPI_Transmit, яка відповідатиме за передачу масиву байтів data через вибраний SPI, та SPI_ChipSelect, яка змінюватиме стан піна CS залежно від параметра level. Після цього варто визначити структуру для опису мікросхеми, яка дозволить управляти нею в програмі.
typedef struct{
SPI_Transmit spiTransmit;
SPI_ChipSelect spiChipSelect;
}MAX7219_st;
В даному випадку буде достатньо полів, які приймають покажчики на функції роботи з SPI. Буфер із даними на матриці буде описано на наступному рівні абстракції. Наприкінці, визначимо основні функції:
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);
У функцію ініціалізації MAX7219_Init ми передамо покажчик на структуру MAX7219_st та покажчики на функції роботи з SPI, які опишемо в основній програмі. Переходимо до файлу max7219.c.
#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);
}
}
У функції MAX7219_Init відбувається присвоєння покажчиків на функції в полях переданої структури. У функції MAX7219_WriteReg викликаються функції для передачі даних через SPI. Якщо мікроконтролер підтримує автоматичне перемикання піна CS, можна не перемикати його програмно і передати NULL як параметр spiChipSelect під час ініціалізації. На цьому завершено реалізацію базового драйвера. Далі планується розробка більш високорівневої логіки для роботи зі світлодіодною матрицею, що використовує цей драйвер.
На цьому етапі ми створюватимемо бібліотеку, яка надасть зручний інтерфейс розробнику для роботи зі світлодіодною матрицею. Створимо пару файлів MatrixLed.h та MatrixLed.c. У MatrixLed.h підключимо раніше створений драйвер max7219 та опишемо структуру модуля матриці.
#include "max7219.h"
#define MATRIX_SIZE 8
typedef struct{
MAX7219_st max7219;
uint8_t displayBuffer[MATRIX_SIZE];
}MatrixLed_st;
Структура MatrixLed_st містить екземпляр драйвера MAX7219_st і буфер зображення на матриці.
Далі оголосимо такі функції:
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);
У функцію MatrixLed_Init передається покажчик на структуру MatrixLed_st та функції для роботи з SPI. Функція MatrixLed_SetPixel дозволяє встановлювати стан окремого пікселя за його координатами, але не змінює стан світлодіодів одразу. Для цього використовується окрема функція MatrixLed_DrawDisplay, яка оновлює стан всіх світлодіодів і забезпечує візуалізацію змін на матриці. Цей підхід розділяє логіку зміни стану пікселів і оновлення дисплея для більш гнучкої роботи.
Переходимо до 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);
}
У функції ініціалізації приймаємо покажчики функцій роботи з SPI, налаштовуємо модуль і гасимо всі світлодіоди на матриці.
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 встановлюються потрібні біти у буфері зображення матриці згідно з переданими координатами.
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 записує в регістри мікросхеми дані з буфера.
Для реалізації одного і того ж алгоритму на різних мікроконтролерах ми запалюємо світлодіоди по діагоналі з періодом в 1 секунду, починаючи з нижнього лівого кута до правого верхнього. Основна відмінність між прикладами буде полягати в реалізації функцій роботи з SPI, що специфічні для кожного мікроконтролера. Незважаючи на різні апаратні платформи, логіка коду залишається практично однаковою. Результати демонстрації будуть показані у відповідному розділі (п.6).
Для прикладу будемо використовувати налагоджувальну плату на базі STM32F401. Створимо новий проект у CubeIDE, і налаштуємо SPI.

Розпинування:
Далі в main.c опишемо такий фрагмент коду:
#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 < MATRIX_SIZE && y < MATRIX_SIZE){
MatrixLed_SetPixel(&matrixLed, x, y, 1);
MatrixLed_DrawDisplay(&matrixLed);
HAL_Delay(1000);
MatrixLed_SetPixel(&matrixLed, x, y, 0);
MatrixLed_DrawDisplay(&matrixLed);
x++;
y++;
}
}
}
MatrixLed_SPI_ChipSelect встановлює потрібний рівень на піні CS згідно з переданим параметром. MatrixLed_SPI_Transmit здійснює відправлення переданого буфера SPI. Вказівники на дані функції передаються MatrixLed_Init. У циклі запалюються світлодіоди згідно з поставленим у прикладі задачі.
Для прикладу будемо використовувати налагоджувальну плату на базі ESP32C3. Створимо новий проект в ESP-IDE, і налаштуємо SPI.
Розпинування:
Код main.c:
#include <stdio.h>
#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 < MATRIX_SIZE && y < MATRIX_SIZE){
MatrixLed_SetPixel(&matrixLed, x, y, 1);
MatrixLed_DrawDisplay(&matrixLed);
vTaskDelay(1000/portTICK_PERIOD_MS);
MatrixLed_SetPixel(&matrixLed, x, y, 0);
MatrixLed_DrawDisplay(&matrixLed);
x++;
y++;
}
}
}
Розпинування:
Код main.c:
#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 < size; i++){
SPDR = data[i];
while(!(SPSR & (1 << SPIF)));
}
}
void SPI_Init(){
DDRB = (1 << DDB2)|(1 << DDB3)|(1 << DDB5);
SPCR = (1 << SPE)|(1 << MSTR)|(1 << SPR0);
}
void setup() {
SPI_Init();
MatrixLed_Init(&matrixLed, MatrixLed_SPI_Transmit, MatrixLed_SPI_ChipSelect);
}
void loop() {
uint8_t x = 0;
uint8_t y = 0;
while(x < MATRIX_SIZE && y < MATRIX_SIZE){
MatrixLed_SetPixel(&matrixLed, x, y, 1);
MatrixLed_DrawDisplay(&matrixLed);
delay(1000);
MatrixLed_SetPixel(&matrixLed, x, y, 0);
MatrixLed_DrawDisplay(&matrixLed);
x++;
y++;
}
}
Реалізація функцій MatrixLed_SPI_ChipSelect і MatrixLed_SPI_Transmit аналогічна прикладу STM32. Код реалізації завдання змінився, крім функції затримки.
Цей підхід забезпечує простоту використання однієї і тієї ж бібліотеки на різних апаратних платформах без необхідності змінювати саму бібліотеку. Потрібно лише описати кілька функцій для взаємодії з інтерфейсом у проекті, враховуючи специфіку платформи. Репозиторій драйвера доступний за посиланням: https://github.com/krllplotnikov/MAX7219.
Якщо у Вас виникають проблеми, то Ви можете зв’язатися з нами за допомогою [email protected].