Основы разработки прикладных виртуальных драйверов
Часть 10. Синхронизация обработчиков прерываний в 32-разрядных приложениях Windows
Методика, описанная в предыдущей статье (см. КомпьютерПресс № 11’2001), далека от совершенства. Она годится лишь для таких систем реального времени, в которых все действия по обработке прерываний могут быть целиком возложены на обработчик прерываний. Например, получив сигнал об изменении состояния установки, обработчик прерываний путем посылки соответствующих команд в порты установки переключает ее управляющие элементы. Основная программа в этом процессе может не участвовать и даже не знать о поступлении сигнала прерывания.
Чаще сигнал прерывания свидетельствует о наступлении такого события, о котором должна быть извещена основная программа. Рассмотрим дальнейшее видоизменение программного комплекса по управлению таймером-счетчиком, в который теперь введена асинхронная обработка прерываний не только в виртуальном драйвере, но и в самой программе. Учитывая относительную сложность этой методики, ниже мы приводим все исходные тексты комплекса (здесь он носит имя INTR32-3), но фактически в них, по сравнению с предыдущими примерами, лишь внесены некоторые дополнения.
Тексты исходных файлов, входящих в приложение Windows
Заголовочный файл INTR32-3.H
//Определения констант #define MI_START 100 #define MI_ADDR 101 #define MI_EXIT 102 #define MI_DATA 103 #define DIOC_INIT 1 #define DIOC_ADDR 2 #define DIOC_EXIT 3 //Прототипы функций void Register(HINSTANCE); void Create(HINSTANCE); LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM); BOOL OnCreate(HWND,LPCREATESTRUCT); void OnCommand(HWND,int,HWND,UINT); void OnDestroy(HWND); void InitCard(); void GetAddr(); void ReadData(); DWORD WINAPI Isr(LPVOID); Файл ресурсов INTR32-3.RC #include "intr32-3.h" Main MENU{ POPUP "Режимы" { MENUITEM "Пуск",MI_START MENUITEM "Адрес драйвера",MI_ADDR MENUITEM "Чтение данных",MI_DATA MENUITEM SEPARATOR MENUITEM "Выход",MI_EXIT } }
Программный файл INTR32-3.CPP
#define STRICT #include <windows.h> #include <windowsx.h> #include "intr32-3.h" char szClassName[]="MainWin"; char szTitle[]="Аппаратные прерывания - 3"; HANDLE hVMyD,hEvent,hThread; unsigned short int Data=0x1234; OVERLAPPED ovlp; DWORD dwThreadID,cbRet; HINSTANCE hInst; int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE,LPSTR,int){ MSG msg; Register(hInstance); hInst=hInstance; Create(hInstance); while(GetMessage(&msg,NULL,0,0)) DispatchMessage(&msg); return 0; } void Register(HINSTANCE hInst){ WNDCLASS wc; memset(&wc,0,sizeof(wc)); wc.lpszClassName=szClassName; wc.hInstance=hInst; wc.lpfnWndProc=WndProc; wc.lpszMenuName="Main"; wc.hCursor=LoadCursor(NULL,IDC_ARROW); wc.hIcon=LoadIcon(NULL,IDI_APPLICATION); wc.hbrBackground=GetStockBrush(WHITE_BRUSH); RegisterClass(&wc); } void Create(HINSTANCE hInst){ HWND hwnd=CreateWindow(szClassName,szTitle,WS_OVERLAPPEDWINDOW, 10,10,250,140,HWND_DESKTOP,NULL,hInst,NULL); ShowWindow(hwnd,SW_SHOWNORMAL); } LRESULT CALLBACK WndProc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam){ switch(msg) { HANDLE_MSG(hwnd,WM_CREATE,OnCreate); HANDLE_MSG(hwnd,WM_COMMAND,OnCommand); HANDLE_MSG(hwnd,WM_DESTROY,OnDestroy); default: return(DefWindowProc(hwnd,msg,wParam,lParam)); } } BOOL OnCreate(HWND,LPCREATESTRUCT){ hVMyD=CreateFile("\\\\.\\VMYD",0,0,NULL,0, FILE_FLAG_DELETE_ON_CLOSE|FILE_FLAG_OVERLAPPED,NULL); return TRUE; } void OnCommand(HWND hwnd,int id,HWND,UINT){ switch(id) { case MI_START: InitCard(); break; case MI_DATA: ReadData(); break; case MI_ADDR: GetAddr(); break; case MI_EXIT: DestroyWindow(hwnd); } } void OnDestroy(HWND){ DeviceIoControl (hVMyD,DIOC_EXIT,NULL,0,NULL,0,&cbRet,0); PostQuitMessage(0); } void InitCard(){ struct { short unsigned int C0; short unsigned int C1; short unsigned int C2; unsigned short int* dptr; }InBuf; InBuf.C0=20; //Канал 0 InBuf.C1=50000; //Канал 1 InBuf.C2=10000; //Канал 2 InBuf.dptr=&Data; //Адрес переменной Data hEvent=CreateEvent(NULL,FALSE,FALSE,NULL);//Создадим событие ovlp.hEvent=hEvent; hThread=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Isr,NULL,0,&dwThreadID); DeviceIoControl(hVMyD,DIOC_INIT,&InBuf,10,NULL,0,&cbRet,&ovlp); } void ReadData(){ char txt [80]; wsprintf(txt,"Отсчет = %d = %xh",Data,Data); MessageBox(NULL,txt,"Info",MB_ICONINFORMATION); } void GetAddr(){ char txt [80]; int unsigned Addr; DeviceIoControl (hVMyD,DIOC_ADDR,NULL,0,&Addr,4,&cbRet,0); wsprintf(txt,"Базовый адрес = %Xh",Addr); MessageBox(NULL,txt,"Info",MB_ICONINFORMATION); } /*Асинхронная функция, вызываемая драйвером из обработчика аппаратного прерывания*/ DWORD WINAPI Isr(LPVOID){ char txt [80]; GetOverlappedResult(hVMyD,&ovlp,&cbRet,TRUE); wsprintf(txt,"Время истекло!\nData = %d = %xh",Data,Data); MessageBox(NULL,txt,"Асинхронный вызов",MB_ICONINFORMATION); return 0; }
В текст приложения внесено не так уж много изменений, хотя они носят весьма принципиальный характер. В полях данных появились два новых дескриптора типа HANDLE для события hEvent и потока hThread, а также переменная dwThreadID, в которую система вернет идентификатор созданного потока. И событие, и поток понадобятся для включения в программу асинхронной функции (она у нас названа Isr), которая будет активизироваться из обработчика аппаратного прерывания, включенного в состав виртуального драйвера. Соответственно в заголовочном файле INTR32-3.H описан ее прототип:
DWORD WINAPI Isr(LPVOID);
В полях данных приложения объявлена также структурная переменная ovlp типа OVERLAPPED. Она содержит информацию, используемую при выполнении асинхронных операций. Заметим, что слово overlapped («с перекрытием»), с которым мы еще встретимся в других местах программы, в данном контексте обозначает именно асинхронные операции.
Структура OVERLAPPED описана в файле WINBASE.H следующим образом:
typedef struct _OVERLAPPED { DWORD Internal; //Системное состояние DWORD InternalHigh; //Длина передаваемых данных DWORD Offset; //Позиция в файле DWORD OffsetHigh; //Старшее слово позиции в файле HANDLE hEvent; //Дескриптор события, служащего для синхронизации потоков } OVERLAPPED, *LPOVERLAPPED;
На основе представленных комментариев можно сделать заключение, что структура OVERLAPPED предназначена главным образом для асинхронных файловых операций. В нашем случае асинхронность возникает по иной причине, и в этой структуре нам понадобятся только два члена — первый и последний.
Для того чтобы виртуальный драйвер мог выполнять асинхронные операции, при его открытии функцией CreateFile() необходимо указать, кроме флага FILE_FLAG_DELETE_ON_CLOSE, еще и флаг FILE_FLAG_OVERLAPPED:
hVMyD=CreateFile("\\\\.\\VMYD",0,0,NULL,0, FILE_FLAG_DELETE_ON_CLOSE|FILE_FLAG_OVERLAPPED,NULL);
Синхронизация приложения Windows и драйвера (точнее, его обработчика прерываний) осуществляется с помощью двух фундаментальных понятий 32-разрядной среды (будем называть ее для краткости Win32) — потока и события. Экземпляр загруженной в память программы представляет собой процесс по терминологии Win32. Процесс не является активным объектом — ему просто принадлежит 4-гигабайтное адресное пространство, а также другие ресурсы, в частности файлы, с которыми работает программа. Для выполнения программы следует создать в рамках процесса по крайней мере один поток. Первичный поток, который является последовательностью выполнения предложений программы, операционная система всегда создает сама при инициализации процесса. В программе с одним потоком ход выполнения определяется последовательностью ее предложений, а все процессорное время отдается единственному потоку (разумеется, если в системе запущено несколько программ, то процессорное время будет разделяться между ними).
Если необходимо организовать параллельные вычисления, программист может создать в рамках процесса несколько потоков. Тогда, если один из потоков, например, ждет ввода с клавиатуры, процессорное время будет отдано другому потоку, который может выполнять математическую обработку имеющихся данных или другие независимые операции.
Наличие в процессе нескольких потоков требует их взаимной синхронизации. Для этого в Win32 предусмотрен целый ряд синхронизирующих объектов: критические секции, мьютексы, события и др. В наших целях удобно воспользоваться событием.
Событие, как и другие синхронизирующие объекты, может находится в одном из двух состояний: свободном (signaled) и занятом (nonsignaled). Поток же, в свою очередь, можно остановить в ожидании освобождения конкретного события. Если это событие занято, то поток спит и операционная система не выделяет ему процессорного времени. При этом система знает, какое именно событие может разбудить поток. Как только это событие перейдет с свободное состояние (то есть начнет сигнализировать о себе), поток просыпается и начинает свое выполнение.
В приведенном примере создание потока и синхронизирующего события выполняется в функции InitCard(), активизируемой выбором пункта «Пуск» главного меню приложения. Сначала с помощью функции CreateEvent() создается событие:
hEvent=CreateEvent(NULL,FALSE,FALSE,NULL);//Создадим событие
Первый параметр этой функции представляет собой адрес структуры защиты, позволяющей ограничить доступ к объекту. Защита объектов применяется главным образом в многопользовательских системах и поэтому нас интересовать не будет.
Второй параметр позволяет задать тип события: со сбросом вручную (тогда этот параметр должен иметь значение TRUE) или с автосбросом (FALSE). Тип сброса определяет поведение события после освобождения синхронизируемых им потоков: при автосбросе событие автоматически переводится в занятое состояние, а при сбросе вручную для этого необходима функция ResetEvent(). Сброс вручную используется в основном при синхронизации одним событием нескольких потоков. Поскольку в нашем случае синхронизируется один поток, то мы задаем режим автосброса.
Третий параметр является флагом начального состояния события. Создаваемый нами дополнительный поток должен находиться в спящем состоянии до прихода прерывания, поэтому исходное состояние синхронизирующего его события должно быть занятым, чему соответствует значение FALSE.
Наконец, последний параметр определяет имя события, без которого в данном случае можно обойтись.
Функция CreateEvent() в случае успешного выполнения возвращает дескриптор созданного события. Его необходимо заслать в последний элемент структуры OVERLAPPED:
ovlp.hEvent=hEvent;
Создав событие и назначив ему занятое состояние (то есть состояние, которое будет блокировать выполнение связанного с этим событием потока), можно создать сам поток. Фактически потоком будет являться асинхронная процедура Isr(), входящая в состав нашего приложения. Поток создается функцией CreateThread():
hThread=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Isr,NULL,0,&dwThreadID);
Нулевые значения двух первых параметров этой функции определяют использование значений по умолчанию для атрибутов защиты потока и размера его стека. Третий параметр самый важный: он определяет адрес той функции, которая начнет выполняться, когда поток будет активизирован. Этот адрес представляет собой переменную типа LPTHREAD_START_ROUTINE; использование имени типа в скобках перед именем функции преобразует ее адрес в требуемый тип. Четвертый параметр передается создаваемому потоку, который может использовать его в качестве инициализирующего значения. Указание в качестве пятого параметра константы CREATE_SUSPENDED позволяет задержать начало выполнения создаваемого потока до выполнения функции ResumeThread(); нулевое значение этого параметра определяет немедленное исполнение потока (конкретно функции Isr()). В качестве последнего параметра указывается адрес переменной, в которую функция CreateThread() вернет идентификатор нового потока.
Итак, сразу после создания потока начинает выполняться функция Isr(). Нам же надо, чтобы она активизировалась лишь в случае прихода в виртуальный драйвер аппаратного прерывания. Для перевода потока с функцией Isr() в спящее состояние выполняется вызов:
GetOverlappedResult(hVMyD,&ovlp,&cbRet,TRUE);
Если в качестве последнего параметра этой функции указывается значение TRUE, функция приостанавливает выполнение потока в ожидании установки в свободное состояние того события, дескриптор которого находится в структурной переменной типа OVERLAPPED. Адрес этой переменной указывается в качестве второго параметра функции GetOverlappedResult(). Первым параметром функции выступает дескриптор драйвера, участвующего в асинхронной операции, а третьим — адрес счетчика числа передаваемых в асинхронной операции байтов (у нас передача байтов отсутствует). Таким образом, запустив поток, мы тут же остановили его в ожидании установки события.
Вернемся к функции InitCard(). Завершающим действием в этой функции после создания события и потока является обращение к драйверу, чтобы, во-первых, передать ему константы инициализации платы и адрес переменной Data, а во-вторых, активизировать механизм асинхронных операций. Последнее достигается указанием в качестве последнего параметра функции DeviceIoControl() адреса структуры OVERLAPPED. Этот адрес помещается в элемент lpoOverlapped структуры DIOCParams, передаваемой драйверу вместе с сообщением W32_DeviceIOControl, и может быть извлечен оттуда драйвером так же, как извлекаются из этой структуры передаваемые в драйвер прикладные данные.
На рис. 1 показано главное окно приложения INTR32-3 с открытым меню (результаты выполнения приложения будут проиллюстрированы позже, после рассмотрения программы соответствующего ему виртуального драйвера).
Исходный текст виртуального драйвера INTR32-3.ASM
;Функции драйвера: ;0 Проверка версии ;1 VMyD_Init - Инициализация и пуск платы ;2 VMyD_Addr - Передача в приложение базового адреса драйвера ;3 VMyD_Exit - Разблокировка физической памяти DIOC_INIT=1 DIOC_ADDR=2 DIOC_EXIT=3 .386p .XLIST include vmm.inc include vwin32.inc include shell.inc include vpicd.inc .LIST Declare_Virtual_Device VMyD,1,0,VMyD_Control,8000h, \ Undefined_Init_Order ;======================= VxD_REAL_INIT_SEG ... ;Стандартная процедура инициализации реального режима VxD_REAL_INIT_ENDS ;====================== VxD_DATA_SEG result dw 0 IRQ_Handle dd 0 VMyD_Int13_Desc label dword VPICD_IRQ_Descriptor <5,,OFFSET32 VMyD_Int_13> lpdata dd 0 ;Линейный адрес данного из приложения ndata dd 0 ;Число блокируемых страниц с данными lpovlp dd 0 ;Линейный адрес структуры ovlp из приложения novlp dd 0 ;Число блокируемых страниц с ovlp VxD_DATA_ENDS ;====================== VxD_CODE_SEG BeginProc VMyD_Control Control_Dispatch W32_DeviceIOControl,IOControl Control_Dispatch Device_Init, VMyD_Device_Init clc ret EndProc VMyD_Control ;----------------------------- BeginProc VMyD_Device_Init mov EDI,OFFSET32 VMyD_Int13_Desc VxDCall VPICD_Virtualize_IRQ mov IRQ_Handle,EAX clc ret EndProc VMyD_Device_Init ;----------------------- BeginProc IOControl cmp dword ptr [ESI.dwIOControlCode],DIOC_GETVERSION je GetVer cmp dword ptr [ESI.dwIOControlCode],DIOC_INIT je VMyD_Init cmp dword ptr [ESI.dwIOControlCode],DIOC_ADDR je GetAddr cmp dword ptr [ESI.dwIOControlCode],DIOC_EXIT je VMyD_Exit clc ret EndProc IOControl ;-------------------------- BeginProc GetVer xor EAX,EAX clc ret EndProc GetVer ;-------------------------- BeginProc GetAddr mov EDI,dword ptr [ESI.lpvOutBuffer] mov [EDI],offset32 VMyD_Control clc ret EndProc GetAddr ;----------------------------- BeginProc VMyD_Init ;Общий сброс платы mov DX,30Ch in AL,DX ;Заблокируем данные Data приложения mov EDI,dword ptr [ESI.lpvInBuffer];Адрес буфера приложения mov EAX,[EDI+6] ;Получим адрес переменной Data приложения and EAX,0FFFh ;Выделим младшие 12 бит – смещение на странице mov EBX,EAX ;Сохраним его в EBX mov EAX,[EDI+6] ;Получим адрес переменной Data приложения mov ECX,EAX ;Занесем его также в ECX shr ECX,12 ;Полный номер начальной физической страницы add EAX,1 ;Прибавим длину данных, равную 1, к адресу переменной shr EAX,12 ;Полный номер конечной физической страницы с данными sub EAX,ECX ;Число страниц, требуемых для данных, равное 1 inc EAX ;Число страниц, требуемых для данных mov ndata,EAX ;Сохраним его push PAGEMAPGLOBAL;Вид блокирования push EAX ;Число блокируемых страниц push ECX ;Полный номер начальной блокируемой страницы VMMCall _LinPageLock;EAX=новый линейный адрес страницы add EAX,EBX ;Новый линейный адрес данных mov lpdata,EAX ;Сохраним его add ESP,12 ;Восстановление стека ;Заблокируем структуру ovlp в приложении mov EAX,dword ptr [ESI.lpoOVerlapped];Получим адрес структуры ovlp and EAX,0FFFh ;Выделим младшие 12 бит – смещение на странице mov EBX,EAX ;Сохраним его в EBX mov EAX,dword ptr [ESI.lpoOverlapped];Получим адрес структуры ovlp mov ECX,EAX ;Занесем его также в ECX shr ECX,12 ;Полный номер начальной физической страницы add EAX,19 ;Прибавим длину блокируемых данных, равную 1 shr EAX,12 ;Полный номер конечной физической страницы с ovlp sub EAX,ECX ;Число страниц, требуемых для ovlp, равное 1 inc EAX ;Число страниц, требуемых для ovlp mov novlp,EAX ;Сохраним его push PAGEMAPGLOBAL;Вид блокирования push EAX ;Число блокируемых страниц push ECX ;Полный номер начальной блокируемой страницы VMMCall _LinPageLock;EAX=новый линейный адрес страницы с ovlp add EAX,EBX ;Новый линейный адрес ovlp mov lpovlp,EAX ;Сохраним его add ESP,12 ;Восстановление стека ;Получим адрес буфера с данными настройки mov EDI,dword ptr [ESI.lpvInBuffer] ;Засылаем управляющие слова по каналам mov DX,303h ;Регистр команд mov AL,36h ;Канал 0, режим 3 out DX,AL mov AL,70h ;Канал 1, режим 0 out DX,AL mov AL,0B6h ;Канал 2, режим 3 out DX,AL ;Программируем канал 0 - 1-ю половину таймера mov AX,[EDI+0] ;1-й параметр mov DX,300h out DX,AL ;Младший байт частоты xchg AL,AH out DX,AL ;Старший байт частоты ;Програмируем канал 1 - 2-ю половину таймера mov AX,[EDI+2] ;2-й параметр mov DX,301h out DX,AL ;Младший байт интервала xchg AL,AH out DX,AL ;Старший байт интервала ;Программируем внутренний генератор mov AX,[EDI+4] ;3-й параметр mov DX,302h out DX,AL xchg AH,AL out DX,AL ;Сбрасываем счетчик mov DX,308h out DX,AL ;Установим флаг S2 разрешения счета mov DX,30Bh in AL,DX ;Размаскируем прерывания mov EAX,IRQ_Handle VxDCall VPICD_Physically_Unmask clc ret EndProc VMyD_Init ;-------------------------- BeginProc VMyD_Exit ;Разблокируем переменную Data приложения mov EAX,lpdata ;Адрес-псевдоним shr EAX,12 ;Полный номер начальной страницы push PAGEMAPGLOBAL push ndata ;Число страниц push EAX ;Полный номер начальной страницы VMMCall _LinPageUnLock add ESP,12 ;Разблокируем структуру ovlp приложения mov EAX,lpovlp shr EAX,12 ;Полный номер начальной страницы push PAGEMAPGLOBAL push novlp ;Число страниц push EAX ;Полный номер начальной страницы VMMCall _LinPageUnLock add ESP,12 clc ret EndProc VMyD_Exit ;--------------------------------- BeginProc VMyD_Int_13, High_Freq pushad ;Получим данное mov DX,309h in AL,DX mov AH,AL dec DX in AL,DX mov result,AX ;Сброс флагов готовности и разрешения счета mov DX,30Ah out DX,AL ;Выполним завершающие действия в PIC и выведем результаты mov EAX,IRQ_Handle VxDCall VPICD_Phys_EOI VxDCall VPICD_Physically_Mask mov AX,result mov EDI,lpdata ;Передадим данное mov [EDI],AX ;в буфер приложения mov EAX,lpovlp ;Адрес структуры OVERLAPPED mov EBX,[EAX.O_Internal];Значение члена Internal VxDCall VWIN32_DIOCCompletionRoutine popad clc ret EndProc VMyD_Int_13 VxD_CODE_ENDS end VMyD_Real_Init
Этот вариант виртуального драйвера отличается от предыдущего варианта двумя особенностями. Во-первых, в нем осуществляется блокирование в памяти физических страниц не только для данного Data, но и для структурной переменной ovlp, к которой также надо обеспечить доступ из обработчика аппаратных прерываний. Во-вторых, вызовом функции VWIN32_DIOCCompletionRoutine перед завершением обработчика прерываний драйвер оповещает приложение об окончании асинхронной операции. Этот вызов приводит к установке события и переводу потока с функцией Isr() в состояние выполнения.
Для обеспечения блокирования переменной ovlp адрес этой структуры, как уже отмечалось выше, пересылается в драйвер на этапе инициализации платы вместе с константами инициализации платы и адресом переменной Data. В сегменте данных драйвера резервируются двухсловные ячейки lpdata и lpovlp для адресов блокируемых переменных приложения, а также ячейки ndata и novlp для хранения числа блокируемых страниц.
В процедуре VMyD_Init драйвера теперь блокируются два участка физической памяти приложения: с переменной Data и со структурой ovlp.
Установка события, разблокирующего спящий поток с функцией Isr() приложения, осуществляется в конце процедуры VMyD_Init_13 обработчика аппаратных прерываний драйвера. После чтения результата измерений, посылки в контроллер прерываний команды EOI, маскирования нашего уровня прерываний и передачи в приложение (в переменную Data) результата измерений выполняется вызов функции VWIN32_DIOCCompletionRoutine, которую предоставляет системный виртуальный драйвер VWIN32.VXD. Этот вызов оповещает систему о завершении асинхронной операции в виртуальном драйвере и переводит в свободное состояние то событие, дескриптор которого содержится в элементе hEvent структуры OVERLAPPED (вспомним, что, создав событие, мы поместили его дескриптор hEvent в элемент ovlp.hEvent этой структуры). Функция VWIN32_DIOCCompletionRoutine требует, чтобы в регистре EBX содержалось такое значение элемента Internal структуры OVERLAPPED, которое было передано в драйвер на этапе активизации механизма асинхронных операций. Поэтому вызов этой функции выглядит следующим образом:
mov EAX,lpovl ;Адрес структуры OVERLAPPED mov EBX,[EAX.O_Internal];Значение члена Internal VxDCall VWIN32_DIOCCompletionRoutine
Для того чтобы обратиться к элементу Internal в структуре OVERLAPPED, надо знать символическое обозначение смещения этого элемента. Состав структуры OVERLAPPED, который был приведен выше и в котором фигурирует имя Internal, определен в файле WINBASE.H — заголовочном файле для программ, написанных на языке Си. В программах на языке ассемблера его, естественно, использовать нельзя. Однако такая же структура OVERLAPPED описана и в файле VWIN32.INC, входящем в состав пакета DDK и предназначенном для программ на языке ассемблера (только там она называется _OVERLAPPED). Первый элемент этой структуры носит имя O_Internal, которое мы и использовали в приведенном выше фрагменте.
На рис. 2 показан ход выполнения программы. Видно, что после выбора пункта «Пуск» и истечения заданного интервала времени на экран было выведено окно сообщения из асинхронной функции Isr(). После этого для контроля был выбран пункт меню «Чтение данных», в котором вызывается прикладная функция ReadData(). Она, естественно, вывела из переменной Data то же число (101 событие).
КомпьютерПресс 12'2001