Создание собственных драйверов под Linux

в 21:21, , рубрики: Без рубрики

Создание собственных драйверов под Linux

Многие мои друзья и знакомые крутят пальцем у виска или задаются вопросом: не жмёт ли мне череп, когда узнают, что я пишу драйвера под Linux. Слово “драйвер” окутано каким-то почти мистическим смыслом, и постичь Дао его написания способны лишь избранные гуру.
К счастью это не так. Не знаю, как обстоят дела с написанием драйверов под другие операционные системы, в т.ч. и наиболее популярные, но под linux, вне зависимости от аппаратной архитектуры драйвера пишутся очень просто. Для написания драйвера необходимы базовые знания языка си, представление о работе ОС линукс (базовые), понимание того, что мы хотим получить, желание чтения документации и исходных кодов, ну и усидчивость. Всё.
Вы хотите посмотреть как написать драйвер для своего устройства? Тогда ныряйте под кат!

Лёгкость написания драйверов объясняется тем, что исходный код linux великолепно задокументирован и доступен в исходниках, в сети множество примеров и куча литературы. А для создания драйвера вам нужен только любимый дистрибутив и исходники ядра, ну и компилятор под нужную вам архитектуру. Свой первый, не учебный, драйвер я писал для процессора PowerPC 8360, он взаимодействовал с микросхемой ПЛИС и осуществлял сброс настроек по умолчанию. Как это не странно звучит, я его сделал, немного переписав драйвер SPI для этой архитектуры. Я это говорю к тому, что единая стандартизация написания программ в ядре Linux позволяет проводить такие штуки.
Настольная книга разработчика драйверов под Linux — это “Linux Device Drivers”. Книжка является исчерпывающим руководством по разработке и если что-то не хватает в книге, то это точно можно найти самостоятельно в исходниках ядра. Разумеется данный пост не претендует на то, чтобы заменить эту замечательную книгу. Более того, написан он по мотивам прочтения онных трудов, но всё же не является кратким пересказом данной книги. Автор этих строк ставит собой цель заинтересовать читателя простотой и изяществом ОС Linux, и понизить порог вхождения людей в разработку драйверов. Поверьте — это очень просто!

То, для чего будем писать драйвер

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

Мы будем писать драйвер для дисплея семейства HD44780 подключённого через LPT-порт к компьютеру. Знаю, что порт древний, а типу дисплеев лет не меньше, чем мне, но всё же — это красивый и простой пример написания драйверов под linux, не углубляясь в аппаратные подробности. И тем более, что переделать этот драйвер для этого экрана для других архитектур будет делом замены одной строчки!

Итак, нам понадобится LPT-порт; шнурок для старого принтера и опционально разъём CENTRONICS на 36 контактов, хотя можно просто распатронить кабель; разумеется экран семейства HD44780, самое лучшее — это минимум 4х40 символов, хотя подойдут даже самые маленькие; ну и переменный резистор на 10 кОм, для регулировки контраста. Поскольку LPT-порта у меня нет уже давным-давно, я прикупил первую попавшуюся плату на PCI (можно и PCI-E). При чём плата попалась на столько китайская, что я даже не смог найти никакой документации по напаянному чипу на ней. Ничего, будем ещё и реверсить её :)))).

Создание собственных драйверов под Linux
Аппаратные исходники

Будем считать, что достать эти компоненты не составило труда, и ваши руки горят от нетерпения всё сделать.

Предупреждение!!!
Внимательно прочитать!

Создание собственных драйверов под Linux

Хочу обозначить, что мы работаем в самых тяжёлых условиях, с железом, да ещё и будем работать на уровне ядра. Живём, так сказать, внутри пациента. Любая ошибка в программе и ядро падает. При чём оно может даже не успеть помахать вам kernel_panik-ом, просто внезапно всё перестаёт работать. Обратите внимание, что вы запустили драйвер, выгрузили его, и вдруг начались неявные глюки — всё, ваш путь — перезагрузка. Если система жива, то командой reboot. Если уже нет, то аппаратный reset. Хотя при тяжёлых случаях иногда система может сама себя перезагрузить.

Плюс, если вы не знаете что делаете (пишете в порт, обращаетесь к областям памяти и т.п.), то лучше этого не делать. Ибо всё это в лучшем случае может сбить настройки (записанное неверное значение в порт 70h раньше сбрасывало BIOS в х86) или даже попортить оборудование (например посадив ножку GPIO на землю). По этому чётко понимайте, что вы делаете, как на аппаратном, так на программном уровне!

Приступаем

Для счастливых обладателей встроенного lpt-порта в своём компьютере скажу, что в вашем случае порт живёт по стандартному адресу 378h. Меня можно среди ночи разбудить и я скажу эту цифру, столько с ней связанно. Но ныне нам дали вражескую плату, и нам предстоит её победить!
Вставляем плату в слот PCI или PCI-E, загружаемся и смотрим что у нас происходит на шине PCI следующей коммандой:

lspci  -v
…
05:01.0 Serial controller: Device 4348:5053 (rev 10) (prog-if 02 [16550])
	Subsystem: Device 4348:5053
	Flags: medium devsel, IRQ 18
	I/O ports at c030 [size=8]
	I/O ports at c020 [size=8]
	Kernel driver in use: serial

Как видим линух нашёл что-то на шине PCI, и даже попытался дать ему какой-то левый, явно неправильный драйвер. Но нас более всего интересует адреса, на которых сидит данный порт. Это строки: I/O ports, а именно цифры c030h и c020h.
Нам теперь нужно найти какие же адреса за что отвечают. Для этого к порту вывода подключим светодиоды с резисторами примерно по такой схеме (резисторы примерно 310 Ом, или больше или меньше, не важно)

Создание собственных драйверов под Linux
Схема проверки

Поскольку у меня уже есть заготовка светодиодов для теста такого рода, то я вставлял провода прямо в разъем CENTRONICS и у меня не влезло все 8 штук проводов, поэтому вставил только 6 (первые 4 и последние два).

Создание собственных драйверов под Linux
Собранный тестер порта

И набросаем небольшую программку для проверки.

Для работы с портом я взял пример отсюда www.faqs.org/docs/Linux-mini/IO-Port-Programming.html#s9. Единственное, что надо исправить строку:

#include <asm/io.h>

на

#include <sys/io.h>

Делаем нормальные задержки в секунду (вместо usleep(100000);
, пишем sleep(1);) и делаем последовательно три вывода в порт:

outb(0, BASEPORT);
sleep(1);
outb(0xAA, BASEPORT);
sleep(1);
outb(0xFF, BASEPORT);
sleep(1);

Дефайн #define BASEPORT меняем на порт 0xc030. Компилируем, запускаем из под рута: светодиоды не горят. Меняем на другую цифру — 0xc020 — але оп:

Создание собственных драйверов под Linux
Нуль

Создание собственных драйверов под Linux
На выводах AAh

Создание собственных драйверов под Linux
И, разумеется через секунду FFh

Результат на лицо. Т.е. с нумерацией портов мы угадали, можно приступать к дровам. LPT-порт данной платы живёт по адресу. 0Xc020!

Хочу обратить внимание программистов жестянщиков на магическое число 0xAA — это последовательность единиц и нулей. Вот так: 10101010b. Это очень удобно для всяких отладок. А вообще всякий кто работает с железом должен легко в уме переводить двоичное, десятичное, шестнадцатеричное туда сюда обратно.

Таки дисплей

Барабанная дробь, настало время собрать железку воедино! Для этого нам осталось всё спаять вместе. Всё собираем по следующей схеме.

Создание собственных драйверов под Linux
Знаю, что схема не по ГОСТу, но это и не журнал «Радио»

Для питания экрана я распатронил провод USB, и сейчас у меня к экрану идут два провода. Есть хитрый вариант, просто до него никак не доходят руки — это пустить питание USB прямо в проводе LPT, отпаяв один из многочисленных земляных проводов. Как дойдут руки — обязательно сделаю. Допишу только эту статью :)))).
В результате, после подачи питания, мы должно получить что-то вроде этого. Подрегулируйте яркость резистором, чтобы появились две полоски. Они свидетельствуют нам о том, что дисплей не проинициализирован.

Создание собственных драйверов под Linux
Девайс в сборе

Фух, с аппаратной частью мы закончили, пришла пора переходить к программе.

Приступаем злобным опытам с экраном в программе

В качестве готовой либы для работы с дисплеем использовал код от Arduino взятый отсюда. Я тоже не люблю эту платформу, но код годный для дисплеев, подключаемых по I2C. Если последнее слово вам ни о чём не говорит, не пугайтесь. Суть такова, что тот же самый дисплей подключён по про двум проводам (как следует из названия шины), а далее там стоит регистр, который снова преобразует переданный байт в параллельную шину, прям как у нашего LPT-порта. В результате выкинув часть работы с I2C, получим годную либу.
Для чистоты переписал на сях, добавил парочку нужных функций, переименовал некоторые конфликтные сделал ещё какие-то изменения (не помню, много воды утекло) и получил конфетку.
К слову сказать код использовался на STM32, так же с дисплеями подключающимися по I2C. А теперь мы его портируем уже на х86 да под linux. Вот она великая сила переносимости кода! Потратив всего 10 минут, чтобы код адаптировать уже под новую платформу.
Работа с портом осуществляется в одной единственной функции — это непереносимый узел, и в зависимости от платформы способ вывода следует менять. Тут был вывод по I2C:

 void expanderWrite(unsigned char _data){                                        
	outb(((_data) | _backlightval), BASEPORT); //x86 instruction!!!!!
}

Обратите внимание на эту функцию. Заменив её, код можно запустить в AVR, STM32, MIPS и т. д.
Уже знакомый нам вывод в порт. Дефайн порта определяем в хедере lcd.h. Всё, теперь можно попробовать сделать вывод. Программа стала ещё проще и нагляднее.
Для вывода на экран используется функция-обёртка print_to_string

void print_to_string (unsigned char col, unsigned char row, unsigned char c[], unsigned char len);

Первый параметр номер столбца (от нуля до 19), второй номер строки (от нуля до трёх), третий — указатель на символьную строку и последний — длинна строки. Строка не проверяется на окончание “”, длинну надо контролировать самостоятельно! В результате попробуем дисплейчик:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/io.h>
#include "lcd.h"

#define LCD_STRINGS	4 //строк
#define LCD_COLUMNS 	20 //столбцов


int main()
{
	LCD_init(0, LCD_COLUMNS, LCD_STRINGS);//инициализация экрана
	print_to_string (0, 0, "XXXXXXXXXXXXXXXXXXXX"			, 20);
	print_to_string (0, 1, "YYYYYYYYYYYYYYYYYYYY"			, 20);
	print_to_string (0, 2, "ZZZZZZZZZZZZZZZZZZZZ"			, 20);
	print_to_string (0, 3, "MMMMMMMMMMMMMMMMMMMM"			, 20);
	exit(0);
}

В результате на экране должно появится что-то типа этого:

Создание собственных драйверов под Linux
Наш первый дебют!

Не пугайтесь этой порнографии. Этот экран был списан за битые пиксели, точнее там просто погиб контроллер от статики (их там несколько). Для работы он не годится, а для всяких подобных опытов — самое оно. Такие хреновые экраны делают китайцы!

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

Инструментарий для сборки драйвера

Чтобы приступить к написанию драйвера, надо поставить необходимый инструментарий. Как подготовить инструментарий для Ubuntu и Debian хорошо рассказывается вот в этой статье blog.markloiseau.com/2012/04/hello-world-loadable-kernel-module-tutorial/
Лишь кратко скажу, что если вам не нужно собирать debian-пакеты, а вы пишете модуль только для себя, то достаточно будет установить необходимые компоненты следующей командой:

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

Всячески рекомендую так же скачать исходные тексты своего ядра. Есть два варианта это сделать. Первый, и очевидный, но возможно слегка геморройный — это узнать версию своего ядра командой:

uname - a

и скачать его с www.kernel.org/. Я же лентяй лазать по ссылкам и искать версию ядра, доверю это дело автоматике.

#переходим в режим суперпользователя
sudo -s
cd /usr/src/
apt-get source linux-image-$(uname -r)
#после чего вы получите архив вашего ядра.
#в моём случае это был файл linux_3.8.0-9.18.tar.gz он же и распаковался в /usr/src/linux-3.8.0

Усё, запомните этот путь или добавьте ссылку на него, например /usr/src/linux, но не рекомендую это делать, т.к. ядра имеют тенденцию к обновлению. Тут вопрос спорный быть или не быть, решайте сами.
Теперь вы имеете у себя настоящее сокровище: кладезь актуальный документации и чудовищное количество примеров исходного кода и разумеется необходимый плацдарм для сборки модуля.
Настоятельнейше рекомендую лазать в папочку Documents, drivers и прочие, просматривать исходные тексты — это невероятно полезно!

Собстна драйвер

Если вы думаете, что мы сейчас просто поправим мейкфайл и получим готовый драйвер, то вы глубоко заблуждаетесь. Всё, теперь мы находимся в другом мире: не в пространстве пользователя, в пространстве ядра. И тут действуют совсем другие правила.
Чтобы осознать всю бедовость ситуации, то вы должны понимать, что ядро — это одна большая-пребольшая программа. А это означает, что глобальные переменные, функции и т.п. могут быть доступны в других местах ядра! По этому ВСЕ глобальные переменные (их следует избегать по максимуму) должны быть объявлены как static!
Мы не будем пересобирать ядро, встраивая в него драйвер, и каждый раз перезапускаться, пробуя его. Это конечно забавно, но очень долго. Вместо этого, мы сделаем модуль ядра, который мы будем загружать и выгружать прямо во время работы.
В нашем будущем модуле, по сравнении с программой для пространства пользователя изменения будут не очень значительные. Во первых, перенесём все сишные файлы в один, чтобы не иметь проблем с компиляцией (ну или инклудим сишники в друг друга). Хедер оставляем, но чётко определяем все параметры функций (если никаких параметров, то надо прописать void). Вспоминаем, что мы в пространстве ядра, и все библиотеки у нас другие. Меняем все хидеры на ядерные:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>  /* everything... */

#include <asm/io.h>
#include <linux/unistd.h>
#include <linux/delay.h>	/* udelay */
#include <asm/uaccess.h>
#include <linux/miscdevice.h>

Обратите внимание, что даже unistd.h и io.h стали ядерными.

В качестве первичной основы драйвера возьмём модуль hello world. Поправим функцию init:

static int __init hello_init(void)
{
    	LCD_init(0, LCD_COLUMNS, LCD_STRINGS);
	print_to_string (0, 1, "Hello Habrahabr     "			, 20);
	printk(KERN_INFO "Lpt module initn");
	return 0;    // Non-zero return means that the module couldn't be loaded.
}

Здесь всё практически без изменений. Меняется у нас функция работы с железом (самая платформозависимая), т.к. будет работать уже с ядерной функцией вывода в порт:

void expanderWrite(unsigned char _data){                                        
	//outb(((_data) | _backlightval), BASEPORT); //x86 instruction rootfs!!!!!
	outb_p(((_data) | _backlightval),BASEPORT); //x86 instruction kernel!!!!!
}

И, как это ни странно, функция задержек. Вообще задержки — отдельная песня, которой можно посвятить целую статью. У нас же должно быть всё быстро, и как можно меньше занимать ядро на всякую ерунду. Но всё же:

static inline int delayMicroseconds(int value)
{
	//usleep(value); //rootfs
	if (value > 1000)
		msleep(value/1000);
	udelay(value%1000);
	return 0; //kernel
}

Объяснение такой махинации простое. udelay внутри ядра не может принимать значения больше тысячи. Чтобы избежать казусов, необходима такая заглушка.
После этого собираем примерно таким мейкфайлом:

TARGET = lptlcd
obj-m	:= $(TARGET).o

KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD       := $(shell pwd)
CC = gcc
all:
	$(MAKE) -C $(KERNELDIR) M=$(PWD)

clean:
	rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions

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

sudo insmod lptlcd.ko

Если мы всё сделали правильно, то нам не будет выведено ошибок, а экран верно проинициализируется. dmesg покажет последней строкой сообщение вашего модуля:

[10036.950566] Lpt module init

Выгрузить модуль можно коммандой

sudo rmmod lptlcd

dmesg скажет:

[10077.176714] Cleaning up module

Вообще, чтобы не жать постоянно dmesg существует скрипт существует решение, которое мне подсказал камрад Jtu:

while true; do sudo dmesg -c; sleep 1; done

Или, если вы используете дистрибутив отличный от убутны, то следует исполнять это под root, и убрать sudo.

Результат на экране:

Создание собственных драйверов под Linux
Привет вам!

Если вы всё это увидели — поздравляю, это ваш первый рабочий бесполезный модуль ядра, который выводит надпись на экран! Это конечно занимательно, но надо таки превращать наши эксперименты в нормальный драйвер, с которым можно работать. А для этого всё же придётся рассказать о матчасти, хоть немного.

Пару слов о матчасти

Как ни крути, но без знания некой матчасти далеко не удешь.Есть символьные, блочные и сетевые устройства. Так же у каждого устройства есть старший и младший номер устройства. Лучше, чем в книге “The Linux Kernel Module Programming Guide” сказать нельзя, по этому приведу цитату из книги в спойлере:

Старший и младший номер устройства

Старший и младший номер устройства
Давайте взглянем на некоторые файлы устройств. Ниже перечислены те из них, которые представляют первые три раздела на первичном жестком диске:

# ls -l /dev/hda[1-3]
brw-rw----  1 root  disk  3, 1 Jul  5  2000 /dev/hda1
brw-rw----  1 root  disk  3, 2 Jul  5  2000 /dev/hda2
brw-rw----  1 root  disk  3, 3 Jul  5  2000 /dev/hda3

Обратили внимание на столбец с числами, разделенными запятой? Первое число называют «Старшим номером» устройства. Второе — «Младшим номером». Старший номер говорит о том, какой драйвер используется для обслуживания аппаратного обеспечения. Каждый драйвер имеет свой уникальный старший номер. Все файлы устройств с одинаковым старшим номером управляются одним и тем же драйвером. Все из выше перечисленных файлов устройств имеют старший номер, равный 3, потому что все они управляются одним и тем же драйвером.
Младший номер используется драйвером, для различения аппаратных средств, которыми он управляет. Возвращаясь к примеру выше, заметим, что хотя все три устройства обслуживаются одним и тем же драйвером, тем не менее каждое из них имеет уникальный младший номер, поэтому драйвер «видит» их как различные аппаратные устройства.
Устройства подразделяются на две большие группы — блочные и символьные. Основное различие блочных и символьных устройств состоит в том, что обмен данными с блочным устройством производится порциями байт — блоками. Они имеют внутренний буфер, благодаря чему повышается скорость обмена. В большинстве Unix-систем размер одного блока равен 1 килобайту или другому числу, являющемуся степенью числа 2. Символьные же устройства — это лишь каналы передачи информации, по которым данные следуют последовательно, байт за байтом. Большинство устройств относятся к классу символьных, поскольку они не ограничены размером блока и не нуждаются в буферизации. Если первый символ в списке, полученном командой ls-l /dev, 'b', тогда это блочное устройство, если 'c', тогда — символьное. Устройства, которые были приведены в примере выше — блочные. Ниже приводится список некоторых символьных устройств (последовательные порты):

crw-rw----  1 root  dial 4, 64 Feb 18 23:34 /dev/ttyS0
crw-r-----  1 root  dial 4, 65 Nov 17 10:26 /dev/ttyS1
crw-rw----  1 root  dial 4, 66 Jul  5  2000 /dev/ttyS2
crw-rw----  1 root  dial 4, 67 Jul  5  2000 /dev/ttyS3

Если вам интересно узнать, как назначаются старшие номера устройств, загляните в файл /usr/src/linux/documentation/devices.txt.
Все файлы устройств создаются в процессе установки системы с помощью утилиты mknod. Чтобы создать новое устройство, например с именем «coffee», со старшим номером 12 и младшим номером 2, нужно выполнить команду mknod /dev/coffee c 12 2. Вас никто не обязывает размещать файлы устройств в каталоге /dev, тем не менее, делается это в соответствии с принятыми соглашениями. Однако, при разработке драйвера устройства, на период отладки, размещать файл устройства в своем домашнем каталоге — наверное не такая уж и плохая идея. Единственное — не забудьте исправить место для размещения файла устройства после того, как отладка будет закончена.
Еще несколько замечаний, которые явно не касаются обсуждаемой темы, но которые мне хотелось бы сделать. Когда происходит обращение к файлу устройства, ядро использует старший номер файла, для определения драйвера, который должен обработать это обращение. Это означает, что ядро в действительности не использует и даже ничего не знает о младшем номере. Единственный, кто обеспокоен этим — это сам драйвер. Он использует младший номер, чтобы отличить разные физические устройства.
Между прочим, когда я говорю «устройства», я подразумеваю нечто более абстрактное чем, скажем, PCI плата, которую вы можете подержать в руке. Взгляните на эти два файла устройств:

% ls -l /dev/fd0 /dev/fd0u1680
brwxrwxrwx   1 root  floppy   2,  0 Jul  5  2000 /dev/fd0
brw-rw----   1 root  floppy   2, 44 Jul  5  2000 /dev/fd0u1680

К настоящему моменту вы можете сказать об этих файлах устройств, что оба они — блочные устройства, что обслуживаются одним и тем же драйвером (старший номер 2). Вы можете даже заявить, что они оба представляют ваш дисковод для гибких дисков, несмотря на то, что у вас стоит только один дисковод. Но почему два файла? А дело вот в чем, один из них представляет дисковод для дискет, емкостью 1.44 Мб. Другой — тот же самый дисковод, но для дискет емкостью 1.68 Мб, и соответствует тому, что некоторые люди называют «суперотформатированным» диском («superformatted» disk). Такие дискеты могут хранить больший объем данных, чем стандартно-отформатированная дискета. Вот тот случай, когда два файла устройства, с различным младшими номерами, фактически представляют одно и то же физическое устройство. Так что, слово «устройство», в нашем обсуждении, может означать нечто более абстрактное.

Создаём файл устройства

Мы будем использовать файл-драйвер символьного устройства. Чтобы не заниматься пересказом книги Linux Device Driver, а так же упростить себе жизнь, то я взял готовый пример регистрации символьного драйвера из этой статьи. В данной статье не занимаются поиском свободного фиксированного минора, а используется динамический минор. Для нас — это самое то.

Итак, модуль теперь будет выглядеть следующем образом: работа с экраном, которую мы реализовали выше остаётся без изменений, а вот остальную часть переписываем с нуля.
Определяем структуру file_operations

static const struct  lptlcd_fops = {
	.owner  = THIS_MODULE,
	.read   = dev_read,
	.write  = dev_write,
};

Где dev_read и dev_write — это указатели на функции (регистрация обратного вызова), которые будут обрабатывать соответственно чтение и запись из файла устройства. Здесь ещё можно, да наверное и нужно добавить указатели .open и .release для проверки однократного открытия и закрытия файла устройства, но мне пока лениво.

Для регистрации драйвера заводим вот такую структуру

static struct miscdevice lptlcd_dev = {
	MISC_DYNAMIC_MINOR,    
	"lptlcd",
	&lptlcd_fops
};

где MISC_DYNAMIC_MINOR — макрос для динамического минора, строка «lptlcd» — это название устройства, как оно будет выглядить в папке /dev/ (в нашем случае будет /dev/lptlcd), &lptlcd_fops — указатель на структуру file_operations.

Код регистрации драйвера тоже весьма прост

static int __init dev_init( void ) {
	int ret;
//регистрируем файл устройство
	ret = misc_register( &lptlcd_dev );
//проблемы с регистрацией ругаемся
	if( ret ) printk( KERN_ERR "=== Unable to register misc devicen" );
//Инициализируем экран
	LCD_init(0, LCD_COLUMNS, LCD_STRINGS);
	print_to_string (0, 0, "lptlcd init     "			, 16);
	return ret;
}

Дерегистрация устройства проста:

static void __exit dev_exit( void ) {
	misc_deregister( &lptlcd_dev );
}

Ну и не забываем макросы установки колбеков, лицензии, автора и версии

module_init( dev_init );

module_exit( dev_exit );

MODULE_LICENSE("GPL");
MODULE_AUTHOR( "Dolin Sergey <dlinyj@gmail.com>" );
MODULE_VERSION( "0.1" );

Магия начинается в функции dev_read и dev_write. Начну с первой

static char *info_str = "lcdlpt device drivernAutor Dolin Sergey aka dlinyj dliny@gmail.comn";         // buffer!

static ssize_t dev_read( struct file * file, char * buf,
						size_t count, loff_t *ppos ) {
	int len = strlen( info_str );
	if( count < len ) return -EINVAL;
	if( *ppos != 0 ) {
		return 0;
	}
	if( copy_to_user( buf, info_str, len ) ) return -EINVAL;
	*ppos = len;
	return len;
}

Делает она следующее: если мы произведём чтение файла устройства, например так:

cat /dev/lptlcd

То нам на экран будет выведена строка за указателем info_str.
Код настолько очевидный, что в комментариях надеюсь не нуждается. Единственное, что функция copy_to_user на равне с функцией copy_from_user используется копирования данных между адресными пространствами пользователя и ядра.

Другая функции dev_write, она-то и пихает данные в экран.


static int str_pos = 0; //номер строки
static int col_pos = 0; //номер столбца

static ssize_t dev_write( struct file *file, const char *buf, size_t count, loff_t *ppos ) {
	int i;
//Начинаем копировать байты.
	for (i=0; i<count;i++) { 
//переводим курсор в текущую позицию
		setCursor(col_pos, str_pos);
//если позиция у нас нулевая по обоим координатам, то очищаем экран 
		if ((col_pos==0) && (str_pos==0)) clear(); 
//если не перевод каретки, то выводим на экран
		if (buf[i] != 'n') { 
			write_l(buf[i]);
			col_pos++;
			}
//если перевод каретки, то делаем позицию курсора максимальной
		else {col_pos=LCD_COLUMNS;}
//при максимальной позиции курсора преходим на следующую строку
		if (col_pos == LCD_COLUMNS) {
			col_pos=0;
			str_pos++;
//если исчерпали лимит строк, то идём в нулевую строку
			if (str_pos == LCD_STRINGS)	{
				str_pos=0;
			}
		}
	}
	return count;
}

На мой взгляд не добавить и не убрать. Надеюсь тут всё понятно.
Компилируем, добавляем модуль в ядро и смотрим, что у нас появился файл:

$ ls /dev/lptlcd
/dev/ttylptlcd

Пробуем прочитать из него и записать что-то:

$ cat /dev/lptlcd
lcdlpt device driver
Autor Dolin Sergey aka dlinyj dliny@gmail.com
echo -ne "Trolo PyshnPysh" > /dev/lptlcd

И увидим вот это:

Создание собственных драйверов под Linux
Я водитель НЛО

Всё, мы теперь имеем полностью рабочий драйвер вместе с отвечающим взаимностью устройством! Поздравляю, можно бежать в лабаз за шампанским, и потом в юзерспейсе под волшебные пузырьки писать программы, работающие с этим девайсом.
Но всё же меня лично гложут какие-то сомнения, вот что-то не так…

Хочется БОЛЬШЕГО!!!

Итак, сейчас мы написали вполне рабочий драйвер, который уже можно использовать в повседневной практике. Выводить на него любой текст, даже текст из файла, использовать в своих программах. Но есть ощущение незавершённости, несовершенства.
Если вы возьмёте и почитаете любую документацию на экран, например статью, которую написал dihalt о инициализации дисплея для AVR easyelectronics.ru/avr-uchebnyj-kurs-podklyuchenie-k-avr-lcd-displeya-hd44780.html, то будете удивлены богатством возможностей. Например:
1. Можно загрузить текст в память, а затем просто двигать видимую область, экономя такты на загрузку новых данных (как помним, у нас могучие задержки).
2. Можно выводить текст в произвольную позицию.
3. Можно очищать экран.
4. И конечно, можно загружать свои шрифты. Не стоит обольщаться по этому поводу, доступно всего 8 символов. Например, задача реализация русского меню с этим экраном без русского шрифта, с латинскими буквами и частично загруженными русскими (8 символов) превращается во вполне занимательный ребус для целого отдела (было, плавали). Так, что это юзается для каких-то специфических символов.

Хороший пример применения возможности загрузки шрифтов я нашёл на кофейном автомате на работе, в котором как раз установлен аналогичный экран, только с синей подсветкой.

Создание собственных драйверов под Linux
Экран на кофейном автомате

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

Итого 2+5 — остаётся всего один запасной символ, который так же вероятно используется. Не очень-то разбежишься.

В общем фишек у дисплея полно, так много, что мне уже лениво читать в документации. Но, как мы видим всего этого мы делать пока не можем. И в этот чудесный момент настало время достать мой рояль из кустов, который уже давным-давно там стоит.
Прежде, чем я приступил к реализации данной идеи (даже до того, как начал покупать детали), я погуглил, а делал ли кто-то подобное. И, о чудо(!) таковой драйвер уже существует. Его написал Michael McLellan и обитает драйвер тут.
Честно скажу, данный драйвер на меня произвёл смешанное впечатление. Скажем так, он стал ценным пособием, как НЕ надо писать драйвера — переписать его под другую схему включения будет нетривиальной задачей, поскольку идёт тупая запись байтов, а каких, куда и зачем — непонятно. Переносимость отсутствует как класс. А куча define-ов для разных ядер выносят моск. Плюс, изначально драйвер задумывался под полную схему включения (8 бит на порт данных, см. схему lcd-mod.sourceforge.net/wiring.php ), а я сразу был нацелен на 4-х битную шину. Она хоть и медленнее (примерно в два раза, т.к. байт посылается двумя посылками), но во первых будет меньше паять, а во вторых — не потребуется отыскивать другие порты. Ну и в третьих, у меня уже был отличный и логичный код для 4-х битной шины. В результате я решил таки написать свой драйвер сам и с нуля, попутно рассказывая как это сделать.

Ладно, к чёрту лирику. В том драйвере есть бесценная штука — это обработка управляющих или ESC-последовательностей. Если потрудится и качнуть этот “рояль”, то можно там найти всякие ридми и хау-ту. Так же на офсайте есть фак. Чтобы вам не пришлось лазать по этим всем данным, я решил собрать всё в одну кучу и на русском языке. Во первых, пару слов об этих последовательностях, вы все уже с ними сталкивались, когда операторе printf добавляли перевод строки в виде ‘n’ — это и есть простейшая ESC-последовательность, которая интерпретируется, как символ 0Ah в ASCII. Такая же петрушка лежит в управлении данным дисплеем.

Итак, управляющие последовательности, для нашего экрана:

  • 33 = Отправка ESC-последовательности, с которой начинаются команды
  • [A = Переместить курсор на одну строку вверх
  • [B = Переместить курсор на одну строку вниз
  • [C = Сдвинуть курсор на одну позицию вправо
  • [D = Сдвинуть курсор на одну позицию влево
  • [H = Переместить курсор в левый верхний угол — домой (позиция 0,0)
  • [J = Очистить всё, НЕ возвращает курсор домой!
  • [K = Стирает до конца строки, НЕ возвращает курсор домой!
  • [M = Новая карта символов (ДЗ — объяснить зачем!)
  • [Y = Позиция Y (см. FAQ ниже)
  • [X = Позиция X (см. FAQ ниже)
  • [R = CGRAM Выбор ячейки памяти
  • [V = Прокрутка включена
  • [W = Прокрутка вылючена
  • [b = Подсветка включена-выключена (у нас работать не будет).

Другие полезные комманды, работают без префикса 33!

  • r = Возврат каретки (возвращают курсор в позицию 0 на текущей линии!)
  • n = Новая линия
  • t = Табуляция (по умолчанию 3 символа)

Мне показалось идея очень крутой, и я нагло решил позаимствовать эту функцию в нашем проекте.

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

Кто ещё не понял, что это и зачем, то более подробно у него описанно тут lcd-mod.sourceforge.net/faq.php. Чтобы не мучаться, я вольно перевёл его и заботливо положил в спойлер с картинками

FAQ

Q. Хорошо, я установил модуль, как теперь я могу его использовать?
A. Экран будет вести себя почти так же, как vt52-терминал ru.wikipedia.org/wiki/VT52, отличие состоит только в использовании пользовательских шрифтов. Если вы хотите просто вывести текст на экран, вам следует послать его в формате ASCII на устройство. Для начала новой строки, следует отправить символ подачи строки — ‘r’ и символ перевода каретки ‘n’ (см. википедию ru.wikipedia.org/wiki/%D0%9F%D0%B5%D1%80%D0%B5%D0%B2%D0%BE%D0%B4_%D1%81%D1%82%D1%80%D0%BE%D0%BA%D0%B8). Например, дав комманду:

echo -en "Line OnernLine Two" > /dev/lptlcd

Создание собственных драйверов под Linux
Перевод каретки

на экран будет выведено “Line One” и “Line Two” на двух строках экрана. От себя замечу, что опции команды “echo” означают:
-n — не завершать строку символом перевода каретки ‘n’
-e — включается поддержка интерпретации управляющих последовательностей

Q. Когда я вывожу новую строку на экран, курсор не перемещается в начало строки?
A. Вы должны так же послать на экран символ перевода на новую строку и перевод каретки. Например:

echo -en "line1rnline2" > /dev/lptlcd

(см. предыдущий пример)
Q. Почему, когда я пишу более, чем n линий на мой n-строчный эран, я вижу только n-1 линий, а последняя линия пустая?
A. Это потому, что вы используете комманду “echo”. Данная комманда, без опций добавляет символ перевода каретки ‘n’ в конце строки. Необходимо использовать аргумент -n, как я говорил выше.

Q. Как я могу передвигать курсор по кругу?
A. Вы можете установить курсор где хотите с помощью специальной управляющей последовательности: ESC-Y[Y-координата+037][X-координата+037]. Например:

echo -en "33[Y3737Hello" > /dev/lptlcd

Создание собственных драйверов под Linux
Hello в позиции 0:0

Курсор будет перемещён в нулевую строку, нулевой столбец и будет выведенно на экран слово “Hello”;

echo -en "33[Y4040Hello" > /dev/lptlcd 

будет выведено тоже самое, но в позицию 1, 1;

Создание собственных драйверов под Linux
Hello в позиции 1:1 (без очистки экрана)

echo -en "33[Y4141Hello" > /dev/lptlcd

аналогично, но напечатано будет в позицию 2, 2

Создание собственных драйверов под Linux
Hello в позиции 2:2 (без очистки экрана, с предыдущими сообщениями)

Число “037” может немного запутать особенно, если вы не привыкли использовать восьмиричную арифметику, в будущих версиях я думаю о том, чтобы отбросить весь протокол vt52 и заменить его в более понятную реализацию (как автор жестоко ошибается :) )..

Q. Как я могу использовать мои собственные шрифты?
A. Контроллер экрана HD4480 поддерживат до восьми символов определённых пользователем и наш модуль ядра поддерживает это, но это может быть довольной сложной операцией. Я собираюсь написать красивую GUI делающее это для вас, когда я вернусь к ней (или когда вы сможете написать об этом мне). Готов поспорить, что это предложение прочтут сущие единицы. Трололо, проверка на внимательность.
Вы можете установить один символ, отправив в устройство следующую комманду: Esc-R[позиция шрифта][8 байт, определяющих битовую маску символа]. Символ определяет только последние пять бит во всём байте, поскольку в символе используется всего 5 столбцов. Я уже говорил об этом выше. Например:

echo -en "33[R13737373737373737" > /dev/lptlcd 

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

Создание собственных драйверов под Linux
Символ полностью закрашенного блока

echo -en "33[R23700370037003700" > /dev/lptlcd

Устанавливает символ по адресу 0 в виде горизонтальных полос.

Создание собственных драйверов под Linux
Символ горизонтальных полос

Чтобы вывести все «левые» символы, надо ввести следующую команду:

echo -en "0001020304050607" > /dev/lptlcd

Создание собственных драйверов под Linux
Вся порнография, что у нас есть сейчас в памяти.

В общем, как видно, требуется переписать функцию dev_write. И она теперь стала выглядеть так:

static ssize_t dev_write( struct file *file, const char *buf, size_t count, loff_t *ppos ) {
	int i;
	for (i=0; i<count;i++) 
		handleInput(buf[i]);
	return count;
}

А всю грязную работу, по разбору полётов на себя взяла функция handleInput. Код данной функции монструозен, по этому я его приводить тут не буду. Оставлю вам на домашнее задание разобраться с ним и найти бекдор. Кто найдёт пасхальное яйцо в коде, отмечайтесь в комментах, тому конфетка.
Для затравки видос бекдора (он реализован в драйвере)


ASCII-ART Move! Запускается пасхалкой

Те, кто покажут мне аналогичный видос на своих экранах получат от меня подарок!

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

while true; do     echo -en "33[H`date +"%d.%m.%Y"` rn`date +%r`" > /dev/lptlcd ;    sleep 1s; done

Создание собственных драйверов под Linux
И они таки ходят!

Разумеется, написал — поделись с народом! Исходные коды доступны по ссылке (всё никак не соберусь юзать гитхаб для обмена кодом, позор).

Чего хотелось бы сделать

Просто так экранчик на столе мало понятно зачем нужен, это правда. Тем более в век планшетов и прочей техники. Когда всё можно перенаправить на ваш смартфон. Другое дело, что скоро грядёт Новый Год, и вспоминается один замечательный новогодний девайс — ёлочка на процессоре lpc2104

Создание собственных драйверов под Linux
Embedded Artists Cristmas Tree

Не стоит гуглить этот дейвас, все упоминания о нём убраны даже с официального сайта, а редкие упоминания о нём сложно найти даже на просторах интернета.
Я даже снял видео работы этой ёлочки, но что-то ютуб тупит и не кажет его. По сему я оставлю тут ссылочку на видео, если проглючится, то посмотрите www.youtube.com/watch?v=MQ-g7VSCXIQ

Кроме весёлого мигания огоньками у этого забавного устройства была крутая особенность — можно было вести переписку с другими владельцами таких ёлочек. Был некий сервант, куда через специальную форму можно было вбить сообщение и оно рассылалось по ёлочкам и выводилось на экран. Это было очень и очень круто и весело.
Теперь внимание идея! Сейчас ничего не мешает заплатить на месяц 500 рублей (а если скинемся, сумма будет совсем мелкой), и арендовать сервак на виртуалке. Туда поставить программулину, которая будет рассылать сообщения. Либо jabber либо какие-то другие способы (да хоть nc и telnet всё сделать). А на компе поставить приёмник этих сообщений. И на Новый Год все мы будем иметь поздравления.
Кто готов помочь это реализовать, и кто будет делать себе такие экраны? Если нас наберётся хотя бы три человека, то будет весьма и весьма весело! Вливайтесь в тусовку разработчиков :)))). Выслушаю ЛЮБЫЕ идеи!

Итог

Буду краток. Данным постом я ставил цель не сделать пересказ книги Linux Device Drivers, а просто показать, что написание дров это нетривиальная задача. Я умышленно ушёл от USB, т.к. там достаточно много теории, подводных камней и прочего геморроя. Но вы вполне можете переписать данный модуль для USB-LPT-шнурков, типа таких

Создание собственных драйверов под Linux

Делается относительно просто: перехватывается протокол обмена с usb и реализуется в вашей программе. Поверьте — это просто. Будет интересно как — я вам расскажу!

А данный драйвер, поменяв всего ОДНУ(!!!) строку вывода в порт можно переписать для применения, например на Raspberry Pi на порт GPIO. Точно, что написать я вам не скажу, но всё решается чтением даташита на используемый проц и занимает всего несколько минут.

Благодарности:

1. Хочу высказать главную благодарность своему учителю Боронину Сергею Сергеевичу sboronin за обучению программированию под linux и прекрасному курсу разработки драйверов под linux. Благодаря его курсам я весьма успешно работаю разработчиком.
2. Камраду Ariman с его замечательной серией статей про разработку дисплея для роутера (первая, вторая, третья часть (а ведь обещалась четвёртая...)). Эта серия, хоть и весьма специфическая, прекрасно иллюстрирует создание устройства и написание для него соответствующих драйверов. И я неоднократно обращался к ней, как к источнику бесценной информации, как в разработке драйверов, так и в разработке модулей для OpenWRT.
3. Разумеется автору книги “Linux Device Driver” Greg Kroah-Hartman. Эта книга просто библия, и в ядре часто можно встретить драйвера, которые базируются на его примерах. Например, в драйверах USB встретил такой:

/drivers/usb/misc/idmouse.c

Derived from the USB Skeleton driver 1.1,
Copyright © 2003 Greg Kroah-Hartman (greg@kroah.com)

И так во многих драйверах. Так, что пользуйтесь книгой — это библия.
4. Michael McLellan автору аналогичного драйвера. Как он говорит в своём драйвере:

* LCD driver for HD44780 compatible displays connected to the parallel port,
* because real men use device files.

К сожалению у меня нет его координат, кроме ссылки на linkedin. Почта из его программ уже не работает. Так, что как ему лично сказать спасибо, я не знаю… Если кто ему напишет, я буду очень признателен.

Ссылки

1. Исходники моей программы для rootfs работающая с дисплеем
2. Исходники модуля ядра, который мы разбирали в этом посте
1. dmilvdv.narod.ru/translate.html Переводы статей. Настольная книга — Linux Device Drivers, Third Edition".
2. Отличная библиотека статей по программированию на русском от IBM www.ibm.com/developerworks/ru/views/global/libraryview.jsp. Конкретно по разработке модулей ядра www.ibm.com/developerworks/ru/views/linux/libraryview.jsp?sort_by=&show_abstract=true&show_all=&search_flag=&contentarea_by=Linux&search_by=%D0%A0%D0%B0%D0%B7%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%BA%D0%B0+%D0%BC%D0%BE%D0%B4%D1%83%D0%BB%D0%B5%D0%B9+%D1%8F%D0%B4%D1%80%D0%B0+Linux&topic_by=-1&type_by=%D0%B2%D1%81%D0%B5&ibm-search=%D0%9F%D0%BE%D0%B8%D1%81%D0%BA

3. Описание дисплея HD44780 (для AVR, но в целом оно универсально) easyelectronics.ru/avr-uchebnyj-kurs-podklyuchenie-k-avr-lcd-displeya-hd44780.html
4. Программы для дисплея (модуль ядра и спектральный анализатор для этого модуля) от Michael McLellan linux.downloadatoz.com/developer-michael-mclellan.html

P.S. Не стреляйте в пианиста, он играет как умеет. Такую громадную статью при моей врождённой неграмотности написать нормально нельзя. Обязательно присылайте мне замечания, правки, и дополнения, буду очень признателен!!!
P.P.S. Ничто не греет душу так, как оставленный комментарий.

Автор: dlinyj

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js