Software Defined Radio — как это работает? Часть 4

Привет Хабр.

В третьей части было рассказано, как получить доступ к SDR-приемнику посредством языка Python. Сейчас мы познакомимся с программой GNU Radio — системой, позволяющей создать достаточно сложную конфигурацию радиоустройства, не написав ни единой строчки кода.

lz0o-askafi53h9qmtsy0ktkdog.png

Для примера рассмотрим задачу параллельного приема нескольких 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, добавляем на экран и соединяем два блока, как показано на скриншоте.

apzmd5drzq9g-0fkn2yzeosagz0.png

Мы видим два типа блоков — Source (источник) и Sink (выход, «слив»). RTL-SDR — это наш приемник, FFT GUI — это виртуальный спектроанализатор.

Переменную Sample Rate устанавливаем в 2048000, это частота дискретизации нашего приемника. Частоту RTL-SDR оставляем по умолчанию равной 100МГц.

Запускаем проект — все работает, мы видим спектр FM-станций. Первая программа для GNU Radio готова!

cgiimukqq7tujdtv-mprdvtl8wc.png

Если мы посмотрим лог, то увидим такие строки.

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 минуту.

top_blocks.py
#!/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.

mzkclq8dusvoy7kxqqcrbajlxzc.png

Т.к. система построена на базе Python, то в полях ввода параметров мы можем использовать выражения, что достаточно удобно.

Схема в итоге должна выглядеть так:

b0yl7z1omt-7kidj7i863nc2exq.png

Теперь необходимо добавить сразу несколько блоков — уменьшить тактовую частоту входного сигнала (она равна 2048КГц), отфильтровать сигнал, подать его на FM-декодер, затем еще раз уменьшить тактовую частоту до 48КГц.

Результат показан на картинке:

8nwqhtrflggwkkdia9xhbuok2io.png

Считаем внимательно. Мы делим тактовую частоту 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, копируем блоки и соединяем их точно также.

wb8-dowoop7frpjeigt1pmb0_jo.png

Результат вполне сюрреалистичный — две FM-станции можно слушать одновременно. Тем же самым методом (Ctrl+V) можно добавить и третью станцию.

Запись


Слушать две станции оригинально, но на практике мало полезно. Сделаем что-то более нужное, например добавим запись звука в отдельные файлы. Это может быть достаточно удобно — с одного физического приемника можно параллельно записывать несколько каналов.

Добавим к каждому выходу компонент File Sink, как показано на скриншоте.

dv-zahcv6ushuetsll7by3jyvki.png

Windows-версия почему-то требует абсолютные пути файлов, иначе запись не работает. Запускаем, убеждаемся что все нормально. Размер сохраняемых файлов довольно большой, т.к. по умолчанию записывается формат float. Запись в формате int оставлю читателям в качестве домашнего задания.

Получившиеся файлы можно открыть в Cool Edit и убедиться, что звук записался нормально.

zoozlx5plq7aw_jftgdc-ddyrl0.png

p9z9tuscd-0w3fa_tkdqq0qii68.png

Разумеется, число записываемых каналов можно увеличить, оно ограничено только полосой пропускания приемника и можностью компьютера. Кроме File Sink можно использовать и UDP Sink, так что программу можно использовать для трансляции по сети.

Запуск из командной строки


И последнее. Если использовать программу автономно, например для многоканальной записи, то UI в принципе и не нужен. В верхнем левом блоке Options меняем параметр Run Options на No UI. Запускаем программу еще раз, убеждаемся что все работает. Теперь сохраняем сгенерированный файл top_block.py — мы можем просто запускать его из командной строки, например из bat-файла или из консоли.

ypgnqytpqchjfwsk_fung67k5ck.png

Если кому интересно, сгенерированный файл сохранен под спойлером.

recorder.py
#!/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 наверно можно закрыть — все основные моменты уже рассмотрены (да и количество просмотров от первой к третьей части падает почти по экспоненте;). Надеюсь все же, что некоторое понимание того как это работает, у читателей осталось. Ну и всем удачных экспериментов.

© Habrahabr.ru