8.4. Время суток

Канал 0 таймера 8253 имеет специальное назначение в IBM PC. Выход этого канала таймера подключен к уровню прерывания 0 микросхемы 8259. Это означает, что всякий раз, когда выход канала 0 имеет активный уровень сигнала, возникает прерывание (при условии, что все остальное установлено корректно). Процедура самопроверки при включении питания инициализирует канал 0 таймера, загружая в него число 0. Это дает наибольшее (не наименьшее) значение счетчика, которое может записать в него программа. Имея на входе частоту 1.19 МГц, счетчик считает обратно к нулю чуть быстрее, чем за 55 миллисекунд. Программа инициализации устанавливает таймер таким образом, что он считает непрерывно. Это означает, что прерывание 0 возникает 18.2 раза в секунду.

Как мы увидим в следующей главе, встроенная система программ BIOS использует это постоянное прерывание от таймера, чтобы следить за текущим временем. BIOS продвигает часы текущего времени вперед всякий раз, когда возникает прерывание. Затем, с помощью соответствующих вычислений, Вы можете преобразовать число циклов таймера в часы, минуты и секунды.

Почему же выбрано значение 18.2? Почему счетчик не программируется так, чтобы давать прерывание 20 раз в секунду, или другое "хорошее" число раз? Это объясняет следующий пример.

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


       MOV    CX, LOOP_VALUE
HERE:  LOOP   HERE

Вы выбираете константу LOOP_VALUE так, что цикл выполняется в точности нужное число раз. Это очень хороший метод, если вам нужна задержка на определенное время. В выше приведенном примере начальное значение константы LOOP_VALUE, равное 0FFFFH, дает время выполнения около 250-миллисекунд.

Но предположим, что вы хотите понаблюдать за внешним событием, и определить, сколко времени займет его наступление. Можно использовать вариант временного цикла такого, например, вида:


        MOV     CX, 0
HERE:
; --- проверка возникновения события
        IN      AL, DX
        TEST    AL, MASK_BIT
        LOOPNE  HERE
DONE:
; --- CX содержит число итераций цикла

Таким способом вы считаете число итераций цикла, чтобы вычислить затраченное на него время. Этот метод предполагает, что событие возникнет до того, как содержимое регистра CX второй раз достигнет 0. Но если вам нужно измерить что-то с точностью до микросекунд, этот метод не удобен, так как каждая итерация цикла требует от 10 до 20 микросекунд. Системный таймер дает лучшее решение. Поскольку он изменяет свое значение каждые 840 наносекунд, вы сможете определить длительность события с точностью до микросекунды.

На Фиг. 8.5 показан пример программы, вычисляющей время события с помощью системного таймера. В этом примере в качестве регистрируемого события используется канал 2 таймера. В первой


Microsoft (R) Macro Assembler Version 5.00                  1/1/80 04:03:56
Фиг. 8.5 Управление системным таймером                            Page  1-1
PAGE  ,132
                         TITLE    Фиг. 8.5 Управление системным таймером

0000                     STACK    SEGMENT STACK
0000    0040[                     DW        64 DUP (?)
             ????
            ]
0080                     STACK    ENDS

0000                     CODE     SEGMENT

                                  ASSUME    CS:CODE
0000                     TIMER    PROC      FAR
0000    1E                        PUSH      DS            ; Занесение адреса возврата
0001    B8 0000                   MOV       AX, 0
0004    50                        PUSH      AX
0005    B0 B6                     MOV       AL, 10110110B ; Выборка таймера 2
0007    E6 43                     OUT       43H, AL
0009    B8 0500                   MOV       AX, 500H
000C    E6 42                     OUT       42H, AL       ; Таймер 2 установлен на 500 отсчетов
000E    8A C4                     MOV       AL, AH
0010    E6 42                     OUT       42H, AL 
0012    E8 001D R                 CALL      LOW_TO_HIGH   ; Выборка времени первого 
                                                          ; перехода с 0 на 1
0015    8B D8                     MOV       BX, AX        ; Сохранение значения в регистре BX
0017    E8 001D R                 CALL      LOW_TO_HIGH   ; Выборка времени второго 
                                                          ; перехода с 0 на 1
001A    2B D8                     SUB       BX, AX        ; Вычитая получаем длину цикла
001C    CB                        RET
001D                     TIMER    ENDP

                         ;---------------------------------------------------------
                         ; Эта подпрограмма ждет перехода с нижнего уровня
                         ; сигнала на верхний (с 0 на 1) в таймере 2
                         ; и возвращает в регистре AX значение счетчика таймера 0
                         ;---------------------------------------------------------

001D                     LOW_TO_HIGH        PROC      NEAR
001D    E4 62                     IN        AL, 62H       ; Проверка разряда таймера 2
001F    A8 20                     TEST      AL, 20H
0021    75 FA                     JNZ       LOW_TO_HIGH   ; Цикл: сигнал таймера 2 на
                                                          ; нижнем уровне
0023                     WAIT_HIGH:
0023    E4 62                     IN        AL, 62H       ; Проверка разряда таймера 2
0025    A8 20                     TEST      AL, 20H
0027    74 FA                     JZ        WAIT_HIGH     ; Цикл: сигнал таймера 2 на
                                                          ; верхнем уровне
0029    B0 00                     MOV       AL, 0         ; Послать команду в регистр
                                                          ; управления таймером 2,
002B    E6 43                     OUT       43H, AL       ; которая "замораживает" таймер 2
002D    90                        NOP
002E    90                        NOP                     ; Задержка, необходимая для 8253
002F    E4 40                     IN        AL, 40H       ; Чтение младшего байта счетчика
0031    8A E0                     MOV       AH, AL
0033    90                        NOP
0034    E4 40                     IN        AL, 40H       ; Чтение старшего байта счетчика
0036    86 E0                     XCHG      AH, AL
0038    C3                        RET                     ; Возвращение значения в AX
0039                     LOW_TO_HIGH      ENDP
0039                     CODE    ENDS
                         END

                                    Фиг. 8.5 Системный таймер

части программы этот канал таймера загружается известным значением. Здесь мы произвольно выбрали число 500H. Обратите внимание, что эта часть программы идентична способу генерации звуков с помощью втого канала таймера.

Наша программа вызывает подпрограмму LOW_TO_HIGH, которая возвращает значение таймера в тот момент, когда на выходе канала 2 таймера отмечается переход от низкого к высокому уровню. Программа следит именно за переходом; если бы она регистрировала только высокий уровень, было бы неизвестно, стал ли сигнал высоким только что или уже готов стать низким. Подпрограмма посылает нуль в управляющий регистр таймера (порт 43H), чтобы "заморозить" текущее значение канала 0. Это позволяет ей прочитать текущее значение таймера, продолжающего счет. Если бы программа временно не зафиксировала таймер, она не смогла бы прочитать без ошибки его 16-битовое значение.

Обратим внимание на то, что подпрограмма на Фиг. 8.5 содержит несколько команд NOP. Эти команды записаны в программе для выдержки временных соотношений. Если очень внимательно прочитать инструкции микросхеме 8253, мы заметим, что между командами IN и OUT, выполняемыми этой микросхемой, проходит не меньше 1 микросекунды. Команда NOP занимает как раз достаточно времени, чтобы исключить нарушение требований микросхемы по времени. После возврата из подпрограммы программа сохраняет в регистре BX значение счетчика таймера во время первого перехода с низкого уровня на высокий. Затем программа снова вызывает подпрограмму, чтобы зарегистрировать следующий переход с низкого уровня на высокий на выходе канала 2 таймера. Потом она вычитает одно число из другого, чтобы определить время цикла канала 2.

Мы уже говорили о том, что загрузка регистра 0 таймера значением счета 0 - очень полезна. Данная программа подтверждает это, так как она вычитает два значения таймера, не обращая внимания на то, какое из них больше, а какое меньше. Так как канал 0 таймера работает асинхронно по отношению к этой программе, нет никакой гарантии, что первое читаемое из него число больше второго. Например, предположим, что первый переход с низкого на высокий уровень происходит, когда таймер 0 имеет значение 100H. После 500H циклов значение числа в таймере будет 0FC00H. Счетчик таймера 0 автоматически "проскочил" от значения 0 к значению 0FFFFH, и значение прочитанное вторым оказалось численно больше первого. Но из-за того, что регистр таймера снова начинает счет со значения 0FFFFH, мы всегда можем вычитать эти два числа. При этом иногда будет появляться перенос, иногда нет, но разность этих двух чисел всегда будет равна числу отсчетов.

Чтобы убедить вас в правильности этого положения, рассмотрим случай, когда счетчик загружается числом 8000H. Если первый переход возникает при значении 6000H, второй появится при значении 5B00H, и разница между ними составляет 500H. Но если первый переход возникает при значении 100H, второй возникает при значении 7C00H, и разница станет равна 8500H. Чтобы правильно отреагировать на эту ситуацию, программа должна была бы проверить, не произошло ли переполнение счетчика за время отсчета.

При выполнении этой программы вы обнаружите, что значение в регистре BX составляет около 0A00H, и не равно ожидаемому значению 500H. Так происходит потому, что таймер работает в режиме уменьшения содержимого счетчика на два по каждому временному импульсу. Чтобы разобраться в работе микросхемы 8253, нужно ознакомиться с инструкцией по ее программированию.

Фиг. 8.6 дает сводку для управляющего слова микросхемы 8253. Для настройки одного из каналов на конкретный режим работы вы выводите это управляющее слово в порт 43H. Мы уже встречались с выводом некотрых значений в порт 43H. Чтобы "заморозить" счетчик, мы послали в этот порт нуль, а для настройки генератора тональности - код 0B6H. Посмотрим, откуда берутся эти значения.

Два старших бита управляющего слова определяют канал таймера. Следующие два бита - выполняемую операцию. Когда мы выводим значение 0, выбирается таймер 0 и запирание данных в счетчике. Следующие 3 бита задают режим работы выбранного таймера. Эти биты е играют роли, когда счетчик заперт, но нужны при инициализации таймера. Оставшийся бит определяет, будет ли счетчик работать как 16-битовое двоичное число или как четырехзначное десятичное в двоичном представлении.

Формат управляющего слова
D7D6D5D4D3D2D1D0
SC1 SC2 RL1 RL0 M2 M1 M0 BCD
Определение управления
SC1 SC2 SC - выбор счетчика:
0 0 задать счетчик 0
0 1 задать счетчик 1
1 0 задать счетчик 2
1 1 неопределено
RL1 RL0 RL - чтение/загрузка
0 0 Операция запирания счетчика (см. раздел процедуры READ/WRITE)
0 1 Считать/загрузить младший байт
1 0 Считать/загрузить старший байт
1 1 Считать/загрузить младший байт, затем - старший
M2 M1 M0 M - режим
00 0 Режим 0
00 1 Режим 1
X1 0 Режим 2
X1 1 Режим 3
10 0 Режим 4
10 1 Режим 5
BCD BCD
0 16-битовый двоичный счетчик
1 десятичный в двоичном представлении (BCD) счетчик (4-х разрядный)

Фиг. 8.6 Программирование таймера/счетчика
(с разрешения фирмы Intel; © Intel 1981г.)

Управляющий код 0B6H, который использовался для генерации тональности, можно расписать таким образом:


0B6H = 10110110B = 01 11 011 0

Это управляющее слово выбирает канал 2. Счетчик этого канала работает с 16-битовым значением, младший байт которого загружается в счетчик первым. Последный бит показывает, что счет будет двоичным. Можно видеть также, что выбирается третий режим работы.

Микросхема 8253 может работать в шести разных режимах. Но для наших целей, в конфигурации системы IBM, удобны только два из них. Режим 3 устанавливается по умолчанию для всех трех каналов таймера. Это режим работы генератора прямоугольных импульсов: половину периода выход канала дает нижний уровень, а другую половину периода - верхний. Счетчик работает в этом режиме, вычитая из своего значения по два, а не по одному. Во время первого обратного счета выход низкий, а затем, во время второго счета - высокий. Так как счетчик считает по два, выход имеет высокий уровень точно половину заданного времени, и низкий тоже точно половину. Из-за того, что канал 0 таймера обычно работает в режиме 3, пример на Фиг. 8.5 заканчивает счет со значением счетчика 0A00H, а не 500H, как ожидалось. Каждый отсчет таймера уменьшает содержимое счетчика на два.

Так как счетчик считает двойками, он переполняется каждые 27 микросекунд. Если вы хотите измерять события более длительные, то вам придется воспользоваться другим способом измерения времени.

Другой режим таймера, который используется в IBM PC - это режим 0. Этот режим называют прерыванием по завершению счета. В этом режиме таймер не работает непрерывно. После его установки таймер до тех пор не начинает считать (единицами), пока в него полностью не будет загружено число. Затем счетчик считает в сторону уменьшения с частотой синхроимпульсов, пока не достигнет нуля. В этот момент его выход становится высоким. Поскольку выход канала 0 таймера подключен к прерыванию 0 контроллера 8259, в системе возникает прерывание.

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

Если вы хотите измерять интервал времени в секундах, нужно оставить таймер в его обычном режиме работы. Система BIOS позволяет захватывать управление системой каждые 55 миллисекунд, и в каждый такой момент вы можете решить, не исчерпался ли нужный промежуток времени. Если время нужной вам задержки находится между 55 миллисекундами и 5 секундами, можно использовать метод без использования программ BIOS. Например, вам хочется сделать задержку на 150 миллисекунд. Используя режим прерывания по завершению счета, вы настраиваете таймер на прерывание через 50 миллисекунд (этому соответствует значенние счетчика около 59500). Обработчик прерывания программируется так, чтобы, получая управление первые два раза, он заново устанавливал таймер на 50 миллисекунд. По третьему прерыванию от таймера, когда 150 миллисекунд исчерпаны, можно предпринять нужные действия.

При организации задержек через таймер всегда нужна некоторая осторожность. Как упоминалось выше, канал 1 таймера выполняет одну важную аппаратную функцию. Если вы модифицируете число в канале 1, ваша программа может немедленно разрушиться. Использование канала 2 таймера безопасно. Этот канал подключен только к динамику и выходу кассетного магнитофона. Отсюда, очевидно, следует, что нельзя использовать канал 2 таймера для отсчета промежутков времени в одно время с попытками воспроизводить мелодии через динамик. И наконец, BIOS пользуется услугами таймера 0 для различных системных функций. При обсуждении BIOS будет видно, что прерывание по времени суток управляет не только текущим временем, но также обслуживает и двигатель накопителя на дискетах. Перед тем, как изменять настройку канала 0 таймера для любых целей, нужно понять, какие существующиефункции вы можете при этом изменить.