Exploring software-defined radio (without the annoying RF)—Part 2
Editor’s Note: See Part 1 here
In the last installment we looked at the ultrasonic hardware used in the development of a software designed ultrasonic data transmission system we are calling the SDU-X.
In this second, and last, installment we will discuss the firmware that enables the system to send and receive data using software defined modulation schemes.
Wow the engineering world with your unique design: Design Ideas Submission Guide
The SDU-X firmware
(The Arduino source code can be downloaded in the link at the bottom of this article.)
It needs to be mentioned that an Arduino Nano will have a number of constraints when creating the SDU-X firmware. These range from low clock speed, a small amount of RAM, a large tolerance on the resonator frequency, and the lack of a hardware floating point. So long as we’re aware of the constraints we can work around them.
So, knowing the constraints, let’s figure out a sample rate for the 40 kHz carrier. Nyquist would say it needs to be greater than 80 kilo-samples per second (ksps) but typical practice is to be at least 5 times the 40 kHz. For various reasons, and much testing, I settled for 8 times the carrier, or 320 ksps. For a baud rate, and again after much testing, I decided on 250 baud. Yes, it’s slow but sufficient for sending data back and forth from solar panels or for experimenting. (For those that remember, early PCs communicated at 300 baud over phone lines.)
Sending bytes to the DAC, for transmission, 320k times per second means we will have time to execute less than 50 instructions between bytes. Obviously, we cannot calculate sine values for the transmitted waveform on the fly, so we need to precalculate these. But again, we are constrained by the amount of RAM we have to store this data. A symbol would require 320000/250 = 1280 bytes, and we would need one array for a “0” symbol and another for a “1” symbol. That’s 2560 bytes—that’s 2k more than the Nano has.
(Let me stop for a second to make the point that even on the larger SDR system I worked on, even though there was a lot of RAM and speed, these were still constraints that needed to be worked around. The point is that developing on the Nano is a good proxy for the development on larger SDR systems.)
So, the trick for this scarcity of memory issue I came up with was to break symbols (carrier modulated by “0”s and “1”s) into subsymbols which are ¼ of the symbol time. At transmission time, the subsymbol will be repeated 4 times to make a full symbol. Subsymbols now only require 320 bytes each for “0” and “1”. Note that the subsymbols need to connect together without a discontinuity in the sine wave, whether it is connecting to a “1” or a “0”. This was part of the reason the sample rate and baud rate numbers, above, were chosen.
There is one piece of firmware, and it is shared by both the Requester and the Responder. You can compile it for use in the Requester, or compile it for use in the Responder by changing one “#define” statement in the code. To compile code for the Requester, the line towards the top of the main code that reads “#define REQUESTER” should be active, not commented out. To compile code for the Responder that same line should be commented out.
Starting in the main “loop”, there is a section to select a modulation scheme we would like to use. There are several types to choose from but only four are fully implemented: On-Off Keying (OOK), Binary Phase Shift Keying (BPSK), NONE, and Noise. By uncommenting the OOK line alone and compiling the code, we will be communicating using OOK as the modulation. Selecting BPSK by uncommenting that line alone will allow communications using BPSK. Selecting NONE will transmit short bursts of unmodulated 40 kHz sine waves. Selecting Noise transmits short bursts of somewhat random noise.
Now, let’s look at the Requester compiled code that is surrounded with “#ifdef REQESTER” and “#endif” which tells the compiler to include this code in the Requester compile but not in the Responder compile. The main pieces of Requester code in this block are:
Get the packet to send
Preprocess the packet
Create the modulated symbols
Break the packet into modulated symbols
Transmit the packet
Receive the packet sent from the Responder
Print received info to the serial port
First, we get some data to send. Currently this is a call to a routine that sets some hardcoded data bytes to send, and a fixed length. But the intention is for the user to modify this to send what they want such as something from the serial port, I2C port, or a timed command for a task like periodic data logging. In the Requester, the first, and sometimes the second, byte is currently used to identify the data it wants the Responder to send or the command it wants executed.
Packet structure
Following this is a call to preprocess the data packet (Figure 5). This first moves a preamble to the packet to be transmitted. This preamble is based on the modulation selected. The preamble is useful for finding a signal amplitude, which aids in slicing. The preamble also contains a bit pattern to assist in syncing to the start of a symbol (bit). It also has a bit sequence to signal the start of the actual data (this is known as the “start-of-packet delimiter”). For OOK modulation, this routine also inserts some pilot bits into the data packet to break up long strings of “1”s or “0”s, which can inhibit symbol sync in the receiver. Lastly, the preprocessor routine creates a 16-bit CRC and appends it to the end of all its bytes of the packet (preamble, data, pilot bits, and now CRC).
Figure 5 The packet structure SDU-X packet structure with the preamble, data bytes, pilot bits, and CRC.
After that we execute one of the tricks used to save us cycles—we create the modulated subsymbols by predetermining parts of the carrier wave that we will transmit. Using a trig function during transmission would reduce execution to a crawl, so we do it in advance. There is a set of routines to do the calculations for a few modulation types and a couple of test types.
To save time in the transmit routine, we next breakdown all the packet bytes to be sent into an array of bits. This is wasteful in memory but saves significant time when we start transmitting the bits at 320k bps.
Transmitting the packet
We are now ready to transmit the packet. After some initialization, we enter a loop controlled by a timer set to the bit time. The code polls the timer until it sees the timer’s timeout bit set, and if it is, it is time to send the next byte to the DAC. The byte is grabbed, in sequence, from the correct precalculated array. This means if the next symbol is a “1”, the byte is taken from the precalculated array built for transmission of a “1” modulated symbol. If it is a “0”, then the byte is drawn from the recalculated array for the “0” modulated symbol. After the 320 subsymbol bytes are sent, they are repeated three more times to complete a full symbol. After this, the next bit to be sent is used to select the correct, precalculated array. This process continues until all bytes in the packet are sent.
During this time that the Requester was transmitting, the Responder was receiving the transmission. To sample the 40 kHz received signal the sampling routine would need to sample at something faster than 80 ksps to satisfy the Nyquist criterion. The maximum speed of the Nano’s ADC is around 9 ksps for 10-bit resolution so, for both OOK and BPSK, the input signal is actually downsampled (taken at less than Nyquist criteria). This means the 40 kHz modulated signal will appear in a new, lower, frequency.
To do the sampling in the Responder a timer is again initialized and then polled in the receiving loop to obtain samples at the correct time. Samples are read from the Nano’s ADC which is connected to the receiver amplifier. OOK and BPSK start to diverge from here, so I’ll just break each down the major steps for each.
OOK: The sample is taken at 4800 sps. The absolute value of the sample is taken and filtered to get a signal that goes up and down with the transmitted “1”s and “0”s. It then finds the mid-value of the two levels so it can slice the stream to capture the bits, but it can’t slice until it syncs to the timing of the filtered symbol stream. This is done by noticing transitions of the filtered stream rising or falling past the mid-value. We also count samples so we can keep synced when there are no transitions such as “000000” or “111”. Now that we have a sync we can wait for the time-center of the symbol and then slice (above mid-value is a “1”, below is a “0”).
While the code is gathering bits by slicing, it is also comparing the last 16 bits received to the defined start-of-packet delimiter. If they compare, then the next bit received is the first data bit.
Bits are collected until the known number of bits in a complete packet have been received. Then a CRC is calculated from the collected bits and compared to the CRC received in the data packet. If they agree, it is marked as a good packet and the receive routine returns to the main code, otherwise the process is reinitialized and the system starts receiving again.
BPSK: This is similar to OOK in that it is downsampled, but is sampled at a more controlled 4500 sps (and the timer is hand calibrated). Due to aliasing, the 40 kHz phase modulated signal will now appear at 500 Hz. The samples are now run through a correlator which is simply a multiplication of the current sample with a sample exactly one symbol time previously, and run through an averager of a length equal to the number of samples in a symbol. The correlator output is now a rising and falling signal and a mid-value is calculated, which will be used for slicing. But slicing is different than OOK. When the correlator output is above the mid-value, it signals that the symbol has not changed from the last symbol. If the correlator output is below the mid-value, it signals that the symbol has changed from the last symbol. So, we are actually using differential BPSK (DBPSK). Again, we need to sync the symbol timing first. A sync occurs as the correlator output passes the midpoint and will occur at every 4 ms after that (4ms is 1/Baud). Slicing is then executed at each sync time.
Similar to OOK, we also search for the 16 bits of the start-of-packet delimiter but with a twist. Because this is differential detection, we don’t know if we will get the bits as defined or the complement of the bits. This is because we did not know if we should have started from a “1” or a ”0”. Therefore, the start-of-packet delimiter code does a compare of both and, if it compares to the complement, it complements the last bit to get differential bit tracking corrected.
Along the way of receiving both OOK and BPSK the signal level and noise level are computed so it can later be output to the user.
It’s interesting to note that, even though OOK is considered a simpler modulation scheme, the OOK receiver code takes more time to execute than the BPSK code. It takes about 170 µs, worst case, to process an OOK symbol and a BPSK symbol takes about 100 µs. Correlation in BPSK is a much more elegant and efficient detection method than energy detection as used in OOK.
One point to remember—transmit code is always easier to write than receiver code. This is mostly due to the fact that receiver code has to deal with variable receive amplitudes and also needs to find and track symbol edges for syncing. It also must scan for the preamble. Transmit code also executes faster than the receiver code as everything it needs is prepared in advance. That’s why we can use much higher sample rates on the transmitter, which is necessary to create the modulated signal.
Now, let’s take a look at the Responder code that is surrounded with “#ifdef RESPONDER” and “#endif” which tells the compiler to include this code in the Responder compile but not in the Requester compile. The main pieces of the Responder code are executed very much like the Requester but the “Receive the packet” is the first thing called. The receiver code is executed until a valid packet is found. After a valid packet is found, the Responder code returns to the main code and calls a routine to execute the command that it received. Inside this routine, the data to send back to the Requester is also set up. The Responder then executes the steps to send the packet, as described above.
Examples
Included with the code are some debug code snippets (located in the tab labeled “Debug”). These can be used while experimenting with the code to view some parameters and look at timing in the code, or checking to see if certain parts of the code are executed.
For example, to view a value of a variable I sometimes send the value to the DAC and watch the DAC output with a scope probe on TP2.This is useful for looking at things like the correlator values but must be scaled for the 8-bit DAC output. You will find an example of this code commented out in the BPSK code.
An example of checking timing is placing code to set TP11 high and then low around the lines of code you want to time (then subtract 3.6 µs for digitalWrite execution time). Setting TP11 high can be done with the line “digitalWrite(TP11, ON)” and setting TP11 low can be done with the line “digitalWrite(TP11, OFF)”. Setting TP11 high and then low is also useful to see things like when the sync is found. You will also find an example of this code commented out in the BPSK code.
That’s the code.
Wrap Up
If you’re interested in the SDU-X you can download all the info such as:
Source code
PCB design
Schematic
PCB design and artwork (KiCad format)
Bill of materials
Design notes
STL files for 3D printing the parabolic transmit/receive tower
Find them at: https://www.thingiverse.com/thing:6268613
There are many other modulation schemes to explore such as FSK, QPSK, QAM, etc. It may also be an interesting exercise to add error correcting to the packet and explore improvements in packet errors versus S/N. Adding an address to the packet may also be interesting in exploring systems with multiple Requesters and Responders. A couple of the more advanced areas to explore are using the power of I/Q data and the beauty of negative frequencies.
After you experiment with the SDU-X for a while you may want to explore other uses for the hardware. Since it is flexible, you can create code to make the hardware perform different functions. Some ideas are measuring distance or speed of an object, or measuring windspeed and direction, or downsampling the receiver stream and listening for sound in the 40 kHz band.
I hope the SDU-X is useful to those that want to learn more about SDR or are teaching others the basics of SDR.
Damian Bonicatto is a consulting engineer with decades of experience in embedded hardware, firmware, and system design. He holds over 30 patents.
Phoenix Bonicatto is a freelance writer.
Related Content
Exploring software-defined radio (without the annoying RF) – Part 1
Obtaining a patent in a corporate environment
Simple GPS Disciplined 10MHz Reference uses Dual PWMs
Time for a second 3D printer in the lab
googletag.cmd.push(function() { googletag.display(‘div-gpt-ad-native’); });
–>
The post Exploring software-defined radio (without the annoying RF)—Part 2 appeared first on EDN.

