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