For my electronics hobby where I make a Z80 computer system, I am building a Z80 in-circuit emulator. The idea is that the physical Z80 chip is removed from the circuit and the emulator is inserted in its socket and emulates the Z80 precisely. Additionally the emulator would implement debug and diagnostic support - but that is not what the question is about. The idea now is that this in-circuit emulator will run inside a PSoC5 module and talk to the PC over USB.
I have currently setup a ginormous state machine in code (C) that is advanced every clock edge change (pos/neg-edge) - twice per clock cycle. I have called this clock-ticks.
The problem is that this state-machine code is becoming unwieldy large and complex.
I have generated structs for every Z80 instruction that contains details on what functions to call for each processing cycle. (a Z80 instruction may take as much as 6 processing (Machine) cycles, which each take at least 3 (usually 4 or more) clock cycles.
Here is an example of a more elaborate instruction that take 4 machine cycles to complete. The strange names are used to encode the attributes of each instruction and generate unique names. During each machine cycle, the appropriate OnClock_Xxxx function is called, multiple times - for each clock tick within that machine cycle.
// ADD IY, SP - ADDIY_SP_FD2 - FD, 39
const InstructionInfo instructionInfoADDIY_SP_FD2 =
{
4,
0,
{
{ 4, OnClock_OF },
{ 4, OnClock_ADDIY_o_FD2_OF },
{ 4, OnClock_ADDIY_o_FD2_OP },
{ 3, OnClock_ADDIY_o_FD2_OP },
{ 0, nullptr },
{ 0, nullptr },
},
{
{ Type_RegistersSP16, {3} },
{ Type_None, {0} },
}
};
References to these instruction information structs are stored in tables for quick lookup during decoding.
I have a global structure that contains the state of the Z80, like clock cycle counting, registers and state used during instruction processing - like operands etc. All code operates on this global state.
To interact with the host (either a unit test or the PSoC5 micro-controller) I have setup a simple interface that controls the pins of the Z80, either requesting input (read the data bus) or output (activate MEMREQ).
To implement the state-machine in code I have used a dirty C-trick that involves jumping in and out of a switch statement, tucked away behind macros. This makes the code readable as normal (but async) code.
Here's an example of what this async state-machine code would look like for the logic to fetch and decode an opcode:
Async_Function(FetchDecode)
{
AssertClock(M1, T1, Level_PosEdge, 1);
setRefresh(Inactive);
setAddressPC();
setM1(Active);
Async_Yield();
_state.Clock.TL++;
AssertClock(M1, T1, Level_NegEdge, 2);
setMemReq(Active);
setRd(Active);
Async_Yield();
NextTCycle();
AssertClock(M1, T2, Level_PosEdge, 3);
// time for some book keeping
if (_state.Instruction.InstructionAddress == 0)
_state.Instruction.InstructionAddress = _state.Registers.PC - 1;
Async_Yield();
_state.Clock.TL++;
AssertClock(M1, T2, Level_NegEdge, 4);
Async_Yield();
NextTCycle();
AssertClock(M1, T3, Level_PosEdge, 5);
_state.Instruction.Data = getDataBus();
setRd(Inactive);
setMemReq(Inactive);
setM1(Inactive);
setAddressIR();
setRefresh(Active);
Async_Yield();
_state.Clock.TL++;
AssertClock(M1, T3, Level_NegEdge, 6);
setMemReq(Active);
Decode();
Async_Yield();
}
Async_End
Async_Yield()
exits the function and a next call to the function will resume execution there.
Ok, now for the question: I have trouble getting the state machine to behave just right, which make me question my line of reasoning about the problem. Because processing the more complex instructions involves a lot more states in the state machine, I find it hard to reason about the code - which is a sign/smell.
Are there any obvious algorithms and/or patterns that one uses for writing this type of clock-cycle accurate emulator?