assembler 6502 (nes, famicom, dendy), миграция с мапера MMC1 на MMC3 прерывание IRQ

от автора

Пришло время для того что бы сделать в игре статус бар, где можно было бы отображать жизни, удары, инвентарь. Для разработки игровых механик на подобие powerup’ов, а так же механик рпг, к примеру что бы пройти на следующую часть уровня надо разбить некое препятствие условным молотком. Есть два основных способа решения данной задачи:

  1. Sprite 0 hit — смысл алгоритма спрайт зеро хит, то что при появление первого непрозрачного пикселя спрайта который перекрывает не прозрачный пиксель фона, нес в статус флаге CPU устанавливает бит флага нулевого спрайта. За этот момент можно зацепится и к примеру до установки данного флага прокрутку устанавливать на 0 а после установки уже прокручивать фон. Способ довольно простой но для простых статус баров с верху. При этом есть объективные минусы в том что мы должны ждать наступления спрайта 0 по этому это должна быть небольшая область с верху. Типичный пример реализации данного метода это супер марио брос, у него в меню есть монетка нижняя часть которой перекрыта спрайтом.

  2. Генерировать событие IRQ на определенной линии отрисовки экрана — такое прерывание может генерировать чуть ли не самый используемый маппер MMC3, данный способ более прямой чем спрайт зеро хит по этому для меня он стал более приемлемым разберем подробнее чуть ниже его.

Отличия MMC1 от MMC3:

  • MMC1 — переключает страницы памяти PRG полностью и CHR страницы полностью по 4 кб.

  • MMC3 — переключает банки и страницы PRG а CHR условно говоря собирает из нескольких частей, что более оптимально потому как можно использовать разные части графики в разных готовый наборах pattern 0 и pattern 1

  • MMC1 — управляется последовательным портом надо по сути сделать 8 операций записи битов 0 или 1 что бы управлять последним

  • MMC3 — управляется записью в два регистра $8000 и $8001 байта указывающего на действие нужное нам

  • MMC1 — не генерирует IRQ прерывания

  • MMC3 — генерирует IRQ прерывание на событие hBlank

  • MMC1 и MMC3 имеют различные маппинги памяти.

Инициализация MMC3 и работа с ним

В первую очередь нам необходимо переделать нашу конфигурацию линкера для того что бы ром собрался корректна. Пока конфигурацию для MMC3 выбрал ту же самую что и для MMC1 — 128кб PRG памяти и 128кб CHR.  У меня получилась следующая конфигурация:

Пример конфигурации
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;    ROM_0:          start = $8000,  size = $2000, type = ro, file = %O, fill=yes, fillval = $D0;   ROM_1:          start = $8000,  size = $2000, type = ro, file = %O, fill=yes, fillval = $D1;   ROM_2:          start = $8000,  size = $2000, type = ro, file = %O, fill=yes, fillval = $D2;   ROM_3:          start = $8000,  size = $2000, type = ro, file = %O, fill=yes, fillval = $D3;   ROM_4:          start = $8000,  size = $2000, type = ro, file = %O, fill=yes, fillval = $D4;   ROM_5:          start = $8000,  size = $2000, type = ro, file = %O, fill=yes, fillval = $D5;   ROM_6:          start = $8000,  size = $2000, type = ro, file = %O, fill=yes, fillval = $D5;   ROM_7:          start = $8000,  size = $2000, type = ro, file = %O, fill=yes, fillval = $D5;   ROM_8:          start = $8000,  size = $2000, type = ro, file = %O, fill=yes, fillval = $D5;   ROM_9:          start = $8000,  size = $2000, type = ro, file = %O, fill=yes, fillval = $D5;   ROM_10:          start = $8000,  size = $2000, type = ro, file = %O, fill=yes, fillval = $D5;   ROM_11:          start = $8000,  size = $2000, type = ro, file = %O, fill=yes, fillval = $D5;   ROM_12:          start = $8000,  size = $2000, type = ro, file = %O, fill=yes, fillval = $D5;    ROM_H:          start = $A000,  size = $4000, type = ro, file = %O, fill=yes, fillval = $CC;   PRG_FIXED:   start = $E000,  size = $2000, type = ro, file = %O, fill = yes, fillval = $FF;    CHR: start=$1000, size=$20000; }  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_H, type=ro, align=64, optional=yes;    CODE_1: load=ROM_0, type=ro, align=$0100;   CODE_2: load=ROM_1, type=ro, align=$0100;   CODE_3: load=ROM_2, type=ro, align=$0100;   CODE_4: load=ROM_3, type=ro, align=$0100;   CODE_5: load=ROM_4, type=ro, align=$0100;   CODE_6: load=ROM_5, type=ro, align=$0100;   CODE_7: load=ROM_6, type=ro, align=$0100;   CODE_8: load=ROM_7, type=ro, align=$0100;   CODE_9: load=ROM_8, type=ro, align=$0100;   CODE_10: load=ROM_9, type=ro, align=$0100;   CODE_11: load=ROM_10, type=ro, align=$0100;   CODE_12: load=ROM_11, type=ro, align=$0100;   CODE_13: load=ROM_12, type=ro, align=$0100;    CODE: load=PRG_FIXED, type=ro, align=$0100;   RODATA: load=ROM_12, type=ro, align=$0100;   VECTORS: load=PRG_FIXED, type=ro, start=$FFFA;    CHR: load=CHR, type=ro, align=16, optional=yes; }

Самое важное в конфигурации то что код инициализации маппера а так же векторы прерывания должны располагаться в фиксированной области памяти. Фиксированный банк памяти находиться по адресам $E000 — $EFFF. CHR просто выделил полностью память под график в одной секции куда в будущем сможем добавить все нужные нам файлы без деления на 2кб, 2кб, 1кб, 1кб и так далее особенность хранения chr в маппере MMC3. Для начала я просто использовал старые CHR еще не дорабатывал аспект разбиения и составления данных таблицы паттернов.

Для инициализации маппера, необходимо инициализировать регистры и загрузить банки памяти, на самом деле способ подсмотрел в интернете, и данные процедуры были  copy-past’ом перенесены в проект привожу пример кода:

Код инициализации MMC3
.proc loadBanks         LDX #$08            ; Start of Page to load Add 8 Hex per CHR .. CHR 3 = $10 or 16         LDA #$80            ; Starting Address for $8000 0,1,2,3,4,5,6         LDY #$00            ; Loop Counter         LoadPPU2k:          ; load two sets 2x2k to make first 4k (BACKGROUND)             STA $8000       ; Bank Selection with Inversion             STX $8001       ; Selection of Bank             INY             INX             ; Increase X x 2 as 2k             INX             CLC             ADC #$01             CPY #$02        ; For loop             BNE LoadPPU2k         LoadPPU1k:              ; load 4 * 1k sets to make FORGROUND 4k             STA $8000           ; Bank Selection with Inversion             STX $8001           ; Selection of Bank             INY             INX                 ; Increase X * x as 1k             CLC             ADC #$01             CPY #$06        ; For loop             BNE LoadPPU1k     RTS .endproc  .proc initMMC3     LDX #$00     LDA #$00     STA $E000       ; IRQ disable     STA $A000       ; mirroring 0 Vertical; 1 Horizontal     :     STX $8000       ; select register     LDA mmc3Register, X     STA $8001       ; initialize register     INX     CPX #8          ; Compare 8 to X     BCC :-          ; Branch not Equal      ;PRG ROM Selections          LDY #02            ; Starting Banks Change This from 0 2 4 6 ETC to change Starting Color Startup $00         LDA #6              ; $8000 Selection Bank = 6 (NOTE: Not HEX)         STA $8000         STY $8001           ; Select Bank LOW         LDA #7              ; $A000 Selection Bank = 6 (NOTE: Not HEX)         STA $8000         INY         STY $8001           ; Select Bank HIGH          rts .endproc

Просто вызываем этот код в процедуре reset и мы готовы работать с маппером.

Модифицируем процедуры переключения банков программной памяти:

.proc setPrgBank     LDA #%00000110                 STA $8000     STX $8001        RTS .endproc

Для того что бы переключить банк памяти, необходимо просто загрузить в X номер банка и после вызвать процедуру, к примеру так:

LDX #$02 JSR setPrgBank

Процедуру переключения графических банков и страниц модифицируем следующим образом

.proc switchChr     STA $8000     STX $8001      RTS .endproc

Для переключения CHR необходимо загрузить в A номер банка памяти а в X номер страницы. Об этом поговорим в следующих статьях.

Так же меняем методы зеркалирования, в MMC3 это делается довольно просто записью в порт $A000: либо #$00 — Вертикальное зеркалирование, либо #$01 — Горизонтальное зеркалированние.

Код смены зеркалириования
.proc setVerticalMirror     LDA #$00     STA $A000      RTS .endproc  .proc setHorizontalMirror     LDA $01     STA $A000      RTS .endproc

После всех манипуляций наш старый код должен запуститься, в теории конечно, у меня далеко не с первого раза удалось корректно пере-собрать мой проект. Были проблемы из за банальной не внимательности не туда загружалась номер страницы, код был в другой совсем области, где то RODATA не подгружалась, а где то неправильно вызывалась функция. Очень сильно смущало что код работает но графики нет, решилось все банально корректным переключением банка памяти.

Прерывание IRQ

В первую очередь в процедуре RESET, нам необходимо отключить стандартное прерывание IRQ которое генерируются самой платформой NES дабы не обработка алгоритма не происходила повторно. Для этого надо записать #$40 в порт $4017

    LDA #$40     STA $4017     CLI

С помощью CLI мы включаем прерывания IRQ.

Далее, в начале вектора RESET, я расположил код инициализации точки срабатывания прерывания, опять же инициализация это образно сказано с моей стороны для простого понимания материала. Приведу код данной «инициализации»:

LDA #$C0 ; 192 линия STA $E000 ; отключаем прерывание STA $C000 ; записываем счетчик строк STA $C001 ; еще один счетчик STA $E000 ; еще раз отключаем прерывание что бы зафиксировать значение счетчика STA $E001 ; включаем прерывание

Немного распишу порты выше:

  1. E000 — запись любого значения в этот порт отключает прерывание

  2. E001 — запись любого значения включает прерывание

  3. C000 — счетчик обратного отсчета линий развертки, при прохождение каждой лини уменьшается на 1 и при достижение 0 будет сгенерировано прерывание IRQ

  4. С001 — фиксирует значение счетчика (на самом деле на многих сайтах по английски звучит как IRQ counter latch что я перевожу как некое запирание счетчика)

Переходим в вектор IRQ

.proc irq_isr     PHA ; а в стэк     TXA ; x -> a перенос     PHA     TYA ; y -> a перенос     PHA      LDA #$00     STA $E000 ; отключаем прерывание IRQ     STA $2005 ; фиксируем прокрутку на 0 по X и Y     STA $2005      PLA ; загрузить а из стэка     TAY ; a -> y перенос     PLA     TAX ; a -> x перенос     PLA ; загрузить а      RTI .endproc

Тут есть небольшой но довольно важный момент последовательность пуша аккумулятора в стек и пула  для того что бы сохранить значения так как IRQ может сработать где то по середине выполнения программы, и привести к непредвиденным ошибкам выполнения программы.

В качестве заключения материалы по теме:

  1. документация по MMC3

  2. полезный документ о IRQ прерываниях

Предыдущие статьи


ссылка на оригинал статьи https://habr.com/ru/articles/753794/


Комментарии

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

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