diff --git a/iq.py b/iq.py index cb4fd13..d266831 100755 --- a/iq.py +++ b/iq.py @@ -31,6 +31,9 @@ # 01-04-2014 Initial release (QST article 4/2014) # 05-17-2014 Improvements for RPi timing, etc. # Add REV, skip, sp_max/min, v_max/min options +# 05-31-2014 Add Si570 freq control option (DDS chip provided in SoftRock, eg.) +# Note: Use of Si570 requires libusb-1.0 wrapper from +# https://pypi.python.org/pypi/libusb1/1.2.0 # Note for directfb use (i.e. without X11/Xorg): # User must be a member of the following Linux groups: @@ -73,6 +76,7 @@ opt = options.opt # Get option object from options module # print list of parameters to console. print "identification:", opt.ident print "source :", opt.source +print "freq control :", opt.control print "waterfall :", opt.waterfall print "rev i/q :", opt.rev_iq print "sample rate :", opt.sample_rate @@ -82,8 +86,11 @@ print "skipping :", opt.skip print "hamlib :", opt.hamlib print "hamlib rigtype:", opt.hamlib_rigtype print "hamlib device :", opt.hamlib_device -print "rtl frequency :", opt.rtl_frequency -print "rtl gain :", opt.rtl_gain +if opt.source=="rtl": + print "rtl frequency :", opt.rtl_frequency + print "rtl gain :", opt.rtl_gain +if opt.control=="si570": + print "si570 frequency :", opt.si570_frequency print "pulse :", opt.pulse print "fullscreen :", opt.fullscreen print "hamlib intvl :", opt.hamlib_interval @@ -340,7 +347,9 @@ if opt.waterfall: # Instantiate the waterfall and palette data mywf = wf.Wf(opt, v_min, v_max, nsteps, wf_pixel_size) -if opt.hamlib: +if (opt.control == "si570") and opt.hamlib: + print "Warning: Hamlib requested with si570. Si570 wins! No Hamlib." +if opt.hamlib and (opt.control != "si570"): import Hamlib # start up Hamlib rig connection Hamlib.rig_set_debug (Hamlib.RIG_DEBUG_NONE) @@ -385,7 +394,7 @@ print "Update interval = %.2f ms" % float(1000*chunk_time) # Initialize input mode, RTL or AF # This starts the input stream, so place it close to start of main loop. -if opt.source=="rtl": # input from RTL dongle +if opt.source=="rtl": # input from RTL dongle (and freq control) import iq_rtl as rtl dataIn = rtl.RTL_In(opt) elif opt.source=='audio': # input from audio card @@ -396,6 +405,11 @@ else: print "unrecognized mode" quit_all() +if opt.control=="si570": + import si570control + mysi570 = si570control.Si570control() + mysi570.setFreq(opt.si570_frequency / 1000.) # Set starting freq. + # ** MAIN PROGRAM LOOP ** run_flag = True # set false to suspend for help screen etc. @@ -422,11 +436,17 @@ while True: # displayed in the "2d" graph, a new line of the waterfall is generated. # Line of text with receiver center freq. if available - if opt.hamlib: + showfreq = True + if opt.control == "si570": + msg = "%.3f kHz" % (mysi570.getFreqByValue() * 1000.) # freq/4 from Si570 + elif opt.hamlib: msg = "%.3f kHz" % rigfreq # take current rigfreq from hamlib thread - elif opt.source=='rtl': + elif opt.control=='rtl': msg = "%.3f MHz" % (dataIn.rtl.get_center_freq()/1.e6) - if opt.hamlib or (opt.source=='rtl'): + else: + showfreq = False + + if showfreq: # Center it and blit just above 2d display ww, hh = lgfont.size(msg) surf_main.blit(lgfont.render(msg, 1, BLACK, BGCOLOR), @@ -533,7 +553,7 @@ while True: "Change lower plot dB limit: (L) increase; (l) decrease", "Change WF palette upper limit: (B) increase; (b) decrease", "Change WF palette lower limit: (D) increase; (d) decrease" ] - if opt.source=='rtl' or opt.hamlib: + if opt.control != "none": lines.append("Change rcvr freq: (rt arrow) increase; (lt arrow) decrease") lines.append(" Use SHIFT for bigger steps") lines.append("RETURN - Cycle to next Help screen") @@ -645,31 +665,35 @@ while True: # arrows and "Enter". (Same as keyboard buttons) elif event.key == pg.K_RIGHT: # right arrow + freq - if opt.source=='rtl': + if opt.control == 'rtl': finc = 100e3 if shifted else 10e3 dataIn.rtl.center_freq = dataIn.rtl.get_center_freq()+finc - else: # audio mode - if opt.hamlib: - finc = 1.0 if shifted else 0.1 - rigfreq_request = rigfreq + finc - else: - print "Rt arrow ignored, no Hamlib" + elif opt.control == 'si570': + finc = 1.0 if shifted else 0.1 + mysi570.setFreqByValue(mysi570.getFreqByValue() + finc*.001) + elif opt.hamlib: + finc = 1.0 if shifted else 0.1 + rigfreq_request = rigfreq + finc + else: + print "Rt arrow ignored, no Hamlib" elif event.key == pg.K_LEFT: # left arrow - freq - if opt.source=='rtl': + if opt.control == 'rtl': finc = -100e3 if shifted else -10e3 dataIn.rtl.center_freq = dataIn.rtl.get_center_freq()+finc - else: # audio mode - if opt.hamlib: - finc = -1.0 if shifted else -0.1 - rigfreq_request = rigfreq + finc - else: - print "Lt arrow ignored, no Hamlib" + elif opt.control == 'si570': + finc = -1.0 if shifted else -0.1 + mysi570.setFreqByValue(mysi570.getFreqByValue() + finc*.001) + elif opt.hamlib: + finc = -1.0 if shifted else -0.1 + rigfreq_request = rigfreq + finc + else: + print "Lt arrow ignored, no Hamlib" elif event.key == pg.K_UP: print "Up" elif event.key == pg.K_DOWN: print "Down" elif event.key == pg.K_RETURN: - info_phase += 1 # Jump to phase 1 or 2 overlay + info_phase += 1 # Jump to phase 1 or 2 overlay info_counter = 0 # (next time) # We can have an alternate set of keyboard (LCD button) responses diff --git a/iq_opt.py b/iq_opt.py index 76ae0ff..5c73484 100755 --- a/iq_opt.py +++ b/iq_opt.py @@ -23,6 +23,7 @@ # HISTORY # 01-04-2014 Initial release # 05-05-2014 Changed options +# 05-31-2014 Si570 control (vs RTL control vs None [af]) import optparse @@ -30,6 +31,7 @@ import optparse # Note options changed: # Add "skip", "REV", remove "RPI", "taking", "max_queue" +# Add --SI570 # Set up command line parser. (Use iq.py --help to see a formatted qlisting.) op = optparse.OptionParser() @@ -45,6 +47,8 @@ op.add_option("--LCD4", action="store_true", dest="lcd4", help='Use 4" LCD instead of large screen') op.add_option("--RTL", action="store_true", dest="source_rtl", help="Set source to RTL-SDR") +op.add_option("--SI570", action="store_true", dest="control_si570", + help="Set freq control to Si570, not RTL or Hamlib") op.add_option("--REV", action="store_true", dest="rev_iq", help="Reverse I & Q to reverse spectrum display") op.add_option("--WATERFALL", action="store_true", dest="waterfall", @@ -75,6 +79,8 @@ op.add_option("--rtl_freq", action="store", type="float", dest="rtl_frequency", help="Initial RTL operating frequency (float kHz)") op.add_option("--rtl_gain", action="store", type="int", dest="rtl_gain", help="RTL_SDR gain, default 0.") +op.add_option("--si570_frequency", action="store", type="float", dest="si570_frequency", + help="Si570 LO initial frequency, (float kHz)") op.add_option("--size", action="store", type="int", dest="size", help="size of FFT. Default is 512.") op.add_option("--skip", action="store", type="int", dest="skip", @@ -98,6 +104,7 @@ op.add_option("--waterfall_palette", action="store", type="int", dest="waterfall DEF_SAMPLE_RATE = 48000 op.set_defaults( buffers = 12, # no. buffers in sample chunk (RPi-40) + control_si570 = False, # normally, talk to RTL or Hamlib for freq info cpu_load_interval = 3.0, # cycle time for CPU monitor thread fullscreen = False, # Use full screen mode? (if not LCD4) hamlib = False, # Using Hamlib? T/F (RPi-False) @@ -113,6 +120,7 @@ op.set_defaults( rtl_frequency = 146.e6, # RTL center freq. Hz rtl_gain = 0, # auto sample_rate = DEF_SAMPLE_RATE, # (stereo) frames/second (Hz) + si570_frequency = 7050.0, # initial freq. for Si570 LO. size = 384, # size of FFT --> freq. resolution skip = 0, # if not =0, skip some input data source_rtl = False, # Use sound card, not RTL-SDR input @@ -128,10 +136,21 @@ op.set_defaults( opt, args = op.parse_args() # This is an "option" that the user can't change. -opt.ident = "IQ.PY v. 0.30 de AA6E" +opt.ident = "IQ.PY v. 0.35 de AA6E" -# --RTL option forces source=rtl, but normally source=audio -opt.source = "rtl" if opt.source_rtl else "audio" +# 'source' refers to signal source (RTL or audio sound card) +# 'control' refers to freq. readout/control (RTL, si570, or none) + +opt.control = "none" +if opt.hamlib: + opt.control = "hamlib" +if opt.source_rtl: + opt.source = "rtl" + opt.control= "rtl" +else: + opt.source = "audio" + if opt.control_si570: + opt.control = "si570" # Change default Freq for RTL to an appropriate (legal) value (tnx KF3EB) # However, do not override user's --rate setting, if present. diff --git a/si570control.py b/si570control.py new file mode 100755 index 0000000..4b987e4 --- /dev/null +++ b/si570control.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python + +# Si570control class gives python access to the Si570 Digital +# Sythesizer via a USB connection. +# Copyright (C) 2014 Martin Ewing +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# Contact the author by e-mail: aa6e@arrl.net + +# This is "middleware" that defines an API for general user programming +# of radio systems using the Si570 Programmable VCXO as an inexpensive +# digital VFO. These routines connect via USB to an ATtiny45 USB-to-I2C +# device, which is running the usbavrsi570 code from PE0FKO. + +# Tested on SoftRock RxTx Ensemble that uses the ATtiny85 MPU chip and a +# SiLabs 570 ("CAC000141G / D1HOS144") chip +# [3.3v CMOS, 61 ppm stab., 10-160 MHz] + +# partially based on a subset of operations.c from Andrew Nilsson VK6JBL +# Also, see http://www.silabs.com/Support%20Documents/TechnicalDocs/si570.pdf +# and https://code.google.com/p/usbavrsi570/ + +# require libusb-1.0 wrapper from https://pypi.python.org/pypi/libusb1/1.2.0 +import libusb1, usb1 + +import math, sys +from sidefs import * + +# flags used for most usb i/o +#input +UFLGS1 = libusb1.LIBUSB_TYPE_VENDOR | \ + libusb1.LIBUSB_RECIPIENT_DEVICE | \ + libusb1.LIBUSB_ENDPOINT_IN +#output +UFLGS2 = libusb1.LIBUSB_TYPE_VENDOR | \ + libusb1.LIBUSB_RECIPIENT_DEVICE | \ + libusb1.LIBUSB_ENDPOINT_OUT + +# Note changes from operation.c: +# 1. method names have changed to make them more regular. +# 2. get/set freq always work with floating MHz of signal frequency = +# osc frequency / multiplier. + +class Si570control(object): + def __init__(self, verbose=0, fXtal=114.285, multiplier=4, i2c=0x55, + vendor_id=0x16c0, product_id=0x05dc): + self.verbose = verbose + self.fXtal = fXtal + self.multiplier = multiplier + self.i2c = i2c + self.context = usb1.USBContext() + self.device = self.context.getByVendorIDAndProductID(vendor_id, product_id) + if self.verbose: + print "device", self.device + self.handle = self.device.open() + self.version = self.getVersion() + + # get version numbers + def getVersion(self): + bb = bytearray(self.handle.controlRead + (UFLGS1,REQUEST_READ_VERSION, 0x0E00, 0, 2, 500)) + if len(bb)==2: + ver = "%d.%d" % (bb[1], bb[0]) + if self.verbose: + print "Version", ver + return ver + else: + if self.verbose: + print "Version Unknown." + return None + + def enum_devices(self): + mydevices = self.context.getDeviceList() + print "n=",len(mydevices) + for i,x in enumerate(mydevices): + print "%2d %2d %d %3d %d %d %0.4x %0.4x" % (i, + x.getBusNumber(), + x.getDeviceAddress(), + x.getDeviceClass(), + x.getDeviceProtocol(), + x.getDeviceSpeed(), + x.getVendorID(), + x.getProductID() ) + + def getFreqByValue(self): # return osc freq / multiplier + bb = bytearray( self.handle.controlRead + (UFLGS1, REQUEST_READ_FREQUENCY, 0, 0, 4, 500) ) + fint = ((bb[3] << 8 | bb[2]) << 8 | bb[1] ) << 8 | bb[0] + if len(bb) == 4: + ans = (float(fint)/(1<<21)) / self.multiplier + return ans + else: + return None + + def getRegisters(self): + bb = bytearray( self.handle.controlRead + (UFLGS1, REQUEST_READ_REGISTERS, SI570_I2C_ADDR, 0, 6, 5000) ) + if len(bb) > 0: + for i in range(6): + print "Register %d = %X (%d)" % ( i+7, bb[i], bb[i] ) + + def calculateFreq(self, s): # s is string, could be bytearray? + si = bytearray(s) + RFREQ_int = ((si[2] & 0xf0) >> 4) + ((si[1] & 0x3f) * 16) + RFREQ_frac = ((si[2] & 0xf ) << 24) + (si[3] << 16) + (si[4] << 8) + si[5] + RFREQ = RFREQ_int + (float(RFREQ_frac) / 268435456.0) + N1 = ((si[1] & 0xc0 ) >> 6) + ((si[0] & 0x1f) << 2) + HS_DIV = (si[0] & 0xE0) >> 5 + fout = self.fXtal * RFREQ / ((N1 + 1) * HS_DIV_MAP[HS_DIV]) + if self.verbose >= 2: + print "RFREQ = %f" % RFREQ + print "N1 = %d" % N1 + print "HS_DIV = %d" % HS_DIV + print "nHS_DIV = %d" % HS_DIV_MAP[HS_DIV] + print "fout = %f" % fout + return fout # actual osc freq. + + def getFreq(self): # return osc freq / multiplier + strg = self.handle.controlRead \ + (UFLGS1, REQUEST_READ_REGISTERS, SI570_I2C_ADDR, 0, 6, 5000) + # keep strg for calculateFrequency, avoiding unicode issues. + bb = bytearray( strg ) + if len(bb) > 0: + if self.verbose >= 2: + for i in range(6): + print "Register %d = %X (%d)" % ( i+7, bb[i], bb[i] ) + return self.calculateFreq(strg) / self.multiplier + else: + return None + + def getPTT(self): + bb = bytearray( self.handle.controlRead + (UFLGS1, REQUEST_READ_KEYS, 0, 0, 1, 5000) ) + if bb[0] & 0x40: + return 1 + else: + return 0 + + def getKeys(self): + bb = bytearray( self.handle.controlRead + (UFLGS1, REQUEST_READ_KEYS, 0, 0, 1, 5000) ) + if bb[0] & 0x20: # CW_KEY_1 high: not pressed low: pressed + keys = 0 + else: + keys = 1 + if not (bb[0] & 0x02): + keys += 2 # CW_KEY_2 high: not pressed low: pressed + return keys # keys = 3 if both pressed + + def setPTT(self, value): + bb = bytearray( self.handle.controlRead + (UFLGS1, REQUEST_SET_PTT, value, 0, 3, 5000) ) + if self.verbose >= 2: + print "buffer=",bb[0],bb[1],bb[2] + + def calcDividers(self, f): # Returns solution = [HS_DIV, N1, f0, RFREQ] + # Instead of solution structure, use simple list for each variable. + cHS_DIV = list() + cN1 = list() + cf0 = list() + cRFREQ = list() + for i in range(7,-1,-1): # Count down through the dividers + if HS_DIV_MAP[i] > 0: + cHS_DIV.append(i) + y = (SI570_DCO_HIGH + SI570_DCO_LOW) / (2 * f) + y = y / HS_DIV_MAP[i] + if y < 1.5: + y = 1.0 + else: + y = 2 * round(y/2.0) + if y > 128: + y = 128 + cN1.append( math.trunc(y) - 1 ) + cf0.append( f * y * HS_DIV_MAP[i] ) + else: + cHS_DIV.append(None) # dummy result + cN1.append(None) # another dummy + cf0.append( 1.0E16 ) + imin = -1 + fmin = 1.0E16 + for i in range(8): + if (cf0[i] >= SI570_DCO_LOW) & (cf0[i] <= SI570_DCO_HIGH) : + if cf0[i] < fmin: + fmin = cf0[i] + imin = i + if imin >= 0: + solution = [ cHS_DIV[imin], cN1[imin], cf0[imin], cf0[imin]/self.fXtal ] + if (self.verbose >= 2): + print "Solution:" + print " HS_DIV = %d" % solution[0] + print " N1 = %d" % solution[1] + print " f0 = %f" % solution[2] + print " RFREQ = %f" % solution[3] + else: + solution = None # This is the error return + return solution + + def setLongWord(self, v ): # v = int value; return bytearray(4) + iv = int(v) # be sure of int type + b = bytearray(4) + b[0] = iv & 0xff + b[1] = ((iv & 0xff00) >> 8) & 0xff + b[2] = ((iv & 0xff0000) >> 16) & 0xff + b[3] = ((iv & 0xff000000) >> 24) & 0xff + return b # NB bytearray, not long word! + + def setFreq(self, frequency): + f = self.multiplier * frequency + value = 0x700 + self.i2c + index = 0 + if self.verbose: + print "Setting Si570 Frequency by registers to: %f" % f + sHS_DIV, sN1, sf0, sRFREQ = self.calcDividers(f) + RFREQ_int = math.trunc(sRFREQ) + RFREQ_frac= int( round((sRFREQ - RFREQ_int) * 268435456) ) # check int ok + intbuf = self.setLongWord( RFREQ_int ) + fracbuf = self.setLongWord( RFREQ_frac) + outbuf = bytearray(6) + outbuf[5] = fracbuf[0] + outbuf[4] = fracbuf[1] + outbuf[3] = fracbuf[2] + outbuf[2] = fracbuf[3] | ((intbuf[0] & 0xf) << 4) + outbuf[1] = RFREQ_int / 16 + ((sN1 & 0x3) << 6) + outbuf[0] = sN1/4 + (sHS_DIV << 5) + sout = str() + for x in outbuf: + sout += chr(x) + r = self.handle.controlWrite \ + (UFLGS2, REQUEST_SET_FREQ, value, index, sout, 5000) + if r: + if self.verbose >= 2: + print "Set Freq Buffer", + print "%x %x" % (outbuf[0], outbuf[1]) + else: + print "Failed writing frequency to device" + + def setFreqByValue(self, frequency): + f = self.multiplier * frequency + value = 0x700 + self.i2c + index = 0 + buf = self.setLongWord(round(f * 2097152.0)) + if self.verbose: + print "Setting Si570 Frequency by value to: %f" % f + if self.verbose >= 2: + print "Set Freq Buffer: %x %x %x %x" % (buf[0], buf[1], + buf[2], buf[3]) + sout = str() + for x in buf: + sout += chr(x) + r = self.handle.controlWrite \ + (UFLGS2, REQUEST_SET_FREQ_BY_VALUE, value, index, sout, 5000) + if r: + if self.verbose >= 2: + print "Set Freq Buffer: %x %x %x %x" % (buf[0], buf[1], + buf[2], buf[3]) + else: + print "Failed setting frequency" +# End of Si570 class + +if __name__ == "__main__": + # debug code goes here + si = Si570control(verbose=0) + freq = si.getFreqByValue() + print "freq by value", freq + #si.getRegisters() + + #f = si.getFreq() + #print "returned freq", f + + #si.setFreq( 1.8) + #print "set freq check" + #si.getFreq() + + print "SET FREQ BY VALUE" + si.setFreqByValue(7.5) + print "checking" + print si.getFreqByValue() + + if False: + print "Calc. dividers [HS_DIV, N1, f0, RFREQ]" + a = si.calcDividers(28.0) + print a + print "Done." + diff --git a/sidefs.py b/sidefs.py new file mode 100755 index 0000000..c88d3e3 --- /dev/null +++ b/sidefs.py @@ -0,0 +1,36 @@ + +# "DG8SAQ specific values" +SI570_I2C_ADDR = 0x55 +SI570_DCO_HIGH = 5670.0 +SI570_DCO_LOW = 4850.0 +SI570_NOMINAL_XTALL_FREQ = 114.285 +SI570_XTALL_DEVIATION_PPM = 2000 +SI570_DEFAULT_STARTUP_FREQ = 56.32 + +REQUEST_READ_VERSION = 0x00 +REQUEST_SET_DDRB = 0x01 +REQUEST_SET_PORTB = 0x04 +REQUEST_READ_EEPROM = 0x11 +REQUEST_FILTERS = 0x17 +REQUEST_SET_BPF_ADDRESS = 0x18 +REQUEST_READ_BPF_ADDRESS = 0x19 +REQUEST_SET_LPF_ADDRESS = 0x1A +REQUEST_READ_LPF_ADDRESS = 0x1B +REQUEST_SET_FREQ = 0x30 +REQUEST_SET_MULTIPLY_LO = 0x31 +REQUEST_SET_FREQ_BY_VALUE = 0x32 +REQUEST_SET_XTALL_FREQ = 0x33 +REQUEST_SET_STARTUP_FREQ = 0x34 +REQUEST_READ_MULTIPLY_LO = 0x39 +REQUEST_READ_FREQUENCY = 0x3A +REQUEST_READ_SMOOTH_TUNE_PPM= 0x3B +REQUEST_READ_STARTUP = 0x3C +REQUEST_READ_XTALL = 0x3D +REQUEST_READ_REGISTERS = 0x3F +#REQUEST_SET_SI570_ADDR = 0x41 +REQUEST_SET_PTT = 0x50 +REQUEST_READ_KEYS = 0x51 + +HS_DIV_MAP = [4, 5, 6, 7, -1, 9, -1, 11] + + diff --git a/try.sh b/try.sh index 22cb505..f0eb666 100755 --- a/try.sh +++ b/try.sh @@ -9,7 +9,10 @@ #python iq.py --RTL --WATERFALL --rtl_gain=0 --n_buffers=12 --size=384 --REV # Audio test, BBB, iMic, USB 2.0 ~90% cpu load -python iq.py --index=-1 --size=256 --n_buffers=6 --WATERFALL --HAMLIB +#python iq.py --index=-1 --size=256 --n_buffers=6 --WATERFALL --HAMLIB # RTL Test, BBB ~95% cpu [ Set extra RTL delay in iq.py to zero] #python iq.py --RTL --WATERFALL --n_buffers=10 --size=384 + +# Audio test, PC, Si570 / SoftRock +python iq.py --SI570 --WATERFALL --index=0 --size=512 --n_buffers=8 --rate=48000