В качестве источника информации этого раздела частично послужил форум zx.pk.ru. Благодарю участника форума под ником «krt17».
Расположение программ на картридже подчиняется некоторым правилам. При включении приставка «сканирует» начальные байты банков картриджа на наличие информации о составе программ на картридже. Проверяется не каждый банк, а с кратностью 8. Т.е. будут проверяться банки памяти 0, 8, 16, 24, 32, 40, 48 и 56Напомню, что внешний картридж может содержать не более чем 64 банка памяти..
Признаком наличия меню со списком программ является содержимое нулевого байта банка памяти. А именно если байт со смещением +0 содержит значение #53 (символ «S»), то приставка считает, что далее за этим байтом будет идти информация о списке программ, содержащихся на картридже.
Важный момент! Все остальные банки ПЗУ картриджа, где не содержится стартовое меню, должны иметь значение 0-го байта, не равное #53 или #FF. Обычно этот байт равен 0. Это связано с особенностьюЭто глюк процедуры сканирования банков ПЗУ - для банков памяти, не кратных 8, первый байт равный #FF считается как признак наличия меню. Более подробно об особенности работы этой процедуры будет написано ниже по тексту статьи. работы приставки.
Процедура сканирования содержимого ПЗУ приставки
Подпрограмма сканирования банков ПЗУ на наличие игр написана несколько глючно, из-за чего правила размещения игр в ПЗУ имеют некоторые нюансы. Это касается содержимого 0-го байта банка ПЗУ.
Для начала вот полный текст процедуры:
#855E PUSH AF
PUSH BC
PUSH DE
PUSH HL
LD DE,#868A ;адрес, где будут сохраняться
;найденные описатели игр (по 20 байт длиной каждый)
LD C,2 ;начинаем сканирование с банка №2
;(внутреннее ПЗУ, первый свободный банк)
#8567 PUSH BC
LD A,C
OUT (#5F),A ;включаем банк ПЗУ в раздел #0000-#3FFF
LD HL,0 ;адрес #0000
AND %00000111 ;проверка номера банка ПЗУ на кратность 8
LD A,(HL) ;содержимое байта по адресу #0000
JR NZ,#8585 ;если банк ПЗУ не кратен 8, проверяем его
;только на наличие байта #FF по адресу #0000
;банк ПЗУ кратен 8
;проверка на строку "COD" в начале банка
CP "C"
JR NZ,#8590 ;не сходится, переходим к проверка на байт "S"
INC HL
LD A,(HL)
CP "0" ;#4F
JR NZ,#85BA ;первый символ "C", а второй не "O", в этом
;банке нет описателей игр, поэтому проскакиваем
;на 8 банков вперёд
INC HL
LD A,(HL)
CP "D" ;проверка на "D"
JR NZ,#85BA ;первые символы "CO", а третий не "D", в этом банке
;нет описателей игр, поэтому проскакиваем на 8 банков вперёд
INC HL
LD A,(HL) ;вслед на "COD" должен быть символ #FF, тогда это
;будет расценено как признак наличия описателей игр
#8585 INC A ;проверка байта на #FF
JR NZ,#85A1 ;байт не #FF, описателей игр тут нет. Однако
;не проскакиваем не 8 банков вперед, а просто
;проверяем следующий по номеру банк.
;Найден признак наличия описателя игры в текущем банке.
;Переносим 20 байт описателя в буфер описателей.
#8588 INC HL
#8589 LD BC,20
LDIR
JR #859D ;проверка на наличие следующего описателя вслед за текущим.
;Проверка первого байта банка ПЗУ на символ "S"
#8590 CP "S"
JR NZ,#85BA ;это не символ "S", проскакиваем на 8 банков вперёд
JR #8588 ;символ "S" найден, копируем 20 байт описателя игры в буфер
;Проверка на наличие описателя игры вслед за текущим описателем.
;Если следующий байт не равен #FF, считаем, что вслед идёт ещё один описатель игры.
#859D LD A,(HL) ;проверка следующего байта на #FF
INC A
JR NZ,#8589 ;байт не равен #FF, значит следом идёт
;ещё один описатель, копируем его в буфер.
;Найден байт #FF, это конец описателей игр в текущем банке.
;Переходим к следующему банку ПЗУ
#85A1 POP BC ;восстанавливаем номер текущего банка ПЗУ
INC C ;следующий банк ПЗУ
;Проверка - мы в ПЗУ картриджа или во встроенном ПЗУ?
BIT 7,C ;проверка идёт по биту 7 номера банка ПЗУ.
;Если там 1, то мы в ПЗУ картриджа
JR NZ,#85AD ;мы в ПЗУ картриджа
;Мы в основном ПЗУ приставки (в нём может быть не более 8 банков)
BIT 3,C ;мы уже перебрали все банки встроенного ПЗУ? Т.е. достигли ли банки №8?
JR Z,#8567 ;номер банки еще меньше 8, значит мы во встроенном ПЗУ,
;Все банки встроенного ПЗУ перебраны, перебираемся в ПЗУ картриджа
LD C,#80 ;Установленный бит 7 в номере банка ПЗУ
;означает, что мы работаем с ПЗУ картриджа
#85AD BIT 6,C ;Проверка перебрали ли 64 банка ПЗУ
;(в данном случае организован перебор только 64 банков ПЗУ)
JR Z,#8567 ;если еще не перебрали 64 банка ПЗУ,
;то проверяем очередной банк
;Перебор банков ПЗУ завершён
LD A,0 ;включаем обратно 0-й банк ПЗУ
OUT (#5F),A
POP HL ;восстанавливаем все регистры и выходим
POP BC
POP AF
RET
#85BA POP BC ;восстановим номер текущего сканируемого банка ПЗУ
LD A,C ;прибавим 8, т.е. проверяем не все банки,
ADD A,8 ;а каждый 8-й
LD C,A
JR L85AD ;проверка номера банка и сканирование
Алгоритм работы процедуры сканирования банков ПЗУ
Алгоритм работы процедуры сканирования банков ПЗУ
Из этой процедуры видно, что:
Для банков внутреннего ПЗУ приставки:
Перебираются банки ПЗУ с номерами от 2 до 7 включительно;
Признаком наличия описателей игр может являться только байт #FF в начале банка ПЗУ;
Из-за особенностей дешифрации банков внутреннего ПЗУ из неиспользуемых банков ПЗУ будет читаться либо содержимое банка 0, либо банка 1. В любом случае из 0-го байта любой неиспользуемого банка будет читаться #F3 (код команды DI). Таким образом не будет никаких «ложных» определений несуществующих описателей игр.
Для банков ПЗУ картриджа:
Перебираются только 64 банка ПЗУ (хотя технически в картридже доступно 127 банков);
В банках, кратных 8, признаком наличия описателей игр является 0-й байт банка равный «S» либо по байтам «C»,«O»,«D»,#FF в начале банка ПЗУ.
Если в банке с номером, кратным 8, не найдено описателей игр, то перескакиваем сразу на 8 банков вперёд и проверяем там.
Если в банке с номером, кратным 8, были найдены описатели игр, то после этого банка проверяем ещё семь банков ПЗУ подряд по первому байту на значение #FF. Если там #FF, то считаем, что в этом банке есть как минимум один описатель игры.
Из этого следует, что минимальный объём картриджа должен быть не менее 8 банков ПЗУ (128К), иначе при чтении из несуществующих банков не кратных 8 получим байты #FF, что вызовет ложные определения описателей игр. По этой же причине во всех банках ПЗУ картриджа, где нет описателей игр самый первый байт каждого банка не должен быть равен #FF!
Описатели меню необязательно должны находиться все вместе рядом. Они могут быть «раскиданы» по банкам ПЗУ. Но с тем условием, чтобы в самом первом банке ПЗУ картриджа был хотя бы один описатель игры. В банках памяти, не кратных 8, с описателями игр самый первый байт банка должен быть равен #FF.
Вот такая несколько сложная процедура сканирования. Дла нашего случая, если адаптировать игры на картридж, требуется помнить следующее:
Минимальный объём картриджа должен быть не менее 8 банков ПЗУ;
Максимальный объём картриджа - 64 банка (1МБ);
В 0-м банке ПЗУ картриджа обязательно должен быть хотя бы один описатель игры (первый байт банка ПЗУ должен быть «S»);
Если ещё в каком-либо банке ПЗУ есть описатели игр, этот банк должен начинаться с байта со значением #FF;
Все остальные банки ПЗУ вне зависимости от того есть в них полезные данные или нет, не должны содержать 0-й байт равный #FF (обычно туда заносится 0);
Теперь пройдёмся по структуре описателя игры...
Структура описателя игры
В общем случае формат банка ПЗУ с меню можно представить в следующем виде:
#53 - содержимое первого байта банка ПЗУ. Означает, что банк содержит в себе меню с информацией о составе программ на картридже.
Далее идёт меню. Оно состоит из записей вида:
defm " NAZVANIE" ; название пункта меню. Должно начинаться с пробела!
; длина названия (с пробелом) долна быть ровно 13 байт.
defb " " ; тут 4 пробела (они дополняют название до 13 байт длины)
; если название меньше 13 байт,
; дополняем его пробелами до 13 байт длины!
defb bank ; номер банка ПЗУ картриджа, откуда копируются данные
defw start ; адрес в ПЗУ, где начинается блок данных (#0000-#3FFF)
defw dest ; адрес назначения (адрес в ОЗУ, куда копируется блок данных)
defw length ; длина блока данных
defb #ff ; признак последнего пункта в меню в текущем банке памяти (!)
Последний байт - #FF, является закрывающим всё меню. Он означает, что после него нет никаких данных, описывающих структуру программ на картридже. Этот байт означает, что в текущем банке памяти больше нет никаких записей о программах. Это касается только текущего банка памяти! Это не означает, что приставка, встретив этот байт, больше не будет искать описатели программ нигде. Описатели программ могут быть «раскиданы» по разным банкам памяти.
Теперь - расшифровка записи меню:
Название программы - это то, что будет отображаться в списке программ, появляющемся при включении приставки. Под название отведено ровно 13 байт. Из них первый байт должен быть пробел. Если название по длине меньше, чем 13 байт, оно должно дополняться пробелами до полной длины 13 байт. Кроме того следует помнить, что меню приставки русскоязычное, поэтому следует писать название программы латиницей так, чтобы оно на приставке выглядело читаемо. Заглавные символы - русские. Строчные символы вообще не поддерживаются. Т.е. единственный шрифт в приставке - русский, латинских символов нет.
Полный знакогенератор в меню приставки «Эльф»:
Как видно в знакогенераторе отсутствует символ "Ъ". При написании текста он заменяется на символ с кодом #27 - "  '  ".
Кроме этого на момент рисования рамки окна в знакогенератор подставляются символы рамки со следующими кодами:
bank - номер банка ПЗУ, в котором расположен текущий блок данных (Program Data), который будет копироваться в ОЗУ. Тут имеется в виду то значение, посылаемое в порт #5F, которое включает нужный нам банк ПЗУ. Так, для 0-го банка ПЗУ картриджа это будет значение #80, для 1-го - #81 и так далее. Короче, это значение указывает приставке, из какого банка будет браться блок данных для переноса в ОЗУ.
start - адрес в ПЗУ, указывающий на начало блока данных (Program Data). Т.к. банк ПЗУ имеет объём 16К, то этот адрес может принимать значения от #0000 до #3FFF включительно.
dest - адрес в ОЗУ, куда будет перенесён блок данных (Program Data). Тут, думаю, всё ясно.
length - длина блока данных (Program Data).
В общем случаче кодовый блок программы, которую надо загрузить, можно представить следующим образом:
Description - это ни что иное как описание программы, которое вы можете увидеть при выборе программы в меню. Оно представляет собой сплошной текст без каких-либо управляющих символов. Текст показывается на экране в виде 27 строк длиной по 28 символовОбщая длина строки - 32 символа (8x8 точек). Отнимаем два знакоместа на рамку и ещё два знакоместа на пробелы между текстом и рамкой. Получаем 28 символов. каждая.
Длина описания - ровно 27x28 = #02F4 байт. Описание всегда расположено перед первым блоком программы! Ещё один момент - параметр start в описателе меню указывает не на начало описания, а на начало блока данных программы! А позиция описания уже вычисляется автоматически и нас волновать не должна никак. Опять же - описание должно быть на русском языке, т.к. знакогенератор у загрузочного меню приставки русский.
Program Data - непосредственно блок данных программы, который должен быть скопирован из ПЗУ в ОЗУ по адресу dest. Блоков может быть несколько (если программа не помещается в 16К). Кроме того, конец блока не должен находиться "впритык" к концу банка памяти, т.к. после окончания блока данных потребуется немного места для Execution code.
Execute code - исполняемый код. Сюда передаётся управление после загрузки кодового блока программы. Более подробный разбор работы Execute code находится чуть ниже по тексту страницы.
Пример №1 загрузки программ с ПЗУ картриджа
Начну с того факта, что при работе ПЗУ приставки «Эльф» в память по адресу #4000 помещается следующий код длиной 5 байт:
#4000 OUT (#5F),A
#4002 LDIR ;переносим первый блок игры в ОЗУ
#4004 JP (HL) ;после переноса HL будет указывать на первый байт за
;концом блока игры в ПЗУ. Переходим на него.
Бессмыслица? Вовсе нет. Поясню, зачем этот код нужен: возьмём, к примеру, некую программу, состоящую из одного блока объёмом менее 16К, чтобы он поместился в один банк ПЗУ. Пусть длина блока будет #3000 байт, а адрес запуска программы после её загрузки будет #6000. Сам кодовый блок будем загружать по тому же адресу. Помещаем кодовый блок этой программы в банке ПЗУ так, чтобы между концом блока и концом банка ПЗУ оставалось 13 байт.
Пусть всё это будет размещено в самом первом банке ПЗУ картриджа. Структура банка будет следующего вида:
#0000
defm "S" ; признак, что в данном банке ПЗУ есть описатель (или несколько) с играми.
defm " NAZVANIE " ; название программы.
defb #80 ; первый банк ПЗУ картриджа.
defw #0FF6 ; адрес в ПЗУ, где начинается блок данных.
defw #6000 ; адрес назначения (адрес в ОЗУ, куда копируется блок данных).
defw #3000 ; длина блока данных. Обратите внимание, что тут должна быть длина самого блока,
; без тех дополнительных 13 байт, которые мы поместили за концом блока данных.
defb #ff ; признак последнего пункта в меню в текущем банке памяти.
До адреса #0D02 будет свободное неиспользуемое место.
#0D02 ; тут располагается описание игры. Длина описания - #02F4 байт.
#0FF6 ; кодовый блок игры длиной #3000 байт.
А с адреса #3FF6 помещаем 10 байт следующего кода:
#3FF6 LD A,1 ;включаем банк ПЗУ с BASIC-48
#3FF8 LD HL,0 ;затираем команду LDIR
#3FFB LD (#4002),HL
#3FFE LD H,#60 ;#6000 - стартовый адрес нашей программы
Как оно работает? При выборе игры из описателя игры берётся начальный адрес кодового блока в банке ПЗУ (#0FF6), адрес в ОЗУ, куда надо его поместить (#6000) и длина блока (#3000). Этот блок переносится из ПЗУ в ОЗУ. Для справки, вот код в ПЗУ, который выполняет эту операцию:
LD A,(IX+13) ;берём из описателя игры номер банка ПЗУ с первым
;блоком игры
LD L,(IX+14)
LD H,(IX+15) ;HL-адрес первого блока игры в ПЗУ
LD E,(IX+16)
LD D,(IX+17) ;DE-куда копировать в ОЗУ первый блок игры
LD C,(IX+18)
LD B,(IX+19) ;BC-длина блока
JP #4000 ;Переход на код переноса блока и запуск
После того, как блок будет перенесён в память средствами меню, выполняется команда JP (HL) по адресу #4004 (см. выше). В данном случае HL будет указывать на первый байт за концом загружаемого блока, то есть по адресу #3FF6, куда, если помните, мы уже поместили 10 байт кода. Этот код включит ПЗУ с Basic-48, затрёт команду LDIR по адресу #4002, и, казалось бы закончится командой LD H,#60, но это лишь кажется. После этой команды код начнёт выполняться дальше со следующего адреса - #4000, где, как уже было написано выше, будет находиться модифицированный код:
#4000 OUT (#5F),A ;В регистре A будет значение #01. Включаем ПЗУ с Basic-48
#4002 NOP ;Эти два байта мы "затёрли".
#4003 NOP
#4004 JP (HL) ;H=#60, L=#00, адрес запуска загруженного блока кодов.
;Переходим на этот адрес, то есть запускаем нужный нам блок кодов.
Но что же делать, если у нас программа большая, и её нельзя загрузить в один заход? Поясню на ещё одном примере. Допустим, у нас запускаемый кодовый блок игры имеет длину не #3000 байт, а #6500. Игра займёт 2 банка ПЗУ картриджа:
; БАНК ПЗУ #80
#0000 defm "S" ; признак, что в данном банке ПЗУ есть описатель (или несколько) с играми.
#0001 defm " NAZVANIE " ; название программы (13 байт).
#000E defb #80 ; первый банк ПЗУ картриджа.
#000F defw #0FF6 ; адрес в ПЗУ, где начинается блок данных.
#0011 defw #6000 ; адрес назначения (адрес в ОЗУ, куда копируется блок данных).
#0013 defw #3000 ; длина блока данных. Обратите внимание, что тут должна быть
; длина самого блока, без тех дополнительных 13 байт,
; которые мы поместили за концом блока данных.
#0015 defb #ff ; признак последнего пункта в меню в текущем банке памяти.
#0016 (#02F4) ; описание игры. Длина описания - #02F4 байт.
#030A ; первая часть кодового блока игры длиной #3CEE байт.
; Затем 8 байт кода.
#3FF8 LD A,#81
#3FFA LD HL,1
#3FFD LD BC,#2812
; БАНК ПЗУ #81
#0000 DEFB #00 ;первый байт банка ПЗУ, где нет описателей игр, должен иметь значение #00.
#0001 (#2812) ;вторая часть кодового блока игры длиной #2812 байт.
#2813 LD A,1 ;включаем банк ПЗУ с BASIC-48
#2815 LD HL,0 ;затираем команду LDIR
#28818 LD (#4002),HL
#281B LD H,#60 ;#6000 - стартовый адрес нашей программы
Кодовый блок игры разбиваем на 2 части - длиной #3CEE байт и длиной #2812 байт. Длина первой части выбирается так, чтобы она влезла между описателем игры, описанием игры и чтобы до конца банка ПЗУ осталось 8 байт.
При выборе игры в меню приставки, оно загружает первую половинку кодового блока игры и делает переход на первый байт за концом загруженного блока, то есть по адресу #3FF8. Код, расположенный по этому адресу, загружает регистры данными, которые нужны для загрузки следующей "половинки" кодового блока игры (#81 - номер банка ПЗУ, где расположена вторая "половинка", #0001 - адрес начала второй "половинки" кода, #2812 - длина загружаемого остатка кода. После этого начнётся выполнение кода в адресе #4000, в результате чего в память будет загружена вторая "половинка" кода игры, и выполнится переход по адресу за концом этой "половинки", то есть по адресу #2813, где расположен очередной код, который включит ПЗУ с Basic-48 и выполнит переход по адресу #6000 для запуска игры.
Именно таким образом сделана загрузка игр на оригинальных картриджах приставки.
Пример №2 загрузки программ с ПЗУ картриджа - игра Exolon
Есть и другой способ загрузки программ с картриджа. Помещаем в память загрузчик игры и передаём управление ему. А он, в свою очередь, будет загружать из ПЗУ нужные блоки игры.
В качестве работающего примера приведу загрузчик игры Exolon:
;Игра занимает 2 банка ПЗУ.
;--------Банк ПЗУ №0 картриджа-------------
#0000 DEFB #53 ;"S" - признак 0-го банка ПЗУ картриджа
DEFB " \KSOLON " ;название игры, обязательно начинается с пробела
;и дополняется пробелами до 13 байт длины
DEFB #80+#00 ;загрузчик располагается в 0-м банке ПЗУ.
;#80 - установленный бит 7, означающий, что работаем с ПЗУ картриджа.
DEFW #030A ;адрес в банке ПЗУ, где располагается начало загрузчика
DEFW #6000 ;адрес в памяти, куда скопируется загрузчик
DEFW #006E ;длина загрузчика
DEFB #FF ;байт, закрывающий меню. Означает, что в данном банке ПЗУ больше нет описателей игр.
... - тут находится текстовое описание игры длиной ровно #02F4 байт.
;Непосредственно загрузчик игры. Переносится и запускается с адреса #6000.
;При запуске игры прерывания уже отключены.
#030A LD SP,#5FFF
LD A,#80 ;Включаем банк 0 картриджа
OUT (#5F),A
LD HL,#0400 ;Переносим упакованную заставку игры в память
LD BC,#16B1
LD DE,#8000
LDIR
LD A,#01 ;Включаем ПЗУ с BASIC-48.
;Точно не вспомню, но без ПЗУ с Бейсиком заставка не распаковывалась.
OUT (#5F),A
CALL #8000 ;Распаковка заставки игры
LD A,#80 ;Обратно включаем банк 0 картриджа
OUT (#5F),A
#0327 IN A,(#1F) ;Ждём отпускания кнопок джойстика 1 (если они были до того нажаты -
;при старте игры мы могли оставить нажатой кнопку выбора игры на джойстике)
AND #1F
JR NZ,#0327
CALL #036F ;Пауза
LD B,5 ;Делаем длинную паузу после показа заставки.
#0332 LD DE,#FFFF
#0335 IN A,(#1F) ;Но при нажатии любой кнопки джойстика 1 начнём загрузку основного блока игры,
;не дожидаясь, пока пауза закончится.
AND #1F
JR NZ,#0342
DEC DE
LD A,D
OR E
JR NZ,#0335
DJNZ #0332
#0342 LD HL,#1AB1 ;Загружаем первую часть основного блока игры в память
LD DE,#6D60
LD BC,#254F
LDIR
LD A,#81 ;Переключаемся на 1-й банк ПЗУ картриджа
OUT (#5F),A
LD HL,#0001 ;Догружаем остаток основного блока игры из ПЗУ в память
LD BC,#3245
LDIR
LD A,1 ;Включаем ПЗУ с Basic-48
OUT (#5F),A
CALL #6D60 ;Распаковываем основной блок игры
IN A,(#1F) ;Если при распаковке игры держать нажатыми кнопки Up+Down+Left+Right джойстика 1,
;включаем режим бесконечных жизней в игре.
AND #1F
CP %00001111
JR NZ,#036C
XOR A
LD (#9D1D),A
#036C JP #6D60 ;Запуск игры
;Подпрограмма делает небольшую паузу
#036F LD BC,#1000
#0372 DEC BC
LD A,B
OR C
JR NZ,#0372
RET
; Конец загрузчика
;Эти три байта располагаются за загрузчиком игры и не входят в сам загрузчик!
;Эти байты - это запуск загрузчика. ПЗУ Эльфа при старте игры переносит в память загрузчик
;и делает переход на первый байт после конца загрузчика.
;Следовательно после конца загрузчика должна быть команда запуска загрузчика.
#037C JP #6000 ;Загрузчик был скопирован в память по адресу #6000, делаем переход на него.
Дальше до адреса #0400 заполнено байтами #FF.
Адрес #0400 выбран просто для удобства, как «круглое» число.
#0400 - тут хранится упакованная заставка игры (длина #16B1 байт)
#1AB1 - первая часть упакованного основного блока игры (длина #254F байт)
;Конец 0-го банка ПЗУ.
;--------Банк ПЗУ №1 картриджа-------------
#0000 DEFB #00 - признак того, что в этом банке ПЗУ нет описателей игр.
#0001 - вторая часть упакованного основного блока игры (длина #3245 байт).
;Дальше до конца банка ПЗУ всё заполнено байтом #FF.
Ограничения при загрузке программ с картриджа
Важный момент! Никакие системные переменные BASIC-48 при старте приставки не инициализированы! Можно так сказать память девственно чиста. Прерывания запрещены. Указатель стека SP=#0000. Однако некоторые программы могут требовать уже проинициализированных переменных BASIC. Для такого случая можно хранить заранее подготовленный кусок области ОЗУ от адреса #5C00, где находятся нужные нам системные переменные, и копировать этот код вместе с блоком данных из ПЗУ картриджа в ОЗУ.
Момент №2 - нельзя использовать для загрузчика область ОЗУ с адреса #4000 и длиной 5 байт. В эту область памяти помещается код для запуска загруженных из ПЗУ данных, и его нельзя портить.
Момент №3 - загрузочный блок + описание игры не могут быть «разорваны» границей банков ПЗУ. То есть этот блок должен целиком поместиться внутрь одного банка ПЗУ.
Полный дизассемблер ПЗУ приставки «Эльф»
Имеется в виду банк ПЗУ с номером 0, в котором содержится загрузочное меню с поиском игр на картридже. Этот банк состоит как бы из двух частей: в одной части по старту приставки делаются подготовительные операции (перенос знакогенератора и исполняемого кода в память), потом стартует вторая часть, которая собственно и является меню.
В этом разделе я поместил исходный код этих двух частей.
Оригинальное ПЗУ приставки «Эльф» содержит защиту от запуска прошивки на других Спектрумах. При старте проверяется соответствие «железа» приставке «Эльф», и в случае несоответствия на экран выводится сообщение «НЕ УКРАДИ!!!» и происходит сброс:
Процедура проверки очень проста: в приставке «Эльф» порт #1F сделан таким образом, что из его трёх старших битов читаются значения 101. Процедура в ПЗУ просто проверяет эти три бита. И если там не 101, выводится сообщение «НЕ УКРАДИ!!!».
Кроме оригинального ПЗУ имеет хождение модифицированное ПЗУ, где процедура проверки просто занулена. Больше никаких отличий в коде нет.
Полный дизассемблер оригинального и модифицированного ПЗУ можно скачать одним архивом в конце этой страницы.
Первая часть:
ORG #0000
DI
XOR A
LD I,A
LD R,A
IM 0
NOP
LD HL,#4000 ;очищаем полностью ОЗУ
L000C LD (HL),0
INC HL
LD A,H
OR L
JR NZ,L000C
;Переносим знакогенератор в #A100
;Знакогенератор не полный и содержит ТОЛЬКО символы русского алфавита и
;цифры. Латинских символов НЕТ!
LD HL,#0100
LD DE,#A100
LD BC,#0200
LDIR
;Перенос исполняемого кода загрузочного меню приставки в #8000
;и переход на него
LD HL,#2000
LD DE,#8000
LD BC,#1000
LDIR
JP #8000
;Исполняемый код загрузочного меню приставки. Переносится в #8000 и оттуда
;выполняется. Соответственно он скомпилирован под адрес #8000.
ORG #8000
LD SP,0
LD IX,L868A ;установка начального значения адреса буфера
;для найденных на картриджах описателей игр
LD (L8620),IX
CALL L8250 ;очистка экрана
;перенос кодов символов для рисовании рамки окна в знакогенератор
;используемые символы (6 штук) - #22,#23,#24,#25,#26,#27
LD HL,L8643
LD DE,#A110
LD BC,#0030
LDIR
L8019 LD A,0 ;BORDER=0
OUT (#FE),A
;Заполняем экран цветом PAPER=1, INK=0
LD L,8 ;Атрибут заполнения окна
LD BC,0 ;Координаты левого верхнего угла окна
LD DE,#1F17 ;Координаты правого нижнего угла (X=31, Y=23)
XOR A ;CF=0, вывод окна с очисткой пикселей
EX AF,AF'
CALL L851F ;чистим содержимое окна
;Вывод рамок двух окон (верхнее - для списка игр и нижнее - подпись)
LD A,#0F ;атрибуты рамок, PAPER=1, INK=7
CALL L8266
CALL L85C1 ;чистка буфера под найденные описатели игр
CALL L855E ;сканирование внутреннего ПЗУ приставки и картриджа
;на предмет наличия игр
CALL L8539 ;вывод первых 18 найденных игр в главное окно
CALL L8287 ;в текущих координатах выводим рамку выбора игры
;(текущие координаты при первом вызове соответствуют левой
;верхней игре в меню)
;Вывод текста "СКБ ЗАПАД" вверху главного окна
LD BC,#0A00 ;координаты вывода X=10, Y=0
LD A,#0E ;атрибуты цвета текста
LD D,#0D ;длина текста
LD HL,L8126 ;адрес текста
CALL L8432 ;вывод текста
;Вывод текста "МЕНЮ ИГР" внутри нижнего окна
LD BC,#0716
LD A,#0E
LD D,#10
LD HL,L8133
CALL L8432
;Проверка на "оригинальность" приставки.
;Если прошивка запущена не на приставке "Эльф", то выводим надпись "Не укради" и делаем сброс.
;В качестве критерия проверки выступают 3 старших бита порта #1F. В "Эльфе" из них читается 101.
;Если из этих битов при проверке не считывается 101, то выводим надпись "Не укради".
IN A,(#1F) ;читаем значение из порта #1F
AND %11100000 ;отбрасываем биты 0-4
CP %10100000 ;в оставшихся битах должно быть 101
JP NZ,L8181 ;если это не так, переходим на процедуру вывода надписи "Не укради".
;Опрос Джойстика-1 в главном меню. Главный цикл.
L805E IN A,(#1F)
AND %00011111
JR Z,L805E ;ни одна кнопка не нажата
CP 8
CALL Z,L834B ;обработка нажатия "курсор вверх"
CP 4
CALL Z,L82DA ;обработка нажатия "курсор вниз"
CP 2
CALL Z,L83E1 ;обработка нажатия "курсор влево"
CP 1
CALL Z,L83B6 ;обработка нажатия "курсор вправо"
CP 16
JR Z,L807E ;выбор игры
JR L805E
;Выбор игры, на которой стоит курсор
L807E LD A,(IX+17) ;если выбрана игра на пустом месте меню, то для неё
;в этой ячейке описателя будет #20 (перед сканированием
;игр записи для первыз 18 игр заполняются пробелами).
;На экране помещается как раз 18 игр одновременно.
CP #20
JR Z,L805E ;если выбрана игра на пустом месте меню, то игнорируем
;выбор
LD A,7
CALL L8250 ;чистим экран (заполняем нулями пиксели и атрибуты)
CALL L8266 ;Вывод рамок двух окон (верхнее - для списка игр
;и нижнее - подпись) с атрибутами в рег.А - PAPER=0, INK=7
;установка атрибутов внутри нижнего окна
LD L,6 ;атрибут PAPER=0, INK=7
LD BC,#0015 ;координаты левого верхнего угла окна X=0, Y=21
LD DE,#2018 ;координаты правого нижнего угла окна X=32, Y=24
SCF ;CF=1, чистим только атрибуты, без пикселей
EX AF,AF'
CALL L851F
;Печать сообщения "СКБ ЗАПАД"
LD BC,#0A00 ;Координаты X=10, Y=0
LD D,#0D
LD A,4
LD HL,L8126
CALL L8432
;Вывод сообщения "МЕНЮ ИГР"
LD BC,#0A16
LD A,4
LD D,#0D
PUSH IX
POP HL
CALL L8432
XOR A ;CF=0
LD A,(IX+13) ;номер банка ПЗУ с первым блоком игры
LD L,(IX+14)
LD H,(IX+15) ;адрес в ПЗУ начала блока данных игры
LD DE,#02F4 ;отнимаем от него #02F4 (длина описания игры) и получаем
;адрес начала описания игры
SBC HL,DE
LD DE,L8D20 ;переносим описание игры в ОЗУ (т.к. ПЗУ мы будем
;отключать)
PUSH DE
LD BC,#02F4
OUT (#5F),A ;включаем банк ПЗУ с игрой
POP HL
CALL L85E4 ;печатаем описание игры в верхнем (большом) окне
LD C,1
L80D8 CALL L81AD ;Вывод окна с подсказкой (назначение кнопок джойстика
;при просмотре описания игры) и опрос Джойстика-1.
;По возвращению сюда окно с подсказкой уже будет убрано
;и возвращено описание игры.
CP #10 ;был нажат "огонь" на джойстике-1 ?
JR Z,L8100 ;да, переходим, это старт игры
CP 1 ;нажатие "вправо" - возврат в меню
JP Z,L8019
JR L80EA ;другие кнопки - просмотр описания игры
;Опрос Джойстик-1 в режиме просмотра описания игры
L80E6 IN A,(#1F)
AND %00011111
L80EA CP 4 ;кнопка "вниз"
CALL Z,L860F
CP 8 ;кнопка "вверх"
CALL Z,L85FF
CP 16 ;кнопка "огонь" - вызов окна с подсказкой
JR Z,L80D8
JP L80E6
;Неиспользуемый код
L80FB JR L80FB
;L80FD
JP L8019
;Старт игры с картриджа
L8100 CALL L8250 ;чистим экран
LD HL,L81A8 ;переносим кодовый блок запуска игры в #4000
LD DE,#4000
LD BC,5
LDIR
LD A,(IX+13) ;берём из описателя игры номер банка ПЗУ с первым
;блоком игры
LD L,(IX+14)
LD H,(IX+15) ;HL-адрес первого блока игры в ПЗУ
LD E,(IX+16)
LD D,(IX+17) ;DE-куда копировать в ОЗУ первый блок игры
LD C,(IX+18)
LD B,(IX+19) ;BC-длина блока
JP #4000 ;Переход на код переноса блока и запуск
L8126 DEFM " SKB "ZAPAD" "
L8133 DEFM " MEN@ IGR "
L8143 DEFM " POMO]X "
L814B DEFM "_ - START IGRY "
DEFM "/* - ^TENIE PRAWIL"
DEFM "> - WOZWRAT "
;Вывод сообщения "Не укради".
L8181 CALL L8250 ;Очистка экрана
LD BC,#0A0A ;Координаты для вывода
LD D,#0C
LD A,7
LD HL,L819C ;Текст "Не укради"
CALL L8432 ;Вывод текста
LD BC,#0100
L8194 CALL L841B ;Моргаем бордюром и выводим звук в динамик
DJNZ L8194
JP #0000 ;Потом делаем сброс.
L819C DEFM "NE UKRADI!!!"
;Код запуска первого блока игры.
;В чем суть: первый блок игры копируется в ОЗУ и делается переход обратно
;в ПЗУ на первый байт за концом блока. Там должен находиться обработчик,
;которому передаётся управление после копирования блока. Обработчик пишет
;сам пользователь. В случае наличия более чем одного блока игры обработчик
;должен сам подключать и копировать данные последующих блоков игры.
;Почему этот код находится в ОЗУ по адрему #4000? Потому, что мы будем
;выключать ПЗУ, следовательно этот код в ПЗУ не может находиться.
L81A8 OUT (#5F),A ;Подключаем банк ПЗУ с первым блоком игры
LDIR ;переносим первый блок игры в ОЗУ
JP (HL) ;после переноса HL будет указывать на первый байт за
;концом блока игры в ПЗУ. Переходим на него.
;Вывод окна с подсказкой, опрос Джойстик-1 и выход по нажатию любой
;кнопки джойстика. Значение, прочитанное из порта #1F возвращается
;в регистре А.
;На выходе содержимое экрана восстанавливается в то состояние, которое
;было на входе в процедуру.
L81AD PUSH AF
PUSH BC
PUSH DE
PUSH HL
CALL L8428 ;пауза
CALL L8212 ;сохраняем среднюю треть экрана с атрибутами
;Вывод окна подсказки в среднюю треть экрана
LD L,#17 ;атрибуты PAPER=2, INK=7
LD BC,#0209 ;координаты окна X=2, Y=9
LD DE,#1D0E ;X=29, Y=14
XOR A
EX AF,AF'
CALL L851F ;Чистим атрибуты и пиксели в окне
;Печать рамки в том же окне
LD BC,#0209
LD DE,#1D0E
LD HL,L8637
XOR A
EX AF,AF'
LD A,#17 ;атрибуты рамки PAPER=2, INK=7
CALL L8494
;Печать текста "ПОМОЩЬ"
LD HL,L8143
LD BC,#0C09
LD D,8
LD A,#17
CALL L8432
;Печать текста "СТАРТ ИГРЫ"
LD HL,L814B
LD BC,#070B
LD D,18
LD A,#17
CALL L8432
;Печать текста "ЧТЕНИЕ ПРАВИЛ"
LD BC,#070C
LD D,#12
LD A,#17
CALL #8432
;Печать текста "ВОЗВРАТ"
LD BC,#070D
LD D,#12
LD A,#17
CALL L8432
L8202 IN A,(#1F) ;опрос Джойстик-1
AND %00011111
JR Z,L8202
EX AF,AF'
CALL L8231 ;По нажатию любой кнопки восстанавливаем прежний
;текст на экране
POP HL
POP DE
POP BC
POP AF
EX AF,AF' ;Выходим со значением порта #1F в регистре А.
RET
;Сохранение средней трети экрана с атрибутами в буфер.
L8212 PUSH AF
PUSH BC
PUSH DE
PUSH HL
LD HL,#4800
LD DE,#5B00
LD BC,#0800
LDIR
LD HL,#5900
LD DE,#6300
LD BC,#0100
LDIR
POP HL
POP DE
POP BC
POP AF
RET
;Восстановление из буфера содержимого средней трети экрана с атрибутами.
L8231 PUSH AF
PUSH BC
PUSH DE
PUSH HL
LD HL,#5B00
LD DE,#4800
LD BC,#0800
LDIR
LD HL,#6300
LD DE,#5900
LD BC,#0100
LDIR
POP HL
POP DE
POP BC
POP AF
RET
;Очистка экрана - пиксели и атрибуты, заполняем нолями.
L8250 PUSH AF
PUSH BC
PUSH DE
PUSH HL
LD HL,#4000
LD DE,#4001
XOR A
LD (HL),A
LD BC,#1AFF
LDIR
POP HL
POP DE
POP BC
POP AF
RET
;Печать рамок двух окон (верхнего главного и нижнего) с атрибутами в рег.А
L8266 PUSH AF
PUSH AF
LD HL,L8637 ;адрес символов рамки
LD BC,0 ;задаём координаты окна X=0, Y=0
LD DE,#1F14 ;X=31, Y=20 правый нижний угол
XOR A
EX AF,AF'
POP AF
CALL L8494 ;вывод рамки
LD HL,L8637 ;адрес символов рамки
LD BC,#0015 ;X=0, Y=21 левый верхний угол
LD DE,#1F17 ;X=31, Y=23 правый нижний угол
XOR A
EX AF,AF'
POP AF
CALL L8494 ;вывод рамки
RET
;Вывод в текущих координатах окна выбранной игры с тенью
L8287 LD BC,(L8673) ;Координаты окна с тенью
LD DE,(L8675)
SCF ;CF=1 установка только атрибутов
EX AF,AF'
LD A,2 ;атрибуты окна INK=2 PAPER=0
CALL L8494 ;установка атрибутов окна
LD BC,(L8677) ;координаты окна выбора игры
LD DE,(L8679)
XOR A
EX AF,AF'
LD A,#17 ;атрибуты PAPER=2 INK=7
LD HL,L8637
CALL L8494 ;вывод рамки с атрибутами
LD L,#17 ;атрибуты PAPER=2 INK=7
LD BC,(L867B) ;заполнение внутреннего содержимого окна выбора
;игры атрибутами
LD DE,(L867D)
SCF
EX AF,AF'
CALL L851F
RET
;В текущих координатах убираем окно выбора игры (с рамкой)
L82B8 LD L,#0F ;PAPER=1, INK=7
LD BC,(L8683)
LD DE,(L8685)
SCF
EX AF,AF'
CALL L851F ;убираем окно рамки игры и тень
LD BC,(L867F)
LD DE,(L8681)
LD HL,L863D ;адрес символов рамки, состоящей из пробелов
XOR A
EX AF,AF'
LD A,#0F
CALL L8494 ;вывод рамки окна из прбелов
RET
;Обработка нажатия "вниз" на Джойстик-1 при выборе игры
L82DA PUSH AF
PUSH BC
PUSH DE
PUSH HL
LD BC,#0014 ;перескакиваем на описатель следующей игры
;(на 20 байт вперёд)
ADD IX,BC
CALL L8428 ;пауза
LD HL,L8673 ;проверка Y координаты текущего окна с рамкой
LD A,(HL)
CP 18 ;если она равна 18, то курсор находится на
;крайней нижней позиции
JR Z,L8300
;курсор ещё можно подвигать вниз
PUSH HL
CALL L82B8 ;убираем окно с рамкой в текущих координатах
POP HL
LD B,10 ;меняем Y координату для всех 5 окон
L82F5 INC (HL)
INC (HL)
INC HL
INC HL
DJNZ L82F5
CALL L8287 ;выводим окно с рамкой в новых координатах
JR L831D
;курсор достиг низа экрана. Но если курсор в левой колонке, то его можно
;переместить в верх правой колонки
L8300 INC HL ;проверяем X координату текущего окна
LD A,(HL) ;если курсор в правой колонке, то переходим на
;следующую проверку
CP 17
JR Z,L8322
;курсор находится внизу левой колонки, сдвигаем его в верх правой колонки игр
DEC HL
PUSH HL
CALL L82B8 ;убираем текущее окно выбора игры с экрана
POP HL
LD B,10 ;правим координаты всех 5 выриантов окон с рамкой
;выбора игры
L830E LD A,(HL)
SUB 16
LD (HL),A
INC HL
LD A,(HL)
ADD A,15
LD (HL),A
INC HL
DJNZ L830E
CALL L8287 ;рисуем курсор выбора игры на новом месте
L831D POP HL ;выходим
POP DE
POP BC
POP AF
RET
;если курсор внизу правой колонки, проверим может можно сдвинуть общий
;список игр, если в нём больше чем 18 игр (на экран влезает одновременно
;только 18 игр)
L8322 LD A,(IX+0) ;проверим начало описателя следующей игры
CP #FF ;если там #FF, то это означает, что описателя нет,
;игры нет
JR NZ,L8337 ;игра есть, двигаем общий список игр, оставляя курсор
;на месте
PUSH IX ;игр дальше нет, восстанавливаем адрес прежнего
;описателя игры
POP HL
LD DE,20
XOR A
SBC HL,DE
PUSH HL
POP IX
JR L831D ;и выходим ничего не делая
;двигаем список игр на 1 игру вперёд
L8337 PUSH IX
POP HL
XOR A
LD DE,#0154 ;адрес начала отображаемого списка игр меняем на
;20 байт вперёд
SBC HL,DE ;(это будет если от адреса описателя текущей игры
;отнять #0154
LD (L8620),HL ;17 игр x 20 байт)
CALL L853C ;печать меню игр с нового начального адреса
CALL L8287 ;печать курсора выбора игры на прежнем месте
JR L831D ;выход
;Обработка нажатия "вверх" на Джойстик-1 в меню выбора игры
L834B PUSH AF
PUSH BC
PUSH DE
PUSH HL
PUSH IX
POP HL
LD DE,20 ;адрес указателя на игру уменьшаем на 20 байт
XOR A
SBC HL,DE
PUSH HL
POP IX
CALL L8428 ;пауза
;проверка координаты текущего окна с рамкой выбора игры
LD HL,L8673 ;если она равна 2, то рамка находится на крайней
;верхней позиции
LD A,(HL)
CP 2
JR Z,L8378
;окно выбора игры можно подвигать вверх
PUSH HL
CALL L82B8 ;убираем окно с рамкой с прежних координат
POP HL
LD B,#0A ;правим координаты все 5 вариантов окон с рамкой
;выбора игры
L836D DEC (HL)
DEC (HL)
INC HL
INC HL
DJNZ L836D
CALL L8287 ;рисуем окно выбора игры на новом месте
JR L8395
;окно выбора игры находится в крайней верхней позиции
L8378 INC HL ;проверим X координату окна выбора игры
LD A,(HL) ;если она равна 2, то окно находится в левом столбце
CP 2
JR Z,L839A
;окно находится в правом столбце, значит его можно подвигать
;в левый столбец вниз экрана
DEC HL
PUSH HL
CALL L82B8 ;убираем окно со старого места
POP HL
LD B,#0A ;правим координаты всех 5 вариантов окон
L8386 LD A,(HL)
ADD A,16
LD (HL),A
INC HL
LD A,(HL)
SUB 15
LD (HL),A
INC HL
DJNZ L8386
CALL L8287 ;рисуем окно на новом месте
L8395 POP HL
POP DE
POP BC
POP AF
RET
;пробуем подвигать назад список игр (на случай, если обнаружено
;более чем 18 игр, они все не поместятся на экран, и список можно двигать)
L839A LD A,(IX+19) ;проверяем последний байт описателя игры
CP #FF ;если там #FF, то это пустой описатель, список игр
;двигать никак нельзя
JR NZ,L83A8 ;список игр можно двигать
;список игр подвигать нельзя
LD BC,20 ;восстанавливаем указатель на описатель игры
ADD IX,BC
JR L8395 ;и выходим
;двигаем список игр на одну игру назад
L83A8 PUSH IX
POP HL
LD (L8620),HL ;сохраняем новое значение адреса начала списка
;отображаемых игр
CALL L853C ;перепечатываем список игр на экране
CALL L8287 ;выводим рамку выбора игры в прежней позиции
JR L8395 ;и выходим
;Обработка нажатия "вправо" на джойстик-1 в меню выбора игр
L83B6 PUSH AF
PUSH BC
PUSH DE
PUSH HL
CALL L8428 ;пауза
LD HL,L8673+1 ;проверка X координаты окна с выбранной игрой
LD A,(HL) ;если мы в правом столбце игр, курсор двигать нельзя,
;выходим
CP 17
JR Z,L83DC
;курсор двигать можно
LD BC,#00B4 ;правим адрес описателя текущей игры на 9 игр вперёд
ADD IX,BC ;(в одном столбце помещается 9 игр)
PUSH HL
CALL L82B8 ;убираем окно с выбранной игрой со старого места
POP HL
LD B,#0A ;правим координаты всех 5 вариантов окон выбора игры
L83D1 LD A,(HL)
ADD A,15
LD (HL),A
INC HL
INC HL
DJNZ L83D1
CALL L8287 ;рисуем рамку выбора игры на новом месте
L83DC POP HL
POP DE
POP BC
POP AF
RET
;Обработка нажатия "влево" на джойстике-1 в меню выбора игр
L83E1 PUSH AF
PUSH BC
PUSH DE
PUSH HL
CALL L8428 ;пауза
LD HL,L8673+1 ;проверка X координаты окна с выбранной игрой
LD A,(HL) ;если там 2, то мы находимся в левом столбце,
CP 2 ;ничего не делаем и выходим
JR Z,L840E
;окно можно подвигать в левый столбец
PUSH HL
PUSH IX
POP HL
LD DE,#00B4 ;сдвигаем указатель описателя текущей игры на 9 игр назад
XOR A ;(в одном столбце отображается 9 игр)
SBC HL,DE
PUSH HL
POP IX
CALL L82B8 ;убираем рамку окна выбора игры с прежнего места
POP HL
LD B,10 ;правим координаты всех 5 вариантов окна с выбранной игрой
L8403 LD A,(HL)
SUB 15
LD (HL),A
INC HL
INC HL
DJNZ L8403
CALL L8287 ;рисуем окно с выбранной игрой на новом месте
L840E POP HL
POP DE
POP BC
POP AF
RET
;Эти процедуры используются при выводе сообщения "Не укради".
L8413 PUSH IX
CALL #03B5
POP IX
RET
L841B LD HL,#03E0
LD DE,#0030
CALL L8413
XOR A
OUT (#FE),A
RET
;Подпрограмма делает паузу
L8428 LD BC,#9010
L842B DEC C
JR NZ,L842B
DEC B
JR NZ,L842B
RET
;Печать текста
;HL-адрес текста, D-сколько символов печатать, BC- координаты XY,
;A-атрибут, которым закрашивается выводимый текст
L8432 PUSH AF
LD A,(HL) ;печать символа
CALL L8448
POP AF ;атрибут, которым закрашиваем текст
CALL L8475 ;подкрашиваем выведенный символ
INC B ;увеличиваем X координату на 1
BIT 5,B ;достигли правого края экрана?
JR Z,L8443
;залезли за правый край экрана
LD B,0 ;X координата = 0
INC C ;Y координату увеличиваем на 1
L8443 INC HL ;следующий адрес в тексте
DEC D ;счётчик выведенных символов уменьшаем на 1
JR NZ,L8432 ;если есть что печатать, повторяем процесс
RET
;Печать символа из рег.A в координатах BC (XY)
L8448 PUSH AF
PUSH BC
PUSH DE
PUSH HL
LD E,A
LD A,%00011000 ;выделяем треть экрана, в которую будет
; происходить печать
AND C
ADD A,#40 ;формируем в зависимости от трети экрана старший
;байт экранного адреса
LD H,A
LD A,C ;Y координата
ADD A,A ;умножаем на 32
ADD A,A
ADD A,A
ADD A,A
ADD A,A
ADD A,B ;добавляем X координату
LD L,A ;формируем младший байт экранного адреса
PUSH HL ;HL-адрес в экране для вывода символа
LD L,E ;код печатаемого символа
LD H,0
ADD HL,HL ;умножаем на 8
ADD HL,HL
ADD HL,HL
;В ПЗУ знакогенератор расположен с адреса #0100, поэтому тут должно
;быть #0000. Тогда первый символ знакогенератора
;с кодом #20 будет браться с адреса #0100
LD DE,0 ;адрес знакогенератора-256 байт.
L8464 EQU $-1
ADD HL,DE ;определяем адрес символа в знакогенераторе
EX DE,HL
POP HL
LD B,8 ;переносим коды символа из знакогенератора на экран
L846A LD A,(DE)
LD (HL),A
INC H
INC DE
DJNZ L846A
POP HL
POP DE
POP BC
POP AF
RET
;Процедура может задать атрибут из рег.A в координатах BC либо взять
;содержимое атрибута по координатам BC (XY)
;Если A=#FF, то берём в регистр A значение атрибута. При любом другом
;значении регистра A устанавливаем из него атрибут
L8475 PUSH BC
PUSH HL
LD H,0
LD L,C ;HL - Y координата
LD C,A
ADD HL,HL ;умножаем на 32
ADD HL,HL
ADD HL,HL
ADD HL,HL
ADD HL,HL
LD A,L
ADD A,B ;прибавляем X координату
LD L,A
LD A,#58 ;суммируем с началом области атрибутов #5800
ADD A,H
LD H,A ;HL-адрес в области атрибутов, соответствующий
;указанным координатам
LD A,C ;если на входе A=#FF, то извлекаем значение атрибута
CP #FF
JR NZ,L8490 ;иначе пишем атрибут из регистра A на экран
LD A,(HL) ;извлекаем атрибут
POP HL
POP BC
RET
L8490 LD (HL),A ;пишем атрибут
POP HL
POP BC
RET
;Процедура печати рамки окна
;HL-адрес строки символов с кодами рамки. 6 байт. (левый верхний угол рамки,
;левый нижний угол рамки, правый нижний угол рамки, правый верхний угол
;рамки, горизонтальная рамка, вертикальная рамка)
;A-атрибут, которым закрашивается рамка
;BC-координаты левого верхнего угла окна (XY)
;DE-координаты правого нижнего угла окна (XY)
;CF=0 - вывод символа рамки и атрибута,
;CF=1 - вывод только атрибута без вывода символа рамки
L8494 EX AF,AF' ;сохраним код атрибута
LD A,#A0 ;адрес знакогенератора #A000-256 байт
LD (L8464),A ;сохраним в процедуре вывода символа
;Печать левого верхнего угла рамки
JR C,L84A0 ;если только заполняем атрибутом, то пропускаем
;печать символа рамки
LD A,(HL) ;код печатаемого символа рамки (левый верхний угол)
CALL L8448 ;печатаем символ рамки
L84A0 EX AF,AF' ;атрибут
CALL L8475 ;выводим атрибут
PUSH BC
PUSH BC
PUSH BC
;Печать левого нижнего угла рамки
LD C,E ;координаты левого нижнего угла рамки
INC HL ;следующий символ из буфера
EX AF,AF'
JR C,L84B0 ;если надо вывести только атрибуты, то пропускаем
;печать символа
LD A,(HL) ;код очередного символа рамки
CALL L8448 ;печать символа
L84B0 EX AF,AF' ;атрибут
CALL L8475 ;вывод атрибута
;Печать правого нижнего угла рамки
LD B,D ;координаты правого нижнего угла рамки
INC HL ;следующим символ буфера
EX AF,AF'
JR C,L84BD ;если надо вывести только атрибуты, то пропускаем
;печать символа
LD A,(HL) ;код очередного символа рамки
CALL L8448 ;печат символа
L84BD EX AF,AF' ;код атрибута
CALL L8475 ;печать атрибута
;правый верхний угол рамки
POP BC
LD B,D ;координаты правого верхнего угла окна
INC HL ;следующий символ рамки
EX AF,AF'
JR C,L84CB ;если выводим только атрибут, то переходим
LD A,(HL) ;код символа рамки
CALL L8448 ;печать символа рамки
L84CB EX AF,AF' ;значение атрибута
CALL L8475 ;вывод атрибута
;печать горизонтальной рамки
POP BC ;левый верхний угол окна
INC HL ;следующий символ - символ горизонтальной рамки
L84D1 INC B ;X координата+1
PUSH AF ;сохраним код атрибута
LD A,B ;достигли правой границы окна?
CP D
JR Z,L84F3 ;да, достигли, переходим
POP AF ;восстановим код атрибута
EX AF,AF' ;признак печати символ/атрибут
JR C,L84DF ;если вывод только атрибута, то переходим
LD A,(HL) ;код символа горизонтальной рамки
CALL L8448 ;печать рамки по верхней части окна
L84DF EX AF,AF' ;атрибут
CALL L8475 ;вывод атрибута в текущих координатах
PUSH BC
LD C,E ;координаты нижнего края окна
EX AF,AF'
JR C,L84EC
LD A,(HL)
CALL L8448 ;печать рамки по нижнему краю окна
L84EC EX AF,AF'
CALL L8475 ;вывод атрибута для нижней рамки
POP BC
JR L84D1 ;повторяем, пока не напечатаем всю горизонтальную рамку
;вывод вертикальной рамки по левому и правому краям окна
L84F3 POP AF ;атрибут
POP BC ;исходная координата левого верхнего угла окна
INC HL ;следующий символ - вертикальная рамка
L84F6 INC C ;Y координата+1
PUSH AF
LD A,C
CP E ;достигли нижнего края окна?
JR Z,L8518 ;да, достигли, выходим из процедуры
POP AF
EX AF,AF'
JR C,L8504 ;переход, если нужно вывести только атрибут
LD A,(HL) ;код символа вертикальной рамки
CALL L8448 ;выводим рамку по левому краю окна
L8504 EX AF,AF'
CALL L8475 ;вывод атрибута для этой рамки
PUSH BC
LD B,D ;координаты для рамки по правой части окна
EX AF,AF'
JR C,L8511 ;переход, если вывод только атрибута без рамки
LD A,(HL) ;код символа вертикальной рамки
CALL L8448 ;вывод вертикальной рамки по правой части окна
L8511 EX AF,AF'
CALL L8475 ;вывод атрибута для этой рамки
POP BC
JR L84F6 ;повторяем, пока не выведем всю вертикальную рамку
L8518 LD A,0 ;восстанавливаем оригинальный знакогенератор
LD (L8464),A
POP AF
RET
;Заплнение пикселов окна пробелом.
;Вход: AF' NC-очистка пикселов окна с атрибутами, CF-только атрибуты
;BC-координаты левого верхнего угла окна (XY)
;DE-координаты правого нижнего угла окна
;L-атрибут заполнения окна
L851F PUSH BC
L8520 EX AF,AF'
JR C,L8528 ;переход, если надо почистить только атрибуты
LD A,#20 ;чистим содержимое окна, заполняем пробелами
CALL L8448 ;печать пробела
L8528 EX AF,AF'
LD A,L ;атрибут заполнения
CALL L8475 ;выводим атрибут
INC B ;X-coord+1
LD A,B
CP D
JR NZ,L8520 ;повторяем пока не достигнем правой части окна
POP BC
INC C ;Y координата+1
LD A,C
CP E
JR NZ,L851F ;повторяем пока не достигнем низа окна
RET
;Печать списка игр начиная с адреса в указателе L8620. На экран помещается
;18 игр
L8539 LD HL,(L8620) ;адрес указателя на начало списка найденных игр
L853C LD BC,#0202 ;координаты для печати первой игры
L853F LD A,#0F ;атрибут PAPER=1 INK=7
LD D,#0D ;название игры 13 символов длиной
PUSH BC
CALL L8432 ;печатаем имя игры
;на выходе HL=HL+13
LD BC,7 ;пропускаем еще 7 байт до следующего описателя игры
ADD HL,BC ;(один описатель игры 20 байт длиной)
POP BC
INC C
INC C ;Y координата+2
LD A,C
CP 20 ;достигли низа окна?
JR NZ,L853F ;нет, повторяем печать
LD A,B ;мы печатали первую или вторую колонки с играми?
CP 17
JR Z,L855D ;вторая колонка, значит больше ничего не печатаем
LD BC,#1102 ;установим координаты для печати с начала второй колонки
JR L853F ;повторим печать колонки игр
L855D RET
;Сканирвание банков внутреннего ПЗУ приставки и картриджа на предмет наличия
;игр и занесение найденных описателей игр в буфер с адреса L868A
L855E PUSH AF
PUSH BC
PUSH DE
PUSH HL
LD DE,L868A ;адрес, где будут сохраняться
;найденные описатели игр (по 20 байт длиной каждый)
LD C,2 ;начинаем сканирование с банка №2
;(внутреннее ПЗУ, первый свободный банк)
L8567 PUSH BC
LD A,C
OUT (#5F),A ;включаем банк ПЗУ в раздел #0000-#3FFF
LD HL,0 ;адрес #0000
AND %00000111 ;проверка номера банка ПЗУ на кратность 8
LD A,(HL) ;содержимое байта по адресу #0000
JR NZ,L8585 ;если банк ПЗУ не кратен 8, проверяем его
;только на наличие байта #FF по адресу #0000
;банк ПЗУ кратен 8
;проверка на строку "COD" в начале банка
CP "C"
JR NZ,L8590 ;не сходится, переходим к проверка на байт "S"
INC HL
LD A,(HL)
CP "0" ;#4F
JR NZ,L85BA ;первый символ "C", а второй не "O", в этом
;банке нет описателей игр, поэтому проскакиваем
;на 8 банков вперёд
INC HL
LD A,(HL)
CP "D" ;проверка на "D"
JR NZ,L85BA ;первые символы "CO", а третий не "D", в этом банке
;нет описателей игр, поэтому проскакиваем на 8 банков вперёд
INC HL
LD A,(HL) ;вслед на "COD" должен быть символ #FF, тогда это
;будет расценено как признак наличия описателей игр
L8585 INC A ;проверка байта на #FF
JR NZ,L85A1 ;байт не #FF, описателей игр тут нет. Однако
;не проскакиваем не 8 банков вперед, а просто
;проверяем следующий по номеру банк.
;Найден признак наличия описателя игры в бекущем банке.
;Переносим 20 байт описателя в буфер описателей.
L8588 INC HL
L8589 LD BC,20
LDIR
JR L859D ;проверка на наличие следующего описателя вслед за текущим.
;Проверка первого байта банка ПЗУ на символ "S"
L8590 CP "S"
JR NZ,L85BA ;это не символ "S", проскакиваем на 8 банков вперёд
JR L8588 ;символ "S" найден, копируем 20 байт описателя
;игры в буфер
;Проверка на наличие описателя игры вслед за текущим описателем.
;Если следующий байт не равен #FF, считаем, что вслед идёт ещё один описатель игры.
L859D LD A,(HL) ;проверка следующего байта на #FF
INC A
JR NZ,L8589 ;байт не равен #FF, значит следом идёт
;ещё один описатель, копируем его в буфер.
;Найден байт #FF, это конец описателей игр в текущем банке.
;Переходим к следующему банку ПЗУ
L85A1 POP BC ;восстанавливаем номер текущего банка ПЗУ
INC C ;следующий банк ПЗУ
;Проверка - мы в ПЗУ картриджа или во встроенном ПЗУ?
BIT 7,C ;проверка идёт по биту 7 номера банка ПЗУ.
;Если там 1, то мы в ПЗУ картриджа
JR NZ,L85AD ;мы в ПЗУ картриджа
;Мы в основном ПЗУ приставки (в нём может быть не более 8 банков)
BIT 3,C ;мы уже перебрали все банки встроенного ПЗУ?
;Т.е. достигли ли банки №8?
JR Z,L8567 ;номер банки еще меньше 8, значит мы во встроенном ПЗУ,
;Все банки встроенного ПЗУ перебраны, перебираемся в ПЗУ картриджа
LD C,#80 ;Установленный бит 7 в номере банка ПЗУ
;означает, что мы работаем с ПЗУ картриджа
L85AD BIT 6,C ;Проверка перебрали ли 64 банка ПЗУ
;(в данном случае организован перебор только 64 банков ПЗУ)
JR Z,L8567 ;если еще не перебрали 64 банка ПЗУ,
;то проверяем очередной банк
;Перебор банков ПЗУ завершён
LD A,0 ;включаем обратно 0-й банк ПЗУ
OUT (#5F),A
POP HL ;восстанавливаем все регистры и выходим
POP BC
POP AF
RET
L85BA POP BC ;восстановим номер текущего сканируемого банка ПЗУ
LD A,C ;прибавим 8, т.е. проверяем не все банки,
ADD A,8 ;а каждый 8-й
LD C,A
JR L85AD ;проверка номера банка и сканирование
;Чистка буфера для найденных игр. Первые 18 записей заполняем пробелами,
;остальные заполняются значением #FF
;Чистятся записи для 64-х игр (#0500 байт)
L85C1 PUSH AF
PUSH BC
PUSH DE
PUSH HL
LD HL,L868A ;Чистка буфера на 64 игры.
LD DE,L868A+1 ;#0500 байт
PUSH HL
PUSH DE
LD A,#FF ;заполняем значением #FF
LD (HL),A
LD BC,#04FF
LDIR
POP DE
POP HL
LD A,#20 ;записи для первых 18 игр заполняем
LD (HL),A ;значением #20 (пробел)
LD BC,#0167 ;эти записи будут сразу отображаться на экране
LDIR ;поэтому их нельзя заполнять значением #FF
POP HL
POP DE
POP BC
POP AF
RET
;Вывод на экран текста описания игры начиная с байта в HL
L85E4 PUSH AF
PUSH BC
PUSH DE
PUSH HL
LD BC,#0202 ;Начальная координата вывода текста X=2 Y=2
L85EB LD D,#1C ;печатаем строками длиной 28 байт
LD A,6 ;описание игры выводим с цветом PAPER=0 INK=6
PUSH BC
CALL L8432 ;печать строки HL длиной 28 байт
POP BC
INC C ;Y координата+1
LD A,C
CP 20 ;если заполнили всё окно (высота окна 20 строк)
JR NZ,L85EB ;повторяем, если есть что печатать
POP HL ;восстанавливаем регистры и выходим
POP DE
POP BC
POP AF
RET
;Нажатие "вверх" при чтении описания игры.
;Сдвигаем текст вниз
L85FF INC C ;Y координата+1
LD A,C
CP #0B
JR Z,L860D ;если дальше текст сдвинуть нельзя, то выход
LD DE,#001C ;следующая строка описания (+28 байт)
ADD HL,DE
CALL L85E4 ;печать описания игры с нового адреса
RET
L860D DEC C ;возвращаем на место Y координату
RET
;Нажатие "вниз" при чтении описания игры
;Сдвигаем текст вверх
L860F DEC C ;Y координата-1
LD A,C ;Если текст вверх двигать уже нельзя, то выходим
OR A
JR Z,L861E
LD DE,#001C ;адрес текста уменьшаем на одну строку
XOR A ;(28 байт)
SBC HL,DE
CALL L85E4 ;печать описания игры с нового адреса
RET
L861E INC C ;Возвращаем Y координату на место
RET
L8620 DEFW 0 ;текущий адрес в буфере найденных описателей игр
DEFB 0,0,0,0,0,0,0,0,0
DEFB 0,0,0,0,0,0,0,0,0,0
DEFB #FF
;символы рамки окна - верхний левый угол, нижний левый угол,
;нижний правый угол, верхний правый угол, горизонтальная рамка,
;вертикальная рамка
;всего 6 символов
L8637 DEFB #22,#26,#27,#24,#23,#25
;символы пустой рамки окна - все пробелы
L863D DEFB #20,#20,#20,#20,#20,#20
;Коды символов рамки окна (6 символов по 8 байт)
L8643 DEFB #00,#00,#3F,#20,#2F,#28,#28,#28 ; ╔
DEFB #00,#00,#FF,#00,#FF,#00,#00,#00 ; ═
DEFB #00,#00,#F8,#08,#E8,#28,#28,#28 ; ╗
DEFB #28,#28,#28,#28,#28,#28,#28,#28 ; ║
DEFB #28,#28,#2F,#20,#3F,#00,#00,#00 ; ╚
DEFB #28,#28,#E8,#08,#F8,#00,#00,#00 ; ╝
;Все координаты ниже по умолчанию установлены для рамки окна выбора
;самой первой игры в меню
;Координаты тени окна выбора игры
L8673 DEFW #0202 ;верхний левый угол
L8675 DEFW #1004 ;нижний правый угол
;Координаты рамки окна выбора игры
L8677 DEFW #0101
L8679 DEFW #0F03
;Координаты тела окна выбора игры (внутри рамки)
L867B DEFW #0202
L867D DEFW #0F03
;Окно выбора игры с рамкой (для заполнения рамки пробелами)
L867F DEFW #0101
L8681 DEFW #0F03
;Большое окно, которое гарантированно "затирает" окно выбора игры с тенью
L8683 DEFW #0101
L8685 DEFW #1105
DEFB 0
DEFB 0
DEFB #FF ;IX+#13 этот байт используется для проверки достижения
;самой первой игры в меню при навигации по пунктам меню
;-------------------------
;конец блока данных, дальше расположены буфера под данные
;-------------------------
;Буфер для сохранения найденных описателей игр. Расчётная максимальная
;длина буфера - #0500 байт
;изначально записи для первых 18 игр заполняются пробелами,
;остальные - кодом #FF
L868A
;сюда переносится описание игры из ПЗУ
L8D20 EQU L868A+#0696
Схемы, исходники и прочие вкусности
Документация:
Схема приставки «Эльф» (редакция от 17.07.2023)Внимание! Схему составлял я сам, схема неполная - есть только цифровая часть схемы без SECAM-кодера и ВЧ-модулятора. Этого вполне достаточно для понимания принципов работы приставки. Оставшуюся часть схемы я составлять не намерен из-за сложности прозвонки соединений
Монтажный чертёж платы приставки «Эльф» (редакция от 18.03.2023)Чертёж неполный - есть только цифровая часть схемы без SECAM-кодера и ВЧ-модулятора.
Расширение памяти Спектрум-48 на ПЛМСтатья из журнала ZX-Ревю №2 за 1995 год.
Расширение памяти "новодельной" приставки Эльф до 128КВ архиве только схема платы расширения.
Прошивки ПЗУ
Прошивки оригинального и модифицированного (без защиты) ПЗУ приставки "Эльф" (ПЗУ 27C256) без встроенных игр
Прошивка модифицированного (без защиты) ПЗУ приставки "Эльф" (ПЗУ 27C010) с тремя играми
Прошивка DD24 К155РЕ3
Полный дизассемблер прошивки ПЗУ приставки «Эльф»В архиве - несколько вариантов исходников (текстовый вид и готовый проект для компиляции в среде iS-DOS/TASiS). Читаем файл readme.txt внутри архива.
Картриджи (схемы и прошивки)
Схема картриджа с вариантом платы №1В архиве два варианта схем с платой №1
Схема картриджа с вариантом платы №2
Прошивка ПЗУ картриджа "Эльф-1" (ПЗУ 256КБ)
Прошивка ПЗУ картриджа "Эльф-2" (ПЗУ 256КБ)
Прошивка ПЗУ картриджа "Эльф-2" (ПЗУ 256КБ) (ещё один вариант картриджа)
За предоставленный образ ПЗУ благодарю московский Клуб истории развития информатики и ЭВМ.
Исходник в виде trd-образа, прошивка для записи в ПЗУ 256К
Игра «METAL MAN Reloaded»Автор адаптации - krt17
Игра «Tetris 2»Внимательно читаем файл readme.txt внутри архива! Версия адаптации от 3.04.2016
Игра «l'Abbaye»Автор адаптации - Jerri
Игра «Target Renegade»Внимательно читаем файл readme.txt внутри архива! Версия адаптации от 23.01.2022
Игра «Level 5»Дата адаптации - 29.05.2022
Игра «Exolon»Дата адаптации - 2.06.2022
Игра «Dizzy-Y»Дата адаптации - 20.06.2022
Игра «Worsesea» («Беда в море»)Дата адаптации - 6.02.2023
Игра «Ms. PAC-MAN»Дата адаптации - 15.09.2024, автор адаптации - san0101010
Игра «Nether Earth» («Адская земля»)Дата адаптации - 15.10.2024
Игра «Bruce Lee» («Брюс Ли»)Дата адаптации - 12.11.2024
Игра «Star Raiders 2» («Звёздные рейдеры 2»)Дата адаптации - 14.11.2024
Картридж для приставки «Эльф» со всеми адаптированными и проверенными играми - рабочее наименование «Эльф-11» Требуемый объём ПЗУ - 1024K (общий объём адаптированных игр - 587 565 байт)
Корпусные детали приставки для распечатки на 3D-принтере
Нижняя крышка приставки В моей приставке была утеряна нижняя крышка, поэтому пришлось спроектировать и распечатать замену. Внимание: деталь не повторяет в точности оригинальную нижнюю крышку приставки!
Самодельный корпус для приставки «Эльф-01»Корпус не мой, мне прислали. Поэтому выкладываю как есть.
Новодельный универсальный картридж для приставки «Эльф»
Полный комплект документации для изготовления новодельного картриджа для приставки «Эльф»В архиве содержится схема картриджа, разводка платы, файлы для заказа изготовления платы, файлы деталей корпуса для печати на 3D принтере.
Новодельный универсальный джойстик для приставки «Эльф»
Полный комплект документации для изготовления новодельного джойстика для приставки «Эльф»В архиве содержится схема джойстика, фотографии, разводка платы, файлы для заказа изготовления платы, файлы деталей корпуса для печати на 3D принтере. На одной плате можно собрать один из двух вариантов джойстика.
Прочее
Логотип приставки в хорошем качестве Обрисовывал и делал векторизацию оригинального логотипа Igor Family из объединения Replycont (https://vk.com/replycont)
Использование текстовых, фото- и видеоматериалов сайта допускается только при условии указания ссылки на https://zxbyte.ru.
Есть вопросы, замечания, предложения по материалам сайта? Жмите сюда.