这两天撸代码,看别人的源码,总算是有了点收获。除了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实现。