
开店的朋友应该都遇到过这种情况周末客流高峰店员一边收银一边还要用眼睛去数进店人数数着数着就被打断了最后只能靠感觉报个数。疫情期间这个问题被放到了台面上——不少门店被要求限流进店人数必须精确管控超了就要在外面排队。那时候我就用 ESP32 MCU 做了一个双红外传感器的客流计数器装在门口自动判断“进”和“出”实时显示当前店内人数超阈值就闪灯提示。今天把这个项目的完整方案、硬件选型、固件实现和踩坑记录整理出来给有类似需求的朋友一份可以直接参考的作业。先说明一下这个项目并不是只服务于疫情场景。客流数据对于商场、餐饮、工作室、展览馆等空间管理都有长期价值疫情只是它被人注意到的契机。除此之外它也是一个非常典型的 ESP32 实战项目覆盖了 GPIO 中断、I2C 外设、Flash 存储、WiFi 通信、低功耗设计等多个嵌入式开发关键点用来练手和学习 MCU 开发非常合适。无论你是店主想自己搞一套客流统计还是嵌入式学习者想找一个难度适中、可平滑扩展的小项目这篇博文都能帮到你。1. 项目背景与整体设计思路1.1 疫情倒逼出来的“人口计数”需求2020 年前后“店内同时最多容纳 XX 人”成了很多实体门店必须面对的新要求。传统靠人工数数的方式在高客流时段非常不可靠忘数、漏数、重复数是常态。而市面上成品客流统计设备价格不低贵的几千上万小店很难承受。另一个选择是用摄像头加视觉识别但涉及隐私和复杂的算法部署对小店面来说明显是杀鸡用牛刀。我就是在这种背景下开始琢磨能不能用最便宜的方式在门口做一个“只记录人数进出”的装置需求其实很明确一是能区分进店和出店二是能实时得到当前店内人数三是数据能留存能从总数退出历史客流。顺着这个需求细化方案其实有很多但核心无非是传感器加 MCU 的组合。MCU 选择很关键它决定了后续能不能联网、能不能升级、开发麻不麻烦。1.2 为什么是 ESP32 而不是其他 MCU我在早期原型阶段试过 51 单片机和 STM32。51 单片机做简单计数没有问题但它没有内置 WiFi想要远程查看数据就得额外加网卡开发复杂度直接翻倍。STM32 性能强但作为一个只需要数人的轻量项目配置真外设的时间和资源又多了一些。这时候再看 ESP32它的优势非常明显成本低一块 WROOM-32 芯片模组版价格大概在 15 到 30 元之间和普通单片机模组差距不大。内置 WiFi 和蓝牙不需要外挂通信芯片数据可以直接上报到服务器也能用手机 App 查看。外设丰富支持多路 ADC、I2C、SPI、UART 和 GPIO 中断足够应付各种传感器接口。Flash 空间大常见模块自带 4MB 到 16MB Flash写日志、存配置、做网页服务器都够用。生态成熟Arduino 框架下有大量现成库ESP-IDF 原生框架资料也在社区里沉淀了很多。选 ESP32 不是因为它最强而是因为它恰好卡在这个项目的成本、功能和开发效率平衡点上。如果后续要接摄像头做视觉客流统计ESP32-S3 也有相应的算力支撑如果只是这种简单对射或红外测距方案入门级的 ESP32 就已经绰绰有余不需要过度设计。1.3 整体方案路线的确定我的最终方案是这样的在门口通道两侧分别安装两个传感器传感器 A 靠近门外传感器 B 靠近门内两个传感器沿进门方向排开间距保持在 40 到 60 厘米。当有人从外往里走时会先触发 A 再触发 BMCU 判定为“进店”反向先触发 B 再触发 A判定为“出店”。ESP32 实时维护一个“店内人数”变量进店加一出店减一数量下限设置为 0。这个方案的核心是“双传感器顺序判定”相比于单传感器只能统计“总经过次数”它能得出“当前在场人数”后者才是限流场景中真正关心的数据。有了这个顺序判定逻辑还可以进一步扩展出瞬间客流、平均停留时间、高峰时段分布等维度。数据层用 Flash 做定时快照防止断电丢计数网络层周期性通过 WiFi 把数据推送到自建网页或云接口。整个项目从硬件到固件都比较轻量一个人用周末两三天就可以搞定。2. 硬件选型与计数原理2.1 传感器方案对比哪种才靠谱计数项目里传感器是灵魂。我对比过几类常见的方案各有各的适用场景但用在门禁计数上差距很大。第一类是被动红外传感器PIR像 HC-SR501。它便宜且省电但它输出的只是“有没有人在移动”无法区分方向延迟还比较严重人走过去了信号才稳定下来。用在“探测有人”的场景可以用在“计数进出”就完全不行。第二类是超声波测距传感器HC-SR04。它可以测量人与传感器的距离通过距离变化判断人员通过。但它探测波束较宽容易把旁边的门框、墙壁反射混进来而且检测速度比较慢快速通过时容易出现丢帧。第三类是主动红外对射传感器比如 E18-D80NK。它自带一对红外发射和接收管有人遮挡时输出电平变化响应速度快而且不易受可见光干扰。我把它加装固定挡光板后能稳定输出遮挡信号。缺点是模块体积稍大要装在门框两侧。不过做计数这种“遮挡/不遮挡”的二元判断它非常靠谱。第四类是激光测距传感器比如 VL53L0XToF 方案。它的核心原理是发射光子并计算反射回来的时间差能直接得到障碍物距离。优点是可以不装光墙用一个传感器放在门侧就能感知“有人通过”通过阈值设置来触发。缺点是它本质上是测距不是“计数专用”需要对连续距离值做滤波和状态切换开发量稍大。我在这个项目里最终用了 E18-D80NK两个模块一左一右分别装在外侧和内侧依靠遮挡顺序来判断进出方向。如果你手头已经有 VL53L0X也可以做后面代码部分我会给出兼容思路。单纯说“省事和稳定性”E18-D80NK 这种红外对射方案是我最推荐的选择。2.2 为什么选用双传感器判定方向单传感器能做什么它只能告诉你“有人经过了一次”但不知道这个人是从外面进来还是从里面出去。如果场地只有一个出入口且每天开业前店内人数是已知的那么“总经过次数 - 当前循环外的参照”可以算出店内人数但一旦有人站在门口徘徊、探个脑袋又缩回去单传感器就会多计一次导致数据失真。双传感器的顺序判定能很好地解决这一点。它的逻辑是A 先触发、B 后触发判定为“进”B 先触发、A 后触发判定为“出”。这样即便有人在门口犹豫只要他没有完整走完“先进 A 再进 B”或“先进 B 再进 A”的过程计数就不会改变。这个方案不是 100% 完美但对绝大多数单出入口场景来说已经非常接近商用人流量统计设备的准确性了。还需要注意一点两个传感器之间要有足够的距离通常不小于 40 厘米否则一个人同时遮挡两个传感器顺序就分不出来了。距离太远也不行如果大于 1 米一个人正常走路的步幅可能让他先被 A 检测、迈一步后被 B 检测这个没问题但如果人跑得快A 和 B 的触发间隔太短MCU 可能来不及处理。所以安装间距建议在 50 厘米左右同时代码里要做足够的去抖和超时判断。2.3 硬件清单和接线参考先列出我这次用到的物料清单物料型号/规格数量参考价格MCU 开发板ESP32 DevKitC V4WROOM-321约 20 元红外对射传感器E18-D80NK3.3V-5V 供电2约 8 元/个降压模块AMS1117-3.3如需 5V 转 3.3V1约 2 元蜂鸣器模块有源蜂鸣器1约 3 元OLED 显示屏SSD1306 0.96 寸 I2C1约 12 元电源5V/1A USB 充电器或充电宝1自备外壳3D 打印或塑料接线盒1约 10 元杜邦线若干公对公、公对母若干约 5 元接线方面E18-D80NK 有三根线棕色接电源正极、蓝色接电源负极、黑色接信号输出。它的信号输出是开集电极形式MCU 读取时需要接一个上拉电阻。ESP32 内部可以开启 GPIO 上拉所以可以省掉外部电阻在代码初始化时设置pinMode(pin, INPUT_PULLUP)即可。我给一个标准的接线参考E18-D80NK 外侧传感器负责先检测进店方向的信号线接 GPIO 18内侧传感器信号线接 GPIO 19两个传感器电源正极接 5V负极共地OLED 显示屏的 SDA 接 GPIO 21SCL 接 GPIO 22VCC 接 3.3V蜂鸣器接 GPIO 23另一端接 GND如果你用的是 VL53L0X接线是 VCC 接 3.3VGND 接 GNDSDA 接 GPIO 21SCL 接 GPIO 22。两个 VL53L0X 需要把其中一个的 XSHUT 引脚接到独立 GPIO比如 GPIO 17上电后先关掉一个再修改它的 I2C 地址避免地址冲突。这部分代码稍微复杂一些但整体思路并不难。2.4 设备安装与探测区域设计硬件装起来不难但安装位置往往是决定计数准确率的最大因素。我这里分享几个实测后得出的关键经验第一传感器高度要选对人体的“腰部偏下”位置。如果装得太高比如 1.5 米两个人并排走会同时挡住这样就会被算成一个人如果装得太低比如 0.3 米小孩和宠物容易触发误判。我实测 0.8 米到 1 米高度比较合适正常成年人走路时腿部或腰部会经过探测区域而并排概率会降低。第二要确保两个传感器的探测区域在空间上是“线形排列”而不是“扇形散射”。E18-D80NK 自带红外光束探测区域本身是收窄的安装时让两束光尽量平行于门的方向这样做可以减少从侧边经过的人触发误判。第三如果门口有玻璃门或金属门框要留意红外反射。强反射可能导致传感器在没人经过时也偶尔触发我遇到过几次后来在传感器前方加装了小挡板让光束的发射与接收路径固定问题就消失了。我还见过有人采用“单侧安装双传感器”的方案把两个 VL53L0X 都装在同一侧墙壁上一个偏门外一个偏门内通过测距变化做方向判断。这种方法优点是安装方便不需要门两侧布线但算法上要求更精细对干扰也更敏感。新手建议先做双对射方案稳定后再折腾测距方案。3. 固件开发与核心逻辑3.1 开发环境Arduino IDE 与 ESP32 扩展包很多人第一次碰 ESP32 时卡在开发环境搭建上。这里我推荐用 Arduino IDE不是因为它最强而是因为它对新手最友好、社区资料最多。Arduino IDE 从 2.x 版本开始体验已经很好安装 ESP32 支持包也很简单。有两种方式安装 ESP32 扩展包在线安装和离线安装。在线安装需要网络顺畅在“文件 - 首选项 - 附加开发板管理器网址”里填入 Espressif 官方 JSON 地址之后在开发板管理器里搜索 ESP32 安装即可。国内网络环境不太稳定时经常下载失败所以社区里流传的离线安装包就很实用。离线包一般是一个压缩文件解压后覆盖到 Arduino 的硬件目录比如~/Documents/Arduino/hardware/espressif然后重启 IDE 就能在开发板列表里看到 ESP32 Dev Module。开发板选好后还需要设置几个关键参数Flash Size 选 4MB或根据模块实际容量选Partition Scheme 选 “Default 4MB with spiffs”这样可以留出 1.5MB 左右的 SPIFFS 空间用来存数据和网页文件。Upload Speed 我选 921600烧录速度快如果总是失败再降回 115200。如果你下载的 ESP32 包版本过老比如 2.0.x部分新库可能不兼容建议直接用 2.0.17 或更新的版本我目前用 2.0.17 跑得很稳。3.2 计数状态机的核心实现计数逻辑是整个固件的灵魂我把它做成一个简单的状态机。先定义三种状态空闲IDLE、等待第二个传感器触发WAIT_ENTER / WAIT_EXIT。变量维护上用peopleInside表示当前店内人数用totalIn和totalOut分别记录累计进店和出店次数。主循环里我采用“非阻塞扫描”方式不依赖延时函数避免阻塞其他任务。每次扫描两个传感器引脚状态记录触发时间和顺序。当传感器 A 从高电平变为低电平对射被遮挡时记录 A 触发如果在设定时间窗口内 B 又触发则判定为“进店一次”。反之亦然。如果超时只有 A 触发B 没触发就判定为无效事件计数器不变。下面是我实际使用的核心代码片段用的 Arduino C 语法#define SENSOR_A_PIN 18 // 外侧 #define SENSOR_B_PIN 19 // 内侧 bool aTriggered false; bool bTriggered false; unsigned long aTriggerTime 0; unsigned long bTriggerTime 0; const unsigned long EVENT_TIMEOUT 500; // 500ms 内完成才算一次有效通过 const unsigned long DEBOUNCE_TIME 50; int peopleInside 0; int totalIn 0; int totalOut 0; void IRAM_ATTR sensorA_ISR() { static unsigned long lastTime 0; unsigned long now millis(); if (now - lastTime DEBOUNCE_TIME) return; lastTime now; if (digitalRead(SENSOR_A_PIN) LOW) { // 下降沿触发 aTriggered true; aTriggerTime now; } } void IRAM_ATTR sensorB_ISR() { static unsigned long lastTime 0; unsigned long now millis(); if (now - lastTime DEBOUNCE_TIME) return; lastTime now; if (digitalRead(SENSOR_B_PIN) LOW) { bTriggered true; bTriggerTime now; } } void updateCounter() { if (aTriggered bTriggered) { if (bTriggerTime aTriggerTime) { // A 先触发B 后触发 进店 peopleInside; totalIn; } else { // B 先触发A 后触发 出店 if (peopleInside 0) peopleInside--; totalOut; } aTriggered false; bTriggered false; } else if (aTriggered millis() - aTriggerTime EVENT_TIMEOUT) { aTriggered false; } else if (bTriggered millis() - bTriggerTime EVENT_TIMEOUT) { bTriggered false; } }这里有一个细节我要特别强调出店时peopleInside不能减成负数。如果系统刚启动还没有历史数据而有人直接出店应该将计数下限设为 0避免负数显示影响后续判断。这在商场每天开门营业时的场景尤其重要因为系统开机时店内可能是空的也可能已经有早到的员工这时需要一个手动预设功能由管理员在 OLED 上或者网页里设置初始人数。我的做法是加了一个“开业预设人数”参数存在 Flash 里每天早上开机时不从 0 开始而是从预设值开始。3.3 数据的保存与查看Flash 存储加网页仪表盘计数结果如果只在内存里一旦断电就全丢了。所以我在代码里引入了 Preferences 库它基于 NVS 存储在 ESP32 的 Flash 里。每次人数发生变化把peopleInside、totalIn、totalOut写入 NVS。NVS 有写入寿命限制虽然不必太担心但也不要每次中断都立刻写我是每 10 秒写一次快照再加上掉电时主动写一次。#include Preferences.h Preferences prefs; void saveData() { prefs.begin(counter, false); prefs.putInt(inside, peopleInside); prefs.putInt(totalIn, totalIn); prefs.putInt(totalOut, totalOut); prefs.end(); } void loadData() { prefs.begin(counter, true); peopleInside prefs.getInt(inside, 0); totalIn prefs.getInt(totalIn, 0); totalOut prefs.getInt(totalOut, 0); prefs.end(); }除了 OLED 本地显示我还给这个设备加了一个简单的网页仪表盘。ESP32 启动 WiFi 后同时开启一个轻量 HTTP Server。PC 或手机在同一个局域网内访问它的 IP就能看到当前人数、累计进出数据和历史曲线。网页文件如果直接放在代码里会显得很长我选择了放在 SPIFFS 文件系统中用插件上传 HTML、CSS、JS 和 Chart.js 库文件。SPIFFS 也是热词里常出现的给不熟悉的朋友解释一句它相当于 ESP32 Flash 里划分出的一块小文件系统可以把网页、配置、日志当文件来读写比在代码里用字符串拼页面要灵活得多。如果设备需要远程联网还可以用 ESP32 的 WiFi 功能每 30 秒向你的服务器发送一次 JSON 数据POST 请求格式类似{inside:12,total_in:345,total_out:333}。后端用一个简单的 Node.js 或 Python Flask 服务接收并写入数据库。我自己现在用的是 MQTT 方式把计数消息发到本地 broker再由一个数据采集服务订阅入库这样的解耦结构更适合以后扩展多个门店设备。3.4 省电与长期运行考虑这个项目如果要长时间在门店里跑功耗和稳定性都得考虑。ESP32 正常运行时 WiFi 打开电流大概在 80 到 240mA 之间波动较大。对常年插电的门店来说没有问题但如果要考虑电池供电就必须加入 Deep Sleep 模式。我的做法是正常工作时开 WiFi只在每次人数变化或每 10 秒上报一次数据当检测到“一段时间内没有触发事件”时比如打烊后ESP32 进入 Deep Sleep。Deep Sleep 下 WiFi 关闭电流可以降到 10uA 级别。为了唤醒我给两个传感器引脚配置了 EXT1 唤醒源当有人触发传感器时GPIO 电平变化会把 ESP32 从睡梦中唤醒。这种方案适合户外或临时场所使用一块 2000mAh 锂电池理论上可以坚持数周甚至更久。不过要注意Deep Sleep 模式下 RTC 内存仍然保持供电但普通变量会丢失。我通常把计数值和关键状态变量放在RTC_DATA_ATTR段里这样唤醒后还能保留上次的数据。每次入 Sleep 前再调一次saveData()。实测下来这个机制比依赖 Flash 频繁写入要稳妥得多。4. 现场调试与常见坑实录4.1 误检与双人并行问题我在实验室测试时一切正常一装上真实门店就出问题。最常见的坑就是两个人并排进出。两个传感器间隔只有 50 厘米时并排的两个人可能同时遮挡 A 和 B导致顺序判定失效。我当时的处理办法是加长传感器间距到 80 厘米并在代码里增加“只有当两个触发时间差大于 150 毫秒时才计为有效通过”的条件。如果时间差太短说明可能是并排异常或者是非人类物体比如购物车快速通过这时候宁可漏计也不误计因为误计带来的异常数据比漏计更难处理。第二个高频问题是“有人在门口停留”。人站在两个传感器中间不动或者来回踮脚会造成 A 和 B 反复触发。我之后在代码里加入了“同一个方向完成一次完整触发后必须等待 800ms 冷却时间才允许下一次计数”这个时间窗口可以过滤掉很多站立抖动的误计。这里的 800ms 不是拍脑袋定的我记录过人在门口正常停留时传感器触发波形的持续时间发现大多数抖动间隔在 200ms 到 600ms 之间所以取 800ms 才能有效过滤。4.2 计数重启后不准确很多朋友做完设备后会遇到这个问题设备重启后店内人数变成 0 或者恢复成预设值但实际店里可能已经有顾客了。解决思路是让系统记录“最近一次断电在几点几分”然后结合最后一次计数时间来判断数据有效性。这个操作在 ESP32 上很容易做用time(nullptr)获取时间戳存到 NVS 里。下次开机时如果断电时间超过营业时间窗口就把人数重置为 0如果断电时间很短比如意外重启恢复之前快照。如果你对实时性要求不高还有一个“笨办法”每天打烊后自动重置所有计数第二天开业时重新从 0 累计。这种方案更适合不需要跨天累计数据的门店。4.3 烧录失败的排查方法ESP32 烧录失败是新手最容易遇到的第一道坎。常见现象是 Arduino IDE 提示A fatal error occurred: Failed to connect to ESP32: Timed out waiting for packet header。这个问题的原因多半是芯片没有进入下载模式。解决方法是按住开发板上的 BOOT / IO0 按键点击上传在提示Connecting...时松开 BOOT就能正常烧录。还有一种情况是串口驱动没装好。老一些的开发板用 CH340 芯片新一些的用 CP2102。在设备管理器里如果看不到 COM 口或者看到带感叹号的未知设备就需要去官网装对应驱动。另外注意某些劣质 USB 线只能充电不能传数据会表现为“串口能识别但一烧录就失败”这个我踩过坑换根带数据功能的短线基本解决。如果你用的是 ESP32-S3需要注意部分开发板没有自动下载电路需要在烧录前手动按 BOOT。我偶尔也会直接用 ESP32 的在线烧录工具来测试 bootloader比较方便。4.4 红外传感器受环境干扰红外对射方案在强烈阳光下容易误触发。因为太阳光里面包含红外波段直射到接收管会导致传感器输出抖动。我在门店门口实测时下午阳光从门口斜射进来计数器就莫名跳动。解决办法是给传感器加装遮光套并选择探测距离可调的型号把检测距离缩短到只覆盖门口通道宽度。E18-D80NK 上有一个电位器可以调距离我用螺丝刀把检测距离从 80 厘米调到 50 厘米左右误触发现象明显改善。如果你用 VL53L0X环境光干扰会少一些但镜面反射仍然可能导致问题特别是在有地砖反光的环境里需要仔细调节阈值。5. 从客流计数器到空间管理平台5.1 从单一门店到多设备联网单个计数器的数据量其实是有限的但如果你有多个门店或者一个商场有多层多个入口把每一处设备都独立部署信息就非常分散了。这时候 ESP32 的联网能力就成了关键优势。每个计数器通过 WiFi 把数据上报到同一个 MQTT Broker后端订阅所有设备话题再按设备 ID 分开存储。前端仪表盘可以按楼层、区域、门店维度展示实时人数。这种架构在数据层面并不难难的是设备的唯一标识和人店关系配置。我自己的做法是给每台设备设置一个四字节的短 ID比如store_01在网页配置页面填写门店名称。上报到服务器时带上这个 ID。如果你还想做更复杂的“店铺总人数上限联动”可以让后端根据实时人数向进店闸机或门锁发送控制指令。ESP32 这边只需要做好“服从指令”的角色不涉及复杂的业务逻辑维护起来很轻松。5.2 数据维度扩展停留时间与高峰预警双传感器方案还有一个衍生能力记录一次“进店触发”和“出店触发”之间的时间差再加上人的身份ID如果需要就能统计顾客的平均停留时间。这个数据对门店运营非常有用比如发现顾客数量多但停留时间短说明动线和陈列可能存在问题。平均停留时间不用特别精确用进出时间差减去平均值误差即可对于大多数决策场景来说已经足够。基于停留时间还能做超时预警比如有人进店超过 30 分钟没出来系统可以提示店员关注在特殊时期这种“异常长时间停留”也曾经被用作辅助排查手段。现在回过头看这些能力都不是疫情专属而是空间管理的通用需求。5.3 后续升级的方向如果你想把项目往更专业的客流分析方向推进还有几个方向可以升级。一是换成 ESP32-S3 加摄像头模组用轻量级人脸或人体检测模型做视觉计数可以做到单摄像头多方向识别准确率更高部署也简单一些代价是硬件成本和代码复杂度会上升。二是加入 LoRa 通信模块在无 WiFi 的户外场景用低功耗广域网汇总多个计数点数据适合临时展会或景区。三是把数据接入到开源数据可视化工具例如 Grafana 或 Node-RED配上图表、报警规则直接把客流数据变成管理面板。这些扩展都基于同一个底座ESP32 的 GPIO 中断、Flash 存储、WiFi 通信、低功耗管理这四个能力。把它们吃透了不管是做人数计数还是做别的物联网设备都能少走很多弯路。最后分享一个我个人的使用心得这个项目做完以后我最大的体会是嵌入式开发里“传感器数据处理”远比“接线上电”重要。相同的硬件有的人做出来计数准确率 95%有的人做出来 60%差别全在去抖、状态机、超时判断这些细节里。建议你跟着代码实现一遍然后拿着示波器或者串口打印去观察真实触发的波形看看每个参数为什么这么设置。动手实验一轮之后你对 ESP32 MCU 的理解会上升一个层次再看其他传感器项目也会觉得思路清晰很多。