Learn how to use the Raspberry Pi Pico to sample at up to 500 kHz and perform a Fast Fourier Transform on the recorded data.

In this project, we'll leverage some unique capabilities to gather data from the Raspberry Pi Pico's analogue to digital converter (ADC) at a very high rate and then do a Fast Fourier Transform on it. Many tasks, such as those involving audio processing or radio, need this job.

If you're reading this, you probably already have a sensor in mind that you'd like to gather data from. In my situation, I've connected a microphone to the Pico's A0 input. If you're only looking to learn, you may keep the analogue input open and unconnected.

The complete programme is available on GitHub.

The Raspberry Pi Pico's multitude of hardware capabilities spare the CPU from performing regular I/O chores, which is one of the reasons why it's so helpful. We'll utilise the Pico's Direct Memory Access (DMA) module in this situation. This is a hardware feature that allows you to automate operations like moving huge volumes of data from memory to IO at a high pace.

The DMA module may be set up to automatically grab samples from the ADC as soon as they are ready. You can sample at up to 0.5 MHz at its fastest!

After you've gathered all of this information, you'll probably want to process it. Converting your data from the time domain to the frequency domain for additional processing is a typical operation. In my situation, I have a microphone from which I want to gather audio samples and then calculate the samples' highest frequency component. The Fast Fourier Transform is the most often used algorithm for this.

Code for ADC Sampling

I strongly advise you to clone Raspberry Pi's pico-examples library on GitHub if you haven't previously. This is where I obtained all of my first sample code from. The dma_capture example in this repository provided a large chunk of the code used below.

To clarify what's going on, I'll go through several essential features of my software. The complete programme may be found in the Code section.

// set sample rate
adc_set_clkdiv(CLOCK_DIV);

The rate at which the ADC gathers samples is determined by this line. Clock divide (abbreviated as "clkdiv") allows you to split the 48 MHz base clock and sample at a lesser rate. Currently, collecting a single sample takes 96 cycles. This results in a maximum sample rate of 500, 000 samples per second (48, 000, 000 cycles per second / 96 cycles each sample).

You can increase clock divisions to sample at a slower rate. When CLOCK DIV is set to 960, the number of cycles each sample is multiplied by ten, resulting in 50, 000 samples per second. When you set CLOCK DIV to 9600, you get 5, 000 samples per second.

void sample(uint8_t *capture_buf) {
    adc_fifo_drain();
    adc_run(false);

    dma_channel_configure(dma_chan, &cfg,
        capture_buf, // dst
        &adc_hw->fifo, // src
        NSAMP, // transfer count
        true // start immediately
    );

    gpio_put(LED_PIN, 1);
    adc_run(true);
    dma_channel_wait_for_finish_blocking(dma_chan);

The samples from the ADC are collected by this function. The CPU starts sampling after resetting the ADC and draining its buffer. During the sample time, it will also turn on the LED so you can see what's going on.

FFT Code

/ get NSAMP samples at FSAMP
sample(cap_buf);
// fill fourier transform input while subtracting DC component
uint64_t sum = 0;
for (int i=0;i<NSAMP;i++) {sum+=cap_buf[i];}
float avg = (float)sum/NSAMP;
for (int i=0;i<NSAMP;i++) {fft_in[i]=(float)cap_buf[i]-avg;}

The cap_buf array is filled with samples from the ADC in this part, which is then preprocessed for the Fourier transform library. In many cases, subtracting the mean from your data series before applying a Fourier transform to it is helpful. Without this, any DC level (signal offset over zero) will result in large magnitudes in the outputted frequency bins near to zero. Because the package I'm using, KISS FFT, needs signals to be of the type...

Read more »