CDM324 Doppler Motion Sensor Backpack, with FFTs!

A very small form factor project, not only measuring an object's speed but also displaying it!


If you follow this blog, you may have noticed that this is my third project using doppler sensors, and my second with the CDM324. This tiny sensor is just so neat for its price!

Doppler effect
A quick refresher on the Doppler effect: when sending an RF tone at a given frequency towards a moving target, the reflected signal's frequency will be shifted. This is the reason why a fire truck's siren has a higher pitch when the truck is going towards you than when it is still.

And what's nice with the CDM324 sensor above is that it actually outputs that frequency shift, so you "just" need to measure that frequency to compute the moving object's speed!

The project in action

Before I start getting into the technical details of this project, I figured you may want to see it in action:

Why re-do this project? Why is it better now?

I learned quite a few things about electronics since I first made that amplification circuit for the CDM324 6 years ago.
Here are the main changes implemented in this new version:
- using an FFT to extract the measured speed from the CDM324 output
- using an automatic gain circuit to increase the detection range
- adding an onboard speed display for reading convenience
Computing the Fast Fourrier Transform (FFT) of a given signal esssentially extracts all its frequency contents.
It is therefore more reliable and sensitive than the simple comparator circuit I had previously used, as you effectively look for a big peak in the FFT output and divide its X coordinate by a constant to get your detected object speed.
Sure, it required adding a microcontroller to first digitize that analog signal, but the detected speed can now directly be output through UART.
The previous CDM324 amplification circuit had 84dB of gain which in some cases was not enough. Instead of two fixed gain stages, I'm now using one fixed gain stage and a variable one for up to 92dB of gain (or 40000x voltage gain!). This second gain stage has automatic gain control, automatically varying the overall gain between 72dB and 92dB to hopefully not saturate the overall amplification circuit output.
Finally, as it didn't really make sense for the previous project to require an extra micro-controller to compute the detected speed, this new CDM324 project includes a small display directly displaying the detected object speed to the user.

Digging into the Analog Circuit

Starting from left to right, you'll first see the fixed gain stage of 130 (42.3dB) with a 7.2Hz high pass filter (C2 & R1) and a 6.1kHz low pass filter (R3 & C4). This allows progressively filtering out speeds not between 0.16km/h and 138km/h.
This particular op-amp was selected for its low input noise and high power-supply rejection ratio.
As it's only powered by 3v3, its positive input is set to a fixed voltage generated by the next amplification IC. This effectively allows us to bias (or center) the op-amp output to a voltage between its two power supply rails (0V and 3.3V).
The second amplification stage... is a microphone amplifier!
Thinking about it, it made perfect sense using an amplifier made for the exact same frequencies I was interested in, especially when it comes with automatic gain circuitry. It'll therefore automatically amplify less if your speeding object is close to the sensor, and amplify more if the the object is far away. Cherry on top of the cake, its output voltage is already centered around 1.23V so there's no need to re-bias it before feeding it to the microcontroller's Analog to Digital Converter (ADC).

What About the Digital Circuitry?

The digital circuitry is simply composed of an STM32F301 microcontroller and an I2C-controlled LCD segment driver.
The STM32F301 was selected for its small package, relatively low price and Cortex M4 core which can easily compute FFTs (more on that later).

As there was still some board space available during the layout stage, I also broke out a full I2C and SPI bus to 2 expansion connectors... and that's on top of the existing UART bus for MCU flashing and the spare IOs/reset/boot signals!
I therefore fully hope to develop some expansion boards that would plug to that module.

A TN display was selected for its size and readability: it's of the same type that the ones used in multimeters.
Reading the measured speed in a bright environment therefore shouldn't be an issue! It's however quite tricky to drive as it requires some interesting waveforms, and while I had thought of letting the MCU directly drive it for a moment, the number of resistors required to do so made me go for the embedded controller you see in the above schematics. Interestingly, you'll find online some people who did manage to pull it off!
As a side note, I had originally gone for a 7 segments display, but the current transient when changing digits was so high that the introduced perturbation on the CDM324 5V rail would lead to glitches in its output:

Things Worth Mentioning

I figured I'd make a dedicated paragraph to mention the things I really like about this little board.
No programmers are required to update the MCU: the STM32F301 comes with an embedded UART bootloader! By simply connecting a USB-UART adapter to the dedicated connector, tying the BOOT0 pin to 3V3 and then briefly bringing the RESET pin to ground, the STM32F301 will enter bootloader mode.
The overall circuit is very simple and expandable: there are only a handful of components and the two expansion connectors with I2C, SPI, UART and IOs will hopefully enable projects from electronics enthusiasts!


The folks at were kind enough to sponsor this project!
I'm pretty sure this is the one of the most compact PCB I've ever laid out. It's only 32 by 32.5mm and is made to be put behind the CDM324. A general rule when amplifying small signals is to have as short traces as possible!

Measuring the Transfer Function

To make sure the amplification chain could sense the speeds I was interested in, I needed to measure the circuit amplification across frequency.
To do so, I used my Bode100 but could definitely had manually done it using a signal generator and an oscilloscope (it just would have taken longer). Given the extremely high amplification present in the analog circuitry, I used many attenuators (for a total of 80dB!) at the Bode100 output to present a -100dBm signal at the amplification chain input (that's 6.3uV peak peak).
Using a large averaging factor and a 10Hz measurement bandwidth, the following graph was gotten 10 hours later:

Let's ignore for the moment the overall gain and focus on the -3dB corner frequencies, even though it's a bit ironic to do so when dealing with such a high circuit gain. While the measured low pass corner frequency was bang on (6.1kHz), the same couldn't be said of the high pass filter corner frequency. The reason was then found inside the microphone amplifier datasheet:

Even though this is actually fine as a 100Hz frequency corresponds to a 2.3km/h speed, I still took that opportunity to update my first amplification stage to the following:

I then ran another frequency sweep (this time with 70dB of external attenuation) and got this:

... and if we zoom in between the -3dB corner frequencies:

Taking into account the 70dB of external attenuation, the overall amplification gain ended up being 95.4dB, with 3dB corner frequencies of 150Hz (3.4km/h) and ~7.9kHz (180km/h).
Finally, here's a very interesting way to test the microphone amplifier automatic gain control feature: in the below measurement, I not only perform a frequency sweep but also a power sweep:

Given that I'm linearly sweeping between -90dBm and -60dBm between 1kHz and 1.3kHz, we can conclude that the AGC starts kicking at an input power of -89.9dBm and seems to stop intervening at -66.3dBm. This 23.6dB range is however a bit high as the datasheet mentions 20dB.

Checking for Oscillations

An extremely common issue when having an amplification chain with such a massive gain is oscillations: without any signal present at your device input, when probing your output you'd typically see a rogue sine wave.
This happens when some of your output signal gets coupled into the device input (imagine putting a live microphone in front of speakers).
But how does it get coupled? you may then ask.
In my experience either through electromagnetic coupling (output PCB trace close to the input trace) or through power supplies (a current draw proportional to the output signal introduces a voltage drop on the first amplifier supply which then gets coupled into the input signal).
While one could typically use an oscilloscope to measure the amplification chain output signal to check for small oscillations, one would be limited by said oscilloscope input noise level. One way to get around that could be to use my low noise amplifier board but I instead went with my SA44B spectrum analyzer and got the following spectrum:

The black trace is the output spectrum with a CDM324 connected and the blue trace is the output spectrum with a 50 ohms resistor connected to the amplification chain input.
The blue curve being below the black curve means that my amplification chain noise floor is below the CDM324 output "noise"! This means the complete device is "limited" by the CDM324.... which is what I wanted to see.
Also worth mentioning is the relative lack of pronounced spikes that could be caused the AC grid frequency, TN display scanning or USB lines: remember that the above measurement was taken with a very sensitive spectrum analyzer while the cdm324 was standing still. What really matters is that the spectrum computed by the STM32 is flat when nothing is moving around.
If we "walk back" the amplification chain we can try to compute our amplification chain input noise rating: -80dB (noise level at 1kHz) + 21dB (500 / 50r resistor divider at the specan input) - 95.4dB (circuit amplification) = -154.4dB/sq(Hz) = 4.26nV RMS/sq(Hz).
Comparing that value to the theoretical one can be done using the following formula from that document (from page 6):

Rather than doing these math however, one may notice that this 4.26nV/sq(Hz) measurement is already below the 4k7 input resistor johnson noise of 8.7nV/sq(Hz)... and I'm not even mentioning the LMP7731 input voltage noise (2.7nV/sq(Hz)) and LMP7731 input current noise (1.1pA/sq(Hz)) going to the 4k7//620k.
I wish I had an explanation for that difference... but I repeated that measurement several times, even used a signal generator and a table top multimeter to double check the circuit amplification (I actually measured more than 95.4dB)... but still couldn't find an explanation on that better than expected result. If you're reading this and have an idea, send me a message and I'll send you a device for free!

Mechanical Assembly

Similarly to my previous CDM324 project, I've designed and 3D printed a small support to hold everything together.
The overal assembly only measures 36x36.5x17.5mm. Tiny!

MCU Performance & ADC Sampling Rate

You may know that FFT computation is quite resource intensive.
In a "release' build configuration, by toggling a MCU pin I measured that 2.4ms was needed to:
- convert 1024 ADC samples to floats
- compute a 1024 points FFT
- compute the magnitude of the FFT output vector elements
This effectively translates to a maximum of 417 FFTs per second, which is more than enough for our purposes!
I therefore selected an ADC sampling rate of 35.7ks/s, meaning that for a 1024 samples buffer I should theoretically be able to measure frequencies between 34.9Hz and 17.8KHz (or a minimum of 0.8km/h). That also meant an FFT frequency bin width of 0.8km/h (0.5mph).
By using double buffering (one buffer filling with data while another is getting processed), 34.9 FFTs per second can be computed. If we want to stream raw ADC values and FFT data through UART, a minimum bit rate of 34.9*(1024*uint16 + 512*float32)*10/8 = 1428kb/s is required. But because of baud rate incompatibilities between the MCU & possible USB to UART adapters, that actually means a 2Mbit/s minimum baud rate.
However, it turned out that many USB to UART adapters couldn't handle a continuous 2Mbit/s stream.
In the scripts located at the end of this page, you'll therefore notice that I actually use 1Mbit/s, and am only sending the "interesting" part of the FFT data.
One last thing to mention: I did find that the required impedance between signal source and ADC input needed to be small, as otherwise what seems to be a current bias would be introduced during the sampling phase.... odd.

On the topic of Hardware Abstraction Layers

When it comes to Hardware Abstraction Layers (HALs) everyone has his/her own opinions.
I personally tried several of them (for different platforms and IDEs) and until now have been quite against using them as it'd always take me as much time learning the HAL as I'd spend reading the datasheet.
With the STM32 environment, things are quite different: you actually get a nice graphical user interface (STM32CubeMX, also embedded in the STM32CubeIDE) that allows you to fully configure your microcontroller and its internal peripherals (think Clocks, SPIs, UARTs, Timers, Interrupts, DMA transfers...). Once you save your .ioc file, the IDE will automatically generate the HAL initialization code and interrupt routines in 3 files (main.c, stm32f3xx_hal_msp.c, stm32f3xx_it.c) and you'll be more or less good to go.
Why more or less? Well in the case of configured timers, it'll still be up to you to call the actual HAL start routines depending on your use case (HAL_TIM_Base_Start, HAL_TIM_Base_Start_IT, HAL_TIM_Base_Start_DMA...). Luckily, given the STM32 platform is used all over the globe, the time between realizing something doesn't work and the time you find a solution online will be less than a few minutes. Getting something up and running is therefore extremely quick.
However... there are times where the code generation tool messes with your complete software solution. It happened to me once that when changing an SPI baud rate in STM32CubeMX and saving the changes my platform wouldn't boot anymore. After comparing my main.c with a previous version (don't forget to use code versioning!) I discovered that the generated code initializing the external crystal driver had simply disappeared!
Moreover, while relying on this code generation tool is great when wanting to rapidly tweak a few settings... it actually prevents you from compartimentalizing your code in different files. Even though I managed to do so, identifying all the functions and global variables that are required for a given feature rapidly gets annoying (think interrupt routines for peripherals and DMA transfers, end of transfer HAL callbacks, HAL descriptor objects...).
You also obviously get a performance hit as the HAL performs a few checks before letting you do what you'd like to. For a simple 24bits SPI TX transfer I actually measured a performance penalty factor of 9 between using the HAL and directly writing the registers myself. STM however does also provide a Low Layer API (LL), with "better optimization but less portability, requiring deep knowledge of the MCU and peripheral specifications."... but at this point you may as well go bare-metal :).

Writing a Debug GUI

Can you guess the cars passing me by and the ones stopping next to me?
As previously mentioned, the onboard MCU can use its UART peripheral to output data to the outside world.
For debugging purposes (and nice demo effect), I therefore wrote a python GUI that leverages the Enthought Chaco framework (itself based on qt4) to display live data on your computer screen.
Depending on your computer configuration, it can (surprisingly) handle a 1600*1200 pixels window at a refresh rate of 34 frames per second to show raw ADC samples, computed FFT data and detected peaks over time.

So sensitive that it can debug your computer

Remember when I mentioned that I switched from a 7 segment display to a TN display because of voltage transients?
In the picture above you can see the output spectrogram when no movement is happening in front of the sensor. At that moment, the AGC is not kicking in and the gain is at its maximum (94dB). That means the board amplifies.... everything, and that includes potential noise present on your USB power supply!
So what happened at the end of that green arrow? I'm pretty sure it has something to do with either my trackpad or power saving features as this transition only happens when not touching my laptop for 30 seconds. I'm assuming the noise is picked up through the USB +5V power line, but it could also also be picked up through RF.... fun, right?

A way to improve things is to use a USB isolator as the one pictured above though!

Making a Companion Board

As it seems I was particularly motivated by this project, I then took some additional time to make a breakout board.
It offers the following features:
- a USB to UART converter, connected to the RESET & BOOT0 lines
- a jack connector so you can listen to the doppler noise
- a switch to toggle between MPH and KM/H display
- a uSD card connector to log speeds
That 1.6mm PCB with its two 8pins connectors means that it's firmly attached to the CDM324 module!
Something great that is now enabled is one click firmware updates using the RTS & DTS lines of the USB-UART converter and the stm32loader project... how awesome is that? All you need to type is:

Tying it all together: making a Control GUI

Rather than having a collection of scripts, I decided to make a single python tool that:
- automatically fetches and displays the detected object's speed
- launches the speed debug tool I previously mentioned
- allows you to flash the device
This is just the beginning, pull requests are strongly welcome!

Project source files

As with all my other projects, you'll find all source files made for this project here!
Want to buy the complete assembled device? Have a look at my US shop or EU shop!
Thanks a lot for reading!


1. On Friday, August 25 2023, 17:30 by SROBINSON

This looks amazing. Are the parts available for purchase and shipping to the US?

2. On Tuesday, August 29 2023, 16:07 by limpkin

@SROBINSON: of course:

3. On Friday, September 1 2023, 15:20 by Bob

The article does not give the range being detected, as far as I can see.

How accurate is the data from different distances?

4. On Sunday, September 3 2023, 10:09 by limpkin

@Bob: The range is highly dependent on the moving object's material, so I can't provide you with a clear cut answer I'm afraid. For a car it can be 40 meters for example. The data will always be accurate as the doppler effect doesn't depend on distance

5. On Monday, September 4 2023, 13:01 by Bj

Hi, I was wondering if this could be used to measure the speed of a softball/baseball? My daughter is a softball pitcher and it would be interesting the measure the speed of her pitches from the position of the catcher. There are commercial products but the are expensive Also could this be adapted to drive a large display like tjis to be seen from further away?

6. On Tuesday, September 5 2023, 09:42 by limpkin

@Bj: placed close enough, the sensor could absolutely measure that speed... and you could use one of the spare bus on the connectors to interface with a (custom) display of some sort yes.

7. On Saturday, September 23 2023, 20:28 by vagyver

Hello! Excellent job and explaination too!

I would like to use it to measure the speed of tennis ball.
Would it be capable? If yes, how close enough have to place it?

Will be any benefit at high speed balls if i increase low pass filter from 7.9kHz to 9-10 khz but at the same time make higher the high pass from 72 Hz to 200-250 hz ? (i don't care about low speed balls).

8. On Wednesday, September 27 2023, 12:41 by PeterF

Hi Mathieu,
Many thanks for designing and producing this CDM324 project. I purchased a complete kit from your online Tindie store (CDM324 sensor display module, expander board and USB isolator. The kit arrived within 8 days from China and worked first time on assembly and powering up.

I was very pleased to see that the ST32F301 had already been programmed with the CDM324 software - many thanks, this has saved me hours, since I'm not a Windows 10 user and normally use Linux and Chromebooks.

I haven't had a chance to 'borrow' a Windows machine to try out the GUI debug software, but have used a Python script to manually read the mph and km data over the serial port by issuing 'm' and 'k' command characters respectively, to retrieve the latest speed values .

I did notice that by issuing 'a' there is a continuous stream of data output that varies if I wave my hand in front of the radar sensor. I could not find the command to switch back to 'normal' mode. Is this mode normally controlled by the GUI debug software.

I couldn't find any information about SD card operation, is this already programmed into the STF301? I was just a bit worried about turning the power on and off to the back pack if an SD card was present.

Thank you once again for all the time and effort you have put into this project, it is much appreciated and has helped me greatly progress with the building of a vehicle speed monitor for the local community.

Kind Regards,

9. On Wednesday, September 27 2023, 21:19 by limpkin

@vagyver: thanks a lot! that's a tough one... would say fairly close (<30cm) as the ball is not metallic. I think you could only modify the low pass filter without touching the high pass one :)

@Peterf: thanks for the kind words, they mean a lot! It seems you have a good overview of the ecosystem :). The counterpart of 'a' is 's'.
About SD card: nothing is programmed at the moment, but the low level routines are there:
You may need to format the card with a 512 block size. Send me a quick email at (mydomainname)@(mydomainname).fr for faster communication :)

10. On Friday, September 29 2023, 09:47 by Will


I've just received my board and it works amazingly! I'm hoping to tie this into another arduino board to run as a speed sign.

Is there a way to get it to only show the speed of things coming towards the sensor, rather than away from it?


11. On Friday, September 29 2023, 13:28 by limpkin


Thanks a lot for the kind words!
At the moment I haven't coded this functionality yet, but in theory it shouldn't be that complex as we would need to store the "FFT energy" and check if it increases or decreases over time to know if the object is moving toward / away from the device :)

Add a comment

Comments can be formatted using a simple wiki syntax.

They posted on the same topic

Trackback URL :

This post's comments feed