CDM324 Doppler Speed Sensor, with FFTs!
A very small form factor project, not only measuring an object's speed but also displaying it!
With its UART interface, interfacing with Arduino platforms is dead simple too!
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!
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 PCBWay.com were kind enough to sponsor this project!
As usual, the quality was great, shipping was fast with DHL, PCB assembly was without any issue so the PCB worked right away... these days I always go with them.
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!
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!
Want to interface that sensor to an Arduino? Have a look at the getting started instructions.
Thanks a lot for reading!