// ===== ESP32 + PCNT (rise-only) стабильный медленный поворот + LongPress ===== // Модуль энкодера: "GND S1 S2 Key 5V" // Подключение: S1->GPIO32 (A / pulse), S2->GPIO33 (B / dir), Key->GPIO26, 5V->3V3, GND->GND #include #include #include "driver/pcnt.h" // ---- ВЫБОР ДИСПЛЕЯ ---- // U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE); // если SSD1306 U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE); // SH1106 по умолчанию // ---- Пины энкодера ---- #define ENC_A 32 // S1 #define ENC_B 33 // S2 #define ENC_SW 26 // кнопка // Направление (если «вверх/вниз» наоборот — поменяй на -1) #define ENC_DIR (+1) // Сколько событий PCNT = 1 «щёлчок». // Т.к. считаем ТОЛЬКО фронты A, для большинства модулей достаточно 1. #define DETENT_DIV 1 // Аппаратный фильтр PCNT (микросекунды). Реально ограничивается ≈12.8 мкс. #define FILTER_US 8 // не глушим реальные импульсы // ---- Кнопка ---- bool btnPressed=false, btnShort=false, btnLongRel=false; uint32_t btnDownAt=0; const uint32_t DB_MS=15, LONG_MS=700; void buttonUpdate() { static bool lastRaw=true; static uint32_t lastT=0; bool raw = digitalRead(ENC_SW); // PULLUP: 1=отпущена, 0=нажата uint32_t now=millis(); if (raw!=lastRaw){ lastRaw=raw; lastT=now; } if (now-lastT < DB_MS) return; if (!raw && !btnPressed){ btnPressed=true; btnShort=false; btnLongRel=false; btnDownAt=now; } else if (raw && btnPressed){ btnPressed=false; if(now-btnDownAt>=LONG_MS) btnLongRel=true; else btnShort=true; } } // ---- PCNT (только фронты A; направление берём с B) ---- void pcntInit() { pcnt_config_t cfg = {}; cfg.pulse_gpio_num = (gpio_num_t)ENC_A; // считаем импульсы по A cfg.ctrl_gpio_num = (gpio_num_t)ENC_B; // направление по уровню B cfg.unit = PCNT_UNIT_0; cfg.channel = PCNT_CHANNEL_0; // Считаем ТОЛЬКО фронты (rise). Спады игнорируем — это даёт стабильность. cfg.pos_mode = PCNT_COUNT_INC; // фронт A -> +1 cfg.neg_mode = PCNT_COUNT_DIS; // спад A -> игнор // B=LOW -> реверс, B=HIGH -> keep (так квадратура даёт правильное направление) cfg.lctrl_mode = PCNT_MODE_REVERSE; cfg.hctrl_mode = PCNT_MODE_KEEP; pcnt_unit_config(&cfg); // Аппаратный фильтр (0..1023 тиков APB @ 80 МГц ≈ до 12.8 мкс) uint16_t filt_ticks = (uint16_t)min(1023, (int)(FILTER_US * 80)); pcnt_set_filter_value(PCNT_UNIT_0, filt_ticks); pcnt_filter_enable(PCNT_UNIT_0); pcnt_counter_pause(PCNT_UNIT_0); pcnt_counter_clear(PCNT_UNIT_0); pcnt_counter_resume(PCNT_UNIT_0); } // аккумулируем события, ничего не теряем при медленном вращении long readDetents() { static long accum = 0; int16_t cnt = 0; pcnt_get_counter_value(PCNT_UNIT_0, &cnt); if (cnt != 0) pcnt_counter_clear(PCNT_UNIT_0); accum += (long)cnt * ENC_DIR; long steps = accum / DETENT_DIV; // DETENT_DIV=1 -> каждый фронт = 1 шаг меню if (steps != 0) accum -= steps * DETENT_DIV; return steps; } long value = 0; uint32_t uiT=0; void draw(const char* msg=nullptr){ u8g2.clearBuffer(); u8g2.setFont(u8g2_font_6x12_t_cyrillic); u8g2.setCursor(0,12); u8g2.print("PCNT rise-only (slow OK)"); u8g2.setCursor(0,28); u8g2.print("Значение: "); u8g2.print(value); u8g2.setCursor(0,42); u8g2.print("DETENT_DIV="); u8g2.print(DETENT_DIV); if (msg){ u8g2.setCursor(0,58); u8g2.print(msg); } u8g2.sendBuffer(); } void setup(){ Wire.begin(21,22); u8g2.begin(); u8g2.enableUTF8Print(); u8g2.setI2CAddress(0x3C<<1); pinMode(ENC_A, INPUT_PULLUP); pinMode(ENC_B, INPUT_PULLUP); pinMode(ENC_SW, INPUT_PULLUP); pcntInit(); draw("Крутите ручку"); } void loop(){ long d = readDetents(); if (d) { value += d; draw(); } buttonUpdate(); if (btnShort) { draw("Кнопка: короткое"); btnShort=false; } if (btnLongRel) { draw("Кнопка: длительное"); btnLongRel=false; } if (millis()-uiT>200 && d==0 && !btnShort && !btnLongRel){ uiT=millis(); draw(); } }