Я хочу продолжить цикл статей по кодингу для старенькой консоли NES. Сегодня рассмотрим классический пример в программировании — программу выводящую на экран Hello World.

Я предполагал во второй части статьи описать архитектуру консоли, а также описать работу с видеопроцессором, но по данным вопросам уже есть очень много информации. Ниже прилагаю списочек ссылок с материалами по архитектуре и программированию для NES.
http://dendy.migera.ru/nes — Русскоязычный сайт с информацией по архитектуре NES, рекомендуется к прочтению в первую очередь
http://wiki.nesdev.com/w/index.php/Nesdev_Wiki — Очень много информации по тематике, есть примеры программ с исходниками
http://web.textfiles.com/games/6502jsm.txt — Описание инструкций ассемблера

В прошлой части мы рассмотрели как настроить сборку приложения из редактора Sublime, но в дополнении я хочу рассказать как добавить возможность запуска приложения из редактора. Для этого добавим в конфигурацию билд-системы команду запуска эмулятора. (Пользовательские конфигурации Sublime на linux платформе обычно хранятся в директории ~/.config/sublime-text-2/Packages/User/)

{
«cmd»: [«<путь к nesasm>/nbasic_2004_03_14/bin_linux/nesasm», «$file»],»variants»:
[
{
«name»: «Run»,
«cmd»: [«mednafen», «$file_base_name.nes»]
}
]
}

 

Данная схема настроена на запуск через эмулятор Mednafen, он должен быть установлен в систему (sudo apt-get install mednafen — установка в убунту).
Теперь сборка и запуск приложения доступны по горячим клавишам: Ctrl+b — сборка; Ctrl+Shift+b — запуск. В качестве параметра эмулятору передается имя текущего файла в редакторе, нужно быть внимательным когда запускаешь приложение со вкладки, не являющейся исходником программы (например, если активна вкладка файла с макросами), в таком случаем программа не будет работать

Логически программу можно разбить на 3 этапа: инициализация PPU, загрузка аттрибутов, вывод надписи Hello World. Для подготовки каждого из них нам поможет такая вещь в ассемблере NESASM как макросы. Макросы — это именованый блок кода, который разворачивается в каждое место, где задано имя макроса. Я предлагаю выносить объявления всех макросов в отдельный файл(ы) для более удобной навигации по коду в дальнейшем.
Итак, начало программы начинается с хидера.

  .inesprg 1 ; 1x 16KB PRG code
.ineschr 1 ; 1x CHR bank
.inesmir 0 ; mirroring type 0
.inesmap 0 ; memory mapper NROM

 

Директива INESPRG определяет кол-во 16тикилобайтных банков программ, самая простая модель картриджа (NROM) содержит одну микросхему ПЗУ объемом 32Кб, т.е. максимально в такой конфигурации может быть 2 банка по 16Кб памяти программ.
INESCHR — определяет кол-во банков знакогенератора, каждый такой банк в размере 8Кб. В стандартной конфигурации картриджа идет одна микросхема ПЗУ размером 8Кб.
INESMAP — тип маппера, 0 в данном случае указывает на маппер NROM
INESMIR — //TODO
Далее необходимо задать векторы 3х прерываний: NMI, RESET и пользовательское прерывание IRQ. NMI прерывание генерируется видеопроцессором и говорит о том, что PPU закончил отрисовку кадра. Теоретически, по генерации этого прерывания должна происходить работа с видеопамятью, т.к. видеопамять недоступна для изменений в процессе отрисовки кадра. Это прерывание называется немаскируемым, это означает что при происхождении, оно будет обработано всегда. Следует учесть, что NMI также будет сгенерировано при каждом чтении регистра видеопроцессора $2002.
Прерывание RESET генерируется при включении питания консоли или при нажатии на кнопку сброса. И фактически это точка вхождения в программу. По нему мы будем делать базовую инициализацию
Ну и последний тип — пользовательское прерывание IRQ, происходит при сигнале на входе процессора или при появлении инструкции BRK в коде программы. В текущем практическом примере рассмотрен не будет
Приоритеты выполнения прерываний идут в следующем порядке от старшего к младшему: RESET, NMI, IRQ
Чтобы задать векторы прерываний необходимо записать 3 двухбайтных числа (адреса), начиная с адреса $FFFA. Ниже макет программы с заданием адресов 2х прерываний и заданием основного цикла программы, можно назвать этот макет основным костяком программы.

  .inesprg 1 ; 1x 16KB PRG code
.ineschr 1 ; 1x CHR bank
.inesmir 0 ; mirroring type 0
.inesmap 0 ; memory mapper NROM.bank 0
.org $C000RESET:
SEI ; disable IRQs

MainLoop: ; main loop of app
JMP MainLoop

NMI:
RTI

.bank 1
.org $FFFA ;first of the three vectors starts here NMI, RESET and IRQ
.dw NMI
.dw RESET
.dw 0

 

Здесь можно увидеть что в банке 0 начиная с адреса $C000, располагается основной код программы. В нем заданы адреса обработчиков прерываний RESET и NMI. Первая инструкция SEI в обработчике RESET отключает пользовательское прерывание IRQ. По достижению конца обработчика RESET расположился бесконечный цикл, в котором будет находится основной цикл программы. Для прерывания IRQ мы задали адрес 0, тем самым указывая что оно не будет обработано.

Теперь займемся наполнением остальной части программы. В первую очередь это инициализация видеопроцессора. Вынесем код инициализации PPU в макрос, а еще лучше перенести объавление всего макроса в отдельный файл. Я для своих целей завел 2 файла с макросами, первый — macros_system.asm, и второй — macros_video.asm. В первом файле храню макросы связанные с системной инициализацией, как например та же инициализация видеопроцессора. А во втором файле макросы для работы с видеопроцессорами, такие как очистка экрана, или макрос задающий видеопроцессору, по координатам столбца и строки, адрес, с которого будет начинаться отрисовка будущих тайлов в бекграунд слой.
текущую программу достаточно использовать директиву INCLUDE после блока заголовка, например:

  .inesprg 1 ; 1x 16KB PRG code
.ineschr 1 ; 1x CHR bank
.inesmir 0 ; mirroring type 0
.inesmap 0 ; memory mapper NROM.include «macros_video.asm»
.include «macros_system.asm»

 

Объявление макроса задается ключевым словом .macro после или до имени макроса и заканчивается словом .endm. Все инструкции, находящиеся между двумя этими директивами впоследствии будут «инжектированы» в места использования макроса. Макросы позволяют использовать 9 параметров, доступ к каждому параметру предваряется обратным слешем перед индексом параметра (\1 — \9). Чтобы узнать сколько было передано параметров можно воспользоваться сокращением \#. Также еще одним полезным свойством обладает параметр \@, который задает для каждого объавления макроса уникальный номер в месте объавления. Это позволяет использовать внутри макросов метки с одинаковыми именами, не боясь за конфликт имен меток. Чтобы им воспользоваться необходимо после имени метки записать \@, например:

VBlankWait .macro
VWait\@:
BIT $2002
BPL VWait\@
.endm

 

Где, VWait\@ метка, используемая для цикла внутри макроса. Кстати, вышеприведенный пример макроса позволяет выполнить ожидание обратного хода луча, т.е. дожидается когда видеопроцессор закончит отрисовку кадра. После вызова этого макроса можно смело начинать отрисовку картинки в видеопамять
Ниже пример кода для инициализации видеопроцессора, значение каждого бита записываемых в регистры видеопроцессора смотрите по ссылкам в начале статьи

PPUInit .macro
LDA #%10010000 ; enable NMI, sprites from Pattern Table 0, back ground from Pattern Table 1
STA $2000
LDA #011110 ; enable sprites, enable background, no clipping on left side
STA $2001
.endm

 

После того как проинициализировали PPU, необходимо загрузить палитру цветов. Палитра представляет из себя 32 байта по 16 цветов на бекграунд и столько же на спрайты. Один байт в палитре может представлять один из 64 доступных цветов. Ниже представлена таблица с соответствием RGB цвета с его кодом в PPU

0 1 2 3 4 5 6 7 8 9 A B C D E F
0                                                                                
1                                                                                
2                                                                                
3                                                                                

 

Объявим где-то в коде метку pPalette, после которой расположим 32 байта — данные палитры. (префикс p перед меткой говорит что это указатель, т.е. адрес. Также я буду использовать префикс k для констант)

pPalette:
.db $0F,$21,$19,$11, $22,$36,$17,$0F, $22,$30,$21,$0F, $22,$27,$17,$0F ;;background palette
.db $0F,$21,$19,$11, $22,$36,$17,$0F, $22,$30,$21,$0F, $22,$27,$17,$0F ;;sprite palette

 

Работа с палитрой выполняется через регистры PPU, в общей сложности мы должны записать в регистр видеопроцессора $2006 последовательно сначала старший байт, затем младший — адрес начала памяти, с которым будет работать PPU. После установления базового адреса, можно выполнять запись значений, делается это через запись значения в регистр $2007, при этом для каждого обращения записи в этот регистр, базовый адрес работы с PPU будет увеличен на 1. Т.е. если мы записали в PPU базовый адрес $3F00, и затем сделали сохранение значение в регистр $2007, то это значение сохранится по указанному адресу $3F00, но следующая запись в регист $2007 будет уже осщуствлена по адресу $3F01, и так далее..
Базовый адрес для работы с палитрой начинается с $3F00-$3F0F — этот диапазон будет сохранять значение палитры для бекграунда, а запись по адресам $3F10=$3F1F — для спрайтов. Напишем макрос который получает в параметрах адрес начала данных палитры, читает 32 байта начиная с этого адреса и сохраняет их в памяти видеопроцессора.

LoadPalette .macro
LDA #$3F ; set PPU working address 3F00 — palette
STA $2006
LDA #$00
STA $2006
LDX #$00 ; reset counter
LoadPaletteLoop\@:
LDA \1, x ; load data from address (pallete addr + the value in x)
STA $2007
INX
CPX #$20 ; Compare X to 32, looping if not equal
BNE LoadPaletteLoop\@
.endm

 

В начале мы установили стартовый адрес $3F00, соответствующий началу палитры бекраунда, т.к. адреса палитры бекграунда и спрайтов соединены бесшовно, то можно дальше просто записывать 32 байта данных палитры. В регистре X находится индекс цвета, который меняет свое значение с 0 до 31. Этот идекс задает смещение относительно начального адреса палитры, т.о. используя инструкцию LDA pPalette, x мы говорим что значения с адреса $(pPalete + X) будет сохранено в регистр A.

Одним из полезных макросов, я считаю, макрос задания адреса видеопамяти по значению столбца и строки. Удобство использования этого макроса очень высоко, т.к. позволяет оперировать более понятными человеку вещами.
Как уже ранее говорилось, через запись в регистр $2006 производится установления адреса видеопроцессора, с которого будет производится дальнейшие операции. Если разбить побитово этот адрес, то для адресов видеостраниц ($2000, $2400, $2800, $2C00) легко можно извлечь расположение следующиx компонент:

+———— Старший байт
         |            +—— Младший байт
|            |
0010NNYYYYYXXXXX
|  |    |
|  |    +—- Позиция столбца тайла (от 0 до 32)
|  +——— Позиция строки тайла (от 0 до 30)
+———— Номер видеостраницы (0, 1, 2, 3 соответственно для $2000,$2400,$2800,$2C00)

Таким образом можно вычислить старший и младший байты адреса:

    hByte = 0x20 + (N << 2) + (Y >> 3)
lByte = (Y << 5) + X

 

И сам код:

MoveTo .macro
LDA #HIGH(\1) ; Load page address
STA TMP
LDA \3 ; Load Y position, extract from it bits 3, 4 and store them to bit 2, 3 in high byte
LSR A
LSR A
LSR A
ADC TMP
STA $2006

LDA \3 ; Load Y position, extract from it bits 0, 1 and 2 and store them to bits 5, 6 and 7 in low byte
AND #000111
ASL A
ASL A
ASL A
ASL A
ASL A
LDX \2
STX TMP
ADC TMP
STA $2006
.endm