Итак, вам дали плату, а в ней 4 навороченных умных периферийных чипа с собственными внутренними конфигурационными по SPI/I2C регистрами. Это могут быть такие чипы, как si4703, tic12400 или drv8711. Не важно, какой конкретно чип. Допустим, что на GitHub драйверов для вашего I2C/SPI чипа нет или качество этих open sourсe драйверов оставляет желать лучшего. Как же собрать качественный драйвер для I2C/SPI чипа?

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

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

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

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

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

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

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

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

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

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

Каждый драйвер нуждается в энергонезависимых параметрах с настройками. Именно эти настройки будут применятся при инициализации при старте питания. Надо где-то указать как минимум тип данных и имя параметра.

#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  */

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

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

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

У каждого взрослого компонента должна быть ручка для управления. В мире компьютеров исторически такой ручкой является интерфейс командной строки (CLI) поверх UART. Поэтому создаем отдельные файлы для интерпретатора команд для каждого конкретного драйвера. Так можно будет изменить логику работы драйвера в Run-Time. Вычитать сырые значения регистров, прописать конкретный регистр. Показать диагностику, серийный номер, ревизию , пулять пакеты в I2C, SPI, UART, MDIO и т. п.

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

У каждого драйвера есть куча констант. Эти константы надо интерпретировать в строки для человека. Поэтому создается файл с Hash функциями. Суть проста: даешь бинарное значение константы и тут же получаешь ее значение в виде строки. Эти Hash функции как раз вызывает 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;
}

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

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

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

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

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

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

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

1--Должна быть инициализация чипа. Первоначальная проверка Link(а), запись в логе загрузки и прописывание либо конфигов по умолчанию, либо конфигов из On-Chip NorFlashFs.

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

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

3--У каждого драйвера должна быть функция вычитывания всех сырых регистров разом memory blob, так как поведение чипа целиком и полностью определяется значениями его регистров. Вычитывание memory blob(а) позволит визуально сравнить конфигурацию с тем, что написано datasheet(е) и понять в каком режиме чип работает прямо сейчас.

4--Должен быть механизм непрерывной проверки link(а). Это позволит сразу определить проблему с проводами, если произойдет потеря link(а). Обычно в нормальных чипах есть регистр ChipID. Прочитали регистр ID, проверили с тем, что должно быть в спеке(datasheet), значение совпало - значит есть link. Успех. С точки зрения надежности не стоит закладывать в проект чипы без ChipID именно по этой причине.

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

6--В суперцикле должна быть функция xxxxx_proc() для опроса (poll(инга)) регистров чипа, его переменных и событий. Эта функция будет синхронизировать удаленные регистры чипа и их отражение в RAM памяти микроконтроллера. Эта функция proc и будет делать всю основную работу по функционалу. Она может работать как в суперцикле, так и в отдельном потоке.

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

Вывод

Это базис любого драйвера. Своего рода строительные леса. Остальной код зависит от конкретного чипа, будь это чип управления двигателем, беспроводной трансивер, RTC или простой датчик давления. Как видите, чтобы написать адекватный драйвер чипа надо учитывать достаточно много нюансов и проделать некоторую инфраструктурную работу. Не стесняйтесь разбивать драйвер на множество файлов. Это потом сильно поможет при custom(мизации), переносе и упаковке драйвера в разные проекты с разными ресурсами.

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

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


  1. INSTE
    30.08.2022 12:56
    +8

    Столы принесли, стулья поставили, салфетки раздали... А обед-то где?
    Тьфу, драйвер-то как написать?


    1. anwender95
      30.08.2022 13:00
      +6

      Я начинал так — смотрел на соседние драйвера и писал по аналогии =)


  1. N1X
    30.08.2022 13:00
    +8

    Как-то уж очень абстрактно и категорично...

    Во-первых это не туториал явно. Здесь нет ничего о том, как получить конкретный результат.

    Во-вторых заявления про "адекватность" драйвера, "должен быть интерфейс командной строки", они кхм... Смелые, наверное. Особенно учитывая расплывчатость термина. Кусок кода для AVR это тоже вполне себе драйвер (прослойка/абстракция между железкой и остальным кодом, даже без ОС), а там память то нужно и на что-то еще оставить, кроме драйвера. Да и UART отладочный один, и управляется он совсем в другом месте.


  1. Goron_Dekar
    30.08.2022 13:24
    +2

    Выше народ написал, что в статье не указано, как драйвер писать. Не согласен с этим. Как писать логику драйвера или запись spi регистров тут не написано, да. Но это и не драйвер. А вот как организовать нормальный, тестируемый и поддерживаемый HAL примерно объяснили.

    Единственное, что слегка огорчает, это зацикленности на CLI отладке. В статье не расписано, как отлаживать драйверы периферии с жёстким таймингом, хотя легко можно было бы указать, что тот же blob регистров и счётчики для разной перифирии, чувствительной к таймингом, можно лить в комп непрерывно usart/swo/USB и разбирать уже там.

    Судя по тексту статьи, драйвер предлагается писать только в прерываниях, потому что ни pool'ер, ни поток для FreeRTOS не упоминаются. Тоже не очень хорошо, вынуждает логику состояний держать в прерываниях или в пользовательском коде. Но для простых периферий в самый раз.


    1. INSTE
      30.08.2022 14:22
      +3

      Если бы статья называлась "Архитектура хорошо поддерживаемого драйвера" - вопросов бы не возникло.


      1. DungeonLords
        30.08.2022 14:48
        +2

        "Если бы статья называлась "Архитектура хорошо поддерживаемого драйвера" - вопросов бы не возникло." [2]
        Предлагаю @aabzelпереименовать.


  1. fiego
    30.08.2022 14:09
    +2

    Текст выглядит обобщением конкретного опыта на глобальный уровень. Но я смотрю со своей колокольни, я писал драйвера для I2C/SPI/8 bit parallel дисплеев. Формально подходит под заголовок статьи, но там:

    • тяжело покрыть тестами, поскольку неработающий дисплей видно глазами, а вебкамеру для контроля теста не поставишь;

    • не особо требуется перечисление типов;

    • нет прерываний (хотя, такие дисплеи тоже сущестуют);

    • сырые регистры вообще непонятно зачем знать;

    • непонятно как и зачем делать "интерфейс командной строки";

    • хотел бы я знать "механизм проверки линка", когда забыли подать питание на подсветку дисплея;

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


  1. kipar
    30.08.2022 16:43
    +2

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

    Если же рассматривать статью "как я для наших проектов пишу драйвер для очередного чипа" - всё более-менее ок, единственное что не понравилось - много возможностей для опечаток при копипасте. Я предпочитаю за счет макропроцессора делать как-то так:

    DECLARE_ENUM(DacLevel, 
     V_SET(DAC_LEV_CTRL_INTERNALY, 0x01)
     ...
     )

    и это объявление сразу и объявляет enum и задает преобразование в строку (параметр типа DacLevel будет отображаться в гуи не как число 1, а сразу как "DAC_LEV_CTRL_INTERNALY") и соответствующие команды для его установки.


  1. buldo
    31.08.2022 00:53
    +1

    По программировав немного для pi pico проникся CMake. И синтаксис понятнее, чем у make и внешнюю библиотеку( тот же драйвер), можно подключить, указав ссылку на репозиторий.


    1. aabzel Автор
      31.08.2022 14:02

      CMake не настолько гибкий как make.