- Игровая консоль своими руками v1.1
- Делаем игровую приставку. Как я собрал ретроконсоль в домашних условиях
- Содержание статьи
- Как все начиналось
- Собираем консоль
- Видеосигнал
- Xakep #247. Мобильная антислежка
- Подходящая графическая система
- Процессор
- Соединяем CPU и PPU
- Время для первой настоящей игры
- Добавляем кастомную графику
- И наконец, звук
- Конечный результат
- Архитектура
- Итоговые характеристики
- Разработка софта для консоли
- Маппинг памяти и ввода-вывода
- Управление PPU
- Кодим на ассемблере
- Используем инструменты C
- Кастомная графика
- Соберем все вместе
- Используем эмулятор для разработки
- Демонстрируем работу консоли
- Заключение
Игровая консоль своими руками v1.1
Портативная игровая система (портативная игровая консоль) — лёгкое, компактное, портативное электронное устройство, предназначенное для того, чтобы играть в видеоигры. От игровых приставок (игровых консолей) такие устройства отличаются компактностью и мобильностью; игровой контроллер, экран и звуковоспроизводящие элементы, как правило, являются здесь частью самого устройства.
Т.к. консоль самодельная, она поддерживает всего одну игру. Это электронная версия всем известной игры «крестики-нолики» Т.к. в основе консоли лежит микроконтроллер, в ней будет как PVP так и PVE режим (специально для тех, у кого нет друзей).
В этом блоге я подробно опишу процесс изготовления, чтобы вы смогли сами сделать такую же.
Итак, какие инструменты нам понадобятся:
1.
2.
3.
4. Микродрель
Её можно как
так и сделать самому
5.Программатор для PIC микрононтроллеров
Его тоже можно Но я сделал
Новичкам советую все-таки купить.
6.
7. Вот большой список деталей, необходимых для схемы:
Микрокотроллер PIC16F628A
Микросхема 74HC164N x 2шт
Резисторы (0.125 Вт):
4.7к х 2 шт
3к х 3 шт
100 ом х 15 шт
10к х 1 шт
Конденсаторы:
100 мк 16 в х 1 шт
0.1 мк х 1 шт
Светодиоды:
L-59EGW или другие двухцветные с общим минусом х 9 шт
Транзистор BC337-40 (КТ3102)
Кнопки 4х ногие х2 шт
Панелька под мк DIP18
Текстолит Теперь ознакомимся со схемой:
Как вы видите схема сложная, поэтому для ее создания нужен опыт. Тем, кто первый раз держит паяльник в руках, я советую потренироваться на чем-нибудь попроще.
Для схемы понадобится печатная плата. Я подготовил ее в программе Sprint Layout.
Вам ее нужно только распечатать на лазерном принтере на фотобумаге. Причем чем старше принтер, тем лучше. Фотографии этой печатки у меня не осталось, но получится примерно так:
Потом травим плату в растворе хлорного железа до полного растворения открытой меди.
Теперь нужно перенести это изображение на текстолит. Для этого зачищаем его нулевой наждачной бумагой, обезжириваем ацетоном (пойдет жидкость для снятия лака). Потом прикладываем нашу распечатку тонером вниз к текстолиту и 2-3 минуты проглаживаем утюгом. За это время тонер расплавится и прилипнет к текстолиту. Потом несем все это к раковине и аккуратно под струей воды отдираем фотобумагу. Рисунок должен остаться на текстолите. Получится вот так:
Теперь смываем тонер тем же ацетоном и останется только то, что нам нужно.Теперь самая трудоемкая работа. Высверлить все отверстия под детали. Вот что выйдет:
Затем переворачиваем плату и размечаем отверстия в соответствии с чертежом в Sprint layout.
Потом включаем паяльник и лудим дорожки.
Дальше припаиваем все детали в соответствии с разметкой. Микроконтроллер не впаиваем напрямую, а используем панельку т.к. нам с ним надо еще кое-что сделать. Вот что получится:
Почти готово. Осталось только залить прошивку на контроллер, используя программатор. Т.к. они могут быть разные, то и программы там могут использоваться разные. Поэтому подробнее о том, как прошить мк читайте в инструкции программатора. Теперь вставляем прошитый микроконтроллер в панельку, подключаем батарейки и пробуем. Если какие-то светодиоды загорелись, значит все правильно.
Вообще-то уже можно играть, но без корпуса как-то не торт. Вам бы понравилось играть в такую PS Vit’у? Думаю, что нет. Поэтому надо сделать корпус. Для начала я сделал его из бумаги. Так удобнее что-то редактировать.
Но бумага слишком хрупкая, нужно что-нибудь более прочное. Пришлось пожертвовать коробкой из-под какого-то диска.
По бумажным заготовкам вырезаем то же самое из этого материала.
В верхней стенке вырезаем отверстия под светодиоды и кнопки.
Начинаем собирать корпус. Сперва склеиваем стенки изолентой.
Затем склеиваем стенки с помощью термоклея.
Поскольку кнопки не достают до верхней стенки корпуса, нужно их удлинить. У себя в куче хлама я нашел 2 такие штуки.
Для красоты их можно перекрасить в черный цвет. Снаружи для удобства можно приклеить куски того же материала от диска.
В нижнюю стенку встраиваем контейнер для батареек. Для этого вырезаем в ней прямоугольник, соответствующий размерам контейнера. С помощью термоклея крепим контейнер изнутри. Вырезанный кусок будет его крышкой. Чтобы она открывалась и закрывалась, один конец крышки приклеиваем с обратной стороны изолентой, а к другому приделываем «хвост».
Просверливаем отверстие, вкручиваем шуруп. Теперь мы можем в любой момент открыть батарейный отсек и заменить бабарейки.
Затем в нижнюю стенку вкручиваем по углам 2 шурупа. На них она будет прикручиваться. В остальном корпусе по углам, в которых будут шурупы оставляем по большой капле термоклея. Пока он еще горячий, вставляем нижнюю стенку с шурупами и держим, пока он не остынет. Теперь ее тоже можно откручивать по надобности.
Ну все, теперь собираем все вместе и пробуем.
Теперь все готово. Самое время рассказать об управлении
После включения микроконтроллер переходит в режим игры в котором противником игрока является “электронный интеллект” микроконтроллера. При этом на игровом поле высвечивается символ “+” (плюс) зеленого цвета. Игрок может играть только “крестиками”, которым присвоен красный цвет свечения светодиодов. Микроконтроллер играет “ноликами”, которым соответственно присвоен зеленый цвет свечения светодиодов. При выборе данного режима игры первой ход делает игрок, в следующей партии первый ход за микроконтроллером, затем опять за игроком, таким образом право первого хода передается по очереди. Ход крестиками осуществляется с помощью кнопки SB1, после нажатия которой начинает мигать красным цветом светодиод HL1 с частотой 1Гц, тем самым указывая клетку игрового поля на которую можно сделать ход. При повторном нажатии на кнопку SB1, светодиод HL1 гаснет, и начинает мигать светодиод HL2. При последующих нажатиях поочередно мигают остальные светодиоды поля, после светодиода HL9 снова начинает мигать светодиод HL1. Для того чтобы сделать ход на выбранную клетку необходимо удерживать нажатой кнопку SB1 в течении 1 секунды, после чего светодиод перестанет мигать и будет постоянно гореть красным цветом. После того как микроконтроллер сделает ход, игрок выбирает необходимую клетку как было описано выше и делает ход. Во время выбора клетки игроком, если она занята, то она пропускается, вместо нее мигает следующая свободная клетка.
В случае победы игрока, через секунду после того как сделан последний ход, на игровом поле высвечивется символ “Х” (крестик) красного цвета. Если побеждает микроконтроллер то высвечивается символ “О” (нолик) зеленого цвета. Ничья отображается в виде символа “Н” (Ничья), оранжевого цвета, то есть в каждом светодиоде зажигаются оба кристалла, красный и зеленый. Для начала новой игры необходимо нажать кнопку SB1.
Для переключения режима игры необходимо одновременно удерживать нажатыми кнопки SB1 и SB2 в течении 1 секунды. Переключение возможно после подачи питания, а также после завершения каждой партии в обоих режимах. После перехода в режим игры для двух игроков, на игровом поле высвечивается символ “+” (плюс) красного цвета. Для началы игры необходимо нажать кнопку SB1 или SB2, соответственно первым ходит тот игрок, кто раньше нажмет кнопку. То же правило справедливо для начала любой следующей партии. Здесь также кнопкой SB1 осуществляется ход крестиками, а кнопкой SB2 ход ноликами. Выбор клетки для крестиков описан выше, для ноликов справедливы те же действия, с одной разницей, после нажатия кнопки SB2 начинает мигать светодиод HL9, при следующем нажатии HL8, то есть светодиоды переключаются в обратном направлении.
Если в течении 4 минут не были нажаты кнопки SB1 или SB2, то устройство переходит в режим пониженного энергопотребления, микроконтроллер отключает все светодиоды и переходит в спящий режим. Устройство “просыпается” после нажатия кнопки SB1, и возвращается в прежнее состояние.
Видео с демонстрацией работы я снимал еще до того, как сделал корпус, но там и так все понятно.
Источник
Делаем игровую приставку. Как я собрал ретроконсоль в домашних условиях
Содержание статьи
Это перевод статьи Сержиу Виейры, впервые опубликованной в его блоге. Перевела Алёна Георгиева.
Как все начиналось
Меня зовут Сержиу Виейра, и я португальский парень, выросший в 80–90-е. Я всегда ностальгировал по ретроконсолям, особенно третьего и четвертого поколений. Несколько лет назад я решил глубже изучить электронику и попробовать собрать свою собственную видеоконсоль. Работаю я инженером софта, и ранее никакого опыта работы с электроникой у меня не было — если не считать сборки и апгрейда моего десктопа (что, конечно, не считается). Но, несмотря на отсутствие опыта, я сказал себе: «Почему нет?», купил несколько книг и комплектов электроники — и начал учиться.
Я хотел собрать консоль, похожую на те, что всегда мне нравились, — что-то среднее между NES и Super Nintendo или между Sega Master System и Mega Drive. Все эти консоли обладали CPU, кастомным видеочипом (тогда он еще не назывался GPU) и аудиочипом — интегрированным или выделенным. Игры к ним распространялись на картриджах, которые обычно представляли собой аппаратные расширения с чипом ПЗУ и иногда другими компонентами.
Моим изначальным планом было собрать консоль со следующими характеристиками.
- Никакой эмуляции, все игры/программы должны запускаться на реальном железе — не обязательно железе «из тех времен», просто достаточно шустром для своих задач.
- Настоящий ностальгический «ретрочип» процессора.
- Вывод на телевизор (аналоговый сигнал).
- Способность производить звук.
- Поддержка двух контроллеров.
- Прокрутка фона и движущиеся спрайты.
- Поддержка платформеров в стиле «Марио» (и, конечно, других типов игр).
- Возможность запускать игры и программы с карты SD.
Я решил использовать поддержку SD-карт вместо картриджей, потому что запускать игры с карты намного практичней — к тому же на нее проще копировать файлы с компьютера. Используй я картриджи, пришлось бы накрутить куда больше железа и завести отдельное железо для каждой программы.
Собираем консоль
Видеосигнал
Начал свою работу я с генерации видеосигнала. У всех консолей эпохи, на которую я ориентировался, были свои проприетарные графические чипы — что давало им очень разные характеристики. По этой причине я не стал использовать готовый графический чип — хотелось, чтобы у моей консоли были уникальные возможности графики. Но, поскольку собрать собственный чип я бы не потянул, а ПЛИС использовать не умел, я выбрал базирующийся на софте графический чип с двадцатимегагерцевым восьмибитным микроконтроллером. Это не перебор: у него ровно такая производительность, чтобы генерировать нужный мне тип графики.
Итак, я начал с микроконтроллера ATmega644, работающего на частоте 20 МГц, который посылал PAL-сигнал на телевизор. Поскольку сам по себе микроконтроллер этот формат не поддерживает, пришлось добавить внешний ЦАП.
Xakep #247. Мобильная антислежка
Наш микроконтроллер выдает восьмибитную цветность (RGB332: три бита на красный, три бита на зеленый и два — на синий), а пассивный ЦАП конвертирует всю эту красоту в аналоговый RGB. По счастью, в Португалии внешние устройства к телевизору чаще всего подключают через разъем SCART — и большая часть телевизоров принимает RGB-сигнал через него же.
Подходящая графическая система
Поскольку первый микроконтроллер я хотел использовать исключительно для передачи сигнала на телевизор (я назвал его VPU — Video Processing Unit), для графики в целом я решил использовать способ двойной буферизации.
Я взял второй микроконтроллер для PPU (Picture Processing Unit) — ATmega1284 тоже на 20 МГц. Он должен генерировать изображение на чип ОЗУ (VRAM1), после чего первый микроконтроллер передаст содержимое другого чипа оперативки (VRAM2) на телевизор. После каждого кадра (два кадра в PAL или 1/25 секунды) VPU переключает чипы ОЗУ и передает изображение с VRAM1 на телевизор, пока PPU генерирует новое на VRAM2.
Видеоплата получилась довольно сложной: мне пришлось использовать дополнительное железо, чтобы дать микроконтроллерам доступ к обоим чипам ОЗУ, а также ускорить доступ к памяти, которая к тому же используется для вывода видеосигнала методом битбэнга. Для этого я добавил в цепочку несколько чипов 74-й серии в качестве счетчиков, линейных селекторов, трансиверов и прочего.
Прошивка для VPU и особенно для PPU тоже вышла довольно сложной, поскольку мне нужно было написать чрезвычайно производительный код — если я хотел получить все искомые графические возможности. Изначально я все писал на ассемблере, позже кое-что кодил на C.
В итоге мой PPU генерировал изображение 224 × 192 пикселя, которое VPU транслировал на экран телевизора. Разрешение может показаться слишком низким, но на самом деле оно немногим меньше, чем у консолей-прототипов — они обычно имели разрешение 256 × 224. Зато более низкое разрешение позволило мне втиснуть больше графических фич в тот временной отрезок, что уходил на отрисовку каждого кадра.
Прямо как в старые добрые времена, у моего PPU есть «фиксированные» графические возможности, которые можно настроить. Фон рендерится из символов размером 8 × 8 пикселей (их еще иногда называют тайлами). Это значит, что размер всего фона — 28 × 24 тайла. Для попиксельной прокрутки и возможности плавного обновления фона я сделал четыре виртуальных экрана 28 × 24 тайла — все они смежные и «обтекают» друг друга.
Поверх фона PPU рендерит до 64 спрайтов шириной и высотой от 8 до 16 пикселей (то есть один, два или четыре символа), которые можно повернуть по вертикали, горизонтали или по обеим осям. Еще над фоном можно отрендерить оверлей — такую плашку размером 28 × 6 тайлов. Она пригодится для игр, где нужны элементы интерфейса поверх основного экрана (HUD), фон скроллится, а спрайты используются не только для подачи информации, но и для других целей.
Другая «продвинутая» фича — возможность прокручивать фон в разных направлениях по отдельным строкам, что позволяет добавить эффекты типа ограниченного параллакс-скроллинга или разделенного экрана.
Еще есть таблица атрибуции, которая позволяет задать каждому тайлу значение от 0 до 3. А дальше можно, например, назначить все тайлы с определенным значением на конкретную тайловую страницу или увеличить номер их символа. Это полезно, когда конкретные элементы фона постоянно меняются, — в таком случае CPU не нужно обновлять каждый тайл в отдельности, он может просто передать команду вроде «все тайлы со значением 1 увеличивают свое значение на 2». Разными способами этот подход используется, например, в играх с Марио, где на фоне двигаются знаки вопроса, или в других играх с постоянно льющимися водопадами.
Процессор
Когда была готова функциональная видеоплата, я приступил к работе над CPU — для своей консоли я выбрал Zilog Z80. Помимо того что Z80 — просто крутой ретропроцессор, у него есть отдельно 16 бит на память и 16 бит для I/O, чем другие подобные восьмибитные процессоры, например знаменитый 6502, похвастаться не могут. У того же 6502 есть только 16 бит памяти, а значит, эти 16 бит придется делить между собственно памятью и дополнительными устройствами: аудио, видео, ввода и прочими. Если же у нас есть отдельный участок для I/O, то он возьмет на себя все внешние устройства, а 16 бит памяти (то есть 64 Кбайт кода или данных) мы сможем использовать по прямому назначению.
Для начала я соединил свой CPU с EEPROM, добросив немного тестового кода. Еще я прикрутил к CPU через участок I/O микроконтроллер, который связывается с ПК по RS-232, — чтобы проверить, нормально ли работает мой процессор и все остальные соединения. Этот микроконтроллер (двадцатимегагерцевый ATmega324) должен был стать IO MCU (микроконтроллером ввода-вывода) и отвечать за доступ к игровым контроллерам, карте SD, клавиатуре PS/2 и коммуникацию с компом через RS-232.
Потом я прикрутил к процессору чип ОЗУ на 128 Кбайт, из которых были доступны 56 (это может показаться пустой тратой ресурса, но у меня были чипы ОЗУ только по 128 и по 32 Кбайт). Таким образом, вся память процессора состоит из 8 Кбайт ПЗУ и 56 Кбайт ОЗУ.
Следом я обновил прошивку своего микроконтроллера ввода-вывода с помощью этой библиотеки и добавил ему поддержку карт SD. Теперь CPU научился перемещаться по директориям SD-карты, просматривать их содержимое, открывать и читать файлы — считывая и записывая данные на конкретные адреса участка ввода-вывода.
Соединяем CPU и PPU
Пришло время реализовать взаимодействие между CPU и PPU. Для этого я нашел «простое решение» — чип ОЗУ с двойным портом (то есть тот, который можно одновременно подключить по двум разным шинам). Он спас меня от накручивания новых микросхем типа линейных селекторов — и к тому же сделал доступ к оперативной памяти для обоих чипов практически одновременным. Также PPU связывается с CPU напрямую каждый кадр, активируя его немаскируемое прерывание (NMI). Это значит, что каждый кадр процессор прерывается (ценное умение для синхронизации и своевременного обновления графики).
Каждый кадр взаимодействие между CPU, PPU и VPU развивается по следующему сценарию.
- PPU копирует информацию с внешнего ОЗУ (на рисунке ниже обозначено как PPU RAM) на встроенное ОЗУ.
- PPU посылает CPU сигнал немаскируемого прерывания.
- Одновременно с этим:
- CPU немедленно обращается к функции немаскируемого прерывания и обновляет в PPU RAM информацию о состоянии графики в следующем кадре (программа должна выйти из прерывания до его начала);
- PPU рендерит изображение, основываясь на информации, которую перед этим скопировал в один из двух ОЗУ графической системы (VRAM1 или VRAM2);
- VPU посылает изображение с другого VRAM на телевизор.
Примерно в то же время я добавил поддержку игровых контроллеров. Изначально я хотел использовать контроллеры Super Nintendo, но их разъем проприетарный — и достать его непросто. Поэтому я выбрал совместимые шестикнопочные контроллеры Mega Drive/Genesis: они используют стандартные, распространенные и доступные разъемы DB-9.
Время для первой настоящей игры
У меня был процессор с поддержкой игровых контроллеров, который мог управлять PPU и загружать программы с SD-карты, так что… пришло время сделать игру. Я написал ее, конечно, на языке ассемблера Z80 — это заняло у меня пару дней (исходный код игры).
Добавляем кастомную графику
Все отлично, у меня есть рабочая консоль, но… этого недостаточно. Игры пока не могут использовать кастомную графику — только ту, что хранится в прошивке PPU. А единственный способ поменять встроенную графику — обновить прошивку. Поэтому я решил добавить отдельный чип ОЗУ с графикой (символьное ОЗУ, Character RAM) — он должен быть доступен PPU и загружать графику согласно инструкциям, пришедшим из CPU. При этом нужно было использовать как можно меньше новых компонентов, потому что консоль уже получилась довольно большой и сложной.
Я нашел следующий выход: доступ к новому ОЗУ будет только у PPU, а CPU станет передавать ему информацию через PPU. И пока эти данные передаются, наше новое ОЗУ не будет использоваться для графики — его функции временно возьмет на себя встроенная графика.
После передачи данных процессор переключится из режима встроенной графики в режим работы с символьным ОЗУ (CHR RAM на схеме ниже), и PPU сможет использовать кастомную графику. Возможно, это не идеальное решение, но оно работает. В итоге новое ОЗУ имело объем 128 Кбайт и могло хранить 1024 символа размером 8 × 8 пикселей для фона и 1024 символа того же размера для спрайтов.
И наконец, звук
Реализацию звука я оставил на финал. Изначально я собирался дать своей консоли те же звуковые возможности, что у Uzebox, и встроить микроконтроллер, который генерировал бы четыре канала PWM-звука. Однако я выяснил, что можно относительно легко достать винтажные чипы, — и заказал несколько чипов YM3438, работающих на принципе частотно-модуляционного синтеза. Они полностью совместимы с YM2612, которые установлены в Mega Drive/Genesis. Установив этот чип, я получаю музыку качества Mega Drive и звуковые эффекты, которые производит контроллер. CPU управляет звуковым модулем (я назвал его SPU, Sound Processor Unit, — он отдает команды YM3438 и сам производит звуки) снова через маленькое ОЗУ с двойным портом, на сей раз емкостью всего в 2 Кбайт.
Так же как у графического, у звукового модуля есть 128 Кбайт на хранение звуковых патчей и семплов PCM. Процессор же выгружает информацию в эту память через SPU. Таким образом, процессор может как велеть SPU воспроизводить команды из этого ОЗУ, так и обновлять команды для SPU каждый кадр.
CPU управляет четырьмя PWM-каналами через четыре кольцевых буфера, которые есть в специальном ОЗУ (SPU RAM на схеме ниже). SPU проходит через эти буферы и выполняет имеющиеся в них команды. Таким же образом работает еще один кольцевой буфер в SPU RAM — он обслуживает чип частотно-модуляционного синтеза (YM3438).
Взаимодействие между процессором и звуковым модулем похоже на историю с графикой — и устроено по следующей схеме.
- SPU копирует информацию из SPU RAM во встроенную оперативку.
- SPU ждет сигнала NMI от PPU (для синхронизации).
- Одновременно с этим:
- процессор обновляет буферы PWM-каналов и чипа частотно-модуляционного синтеза;
- SPU выполняет команды в буферах согласно информации, сохраненной во встроенной памяти.
- Пока все это происходит, SPU непрерывно обновляет PWM-звук с частотой 16 кГц.
Конечный результат
После разработки всех модулей я поместил некоторые из них на макетные платы. Для модуля CPU я сумел придумать и заказать кастомную плату. Не знаю, буду ли делать то же самое для других модулей, — полагаю, мне довольно сильно повезло получить рабочую кастомную плату с первой попытки. Только звуковой модуль пока что остается в виде макета.
Вот как выглядит консоль на момент написания этого текста.
Архитектура
Схема ниже иллюстрирует, какие компоненты входят в каждый модуль и как они взаимодействуют друг с другом. Единственное, что не показано, — это сигнал в форме NMI, который PPU передает непосредственно процессору каждый кадр, а также аналогичный сигнал, передаваемый SPU.
- CPU: Zilog Z80, работающий на частоте 10 МГц.
- CPU-ROM: EEPROM на 8 Кбайт, содержит код загрузчика.
- CPU-RAM: 128 Кбайт оперативной памяти (из них используются 56 Кбайт), содержит код и данные для программ/игр.
- IO MCU: ATmega324, служит интерфейсом между CPU и RS-232, клавиатурой PS/2, игровыми контроллерами и файловой системой SD-карты.
- PPU-RAM: двухпортовое ОЗУ на 4 Кбайт, это интерфейсное ОЗУ между CPU и PPU.
- CHRRAM: 128 Кбайт оперативки, содержит кастомные тайлы фона и спрайтовую графику (8 × 8 пикселей каждый символ).
- VRAM1, VRAM2: 128 Кбайт оперативки (используются 43 008 байт), служат для хранения кадрового буфера; информацию в них записывает PPU, а считывает — VPU.
- PPU (Picture Processing Unit): ATmega1284, отрисовывает кадр и отправляет его в кадровый буфер.
- VPU (Video Processing Unit): ATmega324, считывает кадровый буфер и генерирует RGB и PAL-сигнал.
- SPU-RAM: двухпортовое ОЗУ на 2 Кбайт, служит интерфейсом между CPU и SPU.
- SNDRAM: 128 Кбайт оперативки, содержит патчи PWM, семплы PCM и блоки инструкций для частотно-модуляционного синтеза.
- YM3438: одноименный чип частотно-модуляционного синтеза.
- SPU (Sound Processing Unit): ATmega644, генерирует звук на базе PWM и управляет YM3438.
Итоговые характеристики
Процессор:
- восьмибитный CPU Zilog Z80 с частотой 10 МГц;
- 8 Кбайт постоянной памяти для загрузчика;
- 56 Кбайт оперативной памяти.
Ввод/вывод (I/O):
- чтение данных с SD-карт файловых систем FAT16/FAT32;
- чтение и запись на порт RS-232;
- два игровых контроллера, совместимых с Mega Drive/Genesis;
- клавиатура PS/2.
Видео:
- разрешение 224 × 192 пикселя;
- 25 кадров в секунду;
- 256 цветов (схема RGB332);
- виртуальное фоновое пространство 2 × 2 (448 × 384 пикселя) с двунаправленной попиксельной прокруткой, которое описывают четыре именные таблицы;
- 64 спрайта высотой и шириной 8 или 16 пикселей с возможностью развернуть их как по вертикали, так и по горизонтали;
- фон и спрайты, состоящие из символов 8 × 8 пикселей каждый;
- символьное ОЗУ с 1024 фоновыми и 1024 спрайтовыми символами;
- независимая горизонтальная прокрутка фона по кастомным строкам на 64;
- независимая вертикальная прокрутка фона по кастомным строкам на 8;
- наложение плашки размером 224 × 48 пикселей с прозрачностью или без нее;
- таблица атрибуции для фона;
- RGB и композитный PAL-вывод через разъем SCART.
Звук:
- генерируемый с помощью PWM восьмибитный четырехканальный звук с заранее заданными формами волны (меандр, синусоида, пилообразная, шумовая и так далее);
- восьмибитные и восьмикилогерцевые семплы PCM на одном из каналов PWM;
- чип частотно-модуляционного синтеза YM3438 с обновляемыми инструкциями на частоте 50 Гц.
Разработка софта для консоли
Первый кусок софта, написанный для консоли, — это загрузчик. Он хранится в постоянной памяти процессора и занимает до 8 Кбайт. Он же использует первые 256 байт оперативки процессора. Загрузчик — первый софт, запускаемый на процессоре. Его цель — показать программы, доступные на SD-карте. Эти программы хранятся в файлах, которые содержат скомпилированный код и могут также содержать данные кастомной графики и звука.
После выбора программы она загружается в оперативку процессора, символьное ОЗУ и ОЗУ звукового модуля. Там соответствующая программа выполняется. Код программ, загружаемых на консоль, может занимать до 56 Кбайт памяти — за исключением первых 256 байт; также, конечно, нужно учитывать объем стека и оставлять место для данных.
И загрузчик, и программы для этой консоли разрабатываются похожим образом. Коротко поясню, как я их сделал.
Маппинг памяти и ввода-вывода
При разработке для консоли следует обратить особое внимание на то, как CPU может получить доступ к другим модулям, поэтому представление памяти и ввода-вывода имеет решающее значение.
Процессор обращается к своему загрузчику на ПЗУ и ОЗУ через память. Представление памяти выглядит так.
К PPU-RAM и SPU-RAM, а также к IO MCU он обращается через участок ввода-вывода. Представление участка ввода-вывода процессора будет таким.
Внутри представления участка ввода-вывода IO MCU, PPU и SPU имеют свои конкретные адреса.
Управление PPU
Мы можем управлять PPU с помощью записи на PPU-RAM, а доступ к PPU-RAM, как мы знаем из таблицы выше, организован через участок ввода-вывода с адресов от 1000h до 1FFFh .
Вот как выглядит этот диапазон адресов, если представить его более подробно.
Состояние PPU (PPU Status) может принимать следующие значения:
0 — режим встроенной графики;
1 — режим кастомной графики;
2 — режим записи в символьное ОЗУ;
4 — запись закончена, ожидание подтверждения от CPU.
А вот пример того, как можно работать со спрайтами. Консоль может рендерить до 64 спрайтов одновременно. Информация об этих спрайтах передается через адреса с 1004h по 1143h (320 байт), по 5 байт информации на каждый спрайт (5 × 64 = 320 байт):
- Смешанный байт (каждый его бит — это флаг: Active , Flipped_X , Flipped_Y , PageBit0 , PageBit1 , AboveOverlay , Width16 и Height16 ).
- Символьный байт (какой символ является спрайтом на странице, описанной соответствующими флагами смешанного байта).
- Байт хромакея (описывает, какой цвет будет прозрачным).
- Байт позиции по горизонтали (оси X).
- Байт позиции по вертикали (оси Y).
Итак, чтобы сделать спрайт видимым, мы должны присвоить флагу Active значение 1, а также установить координаты, при которых он станет видимым (координаты x = 32 и y = 32 разместят спрайт в левый верхний угол экрана; если значения x и y будут меньше, то спрайт окажется за пределами экрана — частично или полностью). Затем мы можем присвоить ему символ и определить, какой цвет спрайта будет прозрачным.
Например, если мы хотим сделать видимым десятый спрайт, мы должны присвоить адресу ввода-вывода 4145 ( 1004h + (5 x 9) ) значение 1. Затем устанавливаем координаты спрайта — скажем, x = 100 и y = 120 , — присвоив адресу 4148 значение 100, а адресу 4149 — значение 120.
Кодим на ассемблере
Один из способов написать программу для нашей консоли — использовать язык ассемблера.
Ниже — пример кода, который заставляет первый спрайт двигаться и сталкиваться с углами экрана:
Используем инструменты C
Еще можно писать программы для консоли на C, используя компилятор SDCC или другие кастомные инструменты. Разработка так идет быстрее, хотя производительность кода, конечно, падает.
В качестве примера покажу код на С, который выполняет ту же самую задачу, что и приведенный выше ассемблерный. Чтобы облегчить обращение к PPU, я здесь использовал библиотеку.
Кастомная графика
У консоли есть встроенная и предназначенная только для чтения графика, которая хранится в прошивке PPU (одна страница тайлов для фона и одна страница графики для спрайтов). Однако для программ можно использовать и кастомную графику.
Цель в том, чтобы перевести всю необходимую графику в двоичную форму — в таком виде загрузчик консоли сможет грузить ее в символьное ОЗУ. Чтобы этого добиться, я начал с нескольких изображений уже нужного размера — в данном случае они предназначены для фона сразу в нескольких игровых ситуациях.
Поскольку кастомная графика состоит из четырех страниц по 256 символов размером 8 × 8 пикселей для фона и четырех таких же страниц для спрайтов, я преобразовал графику с картинки выше в файл PNG для каждой страницы, используя специальный инструмент (за исключением повторяющихся результирующих символов).
А следом я использовал еще один инструмент, чтобы сконвертировать результат в бинарник с символами 8 × 8 пикселей в цветовой схеме RGB332.
В результате получаются двоичные файлы, состоящие из символов 8 × 8 пикселей (символы в памяти являются смежными, каждый занимает 64 байта).
Образцы звуковых волн конвертируем в восьмибитные и восьмикилогерцевые семплы PCM. Патчи звуковых эффектов и музыки PWM можно составить, используя заранее определенные инструкции. Что касается ямаховского чипа частотно-модуляционного синтеза YM3438, то для него я нашел приложение DefleMask. С помощью DefleMask делают синхронизированную с PAL музыку для звукового чипа YM2612 от Genesis, который совместим с нашим YM3438.
DefleMask конвертирует музыку в формат VGM, а дальше я уже использую другой специальный инструмент, чтобы превратить VGM в самопальный звуковой бинарник.
Бинарники со всеми тремя видами звуков объединяются в один двоичный файл, который загрузчик потом сможет загрузить в ОЗУ звукового модуля (SNDRAM).
Соберем все вместе
Программный бинарник, графику и звук нужно соединить в один файл формата PRG. В файле PRG есть заголовок, который сообщает, использует ли программа кастомную графику и/или звук и каков размер каждого из этих компонентов. Там же содержится вся прочая соответствующая двоичная информация.
Затем получившийся файл можно разместить на карту SD, загрузчик консоли оттуда его прочитает, пошлет всю необходимую информацию соответствующим ОЗУ и запустит программу.
Используем эмулятор для разработки
Чтобы облегчить разработку софта для консоли, я написал на C++ эмулятор, используя wxWidgets. Чтобы эмулировать процессор, я обратился к библиотеке libz80.
Я добавил в эмулятор несколько отладочных функций. В частности, я могу оказаться в конкретной точке останова и пройти из нее по всем ассемблерным инструкциям. Также есть связь с исходным кодом, если программа была скомпилирована на C. Что касается графики, то тут я могу проверить, что хранится на страницах тайлов и в именных таблицах (представление фона размером в четыре экрана), а также что находится в символьном ОЗУ (CHRRAM).
Вот пример того, как запускать программу на эмуляторе и использовать некоторые отладочные инструменты.
Демонстрируем работу консоли
Видео из этого раздела — это съемка экрана электронно-лучевого телевизора на камеру телефона. Прошу прощения, что качество не очень высокое.
Запускаем с помощью бейсика и клавиатуры PS/2. На этом видео я — сразу после создания первой программы — записываю напрямую в ОЗУ графического модуля (PPU-RAM) через участок ввода-вывода команды включить и настроить спрайт, а в конце переместить его.
Демонстрация возможностей графики. На этом видео показана программа, которая отображает 64 спрайта размером 16 × 16 пикселей, кастомную прокрутку фона, а также наложенную плашку, которая двигается вверх и вниз — как перед спрайтами, так и за ними.
Демонстрация возможностей звука показывает, на что способен YM3438 в сочетании с проигрыванием семплов PCM. Частотно-модуляционная музыка вместе с семплами PCM на этом демо почти полностью занимают 128 Кбайт ОЗУ звукового модуля.
Тетрис, использующий почти исключительно фоновые тайлы для графики, YM3438 для музыки и патчи PWM для звуковых эффектов.
Заключение
Этот проект стал настоящей воплощенной мечтой, я работаю над ним уже несколько лет в свободное и не очень время. Никогда не думал, что зайду так далеко, пытаясь собрать собственную игровую консоль в ретростиле. Конечно, она не идеальна — я по-прежнему вовсе не эксперт в электронике. В консоли слишком много компонентов, и ее, несомненно, можно было бы сделать лучше и эффективней — наверняка кто-нибудь из читающих этот текст думает именно так. Тем не менее, пока я занимался этим проектом, я узнал много нового об электронике, игровых консолях и компьютерном дизайне, языке ассемблера и других интересных темах. Ну и сверх того я получил огромное удовлетворение, играя в игру, которую сделал сам, на железе, которое я тоже сдизайнил и сделал сам.
Я планирую собирать и другие консоли и компьютеры. На самом деле я уже почти закончил еще одну игровую приставку. Это упрощенная консоль в ретростиле, в основе которой лежит дешевая плата ПЛИС и несколько других компонентов (но их, очевидно, не так много, как в первом проекте). Она изначально задумана как дешевая и тиражируемая.
Сайт и каналы, которые не только вдохновили меня, но и помогли разрешить трудности, с которыми я столкнулся со время работы над проектом.
Источник