Оглавление

  1. Вступление

  2. Описание проблематики

  3. Реализация базового драйвера

  4. Реализация библиотеки для модуля светодиодной матрицы

  5. Примеры использования

  6. Демонстрация результатов

  7. Заключение

1. Вступление

В данной статье я хотел бы вам расказать, как можно создавать свои аппаратно-независимые библиотеки для микроконтроллеров для работы с цифровыи микросхемами.

Суть создания аппаратно-независимой библиотеки состоит в том, чтобы отвязаться от того уровня абстракции (библиотеки и фреймворки), который предоставляет производитель микроконтроллеров, внутри реализуемой библиотеки. Например, для STM32 - HAL, ESP32 - ESP-IDF или Arduino, для AVR зачастую используют Arduino. Это позволит использовать одну и ту же библиотеку на различных микроконтроллерах (и не только) без изменения кода библиотеки под каждый камень.

Большинство микросхем работают через цифровые интерфейсы (UART, SPI, I2C и т.п.). При помощи этих интерфейсов мы взаимодействуем с регистрами микросхемы и получаем определенный результат. Для этого будет достаточно описать несколько функций работы с интерфейсом и передать указатели на эти функции в нашу библиотеку. Это значит, что в реализации самой библиотеки можно описывать только логику работы и на выходе предоставить интерфейс для работы практически с любым микроконтроллером.

Как можно такое реализовать, буду объяснять на примере довольно простой по функционалу микросхемы MAX7219 и модуле светодиодной матрицы на базе нее. Думаю, многие знакомы с этой микросхемой и видели на базе нее модули светодиодной матрицы и семисегментных индикаторов. В ходе реализации я не буду подробно углубляться как работает микросхема, все это вы либо уже знаете, либо можете найти в документации.

Рис. 1. - Модуль светодиодной матрицы на базе микросхемы 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 <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);
}

Минусы данного подхода:

  1. Библиотека может работать только с одним подключенным модулем.

  2. Зависимость от HAL.

  3. При подключении библиотеки в свой проект необходимо залазить в файлы библиотеки и конфигурировать под себя.

  4. Перенести библиотеку на микроконтроллеры других производителей будет проблематично.

Что я предлагаю сделать:

Во первых, описать структуру, при помощи которой мы сможем создавать несколько экземпляров подключаемых модулей. Данный подход решит первую проблему.

Во вторых, чтобы не использовать 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(&reg, 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
Рис. 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 <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++;
        }    
    }
}

Реализация функции 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
Рис. 3 - Демонстрация результата на микроконтроллере STM32

7. Заключение

Данный подход позволяет легко использовать одну и ту же библиотеку на различных аппаратных платформах без непосредственного вмешательства в библиотеку. Все что нужно сделать в таком случае, это описать несколько функций для работы с интерфейсом в самом проекте, с учетом особенностей используемой платформы. Ссылка на репозиторий драйвера https://github.com/krllplotnikov/MAX7219.

Комментарии (35)


  1. rsashka
    27.09.2024 19:42
    +6

    В принципе рабочий метод, но сложный и очень трудоемкий (требуется реализация низкоуровневого взаимодействия) и хорошего понимание работы самой библиотеки в случае необходимости частичной реализации низкоувровненго взаимодействия.

    Для подобный целей я использую другой, более простой подход - реализую библиотеку на уровне обработки потока данных (в виде нескольких функций или отдельного класса) без привязки к аппаратным интерфейсам. Это позволяет не зависеть от сложностей взаимодествия с железом, когда оно не требуется, тогда как коннектор можно сделать на любой интерфейс. Дополнительно гораздо проще все это разработывать и покрывать все это тестами, так как не требуется держать в голове или эмулировать взаимодействие с железом.

    Все это так же можер работать без изменений на любом микроконтроллере, HAL или без него, и даже на обычном компе (например, для эмуляции при тестировании)


    1. gev
      27.09.2024 19:42
      +1

      Мы пошли дальше и взяли для этих целей Haskell!


  1. ssurneed
    27.09.2024 19:42

    а ведь 2024 год(


    1. nerudo
      27.09.2024 19:42
      +2

      А что 2024? В этом самом 2024 в baremetal на С у меня нет возможности асинхронно опросить stdin и узнать нажали кнопку или нет. Ну то есть абстрактно есть, куча способов "разного уровня совместимости", а в конкретной реализации - хрен тебе. Или отсутствует в библиотеке или собирается, но молча не работает. А инженеры производителя рекомендуют пойти и взять их собственный драйвер для работы с UART. Который работает, но, естественно, не совместим со стандартыми функциями С. ARM A53 / Xilinx, если что.


  1. ruomserg
    27.09.2024 19:42
    +8

    Это хорошо, что в статье предложили - но по моим наблюдениям, жизнь сложнее. То несколько устройств сидят на одной шине, и нельзя из другого места вызывать SPI или I2C процедуру пока предыдущая транзакция не завершилась. При приеме данных, хочется использовать прерывания, а не зависать синхронно в ожидании (чем грешит ардуина). Еще хуже, если вам надо режим ноги периодически переключать между digital output и digital input (или analog input), и при этом желательно делать это в синхронизации с устройством на той стороне (чтобы вы вдвоем не драйвили одну и ту же линию вверх с одной стороны и вниз - с другой). И вот когда все это начинает предусматриваться в библиотеке - вы уже не обойдетесь универсальными указателями на функции чтобы одно и то же одинаково работало на разных контроллерах. А если все это обмазать слоями абстракции чтобы можно было теоретически сделать что угодно и где угодно - оно начинает быть таким сложным в разработке и отладке, что проще разработать отдельную прошивку для каждой целевой платформы...


    1. Nansch
      27.09.2024 19:42
      +4

      Слой абстракции железа с виртуальным SPI, буферами, приоритетами, балансировщиком, всё по взрослому. Так незаметно и напишите кусок rtos.


      1. ruomserg
        27.09.2024 19:42
        +2

        Но есть ли смысл ? У меня одна половина жизни - это большие энтерпрайз системы с кучей слоев абстракции, write once run anywhere и вот это все... И там оно имеет смысл потому что бизнес меняется, требования меняются, и с нуля этих монстров не перепишешь. А другая - когда что-то делается на Tiny13 или ESP32. Это обычно конкретная задача которую надо один раз сделать, и она годами будет что-то регулировать или включать/выключать. С моей точки зрения, тут нет смысла гнаться за переносимостью. В крайнем случае - можно сделать двухуровнвую систему типа как сейчас сделан Klipper в 3d-печати: сложная логика и красивый UI на полноценном линуксе на встраиваемом ПК, а real-time задачи вытащены на тонкий слой логики в микроконтроллере. Захотите сменить контроллер - ну перепишете тонкий слой, да и все. Один черт, каждый производитель лепит свое в тактировании, инициализации, прерываниях, DMA - и под новый тип контроллера хоть так, хоть этак писать прилично приходится...


        1. gleb_l
          27.09.2024 19:42
          +1

          Я бы сказал больше - даже в ERP, спроектированных полностью изолированно-абстрактно по слоям, с полной примитивизацией возможностей СУБД до CRUD, замена RDBMS - это авантюра с недетерминированным исходом - то есть на деле все эти задекларированные преимущества (привинтим двигатель от Мерседеса к коробке БМВ) не работают, или требуют (зачастую неочевидной и кропотливой) доработки - особенно, если мы хотим получить от информационной системы гарантированно высокую отдачу.

          Смотрите на Природу - она использует все частные возможности биологических созданий для внутривидовой и межвидовой борьбы. Наследование - только ссылка назад, но никак не ограничитель движения вперед :)


          1. acc0unt
            27.09.2024 19:42
            +2

            Я смотрю на природу, и вижу леденящий кровь архитектурный кошмар, с миллионами лет legacy и без единого рефактора. Нафиг-нафиг.


            1. AbuMohammed
              27.09.2024 19:42

              Рефакторинг в природе называется Чернобылем)


            1. devprodest
              27.09.2024 19:42

              Был рефакторинг... Или у вас до сих пор ещё есть жабры и хвост?


      1. CitizenOfDreams
        27.09.2024 19:42
        +2

        Слой абстракции железа с виртуальным SPI, буферами, приоритетами, балансировщиком, всё по взрослому. Так незаметно и напишите кусок rtos.

        И будете "мигать светодиодом" - то есть выполнять задачи, с которыми когда-то справлялся PIC16 - на STM32 с мегабайтом памяти.


    1. yatanai
      27.09.2024 19:42
      +2

      Так беда не в том чтобы предусмотреть это всё, а в том чтобы сделать правильную абстрактную модель.

      ИМХО, почти все кейсы использования дефолтных интерфейсов уже изучены в доль и поперёк. Если тебе нужно что-то специфическое, то нанимай деда по серьёзнее, а для остальных 99% хватит и дефолтных либ.

      Особенно я не понимаю почему не сделают нормальные либы для новых плюсов(С++20 хотяб). Был бы у тебя условный dev::ina219 в котором тупо карта регистров забита и пару функций для удобной работы, а инициализировался бы он от hal::i2c[0] драйвера который вообще на этапе компиляции считается. И всё были бы счастливы. (Эх... может когда-нибудь и раскрою свой фреймворк)

      ЗЫ Я вообще к тому, что слишком абстрактные модели не нужны. Они должны делать ровно то что написано в документации, если клас делает что-то ещё этот клас не имеет права называться аппаратной абстракцией, а скорее клас для обычных пользователей, так сказать.


  1. gleb_l
    27.09.2024 19:42
    +13

    А что будет, если у вас, например таймеры-счетчики в одном кристалле 16-разрядные, а в другом - 8? И prescaler где-то есть, где-то - нет? Это я к тому, что универсальная абстракция либо имеет свойства наихудшей имплементации, либо сложность, равную наименьшему общему кратному свойств покрываемых имплементаций.


    1. osmanpasha
      27.09.2024 19:42
      +4

      Неистово плюсую и хочу развернуть ответ. Даже такая простая вещь, как GPIO, у каждого производителя имеет свои особенности; у кого-то вход только PULLUP, у кого-то и PULLUP, и PULLDOWN , где-то выходы push-pull и открытый коллектор, а где-то ещё можно включить высокотоковый драйвер, но только на некоторых пинах.

      В итоге универсальная библиотека будет либо иметь самый простой интерфейс и не уметь никаких расширенных возможностей, либо будет покрыта ifdef'ами и все равно код, использующий возможности одного МК, не будет работать на других.


      1. ruomserg
        27.09.2024 19:42

        В этом смысле представляет интерес event-driven архитектура, где все процессы внутри микроконтроллера представляют собой разновидности конечных автоматов, и единственным нормальным способом взаимодействия является запуск события в очередь. Тогда только КА нижнего уровня надо будет переписывать под железо, а логика будет плюс-минус переносима. Был тут цикл статей на хабре под общим заголовком "Автоматное программирование". Но тут свои заморочки - сложный процесс разложить в автомат может быть дурная работа! Ну или генерировать автоматы из промежуточного императивного языка, что тоже такое себе - особенно если оно не работает и это надо как-то отладить... Но зато в event можно включать информацию вне зависимости от того, поддерживается ли она конкретным железом или нет. По принципу - аппаратный уровень постарается выполнить наилучшим образом то, что он умеет и прогинорирует то - что не умеет. Ибо если не дал бог на ноге высокотоковый выход, никакими программными ухищрениями его туда посадить нельзя... Но хотя бы мы не обязаны писать под "наименьший общий знаменатель" аппаратуры, который неизбежно получается очень убогим...


  1. jpegqs
    27.09.2024 19:42

    Полгода назад библиотеку для Arduino для SPI устройства портировал для запуска на Линукс на CH341A. Просто написав хидеры имитирующие API из хидеров к Arduino.


    1. randomsimplenumber
      27.09.2024 19:42

      Иногда можно и так.


  1. tonyk_av
    27.09.2024 19:42
    +3

    К чему это кокетство с указателями на функции? В С++ это называется виртуальной функцией. Зачем писать на неООП-языке в ООП стиле? Или вам удобней чесать за правым ухом левой ногой? Если нет, то используйте С++.

    Ещё одно крупное упущение. Как только вы замахиваетесь на что-то более-менее универсальное, сразу возникает вопрос синхронизации доступа, иначе всё, что вы тут написали, будет не возможно использовать в многозадачной среде. Упоминаний об этом, как и примитивов синхронизации доступа, я в статье не заметил.


    1. kenomimi
      27.09.2024 19:42
      +2

      Плюсы в дефолтном состоянии тянут за собой нефиговый такой рантайм, который в мелкие контроллеры не влезет. Да, его можно уменьшить, но это уже не уровень ардуино, порог входа таки отрастает на понимание работы тонны опций сборки...


      1. ruomserg
        27.09.2024 19:42

        C++ на микроконтроллерах надо использовать очень аккуратно, и по моей практике - как его задумал Страуструп: "С с классами". Ибо любое использование темплейтов (а на них построен весь std) - запускает неистовую кодогенерацию, которая потом то-ли вырежется оптимизатором, то-ли нет...


        1. voldemar_d
          27.09.2024 19:42
          +2

          Для того же Arduino предостаточно библиотек, написанных с использованием шаблонов. Все ими просто пользуются, не задумываясь о каком-то неистовстве кодогенерации. И что значит "то-ли вырежется, то-ли нет"? После того, как шаблонный код выведен, компилятор обрабатывает его, как код без шаблонов. В чем проблема?


          1. ruomserg
            27.09.2024 19:42

            На arduino mega, или STM32 или ESP32 - это проходит. А вот написать с arduino-библиотеками под avr tiny (2313, 13, etc) - тот еще цирк. То не хватает флэша под код, то SRAM под переменные. А на C - прекрасно программируется...


            1. voldemar_d
              27.09.2024 19:42

              Возникает вопрос, так ли уж нужны avr tiny. Периодически в обсуждениях возникает вопрос, зачем нужны Arduino, если есть STM32 или Pi Pico, для которых можно на питоне писать, и цена которых сравнима со всем перечисленным. А мощность несравнимо выше.

              Допустим, для "помигать светодиодами" достаточно и tiny, а в более сложных случаев лично мне куда проще взять что-то чуть подороже, но для чего можно писать на C++. Если бы для меня это было работой, а не хобби, и была бы экономическая необходимость, я бы писал на ассемблере, но пока что никакой необходимости в этом не вижу.


            1. yatanai
              27.09.2024 19:42
              +2

              Может вы просто плохо понимаете современный С++...


            1. randomsimplenumber
              27.09.2024 19:42

              Ну Arduino достаточно жирный фреймворк. Да, он портирован чуть ли не все что с ножками. Но жирный


      1. tonyk_av
        27.09.2024 19:42
        +4

        Ну вот только не надо про " нефиговый такой рантайм"! Если вы не используете потоковый ввод-вывод и исключения, то ничего лишнего в память не заносится. И про тонны опций сборки тоже не надо ля-ля. Сразу видно человека, который не владеет вопросом, а начитался бредней малограмотных погромистов. Даже реализация поддержки С++ в лице g++ на всяких Атмегах требует реализации нескольких функций обработки внутренних ошибок размером в несколько десятков байт кода.

        Впредь не говорите того, чего не знаете.


        1. ruomserg
          27.09.2024 19:42

          Тогда может стоит сначала договориться, что мы понимаем под C++ ? Если вы убираете исключения, шаблоны, стандартные коллекции, RTTI - то это "С с классами", а не C++... И да, на "C с классами" можно прекрасно писать под любые контроллеры...


          1. voldemar_d
            27.09.2024 19:42
            +3

            Если есть шаблоны, то это уже C++. Для тех же Arduino достаточно библиотек, в которых шаблоны ещё как используются. Например, EncButton от AlexGyver для работы с кнопками и энкодерами. Библиотека выдаёт весьма компактный и быстрый код. Или, например, библиотеки для работы с экранами - SSD1306Ascii. Также весьма компактная библиотека, в которой ничего лишнего.


          1. osmanpasha
            27.09.2024 19:42
            +2

            А почему вы считаете, что если не использовать исключения, RTTI и стандартную библиотеку, то это не С++? С++ это большой набор возможностей, и каждую можно использовать, а можно не использовать. И ещё хорошо понимать, какую цену имеет каждая. Так-то в С++ есть куча фич, которые работают на этапе компиляции и абсолютно бесплатны в рантайме. Как пример, constexpr, namespaces, enum classes, перегрузка операторов, дефолтные значения для аргументов функций.

            Об этом куча статей написана, вот например: http://fjrg76.com/2023/04/22/7-tips-noo-cpp-en/


            1. ruomserg
              27.09.2024 19:42

              Я об этом и говорю - надо договориться, что такое тогда C++. Я его видел еще с компилятора Borland C++ 3.1 - и по сравнению с тем, что было тогда, из языка сделали того еще монстрика, который вроде хочет пойти сразу во все стороны, но ни в какую из них не идет так, чтобы это было удобно. Для меня C++ без коллекций и std:: становится именно "C с классами" который когда-то через cfront компилировал программу на C++ в программу на чистом "C" делая this явным параметром функций. Что касается вычислений и мета-программирования на этапе компиляции - то этого я вообще стараюсь не касаться. В моем уютном мире, разработчик пишет код - компилятор транслирует - процессор выполняет. Идея запрограммировать хитрым инпутом компилятор чтобы он сгенерировал код, который при компиляции чего-то там сделает - мне кажется ущербной. Развитием этой идеи является мета-мета программирование: давайте напишем хитрый промпт для LLM, чтобы она сгенерировала хитрый код для компилятора, чтобы он (и далее по-тексту). Это, безусловно, очень интересно и занимательно - но я в таком не участвую.

              В сухом итоге - если вы хотите программировать близко к железу или в условиях сильно ограниченных ресурсов - есть "C", если не нравятся явные указатели на функции - расширьте до "C с классами".

              Если у вас много ресурсов - поставьте ОС и пишите на чем-нибудь безопасном типа джавы или питона...

              А C++ со всем его могуществом и UB - для программирования микроконтроллеров подходит не очень...


              1. rsashka
                27.09.2024 19:42
                +2

                C++ это в первую очередь реализация принципа "не платить за то, что не используется", что позволяет с одной стороны расти языку в монстроподобную кашу всяких новомодных фишек, и одновременно игнорить их, если они вам не требуются.

                Поэтому С++ вполне себе нормальный язык для микроконтроллеров, а какие его возможности вы будете использовать, это исключительно ваше дело, так как не используемые вами фичи не добавляют в исполняемый код программы никакого лишнего оверхеда.


      1. osmanpasha
        27.09.2024 19:42
        +3

        Это какое-то заблуждение про рантайм С++. Ардуино-ядро для attiny13 с 1Кб флэша компилируется с помощью g++ и ничего, никакие тонны рантайма не добавляются.

        Скорее наоборот, чтобы подтянуть, например, исключения (которые добавляют несколько десятков Кб) нужны знания уровнем выше, чем у среднего ардуино-программиста


  1. randomsimplenumber
    27.09.2024 19:42
    +3

    Вместо указателей на функции использовать ifdef __название_камня, и будет совсем хорошо.


  1. aitras
    27.09.2024 19:42

    Кажется, видел такой подход в библиотеке для W5500, предлагаемой производителем.