Карта памяти NES
by Shiru 10'15 mailto:shiru at mail dot ru
Одна из первых вещей, которую приходится изучать при освоении низкоуровневого программирования для новой платформы – её карта памяти, то есть где в адресном пространстве находятся ОЗУ, ПЗУ, регистры устройств и тому подобное. Если простота устройства ZX Spectrum, которое можно описать одним предложением – 'нижние 16 килобайт ПЗУ, остальное ОЗУ, экран в начале ОЗУ, весь ввод и вывод через один порт' – позволяет не особо задумываться о самом существовании этого вопроса, то у многих других современных ему платформ дела обстоят несколько сложнее. В особенности это справедливо для NES, карту памяти которой, а также причины именно такой её организации, мы и рассмотрим в этой статье.
Для начала ограничимся только простейшей, 'стандартной' конфигурацией карты памяти, а тему возможных её расширений посредством так называемых 'мапперов' оставим для отдельной статьи. Заодно начнём вводить по ходу дела общепринятые среди современных разработчиков для NES англоязычные термины и сокращения, знание которых пригодится при поиске дополнительных сведений в накопленной любителями информации.
Архитектура
Архитектура NES нетипична для домашних компьютеров, однако свойственна большинству 8-16 битных игровых приставок, компьютерам семейства MSX и их близким родственникам.
В системах с подобной архитектурой есть основной процессор и относительно сложный видеоконтроллер, имеющие отдельные шины адреса и данных, а значит и отдельные независимые адресные пространства и память. При этом процессор и видеоконтроллер не имеют непосредственного доступа к памяти друг друга, а для передачи данных между ними используется порт ввода-вывода, через который процессор просит видеоконтроллер принять или отдать байт данных. Такой подход позволяет избежать неудобств разделяемого одновременного доступа к памяти, типа торможения процессора при проходе луча по экрану, а также повышает гибкость построения систем, позволяя совмещать разные процессоры и видеоконтроллеры. Например, видеоконтроллер NES при желании несложно применить в системах с Z80 или 8080.
Помимо этого, у NES есть пара редких особенностей – все шины выведены на разъём картриджа, позволяя при необходимости модифицировать карту памяти извне, а также в качестве видеопамяти часто применяется ПЗУ, а не ОЗУ. Наличие двух раздельных адресных пространств означает, что стандартный картридж содержит минимум две микросхемы памяти, по одной для процессора и видеоконтроллера.
Процессор
Адресная шина процессора (CPU) имеет 16 линий, размер адресного пространства 65536 байт. У 6502 нет отдельного пространства для портов ввода-вывода, как у Z80 или x86, поэтому все порты устройств отображаются на память, занимая часть адресного пространства. В нём находятся:
$0000..$1fff – внутреннее ОЗУ приставки
$2000..$3fff – регистры видеоконтроллера
$4000..$5fff – регистры синтезатора звука и джойстиков
$6000..$7fff – опциональное внешнее ОЗУ
$8000..$ffff – внешнее ПЗУ программы
В нижних восьми килобайтах адресного пространства находится основное ОЗУ (RAM). Его реальный объём составляет всего два килобайта, поэтому содержимое ОЗУ повторяется в отведённом диапазоне адресов четыре раза. Следующие восемь килобайт занимают регистры видеоконтроллера (PPU). Их всего восемь штук, и они также повторяются в пределах всего диапазона. Принято считать, что следующие восемь килобайт заняты регистрами синтезатора звука (APU) и портов джойстиков, но на самом деле они занимают только 32 байта. Таким образом диапазон $0000..$401f зарезервирован для внутренних устройств приставки.
Подобная неравномерность в распределении адресного пространства объясняется просто. Основное ОЗУ и видеоконтроллер являются для процессора внешними устройствами, их адреса декодируются внешней логикой с шагом в 8 килобайт. Регистры же синтезатора звука и портов джойстиков находятся с процессором на одном кристалле и декодируются полностью. Выбор довольно неоптимального шага в 8 килобайт скорее всего связан с оптимизацией стоимости приставки, для внешней дешифрации адреса используется всего одна микросхема 74139. Возможно, что в какой-то момент при подготовке к производству приставки имело место и урезание планируемого объёма ОЗУ, все виды которого (внутреннее, дополнительное, видеопамять) в NES выполнены на довольно дорогостоящей в те времена SRAM.
Технически весь остальной диапазон адресов, $4020..$ffff, может использоваться устройствами на картридже в любых целях. Но для декодирования адресов в нижних 32 килобайтах требуется применение дополнительных микросхем, поэтому область $4020..$5fff на практике никогда не задействовалась, а область $6000..$7fff опционально использовалась для дополнительного ОЗУ (WRAM, оно же PRG RAM). Для подключения последнего также требуется дополнительная микросхема мелкой логики.
ПЗУ кода (PRG ROM) традиционно располагается в верхней половине адресного пространства, $8000..$ffff. В отличие от многих других платформ оно всегда только внешнее, встроенного ПЗУ у приставки нет, а значит никакой код до запуска игры не выполняется. 'Верхнее' расположение ПЗУ является обычным для процессора 6502, которому для нормальной работы требуется, чтобы в адресах $fffa..$ffff неизменно находились шесть байт так называемых 'векторов' – адреса сброса, обработчиков NMI и IRQ. На слоте картриджа есть готовый сигнал выборки верхней половины памяти, что позволяет использовать до 32 килобайт ПЗУ без необходимости декодирования адреса с помощью дополнительных микросхем. Также система аппаратного проигрывания DPCM-звука рассчитана на расположение данных в диапазоне $c000..$ffff.
Видеосистема
Адресное пространство видеоконтроллера имеет размер 16384 байт, так как его шина адреса имеет только 14 линий. Распределяется оно следующим образом:
$0000..$1fff – внешняя память для графики тайлов
$2000..$3eff – память тайловых карт
$3f00..$3fff – палитра
Первую половину, $0000..$$1fff, занимают адреса для графической памяти, располагаемой на картридже. Иначе говоря, это та область, данные из которой видеоконтроллер использует как изображения тайлов. Область условно делится на два набора по 256 тайлов. Графическая память может быть выполнена на ПЗУ или ОЗУ, при этом она называется соответственно CHR ROM или CHR RAM. Сокращение CHR происходит от слова 'Character' (символ), так как слой фона по сути представляет собой обычный аппаратный текстовый режим, а тайлы можно считать символами знакогенератора.
Далее следует память тайловых карт (Nametable), $2000..$3eff. Данные из этой области видеоконтроллер использует аналогично буферу текста в аппаратном текстовом режиме. Для организации тайловых карт нескольких разных размеров, от 32x30 до 64x60, может использоваться до четырёх килобайт этой памяти. Но сама приставка имеет только два килобайта видео-ОЗУ, что вероятно опять же связано с оптимизацией стоимости. Эта память может быть расширена или подменена снаружи, причём для её расширения может использоваться и ПЗУ. Но на практике расширение памяти тайловых карт встречалось крайне редко. Для выбора формата тайловой карты с использованием имеющихся двух килобайт (так называемый 'mirroring') применялась жёстко заданная перемычкой или управляемая маппером коммутация адресных линий на плате картриджа.
Тайловая карта может состоять из одной или нескольких однокилобайтных страниц размером 32x30 тайлов. Первые 960 байт содержат линейный буфер с однобайтными номерами тайлов. Оставшиеся 64 байта – область атрибутов, назначающая палитры блокам 2x2 тайла, по два бита на блок. В зависимости от выбранного формата тайловой карты расположение однокилобайтных страниц в адресном пространстве меняется, они могут находиться по адресам $2000, $2400, $2800, $2c00.
Остаток отведённого для тайловых карт диапазона адресов, $3000..$3eff, никак не задействуется видеоконтроллером и не несёт полезной функции, в нём просто дублируются предыдущие четыре килобайта.
Диапазон $3f00..$3fff занят палитрой. Эта память находится внутри видеоконтроллера и не может быть расширена. Палитра занимает всего 32 байта и повторяется по всему диапазону адресов.
Можно заметить, что нигде в адресном пространстве PPU не встречается память списка спрайтов (Object Attribute Memory, OAM). Она находится внутри видеоконтроллера и имеет своё собственное отдельное адресное пространство, с отдельными портами доступа к нему, а также простейшим контроллером DMA. Это единственный вид памяти на NES, выполненный на DRAM, если не считать регистры 6502.
Внутреннее видео-ОЗУ приставки разрешается внешним сигналом, обычно соединяемым на плате картриджа с линией PPU A13, что помещает его в отведённый для тайловых карт диапазон. Поэтому считается, что внутреннее видео-ОЗУ предназначено только для хранения тайловых карт. Однако, при желании его можно использовать и для хранения графики тайлов, просто установив постоянный сигнал разрешения, и таким образом получив экзотическую конфигурацию картриджа всего с одной микросхемой памяти. На практике этот трюк осуществлялся только любителями.
Распределение ОЗУ
В принципе, программист волен распределять объём основного ОЗУ любым удобным ему образом. Но устройство процессора и видеоконтроллера NES, а также некоторые решения, всегда требующиеся в играх, сформировали типовое распределение основного ОЗУ, знание которого пригодится при планировании распределения памяти в своих программах.
$0000..$00ff нулевая страница
$0100..$01ff страница стека
$0200..$07ff основное ОЗУ
$xx00..$xxff копия списка спрайтов
Так как 6502 является по-настоящему восьмибитным процессором, адресное пространство для его внутреннего устройства является набором 256-байтных страниц. За первыми двумя из них жёстко закреплены особые роли – это так называемая 'нулевая страница' (Zero Page, ZP) и страница стека.
К нулевой странице могут обращаться как обычные команды чтения и записи памяти, так и команды с укороченной, однобайтовой адресацией. Они на один байт короче и на один такт быстрее. Также единственный способ косвенной 16-битной адресации (то есть аналог привычных ld a,(hl) и ld (hl),a на Z80) требует хранения адреса в двух байтах нулевой страницы. Поэтому для более эффективной работы кода требуется тщательно распределять место в ней, отдавая его под часто используемые переменные, счётчики циклов и 16-битные указатели.
В странице стека располагается собственно стек. Он имеет однобайтовую адресацию, зациклен в пределах своей страницы и растёт вниз. Так как команды работы со стеком на 6502 несколько менее эффективны, чем команды работы с нулевой страницей, стек в программах используется не очень активно, и нередко значительная часть страницы стека пустует. Например, во всех моих играх стек занимал не более 32 байт. Так как объём ОЗУ довольно ограничен, имеет смысл задействовать часть страницы стека под хранение переменных.
Процессор не накладывает ограничений на распределение остальных 1536 байт ОЗУ, они могут быть использованы в любых целях. Однако, для нормальной работы видеоконтроллера требуется в начале каждого кадра обновлять внутренний 256-байтный список спрайтов. Видеоконтроллер предоставляет возможность побайтовой записи в память списка спрайтов аналогично доступу к видеопамяти, но на практике она используется очень редко, так как неполное обновление списка спрайтов вызывает визуальные артефакты, а само такое обновление работает довольно медленно. Существенно более быстрый и надёжный способ обновления списка спрайтов – через устройство OAM DMA, которое требует расположения копии списка в основном ОЗУ по адресу, кратному 256, и обновляет только список целиком. Поэтому большинство игр использует одну из страниц ОЗУ под копию списка спрайтов. Часто это вторая страница, с адресом $0200.
Ещё один типовой потребитель ОЗУ, который заслуживает упоминания – проигрыватель музыки и звуковых эффектов, присутствующий практически в каждой игре. Традиционно под его переменные в программах отводится одна из страниц ОЗУ. В зависимости от сложности проигрывателя переменные могут занимать от сотни байт до страницы целиком, плюс несколько байт в нулевой странице. Оставшееся место в странице может использоваться в других целях. Если переменные занимают пару сотен байт или меньше, они становятся хорошим кандидатом на размещение в странице стека.