skeezix
Internal Development
This comes up once in awhile, so I wrote up this monster blog entry about how emulators work, how to write a CPU emulator, etc.
Hope you enjoy it!
http://www.codejedi.com/cgi-bin/blog.cgi/t....blog?seemore=y
Pasted here:
Coding: How to write a CPU emulator
Tue, 18 Oct 2005
Fairly regularly folks will ask me how a CPU emulator works, or how to write one.. or even the whole shbang - how an emulator (for a game console or arcade machine, say) is written. I'm sorry to report its not really all that hard or mysterious (just tedious and painstaking to do from scratch!) as in this day and age the whole virtual machine (VM) concept, and emulation in general, has gotten pretty widespread and well understood. Its like anything else - a car is pretty magical until you look under the hood and start taking pieces off, and writing an emulator is no different. Let me know if you'd like me to post another article about porting or writing a whole emulator as its too large a topic for just one post.. but here I'll give you the high-level overview of CPU emulation.. the heart/engine of any emulation project.
What is a CPU emulator?
An arcade game, a console game machine, a desktop or mobile computer.. all have a CPU at their core - the brain of the operations. For instance, your desktop PC might have a Intel Pentium III in it, while your good old Commodore Vic-20 would have a variant of the 6502 in it. Each CPU processor has a specific set of specifications it fulfills - it understands certain command sets and executes each command in a very carefully defined way in a specific period of time. For instance, a chip might read memory and see a "ADD 3 + 3" operation there, and perform it in some specific number of "cycles" (time), resulting in some specific set of changes (the result written to memory, or registers in the chip.) A CPU is like a magic 'black box' that performs all the calculations and work in a machine, but without doing all the leg-work.
A CPU emulator or CPU core emulates this functionality. Its a software implementation (some source code) that will ask some other code to get data from emulated memory, and process it, and return the modifications to emulated memory or emulated registers. So while a real 6502 chip fetches data across wires in hardware, an emulated 6502 merely asks a function to 'get me the piece of data that would be across the wire at this address'.
A mailman goes to an address '50 Bloor St' to find 'Mr Smith' there; an emulated mailman might just say 'if I was to go to 50 Bloor, who would I see there?'.
What a CPU emulator is not.
A CPU emulator is a very specific component, that although being very important to a whole emulation system, isn't really all that useful on its own. If you pry open our mythical Vic-20 again, you'll find a whole pile of chips on the motherboard, only one of which is the 6502 CPU. Some of the chips know how to turn digital instructions into sound, or process digital instructions and operate a floppy disk or tape drive, or fetch or put into memory. Theres a lot going on, and en emulator must emulate each component. The CPU emulation is the heart of the machine, but is otherwise just one small piece of the pie.
Component programming
It is common for a CPU core to be written as one distinct unit, just like its real world chip package would be. This is a common programmer trick - to build a software component with as few ties to the outside world as possible. It lends itself to reusable code, saving both time and money down the road as others re-use the component. So the developer establishes a contract from the component to the outside world -- "hey you, world, all you need to use me is some functions to get and put to emulated memory, and handle interrupts for me, and we're good.. okay?" You don't need to care so much, but I mention it because you'll often here some developer say "hey, you using the Musashi core, or the UAE core, or the CaSTaway 68000 CPU?" when talking about making or porting a Sega Genesis (Megadrive) emualator. Why? Because some people like to write the emu from scratch (like a fine pasta sauce), including the CPU emulation (arguably one of the harder or at least more tedious components, especially once you pass the 8-bit era of CPUs), while others want to write the machine emulation without worrying about the CPU specifics, while still others are just porting an existing emulator. These latter two cases may want easier to use CPU emulations, easier to debug ones, or faster ones.. or perhaps dynarec instead of VM implementations. This is why its important to use component coding styles -- so you can be halfway through developing your own 6502 core, and then swap in someone elses and compare notes to see where a parallel run diverges. Find the bugs in both
What kinds are there?
There are numerous 'flavours' of CPU core, and many variations, but to me it really boils down to these three main approaches:
* Virtual machine
* Dynamic recompilation ("dynarec")
* Static recompilation ("statrec")
Most common are the plain old VM CPU cores -- just a software component that emulates the function a CPU would take in a PCB (printed circuit board.) A VM implementation will fetch data from emulated memory, transform it, and write back results into the emulated memory and emulated CPU registers. Less common (and more complex to implement) are both dynarec and statrec emulators - these bad-boys are akin to "JIT" (just in time) Java Virtual Machines - in which the code being run through the emulator is translated into the instructions the local ("real") architecture needs. By example, if you're running a dynarec Super Nintendo emulator on a typical Windows PC, the emulator would sneakily translate the SNES code into Intel x86 code. This is complex, but obviously during the runtime theres no real time emulation going on, so it can be very very fast. Dynamic recompilation (performing this magic translation from emulated CPU to local CPU during runtime) is getting more and more common (especially with the Playstation 1 emulators I'd say), though you do see static recompilators around on occasion. Lastly, a static recompilator performs its translations during off-time (not live during runtime) -- for instance, a developer might write a translator and run it once to convert a specific PSX game to C code for compiling on any platform, or perhaps the statrec translator runs when you select a game and not while the game runs. A dynarec will translate bits and pieces of the code "on demand", while a VM emulator just emulates real time all the time.
Alright, lets get to it -- how do you write a CPU core? How do they work? Whats going on in there?
Theres a lot going on in a CPU emulator, and I'm not going to cover it all here (already a very long posting, no?), but heres the basic cycle. The CPU (emulated or real) will loop through this sequence over and over -- modern, old, they all do it.
* Fetch (get some data from memory)
* Decode (analyze, decompose; figure out what the data means.)
* Process (do something)
* Put (store; put output into memory, registers, flags, whatever.)
* (And other stuff; deal with interupts, serial input, etc.)
In case you're curious, a modern CPU does essentially the same things but with all sorts of cool tricks going on. ie: An old CPU would begin a Fetch after everything else is done and the CPU is back to its 'ready' state, while a modern CPU knows better - rather than have the Fetch circuitry idle while in the Process phase, it fetches something else. So as each phase is passed, a whole new cycle can start.. so you get many more things going on at once. Some modern CPUs will re-order operations as they see them, knowing it can do independant operations faster in some specific order (utilizing more pieces of the CPU at once), or fetching from cache instead of across the wire.. etc etc. Whacky stuff.
Looking a little more closely..
A Fetch is where the CPU figures out what to do next. If its got nothing on the go, it can fetch another operation (at the next 'PC' (program counter) address in memory. For instance, memory might be laid out like this:
ADD into-A from B and C
MOVE A into D
After the ADD is completed, the CPU is idle and so does a Fetch to get the next thing to do.. a MOVE in this case.
A Decode is where the CPU figures out what to do with the just fetched data. For instance, an instruction is not the string "ADD" as it would be wasteful for a computer to figure that out. Instead, while a programmer might write "+", the programmers tools (compiler, assembler, interpreter) might encode it as an "ADD" instruction to the CPU in question. (A 6502 ADD instruction is encoded differently than a Motorola 68000 ADD instruction.) Lastly, when the poor CPU receives a piece of data in binary, it has to figure out whats in it -- it receives a package in the mail, and has to open it up.
For instance, on an 8-bit CPU like the 6502, there are many ADD instructions - one for adding two small memory values together, while another to add a value from memory to seomthing in a specific piece of memory. While to a programmer these might both be "ADD", to the CPU they're very different, since one might require a couple of subsequent Fetches (to get values to add), while the other needs only one (one value from memory, another from a register or specific memory area.) So the CPU receives the ADDxxxx instruction and Decodes it into "this ADD" or "that ADD" so it knows what to do.
Finally, Processing occurs. Now that the CPU knows it has to ADD a couple values, it Fetches the data needed (if needed), and then does the addition. The output from the addition routine in the chips circuitry has a few outputs -- depending on the operation it might stuff the result of the ADD into a register or into some piece of programmer specified memory. Additionally, the CPU will set some 'flags' (like on a mailbox) so you can find out what may have occurred - do we overflow? is the result value now a zero? Just some handy bits the programmer can refer to later (such as if to say, "if we overflowed, jump over to this other code so we can react accordingly rather than just crash the machine.")
Lastly, a Put occurs, to store the values into the various places the instruction suggests (during its decode phase.) This is what gets us the goods. Whew!
And this is done in code, how? Here is how RAM is emulated.
Glad you asked!
First, lets put together a really wimpy emulation of a small bank of RAM. To keep it simple, lets refer to it as an array with some access functions (to keep OOP folks from frothing at the mouth, and because it gives us that well defined contract piece software components I wrote about above.)
// A wimped-out memory bank emulator:
typedef unsigned char UInt8;
typedef unsigned short int UInt16;
UInt8 ram_bank [ 1024 ]; // should memset to all 'zero'
UInt8 fetch_ram ( UInt16 address ) {
if ( address < 1024 ) {
return ( ram_bank [ address ] );
}
// if we get here, an error has occured!
return ( 0 );
}
void store_ram ( UInt16 address, UInt8 value ) {
if ( address < 1024 ) {
ram_bank [ address ] = value;
return;
}
// if we get here, an error has occured!
return;
}
This is pretty short and pretty naive, but something very much like this is in most emulators - this piece of code allows for a simple 1K buffer of memory and protects access to it, letting you read only from within the bank, or write to within the bank. It could be 1K or 1MB.. nomatter. Of course, I'm using UInt16 for the address range so it can only handle 64k at a time, but more modern machines would use UInt32 say.
A more complex emulation would return special values when certain memory addresses are read from (as in the motherboard, certain addresses are wired to return (for example) the pixel colour at a certain location, while other addresses are just pure read-write-RAM. Also, some writes could be captured in order to perform certain things in the emulated hardware, such as display a pixel on the emulated screen somewhere. So you could easily modify the fetch_ram() function thusly:
UInt8 fetch_ram ( UInt16 address ) {
if ( address >= 838 && address <= 848 ) {
return ( audio_system_volume );
} else if ( address < 1024 ) {
return ( ram_bank [ address ] );
}
// if we get here, an error has occured!
return ( 0 );
}
This works fine and is quite common, though a more organized approach has come into vogue (years and years ago, as most of these ideas were worked out by devs in the early mid-1990's.):
typedef struct {
UInt16 begin;
UInt16 end;
FuncPtr handler;
UInt32 mydata;
} handler_t;
handler_t fetch_regions[] = {
{ 838, 848, fetch_audio_system_volume, 1 /* audio chip 1 */ },
{ 0, 1024, fetch_any_ram, 0 /* no special data */ },
{ 0, 0xFFFF, NULL /* error */, 0 /* no special data */}
};
In this setup, an array is used to define the various regions in memory, rather than if-statements. This is easier to manage for the developer, since the CPU core would just use a while() or for() loop over the array and use the first matching "from -> to" handler for the memory region its looking for. Further, if one was nutty, the CPU core emulator could re-sort the array contents to keep most-used regions at the top for faster access etc.. but thats not generally bothered with. Still, an array is easier to read then 10 screenfulls of if-statements!
NOTE: A "store" memory handler is pretty much the same; rather than report on values in a memory region, they store into memory regions and react accordingly.
Still with me? Finally - how a CPU emulator is written.
OKay, I know this has been long and complex, but you're nearly there Above I've shown how basic RAM is emulated. Its similar in a Java virtual machine, so this is actually useful to know.. sure, really
Remember the basic cycles an old CPU goes through -- Fetch, Decode, Process, Store. They're directly represented in code:
UInt16 PC = 0; // the 'program counter' - the current instruction we look at
UInt16 reg_A, reg_B; // some registers - internal CPU variable holders
UInt8 flag_zero; // some flags - internal CPU flags
typedef enum {
ADD = 1,
SUB = 2,
MUL = 3,
CMP = 4 // compare
} opcode_t;
// pass in the number of cycles to run; you could pass a big number to
// let the CPU emulator run all day, but its usually cool to pass in a
// low number.. so as the number of cycles one 'frame' of the display
// would take, so that when the CPU core returns, you can draw the
// screen. For instance, if a CPU runs at 5MHz (5000000 cycles per second),
// then you might invoke as "CPU_main_loop ( 5000000 / 60 )" to let it
// run for 1/60th of an emulated second.
void CPU_main_loop ( UInt16 cycles_to_run ) {
// run the number of cycles we were told
while ( cycles_to_run ) {
// FETCH some data (opcode + junk) from emulated RAM
UInt8 data = fetch_ram ( PC );
PC++; // move PC to next address, so next fetch gets the
// next instruction or piece of data
// DECODE it - in this fake CPU emulator, I'll assume the
// bottom 5-bits (out of 8-bits in a byte!) are used for the
// opcode (the real instruction), while the other 3 bits are
// some encoded data for that opcode. ("addressing mode" and
// such.)
UInt8 opcode = data & 0x1F;
UInt8 mode = data >> 5; // shift down to knock off the opcode bits
// decrement our cycles-to-run counter
cycles_to_run--;
// PROCESS the opcode; now that we know which opcode it is,
// lets do something with it. STORE the results right in the
// opcode handler.
if ( opcode == ADD ) {
if ( mode == memory ) {
// In this fake ADD mode we're implementing, the opcode wants
// to get the addresses to find the values to add, so we first
// fetch the addresses (which costs a program cycle (PC) to
// do!) and then add the values!
UInt16 address_a = fetch_ram ( PC ++ );
cycles_to_run--;
UInt16 address_b = fetch_ram ( PC ++ );
cycles_to_run--;
UInt16 value_a = fetch_ram ( address_a );
cycles_to_run--;
UInt16 value_b = fetch_ram ( address_b );
cycles_to_run--;
// In this emulated fake instruction we return the result
// in a register. It could do a memory put (with associated
// cost top the cycles_to_run) - you'd have to check the
// CPU specification for the CPU you're emulating.
reg_A = value_a + value_b;
// set any flags we need to set in the emulated CPU; there
// is opcodes to test these flag values and react accordingly!
if ( reg_A == 0 ) {
flag_zero = 1;
} else {
flag_zero = 0;
}
} else if ( mode == registers ) {
// perform an ADD instruction, like above, but working in
// some other method; perhaps add two registers together
// without fetching.. as you can see, this would be faster
// without having to hit the system bus to get addresses or
// values stored there.
// Obviously, each CPU has a different set of opcodes and
// addressing modes and other things encoded in the 'data'
// first fetch above, so when writing a CPU emulator you have
// to be careful to implement the real hardware spec very
// carefully.
// As you can see, theres a lot going on in each 'opcode',
// which is why timing is usually buggered up in a CPU
// emulator for a few versions
// ... assorted code
// ... at the end, sotre to ram instead of register for this
// example opcode. reg_B has been filled in "..." above with
// an address to store to, and value has been calculed from
// registers or memory..
store_ram ( reg_B, value );
}
} else if ( opcode == SUB ) {
// implement the subtraction series of opcodes
} else if ( opcode == CMP ) {
// implement the comparison series of opcodes
}
} // while
}
Whew; I tried to comment that so you can follow whats going on without writing up a paragraph per line, so let me know if I need to break it up more.
Of course, there are lots of tricks you can do to make this easier to read -- handle read or write arrays (as discussed above, or leave that in the fetch_mem and put_mem functions.), handle endianness of your emulated CPU to your local CPU, etc etc.
For example, you can see that an 8-bit CPU could well have a full 256 opcodes available to it. Usually some 10 of them or so could be ADD variations, with the 'mode' bits defining which specific ADD they are. So you might have 256 different 'instructions', of which theres really only 20 'opcodes' with a few 'modes' each. If this is true, you might have 20 if-statements, each with an if-statement to handle by mode.. lots of if-statements. And if you recall your coding, each if-statement is evaluated until one matches.. so if all your opcodes happen to be the last of the 20, then your emulation is 20 times slower than it could be.. right off the top, without even getting into the emulation!
This is easily remedied. Some folks make a separate function for each opcode, and then make an array of all 256 possible values, with function pointers to the appropriate opcode handler function. Then each opcode is evaluated with the same 'cost' - look up function in array (fast), and call the function to handle it (relatively fast.) Still, it'd be nice to avoid all that function calling jazz, since when your real CPU sets up a funciton call, and returns from one, theres lots of 'business' going on.
So a common approach is to, instead of setting up a separate function for each opcode, a separate 'goto' or 'case' label, and then do a 'switch' on the opcode or data, and 'goto' or 'case' to the right piece of code. The compiler/assemble can do wonders with this, and you avoid all those if-statements and function-calls.
Back in the day, I implemented one CPU emulator _entirely_ without function calls.. it was all switch-case and if-goto, and it was seriosuly goddamned fast (for a Intel 486 chip
Whew.
So there you have it. Probably my longest blog entry yet. Hope you found it entertaining.. comments welcome!
--
jeff
Hope you enjoy it!
http://www.codejedi.com/cgi-bin/blog.cgi/t....blog?seemore=y
Pasted here:
Coding: How to write a CPU emulator
Tue, 18 Oct 2005
Fairly regularly folks will ask me how a CPU emulator works, or how to write one.. or even the whole shbang - how an emulator (for a game console or arcade machine, say) is written. I'm sorry to report its not really all that hard or mysterious (just tedious and painstaking to do from scratch!) as in this day and age the whole virtual machine (VM) concept, and emulation in general, has gotten pretty widespread and well understood. Its like anything else - a car is pretty magical until you look under the hood and start taking pieces off, and writing an emulator is no different. Let me know if you'd like me to post another article about porting or writing a whole emulator as its too large a topic for just one post.. but here I'll give you the high-level overview of CPU emulation.. the heart/engine of any emulation project.
What is a CPU emulator?
An arcade game, a console game machine, a desktop or mobile computer.. all have a CPU at their core - the brain of the operations. For instance, your desktop PC might have a Intel Pentium III in it, while your good old Commodore Vic-20 would have a variant of the 6502 in it. Each CPU processor has a specific set of specifications it fulfills - it understands certain command sets and executes each command in a very carefully defined way in a specific period of time. For instance, a chip might read memory and see a "ADD 3 + 3" operation there, and perform it in some specific number of "cycles" (time), resulting in some specific set of changes (the result written to memory, or registers in the chip.) A CPU is like a magic 'black box' that performs all the calculations and work in a machine, but without doing all the leg-work.
A CPU emulator or CPU core emulates this functionality. Its a software implementation (some source code) that will ask some other code to get data from emulated memory, and process it, and return the modifications to emulated memory or emulated registers. So while a real 6502 chip fetches data across wires in hardware, an emulated 6502 merely asks a function to 'get me the piece of data that would be across the wire at this address'.
A mailman goes to an address '50 Bloor St' to find 'Mr Smith' there; an emulated mailman might just say 'if I was to go to 50 Bloor, who would I see there?'.
What a CPU emulator is not.
A CPU emulator is a very specific component, that although being very important to a whole emulation system, isn't really all that useful on its own. If you pry open our mythical Vic-20 again, you'll find a whole pile of chips on the motherboard, only one of which is the 6502 CPU. Some of the chips know how to turn digital instructions into sound, or process digital instructions and operate a floppy disk or tape drive, or fetch or put into memory. Theres a lot going on, and en emulator must emulate each component. The CPU emulation is the heart of the machine, but is otherwise just one small piece of the pie.
Component programming
It is common for a CPU core to be written as one distinct unit, just like its real world chip package would be. This is a common programmer trick - to build a software component with as few ties to the outside world as possible. It lends itself to reusable code, saving both time and money down the road as others re-use the component. So the developer establishes a contract from the component to the outside world -- "hey you, world, all you need to use me is some functions to get and put to emulated memory, and handle interrupts for me, and we're good.. okay?" You don't need to care so much, but I mention it because you'll often here some developer say "hey, you using the Musashi core, or the UAE core, or the CaSTaway 68000 CPU?" when talking about making or porting a Sega Genesis (Megadrive) emualator. Why? Because some people like to write the emu from scratch (like a fine pasta sauce), including the CPU emulation (arguably one of the harder or at least more tedious components, especially once you pass the 8-bit era of CPUs), while others want to write the machine emulation without worrying about the CPU specifics, while still others are just porting an existing emulator. These latter two cases may want easier to use CPU emulations, easier to debug ones, or faster ones.. or perhaps dynarec instead of VM implementations. This is why its important to use component coding styles -- so you can be halfway through developing your own 6502 core, and then swap in someone elses and compare notes to see where a parallel run diverges. Find the bugs in both
What kinds are there?
There are numerous 'flavours' of CPU core, and many variations, but to me it really boils down to these three main approaches:
* Virtual machine
* Dynamic recompilation ("dynarec")
* Static recompilation ("statrec")
Most common are the plain old VM CPU cores -- just a software component that emulates the function a CPU would take in a PCB (printed circuit board.) A VM implementation will fetch data from emulated memory, transform it, and write back results into the emulated memory and emulated CPU registers. Less common (and more complex to implement) are both dynarec and statrec emulators - these bad-boys are akin to "JIT" (just in time) Java Virtual Machines - in which the code being run through the emulator is translated into the instructions the local ("real") architecture needs. By example, if you're running a dynarec Super Nintendo emulator on a typical Windows PC, the emulator would sneakily translate the SNES code into Intel x86 code. This is complex, but obviously during the runtime theres no real time emulation going on, so it can be very very fast. Dynamic recompilation (performing this magic translation from emulated CPU to local CPU during runtime) is getting more and more common (especially with the Playstation 1 emulators I'd say), though you do see static recompilators around on occasion. Lastly, a static recompilator performs its translations during off-time (not live during runtime) -- for instance, a developer might write a translator and run it once to convert a specific PSX game to C code for compiling on any platform, or perhaps the statrec translator runs when you select a game and not while the game runs. A dynarec will translate bits and pieces of the code "on demand", while a VM emulator just emulates real time all the time.
Alright, lets get to it -- how do you write a CPU core? How do they work? Whats going on in there?
Theres a lot going on in a CPU emulator, and I'm not going to cover it all here (already a very long posting, no?), but heres the basic cycle. The CPU (emulated or real) will loop through this sequence over and over -- modern, old, they all do it.
* Fetch (get some data from memory)
* Decode (analyze, decompose; figure out what the data means.)
* Process (do something)
* Put (store; put output into memory, registers, flags, whatever.)
* (And other stuff; deal with interupts, serial input, etc.)
In case you're curious, a modern CPU does essentially the same things but with all sorts of cool tricks going on. ie: An old CPU would begin a Fetch after everything else is done and the CPU is back to its 'ready' state, while a modern CPU knows better - rather than have the Fetch circuitry idle while in the Process phase, it fetches something else. So as each phase is passed, a whole new cycle can start.. so you get many more things going on at once. Some modern CPUs will re-order operations as they see them, knowing it can do independant operations faster in some specific order (utilizing more pieces of the CPU at once), or fetching from cache instead of across the wire.. etc etc. Whacky stuff.
Looking a little more closely..
A Fetch is where the CPU figures out what to do next. If its got nothing on the go, it can fetch another operation (at the next 'PC' (program counter) address in memory. For instance, memory might be laid out like this:
ADD into-A from B and C
MOVE A into D
After the ADD is completed, the CPU is idle and so does a Fetch to get the next thing to do.. a MOVE in this case.
A Decode is where the CPU figures out what to do with the just fetched data. For instance, an instruction is not the string "ADD" as it would be wasteful for a computer to figure that out. Instead, while a programmer might write "+", the programmers tools (compiler, assembler, interpreter) might encode it as an "ADD" instruction to the CPU in question. (A 6502 ADD instruction is encoded differently than a Motorola 68000 ADD instruction.) Lastly, when the poor CPU receives a piece of data in binary, it has to figure out whats in it -- it receives a package in the mail, and has to open it up.
For instance, on an 8-bit CPU like the 6502, there are many ADD instructions - one for adding two small memory values together, while another to add a value from memory to seomthing in a specific piece of memory. While to a programmer these might both be "ADD", to the CPU they're very different, since one might require a couple of subsequent Fetches (to get values to add), while the other needs only one (one value from memory, another from a register or specific memory area.) So the CPU receives the ADDxxxx instruction and Decodes it into "this ADD" or "that ADD" so it knows what to do.
Finally, Processing occurs. Now that the CPU knows it has to ADD a couple values, it Fetches the data needed (if needed), and then does the addition. The output from the addition routine in the chips circuitry has a few outputs -- depending on the operation it might stuff the result of the ADD into a register or into some piece of programmer specified memory. Additionally, the CPU will set some 'flags' (like on a mailbox) so you can find out what may have occurred - do we overflow? is the result value now a zero? Just some handy bits the programmer can refer to later (such as if to say, "if we overflowed, jump over to this other code so we can react accordingly rather than just crash the machine.")
Lastly, a Put occurs, to store the values into the various places the instruction suggests (during its decode phase.) This is what gets us the goods. Whew!
And this is done in code, how? Here is how RAM is emulated.
Glad you asked!
First, lets put together a really wimpy emulation of a small bank of RAM. To keep it simple, lets refer to it as an array with some access functions (to keep OOP folks from frothing at the mouth, and because it gives us that well defined contract piece software components I wrote about above.)
// A wimped-out memory bank emulator:
typedef unsigned char UInt8;
typedef unsigned short int UInt16;
UInt8 ram_bank [ 1024 ]; // should memset to all 'zero'
UInt8 fetch_ram ( UInt16 address ) {
if ( address < 1024 ) {
return ( ram_bank [ address ] );
}
// if we get here, an error has occured!
return ( 0 );
}
void store_ram ( UInt16 address, UInt8 value ) {
if ( address < 1024 ) {
ram_bank [ address ] = value;
return;
}
// if we get here, an error has occured!
return;
}
This is pretty short and pretty naive, but something very much like this is in most emulators - this piece of code allows for a simple 1K buffer of memory and protects access to it, letting you read only from within the bank, or write to within the bank. It could be 1K or 1MB.. nomatter. Of course, I'm using UInt16 for the address range so it can only handle 64k at a time, but more modern machines would use UInt32 say.
A more complex emulation would return special values when certain memory addresses are read from (as in the motherboard, certain addresses are wired to return (for example) the pixel colour at a certain location, while other addresses are just pure read-write-RAM. Also, some writes could be captured in order to perform certain things in the emulated hardware, such as display a pixel on the emulated screen somewhere. So you could easily modify the fetch_ram() function thusly:
UInt8 fetch_ram ( UInt16 address ) {
if ( address >= 838 && address <= 848 ) {
return ( audio_system_volume );
} else if ( address < 1024 ) {
return ( ram_bank [ address ] );
}
// if we get here, an error has occured!
return ( 0 );
}
This works fine and is quite common, though a more organized approach has come into vogue (years and years ago, as most of these ideas were worked out by devs in the early mid-1990's.):
typedef struct {
UInt16 begin;
UInt16 end;
FuncPtr handler;
UInt32 mydata;
} handler_t;
handler_t fetch_regions[] = {
{ 838, 848, fetch_audio_system_volume, 1 /* audio chip 1 */ },
{ 0, 1024, fetch_any_ram, 0 /* no special data */ },
{ 0, 0xFFFF, NULL /* error */, 0 /* no special data */}
};
In this setup, an array is used to define the various regions in memory, rather than if-statements. This is easier to manage for the developer, since the CPU core would just use a while() or for() loop over the array and use the first matching "from -> to" handler for the memory region its looking for. Further, if one was nutty, the CPU core emulator could re-sort the array contents to keep most-used regions at the top for faster access etc.. but thats not generally bothered with. Still, an array is easier to read then 10 screenfulls of if-statements!
NOTE: A "store" memory handler is pretty much the same; rather than report on values in a memory region, they store into memory regions and react accordingly.
Still with me? Finally - how a CPU emulator is written.
OKay, I know this has been long and complex, but you're nearly there Above I've shown how basic RAM is emulated. Its similar in a Java virtual machine, so this is actually useful to know.. sure, really
Remember the basic cycles an old CPU goes through -- Fetch, Decode, Process, Store. They're directly represented in code:
UInt16 PC = 0; // the 'program counter' - the current instruction we look at
UInt16 reg_A, reg_B; // some registers - internal CPU variable holders
UInt8 flag_zero; // some flags - internal CPU flags
typedef enum {
ADD = 1,
SUB = 2,
MUL = 3,
CMP = 4 // compare
} opcode_t;
// pass in the number of cycles to run; you could pass a big number to
// let the CPU emulator run all day, but its usually cool to pass in a
// low number.. so as the number of cycles one 'frame' of the display
// would take, so that when the CPU core returns, you can draw the
// screen. For instance, if a CPU runs at 5MHz (5000000 cycles per second),
// then you might invoke as "CPU_main_loop ( 5000000 / 60 )" to let it
// run for 1/60th of an emulated second.
void CPU_main_loop ( UInt16 cycles_to_run ) {
// run the number of cycles we were told
while ( cycles_to_run ) {
// FETCH some data (opcode + junk) from emulated RAM
UInt8 data = fetch_ram ( PC );
PC++; // move PC to next address, so next fetch gets the
// next instruction or piece of data
// DECODE it - in this fake CPU emulator, I'll assume the
// bottom 5-bits (out of 8-bits in a byte!) are used for the
// opcode (the real instruction), while the other 3 bits are
// some encoded data for that opcode. ("addressing mode" and
// such.)
UInt8 opcode = data & 0x1F;
UInt8 mode = data >> 5; // shift down to knock off the opcode bits
// decrement our cycles-to-run counter
cycles_to_run--;
// PROCESS the opcode; now that we know which opcode it is,
// lets do something with it. STORE the results right in the
// opcode handler.
if ( opcode == ADD ) {
if ( mode == memory ) {
// In this fake ADD mode we're implementing, the opcode wants
// to get the addresses to find the values to add, so we first
// fetch the addresses (which costs a program cycle (PC) to
// do!) and then add the values!
UInt16 address_a = fetch_ram ( PC ++ );
cycles_to_run--;
UInt16 address_b = fetch_ram ( PC ++ );
cycles_to_run--;
UInt16 value_a = fetch_ram ( address_a );
cycles_to_run--;
UInt16 value_b = fetch_ram ( address_b );
cycles_to_run--;
// In this emulated fake instruction we return the result
// in a register. It could do a memory put (with associated
// cost top the cycles_to_run) - you'd have to check the
// CPU specification for the CPU you're emulating.
reg_A = value_a + value_b;
// set any flags we need to set in the emulated CPU; there
// is opcodes to test these flag values and react accordingly!
if ( reg_A == 0 ) {
flag_zero = 1;
} else {
flag_zero = 0;
}
} else if ( mode == registers ) {
// perform an ADD instruction, like above, but working in
// some other method; perhaps add two registers together
// without fetching.. as you can see, this would be faster
// without having to hit the system bus to get addresses or
// values stored there.
// Obviously, each CPU has a different set of opcodes and
// addressing modes and other things encoded in the 'data'
// first fetch above, so when writing a CPU emulator you have
// to be careful to implement the real hardware spec very
// carefully.
// As you can see, theres a lot going on in each 'opcode',
// which is why timing is usually buggered up in a CPU
// emulator for a few versions
// ... assorted code
// ... at the end, sotre to ram instead of register for this
// example opcode. reg_B has been filled in "..." above with
// an address to store to, and value has been calculed from
// registers or memory..
store_ram ( reg_B, value );
}
} else if ( opcode == SUB ) {
// implement the subtraction series of opcodes
} else if ( opcode == CMP ) {
// implement the comparison series of opcodes
}
} // while
}
Whew; I tried to comment that so you can follow whats going on without writing up a paragraph per line, so let me know if I need to break it up more.
Of course, there are lots of tricks you can do to make this easier to read -- handle read or write arrays (as discussed above, or leave that in the fetch_mem and put_mem functions.), handle endianness of your emulated CPU to your local CPU, etc etc.
For example, you can see that an 8-bit CPU could well have a full 256 opcodes available to it. Usually some 10 of them or so could be ADD variations, with the 'mode' bits defining which specific ADD they are. So you might have 256 different 'instructions', of which theres really only 20 'opcodes' with a few 'modes' each. If this is true, you might have 20 if-statements, each with an if-statement to handle by mode.. lots of if-statements. And if you recall your coding, each if-statement is evaluated until one matches.. so if all your opcodes happen to be the last of the 20, then your emulation is 20 times slower than it could be.. right off the top, without even getting into the emulation!
This is easily remedied. Some folks make a separate function for each opcode, and then make an array of all 256 possible values, with function pointers to the appropriate opcode handler function. Then each opcode is evaluated with the same 'cost' - look up function in array (fast), and call the function to handle it (relatively fast.) Still, it'd be nice to avoid all that function calling jazz, since when your real CPU sets up a funciton call, and returns from one, theres lots of 'business' going on.
So a common approach is to, instead of setting up a separate function for each opcode, a separate 'goto' or 'case' label, and then do a 'switch' on the opcode or data, and 'goto' or 'case' to the right piece of code. The compiler/assemble can do wonders with this, and you avoid all those if-statements and function-calls.
Back in the day, I implemented one CPU emulator _entirely_ without function calls.. it was all switch-case and if-goto, and it was seriosuly goddamned fast (for a Intel 486 chip
Whew.
So there you have it. Probably my longest blog entry yet. Hope you found it entertaining.. comments welcome!
--
jeff