Rádio definido por software - como funciona? Parte 3

Oi Habr.

A segunda parte examinou os aspectos práticos do uso do SDR. Nesta parte, veremos como receber dados de satélite meteorológico NOAA usando Python e o receptor RTL-SDR barato (US $ 30). O código considerado funcionará em qualquer lugar - no Windows, OSX, Linux e até no Raspberry Pi.



Quem se importa, continuou sob o corte.

SoapySDR


Existem muitos fabricantes de vários dispositivos SDR, e seria muito inconveniente oferecer suporte a cada um separadamente, e é caro em termos de compra de hardware para teste. Em princípio, para acesso unificado, existem duas bibliotecas que se tornaram essencialmente o padrão. A primeira é a interface ExtIO DLL já bastante antiga, que provavelmente tem pelo menos 10 anos de idade, a segunda é a biblioteca SoapySDR mais moderna, que veremos.

O SoapySDR é um conjunto de bibliotecas de plataforma cruzada escritas em C ++ que fornecem acesso unificado a dispositivos SDR, receptores e transceptores. Se o fabricante fizer essa interface, seu dispositivo funcionará automaticamente com um número bastante grande de programas populares (GQRX, GNU Radio, CubicSDR, etc.). Quase todos os fabricantes adequados, com exceção de alguns, (aproveitando a oportunidade, envio meus cumprimentos à EE) têm suporte ao SoapySDR, uma lista de dispositivos suportados pode ser encontrada na página do projeto . Como você pode ver, é bastante grande e inclui HackRF, USRP, SDRPlay, LimeSDR, RTL-SDR, Red Pitaya e muitos outros.

A biblioteca SoapySDR é multiplataforma, ou seja, o código escrito para ele funcionará no Windows, OSX, Linux e até no Raspberry Pi. Para Windows, as bibliotecas necessárias fazem parte do pacote PothosSDR ; para outras plataformas, o SoapySDR terá que ser compilado de forma independente. É necessário compilar duas partes - a própria biblioteca e o "plug-in" para o receptor desejado, no nosso caso será SoapyRTLSDR (no Windows, a biblioteca também pode ser montada a partir da fonte, para isso você precisa do Visual Studio, Cmake e SWIG). Agora está tudo pronto e você pode escrever código.

Importamos a biblioteca e obtemos uma lista de receptores:

from __future__ import print_function import SoapySDR # Enumerate devices print("SDR devices:") for d in SoapySDR.Device.enumerate(''): print(d) print() 

Conectamos o receptor, executamos o código e vemos uma lista de dispositivos, entre os quais o nosso rtlsdr.



O restante dos dispositivos são placas de som, como lembramos, historicamente os primeiros SDRs funcionavam precisamente através da entrada linear do PC, e a biblioteca também os suporta. Nós obtemos informações sobre o dispositivo - o número de canais disponíveis, faixa de frequência, etc .:

 soapy_device = "rtlsdr" device = SoapySDR.Device(dict(driver = soapy_device)) channels = list(range(device.getNumChannels(SoapySDR.SOAPY_SDR_RX))) print("Channels:", channels) ch = channels[0] sample_rates = device.listSampleRates(SoapySDR.SOAPY_SDR_RX, ch) print("Sample rates:\n", sample_rates) bandwidths = list(map(lambda r: int(r.maximum()), device.getBandwidthRange(SoapySDR.SOAPY_SDR_RX, ch))) print("Bandwidths:\n", bandwidths) print("Gain controls:") for gain in device.listGains(SoapySDR.SOAPY_SDR_RX, ch): print(" %s: %s" % (gain, device.getGainRange(SoapySDR.SOAPY_SDR_RX, ch, gain))) frequencies = device.listFrequencies(SoapySDR.SOAPY_SDR_RX, ch) print("Frequencies names:", frequencies) frequency_name = frequencies[0] print("Frequency channel name:", frequency_name) print("Frequency range:", device.getFrequencyRange(SoapySDR.SOAPY_SDR_RX, ch, frequency_name)[0]) 

Iniciamos o programa e vemos informações sobre o receptor:



Vimos que o receptor possui um canal de entrada com o nome "RF", as possíveis frequências de amostragem [250000.0, 1024000.0, 1536000.0, 1792000.0, 1920000.0, 2048000.0, 2160000.0, 2560000.0, 2880000.0, 3200000.0] e a faixa de frequências de 24 MHz a 1.7 GHz.

Life hack - os mesmos dados também podem ser obtidos na linha de comando, digitando o comando SoapySDRUtil --probe = "driver = rtlsdr" .

Sabendo disso, podemos gravar o fluxo de dados em WAV. Como mencionado na parte anterior, os dados do SDR são representados por um fluxo de sinais chamado I e Q, que são amostras do ADC, aproximadamente eles podem ser representados como dados RAW da câmera. Qualquer pessoa interessada em mais detalhes pode ler aqui, por exemplo. Basta que saibamos que podemos gravar esses dados e outros programas de SDR podem trabalhar com eles.

O registro em si é bastante simples - a função readStream preenche o buffer se houver dados, se não houver dados, então -1 será retornado. Abaixo está um código de registro de 10 amostras (partes não essenciais do código são omitidas).

 device.setFrequency(SoapySDR.SOAPY_SDR_RX, channel, "RF", frequency) device.setGain(SoapySDR.SOAPY_SDR_RX, channel, "TUNER", gain) device.setGainMode(SoapySDR.SOAPY_SDR_RX, channel, False) device.setSampleRate(SoapySDR.SOAPY_SDR_RX, channel, sample_rate) # Number of blocks to save block, max_blocks = 0, 10 block_size = device.getStreamMTU(stream) print("Block size:", block_size) buffer_format = np.int8 buffer_size = 2*block_size # I+Q buffer = np.empty(buffer_size, buffer_format) while True: d_info = device.readStream(stream, [buffer], buffer_size) if d_info.ret > 0: wav.write(buffer[0:2*d_info.ret]) print("Bytes saved:", 2*d_info.ret) block += 1 if block > max_blocks: break 

O resultado na captura de tela:



Como você pode ver, obtemos blocos de dados do dispositivo, o tamanho de um bloco é 131072 bytes, o que a uma taxa de amostragem de 250.000 nos dá uma duração de cerca de meio segundo. Em geral, aqueles que trabalharam anteriormente com placas de som no Windows encontrarão muito em comum.

Para o teste, escreva o arquivo e verifique se está tudo bem - ele pode ser reproduzido em SDR #. Há mais um truque - para que o SDR # mostre corretamente as frequências das estações, o nome do arquivo deve ser escrito em um formato compatível com HDSDR, no formato “HDSDR_20190518_115500Z_101000kHz_RF.wav” (como você deve imaginar, a data e a hora estão no GMT no início e, em seguida, a frequência em kilohertz) . É fácil escrever em Python:

 frequency = 101000000 file_name = "HDSDR_%s_%dkHz_RF.wav" % (datetime.datetime.utcnow().strftime("%Y%m%d_%H%M%SZ"), frequency/1000) 

Primeiro, verifique a banda FM. Está tudo bem, a estação está visível, a música está tocando, o RDS está funcionando.



Você pode começar a gravar NOAA.

Ingestão de NOAA


Então, nós temos um receptor e um programa de gravação. Estaremos interessados ​​nos satélites meteorológicos NOAA 15, NOAA 18 e NOAA 19 que transmitem imagens da superfície da Terra nas frequências de 137.620, 137.9125 e 137.100 MHz. A principal dificuldade aqui é que você precisa "capturar" o momento em que o satélite voa sobre nós. Você pode descobrir o tempo de voo on-line em https://www.n2yo.com/passes/?s=25338 , https://www.n2yo.com/passes/?s=28654 e https://www.n2yo.com / passa /? s = 33591 respectivamente.



Para não ficar sentado no computador, adicione ao programa a espera da hora certa. Isso também permitirá que você execute o programa no Raspberry Pi, sem tela e teclado.

 import datetime def wait_for_start(dt): # Wait for the start while True: now = datetime.datetime.now() diff = int((dt - now).total_seconds()) print("{:02d}:{:02d}:{:02d}: Recording will be started after {}m {:02d}s...".format(now.hour, now.minute, now.second, int(diff / 60), diff % 60)) time.sleep(5) if diff <= 1: break wait_for_start(datetime.datetime(2019, 5, 18, 21, 49, 0)) 


A propósito, para executar o script no Raspberry Pi e deixá-lo funcionando após o fechamento do console, você precisa digitar o comando "nohup python recorder.py &".

Está tudo pronto, execute o script e pode fazer outras coisas, a gravação dura cerca de 20 minutos. Paralelamente, a questão pode surgir - é possível ver a passagem do satélite a olho nu? De acordo com a tabela, seu brilho máximo é de cerca de 5,5m de magnitude , o limite do olho humano em condições ideais é de 6m. I.e. Em um céu realmente escuro, muito além da cidade, a passagem do satélite NOAA pode teoricamente ser percebida; na cidade, é claro, não há chance (como escreveram em Habré , uma geração de pessoas que nunca viu a Via Láctea em sua vida já cresceu).

O resultado do script é um arquivo wav gravado, seu espectro é mostrado na captura de tela.



Vemos um sinal completamente distinto, embora, é claro, com uma antena especial para receber NOAA, a qualidade seja muito melhor. O formato do sinal é chamado APT ( Automatic Picture Transmission ), a partir dele você pode obter uma imagem da superfície da Terra; se alguém estiver interessado, você pode considerar separadamente sua decodificação. Mas, é claro, existem programas prontos, você pode decodificar esses sinais usando WxToImg ou MultiPSK.

É interessante ver a mudança Doppler no espectro, que ocorre porque o satélite passa por nós. Provavelmente, não é difícil calcular sua velocidade, sabendo o tempo e a mudança de frequência em hertz.

Obviamente, o programa pode ser usado não apenas para gravar NOAA, como também pode ser especificada qualquer largura de banda e frequência nas configurações. Para aqueles que desejam experimentar o SoapySDR, o código do programa está inteiramente localizado abaixo do spoiler.

Código fonte
 from __future__ import print_function import SoapySDR import numpy as np import struct import sys import time import datetime def wait_for_start(dt): # Wait for the start while True: now = datetime.datetime.now() diff = int((dt - now).total_seconds()) print("{:02d}:{:02d}:{:02d}: Recording will be started after {}m {:02d}s...".format(now.hour, now.minute, now.second, int(diff / 60), diff % 60)) time.sleep(5) if diff <= 1: break def sdr_enumerate(): # Enumerate SDR devices print("SDR devices:") for d in SoapySDR.Device.enumerate(''): print(d) print() def sdr_init(): soapy_device = "rtlsdr" device = SoapySDR.Device({"driver": soapy_device}) channels = list(range(device.getNumChannels(SoapySDR.SOAPY_SDR_RX))) print("Channels:", channels) ch = channels[0] sample_rates = device.listSampleRates(SoapySDR.SOAPY_SDR_RX, ch) print("Sample rates:\n", sample_rates) print("Gain controls:") for gain in device.listGains(SoapySDR.SOAPY_SDR_RX, ch): print(" %s: %s" % (gain, device.getGainRange(SoapySDR.SOAPY_SDR_RX, ch, gain))) frequencies = device.listFrequencies(SoapySDR.SOAPY_SDR_RX, ch) print("Frequencies names:", frequencies) frequency_name = frequencies[0] print("Frequency channel name:", frequency_name) print("Frequency range:", device.getFrequencyRange(SoapySDR.SOAPY_SDR_RX, ch, frequency_name)[0]) print() return device def sdr_record(device, frequency, sample_rate, gain, blocks_count): print("Frequency:", frequency) print("Sample rate:", sample_rate) print("Gain:", gain) channel = 0 # Always for RTL-SDR device.setFrequency(SoapySDR.SOAPY_SDR_RX, channel, "RF", frequency) device.setGain(SoapySDR.SOAPY_SDR_RX, channel, "TUNER", gain) device.setGainMode(SoapySDR.SOAPY_SDR_RX, channel, False) device.setSampleRate(SoapySDR.SOAPY_SDR_RX, channel, sample_rate) data_format = SoapySDR.SOAPY_SDR_CS8 # if 'rtlsdr' in soapy_device or 'hackrf' in soapy_device else SoapySDR.SOAPY_SDR_CS16 stream = device.setupStream(SoapySDR.SOAPY_SDR_RX, data_format, [channel], {}) device.activateStream(stream) block_size = device.getStreamMTU(stream) print("Block size:", block_size) print("Data format:", data_format) print() # IQ: 2 digits ver variable buffer_format = np.int8 buffer_size = 2 * block_size # I + Q samples buffer = np.empty(buffer_size, buffer_format) # Number of blocks to save block, max_blocks = 0, blocks_count # Save to file file_name = "HDSDR_%s_%dkHz_RF.wav" % (datetime.datetime.utcnow().strftime("%Y%m%d_%H%M%SZ"), frequency/1000) print("Saving file:", file_name) with open(file_name, "wb") as wav: # Wav data info bits_per_sample = 16 channels_num, samples_num = 2, int(max_blocks * block_size) subchunk_size = 16 # always 16 for PCM subchunk2_size = int(samples_num * channels_num * bits_per_sample / 8) block_alignment = int(channels_num * bits_per_sample / 8) # Write RIFF header wav.write('RIFF'.encode('utf-8')) wav.write(struct.pack('<i', 4 + (8 + subchunk_size) + (8 + subchunk2_size))) # Size of the overall file wav.write('WAVE'.encode('utf-8')) # Write fmt subchunk wav.write('fmt '.encode('utf-8')) # chunk type wav.write(struct.pack('<i', subchunk_size)) # subchunk data size (16 for PCM) wav.write(struct.pack('<h', 1)) # compression type 1 - PCM wav.write(struct.pack('<h', channels_num)) # channels wav.write(struct.pack('<i', int(sample_rate))) # sample rate wav.write(struct.pack('<i', int(sample_rate * bits_per_sample * channels_num/ 8))) # byte rate wav.write(struct.pack('<h', block_alignment)) # block alignment wav.write(struct.pack('<h', bits_per_sample)) # sample depth # Write data subchunk wav.write('data'.encode('utf-8')) wav.write(struct.pack('<i', subchunk2_size)) while True: d_info = device.readStream(stream, [buffer], buffer_size) if d_info.ret > 0: data = buffer[0:2*d_info.ret] fileData = data if data_format == SoapySDR.SOAPY_SDR_CS8: fileData = data.astype('int16') wav.write(fileData) print("Block %d saved: %d bytes" % (block, 2*d_info.ret)) block += 1 if block > max_blocks: break device.deactivateStream(stream) device.closeStream(stream) if __name__ == "__main__": print("App started") # Forecast for active NOAA satellites # NOAA 15: 137.620, https://www.n2yo.com/passes/?s=25338 # NOAA 18: 137.9125, https://www.n2yo.com/passes/?s=28654 # NOAA 19: 137.100, https://www.n2yo.com/passes/?s=33591 # Wait for the start: 18-May 21:49 21:49: wait_for_start(datetime.datetime(2019, 5, 18, 21, 49, 0)) device = sdr_init() t_start = time.time() sdr_record(device, frequency=137912500, sample_rate=250000, gain=35, blocks_count=2100) print("Recording complete, time = %ds" % int(time.time() - t_start)) print() 


O SoapySDR plus é que o mesmo programa com alterações mínimas funcionará com outros receptores, por exemplo, com SDRPlay ou HackRF. Bem, sobre plataformas cruzadas também já foi mencionado.

Se os leitores ainda tiverem interesse no tópico de recepção de rádio, considere um exemplo de uso do SDR com o GNU Radio criando vários receptores virtuais com base em um hardware.

Source: https://habr.com/ru/post/pt452390/


All Articles