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

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

Эмулятор мы будем писать на языке C++ с использованием графической библиотеки SDL. Код основывается на исходниках моего эмулятора emuChip.

История CHIP-8

CHIP-8 — это небольшой, интерпретируемый язык программирования и интерпретатор для него, впервые появившийся на домашних компьютерах COSMAC VIP, Telmac 1800, DREAM 6800 в конце 70-х годов. Создателем является Joseph Weisbecker. CHIP-8 использовался для создания игр, таких как Pong, Tetris, Space Invaders и т.п.

Изображение компьютера

В 1990 году Andreas Gustafsson написал интерпретатор CHIP-8 для графического калькулятора HP-48. Эта версия была названа CHIP-48.

первая программа на с++

В 1991 году Erik Bryntse добавил в язык несколько нововведений, позволившие в 2 раза увеличить разрешение экрана в играх и использовать скроллинг. Данная версия стала называться SCHIP (Super Chip).

В настоящее время существует еще и MegaChip разработанный Revival Studios. В эту версию добавлена цветная графика и звук.

Технические характеристики

Экран

Оригинальная реализация CHIP-8 имеет монохромный(черно-белый) экран размером 64х32 пикселя. SCHIP в дополнение к основному имеет расширенный режим 128×64.

CHIP-8 рисует графику на экране используя спрайты. Спрайт имеет 8 пикселей в ширину и от 1 до 15 пикселей в высоту. Так же интерпретатор предоставляет 16 предопределенных спрайтов размером 4×5 пикселей. Это шестнадцатеричные числа от 0 до F.

Клавиатура

Компьютеры на которых использовался оригинальный CHIP-8 имели 16-клавишную клавиатуру следующего вида.

1 2 3 C
4 5 6 D
7 8 9 E
A B F

Память

CHIP-8 имеет 4 кб памти (адреса 0x000h-0xFFFh). Первые 512 байт (адреса 0x000h-0x200h) зарезервированы для интерпретатора, так что игре доступно только 3,584 байт. Соответственно игра располагается в памяти начиная с адреса 0x200h.

Регистры

CHIP-8 имеет 16 восьмибитных регистров общего назначения V0-VF. Регистр VF используется как флаг переноса и индикатор столкновений спрайтов.

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

Помимо регистров существует два восьмибитных таймера: задержки и звука. Оба таймера уменьшают свое значение 60 раз в секунду, пока не достигнут нуля. Если значение звукового таймера отлично от нуля интерпретатор выводил звук (beep!).

Эмулятор в Android Studio | Эмулятор андроид устройства, как создать? Эмулятор андроид

Стек

В стеке сохраняются адреса возврата при вызове функций. Оригинальный интерпретатор имел 12 уровней вложенности стека. Сейчас принято делать 16. Каждое значение стека имеет размер 2 байта.

На сегодня это все

В следующей статье мы перейдем к изучению команд языка и их опкодов.

Похожие статьи:

  • Lazy IPS
  • Sega Mega Drive Fix Checksum

Источник: emunix.org

Пишем никому не нужный эмулятор

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

Имя велосипеду — V16, от склеивания слова Virtual и, собственно, разрядности.

С чего начать?

А начать нужно, разумеется, с описания процессора.

В самом начале, я планировал написать эмулятор DCPU-16, но таких чудес на просторах Интернета хватает с лихвой, поэтому я решил остановиться только на «слизывании» самого основного с DCPU-16 1.1.

Читайте также:
Программа которая скачивает видео из ВК на Айфон

Архитектура

Память и порты

  • V16 адресует 128Kb (65536 слов) оперативной памяти, которая также может использоваться как буферы устройств и стек.
  • Стек начинается с адреса FFFF, следовательно, RSP имеет стандартное значение 0xFFFF
  • Портов ввода-вывода V16 имеет 256, все они имеют длину в 16 бит. Чтение и запись из них осуществляется через инструкции IN b, a И OUT b, a .

Регистры

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

Инструкции

Все инструкции имеют максимальную длину в три слова и полностью определяются первым
Первое слово делится на три значения: младший байт — опкод, старший байт в виде двух 4-битных значений — описание операндов.

Прерывания

Прерывания здесь — не более чем таблица с адресами, на которые процессор дублирует инструкцию CALL . Если значение адреса равно нулю, то прерывание не делает ничего, просто обнуляет флаг HF.

Диапазон значений Описание
0x0. 0x3 Регистр как значение
0x4. 0x7 Регистр как значение по адресу
0x8. 0xB Регистр + константа как значение по адресу
0xC Константа как значение по адресу
0xD Константа как значение
0xE Регистр RIP как значение только для чтения
0xF Регистр RSP как значение

Пример псевдокода и слов, в которые все это должно странслироваться:

MOV RAX, 0xABCD ; 350D ABCD MOV [RAX], 0x1234 ; 354D 1234

Cycles (Такты)

V16 может выполнять одну инструкцию за 1, 2 или 3 такта. Каждое обращение к оперативной памяти это один отдельный такт. Инструкция это не такт!

Начнем писать!

Реализация основных структур процессора

    Набор регистров. Регистров всего четыре, но ситуацию улучшает то, что таких наборов в процессоре целых два. Переключение происходит при помощи инструкции XCR .

typedef struct Regs < uint16_t rax, rbx; //Primary Accumulator, Base Register uint16_t rcx, rdx; //Counter Register, Data Register >regs_t;
//Чтобы было красиво, нужно включить заголовок stdbool.h typedef struct Flags < bool IF, IR, HF; bool CF, ZF; bool EF, GF, LF; >flags_t;

typedef struct CPU < //CPU Values uint16_t ram[V16_RAMSIZE]; //Random Access Memory uint16_t iop[V16_IOPSIZE]; //Input-Output Ports uint16_t idt[V16_IDTSIZE]; //Interrupt vectors table (Interrupt Description Table) flags_t flags; //Flags regs_t reg_m, reg_a; //Main and Alt register files regs_t * reg_current; //Current register file uint16_t rip, rsp, rex; //Internal Registers: Instruction Pointer, Stack Pointer, EXtended Accumulator //Emulator values bool reg_swapped; //Is current register file alt bool running; //Is cpu running uint32_t cycles; //RAM access counter >cpu_t;
typedef struct Opd < uint8_t code : 4; uint16_t value; uint16_t nextw; >opd_t;

Функции для работы со структурами

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

cpu_t * cpu_create(void); //Создаем экземпляр процессора void cpu_delete(cpu_t *); //Удаляем экземпляр процессора void cpu_load(cpu_t *, const char *); //Загружаем ROM в память void cpu_rswap(cpu_t *); //Меняем наборы регистров uint16_t cpu_nextw(cpu_t *); //RAM[RIP++]. Nuff said void cpu_getop(cpu_t *, opd_t *, uint8_t); //Читаем операнд void cpu_setop(cpu_t *, opd_t *, uint16_t); //Пишем операнд void cpu_tick(cpu_t *); //Выполняем одну инструкцию void cpu_loop(cpu_t *); //Выполняем инструкции, пока процессор работает

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

Функция tick()

Также здесь присутствуют вызовы static-функций, предназначенных только для вызова из tick() .

void cpu_tick(cpu_t *cpu) < //Если была выполнена инструкция HLT, то функция ничего не сделает if(cpu->flags.HF) < //Если к тому же обнулен флаг прерываний, то паузу снимать уже нечему if(!cpu->flags.IF) < cpu->running = false; > return; > //Получаем следующее слово и декодируем как инструкцию uint16_t nw = cpu_nextw(cpu); uint8_t op = ((nw >> 8) uint8_t ob = ((nw >> 4) uint8_t oa = ((nw >> 0) //А потому что дизайн кода //Создаем структуры операндов opd_t opdB = < 0 >; opd_t opdA = < 0 >; //И читаем их значения cpu_getop(cpu, cpu_getop(cpu, //Дальше для сокращения и улучшения читабельности кода делаем переменные-значения операндов uint16_t B = opdB.value; uint16_t A = opdA.value; uint32_t R = 0xFFFFFFFF; //Один очень интересный костыль bool clearf = true; //Будут ли флаги условий чиститься после выполнения инструкции? //И начинаем творить магию! switch(op) < //Здесь мы проходим все возможные опкоды. Те, которые пишут результаты, меняют значение переменной R >//Чистим флаги условий if(clearf) < cpu->flags.EF = false; cpu->flags.GF = false; cpu->flags.LF = false; > //Очень интересный костыль, максимальное 32-битное значение при 16-битных операциях // равно 0xFFFF0000, то есть 0xFFFF rex = ((R >> 16) cpu->flags.CF = (cpu->rex != 0); cpu->flags.ZF = (R == 0); > return; >

Читайте также:
В какой программе проектировать каркасный дом

Что делать дальше?

В попытках найти ответ на сей вопрос, я раз пять переписал эмулятор с C на C++, и обратно.

Однако главные цели можно выделить уже сейчас:

  • Прикрутить нормальные прерывания (Вместо простого вызова функции и запрета на прием других прерываний сделать вызов функции и добавление новых прерываний в очередь).
  • Прикрутить устройства, а также способы общения с ними, благо опкодов может быть 256.
  • Научить себя не писать всякую ересь на хабр процессор работать с определенной тактовой частотой в 200 МГц.

Заключение

Надеюсь, что кому-нибудь эта «статья» станет полезной, кого то подтолкнет на написание чего-то похожего.

Мои куличики можно посмотреть на github.

Также, о ужас, у меня есть ассемблер для старой версии этого эмулятора (Нет, даже не пытайтесь, эмулятор как минимум пожалуется на неправильный формат ROM)

  • эмулятор
  • программирование

Источник: habr.com

Пишем никому не нужный эмулятор

Пишем никому не нужный эмулятор

2019-06-22 в 6:22, admin , рубрики: C, Программирование, Процессоры, эмулятор

Доброго времени суток.

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

Имя велосипеду — V16, от склеивания слова Virtual и, собственно, разрядности.

Пишем никому не нужный эмулятор - 1

С чего начать?

А начать нужно, разумеется, с описания процессора.

В самом начале, я планировал написать эмулятор DCPU-16, но таких чудес на просторах Интернета хватает с лихвой, поэтому я решил остановиться только на «слизывании» самого основного с DCPU-16 1.1.

Архитектура

Память и порты

  • V16 адресует 128Kb (65536 слов) оперативной памяти, которая также может использоваться как буферы устройств и стек.
  • Стек начинается с адреса FFFF, следовательно, RSP имеет стандартное значение 0xFFFF
  • Портов ввода-вывода V16 имеет 256, все они имеют длину в 16 бит. Чтение и запись из них осуществляется через инструкции IN b, a И OUT b, a .

Регистры

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

Инструкции

Все инструкции имеют максимальную длину в три слова и полностью определяются первым
Первое слово делится на три значения: младший байт — опкод, старший байт в виде двух 4-битных значений — описание операндов.

Прерывания

Прерывания здесь — не более чем таблица с адресами, на которые процессор дублирует инструкцию CALL . Если значение адреса равно нулю, то прерывание не делает ничего, просто обнуляет флаг HF.

Диапазон значений Описание
0x0. 0x3 Регистр как значение
0x4. 0x7 Регистр как значение по адресу
0x8. 0xB Регистр + константа как значение по адресу
0xC Константа как значение по адресу
0xD Константа как значение
0xE Регистр RIP как значение только для чтения
0xF Регистр RSP как значение

Пример псевдокода и слов, в которые все это должно странслироваться:

MOV RAX, 0xABCD ; 350D ABCD MOV [RAX], 0x1234 ; 354D 1234

Cycles (Такты)

V16 может выполнять одну инструкцию за 1, 2 или 3 такта. Каждое обращение к оперативной памяти это один отдельный такт. Инструкция это не такт!

Начнем писать!

Реализация основных структур процессора

    Набор регистров. Регистров всего четыре, но ситуацию улучшает то, что таких наборов в процессоре целых два. Переключение происходит при помощи инструкции XCR .
Читайте также:
Запуск программы невозможен так как на компьютере отсутствует d3dx9 38 dll

typedef struct regs_t < uint16_t rax, rbx; //Primary Accumulator, Base Register uint16_t rcx, rdx; //Counter Register, Data Register >regs_t;
//Чтобы было красиво, нужно включить заголовок stdbool.h typedef struct flags_t < bool IF, IR, HF; bool CF, ZF; bool EF, GF, LF; >flags_t;

typedef struct cpu_t < //CPU Values uint16_t ram[V16_RAMSIZE]; //Random Access Memory uint16_t iop[V16_IOPSIZE]; //Input-Output Ports uint16_t idt[V16_IDTSIZE]; //Interrupt vectors table (Interrupt Description Table) flags_t flags; //Flags regs_t reg_m, reg_a; //Main and Alt register files regs_t * reg_current; //Current register file uint16_t rip, rsp, rex; //Internal Registers: Instruction Pointer, Stack Pointer, EXtended Accumulator //Emulator values bool reg_swapped; //Is current register file alt bool running; //Is cpu running uint32_t cycles; //RAM access counter >cpu_t;
typedef struct opd_t < uint8_t code : 4; uint16_t value; uint16_t nextw; >opd_t;

Функции для работы со структурами

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

cpu_t * cpu_create(void); //Создаем экземпляр процессора void cpu_delete(cpu_t *); //Удаляем экземпляр процессора void cpu_load(cpu_t *, const char *); //Загружаем ROM в память void cpu_rswap(cpu_t *); //Меняем наборы регистров uint16_t cpu_nextw(cpu_t *); //RAM[RIP++]. Nuff said void cpu_getop(cpu_t *, opd_t *, uint8_t); //Читаем операнд void cpu_setop(cpu_t *, opd_t *, uint16_t); //Пишем операнд void cpu_tick(cpu_t *); //Выполняем одну инструкцию void cpu_loop(cpu_t *); //Выполняем инструкции, пока процессор работает

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

Функция tick()

Также здесь присутствуют вызовы static-функций, предназначенных только для вызова из tick() .

void cpu_tick(cpu_t *cpu) < //Если была выполнена инструкция HLT, то функция ничего не сделает if(cpu->flags.HF) < //Если к тому же обнулен флаг прерываний, то паузу снимать уже нечему if(!cpu->flags.IF) < cpu->running = false; > return; > //Получаем следующее слово и декодируем как инструкцию uint16_t nw = cpu_nextw(cpu); uint8_t op = ((nw >> 8) uint8_t ob = ((nw >> 4) uint8_t oa = ((nw >> 0) //А потому что дизайн кода //Создаем структуры операндов opd_t opdB = < 0 >; opd_t opdA = < 0 >; //И читаем их значения cpu_getop(cpu, cpu_getop(cpu, //Дальше для сокращения и улучшения читабельности кода делаем переменные-значения операндов uint16_t B = opdB.value; uint16_t A = opdA.value; uint32_t R = 0xFFFFFFFF; //Один очень интересный костыль bool clearf = true; //Будут ли флаги условий чиститься после выполнения инструкции? //И начинаем творить магию! switch(op) < //Здесь мы проходим все возможные опкоды. Те, которые пишут результаты, меняют значение переменной R >//Чистим флаги условий if(clearf) < cpu->flags.EF = false; cpu->flags.GF = false; cpu->flags.LF = false; > //Очень интересный костыль, максимальное 32-битное значение при 16-битных операциях // равно 0xFFFF0000, то есть 0xFFFF rex = ((R >> 16) cpu->flags.CF = (cpu->rex != 0); cpu->flags.ZF = (R == 0); > return; >

Что делать дальше?

В попытках найти ответ на сей вопрос, я раз пять переписал эмулятор с C на C++, и обратно.

Однако главные цели можно выделить уже сейчас:

  • Прикрутить нормальные прерывания (Вместо простого вызова функции и запрета на прием других прерываний сделать вызов функции и добавление новых прерываний в очередь).
  • Прикрутить устройства, а также способы общения с ними, благо опкодов может быть 256.
  • Научить себя не писать всякую ересь на хабр процессор работать с определенной тактовой частотой в 200 МГц.

Заключение

Надеюсь, что кому-нибудь эта «статья» станет полезной, кого то подтолкнет на написание чего-то похожего.

Мои куличики можно посмотреть на github.

Также, о ужас, у меня есть ассемблер для старой версии этого эмулятора (Нет, даже не пытайтесь, эмулятор как минимум пожалуется на неправильный формат ROM)

Источник: www.pvsm.ru

Рейтинг
( Пока оценок нет )
Загрузка ...
EFT-Soft.ru