Программирование NES (dendy), assembler 6502

от автора

У меня с детства была мечта написать игру для любимой приставки денди, шло время, мечта то появлялась то затихала снова. Она меня направляла в сторону магии программирование, и вот прошло больше 20-ти лет я программист, и снова в который раз пытаюсь постигнуть магию той самой денди что так будоражило моё воображение в тяжелом но счастливом детстве. Сегодня Я расскажу вам как постиг секреты этой магии, наконец то смог вывести спрайты на экран и научился рисовать фон.


Я php программист и по этому довольно далек от низкоуровневого программирования, по этому мне приходилось довольно долго изучать сам assembler но он оказался проще чем я думал, оказалось надо просто освободить голову от стереотипов современного программирования и смотреть на assembler как на инструмент передачи информации в некое api черный ящик. Так вот этим черным ящиком и является для нас Dendy (или NES, Famicom), так вот у этого ящика есть некие области памяти, одни предназначены только для чтения, другие для записи, а третьи и для чтения и для записи. Приставка воспроизводя игру использует несколько ключевых подсистем (процессоров):

1. CPU — центральный процессор
2. PPU — Графический процессор
3. APU — Аудио процессор

Сегодня, Я хотел бы затронуть тему работы с PPU ибо это наверное ключевое для многих людей графическое представление данных. Для начала разберем из чего состоит картридж, важными в картридже являются 2 чипа с памятью один это CHR (тут храниться битовая карта спрайтов и фона) имеет два банка памяти. Битовая карта представляет из себя набор 0-й и 1-к, размером 8х8, один спрайт состоит из 2-х таких областей. Пример ниже.

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0
0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0
0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0
0 1 1 1 1 1 0 0 + 1 1 1 1 1 1 1 1
0 1 0 0 0 1 0 0 1 0 0 0 0 0 1 0
0 1 0 0 0 1 0 0 1 0 0 0 0 0 1 0
0 1 0 0 0 1 0 0 1 0 0 0 0 0 1 0

При сложение этих двух банков они накладываются друг на друга и определяется цвет пикселя, таких цвета может быть 3-и для спрайта:

  • 0 + 0 = 0 — прозрачный цвет

  • 0 + 1 = 1 — первый цвет

  • 1 + 1 = 2 — второй цвет

  • 1 + 0 = 3 — третий цвет

Из этого примера и вытекают следующее умозаключение, что у background 4 цвета в палитре, а у спрайта всего 3-три. Потому что фон не имеет прозрачного цвета, 1-й цвет у него цвет фона.

Давайте перейдем к ассемблеру, у нес есть несколько аккумуляторов, их можно сравнить с переменными это A, X, Y. X, Y они имеют косвенное отношение к координатам. Чаще всего мы будем пользоваться аккумулятором A.

LDA {argument} - Загрузить в A значение argument (десятичное #20, hex #$20, двоичное %00010010) STA {address} - Сохранить A в память (адрес ppu для примера)

Команды с аккумулятором X и Y

LDX {argument} - Загрузить аргумент в X LDY {argument} STX {address} - Сохранить X STY {address} - Сохранить Y INX - increment X INY - increment Y CPX {argument} - Сравнить x с аргументом CPY {argument} - Сравнить y с аргументом

Команды ветвления:

JMP {address} - переход к адресу BEQ {address} - перейти к адресу если CPX true BNQ {address} - перейти если условие в CPX не равно 

С первоначальными командами разобрались, дальше надо разобраться с двумя основными вопросами:

  • Как отобразить фон?

  • Как нарисовать спрайт?

Отображение фона

Разрешение NES, В системе PAL 256×240, в системе NTSC 256×224. Фон состоит из спрайтов 8х8, соответственно 32х30 спрайтов в PAL, в NTSC верхний ряд спрайтов и нижний обрезаются. Для того что бы отобразить фон, необходимо 3 условия: палитра, nametable (таблица имен), таблица атрибутов.

  1. Палитра — палитра для фона содержит 16 цветов 4 вариации по 4 цвета. Как и палитра спрайтов собственно.

  2. Nametable — таблица 32х30 спрайтов, которая заполнена для отображения спрайтов на одном экране, таких таблиц может быть 2 без дополнительной оперативной памяти, если картридж содержит дополнительную память в размере 2КБайт то таких таблиц может быть 4-ре процессор поддерживает 4 таблицы. Но из за ограничения по памяти их две из коробки, в денди существует функционал зеркального отображения фона, который мы разберем в следующих статьях.

  3. Таблица атрибутов — в nes экран делиться на сетку с размером ячейки 32х32 или 2х2 спрайта, 8х8 ячеек, каждая ячейка содержит 1 байт битовой маски, %00000000. Что бы понять как она формируется давайте рассмотрим ячейку спрайтов 2 на 2 спрайта.
    ——————
    | 0 | 1 |
    ——————
    | 2 | 3 |
    ——————

    Я специально в разные ячейки поставил разные цифры, эти цифры означают порядковый номер политры, который будет применен к спрайту фона. Так вот каждые 2 бита маски обозначают какую палитру применить к спрайту в ячейке:

00 - 0-вая последовательность 4-х цветов 01 - 1-я 11 - 2-я 10 - 3-я

Немного отвлечемся и поговорим о принципе программирование на nes

Для того что бы отобразить к примеру спрайт, фон, или сделать скролинг экрана необходимо просто записать в соответствующий адрес данные, к примеру для прокрутки экрана по оси X нужно просто в адрес PPU $2005 записать сначала X координату потом Y, подчеркну последовательно.

LDA #25 ; десятичное значение X координаты STA $2005 ; записываем #25 в адрес LDA #00 ; 0 по оси Y STA $2005 ; записываем #0 в адрес

Разберем другой пример рисование спрайта он довольно прост

LDA #100     ; координата Y спрайта STA $2004   ; записываем в адрес OEM data PPU LDA #00     ; номер спрайта в таблице тайлов (по сути таил и есть таблица спрайтов) STA $2004   ; записываем в адрес LDA #%00010111  ; маска которая определяет отражение спрайта по вертикали  								; и горизонтали, а так же номер палитры STA $2004   ; сохраняем в адрес LDA #192    ; загружаем Y координату STA $2004   ; и снова записываем

Такая последовательность команд выведет спрайт 8х8 в скриншоте таких спрайтов 6. Теперь перейдем к более сложному, допустим мы определили что у нас есть таблица имен адресное пространство таблицы от $2000 — $2400 видео памяти, вторая таблица $2400 — $2800 и так далее. Для начала наша задача записать в данную ячейку памяти данные nametable. Для этого нам надо просто

LDA #$20 ; старший байт STA $2006 ; регистр 2байта тут мы указываем какой участок памяти мы бы хотели использовать LDA #$00 ; младший байт STA $2006

Тут мы снова видим принцип обычной последовательности записи, сначала X потом Y, либо адрес памяти в которую хотим произвести запись данных.

LDA $24 ; номер тайла или спрайта в банке CHR STA $2007 ; регистр для записи данных ; далее возможно записывать каждый спрайт в таблицу имен отдельно

Конечно ни кто не записывает руками данные а использует циклы, как и Я, приведу простой пример цикла на ассемблере

  LDX #00 ; загружаем 0 в x значение decimal так проще чем #$00 - #$FF к примеру section:    ; какие то действия загрузка тех же спрайтов    LDA $24   STA $2007   CPX #255   INX   BNE section

Код секции выполниться 255 раз и заполнит 255 значений таблице имен, это как пример. Для заполнения всех значений необходимо использовать аккумулятор Y и пройтись 4 раза таким циклом. Далее используя ту же логику мы можем загрузить палитру, палитра в памяти храниться с адреса $3F00-$3F0F — 16 байт соответственно. Используя предыдущую логику загрузки nametable нам необходимо сделать следующее

LDA #$3F ; старший байт адреса $3F STA $2006 LDA #$00 ; младший байт  STA $2006  LDA $22 ; цвет палитры nes синий STA $2007 ; записываем цвет в адресс $3F00  LDA $05 ; красный STA $2007 ; записываем цвет в адресс $3F01  ; и так далее еще 14 раз

Опять же палитру можно определить как последовательность байт и загрузить ее циклом который пробежится 16 раз или 16 байт

background_pallete:   .byt $01, $02, $03, $04    .byt $05, $22, $23, $24    .byt $25, $26, $27, $28   .byt $31, $32, $33, $34     LDX #$00 ; x равен 0     LDA #$3F ; старший байт адреса $3F 	 STA $2006 	 LDA #$00 ; младший байт  	 STA $2006  loadPallete:    LDA background_pallete, x ; background_pallete ссылается на адресс                               ; в котором храниться байт , x добавляет +1 к адресу    STA $2007 ; записываем данные    CPX #$0F ; сравниваем X с 16     INX ; X = X+1    BNE loadPallete ; повторяем до тех пор пока X не будет 16                     ; Тогда будет загруженны все цвета палитры в видеопамять 

Осталось разобраться с тем как загрузить атрибуты фона, это тоже довольно просто атрибуты хранятся в адресном пространстве видеопамяти начиная с $23C0 — $23FF каждый ряд начинается с $23C0, $23C8 и так далее то есть 8 рядов по 8 столбцов.

Теперь нам известны область памяти для применения определенной палитры для спрайта фона, так что, что бы ее применить нам необходимо, снова, очень просто сказать в какую ячейку памяти мы бы хотели записать нашу маску, загрузить ее в аккумулятор А и записать в регистр $2007

LDA #$23 STA $2006 LDA #$C2 STA $2006 ; получаем адрес $23C2  LDA %00001100 ; загружаем нашу маску атрибута, как ее вычислить я писал выше STA $2007 ; записываем в регистр 

Таким образом отображение фона превращается в довольно тривиальную задачу, результатом 2-х недель изучения темы асемблеры вы видели на скриншоте в заголовке статьи. На самом деле assembler nes не такой сложный если сломать стереотипы программирования которыми мы сейчас пользуемся, и взглянуть на все это проще, когда наступает момент понимание того что, асемблером возможно обратиться в доступный участок памяти записать туда некоторую информацию, которая потом волшебным образом отобразиться на экране. Это маленькая победа над собой, маленький шаг к мечте. Спасибо всем за внимание и до скорых встреч.

ссылка на оригинал статьи https://habr.com/ru/post/551488/