Ввод символов с клавиатуры
Назад | Оглавление | Дальше Новая версия документа | Форум

ASCII и Scan коды
Считывание одной шестнадцатеричной цифры
Считывание двузначного шестнадцатеричного числа
Процедуры
Стек
Использование инструкций PUSH и POP
Организация пауз в программах

Для ввода символов с клавиатуры используется функция 01h прерывания INT 21h: по адресу 100h введите INT 21h; в регистр AH занесите номер функции 01h; выполните прерывание командой "p". В результате появится мерцающий курсор - это приглашение операционной системы (OS) ввести с клавиатуры любой символ. Например, введите букву "h":

h
AX=0168  BX=0000  CX=0000  DX=0000  SP=FFEE  BP=0000  SI=0000  DI=0000
DS=0ABD  ES=0ABD  SS=0ABD  CS=0ABD  IP=0102   NV UP EI PL NZ NA PO NC
0ABD:0102 96            XCHG    SI,AX
-

После выполнения прерывания, в регистре AL появилось число 68h (ASCII код буквы "h").

Задачи
  1. Используя данный алгоритм, определите ASCII коды символов: "q", "Q", "й", "Й", "*", "#".
  2. Попробуйте определить ASCII код клавиши [F1].

При определении ASCII кода клавиши [F1], в регистр AL заносится число 00h, а рядом с приглашением Debug появляется символ ";". Почему получается такой результат? Следующий раздел поможет разобраться в этом.


ASCII и Scan коды

Каждая клавиша клавиатуры при нажатии вырабатывает код сканирования (Scan code): [F1] формирует Scan код 3Bh, [Q] формирует Scan код 10h и т.д. Коды сканирования позволяют вводить информацию в компьютер и управлять его работой.

В отличие от Scan кода, ASCII код описывает не клавишу, а символ, который изображен на ней. Например, при нажатии клавиши с буквой "Z" вырабатывается Scan код 2Сh. Далее этот код преобразуется в один из ASCII кодов: "Z"=5Ah, "z"=7Ah, "Я"=9Fh, "я"=EFh. Преобразование кода зависит от состояния клавиш [Shift], [Caps Lock] и от текущей кодовой страницы.

Управляющие клавиши: [F1]...[F12], [Ctrl], [Alt] и др., не предназначены для ввода символов, и вырабатывают только Scan код. Для каждой из этих клавиш посылается два кода, один за другим. Первый код всегда 0h, он означает, что следом идет Scan код специальной клавиши.

Чтобы Scan код клавиши [F1] попал в регистр AL, надо выполнить INT 21h дважды:

0ABD:0100 INT 21
0ABD:0102 INT 21

Проверьте функцию в регистре AH и запустите программу командой "g 104". На приглашение OS надо ответить нажатием клавиши [F1], после чего вы увидите:

-g 104
;
AX=013B  BX=0000  CX=0000  DX=0000  SP=FFEE  BP=0000  SI=0000  DI=0000
DS=0ABD  ES=0ABD  SS=0ABD  CS=0ABD  IP=0104   NV UP EI PL NZ NA PO NC
0ABD:0104 CC            INT     3

Регистр AL содержит Scan код клавиши [F1].

Задачи
  1. Определите Scan коды функциональных клавиш: [F2] ... [F10].
  2. Определите Scan коды клавиш управления курсором: [], [], [].

Позже мы используем Scan коды в программе редактирования секторов диска, для операций: управление псевдокурсором, листинг секторов диска и запуск отдельных фрагментов кода.


Считывание одной шестнадцатеричной цифры

Любой символ, запрашиваемый функцией 01h прерывания INT 21h, отображается в регистре AL в виде ASCII кода. Например, клавиатурный ввод символа "9" завершается записью в AL числа 39h. Для преобразования кода в цифру достаточно выполнить вычитание: 39h-30h=9. Этот алгоритм можно использовать для ввода с клавиатуры лобой десятичной цифры. Ввод шестнадцатеричных цифр A ... F выполняется аналогично, с той лишь разницей, что код символа уменьшается на 37h.

Следующая программа выполняет запрос символа с клавиатуры, и преобразует этот символ в соответствующее шестнадцатеричное число:

0ABD:0100 B401          MOV     AH,01
0ABD:0102 CD21          INT     21
0ABD:0104 2C30          SUB     AL,30
0ABD:0106 3C09          CMP     AL,09   если AL  9,
0ABD:0108 7E02          JLE     010C    то переход на адрес 010Ch
0ABD:010A 2C07          SUB     AL,07   
0ABD:010C CD20          INT     20

Инструкция JLE (Jump if Less or Equal to) - перейти, если меньше или равно.

Программу следует выполнять в два этапа: "g 10C" и "g". Остановка по адресу 10Сh нужна для просмотра числа, введенного в регистр AL, до того, как прерывание INT 20h вернет все регистры в исходное состояние.

Проведите следующие эксперименты с программой:
- проверьте ввод нечисловых символов ("k", "z", "w" ... );
- проверьте ввод строчных числовых символов ("a" ... "f").

Данная программа работает корректно только с символами "0"..."9" и "A"..."F". Позже мы исправим этот недостаток, а пока ввод символов надо контролировать самостоятельно.


Считывание двузначного шестнадцатеричного числа

Алгоритм ввода многозначного числа сводится к последовательному запросу всех его цифр: от старшего разряда к младшему. Далее цифры легко объединяются в исходное число.

Например, надо ввести число B9h. После ввода первой цифры, в регистре AL окажется число Bh. Скопируем это число в регистр DL и сдвинем влево на четыре бита:

Сдвиг выполняется инструкцией SHL (Shift Left - сдвиг влево, работает аналогично SHR). После сдвига, в DL получается число B0h. Далее вводится вторая цифра (в AL попадает 9h). Остается выполнить сложение DL + AL: B0h + 9h = B9h.

Программа ввода двузначного шестнадцатеричного числа:

0ABD:0100 B401          MOV     AH,01    устанавливаем функцию запроса символа
0ABD:0102 CD21          INT     21       запрашиваем символ старшей цифры
0ABD:0104 88C2          MOV     DL,AL    копируем код символа в DL
0ABD:0106 80EA30        SUB     DL,30    |
0ABD:0109 80FA09        CMP     DL,09    |
0ABD:010C 7E03          JLE     0111     |  подготовка
0ABD:010E 80EA07        SUB     DL,07    | старшей цифры
0ABD:0111 B104          MOV     CL,04    |
0ABD:0113 D2E2          SHL     DL,CL    |
0ABD:0115 CD21          INT     21       запрашиваем символ младшей цифры
0ABD:0117 2C30          SUB     AL,30    |
0ABD:0119 3C09          CMP     AL,09    |  подготовка
0ABD:011B 7E02          JLE     011F     | младшей цифры
0ABD:011D 2C07          SUB     AL,07    |
0ABD:011F 00C2          ADD     DL,AL    объединяем цифры в число
0ABD:0121 CD20          INT     20

Запустите программу командой "g 121". Введите двузначное шестнадцатеричное число. Результат ввода должен отобразиться в регистре DL. Завершите программу командой "g".

Испытайте программу с различными двузначными числами. Для ввода шестнадцатеричных чисел используйте только заглавные буквы: "A"..."F".


Процедуры

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

Для работы с процедурами используется две инструкции:
CALL XXXX - вызов процедуры, расположенной по адресу XXXX
RET       - завершение процедуры и переход на инструкцию, следующую за CALL

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

0ABD:0100 B402          MOV     AH,02    грузим функцию печати символа
0ABD:0102 88DA          MOV     DL,BL    копируем исходное число из BL в DL
0ABD:0104 B104          MOV     CL,04    устанавливаем шаг сдвига
0ABD:0106 D2EA          SHR     DL,CL    сдвигм старшей цифры на место младшей
0ABD:0108 E80A00        CALL    0115     переход на процедуру печати HEX-цифры
0ABD:010B 88DA          MOV     DL,BL    восстанавливаем копию числа в DL
0ABD:010D 80E207        AND     DL,0F    обнуляем старшую цифру     
0ABD:0110 E80200        CALL    0115     переход на процедуру печати HEX-цифры
0ABD:0113 CD20          INT     20
0ABD:0115 80C230        ADD     DL,30    процедура вывода на экран
0ABD:0118 80FA3A        CMP     DL,3A    шестнадцатеричной цифры
0ABD:011B 7C03          JL      0120     из регистра DL
0ABD:011D 80C207        ADD     DL,07
0ABD:0120 CD21          INT     21
0ABD:0122 C3            RET              возврат в программу

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

Программа вывода на экран строки символов от "A" до "J":
0ABD:0100 B241          MOV     DL,41    грузим код символа "A" в регистр DL
0ABD:0102 B10A          MOV     CL,0A    устанавливаем счетчик для цикла LOOP
0ABD:0104 E80400        CALL    010B     переход на процедуру печати символа
0ABD:0107 E2FB          LOOP    0104     если CX > 0, то переход на адрес 104
0ABD:0109 CD20          INT     20
0ABD:010B B402          MOV     AH,02    грузим функцию печати символа
0ABD:010D CD21          INT     21       выводим символ с кодом в DL на экран
0ABD:010F FEC2          INC     DL       увеличиваем число в DL на единицу
0ABD:0111 C3            RET              возврат в программу

Каждый вызов инструкции LOOP сопровождается уменьшением регистра CX на единицу, и проверкой: если CX > 0, то выполняется переход на адрес, указанный в инструкции LOOP. Использование регистра CL (вместо CX) сокращает длину кода на 1 байт, но подвергает программу опасности: если до запуска программы в CH будет записано некоторое число, то LOOP выполнит не 10, а значительно большее количество циклов.

Инструкция INC (Increment - приращение) увеличивает содержимое регистра на единицу. Для уменьшения содержимого регистра на единицу используется инструкция DEC (Decrement).

Запустите программу командой "g". Далее выполните трассировку программы. Не забывайте использовать точку останова (или команду "p") для запуска прерываний.

Задачи:
  1. Используя инструкцию DEC, модифицируйте последнюю программу так,
    чтобы она выводила на экран строку символов: "9876543210".
  2. Используя процедуру печати шестнадцатеричной цифры, напишите программу,
    которая будет выводить на экран содержимое регистровой пары SS:SP.

Стек

Инструкция RET передает управление на адрес, следующий за вызовом CALL. Предположим, в программе используется несколько вызовов CALL, передающих управление единственной процедуре. То есть, при каждом вызове, инструкция RET должна возвращать управление на различные адреса. Как RET определяет адрес возврата?

До передачи управления процедуре, CALL копирует адрес возврата в специальную область ОЗУ, называемую - СТЕК. Инструкция RET извлекает адрес возврата из стека, и передает управление инструкции, расположенной по этому адресу.

Стек работает подобно стопке подносов на пружине: когда на верх стопки ставят очередной поднос, то вся стопка немного опускается, а верхний поднос является первым кандидатом на выход. Другими словами, работа стека организована по принципу LIFO ("Last In, First Out" - последним пришел, первым ушел).

При реализации некоторых алгоритмов возникает необходимость написать одну процедуру внутри другой. Такую организацию называют вложенной структурой (вложенным вызовом):

0ABD:0100  CALL 0200
0ABD:0103  INT  20h

0ABD:0200  CALL 0300
0ABD:0203  RET           переходы между инструкциями
                           во вложенной структуре
0ABD:0300  CALL 0400
0ABD:0303  RET

0ABD:0400  RET
0303
0203
0103
...

вершина стека (SS:SP) ->

В данном примере выполняется три вложенных вызова:
CALL 200, CALL 300, CALL 400. В стек заносятся адреса 103, 203, 303:

0203
0103
...
...

вершина стека (SS:SP) ->

По адресу 400 находится инструкция RET, которая извлекает адрес c вершины стека,
и передает управление инструкции по этому адресу. Картина в стеке меняется:

Очередной RET вновь извлекает значение с вершины стека, и передает управление на адрес 203. Последний RET забирает из стека адрес инструкции INT 20, и передает ей управление.

Вершина стека всегда хранится в регистровой паре SS:SP:
SS (Stack Segment - сегмент стека);
SP (Stack Point - смещение стека, или указатель стека).

Чтобы лучше разобраться с работой стека, выполните трассировку приведенного примера. Каждый трассировочный шаг сопровождайте анализом регистров SP и IP.

Работа стека может показаться не очень интересной, однако существует много способов его использования. Полезные свойства стека мы начнем обсуждать уже в следующем разделе.


Использование инструкций PUSH и POP

Инструкция CALL помещает адрес возврата в стек, а RET извлекает и передает его в регистр IP (так управление возвращается из процедуры в программу). Аналогичные действия можно выполнить, используя инструкции PUSH и POP:

PUSH - помещает число в стек
POP - извлекает число из стека

Инструкции PUSH и POP позволяют использовать стек в качестве временного хранилища данных. Например, в стеке очень удобно сохранять значения регистров.

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

На помощь приходят инструкции PUSH и POP. В начале каждой процедуры нужно сохранить текущие значения регистров, которые будут использованы в процедуре. В конце процедуры (перед возвращением в программу) восстановить значения регистров из стека, например так:

0ABD:0200 50            PUSH    AX      сохранение и восстановление регистров
0ABD:0201 52            PUSH    CX      происходит в обратном порядке -
0ABD:0202 51            PUSH    DX      - это соответствует принципу LIFO
0ABD:0203 B402          MOV     AH,02
0ABD:0205 B241          MOV     DL,41
0ABD:0207 B90900        MOV     CX,0009
0ABD:020A CD21          INT     21
0ABD:020C E2FC          LOOP    020A
0ABD:020E 59            POP     DX
0ABD:020F 5A            POP     CX
0ABD:0210 58            POP     AX
0ABD:0211 C3            RET

Сохранение и восстановление регистров AX, CX и DX позволяет использовать их в качестве локальных переменных внутри процедуры.

Работать со стеком надо очень аккуратно. Кроме значений регистров, в стеке хранится адрес возврата из процедуры. Т.е., если хоть один регистр не будет восстановлен, то адрес возврата вовремя не окажется на вершине стека, инструкция RET передаст в IP неправильный адрес, и нормальный ход программы будет нарушен.


Организация пауз в программах

Один из вариантов использования вложенных структур - это организация фиксированных по времени пауз. Наиболее простая процедура задержки выглядит так:

0ABD:0200 51            PUSH    CX
0ABD:0201 B9FFFF        MOV     CX,FFFF
0ABD:0204 E2FE          LOOP    0204
0ABD:0206 59            POP     CX
0ABD:0207 C3            RET

Цикл LOOP адресован сам на себя, и выполняет FFFFh - "пустых оборотов". При частоте процессора 1 GHz, такая процедура будет выполнена за несколько десятков микросекунд.

Для более длительной паузы надо поместить данную процедуру внутрь аналогичного цикла:
0ABD:0200 51            PUSH    CX
0ABD:0201 B9FF00        MOV     CX,0FFF
0ABD:0204 51            PUSH    CX
0ABD:0205 B9FFFF        MOV     CX,FFFF
0ABD:0208 E2FE          LOOP    0208      внутренний цикл
0ABD:020A 59            POP     CX
0ABD:020B E2F7          LOOP    0204      внешний цикл
0ABD:020D 59            POP     CX
0ABD:020E C3            RET

0FFFh - определяет задержку в секундах (внешний цикл);
FFFFh - определяет задержку в долях секунды (внутренний цикл).

Выполним приблизительную оценку длительности паузы. Общее количество операций CPU: 0FFF x (FFFF + 3) + 4 = FFF2002. Количество тактов CPU на каждую команду может быть 3, 4 и более. Упростим задачу, взяв 4 такта на операцию: FFF2002 x 4 = 3FFC8008 (тактов CPU). Для CPU с частотой 1 GHz получаем: 3FFC8008h / 1000000000d = 1,073512456 (секунд).

Задача:

Используя процедуру задержки, напишите программу, которая выводит на экран сообщение: "Выполняется форматирование диска C: ... ", а через несколько секунд, в другой строке: "Форматирование завершено!". Для перевода строки используйте символы с кодами Ah, Dh.


Назад | Оглавление | Дальше Новая версия документа | Форум