У практиці любителів радіо, вимірювальної та робототехніки найбільшою популярно стю користується паралельний порт персонального комп’ютера. Багато пристроїв як з будівлею, так і розробляються, управляються від паралельного порту. Вивчивши основи раз работки драйверів, ми може без особливих зусиль створити нескладний драйвер для пристрою, приєднаного до паралельного порту ПК. Сам по собі, без додаткової електроніки, паралельний порт може служити непоганим генератором звукових коливань, в якості аналізатора вхідних цифрових сигналів або використовуватися для функціонування більш складних схем, наприклад, аналого цифрових перетворювачів. Розглянемо два апаратно програмних проекту, що працюють з паралельним портом принтера, для яких створимо найпростіші драйвери.

Перше додаток являє собою генератор прямокутних імпульсів частотою

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 с.: Іл.