[math-fun] Bell 202 1200 baud modem emulation
The Bell 202 modem (1976) was the first digital modem in widespread use. https://en.wikipedia.org/wiki/Bell_202_modem It uses "audio frequency shift keying" with the two frequencies being 1200Hz ("1") and 2200Hz ("0"). It is still used to transmit Caller ID on wireline phones and by ham radio operators in the ham radio bands. The choice of the frequencies 1200&2200 was a tiny bit unfortunate, because although a "1" consists of a single complete sinusoidal cycle, a "0" consists of *not quite* 2 complete cycles. Here's the rub: when you transition from a 0->1 or from a 1->0, you want the waveform to be *continuous*. So this means that you're going to have to adjust the *phase* of the sinusoid that you generate when you make the transition; i.e., you can't simply start at zero for every bit, else these discontinuities will generate high frequency noise which will widen the overall bandwidth used by the modem. Assuming that you're going to implement the "0" and the "1" by means of *tables* of *digital samples* of the appropriate sinusoids, how many tables do you need? * * * * * * * * * * * * * * * * ** * ------ I believe that 6 tables will be needed, because 11 full cycles of 2200Hz fit exactly 6 full cycles of 1200Hz. We need to keep a modulo 6 counter ("mod6") which keeps track of the number of "0" bits encountered. With each "0" bit, the mod6 counter is incremented, and we use this counter to index which sinusoid table. How come we don't have to keep track of the number of "1" bits? Extra credit: If the overall sampling rate is 6*1200=7200, then we can overlap all of the "0" tables together and index the samples using table0[mod(mod6+i,6)]; ditto for table1. Of course, 6 samples/sinusoid is pretty crude, but we can choose a sampling rate N*6*1200 for integer N>1 and then index the samples accordingly.
Here's an even better way to think about this using complex numbers and roots of unit. Let w be a primitive 66'th root of unity: w^66=1, e.g., w=exp(2*pi*i/66). Then w^11 is a 6'th root of unity: (w^11)^6=1. And w^6 is an 11'th root of unity: (w^6)^11=1. Now consider a clock frequency of 1200*N Hz. At any point in time, the modem is transmitting nothing, it is transmitting a "1" bit, or it is transmitting a "0" bit. At any point in time, the current state of the transmitter is a complex number S, s.t. |S|=1. If the transmitter is transmitting either a "0" bit or a "1" bit, the state S of the transmitter is updated after every clock by multiplying S by another complex number. During the transmission of a full "1" bit, the state S is updated like so: S<-S*(w^6)^N=S*w^(6*N). During the transmission of a full "0" bit, the state S is updated like so: S<-S*(w^11)^N=S*w^(11*N). We can see that N=11 is a convenient choice, so that the clock frequency is 1200*11=13.2kHz. (Of course, we might also have N be some multiple of 11 and thereby gain some improvement in the quality of the generated sinusoids; details are left to the reader.) A "1" transmission will last 11 clock cycles, during which time we will output a complete sinusoid. In order to achieve this, after each clock during a "1" transmission, we update S: S<-S*(w^6)=S*exp(12*pi*i/66)=S*exp(2*pi*i/11). Similarly, a "0" transmission will also last 11 clock cycles, during which time we will output 1 5/6'ths of a sinusoid. In order to achieve this, after each clock during a "0" transmission, we update S: S<-S*(w^11)=S*exp(22*pi*i/66)=S*exp(2*pi*i/6). We can verify that after 11 clock cycles of a "1" transmission, S<-S*(w^6)^11=S*(w^66)=S, which is correct, since a single "1" bit always transmits one complete sinusoid cycle. But after 11 clock cycles of a "0" transmission, S<-S*(w^11)^11=S*w^121=S*w^55, which is correct, since a "0" transmission consists of 1+5/6's of a complete sinusoid cycle. We note that even if we initialize S=1, before starting to transmit a packet of bits, an unfortunate number of "0" bits will *not* return S to 1, so when we stop transmitting, we get a small glitch in the output. With floating point being almost ubiquitous in small CPU chips, these calculations may even be faster even than using a RAM table lookup. One thing to keep in mind: S should be *normalized* from time to time -- S<-S/|S| -- in order to preserve a constant waveform envelope. At 05:35 PM 1/15/2018, Henry Baker wrote:
The Bell 202 modem (1976) was the first digital modem in widespread use.
https://en.wikipedia.org/wiki/Bell_202_modem
It uses "audio frequency shift keying" with the two frequencies being 1200Hz ("1") and 2200Hz ("0"). It is still used to transmit Caller ID on wireline phones and by ham radio operators in the ham radio bands.
The choice of the frequencies 1200&2200 was a tiny bit unfortunate, because although a "1" consists of a single complete sinusoidal cycle, a "0" consists of *not quite* 2 complete cycles.
Here's the rub: when you transition from a 0->1 or from a 1->0, you want the waveform to be *continuous*. So this means that you're going to have to adjust the *phase* of the sinusoid that you generate when you make the transition; i.e., you can't simply start at zero for every bit, else these discontinuities will generate high frequency noise which will widen the overall bandwidth used by the modem.
Assuming that you're going to implement the "0" and the "1" by means of *tables* of *digital samples* of the appropriate sinusoids, how many tables do you need?
Ok, now we have to *receive* these 2-tone signals. Below is my high school "crystal radio" sophistication level version of a software Bell 202 1200 baud receiver. As in our transmitter, we have digital samples s(t) of (real) numbers coming in at a rate of 1200*6=13.2kHz. We'd like to decide if the last 6 samples denote a 1200Hz frequency or a 2200Hz frequency. We can't guarantee strict clock synchronization, so we're going to have to deal with that, too. Recall that w is a primitive 66'th root of 1: w^66=1. So (w^11)^6=1 and (w^6)^11=1. We'd like to take the input stream s(t) and *convolve* it with 6 samples of the sinusoidal stream (w^11)^i. I.e., we'd like to keep a running (complex) sum of the last 6 samples: S(i) = s(i)+s(i-1)*w^11+s(i-2)*(w^11)^2 +s(i-3)*(w^11)^3+s(i-4)*(w^11)^4 +s(i-5)*(w^11)^5 So when we get s(i+1), we need to subtract s(i-5)*(w^11)^5 from S(i) and then multiply it by w^11: S(i+1) = s(i+1) + [S(i)-s(i-5)*(w^11)^5]*(w^11) We also compute |S(i+1)|^2 = S(i+1)*S(i+1)^* We do a similar calculation for w^6, which will help us detect 1's: T(i+1) = s(i) + [T(i)-s(i-5)*(w^6)^5]*(w^6) and compute |T(i+1)|^2 = T(i+1)*T(i+1)^* The reason for performing *convolutions* is that a suitably long sample of a 1200Hz sinusoid are *orthogonal* to a similarly long sample of a 2200Hz sinusoid *no matter what the phase* of each. And by doing the convolution using *complex numbers* instead of just *real numbers*, we don't have to know the phase in advance, but can perform the convolution and then are free to later *throw away* that phase information by computing the absolute value. Hopefully, with only 6 iterations of each calculation on a new sample, we should know whether the state S has a stronger claim on 0 or T has a stronger claim on 1. Note that if we utilize a floating point representation for the (real,imaginary) components of w, S and T, we will likely get *drift*, since the cancellation of the obsolete samples will not be exact. One obvious way to deal with this is to simply *reset* S=T=0 at the end of each bit time, which allows the receiver to *forget* which bit preceded the current bit being received. When floating point representation isn't convenient or precise enough, we can dispense with almost all error by keeping S,T as high-precision numbers in *Chinese Remainder Theorem* (CRT) representation. With modern cheap memory, we can use table lookup to interconvert between CRT numbers and other representations. At 10:38 AM 1/16/2018, Henry Baker wrote:
Here's an even better way to think about this using complex numbers and roots of unit.
Let w be a primitive 66'th root of unity: w^66=1, e.g., w=exp(2*pi*i/66).
Then w^11 is a 6'th root of unity: (w^11)^6=1.
And w^6 is an 11'th root of unity: (w^6)^11=1.
Now consider a clock frequency of 1200*N Hz.
At any point in time, the modem is transmitting nothing, it is transmitting a "1" bit, or it is transmitting a "0" bit.
At any point in time, the current state of the transmitter is a complex number S, s.t. |S|=1. If the transmitter is transmitting either a "0" bit or a "1" bit, the state S of the transmitter is updated after every clock by multiplying S by another complex number.
During the transmission of a full "1" bit, the state S is updated like so: S<-S*(w^6)^N=S*w^(6*N).
During the transmission of a full "0" bit, the state S is updated like so: S<-S*(w^11)^N=S*w^(11*N).
We can see that N=11 is a convenient choice, so that the clock frequency is 1200*11=13.2kHz. (Of course, we might also have N be some multiple of 11 and thereby gain some improvement in the quality of the generated sinusoids; details are left to the reader.)
A "1" transmission will last 11 clock cycles, during which time we will output a complete sinusoid. In order to achieve this, after each clock during a "1" transmission, we update S: S<-S*(w^6)=S*exp(12*pi*i/66)=S*exp(2*pi*i/11).
Similarly, a "0" transmission will also last 11 clock cycles, during which time we will output 1 5/6'ths of a sinusoid. In order to achieve this, after each clock during a "0" transmission, we update S: S<-S*(w^11)=S*exp(22*pi*i/66)=S*exp(2*pi*i/6).
We can verify that after 11 clock cycles of a "1" transmission, S<-S*(w^6)^11=S*(w^66)=S, which is correct, since a single "1" bit always transmits one complete sinusoid cycle.
But after 11 clock cycles of a "0" transmission, S<-S*(w^11)^11=S*w^121=S*w^55, which is correct, since a "0" transmission consists of 1+5/6's of a complete sinusoid cycle.
We note that even if we initialize S=1, before starting to transmit a packet of bits, an unfortunate number of "0" bits will *not* return S to 1, so when we stop transmitting, we get a small glitch in the output.
With floating point being almost ubiquitous in small CPU chips, these calculations may even be faster even than using a RAM table lookup. One thing to keep in mind: S should be *normalized* from time to time -- S<-S/|S| -- in order to preserve a constant waveform envelope.
At 05:35 PM 1/15/2018, Henry Baker wrote:
The Bell 202 modem (1976) was the first digital modem in widespread use.
https://en.wikipedia.org/wiki/Bell_202_modem
It uses "audio frequency shift keying" with the two frequencies being 1200Hz ("1") and 2200Hz ("0"). It is still used to transmit Caller ID on wireline phones and by ham radio operators in the ham radio bands.
The choice of the frequencies 1200&2200 was a tiny bit unfortunate, because although a "1" consists of a single complete sinusoidal cycle, a "0" consists of *not quite* 2 complete cycles.
Here's the rub: when you transition from a 0->1 or from a 1->0, you want the waveform to be *continuous*. So this means that you're going to have to adjust the *phase* of the sinusoid that you generate when you make the transition; i.e., you can't simply start at zero for every bit, else these discontinuities will generate high frequency noise which will widen the overall bandwidth used by the modem.
Assuming that you're going to implement the "0" and the "1" by means of *tables* of *digital samples* of the appropriate sinusoids, how many tables do you need?
participants (1)
-
Henry Baker