Как написать эмулятор nes

Вы наверняка слышали про эмуляторы игровых приставок и, возможно, даже играли в них не один час. Но задавались ли вы вопросом, как работает эмулятор игровой консоли? На примере игровой приставки NES, известной в наших краях как Dendy, я покажу, как написать собственный эмулятор. Мы разберемся с хитрой архитектурой данной консоли, выдававшей великолепную графику для своего времени и своей небольшой цены.

Все игровые приставки различными средствами адаптированы для запуска игр, что их и отличает от обычных ПК. Особенно это касается древних консолей вроде NES: в восьмидесятые годы с аппаратными ресурсами было дела плохи, особенно если нужно было сделать недорогой домашний агрегат. Экономить надо было буквально на всем, чем и объясняются некоторые инженерные решения.

Содержание

  1. Виды эмуляции
  2. MOS 6502: регистры, режимы адресации и инструкции
  3. Мапперы
  4. Picture Processing Unit
  5. Отрисовка фона
  6. Таблица имен
  7. Палитры
  8. Таблица атрибутов
  9. Отрисовка спрайтов
  10. Синхронизация между CPU и PPU
  11. NMI
  12. Пример кода PPU для отрисовки
  13. Выводы

Виды эмуляции

  • Интерпретация. Интерпретатор дает возможность программно эмулировать все части консоли. Данный способ самый простой и вместе с тем наименее производительный.
  • Динамическая рекомпиляция. Рекомпилятор довольно сложен в написании, но при этом значительно превосходит интерпретатор в производительности. Данный эмулятор может рекомпилировать инструкции эмулируемой машины в машинные инструкции вашего ПК. Проще говоря, рекомпилятор выступает в роли переводчика с одного машинного языка на другой.

Существуют эмуляторы для всех старых и даже некоторых новых приставок. Вот несколько примеров: Dolphin — эмулятор Wii и GameCube, ePSXe — PS1, PCSX2 — PS2, PPSSPP — PSP.

Среди пока что незаконченных, но быстро развивающихся эмуляторов: Cemu — эмулятор Wii U, RPCS3 — PS3, Yuzu — Switch, Xenia — эмулятор Xbox 360.

MOS 6502: регистры, режимы адресации и инструкции

NES неспроста снискала мировую популярность. Разработчики оборудования пытались создать наиболее производительные решения, применяя разного рода уловки. Разработчики игр досконально изучали особенности платформы и находили новые трюки, чтобы создать красивую картинку. Порой даже очень красивую картинку для своего времени.

Как и в случае с компьютером, основная логика программ выполняется на центральном процессоре приставки. Поэтому лучше всего начинать написание эмулятора именно с него. В NES установлен восьмибитный процессор MOS 6502 с комплексным набором инструкций (то есть как у Intel, а не как у ARM или PowerPC).

У процессора MOS 6502 шесть регистров, один из которых недоступен пользователю:

  • A — регистр, куда складываются результаты всех арифметических операций;
  • X, Y — индексные регистры;
  • SP — указатель на вершину стека;
  • P — регистр флагов, в x86 EFLAGS выполняет ту же функцию;
  • PC — счетчик команд, регистр, который указывает, какую команду выполнять следующей. Этот регистр недоступен напрямую.

Режимов адресации великое множество, и узнать о них будет полезно и за рамками этой статьи.

Название Определение Пример
Аккумуляторный Операндом инструкции является аккумулятор Арифметический сдвиг влево
ASL
Предполагаемый Операнд явно указывается инструкцией Перенос значения A в X
TAX
Немедленный Операнд дается в инструкции Загрузка значения в A
LDA #$34
По абсолютному адресу Операндом является значение по абсолютному адресу Загрузка значения в X
LDX $9010
По адресу в нулевой странице По абсолютному адресу первых 256 байт Загрузка значения в Y
LDY $23
Относительный Адрес задается относительно PC Ветвление, если предыдущий операнд равен 0
BEQ $4A

Нулевая страница — первые 256 байт памяти, в которых размещаются наиболее часто используемые переменные ради более быстрого доступа к ним.

Всего у нашего процессора 256 инструкций, из которых 78 — это настоящие инструкции без учета разных режимов адресации. Сразу сократим работу: мы не будем писать обработчик для всех 256 инструкций. Напишем обработчики для всех режимов адресации и для 78 инструкций.

Начнем с кода для режима адресации нулевой страницы.

void

addr_mode_zp()

{

  cpu_addr = ram_getb(reg.PC);

  reg.PC++;

}

В данном случае инструкция состоит из двух байт: один — это сама инструкция, второй — адрес операнда инструкции. Функция
ram_getb возвращает значения байта в RAM по адресу.

РЕКОМЕНДУЕМ:
Как самому создать игру

А вот пример кода для инструкции
STX.

void

op_stx()

{

  ram_setb(cpu_addr, reg.X);

}

Функция
ram_setb заменяет значение байта в RAM по адресу
cpu_addr (операнд инструкции) на значение регистра
X.

Таблица инструкций

Таблица инструкций

Также не стоит забывать, что MOS 6502 — мультицикличный процессор и разные инструкции могут выполняться разное время. Поэтому, чтобы выверить точное время выполнения, нужно знать, за сколько циклов выполняется инструкция.

Полный список инструкций и режимов адресации можно найти в «Викиучебнике» и описании инструкций.

Мапперы

Так как в NES могут адресоваться только 64 Кбайт данных, чтобы переключаться между разными банками, в картридже существуют так называемые мапперы.

Адрес Назначение
$0000 — $07FF 2 Кбайт внутренней RAM
$0800 — $1FFF Ссылки на $0000 — $07FF
$2000 — $2007 Регистры PPU
$2008 — $3FFF Ссылки на $2000 — $2007
$4000 — $401F I/O и APU-регистры
$6000 — $7FFF Банк PRG RAM
$8000 — $BFFF Нижний банк PRG ROM
$C000 — $FFFF Верхний банк PRG ROM

Маппер отвечает за переключение PRG (Program) ROM и CHR (Character) ROM. В PRG ROM лежит основной код игр, он подключен напрямую к CPU. В CHR ROM лежат графические объекты, и он уже подключен к PPU.

Одновременно в процессор могут быть подключены только два банка PRG ROM по 16 Кбайт. Нижний банк расположен по адресам
$8000 $BFFF, верхний находится по адресам
$C000 $FFFF.

В разных картриджах могут быть разные мапперы. Всего их более двухсот, но не обязательно эмулировать их все. Достаточно сделать реализацию нескольких мапперов для запуска самых популярных игр.

Можно реализовать маппер как три функции:

  • функция инициализации;
  • врапперы над функциями получения и установки байтов RAM, такие как
    ram_getb и
    ram_setb.

Вот как выглядит маппер UxROM.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

void

uxrom_init()

{

  prg_rom.low = 0;

  prg_rom.up = prg_rom.n 1;

  chr_rom.cur = 0;

}

uint8_t

uxrom_getb(uint16_t addr)

{

  return ram_general_getb(addr);

}

void

uxrom_setb(uint16_t addr, uint8_t b)

{

  if (addr < 0x8000)

    ram_general_setb(addr, b);

  else if (ram_getb(addr) == b)

    prg_rom.low = b;

}

В данном случае маппер изначально выставляет верхний банк PRG ROM последним из возможных, и его адрес остается фиксированным все время. Нижняя же часть может меняться. Для этого пользователю нужно записать номер PRG ROM в память картриджа. Если номер картриджа совпадает со значением, которое лежит по адресу, куда пользователь записывает, то маппер меняет нижний банк.

Picture Processing Unit

Ключевую роль в отрисовке играет PPU — Picture Processing Unit. Именно благодаря ему у NES для своего времени была хорошая графика. 256 на 240 пикселей и палитра из 64 цветов прекрасно смотрелись на телевизорах того времени.

Если посчитать, то выйдет, что для хранения одного изображения на экране понадобится 45 Кбайт. Сейчас это значение выглядит смешно, но еще тридцать лет назад эта цифра была непосильна для консолей. Поэтому PPU достигает компромисса между эффективностью, памятью и качеством картинки.

Скриншот из Castlevania

Скриншот из Castlevania

Отрисовка фона

Весь экран можно разделить на 32 × 30 тайлов, каждый из которых — квадрат 8 × 8 пикселей. Четыре тайла составляют блок. Можно добавить разметку, чтобы лучше видеть структуру картинки.

Тайлы и блоки

Тайлы и блоки

Сетка темно-зеленая в случае с блоками и светло-зеленая для тайлов.

Все символы рисуются как пиксель-арт, в котором может быть только четыре цвета (2 бита на пиксель, 16 байт на тайл). Таким образом, все изображение занимает 15 Кбайт, тогда как PPU доступно только около 12 Кбайт.

Адрес Назначение
$0000 — $0FFF Таблица символов 0(CHR ROM)
$1000 — $1FFF Таблица символов 1(CHR ROM)
$2000 — $23FF Таблица имен 0
$2400 — $27FF Таблица имен 1
$2800 — $2BFF Таблица имен 2
$2C00 — $2FFF Таблица имен 3
$3000 — $3EFF Ссылки на $2000 — $2EFF
$3F00 — $3F1F Палитры

Таблица имен

Для еще большей экономии места существуют таблицы имен и таблицы атрибутов. Каждый байт в таблице имен назначает, какой символ будет находиться в тайле, своеобразный индекс в таблице шаблонов. На одну таблицу имен уходит 960 байт в памяти PPU.

Скриншот с таблицей имен

Скриншот с таблицей имен

Палитры

В NES есть внутренняя палитра из 64 цветов. Также есть восемь палитр (четыре для фона, четыре для спрайтов), состоящие из четырех цветов, один из которых — цвет фона. Создатель игр может менять эти палитры, чтобы добиться наилучшей картинки. Таким образом, в них хранятся индексы внутренней палитры NES.

Пример игровых палитр

Пример игровых палитр

В таблице атрибутов хранятся как раз индексы палитр. Каждый из этих индексов соответствует одному блоку, поэтому внутри одного блока можно использовать только четыре цвета. Благодаря этим особенностям игры на NES выглядят так, будто состоят из отдельных блоков, хотя во многих случаях разработчики игр умело обходят ограничения консоли.

За выбор палитры отвечает таблица атрибутов, последний компонент отрисовки.

Таблица атрибутов

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

Скриншот с палитрами

Скриншот с палитрами

Как вы заметили, в разных блоках расположены разные палитры, всего их четыре. Но разработчики научились умело скрывать это, чередуя палитры и тайлы. Так как на один атрибут уходит два бита, то на всю таблицу уходит не более 64 байт. Всего с учетом того, что NES имеет две таблицы шаблонов (или 512 символов), четыре таблицы имен и четыре таблицы атрибутов, в итоге занято около 12 Кбайт. Существенно меньше и намного более гибко!

Отрисовка спрайтов

Для того чтобы рисовать динамические объекты, существуют спрайты. Их отрисовка несильно отличается от отрисовки фона, но тут есть ряд особенностей.

PPU имеет отдельную память — OAM (object attribute memory), в которой находятся параметры разных спрайтов. Всего спрайтов может быть до 64, каждый спрайт занимает четыре байта. Они отвечают за индекс символа для отрисовки, позицию на экране (x, y), флаги. Во флагах находится номер палитры, флаг отражения по вертикали и горизонтали, а также приоритет спрайта.

РЕКОМЕНДУЕМ:
Лучшие игры для программистов и технарей

Флаги отражения предназначены для простого отражения спрайта по вертикали и горизонтали. Это позволяет экономить на памяти для спрайтов и сохранить время процессора. Приоритет отвечает за то, какой спрайт должен быть отрисован, если спрайты перекрываются. Часто используются комбинации из нескольких спрайтов для отрисовки больших объектов — например, персонажей.

Синхронизация между CPU и PPU

Для общения между CPU и PPU в память CPU отображены некоторые регистры PPU. С их помощью CPU может управлять работой PPU.

  • Controller отвечает за выбор таблиц шаблонов и таблиц имен, размер спрайтов, а также за NMI.
  • С помощью регистра Mask можно включать и выключать отображение фона, спрайтов, менять цветовую гамму изображения.
  • Регистр Scroll отвечает за перемещение камеры для заднего фона. Скроллинг может быть не только горизонтальным, но и вертикальным.

Пример скроллинга

Пример скроллинга

Полный список регистров PPU

NMI

CPU и PPU работают на разных частотах, но, чтобы отрисовывать картинку, они должны работать синхронно. Весь цикл рендеринга PPU состоит из 262 тактов.

Такты Происходящее на экране
0–240 Отрисовывается картинка
241 Пропуск
242–261 Установка флага Vblank и срабатывание NMI

С 242-го по 261-й такт PPU устанавливает Vblank и вызывает NMI. Он уведомляет процессор, что отрисовка кадра закончена и PPU не будет обращаться к памяти, чтобы избежать конфликтов с CPU, а также сообщает, что ждет дальнейших команд от процессора.

NMI — non maskable interrupt. Прерывание, которое срабатывает после того, как PPU отрендерил кадр. Во время него у CPU есть около 2273 циклов, чтобы подготовить следующий кадр.

Пример кода PPU для отрисовки

PPU отрисовывает линии по очереди. Так будет выглядеть код для отрисовки линии для фона.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

void

ppu_draw_tile_line(int tile, int screen_x, int screen_y, int ny, int pal)

{

  uint8_t low, high;

  int i, clr;

  low  = ppu_getb(tile + ny % 8);

  high = ppu_getb(tile + ny % 8 + 8);

  for (i = 0; i < 8; i++) {

    int clr0, clr1;

    if (scr_x + i < 0 || scr_x + i > 255)

      continue;

    clr0 = (high >> (7 i)) & 1;

    clr1 = (low  >> (7 i)) & 1;

    clr = (clr0 << 1) | clr1;

    set_pixel(screen_y, screen_x + i, ppu_getb(pal + clr));

  }

}

void

ppu_draw_bg_line()

{

  int x, y, nx, ny;

  int screen_x, screen_y;

  uint16_t patt, name, name_right, attrib_left, attrib_right;

  screen_y = ppu.scanline;

  y = ppu.PPUSCROLL_Y + ppu.scanline;

  ny = y % 240;

  patt = ppu_bg_patt_tbl();

  name_left  = ppu_get_name_tbl_left(ppu.PPUSCROLL_X, y);

  name_right = ppu_get_name_tbl_right(ppu.PPUSCROLL_X, y);

  attrib_left  = name_left  + 0x3C0;

  attrib_right = name_right + 0x3C0;

  for (screen_x = ppu.PPUSCROLL_X % 8; screen_x < 256; screen_x += 8) {

    uint16_t name, attrib, pal;

    int tile, tile_idx;

    x = screen_x + ppu.PPUSCROLL_X;

    nx = x % 256;

    if (x < 0)

      continue;

    if (x < 256) {

      name = name_left;

      attrib = attrib_left;

    } else {

      name = name_right;

      attrib = attrib_right;

    }

    tile_idx = ppu_getb(name + (ny >> 3) * 32 + (nx >> 3));

    tile = patt + tile_idx * 16;

    pal = attrib + (ny >> 5) * 8 + (nx >> 5);

    pal = ppu_getb(pal);

    if (ny % 32 >= 16)

      pal >>= 4;

    if (nx % 32 >= 16)

      pal >>= 2;

    pal = 0x3f00 + (pal % 4) * 4;

    ppu_draw_tile_line(tile, screen_x, screen_y, ny, pal);

  }

}

На одном экране может присутствовать больше одной таблицы имен. Поэтому в коде нужно учитывать и это.
Две таблицы имен на экране

Две таблицы имен на экране

Функция
ppu_draw_tile_line получает на вход адрес тайла, координаты, по которым он должен появиться, номер строки и палитру. На выходе она отрисовывает одну из линий этого тайла. Функция
ppu_getb отвечает за получение байта памяти из PPU.

В функции
ppu_draw_bg_line происходит непосредственная отрисовка одной линии заднего вида. Функция проходит по всем тайлам и отрисовывает их с учетом скроллинга. Для этого она в первую очередь определяет, какую таблицу имен использовать. Она также находит индекс тайла в таблице имен и его адрес в таблице шаблонов. После этого функция находит номер палитры в таблице атрибутов, а позже и адрес самой палитры.

РЕКОМЕНДУЕМ:
Лучшие эмуляторы SNES для Android

Выводы

На мой субъективный взгляд, этого материала достаточно, чтобы получить общее представление о работе NES и начать разрабатывать эмулятор. Всех желающих разобраться в этой теме подробнее приглашаю посетить репозиторий с уроками по созданию игр на NES и репозиторий с эмулятором NES.

Статья была опубликована 27.12.2018 и обновлена 31.08.2019.

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (7 оценок, среднее: 5,00 из 5)

Загрузка…

Facebook Twitter VK Telegram Youtube Яндекс Дзен

Техническая поддержка

Вернуться на старую версию

© 2006–2023, Habr

image

2. Фундаментальные понятия

Содержание:

  • Работа с данными
  • Регистры процессора
  • Память
  • Как задаются данные
  • Как сделать данные человекочитаемыми
  • Соединяем всё вместе

Что такое компьютер?

Вопрос кажется простым, но он затрагивает самую суть того, что делаем мы как программисты. Пока скажем, что «компьютер» — это нечто, исполняющее программу. «Программа» — это просто последовательность команд, а под исполнением программы подразумевается, что команды выполняются с начала и одна за другой. (Если вы читаете программу и сами исполняете команды, то поздравляю! Вы — компьютер!)

У каждого компьютера есть конкретный набор команд, которые он умеет исполнять. Мы называем его набором команд компьютера (да, очень оригинальное название). Набор команд можно описать множеством способов, но пока давайте будем считать, что команды в наборе команд обозначены числами. То есть программа — это просто перечень чисел, каждое из которых задаёт определённое выполняемое действие. Вот пример гипотетического набора команд:

  • 1: двигаться вперёд
  • 2: повернуть влево
  • 3: повернуть вправо

По сути, это набор команд Logo — языка программирования «черепашьей графики», позволяющего перемещать робота по листу бумаги при помощи ручки, чтобы он создавал интересные рисунки. Изображение: Valiant Technology Ltd., CC-BY-SA 3.0.

Запущенная на компьютере с этим набором команд программа, которая должна переместиться вперёд три раза, повернуть вправо, дважды переместиться вперёд, повернуть влево и переместиться вперёд четыре раза, будет выглядеть так:

1 1 1 3 1 1 2 1 1 1 1

Работа с данными

Часто команды, которые должен исполнять компьютер, получают в каком-нибудь виде данные. Компьютеры часто складывают числа; гораздо проще иметь одну команду сложения, чем целое множество подобных команд:

  • 1: прибавить 1
  • 2: прибавить 2
  • 3: прибавить 3
  • 4: прибавить 4

Или же вместо одной команды «прибавить 1», вызываемой нужное количество раз, которую было бы столь же неудобно использовать. Программа, прибавляющая к числу 1000, занимала бы в 1000 раз больше места на накопителе (и выполнялась бы в 1000 раз дольше) по сравнению с программой, прибавляющей 1!

Сопровождающие команду данные должны находиться в какой-то части программы. В различных языках программирования эта задача решается по-разному. В некоторых языках «код» (команды) должен храниться совершенно отдельно от данных, в других они объединяются. Оба подхода имеют свои плюсы и минусы, но пока давайте рассмотрим объединённые команды и данные.

В нашем гипотетическом компьютере для «сложения каких-то чисел» набор команд мог бы выглядеть следующим образом:

  • 1: сохранить следующее число как «первое число»
  • 2: прибавить следующее число к первому числу, если оно сохранено

Программа, складывающая числа 2 и 7, выглядела бы так:

1 2 2 7

Пошагово двигаясь по программе по одному числу за раз, мы видим команду «1» («сохранить следующее число как первое число»). Следующее число — это «2», поэтому 2 сохраняется как первое число. Далее мы видим команду «2» («прибавить следующее число к первому числу»). Следующее число — это «7», поэтому наша программа прибавляет 7 к 2, получая результат 9. Здесь данные и команды перемешаны. Увидев «1 2 2 7», невозможно понять, какие из «2» — это команда «прибавить следующее число к первому числу», а какие — алгебраическое число «2», не посмотрев на начало и не пройдя пошагово всю программу.

Где находится результат (9)? Как дальше в программе нам сделать что-нибудь с этим результатом? И что же подразумевается под «сохранением» чего-либо?

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

Как мы только что увидели, программам часто требуется место для временного хранения данных. В большинстве компьютеров для этого используются регистры — небольшие участки внутри процессора, каждое из которых может хранить одно значение. [«Значения» — это просто числа; как мы уже делали с командами в наборе команд, можно взять значение любого типа и представить его в виде числа при условии, если у нас есть какое-то сопоставление между числами и обозначаемыми ими данными. Например, Unicode при помощи 32-битного числа описывает все возможные символы из каждой системы письма на Земле. (Подробнее о «битах» мы поговорим ниже.)]

Регистры могут быть обобщёнными или связанными с определёнными типами функциональности. Например, в процессоре NES есть регистр под названием накопитель (accumulator), часто сокращаемый до «A»; он занимается всеми математическими операциями. В наборе команд 6502 есть команды, работающие следующим образом:

  • сохранить следующее число в накопитель
  • прибавить следующее число к накопителю, результат записать в накопитель
  • поместить куда-нибудь число из накопителя

Это решает проблему сохранения чисел и доступа к ним. Но мы не ответили на ещё один вопрос: когда мы «помещаем куда-нибудь число из накопителя», где находится это «куда-нибудь»? Процессор 6502 консоли NES имеет всего три регистра, поэтому сложные программы не могут использовать для хранения результатов только регистры.

Память

Компьютеры предоставляют программам доступ к какому-то объёму (непостоянной) памяти для временного хранения, что позволяет компьютеру иметь небольшое количество (дорогих) регистров, в то же время обеспечивая возможность хранения за пределами самой программы приемлемого количества значений. Эта память выглядит как последовательность «ящиков» размером с регистр, каждый из которых содержит одно значение и ссылка к которому осуществляется по номеру. NES предоставляет разработчику программу с двумя килобайтами (2 КБ) пространства памяти, пронумерованную от нуля до 2047 — номер в пространстве памяти называется его адресом (как адрес дома). То есть рассмотренный нами ранее набор команд 6502 на самом деле ближе к такому:

  • сохранить следующее число в накопитель
  • прибавить следующее число к накопителю, результат записать в накопитель
  • поместить число из накопителя по адресу памяти следующего числа

Как задаются данные

И это приводит нас к последнему вопросу этой главы — как все эти числа представлены внутри компьютера?

Ранее мы использовали «стандартные» десятичные числа (по основанию 10). Это те числа, которые мы пользуемся повседневно, например, «2», или «7», или «2048». Однако компьютеры работают на электрических токах, которые могут быть или «включенными» или «выключенными», без каких-то промежуточных значений. Эти токи образуют основу всех данных внутри компьютера, поэтому компьютеры используют двоичные числа (по основанию 2).

Наименьшей единицей информации, которую может обработать компьютер, является «бит» (bit, сокращение от binary digit). Бит хранит в себе одно из двух значений — 0 или 1, «включено» или «выключено». Если мы объединим как одно число несколько битов, то сможем задавать больший диапазон значений. Например, двумя битами можно задать четыре разных значения:

00 01 10 11

Три бита позволяют задать восемь разных значений:

000 001 010 011 100 101 110 111

Каждый добавленный нами бит позволяет задавать в два раза больше значений, аналогично тому, как каждый десятичный разряд, добавляемый к десятичному числу, позволяет задавать в десять раз больше значений (1 → 10 → 100 → 1000). Объединённые вместе восемь бит, задающих одно значение, используются настолько часто, что имеют собственное название: байт (byte). В байте может храниться одно из 256 значений. Так как четыре бита являются половиной байта, их иногда называют полубайтом (nybble). В полубайте может храниться одно из 16 значений.

Часто говорят, что компьютеры (в том числе и видеоигровые консоли), имеют определённую битность. Современные десктопные компьютеры/ноутбуки обычно являются 64-битными, старые версии Windows, например, Windows XP, называют 32-битными операционными системами, а NES — это 8-битная система. Все эти числа характеризуют размер регистров компьютера — количество битов, которые может одновременно хранить один регистр. [Несколько усложняет понимание то, что адресная шина NES имеет ширину 16 бит, то есть NES может обрабатывать 65536 различных адресов памяти, а не 256. Однако каждый адрес памяти всё равно хранит только один байт.] Так как NES — это «8-битный» компьютер, каждый его регистр хранит 8-битное значение (один байт). Кроме того, каждый адрес памяти может хранить один байт.

Как же работать с числами больше 255? Игроки в Super Mario Bros. часто зарабатывают десятки тысяч очков, и одного байта явно недостаточно для хранения таких чисел. Когда нам нужно задать значение намного больше, чем может храниться в одном байте, мы используем несколько байтов. В двух байтах (16 бит) можно хранить одно из 65536 значений, а при увеличении количества байтов возможности задания чисел резко возрастают. В трёх байтах можно хранить число до 16777215, а в четырёх — до 4294967295. Когда мы используем таким образом больше одного байта, мы всё равно ограничены размером регистра компьютера. Чтобы работать с 16-битным числом на 8-битной системе, нам нужно получать или сохранять число в двух частях — «младший» (low) байт справа, «старший» (high) байт слева. [Именно из-за необходимости работы с такими значениями из нескольких регистров у процессоров есть порядок следования байтов (endianness) — т. е., у них определено, какой байт идёт первым при работе с большими числами. В процессорах с прямым порядком байтов (он называется little-endian), например, в 6502, сначала идёт младший байт, а затем старший. В процессорах с обратным порядком байтов (big-endian), например, в Motorola 68000, ситуация противоположная — ожидается, что сначала идёт старший байт, а за ним следует младший. Большинство современных процессоров является little-endian из-за очень популярной архитектуры x86 компании Intel, тоже являющейся little-endian.]

Так как управляющий консолью NES процессор 6502 одновременно работает с восемью битами данных, для задания чисел меньшего размера всё равно используется восемь бит. Это может быть неэффективно, поэтому при необходимости в одном байте часто хранят несколько значений меньшего размера. Один байт может содержать два четырёхбитных числа, или четыре двухбитных числа, или даже восемь отдельных значений «включено»/«выключено» (мы называем их флагами).

Например, байт 10110100 может задавать:

  • Одно 8-битное значение: 180
  • Два 4-битных значения: 11 (1011) и 4 (0100)
  • Четыре 2-битных значения: 2 (10), 3 (11), 1 (01) и 0 (00)
  • Восемь значений «включено»/«выключено» (или «истина»/«ложь»): вкл., выкл., вкл., вкл., выкл., вкл., выкл., выкл.
  • Любое другое сочетание битовых длин, в сумме составляющих восемь

Для удобства обсуждения таких ситуаций, когда в одном байте хранится несколько значений, обычно каждому биту в байте присваивается номер, почти так же, как мы давали название «младшему» и «старшему» байтам в 16-битном значении. Самый правый бит называется «бит 0» и счёт идёт до самого левого «бита 7». Вот пример:

байт:   1 0 1 1 0 1 0 0
№ бита: 7 6 5 4 3 2 1 0

Как сделать данные человекочитаемыми

Как мы видели, байты — это очень гибкий способ задания различных типов данных в компьютерной системе. Однако недостаток использования байтов заключается в том, что их сложно читать. Приходится прикладывать усилия, чтобы мысленно преобразовать «10110100» в десятичное число «180». Когда вся программа представлена в виде последовательности байтов, проблема сильно усугубляется.

Для решения этой проблемы основная часть кода представлена в виде шестнадцатеричных чисел. «Шестнадцатеричное» означает «по основанию 16»; одно шестнадцатеричное (hexadecimal, «hex») число может содержать одно из шестнадцати значений. Вот числа от нуля до пятнадцати, представленные в шестнадцатеричном виде:

0 1 2 3 4 5 6 7 8 9 a b c d e f

Шестнадцатеричная запись полезна, потому что шестнадцатеричное число и полубайт хранят одинаковый диапазон значений. Это значит, что можно задать байт двумя шестнадцатеричными значениям, то есть в гораздо более компактном и удобном виде.

Работа с числами, которые могут быть десятичными, двоичными или шестнадцатеричными, может запутывать. Например, число «10» обозначает 10, если оно десятичное, 2, если оно двоичное или 16, если оно шестнадцатеричное. Чтобы понимать, какое значение мы имеем в виду, принято использовать префиксы. 10 — это десятичное число, %10 — двоичное, а $10 — шестнадцатеричное.

Соединяем всё вместе

Рассмотрев множество вопросов, связанных с работой компьютеров (и программ), давайте ещё раз взглянем на весь процесс, происходящий при выполнении программы.

Во-первых, сама программа представлена в виде последовательности байтов (так называемого машинного кода). Каждый байт — это или команда для процессора, или сопровождающие команду данные.

С самого начала программы процессор многократно выполняет трёхэтапный процесс. Сначала процессор получает следующий байт из программы. В процессоре есть специальный регистр под названием счётчиком программ (program counter), он отслеживает, каким будет следующий номер байта программы. Счётчик программ (program counter, PC) работает совместно с регистром под названием адресная шина (address bus), отвечающим за получение и сохранение байтов из программы или из памяти, для получения байтов.

Далее процессор декодирует полученный им байт, выясняя, какой записи в наборе команд соответствует этот байт (или какую команду сопровождает байт данных). Наконец, он исполняет команду, внося изменения в регистры процессора или в память. Процессор выполняет инкремент счётчика программ на единицу, чтобы получить следующий байт программы, и цикл начинается снова.

3. Приступаем к разработке

Содержание:

  • Настройка среды разработки
  • Текстовый редактор
  • Ассемблер и компоновщик
  • Эмулятор
  • Графические инструменты
  • Инструменты для создания музыки
  • Соединяем всё вместе
  • Дальнейшие шаги

Давайте начнём программировать для NES! В этой главе мы настроим среду разработки, установим все инструменты, которые необходимы для описанной в этой книге работы, а затем соберём и запустим самую простую игру, чтобы убедиться, что всё работает.

Настройка среды разработки

Здесь перечислены все инструменты, которые мы будем устанавливать. Некоторые из них мы будем использовать сразу (и постоянно), другие более специализированы и пригодятся позже. Для каждой категории я указал конкретное ПО, которое буду использовать в этой книге; однако есть множество других вариантов, так что освоившись с рекомендуемыми мной, вы сможете поэкспериментировать и с другими инструментами.

  • Текстовый редактор (на ваш выбор)
  • Ассемблер/компоновщик (ca65 и ld65)
  • Эмулятор (Nintaco)
  • Графический инструмент, способный считывать/сохранять изображения в формате NES (NES Lightbox)
  • Инструмент для создания музыки (FamiStudio)

Текстовый редактор

Во-первых, вам понадобится текстовый редактор. Думаю, вы уже что-то программировали, поэтому у вас есть любимый текстовый редактор. Если нет, то можно попробовать одну из следующих программ:

  • Sublime Text. Кроссплатформенная, популярная среди разработчиков, простая в освоении; когда вы освоитесь с основами, становится мощным инструментом.
  • Atom. По сути, ответ GitHub’а на Sublime Text. Кроссплатформенная, с гибкими настройками.
  • Visual Studio Code. Качественная платформа Microsoft для редактирования текста. Создавалась для веб-разработки, но имеет возможность расширения для любого вида программирования. Также кроссплатформенная, не только для Windows.
  • Vim, emacs, nano, и т. д. Текстовые редакторы с давней историей, работающие из командной строки. (Лично я пользуюсь Vim, но выбор за вами.)

Ассемблер и компоновщик

Ассемблер компилирует ассемблерный код (который мы будем писать в этой книге) в машинный код — сырой поток байтов, считываемый процессором. Компоновщик (linker) берёт набор файлов, который был пропущен через ассемблер, и превращает их в единый файл программы. Так как у каждого процессора свой машинный код, ассемблеры обычно предназначены только для одного типа процессора. Существует множество вариантов ассемблеров и компоновщиков для процессора 6502, но в этой книге мы будем использовать ca65 и ld65. Они имеют открытый исходный код и являются кроссплатформенными, а также обладают очень полезными функциями для разработки больших программ. ca65 и ld65 — это часть более масштабного комплекта программ «cc65», включающего в себя компилятор C и многое другое.

Mac

Для установки ca65 и ld65 на Mac нужно для начала установить менеджер пакетов Mac Homebrew. Скопируйте команду с главной страницы, вставьте её в терминал и нажмите Enter; выполните инструкции, после чего Homebrew будет готов к работе. Установив Homebrew, введите brew install cc65 и нажмите Enter.

Windows

В Windows необходимо скачать ca65 и ld65 в определённую папку на компьютере. Скачайте последний «Windows Snapshot» с страницы основного проекта cc65. Распакуйте содержимое в C:cc65. Также вам нужно будет дополнить системные пути, чтобы ca65 и ld65 были видны из любой папки. Этот процесс зависит от того, какой версией Windows вы пользуетесь. В самых новых версиях Windows можно нажать правой кнопкой мыши на «Мой компьютер», выбрать «Свойства», «Дополнительные параметры системы», а затем «Переменные среды». Вам нужно будет найти запись %PATH% и добавить в её конец C:cc65bin.

Linux

Вам нужно будет собрать cc65 из исходников. К счастью, это довольно простой процесс. Сначала убедитесь, что у вас есть git и базовая среда для сборки — например в Ubuntu для этого достаточно выполнить sudo apt-get install git build-essential. Затем перейдите в папку, куда вы хотите установить cc65, клонируйте репозиторий cc65 и соберите его:

git clone https://github.com/cc65/cc65.git
cd cc65
make

Затем сделайте программы cc65 доступными из любой папки, выполнив sudo make avail. Эта команда добавит символическую ссылку из вашей папки cc65 в /usr/local/bin.

Эмулятор

Эмулятор — это программа, запускающая программы, предназначенные для другой компьютерной системы. Мы будем использовать эмулятор NES, чтобы запускать создаваемые нами программы на том же компьютере, где их разрабатываем, вместо запуска на аппаратной NES. Существует множество эмуляторов NES (а когда вы наберётесь опыта в разработке для NES, будет интересно и написать собственный!), но для этой книги мы будем использовать Nintaco.

Nintaco.

Он кроссплатформенный, и к тому же является одним из немногих эмуляторов, имеющих инструменты отладки, которые пригодятся, когда мы будем писать программы.

Установка Nintaco на всех платформах происходит одинаково — достаточно скачать его с веб-сайта Nintaco и распаковать. Чтобы запустить Nintaco, нужно дважды нажать на Nintaco.jar. Для запуска Nintaco требуется Java; если на вашем компьютере не установлена Java, скачайте «Java Runtime Environment» с сайта java.com.

Графические инструменты

NES хранит графику в собственном уникальном формате, непохожем на традиционные типы изображений наподобие JPEG или PNG. Нам понадобится программа, способная работать с изображениями NES. Существуют плагины для больших графических пакетов типа Photoshop или GIMP, но мне нравится использовать для этого компактный специализированный инструмент. Для этой книги мы будем использовать NES Lightbox — кроссплатформенную производную от NES Screen Tool.

NES Lightbox.

Windows

Скачайте Windows-установщик (для 64-битных систем). Дважды нажмите на «NES Lightbox Setup 1.0.0.exe», чтобы установить программу.

Mac

Скачайте Mac DMG. Дважды нажмите на файл .dmg, чтобы открыть его, и перетащите приложение NES Lightbox в папку Applications. При первом запуске приложения нужно будет нажать на него правой клавишей мыши и выбрать «Open», потому что оно не «подтверждено» компанией Apple.

Linux

В системах с Ubuntu можно скачать файл Snap, который является автономным пакетом приложения. В случае других дистрибутивов Linux (или если вы предпочитаете AppImage) нужно скачать файл AppImage. Прежде чем запустить файл AppImage, его нужно пометить как исполняемый.

Инструменты для создания музыки

Как и в случае с графикой, звук на NES представлен в уникальном формате — это команды аудиопроцессора, а не что-то типа MP3. Самая популярная программа для создания звука для NES — FamiTracker, это мощный, но сложный инструмент, предназначенный только для Windows. Для этой книги мы будем использовать FamiStudio — кроссплатформенную программу с более дружественным интерфейсом, результаты работы в которой сохраняются в простой для интеграции формат.

FamiStudio.

Windows / Mac / Linux

Скачайте последнюю версию с веб-сайта FamiStudio.

Соединяем всё вместе

Установив все инструменты, нужно убедиться, что они работают. Мы создадим аналог «Hello World» для игр на NES: заполним весь экран одним цветом.

Откройте текстовый редактор и создайте новый файл helloworld.asm. Скопируйте и вставьте в файл следующий код:

Код в текстовом виде

.segment "HEADER"
.byte $4e, $45, $53, $1a, $02, $01, $00, $00

.segment "CODE"
.proc irq_handler
RTI
.endproc

.proc nmi_handler
RTI
.endproc

.proc reset_handler
SEI
CLD
LDX #$00
STX $2000
STX $2001
vblankwait:
BIT $2002
BPL vblankwait
JMP main
.endproc

.proc main
LDX $2002
LDX #$3f
STX $2006
LDX #$00
STX $2006
LDA #$29
STA $2007
LDA #%00011110
STA $2001
forever:
JMP forever
.endproc

.segment "VECTORS"
.addr nmi_handler, reset_handler, irq_handler

.segment "CHARS"
.res 8192
.segment "STARTUP"

Теперь нам нужно воспользоваться ассемблером. В папке, в которую вы сохранили helloworld.asm, выполните команду ca65 helloworld.asm. В результате появится новый файл helloworld.o. Это объектный файл — машинный код. Но он пока не находится в формате, готовом для запуска в эмуляторе. Чтобы преобразовать его, нужно запустить компоновщик. В той же папке выполните команду ld65 helloworld.o -t nes -o helloworld.nes. В результате этого должен появиться новый файл helloworld.nes — файл «ROM» для эмулятора.

Запустите Nintaco и выберите в меню «File» пункт «Open». Выберите только что созданный файл helloworld.nes и нажмите Open. В результате вы увидите зелёный экран.

[В оригинале главы зелёный экран — это настоящий работающий в браузере эмулятор NES! Я воспользовался потрясающим jsnes, созданным Беном Фиршманом. Каждый раз, когда мы будем компилировать файл .nes, я буду включать в текст подобное работающее демо. (Сейчас сложно это понять, но на самом деле эмулятор работает с частотой кадров 60fps.)]

Дальнейшие шаги

Если вы увидели в Nintaco зелёный экран, поздравляю! Ваша среда разработки готова к использованию. В следующей главе мы расскажем, что же делает скопированный нами код, и немного узнаем о том, как работает оборудование NES.

Создание игр для NES на ассемблере 6502: оборудование NES и знакомство с ассемблером

Страницы: [1] 2 3 Далее  Все   Вниз

Тема: Написать эмулятор NES самому  (Прочитано 23887 раз)

0 Пользователей и 1 Гость просматривают эту тему.

Хочу летом, на досуге, написать эмулятор Nes на Java.
Но как?  :-
Java немного знаю и постоянно совершенствуюсь в этом направлении, а вот как писать эмулятор, не представляю.
Какую литературу почитать? Может кто какие документы по теме подкинет. Буду крайне признателен.  :D


Ну можешь посмотреть исходники другого nes эмулятора, этого например.



gepar, спасибо.
HoRRoR, спасибо, то, что нужно.
Всем добра  :hi:


Охренеть. Во народ пошел…

Java немного знаю…

и сразу же эмулятор писать.  o_0


CrazyMax,ну так всегда же хочется сразу что-то интересное написать, все эти элементарные программы вроде калькулятора это же не так интересно  :)


Эмуляция калькулятора — это самое сложное, пусть сначала эмуль NESа напишет  ;)



HoRRoR,при участии новичков — да, обычно все. На хабре кстати была статья о написание эмулятора старенькой атари, это так чтобы в общем представить что же именно надо делать, можешь поискать, эмулятор Atari 2600 если не ошибаюсь автор там писал.


Если делать эмуль то такой который превосходит уже существующие.


Если делать эмуль то такой который превосходит уже существующие.

Поэтому я и подчеркнул «…Java немного…», а то что знать как работают процессоры (все равно какие), их регистры, флаги и т.д., как работает шина, память (как мапится, как идет обращение…)… как использовать битовые/логические операции (без них тут вообще никак) так это все не надо?
А так получается, выучил «немножко» язык программирования, вывел на экран пару строчек (окон) и вперед эмулятор писать.

« Последнее редактирование: 11 Май 2011, 19:22:43 от CrazyMax »


CrazyMax, год на C/C++, несколько лет на Pascal, Около полугода на Java…  <_<


CrazyMax, год на C/C++, несколько лет на Pascal, Около полугода на Java…  <_<

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

P.S. ilua_great, я не пытаюсь разубедить, просто изучать языки программирования с написания эмулятора — это полный бред (поверь мне, я начал программировать в 1989 году, правда тогда о эмуляторах ничего не знали :) ) .

« Последнее редактирование: 11 Май 2011, 19:36:59 от CrazyMax »


Документы я получил. Буду разбираться.
Откуда такая уверенность в провале?


ilua_great,
Эмулятор на квантовый наладонник пишешь?


Именно!
Люди пишут эмуляторы на Java, чем я хуже?
Потом на наладонник перенесу.
Буду на парах в танчики гонять на эмуляторе собственного производства же.  :cool:


Какие все умные, лол. Правда в компетентности умников сомневаюсь.
Может человек для саморазвития взялся писать, какая вам нахрен разница?

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

Что ты этим хочешь сказать? Что надо знать как работает микроволновка, чтобы разогреть в ней еду? Реализации могут быть разными, интерфейсы, протоколы и прочее стандартизированы. И зачем знать подробности конкретной реализации для воспроизведения программного кода? Достаточно иметь спецификацию, в которой описаны стандарты. А уж как это будет работать — решать тебе, исходя из обстановки.


Не путай разные вещи — пользоваться чем-либо и эмулировать ее работу.

О компетентности «умников» я промолчу.


(поверь мне, я начал программировать в 1989 году, правда тогда о эмуляторах ничего не знали  ) .

Да ладно? Мне почему-то кажется что первые атари додумались эмулировать в те времена  :)


Не путай разные вещи — пользоваться чем-либо и эмулировать ее работу.

Ну, скажем так, в данном случае надо эмулировать нагревание пищи в замкнутом пространстве электромагнитными волнами определённой частоты, а не генерацию оных и прочее. Нам дан интерфейс — генератор электромагнитных волн, как он был реализован в каждом конкретном случае — нас не колышит.
Если передо мной, например, стоит цель эмулировать выполнение программного кода, зачем мне знать, как это делает процессор? Мне достаточно знать информацию о каждой эмулируемой инструкции, эмуляция процессора будет крайне абстрактна, чем-то даже можно пренебречь. А вся необходимая информация о железе прекрасно описана в документации.
Речь идёт об эмуляторе платформы (принципов работы железа), а не об эмуляторе конкретного железа со всеми его особенностями. Другой вопрос — насколько надо абстрагироваться, ибо не всегда спецификации достаточно, не всё бывает описано и часто встречаются подводные камни, особенно в сложных системах.


Мы говорим о разных вещах, я не говорю, что должен знать каждый сигнал проходящий в крестале, а о том что должен знать как все это работает (флаги, регистры и т.д.), а если это не знать, как найти ошибку в инструкции, как отладить.

Начинается оффтоп.


Мы говорим о разных вещах, я не говорю, что должен знать каждый сигнал проходящий в крестале, а о том что должен знать как все это работает (флаги, регистры и т.д.), а если это не знать, как найти ошибку в инструкции, как отладить.

Ну а разве в документации конкретно по этой платформе это не описано? На чём тогда познавать как это работает? Можно, конечно, читать литературу с абстрактными примерами, но ведь на реальном примере даже гораздо легче разобраться.
Вообще автору темы бы, конечно, не помешало бы позаниматься отладкой игрушек.

Начинается оффтоп.

А по-моему он начался с пятого поста.


Люди пишут эмуляторы на Java, чем я хуже?

Правильная позиция. Сможешь, если захочешь, дело времени.
Самое ужасное, я считаю, это то, что эмулятор, очень сложно проверить по частям, в отличии от программы.
Какой-нибудь косяк — и у тебя игра не запускается.
Единственный видимый мною, как не эмуляторщика, метод борьбы с этим: написание своей, так скажем «игры»… хоть это и не игра, а краш-тест.
Но наверно стоит для начала поискать уже готовые эмуляторы, или эмуляторы процессоров которые идут в составе системы, и не только процессоров…
А то один только процессор эмулировать уже задница — много очень опкодов реализовывать, да и чтобы была должная скорость.
Тут только можно посоветовать удачи, и не сдаваться ) Уж очень много народу сдуваются, хотя может быть оно и к лучшему.


Но наверно стоит для начала поискать уже готовые эмуляторы, или эмуляторы процессоров которые идут в составе системы, и не только процессоров…

Даж создатель генса, писал эмуль это на ядре, которое писал не он, только правил.  Starscream 680×0 вот это.


CrazyMax, год на C/C++, несколько лет на Pascal, Около полугода на Java…  <_<

паскаль в школе. И первый год в вузе на с++ — сейм стори, давай вместе напишем!
ах да еще лого знаю.
Данный пост, несет чисто саркастический характер

« Последнее редактирование: 21 Июнь 2011, 00:38:52 от Insane »


ilua_great, Как успехи в написании эмулятора?


Написание эмулятора — большой опыт.
Если найти исходники любого эмулятора, а потом почитать их, зная естественно как обстоит программирование, ты понимаешь как эмулятор устроен. Непонятная строка? Ты знаешь саму команду, но что она тут выполняет? Загугли! Когда поймешь, разберешся в коде эмулятора, можешь уже читать устройство самого процессора. А то так начинаешь читать и ниче не понимаешь и зачем тебе это надо.  А так хотя-бы понимать, где это в эмуле надо будешь.
Жаба — не очень для эмуляторов. Сильно грузит. Тот же случай — игра Minecraft. Лучше C или ASM.
Я кончено в эмуляции сам не особо понимаю, но разбираюсь в программировании и в основном так и учусь- почитал, разобрался в VB и дальше уже так пошло поехало. Доучился до Managed DirectX.


akip,
теперь стоит найти словарь интернет-сленга и прочитать про «некропостинг».


Yoti,  это уже стало модным поднимать старые темы. :)
А по созданию эмулятора, да это не то слово как нелёгкое дело нормальный сделать. Ну можно на худой конец хотя бы приемлемый сделать с готовых исходников.


Ну можно на худой конец хотя бы приемлемый сделать с готовых исходников.

И как же после этого можно будет называть эмулятор «твоим»? Занятие формошлёпством да дописывания эмулятору возможности пищать (beep) при старте игры это не «Написать эмулятор NES самому».
Прочитал всю тему и стала любопытна точка зрения HoRRoR. Вот как же он и правда собрался направлять на писанину эмулятора на java без знаний у человека о работе процессора, оно то можно не зная как что работает использовать чужую реализацию и получить результат, но где он NESAPI брать собрался, в котором уже всё будет сделано, а ты только бери ползуйся, не понятно  :)
Я вообще  смутно представляю как на этой java можно что-то написать полезное, она вроде и распространена и как-то люди на ней что-то пишут, но мне лично совсем неуютно после с++ отказаться от идеи указателей и ссылок. Ещё сборщик мусора гуано у этой явы, и ведь нельзя же его поторопить указав ему «ану освободи мне места быстра бл!», можно сделать указатель на NULL и любоваться засоренной памятью мусором находящимся в n поколении и твоим мега приложением которое нифига не делает и на форме одна кнопка, но ему столь необходимо 50 мегабайт оп. Ну о layout manager’ах для всех этих формочек я умолчу, для эмулятора этот шаг можно было бы пропустить (кроме создания менюшки).
Автор темы что-то долго молчит, даже больше не спрашивал ничего, наверное написание его лично го эмулятора java закончилось созданием диалогового окошка «О авторе эмулятора» да изменения названия формы на «JABA NES emulator», ну может ещё менюшку открыть файл/закрыть файл прикрутил. Но боюсь он сейчас уже не признается прав я или нет.


Страницы: [1] 2 3 Далее  Все   Вверх

layout title grand toc

post

Writing a NES/Famicom Emulator in C++

true

true

Introduction

So you want to write an NES emulator? The usual question is; «Why write yet another emulator when there are so many allready out there?». My own answer, and I think many would agree; it’s fun and something I always wanted to do ever since I started programming.

Writing an emulator can seem like a enormous task, and figuring out where and how to start can be a bit daunting. There are a lot of information out there, the problem is that it’s quite scattered and a bit hard to «just» dive into. The main reason for this article is to give something back to the emulation community and help anyone just starting out with their own NES emulator.
The information written here can probably be applied to other architectures than the NES, and generalized to other languages than C/C++.

This document is written as a rough guide, not a true tutorial on how I implemented a NES emulator. This means you will not have a fully working emulator by just using the code examples in this article, you will however have a rough idea how to implement one and where to start on your own. You will encounter problems along the way that I and other people already have run into, this should serve as a good point to start searching for solutions.

I do not claim this is the best or easiest way to implement an emulator, but this is how I did it. Now, lets get started!

Please note: Before going further, you should have basic knowledge of how a CPU works.

Emulating NES Architecture

I’m going to assume you allready have skimmed through one or two NES documentations before coming here. So, you probably have at least a vague idea what the NES hardware consist of. There are a lot of information regarding emulation in general on the internet, as well as in depth information on the NES architecture. I will try to organize a collection of relevant links at the bottom of this article as reference. For the rest of this section I’ll try to give a concice description of the hardware and its different units, then we will start to implementing each part in the subsequent sections.

Generally speaking there are two different ways to implement an emulator, either as an interpreter or using some type of dynamic recompilation. This guide will talk about the easiest of the two, namely as an interpreter.

The NES has a CPU chip called 2A03 (2A07 on the PAL versions) which is based on a 8bit 6502 processor, with some additional memory-mapped I/O-registers. An additional chip called the Picture Processing Unit (PPU, also called 2C02), sits in the NES and is responsible for, as the name suggest, all graphical operations.

These two chips are quite decoupled and can thus be implemented seperately as two different functions. The benefit of this is that the CPU can be implemented first without any knowledge of how the PPU works, which means the first thing we need to do is «simply» to implement a 6502 CPU simulator.

The NES has 2KB internal RAM, and as previously stated, has something called memory-mapped I/O-registers. This means the CPU does not have specific op codes to access I/O devices, but instead these are hardwired as memory adresses. For example, the PPU and CPU communicate with eachother using such adresses.

Game cartridges that are inserted into the NES also contain memory that is mapped into a special space of the memory adresses. It’s this memory that contains the actual game logic. This special memory is called the «Program ROM», or PRG ROM for short. Some game cartridges even have built in, battery powered, RAM used for game saves. The adress space where the PRG ROM and RAM is mapped is however limited, and special units on some cartridges, called memory mappers, is used to swap in and out chunks of a larger memory into this limited space.

** TODO ** write about CHR ROM/RAM and how it is connected to the PPU

** TODO ** write about the iNES format

So, now lets start describing our NES hardware in code, our entry point will have to look something like this:
{% highlight c++ %}
void run_emulator( const char* _rom_path )
{
// reset flags, registers and memory
boot_nes();

// read a NES ROM from path
iNES_ROM rom = load_rom( _rom_path );

while (true)
{
	tick_cpu( rom );
	tick_ppu( rom );
}

}
{% endhighlight %}
Simple stuff, right? Now we only have to implement these functions, and we should end up with a fully functioning emulator. Just one step at a time…

Memory

  • TODO memory layout

Booting/Reset

  • TODO First of all, we need to emulated how the hardware boots up. This means reseting all internal flags and registers to their default values.
    {% highlight c++ %}
    void boot_nes()
    {
    // …
    }
    {% endhighlight %}

  • TODO iNES — Cartridge/ROM format used by most NES emulators.
    {% highlight c++ %}
    iNES_ROM load_rom( const char* _rom_path )
    {
    iNES_ROM rom;

    // …

    return rom;
    }
    {% endhighlight %}

Implementing the CPU

  • TODO cpu loop etc
    {% highlight c++ %}
    void tick_cpu( iNES_ROM &_rom )
    {
    // fetch op code
    // todo

    // …

    switch (op_code)
    {
    case OPCODE_LDA:
    // do stuff
    break;

      default:
      	// unknown or unimplemented op code!
      break;
    

    };
    }
    {% endhighlight %}

  • TODO validation To validate that we have a working simulator, some great people have created some test-ROMs that only test the CPU capabilities and its OP codes.

  • TODO op codes

  • TODO memory access

Unsupported Opcodes

  • TODO make a list of diffuculties etc

Implementing the PPU

  • TODO where to start, making the ppu-tick
    {% highlight c++ %}
    void tick_ppu( iNES_ROM &_rom )
    {
    // …
    }
    {% endhighlight %}

  • TODO memory areas and data chunks

Implementing the APU

  • TODO no idea

Miscellaneous

  • TODO debugging using nestest.nes
  • TODO accuracy, counting clock cycles

Additional Information

  • NesDev Wiki — One of the greatest sources for NES development, both ROM programming and system architecture information.
  • Blargg’s 6502 Emulation Notes — Blargg is well known in emulator circles, here are some of his own tips for writing an emulator.
  • NESTech — Good overview of the NES hardware; CPU, PPU and memory info.
  • Emulating the Nintendo Entertainment System by Rupert Shuttleworth — A report, with source code, for a computer class. Goes into depth on how he implemented his own NES emulator, a very good read.
  • How To Write a Computer Emulator by Marat Fayzullin — An emulation introduction guide written by Marat of iNES fame.
  • nes-test-roms — Collection of test ROMs for testing your NES emulator.

Changes

  • 2013-12-05 — Initial draft
layout title grand toc

post

Writing a NES/Famicom Emulator in C++

true

true

Introduction

So you want to write an NES emulator? The usual question is; «Why write yet another emulator when there are so many allready out there?». My own answer, and I think many would agree; it’s fun and something I always wanted to do ever since I started programming.

Writing an emulator can seem like a enormous task, and figuring out where and how to start can be a bit daunting. There are a lot of information out there, the problem is that it’s quite scattered and a bit hard to «just» dive into. The main reason for this article is to give something back to the emulation community and help anyone just starting out with their own NES emulator.
The information written here can probably be applied to other architectures than the NES, and generalized to other languages than C/C++.

This document is written as a rough guide, not a true tutorial on how I implemented a NES emulator. This means you will not have a fully working emulator by just using the code examples in this article, you will however have a rough idea how to implement one and where to start on your own. You will encounter problems along the way that I and other people already have run into, this should serve as a good point to start searching for solutions.

I do not claim this is the best or easiest way to implement an emulator, but this is how I did it. Now, lets get started!

Please note: Before going further, you should have basic knowledge of how a CPU works.

Emulating NES Architecture

I’m going to assume you allready have skimmed through one or two NES documentations before coming here. So, you probably have at least a vague idea what the NES hardware consist of. There are a lot of information regarding emulation in general on the internet, as well as in depth information on the NES architecture. I will try to organize a collection of relevant links at the bottom of this article as reference. For the rest of this section I’ll try to give a concice description of the hardware and its different units, then we will start to implementing each part in the subsequent sections.

Generally speaking there are two different ways to implement an emulator, either as an interpreter or using some type of dynamic recompilation. This guide will talk about the easiest of the two, namely as an interpreter.

The NES has a CPU chip called 2A03 (2A07 on the PAL versions) which is based on a 8bit 6502 processor, with some additional memory-mapped I/O-registers. An additional chip called the Picture Processing Unit (PPU, also called 2C02), sits in the NES and is responsible for, as the name suggest, all graphical operations.

These two chips are quite decoupled and can thus be implemented seperately as two different functions. The benefit of this is that the CPU can be implemented first without any knowledge of how the PPU works, which means the first thing we need to do is «simply» to implement a 6502 CPU simulator.

The NES has 2KB internal RAM, and as previously stated, has something called memory-mapped I/O-registers. This means the CPU does not have specific op codes to access I/O devices, but instead these are hardwired as memory adresses. For example, the PPU and CPU communicate with eachother using such adresses.

Game cartridges that are inserted into the NES also contain memory that is mapped into a special space of the memory adresses. It’s this memory that contains the actual game logic. This special memory is called the «Program ROM», or PRG ROM for short. Some game cartridges even have built in, battery powered, RAM used for game saves. The adress space where the PRG ROM and RAM is mapped is however limited, and special units on some cartridges, called memory mappers, is used to swap in and out chunks of a larger memory into this limited space.

** TODO ** write about CHR ROM/RAM and how it is connected to the PPU

** TODO ** write about the iNES format

So, now lets start describing our NES hardware in code, our entry point will have to look something like this:
{% highlight c++ %}
void run_emulator( const char* _rom_path )
{
// reset flags, registers and memory
boot_nes();

// read a NES ROM from path
iNES_ROM rom = load_rom( _rom_path );

while (true)
{
	tick_cpu( rom );
	tick_ppu( rom );
}

}
{% endhighlight %}
Simple stuff, right? Now we only have to implement these functions, and we should end up with a fully functioning emulator. Just one step at a time…

Memory

  • TODO memory layout

Booting/Reset

  • TODO First of all, we need to emulated how the hardware boots up. This means reseting all internal flags and registers to their default values.
    {% highlight c++ %}
    void boot_nes()
    {
    // …
    }
    {% endhighlight %}

  • TODO iNES — Cartridge/ROM format used by most NES emulators.
    {% highlight c++ %}
    iNES_ROM load_rom( const char* _rom_path )
    {
    iNES_ROM rom;

    // …

    return rom;
    }
    {% endhighlight %}

Implementing the CPU

  • TODO cpu loop etc
    {% highlight c++ %}
    void tick_cpu( iNES_ROM &_rom )
    {
    // fetch op code
    // todo

    // …

    switch (op_code)
    {
    case OPCODE_LDA:
    // do stuff
    break;

      default:
      	// unknown or unimplemented op code!
      break;
    

    };
    }
    {% endhighlight %}

  • TODO validation To validate that we have a working simulator, some great people have created some test-ROMs that only test the CPU capabilities and its OP codes.

  • TODO op codes

  • TODO memory access

Unsupported Opcodes

  • TODO make a list of diffuculties etc

Implementing the PPU

  • TODO where to start, making the ppu-tick
    {% highlight c++ %}
    void tick_ppu( iNES_ROM &_rom )
    {
    // …
    }
    {% endhighlight %}

  • TODO memory areas and data chunks

Implementing the APU

  • TODO no idea

Miscellaneous

  • TODO debugging using nestest.nes
  • TODO accuracy, counting clock cycles

Additional Information

  • NesDev Wiki — One of the greatest sources for NES development, both ROM programming and system architecture information.
  • Blargg’s 6502 Emulation Notes — Blargg is well known in emulator circles, here are some of his own tips for writing an emulator.
  • NESTech — Good overview of the NES hardware; CPU, PPU and memory info.
  • Emulating the Nintendo Entertainment System by Rupert Shuttleworth — A report, with source code, for a computer class. Goes into depth on how he implemented his own NES emulator, a very good read.
  • How To Write a Computer Emulator by Marat Fayzullin — An emulation introduction guide written by Marat of iNES fame.
  • nes-test-roms — Collection of test ROMs for testing your NES emulator.

Changes

  • 2013-12-05 — Initial draft

01 Jan 2018 in NES emulation series / Emulator

How to write your own NES emulator — overview

I’ve recently wrote a NES Emulator NesChan. I had a lot of fun building this emulator and is planning to write a series of post documenting my experience and maybe help anyone who also want to write one. This is the first post.

NESChan mac version

Many people growing up in the 80s probably have fond memory of playing 8-bit NES games — those games are surprisingly fun (and many of them challenging). Those 8-bit style games are also making a come back in indie games such as Shovel Knight and Stardew Valley.

This post is intended as a high-level overview of what it would take to write a NES emulator so that if you are interested to write your own you’d be prepared to know what is involved, or simply just to understand some of the interesting problems involved in writing one. More detailed technical writeup of emulating different components would come later as separate posts.

NES Hardware Capabilities

Being a 8-bit system in 80s, it has hilariously limited spec comparing to today’s standard:

  • 8-bit 6502 CPU running at 1.79 MHZ. It has 3 general purpose register A/X/Y, and 3 special register P (status) /SP (stack pointer) /PC (program counter, or instruction pointer), all of them being 8-bit except PC which is 16-bit.

  • 16-bit addressable memory space. In theory it can address 64K memory however it only has 2KB onboard RAM. Rest is either not wired up (and are mirrors of those 2KB), or mapped to special I/O registers, or catridge ROM/RAM space.

  • PPU (Picture Processing Unit) supporting rendering 256×240 screen composed of 8×8 tiles for background, up to 64 8×8 or 8×16 sprites for moving objects. It supports pixel-level scrolling (which is a big deal back in that day).

  • APU (Audio Processing Unit) supporting 2 pulse channel, 1 triangle channel, 1 noise channel, and 1 DMC (delta modulation) channel. One can still make good music with these — just not great sound effects.

  • Controllers — from classic NES controller to NES mouse.

  • Catridge boards (and mappers) — there are many different kinds of game catridge boards. They come with game data as ROMsm, sometimes their own battery-backed RAM, or some cases, their own audio processing unit. Most importantly, they also come with special hardware, refered to as mappers, that dynamically maps ROM/RAM into CPU and PPU memory space, bypassing the limitation of 16-bit address space. Some game catridge come with more than 256KB of CHR ROM and swap/map portion of it on demand.

Before your start

Assuming you haven’t done NES programming on a real NES hardware before, there are a bundance of material that covers NES hardware behavior.

You need to have a good understanding of following topics:

  • CPU — instructions, addressing modes, registers and status flags, interrupts
  • PPU — PPU registers, pattern table, name table, sprites, rendering pipeline, and scrolling
  • APU — APU registers, and how to generate square/triangle waves.
  • iNES format — most games are in this format.
  • Controller — controller register
  • Mappers — how mappers controls memory mapping. Different mappers have different capabilities.

It took me about a week and half to add CPU, PPU, and a few mappers in order to get some of the major commercial games to work perfectly (Super Contra, Super Mario Bros, Shadow of the Ninja, etc). If you want to support most of the games out there, prepare for a lot of work (implementing mappers and debugging). But most of those work are incremental and you can decide to stop at any time.

Which language/framework to pick

Language choice probably doesn’t matter that much. People have written NES emulators using all kinds of languages — C/C++, JavaScript, Go, C#, etc. Just pick your favorite language and go. It’s a 8-bit processor, so emulation performance on today’s powerful machines is usually not an issue. Just don’t go crazy creating new objects and trigger GC, if you are using a language that has one.

Do find a good library for your language that supports rendering 2D graphics, controllers, and audio. You can choose a cross-platform one, or work with the OS specific libraries.

For me I decided to go with C++, and SDL for rendering/input/audio. This has nice benefit that everything is cross-platform by default.

The core game engine is its own library and is agnostic about which framework your choose, and the main app uses SDL to provide the rendering/input/audio capabilities and can be swapped to use whatever technology/framework that is appropriate for the platform. For example, you can use a JavaScript framework in the browser to interact with the C++ game engine, and do the rendering / input entirely in the browser.

Have a plan

Before you actually go write the emulator, it’ll be good to have a plan of attack — which component to emulate first and what to test, etc. It’s definitely not a good idea to run Super Mario Bro as your first test.

This is the plan I went with.

1. Start from CPU first. And make sure it’s really solid.

Duh. You can’t do anything without CPU. Just go implement the instructions and the main loop — stick to the official instructions first. There are very few games use unofficial/undocumented instructions.

2. Add NES rom support (and mapper 0)

Why add ROM support before you can actually render anything to screen? You want to use test roms to make sure your CPU simulation is really good and squash bugs as early as possible. This will save you a ton of time.

Trust me — you don’t want to find your CPU bugs in real games. And you’ll als catch your own regressions. Many ROMs would automatically write a success/fail code at a well known location — this can be your check/asserts, depending on what test framework you use.

And eventually you’d want to load some games, right? :)

If you find that some test ROM need unofficial instructions, add them as needed.

3. Then go implement your PPU.

This is probably going to be fairly involved if not challenging. Even for experienced programmers, the PPU rendering pipeline takes quite a bit of time to wrap one’s head around. Due to memory constraints, the rendering tiles/sprites is completely 8×8 tile (block) based, and the way those tiles are represented in memory takes a bit getting used too (bitplanes, etc), and implementing the precise rendering pipeline has a lot of details.

Don’t worry about rendering to screen yet. Just observe your VRAM and see with your inner eye to imagine.

Don’t add scroll. That should be the next step.

Now you can add the rendering and the main game loop. Make sure your game loop process as the exact cycle as the real hardware by doing some math over elapsed tick count and CPU MHZ.

4. Go try some simple games

Try some games that don’t scroll and use mapper 0. Donkey Kong/Popeye/balloon fight are solid choices. You want to focus on those simple ones before you attempt your favorite games.

5. Add scrolling

Scrolling is tricky because you need to locate the exact pixel within 8×8 tile, and you’ll also render one more tile if the X scroll isn’t a factor of 8. Go read the scrolling doc and make sure all the interaction between PPU register, PPUADDR and scrolling parameters are done exactly right.

6. Test a scrolling game

Ice Climber is a great choice for Y scroll. Horizontal scrolling games are trickier and should be tested in step 7.

7. Try out Super Mario Bros.

Once quite a few simple mapper 0 game work great, now it’s time to take your emulator to a real test. Super Mario Bros isn’t particularly tricky, but is quite demanding that your emulator should have a fairly complete CPU and PPU emulation with reasonable accuracy.

8. Add APU support

I haven’t finished this one personally. Will update once I got it working. To get this to work you need to understand square waves, triangle waves, etc. Audio programming was black magic to me but I’m starting to get it.

9. Add more mappers (MMC1, MMC3, etc)

More mappers = more games. In general the first few mappers are great candidates as it is supported by most games.

10. Try a real tricky game to emulate

Battletoads, if you are up for a challenge. Or there are more to choose from.

11. Add fancy features

At this point, you should have many games working. Now you can decide what matter most to your emulator — there are a lot of things you can do now:

  • Add a debugger support
  • Add load/save state
  • Add reverse/forward
  • Add cheating support
  • Add more games and fix games
  • Add more test roms

When things go wrong

This is when things get really interesting (and frustrating).

There are a few strategies that I found really helpful:

  • Use logs — your emulator should support writing diagnostic logs — including CPU instructions getting executed, what cycle is it at, scanline start/end, whether there is an interrupt, etc. In many cases comparing logs with your emulator and some other emulator would immediately identify the problem with CPU bugs, timing issues, freezes.

  • Compare with a excellent NES emulator with great debugger support. Mesen’s excellent debugger makes debugging much easier. Debugging the code and observe what it does, compare the cycles, looking at memory, and tile/sprites data in PPU. This is extremely helpful.

  • If you have time, build your own debugger for your emulator. If you can directly see the contents of your sprites/background tiles represented in your own debugger window, you can easily see whether the problem is happening — is it a rendering bug (the data is correct), or the data simply isn’t there (most likely timing issue, CPU bug, or mappper bug).

All in all,dDebugging is pretty painful, so avoid it if you can. This means having really good tests using the test roms and automate them. So that you can know if you made a regression after every build.

What’s next

In my next post, I’m going to talk about how a emulator works when it comes up emulating multiple parallel executing hardware components. After that we’ll dive into details of how to emulate CPU/PPU, with real working code.

You can find my emulator here in github — neschan.

The series so far…

  • Part 1 — NES Emulator Overview
  • Part 2 — Writing the main loop
  • Part 3 — Emulating the 6502 CPU

If you are hungry for more NES…

Head to NESDev Wiki — I’ve learned pretty much everything about NES there. There is also a great book on NES called I am error, which is surprisingly deeply technical for a book about history of NES.

Понравилась статья? Поделить с друзьями:
  • Как написать эмоциональный текст
  • Как написать эмодзи на компьютере
  • Как написать эмодзи на клавиатуре виндовс 10
  • Как написать эмоджи я тебя люблю
  • Как написать эмоджи на компьютере