Как написать драйвер для 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). Причём плата попалась на столько китайская, что я даже не смог найти никакой документации по напаянному чипу на ней. Ничего, будем ещё и реверсить её :)))).
Аппаратные исходники
Будем считать, что достать эти компоненты не составило труда, и ваши руки горят от нетерпения всё сделать.
Предупреждение!!!
Внимательно прочитать!
Хочу обозначить, что мы работаем в самых тяжёлых условиях, с железом, да ещё и будем работать на уровне ядра. Живём, так сказать, внутри пациента. Любая ошибка в программе и ядро падает. При чём оно может даже не успеть помахать вам kernel_panic-ом, просто внезапно всё перестаёт работать. Обратите внимание, что вы запустили драйвер, выгрузили его, и вдруг начались неявные глюки — всё, ваш путь — перезагрузка. Если система жива, то командой 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 Ом, или больше или меньше, не важно)
Схема проверки
Поскольку у меня уже есть заготовка светодиодов для теста такого рода, то я вставлял провода прямо в разъем CENTRONICS и у меня не влезло все 8 штук проводов, поэтому вставил только 6 (первые 4 и последние два).
Собранный тестер порта
И набросаем небольшую программку для проверки.
Для работы с портом я взял пример отсюда 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 — але оп:
Нуль
На выводах AAh
И, разумеется через секунду FFh
Результат налицо. Т.е. с нумерацией портов мы угадали, можно приступать к дровам. LPT-порт данной платы живёт по адресу. 0Xc020!
Хочу обратить внимание программистов-жестянщиков на магическое число 0xAA — это последовательность единиц и нулей. Вот так: 10101010b. Это очень удобно для всяких отладок. А вообще всякий кто работает с железом должен легко в уме переводить двоичное, десятичное, шестнадцатеричное туда-сюда-обратно.
Таки дисплей
Барабанная дробь, настало время собрать железку воедино! Для этого нам осталось всё спаять вместе. Всё собираем по следующей схеме.
Знаю, что схема не по ГОСТу, но это и не журнал «Радио»
Для питания экрана я распатронил провод USB, и сейчас у меня к экрану идут два провода. Есть хитрый вариант, просто до него никак не доходят руки — это пустить питание USB прямо в проводе LPT, отпаяв один из многочисленных земляных проводов. Как дойдут руки — обязательно сделаю. Допишу только эту статью :)))).
В результате, после подачи питания, мы должны получить что-то вроде этого. Подрегулируйте яркость резистором, чтобы появились две полоски. Они свидетельствуют нам о том, что дисплей не проинициализирован.
Девайс в сборе
Фух, с аппаратной частью мы закончили, пришла пора переходить к программе.
Приступаем злобным опытам с экраном в программе
В качестве готовой либы для работы с дисплеем использовал код от 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), второй номер строки (от нуля до трёх), третий — указатель на символьную строку и последний — длинна строки. Строка не проверяется на окончание “\0”, длину надо контролировать самостоятельно! В результате попробуем дисплейчик:
#include #include #include #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); }
В результате на экране должно появится что-то типа этого:
Наш первый дебют!
Не пугайтесь этой порнографии. Этот экран был списан за битые пиксели, точнее там просто погиб контроллер от статики (их там несколько). Для работы он не годится, а для всяких подобных опытов — самое оно. Такие хреновые экраны делают китайцы!
На этом можно было бы остановиться, т.к. с данным примером можно выводить любые символы, загружать шрифты и т.п. Но, не в качестве драйвера. Хотя можно переделать в демон или как-то ещё, насколько позволит фантазия.
Наша глобальная цель — драйвер устройства, поэтому к нему и приступим.
Инструментарий для сборки драйвера
Чтобы приступить к написанию драйвера, надо поставить необходимый инструментарий. Как подготовить инструментарий для 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 и прочие, просматривать исходные тексты — это невероятно полезно!
Собственно драйвер
Если вы думаете, что мы сейчас просто поправим makefile и получим готовый драйвер, то вы глубоко заблуждаетесь. Всё, теперь мы находимся в другом мире: не в пространстве пользователя, в пространстве ядра. И тут действуют совсем другие правила.
Чтобы осознать всю бедовость ситуации, то вы должны понимать, что ядро — это одна большая-пребольшая программа. А это означает, что глобальные переменные, функции и т.п. могут быть доступны в других местах ядра! Поэтому ВСЕ глобальные переменные (их следует избегать по максимуму) должны быть объявлены как 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 init\n"); 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
Или, если вы используете дистрибутив отличный от Ubuntu, то следует исполнять это под root, и убрать sudo.
Привет вам!
Если вы всё это увидели — поздравляю, это ваш первый рабочий бесполезный модуль ядра, который выводит надпись на экран! Это конечно занимательно, но надо таки превращать наши эксперименты в нормальный драйвер, с которым можно работать. А для этого всё же придётся рассказать о матчасти, хоть немного.
Пару слов о матчасти
Как ни крути, но без знания некой матчасти далеко не удешь.Есть символьные, блочные и сетевые устройства. Также у каждого устройства есть старший и младший номер устройства. Лучше, чем в книге “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 file_operations 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 device\n" ); //Инициализируем экран 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 driver\nAuthor Dolin Sergey aka dlinyj dliny@gmail.com\n"; // 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 Author Dolin Sergey aka dlinyj dliny@gmail.com echo -ne "Trolo Pysh\nPysh" > /dev/lptlcd
И увидим вот это:
Всё, мы теперь имеем полностью рабочий драйвер вместе с отвечающим взаимностью устройством! Поздравляю, можно бежать в лабаз за шампанским, и потом в юзерспейсе под волшебные пузырьки писать программы, работающие с этим девайсом.
Но всё же меня лично гложут какие-то сомнения, вот что-то не так…
Хочется БОЛЬШЕГО!!!
Итак, сейчас мы написали вполне рабочий драйвер, который уже можно использовать в повседневной практике. Выводить на него любой текст, даже текст из файла, использовать в своих программах. Но есть ощущение незавершённости, несовершенства.
Если вы возьмёте и почитаете любую документацию на экран, например статью, которую написал DIHALT о инициализации дисплея для AVR, то будете удивлены богатством возможностей. Например:
- Можно загрузить текст в память, а затем просто двигать видимую область, экономя такты на загрузку новых данных (как помним, у нас могучие задержки).
- Можно выводить текст в произвольную позицию.
- Можно очищать экран.
- И конечно, можно загружать свои шрифты. Не стоит обольщаться по этому поводу, доступно всего 8 символов. Например, задача реализация русского меню с этим экраном без русского шрифта, с латинскими буквами и частично загруженными русскими (8 символов), превращается во вполне занимательный ребус для целого отдела (было, плавали). Так что это юзается для каких-то специфических символов.
Хороший пример применения возможности загрузки шрифтов я нашёл на кофейном автомате на работе, в котором как раз установлен аналогичный экран, только русифицированный.
Экран на кофейном автомате
Вот тут отлично видно использование символов:
- два символа на сахар, пустой кружок (не догадался снять) и полный;
- пять символов бегущей строки (в символе 5 столбцов).
Итого 2+5 — остаётся всего один запасной символ, который так же, вероятно, используется. Не очень-то разбежишься.
В общем фишек у дисплея полно, так много, что мне уже лениво читать в документации. Но, как мы видим, всего этого мы делать пока не можем. И в этот чудесный момент настало время достать мой рояль из кустов, который уже давным-давно там стоит.
Прежде, чем я приступил к реализации данной идеи (даже до того, как начал покупать детали), я погуглил, а делал ли кто-то подобное. И, о чудо(!) таковой драйвер уже существует. Его написал Michael McLellan и обитает драйвер на http://lcd-mod.sourceforge.net/.
Честно скажу, данный драйвер на меня произвёл смешанное впечатление. Скажем так, он стал ценным пособием, как НЕ надо писать драйвера — переписать его под другую схему включения будет нетривиальной задачей, поскольку идёт тупая запись байтов, а каких, куда и зачем — непонятно. Переносимость отсутствует как класс. А куча define-ов для разных ядер выносят моск. Плюс, изначально драйвер задумывался под полную схему включения (8 бит на порт данных, см. схему lcd-mod.sourceforge.net/wiring.php ), а я сразу был нацелен на 4-х битную шину. Она хоть и медленнее (примерно в два раза, т.к. байт посылается двумя посылками), но во-первых, будет меньше паять, а во-вторых — не потребуется отыскивать другие порты. Ну и в третьих, у меня уже был отличный и логичный код для 4-х битной шины. В результате я решил таки написать свой драйвер сам и с нуля, попутно рассказывая как это сделать.
Ладно, к чёрту лирику. В том драйвере есть бесценная штука — это обработка управляющих или ESC-последовательностей. Если потрудится и качнуть этот “рояль”, то можно там найти всякие ридми и хау-ту. Так же на офсайте есть фак. Чтобы вам не пришлось лазать по этим всем данным, я решил собрать всё в одну кучу и на русском языке. Во первых, пару слов об этих последовательностях, вы все уже с ними сталкивались, когда операторе printf добавляли перевод строки в виде ‘\n’ — это и есть простейшая ESC-последовательность, которая интерпретируется, как символ 0Ah в ASCII. Такая же петрушка лежит в управлении данным дисплеем.
Итак, управляющие последовательности, для нашего экрана:
- \033 = Отправка ESC-последовательности, с которой начинаются команды
- [A = Переместить курсор на одну строку вверх
- [B = Переместить курсор на одну строку вниз
- [C = Сдвинуть курсор на одну позицию вправо
- [D = Сдвинуть курсор на одну позицию влево
- [H = Переместить курсор в левый верхний угол — домой (позиция 0,0)
- [J = Очистить всё, НЕ возвращает курсор домой!
- [K = Стирает до конца строки, НЕ возвращает курсор домой!
- [M = Новая карта символов (ДЗ — объяснить зачем!)
- [Y = Позиция Y (см. FAQ ниже)
- [X = Позиция X (см. FAQ ниже)
- [R = CGRAM Выбор ячейки памяти
- [V = Прокрутка включена
- [W = Прокрутка вылючена
- [b = Подсветка включена-выключена (у нас работать не будет).
Другие полезные команды, работают без префикса \033!
- \r = Возврат каретки (возвращают курсор в позицию 0 на текущей линии!)
- \n = Новая линия
- \t = Табуляция (по умолчанию 3 символа)
Идея мне показалось очень крутой, и я нагло решил позаимствовать эту функцию в нашем проекте.
К слову, все эти последовательности чудесно работают в обычной консоли, ими можно пользоваться при выводе текста.
Кто ещё не понял, что это и зачем, то более подробно у него описанно тут lcd-mod.sourceforge.net/faq.php. Чтобы не мучаться, я вольно перевёл его и заботливо положил в спойлер с картинками
FAQ
Q. Хорошо, я установил модуль, как теперь я могу его использовать?
A. Экран будет вести себя почти так же, как vt52-терминал ru.wikipedia.org/wiki/VT52, отличие состоит только в использовании пользовательских шрифтов. Если вы хотите просто вывести текст на экран, вам следует послать его в формате ASCII на устройство. Для начала новой строки, следует отправить символ подачи строки — ‘\r’ и символ перевода каретки ‘\n’ (см. википедию ). Например, дав комманду:
echo -en "Line One\r\nLine Two" > /dev/lptlcd
Перевод каретки
на экран будет выведено “Line One” и “Line Two” на двух строках экрана. От себя замечу, что опции команды “echo” означают:
-n — не завершать строку символом перевода каретки ‘\n’
-e — включается поддержка интерпретации управляющих последовательностей
Q. Когда я вывожу новую строку на экран, курсор не перемещается в начало строки?
A. Вы должны так же послать на экран символ перевода на новую строку и перевод каретки. Например:
echo -en "line1\r\nline2" > /dev/lptlcd
(см. предыдущий пример)
Q. Почему, когда я пишу более, чем n линий на мой n-строчный эран, я вижу только n-1 линий, а последняя линия пустая?
A. Это потому, что вы используете комманду “echo”. Данная комманда, без опций добавляет символ перевода каретки ‘\n’ в конце строки. Необходимо использовать аргумент -n, как я говорил выше.
Q. Как я могу передвигать курсор по кругу?
A. Вы можете установить курсор где хотите с помощью специальной управляющей последовательности: ESC-Y[Y-координата+037][X-координата+037]. Например:
echo -en "\033[Y\037\037Hello" > /dev/lptlcd
Hello в позиции 0:0
Курсор будет перемещён в нулевую строку, нулевой столбец и будет выведенно на экран слово “Hello”;
echo -en "\033[Y\040\040Hello" > /dev/lptlcd
будет выведено тоже самое, но в позицию 1, 1;
Hello в позиции 1:1 (без очистки экрана)
echo -en "\033[Y\041\041Hello" > /dev/lptlcd
аналогично, но напечатано будет в позицию 2, 2
Hello в позиции 2:2 (без очистки экрана, с предыдущими сообщениями)
Число “037” может немного запутать особенно, если вы не привыкли использовать восьмиричную арифметику, в будущих версиях я думаю о том, чтобы отбросить весь протокол vt52 и заменить его в более понятную реализацию (как автор жестоко ошибается :) )..
Q. Как я могу использовать мои собственные шрифты?
A. Контроллер экрана HD4480 поддерживат до восьми символов определённых пользователем и наш модуль ядра поддерживает это, но это может быть довольной сложной операцией. Я собираюсь написать красивую GUI делающее это для вас, когда я вернусь к ней (или когда вы сможете написать об этом мне). Готов поспорить, что это предложение прочтут сущие единицы. Трололо, проверка на внимательность.
Вы можете установить один символ, отправив в устройство следующую комманду: Esc-R[позиция шрифта][8 байт, определяющих битовую маску символа]. Символ определяет только последние пять бит во всём байте, поскольку в символе используется всего 5 столбцов. Я уже говорил об этом выше. Например:
echo -en "\033[R1\037\037\037\037\037\037\037\037" > /dev/lptlcd
Устанавливает символпо адресу 1, представляющую собой полностью закрашенный блок. Такой же, как я выше приводил у кофейного автомата в полосе готовности.
Символ полностью закрашенного блока
echo -en "\033[R2\037\000\037\000\037\000\037\000" > /dev/lptlcd
Устанавливает символ по адресу 0 в виде горизонтальных полос.
Символ горизонтальных полос
Чтобы вывести все «левые» символы, надо ввести следующую команду:
echo -en "\000\001\002\003\004\005\006\007" > /dev/lptlcd
Вся порнография, что у нас есть сейчас в памяти.
В общем, как видно, требуется переписать функцию 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. Код данной функции монструозен, поэтому я его приводить тут не буду.
В общем, в результате мы получили кошерный экран, который можно использовать для вывода разной полезной и бесполезной информации. Вот, например на него можно выводить текущее время.
while true; do echo -en "\033[H`date +\"%d.%m.%Y\"` \r\n`date +%r`" > /dev/lptlcd ; sleep 1s; done
И они таки ходят!
Разумеется, написал — поделись с народом! Исходные коды lptlcd
Чего хотелось бы сделать
Просто так экранчик на столе мало понятно зачем нужен, это правда. Тем более в век планшетов и прочей техники. Когда всё можно перенаправить на ваш смартфон. Другое дело, что скоро грядёт Новый Год, и вспоминается один замечательный новогодний девайс — ёлочка на процессоре lpc2104
Embedded Artists Cristmas Tree
Не стоит гуглить этот дейвас, все упоминания о нём убраны даже с официального сайта, а редкие упоминания о нём сложно найти даже на просторах интернета.
Я даже снял видео работы этой ёлочки
Как работает эмбеддеровская ёлочка
Кроме весёлого мигания огоньками у этого забавного устройства была крутая особенность — можно было вести переписку с другими владельцами таких ёлочек. Был некий сервант, куда через специальную форму можно было вбить сообщение и оно рассылалось по ёлочкам и выводилось на экран. Это было очень и очень круто и весело.
Итог
Буду краток. Данным постом я ставил цель не сделать пересказ книги Linux Device Drivers, а просто показать, что написание дров это тривиальная задача. Я умышленно ушёл от USB, т.к. там достаточно много теории, подводных камней и прочего геморроя. Но вы вполне можете переписать данный модуль для USB-LPT-шнурков, типа таких
Делается относительно просто: перехватывается протокол обмена с usb и реализуется в вашей программе. Поверьте — это просто.
А данный драйвер, поменяв всего ОДНУ(!!!) строку вывода в порт можно переписать для применения, например на Raspberry Pi на порт GPIO. Точно, что написать я вам не скажу, но всё решается чтением даташита на используемый проц и занимает всего несколько минут.
Ссылки
1. Исходники моей программы для rootfs работающая с дисплеем lptlcd
2. Исходники моего модуля ядра, который мы разбирали в этом посте lptlcd_kernel
4. dmilvdv.narod.ru/translate.html Переводы статей. Настольная книга — Linux Device Drivers, Third Edition”.
5. Отличная библиотека статей по программированию на русском от IBM обитает тут. Конкретно по разработке модулей ядра
6. Описание дисплея HD44780 (для AVR, но в целом оно универсально) easyelectronics.ru/avr-uchebnyj-kurs-podklyuchenie-k-avr-lcd-displeya-hd44780.html
7. Программы для дисплея (модуль ядра и спектральный анализатор для этого модуля) от Michael McLellan linux.downloadatoz.com/developer-michael-mclellan.html