AY-Track: Muzic theme from game"FAIRLIGHT"
__________________________________________
(C) Иван Рощин
__________________________________________
╔─────────────────────────────────╗
│ │
│ Недокументированная особенность │
│ процессора Z80 │
│ │
╚─────────────────────────────────╝
Разрешается свободное распространение
этой статьи при условии не внесения каких-
либо изменений и сохранения моего копирай-
та.
1. С чего все началось
──────────────────────
Писал я как-то очередную версию прог-
раммы BestView (v2.4) и использовал в ней
вот такой фрагмент:
....
EI
CALL SUBR1
HALT
....
SUBR1 LD A,R
PUSH AF
DI
....
POP AF
DI
RET PO
EI
RET
В этом фрагменте происходит вызов про-
цедуры SUBR1, которая на время своей рабо-
ты запрещает прерывания, а при выходе вос-
станавливает прежний режим их работы.
Проверка того, разрешены или запрещены
прерывания при вызове процедуры, и восста-
новление режима прерываний происходит сле-
дующим образом:
- команда LD A,R заносит во флаг P/V сос-
тояние триггера прерываний IFF2;
- регистровая пара AF запоминается в сте-
ке (PUSH AF);
- запрещаются прерывания (DI);
- выполняются, собственно,те функции, для
которых и предназначалась процедура
SUBR1;
- содержимое AF восстанавливается (POP
AF);
- прерывания запрещаются (DI);
- если флаг P/V сброшен, происходит выход
из процедуры с запрещенными прерывания-
ми (RET PO);
- иначе происходит выход с разрешенными
прерываниями (EI:RET).
Я стал замечать, что при работе этого
фрагмента BestView зависает - не всегда, и
даже не слишком часто, а в очень редких
случаях. Но все равно,это было не очень-то
приятно. Программа, вроде бы, не содержала
никаких ошибок, по крайней мере с первого
взгляда ничего подозрительного я не заме-
тил. Оставалось лишь прибегнуть к более
сильным средствам...
2. Ситуация начинает проясняться
────────────────────────────────
После очередного зависания, я вставил
чистый диск и уверенно нажал кнопку MAGIC.
Затем загрузил отладчик "STS 6.2 +@" (не
зря я его переделывал - теперь с его по-
мощью после загрузки @-файла можно восста-
новить содержимое регистров процессора на
момент сброса программы на диск). Нажатие
пары клавиш - и вот я вижу, в каком месте
программы произошло зависание.
....
EI
CALL SUBR1
HALT <------------ вот здесь!
....
Типичный случай - прерывания запрещены,
и процессор прекратил выполнение программы
на команде HALT. Но почему прерывания ока-
зались запрещены - неясно. Ведь перед вы-
зовом процедуры SUBR1 они были разрешены
командой EI, а после окончания работы
SUBR1 они тоже должны быть разрешены -про-
цедура SUBR1 не должна оказывать влияния
на режим их работы.
Трассирую SUBR1. Все идет как положено
- и при входе, и при выходе прерывания ос-
таются разрешенными. Повторяю трассировку:
раз,другой, ... десятый. Все идет нормаль-
но.
А может быть, дело в том, что в SUBR1
что-то происходит со стеком? И из-за этого
иногда неправильно восстанавливается со-
держимое AF? Надо бы проверить...
3. Зависания: дубль второй
──────────────────────────
Ну вот, переделал программу. Теперь уж
точно буду знать, в чем дело:
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! Наверное,
просто Спектрум перегрелся и потому такие
глюки. Ничего более умного я в этот день
так и не придумал.
4. Ложный след
──────────────
На следующий день я нашел возможное
объяснение таинственному запрещению преры-
ваний. Допустим, после команды EI, но до
выполнения команды LD A,R, произошло пре-
рывание. Как известно, процедура его обра-
ботки должна заканчиваться командами EI:
RET (потому что в начале обработки проис-
ходит автоматическое запрещение прерыва-
ний). Если же обработчик прерываний завер-
шается просто командой RET, то прерывания
останутся запрещенными.
Конечно, вероятность того, что прерыва-
ние произойдет именно между командами EI и
LD A,R очень мала,но ведь и зависания про-
исходят очень редко. Так что это лишний
раз подтверждало мою гипотезу.
Тем не менее, оставалось неясным, с че-
го бы это обработчик прерываний завершался
командой RET, а не EI:RET. Я решил прове-
рить, действительно ли в этом все дело, и
для этого добавил после EI команду HALT
(см.ниже). Если обработчик прерываний дей-
ствительно завершается некорректно,то пос-
ле добавленного HALT'а прерывания всегда
будут запрещены, и, соответственно,BestVi-
ew всегда будет зависать.
....
EI
HALT <------ добавленная команда
CALL SUBR1
HALT
....
SUBR1 LD A,R
PUSH AF
DI
....
POP AF
DI
RET PO
EI
RET
Компилирую, запускаю... Совершенно не-
ожиданный результат - зависания полностью
прекратились! Хотел все так и оставить, но
все же решил разобраться, почему так полу-
чается.
5. Прием "упрощение программы"
──────────────────────────────
Когда найти ошибку обычными средствами
не удается, я удаляю из программы все что
можно, но чтобы ошибка при этом остава-
лась. В итоге, когда от программы остается
с десяток строк, ошибка заметна сразу. Так
я поступил и в этот раз:
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. Эта про-
цедура устанавливает зеленый border, если
при входе в нее прерывания были запрещены,
и не меняет цвет border, если прерывания
разрешены. Таким образом, если произойдет
самопроизвольное запрещение прерываний,
это сразу же будет заметно.
Запускаю - да, border меняет свой цвет
на зеленый. Причина столь странного пове-
дения программы остается неизвестной. Мо-
жет быть, в этом виновата процедура обра-
ботки прерываний 1-го рода? Добавляю к
программе несколько команд, устанавливаю-
щих режим IM 2 с обработчиком, состоящим
всего из двух команд: EI:RET.
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
Запускаю - тот же результат! Хотя при
трассировке и этой, и предыдущей программы
в отладчике border остается черным. Внима-
тельное изучение программы приводит к
предположению: может быть, команда LD A,R
иногда устанавливает бит P/V так, как буд-
то прерывания запрещены, в то время как на
самом деле они разрешены?
6. Неужели ошибка в процессоре?
───────────────────────────────
Еще раз изменяю программу. Теперь пре-
рывания вообще не будут запрещаться (убра-
на команда DI). Если при выполнении коман-
ды LD A,R бит P/V станет равным 0, на не-
которое время border станет зеленым (для
этого предусмотрена задержка):
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
Запускаю... И что я вижу? Верхняя часть
border'а мигает зеленым цветом:
┌─────────────────────────────────┐
│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│
│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│
│▒▒▒▒ ▒▒▒▒│
│▒▒▒▒ ▒▒▒▒│
│ │
│ │
│ │
│ │
│ │
│ │
└─────────────────────────────────┘
Это говорит о том, что, во-первых, ко-
манда LD A,R действительно иногда неверно
устанавливает бит P/V, и во-вторых - что
это происходит в момент прихода прерыва-
ния, а не когда угодно (действительно,
тогда бы border мигал зеленым в совершенно
произвольных местах).
То, что верхняя часть border'а мигает,
а не постоянно окрашена в зеленый цвет,
тоже получает свое объяснение. По-видимо-
му, команда LD A,R неправильно работает
лишь тогда, когда импульс прерывания при-
ходит во время ее выполнения, а так бывает
далеко не всегда - прерывание может прои-
зойти и во время выполнения другой коман-
ды.
7. Окончательное подтверждение
──────────────────────────────
Проверим этот факт. Пусть обработчик
прерывания определяет, в каком месте была
прервана программа. Если она была прервана
именно после команды 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
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
не связана с тем, что во время ее выполне-
ния приходит импульс прерывания, то мы
увидим, как верхняя часть border'а будет
мигать то зеленым цветом, то желтым. Но
если связь между этими двумя событиями су-
ществует, то мы должны увидеть, как
верхняя часть border'а мигает желтым цве-
том, а нижняя часть - зеленым, причем они
должны мигать совершенно синхронно.
Запускаю - и вижу именно то, что и
предполагал. Действительно, такая связь
существует:
┌─────────────────────────────────┐
│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│
│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│
│▓▓▓▓ ▓▓▓▓│
│▓▓▓▓ ▓▓▓▓│
│▒▒▒▒ ▒▒▒▒│
│▒▒▒▒ ▒▒▒▒│
│▒▒▒▒ ▒▒▒▒│
│▒▒▒▒ ▒▒▒▒│
│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│
│ │
└─────────────────────────────────┘
Но как могут быть связаны прерывания и
работа команды LD A,R? Эта команда помеща-
ет во флаг P/V содержимое триггера преры-
ваний IFF2. При разрешенных прерываниях
этот триггер равен 1, а когда приходит им-
пульс прерывания, он автоматически сбрасы-
вается в 0, чтобы исключить повторную об-
работку прерывания. Но обработка запроса
на прерывание начинается во время выполне-
ния последнего такта выполняемой команды
(т.е. команды LD A,R). И, видимо, уже
сброшенный триггер IFF2 копируется во флаг
P/V (действительно, с точки зрения процес-
сора, прерывания в этот момент уже запре-
щены).
Все сказанное относится и к команде LD
A,I. Приведенная информация была проверена
на оригинальном процессоре Z80 фирмы ZILOG
и на отечественном аналоге КР1858ВМ1.
8. К чему это приводит и что делать?
────────────────────────────────────
Применение команд LD A,R и LD A,I для
определения состояния триггера прерываний,
вообще говоря, используется во многих
программах (и даже в ПЗУ TR-DOS). Вот вам
и объяснение некоторого числа странных за-
висаний. Кажется, будто вероятность прихо-
да импульса прерывания именно во время вы-
полнения команды LD A,R невелика. Но, во--
первых, вероятность увеличивается за счет
того, что эта команда может выполняться в
программе не один раз (а для зависания
достаточно единственного неверного выпол-
нения), и, во-вторых - если в программе до
этого встречалась команда HALT, т.е.
синхронизация с прерываниями, может слу-
читься так, что команда LD A,R будет каж-
дый раз выполняться в то время, когда наи-
более вероятно очередное прерывание (так и
было в BestView).
Итак, этот способ ненадежен. Но как же
быть? Оказывается, можно со 100% точностью
определять состояние триггера прерываний
по следующему простому правилу:
- выполнить команду LD A,R;
- если флаг P/V = 1 - значит, прерывания
в самом деле разрешены;
- если флаг P/V = 0 - либо прерывания в
самом деле запрещены, либо они разрешены,
но команда LD A,R неверно установила флаг.
Чтобы устранить неопределенность, еще раз
выполняем команду LD A,R. Если и теперь
флаг P/V = 0 - значит, прерывания запреще-
ны (в самом деле, не может быть так, чтобы
и во время выполнения второй команды LD
A,R произошло прерывание - между двумя
прерываниями проходит 1/50 секунды, а меж-
ду двумя командами LD A,R - гораздо меньше
времени). Если же флаг P/V = 1 - значит,
прерывания разрешены.
Вот соответствующий фрагмент программы:
SUBR1 LD A,R
JP PE,M1
LD A,R
M1 PUSH AF
DI
....
POP AF
DI
RET PO
EI
RET
9. Как это можно использовать?
──────────────────────────────
С помощью команды 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 и т.п.).
10. Исправление STS 6.2
───────────────────────
В известном отладчике STS определение
состояния триггера прерываний также проис-
ходит с помощью команды LD A,R. Из-за это-
го может неправильно выполняться трасси-
ровка программы. При трассировке STS за-
пускает каждую команду (кроме команд пере-
дачи управления) с помощью резидента, а
после окончания ее выполнения запоминает
содержимое регистров процессора и состоя-
ние триггера прерываний. Вот тут и возмож-
ны ошибки.
Допустим, прерывания разрешены и трас-
сируется такая простейшая программа:
#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 6C1
#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, и надо скомпенсировать это измене-
ние.
После этого остается только записать
измененный файл на диск.