RCWL-0516 Radar + ESP32: Build a Room Occupancy Sensor That Works Through Its Own Box
Share
PIR sensors are great until you try to hide one. Put a PIR behind a plastic lid and it goes blind, because it needs to see body heat directly. The RCWL-0516 doesn't care. It's a tiny microwave Doppler radar that notices movement through plastic, glass and thin wood, so you can seal the whole build inside a project box and it still works.
Today we'll wire one to an ESP32 and turn it into a room occupancy sensor: an OLED that says OCCUPIED or EMPTY, how long ago something last moved, and a little two-minute activity graph along the bottom. About 20 minutes on a breadboard.
What you need
- RCWL-0516 Microwave Radar Sensor
- ESP32 30-Pin Development Board (CP2102)
- 0.96 inch OLED Display 128x64 I2C (SSD1306)
- MB102 830-Point Breadboard with Jumper Wires
- Jumper Wire Combo (M-M, M-F, F-F)
Wiring
| Part | Pin | ESP32 |
|---|---|---|
| RCWL-0516 | VIN | VIN (5V from USB) |
| RCWL-0516 | GND | GND |
| RCWL-0516 | OUT | GPIO27 |
| RCWL-0516 | 3V3 | leave unconnected (it's an output) |
| RCWL-0516 | CDS | leave unconnected |
| OLED | VCC | 3V3 |
| OLED | GND | GND |
| OLED | SDA | GPIO21 |
| OLED | SCL | GPIO22 |
| Onboard LED | (on the board) | GPIO2, mirrors the radar output |
A few notes so it works the first time:
- Power the radar from VIN, not 3V3. The module wants 4 to 28V on its VIN pin. With the ESP32 plugged into USB, the board's VIN pin sits at roughly 5V, which is perfect.
- The module's 3V3 pin is an output. It comes from the radar's own regulator. Don't feed 3.3V into it, just leave it alone.
- OUT is already 3.3V logic. Even though the module runs from 5V, OUT swings between 0 and about 3.3V, so it goes straight into GPIO27. No divider, no level shifter.
- One detection = about 2 seconds of HIGH. Keep moving and it keeps re-triggering, so OUT stays HIGH as long as there's motion. That's why the sketch does its own "room empty" timer on top.
- Give it breathing room. Keep about 1 cm of clear space around the module, and no metal right next to it or behind it. Metal blocks the signal, and a metal box or a big ground plane underneath will make it deaf or jumpy.
- CDS is for a light sensor. Solder an LDR on the CDS pads and the module only triggers in the dark. We don't need it here.
How it works
The RCWL-0516 sends out a low-power microwave signal at around 3 GHz and listens for the echo. When something moves, the echo shifts slightly in frequency (the Doppler effect), and the onboard chip flips OUT high. Range is around 5 to 7 metres indoors, so it will happily see you through a thin wall or the far side of a room.
The sketch:
- Turns the onboard blue LED on whenever OUT is HIGH, so you can see it react while you tune the placement.
- Counts each new trigger and prints it to the Serial Monitor at 115200.
- Shows OCCUPIED while there's been motion in the last 2 minutes, then flips to EMPTY. Change
EMPTY_AFTER_MSto whatever suits the room. - Draws a 120-second activity strip along the bottom of the OLED: a tall bar for every second with motion, a dot for every quiet one.
Libraries: U8g2 from the Library Manager. Board package: ESP32 by Espressif. Board: "ESP32 Dev Module".
The code
#include <Wire.h>
#include <U8g2lib.h>
#define RADAR_PIN 27 // RCWL-0516 OUT (3.3V logic, HIGH for ~2 s per detection)
#define LED_PIN 2 // the blue LED on most ESP32 DevKit boards
const unsigned long EMPTY_AFTER_MS = 2UL * 60 * 1000; // no motion for 2 min = room empty
const int HISTORY = 120; // 120 one-second columns on the graph
U8G2_SSD1306_128X64_NONAME_F_HW_I2C oled(U8G2_R0, U8X8_PIN_NONE);
bool lastOut = false;
unsigned long lastMotionAt = 0; // millis() of the last HIGH we saw
bool everSeen = false;
unsigned int triggers = 0; // LOW-to-HIGH edges since power-on
uint8_t history[HISTORY]; // 1 = motion during that second
int head = 0;
bool motionThisSecond = false;
void readRadar() {
bool out = digitalRead(RADAR_PIN) == HIGH;
if (out) {
lastMotionAt = millis();
everSeen = true;
motionThisSecond = true;
}
if (out && !lastOut) {
triggers++;
Serial.printf("[%lus] motion #%u\n", millis() / 1000, triggers);
}
lastOut = out;
digitalWrite(LED_PIN, out ? HIGH : LOW);
}
void tickHistory() {
static unsigned long lastTick = 0;
if (millis() - lastTick < 1000) return;
lastTick += 1000;
history[head] = motionThisSecond ? 1 : 0;
head = (head + 1) % HISTORY;
motionThisSecond = false;
}
bool occupied() {
return everSeen && millis() - lastMotionAt < EMPTY_AFTER_MS;
}
void draw() {
char line[24];
oled.clearBuffer();
oled.setFont(u8g2_font_6x10_tf);
oled.drawStr(0, 9, "RADAR ROOM SENSOR");
if (lastOut) oled.drawDisc(122, 5, 4); // dot while OUT is HIGH
oled.setFont(u8g2_font_logisoso16_tf);
oled.drawStr(0, 30, occupied() ? "OCCUPIED" : "EMPTY");
oled.setFont(u8g2_font_6x10_tf);
if (!everSeen) strcpy(line, "Waiting for motion");
else snprintf(line, sizeof(line), "Last: %lus ago", (millis() - lastMotionAt) / 1000);
oled.drawStr(0, 42, line);
snprintf(line, sizeof(line), "Triggers: %u", triggers);
oled.drawStr(0, 52, line);
// last 2 minutes, oldest on the left, one column per second
for (int i = 0; i < HISTORY; i++) {
int idx = (head + i) % HISTORY;
if (history[idx]) oled.drawVLine(4 + i, 56, 8);
else oled.drawPixel(4 + i, 63);
}
oled.sendBuffer();
}
void setup() {
Serial.begin(115200);
pinMode(RADAR_PIN, INPUT); // the module drives OUT itself, no pull-up needed
pinMode(LED_PIN, OUTPUT);
Wire.begin(21, 22); // SDA, SCL
oled.begin(); // SSD1306 at the usual 0x3C address
Serial.println("Radar warming up, keep still for a few seconds...");
}
void loop() {
readRadar();
tickHistory();
static unsigned long lastDraw = 0;
if (millis() - lastDraw > 200) {
lastDraw = millis();
draw();
}
}
If something's off
- Always OCCUPIED, even in an empty room: the radar is seeing something you aren't. A ceiling fan, curtains in a breeze, or a person in the next room through the wall. Turn it so the component side faces away from the culprit, or move it.
- Triggers by itself every few seconds: usually noisy power. Try a different USB cable or port, keep the module off the edge of a laptop, and keep the jumper wires to it short.
- Never triggers: check VIN is on the ESP32's VIN pin, not 3V3, and that OUT goes to GPIO27. Give it a few seconds after power-up to settle.
- Blank OLED: swap SDA and SCL. Most of these modules live at address 0x3C.
- Too sensitive for a small room: the module has an R-GN footprint. A resistor there (around 1M ohm) shortens the range. Check the pad layout on your board before soldering.
Putting it in a box
This is the fun part. Close everything up in a plastic enclosure and stick it on a shelf or above a door frame. The radar sees right through the lid, and nobody will know what it is. Hook the occupancy state into a relay for a hallway light, or send it over Wi-Fi to see at a glance whether the study room is empty.
Want a build like this designed for you from just an idea? Soldr, Compoden's AI, designs hardware from an idea at soldr.ai.