-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathnanovna_snp.py
executable file
·174 lines (138 loc) · 5.89 KB
/
nanovna_snp.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
#!/usr/bin/python
# SPDX-License-Identifier: GPL-3.0-or-later
'''
Command line tool to fetch S11, S21 or S11 & S21 parameter from NanoVNA-H
and save as S-parameter or Z-parameter in "touchstone" format (rev 1.1).
Connect via USB serial, issue the command, calculate, and format the response.
Do it as an exercise - step by step - without using tools like scikit-rf.
'''
import argparse
import serial
from serial.tools import list_ports
import sys
from datetime import datetime
# ChibiOS/RT Virtual COM Port
VID = 0x0483 #1155
PID = 0x5740 #22336
# Get nanovna device automatically
def getdevice() -> str:
device_list = list_ports.comports()
for device in device_list:
if device.vid == VID and device.pid == PID:
return device.device
raise OSError("device not found")
# default output
outfile = sys.stdout
# construct the argument parser and parse the arguments
ap = argparse.ArgumentParser( description='Save S parameter from NanoVNA-H in "touchstone" format')
ap.add_argument( '-d', '--device', dest = 'device',
help = 'connect to device' )
ap.add_argument( '-o', '--out', nargs = '?', type=argparse.FileType( 'wb' ),
help = f'write output to FILE, default = {outfile.name}', metavar = 'FILE', default = outfile )
ap.add_argument( '-c', '--comment', dest = 'comment', default = False, action= 'store_true',
help = 'add comments to output file (may break some simple tools, e.g. octave\'s load("-ascii" ...))' )
ap.add_argument( '-t', '--timeout', dest = 'timeout', type = int, default = 3,
help = 'timeout for data transfer (default = 3 s)' )
fmt = ap.add_mutually_exclusive_group()
fmt.add_argument( '-1', '--s1p', action = 'store_true',
help = 'store S-parameter for 1-port device (default)' )
fmt.add_argument( '-2', '--s2p', action = 'store_true',
help = 'store S-parameter for 2-port device' )
fmt.add_argument( '-z', '--z1p', action = 'store_true',
help = 'store Z-parameter for 1-port device' )
options = ap.parse_args()
nanodevice = options.device or getdevice()
outfile = options.out
s1p = options.s1p
s2p = options.s2p
z1p = options.z1p
cr = '\r'
lf = '\n'
crlf = cr + lf
prompt = 'ch> '
Z0 = 50 # nominal impedance
with serial.Serial( nanodevice, timeout=options.timeout ) as NanoVNA: # open serial connection
def execute( cmd ):
NanoVNA.write( (cmd + cr).encode() ) # send command and options terminated by CR
echo = NanoVNA.read_until( (cmd + crlf).encode() ) # wait for command echo terminated by CR LF
echo = NanoVNA.read_until( prompt.encode() ) # get command response until prompt
return echo[ :-len( crlf + prompt ) ].decode().split( crlf ) # remove trailing '\r\nch> ', split in lines
execute( 'pause' ) # stop display
# get start and stop frequency as well as number of points
f_start, f_stop, n_points = execute( 'sweep' )[0].split()
if s2p: # fetch S11 and S21
outmask = 7 # freq, S11.re, S11.im, S21.re, S21.im
else: # fetch only S11
outmask = 3 # freq, S11.re, S11.im
cmd = f'scan {f_start} {f_stop} {n_points} {outmask}' # prepare command
comment = datetime.now().strftime( f'! NanoVNA %Y%m%d_%H%M%S\n! {cmd}' )
if z1p:
comment += '\n! 1-port normalized Z-parameter (R/Z0 + jX/Z0)'
elif s2p:
comment += '\n! 2-port S-parameter (S11.re S11.im S21.re S21.im 0 0 0 0)'
else:
comment += '\n! 1-port S-parameter (S11.re S11.im)'
scan_result = execute( cmd ) # scan and receive S-parameter
execute( 'resume' ) # resume display
def format_parameter_line( line ):
if z1p:
# calculate normalized impedance as Rn + jXn = R/Z0 + jX/Z0 according to this doc
# https://pa3a.nl/wp-content/uploads/2022/07/Math-for-nanoVNA-S2Z-and-Z2S-Jul-2021.pdf
freq, S11r, S11i = line.split()
freq = float( freq )
S11r = float( S11r )
S11i = float( S11i )
Sr2 = S11r * S11r
Si2 = S11i * S11i
Denom = ( 1 - S11r ) * ( 1 - S11r ) + Si2
Rn = ( 1 - Sr2 - Si2 ) / Denom
Xn = ( 2 * S11i ) / Denom
return f'{freq:.0f} {Rn:15.9f} {Xn:15.9f}'
elif s2p:
# format a line with freq, S11, S21, S12, S22 (Sxx as re/im pair)
freq, S11r, S11i, S21r, S21i = line.split()
freq = float( freq )
S11r = float( S11r )
S11i = float( S11i )
S21r = float( S21r )
S21i = float( S21i )
line = f'{freq:.0f} {S11r:12.9f} {S11i:12.9f}'
line += f' {S21r:12.9f} {S21i:12.9f}'
line += ' 0 0 0 0' # S12 and S22 are 0+j0
return line
else: # s1p
# format a line with freq, S11.re, S11.im
freq, S11r, S11i = line.split()
freq = float( freq )
S11r = float( S11r )
S11i = float( S11i )
return f'{freq:.0f} {S11r:12.9f} {S11i:12.9f}'
def output_string( line ):
if outfile == sys.stdout:
print( line )
else:
outfile.write( ( line + lf ).encode() )
# Touchstone data files may include comments. Comments are preceded by an exclamation mark (!).
# Comments may appear on a separate line, or after the last data value on a line. Comments are
# terminated by a line termination sequence or character (i.e., multi-line comments are not allowed).
# The syntax rules for comments are identical for Version 1.0 and Version 2.0 files.
if options.comment:
output_string( comment )
# Rules for Version 1.0 Files:
# For Version 1.0 files, the option line shall precede any data lines
# and shall be the first non-comment, nonblank line in the file.
# write data as touchstone file (Rev. 1.1)
# Frequency unit: Hz
# Parameter: S = scattering or Z = impedance
# Format: RI = real-imag
# Reference impedance: 50 Ohm
frequency_unit = 'HZ'
format = 'RI'
if z1p:
parameter = 'Z'
else:
parameter = 'S'
# option header
output_string( f'# {frequency_unit} {parameter} {format} R {Z0}' )
for line in scan_result:
output_string( format_parameter_line( line ) )