
В современных встраиваемых системах SPI остаётся одним из ключевых интерфейсов для обмена данными с периферийными устройствами — от датчиков до Flash-памяти. При этом эффективная работа с SPI требует не просто доступа к регистрам контроллера, а продуманной архитектуры, где драйвер выступает как последнее звено между программной логикой и аппаратной реализацией. В данной статье мы расскажем про архитектуру SPI и на практике разберем все этапы разработки SPI драйвера для нашей операционной системы реального времени "Нейтрино".
Общее представление архитектуры SPI драйверов
Прежде, чем затронуть непосредственно саму разработку SPI драйвера, стоит немного изучить взаимодействие компонентов подсистемы, в которой он функционирует. Подистема с SPI-периферией включает несколько уровней: клиентское приложение, управляющий менеджер, промежуточную библиотеку абстракции и, наконец, сам драйвер. Клиент, в роли которого может быть, например, приложение для опроса датчика или драйвер часов реального времени, не работает напрямую с аппаратной шиной. Вместо этого он вызывает высокоуровневые API, которые абстрагируют детали передачи.
В итоге подсистема состоит из следующих элементов:
Библиотека
libspi-master.a
— содержит в себе клиентские и драйверные интерфейсыМенеджер ресурсов
spi-master
— принимает клиентские запросы, координирует их и передает драйверу, обеспечивая согласованную работу нескольких контроллеров SPIЗагружаемый менеджером драйвер
spi-*.so
— взаимодействует с аппаратурой для конкретной платформы, управляя регистрами контроллера SPI
Взаимодействие клиента с менеджером осуществляется через клиентское API, функции которого формируют сообщение и передает его менеджеру через вызов MsgSendv(). В конечном итоге менеджер обрабатывает это сообщение и по нему определяет какую именно необходимо вызвать функцию, реализация которой уже определена в нашем драйвере.
Их взаимное расположение в архитектуре будет иметь следующий вид:

Типы данных SPI драйвера
Для начала разберем основные типы данных SPI драйвера:
spi_funcs_t
— структура, определяющая драйверный интерфейс:
#include <hw/spi-master.h>
ㅤ
typedef struct
{
size_t size;
void* (*init)( void *hdl, char *options );
void (*fini)( void *hdl );
int (*drvinfo)( void *hdl, spi_drvinfo_t *info );
int (*devinfo)( void *hdl, uint32_t device, spi_devinfo_t *info );
int (*setcfg)( void *hdl, uint16_t device, spi_cfg_t *cfg );
void* (*xfer)( void *hdl, uint32_t device, uint8_t *buf, int *len );
int (*dma_xfer)
( void *hdl, uint32_t device, spi_dma_paddr_t *paddr, int len );
} spi_funcs_t;
Структура содержит указатели на функции, которые реализуются драйвером, и к которым менеджер ресурсов обращается, делегируя аппаратные операции конкретному драйверу. Подробнее о них будет рассказано дальше.
spi_dev_t
— структура, определяющая дескриптор драйвера:
typedef struct
{
SPIDEV spi;
uint64_t pbase;
uintptr_t vbase;
uint8_t *pbuf;
uint8_t *rbuf;
int32_t xlen, tlen, rlen;
/* ... */
} spi_dev_t;
Данная структура создается разработчиком и может включать в себя различные данные в зависимости от потребностей устройства. Для примера мы включим базовые поля, такие как указатели на буферы данных, размер запрошенного и фактически обработанного объема информации, физический и смапированный адрес контроллера SPI.
Вероятно, вы уже успели заметить первое поле структуры — SPIDEV spi
. Для чего же всё-таки оно нужно? Данное поле необходимо для безопасного приведения указателя на данную структуру к указателю типа SPIDEV. Такой подход используется для интеграции с менеджером ресурсов, который оперирует структурами типа SPIDEV. Поскольку адрес структуры совпадает с адресом ее первого поля, функции менеджера могут корректно взаимодействовать с драйвером, даже не зная о существовании структуры типа spi_dev_t
.
spi_cfg_t
— структура, определяющая параметры SPI устройства:
#include <hw/spi-master.h>
ㅤ
typedef struct
{
uint32_t mode;
#define SPI_MODE_CHAR_LEN_MASK (0xFF)
#define SPI_MODE_CKPOL_HIGH (1 << 8)
#define SPI_MODE_CKPHASE_HALF (1 << 9)
#define SPI_MODE_BODER_MSB (1 << 10)
#define SPI_MODE_CSPOL_HIGH (1 << 11)
#define SPI_MODE_CSSTAT_HIGH (1 << 12)
#define SPI_MODE_CSHOLD_HIGH (1 << 13)
#define SPI_MODE_RDY_MASK (3 << 14)
#define SPI_MODE_RDY_NONE (0 << 14)
#define SPI_MODE_RDY_EDGE (1 << 14)
#define SPI_MODE_RDY_LEVEL (2 << 14)
#define SPI_MODE_IDLE_INSERT (1 << 16)
ㅤ
#define SPI_MODE_LOCKED (1 << 31)
ㅤ
uint32_t clock_rate;
} spi_cfg_t;
Структура необходима для конфигурации SPI-устройства. С помощью нее мы определяем как именно будет осуществляться передача данных с конкретным SPI устройством. Здесь присутствуют такие параметры как длина данных (SPI_MODE_CHAR_LEN_MASK
), настройка режимов SPI, установка полярности chip select и многое другое.
Разработка SPI драйвера
Инициализация SPI драйвера
Для начала необходимо создать структуру драйверного дескриптора. Возьмем для нашего драйвера самые базовые опции — установка адреса контроллера SPI, частоты и номера прерывания. И тогда структура будет выглядеть следующим образом:
#include <hw/spi-master.h>
ㅤ
typedef struct
{
SPIDEV spi;
uint64_t pbase; // физический адрес SPI контроллера
uintptr_t vbase; // виртуальный адрес SPI контроллера
uint32_t clock;
uint8_t irq;
uint8_t *pbuf; // указатель на начало буфера записи
uint8_t *rbuf; // указатель на начало буфера чтения
int32_t xlen, tlen, rlen;
} spi_dev_t;
Далее необходимо как-то обрабатывать наши опции драйвера и для этого реализуем простую функцию-парсер. В массиве определим наши опции:
static char *spi_opts[] =
{
[BASE] = "base",
[CLOCK] = "clock",
[IRQ] = "irq",
[END] = NULL
};
И тогда функция-парсер примет следующий вид:
#include <hw/spi-master.h>
ㅤ
int spi_options( spi_dev_t *dev, char *optstring )
{
int opt, rc = EOK;
char *options, *startptr, *c, *value;
if ( optstring == NULL )
{
return rc;
}
startptr = options = strdup( optstring );
while ( options && *options != '\0' )
{
c = options;
if ( (opt = getsubopt( &options, spi_opts, &value )) == -1 )
{
/* Error message */
rc = -1;
}
switch ( opt )
{
case BASE:
dev->pbase = strtoull( value, 0, 0 );
continue;
case CLOCK:
dev->clock = strtoul( value, 0, 0 );
continue;
case IRQ:
dev->irq = strtoul( value, 0, 0 );
continue;
}
}
free( startptr );
return rc;
}
Но при следующем этапе инициализации всплывает одна проблема — как же задать частоту SPI контроллера? Необходимо откуда-то драйверу узнать какая частота подается на SPI контроллер. Это можно сделать разными способами:
Через конфигурационные файлы, в которых захардкожена частота. Просто, но негибко. Такой подход снижает переносимость кода и не учитывает возможную динамическую смену частоты тактирования.
Через регистры SoC. Более надёжно, но каждый производитель имеет свою схему тактирования и разработчику придётся тратить время на разбор работы схемы.
Через API платформы. Наиболее корректное решение. Оно обеспечивает независимость от аппаратной специфики, позволяет централизованно управлять тактовыми частотами и даёт всем драйверам единый источник информации.
Мы будем использовать именно третий вариант. Для нашей ОС существует менеджер управления общими элементами платформ platform-control
. Про менеджер и саму подсистему подробно рассказывается в нашей статье.
В итоге функция инициализации spi_funcs_t::init() будет представлять из себя следующее:
#include <hw/spi-master.h>
ㅤ
void* spi_init( void *hdl, char *options )
{
spi_dev_t *dev;
uint32_t fd, clock;
int status = EOK;
dev = calloc( 1, sizeof(spi_dev_t) );
if( dev == NULL )
{
return NULL;
}
/* Установка дефолтных значений адреса и частоты */
dev->pbase = SPI0_BASE;
dev->irq = SPI_IRQ0;
dev->clock = SPI_RATE;
status = spi_options( dev, options );
if( status == -1 )
{
free( dev );
return NULL
}
/* Маппирование и сохранение виртуального адреса в dev->vbase */
/* ... */
/* Подключение обработчика прерывания */
/* ... */
/* Извлечение частоты с помощью platform-control
и запись делителя частоты в регистр */
fd = plat_ctrl_open();
clock = plat_ctrl_clk_get_rate(fd, SPI0_CLK);
/* ... */
/* Сохранение hdl дескриптора менеджера ресурсов
и возврат ему своего дескриптора */
dev->spi.hdl = hdl;
return dev;
}
Настройка конфигурации SPI устройств
Следующим этапом будет реализация функции настройки конфигурации для SPI устройств — spi_funcs_t::setcfg(). Для этого был заведен простой массив структур, в котором содержатся ID устройства, имя устройства и структура конфигурации spi_cfg_t
.
И тогда функция будет иметь следующий вид:
#include <hw/spi-master.h>
ㅤ
int spi_setcfg( void *hdl, uint16_t device, spi_cfg_t *cfg )
{
spi_dev_t *dev = hdl;
uintptr_t base = dev->vbase;
uint32_t devmode = 0;
uint8_t len;
/* По номеру device определяем для какого
именно устройства мы производим настройку */
memcpy( &devlist[id].cfg, cfg, sizeof(spi_cfg_t) );
devmode = devlist[id].cfg.mode;
/* Запись в регистры настроек тактовой частоты, chip select и т.д. */
/* ... */
return ( EOK );
}
Передача данных
И последняя из основных функций — это spi_funcs_t::xfer(), с помощью которой и осуществляется обмен данными.
Менеджер может передать в эту функцию 4 типа операций:
SPI_DEV_EXCHANGE
— данные пишутся и читаются одновременно;SPI_DEV_WRITE
— производится только запись;SPI_DEV_READ
— производится только чтение;SPI_DEV_WRITE_READ
— сначала идет запись, после которой сразу же осуществляется чтение.
Менеджер сдвигает номер device
на число в зависимости от типа операции, поэтому в функции драйвера можно определить этот тип операции. И если это необходимо, мы можем реализовывать логику для конкретного типа передачи. SPI_DEV_XFER_SHIFT
— сдвиг битов для размещения типа операции; SPI_DEV_XFER_MASK
— двухбитовая маска, применяемая после сдвига для извлечения значения типа. Все вышеперечисленные значения берутся из заголовочного файла библиотеки spi-master.h
Примерный шаблон функции передачи данных будет выглядеть так:
#include <hw/spi-master.h>
ㅤ
void* xfer( void *hdl, uint32_t device, uint8_t *buf, int *len )
{
spi_dev_t *dev = hdl;
uint8_t op_type;
/* Определние типа операции путем сдвига */
op_type = ( device >> SPI_DEV_XFER_SHIFT ) & SPI_DEV_XFER_MASK;
dev->xlen = *len;
dev->rlen = 0;
dev->tlen = 0;
/* Если необходима особая настройка или логика для типов операций */
if( op_type == SPI_DEV_EXCHANGE )
{
/* ... */
}
if( op_type == SPI_DEV_WRITE )
{
/* ... */
}
if( op_type == SPI_DEV_READ )
{
/* ... */
}
if( op_type == SPI_DEV_WRITE_READ )
{
/* ... */
}
/* Общая логика работы передачи */
/* ... */
return buf;
}
Но стоит учесть, что для операции SPI_DEV_WRITE_READ:
Длина буфера передачи
uint32_t
будет размещена сразу в буфереbuf
Длина буфера приема
uint32_t
будет размещена в буфере со смещением вuint32_t
Буфер передачи будет размещен в
buf
со смещением в2 * uint32_t
Буфер приема будет размещен в
buf
со смещением в2 * uint32_t + len
Заключение
В данной статье мы кратко познакомились с разработкой SPI драйверов для операционной системы реального времени Нейтрино. Разумеется, мы не смогли охватить все аспекты темы. Если вас интересует более глубокое погружение в реализацию SPI-драйверов — по ссылке вы найдёте подробную документацию по драйверному API, а также пример драйвера в нашем публичном репозитории. В будущем к документации добавится практическое руководство для разработчиков (dev-гайд).

da-nie
Вот есть у нас какой-нибудь микроконтроллер, где эта Нейтрино поднята. Как в системе вообще видится SPI этого контроллера? У вас тут указан:
uint64_t pbase; // физический адрес SPI контроллера uintptr_t vbase; // виртуальный адрес SPI контроллера
Я правильно понимаю, что сама низкоуровневая работа с каким-либо контроллером не показана?
Кстати, попутно вопрос на засыпку: как бы в Нейтрино узнать номер прерывания RTC (и не только), куда его назначила APIC? cat /proc/interrupts в QNX/Нейтрино ведь отсутствует.