RCWL-0516 Radar + ESP32: Build a Room Occupancy Sensor That Works Through Its Own Box - Compoden

RCWL-0516 Radar + ESP32: Build a Room Occupancy Sensor That Works Through Its Own Box

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

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_MS to 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.

Back to blog

Open this project in the Soldr app → What is Soldr.ai → parts, wiring and code for what this guide builds