Как Написать Драйвер для очередного I2C/SPI Чипа

Итак, вам дали плату, а в ней 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

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

326009df820dd73bec8a604903ef3903.png

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

1--Должна быть инициализация чипа.

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

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

09ba2e46a08e7bddb8b6e359a5590b56.PNG

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

bbe648eba69af1dd9ef6c07abe384ba4.png

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

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 чипа, то пишите в комментариях.

© Habrahabr.ru