139 lines
5.0 KiB
Python
139 lines
5.0 KiB
Python
#!/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())
|