Выключатель ночника в шкафу: рука занята корзиной, кнопка за дверью неудобна — а прикосновение к металлической ручке шкафа удобно. За вечер собрал схему на ESP32, скопировал из туториала порог 40 — и свет в шкафу включался сам: база на моей плате была 52, а рука опускала её лишь до 38. Никакой магии: сырые значения touchRead зависят от платы, обвязки и окружения, и чужой туториал их за вас не подберёт. Так я понял главное про touch-пины: у каждой сборки свои числа, и порог с чужого скетча — лотерея. Что типично, на макетке в тот вечер всё работало безукоризненно: короткий провод, стабильное питание, рука ровно над площадкой — и подвело только на финальной сборке, когда провод вырос и огибал дверцу. С тех пор в моих скетчах нет ни одной константы порога — только числа, откалиброванные платой у себя дома.
Сырые значения touchRead, фантомные срабатывания и выбор пина уже разобраны в уроке про touch-пины с охотой на фантомы — пересказывать не буду. Здесь этаж выше: как откалибровать порог от базового значения при старте и зачем нужен гистерезис, что делают десять сантиметров провода до сенсорной площадки, и как собирается стилизованный умный выключатель — касание к площадке щёлкает реле, а тумблер-модуль оставляет ручной режим. Всё это делается без единой сторонней библиотеки: touchRead и touchAttachInterrupt входят в пакет плат ESP32 для Arduino IDE, и скетч получается короче, чем у любой кнопки с дребезгом.
Калибровка при старте: база вместо константы
Идея простая: при старте скетч сам замеряет состояние «нет руки рядом» и пляшет от этой базы. В setup делаю 10-12 замеров touchRead с паузой 20 мс и беру среднее — это база; порог включения ставлю на 60% базы, границу отпускания — на 80%. На моём DevKit T0 в покое давал 50-60, при касании проваливался до 10-20: порог в 60% отсекает фантомные проседания на десять процентов и ловит уверенное касание. Числа у вас будут другие, и это нормально — они зависят от платы, длины проводника и того, что стоит рядом с площадкой; важно само соотношение, а не абсолютные единицы.
Гистерезис — та же песня, что и с обычной кнопкой: без него значение у порога дрожит, и свет стрекочет. Включение на 60% базы, выключение на 80% — между ними мёртвая зона, где ничего не меняется. Механика антидребезга четырьмя способами лежит в уроке про способы антидребезга — там же таблица, какой способ когда брать; touch-сенсор добавляет к ним только калибровку. И журнал сырых значений в Serial при настройке экономит целый вечер: видно и базу, и провал от руки, и фантомы. Проценты, кстати, не прихоть: они переносятся между экземплярами плат, а абсолютные единицы — нет.
Процедура на живой плате выглядит так: снимаю журнал сырых значений секунд на десять, рука вне зоны; смотрю вилку значений и беру середину за базу; считаю два порога и вывожу их в Serial рядом с базой. Раз в сезон база уезжает — нагрев корпуса летом, влажная уборка, перестановка на другую стену, — потому в серьёзных проектах я пересчитываю её не только при старте, но и по команде из веб-интерфейса: тот же замер, что в setup, вызывается кнопкой «перекалибровать». Если пороги должны пережить перепрошивку, два числа базы и порога я кладу в Preferences — чем мы и занимались в прошлом уроке раздела.
| Сенсор | GPIO | Замечание из практики |
|---|---|---|
| T0 | GPIO4 | самый ходовой: без стрэппинга, площадку вешаю сюда в первую очередь |
| T4, T6, T7, T9 | GPIO13, 14, 27, 32 | свободные — годятся для второй и третьей площадки в том же проекте |
| T1, T2, T5, T3 | GPIO0, 2, 12, 15 | стрэппинговые: уровень при загрузке читает bootloader — для площадки не беру, детали в карте распиновки |
| T8 | GPIO33 | правый край DevKit: удобно вывести на дверцу корпуса |
Теперь грабля, на которой я потерял вечер: провод до сенсорной площадки — часть сенсора. Каждый его сантиметр добавляет ёмкость, и база уезжает вниз: двадцать сантиметров дюпона опустили мою базу с 58 до 41, полметра — до 30, а дельта от касания сжалась с тридцати единиц до двенадцати. Порог, откалиброванный на короткой макетке, на длинном проводе переставал ловить руку, а после переезда провода вдоль другой трассы ловил фантомы. Лечение одно — калибровать на финальной сборке: та же логика, что и с подбором порога обычной кнопки в уроке про порог кнопки и EEPROM. Отдельно замечу, что ёмкость добавляет не только провод, но и всё, что рядом с ним: фольгированная изоляция, соседний жгут, металлический корпус.
Правила, которые я вывел из того вечера: провод короче полуметра и подальше от силовых линий; соседний проводник — к земле, а не к соседнему GPIO; площадка — кусок фольги или монетка под наклейкой. Если провод нужен длиннее метра, дельта тает на глазах — тут честнее взять ёмкостный модуль TTP223 за копейки: у него порог подобран на заводе, и в скетче он выглядит как обычная кнопка на выходе.
Материал площадки влияет сильнее, чем кажется. Монетка под скотчем работала лучше куска фольги: больше площадь — больше дельта при том же касании. Влажная тряпка поверх площадки поднимала базу процентов на пятнадцать и касание ловилось хуже, а капля воды от мытья давала фантомное нажатие, пока не высыхала. Отсюда бытовой вывод: площадка под стеклом или пластиком толщиной до пары миллиметров живёт нормально, а вот под металлической накладкой сенсор читает уже не руку, а весь корпус — там его и не ставлю.
Умный выключатель: касание, тумблер, прерывание
Соберём выключатель: реле на GPIO25 щёлкает светильником, площадка T0 — это ручка шкафа, а тумблер-модуль на GPIO26 переключает режимы: щёлкнули — свет горит всегда, ручной цех; вернули — работает касание. Тумблер главнее сенсора сознательно: режим обслуживания не должен зависеть от того, чистые ли у сенсора руки. Фронт касания переворачивает состояние: рука коснулась — свет переключился, и пока палец на площадке, повторов нет. Отдельно показываю touchAttachInterrupt(): прерывание по касанию ставит volatile-флаг из ISR, а тем же сенсором плата просыпается из deep sleep — что ещё будит ESP32, разобрано в уроке про причины пробуждения.
Почему фронт, а не уровень: выключатель обязан щёлкать один раз за касание. Я слежу за переходом «выше порога — ниже порога» и переворачиваю свет только на границе, а пока палец лежит на площадке, ничего не происходит — ту же логику использует любой тумблер. Опрос с шагом 30 мс даёт тридцать с лишним точек на секунду — с запасом для короткого касания; если в loop() заведётся тяжёлая работа и шаг поплывёт, опрос сенсора уезжает в Ticker из урока про периодические задачи — там же про то, почему плавающий интервал врёт в графиках.
const int TOUCH = T0; // GPIO4 — сенсорная площадка (ручка шкафа)const int TOGGLE = 26; // тумблер-модуль: ручной режимconst int RELAY = 25; // реле светильника int baza = 0;int porogOn = 0;int porogOff = 0;bool svet = false;bool byloNizhe = false;volatile bool tinalo = false; // флаг из прерывания по касанию void kosnulsya() { // ISR: только флаг, никакой логики tinalo = true;} void setup() { Serial.begin(115200); pinMode(TOGGLE, INPUT_PULLUP); // тумблер на землю pinMode(RELAY, OUTPUT); long sum = 0; // калибровка при старте for (int i = 0; i < 12; i++) { sum += touchRead(TOUCH); delay(20); } baza = sum / 12; porogOn = baza * 6 / 10; // включение: 60% базы porogOff = baza * 8 / 10; // отпускание: 80% базы touchAttachInterrupt(TOUCH, kosnulsya, porogOn); Serial.print("baza="); Serial.print(baza); Serial.print(" on="); Serial.print(porogOn); Serial.print(" off="); Serial.println(porogOff);} void loop() { if (tinalo) { // прерывание поймало касание tinalo = false; Serial.println("[irq] касание поймало прерывание"); } bool ruchnoj = digitalRead(TOGGLE) == LOW; // тумблер главнее сенсора if (ruchnoj) { digitalWrite(RELAY, HIGH); return; } int raw = touchRead(TOUCH); bool nizhe = raw < porogOn; if (nizhe && !byloNizhe) { // фронт: рука коснулась только что svet = !svet; Serial.print("kasanie "); Serial.print(raw); Serial.print(" -> svet "); Serial.println(svet ? "vk" : "vyk"); } byloNizhe = nizhe; digitalWrite(RELAY, svet && raw < porogOff ? HIGH : LOW); // гистерезис delay(30); // 30 Гц опроса хватает с головой}Движок touchRead не умеет — и притворяться не будет: в sim-блоке ту же логику показывает пороговая кнопка на analogRead. Потенциометр A0 изображает сырое значение сенсора: при старте скетч снимает базу и вычисляет порог, движение ползунка вниз играет роль касания, LED — светильник. Вся механика — калибровка, фронт, гистерезис — в симе ровно та, что уйдёт на настоящую плату, где analogRead(A0) заменится на touchRead(T0).
Проверка руками: поднимите ползунок повыше и запустите скетч — в Serial придёт база и оба порога. Тяните ползунок вниз: ниже шестидесяти процентов базы пройдёт сообщение о касании, LED зажжётся; верните наверх — отпускание зарегистрируется только за восьмидесятипроцентной отметкой, мёртвая зона между порогами на глазах. Проведите ползунок на границе порога и подержите: LED не дёргается, потому что между порогами ничего не решается. Если запустить скетч с ползунком у дна, калибровка честно отработает от нуля и порог станет недостижимым — та же ловушка ждёт реальную плату, чей сенсор прикрыт при старте.
- 1. Почему порог 40 из чужого туториала может не сработать на вашей плате?
- 2. Что делает удлинение провода до сенсорной площадки?
- 3. touchAttachInterrupt(T0, fn, 40) вызывает fn, когда:
- 4. Какие сенсорные входы лучше оставить в покое для площадки?
- 5. Зачем гистерезис: включение на 60% базы, выключение на 80%?
- Порог не берут из чужого скетча: 10-12 замеров touchRead при старте дают базу; включение — 60% базы, отпускание — 80%.
- Провод до площадки — часть сенсора: двадцать сантиметров дюпона уводят базу вниз и сжимают дельту; калибровка — на финальной сборке.
- Гистерезис с мёртвой зоной между 60% и 80% убирает стрекоту у порога — тот же принцип, что у обычной кнопки.
- T1, T2, T5, T3 (GPIO 0, 2, 12, 15) — стрэппинговые: для площадок беру T0, T4, T6-T9.
- touchAttachInterrupt(pin, fn, порог) ставит volatile-флаг из ISR — тем же сенсором плата просыпается из deep sleep.
Частые вопросы по теме урока
- Почему порог 40 из чужого туториала может не сработать на вашей плате?
- Сырые значения зависят от платы, длины проводника и окружения: у одного DevKit база 52, у другого 70. Порог считают от собственной базы при старте — 60% и 80%. База touchRead уникальна для сборки: плата, провод, соседи по плате
- Что делает удлинение провода до сенсорной площадки?
- Провод — часть сенсора: двадцать сантиметров дюпона заметно опускают базу и сжимают дельту. Калибруют на финальной сборке, а для метров провода берут модуль TTP223. Добавляет ёмкость: база уезжает вниз, дельта от касания тает
- touchAttachInterrupt(T0, fn, 40) вызывает fn, когда:
- Прерывание срабатывает, когда значение сенсора пересекает порог вниз — касание просаживает показание. В ISR ставят только volatile-флаг, логику делают в loop(). touchRead(T0) уходит ниже 40
- Какие сенсорные входы лучше оставить в покое для площадки?
- GPIO 0, 2, 12 и 15 читаются bootloader при загрузке: площадка на них способна подвесить старт. Для касаний беру T0, T4, T6-T9. T1, T2, T5, T3 — их GPIO стрэппинговые
- Зачем гистерезис: включение на 60% базы, выключение на 80%?
- Мёртвая зона между двумя порогами не даёт состоянию перескакивать туда-сюда у границы. Тот же приём работает у любой пороговой кнопки. Без него значение у порога дрожит, и свет стрекочет