Files
eumirts5-dds/stub/01-dds-generator-rp2040.ino
T

244 lines
13 KiB
Arduino
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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 <f0> <f1> <n> <dwell_ms>
* — логарифмический свип; на каждой точке выдаёт
* "P <Гц> <мВ>" и импульс синхро на PIN_TRIG
* STOP — прервать свип
*/
#include <Arduino.h>
#include <math.h>
// ─────────────────────────── пины ───────────────────────────
#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;
}
}
}