CHIP8 Emulator(2)——動手做!

這兩天擼代碼,看別人的源碼,總算是有了點收穫。除了GLUT部分還不太懂外,其他核心部分都已經搞定。

動手!從哪裏下手?

觀看了前篇的CHIP8介紹,對CHIP8這種語言有了初步的瞭解,現在就是用代碼實現一個CHIP8的虛擬機。參考源碼是用C++寫的,不太熟。我這裏用C語言進行了實現。
下面講講實現流程,這裏主要要實現三個文件:

  • mychip8.c CHIP8實現的核心代碼
  • mychip8.h CHIP8的相關定義
  • main.c 利用GLUT實現圖形/輸入等邏輯

首先我們來通過寫mychip8.h來了解下怎麼用代碼來定義CHIP8:

CHIP8基本變量定義

//變量聲明
unsigned char Gfx[64*32];   //chip8顯存
unsigned char V[16]; //16個寄存器,V0~VF 
unsigned char Memory[4096]; //4K內存
unsigned short int I; //地址寄存器
unsigned short int Pc; //程序指針
unsigned short int Stack[16];//棧
unsigned short int Sp; //棧指針
unsigned char Keyboard[16];//16個鍵值
unsigned short int Opcode; //操作碼
unsigned char Delaytimer;//延時定時器
unsigned char Soundtimer;//聲音定時器
unsigned char DrawFlag; //繪圖標識

上面的這些變量定義都是跟前篇CHIP8介紹有關的,我們在實現一個CHIP8虛擬機時,都要用到上面的變量。

CHIP8用到的幾個函數:

void InitializeChip8(); //CHIP8初始化
void HandleOpcode();    //操作碼處理,核心部分
int  LoadApp(const char *filename); //加載應用(遊戲)

void InitializeChip8()

主要是對上述定義CHIP8變量的初始化,比如顯存/棧/程序指針等初始化。

代碼如下:

void InitializeChip8()
{
    unsigned int i;

    for(i=0;i<15;i++)
    {
        V[i] = 0;       //寄存器清零
        Stack[i] = 0;   //棧清零
        Keyboard[i] = 0;//按鍵清零  
    }

    for(i=0;i<4095;i++)
        Memory[i] = 0;  //內存清零

    for(i=0; i<2048; ++i)
        Gfx[i] = 0;     //顯存清零

    I = 0;              //地址寄存器清零
    Sp = 0;             //棧指針清零
    Pc = 0x200;         //PC指針指向程序開始的地方
    Opcode = 0;         //操作碼清零
    Delaytimer = 0;     //定時器清零
    Soundtimer = 0;

    DrawFlag = 1;   //繪畫標識爲真

    srand(time(NULL)); //產生隨機數種子,後面一個操作碼要用到
}

上面代碼需要注意的就是程序指針初始是指向0x200處的,程序運行代碼開始的地方。

void HandleOpcode()

這個函數中的代碼就是CHIP8實現的關鍵代碼,就是對CHIP8操作碼取碼解碼的實現。

代碼如下:

void HandleOpcode()
{
    int i;

    Opcode = Memory[Pc] << 8 | Memory[Pc+1]; //取操作碼,高位在低地址

    switch(Opcode & 0xF000)
    {
    case 0x0000:
        switch(Opcode & 0x000F)
        {
        case 0x0000:    //Clears the screen.
            for(i=0; i<2048; i++)
                Gfx[i] = 0x0;
            DrawFlag = 1;
            Pc += 2;
            break;
        case 0x000E:    //Returns from a subroutine.
            Sp -= 1;
            Pc = Stack[Sp];
            Pc += 2; //Important!
            break;
        default:
            printf ("Unknown Opcode [0x0000]: 0x%X\n", Opcode); 
        }
        break;

    case 0x1000: // Jumps to address NNN.
        Pc = Opcode & 0x0FFF;
        break;

    case 0x2000: //Calls subroutine at NNN.
        Stack[Sp] = Pc;
        Sp++;
        Pc = Opcode & 0x0FFF;
        break;

    case 0x3000: //Skips the next instruction if VX equals NN.
        if( V[(Opcode&0x0F00) >> 8] == (Opcode&0x00FF))
            Pc += 4;
        else
            Pc += 2;
        break;

    case 0x4000: // Skips the next instruction if VX doesn't equal NN.
        if( V[(Opcode&0x0F00) >> 8] != (Opcode&0x00FF))
            Pc += 4;
        else
            Pc += 2;
        break;

    case 0x5000: //Skips the next instruction if VX equals VY.
        if( V[(Opcode&0x0F00)>>8] == V[(Opcode&0x00F0)>>4])
            Pc += 4;
        else
            Pc += 2;
        break;

    case 0x6000: //Sets VX to NN.
        V[(Opcode&0x0F00) >> 8] = Opcode & 0xFF;
        Pc += 2;
        break;

    case 0x7000:    //  Adds NN to VX.
        V[(Opcode&0x0F00)>>8] += Opcode & 0xFF;
        Pc += 2;
        break;

    case 0x8000:    
        switch(Opcode & 0x000F)
        {
        case 0x0000:    //  Sets VX to the value of VY.
            V[(Opcode&0x0F00)>>8] = V[(Opcode&0x00F0)>>4];
            Pc += 2;
            break;

        case 0x0001:    //Sets VX to VX or VY.
            V[(Opcode&0x0F00)>>8] |= V[(Opcode&0x00F0)>>4];
            Pc += 2;
            break;

        case 0x0002:    //Sets VX to VX and VY.
            V[(Opcode&0x0F00)>>8] &= V[(Opcode&0x00F0)>>4];
            Pc += 2;
            break;

        case 0x0003:    //  Sets VX to VX xor VY.
            V[(Opcode&0x0F00)>>8] ^= V[(Opcode&0x00F0)>>4];
            Pc += 2;
            break;

        case 0x0004:    //Adds VY to VX. VF is set to 1 when there's a carry, and to 0 when there isn't.
            if (V[(Opcode&0x0F00)>>8] + V[(Opcode&0x00F0)>>4] > 255)
                V[15] = 1;
            else
                V[15] = 0;
            V[(Opcode&0x0F00)>>8] += V[(Opcode&0x00F0)>>4];
            Pc += 2;
            break;

        case 0x0005:    //  VY is subtracted from VX. VF is set to 0 when there's a borrow, and 1 when there isn't.
            if(V[(Opcode & 0x00F0) >> 4] > V[(Opcode & 0x0F00) >> 8]) 
                V[15] = 0;
            else
                V[15] = 1;
            V[(Opcode&0x0F00)>>8] -= V[(Opcode&0x00F0)>>4];
            Pc += 2;
            break;

        case 0x0006:    //  Shifts VX right by one. VF is set to the value of the least significant bit of VX before the shift
            V[15] = V[(Opcode&0x0F00)>>8] & 0x0001;
            V[(Opcode&0x0F00)>>8] >>= 1;    
            Pc += 2;
            break;

        case 0x0007:    //  Sets VX to VY minus VX. VF is set to 0 when there's a borrow, and 1 when there isn't.
            if(V[(Opcode & 0x0F00) >> 8] > V[(Opcode & 0x00F0) >> 4])
                V[15] = 0;
            else
                V[15] = 1;
            V[(Opcode&0x0F00)>>8] = V[(Opcode&0x00F0)>>4] - V[(Opcode&0x0F00)>>8];
            Pc += 2;
            break;

        case 0x000E:    //Shifts VX left by one. VF is set to the value of the most significant bit of VX before the shift.
            V[15] = V[(Opcode&0x0F00)>>8] >> 7;
            V[(Opcode&0x0F00)>>8] <<= 1;    
            Pc += 2;
            break;
        default:
            printf ("Unknown Opcode [0x8000]: 0x%X\n", Opcode);
        }
        break;
    case 0x9000:    //  Skips the next instruction if VX doesn't equal VY.
        if (V[(Opcode&0x0F00)>>8] != V[(Opcode&0x00F0)>>4] )
            Pc += 4;
        else
            Pc += 2;
        break;

    case 0xA000:    //Sets I to the address NNN.
        I = Opcode & 0x0FFF;
        Pc += 2;
        break;

    case 0xB000:    //  Jumps to the address NNN plus V0.
        Pc = V[0] + (Opcode & 0x0FFF);
        break;

    case 0xC000:    //Sets VX to a random number, masked by NN.
        V[(Opcode&0x0F00)>>8] = (rand() % 0xFF) & (Opcode & 0xFF); //random
        Pc += 2;
        break;

    case 0xD000:    //  Sprites stored in memory at location in index register (I), maximum 8bits wide. Wraps around the screen. If when drawn, clears a pixel, register VF is set to 1 otherwise it is zero. All drawing is XOR drawing (i.e. it toggles the screen pixels)
    {
        unsigned short x = V[(Opcode & 0x0F00) >> 8];
        unsigned short y = V[(Opcode & 0x00F0) >> 4];
        unsigned short height = Opcode & 0x000F;
        unsigned short pixel;
        unsigned int yline, xline;

        V[15] = 0;
        for (yline = 0; yline < height; yline++)
        {
            pixel = Memory[I + yline];
            for(xline = 0; xline < 8; xline++)
            {
                if((pixel & (0x80 >> xline)) != 0)
                {
                    if(Gfx[(x + xline + ((y + yline) * 64))] == 1)
                    {
                        V[0xF] = 1;                                    
                    }
                    Gfx[x + xline + ((y + yline) * 64)] ^= 1;
                }
            }
        }

        DrawFlag = 1;
        Pc += 2;
    }
    break;

    case 0xE000:
        switch(Opcode & 0x00FF)
        {
        case 0x009E:    //  Skips the next instruction if the key stored in VX is pressed.
            if(Keyboard[V[(Opcode & 0x0F00) >> 8]]!= 0)
                Pc += 4;
            else
                Pc += 2;
            break;
        case 0x00A1:    //Skips the next instruction if the key stored in VX isn't pressed.
            if(Keyboard[V[(Opcode & 0x0F00) >> 8]] == 0)
                Pc += 4;
            else
                Pc += 2;
            break;
        default:
            printf ("Unknown Opcode [0xE000]: 0x%X\n", Opcode);
        }
        break;

    case 0xF000:    
        switch(Opcode & 0xFF)
        {
        case 0x07:  //Sets VX to the value of the delay timer.
            V[(Opcode&0x0F00)>>8] = Delaytimer;
            Pc += 2;
            break;
        case 0x0A:  //A key press is awaited, and then stored in VX.
        {
            unsigned char keyPress = 0;

            for(i = 0; i < 16; ++i)
            {
                if(Keyboard[i] != 0)
                {
                    V[(Opcode & 0x0F00) >> 8] = i;
                    keyPress = 1;
                }
            }
            if(!keyPress)                       
                return;

            Pc += 2;
        }
        break;

        case 0x15:  //Sets the delay timer to VX.
            Delaytimer = V[(Opcode&0x0F00)>>8];
            Pc += 2;
            break;

        case 0x18:  //  Sets the sound timer to VX.
            Soundtimer = V[(Opcode&0x0F00)>>8];
            Pc += 2;
            break;

        case 0x1E:  //Adds VX to I
            if(I + V[(Opcode & 0x0F00) >> 8] > 0xFFF)   
                V[15] = 1;
            else
                V[15] = 0;
            I += V[(Opcode&0x0F00) >> 8];
            Pc += 2;
            break;

        case 0x29:  //Sets I to the location of the sprite for the character in VX. Characters 0-F (in hexadecimal) are represented by a 4x5 font
            I = V[(Opcode&0x0F00)>>8] * 0x5;
            Pc += 2;
            break;

        case 0x33:  //  Stores the Binary-coded decimal representation of VX, with the most significant of three digits at the address in I, the middle digit at I plus 1, and the least significant digit at I plus 2. (In other words, take the decimal representation of VX, place the hundreds digit in memory at location in I, the tens digit at location I+1, and the ones digit at location I+2.)
            Memory[I] = V[(Opcode&0x0F00)>>8] / 100;
            Memory[I+1] = V[(Opcode&0x0F00)>>8] / 10 % 10;
            Memory[I+2] = (V[(Opcode&0x0F00)>>8] % 100) % 10;
            Pc += 2;
            break;

        case 0x55:  //  Stores V0 to VX in memory starting at address I

            for(i=0; i<((Opcode&0x0F00)>>8); i++)
            {
                Memory[I+i] = V[i];

            }
            I += ((Opcode & 0x0F00) >> 8) + 1;
            Pc += 2;
            break;

        case 0x65:  //  Fills V0 to VX with values from memory starting at address I
            for(i=0; i<((Opcode&0x0F00)>>8); i++)
            {
                V[i] = Memory[I+i];
            }
            I += ((Opcode & 0x0F00) >> 8) + 1;
            Pc += 2;
            break;
        default:
                    printf ("Unknown Opcode [0xF000]: 0x%X\n", Opcode);
        }
        break;
        default:
            printf ("Unknown Opcode: 0x%X\n", Opcode);
    }


    if(Delaytimer > 0)
        --Delaytimer;

    if(Soundtimer > 0)
    {
        if(Soundtimer == 1)
            printf("BEEP!\n");
        --Soundtimer;
    }   

}

對上面的代碼有幾個地方需要說明下:

  • 不要忘了PC指針的增加
  • 因爲用到了switch-case語句,所以不要忘了break和default.
  • 比較難理解的操作碼可能就是0xDXYN,事關繪圖。操作碼說明如下:

Sprites stored in memory at location in index register (I), maximum
8bits wide. Wraps around the screen. If when drawn, clears a pixel,
register VF is set to 1 otherwise it is zero. All drawing is XOR
drawing (i.e. it toggles the screen pixels)

CHIP8繪圖是以Sprites來進行的,Sprites是8個像素點來表示。其存放地址已I地址開始進行索引。繪畫主要通過異或運算進行,需要注意的是如果發現某個像素點由1變爲0的畫,則需要設置VF爲1,這是用來進行碰撞檢測的。

int LoadApp(const char *filename)

加載應用主要就是將我們要加載的ROM,這裏是8位遊戲源程序。注意其存放的地址應該放到內存的0x200開始處,也就是512字節後。

代碼如下:

int LoadApp(const char *filename)
{
    long lSize;
    char *buffer=NULL;
    FILE *pFile =NULL;
    size_t result;
    int i;

    InitializeChip8();  

    pFile = fopen(filename, "rb");
    if (pFile == NULL)
    {
        fputs ("File error", stderr);
    }

    fseek(pFile, 0, SEEK_END);//移動指針到文件末尾
    lSize = ftell(pFile);//獲得文件大小
    rewind(pFile);//移動指針到文件開頭
    printf("Filesize: %d\n", (int)lSize);

    buffer = (char*)malloc(sizeof(char)*lSize);
    if (buffer == NULL)
    {
        fputs ("Memory error", stderr); 
        return 0;
    }

    // Copy the file into the buffer
    result = fread (buffer, 1, lSize, pFile);
    if (result != lSize) 
    {
        fputs("Reading error",stderr); 
        return 0;
    }

    // Copy buffer to Chip8 memory
    if((4096-512) > lSize)
    {
        for(i = 0; i < lSize; ++i)
            Memory[i + 512] = buffer[i];
    }
    else
        printf("Error: ROM too big for memory");

    fclose(pFile);
    free(buffer);

    return 1;
}

上面的代碼就是兩個關鍵文件mychip8.c和mychip8.h的實現。剩下的就是main.c實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章