It is believed that before starting to emulate complex systems, you need to start with something simple, for example, with Chip-8. In this article I will try to consider all aspects of how you can write your own implementation of this language in a virtual machine. Any programming language will go completely, but because of the simplicity, I will choose Delphi.
Wait, do not immediately rush into the editor, first you will need a pen and a notebook, where we will write down all the important information for ourselves. It all starts with her search. First of all, this is of course Google. After half an hour of following links, we can see how many implementations have already been written for any system, I don’t want to choose, but we will not peek into other people's sources, but write our own.
CHIP-8 is a language interpreter that was used in the late 70s, early 80s on some small commercial computers, such as RCA's TELMAC 1800 and COSMAC VIP, and cheap “create-it-yourself” computers of the time like ETI 660 and DREAM 6800 ...
CHIP-8 made it easy to program games. TELMAC 1800 and COSMAC VIP were based on RCA CDP-1802 processors. Both came with an audio cassette containing more than 12 games, dated 1977. The interpreter had less than 40 commands, including math, flow control, graphics and sound.
The interpreter should have been very small, due to the memory limits of these computers: COSMAC VIP had 2Kb (but could be expanded to 32Kb), and TELMAC had 4Kb. CHIP-8 was only 512 bytes long.
The simplicity of the language allowed us to create the Pong, Brix, Invaders and Tank that we played at the very beginning of the video games. A good programmer could put these games in less than 256 bytes.
Here is a short story about the CHIP-8 of one of the DREAM-6800 users:
" ... the DREAM and ETI 660 appeared in Australian Electronic Journals as projects for assembly. These computers combined their incredibly low price (about $ 100), using a hex keyboard, the ability to reproduce very limited graphics of 64 x 32 pixels (ETI 660 could get 64 x 48 or 64 x 64 with its modification) transmitted to the TV, about one kilobyte of RAM, and the ability to run a high level pseudo language, called CHIP-8 (developed by RCA for displaying COSMAC graphics, I think).
One day, my older brother assembled the DREAM 6800. What a computer that was! Along with the DREAM & ETI 660 build articles, there were mountains of listing for CHIP-8 games. Some games were less than 200 bytes or so, so typing them would not take forever. And it was great games. They were not slow. So CHIP-8 was very well designed for classic TV games.
Paul Heiter (Author CHIP-8 Interpreter for Amiga)
Later, CHIP-8 was used in the early 90s on HP48 calculators, because there was no way to write games on it faster. Almost all of the original games from CHIP-8 worked with the CHIP48 interpreter, but many new ones were written.
Then a new version of the language was released - SUPER-CHIP. He had all the features of the standard, but could already operate with a resolution of 128x64.
All programs in CHIP-8 start at 200h, excluding ETI-660, which starts at 600h. This is done because at the address 000h-1FFh the language interpreter itself is located.
All memory is fully addressable and accessible. Since the instructions occupy 16-bit, they usually have even addresses, and if some 8 bits are inserted inside the code, their addresses become odd.
Based on the 12 bits used per memory address, you can calculate that the maximum memory without tweaks could be 4096 bytes (000h-FFFh). However, the addresses F00h-FFFh occupies video memory, and EA0h-EFFh is used to store the stack and internal CHIP-8 variables.
The interpreter uses 16 general-purpose 8-bit registers. They are available as V0..VF. Moreover, VF is used as a flag for arithmetic operations, in the case of transfer, and a collision detector when drawing sprites.
There is also 1 address register (I) 16 bits in size. Since the memory is only 4 kilobytes, the interpreter used only its low 12 bits. However, the upper 4 bits could be used for the font download function, since the font was located at 8110.
In addition to the registers, there were 2 timers. One is a delay timer, and one is a sound timer. Both had a length of 8 bits and reduced their contents 60 times per second, if they were not zero at that time. That is, they had a frequency of 60 hertz. If the sound timer had a nonzero value, it would play a sound.
The dimension of the stack remained unknown, but it’s accepted to do it in 16 levels (2x16 bytes).
Graphics are drawn with 8 by 1..15 pixel sprites that are encoded bytes. The origin of coordinates is in the upper left corner and begins at point 0. All coordinates are positive and are considered as the remainder method of dividing by 64 or 32, respectively. The output to the screen is in XOR mode. If one or more pixels are cleared (change their color from 1 to 0) the VF register is set to 01h and 00h otherwise. Chip-8 has a 4x5 pixel font containing characters 0-9 and AF.
For clarity, consider the example of the sprite and its coding. Take the 8x5 sprite.
We get the following set of bytes:
0 A0 0 A0 0
To make it completely understandable, let's take another 8x8 sprite.
Sprite will occupy 8 bytes and will have the following structure:
0 60 30 18 0 06 03
The keyboard for CHIP-8 is 16th and looks like this:
One can only argue and reason about the convenience and expediency of such a keyboard. But we will not move away from the original, because everything was designed for just such a keyboard.
It should immediately be said that as NNN we will denote the address, KK - 8 bit constant, X and Y - 4 bit constants.
Now consider the list of commands.
0NNN Syscall nnn Call the instructions of the machine instruction of the processor 1802 with code NNN.
00CN * scdown n Scrolls the screen down N lines.
00FB * Scright Scrolling the screen 4 pixels to the right
00FC * Scleft Scrolling the screen 4 pixels to the left
00FD * Exit Emulator
00FE * Low Set the CHIP-8 (64x32) graphic mode
00FF * High Set the graphic mode SUPER CHIP (128x64)
00E0 Cls Clear Screen
00EE Rts Return from subroutine
1NNN jmp nnn Translate program execution to address NNN
2NNN jsr nnn Function call at address NNN. The previous address is pushed onto the stack.
3XKK skeq vx, kk Skip the following instruction (2 bytes) if VX = KK
4XKK skne vx, kk Skip the following instruction (2 bytes) if VX <> KK
5XY0 skeq vx, vy Skip the following instruction if VX = VY
6XKK mov vx, kk Write KK to VX register
7XKK add vx, kk Write VX + KK to VX register (According to information from Wikipedia and other sources it does not affect the flag)
8XY0 mov vx, vy In VX write the value of the register VY
8XY1 or vx, vy VX = VX OR VY.
8XY2 and vx, vy VX = VX AND VY
8XY3 xor vx, vy VX = VX XOR VY (not documented in original documents)
8XY4 add vx, vy VX = VX + VY. In VF = carry.
8XY5 sub vx, vy VX = VX - VY. (*) VF = NOT borrow.
8X06 shr vx VX = VX SHR 1 (VX = VX / 2), VF = carry
8XY7 sub vy, vx VX = VY - VX, VF = not borrow (*) (undocumented in original documents)
8XYE shl vx VX = VX SHL 1 (VX = VX * 2), VF = carry
9XY0 skne vx, vy Skip the following instruction if VX <> VY
ANNN mov I, nnn I = NNN
BNNN Jmi NNN Translate program execution to NNN + V0
CXKK Rand vx, kk VX = (random number 0..255) AND KK
DXYN Draw vx, vy, n Draw a sprite of height N (with N = 0 count N = 16) and width 8 along the coordinates (VX, VY) beginning in memory at the address contained in register I. VF = collision.
EX9E Skpr vx Skip the next instruction if the number key in VX is pressed.
EXA1 Skup vx Skip the next instruction if the number key in the VX is not pressed.
FX07 Gdelay vx VX = Delay timer
FX0A Key vx We are waiting for pressing the button and add it to VX.
FX15 Sdelay vx Delay timer = VX
FX18 Ssound vx Sound timer = VX
FX1E Add I, vx I = I + VX
FX29 Font vx Place in I the address of the sprite font 4 x 5 hexadecimal character contained in the VX
FX33 Bcd vx Place the BCD representation of the VX into memory at the addresses I..I + 2. For example, if VX contains 4Fh, then 00h 07h 09h, that is, a decimal representation of 4Fh will be stored in memory
FX55 Save vx Saves the V0 ... VX registers in memory starting at I
FX65 Load vx Loads V0 ... VX registers from memory starting at I
FX75 * Ssave vx Saves V0 ... VX (X <8) to HP48 flags
FX85 * Sload vx Downloads V0 ... VX (X <8) from HP48 flags
* - it means the command is relevant only for SUPER CHIP interpreter.
(*): When VX - VY occurs, VF is set to denial of the loan. This means that if VX is greater than or equal to VY, VF will be set to 01, because loan = 0. If VX is less than VY, VF is set to 00, because loan = 1.
Preparing base sprites
Using a piece of paper and a pen, we sketch the sprites of the font. You’ll get something like this:
Using it, we write the bytes of the value of each sprite.
Principles of emulation
It is believed that all emulation should occur in a loop of approximately this type.
// do emulation
Until quit_pr ;
Here quit_pr is considered a sign of stopping emulation, and can change not only from the outside, but also from the inside of the emulation.
One of the most important points that should be followed is that the frequencies of the equipment and the optimization are respected. If it is incorrect to describe the emulation of even such a simple language like CHIP-8, you can get very low performance and extremely high CPU time, while the correct emulation will provide sufficient performance at minimum cost.
Using Delphi, I decided to develop an emulation of the system as a class and gave it the name "TCpu1802", based on the processor model used within this system. Consider the main procedure for it run.
procedure tcpu1802 . run ;
delaytimer . Enabled : = true ;
soundtimer . Enabled : = true ;
drawtimer . Enabled : = true ;
work : = true ;
if cpumulty <> 0 then
if ( round ( cycle ) mod ( 2 ) ) = 0 then sleep ( cpumulty ) ;
application . ProcessMessages ;
until work = false ;
delaytimer . Enabled : = false ;
soundtimer . Enabled : = false ;
drawtimer . Enabled : = false ;
First, at startup, we activate two standard for CHIP-8 timers and one entered by yourself to redraw the screen. Cycle - a variable containing the current cycle of commands, the call of each command increases the cycle by one. Every second action we will try to suspend the interpreter for cpumulty milliseconds. This is necessary so that no speeding occurs.
Soundtimer and Drawtimer timers have an interval of 17 milliseconds, that is, they operate at a frequency of 58.82 hertz, which is as close as possible to the original.
Drawtimer delay is 10 milliseconds. That is, on average, every 3-4 interpreter commands will be executed. Allocation to the timer is done in order not to clutter up the process of the interpreter execution and allows regular means to perform parallelization of processes.
Let's look at the list of variables that we need to perform the emulation itself:
keycode : byte ; // key pressed code
memory : array [ 0 .. 8191 ] of byte ; // 8 kilobytes of RAM, allocated due to the location of fonts
stack : array [ 0 .. 255 ] of word ; // stack
stacksize : byte ; // current stack expansion
videoarray : array [ 0 .. 2047 ] of boolean ; // video array
mask : array [ 0 .. 2047 ] of boolean ; // mask video array
Vreg : array [ 0 .. 15 ] of byte ; // - registers V0..VF
Ireg : word ; // 2 byte address register
CodeSender : word ; // location of the next opcode in memory
sound : boolean ; // is the sound playing now?
According to the architecture, video memory is located in the same amount of RAM as everything else, however, constant reading and writing there, considering the bytes and analyzing them, would take a lot of processor resources and is impractical, however, assuming that some program can read something directly from there, in case there is a reading from that memory area, we can simply call an additional procedure called mirrorvideomem, which allows writing the current contents of the video memory there. Such a solution is effective, although it causes a slightly larger use of RAM. The mirrorstack procedure is similar.
procedure tcpu1802 . mirrorvideomem ;
i , j : integer ;
tmp : byte ;
if ( Ireg> = $ F00 ) and ( Ireg < = $ FFF ) then
for i : = 0 to 255 do
tmp : = 0 ;
for j : = 0 to 7 do
if videoarray [ i * j ] then tmp : = tmp + 1 ;
tmp : = tmp shl 1 ;
memory [ $ F00 + i ] : = tmp ;
procedure tcpu1802 . mirrorstack ;
n , i : integer ;
if ( Ireg> = $ EA0 ) and ( Ireg < = $ EFF ) then
if stacksize> 12 then n : = 11 else n : = stacksize - 1 ;
for i : = 0 to n do
memory [ $ EFF - i * 2 ] : = stack [ i ] div 256 ;
memory [ $ EFF - i * 2 + 1 ] : = stack [ i ] mod 256 ;
Before writing the processor, you can see that each command is conditionally divided into 4 sections of 4 bits. To work with them, the opcode data type is entered (type opcode = array [0..3] of byte;).
I will not dwell on the conversion and reading of the next data from the memory, however, I will pay attention to the structure of the run procedure that performs the emulation. In order to optimize it, it uses not “if then else”, but “case”, and moreover, a number of commands allow them to be used with the help of an assembler, which further accelerates the work of emulation. Here is an example of such a command:
if op [ 3 ] = 1 then
tmp : = Vreg [ op [ 1 ] ] ;
tmp2 : = Vreg [ op [ 2 ] ] ;
mov ah , [ tmp ]
or ah , [ tmp2 ] ;
mov [ tmp ] , ah ;
Vreg [ op [ 1 ] ] : = tmp ;
used : = true ;
codesender : = codesender + 2 ;
Now, having considered all moments of emulation, you can write your own CHIP-8 emulation. I hope you liked my article and it was useful. Thank you for reading.
Articles used:A CHIP-8 / SCHIP emulator
By David WINTER (HPMANIAC)Wikipedia
Module source:source code
It was for her that I received an invitation from the user nsinreal
, for which I thank him so much.
Please do not consider the “copy-paste” of the article
that appeared an hour before the publication of this one, due to the fact that it could not be sent. Please consider that we have not seen each other's articles, due to the different content of the modules and different approaches to emulation, thanks in advance.