КОНТРОЛЬ ВРЕМЕНИ (РАБОТА СПРЕРЫВАНИЯМИ)
КОНТРОЛЬ ВРЕМЕНИ (РАБОТА С ПРЕРЫВАНИЯМИ)
Прерывания с полным правом можно отнести к наиболее мощным и интересным ресурсам компьютера. К сожалению, работа с ними из Бейсика абсолютно невозможна и именно поэтому данный вопрос для многих из вас может оказаться совершенно новым и неизведанным. К настоящему моменту вы уже, должно быть, достаточно освоились с ассемблером и, надеемся, неплохо представляете, как работает микропроцессор, что дает возможность приступить наконец к изучению этой серьезной темы.
Для начала выясним, что же собой представляют прерывания. Попробуем, не вдаваясь в конструкторские тонкости, объяснить принцип этого явления просто «на пальцах». Когда вы находитесь в редакторе Бейсика или 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.
Для установки нового обработчика прерываний нужно выполнить ряд действий. Перечислим их в том порядке, в котором они должны производиться:
- Запретить прерывания, так как есть вероятность того, что сигнал прерываний придет во время установки, а это может привести к нежелательным последствиям. Достигается это выполнением команды микропроцессора DI.
- Записать в память по рассчитанному заранее адресу указатель на процедуру обработки прерываний (то есть адрес этой процедуры).
- Задать в регистре вектора прерываний I старший байт адреса указателя на обработчик.
- Установить командой IM 2 второй режим прерываний.
- Вновь разрешить прерывания командой EI.
- Запретить прерывания.
- Не помешает восстановить значение регистра I, записав в него число 63.
- Назначить командой IM 1 первый режим прерываний.
- Разрешить прерывания.
Естественно, что к этому моменту сама процедура обработки прерываний должна иметься в памяти. Для возврата к стандартному режиму обработки прерываний нужно выполнить похожие действия:
Несколько подробнее нужно остановиться на втором и третьем пунктах установки прерываний. Предположим, что процедура-обработчик находится по адресу 60000 (#EA60) и память, начиная с адреса 65000, никак в программе не используется. Значит указатель можно поместить именно в эту область. Для регистра I в этом случае можно выбрать одно из двух значений: 253 или 254. Тогда для размещения указателя можно использовать либо адреса 65023/65024 (253ґ256+255/256) либо 65279/65280 (254ґ256+255/256). Например, при I равном 254 запишем по адресу 65279 младший байт адреса обработчика - #60, а в 65280 поместим старший байт - #EA.
Однако нужно учитывать, что некоторые внешние устройства могут изменять значение вектора прерывания. Кроме того, если ваш 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 ;установка прерывания
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