![image](https://habrastorage.org/getpro/habr/post_images/6d8/4e4/b59/6d84e4b592725f286fee115f86ba0ef8.png)
Оглавление
Оглавление
Часть I: подготовка
- Введение
- 1. Краткая история NES
- 2. Фундаментальные понятия
- 3. Приступаем к разработке
- 4. Оборудование NES
- 5. Знакомство с языком ассемблера 6502
- 6. Заголовки и векторы прерываний
- 7. Зачем вообще этим заниматься?
- 8. Рефакторинг
Часть II: графика
2. Фундаментальные понятия
Содержание:
- Работа с данными
- Регистры процессора
- Память
- Как задаются данные
- Как сделать данные человекочитаемыми
- Соединяем всё вместе
Что такое компьютер?
Вопрос кажется простым, но он затрагивает самую суть того, что делаем мы как программисты. Пока скажем, что «компьютер» — это нечто, исполняющее программу. «Программа» — это просто последовательность команд, а под исполнением программы подразумевается, что команды выполняются с начала и одна за другой. (Если вы читаете программу и сами исполняете команды, то поздравляю! Вы — компьютер!)
У каждого компьютера есть конкретный набор команд, которые он умеет исполнять. Мы называем его набором команд компьютера (да, очень оригинальное название). Набор команд можно описать множеством способов, но пока давайте будем считать, что команды в наборе команд обозначены числами. То есть программа — это просто перечень чисел, каждое из которых задаёт определённое выполняемое действие. Вот пример гипотетического набора команд:
- 1: двигаться вперёд
- 2: повернуть влево
- 3: повернуть вправо
![](https://habrastorage.org/getpro/habr/post_images/e2a/0fe/86e/e2a0fe86e672f906df4c892d39441ca0.jpg)
По сути, это набор команд Logo — языка программирования «черепашьей графики», позволяющего перемещать робота по листу бумаги при помощи ручки, чтобы он создавал интересные рисунки. Изображение: Valiant Technology Ltd., CC-BY-SA 3.0.
Запущенная на компьютере с этим набором команд программа, которая должна переместиться вперёд три раза, повернуть вправо, дважды переместиться вперёд, повернуть влево и переместиться вперёд четыре раза, будет выглядеть так:
1 1 1 3 1 1 2 1 1 1 1
Работа с данными
Часто команды, которые должен исполнять компьютер, получают в каком-нибудь виде данные. Компьютеры часто складывают числа; гораздо проще иметь одну команду сложения, чем целое множество подобных команд:
- 1: прибавить 1
- 2: прибавить 2
- 3: прибавить 3
- 4: прибавить 4
Или же вместо одной команды «прибавить 1», вызываемой нужное количество раз, которую было бы столь же неудобно использовать. Программа, прибавляющая к числу 1000, занимала бы в 1000 раз больше места на накопителе (и выполнялась бы в 1000 раз дольше) по сравнению с программой, прибавляющей 1!
Сопровождающие команду данные должны находиться в какой-то части программы. В различных языках программирования эта задача решается по-разному. В некоторых языках «код» (команды) должен храниться совершенно отдельно от данных, в других они объединяются. Оба подхода имеют свои плюсы и минусы, но пока давайте рассмотрим объединённые команды и данные.
В нашем гипотетическом компьютере для «сложения каких-то чисел» набор команд мог бы выглядеть следующим образом:
- 1: сохранить следующее число как «первое число»
- 2: прибавить следующее число к первому числу, если оно сохранено
Программа, складывающая числа 2 и 7, выглядела бы так:
1 2 2 7
Пошагово двигаясь по программе по одному числу за раз, мы видим команду «1» («сохранить следующее число как первое число»). Следующее число — это «2», поэтому 2 сохраняется как первое число. Далее мы видим команду «2» («прибавить следующее число к первому числу»). Следующее число — это «7», поэтому наша программа прибавляет 7 к 2, получая результат 9. Здесь данные и команды перемешаны. Увидев «1 2 2 7», невозможно понять, какие из «2» — это команда «прибавить следующее число к первому числу», а какие — алгебраическое число «2», не посмотрев на начало и не пройдя пошагово всю программу.
Где находится результат (9)? Как дальше в программе нам сделать что-нибудь с этим результатом? И что же подразумевается под «сохранением» чего-либо?
Регистры процессора
Как мы только что увидели, программам часто требуется место для временного хранения данных. В большинстве компьютеров для этого используются регистры — небольшие участки внутри процессора, каждое из которых может хранить одно значение. [«Значения» — это просто числа; как мы уже делали с командами в наборе команд, можно взять значение любого типа и представить его в виде числа при условии, если у нас есть какое-то сопоставление между числами и обозначаемыми ими данными. Например, Unicode при помощи 32-битного числа описывает все возможные символы из каждой системы письма на Земле. (Подробнее о «битах» мы поговорим ниже.)]
Регистры могут быть обобщёнными или связанными с определёнными типами функциональности. Например, в процессоре NES есть регистр под названием накопитель (accumulator), часто сокращаемый до «A»; он занимается всеми математическими операциями. В наборе команд 6502 есть команды, работающие следующим образом:
- сохранить следующее число в накопитель
- прибавить следующее число к накопителю, результат записать в накопитель
- поместить куда-нибудь число из накопителя
Это решает проблему сохранения чисел и доступа к ним. Но мы не ответили на ещё один вопрос: когда мы «помещаем куда-нибудь число из накопителя», где находится это «куда-нибудь»? Процессор 6502 консоли NES имеет всего три регистра, поэтому сложные программы не могут использовать для хранения результатов только регистры.
Память
Компьютеры предоставляют программам доступ к какому-то объёму (непостоянной) памяти для временного хранения, что позволяет компьютеру иметь небольшое количество (дорогих) регистров, в то же время обеспечивая возможность хранения за пределами самой программы приемлемого количества значений. Эта память выглядит как последовательность «ящиков» размером с регистр, каждый из которых содержит одно значение и ссылка к которому осуществляется по номеру. NES предоставляет разработчику программу с двумя килобайтами (2 КБ) пространства памяти, пронумерованную от нуля до 2047 — номер в пространстве памяти называется его адресом (как адрес дома). То есть рассмотренный нами ранее набор команд 6502 на самом деле ближе к такому:
- сохранить следующее число в накопитель
- прибавить следующее число к накопителю, результат записать в накопитель
- поместить число из накопителя по адресу памяти следующего числа
Как задаются данные
И это приводит нас к последнему вопросу этой главы — как все эти числа представлены внутри компьютера?
Ранее мы использовали «стандартные» десятичные числа (по основанию 10). Это те числа, которые мы пользуемся повседневно, например, «2», или «7», или «2048». Однако компьютеры работают на электрических токах, которые могут быть или «включенными» или «выключенными», без каких-то промежуточных значений. Эти токи образуют основу всех данных внутри компьютера, поэтому компьютеры используют двоичные числа (по основанию 2).
Наименьшей единицей информации, которую может обработать компьютер, является «бит» (bit, сокращение от binary digit). Бит хранит в себе одно из двух значений — 0 или 1, «включено» или «выключено». Если мы объединим как одно число несколько битов, то сможем задавать больший диапазон значений. Например, двумя битами можно задать четыре разных значения:
00 01 10 11
Три бита позволяют задать восемь разных значений:
000 001 010 011 100 101 110 111
Каждый добавленный нами бит позволяет задавать в два раза больше значений, аналогично тому, как каждый десятичный разряд, добавляемый к десятичному числу, позволяет задавать в десять раз больше значений (1 → 10 → 100 → 1000). Объединённые вместе восемь бит, задающих одно значение, используются настолько часто, что имеют собственное название: байт (byte). В байте может храниться одно из 256 значений. Так как четыре бита являются половиной байта, их иногда называют полубайтом (nybble). В полубайте может храниться одно из 16 значений.
Часто говорят, что компьютеры (в том числе и видеоигровые консоли), имеют определённую битность. Современные десктопные компьютеры/ноутбуки обычно являются 64-битными, старые версии Windows, например, Windows XP, называют 32-битными операционными системами, а NES — это 8-битная система. Все эти числа характеризуют размер регистров компьютера — количество битов, которые может одновременно хранить один регистр. [Несколько усложняет понимание то, что адресная шина NES имеет ширину 16 бит, то есть NES может обрабатывать 65536 различных адресов памяти, а не 256. Однако каждый адрес памяти всё равно хранит только один байт.] Так как NES — это «8-битный» компьютер, каждый его регистр хранит 8-битное значение (один байт). Кроме того, каждый адрес памяти может хранить один байт.
Как же работать с числами больше 255? Игроки в Super Mario Bros. часто зарабатывают десятки тысяч очков, и одного байта явно недостаточно для хранения таких чисел. Когда нам нужно задать значение намного больше, чем может храниться в одном байте, мы используем несколько байтов. В двух байтах (16 бит) можно хранить одно из 65536 значений, а при увеличении количества байтов возможности задания чисел резко возрастают. В трёх байтах можно хранить число до 16777215, а в четырёх — до 4294967295. Когда мы используем таким образом больше одного байта, мы всё равно ограничены размером регистра компьютера. Чтобы работать с 16-битным числом на 8-битной системе, нам нужно получать или сохранять число в двух частях — «младший» (low) байт справа, «старший» (high) байт слева. [Именно из-за необходимости работы с такими значениями из нескольких регистров у процессоров есть порядок следования байтов (endianness) — т. е., у них определено, какой байт идёт первым при работе с большими числами. В процессорах с прямым порядком байтов (он называется little-endian), например, в 6502, сначала идёт младший байт, а затем старший. В процессорах с обратным порядком байтов (big-endian), например, в Motorola 68000, ситуация противоположная — ожидается, что сначала идёт старший байт, а за ним следует младший. Большинство современных процессоров является little-endian из-за очень популярной архитектуры x86 компании Intel, тоже являющейся little-endian.]
Так как управляющий консолью NES процессор 6502 одновременно работает с восемью битами данных, для задания чисел меньшего размера всё равно используется восемь бит. Это может быть неэффективно, поэтому при необходимости в одном байте часто хранят несколько значений меньшего размера. Один байт может содержать два четырёхбитных числа, или четыре двухбитных числа, или даже восемь отдельных значений «включено»/«выключено» (мы называем их флагами).
Например, байт
10110100
может задавать:- Одно 8-битное значение: 180
- Два 4-битных значения: 11 (
1011
) и 4 (0100
) - Четыре 2-битных значения: 2 (
10
), 3 (11
), 1 (01
) и 0 (00
) - Восемь значений «включено»/«выключено» (или «истина»/«ложь»): вкл., выкл., вкл., вкл., выкл., вкл., выкл., выкл.
- Любое другое сочетание битовых длин, в сумме составляющих восемь
Для удобства обсуждения таких ситуаций, когда в одном байте хранится несколько значений, обычно каждому биту в байте присваивается номер, почти так же, как мы давали название «младшему» и «старшему» байтам в 16-битном значении. Самый правый бит называется «бит 0» и счёт идёт до самого левого «бита 7». Вот пример:
байт: 1 0 1 1 0 1 0 0
№ бита: 7 6 5 4 3 2 1 0
Как сделать данные человекочитаемыми
Как мы видели, байты — это очень гибкий способ задания различных типов данных в компьютерной системе. Однако недостаток использования байтов заключается в том, что их сложно читать. Приходится прикладывать усилия, чтобы мысленно преобразовать «10110100» в десятичное число «180». Когда вся программа представлена в виде последовательности байтов, проблема сильно усугубляется.
Для решения этой проблемы основная часть кода представлена в виде шестнадцатеричных чисел. «Шестнадцатеричное» означает «по основанию 16»; одно шестнадцатеричное (hexadecimal, «hex») число может содержать одно из шестнадцати значений. Вот числа от нуля до пятнадцати, представленные в шестнадцатеричном виде:
0 1 2 3 4 5 6 7 8 9 a b c d e f
Шестнадцатеричная запись полезна, потому что шестнадцатеричное число и полубайт хранят одинаковый диапазон значений. Это значит, что можно задать байт двумя шестнадцатеричными значениям, то есть в гораздо более компактном и удобном виде.
Десятичные | Двоичные (1 байт) | Шестнадцатеричные |
---|---|---|
0 | 00000000 |
00 |
7 | 00000111 |
07 |
31 | 00011111 |
1f |
94 | 01011110 |
5e |
187 | 10111011 |
bb |
255 | 11111111 |
ff |
Работа с числами, которые могут быть десятичными, двоичными или шестнадцатеричными, может запутывать. Например, число «10» обозначает 10, если оно десятичное, 2, если оно двоичное или 16, если оно шестнадцатеричное. Чтобы понимать, какое значение мы имеем в виду, принято использовать префиксы.
10
— это десятичное число, %10
— двоичное, а $10
— шестнадцатеричное.Соединяем всё вместе
Рассмотрев множество вопросов, связанных с работой компьютеров (и программ), давайте ещё раз взглянем на весь процесс, происходящий при выполнении программы.
Во-первых, сама программа представлена в виде последовательности байтов (так называемого машинного кода). Каждый байт — это или команда для процессора, или сопровождающие команду данные.
С самого начала программы процессор многократно выполняет трёхэтапный процесс. Сначала процессор получает следующий байт из программы. В процессоре есть специальный регистр под названием счётчиком программ (program counter), он отслеживает, каким будет следующий номер байта программы. Счётчик программ (program counter, PC) работает совместно с регистром под названием адресная шина (address bus), отвечающим за получение и сохранение байтов из программы или из памяти, для получения байтов.
Далее процессор декодирует полученный им байт, выясняя, какой записи в наборе команд соответствует этот байт (или какую команду сопровождает байт данных). Наконец, он исполняет команду, внося изменения в регистры процессора или в память. Процессор выполняет инкремент счётчика программ на единицу, чтобы получить следующий байт программы, и цикл начинается снова.
3. Приступаем к разработке
Содержание:
- Настройка среды разработки
- Текстовый редактор
- Ассемблер и компоновщик
- Эмулятор
- Графические инструменты
- Инструменты для создания музыки
- Соединяем всё вместе
- Дальнейшие шаги
Давайте начнём программировать для NES! В этой главе мы настроим среду разработки, установим все инструменты, которые необходимы для описанной в этой книге работы, а затем соберём и запустим самую простую игру, чтобы убедиться, что всё работает.
Настройка среды разработки
Здесь перечислены все инструменты, которые мы будем устанавливать. Некоторые из них мы будем использовать сразу (и постоянно), другие более специализированы и пригодятся позже. Для каждой категории я указал конкретное ПО, которое буду использовать в этой книге; однако есть множество других вариантов, так что освоившись с рекомендуемыми мной, вы сможете поэкспериментировать и с другими инструментами.
- Текстовый редактор (на ваш выбор)
- Ассемблер/компоновщик (ca65 и ld65)
- Эмулятор (Nintaco)
- Графический инструмент, способный считывать/сохранять изображения в формате NES (NES Lightbox)
- Инструмент для создания музыки (FamiStudio)
Текстовый редактор
Во-первых, вам понадобится текстовый редактор. Думаю, вы уже что-то программировали, поэтому у вас есть любимый текстовый редактор. Если нет, то можно попробовать одну из следующих программ:
- Sublime Text. Кроссплатформенная, популярная среди разработчиков, простая в освоении; когда вы освоитесь с основами, становится мощным инструментом.
- Atom. По сути, ответ GitHub'а на Sublime Text. Кроссплатформенная, с гибкими настройками.
- Visual Studio Code. Качественная платформа Microsoft для редактирования текста. Создавалась для веб-разработки, но имеет возможность расширения для любого вида программирования. Также кроссплатформенная, не только для Windows.
- Vim, emacs, nano, и т. д. Текстовые редакторы с давней историей, работающие из командной строки. (Лично я пользуюсь Vim, но выбор за вами.)
Ассемблер и компоновщик
Ассемблер компилирует ассемблерный код (который мы будем писать в этой книге) в машинный код — сырой поток байтов, считываемый процессором. Компоновщик (linker) берёт набор файлов, который был пропущен через ассемблер, и превращает их в единый файл программы. Так как у каждого процессора свой машинный код, ассемблеры обычно предназначены только для одного типа процессора. Существует множество вариантов ассемблеров и компоновщиков для процессора 6502, но в этой книге мы будем использовать ca65 и ld65. Они имеют открытый исходный код и являются кроссплатформенными, а также обладают очень полезными функциями для разработки больших программ. ca65 и ld65 — это часть более масштабного комплекта программ «cc65», включающего в себя компилятор C и многое другое.
Mac
Для установки ca65 и ld65 на Mac нужно для начала установить менеджер пакетов Mac Homebrew. Скопируйте команду с главной страницы, вставьте её в терминал и нажмите Enter; выполните инструкции, после чего Homebrew будет готов к работе. Установив Homebrew, введите
brew install cc65
и нажмите Enter.Windows
В Windows необходимо скачать ca65 и ld65 в определённую папку на компьютере. Скачайте последний «Windows Snapshot» с страницы основного проекта cc65. Распакуйте содержимое в
C:\cc65
. Также вам нужно будет дополнить системные пути, чтобы ca65 и ld65 были видны из любой папки. Этот процесс зависит от того, какой версией Windows вы пользуетесь. В самых новых версиях Windows можно нажать правой кнопкой мыши на «Мой компьютер», выбрать «Свойства», «Дополнительные параметры системы», а затем «Переменные среды». Вам нужно будет найти запись %PATH%
и добавить в её конец C:\cc65\bin
.Linux
Вам нужно будет собрать cc65 из исходников. К счастью, это довольно простой процесс. Сначала убедитесь, что у вас есть git и базовая среда для сборки — например в Ubuntu для этого достаточно выполнить
sudo apt-get install git build-essential
. Затем перейдите в папку, куда вы хотите установить cc65, клонируйте репозиторий cc65 и соберите его:git clone https://github.com/cc65/cc65.git
cd cc65
make
Затем сделайте программы cc65 доступными из любой папки, выполнив
sudo make avail
. Эта команда добавит символическую ссылку из вашей папки cc65 в /usr/local/bin
.Эмулятор
Эмулятор — это программа, запускающая программы, предназначенные для другой компьютерной системы. Мы будем использовать эмулятор NES, чтобы запускать создаваемые нами программы на том же компьютере, где их разрабатываем, вместо запуска на аппаратной NES. Существует множество эмуляторов NES (а когда вы наберётесь опыта в разработке для NES, будет интересно и написать собственный!), но для этой книги мы будем использовать Nintaco.
![](https://habrastorage.org/getpro/habr/post_images/6d8/4e4/b59/6d84e4b592725f286fee115f86ba0ef8.png)
Nintaco.
Он кроссплатформенный, и к тому же является одним из немногих эмуляторов, имеющих инструменты отладки, которые пригодятся, когда мы будем писать программы.
Установка Nintaco на всех платформах происходит одинаково — достаточно скачать его с веб-сайта Nintaco и распаковать. Чтобы запустить Nintaco, нужно дважды нажать на Nintaco.jar. Для запуска Nintaco требуется Java; если на вашем компьютере не установлена Java, скачайте «Java Runtime Environment» с сайта java.com.
Графические инструменты
NES хранит графику в собственном уникальном формате, непохожем на традиционные типы изображений наподобие JPEG или PNG. Нам понадобится программа, способная работать с изображениями NES. Существуют плагины для больших графических пакетов типа Photoshop или GIMP, но мне нравится использовать для этого компактный специализированный инструмент. Для этой книги мы будем использовать NES Lightbox — кроссплатформенную производную от NES Screen Tool.
![](https://habrastorage.org/getpro/habr/post_images/30b/381/b76/30b381b769bf964f1f120a1fbe24a3f8.png)
NES Lightbox.
Windows
Скачайте Windows-установщик (для 64-битных систем). Дважды нажмите на «NES Lightbox Setup 1.0.0.exe», чтобы установить программу.
Mac
Скачайте Mac DMG. Дважды нажмите на файл .dmg, чтобы открыть его, и перетащите приложение NES Lightbox в папку Applications. При первом запуске приложения нужно будет нажать на него правой клавишей мыши и выбрать «Open», потому что оно не «подтверждено» компанией Apple.
Linux
В системах с Ubuntu можно скачать файл Snap, который является автономным пакетом приложения. В случае других дистрибутивов Linux (или если вы предпочитаете AppImage) нужно скачать файл AppImage. Прежде чем запустить файл AppImage, его нужно пометить как исполняемый.
Инструменты для создания музыки
Как и в случае с графикой, звук на NES представлен в уникальном формате — это команды аудиопроцессора, а не что-то типа MP3. Самая популярная программа для создания звука для NES — FamiTracker, это мощный, но сложный инструмент, предназначенный только для Windows. Для этой книги мы будем использовать FamiStudio — кроссплатформенную программу с более дружественным интерфейсом, результаты работы в которой сохраняются в простой для интеграции формат.
![](https://habrastorage.org/getpro/habr/post_images/c66/e24/ab0/c66e24ab08421b8879ced3e8e754532a.png)
FamiStudio.
Windows / Mac / Linux
Скачайте последнюю версию с веб-сайта FamiStudio.
Соединяем всё вместе
Установив все инструменты, нужно убедиться, что они работают. Мы создадим аналог «Hello World» для игр на NES: заполним весь экран одним цветом.
Откройте текстовый редактор и создайте новый файл
helloworld.asm
. Скопируйте и вставьте в файл следующий код:![](https://habrastorage.org/webt/2p/ql/my/2pqlmy1g6o87izh_qr6z7fo38by.png)
Код в текстовом виде
.segment "HEADER"
.byte $4e, $45, $53, $1a, $02, $01, $00, $00
.segment "CODE"
.proc irq_handler
RTI
.endproc
.proc nmi_handler
RTI
.endproc
.proc reset_handler
SEI
CLD
LDX #$00
STX $2000
STX $2001
vblankwait:
BIT $2002
BPL vblankwait
JMP main
.endproc
.proc main
LDX $2002
LDX #$3f
STX $2006
LDX #$00
STX $2006
LDA #$29
STA $2007
LDA #%00011110
STA $2001
forever:
JMP forever
.endproc
.segment "VECTORS"
.addr nmi_handler, reset_handler, irq_handler
.segment "CHARS"
.res 8192
.segment "STARTUP"
Теперь нам нужно воспользоваться ассемблером. В папке, в которую вы сохранили helloworld.asm, выполните команду
ca65 helloworld.asm
. В результате появится новый файл helloworld.o. Это объектный файл — машинный код. Но он пока не находится в формате, готовом для запуска в эмуляторе. Чтобы преобразовать его, нужно запустить компоновщик. В той же папке выполните команду ld65 helloworld.o -t nes -o helloworld.nes
. В результате этого должен появиться новый файл helloworld.nes — файл «ROM» для эмулятора.Запустите Nintaco и выберите в меню «File» пункт «Open». Выберите только что созданный файл helloworld.nes и нажмите Open. В результате вы увидите зелёный экран.
![](https://habrastorage.org/webt/uz/fm/6h/uzfm6hhjzvchlnv0wnpyq8rtsv4.png)
[В оригинале главы зелёный экран — это настоящий работающий в браузере эмулятор NES! Я воспользовался потрясающим jsnes, созданным Беном Фиршманом. Каждый раз, когда мы будем компилировать файл .nes, я буду включать в текст подобное работающее демо. (Сейчас сложно это понять, но на самом деле эмулятор работает с частотой кадров 60fps.)]
Дальнейшие шаги
Если вы увидели в Nintaco зелёный экран, поздравляю! Ваша среда разработки готова к использованию. В следующей главе мы расскажем, что же делает скопированный нами код, и немного узнаем о том, как работает оборудование NES.
Создание игр для NES на ассемблере 6502: оборудование NES и знакомство с ассемблером