- PVSM.RU - https://www.pvsm.ru -

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

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

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

Для примера рассмотрим задачу параллельного приема нескольких FM-станций на один приемник. В качестве приемника будем использовать все тот же RTL SDR V3.

Продолжение под катом.

Установка

Для начала работы GNU Radio необходимо установить, дистрибутив для Windows можно скачать здесь [3]. Система эта кроссплатформенная, версии есть также под Linux и под OSX (вроде бы GNU Radio успешно запускали и на Raspberry Pi, но 100% гарантии дать не могу).

По сути, GNU Radio это целый фреймворк для цифровой обработки сигналов, в котором программа «собирается» из отдельных модулей. Есть большое количество уже готовых блоков, при желании также можно создавать свои собственные. Сами модули написаны на С++, а для взаимодействия блоков друг с другом используется Python. Желающие могут посмотреть на API более подробно [4], но на практике это, скорее всего, не пригодится — все действия можно делать визуально в программе GNU Radio Companion.

Система ориентирована на обработку потоков данных, так что каждый блок обычно имеет вход и выход. Далее, соединяя блоки в редакторе, мы получаем готовую систему. Сам интерфейс GNU Radio довольно простой, сложность состоит в понимании того, что делает тот или иной блок. Как говорилось ранее, низкоуровневая работа с SDR имеет высокий порог входа и требует некоторого знания в DSP и математике. Но мы рассмотрим простую задачу, для которой никаких специальных знаний не потребуется. Итак, приступим.

Начало работы

Запускаем GNU Radio Companion, создаем новый проект, тип проекта выбираем WX GUI, добавляем на экран и соединяем два блока, как показано на скриншоте.

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

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

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

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

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

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

Generating: 'D:\MyProjects\GNURadio\top_block.py'
Executing: C:Python27python.exe -u D:MyProjectsGNURadiotop_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.

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

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

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

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

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

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

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

Считаем внимательно. Мы делим тактовую частоту 2048КГц в 4 раза блоком Rational Resampler (получаем 512КГц), затем после Low Pass фильтра стоит WBFM-декодер с децимацией 10 (получаем 51.2КГц). В принципе, этот сигнал уже можно подать на звуковую карту, но высота тона будет чуть отличаться. Еще раз меняем тактовую частоту в 48/51, в результате будет тактовая частота 48.2КГц, разницей уже можно пренебречь.

Второй важный момент — тип входов. С приемника поступает комплексный IQ-сигнал (входы-выходы синего цвета), с FM-декодера выходит вещественный сигнал — входы и выходы желтого цвета. Если перепутать, ничего не заработает. Подробнее уже было на Хабре [5], нам достаточно понять общий принцип.

В общем, запускаем, убеждаемся что все работает. Можно запустить программу и слушать радио. Мы пойдем дальше — у нас же все-таки Software Defined радио — добавим одновременный прием второй станции.

Второй приемник добавляется любимым программистским методом — Ctrl+C/Ctrl+V. Добавляем переменную freq_2, копируем блоки и соединяем их точно также.

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

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

Запись

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

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

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

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

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

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

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

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

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

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

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

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

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

Автор: DmitrySpb79

Источник [7]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/gadzhety/318718

Ссылки в тексте:

[1] третьей части: https://habr.com/ru/post/452390/

[2] GNU Radio: https://www.gnuradio.org/

[3] здесь: https://downloads.myriadrf.org/builds/PothosSDR/

[4] более подробно: https://www.gnuradio.org/doc/doxygen/index.html

[5] уже было на Хабре: https://habr.com/ru/post/419735/

[6] почти по экспоненте: https://habrastorage.org/webt/ph/qh/oe/phqhoeh5j8osiqqttd2s2lbntow.png

[7] Источник: https://habr.com/ru/post/453038/?utm_source=habrahabr&utm_medium=rss&utm_campaign=453038