Современнные Системы-на-Кристалле (SoC) содержат в себе десятки различных контроллеров, вариативность которых меняется в зависимости от поколения или ревизии чипов того или иного производителя. Особо выделяются контроллеры системного тактирования (Clock) и сброса (Reset), объем функциональности которых охватывает все оставшиеся контроллеры более узкого назначения.
В этой статье мы расскажем о новой разработанной подсистеме управления такими блоками в контексте операционной системы реального времени "Нейтрино". Затронем небольшую предысторию её создания, общую архитектуру с примерами кода и пример использования.
Вступление
Одной из ключевых особенностей ЗОСРВ "Нейтрино" является микроядерная архитектура, представляющая из себя инфраструктуру изолированных сервисов (менеджеры ресурсов, драйвера и пользовательские программы). Коммуникации между ними строятся вокруг концепций общей памяти и передачи сообщений микроядром.
В процессе разработки и портирования различных драйверов интерфейсов, зачастую возникает проблема синхронизации и разделения доступа к физическим ресурсам (в случаях когда ряд драйверов или пользовательских программ напрямую обращаются к регистрам одного и того же контроллера), что приносит некоторые неудобства и затягивает процесс разработки, не говоря уже и об отсутствии продуманного HAL (Hardware Abstraction Layer) для общих блоков SoC.
Регулярно решая такие задачи необобщенными способами при разработке BSP и драйверов различных интерфейсов, было решено спроектировать и разработать подсистему, предоставляющую интерфейс к системным управляемым блокам СнК (SoC).
Архитектура подсистемы
Подсистема получила название "Подсистема управления общими элементами платформ" или platform-control
.
Состав:
- Библиотека
libplatform-control
, содержащая в себе прикладные интерфейсы подсистемы (о них будет расскзано позже). - Менеджер ресурсов
platform-control
, обрабатывающий запросы клиентов. - Внутренний драйверный интерфейс динамически подгружаемых драйверов (частично также будет рассмотрен).
- Расширяемый набор загружаемых драйверов для различных платформ (SoC), имеющих в имени префикс
devp-*
. В их задачи входит непосредственное управление аппаратными блоками платформы (Clock, Reset, Power-Domain и т.д.).
Взаимозависимость компонентов в архитектуре имеет вид:
Клиентские приложения в данном случае — преимущественно более высокоуровневые драйвера, которым требуется сервис доступа к блокам SoC.
В микроядерных ОС любой код, даже драйверный, интерпретируется со стороны ядра как прикладной. Их можно спокойно перезапускать, дебажить и издеваться разными способами без существенного влияния на работоспособность системы в целом.
Модель работы подсистемы
При запуске менеджер ресурсов platform-control
загружает драйвера devp-*.so
и ищет их точки входа. Она определяется в драйверах с помощью структуры plat_ctrl_funcs_t
. Далее, для коммуникации клиента с менеджером используется клиентское API, функции которого формируют и отправляют команды используя devctl(). platform-control
обрабатывает принятые команды и вызывает функции драйверного API, реализации которых определены в драйвере через стркутуру plat_ctrl_funcs_t
.
Последовательность вызова функций в общей модели взаимодействия имеет вид:
Библиотека управления подсистемой
Функциональность библиотеки включает реализацию пользовательского и драйверного интерфейсов, необходимые макроопределения, типы данных и структуры. Давайте немного разберемся в них.
Типы данных для различных блоков
Каждая из структур представляет унифицированное описание состояния, которое требуется настроить для того или иного управляемого блока. Пользователь (со стороны клиентского приложения) напрямую не взаимодействует с полями этих структур, т.к. для удобства самого пользователя эти действия инкапсулированы внутри функций пользовательского API (их мы рассмортим в следующем подразделе), но они важны для понимания тонкостей работы всей подсистемы.
Для примера рассмотрим структуру для управления подблоком Reset. Более предметное описание других структур будет расмотрено в следующей статье, которую планируем посвятить разработке эталонного платформоспецифичного драйвера "от и до".
/* Reset Controller configuration */
typedef struct {
uint32_t id;
uint32_t state;
#define PLAT_CTRL_RST_STATE_DISABLED (0 << 0)
#define PLAT_CTRL_RST_STATE_ENABLED (1 << 0)
} plat_ctrl_reset_cfg_t;
Структура состоит из двух полей определяющих состояние управляемого подблока:
-
id
— псевдо-номер линии сброса внутри управляемого подблока (определяется в заголовочном файле драйвераdevp-*
) -
state
— состояние, которое требуется присвоить линии, согласно псевдо-номеру, указанному полемid
.
В свою очередь поле state
описывается двумя состояниями, пресущими всем аппаратным линиям сброса современных СнК (SoC):
-
PLAT_CTRL_RST_STATE_ENABLED
— макроопределение состояния, соответсвующее включенной линии сброса. -
PLAT_CTRL_RST_STATE_DISABLED
— макроопределение состояния, соответсвующее выключенной линии сброса.
После заполнения данной структуры, формируется сообщение, которое отправляется для обработки менеджеру ресурсов platform-control
, используя вызов API int plat_ctrl_reset_assert(int fd, int id);
, основанный на общесистемной функции devctl().
Функции пользовательского API
Для основных типовых действий есть вызовы API, основные действия:
- включение/выключение линий сброса (Reset)
- включение/выключение линий питания (Power Domain)
- включение/выключение линий тактирования (Clock)
- настройка/получение частоты линий тактирования (Clock/PLL)
- настройка источника тактирования (Clock)
Данный набор функций предоставляет клиентскому приложению взаимодействовать с требуемыми аппаратными блоками SoC. Объявления функций пользовательского интерфейса хранятся в заголовочном файле platform-control.h
:
int plat_ctrl_close(int fd);
int plat_ctrl_open(void);
int plat_ctrl_getdrvinfo(int fd, plat_ctrl_drvinfo_t *drvinfo);
int plat_ctrl_pd_attach(int fd, int id);
int plat_ctrl_pd_deattach(int fd, int id);
int plat_ctrl_reset_assert(int fd, int id);
int plat_ctrl_reset_deassert(int fd, int id);
int plat_ctrl_clk_enable(int fd, int id);
int plat_ctrl_clk_disable(int fd, int id);
int plat_ctrl_clk_set_rate(int fd, int id, uint32_t rate);
int plat_ctrl_pll_set_rate(int fd, int id, uint32_t rate);
int plat_ctrl_clk_set_parent(int fd, int id, uint32_t parent);
uint32_t plat_ctrl_clk_get_rate(int fd, int id);
uint32_t plat_ctrl_pll_get_rate(int fd, int id);
На примере уже известной нам структуры с описанием состояния блока Reset, рассмотрим изнутри логику работы реализующей функции plat_ctrl_reset_assert()
. Данная функция заполняет структуру plat_ctrl_reset_cfg_t
и передает её менеджеру platform-control
, используя devctl(). Это приведёт к вызову на стороне сервиса функцию драйверного интерфейса с именем set_reset
, которая предоставляется драйвером devp-*
. Аргументом этого callback-а является ранее заполненная структура plat_ctrl_reset_cfg_t
с конфигурацией.
Псевдокод данной функции:
int plat_ctrl_reset_assert( int fd, int id )
{
plat_ctrl_reset_cfg_t cfg = {id, PLAT_CTRL_RST_STATE_ENABLED};
int error = EINVAL;
...
pthread_mutex_lock(&platform_mutex);
error = devctl(fd, DCMD_PLAT_CTRL_SET_RESET, &cfg, sizeof(plat_ctrl_reset_cfg_t), NULL);
pthread_mutex_unlock(&platform_mutex);
...
return (error);
}
В качестве синхронизации для многопоточных приложений используется мьютекс. Спин-блокировки в этом месте опасны, поскольку длительность отработки драйвером devctl() не детерминирована.
Обзор драйверного API
Низкоуровневая реализация интерфейса взаимодействия с контроллерами в составе SoC основана на концепции драйверных callback-функций. Эти функции принимают в качестве параметра структуры описания состояния управляемого устройства и на их основе устанавливают новое состояние аппаратного блока.
Интерфейс драйвера имеет вид:
typedef struct {
size_t size;
void* (*init)(void *hdl, char *options);
void* (*fini)(void *hdl);
int (*drvinfo)(void *hdl, plat_ctrl_drvinfo_t *info);
int (*set_pd)(void *hdl, plat_ctrl_pd_cfg_t *cfg);
int (*set_reset)(void *hdl, plat_ctrl_reset_cfg_t *cfg);
int (*set_clk)(void *hdl, plat_ctrl_clk_cfg_t *cfg);
int (*get_clk)(void *hdl, plat_ctrl_clk_cfg_t *cfg);
} plat_ctrl_funcs_t;
В следующей главе рассмотрим примеры использования функций драйверного и пользовательского интерфейсов.
Взаимодействие пользователя с подсистемой
Разобравшись с компонентами подсистемы и их взаимодействием друг с другом, погрузимся глубже и рассмотрим выдержки кода драйвера и общие примеры использования подсистемы со стороны клиентского приложения.
Драйвер
Драйвер отвечает за низкоуровневую настройку контроллеров, путем записи/чтения регистров на основе принятой конфигурации. Для сборки необходим заголовочный файл библиотеки platform-control.h
для доступа к структурам подсистемы.
Обязательным этапом является определение структуры plat_ctrl_funcs_t
с именем plat_ctrl_drv_entry
, именно её менеджер будет "искать" как точку входа при запуске драйвера. Поля струтуры указывают на функции драйверного API, содержащие реализацию того или иного функционала.
Обобщенный пример кода платформоспецифичного драйвера:
#include <hw/platform-control.h>
/* Точка входа при запуске менеджера ресурсов */
plat_ctrl_funcs_t plat_ctrl_drv_entry = {
sizeof(plat_ctrl_funcs_t),
soc_init, /* init() */
soc_fini, /* fini() */
NULL, /* drvinfo() */
NULL, /* set_pd() */
soc_set_reset, /* set_reset() */
NULL, /* set_clk() */
NULL /* get_clk() */
};
void * soc_init(void *hdl, char *options)
{
/* Выделение необходимых ресурсов */
...
return (hdl);
}
void * soc_fini(void *hdl)
{
/* Освобождение выделенных в `init` ресурсов */
...
free(hdl);
...
}
int soc_set_reset(void *hdl, plat_ctrl_reset_cfg_t *cfg)
{
/* Низкоуровневая настройка регистров Reset контроллера согласно параметрам из структкры `plat_ctrl_reset_cfg_t`*/
...
return (EOK);
}
В результате сборки, на выходе получаем динамическую библиотеку devp-example.so
.
Клиентское приложение
При запуске подсистемы, менеджер platform-control
создает устройство /dev/platform
, все взаимодействовия с подсистемой из клиентского приложения происходит через эту точку, используя функции (plat_ctrl_*
) пользовательского API.
platform-control -d example &
В первую очередь подключаем необходимый заголовочный файл библиотеки platform-control.h
и регистрируем файловый дескриптор подсистемы, открыв файл /dev/platform
с помощью функции plat_ctrl_open()
(в конце не забываем его закрыть используя plat_ctrl_close()
). Получив файловый дескриптор, можно эффективно использовать необходимый функционал подсистемы.
Пример сброса состояния произвольного контроллера из процесса клиентского приложения:
#include <hw/platform-control.h>
...
/* Регистрируем файловый дескриптор `/dev/platform` */
int fd = plat_ctrl_open();
...
/* Включаем сигнал сброса необходимой линии согласно параметру `id` */
if(plat_ctrl_reset_assert(fd, id) != EOK)
{
...
/* Сигнализирование о некорректной работе вызова */
...
}
delay(20);
/* Выключаем сигнал сброса необходимой линии согласно параметру `id` */
if(plat_ctrl_reset_deassert(fd, id) != EOK)
{
...
/* Сигнализирование о некорректной работе вызова */
...
}
...
/* Закрываем файловый дескриптор */
plat_ctrl_close(fd);
Заключение
Данная статья является ознакомительной и направлена на предварительное ознакомление пользователей с нововведениями в следующем номерном релизе ЗОСРВ "Нейтрино". Уже сейчас билды этих компонентов доступны в нашем публичном GIT-репозитории в составе BSP для OrangePi PC в рамках нашей академической программы. Таким образом данная статья послужит отличным дополнением к уже опубликованным материалам и поможет разобраться в архитектуре подсистемы.
В следующей статье по данной подсистеме будет рассмотрен законченный пример по разработке devp-*
драйвера и его использованию в клиентских приложениях. В скором времени будет опубликован пример реального кода в нашем GIT-репозитории, который ляжет в основу следующей статьи и позволит получить исчерпывающее представление о подсистеме.
Подписывайтесь на наш канал, чтобы быть в курсе свежих новостей
xztau
А какова цена ОС для использования на 100-150 устройств?