Создание аппаратно-независимых библиотек для микроконтроллеров
Оглавление
Вступление
Описание проблематики
Реализация базового драйвера
Реализация библиотеки для модуля светодиодной матрицы
Примеры использования
Демонстрация результатов
Заключение
1. Вступление
В данной статье я хотел бы вам расказать, как можно создавать свои аппаратно-независимые библиотеки для микроконтроллеров для работы с цифровыи микросхемами.
Суть создания аппаратно-независимой библиотеки состоит в том, чтобы отвязаться от того уровня абстракции (библиотеки и фреймворки), который предоставляет производитель микроконтроллеров, внутри реализуемой библиотеки. Например, для STM32 — HAL, ESP32 — ESP-IDF или Arduino, для AVR зачастую используют Arduino. Это позволит использовать одну и ту же библиотеку на различных микроконтроллерах (и не только) без изменения кода библиотеки под каждый камень.
Большинство микросхем работают через цифровые интерфейсы (UART, SPI, I2C и т.п.). При помощи этих интерфейсов мы взаимодействуем с регистрами микросхемы и получаем определенный результат. Для этого будет достаточно описать несколько функций работы с интерфейсом и передать указатели на эти функции в нашу библиотеку. Это значит, что в реализации самой библиотеки можно описывать только логику работы и на выходе предоставить интерфейс для работы практически с любым микроконтроллером.
Как можно такое реализовать, буду объяснять на примере довольно простой по функционалу микросхемы MAX7219 и модуле светодиодной матрицы на базе нее. Думаю, многие знакомы с этой микросхемой и видели на базе нее модули светодиодной матрицы и семисегментных индикаторов. В ходе реализации я не буду подробно углубляться как работает микросхема, все это вы либо уже знаете, либо можете найти в документации.
Рис. 1. — Модуль светодиодной матрицы на базе микросхемы MAX7219
2. Описание проблематики
Примечание: весь код в этом разделе приведен для STM32 и был найден в репозиториях GitHub.
Когда я ищу готовые библиотеки в интернете, в основном все что я нахожу выглядит подобным образом:
В .h файлах библиотеки дефайнятся и экстернятся 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
…
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.
При подключении библиотеки в свой проект необходимо залазить в файлы библиотеки и конфигурировать под себя.
Перенести библиотеку на микроконтроллеры других производителей будет проблематично.
Что я предлагаю сделать:
Во первых, описать структуру, при помощи которой мы сможем создавать несколько экземпляров подключаемых модулей. Данный подход решит первую проблему.
Во вторых, чтобы не использовать HAL внутри библиотеки, можно создать указатели на функции для работы с нужным нам интерфейсом и портами. Реализацию функции необходимо будет самому описать в основной программе и передать на нее указатель в библиотеку. Это решит оставшиеся три проблемы.
Все это касается не только STM32 и HAL, а и всех остальных реализаций библиотек под конкретный микроконтроллер, созданных по такому принципу.
3. Реализация базового драйвера
Для начала давайте создадим небольшой драйвер для работы с регистрами микросхемы. В основной программе разработчик не должен будет использовать этот драйвер. Данный драйвер будет описывать самый низкий уровень абстракции, а далее на основе этого драйвера будет написана библиотека, уже которую разработчик сможет использовать в проекте.
Первое, с чего стоит начать, это создать пару файлов max7219.h и max7219.c.
В файле max7219.h определим регистры микросхемы:
#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 размером size по выбранному нами 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. Есть один нюанс с SPI_ChipSelect. Дело в том что в некоторых микроконтроллерах можно настроить автоматическое переключение пина CS, в таком случае нет необходимости программно переключать этот пин. Если вы так конфигурируете свой SPI, то можно просто передать NULL в параметр spiChipSelect во время инициализации.
На этом реализация базового драйвера закончена. В следующем разделе реализуем более высокоуровневую логику для работы со светодиодной матрицей с использованием данного драйвера.
4. Реализация библиотеки для модуля светодиодной матрицы
На этом этапе мы будем создавать библиотеку, которая предоставит удобный интерфейс разработчику для работы со светодиодной матрицей.
Создадим пару файлов 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 записывает в регистры микросхемы данные из буфера.
5. Примеры использования
Для примера будем реализовывать один и тот же алгоритм на различных микроконтроллерах.
Задача: циклично по очереди зажигать светодиоды по диагонали начиная с нижнего левого угла, до правого верхнего с периодом в 1 секунду.
Во всех примерах код будет практически одинаковым. Основные отличия будут только в реализации функций работы с SPI для конкретного микроконтроллера. Демонстрация результатов показана в п.6. Демонстрация результатов.
5.1. Пример использования на микроконтроллере STM32
Для примера будем использовать отладочную плату на базе STM32F401. Создадим новый проект в CubeIDE, и сконфигурируем SPI.
Рис. 2 — Конфикурация SPI в CubeIDE
Распиновка:
MAX7219 | STM32 |
---|---|
VCC | 3V3 |
GND | GND |
DIN | PA7 |
CS | PA4 |
CLK | PA5 |
Далее в 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. В цикле зажигаются светодиоды согласно поставленой в примере задаче.
5.2 Пример использования на микроконтроллере ESP32
Для примера будем использовать отладочную плату на базе ESP32C3. Создадим новый проект в ESP-IDE, и сконфигурируем SPI.
Распиновка:
MAX7219 | ESP32 |
---|---|
VCC | 3V3 |
GND | GND |
DIN | GPIO4 |
CS | GPIO3 |
CLK | GPIO2 |
Код main.c:
#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 < 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++;
}
}
}
Реализация функции MatrixLed_SPI_Transmit аналогична примеру с STM32, но в данном случае можно не реализовывать функцию MatrixLed_SPI_ChipSelect т.к. SPI сконфигурирован так, чтобы автоматически управлять пином CS. Код реализации задачи не изменился, за исключением функции задержки.
5.3 Пример использования на микроконтроллере AVR
Для примера будем использовать отладочную плату на базе Atmega328. Создадим новый проект в PlatformIO, и сконфигурируем SPI. Проект создан на базе Arduino, но в реализации не будут использованы ардуиновские функции кроме delay ().
Распиновка:
MAX7219 | Atmega328 |
---|---|
VCC | 3V3 |
GND | GND |
DIN | PB3 |
CS | PB2 |
CLK | PB5 |
Код 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. Код реализации задачи не изменился, за исключением функции задержки.
6. Демонстрация результатов
Так как результаты на всех трех платах одинаковые, прикреплю только одну гифку с реализацией на STM32. На остальных платах результат идентичный.
Рис. 3 — Демонстрация результата на микроконтроллере STM32
7. Заключение
Данный подход позволяет легко использовать одну и ту же библиотеку на различных аппаратных платформах без непосредственного вмешательства в библиотеку. Все что нужно сделать в таком случае, это описать несколько функций для работы с интерфейсом в самом проекте, с учетом особенностей используемой платформы. Ссылка на репозиторий драйвера https://github.com/krllplotnikov/MAX7219.