Оглавление
1. Вступление
В данной статье я хотел бы вам расказать, как можно создавать свои аппаратно-независимые библиотеки для микроконтроллеров для работы с цифровыи микросхемами.
Суть создания аппаратно-независимой библиотеки состоит в том, чтобы отвязаться от того уровня абстракции (библиотеки и фреймворки), который предоставляет производитель микроконтроллеров, внутри реализуемой библиотеки. Например, для STM32 - HAL, ESP32 - ESP-IDF или Arduino, для AVR зачастую используют Arduino. Это позволит использовать одну и ту же библиотеку на различных микроконтроллерах (и не только) без изменения кода библиотеки под каждый камень.
Большинство микросхем работают через цифровые интерфейсы (UART, SPI, I2C и т.п.). При помощи этих интерфейсов мы взаимодействуем с регистрами микросхемы и получаем определенный результат. Для этого будет достаточно описать несколько функций работы с интерфейсом и передать указатели на эти функции в нашу библиотеку. Это значит, что в реализации самой библиотеки можно описывать только логику работы и на выходе предоставить интерфейс для работы практически с любым микроконтроллером.
Как можно такое реализовать, буду объяснять на примере довольно простой по функционалу микросхемы 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);
}
Минусы данного подхода:
Библиотека может работать только с одним подключенным модулем.
Зависимость от 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.
Распиновка:
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. На остальных платах результат идентичный.
7. Заключение
Данный подход позволяет легко использовать одну и ту же библиотеку на различных аппаратных платформах без непосредственного вмешательства в библиотеку. Все что нужно сделать в таком случае, это описать несколько функций для работы с интерфейсом в самом проекте, с учетом особенностей используемой платформы. Ссылка на репозиторий драйвера https://github.com/krllplotnikov/MAX7219.
Комментарии (35)
ssurneed
27.09.2024 19:42а ведь 2024 год(
nerudo
27.09.2024 19:42+2А что 2024? В этом самом 2024 в baremetal на С у меня нет возможности асинхронно опросить stdin и узнать нажали кнопку или нет. Ну то есть абстрактно есть, куча способов "разного уровня совместимости", а в конкретной реализации - хрен тебе. Или отсутствует в библиотеке или собирается, но молча не работает. А инженеры производителя рекомендуют пойти и взять их собственный драйвер для работы с UART. Который работает, но, естественно, не совместим со стандартыми функциями С. ARM A53 / Xilinx, если что.
ruomserg
27.09.2024 19:42+8Это хорошо, что в статье предложили - но по моим наблюдениям, жизнь сложнее. То несколько устройств сидят на одной шине, и нельзя из другого места вызывать SPI или I2C процедуру пока предыдущая транзакция не завершилась. При приеме данных, хочется использовать прерывания, а не зависать синхронно в ожидании (чем грешит ардуина). Еще хуже, если вам надо режим ноги периодически переключать между digital output и digital input (или analog input), и при этом желательно делать это в синхронизации с устройством на той стороне (чтобы вы вдвоем не драйвили одну и ту же линию вверх с одной стороны и вниз - с другой). И вот когда все это начинает предусматриваться в библиотеке - вы уже не обойдетесь универсальными указателями на функции чтобы одно и то же одинаково работало на разных контроллерах. А если все это обмазать слоями абстракции чтобы можно было теоретически сделать что угодно и где угодно - оно начинает быть таким сложным в разработке и отладке, что проще разработать отдельную прошивку для каждой целевой платформы...
Nansch
27.09.2024 19:42+4Слой абстракции железа с виртуальным SPI, буферами, приоритетами, балансировщиком, всё по взрослому. Так незаметно и напишите кусок rtos.
ruomserg
27.09.2024 19:42+2Но есть ли смысл ? У меня одна половина жизни - это большие энтерпрайз системы с кучей слоев абстракции, write once run anywhere и вот это все... И там оно имеет смысл потому что бизнес меняется, требования меняются, и с нуля этих монстров не перепишешь. А другая - когда что-то делается на Tiny13 или ESP32. Это обычно конкретная задача которую надо один раз сделать, и она годами будет что-то регулировать или включать/выключать. С моей точки зрения, тут нет смысла гнаться за переносимостью. В крайнем случае - можно сделать двухуровнвую систему типа как сейчас сделан Klipper в 3d-печати: сложная логика и красивый UI на полноценном линуксе на встраиваемом ПК, а real-time задачи вытащены на тонкий слой логики в микроконтроллере. Захотите сменить контроллер - ну перепишете тонкий слой, да и все. Один черт, каждый производитель лепит свое в тактировании, инициализации, прерываниях, DMA - и под новый тип контроллера хоть так, хоть этак писать прилично приходится...
gleb_l
27.09.2024 19:42+1Я бы сказал больше - даже в ERP, спроектированных полностью изолированно-абстрактно по слоям, с полной примитивизацией возможностей СУБД до CRUD, замена RDBMS - это авантюра с недетерминированным исходом - то есть на деле все эти задекларированные преимущества (привинтим двигатель от Мерседеса к коробке БМВ) не работают, или требуют (зачастую неочевидной и кропотливой) доработки - особенно, если мы хотим получить от информационной системы гарантированно высокую отдачу.
Смотрите на Природу - она использует все частные возможности биологических созданий для внутривидовой и межвидовой борьбы. Наследование - только ссылка назад, но никак не ограничитель движения вперед :)
acc0unt
27.09.2024 19:42+2Я смотрю на природу, и вижу леденящий кровь архитектурный кошмар, с миллионами лет legacy и без единого рефактора. Нафиг-нафиг.
CitizenOfDreams
27.09.2024 19:42+2Слой абстракции железа с виртуальным SPI, буферами, приоритетами, балансировщиком, всё по взрослому. Так незаметно и напишите кусок rtos.
И будете "мигать светодиодом" - то есть выполнять задачи, с которыми когда-то справлялся PIC16 - на STM32 с мегабайтом памяти.
yatanai
27.09.2024 19:42+2Так беда не в том чтобы предусмотреть это всё, а в том чтобы сделать правильную абстрактную модель.
ИМХО, почти все кейсы использования дефолтных интерфейсов уже изучены в доль и поперёк. Если тебе нужно что-то специфическое, то нанимай деда по серьёзнее, а для остальных 99% хватит и дефолтных либ.
Особенно я не понимаю почему не сделают нормальные либы для новых плюсов(С++20 хотяб). Был бы у тебя условный
dev::ina219
в котором тупо карта регистров забита и пару функций для удобной работы, а инициализировался бы он отhal::i2c[0]
драйвера который вообще на этапе компиляции считается. И всё были бы счастливы. (Эх... может когда-нибудь и раскрою свой фреймворк)ЗЫ Я вообще к тому, что слишком абстрактные модели не нужны. Они должны делать ровно то что написано в документации, если клас делает что-то ещё этот клас не имеет права называться аппаратной абстракцией, а скорее клас для обычных пользователей, так сказать.
gleb_l
27.09.2024 19:42+13А что будет, если у вас, например таймеры-счетчики в одном кристалле 16-разрядные, а в другом - 8? И prescaler где-то есть, где-то - нет? Это я к тому, что универсальная абстракция либо имеет свойства наихудшей имплементации, либо сложность, равную наименьшему общему кратному свойств покрываемых имплементаций.
osmanpasha
27.09.2024 19:42+4Неистово плюсую и хочу развернуть ответ. Даже такая простая вещь, как GPIO, у каждого производителя имеет свои особенности; у кого-то вход только PULLUP, у кого-то и PULLUP, и PULLDOWN , где-то выходы push-pull и открытый коллектор, а где-то ещё можно включить высокотоковый драйвер, но только на некоторых пинах.
В итоге универсальная библиотека будет либо иметь самый простой интерфейс и не уметь никаких расширенных возможностей, либо будет покрыта ifdef'ами и все равно код, использующий возможности одного МК, не будет работать на других.
ruomserg
27.09.2024 19:42В этом смысле представляет интерес event-driven архитектура, где все процессы внутри микроконтроллера представляют собой разновидности конечных автоматов, и единственным нормальным способом взаимодействия является запуск события в очередь. Тогда только КА нижнего уровня надо будет переписывать под железо, а логика будет плюс-минус переносима. Был тут цикл статей на хабре под общим заголовком "Автоматное программирование". Но тут свои заморочки - сложный процесс разложить в автомат может быть дурная работа! Ну или генерировать автоматы из промежуточного императивного языка, что тоже такое себе - особенно если оно не работает и это надо как-то отладить... Но зато в event можно включать информацию вне зависимости от того, поддерживается ли она конкретным железом или нет. По принципу - аппаратный уровень постарается выполнить наилучшим образом то, что он умеет и прогинорирует то - что не умеет. Ибо если не дал бог на ноге высокотоковый выход, никакими программными ухищрениями его туда посадить нельзя... Но хотя бы мы не обязаны писать под "наименьший общий знаменатель" аппаратуры, который неизбежно получается очень убогим...
jpegqs
27.09.2024 19:42Полгода назад библиотеку для Arduino для SPI устройства портировал для запуска на Линукс на CH341A. Просто написав хидеры имитирующие API из хидеров к Arduino.
tonyk_av
27.09.2024 19:42+3К чему это кокетство с указателями на функции? В С++ это называется виртуальной функцией. Зачем писать на неООП-языке в ООП стиле? Или вам удобней чесать за правым ухом левой ногой? Если нет, то используйте С++.
Ещё одно крупное упущение. Как только вы замахиваетесь на что-то более-менее универсальное, сразу возникает вопрос синхронизации доступа, иначе всё, что вы тут написали, будет не возможно использовать в многозадачной среде. Упоминаний об этом, как и примитивов синхронизации доступа, я в статье не заметил.kenomimi
27.09.2024 19:42+2Плюсы в дефолтном состоянии тянут за собой нефиговый такой рантайм, который в мелкие контроллеры не влезет. Да, его можно уменьшить, но это уже не уровень ардуино, порог входа таки отрастает на понимание работы тонны опций сборки...
ruomserg
27.09.2024 19:42C++ на микроконтроллерах надо использовать очень аккуратно, и по моей практике - как его задумал Страуструп: "С с классами". Ибо любое использование темплейтов (а на них построен весь std) - запускает неистовую кодогенерацию, которая потом то-ли вырежется оптимизатором, то-ли нет...
voldemar_d
27.09.2024 19:42+2Для того же Arduino предостаточно библиотек, написанных с использованием шаблонов. Все ими просто пользуются, не задумываясь о каком-то неистовстве кодогенерации. И что значит "то-ли вырежется, то-ли нет"? После того, как шаблонный код выведен, компилятор обрабатывает его, как код без шаблонов. В чем проблема?
ruomserg
27.09.2024 19:42На arduino mega, или STM32 или ESP32 - это проходит. А вот написать с arduino-библиотеками под avr tiny (2313, 13, etc) - тот еще цирк. То не хватает флэша под код, то SRAM под переменные. А на C - прекрасно программируется...
voldemar_d
27.09.2024 19:42Возникает вопрос, так ли уж нужны avr tiny. Периодически в обсуждениях возникает вопрос, зачем нужны Arduino, если есть STM32 или Pi Pico, для которых можно на питоне писать, и цена которых сравнима со всем перечисленным. А мощность несравнимо выше.
Допустим, для "помигать светодиодами" достаточно и tiny, а в более сложных случаев лично мне куда проще взять что-то чуть подороже, но для чего можно писать на C++. Если бы для меня это было работой, а не хобби, и была бы экономическая необходимость, я бы писал на ассемблере, но пока что никакой необходимости в этом не вижу.
randomsimplenumber
27.09.2024 19:42Ну Arduino достаточно жирный фреймворк. Да, он портирован чуть ли не все что с ножками. Но жирный
tonyk_av
27.09.2024 19:42+4Ну вот только не надо про " нефиговый такой рантайм"! Если вы не используете потоковый ввод-вывод и исключения, то ничего лишнего в память не заносится. И про тонны опций сборки тоже не надо ля-ля. Сразу видно человека, который не владеет вопросом, а начитался бредней малограмотных погромистов. Даже реализация поддержки С++ в лице g++ на всяких Атмегах требует реализации нескольких функций обработки внутренних ошибок размером в несколько десятков байт кода.
Впредь не говорите того, чего не знаете.ruomserg
27.09.2024 19:42Тогда может стоит сначала договориться, что мы понимаем под C++ ? Если вы убираете исключения, шаблоны, стандартные коллекции, RTTI - то это "С с классами", а не C++... И да, на "C с классами" можно прекрасно писать под любые контроллеры...
voldemar_d
27.09.2024 19:42+3Если есть шаблоны, то это уже C++. Для тех же Arduino достаточно библиотек, в которых шаблоны ещё как используются. Например, EncButton от AlexGyver для работы с кнопками и энкодерами. Библиотека выдаёт весьма компактный и быстрый код. Или, например, библиотеки для работы с экранами - SSD1306Ascii. Также весьма компактная библиотека, в которой ничего лишнего.
osmanpasha
27.09.2024 19:42+2А почему вы считаете, что если не использовать исключения, RTTI и стандартную библиотеку, то это не С++? С++ это большой набор возможностей, и каждую можно использовать, а можно не использовать. И ещё хорошо понимать, какую цену имеет каждая. Так-то в С++ есть куча фич, которые работают на этапе компиляции и абсолютно бесплатны в рантайме. Как пример, constexpr, namespaces, enum classes, перегрузка операторов, дефолтные значения для аргументов функций.
Об этом куча статей написана, вот например: http://fjrg76.com/2023/04/22/7-tips-noo-cpp-en/
ruomserg
27.09.2024 19:42Я об этом и говорю - надо договориться, что такое тогда C++. Я его видел еще с компилятора Borland C++ 3.1 - и по сравнению с тем, что было тогда, из языка сделали того еще монстрика, который вроде хочет пойти сразу во все стороны, но ни в какую из них не идет так, чтобы это было удобно. Для меня C++ без коллекций и std:: становится именно "C с классами" который когда-то через cfront компилировал программу на C++ в программу на чистом "C" делая this явным параметром функций. Что касается вычислений и мета-программирования на этапе компиляции - то этого я вообще стараюсь не касаться. В моем уютном мире, разработчик пишет код - компилятор транслирует - процессор выполняет. Идея запрограммировать хитрым инпутом компилятор чтобы он сгенерировал код, который при компиляции чего-то там сделает - мне кажется ущербной. Развитием этой идеи является мета-мета программирование: давайте напишем хитрый промпт для LLM, чтобы она сгенерировала хитрый код для компилятора, чтобы он (и далее по-тексту). Это, безусловно, очень интересно и занимательно - но я в таком не участвую.
В сухом итоге - если вы хотите программировать близко к железу или в условиях сильно ограниченных ресурсов - есть "C", если не нравятся явные указатели на функции - расширьте до "C с классами".
Если у вас много ресурсов - поставьте ОС и пишите на чем-нибудь безопасном типа джавы или питона...
А C++ со всем его могуществом и UB - для программирования микроконтроллеров подходит не очень...
rsashka
27.09.2024 19:42+2C++ это в первую очередь реализация принципа "не платить за то, что не используется", что позволяет с одной стороны расти языку в монстроподобную кашу всяких новомодных фишек, и одновременно игнорить их, если они вам не требуются.
Поэтому С++ вполне себе нормальный язык для микроконтроллеров, а какие его возможности вы будете использовать, это исключительно ваше дело, так как не используемые вами фичи не добавляют в исполняемый код программы никакого лишнего оверхеда.
osmanpasha
27.09.2024 19:42+3Это какое-то заблуждение про рантайм С++. Ардуино-ядро для attiny13 с 1Кб флэша компилируется с помощью g++ и ничего, никакие тонны рантайма не добавляются.
Скорее наоборот, чтобы подтянуть, например, исключения (которые добавляют несколько десятков Кб) нужны знания уровнем выше, чем у среднего ардуино-программиста
randomsimplenumber
27.09.2024 19:42+3Вместо указателей на функции использовать ifdef __название_камня, и будет совсем хорошо.
aitras
27.09.2024 19:42Кажется, видел такой подход в библиотеке для W5500, предлагаемой производителем.
rsashka
В принципе рабочий метод, но сложный и очень трудоемкий (требуется реализация низкоуровневого взаимодействия) и хорошего понимание работы самой библиотеки в случае необходимости частичной реализации низкоувровненго взаимодействия.
Для подобный целей я использую другой, более простой подход - реализую библиотеку на уровне обработки потока данных (в виде нескольких функций или отдельного класса) без привязки к аппаратным интерфейсам. Это позволяет не зависеть от сложностей взаимодествия с железом, когда оно не требуется, тогда как коннектор можно сделать на любой интерфейс. Дополнительно гораздо проще все это разработывать и покрывать все это тестами, так как не требуется держать в голове или эмулировать взаимодействие с железом.
Все это так же можер работать без изменений на любом микроконтроллере, HAL или без него, и даже на обычном компе (например, для эмуляции при тестировании)
gev
Мы пошли дальше и взяли для этих целей Haskell!