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

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

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

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

© 2006–2023, Habr

Эмулятор процессора 1804

Build Status
codecov

При запуске требуется передать путь к настроечному файлу, который лежит в resources/settings.cfg.

Требования:

  • JDK 11
  • опционально плагин lombok (для подсветки генерируемого перед компиляцией кода)

Некоторые особенности работы процессора

  • Флаги результатов доступны на следующем такте;
  • При выполнении условного перехода, флаги блокируются (не меняются);
  • Флаги меняются при выполнении арифметических (OVR, F3, C4, Z) и логических операций (OVR, F3, Z);
  • Сдвиги не меняют флаги;
  • При не выполнении условия в условных переходах, выполняется переход на следующий адрес (например, c 0 на 1);
  • Арифметический сдвиг 8-разрядного слова влево реализован с ошибкой (ввод 1 вместо 0).

Правила записи программы

Программа записывается в виде символов 0, 1, x. Все символы x будут заменены на 0.
Длина команды должна составлять 36 символов. Для написания коммантария нужно начать строку с символа #. Разрешены
только однострочные комментарии. Чтобы видеть комментарий в выводе программы, нужно написать ## перед комментарием.
Написанный таким образом комментарий привязывается к команде, которая следует за ним. Если будет указано несколько однострочных комментарив для передачи на вывод, будет выбран самый последний из них.

Формат команды микропроцессора 1804 можно посмотреть в wiki этого проекта.

Настроечный файл

Настроечный файл содержит следующие поля:

  1. input — файл с программой;
  2. output — файл для записи информации о состоянии регистров;
  3. writer — файл настройки печати;
  4. help — файл с информацией о командах эмулятора;
  5. welcome — файл с сообщением приветсвия.

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

Файл настройки вывода

Файл writer содержит поле line, которое определяет формат выводимой в файл и в консоль информации.
Все необходимые поля нужно указывать разделяя их символом :. Список доступных полей можно посмотреть в файле PrintableValue.java. Они соответсвуют элементам перечисления.

Выдержка из PrintableValue.java

public enum PrintableValue {
    COMMENT, N, Y, F, Q, NEXT, ADDR, FLAGS, OVR, C4, F3, Z,
    REGS, REG0, REG1, REG2, REG3, REG4, REG5, REG6, REG7, 
    REG8, REG9, REG10, REG11, REG12, REG13, REG14, REG15,
    STACKS, STACK0, STACK1, STACK2, STACK3,
    SP
}

Все названия говорящие, поэтому можно легко сориентироваться в них и настроить вывод нужным образом, но некоторые могут потребовать дополнительного объяснения. N — номер такта процессора, после которого были получены значения, NEXT — адрес следующей команды, ADDR — текущий адрес. Поле COMMENT выводить комментарии из входного файла с программой. Также стоить обратить внимание на то, что поля REGS, STACKS, FLAGS не могут быть использованы с их более специфичными формами: REG0REG15, STACK0STACK4, OVR, F3, C4, Z. Дублировать поля тоже запрещено.

Пример файла настройки вывода:

line = N:ADDR:NEXT:Y:F:Q:REG0:COMMENT

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

Команды эмулятора

После того как эмулятор будет запущен вы получите сообщение приветсвия и информацию о командах.
После чего последует приглашение на ввод команды:

На данный момент доступны следующие команды:

  1. cclk [NUM] — выполнить 1 так (или NUM тактов, если указано) процессора;
  2. sstate — вывести текущее состояние;
  3. shstateHistory — вывести все состояния процессора;
  4. hhelp — вывести информационное сообщение;
  5. eexit — завершить работу и записать всю историю состояний в выходной файл.

Пример работы

Пусть у нас есть следующая программа:

input/gray2bin.txt

# Этот комментарий будет проигнорирован
# Код Грея в двоичный
# b = {REG0, Q}, result g = {REG1, REG2}
## Загрузка 1000 в REG0
xxxx xxxx 0010 x011 x111 x011 xxxx 0000 1000
## Загрузка 0000 в Q
xxxx xxxx 0010 x000 x111 x011 xxxx xxxx 0000
## 	Загрузка 0000 в REG1
xxxx xxxx 0010 x011 x111 x011 xxxx 0001 0000
## 	Загрузка 0000 в REG2
xxxx xxxx 0010 x011 x111 x011 xxxx 0010 0000
#4 	Начинаем цикл
## 	Сделать REG2 = (REG2 xor Q)
xxxx xxxx 0010 x011 x000 x110 0010 0010 xxxx
## Сделать REG1 = (REG1 xor REG0)
xxxx xxxx 0010 x011 x001 x110 0000 0001 xxxx
## Циклический сдвиг вправо {REG0, Q} / 2-> {REG0, Q}
xxxx xxxx 0010 1100 0011 x011 xxxx 0000 xxxx
## REG0 = REG0 & 0111 (маска), дальше
xxxx xxxx 0010 x011 x101 x100 0000 0000 0111
#8 Выполняем REG0 or Q чтобы узнать ноль или нет
## REG0 or Q, don't write, just for Z flag
xxxx xxxx 0010 x001 x000 x011 0000 xxxx xxxx
## Перейти в 4, если Z = 0
0000 0100 0000 x001 x000 x011 0000 xxxx xxxx
#10
## Y = REG0
xxxx xxxx 0010 x011 x011 x011 xxxx 0000 xxxx
#11
## Y = REG1, перейти в 10
0000 1010 0001 x011 x011 x011 xxxx 0001 xxxx

Настроечный файл вывода выглядит следующим образом:

line = N:ADDR:NEXT:Y:F:REG0:REG3:REG2:REG1:Q:COMMENT

Запустим эмулятор передав ему путь к настроечному файлу: resources/settings.cfg. Если вы не знаете как в Intelij IDEA передать аргументы программе, то мне вас очень жаль.
После запуска будет получено сообщение с приветсвием.
Введем сразу s для вывода текущего состояния регистров процессора.

command: s
<--- state
+----+----+----+----+----+-----+-----+-----+-----+----+-------------------------------------------+
| clk|addr|next|   Y|   F| REG0| REG3| REG2| REG1|   Q|comment                                    |
+----+----+----+----+----+-----+-----+-----+-----+----+-------------------------------------------+
|   0|   0|   0|0000|0000| 0000| 0000| 0000| 0000|0000|                                           |
+----+----+----+----+----+-----+-----+-----+-----+----+-------------------------------------------+
command: 

Так как ни одного такта еще не было, и мы даже не начали читать команды, то все значения будут нулевыми.

Сделаем теперь один такт процессора и выведем новое состояние.

command: c
<--- clk
command: s
<--- state
+----+----+----+----+----+-----+-----+-----+-----+----+-------------------------------------------+
| clk|addr|next|   Y|   F| REG0| REG3| REG2| REG1|   Q|comment                                    |
+----+----+----+----+----+-----+-----+-----+-----+----+-------------------------------------------+
|   1|   0|   1|0011|0011| 0011| 0000| 0000| 0000|0000|                                           |
+----+----+----+----+----+-----+-----+-----+-----+----+-------------------------------------------+
command: 

Сделаем еще 5 тактов и выведем всю историю состояний.

command: c 5
<--- clk 5 times
command: sh
<--- stateHistory
+----+----+----+----+----+-----+-----+-----+-----+----+-------------------------------------------+
| clk|addr|next|   Y|   F| REG0| REG3| REG2| REG1|   Q|comment                                    |
+----+----+----+----+----+-----+-----+-----+-----+----+-------------------------------------------+
|   0|   0|   0|0000|0000| 0000| 0000| 0000| 0000|0000|                                           |
|   1|   0|   1|0011|0011| 0011| 0000| 0000| 0000|0000|Загрузка 0010 в REG0                       |
|   2|   1|   2|0000|0000| 0011| 0000| 0000| 0000|0000|Загрузка 1111 в Q                          |
|   3|   2|   3|0000|0000| 0011| 0000| 0000| 0000|0000|Загрузка 0000 в REG1                       |
|   4|   3|   4|0000|0000| 0011| 0000| 0000| 0000|0000|Загрузка 0000 в REG2                       |
|   5|   4|   5|0000|0000| 0011| 0000| 0000| 0000|0000|Сделать REG2 = !(REG2 xor Q)               |
|   6|   5|   6|0011|0011| 0011| 0000| 0000| 0011|0000|Сделать REG1 = !(REG1 xor REG0)            |
+----+----+----+----+----+-----+-----+-----+-----+----+-------------------------------------------+
command: 

Для завершения работы введем команду e после чего все состояния процессора на всех тактах его работы будут
записаны в файл, указанный в resources/settings.cfg.

Таким образом работает эмулятор. Обо всех багах прошу сообщать незамедлительно. Пишите issues, а лучше
найдите причину ошибки, избавьтесь от нее и сделайте pull request.

Unit тесты:

  • AbstractRegister
  • Register8
  • Register4
  • Register3
  • Register
  • Command
  • Flags
  • AddressUnit
  • ALU
  • ControlUnit
  • MuxInputData
  • ProgramMemory
  • RegisterMemoryUnit
  • RegQUnit
  • Shifter
  • Stack
  • Processor
  • ProcState


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

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

Только хардкор

. Однако язык будет условным, как ARM condition language. Раз он условный, нужно вырабатывать условия, пусть это будут делать сравнения.

Теперь простейший пример программы на языке:

al mov r00, #0

al cmp r00, #400

av return #0

le mul r01, r00, r00
le swi #5
le inc r00
le mov r14, #1 

Разбор кода:
al mov r00, #0 — в любом случае r00 = 0
al cmp r00, #400 — в любом случае сравним r00 и 400
av return 0 — если r00 больше, завершаем выполнение с кодом 0
le mul r01, r00, r00 — если r00 меньше или равно, r01 = r00 * r00
le swi #5 — если r00 меньше или равно, закрашиваем пиксель(r00, r01)
le inc r00 — если r00 меньше или равно, увеличим r00 на 1.
le mov r14, #1 — если r00 меньше или равно, счетчик программы = 1

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

Заголовочный файл:

#ifndef PARSER_H
#define PARSER_H

struct operation {
	char cond[32]; // Условие выполнения
	char cmd[32];  // Сама команда
	char arg0[32]; // Первый аргумент
	char arg1[32]; // Второй аргумент
	char arg2[32]; // Третий агрумент
};

void dump_operation(struct operation *op); // Вывод данных операции
struct operation *parse_code(const char *code); // Парсит строку в общее представление кода

#endif // PARSER_H

Исходный файл:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>

#include "parser.h"

#define SON(x) (((x) && (*(x) != ''))?(x):"NONE") //Если строка пуста или указатель нулевой,  то выдаем NONE (для dump_operation)

void dump_operation(struct operation *op)
{
	if (!op) { // Если дан неверный указатель
		printf("OP: NULLn");
		return;
	}

        // Если указатель в порядке
	printf("COND: "%s"nOP: "%s"nARGS: "%s", "%s", "%s"nn",
               SON(op->cond), SON(op->cmd), SON(op->arg0),
               SON(op->arg1), SON(op->arg2));
}

static inline const char *skip(const char *line, char *skips) // Пропуск строки в строке
{
	line += strlen(skips); // Увеличим указатель на пропарсиваемую строку на длину пропускаемой
	while ((*line == ' ') || (*line == ',')) // Пропустим ненужные символы
		line++;

	if ((*line == '') || (*line == 'n') || (*line == ';')) // Если конец строки 
		return 0; // Вернем нулевой указатель

	return line; // Иначе вернем полученный указатель
}

static inline void str_to_lower(char *str) // Переводит строку в нижний регистр
{
	unsigned int l = strlen(str);
	unsigned int i = 0;

	for (i = 0; i < l; i++)
		str[i] = (char)tolower(str[i]);
}

struct operation *parse_code(const char *code)
{
	struct operation *op = (struct operation*)calloc(1, sizeof(*op)); // Выделим память под результат
	char *tokens[5] = {op->cond, op->cmd, op->arg0, op->arg1, op->arg2};
	unsigned int i = 0;

	for (i = 0; i < 5; i++) {
        	if (sscanf(code, "%[0-9a-zA-Z@#$]", tokens[i]) <= 0/*Получаем строку в текущий токен*/) { // Если встретили запрещенный символ
			fprintf(stderr, "Error!nUnknown symbol!n"); // Говорим об этом

			free((void *)op); // Освобождаем память
			return 0; // Возвращаем нулевой указатель
        	}

		str_to_lower(tokens[i]); // Переводим строку в нижний регистр

		code = skip(code, tokens[i]); // Пропускаем полученный токен

		if (!code) // Если код закончился
            		break; // Завершаем цикл
	}

	return op; // Возвращаем объектное представление кода
}

В следующей части расскажу об анализе кода и переводе его в байт-код.

Литература по теме
Э. Таненбаум Т.Остин Архитектура компьютера
А. Ахо, Р. Сети, Д. Ульман Компиляторы: принципы, технологии и инструменты

P.S народ, комментируйте, что не нравится. Постараюсь исправить. Заранее спасибо!

Chip-8

Автор провёл детство за играми в эмуляторах NES и SNES на своём компьютере, но никогда не думал, что однажды сам напишет эмулятор. Иван Сергеев поставил перед автором задачу написать интерпретатор Chip-8, чтобы изучить основные понятия низкоуровневых языков программирования и то, как работает процессор.

Результат — эмулятор Chip-8 на JavaScript, который автор написал под его руководством. Подробности рассказываем, пока у нас начинается курс по Fullstack-разработке на Python.


Хотя есть множество реализаций интерпретатора Chip-8 на всевозможных языках программирования, мой Chip8.js работает с тремя средами: это веб-приложение, приложение CLI и нативное приложение. Исходный код и демо:

  • Демо.
  • Исходный код.

Есть и множество руководств о том, как сделать эмулятор Chip-8, таких как Mastering Chip8, How to Write an Emulator и, самое главное — Cowgod’s Chip-8 Technical Reference, основной ресурс для моего собственного эмулятора, а ещё веб-сайт, настолько старый, что его адрес заканчивается .HTM.

Таким образом, это не руководство, но обзор того, как я создала эмулятор, какие основные концепции я изучила, и о кое-какой специфике JavaScript в смысле создания браузерного, нативного и CLI-приложения.

Что такое Chip-8


Я не слышала о Chip-8 до начала этого проекта, поэтому предполагаю, что большинство людей тоже не слышали, если не ладят с эмуляторами. Chip-8 — это очень простой интерпретируемый язык программирования, разработанный в 1970-х годах для любителей компьютеров.

Люди писали простые программы Chip-8, которые имитировали популярные игры того времени: Pong, Tetris, Space Invaders и, вероятно, другие игры, потерянные в анналах истории.

Виртуальная машина, которая играет в них, на самом деле технически является интерпретатором Chip-8, а не эмулятором, поскольку эмулятор — это программное обеспечение, эмулирующее аппаратное обеспечение конкретной машины, а программы Chip-8 не привязаны к какому-либо конкретному оборудованию. Интерпретаторы Chip-8 часто использовались на графических калькуляторах.

Тем не менее этот интерпретатор достаточно близок к эмулятору, поэтому с него обычно начинают те, кто хочет научиться создавать эмуляторы; это значительно проще, чем создавать эмулятор NES или чего-либо ещё. А ещё это хорошая отправная точка для изучения многих концепций процессора в целом, таких как память, стеки и ввод-вывод, с которыми я ежедневно имею дело в бесконечно более сложном мире среды выполнения JavaScript.

Что входит в интерпретатор Chip-8?


Мне пришлось пройти много предварительной подготовки, чтобы хотя бы начать понимать, с чем я работаю; я никогда раньше не изучала основы информатики. Поэтому написала статью Понимание битов, байтов, оснований и запись шестнадцатеричного дампа в JavaScript, где подробно рассказала об этом.Из неё можно сделать два основных вывода:

Общие сведения о битах и байтах

  • Биты и байты. Бит — это двоичная цифра — 0 или 1, истина или ложь, включено или выключено. Восемь бит — это байт, основная единица информации, с которой работают компьютеры.
  • Основания чисел. Десятичная система счисления является наиболее привычной для нас, но компьютеры обычно работают с двоичной (основание 2) или шестнадцатеричной (основание 16). 1111 в двоичной системе, 15 в десятичной и f в шестнадцатеричной — это одно и то же число.
  • Полубайты. Кроме того, 4 бита — это полубайт, что очень мило. Мне пришлось немного повозиться с ними.
  • Префиксы. В JS 0x — это префикс шестнадцатеричных чисел, 0b — это префикс двоичных чисел.

Я также написала Змейку в CLI, чтобы понять, как здесь работать с пикселями в терминале.

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

Память


Chip-8 может получить доступ к 4 Кб памяти ОЗУ. Это 0,002% от объёма памяти на дискете. Большая часть данных процессора хранится в памяти.

4 Кb — это 4096 байт, и JavaScript поддерживает полезные типизированные массивы, к примеру, Uint8Array с фиксированным размером элементов — здесь это 8 бит.

let memory = new Uint8Array(4096)

Вы можете получить доступ и использовать этот массив как обычный массив, от memory[0] до memory[4095], устанавливая элементы массива в значения до 255. Значения выше 255 преобразуются в 255.

Счётчик команд (PC)


Этот счётчик хранит адрес текущей инструкции в виде 16-битного целого числа. Каждая инструкция в Chip-8 обновляет PC, когда она завершена, чтобы перейти к следующей инструкции, обращаясь к инструкции по адресу, который записан в PC.

Что касается схемы размещения ячеек памяти Chip-8, 0x000 to 0x1FF зарезервировано, так что память начинается с адреса 0x200.

let PC = 0x200 // memory[PC] будет обращаться к адресу текущей инструкции

Вы заметите, что массив памяти 8-битный, а PC — 16-битное целое число, поэтому, чтобы получился опкод big endian, объединяются два программных кода.

Регистры


Память обычно используется для долгосрочного хранения и программирования данных, поэтому регистры существуют как своего рода «кратковременная память» для немедленного получения данных и вычислений. Chip-8 имеет 16 8-битных регистров, от V0 до VF.

let registers = new Uint8Array(16)

Индексный регистр


Существует специальный 16-битный регистр, который обращается к определённой точке в памяти, так называемый I. Регистр I существует в основном для чтения и записи в память, поскольку адресуемая память также 16-битная.

let I = 0

Стек


Chip-8 имеет возможность переходить в подпрограммы, а также в стек для отслеживания того, куда возвращаться. Стек имеет размер 16 16-битных значений: до «переполнение стека» программа может перейти в 16 вложенных подпрограмм.

let stack = new Uint16Array(16)

Указатель стека


Указатель стека (SP) — это 8-битное целое число, которое указывает на место в стеке. Он должен быть только 8-битным, хотя стек 16-битный. Поскольку указатель ссылается только на индекс стека, он должен иметь значения только от 0 до 15.

let SP = -1
// stack[SP] получит доступ к текущему адресу возврата в стеке.

Таймеры


Chip-8 способен издавать великолепный одиночный звуковой сигнал. Честно говоря, я не потрудилась реализовать реальный вывод «музыки», хотя сам процессор может с ней работать.

Есть два таймера, оба — 8-битные регистры: звуковой таймер (ST) для определения времени звукового сигнала и таймер задержки (DT) для определения времени некоторых событий в игре. Они отсчитывают время с частотой 60 Гц.

let DT = 0
let ST = 0

Ввод с клавиатуры


Chip-8 поставлялся вот с такой удивительной шестнадцатеричной клавиатурой:

┌───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ C │
│ 4 │ 5 │ 6 │ D │
│ 7 │ 8 │ 9 │ E │
│ A │ 0 │ B │ F │
└───┴───┴───┴───┘

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

Графический вывод


В Chip-8 используется монохромный дисплей с разрешением 64x32. Каждый пиксель либо включён, либо выключен.

Спрайты, которые можно сохранить в памяти, имеют размер 8x15 — восемь пикселей в ширину и пятнадцать — в высоту. Chip-8 также поставляется с набором шрифтов, но он содержит только символы шестнадцатеричной клавиатуры.

CPU


Сложите всё это вместе, и вы получите состояние процессора. Вот его класс:

class CPU {
  constructor() {
    this.memory = new Uint8Array(4096)
    this.registers = new Uint8Array(16)
    this.stack = new Uint16Array(16)
    this.ST = 0
    this.DT = 0
    this.I = 0
    this.SP = -1
    this.PC = 0x200
  }
}

Декодирование инструкций Chip-8


Chip-8 имеет 36 инструкций. Все инструкции перечислены здесь. Все инструкции имеют длину 2 байта (16 бит). Каждая инструкция кодируется опкодом (кодом операции) и операндом — данными, над которыми производится операция. Примером инструкции может быть такая операция:

x = 1.
y = 2.

ADD x, y,

где ADDопкод и x, y — операнды. Этот тип языка известен как язык ассемблера. Эта инструкция будет отображаться на:

x = x + y

При таком наборе инструкций мне придётся хранить эти данные в 16 битах, так что каждая инструкция будет представлять собой число от 0x0000 до 0xffffff. Каждая позиция разряда в этих наборах состоит из 4 битов.

Как же мне перейти от nnnn к чему-то вроде ADD x, y? Начну с инструкции, аналогичной примеру выше:

Есть одно ключевое слово, ADD, а ещё два установленных в регистрах аргумента, Vx и Vy. Также есть несколько мнемоник опкодов, похожих на ключевые слова:

  • ADD (сложение).
  • SUB (вычитание).
  • JP (переход).
  • SKP (пропуск).
  • RET (возврат).
  • LD (загрузка).

И несколько типов значений операндов, таких как:

  • Адрес (I).
  • Регистр (Vx, Vy).
  • Константа(N или NN для полубайта или байта).

Теперь нужно найти способ интерпретации 16-битного опкода как более понятных инструкций.

Битовые маски


Каждая инструкция содержит шаблон, и он всегда будет одним и тем же, и переменные, которые могут меняться. Для 8xy4 паттерном является 8__4. Два полубайта в середине — это переменные. Создав битовую маску для этого шаблона, я могу определить инструкцию.

Для маскирования используется побитовое AND (&) с маской и сопоставляется с шаблоном. Таким образом, если появится команда 8124, захочется гарантировать, что полубайт в позиции 1 и 4 включён (пропущен), а полубайт в позиции 2 и 3 выключен (замаскирован). И вот маска: f00f.

const opcode = 0x8124
const mask = 0xf00f
const pattern = 0x8004

const isMatch = (opcode & mask) === pattern // true
  8124
& f00f
  ====
  8004

Аналогично 0f00 м 00f0 будет маскировать переменные, а сдвигом вправо (>>) они получат доступ к нужному полубайту.

const x = (0x8124 & 0x0f00) >> 8 // 1

// (0x8124 & 0x0f00) is 100000000 in binary
// правый сдвиг на 8 (>> 8) удалит 8 нулей справа
// Останется 1

const y = (0x8124 & 0x00f0) >> 4 // 2
// (0x8124 & 0x00f0) — это 100000 в двоичном коде
// правый сдвиг на 4 (>> 4) удалит четыре нуля справа
// Останется 10, то есть двоичный эквивалент 2

Поэтому для каждой из 36 инструкций я создала объект с уникальным идентификатором, маской, шаблоном и аргументами.

const instruction = {
  id: 'ADD_VX_VY',
  name: 'ADD',
  mask: 0xf00f,
  pattern: 0x8004,
  arguments: [
    { mask: 0x0f00, shift: 8, type: 'R' },
    { mask: 0x00f0, shift: 4, type: 'R' },
  ],
}

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

Я создала массив INSTRUCTION_SET, содержащий все эти инструкции, а также написала дизассемблер и тесты, чтобы гарантировать, что все они работают правильно. Вот дизассемблер:

function disassemble(opcode) {
  // Ищем инструкцию исходя из байт-кода
  const instruction = INSTRUCTION_SET.find(
    (instruction) => (opcode & instruction.mask) === instruction.pattern
  )
  // Ищем аргументы
  const args = instruction.arguments.map(
    (arg) => (opcode & arg.mask) >> arg.shift
  )

  // Возвращает объект, содержащий инструкции и аргументы
  return { instruction, args }
}

Чтение ПЗУ


Поскольку мы рассматриваем этот проект как эмулятор, каждый программный файл Chip-8 можно считать ПЗУ. ПЗУ — это просто двоичные данные, а мы пишем программу для их интерпретации. Мы можем представить процессор Chip8 как виртуальную консоль, а ПЗУ Chip-8 как виртуальный картридж.

Буфер ПЗУ примет необработанный двоичный файл и преобразует его в 16-битные слова big endian (слово — это единица данных, состоящая из определённого количества битов). Вот где пригодится статья о шестнадцатеричном дампе. Я собираю двоичные данные и преобразую их в блоки, которые я могу использовать, в нашем случае 16-битные опкоды. Big endian означает, что старший байт будет первым в буфере, поэтому, когда он встретит два байта 12 34, он создаст 1234 16-битный код. Код с little endian выглядел бы так: 3412.

class RomBuffer {
  /**
   * @param {binary} fileContents ROM binary
   */
  constructor(fileContents) {
    this.data = []

    // Читаем сырые данные буфера из файла
    const buffer = fileContents

    // Создаём 16-битные опкоды big endian из буфера
    for (let i = 0; i < buffer.length; i += 2) {
      this.data.push((buffer[i] << 8) | (buffer[i + 1] << 0))
    }
  }
}

Возвращаемые из этого буфера данные — это и есть «игра».

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

Цикл выполнения инструкций — извлечение, декодирование, выполнение


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

  • Извлечение (fetch) — получение данных, хранящихся в памяти, при помощи счётчика программы.
  • Декодирование — разбор 16-битного опкода для получения декодированной инструкции и значений аргументов.
  • Выполнение — выполнение операции на основе декодированной инструкции и обновление счётчика программы.

Вот сжатая и упрощённая версия того, как работает цикл. Эти методы цикла ЦП являются частными и не раскрываются.

Первый шаг, fetch, обращается к текущему опкоду из памяти.

// Берём адрес из памяти
function fetch() {
  return memory[PC]
}

decode разберёт опкод на понятный набор команд:

// Декодируем инструкцию
function decode(opcode) {
  return disassemble(opcode)
}

Execute состоит из switch со всеми 36 инструкциями в качестве case, и выполнит для найденной инструкции соответствующую операцию, обновив затем счётчик программы, чтобы следующий цикл извлечения нашёл следующий опкод. Любая обработка ошибок будет проходить здесь же, что приведёт к остановке процессора.

// Выполняем инструкцию
function execute(instruction) {
  const { id, args } = instruction

  switch (id) {
    case 'ADD_VX_VY':
      // Выполняем операцию инструкции
      registers[args[0]] += registers[args[1]]

      // Обновляем счётчик
      PC = PC + 2
      break
    case 'SUB_VX_VY':
    // и т д.
  }
}

В итоге я получаю процессор со всеми состояниями и циклом команд. Есть два метода, открытые на CPU, — load — эквивалент загрузки картриджа в консоль с romBuffer в качестве игры, и step, который представляет собой три функции цикла инструкций (извлечение, декодирование, выполнение). step будет работать в бесконечном цикле.

class CPU {
  constructor() {
    this.memory = new Uint8Array(4096)
    this.registers = new Uint8Array(16)
    this.stack = new Uint16Array(16)
    this.ST = 0
    this.DT = 0
    this.I = 0
    this.SP = -1
    this.PC = 0x200
  }

  // Загружаем буфер в память
  load(romBuffer) {
    this.reset()

    romBuffer.forEach((opcode, i) => {
      this.memory[i] = opcode
    })
  }

  // Шаг по инструкциям
  step() {
    const opcode = this._fetch()
    const instruction = this._decode(opcode)

    this._execute(instruction)
  }

  _fetch() {
    return this.memory[this.PC]
  }

  _decode(opcode) {
    return disassemble(opcode)
  }

  _execute(instruction) {
    const { id, args } = instruction

    switch (id) {
      case 'ADD_VX_VY':
        this.registers[args[0]] += this.registers[args[1]]
        this.PC = this.PC + 2
        break
    }
  }
}

Сейчас не хватает только одного — возможности поиграть.

Создание интерфейса ЦП для ввода-вывода


Итак, у меня есть процессор, который интерпретирует и выполняет инструкции и обновляет все свои состояния, но я ещё ничего не могу с ним сделать.

Именно здесь в дело вступает ввод/вывод — связь между центральным процессором и внешним миром.

  • Ввод — это данные, полученные центральным процессором.
  • Вывод — это данные, отправленные центральным процессором.

Ввод будет с клавиатуры, вывод — в виде графики.

Я могла просто смешать код ввода-вывода с процессором напрямую, но тогда я была бы привязан к одной среде. Создав общий интерфейс CPU для соединения ввода/вывода и CPU, я могу взаимодействовать с любой системой.

Первое, что нужно было сделать, — это просмотреть инструкции и найти те, что имеют отношение к вводу/выводу. Несколько примеров таких инструкций:

  • CLS — очистить экран.
  • LD Vx, K — ожидание нажатия клавиши, сохранение значения клавиши в Vx.
  • DRW Vx, Vy, nibble — отображение n-байтового спрайта, начинающегося в ячейке памяти I.

Исходя из этого, мы хотим, чтобы интерфейс имел такие методы:

  • clearDisplay().
  • waitKey().
  • drawPixel() (drawSprite было бы 1:1, но в итоге оказалось, что проще делать это попиксельно из интерфейса).

В JavaScript нет понятия абстрактного класса, но я создала класс, который не может быть инстанцирован, с методами, работающими только из классов, которые расширяют этот класс. Вот все методы интерфейса этого класса:

// Абстрактный класс интерфейса CPU
class CpuInterface {
  constructor() {
    if (new.target === CpuInterface) {
      throw new TypeError('Cannot instantiate abstract class')
    }
  }

  clearDisplay() {
    throw new TypeError('Must be implemented on the inherited class.')
  }

  waitKey() {
    throw new TypeError('Must be implemented on the inherited class.')
  }

  getKeys() {
    throw new TypeError('Must be implemented on the inherited class.')
  }

  drawPixel() {
    throw new TypeError('Must be implemented on the inherited class.')
  }

  enableSound() {
    throw new TypeError('Must be implemented on the inherited class.')
  }

  disableSound() {
    throw new TypeError('Must be implemented on the inherited class.')
  }
}

Вот как это будет работать: интерфейс будет загружен в CPU при инициализации, и CPU сможет получить доступ к его методам.

class CPU {
  // Инстанцируем интерфейс
  constructor(cpuInterface) {
    this.interface = cpuInterface
  }

  _execute(instruction) {
    const { id, args } = instruction

    switch (id) {
      case 'CLS':
        // Используем интерфейс при выполнении инструкции
        this.interface.clearDisplay()
  }
}

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

Экран


Экран имеет разрешение 64 пикселя в ширину на 32 пикселя в высоту. Итак, что касается процессора и интерфейса, то это 64×32 сетка битов, которые либо включены, либо выключены. Чтобы создать пустой экран, я могу просто создать 3D-массив нулей, представляя все пиксели выключенными. Буфер кадра — это часть памяти, содержащая растровое изображение, которое будет выведено на дисплей.

// Интерфейс для тестирования
class MockCpuInterface extends CpuInterface {
  constructor() {
    super()

    // Храним данные экрана в буфере кадров
    this.frameBuffer = this.createFrameBuffer()
  }

  // Создаём 3D массив нулей
  createFrameBuffer() {
    let frameBuffer = []

    for (let i = 0; i < 32; i++) {
      frameBuffer.push([])
      for (let j = 0; j < 64; j++) {
        frameBuffer[i].push(0)
      }
    }

    return frameBuffer
  }

  // Обновляем пиксель (0 или 1)
  drawPixel(x, y, value) {
    this.frameBuffer[y][x] ^= value
  }
}

В итоге в смысле представления экрана я получаю что-то вроде этого:

0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
и т.д.

В функции DRW процессор пройдётся по извлечённому из памяти спрайту и обновит каждый пиксель в нём. Детали опущены для краткости.

case 'DRW_VX_VY_N':
  // Интерпретатор считывает n байт из памяти, начиная с адреса в I
  for (let i = 0; i < args[2]; i++) {
    let line = this.memory[this.I + i]
      // Каждый байт представляет собой строку из восьми пикселей
      for (let position = 0; position < 8; position++) {
        // ...Получаем значение, x, и y...
        this.interface.drawPixel(x, y, value)
      }
    }

Функция clearDisplay() — единственный метод, который будет использоваться для взаимодействия с экраном. Это всё, что нужно интерфейсу процессора для такого взаимодействия.

Клавиши


Я сопоставила оригинальную клавиатуру со следующей сеткой клавиш:

┌───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │
│ Q │ W │ E │ R │
│ A │ S │ D │ F │
│ Z │ X │ C │ V │
└───┴───┴───┴───┘

И поместила ключи в массив.

// лучше игнорировать
const keyMap = [
  '1', '2', '3', '4',
  'q', 'w', 'e', 'r', 
  'a', 's', 'd', 'f', 
  'z', 'x', 'c', 'v'
]

Для хранения текущих нажатых клавиш опишите состояние:

this.keys = 0

В интерфейсе keys — это двоичное число из 16 цифр, индекс представляет клавишу. Chip-8 просто хочет знать, какие клавиши нажаты, и на основе этого принимает решение:

0b1000000000000000 // V нажата (keyMap[15], или индекс 15)
0b0000000000000011 // 1 и 2 нажаты (index 0, 1)
0b0000000000110000 // Q и W нажаты (index 4, 5)

Теперь, если, например, нажата V (keyMap[15]) и операнд — 0xf (десятичное 15), то клавиша нажата. Левый сдвиг (<<) и 1 создаст двоичное число с 1, за которым следует столько нулей, сколько находится в левом сдвиге.

case 'SKP_VX':
  // Пропустить следующую инструкцию, если нажата клавиша со значением VX
  if (this.interface.getKeys() & (1 << this.registers[args[0]])) {
   // Пропускаем инструкцию
  } else {
    // Идём к следующей инструкции
  }

Есть ещё один метод клавиш, waitKey, где инструкция заключается в ожидании нажатия клавиши и возврате этой нажатой клавиши.

Приложение CLI — взаимодействие с терминалом


Первый интерфейс, который я сделала, был для терминала. Это было мне не так знакомо, как работа с DOM: я никогда не создавала графических приложений в терминале, но это не слишком сложно.

Curses — это библиотека, используемая для создания текстовых пользовательских интерфейсов в терминале. Blessed — это библиотека, оборачивающая curses для Node.js.

Экран


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

С помощью blessed я определила объект экрана:

this.screen = blessed.screen({ smartCSR: true })

И использовала fillRegion или clearRegion на пикселе с полным блоком юникода, чтобы заполнить его c frameBuffer в качестве источника данных.

drawPixel(x, y, value) {
  this.frameBuffer[y][x] ^= value

  if (this.frameBuffer[y][x]) {
    this.screen.fillRegion(this.color, '█', x, x + 1, y, y + 1)
  } else {
    this.screen.clearRegion(x, x + 1, y, y + 1)
  }

  this.screen.render()
}

Клавиши


Обработчик клавиш не слишком отличался от того, что в DOM. Если клавиша нажата, обработчик передаёт ключ, затем я могу использовать его для поиска индекса и обновления объекта keys с любыми дополнительными клавишами, которые были нажаты.

this.screen.on('keypress', (_, key) => {
  const keyIndex = keyMap.indexOf(key.full)

  if (keyIndex) {
    this._setKeys(keyIndex)
  }
})

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

setInterval(() => {
  // Эмулируем keyup, чтобы очистить все нажатые клавиши
  this._resetKeys()
}, 100)

Точка входа


Всё готово — буфер rom для преобразования двоичных данных в опкоды, интерфейс для подключения ввода/вывода, процессор, содержащий состояние, цикл команд и два открытых метода — один для загрузки игры, другой для выполнения цикла. Поэтому я создаю функцию cycle, которая будет запускать инструкции процессора в бесконечном цикле.

const fs = require('fs')
const { CPU } = require('../classes/CPU')
const { RomBuffer } = require('../classes/RomBuffer')
const {
  TerminalCpuInterface,
} = require('../classes/interfaces/TerminalCpuInterface')

// Извлекаем файл ПЗУ
const fileContents = fs.readFileSync(process.argv.slice(2)[0])

// Инициализируем интерфейс терминала
const cpuInterface = new TerminalCpuInterface()

// Инициализируем CPU с интерфейсом
const cpu = new CPU(cpuInterface)

// Преобразуем двоичные данные в опкоды
const romBuffer = new RomBuffer(fileContents)

// Загружаем игру
cpu.load(romBuffer)

function cycle() {
  cpu.step()

  setTimeout(cycle, 3)
}

cycle()

В функции цикла также есть таймер задержки, но я удалила его из примера для наглядности.

Теперь, чтобы играть, я могу запустить файл точки входа терминала и передать ROM как аргумент.

npm run play:terminal roms/PONG

Веб-приложение — взаимодействие с браузером


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

Экран


Для экрана я использовала Canvas API и его CanvasRenderingContext2D для поверхности рисования. fillRect и canvas в основном то же, что fillRegion в blessed.

this.screen = document.querySelector('canvas')
this.context = this.screen.getContext('2d')
this.context.fillStyle = 'black'
this.context.fillRect(0, 0, this.screen.width, this.screen.height)

Одно небольшое отличие: я умножила все пиксели на 10, чтобы экран стал заметнее.

this.multiplier = 10
this.screen.width = DISPLAY_WIDTH * this.multiplier
this.screen.height = DISPLAY_HEIGHT * this.multiplier

Это сделало команду drawPixel более многословной, но в остальном концепция осталась прежней.

drawPixel(x, y, value) {
  this.frameBuffer[y][x] ^= value

  if (this.frameBuffer[y][x]) {
    this.context.fillStyle = COLOR
    this.context.fillRect(
      x * this.multiplier,
      y * this.multiplier,
      this.multiplier,
      this.multiplier
    )
  } else {
    this.context.fillStyle = 'black'
    this.context.fillRect(
      x * this.multiplier,
      y * this.multiplier,
      this.multiplier,
      this.multiplier
    )
  }
}

Клавиши


У меня был доступ к гораздо большему количеству обработчиков событий клавиш в DOM, поэтому я смогла легко обрабатывать события keyup и keydown.

// Устанавливаем клавиши ненажатыми
document.addEventListener('keydown', event => {
  const keyIndex = keyMap.indexOf(event.key)

  if (keyIndex) {
    this._setKeys(keyIndex)
  }
})

// Сбрасываем клавиши по нажатию
document.addEventListener('keyup', event => {
  this._resetKeys()
})
}

Точка входа


Для работы с модулями я импортировала все модули и установила их в глобальный объект, а затем использовала Browserify для работы в браузере. Установка их в глобальные делает их доступными в окне, чтобы я могла использовать вывод кода в сценарии браузера. Сегодня для этого можно использовать Webpack или что-то другое, но это было быстро и просто.

const { CPU } = require('../classes/CPU')
const { RomBuffer } = require('../classes/RomBuffer')
const { WebCpuInterface } = require('../classes/interfaces/WebCpuInterface')

const cpuInterface = new WebCpuInterface()
const cpu = new CPU(cpuInterface)

// Устанавливаем буфер CPU и ROM в глобальный объект, который станет окном в браузере.
global.cpu = cpu
global.RomBuffer = RomBuffer

Точка входа в веб использует ту же функцию cycle, что и реализация терминала, но имеет функцию для получения каждого ПЗУ и сброса данных дисплея каждый раз, когда выбирается новое ПЗУ. Я привыкла работать с json данными и fetch, но в этом случае извлекла необработанный arrayBuffer из ответа.

// Извлекаем ПЗУ и загружаем игру
async function loadRom() {
  const rom = event.target.value
  const response = await fetch(`./roms/${rom}`)
  const arrayBuffer = await response.arrayBuffer()
  const uint8View = new Uint8Array(arrayBuffer)
  const romBuffer = new RomBuffer(uint8View)

  cpu.interface.clearDisplay()
  cpu.load(romBuffer)
}

// Добавляем возможность выбирать игру
document.querySelector('select').addEventListener('change', loadRom)

HTML содержит canvas и select.

<canvas></canvas>
<select>
  <option disabled selected>Load ROM...</option>
  <option value="CONNECT4">Connect4</option>
  <option value="PONG">Pong</option>
</select>

Затем я просто развернула код на страницах GitHub, потому что он статический.

Нативное приложение — взаимодействие с нативной платформой


Я также сделала экспериментальную реализацию нативного UI. Я использовала Raylib для программирования простых игр, которая имела биндинг для Node.js.

Я считаю эту версию экспериментальной только потому, что она очень медленная по сравнению с другими, поэтому она менее удобна в использовании, но с клавишами и экраном всё работает правильно.

Точка входа


Raylib работает немного иначе, чем другие реализации, поскольку сама работает в цикле, а это значит, что я не буду использовать функцию cycle.

const r = require('raylib')

// Пока окно не закроется...
while (!r.WindowShouldClose()) {
  // Извлекаем, декодируем, выполняем
  cpu.step()

  r.BeginDrawing()
  // Отрисовываем экран с изменениями
  r.EndDrawing()
}

r.CloseWindow()

Экран


В рамках методов beginDrawing() и endDrawing() экран будет обновляться. Для реализации Raylib, вместо того чтобы держать всё в интерфейсе, я обращалась к интерфейсу прямо из скрипта. Это работает.

r.BeginDrawing()

cpu.interface.frameBuffer.forEach((y, i) => {
  y.forEach((x, j) => {
    if (x) {
      r.DrawRectangleRec({ x, y, width, height }, r.GREEN)
    } else {
      r.DrawRectangleRec({ x, y, width, height }, r.BLACK)
    }
  })
})

r.EndDrawing()

Клавиши


Заставить ключи работать на Raylib — это последнее, над чем я работала. Мне приходилось делать всё в методе IsKeyDown — существовал метод GetKeyPressed, но он имел побочные эффекты и вызывал проблемы. Поэтому, вместо того чтобы просто ждать нажатия клавиши, как в других реализациях, я должна была перебирать все клавиши и проверять, нажаты ли они, и, если так, добавлять их в битовую маску клавиши.

let keyDownIndices = 0
// Выполняем для всех клавиш
for (let i = 0; i < nativeKeyMap.length; i++) {
  const currentKey = nativeKeyMap[i]
  // Если клавиша нажата, добавляем индекс в отображение нажатых клавиш
  // Также отожмёт все клавиши, которые не были нажаты
  if (r.IsKeyDown(currentKey)) {
    keyDownIndices |= 1 << i
  }
}

// Устанавливаем нажатые клавиши
cpu.interface.setKeys(keyDownIndices)

Вот и всё. Эта задача сложнее остальных, но я рада, что сделала это, чтобы завершить интерфейс и посмотреть, насколько хорошо этот интерфейс работает на разных платформах.

Заключение


И вот мой проект «Chip-8». Вы можете посмотреть исходники на GitHub. Я узнала много нового о концепциях низкоуровневого программирования и о том, как работает процессор, а также о возможностях JavaScript за пределами браузерного приложения или сервера REST API. Мне ещё предстоит сделать несколько вещей, например попытаться написать простую игру, но эмулятор завершён, и я горжусь этим.

Продолжить изучение JS вы сможете на наших курсах:

  • Профессия Frontend-разработчик (7 месяцев)
  • Профессия Fullstack-разработчик на Python (15 месяцев)

Узнайте подробности здесь.

Передо мной стоит вот такая задача:
Разработать эмулятор процессора, создать для него язык ассемблера и написать несколько программ. Нужно придумать свою архитектуру CPU и придумать ряд команд, почитайте про современные архитектуры и например наборы команд SSE и AVX.

Помогите пожалуйста разобраться. Я понял так(хотя понимание пока очень поверхностное):
Мне не надо напрямую возиться с регистрами и все такое? Главное на логическом уровне все правильно сделать а не физическом? То есть можно взять любой ЯП, например Java, и на нем писать процессор? Он не будет иметь ничего общего с физическими процессорами, но по логике будем им соответствовать. То есть у нас может быть программа, в которой создан массив — наша эмуляция памяти, например так: long[] memory = new long[1024 * 1024]. Еще один массив — эмулирет регистры. Наша основная программа стартует, создаются оба массива, дальше она начинает считывать из заранее заготовленного файла по одной строке. А вот в файле мы заранее напишем нашу систему команд — Логические — and, or, not, xor. Численные — add, sub, mul, div. Программа будет их считывать и выполнять. То есть в основной функции программы, которая считывает и выполняет команды, можно будет написать switch если add -> складываем, если mul -> умножаем и т.д. Подскажите пожалуйста, хотя бы направление мыслей правильное?

December 25 2013, 01:37

Category:

  • Компьютеры
  • Cancel

простой эмулятор (программистское)

x5602 — a simple 6502 CPU emulator

Проект, демонстрирующий очень простой эмулятор микропроцессора 6502 (который широко использовался в самом начале эры персональных компьютеров — Atari, Apple II, Commodore 64…). Если вы знаете язык C даже в общих чертах, и никогда не писали код, симулирующий работу процессора, то этот проект — хорошее введение в то, как к этому вообще подходить.

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

Как создать эмулятор?

Автор Сообщение
Сообщение 29 ноя 2010, 20:01

Профиль

Зарегистрирован:
14 ноя 2007, 11:19
Сообщения: 370

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

Сообщение 29 ноя 2010, 20:46

Профиль

GManiac

Аватара пользователя

Зарегистрирован:
22 июл 2007, 02:10
Сообщения: 313
Откуда: ниоткуда

Ничего себе PSX простая система :)
Мне было бы интересно почитать, «просто так». Хоть я и представляю, как устроена приставка и эмулятор.
Вообще говоря, имхо, если человек задаётся вопросом «как написать эмулятор?», это значит, что ему рано писать эмулятор. Скорей всего, он даже не знает элементарных вещей, не знает, как устроены отдельные узлы и не умеет грамотно проектировать программы. Постепенно он сам придёт к ответу. В двух словах это всё не объяснить.


_________________
Мысль — это интеллектуальный эксцесс данного индивидуума.

Сообщение 29 ноя 2010, 20:53

Профиль

Wind

Зарегистрирован:
14 ноя 2007, 11:19
Сообщения: 370

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

Сообщение 29 ноя 2010, 21:03

Профиль WWW

Eevon

Основатель сайта

Аватара пользователя

Зарегистрирован:
21 июл 2007, 15:40
Сообщения: 2232
Откуда: Москва

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

Сообщение 29 ноя 2010, 21:08

Профиль

edgbla

Зарегистрирован:
18 янв 2009, 14:20
Сообщения: 805

И я с интересом почитаю, если будет красиво и полно, несмотря на то что имею опыт. И идея с PSX мне тоже нравится. :-D

Сообщение 29 ноя 2010, 21:35

Профиль

Zeru-j

Аватара пользователя

Зарегистрирован:
15 сен 2008, 16:22
Сообщения: 407
Откуда: Neo Arcadia

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

Сообщение 30 ноя 2010, 14:50

Профиль

EvGS

Аватара пользователя

Зарегистрирован:
23 июл 2007, 19:37
Сообщения: 401
Откуда: Мытищи

И мне очень интересно — особенно NES. Заодно и реализовал бы давнюю задумку:

Spoiler: показать

Также интересует качественная эмуляция звуковых чипов, хотя бы самых простых PSG (SN76489AN, 2A03, AY-3-8910)

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

Spoiler: показать

В общем, «школота» полная. Но я с удовольствием поддерживаю предложение Wind’a


_________________
Nestopia 1.37/1.40 Fixed

Сообщение 30 ноя 2010, 15:26

Профиль

HardWareMan

Аватара пользователя

Зарегистрирован:
24 июл 2007, 06:54
Сообщения: 492
Откуда: Embedded

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


_________________
Tried so hard and got so far, but in the end, it doesn’t even matter…

Сообщение 30 ноя 2010, 15:44

Профиль

EvGS

Аватара пользователя

Зарегистрирован:
23 июл 2007, 19:37
Сообщения: 401
Откуда: Мытищи

Вот это книжечка. Даже не подозревал о её существовании. Спасибо, будем курить вдумчиво.


_________________
Nestopia 1.37/1.40 Fixed

Сообщение 30 ноя 2010, 18:34

Профиль

Wind

Зарегистрирован:
14 ноя 2007, 11:19
Сообщения: 370

Ну для «затравочки» решил черкнуть, немного для начала:
Много лет назад я уже писал немного о эмуляции проца, трансляции памяти и т.п., вроде даже на форуме те статьи есть, но я решил немного повторится и написать все с начала:
Итак, собственно эмуляция что это такое, если верить русскоязычной WIKI то это -«воспроизведение программными или аппаратными средствами либо их комбинацией работы других программ или устройств». Коротко и лаконично, но в большинстве своем мало понятно, а скорей совсем не ясно. У меня определения собственного впрочем нет, так что будем раскрывать данное понятие по шагам:
Начнем, для начала представим себе абстрактную «систему», а точнее рассмотрим основные модули которые обычно присутствуют (хотя могут и отсутствовать все же) в любой «системе»: Процессор основа всего, устройство видеоаудио вывода, ну и собственно устройство ввода (клавиатура, джойстик, мышка и много чего еще).
Эмуляция любой системы начинается с эмуляции процессора. Как же работает процессор с точки зрения программиста, а тут все довольно просто, в стародавние времена, каждый мало-мальский человек называющий себя программистом умел писать на «ассемблере» (увы для нынешнего поколения это уже незнакомое слово), так вот ассемблер представлял не что иное как мнемоническое представление команд процессора, т.е. если программист писал «add eax, edx», то он хотел, не что иное как прибавить к значению содержащемуся в регистре процессора eax, значение регистра edx. Компилятор, ассемблируя исходный код, просто вместо мнемоник подставлял в выходной файл машинное представление каждой из встреченных в коде команды.
При запуске конечного файла, данные загружались в оперативную память и указатель на код устанавливался в точку входа написанного кода. Процессор в свою очередь начинал считывать данные из оперативной памяти декодировать команды и исполнять, так вот эмулируя процессор необходим делать абсолютно тоже самое, т.е. считать команду из оперативной памяти, декодировать её и соответственно исполнить её, затем также считать следующую и проделать тоже самое и так постоянно читать и исполнять, более подробно как это делается напишу позже.

Сообщение 30 ноя 2010, 19:27

Que

Может быть не стоило начинать описание прямо в опросе? Раз дело начато, значит опрос сделал своё дело.

Сообщение 30 ноя 2010, 19:39

Профиль

Wind

Зарегистрирован:
14 ноя 2007, 11:19
Сообщения: 370

Ну опрос добавлен для выяснения настроений, а там если будет нужно отдельную тему выделить не проблема или даже целый раздел форума

Сообщение 01 дек 2010, 04:04

Профиль

iddqd

Зарегистрирован:
09 авг 2007, 11:23
Сообщения: 525

Тогда уж и Capcom ZN 1/2 заэмулить:)


_________________
Применение ArtMoney в эмуляторных играх

Сообщение 01 дек 2010, 05:18

Профиль

Wind

Зарегистрирован:
14 ноя 2007, 11:19
Сообщения: 370

Помнится оно на железе PSX так что совсем не проблема

Сообщение 01 дек 2010, 12:11

Профиль

iddqd

Зарегистрирован:
09 авг 2007, 11:23
Сообщения: 525

Если что, книга из поста HWM есть тут

http://tv-games.ru/media/view/consoles.html


_________________
Применение ArtMoney в эмуляторных играх

Сообщение 01 дек 2010, 12:28

Профиль

Flash

Зарегистрирован:
30 окт 2007, 00:53
Сообщения: 75

iddqd писал(а):

Тогда уж и Capcom ZN 1/2 заэмулить:)

Ну вообще-то уж не один ZN а всё PSX-based лучше.

Сообщение 01 дек 2010, 13:02

Профиль

organic

Зарегистрирован:
04 ноя 2010, 18:55
Сообщения: 29

Хорошая тема, заодно и эмулятор будет :D

Сообщение 01 дек 2010, 14:30

Профиль WWW

Eevon

Основатель сайта

Аватара пользователя

Зарегистрирован:
21 июл 2007, 15:40
Сообщения: 2232
Откуда: Москва

iddqd писал(а):

Тогда уж и Capcom ZN 1/2 заэмулить:)

А чего его эмулировать, если есть ZiNc? Или речь о другом оборудовании идёт?

Сообщение 01 дек 2010, 14:41

Профиль

iddqd

Зарегистрирован:
09 авг 2007, 11:23
Сообщения: 525

//Eevon, думаю, лишним не будет.


_________________
Применение ArtMoney в эмуляторных играх

Сообщение 04 дек 2010, 13:32

Профиль

Wind

Зарегистрирован:
14 ноя 2007, 11:19
Сообщения: 370

Итак, после недолго затишья продолжим:
Как я и писал выше сегодня я постараюсь рассказать Вам, что же из себе представляет эмуляция процессора, различают два способа эмуляции: «интерпретация», «динамическая рекомпиляция», некоторые еще выделяют так называемую «статическую рекомпиляцию», но я не представляю как можно заставить работать данный вид эмуляции в жизни, впрочем утверждалось, что некий «сorn» эмулятор Nintendo 64 использовал именно этот способ эмуляции, но учитывая что сырцы не были никогда открыты, поверь в это я не могу, а поэтому о ней рассказывать смысла не вижу.
Итак, что же есть такое интерпретация кода процессора?
Собственно, я в двух словах написал ранее, так что просто повторюсь, считываем команду процессора из памяти, декодируем ее и сразу исполняем.
Чем же хорош это способ? А все просто, реализовать данный метод эмуляции крайне просто и не трудозатратно, минимум ошибок, считается более аккуратным способом эмуляции, но с этим утверждением я не согласен, но у этого метода есть очень большой минус, скорость эмуляции даже очень быстрых системах, крайне низка.
Поэтому и был придуман другой способ эмуляции, «динамическая рекомпиляция»:
В чем отличия от первого? А отличие, в том что создается как бы кеш декодированных инструкций, т.е. мы также считываем команду за командой, также ее декодируем, НО, вместо того чтобы ее исполнить, мы пишем в буфер код, который равнозначен тому, что должна исполнить данная команда, причем с применением всяческих оптимизаций, вплоть до того, что кол-во команд становится даже меньше чем было в изначальном коде, более подробно о данных оптимизациях расскажу в последующем. Собственно весь код бьется на блоки, после компляции очередного блока его исполняем.

Ну и хочу привести немного кода и заодно сказать почему же PSX самая простая для эмуляции система, а все потому что у данной системы в основе лежит MIPS R3000A, этот процессор настолько прост, что проще не бывает, собственно в качестве примера приведу код из PCSX:
Я убрал лишне чтобы не засорять:

Код:

void execute() {
   u32 code = *(u32 *)PSXM(psxRegs.pc); /* собственно считывание очередной инструкции */

   psxRegs.pc += 4; /* увеличиваем указатель на код на размер инструкции */

   psxBSC[psxRegs.code >> 26](); /* ну и самое главное стадия декодирования, переход на ф-ию считанной нами инструкции */
}

В качестве примера эмуляции инструкции:

Код:

void psxADDI()    
{
if (!_Rt_) return; /* проверка не производит ли команда запись в R0, дело в том что данный регистр зареверзирован и всегда возвращает ноль, т.е. читать его можно, но нельзя его изменять */
_rRt_ = _u32(_rRs_) + _Imm_ ; /* собственно исполнение команды, суть запись в номер регистра который закодирован в _rRt_, результата сложения регистра закодированного в _rRs_ с некой константой закодированной в _Imm_
}

Итак, поступаем с каждой известной командой.

Показать сообщения за:  Поле сортировки  

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 0

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете добавлять вложения

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