
Если вы уже работали с векторным анализатором NanoVNA, то скорее всего использовали для этого экран и стилус, а также программу NanoVNA-Saver. Эти способы я рассмотрел в статье «Векторный анализатор NanoVNA для радиолюбителей» и других статьях, посвящённых NanoVNA.
Но есть ещё одна полезная возможность — создание собственных программ для обмена данными с NanoVNA через порт USB. Это даёт огромные возможности для автоматизации, анализа и интеграции измерений.
Например, можно автоматически, без участия оператора, снимать S-параметры (S11, S21) для набора образцов, антенн, фильтров, кабелей и других устройств. Программа может измерять длину кабеля, определять место повреждения и КСВ. Также становится доступным отслеживание характеристик во времени.
Ваши программы могут выполнять обработку, недоступную в таких программах, как NanoVNASaver. Также вы можете использовать NanoVNA как часть измерительного комплекса, интегрируя анализатор с системами сбора данных.
Я подготовил несколько программ, управляющих NanoVNA на языке Python. Вы сможете запускать их на компьютере с ОС Microsoft Windows 11, а также на платформе Raspberry Pi 3 B+ и других аналогичных платформах.
В статье описаны следующие программы для Microsoft Windows:
nanovna-find-port.py — программа автоматического поиска порта USB, к которому подключен NanoVNA;
nanovna-calibrate.py — программа калибровки NanoVNA;
nanovna-s21-gain.py — получение АЧХ режекторного FM-фильтра;
nanovna-cable_measurement.py — определение длины коаксиального кабеля
Версии этих же программ для ОС Raspbian (Raspberry Pi OS для новых моделей Raspberry Pi) представлены ниже:
Заметим, что такие платформы, как Raspberry Pi, удобны для создания систем автоматизации. Микрокомпьютер может не только обмениваться данными с NanoVNA, но и управлять различными внешними устройствами через порт GPIO.
Команды для работы с NanoVNA
Векторный анализатор NanoVNA обменивается данными с компьютером через порт USB. Для анализатора NanoVNA-H 4 используются текстовые команды с параметрами, описанные в документе «NanoVNA Console Commands». Также можно изучить обсуждение «List of NanoVNA Console Commands».
Все программы, приведённые в этой статье, были отлажены на анализаторе NanoVNA-H 4. Для NanoVNA V2 описание команд приведено в разделе "6 - Appendix II – USB data interface" документа "User Manual - NanoVNA V2".
Поиск нужного порта USB
Перед началом работы программе нужно определить порт USB, к которому подключен анализатор NanoVNA. Процедуры поиска немного различаются для платформ Microsoft Windows и Raspberry Pi.
Определение порта NanoVNA в Microsoft Windows
Для поиска порта USB, к которому подключен NanoVNA в среде Microsoft Windows 11, я подготовил программу nanovna-find-port.py:
import serial.tools.list_ports
import serial
import time
def find_nanovna_auto():
print("Автопоиск NanoVNA...")
for port in serial.tools.list_ports.comports():
try:
with serial.Serial(port.device, 115200, timeout=1) as ser:
time.sleep(2)
ser.write(b'version\r\n')
time.sleep(0.5)
response = ser.read(100).decode('ascii', errors='ignore')
if 'nanovna' in response.lower() or 'ch>' in response:
return port.device
except:
continue
return None
if __name__ == "__main__":
port = find_nanovna_auto()
if port:
print(f"Используется порт: {port}")
else:
print("Проверьте подключение NanoVNA")
Поиск нужного порта выполняет функция find_nanovna_auto. Она подключается последовательно ко всем доступным портам на скорости 115200 бод, отправляя в них тестовую команду version. Данная команда возвращает версию прошивки NanoVNA, а также информацию об устройстве.
В ответе команды должно присутствовать ключевое слово «nanovna» в любом регистре или строка «ch>» — приглашение командной строки NanoVNA. Если эти ключевые слова найдены, функция find_nanovna_auto возвращает найденный порт, который затем выводится на консоль:
$ python nanovna-find-port.py
Автопоиск NanoVNA...
Используется порт: COM3
Итак, программа определила, к какому порту подключен векторный анализатор NanoVNA. Теперь нужно выполнить калибровку анализатора, чтобы получить верные результаты измерений.
Определение порта в ОС Raspbian на платформе Raspberry Pi
У микрокомпьютера Raspberry Pi имеются четыре порта USB, к одному из которых можно подключить NanoVNA (рис. 1).

Программа nanovna-find-port-rpi.py, позволяющая автоматически находить порт USB анализатора NanoVNA, представлена ниже:
import serial.tools.list_ports
import serial
import time
import subprocess
import os
def find_nanovna_auto():
print("Автопоиск NanoVNA на Raspberry Pi...")
# Проверяем доступные порты
ports = list(serial.tools.list_ports.comports())
if not ports:
print("Не найдено последовательных портов")
return None
print(f"Найдено портов: {len(ports)}")
for port in ports:
print(f"Проверка порта: {port.device} - {port.description}")
# Пропускаем порты, которые вряд ли будут NanoVNA
if 'ttyAMA' in port.device or 'ttyS' in port.device:
print(f"Пропуск системного порта: {port.device}")
continue
try:
with serial.Serial(port.device, 115200, timeout=1) as ser:
print(f"Подключение к {port.device}...")
time.sleep(2) # Даем время для инициализации
# Очищаем буферы
ser.reset_input_buffer()
ser.reset_output_buffer()
# Отправляем команду
ser.write(b'version\r\n')
time.sleep(0.5)
# Читаем ответ
response = ser.read(100).decode('ascii', errors='ignore')
print(f" Ответ: {response.strip()}")
# Проверяем признаки NanoVNA
if any(keyword in response.lower() for keyword in ['nanovna', 'ch>', 'version']):
return port.device
except serial.SerialException as e:
print(f"Ошибка подключения к {port.device}: {e}")
continue
except Exception as e:
print(f"Общая ошибка на {port.device}: {e}")
continue
return None
def check_usb_permissions():
print("Проверка прав доступа...")
# Проверяем, есть ли пользователь в группе dialout
try:
groups = subprocess.check_output(['groups'], text=True).strip().split()
if 'dialout' in groups:
print("Пользователь в группе dialout")
else:
print("Пользователь не в группе dialout. Добавьте: sudo usermod -a -G dialout $USER")
except:
print("Не удалось проверить группы пользователя")
def list_available_ports():
print("\nДоступные последовательные порты:")
ports = list(serial.tools.list_ports.comports())
for i, port in enumerate(ports):
print(f" {i+1}. {port.device} - {port.description}")
return ports
if __name__ == "__main__":
check_usb_permissions()
list_available_ports()
port = find_nanovna_auto()
if port:
print(f"\nНайден NanoVNA на порту: {port}")
else:
print(f"\nNanoVNA не найден")
Получив управление, функция find_nanovna_auto получает список доступных портов:
ports = list(serial.tools.list_ports.comports())
После пропуска системных портов программа очищает буферы и выдает в NanoVNA команду version:
ser.reset_input_buffer()
ser.reset_output_buffer()
ser.write(b'version\r\n')
Если ответ команды похож на ответ NanoVNA, программа считает, что порт найден.
Функция check_usb_permissions проверяет наличие текущего пользователя в группе dialout. Если он там есть, это позволяет запускать программу с правами обычного пользователя.
Перед запуском программы nanovna-find-port-rpi.py установите зависимости:
sudo apt update
sudo apt install python3-pip
sudo apt install python3-serial
При запуске программа nanovna-find-port-rpi.py выведет на консоль результаты поиска:
$ python3 nanovna-find-port-rpi.py
Проверка прав доступа...
Пользователь в группе dialout
Доступные последовательные порты:
1. /dev/ttyACM0 - NanoVNA-H4
Автопоиск NanoVNA на Raspberry Pi...
Найдено портов: 1
Проверка порта: /dev/ttyACM0 - NanoVNA-H4
Подключение к /dev/ttyACM0...
Ответ: version
1.2.43
ch>
Найден NanoVNA на порту: /dev/ttyACM0
Найденную строку порта, такую как /dev/ttyACM0, мы будем использовать в других программах, работающих с NanoVNA в среде ОС Raspbian.
Работа с NanoVNA через терминальную программу
Прежде чем отлаживать программы, работающие с NanoVNA, вы можете попробовать выдать некоторые команды вручную с помощью терминальных программ PuTTY (в ОС Microsoft Windows) или minicom (ОС Raspbian).
Работа в ОС Microsoft Windows c PuTTY
После того как вы определили, к какому порту подключен анализатор, введите в системном приглашении Windows такую команду:
mode COM3: BAUD=115200 PARITY=n DATA=8 STOP=1
Далее запустите PuTTY. В списке Connection type выберите Serial. Далее в поле Serial line укажите порт (у меня это COM3), а в поле Speed — значение 115200 (рис. 2).

Чтобы подключиться к NanoVNA, щёлкните кнопку Open. Появится окно, где вы можете ввести такие команды, как help, info и version (рис. 3).

Вы можете тестировать здесь и другие команды, однако соблюдайте осторожность и внимательно почитайте описание команд, прежде чем их использовать.
Работа в ОС Raspbian с minicom
Для работы с NanoVNA через терминал в ОС Raspbian установите программу minicom и запустите её следующим образом:
$ sudo apt install minicom
$ minicom -D /dev/ttyACM0 -b 115200
Здесь нужно указать порт USB, к которому подключен NanoVNA.
После запуска в окне консоли появится сообщение:
Welcome to minicom 2.8
OPTIONS: I18n
Port /dev/ttyACM0, 13:03:04
Press CTRL-A Z for help on special keys
Вводите здесь такие команды help, version, info и вы увидите их ответы:
help
Commands: scan scan_bin data frequencies freq sweep power offset bandwidth time sd_list sd_read sd_r
ch> version
1.2.43
ch> info
Board: NanoVNA-H 4
2019-2025 Copyright NanoVNA.com
based on @DiSlord @edy555 ... source
Licensed under GPL.
Version: 1.2.43 [p:401, IF:12k, ADC:384k, Lcd:480x320]
Build Time: Feb 19 2025 - 21:33:35
Architecture: ARMv7E-M Core Variant: Cortex-M4F
Platform: STM32F303xC Analog & DSP
ch>
Для завершения работы minicom нажмите комбинацию клавиш Ctrl+A, а затем клавишу X.
Программа калибровки NanoVNA
В статье «Векторный анализатор NanoVNA для радиолюбителей» я рассказал о необходимости выполнения калибровки векторного анализатора NanoVNA перед выполнением измерений. Там же я подробно рассмотрел выполнение этой процедуры с помощью экрана анализатора и стилуса, а также программы NanoVNASaver с графическим интерфейсом.
Если вы решили создать собственное программное обеспечение (ПО) для анализатора, то калибровку можно выполнить программой nanovna-calibrate.py:
import serial
import time
PORT = "COM3"
BAUDRATE = 115200
def send_command(ser, cmd, wait=0.2):
ser.write((cmd + '\r\n').encode('utf-8'))
time.sleep(wait)
return ser.read_all().decode('utf-8', errors='ignore').strip()
def calibrate(ser):
print("\nНачало процедуры калибровки NanoVNA-H4")
start_freq = 50_000
stop_freq = 1_500_000_000
points = 201
print(f"Установка диапазона: {start_freq/1e3:.1f} кГц – {stop_freq/1e6:.1f} МГц, {points} точек")
send_command(ser, f"sweep {start_freq} {stop_freq} {points}")
print(send_command(ser, "frequencies")[:200] + "...")
print("Сброс текущей калибровки")
print(send_command(ser, "cal reset"))
input("\nПодключите OPEN (открытый порт PORT1) и нажмите Enter...")
print(send_command(ser, "cal open"))
input("Подключите SHORT (замыкание на PORT1) и нажмите Enter...")
print(send_command(ser, "cal short"))
input("Подключите LOAD (50Ω) к PORT1 и нажмите Enter...")
print(send_command(ser, "cal load"))
input("Соедините PORT1 и PORT2 (THRU) и нажмите Enter...")
print(send_command(ser, "cal thru"))
print("\nЗавершение и расчёт калибровки")
print(send_command(ser, "cal done"))
print("Сохранение в слот 0")
print(send_command(ser, "save 0"))
print("\nКалибровка успешно выполнена и сохранена (слот 0)")
def main():
print(f"Подключение к NanoVNA-H4 через {PORT}...")
with serial.Serial(PORT, BAUDRATE, timeout=0.5) as ser:
time.sleep(1.0)
version = send_command(ser, "version")
print("Версия прошивки:", version or "Нет ответа")
calibrate(ser)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\nОперация прервана пользователем.")
except serial.SerialException as e:
print(f"Ошибка доступа к {PORT}: {e}")
Указание порта для подключения анализатора
В тексте программы указан порт COM3. Перед запуском укажите порт, к которому подключен ваш анализатор.
Указание диапазона калибровки и количества точек сканирования
Также в программе задаётся диапазон калибровки от 50 кГц до 1500 МГц и количество точек сканирования, равное 201:
start_freq = 50_000
stop_freq = 1_500_000_000
points = 201
Вы можете изменить эти значения на те, которые необходимы для ваших измерений.
Сразу после запуска программа nanovna-calibrate.py выдаёт в NanoVNA команду sweep, чтобы установить диапазон сканирования и количество точек. Этой команде необходимо указать начальную и конечную частоту сканирования, а также количество точек сканирования в следующем формате:
sweep <start_freq> <stop_freq> <points>
Проверка корректности настройки
Далее для проверки корректности настройки программа запрашивает список частот сканирования командой frequencies и выводит первые 200 символов списка. Он содержит по одному значению частоты на каждую строку.
Если всё правильно, ответ команды frequencies не будет пустым. На консоли должны появиться числовые значения частот в Гц. При этом первые частоты должны соответствовать установленной стартовой частоте start_freq.
Сброс текущей калибровки и новая калибровка
После вывода списка частот сканирования программа сбрасывает текущую калибровку командой cal reset и выводит на консоль инструкции по подключению эталонов для калибровки. Калибровка выполняется командами cal open, cal short, cal load и cal thru.
Завершение калибровки и сохранение её результатов
Для завершения и расчета калибровки выдается команда cal done, а для сохранения результатов калибровки в слоте с номером 0 — команда save 0.
Запуск программы nanovna-calibrate.py
Команда запуска программы nanovna-calibrate.py и её диалога на консоли показана ниже:
$ python nanovna-calibrate.py
Подключение к NanoVNA-H4 через COM3...
Версия прошивки: version
1.2.43
ch>
Начало процедуры калибровки NanoVNA-H4
Установка диапазона: 50.0 кГц – 1500.0 МГц, 201 точек
frequencies
50000
7549750
15049500
22549250
30049000
37548750
45048500
52548250
60048000
67547750
75047500
82547250
90047000
97546750
105046500
112546250
120046000
127545750
1350455...
Сброс текущей калибровки
cal reset
Подключите OPEN (открытый порт PORT1) и нажмите Enter...
ch> cal open
Подключите SHORT (замыкание на PORT1) и нажмите Enter...
ch> cal short
Подключите LOAD (50Ω) к PORT1 и нажмите Enter...
ch> cal load
Соедините PORT1 и PORT2 (THRU) и нажмите Enter...
ch> cal thru
Завершение и расчёт калибровки
ch> cal done
Сохранение в слот 0
ch> save 0
Калибровка успешно выполнена и сохранена (слот 0)
Запустив программу, подключайте по очереди к портам NanoVNA необходимые эталоны. После выполнения калибровки можно переходить к измерениям.
Версия программы калибровки для ОС Raspbian
В файле nanovna-calibrate-rpi.py вы найдёте версию программы калибровки NanoVNA для работы в ОС Raspbian:
#!/usr/bin/env python3
import serial
import time
def simple_calibrate():
ser = serial.Serial('/dev/ttyACM0', 115200, timeout=1)
time.sleep(2)
steps = [
"cal reset",
"cal open",
"cal short",
"cal load",
"cal thru",
"cal done",
"save 0"
]
prompts = [
"Сброс... (Enter)",
"OPEN подключен? (Enter)",
"SHORT подключен? (Enter)",
"LOAD подключен? (Enter)",
"THRU подключен? (Enter)",
"Расчет... (Enter)",
"Сохранение... (Enter)"
]
for i, (cmd, prompt) in enumerate(zip(steps, prompts)):
input(f"{i+1}/7 {prompt}")
ser.write((cmd + '\r\n').encode())
time.sleep(1)
print(ser.read(100).decode())
ser.close()
print("Готово!")
if __name__ == "__main__":
simple_calibrate()
После запуска программа открывает соединение с портом, к которому подключено устройство NanoVNA:
ser = serial.Serial('/dev/ttyACM0', 115200, timeout=1)
Далее программа выполняет сброс калибровки и предлагает подключать по очереди эталоны, нажимая после каждого подключения клавишу Enter:
$ python3 nanovna-calibrate-rpi.py
1/7 Сброс... (Enter)
cal reset
ch>
2/7 OPEN подключен? (Enter)
cal open
3/7 SHORT подключен? (Enter)
ch> cal short
4/7 LOAD подключен? (Enter)
ch> cal load
5/7 THRU подключен? (Enter)
ch> cal thru
6/7 Расчет... (Enter)
ch> cal done
ch>
7/7 Сохранение... (Enter)
save 0
ch>
Готово!
Результаты калибровки сохраняются в слоте 0.
Проверка режекторного FM-фильтра
В статье «Улучшаем качество приёма с помощью фильтров и малошумящих усилителей» я привёл результаты исследования NOTCH-фильтра (заградительного фильтра), подавляющего сигналы FM-радио (рис. 4), полученные с помощью программы NanoVNASaver.

Программа сканирования nanovna-s21-gain.py
Вы можете создавать программы для подобного сканирования самостоятельно. Для сканирования режекторного FM-фильтра я подготовил программу nanovna-s21-gain.py:
Посмотреть код программы
import serial
import matplotlib.pyplot as plt
import numpy as np
import time
def send_command(ser, command, wait_time=0.5):
print(f"Отправка команды: {command}")
ser.write((command + '\r\n').encode())
time.sleep(wait_time)
response = b''
start_time = time.time()
while time.time() - start_time < wait_time:
if ser.in_waiting > 0:
response += ser.read(ser.in_waiting)
time.sleep(0.01)
return response.decode('ascii', errors='ignore')
def setup_nanovna(ser, cal_slot=0):
print("Настройка NanoVNA...")
commands = [
f"cal load {cal_slot}",
"sweep 30000000 250000000 101",
"pause",
]
for cmd in commands:
response = send_command(ser, cmd, 0.5)
if response:
response_clean = response.replace('ch>', '').strip()
if response_clean:
print(f"Ответ на {cmd}: {response_clean}")
cal_status = send_command(ser, "cal", 0.5)
if cal_status:
print(f"Статус калибровки: {cal_status}")
time.sleep(1)
def get_nanovna_data(ser):
print("Получение данных S21...")
send_command(ser, "resume", 2)
freq_data = send_command(ser, "frequencies", 1)
print(f"Получено данных частот: {len(freq_data)} байт")
s21_data = send_command(ser, "data 1", 1)
print(f"Получено данных S21: {len(s21_data)} байт")
return freq_data, s21_data
def parse_frequency_data(data):
frequencies = []
lines = data.strip().split('\n')
for line in lines:
line = line.strip()
if line and not line.startswith('ch>'):
try:
parts = line.split()
for part in parts:
freq = float(part)
frequencies.append(freq)
except ValueError:
continue
return frequencies
def parse_s21_data(data):
"""Парсинг данных S21 (комплексные числа)"""
s21_points = []
lines = data.strip().split('\n')
for line in lines:
line = line.strip()
if line and not line.startswith('ch>'):
try:
# Данные S21 в формате: real imag
parts = line.split()
if len(parts) >= 2:
real = float(parts[0])
imag = float(parts[1])
s21_points.append((real, imag))
except ValueError:
continue
return s21_points
def calculate_s21_db(s21_points):
s21_db = []
for real, imag in s21_points:
magnitude = np.sqrt(real**2 + imag**2)
if magnitude > 0:
db = 20 * np.log10(magnitude)
else:
db = -120 # Минимальное значение
s21_db.append(db)
return s21_db
def plot_filter_response(frequencies, s21_db):
if not frequencies or not s21_db:
print("Недостаточно данных для построения графика")
return
min_len = min(len(frequencies), len(s21_db))
frequencies = frequencies[:min_len]
s21_db = s21_db[:min_len]
frequencies_mhz = [f / 1e6 for f in frequencies]
plt.figure(figsize=(12, 8))
plt.plot(frequencies_mhz, s21_db, 'b-', linewidth=2, label='S21 (Transmission)')
plt.title('АЧХ режекторного FM фильтра\nNanoVNA-H4 (с калибровкой)', fontsize=14, fontweight='bold')
plt.xlabel('Частота (МГц)', fontsize=12)
plt.ylabel('S21 (дБ)', fontsize=12)
plt.grid(True, alpha=0.3)
fm_start, fm_end = 87.5, 108
plt.axvspan(fm_start, fm_end, alpha=0.2, color='red', label='FM диапазон')
plt.axvline(fm_start, color='red', linestyle='--', alpha=0.7)
plt.axvline(fm_end, color='red', linestyle='--', alpha=0.7)
min_db_index = np.argmin(s21_db)
min_freq = frequencies_mhz[min_db_index]
min_db = s21_db[min_db_index]
plt.plot(min_freq, min_db, 'ro', markersize=8,
label=f'Подавление: {min_freq:.1f} МГц, {min_db:.1f} дБ')
plt.xlim(min(frequencies_mhz), max(frequencies_mhz))
plt.ylim(min(s21_db) - 5, max(s21_db) + 5)
plt.xticks(rotation=45)
from matplotlib.ticker import FuncFormatter
def format_freq(x, pos):
if x >= 1000:
return f'{x/1000:.0f}00'
else:
return f'{x:.0f}'
plt.gca().xaxis.set_major_formatter(FuncFormatter(format_freq))
plt.legend(fontsize=10)
plt.tight_layout()
plt.show()
print(f"\n=== РЕЗУЛЬТАТЫ ИЗМЕРЕНИЯ ===")
print(f"Диапазон: {min(frequencies_mhz):.1f} - {max(frequencies_mhz):.1f} МГц")
print(f"Точка подавления: {min_freq:.2f} МГц")
print(f"Глубина подавления: {min_db:.1f} дБ")
print(f"FM диапазон: {fm_start} - {fm_end} МГц")
# Проверяем эффективность подавления в FM диапазоне
fm_indices = [i for i, f in enumerate(frequencies_mhz) if fm_start <= f <= fm_end]
if fm_indices:
fm_attenuation = [s21_db[i] for i in fm_indices]
avg_fm_attenuation = np.mean(fm_attenuation)
print(f"Среднее подавление в FM диапазоне: {avg_fm_attenuation:.1f} дБ")
def main():
ser = None
try:
print("Подключение к NanoVNA-H4 на COM3...")
ser = serial.Serial(
port='COM3',
baudrate=115200,
timeout=2,
write_timeout=2,
)
time.sleep(2)
setup_nanovna(ser, cal_slot=0)
freq_data, s21_data = get_nanovna_data(ser)
frequencies = parse_frequency_data(freq_data)
s21_points = parse_s21_data(s21_data)
s21_db = calculate_s21_db(s21_points)
print(f"\nОбработано {len(frequencies)} частот и {len(s21_points)} точек S21")
if frequencies and s21_db:
plot_filter_response(frequencies, s21_db)
else:
print("Не удалось получить данные для построения графика")
except Exception as e:
print(f"Ошибка: {e}")
import traceback
traceback.print_exc()
finally:
if ser and ser.is_open:
ser.close()
print("\nСоединение закрыто")
if __name__ == "__main__":
main()Инициализация
Программа nanovna-s21-gain.py открывает порт COM3 (замените на тот, который используется у вас для подключения NanoVNA), а затем ожидает завершения инициализации:
ser = serial.Serial(
port='COM3',
baudrate=115200,
timeout=2,
write_timeout=2,
)
time.sleep(2)
Настройка и загрузка калибровки
Далее вызывается функция настройки setup_nanovna, загружающая калибровку из слота 0:
setup_nanovna(ser, cal_slot=0)
Эта функция выдаёт по очереди три команды:
commands = [
f"cal load {cal_slot}",
"sweep 30000000 1500000000 101",
"pause",
]
Команда cal load загружает калибровку из слота, номер которого передаётся ей в качестве параметра. Далее команда sweep задаёт диапазон и количество точек сканирования. Затем вызывается команда pause для ожидания завершения измерений.
Проверка статуса калибровки
Также функция setup_nanovna проверяет статус калибровки:
cal_status = send_command(ser, "cal", 0.5)
if cal_status:
print(f"Статус калибровки: {cal_status}")
time.sleep(1)
Команда cal без параметров запрашивает состояние калибровки. При вызове команды cal функция send_command ожидает 0.5 сек.
Получение данных сканирования
Для получения данных сканирования программа nanovna-s21-gain.py вызывает функцию get_nanovna_data:
freq_data, s21_data = get_nanovna_data(ser)
Эта функция выдает команду resume для запуска однократного сканирования и дожидается его завершения в течение 2 секунд:
send_command(ser, "resume", 2)
Далее командой frequencies функция получает список частот сканирования:
freq_data = send_command(ser, "frequencies", 1)
После этого устройству передаётся команда data 1 для получения данных S21:
s21_data = send_command(ser, "data 1", 1)
Параметр 1 команды data означает необходимость получения данных S21.
После получения данных функция get_nanovna_data возвращает список частот сканирования и данные S21:
return freq_data, s21_data
Парсинг полученных данных и пересчет в децибелы
Далее программа nanovna-s21-gain.py выполняет парсинг полученных данных и пересчет в децибелы:
frequencies = parse_frequency_data(freq_data)
s21_points = parse_s21_data(s21_data)
s21_db = calculate_s21_db(s21_points)
Построение графика АЧХ
Для построения графика АЧХ вызывается функция plot_filter_response.
Запуск программы nanovna-s21-gain.py
Программа nanovna-s21-gain.py запускается следующим образом:
$ python nanovna-s21-gain.py
Подключение к NanoVNA-H4 на COM3...
Настройка NanoVNA...
Отправка команды: cal load 0
Ответ на cal load 0: cal load 0
Отправка команды: sweep 30000000 250000000 101
Ответ на sweep 30000000 250000000 101: sweep 30000000 250000000 101
Отправка команды: pause
Ответ на pause: pause
Отправка команды: cal
Статус калибровки: cal
load
ch>
Получение данных S21...
Отправка команды: resume
Отправка команды: frequencies
Получено данных частот: 1096 байт
Отправка команды: data 1
Получено данных S21: 2625 байт
Обработано 101 частот и 101 точек S21
Она выводит на консоль отправляемые команды и полученные от них данные, после чего рисует АЧХ режекторного фильтра в графическом виде (рис. 5).

Как видно из графика, полоса подавления проверенного режекторного фильтра заметно шире полосы FM-радио и заходит в область более высоких частот.
Версия программы сканирования фильтра для ОС Raspbian
На рис. 6 показано подключение режекторного фильтра к NanoVNA, соединённого кабелем USB с микрокомпьютером Raspberry Pi.

Программу сканирования режекторного фильтра для OC Raspbian можно найти в файле nanovna-s21-gain-rpi.py:
Посмотреть код программы
import serial
import matplotlib
matplotlib.use('Agg') # Используем бэкенд без GUI
import matplotlib.pyplot as plt
import numpy as np
import time
import os
from datetime import datetime
def send_command(ser, command, wait_time=0.5):
print(f"Отправка команды: {command}")
ser.write((command + '\r\n').encode())
time.sleep(wait_time)
response = b''
start_time = time.time()
while time.time() - start_time < wait_time:
if ser.in_waiting > 0:
response += ser.read(ser.in_waiting)
time.sleep(0.01)
return response.decode('ascii', errors='ignore')
def setup_nanovna(ser, cal_slot=0):
print("Настройка NanoVNA...")
commands = [
f"cal load {cal_slot}",
"sweep 30000000 250000000 101",
"pause",
]
for cmd in commands:
response = send_command(ser, cmd, 0.5)
if response:
response_clean = response.replace('ch>', '').strip()
if response_clean:
print(f"Ответ на {cmd}: {response_clean}")
cal_status = send_command(ser, "cal", 0.5)
if cal_status:
print(f"Статус калибровки: {cal_status}")
time.sleep(1)
def get_nanovna_data(ser):
send_command(ser, "resume", 2)
freq_data = send_command(ser, "frequencies", 1)
print(f"Получено данных частот: {len(freq_data)} байт")
s21_data = send_command(ser, "data 1", 1)
print(f"Получено данных S21: {len(s21_data)} байт")
return freq_data, s21_data
def parse_frequency_data(data):
frequencies = []
lines = data.strip().split('\n')
for line in lines:
line = line.strip()
if line and not line.startswith('ch>'):
try:
parts = line.split()
for part in parts:
freq = float(part)
frequencies.append(freq)
except ValueError:
continue
return frequencies
def parse_s21_data(data):
s21_points = []
lines = data.strip().split('\n')
for line in lines:
line = line.strip()
if line and not line.startswith('ch>'):
try:
# Данные S21 в формате: real imag
parts = line.split()
if len(parts) >= 2:
real = float(parts[0])
imag = float(parts[1])
s21_points.append((real, imag))
except ValueError:
continue
return s21_points
def calculate_s21_db(s21_points):
s21_db = []
for real, imag in s21_points:
magnitude = np.sqrt(real**2 + imag**2)
if magnitude > 0:
db = 20 * np.log10(magnitude)
else:
db = -120 # Минимальное значение
s21_db.append(db)
return s21_db
def save_filter_response(frequencies, s21_db, filename=None):
if not frequencies or not s21_db:
print("Недостаточно данных для построения графика")
return None
min_len = min(len(frequencies), len(s21_db))
frequencies = frequencies[:min_len]
s21_db = s21_db[:min_len]
frequencies_mhz = [f / 1e6 for f in frequencies]
# Создаем папку для результатов если её нет
results_dir = "/home/frolov"
if not os.path.exists(results_dir):
os.makedirs(results_dir)
# Генерируем имя файла с временной меткой
if filename is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"filter_response_{timestamp}.png"
filepath = os.path.join(results_dir, filename)
plt.figure(figsize=(12, 8))
plt.plot(frequencies_mhz, s21_db, 'b-', linewidth=2, label='S21 (Transmission)')
plt.title('АЧХ режекторного FM фильтра\nNanoVNA-H4 (с калибровкой)', fontsize=14, fontweight='bold')
plt.xlabel('Частота (МГц)', fontsize=12)
plt.ylabel('S21 (дБ)', fontsize=12)
plt.grid(True, alpha=0.3)
fm_start, fm_end = 87.5, 108
plt.axvspan(fm_start, fm_end, alpha=0.2, color='red', label='FM диапазон')
plt.axvline(fm_start, color='red', linestyle='--', alpha=0.7)
plt.axvline(fm_end, color='red', linestyle='--', alpha=0.7)
min_db_index = np.argmin(s21_db)
min_freq = frequencies_mhz[min_db_index]
min_db = s21_db[min_db_index]
plt.plot(min_freq, min_db, 'ro', markersize=8,
label=f'Подавление: {min_freq:.1f} МГц, {min_db:.1f} дБ')
plt.xlim(min(frequencies_mhz), max(frequencies_mhz))
plt.ylim(min(s21_db) - 5, max(s21_db) + 5)
plt.xticks(rotation=45)
from matplotlib.ticker import FuncFormatter
def format_freq(x, pos):
if x >= 1000:
return f'{x/1000:.0f}00'
else:
return f'{x:.0f}'
plt.gca().xaxis.set_major_formatter(FuncFormatter(format_freq))
plt.legend(fontsize=10)
plt.tight_layout()
# Сохраняем график
plt.savefig(filepath, dpi=150, bbox_inches='tight')
plt.close()
print(f"График сохранен как: {filepath}")
# Сохраняем данные в текстовый файл
data_filename = filename.replace('.png', '.txt')
data_filepath = os.path.join(results_dir, data_filename)
with open(data_filepath, 'w') as f:
f.write("Частота (МГц)\tS21 (дБ)\n")
for freq, db in zip(frequencies_mhz, s21_db):
f.write(f"{freq:.3f}\t{db:.3f}\n")
print(f"Данные сохранены как: {data_filepath}")
print(f"\n=== РЕЗУЛЬТАТЫ ИЗМЕРЕНИЯ ===")
print(f"Диапазон: {min(frequencies_mhz):.1f} - {max(frequencies_mhz):.1f} МГц")
print(f"Точка подавления: {min_freq:.2f} МГц")
print(f"Глубина подавления: {min_db:.1f} дБ")
print(f"FM диапазон: {fm_start} - {fm_end} МГц")
# Проверяем эффективность подавления в FM диапазоне
fm_indices = [i for i, f in enumerate(frequencies_mhz) if fm_start <= f <= fm_end]
if fm_indices:
fm_attenuation = [s21_db[i] for i in fm_indices]
avg_fm_attenuation = np.mean(fm_attenuation)
print(f"Среднее подавление в FM диапазоне: {avg_fm_attenuation:.1f} дБ")
return filepath
def main():
ser = None
try:
ser = serial.Serial('/dev/ttyACM0', 115200, timeout=1)
print("Подключение установлено")
setup_nanovna(ser, cal_slot=0)
freq_data, s21_data = get_nanovna_data(ser)
frequencies = parse_frequency_data(freq_data)
s21_points = parse_s21_data(s21_data)
s21_db = calculate_s21_db(s21_points)
print(f"\nОбработано {len(frequencies)} частот и {len(s21_points)} точек S21")
if frequencies and s21_db:
plot_filename = save_filter_response(frequencies, s21_db)
print(f"\nИзмерение завершено. Результаты сохранены в: {plot_filename}")
else:
print("Не удалось получить данные для построения графика")
except Exception as e:
print(f"Ошибка: {e}")
import traceback
traceback.print_exc()
finally:
if ser and ser.is_open:
ser.close()
if __name__ == "__main__":
main()После запуска программа открывает соединение с портом /dev/ttyACM0 и вызывает фукнцию инициализации setup_nanovna. Эта функция выдаёт последовательно три команды, предназначенные для загрузки калибровки из слота 0 (команда cal load), установки диапазона сканирования (команда sweep) и остановки режима непрерывного сканирования (pause):
commands = [
f"cal load {cal_slot}",
"sweep 30000000 250000000 101",
"pause",
]
Также эта функция получает и выводит на консоль статус калибровки, вызывая для этого команду cal без параметров.
Результаты сканирования считываются из анализатора функцией get_nanovna_data. При этом в устройство выдаются команды resume, frequencies и data 1.
Для парсинга данных вызываются функции parse_frequency_data и parse_s21_data. Функция calculate_s21_db пересчитывает данные в децибелы.
Обработанные данные передаются функции save_filter_response. Она сохраняет график и текстовые данные в каталог /home/frolov (не забудьте подставить здесь путь к вашему каталогу).
При запуске программа выводит результаты сканирования на консоль:
$ python3 nanovna-s21-gain-rpi.py
Подключение установлено
Настройка NanoVNA...
Отправка команды: cal load 0
Ответ на cal load 0: cal load 0
Отправка команды: sweep 30000000 250000000 101
Ответ на sweep 30000000 250000000 101: sweep 30000000 250000000 101
Отправка команды: pause
Ответ на pause: pause
Отправка команды: cal
Статус калибровки: cal
load
ch>
Получение данных S21...
Отправка команды: resume
Отправка команды: frequencies
Получено данных частот: 1096 байт
Отправка команды: data 1
Получено данных S21: 2624 байт
Обработано 101 частот и 101 точек S21
График сохранен как: /home/frolov/filter_response_20251110_15194_
Пример созданного графического файла с результатами сканирования показан на рис. 7

В текстовый файл записываются частоты и измеренные для них значения S21 в децибелах:
Частота (МГц) S21 (дБ)
30.000 -2.028
32.200 -2.038
…
69.600 -3.506
71.800 -3.719
74.000 -3.919
76.200 -4.174
78.400 -4.922
80.600 -7.264
82.800 -11.556
85.000 -15.891
87.200 -20.147
89.400 -38.016
91.600 -53.843
93.800 -58.510
96.000 -58.180
98.200 -58.150
100.400 -58.141
102.600 -58.340
104.800 -58.879
107.000 -59.167
109.200 -59.214
111.400 -58.973
113.600 -58.702
115.800 -58.114
118.000 -58.412
120.200 -60.828
122.400 -57.835
124.600 -40.973
126.800 -28.832
129.000 -18.812
131.200 -11.578
133.400 -8.130
135.600 -6.879
137.800 -6.241
140.000 -5.751
142.200 -5.288
144.400 -4.843
146.600 -4.431
148.800 -4.038
151.000 -3.712
153.200 -3.444
…
250.000 -3.281
Исследование сборки коаксиального кабеля
В разделе «Тестирование фидера» статьи «Векторный анализатор NanoVNA для радиолюбителей» я рассказал об использовании NanoVNA для исследования кабеля RG-174. Результаты измерений были получены с помощью программы NanoVNASaver.
Программа определения длины кабеля
Если вам нужна собственная программа определения длины кабеля, то за основу вы можете взять программу nanovna-cable_measurement.py:
Посмотреть код программы
import serial
import matplotlib.pyplot as plt
import numpy as np
import time
from scipy.signal import find_peaks
import math
def send_command(ser, command, wait_time=0.5):
print(f"Отправка команды: {command}")
ser.write((command + '\r\n').encode())
time.sleep(wait_time)
response = b''
start_time = time.time()
while time.time() - start_time < wait_time:
if ser.in_waiting > 0:
response += ser.read(ser.in_waiting)
time.sleep(0.01)
return response.decode('ascii', errors='ignore')
def setup_nanovna_for_cable_measurement(ser, start_freq=1e6, stop_freq=300e6, points=201):
print("Настройка NanoVNA для измерения кабеля...")
commands = [
f"sweep {int(start_freq)} {int(stop_freq)} {points}",
"pause",
]
for cmd in commands:
response = send_command(ser, cmd, 0.5)
if response:
print(f"Ответ на {cmd}: {response.strip()}")
time.sleep(1)
def get_s11_data(ser):
print("Получение данных S11...")
send_command(ser, "resume", 2)
# Получаем данные частот
freq_data = send_command(ser, "frequencies", 1)
# Получаем данные S11
s11_data = send_command(ser, "data 0", 1) # S11 - reflection
return freq_data, s11_data
def parse_frequency_data(data):
frequencies = []
lines = data.strip().split('\n')
for line in lines:
line = line.strip()
if line and not line.startswith('ch>'):
try:
parts = line.split()
for part in parts:
freq = float(part)
frequencies.append(freq)
except ValueError:
continue
return frequencies
def parse_s11_data(data):
s11_points = []
lines = data.strip().split('\n')
for line in lines:
line = line.strip()
if line and not line.startswith('ch>'):
try:
parts = line.split()
if len(parts) >= 2:
real = float(parts[0])
imag = float(parts[1])
s11_points.append((real, imag))
except ValueError:
continue
return s11_points
def calculate_phase(s11_points):
phases = []
for real, imag in s11_points:
phase = np.arctan2(imag, real) # Фаза в радианах
phases.append(phase)
return phases
def calculate_vswr(s11_points):
vswr_values = []
for real, imag in s11_points:
magnitude = np.sqrt(real**2 + imag**2)
if magnitude < 1:
vswr = (1 + magnitude) / (1 - magnitude)
else:
vswr = 100 # Большое значение для плохого КСВ
vswr_values.append(vswr)
return vswr_values
def find_cable_length(frequencies, phases, vswr_values, vf=0.66):
"""
Определение длины кабеля по фазовому сдвигу
vf - коэффициент укорочения (velocity factor)
"""
# Находим пики в КСВ (минимумы отражения)
peaks, _ = find_peaks(-np.array(vswr_values), prominence=0.1)
if len(peaks) < 2:
print("Не удалось найти достаточное количество резонансов")
return None, None
# Берем первые два резонансных пика
peak1_idx = peaks[0]
peak2_idx = peaks[1]
freq1 = frequencies[peak1_idx]
freq2 = frequencies[peak2_idx]
# Разность частот между соседними резонансами
delta_f = abs(freq2 - freq1)
# Длина кабеля: L = c / (2 * delta_f * vf)
c = 3e8 # Скорость света м/с
cable_length = c / (2 * delta_f * vf)
# Альтернативный метод: по фазовому сдвигу
phase_slope = np.polyfit(frequencies, phases, 1)[0] # Наклон фазы
electrical_length = -phase_slope * c / (4 * np.pi * vf)
return cable_length, electrical_length, delta_f, freq1, freq2
def plot_cable_measurement(frequencies, phases, vswr_values, cable_length, delta_f):
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10))
print(f"\n=== РЕЗУЛЬТАТЫ ИЗМЕРЕНИЯ КАБЕЛЯ ===")
print(f"Разность частот между резонансами: {delta_f/1e6:.2f} МГц")
print(f"Расчетная длина кабеля: {cable_length:.2f} метров")
print(f"Длина кабеля в сантиметрах: {cable_length * 100:.1f} см")
# График 1: КСВ
frequencies_mhz = [f / 1e6 for f in frequencies]
ax1.plot(frequencies_mhz, vswr_values, 'b-', linewidth=2, label='КСВ')
ax1.set_title('КСВ кабеля', fontsize=14, fontweight='bold')
ax1.set_xlabel('Частота (МГц)', fontsize=12)
ax1.set_ylabel('КСВ', fontsize=12)
ax1.grid(True, alpha=0.3)
ax1.legend()
# График 2: Фаза
phases_deg = [p * 180 / np.pi for p in phases]
ax2.plot(frequencies_mhz, phases_deg, 'r-', linewidth=2, label='Фаза S11')
ax2.set_title('Фаза коэффициента отражения', fontsize=14, fontweight='bold')
ax2.set_xlabel('Частота (МГц)', fontsize=12)
ax2.set_ylabel('Фаза (градусы)', fontsize=12)
ax2.grid(True, alpha=0.3)
ax2.legend()
plt.tight_layout()
plt.show()
def measure_cable_with_different_vf(ser):
setup_nanovna_for_cable_measurement(ser, start_freq=1e6, stop_freq=500e6, points=501)
freq_data, s11_data = get_s11_data(ser)
frequencies = parse_frequency_data(freq_data)
s11_points = parse_s11_data(s11_data)
if not frequencies or not s11_points:
print("Не удалось получить данные")
return
phases = calculate_phase(s11_points)
vswr_values = calculate_vswr(s11_points)
# Стандартные коэффициенты укорочения для разных кабелей
cable_types = {
"RG-58": 0.66,
"RG-174": 0.66,
"RG-213": 0.66,
"LMR-400": 0.85,
"Коаксиал с полиэтиленом": 0.66,
"Коаксиал с тефлоном": 0.70,
"Воздушный коаксиал": 0.80
}
print("\n=== РЕЗУЛЬТАТЫ ДЛЯ РАЗНЫХ ТИПОВ КАБЕЛЕЙ ===")
for cable_type, vf in cable_types.items():
length, _, delta_f, freq1, freq2 = find_cable_length(frequencies, phases, vswr_values, vf)
if length:
print(f"{cable_type} (VF={vf}): {length:.2f} м")
# Используем средний коэффициент для построения графика
vf = 0.66
# Расчет длины с выбранным коэффициентом
cable_length, electrical_length, delta_f, freq1, freq2 = find_cable_length(
frequencies, phases, vswr_values, vf)
if cable_length:
plot_cable_measurement(frequencies, phases, vswr_values, cable_length, delta_f)
else:
print("Не удалось определить длину кабеля")
def main():
ser = None
try:
ser = serial.Serial(
port='COM3',
baudrate=115200,
timeout=2,
write_timeout=2,
)
time.sleep(2)
measure_cable_with_different_vf(ser)
except Exception as e:
print(f"Ошибка: {e}")
import traceback
traceback.print_exc()
finally:
if ser and ser.is_open:
ser.close()
if __name__ == "__main__":
main()Методы определения длины кабеля
Для определения длины кабеля в программе используется функция find_cable_length. Её работа основана на волновых принципах отражения сигнала в линии передачи (коаксиальном кабеле) и на связи между резонансными частотами и длиной электрической линии.
В процессе исследования в кабеле возникает интерференция отражённой и падающей волны. При этом создаются стоячие волны, а на определённых частотах наблюдаются резонансы — минимумы коэффициента стоячей волны (КСВ, VSWR).
Частоты резонансов соответствуют случаям, когда длина кабеля кратна половине длины волны:
Здесь — длина кабеля,
— длина волны в кабеле,
= 1, 2, 3, и так далее.
Функция find_cable_length определяет длину кабеля по интервалу между соседними резонансами КСВ с помощью функции:
cable_length = c / (2 * delta_f * vf)
Также в этой функции используется альтернативный метод — по фазовому сдвигу. Он предполагает, что фаза отражённого сигнала изменяется с частотой.
При изменении частоты фаза отражения на конце кабеля изменяется линейно:
Следовательно, производная фазы по изменению частоты равна:
Отсюда длина кабеля:
В коде это реализовано так:
phase_slope = np.polyfit(frequencies, phases, 1)[0]
electrical_length = -phase_slope * c / (4 * np.pi * vf)
Коэффициент укорочения
В программе используется коэффициент укорочения vf. Он учитывает, что в диэлектрике скорость распространения волны меньше скорости света. Коэффициенты укорочения и потери для кабелей различных типов можно найти в статье «Cable Velocity Factor and Loss Data».
Инициализация
На этапе инициализации функция setup_nanovna_for_cable_measurement задаёт интервал и количество точек сканирования, отправляя для этого команду sweep, после которой следует команда pause.
Для получения данных S11 функция get_s11_data выдаёт команду data с параметром 0:
s11_data = send_command(ser, "data 0", 1)
Запуск программы nanovna-cable_measurement.py
Перед запуском программы nanovna-cable_measurement.py выполните калибровку устройства и подключите один конец кабеля к порту PORT1 анализатора NanoVNA. Ко второму концу кабеля подключите нагрузку 50 Ом, например, из комплекта эталонов.
Далее запустите программу:
$ python nanovna-cable_measurement.py
Настройка NanoVNA для измерения кабеля...
Отправка команды: sweep 1000000 500000000 501
Ответ на sweep 1000000 500000000 501: sweep 1000000 500000000 501
ch>
Отправка команды: pause
Ответ на pause: pause
ch>
Получение данных S11...
Отправка команды: resume
Отправка команды: frequencies
Отправка команды: data 0
=== РЕЗУЛЬТАТЫ ДЛЯ РАЗНЫХ ТИПОВ КАБЕЛЕЙ ===
RG-58 (VF=0.66): 2.25 м
RG-174 (VF=0.66): 2.25 м
RG-213 (VF=0.66): 2.25 м
LMR-400 (VF=0.85): 1.75 м
Коаксиал с полиэтиленом (VF=0.66): 2.25 м
Коаксиал с тефлоном (VF=0.7): 2.12 м
Воздушный коаксиал (VF=0.8): 1.86 м
=== РЕЗУЛЬТАТЫ ИЗМЕРЕНИЯ КАБЕЛЯ ===
Разность частот между резонансами: 101.05 МГц
Расчетная длина кабеля: 2.25 метров
Длина кабеля в сантиметрах: 224.9 см
После того как программа завершит измерения, вы увидите на консоли результаты, полученные для кабелей различных типов с разными коэффициентами укорочения. Также будет показана разность частот между резонансами.
Дополнительно программа нарисует графики изменения КСВ кабеля и фазы коэффициента отражения, построенные на основании данных измерений (рис. 8).

Исследование кабеля с помощью Raspberry Pi
Для исследования кабеля с помощью NanoVNA и Raspberry Pi вы можете использовать программу nanovna-cable_measurement-rpi.py:
Посмотреть код программы
import serial
import time
import os
import subprocess
import math
try:
import RPi.GPIO as GPIO
RASPBERRY_PI = True
except ImportError:
RASPBERRY_PI = False
print("Предупреждение: RPi.GPIO не доступен")
class CableAnalyzer:
def __init__(self):
self.ser = None
def send_command(self, command, wait_time=0.5):
print(f"Отправка команды: {command}")
try:
self.ser.reset_input_buffer()
self.ser.reset_output_buffer()
self.ser.write((command + '\r\n').encode())
time.sleep(wait_time)
response = b''
start_time = time.time()
while time.time() - start_time < wait_time:
if self.ser.in_waiting > 0:
response += self.ser.read(self.ser.in_waiting)
time.sleep(0.01)
return response.decode('ascii', errors='ignore').strip()
except Exception as e:
print(f"Ошибка при отправке команды: {e}")
return ""
def setup_nanovna(self, start_freq=1e6, stop_freq=300e6, points=201):
print("Настройка NanoVNA для измерения кабеля...")
test_response = self.send_command("info", 1)
if not test_response or "ch>" not in test_response:
print("Ошибка: NanoVNA не отвечает")
return False
commands = [
f"sweep {int(start_freq)} {int(stop_freq)} {points}",
"pause",
]
for cmd in commands:
response = self.send_command(cmd, 0.5)
if response:
print(f"Ответ: {response}")
time.sleep(1)
return True
def get_s11_data(self):
print("Получение данных S11...")
self.send_command("resume", 2)
freq_data = self.send_command("frequencies", 1)
s11_data = self.send_command("data 0", 1)
return freq_data, s11_data
def parse_frequency_data(self, data):
frequencies = []
if not data:
return frequencies
lines = data.strip().split('\n')
for line in lines:
line = line.strip()
if line and not line.startswith('ch>'):
try:
parts = line.split()
for part in parts:
freq = float(part)
frequencies.append(freq)
except ValueError:
continue
return frequencies
def parse_s11_data(self, data):
s11_points = []
if not data:
return s11_points
lines = data.strip().split('\n')
for line in lines:
line = line.strip()
if line and not line.startswith('ch>'):
try:
parts = line.split()
if len(parts) >= 2:
real = float(parts[0])
imag = float(parts[1])
s11_points.append((real, imag))
except ValueError:
continue
return s11_points
def calculate_vswr(self, s11_points):
vswr_values = []
for real, imag in s11_points:
magnitude = math.sqrt(real**2 + imag**2)
if magnitude < 1:
vswr = (1 + magnitude) / (1 - magnitude)
else:
vswr = 100
vswr_values.append(vswr)
return vswr_values
def calculate_phase(self, s11_points):
phases = []
for real, imag in s11_points:
phase = math.atan2(imag, real)
phases.append(phase)
return phases
def find_peaks_simple(self, data, min_distance=5):
peaks = []
for i in range(min_distance, len(data) - min_distance):
if (data[i] == max(data[i-min_distance:i+min_distance+1])):
peaks.append(i)
return peaks
def find_cable_length(self, frequencies, phases, vswr_values, vf=0.66):
if len(frequencies) < 10:
print("Недостаточно данных для анализа")
return None, None, None, None, None
try:
# Ищем минимумы в КСВ (используем обратные значения)
inverse_vswr = [-v for v in vswr_values]
peaks = self.find_peaks_simple(inverse_vswr, min_distance=10)
if len(peaks) < 2:
print("Не удалось найти резонансы, использую фазовый метод")
# Простой фазовый метод
phase_diff = phases[-1] - phases[0]
freq_diff = frequencies[-1] - frequencies[0]
if freq_diff > 0:
phase_slope = phase_diff / freq_diff
c = 3e8
electrical_length = -phase_slope * c / (4 * math.pi * vf)
return electrical_length, electrical_length, freq_diff/10, frequencies[0], frequencies[-1]
return None, None, None, None, None
# Берем первые два пика
peak1_idx = peaks[0]
peak2_idx = peaks[1]
freq1 = frequencies[peak1_idx]
freq2 = frequencies[peak2_idx]
delta_f = abs(freq2 - freq1)
if delta_f == 0:
print("Нулевая разность частот")
return None, None, None, None, None
c = 3e8
cable_length = c / (2 * delta_f * vf)
# Фазовый метод как проверка
phase_slope = (phases[-1] - phases[0]) / (frequencies[-1] - frequencies[0])
electrical_length = -phase_slope * c / (4 * math.pi * vf)
return cable_length, electrical_length, delta_f, freq1, freq2
except Exception as e:
print(f"Ошибка при расчете длины кабеля: {e}")
return None, None, None, None, None
def measure_cable(self):
if not self.setup_nanovna(start_freq=1e6, stop_freq=500e6, points=101):
return
freq_data, s11_data = self.get_s11_data()
if not freq_data or not s11_data:
print("Не удалось получить данные от NanoVNA")
return
frequencies = self.parse_frequency_data(freq_data)
s11_points = self.parse_s11_data(s11_data)
if len(frequencies) < 10 or len(s11_points) < 10:
print("Недостаточно данных для анализа")
return
phases = self.calculate_phase(s11_points)
vswr_values = self.calculate_vswr(s11_points)
cable_types = {
"RG-58": 0.66,
"RG-174": 0.66,
"RG-213": 0.66,
"LMR-400": 0.85,
"Коаксиал с полиэтиленом": 0.66,
"Коаксиал с тефлоном": 0.70,
}
print("\n" + "="*60)
print("РЕЗУЛЬТАТЫ ИЗМЕРЕНИЯ КАБЕЛЯ")
print("="*60)
results = {}
for cable_type, vf in cable_types.items():
length, _, delta_f, freq1, freq2 = self.find_cable_length(
frequencies, phases, vswr_values, vf)
if length:
results[cable_type] = length
print(f"{cable_type:30} (VF={vf}): {length:.2f} м")
# Основной результат
vf = 0.66
cable_length, electrical_length, delta_f, freq1, freq2 = self.find_cable_length(
frequencies, phases, vswr_values, vf)
if cable_length:
self.print_detailed_results(cable_length, delta_f, frequencies, vswr_values)
self.save_results(frequencies, s11_points, cable_length, results)
else:
print("Не удалось определить длину кабеля")
def print_detailed_results(self, cable_length, delta_f, frequencies, vswr_values):
print(f"\nОСНОВНЫЕ РЕЗУЛЬТАТЫ:")
print(f"Разность частот между резонансами: {delta_f/1e6:.2f} МГц")
print(f"Расчетная длина кабеля: {cable_length:.3f} метров")
print(f"В сантиметрах: {cable_length * 100:.1f} см")
print(f"\nДИАПАЗОН ИЗМЕРЕНИЙ:")
print(f"Начальная частота: {frequencies[0]/1e6:.1f} МГц")
print(f"Конечная частота: {frequencies[-1]/1e6:.1f} МГц")
print(f"Количество точек: {len(frequencies)}")
print(f"\nКАЧЕСТВО КАБЕЛЯ:")
avg_vswr = sum(vswr_values) / len(vswr_values)
min_vswr = min(vswr_values)
max_vswr = max(vswr_values)
print(f"Средний КСВ: {avg_vswr:.2f}")
print(f"Минимальный КСВ: {min_vswr:.2f}")
print(f"Максимальный КСВ: {max_vswr:.2f}")
if avg_vswr < 1.5:
print("Оценка: Отличное качество кабеля")
elif avg_vswr < 2.0:
print("Оценка: Хорошее качество кабеля")
else:
print("Оценка: Проверьте соединения и кабель")
def save_results(self, frequencies, s11_points, cable_length, results):
try:
timestamp = time.strftime("%Y%m%d_%H%M%S")
filename = f"cable_results_{timestamp}.txt"
with open(filename, 'w', encoding='utf-8') as f:
f.write("Результаты измерения кабеля NanoVNA\n")
f.write(f"Время: {time.strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"Длина кабеля: {cable_length:.3f} м\n\n")
f.write("Результаты для разных кабелей:\n")
for cable_type, length in results.items():
f.write(f"{cable_type}: {length:.3f} м\n")
f.write("\nИзмеренные данные:\n")
f.write("Частота(МГц)\tReal\tImag\n")
for i, (freq, point) in enumerate(zip(frequencies, s11_points)):
if i < len(s11_points):
f.write(f"{freq/1e6:.1f}\t{point[0]:.6f}\t{point[1]:.6f}\n")
print(f"\nРезультаты сохранены в: {filename}")
except Exception as e:
print(f"Ошибка сохранения: {e}")
def run(self):
try:
self.ser = serial.Serial('/dev/ttyACM0', 115200, timeout=1)
time.sleep(2)
self.ser.reset_input_buffer()
self.ser.reset_output_buffer()
print("Подключение к NanoVNA установлено")
self.measure_cable()
except serial.SerialException as e:
print(f"Ошибка порта: {e}")
except KeyboardInterrupt:
print("\nИзмерение прервано")
except Exception as e:
print(f"Ошибка: {e}")
finally:
if self.ser and self.ser.is_open:
self.ser.close()
print("Порт закрыт")
if __name__ == "__main__":
analyzer = CableAnalyzer()
analyzer.run()За получение результатов исследования кабеля в этой программе отвечает функция measure_cable.
При запуске эта функция устанавливает диапазон и количество точек сканирования:
if not self.setup_nanovna(start_freq=1e6, stop_freq=500e6, points=101):
return
Далее функция получает результаты сканирования функцией get_s11_data, запускает парсинг списка частот и данных S11 при помощи функций parse_frequency_data и parse_s11_data, соответственно.
После этого вызываются функции вычисления фазы calculate_phase и КСВ calculate_vswr. Результаты измерений выводятся на консоль:
$ python3 nanovna-cable_measurement-rpi.py
Подключение к NanoVNA установлено
Настройка NanoVNA для измерения кабеля...
Отправка команды: info
Отправка команды: sweep 1000000 500000000 101
Ответ: sweep 1000000 500000000 101
ch>
Отправка команды: pause
Ответ: pause
ch>
Получение данных S11...
Отправка команды: resume
Отправка команды: frequencies
Отправка команды: data 0
============================================================
РЕЗУЛЬТАТЫ ИЗМЕРЕНИЯ КАБЕЛЯ
============================================================
RG-58 (VF=0.66): 2.40 м
RG-174 (VF=0.66): 2.40 м
RG-213 (VF=0.66): 2.40 м
LMR-400 (VF=0.85): 1.86 м
Коаксиал с полиэтиленом (VF=0.66): 2.40 м
Коаксиал с тефлоном (VF=0.7): 2.26 м
ОСНОВНЫЕ РЕЗУЛЬТАТЫ:
Разность частот между резонансами: 94.81 МГц
Расчетная длина кабеля: 2.397 метров
В сантиметрах: 239.7 см
ДИАПАЗОН ИЗМЕРЕНИЙ:
Начальная частота: 1.0 МГц
Конечная частота: 500.0 МГц
Количество точек: 101
КАЧЕСТВО КАБЕЛЯ:
Средний КСВ: 1.13
Минимальный КСВ: 1.02
Максимальный КСВ: 1.25
Оценка: Отличное качество кабеля
Результаты сохранены в: cable_results_20251111_144801.txt
Порт закрыт
Содержимое файла cable_results_20251111_144801.txt показано ниже в сокращённом виде:
Результаты измерения кабеля NanoVNA
Время: 2025-11-11 14:48:01
Длина кабеля: 2.397 м
Результаты для разных кабелей:
RG-58: 2.397 м
RG-174: 2.397 м
RG-213: 2.397 м
LMR-400: 1.861 м
Коаксиал с полиэтиленом: 2.397 м
Коаксиал с тефлоном: 2.260 м
Измеренные данные:
Частота(МГц) Real Imag
1.0 0.011857 0.019204
6.0 0.055140 0.039618
11.0 0.090410 0.017665
16.0 0.095088 -0.021885
21.0 0.068050 -0.050912
25.9 0.029303 -0.049788
30.9 0.006237 -0.021477
35.9 0.012974 0.010833
40.9 0.039808 0.022761
45.9 0.062093 0.007966
50.9 0.060779 -0.018027
55.9 0.037090 -0.031619
60.9 0.010468 -0.019539
65.9 0.003137 0.010049
70.9 0.021296 0.034555
75.8 0.052038 0.035669
80.8 0.070442 0.013121
…
470.1 0.043759 0.047831
475.1 0.068886 0.063182
480.0 0.097440 0.052546
485.0 0.106244 0.022774
490.0 0.089125 -0.001861
495.0 0.060504 -0.002928
500.0 0.044155 0.020404
Итоги
Надеюсь, что вам удалось автоматизировать измерения NanoVNA с помощью собственных программ. При весьма скромной цене этот векторный анализатор открывает перед вами широкое поле для творчества.
Освоив управление NanoVNA из программ, вы можете использовать его не только как готовый инструмент, но и создавать на его основе сложные автоматизированные измерительные комплексы. В этом вам помогут одноплатные компьютеры, такие как Raspberry Pi и аналогичные. Здесь все ограничивается только вашей фантазией!
Пишите в комментариях, чтобы ещё вам хотелось узнать про NanoVNA!
Автор @AlexandreFrolov
НЛО прилетело и оставило здесь промокод для читателей нашего блога:
-15% на заказ любого VDS (кроме тарифа Прогрев) — HABRFIRSTVDS.