An SX Morse Code Keyer

By Guenther Daubach

Im Eulenflug 25
D-51399 Burscheid, Germany
eMail: guenther@g-daubach.com
Home: www.g-daubach.com

This application makes use of an SX Controller to build a Morse Code Keyer with the following features:

The schematic below shows the required external components:

If you are using an SX-Key Demo Board from Parallax, the required components are already in place.

To read the speed potentiometer, this application makes use of the bitstream ADC VP published by Ubicom and Parallax. The LED at RB6 is optional; it gives optical feedback as it is toggled according to the generated Morse code. RB6 is also used to drive an external circuit that keys the transmitter.

If connected to RB7, the piezo speaker provides an acoustic feedback. As most transmitters generate their own monitor tones, you might consider to not install the speaker. You may also add a switch to turn the speaker on or off.

RB0 is the "Dot" input, i.e. when this pin is pulled low, a short "Dot" signal will be generated, followed by a pause of the same length. As long as the line is held low, "Dots" and pauses will be repeated.

When RB1, the "Dash" input is pulled low, a "Dash" signal will be generated, that is three times longer than a "Dot". Again, a pause of one dot-length follows each "Dash". As long as the line is held low, "Dashes" and pauses will be repeated.

When you use a "Paddle-Type" input device, either the "Dot" or the "Dash" input can be low. If you use a "Squeeze-Type" device instead, either the "Dot", the "Dash", or both inputs can be low at a time. When both inputs are low, the "Dot" input has higher priority, i.e. "Dots" will be generated as long as it is low. When the line is released while the "Dash" line is still low, the system will continue sending "Dashes". If you pull the "Dot" line low, while the "Dash" line is low, "Dots" will be generated again.

If you press the "CQ" button, the CQ message, e.g. "cq cq cq de dk4tt dk4tt dk4tt" will be continuously sent and repeated.

When you press the "AR" button while the CQ message is being sent, this message will be completed, and then the AR message, e.g. "ar pse k" will be sent once, before the system enters into idle mode.

Pressing the "AR" button when the CQ message is not currently being sent, starts the transmission of one AR message before the system goes back to idle.

You can stop any automated message by pulling low the "Dot" or "Dash" lines. In this case, the message is interrupted after having completed the current character, the system sends a "Dot" or "Dash" (depending on what line went low), and returns to idle mode, i.e. it continues monitoring the RB3…0 input lines.

This is the program:

SX_KEY

ifdef SX_KEY
  DEVICE  SX28L, STACKX_OPTIONX, OSCXT5
  FREQ    50_000_000
else
  DEVICE  SX28AC, OSCHS, OPTIONX
endif
DEVICE TURBO
RESET  Start

TRIS  equ $0f
LVL   equ $0d
PLP   equ $0e

Frequ equ 125	; Beep frequency

org      $08	; Global registers
FsrSave  ds 1	; Storage for FSR 
Speed    ds 1	; Morse code speed
State    ds 1	; ISR Morse handler state
Flags    ds 1	; Flags to control the mainline
	;  program
Ix       ds 1	; Message table index
Count    ds 1	; Storage for "Morse Bitcount"
Char     ds 1	; Storage for Morse character

SendCq   =  Flags.0	; State flags for mainline program
SendAr   =  Flags.1
SendDot  =  Flags.2
SendDash =  Flags.3
Tone     =  State.7	; When this flag is set, a Dot or Dash
	;  is generated instead of a pause,
	;  and the speaker pin RB7 is toggled
	;  so generate a tone (Frequ deter-
	;  mines the tone frequency)

org        $10	; Registers used by the ISR
IsrVars    =  $
PortBBuff  ds 1	; Buffer for Port B data
Beep       ds 1	; Beep timer for speaker output
Length     ds 1	; Determines the length of the
	;  current signal or pause in
	;  multiples of a dot length
SpeedTimer ds 1	; This timer controls the length
	;  of a dot, i.e. the CPS of the
	;  Morse code
BaseTimer  ds 1	; This is the basic timer used to
	;  divide down the ISR call frequency

org        $30	; ADC registers
ADC        =  $
AdcCount   ds 1	; Overflow counter
AdcAcc     ds 1	; Accumulator
PortCBuff  ds 1	; Buffer for Port C data

org    $000

;--------------------------------------------------------------
ISR ; The Interrupt Service Routine
;--------------------------------------------------------------
  mov     FsrSave, FSR	; Save the original FSR for later
	;  restore
       
  ;-------------------------------------------------------
  ; ADC VP    rc.1 = ADC in, rc.0 = charge/discharge
  ;-------------------------------------------------------
        
  bank    ADC
  mov     PortCBuff, rc
  and     PortCBuff, #%11111100	; Mask ADC pins
  mov     w, >>rc
  not     w
  and     w, #%00000011
  or      PortCBuff, w
  sb      PortCBuff.0
    incsz AdcAcc
  inc     AdcAcc
  dec     AdcAcc
  mov     w, AdcAcc
  inc     AdcCount
  snz
    call  PotAdjust	; Transform ADC value (0...255)
	;  into 32...160
  snz
    mov   Speed, w	; Save the transformed value
  snz
    clr   AdcAcc
  mov     rc, PortCBuff	; Set the ADC pins
  page    ISR	; Reset the page (changed
	;  by PotAdjust)
  ;-------------------------------------------------------
  ; Morse code handler
  ;-------------------------------------------------------
       
  bank    IsrVars

  ; If a button is down, set the associated flag for the
  ; mainline program.
  ;
  sb      rb.0	; Dot line
    setb  SendDot
  sb      rb.1	; Dash line
    setb  SendDash
  sb      rb.2	; CQ button
    setb  SendCq
  sb      rb.3	; AR button
    setb  SendAr

  ; Morse timer states
  ;
  Idle      = 0
  Pause1    = 1
  Pause3    = 2
  Pause5    = 3
  Dot       = 4
  Dash      = 5
  Delay     = 6
  DelayS    = Delay | $80

  mov     w, State	; Get the current state
  and     w, #%01111111	;  and ignore bit 7
  jmp     pc+w
  jmp     :ExitIsr
  jmp     :InitPause1
  jmp     :InitPause3
  jmp     :InitPause5
  jmp     :InitDot
  jmp     :InitDash
  jmp     :Delay

:InitPause1	; Init for 1 dot-length pause
  mov     Length, #1
  jmp     :EndInitP
       
:InitPause3
  mov     Length, #2	; Init for 3 dots-lengths pause.
	;  As a one dot-length pause is
	;  automatically appended  to each
	;  dot and dash, the actual pause
	;  length is 2 dots here.
  jmp     :EndInitP

:InitPause5
  mov     Length, #4	; Init for a 5 dots-lengths pause,
	;  (see above).
:EndInitP
  mov     SpeedTimer, Speed	; Initialize the speed timer
  mov     State, #Delay	; Set next state
  jmp     :ExitIsr
       
:InitDot
  mov     Length, #1	; Setup for a dot
  jmp     :EndInitD
       
:InitDash
  mov     Length, #3	; Setup for a dash (3 dots-lengths)
       
:EndInitD
  mov     SpeedTimer, Speed	; Initialize the speed timer
  mov     State, #DelayS	; Set next state
  jmp     :ExitIsr
       
:Delay	; Cause a delay
  decsz   BaseTimer	; Decrement the base timer
    jmp   :EndDelay
  decsz   SpeedTimer	; Decrement the speed timer
    jmp   :EndDelay
  mov     SpeedTimer, Speed	; Re-initialize the speed timer
  decsz   Length	; Decrement the length counter
    jmp   :EndDelay
  sb      Tone	; If a dot or dash is finished,
    jmp   :EndPause
  mov     State, #Pause1 	;  automatically add a 1 dot-length
  jmp     :EndDelay	;  pause

:EndPause
  mov     State, #Idle	; When a pause has been finished, idle

:EndDelay
  clrb    PortBBuff.6	; Prepare the LED bit
  sb      Tone	; When the Tone flag is clear,
    jmp   :ExitIsr	;  no LED and no sound, else
  setb    PortBBuff.6	;  turn LED on and
  decsz   Beep	;  decrement the Beep timer
    jmp   :ExitIsr	; 
  xor     PortBBuff, #$80 	; Toggle the beeper pin
  mov     Beep, #Frequ	; Re-initialize the Beep timer

:ExitIsr
  mov     rb, PortBBuff	; Set the port pins
  mov     FSR, FsrSave	; Restore the FSR
  mov     w, #-200	; Call the ISR every 4 us
  retiw

;--------------------------------------------------------------
; This routine reads the value indexed by w from Table.
; The table is used to transform the ADC values from 0 to 255 
; into a range from 32 to 160, and to provide a better pot
; resolution for higher speed values.
;--------------------------------------------------------------
PotAdjust
  page    Table
  jmp     w

org       $100

;--------------------------------------------------------------
; The mainline program
;--------------------------------------------------------------

Start
  ; Initialize the ports
  ;
  mode PLP
  mov  !rb, #%11110000	; Enable pull ups on port B inputs
  mode    LVL	; Set cmos input levels
  mov     !rc,#0	;  on port C inputs
  mode    TRIS	; Setup inputs/outputs
  clr     rc
  mov     !rc,#%11111110	; rc.1 = ADC in
	; rc.0 = charge/discharge
  clr     rb
  mov     !rb, #%00111111         ; rb.7: beeper output,
	; rb.6: LED output
  
  clr     PortBBuff	; Clear some registers
  clr     Flags
  clr     State

  mov     !option, #%10011111	; Enable RTCC interrupt

; The bits in Flags are used to control the states of the main
; loop.

:MainLoop
  snb     SendDot	; When the dot contact is closed, go
    jmp   :SendDot	;  and send a dot (highest priority)
  snb     SendDash	; When the dash contact is closed,
    jmp   :SendDash	;  go and send a dash
    
  test    Flags	; If no flags are set at all, we are
  snz	;  idle
    jmp   :MainLoop
    
  mov     w, #ArTab	; Prepare the address to send the
	;  "+ pse k" message
  sb      SendAr	; If not SendAr, prepare the address
  mov     w, #CqTab	;  to send the "cq cq cq de..."
	;  message
  mov     Ix, w	; Setup the message pointer
  snb     SendAR	; If the user has pushed the AR 
	;  button, while sending the
	;  "cq cq..." message, stop the CQ
	;  message after it is completed.
    clrb  SendCQ

:Loop
  mov     w, Flags	; Get the flags
  and     w, #%00001100	; Mask the SendAr and SendCq flags
  sz	; If no flags are set,
    jmp   :MainLoop	;  don't send a message
  snb     SendCQ	; While sending the "cq..." message,
    jmp   :NoDebounce	;  AR button pushes are accepted.
  snb     SendAr	; While sending the "+ pse k" message,
    clr   Flags	;  no more AR button pushes are
		;  accepted.
:NoDebounce
  mov     m, #CqTab >> 8	; Setup m:w for iread
  mov     w, Ix
  iread							; Read the table item indexed by Ix
  mov     Char, w	; Save the dash/dot pattern
  mov     Count, m	; Save the dash/dot count
  test    Count	; When Count = 0, we have either a
  snz	;  pause, or an end of table. We	
    jmp   :TestEnd	;  go to :TestEnd to find out.

:Next	; Output a Morse character
  rl      Char	; Next bit -> c
  mov     w, #Dash	; Prepare for a dash
  sc	; If c is set, it is a dash,
    mov   w, #Dot	;  else a dot
  mov     State, w	; Setup the state

:Send
  test    State	; Wait until the ISR has sent the
  sz	;  dash or dot
    jmp   :Send
  dec     Count	; Decrement the dash/dot count
  sz	; If there are more dashes/dots to be
    jmp   :Next	;  sent, loop back
  mov     State, #Pause3	; Generate a 3 dots-length pause

:Pause
  test    State	; Wait until the ISR has generated
  sz	;  the pause
    jmp   :Pause

  inc     Ix	; Next character in the message table
  jmp     :Loop

:TestEnd	; When a table item has a Count of 0,
  test    Char	;  it is either a Pause5 (Char != 0),
  snz	;  or the end of the table (Char == 0)
    jmp   :MainLoop
  mov     State, #Pause5	; Setup state for Pause5
  jmp     :Pause	; Go and wait until the pause is done

:SendDot
  mov     State, #Dot	; Send a dot when the user has pressed
  jmp     :WaitSend	;  the dot button

:SendDash
  mov     State, #Dash	; Send a dash when the use has pressed
  	;  the dash button
:WaitSend	; Wait until the ISR has sent the
  test State	;  dash or the dot
  sz
    jmp   :WaitSend
  clr     Flags	; Clear the flags in order to stop
  jmp     :MainLoop	;  any message being currently sent.

org       $200
       
; The message tables
;
; The first four bits in each item specify the number of
; dashes and dots. The next eight bits specify from "left
; to right" the dashes (1) and dots (0).
;
; A value of $001 specifies a 5 dot-lengths pause, and 
; a value of $000 specifies the end of a message.
;
CqTab
  dw      $001	; Pause 5
  dw      $400 + %10100000	; C
  dw      $400 + %11010000	; Q
  dw      $001	; Pause 5
  dw      $400 + %10100000	; C
  dw      $400 + %11010000	; Q
  dw      $001	; Pause 5
  dw      $400 + %10100000	; C
  dw      $400 + %11010000	; Q
  dw      $001	; Pause 5
  dw      $300 + %10000000	; D
  dw      $100 + %00000000	; E
  dw      $001	; Pause 5
  dw      $300 + %10000000 	; D
  dw      $300 + %10100000	; K
  dw      $500 + %00001000	; 4
  dw      $100 + %10000000	; T
  dw      $100 + %10000000	; T
  dw      $001	; Pause 5
  dw      $300 + %10000000	; D
  dw      $300 + %10100000	; K
  dw      $500 + %00001000	; 4
  dw      $100 + %10000000	; T
  dw      $100 + %10000000	; T
  dw      $001	; Pause 5
  dw      $300 + %10000000	; D
  dw      $300 + %10100000	; K
  dw      $500 + %00001000	; 4
  dw      $100 + %10000000	; T
  dw      $100 + %10000000	; T
  dw      $001	; Pause 5
  dw      $000	; End

ArTab
  dw      $001	; Pause 5
  dw      $500 + %01010000	; AR
  dw      $001	; Pause 5
  dw      $400 + %01100000	; P
  dw      $300 + %00000000	; S
  dw      $100 + %00000000	; E
  dw      $001	; Pause 5
  dw      $300 + %10100000	; K
  dw      $000	; End

org       $400
Table
          retw  32, 32, 32, 32
          retw  33, 33, 33, 33
          retw  34, 34, 34, 34
          retw  35, 35, 35, 35
          retw  36, 36, 36, 36
          retw  37, 37, 37, 37
          retw  38, 38, 38
          retw  39, 39, 39
          retw  40, 40, 40
          retw  41, 41, 41
          retw  42, 42, 42
          retw  43, 43, 43
          retw  44, 44, 44
          retw  45, 45, 45
          retw  46, 46, 46
          retw  47, 47, 47
          retw  48, 48, 48
          retw  49, 49, 49
          retw  50, 50
          retw  51, 51
          retw  52, 52
          retw  53, 53
          retw  54, 54
          retw  55, 55
          retw  56, 56
          retw  57, 57
          retw  58, 58
          retw  59, 59
          retw  60, 60
          retw  61, 61
          retw  62, 62
          retw  63, 63
          retw  64, 64
          retw  65, 65
          retw  66, 66
          retw  67, 67
          retw  68, 68
          retw  69, 69
          retw  70, 70
          retw  71, 71
          retw  72, 72
          retw  73, 73
          retw  74, 74
          retw  75, 75
          retw  76, 76
          retw  77, 77
          retw  78, 78
          retw  79, 79
          retw  80, 80
          retw  81, 81
          retw  82, 82
          retw  83, 83
          retw  84, 84
          retw  85, 85
          retw  86, 86
          retw  87, 87
          retw  88, 88
          retw  89, 89
          retw  90, 90
          retw  91, 91
          retw  92, 92
          retw  93, 93
          retw  94, 94
          retw  95, 95
          retw  96
          retw  97, 97
          retw  98, 98
          retw  99, 99
          retw 100, 100
          retw 101, 101
          retw 102, 102
          retw 103, 103
          retw 104, 104
          retw 105, 105
          retw 106, 106
          retw 107, 107
          retw 108, 108
          retw 109, 109
          retw 110, 110
          retw 111, 111
          retw 112, 112
          retw 113, 113
          retw 114, 114
          retw 115, 115
          retw 116, 116
          retw 117, 117
          retw 118, 118
          retw 119, 119
          retw 120, 120
          retw 121, 121
          retw 122, 122
          retw 123, 123
          retw 124, 124
          retw 125, 125
          retw 126, 126
          retw 127, 127
          retw 128, 128
          retw 129, 129
          retw 130, 130
          retw 131, 131
          retw 132, 132
          retw 133, 133
          retw 134, 134
          retw 135, 135
          retw 136
          retw 137
          retw 138
          retw 139
          retw 140
          retw 141
          retw 142
          retw 143
          retw 144
          retw 145
          retw 146
          retw 147
          retw 148
          retw 149
          retw 150
          retw 151
          retw 152
          retw 153
          retw 154
          retw 155
          retw 156
          retw 157
          retw 158
          retw 159
          retw 160

In this application, the mainline program initializes the ports and some registers, and then enters into a loop that handles sending pre-defined messages. It also handles the button-down and Morse-key device events but it does not read the associated input lines. Instead, it evaluates the states of the flags eventually set by the ISR which actually reads the input lines.

The ISR runs the ADC VP that is used to read the current setting of the speed potentiometer. The ISR also polls the input lines at Port B, and takes care of the necessary timing to generate "Dots", "Dashes", pauses, and the audio signal for the piezo speaker.

The timer part of the ISR is designed as a state engine. Depending on the value of the State variable, various parts of the ISR code are executed.

As pauses, "Dots", and "Dashes" also require a time delay with the only difference that "Dots" and "Dashes" must control the output signal, and also must generate the audio signal for the speaker, bit Status.7 has a special meaning. If this bit is set, "Dots" or "Dashes" are sent, i.e. the speaker and output lines will be activated in this case.

The two pre-defined messages (the CQ and the AR message) are stored in program memory. The program uses the iread instruction to read items from those tables.

As Morse code characters have variable lengths, the first four bits of a table entry are used to specify the total number of "Dots" and "Dashes" for each character. The remaining eight bits contain the Morse code pattern. Each "Dot" is represented by a 0-bit, and each "Dash" is represented by a 1-bit. The "Dash" and "Dot" codes are arranged from "left" to "right", i.e. the first code element is located at bit 7, the second one at bit 6, etc. When a Morse character has less than eight "Dashes" and "Dots", the remaining lower bits are cleared.

To either indicate a pause of five dot-lengths or the end of a message, the upper four bits of a table entry are cleared. When the lower eight bits are also cleared, this means that the end of the table has been reached. If the lower eight bits contain %00000001, a pause of five dot-lengths will be generated instead.

The ADC VP used here, returns a result from 0 through 255 for an input voltage between 0 and 5 V. Using this full range to setup the Speed timer would result in extremely high and low Morse speeds when the potentiometer is turned to its two end-positions. In addition, when you use a linear potentiometer, the speed variation is very sensitive at higher speeds. Therefore, we use a table in order to convert the ADC value (0…255) into a range from 32 through 160 and to "flatten" the potentiometer curve at higher speed values.

Questions: