Create a console emulator

    Probably, many programmers, if they had not dreamed, at least thought about writing their own emulator of any processor. Perhaps some even experimented with something like the Z80. But not many reached the final implementation of the emulator.



    In this article, I would like to talk about creating a simple CHIP-8 game platform emulator from the distant 70s. Firstly, we will touch on the history, and secondly, because of its simplicity, this platform will make it possible to create a fully functional emulator even for novice programmers.


    the end


    No matter how strange it may be, I’ll start from the end. Here is such a program

    OPTION BINARY; We want a binary file, not an HP48 one.
    ALIGN OFF; And we don't want auto alignement, as some
                    ; data can be made of bytes instead of words.
     
        LD V0, 0
        LD V1, 0
     
    LOOP:
        LD I, LEFT; We draw a left line by default, as the random number
                        ; is 0 or 1. If we suppose that it will be 1, we keep
                        ; drawing the left line. If it is 0, we change register
                        ; I to draw a right line.
     
        RND V2, 1; Load in V2 a 0 ... 1 random number
     
        SE V2, 1; Is it 1? If yes, I still refers to the left line
                        ; bitmap.
     
        LD I, RIGHT; If not, we change I to make it refer the right line
                        ; bitmap.
     
        DRW V0, V1, 4; And we draw the bitmap at V0, V1.
     
        ADD V0, 4; The next bitmap is 4 pixels right. So we update
                        ; V0 to do so.
     
        SE V0, 64; If V0 == 64, we finished drawing a complete line, so we
                        ; skip the jump to LOOP, as we have to update V1 too.
     
        JP LOOP; We did not draw a complete line? So we continue!
     
        LD V0, 0; The first bitmap of each line is located 0, V1.
     
        ADD V1, 4; We update V1. The next line is located 4 pixels doan.
     
        SE V1, 32; Have we drawn all the lines? If yes, V1 == 32.
        JP LOOP; No? So we continue!
     
    FIN: JP FIN; Infinite loop ...
     
    RIGHT:; 4 * 4 bitmap of the left line
     
        DB $ 1 .......
        DB $ .1 ......
        DB $ .. 1 .....
        DB $ ... 1 ....
     
    LEFT:; 4 * 4 bitmap of the right line
                        ; And YES, it is like that ...
        DB $ .. 1 .....
        DB $ .1 ......
        DB $ 1 .......
        DB $ ... 1 ....
     

    occupying 38 bytes and in compiled form that looks like this


    should eventually be executed in our emulator and display about the following picture: We



    ’ve finished the end, we turn to a slightly tedious but necessary theory.

    Architecture


    So what is the CHIP-8 gaming platform? Those in English can read the detailed Wikipedia article , but I’ll try to retell the main points in my own words.

    CHIP-8 is an interpreted programming language created in the mid-70s for COSMAC VIP and Telmac 1800 game consoles. Programs written and compiled for CHIP-8 are run on the consoles themselves in virtual machines. Well, by modern analogy, it's a bit of Java bytecode. I generally advise you to forget at the time of the creation of the emulator that it is an interpreted language, and consider that we emulate an iron platform - a certain processor with its own set of instructions. Further, when I say “prefix”, I will mean CHIP-8.



    Our prefix has a memory, a processor, a video output device, sound, and of course an input device. Let's consider all components in more detail:

    Memory

    The prefix has 4Kb main memory (RAM). The memory starts at offset 200h and ends at offset FFFh, respectively. Why does program memory start at 200h? Everything is very simple - the first 512 bytes of memory in the original consoles is just occupied by the interpreter of the CHIP-8 language in the machine codes of the processor on which the prefix is ​​built.

    Registers

    In CHIP-8 there are sixteen 8-bit data registers with the names V0 ... VF. The VF register is responsible for the carry flag during addition / subtraction operations. The prefix also has a 16-bit address register I.

    The stack

    The stack is used to save the return address when the routine completes. The original version of the set-top box has a stack size of 48 bytes, which corresponds to twelve subroutine attachment levels. As we are not limited in resources, we will use 16 levels of investments. This is what most CHIP-8 emulators do.

    Timers

    There are two 8-bit timers in the set-top box, both of them decrease at a frequency of 60 Hz until they reach zero.
    Delay timer: This timer is used for various delays in games, its value can be read / changed using commands.
    Sound timer: When the timer value is non-zero, a squealing sound is output.

    Input device

    Entering is done using 16 keys. In the original console, the keys have codes from 0h to Fh. If we emulate on a computer, it is most convenient to use the right NumPad part of the keyboard, the one where the numbers 0-9 and NumLock are located. The keys '8', '4', '6', and '2' are usually used to move, although not always so. It depends on the game.

    Graphics and sound

    In our set-top box, the screen resolution is 64x32 pixels, one color (monochrome). The output is implemented using sprites, which always have a width of 8 pixels and can have a length of 1 to 15 pixels. If during drawing the sprite is superimposed on another sprite, then at the overlay point the color is inverted, and the VF (carry flag) register takes the value 1. Otherwise, it takes the value 0.

    As noted above, a nasty squeaking sound is played if the Sound timer value is non-zero. I think we won’t realize sound at all, I don’t like these beeps.

    Commands

    Our processor (actually CHIP-8) has exactly 35 instructions, each instruction always has a length of two bytes. Here I will not retype the table of commands, it is on Wikipedia . You can parse a few examples from there, for example:
    00E0 Clears the screen. - when we meet in the code 00E0, just clear the screen.
    6XNN Sets VX to NN. - Set the VX register to NN. For example, if you met the 635A command, then you need to write the value 5Ah to the V3 register.

    Practice


    From the above, it can be seen that this platform is the best suited to begin studying the principles of emulators. Here we do not have tricky masked and non-masked interrupts, no heaps of peripherals with I / O ports, no complicated timers, and so on. Know, read commands for yourself two bytes from the file, compare them with the opcodes yes and do what is required. And there’s nothing at all for the teams - 35 pieces. There are pitfalls, but where without them? Well then, let's get started. And let's start with the memory.

    It is clear that the first thing we do when starting the emulator is to initialize our virtual machine. That is, clear memory, stack, registers and video memory. As I wrote above, the offset at which we will load our emulated program is 200h. Prior to this, that is, from offset 000h to 1FFh, the original interpreter must be located. In it, among other things, there is a small font that starts with the offset 000h and up to 050h and takes 80 bytes. It can be seen in the source code of my emulator. Yes, I apologize for my French Delphi, but I program on it, do not blame me. For simplicity, I created this structure:

          Display: Array [0..64 * 32-1] of Byte; // video memory
          Memory: Array [0..4095] of Byte; // RAM memory
          stack: Array [0..15] of Word; // stack
          Registers: Array [0..15] of Byte; // registers
          rI: Word = $ 200; // I register
          SP: Byte = 0; // stack counter
          PC: Word = $ 200; // mem offset counter
          delay_timer: Byte = 255; // delay timer;
          sound_timer: Byte = 255; // sound timer;
     


    So, at the beginning we fill all arrays with zeros, then copy the font (Font: array [1..80] of byte) into the Memory array starting from zero and initialize all the values:

    FillChar (Memory, 4096.0); // clear the main memory
    Move (Font, Memory, 80); // copy the font into it at offset 000h
    FillChar (Stack, 16.0); // clear the stack
    FillChar (Registers, 16,0); // reset the registers to zero
     
    rI: = $ 200; // address register I at the beginning of the
    PC program : = $ 200; // array offset
     
    SP: = 0; // stack counter
    delay_timer: = 0; // timers to zeros
    sound_timer: = 0;
     


    Now everything is prepared, you can read the emulated program at offset 200h into memory and take up the interpretation of the codes. Here you will have to remember a little who bits are and how to extract them from bytes and words (word). For simplicity, I created an ExecuteOpcode (opcode: word) procedure, into which the two-byte opcode is passed, interpreted and executed. To understand the meaning, you can check the table of commands from Wikipedia .

    Procedure ExecuteOpcode (opcode: word);
    Begin
        case (op_code and $ F000) shr 12 of // select the first 4 bits of
            $ 00 from the opcode: Begin // the opcode
                      starts from scratch Case op_code and $ 00FF of
                            // This is our opcode 00E0 - clear the screen
                            $ E0: Begin
                                        // We do things, that is, stupidly clear the
                                        exit screen ;
                                  End;
                            // And this is 00EE - exit from the
                            $ EE procedure : Begin
                                        // Restore the address from the stack, jump to it
                                        exit
                                  End;
                      End;
                      // And get here if the opcode started from scratch, but neither E0 nor EE ended
                      // Therefore, we either grapple or display the message Invalid Opcode
                      exit;
                 End; // end check for null opcode
            $ 01: Begin // the first four bits of the opcode are 1 (the opcode started with one)
                       // This is JMP, jump. We jump to the desired
                      PC address : = op_code and $ 0FFF;
                      exit
                 End;
            $ 02: Begin // the first four bits of the opcode are 2 (the opcode started with a two)
                       // Call the subroutine.
                       // increase the stack pointer
                       // push the current address
                       // onto the stack // and poke on the
                 End routine ;
           //
           // This continues until the opcode starts with 7.
           //
     
            $ 08: Begin // the opcode starts with 8. Here you need to look at the last 4 bits
                   case op_code and $ 000F of // last 4 bits of the opcode
                    // mov vx, vy
                    $ 00: Begin
                            // Put the value VY
                            exit in the VX register ;
                         End;
                    // or vx, vy
                    $ 01: Begin
                            // VX = VX or VY
                            exit;
                         End;
                    //
                    // this continues to 0E
                    //
     
                   End; // end of the check of the last 4 bits of the opcode
                   // we get here if Invalid Opcode
                  exit;
                End; // end of check if opcode started at 8
     

    And so on, I think the idea should be more or less clear. While writing the interpreter, you can use stubs for some commands. Now, when we implement the basic processor instructions, it remains to draw an output to the screen and implement the input device. The DXYN command is responsible for displaying on the screen. In the register VX is the coordinate X, in the register VY is the coordinate Y from which we must begin to draw a sprite. Address register I at this time indicates the bitmap image of the sprite. I will not apply the implementation of drawing graphics, I think there should not be any difficulties, especially since you can always see in the source at the end of this post. So is the keyboard.

    Conclusion



    Of course, I could not mention all the details of the implementation in this article. The goal is to simply come up with a thought and show the analysis of the opcodes. If anyone is interested, you can look at my implementation of the emulator on Delphi, or find other implementations of emulators on the Internet. How fashionable to say, thousands of them. Starting from Visual Basic and ending with iron solutions.
    I apologize in advance for my code, I did not put it in order - I poured it as it is. The main interesting file there is hchip.pas, it implements all the emulation.

    There is also a good English-speaking forum EmuTalk, in which a thread dedicated to emulating Chip-8 is specially highlighted .

    The page where you can download probably one of the best chip8 emulators and games for it.

    Anyway, on request in Google “chip-8” you can find everything you need.

    What else can be done? You can slightly modify our emulator to support Super chip-8 instructions and sprites. Yes, much more is possible.

    Have a nice day everyone.

    Also popular now: