In this guide, we will be learning how to use the RD-03D radar sensor with a Raspberry Pi Pico so you can detect and track humans in your next project. We will be looking at how it operates and what you will get out of it, how to connect it to your Pico and read data from it, and to top it all off we will be creating some fun radar visualisations and a short little project that measures your sprinting speed and compares it to Usain Bolt's 100 meter world record. Lets get into it!
We also have a version of this guide for an Arduino with C++ or Raspberry Pi 5 with Python if you want to check them out. |
If you are coming from an external site and looking for the library, you can download it here!
How the Radar Works and What Data it Outputs
The Rd-03D is a 24 GHz millimetre wave radar sensor, and it really is a testament to our current state of technology as it achieves a great deal of things for such an inexpensive device.
This sensor emits radio waves around the 24 GHz frequency and listens for the reflections that return, kind of like an ultrasonic distance sensor. However, this sensor is far more advanced. Not only does it use the time it takes for those radio waves to reflect back to measure the distance of a person (by the way those radio waves are travelling at the speed of light), but it is also listening for any shift in frequencies from the Doppler effect to measure how fast the person is moving. This is the same effect that causes a car to change pitch as it passes you and the greater the shift in pitch (caused by a shift in frequency), the the greater the speed of the car. This same principle works with radar waves bouncing off a human target. A very important note here is that this sensor will only be able to measure the speed of a target in the direction towards or away from the sensor.
On top of that, it also has 2 receiver antennas that are used to detect the angle of a target. If a radio wave is emitted it bounces off a human to the side of the sensor, as the radio wave returns to the sensor it will hit one of the antennas before the other and this is used to detect the angle of the sensor. With that radio wave travelling at the speed of light, there is less than 0.025 nanosecond between it hitting the 2 different receivers. The fastest human blink is 100,000 nanoseconds, yet it can take that incomprehensibly small time and accurately figure out the angle the person is at relative to the sensor.
But how does it only detect people (and maybe other animals that are large enough)? Well, this is really where this sensor does some magic. When it emits those radio waves, most of the objects in the room reflect - your chair, the wall, your computer, your phone, so how does it know what's human? A very streamlined explanation is that humans have unique patterns in the waves they reflect. Like how visible light (which starts at 400,000 Ghz) reflects differently on different materials, our 24 GHz radio waves reflect differently on humans. Humans are also constantly moving creatures, everything from your breathing to the slight movement in your arms creates micro-doppler shifts as they move toward and away from the sensor. Humans are a certain "brightness" to the radar and are a sea of micro-doppler shifts, and with the onboard processing, it uses these to figure out what and where a human is.
If you took this sensor back 40-50 years in time, the signal processing alone would baffle the smart people on Earth, and it would seem like magic to even a specialised radar engineer.
So with this sensor, you will be able to use 24 GHz radio waves to detect a person, how far away they are from the sensor (up to its 8-meter range), what angle they are relative to the sensor, and how far they are moving toward or away from the sensor. It should also be noted that this sensor has to be stationary - it cannot move around. We are not entirely sure why, but we guess that it runs some sort of room calibration to get a baseline reading. If you move the sensor, this calibration is thrown out of whack and it gives bad readings. Ensure that it stands still, we will talk more about mounting this later.
Now that you know how it works and what you can expect of it, let's learn how to use it!
What You Will Need
To follow along you will need:
- The RD-03D radar sensor.
- A method of connecting the Rd-03D sensor. There are contact pads that you can solder your own wire to, or you can grab a jumper wire adapter for the onboard connector. The board DOES NOT come with one of these.
- A Raspberry Pi PIco. We are using the Pico 2 as it's the first one we grabbed out of our microcontroller box, but this should work with nearly any variant.
- Some jumper wires to connect everything with.
Connecting it to The Pico
If you are using the jumper wire adapter, connect it to the Rd-03D. However, please note that the cable we have linked is a generic one and the colours may not match the board very well. For example, mine has red and black as the TX and RX pins, with green as the 5 Volt pin and yellow as the Ground pin. Don't rely on the colours of the cables too much here and always follow the pin label from the board instead.
First, let's power the radar by connecting its 5 Volt pin to the Pico's Vbus pin, and the ground pin of the radar to one of the Pico's ground pins. This radar needs 5 Volts to operate which will be supplied through Vbus when it is connected with USB. If you are not powering the Pico through USB, you will need to supply it with another 5-volt source capable of providing the 200 milliamps the radar needs to operate.
The radar outputs all of its data through UART so we will connect the radar's TX pin to pin 1 of the Pico (an RX pin). We will also be sending commands to the radar so connect the RX pin of the radar to pin 0 of the Pico (a TX pin). Remember, you transmit on a TX pin and receive on an RX pin so "RX to TX and TX to RX".
You will likely need to use jumper wires to make these connections, if so this is a good opportunity to use some more appropriately coloured wires (e.g. using red and black jumper wires for your power pins).
Programming the Pico and Using the Library
We have written a basic library that greatly simplifies the process of using the module and you can download it here:
Download and unzip the file. Then in Thonny, ensure that you have the file explorer side menu open. You can open this by opening the View tab at the top and ticking Files. On the top window (which is your computer's storage), navigate to the unzipped library, right-click it and upload it to the Pico like in the image on the right.
After uploading you can confirm it is now on your Pico as it should be visible in the lower window (which is your Pico's storage).
This library handles all of the UART interactions with the radar sensor, as well as decoding the string of hex values it sends, turning it into nice and human-readable numbers. The following code is a demonstration of how to use it:
from rd03d import RD03D import time radar = RD03D() # use defaults, multi-target mode and pins GP0/GP1 radar.set_multi_mode(False) # Switch to multi-target while True: if radar.update(): target1 = radar.get_target(1) target2 = radar.get_target(2) target3 = radar.get_target(3) print('1 dist:', target1.distance, 'mm Angle:', target1.angle, " deg Speed:", target1.speed, "cm/s X:", target1.x, "mm Y:", target1.y, "mm") print('2 dist:', target2.distance, 'mm Angle:', target2.angle, " deg Speed:", target2.speed, "cm/s X:", target2.x, "mm Y:", target2.y, "mm") print('3 dist:', target3.distance, 'mm Angle:', target3.angle, " deg Speed:", target3.speed, "cm/s X:", target3.x, "mm Y:", target3.y, "mm \n") else: print('No radar data received.')
Before we run that, let's take a quick look at how the demo code and library work.. Starting we import the libraries we need (including the library we just installed), then we create an instance of the radar. After that, we have an initialisation option for whether we want to use it in single-target mode or multi-target mode:
from rd03d import RD03D import time radar = RD03D() # use defaults, multi-target mode and pins GP0/GP1 radar.set_multi_mode(False) # Switch to multi-target
Multi-target mode can track up to 3 targets at once but it can be a bit hit and miss sometimes, especially when people are close to the sensor. It also opens up a world of false detections from reflections. The radar's waves can bounce off walls, TVs, or any large flat surfaces to create a false reading, almost like there being a mirror on the wall and seeing the same person twice. Single-target mode will always pick up the strongest and most likely target, which will eliminate a lot of reflections as they are weaker. For reliability, single-target may be the go-to, but it's worth trying if multi-target will work in your set-up.
With the radar now set up, we can call radar.update() which will tell the library to pull the next reading from the sensor and hold onto it for us. It will hold onto that reading until we call the update command again. When the library does this it will also return true if it gets a valid reading, or false if something goes wrong (like if it gets a corrupt UART reading or a wire is loose). In the demo code, we have this command inside an if statement which means it will only do what we nest inside if it gets a valid reading. This is just some nice and condensed error handling - if we got an incomplete reading and tried to print it out, our Pico may crash.
while True: if radar.update(): Do what we want with the code in here... else: print('No radar data received.')
With the information waiting in the library, all we need to do now is grab it and do whatever we need to do with it. The code below demonstrates how we can get data for each of the 3 targets that can be detected. If you try to get information for target 3 in single-target mode, or if it doesn't detect a 3rd person, it will just give you zero for all the data points.
target1 = radar.get_target(1) target2 = radar.get_target(2) target3 = radar.get_target(3)
Each target variable now has a few bits of information stored in it with something called a structure. To get the individual bits of information out of it we just need to use a full stop and then the name of the information. Below is an example of printing out all the data points stored in the variables:
print('1 dist:', target1.distance, 'mm Angle:', target1.angle, " deg Speed:", target1.speed, "cm/s X:", target1.x, "mm Y:", target1.y, "mm") print('2 dist:', target2.distance, 'mm Angle:', target2.angle, " deg Speed:", target2.speed, "cm/s X:", target2.x, "mm Y:", target2.y, "mm") print('3 dist:', target3.distance, 'mm Angle:', target3.angle, " deg Speed:", target3.speed, "cm/s X:", target3.x, "mm Y:", target3.y, "mm \n")
Run that code and you should see all of this data being printed out in the shell. Distance, angle and speed are all pretty straightforward, but the x and y can be a bit tricky. The X-axis is a measurement of how far a person is away from the centre line of the radar, and the Y-axis is a measurement of how far a person is from the parallel of the radar. The image below shows both of these. Also note that both the X-axis measurement and angle have positive and negative values which are also shown in the image below. The maximum angles of this sensor range from -60 degrees to +60 degrees.
Some quick tips on using this in your project:
- This is only set up to detect one person. If you have multiple people in front of the sensor, it will track the person with the strongest return - this is usually the person who is the closest to it.
- If you are going to mount this somewhere to detect people, place it between chest and head height for the best results.
- Radio waves will bounce off surfaces. When testing it, sometimes I was directly behind a sensor yet it was picking up phantom targets several meters away. This was the radar waves bouncing off the wall and seeing me in the reflection like a mirror. If there are random readings it is not a ghost, it is likely picking up a reflection of a person.
- If you are using multiple of these sensors they will interfere. Ensure they are far enough apart from each other so that they don't pick up each other's signals.
Radar Visualisation with Processing IDE
We are now going to visualise that data with Processing IDE, a fantastic open-source cross-platform program that is an easy way to create visuals with data from a microcontroller. Go ahead and download and install Processing IDE.
Before we use it though, paste the following code into Thonny and save it to the pico as a file called "main.py". When the Pico is powered on it will try to look for this file and automatically run the code in it. After saving it to the Pico, you should now be able to plug it in via USB to any device and it will output the sensor data through USB.
from rd03d import RD03D import time radar = RD03D() # Defaults, multi-target mode and pins GP0/GP1 while True: if radar.update(): target1 = radar.get_target(1) target2 = radar.get_target(2) target3 = radar.get_target(3) print(f"{target1.angle},{target1.distance},{target1.speed},{target2.angle},{target2.distance},{target2.speed},{target3.angle},{target3.distance},{target3.speed}")
This code is quite similar to the previous demo code but it instead outputs the distance, angle and speed of each of the 3 targets in a CSV format (comma, spaced, variable format). This format isn't required for Processing IDE, it's just a standard format we chose and built this example around. If you run this code you can see this data being printed to the shell.
Now that the Pico is ready, open up Processing IDE, create a new script and paste in the following code:
import processing.serial.*; Serial radarPort; float[] angle = new float[3]; float[] distance = new float[3]; float[] speed = new float[3]; color[] targetColor = { color(255,0,0), color(0,128,255), color(180,0,255) }; // red, blue, purple int radarRadius; PFont radarFont; void setup() { size(1600, 900); radarRadius = height - 150; radarFont = createFont("Arial", 16); textFont(radarFont); radarPort = new Serial(this, "COM6", 115200); radarPort.bufferUntil('\n'); for (int i=0;i<3;i++) { angle[i]=0; distance[i]=0; speed[i]=0; } } void draw() { background(0); translate(width/2, height - 70); drawRadarDisplay(); // Draw all three targets for (int i=0;i<3;i++) { displayTarget(angle[i], distance[i], speed[i], targetColor[i], i); } } void serialEvent(Serial port) { String in = port.readStringUntil('\n'); if(in != null){ in = trim(in); String[] values = split(in, ','); if(values.length == 9){ for (int i=0; i<3; i++) { angle[i] = float(values[i*3]); distance[i] = float(values[i*3+1]); speed[i] = float(values[i*3+2]); } } } } void drawRadarDisplay(){ stroke(0, 255, 0); strokeWeight(1); noFill(); for(int r = radarRadius/4; r <= radarRadius; r += radarRadius/4) { arc(0, 0, r*2, r*2, radians(-150), radians(-30)); } int[] angles = {-60, -45, -30, -15, 0, 15, 30, 45, 60}; for(int i = 0; i < angles.length; i++){ float currentAngle = angles[i]; float rad = radians(currentAngle - 90); float x = radarRadius * cos(rad); float y = radarRadius * sin(rad); strokeWeight(1); stroke(0, 255, 0); line(0, 0, x, y); fill(0, 255, 0); noStroke(); textAlign(CENTER, CENTER); textSize(14); float tx = (radarRadius + 20) * cos(rad); float ty = (radarRadius + 20) * sin(rad); text(currentAngle + "°", tx, ty); } stroke(0,255,0); line(0,0,0,-radarRadius); } void displayTarget(float angleDeg, float distanceMM, float spd, color dotColor, int index){ if(distanceMM <= 8000){ float scaledDist = map(distanceMM, 0, 8000, 0, radarRadius); float rad = radians(angleDeg - 90); float x = scaledDist * cos(rad); float y = scaledDist * sin(rad); fill(dotColor); noStroke(); ellipse(x, y, 14, 14); // Display info for each target at separate vertical offsets fill(dotColor); textSize(16); textAlign(LEFT, CENTER); float yOffset = -radarRadius - 50 + 55*index; float spacing = 18; text("Target " + (index+1), -width/2 + 20, yOffset-10); fill(255); text("Angle: " + nf(angleDeg,1,1) + "°", -width/2 + 40, yOffset + 0*spacing); text("Distance: " + nf(distanceMM/1000.0,1,2) + " m", -width/2 + 40, yOffset + 1*spacing); text("Speed: " + nf(spd,1,2) + " cm/s", -width/2 + 40, yOffset + 2*spacing); } }
This code is going to take the formatting messages being sent by the Pico and visualise them in a display. Explaining this code is outside the scope of this guide, but large language models like ChatGPT and Claude are fluent in this Processing IDE language. Before you run the code you will need to input the correct COM port of your Pico. The easiest way you can find the COM port is in the Thonny IDE in the bottom right, it should be the same one you used to program the Pico. While you are there, you will also need to disconnect Thonny from the COM port. You can do this by either selecting a different COM port (or Local Python 3) or by closing Thonny altogether. We have to disconnect Thonny as only 1 program at a time can be connected.
Once it's closed, run the Processing code and you should see something like the radar visualisation on the right (we have overlayed a camera image on ours, yours won't have that). The circular lines are 2-meter markers and the visualisation extends to the maximum 8-meter range of the sensor. This is a great deal of fun to play around with, and it's a helpful tool in a project using this sensor as it helps visualise information.
Project: Human Speedometer
We currently have a Pico that can measure the distance, angle and speed of a person - let's do something fun with that and make a device that will track your sprinting speed and also compare it to Usain Bolt's 100-meter world record. Your setup can differ a little from ours but we will be using a:
- Piicodev expansion board to connect everything to.
- Piicodev OLED to display the information on. A generic 128x64 SSD1306 OLED will also work.
- USB AA battery pack to power the Pico.
- Piicodev cable to connect the screen.
We already know how to connect the radar sensor to the Pico so assembly is straightforward. The image on the right is our version fully assembled.
To use the screen we are going to need some additional libraries. The process of adding these is the same as adding the radar library except these do not need to be unzipped, they are downloaded directly as the library file. Right-click and "save link as" to download them.
- Download the PiicoDev Unified Library: PiicoDev_Unified.py
- Download the device module: PiicoDev_MMC5603.py
And now we can finally copy and paste the demo code below into Thonny. There are 2 options at the top to change; the units that you want this radar to operate in (m/s, km/h or mph) and whether you want to print out Usain Bolt's 100-meter world record sprint. Save the code as "main.py" to ensure it runs whenever the Pico is powered.
from rd03d import RD03D from PiicoDev_SSD1306 import * import time # User-selectable options ####################################### speed_unit = 'km/h' # Options: 'm/s', 'km/h', 'mph' compare_usain_bolt = True # Run with Bolt comparison or not ################################################################# # radar and display setup radar = RD03D() display = create_PiicoDev_SSD1306() WIDTH, HEIGHT = 128, 64 DISTANCE_RANGE = 6.8 # meters PIXELS_PER_METER = WIDTH / DISTANCE_RANGE # Unit conversion unit_factors = {'m/s':1, 'km/h':3.6, 'mph':2.23694} if compare_usain_bolt == True: max_speeds = {'m/s':7.2,'km/h':26,'mph':16} # max speed per unit (Bolt reaches ~7 m/s, set slightly higher) else: max_speeds = {'m/s':5,'km/h':18,'mph':11} # set it a bit lower than bolts speed to help visualise MAX_SPEED = max_speeds[speed_unit] UNIT_FACTOR = unit_factors[speed_unit] usain_data_points = [(0.0,0.5),(0.1,1.03),(0.23,1.63),(0.43,2.23),(0.68,2.83), (0.98,3.33),(1.35,3.83),(1.75,4.33),(2.21,4.78),(2.71,5.23), (3.26,5.72),(3.85,6.18),(4.48,6.45),(5.15,6.8),(5.84,7.11), (6.57,7.52),(7.35,7.96),(8.16,8.34),(9.01,8.67),(9.89,8.93)] # Interpolate Bolt's speed def get_bolt_speed(distance): for i in range(len(usain_data_points)-1): d0, v0 = usain_data_points[i] d1, v1 = usain_data_points[i+1] if d0 <= distance <= d1: return v0 + ((v1 - v0)/(d1 - d0)) * (distance - d0) return usain_data_points[-1][1] # Pixel conversions def distance_to_pixel(distance_m): pixel_x = round(distance_m * PIXELS_PER_METER) return min(max(pixel_x, 0), WIDTH-1) def speed_to_pixel(speed): pixel_y = HEIGHT - round((speed / MAX_SPEED) * HEIGHT) return min(max(pixel_y, 0), HEIGHT - 1) # Initialize plot buffers user_speed_buffer = [None]*WIDTH bolt_speed_buffer = [None]*WIDTH if compare_usain_bolt else None last_distance = None bolt_last_pixel_x = -1 max_recorded_speed = 0 # Track maximum measured speed # Drawing axes and ticks def draw_axes(): display.fill(0) # y-axis ticks every 5 units tick_interval = 5 num_ticks = int(MAX_SPEED / tick_interval) for i in range(1,num_ticks+1): y = speed_to_pixel(i*tick_interval) display.hline(0,y,3,1) display.hline(0,HEIGHT-1,WIDTH,1) # bottom line, speed=0 # Display current max speed top-left corner max_speed_text = "{:.1f} {}".format(max_recorded_speed, speed_unit) display.text(max_speed_text, 6, 0, 1) # Slight spacing from top-left corner while True: if radar.update(): target = radar.get_target(1) distance_m = target.distance / 1000.0 speed_m_s = abs(target.speed) / 100.0 speed = speed_m_s * UNIT_FACTOR # Update the max recorded speed value if new speed is greater if speed > max_recorded_speed: max_recorded_speed = speed if 0.2 <= distance_m <= DISTANCE_RANGE: if (last_distance is None) or (distance_m > last_distance): pixel_x = distance_to_pixel(distance_m) pixel_y = speed_to_pixel(speed) if last_distance is not None: prev_pixel_x = distance_to_pixel(last_distance) prev_pixel_y = user_speed_buffer[prev_pixel_x] if pixel_x - prev_pixel_x > 1: for ix in range(prev_pixel_x+1,pixel_x): interp_ratio = (ix-prev_pixel_x)/(pixel_x-prev_pixel_x) interp_y = round(prev_pixel_y+(pixel_y-prev_pixel_y)*interp_ratio) user_speed_buffer[ix]=interp_y user_speed_buffer[pixel_x]=pixel_y if compare_usain_bolt: bolt_speed_m_s=get_bolt_speed(distance_m) bolt_speed=bolt_speed_m_s*UNIT_FACTOR bolt_pixel_y=speed_to_pixel(bolt_speed) if bolt_last_pixel_x>=0 and pixel_x-bolt_last_pixel_x>1: bolt_prev_y=bolt_speed_buffer[bolt_last_pixel_x] for ix in range(bolt_last_pixel_x+1,pixel_x): frac=(ix-bolt_last_pixel_x)/(pixel_x-bolt_last_pixel_x) interp_bolt_y=round(bolt_prev_y+(bolt_pixel_y-bolt_prev_y)*frac) bolt_speed_buffer[ix]=interp_bolt_y bolt_speed_buffer[pixel_x]=bolt_pixel_y bolt_last_pixel_x=pixel_x last_distance=distance_m draw_axes() # plot data for x in range(WIDTH): if user_speed_buffer[x]!=None: display.pixel(x,user_speed_buffer[x],1) if compare_usain_bolt and bolt_speed_buffer[x]!=None: display.pixel(x,bolt_speed_buffer[x],1) if x>0 and bolt_speed_buffer[x-1]!=None: y0=bolt_speed_buffer[x-1] y1=bolt_speed_buffer[x] display.line(x-1,y0,x,y1,1) display.show() time.sleep(0.02)
Power on your Pico, and you should be greeted by a largely blank screen. Stand close to the radar sensor and start sprinting away from it as fast as possible (being safe and ensuring you are in a suitable environment). You will see your speed being plotted, and if you have it turned on, you will see Usain Bolt's acceleration curve as well (or you know it might be my sprinting speed you never know).
You can also turn off his record plot and sprint by the sensor at full speed. The number displayed on the screen is the maximum speed it has recorded. If you want to reset the speedometer, turn the Pico off and on again.
Where to From Here?
We now know how to use a sensor that beams out invisible radio waves and uses advanced processing algorithms to detect and track humans. There are a great deal of applications for this, but if you are looking for some inspiration, we had some projects come to mind that we weren't able to fit into this guide:
- A more advanced home automation sensor - PIR sensors are great, but having distance, direction and speed at your fingertips is a great addition to any sort of home automation setup. This might work better when implemented on an ESP32 though. Check out our MQTT guide for a way to send detection information wirelessly to any device.
- Fan tracking - On my desk, I have a fan that I have to shift every time I move. Having it on a rotating platform with the sensor tracking a person to directly blow air on them would be great (especially for the Australian summer heat).
Regardless, if you do make something cool with it, or you need a hand with anything from this guide, feel free to drop a comment in the forum below, we are all makers and happy to help. Until next time, happy making!