Is CW Digital?

Is CW Digital?

by Walt Fair, Jr., W5ALT

Some people like to claim that CW was the first digital communications mode. Others dispute that claim and say it isn't, citing various reasons. In this section, I'll take a hard look at CW and decide whether or not it really is a digital mode. In the process I'll also take a deeper look at the data structure of CW. I hope you aren't bored to death!

So What IS Digital?

One definition that I found of the term "digital" is as follows: "Representation of quantities in discrete (quantized) units. A digital system is one in which the information is contained and manipulated as a series of discrete numbers, as opposed to an analog system, in which the information is represented as a continuous trace or curve of the quantity constituting the signal." Well, CW is sent by either turning a carrier on or off, which is 2 states, so it qualifies as digital by that definition.

But things are never quite as simple as they seem! Here's another definition that I found: "A description of data that is stored or transmitted as a sequence of ones and zeros. Most commonly, this means binary data represented using electronic or electromagnetic signals." So by that definition, CW wouldn't be considered digital unless it is also binary.

At this point I can take 2 approaches. One is just to accept the first definition and say that CW is digital since it involves representing things in discreet states rather than continuous. The other is to figure out whether CW can be represented in binary with 1's and 0's or not. If it can, then it is also digital by the second definition. I'll take the second approach, since I prefer a little bit of rigor here and there.

It turns out that it is fairly easy to define CW and Morse code in terms of a binary code that can be implemented. And if I can implement it in binary, then it must be binary. In mathematical terms that would be known as a "proof by construction" - if we show exactly how to do something, then it must be true. So where do we start with CW?

First, let's define a machine, mechanical or electrical, that generates a 1 or 0 level signal of duration δt at each time increment and further specify that the machine can do nothing else. During each time increment δt it generates a pulse or it doesn't. Obviously we can control such a machine with a string of binary 1's and 0's fed to it at the proper times. Further, let's define the time increments as 1.2/WPM seconds, so each pulse would be equal in length to a Morse dit. I think we would all agree that I just defined a digital, binary machine by whatever definition we might choose. So now we must show that it is possible to use this machine to generate Morse code.

If I want to use such a machine to send CW I can send a 10 to form a dit and an element space and a 1110 to form a dah and an element space. After an element I can send 00 to make a space between characters and after a word I can send a 00000 to generate the space between words. So that means I can send various sequences of 1's and 0's to string together dits and dahs and spaces in any combination that I want. Note that not all possible sequences are valid for Morse, since combinations like 0110 should never occur. But that isn't important, the important thing is that I can easily represent strings of Morse symbols in a binary code and feed those strings to my ideal machine and it will generate perfect Morse code.

So is Morse code and CW digital? You bet it is, by either definition!

So How Can a Computer Send CW?

That is almost an easy question, since I just described a binary machine that can send CW. The trick is that we need to figure out how to generate the correct sequence of 1's and 0's. It should be simple to make a lookup table that takes any letter or symbol and returns the correct sequence. For example, "A" would give 101110, "B" would give 1110101010, etc. In fact I can even simplify it a little more and say that the table only contains a 0 for a dit and a 1 for a dah, or vice versa. So that way "A" would be 01 and "B" would be 1000, etc. The software then takes the letter or symbol (probably as an ASCII code), retrieves the table entry and substitutes 10 for every 0 and 1110 for every 1 in the table code. Nice compact, easily programmed - and it almost works.

The problem you'll soon discover if you try to take that approach is that computer memory and most programing languages store data in fixed size chunks that are normally considered to be 8 or 16 or some multiple of 8 bits wide. So if we store 01 for "A" in an 8-bit memory it might look like 00000001 or like 01000000 or something and there's no way for us to tell what is really being stored. We could always store the table as "strings," but that uses 8 bits for each 0 or 1, and doesn't seem too efficient.

The solution I usually use is to store the data with a 1 added on the front, so "A" becomes 101, "B" becomes 11000, etc. Now we can save those in an fixed bit sized register as "A" = 00000101 = 5, "B" = 00011000 = 24, etc. When I'm ready to send CW, I look up the code based on the ASCII character, start counting from the left side of the bit string until I get to a 1, then send either 10 for a 0 or 1110 for a 1 until I get to the end, then send 00 to form the space at the end of a letter. The only exception is for the "space", which simply requires sending 00000 and can be handled as an exceptional case.

Well, almost. If you're working with 16 bit computer memory or normal integers, that works just fine. But if you are limited to 8-bit memory or want to use 8-bit numbers to save space, you run into the inevitable exception. The longest CW sequences are things like "period" = 1010101, "comma" = 1110011, etc. Those all fit into 7 bits. However, there is the official code for "error" which is 8 dits, so with the leading 1 it would require 9 bits to store. The solution is either to never make mistakes or to handle it as another special case.

So now that I've shown how to generate perfect CW in a binary machine, can there be any doubt left that Morse code and CW is really binary?

And Then There Are Soundcards

In case you were wondering, no, you can't just send 0's and 1's as described above directly to a soundcard to generate CW. If you tried, all you'd hear is some sort of weak buzzing sound bearing no resemblance to CW. (No, I'll never admit whether or not I tried it!) To generate CW using a PC sound card you have to generate the complete audio signal waveform, so that's a little more troublesome. But since you might be interested if you've read this far, here's how you do it.

I'm not going to get into actual soundcard interrupts, etc. You'll have to figure that out on your own or find one of the soundcard handling widgets, components, controls, etc. But you do need to understand how a common PC soundcard works for output. Essentially the soundcard output is a "digital to analog converter" (DAC). You feed it a number and it outputs a voltage. And it does that at a specified time increment that depends on the sample rate. Most soundcards have a 16-bit DAC, which means you give it a 16-bit number and it generates a voltage corresponding to that number. The 16-bits is nice, since most computers represent integers as 16-bit numbers, so we can represent all of the signals that can be sent to a soundcard as a string of integers inside the PC. But the soundcard (or the soundcard software drivers) take care of sending the integers at the right times, so all we need to do is fill a "buffer" with the integers that we want sent. You can think of a buffer as an array of memory, if you're not familiar with the term.

In practice most soundcards handle stereo, so they actually have 2 DAC's, and the integers representing the voltages for each channel are interleaved, so the first integer is the right channel, the 2nd the left channel, the 3rd the right channel at the next time increment, etc. But the driver allows us to use mono-sound, too, so for mono we only need one string of integers. In addition, most soundcards operate at some standard sampling rates. Most handle up to 44kHz sampling, which is equivalent to audio CD quality sound. OK, it's really 44,200 Hz if you want to get precise. Most can also handle lower rates for 22,100 and 11,050 Hz just fine, too. There are other higher quality cards that handle high sample rates, use more than 16 bits, etc., but I'll just talk about the standard soundcard here.

What if you want to save the sound in a WAVE file? No problem! You can look up the specifications for a WAVE file to get the exact format, but essentially you just save the string of integers to a file with appropriate header information to define the sample rate, number of channels, number of bits per sample, the file size, etc.

So that aside, here's the problem: We have a string of 1's and 0's and we need to send a pulse every time period there's a 1 and send silence every time period there's a 0. We need to know 3 basic things. First, what is the audio frequency of the CW tone, what code speed are we trying to send and what sample rate that the soundcard is set for.

Let's start with the soundcard. The time increment is equal to 1/S, where S is the sample rate in Hz. So for a 44,200 Hz sample rate, each sample occurs at intervals of 1/44200 = 0.000022624 sec. So we can start with 0 and for every sample we generate, add 1/44200 seconds to find the elapsed time. That's easy enough.

Next lets look at the audio frequency. The time for 1 wavelength is 1/F where F is the audio frequency in Hz, so for a 800 Hz tone, the audio sound wave takes 1/800 = 0.00125 sec. That also means that for every audio cycle, we will need to generate 44200/800 = 55.25 samples, or integer numbers.

Finally, lets think about the element time, the length of a dit. We already know that the dit-length is 1.2/W where W is the code speed in words per minute. That means that a dit comprises (1.2 F/W) audio cycles and will occupy (1.2 S/W) audio samples. So for 15 WPM CW at a frequency of 800 Hz and a 44200 Hz sample rate, a dit will occupy 0.08 sec, be comprised of 64 cycles and take up 3536 audio samples. So now we to generate those audio samples.

There are, as in most things, various way to generate CW on a computer. First, we know that a perfect audio tone is a sine wave and its cycle time is T = 1/F seconds. So the equation of the sine wave is V = A sin(t/T), where A is the amplitude - we'll get to that later. We also know that we need to generate samples at the sample rate, S, and that each sample occurs every 1/S seconds. I prefer to define a "numerical oscillator" to take care of the audio tone generation. In pseudo-code that would be something like the following, where AudioDT, SampleDT and Phase are static variables. Note that I can call ResetOsc to set the oscillator up, then just call Osc every time I need an oscillator sample.

procedure OscReset(AFreq : integer; ASampleRate : real);
begin
  AudioDT:= 1.0/AFreq;
  SampleDT:= 1.0/ASampleRate;
  Phase:= 0;
end;

function Osc : real;
begin
  Osc:= sin(Phase);
  Phase:= Phase + TwoPi*SampleDT/AudioDT;
  if (Phase > TwoPi) then Phase:= Phase - TwoPi;
end;

With that little bit of code out of the way, now all I need to do is generate enough samples to form a dit and dah. Spaces are easy, they are just a string of 0's indicating no signal. My approach is to create an integer array that represents a dit and a dah and an element space. If I have those predefined, all I need to do is copy them to the sound buffer any time I need to send an element. One way to do define those arrays would be as follows in pseudo-code. The same holds for the dah and for the space, except the dah is 3 times longer and the space is all 0's, so no need to call the oscillator.

begin
  dt:= 0;
  ResetOsc(Tone, SampleRate);
  i:= 0;
  while (dt < DitTime) do
    begin
    ditArray[i]:= A*Osc;
    dt:= dt + SampleDT;
    Inc(i);
  end;
end;

The only problem with that simple bit of code becomes apparent when you implement it and listen to it. What we have done is perfectly modulate the audio tone with a square wave, so extreme clicking is obvious and the key-clicks extend over a large bandwidth. What we need to do is filter out the key-clicks to make this into a usable CW signal.

There is a large body of literature on how to do DSP filtering, but in this case it's actually easier to generate a signal that needs no filtering instead of trying to filter the key-clicks out. In an old and dusty trigonometry book I found the following relation, which holds for any angles:

sin(X) sin(Y) = [cos(X-Y) - cos(X+Y)]/2

What that says is that if we multiply two sine waves (i.e. sinusoidal signals) together, we end up with a the sum of 2 sine waves occupying a bandwidth of twice the difference between them, because (X+Y)-(X-Y) = 2Y. The only "trick" might be to recognize that the signals are now cosines rather than sines, but since we don't care about the absolute phase it makes no difference. In other words if we take our audio sine wave and multiply it by a sine wave that represents the width of a dit and the space following the dit, we generate the narrowest possible CW signal with absolutely no key clicks. With this modification the pseudo-code becomes:

begin
  dt:= 0;
  ResetOsc(Tone, SampleRate);
  i:= 0;
  while (dt < DitTime) do
    begin
    ditArray[i]:= A*sin(Pi*dt/DitTime)*Osc;
    dt:= dt + SampleDT;
    Inc(i);
  end;
end;

Of course you can do the same thing with the dah pulse, except only the first and last part should be "shaped" using something like the following code:

if (dt < DitTime/2)
  then dahArray[i]:= A*sin(Pi*dt/DitTime)*Osc;
else if (dt > (5*DitTime/6))
  then dahArray[i]:= A*sin(Pi*(5*DitTime/6 - dt)/DitTime)*Osc
else dahArray[i]:= A*Osc;

The only thing I haven't mentioned is what to use for the signal amplitude, A. Actually that's pretty easy. The maximum signal amplitude has to fit into the soundcard DAC's, so we need to use a value less than that specified by the soundcard. For a typical 16-bit soundcard, the maximum integer magnitude it can handle is +/-32767, so anything less than that will work. Larger values will give a louder sound volume while smaller values yield less volume. I normally use 32000 just to make sure I don't get close to the edge of the soundcard specs, but there's no real good reason for that particular value.

So now you probably know more about computer generating CW than you ever wanted to know, right?

However, I think the above discussion has shown that as a binary mode, CW is pretty simple to implement. All the problems come in when we're trying to turn it back into an analog signal!

Previous
Next