logo

Маппинг памяти NES

Введение.
Существует множество документов, повествующих на тему 6502 и ассемблерного хакинга игр NES. Они хорошо описывают как можно модифицировать игровой код, чтобы он делал что-нибудь новое. Некоторые даже рассказывают как вставить свой собственный код и прыгнуть к нему, добавляя к своему хаку новые возможности. Однако есть область, играющая решающее значение в понимании всего этого, которая либо мало объяснена, либо не освещена вообще. У читателей возникало множество вопросов.
Я говорю, разумеется, о мапперах. Часто встречаются такие выражения, как “маппер” или “банкование PRG”, однако мало кто знает, что это означает или как это работает. Если вы не принадлежите к этой группе людей, надеюсь, документ прольёт свет на этот вопрос.
В основном, этот документ будет рассказывать о переключениях PRG/CHR банков мапперами. Другие аспекты, связанные с мапперами (такие как генерирование IRQ, зеркалирование, звук и т.п.) здесь описаны не будут.

Что вам нужно знать? Этот документ не будет рассказывать о процессоре 6502. Предполагается, что читатель знаком с ним, сомневаюсь, что большая часть информации будет вам полезна, если это не так.
Однако вы можете ничего не знать об архитектуре NES и документ немного расскажет об основных областях взаимодействия маппера и приставки.
Хотя и предполагается, что вы не очень опытны в этом вопросе, но чтобы хорошо усвоить информацию, возможно, вам придётся несколько раз перечитать документ.
Смещения в ROMе против Адресов CPU
Смещение в ROMе и адрес CPU (центральный процессор NES) - две абсолютно разные вещи. Первый показывает позицию в .nes файле, тогда как второй показывает положение в памяти CPU. Мапперы - ключ к переходу от первого ко второму и наоборот. В этом документе… Смещения в ROMе будут с префиксом ‘0x’, а адреса CPU - с ‘$’.
Между прочим, некоторые документы и туториалы ошибочно называют адреса CPU “смещениями в RAM (ОЗУ)” (фактически, это можно увидеть и в FCEUXD). Хочется поправить авторов этих документов, так как этот термин неверен и не подходит к нашей ситуации (мы не всегда имеем дело именно с RAM и это не совсем “смещение” от чего-то). Убедительно прошу удерживаться от употребления такого термина, и просто использовать слово “адрес” для адресов CPU и “смещение” для смещений ROMа. Может показаться, что тут большой разницы нет, но в дальнейшем могут возникнуть сложности, если нужно будет различать что является RAM, а что нет (и когда приходится иметь дело с настоящими смещениями в RAM).

Взгляд на распределение памяти NES .
Первый шаг к пониманию первичной функции мапперов (маппинг памяти) - изучение распределения памяти NES. Возможно, вы видели подобные таблицы и в других документах по NES, но обычно в них пространства картриджа и NES слиты воедино: Распределение памяти CPU NES:

     +-------------+-----------------------------+

     | Адрес       |  Закреплено за              |

     +-------------+-----------------------------+

     |$0000 - $1FFF|  RAM (ОЗУ) системы          |

     |$2000 - $3FFF|  Регистры PPU               |

     |$4000 - $401F|  Регистры CPU/pAPU/Джойстика|

     |$4020 - $FFFF|  Картридж                   |

     +-------------+-----------------------------+

NES не может адресовать больше, чем $FFFF
Типичное распределение памяти картриджа:

     +-------------+----------------------------------------+

     | Адрес       |  Закреплено за                         |

     +-------------+----------------------------------------+

     |$4020 - $5FFF|  Unused                                |

     |$6000 - $7FFF|  PRG-RAM (aka SRAM)                    |

     |$8000 - $FFFF|  PRG-ROM (т.е. непостредственно игра)  |

     +-------------+----------------------------------------+

Так обстоит дело в подавляющем большинстве игр NES. Распределение памяти CPU, описаное выше, *всегда* одно и то же. Однако распределение памяти картриджа может сильно разниться в зависимости от типа маппера и картриджа.
Что это означает? Ну, это значит, что каждый раз, когда игра читает или записывает в ячейку ниже $4020, она ничего не делает внутри приставки. Но это означает, что игра получает доступ к чему-то на картридже и это предмет вешательства маппера:

LDA $0300   ;  чтение из системного ОЗУ NES
LDA $8123   ;  чтение из картриджа (скорее всего, PRG-ROM)

Взгляд на .nes ROM файл
Следующий шаг к пониманию как маппер распределяет информацию картриджа - понять что он непосредственно распределяет. Здесь все просто… .nes файл состоит из трех основных частей:

  1. iNES Хэдер (заголовок)
  2. PRG-ROM
  3. CHR-ROM (если он есть у игры)

Размеры PRG и CHR ROMов указыны в заголовке.
iNES заголовок размером в $10 байт. Это означает, что PRG-ROM начинается по смещению 0x00010 в файле .nes.

Зачем нужен маппер?
В качестве первого примера… Разберем небольшую и простую игру, вроде Super Mario Bros. У этого ROMа один PRG банк размером 32K. Это легко увидеть, загрузив игру в FCEUXD, и перейдя: Help | Message Log (там будет сказано “2 x 16KiB”)
32K - это $8000 байт. Этот объем ИДЕАЛЬНО впишется в обычную память картриджа. Для этого частного случая… Маппер не нужен вовсе, потому что весь PRG может быть полностью размещен в отведенное пространство: 8000−FFFF. Это означает, что если игра читает из ячейки по адресу $8000, то чтение производится по по смещению 0x00010 (первый байт PRG-ROM).
Однако, заметьте, что это пространство - ВСЯ память, которая предназначена для PRG-ROM. Тогда как же игры способны разместить гораздо больше PRG? Это достигается действиями мапперов, называемыми переключением PRG банков. Но перед тем, как перейти к переключению PRG банков… Давайте начнём с чего-нибудь понагляднее…

Переключение CHR банков
Переключение CHR банков работает по тому же принципу, что и PRG, только… Здесь переключаются CHR банки, а не PRG, и это гораздо нагляднее.
Игра Super Mario Bros. 2 - хороший пример. Загрузите ROM в FCEUXD и начните первый уровень, затем откройте PPU Viewer (Tools | PPUViewer). Теперь загрузите ROM в ваш любимый тайловый редактор и перейдите по смещению 0x26010. Заметьте как анимируются тайлы в нижней части правой таблицы тайлов в FCEUXD. Если вы теперь обратите внимание на то, как расположены анимирующие тайлы в тайловом редакторе, то станет ясно: игра просто циклически меняет блок тайлов с одного на другой.
Это великолепная демонтрация того, что понимается под переключение банков и как оно работает. Попробуйте увидеть в таблицах тайлов не кучу разных тайлов, а некоторые “слоты”:

    +----------------------------++----------------------------+

   |           Слот 0           ||           Слот 4           |

   |                            ||                            |

   +----------------------------++----------------------------+

   |           Слот 1           ||           Слот 5           |

   |                            ||                            |

   +----------------------------++----------------------------+

   |           Слот 2           ||           Слот 6           |

   |                            ||                            |

   +----------------------------++----------------------------+

   |           Слот 3           ||           Слот 7           |

   |                            ||                            |

   +----------------------------++----------------------------+

В каждом слоте по 4 ряда тайлов (64 тайла)… В сумме 1к ($400 байт) в CHR. NES не может “увидеть” больше 8к за раз (больше 8 слотов). Однако… Игры могут иметь и больше графики, подсовывая разные “банки” CHR в эти слоты. Когда игре нужно подложить другой CHR банк в слот, она “переключает” на новый банк. Банк, который был раньше видимым, теперь убран и видимым становится новый банк.
SMB2 постоянно переключает банки в слотах 6 и 7 - получается, что тайлы в этих слотах анимируются. Такой способ анимации довольно распространен в играх NES. Сами банки можно увидеть в тайловом редакторе. CHR-ROM в SMB2 начинается по смещению 0x20010, это значит:

банк $00 = 0x20010 - 0x2040F   (первые 64 тайла)
банк $01 = 0x20410 - 0x2080F   (следующие 64 тайла)
банк $02 = 0x20810 - 0x20C0F
...
банк $7F = 0x3FC10 - 0x4000F   (последние 64 тайла)

(Все это справедливо для банков размером в 1k)

Замечание о CHR-RAM:
Некоторые игры используют вместо CHR-ROM CHR-RAM (у них совсем нет CHR-ROM, а вся графика содержится в PRG. Это можно встретить в таких играх, как Castlevania и Final Fantasy). Для таких игр все вышесказанное несправедливо.Работа с CHR-RAM несколько отличается от работы с CHR-ROM и это тема отдельного документа (скорее, это связано с регистрами PPU, чем с мапперами).

Переключение PRG банков
Многие в состоянии легко усвоить суть переключения CHR банков, но испытывают сложности с PRG. Это довольно странно, потому что суть ОДНА И ТА ЖЕ. Только нужно смотреть на данные по адресам 8000−FFFF CPU как на большой кусок данных из PRG… Взгляните на эту серию слотов. Это типичное распределение памяти CPU NES:


     $0000  +-------------------+      $8000  +-----------------+

           |                   |             |                 |

           |  RAM системы      |             |  PRG Слот 0     |

           |                   |             |                 |

    $2000  +-------------------+      $A000  +-----------------+

           |                   |             |                 |

           | Регистры PPU      |             |  PRG Слот 1     |

           |                   |             |                 |

    $4000  +-------------------+      $C000  +-----------------+

           |Регистры CPU и т.д.|             |                 |

    $5000  +-------------------+             |  PRG Слот 2     |

           |  Не используется  |             |                 |

    $6000  +-------------------+      $E000  +-----------------+

           |                   |             |                 |

           |  SRAM             |             |  PRG Слот 3     |

           |                   |             |                 |

           +-------------------+             +-----------------+

Точно так же, как при переключении CHR банков слоты заполнялись банками, состоящими из тайлов… При переключении PRG банков слоты заполняются банками PRG.
NES не может “увидеть” больше 32k PRG данных за раз… Однако, переключая различные PRG банки, возможен доступ к гораздо большему, чем 32k объему данных.
Аналогично тому, как расположены CHR банки в .nes файле, PRG банки:

банк $00 = 0x00010 - 0x0200F
банк $01 = 0x02010 - 0x0400F
банк $02 = 0x04010 - 0x0600F
...
и т.д.

(Все это справедливо для банков размером в 8k)

Перевод из Смещения ROM в Адрес CPU и наоборот
Ассемблерный код 6502 и игры NES умеют обращаться только с адресами CPU. И никогда не имеют дела со смещениями в ROM. Поэтому указатели NES так непонятны для новичков. Например, человек получает указатель “63 91” и не может понять, почему он указывает на ячейку по смещению 0x15173. Если принять во внимание все вышесказанное, то все становится предельно ясно:

$0A * $2000 = 0x14000      (№ банка * размер банка = начало банка)
0x14000 + $1163 = 0x15163  (начало банка + объем байтов в слоте = смещение)
0x15163 + 0x10 =  0x15173  (прибавили размер заголовка iNES)

Этот же самый поинтер, тем не менее, мог бы указывать и на 0x0D173, и на 0x05173, и на 0x11173. Все зависит от того, какой банк переключен.
Итак, как же узнать какой банк включен сейчас? Это довольно просто выяснить в FCEUXD. Просто откройте хексредактор (Tools | Hex Editor), перейдите View | NES Memory, затем к интересующему вас адресу CPU, щелкните на нем правой кнопкой, выберите “Go Here In ROM File”. Редактор перейдет в режим показа данных ROM и покажет вам смещение нужного PRG банка.
Однако, игры ПОСТОЯННО переключают PRG банки. Не редкость, когда банки переключаются несколько раз за фрейм. Поэтому рекомендуется остановить отладчик в нужное время перед тем, как выяснять какой банк включен в данный момент. И помните: то, что банк включен сейчас не означает, что он будет включен по этому же адресу и позднее. Углубленно на эту тему можно прочитать в моем документе по поинтерам NES.

Различные размеры слотов, переключаемые и непереключаемые слоты
До этого момента мы рассматривали примеры с банками по 8k. Потому что самый распространенный маппер (MMC3, mapper 4) использует PRG банки объемом 8k. Однако другие мапперы могут использовать банки иных размеров. А некоторые даже позволяют игре ВЫБИРАТЬ размер банков.
Разные размеры банков работают довольно предсказуемо. Банки по 16k ($4000 байт) будут храниться в ROM так:

банк $00 = 0x00010 - 0x0400F
банк $01 = 0x04010 - 0x0800F
... и т.д.

А банки по 32k ($8000 - да, некоторые мапперы оперируют такими большими банками) будут храниться так:

банк $00 = 0x00010 - 0x0800F
банк $01 = 0x08010 - 0x1000F
... и т.д.

Итак, слоты соотносятся следующим образом:


            8k         16k         32k

$8000   +--------+  +--------+  +--------+

        |        |  |        |  |        |

$A000   +--------+  |        |  |        |

        |        |  |        |  |        |

$C000   +--------+  +--------+  |        |

        |        |  |        |  |        |

$E000   +--------+  |        |  |        |

        |        |  |        |  |        |

        +--------+  +--------+  +--------+

Мапперы не всегда позволяют выбирать игре в какой слот переключать банк. Некоторые слоты фиксированы и в них всегда содержится один и тот же банк. Обычно эти слоты называют “hardwired” слотами (этот термин не совсем корректен… Скорее, это “fixed” слоты). Обычно последний слот жестко закрепляется за последним банком PRG, который не может быть переключен. Все зависит от того, какой маппер применен и, возможно, от установок маппера.

Специфичные особенности некоторых мапперов
Все, что было сказано до этого момента относится ко всем мапперам. Однако каждый маппер имеет свои небольшие отличия в расположении слотов, способности переключения PRG/CHR банков, переключаемых и непереключаемых слотах и даже несколько других особенностей, не описанных в данном документе. И так как изложенная информация довольно общая, наверное, вы хотели бы знать детали.
Итак, вот короткий список несокльких самых распространенных мапперов и их особенностей. Это ни в коем случае не полный список, я и не пытаюсь таковой создать. Есть и другие источники по этой информации, так что если ваша игра, имеет другой маппер, не описанный здесь, вам придется поискать где-нибудь еще.
Этот список также не содержить детального описания регистров мапперов. Если вам нужно знать как вручную переключить банк или сделать что-нибудь другое, вам лучше обратиться к другому техническому документу по вашему мапперу. Выяснить какой маппер у вашей игры довольно просто. Многие эмуляторы выдают информацию по загруженному ROM. В FCEUXD после загрузки ROM, нужно перейти Help | Message Log, там можно увидеть и номер маппера.

Замечание напоследок
Когда речь идет о мапперах… Речь идет обо всем. “Маппер” - это практически комбинация схем картриджа, соединеняющих элементов, контроллера памяти и всего остального, что есть на картридже. Теоретически, на картридже возможно размещение дополнительного процессора!
Таким образом “правил” для мапперов существовать не может. Весь этот материал нужно воспринимать, постоянно держа в голове, что он не высечен в камне… И что есть мапперы, которые выделяются из общей массы.