
给按钮留位置是很多自制项目的老大难:外壳要开孔、机械按键用久了会磨损,偶尔还接触不良。其实ESP32芯片里就自带一套电容触摸外设,GPIO上接一根杜邦线,露出一小截金属头,就能当按键用——手指靠近甚至不用按下去,读数就有反应。这篇把touchRead的用法、阈值判断,再到中断和深睡唤醒一次理顺,API细节逐条对照过乐鑫官方文档和现行core源码,老教程里容易踩的坑也标了出来。
先认引脚:T0-T9和GPIO的对应关系
原版ESP32一共有10个电容触摸引脚,编号T0到T9。这些引脚能感知任何带电荷物体的靠近,比如人的皮肤,所以最适合替代机械按键,还能在深度睡眠时当唤醒源。代码里写T0还是写GPIO数字都可以,两者等价。对照表如下,建议存一份:
| 触摸编号 | GPIO | 触摸编号 | GPIO |
|---|---|---|---|
| T0 | GPIO4 | T5 | GPIO12 |
| T1 | GPIO0 | T6 | GPIO14 |
| T2 | GPIO2 | T7 | GPIO27 |
| T3 | GPIO15 | T8 | GPIO33 |
| T4 | GPIO13 | T9 | GPIO32 |
两个提醒。第一,T1对应GPIO0,常见的30脚DevKit板上没有把这个脚引出来,要用T1得选36脚的版本。第二,老教程里常提醒"GPIO32和GPIO33在Arduino IDE里标反了",那是早期版本的已知问题,想用GPIO32得写T8、想用GPIO33得写T9;对照现行core的引脚定义,T8已经是GPIO33、T9是GPIO32,按数据手册的正常映射走了,新装环境的读者不用再绕这个弯。
另外,ESP32-S2和ESP32-S3的触摸外设是第二代,数量增加到14个,编号T1到T14,一一对应GPIO1到GPIO14。数值方向上和原版ESP32不一样,后面细说。贴一张30脚DevKit的引脚总图,触摸引脚在图上有高亮标注,找脚方便:

touchRead:一个函数读出触摸值
读触摸引脚只要一个函数:touchRead(),参数传GPIO号或T编号都行。它的工作原理是统计一段固定时间里的充放电周期数:手指碰到引脚,人体电容并入电路,这个计数就会变化。下面是乐鑫官方文档里的示例,读T2(GPIO2):
// ESP32 Touch Test
// Just test touch pin - Touch2 is T2 which is on GPIO 2.
#include "Arduino.h"
void setup() {
Serial.begin(115200);
delay(1000); // give me time to bring up serial monitor
Serial.println("ESP32 Touch Test");
}
void loop() {
Serial.println(touchRead(T2)); // get value using T2
delay(1000);
}烧录后打开串口监视器,每秒打印一个数。原版ESP32上有个关键特性:不碰的时候数值高,手指一碰数值明显下降——电容变大,充放电周期变少了。所以判断触摸的逻辑是"读数低于阈值就算碰到"。这和很多新手的直觉相反,老代码抄错了方向,现象就是"怎么碰都没反应"。
ESP32-S2/S3正好反过来:触摸时数值上升,判断条件是"高于阈值触发",而且返回值类型从16位变成32位,数值量级大得多。乐鑫文档里写明touchRead在ESP32上返回uint16_t、在S2/S3上返回uint32_t,两代芯片硬件测量电路不同。拿着为原版ESP32写的阈值直接搬到S3上,几乎必然失灵,这是新版教程评论区最常见的翻车点。
动手测试:从串口监视器到绘图器
自己试的话,准备一根杜邦线插到GPIO4上,金属端露在外面当触摸面。板子型号和COM口选对,串口监视器波特率设115200,就能看到数值刷屏。第一次测建议什么都别接,先看裸读数:数值在某个区间稳稳跳动,说明外设工作正常,再去折腾触摸面。线越长、触摸面越大,数值变化越明显,但也越容易受环境干扰——USB供电不稳、桌面材质、旁边有没有金属物,都会让基线漂移,所以阈值宁可取得保守一点。下面是读GPIO4的实测输出,手指碰上线的那几行数值肉眼可见地往下掉:

光看数字不够直观的话,换串口绘图器:关掉监视器,在工具菜单里打开Serial Plotter,触摸值会画成一条曲线,手指一碰就是一个向下的坑,一眼就能看出稳定区间:

想让触摸面更大更好碰,剪一小块铝箔包在杜邦线头上就行,接触面积大了数值变化也更明显。铝箔别贪大,两三厘米见方足够按键用;面积越大,基线漂移和误触发越难伺候,先用小块把逻辑跑通,再按项目外壳的形状慢慢加大。铝箔和线头之间建议用胶带压紧,虚接会让读数时不时跳一下,看起来就像有人乱碰:

阈值的确定没有玄学,就靠实测:记下不碰时的正常读数和碰住时的读数,在两者之间取一个值。一篇英文教程里给出的参考是,不碰时读数在70以上,按住铝箔会掉到10以下,于是阈值取20。你的板子、你的线长、你的环境都会影响这两个数,别直接照抄,用绘图器看一眼再定。
实战:触摸点亮LED
读数会看了,接一个输出就成完整项目。要用的东西:ESP32 DOIT DEVKIT V1板、5mm LED一颗、330欧电阻一颗、面包板和几根杜邦线。LED正极经电阻接GPIO16,负极接GND,接线照这张原理图:

完整代码如下,逻辑就是循环里读GPIO4,读数低于阈值20就点亮LED,否则熄灭。阈值那行按自己的实测改:
// set pin numbers
const int touchPin = 4;
const int ledPin = 16;
// change with your threshold value
const int threshold = 20;
// variable for storing the touch pin value
int touchValue;
void setup(){
Serial.begin(115200);
delay(1000); // give me time to bring up serial monitor
// initialize the LED pin as an output:
pinMode (ledPin, OUTPUT);
}
void loop(){
// read the state of the pushbutton value:
touchValue = touchRead(touchPin);
Serial.print(touchValue);
// check if the touchValue is below the threshold
// if it is, set ledPin to HIGH
if(touchValue < threshold){
// turn LED on
digitalWrite(ledPin, HIGH);
Serial.println(" - LED on");
}
else{
// turn LED off
digitalWrite(ledPin, LOW);
Serial.println(" - LED off");
}
delay(500);
}烧录后用手按住铝箔,LED亮;松开就灭。这套"读数加阈值"的判断再往前走一步就是无接触触发:不摸引脚,手指隔着薄塑料片甚至靠近到几毫米,读数就会往下掉,灵敏度用阈值微调。做非接触开关、湿度检测(贴片受潮读数变化)、或者面板底下藏一块铝箔当隐藏按键,都是同一个原理:

新手常遇到的三种异常也说一下。读数纹丝不动、恒为0或者恒为满值,多半是引脚号写错了,或者板上那个脚已经被别的外设占用,换一个T编号再试。数值乱跳、阈值形同虚设,先缩短杜邦线,再换一个供电稳定的USB口,最后才怀疑板子本身。LED不亮但读数正常,那是LED极性或者330欧电阻插错了行,触摸部分没问题就别乱动代码。排查顺序永远是从读数到接线,别一上来就重烧固件。
进阶:中断和深睡唤醒,新core的几个变化
轮询要一直占着loop,更优雅的做法是中断:touchAttachInterrupt(引脚, 回调函数, 阈值),读数越过阈值就调用回调。触发方向跟读数方向一致——原版ESP32是低于阈值触发,S2/S3是高于阈值触发。这里顺带澄清一个流传的说法:有文章提到新版core把touchAttachInterrupt弃用了。对照现行core(包括3.x各版本)的头文件esp32-hal-touch.h和官方迁移指南,这个函数没有被弃用,仍然是标准用法;3.x真正的变化是S2/S3读数方向相反、返回类型变宽,以及按芯片代际拆出了两个专属函数:touchInterruptSetThresholdDirection()只有原版ESP32能用,用来指定低于还是高于阈值触发(默认低于);touchInterruptGetLastStatus()只有S2/S3能用,用来在中断后查是哪个脚、是否仍在按住。3.x还支持阈值传0,让驱动用基准值自动判断,默认变化1.5%算触发,可以用touchSetDefaultThreshold()调百分比。官方中断示例:
/*
This is an example how to use Touch Intrrerupts
The bigger the threshold, the more sensible is the touch
*/
#include "Arduino.h"
int threshold = 0; // if 0 is used, benchmark value is used. Its by default 1,5% change, can be changed by touchSetDefaultThreshold(float percentage)
bool touch1detected = false;
bool touch2detected = false;
void gotTouch1() {
touch1detected = true;
}
void gotTouch2() {
touch2detected = true;
}
void setup() {
Serial.begin(115200);
delay(1000); // give me time to bring up serial monitor
//Optional: Set the threshold to 5% of the benchmark value. Only effective if threshold = 0.
touchSetDefaultThreshold(5);
Serial.println("ESP32 Touch Interrupt Test");
touchAttachInterrupt(T2, gotTouch1, threshold);
touchAttachInterrupt(T3, gotTouch2, threshold);
}
void loop() {
if (touch1detected) {
touch1detected = false;
Serial.println("Touch 1 detected");
}
if (touch2detected) {
touch2detected = false;
Serial.println("Touch 2 detected");
}
}最后是触摸引脚的招牌用法:深度睡眠唤醒。用touchSleepWakeUpEnable(引脚, 阈值)把某个触摸脚设成唤醒源,设备深睡后手指一碰就醒,做电池供电的低功耗项目很好用。想象一个贴在门边的门铃或者床头小夜灯,平时深睡几乎不耗电,手一伸过去就被唤醒、干活、再睡回去,不用留一个一直待机的机械按键。注意S2/S3只支持一个触摸唤醒脚,设多个会报错。
总结一下:原版ESP32有T0到T9共10个触摸引脚,touchRead读数、不碰值高碰了值降、低于阈值判触摸;S2/S3是T1到T14、方向相反、返回32位。阈值靠实测定,中断用touchAttachInterrupt,深睡唤醒用touchSleepWakeUpEnable。一根线加一块铝箔的成本,就能把项目里的机械按键换掉,值得在下一块板子上试一试。











阈值必须现场实测,抄别人的必翻车,我那块板子基线跟教程差了快一倍。
S3方向反过来这点太真实了,老代码直接搬过去,怎么碰都没反应,还以为板子坏了,后来打印数值才发现是越碰越大。
无接触那玩法可以啊,隔塑料片都能触发,门铃有思路了。
这玩意我去年就玩过,一根杜邦线加锡纸做的门铃,差点忘了T8和T9在老版core里标反的坑,那会儿对着GPIO32写了半天T9死活没反应,查了一晚上才知道是T8。文里把新旧版的映射都列出来了,现在新装环境的人少走弯路,属实功德一件。