【问题标题】:What algorithms and/or patterns to use for an In-Circuit Processor Emulator (Z80)用于在线处理器仿真器 (Z80) 的算法和/或模式
【发布时间】:2019-12-29 19:44:35
【问题描述】:

出于我制作 Z80 计算机系统的电子爱好,我正在构建 Z80 在线仿真器。这个想法是将物理 Z80 芯片从电路中移除,并将仿真器插入其插槽并精确仿真 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 testPSoC5 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() 退出函数,下一次调用函数将在那里恢复执行。

好的,现在问题来了: 我很难让状态机表现得恰到好处,这让我质疑我对这个问题的推理方式。因为处理更复杂的指令涉及状态机中的更多状态,我发现很难对代码进行推理——这是一种符号/气味。

是否有任何明显的算法和/或模式可用于编写这种类型的时钟周期精确仿真器?

【问题讨论】:

  • 可能最适合codereview.stackexchange.com 或软件工程。
  • 我不知道 codereview,但我检查了软件工程——这意味着更多的 ALM 和部署类型主题。 meta.stackoverflow.com/questions/254570/…
  • 那么可能会进行逆向计算。尽管我很喜欢逆向计算和旧 CPU,但我不确定你会得到关于 SO 的答案。例如,“是否有任何明显的算法和/或模式可用于编写这种类型的时钟周期精确仿真器?” 看起来很像推荐。您可能需要重新表述这一点,并研究您的问题外观。
  • 请参阅What's the proper implementation for hardware emulation? 它拥有一个链接到我的 Z80 iset,每个 MC 计时和 MC 类型或多或少是你现在做的硬编码。它是一个格式化的 TXT 文件,可以直接加载到您的模拟器中……大大简化了 CPU 内核……它包含高达 4Byte(包括)的所有操作码并通过 ZEXALL 100%。因此,在您的情况下,我只会对每种 MC 类型的时序进行编码(只是其中的几个)...... t 从 MC 时序转变为单独的时钟周期

标签: c emulation state-machine z80


【解决方案1】:

我已经写了两次类似的代码,假设这意味着我什么都知道,并且分别实现了 6502 和 68000 的类似模拟。

我认为主要提示是:只有极少数的潜在机器周期,并且无论涉及的指令如何,它们都呈现相同的总线活动(数据线除外)。这意味着您可以通过在运行时使用额外的间接级别或通过自动代码构造来避免冗长、难以维护的代码——我倾向于只依赖预处理器,但其他人已经编写了构造代码的代码。

例如您可以将其简洁地描述为:

  1. 标准的获取、解码、执行;
  2. 递减堆栈指针;
  3. 对堆栈指针执行标准的 3 周期写机器周期,无论您正在写什么,高位部分;
  4. 递减堆栈指针;
  5. 对堆栈指针执行标准的 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 行。

我的实际上设置为在元周期级别进行对话,因此它会直接广播“我现在执行了 3 个周期读取”,而不是拼写介于两者之间的 6 个半周期状态,但它在半周期宣布 -周期精度并准确提供允许以半周期保真度进行广播的细节量。为了计算简单,我只是省略了额外级别的总线接口,因为我不像你的那样与原始硬件通信。但是没有细节的语义损失。

在较早的实现中,我避免了这种简化:一切都被宣布为完整的总线状态——作为一个原始的 64 位 int,其中包含传输信号而不是电源或接地的原始 40 个引脚的那些。这很棒,但计算量太大了,因为一旦您有几个组件监听总线,就会产生大量的函数调用,并且像这样在整个地方跳跃会对处理器缓存产生影响。

【讨论】:

  • 我需要触发电路提供的硬件时钟信号的每个时钟滴答。所以元循环不会削减它。此代码运行而不是真正的芯片。
  • @obiwanjacobi 是的,这就是“因为我的硬件不像你的那样与原始硬件交谈”附带条件。我的意思是,您可以将元周期广播给某人,然后将它们序列化为总线,然后按需请求下一个元周期,在这种情况下,您将保留while(true),但一旦您进入switch(op->machine_cycle),就返回机器循环。然后调用者在其中包含“如果这是一个刷新周期,则将这四个总线状态排序,在每个下一个时钟沿上一个”逻辑。
猜你喜欢
  • 2023-04-06
  • 1970-01-01
  • 1970-01-01
  • 2013-04-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多