Получив из предыдущей главы некоторое представление о структуре ассемблерной строки, редакторе GENS и обо всем прочем, что совершенно необходимо для работы с ассемблером, можно, наконец, приступать к программированию. С чего же начать? Наверное, мы не сильно ошибемся, если предположим, что первейшее желание любого, начинающего изучать новый язык - это получить что-то на экране. Пусть это будет всего лишь какая-нибудь надпись, или даже просто одна-единственная буква.
Именно с таких простых действий мы и начнем наши опыты в программировании, постепенно усложняя задания и изучая все новые и новые команды ассемблера. На первых порах вы должны научиться выводить в определенное место экрана символы и числа, причем с заранее заданными атрибутами, уметь ставить точки, проводить линии, дуги и окружности. Освоив «джентльменский» набор команд и приемов программирования, можно попытаться создать на экране нечто полезное, например, текст, заключенный в рамку или один из кадров заставки с надписями как русскими, так и латинскими буквами.
Так вот, смеем вас заверить, что ничего подобного вам не грозит. Очень скоро вы убедитесь, что большинство операций, доступных Бейсику, в ассемблере выполняется почти так же просто. Ведь, как мы уже говорили, в ПЗУ компьютера имеются необходимые подпрограммы для выполнения всех бейсиковских операторов. Поэтому во многих случаях достаточно знать лишь две вещи: первое - по какому адресу расположена та или иная подпрограмма, и второе - как этой подпрограмме передать требуемые параметры. Ну и, конечно же, нужно представлять, каким образом вообще вызываются подпрограммы в ассемблере. А выполняет это действие команда микропроцессора CALL (звать, вызывать), которую можно сравнить с известным вам оператором GO SUB. Только вместо номера строки после команды указывается адрес перехода (еще раз напомним, что адреса обозначаются числами в диапазоне от 0 до 65535).
Сначала разберемся, что требуется для печати символов.
Общаясь с Бейсиком, вы могли заметить, что оператор PRINT весьма универсален и используется для многих целей. С его помощью можно выводить символы и строки не только на основной экран, но и в служебное окно, если написать PRINT #0 или PRINT #1. В системе TR-DOS этот же оператор применяется для записи в файлы прямого и последовательного доступа, а для вывода на принтер имеется другая его разновидность - оператор LPRINT.
Для многих, вероятно, не будет новостью, что LPRINT, в сущности, это уже некоторое излишество Бейсика, так как часто удобнее бывает заменять его на PRINT #i, где i=3, который выводит информацию в поток #3 (подробно о каналах и потоках можно прочитать в [2]), то есть на принтер. Если же номер потока в операторе PRINT не конкретизирован, то по умолчанию вывод осуществляется в поток #2 - на основной экран.
В то время как в Бейсике нужный поток устанавливается автоматически, в ассемблере программист сам должен позаботиться о своевременном и правильном включении текущего потока. Для этой цели в ПЗУ имеется специальная подпрограмма, расположенная по адресу 5633 (или в шестнадцатеричном формате - #1601). Перед ее вызовом в аккумулятор следует поместить номер требуемого потока. Вы, наверное, еще не забыли, что для занесения в какой-либо регистр или регистровую пару некоторого значения используется команда LD. Таким образом, назначить поток #2 для вывода на основной экран можно всего двумя командами микропроцессора:
LD A,2 CALL 5633После этого можно что-нибудь написать на экране.
Подпрограмма, соответствующая оператору PRINT (или LPRINT) располагается по адресу 16, а перед ее вызовом в регистре A следует указать код выводимого символа. То есть, чтобы напечатать, например, букву A, загрузим в аккумулятор код 65 и вызовем подпрограмму с адресом 16:
LD A,65 CALL 16Теперь остается дописать команду RET и программка, печатающая на экране букву A, будет, в принципе, готова. Но, прежде чем привести законченный текст, хотелось бы сказать еще вот о чем. Во-первых, для определения кода нужного символа не обязательно каждый раз заглядывать в таблицу, можно предложить ассемблеру самостоятельно вычислять коды. Достаточно нужный символ заключить в кавычки. В нашем случае это будет выглядеть так:
LD A,"A"И еще один момент.
Если вы дизассемблируете даже целую сотню фирменных игрушек, то вряд ли где-то обнаружите инструкцию CALL 16, хотя добрая половина из них не отказывает себе в удовольствии попользоваться возможностями ПЗУ. Объясняется это тем, что в системе команд микропроцессора Z80 для вызова подпрограмм помимо CALL имеется еще одна инструкция, более ограниченная в применении, но зато и более эффективная. Это команда RST. Она отличается от CALL, в сущности, только одним: с ее помощью можно обратиться лишь к нескольким первым, причем строго фиксированным, адресам. В частности, к адресу 16. А основное преимущество этой команды состоит в том, что она очень компактна и занимает в памяти всего один байт вместо трех, требуемых для размещения кодов команды CALL. Поэтому вместо
CALL 16значительно выгоднее писать
RST 16Подводя итог сказанному, можно написать программку, которая будет работать подобно оператору PRINT #2;"A". Загрузите GENS и в редакторе наберите такой текст:
10 ORG 60000 20 LD A,2 30 CALL 5633 40 LD A,"A" 50 RST 16 60 RETЧтобы проверить работу этой программки, оттранслируйте ее, введя в редакторе команду A, а затем выйдите в Бейсик и, предварительно очистив экран, запустите ее с адреса 60000 оператором RANDOMIZE USR 60000.
После этого можете поэкспериментировать, подставляя в строке 40 другие значения для регистра A. Посмотрите, что получится, если указать коды псевдографических символов, UDG или ключевых слов Бейсика, которые имеют значения от 128 до 255. Правда, в этом случае придется отказаться от символьного представления кодов и нужно будет вводить непосредственные числовые величины. Тем не менее, вы убедитесь, что команда RST 16 превосходно справляется с поставленной задачей и работает точно так же, как работал бы в этом случае и оператор PRINT.
Эксперименты экспериментами, но здесь мы должны сделать небольшое предупреждение. В программировании на ассемблере имеется множество «подводных камней», поэтому не очень удивляйтесь, если после очередного опыта ваша программа «улетит» в неизвестном направлении. Дабы избежать стрессов, вызванных подобной неприятностью, всегда перед стартом программы сохраняйте измененный исходный текст, а лучше - не заходите в своих изысканиях чересчур далеко, пока не изучите книгу до конца. В свое время мы, по возможности, расскажем обо всех (ну, по крайней мере, о многих) «крутых поворотах», подстерегающих программистов-ассемблерщиков на пути создания полноценных программ.
Сделав первый, самый трудный шаг, двинемся дальше и несколько усложним нашу программку. Попробуем напечатать символ в определенном месте экрана, например, в 10-й строке и 8-м столбце, то есть попытаемся воспроизвести оператор PRINT AT 10,8;"X". Оказывается, и это в ассемблере сделать не многим труднее, чем в Бейсике.
Помимо обычных «печатных» символов (так называемых, ASCII-кодов), псевдографики, UDG и токенов (ключевых слов) Бейсика существует ряд специальных кодов, которые не выводятся, а служат для управления печатью. Часто их так и называют - управляющие символы. Они имеют коды от 0 до 31, хотя при выводе на экран используются не все, а только некоторые из них.
Директиве AT соответствует управляющий символ с кодом 22. И кроме этого кода необходимо вывести еще два, указывающих номера строки и столбца на экране. То есть, команду RST 16 нужно выполнить трижды:
LD A,22 RST 16 LD A,10 RST 16 LD A,8 RST 16После этого можно вывести и сам символ:
LD A,"X" RST 16Управляющие коды имеются и для всех прочих директив оператора PRINT: TAB, INK, PAPER, FLASH, BRIGHT, OVER, INVERSE, а также для запятой и апострофа. В табл. 4.1 приведены значения всех управляющих кодов, а также указано, какие байты требуется передать в качестве параметров. Как видите, для кодов 6, 8 и 13 дополнительных данных не требуется, коды 16...21 нуждаются еще в одном байте, а 22 и 23 ожидают ввода двух значений. Обратите внимание, что код 23 (TAB), вопреки ожиданиям, требует не одного, а двух байт, хотя на самом деле роль играет только первый из них, а второй игнорируется и может быть каким угодно (на это в таблице указывает вопросительный знак).
Код | Байты параметров | Значение |
6 | - | Запятая |
8 | - | Забой |
13 | - | Перевод строки (апостроф) |
16 | colour | Цвет INK |
17 | colour | Цвет PAPER |
18 | flag | FLASH |
19 | flag | BRIGHT |
20 | flag | INVERSE |
21 | flag | OVER |
22 | Y, X | Позиция AT |
23 | X, ? | Позиция TAB |
Допустимые значения для параметров следующие:
colour - 0...9 flag - 0 или 1 X - 0...31 Y - 0...21Теперь напишем на ассемблере пример, соответствующий оператору
PRINT AT 20,3; INK 1; PAPER 5, BRIGHT 1; "OK."Для большей наглядности снабдим нашу программку комментариями. В ассемблере комментарии записываются после символа «точка с запятой» (;), который может находиться в любом месте программы. Весь текст от этого символа до конца строки при трансляции пропускается и на окончательном машинном коде никак не сказывается. Само собой, при наборе примеров вы можете пропускать все или часть комментариев, тем более, что в книге многие из них даны на русском языке, а GENS, к сожалению, с кириллицей не знаком.
10 ORG 60000 20 LD A,2 ; вывод на основной экран (PRINT #2). 30 CALL 5633 40 ;---------------- 50 LD A,22 ; AT 20,3 60 RST 16 70 LD A,20 80 RST 16 90 LD A,3 100 RST 16 110 ;---------------- 120 LD A,16 ; INK 1 130 RST 16 140 LD A,1 150 RST 16 160 ;---------------- 170 LD A,17 ; PAPER 5 180 RST 16 190 LD A,5 200 RST 16 210 ;---------------- 220 LD A,19 ; BRIGHT 1 230 RST 16 240 LD A,1 250 RST 16 260 ;---------------- 270 LD A,"O" ; печать трех символов строки OK. 280 RST 16 290 LD A,"K" 300 RST 16 310 LD A,"." 320 RST 16 330 RETНе правда ли, получилось длинновато? Даже не верится, что этот пример после трансляции будет занимать в памяти меньше полусотни байт. А на самом деле его можно сократить еще в несколько раз. Для этого нужно воспользоваться подпрограммой ПЗУ, позволяющей выводить строки символов, да научиться формировать такие строки в программе.
Ассемблер предоставляет несколько директив для определения в программе текстовых строк и блоков данных. Вот они:
DEFB - через запятую перечисляется последовательность однобайтовых значений; DEFW - через запятую перечисляется последовательность двухбайтовых значений; DEFM - в кавычках задается строка символов; DEFS - резервируется (и заполняется нулями) область памяти длиной в указанное число байт.Эти директивы чем-то напоминают оператор Бейсика DATA, но в отличие от него не могут располагаться в произвольном месте программы. Мы уже говорили, что ассемблер, как никакой другой язык, «доверяет» программисту. Это, в частности, объясняется тем, что микропроцессор не способен сам отличить, к примеру, код буквы A от кода команды LD B,C - и то и другое обозначается десятичным числом 65. Поэтому недопустимо размещать блоки данных, скажем, внутри какой-либо процедуры, так как в этом случае они будут восприниматься микропроцессором как коды команд, и чтобы избежать конфликтов, все данные лучше размещать в самом конце программы или уж, по крайней мере, между процедурами, после команды RET.
Для преобразования вышеприведенного примера выпишем последовательность кодов, выводимых командой RST 16, следом за директивой DEFB:
DEFB 22,10,8,16,1,17,5,19,1,"O","K","."Подпрограмма вывода последовательности кодов располагается по адресу 8252 и требует передачи двух параметров: адрес блока данных перед обращением к ней нужно поместить в регистровую пару DE, а длину строки - в BC. И если вычисление второго параметра не должно вызвать трудностей, то об определении адреса стоит поговорить.
В предыдущей главе, в разделе «Структура ассемблерной строки», мы упоминали о существовании такого понятия как метка, но еще ни разу им не воспользовались - не было особой надобности. Но теперь нам без них просто не обойтись. Как уже говорилось, метки ставятся в самом начале строки, в поле, которое мы до сих пор пропускали, и служат для определения адреса первого байта команды или блока данных, записанных следом. Имена меток в GENS должны состоять не более чем из шести символов (если метка состоит более чем из 6 символов, лишние при трансляции автоматически отбрасываются, поэтому более длинные имена возможны, но исключительно ради наглядности), среди которых могут быть такие:
0...9 A...Z a...z _ [ ] \ # $ и Јно помните, что они не могут начинаться с цифры или знака #. Кроме того, метки не должны совпадать по написанию с зарезервированными словами, то есть с именами регистров и мнемониками условий. Например, недопустимо использование метки HL, однако, благодаря тому, что GENS делает различие между строчными и прописными буквами, имя hl вполне может быть меткой. Ниже перечислены все зарезервированные слова в алфавитном порядке:
Зарезервированные слова GENS $ A AF AF' B BC C D E E H HL I IX IY L M NC NZ P PE PO R SP ZИ еще один очень важный момент, связанный с метками. Их имена должны быть уникальными, то есть одно и то же имя не может появляться в поле меток дважды. Хотя, конечно, ссылок на метку может быть сколько угодно.
Можно наконец переписать наш пример с использованием блока данных, присвоив последнему имя TEXT:
ORG 60000 LD A,2 CALL 5633 LD DE,TEXT ;в регистровую пару DE записывается ; метка TEXT, соответствующая адресу ; начала блока данных. LD BC,12 ;в регистровую пару BC заносится число, ; соответствующее количеству кодов ; в блоке данных. CALL 8252 ;обращение к подпрограмме ПЗУ, ; которая печатает строку на экране. RET TEXT DEFB 22,10,8,16,1,17,5,19,1,"O","K","."Не удивляйтесь, что в этом примере отсутствуют номера строк. В дальнейшем мы везде будем приводить тексты программ именно в таком виде, во-первых, потому, что нумерация не несет никакой смысловой нагрузки, а во-вторых, многие фрагменты в ваших программах, скорее всего, будут пронумерованы совершенно иначе.
В заключение этого раздела расскажем еще об одной полезной подпрограмме ПЗУ, связанной с печатью символов. Вы знаете, что в Бейсике при использовании временных атрибутов в операторе PRINT их действие заканчивается после выполнения печати, и следующий PRINT будет выводить символы с постоянными атрибутами. В ассемблере же команда RST 16 временные установки не сбрасывает и для восстановления печати постоянными атрибутами нужно вызвать подпрограмму, расположенную по адресу 3405. Продемонстрируем это на таком примере:
ORG 60000 LD A,2 CALL 5633 LD DE,TEXT1 ;печать текста, обозначенного меткой LD BC,16 ; TEXT1, длиной в 16 байт. CALL 8252 CALL 3405 ;восстановление постоянных атрибутов. LD DE,TEXT2 ;печать текста, обозначенного меткой LD BC,11 ; TEXT2, длиной в 11 байт. CALL 8252 RET TEXT1 DEFB 22,3,12,16,7,17,2 DEFM "TEMPORARY" TEXT2 DEFB 22,5,12 DEFM "CONSTANT"После трансляции и выполнения этой программки вы увидите на экране две надписи: верхняя (TEMPORARY) выполнена с временными атрибутами (белые буквы на красном фоне), а нижняя (CONSTANT) - постоянными.
Прежде всего необходимо задать постоянные атрибуты. Сделать это можно по-разному, но проще всего рассчитать байт атрибутов и поместить его в системную переменную ATTR_P по адресу 23693. Напомним, что в байте атрибутов биты 0..2 определяют цвет «чернил» INK, биты 3..5 отвечают за цвет «бумаги» PAPER, а 6-й и 7-й биты устанавливают или сбрасывают соответственно атрибуты яркости BRIGHT и мерцания FLASH. Поэтому требуемое значение цвета можно подсчитать по формуле
INK+PAPERґ8+BRIGHTґ64+FLASHґ128Так для
INK 6: PAPER 0: BRIGHT 1: FLASH 0искомый байт будет равен
6+0ґ8+1ґ64+0ґ128=70А если вам лень считать, можете поступить проще: очистите экран и введите с клавиатуры последовательно две строки
PRINT INK 6; PAPER 0; BRIGHT 1; FLASH 0; " " PRINT ATTR (0,0)В верхнем левом углу экрана появится черный квадратик, а под ним - искомое число 70.
Теперь остается полученное число поместить в ячейку с адресом 23693, то есть выполнить инструкцию, аналогичную оператору Бейсика POKE 23693,70. Но вот беда - микропроцессор Z80 не располагает командами пересылки в память или из памяти непосредственных значений. Поэтому такую простую операцию приходится выполнять в два захода: сначала число нужно поместить в аккумулятор (и только в аккумулятор - никакой другой регистр для этого не подходит!), а затем значение из него переписать в ячейку. Команда записи в память очень напоминает загрузку регистров, только адрес или метка в этом случае заключается в круглые скобки. То есть предложение «загрузить ячейку с адресом 23693 значением из аккумулятора» записывается как LD (23693),A. Обратите внимание, что данный тип команд может выполняться только с регистром A!
Раз уж мы заговорили о способах пересылки значений между регистрами и памятью, приведем и другие инструкции, относящиеся к этой группе. Действие, обратное LD (Address),A и аналогичное функции Бейсика PEEK Address, выполняется командой LD A,(Address). Все прочие регистры могут обмениваться числовыми значениями с памятью только в парах. Выглядят такие команды следующим образом:
LD (Address),rp LD rp,(Address)где rp - одна из регистровых пар BC, DE или HL. (Забегая вперед, добавим, что в указанных командах могут участвовать также регистры IX, IY и SP.) Первая из них загружает две смежные ячейки памяти значением из регистровой пары, а вторая, наоборот, пересылает из памяти двухбайтовое число в обозначенные регистры. Заметим, что всегда предпочтительнее в данных командах применять пару HL, так как с ее участием эти инструкции занимают на байт меньше памяти и выполняются быстрее.
Таким образом, для установки постоянных атрибутов можно написать две строки вроде:
LD A,70 ;байт атрибутов LD (23693),A ;помещаем в системную переменную ATTR_PХотя таким способом можно пользоваться в большинстве случаев, он оказывается не всегда удобен. Например, если нужно установить какой-то один из атрибутов, то придется изменять не весь байт, а только некоторые его биты. А если требуется указать режимы OVER или INVERSE, либо для INK и PAPER задать значения 8 или 9, то описанный метод и вовсе непригоден.
В этих случаях можно поступить так. Первым делом необходимо установить текущий поток, связанный с основным экраном так же, как мы это делали раньше. Затем вызвать уже известную вам подпрограмму 3405 для «сброса» временных атрибутов. Следующим этапом с помощью команды RST 16 или процедуры 8252 установить новые временные атрибуты. И, наконец, временные атрибуты перевести в постоянные, для чего лучше всего вызвать соответствующую подпрограмму ПЗУ, находящуюся по адресу 7341.
Для иллюстрации этого способа напишем фрагмент, устанавливающий режимы OVER 1 и PAPER 8:
LD A,2 CALL 5633 ;определяем вывод на основной экран CALL 3405 ;«сбрасываем» временные атрибуты LD DE,ATTR1 LD BC,4 CALL 8252 ;выводим управляющие коды для новых ; временных атрибутов CALL 7341 ;переводим временные атрибуты ; в постоянные RET ATTR1 DEFB 21,1,17,8 ;последовательность управляющих кодов ; для OVER 1 и PAPER 8Стоит ли говорить, что подобное действие может выполняться в реальной игровой программе неоднократно и при этом наверняка потребуется каждый раз изменять различные атрибуты. Посему было бы очень полезно иметь универсальную процедуру, которая работала бы по-разному в зависимости от входных параметров.
Сложность здесь заключается лишь в том, как до подпрограммы 8252 «донести» содержимое регистровых пар BC и DE - ведь перед ней должны выполниться две процедуры (CALL 5633 и CALL 3405), которые обязательно изменят значения нужных регистров. Значит, до поры до времени их нужно как-то сохранить.
Решение может показаться простым и очевидным: нужно запомнить значения регистров где-то в памяти и тем самым освободить их для каких-либо нужд, а затем восстановить их первоначальный вид, прочитав из памяти записанные ранее числа. Да, действительно, иногда так и делают. Так же поступает и большинство компиляторов, но как вы знаете, они не отличаются сообразительностью и используют ресурсы компьютера не самым оптимальным образом. Ведь известно, что команды пересылок между регистрами и памятью выполняются заметно дольше, чем обмен данными непосредственно между регистрами. Кроме того, дополнительные временные переменные лишь попусту транжирят память. И ведь еще необходимо помнить, где что лежит!
Применение такой методики чревато и еще одной неприятностью. Когда вы начнете писать на ассемблере достаточно большие программы (а мы надеемся, что это время не за горами), то очень скоро обнаружите, что шести символов для меток маловато. В результате этого очень легко можно ошибиться и поставить метку с уже существующим именем. Поэтому старайтесь везде, где только можно, обходиться без лишних меток.
Всего перечисленного можно избежать, если пойти другим путем, используя гораздо более удобное и эффективное средство - машинный стек. Во второй главе мы уже объясняли, что это такое, но не рассказывали, как с ним работать. Вообще-то, к помощи стека мы уже прибегали много раз, даже не подозревая об этом. Дело в том, что все команды вызова подпрограмм, будь то CALL или RST, прежде всего заносят в стек адрес возврата, то есть адрес следующей за вызовом команды. После выполнения подпрограммы завершающая команда RET снимает со стека этот адрес и тем самым возвращает управление основной программе.
Кроме такого косвенного взаимодействия со стеком имеется возможность непосредственного обмена с ним числовыми значениями из регистровых пар. Для этих целей служат две команды: PUSH (втолкнуть), которая помещает в стек значение из регистровой пары и POP (вытолкнуть, выскочить), забирающая с вершины стека двухбайтовое число в регистровую пару. Например, предложение «Запомнить в стеке значение регистровой пары BC» запишется так:
PUSH BCа команда «Взять в регистровую пару HL значение с вершины стека» будет выглядеть следующим образом:
POP HLКак мы уже говорили, существует определенный порядок работы со стеком, и числа, занесенные на его вершину последними, должны быть сняты в первую очередь. При этом нужно очень внимательно следить не только за очередностью обмена со стеком, но и за тем, чтобы его состояние при выходе из подпрограммы было таким же, как и при входе. Иными словами, необходимо, чтобы количество команд PUSH и POP в каждой подпрограмме было одинаковым (хотя, заметим, что вовсе не обязательно забирать числа со стека в те же регистровые пары, из которых производилась запись). Несоблюдение этих правил может привести к совершенно непредсказуемым результатам. Кстати, стековые ошибки относятся к наиболее распространенным, поэтому, если при отладке программы вы обнаружите, что в какой-то момент компьютер «зависает», «сбрасывается» или ведет себя как-то странно, то первым делом следует проверить те строки, где встречаются команды обмена со стеком.
Все только что сказанное верно, однако, изучая фирменные игрушки, можно заметить, что опытные программисты порой обращаются со стеком весьма своевольно, совершенно не придерживаясь общепринятых правил. В будущем мы поговорим о некоторых оригинальных приемах программирования, позволяющих иногда заметно сократить машинный код, а главное, увеличить быстродействие программы. Но пока вы не прочувствуете хорошенько идею стека, слишком увлекаться экспериментами в этой области мы вам все же не советуем.
Теперь, зная кое-что о машинном стеке, можно переписать предыдущий пример, оформив его в виде самостоятельной процедуры:
ATTRIB PUSH BC ;сохраняем в стеке значения регистровых PUSH DE ; пар BC и DE LD A,2 CALL 5633 CALL 3405 ; Снимаем с вершины стека сохраненные ранее значения в обратном порядке: POP DE ;сначала в DE, POP BC ; а затем в BC CALL 8252 CALL 7341 RETДля вызова этой процедуры необходимо задать строку DEFB с перечислением управляющих кодов, занести адрес этой строки в DE и в регистровой паре BC указать ее длину:
LD DE,ATTR2 LD BC,6 CALL ATTRIB RET ATTR2 DEFB 16,5,17,1,19,1Если установка постоянных атрибутов иногда может оказаться довольно сложным процессом, то остальные настройки экрана не должны вызвать никаких трудностей. Окрасить бордюр в любой нужный цвет можно двумя способами. Первый из них почти в точности повторяет оператор Бейсика
OUT 254,colorПри записи в порт, так же, как и при записи в память, в ассемблере нельзя использовать непосредственные значения, поэтому код цвета прежде необходимо поместить в регистр A. Адрес порта обязательно нужно заключать в круглые скобки. Например, для установки красного бордюра можно воспользоваться такими командами:
LD A,2 OUT (254),AКак вы знаете из Бейсика, установленный подобным образом цвет бордюра обычно надолго не задерживается. Для более долговечного его изменения используется оператор BORDER, а выполняет эту процедуру подпрограмма ПЗУ по адресу 8859. Перед обращением к ней в аккумуляторе должен содержаться код цвета. Скажем, для установки голубого бордюра следует написать:
LD A,5 CALL 8859Для полноты информации напомним также, что байт атрибутов для бордюра обычно сохраняется в области системных переменных по адресу 23624.
Что же касается очистки экрана, то это самая простая операция. Соответствующая подпрограмма находится по адресу 3435 и не требует никаких входных параметров. Единственное, что нужно помнить - после выполнения команды CALL 3435 текущим устанавливается поток, связанный с выводом в служебное окно экрана. Таким образом, после вызова этой процедуры необходимо вновь назначить требуемый поток. Например:
CALL 3435 LD A,2 CALL 5633В завершение этого раздела предлагаем вам законченную процедуру, окрашивающую экран и бордюр в черный цвет, устанавливающую голубой цвет для выводимых символов и тем самым подготавливающую основной экран к приему текстовой и графической информации:
SETSCR LD A,5 LD (23693),A LD A,0 CALL 8859 CALL 3435 LD A,2 CALL 5633 RETМы присвоили этой процедуре собственное имя (метку) SETSCR не случайно. В последующих разделах и главах эта подпрограмма будет использоваться неоднократно, и чтобы не переписывать ее каждый раз заново, мы будем просто ссылаться на нее, вставляя в текст команду CALL SETSCR. Вам же мы посоветуем записать ее (вместе с процедурой ATTRIB) на ленту или дискету в виде отдельного файла, тогда в будущем вам достаточно будет только подгрузить ее к основному тексту с помощью команды редактора G. Точно так же рекомендуем вам поступить и с другими подпрограммами, которые встретятся в книге, и в конце концов в вашем распоряжении появится библиотека наиболее важных и часто используемых в игровых программах процедур.
Используя калькулятор, можно выполнять множество математических расчетов с любыми целыми и дробными числами, доступными Бейсику, но пока мы не будем объяснять, как это делается, поскольку тема эта носит самостоятельный характер, а также требует определенной предварительной подготовки. (Работе с калькулятором будет посвящен отдельный раздел девятой главы.) Сейчас же мы сосредоточимся на выводе только целых чисел из диапазона 0...65535, для чего используем особую рабочую область памяти, именуемую стеком калькулятора. Мы уже говорили во второй главе, что эта область не имеет определенного строго фиксированного адреса, да в большинстве случаев нам и не обязательно знать это, поскольку операционная система сама следит за ее местоположением и размером. Но при желании вы можете выяснить и то и другое. Адрес дна стека калькулятора можно прочитать из системной переменной STKBOT (23651/23652), а адрес его вершины определяется другой переменной - STKEND (23653/23654).
Для печати чисел можно вызвать подпрограмму, находящуюся по адресу 11747. Она напечатает число, расположенное в данный момент на вершине стека калькулятора, значит, требуемое число прежде нужно туда поместить. В этом вам поможет другая подпрограмма - 11563, размещающая на стеке калькулятора значение из регистровой пары BC. Следовательно, программа, которая выводит на экран, например, число 12345, может иметь следующий вид:
ORG 60000 ENT $ CALL SETSCR ;установка экрана LD BC,12345 ;требуемое число 12345 загружаем ; в регистровую пару BC CALL 11563 ;подпрограмма ПЗУ заносит в стек ; калькулятора число из BC CALL 11747 ;подпрограмма ПЗУ печатает число, ; находящееся на вершине ; стека калькулятора RET SETSCR ......... ;здесь должна располагаться подпрограмма, ; описанная в предыдущем разделеВ примере мы использовали еще одну полезную директиву ассемблера - ENT. Она дает возможность запускать оттранслированную программу непосредственно из редактора GENS, без выхода в Бейсик. Это бывает особенно удобно при отладке небольших фрагментов, в которых требуется подобрать тот или иной параметр методом «научного тыка», не прибегая к расчетам.
Знак доллара ($) при трансляции заменяется адресом начала строки, в которой он встретился, и в нашем случае он примет значение 60000. После трансляции программы, кроме обычной информации, вы увидите на экране строку
Execute 60000говорящую о том, что программа может быть выполнена с адреса 60000. Теперь для запуска полученного машинного кода достаточно ввести команду редактора R, завершив ввод, как всегда, нажатием клавиши Enter.
Описанный способ позволяет выводить любые числа от 0 до 65535. Для печати же больших значений требуется совершенно иной подход, и здесь нет столь простых решений, поэтому мы оставим этот вопрос «на потом». А сейчас представим еще одну подпрограмму из ПЗУ компьютера, предназначенную для вывода целых чисел.
В тех случаях, когда можно обойтись гораздо меньшими числовыми величинами, не превышающими 9999, лучше пользоваться подпрограммой, находящейся по адресу 6683. Она печатает число, находящееся в регистровой паре BC без привлечения стека калькулятора, что в некоторой степени облегчает обращение к ней. В качестве примера приведем такой фрагмент:
ORG 60000 ENT $ CALL SETSCR LD BC,867 CALL 6683 ;печать 4-значного десятичного числа ; из регистровой пары BC RET SETSCR .........Для любознательных можем добавить относительно подпрограммы 6683, что она используется операционной системой компьютера для вывода номеров строк бейсик-программ. Этим, в общем-то и объясняется то, что она может работать только с четырехзначными числами. Если же попытаться напечатать с ее помощью число, превышающее 9999, то ничего страшного не произойдет, просто вы получите довольно бессмысленную последовательность символов, имеющую мало общего с заданным значением.
При описании обоих методов печати чисел мы пользовались постоянными атрибутами, но это ни в коем случае не означает, что здесь недопустима установка временных цветов. Все сказанное в разделе «Вывод буквенных и цифровых символов» в полной мере относится и к числам. Вы можете устанавливать любые допустимые режимы печати, указывать координаты экрана, использовать табуляцию и так далее. Словом, с числовыми значениями вы можете обращаться в точности так, как и с отдельными символами. И чтобы подтвердить это, приведем пример программы, печатающей число 3692 в 5-й строке и 14-м столбце экрана с применением инверсии:
ORG 60000 ENT $ CALL SETSCR LD DE,DATA1 LD BC,5 CALL 8252 LD BC,3692 CALL 6683 RET DATA1 DEFB 22,5,14 ;управляющие коды для AT 5,14 DEFB 20,1 ;управляющие коды для INVERSE 1 SETSCR .........Если в данном примере заменить команду CALL 6683 на CALL 11563 и CALL 11747, результат окажется тем же самым.
LD C,45 LD B,110 CALL 8933Так как регистры C и B относятся к одной паре, то часто можно сокращать запись, загружая их не последовательно, а одновременно. При этом удобнее воспользоваться шестнадцатеричной системой счисления:
LD BC,#6E2D ;45 = #2D, 110 = #6E CALL 8933Чтобы нарисовать точку с определенными атрибутами так же, как и при выводе символов, можно воспользоваться подпрограммой ATTRIB, описанной в разделе «Подготовка экрана к работе из ассемблера», или установить временные цвета, изменив системную переменную ATTR_T (23695). Сразу заметим, что это в равной степени относится и к подпрограммам рисования линий и окружностей.
Для примера поставим точку красного цвета на желтом фоне. (Задавая атрибуты при выводе графики, помните, что в Speccy цвета определяются для целого знакоместа.) Рассчитав значения байта атрибутов для заданной комбинации цветов, получим число 50 (2+6ґ8). Перед выводом точки занесем это число в ячейку 23695, предварительно подготовив экран подпрограммой SETSCR из раздела «Подготовка экрана к работе из ассемблера»:
ORG 60000 ENT $ CALL SETSCR LD A,50 LD (23695),A LD BC,#6E2D CALL 8933 RET SETSCR .........
PLOT 120,80: DRAW 35,-60Оператор DRAW реализует подпрограмма ПЗУ, находящаяся по адресу 9402. Перед обращением к ней в регистры C и B необходимо последовательно занести значения параметров, взятые по абсолютной величине, то есть в C в нашем примере помещается число 35, а в B нужно загрузить не -60, а 60. Но чтобы не потерять знаки, их следует разместить на регистрах E и D. Это значит, что в регистр E заносится единица, а в D - минус единица (или, что то же самое, 255). Таким образом, приведенная выше строка Бейсика на ассемблере запишется так:
LD BC,#5078 ;C = 120 (#78), B = 80 (#50) CALL 8933 LD BC,#3C23 ;C = 35 (#23), B = 60 (#3C) LD DE,#FF01 ;E = 1 (#01), D = -1 = 255 (#FF) CALL 9402 RETВроде бы все просто, однако здесь вы можете столкнуться с одной серьезной проблемой. Если запустить эту программку не из ассемблера (использовав директиву ENT и команду редактора R), а из Бейсика с помощью функции USR, то вы заметите, что компьютер ведет себя довольно странно. В лучшем случае появится какое-нибудь сообщение об ошибке, а в худшем - компьютер «зависнет» или «сбросится». А происходит это оттого, что при выполнении подпрограммы 9402 теряется некоторая информация, необходимая для нормального завершения функции USR. Значит, нам нужно выяснить, что это за информация и где она находится, чтобы можно было сохранить ее на входе и восстановить на выходе из нашей программы.
До сих пор мы говорили только о семи регистрах общего назначения, но на самом деле микропроцессор Z80 имеет их гораздо больше. Постепенно мы изучим их все, а сейчас скажем еще несколько слов об уже известных вам регистрах. Дело в том, что имеется не один, а два набора регистров данных, но активным в каждый момент времени может быть только какой-то один из них, то есть одновременно вы все равно можете работать только с семью регистрами общего назначения.
Регистры второго, или, как говорят, альтернативного набора абсолютно ничем не отличаются от регистров первого набора. Они имеют те же имена (для отличия альтернативных регистров от активных в данный момент времени после имени пары, включающей в себя этот регистр, ставят символ апострофа, например, DE') и выполняют те же функции, поэтому нет никакой возможности определить, какой из двух наборов активен в данный момент - об этом должен позаботиться программист.
В любой момент вы можете переключиться на альтернативные регистры, использовав команду EXX. Выполнив эту же команду повторно, вы вернете прежние значения регистров, ничего не потеряв. Потому данная команда, как правило, в программах встречается парами, подобно PUSH и POP.
Применяя команду EXX, нужно также помнить, что она переключает на альтернативный набор не все семь регистров, а только 6: BC, DE и HL. Для переключения аккумулятора существует другая команда. Не вдаваясь пока в смысл символики, скажем, что записывается она так:
EX AF,AF'Добавим еще к сказанному, что мнемоника EX и EXX происходит от английского слова exchange - обменивать.
Вернемся снова к функции USR. В простых и небольших по объему программах семи регистров общего назначения, как правило, вполне хватает (во всяком случае, мы вам советуем не слишком злоупотреблять командой EXX, лучше при необходимости для временного хранения информации пользоваться стеком). Но в таких больших и сложных программах, как операционная система ZX Spectrum, иногда возникает необходимость привлекать и альтернативный набор регистров. Так функция USR перед вызовом программы в машинных кодах заносит важную информацию в регистровую пару HL', поэтому для нормального выхода в Бейсик ее необходимо сохранять всегда, когда она может измениться. В частности, при использовании подпрограммы рисования линий 9402.
Перепишем предыдущий пример таким образом, чтобы его можно было вызвать из Бейсика:
ORG 60000 EXX ;в начале программы меняем ; на альтернативный набор PUSH HL ;сохраняем регистровую пару HL LD BC,#5078 CALL 8933 LD BC,#3C23 LD DE,#FF01 CALL 9402 POP HL ;восстанавливаем значение HL EXX ;делаем его альтернативным RETПоскольку перед вызовом подпрограммы в машинных кодах функция USR загружает регистр HL' всегда одним и тем же значением, а именно, числом 10072, то можно не сохранять его в стеке, а просто записать перед выходом в Бейсик:
LD HL,10072 EXX RETПри желании вы можете проверить содержимое пары HL', оттранслировав такую программку:
ORG 60000 EXX ;меняем на альтернативный набор PUSH HL ;запоминаем в стеке значение HL' EXX ;возвращаем «стандартные» регистры POP BC ;забираем число из стека в пару BC ; для передачи в Бейсик RET ;возврат в Бейсики затем выполнив ее строкой
PRINT USR 60000в результате чего на экране должно появиться число 10072.
Как пример, приведем программку, соответствующую строке Бейсика
PLOT 100,80: DRAW 30,50,3Для занесения чисел в стек калькулятора можно, конечно, воспользоваться уже известной подпрограммой 11563, но в данном случае нам не требуются числа, превышающие байтную величину (255), поэтому программа получится короче, если в стеке калькулятора размещать значения из аккумулятора, применив процедуру ПЗУ 11560, исходным данным для которой и является содержимое регистра A. Порядок действий будет совершенно таким же, как и при использовании подпрограммы 11563. Например, для занесения в стек калькулятора числа 123 можно написать такую последовательность инструкций:
LD A,123 CALL 11560Зная это, можно написать такой фрагмент на ассемблере, соответствующий приведенной выше строке Бейсика:
ORG 60000 ENT $ LD BC,#5064 ;C = 100 (#64), B = 80 (#50) CALL 8933 ;PLOT 100,80 LD A,30 ;заносим в стек калькулятора CALL 11560 ; первый параметр LD A,50 ;второй параметр CALL 11560 LD A,3 ;третий параметр CALL 11560 CALL 9108 ;DRAW 30,50,3 LD HL,10072 ;восстанавливаем значение пары HL' EXX ; для нормального выхода в Бейсик RET
Для примера напишем программку, выполняющую то же самое, что и оператор
CIRCLE 120,80,60Внешне она очень напоминает программку, рисующую на экране фрагменты дуг:
ORG 60000 ENT $ CALL SETSCR ;Установка атрибутов экрана LD A,120 ;Заносим в стек калькулятора CALL 11560 ; X-координату центра окружности LD A,80 ;Y-координата центра окружности CALL 11560 LD A,60 ;Радиус CALL 11560 CALL 9005 ;CIRCLE 120,80,60 LD HL,10072 EXX RET SETSCR .........Процедура 9005 также «портит» регистровую пару HL', то есть при необходимости возврата в Бейсик ее обязательно нужно восстановить.
Еще раз повторим, что при желании вы можете рисовать все графические примитивы, описанные в этом разделе, заданным цветом, для чего нужно воспользоваться одним из способов, перечисленных в разделе «Подготовка экрана к работе из ассемблера».
Во время игры на экран обычно выводится масса различной информации. Играющему нет никакой необходимости задумываться над тем, к какому типу эта информация принадлежит, анализировать и классифицировать происходящее на экране. Достаточно правильно нажимать клавиши в соответствии с указаниями, данными в программе. Но совсем другое отношение ко всему происходящему должно быть у человека, планирующего свою собственную игру.
Все кадры, появляющиеся на экране во время игры, можно условно разделить на два типа: статические и динамические. К первым можно отнести такие, в которых изображение со временем не меняется. Это могут быть, например, отдельные кадры многокадровой заставки, такие как Правила Игры или Таблица Результатов. Сюда же можно отнести различные информационные панели: схемы лабиринтов, карты места боевых действий и т. п.
Динамическими заставками (или кадрами) будем называть такие, в которых с течением времени что-то изменяется на экране. В простейшем случае это может быть изменение цвета рамки или надписей, подвижный курсор, возможность ввода с клавиатуры. В более сложных динамических кадрах появятся элементы мультипликации, перемещения спрайтов и пейзажей.
О создании динамических кадров мы поговорим позже, а сейчас разберемся со способами изготовления статических заставок.
Как и при программировании на Бейсике, мы рекомендуем вам прежде всего нарисовать на листе клетчатой бумаги прямоугольник размером 32 клетки по горизонтали и 24 - по вертикали (то есть по размеру экрана в знакоместах) и внутри изобразить все то, что вы хотите увидеть на экране. А затем составить блоки данных DEFB так, чтобы их осталось только распечатать командой CALL 8252.
Рис. 4.1. Пример статической заставки
Предположим, что первая заставка выглядит, как показано на рис. 4.1. Как видно, на экране должны появляться не только тексты, но и достаточно сложные графические изображения, составляющие рамку. В Бейсике для получения такого рисунка мы воспользовались бы определяемыми пользователем символами UDG, закодировав в них отдельные элементы целого изображения. Но и в ассемблере можно поступить так же. На рис. 4.2 показаны четыре элемента, из которых состоят углы рамки, а справа от каждого квадратика расставлены десятичные значения восьми байтов изображенного символа. Таким же образом следует нарисовать и все остальные элементы, а затем рассчитать коды каждого байта в каждом из них.
Рис. 4.2. Элементы углов рамки
Для упрощения кодирования мы можем предложить простую сервисную программку на Бейсике, перед использованием которой следует создать полный набор символов, прибегнув к помощи одного из редакторов фонтов (например, из Art Studio). Но не обязательно все символы будут взяты из полученного шрифта, например, в нашей программе заставки их использовано только 16. Вот текст кодировщика символов (в дальнейшем везде, где в программах потребуется ввести более одного пробела подряд, для наглядности будем обозначать их символом «·».):
10 BORDER 0: PAPER 0: INK 5: CLEAR 64255 30 INPUT "Font file name:"; LINE a$ 40 LOAD a$CODE 64256 50 INPUT "start symbol:"; LINE a$ 60 CLS : PRINT AT 15,0; INK 6;"'E' - to exit" 70 LET a$=a$(1) 80 LET start=64256+((CODE a$)-32)*8 90 LET s=CODE a$: LET c=0 100 GO TO 110 110 FOR m=start TO 64256+768 120 LET a=PEEK m: LET byte=a 130 LET c=c+1 140 DIM b(8) 150 FOR n=1 TO 8 160 LET a=a/2: LET b(n)=a<>INT a: LET a=INT a 170 NEXT n 180 PRINT AT 2,1; INK 2;"Symbol: "; INK 7; PAPER 1;CHR$ s; PAPER 0; INK 2;" Code: "; INK 7;s 190 FOR n=1 TO 8 200 IF b(n)=0 THEN PAPER 1 210 IF b(n)=1 THEN PAPER 6 220 PRINT AT c+3,9-n;" " 230 NEXT n 240 PAPER 0: PRINT AT c+3,10;byte;"··" 250 IF c>7 THEN LET c=0: LET s=s+1: GO TO 270 260 NEXT m 270 REM ----Если нажата клавиша E---- 280 LET k$=INKEY$ 290 IF k$="E" OR k$="e" THEN BEEP .01,0: GO TO 50 300 IF k$<>"" THEN BEEP .01,20: GO TO 260 310 GO TO 280Использование этой программы не вызовет никаких затруднений - достаточно отвечать на вопросы, появляющиеся на экране. Добавим только, что при работе с дисководом следует включить в программу строку
20 RANDOMIZE USR 15619: REM : CATа строку 40 заменить на такую:
40 RANDOMIZE USR 15619: REM : LOAD a$CODE 64256Мы пропустим утомительный этап кодирования символов, в результате чего получится довольно длинный ряд цифр, которые запишем после инструкции DEFB и присвоим первому байту этого блока данных метку UDG:
UDG DEFB 0,63,64,95,95,95,95,95 ;A (144) DEFB 0,252,30,250,250,250,242,242 ;B (145) DEFB 95,95,127,127,124,96,63,0 ;C (146) DEFB 226,194,130,2,2,2,252,0 ;D (147) DEFB 0,63,0,95,107,95,107,95 ;E (148) DEFB 0,244,0,208,234,208,234,208 ;F (149) DEFB 107,95,107,95,107,0,63,0 ;G (150) DEFB 234,208,234,208,234,0,244,0 ;H (151) DEFB 107,95,107,95,107,95,107,95 ;I (152) DEFB 234,208,234,208,234,208,234,208 ;J (153) DEFB 0,31,85,74,95,74,95,95 ;K (154) DEFB 0,255,85,170,255,170,255,255 ;L (155) DEFB 95,95,85,74,21,64,21,0 ;M (156) DEFB 255,255,85,170,85,0,85,0 ;N (157) DEFB 0,248,82,170,250,170,250,250 ;O (158) DEFB 250,250,82,170,80,2,80,0 ;P (159)В комментариях справа от каждой строки указаны клавиши, соответствующие символам, вводимым в режиме курсора [G], и их десятичные коды. Буквы выписаны, скорее, как воспоминание о Бейсике, а вот коды нам понадобятся при составлении следующего блока, так как в редакторе GENS нет возможности вводить символы UDG непосредственно с клавиатуры.
Для получения блока данных, описывающего внешний вид рамки, за неимением какого-либо специализированного редактора лучше всего «включить» полученный набор UDG и составить программу сначала на Бейсике. При таком способе не нужно будет после каждого введенного символа транслировать программу заново и проверять правильность ввода, достаточно дать команду RUN. Кроме того, в Бейсике вы застрахованы от возникновения таких критических ошибок, после которых всю работу нужно начинать сначала. В конце останется только переписать полученный блок уже в редакторе GENS, не опасаясь, что где-то закралась ошибка.
«Включить» новые символы UDG в ассемблере до смешного просто. В Бейсике для этого требуется прежде всего определить адрес размещения символов в памяти, затем в цикле последовательно считывать коды из блока данных и переносить их по рассчитанному адресу. В ассемблере же ничего никуда перемещать не требуется, достаточно изменить адрес области UDG в системных переменных. А выполняется это всего двумя командами микропроцессора:
LD HL,UDG LD (23675),HLМы избавим вас от неблагодарной работы по составлению блока данных для печати рамки и приведем его в уже готовом виде:
RAMKA DEFB 22,4,0,16,5 DEFB 144,145,154,155,155,155,155,155 DEFB 155,155,158,154,155,155,155,155 DEFB 155,155,155,155,158,154,155,155 DEFB 155,155,155,155,155,158,144,145 DEFB 146,147,16,4,156,157,157,157,157,157 DEFB 157,157,159,156,157,157,157,157 DEFB 157,157,157,157,159,156,157,157 DEFB 157,157,157,157,157,159,16,5,146,147 DEFB 16,5,148,16,4,149,22,6,30,16,5,148,16,4,149 DEFB 16,5,152,16,4,153,22,7,30,16,5,152,16,4,153 DEFB 16,5,152,16,4,153,22,8,30,16,5,152,16,4,153 DEFB 16,5,150,16,4,151,22,9,30,16,5,150,16,4,151 DEFB 16,5,148,16,4,149,22,10,30,16,5,148,16,4,149 DEFB 16,5,152,16,4,153,22,11,30,16,5,152,16,4,153 DEFB 16,5,152,16,4,153,22,12,30,16,5,152,16,4,153 DEFB 16,5,150,16,4,151,22,13,30,16,5,150,16,4,151 DEFB 16,5 DEFB 144,145,154,155,155,155,155,155 DEFB 155,155,158,154,155,155,155,155 DEFB 155,155,155,155,158,154,155,155 DEFB 155,155,155,155,155,158,144,145 DEFB 146,147,16,4,156,157,157,157,157,157 DEFB 157,157,159,156,157,157,157,157 DEFB 157,157,157,157,159,156,157,157 DEFB 157,157,157,157,157,159,16,5,146,147Вы можете заметить, что кроме кодов символов UDG в блок данных включены также и управляющие коды, изменяющие цвет и позицию печати.
Значительно проще составить блок данных для печати текста заставки. Он будет выглядеть примерно так:
TEXT DEFB 22,2,8,16,6 DEFM "*" DEFB 22,2,10,16,3 DEFM "F I G H T E R" DEFB 22,2,24,16,6 DEFM "*" DEFB 22,7,10,16,7 DEFM "Written by :" DEFB 22,9,7 DEFM "Kapultsevich···Igor" DEFB 22,12,5 DEFM "Saint-Petersburg··1994" DEFB 22,17,3,16,6 DEFM "Press any··key to continue"Покончив с самой утомительной частью работы, нам остается вывести на экран подготовленные блоки данных. Вы уже знаете, как это можно сделать, но тем не менее позвольте дать небольшой совет. Все было очень просто, пока мы не сталкивались с блоками данных внушительных размеров. До сих пор мы ограничивались выводом десятка-другого символов. Но попробуйте-ка подсчитать, сколько байт занимает, например, блок под названием RAMKA. Думается, вас не очень вдохновит такая работа. А что если в текст заставки захочется внести какие-то изменения или дополнения? Снова пересчитывать его длину?
Ни в коем случае! Ведь ассемблер сам может выполнять некоторые несложные расчеты, оперируя как с числами, так и с метками. Расположим блоки данных в таком порядке: сначала блок TEXT, затем - RAMKA и самым последним запишем блок UDG. Таким образом, длина блока TEXT будет равна разности его конечного и начального адресов, а так как сразу за ним следует блок RAMKA, то его длина определяется выражением RAMKA-TEXT. (Обратите внимание на тот факт, что ассемблер может вычислять выражения с метками даже до того, как эти метки встретятся в программе. Именно для обеспечения таких «ссылок вперед» и необходим второй проход ассемблирования - Примеч. ред.) Аналогично длина блока RAMKA вычисляется выражением UDG-RAMKA.
Теперь можно написать программку, формирующую на экране статическую заставку:
ORG 60000 ENT $ ; Подготовка экрана LD A,5 LD (23693),A LD A,0 CALL 8859 CALL 3435 LD A,2 CALL 5633 ; «Включение» символов UDG LD HL,UDG LD (23675),HL ; Вывод заставки на экран LD DE,RAMKA LD BC,UDG-RAMKA CALL 8252 LD DE,TEXT LD BC,RAMKA-TEXT CALL 8252 RET ; Подготовленные заранее блоки данных ; -------------- TEXT ......... ; -------------- RAMKA ......... ; -------------- UDG .........
Многие из вас наверняка знакомы с различными видами русификации ZX Spectrum - кто-то из книг, например, из [1] или [2], а кому-то повезло раздобыть готовые знакогенераторы. Тем не менее, в этом разделе мы хотим напомнить способы создания новых фонтов и привести два полных символьных набора: латинский и русский.
В некоторых программах для русификации используется область графических символов, определяемых пользователем (UDG). Но это, в основном, касается программ на Бейсике, а в ассемблере, как вы могли заметить из примера, приведенного в предыдущем разделе, прибегать к помощи символов UDG не слишком удобно. Кроме всего прочего в этом случае возможно получить не более 22 новых символов, что для полного набора букв явно маловато. Поэтому в ассемблерных программах область определяемых символов практически никогда не используется (тем не менее, некоторое время мы еще будем к ним прибегать, пока не научимся выводить символы и графику без использования команды RST 16).
Полный набор символов можно изготовить, воспользовавшись специальным редактором шрифта (Font Editor). Эту программу, как вы знаете, можно найти в качестве составной части в таких графических редакторах как Art Studio или The Artist II. Как работать с этими программами не раз объяснялось в литературе (см. [1] или [2]) и, наверное, нет нужды еще раз распространяться на сей счет. Вместо этого мы сразу приведем программки, содержащие коды символьных наборов, созданных именно таким образом. Набрав и выполнив их, вы получите в свое распоряжение готовые кодовые блоки, которые затем можете сразу использовать в своих программах или предварительно подкорректировать в упомянутых редакторах.
Рис. 4.3. Полный набор латинских символов
Первая программа создает полный набор, состоящий из латинских букв, цифр, знаков препинания и специальных символов (рис. 4.3). Наберитесь терпения и введите ее строки в компьютер. Львиную долю программы составляют блоки данных, содержащие коды всех символов. Если вы нечаянно ошибетесь при вводе хотя бы одного числа, программа в конце работы сообщит об этом и текстом и звуком. А чтобы ошибку легче было обнаружить, все символы, один за другим, выводятся на экран по мере выполнения программы. Если текст программы введен без ошибок, то через некоторое время на экране появится стандартное сообщение Start tape then press any key. Включите магнитофон на запись и нажмите любую клавишу. Коды нового фонта запишутся на ленту. Если вы работаете с дисководом, слегка измените строку 90:
90 RANDOMIZE USR 15619: REM: SAVE "latfont"CODE 64000,768В этом случае нужно вставить дискету в карман дисковода еще до запуска программы, а полученные коды запишутся сразу по окончании обработки блока данных.
10 PAPER 5: BORDER 5: CLEAR 63999: LET s=0: LET ad=64000 20 PRINT INK 1;"Please wait"''': PLOT 0,160: DRAW INK 6;255,0: PLOT 0,116: DRAW INK 6;255,0 25 RANDOMIZE ad-256: POKE 23606,PEEK 23670: POKE 23607,PEEK 23671 30 FOR i=CODE " " TO CODE "<189>": FOR j=1 TO 8 40 READ a: LET s=s+a: POKE ad,a: LET ad=ad+1 50 NEXT j 60 PRINT CHR$ i;: NEXT i 70 POKE 23606,0: POKE 23607,60 80 IF s<>37874 THEN PRINT AT 10,8; INK 2; FLASH 1; "Error in DATA!!!": BEEP .5,-20: STOP 90 SAVE "latfont"CODE 64000,768 1000 DATA 0,0,0,0,0,0,0,0: REM Space 1010 DATA 0,48,48,48,48,0,48,0: REM ! 1020 DATA 0,108,108,0,0,0,0,0: REM " 1030 DATA 0,54,127,54,54,127,54,0: REM # 1040 DATA 0,8,62,104,60,22,124,16: REM $ 1050 DATA 0,99,102,12,24,51,99,0: REM % 1060 DATA 0,24,44,24,58,108,58,0: REM & 1070 DATA 48,48,96,0,0,0,0,0: REM ' 1080 DATA 0,12,24,24,24,24,12,0: REM ( 1090 DATA 0,48,24,24,24,24,48,0: REM ) 1100 DATA 0,0,54,28,127,28,54,0: REM * 1110 DATA 0,0,24,24,126,24,24,0: REM + 1120 DATA 0,0,0,0,0,48,48,96: REM , 1130 DATA 0,0,0,0,126,0,0,0: REM - 1140 DATA 0,0,0,0,0,48,48,0: REM . 1150 DATA 0,3,6,12,24,48,96,0: REM / 1160 DATA 0,60,102,110,118,102,60,0: REM 0 1170 DATA 0,24,56,24,24,24,60,0: REM 1 1180 DATA 0,60,102,6,60,96,126,0: REM 2 1190 DATA 0,60,102,12,6,102,60,0: REM 3 1200 DATA 0,12,28,44,76,126,12,0: REM 4 1210 DATA 0,124,96,124,6,70,60,0: REM 5 1220 DATA 0,60,96,124,102,102,60,0: REM 6 1230 DATA 0,126,6,12,24,48,48,0: REM 7 1240 DATA 0,60,102,60,102,102,60,0: REM 8 1250 DATA 0,60,102,102,62,6,60,0: REM 9 1260 DATA 0,0,48,48,0,48,48,0: REM : 1270 DATA 0,0,48,48,0,48,48,96: REM ; 1280 DATA 0,0,12,24,48,24,12,0: REM < 1290 DATA 0,0,0,126,0,126,0,0: REM = 1300 DATA 0,0,48,24,12,24,48,0: REM > 1310 DATA 0,56,108,12,24,0,24,0: REM ? 1320 DATA 0,60,110,110,110,96,62,0: REM @ 1330 DATA 0,60,102,102,126,102,102,0: REM A 1340 DATA 0,124,102,124,102,102,124,0: REM B 1350 DATA 0,60,102,96,96,102,60,0: REM C 1360 DATA 0,124,102,102,102,102,124,0: REM D 1370 DATA 0,126,96,124,96,96,126,0: REM E 1380 DATA 0,126,96,124,96,96,96,0: REM F 1390 DATA 0,60,102,96,110,102,60,0: REM G 1400 DATA 0,102,102,126,102,102,102,0: REM H 1410 DATA 0,60,24,24,24,24,60,0: REM I 1420 DATA 0,28,12,12,12,76,56,0: REM J 1430 DATA 0,100,104,120,104,100,102,0: REM K 1440 DATA 0,96,96,96,96,98,126,0: REM L 1450 DATA 0,99,119,107,107,99,99,0: REM M 1460 DATA 0,102,102,118,110,102,102,0: REM N 1470 DATA 0,60,102,102,102,102,60,0: REM O 1480 DATA 0,124,102,102,124,96,96,0: REM P 1490 DATA 0,60,102,102,102,124,58,0: REM Q 1500 DATA 0,124,102,102,124,108,102,0: REM R 1510 DATA 0,60,96,60,6,102,60,0: REM S 1520 DATA 0,126,24,24,24,24,24,0: REM T 1530 DATA 0,102,102,102,102,102,60,0: REM U 1540 DATA 0,102,102,102,102,60,24,0: REM V 1550 DATA 0,99,99,99,107,127,34,0: REM W 1560 DATA 0,76,76,56,56,100,100,0: REM X 1570 DATA 0,102,102,60,24,24,24,0: REM Y 1580 DATA 0,126,14,28,56,112,126,0: REM Z 1590 DATA 0,28,24,24,24,24,28,0: REM [ 1600 DATA 0,96,48,24,12,6,3,0: REM \ 1610 DATA 0,56,24,24,24,24,56,0: REM ] 1620 DATA 24,60,126,24,24,24,24,0: REM 1630 DATA 0,0,0,0,0,0,0,255: REM _ 1640 DATA 0,28,50,120,48,48,126,0: REM Ј 1650 DATA 0,0,60,6,62,102,62,0: REM a 1660 DATA 0,96,96,124,102,102,124,0: REM b 1670 DATA 0,0,60,102,96,102,60,0: REM c 1680 DATA 0,6,6,62,102,102,62,0: REM d 1690 DATA 0,0,60,102,124,96,60,0: REM e 1700 DATA 0,28,48,56,48,48,48,0: REM f 1710 DATA 0,0,62,102,102,62,6,60: REM g 1720 DATA 96,96,108,118,102,102,102,0: REM h 1730 DATA 24,0,56,24,24,24,60,0: REM i 1740 DATA 12,0,14,12,12,108,44,24: REM j 1750 DATA 96,96,102,108,120,108,102,0: REM k 1760 DATA 56,24,24,24,24,24,60,0: REM l 1770 DATA 0,0,118,127,107,107,99,0: REM m 1780 DATA 0,0,108,118,102,102,102,0: REM n 1790 DATA 0,0,60,102,102,102,60,0: REM o 1800 DATA 0,0,124,102,102,124,96,96: REM p 1810 DATA 0,0,60,76,76,60,12,14: REM q 1820 DATA 0,0,92,102,96,96,96,0: REM r 1830 DATA 0,0,60,96,60,6,124,0: REM s 1840 DATA 48,48,120,48,48,54,28,0: REM t 1850 DATA 0,0,102,102,102,110,54,0: REM u 1860 DATA 0,0,102,102,102,60,24,0: REM v 1870 DATA 0,0,99,107,107,127,54,0: REM w 1880 DATA 0,0,70,44,24,52,98,0: REM x 1890 DATA 0,0,102,102,102,62,6,60: REM y 1900 DATA 0,0,126,28,56,112,126,0: REM z 1910 DATA 0,12,24,24,48,24,24,12: REM { 1920 DATA 0,24,24,24,24,24,24,0: REM | 1930 DATA 0,48,24,24,12,24,24,48: REM } 1940 DATA 108,108,36,72,0,0,0,0: REM ~ 1950 DATA 60,98,221,217,217,221,98,60: REM ©Получить новый набор, включающий в себя русские буквы (рис. 4.4.), поможет следующая программа. Она отличается от предыдущей фактически только блоком данных, поэтому все вышесказанное в полной мере относится и к ней.
Рис. 4.4. Полный набор русских символов
10 PAPER 5: BORDER 5: CLEAR 64767: LET s=0: LET ad=64768 20 PRINT INK 1;"Please wait"''': PLOT 0,160: DRAW INK 6;255,0: PLOT 0,116: DRAW INK 6;255,0 25 RANDOMIZE ad-256: POKE 23606,PEEK 23670: POKE 23607,PEEK 23671 30 FOR i=CODE " " TO CODE "<189>": FOR j=1 TO 8 40 READ a: LET s=s+a: POKE ad,a: LET ad=ad+1 50 NEXT j 60 PRINT CHR$ i;: NEXT i 70 POKE 23606,0: POKE 23607,60 80 IF s<>43996 THEN PRINT AT 10,8; INK 2; FLASH 1; "Error in DATA!!!": BEEP .5,-20: STOP 90 SAVE "rusfont"CODE 64768,768 1000 DATA 0,0,0,0,0,0,0,0: REM Space 1010 DATA 0,48,48,48,48,0,48,0: REM ! 1020 DATA 0,102,102,34,68,0,0,0: REM " 1030 DATA 0,32,96,255,255,96,32,0: REM # 1040 DATA 0,4,6,255,255,6,4,0: REM $ 1050 DATA 24,60,126,24,24,24,24,24: REM % 1060 DATA 24,24,24,24,24,126,60,24: REM & 1070 DATA 0,224,96,124,102,102,124,0: REM ' 1080 DATA 0,6,12,12,12,12,6,0: REM ( 1090 DATA 0,96,48,48,48,48,96,0: REM ) 1100 DATA 0,0,54,28,127,28,54,0: REM * 1110 DATA 0,0,24,24,126,24,24,0: REM + 1120 DATA 0,0,0,0,0,48,48,96: REM , 1130 DATA 0,0,0,0,124,0,0,0: REM - 1140 DATA 0,0,0,0,0,48,48,0: REM . 1150 DATA 0,4,12,24,48,96,64,0: REM / 1160 DATA 0,60,102,110,118,102,60,0: REM 0 1170 DATA 0,24,56,24,24,24,60,0: REM 1 1180 DATA 0,60,70,6,60,96,126,0: REM 2 1190 DATA 0,60,102,12,6,102,60,0: REM 3 1200 DATA 0,12,28,44,76,126,12,0: REM 4 1210 DATA 0,124,96,124,6,70,60,0: REM 5 1220 DATA 0,60,96,124,102,102,60,0: REM 6 1230 DATA 0,126,6,12,24,48,48,0: REM 7 1240 DATA 0,60,102,60,102,102,60,0: REM 8 1250 DATA 0,60,102,102,62,6,60,0: REM 9 1260 DATA 0,0,48,48,0,48,48,0: REM : 1270 DATA 0,0,48,48,0,48,48,96: REM ; 1280 DATA 0,0,12,24,48,24,12,0: REM < 1290 DATA 0,0,0,126,0,126,0,0: REM = 1300 DATA 0,0,48,24,12,24,48,0: REM > 1310 DATA 0,56,76,12,24,0,24,0: REM ? 1320 DATA 0,102,107,123,123,107,102,0: REM @ 1330 DATA 0,60,102,102,126,102,102,0: REM A 1340 DATA 0,124,96,124,102,102,124,0: REM B 1350 DATA 0,100,100,100,100,100,126,2: REM C 1360 DATA 0,30,38,38,38,38,127,0: REM D 1370 DATA 0,126,96,124,96,96,126,0: REM E 1380 DATA 0,126,219,219,219,126,24,0: REM F 1390 DATA 0,126,98,96,96,96,96,0: REM G 1400 DATA 0,70,46,28,56,116,98,0: REM H 1410 DATA 0,102,102,110,126,118,102,0: REM I 1420 DATA 24,90,102,110,126,118,102,0: REM J 1430 DATA 0,100,104,112,120,108,102,0: REM K 1440 DATA 0,30,38,38,38,38,102,0: REM L 1450 DATA 0,99,119,107,107,99,99,0: REM M 1460 DATA 0,102,102,126,102,102,102,0: REM N 1470 DATA 0,60,102,102,102,102,60,0: REM O 1480 DATA 0,126,102,102,102,102,102,0: REM P 1490 DATA 0,62,102,102,62,102,102,0: REM Q 1500 DATA 0,124,102,102,124,96,96,0: REM R 1510 DATA 0,60,102,96,96,102,60,0: REM S 1520 DATA 0,126,24,24,24,24,24,0: REM T 1530 DATA 0,102,102,102,62,6,60,0: REM U 1540 DATA 0,153,90,126,90,153,153,0: REM V 1550 DATA 0,124,102,124,102,102,124,0: REM W 1560 DATA 0,96,96,124,102,102,124,0: REM X 1570 DATA 0,99,99,121,109,109,121,0: REM Y 1580 DATA 0,60,102,12,6,102,60,0: REM Z 1590 DATA 0,98,98,106,106,106,126,0: REM [ 1600 DATA 0,60,102,14,6,102,60,0: REM \ 1610 DATA 0,98,98,106,106,106,127,1: REM ] 1620 DATA 0,102,102,102,62,6,6,0: REM 1630 DATA 0,0,224,96,124,102,124,0: REM _ 1640 DATA 0,0,102,107,123,107,102,0: REM Ј 1650 DATA 0,0,60,6,62,102,62,0: REM a 1660 DATA 0,0,124,96,124,102,124,0: REM b 1670 DATA 0,0,100,100,100,100,126,2: REM c 1680 DATA 0,0,30,38,38,38,127,0: REM d 1690 DATA 0,0,60,102,124,96,60,0: REM e 1700 DATA 0,0,60,90,90,60,24,24: REM f 1710 DATA 0,0,124,100,96,96,96,0: REM g 1720 DATA 0,0,76,108,56,108,100,0: REM h 1730 DATA 0,0,102,102,110,118,102,0: REM i 1740 DATA 24,24,102,102,110,118,102,0: REM j 1750 DATA 0,0,102,108,120,108,102,0: REM k 1760 DATA 0,0,30,38,38,38,102,0: REM l 1770 DATA 0,0,99,119,107,107,99,0: REM m 1780 DATA 0,0,102,102,126,102,102,0: REM n 1790 DATA 0,0,60,102,102,102,60,0: REM o 1800 DATA 0,0,126,102,102,102,102,0: REM p 1810 DATA 0,0,62,102,62,102,102,0: REM q 1820 DATA 0,0,124,102,102,124,96,96: REM r 1830 DATA 0,0,60,102,96,102,60,0: REM s 1840 DATA 0,0,126,24,24,24,24,0: REM t 1850 DATA 0,0,102,102,102,62,6,60: REM u 1860 DATA 0,0,219,90,60,90,219,0: REM v 1870 DATA 0,0,124,102,124,102,124,0: REM w 1880 DATA 0,0,96,96,124,102,124,0: REM x 1890 DATA 0,0,99,99,121,109,121,0: REM y 1900 DATA 0,0,60,102,12,102,60,0: REM z 1910 DATA 0,0,98,106,106,106,126,0: REM { 1920 DATA 0,0,60,102,14,102,60,0: REM | 1930 DATA 0,0,98,106,106,106,127,1: REM } 1940 DATA 0,0,102,102,62,6,6,0: REM ~ 1950 DATA 60,98,221,217,217,221,98,60: REM ©Теперь попробуем, используя вновь приобретенные фонты, сотворить что-нибудь полезное и продемонстрируем на практике применение новых наборов символов. Например, сделаем один из кадров заставки - страничку «Правила игры» (рис. 4.5).
Рис. 4.5. Правила игры
После ввода исходного текста программы следует выйти в Бейсик и подгрузить в память только что полученные кодовые файлы:
LOAD "latfont"CODE 64000 LOAD "rusfont"CODE 64768Затем оттранслируйте набранный текст и введите команду редактора R для исполнения машинного кода.
Вот текст программы «Правила игры»:
ORG 60000 ENT $ LATF EQU 64000-256 RUSF EQU LATF+768 LD A,5 LD (23693),A LD A,0 CALL 8859 CALL 3435 LD A,2 CALL 5633 ; Печать «отрывной части блокнота» LD DE,TXT LD BC,TXT1-TXT CALL 8252 ; Печать названия игры «MOON SHIP» LD HL,LATF LD (23606),HL LD DE,TXT1 LD BC,TXT2-TXT1 CALL 8252 ; Печать русского текста LD HL,RUSF LD (23606),HL LD DE,TXT2 LD BC,TXT3-TXT2 CALL 8252 ; Печать слова «Enter» LD HL,LATF LD (23606),HL LD DE,TXT3 LD BC,TXT4-TXT3 CALL 8252 ; Печать русского текста LD HL,RUSF LD (23606),HL LD DE,TXT4 LD BC,TXT5-TXT4 CALL 8252 ; Печать слова «Space» LD HL,LATF LD (23606),HL LD DE,TXT5 LD BC,TXT6-TXT5 CALL 8252 ; Печать русского текста LD HL,RUSF LD (23606),HL LD DE,TXT6 LD BC,END-TXT6 CALL 8252 ; Рисование «листка блокнота» EXX ;сохранение HL' PUSH HL LD A,5 LD (23695),A ; Левая вертикальная линия LD DE,#101 ;DRAW 169,0 LD BC,#A900 CALL 9402 ; Верхняя горизонтальная линия LD DE,#101 ;DRAW 0,255 LD BC,255 CALL 9402 ; Правая вертикальная линия LD DE,#FF01 ;DRAW -169,0 LD BC,#A900 CALL 9402 ; Нижняя горизонтальная линия LD DE,#1FF ;DRAW 0,-255 LD BC,255 CALL 9402 POP HL ;восстановление HL' EXX ; Восстановление стандартного шрифта LD HL,15360 LD (23606),HL RET ; Данные для печати «отрывной части блокнота» TXT DEFB 22,1,0,16,5 DEFB 32,131,32,131,32,131,32,131 DEFB 32,131,32,131,32,131,32,131 DEFB 32,131,32,131,32,131,32,131 DEFB 32,131,32,131,32,131,32,131 ; Данные текста правил игры TXT1 DEFB 22,2,8,16,4 DEFM "M O O N··S H I P" DEFB 22,3,7,16,6 DEFM "------------------" TXT2 DEFB 22,4,10,16,7 DEFM "Prawila igry" DEFB 22,6,2,16,2,"1",".",16,5 DEFM " Dlq··uprawleniq···korablem" DEFB 22,7,1 DEFM "ispolxzujte··klawi{i " DEFB 16,7 DEFM "# $ % " DEFB 16,5,"i" DEFB 16,7 DEFM " &" DEFB 22,9,2,16,2,"2",".",16,5 DEFM " Dlq ustanowki dozy gorЈ~e-" DEFB 22,10,1 DEFM "go naberite ~islo ot 10 do 200" DEFB 22,11,1 DEFM "ili 0 i··navmite klawi{u " TXT3 DEFB 16,3 DEFM "Enter" TXT4 DEFB 22,13,2,16,2,"3",".",16,5 DEFM " Wremennaq ostanowka -" TXT5 DEFB 16,3 DEFM "Space" TXT6 DEFB 22,15,2,16,2,"4",".",16,5 DEFM " Blestq}aq posadka dostiga-" DEFB 22,16,1 DEFM "etsq, esli··na··wysote, rawnoj" DEFB 22,17,1 DEFM "nulЈ, skorostx··lunnogo modulq" DEFB 22,18,1 DEFM "takve budet rawna nulЈ" DEFB 22,20,7,16,1 DEFM "Prodolvatx ? (" DEFB 16,2,18,1,"D" DEFB 16,1,18,0,"/" DEFB 16,2,20,1,18,1,"N" DEFB 16,1,20,0,18,0,")" ENDНесколько слов о самой программе. Как вы заметили, здесь впервые встретилась директива ассемблера EQU (Equal - равный), которая служит для определения констант. Эта директива обязательно должна располагаться следом за какой-нибудь меткой, и именно этой метке в результате будет присвоено значение выражения после EQU. Таким образом, метка LATF в нашей программе становится численно равной адресу размещения латинского шрифта, уменьшенного на 256, то есть 63744, а метка RUSF принимает значение 64512.
Директиву EQU особенно удобно использовать в тех случаях, когда какая-либо постоянная величина многократно встречается в тексте программы. Если во время отладки ее потребуется изменить, то не нужно будет редактировать множество строк, достаточно будет лишь скорректировать выражение после EQU.
Есть одна особенность применения констант. Если вы используете в выражении ссылки на какие-нибудь другие имена, например
ENDTXT EQU TEXT+LENTXTто все они (в данном случае TEXT, и LENTXT) должны быть определены в программе до строки EQU, иначе ассемблер не сможет вычислить значение выражения и выдаст сообщение об ошибке.
Использование констант LATF и RUSF в программе «Правила игры» позволяет легко изменить при желании адреса загрузки шрифтов, а «включение» того или иного набора в тексте программы становится более наглядным. Эти «переключения» выполняют строки
LD HL,LATF LD (23606),HLили
LD HL,RUSF LD (23606),HLЗдесь число 23606 - адрес системной переменной CHARS, которая указывает местоположение в памяти текущего символьного набора. Напомним, что для «включения» нового фонта необходимо прежде уменьшить его адрес на 256, а затем записать два байта полученного числа в ячейки 23606 и 23607 (младший байт, как всегда, на первом месте).
Возможно, вам не совсем понятно, зачем нужно уменьшать адрес загрузки шрифта на 256 перед занесением его в системную переменную CHARS, поэтому поясним, отчего так происходит. Назвав новые наборы символов полными, мы слегка погрешили против истины, так как на самом деле полный набор должен включать все коды от 0 до 255. Мы же пользуемся только «печатными» символами, имеющими коды от 32 до 127 включительно, то есть первые 32 символа оказываются как бы «выброшенными». Каждый символ занимает в памяти 8 байт, поэтому «реальный» адрес размещения фонта и будет равен адресу загрузки нового набора минус 32ґ8=256 байт. (Вообще говоря, в недрах подпрограммы печати символов из ПЗУ скрывается операция, вновь увеличивающая значение адреса на 256, но этот «тонкий» ход остается на совести разработчиков интерпретатора Бейсика для Speccy Примеч. ред.)
Глава 3 | Глава 5