#include #include #include #include #include #include // ===================== PINOUT ===================== constexpr uint8_t PIN_SPI_SCLK = 18; constexpr uint8_t PIN_SPI_MOSI = 23; // TFT constexpr uint8_t PIN_TFT_CS = 5; constexpr uint8_t PIN_TFT_DC = 2; constexpr uint8_t PIN_TFT_RST = 4; constexpr uint8_t PIN_TFT_BL = 12; // PWM (LEDC) // AD9833 constexpr uint8_t PIN_AD_CS = 15; // FSYNC // MCP41010 constexpr uint8_t PIN_MCP_CS = 13; // Inputs constexpr uint8_t PIN_ENC_A = 32; constexpr uint8_t PIN_ENC_B = 33; constexpr uint8_t PIN_BTN = 27; // ===================== TFT ===================== Adafruit_ST7789 tft(PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_RST); // ===================== Button ===================== OneButton button(PIN_BTN, /*activeLow=*/true, /*pullupActive=*/true); // ===================== Preferences (NVS) ===================== Preferences prefs; // ===================== AD9833 Driver ===================== // Maski rejestrów constexpr uint16_t AD_B28 = 0x2000; constexpr uint16_t AD_HLB = 0x1000; constexpr uint16_t AD_FSELECT = 0x0800; constexpr uint16_t AD_PSELECT = 0x0400; constexpr uint16_t AD_RESET = 0x0100; constexpr uint16_t AD_SLEEP1 = 0x0080; constexpr uint16_t AD_SLEEP12 = 0x0040; constexpr uint16_t AD_OPBITEN = 0x0020; constexpr uint16_t AD_DIV2 = 0x0008; constexpr uint16_t AD_MODE = 0x0002; constexpr uint16_t AD_FREQ0 = 0x4000; constexpr uint16_t AD_FREQ1 = 0x8000; constexpr uint16_t AD_PHASE0 = 0xC000; constexpr uint16_t AD_PHASE1 = 0xE000; class AD9833 { public: explicit AD9833(uint8_t cs) : _cs(cs) {} void begin() { pinMode(_cs, OUTPUT); digitalWrite(_cs, HIGH); // SPI wspólne – inicjalizacja globalna w setup() // Reset + B28 writeCtrl(AD_B28 | AD_RESET); delay(2); writeCtrl(AD_B28 & ~AD_RESET); } void setClockHz(uint32_t hz) { _mclk = hz; } void setOutput(bool on) { if (on) _ctrl &= ~AD_SLEEP1; else _ctrl |= AD_SLEEP1; writeCtrl(_ctrl); } void setWave(uint8_t wave /*0=sin,1=tri,2=sqr*/, bool div2) { _ctrl &= ~(AD_MODE | AD_OPBITEN | AD_DIV2); switch (wave) { case 0: break; // sine case 1: _ctrl |= AD_MODE; break; // triangle case 2: _ctrl |= AD_OPBITEN; if (div2) _ctrl |= AD_DIV2; break; // square } writeCtrl(_ctrl); } void selectFreqReg(bool reg1) { if (reg1) _ctrl |= AD_FSELECT; else _ctrl &= ~AD_FSELECT; writeCtrl(_ctrl); } void selectPhaseReg(bool reg1) { if (reg1) _ctrl |= AD_PSELECT; else _ctrl &= ~AD_PSELECT; writeCtrl(_ctrl); } void setFrequency(double hz, bool reg1) { uint32_t word28 = (uint32_t) llround(hz * (double)(1ULL<<28) / (double)_mclk); uint16_t lsb = (uint16_t)(word28 & 0x3FFF); uint16_t msb = (uint16_t)((word28 >> 14) & 0x3FFF); uint16_t base = reg1 ? AD_FREQ1 : AD_FREQ0; writeWord(base | lsb); writeWord(base | msb); } void setPhaseDeg(double deg, bool reg1) { deg = fmod(deg, 360.0); if (deg < 0) deg += 360.0; uint16_t word = (uint16_t) llround(deg * 4096.0 / 360.0) & 0x0FFF; writeWord((reg1 ? AD_PHASE1 : AD_PHASE0) | word); } void resetChip() { writeCtrl(_ctrl | AD_RESET); delay(2); writeCtrl(_ctrl & ~AD_RESET); } private: uint8_t _cs; uint32_t _mclk = 25000000; // domyślnie 25 MHz uint16_t _ctrl = AD_B28 | AD_SLEEP1; // mirror ctrl void writeWord(uint16_t w) { SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE2)); // AD9833: mode 2 digitalWrite(_cs, LOW); SPI.transfer16(w); digitalWrite(_cs, HIGH); SPI.endTransaction(); } void writeCtrl(uint16_t w) { _ctrl = (w & 0x3FFF) | (AD_B28); writeWord(0x2000 | (_ctrl & 0x3FFF)); } }; // ===================== MCP41010 Driver ===================== class MCP41010 { public: MCP41010(uint8_t csPin, uint8_t minWiper=2) : _cs(csPin), _min(minWiper) {} void begin() { pinMode(_cs, OUTPUT); digitalWrite(_cs, HIGH); setPercent(50.0f); } void setWiperRaw(uint8_t val) { uint8_t v = val < _min ? _min : val; SPI.beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE0)); // MCP41xxx: mode 0 digitalWrite(_cs, LOW); SPI.transfer(0x11); // Write data pot0 SPI.transfer(v); digitalWrite(_cs, HIGH); SPI.endTransaction(); _last = v; } void setPercent(float pct) { if (pct < 0) pct = 0; if (pct > 100) pct = 100; uint8_t v = (uint8_t) roundf(pct * 255.0f / 100.0f); setWiperRaw(v); } uint8_t lastRaw() const { return _last; } private: uint8_t _cs; uint8_t _min; uint8_t _last=0; }; // ===================== Global instances ===================== AD9833 ad(PIN_AD_CS); MCP41010 mcp(PIN_MCP_CS); // ===================== Encoder (ISR) ===================== volatile int8_t enc_state = 0; volatile int32_t enc_steps = 0; volatile uint32_t enc_last_us = 0; void IRAM_ATTR enc_isr() { int a = digitalRead(PIN_ENC_A); int b = digitalRead(PIN_ENC_B); int8_t s = (a << 1) | b; // Dekodowanie Gray (pełny krok = 4 stany). Tu liczymy każdy sub-krok. static const int8_t lut[16] = {0,-1, +1, 0, +1, 0, 0,-1, -1, 0, 0,+1, 0, +1,-1, 0}; int idx = (enc_state << 2) | s; int8_t d = lut[idx & 0x0F]; enc_state = s; if (d) { enc_steps += d; enc_last_us = micros(); } } // ===================== UI / Menu ===================== enum Wave { W_SIN=0, W_TRI=1, W_SQR=2 }; enum Mode { M_SINGLE=0, M_SWEEP=1, M_FSK=2, M_PSK=3, M_BURST=4 }; enum SweepType { SW_LIN=0, SW_LOG=1 }; enum SweepDir { SW_UP=0, SW_DOWN=1, SW_PINGPONG=2 }; struct SweepCfg { bool enable=false; SweepType type=SW_LIN; SweepDir dir=SW_UP; double fStart=1000, fStop=10000; uint16_t steps=50; uint16_t dwell_ms=50; bool repeat=true; // runtime: uint16_t idx=0; int8_t direction=+1; uint32_t tNext=0; }; struct FskCfg { bool enable=false; double f0=100000; double f1=120000; enum Source { MANUAL=0, AUTO=1 } src=AUTO; uint16_t rate_hz=5; // runtime bool sel=false; // false=F0,true=F1 uint32_t tNext=0; }; struct PskCfg { bool enable=false; double p0_deg=0; double p1_deg=180; enum Source { MANUAL=0, AUTO=1 } src=AUTO; uint16_t rate_hz=5; bool sel=false; uint32_t tNext=0; }; struct BurstCfg { bool enable=false; uint32_t period_ms=1000; uint32_t on_ms=200; // runtime bool on=false; uint32_t tNext=0; }; struct Settings { // Generator podstawowy bool run=false; Wave wave=W_SIN; bool sqr_div2=false; double mclk=25e6; // kalibracja MCLK double freq=1000000.0; // Single mode double phase_deg=0.0; // podstawowa faza (PHASE0) // FSK/PSK/Sweep/Burst SweepCfg sweep; FskCfg fsk; PskCfg psk; BurstCfg burst; // Amplituda (MCP) – procent float amplitude_pct=50.0f; // UI/ustawienia ogólne uint8_t encoder_accel=2; // 0..3 uint8_t brightness=255; // 0..255 uint8_t currentProfile=1; // 1..8 } GS; // ---------- Presety ---------- struct Profile { Settings s; char name[12] = "Preset"; }; Profile profiles[8]; // RAM kopie // ---------- Render helpers ---------- uint16_t bg = ST77XX_BLACK; uint16_t fg = ST77XX_WHITE; uint16_t ac = ST77XX_YELLOW; uint16_t ok = ST77XX_GREEN; uint16_t hl = ST77XX_BLUE; uint16_t rd = ST77XX_RED; void setText(int size, uint16_t color, int x, int y, const String& txt) { tft.setTextSize(size); tft.setTextColor(color); tft.setCursor(x,y); tft.print(txt); } void drawHeader(const char* title) { tft.fillRect(0,0,240,24, bg); setText(2, fg, 4, 4, title); tft.drawLine(0,24,240,24, fg); // Status RUN/STOP setText(2, GS.run ? ok : rd, 180, 4, GS.run ? "RUN" : "STOP"); } void ledcInitBacklight() { // Kanał 0, 5kHz, 8 bit ledcSetup(0, 5000, 8); ledcAttachPin(PIN_TFT_BL, 0); ledcWrite(0, GS.brightness); } // ====== Menu model ====== enum Page { P_HOME, P_GEN, P_SWEEP, P_FSK, P_PSK, P_BURST, P_PRESETS, P_SETTINGS, P_CALIB, P_SERVICE, P_EDIT_STRING }; Page page = P_HOME; int cursor = 0; bool edit = false; String fmtFreq(double f) { char buf[32]; if (f < 1e3) snprintf(buf, sizeof(buf), "%.2f Hz", f); else if (f < 1e6) snprintf(buf, sizeof(buf), "%.2f kHz", f/1e3); else snprintf(buf, sizeof(buf), "%.2f MHz", f/1e6); return String(buf); } // ====== Zapis/odczyt NVS ====== const char* NVS_NS = "ad9833gen"; const uint16_t FW_VER = 0x0102; void loadSettings() { prefs.begin(NVS_NS, false); uint16_t ver = prefs.getUShort("fwver", 0); if (ver != FW_VER) { prefs.clear(); // nowe defaulty prefs.putUShort("fwver", FW_VER); } GS.mclk = prefs.getULong("mclk", (uint32_t)25e6); GS.brightness = prefs.getUChar("bright", 200); GS.encoder_accel = prefs.getUChar("accel", 2); GS.currentProfile = prefs.getUChar("curprof", 1); // profile sloty for (int i=0;i<8;i++) { char key[16]; snprintf(key,sizeof(key),"p%d_exists",i+1); bool ex = prefs.getBool(key, false); if (!ex) { // domyślny preset profiles[i].s = GS; // z bazowych snprintf(profiles[i].name,sizeof(profiles[i].name),"P%d", i+1); } else { // wczytaj preset snprintf(key,sizeof(key),"p%d_name",i+1); prefs.getString(key, profiles[i].name, sizeof(profiles[i].name)); // każde pole osobno (dla prostoty parę kluczowych) snprintf(key,sizeof(key),"p%d_freq",i+1); profiles[i].s.freq = prefs.getULong(key, 1000000); snprintf(key,sizeof(key),"p%d_wave",i+1); profiles[i].s.wave = (Wave)prefs.getUChar(key, 0); snprintf(key,sizeof(key),"p%d_phase",i+1); profiles[i].s.phase_deg = prefs.getFloat(key, 0.0f); snprintf(key,sizeof(key),"p%d_amp",i+1); profiles[i].s.amplitude_pct = prefs.getFloat(key, 50.0f); // tryby snprintf(key,sizeof(key),"p%d_sw_en",i+1); profiles[i].s.sweep.enable = prefs.getBool(key, false); snprintf(key,sizeof(key),"p%d_fsk_en",i+1); profiles[i].s.fsk.enable = prefs.getBool(key, false); snprintf(key,sizeof(key),"p%d_psk_en",i+1); profiles[i].s.psk.enable = prefs.getBool(key, false); snprintf(key,sizeof(key),"p%d_b_en",i+1); profiles[i].s.burst.enable = prefs.getBool(key, false); } } prefs.end(); } void saveSettingsBasics() { prefs.begin(NVS_NS, false); prefs.putULong("mclk", (uint32_t)GS.mclk); prefs.putUChar("bright", GS.brightness); prefs.putUChar("accel", GS.encoder_accel); prefs.putUChar("curprof", GS.currentProfile); prefs.end(); } void saveProfile(uint8_t idx /*1..8*/) { if (idx<1 || idx>8) return; int i = idx-1; prefs.begin(NVS_NS, false); char key[16]; snprintf(key,sizeof(key),"p%d_exists",idx); prefs.putBool(key, true); snprintf(key,sizeof(key),"p%d_name",idx); prefs.putString(key, profiles[i].name); snprintf(key,sizeof(key),"p%d_freq",idx); prefs.putULong(key, (uint32_t)profiles[i].s.freq); snprintf(key,sizeof(key),"p%d_wave",idx); prefs.putUChar(key, (uint8_t)profiles[i].s.wave); snprintf(key,sizeof(key),"p%d_phase",idx); prefs.putFloat(key, (float)profiles[i].s.phase_deg); snprintf(key,sizeof(key),"p%d_amp",idx); prefs.putFloat(key, profiles[i].s.amplitude_pct); snprintf(key,sizeof(key),"p%d_sw_en",idx); prefs.putBool(key, profiles[i].s.sweep.enable); snprintf(key,sizeof(key),"p%d_fsk_en",idx); prefs.putBool(key, profiles[i].s.fsk.enable); snprintf(key,sizeof(key),"p%d_psk_en",idx); prefs.putBool(key, profiles[i].s.psk.enable); snprintf(key,sizeof(key),"p%d_b_en",idx); prefs.putBool(key, profiles[i].s.burst.enable); prefs.end(); } void applyProfileToGS(uint8_t idx) { if (idx<1 || idx>8) return; GS = profiles[idx-1].s; } // ====== Generator logika (tick) ====== uint32_t nowMs; void adApplyBasics() { ad.setClockHz((uint32_t)GS.mclk); ad.setWave((uint8_t)GS.wave, GS.sqr_div2); ad.setPhaseDeg(GS.phase_deg, false); // PHASE0 ad.setFrequency(GS.freq, false); // FREQ0 ad.setOutput(GS.run && !GS.burst.enable); // jeśli burst aktywny, włączane przez tick } double sweepFreqAt(uint16_t idx) { double f0 = GS.sweep.fStart, f1 = GS.sweep.fStop; if (GS.sweep.type == SW_LIN) { if (GS.sweep.steps <= 1) return f0; double t = (double)idx / (double)(GS.sweep.steps - 1); return f0 + (f1 - f0) * t; } else { // LOG if (f0 <= 0) f0 = 1.0; if (f1 <= 0) f1 = 1.0; double r = pow(f1/f0, (GS.sweep.steps<=1)?0: (double)idx/(double)(GS.sweep.steps-1)); return f0 * r; } } void tickSweep() { if (!GS.sweep.enable || !GS.run) return; if (nowMs < GS.sweep.tNext) return; // Ustaw nową częstotliwość double f = sweepFreqAt(GS.sweep.idx); ad.setFrequency(f, false); // Następny krok GS.sweep.tNext = nowMs + GS.sweep.dwell_ms; // Kierunek if (GS.sweep.dir == SW_UP) { if (++GS.sweep.idx >= GS.sweep.steps) { if (GS.sweep.repeat) GS.sweep.idx = 0; else GS.sweep.idx = GS.sweep.steps-1; } } else if (GS.sweep.dir == SW_DOWN) { if (GS.sweep.idx == 0) { if (GS.sweep.repeat) GS.sweep.idx = GS.sweep.steps-1; } else GS.sweep.idx--; } else { // PINGPONG GS.sweep.idx += GS.sweep.direction; if ((int)GS.sweep.idx >= (int)GS.sweep.steps) { GS.sweep.idx = GS.sweep.steps-2; GS.sweep.direction = -1; } if (GS.sweep.idx == 0) { GS.sweep.direction = +1; } } } void tickFSK() { if (!GS.fsk.enable || !GS.run) return; if (GS.fsk.src == FskCfg::AUTO) { uint32_t period = max(1, 1000 / max(1, GS.fsk.rate_hz)); if (nowMs >= GS.fsk.tNext) { GS.fsk.tNext = nowMs + period; GS.fsk.sel = !GS.fsk.sel; ad.selectFreqReg(GS.fsk.sel); } } } void tickPSK() { if (!GS.psk.enable || !GS.run) return; if (GS.psk.src == PskCfg::AUTO) { uint32_t period = max(1, 1000 / max(1, GS.psk.rate_hz)); if (nowMs >= GS.psk.tNext) { GS.psk.tNext = nowMs + period; GS.psk.sel = !GS.psk.sel; ad.selectPhaseReg(GS.psk.sel); } } } void tickBurst() { if (!GS.burst.enable) return; if (nowMs < GS.burst.tNext) return; if (!GS.run) { // jeśli STOP, trzymaj wyjście OFF ad.setOutput(false); GS.burst.tNext = nowMs + 100; return; } GS.burst.on = !GS.burst.on; ad.setOutput(GS.burst.on); GS.burst.tNext = nowMs + (GS.burst.on ? GS.burst.on_ms : (GS.burst.period_ms - GS.burst.on_ms)); } // ====== UI – strony ====== void drawHome() { tft.fillScreen(bg); drawHeader("AD9833 Home"); int y = 32; setText(2, fg, 4, y, "Wave: "); setText(2, ac, 120, y, (GS.wave==W_SIN?"SIN":GS.wave==W_TRI?"TRI":"SQR")); y+=24; setText(2, fg, 4, y, "Freq: "); setText(2, ac, 120, y, fmtFreq(GS.freq)); y+=24; setText(2, fg, 4, y, "Phase: "); setText(2, ac, 120, y, String(GS.phase_deg,1)+" deg"); y+=24; setText(2, fg, 4, y, "Amp: "); setText(2, ac, 120, y, String(GS.amplitude_pct,1)+" %"); y+=24; setText(2, fg, 4, y, "Mode: "); String mode = "Single"; if (GS.sweep.enable) mode = "Sweep"; else if (GS.fsk.enable) mode = "FSK"; else if (GS.psk.enable) mode = "PSK"; else if (GS.burst.enable) mode = "Burst"; setText(2, ac, 120, y, mode); y+=24; setText(2, ok, 4, y+10, "[Click] RUN/STOP [Rot] Menu"); } void drawList(const char* title, const char* const items[], int n) { tft.fillScreen(bg); drawHeader(title); int y = 30; for (int i=0;i maxv) val = maxv; } void changeFloat(float &val, float step, float minv, float maxv, int dir) { int af = accelFactor(); float delta = step * af * dir; val += delta; if (val < minv) val = minv; if (val > maxv) val = maxv; } void changeInt(int &val, int step, int minv, int maxv, int dir) { int af = accelFactor(); int delta = step * af * dir; val += delta; if (val < minv) val = minv; if (val > maxv) val = maxv; } // ====== Obsługa stron – logika obrotu/kliku ====== void redrawPage() { go(page); } // prosty redraw void handleHomeRotate(int d) { // przewijaj pomiędzy stronami skrótowo if (d>0) go(P_GEN); else go(P_SETTINGS); } void handleGenRotate(int d) { if (!edit) { cursor = (cursor + (d>0?1:-1) + 6) % 6; redrawPage(); return; } switch (cursor) { case 0: // Waveform GS.wave = (Wave)((3 + (int)GS.wave + (d>0?1:-1)) % 3); ad.setWave((uint8_t)GS.wave, GS.sqr_div2); break; case 1: // Frequency changeDouble(GS.freq, 1.0, 0.0, 125e6, (d>0?+1:-1)); ad.setFrequency(GS.freq, false); break; case 2: // Phase changeDouble(GS.phase_deg, 0.1, 0.0, 360.0, (d>0?+1:-1)); ad.setPhaseDeg(GS.phase_deg, false); break; case 3: // Amplitude % changeFloat(GS.amplitude_pct, 1.0f, 0.0f, 100.0f, (d>0?+1:-1)); mcp.setPercent(GS.amplitude_pct); break; case 4: // RUN/STOP if (d!=0) { GS.run = !GS.run; ad.setOutput(GS.run && !GS.burst.enable); } break; case 5: // Square DIV2 if (d!=0) { GS.sqr_div2 = !GS.sqr_div2; ad.setWave((uint8_t)GS.wave, GS.sqr_div2); } break; } drawHome(); } void handleSweepRotate(int d) { const int n=8; if (!edit) { cursor = (cursor + (d>0?1:-1) + n) % n; redrawPage(); return; } switch (cursor) { case 0: GS.sweep.enable = (d!=0)?!GS.sweep.enable:GS.sweep.enable; break; case 1: GS.sweep.type = (SweepType)((2 + (int)GS.sweep.type + (d>0?1:-1))%2); break; case 2: GS.sweep.dir = (SweepDir)((3 + (int)GS.sweep.dir + (d>0?1:-1))%3); break; case 3: changeDouble(GS.sweep.fStart, 1.0, 0.1, 125e6, (d>0?+1:-1)); break; case 4: changeDouble(GS.sweep.fStop, 1.0, 0.1, 125e6, (d>0?+1:-1)); break; case 5: changeInt((int&)GS.sweep.steps, 1, 2, 2000, (d>0?+1:-1)); break; case 6: changeInt((int&)GS.sweep.dwell_ms, 1, 1, 5000, (d>0?+1:-1)); break; case 7: GS.sweep.repeat = (d!=0)?!GS.sweep.repeat:GS.sweep.repeat; break; } redrawPage(); } void handleFSKRotate(int d) { const int n=5; if (!edit) { cursor = (cursor + (d>0?1:-1) + n) % n; redrawPage(); return; } switch (cursor) { case 0: GS.fsk.enable = (d!=0)?!GS.fsk.enable:GS.fsk.enable; break; case 1: changeDouble(GS.fsk.f0, 1.0, 0.0, 125e6, (d>0?+1:-1)); ad.setFrequency(GS.fsk.f0, false); break; case 2: changeDouble(GS.fsk.f1, 1.0, 0.0, 125e6, (d>0?+1:-1)); ad.setFrequency(GS.fsk.f1, true); break; case 3: GS.fsk.src = (FskCfg::Source)((2 + (int)GS.fsk.src + (d>0?1:-1))%2); break; case 4: changeInt((int&)GS.fsk.rate_hz, 1, 1, 1000, (d>0?+1:-1)); break; } redrawPage(); } void handlePSKRotate(int d) { const int n=5; if (!edit) { cursor = (cursor + (d>0?1:-1) + n) % n; redrawPage(); return; } switch (cursor) { case 0: GS.psk.enable = (d!=0)?!GS.psk.enable:GS.psk.enable; break; case 1: changeDouble(GS.psk.p0_deg, 0.1, 0.0, 360.0, (d>0?+1:-1)); ad.setPhaseDeg(GS.psk.p0_deg, false); break; case 2: changeDouble(GS.psk.p1_deg, 0.1, 0.0, 360.0, (d>0?+1:-1)); ad.setPhaseDeg(GS.psk.p1_deg, true); break; case 3: GS.psk.src = (PskCfg::Source)((2 + (int)GS.psk.src + (d>0?1:-1))%2); break; case 4: changeInt((int&)GS.psk.rate_hz, 1, 1, 1000, (d>0?+1:-1)); break; } redrawPage(); } void handleBurstRotate(int d) { const int n=3; if (!edit) { cursor = (cursor + (d>0?1:-1) + n) % n; redrawPage(); return; } switch (cursor) { case 0: GS.burst.enable = (d!=0)?!GS.burst.enable:GS.burst.enable; break; case 1: changeInt((int&)GS.burst.period_ms, 1, 10, 100000, (d>0?+1:-1)); break; case 2: changeInt((int&)GS.burst.on_ms, 1, 1, (int)GS.burst.period_ms, (d>0?+1:-1)); break; } redrawPage(); } void handlePresetsRotate(int d) { const int n=2; if (!edit) { cursor = (cursor + (d>0?1:-1) + n) % n; redrawPage(); return; } // W trybie edycji wejdziemy do podrutyny klików } void handleSettingsRotate(int d) { const int n=3; if (!edit) { cursor = (cursor + (d>0?1:-1) + n) % n; redrawPage(); return; } switch (cursor) { case 0: changeInt((int&)GS.brightness, 1, 0, 255, (d>0?+1:-1)); ledcWrite(0, GS.brightness); break; case 1: changeInt((int&)GS.encoder_accel, 1, 0, 3, (d>0?+1:-1)); break; case 2: changeInt((int&)GS.currentProfile, 1, 1, 8, (d>0?+1:-1)); break; } saveSettingsBasics(); redrawPage(); } void handleCalibRotate(int d) { const int n=3; if (!edit) { cursor = (cursor + (d>0?1:-1) + n) % n; redrawPage(); return; } switch (cursor) { case 0: changeDouble(GS.mclk, 1.0, 1e6, 200e6, (d>0?+1:-1)); ad.setClockHz((uint32_t)GS.mclk); break; case 1: if (d!=0) ad.resetChip(); break; case 2: if (d!=0) { prefs.begin(NVS_NS,false); prefs.clear(); prefs.end(); loadSettings(); } break; } saveSettingsBasics(); redrawPage(); } // ====== Nawigacja przycisk ====== void onClick() { switch (page) { case P_HOME: GS.run = !GS.run; ad.setOutput(GS.run && !GS.burst.enable); drawHome(); break; case P_GEN: edit = !edit; if (!edit) drawHome(); break; case P_SWEEP: edit = !edit; break; case P_FSK: edit = !edit; break; case P_PSK: edit = !edit; break; case P_BURST: edit = !edit; break; case P_SETTINGS: edit = !edit; break; case P_CALIB: edit = !edit; break; case P_PRESETS: if (!edit) { edit=true; } else { // cursor: 0=Load, 1=Save if (cursor==0) { // Wczytaj slot GS.currentProfile applyProfileToGS(GS.currentProfile); adApplyBasics(); mcp.setPercent(GS.amplitude_pct); drawHome(); page = P_HOME; edit=false; } else { // Zapisz do slotu profiles[GS.currentProfile-1].s = GS; saveProfile(GS.currentProfile); go(P_HOME); } } break; default: break; } } void onLongPress() { // Back if (page == P_HOME) return; if (edit) { edit=false; redrawPage(); return; } go(P_HOME); } void onDoubleClick() { // szybkie wyjście do HOME go(P_HOME); } // ====== Pętla odświeżania UI / obsługa enkodera ====== void routeRotate(int d) { switch (page) { case P_HOME: handleHomeRotate(d); break; case P_GEN: handleGenRotate(d); break; case P_SWEEP: handleSweepRotate(d); break; case P_FSK: handleFSKRotate(d); break; case P_PSK: handlePSKRotate(d); break; case P_BURST: handleBurstRotate(d); break; case P_PRESETS: handlePresetsRotate(d); break; case P_SETTINGS: handleSettingsRotate(d); break; case P_CALIB: handleCalibRotate(d); break; default: break; } } void showMainMenu() { // Proste menu poziome na HOME – kręcąc enkoderem idziesz do sekcji // Tu zrobimy multi-klik: obrót + długi = menu wyboru // W praktyce: użyjemy klików: krótkie=RUN/STOP, obrót=skok do GEN/SETTINGS } void drawMenuBar() { // Pasek skrótów – dla prostoty pomijamy; główne strony dostępne przez długi klik z HOME lub sekwencję } // ====== setup / loop ====== void setup() { Serial.begin(115200); // Piny wejść pinMode(PIN_ENC_A, INPUT_PULLUP); pinMode(PIN_ENC_B, INPUT_PULLUP); pinMode(PIN_BTN, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(PIN_ENC_A), enc_isr, CHANGE); attachInterrupt(digitalPinToInterrupt(PIN_ENC_B), enc_isr, CHANGE); // Global SPI SPI.begin(PIN_SPI_SCLK, /*MISO*/-1, PIN_SPI_MOSI, PIN_TFT_CS); // TFT tft.init(240, 240); tft.setRotation(0); tft.fillScreen(bg); // Backlight ledcInitBacklight(); // NVS loadSettings(); // Sterowniki ad.begin(); ad.setClockHz((uint32_t)GS.mclk); adApplyBasics(); mcp.begin(); mcp.setPercent(GS.amplitude_pct); // Button button.attachClick(onClick); button.attachLongPressStart(onLongPress); button.attachDoubleClick(onDoubleClick); // Start UI go(P_HOME); } void loop() { button.tick(); int32_t steps = readEncoderSteps(); if (steps != 0) { routeRotate(steps > 0 ? +1 : -1); } nowMs = millis(); // Tryby (nieblokujące) tickSweep(); tickFSK(); tickPSK(); tickBurst(); // Krótkie odświeżanie HOME co ~200 ms (status RUN/STOP, itp.) static uint32_t tUi=0; if (nowMs - tUi > 200 && page==P_HOME) { tUi = nowMs; drawHome(); } }