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

Источник: habrahabr
dlinyj

Многие мои друзья и знакомые крутят пальцем у виска или задаются вопросом: не жмёт ли мне череп, когда узнают, что я пишу драйвера под 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 <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); }

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


Наш первый дебют!

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

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

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

Чтобы приступить к написанию драйвера, надо поставить необходимый инструментарий. Как подготовить инструментарий для 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 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" сказать нельзя, по этому приведу цитату из книги в спойлере:

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

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

Мы будем использовать файл-драйвер символьного устройства. Чтобы не заниматься пересказом книги 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, то будете удивлены богатством возможностей. Например:
1. Можно загрузить текст в память, а затем просто двигать видимую область, экономя такты на загрузку новых данных (как помним, у нас могучие задержки).
2. Можно выводить текст в произвольную позицию.
3. Можно очищать экран.
4. И конечно, можно загружать свои шрифты. Не стоит обольщаться по этому поводу, доступно всего 8 символов. Например, задача реализация русского меню с этим экраном без русского шрифта, с латинскими буквами и частично загруженными русскими (8 символов), превращается во вполне занимательный ребус для целого отдела (было, плавали). Так что это юзается для каких-то специфических символов.

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


Экран на кофейном автомате

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

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

В общем фишек у дисплея полно, так много, что мне уже лениво читать в документации. Но, как мы видим, всего этого мы делать пока не можем. И в этот чудесный момент настало время достать мой рояль из кустов, который уже давным-давно там стоит.
Прежде, чем я приступил к реализации данной идеи (даже до того, как начал покупать детали), я погуглил, а делал ли кто-то подобное. И, о чудо(!) таковой драйвер уже существует. Его написал Michael McLellan и обитает драйвер тут.
Честно скажу, данный драйвер на меня произвёл смешанное впечатление. Скажем так, он стал ценным пособием, как НЕ надо писать драйвера - переписать его под другую схему включения будет нетривиальной задачей, поскольку идёт тупая запись байтов, а каких, куда и зачем - непонятно. Переносимость отсутствует как класс. А куча 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

В общем, как видно, требуется переписать функцию 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 "\033[H`date +\"%d.%m.%Y\"` \r\n`date +%r`" > /dev/lptlcd ; sleep 1s; done


И они таки ходят!

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

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

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


Embedded Artists Cristmas Tree

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

Как работает эмбеддеровская ёлочка

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

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

Итог

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

Делается относительно просто: перехватывается протокол обмена с 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. Исходники моего модуля ядра, который мы разбирали в этом посте качнуть
3. "Рояль в кустах"
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


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=35702