Пролог

Итак, вам надо разработать какой-либо программный компонент (ПК или SWC). Это могут быть такие драйверы для ASIC чипов, как lis3dh, at24cxx, si4703, ksz8081, sx1262, wm8731, tcan4550, fda801, tic12400, ssd1306, dw1000, или drv8711. Не важно, какой конкретно чип. Все они работают по одному принципу. Прописываешь по проводному интерфейсу чиселки в их внутренние регистры и там внутри чипа заводится какая-то электрическая цепочка. Easy.

Или вам надо написать HAL для GPIO, UART, SPI и т п. Или вам надо написать реализацию UDS, NMEA или xModem. Условно разработать какой - либо программный модуль.

Допустим, что на GitHub драйверов для вашего модуля нет или качество этих open sourсe драйверов оставляет желать лучшего. Как же оформить и собрать качественный программный компонент ? Это относится как к драйверам UART, SPI, CAN, так и вообще к любому программному компоненту. И почему это вообще важно?

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

Данная структура программного компонента, господа, это не мои пьяные выдумки. Именно в таком виде генерируют embedded коды топовые мировы�� корпоративные генераторов сорсов для автопрома: такие как Vector DaVinci Configurator Classic / Pro; EB tresos Studio; ETAS ISOLAR-A; Mentor VSTAR; Artop и прочие. @Indemsys не даст соврать. Вот так...

А теперь погнали...

*.с/*xxx_drv.h файлы с самим функционалом.*

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

*xxx_isr.с/*xxx_isr.h Отдельные файлы с кодом драйвера, который должен отрабатывать в обработчике прерываний

Это нужно для того, чтобы подчеркнуть тот факт, что к этому ISR коду надо относиться с особенной осторожностью. Например, это ядро программного таймера.

Код работающий внутри обработчика прерывания должен обладать следующими свойствами:

1--Должен быть оптимизирован по быстродействию.

2--Этот код сам не должен вызывать другие прерывания.

3--Надо убедиться, что там нет арифметики с плавающей точкой. Иначе будет медленно выходить из переключения контекста.

4--Внутри ISR нет логирования.

5--Все переменные, которые модифицируются внутри прерывания помечены как volatile.

Отдельный xxx_types.h файл с перечислением типов*

В этом файле следует определить основные типы данных для данного программного компонента. Также определить объединения и битовые поля для каждого регис��ра.

/* page 105
 * 7.2.27 Register file: 0x19 – DW1000 State Information*/
typedef union {
    uint8_t byte[4];
    uint32_t dword;
    struct {
        uint32_t tx_state : 4;     /*bit 0-3: TX_STATE*/
        uint32_t res1 : 4;      /*bit 4-7: */
        uint32_t rx_state: 5; /*bit 8-12: RX_STATE*/
        uint32_t res2 : 3;      /*bit 13-15:*/
        uint32_t pmsc_state : 4;     /*bit 16-19: PMSC_STATE*/
        uint32_t res3 : 12;     /*bit 20-31:*/
    };
} Dwm1000SysState_t;

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

Harvard architecture
Harvard architecture

Отдельный xxx_const.h файл с перечислением констант*

Тут надо определить адреса регистров, перечисления. Это очень важно быстро найти файл с константами и отредактировать их, поэтому для констант делаем отдельный *.h файл.

typedef enum {
    BIT_RATE_110_KBPS = 0,  /*110 kbps*/
    BIT_RATE_850_KBPS = 1,  /*850 kbps*/
    BIT_RATE_6800_KBPS = 2, /*6.8 Mbps*/
    BIT_RATE_RESERVED = 3,  /*reserved*/

    BIT_RATE_UNFED = 4,
} Dwm1000BitRate_t;

xxx_param.h файл с параметрами драйвера*

Каждый драйвер нуждается в энергонезависимых параметрах с настройками (битовая скорость, CAN-трансивера или несущая частота радиопередатчика). Именно эти настройки будут применятся при инициализации при старте питания. Параметры позволят существенно изменять поведение всего устройства без нужды пересборки всех сорцов. Просто прописали через CLI параметры и перезагрузились. И у вас новый функционал. Успех! Поэтому надо где-то указать как минимум тип данных и имя параметров драйвера.

#ifndef SX1262_PARAMS_H
#define SX1262_PARAMS_H

#include "param_drv.h"
#include "param_types.h"

#ifdef HAS_GFSK
#include "sx1262_gfsk_params.h"
#else
#define PARAMS_SX1262_GFSK
#endif

#ifdef HAS_LORA
#include "sx1262_lora_params.h"
#else
#define PARAMS_SX1262_LORA
#endif

#define PARAMS_SX1262       \
    PARAMS_SX1262_LORA      \
    PARAMS_SX1262_GFSK      \
    {SX1262, PAR_ID_FREQ, 4, TYPE_UINT32, "Freq"},   /*Hz*/                              \
    {SX1262, PAR_ID_WIRELESS_INTERFACE, 1, TYPE_UINT8, "Interface"},    /*LoRa or GFSK*/          \
    {SX1262, PAR_ID_TX_MUTE, 1, TYPE_UINT8, "TxMute"},                                        \
    {SX1262, PAR_ID_RX_GAIN, 1, TYPE_UINT8, "RxGain"},         \
    {SX1262, PAR_ID_RETX, 1, TYPE_UINT8, "ReTx"},              \
    {SX1262, PAR_ID_IQ_SETUP, 1, TYPE_UINT8, "IQSetUp"},                                         \
    {SX1262, PAR_ID_OUT_POWER, 1, TYPE_INT8, "OutPower"}, /*loRa output power*/          \
    {SX1262, PAR_ID_MAX_LINK_DIST, 8, TYPE_DOUBLE, "MaxLinkDist"}, /*Max Link Distance*/ \
    {SX1262, PAR_ID_MAX_BIT_RATE, 8, TYPE_DOUBLE, "MaxBitRate"}, /*Max bit/rate*/   \
    {SX1262, PAR_ID_RETX_CNT, 1, TYPE_UINT8, "ReTxCnt"},


#endif /* SX1262_PARAMS_H  */

config_xxx.с/config_xxx.h файл с конфигурацией по умолчанию*

После старта питания надо как-то проинициализировать драйвер. Особенно при первом запуске, когда NVRAM ещё пустая. Для этого создаем отдельные файлы для конфигов по умолчанию. Это способствует методологии "код отдельно, конфиги отдельно".

#include "ds3231_config.h"

#ifdef HAS_I2C
#include "i2c_drv.h"
#endif

#include "data_utils.h"
#include "ds3231_types.h"

const Ds3231Config_t Ds3231Config[]={
    {.num=1, .i2c_num=1, .valid=true, .hour_mode=HOUR_MODE_24H,},
};

Ds3231Handle_t Ds3231Item[]={
    {.num=1, .valid=true, .init=false,}
};

const Ds3231RegConfig_t Ds3231RegConfig[]={
};

uint32_t ds3231_get_reg_config_cnt(void){
    uint8_t cnt=0;
    cnt = ARRAY_SIZE(Ds3231RegConfig);
    return cnt;
}


uint32_t ds3231_get_cnt(void){
    uint8_t cnt=0;
    cnt = ARRAY_SIZE(Ds3231Config);
    return cnt;
}

--Драйвер должен легко масштабироваться

" На всё есть свой конфиг "
(Валентин  из "Фауст" И. В. Гёте)

Конфиг и сам драйвер надо писать так, чтобы драйвер поддерживал сразу несколько экземпляров сущностей драйвера. Так драйвер можно будет масштабировать новыми узлами.

#include "lis3dh_config.h"

#include "data_utils.h"
#include "lis3dh_types.h"

static const Lis3dhRegVal_t MemoryBlob[] = {
    {.addr=LIS3DH_REG_CTRL_REG1, .Reg.CtrlReg1={ .xen = 1, .yen = 1, .zen = 1, .lpen= 0, .odr=LIS3DH_DATA_RATE_100_HZ, },},
    {.addr=LIS3DH_REG_CTRL_REG2, .Reg.CtrlReg2={ .hpm = LIS3DH_HIGH_PASS_FILT_NORMAL, },},
    {.addr=LIS3DH_REG_CTRL_REG4, .Reg.CtrlReg4={ .fs=LIS3DH_FULL_SCALE_16G, .sim = 1, .st=LIS3DH_SELF_TEST_NORMAL, .hr=1, .bdu=0, .ble=0,},}
};

const Lis3dhConfig_t Lis3dhConfig[] = {
    {
        .num = 1,
        .i2c_num = 1,
        .sa0_pin_value = 1, /*See schematic 3.3V*/
        .RegConfig = MemoryBlob,
        .cfg_reg_cnt = ARRAY_SIZE(MemoryBlob),
        .chip_addr = LIS3DH_I2C_ADDR_1,
        .name = "LIS3DHYG",
        .valid = true,
    },
    {
       .num = 2,
        .i2c_num = 2,
        .sa0_pin_value = 1, /*See schematic 3.3V*/
        .RegConfig = MemoryBlob,
        .cfg_reg_cnt = ARRAY_SIZE(MemoryBlob),
        .chip_addr = LIS3DH_I2C_ADDR_0,
        .name = "LIS3DHYG",
        .valid = true,
    },
};

Lis3dhHandle_t Lis3dhInstance[] = {
{    .num = 1,    .valid = true,    .init = false,}
{    .num = 2,    .valid = true,    .init = false,}
};

uint32_t lis3dh_get_cnt(void) {
    uint8_t cnt = 0;
    cnt = ARRAY_SIZE(Lis3dhConfig);
    return cnt;
}

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

Вот вам яркий пример. Даже если в релизной электронной плате у Вас, условно, всего один акселерометр (например lis3dh), то для модульных тестов уже к отладочной плате, как ни крути, а придется прикрепить перемычками два или даже три акселерометра, сконфигурировать их по-разному и убедиться, что они показывают одно и то же ускорение g. Поэтому всегда и везде надо заранее писать код так, чтобы его можно было масштабировать. Масштабирование нужно даже не сколько для приложения, сколько для модульного тестирования этого самого приложения. Так уж завелось, что прошивку обычно пишут для одной платы, а отлаживают части прошивки на другой отладочной плате от вендора. К этому приходится приспосабливаться масштабированием ПК.

xxx_commands.с/xxx_commands.h файл с командами CLI*

У каждого взрослого компонента должна быть ручка для управления. В мире компьютеров исторически еще со времен UNIX (в 197x) такой "ручкой" является интерфейс командной строки (CLI) поверх UART. Поэтому создаем отдельные файлы для интерпретатора команд для каждого конкретного драйвера. Буквально 3-4 команды: инициализация, диагностика, get /set регистра. Так можно будет изменить логику работы драйвера в Run-Time. Вычитать сырые значения регистров, прописать конкретный регистр. Показать диагностику, серийный номер, ревизию , пулять пакеты в I2C, SPI, UART, MDIO и т. п.

#ifndef DWM1000_COMMANDS_H
#define DWM1000_COMMANDS_H

#include <stdbool.h>
#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

#ifndef HAS_DIAG
#error "+ HAS_DIAG"
#endif

#ifndef HAS_DWM1000
#error "+ HAS_DWM1000"
#endif

#ifndef HAS_DWM1000_COMMANDS
#error "+HAS_DWM1000_COMMANDS"
#endif

bool dwm1000_read_register_command(int32_t argc, char* argv[]);
bool dwm1000_tx_buff_command(int32_t argc, char* argv[]);
bool dwm1000_read_reg_file_command(int32_t argc, char* argv[]);
bool dwm1000_read_offset_command(int32_t argc, char* argv[]);
bool dwm1000_read_tx_buff_command(int32_t argc, char* argv[]);
bool dwm1000_read_rx_buff_command(int32_t argc, char* argv[]);
bool dwm1000_write_tx_buff_command(int32_t argc, char* argv[]);
bool dwm1000_init_command(int32_t argc, char* argv[]);
bool dwm1000_diag_command(int32_t argc, char* argv[]);
bool dwm1000_reset_command(int32_t argc, char* argv[]);

#define DWM1000_COMMANDS                                                   \
    CLI_CMD( "drtb", dwm1000_read_tx_buff_command, "Dwm1000ReadTxBuff"),   \
    CLI_CMD( "drrb", dwm1000_read_rx_buff_command, "Dwm1000ReadTxBuff"),   \
    CLI_CMD( "dtb", dwm1000_tx_buff_command, "Dwm1000TxBuff"),             \
    CLI_CMD( "dwt", dwm1000_write_tx_buff_command, "Dwm1000WriteTxBuff"),  \
    CLI_CMD( "drr", dwm1000_read_register_command, "Dwm1000ReadReg"),      \
    CLI_CMD( "dro", dwm1000_read_offset_command, "Dwm1000ReadOffSet"),     \
    CLI_CMD( "dwd", dwm1000_diag_command, "Dwm1000Diag"),                  \
    CLI_CMD( "dwi", dwm1000_init_command, "Dwm1000Init"),                  \
    CLI_CMD( "dwr", dwm1000_reset_command, "Dwm1000Reset"),

#ifdef __cplusplus
}
#endif

#endif /* DWM1000_COMMANDS_H */

xxx_diag.с/xxx_diag.h файлы с диагностикой*

У каждого программного компонента есть куча всяческих констант и структур. Значения констант подобраны вендором обычно специфические. Просто глядя на константу ничего про нее сказать нельзя. Поэтому эти константы надо интерпретировать в строки для человека-понимания. Аналогично с типами данных. Поэтому создается отдельный файл xxx_diag.с с функциями-отображениями. Суть проста: даешь бинарное значение константы и тут же получаешь её значение в виде текстовой строчки. Аналогично с типами данных. Даешь указатель на структуру, получаешь ее представление в виде строки. Это и есть диагностика. Эти функции-отображения как раз вызывает CLI-шка и компонент логирования при логе инициализации программы.

const char* DacLevel2Str(uint8_t code){
    const char *name="?";
    switch(code){
    case DAC_LEV_CTRL_INTERNALY: name="internally"; break;
    case DAC_LEV_CTRL_LOW:       name="low"; break;
    case DAC_LEV_CTRL_MEDIUM:    name="medium"; break;
    case DAC_LEV_CTRL_HIGH:      name="high"; break;
    }
    return name;
}

const char* SpiConfigToStr(const SpiConfig_t* const Config) {
    if(Config) {
        sprintf(text, "SPI%u", Config->num);
        snprintf(text, sizeof(text), "%sRate:%u Hz,", text, Config->bit_rate_hz);
        snprintf(text, sizeof(text), "%sBitOrder:%s,", text, SpiBitOrderToStr(Config->bit_order));
        snprintf(text, sizeof(text), "%sPha:%s", text, SpiPhaseToStr(Config->phase));
        snprintf(text, sizeof(text), "%sPol:%s,", text, SpiPolarityToStr(Config->polarity));
        snprintf(text, sizeof(text), "%sChipSel:%s,", text, SpiChipSelModeToStr(Config->chip_select));
        snprintf(text, sizeof(text), "%sIRQp:%u,", text, Config->irq_priority);
        snprintf(text, sizeof(text), "%s%s,", text, Config->name);
        snprintf(text, sizeof(text), "%sMOSI:%s,", text, GpioPadToStr(Config->PadMosi));
        snprintf(text, sizeof(text), "%sMISO:%s,", text, GpioPadToStr(Config->PadMiso));
        snprintf(text, sizeof(text), "%sCS:%s,", text, GpioPadToStr(Config->PadCs));
        snprintf(text, sizeof(text), "%sSCK:%s,", text, GpioPadToStr(Config->PadSck));
    }

    return text;
}

test_xxx.с/test_xxx.h Файлы с модульными тестами диагностикой*

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

Если в вашем коде нет модульных тестов, то не ждите к себе хорошего отношения. Так как код без тестов - это Филькина грамота. Как ни крути...

Make файл xxx.mk для правил сборки драйвера из Make*

Сборка из Make это самый мощный способ управлять модульностью и масштабируемостью любого кода. С make можно производить выборочную сборку драйвера в зависимости от располагаемых ресурсов на печатной плате. Код станет универсальным и переносимым. При сборке из Makefile(ов) надо для каждого логического компонента или драйвера вручную определять make файл. Make - это целый отдельный язык программирования со своими операторами и функциями. Спека GNU Make всего навсего это 224 страницы.

ifneq ($(SI4703_MK_INC),Y)
    SI4703_MK_INC=Y

    mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST)))
    $(info Build  $(mkfile_path) )

    SI4703_DIR = $(WORKSPACE_LOC)Drivers/si4703
    #@echo $(error SI4703_DIR=$(SI4703_DIR))

    INCDIR += -I$(SI4703_DIR)

    OPT += -DHAS_SI4703
    OPT += -DHAS_MULTIMEDIA
    RDS=Y

    FM_TUNER=Y
    ifeq ($(FM_TUNER),Y)
        OPT += -DHAS_FM_TUNER
    endif
    
    SOURCES_C += $(SI4703_DIR)/si4703_drv.c
    SOURCES_C += $(SI4703_DIR)/si4703_config.c

    ifeq ($(RDS),Y)
        OPT += -DHAS_RDS
        SOURCES_C += $(SI4703_DIR)/si4703_rds_drv.c
    endif

    ifeq ($(DIAG),Y)
        ifeq ($(SI4703_DIAG),Y)
            SOURCES_C += $(SI4703_DIR)/si4703_diag.c
        endif
    endif

    ifeq ($(CLI),Y)
        ifeq ($(SI4703_COMMANDS),Y)
            OPT += -DHAS_SI4703_COMMANDS
            SOURCES_C += $(SI4703_DIR)/si4703_commands.c
        endif
    endif

endif

Вот так должен примерно выглядеть код драйвера в папке с проектом:

Файл xxx_dep.h с указанием зависиммостей
*9--Добавить xxx_dep.h файл с проверками зависимостей на фазе препроцессора. Это позволит отловить на стадии компиляции ошибки отсутствия драйверов, которые нужны для этого драйвера.

#ifndef DWM3000_DEPENDENCIES_H
#define DWM3000_DEPENDENCIES_H

#ifndef HAS_DWM3000
#error "+HAS_DWM3000"
#endif

#ifndef HAS_SPI
#error "+HAS_SPI"
#endif

#ifndef HAS_GPIO
#error "+HAS_GPIO"
#endif

#ifndef HAS_MCU
#error "+HAS_MCU"
#endif

#ifndef HAS_LIMITER
#error "+HAS_LIMITER"
#endif

#endif /* DWM3000_DEPENDENCIES_H  */

10--xxx.gvi файл на языке Graphviz явно указывающий зависимости внутри программного компонента. Это надо для авто генерации документации

subgraph cluster_Keepass {
    style=filled;
    color=khaki1;
    label = "Keepass";

    Base64->KEEPASS
    Salsa20->KEEPASS
    LIFO->XML
    GZip->KEEPASS
    AES256->KEEPASS
    XML->KEEPASS
    SHA256->KEEPASS
    COMPRESSION->KEEPASS
}

Подробнее про генерацию зависимостей можно почитать тут

11--Должен быть файл xxx_preconfig.mk. Дело в том что перед запуском сборки хорошо бы проинициализировать переменные окружения, которые нужны для данной конкретной сборки. Часть этих переменных можно прописать в корневом config.mk. Однако можно случайно упустить какие-то конкретные зависимости. По этой причине каждый драйвер должен содержать файл xxx_preconfig.mk для явного определения зависимостей.

$(info AT24CXX_PRECONFIG_MK_INC=$(AT24CXX_PRECONFIG_MK_INC) )

ifneq ($(AT24CXX_PRECONFIG_MK_INC),Y)
    AT24CXX_PRECONFIG_MK_INC=Y

    TIME=Y
    AT24CXX=Y
    I2C=Y
    GPIO=Y
endif

-------------------------------------------------

Со структурой драйвера приблизительно определились. Хорошо... Теперь буквально несколько слов о функционале этого обобщенного программного компонента.

1--Должна быть инициализация чипа, функция bool xxx_init(void). Причем повторная инициализация драйвера или любого другого программного компонента не должна приводить к зависанию прошивки или иным ошибкам. Первоначальная проверка link(а), запись строчки в логе загрузки, прописывание либо конфигов по умолчанию, либо конфигов из on-chip Nor FlashFs, определение уровня логирования для данного компонента.

2--Каждый программный компонент должен проверять свой конфиг перед его применением. Это требование ISO-26262. Дело в том что очень часто ошибки в программах возникают потому, что программист некорректно сформировал конфигурацию. В этом случае на стадии исполнения программа внутри функции xxx_init() должна выдать ошибку в случае некорректного конфига.

3--У каждого программного компонента должны быть счетчики всяческих разнородных событий: количество отправок, приёмов, счетчики ошибок, прерываний. Это нужно для процедуры health monitor. Чтобы драйвер сам себя периодически проверял на предмет накопления ошибок и в случае обнаружения мог отобразить в лог (UART или SD карта) красный текст.

4--Если ваш чип с I2C, то Вам очень-очень повезло, так как в интерфейсе I2C есть бит подтверждения адреса и можно разом просканировать всю I2C шину. Драйвер I2C должен обязательно поддерживать процедуру сканирования шины и печатать таблицу доступных адресов. Вот как тут.

5--У каждого драйвера должна быть функция вычитывания всех сырых регистров разом. Это называется memory blob. Так как поведение чипа целиком и полностью определяется значениями его внутренних регистров. Вычитывание memory blob(а) позволит визуально сравнить конфигурацию с тем, что прописано в datasheet(е) и понять в каком режиме чип работает прямо сейчас. Сравнивая сырые значения регистров можно быстро выявить причину ошибки в частых случаях, когда два программиста писали свои реализации одного и того же драйвера, например CAN, SPI или ASIC SX1262.

6--Должен быть механизм непрерывной проверки SPI / I2C / MDIO link(а). Это позволит сразу определить проблему с проводами, если произойдет потеря link(а). Обычно в нормальных чипах есть регистр ChipID (например DW1000). В некоторых микросхемах этот регистр называется who_am_I. Прочитали регистр ID, проверили с тем, что должно быть в спеке (datasheet), значение совпало - значит есть link. Успех! С точки зрения надежности не стоит вообще закладывать в проект чипы без ChipID именно по этой простой причине, что их иначе невозможно протестировать.

7--Должен быть предусмотрен механизм записи и чтения отдельных регистров из командной строки поверх UART-shell. Это поможет воспроизводить и находить ошибки далеко в run-time-е.

*8--В суперцикле должна быть функция xxx_proc() для опроса (poll-инга) регистров чипа, его переменных и событий. Эта функция будет синхронизировать удаленные регистры чипа и их отражение в RAM памяти микроконтроллера. Если это HAL драйвер, то всегда периодически опрашивайте статусные регистры прямо в супер цикле. Эта функция proc, в сущности, и будет делать всю основную работу по функционалу и бизнес логике программного компонента. Обычно proc крутит конечные автоматы FSM. Она может работать как в супер цикле main, в отдельном потоке, так и вовсе на отдельном процессорном ядре.

9(Advanced)--Должна быть диагностика чипа. В идеале даже встроенный интерпретатор регистров каждого битика, который хоть что-то значит в карте регистров микросхемы. Либо, если нет достаточно On-Chip NorFlash(а), должна быть подготовлена отдельная DeskTop утилита для полного и педантичного синтаксического разбора memory blob(а), вычитанного из UART. Вот пример такой утилиты: https://github.com/aabzel/tja1101-register-value-blob-parser Так как визуально анализировать переменные, глядя на поток нулей и единиц, если вы не выучили в школе шестнадцатиричную таблицу умножения, весьма трудно и можно легко ошибиться. Поэтому интерпретатор регистров понадобится при сопровождении и отладке гаджета.

*10--У каждого программного компонента должна быть версия. Должна быть поддержка чтения версии компонента в run-time

#define KEEPASS_COMPONENT_VERSION "1.2"

При каждом изменении в коде драйвера версию надо увеличивать на 1.

11--Если у чипа есть внутренние состояния (Idle, Rx, Tx и проч), то об изменении состояния надо сигнализировать в UART Log. Это нужно для отладки драйвера чипа.

*12--Если ваш чип является трансивером в какой-то физический интерфейс, будь-то проводной (10BASE5, CAN, LIN, 1-Wire, RS485, MIL-STD-1553, ARINC) или беспроводной (LoRa, UWB, GFSK), то чип должен периодически посылать Hello пакеты в эфир. Их еще называют Blink пакет или Heartbeat сообщение. Это позволит другим устройствам в сети понять, кто вообще живет на шине, а кто вовсе завис.

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

*14--Если есть функция, которая что-то устанавливает, то должна быть и функция, которая это что-то прочитывает. Проще говоря, у каждого setter(а) должен быть getter, подобно тому к в математике у каждой функции есть обратная функция. У экспоненты - логарифм, у корня - возведение во вторую степень, у sin(x) - arcsin(x) и так далее, понимаете?

Даешь каждому setter(у) - getter !

Что должно быть в папке драйвера?

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

Минимальный набор файлов для одного модуля
Минимальный набор файлов для одного модуля

Вывод

Благодаря такой организации программного компонентов Вы можете наращивать функционал прошивки хоть до бесконечности при этом сложность проекта будет оставаться на том же низком уровне. Таким образом Вы можете масштабироваться хоть до Луны!

То, что тут перечислено - это базис любого драйвера. Своего рода ортодоксально-каноническая форма для любого программного компонента.
Остальной код зависит уже от конкретного ASIC чипа или HAL подсистемы (UART, SPI, DMA и т.п.), будь это чип управления двигателем, беспроводной трансивер, RTC или простой датчик давления.

Ещё раз, данная структура драйвера это, господа, не мои фантазии. Примерно в таком же виде генерируют embedded коды топовые мировые автомобильные корпоративные генераторы сорсов, такие как

1--Vector DaVinci Configurator Classic / Pro
2--EB tresos Studio
3--ETAS ISOLAR-A,
4--Mentor VSTAR,
5--Artop

Вот так...

Если проводить аналогию из медицины, то это как центрифугирование крови. Кровь делится на плазму, эритроциты , тромбоциты, красные кровяные тельца и прочее. Так и программный компонент тоже делится на свои фракции. Это ядро, константы, типы данных, диагностика, параметры для NVRAM, файл скрипта сборки, зависимости, конфиги, CLI и тесты. Вот как-то так.

Как видите, чтобы написать адекватный удобный программный компонент надо учитывать достаточно много нюансов и проделать некоторую инфраструктурную работу.

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

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

Если есть замечания на тему того, какими ещё атрибутами должен обладать обобщенный программный компонент (например периферийного I2C / SPI / MDIO чипа), то пишите в комментариях.

Словарь

Акроним

Расшифровка

SWC

SW component

ПК

Программный компонент

SW

Software

Links

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Вам случалось писать драйвер для какого-нибудь периферийного I2C/SPI/MDIO чипа?
71.3%да82
28.7%нет33
Проголосовали 115 пользователей. Воздержались 5 пользователей.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Вы в общем согласны с отмеченными в тексте атрибутами?
70.65%да65
29.35%нет27
Проголосовали 92 пользователя. Воздержались 17 пользователей.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Вы работали с интерфейсом I2C?
91.67%да88
8.33%нет8
Проголосовали 96 пользователей. Воздержались 2 пользователя.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Вы работали с интерфейсом SPI?
89.58%да86
10.42%нет10
Проголосовали 96 пользователей. Воздержались 2 пользователя.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Вы работали с интерфейсом MDIO?
29.21%да26
70.79%нет63
Проголосовали 89 пользователей. Воздержались 7 пользователей.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Вы писали на DeskTop(е) утилиты-интерпретаторы регистров какого-нибудь I2C/SPI чипа?
21.05%да8
78.95%нет30
Проголосовали 38 пользователей. Воздержались 2 пользователя.