Skip to content

Instantly share code, notes, and snippets.

@mobilinkd
Created July 9, 2019 01:16
Show Gist options
  • Save mobilinkd/3c19f87d4f7b0674d990287443b88241 to your computer and use it in GitHub Desktop.
Save mobilinkd/3c19f87d4f7b0674d990287443b88241 to your computer and use it in GitHub Desktop.
Generate PSK31 with an SDG2000X AWG
#!/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