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

343 lines
18 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-ядрах:
* - 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 <f0> <f1> <n> <dwell_ms>
* — логарифмический свип; на каждой точке выдаёт
* "P <Гц> <мВ>" и импульс синхро на PIN_TRIG
* STOP — прервать свип
*/
#include <Arduino.h>
#include <math.h>
// ───────────────── выбор бэкенда ─────────────────
// Официальное 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;
}
}
}