Игры в денди были ограничены 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 раз
Банки памяти
Банки памяти распределены следующим образом в мапер, обратите внимание что каждый порт в диапазоне адресов памяти является управляющим, они следующие:
- $6000 - $7FFF - оперативная память в нашей конфигурации не используется
- $8000 - $BFFF - 16кб PRG ROM банк, переключаемый, по умолчанию зафиксирован на первой страницы
- $C000 - $FFFF - 16кб PRG ROM банк, переключаемый, зафиксированный на последней странице
- $0000 - $0FFF - 4кб CHR ROM банк, переключаемый
- $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 в своих проектах.
- telegram - https://t.me/dendydevelop
- Донаты donationalert - https://www.donationalerts.com/r/nesdeveloper
- Донаты yoomoney - https://yoomoney.ru/fundraise/3tjwOQQwKPk.230209
- Телеграм для связи - @phpiv