Игры не использующие мапперы в NES ограничены, 16 кб PRG ROM (хранилище программного кода) и 8 кб CHR ROM (хранилище графики). С развитием разработки игр на NES, встал вопрос, а как увеличить данные ограничения и на помощь пришли микросхемы мапперы. Что же такое мапперы мы и разберем сегодня и как их использовать в своем коде.
Что такое мапперы?
Мапперы - это микросхемы которые способны подменять данные подаваемые на выводы (порты) в картридже. К примеру гипотетический упрощенный маппер способен переключаться между двумя микросхемами памяти на картридже и соединять их с выводами порта в консоли. Тогда мы получаем два банка памяти с кодом по 16Кб, в общем уже 32Кб.
Маппер MMC1
Это ASIC (Интегральная схема специального назначения) способная переключать PRG банки памяти и соответсвенно CHR. А так же менять зеркалирование nametable на лету. У данного маппера существует несколько вариантов использования SNROM, SOROM, SZROM, SXROM и так далее, основное отличие между ними в количестве переключаемых банков памяти, и наличие/отсутствие оперативной памяти для PRG/CHR. Многие игры с сохранениями на батарейках такие как The Legend of Zelda использовали RAM память для сохранения и восстановления состояния игры, для того что бы изменения не терялись и была встроена батарейка которая питала RAM память. Но для своих целей Я выбрал конфигурация SLROM, без RAM памяти и с общей памятью по 128кб для графики и программы. Такая же конфигурация маппера была и в игре Chip 'n Dale Rescue Rangers.
Банки памяти
Маппер содержит следующие банки памяти:
$6000 - $7FFF - оперативная память в нашей конфигурации не используется
$8000 - $BFFF - 16кб PRG ROM банк, переключаемый, по умолчанию зафиксирован на первой страницы
$C000 - $FFFF - 16кб PRG ROM банк, переключаемый, зафиксированный на последней странице
$0000 - $0FFF - 4кб CHR ROM банк, переключаемый
$1000 - $1FFF - 4кб CHR ROM банк, переключаемый
Порты маппера
Маппер имеет серийные порты для его управления, а это значит что в такой порт необходимо последовательно записать 1 или 0. Маппер имеет следующие регистры:
Регистр загрузки - $8000 - $FFFF
Регистр управления - $8000 - $9FFF
CHR банк 0 - $A000 - $BFFF
CHR банк 1 - $С000 - $DFFF
PRG банк - $E000 - $FFFF
Заголовки ines
Первым делом что бы корректно заработал маппер в нашем проекте, нам необходимо прописать следующие заголовки
.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
Тем самым мы говорим эмуляторам что будет использовать маппер MMC1 с конфигурацией 128кб для графики и кода.
Конфигурация линкера ld65
Добавлено 8 ПЗУ страниц и 16 ПЗУ страниц с графикой
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;
}
На самом деле этот пункт был один из самых сложных, надо понять 2-ве простые вещи:
Банк содержит страницы, и страницы имеют одинаковые адреса банка памяти
Банки должны быть заполнены $ff
Все векторы прерывания должны быть расположены в фиксированной страницы у меня это $C000 собственно который и был, так же процедура reset так же должна быть в фиксированной области памяти
После этого мы должны дописать недостающие новые секции в файл нашего проекта и можно компилировать. Правда при запуске возможно небудет графики.
Графика
Для этого надо разбить наш 8кб файл chr на два файла по 4096 байт это я сделал с помощью команды split
split -b4096 test.chr
переименовываем получившиеся файлы в соответствие с нашими пожеланиями, и импортируем в сегменты CHR страниц.
.segment "CHR0"
.incbin "test_1_1.chr"
.segment "CHR1"
.incbin "test_1_2.chr"
.segment "CHR2"
.incbin "test_2_1.chr"
.segment "CHR3"
.incbin "test_2_2.chr"
Примерно должно получиться так, теперь мы компилируем наш проект и появиться та графика которая была до миграции на маппер MMC1 с NROM.
Важное замечание
Прежде чем перейдем к работе с маппером необходимо знать несколько моментов:
У регистров есть указатель на порядок записи битов что бы сбросить его надо лишь записать 7 бит в порт прежде чем с ним работать.
В качестве значения для записи в последовательный порт берется младший бит (0-й с права на лево)
При записи на любой адрес диапазона (которые я упоминал выше) значение будет перенаправлено на начальный адрес регистра
Функции управления маппером
Исходя из замечаний выше мной были написаны общие процедуры для управления маппером
Запись в регистр controll
.proc writeToMapper
STA $8000 ; first data bit
LSR A ; shift to next bit
STA $8000 ; second data bit
LSR A ; etc
STA $8000
LSR A
STA $8000
LSR A
STA $8000 ; config bits written here, takes effect immediately
RTS
.endproc
Данная процедура нужна для инициализации маппера и смены зеркалирования
Запись в регистр CHR 0
.proc changeChrZerro
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
регистр CHR 1
.proc changeChrFirst
STA $C000 ; first data bit
LSR A ; shift to next bit
STA $C000 ; second data bit
LSR A ; etc
STA $C000
LSR A
STA $C000
LSR A
STA $C000 ; config bits written here, takes effect immediately
RTS
.endproc
Смена страницы PRG
.proc changePrgBank
STA $C000 ; first data bit
LSR A ; shift to next bit
STA $C000 ; second data bit
LSR A ; etc
STA $C000
LSR A
STA $C000
LSR A
STA $C000
RTS
.endproc
Далее рассмотрим процедуру reset данного маппера
Hidden text
.proc resetMapper
LDA #$80
STA $8000
RTS
.endproc
.proc resetMapperProcedure
INC resetMapper ; тут в порт $8000 будет записан %1000 0001
RTS
.endproc
Если обратиться к документации то мы увидим что для инициализации маппера необходимо в порт $8000 записать значение %1000 0001 где:
7-й бит блокирует банк памяти $C000 - $FFFF
0-й бит включает режим сдвига, младший бит как значение
Вызываем reset в нашем векторе перезагрузки.
Смена CHR страницы
Мы подготовили процедуры для общению с регистрами маппера для того что бы переключить chr страницу нам всего лишь нужно выполнить следующие:
LDA #%00000000 ; номер страницы по порядку 0 и 1 будут страницами 0 и 1 соответственно
JSR changeChrZerro
При этом указанная страница станет parrent table 0, а следующая parent table 1
Следующая функция сменить зеркалирование, так устанавливаю вертикальное зеркалирование
.proc setVerticalMirror
JSR resetMapper ; для начала 7 бит должен быть 1 что бы сбросить счетчик
LDA #%00001110 ; 8KB CHR, 16KB PRG, $8000-BFFF swappable, vertical mirroring
JSR writeToMapper ; записываем в маппер значение
RTS
.endproc
А так горизонтальное зеркалирование
.proc setHorizontalMirror
JSR resetMapper
LDA #%00001111 ; 8KB CHR, 16KB PRG, $8000-BFFF swappable, vertical mirroring
JSR writeToMapper
RTS
.endproc
Довольно просто, согласитесь? И последним смена страницы PRG
.proc changePrgToFirst
LDA #%00000000 ; порядковый номер страницы 4 бита десятичное число 0-15
JSR setPrgBank
RTS
.endproc
Организация кода
Для себя я выбрал следующую организацию кода все функции связанные с уровнями будут храниться в своих страницах. А общий код на подобие анимации героя некоторых врагов, функций загрузки фона, палитры, атрибутов будут находиться в общей фиксированной странице памяти.
В качестве заключения
В качестве заключения хотелось бы сказать что это мое первое знакомство с MMC1. И в принципе, если разобраться то не так уж сложно всё. Так же хочется сказать что на эмуляторе данная конфигурация линкера работает а вот на реальном железе пока не тестировалось, но в ближайшее время хочу собрать картридж для тестирования с перезаписываемой памятью, либо приобрести флеш-картридж. Я немного сократил теорию ибо можно долго ходить вокруг да около одного порта и не понять ничего.
Полезные ссылки
https://www.youtube.com/channel/UCzgRrIXX4QDiaWkISx6DKFw - канал о программирование на nes
Программирование assembler 6502 nes/famicom/dendy векторы прерывания, процедуры и их вызов
https://habr.com/ru/post/719636/ - вывод спрайтов и анимация
https://habr.com/ru/publication/edit/721168/ - флаги статусов процессора
https://www.nesdev.org/wiki/MMC1 - маппер ммс1 nesdev
https://nerdy-nights.nes.science/ - хорошие стать по использованию маппера