Deja Vu
#08
31 мая 1999 |
|
CODING - Недокументированные особенности процессора Z80.
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, и надо скомпенсировать это измене- ние. После этого остается только записать измененный файл на диск.
Другие статьи номера:
Похожие статьи:
В этот день... 21 ноября