Created
July 9, 2019 01:16
-
-
Save mobilinkd/3c19f87d4f7b0674d990287443b88241 to your computer and use it in GitHub Desktop.
Generate PSK31 with an SDG2000X AWG
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# -*- coding: utf-8 -*- | |
import sys | |
import visa | |
import struct | |
import io | |
import time | |
import numpy as np | |
# Varicode mapping from here: | |
# http://www.arrl.org/psk31-spec | |
varicode_map = { | |
'\x00' : '1010101011', '\x01' : '1011011011', | |
'\x02' : '1011101101', '\x03' : '1101110111', | |
'\x04' : '1011101011', '\x05' : '1101011111', | |
'\x06' : '1011101111', '\x07' : '1011111101', | |
'\x08' : '1011111111', '\x09' : '11101111', | |
'\x0A' : '11101', '\x0B' : '1101101111', | |
'\x0C' : '1011011101', '\x0D' : '11111', | |
'\x0E' : '1101110101', '\x0F' : '1110101011', | |
'\x10' : '1011110111', '\x11' : '1011110101', | |
'\x12' : '1110101101', '\x13' : '1110101111', | |
'\x14' : '1101011011', '\x15' : '1101101011', | |
'\x16' : '1101101101', '\x17' : '1101010111', | |
'\x18' : '1101111011', '\x19' : '1101111101', | |
'\x1A' : '1110110111', '\x1B' : '1101010101', | |
'\x1C' : '1101011101', '\x1D' : '1110111011', | |
'\x1E' : '1011111011', '\x1F' : '1101111111', | |
' ' : '1', '!' : '111111111', | |
'"' : '101011111', '#' : '111110101', | |
'$' : '111011011', '%' : '1011010101', | |
'&' : '1010111011', '\'' : '101111111', | |
'(' : '11111011', ')' : '11110111', | |
'*' : '101101111', '+' : '111011111', | |
',' : '1110101', '-' : '110101', | |
'.' : '1010111', '/' : '110101111', | |
'0' : '10110111', '1' : '10111101', | |
'2' : '11101101', '3' : '11111111', | |
'4' : '101110111', '5' : '101011011', | |
'6' : '101101011', '7' : '110101101', | |
'8' : '110101011', '9' : '110110111', | |
':' : '11110101', ';' : '110111101', | |
'<' : '111101101', '=' : '1010101', | |
'>' : '111010111', '?' : '1010101111', | |
'@' : '1010111101', 'A' : '1111101', | |
'B' : '11101011', 'C' : '10101101', | |
'D' : '10110101', 'E' : '1110111', | |
'F' : '11011011', 'G' : '11111101', | |
'H' : '101010101', 'I' : '1111111', | |
'J' : '111111101', 'K' : '101111101', | |
'L' : '11010111', 'M' : '10111011', | |
'N' : '11011101', 'O' : '10101011', | |
'P' : '11010101', 'Q' : '111011101', | |
'R' : '10101111', 'S' : '1101111', | |
'T' : '1101101', 'U' : '101010111', | |
'V' : '110110101', 'W' : '101011101', | |
'X' : '101110101', 'Y' : '101111011', | |
'Z' : '1010101101', '[' : '111110111', | |
'\\' : '111101111', ']' : '111111011', | |
'^' : '1010111111', '_' : '101101101', | |
'`' : '1011011111', 'a' : '1011', | |
'b' : '1011111', 'c' : '101111', | |
'd' : '101101', 'e' : '11', | |
'f' : '111101', 'g' : '1011011', | |
'h' : '101011', 'i' : '1101', | |
'j' : '111101011', 'k' : '10111111', | |
'l' : '11011', 'm' : '111011', | |
'n' : '1111', 'o' : '111', | |
'p' : '111111', 'q' : '110111111', | |
'r' : '10101', 's' : '10111', | |
't' : '101', 'u' : '110111', | |
'v' : '1111011', 'w' : '1101011', | |
'x' : '11011111', 'y' : '1011101', | |
'z' : '111010101', '{' : '1010110111', | |
'|' : '110111011', '}' : '1010110101', | |
'~' : '1011010111', '\x7F' : '1110110101' | |
} | |
def encode(data): | |
with io.StringIO() as output: | |
output.write('0'*32) | |
for c in data: | |
if c in varicode_map: | |
output.write(varicode_map[c]) | |
output.write('00') | |
return output.getvalue() | |
return '' | |
rm = visa.ResourceManager('@py') | |
class PSK31Encoder(object): | |
"""This encodes a string of 0s and 1s (literally a string of | |
these characters) into a waveform. This will be used to | |
modulate a DSB-AM carrier (dual-sideband, carrier suppressed | |
amplitude modulation). This modulatation causes the carrier | |
to invert when the modulation goes negative. | |
""" | |
BPS = 31.25 | |
def __init__(self, sps=8000, bits=16): | |
self.samples_per_bit = int(sps / self.BPS) | |
# Generate the +/- waveforms (1 values) | |
self.pos = np.full(self.samples_per_bit, 32767, dtype=np.int16) | |
self.neg = np.full(self.samples_per_bit, -32768, dtype=np.int16) | |
# Generate the down/up transition waveforms (0 values) | |
time = np.arange(0, np.pi, np.pi / self.samples_per_bit) | |
self.to_neg = np.array(np.cos(time) * 32767, dtype=np.int16) | |
time = np.arange(np.pi, np.pi * 2, np.pi / self.samples_per_bit) | |
self.to_pos = np.array(np.cos(time) * 32767, dtype=np.int16) | |
# Generate the preamble. This is 32 0's. | |
time = np.arange(0, np.pi / 2, np.pi / self.samples_per_bit) | |
self.start = np.repeat(np.append(self.to_neg, self.to_pos), 16) | |
# Generate the post-amble. This leaves the level at the | |
# last level/phase for 31 bit periods. | |
time = np.arange(0, np.pi / 2, np.pi / self.samples_per_bit) | |
self.end_from_neg = np.repeat(self.neg, 31) | |
self.end_from_pos = np.repeat(self.pos, 31) | |
self.wf = [ | |
[self.to_pos, self.to_neg], | |
[self.neg, self.pos] | |
] | |
def encode(self, msg): | |
"""Encode msg, a varicode-encoded message, into a waveform.""" | |
prev = 1 | |
# Add preamble | |
waveform = self.start | |
# Add message content | |
for c in msg: | |
i = int(c) | |
waveform = np.append(waveform, self.wf[i][prev]) | |
if i == 0: | |
prev = (prev + 1) & 1 | |
# Add post-amble | |
waveform = np.append(waveform, self.end_from_pos if prev else self.end_from_neg) | |
return waveform | |
class Arb(object): | |
def __init__(self, address): | |
self.awg = rm.open_resource('TCPIP::{}::INSTR'.format(address)) | |
self.awg.write_termination = '\r\n' | |
self.awg.encoding = 'latin1' | |
self.sps = 0 | |
print(self.awg.query('*IDN?')) | |
def __del__(self): | |
self.close() | |
def close(self): | |
if self.awg is not None: | |
self.awg.close() | |
self.awg = None | |
def upload(self, data, sps=8000): | |
tmp = io.BytesIO() | |
tmp.write( | |
'C1:WVDT WVNM,pskdata,FREQ,{:.1f}' | |
.format(sps) | |
.encode('latin1')) | |
tmp.write( | |
b',TYPE,8,AMPL,4.0,OFST,0.0,PHASE,0.0,WAVEDATA,') | |
# Little endian, 16-bit 2's complement | |
for i in data: | |
tmp.write(struct.pack('<h', i)) | |
self.awg.write_raw(tmp.getvalue()) | |
self.awg.write("C1:ARWV NAME,pskdata") | |
self.sps = sps | |
self.len = len(data) | |
def send(self, frequency, amplitude=0.02): | |
self.awg.write("C1:ARWV NAME,pskdata") | |
self.awg.write('C1:MDWV STATE,ON') | |
self.awg.write('C1:MDWV DSBAM') | |
self.awg.write('C1:MDWV DSBAM,SRC,INT') | |
self.awg.write('C1:MDWV DSBAM,MDSP,ARB') | |
self.awg.write('C1:MDWV DSBAM,MDSP,ARB,NAME,pskdata') | |
self.awg.write('C1:MDWV DSBAM,FRQ,{}'.format(self.sps/self.len)) | |
self.awg.write("C1:ARWV NAME,pskdata") | |
self.awg.write('C1:MDWV CARR,WVTP,SINE') | |
self.awg.write('C1:MDWV CARR,FRQ,{}'.format(frequency)) | |
self.awg.write('C1:MDWV CARR,AMP,{}'.format(amplitude)) | |
# HACK HACK HACK | |
# Working around missing SCPI feature in SDG2000X. Need to | |
# load the modulating wave form. But there is apparently no | |
# way to do this programatically. THIS CODE ASSUMES THAT | |
# THERE IS ONLY ONE FILE AVAILABLE. | |
self.awg.write('VKEY VALUE,KB_FUNC4,STATE,1') | |
self.awg.write('VKEY VALUE,KB_FUNC6,STATE,1') | |
self.awg.write('VKEY VALUE,KB_FUNC2,STATE,1') | |
self.awg.write('VKEY VALUE,KB_FUNC1,STATE,1') | |
self.awg.write('VKEY VALUE,KB_KNOB_RIGHT,STATE,1') | |
self.awg.write('VKEY VALUE,KB_KNOB_DOWN,STATE,1') | |
self.awg.write('C1:OUTP LOAD,50') | |
self.awg.write('C1:OUTP ON') | |
time.sleep((self.len + 5)/self.sps) | |
self.awg.write('C1:OUTP OFF') | |
import argparse | |
if __name__ == '__main__': | |
psk = PSK31Encoder(8000) | |
waveform = psk.encode(encode("Hello, World! de wx9o\r\n")) | |
arb = Arb('sdg2122x') | |
arb.upload(waveform, 8000) | |
arb.send(28121000) | |
arb.close() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment