В этой серии статей я собираюсь рассказать как написать простейший эмулятор компьютера на примере 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 и, собственно, разрядности.
С чего начать?
А начать нужно, разумеется, с описания процессора.
В самом начале, я планировал написать эмулятор 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_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