/* * DDS-генератор сигналов для лабораторных работ * Платформа: RP2040 (Raspberry Pi Pico, Arduino core — earlephilhower) * * ЧЕРНОВИК (AI-сгенерировано, 2026-10-26). Требует проверки преподавателем * на реальном стенде: номиналы RC-фильтра, диапазоны частот, точность ADC. * * Возможности: * - прямоугольник : аппаратный ШИМ, точная частота, независимая скважность; * - синус : таблица 256 точек + аккумулятор фазы (DDS) в таймерном * ISR -> ШИМ (несущая = SAMPLE_HZ = 62,5 кГц) -> внешний * ФНЧ (схема ниже); амплитуда — командой M; * - свип : логарифмический (f0..f1, N точек) с синхроимпульсом * на отдельном пине — для осциллографа/хост-скрипта; * - измерение : ADC0 (размах за окно) — амплитуда на выходе * исследуемой цепи (DUT) для автоматической АЧХ. * * ─── СХЕМА СИНУСА (обязательна: ШИМ даёт меандр, а не синус) ─── * * GP15 ──[R1]──┬──[R2]──┬──────► OUT (вход DUT) * │ │ * [C1] [C2] * │ │ * GND GND * * Пассивный ФНЧ 2-го порядка. При несущей 62,5 кГц: * R1 = R2 = 1 кОм, C1 = C2 = 47 нФ -> f_c ≈ 3,4 кГц * подавление несущей ≈ (62,5/3,4)^2 ≈ 50 дБ -> пульсации ~10 мВ * Номиналы для другой несущей: f_c и постоянную RC смотреть в README. * * ВНИМАНИЕ (важно): выходное сопротивление фильтра ~1 кОм. Вход * интегратора — R1 = 10 кОм, получается делитель 10/11 (просадка ~9 %). * Если это критично — поставить буфер-повторитель на ОУ (Sallen-Key): * тот же фильтр 2-го порядка + низкое выходное сопротивление. * * Постоянная составляющая: при скважности 50 % среднее = 1,65 В, синус * смещён на Uпит/2. Где смещение недопустимо — разделительный C = 1 мкФ * последовательно + R = 47 кОм на землю (ФВЧ ≈ 3,4 Гц), либо уменьшить * амплитуду командой M. * * Протокол команд (Serial, 115200, строка + \n): * F <Гц> — установить частоту (1..500000) * W Q | W S — режим: Q — прямоугольник, S — синус * D <0..255> — скважность прямоугольника (128 = 50 %) * M <0..127> — амплитуда синуса (127 = максимум) * A — однократно измерить амплитуду на ADC0 * -> ответ "A <мВ размах>" * SWEEP * — логарифмический свип; на каждой точке выдаёт * "P <Гц> <мВ>" и импульс синхро на PIN_TRIG * STOP — прервать свип */ #include #include // ─────────────────────────── пины ─────────────────────────── #define PIN_OUT 15 // выход сигнала (прямоугольник — прямо; синус — через RC) #define PIN_TRIG 14 // синхроимпульс (для осциллографа) #define PIN_ADC 26 // ADC0 = A0 — вход измерения (выход DUT через делитель!) // ─────────────────────── параметры DDS ────────────────────── // ВАЖНО: несущая ШИМ и частота обновления выборок должны СОВПАДАТЬ. // Значение скважности применяется один раз за период ШИМ; если обновлять // выборки с другой частотой, появляется джиттер и биения на выходе фильтра. // При 16 мкс период ISR занимает ~3 % CPU RP2040; при перегрузке — // снизить до 31250 (тогда f_c фильтра пересчитать вниз пропорционально). #define CARRIER_HZ 62500UL // несущая ШИМ для синуса (= частота выборок) #define SAMPLE_HZ 62500UL // частота обновления выборок синуса #define PWM_RANGE 255 // 8-бит (для синуса); прямоугольнику хватает #define AMP_MAX 127 // максимальная амплитуда выборки (единиц duty) // ───────────────────────── состояние ──────────────────────── enum WaveMode : uint8_t { MODE_SQUARE = 0, MODE_SINE = 1 }; static volatile uint32_t g_freq = 1000; // текущая частота, Гц static volatile uint8_t g_duty = 128; // 0..255 (прямоугольник) static volatile uint8_t g_amp = AMP_MAX;// амплитуда синуса, 0..AMP_MAX static volatile WaveMode g_mode = MODE_SQUARE; static volatile uint32_t g_phase = 0; // аккумулятор фазы (32 бит) static volatile bool g_sweep = false; // флаг активного свипа static int8_t g_lut[256]; // sin: -127..127 // ─────────────────────── таблица синуса ───────────────────── static void buildLut() { for (int i = 0; i < 256; ++i) { g_lut[i] = (int8_t)lroundf(127.0f * sinf(2.0f * (float)M_PI * i / 256.0f)); } } // ───────────── ISR: обновление ШИМ для синуса (DDS) ───────── static bool onSample(repeating_timer_t *) { if (g_mode == MODE_SINE) { // шаг фазы = f * 2^32 / Fs (64-бит, чтобы не переполнить) uint32_t step = (uint32_t)((uint64_t)g_freq * (1ULL << 32) / SAMPLE_HZ); g_phase += step; uint8_t idx = (uint8_t)(g_phase >> 24); // центрируем на 128 и масштабируем амплитудой (g_amp 0..127) int32_t s = ((int32_t)g_lut[idx] * g_amp) / 127; analogWrite(PIN_OUT, (uint8_t)(128 + s)); } return true; } static repeating_timer_t g_timer; // ─────────────────── применить режим/частоту ──────────────── static void applySquare() { analogWriteFreq(g_freq); analogWriteRange(PWM_RANGE); // после analogWriteFreq/analogWriteRange текущее значение сбрасывается: analogWrite(PIN_OUT, g_duty); } static void applySine() { // частота ШИМ = SAMPLE_HZ (совпадает с ISR), размах — 8 бит analogWriteFreq(CARRIER_HZ); analogWriteRange(PWM_RANGE); } static void setFrequency(uint32_t hz) { if (hz < 1) hz = 1; if (hz > 500000) hz = 500000; g_freq = hz; if (g_mode == MODE_SQUARE) applySquare(); } static void setMode(WaveMode m) { g_mode = m; if (m == MODE_SQUARE) applySquare(); else applySine(); } // ─────────────────── измерение амплитуды (ADC) ────────────── // Возвращает размах (пик-пик) в милливольтах за окно window_ms. // TODO (студенту): заменить окно по времени на окно по числу периодов // измеряемой частоты, чтобы размах считался по целому числу периодов. static uint32_t measureAmplitudeMv(uint32_t window_ms = 50) { uint16_t vmin = 4095, vmax = 0; uint32_t t0 = millis(); while ((millis() - t0) < window_ms) { uint16_t v = analogRead(PIN_ADC); if (v < vmin) vmin = v; if (v > vmax) vmax = v; } // ADC 12 бит, Vref = 3,3 В return (uint32_t)((uint32_t)(vmax - vmin) * 3300UL / 4095UL); } // ───────────────────────── свип ───────────────────────────── static void triggerPulse() { digitalWrite(PIN_TRIG, HIGH); delayMicroseconds(50); digitalWrite(PIN_TRIG, LOW); } static void runSweep(float f0, float f1, int n, uint32_t dwell_ms) { if (n < 2) n = 2; if (f0 < 1) f0 = 1; if (f1 <= f0) f1 = f0 * 10.0f; g_sweep = true; const float ratio = powf(f1 / f0, 1.0f / (float)(n - 1)); float f = f0; for (int i = 0; i < n && g_sweep; ++i) { setFrequency((uint32_t)lroundf(f)); triggerPulse(); // метка точки для осциллографа delay(dwell_ms); // дать DUT/фильтру устояться uint32_t mv = measureAmplitudeMv(); Serial.print("P "); // точка АЧХ: частота + размах Serial.print((uint32_t)lroundf(f)); Serial.print(' '); Serial.println(mv); f *= ratio; } Serial.println("DONE"); setFrequency((uint32_t)f0); // вернуться к началу } // ─────────────────────── разбор команд ────────────────────── static void handleLine(char *line) { char *cmd = strtok(line, " \t"); if (!cmd) return; if (!strcasecmp(cmd, "F")) { char *a = strtok(nullptr, " \t"); if (a) setFrequency((uint32_t)strtoul(a, nullptr, 10)); Serial.print("OK F "); Serial.println(g_freq); } else if (!strcasecmp(cmd, "W")) { char *a = strtok(nullptr, " \t"); if (a) setMode((!strcasecmp(a, "S")) ? MODE_SINE : MODE_SQUARE); Serial.print("OK W "); Serial.println(g_mode == MODE_SINE ? "S" : "Q"); } else if (!strcasecmp(cmd, "D")) { char *a = strtok(nullptr, " \t"); if (a) { g_duty = (uint8_t)constrain(atoi(a), 0, 255); applySquare(); } Serial.print("OK D "); Serial.println(g_duty); } else if (!strcasecmp(cmd, "M")) { char *a = strtok(nullptr, " \t"); if (a) g_amp = (uint8_t)constrain(atoi(a), 0, AMP_MAX); Serial.print("OK M "); Serial.println(g_amp); } else if (!strcasecmp(cmd, "A")) { Serial.print("A "); Serial.println(measureAmplitudeMv()); } else if (!strcasecmp(cmd, "SWEEP")) { char *a0 = strtok(nullptr, " \t"); char *a1 = strtok(nullptr, " \t"); char *a2 = strtok(nullptr, " \t"); char *a3 = strtok(nullptr, " \t"); if (a0 && a1 && a2 && a3) { runSweep(atof(a0), atof(a1), atoi(a2), strtoul(a3, nullptr, 10)); } } else if (!strcasecmp(cmd, "STOP")) { g_sweep = false; } } // ────────────────────────── setup/loop ────────────────────── void setup() { Serial.begin(115200); buildLut(); pinMode(PIN_TRIG, OUTPUT); digitalWrite(PIN_TRIG, LOW); analogReadResolution(12); // ADC0 — 12 бит add_repeating_timer_us(-(1000000L / SAMPLE_HZ), onSample, nullptr, &g_timer); setMode(MODE_SQUARE); setFrequency(1000); Serial.println("# DDS generator ready (RP2040). help: F/W/D/A/SWEEP/STOP"); } void loop() { static char buf[96]; static size_t len = 0; while (Serial.available()) { char c = (char)Serial.read(); if (c == '\n' || c == '\r') { buf[len] = '\0'; if (len) handleLine(buf); len = 0; } else if (len < sizeof(buf) - 1) { buf[len++] = c; } } }