基于ESP32的电容触摸接近报警器制作指南

📅 2026/8/26 3:40:49
基于ESP32的电容触摸接近报警器制作指南
如果你在作品旁边放一张写有“请勿触摸”的打印纸结果大概率是本来没人碰的东西突然变得人人都想碰。我上个月在公司摆了个半成品机械键盘原型本来是想测试新轴体手感结果每个路过的同事都要上来按两下、扣两下键帽。一开始我以为是手欠后来才意识到是那张“请勿触摸”的纸条激发了大家的好奇心。所以我把方向调过来了与其跟人性对着干不如做一个会自动“喊停”的实物装置。于是就有了这篇记录的主角——一台基于 ESP32 的“Dont Touch That!”触摸报警器。它能感知手指靠近甚至快要碰到物体立刻启动声光报警同时把事件推送到手机。整套东西硬成本不超过四十块适合展品防护、桌面值守、设备间防误碰或者纯粹想玩玩 ESP32 自带的触摸传感器。下面把整个项目的来龙去脉都倒出来包括需求怎么拆、方案怎么选、固件每个参数为什么这么定以及部署一周后踩到的所有干扰坑。1. 先想清楚这个装置到底在“防”什么1.1 一通分析后的真实需求做这个项目之前我先把“防触摸”这件事拆成了三个层面第一是检测要能感知人手的靠近或接触第二是提醒要在第一时间用声音、光线或推送把人怼回去第三是记录至少要留下时间和事件日志方便事后查看。这三个层面看起来很简单但每个都有讲究。检测层要回答“摸到才算还是快摸到就算”提醒层要回答“本地响一下够不够还是要有远程通知”记录层要回答“只记时间还是拍照留证”这几个问题直接决定了后面选传感器、选主控、写固件的方向。以我那个机械键盘原型为例真正让人心疼的不是被人按两下而是有人会把键帽拔下来看看。等发现的时候键帽早就不知道滚哪去了。所以我的核心需求其实是在手指碰到物体之前就用一个无法忽视的信号把动作打断。也就是说检测灵敏度必须覆盖“接近”这个阶段而不是等接触之后才反应。1.2 我明确不要的东西同时我也划掉了一些看起来很像、但实际上没那么必要的功能。比如我没有做远程锁死、震动报警、破坏性反击这些偏安防的方向。原因很简单这个装置的定位是行为干预不是抓贼。它要解决的是“有人手欠摸一下”的问题而不是“有人蓄意盗窃”的问题。如果目标是后者那该上的是保险柜、红外栅栏和摄像头而不是一个蜂鸣器。这个定位想清楚之后很多选择就变简单了检测方案不需要防破解不需要大范围扫描不需要区分身份提醒方案也不需要搞短信轰炸本地声光加一条手机推送就足够。2. 检测方案选了三天最后回到电容触摸2.1 五种方案横评我一开始列了五种检测方案分别是电容触摸、PIR 人体红外、红外对射/激光、微动开关/压力垫、振动传感器。它们的原理和适用场景差别很大我做了个对比表方案感应内容优点缺点适合场景电容触摸手指接近/接触无需接触即可感应可隔非金属外壳成本低无机械结构受温湿度、金属环境干扰需要标定桌面物品、门把手、展台PIR 人体红外人体移动探测范围大价格便宜对慢速移动不敏感分不清“伸手”还是“路过”走廊、房间、安保区域红外对射/激光遮挡光线响应快边界清晰需要精确对光飞虫/影子会误报长期会漂移展柜、门窗、通道微动开关/压力垫物理压力触发最直接几乎没有误报必须有实际压力轻摸不触发可能影响展品稳定性展品底座、报警垫振动传感器振动可以捕捉“已经碰到”的冲击轻触几乎不触发环境振动容易误报保险柜、展柜玻璃这五条路子我都实际搭过一版。PIR 的问题是它只看有没有人在动我人站在装置旁边不动它完全没反应而且同事路过时手没碰它也照样报警跟“触摸”这件事没关系。红外对射的误报来源非常多一次飞蛾飞过就触发一次后期维护得很崩溃。微动开关倒是准但要塞在物体底下物体不稳而且慢速按压的前几毫米完全没有信号。振动传感器就更不用说了轻轻一碰根本不振动。2.2 电容触摸为什么最适合“还没碰到就报警”电容触摸的原理说穿了并不复杂人体本身是一个导电体与大地之间存在约 100~200pF 的分布电容。当你把手靠近一块导电电极时手和电极之间会形成一个变化的电容等效到检测电路上就会改变电极的充放电时间。ESP32 内部集成了触摸传感外设它测量这个充放电过程的计数值数值稳定且随“手靠近”明显变化所以不需要额外的触摸芯片一个 GPIO 就能搞定。这个方案最打动我的地方是它天生就覆盖“接近”阶段。手离电极 2~3 厘米时读数已经开始变化贴上去之后变化量最大。也就是说它比“接触后再触发”的微动开关提前了至少一个量级的反应时间。对于“别碰”这个场景提前预警的价值非常大。2.3 一个容易误解的点有个点必须提醒很多人以为电容触摸就像开关一样碰一下就是 0/1。实际上它的原始输出是一个随距离连续变化的数值而且这个数值跟你板子、电极大小、周围环境都有关系。也就是说没有一劳永逸的固定阈值必须先标定、后判定。不少朋友做这类项目失败不是因为原理不对而是直接把别人工程里的阈值抄过来用结果到了自己的环境里完全失灵。后面我会讲清楚怎么做标定和动态基线。3. 硬件搭建ESP32、铜箔胶带和那只蜂鸣器3.1 元器件清单核心部件很少我列一下器件型号/规格数量备注主控ESP32 DevKitDOIT 版即可1自带 10 个电容触摸引脚触摸电极铜箔胶带宽 10mm 或 30mm1 卷剪成 30×30mm 方块蜂鸣器有源蜂鸣器模块 5V1无源蜂鸣器也能用后面说区别LED5mm 发光二极管 1kΩ 电阻1报警指示连接线杜邦线若干—电极到 ESP32 之间尽量短电源5V/2A 充电头或质量好点的充电宝1别用劣质充电头外壳3D 打印小盒或 ABS 接线盒1主要用来固定电极和保护主控如果你不想用 ESP32 自带的触摸引脚也可以用现成的 TTP223 触摸模块三根线接好就能输出数字信号。但它的灵敏度调节是靠电位器少了点通过代码调参的乐趣而且没法感知“接近”只有“接触”才输出。我做这个项目就是为了要接近检测所以直接用了 ESP32 的触摸引脚。3.2 电极布线与安装位置电极是整个系统的“感知器官”布法直接决定灵敏度。我的做法是剪一块 30×30mm 的铜箔胶带贴在被保护物体前面的亚克力支架上。如果你要保护的对象本身是导电体比如金属摆件、金属尺子那可以直接把导线焊到它上面整个金属物体都变成电极。但注意物体越大感应范围越大可能会出现“人站在一米外就触发”的情况这时要缩小有效感应面积或者只把一小段金属接进来。从 ESP32 的触摸引脚到电极的连接线我吃过大亏后面专门讲。这里先说结论这段线越短越好最好控制在 15 厘米以内。如果实在要拉长建议用屏蔽线屏蔽层接 GND否则开关电源、手部晃动都会带上杂讯。电极与周围金属之间要保持距离。我一开始把电极直接贴在金属机箱侧面结果读数变得极其诡异后来垫了 3mm 厚的亚克力隔离再把机箱接地问题才解决。3.3 供电与驱动电路ESP32 工作电压是 3.3V但开发板上有稳压电路用 USB 的 5V 供电就行。需要注意的是蜂鸣器。小型有源蜂鸣器工作电流大约 20~30mAESP32 单个 GPIO 的灌电流能力虽然标称能到 40mA但长时间直接驱动容易发热也不稳定。我建议用一个 NPN 三极管S8050或者 2N7000 MOSFET 来驱动蜂鸣器用 GPIO 控制基极蜂鸣器接在 5V 和集电极之间。这样既保护了主控声音也更响。有源蜂鸣器和无源蜂鸣器的区别有源蜂鸣器内部带振荡电路给高电平就响无源蜂鸣器需要外部给一个一定频率的方波才能发声。从固件角度看有源蜂鸣器一个digitalWrite就完事简单可靠。如果你手头只有无源蜂鸣器就用ledcWrite输出 2kHz 左右的 PWM 方波效果一样只是代码多几行。4. 固件逻辑原始读数不可信得先“加工”4.1 先串口画波形别急着写判定拿到硬件的第一步不是写逻辑而是把原始数据打出来看看。用 Arduino 开发环境写个最小程序每 20ms 读一次touchRead(kTouchPin)通过串口绘图器画出波形。我那块开发板在没有手靠近的时候读数稳定在 68 左右手指悬空 2cm 时掉到 52碰到电极时掉到 25 以下。这个波形让我一下就有了底触发阈值可以设在基线以下 35 左右这样既能捕捉到明显的接触又不会因为 2cm 悬空这种边缘情况乱叫。不同板子、不同电极尺寸的读数差异非常大。同一个程序在另一块 ESP32-S3 板子上静态基线是 3800和原来的 68 差了五十多倍。所以千万不要抄网上的阈值一定要自己标定。4.2 开机标定、滑动平均、滞回与防抖原始读数其实很毛糙直接做比较的话手指慢速靠近时读数是一点一点往下掉的会在阈值附近反复横跳。我用了三道加工第一道是一阶低通滤波也叫滑动平均。核心式子就一行filtered alpha * raw (1 - alpha) * filteredalpha 取 0.2 左右。它能把采样之间的随机抖动抹平让曲线变得顺滑。第二道是开机标定。上电后不碰电极连读 50 次取平均作为基线。这个基线是后面所有判断的参照物。第三道是滞回判定加连续计数防抖。滞回的意思和空调温控差不多没报警时只有读数低于“基线 - 35”才进入报警报警之后要等读数恢复到“基线 - 20”以内才解除。这两条线错开就能避免读数在阈值附近来回抖动造成的反复报警。连续计数则是要求连续 15 次采样约 300ms都满足条件才真正切换状态进一步过滤毛刺。4.3 完整代码以下是完整固件我在注释里写了每个参数的含义#include WiFi.h #include HTTPClient.h // ---- 引脚配置按你的开发板标注调整---- const int kTouchPin T0; // ESP32 触摸脚通常对应 GPIO4 const int kBuzzerPin 26; // 蜂鸣器驱动引脚 const int kLedPin 2; // 板载 LED // ---- 判定参数 ---- const int kCalibSamples 50; // 开机标定样本数 const float kFilterAlpha 0.2f; // 滑动平均系数 const int kTriggerDelta 35; // 下降到基线以下 35 视为“触摸” const int kReleaseDelta 20; // 恢复到基线以下 20 以内视为“松开” const int kStableCount 15; // 连续 15 次采样约 300ms稳定才切换 const int kSampleMs 20; // 采样间隔 // ---- WiFi ---- const char* kSsid your_ssid; const char* kPassword your_password; // ---- 推送地址BarkiPhoneServer酱微信或你自己的接口---- const char* kPushApi https://api.day.app/your_device_key/Don%27t%20Touch?body; int gBaseline 0; float gFiltered 0; bool gAlarm false; int gStableCounter 0; unsigned long gLastSample 0; void setup() { Serial.begin(115200); pinMode(kBuzzerPin, OUTPUT); pinMode(kLedPin, OUTPUT); digitalWrite(kLedPin, LOW); // 开机基线标定不碰电极的前提下取平均 long sum 0; for (int i 0; i kCalibSamples; i) { sum touchRead(kTouchPin); delay(10); } gBaseline sum / kCalibSamples; gFiltered gBaseline; Serial.printf(baseline %d\n, gBaseline); WiFi.mode(WIFI_STA); WiFi.begin(kSsid, kPassword); // 不阻塞等待网络报警功能不依赖网络 } void loop() { if (millis() - gLastSample kSampleMs) return; gLastSample millis(); int raw touchRead(kTouchPin); gFiltered kFilterAlpha * raw (1.0f - kFilterAlpha) * gFiltered; // 滞回判定没报警时用严格阈值报警后要恢复接近基线才解除 bool wantAlarm gAlarm ? (gFiltered gBaseline - kReleaseDelta) : (gFiltered gBaseline - kTriggerDelta); if (wantAlarm ! gAlarm) { gStableCounter; if (gStableCounter kStableCount) { gAlarm wantAlarm; gStableCounter 0; if (gAlarm) triggerAlarm(); else clearAlarm(); } } else { gStableCounter 0; } // 慢速基线跟踪只处理环境漂移系数极小不会把真实触摸“学”掉 if (!gAlarm gFiltered gBaseline - kReleaseDelta) { gBaseline (int)(0.9995f * gBaseline 0.0005f * gFiltered); } } void triggerAlarm() { Serial.println( TOUCHED); digitalWrite(kBuzzerPin, HIGH); digitalWrite(kLedPin, HIGH); pushNotification(有人碰了你的东西); } void clearAlarm() { Serial.println( CLEAR); digitalWrite(kBuzzerPin, LOW); digitalWrite(kLedPin, LOW); } void pushNotification(const char* msg) { if (WiFi.status() ! WL_CONNECTED) return; HTTPClient http; http.setConnectTimeout(2000); http.begin(String(kPushApi) msg); int code http.GET(); http.end(); Serial.printf(push code %d\n, code); }有一点必须说明中文和特殊字符出现在 URL 里时要先做 URL 编码上面这段代码里我没有把msg转码实际使用建议直接把接口地址写成已经编码好的形式或者用HTTPClient的addHeader走 POST把参数放到 Body 里。我自己的部署是把推送地址固定成编码后的完整 URL省得每次改。5. 报警与联动从“哔哔叫”到“手机弹通知”5.1 本地声光提醒本地提醒是最基础也最有效的一环。报警触发时蜂鸣器持续响LED 常亮。为了让它更有“被抓住”的感觉我在代码里加了闪烁效果报警期间 LED 每 200ms 翻转一次蜂鸣器响 500ms 停 200ms 再响这个节奏比一直长鸣更能引起注意。实测下来同事听到这个节奏的第一反应是缩手然后转头找声音来源直到把手拿开报警才停。如果你想把报警做得更“礼貌”一点可以把蜂鸣器换成语音模块比如 DFPlayer Mini 加一个 MicroSD 卡提前录一句“请勿触摸谢谢配合”。这个在展会上很实用比刺耳的蜂鸣器更容易让人接受展方也不会被投诉噪音扰民。5.2 手机推送与网页事件手机推送是我强烈推荐加上的功能因为很多时候装置响了但人不在现场你根本不知道发生了什么。我接的是 BarkiPhone 端免费应用它的接口就是一个简单的 HTTPS GET 请求推送延迟在一两秒内实测非常稳定。安卓也有类似的服务比如 Server酱逻辑一模一样。如果你家里已经跑了 Home Assistant更好的做法是走 MQTTESP32 报警时向某个 topic 发布一条消息HA 侧再决定是弹通知、拉灯还是录像。这样设备本身不用管推送逻辑耦合度低后面想换推送渠道直接改 HA 自动化就行不用重新烧固件。5.3 留证据日志和拍照只提醒不记录等于没做。我加了事件日志每次报警把millis()对应当前时间写入 Preferences 掉电存储区最多保留最近 50 条。这个用 ESP32 内置的Preferences库就能实现不需要外挂 SD 卡。日志字段只有时间和事件类型足够事后复盘“谁又过来摸了一次”。更进阶的玩法是拍照留证。ESP32-CAM 模块虽然主打摄像头但它也引出了 GPIO2 和 GPIO4 这两个支持触摸的引脚可以一块板子同时干两件事一边做触摸检测一边在报警时拍一张照片存到 SD 卡。我后来给展台版本换成了 ESP32-CAM效果很好唯一需要注意的是这个模块没有 USB 转串口烧录时要用 USB-TTL 工具而且烧录前要把 GPIO0 拉低进下载模式这个坑网上到处都有新手第一次大概率会卡住。6. 部署一周踩过的干扰坑和排查方法6.1 金属桌面上的“薛定谔触发”第一次部署在公司金属桌面上开机标定一切正常但只要我把手机放到桌面一角报警器就会莫名其妙响。排查了很久最后定位到是金属桌面本身带来的问题。原理是这样的桌面是导电体它和电极之间已经形成了一个固定的寄生电容。当我的手机接近桌面时手机内部的天线和金属件