Site menu Modem QAM: módulo TX

Modem QAM: módulo TX

Se você for um macaco velho como eu, seja informata ou não, provavelmente é invadido pela nostalgia ao ouvir este ruído:


(link to audio)

Este artigo trata da modulação digital QAM (Quadrature Amplitude Modulation - Modulação por Amplitude em Quadratura). As modulações PSK e FSK são casos-limite de QAM, portanto a discussão sobre QAM aproveita de certa forma às modulações digitais mais simples.

Escrever o transmissor (TX) de um modem é relativamente fácil. O grande desafio é escrever o receptor (RX). O receptor é objeto de uma segunda parte deste artigo, e você verá que o código RX é consideravelmente mais longo e complicado.

Nestes artigos, não tentei escrever um modem perfeito. Provavelmente ele não funcionaria se utilizado sobre um canal do mundo real, como um VHF radioamador. Vou mencionar as limitações conhecidas, e deve haver toda uma legião de problemas que me são desconhecidos. O objetivo é se divertir.

Minha referência é um excelente, embora antigo, livro: "Modem e Transmissão de Dados" de Fabio Montoro. Está fora de catálogo, mas se você encontrá-lo num sebo, é leitura muito interessante (e nostálgica).

A modulação QAM parte de uma portadora de freqüência fixa, modulando sua fase e amplitude de acordo com os bits da mensagem a enviar. A forma de onda do sinal de saída é algo nesse estilo:

Figura 1: Onda QAM

Note que QAM, assim como AM, nunca altera a freqüência da portadora. Não seria possível alterar a freqüência ao mesmo tempo que fase e amplitude, porque uma alteração "borraria" a outra. (Assim como QAM digital e AM analógica são aparentadas, a modulação FM analógica tem como "prima" digital a modulação FSK.)

Cada mudança no sinal QAM pode representar vários bits. Por exemplo, se estabelecermos que há 4 fases possíveis (0, 90, 180 e 270 graus) e 2 amplitudes possíveis (50% e 100%), temos 8 combinações possíveis, que podem ser associadas a 3 bits por símbolo. Um "símbolo" é uma mudança na portadora QAM.

Então nós temos esse conceito de "baud rate", que é o número de símbolos por segundo. A taxa de bits, ou bitrate, é o baud rate multiplicado pelo número de bits carregado por cada símbolo.

Precisamos agora estabelecer uma relação organizada entre fases, amplitudes e bits transmitidos. A forma mais comum de fazer isso, pelo menos ilustrativamente, é usando um gráfico de "constelação" como este:

Figura 2: Constelação QAM

Na figura acima, temos 3 constelações. À esquerda, uma constelação extremamente simples, com apenas 4 pontos. Se você desenhar uma linha a partir da origem (coordenadas 0,0) até um certo ponto, o comprimento dessa linha é a amplitude do sinal, e o ângulo em relação à origem é a mudança de fase. Basta agora atribuir um padrão de bits diferente para cada ponto da constelação.

Como todos os quatro pontos da constelação esquerda são equidistantes da origem, podemos afirmar que essa constelação não é realmente QAM, mas sim PSK (codificação por mudança de fase), porque apenas as fases são diferentes em cada símbolo. As demais constelações são mais populosas, e possuem pontos de diversas amplitudes.

Sem mais demora, vamos apresentar o código do transmissor (TX) QAM:

#!/usr/bin/env python

import wave, struct, math

SAMPLE_RATE = 44100.0 # Hz
CARRIER = 1800.0 # Hz
BAUD_RATE = 360.0 # Hz
PHASES = 8
AMPLITUDES = 4
BITS_PER_SYMBOL = 5

# generate constellation of phases and amplitudes
# for each possible symbol, and attribute it to a
# certain bit pattern. I avoided deliberately the
# complex numbers, even though they make things
# easier when dealing with constellations.
#
# Make sure that all ones = maximum aplitude and 0 phase
constellation = {}
for p in range(0, PHASES):
    for a in range(0, AMPLITUDES):
        s = p * AMPLITUDES + a
        constellation[s] = (PHASES - p - 1, a + 1)

# This is the message we want to send
msg = "Abracadabra!"
# msg = open("modem_tx.py").read()

# Convert message to a list of bits, adding start/stop bits
msgbits = []
for byte in msg:
    byte = ord(byte)
    msgbits.append(0) # start bit
    for bit in range(7, -1, -1): # MSB
        msgbits.append((byte >> bit) % 2)
    msgbits.append(1) # stop bit

# Round bit stream to BITS_PER_SYMBOL
while len(msgbits) % BITS_PER_SYMBOL:
    msgbits.append(0)

# Group bit stream into symbols
msgsymbols = []
for n in range(0, len(msgbits), BITS_PER_SYMBOL):
    symbol = 0
    for bit in range(0, BITS_PER_SYMBOL):
        symbol += msgbits[n + bit] << (BITS_PER_SYMBOL - bit - 1)
    msgsymbols.append(symbol)

# Add 1 second worth of all-1 bits in header, plus a trailer.
# The 11111 bit pattern generates a pure carrier, that receiver
# can recognize and train.
allones = (1 << BITS_PER_SYMBOL) - 1
train_sequence = [ allones for x in range(0, int(BAUD_RATE)) ]
finish_sequence = [ allones, allones ]
msgsymbols = train_sequence + msgsymbols + finish_sequence

qam = wave.open("qam.wav", "w")
qam.setnchannels(1)
qam.setsampwidth(2)
qam.setframerate(44100)

t = -1
phase_int = 0
samples = []
for symbol in msgsymbols:
    amplitude = 0.9 * constellation[symbol][1] / AMPLITUDES
    # Phase change is integrated so we don't need absolute phase
    # at receiver side.
    phase_int += constellation[symbol][0]
    phase_int %= PHASES
    phase = phase_int / (0.0 + PHASES)

    # It would be easier to manipulate phase using complex numbers,
    # but I thought this way would be clearer.

    # Play the carrier at chosen phase/amplitude for 1/BAUD_RATE secs
    for baud_length in range(0, int(SAMPLE_RATE / BAUD_RATE)):
        t += 1
        sample = math.cos(CARRIER * t * math.pi * 2 / SAMPLE_RATE \
                          + 2 * math.pi * phase)
        sample *= amplitude
        samples.append(int(sample * 32767))

qam.writeframes(struct.pack('%dh' % len(samples), *samples))

print "%d symbols sent" % len(msgsymbols)

Na verdade, a única parte realmente complicada no TX é expansão da mensagem original, em bytes, para uma seqüência de bits individuais, incluindo os start e stop bits. De posse dos bits, é facil gerar o sinal, cortesia da função trigonométrica cos() que é o coração do modulador.

Você lembra que os modems costumavam "assobiar" por alguns segundos antes da conexão? Foi necessário fazer o mesmo aqui, para "treinar" o receptor com o sinal. Apesar de estarmos lidando com um canal de alta qualidade (arquivo de som WAV), a fase de treinamento ainda assim é necessária.

Utilizei uma portadora de 1800Hz por pura nostalgia, pois esse tom faz o sinal QAM parecer com os modems analógicos que utilizávamos na década de 90. Os outros parâmetros foram encontrados empiricamente; estão um pouco abaixo do ponto em que o código RX falharia em recuperar a mensagem.

A constelação gerada pelo código lembraria uma roda de bicicleta, com os "raios" saindo do centro até o aro exterior. Ela não é ótima, porque tem mais pontos perto da origem, o que significa que símbolos de menor amplitude seriam mais facilmente confundidos entre si na presença de ruído. As constelações mostradas na figura mais acima têm uma distribuição melhor.

Há outra razão pela qual minha constelação é sub-ótima. Eu optei por não utilizar números complexos, para manter o código mais legível aos leitores não tão safos em matemática. Infelizmente, gerar uma constelação ótima sem usar números complexos tornaria o código ilegível. Assim preferi manter tudo legível e simples. (Outros artigos mostram esse modem QAM reimplementado com números complexos.)

Outro problema potencial da minha constelação é que há pontos de "0 graus", ou seja, símbolos onde não ocorre mudança de fase. Isso complica a vida do receptor RX, porque, se um símbolo desses é enviado repetidamente, o sinal QAM não tem uma "quebra de fase" entre símbolos, o que dificulta a determinação da duração do símbolo. O receptor precisa então adivinhar essa duração baseado no seu clock local.

Falando de fase, a mudança de fase é integrada no transmissor. Por exemplo, se vamos transmitir diversos símbolos de 45 em seqüência, os ângulos são acumulados, e os símbolos de saída serão 45, 90, 135, 180... defasados em relação à portadora. No lado do receptor, a mudança de fase é que é considerada, de modo que a seqüência de símbolos acima é corretamente interpretada como 45, 45, 45, 45...

Se a fase não fosse integrada no TX, o RX teria de conhecer a fase absoluta da portadora o tempo todo, e perderia o sincronismo para sempre assim que o primeiro ruído corrompesse o sinal.

Já a amplitude, por sua natureza, não precisa ser integrada para ser interpretada corretamente pelo RX. Uma simples média móvel do RX é suficiente para determinar a faixa dinâmica da amplitude.

A seqüência de bits também pode ser condicionada por um "randomizador". Ele evita que apareçam longas seqüências monótonas de bits, de modo que todos os símbolos da constelação sejam igualmente prováveis, e longas seqüências do mesmo símbolo não aconteçam facilmente. Isto torna a vida do RX bem mais fácil. A implementação do randomizador é objeto de outro artigo.