Sunday, June 5, 2016

Summertime! Let's get out there and fox hunt! (Part 1)

With the recent 32 degree C (90 F) weather we have been having lately, I began thinking of summertime ham radio activities and I think it is time to build a hidden transmitter (fox) for our summer Salmoncon group activities.  My buddy Eldon (WA0UWH) and I have provided fox hunt activities for the group since about 2012.  This year I decided to make some changes.

Historically, we have provided 1 to 4 beacons on 30 metres and coached folks on the use of simple loop antennas in radio direction finding (RDF).  This year I have decided to add a VHF component to the hunt as most of the group owns VHF FM equipment.

Our summer camp site sits on about 50 acres up against the western slopes of the Cascade Mountains near North Bend, WA.  It is a truly idyllic location and a wonderful venue for our summer camp-out called Salmoncon.  If you are going to be in the area, we hope you will plan to drop in and visit.

Below and in a previous post I included a picture of a couple of our transmitter hunt participants from years past.  We have done the event every year and I have threatened to not do the event a couple of times but the gathering lynch mob convinced us otherwise.  So, it is a simple thing, but apparently it is quite popular.



What I want to do here is to provide the details of this year's hidden transmitter hardware and firmware so that others who might be interested in hosting such an event might be able to leverage the effort to best benefit.


Here is my little fox hunt transmitter that is able to transmit CW on any HF frequency you like while simultaneously sending an FM signal on 2 metres.



As you can see it fits nicely on the side of a standard 9V battery.  I have attached it with a little blob of putty that is used to adhere things to the wall in your home but is still removable without damaging the wall.  I find that it holds really well, but not as strongly as for example double sided foam tape.  Not shown are the wires that attach to HF and VHF antennas.  These connect to two of the pins along the side of the board.  In practice this gets wrapped in bubble wrap and stuffed into a short length of PVC pipe.  Pipe end caps provide water and dust protection.  Small eye bolts are used to connect the electronics to external antennas and to provide a convenient way to hang the transmitter up in a tree for example using the antenna wire as a support.

The board is a Parallax Propeller Mini board.  I have written previously on various projects that I have based on this processor.  This is a slick little board packing a 32 bit, eight core 160 MIPS 80 MHz processor which is complete overkill for this project, but a key feature of the Propeller chip makes it ideal for this application.

Each core has two counter modules.  Each counter module can control or monitor up to two I/O pins and perform conditional 32-bit accumulation of its FRQ register into its PHS register on every clock cycle.  Each counter module also has its own phase-locked loop (PLL) which can be used to synthesize frequencies up to 128 MHz.  In reality it can synthesize frequencies up to about 220 MHz, but Parallax recommends "for best stability" to limit frequency synthesis to 128 MHz maximum.  I am using it at 146 Mhz without issue.

Let me first discuss the CW HF beacon.  Typically we use four of these transmitters hidden around the venue.  Each is assigned a serial number 0-3 which is used to determine the content of the transmitted beacon.  We picked morse code characters that had the exact same transmission length for each of the beacons based on serial number.  V, F, L, and B.  The beacon will send this letter 5 times on a schedule discussed below.  By turning on all four transmitters simultaneously, the fox hunters will hear zero or more of them identifying at the same time.  The goal is to locate them all and we typically keep the game running for several days to give everyone a chance.  If there is interest and the foxes have been found, we move them.  (Don't forget to check the collar of any of the dogs on site for a transmitter...)

The Propeller chip is programmed in one of two languages.  SPIN or PASM.  Spin is a C-like (sort of) language while PASM is the Propeller ASseMbly language.  All of my code presented in this posting will be in the SPIN language.  I am not going to provide a tutorial on the language, for that you can hit the reference manual for the gory details.  If you get stuck, drop me a line at ko7m at arrl dot org and I will try to help you out.

The top part of the code declares the clock speed, PLL settings and crystal frequency.  This is followed by any constants used by the program.

' HF CW Fox Hunt Beacon
'
' ko7m - Jeff Whitlatch
'
con
  _CLKMODE = XTAL1 + PLL16X
  _XINFREQ = 5_000_000

The following constants are used in generating random numbers and to fine tune the timing accuracy of a delay function.

  Seed = 31414
  WMin = 381

Each transmitter gets a 6 second slot for transmitting, chosen at random and the transmitters re-sync random number generation every two minutes. 
  
  'Sync Each Ten Minutes So that Others can be be started Later, by watching the wall clock
  TimeSync  = 60 * 2            ' Seconds between time syncs (2 min)
  SlotTime  = 6                 ' Seconds
  TimeSlots = TimeSync / SlotTime ' Number of time slots (in this case 20)

We transmit at 15 words per minute on a frequency of 10.1395 MHz with a tone offset of 600 hz.  Here we also define which pin is used to generate the RF at this frequency.  Any available pin may be chosen for this task.

  wpmInit      =         15      ' Initial code speed in words per minute
  defaultFreq  = 10_139_500      ' Default transmit frequency
  defaultTone  =        600      ' Offset from transmit frequency
  defaultRFPin =         16      ' RF output default pin

This data section defines an array of bytes used to identify each transmitter.  These are the letters sent (repeated 5 times) during each time slot.  For example the transmitter with the serial number 2 will send VVVVV in morse code in randomly chosen time slots.

Here we also declare an object instance of the frequency synthesizer object used to generate RF energy.  This module is provided as part of the standard modules available with the development tools for the Propeller processor.  The source code is provided and nothing is done here that you cannot do yourself in your own code.
    
dat
  Ids      BYTE "BFVL"           'Equal Length Single Character IDs, one for each SN

obj
  Freq  : "Synth"

Local variables are now defined.

var
  long TimeSlot
  long Rand
  long WPM
  long Frequency                ' Current frequency
  long toneFreq                 ' Offset from frequency in Hz
  
  byte RFPin
  byte ditTime                  ' Time in milliseconds for a dit (dot)
  byte SN                       ' Serial number (set by switches)
  long Stack[128]
  BYTE Cog

Public functions now follow.  Main performs initialization and then runs the CW beacon routines.

pub Main
  Init
  doHFFox

The Start and Stop functions allow starting this code up on a different core and stopping it.  This allows me to implement both the HF and VHF transmitters in a way that they can function simultaneously.

PUB Start : fSuccess 
  fSuccess := (Cog := cognew(Main, @Stack) + 1) > 0

PUB Stop
  if Cog
    cogstop(Cog~ - 1)

This is the main function that does all the magic.  It schedules the random slot selections and at the appropriate time sends the identifier character in morse code based on the serial number.

During slots 1 and 5, the fox with serial number 0 transmits.  Slots 2 and 6, serial number 1 transmits and so forth.  During slot 9 all foxes transmit and during slot 0, nobody transmits.  The effect is that you may hear zero to four foxes all talking at once during each time slot.

pri doHFFox | Task, C
  C := cnt
      
  repeat
    waitcnt(C += (clkfreq * SlotTime) #> WMin)          ' Wait for our slot time                                       '
    C := cnt                                            ' Get the current clock value

    SN := 2                                             ' Serial number is hard coded - should be on switch input                        

    if TimeSlot == 0                                    ' Reset Sequence
      Rand := Seed

    Task := ||?Rand // 10                               ' Random task number (only use last digit)
    
    case (Task)
      1,5:
        if SN == 0
          sendId(SN)                    ' Send my ID at different times based on serial number and task                        
      2,6:
        if SN == 1
          sendId(SN)
      3,7:
        if SN == 2
          sendId(SN)
      4,8:
        if SN == 3
          sendId(SN)
      9:
        sendId(SN)                                      ' Everybody sends
      0:
        sendNothing                      ' Noboday sends (silence is heard for time required to identify)                       
    
    TimeSlot := (TimeSlot + 1) // TimeSlots

Now, we have the private functions.  Init handles initialization setting the frequency, morse code speed, RF pin to use, etc.

pri Init  
  TimeSlot  := 0
  Rand      := Seed
  WPM       := wpmInit
  ditTime   := 1200 / WPM                        ' Calculate dit time based on 50 dit duration standard
  Frequency := defaultFreq
  toneFreq  := defaultTone
  RFPin     := defaultRFPin                       

Now we have the various "send" functions that handle the actual morse code transmissions.  sendId send the single identifier character based on serial number 5 times.  sendNothing does exactly that.  sendCode works with sendSymbol to allow morse code transmission of strings of characters.

pri sendId(_SN)
  repeat 5
    sendSymbol(Ids[_SN])                                ' Send the associated ID for the serial number

pri sendNothing
  
pri sendCode(stringptr)
  repeat strsize(stringptr)
    sendSymbol(byte[stringptr++])                       ' Send each symbol of the stream

sendSymbol is the main function that provides the conversion of an ascii character into a series of morse code symbols that are used to key the RF energy on and off forming morse code characters.  The character itself is used as the index into a lookup table of morse code symbols for that character.  The function then forms properly constructed morse code and keys the transmitter.

pri sendSymbol(char) | cwBits, dotsToWait, iCodes
  if char == " "                                        ' Handle space character as 7 dot times                
    dotsToWait := 5
    delay(dotsToWait * ditTime)
  else
    if char => "a" and char =< "z"                      ' Convert lower case to upper case
      char := char - "a" + "A"
    iCodes := lookdown(char: "!", $22, "$&'()+,-./0123456789:;=?@ABCDEFGHIJKLMNOPQRSTUVWXYZ_")
    ' If unsupported character, ignore it
    if iCodes == 0
      return
    cwBits := cwCodes[iCodes - 1]                       ' Grab the CW bit codes for this symbol                        

    repeat while cwBits > 1
      if cwBits & 1
        dotsToWait := 3         ' dah
      else
        dotsToWait := 1         ' dit
        
      keyDown
      delay(dotsToWait * ditTime)   
      keyUp                      ' stop sending
      delay(ditTime)              ' one dot time between symbols
      cwBits >>= 1                ' get the next symbol (dit or dah)
                            
  delay(ditTime * 2)            ' two more dot times to give 3 dot character spacing                      

The following functions are use to control the frequency synthesizer to actually generate RF energy.  sendTone turns on an RF carrier at the requested frequency plus a tone offset value.  noTone turns off that RF carrier.  These functions are used by keyDown and keyUp to generate morse code keying transitions.  delay is a general purpose function to delay for the specified number of milliseconds.

pri sendTone(tone)
  Freq.Synth("A", RFPin, Frequency + tone)              ' 

pri noTone
  Freq.Synth("A", RFPin, 0)

pri keyDown
  sendTone(toneFreq)            ' Sidetone or transmit

pri keyUp
  noTone                        ' Stop sidetone or transmit
    
pri delay(Duration)
  waitcnt(((clkfreq / 1_000 * Duration - 3932) #> WMin) + cnt)

This data block is used  to define the morse code characters as described in the comments below.  Each character is stored in a byte where each one bit represents a dash (or dah) and a zero bit indicates a dot (or dit) in morse code. 
         
dat
  ' Morse code tables below are encoded as "1" being a dah and "0" being a dit.  The end of the character
  ' is signified by the most significant "1" bit.  Thus, while shifting the byte value right, we can easily
  ' test for the end of character condition by checking to see if the current value is > 1.
  '
  '            !         "         $         &         '         (         )         +         ,         -         .         /     
  cwCodes byte %1110101, %1010010, %11001000,%100010,  %1011110, %101101,  %1101101, %101010,  %1110011, %1100001, %1101010, %101001
  '            0         1         2         3         4         5         6         7         8         9
          byte %111111,  %111110,  %111100,  %111000,  %110000,  %100000,  %100001,  %100011,  %100111,  %101111
  '            :         ;         =         ?         @         
          byte %1000111, %1010101, %110001,  %1001100, %1010110 
  '            A         B         C         D         E         F         G         H        
          byte %110,     %10001,   %10101,   %1001,    %10,      %10100,   %1011,    %10000
  '            I         J         K         L         M         N         O         P        Q        R        
          byte %100,     %11110,   %1101,    %10010,   %111,     %101,     %1111,    %10110,  %11011,  %1010
  '            S         T         U         V         W         X         Y         Z        _ 
          byte %1000,    %11,      %1100,    %11000,   %1110,    %11001,   %11101,   %10011,  %1101100

          
In the next installment, I will describe the 2 metre FM fox.  Stay tuned!  For your reference I have pasted the entirety of the code above for convenience.  If you use this code, paste it into its own file and save it in a directory where the rest of the fox hunt transmitter files will be stored.  Name the file something like HFFox.spin.  The .spin suffix is used for SPIN program files.

' HF CW Fox Hunt Beacon
'
' ko7m - Jeff Whitlatch
'
con
  _CLKMODE = XTAL1 + PLL16X
  _XINFREQ = 5_000_000

  Seed = 31414
  WMin = 381
   
  'Sync Each Ten Minutes So that Others can be be started Later, by watching the wall clock
  TimeSync  = 60 * 2            ' Seconds between time syncs (2 min)
  SlotTime  = 6                 ' Seconds
  TimeSlots = TimeSync / SlotTime ' Number of time slots (in this case 20)

  wpmInit      =         15      ' Initial code speed in words per minute
  defaultFreq  = 10_139_500      ' Default transmit frequency
  defaultTone  =        600      ' Offset from transmit frequency
  defaultRFPin =         16      ' RF output default pin
    
dat
  Ids      BYTE "BFVL"                          'Equal Length Single Character IDs, one for each SN

obj
  Freq  : "Synth"

var
  long TimeSlot
  long Rand
  long WPM
  long Frequency                ' Current frequency
  long toneFreq                 ' Offset from frequency in Hz
  
  byte RFPin
  byte ditTime                  ' Time in milliseconds for a dit (dot)
  byte SN                       ' Serial number (set by switches)
  long Stack[128]
  BYTE Cog

pub Main
  Init
  doHFFox

PUB Start : fSuccess 
  fSuccess := (Cog := cognew(Main, @Stack) + 1) > 0

PUB Stop
  if Cog
    cogstop(Cog~ - 1)

pri doHFFox | Task, C
  C := cnt
      
  repeat
    waitcnt(C += (clkfreq * SlotTime) #> WMin)          ' Wait for our slot time                                       '
    C := cnt                                            ' Get the current clock value

    SN := 2                                             ' Serial number is hard coded - should be on switch input                        

    if TimeSlot == 0                                    ' Reset Sequence
      Rand := Seed

    Task := ||?Rand // 10                               ' Random task number (only use last digit)
    
    case (Task)
      1,5:
        if SN == 0
          sendId(SN)                                    ' Send my ID at different times based on serial number and task                        
      2,6:
        if SN == 1
          sendId(SN)
      3,7:
        if SN == 2
          sendId(SN)
      4,8:
        if SN == 3
          sendId(SN)
      9:
        sendId(SN)                                      ' Everybody sends
      0:
        sendNothing                                     ' Noboday sends (silence is heard for time required to identify)                       
    
    TimeSlot := (TimeSlot + 1) // TimeSlots

pri Init  
  TimeSlot  := 0
  Rand      := Seed
  WPM       := wpmInit
  ditTime   := 1200 / WPM                               ' Calculate dit time based on 50 dit duration standard
  Frequency := defaultFreq
  toneFreq  := defaultTone
  RFPin     := defaultRFPin                       

pri sendId(_SN)
  repeat 5
    sendSymbol(Ids[_SN])                                ' Send the associated ID for the serial number

pri sendNothing
  
pri sendCode(stringptr)
  repeat strsize(stringptr)
    sendSymbol(byte[stringptr++])                       ' Send each symbol of the stream

pri sendSymbol(char) | cwBits, dotsToWait, iCodes
  if char == " "                                        ' Handle space character as 7 dot times                
    dotsToWait := 5
    delay(dotsToWait * ditTime)
  else
    if char => "a" and char =< "z"                      ' Convert lower case to upper case
      char := char - "a" + "A"
    iCodes := lookdown(char: "!", $22, "$&'()+,-./0123456789:;=?@ABCDEFGHIJKLMNOPQRSTUVWXYZ_")
    ' If unsupported character, ignore it
    if iCodes == 0
      return
    cwBits := cwCodes[iCodes - 1]                       ' Grab the CW bit codes for this symbol                        

    repeat while cwBits > 1
      if cwBits & 1
        dotsToWait := 3         ' dah
      else
        dotsToWait := 1         ' dit
        
      keyDown
      delay(dotsToWait * ditTime)   
      keyUp                      ' stop sending
      delay(ditTime)              ' one dot time between symbols
      cwBits >>= 1                ' get the next symbol (dit or dah)
                            
  delay(ditTime * 2)            ' two more dot times to give 3 dot character spacing                      
                                
pri sendTone(tone)
  Freq.Synth("A", RFPin, Frequency + tone)              ' 

pri noTone
  Freq.Synth("A", RFPin, 0)

pri keyDown
  sendTone(toneFreq)            ' Sidetone or transmit

pri keyUp
  noTone                        ' Stop sidetone or transmit
    
pri delay(Duration)
  waitcnt(((clkfreq / 1_000 * Duration - 3932) #> WMin) + cnt)
         
dat
  ' Morse code tables below are encoded as "1" being a dah and "0" being a dit.  The end of the character
  ' is signified by the most significant "1" bit.  Thus, while shifting the byte value right, we can easily
  ' test for the end of character condition by checking to see if the current value is > 1.
  '
  '            !         "         $         &         '         (         )         +         ,         -         .         /     
  cwCodes byte %1110101, %1010010, %11001000,%100010,  %1011110, %101101,  %1101101, %101010,  %1110011, %1100001, %1101010, %101001
  '            0         1         2         3         4         5         6         7         8         9
          byte %111111,  %111110,  %111100,  %111000,  %110000,  %100000,  %100001,  %100011,  %100111,  %101111
  '            :         ;         =         ?         @         
          byte %1000111, %1010101, %110001,  %1001100, %1010110 
  '            A         B         C         D         E         F         G         H        
          byte %110,     %10001,   %10101,   %1001,    %10,      %10100,   %1011,    %10000
  '            I         J         K         L         M         N         O         P        Q        R        
          byte %100,     %11110,   %1101,    %10010,   %111,     %101,     %1111,    %10110,  %11011,  %1010
  '            S         T         U         V         W         X         Y         Z        _ 
          byte %1000,    %11,      %1100,    %11000,   %1110,    %11001,   %11101,   %10011,  %1101100

          

1 comment: