Оглавление
§ Распределение памяти
Перед тем как рассказать о видеопроцессоре, надо рассмотреть карту памяти, как распределено адресное пространство в Денди.Адрес | Объем | Описание |
---|---|---|
$0000-$07FF | 2k | Оперативная память |
$0800-$1FFF | 6k | "Зеркала" памяти (x3) |
$2000-$2007 | 8 | Видео-регистры |
$2008-$3FFF | $1FF8 | Зеркала видео-регистров (x1023) |
$4000-$4017 | $20 | Регистры аудио и DMA & I/O |
$4018-$4FFF | $0FE8 | Не используется |
$5000-$5FFF | 4k | Расширение ROM / RAM |
$6000-$7FFF | 8k | SRAM (известно как WRAM) |
$8000-$BFFF | 16k | PRG-ROM (1) |
$C000-$FFFF | 16k | PRG-ROM (0) |
Распределение этой оперативной памяти такое:
Адреса | Описание |
---|---|
$0000-$00FF | Быстрая память, нулевая страница, используется процессором для быстрого доступа к памяти, может даже использоваться в качестве расширения регистрового пространства |
$0100-$01FF | Аппаратный стек, 256 байт всего лишь |
$0200-$07FF | Пользовательская область |
Особенность процессора Денди в том, что там нет обращения к портам. Там нет портов, но зато можно обращаться к памяти в качестве портов. Например, по адресам
$2000-$2007
располагаются регистры видеопроцессора, причем там еще сверху зачем-то 1023 "зеркала" (ну зачем?!), и запись и чтение с этих регистров равносильно записи и чтению из портов. Обращаясь к этим регистрам, можно читать и записывать данные в видеопамять, которая располагается не в адресном пространстве процессора. Ниже приведена таблица распределения адресов.Адрес | Размер | Назначение | Где находится |
---|---|---|---|
$0000-$0FFF | 4k | CHR-ROM Знакогенератор 0 | Картридж |
$1000-$1FFF | 4k | CHR-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-$3F0F | 16 | Палитра фона | Регистры PPU |
$3F10-$3F1F | 16 | Палитра спрайтов | Регистры PPU |
$3F20-$3FFF | 224 | Не используются | ? |
§ Формирование картинки
У Денди формируется экран размером 256 x 240 пикселей. Картинка состоит из двух слоев:- Фон — состоит из тайлов размером 8x8 каждый. Получается, что тайлами "вымощен" весь фон, 32 тайла в ширину и 30 строк из тайлов по высоте
- Спрайты — Информация о спрайтах находится в особой области памяти, которая называется OAM, и доступ к ней с процессора весьма ограничен. Есть два способа загрузить OAM — это через DMA и через последовательную запись в регистры видеопроцессора. Все нормальные люди пользуются DMA обычно. Сам буфер размером 256 байт, и один спрайт описывается 4 байтами, то есть на экране одновременно может быть 64 спрайта, но не более 8 на одной линии! Это связано с тем, как спрайты обрабатываются, потом расскажу.
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. Этот код просто приведен для ознакомления и не претендует на полноценное научное обоснование.На картинке выше я попытался нарисовать как укладывается тайловая карта на одной из страниц 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-го цвета палитры, вне зависимости от того, какой сейчас атрибут. Важно не перепутать, а то я перепутал однажды и долго не мог понять, почему не работает нормально.
Палитра в Денди не блещет оптимизмом, там даже не 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 | Бит | Назначение |
---|---|---|---|
$2000 | W | 7 | Формирование запроса прерывания 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) | ||
$2001 | W | 7..5 | Яркость экрана/интенсивность цвета в RGB (в Денди не используется) |
4 | 0 -- Спрайты не отображаются; 1 – Спрайты отображаются | ||
3 | 0 -- Фон не отображается; 1 – Фон отображается | ||
2 | 0 -- Спрайты не видны в крайнем левом столбце; 1 -- Все спрайты видны | ||
1 | 0 -- Рисунок фона не виден в крайнем левом столбце; 1 -- Весь фон виден | ||
0 | Тип дисплея: Color/Monochrome (в Денди не используется) | ||
$2002 | R | 7 | 1 -- PPU генерирует обратный кадровый импульс; 0 -- PPU рисует картинку на экране. Сбрасывается при чтении. |
6 | Устанавливается в 1 после вывода спрайта с номером 0. Сбрасывается при чтении или при кадровом синхроимпульсе. | ||
5 | 1 -- На линии больше 8-и спрайтов; 0 -- меньше | ||
4 | 1 -- Запись в видеопамять разрешена; 0 -- запрещена | ||
3..0 | Не используются | ||
$2003 | W | В регистр $2003 записывается адрес в памяти спрайтов ($00-$FF) | |
$2004 | R | После чего с регистром $2004 производится операция чтения/записи. После каждой операции происходит автоинкремент адреса на единицу. | |
$2005 | W | Аппаратный скроллинг фоновой картинки. В регистр последовательно записываются два байта. Первый -- абсолютное значение вертикального скроллинга; второе – горизонтального. Но тут не все так просто. | |
$2006 | W | Обеспечивают доступ к любой ячейке адресного пространства видеопроцессора. Регистр $2006 -- адрес (2байта), сначала записывается старший, потом младщий. | |
$2007 | RW | Регистр $2007 – операционный буфер (чтение/запись). После каждой операции происходит автоинкремент адреса на 1 или на 32 (см. регистр $2000 -- бит 2). |
§ Спрайты
Информация о них располагается в особом буфере OAM. Каждые 4 байта буфера обозначают некоторый спрайтовый тайл 8x8, приведу формат одной записи.Байт | Биты | Назначение |
---|---|---|
0 | Абсолютная координата верхнего левого угла спрайта по вертикали | |
1 | Номер иконки (icon) из знакогенератора | |
2 | 7 | Отражение спрайта относительно вертикальной оси. 0 -- Обычный; 1 -- Зеркальный |
6 | Отражение спрайта относительно горизонтальной оси. 0 -- Обычный; 1 -- Зеркальный | |
5 | Приоритет спрайта. 0 -- Спрайт перед фоном; 1 -- Спрайт за фоном | |
4..2 | Не используются | |
1..0 | Два старших бита цвета (аналог атрибута цвета для фона) | |
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-играх, и не увидел, чтобы использовался такой метод скроллинга, поэтому сложно сказать, работает этот метод или не работает.
На этом вступительная статья пока закончена, теперь приступаем к делу. К разработке Денди на верилоге.