Компиляция ядра и системное программирование драйвера под Ubuntu Linux

Компиляция ядра и системное программирование драйвера под Ubuntu Linux

17 января 2023

В рамках исследования длительного направленного воздействия электромагнитного поля на организм человека был детально изучен интерфейс передачи данных DisplayPort. Для реализации экспериментальной части требовалось отсутствие сторонних электромагнитных помех, чтобы увеличить точность обнаружения эффектов влияния. Использовалось экранированное помещение, изнутри полностью обустроенное радиопоглощающим материалом и не пропускающим электромагнитные волны — безэховая экранированная камера 1-го класса экранирования. Внутренняя электромагнитная обстановка надежно отделена от внешних воздействий.

В камере располагался обычный персональный компьютер с интерфейсом DisplayPort, а рядом с монитором установлена приемная антенна, на которой размещен помехоустойчивый кабель DisplayPort. Вне камеры установлен измерительный приемник, к которому подключена приемная антенна. На ПК запущена программа управления измерительным приемником, разработанная на графическом языке G в среде LabVIEW. В целях снижения внешних помех при проведении эксперимента в камере отсутствуют люди.

Для реализации проекта потребовалось системное программирование Linux. В ходе исследования определялись следующие параметры.

01 Частота сигнала Частота сигнала.
02 Спидометр Мощность электромагнитного поля сигнала.

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

Целью проекта было тестирование помехоустойчивости канала передачи в зависимости от его внутренних настроек. В ходе предварительного исследования было показано, что, изменяя параметры передачи интерфейса DisplayPort, возможно существенно влиять на помехоустойчивость канала передачи, а также на мощность сигнала. Канал передачи интерфейса DisplayPort в упрощенном виде состоит из цепочки: графический процессор–кабель–монитор.

Экранированная камера
Безэховая экранированная камера 1-го класса экранирования

DisplayPort спроектирован для подключения видео и аудиотехники. Поддерживается передача цифрового контента с разрешением до 8К. В подверсии 2.1 максимальная пропускная способность — 77,37 Гбит/с. Базовая спецификация DisplayPort разработана ассоциацией VESA (Video Electronics Standards Association) в 2006 году. Интерфейс имеет три канала передачи: основной отвечает за передачу графики; дополнительный двунаправленный канал обеспечивает связь передающего ПК и приемного монитора; третий канал — линия горячего подключения (Hot Plug Detect) для фиксации включения или выключения дисплея.

Особенности интерфейса

01 Разрешение 8К Поддержка разрешения 8K.
02 Шифрование данных Шифрование данных.
03 Электромагнитные волны Низкий уровень электромагнитных помех.
04 Полосы воспроизведения Распределение полосы пропускания между аудио и видео.
05 Высокоскоростной канал Высокоскоростной вспомогательный канал.

Поступила задача на программирование низкоуровневого драйвера-фильтра для интерфейса DisplayPort для решения задачи изменения DisplayPort Configuration Data (DPCD) под Windows 10, версию DisplayPort 1.2 и программы для взаимодействия с драйвером. Для тестирования был выделен монитор Philips 242E1GAJ и ПК MSI PRO DP21 11MA-210RU. В начале разработка ядра шла хорошо, решались проблемы с регистрацией драйвера в системе, включением его в стек драйверов монитора, передачей данных вовне и из внешней программы. Но потом началось собственно взаимодействие с DPCD. По официальной документации для этого существует функция DXGKDDI_DPAUXIOTRANSMISSION. Но как ее ни «крутили», какие параметры в нее ни передавали, «вытащить» данные из DPCD не удавалось. В результате исследования файлов WDDM удалось узнать, что у этой функции (и множества других функций DisplayPort) нет реализации, и они представляют из себя лишь удобный шаблон для создания своего драйвера. Времени для решения такой задачи не было выделено, и было решено сменить используемую ОС на любую другую, основанную на Linux. Были испробованы Debian 9, Debian 10, Debian 11, но ни одна из версий не могла «из коробки» работать с DisplayPort и выводить изображение на монитор. В конце-концов, перешли на Ubuntu 22.04.1, который сразу мог работать с монитором через DisplayPort. На этом и остановимся поподробнее.

В Linux нет такого понятия как «драйвер-фильтр», так же, как и уровней драйверов. Стоит уточнить, что в Linux возможно перехватывать вызов определенной функции другого драйвера и выполнять манипуляции с входными данными с последующим вызовом исходной функции. По сути, мы перенаправляем сигнал на вызов исходной функции в нашу. Но в рамках поставленной задачи это и не нужно. Для разработки драйвера для Linux необходимо установить заголовочные файлы, подходящие для вашей версии ядра.

sudo apt-get install linux-headers-$(uname -r)

Если же компиляция ядра Linux произведена из исходников, то linux-headers для вашей версии ядра могут отсутствовать в репозитории. Но это не проблема, так как они создаются при сборке ядра. Начнем с создания драйвера. Прямое взаимодействие внешней программы и драйвера невозможно. Создается символьное устройство, к которому программа может подключиться и выполнять запросы к драйверу на передачу параметров. Доступно 4 режима: чтение, запись, чтение и запись одновременно, без параметров. Устройство должно создаваться в момент регистрации драйвера, и удаляться при его удалении — не забудьте удалить не только устройство, но и класс устройства, вместе с отменой регистрации драйвера.

int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);

Возникали проблемы с созданием структуры file_operations. Дело в том, что нам необходимо определить функцию, в которую будут отправляться запросы при обращении к устройству ioctl (Input/Output control). В версиях ядра Linux до 3.х указание функции-обработчика выполнялось установкой параметра .ioctl.

В последующих версиях ядра параметр недоступен и заменен на unlocked_ioctl по ряду причин, связанных с новой архитектурой. Подробнее можно почитать о Big Kernel Lock.

static struct file_operations fops = 
{
    .unlocked_ioctl = etx_ioctl
};

Большинство документации по разработке драйверов для Linux содержат описания для старых версий ядра и найти информацию сложно. Документацию для текущей версии ядра можно найти здесь. Теперь все ioctl-запросы направляются в функцию, присвоенную unlocked_ioctl. С параметрами функция будет иметь следующий вид.

long etx_ioctl(struct file *file, unsigned int ioctl_num, unsigned long ioctl_param);

Больше всего нас интересуют два последних параметра: ioctl_num и ioctl_param. ioctl_num — как понятно из названия, номер команды. Его необходимо заранее определять.

#define "ioctl name" __IOX("magic number", "command number", "argument type")

Где IOX должен быть:

  • IO — без параметров;
  • IOW — с записью параметров (copy_from_user);
  • IOR — с чтением параметров (copy_to_user);
  • IOWR — с чтением и записью параметров.

В случае c DPCD хотелось предположить, что достаточно команд чтения и записи. Подробнее об этом будет дальше. По сути, необходимо знать адрес и данные, которые можно записать. Но некоторые поля DPCD могут занимать более одного адреса — придется логически разделять цельные данные по адресам. Если хотите из приложения изменять любое доступное поле, предпочтительнее использовать базовый вариант с указанием адреса и данных в паре отдельных команд. Поэтому для каждого поля была определена своя команда.

Ну вот, мы вплотную подобрались непосредственно к DPCD. Что же это такое? Согласно документации — это адресное пространство устройства DisplayPort, данные из которого используются для настройки и инициализации каналов. Какой адрес за какой параметр отвечает ищем в официальной документации, поэтому останавливаться в статье на том не будем. Главное, что следует отметить — значение по каждому адресу состоит из 8 бит. И не каждое поле состоит лишь из одного адреса. Для работы с полями DPCD существует функция drm_dp_dpcd_readb, которая загружается вместе с linux-headers. Ее прототип расположен в drm_dp_helper.h.

ssize_t drm_dp_dpcd_readb(struct drm_dp_aux *aux, unsigned int offset, u8 *valuep)

Сразу возникает вопрос: где взять drm_dp_aux? Средств для этого нет. Но монитор же функционирует, и как-то видеодрайвер ее создает и использует? Стоит уточнить, что в нашем ПК используется интегрированная видеокарта, и, соответственно, используется драйвер Intel. Если же у вас видеокарта от AMD или Nvidia, то действия могут отличаться.

Драйвер для Intel i915 поставляется с ядром Linux. Раз исходники открыты, почему бы нам не вытащить необходимый параметр? Скачиваем исходный код ядра и открываем drivers\gpu\drm\i915\display\intel_dp_aux.c. Добавляем в него функции.

static struct drm_dp_aux *static_dp_aux_ptr;

struct drm_dp_aux *intel_dp_aux_get_struct(void) {
    if (static_dp_aux_ptr == NULL)
        printk(KERN_INFO "Backlight: Could not init the aux_ptr!\n");
    return static_dp_aux_ptr;
}

EXPORT_SYMBOL(intel_dp_aux_get_struct);

static void intel_dp_aux_set_struct(struct drm_dp_aux *dp_aux) {
    static_dp_aux_ptr = dp_aux;
}

В конце функции void intel_dp_aux_init(struct intel_dp *intel_dp) добавляем вызов нашей новой функции.

intel_dp_aux_set_struct(&intel_dp->aux);

Что же мы сделали? Создали указатель на необходимую для чтения DPCD структуру, создали функции для записи и чтения этой структуры, функцию чтения экспортировали для использования в других драйверах (EXPORT_SYMBOL) и добавили вызов функции записи структуры в функцию инициализации. Собираем и устанавливаем ядро и все — через вызов функции intel_dp_aux_get_struct получаем структуру, не забыв прописать в прототипе функции external.

Дальнейшая задача тривиальна и останавливаться подробно на ней не будем — там, где надо, читаем получаемые данные в функции ioctl через copy_from_user и возвращаем данные по запросу через copy_to_user.

Меню DPCD

Следующим было удержание работы монитора в тестовом режиме. Согласно спецификации, режим включается при установке последних двух битов поля DPCD TRAINING_PATTERN_SET в любое значение, отличное от 0. Всего существует 3 варианта тестирования и нулевое значение, обозначающее что тестирование не проводится. Нас интересует только вариант, который устанавливается значением 01 в [1:0] биты поля training pattern 1. Но эффективна настройка максимум 10 мс и затем не остается активной. Поэтому в управляющей программе вынесли в отдельный поток цикл, устанавливающий значение 01 в TRAINING_PATTERN_SET каждую миллисекунду.

Способ сработал: когда текущее значение было единицей, то перезапись поля ни к чему не приводила. Но замена нуля («тестирование завершено») заново приводит к тестированию. В качестве альтернативного варианта рассматривалась модификация драйвера и внедрение в него автоматического зацикливания. Что могло создать проблему — тестовый режим используется самим устройством при включении для настройки параметров. Неизвестно, как поведет себя, если тестирование не сможет завершиться за указанные в спецификации 10 мс. Также в драйвере раньше была возможность зацикливания, которая была исправлена. Согласно комментариям в коде такие случаи считались багом.

Но запись данных в DPCD не означает, что это применение настройки. Например, изменение в поле LANE_COUNT_SET количества используемых каналов фактически ничего не меняет в работе монитора. При запуске тестового режима LANE_COUNT_SET заново вычисляется и перезаписывается. Мы же хотим, чтобы значения записанные в DPCD оставались. Вернемся к драйверу i915. В файле intel_dp.c есть функция intel_dp_retrain_link, которая вызывается при включении тестового режима. В одном из ее циклов вызывается функция intel_dp_start_link_train, которая начинает тест. В этом же цикле создается структура, отображающая реальное состояние всей системы.

const struct intel_crtc_state *crtc_state = to_intel_crtc_state(crtc->base.state)

Значения структуры используются для тестового режима. Значит будем ее менять — сразу уберем const из определения. Далее устанавливаем crtc_state->lane_count на необходимое значение (01, 10 или 11). Но брать значение из DPCD мы не можем — в результате тестирования значение установится согласно результатам теста. При зацикливании режима тестирования установится новое значение. Выходит, значение надо доставить другим способом. И тут опять пригодится EXPORT_SYMBOL. Создаем в intel_dp.c функцию, передающую число каналов. И создаем переменную, в которой значение будем хранить. При записи значения в LANE_COUNT_SET в одной из функций ioctl заодно вызываем и новую функцию.

Чтобы принудительная запись работала только в тестовом режиме, при его отключении внешней программой записываем в переменную нулевой значение, которое не может возникнуть в рамках нормальной логики. Добавляем проверку на значение — если в переменной число каналов больше 0, то выполняем присваивание crtc_state->lane_count. Иначе продолжаем выполнение функции. Для других полей (скремблирование, размытие амплитуды и т.п.) также возможен подобный трюк.

На этом по драйверу больше писать нечего. Кратко опишем функции пользовательского приложения. К устройству можно подключиться с помощью команды: open(DEVICE_FILE_NAME, O_WRONLY), где DEVICE_FILE_NAME — имя символьного устройства, созданного в драйвере.

Функция вернет номер устройства, на которое будем отправлять настройки при помощи ioctl. Стоить уделить внимание последнему параметру — указателю на структуру, которая будет содержать передаваемые и получаемые данные. Не выделяйте память под данные динамически, ведь в драйвере не будет доступа, или передастся мусор.

После включения тестового режима мы принудительно задали crtc_state->lane_count. Тогда перед завершением тестирования необходимо обнулить значение, дабы работа продолжилась штатно. Обязательно добавьте обнуление в событие закрытия приложения.

i в круге

В итоге разработки под Linux были созданы драйвер и приложение, управляющие настройками DPCD, а также изменен видеодрайвер i915 для управления фактическим состоянием, а не только отображением состояния.