传感器 · 人体红外移动检测 · 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。
STM32F103C8T6 接上 HC-SR501 后,人一进入感应范围移动,OLED 立即大字反白显示 MOTION! 并计数加一,板载 LED(PC13)点亮;手停下或移开,OLED 恢复 NO MOTION。开机先经过约 30 秒预热,预热期间不采样、不计数。
完整演示视频(接线 → 预热 → 触发 → 烧录)
OLED 运行页两个状态
运行页固定显示 PIR SENSOR 标题 + STATE + RAW + COUNT,右侧是动画小人:检测到移动时显示反白闪烁的 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 |
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 秒) | 本项目实测 |
| 板载 LED | PC13(低电平点亮),跟随 MOTION / NO MOTION;预热期慢闪 | 本项目实测 |
| OLED | 0.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,是两条不同的电源线。
接线示意图(一眼看懂)
HC-SR501(3 针:VCC / OUT / GND)
| 信号 | 模块丝印 | STM32F103C8T6 | 说明 |
|---|---|---|---|
| 电源 | VCC | 5V | 模块需 5V 供电(4.5~20V),别和 OLED 的 3.3V 搞混 |
| 信号 | OUT | PA0 | 高电平=检测到移动(输出高约 3.3V,实测,PA0 可直读) |
| 地 | GND | GND | 必须共地 |
OLED(0.96 寸 SSD1306,走硬件 I2C)
| 信号 | OLED | STM32F103C8T6 |
|---|---|---|
| 电源 | VCC / VDD | 3.3V(不是 5V) |
| 地 | GND | GND |
| 时钟 | SCL / SCK | PB8 |
| 数据 | SDA | PB9 |
烧录与串口
| 用途 | 连接 | 说明 |
|---|---|---|
| 烧录 | ST-Link → SWD 四线(3.3V / GND / SWDIO=PA13 / SWCLK=PA14) | 烧录用 |
| 串口 | PA9(TX) / PA10(RX) → CP2102 | 115200,可选 |
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 两种措辞,就是为了避免误会。
模块实拍
热释电效应、菲涅尔透镜分区成像、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)
| 阶段 | 时长 | 程序行为 |
|---|---|---|
| BOOT | 1.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,一般不需要改代码。
第一次使用
- 按上面接线表接好:HC-SR501 的 VCC→5V、OUT→PA0、GND→GND;OLED 的 VCC→3.3V、GND→GND、SCL→PB8、SDA→PB9。共 7 根线,接之前核对模块丝印
- 下载 ZIP,解压,用 VS Code + PlatformIO 打开这个文件夹
- 等 Adafruit SSD1306、GFX 两个库自动装完,点 Build,看到 SUCCESS
- 接上 ST-Link,点 Upload,看到
** Verified OK **和[SUCCESS] - 上电先看开机页(1.5 秒),再等预热倒计时(约 30 秒),之后进入运行页。手一挥,OLED 显示 MOTION 并计数;手停下恢复 NO MOTION
** Programming Finished ** → ** Verified OK ** → [SUCCESS](ST-Link 走 SWD)pio device list,找 CP2102 / CP210x / CH340 对应的那个。本项目也可以不看串口——OLED 直接看 MOTION/NO MOTION 就行。
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
「接了没反应」——客户说:线都接了,一点反应都没有
「读数不对 / 乱计数」——客户说:没人它也计数,数字乱跳
References
资料来源
本页信息来源声明
| 内容 | 来源 |
|---|---|
| 接线、代码、OLED 三页 UI、三阶段状态机、40ms 去毛刺、上升沿计数、串口输出 | 本项目实测 2026-08-19 实机测试(完整代码 v1.0 一字未改) |
| 输出高电平约 3.3V、引脚以丝印为准的提醒 | 本项目实测 |
| 工作电压 4.5~20V、检测距离约 3~7m 可调、灵敏度/延时电位器、H/L 触发模式 | 资料参考 模块厂家资料,由我们重新整理表述;其中电位器具体范围标「待确认」 |
| 热释电效应、菲涅尔透镜分区成像、BISS0001 信号处理 | 资料参考 BISS0001 芯片资料与热释电公开通用原理,由我们重新整理表述 |
| SKU | 待确认(本页不写未核实编号,以商品页与客服说明为准) |
本页所有「资料参考」内容均由我们重新整理表述,未直接复制原文。不同厂家 / 批次模块可能存在引脚排列、电位器默认参数差异,接线与参数以你手上模块的实际丝印和卖家说明为准。本页最后更新:2026-08-19(实机验证日期)。