Поступила задача на программирование низкоуровневого драйвера-фильтра для интерфейса 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
.