Добрый день!
Я хочу рассказать о проекте игровой консоли Nintendo Entertainment System (NES) в реализации на FPGA. На постсоветском пространстве она известна как Dendy.
![NES на ПЛИС](https://habrastorage.org/r/w780q1/storage2/7c2/aec/464/7c2aec464f84283a3a436aa29297dea4.jpg)
Желающих посмотреть видео и поностальгировать прошу под кат.
Думаю, большинство людей моего возраста хорошо помнят эту игровую приставку. Была она и у меня. В 90-е годы денег в нашей семье было не очень много, поэтому у меня была даже не Денди, а совсем китайский клон Subor. Надо сказать, что работал он без каких-либо нареканий, если не считать часто ломающиеся джойстики, которые приходилось ремонтировать много раз. Конечно, спустя непродолжительный период я не смог удержаться от соблазна и разобрал приставку. Она была выполнена на двух печатных платах, одна – ВЧ модулятор и источник питания на LM7805, установленная без радиатора она сильно нагревалась, и вторая процессорная плата, которая, к большому сожалению, была выполнена на одной бескорпусной микросхеме – «капле». Насколько помню, больше на ней, кроме кварца, пары конденсаторов и разъема картриджа ничего не было. В те далекие времена очень трудно было найти какую-либо информацию, и я даже не знал, на каком процессоре работает Денди. Всего один раз я видел NES в «дискретной» реализации у соседа — радиолюбителя.
Чуть более полугода назад я заказывал на eBay отладочную плату на STM32 и увидел относительно недорогие комплекты ПЛИС Altera Cyclone II, недолго думая, заказал и его. Вообще, судя по форумам и мнениям знакомых электронщиков, тема ПЛИС до сих пор остается окутанной ореолом неприступности и сложности работы с ней. Я тоже в свое время «повелся» на это заблуждение и не уделял теме ПЛИС должного внимания, как оказалось, совершенно зря. Я влюбился в ПЛИС с первого взгляда! Труднодоступные для обычного радиолюбителя, занимавшегося микроконтроллерами, вещи, вдруг, стали реальностью. Например, полноценная работа со SDRAM, подключение ноутбучной матрицы с интерфейсом LVDS (а частоты там просто убойные). Аппетиты росли, и я купил себе плату Altera DE2-115, которая и используется для проекта NES. Теперь я очень жалею, что не занялся темой ПЛИС лет так 10 назад, не повторяйте моих ошибок, ПЛИС – это весело и вовсе не так сложно!
Помигав светодиодом (кстати, в отличие от того же STM32, где для этого надо настроить кучу периферии, на ПЛИС это делается до неприличия просто), монитором и поиграв со звуком, я решил сделать что-то посерьезнее.
Для чего все это? Как говориться, Just for fun. Конечно, кто-то может сказать, что это уже очень древняя платформа и особого смысла воссоздавать ее на ПЛИС нет, однако лично для меня было очень приятно заниматься этим проектом и увидеть конечный результат. Это, если так можно выразиться, как случайно найти и восстановить игрушку из своего детства, с которой связаны теплые воспоминания.
К тому же, по ощущениям при работе, аппаратная реализация значительно отличается от программных симуляторов. Отчасти это, конечно, и психологический эффект, но NES на ПЛИС точнее «держит» тайминги, отсутствуют едва уловимые задержки и артефакты видео, которые в программной реализации вызваны попытками оптимизировать алгоритм работы графического процессора, что достаточно сложно.
На русском языке в сети можно найти описание архитектуры NES, например, здесь.
В принципе, описание достаточно неплохое, однако создать работающий эмулятор по нему нельзя, так как в той статье очень поверхностно описаны регистры PPU — опущены очень важные моменты.
В этой статье я не буду подробно описывать архитектуру и каждый регистр управления, но постараюсь указать на возможные проблемы при реализации эмуляторов и опишу решения, которые я применял в своем проекте.
Перед вами электрическая принципиальная схема Nintendo Famicom:
![Схема Famicom](https://habrastorage.org/r/w1560/storage2/361/d68/f1c/361d68f1c875b6f22247a6f78d99b6f3.png)
Изображение с сайта nesdev.com
Ядро процессора A203 (U6) основано на базе восьмиразрядного микропроцессора MOS Technology 6502. На одном кристалле с 6502 находится контроллер DMA и аудиопроцессор.
На микросхеме 74LS139 (U3) выполнен декодер адреса CPU. Шина адреса 16-разрядная, поэтому процессор может адресовать до 64 КБ. Адресное пространство распределено следующим образом.
![Адресное пространство CPU](https://habrastorage.org/r/w1560/storage2/e11/c79/fc8/e11c79fc82ee13fab03f585fef59bee6.png)
Графический процессор (PPU) 2C02 – микросхема U5. Для экономии выводов PPU, младшие 8 бит шины адреса мультиплексированы с шиной данных, поэтому на доступ к памяти он тратит два такта. Для демультиплексирования адреса используется микросхема U2 — регистр-защелка 74LS373 (наш аналог ИР22).
Для хранения одной видеостраницы требуется 1 КБ памяти (изображение фона составлено из блоков (тайлов) 8x8 пикселей, а на экране помещается 30 рядов по 32 тайла). Архитектура PPU предусматривает использование 4-х видеостраниц, однако на самой NES установлено всего 2КБ видеопамяти (микросхема U4), а недостающие 2 КБ при необходимости использования всех 4-х страниц должны располагаться на картридже (отражение страниц и адресное пространство PPU будет рассмотрено ниже). Честно говоря, для меня, как инженера, это выглядит немного дико. Понятно, что во время разработки приставки на такие меры пошли из маркетинговых соображений и удешевления приставки ценой повышения стоимости картриджа. Я не знаю, сколько в то время стоили 4 КБ SRAM, но, возможно, из-за этого игр использующих 4 страницы совсем не много.
Задающий генератор выполнен на транзисторах Q2, Q3 и генерирует частоту 21.47727 МГц для NTSC версии и 26.6017 MГц для PAL. Ядро ЦП работает на частоте около 1.79 МГц, а пиксельная частота PPU в три раза выше частоты ЦП и приблизительно равна 5.37 МГц. Относительно высокая частота генератора – 24.47727 МГц требуется для кодирования цветовой информации в композитном видеосигнале и генерации «вспышки» цветовой поднесущей.
Первоначально я хотел найти оригинальные картриджи NES и использовать их, однако это мне не удалось и это хорошо, потому что пришлось делать эмуляцию картриджа. В самом простом варианте картридж – это просто две микросхемы ПЗУ — память программы CPU (PRG ROM) и память знакогенератора PPU (CHR ROM), в этом случае максимальный объем PRG ROM – 32 КБ, а CHR ROM – 8 КБ. Так можно запустить простые игры типа Super Mario Bros.1, Lode Runner, Popeye и т.д. Конечно, 32 КБ крайне мало для более-менее серьёзной игры, поэтому применяются специальные решения (мэпперы) для переключения банков памяти, которые позволяет расширить доступный объем до нескольких мегабайт. Вариантов мэпперов существует огромное множество, как на дискретной логике 74 серии, например, мэппер UxROM построен на счетчике 74HC161, который используется как защелка и 74HC32 – 4 элемента ИЛИ, так и специализированные ASIC решения, например MMC3. Сейчас в проекте реализованы только эти два мэппера. MMC3 выбран не случайно, так как именно на нем была выпущена основная масса всеми любимых игр.
В среднем объем картриджа составляет около 256 КБ, картриджи, объемом более 1 МБ большая редкость. На плате DE2-115 установлено 2 МБ памяти SRAM (1M x 16) и 128 МБ SDRAM. Я решил, что640 КБ хватит всем 2МБ для картриджа более чем достаточно и выделил 1 МБ для PRG ROM и 1 МБ для CHR ROM. Адресное пространство общее, в старшем байте слова хранятся данные PRG, а в младшем — CHR. Загрузка образов осуществляется с SD карты из файлов в формате iNES. Для обслуживания FAT и загрузки файлов я использовал процессор Nios II (в части самой NES все реализовано аппаратно, Nios не используется).
На диаграмме показан блок эмуляции картриджа:
![блок эмуляции картриджа](https://habrastorage.org/r/w1560/storage2/4da/bf2/aa5/4dabf2aa50075f6fe7eb1a7dc344855c.png)
Блок связан с процессором Nios II 4-х разрядной шиной адреса и 8-и битной шиной данных. Контроллер блока имеет 7 регистров управления, с помощью которых можно управлять состоянием NES – приостановить, произвести аппаратный сброс, задать опции отражения видеостраниц, тип мэппера, разрядность адресных шин PRG и CHR ROM. Для загрузки образа предусмотрены команды выбора области загрузки (CHR или PRG), сброса адреса. После записи очередного байта происходит автоинкремент адреса.
Так как шина адреса является общей для PPU и CPU, то блок мультиплексора работает на повышенной частоте, в моем случае это 32.22 МГц – в 6 раз выше частоты PPU. Далее, уже раздельные адресные шины заходят на блок мэпперов, где реализованы MMC3 и UxROM, а выбор активного мэппера задает состояние регистра управления. При необходимости, добавить поддержку любого другого мэппера очень легко.
Например, так выглядит реализация UxROM:
![UxROM](https://habrastorage.org/r/w1560/storage2/4e0/556/464/4e055646459a08e134a3cde4ff42a912.png)
В оригинальной NES, с аппаратной точки зрения, ничто не мешает в картридже вместо ПЗУ знакогенератора установить ОЗУ и инициализировать ее в процессе выполнения программы, производя запись через регистры PPU. Такие картриджи и игры существуют, например это Contra, Castlevania и множество других игр на UxROM. Этот подход имеет свои плюсы, например, часть икон знакогенератора может быть синтезирована программно, также можно хранить содержимое знакогенератора в сжатом виде и производить декомпрессию в CHR RAM, это здорово сэкономит пространство на картридже.
Так как я и так использую ОЗУ для хранения CHR, то эта функция получилась у меня по-умолчанию.
В качестве устройства ввода используется USB джойстик. Обслуживание USB тоже осуществляется процессором Nios II:
![Джойстик](https://habrastorage.org/r/w1560/storage2/800/ac6/205/800ac62052e72e2a4c7a66b9f5881af8.png)
Состояние всех кнопок джойстика упаковывается в слово и по параллельной шине заходит на блок сериализатора. Процессор NES, производя запись по адресу 0x4016, формирует стробирующий импульс, при этом происходит загрузка байта состояния (8 кнопок) в сдвиговый регистр. Далее, при обращении к 0x4016 (для первого джойстика) и 0x4017 (для второго) происходит сдвиг и считывание состояния очередной кнопки. Кнопки «Турбо» в оригинальной NES реализованы в самом джойстике и имитируют частое нажатие кнопок A/B, то есть на самом деле NES не различает какая кнопка зажата, «Turbo A» или «A». В проекте эта функция выполнена в блоке сериализатора, частота около 20 Гц.
Функционально PPU можно разделить на блок отрисовки фона и блок отрисовки спрайтов.
Если говорить про программные эмуляторы, то хочу заметить, что хоть сам алгоритм отрисовки и является простым, но из-за того, что CPU и PPU работают синхронно, создание корректного программного эмулятора является достаточно сложной задачей. Если точнее, то реализовать программный эмулятор PPU в лоб достаточно просто, но такая реализация будет потреблять много ресурсов и работать не оптимально. Думаю, любому программисту обязательно первой придет мысль, что если есть массив тайлов, находящихся на экране, и есть знакогенератор, то можно отрисовать весь кадр целиком или хотя бы его часть. Отрисовать, конечно, можно, но вот тут и начнутся проблемы с синхронной работой PPU и CPU. Все дело в том, что для создания графических эффектов CPU может менять регистры управления PPU прямо в процессе отрисовки кадра и может переключить банк памяти знакогенератора, причем не один раз в течение кадра. Такими действиями реализуются скроллинг экрана с разрывом, как вертикальный, так и горизонтальный, неподвижные области, вывод в них всевозможной статической информации. Поэтому эти моменты обязательно должны быть учтены, вплоть до количества циклов, за которое выполняется каждая команда CPU, чтобы точно знать в какой момент времени CPU производит действия над регистрами управления PPU.
Из-за таких особенностей, некоторые программные эмуляторы грешат визуальными артефактами в играх, особенно тех, которые нестандартно используют возможности PPU.
Адресное пространство PPU распределено следующим образом:
![Адресное пространство PPU](https://habrastorage.org/r/w1560/storage2/0f9/711/1ee/0f97111ee006f3a3c426f513f470d28e.png)
Видеостраницы располагаются таким образом, что, например, при горизонтальном или вертикальном скроллинге на экране будет видно содержимое сразу двух страниц, а при диагональном – четырех. Расположение страниц и координаты показаны на рисунке:
![Видеостраницы](https://habrastorage.org/r/w1560/storage2/b0b/152/bde/b0b152bdec10a58b37dfa3860eb8518c.png)
Как я уже писал выше, на самой NES под память страниц присутствует 2 КБ ОЗУ. Поэтому, если на картридже не установлена недостающая память, применяется отражение страниц. Вертикальное, когда вторая страница отражает нулевую, а третья – первую, и горизонтальное – первая страница отражает нулевую, третья – вторую.
В проекте на FPGA экономить 2 КБ нет никакого смысла, поэтому под видеопамять выделены все 4 КБ. Аппаратно отражение реализовано очень просто – в случае вертикального отражения у адресной шины ОЗУ оторвана от PPU и подключена к нулю линия, относящаяся к 11 биту, а в случае вертикального 11 и 10-я поменяны местами, а старшая линия тоже подключена к нулю:
![Отражение страниц](https://habrastorage.org/r/w1560/storage2/597/58f/011/59758f011dd7160c51ff173eab9d71b6.png)
Некоторые мэпперы могут динамически менять варианты отражения страниц, переключая линии шины адреса. В качестве примера можно привести игру Super Mario Bros. 2, где в самом начале игры при падении используется горизонтальное отражение, а потом производится переключение на вертикальное.
Для взаимодействия с PPU используется 8 регистров (имеется ввиду адресное пространство CPU).
Это регистр контролирует состояние PPU, например, задает размер спрайтов (8x8 или 8x16), страницу видеопамяти, и разрешение генерации прерывания NMI.
Регистр задает разрешение на отрисовку спрайтов и фона. Также с его помощью можно запретить отрисовку вообще, и тогда CPU сможет в любое время обращаться к областям памяти PPU, это используется играми при первоначальной инициализации страниц при смене игровой обстановки и заполнении CHR RAM, если вместо ПЗУ была использована ОЗУ. Без запрета, доступ к памяти PPU возможен только в VBLANK период, когда PPU не производит обращения к ней.
В этом регистре находятся флаги состояния PPU. Это факт начала отрисовки первого непрозрачного пикселя нулевого спрайта, начало VBLANK периода и флаг, указывающий, что на текущую линию попадают более 8 спрайтов.
Задает адрес для последующей манипуляции с памятью спрайтов (Object Attribute Memory – OAM).
Чтение и запись данных OAM. После операции происходит автоинкремент значения адреса. Обычно игры не пишут в OAM таким способом, а используют DMA.
Регистр скроллинга. При первой операции записи в регистр задается значение горизонтального скроллинга, при второй – вертикального.
Задает адрес для последующей операции с памятью видеостраниц. При первой операции задается старшая часть адреса, при второй – младшая.
Чтение и запись данных из памяти видеостраницы. После операции происходит автоинкремент значения адреса на 1 (следующая колонка) или на 32 (следующая строка), это зависит от состояния бита 2 регистра 0x2000.
Регистры с двойной записью используют общий триггер, поэтому нельзя, например, произвести однократную запись в 0x2005, затем в 0x2006, а потом опять вернуться к 0x2005. Точнее, можно, но нужно при этом понимать, зачем это делаешь. Если состояние триггера неизвестно, можно прочитать регистр 0x2002, при этом происходит сброс триггера.
Звучит все просто! Но тут есть крайне важная особенность, которая не указана в упомянутом выше русскоязычном описании.
Дело в том, что перечисленные регистры, так сказать, не совсем независимые.
Существует два 15-битных регистра:
vVRAM – текущий адрес видеопамяти (далее просто «v»);
tVRAM – временный адрес (далее просто «t»);
и 3-х битный регистр «точного» скроллинга по X (fine X scroll). Задает скроллинг (0..7) в пределах 1 тайла.
Адреса v и t формируются следующим образом:
![Формирование адреса](https://habrastorage.org/r/w1560/storage2/b07/7ea/c9e/b077eac9efad2488c3def823cad6990a.png)
Таким образом, задание страницы путем записи в регистр 0x2000 изменяет биты 11,10 регистра t. Запись в регистр 0x2005 задает значение битов 4:0 и 9:5/14:12 регистра t и значение «точного» скроллинга по X. А вот записью в регистр 0x2006 можно вообще все испортить, так как таким образом можно поменять значение сразу всех битов регистра t, причем при второй операции записи происходит копирование v = t.
В процессе отрисовки строки, PPU увеличивает значения грубого скроллинга по X в регистре v и соответствующим образом меняет адрес видеостраницы при переполнении значения грубого скроллинга. В конце видимой строки (пиксели 256-257) PPU увеличивает значение Y и производит копирование компонентов, относящихся к горизонтальному скроллингу (v[4:0] = t[4:0] и v[10] = t[10]). Перед началом нового кадра (pre-render) производится копирование компонентов вертикального скроллинга (v[9:5] = t[9:5], v[14:12] = t[14:12], v[11] = t[11]). И все начинается заново.
Теперь понятно, как меняя значения регистров PPU можно получить различные эффекты разрыва фона.
Цвет каждого пикселя выбирается из палитры. Для фона и спрайтов существуют отдельные палитры. Палитра – это область памяти, размером 16 байт. Нулевой элемент палитры фона задает цвет холста. При рендеринге пикселя фона формируется 4-х битный адрес, указывающий на элемент цвета в палитре. Два старших бита адреса являются значением атрибута группы тайлов, а два младших задаются изображением из знакогенератора. Меняя атрибуты группы можно менять цвет тайлов, используя один и тот же элемент знакогенератора.
Первые 960 байт видеостраницы (name table) задают адреса тайлов из знакогенератора CHR, находящихся на странице. Каждый из оставшихся 64 байтов видеостраницы (attribute table) задает атрибут группе из 16 тайлов (область 32x32 пикселя).
Для хранения изображения одной иконы в памяти знакогенератора используется 16 байт. Каждый пиксель, как уже было сказано выше, кодируется двумя битами. Первые 8 байт относятся к младшему биту пикселя, а следующие 8 байт – к старшему. То есть пара байтов 0+8, 1+9 и т.д. задают строки иконы.
![Формирование изображения](https://habrastorage.org/r/w1560/storage2/1a4/bba/6c7/1a4bba6c7f029babe752b603b62133f2.png)
Цвета в примере выбраны условно. Элементы, которые равны нулю, являются прозрачными и значение атрибута на них не влияет.
Атрибуты группы задаются следующим образом:
![Атрибуты](https://habrastorage.org/r/w1560/storage2/e40/6e4/196/e406e41960d4db2e3e7dbdd18c1f7fff.png)
За один такт PPU должен быть отрисован один пиксель. Надо понимать, что на оригинальной NES, за 2 такта PPU мы можем получить всего 1 значение из памяти. Поэтому перед началом строки в конце HBLANK периода осуществляется выборка данных для первых двух тайлов новой строки.
Выборка данных, необходимых для вывода строки тайла осуществляется за 8 тактов PPU. Сначала из видеопамяти получаем адрес тайла в знакогенераторе (2 такта), затем значение атрибута группы (2 такта), далее младший байт строки тайла из знакогенератора (2 такта), и, наконец, старший байт тайла (тоже 2 такта). И все начинается заново.
Так как я не мультиплексирую шину адреса с шиной данных, то в своем проекте могу получать данные на каждом такте PPU.
В разделе, описывающем регистры PPU, была упомянута область памяти спрайтов — Object Attribute Memory (OAM). Ее размер составляет 256 байт, она расположена на кристалле PPU в отдельном адресном пространстве, это означает, что к этой области возможен одновременный и независимый от, например, VRAM и CHR ROM доступ.
На каждый спрайт в OAM отводится 4 байта – поэтому одновременно на экране могут находиться не более 64-х спрайтов.
Изображения спрайтов также хранятся в знакогенераторе (CHR ROM).
Каждый спрайт на экране описывается его положением на экране по X и Y, адресом иконы в знакогенераторе, атрибутом (то же самое, что атрибут группы при отрисовке фона), флагами отражения спрайта по горизонтали и вертикали (для вывода симметричных объектов можно использовать половинки одной иконы знакогенератора) и флагом приоритета.
Спрайты могут иметь размер 8x8 и 8x16 пикселей.
Формирование изображения спрайта ничем не отличается от формирования изображения фона. Однако, в аппаратной реализации, опять же есть свои особенности.
Одновременно с отрисовкой строки производится поиск спрайтов, которые будут видимы (попадают) на следующую строку (in range evaluation). В PPU существует область памяти (secondary OAM), которая может хранить информацию о 8 спрайтах. Если при поиске окажется, что на следующей строке находятся более 8 спрайтов, то лишние спрайты игнорируются и в регистре PPU 0x2002 взводится флаг, сигнализирующий об этом.
Так как во время отрисовки строки шина CHR ROM занята, то выборка данных из знакогенератора о цвете пикселей этих 8 спрайтов происходит в HBLANK период.
NES формирует картинку разрешением 256x240 точек. Для вывода изображения я использую стандартное разрешение VGA 640x480. PPU производит рендеринг изображения в буфер кадров. Данные из буфера кадров поступают на блок, в котором происходит удвоение разрешения (upscaler). В будущем я хочу реализовать hq2x. Перед подачей данных на видео ЦАП происходит преобразование цвета в RGB.
![Вывод изображения](https://habrastorage.org/r/w1560/storage2/380/030/57b/38003057b5f26a5cd3ccb419fa11f4e3.png)
Для быстрого заполнения OAM, процессор NES может воспользоваться контроллером DMA. Контроллер DMA реализован очень просто. Перед началом операции копирования, CPU должен задать начальный адрес OAM (регистр 0x2003), точнее сбросить его в 0. Затем CPU производит запись по адресу 0x4014 значения начального адреса (0x??00) в адресном пространстве CPU. Контроллер DMA производит останов CPU и начинает копирование 256 байт из области 0x??00 – 0x??FF (где ?? – значение, заданное CPU) в регистр PPU 0x2004. PPU увеличивает адрес OAM на единицу при каждой операции записи. По окончании процедуры контроллер DMA возвращает управление CPU.
Конечное значение цвета пикселя формируется следующим образом:
![Слои изображения](https://habrastorage.org/r/w1560/storage2/da2/a1f/1e3/da2a1f1e3d5ce62c8eafa6737fadaf27.png)
Вполне возможна и часто случается ситуация, когда в одной области находятся более одного спрайта.
В этом случае на приоритет вывода на экран влияют два фактора – это значение адреса спрайта в OAM и флаг приоритета (0 – передний план, 1 – задний). Например, спрайт, у которого флаг приоритета указывает, что он относится к заднему плану, но при этом значение адреса в OAM меньше, чем у спрайта переднего плана, то спрайт заднего плана может закрыть спрайт переднего, что приведет к выводу пикселя фона, но только если он не прозрачный.
При отрисовке первого непрозрачного пикселя спрайта с адресом 0 (фон в этой точке тоже должен быть непрозрачный), в регистре 0x2002 взводится флаг, указывающий, что произошло событие Sprite 0 hit. В качестве примера применения этого флага можно привести игру Super Mario Bros. 1, в ней оно используется для разрыва экрана и отделения статичной информации об очках и времени от игровой области. В качестве фона выступает изображение монетки, а в качестве нулевого спрайта – ее тень. CPU периодически проверяет значение флага, и при возникновении события начинается вывод игрового поля.
![Пример Sprite 0 Hit](https://habrastorage.org/r/w1560/storage2/c35/809/0fc/c358090fcddb5feced18871df2ed8bf5.png)
Мэппер MMC3 имеет в составе счетчик строк, значение которого уменьшается при выводе очередной строки PPU. При достижении нуля счетчик перезагружается значением, которое может быть задано предварительной записью в соответствующий регистр мэппера и, если установлен флаг разрешения, возникает прерывание CPU. Достаточно оригинально организована линия тактирования счетчика – она подключена к адресной линии A12 графического процессора. Для хранения тайлов фона обычно используется младший банк (область 0x000 – 0x0FFF), а для спрайтов – старший банк (область 0x1000 – 0x1FFF). При отрисовке видимой строки PPU обращается к одному банку, а при выборке данных изображения спрайтов в HBLANK период – к другому. Поэтому частота на линии A12 будет соответствовать частоте вывода строк. Прерывания мэппера применяются в основном для разрыва экрана и переключения банка знакогенератора.
Примеры можете посмотреть на видео.
Аудиопроцессор NES находится на одном кристалле с CPU. Функционально APU – это набор регистров управления, счетчик кадров и 5 блоков аудиоканалов.
С аппаратной точки зрения APU это куча счетчиков, подводных камней там нет, поэтому описание будет кратким.
Счетчик кадров формирует тактирующие импульсы частотой примерно 240 Гц, 120 Гц для блоков APU, а также прерывание IRQ для CPU. Генерацию прерывания можно отключить с помощью задания регистров APU. Не следует путать понятие «кадры», в данном случае никакого отношения к PPU оно не имеет.
Кстати, при разработке произошел неприятный случай, счетчик кадров APU был уже реализован и я напутал с флагом разрешения прерывания (он оказался инвертирован), поэтому прерывание генерировалось с частотой примерно 60 Гц. Внешне это проявилось очень неожиданно – в игре «Принц Персии» в левой части экрана не выводился проем двери с решеткой, причем это был единственный графический артефакт. Я голову сломал, пока нашел настоящую причину. Причем думал, естественно, на PPU – сто раз проверил код, смотря в ModelSim. Это было неприятно!
APU обходится пятью каналами:
Два прямоугольных канала, как видно из названия, они формируют сигнал прямоугольной формы, один треугольный канал, один канал шума и канал дельта-модуляции (DMC).
Прямоугольные каналы могут формировать сигнал с изменяемыми скважностью (4 градации) и периодом, с возможностью задания длительности, а также имеют блоки sweep и envelope. Блок sweep может изменять (последовательно увеличивать или уменьшать) период сигнала во времени, а блок огибающей (envelope) – уменьшать размах сигнала во времени с возможностью зацикливания, форма огибающей в этом случае – пилообразная.
Треугольный канал формирует сигнал треугольной формы с изменяемой частотой и длительностью, однако не имеет возможности регулировки громкости.
Канал шума генерирует псевдослучайный сигнал с изменяемой длительностью. Также, как и прямоугольный канал, канал шума имеет блок контроля огибающей (envelope).
Канал дельта-модуляции в проекте пока не реализован.
Микширование каналов производится табличным способом, чтобы не использовать «тяжелые» с точки зрения потребления LE операции.
После микширования сигнал поступает на блок синхронизации домена и фильтр, после чего через блок сериализатора по шине I2S уходит на аудиокодек WM8731.
![Вывод звука](https://habrastorage.org/r/w1560/storage2/bbb/2f5/ff4/bbb2f5ff41bcbfa7aa5aa9088a331249.png)
При реализации проекта я пользовался языком описания аппаратуры Verilog 2001.
Предвосхищая вопросы относительно исходного кода, могу сказать, что, так как это мой первый большой проект на ПЛИС, то наверняка я реализовал многие вещи очень неэффективным способом. К тому же это самая первая версия, код нужно оптимизировать и очищать, потому что многие блоки я переписывал несколько раз. Поэтому я бы не хотел предоставлять его в таком виде. Если я найду время, соберусь и приведу код в порядок, то, возможно он будет выложен под лицензией GNU GPL.
Спасибо за внимание!
Я хочу рассказать о проекте игровой консоли Nintendo Entertainment System (NES) в реализации на FPGA. На постсоветском пространстве она известна как Dendy.
![NES на ПЛИС](https://habrastorage.org/storage2/7c2/aec/464/7c2aec464f84283a3a436aa29297dea4.jpg)
Желающих посмотреть видео и поностальгировать прошу под кат.
Думаю, большинство людей моего возраста хорошо помнят эту игровую приставку. Была она и у меня. В 90-е годы денег в нашей семье было не очень много, поэтому у меня была даже не Денди, а совсем китайский клон Subor. Надо сказать, что работал он без каких-либо нареканий, если не считать часто ломающиеся джойстики, которые приходилось ремонтировать много раз. Конечно, спустя непродолжительный период я не смог удержаться от соблазна и разобрал приставку. Она была выполнена на двух печатных платах, одна – ВЧ модулятор и источник питания на LM7805, установленная без радиатора она сильно нагревалась, и вторая процессорная плата, которая, к большому сожалению, была выполнена на одной бескорпусной микросхеме – «капле». Насколько помню, больше на ней, кроме кварца, пары конденсаторов и разъема картриджа ничего не было. В те далекие времена очень трудно было найти какую-либо информацию, и я даже не знал, на каком процессоре работает Денди. Всего один раз я видел NES в «дискретной» реализации у соседа — радиолюбителя.
Чуть более полугода назад я заказывал на eBay отладочную плату на STM32 и увидел относительно недорогие комплекты ПЛИС Altera Cyclone II, недолго думая, заказал и его. Вообще, судя по форумам и мнениям знакомых электронщиков, тема ПЛИС до сих пор остается окутанной ореолом неприступности и сложности работы с ней. Я тоже в свое время «повелся» на это заблуждение и не уделял теме ПЛИС должного внимания, как оказалось, совершенно зря. Я влюбился в ПЛИС с первого взгляда! Труднодоступные для обычного радиолюбителя, занимавшегося микроконтроллерами, вещи, вдруг, стали реальностью. Например, полноценная работа со SDRAM, подключение ноутбучной матрицы с интерфейсом LVDS (а частоты там просто убойные). Аппетиты росли, и я купил себе плату Altera DE2-115, которая и используется для проекта NES. Теперь я очень жалею, что не занялся темой ПЛИС лет так 10 назад, не повторяйте моих ошибок, ПЛИС – это весело и вовсе не так сложно!
Помигав светодиодом (кстати, в отличие от того же STM32, где для этого надо настроить кучу периферии, на ПЛИС это делается до неприличия просто), монитором и поиграв со звуком, я решил сделать что-то посерьезнее.
Для чего все это? Как говориться, Just for fun. Конечно, кто-то может сказать, что это уже очень древняя платформа и особого смысла воссоздавать ее на ПЛИС нет, однако лично для меня было очень приятно заниматься этим проектом и увидеть конечный результат. Это, если так можно выразиться, как случайно найти и восстановить игрушку из своего детства, с которой связаны теплые воспоминания.
К тому же, по ощущениям при работе, аппаратная реализация значительно отличается от программных симуляторов. Отчасти это, конечно, и психологический эффект, но NES на ПЛИС точнее «держит» тайминги, отсутствуют едва уловимые задержки и артефакты видео, которые в программной реализации вызваны попытками оптимизировать алгоритм работы графического процессора, что достаточно сложно.
На русском языке в сети можно найти описание архитектуры NES, например, здесь.
В принципе, описание достаточно неплохое, однако создать работающий эмулятор по нему нельзя, так как в той статье очень поверхностно описаны регистры PPU — опущены очень важные моменты.
В этой статье я не буду подробно описывать архитектуру и каждый регистр управления, но постараюсь указать на возможные проблемы при реализации эмуляторов и опишу решения, которые я применял в своем проекте.
Перед вами электрическая принципиальная схема Nintendo Famicom:
![Схема Famicom](https://habrastorage.org/storage2/361/d68/f1c/361d68f1c875b6f22247a6f78d99b6f3.png)
Изображение с сайта nesdev.com
Ядро процессора A203 (U6) основано на базе восьмиразрядного микропроцессора MOS Technology 6502. На одном кристалле с 6502 находится контроллер DMA и аудиопроцессор.
На микросхеме 74LS139 (U3) выполнен декодер адреса CPU. Шина адреса 16-разрядная, поэтому процессор может адресовать до 64 КБ. Адресное пространство распределено следующим образом.
![Адресное пространство CPU](https://habrastorage.org/storage2/e11/c79/fc8/e11c79fc82ee13fab03f585fef59bee6.png)
Графический процессор (PPU) 2C02 – микросхема U5. Для экономии выводов PPU, младшие 8 бит шины адреса мультиплексированы с шиной данных, поэтому на доступ к памяти он тратит два такта. Для демультиплексирования адреса используется микросхема U2 — регистр-защелка 74LS373 (наш аналог ИР22).
Для хранения одной видеостраницы требуется 1 КБ памяти (изображение фона составлено из блоков (тайлов) 8x8 пикселей, а на экране помещается 30 рядов по 32 тайла). Архитектура PPU предусматривает использование 4-х видеостраниц, однако на самой NES установлено всего 2КБ видеопамяти (микросхема U4), а недостающие 2 КБ при необходимости использования всех 4-х страниц должны располагаться на картридже (отражение страниц и адресное пространство PPU будет рассмотрено ниже). Честно говоря, для меня, как инженера, это выглядит немного дико. Понятно, что во время разработки приставки на такие меры пошли из маркетинговых соображений и удешевления приставки ценой повышения стоимости картриджа. Я не знаю, сколько в то время стоили 4 КБ SRAM, но, возможно, из-за этого игр использующих 4 страницы совсем не много.
Задающий генератор выполнен на транзисторах Q2, Q3 и генерирует частоту 21.47727 МГц для NTSC версии и 26.6017 MГц для PAL. Ядро ЦП работает на частоте около 1.79 МГц, а пиксельная частота PPU в три раза выше частоты ЦП и приблизительно равна 5.37 МГц. Относительно высокая частота генератора – 24.47727 МГц требуется для кодирования цветовой информации в композитном видеосигнале и генерации «вспышки» цветовой поднесущей.
Эмуляция картриджа
Первоначально я хотел найти оригинальные картриджи NES и использовать их, однако это мне не удалось и это хорошо, потому что пришлось делать эмуляцию картриджа. В самом простом варианте картридж – это просто две микросхемы ПЗУ — память программы CPU (PRG ROM) и память знакогенератора PPU (CHR ROM), в этом случае максимальный объем PRG ROM – 32 КБ, а CHR ROM – 8 КБ. Так можно запустить простые игры типа Super Mario Bros.1, Lode Runner, Popeye и т.д. Конечно, 32 КБ крайне мало для более-менее серьёзной игры, поэтому применяются специальные решения (мэпперы) для переключения банков памяти, которые позволяет расширить доступный объем до нескольких мегабайт. Вариантов мэпперов существует огромное множество, как на дискретной логике 74 серии, например, мэппер UxROM построен на счетчике 74HC161, который используется как защелка и 74HC32 – 4 элемента ИЛИ, так и специализированные ASIC решения, например MMC3. Сейчас в проекте реализованы только эти два мэппера. MMC3 выбран не случайно, так как именно на нем была выпущена основная масса всеми любимых игр.
В среднем объем картриджа составляет около 256 КБ, картриджи, объемом более 1 МБ большая редкость. На плате DE2-115 установлено 2 МБ памяти SRAM (1M x 16) и 128 МБ SDRAM. Я решил, что
На диаграмме показан блок эмуляции картриджа:
![блок эмуляции картриджа](https://habrastorage.org/storage2/4da/bf2/aa5/4dabf2aa50075f6fe7eb1a7dc344855c.png)
Блок связан с процессором Nios II 4-х разрядной шиной адреса и 8-и битной шиной данных. Контроллер блока имеет 7 регистров управления, с помощью которых можно управлять состоянием NES – приостановить, произвести аппаратный сброс, задать опции отражения видеостраниц, тип мэппера, разрядность адресных шин PRG и CHR ROM. Для загрузки образа предусмотрены команды выбора области загрузки (CHR или PRG), сброса адреса. После записи очередного байта происходит автоинкремент адреса.
Так как шина адреса является общей для PPU и CPU, то блок мультиплексора работает на повышенной частоте, в моем случае это 32.22 МГц – в 6 раз выше частоты PPU. Далее, уже раздельные адресные шины заходят на блок мэпперов, где реализованы MMC3 и UxROM, а выбор активного мэппера задает состояние регистра управления. При необходимости, добавить поддержку любого другого мэппера очень легко.
Например, так выглядит реализация UxROM:
![UxROM](https://habrastorage.org/storage2/4e0/556/464/4e055646459a08e134a3cde4ff42a912.png)
В оригинальной NES, с аппаратной точки зрения, ничто не мешает в картридже вместо ПЗУ знакогенератора установить ОЗУ и инициализировать ее в процессе выполнения программы, производя запись через регистры PPU. Такие картриджи и игры существуют, например это Contra, Castlevania и множество других игр на UxROM. Этот подход имеет свои плюсы, например, часть икон знакогенератора может быть синтезирована программно, также можно хранить содержимое знакогенератора в сжатом виде и производить декомпрессию в CHR RAM, это здорово сэкономит пространство на картридже.
Так как я и так использую ОЗУ для хранения CHR, то эта функция получилась у меня по-умолчанию.
Ввод
В качестве устройства ввода используется USB джойстик. Обслуживание USB тоже осуществляется процессором Nios II:
![Джойстик](https://habrastorage.org/storage2/800/ac6/205/800ac62052e72e2a4c7a66b9f5881af8.png)
Состояние всех кнопок джойстика упаковывается в слово и по параллельной шине заходит на блок сериализатора. Процессор NES, производя запись по адресу 0x4016, формирует стробирующий импульс, при этом происходит загрузка байта состояния (8 кнопок) в сдвиговый регистр. Далее, при обращении к 0x4016 (для первого джойстика) и 0x4017 (для второго) происходит сдвиг и считывание состояния очередной кнопки. Кнопки «Турбо» в оригинальной NES реализованы в самом джойстике и имитируют частое нажатие кнопок A/B, то есть на самом деле NES не различает какая кнопка зажата, «Turbo A» или «A». В проекте эта функция выполнена в блоке сериализатора, частота около 20 Гц.
PPU
Функционально PPU можно разделить на блок отрисовки фона и блок отрисовки спрайтов.
Если говорить про программные эмуляторы, то хочу заметить, что хоть сам алгоритм отрисовки и является простым, но из-за того, что CPU и PPU работают синхронно, создание корректного программного эмулятора является достаточно сложной задачей. Если точнее, то реализовать программный эмулятор PPU в лоб достаточно просто, но такая реализация будет потреблять много ресурсов и работать не оптимально. Думаю, любому программисту обязательно первой придет мысль, что если есть массив тайлов, находящихся на экране, и есть знакогенератор, то можно отрисовать весь кадр целиком или хотя бы его часть. Отрисовать, конечно, можно, но вот тут и начнутся проблемы с синхронной работой PPU и CPU. Все дело в том, что для создания графических эффектов CPU может менять регистры управления PPU прямо в процессе отрисовки кадра и может переключить банк памяти знакогенератора, причем не один раз в течение кадра. Такими действиями реализуются скроллинг экрана с разрывом, как вертикальный, так и горизонтальный, неподвижные области, вывод в них всевозможной статической информации. Поэтому эти моменты обязательно должны быть учтены, вплоть до количества циклов, за которое выполняется каждая команда CPU, чтобы точно знать в какой момент времени CPU производит действия над регистрами управления PPU.
Из-за таких особенностей, некоторые программные эмуляторы грешат визуальными артефактами в играх, особенно тех, которые нестандартно используют возможности PPU.
Адресное пространство PPU распределено следующим образом:
![Адресное пространство PPU](https://habrastorage.org/storage2/0f9/711/1ee/0f97111ee006f3a3c426f513f470d28e.png)
Видеостраницы располагаются таким образом, что, например, при горизонтальном или вертикальном скроллинге на экране будет видно содержимое сразу двух страниц, а при диагональном – четырех. Расположение страниц и координаты показаны на рисунке:
![Видеостраницы](https://habrastorage.org/storage2/b0b/152/bde/b0b152bdec10a58b37dfa3860eb8518c.png)
Как я уже писал выше, на самой NES под память страниц присутствует 2 КБ ОЗУ. Поэтому, если на картридже не установлена недостающая память, применяется отражение страниц. Вертикальное, когда вторая страница отражает нулевую, а третья – первую, и горизонтальное – первая страница отражает нулевую, третья – вторую.
В проекте на FPGA экономить 2 КБ нет никакого смысла, поэтому под видеопамять выделены все 4 КБ. Аппаратно отражение реализовано очень просто – в случае вертикального отражения у адресной шины ОЗУ оторвана от PPU и подключена к нулю линия, относящаяся к 11 биту, а в случае вертикального 11 и 10-я поменяны местами, а старшая линия тоже подключена к нулю:
![Отражение страниц](https://habrastorage.org/storage2/597/58f/011/59758f011dd7160c51ff173eab9d71b6.png)
Некоторые мэпперы могут динамически менять варианты отражения страниц, переключая линии шины адреса. В качестве примера можно привести игру Super Mario Bros. 2, где в самом начале игры при падении используется горизонтальное отражение, а потом производится переключение на вертикальное.
Регистры PPU
Для взаимодействия с PPU используется 8 регистров (имеется ввиду адресное пространство CPU).
Регистр 0x2000 (только запись)
Это регистр контролирует состояние PPU, например, задает размер спрайтов (8x8 или 8x16), страницу видеопамяти, и разрешение генерации прерывания NMI.
Регистр 0x2001 (только запись)
Регистр задает разрешение на отрисовку спрайтов и фона. Также с его помощью можно запретить отрисовку вообще, и тогда CPU сможет в любое время обращаться к областям памяти PPU, это используется играми при первоначальной инициализации страниц при смене игровой обстановки и заполнении CHR RAM, если вместо ПЗУ была использована ОЗУ. Без запрета, доступ к памяти PPU возможен только в VBLANK период, когда PPU не производит обращения к ней.
Регистр 0x2002 (только чтение)
В этом регистре находятся флаги состояния PPU. Это факт начала отрисовки первого непрозрачного пикселя нулевого спрайта, начало VBLANK периода и флаг, указывающий, что на текущую линию попадают более 8 спрайтов.
Регистр 0x2003 (только запись)
Задает адрес для последующей манипуляции с памятью спрайтов (Object Attribute Memory – OAM).
Регистр 0x2004 (запись/чтение)
Чтение и запись данных OAM. После операции происходит автоинкремент значения адреса. Обычно игры не пишут в OAM таким способом, а используют DMA.
Регистр 0x2005 (только запись)
Регистр скроллинга. При первой операции записи в регистр задается значение горизонтального скроллинга, при второй – вертикального.
Регистр 0x2006 (только запись)
Задает адрес для последующей операции с памятью видеостраниц. При первой операции задается старшая часть адреса, при второй – младшая.
Регистр 0x2007 (запись/чтение)
Чтение и запись данных из памяти видеостраницы. После операции происходит автоинкремент значения адреса на 1 (следующая колонка) или на 32 (следующая строка), это зависит от состояния бита 2 регистра 0x2000.
Регистры с двойной записью используют общий триггер, поэтому нельзя, например, произвести однократную запись в 0x2005, затем в 0x2006, а потом опять вернуться к 0x2005. Точнее, можно, но нужно при этом понимать, зачем это делаешь. Если состояние триггера неизвестно, можно прочитать регистр 0x2002, при этом происходит сброс триггера.
Звучит все просто! Но тут есть крайне важная особенность, которая не указана в упомянутом выше русскоязычном описании.
Дело в том, что перечисленные регистры, так сказать, не совсем независимые.
Существует два 15-битных регистра:
vVRAM – текущий адрес видеопамяти (далее просто «v»);
tVRAM – временный адрес (далее просто «t»);
и 3-х битный регистр «точного» скроллинга по X (fine X scroll). Задает скроллинг (0..7) в пределах 1 тайла.
Адреса v и t формируются следующим образом:
![Формирование адреса](https://habrastorage.org/storage2/b07/7ea/c9e/b077eac9efad2488c3def823cad6990a.png)
Таким образом, задание страницы путем записи в регистр 0x2000 изменяет биты 11,10 регистра t. Запись в регистр 0x2005 задает значение битов 4:0 и 9:5/14:12 регистра t и значение «точного» скроллинга по X. А вот записью в регистр 0x2006 можно вообще все испортить, так как таким образом можно поменять значение сразу всех битов регистра t, причем при второй операции записи происходит копирование v = t.
В процессе отрисовки строки, PPU увеличивает значения грубого скроллинга по X в регистре v и соответствующим образом меняет адрес видеостраницы при переполнении значения грубого скроллинга. В конце видимой строки (пиксели 256-257) PPU увеличивает значение Y и производит копирование компонентов, относящихся к горизонтальному скроллингу (v[4:0] = t[4:0] и v[10] = t[10]). Перед началом нового кадра (pre-render) производится копирование компонентов вертикального скроллинга (v[9:5] = t[9:5], v[14:12] = t[14:12], v[11] = t[11]). И все начинается заново.
Теперь понятно, как меняя значения регистров PPU можно получить различные эффекты разрыва фона.
Немного о формировании изображения
Цвет каждого пикселя выбирается из палитры. Для фона и спрайтов существуют отдельные палитры. Палитра – это область памяти, размером 16 байт. Нулевой элемент палитры фона задает цвет холста. При рендеринге пикселя фона формируется 4-х битный адрес, указывающий на элемент цвета в палитре. Два старших бита адреса являются значением атрибута группы тайлов, а два младших задаются изображением из знакогенератора. Меняя атрибуты группы можно менять цвет тайлов, используя один и тот же элемент знакогенератора.
Первые 960 байт видеостраницы (name table) задают адреса тайлов из знакогенератора CHR, находящихся на странице. Каждый из оставшихся 64 байтов видеостраницы (attribute table) задает атрибут группе из 16 тайлов (область 32x32 пикселя).
Для хранения изображения одной иконы в памяти знакогенератора используется 16 байт. Каждый пиксель, как уже было сказано выше, кодируется двумя битами. Первые 8 байт относятся к младшему биту пикселя, а следующие 8 байт – к старшему. То есть пара байтов 0+8, 1+9 и т.д. задают строки иконы.
![Формирование изображения](https://habrastorage.org/storage2/1a4/bba/6c7/1a4bba6c7f029babe752b603b62133f2.png)
Цвета в примере выбраны условно. Элементы, которые равны нулю, являются прозрачными и значение атрибута на них не влияет.
Атрибуты группы задаются следующим образом:
![Атрибуты](https://habrastorage.org/storage2/e40/6e4/196/e406e41960d4db2e3e7dbdd18c1f7fff.png)
За один такт PPU должен быть отрисован один пиксель. Надо понимать, что на оригинальной NES, за 2 такта PPU мы можем получить всего 1 значение из памяти. Поэтому перед началом строки в конце HBLANK периода осуществляется выборка данных для первых двух тайлов новой строки.
Выборка данных, необходимых для вывода строки тайла осуществляется за 8 тактов PPU. Сначала из видеопамяти получаем адрес тайла в знакогенераторе (2 такта), затем значение атрибута группы (2 такта), далее младший байт строки тайла из знакогенератора (2 такта), и, наконец, старший байт тайла (тоже 2 такта). И все начинается заново.
Так как я не мультиплексирую шину адреса с шиной данных, то в своем проекте могу получать данные на каждом такте PPU.
Спрайты
В разделе, описывающем регистры PPU, была упомянута область памяти спрайтов — Object Attribute Memory (OAM). Ее размер составляет 256 байт, она расположена на кристалле PPU в отдельном адресном пространстве, это означает, что к этой области возможен одновременный и независимый от, например, VRAM и CHR ROM доступ.
На каждый спрайт в OAM отводится 4 байта – поэтому одновременно на экране могут находиться не более 64-х спрайтов.
Изображения спрайтов также хранятся в знакогенераторе (CHR ROM).
Каждый спрайт на экране описывается его положением на экране по X и Y, адресом иконы в знакогенераторе, атрибутом (то же самое, что атрибут группы при отрисовке фона), флагами отражения спрайта по горизонтали и вертикали (для вывода симметричных объектов можно использовать половинки одной иконы знакогенератора) и флагом приоритета.
Спрайты могут иметь размер 8x8 и 8x16 пикселей.
Формирование изображения спрайта ничем не отличается от формирования изображения фона. Однако, в аппаратной реализации, опять же есть свои особенности.
Одновременно с отрисовкой строки производится поиск спрайтов, которые будут видимы (попадают) на следующую строку (in range evaluation). В PPU существует область памяти (secondary OAM), которая может хранить информацию о 8 спрайтах. Если при поиске окажется, что на следующей строке находятся более 8 спрайтов, то лишние спрайты игнорируются и в регистре PPU 0x2002 взводится флаг, сигнализирующий об этом.
Так как во время отрисовки строки шина CHR ROM занята, то выборка данных из знакогенератора о цвете пикселей этих 8 спрайтов происходит в HBLANK период.
NES формирует картинку разрешением 256x240 точек. Для вывода изображения я использую стандартное разрешение VGA 640x480. PPU производит рендеринг изображения в буфер кадров. Данные из буфера кадров поступают на блок, в котором происходит удвоение разрешения (upscaler). В будущем я хочу реализовать hq2x. Перед подачей данных на видео ЦАП происходит преобразование цвета в RGB.
![Вывод изображения](https://habrastorage.org/storage2/380/030/57b/38003057b5f26a5cd3ccb419fa11f4e3.png)
Контроллер DMA
Для быстрого заполнения OAM, процессор NES может воспользоваться контроллером DMA. Контроллер DMA реализован очень просто. Перед началом операции копирования, CPU должен задать начальный адрес OAM (регистр 0x2003), точнее сбросить его в 0. Затем CPU производит запись по адресу 0x4014 значения начального адреса (0x??00) в адресном пространстве CPU. Контроллер DMA производит останов CPU и начинает копирование 256 байт из области 0x??00 – 0x??FF (где ?? – значение, заданное CPU) в регистр PPU 0x2004. PPU увеличивает адрес OAM на единицу при каждой операции записи. По окончании процедуры контроллер DMA возвращает управление CPU.
Приоритет и Sprite 0 Hit
Конечное значение цвета пикселя формируется следующим образом:
![Слои изображения](https://habrastorage.org/storage2/da2/a1f/1e3/da2a1f1e3d5ce62c8eafa6737fadaf27.png)
Вполне возможна и часто случается ситуация, когда в одной области находятся более одного спрайта.
В этом случае на приоритет вывода на экран влияют два фактора – это значение адреса спрайта в OAM и флаг приоритета (0 – передний план, 1 – задний). Например, спрайт, у которого флаг приоритета указывает, что он относится к заднему плану, но при этом значение адреса в OAM меньше, чем у спрайта переднего плана, то спрайт заднего плана может закрыть спрайт переднего, что приведет к выводу пикселя фона, но только если он не прозрачный.
При отрисовке первого непрозрачного пикселя спрайта с адресом 0 (фон в этой точке тоже должен быть непрозрачный), в регистре 0x2002 взводится флаг, указывающий, что произошло событие Sprite 0 hit. В качестве примера применения этого флага можно привести игру Super Mario Bros. 1, в ней оно используется для разрыва экрана и отделения статичной информации об очках и времени от игровой области. В качестве фона выступает изображение монетки, а в качестве нулевого спрайта – ее тень. CPU периодически проверяет значение флага, и при возникновении события начинается вывод игрового поля.
![Пример Sprite 0 Hit](https://habrastorage.org/storage2/c35/809/0fc/c358090fcddb5feced18871df2ed8bf5.png)
Прерывание от мэппера MMC 3
Мэппер MMC3 имеет в составе счетчик строк, значение которого уменьшается при выводе очередной строки PPU. При достижении нуля счетчик перезагружается значением, которое может быть задано предварительной записью в соответствующий регистр мэппера и, если установлен флаг разрешения, возникает прерывание CPU. Достаточно оригинально организована линия тактирования счетчика – она подключена к адресной линии A12 графического процессора. Для хранения тайлов фона обычно используется младший банк (область 0x000 – 0x0FFF), а для спрайтов – старший банк (область 0x1000 – 0x1FFF). При отрисовке видимой строки PPU обращается к одному банку, а при выборке данных изображения спрайтов в HBLANK период – к другому. Поэтому частота на линии A12 будет соответствовать частоте вывода строк. Прерывания мэппера применяются в основном для разрыва экрана и переключения банка знакогенератора.
Примеры можете посмотреть на видео.
APU
Аудиопроцессор NES находится на одном кристалле с CPU. Функционально APU – это набор регистров управления, счетчик кадров и 5 блоков аудиоканалов.
С аппаратной точки зрения APU это куча счетчиков, подводных камней там нет, поэтому описание будет кратким.
Счетчик кадров формирует тактирующие импульсы частотой примерно 240 Гц, 120 Гц для блоков APU, а также прерывание IRQ для CPU. Генерацию прерывания можно отключить с помощью задания регистров APU. Не следует путать понятие «кадры», в данном случае никакого отношения к PPU оно не имеет.
Кстати, при разработке произошел неприятный случай, счетчик кадров APU был уже реализован и я напутал с флагом разрешения прерывания (он оказался инвертирован), поэтому прерывание генерировалось с частотой примерно 60 Гц. Внешне это проявилось очень неожиданно – в игре «Принц Персии» в левой части экрана не выводился проем двери с решеткой, причем это был единственный графический артефакт. Я голову сломал, пока нашел настоящую причину. Причем думал, естественно, на PPU – сто раз проверил код, смотря в ModelSim. Это было неприятно!
APU обходится пятью каналами:
Два прямоугольных канала, как видно из названия, они формируют сигнал прямоугольной формы, один треугольный канал, один канал шума и канал дельта-модуляции (DMC).
Прямоугольные каналы могут формировать сигнал с изменяемыми скважностью (4 градации) и периодом, с возможностью задания длительности, а также имеют блоки sweep и envelope. Блок sweep может изменять (последовательно увеличивать или уменьшать) период сигнала во времени, а блок огибающей (envelope) – уменьшать размах сигнала во времени с возможностью зацикливания, форма огибающей в этом случае – пилообразная.
Треугольный канал формирует сигнал треугольной формы с изменяемой частотой и длительностью, однако не имеет возможности регулировки громкости.
Канал шума генерирует псевдослучайный сигнал с изменяемой длительностью. Также, как и прямоугольный канал, канал шума имеет блок контроля огибающей (envelope).
Канал дельта-модуляции в проекте пока не реализован.
Микширование каналов производится табличным способом, чтобы не использовать «тяжелые» с точки зрения потребления LE операции.
После микширования сигнал поступает на блок синхронизации домена и фильтр, после чего через блок сериализатора по шине I2S уходит на аудиокодек WM8731.
![Вывод звука](https://habrastorage.org/storage2/bbb/2f5/ff4/bbb2f5ff41bcbfa7aa5aa9088a331249.png)
При реализации проекта я пользовался языком описания аппаратуры Verilog 2001.
Предвосхищая вопросы относительно исходного кода, могу сказать, что, так как это мой первый большой проект на ПЛИС, то наверняка я реализовал многие вещи очень неэффективным способом. К тому же это самая первая версия, код нужно оптимизировать и очищать, потому что многие блоки я переписывал несколько раз. Поэтому я бы не хотел предоставлять его в таком виде. Если я найду время, соберусь и приведу код в порядок, то, возможно он будет выложен под лицензией GNU GPL.
Спасибо за внимание!