Как стать автором
Обновить

Создание игр для NES на ассемблере 6502: заголовки и векторы прерываний

Время на прочтение12 мин
Количество просмотров4.7K
Автор оригинала: Kevin Zurawel


6. Заголовки и векторы прерываний


Содержание:

  • Заголовки iNES
  • Выделение процедур с помощью .proc
  • Векторы прерываний

В предыдущей главе мы разобрали часть «main» тестового проекта, которая задаёт цвет фона, а затем входит в бесконечный цикл. Однако этот код составляет всего 13 из 44 строк исходного кода тестового проекта. В этой главе мы изучим оставшуюся часть кода тестового проекта и узнаем ещё несколько опкодов.

Заголовки iNES


В самом начале тестового проекта мы видим этот любопытный фрагмент на языке ассемблера:


.segment начинается с точки, а значит, это директива ассемблера — команда, которую должен исполнять ассемблер, когда преобразует код в машинный код. Как мы говорили в Главе 4, .segment сообщает, куда поместить наш код в выходном файле. Сегмент HEADER («заголовок»), как и следует ожидать, помещается в самое начало получившегося файла .nes.

Во второй строке используется ещё одна директива ассемблера, которую мы видели ранее. Директива .byte приказывает ассемблеру вставить в выходной файл байты литералов данных, а не пытаться интерпретировать из них опкоды. Три ASCII-элемента кодового пространства ($4e, $45, $53) — это строка «NES» (представление букв «N», «E» и «S» в кодировке ASCII). Следующий байт, $1a, обозначает символ MS-DOS «конец файла». Эти четыре байта — «магическое число», помечающее выходную информацию как игру для NES.

[У большинства типов файлов в начале файла есть «магическое число», благодаря которому операционной системе проще точно определить, чем является файл. Файлы байт-кода Java начинаются с $cafebabe, файлы PDF начинаются с $25504446 ("%PDF"), а файлы zip — с $504b («PK», потому что раньше формат назывался «PKZIP»). Более полный список магических чисел типов файлов можно найти в Википедии.]

Заголовок с магическим числом «NES» помечает файл как игру для NES «iNES». iNES был одним из первых эмуляторов NES, и стал первым, получившим широкую популярность. Автор iNES Марат Файзуллин создал специальный формат заголовков, снабжающий эмулятор информацией о самой игре, например, о регионе (NTSC/PAL), количестве банков PRG и CHR ROM и другом. Полный заголовок iNES имеет длину 16 байт.

Заголовок нашего тестового проекта после «NES» и $1a указывает, что «картридж» игры содержит два банка PRG-ROM по 16 КБ (накопитель на 32 КБ) и один банк CHR-ROM на 8 КБ, и что в нём используется «нулевой мэппер» (mapper zero). Картриджи NES имеют множество (сотни) вариаций, а заголовок iNES назначает каждой вариации номер. «Нулевой мэппер», как можно догадаться, обозначает простейший из картриджей NES, обычно называемый «NROM». Для некоторых первых игр NES использовались картриджи NROM, например, для Balloon Fight, Donkey Kong и Super Mario Bros.. Так как они относительно просты, в большей части книги мы будем создавать игры для картриджей NROM.

[Подробнее о формате заголовков iNES можно прочитать в статье на NESDev Wiki. Многие данные, которые можно задать в заголовке iNES, например, режимы отзеркаливания и PRG-RAM, будут подробнее рассмотрены в книге.]

Выделение процедур с помощью .proc


После заголовка ещё одна директива .segment переключает нас на сегмент «CODE», который начинается через 16 байт после начала файла (т. е. после 16-байтного заголовка). Первое, что мы видим в этом сегменте — новая директива ассемблера .proc. Эта директива позволяет создавать в коде новые лексические области видимости. По сути, это значит, что метки, создаваемые внутри .proc, являются уникальными для этой proc. Вот пример:


Здесь мы используем метку some_label дважды, сначала в .proc foo, а потом в .proc bar. Однако поскольку они находятся внутри разных процедур, эти две метки считаются абсолютно отдельными. Использование some_label внутри bar будет считаться ссылкой на версию внутри bar, и мы никак не сможем получить доступ к версии метки из foo, находясь внутри bar. Обычно независимые части кода оборачиваются в отдельные процедуры, чтобы в них можно было использовать одинаковые метки без затирания друг друга.

[Может показаться, что нужно слишком много возни только для того, чтобы одно имя метки можно было использовать в нескольких местах, и в какой-то степени это правда. Истинная мощь использования .proc проявляется, когда код состоит из несколько файлов, некоторые из которых могли писать и не вы! Процедуры позволяют безопасно использовать метку без необходимости проверки всего кода на предмет того, использовали ли вы (или другой разработчик) ту же метку.]

В тестовом проекте используются четыре процедуры — irq_handler, nmi_handler, reset_handler и main. В предыдущей главе мы рассмотрели процедуру main, но что же делают остальные?

Векторы прерываний


Как говорилось ранее, процессор консоли NES многократно и последовательно получает и исполняет байты. Однако когда происходят определённые события, мы хотим прервать работу процессора и приказать ему делать что-то другое. События, способные вызывать такие прерывания, называются векторами прерывания, и у NES/6502 есть три таких события:

  • Вектор сброса (reset vector) происходит при первом включении системы или когда пользователь нажимает кнопку Reset на передней панели консоли.
  • Вектор NMI (Non-Maskable Interrupt, «немаскируемое прерывание») происходит, когда PPU начинает подготавливать следующий кадр графики, 60 раз в секунду.
  • Вектор IRQ (Interrupt Request, «запрос прерывания») может быть запущен аудиопроцессором NES или другими видами оборудования картриджа.

При срабатывании прерывания процессор прекращает заниматься тем, что он делает, и исполняет код, указанный как «обработчик» (handler) этого прерывания. Обработчик — это просто фрагмент ассемблерного кода, завершающийся новым опкодом: RTI (Return from Interrupt, «возврат из прерывания»). Так как в тестовом проекте не нужно использовать обработчики NMI или IRQ, они состоят из одного RTI:


RTI помечает конец обработчика прерывания, но как процессор узнаёт, где начинается обработчик конкретного прерывания? Процессор смотрит на последние шесть байтов памяти — адреса с $fffa по $ffff, чтобы найти адрес памяти, с которого начинается каждый обработчик.

Адрес памяти Использование
$fffa-$fffb Начало обработчика NMI
$fffc-$fffd Начало обработчика сброса
$fffe-$ffff Начало обработчика IRQ

Так как эти шесть байтов памяти очень важны, у ca65 есть для них особый типа сегмента: .segment "VECTORS". Чаще всего этот сегмент используется так: ему передаётся список из трёх меток, который ca65 преобразует в адреса при ассемблеровании кода. Вот как выглядит сегмент «VECTORS» нашего тестового проекта:


.addr — это новая для нас директива ассемблера. Для указанной метки она выводит адрес памяти, соответствующий этой метке. То есть эти две строки ассемблерного кода записывают в байты памяти $fffa-$ffff адреса обработчиков NMI, сброса и IRQ — именно в том порядке, который указан в таблице выше. Каждая метка в строке 40 является началом .proc для соответствующего обработчика.

[При первом включении NES процессор 2A03 не начинает с адреса памяти $0000, а выполняет определённую последовательность шагов. Он получает адрес памяти, хранящийся в $fffc и $fffd (адрес начала обработчика сброса). Затем он помещает этот адрес в счётчик команд, благодаря чему начало обработчика сброса становится следующей исполняемой командой. Далее он команда за командой проходит весь обработчик сброса.]

Обработчик сброса


Тестовый проект не использует события NMI и IRQ, но ему нужен обработчик сброса. Задача обработчика сброса заключается в подготовке системы при первом включении и в том, чтобы она возвращалась к состоянию «только что включена», когда пользователь нажимает на кнопку Reset. Вот как выглядит обработчик сброса в тестовом проекте:


Несколько примечаний по этой части кода. Во-первых, в отличие от других обработчиков прерываний, она не заканчивается RTI, потому что при первом включении системы процессор не занимался чем-то другим, поэтому ему некуда «возвращаться». Поэтому вместо этого фрагмент заканчивается JMP main. Мы видели JMP в конце main в предыдущей главе, где JMP forever создавал бесконечный цикл. JMP обозначает «jump» («переход»); этот опкод приказывает перейти в какое-то другое место для получения следующей команды. Операндом для JMP является полный двухбайтный адрес памяти, но он практически всегда используется с меткой, которую ассемблер во время ассемблирования преобразует в адрес памяти. Здесь JMP main приказывает процессору после завершения обработчика сброса начать исполнять код в main.

Во-вторых, этот код содержит множество опкодов, которые мы не видели ранее. Давайте изучим их, построчно разобрав обработчик сброса.

В строках 14 и 15 содержатся два опкода, которые обычно встречаются только в обработчиках сброса. SEI — это «Set Interrupt ignore bit» («задание бита игнорирования прерывания»). Всё, что встречается после SEI и способно вызвать событие IRQ, не будет ничего делать. Обработчик сброса вызывает SEI прежде всего остального, потому что нам не нужно, чтобы наш код переходил к обработчику IRQ до того, как завершит инициализацию системы. CLD обозначает «Clear Decimal mode bit» («сброс бита десятичного режима»), он отключает в 6502 режим двоично-десятичного кода.

[Из-за сложных проблем с лицензированием 6502 и юридической возможностью производства этого процессора компанией Ricoh использованный в NES процессор 2A03 содержит схему режима двоично-десятичного кода, но электрические дорожки, которые должны соединять эти схемы с остальной частью чипа, перерезаны. Поэтому CLD (и обратный ему опкод SED) в NES не делают ничего, но рекомендуется «на всякий случай» использовать вызов CLD в обработчике сброса.]

Следующие три строки возвращают нас к привычным загрузкам и сохранениям. Мы уже видели ранее, что $2001 — это PPUMASK, но $2000 — это что-то новое. Этот адрес обычно называют PPUCTRL, он изменяет работу PPU гораздо более сложным образом, чем PPUMASK, который способен только включать или отключать рендеринг. Мы рассмотрим PPUCTRL позже, когда начнём подробно разбирать, как PPU консоли NES рисует графику. Как и PPUMASK, этот операнд является набором битовых полей. С точки зрения инициализации NES самое важное заключается в том, что бит 7 управляет тем, должно ли PPU вызывать NMI в каждом кадре. Сохранив $00 в PPUCTRL и PPUMASK, мы отключим NMI и рендеринг на экран при запуске, чтобы на экране не отрисовывался случайный мусор.

Оставшаяся часть обработчика сброса — это цикл, ожидающий полного запуска PPU, прежде чем перейти к основному коду в main. После первого включения для стабилизации PPU требуется примерно 30 тысяч тактов ЦП, поэтому этот код многократно получает состояние PPU из PPUSTATUS ($2002), пока тот не сообщит, что устройство готово. Кажется, что 30 тысяч тактов — это много времени, однако процессор 2A03 консоли NES работает с частотой 1,78 МГц, поэтому 30 тысяч тактов — это очень малая доля секунды. В этой главе я не буду рассказывать о BIT или BPL, но мы обязательно вернёмся к ним позже.

Завершив подготовку, мы выполняем переход к .proc main и выполняем код самой игры.

Полный сброс


Используемый в нашем тестовом проекте обработчик сброса — это простейший обработчик, способный надёжно запустить NES в гарантированно хорошем состоянии. Обработчик сброса может выполнять множество других задач, например, подготовку аудиопроцессора (Audio Processing Unit, APU) и очистку ОЗУ. При расширении функциональности наших игр мы будем расширять функции и обработчика сброса.

Поздравляю, мы наконец-то разобрали весь код тестового проекта! Прочитав последние несколько глав, вы можете задаться вопросом: «всё это невероятно сложно, зачем вообще этим заморачиваться?» Следующая глава ответит на ваш вопрос!

7. Зачем вообще этим заниматься??


Мы с вами потратили шесть глав на обсуждение того, как заставить тридцатилетнюю игровую систему показать статичный зелёный фон. (Кстати, спасибо, что остаётесь со мной!) Откровенно говоря, это невероятный объём работы для такого посредственного результата. Возможно, сейчас вы задаётесь вопросом: зачем вообще заморачиваться всей этой вознёй? Прежде чем пойти дальше, я хотел бы напомнить вам, почему стоит вложить время в разработку для Nintendo Entertainment System.

Это Классика


Во-первых, NES — это бесспорная классика. На протяжении почти всех 80-х эта консоль доминировала на рынке видеоигр и в США, и в Японии. Появившись после «краха Atari» 1983 года, она самостоятельно возродила рынок домашних видеоигр США. Целое поколение геймеров с любовью вспоминает эту систему, и даже молодёжь, никогда на ней не игравшая, признаёт её историческую важность.


«Я не могу представить мир без домашних видеоигр. Это как остаться без телевидения или без кинотеатров… Это классика. Как классическая виниловая пластинка. NES — это классическая виниловая видеоконсоль», — Итан Джеймс (19 лет), «Teens React to Nintendo (NES)»

Подходит для одиночных разработчиков и небольших команд


Большинство коммерческих игр для NES было создано небольшими командами. Например, Donkey Kong — игра, разработанная для игры на NES:

Должность Имя
Программист Тосихико Накаго
Дизайнер уровня Кента Усуи
Гейм-дизайнер Сигэру Миямото
Музыка и звуковые эффекты Юкио Канеока
Продюсер Масаюки Уэмура

Игра для Famicom целиком была создана командой из пяти людей, и только один из них являлся «программистом». Многие из лучших игр для NES тоже были сделаны такими же маленькими командами, например, Super Mario Bros.:

Должность Имя
Продюсер/режиссёр/дизайнер Сигэру Миямото
Ассистент режиссёра/дизайнер Такаси Тэдзука
Звук и музыка Кодзи Кондо
Программист Тосихико Накаго
Программист Кадзуаки Морита

Super Mario Bros. — одна из самых знаковых игр для NES всех времён и она, как и Donkey Kong, создавалась командой из пяти людей, и только двое из них были программистами.

Достаточно возможностей, чтобы быть интересным


По сравнению со своим предшественником на рынке домашних видеоигр, Atari 2600, консоль NES даёт разработчику гораздо больше средств творческого самовыражения. Atari 2600 предназначалась для игр наподобие Combat:


В Combat нет музыки, только звуковые эффекты, и ограниченный набор графики, описывающей «игровое поле», на котором происходит действие. Консоль 2600 предоставляла разработчикам доступ к одному слою фона, двум объектам-«спрайтам» (в данном случае танкам), и двум объектам-«шарам» (выпускаемым танками снарядам). Вся остальная графика должна была возникать в воображении игрока.

Игра Donkey Kong, предназначенная для NES, значительно от неё отличается:


В ней есть простая монотонная музыка, а также звуковые эффекты бегущего и прыгающего через бочки Марио. На экране одновременно присутствует множество подвижных объектов (деревянные бочки, горящая бочка с нефтью, анимированный Донки Конг в верхней части экрана). Эти две игры сильно отличаются даже в использовании текста. Combat отображает только одно большое число счёта для каждого игрока; Donkey Kong отображает текущий счёт игрока (шесть разрядов), текущий рекорд счёта, количество оставшихся у игрока жизней, а также сумму «бонусных» очков, которые игрок может заработать, пройдя уровень.

В итоге, эти изменения позволяют NES отображать на экране гораздо больше информации, давая разработчикам свободу в создании гораздо более детализированных и сложных игр, чем это было возможно на Atari 2600.

Достаточно легко, чтобы быть доступным


Хотя NES стала серьёзным шагом вперёд по сравнению с возможностями Atari 2600, она бледнеет в сравнении с собственным потомком, Super Famicom / Super NES. Вот краткий список различий между двумя консолями:

NES SNES
ЦП 8-битный, производный от MOS 6502, 1,79 МГц 16-битный, производный от MOS 65c816, 3,58 МГц
Адресуемая память 64 КБ 16 МБ
ОЗУ 2 КБ 128 КБ
Графическое разрешение 256x240 до 512x478
Доступные цвета 64 32768
Фоновые слои 1, до 512x512 4, каждый до 1024x1024
Звук 5 фиксированных каналов 8 полностью программируемых каналов
Самая большая выпущенная игра 1 МБ 6 МБ

Все эти дополнительные мощности дорого обходятся. Программировать для Super NES гораздо сложнее, чем для более ограниченной в возможностях NES. Наличие большей ёмкости для хранения графических ресурсов вынуждает дизайнеров создавать гораздо больше графики. Потрясающий аудиопроцессор Super NES требует, чтобы композитор хорошо знал его тонкости, потому что разработчики должны передавать процессору звуковые сэмплы, а не просто встроенный набор инструментов. Разработка под Super NES гораздо менее возможна для одиночного разработчика или небольшой команды. Просто сравните штат разработчиков игр для NES с командой, создавшей Super Mario World — игру, предназначенную для Super NES:

Должность Имя
Продюсер Сигэру Миямото
Режиссёр Такаси Тэдзука
Композитор Кодзи Кондо
Директор по дизайну карты Хидэки Конно
Директор по дизайну уровней Кацуя Эгути
Директор по программированию Тосихико Накаго
Программист Марио/систем Тосио Иваваки
Программист объектов Кадзуаки Морита
Программист фонов Сигэхиро Касамацу
Программист фонов Тацунори Такакура
Программист карты Тацуо Нисияма
Ввод данных уровней Йосихиро Номото
Ввод данных уровней Эидзи Ното
Ввод данных уровней Сатору Такахата
Графический дизайн персонажей Сигэфуми Хино

Люди и сегодня продолжают делать игры для NES


Nintendo прекратила производство NES в 1995 году, десять лет спустя после её выпуска в США. Последней официально лицензированной игрой для NES, выпущенной в Северной Америке, была Wario's Woods (1994 год). В последующие годы эмуляция NES повышала своё качество, для системы было выпущено множество инди-игр, часто созданных одиночками или очень небольшими командами. Вот лишь некоторые из «самодельных» игр для NES, демонстрирующие, на что способна система.

Battle Kid: Fortress of Peril, Sivak Games, 2010 год


Battle Kid: Fortress of Peril, выпущенная разработчиком-одиночкой, помогла популяризировать идею разработки самодельных игр для NES.


Kira Kira Star Night DX, RIKI, 2013 год


Эксклюзивный для Famicom проект Kira Kira Star Night DX, по сути, является отличным чиптюн-альбомом с добавленной к нему красивой (и простой) игрой. Первый тираж картриджей был продан за один день.


Twin Dragons, Broke Studio, 2018 год


Twin Dragons — это выпущенный на Kickstarter проект французских разработчиков Broke Studio. Он собрал более 30 тысяч евро, с лёгкостью выполнив все цели кампании по финансированию.


Lizard, Брэд Смит, 2018 год


Lizard — это новая игра для NES, выпущенная и на физическом картридже NES, и в виде цифровой копии через Steam (обёрнутой в эмулятор NES).


Активное сообщество


К счастью, разработчики, продолжающие создавать новые игры для NES — это не какие-то отшельники. Они состоят в широком сообществе фанатов и игроков в NES, расширяющем свои знания о работе NES и о том, как лучше всего создавать для неё игры. Сообщество собрало огромные объёмы справочной информации в NESDev Wiki, а форумы NESDev — отличное место для поиска помощи или для знакомства с современными техниками, используемыми разработчиками.

… Продолжить?


Если за последние несколько глав вас немного напугало то, во что вы собираетесь погрузиться, надеюсь, эта глава снова вас воодушевила. NES — крепкая платформа для разработки, на которой часто появляются новые релизы и открытия, и мне не терпится продолжить знакомить вас с тем, как всё это работает. Давайте двигаться дальше!

Создание игр для NES на ассемблере 6502: рефакторинг
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 7: ↑7 и ↓0+7
Комментарии1

Публикации

Истории

Работа

Ближайшие события