Имплементация маппера MMC1 ассемблер 6502 nes/famicom/dendy

от автора

Игры не использующие мапперы в 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.

Банки памяти

Маппер содержит следующие банки памяти:

  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 или 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-ве простые вещи:

  1. Банк содержит страницы, и страницы имеют одинаковые адреса банка памяти

  2. Банки должны быть заполнены $ff

  3. Все векторы прерывания должны быть расположены в фиксированной страницы у меня это $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.

Важное замечание

Прежде чем перейдем к работе с маппером необходимо знать несколько моментов:

  1. У регистров есть указатель на порядок записи битов что бы сбросить его надо лишь записать 7 бит в порт прежде чем с ним работать.

  2. В качестве значения для записи в последовательный порт берется младший бит (0-й с права на лево)

  3. При записи на любой адрес диапазона (которые я упоминал выше) значение будет перенаправлено на начальный адрес регистра

Функции управления маппером

Исходя из замечаний выше мной были написаны общие процедуры для управления маппером

Запись в регистр 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://habr.com/ru/post/721654/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *