Skip to content →

synching components – relative schedulers

So, with our SPC700 and our CPU implemented, we need to be able to sync them, because basically we have 2 separate CPUs running at the same time. If both would be running at the same speed, we could just clock them both each cycle. So, lets’s take a look at their speeds.

NTSC Timings
  NTSC crystal      21.47727000MHz (X1, type number D214K1)     
  NTSC master clock 21.47727000MHz (21.47727MHz/1) (without multiplier/divider) 
  NTSC dot clock     5.36931750MHz (21.47727MHz/4) (generated by PPU chip) 
  NTSC cpu clock     3.57954500MHz (21.47727MHz/6) (without waitstates)

PAL Timings
  PAL crystal       17.73447500MHz (X1, type number D177F2) 
  PAL master clock  21.28137000MHz (17.7344750MHz*6/5) (generated by S-CLK chip) 
  PAL dot clock      5.32034250MHz (21.2813700MHz/4) (generated by PPU chip)   
  PAL cpu clock      3.54689500MHz (21.2813700MHz/6) (without waitstates)

APU Timings
  APU oscillator    24.57600000MHz (X2, type number 24.57MX) 
  DSP sample rate          32000Hz (24.576MHz/24/32) 
  SPC700 cpu clock   1.02400000MHz (24.576MHz/24) 
  SPC700 timer 0+1          8000Hz (24.576MHz/24/128) 
  SPC700 timer 2           64000Hz (24.576MHz/24/16) 

So, neither PAL nor NTSC line up with the SPC700 clock, nor do they divide evenly, which gives us a problem to stay in sync properly.

This is where relative schedulers come in to play. I learned of the concept from the brilliant byuu/near, who has a more extensive article about schedulers here.

Relative schedulers allow us to keep components in sync, but only in a 1:1 relationship. We can of course chain multiple components with multiple relative schedulers if necessary.

We achieve being in sync by keeping track of how far a certain component has run. We use a int64_t variable for this, and add or subtract values, depending on which component has run, and how far.

For our specific example (CPU <-> SPC700) we will subtract from the variable when our CPU has run, and add to the variable when the SPC700 has run. We get the amount of master cycles the component has run, and multiply it by the master clock frequency of the other component.


int64_t scheduler_CPU_SPC700 = 0;
const uint32_t CPU_MASTER_CLOCK = 21477272; // PAL
const uint32_t SPC700_MASTER_CLOCK = 24576000;

//Main Loop begins somewhere here...

if(scheduler_CPU_SPC700 >= 0) {
  uint16_t cpu_cyles = CPU_step();
  uint16_t cpu_master_cycles = cpu_cycles * 6; // because PAL cpu clock = 
  scheduler_CPU_SPC700 -= cpu_master_cycles * SPC700_MASTER_CLOCK;
else {
  uint16_t spc700_cycles = SPC700_step();
  uint16_t spc700_master_cycles = spc700_cycles * 24; // because SPC700 cpu clock = 24.576MHz/24
  scheduler_CPU_SPC700 += spc700_master_cycles * CPU_MASTER_CLOCK;

By orientating each component on the master clock frequency of the other component we can make sure to be as close to perfect sync as possible. This is of course no comparison to a real-time sync (there will [almost always] be times when the CPU is ahead or when the SPC700 is ahead).

(Using a int64_t gives us enough room to handle all weird behaviour. Even if one of the components would drop dead, the other component [theoretically] still could run for almost 5 hrs before causing an under/overflow)


Leave a Reply