PyBoard Timer Question

General discussions and questions abound development of code with MicroPython that is not hardware specific.
Target audience: MicroPython Users.
Post Reply
User avatar
Kip
Posts: 31
Joined: Sat Dec 26, 2015 7:23 am

PyBoard Timer Question

Post by Kip » Mon Jun 03, 2019 3:31 am

Hi all,

I need some advice when it comes to using timers.

I want to read with an ADC and at every n’th (say 5) sample toggle a different GPIO. This is to produce a square wave and get the resulting wave via ADC after induction between 2 coils.
See example https://github.com/KipCrossing/OpenEM/b ... finder.gif.
I would like to be able to measure the phase from toggle. I’m wondering what is the best way to do this.

I can't seem to be able to achieve this with one timer because the ADC reads once every cycle. Is it possible to have 2 counters for the one timer; one for the ADC and the other for a PWM channel (could be a silly question)? My other idea is to take an ADC sample at every tick, set by the prescaler, and toggle the GPIO at every period (also, could be a silly suggestion)

I noticed that there is a way to synchronize timers with the STM module but I have a knowledge gap here and would need a good example to help me through the process.

These are the pins I'm planning to use (but can change):

Code: Select all

tim2 = Timer(2, prescaler=245, period=9)
ch1 = tim2.channel(1, Timer.PWM, pin=Pin.board.X1)
ch2 = tim2.channel(2, Timer.PWM_INVERTED, pin=Pin.board.X2)
adc1 = pyb.ADC(pyb.Pin.board.X3)
adc2 = pyb.ADC(pyb.Pin.board.X4)
Some guidance would be much appreciated.

Kip

User avatar
jimmo
Posts: 2754
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: PyBoard Timer Question

Post by jimmo » Mon Jun 03, 2019 5:56 am

I wonder if it would be possible instead of trying to synchronise your pulse train with the ADC, instead run the pulse train independently using it's own timer/channel at whatever frequency and duty cycle makes sense, then run two ADC measurements in parallel (using read_timed_multi) - one measuring the pulse train, the other measuring the step response of the circuit. That way you'll get phase delay (possibly even more accurate because you won't have an unknown phase delay to compensate for between the PWM, Timer, ADC and GPIO peripherals).

If you run the ADC and N times the frequency of the pulse train, and do at least 2*N samples, you'll see one full measurement.

User avatar
Kip
Posts: 31
Joined: Sat Dec 26, 2015 7:23 am

Re: PyBoard Timer Question

Post by Kip » Mon Jun 03, 2019 7:48 am

I thought of that and it will probably work lower frequencies. With higher frequencies around 20 kHz the precision of measuring the phase will be a lot worse. The read_timed_multi() Method maxes out at around 200 kHz meaning there would be only 10 samples per wavelength. Therefore when observing the PWM pin, the phase may be out by 10%.

I thought of simply resetting the timer and running the 2 methods back to back for until I captured into the buf and rerunning in a while loop. Then simply observing the delay between the two and seeing how much the phase varies for a constant freq for each loop.

Code: Select all

while True:
    tim2 = Timer(2, freq=17100)
    tim9 = Timer(9, freq=17100*10)
    adc1 = pyb.ADC(pyb.Pin.board.X3)
    buf = bytearray(20)

    ch1 = tim2.channel(1, Timer.PWM, pin=Pin.board.X1)
    ch2 = tim2.channel(2, Timer.PWM_INVERTED, pin=Pin.board.X2)
    adc1.read_timed(buf, tim9)
    print(list(buf))
    check_phase(buf)

User avatar
jimmo
Posts: 2754
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: PyBoard Timer Question

Post by jimmo » Mon Jun 03, 2019 9:56 am

Kip wrote:
Mon Jun 03, 2019 7:48 am
Therefore when observing the PWM pin, the phase may be out by 10%.
Yup, that's true. And yeah, like you say, the sample rate isn't great either.

However, the phase issue is not as bad as it would be if the PWM source came from somewhere else. If you choose the frequencies to be multiples of each other, the error is either 0% or 10%, but not in between (because it's running on your clock). (And I'd imagine that one of those two values is disproportionately more likely).
Kip wrote:
Mon Jun 03, 2019 7:48 am
I thought of simply resetting the timer and running the 2 methods back to back for until I captured into the buf and rerunning in a while loop. Then simply observing the delay between the two and seeing how much the phase varies for a constant freq for each loop.
Setting the tim2.counter(0) should be sufficient to reset the timer (and faster).

With this approach (and other ideas I had along the same vein) how do you ensure that you start read_timed at a known point in the PWM cycle. i.e. some unknown amount of time takes place between resetting the timers and starting read_timed. I guess that's why buf is 20 elements long, but how do you later say which sample was the one that corresponded to the rising edge of the pulse? (Hope I'm missing something simple)

(assuming GC is disabled and there's no jitter due to interrupts, and read_timed is the next line after tim9/tim2.counter(0), then at least youd have a constant offset, but that still might be problematic).


I've been trying to think of other ways but no luck yet. Will be very intrigued to see what other people think!

User avatar
Kip
Posts: 31
Joined: Sat Dec 26, 2015 7:23 am

Re: PyBoard Timer Question

Post by Kip » Mon Jun 03, 2019 11:02 am

Thanks for your input! :D
jimmo wrote:
Mon Jun 03, 2019 9:56 am
how do you ensure that you start read_timed at a known point in the PWM
I guess I could use Timer.OC_TOGGLE and half the period to get the same frequency. Then set the counters to 0 each loop (Thanks for that tip btw)

I would really like to have a practical example (code) of how to do what is suggested here:
viewtopic.php?t=986
It's just a little over my head with my limited experience with timers.

chrismas9
Posts: 152
Joined: Wed Jun 25, 2014 10:07 am

Re: PyBoard Timer Question

Post by chrismas9 » Mon Jun 03, 2019 11:12 pm

If you can't find a software solution you could add an analog switch to select the input (PWM) or output waveform to the same ADC. Just stream the ADC as fast as you want and switch to output after enough time to capture some input cycles. Knowing the sample rate you can calculate the average phase zero point of the input waveform and then get accurate phase measurements of the output waveform.

User avatar
jimmo
Posts: 2754
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: PyBoard Timer Question

Post by jimmo » Tue Jun 04, 2019 1:19 am

Kip wrote:
Mon Jun 03, 2019 11:02 am
I guess I could use Timer.OC_TOGGLE and half the period to get the same frequency. Then set the counters to 0 each loop (Thanks for that tip btw)
I'm not quite sure I follow sorry...

However, that thread gave me a possible idea of how you could solve both problems (synchronising the ADC loop with the PWM, _and_ ensuring that you don't start sampling until the pulse goes high).

First of all, synchronising the two timers. Extending @dhyland's example at https://github.com/dhylands/upy-example ... r/gated.py

Code: Select all

import pyb
import stm

# Using TIM2 and TIM5 because they correspond to AFs on X1 (TIM2 CH1) & X2 (TIM5 CH2)

# Choose prescaler/period values make sense for sample rate etc.

tim2 = pyb.Timer(2, prescaler=209, period=19)  # 20 kHz for ADC
ch2_1 = tim2.channel(1, pyb.Timer.IC, pin=pyb.Pin.board.X1, polarity=pyb.Timer.FALLING)

tim5 = pyb.Timer(5, prescaler=209, period=399)  # 1 kHz for PWM
ch5_2 = tim5.channel(2, pyb.Timer.IC, pin=pyb.Pin.board.X2, polarity=pyb.Timer.FALLING)

# Additional config to make TIM2 gated on TI1 (i.e. channel 1)
smcr = stm.mem16[stm.TIM2 + stm.TIM_SMCR]
smcr &= 0b1111111110001000
smcr |= 0b0000000001010101   # TS = 101 (TI1), SMS = 101 (Gated)
stm.mem16[stm.TIM2 + stm.TIM_SMCR] = smcr

# Additional config to make TIM5 gated on TI2 (i.e. channel 2)
smcr = stm.mem16[stm.TIM5 + stm.TIM_SMCR]
smcr &= 0b1111111110001000
smcr |= 0b0000000001100101   # TS = 110 (TI2), SMS = 101 (Gated)
stm.mem16[stm.TIM5 + stm.TIM_SMCR] = smcr
So at this point if were to wire up X1 and X2 to X17 (the user switch) then the timers would only run when the button is held down.

Then enable the PWM output on TIM5's other channel (e.g. channel 3, available on X3), so it will be syncronized to the ADC.

Code: Select all

ch5_3 = tim5.channel(3, pyb.Timer.PWM, pin=pyb.Pin.board.X3, polarity=pyb.Timer.FALLING)
ch5_3.pulse_width(200)  # 50% duty cycle
Then reset both the (stopped) timers. Note I've chosen to set it to the end of the range because setting it to zero will turn X3 on. (But you could change your PWM config to match, or even play with these values to introduce a phase delay).

Code: Select all

tim2.counter(19)
tim5.counter(399)
So now you have two timers at different integer-multiple frequencies, synchronised on the USER button. If you call adc.read_timed, it will block until you press the button because the timers aren't running. So your very first sample will be exactly when the PWM goes high, and N samples to follow at 20x the sample rate of the PWM.

Code: Select all

>>> adc = pyb.ADC(pyb.Pin.board.X4)
>>> buf = bytearray(20)
>>> adc.read_timed(buf, tim2)  # Blocks on the USER button
Exercise for the reader: set up a third timer on another pin that replaces the USER button.
Also I'm sure this can be done without having to externally connect the pins (i.e. you can probably route the timer channels internally) but this is as far as I've tested.

(Thanks for the excuse to dive into this :) )

(Note: it's worth reading the implementation of read_timed just to understand exactly what it gives you. In particular note that you will still have jitter due to interrupts, and there is a very small (but constant) phase delay between the detection of the timer trigger and sampling the ADC).

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: PyBoard Timer Question

Post by pythoncoder » Tue Jun 04, 2019 12:50 pm

Kip wrote:
Mon Jun 03, 2019 7:48 am
...The read_timed_multi() Method maxes out at around 200 kHz meaning there would be only 10 samples per wavelength. Therefore when observing the PWM pin, the phase may be out by 10%...
That doesn't necessarily follow: read_timed_multi() takes N readings in quick succession, but is subject to a repetition rate limit. The phase error between channels corresponds to the delay between the readings which is << 1/rep_rate. Also you can measure the delay between readings as a one-off lab measurement. Stick that number into your code and you can get very accurate relative phase measurements well into the ultrasonic range.

See this project. The images don't do justice to the eventual performance: I submitted the read_timed_multi patch after this project was started: the device was then extended to measure much higher frequencies.
Peter Hinch
Index to my micropython libraries.

wangshujun@tom.com
Posts: 61
Joined: Fri Feb 15, 2019 9:22 am

Re: PyBoard Timer Question

Post by wangshujun@tom.com » Fri May 29, 2020 9:50 am

Can the DMA double buffer non-blocking mode be achieved? Unfortunately, it is not possible to reliably collect real-world continuous data

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: PyBoard Timer Question

Post by pythoncoder » Fri May 29, 2020 10:09 am

There is no way to do continuous real time ADC data acquisition in MicroPython (barring using a timer IRQ). This was previously discussed in terms of a uasyncio compatible driver using DMA as its underlying mechanism. I believe one of the long term aims of the uasyncio rewrite is to facilitate such drivers.
Peter Hinch
Index to my micropython libraries.

Post Reply