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

9400d9a2a469e95912379987257b961c

Игры не использующие мапперы в 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. И в принципе, если разобраться то не так уж сложно всё. Так же хочется сказать что на эмуляторе данная конфигурация линкера работает, а вот на реальном железе пока не тестировалось, но в ближайшее время хочу собрать картридж для тестирования с перезаписываемой памятью, либо приобрести флеш-картридж. Я немного сократил теорию ибо можно долго ходить вокруг да около одного порта и не понять ничего.

Полезные ссылки

© Habrahabr.ru