В качестве иллюстрации приведем программку, вычисляющую выражение 823ґ5503/(32-17) и выводящую результат на экран. При выполнении расчетов необходимо внимательно следить за очередностью выполнения операций, поэтому прежде нужно продумать порядок занесения чисел в стек (помните, что калькулятор имеет доступ только к величинам, находящимся на вершине стека). Поскольку в данном случае первым должно выполняться действие в скобках, а умножение и деление имеют одинаковый приоритет, то укладывать числа в стек будем в той же последовательности, в которой они встречаются в выражении, чтобы калькулятор мог выбирать их в обратном порядке:
ORG 60000
ENT $
CALL 3435 ;очищаем экран
LD A,2 ; и подготавливаем его для печати
CALL 5633
LD BC,823 ;заносим в стек все части выражения
CALL 11563
LD BC,5503
CALL 11563
LD A,32
CALL 11560
LD A,17
CALL 11560
RST 40 ;вызываем калькулятор
DEFB 3 ; X = 32 - 17
DEFB 5 ; X = 5503 / X
DEFB 4 ; X = 823 ґ X
DEFB 56 ; конец расчетов
CALL 11747 ;выводим результат на экран
RET
После запуска этой подпрограммы вы увидите на экране число 301931.27. Тот же результат получается и при выполнении оператора PRINT 823*5503/(32-17).
Обязательным условием при работе с калькулятором является не только соблюдение порядка выполнения расчетов. При ошибке вы в худшем случае получите неверный результат. Гораздо важнее следить за состоянием стека калькулятора, так как если после завершения программы он окажется не в том же виде, как и в начале, последствия могут даже оказаться фатальными. Для безопасности перед выходом в Бейсик можно вызвать процедуру по адресу 5829, которая очистит стек калькулятора, хотя нужно сказать, что и это лекарство в тяжелых случаях может не помочь. Поэтому при особо сложных вычислениях (а по началу и в самых простых случаях) желательно проследить за стеком на каждом шаге расчетов. Для приведенной выше программки можно сделать примерно такую схемку: Последовательно заносим числа в стек: 823 823 5503 823 5503 32 823 5503 32 17Вызываем калькулятор (RST 40): 823 5503 15 ; 32 - 17 823 366.867 ; 5503 / 15 301931.27 ; 823 ґ 366.867Из этой схемы сразу видно, что в конце расчетов на вершине стека калькулятора осталось единственное число - результат. Перед выходом в Бейсик необходимо удалить также и его. Для этого мы вызвали процедуру 11747, которая сняла полученное значение с вершины стека и напечатала его на экране. Таким образом, состояние стека осталось тем же, что до начала работы нашей программки. Если вы не собираетесь сразу после вычислений печатать результат на экране или использовать его для вывода графики, нужно каким-то образом снять полученное значение со стека и сохранить его для будущего применения. Для этого нужно обратиться к одной из перечисленных ниже процедур, выбрав из них наиболее подходящую для каждого конкретного случая. Подпрограмма, расположенная по адресу 11682, снимает число с вершины стека, округляет его до ближайшего целого и помещает в регистровую пару BC. Если число было положительным или нулем, то устанавливается флаг Z, в противном случае он будет сброшен. Может оказаться, что значение в стеке по абсолютной величине превышает максимально допустимое для регистровых пар (как это произошло в предыдущем примере). В этом случае на ошибку укажет флаг CY, который будет установлен в 1. Поэтому если вы не уверены в том, что результат не превысит 65535, лучше всегда проверять условие C и при его выполнении производить в программе те или иные коррекции, либо выводить на экран соответствующее сообщение. Процедура 8980 похожа на предыдущую, но округленное значение из стека калькулятора помещается в аккумулятор. Здесь знак числа возвращается в регистр C: 1 для положительных чисел и нуля и -1 для отрицательных. Если число в стеке превысит величину байта и выйдет из диапазона -255...+255, то будет выдано сообщение Бейсика Integer out of range. Естественно, что ни о каком продолжении программы в этом случае речи быть не может, поэтому не применяйте ее, если не уверены, что результат не окажется слишком велик. Округление чисел не всегда может оказаться удовлетворительным решением. Иногда требуется сохранить число в первозданном виде и для этого можно применить вызов процедуры ПЗУ, находящейся по адресу 11249. Она выполняет действие, обратное подпрограмме 10934 и извлекает из стека калькулятора все 5 байт числа, а затем последовательно размещает их на регистрах A, E, D, C и B. Выделив в программе на ассемблере область в 5 байт с помощью директивы DEFS 5, можно сохранить там полученный результат, чтобы впоследствии вновь им воспользоваться при расчетах. Однако приведенные процедуры мало пригодны при работе с большим количеством пятибайтовых переменных. В этом случае лучше не обращаться за помощью к ПЗУ, а написать собственные процедуры для обмена данными между переменными и стеком калькулятора. В процедуре укладки в стек пятибайтовой переменной не повредит предварительная проверка на предмет наличия свободной памяти. Для этого вызовем подпрограмму 13225, которая проверит, можно ли разместить на стеке 5 байт, и в случае нехватки памяти выдаст сообщение об ошибке Out of memory. Затем перенесем 5 байт переменной на вершину стека калькулятора и увеличим системную переменную STKEND, выполняющую ту же роль, что и регистр SP для машинного стека. Перед обращением к процедуре в паре HL нужно указать адрес пятибайтовой переменной.
PUTNUM CALL 13225 ;проверка наличия свободной памяти
LD BC,5 ;переносим 5 байт
LD DE,(23653) ;адрес вершины стека калькулятора
LDIR ;переносим
LD (23653),DE ;новый адрес вершины стека
RET
Процедура GETNUM будет выполнять противоположное действие: перемещение пяти байт числа с вершины стека калькулятора и уменьшение указателя STKEND. Адрес переменной также будем указывать в HL. Заодно можно выполнить проверку перебора стека, так как именно эта ошибка наиболее опасна.
GETNUM PUSH HL
LD DE,(23653) ;проверка достижения «дна» стека
LD HL,(23651) ;системная переменная STKBOT, адресующая
; основание стека калькулятора
AND A
SBC HL,DE ;сравниваем значения STKEND и STKBOT
JR NC,OUTDAT ;переход на сообщение, если стек
; полностью выбран
POP HL
LD BC,5
ADD HL,BC ;указываем на последний байт переменной
DEC HL
DEC DE
EX DE,HL
LDDR ;переносим 5 байт из стека в переменную
INC HL
LD (23653),HL ;обновляем указатель на вершину
; стека калькулятора
RET
OUTDAT RST 8 ;сообщение об ошибке
DEFB 13 ; Out of DATA
КОНТРОЛЬ ВРЕМЕНИ (РАБОТА С ПРЕРЫВАНИЯМИ)Прерывания с полным правом можно отнести к наиболее мощным и интересным ресурсам компьютера. К сожалению, работа с ними из Бейсика абсолютно невозможна и именно поэтому данный вопрос для многих из вас может оказаться совершенно новым и неизведанным. К настоящему моменту вы уже, должно быть, достаточно освоились с ассемблером и, надеемся, неплохо представляете, как работает микропроцессор, что дает возможность приступить наконец к изучению этой серьезной темы. Для начала выясним, что же собой представляют прерывания. Попробуем, не вдаваясь в конструкторские тонкости, объяснить принцип этого явления просто «на пальцах». Когда вы находитесь в редакторе Бейсика или GENS и размышляете над очередной строкой программы, компьютер не торопит вас и терпеливо ожидает нажатия той или иной клавиши. Может даже показаться, что микропроцессор в это время и вовсе не работает. Но, как вы уже знаете, это не так. Просто выполняется некоторая часть программы, аналогичная процедуре WAIT, описанной ранее: в цикле опрашивается системная переменная LAST_K и когда вы нажимаете какую-то клавишу, код ее появляется в ячейке 23560. Но, спрашивается, откуда он там берется? Программа ведь только читает ее значение, никак не модифицируя ее содержимое. А разрешается эта загадка довольно просто. Дело в том, что 50 раз в секунду микропроцессор отвлекается от основной программы и переключается на выполнение специальной процедуры обработки прерываний, расположенной по адресу 56, словно бы встретив команду RST 56 или CALL 56, только переход этот происходит не программным, а аппаратным путем. У процедуры 56 есть две основных задачи: опрос клавиатуры и изменение текущего значения таймера (системная переменная FRAMES - 23672/73/74). Результаты опроса клавиш также заносятся в область системных переменных, в частности, код нажатой клавиши помещается в LAST_K. После выхода из прерывания микропроцессор как ни в чем не бывало продолжает выполнять основную программу. В результате получается довольно интересный эффект: создается впечатление, будто бы параллельно работают два микропроцессора, каждый из которых выполняет свою независимую задачу. Все это прекрасно, но какую пользу для себя мы можем из этого извлечь? Ведь в ПЗУ ничего не изменишь. Действительно, от прерываний программистам было бы не много проку, если бы невозможно было переопределять адрес процедуры для их обработки. Мы уже говорили о существовании регистра I, называемого регистром вектора прерываний, а сейчас расскажем, какую роль он выполняет в программах, использующих собственные прерывания. Прежде всего вам нужно знать, что существует три различных режима прерываний. Они обозначаются цифрами от 0 до 2. Стандартный режим имеет номер 1, и о нем мы уже кое-что сказали. Нулевой режим нам не интересен, поскольку на практике он ничем не отличается от первого (именно, на практике, потому что на самом деле имеются существенные различия, но в ZX Spectrum они не реализованы). А вот о втором режиме нужно поговорить более основательно. Сначала скажем несколько слов о том, как он работает и что при этом происходит в компьютере. С приходом сигнала прерываний микропроцессор определяет адрес указателя на процедуру обработки прерываний. Он составляется из байта, считанного с шины данных (младший), который, собственно, и называется вектором прерывания и содержимого регистра I (старший байт адреса). Затем на адресную шину переписывается значение полученного указателя, но предварительно прежнее состояние шины адреса заносится в стек. Таким образом, совершается действие, аналогичное выполнению команды микропроцессора CALL. Поскольку в ZX Spectrum вектор прерывания, как правило, равен 255, то на практике адрес указателя может быть определен только регистром I. Для этого его значение нужно умножить на 256 и прибавить 255. Для установки нового обработчика прерываний нужно выполнить ряд действий. Перечислим их в том порядке, в котором они должны производиться:
Однако нужно учитывать, что некоторые внешние устройства могут изменять значение вектора прерывания. Кроме того, если ваш Speccy сработан не слишком добросовестным производителем, то вектор прерывания иногда может скакать совершенно произвольным и непредсказуемым образом. Принимая это во внимание, даже во многих фирменных играх используется несколько иной подход. Вместо записи двух байтов по определенному адресу выстраивается целая таблица размером как минимум 257 байт с таким расчетом, чтобы при любом значении вектора прерываний считывался один и тот же адрес. Понятно, что для этого все байты таблицы должны быть одинаковыми. Это несколько осложняет установку прерывания и требует больше памяти, но зато значительно увеличивает надежность работы программы. Наиболее удачным для такой таблицы представляется байт 255 (#FF). В этом случае обработчик прерываний должен находиться по адресу 65535 (#FFFF). На первый взгляд может показаться странным выбор такого адреса, ведь остается всего один байт! Но и этого единственного байта оказывается достаточным, если в него поместить код команды JR. Следующий байт, находящийся уже по адресу 0, укажет смещение относительного перехода. По нулевому адресу в ПЗУ записан код команды DI (#F3), поэтому полностью команда будет выглядеть как JR 65524. Далее в ячейке 65524 можно разместить уже более «длинную» команду JP address и заданный в ней адрес может быть совершенно произвольным. Приведем пример такой подпрограммы установки прерываний: IMON LD A,24 ;код команды JR LD (65535),A LD A,195 ;код команды JP LD (65524),A LD (65525),HL ;в HL - адрес обработчика прерываний LD HL,#FE00 ;построение таблицы для векторов прерываний LD DE,#FE01 LD BC,256 ;размер таблицы минус 1 LD (HL),#FF ;адрес перехода #FFFF (65535) LD A,H ;запоминаем старший байт адреса таблицы LDIR ;заполняем таблицу DI ;запрещаем прерывания на время ; установки второго режима LD I,A ;задаем в регистре I старший байт адреса ; таблицы для векторов прерываний IM 2 ;назначаем второй режим прерываний EI ;разрешаем прерывания RETПеред обращением к ней в регистровой паре HL необходимо указать адрес соответствующей процедуры обработки прерываний. Учтите, что в области памяти, начиная с адреса 65024, менять что-либо не желательно. Если все же возникнет такая необходимость, убедитесь прежде, что своими действиями вы не затроните установленные процедурой байты. Подпрограмма восстановления первого режима выглядит заметно проще и в комментариях уже не нуждается: IMOFF DI LD A,63 LD I,A IM 1 EI RETПри составлении процедуры обработки прерываний нужно придерживаться определенных правил. Во-первых, написанная вами подпрограмма должна выполняться за достаточно короткий промежуток времени. Желательно, чтобы ее быстродействие было сопоставимо с «пульсом» прерываний, то есть чтобы ее продолжительность не превышала 1/50 секунды. Это правило не является обязательным, но в противном случае трудно будет получить эффект «параллельности» процессов. Во-вторых, и это уже совершенно необходимо, все регистры, которые могут изменить свое значение в вашей процедуре, должны быть сохранены на входе и восстановлены перед выходом. Это же относится и к любым переменным, используемым не только в прерывании, но и в основной программе. В связи с этим не рекомендуется обращаться к подпрограммам ПЗУ, по крайней мере, до тех пор, пока вы не знаете совершенно точно, какие в них используются регистры и какие системные переменные при этом могут быть изменены. Вызов подпрограмм ПЗУ не желателен еще и потому, что некоторые из них разрешают прерывания, что совершенно недопустимо во избежание рекурсии (т. е. самовызова) обработчика, который должен работать при запрещенных прерываниях. Однако использовать команду DI в самом начале процедуры не обязательно, так как это действие выполняется автоматически и вам нужно только позаботиться о разрешении прерываний перед выходом. Если вы не хотите лишаться возможностей, предоставляемых стандартной процедурой обработки прерываний, можете завершать свою подпрограмму командой JP 56. А при использовании прерываний в бейсик-программах без этого просто не обойтись, иначе клавиатура окажется заблокирована. В общем случае обработчик прерываний может иметь такой вид:
INTERR PUSH AF
PUSH BC
PUSH DE
PUSH HL
.......
POP HL
POP DE
POP BC
POP AF
JP 56
В заключение этого раздела приведем процедуру, отсчитывающую секунды, остающиеся до окончания игры. Эта процедура может вызываться как из машинных кодов, так и из программы на Бейсике. В верхнем левом углу экрана постоянно будет находиться число, уменьшающееся на единицу по истечении каждой секунды. Для применения этой подпрограммы в реальной игре вам достаточно изменить адрес экранной области, куда будут выводиться числа и, возможно, начальное значение времени, отводимое на игру. Момент истечения времени определяется содержимым ячейки по смещению ORG+4. Если ее значение окажется не равным 0, значит игра закончилась.
ORG 60000
JR INITI
JR IMOFF
OUTTIM DEFB 0
INITI LD HL,D_TIM0
LD DE,D_TIME
LD BC,3
LDIR
XOR A
LD (OUTTIM),A
LD HL,TIM0
LD (HL),50
INC HL
LD (HL),A
INC HL
LD (HL),A
LD HL,TIMER ;установка прерывания
IMON .........
IMOFF .........
TIMER PUSH AF
PUSH BC
PUSH DE
PUSH HL
CALL CLOCK
POP HL
POP DE
POP BC
POP AF
JP 56
TIM0 DEFB 50 ;количество прерываний в секунду
TIM1 DEFB 0 ;время «проворота» третьего символа
TIM2 DEFB 0 ;время «проворота» второго символа
TIM3 DEFB 0 ;время «проворота» первого символа
D_TIM0 DEFM "150" ;символы, выводимые на экран
D_TIME DEFM "150" ;начальное значение времени
; Проверка необходимости изменения текущего времени
CLOCK LD HL,TIM0
DEC (HL)
JR NZ,CLOCK1
LD (HL),50
; Уменьшение секунд
LD A,8 ;символ «проворачивается» за 8
LD (TIM1),A ; тактов прерывания
LD HL,D_TIME+2
LD A,(HL)
DEC (HL)
CP "0"
JR NZ,CLOCK1
LD (HL),"9"
; Уменьшение десятков секунд
LD A,8
LD (TIM2),A
DEC HL
LD A,(HL)
DEC (HL)
CP "0"
JR NZ,CLOCK1
LD (HL),"9"
; Уменьшение сотен секунд
LD A,8
LD (TIM3),A
DEC HL
LD A,(HL)
DEC (HL)
CP "0"
JR Z,ENDTIM ;если время истекло
CLOCK1 LD DE,#401D ;адрес экранной области
LD A,(D_TIME) ;первый символ - сотни секунд
LD HL,TIM3
CALL PRNT
LD A,(D_TIME+1) ;второй символ - десятки секунд
CALL PRNT
LD A,(D_TIME+2) ;третий символ - секунды
; Печать символов с учетом их «проворота»
PRNT PUSH HL
LD L,A ;расчет адреса символа
LD H,0 ; в стандартном наборе
ADD HL,HL
ADD HL,HL
ADD HL,HL
LD A,60
ADD A,H
LD H,A
EX (SP),HL
LD A,(HL)
LD C,A
AND A
JR Z,PRNT1 ;если символ «проворачивать» не нужно
DEC (HL)
EX (SP),HL
NEG ;пересчет адреса символьного набора для
; создания иллюзии «проворота» цифры
LD B,A
LD A,L
SUB B
LD L,A
JR PRNT2
PRNT1 EX (SP),HL
PRNT2 LD B,8
PUSH DE
PRNT3 LD A,(HL)
LD (DE),A
INC HL
INC D
LD A,C
AND A
JR Z,PRNT4
; После цифры 9 при «провороте» должен появляться 0, а не двоеточие
LD A,L
CP 208 ;адрес символа :
JR C,PRNT4
SUB 80 ;возвращаемся к адресу символа 0
LD L,A
PRNT4 DJNZ PRNT3
POP DE
POP HL
INC DE
DEC HL
RET
; Истечение времени - выключение 2-го режима прерываний
ENDTIM POP HL ;восстановление значения указателя стека
; после команды CALL CLOCK
CALL IMOFF
LD A,1 ;установка флага истечения времени
LD (OUTTIM),A
POP HL ;восстановление регистров
POP DE
POP BC
POP AF
RET
СОДЕРЖАНИЕ:
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||