Автор: Тим Во (Tim Waugh)
Предыдущая часть: Подсистема параллельного порта Linux 2.4 - часть 1, обзор
Драйверы устройств, работающие в пространстве пользователя
Введение в ppdev
Принтер доступен через /dev/lp0, а параллельный порт доступен через /dev/parport0. Разница заключается в уровне управления, который можно осуществлять по проводам в кабеле параллельного порта.Программа, работающая в пространстве пользователя (такая как диспетчер печати), может отправлять байты по «протоколу принтера» при помощи драйвера принтера. Вкратце это означает, что для каждого байта настраиваются восемь линий данных, затем линия «строб» сообщает принтеру о необходимости прочитать данные, затем принтер устанавливает линию «подтверждение», чтобы сообщить, что он получил байт. Драйвер принтера также позволяет программе, работающей в пространстве пользователя, читать байты в полубайтовом режиме, в котором от периферийного устройства на компьютер можно передавать данные по половине байта за раз (из-за чего режим оказывается довольно медленным).
В отличие от драйвера принтера, драйвер ppdev (доступный через /dev/parport0) позволяет:
- проверять линии статуса,
- задавать линии управления,
- задавать/проверять линии данных (и управлять направлением линий данных),
- ожидать прерывания (срабатывает при изменении одной из линий статуса),
- узнавать, сколько произошло новых прерываний,
- задавать ответ на прерывание,
- использовать согласование IEEE 1284 (чтобы сообщить периферийному устройству, какой режим передачи использовать),
- передавать данные с использованием указанного режима IEEE 1284.
Драйвер в пространстве ядра или в пространстве пользователя?
Решение о том, писать ли драйвер, работающий в пространстве ядра, или драйвер, работающий в пространстве пользователя, зависит от нескольких факторов. Один из основных критериев - это скорость: драйверы, работающие в пространстве ядра, работают быстрее, поскольку они не вытесняются другими процессами, в отличие от приложений в пространстве пользователя.Другой критерий - это лёгкость разработки. В большинстве случаев проще написать драйвер, который будет работать в пространстве пользователя, потому что (а) одно неверное движение не приведёт к аварии всего компьютера, (б) имеется доступ к пользовательским библиотекам (таким как библиотека C), и (в) его проще отлаживать.
Интерфейс программирования
Интерфейс ppdev во многом совпадает с интерфейсом символьных специальных устройств, т.к. он тоже поддерживает функции open, close, read, write и ioctl. Константы для команд ioctl имеются в файле include/linux/ppdev.h.Начало и завершение: open и close
Файл устройства /dev/parport0 представляет любое устройство, подключенное к parport0 - первому параллельному порту в системе. Каждый раз при открытии файла устройства, он представляет (для процесса, выполняющего открытие) другое устройство. Он также может быть открыт более одного раза, но в любой момент только один экземпляр действительно будет управлять параллельным портом. Процесс, который открыл /dev/parport0, работает с параллельным портом через механизм совместного доступа таким же образом, как и любой другой драйвер устройства. Драйвер в пространстве пользователя может работать с параллельным портом совместно как с драйверами устройств пространства ядра, так и с драйверами пространства пользователя.Управление: ioctl
Большая часть управления выполняется через вызовы ioctl. При помощи ioctl драйвер пространства пользователя может управлять как драйвером ppdev в ядре, так и самим физическим параллельным портом. Вызов ioctl принимает в качестве параметров дескриптор файла (который был получен при открытии файла устройства), команду, и (не обязательный) указатель на некоторые данные.Большинству драйверов устройств не требуется эксклюзивный доступ к порту. Такой доступ предоставляется в случае, если он действительно нужен. Например, это могут быть устройства, которым требуется доступ на продолжительное время (многие секунды).
Отметим, что ioctl PPEXCL на самом деле не запрашивает доступ к порту - действие откладывается до тех пор, пока не будет выполнена команда ioctl PPCLAIM.
Важно не требовать параллельный порт надолго, потому что драйверам других устройств не останется времени на обслуживание их устройств. Если ваше устройство не позволяет использовать совместный доступ к порту, лучше затребовать параллельный порт в исключительный доступ (см. PPEXCL).
Устройство, совместимое с IEEE 1284, начинает работу в совместимом режиме, а затем компьютер может согласовать другой режим (например, ECP).
Параметр ioctl должен быть указателем на int. В файле incluce/linux/parport.h определены следующие значения:
- IEEE1284_MODE_COMPAT
- IEEE1284_MODE_NIBBLE
- IEEE1284_MODE_BYTE
- IEEE1284_MODE_EPP
- IEEE1284_MODE_ECP
Параметр ioctl должен быть указателем на int.
Параметр ioctl должен быть указателем на структуру timeval.
- PP_FASTWRITE
- PP_FASTREAD
- PP_W91284PIC
- PPWCONTROL
При этом оборудование фактически не затрагивается, т.к. последнее записанное значение запоминается программно. Так сделано, поскольку некоторое аппаратное обеспечение параллельных портов не предоставляет доступ к регистру управления.
Биты управляющих линий определены в include/linux/parport.h:
- PARPORT_CONTROL_STROBE
- PARPORT_CONTROL_AUTOFD
- PARPORT_CONTROL_SELECT
- PARPORT_CONTROL_INIT
ioctl PPFCONTROL для переключения управляющих линий аналогичен PPWCONTROL, но воздействует лишь на ограниченный набор управляющих линий. Параметр ioctl - это указатель на структуру ppdev_frob_struct:
struct ppdev_frob_struct { unsigned char mask; unsigned char val; };Поля mask и val - это битовое ИЛИ над именами управляющих линий (таких же, как в PPWCONTROL). PPFCONTROL выполняет следующую операцию:
new_ctr = (old_ctr & ~mask) | val;Другими словами, сигналы, указанные в mask, примут значения, указанные в val.
Этот вызов бывает нужен только в сочетании с PPWDATA или PPRDATA.
Параметр ioctl - это указатель на int. Если int - ноль, управление линиями данных включается (прямое направление). Если же int - не ноль, то управление линиями данных отключается (обратное направление).
После этого счётчик прерываний сбрасывается в ноль.
Этот ioctl введён по соображениям скорости. Без этого ioctl соответствующее прерывание начнёт обрабатываться в обработчике прерываний, переключится через poll или select на драйвер в пользовательском пространстве, а затем переключится обратно в ядро, чтобы обработать PPWCONTROL. Выполнение всех процедур в обработчике прерывания значительно быстрее.
Передача данных: read и write
Передача данных при помощи read и write осуществляется очевидным образом. Данные передаются с использованием текущего режима IEEE 1284 (см. ioctl PPSETMODE). В режимах, которые позволяют передавать данные только в одном направлении, будет работать только соответствующая функция.Ожидание событий: poll и select
Драйвер ppdev позволяет драйверам устройств, работающим в пространстве пользователя, ожидать прерываний при помощи poll (и select, который реализуется средствами poll).Когда драйвер устройства, работающий в пространстве пользователя, захочет подождать прерывания, он засыпает, выполняя poll. Когда поступает прерывание, ppdev будит его (событием «read», хотя, строго говоря, читать на самом деле нечего).
Примеры
Имеющиеся здесь два примера описывают процесс написания простого драйвера принтера для ppdev. В первом примере используется функция write, а во втором примере - непосредственная манипуляция линиями данных и управления.Сначала нужно открыть устройство.
int drive_printer (const char *name) { int fd; int mode; /* Потребуется позже. */ fd = open (name, O_RDWR); if (fd == -1) { perror ("open"); return 1; }Параметр name из вышеприведённого фрагмента должен быть строкой, содержащей имя файла устройства параллельного порта, например "/dev/parport0". (Если файлов /dev/parport нет, то их можно создать при помощи mknod. Это файлы специальных символьных устройств со старшим номером 99.)
Прежде чем работать с портом, нужно получить к нему доступ.
if (ioctl (fd, PPCLAIM)) { perror ("PPCLAIM"); close (fd); return 1; }Наш драйвер принтера будет просто копировать свой ввод (со стандартного потока ввода) на принтер. Сделать это можно одним из двух способов. Первый способ - передать всё драйверу, работающему в ядре, зная что принтер работает по протоколу, который в IEEE 1284 называется режимом совместимости.
/* Переключимся в совместимый режим. (Фактически этого делать * не нужно, поскольку в начале всегда используется совместимый режим, * но здесь демонстрируется использование PPNEGOT.) */ mode = IEEE1284_MODE_COMPAT; if (ioctl (fd, PPNEGOT, &mode)) { perror ("PPNEGOT"); close (fd); return 1; } for (;;) { char buffer[1000]; char *ptr = buffer; size_t got; got = read (0 /* стандартный поток ввода */, buffer, 1000); if (got < 0) { perror ("read"); close (fd); return 1; } if (got == 0) /* Конец ввода */ break; while (got > 0) { int written = write_printer (fd, ptr, got); if (written < 0) { perror ("write"); close (fd); return 1; } ptr += written; got -= written; } }Определение функция write_printer в фрагменте выше не показано. Это сделано специально, поскольку приведённый в фрагменте главный цикл может использоваться с обоими рассматриваемыми методами управления принтером. Вот первая реализация write_printer:
ssize_t write_printer (int fd, const void *ptr, size_t count) { return write (fd, ptr, count); }При помощи функции write данные передаются драйверу, работающему в пространстве ядра. Дальше он обрабатывает их по протоколу принтера.
Теперь давайте попробуем пойти более сложным путём! В рассматриваемом примере нет никаких причин, чтобы делать что-либо кроме вызова write, потому что принтер работает по протоколу IEEE 1284. С другой стороны, этот пример не требует наличия драйвера в пространстве пользователя, потому что уже есть один, который работает в пространстве ядра. В целях иллюстрации, попробуем представить, что принтер работает по протоколу, который в Linux ещё не реализован.
Получим альтернативную реализацию write_printer (для краткости обработка ошибок не выполняется):
ssize_t write_printer (int fd, const void *ptr, size_t count) { ssize_t wrote = 0; while (wrote < count) { unsigned char status, control, data; unsigned char mask = (PARPORT_STATUS_ERROR | PARPORT_STATUS_BUSY); unsigned char val = (PARPORT_STATUS_ERROR | PARPORT_STATUS_BUSY); struct ppdev_frob_struct frob; struct timespec ts; /* Подождём готовности принтера */ for (;;) { ioctl (fd, PPRSTATUS, &status); if ((status & mask) == val) break; ioctl (fd, PPRELEASE); sleep (1); ioctl (fd, PPCLAIM); } /* Задаём линии данных */ data = * ((char *) ptr)++; ioctl (fd, PPWDATA, &data); /* Немного подождём */ ts.tv_sec = 0; ts.tv_nsec = 1000; nanosleep (&ts, NULL); /* Стробирующий импульс */ frob.mask = PARPORT_CONTROL_STROBE; frob.val = PARPORT_CONTROL_STROBE; ioctl (fd, PPFCONTROL, &frob); nanosleep (&ts, NULL); /* Конец импульса */ frob.val = 0; ioctl (fd, PPFCONTROL, &frob); nanosleep (&ts, NULL); wrote++; } return wrote; }Чтобы продемонстрировать интерфейс ppdev слегка подробнее, приведём небольшой фрагмент кода, который предназначен для имитации протокола принтера со стороны принтера.
for (;;) { int irqc; int busy = nAck | nFault; int acking = nFault; int ready = Busy | nAck | nFault; char ch; /* Задаём управляющие линии на случай прерывания */ ioctl (fd, PPWCTLONIRQ, &busy); /* Теперь мы готовы */ ioctl (fd, PPWCONTROL, &ready); /* Ждём прерывания */ { fd_set rfds; FD_ZERO (&rfds); FD_SET (fd, &rfds); if (!select (fd + 1, &rfds, NULL, NULL, NULL)) /* Сигнал получен? */ continue; } /* На линиях управления выставляется сигнал "занято" */ /* Читаем данные */ ioctl (fd, PPRDATA, &ch); /* Очищаем прерывание */ ioctl (fd, PPCLRIRQ, &irqc); if (irqc > 1) fprintf (stderr, "Аххх! Потеряно %d прерываний!\n", irqc - 1); /* Подтверждаем его */ ioctl (fd, PPWCONTROL, &acking); usleep (2); ioctl (fd, PPWCONTROL, &busy); putchar (ch); }А вот пример (тоже без обработки ошибок), который демонстрирует, как читать данные из порта в режиме ECP, с необязательным начальным согласованием режима ECP.
{ int fd, mode; fd = open ("/dev/parport0", O_RDONLY | O_NOCTTY); ioctl (fd, PPCLAIM); mode = IEEE1284_MODE_ECP; if (negotiate_first) { ioctl (fd, PPNEGOT, &mode); /* PPSETMODE не требуется */ } else { ioctl (fd, PPSETMODE, &mode); } /* Теперь делаем с fd всё, что нужно */ close (0); dup2 (fd, 0); if (!fork()) { /* Потомок */ execlp ("cat", "cat", NULL); exit (1); } else { /* Родитель */ wait (NULL); } /* Ну вот и закончили */ ioctl (fd, PPRELEASE); close (fd); }
Комментариев нет:
Отправить комментарий