Spectrum-совместимые компьютеры «Байт»,«Байт-01» и прочие раритеты

1 / 1 065

Формат хранения программ на картриджах

Список разделов этой страницы:

  • Процедура сканирования содержимого ПЗУ приставки
  • Структура описателя игры
  • Полный дизассемблер ПЗУ приставки "Эльф"
  • В качестве источника информации этого раздела частично послужил форум 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К). Кроме того, конец блока не должен находиться "впритык" к концу банка памяти, т.к. после окончания блока данных потребуется немного места. А для чего оно надо, описано в следующем пункте:
  • Execute code - исполняемый код. Для чего он нужен? А вот для чего. Логика работы приставки при выборе какой-либо программы из меню такова: берём запись меню, определяем в каком банке памяти находится первый (или единственный) блок программы, определяем по какому адресу в ПЗУ он находится, куда его надо перенести, после этого блок переносится в ОЗУ и управление передаётся на первый байт за концом блока данных в ПЗУ, т.е. в область Execute Code. Почему так? Дело в том, что в описателе меню никак не задано каким образом будет запускаться на исполнение выбранная программа, кроме того программа может содержать более чем один блок, а это в описателе меню никоим образом не отражено. Поэтому после обработки одного блока приставка отдаёт управление, и как дальше будет запускаться наша программа - это уже наша забота. Поэтому предполагается наличие некой "запускалки" программы в виде Execute code.
  • И что же делать с этим Execute Code? Возьмём, к примеру, некую программу, состоящую из одного блока объёмом менее 16К, чтобы он поместился в один банк ПЗУ. Допустим, что наша программа должна быть запущена с адреса #6000. Что же нам потребуется? Просто так запустить программу командой JP #6000 не получится! Почему? А потому, что мы ещё находимся в ПЗУ картриджа, и ПЗУ с BASIC-48 не включено. Если программа требует наличия стандартного ПЗУ ZX-Spectrum, то надо сначала включить требуемую банку ПЗУ и лишь потом переходить к запуску программы.

    Находясь в ПЗУ картриджа мы тоже не можем дать команду на переключение ПЗУ, т.к. в момент переключения банков ПЗУ мы окажемся неизвестно где. Нужно, чтобы переключалка ПЗУ находилась в области ОЗУ. И это уже сделано! В ОЗУ по адресу #4000 уже находитсяПодарок от разработчиков прошивки для "Эльфа". Они всё предусмотрели :) кусок кода:

    OUT (#5F),A
    LDIR
    JP (HL)
    

    Как раз всё, что нам требуется! Пишем следующий код:

    LD A,1        ;включаем банк ПЗУ с BASIC-48
    LD HL,0       ;затираем команду LDIR
    LD (#4002),HL
    LD H,#60      ;#6000 - стартовый адрес нашей программы
    JP #4000      ;переход на #4000
    

    Как вариант, для загрузки следующего блока игры я встречал такой код:

    #3FF5 LD A,bank
          LD HL,#0001
          LD DE,addr
          LD BC,length
    

    Адрес расположения кода в банке памяти специально подобран так, чтобы он начинался с #3FF5, тогда после выполнения последней команды выполнение перейдёт на адрес #4000, где банк памяти переключится на значение из регистра A, и следующий блок программы будет перенесён с адреса #0001Напомню, что 0-й байт банки памяти не может быть использован для хранения данных, поэтому мы его пропускаем. из банка в память компьютера. Дальше, после выполнения переноса управление будет передано на следующий байт за концом блока.

    Если ёмкости этого банка памяти будет недостаточно для хранения программы, то в конце банка ПЗУ по адресу #3FF5 размещаем аналогичный кусок кода, только с поправленными значениям банки памяти и адресов, и так далее :)

    Важный момент! Никакие системные переменные BASIC-48 при старте приставки не инициализированы! Можно так сказать память девственно чиста. Прерывания запрещены. Указатель стека SP=#0000. Некоторые программы требуют уже проинициализированных переменных BASIC. Для такого случая можно хранить заранее подготовленный кусок области ОЗУ от адреса #5C00, где находятся нужные там системные переменные, и копировать этот код вместе с блоком данных из ПЗУ в ОЗУ.

    Думаю, тут понятно. Однако как быть, если программа не помещается в 16К? Ничего сложного тут нет. Программа разбивается на несколько блоков, которые хранятся каждый в своём банке ПЗУ картриджа. Первый блок обрабатывается так, как уже описано ранее. Остальные блоки хранятся в несколько упрощённом виде:

    Формат банка ПЗУ с меню

    Все заботы по копированию второго и последующих блоков возложены на пользователя. Для этих блоков не требуется никаких заморочек с меню. Главное, чтобы самый первый байт банка ПЗУ не был равен #FF (обычно туда пишут 0). Остальной объём банка ПЗУ отдаётся блоку с данными. В обработчик Execute Code потребуется включить код для включения ПЗУ со следующим блоком, его копирования в память и т.п.

    В качестве работающего примера можно посмотреть исходник картриджа с тестами для приставки "Эльф".

    Полный дизассемблер ПЗУ приставки "Эльф"

    Имеется в виду банк ПЗУ с номером 0, в котором содержится загрузочное меню с поиском игр на картридже. Этот банк состоит как бы из двух частей: в одной части по старту приставки делаются подготовительные операции (перенос знакогенератора и исполняемого кода в память), потом стартует вторая часть, которая собственно и является меню.

    В этом разделе я поместил исходный код этих двух частей. Кроме этого в ПЗУ содержится неиспользуемый код. О его происхождении можно только догадываться. Это обработчик прерывания IM1При том факте, что в меню приставки прерывания не используются вообще. и, по-видимому, остатки какой-то защиты, т.к. среди них есть процедура вывода сообщения "НЕ УКРАДИ!!!". При вызове этой процедуры по центру экрана выводится сообщение и раздаются щелчки из динамика компьютера:

    Вполне возможно, меню приставки кто-то ломал или переделывал. В пользу этой версии говорит наличие занулённых байтов в процедурах ПЗУ.

    Полный дизассемблер ПЗУ можно скачать одним архивом в конце этой страницы.

    Первая часть:

          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
    
          NOP
          NOP
          NOP
          NOP
          NOP
          NOP
          NOP
          NOP
          NOP
    
    ;Опрос Джойстика-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      "
    
    ;Опять неиспользуемый код
    ;#8184
          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-кодера и ВЧ-модулятора.
    Паспорт к игровой приставке «Эльф»
    Паспорт к манипулятору игровому (джойстик)
    Дополнительная периферия
    Адаптер для чтения картриджей приставки "Эльф" на ZX-SpectrumСхема, программа для чтения содержимого ПЗУ картриджей
    Схема картриджа с музыкальным сопроцессоромДля нормального функционирования музыкального сопроцессора потребуется некоторая доработка платы приставки.
    Расширение ОЗУ до 128К
    Расширение памяти Спектрум-48 на ПЛМСтатья из журнала ZX-Ревю №2 за 1995 год.
    Расширение памяти "новодельной" приставки Эльф до 128КВ архиве только схема платы расширения.
    Прошивки ПЗУ
    Прошивка оригинального ПЗУ приставки "Эльф" (ПЗУ 27C256) без встроенных игр
    Прошивка оригинального ПЗУ приставки "Эльф" (ПЗУ 27C010) с тремя играми
    Прошивка DD24 К155РЕ3
    Полный дизассемблер прошивки ПЗУ приставки «Эльф»В архиве - несколько вариантов исходников (текстовый вид и готовый проект для компиляции в среде iS-DOS/TASiS). Читаем файл readme.txt внутри архива.
    Картриджи (схемы и прошивки)
    Схема картриджа с вариантом платы №1В архиве два варианта схем с платой №1
    Схема картриджа с вариантом платы №2
    Прошивка ПЗУ картриджа "Эльф-1" (ПЗУ 256КБ)
    Прошивка ПЗУ картриджа "Эльф-2" (ПЗУ 256КБ)
    Прошивка ПЗУ картриджа "Эльф-2" (ПЗУ 256КБ) (ещё один вариант картриджа)

    За предоставленный образ ПЗУ благодарю московский Клуб истории развития информатики и ЭВМ.

    Прошивка ПЗУ картриджа "Эльф-3" (ПЗУ 256КБ)
    Прошивка ПЗУ картриджа "Эльф 1-4" (ПЗУ 1024КБ)
    Прошивка ПЗУ картриджа "Эльф-4" (ПЗУ 256КБ)
    Прошивка ПЗУ картриджа "Эльф-5" (ПЗУ 256КБ)
    Прошивка ПЗУ картриджа "Эльф-6" (ПЗУ 256КБ)
    Прошивка ПЗУ картриджа "Эльф-7" (ПЗУ 256КБ)
    Прошивка ПЗУ картриджа "Эльф-8" (ПЗУ 256КБ)
    Прошивка ПЗУ картриджа "Эльф-9" (ПЗУ 256КБ)
    Прошивка ПЗУ картриджа "Эльф-10" (ПЗУ 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
    Картридж для «Эльфа» со всеми адаптированными мной играми
    Требуемый объём ПЗУ/Flash - 512K
    Корпусные детали приставки для распечатки на 3D-принтере
    Нижняя крышка приставки
    В моей приставке была утеряна нижняя крышка, поэтому пришлось спроектировать и распечатать замену. Внимание: деталь не повторяет в точности оригинальную нижнюю крышку приставки!
    Самодельный корпус для приставки «Эльф-01»Корпус не мой, мне прислали. Поэтому выкладываю как есть.
    Новодельный универсальный картридж для приставки «Эльф»
    Полный комплект документации для изготовления новодельного картриджа для приставки «Эльф»В архиве содержится схема картриджа, разводка платы, файлы для заказа изготовления платы, файлы деталей корпуса для печати на 3D принтере.
    Прочее
    Логотип приставки в хорошем качестве
    Обрисовывал и делал векторизацию оригинального логотипа Igor Family из объединения Replycont (https://vk.com/replycont)