Updated: Oct 9, 2021
This section describes the structure of the programming and the adaptations necessary to make it work within the limited processing power of the Arduino. There are other, faster, processors available but, for me, the enjoyment of this project is in finding solutions within the limits.
To create the program structure, I defined 256 "timeslots" that repeat sequentially and continuously once the set up tasks have been completed. In order to visualise this I imagined it as a matrix of 8 columns and 32 rows as shown below.
In the actual code there are just 8 scheduling segments, represented as a row in the matrix. Each segment contains the Analog to Digital conversion (ADC) module and 1 other. These are all run 32 times (as per the columns) before the sequence returns to the start. The current row is defined by a counter which is used to determine which modules to execute in that row.
The modules fall into 6 main categories which I have colour coded in the diagram. The individual modules in each category split the overall objective into sections of code that can run in a timeslot without exceeding the sampling time interval. I used counters and flags to provide continuity between modules and ensure they were run in the correct order. I will give an overview of each category with a few examples of the individual modules.
The ADC samples the signal in every timeslot and provides the input value into other modules. The raw signal is a voltage in the range 0 - 3.3v and this gives an integer in the range 0 - 1023. The module subtracts the zero level value (initially determined in the setup) to give the positive and negative waveform values required by the Goertzel algorithm.
This module also records the maximum input signal every 16 samples to provide a peak value that can be used to keep track of the volume and a running total to allow an average value to be calculated to track any changes in the zero level.
The most important module in this category is the carrier detection (SCD) which times the interval between zero level crossing points and looks for consistent values that indicate a carrier tone. This frequency is then calculated and passed to the carrier processing modules.
The carrier detection module is supported by signal level and zero level monitoring modules that set the threshold for measuring a signal zero level crossing. The signal level measurement module introduces a delay to the incoming signal to allow time for any threshold changes to take effect before the CW detection processes see the output carrier level. It also attempts to distinguish a valid signal from noise and apply a digital "squelch" to suppress spurious output when the noise level is high.
I have included the Goertzel algorithm modules in this category as these are responsible for extracting the carrier level from the incoming signal. The Goertzel calculation runs every 8th timeslot and the output values are produced every 64th timeslot (32 samples per output and 4 parallel processes).
The Goertzel algorithm is "tuned" to the carrier frequency by changing the sampling frequency. This means that the algorithm can be simplified and removes the need for floating point arithmetic which is too slow to fit into a timeslot. The downside is that this means that the timeslots get shorter for higher carrier frequencies. but this is accounted for in the allocation of modules to timeslots.
The frequency response of the Goertzel output for each of the programmed timeslot periods (μs) is shown below.
The CW decoding is broken down into 6 modules which execute sequentially each time a new Goertzel output is produced. The carrier state detection module (CSD) runs first to detect the high-low and low-high transitions. This includes a number of checks to avoid false triggers and produces the high/low state time as an output.
The timing modules (CLT & CHT) extract the minimum low and high times respectively and use these to calculate the thresholds that determine whether a high time is a dot or dash and whether a low time is a symbol, character or word gap. These thresholds need to change dynamically to track changes in the data timing but also maintain some stability to filter out spurious timing anomalies that can cause a cascade of errors. The timing refinement module (CTR) does some of this filtering and also adjusts the thresholds to take account of uneven high/low bit times and long/short inter word and character spaces.
The remaining CW modules generate the CW code for character output and add spaces for word breaks as appropriate. These codes are put into a buffer for output to the display by the next module.
A single display module runs continuously every 8 timeslots to take character codes from the buffer and send them to the display. This is done in 8 stages, each setting 1 bit at a time, to avoid exceeding the timeslot processing time, with a counter used to keep track of the current stage. The LCD display uses a 4 bit interface so a write enable pulse is sent every 4 stages to complete the data transfer.
This final category includes the modules for the user controls and indications which are limited to a single push button and a tricolour led.
The button allows the character display to be paused to note down relevant details and a second press resumes the display. There is no character buffer in this version though so any characters sent during the pause will be lost.
The signal level indicator (SLI) module takes an output from the signal level measurement module and sets the colour of the led to indicate if the input signal volume is within the range for decoding. This allows the user to adjust the source volume accordingly.
It is important to ensure that the code in a timeslot does not take up more time than the current ADC period. This would cause the sampling intervals to be irregular and would disturb the output from the Goertzel algorithm. By avoiding the time consuming instructions such as real-time arithmetic, I was able to keep each module down to 10-20μs. As a final check I incremented a test variable in the loop that waits for the period time to expire and checked that this executed at least once in each timeslot.
I have now proved that this program methodology and structure can successfully decode a wide range of CW audio. The next part will look at performance examples and limitations.