![](https://habrastorage.org/webt/td/do/wd/tddowdjivkbg42_t5huq6ecs6po.png)
Оглавление
Оглавление
Часть I: подготовка
- Введение
- 1. Краткая история NES
- 2. Фундаментальные понятия
- 3. Приступаем к разработке
- 4. Оборудование NES
- 5. Знакомство с языком ассемблера 6502
- 6. Заголовки и векторы прерываний
- 7. Зачем вообще этим заниматься?
- 8. Рефакторинг
Часть II: графика
8. Рефакторинг
Содержание:
- Константы
- Файл заголовка
- Импорт и экспорт ca65
- Собственная конфигурация компоновщика
- Соединяем всё вместе
Прежде чем мы перейдём к более глубокому изучению того, как NES рисует графику, давайте поразмыслим над тем, что мы уже создали. Сейчас можно внести несколько улучшений, которые пригодятся нам в будущем. Выполнив рефакторинг, мы создадим полезный шаблон для следующих проектов.
Константы
Во многих местах нашего кода есть конкретные числа, которые не меняются, например, MMIO-адреса для общения с PPU. Глядя на код, сложно понять, что означают эти числа.
К счастью, эти абстрактные значения можно заменить понятным текстом, объявив константы. По сути, константа — это имя одного числа, которое нельзя изменить. Давайте создадим константы для адресов PPU, которые мы уже использовали:
[Обычно эти имена являются стандартными наименованиями для различных MMIO-адресов NES. При изучении источников, например, NESDev wiki, вы их встретите.]
![](https://habrastorage.org/webt/ob/ap/lr/obaplr9ceypo44dalp0uzguasje.png)
Благодаря этим константам наш код
main
становится гораздо более читабельным:![](https://habrastorage.org/webt/rj/th/jp/rjthjpnbpudrddldhcylbvpqtwg.png)
Куда поместить эти константы? Обычно создают отдельный фай констант, который можно включить в основной файл ассемблерного кода. Файл констант мы назовём
constants.inc
.[Почему этот файл имеет расширение
.inc
, а не .asm
? Файл констант содержит не совсем ассемблерный код; в нём нет никаких опкодов. Мы будем использовать расширение .asm
для файлов с ассемблерным кодом, а .inc
для файлов, которые мы включаем в файл с ассемблерным кодом.]Затем мы добавим файл констант в начало файла
.asm
следующим образом:![](https://habrastorage.org/webt/df/ym/jr/dfymjrlgmxywwk59im9zrf5pnb4.png)
Файл заголовка
То же самое можно проделать и с сегментом
.header
, потому что в целом он будет одинаковым для разных проектов. Давайте создадим файл header.inc
, в котором будет находиться содержимое заголовка. Также это подходящее время для добавления комментариев:![](https://habrastorage.org/webt/eh/s-/-q/ehs--qukoeeu9ao2ghdwb5gv9k8.png)
Теперь мы можем удалить раздел
.segment "HEADER"
нашего основного файла .asm
и добавить новый файл заголовка. Начало файла .asm
должно выглядеть вот так:![](https://habrastorage.org/webt/ct/3k/qr/ct3kqr8cmknhabesoydkefddime.png)
При запуске ассемблера и компоновщика они будут брать содержимое
header.inc
и помещать его в нужное место готового ROM, точно так же, как если бы мы поместили его непосредственно в файл с ассемблерным кодом.
Импорт и экспорт ca65
Полный обработчик сброса может стать довольно большим, поэтому будет полезно поместить его в отдельный файл. Но мы не можем просто добавить его с помощью
.include
, потому что нам нужно каким-то образом ссылаться на обработчик сброса в сегменте VECTORS
.Этого можно достичь благодаря тому, что ca65 способен импортировать и экспортировать код
.proc
. Мы используем директиву .export
, чтобы сообщить ассемблеру, что определённая proc должна быть доступна в других файлах, и директиву .import
, чтобы использовать proc в другом месте.Для начала давайте создадим
reset.asm
, содержащий директиву .export
:![](https://habrastorage.org/webt/vg/om/b3/vgomb35dkihkfjqfw5wxoob4i9q.png)
В этом файле стоит упомянуть несколько вещей. Во-первых, файл имеет расширение
.asm
, потому что содержит опкоды. Во-вторых, мы добавляем файл констант, чтобы можно было здесь его использовать. В-третьих, нам нужно указать, какому сегменту кода принадлежит эта .proc
, чтобы компоновщик знал, как собирать всё вместе. В-четвёртых, обратите внимание, что мы импортируем main
. Благодаря этому ассемблер знает, в каком адресе памяти расположена процедура main
, чтобы обработчик сброса мог перейти по правильному адресу.Теперь, когда у нас есть отдельный файл сброса, мы воспользуемся
reset_handler
внутри кода:![](https://habrastorage.org/webt/lm/wj/op/lmwjopp06thbxruuysczrpfhryo.png)
В строке 13, где раньше находилась
.proc reset_handler
, теперь импортируется процедура из внешнего файла. Обратите внимание, что нам не нужно указывать, из какого файла берётся процедура — перед ассемблированием ассемблер сканирует все файлы .asm
на наличие экспорта, поэтому уже знает, какие внешние процедуры доступны и где они расположены. (Стоит также заметить, что из-за этого вы не сможете экспортировать две процедуры с одним именем — ассемблер не поймёт, на какую из них вы ссылаетесь в .import
.)[Возможно, вы заметили, что в
reset.asm
используется .segment "CODE"
, а в нашем основном файле с ассемблерным кодом тоже используется .segment "CODE"
. Что произойдёт, когда мы ассемблируем и скомпонуем эти файлы? Компоновщик находит всё, что принадлежит к тому же сегменту, и соединяет это. Порядок не особо важен, потому что метки преобразуются в адреса на этапе компоновки.]Также нам нужно экспортировать процедуру
main
, чтобы обработчик сброса мог импортировать её и знать, куда переходить после её завершения.Собственная конфигурация компоновщика
При компоновке примера проекта в Главе 3 мы использовали следующую команду:
ld65 helloworld.o -t nes -o helloworld.nes
-t nes
приказывает ld65 использовать стандартную конфигурацию компоновщика для NES. Именно поэтому у нас есть сегмент "STARTUP", хотя мы его никогда не использовали. Стандартная конфигурация подходит для примера проекта, однако когда код станет сложнее и больше, она может привести к проблемам. Поэтому вместо использования стандартной конфигурации мы напишем нашу собственную конфигурацию компоновщика только с теми сегментами и свойствами, которые нам нужны.Наша собственная конфигурация компоновщика будет находиться в файле
nes.cfg
, который выглядит вот так:![](https://habrastorage.org/webt/ds/dr/7f/dsdr7f0ox4ueikmfpjxo0p2mzfc.png)
Код в текстовом виде
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: start=$8000, size=$8000, fill=yes, fillval=$ff;
CHRROM: start=$0000, size=$2000;
}
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, type=ro, align=64, optional=yes;
CODE: load=ROM, type=ro, align=$0100;
RODATA: load=ROM, type=ro, align=$0100;
VECTORS: load=ROM, type=ro, start=$FFFA;
CHR: load=CHRROM, type=ro, align=16, optional=yes;
}
В разделе
MEMORY
указывается структура областей памяти, в которые можно помещать сегменты, а в разделе SEGMENTS
заданы имена сегментов, используемых в нашем коде, и области памяти, в которые они должны компоноваться. Я не буду подробно объяснять, что значит каждый из параметров, описание можно найти в документации ld65.Чтобы использовать собственную конфигурацию компоновщика, нам сначала нужно обновить имена сегментов в нашем коде, чтобы они соответствовали именам сегментов из файла конфигурации. В нашем случае достаточно переместить
"CHARS"
в "CHR"
и удалить "STARTUP"
.Соединяем всё вместе
И, наконец, нам нужно немного изменить структуру файлов. Все файлы
.asm
и .inc
мы переместим в подпапку src
, а новая конфигурация компоновщика будет находиться на верхнем уровне. После рефакторинга структура кода должна выглядеть вот так:08-refactoring
|
|-- nes.cfg
|-- src
|
|-- constants.inc
|-- header.inc
|-- helloworld.asm
|-- reset.asm
Для ассемблирования и компоновки нашего кода мы используем следующие команды (запускаемые из папки верхнего уровня
08-refactoring
):ca65 src/helloworld.asm
ca65 src/reset.asm
ld65 src/reset.o src/helloworld.o -C nes.cfg -o helloworld.nes
Поясню: здесь мы сначала ассемблируем каждый файл
.asm
, создавая файлы .o
. После этого мы передаём все файлы .o
компоновщику. Вместо стандартной конфигурации компоновщика для NES (-t nes
) мы используем нашу новую собственную конфигурацию (-C nes.cfg
). Результат работы компоновщика помещается в тот же файл ROM helloworld.nes
.Копию всех перечисленных выше файлов можно скачать в формате ZIP отсюда. Мы будем использовать эту схему в качестве основы будущих проектов, поэтому прежде чем двигаться дальше, убедитесь, что у вас получится ассемблировать, скомпоновать и запустить код.
Создание игр для NES на ассемблере 6502: Picture Processing Unit (PPU)