Software Defined Radio — как это работает? Часть 4
Привет Хабр.
В третьей части было рассказано, как получить доступ к SDR-приемнику посредством языка Python. Сейчас мы познакомимся с программой GNU Radio — системой, позволяющей создать достаточно сложную конфигурацию радиоустройства, не написав ни единой строчки кода.
Для примера рассмотрим задачу параллельного приема нескольких FM-станций на один приемник. В качестве приемника будем использовать все тот же RTL SDR V3.
Продолжение под катом.
Установка
Для начала работы GNU Radio необходимо установить, дистрибутив для Windows можно скачать здесь. Система эта кроссплатформенная, версии есть также под Linux и под OSX (вроде бы GNU Radio успешно запускали и на Raspberry Pi, но 100% гарантии дать не могу).
По сути, GNU Radio это целый фреймворк для цифровой обработки сигналов, в котором программа «собирается» из отдельных модулей. Есть большое количество уже готовых блоков, при желании также можно создавать свои собственные. Сами модули написаны на С++, а для взаимодействия блоков друг с другом используется Python. Желающие могут посмотреть на API более подробно, но на практике это, скорее всего, не пригодится — все действия можно делать визуально в программе GNU Radio Companion.
Система ориентирована на обработку потоков данных, так что каждый блок обычно имеет вход и выход. Далее, соединяя блоки в редакторе, мы получаем готовую систему. Сам интерфейс GNU Radio довольно простой, сложность состоит в понимании того, что делает тот или иной блок. Как говорилось ранее, низкоуровневая работа с SDR имеет высокий порог входа и требует некоторого знания в DSP и математике. Но мы рассмотрим простую задачу, для которой никаких специальных знаний не потребуется. Итак, приступим.
Начало работы
Запускаем GNU Radio Companion, создаем новый проект, тип проекта выбираем WX GUI, добавляем на экран и соединяем два блока, как показано на скриншоте.
Мы видим два типа блоков — Source (источник) и Sink (выход, «слив»). RTL-SDR — это наш приемник, FFT GUI — это виртуальный спектроанализатор.
Переменную Sample Rate устанавливаем в 2048000, это частота дискретизации нашего приемника. Частоту RTL-SDR оставляем по умолчанию равной 100МГц.
Запускаем проект — все работает, мы видим спектр FM-станций. Первая программа для GNU Radio готова!
Если мы посмотрим лог, то увидим такие строки.
Generating: 'D:\\MyProjects\\GNURadio\\top_block.py'
Executing: C:\Python27\python.exe -u D:\MyProjects\GNURadio\top_block.py
Да, мы можем посмотреть файл top_block.py, который сгенерил нам GNU Radio Companion. Истинные джедаи могут писать непосредственно в Python, но требуемый код, как мы видим, довольно большой. Мы же создали его за 1 минуту.
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
##################################################
# GNU Radio Python Flow Graph
# Title: Top Block
# Generated: Wed May 22 22:05:14 2019
##################################################
if __name__ == '__main__':
import ctypes
import sys
if sys.platform.startswith('linux'):
try:
x11 = ctypes.cdll.LoadLibrary('libX11.so')
x11.XInitThreads()
except:
print "Warning: failed to XInitThreads()"
from gnuradio import eng_notation
from gnuradio import gr
from gnuradio import wxgui
from gnuradio.eng_option import eng_option
from gnuradio.fft import window
from gnuradio.filter import firdes
from gnuradio.wxgui import fftsink2
from grc_gnuradio import wxgui as grc_wxgui
from optparse import OptionParser
import osmosdr
import time
import wx
class top_block(grc_wxgui.top_block_gui):
def __init__(self):
grc_wxgui.top_block_gui.__init__(self, title="Top Block")
##################################################
# Variables
##################################################
self.samp_rate = samp_rate = 2048000
##################################################
# Blocks
##################################################
self.wxgui_fftsink2_0 = fftsink2.fft_sink_c(
self.GetWin(),
baseband_freq=0,
y_per_div=10,
y_divs=10,
ref_level=0,
ref_scale=2.0,
sample_rate=samp_rate,
fft_size=1024,
fft_rate=15,
average=False,
avg_alpha=None,
title='FFT Plot',
peak_hold=False,
)
self.Add(self.wxgui_fftsink2_0.win)
self.rtlsdr_source_0 = osmosdr.source( args="numchan=" + str(1) + " " + '' )
self.rtlsdr_source_0.set_sample_rate(samp_rate)
self.rtlsdr_source_0.set_center_freq(100e6, 0)
self.rtlsdr_source_0.set_freq_corr(0, 0)
self.rtlsdr_source_0.set_dc_offset_mode(0, 0)
self.rtlsdr_source_0.set_iq_balance_mode(0, 0)
self.rtlsdr_source_0.set_gain_mode(False, 0)
self.rtlsdr_source_0.set_gain(10, 0)
self.rtlsdr_source_0.set_if_gain(20, 0)
self.rtlsdr_source_0.set_bb_gain(20, 0)
self.rtlsdr_source_0.set_antenna('', 0)
self.rtlsdr_source_0.set_bandwidth(0, 0)
##################################################
# Connections
##################################################
self.connect((self.rtlsdr_source_0, 0), (self.wxgui_fftsink2_0, 0))
def get_samp_rate(self):
return self.samp_rate
def set_samp_rate(self, samp_rate):
self.samp_rate = samp_rate
self.wxgui_fftsink2_0.set_sample_rate(self.samp_rate)
self.rtlsdr_source_0.set_sample_rate(self.samp_rate)
def main(top_block_cls=top_block, options=None):
tb = top_block_cls()
tb.Start(True)
tb.Wait()
if __name__ == '__main__':
main()
Впрочем, если убрать громоздкую инициализацию, то мы увидим, что ключевых строк кода не так уж много.
from gnuradio import gr
from gnuradio.wxgui import fftsink2
import osmosdr
class top_block(grc_wxgui.top_block_gui):
def __init__(self):
grc_wxgui.top_block_gui.__init__(self, title="Top Block")
self.samp_rate = samp_rate = 2048000
self.wxgui_fftsink2_0 = fftsink2.fft_sink_c(...)
self.Add(self.wxgui_fftsink2_0.win)
self.rtlsdr_source_0 = osmosdr.source(args="numchan=" + str(1) + " " + '' )
self.connect((self.rtlsdr_source_0, 0), (self.wxgui_fftsink2_0, 0))
def main(top_block_cls=top_block, options=None):
tb = top_block_cls()
tb.Start(True)
tb.Wait()
Так что в принципе, это можно написать вручную. Но мышью оно все-таки быстрее. Хотя возможность поменять код иногда может пригодиться, если захочется добавить какую-то нестандартную логику.
Принимаем FM-радио
Теперь попробуем принять одну из станций. Как было видно из скриншотов, центральная частота приемника 100МГц и ширина полосы пропускания около 2МГц. На спектре мы видим две станции, на 100.1МГц и 100.7МГц соответственно.
Первым шагом необходимо перенести спектр станции в центр, сейчас он отстоит вправо на 100КГц. Для этого вспоминаем школьную формулу умножения косинусов — в результате будет две частоты, сумма и разность — нужная станция сдвинется в центр, что нам и нужно (а лишнее мы потом отфильтруем).
Создаем две переменные для хранения частоты freq_center=100000000 и freq_1=100100000, также добавляем генератор сигналов с частотой freq_center — freq_1.
Т.к. система построена на базе Python, то в полях ввода параметров мы можем использовать выражения, что достаточно удобно.
Схема в итоге должна выглядеть так:
Теперь необходимо добавить сразу несколько блоков — уменьшить тактовую частоту входного сигнала (она равна 2048КГц), отфильтровать сигнал, подать его на FM-декодер, затем еще раз уменьшить тактовую частоту до 48КГц.
Результат показан на картинке:
Считаем внимательно. Мы делим тактовую частоту 2048КГц в 4 раза блоком Rational Resampler (получаем 512КГц), затем после Low Pass фильтра стоит WBFM-декодер с децимацией 10 (получаем 51.2КГц). В принципе, этот сигнал уже можно подать на звуковую карту, но высота тона будет чуть отличаться. Еще раз меняем тактовую частоту в 48/51, в результате будет тактовая частота 48.2КГц, разницей уже можно пренебречь.
Второй важный момент — тип входов. С приемника поступает комплексный IQ-сигнал (входы-выходы синего цвета), с FM-декодера выходит вещественный сигнал — входы и выходы желтого цвета. Если перепутать, ничего не заработает. Подробнее уже было на Хабре, нам достаточно понять общий принцип.
В общем, запускаем, убеждаемся что все работает. Можно запустить программу и слушать радио. Мы пойдем дальше — у нас же все-таки Software Defined радио — добавим одновременный прием второй станции.
Второй приемник добавляется любимым программистским методом — Ctrl+C/Ctrl+V. Добавляем переменную freq_2, копируем блоки и соединяем их точно также.
Результат вполне сюрреалистичный — две FM-станции можно слушать одновременно. Тем же самым методом (Ctrl+V) можно добавить и третью станцию.
Запись
Слушать две станции оригинально, но на практике мало полезно. Сделаем что-то более нужное, например добавим запись звука в отдельные файлы. Это может быть достаточно удобно — с одного физического приемника можно параллельно записывать несколько каналов.
Добавим к каждому выходу компонент File Sink, как показано на скриншоте.
Windows-версия почему-то требует абсолютные пути файлов, иначе запись не работает. Запускаем, убеждаемся что все нормально. Размер сохраняемых файлов довольно большой, т.к. по умолчанию записывается формат float. Запись в формате int оставлю читателям в качестве домашнего задания.
Получившиеся файлы можно открыть в Cool Edit и убедиться, что звук записался нормально.
Разумеется, число записываемых каналов можно увеличить, оно ограничено только полосой пропускания приемника и можностью компьютера. Кроме File Sink можно использовать и UDP Sink, так что программу можно использовать для трансляции по сети.
Запуск из командной строки
И последнее. Если использовать программу автономно, например для многоканальной записи, то UI в принципе и не нужен. В верхнем левом блоке Options меняем параметр Run Options на No UI. Запускаем программу еще раз, убеждаемся что все работает. Теперь сохраняем сгенерированный файл top_block.py — мы можем просто запускать его из командной строки, например из bat-файла или из консоли.
Если кому интересно, сгенерированный файл сохранен под спойлером.
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
##################################################
# GNU Radio Python Flow Graph
# Title: Top Block
# Generated: Fri May 24 21:47:03 2019
##################################################
from gnuradio import analog
from gnuradio import audio
from gnuradio import blocks
from gnuradio import eng_notation
from gnuradio import filter
from gnuradio import gr
from gnuradio.eng_option import eng_option
from gnuradio.filter import firdes
from optparse import OptionParser
import osmosdr
import time
class top_block(gr.top_block):
def __init__(self):
gr.top_block.__init__(self, "Top Block")
##################################################
# Variables
##################################################
self.samp_rate = samp_rate = 2048000
self.freq_center = freq_center = 100000000
self.freq_2 = freq_2 = 100700000
self.freq_1 = freq_1 = 100100000
##################################################
# Blocks
##################################################
self.rtlsdr_source_0 = osmosdr.source( args="numchan=" + str(1) + " " + '' )
self.rtlsdr_source_0.set_sample_rate(samp_rate)
self.rtlsdr_source_0.set_center_freq(freq_center, 0)
self.rtlsdr_source_0.set_freq_corr(0, 0)
self.rtlsdr_source_0.set_dc_offset_mode(0, 0)
self.rtlsdr_source_0.set_iq_balance_mode(0, 0)
self.rtlsdr_source_0.set_gain_mode(False, 0)
self.rtlsdr_source_0.set_gain(10, 0)
self.rtlsdr_source_0.set_if_gain(20, 0)
self.rtlsdr_source_0.set_bb_gain(20, 0)
self.rtlsdr_source_0.set_antenna('', 0)
self.rtlsdr_source_0.set_bandwidth(0, 0)
self.rational_resampler_xxx_1_0 = filter.rational_resampler_fff(
interpolation=48,
decimation=51,
taps=None,
fractional_bw=None,
)
self.rational_resampler_xxx_1 = filter.rational_resampler_fff(
interpolation=48,
decimation=51,
taps=None,
fractional_bw=None,
)
self.rational_resampler_xxx_0_0 = filter.rational_resampler_ccc(
interpolation=1,
decimation=4,
taps=None,
fractional_bw=None,
)
self.rational_resampler_xxx_0 = filter.rational_resampler_ccc(
interpolation=1,
decimation=4,
taps=None,
fractional_bw=None,
)
self.low_pass_filter_0_0 = filter.fir_filter_ccf(1, firdes.low_pass(
1, samp_rate/4, 100000, 500000, firdes.WIN_HAMMING, 6.76))
self.low_pass_filter_0 = filter.fir_filter_ccf(1, firdes.low_pass(
1, samp_rate/4, 100000, 500000, firdes.WIN_HAMMING, 6.76))
self.blocks_multiply_xx_0_0 = blocks.multiply_vcc(1)
self.blocks_multiply_xx_0 = blocks.multiply_vcc(1)
self.blocks_file_sink_0_0 = blocks.file_sink(gr.sizeof_float*1, 'D:\\Temp\\1\\audio2.snd', False)
self.blocks_file_sink_0_0.set_unbuffered(False)
self.blocks_file_sink_0 = blocks.file_sink(gr.sizeof_float*1, 'D:\\Temp\\1\\audio1.snd', False)
self.blocks_file_sink_0.set_unbuffered(False)
self.audio_sink_0 = audio.sink(48000, '', True)
self.analog_wfm_rcv_0_0 = analog.wfm_rcv(
quad_rate=samp_rate/4,
audio_decimation=10,
)
self.analog_wfm_rcv_0 = analog.wfm_rcv(
quad_rate=samp_rate/4,
audio_decimation=10,
)
self.analog_sig_source_x_0_0 = analog.sig_source_c(samp_rate, analog.GR_COS_WAVE, freq_center - freq_2, 1, 0)
self.analog_sig_source_x_0 = analog.sig_source_c(samp_rate, analog.GR_COS_WAVE, freq_center - freq_1, 1, 0)
##################################################
# Connections
##################################################
self.connect((self.analog_sig_source_x_0, 0), (self.blocks_multiply_xx_0, 1))
self.connect((self.analog_sig_source_x_0_0, 0), (self.blocks_multiply_xx_0_0, 1))
self.connect((self.analog_wfm_rcv_0, 0), (self.rational_resampler_xxx_1, 0))
self.connect((self.analog_wfm_rcv_0_0, 0), (self.rational_resampler_xxx_1_0, 0))
self.connect((self.blocks_multiply_xx_0, 0), (self.rational_resampler_xxx_0, 0))
self.connect((self.blocks_multiply_xx_0_0, 0), (self.rational_resampler_xxx_0_0, 0))
self.connect((self.low_pass_filter_0, 0), (self.analog_wfm_rcv_0, 0))
self.connect((self.low_pass_filter_0_0, 0), (self.analog_wfm_rcv_0_0, 0))
self.connect((self.rational_resampler_xxx_0, 0), (self.low_pass_filter_0, 0))
self.connect((self.rational_resampler_xxx_0_0, 0), (self.low_pass_filter_0_0, 0))
self.connect((self.rational_resampler_xxx_1, 0), (self.audio_sink_0, 0))
self.connect((self.rational_resampler_xxx_1, 0), (self.blocks_file_sink_0, 0))
self.connect((self.rational_resampler_xxx_1_0, 0), (self.audio_sink_0, 1))
self.connect((self.rational_resampler_xxx_1_0, 0), (self.blocks_file_sink_0_0, 0))
self.connect((self.rtlsdr_source_0, 0), (self.blocks_multiply_xx_0, 0))
self.connect((self.rtlsdr_source_0, 0), (self.blocks_multiply_xx_0_0, 0))
def get_samp_rate(self):
return self.samp_rate
def set_samp_rate(self, samp_rate):
self.samp_rate = samp_rate
self.rtlsdr_source_0.set_sample_rate(self.samp_rate)
self.low_pass_filter_0_0.set_taps(firdes.low_pass(1, self.samp_rate/4, 100000, 500000, firdes.WIN_HAMMING, 6.76))
self.low_pass_filter_0.set_taps(firdes.low_pass(1, self.samp_rate/4, 100000, 500000, firdes.WIN_HAMMING, 6.76))
self.analog_sig_source_x_0_0.set_sampling_freq(self.samp_rate)
self.analog_sig_source_x_0.set_sampling_freq(self.samp_rate)
def get_freq_center(self):
return self.freq_center
def set_freq_center(self, freq_center):
self.freq_center = freq_center
self.rtlsdr_source_0.set_center_freq(self.freq_center, 0)
self.analog_sig_source_x_0_0.set_frequency(self.freq_center - self.freq_2)
self.analog_sig_source_x_0.set_frequency(self.freq_center - self.freq_1)
def get_freq_2(self):
return self.freq_2
def set_freq_2(self, freq_2):
self.freq_2 = freq_2
self.analog_sig_source_x_0_0.set_frequency(self.freq_center - self.freq_2)
def get_freq_1(self):
return self.freq_1
def set_freq_1(self, freq_1):
self.freq_1 = freq_1
self.analog_sig_source_x_0.set_frequency(self.freq_center - self.freq_1)
def main(top_block_cls=top_block, options=None):
tb = top_block_cls()
tb.start()
try:
raw_input('Press Enter to quit: ')
except EOFError:
pass
tb.stop()
tb.wait()
if __name__ == '__main__':
main()
Удобно и то, что система является кросс-платформенной, и получившаяся программа может работать на Linux, Windows и OSX.
Заключение
Можно сказать, что GNU Radio достаточно сложная система, не в плане рисования блоков конечно, а в плане понимания того, как все это работает. Но какие-то несложные вещи сделать вполне посильно и интересно. GNU Radio также удобно использовать как «виртуальную лабораторию» для обучения — к любой части схемы можно подключить виртуальный осциллограф или спектроанализатор и посмотреть, как выглядит сигнал.
Если не будет каких-то отдельных пожеланий, тему введения в SDR наверно можно закрыть — все основные моменты уже рассмотрены (да и количество просмотров от первой к третьей части падает почти по экспоненте;). Надеюсь все же, что некоторое понимание того как это работает, у читателей осталось. Ну и всем удачных экспериментов.