出于我制作 Z80 计算机系统的电子爱好,我正在构建 Z80 在线仿真器。这个想法是将物理 Z80 芯片从电路中移除,并将仿真器插入其 socket 并精确仿真 Z80。此外,模拟器将实现调试和诊断支持——但这不是问题所在。现在的想法是,该在线仿真器将在 PSoC5 模块内运行,并通过 USB 与 PC 通信。
I have currently setup a ginormous state machine in code (C)每个时钟边沿变化(正/负边沿)都会提前 - 每个时钟周期两次。我称此为时钟滴答声。
问题是这个状态机代码变得庞大而复杂。
我已经为每个 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)交互,我设置了一个简单的接口(interface)来控制 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 的类似模拟。
我认为主要提示是:潜在机器周期的数量非常少,并且它们呈现相同的总线事件(数据线除外),而不管涉及的指令如何。这意味着您可以通过在运行时使用额外的间接级别或通过自动代码构建来避免冗长、难以维护的代码——我倾向于只依赖预处理器,但其他人已经编写了构建代码的代码。
例如与其详细地写出 PUSH,不如将其简洁地描述为:
- 标准的获取、解码、执行;
- 递减堆栈指针;
- 对堆栈指针执行标准的 3 周期写入机器周期,无论您正在写入什么内容的高位部分;
- 递减堆栈指针;
- 对堆栈指针执行标准的 3 周期写入机器周期,无论您正在写什么。
这里有一个潜在的虚构:你假设你可以在标准机器周期之间以零时间单位递减堆栈指针。但采用这种虚构的好处是能够在两者之间使用标准机器周期。
如果您遵循这条实现路线,您很可能会陷入更像这样的循环:
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 行。
我的实际上设置为在 metacycle 级别进行对话,因此它会直接广播“我现在执行了 3 个周期读取”而不是拼写出介于两者之间的 6 个半周期状态,但它在半周期宣布 -周期精度并准确提供允许以半周期保真度广播的细节数量。为了简化计算,我刚刚省略了额外的总线接口(interface)级别,因为我的不是与您不同的原始硬件。但是没有细节的语义损失。
在早期的实现中,我避免了这种简化:一切都被宣布为完整的总线状态——作为一个原始的 64 位 int,包含原始 40 个引脚的那些引脚,这些引脚承载信号而不是电源或接地。这很好,但计算量太大,因为一旦你有几个组件监听总线,函数调用的数量就非常多,而且像这样在整个地方跳跃对处理器缓存的影响。
关于c - 用于在线处理器仿真器 (Z80) 的算法和/或模式,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57643974/