/* * DDS-генератор сигналов для лабораторных работ * Платформа: RP2040 (Raspberry Pi Pico) * * Работает в ОБОИХ Arduino-ядрах: * - earlephilhower (arduino-pico) — штатный ШИМ analogWriteFreq/analogWriteRange * и таймер add_repeating_timer_us (pico-sdk); * - официальное ядро "Raspberry Pi Pico/RP2040" (Arduino Mbed OS) — в пользовательском * коде НЕТ ни analogWriteFreq, ни pico-sdk-заголовков (hardware/pwm.h, hardware/timer.h), * поэтому здесь используется только mbed API, который является частью самого ядра: * mbed::PwmOut (ШИМ) + mbed::Ticker (тактовый генератор выборок 62,5 кГц). * Ветка выбирается автоматически по макросу ARDUINO_ARCH_MBED. * * ЧЕРНОВИК (AI-сгенерировано, 2026-10-26). Требует проверки преподавателем * на реальном стенде: номиналы RC-фильтра, диапазоны частот, точность ADC. * * Возможности: * - прямоугольник : аппаратный ШИМ (точная частота, независимая скважность); * ниже 20 Гц — «программный» прямоугольник в таймерном ISR * (аппаратный ШИМ RP2040 не опускается ниже ~7,5 Гц); * - синус : таблица 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) * SF <Гц> — частота синусоиды (1..SAMPLE_HZ/2, защита от алиасинга) * 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 // ───────────────── выбор бэкенда ───────────────── // Официальное mbed-ядро: нет analogWriteFreq, нет pico-sdk в пользовательском // коде -> только mbed API (PwmOut + Ticker). earlephilhower: analogWrite* + // add_repeating_timer_us. #if defined(ARDUINO_ARCH_MBED) #define USE_MBED_API 1 #include "mbed.h" #else #define USE_MBED_API 0 #include "hardware/timer.h" // add_repeating_timer_us, repeating_timer_t #endif // ─────────────────────────── пины ─────────────────────────── #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) // Прямоугольник ниже этой частоты генерится в ISR: аппаратный ШИМ RP2040 // (div 8.4 + wrap 16 бит) физически не опускается ниже ~7,5 Гц. #define SW_SQ_MIN_HZ 20UL // ───────────────────────── состояние ──────────────────────── 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 // ─────────────── универсальный слой ШИМ ─────────────── // pwmStart() — включить ШИМ на PIN_OUT (idempotent) // pwmConfig(f, range) — настроить частоту несущей // pwmDuty(d) — установить скважность (0..255, масштабируется на текущий wrap) #if USE_MBED_API // официальное mbed-ядро: PwmOut (16-бит размах внутри ядра) static mbed::PwmOut g_pwm(digitalPinToPinName(PIN_OUT)); static void pwmStart() { /* PwmOut активен сразу */ } static void pwmConfig(uint32_t freq_hz, uint32_t range) { (void)range; if (freq_hz < 1) freq_hz = 1; g_pwm.period_us(1000000UL / freq_hz); // период в мкс (для 62,5 кГц = 16 мкс) } static void pwmDuty(uint32_t duty) { if (duty > PWM_RANGE) duty = PWM_RANGE; g_pwm.write((float)duty / (float)PWM_RANGE); } #else // earlephilhower arduino-pico static void pwmStart() { /* analogWrite сам включает ШИМ */ } static void pwmConfig(uint32_t freq_hz, uint32_t range) { analogWriteFreq(freq_hz); analogWriteRange(range); } static void pwmDuty(uint32_t duty) { analogWrite(PIN_OUT, duty); } #endif // USE_MBED_API // ─────────────────────── таблица синуса ───────────────────── 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: обновление выборок ──────────────── // В режиме SINE — DDS-шаг фазы и новая скважность каждые 16 мкс; // в режиме SQUARE с f < SW_SQ_MIN_HZ — программный прямоугольник // (тот же ШИМ, только скважность 255 или 0 по фазе); // в режиме SQUARE с f >= SW_SQ_MIN_HZ ШИМ работает аппаратно, ISR молчит. static void sampleIsrBody() { 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; pwmDuty((uint8_t)(128 + s)); } else if (g_freq < SW_SQ_MIN_HZ) { uint32_t step = (uint32_t)((uint64_t)g_freq * (1ULL << 32) / SAMPLE_HZ); g_phase += step; pwmDuty(((g_phase >> 24) < g_duty) ? PWM_RANGE : 0); } } #if USE_MBED_API static void sampleTick() { sampleIsrBody(); } static mbed::Ticker g_tick; static void startSampleClock() { // 16 мкс -> 62,5 кГц; Ticker — аппаратный таймер mbed g_tick.attach_us(&sampleTick, 1000000L / SAMPLE_HZ); } #else static bool onSample(repeating_timer_t *) { sampleIsrBody(); return true; } static repeating_timer_t g_timer; static void startSampleClock() { add_repeating_timer_us(-(1000000L / SAMPLE_HZ), onSample, nullptr, &g_timer); } #endif // ─────────────────── применить режим/частоту ──────────────── static void applySquare() { if (g_freq < SW_SQ_MIN_HZ) { // программный прямоугольник (ISR): несущая = SAMPLE_HZ pwmStart(); pwmConfig(SAMPLE_HZ, PWM_RANGE); pwmDuty(g_duty > 0 ? PWM_RANGE : 0); } else { pwmStart(); pwmConfig(g_freq, PWM_RANGE); pwmDuty(g_duty); } } static void applySine() { // частота ШИМ = SAMPLE_HZ (совпадает с ISR), размах — 8 бит pwmStart(); pwmConfig(SAMPLE_HZ, PWM_RANGE); // уровень выборок обновляет ISR } 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, "SF")) { // частота синусоиды: с защитой от алиасинга (f <= SAMPLE_HZ/2) char *a = strtok(nullptr, " \t"); uint32_t hz = a ? (uint32_t)strtoul(a, nullptr, 10) : g_freq; if (hz < 1) hz = 1; if (hz > SAMPLE_HZ / 2) hz = SAMPLE_HZ / 2; setFrequency(hz); Serial.print("OK SF "); 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 бит startSampleClock(); 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; } } }