Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timestamping #170

Closed
4 tasks done
jordens opened this issue Nov 16, 2020 · 14 comments
Closed
4 tasks done

Timestamping #170

jordens opened this issue Nov 16, 2020 · 14 comments
Labels
enhancement New feature or request

Comments

@jordens
Copy link
Member

jordens commented Nov 16, 2020

Timestamping architecture

External reference on digital input

  • ADC/DAC sampling timer period t, integer in units of some timer clock period τ (e.g 10 ns) locked to the CPU PLL, and thus to the CPU XO. For example t = 128 τ ~ 1.28 µs. τ is a design constant, t is a compile-time constant. We should aim at making t a power of two.
  • n DAC and ADC samples per batch, batch period n*t, n being a power of two compile-time constant, e.g. n = 4.
  • Timestamping timer overflow/reload period is j*n*t. Locked through the CPU PLL to the CPU XO. Not yet/necessarily deterministic (across reboots, resets, timer reconfigurations) phase w.r.t. ADC/DAC sampling timer. j >= 1, a power of two, as large as possible within the timer counter size.
  • Digital input edges capture the timestamping timer counter: s (u16 as i16), Digital input frequency typically anywhere from 50 Hz (mains) to 500 kHz (less than ADC Nyquist typically). These are samples of the batch phase at zero reference phase.
  • Processing routine receives m timestamps for the current batch. Number of timestamps 0 <= m <= n depends on reference frequency, no new timestamps is explicitly possible.
  • Processing routine scales the timestamps from mod j*n*t to natural i32 mod (1 << 32). Pure left shift.
  • Feed all received scaled s into the PLL en-bloc and at the end of the batch, store most recent phase y, frequency f (both i32) and the time k in units of full timer overflow periods between the two last PLL updates (i.e. k = 0 necessarily if m >= 2, though k can also be 0 if m < 2). j > 1 will help with that.
  • Converted frequency and phase: g = (1 << 64)/(n*f + n*(k << 32)), v = -(y << 32)/f. (TODO: distribute exponents or just i64 math).
  • Harmonic index u: i32.
  • Each ADC sample is assigned the phase p_i: i32 = u*(i*g + v).
  • Demodulate with the negative of that phase.

125 MHz from Pounder on ETR

  • Timestamping timer with counter driven from Pounder period σ, σ = r*4*2 ns, typically the timer ETR prescaler is r = 4 or r = 8, compile-time constant. σ is a design constant.
  • Timer overflowing (reloading) at the (known) demodulation period q, integer in units of σ (f_demod = 500 MHz/4/r/q). q = 128 for example. n <= q < u16. q should be run-time configurable. It's the difference between two DDS FTWs. q*r*4 = f_a - f_b. We can restrict q to a power of two.
  • ADC sample timer captures the timestamping timer counter: yielding value s. There will always be n samples per batch.
  • Processing routine scales the timestamps from mod q to natural mod (1 << 32). Pure left shift.
  • Process all timestamps, get latest y, f.
  • Convert frequency and phase: g = f, v = y - n*f

Filtering PLL, done in #188

  • Filter the s_i mod n timestamp sequence (in either of the two cases above) into NCO frequency and phase (relative to the CPU clock/XO or better ADC/DAC batch clock). Configurable IIR filter, higher resolution, lower bandwidth.
  • Use that reconstructed oscillator to provide demodulation phases p_m for the ADC samples.
  • PLL algorithm, maybe it (ftw <- IIR(s_i - phase); phase <- phase + ftw) boils down to natually overflowing integer phase = IIR().update(s_i) with a biquad. (doesn't). Implemented.
  • Even better (thanks @SingularitySurfer): Check whether this is possible with the STM32 timers: Set up a wrapping hardware timer with variable increment as the NCO (phase <- phase + ftw) and feed back the timestamps onto the counter increment (ftw += gain_p*(s_i - phase_i_nominal) + gain_d*(s_i - s_i-1) that's ftw = IIR(s_i)). No such feature and the current PLL looks more flexible.

Context:

@jordens jordens added the enhancement New feature or request label Nov 16, 2020
@matthuszagh
Copy link
Contributor

matthuszagh commented Nov 17, 2020

It's worth mentioning that most of the logic in "External reference on digital input" is already implemented in stabilizer-lockin-nucleo. That implementation expects the s_j timestamps (it needs to know j) for each batch, as well as n, t, τ, and k, and performs the necessary computations to arrive at the demodulation phase from those inputs. It additionally accounts for a scaling factor in the case of harmonic demodulation.

@jordens
Copy link
Member Author

jordens commented Nov 17, 2020

I wrote this down to have one concise place where the design is specified. And Ryan needs this to implement the items 1-4 and the second case. Let's work on merging the DSP code in stabilizer-lockin-nucleo into stabilizer. Could you split it into individual PRs (in branches in stabilizer) so that we can review and discuss them before merging?

@jordens
Copy link
Member Author

jordens commented Nov 17, 2020

And what do you (@ryan-summers and @matthuszagh ) think about laying a bit of groundwork for multiple binaries (in this case just for the nucleo hardware and the stabilizer hardware). I.e. have lib/ and bin/{stabilizer,nucleo}.rs (the way other projects like the defmt app-template do it)?

@ryan-summers
Copy link
Member

I was planning on doing the ground work for multiple binaries for the stabilizer WMS demo - getting started with the nucleo binary is probably a great first step though.

@matthuszagh
Copy link
Contributor

matthuszagh commented Nov 17, 2020

Sounds good - I'll get going on the DSP PRs. I'm also happy to do any part of the work for splitting this up into separate binaries. It seems to me that another benefit of splitting this is that it would move hardware-independent DSP code to a separate library, which would make it easier to perform unit and integration tests on that code.

@jordens
Copy link
Member Author

jordens commented Nov 17, 2020

@matthuszagh Why do you need to know τ? It should drop out of all equations.

I've added classification of design/compile-time constants and run-time parameters for σ, τ, t, n, q.

@matthuszagh
Copy link
Contributor

@jordens I don't. I'd just defined it that way since I wasn't entirely sure which inputs you wanted (everything else was just computed at compile time anyway). But, it's easy to refactor so that τ drops out so I'll do that.

@jordens
Copy link
Member Author

jordens commented Nov 17, 2020

Ok. Good!

@jordens
Copy link
Member Author

jordens commented Dec 7, 2020

Note to self: maybe it's cheaper to do the phase conversion in the first case before the pll. Either with cheaper integer math or by building a "reciprocal" pll that does so internally.

@matthuszagh
Copy link
Contributor

Is k the number of full batch periods (i.e., n*t), or full timer overflow periods (i.e., j*n*t)?

@jordens
Copy link
Member Author

jordens commented Dec 30, 2020

Timer overflow periods.

@matthuszagh
Copy link
Contributor

Thanks. I've updated the original post to reflect that.

@matthuszagh
Copy link
Contributor

matthuszagh commented Jan 2, 2021

I think the equations for g and v in the external reference case should be: g = (1 << 64)/(n*j*(f + (k << 32))) and v = (((a << (32 - log2(j))) - y) << 32) / (f + (k << 32)) + (m << 32). a represents the current index of j. So, for instance, if j=2, then a can be 0 or 1, depending on the batch we're in. log2(j) is just a compile-time constant. This implementation assumes that y and f are converted to u32 and then to i64. Additionally, y is decremented by 1<<32 every timer overflow.

Here's some very simple code to test the correctness of the initial phase calculation:

def initial_phase(a, lgj, y, f, k, m):
    return (
        int(round((((a << (32 - lgj)) - y) << 32) / (f + (k << 32))))
        + (m << 32)
    ) % (1 << 32)


# a == 0, m == 0, k == 0
assert initial_phase(a=0, lgj=1, y=-500, f=2362232012, k=0, m=0) == 909
# a == 0, m == 0, k == 1
assert initial_phase(a=0, lgj=1, y=-400, f=2362232012, k=1, m=0) == 258
assert (
    initial_phase(a=0, lgj=1, y=-2362232412, f=2362232012, k=1, m=0)
    == 1524020911
)
assert (
    initial_phase(a=0, lgj=1, y=-4294977296, f=2362232012, k=1, m=0)
    == 2770953095
)
# a == 0, m == 1, k == 0
assert initial_phase(a=0, lgj=1, y=500, f=2362232012, k=0, m=1) == 4294966387
assert (
    initial_phase(a=0, lgj=1, y=2147483148, f=2362232012, k=0, m=1)
    == 390452480
)
# a == 0, m == 1, k == 1
assert initial_phase(a=0, lgj=1, y=500, f=2362232012, k=1, m=1) == 4294966973
assert (
    initial_phase(a=0, lgj=1, y=2147483148, f=2362232012, k=1, m=1)
    == 2909494297
)
# a == 0, m > 1, k == 0
assert initial_phase(a=0, lgj=1, y=2147483548, f=195225786, k=0, m=11) == 2156

# a == 1, m == 1, k == 0
assert (
    initial_phase(a=1, lgj=1, y=4294967286, f=2362232012, k=0, m=1)
    == 390451589
)

# a == 3 (lgj=2), m == 1, k == 0
assert (
    initial_phase(a=3, lgj=2, y=4294967286, f=2362232012, k=0, m=1)
    == 2342709452
)

Let me know if you'd like me to give the derivations for g and v.

@jordens
Copy link
Member Author

jordens commented Jan 27, 2021

#241 Covers the reciprocal pll

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants