Site menu QAM modem reaches 16800bps
e-mail icon
Site menu

QAM modem reaches 16800bps

e-mail icon

As I said in the last post, I was thinking about direct (time-domain) analysis of QAM signal at RX, in order to allow baud rates comparable to carrier frequency. I didn't plan to go on with that... but I could not resist.

The thing was working sooner than expected, and performance went beyond my expectations. It can handle 2400 baud with a 1800Hz carrier, like faster modems used to do.

It can currently handle 7 bits per symbol, which translates to 16800bps. Going beyond this will demand improvements in symbol detection, which is currently the same as V2 (which is suboptimal).

V3 modem is basically the same as V2, except by decodification of QAM into complex samples, which V2 did in a "classical" fashion (demodulation + FIR filtering), and V3 does by direct analysis of QAM signal. So, I will list only the relevant V3 RX part:

# Proof that we don't need exact carrier frequency
CARRIER += random.random() * 20 - 10
# Proof that we don't need exact carrier phase
ra = random.random() * 2 * math.pi

symbols = []
f = CARRIER * 2 * math.pi / SAMPLE_RATE
carrier_90 = SAMPLE_RATE / CARRIER / 4.0
carrier_90 = int(carrier_90)
phase_error = 2 * math.pi * CARRIER / SAMPLE_RATE / 2.0
recovered = []

for t in range(0, len(qam) - carrier_90 - 2):
    # Take derivatives of now and 90 degrees in future
    d = (qam[t+1] - qam[t]) / f
    d90 = (qam[t + carrier_90 + 1] - qam[t + carrier_90]) / f

    st = math.sin(f * t + ra)
    ct = math.cos(f * t + ra)

    a = d90 * ct + d * st
    b = d90 * st - d * ct

    try:
        tphase = -b / a
        phase = math.atan(tphase)
    except ZeroDivisionError:
        phase = math.pi / 2
        if (-b * a) < 0:
            phase = -phase

    if (abs(d) > abs(d90)):
        amplitude = -d / (st * math.cos(phase) + ct * math.sin(phase))
    else:
        amplitude = -d90 /
           (-st * math.sin(phase) + ct * math.cos(phase))

    if amplitude < 0:
        amplitude = -amplitude
        phase += math.pi

    phase += math.pi * 2
    phase %= math.pi * 2

    cpx = crect(amplitude, phase)
    # print t, int(phase * 180 / math.pi) % 360, cpx

    recovered.append(cpx)

This technique is based on the fact that QAM signal is generated by a very simple formula:

s(t) = m . cos(2πft + p)

where "m" and "p" are the polar coordinates of constellation symbol being currently transmitted. The derivative of this signal is also a simple formula:

s'(t) = 2πf.m.sin(2πft + p)

The QAM decoding problem is: given s(t) and/or s'(t) — which are readily available as samples — find the unknown variables "m" and "p".

We need two samples of s'(t), which are near enough to belong to the same symbol, to make an equation system and solve for "m" and "p".

That's exactly what the piece of code listed above does. The samples are taken 90 degrees apart because the equations would simplify very nicely.

I chose the s'(t) equation instead of s(t) because the QAM signal may not be centered at zero; that is, it may have some offset, or DC component. But the difference between each sample and the next is unaffected by offset. "Difference" of a discrete signal is equivalent to "derivative" of a continous signal.

It is important to measure samples exactly 90 degrees apart, which means that the "distance" between samples must be an integer number. This implies that WAV sampling rate must be a multiple of 1800 / 4 = 450Hz.

Using a WAV of 43200Hz works beautifully, while a WAV of 45000Hz, which is better in theory and matches carrier, will not bear more than 4 bits/symbol. 44100Hz won't work perfectly for as few as 3 bits/symbol.

Maybe I have "cheated" by choosing a "perfect" WAV sampling rate, but I suspect that a real-world modem does just that. Anyway, this technique of mine is not intended to be the best in town, it is just an exercise to try to go beyond "classical" QAM. Certainly there are ways to compensate for non-perfect alignment of carrier and sampling rate.

e-mail icon