Breaking

Raspberry Pi Pico: ADC Sampling and FFT

 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.

Raspberry Pi Pico  ADC Sampling and FFT

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
Raspberry Pi Pico ADC Sampling and FFT
Image credit: Alex Wulff

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 float, I convert the samples before subtracting the mean.
// compute fast fourier transform
kiss_fftr(cfg , fft_in, fft_out);
// compute power and calculate max freq component
float max_power = 0;
int max_idx = 0;
// any frequency bin over NSAMP/2 is aliased (nyquist sampling theorum)
for (int i = 0; i < NSAMP/2; i++) {
float power = fft_out[i].r*fft_out[i].r+fft_out[i].i*fft_out[i].i;
if (power>max_power) {
max_power=power;
max_idx = i;
}
}

float max_freq = freqs[max_idx];
printf("Greatest Frequency Component: %0.1f Hz\n",max_freq);
The FFT is computed in the following part, followed by the largest frequency component in the outputted data. Because FFT outputs are complex-valued, you may use the magnitude of the complex result to get a useful power value.

Also, rather of cycling over all of the FFT's NSAMP output values, we'll only bin NSAMP/2. Any frequencies larger than 1/2 the sample rate will be aliased together due to the Nyquist Sampling Theorem, therefore these bins are useless to us. If you're unfamiliar with signal processing, this is a basic finding worth studying more!

The human ear can typically perceive frequencies up to about 20 kHz in audio. I'm using a CLOCK DIV of 960, which corresponds to a sample rate of 50 kHz. As a result, the highest unaliased frequency I can capture is 25 kHz, which should be plenty!

// BE CAREFUL: anything over about 9000 here will cause things
// to silently break. The code will compile and upload, but due
// to memory issues nothing will work properly
#define NSAMP 1000
The number of samples collected, or NSAMP, is the last piece of code to mention. In signal processing, there is a basic tradeoff between having a large number of samples and having a small number of samples. It will take longer to gather and analyze more data, but the higher-resolution Fourier transforms will be worth it. Fewer samples mean a shorter sampling time and faster processing, but your data will be less accurate. Your Fourier transform, on the other hand, will be more granular.

I've discovered that allocating too much RAM on the Pico results in a difficult-to-debug failure. If you make NSAMP too big, your Pico won't be able to allocate enough memory to the sample arrays. The code will still build and upload, but odd behaviour will most likely result. Keeping NSAMP below 9000 appeared to work great in my case.

Uploading and Compiling

Download Getting Started with Raspberry Pi Pico if you haven't already. This is a great resource that will walk you through setting up your build system as well as compiling and uploading C/C++ code to your Pico.

All of the methods here are for macOS/Linux, although I'm sure CMake on Windows has a similar approach.

  • To compile my code, clone my GitHub repository first.
  • Go to the adc fft directory and open it.
  • Create a "build" directory.
  • Navigate to that folder and type "cmake../" If you installed the Pico build system correctly, everything should compile.
  • Put your Pico in bootloader mode, then drag & drop the adc fft.uf2 file into the disc that appears on the screen.

That ought to be it! The program's output may be monitored via USB. It will output the data sampled from A0 with the highest frequency component, and the LED should flash fast.

In my situation, I attached a microphone to the analogue pin and used a speaker to feed the microphone tones to ensure that my code was accurate.

Credit To Author: Alex Wulff

Posts You May like:


Popular Posts