Застосування драйвера паралельного порту ПК
У практиці любителів радіо, вимірювальної та робототехніки найбільшою популярно стю користується паралельний порт персонального комп’ютера. Багато пристроїв як з будівлею, так і розробляються, управляються від паралельного порту. Вивчивши основи раз работки драйверів, ми може без особливих зусиль створити нескладний драйвер для пристрою, приєднаного до паралельного порту ПК. Сам по собі, без додаткової електроніки, паралельний порт може служити непоганим генератором звукових коливань, в якості аналізатора вхідних цифрових сигналів або використовуватися для функціонування більш складних схем, наприклад, аналого цифрових перетворювачів. Розглянемо два апаратно програмних проекту, що працюють з паралельним портом принтера, для яких створимо найпростіші драйвери.
Перше додаток являє собою генератор прямокутних імпульсів частотою
500Гц. На основі цього прикладу читачі легко можуть створити широкодіапазонний генера тор від кількох до 500 Гц. Прямокутні імпульси виробляються на виході регістра даних (адреса 0x378) паралельного порту, тому апаратної частини в сенсі додаткового тільних мікросхем тут немає. Хоча, якщо ви плануєте підключати виходи генератора до значного навантаження, то на виходах порту встановіть додаткові буферні підсилите чи формувачі, здатні забезпечити необхідний струм в навантаженні.
Для реалізації програмної частини нам потрібно створити драйвер паралельного
порту і додаток користувача, що працює з цим драйвером. Почнемо з драйвера. Са ний простий метод генерації імпульсів на якому або виведення порту – прочитати значення засувки порту, інвертувати його і переслати назад в паралельний порт. Таким обра зом, можна отримати один прямокутний імпульс. Для генерації безперервної последова тельности імпульсів з певною частотою необхідно періодично повторювати опера цію «зчитування інверсія запис» в регістрі 0x378. Період повторення імпульсів можна задавати або в самому драйвері пристрою або в додатку користувача. Ми будемо
використовувати другий спосіб, задаючи інтервал часу в додатку користувача – цей метод незрівнянно простіше, ніж той, який можна було б реалізувати в самому драйвері.
Спочатку розробимо драйвер (назвемо його lptgen.sys), який буде працювати з «Вірт альних» пристроєм lptgen, в якості якого фактично виступатиме порт виводу паралельного порту ПК. Наш драйвер буде обробляти IOCTL команду IOCTL_WRITE в пакеті запиту IRP_MJ_DEVICE_CONTROL. Ось вихідний текст драйвера:
#include <ntddk.h>
#define IOCTL_WRITE CTL_CODE(FILE_DEVICE_UNKNOWN,\
0x801,\ METHOD_BUFFERED,\ FILE_ANY_ACCESS)
#define NT_DEVICE_NAME L"\\Device\\lptgen"
#define DOS_DEVICE_NAME L"\\DosDevices\\lptgen"
#define DATA 0x378
VOID
lptgen_Unload (IN PDRIVER_OBJECT DriverObject)
{
UNICODE_STRING DosDeviceName;
RtlInitUnicodeString(&DosDeviceName, DOS_DEVICE_NAME); IoDeleteSymbolicLink(&DosDeviceName); IoDeleteDevice(DriverObject->DeviceObject);
}
NTSTATUS
lptgen_Create(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS;
}
NTSTATUS
lptgen_Close(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS;
}
NTSTATUS
lptgen_Ioctl(IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp)
{
PIO_STACK_LOCATION pIoStack = IoGetCurrentIrpStackLocation(Irp); ULONG ctlCode = pIoStack->Parameters.DeviceIoControl.IoControlCode;
if (ctlCode == IOCTL_WRITE)
{
asm {
mov DX, DATA in AL, DX not AL
out DX, AL
}
}
Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS;
}
NTSTATUS
DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
PDEVICE_OBJECT DeviceObject; UNICODE_STRING NtDeviceName; UNICODE_STRING DosDeviceName; NTSTATUS status;
RtlInitUnicodeString(&NtDeviceName, NT_DEVICE_NAME);
status = IoCreateDevice(DriverObject, sizeof(MY_DEVICE_EXTENSION),
&NtDeviceName, FILE_DEVICE_UNKNOWN,
0, FALSE,
&DeviceObject);
if (!NT_SUCCESS(status))
{
IoDeleteDevice(DeviceObject);
return status;
}
RtlInitUnicodeString(&DosDeviceName, DOS_DEVICE_NAME);
status = IoCreateSymbolicLink(&DosDeviceName, &NtDeviceName);
if (!NT_SUCCESS(status))
{
IoDeleteSymbolicLink(&DosDeviceName); IoDeleteDevice(DeviceObject);
return status;
}
DeviceObject->Flags &= ~DO_DEVICE_INITIALIZING; DeviceObject->Flags |= DO_BUFFERED_IO;
DriverObject->MajorFunction[IRP_MJ_CREATE] = lptgen_Create; DriverObject->MajorFunction[IRP_MJ_CLOSE] = lptgen_Close; DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = lptgen_Ioctl; DriverObject->DriverUnload = lptgen_Unload;
return status;
}
Багато в чому вихідний текст драйвера нам знаком, тому я зупинюся на реалізації функції lptgen_Ioctl. Тут ми використовуємо блок асемблерних команд. Спочатку вміст регістра 0x378 поміщається в регістр AL командою in, де воно інвертується, після чого виводи диться назад в регістр по команді out. Тут ми припускаємо, що в якості базового реги стра паралельного порту використовується регістр з адресою 0x378. Якщо на вашому комп’ютері базовий адресу іншої, наприклад, 0x278, то необхідно перевизначити константу DATA.
Таким чином, кожен раз при виконанні функції DeviceIoControl () з програми
користувача з кодом команди IOCTL_WRITE в драйвері буде виконуватися інверсія вихо
дов регістра 0x378.
Перейдемо тепер до розробки програми користувача. Тут, як і в попередніх програмах, ми будемо використовувати динамічну завантаження вивантаження драйвера, а для опе рацій з пристроєм використовувати функцію WINAPI DeviceIoControl (), якою будемо переда вати код команди IOCTL_WRITE. Ось вихідний текст користувальницької програми:
#include <windows.h>
#include <stdio.h>
#include <conio.h>
#define IOCTL_WRITE CTL_CODE(FILE_DEVICE_UNKNOWN,\
0x801,\ METHOD_BUFFERED,\ FILE_ANY_ACCESS)
SC_HANDLE scm, svc; SERVICE_STATUS ServiceStatus;
void main(void)
{
HANDLE fh; DWORD bytes;
scm = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (!scm)
{
printf("Cannot open SCM!\n");
return;
}
svc = CreateService(scm, "lptgen", "lptgen",
SERVICE_ALL_ACCESS,
SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, "i:\\lptgen.sys",
NULL, NULL, NULL, NULL, NULL);
if (!svc)
{
printf("Cannot create service!\n"); CloseHandle(scm);
return;
}
StartService(svc, 0, NULL); CloseServiceHandle(svc); CloseServiceHandle(scm);
fh = CreateFile("\\\\.\\lptgen",
GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING,
0,
NULL);
if (fh != INVALID_HANDLE_VALUE)
{
printf("Type any key to exit\n");
while (! _kbhit())
{
DeviceIoControl(fh, IOCTL_WRITE, NULL,
0, NULL,
0,
&bytes,
NULL);
Sleep(1);
};
}
} CloseHandle(fh);
scm = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (!scm)
{
printf("Cannot open SCM!\n");
return;
}
svc = OpenService(scm, "lptgen", SERVICE_ALL_ACCESS); ControlService(svc, SERVICE_CONTROL_STOP, &ServiceStatus); DeleteService(svc);
CloseServiceHandle(svc); CloseServiceHandle(scm);
}
Тут основна робота виконується в циклі while (! _kbhit ()). Цей цикл виконується до тих пір (як і генерація сигналу на виході паралельного порту), поки користувач не на тисне яку клавішу. Функція Sleep (1) виконує затримку на 1 мілісекунду, а оскільки для генерації повного періоду коливань потрібно два цикли, то в результаті отримаємо пе ріод коливань 2 мс, що відповідає частоті 500Гц. Для отримання частоти, наприклад, в 100Гц аргумент функції Sleep () повинен мати значення 5 і т. д.
Наш другий проект набагато складніше. Ми розробимо програмну частину аналого циф рового перетворювача на мікросхемі LTC1286, який управляється з паралельного порту ПК. Цей проект, але з використанням драйвера PortTalk, ми аналізували в розділі 3. Нагадаю, як виглядає апаратна частина проекту (рис. 7.13):
Рис. 7.13
Схема апаратної частини АЦП
Спочатку розробимо драйвер паралельного порту (назвемо його adc1286.sys), який буде збирати дані з АЦП за запитом IRP_MJ_DEVICE_CONTROL і передавати їх додатку нию користувача. Оригінальний текст драйвера наведено далі:
#include <ntddk.h>
#define IOCTL_READ CTL_CODE(FILE_DEVICE_UNKNOWN,\
0x801,\ METHOD_BUFFERED,\ FILE_ANY_ACCESS)
#define NT_DEVICE_NAME L"\\Device\\adc1286"
#define DOS_DEVICE_NAME L"\\DosDevices\\adc1286"
typedef struct _MY_DEVICE_EXTENSION
{
ULONG L1;
} MY_DEVICE_EXTENSION, *PMY_DEVICE_EXTENSION;
#define DATA 0x378
#define STATUS 0x379
VOID
adc_Unload (IN PDRIVER_OBJECT DriverObject)
{
UNICODE_STRING DosDeviceName;
RtlInitUnicodeString(&DosDeviceName, DOS_DEVICE_NAME); IoDeleteSymbolicLink(&DosDeviceName); IoDeleteDevice(DriverObject->DeviceObject);
}
NTSTATUS
adc_Create(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS;
}
NTSTATUS
adc_Close(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS;
ОСНОВИ РОЗРОБКИ драйвери пристроїв в WINDOWS
}
NTSTATUS
adc_IoctlR(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
PMY_DEVICE_EXTENSION dx = (PMY_DEVICE_EXTENSION)DeviceObject-
>DeviceExtension;
PIO_STACK_LOCATION pIoStack = IoGetCurrentIrpStackLocation(Irp);
ULONG ctlCode = pIoStack->Parameters.DeviceIoControl.IoControlCode; ULONG OutputLength = pIoStack-
>Parameters.DeviceIoControl.OutputBufferLength;
PUCHAR buf = (PUCHAR)Irp->AssociatedIrp.SystemBuffer; PULONG Lbuf = &dx->L1;
dx->L1 = 0;
OutputLength = 4;
if (ctlCode == IOCTL_READ)
{
asm {
push EBX
mov DX, DATA xor AX, AX bts AX, 7
out DX, AL
btr AX, 7 out DX, AL
mov BX, 15 next:
xor AX, AX
mov DX, DATA
btr AX, 6 out DX, AL
mov DX, STATUS
in AL, DX bt AX, 3 rcl CX, 1
mov DX, DATA
bts AX, 6 out DX, AL
dec BX
jnz next
mov DX, DATA
bts AX, 7 out DX, AL
pop EBX
and CX, 0x0FFF
mov EDX, DWORD PTR Lbuf mov WORD PTR [EDX], CX
}
RtlMoveMemory(buf,
(PUCHAR)&dx->L1, OutputLength);
}
Irp->IoStatus.Information = OutputLength; Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS;
}
NTSTATUS
DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
PDEVICE_OBJECT DeviceObject; UNICODE_STRING NtDeviceName; UNICODE_STRING DosDeviceName; NTSTATUS status;
RtlInitUnicodeString(&NtDeviceName, NT_DEVICE_NAME);
status = IoCreateDevice(DriverObject, sizeof(MY_DEVICE_EXTENSION),
&NtDeviceName, FILE_DEVICE_UNKNOWN,
0, FALSE,
&DeviceObject);
if (!NT_SUCCESS(status))
{
IoDeleteDevice(DeviceObject);
return status;
}
RtlInitUnicodeString(&DosDeviceName, DOS_DEVICE_NAME);
status = IoCreateSymbolicLink(&DosDeviceName, &NtDeviceName);
if (!NT_SUCCESS(status))
{
ОСНОВИ РОЗРОБКИ драйвери пристроїв в WINDOWS
IoDeleteSymbolicLink(&DosDeviceName); IoDeleteDevice(DeviceObject);
return status;
}
DeviceObject->Flags &= ~DO_DEVICE_INITIALIZING; DeviceObject->Flags |= DO_BUFFERED_IO;
DriverObject->MajorFunction[IRP_MJ_CREATE] = adc_Create; DriverObject->MajorFunction[IRP_MJ_CLOSE] = adc_Close; DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = adc_IoctlR; DriverObject->DriverUnload = adc_Unload;
return status;
}
У цьому драйвері основна робота виконується функцією adc_IoctlR, яка обрабат кість пакет запиту IRP_MJ_DEVICE_CONTROL з кодом IOCTL команди IOCTL_READ. Оскільки драйвер повинен повертати дані додатком, то ми визначили беззнакову целочіс ленну 32 бітову змінну L1 в області розширення. Тут потрібно зробити одне дуже важливе зауваження: драйвер ядра, як правило, обробляє цілочисельні дані, хоча може працювати і з функціями математичного співпроцесора. В даному випадку ми будемо передавати в програму користувача 32 бітовий результат аналого цифрового перетворення вання, а остаточну обробку результату буде виконувати програму користувача. В кінцевому підсумку на екран дисплея буде виведено значення результату перетворення в форматі дійсного числа з плаваючою крапкою.
Другий важливий момент. При роботі з АЦП LTC1286 ми використовуємо асемблерні код. Для такого класу додатків реального часу розмір і ефективність програмного коду мають першорядне значення, хоча в інших випадках цілком можливо використання ня функцій _inp () і _outp мови C + +.
Зверніть увагу, як з ассемблерного коду ми отримуємо доступ до змінної L1 –
багато помилок в роботі драйвером при використанні ассемблерного коду пов’язані саме з неправильною адресацією зовнішніх по відношенню до ассемблерних блоків команд. Хоч це і необов’язково, але бажано зберігати вміст регістрів в стеці командою push перед початком обробки даних і виштовхувати їх звідти командою pop після закінчення роботи. Це особливо важливо при розробці драйверів, в яких працює кілька про програмних потоків. В даному випадку, ми зберігаємо тільки вміст регістра EBX.
Тепер перейдемо до додатка користувача, яке отримує дані від нашого
пристрої adc1286. Оригінальний текст програми показаний далі:
#include <windows.h>
#include <stdio.h>
#define IOCTL_READ CTL_CODE(FILE_DEVICE_UNKNOWN,\
0x801,\ METHOD_BUFFERED,\ FILE_ANY_ACCESS)
SC_HANDLE scm, svc; SERVICE_STATUS ServiceStatus;
void main(void)
{
HANDLE fh; int binRes; float total;
DWORD bytes;
scm = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (!scm)
{
printf("Cannot open SCM!\n");
return;
}
svc = CreateService(scm, "adc1286", "adc1286", SERVICE_ALL_ACCESS,
SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, "i:\\adc1286.sys", NULL,
NULL,
NULL, NULL, NULL);
if (!svc)
{
printf("Cannot create service!\n"); CloseHandle(scm);
return;
}
StartService(svc, 0, NULL); CloseServiceHandle(svc); CloseServiceHandle(scm);
fh = CreateFile("\\\\.\\adc1286", GENERIC_READ | GENERIC_WRITE,
0,
NULL, OPEN_EXISTING,
0, NULL);
if (fh != INVALID_HANDLE_VALUE)
{
DeviceIoControl(fh, IOCTL_READ, NULL,
0,
&binRes, sizeof(binRes),
&bytes, NULL);
total = 5.0 / 4096.0 * binRes;
printf("ADC LTC1286 result: %5.3f V\n", total);
}
CloseHandle(fh);
scm = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (!scm)
{
printf("Cannot open SCM!\n");
return;
}
svc = OpenService(scm, "adc1286", SERVICE_ALL_ACCESS); ControlService(svc, SERVICE_CONTROL_STOP, &ServiceStatus); DeleteService(svc);
CloseServiceHandle(svc); CloseServiceHandle(scm);
}
У цьому додатку, як і в попередніх, використовується WINAPI функція DeviceIoControl (), яка посилає пристрою команду IOCTL_READ. Драйвер пристрою adc1286 повертає
32 бітовий двійковий результат, який потрібно перетворити в дійсне число з пла
вающей крапкою. З цієї причини в додатку оголошені дві змінні:
int binRes;
float total;
Мінлива binRes міститиме двійковий результат перетворення, а змінна total – остаточне дійсне значення аналого цифрового перетворення. Для
12 бітового АЦП з напругою зсуву, що дорівнює 5В, значення результату в форматі пла
вающей точки буде обчислюватися наступним чином:
total = 5.0 / 4096.0 * binRes;
Наше додаток отримує одне значення перетворення, але можна доопрацювати його, організувавши цикл з певним інтервалом сканування за методикою, схожою з тією, що була використана в попередньому проекті.
Для подальшого вдосконалення ваших навичок з розробки драйверів Windows всім бажаючим можу порадити звернутися на сайт www.microsoft.com, на якому, помі мо стандартної документації, є численні статті з описом практичних аспектів створення та налагодження драйверів.
Джерело: Магда Ю. С. Комп’ютер в домашній лабораторії. – М.: ДМК Пресс, 2008. – 200 с.: Іл.
Ваш відгук