为现代 C++ 中的模拟器编写一个以 mhz 一致且有效地触发的时钟(循环)

Writing a clock(loop) that is triggered in the mhz consistently and effeciently for an emulator in modern C++

我目前正在为旧的 CPU(intel 8085)开发模拟器。表示CPU时钟是3.2mhz。我正在尝试尽可能准确,并尽可能跨平台

对我来说,这意味着我需要一个以 3.2mhz 频率调用的时钟。我不在乎它是否太准确,只要在 10% 以内就足够了。

简单的方法是

auto _PrevCycleTime = std::chrono::high_resolution_clock::now();
double _TimeBetweenClockCycles = 1.0 / 3200000;
while (1)
{
    auto now = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> t = std::chrono::duration_cast<std::chrono::duration<double>>(now - _PrevCycleTime);
    
    if (t.count() >= _TimeBetweenClockCycles)
    {
        _PrevCycleTime = now;
        //Clock trigger
    }
}

这会产生每秒调用 240 万次的时钟。 更大的问题是,我意识到即使 运行 一个 while(1) {} 也使用了我 CPU 的 50-60%。这样不行。

我的下一个方法是使用睡眠功能。

long long _TimeBetweenClockCyclesNS = 1000000000 / 3200000
double _TimeBetweenClockCycles = 1.0 / 3200000;
auto now = std::chrono::high_resolution_clock::now();

while (_Running)
{
    std::chrono::duration<double> t = std::chrono::duration_cast<std::chrono::duration<double>>(now - _PrevCycleTime);

    if (t.count() >= _TimeBetweenClockCycles)
    {
        _PrevCycleTime = now;
        //Clock trigger
    }
    else
    {            
        std::this_thread::sleep_for(std::chrono::nanoseconds(_TimeBetweenClockCyclesNS));
    }
}

虽然这(有点)有效,但它根本不一致。我希望它“足够接近”,但上面的代码被调用:

我很可能遗漏了一些明显的东西,而且我把它弄得太复杂了,但我相信一定有更好的方法,因为其他“更重”系统的模拟器似乎使用更少 CPU 同时需要更高的准确性。

在我的用例中,我最关心的是:

我有哪些选择?

(注意: 在没有任何时钟限制逻辑的情况下,我的时钟每秒可以 运行 超过 3000 万次。同样,使用我的大约 50-60% CPU。所以它 应该 能够 运行 300 万次,使用率要低得多 CPU)

注意: 代码 运行 在单独的 std::thread 中,如果重要的话)

要认识到的重要一点是,如果某些事情发生在错误的时间,没有人会注意到。你有一个 CPU 与一些外围设备交谈。假设外围设备是 GPIO 引脚。只要 CPU 在正确的时间打开和关闭 GPIO 引脚,实际上没有人会注意到 CPU 在这些时间之间是否 运行 太快。如果它驱动显示输出,只要以正确的帧速率显示,没有人会注意到显示像素是否计算得太快。等等。


仿真器使用的一项技术是计算 CPU 指令使用的时钟周期数。如果您使用的是解释型设计,则可以在指令处理程序中写入 clockCycles += 5;。如果您使用的是 JIT 设计,则可以在每个基本块的末尾进行。如果你不是使用JIT设计,不知道什么是基本块,可以忽略上一句。

然后,当 CPU 确实做了一些重要的事情时,比如更改 GPIO 引脚,您可以睡觉以赶上进度。如果自上次休眠以来发生了 3,200,000 个时钟周期,但实际时间只有 0.1 秒,那么您可以在更新屏幕之前休眠 0.9 秒。

你拥有的“睡眠点”越少,你的时间就越不准确,你浪费在保持准确时间上的时间就越少。视频游戏模拟器通常会尽可能快地渲染整个帧。事实上,自 N64/PS1 时代以来,许多仿真器根本就懒得模拟 CPU 时序。这些系统的计时非常复杂,以至于每个游戏都已经知道它必须等待下一帧开始,所以模拟器只需要以正确的速率开始帧。


另一个想法是计算计时信息并将其发送到外设,而不实际计时。游戏确实依赖于精确显示时序的早期系统(例如 SNES)的模拟器可以全速 运行 CPU 然后告诉显示代码“在时钟周期 12345 CPU 写入 0x6789注册12。”显示代码然后可以计算显示在该时钟周期内绘制的像素,并更改其绘制方式。仍然没有必要实际同步 CPU 和显示时间。


如果您想要精确计时而又不会严重拖慢程序速度,您可能需要使用 FPGA 而不是 CPU。