воскресенье, 21 апреля 2019 г.

Подсистема параллельного порта Linux 2.4 - часть 2, ppdev - драйверы устройств, работающие в пространстве пользователя

Перевод: The Linux 2.4 Parallel Port Subsystem
Автор: Тим Во (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 принимает в качестве параметров дескриптор файла (который был получен при открытии файла устройства), команду, и (не обязательный) указатель на некоторые данные.
PPCLAIM



Затребовать доступ к порту. Необходимо сделать это перед тем, как приступить к работе с параллельным портом. Отметим, что некоторые операции действуют только на драйвер ppdev, но не на порт - например, PPSETMODE. Они могут осуществляться только в тот момент, когда доступ к порту не затребован.

PPEXCL



Указывает драйверу ядра запретить любое совместное использование с другими драйверами, то есть запрашивается исключительный доступ. Команда PPEXCL действует только пока доступ к порту ещё никем не затребован. Последующий запрос ioctl PPCLAIM может завершится неудачно, т.к. к этому моменту доступ к порту может быть предоставлен какому-то другому драйверу.

Большинству драйверов устройств не требуется эксклюзивный доступ к порту. Такой доступ предоставляется в случае, если он действительно нужен. Например, это могут быть устройства, которым требуется доступ на продолжительное время (многие секунды).

Отметим, что ioctl PPEXCL на самом деле не запрашивает доступ к порту - действие откладывается до тех пор, пока не будет выполнена команда ioctl PPCLAIM.

PPRELEASE



Освобождает порт. Освобождение порта отменяет ранее затребованный доступ к порту. Это позволит драйверам других устройств общаться с их устройствами (если они есть).

PPYIELD



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

Важно не требовать параллельный порт надолго, потому что драйверам других устройств не останется времени на обслуживание их устройств. Если ваше устройство не позволяет использовать совместный доступ к порту, лучше затребовать параллельный порт в исключительный доступ (см. PPEXCL).

PPNEGOT



Производит согласование одного из режимов IEEE 1284. Согласование - это метод, по которому компьютер и периферийное устройство принимают решение об используемом протоколе передачи данных.

Устройство, совместимое с 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 PPNEGOT на самом деле выполняет два действия: производит согласование режима и настраивает поведение последующих вызовов read/write, которые будут работать в этом режиме (но см. также PPSETMODE).

PPSETMODE



Задаёт режим передачи IEEE 1284, который будет использоваться вызовами read и write.

Параметр ioctl должен быть указателем на int.

PPGETMODE



Возвращает текущий режим IEEE 1284, используемый read и write.

PPGETTIME



Возвращает значение таймаута. Вызовы read и write завершаются, если периферийное устройство не ответит достаточно быстро. ioctl PPGETTIME возвращает время, в течение которого периферийное устройство считается доступным, даже если оно не отвечает.

Параметр ioctl должен быть указателем на структуру timeval.

PPSETTIME



Задаёт таймаут. Параметр ioctl должен быть указателем на структуру timeval.

PPGETMODES



Запрашивает возможности оборудования (то есть поле modes из структуры parport).

PPSETFLAGS



Задаёт флаги устройства ppdev, которые могут влиять на последующие операции ввода-вывода. Доступны следующие флаги:
  • PP_FASTWRITE
  • PP_FASTREAD
  • PP_W91284PIC
  • PPWCONTROL
Задаёт линии управления. Параметр ioctl - это указатель на unsigned char, содержащий результат битового ИЛИ над определениями из include/linux/parport.h, соответствующих управляющих линий.

PPRCONTROL



Возвращает последнее значение, записанное в регистр управления, в виде unsigned char: каждый бит соответствует управляющий линии (некоторые из них не используются). Параметр ioctl должен быть указателем на unsigned char.

При этом оборудование фактически не затрагивается, т.к. последнее записанное значение запоминается программно. Так сделано, поскольку некоторое аппаратное обеспечение параллельных портов не предоставляет доступ к регистру управления.

Биты управляющих линий определены в include/linux/parport.h:
  • PARPORT_CONTROL_STROBE
  • PARPORT_CONTROL_AUTOFD
  • PARPORT_CONTROL_SELECT
  • PARPORT_CONTROL_INIT

PPFCONTROL



Переключить управляющие линии. Поскольку часто бывает нужно изменить один из управляющих сигналов, не трогая остальные, было бы не очень эффективно для драйвера в пространстве пользователя сначала воспользоваться PPRCONTROL, внести изменения и затем воспользоваться PPWCONTROL. Конечно, каждому драйверу придётся запоминать состояние управляющих линий (они не могут быть изменены из другого места), но для предоставления PPRCONTROL самому ppdev тоже приходится запоминать состояние управляющих линий.

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.

PPRSTATUS



Возвращает unsigned char, содержащий биты для каждой из активных линий состояния (например, PARPORT_STATUS_BUSY). Параметр ioctl должен быть указателем на unsigned char.

PPDATADIR



Управление направлением линий данных. Обычно параллельный порт компьютера управляет линиями данных, но в байтовом режиме передачи от периферийного устройства на компьютер нужно отключить это управление и позволить периферийному устройству управлять этими сигналами. (Если управление параллельным портом со стороны компьютера останется активным, то порт может повредиться.)

Этот вызов бывает нужен только в сочетании с PPWDATA или PPRDATA.

Параметр ioctl - это указатель на int. Если int - ноль, управление линиями данных включается (прямое направление). Если же int - не ноль, то управление линиями данных отключается (обратное направление).

PPWDATA



Задаёт линии данных (если они находится в режиме прямого направления). Параметр ioctl - указатель на unsigned char.

PPRDATA



Читает линии данных (если они находится в режиме обратного направления). Параметр ioctl - указатель на unsigned char.

PPCLRIRQ



Очищает счётчик прерываний. Драйвер ppdev хранит счётчик произошедших прерываний. PPCLRIRQ сохранит этот счётчик в int, указатель на который передан в качестве параметра ioctl.

После этого счётчик прерываний сбрасывается в ноль.

PPWCTLONIRQ



Задаёт признак ответа. После того, как произойдёт прерывание, обработчик должен выставить линии управления в соответствии с запросом. Параметр ioctl - указатель на unsigned char, который интерпретируется так же, как в PPWCONTROL.

Этот 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);
    }

Комментариев нет: