Найти тему
Roman Hz

Маппер ММС1 программирование dendy

Оглавление

Игры в денди были ограничены 16 кб PRG ROM (программа) и 8 кб CHR ROM (графика).  И этих ограничений быстро стало не хватать для более продвинутых игр, таких как Darkwing Duck, Chip and Dail, Contra, Adventure Island и другие игры. Тут надо уточнить что были игры и очень похожие на то что работали без мапппера, яркий пример super mario bros 2 но марио уникален тем что у него сильно оптимизирована графика, это вообще один из шедевров в плане визуализации, самый банальный пример тучка и кустарник один и тот же спрайт только в разных цветовых гаммах.

Но вернемся к разработке игры и к вопросу имплементации маппера, тут оговорюсь что Я использую пакет ассемблера ca65 и линкер ld65. По этому начнем по порядку:

  • Работа с портами
  • Банки памяти
  • Порты мапера
  • Заголовки ines
  • Рассмотрим конфигурацию линкера ld65
  • Методы инициализации мапера
  • Переключение банков памяти программы
  • Переключение банков памяти графики
  • Смена зеркалирования

Работа с портами

Порты в мапере ММС1 последовательные, а это значит в каждый порт управления последовательно мы должны записать либо 1 либо 0, 8 раз, и тогда мы сможем составить 8 битный код который будет указывать на банк памяти или банк графики. Есть еще один нюанс, в порт записывается младший бит, то есть последний бит, по этому часто при его использование можно в дебагере увидеть конструкцию похожую на

LDA #%00000011
STA $8000
LSR
STA $8000
LSR
STA $8000
; и так 8 раз

Банки памяти

Банки памяти распределены следующим образом в мапер, обратите внимание что каждый порт в диапазоне адресов памяти является управляющим, они следующие:

  1. $6000 - $7FFF - оперативная память в нашей конфигурации не используется
  2. $8000 - $BFFF - 16кб PRG ROM банк, переключаемый, по умолчанию зафиксирован на первой страницы
  3. $C000 - $FFFF - 16кб PRG ROM банк, переключаемый, зафиксированный на последней странице
  4. $0000 - $0FFF - 4кб CHR ROM банк, переключаемый
  5. $1000 - $1FFF - 4кб CHR ROM банк, переключаемый

То есть, к примеру, для переключения банка памяти с 1 на 2, мы можем просто последовательно записать в порт $8000 значение 00000010, так же мы можем записать это же значение и в порт $BFFF и он так же сработает как и при записи в $8000. В этой статье буду использовать только первые порты диапазонов, так проще и нагляднее.

Порты мапера

Исходя из перечня адресов банков памяти выделяются следующие диапазоны портов для управления портов:

  • Регистр загрузки - $8000 - $FFFF
  • Регистр управления - $8000 - $9FFF
  • CHR банк 0 - $A000 - $BFFF
  • CHR банк 1 - $С000 - $DFFF
  • PRG банк - $E000 - $FFFF

Напоминаю что логику работу с этими портами я описал выше просто необходимо записать последовательно 8 раз 0 или 1 в порт.

Заголовки INES

 Для того что бы мапер заработал в эмуляторах необходимо корректно прописать заголовки в файле .nes, для этого мы просто прописываем следующее в файле с кодом ассемблера

.segment "HEADER" .byt "NES",$1A
.byt 8 ; 8 x 16kB PRG block. 128kb
.byt 16 ; 16 x 8kB CHR block. 128kb
.byt 17
.byt 02 ; mapper

Оговорюсь что Я использовал конфигурацию мапера 128кб на 128кб программной и графической памяти.

Конфигурация линкера

Приведу пример для конфигурации линкера ld65, но понимание как правильно собрать участки памяти в образе картриджа будет полезно и для других ассемблеров и инструментов для создания игр на денди.


MEMORY {
HEADER: start=$00, size=$10, fill=yes, fillval=$00;
ZEROPAGE: start=$10, size=$ff;
STACK: start=$0100, size=$0100;
OAMBUFFER: start=$0200, size=$0100;
RAM: start=$0300, size=$0500;

ROM1: start=$8000, size=$4000, fill=yes, fillval=$ff;
ROM2: start=$8000, size=$4000, fill=yes, fillval=$ff;
ROM3: start=$8000, size=$4000, fill=yes, fillval=$ff;
ROM4: start=$8000, size=$4000, fill=yes, fillval=$ff;
ROM5: start=$8000, size=$4000, fill=yes, fillval=$ff;
ROM6: start=$8000, size=$4000, fill=yes, fillval=$ff;
ROM7: start=$8000, size=$4000, fill=yes, fillval=$ff;

ROM: start=$C000, size=$4000, fill=yes, fillval=$ff;
# 16 CHR ROM
CHRROM0: start=$0000, size=$1000;
CHRROM1: start=$0000, size=$1000;
CHRROM2: start=$0000, size=$1000;
CHRROM3: start=$0000, size=$1000;
CHRROM4: start=$0000, size=$1000;
CHRROM5: start=$0000, size=$1000;
CHRROM6: start=$0000, size=$1000;
CHRROM7: start=$0000, size=$1000;
CHRROM8: start=$0000, size=$1000;
CHRROM9: start=$0000, size=$1000;
CHRROM10: start=$0000, size=$1000;
CHRROM11: start=$0000, size=$1000;
CHRROM12: start=$0000, size=$1000;
CHRROM13: start=$0000, size=$1000;
CHRROM14: start=$0000, size=$1000;
CHRROM15: start=$0000, size=$1000;
CHRROM_1: start=$1000, size=$1000;
}

SEGMENTS {
HEADER: load=HEADER, type=ro, align=$10;
ZEROPAGE: load=ZEROPAGE, type=zp;
STACK: load=STACK, type=bss, optional=yes;
OAM: load=OAMBUFFER, type=bss, optional=yes;
BSS: load=RAM, type=bss, optional=yes;
DMC: load=ROM, type=ro, align=64, optional=yes;
CODE: load=ROM, type=ro, align=$0100;

CODE_1: load=ROM1, type=ro, align=$0100;
CODE_2: load=ROM2, type=ro, align=$0100;
CODE_3: load=ROM3, type=ro, align=$0100;
CODE_4: load=ROM4, type=ro, align=$0100;
CODE_5: load=ROM5, type=ro, align=$0100;
CODE_6: load=ROM6, type=ro, align=$0100;
CODE_7: load=ROM7, type=ro, align=$0100;

RODATA: load=ROM, type=ro, align=$0100;
VECTORS: load=ROM, type=ro, start=$FFFA;

CHR0: load=CHRROM0, type=ro, align=16, optional=yes;
CHR1: load=CHRROM1, type=ro, align=16, optional=yes;
CHR2: load=CHRROM2, type=ro, align=16, optional=yes;
CHR3: load=CHRROM3, type=ro, align=16, optional=yes;
CHR4: load=CHRROM4, type=ro, align=16, optional=yes;
CHR5: load=CHRROM5, type=ro, align=16, optional=yes;

CHR_1: load=CHRROM_1, type=ro, align=16, optional=yes;
}

Единственная сложность было понять то что мы имеем, к примеру, 8 страниц памяти, но банк памяти от этого не меняется. Что бы проще было понять это утверждение, просто надо вспомнить как работает мапер на уровне железа. А работает он следующим образом, он как бы подменяет стандартную память в 8 кб наборами страниц. По этому адрес банка памяти не меняется на физическом уровне, а просто подменяется.

Инициализация мапера

И так мы прописали заголовки файла, составили конфигурацию линкера. Теперь необходимо в векторе прерывания перезагрузки "reset", прописать процедуру перезагрузки нашего мапера. Код очень прост.

proc resetMapper
LDA #$80
STA $8000

RTS
.endproc

.proc resetMapperProcedure
INC resetMapper ; тут в порт $8000 будет записан %1000 0001
RTS
.endproc

И так что здесь происходит:

  • 7-й бит блокирует банк памяти $C000 - $FFFF
  • 0-й бит включает режим сдвига, младший бит как значение

Для большего понимания 16-ное число #$80 это #%10000001.

Переключение банков памяти программы

При изучение выше перечисленных особенностях мапера это операция становиться тоже довольно простой

.proc changePrgPage
STA $С000 ; first data bit
LSR A ; shift to next bit
STA $С000 ; second data bit
LSR A ; etc
STA $С000
LSR A
STA $С000
LSR A
STA $С000 ; config bits written here, takes effect immediately RTS .endproc

Теперь что бы переключить банк памяти необходимо просто в A загрузить номер страницы (к примеру LDA #$02), и вызвать процедуру changePrgPage (JSR changePrgPage).

Переключение графической памяти 

Код в принципе остается таким же как в процедуре changePrgPage, единственное меняется порт соответственно с $8000 на $A000.

.proc changeChrPage
STA $A000 ; first data bit
LSR A ; shift to next bit
STA $A000 ; second data bit
LSR A ; etc
STA $A000
LSR A
STA $A000
LSR A
STA $A000 ; config bits written here, takes effect immediately RTS .endproc

Смена зеркалирования

И последнее смена зеркалирования, и опять процедура будет очень похожа на то что выше, единственное порт будет $8000, а процедуру назовем к примеру mapperControl и тогда для того что бы сменить зеркалирование необходимо просто загрузить значение в А равное #%00001110 для вертикального отображения, и #%00001111 для горизонтального. Вы наверное обратили внимание что как то много единиц для просто зеркалирования, все дело в том что порт $8000 отвечает за настройки мапера и зеркалирование это одна из таких его настроек. О них более подробно будет описано в следующих статьях.

В качестве заключения

В следующих статьях рассмотрим как использовать мапер ММС3 в своих проектах.