Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
МЕТ_ОРГ_1.doc
Скачиваний:
29
Добавлен:
18.11.2019
Размер:
1.27 Mб
Скачать
    1. Двоично-десятичные числа

Двоично-десятичные числа - специальный формат хранения данных, используемом в ряде технических приложений. Часто эти числа называют BCD-числами (от binary-coded decimal, двоично-кодированные десятичные числа). Для обработки BCD-чисел (сложения, вычитания, умножения и деления) в МП 86 предусмотрены специальные команды. Рассмотрим этот вопрос на комплексном примере обработки показаний КМОП - часов реального времени. Как известно, в современных компьютеров имеются два независимых таймера. Один из них ("часы реального времени") включен в состав микросхемы с очень низким потреблением тока, питается от батарейки или аккумулятора, находящегося на системной плате, и работает даже на выключенной из сети машине. В этом таймере хранится и автоматически наращивается текущее календарное время (год, месяц, день, час, минута и секунда). После включения компьютера вступает в работу другой таймер, который обычно называют системным. Датчиком сигналов времени для него служит кварцевый генератор, работающий на частоте 1,19318 МГц, сигналы от которого, после пересчета в отношении 65536:1, поступают в контроллер прерываний и инициируют прерывания через вектор 8 с частотой 18,2065 Гц. Эти прерывания активизируют программу BIOS, периодически выполняющую инкремент содержимого четырехбайтовой ячейки памяти с текущим временем, находящейся по адресу 0046Ch.

После включения машины программы BIOS считывают из часов реального времени текущее время суток, преобразуют его в число тактов системного таймера (т.е. в число интервалов по 1/18,2065 с) и записывают в ячейку текущего времени. Далее содержимое этой ячейки наращивается уже системным таймером, работающим в режиме прерываний.

Для определения текущего времени прикладная программа может вызвать соответствующие функции прерывания 21h DOS (конкретно, с номером 2Ah для получения даты и 2Ch для получения времени суток), а может прочитать время непосредственно из часов реального времени с помощью прерывания lAh BIOS. При этом прерывание 1Аh позволяет, помимо чтения текущего времени (функция 02h) и текущей даты (функция 04h), выполнять и целый ряд других функций, среди которых мы отметим только возможность установить "будильник", т.е. записать в микросхему часов значение календарного времени, когда часы должны выдать сигнал аппаратного прерывания. Этот сигнал через вектор 70h инициирует обработчик прерываний, входящий в состав BIOS, который проверяет, возникло ли данное прерывание в результате достижения времени установки будильника (часы реального времени могут инициировать прерывания и по других причинам), тестирует заодно батарейное питание микросхемы, а затем посылает в оба контроллера прерываний команды конца прерываний и завершается командой iret. Однако по ходу своего выполнения обработчик прерывания 70h выполняет команду int 4Ah, которая передает управление на обработчик этого прерывания, тоже входящий в состав BIOS. Системный обработчик прерывания 4Ah ничего особенно полезного не делает, в сущности представляя собой просто программу-заглушку. Однако программист имеет возможность записать в вектор 4Ah адрес прикладного обработчика прерываний, который будет активизироваться прерыванием будильника. Функции прикладного обработчика определяет программист.

В примере 2.2 устанавливается прикладной обработчик прерывания 4Ah, который сам по себе вызваться никогда не будет, так как по умолчанию будильник часов реального не работает. Если, однако, прочитать системное время с помощью функции 02h прерывания lAh, прибавить к нему некоторую величину, например, 1 секунду, и установить будильник на это время (с помощью функции 06h прерывания lAh), то через одну секунду будет активизирован наш обработчик. В примере 2.2 этот процесс сделан бесконечным: в обработчике прерываний будильника снова выполняется чтение времени, прибавление к нему 1 секунды и установка будильника на новое время. В результате наш обработчик будет вызываться каждую секунду до завершения всей программы. Помимо служебной функции установки будильника на следующую секунду, обработчик прерываний выполняет и полезную работу: он выводит текущее время в определенное место экрана. Поскольку обработчик активизируется каждую секунду, выводимое значение времени будет обновляться каждую секунду. Как уже говорилось, в часах реального времени значение времени хранится в виде упакованных двоично-десятичных чисел. При выполнении арифметических операций с числами BCD (а нашем случае операции заключаются в прибавлении 1) необходимо использовать предназначенные для этого команды процессора. В примере проиллюстрировано использование одной из этих команд, конкретно, команды daa.

Для того, чтобы вывести на экран значение времени, его надо преобразовать в последовательность кодов ASCII. Процедура преобразования упакованных двоично-десятичных чисел в строку символов также включена в рассматриваемый пример.

Пример 2.2 - Чтение и обработка показаний часов реального времени

.586 ;Будут использоваться дополнительные команды

assume CS: code, DS: data , SS: stk

code segment use16

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

;Установим будильник

mov AH,02h ;Чтение текущего времени

int 1Ah

call add_time ;Прибавим 1 секунду

mov AH,06h ;Установим будильник на это время

int 1Ah

;Остановим программу, чтобы наблюдать прерывания

mov AH,01h ;Функция ввода с клавиатуры

int 21h

;Завершим программу, прибрав за собой

mov AH,07h ;Сброс будильника

int 1Ah

Lds DX,old_4a ; DS:DX=исходный вектор

mov AX,254Ah ;Установим исходный вектор

int 21h

mov AX,4C00h ;Завершим программу

int 21h

main endp

;Наш обработчик прерывания от будильника

new_4a proc

pusha ;Сохраним все регистры

push DS ;Сохраним еще и

push ES ;сегментные регистры

mov AX ,seg hour ;Настроим DS на наш

mov DS,AX ;сегмент данных

mov AH,02h ;Прочитаем текущее время

int 1Ah ;из часов реального времени

push CX ;Сохраним полученное

push DX ;текущее время

; Преобразуем время из двоично-десятичных упакованных чисел

; в строку символов A S C I I

MOV AL, CH ; перенесём часы в AL

Call conv ; преобразуем старшую цифру в ASCII

MOV hour,AH ; и сохраним

AND CH,0Fh ; выделим младшую цифру

ADD CH,"0" ; преобразуем в ASCII

MOV hour+1,CH ; и сохраним

MOV AL,CL ; перенесём в AL минуты

call conv

MOV min,AH

AND CL,0Fh

ADD CL,'0'

MOV min+1,CL

MOV AL,DH ; перенесём в AL секунды

CALL conv

MOV sec,AH

AND DH,0Fh

Add DH,'0'

MOV sec+1,DH

; Выведем сформированную строку на экран

MOV AX, 0B800h ; настроим ES на сегмент

MOV ES, AX ; видеопамяти

MOV DI, 160-16 ; смещение к 72 -му знакоместу экрана

MOV SI, offset hour ; адрес строки с временем в DS:SI

MOV CX,08 ; цикл по 8 символам

MOV AH,14h ; атрибут красный по синему

CLD

M1: Lodsb ; очередной символ в AL

Stosw ; символ + атрибут на экран

LOOP M1

; Сбросим будильник и установим снова на время через 1 сек

MOV AH,07h ;

INT 1Ah ;

POP DX ; вернём сохранённое в стеке

POP CX ; текущее время

CALL add_time ; прибавим 1 сек

MOV AH,06h ; установим будильник на

INT 1Ah ; время через 1 секунду

POP ES ; восстановим

POP DS ; все

POPa ; регистры

IRET ; выход из обработчика

new_4a ENDp

; Подпрограмма прибавления 1 секунды к текущему времени

add_time proc

; прибавим 1 к числу секунд

MOV AL,DH ; перенесём секунды в AL

CALL add_unit ; прибавим 1

MOV DH,AL ; перенесём назад в DH

JNC ok ; если CF=0, на выход

; если CF=1, прибавим 1 числу минут

MOV AL,CL ; переносим минуты в AL

CALL add_unit ; прибавим 1

MOV CL,AL ;

JNC ok ;

; если перенос, прибавляем 1 к числу часов

MOV AL,CH ; переносим часы в AL

CALL add_unit ; прибавим 1

MOV CH,AL ;

OK: RET ; выход из п/п

add_time ENDP

; подпрограмма прибавления 1 к 2-разрядному BCD-числу

add_unit proc

ADD AL,1 ;

DAA ; коррекция после сложения

CMP AL,60 ; надо ли корректировать следующий разряд времени

JB done ; нет сумма меньше 60

MOV AL,0 ; Да сумма =60

STC ; установим флаг переноса (признак коррекции времени)

Jmp en_d ; и на выход

done: clc ;

en_d: ret ;

add_unit endp

;подпрограмма преобразования старшей половины упакованного BCD –числа в регистре AL в код ASCII символа

Conv proc

MOV AH,0 ;

SHL AX,4 ; сдвиг старшего полубайта AL в AH

ADD AH,'0' ; преобразуем в код ASCII

RET

Conv ENDP

CODE ENDS

; сегмент данных

DATA segment use16

Old_4a dd 0

Hour db 0, 0, ':'

Min db 0, 0, ':'

Sec db 0, 0

DATA ends

Stk segment stack

Dw 128 dup (?)

Stk ends

END main

В примере 2.2 используются несколько команд, отсутствующих в МП 8086: команды сохранения в стеке и восстановления всех регистров общего назначения 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, т.е. старшая цифра числа, перемещается в регистр АН (Рисунок 2.3).

Двоично-десятичная цифра представляет собой просто двоичное представление цифры; прибавление к ее коду кода символа "0" (числа 30h) дает код ASCII этой цифры. Мы преобразовали пока только старший полубайт регистра СН. Для выделения младшего полубайта на регистр СН накладывается маска 0Fh, которая обнуляет старший полубайт, не затрагивая младшего. Прибавление кода ASCII нуля к коду десятичной цифры образует код ASCII этой цифры, который и переносится затем в строку-шаблон. Описанная процедура повторяется затем для регистров CL (минуты) и DH (секунды). Для вывода строки с временем на экран используется прямое обращение в видеопамяти. В регистр ES заносится сегментный адрес видеобуфера B800h, а в регистр DI - требуемое смещение видеопамяти к тому месту, начиная с которого мы хотим вывести строку. В регистр SI заносится адрес строки-источника, в регистр СХ - число шагов, а в регистр АН - выбранный нами атрибут символов (красные символы по синему полю).

Рисунок 2.3- Алгоритм работы подпрограммы conv.

Поскольку перемещение и по строке-шаблону, и по экрану должно осуществляться вперед, командой с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-чисел осуществляет команда aad, которая в приведенном примере преобразует lAh в 20h, и которая должна всегда следовать за командой сложения. Наши двоично-десятичные числа специфичны в том отношении, что они не могут превышать 59. Поэтому после коррекции результат сравнивается с 60h. Если сумма меньше 60h, флаг CF сбрасывается и выполняется команда ret. Если сумма равна 60h, регистр AL обнуляется, флаг CF устанавливается, сигнализируя о переносе 1 в следующий разряд времени (минут или часов) и выполняется та же команда ret. Таким образом, флаг CF процессора в точке возврата из подпрограммы add_unit говорит не о наличии или отсутствии арифметического переноса, а выполняет роль флага "исключительной ситуации" - перехода времени на следующую минуту или на следующий час. Такое нестандартное использование флага CF является общеупотребительным приемом.