© Иван Рощин, Москва
| ZXNet | : 500:95/462.53 |
| : bestview@mtu-net.ru | |
| WWW | : http://www.ivr.da.ru |
(ZX.SPECTRUM,
Deja Vu #8,
Voyager #3,
«Радиолюбитель. Ваш
компьютер» 4/2000 (дополненная версия))
Дата последнего редактирования: 9.04.2003.
С чего всё началось
Ситуация начинает проясняться
Зависания: дубль второй
Ложный след
Приём «упрощение программы»
Неужели ошибка в процессоре?
Окончательное подтверждение
К чему это приводит и что делать?
Как это можно использовать?
Исправление STS 6.2
Послесловие
Писал я
....
EI
CALL SUBR1
HALT
....
SUBR1 LD A,R
PUSH AF
DI
....
POP AF
DI
RET PO
EI
RET
В этом фрагменте происходит вызов процедуры SUBR1, которая на время своей работы запрещает прерывания, а при выходе восстанавливает прежний режим их работы.
Проверка того, разрешены или запрещены прерывания при вызове процедуры, и восстановление режима прерываний происходит следующим образом:
Я стал замечать, что при работе этого фрагмента BestView
зависает не всегда, и даже не слишком часто, а в очень редких
случаях. Но всё равно это было не
После очередного зависания я вставил чистый диск и уверенно нажал кнопку
«MAGIC». Затем загрузил отладчик «STS 6.2 +@» (не
зря я его переделывал: теперь с его помощью после загрузки
....
EI
CALL SUBR1
HALT <------------ Вот здесь!
....
Типичный случай: прерывания запрещены, и процессор прекратил выполнение программы на команде HALT. Но почему прерывания оказались запрещены неясно. Ведь перед вызовом процедуры SUBR1 они были разрешены командой EI, а после окончания работы SUBR1 они тоже должны быть разрешены: процедура SUBR1 не должна оказывать влияния на режим их работы.
Трассирую SUBR1. Всё идёт как положено: и при входе, и при выходе прерывания остаются разрешёнными. Повторяю трассировку: раз, другой, , десятый. Всё идёт нормально.
А может быть, дело в том, что в SUBR1
Ну вот, переделал программу. Теперь уж точно буду знать, в чём дело:
SUBR1 LD A,R
PUSH AF
DI
PUSH HL
PUSH AF
POP HL
LD (WR_HH1),HL
POP HL
....
POP AF
PUSH HL
PUSH AF
POP HL
LD (WR_HH2),HL
POP HL
DI
RET PO
EI
RET
WR_HH1 DW 0
WR_HH2 DW 0
Содержимое AF теперь запоминается не только в стеке, но и в переменной WR_HH1 (для контроля), а при выходе из процедуры снятое со стека значение запоминается в WR_HH2. Если процедура работает правильно, WR_HH1 и WR_HH2 должны совпадать, а флаг P/V быть установленным.
Запускаю Вот уже минуту BestView работает нормально Просматриваю с её помощью тот самый файл, при просмотре которого она зависла в прошлый раз Ну вот, опять! Да и неудивительно, ведь причину я не устранил. Ладно, будем разбираться.
Снова нажимаю «MAGIC», загружаю «STS 6.2 +@» и сразу же проверяю значения WR_HH1 и WR_HH2. И там, и там записано #5908. Значения совпадают следовательно, при работе со стеком ошибок не было. Но если в регистре флагов содержится #08 значит, флаг P/V сброшен, и при вызове процедуры SUBR1 прерывания были запрещены.
Но это же совершенно невозможно! Ведь в программе стоит EI: CALL SUBR1! Наверное, просто Спектрум перегрелся, и потому такие глюки. Ничего более умного я в этот день так и не придумал.
На следующий день я нашёл возможное объяснение таинственному запрещению прерываний. Допустим, после команды EI, но до выполнения команды LD A,R произошло прерывание. Как известно, процедура его обработки должна заканчиваться командами EI: RET (потому что в начале обработки происходит автоматическое запрещение прерываний). Если же обработчик прерываний завершается просто командой RET, то прерывания останутся запрещёнными.
Конечно, вероятность того, что прерывание произойдёт именно между командами EI и LD A,R, очень мала, но ведь и зависания происходят очень редко. Так что это лишний раз подтверждало мою гипотезу.
Тем не менее, оставалось неясным, с чего бы это обработчик прерываний завершался командой RET, а не EI: RET. Я решил проверить, действительно ли в этом всё дело, и для этого добавил после EI команду HALT (см. ниже). Если обработчик прерываний действительно завершается некорректно, то после добавленного HALTа прерывания всегда будут запрещены, и, соответственно, BestView всегда будет зависать.
....
EI
HALT <------------ Добавленная команда
CALL SUBR1
HALT
....
SUBR1 LD A,R
PUSH AF
DI
....
POP AF
DI
RET PO
EI
RET
Компилирую, запускаю Совершенно неожиданный результат: зависания полностью прекратились! Хотел всё так и оставить, но всё же решил разобраться, почему так получается.
Когда найти ошибку обычными средствами не удаётся, я удаляю из программы всё, что можно, но чтобы ошибка при этом оставалась. В итоге, когда от программы остаётся с десяток строк, ошибка заметна сразу. Так я поступил и в этот раз:
ORG #6000
EI
M1 CALL SUBR1
JR M1
SUBR1 LD A,R
DI
JP PO,M2
EI
RET
M2 LD A,4
OUT (254),A
RET
Вот такая программа, всего 19 байтов. Разрешаются прерывания, и в бесконечном цикле вызывается процедура SUBR1. Эта процедура устанавливает зелёный цвет бордюра, если при входе в неё прерывания были запрещены, и не меняет цвет бордюра, если прерывания разрешены. Таким образом, если произойдёт самопроизвольное запрещение прерываний, это сразу же будет заметно.
Запускаю да, бордюр становится зелёным. Причина
столь странного поведения программы остаётся неизвестной. Может быть, в этом
виновата процедура обработки прерываний
ORG #6000
LD HL,#8000
LD (HL),#81
LD DE,#8001
LD BC,#100
LDIR
LD A,#80
LD I,A
IM 2
EI
M1 CALL SUBR1
JR M1
SUBR1 LD A,R
DI
JP PO,M2
EI
RET
M2 LD A,4
OUT (254),A
RET
ORG #8181
EI
RET
Запускаю тот же результат! Хотя при трассировке и этой, и предыдущей программы в отладчике бордюр остаётся чёрным. Внимательное изучение программы приводит к предположению: может быть, команда LD A,R иногда устанавливает бит P/V так, как будто прерывания запрещены, в то время как на самом деле они разрешены?
Ещё раз изменяю программу. Теперь прерывания вообще не будут запрещаться (убрана команда DI). Если при выполнении команды LD A,R бит P/V станет равным 0, бордюр на некоторое время станет зелёным (для этого предусмотрена задержка):
ORG #6000
LD HL,#8000
LD (HL),#81
LD DE,#8001
LD BC,#100
LDIR
LD A,#80
LD I,A
IM 2
EI
M1 CALL SUBR1
JR M1
SUBR1 LD A,R
RET PE
LD A,4
OUT (254),A
LD HL,0
LD DE,0
LD BC,#600
LDIR ;WAIT
XOR A
OUT (254),A
RET
ORG #8181
EI
RET
Запускаю И что я вижу? Верхняя часть бордюра мигает зелёным цветом!
![]() |
Это говорит о том, что,
То, что верхняя часть бордюра мигает, а не постоянно окрашена в
зелёный цвет, тоже получает своё объяснение.
Проверим этот факт. Пусть обработчик прерываний определяет, в каком месте была прервана программа. Если она была прервана именно после команды LD A,R, пусть бордюр на некоторое время станет жёлтым.
ORG #6000
LD HL,#8000
LD (HL),#81
LD DE,#8001
LD BC,#100
LDIR
LD A,#80
LD I,A
IM 2
EI
M1 CALL SUBR1
JR M1
SUBR1 LD A,R
BP1 RET PE
LD A,4
OUT (254),A
LD HL,0
LD DE,0
LD BC,#600
LDIR ;WAIT
XOR A
OUT (254),A
RET
ORG #8181
EXX
EX AF,AF'
POP HL
PUSH HL
LD DE,BP1
AND A
SBC HL,DE
JR NZ,NE_BP1
LD A,6
OUT (254),A
LD HL,0
LD DE,0
LD BC,#600
LDIR ;WAIT
NE_BP1 EXX
EX AF,AF'
EI
RET
Если неправильная работа команды LD A,R не связана с тем, что во время её выполнения приходит импульс прерывания, то мы увидим, как верхняя часть бордюра будет мигать то зелёным цветом, то жёлтым. Но если связь между этими двумя событиями существует, то мы должны увидеть, как верхняя часть бордюра мигает жёлтым цветом, а нижняя часть зелёным, причём они должны мигать совершенно синхронно.
Запускаю и вижу именно то, что и предполагал. Действительно, такая связь существует.
![]() |
Но как могут быть связаны прерывания и работа команды LD A,R? Эта команда помещает во флаг P/V содержимое триггера прерываний IFF2. При разрешённых прерываниях этот триггер равен 1, а когда приходит импульс прерывания, он автоматически сбрасывается в 0, чтобы исключить повторную обработку прерывания. Но обработка запроса на прерывание начинается во время выполнения последнего такта выполняемой команды (т.е. команды LD A,R). И, видимо, уже сброшенный триггер IFF2 копируется во флаг P/V (действительно, с точки зрения процессора, прерывания в этот момент уже запрещены).
Всё сказанное относится и к команде LD A,I. Приведённая информация была проверена на оригинальном процессоре Z80 фирмы ZiLOG и на отечественном аналоге КР1858ВМ1.
Применение команд LD A,R и LD A,I для определения состояния триггера
прерываний, вообще говоря, используется во многих программах (и даже в ПЗУ
Итак, этот способ ненадёжен. Как же быть? Оказывается, можно со 100% точностью определять состояние триггера прерываний по следующему простому правилу:
Вот соответствующий фрагмент программы:
SUBR1 LD A,R
JP PE,M1
LD A,R
M1 PUSH AF
DI
....
POP AF
DI
RET PO
EI
RET
С помощью команды LD A,R удобно выполнять тестирование процессора, чтобы распознать выполнение программы под эмулятором. Эмулятор выполняет команды Z80 последовательно, одну за другой, и команда LD A,R всегда будет правильно устанавливать флаг P/V. А в реальном Z80 это не так.
Вот простейшая процедура для тестирования процессора, которая возвращает в аккумуляторе 1, если она запущена на реальном Z80, и 0 в противном случае. Она пытается 65536 раз прочитать регистр R при разрешённых прерываниях, и если при этом хотя бы один раз флаг P/V установится в 0 делается вывод, что процедура работает на реальном Z80.
TESTZ80 EI
LD BC,0
M1 LD A,R
JP PO,QUIT
INC BC
LD A,B
OR C
JR NZ,M1
RET
QUIT LD A,1
RET
Если распознался эмулятор, можно либо прекращать выполнение программы (своеобразная защита), либо отключать некоторые участки программы, которые могут неверно работать под эмулятором (например, вместо прямой работы с ВГ93 использовать точку входа #3D13 и т.п.).
В известном отладчике STS определение состояния триггера прерываний также
происходит с помощью команды LD A,R.
Допустим, прерывания разрешены и трассируется такая простейшая программа:
#8000 NOP
#8001 JR #8000
Прекратив трассировку через некоторое время (при отключённой опции «Indicate» одной минуты вполне достаточно), мы увидим, что прерывания оказались запрещены. Если бы трассировалась реальная программа, такое запрещение прерываний могло оказать влияние на весь ход её дальнейшего выполнения и даже привести к зависанию (если при трассировке попалась бы команда HALT).
Совершенно очевидно, что в STS надо внести исправления. Вот как это сделать для версии 6.2.
Сначала нужно запустить STS и загрузить файл «sts6.2 <C>», в котором и будут производиться исправления.
Затем нужно отыскать свободные 14 байтов их назначение будет объяснено ниже. Можно использовать буфер функции пользователя (с адреса #FE37). Но в той версии STS, которую я использую, этот буфер занят под процедуру дизассемблирования с метками ассемблера ZX ASM, поэтому я решил сократить некоторые текстовые сообщения:
"Block" > "Bl." (экономится 2 байта) "Save" > "S." (----/----- 2 --/--) "Load" > "L." (----/----- 2 --/--) " DEFB" > " " (----/----- 4 --/--) "FileName" > "Name" (----/----- 4 --/--)
Для этого с адреса #EB24 нужно ввести следующую последовательность байтов:
#EB24: AE 46 72 6F ED 54 EF 46 #EB2C: 69 6C E5 53 65 63 74 6F #EB34: F2 53 AE 4C AE 53 74 6F #EB3C: 70 20 69 E6 42 61 6E EB #EB44: 51 75 69 F4 54 72 61 63 #EB4C: E5 53 74 61 72 F4 44 69 #EB54: 73 61 73 ED A0 46 69 6C #EB5C: E5 42 41 53 49 C3 20 44 #EB64: 4F D3 |
По адресу #E702 заменяем значение #0A на #0E, чтобы правильно печаталось имя файла (т.к. вместо строки «FileName» осталось просто «Name»).
Итак, теперь с адреса #EB66 свободно 14 байтов. Смотрим, где в STS происходит определение состояния триггера прерываний:
#DFFE: LD (#5BA1),SP
LD SP,#5BA1
PUSH BC
PUSH AF
LD A,R
DI
LD BC,#7FFD
LD A,#1F
OUT (C),A
LD B,#BF
LD A,#00
OUT (C),A
JP #E028
Заменим команды LD A,R: DI на NOP, а команду JP #E028 на JP #EB66. С адреса #EB66 поместим такой фрагмент:
#EB66: LD A,R
JP PO,#EB6E \
NOP |
.- JR #EB70 |
| LD A,R <---'
`> DI
JP #E028
Обратите внимание: этот фрагмент в любом случае при своей работе увеличивает регистр R на одинаковую величину (на 7). Дело в том, что далее будет выполнена ещё одна команда LD A,R, на этот раз нужная уже для определения значения регистра R, и будет произведена коррекция полученного значения, т.к. значение регистра R увеличивается с каждой выполненной командой, а надо узнать его значение на момент окончания выполнения трассируемой команды. Вот как это выглядит:
#DCA2: LD A,#5A
LD HL,#FEF4
SLA (HL)
RLA
ADD A,(HL)
RRCA
LD (HL),A
RET
Константу #5A по адресу #DCA3 следует заменить на #53, т.е. уменьшить на 7: ведь в программу был добавлен дополнительный фрагмент, увеличивающий регистр R на 7, и надо скомпенсировать это изменение.
После этого остаётся только записать изменённый файл на диск.
С тех пор, как эта статья была написана, прошло уже много времени, и у меня накопилась дополнительная информация по рассматриваемому вопросу:
Остаётся лишь пожалеть, что в отечественной литературе такой важный вопрос не был освещён. Это тем более странно, что различные «справочные пособия» по микропроцессору Z80 были основаны на фирменной документации.
LD A,R
JP PE,M1
LD A,R
M1 ............
Если состояние прерываний требуется определять более чем в одном месте, имеет смысл реализовать этот фрагмент в виде подпрограммы:
LD A,R
RET PE
LD A,R
RET
Но если состояние прерываний нужно определять в одиннадцати и более местах программы, и при этом установлен режим прерываний IM 2, то можно ещё сильнее сократить объём! Для этого в программе надо использовать команду LD A,R, а обработчик прерываний должен проверять, в каком месте была прервана программа, и уменьшать адрес возврата на два, если она была прервана после команды LD A,R. Тогда после возврата процессор ещё раз выполнит эту команду, правильно установив флаги.
Вот пример обработчика прерываний, решающего эту задачу:
;Запоминаем AF:
PUSH HL
PUSH AF
POP HL
LD (SAVE_AF),HL
POP HL
;Проверяем, после какой команды
;произошло прерывание:
EX (SP),HL
DEC HL
LD A,(HL)
CP #5F
JR NZ,NO_LD_A_R
DEC HL
LD A,(HL)
CP #ED
JR Z,YES_LD_A_R
INC HL
NO_LD_A_R INC HL
YES_LD_A_R EX (SP),HL
;Сохраняем в стеке HL и ранее
;запомненное значение AF:
PUSH HL
LD HL,0
SAVE_AF EQU $-2
PUSH HL
;Если в вашей процедуре обработки
;прерываний используются ещё какие-либо
;регистры, они также должны быть
;сохранены с последующим восстановлением
......... (ваша процедура обработки)
POP AF
POP HL
EI
RET
При использовании такого способа необходимо учесть некоторые тонкости. Дело в том, что обработчик прерываний может определить, после какой команды была прервана программа, только если команды в программе выполняются последовательно. A значит, в вашей программе не должно быть такого:
JR M1 (или другая команда
передачи управления на M1)
.......
.......
LD A,R
M1 .......
Ведь если прерывание произойдёт после команды JR M1, то обработчик не сможет отличить эту ситуацию от той, когда прерывание произошло после команды LD A,R. И в результате команда LD A,R будет выполнена после выхода из обработчика, что совершенно не нужно.
Также надо учитывать, что код команды LD A,R это два байта: #ED #5F. И если в вашей программе встретится такое:
DB #XX,#XX,#XX,#ED ;Какие-то данные.
M1 LD E,A ;Код этой команды - #5F!
то в случае, когда прерывание произойдёт после команды LD E,A,
обработчик
Процедура не портит регистры, её длина 27 байтов. Она обладает свойством реентерабельности (повторной входимости), что может оказаться необходимым в случае, если обработчик прерываний сам вызывает её во время своей работы.
INT_AND_DI PUSH AF
LD A,R
JP PE,NOW_EI
LD A,R
JP PE,NOW_EI
;Прерывания при вызове запрещены -
;просто выходим:
POP AF
RET
;Прерывания при вызове разрешены -
;запрещаем их, заносим в стек #51
;и выходим:
NOW_EI DI
;Далее выполнение идет при запрещённых
;прерываниях, поэтому сохранение HL в
;теле процедуры не нанесёт ущерба её
;реентерабельности.
POP AF
LD (INT_S_HL),HL
LD HL,#0051
EX (SP),HL
PUSH HL ;Адрес возврата.
LD HL,0
INT_S_HL EQU $-2
RET
|
Другие мои статьи о недокументированных особенностях процессора Z80:
|