Двоично-десятичные числа
В гл. 2 уже говорилось о двоично-десятичных числах - специальном формате хранения
данных, используемом в ряде технических приложений. Часто эти числа называют
BCD-числами (от binary-coded decimal, двоично-кодированные десятичные числа).
Для обработки BCD-чисел (сложения, вычитания, умножения и деления) в МП 86 предусмотрены
специальные команды. Рассмотрим этот вопрос на комплексном примере обработки
показаний КМОП-часов реального времени.
Как известно, в современных компьютеров имеются два независимых таймера. Один
из них ("часы реального времени") включен в состав микросхемы с очень
низким потреблением тока, питается от батарейки или аккумулятора, находящегося
на системной плате, и работает даже на выключенной из сети машине. В этом таймере
хранится и автоматически наращивается текущее календарное время (год, месяц,
день, час, минута и секунда).
После включения компьютера вступает в работу другой таймер, который обычно называют
системным. Датчиком сигналов времени для него служит кварцевый генератор, работающий
на частоте 1,19318 МГц, сигналы от которого, после пересчета в отношении 65536:1,
поступают в контроллер прерываний и инициируют прерывания через вектор 8 с частотой
18,2065 Гц. Эти прерывания активизируют программу
BIOS, периодически выполняющую
инкремент содержимого четырехбайтовой ячейки памяти с текущим временем, находящейся
по адресу 46Ch. После включения машины программы BIOS считывают из часов реального
времени текущее время суток, преобразуют его в число тактов системного таймера
(т.е. в число интервалов по 1/18,2065 с) и записывают в ячейку текущего времени.
Далее содержимое этой ячейки наращивается уже системным таймером, работающим
в режиме прерываний.
Для определения текущего времени прикладная программа может вызвать соответствующие
функции прерывания 21h DOS (конкретно, с номером 2Ah для получения даты и 2Ch
для получения времени суток), а может прочитать время непосредственно из часов
реального времени с помощью прерывания lAh
BIOS. При этом прерывание 1А1г позволяет,
помимо чтения текущего времени (функция 02h) и текущей даты (функция 04h), выполнять
и целый ряд других функций, среди которых мы отметим только возможность установить
"будильник", т.е. записать в микросхему часов значение календарного
времени, когда часы должны выдать сигнал аппаратного прерывания. Этот сигнал
через вектор 70h инициирует обработчик прерываний, входящий в состав
BIOS, который
проверяет, возникло ли данное прерывание в результате достижения времени установки
будильника (часы реального времени могут инициировать прерывания и по других
причинам), тестирует заодно батарейное питание микросхемы, а затем посылает
в оба контроллера прерываний команды конца прерываний и завершается командой
iret. Однако по ходу своего выполнения обработчик прерывания 70h выполняет команду
hit 4Ah, которая передает управление на обработчик этого прерывания, тоже входящий
в состав BIOS. Системный обработчик прерывания 4Ah ничего особенно полезного
не делает, в сущности представляя собой просто программу-заглушку. Однако программист
имеет возможность записать в вектор 4Ah адрес прикладного обработчика прерываний,
который будет активизироваться прерыванием будильника. Функции прикладного обработчика
определяет программист.
В примере 3-9 устанавливается прикладной обработчик прерывания 4All, который
сам по себе вызваться никогда не будет, так как по умолчанию будильник часов
реального не работает. Если, однако, прочитать системное время с помощью функции
02h прерывания lAh, прибавить к нему некоторую величину, например, 1 секунду,
и установить будильник на это время (с помощью функции 06h прерывания
lAh),
то через одну секунду будет активизирован наш обработчик. В примере 3-9 этот
процесс сделан бесконечным: в обработчике прерываний будильника снова выполняется
чтение времени, прибавление к нему 1 секунды и установка будильника на новое
время. В результате наш обработчик будет вызываться каждую секунду до завершения
всей программы.
Помимо служебной функции установки будильника на следующую секунду, обработчик
прерываний выполняет и полезную работу: он выводит текущее время в определенное
место экрана. Поскольку обработчик активизируется каждую секунду, выводимое
значение времени будет обновляться каждую секунду.
Как уже говорилось, в часах реального времени значение времени хранится в виде
упакованных двоично-десятичных чисел. При выполнении арифметических операций
с числами BCD (а нашем случае операции заключаются в прибавлении 1) необходимо
использовать предназначенные для этого команды процессора. В примере проиллюстрировано
использование одной из этих команд, конкретно, команды
daa.
Для того, чтобы вывести на экран значение времени, его надо преобразовать в
последовательность кодов ASCII. Процедура преобразования упакованных двоично-десятичных
чисел в строку символов также включена в рассматриваемый пример.
Пример 3-9. Чтение и обработка показаний часов реального времени
.586 ;Будут использоваться дополнительные команды
assume CS:code,ds:data
code segment use 16
main proc
mov AX,data ;Настроим DS наш
mov DS,Ax ;сегмент данных
;Сохраним исходный вектор 4Ah
mov AX,354Ah
int 21h
mov word ptr old_4a,BX
mov word ptr old_4a+2,ES
;Установим наш обработчик прерываний 4Ah
mov AX,254Ah
push DS ;Сохраним DS
push CS ;Настроим DS на сегмент
pop DS ;команд
mov DX,offset new_4a: DS:DX->new_4a
int 21h
pop DS ;Восстановим DS
;Установим будильник
movAH,02h ;Чтение текущего времени
int 1Ah
call add_time ;Прибавим 1 секунду
mov AH,06h ;Установим будильник на это время
int 1Ah
;Остановим программу, чтобы наблюдать прерывания
mov AH,01h ;Функция ввода с клавиатуры
int 21h
;Завершим программу, прибрав за собой
mov AH,07h ;Сброс будильника
int 1Ah
Ids DX,old_4a/DS:DX=исходный вектор
mov AX,254Ah ;Установим исходный вектор
int 21h
mov AX,4C00h ;Завершим программу
int 21h
main endp
;Наш обработчик прерывания от будильника new_4a proc
push a ;Сохраним все регистры
push DS ;Сохраним еще и
push ES ;сегментные регистры
mov AX ,seg hour ;Настроим DS на наш
mov DX,AX ;сегмент данных
mov AH,02h ;Прочитаем текущее время
int 1Ah ;из часов реального времени
push CX ;Сохраним полученное
push DX ;текущее время
В примере 3-9 используются
несколько команд, отсутствующих в МП 86: команды сохранения в стеке и восстановления
всех регистров общего назначения pusha и рора, а также команда сдвига shl с
числовым операндом. Для того, чтобы эти команды распознавались ассемблером,
в программу включена
директива .586 (можно было бы обойтись и директивой .386). В этом случае необходимо
оба сегмента объявить с описателем use16.
Программа состоит из главной процедуры main, процедуры new_4a обработчика прерываний
от будильника, а также трех вспомогательных процедур-подпрограмм
add_time, add_unit
и conv. Главная процедура сохраняет исходный вектор прерывания 4Ah, устанавливает
новый обработчик этого прерывания, читает текущее время и устанавливает будильник
на время, отстоящее от текущего на 1 секунду, а затем останавливается в ожидании
нажатия любой клавиши. Пока программа стоит, обрабатываются прерывания от будильника
и в правый верхний угол экрана каждую секунду выводится текущее время. После
нажатия любой клавиши программа завершается, предварительно сбросив будильник
и восстановив исходное содержимое вектора 4Ah.
Легко видеть, что в предложенном варианте программа имеет мало практического
смысла, так как она не выполняет, кроме вывода времени, никакой полезной работы.
В то же время, пока эта программа не завершилась, запустить другую программу
нельзя, так как DOS является однозадачной системой. Если, однако, написать нашу
программу в формате .СОМ и сделать ее резидентной, мы получим возможность запускать
любые программы и одновременно наблюдать на экране текущее время. Такого средства
в DOS нет, и в какой-то ситуации оно может оказаться полезным. Методика разработки
резидентных программ описана выше; читатель может выполнить необходимые преобразования
самостоятельно.
Рассмотрим теперь программу обработчика прерываний будильника. Прежде всего
в нем командой pusha (push all, сохранить все) сохраняются все регистры общего
назначения и, кроме того, два сегментных регистра DS и
ES, которые будут использоваться
в обработчике. Далее регистр DS настраивается на сегментный адрес того сегмента,
в который входит ячейка hour, т.е. фактически на наш сегмент команд. На первый
взгляд это действие может показаться бессмысленным. Ведь в начале процедуры
main в регистр DS уже был помещен адрес нашего сегмента данных
data. Зачем же
эту операцию повторять? Дело в том, что процедура new_4a, будучи формально обработчиком
программного прерывания 4Ah, фактически представляет собой обработчик аппаратного
прерывания от часов реального времени, которое, как и любое аппаратное прерывание,
может придти в любой момент времени. В принципе прерываемая программа в этот
момент может выполнять любые действия, и содержимое регистра DS может быть любым.
Если же говорить о нашей программе, то она находится в цикле ожидания нажатия
клавиши. Этот цикл организует функция 01h DOS, которая, между прочим, время
от времени обращается к своему драйверу клавиатуры, а тот - к программам BIOS
ввода символа с клавиатуры. Вполне вероятно (а на самом деле так оно и есть),
что при выполнении упомянутых операций используется регистр
DS, который в этом
случае указывает уже не на наш сегмент данных, а на различные системные области.
Другими словами, при входе в обработчик прерывания содержимое регистра DS неизвестно,
и его следует инициализировать заново, обязательно сохранив исходное значение.
Если перед выходом из обработчика это исходное значение не восстановить, будет
неминуемо разрушена DOS.
Сохранив регистры и настроив DS, мы вызываем функцию 02h прерывания lAh чтения
текущего времени. Время возвращается, как уже говорилось, в упакованном двоично-десятичном
формате (по две цифры в байте) в регистрах СН (часы), CL (минуты) и DH (секунды).
Нам это время понадобится еще раз в конце обработчика для установки будильника
заново, и чтобы второй раз не вызывать функцию 02h, полученное время (т.е. содержимое
регистров СХ и DX) сохраняется в стеке.
Далее выполняется последовательное преобразование BCD-цифр, составляющих время,
в коды ASCII соответствующих символов. Число часов (две упакованные BCD-цифры)
переносится в регистр AL, и вызывается подпрограмма
conv, которая преобразует
старшую цифру часов в код ASCII и возвращает его в регистре АН. Этот код помещается
в объявленную в сегменте данных строку-шаблон
hour, в которой заготовлены пустые
пока места для символов цифр, составляющих время, а также имеются разделительные
двоеточия. Для удобства обращения к элементам этой строки, она разделена на
части и каждая часть снабжена собственным именем - min для поля минут и sec
для поля секунд.
Подпрограмма conv преобразования BCD-цифры в код ASCII состоит всего из трех
предложений, не считая заключительной команды
ret. Двух разрядное BCD-число
передается в подпрограмму в регистре AL. После обнуления регистра АН, который
будет служить приемником для образования конечного результата, содержимое AL
сдвигается командой shl влево на 4 бит, в результате чего старший полубайт регистра
AL, т.е. старшая цифра числа, перемещается в регистр АН (рис.
3.9). Двоично-десятичная
цифра представляет собой просто двоичное представление цифры; прибавление к
ее коду кода символа "0" (числа 30h) дает код ASCII этой цифры.
Мы преобразовали пока только старший полубайт регистра СН. Для выделения младшего
полубайта на регистр СН накладывается маска 0Fh,
Рис. 3.9. Алгоритм работы подпрограммы conv.
которая обнуляет старший
полубайт, не затрагивая младшего. Прибавление кода ASCII нуля к коду десятичной
цифры образует код ASCII этой цифры, который и переносится затем в строку-шаблон.
Описанная процедура повторяется затем для регистров CL (минуты) и DH (секунды).
Для вывода строки с временем на экран используется прямое обращение в видеопамяти.
В регистр ES заносится сегментный адрес видеобуфера BS00h, а в регистр DI -
требуемое смещение видеопамяти к тому месту, начиная с которого мы хотим вывести
строку. В регистр SI заносится адрес строки-источника, в регистр СХ - число
шагов, а в регистр АН - выбранный нами атрибут символов (красные символы по
синему полю). Поскольку перемещение и по строке-шаблону, и по экрану должно
осуществляться вперед, командой сld сбрасывается флаг
DF. Наконец, циклическое
выполнение пары команд
lodsb stosw
приводит к выводу в заданное
место экрана всей строки hour.
Выполнив вывод на экран текущего времени, надо снова установить будильник. Для
этого сначала запрещается работа ранее установленного будильника, восстанавливается
текущее время в регистрах DX и СХ, и вызовом процедуры add_time к текущему времени
прибавляется 1 секунда. Далее вызовом функции 06h заново устанавливается будильник,
восстанавливаются сохраненные в начале программы обработчика регистры, и, наконец,
командой iret обработчик завершает свою работу.
Рассмотрим теперь процедуру прибавления 1 к текущему времени. Она состоит из
двух компонентов - подпрограммы add_time, которая организует правильное сложение
чисел, обозначающих время, чтобы прибавление 1 секунды к 59 секундам дало 0
секунд и увеличило на 1 число минут (и то же самое для минут) и подпрограммы
add_uuit, выполняющей прибавление 1 к упакованному коду
BCD.
Подпрограмма add_time переносит число секунд из DH в
AL, с помощью подпрограммы
add_unit увеличивает его на 1 и возвращает в DH. Подпрограмма add_unit сигнализирует
установкой флага CF о необходимости переноса 1 в следующий разряд времени (число
секунд составляло 59). Поэтому после возврата из add_iuit проверяется флаг CF
и, если он сброшен, т.е. следующий разряд времени модифицировать не надо, подпрограмма
add_time завершается. Если же флаг CF установлен, выполняется аналогичная процедура
прибавления 1 к числу минут, которое находится в регистре
CL. Далее опять анализируется
флаг CF, и если он установлен (текущее время было 59 мин 59 с), прибавляется
1 к числу часов. Наконец, подпрограмма завершается командой
ret.
Подпрограмма add_unit получает упакованное двоично-десятичное число, к которому
надо прибавить 1, в регистре AL. Командой add к нему прибавляется 1, после чего
в некоторых случаях образуется правильная сумма, а в некоторых - неправильная.
Так, 14h + 1 = 15h, что правильно, однако 19h + 1 = lAh, что неверно. Такого
двоично-десятичного числа не существует, а после прибавления 1 к 19 должно получиться
20 (и записано в виде 20h). Коррекцию после сложения BCD-чисел осуществляет
команда daa, которая в приведенном примере преобразует lAh в 20h, и которая
должна всегда следовать за командой сложения.
Наши двоично-десятичные числа специфичны в том отношении, что они не могут превышать
59. Поэтому после коррекции результат сравнивается с 60h. Если сумма меньше
60h, флаг CF сбрасывается и выполняется команда
ret. Если сумма равна 60h, регистр
AL обнуляется, флаг CF устанавливается, сигнализируя о переносе 1 в следующий
разряд времени (минут или часов) и выполняется та же команда
ret. Таким образом,
флаг CF процессора в точке возврата из подпрограммы add_unit говорит не о наличии
или отсутствии арифметического переноса, а выполняет роль флага "исключительной
ситуации" - перехода времени на следующую минуту или на следующий час.
Такое нестандартное использование флага CF является общеупотребительным приемом.