Драйвер elcore50

Описание

Драйвер elcore50 создаёт файл устройства /dev/elcore, а также по одному файлу устройства на каждое DSP-ядро: /dev/elcore0, /dev/elcore1 и т. д.

Взаимодействие с устройствами осуществляется через стандартные системные вызовы: open(), ioctl() и close().

Интерфейс драйвера elcore50 предоставляет следующую функциональность:

  1. Отображение областей памяти пользовательского процесса в адресное пространство DSP.

  2. Создание нового задания.

  3. Постановка заданий в локальную и глобальную очереди на выполнение на DSP.

  4. Уведомление о готовности заданий.

  5. Отслеживание текущего состояния задания.

  6. Обеспечение согласованности данных между CPU и DSP для заданной области памяти.

  7. Поддержка huge pages.

  8. Поддержка экспорта и импорта dma-buf.

  9. Отладка заданий.

Драйвер elcore50 отвечает за:

  1. Управление энергопотреблением DSP.

  2. Запуск заданий DSP.

  3. Отображение областей памяти между адресными пространствами пользовательского процесса и DSP.

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

  5. Идентификацию конкретного устройства DSP. У каждого устройства должен быть свой уникальный идентификатор, соответствующий физическому расположению устройства на чипе.

  6. Выделение XYRAM в DSP.

  7. ELcore-50 имеет возможность распределять внутреннюю память на две области: одна область работает как L2-кэш, вторая — как обычная память. Эти области могут быть разных размеров. Драйвер осуществляет корректную настройку распределения между L2-кэшем и обычной внутренней памятью.

  8. Управление сбросами DSP.

  9. Обработка системных вызовов DSP.

  10. Профилирование заданий DSP.

Параметры модуля ядра

Модуль ядра имеет следующие параметры:

  • caches — битовая маска, указывающая, какие из кэшей L1 или L2 должны быть включены или выключены. Значение 1 — кэш включен, значение 0 — кэш выключен.

    • Бит 0 управляет включением кэша L1.

    • Бит 1 управляет включением кэша L2.

    При этом, если задание затребовало в качестве локальной памяти более 256 КБайт, кэш L2 выключается принудительно, так как физически L2-кэш и XYRAM — это одно устройство с общей памятью.

    По умолчанию все кэши включены.

  • dbg_registers_simple — флаг, определяющий доступ отладчика к управляющим регистрам DSP-ядра. Допустимые значения:

    • 0 - доступ возможен к любым программно-доступным регистрам DSP-ядра.

    • 1 - доступ возможен только к регистровому и векторному файлам DSP-ядра, а также регистру PC.

    По умолчанию доступ возможен к любым программно-доступным регистрам DSP-ядра.

  • timer_period — период таймера для профилирования в микросекундах. См. Профилирование заданий DSP.

    По умолчанию таймер настроен на период 10 мс.

  • use_kretprobes — флаг для включения профилирования с помощью Kretprobes. См. Профилирование на базе Kretprobes.

    По умолчанию не задан.

  • kretprobes_max_active — количество параллельно обрабатываемых экземпляров вызовов одной и той же функции. См. Профилирование на базе Kretprobes.

    По умолчанию до 16 параллельных вызовов для каждой функции.

Параметры задаются при загрузке модуля ядра elcore50 в ОС Linux. Например, для того, чтобы выключить кэши L1 и L2 на уровне драйвера, необходимо выполнить:

# Выгрузить модуль ядра elcore50
modprobe -r elcore50

# Загрузить модуль ядра elcore50 с параметром caches=0
modprobe elcore50 caches=0

Подробнее о способах передачи параметров см. официальную документацию Linux Kernel.

Стандартные системные вызовы

Драйвер elcore50 реализует стандартные для файловых дескрипторов системные вызовы со следующими особенностями:

  • poll()

    • не реализован для файлового дескриптора устройства /dev/elcore*.

    • при указании файлового дескриптора задания ожидает завершения задания.

  • ioctl() c функциями, описанными в следующем разделе.

Описание IOCTL для устройства /dev/elcore

ioctl() данной группы работают с объектами, которые могут использоваться любым DSP-ядром.

Перед выполнением ioctl() данной группы необходимо открыть файл устройства /dev/elcore с помощью системного вызова open().

ELCORE50_IOC_CREATE_BUFFER

Экспорт dma-buf.

enum elcore50_buf_type {
  ELCORE50_CACHED_BUFFER_FROM_UPTR,
  ELCORE50_NONCACHED_BUFFER
};

struct elcore50_buf {
  /* out */
  int dmabuf_fd;

  /* unused */
  int mapper_fd

  /* in */
  enum elcore50_buf_type type;
  __u64 p;
  __u64 size;
};

Описание полей структуры elcore50_buf:

dmabuf_fd

Файловый дескриптор экспортируемого dma-buf.

type

Тип экспортуремого буфера. Допустимые значения:

  • ELCORE50_CACHED_BUFFER_FROM_UPTR — экспорт dma-buf на основе виртуального адреса из пользовательского пространства.

  • ELCORE50_NONCACHED_BUFFER — выделение некэшируемого буфера и экспорт dma-buf.

p

Виртуальный адрес буфера.

size

Размер буфера.

Перед вызовом ioctl() необходимо заполнить поля структуры elcore50_job из раздела «in». Для буферов типа ELCORE50_NONCACHED_BUFFER поле p игнорируется.

В случае успеха вызов ioctl() возвращает нулевое значение, поле dmabuf_fd содержит файловый дескриптор экспортируемого dma-buf.

ELCORE50_IOC_CREATE_MAPPER

Подготовка буфера к запуску задания. Драйвер инкапсулирует данные dmabuf’а во внутреннюю структуру, сбрасывает кэш буфера и экспортирует файловый дескриптор.

struct elcore50_buf {
  /* in */
  int dmabuf_fd;

  /* out */
  int mapper_fd;

  /* unused */
  enum elcore50_buf_type type;
  __u64 p;
  __u64 size;
};

Описание полей структуры elcore50_buf:

dmabuf_fd

Файловый дескриптор dma-buf, выделенный через ELCORE50_IOC_CREATE_BUFFER или аналогичный ioctl() других драйверов.

mapper_fd

Файловый дескриптор внутренней структуры буфера в драйвере.

Перед вызовом ioctl() необходимо заполнить поле dmabuf_fd.

В случае успеха вызов ioctl() возвращает нулевое значение, поле mapper_fd содержит файловый дескриптор внутренней структуры буфера в драйвере.

ELCORE50_IOC_SYNC_BUFFER

Синхронизация данных буфера.

enum elcore50_buf_sync_dir {
  ELCORE50_BUF_SYNC_DIR_TO_CPU,
  ELCORE50_BUF_SYNC_DIR_TO_DEVICE,
  ELCORE50_BUF_SYNC_BIDIRECTIONAL
};

struct elcore50_buf_sync {
  /* in */
  int mapper_fd;
  size_t offset;
  size_t size;
  enum elcore50_buf_sync_dir dir;
};

Описание полей структуры elcore50_buf_sync:

mapper_fd

Файловый дескриптор внутренней структуры буфера в драйвере, полученный после вызова ioctl() ELCORE50_IOC_CREATE_MAPPER.

offset

Смещение относильно начала буфера, начиная с которого выполняется синхронизация.

size

Размер синхронизируемой области памяти в байтах.

dir

Направление синхронизации. Допустимые значения:

  • ELCORE50_BUF_SYNC_DIR_TO_CPU — инвалидация кэш-памяти CPU.

  • ELCORE50_BUF_SYNC_DIR_TO_DEVICE — сброс кэш-памяти CPU.

  • ELCORE50_BUF_SYNC_BIDIRECTIONAL — сброс и инвалидация кэш-памяти CPU.

Перед вызовом ioctl() необходимо заполнить все поля структуры elcore50_buf_sync.

В случае успеха вызов ioctl() возвращает нулевое значение.

ELCORE50_IOC_CREATE_JOB

Создание нового задания.

#define ELCORE50_MAX_ELF_SECTIONS 64

enum elcore50_job_elf_section_type {
  ELCORE50_ELF_SECTION_CODE = 0,
  ELCORE50_ELF_SECTION_DATA = 1,
  ELCORE50_ELF_SECTION_DATA_CONST = 2
};

struct elcore50_job_elf_section {
  enum elcore50_job_elf_section_type type;
  int mapper_fd;
  int size;
  int virtual_address;
};

struct elcore50_job {
  /*in*/
  uint32_t num_elf_sections;
  uint32_t stack_virtual_address;
  struct elcore50_job_elf_section elf_sections[ELCORE50_MAX_ELF_SECTIONS];
  int hugepages;

  /*out*/
  int job_fd;
};


int ioctl(int fd, ELCORE50_IOC_CREATE_JOB, struct elcore50_job *job);

Описание полей структуры elcore50_job_elf_section:

type

Тип секции. Код, данные или константные данные.

mapper_fd

Файловый дескриптор секции, полученный с помощью ioctl() ELCORE50_IOC_CREATE_MAPPER.

size

Размер секции в байтах.

virtual_address

Адрес, по которому должна располагаться секция в адресном пространстве DSP.

Описание полей структуры elcore50_job:

num_elf_sections

Количество элементов массива elf_sections.

elf_sections

Массив, содержащий информацию о всех секциях ELF-файла.

stack_virtual_address

Адрес дна стека из ELF-файла. Адрес должен принадлежать одной из секций elf_sections.

hugepages

Использование huge page при настройке виртуального адресного пространства задачи. Допустимые значения:

  • 0 — huge page не используются. Виртуальное адресное пространство настраивается с использованием 4К страниц.

  • 1 — huge page используются. Виртуальное адресное пространство настраивается с использованием 4К, 2М и 1G страниц.

job_fd

Возвращемый дескриптор файла для данного задания.

Перед вызовом ioctl() необходимо заполнить поля структуры elcore50_job из раздела «in».

Системный вызов:

  • сохраняет ссылки на mapper’ы образа ELF-файла, созданные с помощью ioctl ELCORE50_IOC_CREATE_MAPPER.

  • создает карту виртуальной памяти.

  • создает фиктивные MMU-таблицы для внутренних диапазонов виртуальных адресов. Поскольку на этапе создания задания неизвестно на каком DSP-ядре будет оно выполняться, выкалываются диапазоны виртуальных адресов для всех DSP-ядер.

  • создает MMU-таблицы для ELF-секций. Не все буферы будут использовать 2M/1G страницы. Для использования 2M/1G страниц необходимо, чтобы буфер имел непрерывный физический блок >2M/1G с 2M/1G виртуальным выравниванием.

    Примечание

    Драйвер не гарантирует использование 2M/1G страниц.

  • создаёт файловый дескриптор экземпляра задания.

В случае успеха вызов ioctl() возвращает нулевое значение.

ELCORE50_IOC_ENQUEUE_JOB

Постановка экземпляра задания в глобальную очередь.

#define ELCORE50_MAX_JOB_AGRS 32

enum elcore50_job_arg_type {
  ELCORE50_TYPE_GLOBAL_MEMORY = 0,
  ELCORE50_TYPE_NC_GLOBAL_MEMORY = 1,
  ELCORE50_TYPE_LOCAL_MEMORY = 2,
  ELCORE50_TYPE_BASIC = 3,
  ELCORE50_TYPE_DMA_MEMORY = 4
};

struct elcore50_job_arg {
  enum elcore50_job_arg_type type;
  union {
    struct {
      int mapper_fd;
    } global_memory;
    struct {
      __u32 size;
    } local_memory;
    struct {
      __u32 size;
      __u64 p;
    } basic;
    struct {
      int mapper_fd;
    } dma_memory;
  };
};

struct elcore50_job_instance {
  /*in*/
  int job_fd;
  uint32_t argc;
  struct elcore50_job_arg args[ELCORE50_MAX_JOB_ARGS];
  uint32_t entry_point_virtual_address;
  uint32_t launcher_virtual_address;
  uint32_t launcher_stop_address;
  char name[255];
  int debug_enable;

  /*out*/
  int job_instance_fd;
};

int ioctl(int fd, ELCORE50_IOC_ENQUEUE_JOB, struct elcore50_job_instance *job_instance);

Описание полей структуры elcore50_job_arg:

type

Тип аргумента, передаваемого заданию.

job_arg

Значение аргумента, в зависимости от типа:

global_memory

Указатель и размер глобальной памяти. В дальнейшем возможно расширение: например, могут передаваться права доступа. Глобальной памятью является обычная системная память. Выделение и подготовка областей памяти осуществляется с помощью ioctl() ELCORE50_IOC_CREATE_BUFFER, ELCORE50_IOC_CREATE_MAPPER.

local_memory

Размер необходимой локальной памяти. Локальная память — это память XYRAM. Её нельзя отобразить в пользовательский процесс. Тем не менее, при запуске задания можно указать в качестве аргумента указатель на внутреннюю память определённого объёма, причём объём динамически задаёт пользовательский процесс. Эта память никак не инициализируется и может использоваться заданием для оверлейной обработки данных. Если в списке аргументов присутствует несколько аргументов типа ELCORE50_TYPE_LOCAL_MEMORY, то они будут располагаться во внутренней памяти друг за другом и если общий объём затребованной локальной памяти больше реального объёма, системный вызов вернёт ошибку.

basic

Размер аргумента и указатель на его значение. Например, если задание принимает в качестве аргумента cl_int, то size = sizeof(cl_int), а указатель будет представлять из себя указатель на целое число. Аргументы этого типа интерпретируются как передаваемые по значению и копируются на стек или в регистры в соответствии с документом «Компилятор С/С++ Clang для DSP Elcore50 (32-х битный режим). Соглашение о вызовах». Кроме того, эти аргументы должны быть скопированы драйвером во время системного вызова и должны храниться в элементе очереди заданий, чтобы изменение в пользовательском процессе соответствующей памяти (если задание долго находилось в очереди) не повлекло изменений значений аргументов.

dma_memory

Указатель и размер глобальной памяти. Аналогичен типу global_memory за исключением того, что выделяемый адрес имеет размер 48 бит, вследствие чего обращение по этому адресу возможно через VDMA и невозможно через DSP.

Описание полей структуры elcore50_job_instance:

job_fd

Файловый дескриптор задания, полученный с помощью ioctl() ELCORE50_IOC_CREATE_JOB.

argc

Количество аргументов в задании.

args

Аргументы задания.

entry_point_virtual_address

Адрес функции, которую необходимо выполнить. Задаётся в адресном пространстве DSP.

launcher_virtual_address

Адрес функции elcorecl_job_launcher(). Задаётся в адресном пространстве DSP. Если необходимо запустить задание без функции elcorecl_job_launcher(), записать в это поле ноль.

launcher_stop_address

Адрес инструкции, следующей за STOP в коде библиотеки elcore-runtime.

name

Название экземпляра задания.

debug_enable Флаг отладки. Допустимые значения:

  • 0 — Режим отладки выключен.

  • 1 — Режим отладки включен. Выполнение экземпляра задания будет остановлено на

    первой инструкции для дальнейшей отладки в соответствии с Отладка заданий DSP.

job_instance_fd

Возвращемый дескриптор файла для данного экземпляра задания.

Перед вызовом ioctl() необходимо заполнить поля структуры elcore50_job из раздела «in».

Системный вызов:

  • получает доступ к памяти пользовательского процесса (get_user_pages()) для аргументов задания, имеющих тип ELCORE50_TYPE_GLOBAL_MEMORY, ELCORE50_TYPE_NC_GLOBAL_MEMORY и ELCORE50_TYPE_DMA_MEMORY.;

  • формирует стек в соответствии с документом «Компилятор С/С++ Clang для DSP Elcore50 (32-х битный режим). Соглашение о вызовах»:

    • аргументы типа ELCORE50_TYPE_BASIC интерпретируются как передаваемые по значению;

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

    • аргументы типа ELCORE50_TYPE_DMA_MEMORY аналогичны аргументам типа ELCORE50_TYPE_GLOBAL_MEMORY за исключением того, что виртуальный адрес имеет размер 48 бит и предназначен для работы с VDMA;

    • аргументы типа ELCORE50_TYPE_LOCAL_MEMORY интерпретируются как указатели в виртуальном адресном пространстве DSP, направленные на соответствующую внутреннюю память.

  • рассчитывает размер L2-кэша, исходя из размера затребованной аргументами типа ELCORE50_TYPE_LOCAL_MEMORY локальной памяти. Используется максимально возможный размер L2-кэша для данной задачи.

  • создаёт файловый дескриптор экземпляра задания.

Все промежуточные данные, а именно: образ стека и значения аргументов, передаваемые через регистры; pinned страницы памяти пользовательского процесса (см. описание get_user_pages()); созданный файловый дескриптор экземпляра задания; — сохраняются как элемент очереди заданий и этот элемент ставится в конец очереди.

После этого системный вызов возвращает управление пользовательскому процессу.

Когда очередь доходит до некоторого экземпляра задания, драйвер:

  • создает MMU-таблицы для аргументов. Не все буферы будут использовать 2M/1G страницы. Для использования 2M/1G страниц необходимо, чтобы буфер имел непрерывный физический блок >2M/1G с 2M/1G виртуальным выравниванием.

    Примечание

    Драйвер не гарантирует использование 2M/1G страниц.

  • настраивает регистры MMU.

  • устанавливает указатель стека на вершину стека в данном образе;

  • настраивает размер L2-кэша;

  • копирует секцию PRAM в память PRAM;

  • копирует секцию XYRAM в память XYRAM;

  • если значение launcher_virtual_address не равно нулю, то в регистр PC записывает это значение, а регистр R8 устанавливает в значение, равное entry_point_virtual_address и запускает DSP-ядро. Если значение launcher_virtual_address равно нулю, то в регистр PC записывается значение, равное entry_point_virtual_address.

  • если значение debug_enable равно нулю, то запускает DSP-ядро. Если значение debug_enable не равно нулю, то переводит задание в состояние ELCORE50_JOB_STATUS_INTERRUPTED и не запускает DSP-ядро до выполнения ELCORE50_IOC_DBG_JOB_INSTANCE_CONTINUE, ELCORE50_IOC_DBG_STEP или завершения отладки путем закрытия отладочного файлового дескриптора, полученного путем выполнения ELCORE50_IOC_DBG_JOB_ATTACH.

Пользовательский процесс может вызвать функцию poll() c параметром elcore50_job_instance.job_instance_fd при необходимости дождаться завершения выполнения задания.

Задание во время выполнения не запрашивает у драйвера дополнительной памяти и работает только со стеком, данными переданными через аргументы типа ELCORE50_TYPE_GLOBAL_MEMORY и ELCORE50_TYPE_LOCAL_MEMORY, а также с секциями ELF-файла.

ELCORE50_IOC_GET_JOB_STATUS

Получение текущего состояния задания.

enum elcore50_job_instance_state {
  ELCORE50_JOB_STATUS_ENQUEUED = 0,
  ELCORE50_JOB_STATUS_RUN = 1,
  ELCORE50_JOB_STATUS_INTERRUPTED = 2,
  ELCORE50_JOB_STATUS_SYSCALL = 3,
  ELCORE50_JOB_STATUS_DONE = 4
};

enum elcore50_job_instance_error {
  ELCORE50_JOB_STATUS_SUCCESS = 0,
  ELCORE50_JOB_STATUS_ERROR = 1
};

struct elcore50_job_instance_status
{
  /*in*/
  int job_instance_fd;

  /*out*/
  enum elcore50_job_instance_state state;
  enum elcore50_job_instance_error error;
};

int ioctl(int fd, ELCORE50_IOC_GET_JOB_STATUS, struct elcore50_job_instance_status
  *job_status);

Описание полей структуры elcore50_job_instance_status:

job_instance_fd

Файловый дескриптор, возвращённый функцией ELCORE50_IOC_ENQUEUE_JOB.

state

Состояние задачи. Допустимые значения:

  • ELCORE50_JOB_STATUS_ENQUEUED — задание помещено в очередь готовых к выполнению задач.

  • ELCORE50_JOB_STATUS_RUN — задание находится на этапе выполнения.

  • ELCORE50_JOB_STATUS_INTERRUPTED — задание остановлено отладчиком.

  • ELCORE50_JOB_STATUS_SYSCALL — задание приостановлено. Выполняется системный вызов.

  • ELCORE50_JOB_STATUS_DONE — задание завершено.

error

Актуально в состоянии ELCORE50_JOB_STATUS_DONE. Содержит ошибку, если:

  • задание не смогло выполниться успешно, в этом случае в кольцевой буфер ядра Linux будет выведено сообщение вида:

    Job <jobname> failed with DQSTR: <dqstrvalue>

    где jobname - имя вызываемой функции, dqstrvalue - значение регистра признаков запросов на внешние прерывания (DQSTR).

    При отладке ошибок может быть полезным вывод дампа регистров DSP.

  • после завершения задания имеется активность в VDMA-каналах.

задание не смогло выполниться успешно. В противном случае ELCORE50_JOB_STATUS_SUCCESS.

Перед вызовом ioctl() необходимо заполнить поля структуры elcore50_job_instance_status из раздела «in».

Описание IOCTL для устройств /dev/elcoreX

Перед выполнением ioctl данной группы необходимо открыть файл устройства /dev/elcoreX, где X - номер DSP-ядра (/dev/elcore0, dev/elcore1 и т.д.) с помощью системного вызова open().

ELCORE50_IOC_CREATE_JOB

Аналогичен IOC_CREATE_JOB для файла устройства /dev/elcore.

Данная реализация оставлена для обратной совместимости со старым ПО.

ELCORE50_IOC_ENQUEUE_JOB

Постановка экземпляра задания в локальную очередь ядра.

int ioctl(int fd, ELCORE50_IOC_ENQUEUE_JOB, struct elcore50_job_instance *job_instance);

Описание ioctl аналогично ELCORE50_IOC_ENQUEUE_JOB для файла устройства /dev/elcore.

ELCORE50_IOC_GET_JOB_STATUS

Аналогичен ELCORE50_IOC_GET_JOB_STATUS для файла устройства /dev/elcore.

Данная реализация оставлена для обратной совместимости со старым ПО.

ELCORE50_IOC_GET_JOB_COUNT

Получение числа доступных экземпляров заданий, находящихся либо в локальной очереди, либо на исполнении.

int ioctl(int fd, ELCORE50_IOC_GET_JOB_COUNT, __u32 *count);

Предупреждение

Число экземпляров заданий count возвращается без учета экземпляров заданий, находящихся в глобальной очереди, для которых еще не назначено DSP-ядро.

В случае успеха вызов ioctl() возвращает нулевое значение.

ELCORE50_IOC_GET_JOB_LIST

Получения списка доступных экземпляров заданий, находящихся либо в локальной очереди, либо на исполнении.

struct elcore50_job_instance_info {
  long id;
  int pid;
  char name[255];
};

struct elcore50_job_instance_list {
  /* in */
  __u32 job_instance_count;
  struct elcore50_job_instance_list *list;

  /* out */
  __u32 job_instance_ret;
};

int ioctl(int fd, ELCORE50_IOC_GET_JOB_LIST, struct elcore50_job_instance_list *list);

Описание полей структуры elcore50_job_instance_info:

id

Уникальный идентификатор экземпляра задания.

pid

Идентификатор процесса, создавшего экземпляр задания.

name

Название экземпляра задания.

Описание полей структуры elcore50_job_instance_list:

job_instance_count

Число запрашиваемых экземпляров задания, полученное с помощью ioctl() ELCORE50_IOC_GET_JOB_COUNT.

info

Описание экземпляров задания.

job_instance_ret

Число описанных экземпляров заданий в поле elcore50_job_instance_list.info.

Поля elcore50_job_instance_list.job_instance_count и elcore50_job_instance_list.job_instance_ret могут отличаться в случае, если между вызовами ELCORE50_IOC_GET_JOB_COUNT и ELCORE50_IOC_GET_JOB_LIST добавлялись или удалялись некоторые экземпляры заданий из очереди.

Перед вызовом ioctl() необходимо заполнить поля структуры elcore50_job_instance_list из раздела «in»: определить elcore50_job_instance_list.job_instance_count с помощью ioctl() ELCORE50_IOC_GET_JOB_COUNT и выделить необходимую память под elcore50_job_instance_list.info. В случае успеха вызов ioctl() возвращает нулевое значение.

Предупреждение

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

ELCORE50_IOC_DBG_JOB_ATTACH

Подключение к экземпляру задания для отладки.

struct elcore50_job_instance_dbg {
  /* in */
  long job_instance_id;

  /* out */
  int job_instance_dbg_fd;
};

int ioctl(int fd, ELCORE50_IOC_DBG_JOB_ATTACH, struct elcore50_job_instance_dbg *inst_dbg);

Описание полей структуры elcore50_job_instance_dbg:

job_instance_id

Уникальный идентификатор экземпляра задания, полученный с помощью ioctl() ELCORE50_IOC_GET_JOB_LIST.

job_instance_dbg_fd

Файловый дескриптор отладки.

При вызове ioctl() выполнение задания прерывается и переводится в состояние отладки ELCORE50_JOB_STATUS_INTERRUPTED. Выполнение задания может быть продолжено после выполнения ioctl() ELCORE50_IOC_DBG_JOB_INSTANCE_CONTINUE, ELCORE50_IOC_DBG_STEP или завершения отладки путем закрытия файлового дескриптора :c:member:job_instance_dbg_fd.

Если экземпляр задания был поставлен в очередь с флагом :c:member:debug_enable, вызов ioctl() будет завершен с ошибкой.

Перед вызовом ioctl() необходимо заполнить поля структуры elcore50_job_instance_info из раздела «in». В случае успеха вызов ioctl() возвращает нулевое значение.

ELCORE50_IOC_GET_CORE_IDX

Получение информации о ядре.

struct elcore50_device_info {
  /* out */
  int nclusters;
  int cluster_id;
  int cluster_cap;
  int core_in_cluster_id;
};

int ioctl(int fd, ELCORE50_IOC_GET_CORE_IDX, struct elcore50_device_info
          *dev_info);

Описание полей структуры elcore50_device_info:

nclusters

Общее количество кластеров.

cluster_id

Порядковый номер кластера.

cluster_cap

Количество ядер в одном кластере.

core_in_cluster_id

Порядковый номер ядра в кластере.

В случае успеха вызов ioctl() возвращает нулевое значение.

ELCORE50_IOC_CREATE_BUFFER

Аналогичен IOC_CREATE_BUFFER для файла устройства /dev/elcore.

Данная реализация оставлена для обратной совместимости со старым ПО.

ELCORE50_IOC_CREATE_MAPPER

Аналогичен IOC_CREATE_MAPPER для файла устройства /dev/elcore.

Данная реализация оставлена для обратной совместимости со старым ПО.

ELCORE50_IOC_SYNC_BUFFER

Аналогичен IOC_SYNC_BUFFER для файла устройства /dev/elcore.

Данная реализация оставлена для обратной совместимости со старым ПО.

Описание отладочных IOCTL

Нижеописанные ioctl применяются к отладочным файловым дескрипторам заданий, полученных с помощью ELCORE50_IOC_DBG_JOB_ATTACH или ELCORE50_IOC_ENQUEUE_JOB с установленным флагом debug_enable структуры elcore50_job_instance.

ELCORE50_IOC_DBG_MEMORY_[READ|WRITE]

Чтение/запись памяти.

struct elcore50_dbg_mem {
 /* in */
  __u64 vaddr;
  size_t size;

 /* in-out */
  void *data;
};

int ioctl(int dbg_fd, ELCORE50_IOC_DBG_MEMORY_READ, struct elcore50_dbg_mem *mem);
int ioctl(int dbg_fd, ELCORE50_IOC_DBG_MEMORY_WRITE, struct elcore50_dbg_mem *mem);

Описание полей структуры elcore50_dbg_mem:

vaddr

Виртуальной адрес читаемого/записываемого блока памяти (или смещение регистра).

size

Размер блока памяти.

data

Указатель на блок памяти в пользовательском пространстве, содержащего данные DSP.

Предупреждение

Для записи/чтения регистров DSP необходимо использовать ioctl() ELCORE50_IOC_DBG_REGISTER_[READ|WRITE].

Чтение и запись памяти возможны только для PRAM, XYRAM и отмапированных через VMMU областей.

Перед вызовом ioctl() необходимо заполнить поля структуры elcore50_dbg_mem из раздела «in», а также выделить память под поле data. При вызове ioctl() драйвер:

  1. отключает все префетчи;

  2. сбрасывает кэши DSP;

  3. выталкивает из конвейера DSP все инструкции;

  4. считывает/записывает данные;

  5. сбрасывает кэши CPU;

  6. восстанавливает префетчи.

В случае успеха вызов ioctl() возвращает нулевое значение.

ELCORE50_IOC_DBG_REGISTER_[READ|WRITE]

Чтение/запись регистров DSP.

int ioctl(int dbg_fd, ELCORE50_IOC_DBG_REGISTER_READ, struct elcore50_dbg_mem *mem);
int ioctl(int dbg_fd, ELCORE50_IOC_DBG_REGISTER_WRITE, struct elcore50_dbg_mem *mem);

Перед вызовом ioctl() необходимо заполнить поля структуры elcore50_dbg_mem из раздела «in», а также выделить память под поле data.

Если драйвер загружен с параметром ядра dbg_registers_simple (см. Параметры модуля ядра), равным единице, доступ возможен только к регистровому и векторному файлам DSP-ядра, а также регистру PC. В противном случае, доступ возможен к любым программно-доступным регистрам DSP-ядра.

В случае успеха вызов ioctl() возвращает нулевое значение.

ELCORE50_IOC_DBG_HW_BREAKPOINT_SET

Установка аппаратной точки останова.

int ioctl(int dbg_fd, ELCORE50_IOC_DBG_HW_BREAKPOINT_SET, __u32 *vaddr);

Для установки аппаратной точки останова передать виртуальный адрес точки останова в качестве параметра vaddr. Драйвер записывает адрес точки останова в один из свободных регистров dbSAR. В случае, если все регистры заняты, возвращается ошибка -EBUSY. В случае успеха вызов ioctl() возвращает нулевое значение.

ELCORE50_IOC_DBG_HW_BREAKPOINT_CLEAR

Сброс аппаратной точки останова.

int ioctl(int dbg_fd, ELCORE50_IOC_DBG_HW_BREAKPOINT_CLEAR, __u32 *vaddr);

Для сброса аппаратной точки останова, ранее установленной с помощью ioctl() ELCORE50_IOC_DBG_HW_BREAKPOINT_SET необходимо передать ее виртуальный адрес в качестве параметра vaddr. В случае успеха вызов ioctl() возвращает нулевое значение.

ELCORE50_IOC_DBG_JOB_INSTANCE_INTERRUPT

Принудительный останов исполнения.

int ioctl(int dbg_fd, ELCORE50_IOC_DBG_JOB_INSTANCE_INTERRUPT, NULL);

В случае успеха вызов ioctl() возвращает нулевое значение.

ELCORE50_IOC_DBG_GET_STOP_REASON

Запрос причины останова.

enum elcore50_stop_reason {
  ELCORE50_STOP_REASON_HW_BREAKPOINT,
  ELCORE50_STOP_REASON_SW_BREAKPOINT,
  ELCORE50_STOP_REASON_EXTERNAL_REQUEST,
  ELCORE50_STOP_REASON_STEP,
  ELCORE50_STOP_REASON_DBG_INTERRUPT,
  ELCORE50_STOP_REASON_APP_EXCEPTION
};

struct elcore50_dbg_stop_reason {
  enum elcore50_stop_reason reason;
};

int ioctl(int dbg_fd, ELCORE50_IOC_DBG_GET_STOP_REASON, elcore50_dbg_stop_reason *state);

Описание полей структуры elcore50_dbg_stop_reason:

reason

Причина остановка задания. Допустимые значения:

  • ELCORE50_STOP_REASON_HW_BREAKPOINT — задание остановлено по причине срабатывания аппаратной точки останова.

  • ELCORE50_STOP_REASON_SW_BREAKPOINT — задание остановлено по причине срабатывания программной точки останова.

  • ELCORE50_STOP_REASON_EXTERNAL_REQUEST — задание остановлено по внешнему запросу.

  • ELCORE50_STOP_REASON_STEP — задание остановлено по причине завершения пошагового выполнения после ioctl() ELCORE50_IOC_DBG_STEP.

  • ELCORE50_STOP_REASON_DBG_INTERRUPT — задание остановлено по причине вызова ioctl() ELCORE50_IOC_DBG_JOB_INSTANCE_INTERRUPT.

  • ELCORE50_STOP_REASON_APP_EXCEPTION — задание остановлено по причине завершения работы.

В случае успеха вызов ioctl() возвращает нулевое значение.

ELCORE50_IOC_DBG_STEP

Выполнение по шагам.

int ioctl(int dbg_fd, ELCORE50_IOC_DBG_STEP, __u32 *steps);

Пошаговое выполнение задания возможно для ранее остановленного задания. Для выполнения N инструкций задания необходимо передать N в качестве параметра steps. В случае успеха вызов ioctl() возвращает нулевое значение.

ELCORE50_IOC_DBG_JOB_INSTANCE_CONTINUE

Продолжить выполнение задания.

int ioctl(int dbg_fd, ELCORE50_IOC_DBG_JOB_INSTANCE_CONTINUE, NULL);

В случае успеха вызов ioctl() возвращает нулевое значение.

Локальная и глобальная очереди задач

В драйвере elcore50 реализовано два типа очередей: локальная и глобальная.

Локальная (или частная) очередь существует отдельно для каждого из DSP-ядер и содержит задания, которые будут выполняться на соответствующем DSP-ядре, когда до них дойдет очередь.

Постановка задания в локальную очередь осуществляется путем выполнения ioctl() ELCORE50_IOC_ENQUEUE_JOB применительно к устройству /dev/elcoreX. Подробнее см. описание IOCTL.

Глобальная (или общая) очередь содержит задания, для которых в момент постановки в очередь неизвестно DSP-ядро, где это задание будет выполняться. Назначение DSP-ядра на задание осуществляется планировщиком.

Постановка задания в глобальную очередь осуществляется путем выполнения ioctl() ELCORE50_IOC_ENQUEUE_JOB применительно к устройству /dev/elcore. Подробнее см. описание IOCTL.

Планировщик — поток ядра Linux, который создается отдельно для каждого из существующих DSP-ядер при загрузке драйвера elcore50. При появлении в очередях новых заданий, планировщик:

  1. Просматривает глобальную очередь заданий. При наличии задания, извлекает из очереди, назначает на выполнение соответствующее DSP-ядро и переходит к п. 4.

  2. Просматривает локальную очередь заданий. При наличии задания, извлекает из очереди и переходит к п. 4.

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

  4. Помещает задание в список запущенных и запускает на соответствующем DSP-ядре.

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

Управление таймаутом заданий

В драйвере elcore50 реализовано отображение значения таймаута заданий в файл /sys/kernel/debug/elcore50/irq-timeout-msec через debugfs. Установка выполняется записью значения таймаута (в мс) в файл. По умолчанию таймаут настроен на значение 0 мс. Это означает, что все последующие задания будут запущены без таймаута.

Мониторинг производительности

Драйвер elcore50 публикует метрики производительности в директорию /proc/elcore50 виртуальной файловой системы procfs:

/proc/elcore50/info

Файл содержит информацию о ревизии, подревизии, позиционировании и порядковым номером DSP. Файл доступен только на чтение.

/proc/elcore50/usage

Файл содержит информацию о загрузке (в процентах) для каждого DSP. Файл доступен только на чтение.

Значение загрузки DSP рассчитывается как отношение общего времени работы DSP за период таймера к величине периода. Значение периода таймера установлено равным 500 мс.

Управление печатью состояния заданий

В драйвере elcore50 реализована возможность печати дампа состояния заданий в кольцевой буфер ядра Linux. Управление печатью осуществляется через запись в файл /sys/kernel/debug/elcore50/job-status-dump-enable, расположенного в debugfs, единицы для включения печати дампа и нуля для его отключения. По умолчанию печать дампов отключена.

Управление сбросами DSP

В драйвере elcore50 реализовано 2 вида сброса:

  1. Сброс Quelcore: снимается при загрузке модуля ядра, устанавливается при выгрузке модуля ядра.

  2. Сброс DSP: после завершения задания.

Обработка системных вызовов DSP

Поддержка системных вызовов DSP осуществляется путем реализации их в пользовательском процессе на CPU, в качестве которого выступает библиотека libelcorecl.

#define SC_GETTIMEOFDAY       1
#define SC_WRITE              2
#define SC_READ               3
#define SC_OPEN               4
#define SC_CLOSE              5
#define SC_FSTAT              6
#define SC_LSEEK              7
#define SC_ISATTY             8
#define SC_CHDIR              9
#define SC_STAT               10
#define SC_TIMES              11
#define SC_LINK               12
#define SC_UNLINK             13

enum elcore50_message_type {
  ELCORE50_MESSAGE_EMPTY = 0,
  ELCORE50_MESSAGE_SYSCALL_REPLY = 1,
  ELCORE50_MESSAGE_SYSCALL = 2,
};

struct elcore50_message {
  enum elcore50_message_type type;
  int num;
  __u64 arg0;
  __u64 arg1;
  __u64 arg2;
  __s64 retval;
};

Описание полей структуры elcore50_message:

type

Тип сообщения. Возможные значения:

  • ELCORE50_MESSAGE_EMPTY — пустое сообщение. Выставляется драйвером elcore50.

  • ELCORE50_MESSAGE_SYSCALL_REPLY — подтверждение. Выставляется пользовательским процессом после окончания обработки системного вызова.

  • ELCORE50_MESSAGE_SYSCALL — системный вызов. Используется драйвером elcore50.

num

Номер системного вызова. Возможные значения:

  • SC_GETTIMEOFDAYgettimeofday(2).

  • SC_WRITEwrite(2).

  • SC_READread(2).

  • SC_OPENopen(2).

  • SC_CLOSEclose(2).

  • SC_FSTATfstat(2).

  • SC_LSEEKlseek(2).

  • SC_ISATTYisatty(2).

  • SC_CHDIRchdir(2).

  • SC_STATstat(2).

  • SC_TIMEStimes(2).

  • SC_LINKlink(2).

  • SC_UNLINKunlink(2).

  • SC_PROFILprofil(2).

  • SC_GET_ENV — вспомогательный системный вызов для получения списка переменных окружений CPU-процесса, разделенных символами \0. Системный вызов имеет сигнатуру int _syscall_get_env_array(char *, uint32_t *).

  • SC_GET_KERNELNAME — вспомогательный системный вызов для получения имени вызываемой функции для данного экземпляра задания. Системный вызов имеет сигнатуру int _syscall_get_kernel_name(char *).

arg0

Первый аргумент системного вызова.

arg1

Второй аргумент системного вызова.

arg2

Третий аргумент системного вызова.

retval

Возвращаемое значение системного вызова.

При вызове на DSP системного вызова драйвер посылает событие в пользовательское приложение на CPU через poll() и выставлением статуса задания в ELCORE50_JOB_STATUS_SYSCALL. Пользовательское приложение читает сообщение elcore50_message из файла задачи, используя системный вызов read() применительно к файловому дескриптору задачи, полученного после вызова ELCORE50_IOC_ENQUEUE_JOB. В зависимости от значения num, пользовательское приложение выполняет соответствующий системный вызов, изменяет поле retval, тип сообщения на ELCORE50_MESSAGE_SYSCALL_REPLY и записывает сообщение elcore50_message в файл задачи, используя системный вызов write(). Ниже приведена диаграмма последовательности, которая описывает механизм взаимодействия драйвера elcore50 и пользовательского приложения при возникновении прерывания от системного вызова DSP.

box
    participant "DSP" as DSP
end box

box "elcore50 driver"
    participant "elcore50_irq" as IrqHandler
    participant "elcore50_job_run" as JobRunner
    participant "syscall_handler" as SysHandler
    participant "elcore50_job_read" as job_read
    participant "elcore50_job_write" as job_write
end box

box
    participant "User application" as UserApp
end box

activate DSP
DSP -> IrqHandler: Исключение
deactivate DSP
activate IrqHandler
IrqHandler -> JobRunner: Отправка события
deactivate IrqHandler
activate JobRunner
JobRunner -> SysHandler: Отправка структуры задания
deactivate JobRunner
activate SysHandler
SysHandler -> SysHandler: Заполнение сообщения elcore50_message
SysHandler -> UserApp: Отправка события
deactivate SysHandler
activate UserApp
UserApp -> job_read: Чтение сообщения elcore50_message
activate job_read
UserApp <-- job_read: Отправка сообщения
deactivate job_read
UserApp -> UserApp : Обработка системного вызова,\nЗапись кода возврата,\nУстановка типа сообщения ELCORE50_MESSAGE_SYSCALL_REPLY
UserApp -> job_write: Запись сообщения elcore50_message
activate job_write
job_write -> SysHandler: Отправка события
activate SysHandler
UserApp <-- job_write: Отправка подтвержения записи
deactivate job_write
UserApp -> UserApp: Ожидание следующего события
deactivate UserApp
SysHandler -> DSP: Запуск DSP
activate DSP
deactivate SysHandler

Обработка системных вызовов DSP в elcore50

Профилирование заданий DSP

Профилирование заданий DSP реализована на базе gprof (подробнее см. The GNU Profiler).

Драйвер elcore50 выполняет сэмплирование выполняемого задания DSP. Период сэмплирования определяется параметром timer_period, описанным в разделе Параметры модуля ядра.

Включение/завершение сэмплирования осуществляется в обработчике системного вызова SC_PROFIL, аргументами которого являются буфер сэмплов samples, его размер size, минимальный виртуальный адрес, по которому может находиться код offset и масштаб scale.

Во время сэмплирования драйвер elcore50 считывает текущее значение регистра DSP PC. Исходя из полученного значение рассчитывается индекс элемента массива samples, к которому затем прибавляется единица. Индекс рассчитывает по следующей формуле:

\[idx = (((PC - offset) / 2) * scale) / 65536\]

Профилирование драйвера

Профилирование драйвера elcore50 возможно с помощью механизмов ядра ftrace и kretprobes.

Профилирование на базе ftrace

Профилирование выполняется с использованием утилит elcore-profile.py и ftrace-parser. Описание утилит приведено в документе Профилирование.

Результатом профилирования драйвера являются:

  • Общее время выполнения приложения, время выполнения DSP;

  • Доля драйвера elcore50 в общем времени выполнения приложения;

  • Накладные расходы, связанные с обработкой прерываний;

  • Накладные расходы, связанные с обработкой системных вызовов DSP;

  • Накладные расходы на создание и освобождение буферов;

  • Накладные расходы на создание MMU-таблиц для буферов;

  • Граф вызовов функций ядра, отсортированный по убыванию времени выполнения, включающий события.

Драйвер реализует следующие события (tracepoints):

elcore50_buf_create и elcore50_buf_release

Данные события соответствуют функциям создания буфера с помощью ioctl() ELCORE50_IOC_CREATE_BUFFER, а также освобождения через close(fd), где fd соответствует файловому дескриптору, возвращаемого ioctl() ELCORE50_IOC_CREATE_BUFFER. При срабатывании выводится type и size структуры elcore50_buf.

elcore50_syscall

Данное событие срабатывает при обработке драйвером системных вызовов DSP. При срабатывании выводится имя системного вызова, а также размеры аргументов.

elcore50_mmu_map

Данное событие срабатывает при настройке MMU-таблиц для буферов и ELF-секций. При срабатывании выводится назначенный виртуальный адрес DSP, а также размер буфера.

elcore50_uptime

Данное событие срабатывает при завершении задания.

При срабатывании выводится время выполнения задания в тактах DSP.

Профилирование на базе Kretprobes

Kretprobes - это механизм ядра Linux, который позволяет отслеживать возвраты из функций. Каждый раз, когда функция трассируется с помощью kretprobe, ядро выделяет ресурсы для отслеживания её возврата. Количество одновременно трассируемых функции определяется параметром kretprobes_max_active, который по умолчанию равен 16. Если количество активных kretprobes достигает этого предела, новые трассировки не будут активированы, пока не освободятся ресурсы (например, после возврата из ранее отслеживаемых функций).

Список активных трассировок функции доступен в debugfs по следующему пути: /sys/kernel/debug/kprobes/list.

Метрики функции экспортируются в debugfs по следующему пути: /sys/kernel/debug/elcore50/kretprobes.

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

kretprobes
|-- <job1>          --  Наименование DSP задачи
|   |-- avg_time    --  Среднее время выполнения
|   |-- call_times  --  Количество вызовов
|   |-- exec_time   --  Время работы DSP когда эта задача выполнялась последний раз
|   |-- max_time    --  Максимальное время выполнения
|   |-- min_time    --  Минимальное время выполнения
|   `-- total_time  --  Суммарное время выполнения
|-- ...
|-- <jobN>
|-- <function1>     --  Наименование функции, вызванной внутри драйвера
|   |-- avg_time
|   |-- call_times
|   |-- max_time
|   |-- min_time
|   `-- total_time
|-- ...
|-- <functionM>
`-- reset           --  Файл для сброса метрик. Сброс осуществляется записью '1'

Атрибуты времени представленны в наносекундах. Атрибут call_times – разы.

Включение Kretprobes для драйвера elcore50 осуществляется через параметр модуля ядра use_kretprobes.

Просмотр метрик доступен с помощью утилиты kretprobes-parser.

Управление энергопотреблением DSP

С целью снижения энергопотребления включение частоты DSP происходит после открытия файла устройства /dev/elcoreX, а выключение — после закрытия устройства. Включение/выключение частоты DSP осуществляется с использованием clock framework.

Драйвер elcore50 реализует возможность динамического изменения частоты DSP через sysfs с помощью devfreq.