Добавить firmware/encoder.ino

This commit is contained in:
elios 2025-08-22 17:10:00 +00:00
parent 3a54580d7b
commit da37904f71

119
firmware/encoder.ino Normal file
View file

@ -0,0 +1,119 @@
// ===== 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 <Wire.h>
#include <U8g2lib.h>
#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(); }
}