Оглавление


§ Распределение памяти

Перед тем как рассказать о видеопроцессоре, надо рассмотреть карту памяти, как распределено адресное пространство в Денди.
АдресОбъемОписание
$0000-$07FF2kОперативная память
$0800-$1FFF6k"Зеркала" памяти (x3)
$2000-$20078Видео-регистры
$2008-$3FFF$1FF8Зеркала видео-регистров (x1023)
$4000-$4017$20Регистры аудио и DMA & I/O
$4018-$4FFF$0FE8Не используется
$5000-$5FFF4kРасширение ROM / RAM
$6000-$7FFF8kSRAM (известно как WRAM)
$8000-$BFFF16kPRG-ROM (1)
$C000-$FFFF16kPRG-ROM (0)
Очень мало оперативной памяти выделили для Денди, всего лишь 2 килобайта, и это правда. Конечно, это не беда, ведь на картридже может быть официально выделено еще и 12К памяти, на самом деле, там может быть выделено и хоть гигабайт памяти, только обращение к такому объему будет занимать немало времени при скорости процессора в примерно 1.78 мгц всего. Курам на смех, но на самом деле, с такой скоростью и работают все игры. То что экран "едет" плавно, это только благодаря тому, что в PPU (Pixel Processing Unit) есть возможность аппаратного скроллера, без которого бы нормально в игры никто не смог поиграть.
Распределение этой оперативной памяти такое:
АдресаОписание
$0000-$00FFБыстрая память, нулевая страница, используется процессором для быстрого доступа к памяти, может даже использоваться в качестве расширения регистрового пространства
$0100-$01FFАппаратный стек, 256 байт всего лишь
$0200-$07FFПользовательская область
Есть очень интересная особенность памяти Денди — это "зеркала" (mirrors), так называемые. Они получаются из-за неполной дешифрации адреса, например, обращение к $1025 будет абсолютно равнозначно к обращению к $0025. Выходит, что есть основная 2кб памяти и остальные 6к составляют копии этой памяти. Зашибись, ничего не скажешь. Могли бы и 8Кб впихнуть, что ли. Но нет. Пожалели транзисторов, а народ выкручивается теперь как может.
Особенность процессора Денди в том, что там нет обращения к портам. Там нет портов, но зато можно обращаться к памяти в качестве портов. Например, по адресам $2000-$2007 располагаются регистры видеопроцессора, причем там еще сверху зачем-то 1023 "зеркала" (ну зачем?!), и запись и чтение с этих регистров равносильно записи и чтению из портов. Обращаясь к этим регистрам, можно читать и записывать данные в видеопамять, которая располагается не в адресном пространстве процессора. Ниже приведена таблица распределения адресов.
АдресРазмерНазначениеГде находится
$0000-$0FFF4kCHR-ROM Знакогенератор 0Картридж
$1000-$1FFF4kCHR-ROM Знакогенератор 1Картридж
$2000-$23BF$3C0Экранная страница 1 – СимволыПриставка
$23C0-$23FF$40Экранная страница 1 – АтрибутыПриставка
$2400-$26BF$3C0Экранная страница 2 – СимволыПриставка
$27C0-$27FF$40Экранная страница 2 – АтрибутыПриставка
$2800-$2BBF$3C0Экранная страница 3 – СимволыКартридж
$2BC0-$2BFF$40Экранная страница 3 – АтрибутыКартридж
$2C00-$2FBF$3C0Экранная страница 4 – СимволыКартридж
$2FC0-$2FFF$40Экранная страница 4 – АтрибутыКартридж
$3000-$3EFF$F00Зеркала $2000-2EFFПриставка/Картридж
$3F00-$3F0F16Палитра фонаРегистры PPU
$3F10-$3F1F16Палитра спрайтовРегистры PPU
$3F20-$3FFF224Не используются?
Как можно заметить, кое-какие данные находятся в приставке (например 2Кб VRAM), кое-какие на картридже. Знакогенератор точно находится на картридже, и он может, кстати говоря, меняться, это зависит от так называемого "маппера", который представляет из себя схему внутри картриджа, меняющего его поведение, при отсылке неких данных. Это другая тема, сложная, ее касаться не буду сегодня.

§ Формирование картинки

У Денди формируется экран размером 256 x 240 пикселей. Картинка состоит из двух слоев:
  • Фон — состоит из тайлов размером 8x8 каждый. Получается, что тайлами "вымощен" весь фон, 32 тайла в ширину и 30 строк из тайлов по высоте
  • Спрайты — Информация о спрайтах находится в особой области памяти, которая называется OAM, и доступ к ней с процессора весьма ограничен. Есть два способа загрузить OAM — это через DMA и через последовательную запись в регистры видеопроцессора. Все нормальные люди пользуются DMA обычно. Сам буфер размером 256 байт, и один спрайт описывается 4 байтами, то есть на экране одновременно может быть 64 спрайта, но не более 8 на одной линии! Это связано с тем, как спрайты обрабатываются, потом расскажу.
Один тайл занимает в видеопамяти 16 байт, и начертание тайла читается из CHR-ROM знакогенератора. Всего два банка знакогенератора, для фона и для спрайтов может быть выбран только один. То есть, мы можем программно указать, какой именно знакогенератор использовать для тайлов фона, и какой для тайлов спрайтов. Ниже приведен пример, как это работает.
0 %00010000 = $10 --
1 %00000000 = $00   ¦
2 %01000100 = $44   ¦
3 %00000000 = $00   +-- Бит 0
4 %11111110 = $FE   ¦
5 %00000000 = $00   ¦
6 %10000010 = $82   ¦
7 %00000000 = $00 --
8 %00000000 = $00 --
9 %00101000 = $28   ¦
A %01000100 = $44   ¦
B %10000010 = $82   +-- Бит 1
C %00000000 = $00   ¦
D %10000010 = $82   ¦
E %10000010 = $82   ¦
F %00000000 = $00 --
Дело в том, что один пиксель может принимать значение от 0 до 3, то есть, пиксель кодируется 4-мя цветами. Нулевой цвет всегда прозрачный. Всегда. Абсолютно всегда. Никаких исключений. Младший бит цвета берется из 0..7 байтов, а старший бит соответственно, из 8..15 байтов. Здесь строка 0..7 — это именно строка в тайле, а строки 8..15 это тоже строки от 0 до 7 в тайле, только обозначают старшие биты.
1int get_tile_pixel(int tile_id, int x, int y) {
2
3  int ax = 16*tile_id + y;               // Адрес в памяти
4  int b0 = ((chr_rom[ax    ]) >> x) & 1; // Младший бит
5  int b1 = ((chr_rom[ax + 8]) >> x) & 1; // Старший бит
6
7  return b0 + 2*b1;
8}
Выше начертан код, с помощью которого можно получить один пиксель из тайла, указывая tile_id, а также x=0..7, y=0..7. Этот код просто приведен для ознакомления и не претендует на полноценное научное обоснование.
Snimok_ekrana_ot_2023-05-29_06-24-31.png
На картинке выше я попытался нарисовать как укладывается тайловая карта на одной из страниц VRAM (Video RAM). Все страниц может быть 4, две из которых гарантированно находятся на картридже, потому что в приставке места нет для них в оперативной памяти.
Как я ранее уже упоминал, тайл может содержать всего лишь 4 цвета, формально 3, один прозрачный. Но в Денди есть атрибуты, которые расширяют количество доступных цветов до 16. Также есть две палитры, которые мы можем заполнять одним из 64 доступных цветов, но об этом позже. На картинке выше я показал разными цветами (логотипа винды) кластеры по 2x2 тайла. Дело в том, что в области атрибутов (64 байта), которая идет сразу же за тайловой картой, размер тайловой карты 960 байт, находится как раз информация о том, как раскрашивать эти тайлы.
Атрибут это байт, который содержит 4 блока по 2 бита:
   7 6 |   5 4 |   3 2   |    1 0
Желтый | Синий | Зеленый | Красный
Там где красный, зеленый, синий и желтый — это блоки, а не цвета, которые записаны в атрибуте. Например, биты 1 и 0 отвечают за тот блок из 2x2 тайла, которые на картинки помечены у меня красным цветом. Получается, что 1 байт атрибута кодирует сетку тайлов 4x4, 16 тайлов за 1 байт. Так как тайлов всего 960, то потребуется 60 байта атрибутов, чтобы закодировать всю область. Остается 4 байта, они ни к селу, ни к городу, не используются.
Цвет атрибута добавляет 2 старших бита к пикселю тайла, получая 4-х битный цвет. Как можно заметить, нарисовать нормальную картинку все равно не получится из-за того, что в пределах 2x2 тайлов мы все равно не можем использовать все 16 цветов, там будут 4 цвета, раскрашенные определенной группой цветов из палитры.
Существуют две палитры — одна для фона, другая для спрайтов. Причем в палитре 3 из 16 цвета бесполезны, потому что прозрачный цвет, который всегда выбирается из 0-го цвета палитры, вне зависимости от того, какой сейчас атрибут. Важно не перепутать, а то я перепутал однажды и долго не мог понять, почему не работает нормально.
Clipboard01.png
Палитра в Денди не блещет оптимизмом, там даже не 64 цвета, как можно отметить, есть много цветов, которые отображают черный цвет.
1uint32_t colormap[64] = {
2  /* 00 */ 0x757575, 0x271B8F, 0x0000AB, 0x47009F,
3  /* 04 */ 0x8F0077, 0xAB0013, 0xA70000, 0x7F0B00,
4  /* 08 */ 0x432F00, 0x004700, 0x005100, 0x003F17,
5  /* 0C */ 0x1B3F5F, 0x000000, 0x000000, 0x000000,
6  /* 10 */ 0xBCBCBC, 0x0073EF, 0x233BEF, 0x8300F3,
7  /* 14 */ 0xBF00BF, 0xE7005B, 0xDB2B00, 0xCB4F0F,
8  /* 18 */ 0x8B7300, 0x009700, 0x00AB00, 0x00933B,
9  /* 1C */ 0x00838B, 0x000000, 0x000000, 0x000000,
10  /* 20 */ 0xFFFFFF, 0x3FBFFF, 0x5F97FF, 0xA78BFD,
11  /* 24 */ 0xF77BFF, 0xFF77B7, 0xFF7763, 0xFF9B3B,
12  /* 28 */ 0xF3BF3F, 0x83D313, 0x4FDF4B, 0x58F898,
13  /* 2C */ 0x00EBDB, 0x000000, 0x000000, 0x000000,
14  /* 30 */ 0xFFFFFF, 0xABE7FF, 0xC7D7FF, 0xD7CBFF,
15  /* 34 */ 0xFFC7FF, 0xFFC7DB, 0xFFBFB3, 0xFFDBAB,
16  /* 38 */ 0xFFE7A3, 0xE3FFA3, 0xABF3BF, 0xB3FFCF,
17  /* 3C */ 0x9FFFF3, 0x000000, 0x000000, 0x000000
18};

§ Регистры видеопроцессора

Приведу некоторую информацию по регистрам.
РегистрRWБитНазначение
$2000W7Формирование запроса прерывания NMI при кадровом синхроимпульсе (0 -- запрещено; 1 -- разрешено)
6Не используется (должен быть 0)
5Размер спрайтов (0 -- 8x8; 1 -- 8x16)
4Выбор знакогенератора фона (0/1)
3Выбор знакогенератора спрайтов (0/1)
2Выбор режима инкремента адреса при обращении к видеопамяти (0 -- увеличение на единицу "горизонтальная запись"; 1 -- увеличение на 32 "вертикальная запись")
1..0Адрес активной экранной страницы (00 -- $2000; 01 -- $2400; 10 -- $2800; 11 -- $2C00)
$2001W7..5Яркость экрана/интенсивность цвета в RGB (в Денди не используется)
40 -- Спрайты не отображаются; 1 – Спрайты отображаются
30 -- Фон не отображается; 1 – Фон отображается
20 -- Спрайты не видны в крайнем левом столбце; 1 -- Все спрайты видны
10 -- Рисунок фона не виден в крайнем левом столбце; 1 -- Весь фон виден
0Тип дисплея: Color/Monochrome (в Денди не используется)
$2002R71 -- PPU генерирует обратный кадровый импульс; 0 -- PPU рисует картинку на экране. Сбрасывается при чтении.
6Устанавливается в 1 после вывода спрайта с номером 0. Сбрасывается при чтении или при кадровом синхроимпульсе.
51 -- На линии больше 8-и спрайтов; 0 -- меньше
41 -- Запись в видеопамять разрешена; 0 -- запрещена
3..0Не используются
$2003WВ регистр $2003 записывается адрес в памяти спрайтов ($00-$FF)
$2004RПосле чего с регистром $2004 производится операция чтения/записи. После каждой операции происходит автоинкремент адреса на единицу.
$2005WАппаратный скроллинг фоновой картинки. В регистр последовательно записываются два байта. Первый -- абсолютное значение вертикального скроллинга; второе – горизонтального. Но тут не все так просто.
$2006WОбеспечивают доступ к любой ячейке адресного пространства видеопроцессора. Регистр $2006 -- адрес (2байта), сначала записывается старший, потом младщий.
$2007RWРегистр $2007 – операционный буфер (чтение/запись). После каждой операции происходит автоинкремент адреса на 1 или на 32 (см. регистр $2000 -- бит 2).
Здесь много чего написано, что разбирать долго. Могу сказать, что работа с видеопамятью довольно-таки непростая получается. Для того, чтобы чего-то записать в видеопамять, надо сначала отослать адрес в $2006, отправить сначала старший байт адреса, потом отправить младший. При этом, при чтении из $2007-го порта, читается не то, что находится по тому адресу, которые мы отправили, а то, что было ранее во внутреннем регистре. После того, как чтение было произведено, только после этого во внутренний регистр записывается информация по указанному адресу.

§ Спрайты

Информация о них располагается в особом буфере OAM. Каждые 4 байта буфера обозначают некоторый спрайтовый тайл 8x8, приведу формат одной записи.
БайтБитыНазначение
0Абсолютная координата верхнего левого угла спрайта по вертикали
1Номер иконки (icon) из знакогенератора
27Отражение спрайта относительно вертикальной оси. 0 -- Обычный; 1 -- Зеркальный
6Отражение спрайта относительно горизонтальной оси. 0 -- Обычный; 1 -- Зеркальный
5Приоритет спрайта. 1 -- Спрайт перед фоном; 0 -- Спрайт за фоном
4..2Не используются
1..0Два старших бита цвета (аналог атрибута цвета для фона)
3Координата верхнего левого угла спрайта по горизонтали
Как можно заметить, с цветами у спрайтов получше будет, потому что каждый тайл хоть и содержит 3 цвета (исключается прозрачный), но в отличии от фоновой картинки, цветовая группа выбирается для каждого спрайта.
Интересная особенность спрайтов в том, что они могут быть либо за фоном, либо перед ним, это указывается в бите 5 второго байта. Также спрайт можно отразить по горизонтали и вертикали. Спрайты могут быть не только 8x8, но и 8x16 размером. В этом случае имеется возможность выбирать одну из тайловых карт. В младшем бите номера спрайта указывается номер банка, откуда тайл будет извлечен (0-й или 1-й банк). Номер иконки рассчитывается как icon & 0xFE, и соответственно, извлекается из 0, 2, 4 и т.д.
Когда рисуется очередная линия, видеопроцессор Денди пробегает все 256 байт в OAM и выбирает первые подходящие под условия отображения спрайты, то есть такие, которые находятся на текущей линии, и записывает их в особый буфер для отображения, которое происходит на следующей линии. Если на линии попадается спрайт с номером 0, и реально рисуется (прозрачный цвет не учитывается), то в бите 6 в порту $2002 устанавливается 1. При чтении центральным процессором эта единица сбрасывается в 0. Этот трюк был придуман для того, чтобы отслеживать положение луча, и знать, где конкретно он находится. В Марио к примеру, это нужно для того, чтобы начать скроллинг экрана в определенной точке.

§ Аппаратный скроллинг

Пожалуй, самая сложная тема из всех. Дело в том, что скроллинг в PPU устроен весьма своеобразно. Мало того, что есть регистры, куда можно записывать скроллер как нормальные люди, то есть, сколько будет по X, сколько по Y слева скроллится, так еще зависит от того, какой адрес будет установлен в данный момент перед рисованием строки. И записывая адрес в $2006, важно помнить, что это также повлияет на скроллер!
Сначала разберемся с простой темой. Есть регистр $2005, куда последовательно записываем сначала один байт, потом второй байт. Я намеренно не говорю, какой именно байт, потому что там работает так, что в зависимости от счетчика количества записей в $2005 будет играть роль, в какой именно байт (X или Y) будет записано. Допустим, это будет регистр first_write. Если он равен 0, то при записи в $2005 значение скроллинга будет записано по X. После каждой записи first_write будет меняться на противоположный, и станет равным 1. Когда будет записано значение в $2005, то запись произойдет в регистр скроллинга Y, соответственно, first_write опять вернется в 0. Но, читая что угодно из регистра $2002, значение first_write будет сброшено в 0. Так что перед записью скроллинга, лучше прочесть регистр $2002, чтобы быть уверенным в том, что запишется правильно в регистр $2005.
1LDA $2002   ; first_write = 0
2LDX #$20
3LDY #$30
4STX $2005   ; scroll.x = $20
5STY $2005   ; scroll.y = $30
В коде привел пример, как можно записывать в регистры скроллинга. Самое сложное начинается при записи в $2006. Так же, как и при записи в $2005, там учитывается first_write, и он влияет на то, куда будет записан как скроллинг, так и адрес памяти. Как я говорил ранее, при записи в первый раз, сначала происходит запись в старший адрес внутреннего указателя на видеопамять, а потом в младший байт адреса. Это же касается и скроллинга. Но пишется он туда весьма необычно.
После того, как адрес будет записан, значение регистров скроллинга примут определенные значения, основанные на текущем адресе указателя.
1scroll_x = 8*(address & 31)
2scroll_y = 8*((address >> 5) & 31) + ((address >> 12) & 3)
Я проверил на нескольких Deaendy-играх, и не увидел, чтобы использовался такой метод скроллинга, поэтому сложно сказать, работает этот метод или не работает.
На этом вступительная статья пока закончена, теперь приступаем к делу. К разработке Денди на верилоге.