© Nikphe/Anarchia, Alone Coder/i8/Anarchia
                             ░           ░
                            ░▒         ░▒░
        ░░░                 ░▓░        ░▒░
       ░▒▓▒                 ░▓░        ▒▓░
       ▒▒░▒     ░▒▓▓▒░░     ▒▓▒       ░▒░▒
      ░▓░ ▒    ▒▓▓▓▓▓▓▓░    ▒▒▓       ▒░░▒
      ▒▒  ▒   ░▓▒░░░░░▓▓   ░▒░▒      ░▒░░▒
     ░▓░  ▒   ▒▒      ░▒   ░▒░▒░     ░▒ ▒▓
     ▒▒   ▒   ▒░       ░   ░░ ▒░    ░▒░ ▒▓
    ░▒░   ▒  ░▓░           ▒░ ░▒    ░▒  ▒░
    ░▒    ▒  ░▒▒░         ░▒░ ░▒    ▒░ ░▓░
   ░▒░    ▒   ▒▓▒░        ░▒  ░▒   ░▒  ░▓░
   ░▒░    ▒   ░░▓▓▒       ░▒   ▒░  ▒░  ░▓░
   ▒░     ▒░    ░▒▓▒░     ▒▒   ▒░ ░▒░  ░▒░
  ░▒░     ▒░     ░░▓▓░    ▒░   ░▒ ░▒   ▒▓
  ░▒    ░░▒░       ░▒▓▒  ░▓░   ░▒ ▒░   ▒▓
  ▒░░░▒▒▓▓█░        ░░▓░ ░▒     ▒░▒   ░▒▓
 ░▓▓▓▓▓▒▒░▒░          ▒▓ ░▒     ░▓░   ░▓░
 ▒▓░░     ▒░          ░▒ ░▒     ░▒    ░▓░
 ▒▒       ▒░░░        ░▒ ░░      ░    ░▓░
░▓░       ▒░░▓▒░      ░▒ ░░      ░    ░▓░
░▓░       ▒░░▓▓▓▒░░░▒▒▒░ ▒            ░▒
░▒        ▒░ ░░▒▓▓▓▓▓▒░  ░            ░▒
░▒                                    ░░
        ░                       ░
         ░ ░     ░▓  ░▒     ░    ░    ░░
░░░   ░░  ░ ░   ░▒▒  ▓░    ░  ░░░   ░░
   ░░   ░    ░░░░▓▒ ░▓▒     ░░   ░░░
 ░░  ░░ ░░  ░ ░░░▓▒ ░▒░ ░ ░░   ░    ░░
       ░  ░░   ░░▒▓▓▓▒░░░░   ░░
    ░░   ░  ░ ░  ░░▒░▓ ░░░░░░  ░░░
   ░  ░░░    ░   ░  ░▓░░ ░░  ░
           ░░   ░   ░▒░    ░  ░░
 

      Урок ассемблера для ламеров.
              (продолжение)

   После  изучения  трёх  первых уроков вы
сильно приблизились к званию программиста,
но чтобы стать  хорошим кодером, вам необ-
ходимо научиться оптимизировать свои (и не
только) программы.
                           ──────────────┼
                         Если ты перестал│
                     встречать трудности,│
                 значит,ты сбился с пути.│
               ──────────────────────────┼

                  Глава 1
                  "СТЕК"
  1 "Стек и его прямое предназначение"

   Стек - это  хранилище, работа с которым
ведётся по следующему принципу:элемент,за-
писаный  в  стек последним, считывается из
него первым.
   Для стека можно отвести практически лю-
бую область памяти компьютера. Стек запол-
няется сверху вниз: первый элемент записы-
вается в самый конец области стека (в яче-
йку области с наибольшим адресом), следую-
щий элемент записывается "под" ним и т. д.
При чтении же  из стека первым всегда уда-
ляется самый нижний элемент. Поэтому полу-
чается,что верх стека фиксирован (это пос-
ледняя ячейка области стека),а вот низ по-
стоянно сдвигается.Как ни странно,низ сте-
ка, несмотря на то, что он располагается в
более низких адресах,называют вершиной.Для
того, чтобы  знать  текущее положение этой
вершины,  используется  регистр  SP (stack
pointer, указатель  стека). В нём хранится
адрес ячейки, в которой находится элемент,
записанный в стек последним.
   Элементы стека могут иметь "любой" раз-
мер;это могут быть и байты,и слова (2 бай-
та), и если постараться,то и двойные слова
(4 байта) и т.д.Однако имеющиеся в Z80 ко-
манды  записи  в стек и считывания из него
работают только со словами. Поэтому обычно
подстраиваются под эти команды  и считают,
что элементы стека имеют размер слова. Об-
работка же байтов и двойных слов подгоняе-
тся под обработку слов.
   Имена ячейкам стека обычно не дают,т.к.
доступ к ним всё равно будет осуществлять-
ся не по именам, а косвенно, через регистр
SP.Чаще всего не задают и начальные значе-
ния для этих ячеек,но не всегда.
   Прежде  чем начать работу со стеком, не
забудьте установить в SP его вершину.В на-
чале работы программы стек должен быть,как
правило, пустым,в этом случае SP указывает
на первую ячейку за областью стека.
   В бейсике стек можно сместить командой:
CLEAR addr-1
   Где addr - верхний адрес области стека.


          2 "Стековые команды"

   Для  работы со стеком имеется несколько
команд,которые принято называть стековыми.
   Основными  стековыми командами являются
команды записи и считывания слов на стеке.

   Запись слова в стек: PUSH rp
(rp (регистровая пара):HL,DE,BC,AF,IX,IY)
Команда  PUSH ("вталкивать")  записывает в
стек свой операнд. Условно это можно изоб-
разить так:

#0000 │▒▒│<-sp   #0000 │▒▒│
      ├──┤             ├──┤
#FFFF │▒▒│       #FFFF │ст│
      ├──┤             ├──┤
#FFFE │▒▒│       #FFFE │мл│<-sp
      ├──┤             ├──┤
#FFFD │▒▒│       #FFFD │▒▒│

   Более точно,команда PUSH действует так:
сначала  значение  регистра  SP сдвигается
вниз, и теперь указывает на свободную яче-
йку области стека,а затем в неё записывае-
тся операнд.

   Чтение слова из стека: POP rp
(rp:HL,DE,BC,AF,IX,IY)
Команда POP ("выталкивать") считывает сло-
во из вершины стека и присваивает его ука-
занному операнду.Более точно:слово из яче-
йки, на которую указывает регистр SP,пере-
сылается в операнд,а затем SP увеличивает-
ся на 2.

#0000 │▒▒│       #0000 │▒▒│<-sp
      ├──┤             ├──┤
#FFFF │ст│       #FFFF │ст│
      ├──┤             ├──┤
#FFFE │мл│<-sp   #FFFE │мл│
      ├──┤             ├──┤
#FFFD │▒▒│       #FFFD │▒▒│

  Остальные команды работы со стеком:

LD SP,addr
 -установка SP на адрес addr;

LD SP,rp
 -установка SP на адрес, хранящийся в rp
  (HL,IX,IY);

LD (addr),SP
 -сохранение SP по адресу addr;

LD SP,(addr)
 -прочитать значение SP из ячейки с адре-
сом addr;

INC SP
 -смещение SP на один байт вверх;

DEC SP
 -смещение SP на один байт вниз;

EX (SP),rp
 -обмен числа с вершины стека и rp
(HL,IX,IY);

ADD rp,SP
 -прибавить SP к rp (HL,IX,IY);

ADC HL,SP
 -сложение HL и SP с учётом переноса;

SBC HL,SP
 -вычитание SP из HL с учётом переноса.


  3 "Влияние некоторых команд на стек"

   Значительная часть глюков, появляющихся
при написании программ,возникает в связи с
неумелой работой со стеком.

   Команда вызова процедуры: CALL addr

   Эта команда работает через стек и экви-
валентна последовательности:

      LD HL,label
      PUSH HL
      JP addr
label ....

   Но не портит регистр HL.Т.е.полноценная
замена  команды  CALL  могла  бы выглядеть
так:
      PUSH HL
      LD HL,$+7
      EX (SP),HL
      JP addr

   Таким образом,команда CALL сначала сох-
раняет на стеке адрес возврата из процеду-
ры,и только потом переходит в процедуру по
адресу addr.

   Команда возврата из процедуры: RET

   Её можно приблизительно заменить на ко-
манды:

      POP HL
      JP (HL)

   То есть команда  RET берёт адрес с вер-
шины стека и пo нему возвращается из подп-
рограммы.


   4 "Правильное использование стека"

   Собственно,использование стека уже зат-
рагивалось и в первом и третьем уроках.Да,
даже  если внимательно прочитать вышеизло-
женное, то и так будет ясно, как правильно
работать со стеком.
   Так что затрону я сдесь наиболее важный
момент:
 Например,вот так работать со стеком низя:

      LD BC,????
      PUSH BC
      CALL label
      .....
      RET

label .....
      LD BC,????
      .....
      POP BC
      .....
      RET

   Так как в вызываемой подпрограмме label
после команды POP BC вы получите не сохра-
няемую  ранее  rp  BC, а адрес возврата из
процедуры, а по команде RET вернётесь не в
исходную программу,а "неизвестно куда",по-
сле  чего  вы рискуете потерять управление
над компьютером.
   В подобных ситуациях лучше делать так:

      LD BC,????
      LD (l1+1),BC
      CALL label
      .....
      RET

label .....
      LD BC,????
      .....
l1    LD BC,0
      .....
      RET

   Надеюсь, что теперь вы таких глюков ло-
вить не будете ;)))).


      5 "Очищение памяти через стек"

   Вы,надеюсь,знаете стандартную процедуру
очистки памяти через LDIR:

      LD HL,addr  ;адрес очищаемой области
      LD DE,addr+1
      LD BC,len-1  ;len:длина этой области
      LD (HL),0    ;заполнение её нулём
      LDIR         ; (очистка)

   Но  эта процедура довольно медленна, и,
например, за  счёт  неё, очистить экран за
один фрейм (71680t) не удастся.
   Для этого можно воспользоваться стеком.

   Напомню,что команды

      LD HL,0
      PUSH HL

   Заносят в память два нуля,т.е."очищают"
её.То есть,если мы установим вершину стека
на экранную область и повторим эти команды
len/2 раз (т.к. заносится сразу по два ба-
йта),то экран очистится:

      LD SP,#5800
      LD HL,0
     DUP len/2
      PUSH HL
     EDUP
      RET

   Желаемого результата мы добились,но по-
чему после очистки экрана программа повис-
ла? Да потому, что адрес возврата, который
мы сохраняли на стеке,безвозвратно потерян
;),так как мы переустановили вершину стека
в экранную область. Спросите,как же быть?!
А очень просто, нам достаточно перед вызо-
вом  программы сохранять предыдущее значе-
ние стека,а после окончания работы восста-
навливать:

      LD (stek+1),SP
      LD SP,#5800
      LD HL,0
     DUP len/2
      PUSH HL
     EDUP
stek  LD SP,0
      RET

   Есть  ещё один наш недочёт. Так как наш
экран занимает 6144 байта (без атрибутов),
то такая комбинация команд

     DUP len/2
      PUSH HL
     EDUP

   в памяти будет занимать 6144/2=3072 ба-
йта (3072 команды PUSH HL), что приводит к
опупенной  неэкономии  памяти (хотя  после
компресии  она  сильно ужмётся;). Если вам
катастрофически  мало памяти, то поступите
так:

      LD (stek+1),SP
      LD SP,#5800
      LD HL,0
      LD B,192
cls0 DUP len/2/192 ;=16
      PUSH HL
     EDUP
      DJNZ cls0
stek  LD SP,0
      RET

   Ну  вот, теперь и скорость приемлема, и
размер невелик.


      6 "Вывод спрайтов через стек"

   В  прошлом уроке я Вам рассказывал, как
выводить  спрайты размером 1x1, 1x2 и 2x1,
причём все они были по высоте кратны одно-
му знакоместу,т.е.кратны 8 байтам (если не
забыли,что в знакоместе восемь байт;).
   Попробуем  же вывести спрайт произволь-
ного  размера шириной не более 32 байт (не
забыли,что ширина экрана 32 байта?;) и вы-
сотой не более 192 байт ;).
   Спрайт расположим по адресу SPRITE. Вы-
водить надо так,чтобы при выводе не произ-
водить  лишние вычисления и учитывать пос-
ледовательность  хранения  данных спрайта.
Обычно спрайт хранят так:сначала идут под-
ряд байты спрайта,слева направо,первой ве-
рхней строчки, затем,в такой же последова-
тельности,второй строчки и т.д. %-].
   Да,высота спрайта не обязательно должна
быть кратна восьми (знакоместу!),что,впро-
чем,предстоит выбирать вам.
   Сначала покажу,как нужно выводить через
команду ldi:

   LD HL,SPRITE ;адрес спрайта
   LD DE,#4000  ;адрес вывода
   LD B,HGH     ;высота спрайта в пикселах
S0 PUSH DE
   LD C,L
  DUP LEN    ;ширина спрайта в знакоместах
   LDI
  EDUP
   POP DE
   CALL DDE  ;подрограмма расчитывания ад-
             ;реса в экране лежащего на
             ;пиксел  ниже  адреса взятого
             ;из DE (такие процедурки я
             ;приводил в третьем уроке)
   DJNZ S0
   RET

SPRITE DB ?,?,?,... ;сам спрайт

   Надо заметить,что команды

   LD C,L
  DUP LEN
   LDI
  EDUP

   поставлены  не спроста, а для скорости,
если  для вас не важна скорость (всё может
быть;),а её размер,то замените их на

   PUSH BC
   LD BC,LEN
   LDIR
   POP BC

   Но  вот  если вы хотите наоборот увели-
чить скорость вывода,то для этого восполь-
зуемся стеком.
   Метод,который я вам сейчас расскажу,ис-
пользует команду POP.
   Учитывая  все  описанные  свойства этой
команды, мы можем установить вершину стека
на адрес рассположения спрайта (LD SP,??).
Выводить будем,как и в прошлый раз,в адрес
#4000.
   Ну вот,собственно,и прогза:

   LD (STEK+1),SP ;не забываем сохранять
                ;прежнее значение стека
   LD SP,SPRITE ;устанавливаем на спрайт
   LD HL,#4000  ;адрес вывода
   LD B,HGH     ;высота в пикселах
ST LD C,L       ;сохраняем L в C
  DUP LEN/2     ;длина в байтах кратна 2-м
   POP DE       ;берём два байта
   LD (HL),E    ;выводим сначала младший
   INC L
   LD (HL),D    ;потом старший
   INC L
  EDUP
   ORG $-1      ;последний INC L не нужен
   LD L,C       ;востанавливаем L из C
   INC H        ;─┐
   LD A,H       ; │
   AND 7        ; │
   JR NZ,S0     ; │
   LD A,L       ; │
   ADD A,32     ; ├─ (Down HL)
   LD L,A       ; │
   JR C,S0      ; │
   LD A,H       ; │
   SUB 8        ; │
   LD H,A       ;─┘
S0 DJNZ ST
STEK LD SP,0
   RET
SPRITE DB ?,?,?,...

  Да,не забывайте,что ширина спрайта (LEN)
всегда должна быть кратна двум,т.к. коман-
да POP снимает сразу два байта!
   Ещё: при снятии байтов командой POP,вы-
водить  их  надо на экран в таком порядке:
сначала младший,а потом старший.
   Конечно, эта  программа далеко не опти-
мальна - для увеличения скорости ее работы
желательно  DOWN HL использовать только на
каждой 8-й строке,а на остальных использо-
вать INC H. Причем,если вы собираетесь вы-
водить  спрайт  с  точностью до пиксельной
строки, то нужно предусмотреть вход в про-
цедуру  и выход из нее не только с начала,
но  и с любого другого  места. (Этот метод
называется DMD - Down Micro Dub.)
   Если  хотите  как-то навернуть програм-
мку, то не забывайте,что в середине проце-
дурки  низя  ставить CALL, PUSH, POP (кро-
ме...),т.е. низя трогать стек... хотя,если
умудриться, то можно кое-где схитрить,нап-
ример, ещё раз сохранить состояние вершины
стека и установить третье (это мне напоми-
нает какую-то вложенность;).


  7 "Вывод всего экрана за один фрейм"

   Собственно, это,наверно,последнее,что я
хотел сказать по поводу стека ;).Это метод
уже где только не описывался, но,к сожале-
нию, не все его поняли,да и не все знают о
его существовании. Хотя метод в самом деле
очень лёгкий,и понять его не стоит большо-
го труда.
   Статичную картинку легко вывести коман-
дами
   LD BC,...
   PUSH BC
(или с помощью DE, аналогично)
записав их 3072 раза. При этом вместо мно-
готочий  в  каждой  команде  должны стоять
данные  для  выводимого экрана. Естествен-
но, такая последовательность команд должна
быть сгенерирована программно. Для написа-
ния такой программы вам потребуется книжка
с кодами команд.В дальнейшем будет предпо-
лагаться, что такая книжка у вас есть (или
вы  умеете  узнавать коды команд с помощью
STS ;)), и что написание таких генераторов
для вас не в новинку.
   А теперь поставим реальную задачу:выве-
сти  за прерывание экран, скроллируемый по
вертикали.
   Допустим, попиксельно.
   Для  этого нам  нужно разбить выводилку
на отдельные строки (назовём  такую строку
"кидалка"):

  DUP 16
   LD BC,...
   PUSH BC
  EDUP

а в промежутке между ними придётся модифи-
цировать  указатель  стека (назовём  такую
процедуру "менялка"):

   INC H
   RRCA ;один бит рег.A установлен
   JR NC,$+8─┐
   EX DE,HL  │
   LD L,(HL) │
   INC HL    │
   LD H,(HL) │
   INC HL    │
   EX DE,HL  │
           <─┘
   LD SP,HL

или проще,но медленнее:

   EX DE,HL
   LD L,(HL)
   INC HL
   LD H,(HL)
   INC HL
   EX DE,HL
   LD SP,HL

или быстрее, но с таблицей,разбросанной по
страничке:

   SET 7,H
   LD SP,HL
   POP HL
   LD SP,HL

или просто LD SP...,а параметр менять вне-
шней  процедурой. Это  самый медленный, но
экономный по памяти метод.
   Если скроллинг планируется познакомест-
ный, то  достаточно  указывать такой кусок
программы только на каждой 8-й строке,а на
остальных:

   INC H
   LD SP,HL

   Теперь  нужно выбрать, будет у нас пос-
тоянная отображаемая картинка размером вы-
ше экрана  или  же  мы  собираемся  просто
сдвигать экран и дорисовывать освободивши-
еся строки.
   В первом случае мы будет  иметь длинную
последовательность
   "менялка"
   "кидалка"
   "менялка"
   "кидалка"
   ...
в сумме  занимающую  чуть больше 2 байт на
каждый выводимый байт. Но её можно оптими-
зировать  как по длине, так и по скорости,
заменив все вхождения
   LD DE,0
   PUSH DE
(если у нас кидалка работает через DE)
на
   PUSH BC
или  как-нибудь более интересно, например,
переприсваивая только тот регистр, который
изменился:
   LD D,24
   PUSH DE
и т.п.
   Вызов  нашей  последовательности  будет
осуществляться  с любой "менялки" или "ки-
далки",как вам удобнее. Перед вызовом (че-
рез JP), естественно,надо поставить в нуж-
ном месте точку выхода - например,JP (IX).
При вызове нужно также установить в HL, A,
DE (или что у нас там требуется для "меня-
лки"?) значения, соответствующие верху эк-
рана.
   При  установке JP (IX) нужно запомнить,
что  раньше  было в этих 2 байтах, а после
выполнения выводилки восстановить их.
   Находить  адреса входов и выходов лучше
всего по табличке,которая должна быть пос-
троена во время генерации выводилки.

   Второй случай используется так: в конце
выводилки  стоит  переход  к  ее началу. А
скроллинг происходит из-за изменения точки
входа и выхода в процедуру (это, собствен-
но, одна и та же точка;))
   Освободившиеся  при  скроллинге  строки
нужно заполнять,но не на экране! С экраном
пускай работает выводилка,а мы будем поме-
щать данные в неё. Так как обычная единица
информации - знакоместная  строка, а она в
выводилке  занимает 512 байт (или чуть бо-
льше), удобно использовать для печати,ска-
жем, символов, регистры IX и IY, где IY на
256 больше, чем IX:

   POP DE
   LD C,E
   LD L,D
   LD A,(BC)
   OR (HL)
   LD (IX-128),A
   INC B
   INC H
   LD A,(BC)
   OR (HL)
   LD (IX-94),A
   INC B
   INC H
   ...
и т.д.

   Вывод  экрана сверху вниз ведёт к появ-
лению глюка, называющегося "юла" или "кле-
шинг" - когда  электронный  луч, выводящий
экран, обгоняет нашу выводилку.
   Бороться с этим надо так:вызывать выво-
дилку не целиком,а: сперва вывести верхнюю
половину  экрана, потом HALT, потом нижнюю
половину экрана. Это несложно.

──────────────────────────────────────────

   Если ты уже было совсем забросил писать
свою программу,так как не можешь и не уме-
ешь найти в ней глюк,то прочитай это - до-
лжно помочь...

      Самые частые ошибки и опечатки
     в программах начинающих кодеров:

   (указаны только отдельные примеры,в ко-
торых может встретиться ошибка.Разумеется,
вариант с другим регистром или другим нап-
равлением  инкремента/декрамента более чем
вероятен :))

1. В конструкции
      LD A,(HL)
      INC HL
   пропущен INC HL
Проявления: не  может заполниться какая-то
табличка,программа не выходит из цикла или
что-то в этом роде.
Поиск: обычно  видно уже на первом проходе
цикла при трассировке.

2. Вместо
      LD BC,#10ff
   написано
      LD B,#10ff
(видно безо всякой трассировки)

3. Программа  активно  использует стек, но
прерывания забыли отключить.
Проявления: повисание; или через некоторое
время в памяти портится какая-то табличка;
или на экране появляются загадочные пиксе-
ли;иногда никак не проявляется - например,
в случае  POP: LD  с постоянно обновляемой
табличкой, а также когда программа помеща-
ется в прерывание.
Поиск: BreakPoint на программу со стеком,и
смотрим состояние прерываний. Не помогает,
если программа вызывается регулярно,а пре-
рывания включает кто-то другой при сложном
событии (обычно - при обращении к диску).

4. Включен  IM 1, но  программа использует
регистр IY.
Проявляется  обычно как сброс/вис при дис-
ковых  операциях. Происходящий, причём, не
всегда! Ох, как меня достал этот глюк ;)
(не лечится: включите IM 2 без обращения к
RST 56  и восстанавливайте IY перед диско-
выми операциями,либо вообще не используйте
IY)

5. В конце программы допущен ORG, не имею-
щий отношения к адресу запуска программы.
Проявления: программа не запускается.
Поиск: после ассемблирования заходим в STS
и видим этот ORG.
(лучший совет:поставь в точке запуска про-
граммы метку GO, а последней строкой прог-
раммы сделай ORG GO)

6. Вместо
      INC L
      JR NZ,...
   написано
      INC L
      DJNZ ...
   (или наоборот)
Проявления: табличка занимает места больше
или меньше положенного;возможно повисание.
Поиск: трассировка цикла.Ошибка выявляется
на первом или втором проходе.