Close

Implementing the WSPR Task (skeleton)

A project log for Careless WSPR

A desultorily executed Weak Signal Propagation Reporter beacon.

ziggurat29ziggurat29 08/17/2019 at 15:270 Comments

Summary

The WSPR task skeleton is implemented.  This goes through all the motions of scheduling transmissions, shifting out bits at the correct rate, and some other things like re-encoding the message when GPS is locked and syncing the on-chip RTC witht he GPS time.

Deets

For the WSPR activity, I define another FreeRTOS task.  This will be the last one!  The task will run a state machine, cranking out the bits of the WSPR message on a scheduled basis.  It will be driven by two on-chip resources:  the Real Time Clock (RTC), and a timer (TIM4).  The RTC will be used for it's 'alarm' feature to schedule the start of a transmission at the start of an even-numbered minute, as required by the WSPR protocol.  The TIM4 will be used to pace the bits we send.  The WSPR protocol requires the bits to be sent at 1.464 Hz, or about 0.6827 bps.  I assigned some other duties to the WSPR task, such as keeping the RTC synced when GPS lock comes in.

CubeMX Changes for RTC and TIM4

The RTC is used to kick off a WSPR transmission when an even numbered minute begins.  The on-chip RTC has an 'alarm' capability that can be used to generate an interrupt at the designated time.  You will need to open CubeMX, and ensure that under the NVIC settings for the Timers, RTC, that the following is enabled:

    RTC alarm interrupt through EXTI line 17

The RTC interrupt works by way of a weak symbol callback function.  You simply define your own 'HAL_RTC_AlarmAEventCallback()' and that is sufficient for you to hook into the interrupt handler.  I put mine in main.c, since CubeMX likes to put things like this there, but that is not required.  The implementation is to simply forward the call into the WSPR task implementation by calling WSPR_RTC_Alarm(), which is exposed in the header 'task_wspr.h'.

While that kicks of the start of the transmission, the subsequent bits are shifted out under the control of a general purpose timer which also can generate an interrupt when the configured period expires.  In this case I am using TIM4, and under it's NVIC settings in CubeMX you need to ensure that the following is enabled:

    TIM4 global interrupt

While in CubeMX, we need to also set some values for TIM4 so that the interrupts come at the correct rate.  The timers are driven by the CPU clock.  We have configured to run at the maximum speed of 72 MHz, so we need to divide that down to 0.6827 Hz.  That means dividing down by 49,154,400.  The timers have a 16-bit prescaler and then also a 16-bit period, so we need to find a product of these two that will work out to be pretty close to that number.  I chose a prescaler of 4096, and a period of 12000, which works out to be 49,152,000 and that is close enough.

You set those values in CubeMX by subtracting 1 from each of them.  This is because 0 counts as dividing by 1.  So the prescaler will be 4095 and the period will be 11999.

Set those values and regenerate the project.  This will cause the init code to have changed appropriately.  Do not forget to re-apply the fixups that are in the #fixup directory!

The Timer interrupts work somewhat the same, but there is one for all the timers, and it already exists down in main.c because we configured TIM2 to be the system tick timer.  We just add to that implementation in the user block:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  /* USER CODE BEGIN Callback 0 */

  /* USER CODE END Callback 0 */
  if (htim->Instance == TIM2) {
    HAL_IncTick();
  }
  /* USER CODE BEGIN Callback 1 */

	else if (htim->Instance == TIM4)
	{
		//we use TIM4 for WSPR bit pacing
		WSPR_Timer_Timeout();
	}

  /* USER CODE END Callback 1 */
}

ISR Code

So, we have two methods WSPR_RTC_Alarm() and WSPR_Timer_Timeout() that are exposed by the task.wspr.h and implemented therein.  I generally avoid doing significant work in an Interrupt Service Routine (ISR) handler, and in this case those simply post some new task notification bits into the WSPR task.

in task_notification_bits.h

	//bits for the WSPR process
	TNB_WSPRSTART = 0x00010000,		//start the transmission
	TNB_WSPRNEXTBIT = 0x00020000,	//send next bit in transmission
	TNB_WSPR_GPSLOCK = 0x00040000,	//GPS lock status changed

and in task_wspr.c

//our bit clock timed out; time to shift a new bit
void WSPR_Timer_Timeout ( void )
{
	//we are at ISR time, so we avoid doing work here
	BaseType_t xHigherPriorityTaskWoken = pdFALSE;
	xTaskNotifyFromISR ( g_thWSPR, TNB_WSPRNEXTBIT, eSetBits, &xHigherPriorityTaskWoken );
	portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}


//our WSPR scheduled transmission should now begin
void WSPR_RTC_Alarm ( void )
{
	//we are at ISR time, so we avoid doing work here
	BaseType_t xHigherPriorityTaskWoken = pdFALSE;
	xTaskNotifyFromISR ( g_thWSPR, TNB_WSPRSTART, eSetBits, &xHigherPriorityTaskWoken );
	portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}

This causes the WSPR task to wake, and it will do the actual work there instead of in the ISR.  The latency between the ISR and the waking of the task is insignificant in this application, and by doing the work in 'user mode' instead of the ISR we are free to do pretty much anything we want.  (Many things cannot be done in an ISR, and you have to take special care to consider this.)

Also, the added GPS Lock bit is used to allow the GPS task to notify that it has acquired or lost a lock.  We use this opportunity on lock acquisition to set the RTC, and also to automatically compute the maidenhead locator based on lat/long.

void thrdfxnWSPRTask ( void const* argument )
{
	uint32_t msWait = 1000;
	for(;;)
	{
		//wait on various task notifications
		uint32_t ulNotificationValue;
		BaseType_t xResult = xTaskNotifyWait( pdFALSE,	//Don't clear bits on entry.
				0xffffffff,	//Clear all bits on exit.
				&ulNotificationValue,	//Stores the notified value.
				pdMS_TO_TICKS(msWait) );
		if( xResult == pdPASS )
		{

			//if we gained or lost GPS lock, do some things
			if ( ulNotificationValue & TNB_WSPR_GPSLOCK )
			{
				PersistentSettings* psettings = Settings_getStruct();
				if ( g_bLock )	//got a lock
				{
					//first, update the RTC time
					RTC_TimeTypeDef sTime;
					RTC_DateTypeDef sDate;
					sTime.Hours = g_nGPSHour;
					sTime.Minutes = g_nGPSMinute;
					sTime.Seconds = g_nGPSSecond;
					sDate.WeekDay = RTC_WEEKDAY_SUNDAY;	//(arbitrary)
					sDate.Date = g_nGPSDay;
					sDate.Month = g_nGPSMonth;
					sDate.Year = g_nGPSYear - 2000;
					HAL_RTC_SetTime ( &hrtc, &sTime, RTC_FORMAT_BIN );
					HAL_RTC_SetDate ( &hrtc, &sDate, RTC_FORMAT_BIN );

					if ( psettings->_bUseGPS )	//do we care about GPS?
					{
						//now, update the maidenhead
						toMaidenhead ( g_fLat, g_fLon, psettings->_achMaidenhead, 4 );

//XXX update our WSPR message
					}
				}
				else	//lost lock
				{
					//XXX we can carry on with our previous lock and RTC
					//WSPR_StopWSPR();
				}
			}

//XXX other bits handled here

		}
		else	//timeout on wait
		{
			//things to do on periodic idle timeout
		}
	}
}

Scheduling

Scheduling a WSPR transmission is straightforward:  get the current time, round up to the next even numbered minute, and set the RTC alarm.  It is a quirk of the RTC that any existing alarm must first be disabled before setting a new one, but we need a disable method anyway for other purposes.

extern RTC_HandleTypeDef hrtc;	//in main.c


//cancel any scheduled future WSPR transmissions
static void _impl_WSPR_CancelSchedule ( void )
{
	HAL_StatusTypeDef ret = HAL_RTC_DeactivateAlarm ( &hrtc, RTC_ALARM_A );
	(void)ret;
}


//schedule a WSPR transmission at the next interval
static void _impl_WSPR_ScheduleNext ( void )
{
	_impl_WSPR_CancelSchedule();	//ensure any alarms are off

	//get current time
	RTC_TimeTypeDef sTime;
	HAL_RTC_GetTime ( &hrtc, &sTime, RTC_FORMAT_BIN );

	//round up to next even minute start
	RTC_AlarmTypeDef sAlarm;
	sAlarm.Alarm = RTC_ALARM_A;
	sAlarm.AlarmTime = sTime;
	sAlarm.AlarmTime.Seconds = 0;	//always at start of minute
	sAlarm.AlarmTime.Minutes = (sAlarm.AlarmTime.Minutes + 3) & 0xfe;
	if ( sAlarm.AlarmTime.Minutes > 59 )	//check for rollover minute
	{
		sAlarm.AlarmTime.Minutes -= 60;
		++sAlarm.AlarmTime.Hours;
	}
	if ( sAlarm.AlarmTime.Hours > 23 )	//check for rollover hour
	{
		sAlarm.AlarmTime.Hours -= 24;
	}

	//set the alarm
	HAL_StatusTypeDef ret = HAL_RTC_SetAlarm_IT ( &hrtc, &sAlarm, RTC_FORMAT_BIN );
	(void)ret;
}

The exposed methods from the task uses these internally:

void WSPR_Initialize ( void )
{
//XXX extinguish signal; if any
	StopBitClock();	//can be running at app startup
	_impl_WSPR_CancelSchedule();	//unlikely at app startup, but ensure
	g_bDoWSPR = 0;
}


void WSPR_StartWSPR ( void )
{
	if ( ! g_bDoWSPR )	//not if we're already doing it
	{
		g_bDoWSPR = 1;
		_impl_WSPR_ScheduleNext();
	}
}


void WSPR_StopWSPR ( void )
{
	_impl_WSPR_CancelSchedule();
	g_bDoWSPR = 0;
}

Handling Alarm Events

As mentioned, the alarm ISR simply posts a task notification which will be handled later.  This is done in the task loop, when awakened by a task notification change.

WSPR beacons are meant to randomly decide between sending and receiving for any given transmission period.  We have a setting for duty cycle that determines how often this occurs.  It can be 0 for 'never' or 100 for 'always', and any value in between for an average percentage duty cycle.

Additionally, the WSPR protocol defines a 200 Hz bandwidth (within a 2.5 KHz USB audio channel).  However, the WSPR signal itself is only 6 Hz wide.  That means there are 33 1/3 'sub bands' within the 200 Hz (sub) band of the 2.5 KHz USB band.  The center of the 200 Hz band is always 1.5 KHz above what is called the 'dial frequency' of the USB audio channel.  WSPR receivers scan this band looking for transmissions and decode them.

The use of the duty cycle is obvious to allow multiple stations to stochastically avoid interference (in an Aloha-esque way), and I presume that the 200 Hz is to allow even then many to coexist at the same time.  As such, I also randomly pick a 6 Hz sub-band on which to transmit.  There is a provision to explicitly specify the sub-band though this feature is really just to help with testing.  I compute the effective band start frequency as:

    band start = dial frequency + 1.5 KHz - 200 Hz / 2 + sub-band * 6 Hz

Because there are 33 1/3 bands, and because 1/3 of a band is 2 Hz, I distribute that at the bottom and top as 'guard' and so I adjust the 200/2 part to 99, putting one Hz at the bottom and 1 Hz at the top.  I don't know if this is standard practice, it just seemed appropriate to me.

The handler for the notification of the start of transmission looks like this:

//if our scheduled WSPR start has occurred, start
if ( ulNotificationValue & TNB_WSPRSTART )
{
	if ( g_bDoWSPR )	//should be wspr'ing at all?
	{
		//randomize duty cycle selection
		int chance = rand() / (RAND_MAX/100);
		PersistentSettings* psettings = Settings_getStruct();
		if ( chance < psettings->_nDutyPct )
		{
			//start transmission
			StartBitClock();	//get bit clock cranked up
			//compute the base frequency.  first determine the
			//6 Hz sub-band within the 200 Hz window.
			int nIdxSubBand;
			if ( psettings->_nSubBand < 0 )	//random sub-band, 0 - 32
			{
				nIdxSubBand = rand() / (RAND_MAX/32);
			}
			else	//explicitly chosen sub-band
			{
				nIdxSubBand = psettings->_nSubBand;
			}
			//now compute the sub-band base frequency
			// fDial + 1.5 KHz - 100 Hz + nIdxSubBand * 6Hz
			//Note; adjusted 100 to 99 because there is 1/3 sub-
			//band extra in the 200 Hz, and this spreads that
			//evenly at the top and bottom.  Probably unneeded.
			g_nBaseFreq = psettings->_dialFreqHz + 1500 - 99 +
					nIdxSubBand * 6;
			//emit this first symbol's tone now
			//note, the frequency is in centihertz so we can get
			//the sub-Hertz precision we need
			g_nSymbolIndex = 0;
			uint64_t nToneCentiHz = g_nBaseFreq * 100ULL + 
					g_abyWSPR[g_nSymbolIndex] * 146ULL;
//XXX emit signal
_ledOnGn();
			g_nSymbolIndex = 1;	//prepare for next symbol
		}
		//irrespective of whether we are actually transmitting this
		//period, we should schedule to check in the next one.
		_impl_WSPR_ScheduleNext();
	}
}

This will keep a stream of 2-minute notifications coming in that will schedule the start of transmission.  I haven't implemented the WSPR encoder yet, or the signal generator from the Si5351, so I am now just setting the LED on so that I can visually see what is happening.  The 1.464 Hz bit rate makes this really easy to see with the human eye!

Once the transmission has been started, it is then bit shifted out under the control of the TIM4 resource.  This posts a different task notification bit.

Handling TIM4 Events

The subsequent bits are easier to handle, because most of the work has already been done.  We merely have to compute the tone that is appropriate for this bit period and emit it.  Again, we don't have the signal generator implemented yet, so we just twiggle the LED for visual confirmation that it is happening on-schedule:

//if it is time to shift out the next WSPR symbol, do so
if ( ulNotificationValue & TNB_WSPRNEXTBIT )
{
	if ( g_nSymbolIndex >= 162 )	//done; turn off
	{
_ledOffGn();
//XXX terminate signal
		StopBitClock();	//stop shifting bits
		g_nSymbolIndex = 0;	//setup to start at beginning next time
	}
	else
	{
		//emit this next symbol's tone now
		//note, the frequency is in centihertz so we can get the
		//sub-Hertz precision we need
		uint64_t nToneCentiHz = g_nBaseFreq * 100ULL + 
				g_abyWSPR[g_nSymbolIndex] * 146ULL;
//XXX emit signal
_ledToggleGn();
		++g_nSymbolIndex;
	}
}

And that's it!  Well, not quite.  Something's got to cause it to start at all.  I haven't really decided on the logic for that -- be it automatic once a lock is obtained, or is there a need for an explicit on/off capability, or whatever.  I consider that a separate activity, so in the short term I make a bogus command 'exp001' that takes a parameter of 'on' or 'off' and starts the process.

Handling 'exp001'

This was straightforward:

//========================================================================
//'exp001' command handler

#ifdef DEBUG

#include "task_wspr.h"

//this is used for doing experiments during development
static CmdProcRetval cmdhdlExp001 ( const IOStreamIF* pio, const char* pszszTokens )
{

	const char* pszArg1 = pszszTokens;
	if ( 0 == strcmp ( pszArg1, "on" ) )
	{
		//start (potentially) WSPR'ing at the next even minute
		WSPR_StartWSPR();
		_cmdPutString ( pio, "WSPR'ing started\r\n" );
	}
	else
	{
		//stop any WSPR'in
		WSPR_StopWSPR();
		_cmdPutString ( pio, "WSPR'ing stopped\r\n" );
	}

	return CMDPROC_SUCCESS;
}

#endif

So, irrespective of this being a temporary hack for testing, you can see that to start WSPR'ing, you just call WSPR_StartWSPR() and to stop you call WSPR_StopWSPR().

Now that's all there is to it!  The build was tested and verified to this point.

Next

Encoding a WSPR message

Discussions