ai: студенческая копия лабораторной 5 (DDS-генератор RP2040 + АЧХ интегратора) из РПД

This commit is contained in:
mss
2026-10-26 15:19:12 +04:00
commit 45d6982d17
6 changed files with 879 additions and 0 deletions
+243
View File
@@ -0,0 +1,243 @@
/*
* 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;
}
}
}
+138
View File
@@ -0,0 +1,138 @@
#!/usr/bin/env python3
"""
Хост-скрипт: автоматическое снятие АЧХ с DDS-генератора (RP2040).
ЧЕРНОВИК (AI-сгенерировано, 2026-10-26). Требует проверки преподавателем.
Управляет генератором 01-dds-generator-rp2040.ino по Serial, запускает
логарифмический свип, собирает точки "P <Гц> <мВ>" и строит АЧХ в дБ.
Использование:
python 02-izmerenie-achh.py --port /dev/ttyACM0 \
--f0 10 --f1 100000 --n 25 --dwell 150 --out achh.csv
Зависимости: pyserial (обязательно), matplotlib (опционально, для графика).
pip install pyserial matplotlib
ПРОТОКОЛ (строки, \n):
-> W S режим синуса
-> SWEEP <f0> <f1> <n> <dwell_ms>
<- P <Гц> <мВ> точка АЧХ
<- DONE свип завершён
ВНИМАНИЕ: вход ADC0 генератора — 0..3,3 В. Сигнал с выхода исследуемой
цепи выше 3,3 В подавать только через делитель/пиковый детектор.
Нормировка на выходе: gain_dB = 20*log10(Uout/Uin). Входное напряжение
Uin берётся из аргумента --uin (действующая или амплитудная — как задано,
скрипт работает с размахом, мВ).
"""
from __future__ import annotations
import argparse
import csv
import math
import sys
import time
try:
import serial # type: ignore
except ImportError: # pragma: no cover
print("Нужен pyserial: pip install pyserial", file=sys.stderr)
raise SystemExit(2)
def open_port(port: str, baud: int = 115200, timeout: float = 0.2):
return serial.Serial(port, baud, timeout=timeout)
def send(ser, line: str) -> None:
ser.write((line + "\n").encode("ascii"))
ser.flush()
def collect_sweep(ser, f0: float, f1: float, n: int, dwell: int,
uin_mv: float, timeout_s: float = 120.0):
"""Запустить свип и вернуть список (f, uout_mv, gain_db)."""
send(ser, "W S") # синус
time.sleep(0.2)
send(ser, f"SWEEP {f0:.3f} {f1:.3f} {n} {dwell}")
points: list[tuple[float, float, float]] = []
t0 = time.time()
while True:
if time.time() - t0 > timeout_s:
print("таймаут свипа", file=sys.stderr)
break
raw = ser.readline().decode("ascii", "replace").strip()
if not raw:
continue
if raw == "DONE":
break
if raw.startswith("P "):
_, fs, mv = raw.split()
f = float(fs)
uout = float(mv)
gain = 20.0 * math.log10(max(uout, 1e-6) / max(uin_mv, 1e-6))
points.append((f, uout, gain))
print(f" {f:>9.1f} Гц {uout:>7.1f} мВ {gain:+6.2f} дБ")
return points
def write_csv(path: str, points) -> None:
with open(path, "w", newline="", encoding="utf-8") as fh:
w = csv.writer(fh)
w.writerow(["freq_hz", "uout_mv", "gain_db"])
for row in points:
w.writerow([f"{row[0]:.3f}", f"{row[1]:.3f}", f"{row[2]:.4f}"])
print(f"CSV: {path}")
def plot(points) -> None:
try:
import matplotlib.pyplot as plt # type: ignore
except ImportError:
print("matplotlib нет — график пропущен (CSV сохранён)", file=sys.stderr)
return
xs = [p[0] for p in points]
ys = [p[2] for p in points]
plt.semilogx(xs, ys, "o-")
plt.xlabel("Частота, Гц")
plt.ylabel("Усиление, дБ")
plt.grid(True, which="both")
plt.title("АЧХ (автоснятие с RP2040 DDS)")
plt.tight_layout()
plt.show()
def main() -> int:
ap = argparse.ArgumentParser(description="Автоснятие АЧХ через RP2040 DDS")
ap.add_argument("--port", required=True, help="например /dev/ttyACM0 или COM5")
ap.add_argument("--f0", type=float, default=10.0)
ap.add_argument("--f1", type=float, default=100000.0)
ap.add_argument("--n", type=int, default=25)
ap.add_argument("--dwell", type=int, default=150, help="мс на точку")
ap.add_argument("--uin", type=float, default=500.0,
help="входное напряжение (размах), мВ")
ap.add_argument("--out", default="achh.csv")
ap.add_argument("--plot", action="store_true")
args = ap.parse_args()
ser = open_port(args.port)
time.sleep(0.5)
ser.reset_input_buffer()
try:
points = collect_sweep(ser, args.f0, args.f1, args.n, args.dwell, args.uin)
finally:
ser.close()
if not points:
print("точек не получено", file=sys.stderr)
return 1
write_csv(args.out, points)
if args.plot:
plot(points)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+150
View File
@@ -0,0 +1,150 @@
# Бланк отчёта — интегратор/дифференциатор на ОУ
Лабораторная `lab5-peredatochnaya-funkciya-na-ou`, 4 ч.
ФИО / группа / вариант: _____________________________
> Файл заполняется студентом. Все значения — с единицами измерения.
---
## 1. Параметры варианта
| Параметр | Значение |
|---|---|
| R1 | кОм |
| R2 | кОм |
| C1 | мкФ |
| U_пит | ± В |
**Расчёт (до эксперимента):**
- K = R2/R1 = _____
- τ = R2·C1 = _____ мс
- f_c = 1/(2π·τ) = _____ Гц
---
## 2. Симуляция: АЧХ/ФЧХ (задание 1)
Директива: `.ac dec 20 1 100k`
| Параметр | Расчёт | Из симуляции | Расхождение |
|---|---|---|---|
| Усиление на плоском участке, дБ | | | |
| f_c, Гц | | | |
| Наклон за f_c, дБ/дек | −20 | | |
Вставить графики АЧХ и ФЧХ ↓
```
(место для скриншота LTSpice)
```
---
## 3. Monte Carlo (задание 2)
Директивы:
```
.step param tol ...
```
Число прогонов: ____
| Параметр | Среднее | СКО | Мин | Макс | Доля в ±10 % |
|---|---|---|---|---|---|
| K | | | | | |
| f_c, Гц | | | | | |
Гистограммы ↓
```
(место для гистограмм)
```
**Вывод о вкладе компонентов:**
-
---
## 4. Железо: переходные процессы (задание 3)
### 4.1. Интегратор (прямоугольник 1 кГц)
| Параметр | Расчёт | Измерение |
|---|---|---|
| Размах пилы U_m, В | | |
| Частота входного сигнала, кГц | 1,0 | |
Осциллограмма ↓
```
(место для осциллограммы)
```
### 4.2. Дифференциатор (пила 1 кГц)
| Параметр | Расчёт | Измерение |
|---|---|---|
| Амплитуда импульса, В | | |
### 4.3. Роль резистора утечки
Что меняется без R2: ______________________________
---
## 5. Железо: автоснятие АЧХ (задание 4)
Команда запуска:
```
python 02-izmerenie-achh.py --port ____ --f0 10 --f1 100000 --n 25 --dwell 150 --uin ___ --out achh.csv
```
Таблица точек (первые 5 и последние 5):
| f, Гц | U_вых, мВ | K, дБ |
|---|---|---|
| | | |
| | | |
Совмещённый график (расчёт / модель / эксперимент) ↓
```
(место для графика)
```
---
## 6. Сводная таблица (задание 5)
| Параметр | Расчёт | Симуляция | Железо |
|---|---|---|---|
| K | | | |
| τ, мс | | | |
| f_c, Гц | | | |
**Причины расхождений:**
1. Расчёт vs симуляция: ______________________________
2. Симуляция vs железо: ______________________________
3. Роль Monte Carlo: ______________________________
---
## 7. Выводы
-
-
-
---
## 8. Ответы на контрольные вопросы
1.
2.
3.