oldi

Основы разработки прикладных виртуальных драйверов

Часть 5. Обработка аппаратных прерываний в системе Windows

К.Г.Финогенов

Автоматизированные установки, построенные на базе персонального компьютера, используют, как правило, режим прерываний. Прерывания могут появляться в установке по самым разным причинам, среди которых: накопление заданного объема данных, выход параметров установки за установленные пределы, регистрация ожидаемого события и т.д. В любом случае приход прерывания должен активизировать в приложении определенную процедуру — обработчик прерывания, где выполняется то или иное обслуживание установки: прием накопленной информации, включение или выключение управляющих элементов, изменение режима работы установки и пр.

Выходы сигналов прерываний из установки подсоединяются к свободным уровням прерываний, а в программное обеспечение включаются обработчики прерываний, соответствующих этим уровням векторов. Обычно используются уровни 3 или 5 ведущего контроллера (векторы 0Bh и 0Dh реального режима) или уровни 9...13 и 15 ведомого контроллера (векторы 71h...75h и 77h).

При работе в операционной системе MS-DOS контроллеры прерываний программируются так, что ведущему контроллеру назначается базовый вектор 8, а ведомому — 70h. Однако такая схема не может использоваться в защищенном режиме, поскольку первые 18 векторов заняты исключениями и не могут использоваться аппаратными прерываниями. Поэтому при загрузке Windows контроллеры прерываний программируются иначе: ведущему контроллеру назначается базовый вектор 50h, ведомому — 58h. Эти векторы отвечают последним 16 дескрипторам таблицы дескрипторов прерываний IDT, содержащей в целом 60h дескрипторов.

В дескрипторах IDT, соответствующих аппаратным прерываниям, хранятся адреса системных обработчиков, которые принадлежат виртуальному драйверу контроллера прерываний (виртуальному контроллеру) VPICD. Эти адреса назначаются системой, не могут быть изменены пользователем и вообще не видны из приложения. Если программист включил в 16-разрядное приложение Windows собственный обработчик какого-либо аппаратного прерывания, этот обработчик устанавливается с помощью функций Си getvect() и setvect(), которые преобразуются компилятором в вызовы функций 35h и 25h DOS. При вызове этих функций следует указывать номера векторов, которые использовались в DOS (8 — для таймера, 70h — для часов реального времени и т.д.). Вызовы указанных функций в защищенном режиме перехватываются VPICD, который записывает адреса прикладных обработчиков не в IDT, а в отдельную таблицу векторов защищенного режима, которая создается в каждой виртуальной машине (если их несколько). По приходе аппаратного прерывания процессор вызывает его обработчик в соответствии с содержимым IDT. Этот обработчик, входящий в состав VPICD и обслуживающий прерывание по правилам Windows, в конце концов передает управление по адресу, который хранится в таблице векторов защищенного режима.

Соответствие номеров векторов, номеров уровней прерываний и устройств, характерное для системы Windows, показано в табл. 1.

На старых машинах типа XT с одним контроллером прерываний уровень IRQ2 был свободен и мог использоваться при подключении к компьютеру нестандартной аппаратуры, работающей в режиме прерываний. Этому уровню соответствует вектор 0Ah, который обычно и назначался прикладным обработчикам аппаратных прерываний (разумеется, нестандартное устройство можно было подключить и к любому другому свободному уровню).

В современных машинах, где используются два контроллера прерываний, второй (ведомый)  контроллер подключается как раз ко входу 2 первого (ведущего). Приход аппаратного прерывания на любой вход ведомого контроллера приводит к возникновению сигнала на входе 2 ведущего контроллера, который, однако, сопровождается посылкой в процессор не вектора 0Ah (уровень IRQ2), а вектора, который соответствует в ведомом контроллере пришедшему прерыванию (например, 70h для уровня IRQ8).

Линия IRQ2 внешней магистрали и соответствующий контакт разъема расширения подключаются теперь не ко входу 2 ведущего контроллера (этот вход занят), а ко входу 1 ведомого контроллера, которому соответствуют уровень IRQ9 и вектор 71h реального режима. Для того чтобы обеспечить программную совместимость старых и новых машин, в систему MS-DOS включен обработчик прерывания 71h, в котором вызывается программное прерывание 0Ah. Таким образом, хотя внешнее устройство, подключенное к линии IRQ2, фактически работает на уровне IRQ9, его могут обслуживать обработчики прерываний, вызываемые через вектор 0Ah.

В системе Windows перенаправление вектора 71h не реализуется. Поэтому внешние устройства, подключаемые к линии IRQ2 (а фактически к в уровню IRQ9, то есть к входу 1 ведомого контроллера прерываний), необходимо программировать через вектор 71h, соответствующий этому уровню. Устройства же, подключаемые к другим свободным уровням, например IRQ3 или IRQ5, как и раньше, программируются через векторы 0Bh или 0Dh. При этом, хотя на первый взгляд вектор 0Dh совпадает по номеру с вектором исключения общей защиты, никаких конфликтов не возникает, так как обработчик исключения общей защиты вызывается через аппаратный вектор 0Dh таблицы IDT, а «вектор 0Dh», используемый в программе, в аппаратном плане соответствует дескриптору IDT с номером 55h, а в программном — ячейке с номером 0Dh в таблице векторов защищенного режима, не имеющей никакого отношения к обработке исключений.

Общие правила обработки прерываний от нестандартной аппаратуры в системах DOS и Windows одинаковы. В основной программе выполняются сохранение исходного содержимого используемого вектора и занесение в вектор полного адреса нового обработчика. Далее необходимо размаскировать используемый уровень прерываний в контроллере прерываний (порт 21h в ведущем контроллере или A1h — в ведомом) и, возможно, разрешить прерывания в подключенной к компьютеру аппаратуре, если в регистре управления интерфейсной платы, осуществляющей связь с нестандартной аппаратурой, имеется бит управления прерываниями. Выполнив эти инициализирующие действия, основная программа может заниматься чем угодно. Сигнал прерывания из аппаратуры передаст управление установленному обработчику, который должен перед своим завершением послать в контроллер прерываний команду EOI, чтобы снять блокировку в контроллере прерываний. Перед завершением всей программы необходимо в общем случае восстановить в используемом векторе его исходное содержимое, замаскировать используемый уровень в контроллере прерываний и запретить прерывания в регистре управления аппаратурой.

Для изучения базовых, архитектурных вопросов обработки прерываний можно воспользоваться простыми обработчиками прерываний от системного таймера, клавиатуры или мыши. Эти периферийные устройства имеются на любом компьютере и везде работают одинаково. Однако обсуждение особенностей обработки прерываний от нестандартной аппаратуры в системе Windows требует привязки к конкретной измерительной или управляющей аппаратуре, которая, разумеется, отсутствует на компьютере пользователя. Примеры, которые мы приводим ниже отлаживались на специально разработанном макете, оформленном в виде интерфейсной платы и представляющем собой программируемый таймер-счетчик внешних событий. Этот макет используется в лабораторном студенческом практикуме по курсам, посвященным программированию аппаратуры физического эксперимента. В частности, с помощью этой интерфейсной платы демонстрируется методика разработки драйверов Windows для управления нестандартной аппаратурой. Хотя разработанный нами макет является сугубо специфическим устройством и имеется только в лаборатории автора, в нем используются общепринятые методы построения микропроцессорной аппаратуры, и в этом плане макет вместе с компьютером представляет собой типичную программно-управляемую измерительную установку. Читателю не составит труда трансформировать описываемые ниже примеры применительно к собственным потребностям.

На рис. 1 приведена условная логическая схема интерфейсной платы таймера-счетчика, по которой можно проследить ее функционирование и принципы программного управления.

Плата предназначена для построения на базе персонального компьютера автоматизированного устройства для счета событий. Плата включает микросхему 3-канального таймера КР580ВИ53, 16-битовый счетчик для счета поступающих событий и ряд логических узлов. Счетчик может считать внешние события или импульсы от встроенного в плату генератора. Последний режим используется для проверки работоспособности платы и отладки разрабатываемого программного обеспечения.

Каналы 0 и 1 микросхемы таймера, соединенные на плате последовательно, позволяют задать временной интервал счета событий. Длительность интервала определяется константами С0 и С1, занесенными в буферные регистры каналов 0 и 1, и равна

T = (C0 * C1): f ,

где f — частота импульсов, подаваемых на вход канала 0. Канал 0 работает в режиме 3 пересчета импульсов; канал 1 работает в режиме 0 генерации единственного импульса с длительностью Т. На плате установлен кварцевый генератор с частотой 1 МГц, поэтому

T = (C0 * C1) мкс.

Соотношение констант С0 и С1 в принципе не имеет значения, однако в силу некоторых особенностей микросхемы таймера (в котором, строго говоря, не предусмотрена возможность последовательного соединения каналов) он дает погрешность приблизительно в ½ периода канала 0, и для ее уменьшения желательно выбирать С0 как можно меньше (однако запрещается значение С0 = 3).

Канал 2 микросхемы таймера, используемый при отладке программ управления в качестве внутреннего источника считаемых событий, работает в режиме 3 пересчета импульсов. При занесении в его буферный регистр константы С2 частота импульсов на его выходе составляет

f2 = 106: C2 Гц.

В состав электронной схемы платы входят четыре флага — S0, S1, S2 и S3, служащие для управления устройством и индикации его состояния. Флаги S0 и S2 являются управляющими, S1 и S3 —  индикаторными. Флаг S0 переключает вход счетчика (счет внешних сигналов или импульсов от встроенного генератора), флаг S2 разрешает счет, а также установку флага S3 по завершении временного интервала. Индикаторный флаг S3 указывает на завершение установленного временного интервала, а S1 —  на переполнение счетчика. Установка любого из этих флагов приводит к возбуждению сигнала прерывания на линии IRQ5.

Все четыре флага входят в состав 4-битового регистра с адресом 30Ah, где они занимают биты 4, 5, 6 и 7 соответственно и могут быть прочитаны командой in ввода из порта. Однако выполнить запись в любой из флагов через порт 30Ah нельзя — для установки и сброса флагов предусмотрены специальные операции. Так, для установки флага S2 разрешения счета следует выполнить чтение из порта 30Bh, а для сброса флага S1 переполнения счетчика — запись (чего угодно) в порт 309h. Полный состав операций, реализованных в рассматриваемой схеме, также приведен на рис. 1.

Общая структурная схема программы управления платой в режиме ожидания завершения временного интервала выглядит следующим образом:

  • общий сброс (чтение или запись через порт 30Ch);
  • засылка управляющих слов в регистр команд (порт 303h) для задания режима каналов 0, 1 и 2;
  • засылка констант счета в буферные регистры каналов 0, 1 и 2 (порты 300h, 301h и 302h);
  • установка флага S2 для разрешения счета (чтение из порта 30Bh);
  • ожидание установки флага S3, то есть окончания заданного временного интервала;
  • после установки флага S3 считывание содержимого счетчика платы (в два этапа: сначала один байт счетчика, затем другой; чтение младшего байта осуществляется через порт 308h, чтение старшего — через порт 309h).

При использовании режима прерываний на этапе инициализации следует выполнить первые четыре из перечисленных выше операций; флаг разрешения-запрещения прерываний в схеме не предусмотрен. Поскольку прерывание возбуждается как при завершении временного интервала, так и при переполнении счетчика, в обработчике прерываний следует проверять состояние флагов S1 и S3 и определять причину прерывания.

Рассмотрим сначала обычное 16-разрядное приложение Windows, выполняющее управление платой таймера-счетчика в режиме прерываний без драйвера. Чтобы продемонстрировать проблемы, возникающие при обработке прерываний в приложениях Windows, нам придется сделать эту программу более типичной (ввести в нее главное окно приложения с меню) и, следовательно, значительно более сложной по сравнению с рассмотренными выше.

Для того чтобы в главном окне приложения появилась стандартная полоска с пунктами меню, необходимо в тексте программы заказать вывод окна с меню, а состав меню описать в специальном файле ресурсов. Этот файл является текстовым файлом и имеет стандартное расширение .RC. Различные ресурсы описываются здесь в специальных форматах, понятных для компилятора ресурсов, входящего в состав любой интегрированной среды разработки приложений Windows (IDE), например пакета Borland C++. Кроме меню в файле ресурсов можно описывать состав диалоговых окон, таблицы символьных строк и другие объекты, выводимые в окно приложения. Для того чтобы IDE выполнила обработку файла ресурсов, его вместе с основным, программным файлом необходимо включить в состав проекта, создаваемого в рамках IDE.

Файл ресурсов для приложения Windows, управляющего платой таймера-счетчика в режиме прерываний:

#define MI_START 100        //Константы, используемые  
#define MI_EXIT 101        //для идентификации  
#define MI_READ 102        //пунктов меню  
Main MENU{  
  POPUP "Режим"  {  
    MENUITEM "Пуск",MI_START  
    MENUITEM "Чтение",MI_READ  
    MENUITEM SEPARATOR  
    MENUITEM "Выход",MI_EXIT  
  }  
}  

Состав, или сценарий, меню описывается в файле ресурсов с помощью предусмотренных для этого ключевых слов. Сценарий меню начинается с ключевого слова MENU, за которым следует перечень пунктов меню, заключенный в фигурные скобки. Слово MENU предваряется произвольным именем (у нас это имя Main), которое выступает как идентификатор меню и далее будет использовано в тексте программы.

Каждый пункт перечня пунктов меню начинается с ключевого слова POPUP, за которым следует название пункта меню. В нашем примере все меню состоит из единственного пункта «Режим».

Вслед за предложением POPUP следует перечень подпунктов этого пункта меню, который появится на экране при его выборе (щелчком левой клавиши мыши). Перечень заключается в фигурные скобки. Каждый подпункт начинается ключевым словом MENUITEM, за ним указываются название данного подпункта и его идентификатор. Идентификаторы обычно имеют символическую форму. У нас это обозначения MI_START, MI_EXIT и MI_READ, которым в начале файла ресурсов присваиваются  (оператором препроцессора #define) произвольные значения: 100, 101 и 102.

Вид меню, соответствующего приведенному выше файлу ресурсов, показан на рис. 2.

Теперь перейдем к рассмотрению основного файла приложения.

Исходный текст приложения Windows, управляющего платой таймера-счетчика в режиме прерываний:

#define STRICT  
#include <windows.h>  
#include <windowsx.h>  
#include <string.h>  
#include <dos.h>        //Поддержка средств MS-DOS  
//Определения констант  
#define MI_START 100        //Константы, используемые  
#define MI_EXIT 101        //для идентификации  
#define MI_READ 102        //пунктов меню  
#define VECTOR 13        //Номер используемого вектора прерываний (уровень IRQ5)  
//Прототипы функций  
void Register(HINSTANCE);        //Вспомогательные  
void Create(HINSTANCE);        //функции  
void interrupt isr(...);        //Обработчик прерываний  
LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);//Оконная функция  
void OnCommand(HWND,int,HWND,UINT);//Функции обработки  
void OnDestroy(HWND);        //сообщений Windows  
void InitCard();        //Функция инициализации платы  
//Глобальные переменные  
void interrupt (*old_isr)(...);//Для сохранения исходного содержимого вектора  
char szClassName[]="MainWindow";//Имя класса  
char szTitle[]="Управление";//Заголовок окна  
int unsigned data,parm;        //Результат измерений и передаваемый в драйвер параметр  
unsigned char mask,half;        //Маска и байт для получения результата  
//Главная функция WinMain  
int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE,LPSTR,int){  
  MSG msg;            //Структура типа MSG  
  Register(hInstance);        //Зарегистрируем класс главного окна  
  Create(hInstance);        //Создадим и покажем окно  
  while(GetMessage(&msg,NULL,0,0))//Цикл обработки сообщений  
     DispatchMessage(&msg);  
  return 0;           //Выход из приложения в Windows  
  }  
//Функция Register регистрации класса окна  
void Register(HINSTANCE hInst){  
  WNDCLASS wc;             //Структура с характеристиками класса окна  
  memset(&wc,0,sizeof(wc));//Обнуление структуры 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);        //Регистрация класса главного окна  
  }  
//Функция Create создания и показа окна  
void Create(HINSTANCE hInst){  
  HWND hwnd=CreateWindow(szClassName,szTitle,WS_OVERLAPPEDWINDOW,  
      10,10,200,100,HWND_DESKTOP,NULL,hInst,NULL);  
  ShowWindow(hwnd,SW_SHOWNORMAL);  
  }  
//Оконная функция WndProc главного окна  
LRESULT CALLBACK WndProc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam){  
  switch(msg){  
    HANDLE_MSG(hwnd,WM_COMMAND,OnCommand);  
    HANDLE_MSG(hwnd,WM_DESTROY,OnDestroy);  
  default:  
    return(DefWindowProc(hwnd,msg,wParam,lParam));  
  }  
}  
//Функция OnCommand обработки сообщений WM_COMMAND от пунктов меню  
void OnCommand(HWND hwnd,int id,HWND,UINT){  
  switch(id){  
    case MI_START:        //Выбран пункт "Пуск"  
      old_isr=getvect(VECTOR);//Сохраним исходный вектор  
      setvect(VECTOR,isr);//Установим наш обработчик isr  
      InitCard();        //Инициализируем плату  
      break;  
    case MI_READ:        //Выбран пункт "Чтение"  
      char txt [80];  
      wsprintf(txt,"Накоплено %d событий",data);  
      MessageBox(hMainWnd,txt,"Данные",MB_ICONINFORMATION);  
      break;  
    case MI_EXIT:        //Выбран пункт "Выход"   
      DestroyWindow(hwnd);        //Завершение приложения  
  }  
}  
//Функция OnDestroy обработки сообщения WM_DESTROY  
void OnDestroy(HWND){  
  mask|=0x20;         //Установим в маске бит 5  
  outportb(0x21,mask); //и выведем в порт  
  setvect(VECTOR,*old_isr);//Восстановим исходный вектор  
  PostQuitMessage(0);        //Завершим приложение  
}  
//Функция InitCard инициализации платы  
void InitCard (){  
  inportb(0x30c);        //Сброс  
//Установим режимы каналов  
  outportb(0x303,0x36);        //Канал 0, режим 3  
  outportb(0x303,0x70);        //Канал 1, режим 0  
  outportb(0x303,0xb6);        //Канал 2, режим 3  
//Занесем константы каналов  
  parm=100;  
  outportb(0x300,LOBYTE(parm));//Младший байт константы C0  
  outportb(0x300,HIBYTE(parm));//Старший байт константы C0  
  parm=20000;  
  outportb(0x301,LOBYTE(parm));//Младший байт константы C1  
  outportb(0x301,HIBYTE(parm));//Старший байт константы C1  
  parm=1000;  
  outportb(0x302,LOBYTE(parm));//Младший байт константы C2  
  outportb(0x302,HIBYTE(parm));//Старший байт константы C2  
//Размаскируем уровень 5 в контроллере прерываний  
  mask=inportb(0x21);        //Прочитаем текущую маску  
  mask&=0xdf;         //Сбросим в ней бит 5  
  outportb(0x21,mask);        //Выведем в порт  
  inportb(0x30b);      //Пуск  
  }  
//Функция isr — обработчик прерываний от платы  
void interrupt isr(...){  
  half=inportb(0x309);        //Чтение старшего байта результата  
  data=(WORD)half;  
  data<<=8;           //Сдвиг в старшую половину ячейки  
  half=inportb(0x308);        //Чтение младшего байта результата  
  data+=half;         //Объединение его со старшим байтом  
  outportb(0x20,0x20);        //Команда конца прерываний EOI в контроллер прерываний  
  }  

В типичном приложении Windows главная функция WinMain() должна выполнить по меньшей мере три важных процедуры:

  • зарегистрировать в системе Windows класс главного окна. Если помимо главного окна планируется выводить на экран внутренние, порожденные окна, то их классы тоже необходимо зарегистрировать, поскольку Windows выводит на экран и обслуживает только зарегистрированные окна;
  • создать главное окно и показать его на экране. Порожденные окна также необходимо создать, хотя это можно сделать и позже, причем  не обязательно в функции WinMain();
  • организовать цикл обработки сообщений, поступающих в приложение. Вся дальнейшая жизнь приложения будет состоять фактически из бесконечного выполнения этого цикла и из обработки поступающих в приложение сообщений. Запущенное приложение Windows обычно функционирует до тех пор, пока пользователь не подаст команду его завершения с помощью системного меню или посредством ввода <Alt>+<F4>. Эти действия приводят к завершению главной функции и удалению приложения из списка действующих задач.

Первое из перечисленных действий выполняется в нашем примере вызовом прикладной функции Register(), второе — вызовом функции (тоже прикладной) Create(), третье — циклом, включающим две функции Windows: GetMessage() и DispatchMessage().

Как уже отмечалось, при запуске приложения Windows управление всегда передается функции WinMain(). Эта функция, имеющая в принципе циклический характер, выполняется в течение всей жизни приложения. Основное назначение функции WinMain — выполнение инициализирующих действий и организация цикла обработки сообщений.

Сообщения Windows являются, пожалуй, самой важной концепцией этой системы. Каждый раз, когда происходит какое-то событие, затрагивающее интересы программы (например, пользователь выбирает пункт меню или нажимает на кнопку в окне диалога), Windows посылает приложению сообщение об этом событии. Задача функции WinMain() заключается в приеме этого сообщения и передаче его второму важнейшему компоненту любого приложения Windows — функции главного окна, или, проще говоря, оконной функции (оконной процедуре). В отличие от главной функции WinMain(), имя которой изменять нельзя, имя оконной функции может быть любым. В наших примерах оконная функция главного окна именуется WndProc.

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

В функцию Register() вынесены действия по регистрации класса окна. В ней объявляется и заполняется структура типа WNDCLASS, в которой указываются, в частности, имя меню из файла ресурсов и имя оконной функции, а затем вызывается функция Windows RegisterClass(), которая и выполняет регистрацию данного класса.

Зарегистрировав класс окна, можно приступить к его созданию и показу. Эта процедура вынесена у нас в подпрограмму Create(), в которой последовательно вызываются две функции Windows: CreateWindow() — для создания главного окна и ShowWindow() — для его вывода на экран.

После выполнения функции ShowWindow() на экране появляется главное окно с заданными нами характеристиками. Теперь для правильного функционирования приложения необходимо организовать цикл обработки сообщений, который в простейшем виде состоит из одного предложения:

while(GetMessage(&msg,NULL,0,0))  
    DispatchMessage(&msg);  

В этом бесконечном (если его не разорвать изнутри) цикле вызывается функция Windows GetMessage() и, в случае если она возвращает ненулевое значение, вызывается функция DispatchMessage().

Функция GetMessage() анализирует очередь сообщений приложения. Если в очереди обнаруживается сообщение, GetMessage() изымает его из очереди и передает в структурную переменную msg типа MSG, объявленную в начале функции WinMain(). После этого вызывается функция DispatchMessage(), вызывающая оконную функцию для того окна, которому предназначено очередное сообщение. После того как оконная функция обработает сообщение, возврат из нее приводит к возврату из функции DispatchMessage() на продолжение цикла while. При отсутствии сообщений в очереди приложения функция GetMessage() передает управление системе Windows, которая последовательно опрашивает очереди сообщений других запущенных приложений (если, конечно, они имеются). После опроса всех приложений управление возвращается в функцию GetMessage(), которая опять анализирует состояние очереди нашего приложения.

Оконная функция главного окна состоит из стольких фрагментов, сколько предполагается обрабатывать сообщений. Конкретное содержимое каждого фрагмента определяет программист. Все сообщения, не нуждающиеся в прикладной обработке, должны поступать в функцию обработки сообщений по умолчанию DefWindowProc() и обрабатываться системой Windows. Эти действия можно описать в программе с помощью конструкции switch-case:

switch(msg){  
  case WM_COMMAND:  
    //Действия в ответ на выбор пункта меню  
    return 0;  
  case WM_DESTROY:  
    //Действия, необходимые перед завершением программы  
    PostQuitMessage(0);//Завершение программы  
    return 0;  
  default:  
    return(DefWindowProc(hwnd,msg,wParam,lParam));  
  }  

Однако такая конструкция неудобна. Оконная функция представляет собой в этом случае один длинный оператор switch со столькими блоками case, сколько сообщений Windows предполагается обрабатывать в программе. При обработке ряда сообщений, например WM_COMMAND, внутрь блоков case приходится включать вложенные операторы switch-case,  к тому же не одного уровня вложенности. В результате функция WndProc() становится чрезвычайно длинной и запутанной. Весьма полезная идея структурированности программы исчезает почти полностью, так как все приложение оказывается состоящим из едва ли не единственной функции WndProc() со множеством разветвлений внутри.

Заметного упрощения структуры программы можно добиться, используя макрос HANDLE_MSG, определенный в файле windowsx.h. Этот макрос, в зависимости от его параметров, преобразуется в один из многочисленных макросов вида HANDLE_WM_COMMAND, HANDLE_WM_DESTROY и т.д., которые расширяются в конструкции вида:

case WM_COMMAND: OnCommand(...); return 0;  
case WM_DESTROY: OnDestroy(...); return 0;  

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

В нашей программе обрабатываются всего два сообщения: WM_COMMAND от пунктов меню и WM_DESTROY, которое создается системой, если пользователь завершает приложение, например,  введя команду <Alt>+<F4>. Соответственно в программу введены две функции обработки этих сообщений: OnCommand() и OnDestroy().

Для обеспечения работы системы прерываний в начале программы указан прототип функции isr() с описателем interrupt, которая будет служить в качестве прикладного обработчика прерывания, а в области глобальных переменных объявлен указатель old_isr на функцию типа «interrupt», где будет храниться адрес исходного обработчика.

При выборе пункта «Пуск» (идентификатор MI_START) с помощью функции C getvector() читается и сохраняется в переменной old_isr исходное содержимое вектора 13, после чего функцией setvector() в вектор 13 заносится адрес нашего обработчика isr. Как было показано в начале статьи, функции getvector() и setvector() работают не с истинной таблицей дескрипторов прерываний IDT, а со специальной таблицей векторов защищенного режима, входящей в состав виртуальной машины.

Процедура инициализации платы вынесена в отдельную функцию InitCard(). В ней прежде всего выполняется общий сброс всех узлов платы, далее посредством посылки кодов 36h, 70h и B6h в порт регистра команд 303h задаются режимы всех трех каналов таймера и, наконец, во все три канала последовательно засылаются константы С0, С1 и С2. Для простоты значения констант определены прямо в тексте программы; в реальной программе их следовало бы вводить с клавиатуры. Для выделения младших и старших половин констант (вспомним, что константы размером в одно слово засылаются в порты таймера в два приема, по байтам) используются удобные макросы Windows: LOBYTE и HIBYTE.

Нам осталось размаскировать уровень 5 (соответствующий вектору 13) контроллера прерываний. Текущая маска читается в переменную mask, оператором & в ней обнуляется бит 5 (нулю в бите 5 соответствует число DFh), и новое значение маски отправляется в порт 21h. Еще раз отметим, что VMM перехватывает обращение к запрещенному порту 21h, в результате чего наше данное поступает не в аппаратный порт контроллера прерываний, а в его программную копию — виртуальную маску прерываний, входящую в состав виртуальной машины. «Заведует» этой маской, как и всей системой прерываний, виртуальный контроллер прерываний VPICD.

Последним предложением функции InitCard() в схеме счетчика-таймера устанавливается флаг S2, открывающий вход счетчика и одновременно разрешающий установку флага S3 завершения временного интервала.

Обработчик прерываний в реальной установке может выполнять различные функции. Наверное, самое естественное — вывести на экран сообщение об окончании измерений. Однако сделать это не так-то просто. Дело в том, что возможности обработчика прерываний ограничены. В нем можно выполнять вычислительные действия, а также чтение и запись ячеек памяти, но нельзя, например, вызвать функцию Windows MessageBox() для вывода на экран сообщения. Чтобы не усложнять рассматриваемый пример, в функции isr() мы просто читаем содержимое счетчика платы и заносим его в глобальную переменную data, давая тем самым программе возможность работать с этими данными. Чтение числа накопленных событий приходится выполнять в два приема. Старшая половина данных из порта 309h читается в байтовую переменную half, переносится в переменную data с преобразованием в слово и сдвигается в этой переменной влево на 8 бит, то есть переносится в старший байт. Далее младшая половина данных читается в ту же переменную half и складывается со старшей в переменной data.

Последнее (обязательное!) действие в обработчике прерываний — снятие блокировки в контроллере прерываний командой EOI. И эта операция в действительности выполняется не в физическом контроллере прерываний, а в виртуальном.

При выборе пункта меню «Чтение» выполняются относительно простая операция: преобразование с помощью функции Windows wsprintf() числа в переменной data в символьную форму и вывод его в окно сообщения функцией MessageBox()  (результат этих действий был показан на рис. 3 части 4 этой статьи).

Таким образом, программное управление аппаратурой, работающей в режиме прерываний, можно реализовать и без использования виртуального драйвера. При этом структуры, макросы и функции языка C, относящиеся к обслуживанию прерываний (в частности, функции getvect(), setvect(), inport(), outport() и др.), описаны в заголовочном файле DOS.H, который необходимо подключить к исходному тексту программы. Отметим, что сказанное относится только к 16-разрядным приложениям. Для 32-разрядных приложений аналога файла DOS.H нет, и обработка прерываний в таких программах невозможна без соответствующего виртуального драйвера. Однако и для 16-разрядных приложений разработка специализированного виртуального драйвера может оказать благотворное влияние, так как при его использовании заметно сокращается время реакции на прерывание, что в некоторых случаях может оказаться полезным для работы аппаратуры. Следующая часть статьи будет посвящена принципам построения виртуальных драйверов для обслуживания аппаратных прерываний.

КомпьютерПресс 7'2001