Site menu Modulação FM

Modulação FM

FM (Frequency Modulation ou Modulação em Freqüência) é a tecnologia mais empregada hoje em rádio analógico, tanto nas FMs comerciais quanto em comunicação de walkie-talkie e radioamador. (Uma grande exceção é a comunicação dos aviões, que ainda é AM.)

Até mesmo as modalidades de rádio digital como DMR e D-Star, cada vez mais utilizadas, possuem um estágio final de FM, o que permite o compartilhamento de circuitos dentro do rádio, e a convivência pacífica dos modos analógico e digital na mesma banda.

Neste artigo, tentarei demonstrar como um sinal é transformado durante esta modulação, e exibirei uma simples implementação em Python.

Temos este áudio com minha voz — o mesmo que usei no artigo sobre modulação AM:


(link to audio)

Agora farei a modulação FM, fazendo uso de uma portadora de 10.000Hz. A largura de banda do sinal original foi limitada a 1000Hz, e o desvio máximo de freqüência também foi fixado em 1000Hz. Por favor, não ouça o resultado em volume alto, sob pena de arrancar sua cabeça fora!


(link to audio)

Parece impossível recuperar voz disso aí, mas de fato ela pode ser recuperada. Há alguma perda de qualidade devido à filtragem na "recepção":


(link to audio)

Agora, vamos ver como a salsicha é feita. Primeiro, um pouco de teoria. A modulação FM consiste em desviar a freqüência da portadora, conforme o sinal de entrada.

A saída modulada tem potência constante, mesmo quando o sinal é puro silêncio. Isto torna o FM muito resistente a ruídos e interferência. Também permite que o transmissor seja simples e eficiente, pois ele não precisa modular a potência.

Em AM e principalmente em SSB, a potência de saída guarda proporção com o sinal de entrada, o que deixa o sinal vulnerável nos instantes de baixa potência. Como transmissores AM e SSB precisam variar a potência de saída de forma linear, são mais complexos e menos eficientes.

A priori, AM e SSB usam a banda de forma mais eficiente que FM, pois modulam tanto freqüência quanto amplitude, enquanto FM modula apenas freqüência.

Um transmissor FM analógico pode ser incrivelmente simples. Usando um varicap (diodo que funciona como capacitor variável controlado por tensão) no oscilador, o sinal de áudio pode influenciar diretamente o varicap, e portanto a freqüência de rádio.

Modular digitalmente é mais enrolado. A ideia é ir adiantando ou atrasando a fase da portadora conforme o sinal de entrada. Um atraso continuado corresponde a um desvio da freqüência para baixo, e vice-versa.

O maior cuidado que temos de ter, é relacionar o sinal com o desvio de fase da portadora, de modo que o resultado final não caia fora da banda (no exemplo, de 9000 a 11000Hz). O outro detalhe é que o sinal de entrada é integrado. O desvio de fase não volta a zero quando a entrada é silêncio; apenas deixamos o desvio corrente intocado.

Em vez de mexer na fase da modulação diretamente, traduzimos a fase para coordenadas cartesianas (I/Q) e usamos um modulador de quadratura:

#!/usr/bin/env python3

# FM modulator based on I/Q (quadrature) modulation

import wave, struct, math

input_src = wave.open("paralelepipedo_lopass.wav", "r")
FM_CARRIER = 10000.0
MAX_DEVIATION = 1000.0 # Hz

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

phase = 0 # in radians
for n in range(0, input_src.getnframes()):
        # rush or drag phase accordingly to input signal
        # this is analog to integrating
        inputsgn = struct.unpack('h',
		input_src.readframes(1))[0] / 32768.0

        # translate input into a phase change that changes frequency
        # up to MAX_DEVIATION Hz 
        phase += inputsgn * math.pi * MAX_DEVIATION / 44100
        phase %= 2 * math.pi

        # calculate quadrature I/Q
        i = math.cos(phase)
        q = math.sin(phase)

        carrier = 2 * math.pi * FM_CARRIER * (n / 44100.0)
        output = i * math.cos(carrier) - q * math.sin(carrier)

        fm.writeframes(struct.pack('h', int(output * 32767)))

No lado receptor, FM é bem mais complicado que AM. Não existe "rádio de galena" FM. Mesmo os kits de revista de eletrônica fazem uso de algum circuito integrado ASIC, como o TDA7000. (Recepção de FM comercial tem o complicador adicional do sinal stereo, que envolve uma segunda demodulação AM-SC, que o TDA7000 também faz.)

Existem inúmeros métodos analógicos e digitais para demodular FM. No código abaixo, seguimos a receita padrão, que é essencialmente o caminho inverso do modulador. O demodulador de quadratura extrai os valores I e Q, transformamos para coordenada polar para obter o desvio de fase, e vemos quanto a fase "girou" em relação à amostra anterior para extrair o sinal.

Um detalhe importante é que o sinal I/Q demodulado tem de passar por um filtro passa-baixas. Conforme dissemos no artigo sobre AM, a demodulação cria um sinal-fantasma de alta freqüência. Em AM é apenas um aborrecimento, em FM ele realmente impede o "receptor" de funcionar.

#!/usr/bin/env python3

# FM demodulator based on I/Q (quadrature)

import wave, struct, math, random, filters

input_src = wave.open("iq_fm.wav", "r")
FM_CARRIER = 10000.0
MAX_DEVIATION = 1000.0 # Hz

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

# Prove we don't need synchronized carrier oscilators
initial_carrier_phase = random.random() * 2 * math.pi

last_angle = 0.0
istream = []
qstream = []

for n in range(0, input_src.getnframes()):
        inputsgn = struct.unpack('h',
		input_src.readframes(1))[0] / 32768.0

        # I/Q demodulation, not unlike QAM
        carrier = 2 * math.pi * FM_CARRIER * (n / 44100.0)
		+ initial_carrier_phase
        istream.append(inputsgn * math.cos(carrier))
        qstream.append(inputsgn * -math.sin(carrier))

istream = filters.lowpass(istream, 1500) 
qstream = filters.lowpass(qstream, 1500) 

last_output = 0

for n in range(0, len(istream)):
        i = istream[n]
        q = qstream[n]

        # Determine phase (angle) of I/Q pair
        angle = math.atan2(q, i)

        # Change of angle = baseband signal
        # Were you rushing or were you dragging?!
        angle_change = last_angle - angle

        # Just for completeness; big angle changes are not
        # really expected, this is FM, not QAM
        if angle_change > math.pi:
                angle_change -= 2 * math.pi
        elif angle_change < -math.pi:
                angle_change += 2 * math.pi
        last_angle = angle

        # Convert angle change to baseband signal strength
        output = angle_change / (math.pi * MAX_DEVIATION / 44100)
        if abs(output) >= 1:
                # some unexpectedly big angle change happened
                output = last_output
        last_output = output
        
        demod.writeframes(struct.pack('h', int(output * 32767)))

O filtro é bastante "cru" e foi tomado emprestado do artigo sobre filtros FIR, mas funcionou bem o suficiente para nossos fins:

#!/usr/bin/env python

import numpy, math
from numpy import fft

SAMPLE_RATE = 44100 # Hz
NYQUIST_RATE = SAMPLE_RATE / 2.0
FFT_LENGTH = 512

def lowpass_coefs(cutoff):
        cutoff /= (NYQUIST_RATE / (FFT_LENGTH / 2.0))

        # create FFT filter mask
        mask = []
        negatives = []
        l = FFT_LENGTH // 2
        for f in range(0, l+1):
                rampdown = 1.0
                if f > cutoff:
                        rampdown = 0
                mask.append(rampdown)
                if f > 0 and f < l:
                        negatives.append(rampdown)

        negatives.reverse()
        mask = mask + negatives

        # Convert FFT filter mask to FIR coefficients
        impulse_response = fft.ifft(mask).real.tolist()

        # swap left and right sides
        left = impulse_response[:FFT_LENGTH // 2]
        right = impulse_response[FFT_LENGTH // 2:]
        impulse_response = right + left

        b = FFT_LENGTH // 2
        # apply triangular window function
        for n in range(0, b):
                    impulse_response[n] *= (n + 0.0) / b
        for n in range(b + 1, FFT_LENGTH):
                    impulse_response[n] *= (FFT_LENGTH - n + 0.0) / b

        return impulse_response

def lowpass(original, cutoff):
        coefs = lowpass_coefs(cutoff)
        return numpy.convolve(original, coefs)

if __name__ == "__main__":
        import wave, struct
        original = wave.open("NubiaCantaDalva.wav", "r")
        filtered = wave.open("test_fir.wav", "w")
        filtered.setnchannels(1)
        filtered.setsampwidth(2)
        filtered.setframerate(SAMPLE_RATE)

        n = original.getnframes()
        original = struct.unpack('%dh' % n, original.readframes(n))
        original = [s / 2.0**15 for s in original]

        result = lowpass(original, 1000)
        
        result = [ int(sample * 2.0**15) for sample in result ]
        filtered.writeframes(struct.pack('%dh' % len(result),
		*result))

A técnica de "girar a fase" para executar a modulação FM sugere que FM é basicamente uma versão analógica da modulação por fase (PM), bem conhecida no mundo digital. De fato, modos digitais baseados em fase ou freqüência (PM, FSK, MSK, GMSK) podem ser realizados via (de)modulação FM de um sinal digital (onda quadrada).

Naturalmente, rádios "de verdade" usem técnicas mais eficientes, mas funciona bem o suficiente para ouvir walkie-talkies digitais usando SDR combinado com o programa DSD.