In this project, we'll use some special features to capture data at an extremely fast rate from the Raspberry Pi Pico's analog to digital converter (ADC) and then compute a Fast Fourier Transform on the data. This is a common task for many projects, such as those that involve audio processing or radio.
If you're reading this post, chances are you already have a sensor in mind from which you wish to collect data. In my case, I have a microphone wired to my Pico's A0 input. If you're here just to learn, you can just leave the analog input floating with nothing connected.
You can find the full program on GitHub.
1. BackgroundA major reason why the Raspberry Pi Pico is so useful is its plethora of hardware features that free the processor from performing routine I/O tasks. In our case, we'll use the Pico's Direct Memory Access (DMA) module. This is a hardware feature that can automate tasks involving transferring large amounts of data in and out of memory to IO at an extremely fast rate.
The DMA module can be configured to pull samples out of the ADC as soon as they are ready. At its fastest, you can sample at up to 0.5 MHz!
After collecting all this data, chances are you'll want to do some processing on it. A common task is converting your information from the time domain into the frequency domain for further processing. In my case, I have a microphone from which I want to collect audio samples and then compute the maximum frequency component contained in the samples. The most common algorithm used for this is the Fast Fourier Transform.
2. ADC Sampling CodeIf you haven't done so already, I highly recommend cloning Raspberry Pi's pico-examples library on GitHub. This is where I got all the sampling code I used to get started. A good portion of the code below comes from the dma_capture example in this repository.
I'll go through some key elements of my software to explain what's going on. You can find the full program in the Code section.
// set sample rate
adc_set_clkdiv(CLOCK_DIV);
This line determines how fast the ADC collects samples. "clkdiv" refers to clock divide, which allows you to split the 48 MHz base clock to sample at a lower rate. Currently, one sample takes 96 cycles to collect. This yields a maximum sample rate of 48, 000, 000 cycles per second / 96 cycles per sample = 500, 000 samples per second.
In order to sample slower, you can increase clock divisions. Setting CLOCK_DIV to 960 is a 10x increase in the number of cycles per sample, which yields 50, 000 samples per second. Setting CLOCK_DIV to 9600 yields, you guessed it, 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);
gpio_put(LED_PIN, 0);
}
This function actually collects the samples from the ADC. The processor resets the ADC, drains its buffer, then starts sampling. It'll also turn the LED on during the sampling period so you can see what's going on.
3. 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;}
This section above fills the cap_buf array with samples from the ADC, then preprocesses it for the Fourier transform library. For many applications, it's advantageous to subtract the mean from your sequence of data before you apply a Fourier transform to it. Without this, any DC level (signal offset above zero) will cause the outputted frequency bins close to zero to have huge magnitudes. The library I use, KISS FFT, expects signals to have a type of float so I also convert the samples while subtracting out 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);
This next section computes the FFT and then calculates the greatest frequency component in the outputted data. Outputs from an FFT are complex-valued, so to get a useable power value you can take the magnitude of the complex result.
Also notice that instead of looping through all NSAMP output values of the FFT we're only going to bin NSAMP/2. Due to the Nyquist Sampling Theorem, any frequencies greater than 1/2 the sampling rate will be aliased together and thus these bins are not useful to us. This is a fundamental result in signal processing that's worth investigating further if you're not familiar!
In the case of audio, the human ear can generally hear frequencies up to around 20 kHz. I'm using a CLOCK_DIV value of 960, yielding a sample rate of 50 kHz. The largest unaliased frequency I can capture is therefore 25 kHz, which should be more than enough!
// 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 last bit of code to point out is NSAMP, or the number of samples collected. In signal processing, there's a fundamental tradeoff between higher and lower number of samples. More samples will take longer to collect and process, but will yield higher-resolution Fourier transforms. Fewer samples will result in a shorter sampling period and faster processing, but your Fourier transform will be more granular.
In the case of the Pico, I've found that allocating too much memory produces a hard-to-debug failure. If you set NSAMP too large, your Pico will not have enough memory to allocate to the arrays that hold the samples. The code will still compile and upload just fine, but you will likely get some strange behavior. In my example, keeping NSAMP under 9000 seemed to be fine.
3. Compiling and UploadingIf you haven't done so already, download Getting Started with Raspberry Pi Pico. This is a solid resource that gives you everything you need to setup your build system and compile and upload C/C++ code to your Pico.
All the instructions below are for macOS/Linux, but I imagine there's a similar procedure for CMake on Windows.
- To compile my code, first clone my repository on GitHub.
- Navigate to the adc_fft directory
- Make a directory called "build"
- Navigate inside that, and type "cmake../"
- Type "make", and everything should compile if you installed the Pico build system correctly
- Put your Pico into bootloader mode, and then drag and drop the adc_fft.uf2 file into the drive that appears
That should be all! You can monitor the output of the program via USB. It will output the greatest frequency component in data sampled from A0, and the LED should flash rapidly.
In my case, I connected a microphone to the analog pin and verified that my code was correct by feeding the microphone tones from a speaker. Let me know if you have any questions!
Comments