Measure GPIO pulse width, with nS resolution - use DMA/PIO?

I have 2 puzzles and wonder if anyone can point out a solution to them.

  1. I am using a Pico W, Pico Display Pack 2.0 and MicroPython to make a small oscilloscope. I use DMA to accurately sample the ADC at specific sample time periods and the ADC data is stored into a buffer, which I then read, manipulate and display as a scope display.

The sampling time period for the DMA uses a 48MHz clock and the faster for the Pico ADC is 2uS (500kHz) and so 96 is stored into the DMA ADC clock div register (96 / 48MHz = 2uS period). This clock div register is 16 bits and so the max that can be entered is 65535, which when divided by 48MHz clock gives a sample period = 1365uS (732Hz).

I use core 0 to process the ADC-DMA sampling into a buffer and core 1 to handle the display and change of parameters, etc, using an encoder.

Q: is there any method that this 48MHz ADC-DMA clock can be divided/slowed down, so as to allow a much slower accurate sampling period down to say 1 sec. If the existing 16-bit ADC DMA clock divisor register was 32-bits it could be done but the 16-bit register limits the range. The data sheet shows there is a fractional 8-bit section, but this does not assist and extend the sampling period.

  1. I need to accurately measure the time period/width of a very fast pulse on a GPIO pin. I could read the status of a pin and when it goes low read and store the utime.tick_us() value, and when it returns high, read the utime.tick_us() value then calc the difference to get the pulse width. Apart from the time lag of detecting a pin change, the resolution of the utime.tick_us() is, as the name says, 1uS.

I need to have a resolution of around 20nS as I need to catch a very small change of pulse size, and so time.ticks_us() cannot be used.

A 100MHz clock has a clock period of 10nS and the Pico’s internal clock can be 125MHz or 133MHz and can even be faster.

Q: is there a method to speedily detect the change of status of a GPIO pin, then start a high speed counter (eg using the internal 125MHz) to be able to accurately measure the width of a pulse, with fine resolution.

I wonder if it is possible to use PIO programming to do this, but I do not know how to do this as yet and have never used it.

If anyone has done either of the above, could you please point me in the right direction.

Thanks for looking.

Maybe you could try something like this: pico-MP-modules/PWMCounter at main · phoreglad/pico-MP-modules · GitHub

But in the end I wonder if you should not switch from Python to C. Python has a garbage collector, and this will always be a problem for time-critical stuff.

Thanks @bablokb for the link. It gives 1uS resolution similar to utime.ticks_us, which is a start but I need nS resolution for my project. However, the link prompted me to search for PWM methods and whilst looking I came across this excellent pulse project written by a person called Robert-hh on github, RP2040-Examples/pulses at master · robert-hh/RP2040-Examples · GitHub.

He uses PIO techniques, coded via Micropython, 32-bit registers and can clock in the nS range. It looks most powerful and promising and so I thought I ought share it in case it is use to others. I have a long way to go to fully understand it all and it looks as though it will be a good method to use.

Thanks for the direction.

Nice to hear this helped. Will you publish your oscilloscope work once it is done? That should be interesting to others as well.

The story so far…

I have not sorted out the high speed pulse width section (my question 2) but have found a register in the RP2040 which can I can alter to reduce the ADC-DMA sampling rate and so I have fixed my question 1. I have enclosed my software notes to give an explanation in case it helps others.

I use ADC-DMA sampling to keep accurate sampling periods down to 20.833nS, although 2uS/500KHz is the max speed we can use for the Pico ADC. This ADC-DMA method offloads processing from the Pico cpu, which can get on with other things and it keeps the sampling rate very accurate.

The ADC-DMA sampling clock is derived from the internal 48MHz PLL clock and is first passed through the CLK_ADC_DIV register (rp2040 docs p207). On bootup this CLK_ADC_DIV is set to divide by 1 so the default ADC-DMA clock is 48MHz, ie a 20.83nS clock or 48 clocks per 1uS. This clock then counts down adc_mod.DIV_REG.

The ADC repetitive sampling period is set by storing a countdown value in the u16 b23:b8 bits of the adc_mod.DIV_REG. Using the default 48MHz PLL non-divided clock a value of 48 in this adc_mod.DIV_REG gives a sampling period of 1uS, 96 = 2uS, etc.
When this register clocks down to 0, the sampled ADC data is transferred from the ADC FIFO to your adcbuf via DMA, another sample is initiated and the adc_mod.DIV_REG register countdown starts again, repeating until all requested samples have be completed.

When using the default 48MHz ADC PLL clock the min value in this register should be 96 as the max sampling rate of the Pico ADC is 500KHz = 2uS sample period.

The max value in this u16 register = 65535, so the longest sampling period = 65535/48uS = 1365uS (732Hz).

If you want to reduce the sampling rate, the original 48MHz PLL clock can be divided by altering the CLK_ADC_DIV register (which is at 0x40008000 offset 0x64, RP2040 docs 2.15.7) and the 2 divisor bits are b9:8 to give 1, 2 or 3 division.

b9:8 = 0:1 (256) = div 1 = 48MHz clock, sampling rate = 1 to 1365 uS, resolution = 20.833 nS (default)
b9:8 = 1:0 (512) = div 2 = 24MHz clock, sampling rate = 1 to 2730 uS, resolution = 41.667 nS
b9:8 = 1:1 (768) = div 3 = 16MHz clock, sampling rate = 1 to 4095 uS, resolution = 62.500 nS

The the next part of my project I need to accurately measure the width of varying pulses around 2uS with a resolution down to 50nS or better, using PIO to maintain accuracy which would not be seen when using Python to look for a GPIO change.

1 Like

Further to my above post. If you alter the ADC-DMA source clock from the default 48MHz clock (clksrc_pll_usb) to the 12MHz clock (xosc_clksrc) or 6MHz clock (rosc_clksrc_ph) and then divide that clock by 3 before it passes to the adc-dma DIV_REG, you can slow the sampling period down tremendously, each sample being accurately timed by the PIO.

From my notes, using the rosc_clksrc_ph clock:
ADC SYS clk: num = 2, name = rosc_clksrc_ph, freq = 6000000 Hz, div by = 2 which passes 3000000.0 Hz clk to adc_mod.DIV_REG (1…65535)

adc_mod.DIV_REG clk: freq = 3000000.0 Hz, min adc sampling period with 1 in reg = 0.3333333uS, max period with 65535 in reg = 21845.0 uS

Fastest time to fill 320 samples in adcbuf when adc_mod.DIV_REG = 1 = 106.6667 uS

Slowest time to fill 320 samples in adcbuf when adc_mod.DIV_REG = 65535 = 6990.4 mS = 6.9904 sec

I hope it assists

1 Like