monome / crow

Crow speaks and listens and remembers bits of text. A scriptable USB-CV-II machine
GNU General Public License v3.0
166 stars 34 forks source link

Clock & Metro libraries are slightly out of sync #473

Open trentgill opened 1 year ago

trentgill commented 1 year ago

At present there is some subtle inaccuracies in the timing of the 2 timing libraries on crow.

Metro runs a tiny bit fast, while Clock runs slightly slow.

metro

--- test metro timing accuracy vs cpu base clock
-- should print '1000' every second representing count of milliseconds since last event
last_time = time()
metro[1].event = function()
  local now = time()
  print(now - last_time)
  last_time = now
end
metro[1]:start()

results in: 999, 1000, 1000, 999, 1000, 1000, 1000, 999, 1000, 1000 .... The pattern is not exactly repeating, but averages out to around 999.7

I'm thinking this is related to the math that converts the floating point time provided into scalars for the hardware timer (in ll/timers.c). We already do the math in double precision (64bit) so it's unlikely a precision issue -- much more likely there is a small error (off by one?) in the algorithm.

clock

--- test clock timing accuracy vs cpu base clock
-- should print '1000' every second representing count of milliseconds since last event
last_time = time()
clock.run(function()
  while true do
    clock.sleep(1)
    local now = time()
    print(now - last_time)
    last_time = now
  end
end)

results in 1001 every time, suggesting the clock is always late by 1ms. changing clock.sleep(n) to 0.5 results in 501, and at 2.0 results in 2001. i think this is likely caused by the edge-check in lib/clock.c where we check to see if we've passed the target time for an active clock. have a vague memory that i had to play with an = or <= to avoid infinite loops, but perhaps this shifted the timebase.

EDIT I just came back to the REPL after 40minutes and noted that the clock is now sleeping slightly longer! clock.sleep(2) is now giving me 2002 or 2003. This seems like it could be a floating point accuracy issue where it's comparing 2 large numbers with a small difference between them. I still think there is an off-by-1ms issue, but it seems there some secondary error as well.

asl

i'm not sure whether there is parity in timing between the hardware-timer based speeds above and the DAC samplerate. they are all derived from the same clock (216MHz), but with all the div/mul ratios for the I2S driver, i don't know if they have some error introduced.

it would be good to find a way of measuring the two exactly. i think ASL self-compensates for any delays introduced when stepping through a sequence of slopes (in fact i think there is no delay since casl), so it should be possible to create a pulse every second directly from a metro, and also via an ASL which has a total time of precisely 1s.

there is already a scalar being applied on timing values from lua into asl, so would be zero additional overhead if we need to slightly adjust the DSP loop to be accurately matched.

absolute clock timing

all of the above is about relative time-matching of the different libraries. the intention is to make 1second mean the same thing to all the libraries on crow.

but "1second" in crow land is not precisely calibrated to a terrestrial second. this is because all the timing of the microcontroller is based on a crystal installed on the pcb which is only accurate to about 2% i think. So there can be some substantial pitch inaccuracies when using crow as an oscillator or tempo base for a system.

in order to have a more precise timebase it would be necessary to either find a way to subtly scale the main CPU clock (via the PLL) or a software solution where times are manipulated by some small amount according to a calibration pass. i'm not exactly sure how this would be done and it would likely be quite ugly. but perhaps someone is very passionate about this? the easiest way to measure the inaccuracy would be a simple frequency counter (or oscilloscope with frequency measurement), reading the frequency of output[1](oscillate(1000)) or something similar. take a precise reading of the frequency then multiply all timebases by 1000/measured_freq.

this is probably not necessary, but leaving it here more as a related thought that might encourage some further thoughts.