Связь с нами
Почтовый ящик
 
Обновлённые темы форума
Очень низкоуровневый эмулятор 6502 / NES - [16:27] 14.06.2022
Сайт всё? - [07:51] 08.05.2022
Журнал Downgrade - [22:57] 18.04.2022
Что случилось с ромами на сайте? Куда все пропало? - [10:40] 10.01.2022
Merger для дисковых систем (CD DVD Merger) - [21:39] 05.12.2021
Экологическая катастрофа в Ташкенте - [12:46] 11.11.2021
msxDS 0.8 - [07:45] 17.09.2021
Why was the site deactivated? - [22:19] 23.08.2021
Организация на GitHub - [17:26] 10.08.2021
Twisted Metal III hacking - [21:21] 04.08.2021
 
Статьи - Извлечение и использование ресурсов музыкального сопровождения из игр Sega MegaDrive
Процесс извлечения
Для начала проверим работоспособность ROM файла, открыв его эмулятором. После проверки, идем в меню эмулятора Gens [Processor] => [Debug] => [Genesis - 68000]. Нажимаем [Tab] на клавиатуре и начинаем трассировать программу М68К. Управление простое: [T] шаг на одну инструкцию, [Y] шаг на 10 инструкций, [U] шаг на 100 и [I] шаг на 1000 инструкций. [N] пропуск одного слова без исполнения (можно пропускать некоторые команды, нажимая [N] пока нужная инструкция не будет первой). Итак, шагаем по одной, по пути изучаем код инициализации. Обычно,  сначала идет разблокировка аппаратуры (если этого не сделать, то приставка через некоторое время заблокируется). При достижении адреса $000320 появляется то, что надо изучить подробнее:
000320   MOVEM.L              D0,-(A7)         ; Сохранить d0 в стеке
000324   MOVE.W               #$0000,($A11200) ; Активировать сброс Z80
00032C   ROXR.B               #0,D0            ; Сдвиг d0 – используется для задержки
00032E   ROXR.B               #0,D0            ; Сдвиг d0 – используется для задержки
000330   MOVE.W               #$0100,($A11200) ; Деактивировать сброс Z80
000338   MOVEM.L              (A7)+,D0         ; Восстановить d0
00033C   RTS                                   ; Выйти
 
Адрес $A11200 управляет сбросом ведомого процессора Z80. Значит, $000320 – подпрограмма сброса Z80. Трассируем дальше. При достижении адреса $00030C открывается такая картина:
00030C   MOVE.W               #$0100,($A11100) ; Захват шин Z80
000314   RTS                                   ; Выход
000316   MOVE.W               #$0000,($A11100) ; Освобождение шин Z80
00031E   RTS                                   ; Выход
 
Итак, $A11100 – адрес захвата шин у Z80. И $00030C – захватывает шины, а $000316 – освобождает их. Напомню, что при обращении к ресурсам Z80 необходимо захватывать шину. Это очень важная для нас информация, т.к. мы будем извлекать из игры именно звуковые ресурсы. Наконец, нам повезло. Это первый, имеющий для нас значение, код:
02752C   JSR                  ($00030C)        ; Захват шин процессора
027532   LEA                  ($A00000),A0     ; Загрузим адрес ($A00000) в [A0]
027538   LEA                  ($07DCE4),A1     ; Место, где хранится сам драйвер. Запомним
02753E   MOVE.W               #$1000,D1        ; Размер – 4кБ
027542   MOVE.B               (A1)+,(A0)+      ; Пересылаем байт
027544   SUBQ.W               #1,D1            ; Уменьшить длину
027546   BNE                  #$FA             ; Повторить пока не 0
027548   LEA                  ($A01000),A0     ; Загрузим адрес ($A01000) в [A0]
02754E   LEA                  ($07EBE6),A1     ; Место, где хранится вторая часть драйвера. Запомним
027554   MOVE.W               #$1000,D1        ; Размер – 4кБ
027558   MOVE.B               (A1)+,(A0)+      ; Пересылаем байт
02755A   SUBQ.W               #1,D1            ; Уменьшить длину
02755C   BNE                  #$FA             ; Повторить пока не 0
02755E   LEA                  ($A01C00),A0     ; Загрузим адрес ($A01000) в [A0]
027564   LEA                  ($027582),A1     ; Место, где хранится третья часть драйвера. Запомним
02756A   MOVE.W               #$000A,D1        ; Размер – 10 Б
02756E   MOVE.B               (A1)+,(A0)+      ; Пересылаем байт
027570   SUBQ.W               #1,D1            ; Уменьшить длину
027572   BNE                  #$FA             ; Повторить пока не 0
027574   JSR                  ($000316)        ; Освободить шины Z80
02757A   JSR                  ($000320)        ; Сбросить Z80
027580   RTS                                   ; Выход
 
Это код, загружающий драйвер Z80. Теперь мы знаем его расположение в ROM файле. Непонятно, зачем разработчики игры разделили его на 3 части, видимо были свои причины. Используя Hex – редактор можно собрать все 3 куска воедино. Первая часть находится по адресам $07DCE4…$07ECE3, вторая по адресам $07EBE6…$07FBE5 и последняя часть по адресам $027582…$02758B. Первые два куска идут друг за другом и дополняют друг друга до 8 КБ. Создадим пустой файл на 8кБ и вставим первый кусок с адреса $0000, второй с адреса $1000 и третий кусок перепишем поверх с адреса $1C00. Потом сохраним все 8кБ как файл “driver.dat”. Первый и самый главный ресурс получен – это программа-драйвер для Z80, которая управляет звуковыми чипами. Результатом этой программы будет комплексное звуковое сопровождение. Теперь, надо дизассемблировать полученный код и посмотреть, какие банки ресурсов М68К он использует. Для этой цели воспользуемся дизассемблером для Z80. Например, Xdismic.
.ORG     0000H

0000    F3       LL0000: DI                              ; Запрет прерываний
0001    F3               DI                              ; Запрет прерываний
0002    ED 56            IM        1                     ; Режим прерываний 1
0004    18 58    LL0004: JR        LL005E                ; Переход на $005E
***
;--------------------------------
005E    31 FD 1F LL005E: LD        SP,DL1FFD             ; Установка вершины стека
0061    3E 03            LD        A,03H                 ; Запись числа “03”
0063    32 FF 1F         LD        (DL1FFF),A            ; по адресу $1FFF
0066    FB       LL0066: EI                              ; Разрешение прерываний
0067    3A FF 1F         LD        A,(DL1FFF)            ; Читаем число по адресу $1FFF
006A    B7               OR        A                     ; Установка флагов по числу
006B    C2 66 00         JP        NZ,LL0066             ; Если еще не “0”, то крутимся в цикле
006E    F3               DI                              ; Запрет прерываний
006F    CD 73 08         CALL      PL0873                ; Инициализация памяти мелодии
0072    CD 9B 07         CALL      PL079B                ; Установка банка
***
079B    3A 01 1C PL079B: LD        A,(DL1C01)            ; Загрузить число из $1C01
079E    07               RLCA                            ; Сдвинуть влево (D7 => D0)
079F    32 00 60 LL079F: LD        (EL6000),A            ; Выставить в порт $6000 (младший из 9ти бит)
07A2    06 08            LD        B,08H                 ; Счетчик – 8 бит
07A4    3A 00 1C         LD        A,(DL1C00)            ; Взять число из $1C00
07A7    32 00 60 LL07A7: LD        (EL6000),A            ; Выставить в порт $6000 (имеет значение только бит D0)
07AA    0F               RRCA                            ; Сдвинуть вправо на 1 бит
07AB    10 FA            DJNZ      LL07A7                ; Повторять 8 раз
07AD    C9               RET                             ; Выход
 
Вот, подпрограмма $079B из ячеек $1С01 и $1С00 берет адрес банка и, согласно данным из [1], побитно записывает их в порт $6000. Для начала нам нужно выяснить, что там хранится. Возвращаемся в Hex – редактор и смотрим по адресу $1C00. Там записано $07:$80. Из этого слова используются только старшие 9 бит, поэтому результирующий адрес банка ресурсов М68К для Z80 после выполнения этой процедуры будет такой: $078000…$07FFFF. Как раз конец ROM файла – хранение мелодий в конце практикуется очень часто. Выделим этот диапазон адресов в ROM файле, скопируем его и сохраним как “music.dat”.
Итак, мы уже имеем все необходимые ресурсы, но нужно научиться пользоваться  драйвером Z80. Понятно, что самопроизвольно он ничего не сделает. Ему дает команды М68К. Поэтому продолжим изучать код игры дальше. Очевидно, что М68К будет просто записывать своеобразный запрос в память Z80. Т.е. он будет захватывать шины Z80. Соответственно, в простейшем случае нам нужно, чтобы эмулятор останавливался при достижении адреса $00030C. Напомню, что по этому адресу находится подпрограмма захвата шин Z80. Не все эмуляторы способны задавать точку останова, некоторые вообще не имеют встроенного отладчика. Поэтому, я предлагаю более универсальный способ: мы просто пишем патч-код (“патч” от английского “patch” - заплатка) в меню Game Genie эмулятора на этот адрес. Код будет выглядеть так: 00030C:60FE. Сначала идет полный 6ти разрядный адрес, затем код команды М68К. Замечу, что архитектура М68К не позволяет расположение команд по нечетным адресам и все команды кратны словам (16 битам). И если команде требуется всего 1 бит данных, она использует целое слово (16 бит). В коде указан код команды BSR -2, что обеспечивает бесконечный цикл. Загружаем ROM файл в эмулятор, вводим код в меню эмулятора [File] => [Game Genie] и делаем сброс кнопкой [Tab]. Затем идем в отладчик М68К (как это сделать было сказано выше) и смотрим. Начинаем трассировать (для начала надо пропустить наш патч, нажав [N]) до команды RTS и смотрим: $00036A – ну, здесь мы уже были, жмем [ESC] и повторяем действия. $027532 – здесь мы тоже были, повторяем. Первый останов был при инициализации, второй при загрузке драйвера. А вот третий останов:
027592  MOVE.B D0,($A01C0A)       ; Записать байт по адресу $A01C0A
027598  JSR    ($000316)          ; Освободить шины
02759E  RTS                       ; Вернуться
 
Надо найти точку входа в эту процедуру, для этого смотрим в стек. Известно, что при вызове подпрограммы адрес возврата (т.е. адрес инструкции, следующей сразу за процедурой вызова подпрограммы) сохраняется в стеке. Указателем на стек у М68К является регистр [A7]. Смотрим его текущее значение и кнопками [W], [E], [R] и [S], [D], [F] в окне “Main 68000 MEM” выставляем адрес, близкий к значению в регистре [A7]. Замечу, что у М68К “тупоконечный” принцип хранения данных, поэтому по адресу памяти [A7]=$FFFFEC => $000003A0. Т.е. адрес возврата будет $0003A0. Это адрес следующей команды, поэтому открываем Hex – редактор и идем по этому адресу. Теперь обращаем внимание на 6 байт перед этим адресом: $4EB90002758С. Это код команды JSR ($02758C). Что нам и надо. Пишем патч-код на этот адрес 02758C:60FE (как мы это делали для $00030C) и делаем сброс. Идем в отладчик и отключаем этот патч-код. Смотрим на результат.
02758С  JSR    ($00030С)          ; Захват шин Z80
027592  MOVE.B D0,($A01C0A)       ; Выставить байт по адресу $A01C0A
027598  JSR    ($000316)          ; Освободить шины Z80
02759E  RTS                       ; Вернуться
 
Понятно, что этот код захватывает шины и пишет байт по адресу $A01C0A (для Z80 это $1C0A). Теперь, надо выяснить, кто вызывает этот код. Используя патч-код на адрес $02758C, смотрим, кто сюда обращается. Технология была описана выше (в дебагере пропускаем нашу команду кнопкой [N], [ESC] и снова в отладчик). Первый раз нам ничего не дал, после [ESC] появляется "SEGA" и потом заставка с неверной палитрой. Игра остановилась. Здесь должна была появиться музыка заставки. Смотрим что в [D0] и трассируем до команды RTS. Шагнув еще раз, видим адрес $025F26. Смотрим в стек по адресу в [A7] и видим адрес $0003A6. В Hex – редакторе смотрим на 6 байт ранее и видим $4EB9025E6C, т.е. JSR ($025E6C). Пишем код адрес 025E6C:60FE, перезапускаем игру и возвращаемся в отладчик. Потом “пролистываем” кнопкой [N] до адреса $025F1C и видим, что число $81 заносится в [D0] и вызывается наша процедура. Понятно, что это только вывод “заставочной” мелодии, поэтому снимаем все патч-коды, оставив только на адрес $02758C. И начинаем отслеживать обращения до начала игрового процесса. Сначала активируем патч-код, и делаем сброс клавишей [Tab]. Первый останов холостой, пропускаем его в отладчике и возвращаемся в игру клавишей [Esc]. Второй – заставка, но мы ее уже изучили, поэтому пропускаем ее. Третий – мелодия при обращении Dark Queen. Код, аналогичный тому, что задает мелодию заставки, поэтому нам не интересен. Пропускаем. Наконец, четвертый останов это обращение из игры. Смотрим стек и видим $00056С. Трассируем до этого адреса. Стек пуст, значит это главный код. Поэтому открываем Hex – редактор и смотрим на 6 байт раньше. По адресу $000566 наш вызов JSR ($02758C). На одну строчку выше, видим код $4EBA00000556 – это вызов подпрограммы, пишем патч-код 000556:60FE на этот адрес и отключаем все остальные. Перезапускаем игру, нажимаем на кнопки джойстика до остановки игры и выходим в отладчик:
000558  MOVE.W ($FFDF04),D0        ; Загрузим число в [D0]
00055C  LEA    ($0275A0),A0        ; Загрузим начало таблицы в [A0]
000562  MOVE.B $00(A0,D0),D0       ; Возьмем из таблицы байт с номером в [D0].
000566  JSR    ($02758C)           ; И отдадим его драйверу Z80.
 
Да, это то, что мы ищем. Смотрим по адресу $0275A0 в Hex – редакторе и видим таблицу:
0275A0 80 87 83 84 86 87 88 89 8B 8B 8C 8D 8E 8F 8F 88
0275B0 88 88 FF FF FF FF FF FF FF FF FF FF FF FF FF FF
 
Понятно, что номера мелодий начинаются с $80 и по $8F. $81 мы узнали что это заставка, поэтому напишем патч, подменяющий первые 2 байта таблицы: 0275A0:8081. Остальные коды отключаем.
Перезапускаем игру и запускаем 1 уровень. Действительно, на первом уровне игры играет заставочная мелодия.
Все готово: получили драйвер Z80 и его ресурсы из игры и знаем, что если записать значение между $80..$8F в ячейку $A01C0A (для Z80 - $1C0A), то будет играть какая-нибуть мелодия. Можно поэкспериментировать, подменяя разные коды мелодий. Код $80 ничего не делает, коды выше $93 выключают мелодию. Коды с номерами $90..$92 относятся к служебным: мелодии перехода уровня, конца игры и босса. Дальше попробуем написать простейший PD ROM, использующий полученные данные.
Пишем PD ROM.
Что такое “PD ROM”? Это словосочетание образовано от “Public Domain” и “Read Only Memory”. Т.е. по сути это игра или программа от приставки для общего использования и не требующая лицензии. Как правило, такие программы не имеют лицензию у производителя самой приставки.
Итак, мы уже имеем драйвер Z80 “driver.dat”, мелодии “music.dat” и информацию об управлении этим драйвером. Теперь, можно приступить к написанию PD ROMа.
Изначально, в приставке Sega MegaDrive 1 была встроена загрузочная программа, которая предназначалась для борьбы с пиратством: она определяла регион приставки и установленной игры и, если они отличались, отказывалась игру запускать. Для этого в игре был предусмотрен специальный заголовок. Если он был заполнен неверно, встроенная программа так же отказывалась запускать игру. В поздних моделях MegaDrive 2 и в моделях MegaDrive 3 эта программа отсутствует, однако регистр безопасности остался, поэтому каждая игра должна придерживаться определенного “ритуала” запуска. Для начала, разберем обязательный заголовок. Он состоит из следующих областей:
000000 – 0000FF Область векторов прерываний и исключений.
000100 – 00011F ASCII строка о правах игровую систему, обычно здесь занесена надпись типа “SEGA MEGA DRIVE  (C) SEGA 1988.JUL”. Однако, согласно закону об авторских правах, использовать эту надпись здесь (прочем, как и производство игр для Sega MegaDrive) без лицензирования у компании Sega противозаконно. Но, без этой надписи игра не запустится на приставках со встроенной программой проверки легальности игры, поэтому нам придется ее использовать для поддержки совместимости.
000120 – 00014F ASCII строка “домашнего” (DOMESTIC) названия игры.
000150 – 00017F ASCII строка “зарубежного” (OVERSEAS) названия игры.
000180 – 00018D ASCII строка кода, определяющего тип программы, код производителя и версия программы. Для игр она имеет вид “GM XXXXXXX-XX”, где “X” – это цифры в ASCII. Причем, “GM” видимо означает “Game”, т.е. игру. Для PD ROMа строка может иметь вид “PD XXXXXXX-XX”.
00018E – 00018F Контрольная сумма программы в картридже.
000190 – 00019F ASCII строка используемых программой аппаратных средств, которая состоит из следующих букв: “D” – джойстик Sega Master System, “J” – джойстик Sega MegaDrive, “K” – клавиатура, “R” – контроллер последовательного интерфейса, “P” – принтер, “F” – контроллер дисковода и “C” – привод CD-ROM.
0001A0 – 0001A7 Два двойных слова, определяющие расположение ROMа в адресном пространстве. Первое всегда равно 00000000, а второе содержит адрес последней ячейки.
0001A8 – 0001AF Два двойных слова, определяющие адреса используемого ОЗУ. В первом всегда 00FF0000, а во втором 00FFFFFF.
0001B0 – 0001BB Строка кодов используемого дополнительного ОЗУ:

Адрес

Значение

0001B0

            ASCII символ “R”

0001B1

            ASCII символ “A”

0001B2

D7

Должен быть 1

D6

Если ОЗУ защищено батареей, то он равен 1, иначе - 0.

D5

Должен быть 1

D4 – D3

00 – ОЗУ доступно по четным и нечетным адресам

01 – Код не используется

10 – ОЗУ доступно только по нечетным адресам

11 – ОЗУ доступно только по четным адресам

D2 – D0

Должны быть 0

0001B3

ASCII символ “ “ (20h)

0001B4

Двойное слово, адрес первой ячейки ОЗУ

0001B8

Двойное слово, адрес последней ячейки ОЗУ

0001BC – 0001C7 ASCII строка параметров используемого модема. Если модем не используется – то строка заполняется пробелом (20h).
0001C8 – 0001EF Область не используется, должна быть заполнена кодом пробела (20h).
0001F0 – 0001FF ASCII строка кодов стран, для которых предназначена программа: “U” – США, “J” – Япония и “E” – Европа.
000200 – XXXXXX Собственно код программы.
программа так же отказывалась запускать игру. В поздних моделях MegaDrive 2 и в моделях MegaDrive 3 эта программа отсутствует, однако регистр безопасности остался, поэтому каждая игра должна придерживаться определенного “ритуала” запуска. Для начала, разберем обязательный заголовок. Он состоит из следующих областей:
 
Все неиспользуемые байты в ASCII строках должны быть заполнены кодом пробела (20h). Все неиспользуемые байты остальных структур должны быть заполнены кодом 00h. Микропроцессор М68К использует “тупоконечный” метод хранения чисел, т.е. слово будет начинаться со старшего байта и заканчиваться младшим. Двойное слово будет начинаться со старшего слова, а заканчиваться младшим словом.
Область векторов содержит 64 двойных слова. Из них, особый интерес нам представляют следующие:
000000            Здесь хранится адрес вершины стека при начальном запуске.
000004            Здесь хранится адрес точки входа при начальном старте и сбросе.
000078            Здесь хранится адрес точки входа в прерывание по кадровому прерыванию.
Sega Assembler сам расставит эти вектора, поэтому нет надобности их заполнять. Для этого в начале кода надо поставить директиву “START”, а в начале подпрограммы обработки кадрового прерывания поставить директиву “VBL”. Однако место для векторов все равно необходимо зарезервировать.
Теперь поговорим о начальной инициализации. В типичном виде код будет выглядеть так:
START: move.l sp,a7
move.w #$2700,sr
tst.l $a10008.l
bne.s SkipJoyDetect
tst.w $a1000c.l
SkipJoyDetect:
Bne SkipSetup
lea Table(pc),a5
movem.w (a5)+,d5-d7
movem.l (a5)+,a0-a4
move.b -$10ff(a1),d0 ;Номер версии
andi.b #$0f,d0
beq.s WrongVersion
move.l #$53454741,$2f00(a1) ;Код безопастности (SEGA)
WrongVersion:
move.w (a4),d0
moveq #$00,d0
movea.l d0,a6
move a6,usp
moveq #$17,d1 ;Установка регистров VDP
FillLoop:
move.b (a5)+,d5
move.w d5,(a4)
add.w d7,d5
dbra d1,FillLoop
move.l (a5)+,(a4)
move.w d0,(a3)
move.w d7,(a1)
move.w d7,(a2)
L0250: btst d0,(a1)
bne.s L0250
moveq #$25,d2 ;Инициaлизация A00000
Filla: move.b (a5)+,(a0)+
dbra d2,Filla
move.w d0,(a2)
move.w d0,(a1)
move.w d7,(a2)
L0262: move.l d0,-(a6) ;Обнулить ОЗУ
dbra d6,L0262
move.l (a5)+,(a4)
move.l (a5)+,(a4)
moveq #$1f,d3 ;Инициализация C00000
Filc0: move.l d0,(a3)
dbra d3,Filc0
move.l (a5)+,(a4)
moveq #$13,d4 ;Инициализация C00000
Fillc1: move.l d0,(a3)
dbra d4,Fillc1
moveq #$03,d5 ;Инициализация C00011
Fillc2: move.b (a5)+,$0011(a3)
dbra d5,Fillc2
move.w d0,(a2)
movem.l (a6),d0-d7/a0-a6
move #$2700,sr
tst.w $00C00004
jmp SkipSetup
Table: dc.w $8000,$3fff,$0100,$00a0,$0000,$00a1,$1100,$00a1
dc.w $1200,$00c0,$0000,$00c0,$0004,$0414,$302c,$0754
dc.w $0000,$0000,$0000,$812b,$0001,$0100,$00ff,$ff00
dc.w $0080,$4000,$0080,$af01,$d91f,$1127,$0021,$2600
dc.w $f977,$edb0,$dde1,$fde1,$ed47,$ed4f,$d1e1,$f108
dc.w $d9c1,$d1e1,$f1f9,$f3ed,$5636,$e9e9,$8104,$8f01
dc.w $c000,$0000,$4000,$0010,$9fbf,$dfff
SkipSetup: ;Дальше идет код программы
 
Этот код сначала определяет, нужно ли инициализировать аппаратуру приставки или нет (если произошел сброс, то инициализация не требуется), если инициализация требуется, то сначала программа получает код версии платы, затем заносит ключевое слово в регистр безопасности. Если этого не сделать, то приставка через некоторое время блокируется и программа остановится. Кроме того, эта программа устанавливает начальные значения регистров VDP и обнуляет системное ОЗУ. Это позволяет предотвратить “неопределенные” состояния в программе.
Алгоритм результирующей программы может быть такой:
Алгоритм работы программы
А по кадровым прерываниям мы будем опрашивать джойстик и обновлять положение стрелки в меню. Пришло время написать соответствующие блоки кода. С инициализацией все понятно, теперь загрузка драйвера:
move.w       #$0100,$A11100      ;Захват шин Z80
move.w       #$0000,$A11200      ;Активация сброса Z80
move.w       #$0100,$A11200      ;Деактивация сброса Z80
lea          Z80Driver,a0 ;Загрузка адреса хранения драйвера Z80
move.l       #$A00000,a1  ;Загрузка адреса “посадки” драйвера Z80
move.w       #$2000,d0    ;Размер драйвера Z80
LoadCopy:
move.b       (a0)+,(a1)+  ;Пересылка байта
subq.w       #1,d0        ;Уменьшение длины
bne          LoadCopy     ;Если не конец - повторяем
move.w       #0,$A11100   ;Освобождаем шины Z80
 
С выводом списка все немного сложнее. Дело в том, чтобы на экране получить текст, надо сначала загрузить палитру цветов, затем загрузить сами символы (знакогенератор), и в последнюю очередь в выбранную плоскость занести коды символов. Эти коды будут составлять текст. Если применить стандартную кодировку (например, cp866), то коды символов можно представить в виде текста, что упростит понимание исходного кода. Данный блок нужно разделить на 3 составляющие:
;Установка палитры (стандартная радуга 16ти цветных полос в первой палитре)
move.l       #$c0000000,$c00004  ; Установка режима записи в ОЗУ палитры VDP
move.l       #$0000,$c00000             ; Запись цвета #1 (черный)
move.l       #$0000,$c00000             ; Запись цвета #2 (черный)
move.l       #$0C00,$c00000             ; Запись цвета #3 (синий)
move.l       #$000C,$c00000             ; Запись цвета #4 (красный)
move.l       #$0C0C,$c00000             ; Запись цвета #5 (фиолетовый)
move.l       #$00C0,$c00000             ; Запись цвета #6 (зеленый)
move.l       #$0CC0,$c00000             ; Запись цвета #7 (голубой)
move.l       #$00CC,$c00000             ; Запись цвета #8 (желтый)
move.l       #$0CCC,$c00000             ; Запись цвета #9 (серый)
move.l       #$0E44,$c00000             ; Запись цвета #10 (светло синий)
move.l       #$044E,$c00000             ; Запись цвета #11 (светло красный)
move.l       #$0E4E,$c00000             ; Запись цвета #12 (светло фиолетовый)
move.l       #$04E4,$c00000             ; Запись цвета #13 (светло зеленый)
move.l       #$0EE4,$c00000             ; Запись цвета #14 (светло голубой)
move.l       #$04EE,$c00000             ; Запись цвета #15 (светло желтый)
move.l       #$0EEE,$c00000             ; Запись цвета #16 (белый)
       ;Загрузка знакогенератора
move.w       #$8f02,$c00004             ;Инкремент записи в видеопамять =2 (словами)
move.l       #$40000000,$c00004  ;Настройка начального адреса ($0000) и режима
lea          font(pc),a5         ;Адрес хранения шрифта
move         #4095,d5            ;Размер шрифта (8Кб)
;Пересылка символов
VramFont:
move.w       (a5)+,$c00000       ;Отправим слово
dbra         d5,VramFont         ;Повторять, пока не конец
       ;Пересылка списка мелодий
move.w       #$8f02,$c00004             ;Инкремент записи в видеопамять =2 (словами)
move.l       #$40000003,$c00004  ;Настройка начального адреса ($C000) и режима
lea          BTList,a0           ;Загрузка начала таблицы
move.w       #1791,d1            ;Размер пересылаемой области
move.w       #$8000,d0           ;Подготовить маску палитры
       VramList:
             move.b       (a0)+,d0            ;Взять символ
             move.w       d0,$C00000          ;Записать готовое слово в VDP
             dbra         d0,VramList         ;Повторять до заполнения
move.w       #$8004,$c00004             ;Управление: настройка VDP (R0 = $04)
move.w       #$8164,$c00004             ;Управление: настройка VDP (R1 = $64)
move.w       #$8b08,$c00004             ;Управление: настройка VDP (Rb = $00)
move.b       #$40,$a10009        ;Инициализация джойстика №1
and.w        #$F8FF,sr           ;Разрешение прерываний
 
Регистры VDP R0, R1 и Rb являются ключевыми управляющими регистрами. С их помощью задается начальный режим работы. Наиболее интересен регистр R1. При R1=$64, VDP вырабатывает кадровое прерывание VBlank и настраивается на режим PAL. Подробнее о регистрах VDP можно посмотреть в [1]. Экранная страница состоит из матрицы символов 64x28. Однако, отображается только 40x28, т.е. часть символов не отображается, но может быть использована при скроллинге. Список мелодий будет выглядеть так (количество пробелов имеет значение!):
BTList:      dc.b         '                                        ',’                        ‘
dc.b         '          Battle Toad',$27,'s Trax            ',’                        ‘
dc.b         '                                        ',’                        ‘
dc.b         '                                        ',’                        ‘
dc.b         '    Silence                             ',’                        ‘
dc.b         '    Title                               ',’                        ‘
dc.b         '    Level 1   (Ragnarok',$27,'s Canyon)       ',’                        ‘
dc.b          '    Level 2   (Wookie Hole)             ',’                        ‘
dc.b         '    Level 3.1 (Turbo Tunnel)            ',’                        ‘
dc.b         '    Level 3.2 (Crazy Ride)              ',’                        ‘
dc.b         '    Level 4   (Arctic Caverns)          ',’                        ‘
dc.b         '    Level 5   (Surf City)               ',’                        ‘
dc.b         '    Level 6   (Karnath',$27,'s Lair)          ',’                        ‘
dc.b         '    Level 7   (Volkmire',$27,'s Inferno)      ',’                        ‘
dc.b         '    Level 8   (Intruder Excluder)       ',’                        ‘
dc.b         '    Level 9   (Terra Tubes)             ',’                        ‘
dc.b         '    Level 10  (Rat Race)                ',’                        ‘
dc.b         '    Level 11  (Clinger Winger)          ',’                        ‘
dc.b         '    Level 12  (The Revolution)          ',’                        ‘
dc.b         '    Boss                                ',’                        ‘
dc.b         '    Next Stage                          ',’                        ‘
dc.b         '    Stage Complete                      ',’                        ‘
dc.b         '    Wanna Continue?                     ',’                        ‘
dc.b         '                                        ',’                        ‘
dc.b         '                                        ',’                        ‘
dc.b         '      Up and Down controls cursor       ',’                        ‘
dc.b         '  A, B, C or Start play selected tune   ',’                        ‘
dc.b         '                                        ',’                        ‘
dc.b         '                                        ',’                        ‘
Через запятую отделены неотображаемые символы. Теперь набросаем обработчик кадрового прерывания:
 
VBlank:      dc.w          $48E7,$FFFE  ;MOVEM.L     D0-D7/A0-A6,-(A7) - cохранение регистров
;Вывод курсора
clr.l         d1           ;Счетчик строк
OutLoop:
move.l       d1,d0        ;Занесем номер текущей строки в d0
add.l        #4,d0        ;Прибавим смещение на начало названий мелодий
rol.l        #8,d0        ;Сдвинем на 8 разрядов
rol.l        #8,d0        ;Сдвинем на 8 разрядов
rol.l        #7,d0        ;И еще на 7 разрядов – эти сдвиги нужны для того, чтобы
                           ;номер строки превратить в адрес Vram
or.l         #$40000003,d0 ;Получившийся адрес сложить с битами режима
move.l       d0,$C00004   ;Записать управляющее слово в VDP
move.w       Position,d0  ;Взять позицию курсора
cmp.w        d0,d1        ;Сравнить с текущей
bne          OutSp        ;Если не равно – стираем курсор
move.w       #$8020,d0    ;Берем ‘ ’
move.w       d0,$C00000   ;Записываем в VDP
move.w       #$803D,d0    ;Берем ‘=’
move.w       d0,$C00000   ;Записываем в VDP
move.w       #$803E,d0    ;Берем ‘>’
move.w       d0,$C00000   ;Записываем в VDP
bra           OutCnt       ;Выход
OutSp: move.w       #$8020,d0    ;Берем ‘ ’
move.w       d0,$C00000   ;Записываем в VDP
move.w       d0,$C00000   ;Записываем в VDP
move.w       d0,$C00000   ;Записываем в VDP
OutCnt:      add.l        #1,d1        ;Следующая строка
cmp.l        #$15,d1             ;Уже все?
bne          OutLoop             ;Нет - повторить
;Опрос джойстика
clr.l        d0           ;Очищаем d0
clr.l        d1           ;Очищаем d1
move.b       #$40,$a10003 ;Сигнал SYN джойстика в 1
nop                        ;Задержка
nop                        ;Задержка
move.b       $A10003,d1   ;Считаем первые 6 кнопок
andi.b       #$3f,d1             ;Выделим их
move.b       #$00,$a10003 ;Сигнал SYN джойстика в 0
nop                        ;Задержка
nop                        ;Задержка
move.b       $A10003,d0   ;Считаем вторые 2 кнопки
and.b        #$30,d0             ;Выделим их
rol.b        #2,d0        ;Сдвинем на 2 разряда
or.b         d1,d0        ;И совместим все 8 основных кнопок
move.b       d0,d1        ;Сохраним в d1
move.b       #$40,$a10003 ;Сигнал SYN джойстика в 1
nop                        ;Задержка
nop                        ;Задержка
move.b       #$00,$a10003 ;Сигнал SYN джойстика в 1
nop                        ;Задержка
nop                        ;Задержка
move.b       #$40,$a10003 ;Сигнал SYN джойстика в 1
nop                        ;Задержка
nop                        ;Задержка
move.b       #$00,$a10003 ;Сигнал SYN джойстика в 1
nop                        ;Задержка
nop                        ;Задержка
move.b       #$40,$a10003 ;Сигнал SYN джойстика в 1
nop                        ;Задержка
nop                        ;Задержка – этот ‘мутор’ нужен для активации дополнительных
                           ;четырех кнопок джойстика (XYZM)
move.b       $A10003,d0   ;Считаем дополнительные кнопки
andi.b       #$0F,d0             ;Выделим их
eor.b        #$0F,d0             ;Проинвертируем их
rol.l        #8,d0        ;Сдвинем на 8 бит
or.b         d1,d0        ;И совместим их с основными 8ми
not.b        d0           ;Инвертируем основные кнопки
move.b       #$40,$a10003 ;Сигнал SYN джойстика в 1
move.w       d0,JoyState  ;И сохраним состояние джойстика
dc.w         $4CDF,$7FFF  ;MOVEM.L     (A7)+,D0-D7/A0-A6 – восстановление регистров
rte                        ;Возврат в программу
 
Процедура кадрового прерывания всегда прорисовывает первые 3 столбца символов. Курсор выводится в ту строку, номер которой совпадает с указателем в таблице мелодий. Это позволило упростить код. Почти все готово, осталось только набросать основной цикл:
             clr.l        d1           ;d1 – счетчик номера мелодии
       MainLoop:
             move.w       d1,Position  ;Сохраним позицию
move.w       JoyState,d0  ;Проверим состояние джойстика
cmp.w        #0,d0        ;Если кнопки не нажаты
beq          NextLoop     ;Крутим малый круг
       move.w       JoyState,d2  ;Сохраним состояние кнопок
Press: move.w       JoyState,d0  ;Проверим состояние кнопок
cmp.w        #0,d0        ;Пока кнопка(и) зажата(ы)
bne          Press        ;Крутить по малому кругу
;Проверка на курсор
move.w       d2,d0        ;Проверим состояние кнопок
and.w        #JoyUp,d0    ;Это ‘Вверх’?
cmp.w        #0,d0        ;Нет -
beq          Key1         ;    - пропустить
       ;Кнопка ‘Вверх’
             Cmp.b        #0,d1        ;Уже самый верх?
             Bne          CurUp         ;Нет – курсор вверх
             Move.b       #$12,d1             ;Да – курсор в самый низ
             Bra          MainLoop     ;Вернемся в программу
       CurUp: sub.b        #1,d1        ;Сдвинем курсор вверх
             Bra          MainLoop     ;Вернемся в программу
Key1:  move.w       d2,d0        ;Проверим состояние кнопок
and.w        #JoyDown,d0  ;Это ‘Вниз’?
cmp.w        #0,d0        ;Нет -
beq          Key2         ;    - пропустить
       ;Кнопка ‘Вниз’
             add.b        #1,d1        ;Сдвинем курсор вниз
             Cmp.b        #$13,d1             ;Уже за краем?
             Bne          MainLoop     ;Нет – вернемся в программу
             clr.b        d1           ;Да – курсор в начало
             Bra          MainLoop     ;Вернемся в программу
Key2:  move.w       d2,d0        ;Проверим состояние кнопок
and.w        #JoyABCS,d0  ;Это ‘Кнопки’?
Cmp.w        #0,d0        ;Нет -
Beq          MainLoop     ;    - вернемся в программу
;Установка мелодии
Lea          BTab,a0             ;Загрузим адрес таблицы номеров мелодий
clr.l        d0           ;Обнулим d0
move.b       d1,d0        ;Номер мелодии в d0
add.l        d0,d0        ;Умножить на 2 (таблица состоит из слов)
add.l        d0,a0        ;Прибавим к адресу таблицы
move.w       (a0),d0             ;Возьмем номер мелодии из таблицы
move.w       #$100,$A11100 ;Захватим шины Z80
nop                        ;Ждем
nop                        ;Ждем
lea          $A01C0A,a0   ;Установим адрес ячейки управления драйвером
move.b       d0,(a0)      ;Запишем номер мелодии
move.w        #0,$A11100  ;Освободим шины Z80
bra          MainLoop     ;Возврат в программу
Z80 может обращаться к памяти М68К только блоками по 32Кб. Так как драйвер Z80 рассчитан на работу только с одним банком, то самый ближайший свободный банк – это банк №1 ($008000…$00FFFF). Для того чтобы драйвер мог с ним работать, надо изменить в ячейке с адресом $1C00 значение $07 на $00. При этом после компиляции кода, его необходимо дополнить байтами $FF до объема в 32кб. Это ассемблер может сделать автоматически, если в конце текста поставить оператор ORG $8000. После компиляции, к полученному объектному коду нужно добавить коды мелодий. Это можно сделать, написав бат-файл (“бат” от английского “batch” – “пакетный”) в любом текстовом редакторе и сохранив его как “build.bat”. Вся операция пройдет автоматически при запуске этого файла. Этот файл может выглядеть так:
 
@Echo Off
asm.exe pd-rom.asm pd-rom.tmp
copy /B pd-rom.tmp + music.dat pd-rom.bin > NUL
del pd-rom.tmp
pause
 
Первая строчка пакетного файла отключает лишние консольные сообщения. Вторая строка компилирует наш PD ROM в “pd-rom.tmp”. Третья склеивает этот временный файл и файл мелодий. Четвертая удаляет временный файл. Последняя строка – просто ожидание нажатия клавиши. Она нужна для того, чтобы успеть прочитать сообщения ассемблера об ошибках. В результате, если при компиляции ошибок нет, получается файл “pd-rom.bin”, который можно запустить в эмуляторе.
Полный исходный код данной программы, ассемблер М68К и все необходимые ресурсы прилагаются. Так же прилагается скомпилированный работоспособный PD-ROM.

Автор статьи - Hardwareman. HTML вёрстка - Jim_DI. (с) Emu-Russia, 2009

 
 

 

Эму-Россия © 2001-2022