QRPp WSPR – WSPR Software

The real impetus for converting the PIC software from assembler to C was my dissatisfaction with the original code I'd written for the heater control loop for the crystal "oven". It worked but I'd taken a shortcut with processing the output of the analog to digital converter to be able to use 8 bit arithmetic.

The shortcut was mixing signed and unsigned 8 bit arithmetic – perhaps less of a shortcut than an expedient move to avoid a wholesale re-write in the midst of debugging the code!

Enough was enough! The old code had to go – rather than re-write an already way too long effort in assembler, I decided to go to C.

The re-write wasn't that bad – I'd written C-like pseudo code as the design template for the assembler code so I cut and pasted the pseudo code as the skeleton of my C version.

WSPR – The C Version

All the code is contained in two files built under BoostC from SourceBoost Technologies.

This version of the code reflects the addition of an external GPS receiver to provide Time of Day information and a very accurate 1 pulse per second real-time clock.

Most of the routine names are consistent with the earlier assembler version for QRSS.

The bulk of the processing is done in main() – after some basic initialization, a while loop is entered that controls operation of the controller. TXInhibit is monitored and if FALSE causes the main WSPR send loop to be entered. This loop controls the external switched attenuators and causes the WSPR message to be sent by calling SendWSPR().

The variables Relay1 and Relay2 are mapped to IO outputs and determine whether the WSPR message is to be sent at full or reduced power. Three different WSPR messages are stored in program memory and are selected based on the output power level being used. The current implementation uses two switched attenuators to reduce the 23 dBm to 17 and 10 dBm. In between transmissions, both relays are turned on to maximize the attenuation offered to leakage from the crystal oscillator (which runs all the time for maximum stability). This was done in an attempt to be able to receive on the same band while the QRPp transmitter is in operation – however, even with the attenuators, the leakage still results in a strong signal locally – no real surprise here!

SendWSPR() sets up some variables that are used by the interrupt() routine to signal that a WPSR message is to be sent (wGO TRUE), a pointer to the message to be sent and a count of the number of symbols sent starting at 0. SendWSPR() waits for the start of the next even minute period, delays one second and then signals the interrupt() routine to start processing the message.


This version of the controller code relies heavily on interrupts for its operation. There are four sources of interrupt.

  • GPS 1 pulse per second via RB0 status change
  • Timer 1 soft tick
  • Serial receive
  • Serial transmit

See the section on GPS integration below for more on the first two interrupt sources.

Serial receive is used to process data being received over the serial port from the GPS. An incoming message is accumulated into a RAM buffer until the number of expected characters has been received. A semaphore (rxSem) is used to handle timeouts (lack of receive data) and to show the end of message has been reached. rxSem is set to the timeout value in seconds by GPSStatus() – the timeout value is decremented as part of the soft tick interrupt processing and is set to 0 when the receive character count reaches 0.

Serial transmit processes data to be sent by the transmitter. The same buffer is used for both receive and transmit data. A transmission is initiated (in GPSSend()) by turning on the transmitter and allowing the TX buffer empty interrupt to trigger interrupt() to send the first and subsequent bytes from the buffer. An extra pad byte is queued into the transmitter after the last byte of data has been written into the transmit data holding register (txreg) to make sure that all the bits of the last byte are sent over the serial port before we turn off the transmitter. This allows the interrupt routine to gracefully turn off the transmitter and not have to poll the TRMT bit (txsta) that shows the transmit shift register is empty.

GPS Integration

I use an ancient (circa 1998) Motorola Oncore UT+ as a GPS receiver. This is an 8 channel GPS unit (board) that provides GPS information (date, position, speed, heading etc.) and a very accurate 1 pulse per second TTL compatible signal.

I had a couple of these units on hand and you can find them for around $50 on EBay.

The 1 pps is taken to the RB0 input line on port B of the PIC. This is set to generate an interrupt on the rising edge of the 1 pps signal. The GPS data is read via the PIC USART which is tied to the TTL level serial port on the Oncore.

The GPS data is polled via GPSStatus() which monitors the GPS receiver to a) make sure it has a valid position lock (so that time and date information is valid) and b) to extract the minute and second data from the GPS Position/Status message from the Oncore.

The minute/second data is used to identify even minute periods and initiate the transmission of WSPR messages. The global variable eEpoch is set TRUE by the interrupt routine at the start of an even minute period. eEpoch needs to be cleared after use in order to detect the next even minute event.

The 1 pps interrupt is used to calibrate the number of soft ticks in a second. The soft tick is generated by the timer 1 overflow interrupt – timer 1 is driven by a 32.768 Khz (nominal) crystal that drives the PIC built in oscillator for timer channel 1. Timer 1 will generate 1024 ticks per second IF the oscillator frequency was exactly 32.768 KHz – in my case, there's nop trimmer on the crystal so the period is what it is!

The RB0 interrupt handler saves the number of soft ticks in a second (tSecLO and tSecHI), sets the second semaphore (sSec) which is used by Wait1Sec() to delay until the start of the next second, and determines whether an even minute period is beginning to set eEpoch.

The soft tick via the timer 1 overflow interrupt is used for two purposes:

  • As a backup in case the GPS 1 pps is missing or not connected. This makes sure that the background processing still works – in particular that Wait1Sec() eventually exits and that background tasks like running HeatCtrl() to handle the crystal oven heater still run. This way there is a safeguard to make sure the oven heater is turned off and doesn't get left ON!
  • Drives the WSPR symbol timer to establish the symbol period and select the next symbol to be transmitted

All of the logic to control the WSPR symbol selection is handled in interrupt(). The code block:

if (wGO) {

processes the symbol timer (symLO and symHI) and controls the address lines to the analog multiplexor to select the next WSPR symbol. symCount is incremented to check for the end of the WSPR message. Once the last symbol is sent, the transmitter is turned off and the status LEDs are updated.

WSPR Symbol Timer

The count of soft ticks in a second is used in Wait1Sec() to continually update the number of soft ticks in a WSPR symbol period. Each WSPR symbol is sent for (8192/12000) seconds – 0.6826666 seconds. The symbol period derives from the size of the Fast Fourier Transform (FFT) that the WSPR software uses during message decode and the audio sample rate from the PC sound card (12,000 samples/second).

Wait1Sec() calculates the values for sClkHI and sClkLO which contain the high and low order bytes of the count. These are used in the soft tick interrupt processing to establish new values for the symbol timer to account for drift.

GPS not really needed

You don't need GPS to make WSPR work. I tried this earlier with just the soft tick from timer 1 and using the removal of TXInhibit to signal the start of an even minute. Here's how…

Start by assuming that the 32.768 KHz crystal is exactly on frequency. This establishes a base line for the soft ticks in a second as 1024 and allows the number of soft ticks in a symbol period to be calculated using the assumed 1024 value as accurate.

We assume that TXInhibit is removed at the top of an even minute and so we set eEpoch to true before calling SendWSPR().

Run the transmitter into a dummy load and run the WSPR software on a PC connected to a receiver monitoring the transmission. The PC clock should be accurately set using the Windows NTP client or a third party software product like 4th Dimension. At the end of each WSPR decode, WSPR will show the dT value – a measure of how accurately times this transmission was synchronized.

If you removed TXInhibit on exactly the top of an even minute period and the PC clock is accurate, dT should show as 1.0 seconds.

Run the transmitter for a few WSPR transmissions (lets say N back to back messages) and see how the value of DT changes. If dT decreases, then the number of soft ticks per second is too low – if dT increases, its too high.

You can calculate the per second error as follows

Delta change in dT (start dT – end dT) / (N * 120)

N = number of back to back transmissions
120 = length of each WSPR period

This tells you the per-second error in the soft timer tick count. You convert this into the number of ticks to be added (or removed) by dividing the error per second by (1/1024). Add or subtract this number of ticks from the count of soft ticks in a second that are held in TICKHI (high byte) and TICKLO (low byte).

Now recalculate the number of soft ticks in a symbol period given this new value for the soft ticks per second.

This gave more than enough accuracy to run the QRPp WSPR transmitter for a day or more without reset. During this time, some residual drift in the 32.768 KHz clock accumulated and showed up as dT slowly getting longer (in my case) over the course of a couple of days.

I didn't want to have to keep resetting the soft clock by periodically inhibiting the transmitter and the re-enabling it so I added the GPS to fix the issue in software.

You can find the non-GPS version of the code below – note that this version has the ugly version of the heater control loop still present but commented out.







The comments to this entry are closed.