传感器 · 人体红外移动检测 · HC-SR501 · SKU 待确认

HC-SR501 人体红外移动检测传感器 STM32F103 接线与代码

HC-SR501 是给单片机装的一双「感应人动」的眼睛——人一进感应范围移动,OUT 立刻输出高电平,OLED 大字 MOTION! 并计数;人静止不动,它就安静地报 NO MOTION。先把最重要的一点说在前面:它检测的是「移动」,不是「有人」,人静坐不动时输出 LOW 是特性、不是故障。本页是 STM32F103C8T6 + HC-SR501 + 0.96 OLED 的完整实机验证记录:VCC 接 5V、OUT 接 PA0、共 3 根主线,含接线图、40ms 去毛刺 / 上升沿计数 / 30 秒预热的程序逻辑、完整可编译代码和 PlatformIO 工程下载。最值得看的是「为什么人坐着不动它不报」和「上电为什么要等 30 秒」这两段。

Quick Facts

  • 模块:HC-SR501 人体红外移动检测(PIR,5V 供电,4.5~20V)
  • SKU:待确认(以商品页与客服说明为准)
  • 接线:VCC→5V、OUT→PA0、GND→GND,共 3 根线(不同批次以丝印为准)
  • 输出:OUT 高=检测到移动;输出高电平约 3.3V(实测),PA0 可直读
  • 显示:0.96 寸 OLED,SSD1306,地址 0x3C,SCL=PB8 / SDA=PB9,I2C 400kHz
  • 程序:BOOT→WARMUP(约 30s 预热不采样)→RUN,40ms 去毛刺,仅 LOW→HIGH 上升沿计数 +1
  • 特性提醒:测「移动」不测「有人」,人静坐不动显示 NO MOTION 属正常

Verified

实机验证结果

本项目实测 下面的结果全部来自我们自己的实机测试,更新于 2026-08-19。

3 根线VCC=5V / OUT=PA0 / GND 共地
40 ms稳定确认去毛刺,不误计数
30 秒开机预热,期间不采样不计数
上升沿仅 LOW→HIGH 计数 +1

STM32F103C8T6 接上 HC-SR501 后,人一进入感应范围移动,OLED 立即大字反白显示 MOTION! 并计数加一,板载 LED(PC13)点亮;手停下或移开,OLED 恢复 NO MOTION。开机先经过约 30 秒预热,预热期间不采样、不计数。

HC-SR501 实拍接线全景:模块 + 面包板 + STM32F103C8T6 + OLED 显示 NO MOTION,COUNT 已累计 7 次
实拍接线 + 结果:HC-SR501、面包板、STM32F103C8T6、OLED 同框,此刻显示 NO MOTION(COUNT 已累计 7 次)

完整演示视频(接线 → 预热 → 触发 → 烧录)

HC-SR501 实拍 + 动画演示成片(274 秒,720P):30 秒预热 → 挥手 MOTION! 计数 → 静止恢复 NO MOTION 的完整实测过程

OLED 运行页两个状态

运行页固定显示 PIR SENSOR 标题 + STATE + RAW + COUNT,右侧是动画小人:检测到移动时显示反白闪烁的 MOTION! 并带扩散波纹,小人左右轻摆;无移动时显示 NO MOTION,小人静止。

OLED 实拍特写:MOTION! 大字反白 + 波纹 + 小人动画
挥手触发:STATE MOTION! + 波纹 + 小人摆动
OLED 实拍特写:NO MOTION 状态,小人静止
手移开:NO MOTION,小人静止

实测动作与 OLED 表现

测试动作OLED / 串口表现
刚上电先显示开机页(1.5 秒),再进入预热倒计时(约 30 秒),期间 LED 慢闪、不采样、不计数
预热结束以当前实际电平为基准进入 RUN,串口打印 WARMUP DONE | RUNNING
人走进感应区挥手立即 MOTION!,COUNT +1,LED 点亮
持续移动不离开OUT 保持 HIGH,只计 1 次,不重复累加
手停下 / 移开恢复 NO MOTION,LED 熄灭
再次挥手回落 LOW 后再 HIGH,COUNT 再 +1
人静坐在感应区内不动一直 NO MOTION——这是「测移动不测有人」的模块特性,不是故障

串口输出(115200)

预热阶段每秒报一次剩余秒数,运行阶段每 400ms 报一次状态,与 OLED 完全一致。

HC-SR501 PIR TEST
Firmware: v1.0

PIR | WARMING UP | 30 s
PIR | WARMING UP | 29 s
...
PIR | WARMUP DONE | RUNNING
PIR | RAW=1 | STATE=MOTION    | COUNT=1
PIR | RAW=0 | STATE=NO MOTION | COUNT=1
PIR | RAW=1 | STATE=MOTION    | COUNT=2
PIR | RAW=0 | STATE=NO MOTION | COUNT=2

实测环境

项目本项目实际使用
主控STM32F103C8T6(BluePill)
模块HC-SR501 人体红外移动检测模块(PIR,5V 供电)
显示0.96 寸 OLED,SSD1306,128×64,地址 0x3C
开发工具VS Code + PlatformIO
框架Arduino / STM32duino
烧录ST-Link(SWD 四线)
串口115200,运行期每 400ms 输出一次
I2C 时钟400 kHz(OLED)
依赖库Adafruit SSD1306、Adafruit GFX Library
未验证的环境不当成支持。本项目没有在 Arduino IDE、Keil、STM32CubeIDE 下编译或验证过,所以不提供这些环境的支持说明。

What it does

这个模块干什么

HC-SR501 是一块被动式红外(Passive Infrared,PIR)模块:它不发射任何东西,只被动接收人体红外辐射的变化。人一动,OUT 输出高电平;人静止,OUT 回到低电平。信号直接进 STM32 的 PA0,不需要任何总线协议,3 根线就能用。

三个要点

要点意思
它测「移动」,不测「有人」热释电传感器只对红外辐射的变化有响应。人静坐不动时红外基本不变,模块输出 LOW(NO MOTION)——这是特性,不是故障。需要检测「静止有人」请换毫米波雷达 / ToF / 人体存在传感器(如 LD2410),那是另一种模块。
计数只在上升沿持续移动期间 OUT 一直保持 HIGH,只算 1 次;必须回落 LOW 后再次 HIGH 才再 +1。所以它数的是「触发次数」,不是「移动时长」。
上电要预热约 30 秒模块刚上电内部还不稳定,需要约 30 秒稳定期;本项目的程序在预热期间不采样、不计数,OLED 显示倒计时。

Specs

关键参数

模块/芯片参数和本项目实测口径分开列,各条标明来源类型;没有把握的一律写「待确认」。

模块 / 芯片参数(以厂家与公开资料为主)

项目数值 / 说明来源
模块型号HC-SR501 人体红外移动检测模块(PIR)资料参考
板载信号处理芯片BISS0001(模块背面丝印可见)资料参考
工作电压4.5~20V,本项目实测使用 5V(不是 3.3V)资料参考(模块资料)+ 本项目实测
检测距离约 3~7m 可调(灵敏度电位器)。厂家标称值,未逐一批次实测核实 ⚠资料参考
输出高电平约 3.3V,STM32 的 PA0 可直读,无需电平转换本项目实测
灵敏度电位器调节感应距离/范围;具体调节范围待确认(以实物为准)
延时电位器决定 OUT 高电平保持时间;具体可调范围待确认(以实物为准)
触发模式跳帽H=可重复触发(持续移动顺延保持)/ L=不可重复触发资料参考
引脚3 针:VCC / OUT / GND。不同批次排列顺序可能不同,以模块丝印为准本项目实测
SKU待确认(以商品页与客服说明为准,本页不标注未核实编号)待确认

本项目实测口径(STM32 侧)

项目本项目实际使用来源
输入引脚PA0(pinMode INPUT + digitalRead)本项目实测
去毛刺40ms 稳定确认(30~50ms 均可),毛刺不计数本项目实测
计数规则仅 LOW→HIGH 上升沿 COUNT+1,下降沿不计数本项目实测
预热约 30 秒(PIR_WARMUP_MS 可改,录制演示用 10 秒)本项目实测
板载 LEDPC13(低电平点亮),跟随 MOTION / NO MOTION;预热期慢闪本项目实测
OLED0.96 寸 SSD1306 128×64,0x3C,SCL=PB8 / SDA=PB9,I2C 400kHz本项目实测
串口115200,运行期每 400ms 一行,与 OLED 状态完全一致本项目实测
时序全部 millis() 非阻塞,无长 delay()本项目实测

Wiring

接线

本项目实测 HC-SR501 主接线 3 根 + OLED 4 根,共 7 根线。注意 HC-SR501 用 5V、OLED 用 3.3V,是两条不同的电源线。

接线示意图(一眼看懂)

5V 电源 OUT→PA0 GND 共地 3.3V SCL SDA HC-SR501 人体红外移动检测(PIR) VCC OUT GND 灵敏度 / 延时电位器 (针脚以丝印为准) STM32F103C8T6 BluePill 5V 3.3V PA0 PB8 (SCL) PB9 (SDA) GND 0.96 OLED SSD1306 · 0x3C VCC (3.3V) SCL SDA GND 两条电源线不同:HC-SR501 接 5V(红),OLED 接 3.3V(橙),切勿混接 OUT→PA0 为单独一根黄线;SCL/SDA 只接 OLED;模块、OLED、STM32 的 GND 必须共地
接线示意:红 5V→HC-SR501、橙 3.3V→OLED、黄 OUT→PA0、绿 SCL→PB8、蓝 SDA→PB9、灰 GND 全部共地

HC-SR501(3 针:VCC / OUT / GND)

信号模块丝印STM32F103C8T6说明
电源VCC5V模块需 5V 供电(4.5~20V),别和 OLED 的 3.3V 搞混
信号OUTPA0高电平=检测到移动(输出高约 3.3V,实测,PA0 可直读)
地GNDGND必须共地

OLED(0.96 寸 SSD1306,走硬件 I2C)

信号OLEDSTM32F103C8T6
电源VCC / VDD3.3V(不是 5V)
地GNDGND
时钟SCL / SCKPB8
数据SDAPB9

烧录与串口

用途连接说明
烧录ST-Link → SWD 四线(3.3V / GND / SWDIO=PA13 / SWCLK=PA14)烧录用
串口PA9(TX) / PA10(RX) → CP2102115200,可选
最容易接错的三处:
1. HC-SR501 的 VCC 接 5V,OLED 的 VCC 接 3.3V。两条电源线不一样,别从同一个孔取电乱接——HC-SR501 接 3.3V 可能不工作或极不稳定,OLED 接 5V 有烧坏风险。
2. 不同批次 HC-SR501 的 VCC / OUT / GND 排列顺序可能不同。接线前一定核对模块背面丝印,别照抄图片或旧图的位置硬插。
3. 必须共地,PA13/PA14 别占用。模块、OLED、STM32 的 GND 要接在一起;PA13/PA14 留给 ST-Link 调试。

Principle

工作原理

资料参考 这一节来自模块厂家资料、BISS0001 芯片资料与热释电公开通用原理,不是我们实测的结论,用来帮助理解。实测结果只在上面「实机验证结果」一节。

环节说明
热释电效应人体会发出红外辐射(约 9.4μm 波段),热释电元件感知辐射变化并转成微弱电信号。注意是「变化」——辐射不变就没有输出。
菲涅尔透镜模块正面那个白色圆顶。它把视场分成多个明暗探测区,人体移动时像从一个区移到另一个区,焦点变化传感器才能分辨出「动了」。
BISS0001 信号处理模块背面的主控芯片,把微弱信号两级放大 + 比较,输出稳定的高/低电平(本项目实测输出高电平约 3.3V)。
灵敏度电位器调节感应距离/范围。调太高时,空调出风、暖气、宠物等热源也会反复误触发。
延时电位器决定 OUT 高电平的保持时间。调大后一次触发 OUT 保持更久,MOTION 恢复变慢。
H / L 触发模式跳帽选择:H=可重复触发(持续移动会顺延保持 HIGH);L=不可重复触发(触发一次后要等本轮输出结束)。本项目的计数语义(仅上升沿 +1)不受跳帽影响。

为什么「人静止」检测不到

HC-SR501 是移动检测器,不是存在检测器。人静坐不动时,红外辐射强度基本不变,传感器没有「变化」可测,OUT 输出低电平,程序就报 NO MOTION。要检测「静止有人」,需要换毫米波雷达、ToF 或人体存在传感器(如 LD2410),那是另一种模块,不是把 HC-SR501 调灵敏度能解决的。顺带一提:横向穿过感应区触发最灵敏,正对传感器直线走近时红外变化小、触发相对迟钝,这也是「分区成像 + 测变化」原理的直接结果。

误判高发区。如果按「有人/无人」的语义去用它,静止不动的人会被漏报——这不是模块坏了,而是选型问题。本项目程序与本页全程只用 NO MOTION / MOTION 两种措辞,就是为了避免误会。

模块实拍

HC-SR501 模块背面特写:BISS0001 主控芯片、两个电位器、跳帽
模块背面:BISS0001 主控 + 灵敏度/延时电位器 + 触发模式跳帽
HC-SR501 模块带底座侧面:白色菲涅尔圆顶、三根引脚
带底座侧面:白色菲涅尔透镜圆顶 + VCC/OUT/GND 三线

热释电效应、菲涅尔透镜分区成像、BISS0001 信号处理、电位器与触发模式:资料参考,模块厂家资料与公开通用原理,由我们重新整理表述,未直接复制原文。资料参考

Program logic

程序逻辑

本项目实测 下面是对实际代码 src/main.cpp(v1.0)逻辑的说明,不展开语法,只讲「程序是怎么想的」。

上电 -> BOOT 开机页(1.5s)
     -> WARMUP 预热倒计时(约 30s):不采样、不计数,LED 慢闪
     -> RUN:PA0 采样 -> 40ms 稳定确认(去毛刺) -> 仅 LOW→HIGH 上升沿 COUNT+1
            -> OLED(MOTION!/NO MOTION + 小人动画) + 串口(每 400ms) + PC13 LED 跟随

三阶段状态机(BOOT → WARMUP → RUN)

阶段时长程序行为
BOOT1.5 秒OLED 显示开机页「HC-SR501 PIR / Initializing...」
WARMUP约 30 秒OLED 显示预热倒计时,PC13 每 500ms 慢闪;完全不采样、不计数——模块刚上电输出不可信
RUN持续进入时以「当前实际电平」为基准(避免预热期间残留的高电平一进 RUN 就被当成新触发),开始 40ms 去毛刺采样 + 上升沿计数

四个关键设计

设计作用
40ms 稳定确认(去毛刺)输入电平必须连续稳定 40ms 才承认为一次变化,抖动、毛刺、短暂干扰脉冲不会触发计数,COUNT 不会乱跳。
仅 LOW→HIGH 上升沿计数OUT 保持 HIGH 期间只算 1 次;下降沿不计数,只是「复位」,为下一次上升沿做准备。数的是触发次数,不是移动时长。
OLED 动画MOTION! 大字反白闪烁(交替)+ 以传感器为中心的扩散波纹 + 小人左右轻摆;状态一变化就立即刷新,MOTION 期间每 200ms 播一帧。
millis() 非阻塞开机页、预热倒计时、去毛刺、串口 400ms、动画 200ms 全部用 millis() 计时,没有长 delay(),状态响应不打折。

另外两处防御性细节:PC13 板载 LED 跟随状态(低电平点亮,MOTION 亮 / NO MOTION 灭);setup 时先对 0x3C 做真实 I2C ACK 探测再初始化 OLED,若 OLED 找不到,初始化失败时 PC13 每 100ms 快速闪灯报警,串口给出明确错误,不会假装正常运行。

Code

完整代码

本项目实测 下面是实际烧录进板子、实机测试并拍摄素材时运行的那一份(src/main.cpp,v1.0),一字未改。环境:VS Code + PlatformIO + Arduino(STM32duino)。

关键处说明

1. 40ms 稳定确认。采样电平变化后重新计时,连续稳定 40ms 才更新「确认电平」,毛刺根本进不了状态。

2. 上升沿计数。if (prev == LOW && confirmedRaw == HIGH) motionCount++;——持续移动保持 HIGH 只计 1 次。

3. 预热结束以当前电平为基准。enterRun() 把 confirmedRaw 初始化成当前实际读数,避免预热期间残留电平在进 RUN 瞬间被误判成一次触发。

4. 预热时间可调。PIR_WARMUP_MS 默认 30 秒(模块稳定需要),录制演示可改成 10 秒,代码注释里已标。

5. I2C 用 400kHz,且先 ACK 探测再初始化。Wire.setSDA(PB9); Wire.setSCL(PB8); Wire.setClock(400000);,对 0x3C 探测失败时串口直接给出「check wiring/I2C」的明确提示。

展开完整代码(src/main.cpp,约 330 行)
/*
 * ============================================================================
 * 12_HC-SR501 | STM32F103C8T6 BluePill + HC-SR501 人体红外 + 0.96" OLED(SSD1306)
 * ----------------------------------------------------------------------------
 * 重要概念:
 *   HC-SR501 是被动式红外(PIR)"人体移动"检测模块, 只对"移动的人体红外
 *   辐射"有响应, 不是"静止人体存在"检测。有人静坐不动时模块同样输出 LOW,
 *   这是模块正常特性。因此全程序统一使用:
 *     RAW=0 -> NO MOTION (未检测到移动)
 *     RAW=1 -> MOTION    (检测到移动)
 *   不引入"有人/无人""静止有人"等 HC-SR501 无法可靠判断的状态。
 *
 * 功能:
 *   1. PA0 读取 HC-SR501 OUT, 板载 LED(PC13, 低电平点亮) 跟随状态
 *   2. 仅在 LOW->HIGH 上升沿触发计数 COUNT+1
 *      (持续移动输出保持 HIGH 期间只计 1 次, 必须回落 LOW 后再次 HIGH 才 +1)
 *   3. 输入经 40ms 稳定确认, 毛刺不计数、不乱跳
 *   4. OLED: 开机页 -> 预热倒计时页 -> 运行页(大字 MOTION! + 小人动画)
 *   5. 串口 115200, 每 400ms 输出一次状态
 *   6. 全部使用 millis() 非阻塞时序, 无长 delay()
 *
 * 接线:
 *   HC-SR501 -> BluePill:  VCC->5V   GND->GND   OUT->PA0
 *   OLED     -> BluePill:  VCC->3.3V GND->GND   SCL->PB8  SDA->PB9 (I2C 0x3C)
 *
 * 注意: 不同批次 HC-SR501 引脚排列可能不同, 实际接线以模块丝印为准!
 * ============================================================================
 */

#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <math.h>

#define FIRMWARE_VERSION "v1.0"

/* ------------------------- 引脚常量 ------------------------- */
constexpr uint8_t PIR_PIN      = PA0;   // HC-SR501 OUT, 数字输入
constexpr uint8_t LED_PIN      = PC13;  // 板载 LED, 低电平点亮
constexpr uint8_t I2C_SDA_PIN  = PB9;   // OLED SDA
constexpr uint8_t I2C_SCL_PIN  = PB8;   // OLED SCL
constexpr uint8_t OLED_ADDRESS = 0x3C;  // SSD1306 I2C 地址

/* ------------------------- OLED ------------------------- */
constexpr int SCREEN_WIDTH  = 128;
constexpr int SCREEN_HEIGHT = 64;
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

/* ------------------------- 时序参数 (ms) ------------------------- */
constexpr uint32_t BOOT_SCREEN_MS  = 1500UL;   // 开机画面停留时长
constexpr uint32_t PIR_WARMUP_MS   = 30000UL;  // PIR 预热时间(录制演示可改 10000UL)
constexpr uint32_t INPUT_STABLE_MS = 40UL;     // 输入稳定确认时间(30~50ms)
constexpr uint32_t SERIAL_INTERVAL = 400UL;    // 串口输出周期(200~500ms)
constexpr uint32_t ANIM_TICK_MS    = 200UL;    // 动画帧间隔
constexpr uint32_t LED_BLINK_MS    = 500UL;    // 预热阶段 LED 闪烁周期

/* ------------------------- 状态变量 ------------------------- */
enum class Phase : uint8_t { BOOT, WARMUP, RUN };

Phase    phase         = Phase::BOOT;
uint32_t phaseStartMs  = 0;

uint8_t  lastSample    = LOW;    // 最近一次采样到的电平
uint8_t  confirmedRaw  = LOW;    // 连续稳定 INPUT_STABLE_MS 后确认的电平
uint32_t rawChangeMs   = 0;      // 采样电平发生变化的时刻
uint32_t motionCount   = 0;      // 移动触发次数(仅上升沿 +1)
bool     stateDirty    = false;  // 运行页需要刷新

uint8_t  lastCountdown = 255;    // 上次显示过的预热剩余秒数
bool     displayOk     = false;  // OLED 初始化是否成功
uint32_t lastSerialMs  = 0;
uint32_t lastDisplayMs = 0;
uint32_t lastLedMs     = 0;

/* ------------------------- OLED 绘制辅助 ------------------------- */

// 屏幕水平居中显示一行文字(默认 5x7 字体)
void drawCentered(const char* s, int16_t y, uint8_t size = 1) {
  display.setTextSize(size);
  int16_t x1, y1;
  uint16_t w, h;
  display.getTextBounds(s, 0, 0, &x1, &y1, &w, &h);
  display.setCursor((SCREEN_WIDTH - (int16_t)w) / 2, y);
  display.print(s);
}

// 小人图标: (cx, cy) 为头部圆心
void drawStickFigure(int16_t cx, int16_t cy) {
  display.drawCircle(cx, cy, 3, SSD1306_WHITE);                    // 头
  display.drawLine(cx, cy + 3, cx, cy + 11, SSD1306_WHITE);        // 身体
  display.drawLine(cx, cy + 6, cx - 5, cy + 3, SSD1306_WHITE);     // 左臂
  display.drawLine(cx, cy + 6, cx + 5, cy + 3, SSD1306_WHITE);     // 右臂
  display.drawLine(cx, cy + 11, cx - 4, cy + 19, SSD1306_WHITE);   // 左腿
  display.drawLine(cx, cy + 11, cx + 4, cy + 19, SSD1306_WHITE);   // 右腿
}

// 感应波纹: 以 (cx,cy) 为圆心、半径 r 画一圈点
void drawRipple(int16_t cx, int16_t cy, int16_t r) {
  for (int deg = 0; deg < 360; deg += 30) {
    float rad = deg * (3.14159265f / 180.0f);
    display.drawPixel((int16_t)(cx + r * cosf(rad)),
                      (int16_t)(cy + r * sinf(rad)), SSD1306_WHITE);
  }
}

/* ------------------------- OLED 页面 ------------------------- */

// 开机页
void drawBootPage() {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextWrap(false);
  drawCentered("HC-SR501 PIR", 16);
  drawCentered("Initializing...", 32);
  display.display();
}

// 预热倒计时页
void drawWarmupPage(uint8_t remainSec) {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextWrap(false);
  drawCentered("PIR SENSOR", 0);
  drawCentered("WARMING UP", 16);
  drawCentered("Please wait...", 24);
  char buf[12];
  snprintf(buf, sizeof(buf), "%u s", (unsigned)remainSec);
  drawCentered(buf, 40);
  display.display();
}

// 运行页: motion=是否检测到移动, animPhase=动画相位(0~3)
void drawRunPage(bool motion, uint8_t animPhase) {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextWrap(false);
  drawCentered("PIR SENSOR", 0);

  display.setTextSize(1);
  display.setCursor(0, 16);
  display.print("STATE:");

  if (!motion) {
    /* NO MOTION: 未检测到移动, 小人静止 */
    display.setCursor(0, 26);
    display.print("NO MOTION");
    drawStickFigure(96, 24);
  } else {
    /* MOTION: 检测到移动, 大字反白闪烁 + 波纹 + 小人左右轻摆 */
    if ((animPhase & 1U) == 0U) {
      display.fillRect(0, 26, 84, 16, SSD1306_WHITE);
      display.setTextColor(SSD1306_BLACK);
    } else {
      display.setTextColor(SSD1306_WHITE);
    }
    display.setTextSize(2);
    display.setCursor(0, 26);
    display.print("MOTION!");
    display.setTextSize(1);
    display.setTextColor(SSD1306_WHITE);

    drawRipple(96, 34, 8 + (animPhase % 4U));                    // 扩散波纹
    drawStickFigure(96 + ((animPhase & 1U) ? 1 : -1), 24);       // 轻微左右移动
  }

  char line[20];
  snprintf(line, sizeof(line), "RAW: %u", (unsigned)confirmedRaw);
  display.setCursor(0, 48);
  display.print(line);
  snprintf(line, sizeof(line), "COUNT: %lu", (unsigned long)motionCount);
  display.setCursor(0, 56);
  display.print(line);

  display.display();
}

/* ------------------------- 阶段切换 ------------------------- */

// 预热结束进入正常运行: 以当前实际电平作为基准, 避免预热残留电平误计数
void enterRun(uint32_t now) {
  phase        = Phase::RUN;
  phaseStartMs = now;
  lastSample   = digitalRead(PIR_PIN);
  confirmedRaw = lastSample;
  rawChangeMs  = now;
  motionCount  = 0;
  stateDirty   = true;
  lastSerialMs = now;
  digitalWrite(LED_PIN, (confirmedRaw == HIGH) ? LOW : HIGH);
  Serial.println("PIR | WARMUP DONE | RUNNING");
}

/* ------------------------- setup / loop ------------------------- */

void setup() {
  Serial.begin(115200);

  pinMode(PIR_PIN, INPUT);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, HIGH);   // 开机 LED 灭(PC13 低电平点亮)

  Wire.setSDA(I2C_SDA_PIN);
  Wire.setSCL(I2C_SCL_PIN);
  Wire.begin();
  Wire.setClock(400000);

  Serial.println("HC-SR501 PIR TEST");
  Serial.print("Firmware: ");
  Serial.println(FIRMWARE_VERSION);
  Serial.println();

  // 先探测 I2C 地址, 给出明确错误信息(display.begin 的返回值不保证真实 ACK)
  Wire.beginTransmission(OLED_ADDRESS);
  if (Wire.endTransmission() != 0) {
    Serial.println("ERROR: OLED not found at 0x3C (check wiring/I2C)");
    displayOk = false;
    return;
  }

  displayOk = display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS, true, false);
  if (!displayOk) {
    Serial.println("ERROR: OLED init failed");
    return;   // loop() 中快速闪灯提示
  }

  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextWrap(false);

  phase        = Phase::BOOT;
  phaseStartMs = millis();
  drawBootPage();
}

void loop() {
  uint32_t now = millis();

  if (!displayOk) {
    // OLED 初始化失败: PC13 快速闪灯报警(非阻塞)
    if (now - lastLedMs >= 100) {
      lastLedMs = now;
      digitalWrite(LED_PIN, !digitalRead(LED_PIN));
    }
    return;
  }

  switch (phase) {

    case Phase::BOOT:
      if (now - phaseStartMs >= BOOT_SCREEN_MS) {
        phase         = Phase::WARMUP;
        phaseStartMs  = now;
        lastCountdown = 255;   // 强制立刻刷新倒计时
        lastLedMs     = 0;
      }
      break;

    case Phase::WARMUP: {
      uint32_t elapsed = now - phaseStartMs;
      if (elapsed >= PIR_WARMUP_MS) {
        enterRun(now);
        break;
      }

      // 预热期间不采样、不计数: 此时模块输出不可信

      uint8_t remain = (uint8_t)((PIR_WARMUP_MS - elapsed + 999) / 1000);
      if (remain != lastCountdown) {        // 剩余秒数变化才刷新 OLED
        lastCountdown = remain;
        drawWarmupPage(remain);
      }
      if (now - lastSerialMs >= 1000) {     // 串口每秒报一次预热进度
        lastSerialMs = now;
        Serial.print("PIR | WARMING UP | ");
        Serial.print(remain);
        Serial.println(" s");
      }
      if (now - lastLedMs >= LED_BLINK_MS) {  // 预热阶段 LED 慢闪
        lastLedMs = now;
        digitalWrite(LED_PIN, !digitalRead(LED_PIN));
      }
      break;
    }

    case Phase::RUN: {
      /* 1) 采样 + 稳定确认(去毛刺): 电平连续稳定 INPUT_STABLE_MS 才承认变化 */
      uint8_t sample = digitalRead(PIR_PIN);
      if (sample != lastSample) {
        lastSample  = sample;
        rawChangeMs = now;               // 出现变化, 重新计时
      }
      if (now - rawChangeMs >= INPUT_STABLE_MS) {
        if (confirmedRaw != lastSample) {  // 稳定变化被确认
          uint8_t prev = confirmedRaw;
          confirmedRaw = lastSample;
          if (prev == LOW && confirmedRaw == HIGH) {
            motionCount++;                 // 仅 LOW->HIGH 上升沿计数一次
          }
          stateDirty = true;
        }
      }

      /* 2) LED: 检测到移动->亮, 未检测到移动->灭 (PC13 低电平点亮) */
      digitalWrite(LED_PIN, (confirmedRaw == HIGH) ? LOW : HIGH);

      /* 3) 串口: 每 400ms 输出一次, 与 OLED 状态完全一致 */
      if (now - lastSerialMs >= SERIAL_INTERVAL) {
        lastSerialMs = now;
        Serial.print("PIR | RAW=");
        Serial.print(confirmedRaw);
        Serial.print(" | STATE=");
        Serial.print(confirmedRaw == HIGH ? "MOTION    " : "NO MOTION ");
        Serial.print("| COUNT=");
        Serial.println(motionCount);
      }

      /* 4) OLED: 状态变化立即刷新; MOTION 期间每 200ms 播一帧动画 */
      if (stateDirty ||
          (confirmedRaw == HIGH && now - lastDisplayMs >= ANIM_TICK_MS)) {
        stateDirty    = false;
        lastDisplayMs = now;
        drawRunPage(confirmedRaw == HIGH,
                    (uint8_t)((now / ANIM_TICK_MS) % 4U));
      }
      break;
    }
  }
}

依赖库:Adafruit SSD1306、Adafruit GFX Library,用 PlatformIO 打开工程会按 platformio.ini 自动下载,不用手动安装。

Download

下载

解压后用 VS Code + PlatformIO 打开这个文件夹就能 Build,一般不需要改代码。

PlatformIO 工程 ZIP 含 platformio.ini、src/main.cpp、README.md。已去掉本机构建路径,解压后 VS Code 打开即可 Build。
下载 ZIP

第一次使用

  1. 按上面接线表接好:HC-SR501 的 VCC→5V、OUT→PA0、GND→GND;OLED 的 VCC→3.3V、GND→GND、SCL→PB8、SDA→PB9。共 7 根线,接之前核对模块丝印
  2. 下载 ZIP,解压,用 VS Code + PlatformIO 打开这个文件夹
  3. 等 Adafruit SSD1306、GFX 两个库自动装完,点 Build,看到 SUCCESS
  4. 接上 ST-Link,点 Upload,看到 ** Verified OK ** 和 [SUCCESS]
  5. 上电先看开机页(1.5 秒),再等预热倒计时(约 30 秒),之后进入运行页。手一挥,OLED 显示 MOTION 并计数;手停下恢复 NO MOTION
HC-SR501 烧录成功截图:VS Code + PlatformIO 终端 Programming Finished / Verified OK / SUCCESS
烧录成功截图:终端 ** Programming Finished ** → ** Verified OK ** → [SUCCESS](ST-Link 走 SWD)
不要照抄 COM 号。截图里出现的串口号是我们这台电脑的,你的电脑多半不一样。查当前串口:pio device list,找 CP2102 / CP210x / CH340 对应的那个。本项目也可以不看串口——OLED 直接看 MOTION/NO MOTION 就行。
这块板子不能用网页烧录器。网页烧录(WebSerial)主要面向 ESP32 / ESP8266 这类自带 USB 串口下载的芯片。STM32F103C8T6 本项目用的是 ST-Link + SWD,浏览器接触不到 SWD 接口。

Troubleshooting

常见问题排查

下面每一条都是这个模块真实会遇到的。标注「实测」的是我们自己踩过的。每条按「现象 → 原因 → 怎么办」来。

人坐在感应区里,OLED 一直显示 NO MOTION——是不是坏了?

不是坏了。HC-SR501 只检测移动:人静坐不动时红外辐射没有变化,OUT 就是 LOW,程序报 NO MOTION,这是所有 PIR 模块的正常特性。验证方法:上电等约 30 秒预热结束后,在感应范围内挥手或走动,OLED 应立即 MOTION! 且 COUNT +1。想检测「静止有人」需要换毫米波雷达 / 人体存在传感器(如 LD2410)。

刚上电怎么挥手都没反应

正常。开机后有约 30 秒预热期(OLED 显示 WARMING UP 倒计时、LED 慢闪),期间程序故意不采样、不计数——模块刚上电输出不稳定。等倒计时结束进入运行页后再挥手测试。

OLED 完全不亮

查 OLED 四根线:VCC 是否 3.3V(不是 5V)、GND 是否共地、SCL 是否 PB8、SDA 是否 PB9。本项目代码会先对 0x3C 做 I2C 探测:串口若打印 ERROR: OLED not found at 0x3C 就是接线问题;若 OLED 初始化失败,PC13 会每 100ms 快速闪灯报警。

没人也计数、COUNT 乱跳

按概率排查:① 灵敏度电位器调太高——空调出风、暖气、阳光直射、宠物等热源会反复触发,逆时针调小一点;② 供电不稳——别和电机、继电器等共用不稳的 5V;③ 杜邦线接触不良——重新插紧或换线。程序侧已做 40ms 稳定确认 + 仅上升沿计数,正常的小抖动和短暂干扰脉冲是进不了 COUNT 的,所以还能乱跳基本是上面的外部原因。

MOTION 很久不恢复 NO MOTION

两个常见原因:① 延时电位器调得大,OUT 高电平保持时间长,逆时针调小可加快恢复;② 触发模式跳帽在 H(可重复触发),人在感应区内持续移动会不断顺延保持 HIGH。程序只在 LOW→HIGH 上升沿 +1,保持时间长不会导致多计数。

挥手时灵时不灵

先检查灵敏度电位器是否调得太小、模块离人是否太远。另外热释电分区成像的原理决定了:横向穿过感应区最容易触发,正对传感器直线走近时红外变化小、相对迟钝 资料参考。测试时建议在 2~3 米内横向挥手验证。

串口看不到数据

先查当前串口:

pio device list

找到 CP2102 / CP210x / CH340 对应的那个 COM,然后在自己电脑的 platformio.ini 里加:

monitor_port = COMx
monitor_speed = 115200

COMx 要换成你自己电脑实际识别到的端口号,不要直接抄。只有需要看串口时才加这两行,正常用 ST-Link 烧录不需要;本项目 OLED 屏上直接就有 STATE / RAW / COUNT,不看串口也能用。

Reply scripts

客户回复话术

客服 / 售后可直接整段复制发给客户(旺旺 / 微信通用),已按「先结论安抚 → 再原因 → 最后动作」写好。

「是不是坏了」——客户说:人坐着不动它也显示没检测到人 / 一直 NO MOTION

您好,先放心,这个大概率不是模块坏了。HC-SR501 是「移动检测」传感器,不是「有人检测」:它靠感知人体红外辐射的变化来触发,人如果坐在感应区里基本不动,红外没有变化,模块就会一直输出 NO MOTION——这是这类模块的正常特性,市面上的 HC-SR501 都是这样工作的。您可以这样验证:上电后先等大约 30 秒(预热期间不检测属于正常流程),然后在感应范围内(对着白色圆顶方向 2~3 米)挥手或走动一下,OLED 应立即显示大字 MOTION! 并且计数加一,人停下几秒后就恢复 NO MOTION。如果挥手有反应,说明模块和接线都完全正常。如果觉得感应距离偏短,可以把模块侧面标着「灵敏度」的电位器顺时针微调一点点再试(建议一次不要调太多,调太高时空调、暖气等热源也会引起误触发)。按上面方法验证后仍然完全没有反应的话,麻烦拍一段从上电到预热结束、再在前方挥手的短视频发给我们,我们帮您继续定位。

「接了没反应」——客户说:线都接了,一点反应都没有

您好,「接了完全没反应」请按下面顺序排查,绝大多数问题都出在前三步:1)先看 VCC:HC-SR501 要接 5V 供电(它支持 4.5~20V),不是 3.3V,请确认接在开发板的 5V 引脚上;2)看 OUT 是否接在 PA0、GND 是否与开发板共地,三根线一根都不能悬空,杜邦线两头按紧;3)上电后耐心等 30 秒:程序在预热阶段是故意不采样的(OLED 会显示 WARMING UP 倒计时),30 秒进入运行页后才开始检测,这不是死机;4)然后在感应范围内挥手、走动,看 OLED 是否显示 MOTION!;5)仍无反应的话,把灵敏度电位器微调一下,再换一根线试试 OUT 那根杜邦线是否断芯。另外请特别注意:不同批次模块的 VCC/GND/OUT 排列顺序可能不一样,一定要以模块背面的丝印为准,别按图片或旧资料的位置硬插。都试过还不行的话,麻烦拍一张清晰的接线整体照片和一段上电视频发给我们,我们帮您在板上定位。

「读数不对 / 乱计数」——客户说:没人它也计数,数字乱跳

您好,「没人也计数、读数乱跳」一般不是程序问题,多半是环境干扰或供电接触的问题,按下面三步处理:1)把灵敏度电位器逆时针调小一点——灵敏度调太高时,超出您预期的热源(空调出风、暖气、阳光直射、宠物等)都会反复触发;2)检查供电稳定性——模块不要和电机、继电器等大功率设备共用一路不稳的 5V,可换一个 USB 充电头或电源试试;3)杜邦线接触不良同样会造成电平乱跳,把排针重新插紧或换线试一下。程序这边我们已经做了 40 毫秒的稳定确认(去毛刺):只有连续稳定为高的信号才算一次触发,而且只在从低变高的瞬间计数加一,正常的小抖动和短暂干扰是进不了计数的。所以如果调整后还在乱计数,基本可以确定是上面 1~3 的外部原因,处理好之后再观察即可;如果还有问题,欢迎拍一段现象视频发给我们,我们继续帮您分析。

References

资料来源

关于外部资料:HC-SR501 是公开通用模块,没有单一权威 datasheet 链接可给。本页「资料参考」内容来自 BISS0001 芯片资料、热释电红外传感器公开通用原理与模块厂家资料,均由我们重新整理表述,未直接复制原文。

本页信息来源声明

内容来源
接线、代码、OLED 三页 UI、三阶段状态机、40ms 去毛刺、上升沿计数、串口输出本项目实测 2026-08-19 实机测试(完整代码 v1.0 一字未改)
输出高电平约 3.3V、引脚以丝印为准的提醒本项目实测
工作电压 4.5~20V、检测距离约 3~7m 可调、灵敏度/延时电位器、H/L 触发模式资料参考 模块厂家资料,由我们重新整理表述;其中电位器具体范围标「待确认」
热释电效应、菲涅尔透镜分区成像、BISS0001 信号处理资料参考 BISS0001 芯片资料与热释电公开通用原理,由我们重新整理表述
SKU待确认(本页不写未核实编号,以商品页与客服说明为准)

本页所有「资料参考」内容均由我们重新整理表述,未直接复制原文。不同厂家 / 批次模块可能存在引脚排列、电位器默认参数差异,接线与参数以你手上模块的实际丝印和卖家说明为准。本页最后更新:2026-08-19(实机验证日期)。