В современных встраиваемых системах SPI остаётся одним из ключевых интерфейсов для обмена данными с периферийными устройствами — от датчиков до Flash-памяти. При этом эффективная работа с SPI требует не просто доступа к регистрам контроллера, а продуманной архитектуры, где драйвер выступает как последнее звено между программной логикой и аппаратной реализацией. В данной статье мы расскажем про архитектуру SPI и на практике разберем все этапы разработки SPI драйвера для нашей операционной системы реального времени "Нейтрино".

Общее представление архитектуры SPI драйверов

Прежде, чем затронуть непосредственно саму разработку SPI драйвера, стоит немного изучить взаимодействие компонентов подсистемы, в которой он функционирует. Подистема с SPI-периферией включает несколько уровней: клиентское приложение, управляющий менеджер, промежуточную библиотеку абстракции и, наконец, сам драйвер. Клиент, в роли которого может быть, например, приложение для опроса датчика или драйвер часов реального времени, не работает напрямую с аппаратной шиной. Вместо этого он вызывает высокоуровневые API, которые абстрагируют детали передачи.

В итоге подсистема состоит из следующих элементов:

  • Библиотека libspi-master.a — содержит в себе клиентские и драйверные интерфейсы

  • Менеджер ресурсов spi-master — принимает клиентские запросы, координирует их и передает драйверу, обеспечивая согласованную работу нескольких контроллеров SPI

  • Загружаемый менеджером драйвер spi-*.so — взаимодействует с аппаратурой для конкретной платформы, управляя регистрами контроллера SPI

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

Их взаимное расположение в архитектуре будет иметь следующий вид:

Типы данных SPI драйвера

Для начала разберем основные типы данных SPI драйвера:

  1. 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;

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

  1. 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.

  1. 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-гайд).


Телеграм-канал, в котором публикуются последние новости разработки нашей ОС

t.me/CBD_BC

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


  1. da-nie
    19.09.2025 11:15

    Вот есть у нас какой-нибудь микроконтроллер, где эта Нейтрино поднята. Как в системе вообще видится SPI этого контроллера? У вас тут указан:

    uint64_t pbase; // физический адрес SPI контроллера uintptr_t vbase; // виртуальный адрес SPI контроллера

    Я правильно понимаю, что сама низкоуровневая работа с каким-либо контроллером не показана?

    Кстати, попутно вопрос на засыпку: как бы в Нейтрино узнать номер прерывания RTC (и не только), куда его назначила APIC? cat /proc/interrupts в QNX/Нейтрино ведь отсутствует.