I wrote this document after receiving large amount of email from people who would like to write an emulator of one or another computer, but do not know where to start. Any opinions and advices contained in the following text are mine alone and should not be taken for an absolute truth. The document mainly covers so-called "interpreting" emulators, as opposed to "compiling" ones, because I do not have much experience with recompilation techniques. It does have a pointer or two to the places where you can find information on these techniques.
If you feel that this document is missing something or want to make a correction, feel free to email me your comments. I do not answer to flames, idiocy, and requests for ROM images though. I'm badly missing some important FTP/WWW addresses in the end of this document, so if you know any worth putting there, tell me about it. Same goes for any frequently asked questions you may have, that are not in this document.
So, you decided to write a software emulator? Very well, then this document may be of some help to you. It covers some common technical questions people ask about writing emulators.
Basically, anything which has a microprocessor inside. Of course, only devices running a more or less flexible program are interesting to emulate. Those include:
It is necessary to note that you can emulate any computer system, even if it is very complex (such as Commodore Amiga computer, for example). The perfomance of such an emulation may be very low though.
Emulation is an attempt to imitate the internal design of a device. Simulation is an attempt to imitate functions of a device. For example, a program imitating the Pacman arcade hardware and running real Pacman ROM on it is an emulator. A Pacman game written for your computer but using graphics similar to a real arcade is a simulator.
Although the matter lies in the "gray" area, it appears to be legal to emulate proprietary hardware, as long as the information on it hasn't been obtained by illegal means. You should also be aware of the fact that it is illegal to distribute the system ROMs (BIOS, etc.) with the emulator if the are copyrighted.
There are three basic schemes which can be used for an emulator. They can be combined for the best result.
while(CPUIsRunning) { Fetch OpCode Interpret OpCode }
The pluses of such code include ease of debugging, portability, and ease of synchronization (you can simply count the clock cycles passed and tie the rest of your emulation to the cycle count).
The single, big, and obvious minus is perfomance. The interpretation takes a lot of CPU time, and you may require pretty fast computer to run your code at the decent speed.
In order to write an emulator, you must have a good general knowledge of computer programming and digital electronics. Experience in assembly programming comes very handy too.
The most obvious alternatives are C and Assembly. Here are pros and cons of each of them:
+ Generally, allow to produce faster code. + The emulating CPU registers can be used to directly store the registers of the emulated CPU. + Many opcodes can be emulated with the similar opcodes of the emulating CPU. - The code is not portable, i.e. it can not be run on a computer with different architecture. - It is difficult to debug and maintain the code.
+ The code can be made portable so that it works on different computers and operating systems. + It is relatively easy to debug and maintain the code. + Different hypothesis of how real hardware works can be tested quickly. - C is generally slower than pure assembly code.
Good knowledge of the chosen language is an absolute necessity for writing a working emulator, as it is quite complex project, and your code should be optimized to run as fast as possible. Computer emulation is definitely not one of the projects on which you learn a programming language.
Following is a list of places where you may want to look.
comp.sys.msx MSX/MSX2/MSX2+/TurboR computers comp.sys.sinclair Sinclair ZX80/ZX81/ZXSpectrum/QL comp.sys.apple2 Apple ][ etc.
Please, check the appropriate FAQs before posting to these newsgroups.
Console and Game Programming site in Oulu, Finland
Arcade Videogame Hardware archive at ftp.spies.com
Computer History and Emulation archive at
KOMKON
comp.emulators.misc FAQ
My Homepage
Arcade Emulation Programming
Repository
First of all, if you only need to emulate a standard Z80 or 6502 CPU, you can use one of the CPU emulators I wrote. Certain conditions apply to their usage though.
For those who want to write their own CPU emulation core or interested to know how it works, I provide a skeleton of a typical CPU emulator in C below. In the real emulator, you may want to skip some parts of it and add some others on your own.
Counter=InterruptPeriod; PC=InitialPC; for(;;) { OpCode=Memory[PC++]; Counter-=Cycles[OpCode]; switch(OpCode) { case OpCode1: case OpCode2: ... } if(Counter<=0) { /* check for interrupts and do other hardware emulation here */ ... counter+="InterruptPeriod;" if(exitrequired) break; } }First, we assign initial values to the CPU cycle counter (
Counter
), and the program counter (PC
):
Counter=InterruptPeriod; PC=InitialPC;
TheCounter
contains the number of CPU cycles left to the next suspected interrupt. Note that interrupt should not necessarily occur when this counter expires: you can use it for many other purposes, such as synchronizing timers, or updating scanlines on the screen. More on this later. ThePC
contains the memory address from which our emulated CPU will read its next opcode.
After initial values are assigned, we start the main loop:
for(;;) {
Note that this loop can also be implemented as
while(CPUIsRunning) {
whereCPUIsRunning
is a boolean variable. This has certain advantages, as you can terminate the loop at any moment by settingCPUIsRunning=0
. Unfortunately, checking this variable on every pass takes quite a lot of CPU time, and should be avoided if possible. Also, do not implement this loop as
while(1) {
because in this case, some compilers will generate code checking whether
1
is true or not. You certainly don't want the compiler to
do this unnecessary work on every pass of a loop.
Now, when we are in the loop, the first thing is to read the next opcode, and modify the program counter:
OpCode=Memory[PC++];
While this is the simplest and fastest way to read from the emulated memory, it is not always possible for following reasons:
In these cases, we can read the emulated memory via
ReadMemory()
function:
OpCode=ReadMemory(PC++);
There should also be aWriteMemory()
function to write into emulated memory. Besides handling memory-mapped I/O and pages,WriteMemory()
should also do the following:
ReadMemory()
, it is usually not desirable, as ReadMemory()
gets called much more frequently than WriteMemory()
. Therefore, the more
efficient way would be to implement memory mirroring in the WriteMemory()
function. The ReadMemory()/WriteMemory()
functions usually put a lot of overhead on
the emulation, and must be made as efficient as possible, because they get called very
frequently. Here is an example of these functions:
static inline byte ReadMemory(register word Address) { return(MemoryPage[Address>>13][Address&0x1FFF]); } static inline void WriteMemory(register word Address,register byte Value) { MemoryPage[Address>>13][Address&0x1FFF]=Value; }
Notice theinline
keyword. It will tell compiler to embed the function into the code, instead of making calls to it. If your compiler does not supportinline
or_inline
, try making functionstatic
: some compilers (WatcomC, for example) will optimize short static functions by inlining them.
Also, keep in mind that in most cases the ReadMemory()
is called several
times more frequently than WriteMemory()
. Therefore, it is worth to implement
most of the code in WriteMemory()
, keeping ReadMemory()
as short
and simple as possible.
After the opcode is fetched, we decrease the CPU cycle counter by a number of cycles required for this opcode:
Counter-=Cycles[OpCode];
The Cycles[]
table should contain the number of CPU cycles
for each opcode. Beware that some opcodes (such as conditional
jumps or subroutine calls) may take different number of cycles depending
on their arguments. This can be adjusted later in the code though.
Now comes the time to interpret the opcode and execute it:
switch(OpCode) {
It is a common misconception that theswitch()
construct is inefficient, as it compiles into a chain ofif() ... else if() ...
statements. While this is true for constructs with a small number of cases, the large constructs (100-200 and more cases) always appear to compile into a jump table, which makes them quite efficient.
There are two alternative ways to interpret the opcodes. The first is to make a table
of functions and call an appropriate one. This method appears to be less efficient than a switch()
,
as you get the overhead from function calls. The second method would be to make a table of
labels, and use the goto
statement. While this method is slightly faster than
a switch()
, it will only work on compilers supporting "precomputed
labels". Other compilers will not allow you to create an array of label addresses.
After we successfully interpreted and executed an opcode, the comes a time to check whether we need any interrupts. At this moment, you can also perform any tasks which need to be synchronized with the system clock:
if(Counter<=0) { /* check for interrupts and do other hardware emulation here */ ... counter+="InterruptPeriod;" if(exitrequired) break; }Following is a short list of things which you may want to do in this
if()
statement:
Carefully calculate the number of CPU cycles needed for each task, then use the
smallest number for InterruptPeriod
, and tie all other tasks to it (they
should not necessarily execute on every expiration of the Counter
).
Note that we do not simply assign Counter=InterruptPeriod
, but do a Counter+=InterruptPeriod
:
this makes cycle counting more precise, as there may be some negative number of cycles in
the Counter
.
Also, look at the
if(ExitRequired) break;
line. As it is too costly to check for an exit on every pass of the loop, we do it only when theCounter
expires: this will still exit the emulation when you setExitRequired=1
, but it won't take as much CPU time.
This is about all I have to say about CPU emulation in C. You should be able to figure the rest on your own.
Maintained by Marat Fayzullin [fms@freeflight.com]