Страница Ивана Рощина > Статьи >

© Иван Рощин, Москва

ZXNet: 500:95/462.53
E-mail: bestview@mtu-net.ru
WWW: http://www.ivr.da.ru

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

Радиомир. Ваш компьютер» 3/2002)
Дата последнего редактирования: 25.02.2003.


При написании программ для низкоуровневой работы с внешними устройствами часто возникает необходимость запрограммировать задержку на определённое время. Для этого можно либо использовать таймер (если он есть), либо вставить в программу фрагмент, время выполнения которого равно требуемому времени задержки. Первый способ я здесь рассматривать не буду, а вот о втором расскажу подробнее.

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

Формула для вычисления времени задержки в тактах:


N = F t,

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

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


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


5*10–3 с * 3,5*106 Гц = 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).


Итак, как же, основываясь на вышесказанном, построить участок программы, обеспечивающий нужную задержку? Пусть 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

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

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

TAB_ADR    DB   ADR_0\256
           DB   ADR_1\256
           ..............
           DB   ADR_31\256

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)

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


Требуемая продолжительность задержки в тактах (от 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. Но я при её написании стремился к тому, чтобы минимальная задержка, достигаемая с её помощью, была как можно меньше, т.е. к увеличению области использования процедуры.


Ниже приведён текст ещё одной процедуры, для реализации более продолжительных задержек (от 469 до 232–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_CODE\256
           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_NOP\256
           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

Если в программе уже используется процедура 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 не содержат самомодифицирующихся участков; следовательно, возможна их прошивка в ПЗУ.

Страница Ивана Рощина > Статьи >