Страница с сайта Владислава Пирогова Ассемблер и не только.


Работа с параллельным портом в Windows NT
Александр Тарасенко, Санкт-Петербург


Данная статья посвящена вопросам использования параллельных коммуникационных портов в приложениях Windows NT. В статье рассматриваются способы управления параллельными портами используя стандартные системные драйверы. Не рассматриваются вопросы аппаратного обеспечения и протоколы передачи. Предполагается, что с этими вопросами читатель знаком

Содержание:
1. Стандартные системные драйвера для работы с параллельными портами
2. Работа с параллельным портом через драйвер parclass.sys
2.1 Открытие и закрытие порта
2.2 Запись данных и чтение
2.3 Как получить информацию о состоянии устройства
2.4 Как узнать режим, в котором находится порт?
2.5 Как установить режим порта?
2.6 Как установить адрес для чтения и записи в ECP или EPP режиме?
3 Примеры
3.1 Работа с принтером в текстовом режиме

1. Стандартные системные драйвера для работы с параллельными портами.
В системе существует два стандартных драйвера, обеспечивающих работу параллельных портов и устройств, подключенных к этим портам – parclass.sys и parport.sys. На самом деле, в системе могут быть и другие драйвера, обеспечивающие работу аппаратуры, реализующей параллельные порты, но их работа прозрачна для остальных компонентов системы. Драйвер parclass.sys используется драйверами устройств более высокого уровня, например драйвером сканера, или приложениями для прямого доступа к параллельному порту (row access).
Рассмотрим функции драйверов parclass.sys и parport.sys. Parport.sys предназначен для управления самим параллельным портом, т.е железом. Может показаться, что этот драйвер должен обеспечивать доступ через порты ввода/вывода к регистрам параллельного порта – данных, статуса и управляющему и предоставлять этот сервис вам. Как бы не так! В системе Windows NT все не так просто и прямолинейно. Этот драйвер выполняет совсем другие функции – он поддерживает PnP, регулирует доступ к портам, их захват и освобождение, контролирует режим работы каждого из присутствующих портов.
Parclass.sys – это базовый драйвер устройства, подключенного к параллельному порту. Почувствуйте разницу: parport.sys – драйвер самого порта, parclass.sys – драйвер устройства, подключенного к порту. Parclass.sys обеспечивает низкоуровневый доступ (row access) к устройству, подключенному к параллельному порту. Хотя при этом не стоит думать опять же, что этот драйвер позволит напрямую работать с регистрами параллельного порта. Когда вы из приложения вызывает функцию CreateFile("LPT1", … ) – вы работаете с объектом ядра – именованным устройством, созданным именно драйвером parclass.sys. Короче говоря, если вы собираетесь поуправлять неким устройством через параллельный порт из приложения, вы будете взаимодействовать именно с parclass.sys. Изучением интерфейса этого драйвера мы и займемся в дальнейшем. Если вас все же не оставляет мысль напрямую работать с регистрами параллельного порта – вам придется разработать собственный драйвер, другой дороги у вас нет. Но прежде чем заняться этим – подумайте! Я вас уверяю, что в 99% случаев такой необходимости нет.
Отметим, две особенности:
1)Обычно для устройств, подключенных к параллельному порту, разрабатывается специфичный драйвер, относящий это устройство к определенному классу устройств, например к классу принтеров.
2)Сам порт для приложения выглядит как устройство с именем LPTn. Вы можете спросить: "а где же устройство?". Считайте в данном случае устройством LPT разъем. Все что находится за ним, для системы будет являться черным ящиком – подключайте к нему что хотите (например, микроконтроллер). Этой ситуацией мы и будем в основном интересоваться.
2. Работа с параллельным портом через драйвер parclass.sys
2.1 Открытие и закрытие порта.
Тут все просто, открываем доступ к устройству. Это осуществляется посредством функции CreateFile c параметром LPTn, где LPTn существующее имя для объекта устройства:
HANDLE hLpt = CreateFile( "LPT1", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING,0, NULL );
При удачном завершении операции будет возвращен файловый дескриптор, через который мы и будем управлять параллельным портом. Остается только добавить, что после использования следует освободить порт посредством вызова CloseHandle(hLpt).
2.2 Запись данных и чтение
Запись и чтение происходит как при обращении к обычному файлу. Для этого используются функции WriteFile и ReadFile.
2.3 Как получить информацию о состоянии устройства?
Для этого нам нужно познакомится с функцией, которая не применяется при работе с файлами – DeviceIoContol:

BOOL DeviceIoControl(
HANDLE hDevice, //Дескриптор устройства
DWORD dwIoControlCode, //Управляющий код, номер операции
LPVOID lpInBuffer, //Указатель на буфер с данными для передачи
DWORD nInBufferSize, //Размер буфера
LPVOID lpOutBuffer, //Указатель на буфер с полученными от устройства данными
DWORD nOutBufferSize, //Размер буфера
LPDWORD lpBytesReturned, //Размер возвращенного данных
LPOVERLAPPED lpOverlapped //Указатель на структуру для асинхронных операций
);

Управляющие коды для работы с драйвером parclass.sys объявлены в заголовочном файле . Этот файл входит в состав DDK. Если у Вас не инсталлирован DDK, то управляющие коды и структуры придется задать вручную. Для получения/установки информации о статусе устройства служат следующие коды:

#define IOCTL_PAR_QUERY_INFORMATION CTL_CODE( FILE_DEVICE_PARALLEL_PORT, 1, METHOD_BUFFERED, FILE_ANY_ACCESS )
#define IOCTL_PAR_SET_INFORMATION CTL_CODE (FILE_DEVICE_PARALLEL_PORT, 2, METHOD_BUFFERED, FILE_ANY_ACCESS)
* Не забудьте подключить заголовочный файл !!!
Управляющие структуры:
typedef struct _PAR_QUERY_INFORMATION{
UCHAR Status;
} PAR_QUERY_INFORMATION, *PPAR_QUERY_INFORMATION; typedef struct _PAR_SET_INFORMATION{
UCHAR Init;
} PAR_SET_INFORMATION, *PPAR_SET_INFORMATION;
* В данном случае можно и не определять дополнительные типа данных, поскольку они являются псевдонимами для беззнакового однобайтового целого.
Назначение бит в байте (как это определено в )

#define PARALLEL_INIT 0x01
#define PARALLEL_AUTOFEED 0x02
#define PARALLEL_PAPER_EMPTY 0x04
#define PARALLEL_OFF_LINE 0x08
#define PARALLEL_POWER_OFF 0x10
#define PARALLEL_NOT_CONNECTED 0x20
#define PARALLEL_BUSY 0x40
#define PARALLEL_SELECTED 0x80

Как можно видеть, тут намешаны биты как из регистра состояния, так и из управляющего регистра. Поскольку мы договаривались не рассматривать различные режимы работы и протоколы обмена, будем считать, что назначения этих битовых полей понятно. Отмечу лишь, что некоторые биты доступны только для чтения (вернее сказать, попытка установить их будет проигнорирована).
А вот как это делается:

PAR_QUERY_INFORMATION ParInfo; DWORD ret;
DeviceIoControl(hLpt, IOCTL_PAR_QUERY_INFORMATION, NULL, 0, &ParInfo, sizeof(ParInfo), &ret, NULL);


Обратная операция (PAR_SET_INFORMATION) имеет смысл только при подаче сигнала сброса, т.е бит PARALLEL_INIT обязательно должен быть установлен.

2.4 Как узнать режим, в котором находится порт?
Для этого служит команда IOCTL_IEEE1284_GET_MODE. Она определена следующим образом:
#define IOCTL_IEEE1284_GET_MODE CTL_CODE (FILE_DEVICE_PARALLEL_PORT, 5, METHOD_BUFFERED, FILE_ANY_ACCESS)
При вызове функции DeviceIoControl информация будет возвращена в структуре, определенной следующем образом:

typedef struct _PARCLASS_NEGOTIATION_MASK {
USHORT usReadMask;
USHORT usWriteMask;
} PARCLASS_NEGOTIATION_MASK, *PPARCLASS_NEGOTIATION_MASK;


Одно поле – установленный протокол для чтения, другое – протокол записи. Определены следующие протоколы (опять же в ntddpar.h, так что имеет смысл использовать этот заголовок J ) :

#define NONE 0x0000
#define CENTRONICS 0x0001 /* Write Only */
#define IEEE_COMPATIBILITY 0x0002 /* Write Only */
#define NIBBLE 0x0004 /* Read Only */
#define CHANNEL_NIBBLE 0x0008 /* Read Only */
#define BYTE_BIDIR 0x0010 /* Read Only */
#define EPP_HW 0x0020
#define EPP_SW 0x0040
#define EPP_ANY 0x0060
#define BOUNDED_ECP 0x0080
#define ECP_HW_NOIRQ 0x0100 /* HWECP PIO */
#define ECP_HW_IRQ 0x0200 /* HWECP with IRQ */
#define ECP_SW 0x0400
#define ECP_ANY 0x0780


Теперь узнаем режим, в котором находится порт по умолчанию:

DWORD ret;
DeviceIoControl(hLpt, IOCTL_IEEE1284_GET_MODE, NULL, 0, &Mode, sizeof(Mode), &ret, NULL );


Получим, что по умолчанию порт находится в следующем режиме: для чтения NIBBLE (т.е полубайтный режим ввода), для записи – в режиме CENTRONICS (опять же, надеюсь читатель помнит, что это за режим).
Если вы во время работы изменили режим работы порта, вам, вероятно, будет интересно, какой же режим был установлен изначально (хотя бы чтобы по окончанию работы вернуть порт в исходное состояние). Для этого служит команда IOCTL_PAR_GET_DEFAULT_MODES, определенная как:
#define IOCTL_PAR_GET_DEFAULT_MODES CTL_CODE( FILE_DEVICE_PARALLEL_PORT, 10, METHOD_BUFFERED, FILE_ANY_ACCESS)
Параметры этой команды аналогичны команде IOCTL_IEEE1284_GET_MODE.

2.5 Как установить режим порта?
Для этого опять же есть специальная команда: IOCTL_IEEE1284_NEGOTIATE.
#define IOCTL_IEEE1284_NEGOTIATE CTL_CODE( FILE_DEVICE_PARALLEL_PORT, 6, METHOD_BUFFERED, FILE_ANY_ACCESS).
Для использования этой команды нужно задать как входной параметр, так и выходной буфера. Эти буфера должны иметь размер не менее размера структуры типа PARCLASS_NEGOTIATION_MASK (поскольку данные упакованы именно в эту структуру). Отметим, что как следует из названия команды, порт попробует "договориться" с устройством об используемом протоколе. Если устройство не сможет поддержать инициативу драйвера, запрошенный протокол установлен не будет и в поле структуры PARCLASS_NEGOTIATION_MASK будет возвращен 0 (NONE).
2.6 Как установить адрес для чтения и записи в ECP или EPP режиме?
Следуя нашему уговору, не обсуждаем, что такое адрес, а сразу берем быка за рога:
#define IOCTL_PAR_SET_WRITE_ADDRESS CTL_CODE( FILE_DEVICE_PARALLEL_PORT, 7, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_PAR_SET_READ_ADDRESS CTL_CODE( FILE_DEVICE_PARALLEL_PORT, 8, METHOD_BUFFERED, FILE_ANY_ACCESS)
Адрес имеет размер 1 байт и передается параметр lpInBuffer функции DeviceIoControl. Например, так:

UCHAR addr = 0x10;
BOOL res = DeviceIoControl(hLpt, IOCTL_PAR_SET_READ_ADDRESS, &addr, sizeof(addr), NULL, 0, &ret, NULL );

С помощью управляющих кодов, объявленных следующим образом,:
#define IOCTL_PAR_GET_READ_ADDRESS CTL_CODE( FILE_DEVICE_PARALLEL_PORT, 14, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_PAR_GET_WRITE_ADDRESS CTL_CODE( FILE_DEVICE_PARALLEL_PORT, 15, METHOD_BUFFERED, FILE_ANY_ACCESS)
можно выяснить установленные адреса для чтения и для записи соответственно. Я думаю нет нужды говорить, что адрес будет возвращен через параметр lpOutBuffer функции DeviceIoControl.


3 Примеры

3.1 Работа с принтером в текстовом режиме
В данном примере мы рассмотрим, как работать с принтером в текстовом режиме. На самом деле, ваш любимый принтер может этого и не захотеть (большинство современных принтеров работают в графическом режиме). Но передо мной в данный момент стоит старое доброе печатающее устройство: EPSON LQ-100. Он работает по умолчанию в текстовом режиме, воспринимая ASCII коды, и поддерживает протокол Centronix. Так что с ним нам будет просто:

HANDLE hLpt = CreateFile( "LPT1", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL );
char buf[0x100]; //помните, выделять массивы на стеке не очень здорово!!!
sprintf(buf, "hello printer\ngood bye printer");
BOOL res = WriteFile(hLpt, &buf, strlen(buf), &ret, NULL); CloseHandle(hLpt);


На листочке мы увидим (если не забудем его подсунуть жадному устройству):

hello printer
good bye printer


3.2 Возможно, будет продолжение :)
По крайней мере, хотелось еще рассмотреть реализацию SPI протокола для обмена с микроконтроллерами.