Files

139 lines
5.0 KiB
Python
Raw Permalink Normal View History

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