© 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 ...
(или наоборот)
Проявления: табличка занимает места больше
или меньше положенного;возможно повисание.
Поиск: трассировка цикла.Ошибка выявляется
на первом или втором проходе.
Сайт управляется системой
uCoz