
Оглавление
Часть I: подготовка
- Введение
- 1. Краткая история NES
- 2. Фундаментальные понятия
- 3. Приступаем к разработке
- 4. Оборудование NES
- 5. Знакомство с языком ассемблера 6502
- 6. Заголовки и векторы прерываний
- 7. Зачем вообще этим заниматься?
- 8. Рефакторинг
Часть II: графика
8. Рефакторинг
Содержание:
- Константы
- Файл заголовка
- Импорт и экспорт ca65
- Собственная конфигурация компоновщика
- Соединяем всё вместе
Прежде чем мы перейдём к более глубокому изучению того, как NES рисует графику, давайте поразмыслим над тем, что мы уже создали. Сейчас можно внести несколько улучшений, которые пригодятся нам в будущем. Выполнив рефакторинг, мы создадим полезный шаблон для следующих проектов.
Константы
Во многих местах нашего кода есть конкретные числа, которые не меняются, например, MMIO-адреса для общения с PPU. Глядя на код, сложно понять, что означают эти числа.
К счастью, эти абстрактные значения можно заменить понятным текстом, объявив константы. По сути, константа — это имя одного числа, которое нельзя изменить. Давайте создадим константы для адресов PPU, которые мы уже использовали:
[Обычно эти имена являются стандартными наименованиями для различных MMIO-адресов NES. При изучении источников, например, NESDev wiki, вы их встретите.]

Благодаря этим константам наш код main становится гораздо более читабельным:

Куда поместить эти константы? Обычно создают отдельный фай констант, который можно включить в основной файл ассемблерного кода. Файл констант мы назовём constants.inc.
[Почему этот файл имеет расширение .inc, а не .asm? Файл констант содержит не совсем ассемблерный код; в нём нет никаких опкодов. Мы будем использовать расширение .asm для файлов с ассемблерным кодом, а .inc для файлов, которые мы включаем в файл с ассемблерным кодом.]
Затем мы добавим файл констант в начало файла .asm следующим образом:

Файл заголовка
То же самое можно проделать и с сегментом .header, потому что в целом он будет одинаковым для разных проектов. Давайте создадим файл header.inc, в котором будет находиться содержимое заголовка. Также это подходящее время для добавления комментариев:

Теперь мы можем удалить раздел .segment "HEADER" нашего основного файла .asm и добавить новый файл заголовка. Начало файла .asm должно выглядеть вот так:

При запуске ассемблера и компоновщика они будут брать содержимое header.inc
и помещать его в нужное место готового ROM, точно так же, как если бы мы поместили его непосредственно в файл с ассемблерным кодом.
Импорт и экспорт ca65
Полный обработчик сброса может стать довольно большим, поэтому будет полезно поместить его в отдельный файл. Но мы не можем просто добавить его с помощью .include, потому что нам нужно каким-то образом ссылаться на обработчик сброса в сегменте VECTORS.
Этого можно достичь благодаря тому, что ca65 способен импортировать и экспортировать код .proc. Мы используем директиву .export, чтобы сообщить ассемблеру, что определённая proc должна быть доступна в других файлах, и директиву .import, чтобы использовать proc в другом месте.
Для начала давайте создадим reset.asm, содержащий директиву .export:

В этом файле стоит упомянуть несколько вещей. Во-первых, файл имеет расширение .asm, потому что содержит опкоды. Во-вторых, мы добавляем файл констант, чтобы можно было здесь его использовать. В-третьих, нам нужно указать, какому сегменту кода принадлежит эта .proc, чтобы компоновщик знал, как собирать всё вместе. В-четвёртых, обратите внимание, что мы импортируем main. Благодаря этому ассемблер знает, в каком адресе памяти расположена процедура main, чтобы обработчик сброса мог перейти по правильному адресу.
Теперь, когда у нас есть отдельный файл сброса, мы воспользуемся reset_handler внутри кода:

В строке 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, который выглядит вот так:

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 отсюда. Мы будем использовать эту схему в качестве основы будущих проектов, поэтому прежде чем двигаться дальше, убедитесь, что у вас получится ассемблировать, скомпоновать и запустить код.
ссылка на оригинал статьи https://habr.com/ru/post/597997/
Добавить комментарий