对于我制作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 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()退出该函数,对该函数的下一次调用将在此处继续执行。

好的,现在是这个问题:
我很难使状态机运行正常,这使我对问题的推理方式提出了质疑。因为处理更复杂的指令会在状态机中涉及更多状态,所以我发现很难推理出代码-这是一个符号/气味。

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

最佳答案

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

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

所以与其说出详尽的推文,不如将其紧凑地描述为:

  • 标准获取,解码,执行;
  • 递减堆栈指针;
  • 使用所写内容的大部分来执行对堆栈指针的标准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行。

    我的实际上是在元周期级别设置的,因此它会直接广播“我现在执行了3个周期的读取”,而不是在两者之间拼出6个半周期状态,但是它以半周期精度和提供准确的细节量,以半周期保真度进行广播。为了简化计算,我只是省略了额外一级的总线接口,因为我的在讲原始硬件与您的硬件不同。但是并没有语义上的细节损失。

    在较早的实现中,我避免了这种简化:一切都宣布为完整的总线状态—作为原始的64位int,其中包含原始的40个引脚的引脚,这些引脚承载信号而不是电源或地。那很棒,但是在计算上却是令人望而却步的,因为一旦您有几个组件在监听总线,函数调用的数量就会激增,而且像这样遍及整个地方的跳转对处理器缓存的影响也很大。

    关于c - 在线处理器仿真器(Z80)使用什么算法和/或模式,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/57643974/

    10-10 20:21