CHIP8 Emulator(2)——动手做!
来源:互联网 发布:6数字域名 编辑:程序博客网 时间:2024/05/21 12:39
这两天撸代码,看别人的源码,总算是有了点收获。除了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实现。
- CHIP8 Emulator(2)——动手做!
- CHIP8 Emulator(1)——CHIP8简介
- CHIP8 Emulator(0)——开始
- 动手做ASP.NET 2.0服务器端控件——AutoCheckTreeView(一)功能讨论
- 动手做ASP.NET 2.0服务器端控件——AutoCheckTreeView(二)接口设计
- Android自定义控件开发系列(一)——第一次动手做自定义控件
- Android自定义控件开发系列(一)——第一次动手做自定义控件
- 动手做rom
- Owin中间件动手做
- 手把手教你编写游戏模拟器 - Chip8篇(2)
- 自已动手写ORM框架(2)——学前班
- 亲身动手做网站SEO
- Bug1——动手太慢
- OpenNETCF的动手实验——WIFI
- 动手制作操作系统——开发环境
- pool(二)——动手入门
- 动手敲算法——前言
- 动手敲算法——快速排序
- fatal error C1189: #error : missing -D__STDC_CONSTANT_MACROS / #define __STDC_CONSTANT_MACROS
- 布尔表达式的验证
- Ternary Expression
- 2015062010 - 英雄
- 15HD_OJ——FatMouse's Speed
- CHIP8 Emulator(2)——动手做!
- Struts2获取Web容器资源的方式 访问request,session
- Cygwin中使用ncurses库
- VS高亮显示无扩展名源码文件
- java基础—IO流——将一些字符写入到指定硬盘上的目录中去:
- 输入输出
- Fragment管理的工具类
- 最大自序和问题
- 为什么基类中的析构函数要声明为虚析构函数?