Одна из ключевых функций мапперов это переключить программных банков памяти и банков памяти с графикой CHR. Если брать для сравнение маппер MMC1 то он переключает графику постранично, по факту переключая полностью. MMC3 имеет 5 банков CHR памяти и позволяет их переключать по отдельности не переключая полностью pattern table.

MMCC3 маппит память следующим образом 2кб, 2кб, 1кб, 1кб, 1кб, 1кб. В документации nesdev есть следующая таблица.

7  bit  0
---- ----
CPMx xRRR
|||   |||
|||   +++- Specify which bank register to update on next write to Bank Data register
|||          000: R0: Select 2 KB CHR bank at PPU $0000-$07FF (or $1000-$17FF)
|||          001: R1: Select 2 KB CHR bank at PPU $0800-$0FFF (or $1800-$1FFF)
|||          010: R2: Select 1 KB CHR bank at PPU $1000-$13FF (or $0000-$03FF)
|||          011: R3: Select 1 KB CHR bank at PPU $1400-$17FF (or $0400-$07FF)
|||          100: R4: Select 1 KB CHR bank at PPU $1800-$1BFF (or $0800-$0BFF)
|||          101: R5: Select 1 KB CHR bank at PPU $1C00-$1FFF (or $0C00-$0FFF)
|||          110: R6: Select 8 KB PRG ROM bank at $8000-$9FFF (or $C000-$DFFF)
|||          111: R7: Select 8 KB PRG ROM bank at $A000-$BFFF
||+------- Nothing on the MMC3, see MMC6
|+-------- PRG ROM bank mode (0: $8000-$9FFF swappable,
|                                $C000-$DFFF fixed to second-last bank;
|                             1: $C000-$DFFF swappable,
|                                $8000-$9FFF fixed to second-last bank)
+--------- CHR A12 inversion (0: two 2 KB banks at $0000-$0FFF,
                                 four 1 KB banks at $1000-$1FFF;
                              1: two 2 KB banks at $1000-$1FFF,
                                 four 1 KB banks at $0000-$0FFF)

Записью в порт $8000 (и вообще в любой другой четный адрес $8002, $8004 и т.д. до адреса регистра $9FFE) мы записываем "флаг" составленный по таблице выше, и указываем на банк памяти который стоит переключить. Далее в нечетные регистры маппера  $8001 мы записываем "номер" страницы. Слово номер немного не подходит по смыслу но упрощает понимание механизма.

Прежде чем мы перейдем непосредственно к переключению графики, мы должны снова отредактировать конфигурацию линкера, разбить CHR секцию памяти на 5 частей и выделить для каждой части по 16 страниц графики. Я выбрал конфигурацию 128kb/128kb программной и графической памяти, из этого следует что разделив 128/(2+2+1+1+1+1)=16 проще 128/8=16 страниц памяти. Отдельно для каждой страницы 2*16 + 2*16 + 1*16 + 1*16 + 1*16 + 1*16 = 128. Ну все проверили все правильно. Смотрим на конфиг который получился для линкера

Конфигурация полностью
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;

  CHR_2KB_1: start=$0000, size=$32768;
  CHR_2KB_2: start=$0800, size=$32768;
  CHR_1KB_1: start=$1000, size=$16384;
  CHR_1KB_2: start=$1400, size=$16384;
  CHR_1KB_3: start=$1800, size=$16384;
  CHR_1KB_4: start=$1C00, size=$16384;
}

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_2KB_1_1: load=CHR_2KB_1, type=ro, align=16, optional=yes;
  CHR_2KB_1_2: load=CHR_2KB_1, type=ro, align=16, optional=yes;
  CHR_2KB_1_3: load=CHR_2KB_1, type=ro, align=16, optional=yes;
  CHR_2KB_1_4: load=CHR_2KB_1, type=ro, align=16, optional=yes;
  CHR_2KB_1_5: load=CHR_2KB_1, type=ro, align=16, optional=yes;
  CHR_2KB_1_6: load=CHR_2KB_1, type=ro, align=16, optional=yes;
  CHR_2KB_1_7: load=CHR_2KB_1, type=ro, align=16, optional=yes;
  CHR_2KB_1_8: load=CHR_2KB_1, type=ro, align=16, optional=yes;
  CHR_2KB_1_9: load=CHR_2KB_1, type=ro, align=16, optional=yes;
  CHR_2KB_1_10: load=CHR_2KB_1, type=ro, align=16, optional=yes;
  CHR_2KB_1_11: load=CHR_2KB_1, type=ro, align=16, optional=yes;
  CHR_2KB_1_12: load=CHR_2KB_1, type=ro, align=16, optional=yes;
  CHR_2KB_1_13: load=CHR_2KB_1, type=ro, align=16, optional=yes;
  CHR_2KB_1_14: load=CHR_2KB_1, type=ro, align=16, optional=yes;
  CHR_2KB_1_15: load=CHR_2KB_1, type=ro, align=16, optional=yes;
  CHR_2KB_1_16: load=CHR_2KB_1, type=ro, align=16, optional=yes;

  CHR_2KB_2_1: load=CHR_2KB_2, type=ro, align=16, optional=yes;
  CHR_2KB_2_2: load=CHR_2KB_2, type=ro, align=16, optional=yes;
  CHR_2KB_2_3: load=CHR_2KB_2, type=ro, align=16, optional=yes;
  CHR_2KB_2_4: load=CHR_2KB_2, type=ro, align=16, optional=yes;
  CHR_2KB_2_5: load=CHR_2KB_2, type=ro, align=16, optional=yes;
  CHR_2KB_2_6: load=CHR_2KB_2, type=ro, align=16, optional=yes;
  CHR_2KB_2_7: load=CHR_2KB_2, type=ro, align=16, optional=yes;
  CHR_2KB_2_8: load=CHR_2KB_2, type=ro, align=16, optional=yes;
  CHR_2KB_2_9: load=CHR_2KB_2, type=ro, align=16, optional=yes;
  CHR_2KB_2_10: load=CHR_2KB_2, type=ro, align=16, optional=yes;
  CHR_2KB_2_11: load=CHR_2KB_2, type=ro, align=16, optional=yes;
  CHR_2KB_2_12: load=CHR_2KB_2, type=ro, align=16, optional=yes;
  CHR_2KB_2_13: load=CHR_2KB_2, type=ro, align=16, optional=yes;
  CHR_2KB_2_14: load=CHR_2KB_2, type=ro, align=16, optional=yes;
  CHR_2KB_2_15: load=CHR_2KB_2, type=ro, align=16, optional=yes;
  CHR_2KB_2_16: load=CHR_2KB_2, type=ro, align=16, optional=yes;

  CHR_1KB_1_1: load=CHR_1KB_1, type=ro, align=16, optional=yes;
  CHR_1KB_1_2: load=CHR_1KB_1, type=ro, align=16, optional=yes;
  CHR_1KB_1_3: load=CHR_1KB_1, type=ro, align=16, optional=yes;
  CHR_1KB_1_4: load=CHR_1KB_1, type=ro, align=16, optional=yes;
  CHR_1KB_1_5: load=CHR_1KB_1, type=ro, align=16, optional=yes;
  CHR_1KB_1_6: load=CHR_1KB_1, type=ro, align=16, optional=yes;
  CHR_1KB_1_7: load=CHR_1KB_1, type=ro, align=16, optional=yes;
  CHR_1KB_1_8: load=CHR_1KB_1, type=ro, align=16, optional=yes;
  CHR_1KB_1_9: load=CHR_1KB_1, type=ro, align=16, optional=yes;
  CHR_1KB_1_10: load=CHR_1KB_1, type=ro, align=16, optional=yes;
  CHR_1KB_1_11: load=CHR_1KB_1, type=ro, align=16, optional=yes;
  CHR_1KB_1_12: load=CHR_1KB_1, type=ro, align=16, optional=yes;
  CHR_1KB_1_13: load=CHR_1KB_1, type=ro, align=16, optional=yes;
  CHR_1KB_1_14: load=CHR_1KB_1, type=ro, align=16, optional=yes;
  CHR_1KB_1_15: load=CHR_1KB_1, type=ro, align=16, optional=yes;
  CHR_1KB_1_16: load=CHR_1KB_1, type=ro, align=16, optional=yes;

  CHR_1KB_2_1: load=CHR_1KB_2, type=ro, align=16, optional=yes;
  CHR_1KB_2_2: load=CHR_1KB_2, type=ro, align=16, optional=yes;
  CHR_1KB_2_3: load=CHR_1KB_2, type=ro, align=16, optional=yes;
  CHR_1KB_2_4: load=CHR_1KB_2, type=ro, align=16, optional=yes;
  CHR_1KB_2_5: load=CHR_1KB_2, type=ro, align=16, optional=yes;
  CHR_1KB_2_6: load=CHR_1KB_2, type=ro, align=16, optional=yes;
  CHR_1KB_2_7: load=CHR_1KB_2, type=ro, align=16, optional=yes;
  CHR_1KB_2_8: load=CHR_1KB_2, type=ro, align=16, optional=yes;
  CHR_1KB_2_9: load=CHR_1KB_2, type=ro, align=16, optional=yes;
  CHR_1KB_2_10: load=CHR_1KB_2, type=ro, align=16, optional=yes;
  CHR_1KB_2_11: load=CHR_1KB_2, type=ro, align=16, optional=yes;
  CHR_1KB_2_12: load=CHR_1KB_2, type=ro, align=16, optional=yes;
  CHR_1KB_2_13: load=CHR_1KB_2, type=ro, align=16, optional=yes;
  CHR_1KB_2_14: load=CHR_1KB_2, type=ro, align=16, optional=yes;
  CHR_1KB_2_15: load=CHR_1KB_2, type=ro, align=16, optional=yes;
  CHR_1KB_2_16: load=CHR_1KB_2, type=ro, align=16, optional=yes;

  CHR_1KB_3_1: load=CHR_1KB_3, type=ro, align=16, optional=yes;
  CHR_1KB_3_2: load=CHR_1KB_3, type=ro, align=16, optional=yes;
  CHR_1KB_3_3: load=CHR_1KB_3, type=ro, align=16, optional=yes;
  CHR_1KB_3_4: load=CHR_1KB_3, type=ro, align=16, optional=yes;
  CHR_1KB_3_5: load=CHR_1KB_3, type=ro, align=16, optional=yes;
  CHR_1KB_3_6: load=CHR_1KB_3, type=ro, align=16, optional=yes;
  CHR_1KB_3_7: load=CHR_1KB_3, type=ro, align=16, optional=yes;
  CHR_1KB_3_8: load=CHR_1KB_3, type=ro, align=16, optional=yes;
  CHR_1KB_3_9: load=CHR_1KB_3, type=ro, align=16, optional=yes;
  CHR_1KB_3_10: load=CHR_1KB_3, type=ro, align=16, optional=yes;
  CHR_1KB_3_11: load=CHR_1KB_3, type=ro, align=16, optional=yes;
  CHR_1KB_3_12: load=CHR_1KB_3, type=ro, align=16, optional=yes;
  CHR_1KB_3_13: load=CHR_1KB_3, type=ro, align=16, optional=yes;
  CHR_1KB_3_14: load=CHR_1KB_3, type=ro, align=16, optional=yes;
  CHR_1KB_3_15: load=CHR_1KB_3, type=ro, align=16, optional=yes;
  CHR_1KB_3_16: load=CHR_1KB_3, type=ro, align=16, optional=yes;

  CHR_1KB_4_1: load=CHR_1KB_4, type=ro, align=16, optional=yes;
  CHR_1KB_4_2: load=CHR_1KB_4, type=ro, align=16, optional=yes;
  CHR_1KB_4_3: load=CHR_1KB_4, type=ro, align=16, optional=yes;
  CHR_1KB_4_4: load=CHR_1KB_4, type=ro, align=16, optional=yes;
  CHR_1KB_4_5: load=CHR_1KB_4, type=ro, align=16, optional=yes;
  CHR_1KB_4_6: load=CHR_1KB_4, type=ro, align=16, optional=yes;
  CHR_1KB_4_7: load=CHR_1KB_4, type=ro, align=16, optional=yes;
  CHR_1KB_4_8: load=CHR_1KB_4, type=ro, align=16, optional=yes;
  CHR_1KB_4_9: load=CHR_1KB_4, type=ro, align=16, optional=yes;
  CHR_1KB_4_10: load=CHR_1KB_4, type=ro, align=16, optional=yes;
  CHR_1KB_4_11: load=CHR_1KB_4, type=ro, align=16, optional=yes;
  CHR_1KB_4_12: load=CHR_1KB_4, type=ro, align=16, optional=yes;
  CHR_1KB_4_13: load=CHR_1KB_4, type=ro, align=16, optional=yes;
  CHR_1KB_4_14: load=CHR_1KB_4, type=ro, align=16, optional=yes;
  CHR_1KB_4_15: load=CHR_1KB_4, type=ro, align=16, optional=yes;
  CHR_1KB_4_16: load=CHR_1KB_4, type=ro, align=16, optional=yes;
}

Немного может ввести в заблуждение того что адреса банков разные.

  CHR_2KB_1: start=$0000, size=$32768;
  CHR_2KB_2: start=$0800, size=$32768;
  CHR_1KB_1: start=$1000, size=$16384;
  CHR_1KB_2: start=$1400, size=$16384;
  CHR_1KB_3: start=$1800, size=$16384;
  CHR_1KB_4: start=$1C00, size=$16384;

Размеры банков вычисляются путем  умножение количество страниц на размер банка, к примеру для 2кб банка это будет 16*2048 = 32768 и соответственно 16*1024 = 16384.

Далее я сделал по 16 chr файлов для каждого банка и сложил их в отдельной директории, в отдельный файл asm определил все секции и подключил их. Теперь есть большая база графики.

Для большей визуализации банков приведу пример chr из игры super mario bros если бы он был разделен на банки.

Видите каждый банк вы можете просто заменить своей страницей и составить новый графический файл.

Теперь, когда мы разобрались в конфигурации, банках и страницах. Нам необходимо понять еще очень важную вещь как расположены страницы в памяти, в памяти они по факту склеиваются в один файл. По этому мы должны при переключение указать начало графики в байтах к примеру $20 (32) и тут если выбран банк 2кб то будут взята графика в размере 2кб от данного адреса то есть с $20-$22 (32-34 байта). Есть еще одна особенность мы должны указывать начальный адрес страницы полностью.

Довольно длинное вступление и подготовка, и так, для того что бы переключить chr мы просто должны сделать следующее:

.proc startChr
    LDA #$00 ; 1 2kb банка на первую страницу
    STA $8000
    LDX #$00 ; оно как бы по умолчанию но я просто привел пример в статье
    STX $8001

    LDA #$01 ; 2 2kb банка
    STA $8000
    LDX #$02 ; указание на вторую страницу 00 + 2кб = 02
    STX $8001

    LDA #$02 ; 3 страниц 1kb памяти
    STA $8000
    LDX #$40 ; указывает на первую страницу 1кб 0 + 32 + 32 = 64 в хекс $40
    STX $8001

    RTS
.endproc

В качестве заключения как обычно, ссылки на предыдущие статьи об MMC3:

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

Комментарии (0)