#!/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 <- 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())