输入输出与编程 / 编程实验发布 内容更新

不用机械按钮:用 ESP32-S3 做一个触摸开关

从绝缘面板后的电极开始,记录 ESP32-S3 触摸读数、冻结上电基线并设置回差;附 Arduino3.3.12新触摸驱动版本边界和完整测量表。

读完能做什么
编译带无触摸校准、ON/OFF回差和保持不连发的触摸灯,并完成自己的阈值测量记录。
开始之前
会查S3引脚与基本按钮事件;能够上传Arduino程序并保存115200日志。
所需器材
官方 ESP32-S3-DevKitC-1 v1.1 / ESP32-S3-WROOM-1-N8R8、铜箔电极与绝缘塑料面板、短导线、绝缘覆盖、Kingbright WP7113ID 红LED、330Ω±5%0.25W 电阻、USB 数据线、面包板
版本组合
开发板:DevKitC-1 v1.1 / WROOM-1-N8R8框架:Arduino-ESP32 3.3.12工具:Arduino CLI 1.5.1
适用范围

仅本文S3板与GPIO6触摸通道;无实板读数,提供完整测量方法,不外推到无Touch的C3/C6或经典ESP32比较方向。

这篇验证到了哪一步

资料核对已通过
docs/esp32-evidence/timer-repairs.md#p10;计时修复与固定来源核查,原器件/接线范围保留。版本:Arduino-ESP32 3.3.12验证日期:
构建 / 配置已通过
docs/esp32-evidence/timer-repairs-builds.json;本次计时修复实际编译与当前源/CPP/日志/产物SHA。历史原始记录另存,未替换。对象:esp32:esp32:esp32s3:FlashSize=8M,PSRAM=opi,FlashMode=qio,PartitionScheme=default_8MB,CDCOnBoot=default,USBMode=hwcdc版本:Arduino CLI 1.5.1 / core 3.3.12验证日期:
仿真已通过
docs/esp32-evidence/timer-repairs-host.json;相同头文件及实际.ino/YAML lambda的合成时间检查,不是芯片/器件/调度仿真或49天实跑。验证日期:
本站实板待验证
本站未实板验证;器件行为、物理方向/范围及速率须按本文记录

构建通过只说明指定环境中的代码或配置通过检查;实物功能、功耗和稳定性以对应的实板记录为准。

01 / 编程实验

触摸开关先需要两个能分开的读数范围

把铜箔藏在绝缘塑料面板后面,手指靠近时改变电容耦合,芯片据此测得不同计数。我们要做的不是“超过网上某个神奇数就开灯”,而是先测本装置的无触摸与触摸范围,再放两个分开的阈值,让手指保持时只触发一次。

本篇没有本站实物测量。你会得到可构建固件、未填结果的测量表和故障恢复步骤;电极大小、面板厚度、USB供电、机壳与人的接地条件都要在自己的安装状态下验证。

做到这里,你应该看到
你知道读数单位不是伏特;READY和编译通过也不表示物理触摸已经可靠。
02 / 编程实验

先固定电极、绝缘层与一盏已知LED

使用官方S3-DevKitC-1 v1.1 N8R8。电极先用约12×12mm铜箔,固定在约1mm绝缘塑料面板内侧,接点也盖绝缘层,仅触摸面板外侧。导线先尽量短并固定(约10cm内作为起步),这些是实验候选尺寸,不是厂家保证的灵敏度指标。

先拔USB改接线。电极只连GPIO6/J1-6,官方表明确该脚为TOUCH6;不要上拉到3.3V,更不能接5V或地。GPIO4串330Ω再接WP7113ID阳极,阴极到地。与P01同一具体LED;沿用F03的1kΩ也可更暗。厂家1.9V典型正向压降是在10mA条件,不能把它当成本文较小电流下的实测值。

板只经USB供电,不连接外部带电金属。改变电极/面板/线长前断电,再重新记录两类范围。铜箔、面板与人体的耦合关系会变,无法靠软件把所有物理环境归一成相同绝对数。

部件连接检查
绝缘面板后的铜箔GPIO6 / J1-6 / TOUCH6不接外部电源或地
LED回路GPIO4→330Ω→WP7113ID阳极;阴极→GND按厂家极性确认
供电与监视板载USB-to-UART BridgeUSB供电,Serial115200

参考:[1] DevKitC-1 v1.1 固定提交:GPIO6/TOUCH6与GPIO4[2] Kingbright WP7113ID Ver14A:极性与1.9V典型值条件

03 / 编程实验

3.3.12要读实际的touch-ng路径

在本次固定Arduino3.3.12中,S3预编译SDK的IDF版本为5.5.5。旧esp32-hal-touch.c/h有IDF<5.5版本guard,实际编进来的是esp32-hal-touch-ng.c/h。新头文件的touch_value_t为uint32_t,touchRead(pin)返回计数;S3触摸时读数增加,不能照搬经典ESP32“触摸后数值变小”的教程。

新实现会建立touch controller、配置默认filter、做内核benchmark,再读TOUCH_CHAN_DATA_TYPE_SMOOTH的第一个采样配置。下方变量名raw只是API读数名,并不是承诺未滤波的硬件原始值。本文没有调用旧touchSetCycles,也不把旧实现注释中的0.5ms当本版本实测时延。

本例另在应用层冻结无触摸基线B,不随手指长时间按住而不断追着改B;这不等于关闭内核滤波或benchmark。代码显式拒绝非S3 target;本篇既不宣称C3/C6有片上Touch,也不推广到P4/S2等未验组合。

做到这里,你应该看到
使用uint32_t保存读数;构建目标必须为esp32s3,来源是固定版本实际实现。

参考:[3] Arduino3.3.12 touch-ng.c:IDF版本guard、filter/benchmark与SMOOTH读取[4] Arduino3.3.12 touch-ng.h:uint32_t与API[5] Arduino3.3.12旧touch.c:IDF<5.5版本guard,不能据其描述本例实际驱动

04 / 编程实验

先松手校准,再用ON/OFF和连续样本

上电关灯,先等待2000ms,再按20ms节拍收集64个无触摸值,求均值B、最小lo、最大hi与跨度N=hi-lo。出现0或UINT32_MAX、N>B/5或阈值计算越界就拒绝校准。N>B/5是本例的起步筛查,不是厂家合格限值;读数非零也不代表电极一定连好。

初始ON=B+max(6N,B/4,100),OFF=B+max(3N,B/10,50)。比例和下限都是透明的实验起点,必须用自己的两类范围检查能否分离。若不触摸范围与触摸范围重叠,先处理物理布置,而不是宣称任意阈值都能解决。

未触摸状态下连续5次读数≥ON,产生DOWN并切换一次灯;连续保持高值不会反复切灯。触摸状态下连续5次≤OFF产生UP,才重新允许下一次DOWN。20ms节拍的5次资格判断约0.1秒,实际时序还受调度和touchRead影响,不是承诺响应指标。

代码 / 配置
// SPDX-License-Identifier: MIT
#pragma once
#include <stdint.h>
struct TouchCalibration {
  static constexpr unsigned SAMPLES=64;
  unsigned count=0;
  uint64_t sum=0;
  uint32_t lo=UINT32_MAX,hi=0,baseline=0,on=0,off=0;
  bool failed=false;
  void add(uint32_t raw) {
    if(count>=SAMPLES||failed)return;
    if(raw==0||raw==UINT32_MAX){failed=true;return;}
    sum+=raw;if(raw<lo)lo=raw;if(raw>hi)hi=raw;++count;
  }
  bool finish() {
    if(failed||count!=SAMPLES)return false;
    baseline=uint32_t(sum/SAMPLES);
    uint64_t noise=hi-lo;
    // Starting heuristics, not measured universal thresholds.
    if(noise>baseline/5){failed=true;return false;}
    uint64_t on_delta=baseline/4,off_delta=baseline/10;
    if(on_delta<noise*6)on_delta=noise*6;
    if(off_delta<noise*3)off_delta=noise*3;
    if(on_delta<100)on_delta=100;
    if(off_delta<50)off_delta=50;
    uint64_t a=uint64_t(baseline)+on_delta,b=uint64_t(baseline)+off_delta;
    if(a>=UINT32_MAX||b>=a){failed=true;return false;}
    on=uint32_t(a);off=uint32_t(b);return true;
  }
};
class TouchGate {
 public:
  enum Event{NONE,DOWN,UP,INVALID};
  TouchGate(uint32_t on,uint32_t off):on_(on),off_(off){}
  Event update(uint32_t raw){
    if(raw==0||raw==UINT32_MAX){active_=false;run_=0;return INVALID;}
    bool candidate=active_?(raw<=off_):(raw>=on_);
    if(!candidate){run_=0;return NONE;}
    if(++run_<5)return NONE;
    run_=0;active_=!active_;return active_?DOWN:UP;
  }
  bool active()const{return active_;}
 private:
  uint32_t on_,off_;
  bool active_=false;
  uint8_t run_=0;
};

// One-time startup phase; bounded 20ms sampling still uses uint32 subtraction.
class TouchSchedule {
 public:
  void reset(uint32_t now){started_=last_sample_=now;settled_=false;}
  bool sample_due(uint32_t now){
    if(!settled_){
      if(uint32_t(now-started_)<2000)return false;
      settled_=true;
    }
    if(uint32_t(now-last_sample_)<20)return false;
    last_sample_=now;return true;
  }
  bool settled()const{return settled_;}
 private:
  uint32_t started_=0,last_sample_=0;
  bool settled_=false;
};

参考:[3] Arduino3.3.12 touch-ng.c:IDF版本guard、filter/benchmark与SMOOTH读取[4] Arduino3.3.12 touch-ng.h:uint32_t与API

05 / 编程实验

完整固件与可单独修改的阈值配置

下载包保留.ino、TouchLogic.h和touch_config.h三个文件。USE_MANUAL_THRESHOLDS默认false;首次实验先记录自动阈值与两类范围。手动阈值开启后代码要求baseline<OFF<ON<UINT32_MAX;物理范围是否满足idle_max<OFF<ON<touch_min,仍要你看测量记录。

串口输入小写c会立即关灯并重启等待/校准,必须先移开手。如果上电时手已经压着电极,64个值可能很稳定但基线偏高,程序无法仅凭这批值知道“手已经在上面”;需要释放后c或RESET恢复。任何FAULT都保持关灯,修好后再校准。

TouchSchedule只在启动或c后等待一次2000ms,达到后把settled锁存到下一次重新校准;正常运行不会每圈再拿启动时刻重判。20ms采样和100ms日志仍用无符号短间隔差值,要求循环持续服务、相邻检查不跨完整2³²毫秒;CSV的32位ms会约49.7天回绕,不能当跨周期绝对时间。已完成校准后跨该回绕不会额外停采两秒。

代码 / 配置
// ---- touch_config.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <stdint.h>
// Keep false for the first recorded experiment. After measuring both ranges,
// choose baseline < OFF < ON with idle_max < OFF < ON < touch_min.
constexpr bool USE_MANUAL_THRESHOLDS=false;
constexpr uint32_t MANUAL_ON=0;  // Replace only with your own measured choice.
constexpr uint32_t MANUAL_OFF=0;

// ---- esp32-s3-touch-switch.ino ----
// SPDX-License-Identifier: MIT
#include <Arduino.h>
#include "TouchLogic.h"
#include "touch_config.h"
#if !defined(CONFIG_IDF_TARGET_ESP32S3)
#error "This experiment is locked to ESP32-S3; do not copy its touch direction to other chips."
#endif
constexpr int TOUCH_PIN=6, LED_PIN=4;
TouchCalibration calibration;
TouchGate gate(1,0);
bool ready=false,fault=false,led=false;
TouchSchedule schedule;
uint32_t lastLog=0;

void restartCalibration(){
  calibration=TouchCalibration{};gate=TouchGate(1,0);
  ready=false;fault=false;led=false;digitalWrite(LED_PIN,LOW);
  uint32_t now=millis();schedule.reset(now);lastLog=now;
  Serial.println("CAL release electrode; settle 2000 ms then 64 samples at 20 ms");
}
void setup(){
  pinMode(LED_PIN,OUTPUT);digitalWrite(LED_PIN,LOW);
  Serial.begin(115200);
  restartCalibration();
}
void loop(){
  for(unsigned n=0;n<8&&Serial.available()>0;++n){
    if(Serial.read()=='c')restartCalibration();
  }
  uint32_t now=millis();
  if(fault||!schedule.sample_due(now)){delay(1);return;}
  uint32_t raw=touchRead(TOUCH_PIN); // ESP32-S3: uint32_t raw increases with touch.
  if(!ready){
    calibration.add(raw);
    if(calibration.failed){fault=true;Serial.println("FAULT raw invalid; inspect wiring then release and send c");}
    else if(calibration.count==TouchCalibration::SAMPLES){
      if(!calibration.finish()){
        fault=true;Serial.println("FAULT noisy/overflow calibration; release electrode and send c");
      }else{
        if(USE_MANUAL_THRESHOLDS){
          if(MANUAL_ON<=MANUAL_OFF||MANUAL_OFF<=calibration.baseline||MANUAL_ON==UINT32_MAX){
            fault=true;Serial.println("FAULT manual thresholds require baseline < OFF < ON < UINT32_MAX");
          }else{calibration.on=MANUAL_ON;calibration.off=MANUAL_OFF;}
        }
        if(!fault){
          gate=TouchGate(calibration.on,calibration.off);ready=true;
          Serial.printf("READY baseline=%lu idle_min=%lu idle_max=%lu ON=%lu OFF=%lu\n",
            (unsigned long)calibration.baseline,(unsigned long)calibration.lo,
            (unsigned long)calibration.hi,(unsigned long)calibration.on,(unsigned long)calibration.off);
          Serial.println("ms,raw,baseline,on,off,touch_active,led_latch");
        }
      }
    }
  }else{
    auto event=gate.update(raw);
    if(event==TouchGate::INVALID){
      fault=true;led=false;digitalWrite(LED_PIN,LOW);
      Serial.println("FAULT raw invalid LED=0; release and send c");
    }else if(event==TouchGate::DOWN){
      led=!led;digitalWrite(LED_PIN,led?HIGH:LOW);
      Serial.printf("TOUCH_DOWN led_latch=%d\n",led);
    }else if(event==TouchGate::UP){Serial.println("TOUCH_UP");}
    if(uint32_t(now-lastLog)>=100){
      lastLog=now;
      Serial.printf("%lu,%lu,%lu,%lu,%lu,%d,%d\n",(unsigned long)now,(unsigned long)raw,
        (unsigned long)calibration.baseline,(unsigned long)calibration.on,
        (unsigned long)calibration.off,gate.active(),led);
    }
  }
  delay(1);
}
做到这里,你应该看到
首次保持默认false,不把占位0阈值当实测参数;保存READY行与CSV。

参考:[3] Arduino3.3.12 touch-ng.c:IDF版本guard、filter/benchmark与SMOOTH读取[4] Arduino3.3.12 touch-ng.h:uint32_t与API[6] Arduino3.3.12 millis:从IDF时钟返回32位unsigned long

06 / 编程实验

指定core构建与合成输入检查

ArduinoCLI1.5.1、core3.3.12,无额外库;IDE选择ESP32S3 Dev Module、8MB、OPI、QIO、8M with spiffs、CDC Disabled、Hardware CDC and JTAG,用板载桥接口监视115200。历史默认构建保留在programming-input-builds.json;本次计时修复的默认与手动分支实际构建另列timer-repairs-builds.json。手动构建仅将USE_MANUAL_THRESHOLDS=true及ON=1300/OFF=1100作为合成编译fixture,不是本站实测阈值,下载包仍默认false与0占位。

原5组fixture仍直接检查固件TouchLogic.h。新增production_loop.cpp直接include本页.ino,以合成touchRead/串口/32位时钟检查2000ms只等待一次、64个校准样本、完整周期回绕后继续采样、五次DOWN只切一次、c重新关灯并等待。1000/1300/1400均为合成输入,不测触摸驱动、电极或LED。

代码 / 配置
arduino-cli compile --fqbn 'esp32:esp32:esp32s3:FlashSize=8M,PSRAM=opi,FlashMode=qio,PartitionScheme=default_8MB,CDCOnBoot=default,USBMode=hwcdc' .
clang++ -std=c++17 -Wall -Wextra -Werror tests/touch_test.cpp -o /tmp/p10-test
/tmp/p10-test
clang++ -std=c++17 -DCONFIG_IDF_TARGET_ESP32S3 -Itests/stubs tests/production_loop.cpp -o /tmp/p10-loop
/tmp/p10-loop
做到这里,你应该看到
默认与合成手动构建Exit0;5组原模型和实际.ino生产循环fixture通过。实板触摸范围与电极效果仍pending。
07 / 编程实验

按两类范围、两种电极与错误启动逐项验收

保持手离开面板,RESET并保存READY。按设计节拍启动约3.3秒后可能完成应用校准,但首次touchRead还要建立通道,时间只是估计。若没有READY或已FAULT,停止把灯当成触摸结果。CSV每约100ms输出一次ms/raw/baseline/on/off/touch_active/led_latch;LED字段只是GPIO写入锁存,不是光学检测。

measurements.csv没有本站结果。每个实验都填日期、板、USB供电、铜箔尺寸、面板厚度、线长、启动条件、两类范围和最终阈值。不同电极或供电改变后要重新做一组,不能沿用上一组“通过”。

  1. 同一装配、无触摸启动。READY后保存至少30条无触摸CSV,取raw最小与最大。
  2. 手指触摸同一外侧区域保持至少30条CSV,取raw最小与最大;反复触摸/释放10次,检查DOWN/UP成对,保持手指时灯不连发。
  3. 只有idle_max<OFF<ON<touch_min留出明确间隔,才把USE_MANUAL_THRESHOLDS改true,填自己选出的MANUAL_OFF/ON,重新构建、上传并重做10次。
  4. 若范围重叠,先固定导线、电极和覆盖,调整面积或布局,再重新测量;不复制网上绝对阈值。
  5. 断电换第二种尺寸电极,例如20×20mm候选,保持其余条件;重新测两类范围与10次触发。
  6. 单独测试上电时手放在面板上,READY后释放,观察高基线导致的行为;随后释放状态发c,确认关灯并重新取得无触摸基线。
做到这里,你应该看到
每种电极至少有无触摸/触摸各30条、10次触发、启动条件与恢复记录;本站这些格仍待实测。
08 / 编程实验

读数、基线和电极故障各有不同恢复方式

FAULT raw invalid先查target、GPIO6是否被别的外设占用、接线及完整内核日志。新touchRead部分初始化错误返回0,读取调用失败也可能留下0;把0视为可疑值有助于停止误控制,但这不是完整断线检测。悬空触摸脚仍可能有正常计数,本例不能自动证明电极接好。

噪声校准失败先让手和其他物件离开、固定导线、核电源和装配,再c。始终无DOWN要比较触摸范围与ON,先测再改阈值。灯抖动要看CSV是否不断跨过ON/OFF,再改物理布局和回差。长时间保持不应连发;释放不重置灯,只重新武装下一次触摸。

上电手放电极导致错误基线是必须亲自做的恢复实验,不能以自动均值“总会校准正确”带过。恢复其他项目时拔USB、拆电极和LED再上传;所有状态只在RAM。下一步可回到机械按键的事件模型,理解触摸DOWN/UP如何替代稳定按下/释放;长期机壳防水和产品可靠性另需专门验证。

参考:[3] Arduino3.3.12 touch-ng.c:IDF版本guard、filter/benchmark与SMOOTH读取[4] Arduino3.3.12 touch-ng.h:uint32_t与API

BACK TO THE SOURCE

资料来源与版本说明

固定3.3.12实际走touch-ng(IDF5.5.5),读取SMOOTH数据;没有沿用被版本guard排除的旧touch.c“raw_data/0.5ms”实现。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。

  1. DevKitC-1 v1.1 固定提交:GPIO6/TOUCH6与GPIO4
  2. Kingbright WP7113ID Ver14A:极性与1.9V典型值条件
  3. Arduino3.3.12 touch-ng.c:IDF版本guard、filter/benchmark与SMOOTH读取
  4. Arduino3.3.12 touch-ng.h:uint32_t与API
  5. Arduino3.3.12旧touch.c:IDF<5.5版本guard,不能据其描述本例实际驱动
  6. Arduino3.3.12 millis:从IDF时钟返回32位unsigned long