STM32与PIR传感器、OLED组合:一套入门到实战的人体检测显示项目
2026/9/9 1:41:58 网站建设 项目流程

做嵌入式这些年,我给新手推荐过不少练手项目,如果只让我留一个既简单又有用的组合,那肯定是STM32F103C8T6最小系统板加HC-SR501人体红外传感器,再配一块0.96寸OLED显示屏。这套组合成本低、接线少、逻辑清晰,从GPIO读取到I2C显示,把单片机最常用的几个功能全串起来了,而且做出来的东西确确实实能用在实际场景里——人来灯亮、人员检测、安防提醒都能往上靠。

这篇文章我从HC-SR501的检测原理开始讲,把接线电平、CubeMX配置、HAL库代码逻辑、OLED显示驱动,以及我这些年踩过的各种坑全部拆开揉碎。适合刚学会点灯、想往实际应用走一步的初学者,也适合给课程设计或者小项目找方案的朋友参考。看完之后你可以直接照着搭一套,传感器触发结果实时显示在OLED上,整个过程半天左右就能跑通。

1. 项目整体设计与思路拆解

1.1 为什么是这三件套

STM32F103C8T6这颗芯片在嵌入式圈子里有多普及,不用我多说。最小系统板几十块钱,主频72MHz,20KB RAM、64KB Flash,跑一个传感器采集加OLED刷屏绰绰有余。而且它的资料密度极高,不管是用标准库还是HAL库,遇到问题随便一搜就有答案,对新手极其友好。

HC-SR501则是目前市面上最经典的热释电红外(PIR)传感器模块,十几块钱一块,集成了菲涅尔透镜、热释电探头、信号放大电路和比较器输出,直接输出TTL电平的高/低信号。它对人体移动敏感,不需要像超声波那样主动发射信号,也不像摄像头那样有隐私问题,做“有人/没人”检测是最省事的选择。

OLED选0.96寸I2C接口版本,主要是因为接线少。整块屏幕只用SDA和SCL两根信号线,加上VCC和GND一共四根线,驱动芯片是SSD1306,内部自带显存,单片机只管往显存里写数据就行。相比1602液晶屏需要8根数据线,SSD1306的接线和驱动都要友好太多,而且还能显示中文、图形、小动画。

这三样东西组合在一起,刚好覆盖了嵌入式开发的几个核心技能点:GPIO输入检测、外部信号判读、I2C通信协议、显示驱动、状态机逻辑。做完这个小项目,再往其他方向扩展就有底子了。

1.2 HC-SR501的检测原理到底是怎么回事

很多人拿到HC-SR501就直接接线用,从没想过它内部是怎么工作的。结果遇到“静止的人检测不到”“对着窗户乱触发”这类问题就懵了。所以我先把原理讲透。

人体体温大约37摄氏度,会持续向外辐射波长9到10微米左右的红外线。HC-SR501的核心是一个热释电红外探头,内部有两片(有些型号是四片)热释电元件,它们串联反向连接。当人体静止不动时,两片元件接收到的红外辐射基本相同,差分信号为零;当人体在探测范围内移动时,两片元件感受到的红外辐射出现差异,就会产生一个微弱的交流电信号,这个信号经过模块内部场效应管和高增益比较器处理后,输出端就会输出高电平。

这就是PIR传感器的根本特点:只对移动的人体敏感,对静止的人不敏感。你坐在椅子上不动,传感器不一定能一直保持高电平输出,但只要稍微动一下,立刻就能检测到。明白了这一点,就不会再拿“为什么人坐着不动灯却灭了”这种问题来困惑自己了。

模块前面那个白色半透明的“小圆罩”叫菲涅尔透镜,作用是把探测区域分割成若干个明暗相间的条纹。人体在透镜前移动时,红外辐射会周期性变化,相当于给传感器输入了一个交变信号,这对提高检测可靠性至关重要。如果拆掉这个透镜,探测距离会急剧缩短到一米以内,而且方向性极差。所以使用时要保持透镜干净、不要遮挡,新模块透镜上的保护膜一定要撕掉,否则灵敏度会大打折扣。

1.3 HAL库还是标准库:我为什么推荐HAL库路线

STM32F103C8T6的开发方式目前有三条路:寄存器、标准外设库、HAL库。寄存器最底层、代码最啰嗦,但你能看到每一个位操作;标准库曾经是主流,但ST已经停止更新;HAL库配合STM32CubeMX图形化配置工具,是目前官方主推的方案。

我给新手的建议很明确:直接用HAL库加CubeMX。理由有两个。第一,CubeMX把时钟树、引脚复用、外设初始化这些繁琐的配置全部图形化,鼠标点几下就能生成初始化代码,大大降低入门门槛。第二,HAL库的函数封装层次高,比如I2C通信你只需要调用HAL_I2C_Master_Transmit,内部时序细节全由库处理,出了问题也容易排查。

这篇文章里的所有代码都是基于HAL库加CubeMX生成的工程,标准库的读者也不要紧,逻辑是相通的,核心就是GPIO读电平加I2C写数据。

2. 硬件接线与关键调节:别让电平问题毁掉整个项目

2.1 完整接线表和引脚分配

STM32F103C8T6最小系统板的引脚排列比较规整,周边一圈排针,3.3V、5V、GND都有引出。HC-SR501模块有三个引脚:VCC、GND、OUT,OLED四根线:VCC、GND、SCL、SDA。下面是我用的接线表,直接照着接就行。

HC-SR501引脚接STM32F103C8T6说明
VCC5V(或3.3V,见2.2节分析)传感器供电
GNDGND共地
OUTPA0GPIO输入,检测输出电平
OLED引脚接STM32F103C8T6说明
VCC3.3VSSD1306工作电压
GNDGND共地
SCLPB6I2C1_SCL
SDAPB7I2C1_SDA

PA0选它是因为这脚默认支持外部中断,后面想改成“有人触发中断”模式可以直接用,不用换引脚。PB6和PB7是F103的I2C1默认引脚,在CubeMX里直接选择I2C1之后,这两个引脚会自动被分配到对应功能,不用额外手动映射。

2.2 电平匹配:这可能是你最该注意的坑

很多新手在这里翻车。HC-SR501的典型工作电压是4.5V到20V,官方推荐5V供电。如果直接用5V给它供电,那么OUT引脚输出高电平的时候,电压也接近5V。而STM32F103C8T6的GPIO耐压要看引脚类型:标记为FT(Five-volt tolerant)的引脚可以承受5V,但PA0不是FT引脚,它是普通的TTL引脚,手册上标注最大输入电压就是VDD+0.3,也就是3.6V左右。长期让5V直接灌进PA0,轻则读到的电平不稳定,重则烧坏引脚甚至整个芯片。

解决这个问题有两条路。第一条是传感器用3.3V供电,这样OUT高电平约3.3V,直接接PA0安全。但缺点是HC-SR501在3.3V下工作点有点微妙,有些模块能正常工作,有些模块会出现灵敏度下降、触发不稳定、甚至上电后乱输出高电平的情况,个体差异很大。第二条是传感器保持5V供电,OUT输出先经过电阻分压再接PA0。用两个电阻,比如10K串联加20K并联到地,5V经过分压后大约3.33V,安全且稳定。我实测下来,第二条路最省心,推荐直接采用。

如果你不想加分压电阻又必须用5V给传感器供电,至少也要在OUT和PA0之间串联一个1K的限流电阻,虽然不能把高电平降到理想值,但能降低灌入引脚的风险。

2.3 传感器上的两个旋钮和跳线怎么调

HC-SR501模块正面有两个小的可调电位器,一个是灵敏度调节,一个是延时调节。很多人不知道这俩能调,也不清楚调节方向。

灵敏度旋钮控制的是检测距离,顺时针旋到底,检测距离最大,标称能到7米左右;逆时针旋到底,检测距离可能只有1到2米。实际使用中不建议直接旋到底,因为灵敏度过高会把空调吹出的热风、窗外移动的汽车红外辐射、甚至夏天隔着玻璃的日光都误判成人,导致频繁误触发。我一般会先把灵敏度调到中等偏小的位置,然后在现场实测调整。

延时旋钮控制的是输出高电平的持续时间,顺时针延长,最长可达300秒左右,逆时针缩短,最短约5秒。调的时候注意,延时太长会让“有人”状态一直锁住,看起来就像传感器坏了;延时太短又可能在人还在移动范围内时输出就变低了。调试阶段建议先把延时调到最短,这样响应快,方便观察。

模块背面通常还有一个跳线帽,用于选择触发模式。跳到H位置是可重复触发模式:只要人在探测区内持续活动,输出就一直保持高电平,人离开后才开始计时并恢复低电平;跳到L位置是不可重复触发模式:检测到人后输出一个固定宽度的高电平脉冲,时间由延时旋钮决定,即使人继续在探测区内,也不会重新触发。做“人来灯亮、人走灯灭”就选H模式,做“检测到一次人活动就报警一下”就选L模式。

3. 代码逻辑与核心实现:从CubeMX配置到OLED刷屏

3.1 CubeMX工程配置的完整步骤

打开STM32CubeMX,选择芯片型号STM32F103C8T6,然后按下面步骤配置。

SYS选项里把Debug设为Serial Wire,这个很关键。不设置的话,烧录器下载完程序后SWD引脚功能可能被禁用,下次就没法下载程序了,只能按复位键加下载,非常麻烦。

RCC选项里把HSE设为Crystal/Ceramic Resonator,如果你用的最小系统板上有8MHz晶振的话,这样可以让系统时钟跑满72MHz。CubeMX会自动根据HSE值计算PLL倍频系数。

I2C1选项里把I2C设为I2C标准模式,速度选100KHz即可。SSD1306虽然支持400KHz快速模式,但100KHz更稳定,尤其飞线比较长或者模块质量一般的时候,降低I2C时钟能减少很多诡异的花屏问题。

GPIO里把PA0设为GPIO_Input,下拉电阻方式。这里设置为下拉,是为了保证传感器空闲输出低电平时PA0能读到确定性的低电平。如果PA0悬空,I2C和OLED初始化时序可能会引起引脚电平抖动,导致上电瞬间误报一次“有人”。

时钟树页面基本不用动,CubeMX会自动配置好。生成工程时选择Toolchain为MDK-ARM,代码生成选项里勾选“生成外设初始化函数”的独立.c/.h文件,方便阅读。生成后用Keil打开工程,就可以开始写逻辑了。

3.2 OLED显示的I2C驱动核心

SSD1306驱动芯片的I2C通信其实不复杂。每个传输都先从设备地址开始,0.96寸OLED模块的I2C地址大多数是0x78(7位地址0x3C左移一位),如果模块SA0引脚被拉高则是0x7A。HAL库的HAL_I2C_Master_Transmit函数里传的DevAddress参数需要的是8位地址,所以填0x78或者0x7A,别弄混了。

发送地址之后,还要跟一个控制字节。控制字节0x00表示后面跟的是命令,0x40表示后面跟的是数据。比如写初始化命令、设置显示坐标这些都属于命令,往显存里填充显示内容则属于数据。

下面是一段精简的SSD1306初始化代码,基于HAL库的I2C1:

#define OLED_ADDR 0x78 void OLED_WriteCmd(uint8_t cmd) { uint8_t buf[2] = {0x00, cmd}; HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDR, buf, 2, 100); } void OLED_WriteData(uint8_t data) { uint8_t buf[2] = {0x40, data}; HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDR, buf, 2, 100); } void OLED_Init(void) { HAL_Delay(100); OLED_WriteCmd(0xAE); // 关闭显示 OLED_WriteCmd(0xD5); OLED_WriteCmd(0x80); // 设置显示时钟 OLED_WriteCmd(0xA8); OLED_WriteCmd(0x3F); // 设置复用率 1/64 OLED_WriteCmd(0xD3); OLED_WriteCmd(0x00); // 显示偏移 0 OLED_WriteCmd(0x40); // 起始行 0 OLED_WriteCmd(0x8D); OLED_WriteCmd(0x14); // 开启电荷泵,必须开 OLED_WriteCmd(0x20); OLED_WriteCmd(0x00); // 水平寻址模式 OLED_WriteCmd(0xA1); // 段重映射 OLED_WriteCmd(0xC8); // COM扫描方向 OLED_WriteCmd(0xDA); OLED_WriteCmd(0x12); // COM引脚配置 OLED_WriteCmd(0x81); OLED_WriteCmd(0xCF); // 对比度 OLED_WriteCmd(0xD9); OLED_WriteCmd(0xF1); // 预充电周期 OLED_WriteCmd(0xDB); OLED_WriteCmd(0x40); // VCOMH电平 OLED_WriteCmd(0xA4); // 显示内容跟随RAM OLED_WriteCmd(0xA6); // 正常显示,非反显 OLED_WriteCmd(0xAF); // 打开显示 OLED_Clear(); }

这段初始化序列是SSD1306的标准流程,大部分现成库都是这个套路。重点是那个开启电荷泵的命令0x8D 0x14,如果漏掉,屏幕会一直黑着什么都不显示,这是新手最容易踩的坑。

清屏函数要把显存全部写成0。SSD1306的显存组织方式是页寻址模式,128列分成8页,每页对应8行像素,一页128字节。清屏就是循环操作8页,每页128列都写0x00:

void OLED_Clear(void) { for (uint8_t page = 0; page < 8; page++) { OLED_WriteCmd(0xB0 + page); // 设置页地址 OLED_WriteCmd(0x00); // 列地址低4位 OLED_WriteCmd(0x10); // 列地址高4位 for (uint8_t col = 0; col < 128; col++) OLED_WriteData(0x00); } }

显示字符串需要字库。ASCII字符可以用8x16点阵或者6x12点阵,网上大量ST7920或SSD1306字库源码可以直接移植。汉字需要16x16点阵,用取模软件如PCtoLCD2002生成,取模方式选择“逐行式、阴码”,生成后的数组就是按页排列的,显示时先写上半部分到当前页,再写下半部分到下一页。这个方法我用过很多次,稳定可靠。

3.3 主程序逻辑:PIR状态读取与显示切换

主逻辑的本质就是读GPIO,然后决定OLED显示什么。但为了让代码更合理,我建议不要在循环里每次都刷OLED,而是只在传感器输出状态变化时更新一次显示,这样既能减少I2C通信负担,也能让屏幕显示更干净。

主循环的核心代码如下:

uint8_t last_state = GPIO_PIN_RESET; uint8_t cur_state; while (1) { cur_state = HAL_GPIO_ReadPin(PIR_GPIO_Port, PIR_Pin); if (cur_state != last_state) { if (cur_state == GPIO_PIN_SET) { OLED_ShowString(0, 2, "Human: YES"); HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); } else { OLED_ShowString(0, 2, "Human: NO "); HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); } last_state = cur_state; } HAL_Delay(50); }

OLED_ShowString的第一个参数是起始列,第二个参数是起始页。0.96寸OLED共8页,每页8像素,如果使用8x16字体,一个字符占2页高,那么第一行从第0页开始,第二行从第2页开始,所以我这里传的是2。这个参数如果不理解,显示位置就会错乱,字会出现在屏幕中间甚至乱码位置。

加了LED指示就是因为OLED刷屏有I2C通信时间,如果传感器状态变化非常快,纯粹靠OLED判断会有延迟感。LED点灯是GPIO直驱动,毫秒级响应,更适合做实时状态观察。我实际调试时也是先看LED再看OLED,效率高很多。

如果你想用串口辅助调试,重定义fputc后加printf即可,逻辑更直观:

printf("PIR ON\r\n");

串口输出对排查传感器输出抖动特别有帮助,OLED上可能只看到状态闪了一下,串口里能看出几次跳变。

4. 调试排坑实测:这些问题我基本都踩过

4.1 上电乱触发和一直触发怎么办

HC-SR501上电后会有几十秒的“预热”阶段,内部电路正在建立稳定的工作点,这时候输出可能会随机跳变几次,也可能会输出一个高电平脉冲。这不是坏了,是正常现象。做产品的话,程序里可以加一个开机延时,让系统上电后30秒内忽略传感器信号,或者延时初始化后再开始读取。

如果预热结束后仍然一直稳定输出高电平,大概率是灵敏度过高,环境里有干扰源。最常见的干扰源是空调出风口、暖气片、白炽灯/日光灯附近、阳光直射区域、以及窗外移动的车辆。解决方法是逆时针调小灵敏度旋钮,同时调整传感器朝向,不要正对干扰源。还有一个容易被忽略的情况:夏天房间内开了空调,室温低于体温,人一旦进入探测区红外差分信号特别强,误触发反而比冬天更容易。

如果传感器输出一直低电平、怎么走动都不触发,首先检查有没有撕掉菲涅尔透镜上的保护膜,其次检查供电电压。用3.3V供电的时候,部分模块会出现灵敏度很低甚至完全不触发的情况,这时候换成5V供电加分压电阻就能解决。

4.2 OLED白屏、花屏、乱码的排查思路

OLED白屏是最常见的故障。先确认模块有电,看背面有没有电压标注,VCC接3.3V一般没问题。然后确认SCL和SDA有没有接反,这两个接反了通信完全失败。接着确认地址对不对,0x78和0x7A只差一位,错了就白屏。

如果地址正确、接线没问题还是白屏,八成是初始化时序里少了开启电荷泵的命令。我在3.2节里特别强调过0x8D 0x14这一条,必须要有。还有一种情况是CubeMX生成的I2C初始化在OLED_Init之前没有执行,或者执行顺序不对,导致OLED初始化时I2C根本没有准备好。在main函数里,MX_I2C1_Init必须在OLED_Init之前调用。

花屏或者乱码则和通信稳定性有关。I2C速度太高、飞线太长、供电纹波偏大会引起画面内容错乱。解决办法是先把I2C时钟降到100KHz,再用杜邦线把OLED供电引脚就近对地加一个100nF去耦电容。显示乱码还有可能是字库取模方式不对,导致字符上下颠倒、左右反转,这个属于取模参数问题,重新生成字库即可。

老版本HAL库的I2C还有一个著名的卡死问题,就是调用HAL_I2C_Master_Transmit后程序卡在I2C等待超时标志里。更新到较新的HAL库版本能解决,不想升级库的话,在MX_I2C1_Init函数末尾加上两行:

__HAL_I2C_DISABLE(&hi2c1); __HAL_I2C_ENABLE(&hi2c1);

强制复位一下I2C外设,很多卡死问题就消失了。

4.3 常见问题速查表

现象可能原因解决方法
上电LED闪一下但功能正常传感器预热自检正常现象,程序里延时30秒再检测即可
一直触发,没人却显示有人灵敏度过高或环境干扰源逆时针调灵敏度,调整朝向,避开空调/阳光/暖气
人走过不触发灵敏度过低、透镜保护膜未撕、3.3V供电调高灵敏度、撕膜、改5V供电加分压
静止的人检测不到PIR原理限制只能检测移动人体,这是正常的
OLED白屏接线错误、地址错误、电荷泵命令缺失检查SCL/SDA、换0x7A试、检查初始化序列
OLED花屏I2C速率过高、供电不稳、字库取模错误降速到100KHz、加去耦电容、重新取模
代码卡死在I2C函数旧版HAL库I2C状态机bug更新HAL库或用__HAL_I2C_DISABLE/ENABLE复位
检测距离变很短供电电压低、透镜脏污、灵敏度过低换5V供电、清洁透镜、适当调高灵敏度

4.4 一个容易忽略的安装细节

HC-SR501的安装高度和角度对检测效果影响很大。装在桌面上平放,探测范围是一个很扁的扇形,人从侧边走过容易漏检。合理做法是安装在离地面2米左右的高度,透镜朝下倾斜15到30度,这样探测区域能覆盖到人的正常活动范围,而且不容易被宠物触发。

另外,传感器不要正对门口安装,人推门进来时红外辐射方向变化太快,反而会短暂触发后迅速消失。稍微侧装一点,让检测区域交叉覆盖门口,效果会好很多。这些小细节在实验室静态测试时看不出来,真正装到实际环境里差别非常大。

5. 扩展方向与个人经验

这套系统跑稳之后,扩展空间非常大。加一个继电器模块,就能把“有人”状态直接转成灯控信号,做成走廊和卫生间的感应灯;加一个有源蜂鸣器,就成了简易入侵报警器;加一个ESP8266或者4G模块,就能把“有人/没人”状态上报到物联网平台,手机远程查看。传感器换成一氧化碳或者火焰传感器,代码逻辑几乎不用改动,就变成一个环境监测节点,这也是这套架构最大的价值所在。

这么多年做下来,我给新手朋友的建议就一条:先把GPIO读电平加OLED显示这一步彻底跑通,再考虑加中断、加通信、加云平台。很多人一上来就想把所有功能全加上,结果出了问题根本不知道是传感器的问题、OLED的问题还是网络的问题,排查起来特别痛苦。这个组合最大的优点就是每一层链路都简单独立,分工明确,方便你一步步把整个系统吃透。

模块接线的时候最好用不同颜色的杜邦线,红色接电源,黑色接地,黄色或蓝色接信号,这样排查起来一眼就能分清。PCB板做原型测试时,传感器和单片机之间尽量拉开一点距离,别把散热片或者电源模块紧贴着传感器放置,热辐射会影响PIR的判断。最后再说一句,HC-SR501这东西个体差异确实存在,同一批模块灵敏度都可能不一样,项目里用到多套的时候,每一套都要单独标定一下旋钮位置,不要想着用一个参数通吃所有模块。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询