(C) Иван Рощин, Москва
Music: WORLD without LIMIT/KENOTRON
───────────────────────────────────────────────────
╔════════════════════════════════════╗
║ ║
║ Недокументированная особенность ║
║ процессора 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).
Я стал замечать, что при работе этого фраг-
мента BestViev зависает - не всегда и даже не слиш-
ком часто, а в очень редких случаях. Но, все равно,
это было не очень-то приятно. Программа, вроде бы,
не содержала никаких ошибок, по крайней мере с пер-
вого взгляда ничего подозрительного я не заметил.
Оставалось лишь прибегнуть к более сильным средс-
твам...
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'а прерывания всегда будут запре-
щены, и, соответственно, BestView всегда будет за-
висать.
....
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-го рода? Добавляю
к программе несколько команд, устанавливающих ре-
жим IM2 с обработчиком, состоящим всего из двух
команд: 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
Запускаю... И что я вижу? Верхняя часть bor-
der'а мигает зеленым цветом:
┌─────────────────────────────────┐
│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│
│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│
│▒▒▒▒ ▒▒▒▒│
│▒▒▒▒ ▒▒▒▒│
│ │
│ │
│ │
│ │
│ │
│ │
└─────────────────────────────────┘
Это говорит о том, что, во-первых, команда
LD A,R действительно иногда неверно устанавливает
бит P/V, и во-вторых, что это происходит в момент
прихода прерывания, а не когда угодно (действитель-
но, тогда бы border мигал зеленым в совершенно про-
извольных местах).
То, что верхняя часть border'а мигает, а не
постоянно окрашена в зеленый цвет, тоже получает
свое объяснение. По-видимому, команда LD A,R непра-
вильно работает лишь тогда, когда импульс прерыва-
ния приходит во время ее выполнения, а так бывает
далеко не всегда - прерывание может произойти и во
время выполнения другой команды.
* ПРОДОЛЖЕНИЕ СЛЕДУЕТ *
───────────────────────────────────────────────────
ому, команда LD A,R непра-
вильно работает лишь тогда, когда импульс прерыва-
ния приходит во время ее выполнения, а так бывает
далеко не всегда - прерывание может произойти и во
время выполнения другой команды.
* ПРОДОЛЖЕНИЕ СЛЕДУЕТ *
(C) Иван Рощин, Москва
Music: BURATINO remixed by KENOTRON
───────────────────────────────────────────────────
╔═════════════════════════════════════╗
║ ║
║ Недокументированная особенность ║
║ процессора Z80 ║
║ (окончание) ║
║ ║
╚═════════════════════════════════════╝
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
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_BP1EXX
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 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, и надо скомпенсировать это
изменение.
После этого остается только записать изменен-
ный файл на диск.
───────────────────────────────────────────────────
ь в програм-
му был добавлен дополнительный фрагмент, увеличива-
ющий регистр R на 7, и надо скомпенсировать это
изменение.
После этого остается только записать изменен-
ный файл на диск.