/* * DDS-генератор сигналов для лабораторных работ * Платформа: RP2040 (Raspberry Pi Pico) * * Работает в ОБОИХ Arduino-ядрах: * - earlephilhower (arduino-pico) — штатный ШИМ analogWriteFreq/analogWriteRange; * - официальное ядро "Raspberry Pi Pico/RP2040" (Arduino Mbed OS) — analogWriteFreq * в нём отсутствует, поэтому включается собственный драйвер ШИМ на регистрах * pico-sdk (hardware/pwm.h). Никакой настройки не требуется — ветка выбирается * автоматически по макросу 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) * 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 #include "hardware/timer.h" // add_repeating_timer_us, repeating_timer_t — оба ядра // ───────────────── выбор драйвера ШИМ ───────────────── // Официальное mbed-ядро не имеет analogWriteFreq/analogWriteRange — // используем регистры pico-sdk. earlephilhower — штатный analogWrite*. #if defined(ARDUINO_ARCH_MBED) #define USE_CORE_PWM 0 #include "hardware/pwm.h" #include "hardware/gpio.h" #else #define USE_CORE_PWM 1 #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) // pwmStop() — отключить ШИМ, пин -> обычный GPIO (для программного ШИМ) #if USE_CORE_PWM // 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); } static void pwmStop() { // digitalWrite сам переводит пин из ШИМ в GPIO digitalWrite(PIN_OUT, LOW); } #else // официальное mbed-ядро: драйвер на регистрах pico-sdk static uint slice_num, chan_num; static uint32_t g_wrap = PWM_RANGE; // Точный подбор пары (wrap, clkdiv 8.4): фактическая частота несущей // f = 125 МГц * 16 / ((wrap+1) * div8_4) // Перебираем wrap в окне возле range (чтобы не терять разрядность ШИМ) // и минимальной по модулю ошибке. static void pwmConfig(uint32_t freq_hz, uint32_t range) { const uint64_t CLK16 = 125000000ULL * 16; uint32_t w_lo = 1, w_hi = 65535; if (range > 1) { w_lo = range - 1; w_hi = (uint32_t)range + 1024; if (w_hi > 65535) w_hi = 65535; } uint32_t best_wrap = g_wrap, best_div = 16; int64_t best_err = INT64_MAX; for (uint32_t wrap = w_lo; wrap <= w_hi; ++wrap) { uint64_t den = (uint64_t)(wrap + 1) * freq_hz; uint32_t d = (uint32_t)((CLK16 + den / 2) / den); if (d < 16) d = 16; // div >= 1 if (d > 4095) d = 4095; // div <= 255,9375 (поле 8.4) int64_t fa = (int64_t)(CLK16 / ((uint64_t)(wrap + 1) * d)); int64_t err = fa - (int64_t)freq_hz; if (err < 0) err = -err; if (err < best_err) { best_err = err; best_wrap = wrap; best_div = d; } } g_wrap = best_wrap; pwm_set_clkdiv(slice_num, best_div / 16.0f); pwm_set_wrap(slice_num, best_wrap); } static void pwmDuty(uint32_t duty) { // масштабируем duty (0..255) на текущий wrap uint32_t level = (uint32_t)((uint64_t)duty * g_wrap / 255); pwm_set_chan_level(slice_num, chan_num, level); } static void pwmStart() { slice_num = pwm_gpio_to_slice_num(PIN_OUT); chan_num = pwm_gpio_to_channel(PIN_OUT); gpio_set_function(PIN_OUT, GPIO_FUNC_PWM); pwm_set_clkdiv(slice_num, 1.0f); pwm_set_wrap(slice_num, PWM_RANGE); pwm_set_chan_level(slice_num, chan_num, 0); pwm_set_enabled(slice_num, true); } static void pwmStop() { pwm_set_enabled(slice_num, false); gpio_set_function(PIN_OUT, GPIO_FUNC_SIO); gpio_set_dir(PIN_OUT, GPIO_OUT); gpio_put(PIN_OUT, LOW); } #endif // USE_CORE_PWM // ─────────────────────── таблица синуса ───────────────────── 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; pwmDuty((uint8_t)(128 + s)); } else if (g_freq < SW_SQ_MIN_HZ) { // «программный» прямоугольник: аппаратный ШИМ не тянет < 20 Гц uint32_t step = (uint32_t)((uint64_t)g_freq * (1ULL << 32) / SAMPLE_HZ); g_phase += step; digitalWrite(PIN_OUT, ((g_phase >> 24) < g_duty) ? HIGH : LOW); } // в режиме SQUARE с f >= SW_SQ_MIN_HZ ШИМ работает аппаратно, ISR молчит return true; } static repeating_timer_t g_timer; // ─────────────────── применить режим/частоту ──────────────── static void applySquare() { if (g_freq < SW_SQ_MIN_HZ) { // программный прямоугольник (ISR), ШИМ выключен pwmStop(); digitalWrite(PIN_OUT, g_duty > 0 ? HIGH : LOW); } 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, "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; } } }