Софтпанорама No.1, 1990                 Составитель Н.Н.Безруков
****************************************************************
                                  Данный перевод вопроизводится
                                  с  разрешения редакции электрон-
                                  ного журнала НПО ГОРСИСТЕМОТЕХНИКА
                                  в рамках обмена материалами между
                                  редакциями
   
                      МЕТОДИКА СОЗДАНИЯ И ОТЛАДКИ
                   ДРАЙВЕРА ПЕРИФЕРИЙНОГО УСТРОЙСТВА
                    ДЛЯ ОПЕРАЦИОННОЙ СИСТЕМЫ MS-DOS

                           Росс М. Гринберг


   Создание драйверов периферийных устройств - это тяжелое  и  утоми-
тельное занятие, доступное лишь глубоким знатокам операционной систе-
мы. Решившиеся на этот подвиг могут требовать дополнительную плату за
риск,  несовершеннолетние  должны представить разрешение от родителей
либо опекунов, а кроме того, необходимо подписать определенные  доку-
менты,  ограничивающие право на иск при несчастном случае, и иметь по
крайней мере миллион долларов надежной страховки.
   Это  весьма распространенное мнение, но в действительности процесс
создания драйверов периферийных устройств не так уж отличается от ра-
боты по созданию других программ и не является намного более  сложной
задачей. В настоящей статье исследуются внутренние особенности работы
драйверов  периферийных  устройств  и предлагаются некоторые полезные
приемы их отладки.
   Строящийся  в этой статье драйвер MDM_DRV служит элементарной ком-
муникационной программой для передачи протоколов X-модема с контроль-
ной суммой. Несмотря на то, что для корректной работы этого  драйвера
требуется вторая его копия на другом конце канала передачи, организо-
ванного  через  модем  либо  проводное соединение, он может загружать
файлы с нескольких станций приема/передачи.
   Драйвер периферийного  устройства  служит  логическим  интерфейсом
между  подсистемой ввода/вывода операционной системы и обслуживаемыми
ею аппаратными средствами. В некоторых случаях драйвер  заменяет  или
расширяет  определенные аспекты BIOS'а и таким образом несет ответст-
венность за обработку характеристик обслуживаемых аппаратных средств.
В других драйверах физический ввод/вывод осуществляется исключительно
через BIOS. В ранних версиях операционной системы MS-DOS не была пре-
дусмотрена процедура установки драйверов. Однако,  начиная  с  версии
2.0,  программисты получили возможность дополнять операционную систе-
му. Несмотря на отсутствие общих стандартов для резидентных программ,
интерфейс между операционной системой и драйвером периферийного  уст-
ройства  жестко  определен, поэтому большинство драйверов может рабо-
тать друг с другом совместно.
   Драйверы  бывают  двух  типов:  ориентированные на символьный либо
блоковый обмен данными. Драйверы, обслуживающие хранение и/или доступ
к данным на дисках либо других устройствах  прямого  доступа,  обычно
ориентированы  на  блоковый обмен данными. Блоковые драйверы передают
данные фиксированными порциями - отсюда и происходит их название.
   Данная статья ориентирована на описание драйверов  с  посимвольной
передачей данных. Некоторые затронутые здесь вопросы имеют непосредс-
твенное отношение к резидентной программе последовательных прерываний
TSRCOMM.ASM, описанной в статье "Поддержка контакта с реальным миром:
быстрая  обработка  последовательного ввода/вывода" ("Keeping Up With
the Real World: Speedy Serial I/O Processing" MSJ, Vol.2 No.3).
   Символьные  драйверы  должны уметь работать в двух режимах: подго-
товленном и неподготовленном. В подготовленном режиме драйвер за одно
обращение получает один символ, причем некоторые символы,  такие  как
возврат  каретки, перевод строки (в транслированной либо нетранслиро-
ванной форме), Ctrl-Z и Ctrl-C  требуют  специальной  обработки.  При
каждом  запросе  на ввод/вывод проводится контроль статуса, проверяю-
щий, поступил ли с клавиатуры символ Ctrl-C, и в подготовленном режи-
ме драйвер позволяет  операционной  системе  при  поступлении  Ctrl-C
прервать  вызов.  Так как от вызывающей программы не требуется знание
того, в каком режиме находится драйвер, MS-DOS должна уметь корректно
транслировать типичный многобайтовый запрос на ввод/вывод от вышесто-
ящей прикладной программы в последовательность однобайтовых запросов.
MS-DOS осуществляет это, используя внутренние буфера и пересыая  дан-
ные в буфер пользователя после выполнения запроса.
   В  неподготовленном  режиме  от драйвера и операционной системы не
требуются никакие преобразования либо иные обслуживающие действия.  В
этом  режиме  нет ограничений на число байтов, запрашиваемых при вво-
де/выводе: драйвер должен уметь обработать запрос на любое  указанное
число байтов. Обычно в таких запросах не требуются никакие внутренние
буфера операционной системы, и вся обработка происходит в собственном
буфере  прикладной программы, что значительно повышает быстродействие
неподготовленного режима по сравнению с подготовленным.
   Пользовательская программа может менять режим из подготовленного в
неподготовленный и наоборот вызовом функции IOCTL операционной систе-
мы MS-DOS, - INT 21H с AH=44H (см. Техническое руководство по MS-DOS).


            Структура драйвера периферийного устройства

   Драйвер  состоит  из  трех  основных  частей: заголовка, программы
стратегий и программы прерываний. Заголовок описывает  возможности  и
атрибуты  драйвера, присваивает имя драйверу символьного устройства и
содержит NEAR (только смещение, одно слово)  указатели  на  программы
стратегий и прерываний, а также FAR (смещение и сегмент, двойное сло-
во) указатель на следующий драйвер в цепочке драйверов (цепочка явля-
ется  однонаправленной, что затрудняет поиск ее начала). Указатель на
следующий драйвер устанавливает MS-DOS сразу после завершения  проце-
дуры  инициализации, а в самом драйвере ему должно быть присвоено на-
чальное значение -1 (FFFFFFFFH).
   Драйвер должен помещаться в 64K памяти, т.к. программы стратегий и
прерываний содержат лишь смещения внутри выделенного драйверу сегмен-
та. Наглядная схема заголовка представлена на рис.1, а  описание  се-
мантики битов атрибутного слова - на рис.2.

   00H  _______________________________________________________
       |  Указатель на следующий драйвер, смещение             |
   02H |-------------------------------------------------------|
       |  Указатель на следующий драйвер, сегмент              |
   04H |-------------------------------------------------------|
       |  Атрибутное слово драйвера                            |
   06H |-------------------------------------------------------|
       |  Точка входа программы стратегий, смещение            |
   08H |-------------------------------------------------------|
       |  Точка входа программы прерываний, смещение           |
   0AH |-------------------------------------------------------|
       |  Логическое имя (8 байтов) для символьного устройства.|
       |  Число элементов (1 байт) для блокового устройства    |
       |  с последующими 7 резервными байтами.                 |
        -------------------------------------------------------

                Рис.1. Стандартный заголовок драйвера


   Бит  Описание
   15   0 - если блоковое устройство
        1 - если символьное устройство
   14   0 - функция IOCTL не поддерживается
        1 - функция IOCTL поддерживается
   13   0 - (если бит 15 = 0) формат IBM
        1 -                   формат, отличный от IBM
        0 - (если бит 15 = 1) не поддерживается режим вывода до сиг-
                              нала занятости (Output-Till-Busy)
        1 -                   режим вывода до сигнала занятости
                              поддерживается
   12   0 - не определен
   11   0 - использовать только вызовы DOS 2.x
        1 - для DOS 3.x поддерживаются вызовы, связанные с открытием/
            закрытием устройства и перемещаемой средой (Open/Close/RM),
            игнорируется для DOS 2.x
   10   0 - не определен
    9   0 - не определен
    8   0 - не определен
    7   0 - не определен
    6   0 - не определен
    5   0 - не определен
    4   0 - не определен
    3   0 - нормальное устройство
        1 - устройство преобразования даты/времени суток
    2   0 - нормальное устройство
        1 - драйвер пустого (NUL) устройства
    1   0 - нормальное устройство
        1 - драйвер стандартного вывода (stdout)
    0   0 - нормальное устройство
        1 - драйвер стандартного ввода (stdin)

            Рис.2. Атрибутное слово драйвера


                Программа стратегий

   Программа стратегий вызывается, когда драйвер устанавливается  при
загрузке  операционной  системы,  а  также при каждом сгенерированном
операционной системой запросе  на  ввод/вывод.  Единичный  запрос  на
ввод/вывод  от прикладной программы может породить несколько запросов
к драйверу.
   Задача данной программы заключается лишь в  том,  чтобы  сохранить
где-нибудь  некоторый адрес для дальнейшей обработки. Этот адрес, пе-
редаваемый в паре регистров ES:BX, указывает на структуру, называемую
заголовком запроса (см. рис.3), которая содержит информацию, сообщаю-
щую драйверу, какую операцию он должен выполнить.

   rlength db  0            ; 0  - длина существенных данных
                            ;      в заголовке
   unit    db  0            ; 1  - номер элемента
                            ;      (в MDM_DRV не используется)
   command db  0            ; 2  - фактическая команда
   status  dw  0            ; 3  - состояние после возврата
   reserve db  8 dup (0)    ; 5  - зарезервировано для DOS
   media   db  0            ; 13 - дескриптор среды
                            ;      (в MDM_DRV не используется)
   address dd  0            ; 14 - длинный указатель для ввода/вывода
   count   dw  0            ; 18 - число символов для ввода/вывода
                            ;      (целое без знака)
   sector  dw  0            ; 20 - начальный сектор
                            ;      (в MDM_DRV не используется)

               Рис.3. Схема заголовка запроса

   Существенно то, что программа стратегий  не  осуществляет  никаких
операций  ввода/вывода,  а лишь сохраняет адрес заголовка запроса для
последующей обработки программой прерываний. В мультизадачной системе
этот адрес должен был бы храниться в некотором массиве,  который  при
последующем  вызове  процедуры прерываний подвергался бы сортировке с
целью оптимального использования устройства. Под  управлением  MS-DOS
программа прерываний вызывается сразу после программы стратегий. Сле-
дует обратить внимание на то, что между вызовами программ стратегий и
прерываний  допустимы прерывания, а это может создать трудности, если
драйвер написан в предположении, что между этими двумя вызовами  про-
ходит "нулевое время".


                 Программа прерываний

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

   Командный байт  Действие
              00H  Инициализировать драйвер.  Вызывается  только  при
                   установке драйвера во время загрузки системы.
              01H  Проверка среды. Возвращает статус, сигнализирующий
                   об  изменении  текущей  среды  (не  используется в
                   MDM_DRV).
              02H  Построить блок параметров BIOS'а (BPB). Вызывается
                   при  создании или изменении среды, этот вызов дол-
                   жен построить BPB и вернуть его адрес (не  исполь-
                   зуется в MDM_DRV).
              03H  Читать  IOCTL. Скопировать определенную информацию
                   для драйвера в локальный буфер.
              04H  Читать. Получить определенное  число  символов  из
                   таблиц драйвера.
              05H  Читать символ без удаления. Получить от устройства
                   следующий символ, не удаляя его из входного буфера.
              06H  Статус ввода. Выдать статус, сигнализирующий о на-
                   личии символа во входном буфере.
              07H  Сброс ввода. Очистить входной буфер устройства.
              08H  Писать.  Передать  устройству  определенное  число
                   символов.
              09H  Писать с проверкой. Вывести  символы,  затем  про-
                   честь их для котроля корректности вывода.
              0AH  Статус  вывода.  Выдать  статус, сигнализирующий о
                   том, свободно ли устройство для вывода.
              0BH  Сбросить буфера вывода. Очистить обслуживающие ус-
                   тройство буфера вывода.
              0CH  Писать IOCTL. Скопировать определенную  информацию
                   для  драйвера из локального буфера в таблицы драй-
                   вера.
              0DH  Открыть устройство (только для DOS 3.x). Инициали-
                   зировать устройство.
              0EH  Закрыть устройство (только для DOS 3.x). Вызывает-
                   ся перед закрытием файлов.
              0FH  Программа обработки перемещаемой среды (только для
                   DOS 3.x). Только для блоковых устройств.
              10H  Вывод  до  сигнала занятости (только для DOS 3.x).
                   Ненормальный вариант обычной функции  вывода,  до-
                   пускающий "неполный" вывод.

            Рис.4. Командные байты заголовка запроса и их значения


   Программа прерываний обычно использует командный байт  в  качестве
индекса для некоторой управляющей таблицы, вызывая таким образом нуж-
ную  процедуру для каждой команды. Конечно, при желании можно исполь-
зовать таблицу переходов. Заголовок запроса содержит всю  необходимую
информацию для корректной обработки каждой команды и сообщает вызыва-
ющей программе (в большинстве случаев таковой является MS-DOS) о сос-
тоянии  запроса  после  завершения  соответствующей процедуры. Слово,
хранящее состояние после возврата, разбито на  несколько  полей  (см.
рис.5).  Оно содержит бит ошибки, указывающий на то, что в оставшейся
части содержится специфический код ошибки, бит выполнения, сигнализи-
рующий о том, что требуемая операция была завершена, и бит занятости,
призванный в первую очередь сигнализировать о текущем состоянии  уст-
ройства.

   15   0  нет ошибки
        1  ошибка
   14      зарезервирован
   13      зарезервирован
   12      зарезервирован
   11      зарезервирован
   10      зарезервирован
    9   1  устройство занято
    8   1  операция завершена (если появилось ошибка,
                               данный бит игнорируется)
   0-7  специфический код ошибки:
        0H  нарушение защиты записи
        1H  запрошен неизвестный элемент
        2H  дисковод не готов
        3H  команда не распознана
        4H  ошибка в данных при чтении
        5H  структура запроса на ввод/вывод некорректна
        6H  ошибка при поиске
        7H  возникла нераспознаваемая среда
        8H  запрашиваемый сектор не найден
        9H  сбой принтера (сбой подачи бумаги)
        AH  сбой устройства при записи
        BH  сбой устройства при чтении
        CH  общий сбой устройства
        DH  зарезерверовано
        EH  зарезерверовано
        FH  изменение среды недопустимо

        Рис.5. Слово в драйвере, хранящее состояние после возврата

   Вообще  говоря,  недостаточно  знать лишь состояние операции после
возврата. В тех случаях, когда это имеет смысл, например, при  опера-
циях  чтения или записи, необходиммо также вернуть число обработанных
символов.
   Несмотря  на  то,  что  программа прерываний драйвера вызывается в
действительности не так, как это принято для программ  прерываний,  -
она  вызывается  с  помощью  FAR  CALL  и  таким образом предполагает
FAR RET, - она должна действовать, как настоящая  программа обработки
прерывания,  т.е.  сохранить  все  регистры и флаги и восстановить их
после завершения.


                          Команды

   Команды драйвера, перечисленные на рис.4, встроены в  MDM_DRV  как
вызываемые подпрограммы. Управляющая таблица, расположенная в ассемб-
лерном листинге непосредственно вслед за заголовком запроса, указыва-
ет на необходимые драйверу подпрограммы. Доступ к таблице осуществля-
ет  программа  прерываний,  которая для определения подлежащей вызову
подпрограммы использует в качестве индекса входа в эту таблицу коман-
дный байт из заголовка запроса.
   После  написания программ стратегий и прерываний необходимо реали-
зовать все команды. Если заглянуть в листинг, видно, что здесь  ника-
ких  хитростей нет. Рассматриваемые ниже команды следуют в листинге в
том же порядке. Такая структура может послужить базисной моделью  для
практически  любого  драйвера независимо от его сложности. Дальнейшую
информацию о написании драйверов см. в  "Техническом  руководстве  по
MS-DOS"  либо в книге: Ray Duncan "Advanced MS-DOS" (Microsoft Press,
1986).
   Не  все допустимые команды используются в MDM_DRV, однако для пол-
ноты в программном коде на них имеются ссылки. Неиспользуемые  коман-
ды  - это просто заглушки, которые, ничего не выполняя,  осуществляют
возврат.
   Все используемые команды подчинены следующей общей логике: они вы-
зываются из программы прерываний путем NEAR CALL,  осуществляют  свои
действия  и возвращают состояние в регистре AX. Это состояние склады-
вается с битом выполнения и засылается в исходный  заголовок  запроса
(RH) по адресу RH+3. Если для данной операции требуется счетчик числа
символов, то он модифицируется внутри команды. Если некоторая подпро-
грамма в данном драйвере  не используется,  она возвращает состояние,
сигнализизующее о завершении соответствующей операции.

   ИНИЦИАЛИЗИРОВАТЬ  ДРАЙВЕР  (команда  00H).  Эта команда вызывается
лишь один раз при загрузке драйвера. Она должна инициализировать уст-
ройство так, как это необходимо для дальнейшей работы, и может,  если
это  необходимо, послать приветственное сообщение об успешной загруз-
ке. На этапе разработки драйвера для  удобства  отладки  можно  также
попросить ее выдать адрес своего кодового сегмента.
   Вызов функций MS-DOS допустим лишь через INT 21H с  регистром  AH,
не  превосходящим 0CH, либо с AH равным 30H для получения номера вер-
сии MS-DOS. Так как некоторые подпрограммы зависят от версии  MS-DOS,
может  потребоваться проверка номера версии и при некорректной версии
установка некоторых флагов либо осуществление определенных действий.
   Существуют специфические операции для блоковых устройств, не расс-
матриваемые в данной статье; они касаются установки определенных ста-
тических таблиц, в которых MS-DOS получает  информацию  об  атрибутах
блоковых устройств, обслуживаемых драйвером.
   При вызове команды "ИНИЦИАЛИЗИРОВАТЬ  ДРАЙВЕР"  4-байтовый  указа-
тель, размещенный по адресу RH+18 (RH - заголовок запроса), указывает
на  тот  текст,  который  следовал за знаком равенства в строке файла
CONFIG.SYS, требовавшей загрузки драйвера; это позволяет обрабатывать
опции. В драйвере MDM_DRV данная строка высвечивается на экране.
   Самый старший адрес памяти,  требуемой  драйверу,  возвращается  в
двойном  слове, начиная от RH+14, что дает MS-DOS'у информацию о том,
где можно разместить следующий драйвер. Так как подпрограмма  инициа-
лизации вызывается лишь один раз, для экономного использования памяти
в  заголовке запроса можно вернуть ее собственный адрес. Следует пом-
нить, что начиная с этого адреса, память по  всей  вероятности  будет
затерта.
   Драйвер  MDM_DRV использует две функции MS-DOS, доступные лишь на-
чиная с версии 3.0, а именно, процедуры открытия и закрытия устройст-
ва. Более ранние версии MS-DOS не имели таких функций, а так как  они
необходимы  для  работы драйвера, в случае некорректной версии прихо-
дится прекратить работу.
   Драйвер MDM_DRV использует подпрограмму инициализации для проверки
того, что загружен MS-DOS 3.x, а в противном случае выводит сообщение
об  ошибке  и  прекращает  работу.  Он  инициализирует  порт модема к
1200 бод с отсутствием контроля четности, восемью битами данных и би-
том остановки.
   В  случае некорректной версии MS-DOS можно было бы и не прекращать
работу. Немного подправив программу, можно добиться того, чтобы  пер-
вый  вызов подпрограмм чтения или записи мог бы вызывать подпрограмму
открытия устройства, а последний вызов либо  надлежащая  пауза  после
возврата  из  чтения/записи  могли бы запускать подпрограмму закрытия
устройства. Я предпочел не писать этот драйвер для устаревших  версий
MS-DOS  2.x;  всем  нужно  когда-нибудь обновляться. Наконец выдается
приветственное сообщение, и подпрограмма инициализации заканчивается.

   ОТКРЫТЬ  УСТРОЙСТВО (команда 0DH). Под управлением MS-DOS назначе-
ние этой команды не определено, но тем не менее MS-DOS 3.x обязатель-
но вызовет эту команду раньше любой другой подпрограммы, если включен
соответствующий бит в атрибутном слове (бит 11). Драйвер MDM_DRV  ис-
пользует эту команду с несколькими целями.
   При первом вызове данная команда повторно инициализирует  коммуни-
кационный порт к принятым по умолчанию параметрам: 1200,n,8,1 (в дей-
ствительности  эти  параметры  могут быть в дальнейшем изменены с по-
мощью соответствующих вызовов функции IOCTL). Затем проверяется  сос-
тояние  линии  связи с тем, чтобы определить, является ли данный порт
активным и связан ли он уже с другим компьютером. Если уже достигнуто
состояние готовности, приводимая ниже  серия  вызовов  и/или  ответов
опускается. Если же связи нет, на экран выдается сообщение с просьбой
ввести  телефонный  номер  для  вызова. В зависимости от того, был ли
введен телефонный номер или же что-нибудь иное, модем либо  осуществ-
ляет вызов, либо посылает строку ответа, после чего переходит в режим
ожидания. Немедленный выход происходит по Ctrl-C.
   После  проверки  линии связи вызывается несложная коммуникационная
программа, которая посылает символы, вводимые с  клавиатуры,  как  на
экран,  так и в коммуникационный порт, а символы, обнаруженные в ком-
муникационном порту, посылаются непосредственно на экран. Если на од-
ном из концов линии связи вводится сигнал выхода (escape), обе сторо-
ны покидают этот цикл и продолжают работу. После этого сигнала,  либо
если  линия  связи с самого начала уже была готова, принимается квант
времени таймера на другом конце линии, т.к. настройка таймера критич-
на для протокола X-модема, используемого последующими вызовами чтения
и записи. Наконец управление возвращается вызвавшей программе.

   ЧТЕНИЕ (команда 04H). Это наиболее сложная подпрограмма в драйвере
MDM_DRV ввиду ее двойственного характера.
   Программа,  вызвавшая  драйвер (в больщинстве случаев это MS-DOS),
просит прочитать определенное  число  символов.  Фактический  счетчик
символов расположен по адресу RH+18. Эти символы возвращаются вызвав-
шей программе посредством засылки по адресу, хранящемуся в 4-байтовом
указателе RH+14.
   Однако  X-модем передает данные фиксированными блоками из 128 бай-
тов данных с некоторой дополнительной протокольной  информацией.  Для
того,  чтобы  понять,  был ли блок правильно получен, он предполагает
через определенное время после посылки блока застать выданный получа-
телем символ ACK (правильно) либо NAK (неправильно). Рассмотрим ситу-
ацию, при которой каждый раз делается запрос на один символ. Для пер-
вого вызова нет ожидающих его данных, поэтому он должен ждать прихода
целого блока. Если этот блок не проходит проверку в том  смысле,  что
номер  блока,  его  дополнение и байт контрольной суммы не такие, как
ожидалось, либо передача блока не уложилась по времени, то немедленно
посылается символ NAK, и цикл получения блока  начинается  с  начала.
Если  блок  проходит проверку, символ ACK не может быть послан до тех
пор, пока устройство не будет готово для  приема  следующего  полного
блока.  Однако,  если  была установлена программа TSRCOMM, символ ACK
можно посылать. (TSRCOMM допускает  асинхронные  прерывания,  поэтому
принимаемые  символы  будут обработаны корректно). Если же обработчик
асинхронного прерывания не установлен,  то  немедленная  посылка  ACK
приведет  к потере символов. Таким образом ACK нельзя посылать сразу;
этот сигнал не посылается до тех пор, пока последний символ  в  блоке
не  будет  должным  образом обработан и сохранен в надлежащей позиции
приемного буфера вызывающей программы. Это означает,  что  дальнейшие
запросы  на  следующий байт должны быть обработаны с учетом того, что
этот байт находится в полученном ранее блоке.
   При  этом  возникают проблемы, если обработка 128 байтов запросами
драйвера на ввод/вывод занимает больше времени, чем это допустимо для
задержки между посылкой последнего символа и приходом ACK либо NAK.
   А что происходит в случае, когда запрашивается сразу большое число
символов? Как и ранее, подпрограмма чтения должна ждать прихода цело-
го блока, а затем до возврата в вызвавшую программу обработать столь-
ко символов, сколько допускает счетчик символов из заголовка запроса.
Если число запрошенных байтов превышает число символов,  имеющихся  в
буфере,  подпрограмма остается в цикле, отвечающем за получение блока
и обработку символов, до тех пор, пока  не  будет  обработано  нужное
число символов. Подпрограмма отслеживает число обработанных символов,
увеличивая счетчик при пересылке каждого символа.
   После завершения запроса на чтение управление возвращается вызвав-
шей программе. Если слишком много раз возникал отказ (NAK) и передачу
пришлось отменить, управление возвращается с установленным состоянием
ошибки. Управление возвращается без состояния ошибки, но с  укорочен-
ным  счетчиком, если получен сигнал EOT (конец передачи), указывающий
на то, что передача завершена, либо если запрос на чтение пришел пос-
ле получения этого сигнала.

   СТАТУС ВВОДА (команда 06H). Эта команда не имеет большого значения
для данного драйвера, но тем не менее включена в него. Если есть дос-
тупный  символ  (некоторый  блок был получен, но не полностью обрабо-
тан), то бит занятости в слове, хранящем  состояние  после  возврата,
(адрес  RH+3)  устанавливается  в 0. Если только чтение не происходит
порциями, кратными 128 байтов, эта подпрограмма будет  после  первого
запроса на чтение чаще всего возвращать состояние готовности.

   ЧИТАТЬ  СИМВОЛ  БЕЗ УДАЛЕНИЯ (команда 05H). Эта команда возвращает
следующий символ, подлежащий чтению. Символ  возвращается  по  адресу
RH+13,  но  не  удаляется, т.к. это повлияло бы на текущее содержимое
входного буфера. Непонятно, что делать, если в момент вызова этой ко-
манды входной буфер пуст. Поэтому в слово, хранящее состояние, (адрес
RH+3) заносится код ошибки BH (ошибка чтения), счетчику символов (ад-
рес RH+18) присваивается 1, а в качестве символа возвращается  вопро-
сительный знак.

   СБРОС ВВОДА (команда 07H). Вызов этой команды приводит  к  очистке
входного  буфера.  Последующий запрос на чтение при очищенном входном
буфере не уложится в контроль времени, что приведет к посылке сигнала
NAK. Благодаря этому  сигналу  последний  посланный  блок  посылается
вновь, после чего работа продолжается нормально.

   ПИСАТЬ  (команда  08H). Подсистема записи очень проста посравнению
со сложностями подсистемы записи. При запросе на запись число байтов,
подлежащих передаче, хранится по адресу RH+18.  Указатель  на  начало
передаваемых данных хранится в двойном слове по адресу RH+14. Символы
один  за  другим  переносятся в буфер вывода. Программа посылки блока
вызывается в тот момент, когда в буфере находится 128 символов.  Блок
посылается  до  тех  пор, пока либо он не будет успешно принят, о чем
свидетельствует посланный с другого конца сигнал ACK,  либо  пока  не
прийдет  достаточное  число сигналов сбоя (NAK) или нехватки времени,
что свидетельствует об ошибке передачи.
   Когда уже не осталось символов, предназначенных для  передачи,  по
адресу RH+18 заносится число обработанных символов, которое может от-
личаться от первоначально затребованного, после чего управление возв-
ращается вызывающей программе.

   ПИСАТЬ  С ПОВЕРКОЙ (команда 09H). Для символьных устройств, а осо-
бенно для тех, которые используют Xmodem, эта  команда  неосмысленна.
Вообще  говоря, она должна была бы вызвать подпрограмму записи, затем
попытаться прочесть только что записанные данные, сравнить их с  пер-
воначальными  и вернуть ошибку в случае расхожения. В случае драйвера
MDM_DRV вполне достаточно просто вызвать подпрограмму записи.

   СТАТУС ВЫВОДА (команда 0AH). Эта команда должна взвести бит  заня-
тости, если устройство вывода занято. Это невозможно, так как драйвер
MDM_DRV не использует буферизацию и не управляется прерываниями; под-
программа записи никогда не оставляет себе работу на следующий вызов.

   СБРОСИТЬ БУФЕРА ВЫВОДА (команда 0BH). В  драйвере  MDM_DRV  данная
команда  не  делает ничего, т.к. очистка текущего буфера вывода может
негативно повлиять на происходящую передачу.

   ЗАКРЫТЬ УСТРОЙСТВО (команда 0EH). Данная команда, так  же,  как  и
команда открытия устройства, вызывается лишь в том случае, если драй-
вер  работает под управлением MS-DOS 3.x, а в атрибутном слове из за-
головка взведен соответствующий бит. Эта подпрограмма определяет, ос-
тались ли недописанные байты, и при необходимости заставляет програм-
му посылки блока переслать неполный блок. Скорее всего,  недописанные
байты  должны  встречаться,  т.к. подпрограмма записи всегда посылает
блоки по 128 байтов. После того, как по последнему боку принят сигнал
ACK, посылается сигнал EOT с тем, чтобы на другом конце  линии  стало
известно о конце передачи.
   Эта  подпрограмма  вызывается также при срыве передачи и в случае,
если во время передачи с консоли вводится Ctrl-C. Таким образом  дол-
жен  проверяться  флаг  срыва:  если он взведен, то посылается символ
CAN, который сообщает другому концу линии, что передача была  отмене-
на. Конечно, в этом случае посылка недописанных блоков не осуществля-
ется.  Если  это был телефонный вызов, то с 1-секундным интервалом по
отношению к сигналу "внимание", состоящему из трех плюсов (+++),  по-
сылается строка отбоя, совместимая с предлагаемой фирмой Hayes.
   В конце на порт модема посылается символ DTR, перехваченные векто-
ра прерываний устанавливаются в первоначальное положение и управление
возвращается вызвавшей программе.

   ЧИТАТЬ  УПРАВЛЯЮЩУЮ  ИНФОРМАЦИЮ ВВОДА/ВЫВОДА (команда 03H). Данная
команда так же, как и команда записи IOCTL, позволяет передавать выз-
вавшей программе ту или иную информацию в  зависимости  от  состояния
устройства.  Она  не полностью определена, поэтому ее можно использо-
вать для различных целей.
   В драйвере MDM_DRV данная команда передает копию структуры, содер-
жащей информацию, представленную на рис.6. Эта информация, в частнос-
ти, позволяет прикладной программе задать, какой порт  модема  должен
использовать драйвер MDM_DRV. Данная команда предоставляет прикладной
программе  возможность выяснения текущих значений перечисленных пара-
метров.
   Заметим, что счетчик по адресу RH+18 указывает на то, сколько бай-
тов  можно вернуть при таком вызове. Так как для такого вызова обычно
используется буфер ограниченной длины, возврат большего числа  байтов
по указателю, хранящемуся в двойном слове с адресом RH+14, может при-
вести  к записи в область памяти, которая не была корректно распреде-
лена. Часто требуется запись напрямую в буфер  прикладной  программы,
причем  вместо всей структуры запрашивается лишь первое слово, задаю-
щее текущий коммуникационный порт.

   com_port      dw   0          ; текущий коммуникационный порт
                                 ; COM1 = 0, COM2 = 1 и т.д.
   init_data     db   10000011b  ; байт инициализации (см. Техничес-
                                 ; кое руководство по BIOS'у)
   block_num     dw   0          ; текущий пересылаемый блок
   abort_xfer    dw   0          ; нужно сорвать текущую передачу
                                 ; (TRUE означает "ДА")
   nak_cnt       dw   0          ; сколько было послано либо полу-
                                 ; чено символов NAK
   cancel_flag   dw   0          ; было ли нажато Ctrl-C?
   inrequest     dw   0          ; число байтов, не обработанных на
                                 ; текущий момент
   in_block      dw   0          ; был ли получен блок?
   was_dialed    dw   0          ; модем был вызван по телефону

      Рис.6. Структура управляющей информации ввода/вывода
                   для драйвера MDM_DRV

   ЗАПИСАТЬ  УПРАВЛЯЮЩУЮ  ИНФОРМАЦИЮ  ВВОДА/ВЫВОДА (команда 0CH). Так
же, как и предшествующая, данная команда осуществляет обмен  информа-
цией  между  прикладной  прораммой  и  драйвером. Она устанавливает в
драйвере его внутренние параметры.
   Обычно сначала вызывается команда чтения IOCTL в локальный  буфер,
затем  модифицируются  необходимые  байты и вызывается команда записи
IOCTL с адресом локального буфера и числом символов, которые  необхо-
димо изменить.

   Команды  ПРОВЕРКА  СРЕДЫ  (команда 01H), ПОСТРОИТЬ БЛОК ПАРАМЕТРОВ
BIOS'а (команда 02H), ПЕРЕМЕЩАЕМАЯ СРЕДА (команда  0FH)  и  ВЫВОД  ДО
СИГНАЛА  ЗАНЯТОСТИ  (команда 10H) не используются в драйвере MDM_DRV,
однако для полноты они вставлены в драйвер. Обычно в таблице  смеще-
ний используется адрес общей "пустой" подпрограммы.

                Специфика разработки драйвера

   При  проектировании и написании драйверов возникают некоторые спе-
цифические проблемы. Проектирование драйверов во многом сходно с раз-
работкой приложений для системы Windows: главные процедуры вызываются
внешним заданием, возможно, несколько раз, система не продолжит рабо-
ту до тех пор, пока ей не будет возвращено  управление,  а  интерфейс
задан довольно жестко.
   Первая проблема заключается в том, что  вызов  процедуры  открытия
устройства (dev_open) вовсе не гарантирует, что когда-нибудь это уст-
ройство будет закрыто (вызовом dev_close). Ситуация осложняется еще и
тем,  что  даже  простая команда MS-DOS COPY производит серию вызовов
dev_open/dev_close прежде, чем устройство действительно будет открыто
для соответствующей операции ввода/вывода. (Последовательность дейст-
вий может  быть  такой:  dev_open/dev_close/dev_open/[чтение|запись]/
dev_close).
   Другая  проблема,  возникающая при разработке драйверов, связана с
подсчетом повторных попыток. Где-то в дебрях MS-DOS'а существует чис-
ло,  которое,  как предполагается, способно указывать, сколько должен
драйвер сделать попыток прежде, чем он откажется от операции и MS-DOS
выдаст сакраментальное сообщение "Abort, Retry,  Ignore?"  (созданное
прерыванием  критической  ошибки INT 24H). Оказывается, что нет доку-
ментированного способа заставить драйвер делать лишь одну попытку от-
крытия устройства перед генерацией критической ошибки. Таким  образом
не удается изящно выходить из "мертвой" линии.
   Так как вызовы функций MS-DOS за пределами инициализационной части
драйвера не допустимы, для посимвольного ввода/вывода приходится  ис-
пользовать  BIOS (можно также напрямую читать с клавиатуры и выводить
на экран, но это слишком утомительное  занятие).  Это  означает,  что
Ctrl-C  или Ctrl-Break считается просто очередным символом без всяких
специфических атрибутов, тем самым ввод такого символа не приведет  к
желаемому  результату,  т.е.  из  подпрограммы открытия устройства не
удастся выйти обратно в MS-DOS и даже прерывание  критической  ошибки
не будет вызвано.
   Это  действительно очень сложная проблема, т.к. драйверы MS-DOS по
своей природе не должны быть интерактивными. Разобраться в этой ситу-
ации пока не удалось. Тем самым оказывается, что после входа в  драй-
вер по сути дела нет пути назад.
   Но это всего лишь одна из проблем, возникающих тогда, когда MS-DOS
пытаются  заставить  совершать действия, для которых он не предназна-
чен. Вот проблема, касающаяся внешних  программ:  заключается  она  в
том, что MS-DOS всегда открывает драйверы символьных устройств в под-
готовленном  режиме.  Если передается файл, в котором случайно встре-
тится символ Ctrl-Z, этот символ будет воспринят как  конец  файла  и
передаваемый файл будет закрыт преждевременно. В связи с этим необхо-
димо  держать  драйвер  MDM_DRV  в неподготовленном режиме. Для этого
нужно после каждой команды открытия  устройства  специально  вызывать
функцию IOCTL с целью перевода драйвера в неподготовленный режим.
   Установка  неподготовленного режима распространяется только на те-
кущий  дескриптор  файла  (file handle); последующие команды открытия
устройства по-прежнему будут давать подготовленный режим, если только
не запустить небольшую резидентную программу SET_MDM.  Эта  программа
перехватывает  прерывание по вызову MS-DOS (INT 21H), и если приходит
запрос на открытие дескриптора файла (функция"open-with-file-handle")
и при этом имя файла, на которое указывает пара регистров DS:DX,  со-
держит  подстроку 'MDM' (ограниченную нулем), то после успешного отк-
рытия дескриптор файла переводится в  неподготовленный  режим.  Такие
действия не вызывают затруднений у вызывающей программы и вполне кор-
ректны, хотя и являются очень грубыми.
   У каждого программиста должна быть некоторая эталонная плохо ском-
понованная  программа (kludge) для сравнения; в качестве нового стан-
дарта можно взять SET_MDM. Но в конце концов, если программа  работа-
ет, ею можно пользоваться, как бы плохо она ни была написана.

                      Возможные улучшения

   Так  как  MDM_DRV  -  это  главным образом пример функционирования
драйвера, его программный код не причесан и в нем есть места, требую-
щие улучшения.
   Так,  например,  подпрограмма  get_num не анализирует, является ли
введенный с клавиатуры символ корректным символом в коде  ASCII;  эта
подпрограмма  просто  ожидает,  пока не будет нажат возврата каретки,
либо пока не будет введено двадцать  каких-нибудь  символов.  Неплохо
было  бы, как это обсуждалось выше, иметь возможность изящно выходить
из этой подпрограммы (или из какой-нибудь другой подпрограммы драйве-
ра). Далее, драйвер считает, что к коммуникационному порту  подключен
Hayes-совместимый модем, и это предположение существенно используется
подпрограммами открытия (dev_open) и закрытия (dev_close) устройства.
Если  драйвер MDM_DRV будет использован для других модемов или же для
других устройств, посаженных на последовательный порт,  его  придется
немного дополнить, возможно, вызовами функции IOCTL.
   В  данном  драйвере используется простой протокол X-модема с конт-
рольной суммой. Хорошим дополнением  к  нему  было  бы  использование
улучшенной  версии  этого протокола, т.н. протокола X-модема с цикли-
ческим контролем по избыточности (CRC). Для реализации такого улучше-
ния требуется совсем немного дополнительного программного кода.
   Сигналы аварийного снятия (CAN) и конца передачи (EOT) обрабатыва-
ются не так, как это указано в спецификациях ввиду того, что  реакция
на  эти сигналы наступает сразу после их прихода. Согласно специфика-
циям, первый символ CAN или EOT должен вызывать посылку сигнала NAK с
целью проверки того, что пришедший символ не является шумом,  и  лишь
после повторного приема осуществляются надлежащие действия.

                             Отладка

   Разработка  любой  программы,  решающей достаточно сложную задачу,
требует терпения и внимательности. Отлаживать и улучшать драйверы, не
имея соответствующих средств отладки, довольно трудно.  К  сожалению,
готовых средств отладки драйверов нет; их приходится создавать самому
разработчику.
   Прежде, чем начинать отлаживать драйвер, необходимо создать полную
резервную  копию  и  убедиться в наличии средств загрузки системы без
автоматической установки данного драйвера. Можно,  например,  держать
CONFIG.SYS на системном гибком диске и при тестировании драйвера заг-
ружаться  с него. Ниже перечислены некоторые приемы и идеи, использо-
ванные при создании драйвера MDM_DRV.
   Постройте драйвер как обычный COM-файл и создайте программу, кото-
рая бы имитировала вызовы всех его команд и позволяла бы  исследовать
регистры  и  заголовок  запроса каким-нибудь отладчиком. Такой подход
несколько скучен и не позволяет тестировать драйвер в "реальных"  си-
еуациях,  однако это хорошее начало, позволяющее в дальнейшем перейти
к следующему методу.
   Создайте скелет программы без процедуры инициализации. Пусть прог-
рамма прерываний состоит лишь из засылки в пару регистров адреса  за-
головка запроса, который был сохранен программой стратегий. Затем вы-
зовите зарезервированное Вами прерывание. Вот пример простой програм-
мы прерываний:

   interrupt proc far
      push  es
      push  bx
      mov   es, cs:[old_segment]
      mov   bx, cs:[old_offset]
      int   60h
      pop   bx
      pop   es
      ret
   interrupt endp

   Конечно,  подразумевается,  что есть некоторая процедура обработки
прерывания, котора позже  будет  посажена  на  выбранное  прерывание.
Предлагается  использовать  прерывание  60H,  которое  определено как
"пользовательское". Может пригодиться такой кусок кода:

   int60 proc far
      call real_int_routine
      iret
   int60 endp

Это позволяет строить real_int_routine как FAR-процедуру с  FAR-возв-
ратом.
   Смысл всех этих ухищрений заключается в том, чтобы  сделать  рези-
дентную программу, которая бы просто перехватывала указанное прерыва-
ние,  а  затем  вела  бы себя так же, как драйвер. После отладки этой
программы как обычной резидентной (что позволяет  делать  большинство
отладчиков, включая CodeView) готовый код можно перебросить в настоя-
щий драйвер, и этим сделано все, кроме подпрограммы инициализации.
   Аппаратно поддерживаемый отладчик Periscope легко позволяет связы-
вать  смещения переменных, объявленных "public", с любым кодовым сег-
ментом. Если подпрограмма инициализации при загрузке  сообщает  адрес
своего  кодового сегмента, нетрудно создать "пустую" программу, кото-
рая загружается в отладчик с надлежащей символьной  таблицей  (symbol
map),  а  затем  изменить  сегмент символов на первоначальный кодовый
сегмент  драйвера.  Начиная  с  этого  момента, все  происходит  так,
как-будто Вы отлаживаете "обычную" программу.
   Помните, что при начальном вызове драйвера ему доступна лишь очень
маленькая область стекового пространства. В действительности нет воз-
можности определить, какой объем памяти доступен драйверу. Лучше все-
го  сохранить  указатель  стека  и его сегмент, затем воспользоваться
своим собственным локальным стеком, а в конце  воостановить  первона-
чальный стек. Драйвер MDM_DRV делает это в программе прерываний.
   Помните, что все регистры (включая  регистр  флагов)  должны  быть
восстановлены перед выходом из драйвера. В критических местах при не-
обходимости драйвер должен работать с отключенными прерываниями.
   Необходимо твердо убедиться в том, что счетчик символов показывает
истинное число переданных байтов. Неверное значение счетчика приводит
к очень странным результатам.
   При  малейших сомнениях убедитесь в том, что драйвер не обращается
к памяти, лежащей за пределами начального адреса,  переданного  подп-
рограммой инициализации.


        Приложение. Выборочные фрагменты программы MDM_DRV.ASM

;;         MDM_DRV.ASM      Драйвер для X-модема,
;;                          написанный Россом М. Гринбергом
;;         ниже следуют выборочные фрагменты программы
                           .
                           .
                           .
code       segment                public           'CODE'
;;         Сам драйвер - это большая программа с именем 'driver'

driver     proc   far
           assume cs:code, ds:code, es:code  ; сегментные регистры
                                             ; должны указывать на
                                             ; сегмент code
           org    0                          ; драйвер должен начи-
                                             ; наться со смещения 0

;;                ЗАГОЛОВОК ДРАЙВЕРА

header1    dd     -1          ; требуется начальное значение -1,
                              ; это поле заполняет DOS
           dw     0e800h      ; символьное устройство,
                              ; поддерживается функция IOCTL,
                              ; режим вывода до сигнала занятости и
                              ; вызовы, связанные с открытием/закры-
                              ; тием устройства и перемещаемой средой
           dw     strat       ; указатель на программу стратегий
           dw     ints        ; указатель на программу прерываний
           db     'MDM     '  ; имя устройства, выравненное влево и
                              ; и дополненное пробелами до 8 символов

;;                ЗАГОЛОВОК ЗАПРОСА

request    struc
rlength    db     0           ; 0 - длина данных в заголовке
unit       db     0           ; 1 - номер элемента (не используется)
command    db     0           ; 2  - фактическая команда
status     dw     0           ; 3  - состояние после возврата
reserve    db     8 dup (0)   ; 5  - зарезервировано для DOS
media      db     0           ; 13 - дескриптор среды (не исп.)
address    dd     0           ; 14 - указатель для ввода/вывода
count      dw     0           ; 18 - число символов для ввода/вывода
sector     dw     0           ; 20 - начальный сектор (не исп.)
request    ends

;;                Таблица смещений

dispatch:
           dw     init        ; 0x00 - инициализировать драйвер
           dw     media_chk   ; 0x01 - проверка среды (не исп.)
           dw     bid_bpb     ; 0x02 - построить BPB (не исп.)
           dw     rd_ioctl    ; 0x03 - читать IOCTL
           dw     read        ; 0x04 - читать 'count' символов
           dw     nd_read     ; 0x05 - читать символ без удаления
           dw     inp_stat    ; 0x06 - статус ввода
           dw     inp_flush   ; 0x07 - cброс ввода
           dw     write       ; 0x08 - передать 'count' символов
           dw     write_vfy   ; 0x09 - передать с проверкой
           dw     out_stat    ; 0x0A - статус вывода
           dw     out_flush   ; 0x0B - сбросить буфера вывода
           dw     wrt_ioctl   ; 0x0C - записать IOCTL
           dw     dev_open    ; 0x0D - открыть устройство (DOS 3.x)
           dw     dev_close   ; 0x0E - закрыть устройство (3.x)
           dw     rem_media   ; 0x0F - обработка перемещаемой среды
                              ;        (DOS 3.x)
           dw     out_busy    ; 0x10 - вывод до сигнала занятости
                              ;        (DOS 3.x).

;;                ПРОГРАММА СТРАТЕГИЙ

strat      proc   far
           mov    word ptr cs:[rh_ptr], bx
           mov    word ptr cs:[rh_ptr+2], es
           ret
strat      endp
                           .
                           .
                           .
;;                ПРОГРАММА ПРЕРЫВАНИЙ

ints       proc   far
           cli
           mov    cs:[old_stack], sp
           mov    cs:[old_stack+2], ss
           mov    sp, cs
           mov    ss, sp
           mov    sp, offset cs:new_stack_end
           sti
           PUSHALL
           push   cs
           pop    ds
           les    di, cs:[rh_ptr]
           mov    bl, es:[di.command]
           xor    bh, bh
           cmp    bx, MAX_CMD
           jle    ints1
           mov    ax, ERROR+UNK_COMMAND
           jmp    ints2
ints1:
           shl    bx, 1
           call   word ptr dispatch[bx]
           les    di, cs:[rh_ptr]
ints2:
           or     ax, DONE              ; установить в структуре
           mov    es:[di.status], ax    ; флаг выполнения
           POPALL
           cli
           mov    ss, cs:[old_stack+2]
           mov    sp, cs:[old_stack]
           sti
           ret
ints       endp

;;                ПРОГРАММА ЧТЕНИЯ

read       proc   near
           DO_PRINT         msg_read
           mov    ax, es:[di.count]
           mov    cs:[inrequest], ax      ; сохранить запрашиваемое
                                          ; число символов
           xor    ax, ax                  ; число переданных
                                          ; символов = 0
           mov    es:[di.count], ax       ; обнулить счетчик символов
                                          ; в заголовке запроса
           lds    bx, es:[di.address]     ; ds:bx указывает на данные
top_read:
           cmp    cs:[in_block], TRUE     ; вошли в блок?
           jnz    lp_it
           jmp    read_loop               ; да
lp_it:
           mov    cs:[_incnt], FALSE
           mov    si, offset cs:[_soh]    ; указатель на начало блока
           mov    cs:[timer], TEN_SECONDS ; установка таймера
           mov    cs:[nak_cnt], FALSE     ; установка отсутствия NAK
           cmp    cs:[block_num], 1       ; в первый раз?
           jnz    rd_blk                  ; нет
           call   send_nak                ; послать первый NAK
           STAT   LEFT_BRACKET            ; выдать '['
rd_blk:
           call   get_char                ; есть символы?
           jc     rd_blk2                 ; нет
           cmp    cs:[_incnt], FAULSE     ; первый символ
           jnz    nxt_char                ; нет
           cmp    al, SOH                 ; первый символ - заголовок
                                          ; блока (SOH)?
           jz     nxt_char                ; да, сохранить его
           cmp    al, ABORT               ; прекратить передачу?
           jz     abort_it                ; да, прекратить
           cmp    al, EOT                 ; конец?
           jnz    rd_blk                  ; нет, игнорировать символ
           call   send_ack                ; обработать EOF
           STAT   RIGHT_BRACKET           ; выдать ']'
           STAT   CR                      ; и пустую строку
           STAT   LF
           xor    ax, ax
           ret
nxt_char:
           mov    cs:[si], al             ; сохранить
           inc    cs:[_incnt]             ; подправить счетчик
           inc    si                      ; и указатель
rd_blk2:
           cmp    cs:[_incnt], FULLBLKSIZE; поступило достаточно
           jge    chkblk                  ; символов для проверки?
           call   eat_char                ; для Ctrl-C
           cmp    cs:[timer], FALSE       ; прошло положенное время?
           jnz    rd_blk                  ; нет, пробуйте снова
           STAT   DOT                     ; выдать точку
           jmp    bad_blk2
bad_blk:
           STAT   QUESTION                ; выдать знак вопроса
bad_blk2:
           call   send_nak                ; да, послать NAK
           cmp    word ptr cs:[abort], TRUE ; прервать?
           jnz    rd_blk3                 ; нет, заново установить
                                          ; таймер и пробовать снова
abort_it:
           call   send_abort              ; сообщить о прекращении
           call   send_abort              ; дважды
           STAT   EXCLAIM                 ; заключительное восклицание
           mov    ax, 800ch               ; отметить ошибку
           ret
rd_blk3:
           mov    cs:[timer], TEN_SECONDS ; установить таймер заново
           jmp    rd_blk                  ; и пробовать снова
           ret
chkblk:
           mov    ax, cs:[block_num]
           push   ax
           dec    al                      ; временно
           cmp    al, cs:[_blk1]          ; продублировать блок?
           jnz    real_blk                ; нет
           STAT   DUP_BLK                 ; да
           xor    ax, ax
           mov    cs:[_incnt], ax
           jmp    ack_blk
real_blk:
           pop    ax                   ; вернуться к корректному блоку
           cmp    cs:[_blk1], al          ; счетчик в порядке?
           jnz    bad_blk                 ; нет
           not    al
           cmp    cs:[_blk2], al          ; а дополнение блока?
           jnz    bad_blk                 ; нет
           mov    cx, BLKSIZE
           mov    si, offset _buf
           call   do_chksum
           cmp    cs:[_chksum], al
           jnz    bad_blk                 ; неверная контрольная сумма
           mov    cs:[in_block], TRUE     ; похоже, все нормально!
           mov    cs:[_inptr], offset cs:_buf ; установить указатель
           sub    cs:[_incnt], FULLBLKSIZE-BLKSIZE
read_loop:
           mov    si, cs:[_inptr]         ; установить указатель заново
rd_loop:
           mov    al, cs:[si]             ; прочесть символ
           mov    ds:[bx], al             ; и сохранить его
           inc    bx                      ; поднять указатели
           inc    si
           inc    word ptr es:[di.count]  ; увеличить счетчик
           dec    cs:[_incnt]             ; уменьшить число оставшихся
           jnz    stf_nxt_char            ; символов
           inc    cs:[block_num]
           STAT   STAR                    ; выдать '*'
ack_blk:
           call   send_ack                ; выдать ACK
           mov    cs:[in_block], FALSE    ; кончились символы
           jmp    top_read
stf_nxt_char:
           dec    cs:[inrequest]          ; продолжать?
           jnz    rd_loop
           xor    ax, ax                  ; нет больше запросов
           ret                            ; нет ошибок
read       endp

;;                ПРОГРАММА ЗАПИСИ

write      proc   near
           PUSHALL_AX
           DO_PRINT         msg_write
           mov    cx, es:[di.count]       ; сколько байтов?
           xor    dx, dx                  ; обнулить счетчик
           lds    bx, es:[di.address]     ; ds:bx указывает на данные
           mov    si, cs:[_outptr]        ; куда переносить символы
wr_lp:
           mov    al, ds:[bx]             ; взять символ
           mov    cs:[si], al             ; и сохранить в буфере
           inc    bx                      ; сдвинуть указатель
           inc    cs:[_outcnt]            ; общий счетчик символов
           inc    dx                      ; текущий счетчик символов
           cmp    cs:[_outcnt], BLKSIZE   ; послать блок?
           jnz    wr_ok                   ; пока не надо
           call   send_block              ; послать блок
           cmp    cs:[abort_xfer], FALSE  ; прекратить передачу?
           jz     blk_ok                  ; нет, блок послан нормально
           call   send_abort              ; да
           call   send_abort              ; дважды!
           mov    es:[di.count], FALSE    ; ничего не послано
           mov    ax, ERROR+WRITE_ERROR   ; отметить ошибку передачи
           ret
blk_ok:
           mov    cs:[_outptr], offset _buf;установить заново указатель
           mov    si, offset _buf         ; регистр
           mov    cs:[_outcnt], FALSE     ; и число символов
wr_ok:
           inc    si                      ; указать на след. символ
           loop   wr_lp
           mov    es:[di.count], dx       ; сколько послано нормально
           mov    cs:[_outptr], si        ; сохранить
           xor    ax, ax                  ; нет ошибок
           POPALL_AX
           ret
write      endp

;;                ПРОГРАММА ОТКРЫТИЯ УСТРОЙСТВА

dev_open   proc   near
           PUSHALL_AX
           DO_PRINT         msg_dev_open
           mov    cs:[cancel_flag], FALSE
           mov    cs:[_inptr], offset cs:_soh
           mov    cs:[_incnt], FALSE
           mov    cs:[_outptr], offset cs:_buf
           mov    cs:[_outcnt], FALSE
           mov    cs:[block_num_outcnt], 1
           call   set_timer
           mov    dx, cs:[com_port]       ; инициализ. комм. порт
           mov    al, cs:[init_data]
           mov    ah, 0
           int    14h
eat_loop:
           mov    dx, ds:[com_port]
           mov    ah, 3                   ; установить состояние
           int    14h
           test   ah, 1                   ; есть готовые данные?
           jz     continue                ; нет
           mov    ah, 2
           int    14h                     ; захватить символ и
           jmp    eat_loop                ; отправиться за следующим
continue:
           test   al, 080h                ; линия готова
           jz     off_line
           jmp    on_line                 ; да
off_line:
           mov    dx, offset cs:offline   ; выдать сообщение
           mov    ah, 9
           int    21h
           mov    si, offset cs:numbuf
           mov    cx, 19                  ; максимальная длина
           call   get_num
           cmp    cs:[kb_len], 0          ; просто возврат?
           jnz    dial_it
           mov    dx, offset cs:await
           mov    ah, 9
           int    21h
           mov    si, offset cs:answerstring
           call   out_string
           mov    cs:[timer], FOREVER
           jmp    offline_lp              ; ожидать линию
dial_it:
           mov    dx, offset cs:dialing
           mov    ah, 9
           int    21h
           mov    si, offset cs:dialstring
           call   outstring
           mov    si, offset cs:numbuf
           call   outstring
           mov    si, offset cs:return
           call   outstring
           mov    cs:[timer], ONE_MINUTE
           mov    cs:[was_dialed], TRUE   ; отметка телефонного вызова
offline_lp:
           mov    ah, 3                   ; онлайн?
           mov    dx, cs:[com_port]
           int    14h
           test   al, 080h                ; линия готова?
           jnz    made_con                ; да
           call   eat_char                ; для Ctrl-C
           cmp    cs:[timer], 0           ; прошло положенное время
           jnz    offline_lp              ; нет
abort_call:
           mov    cs:[was_dialed], FALSE
           mov    dx, offset cs:no_con
           mov    ah, 9
           int    21h
           xor    ax, ax
           mov    es:[di.count], ax       ; обнулить счетчик
           mov    ax, ERROR+GEN_FAILURE   ; отметить ошибку
           POPALL_AX
           ret
made_con:
           mov    dx, offset cs:con
           mov    ah, 9
           int    21h
term_em_lp:
           call   get_char
           jc     get_term_char
           cmp    al, ESCAPE
           jz     term_exit               ; выход!
           mov    ah, 02h
           mov    dl, al
           int    21h
get_term_char:
           mov    ah, 0bh
           int    21h                     ; есть символы?
           or     al, al
           jz     term_em_lp              ; нет
           mov    ah, 1                   ; да, взять символ
           int    21h
           mov    ah, 1
           mov    dx, ds:[com_port]
           int    14h
           cmp    si, ESCAPE
           jz     term_exit               ; выход!
           mov    ah, 02h
           mov    dl, al
           int    21h
           jmp    get_term_char
term_exit:
           mov    dx, offset cs:xfer      ; сообщение о состоянии
           mov    ah, 9
           int    21h
on_line:
           xor    ax, ax
           POPALL_AX
           ret
dev_open   endp

;;                ПРОГРАММА ЗАКРЫТИЯ УСТРОЙСТВА

dev_close  proc   near
           DO_PRINT         msg_dev_close
           cmp    cs:[cansel_flag], TRUE  ; была отмена?
           jnz    no_cancel               ; нет
           mov    ah, 1                   ; послать сигнал прекращения
           mov    dx, cs:[com_port]
           mov    al, ABORT
           int    14h
           jmp    no_send                 ; и нормально выйти
no_cancel:
           cmp    cs:[_outcnt], FALSE     ; остались символы?
           jz     no_send                 ; нет
           call   send_block              ; да, послать оставшиеся
           STAT   RIGHT_BRACKET
           STAT   CR
           STAT   LF
           mov    cs:[_outptr], offset _buf;установить заново указатель
           mov    si, offset _buf         ; регистр
           mov    cs:[_outcnt], FALSE     ; и число символов
           inc    cs:[block_num]
no_send:
           cmp    cs:[block_num], 1       ; хоть один блок послан?
           jz     no_blk_sent             ; нет
           mov    ah, 1                   ; послать конец передачи(EOT)
           mov    dx, cs:[com_port]
           mov    al, EOT
           int    14h
no_blk_sent:
           cmp    cs:[was_dialed], TRUE   ; вызов мы осуществляли?
           jnz    no_hang
           mov    cs:[timer], ONE_SECOND  ; подождать 1 секунду
sil_lp1:
           call   set_char                ; для Ctrl-C
           cmp    cs:[timer], FALSE       ; время истекло?
           jnz    sil_lp1
           mov    si, offset cs:pluses    ; получить атрибуты модемов
           call   out_string
           mov    cs:[timer], ONE_SECOND  ; подождать 1 секунду
sil_lp2:
           call   eat_char                ; для Ctrl-C
           cmp    cs:[timer], FALSE       ; время истекло?
           jnz    sil_lp2
           mov    si, offset cs:hangup    ; отбой
           call   out_string
no_hang:
           mov    cs:[was_dialed], FALSE
           mov    cs:[cancel_flag], FALSE
           call   reset_timer
           ret
dev_close  endp

;;      ПРОГРАММА ЧТЕНИЯ БЕЗ УДАЛЕНИЯ     <---+
;;      ПРОГРАММА ВЫДАЧИ СОСТОЯНИЯ ВВОДА       |
;;      ПРОГРАММА СБРОСА БУФЕРОВ ВВОДА         |   В полном листинге
;;      ПРОГРАММА ВЫДАЧИ СОСТОЯНИЯ ВЫВОДА      |-> программы эти
;;      ПРОГРАММА СБРОСА БУФЕРОВ ВЫВОДА        |   процедуры располо-
;;      ПРОГРАММА ЧТЕНИЯ IOCTL                 |   жены здесь
;;      ПРОГРАММА ЗАПИСИ IOCTL                 |
;;      ПРОГРАММА ПОСЫЛКИ БЛОКА           <---+

                        .
                        .
                        .
;;         ПРОГРАММА ИНИЦИАЛИЗАЦИИ ДРАЙВЕРА

init       proc   near
           DO_PRINT           msg_init
           mov    ah, 030h                ; узнать версию DOS
           int    21h
           cmp    ah, 3                   ; версия 3.x?
           jge    okay_dos                ; да, не ниже
           mov    dx, offset cs:wrong_dos ; выдать сообщение
           mov    ah, 9
           int    21h
endless_loop:
           cli
           jmp    enless_loop             ; безобразно, но эффективно!
okay_dos:
           mov    dx, offset cs:greetings ; выдать сообщение
           mov    ah, 9
           int    21h
           push   ds
           mov    ds, es:[[di.count+2]    ; получить сегмент строки
                                          ; файла CONFIG.SYS
           mov    si, es:[[di.count]      ; получить смещение строки
                                          ; файла CONFIG.SYS
           call   output_chars
           pop    ds
           mov    dx, offset cs:end_greetings ; выдать сообщение
           mov    ah, 9
           int    21h
           mov    word ptr es:[di.address], offset init
           mov    word ptr es:[di.address+2], cs
           xor    ax, ax
           ret
init       endp

wrong_dos  db  '??Must run this driver under DOS 3.0 or higher'
               ; Драйвер должен работать под DOS версии не ниже 3.0
                   db  CR, LF,  '  System Halted! $'
               ; Останов системы!
greetings  db  CR, LF, LF,  'MDM_DRV being installed...', CR, LF
               ; Загрузка драйвера MDM_DRV...
                   db  'CONFIG.SYS Line is: $'
               ; Строка файла CONFIG.SYS:
end_reetings       db  CR, LF, LF, LF, '$'

output_chars proc near
output_loop:
           mov    dl, ds:[si]             ; получить символ
           cmp    dl, LF                  ; перевод строки?
           jnz    outit                   ; нет
           ret
outit:
           mov    ah, 2                   ; выдать символ
           int    21h
           inc    si
           jmp    output_loop
output_chars endp

driver     endp
code       ends
           end
