ZXNet эхоконференция «code.zx»


тема: Программирование задержек на ассемблере Z80



от: Ivan Roshin
кому: All
дата: 02 Apr 2002
Hello, All!

═══════════════════ wait_1 .W ══════════════════

(c) Иван Рощин, Москва

Fido : 2:5020/689.53
ZXNet : 500:95/462.53
E-mail: asder_ffc@softhome.net
WWW : http://www.ivr.da.ru

Программирование задержек на ассемблере Z80
═══════════════════════════════════════════

("Радиомир. Ваш компьютер" 3/2002)

При написании программ для низкоуровневой работы с внешними
устройствами часто возникает необходимость запрограммировать
задержку на определенное время. Для этого можно либо
использовать таймер (если он есть), либо вставить в программу
фрагмент, время выполнения которого равно требуемому времени
задержки. Первый способ я здесь рассматривать не буду, а вот о
втором расскажу подробнее.
Каждая команда процессора Z80 выполняется за определенное
количество тактов. Зная величину требуемой задержки в тактах,
можно написать фрагмент программы с соответствующим временем
выполнения. (Сразу же замечу, что работать такой фрагмент должен
при запрещенных прерываниях.)
Формула для вычисления времени задержки в тактах:

N=F*t

где N - количество тактов,
F - тактовая частота процессора,
t - время задержки.

Так как время выполнения команды в процессоре Z80 может
выражаться лишь целым числом тактов, полученный результат
следует округлить до ближайшего целого числа.

Пример: пусть нужна задержка в 5 мс, тактовая частота - 3,5
МГц. Тогда требуемое количество тактов равно:

5*10^-3 с * 3,5*10^6 Гц = 17500.

Начнем с программирования небольших задержек. Минимальная
величина задержки соответствует наименьшему времени выполнения
команды Z80 - 4 такта.
Для задержки на 4 такта идеально подходит команда NOP - "нет
операции". Ее выполнение не зависит от значения флагов и
регистров, и она не выполняет каких-либо действий (иначе говоря,
не имеет побочных эффектов).
С помощью цепочки NOP'ов легко получить задержку на 8, 12,
16,... тактов (т.е. на число вида 4N).
Задержку на 5 тактов можно получить с помощью команды
условного выхода из подпрограммы в случае, когда условие не
выполняется. Таких команд восемь:

RET NC (выход при C=0)
RET C (выход при C=1)
RET NZ (выход при Z=0)
RET Z (выход при Z=1)
RET P (выход при S=0)
RET M (выход при S=1)
RET PO (выход при P/V=0)
RET PE (выход при P/V=1)

Побочных эффектов они не имеют. Добавляя NOP'ы, можно
получить задержку на 9, 13, 17,... тактов (т.е. на число вида
4N+1).
Очевидно, для использования одной из вышеперечисленных
команд необходимо знать состояние хотя бы одного из четырех
флагов (C, Z, S или P/V) к моменту ее выполнения. Если состояние
флагов неизвестно, то задержку на 5 тактов получить нельзя, но
на 9, 13, 17,... - можно. Для этого надо вначале установить флаг
переноса с помощью команды SCF (время ее выполнения - 4 такта),
после чего использовать команду RET NC, а затем при
необходимости добавить NOP'ы. Установка флага переноса будет в
этом случае побочным эффектом.
Задержку на 6 тактов можно получить с помощью одной из
следующих команд:

INC BC DEC BC
INC DE DEC DE
INC HL DEC HL
INC SP DEC SP

Добавляя NOP'ы, можно получить задержку на 10, 14, 18,...
тактов (т.е. на число вида 4N+2).
Вышеуказанные команды не влияют на флаги, однако изменяют
содержимое соответствующей регистровой пары. Если ни одну из
четырех пар изменять нельзя, то и задержку на 6 тактов получить
нельзя. Но на 10, 14, 18,... тактов - можно, с помощью команды
безусловного перехода на следующую команду (JP $+3), на
выполнение которой тратится 10 тактов, и добавленных при
необходимости командах NOP.
Задержку на 7 тактов можно получить различными способами.
Один из них - использование команды LD r,(HL), где r - один из
регистров A,B,C,D,E,H,L. Эта команда не влияет на флаги, но
изменяет содержимое участвующего в операции регистра. Если ни
один регистр изменять нельзя, но изменять флаги можно, то
годится команда CP (HL). Если нельзя изменять ни регистры, ни
флаги, - можно использовать команду условного перехода JR (при
невыполняющемся условии). Таких команд четыре:

JR NC (переход при C=0)
JR C (переход при C=1)
JR NZ (переход при Z=0)
JR Z (переход при Z=1)

Естественно, чтобы одну из них можно было использовать,
необходимо знать состояние флага C или Z на момент ее
выполнения.
Добавляя NOP'ы, можно получить задержку на 11, 15, 19,...
тактов (т.е. на число вида 4N+3).

════════════════════════════════════════════════

С уважением, Иван Рощин.

от: Ivan Roshin
кому: All
дата: 02 Apr 2002
Hello, All!

═══════════════════ wait_2 .W ══════════════════

Итак, как же, основываясь на вышесказанном, построить
участок программы, обеспечивающий нужную задержку? Пусть x -
величина задержки в тактах. Разделим x на 4, получим частное a и
остаток b. Тогда b+4 - это длина первой команды (4, 5, 6 или 7
тактов), а a-1 - количество добавляемых NOP'ов.

Рассмотрим это на примерах.

Пример 1: x=18. Делим на 4: a=4, b=2. Значит, длина первой
команды будет 2+4=6 тактов, а количество NOP'ов будет 4-1=3.
Соответствующий фрагмент программы будет выглядеть так:

INC HL
NOP
NOP
NOP

Пример 2: x=29. Делим на 4: a=7, b=1. Значит, длина первой
команды - 5 тактов, количество NOP'ов - 6. Как уже ранее
упоминалось, задержка на 5 тактов реализуется условным RET'ом, а
для его применения надо знать состояние хотя бы одного из
четырех флагов. Предположим, что о состоянии флагов ничего не
известно. Тогда придется сделать так: с помощью
последовательности команд SCF: RET NC получаем задержку в 9
тактов, а NOP'ов будет на один меньше. В результате получим:

SCF
RET NC
NOP
NOP
NOP
NOP
NOP

Так как, вообще говоря, NOP'ов при таком программировании
задержки может получиться довольно много, то, чтобы не
"раздувать" текст программы и не ошибиться в подсчете NOP'ов при
наборе текста, удобно использовать директиву ассемблера DS N
(другой вариант записи - DEFS N). Она выделяет участок памяти
длиной N байтов и (обычно) заполняет его нулями. Ну а 0 - это
как раз и есть код команды NOP. Только убедитесь, что
используемый вами ассемблер заполняет область действительно
нулями. Если это не так, придется указывать нулевое значение
явно: DS N,0.
Таким образом, рассмотренные выше примеры могут быть
записаны так:

INC HL
DS 3

и

SCF
RET NC
DS 5

Если вас не устраивает, что длинная цепочка NOP'ов занимает
много места в памяти, то можно использовать следующий фрагмент
программы:

LD B,N
DJNZ $

Время его выполнения - 13N+2 тактов (где N=1..255). Таким
образом, если нужна задержка на x тактов, делим x-2 на 13,
получаем частное a и остаток b. Значение a показывает, сколько
раз надо повторить цикл (это число, загружаемое в регистр B), а
b - сколько тактов при этом остается (задержку на это время
придется реализовать обычным способом). Если b=1, 2 или 3, то,
так как задержку на это время получить нельзя, придется
уменьшить a на 1 и увеличить b на 13 (тем самым увеличив
оставшуюся задержку до 14, 15 или 16 тактов).

Пример: x=140. Делим на 13: a=10, b=8. Следовательно,
количество повторов цикла будет 10; задержку на оставшиеся 8
тактов обеспечиваем с помощью двух команд NOP. Получаем:

LD B,10
DJNZ $
DS 2

════════════════════════════════════════════════

С уважением, Иван Рощин.

от: Ivan Roshin
кому: All
дата: 02 Apr 2002
Hello, All!

═══════════════════ wait_3 .W ══════════════════

Теперь перейдем к рассмотрению способов реализации более
длительных задержек. Для задержек от 188 до 65535 тактов удобно
использовать следующую 98-байтную процедуру:

ORG #8000 ;или другой адрес, кратный 256

TAB_ADR DB ADR_0256
DB ADR_1256
..............
DB ADR_31256

ADR_28 NOP
ADR_24 NOP
ADR_20 NOP
ADR_16 NOP
ADR_12 NOP
ADR_8 NOP
ADR_4 NOP
ADR_0 NOP ;4 такта
RET

ADR_29 NOP
ADR_25 NOP
ADR_21 NOP
ADR_17 NOP
ADR_13 NOP
ADR_9 NOP
ADR_5 NOP
ADR_1 RET NZ ;5 тактов
RET

ADR_30 NOP
ADR_26 NOP
ADR_22 NOP
ADR_18 NOP
ADR_14 NOP
ADR_10 NOP
ADR_6 NOP
ADR_2 INC HL ;6 тактов
RET

ADR_31 NOP
ADR_27 NOP
ADR_23 NOP
ADR_19 NOP
ADR_15 NOP
ADR_11 NOP
ADR_7 NOP
ADR_3 LD A,(HL) ;7 тактов
RET

WAIT LD DE,-156
ADD HL,DE
LD A,L
AND 31
LD E,A

;Чтобы разделить HL на 32, достаточно сдвинуть HL на 5 разрядов
;вправо, но есть более короткий и быстрый способ: сдвинуть HL на
;3 разряда влево, помещая выдвигаемые старшие биты результата в
;аккумулятор, и затем произвести перестановку: A -> H -> L.

XOR A
ADD HL,HL
RLA
ADD HL,HL
RLA
ADD HL,HL
RLA
LD L,H
LD H,A

;Цикл с временем выполнения 32 такта:

WAIT_1 DEC HL
LD A,H
OR L
NOP ;для
NOP ;задержки
JP NZ,WAIT_1

EX DE,HL
LD H,TAB_ADR/256
LD L,(HL)
JP (HL)

Напомню, что оператор "" - это вычисление остатка от
деления. В вышеприведенной процедуре он используется, чтобы
получить младший байт двухбайтной величины. Если в используемом
вами ассемблере этот оператор не поддерживается, то, возможно,
предусмотрен специальный оператор вычисления младшего байта, или
же ассемблер сам отбрасывает старший байт, когда результат
должен быть однобайтным. В крайнем случае можно воспользоваться
равенством A256=A-((A/256)*256).

════════════════════════════════════════════════

С уважением, Иван Рощин.

от: Ivan Roshin
кому: All
дата: 02 Apr 2002
Hello, All!

═══════════════════ wait_4 .W ══════════════════

Требуемая продолжительность задержки в тактах (от 188 до
65535) указывается в регистровой паре HL перед вызовом
процедуры. При этом не нужно специально учитывать, что
выполнение команд загрузки этого числа в HL и вызова процедуры
тоже займет какое-то время: это уже учитывается в самой
процедуре.
Если, скажем, в каком-то месте программы нужна задержка в
500 тактов, просто пишем следующее:

LD HL,500
CALL WAIT

Как видим, применение этой процедуры не требует от
программиста специальных знаний о времени выполнения команд. Не
нужно каждый раз думать, какой последовательностью команд
реализовать ту или иную задержку. Ставим нужное число и все!
Проще не бывает!
Также этой процедурой очень удобно пользоваться, если точная
величина задержки заранее не известна, и приходится "подгонять"
ее в процессе отладки программы. Изменить время задержки можно,
изменив лишь одно число в коде программы с помощью отладчика.
При своей работе процедура изменяет значения регистровых пар
HL и DE, а также аккумулятора и регистра флагов. Если это
нежелательно, то, в зависимости от ситуации, можно либо
сохранять их в стеке и затем восстанавливать (командами PUSH,
POP), либо на время выполнения процедуры устанавливать
альтернативный набор регистров, если там не содержится ничего
нужного (командами EXX, EX AF,AF'). При этом из указываемого в
HL времени задержки надо будет вычесть время выполнения команд
сохранения и восстановления регистров (следите, чтобы при этом
указываемое время задержки не стало меньше минимального - 188
тактов!). Напомню: время выполнения команд PUSH HL, PUSH DE,
PUSH AF - 11 тактов; POP HL, POP DE, POP AF - 10 тактов; EXX,
EX AF,AF' - 4 такта.

Пример: пусть нужна задержка в 750 тактов, и значения HL и
DE не должны портиться (альтернативные регистры - тоже). Значит,
HL и DE надо сначала сохранить в стеке (PUSH HL, PUSH DE), а
после вызова процедуры - восстановить (POP DE, POP HL).
Подсчитываем время выполнения этих команд: 11+11+10+10=42 такта.
В программе пишем:

PUSH HL ;сохранение
PUSH DE ;регистров

LD HL,750-42
CALL WAIT

POP DE ;восстановление
POP HL ;регистров

Так как процедура WAIT - универсальная, то в случае
единичного ее использования она займет, очевидно, больше места в
памяти, чем специально написанный для реализации конкретной
задержки фрагмент программы. Но если процедура вызывается во
многих местах программы, с различными временами задержки, то это
будет уже выгоднее по сравнению с тем, как если бы в каждом
месте был свой фрагмент программы, реализующий задержку.
Процедуру WAIT можно было бы, в принципе, сделать короче и
не привязывать к адресу, кратному 256. Но я при ее написании
стремился к тому, чтобы минимальная задержка, достигаемая с ее
помощью, была как можно меньше, т.е. к увеличению области
использования процедуры.

════════════════════════════════════════════════

С уважением, Иван Рощин.

от: Ivan Roshin
кому: All
дата: 02 Apr 2002
Hello, All!

═══════════════════ wait_5 .W ══════════════════

Ниже приведен текст еще одной процедуры, для реализации
более продолжительных задержек (от 469 до 2^32-1 тактов).

WAIT_LONG PUSH BC
PUSH AF

LD BC,-405
ADD HL,BC
LD BC,-1
EX DE,HL
ADC HL,BC
EX DE,HL

LD A,L
AND 3
ADD A,A
ADD A,A ;*4
ADD A,WAIT_CODE256
LD C,A
LD A,WAIT_CODE/256
ADC A,0
LD B,A
PUSH BC

LD A,L
RRA
RRA
CPL
AND 15
ADD A,WAIT_NOP256
LD C,A
LD A,WAIT_NOP/256
ADC A,0
LD B,A
PUSH BC

;Сдвигаем DEHL:

XOR A
ADD HL,HL
EX DE,HL
ADC HL,HL
EX DE,HL
RLA
ADD HL,HL
EX DE,HL
ADC HL,HL
EX DE,HL
RLA
LD L,H
LD H,E
LD E,D
LD D,A

;В DE - количество повторов цикла (каждое - 64 такта).

LD BC,-1

WAIT_L1 ADD HL,BC
INC L ;Проверяем: L=0?
DEC L ;(не изменяя флаг C).
JP Z,WAIT_L2
EX DE,HL
ADC HL,BC
EX DE,HL
JR WAIT_L1

WAIT_L2 LD A,H ;L=0, а HDE=0?
OR D ;Флаг C сбрасывается.
OR E
NOP ;Для
NOP ;задержки
RET C ;в 4+4+5=13 тактов.
JP NZ,WAIT_L1
RET ;Переход по адресу, ранее
;помещенному в стек.

WAIT_NOP DS 15,0 ;15 команд NOP.
RET ;Переход по адресу, ранее
;помещенному в стек.

WAIT_CODE NOP
JP WAIT_EXIT
RET C
JP WAIT_EXIT
INC HL
JP WAIT_EXIT
LD A,(HL)
JP WAIT_EXIT

WAIT_EXIT POP AF
POP BC
RET

Длина процедуры 119 байтов. Требуемая продолжительность
задержки в тактах указывается так: старшие разряды загружаются
в регистровую пару DE, младшие - в HL. Как и в предыдущей
процедуре, время выполнения команд загрузки и вызова специально
учитывать не нужно.

Пример: пусть нужна задержка в 100000 тактов. В
шестнадцатеричном виде это #186A0. В программе пишем:

LD HL,#0001
LD DE,#86A0
CALL WAIT_LONG

════════════════════════════════════════════════

С уважением, Иван Рощин.

от: Ivan Roshin
кому: All
дата: 02 Apr 2002
Hello, All!

═══════════════════ wait_6 .W ══════════════════

Если в программе уже используется процедура WAIT для
задержек, меньших 469 тактов, а величина необходимой задержки не
во много раз превосходит 65535, то можно обойтись без процедуры
WAIT_LONG (тем самым сэкономив в размере программы), просто
поставив рядом несколько вызовов процедуры WAIT.

Пример: нужна задержка в 150000 тактов. Делим 150000 на
65535, получаем частное 2 и остаток 18930. В программе пишем:

LD HL,65535
CALL WAIT
LD HL,65535
CALL WAIT
LD HL,18930
CALL WAIT

Напоследок отмечу, что процедуры WAIT и WAIT_LONG не
содержат самомодифицирующихся участков; следовательно, возможна
их прошивка в ПЗУ.

════════════════════════════════════════════════

С уважением, Иван Рощин.




Темы: Игры, Программное обеспечение, Пресса, Аппаратное обеспечение, Сеть, Демосцена, Люди, Программирование

Похожие статьи:
Ответ - Открытое письмо в ODDY#9: рассказ о Краснодарской ZX сцене.
Реклама - Реклама и объявления ...
Сплошные приколы - 5 приколов. Сборник высказываний советских офицеров (продолжение).
Юмор - анекдоты.
Doomdarks Revenge - полное описание ролевой игры, в которой вам предстоит сражаться со злом.

В этот день...   16 апреля