The HC-SR501 is the PIR in every security and corridor-light project. It needs 5 V, but its OUT pin is a clean 3.3 V signal, so the Pico W reads it directly on GP16 — no divider, unlike most 5 V modules.
Because this is a Pico W, the script can also tell your phone: set a Wi-Fi name and a private ntfy topic, install the ntfy app, subscribe to the same topic, and every new motion (at most one a minute) arrives as a notification.
Wiring
| Raspberry Pi Pico W | HC-SR501 PIR motion sensor | Wire | Notes |
|---|---|---|---|
| VBUS | VCC | red | VBUS (pin 40) is USB 5 V. The HC-SR501 needs 4.5 V or more. |
| GP16 | OUT | yellow | GP16 is physical pin 21. OUT is 3.3 V from the module’s own regulator — no divider needed. |
| GND | GND | black |
4.5–20 V (5 V typical) part, 3.3 V board. Power it from 5 V (VIN/5V pin). Its output is a 3.3 V logic signal, so it connects directly to ESP32, Pico and Uno GPIOs.
Board quirks
- The PIR fires randomly for about a minute after power-up while it settles. The script waits 60 s before it starts reporting.
- Set the yellow jumper to H (repeat trigger) so OUT stays HIGH while someone keeps moving. The two pots set sensitivity (range) and hold time — turn the time pot fully anticlockwise for the shortest ~3 s pulse.
- Phone alerts are optional: with no Wi-Fi name or ntfy topic set, it just prints “Motion detected”. A cooldown keeps it to one alert a minute.
- Wi-Fi settings come from
/settings.json(written by the FlashReady flasher) or the constants at the top ofmain.py. The Pico W is 2.4 GHz only. - Mount it away from windows, ceiling fans and AC vents: moving warm air and sunlight changes trigger it as easily as a person.
Code
# HC-SR501 PIR motion sensor on a Raspberry Pi Pico W (MicroPython) with an optional phone alert
# through ntfy.sh. Wiring: VCC -> VBUS (pin 40, USB 5 V), OUT -> GP16 (pin 21), GND -> GND.
# OUT is already a 3.3 V signal (the module has its own regulator), so no divider is needed.
from machine import Pin
import network
import time
import json
WIFI_SSID = "your-wifi" # optional: leave as-is to just print motion
WIFI_PASS = "your-password"
NTFY_TOPIC = "your-private-topic" # https://ntfy.sh/<topic> - pick something unguessable
COOLDOWN_S = 60 # at most one phone alert a minute
# The FlashReady flasher saves your settings to /settings.json; they win over the lines above.
try:
with open("/settings.json") as f:
_s = json.load(f)
except (OSError, ValueError):
_s = {}
WIFI_SSID = _s.get("wifi_ssid") or WIFI_SSID
WIFI_PASS = _s.get("wifi_pass") or WIFI_PASS
NTFY_TOPIC = _s.get("ntfy_topic") or NTFY_TOPIC
ALERTS = WIFI_SSID != "your-wifi" and NTFY_TOPIC != "your-private-topic"
pir = Pin(16, Pin.IN)
led = Pin("LED", Pin.OUT)
wlan = network.WLAN(network.STA_IF)
def connect():
if wlan.isconnected():
return True
wlan.active(True)
wlan.connect(WIFI_SSID, WIFI_PASS)
for _ in range(30): # up to 15 s
if wlan.isconnected():
print("Wi-Fi connected:", wlan.ifconfig()[0])
return True
time.sleep(0.5)
print("Wi-Fi failed, status", wlan.status())
return False
def alert(msg):
if not ALERTS or not connect():
return
try:
import requests
except ImportError:
import urequests as requests
try:
requests.post("https://ntfy.sh/" + NTFY_TOPIC, data=msg).close()
print("Phone alert sent")
except OSError as e:
print("ntfy failed:", e)
if ALERTS:
try:
network.country("IN")
except AttributeError:
import rp2
rp2.country("IN")
connect()
else:
print("Phone alerts off - no Wi-Fi name or ntfy topic set. Flash from flashready.in or type them into main.py.")
print("PIR on GP16 - warming up for 60 s, keep still")
time.sleep(60) # the HC-SR501 triggers at random for about a minute after power-up
print("Ready")
last = 0
last_alert = -COOLDOWN_S
while True:
now = pir.value()
led.value(now)
if now and not last:
t = time.time()
print("Motion detected")
if t - last_alert >= COOLDOWN_S:
last_alert = t
alert("Motion detected by the Pico W")
elif last and not now:
print("Motion ended")
last = now
time.sleep_ms(50)# HC-SR501 PIR motion sensor on a Raspberry Pi Pico W (MicroPython) with an optional phone alert
# through ntfy.sh. Wiring: VCC -> VBUS (pin 40, USB 5 V), OUT -> GP16 (pin 21), GND -> GND.
# OUT is already a 3.3 V signal (the module has its own regulator), so no divider is needed.
from machine import Pin
import network
import time
import json
WIFI_SSID = "your-wifi" # optional: leave as-is to just print motion
WIFI_PASS = "your-password"
NTFY_TOPIC = "your-private-topic" # https://ntfy.sh/<topic> - pick something unguessable
COOLDOWN_S = 60 # at most one phone alert a minute
# The FlashReady flasher saves your settings to /settings.json; they win over the lines above.
try:
with open("/settings.json") as f:
_s = json.load(f)
except (OSError, ValueError):
_s = {}
WIFI_SSID = _s.get("wifi_ssid") or WIFI_SSID
WIFI_PASS = _s.get("wifi_pass") or WIFI_PASS
NTFY_TOPIC = _s.get("ntfy_topic") or NTFY_TOPIC
ALERTS = WIFI_SSID != "your-wifi" and NTFY_TOPIC != "your-private-topic"
pir = Pin(16, Pin.IN)
led = Pin("LED", Pin.OUT)
wlan = network.WLAN(network.STA_IF)
def connect():
if wlan.isconnected():
return True
wlan.active(True)
wlan.connect(WIFI_SSID, WIFI_PASS)
for _ in range(30): # up to 15 s
if wlan.isconnected():
print("Wi-Fi connected:", wlan.ifconfig()[0])
return True
time.sleep(0.5)
print("Wi-Fi failed, status", wlan.status())
return False
def alert(msg):
if not ALERTS or not connect():
return
try:
import requests
except ImportError:
import urequests as requests
try:
requests.post("https://ntfy.sh/" + NTFY_TOPIC, data=msg).close()
print("Phone alert sent")
except OSError as e:
print("ntfy failed:", e)
if ALERTS:
try:
network.country("IN")
except AttributeError:
import rp2
rp2.country("IN")
connect()
else:
print("Phone alerts off - no Wi-Fi name or ntfy topic set. Flash from flashready.in or type them into main.py.")
print("PIR on GP16 - warming up for 60 s, keep still")
time.sleep(60) # the HC-SR501 triggers at random for about a minute after power-up
print("Ready")
last = 0
last_alert = -COOLDOWN_S
while True:
now = pir.value()
led.value(now)
if now and not last:
t = time.time()
print("Motion detected")
if t - last_alert >= COOLDOWN_S:
last_alert = t
alert("Motion detected by the Pico W")
elif last and not now:
print("Motion ended")
last = now
time.sleep_ms(50)Expected serial output
Wi-Fi connected: 192.168.1.42
PIR on GP16 - warming up for 60 s, keep still
Ready
Motion detected
Phone alert sent
Motion endedWi-Fi connected: 192.168.1.42 PIR on GP16 - warming up for 60 s, keep still Ready Motion detected Phone alert sent Motion ended
Common errors
Motion detected every few seconds with nobody there
Still warming up, a fan or AC is in view, or sensitivity is too high. Turn the sensitivity pot anticlockwise and re-aim the dome.
Never detects anything
VCC is on 3V3 instead of VBUS, or OUT is not on GP16. The module needs at least 4.5 V; on battery power VBUS is dead, so feed it from VSYS with a 5 V supply.
ntfy failed: -2
DNS failed — the Pico W is not really online. Check the Wi-Fi name and password, and that your router or hotspot has 2.4 GHz turned on.