对于我制作Z80计算机系统的电子爱好,我正在构建Z80在线仿真器。这个想法是将物理Z80芯片从电路中移除,并将仿真器插入其插槽中,并精确地仿真Z80。此外,仿真器将实现调试和诊断支持-但这不是问题所在。现在的想法是,该在线仿真器将在PSoC5模块内运行并通过USB与PC通讯。
I have currently setup a ginormous state machine in code (C)在每个时钟边沿更改(pos / neg-edge)时提前-每个时钟周期两次。我称此为时钟滴答声。
问题在于,这种状态机代码变得庞大而复杂。
我为每个Z80指令生成了结构,其中包含有关每个处理周期要调用的函数的详细信息。 (一条Z80指令最多可能需要6个处理(机器)周期,每个周期至少需要3个(通常是4个或更多)时钟周期。
这是一个更复杂的指令示例,需要4个机器周期才能完成。奇怪的名称用于编码每条指令的属性并生成唯一的名称。在每个机器周期内,都会多次调用相应的OnClock_Xxxx函数-对于该机器周期内的每个时钟滴答。
// 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} },
}
};
这些指令信息结构的引用存储在表中,以便在解码期间快速查找。
我有一个包含Z80状态的全局结构,例如时钟周期计数,寄存器和指令处理期间使用的状态-如操作数等。所有代码都在此全局状态下运行。
为了与主机(a unit test或PSoC5 micro-controller)进行交互,我设置了一个简单的接口来控制Z80的引脚,以请求输入(读取数据总线)或输出(激活MEMREQ)。
要在代码I have used a dirty C-trick中实现状态机,该代码涉及跳入和跳出隐藏在宏后面的switch语句。
这使代码像普通(但异步)代码一样可读。
对于获取和解码操作码的逻辑,此异步状态机代码的形式为Here's an example:
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()
退出该函数,对该函数的下一次调用将在此处继续执行。好的,现在是这个问题:
我很难使状态机运行正常,这使我对问题的推理方式提出了质疑。因为处理更复杂的指令会在状态机中涉及更多状态,所以我发现很难推理出代码-这是一个符号/气味。
是否有任何明显的算法和/或模式可用于编写这种时钟周期精确仿真器?
最佳答案
我已经写了两次类似的代码,假设这意味着我知道任何东西,并且分别实现了6502和68000的相似模拟。
我认为主要的提示是:潜在的机器周期只有很少的数目,并且无论所涉及的指令如何,它们都呈现相同的总线活动(不包括数据线)。这意味着您可以通过在运行时使用额外的间接级别,或者通过自动代码构造来避免冗长且难以维护的代码-我倾向于仅依靠预处理器,而其他人则编写了构造代码的代码。
所以与其说出详尽的推文,不如将其紧凑地描述为:
这里有一个基本的假设:您假设您可以在标准机器周期之间以零时间单位递减堆栈指针。但是采用该小说的好处是能够在两者之间使用标准机器周期。
如果遵循此实现方式,您可能最终会陷入类似以下的循环:
MicroOp *next_op = start of reset program;
while(true) {
MicroOp *op = next_op;
next_op = op + 1;
switch(op->action) {
case Increment16: ++op->u16; continue;
case Decrement16:
... etc, etc, etc, all uncounted operations ending in continue ...
case BeginNextInstruction:
next_op = fetch-decode-execute operations;
continue;
case PerformMachineCycle: break;
}
/* Begin machine cycle execution. */
switch(op->machine_cycle) {
case Read3:
... stuff of a standard 3-cycle read, from op->address to op->u8 ...
break;
case Write3:
... etc, etc ...
}
}
听起来好像您实际上希望循环是可中断的,在这种情况下,您唯一可能需要返回的位置是在底部的机器周期执行部分内,因为这是唯一实际花费时间的部分。您可以只保留一个独立的计数器,例如“进入该机器周期的半周期数”,然后在外部op-> machine_cycle开关内进行适当的开关跳转。
这不是我的主循环的确切形成方式,但是已经足够接近了。我总共有546行可以为每条指令设置微操作程序。我在构建时以编程方式进行操作。对于Z80,它主要是基于宏的表格式,尽管在68000上,我最终得到的是反汇编程序,所以如果您愿意,绝对可以这样做-实际取出各个字段并进行处理是防止模糊表的重要保障错字。
执行我作为微操作存储的内容的代码为1062行。
我的实际上是在元周期级别设置的,因此它会直接广播“我现在执行了3个周期的读取”,而不是在两者之间拼出6个半周期状态,但是它以半周期精度和提供准确的细节量,以半周期保真度进行广播。为了简化计算,我只是省略了额外一级的总线接口,因为我的在讲原始硬件与您的硬件不同。但是并没有语义上的细节损失。
在较早的实现中,我避免了这种简化:一切都宣布为完整的总线状态—作为原始的64位int,其中包含原始的40个引脚的引脚,这些引脚承载信号而不是电源或地。那很棒,但是在计算上却是令人望而却步的,因为一旦您有几个组件在监听总线,函数调用的数量就会激增,而且像这样遍及整个地方的跳转对处理器缓存的影响也很大。
关于c - 在线处理器仿真器(Z80)使用什么算法和/或模式,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/57643974/