📄 mz80.txt
字号:
{0x3000, 0x3fff, VideoWrite}, {0x4000, 0x4fff, SpriteWrite}, {(UINT32) -1, (UINT32) -1, NULL}};The above example says that any time a write occurs in the 0x3000-0x3fffrange, call the VideoWrite routine. The same holds true for the SpriteWriteregion as well.NOTE: When your write handler is called, it is passed the address of thewrite and the data that is to be written to it. If your handler doesn'twrite the data to the virtual image, the mz80 internal code will not.NOTE: These routines will *NOT* be called when execution asks for theseaddresses. It will only call them when a particular instruction uses thememory at these locations.If you wish for a region to be RAM, just leave it out of your memory regionexception list. The WriteMemoryByte routine will treat it as read/writeRAM and will write to mz80Base + addr directly.If you wish to protect ROM regions (not often necessary), create a range thatencompasses the ROM image, and have it call a routine that does nothing. Thiswill prevent data from being written back onto the ROM image.Leave your last entry in the table as shown above, with a null handler and0xffffffff-0xffffffff as your read address. Even though the Z80 onlyaddresses 64K of space, the read/write handlers are defined as 32 bit sothe compiler won't pass junk in the upper 16 bits of the address lines. Notonly that, it allows orthoganality for future CPU emulators that may usethese upper bits.You can do a mz80GetContext() if you'd like to read the current context ofthe registers. Note that by the time your handler gets called, the programcounter will be pointing to the *NEXT* instruction.struct MemoryReadByte GameRead[] ={ {0x2000, 0x200f, ReadHandler}, {(UINT32) -1, (UINT32) -1, NULL}};Same story here. If you have a special handler for an attempted read at aparticular address, place its range in this table and create a handlerroutine for it. If you don't define a handler for a particular region, then the ReadMemoryBytein mz80.ASM will actually read the value out of mz80Base + the offset required to complete the instruction.7) Set the intAddr and nmiAddr to the addresses where you want mz80 to start executing when an interrupt or NMI happens. Take a look at the section entitled "INTERRUPTS" below for more information on this.8) Call mz80SetContext() on your Z80 context9) Call mz80Reset(). This will prime the program counter and cause a virtual CPU-wide reset.10) Once you have those defined, you're ready to begin emulation. There's some sort of main loop that you'll want. Maybe something like: while (hit == 0) { if (lastSec != (UINT32) time(0)) { diff = (mz80clockticks - prior) / 3000000; printf("%ld Clockticks, %ld frames, %ld Times original speed\n", MZ80clockticks - prior, frames, diff); frames = 0; prior = mz80clockticks; lastSec = time(0); if (kbhit()) { getch(); hit = 1; } } /* 9000 Cycles per NMI (~3 milliseconds @ 3MHZ) */ dwResult = mz80exec(9000); mz80clockticks += mz80GetElapsedTicks(TRUE); mz80nmi(); /* If the result is not 0x80000000, it's an address where an invalid instruction was hit. */ if (0x80000000 != dwResult) { mz80GetContext(&sCpu1); printf("Invalid instruction at %.2x\n", sCpu1.MZ80pc); exit(1); } }Call mz80exec() With the # of virtual CPU cycles you'd like mz80 toexecute. Be sure to use the mz80GetElapsedTicks() call *AFTER* execution tosee how many virtual CPU cycles it actually executed. For example, if you tellmz80 to execute 500 virtual CPU cycles, it will execute slightly more. Anythingfrom 500 to 524 (24 cycles being the longest any 1 instruction takes in theZ80).Use the mz80GetElapsedTicks() call for more accurate cycle counting. Of course,this is only if you have *NOT* included the -nt option.If you pass FALSE to the mz80GetElapsedTicks() function, the internal CPU elapsed tick clock will not be reset. The elapsed tick counter is something that continues to increase every emulated instruction, and like an odometer,will keep counting unless you pass TRUE to mz80GetElapsedTicks(), of which case it will return you the current value of the elapsed ticks and set it to 0 when complete.NOTE: The bigger value you pass to mz80exec, the greater benefit you get outof the virtual registers persisting within the emulator, and it will runfaster. Pass in a value that is large enough to take advantage of it, butnot so often that you can't handle nmi or int's properly.If you wish to create a virtual NMI, call mz80nmi(), and it will be takenthe next time you call mz80exec, or alternately if you have a handler callmz80nmi/mz80int(), the interrupt will be taken upon return. Note that mz80nmi() doesn't actually execute any code - it only primes the emulator tobegin executing NMI/INT code.NOTE: mz80int() is defined with a UINT32 as a formal parameter. Depending upon what interrupt mode you're executing in (described later), it may or maynot take a value.NMI's can interrupt interrupts, but not the other way around - just like areal Z80. If your program is already in an interrupt, another one will not betaken. The same holds true for an NMI - Just like a real Z80!MUTLI-PROCESSOR NOTES---------------------Doing multi processor support is a bit trickier, but is still fairly straight-forward.For each processor to be emulated, go through steps 1-7 above - giving eachCPU its own memory space, register storage, and read/write handlers.EXECUTION OF MULTI-CPUS:-------------------------When you're ready to execute a given CPU, do the following: mz80SetContext(contextPointer);This will load up all information saved before into the emulator and ready itfor execution. Then execute step 7 above to do your virtual NMI's, interrupts,etc... All CPU state information is saved within a context.When the execution cycle is complete, do the following to save the updatedcontext away for later: mz80GetContext(contextPointer);Give each virtual processor a slice of time to execute. Don't make the valuestoo small or it will spend its time swapping contexts. While this in itselfisn't particularly CPU expensive, the more time you spend executing the better.mz80 Keeps all of the Z80 register in native x86 register (including mostof the flags, HL, BC, and A). If no context swap is needed, then you get theadded advantage of the register storage. For example, let's say you were running two Z80s - one at 2.0MHZ and one at 3.0MHZ. An example like this might be desirable: mz80SetContext(cpu1Context); // Set CPU #1's information mz80exec(2000); // 2000 Instructions for 2.0MHZ CPU mz80GetContext(cpu1Context); // Get CPU #1's state info mz80SetContext(cpu2Context); // Set CPU #2's state information mz80exec(3000); // 3000 Instructions for 3.0MHZ CPU mz80GetContext(cpu2Context); // Get CPU #2's state informationThis isn't entirely realistic, but if you keep the instruction or timingratios between the emulated CPUs even, then timing is a bit more accurate.NOTE: If you need to make a particular CPU give up its own time cycle becauseof a memory read/write, simply trap a particular address (say, a write to aslave processor) and call mz80ReleaseTimeslice(). It will not execute any further instructions, and will give up its timeslice. Put this in your read/write memory trap.NOTE: You are responsible for "holding back" the processor emulator fromrunning too fast.INTERRUPTS----------The Z80 has three interrupt modes: IM 0 - IM 2. Each act differently. Here'sa description of each:IM 0This mode will cause the Z80 to be able to pull a "single byte instruction"off the bus when an interrupt occurs. Since we're not doing bus cycleemulation, it acts identically to mode 1 (described below). The formalparameter to mz80int() is ignored. There is really no point in actually emulating the instruction execution since any instruction that would beexecuted would be a branch instruction!IM 1This mode is the "default" mode that the Z80 (and mz80 for that matter) comesup in. When you call mz80reset(), the interrupt address is set to 38h andthe NMI address is set to 66h. So when you're in IM 1 and mz80int() iscalled, the formal parameter is ignored and the z80intAddr/z80nmiAddr valuesare appropriately loaded into the program counter.IM 2This mode causes the Z80 to read the upper 8 bits from the current valueof the "I" register, and the lower 8 bits from the value passed into mz80int().So, if I contained 35h, and you did an mz80int(0x64), then an interrupt ataddress 3564h would be taken. Simple!OTHER GOODIES-------------MZ80 Has a nice feature for allowing the same handler to handle differentdata regions on a single handler. Here's an example:struct PokeyDataStruct Pokey1;struct PokeyDataStruct Pokey2;struct MemoryWriteByte GameWrite[] ={ {0x1000, 0x100f, PokeyHandler, Pokey1}, {0x1010, 0x101f, PokeyHandler, Pokey2}, {(UINT32) -1, (UINT32) -1, NULL}};void PokeyHandler(UINT32 dwAddr, UINT8 bData, struct sMemoryWriteByte *psMem){ struct PokeyDataStruct *psPokey = psMem->pUserArea; // Do stuff with psPokey here....}This passes in the pointer to the sMemoryWriteByte structure that causedthe handler to be called. The pUserArea is a user defined address that canbe anything. It is not necessary to fill it in with anything or eveninitialize it if the handler doesn't actually use it.This allows a single handler to handle multiple data references. This isparticularly useful when handling sound chip emulation, where there mightbe more than one of a given device. Sure beats having multiple uniquehandlers that are identical with the exception of the data area where itwrites! This allows a good deal of flexibility.The same construct holds for MemoryReadByte, z80PortRead, and z80PortWrite,so all can take advantage of this feature.SHARED MEMORY FEATURES----------------------MZ80 Also has another useful feature for dealing with shared memory regions:UINT8 bSharedRAM[0x100];struct MemoryWriteByte Processor1[] = { {0x1000, 0x10ff, NULL, bSharedRAM}, {(UINT32) -1, (UINT32) -1, NULL}};struct MemoryWriteByte Processor2[] = { {0x1000, 0x10ff, NULL, bSharedRAM}, {(UINT32) -1, (UINT32) -1, NULL}};If the handler address is NULL, mz80 will look at the pUserArea field as apointer to RAM to read from/write to. This comes in extremely handy when youhave an emulation that requires two or more processors writing to the samememory block. And it's lots faster than creating a handler that writes toa common area as well.DEBUGGING---------Several new functions have been added to mz80 that assist the emulatorauthor by providing a standard set of functions for register access:UINT8 mz80SetRegisterValue(void *pContext, UINT32 dwRegister, UINT32 dwValue)This allows setting of any register within the Z80. The register field can beone of the following values (defined in mz80.h): CPUREG_PC CPUREG_Z80_AF CPUREG_Z80_BC CPUREG_Z80_DE CPUREG_Z80_HL CPUREG_Z80_AFPRIME CPUREG_Z80_BCPRIME CPUREG_Z80_DEPRIME CPUREG_Z80_HLPRIME CPUREG_Z80_IX CPUREG_Z80_IY CPUREG_Z80_SP CPUREG_Z80_I CPUREG_Z80_R CPUREG_Z80_A CPUREG_Z80_B CPUREG_Z80_C CPUREG_Z80_D CPUREG_Z80_E CPUREG_Z80_H CPUREG_Z80_L CPUREG_Z80_F CPUREG_Z80_CARRY CPUREG_Z80_NEGATIVE CPUREG_Z80_PARITY CPUREG_Z80_OVERFLOW CPUREG_Z80_HALFCARRY CPUREG_Z80_ZERO CPUREG_Z80_SIGN CPUREG_Z80_IFF1 CPUREG_Z80_IFF2Each individual register's value can be set, including the flags at the end.The only valid values for the flags are 1 and 0. Setting these willautomatically adjust the "F" register. If pContext is NULL, then the registers in the currently active context arechanged. If pContext points to a non-NULL area, that area is assumed to bea CONTEXTMZ80 structure where the new register value will be written.If mz80SetRegisterValue() returns a nonzero value, either the register valueor register is out of range or invalid.UINT32 mz80GetRegisterValue(void *pContext, UINT32 dwRegister)This returns the value of the register given on input (listed above asCPUREG_Z80_xxxxx). Flag values will be 1 or 0.If pContext is NULL, then the registers in the currently active context areread. If pContext points to a non-NULL area, that area is assumed to bea CONTEXTMZ80 structure from which register values are pulled.UINT32 mz80GetRegisterTextValue(void *pContext, UINT32 dwRegister, UINT8 *pbTextArea)This returns the textual representation of the value of a given register.It is a text printable string that can be used in sprintf() statements andthe like. This function is useful because different representations forregisters (like flags) can be a group of 8 flag bytes instead of a singlevalue.On entry, pContext being set to NULL indicates that mz80 should get theregister value from the currently active context. Otherwise, it is assumedto be pointing to a CONTEXTMZ80 structure, which contains the value of theregisters to be read.pbTextArea points to a buffer where the value text can be written. This pointsto a user supplied buffer.On exit, if any nonzero value is encountered, either the register # is outof range or pbTextArea is NULL.UINT8 *mz80GetRegisterName(UINT32 dwRegister)This returns a pointer to the textual name of the register passed in. NULLIs returned if the register index (CPUREG_Z80_xxxx table described above) isout of range. DO NOT MODIFY THE TEXT! It is static data.FINAL NOTES-----------I have debugged MZ80.ASM to the best of my abilities. There might still bea few bugs floating around in it, but I'm not aware of any. I've validatedall instructions (That I could) against a custom built Z80 on an ISA card(that fits in a PC) so I'm quite confident that it works just like a realZ80. If you see any problems, please point them out to me, as I am eager to makemz80 the best emulator that I can. If you have questions, comments, etc... about mz80, please don't hesitateto send me an email. And if you use mz80 in your emulator, I'd love to takea look at your work. If you have special needs, or need implementationspecific hints, feel free to email me, Neil Bradley (neil@synthcom.com). Iwill do my best to help you.Enjoy!Neil Bradleyneil@synthcom.com
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -