How to implement overclocking#

This how-to guide explains how overclocking is implemented in vAmiga. With a similar design you will be able to implement overclocking in your own application, too.

Running the CPU with native speed#

In a real Amiga, the speed of different hardware components varies because they are driven by different clocks. All clock signals are derived from a single master clock, which runs at 28.375160 MHz on PAL machines. From the master clock, the CPU clock is derived by dividing the frequency of the master clock by four. As a result, the CPU runs at 7.093790 MHz on PAL machines. Two more clocks come into play: the E clock and the color clock. The E-clock is generated by the CPU itself by dividing its own clock frequency by 10. This signal drives the two interface adapters (CIAs) and plays no role here. The color clock, however, is of great importance because it determines the pace of the memory bus. The color clock signal is derived by dividing the frequency of the master clock by 8. This results in a bus speed of 3.546895 MHz on PAL machines. In summary: In the default configuration, two CPU cycles are executed within one bus cycle, which makes synchronizing the CPU with the memory bus quite simple in the default configuration. In the following, we use the term DMA cycle as a synonym for bus cycle.

In vAmiga timing is handled by the sync function which is part of the Moira class. Please remember that in precise cycle mode sync is executed before each memory access, thus several times during the execution of a single instruction. In simple cycle mode sync is executed only once per instruction, at the end of the instruction handler. In both modes, the number of elapsed CPU cycles is passed to the sync function as function argument. As explained above, 2 CPU cycles correspond to 1 DMA cycle which means that if sync is called with a value of, say, 4, vAmiga must emulate the environment logic for 2 DMA cycles. After that the environment is up to date and the memory access can be performed.

Let’s have a look how the standard case is implemented in vAmiga:

void
Moira::sync(int cycles)
{
    CPU *cpu = (CPU *)this;

    if (!cpu->config.overclocking) {

        // Advance the CPU clock
        clock += cycles;

        // Emulate Agnus up to the same cycle
        agnus.execute(CPU_AS_DMA_CYCLES(cycles));

    } else {

        // The overclocking case (see below)
        ...
    }
}

The implementation is quite simple. When overclocking is disabled, the internal clock counter (clock) is incremented by the number of elapsed cycles (cycles) and Agnus is asked to emulate the surrounding logic for the corresponding number of DMA cycles. As you may have guessed, converting CPU cycles to DMA cycles is simply a division by two:

// Converts CPU cycles to DMA cycles
#define CPU_AS_DMA_CYCLES(cycles) (cycles >> 1)

Overclocking the CPU#

To emulate an overclocked CPU in vAmiga, simply set config.overclocking to a non-zero value. In this case the value is interpreted as an overclocking factor. For example, if the value is set to 2, the CPU will run at 14.187580 MHz in PAL mode. If the value is set to 1, the CPU runs at native speed, but vAmiga executes the else branch in the code snippet shown above. This is the branch where the overclocking is emulated. Let’s take a closer look:

void
Moira::sync(int cycles)
{
    CPU *cpu = (CPU *)this;

    if (!cpu->config.overclocking) {

        // The standard case (see above)
        ...

    } else {

        // Compute the number of mico-cycles executed in one DMA cycle
        auto microCyclesPerCycle = 2 * cpu->config.overclocking;

        cpu->debt += cycles;

        while (cpu->debt >= microCyclesPerCycle) {

            // Advance the CPU clock by one DMA cycle
            clock += 2;

            // Emulate Agnus for one DMA cycle
            agnus.execute();

            cpu->debt -= microCyclesPerCycle;
        }
    }
}

In the first line of the else branch, vAmiga calculates how many CPU cycles are executed during one DMA cycle. Since two native CPU cycles correspond to one DMA cycle, this value is simply the overclocking factor times 2. After that variable debt is incremented by the number of elapsed cycles which is passed in as function argument. Variable debt can be interpreted as a sub-cycle counter and stores the number of elapsed CPU cycles that have not been emulated on the Agus side yet. The function then executes a while loop which emulates Agnus for all elapsed full bus cycles.