Software Defined Radio — как это работает? Часть 6
Привет, Хабр.
В предыдущей части мы рассмотрели возможность передачи простых сигналов с помощью GNU Radio. Сейчас мы пойдем дальше, и посмотрим, как передать что-нибудь посложнее. Начнем с радиолюбительских сигналов WSPR, а затем создадим работающий программный QAM-модем.

И как и в предыдущем случае, мы сделаем это, не написав ни одной строчки кода, программа также будет кроссплатформенной, и сможет работать как под OSX/Linux, так и под Windows. Я также покажу, как отлаживать модем средствами GNU Radio, вообще не имея никакого «железа».
Продолжение под катом.
Для тех, кто не пользовался GNU Radio, рекомендуется прочитать части 4 и 5, где описывались принципы работы с программой.
WSPR
Начнем с более простого, с WSPR — этот вид связи специально делался для тестов распространения слабых сигналов, т.е. то что нам и нужно — уровень мощности устройств типа LimeSDR не более 100 мВт. Сигнал WSPR передается о очень малой скоростью (2 минуты на сообщение из примерно 30 байт) в очень узкой полосе, что позволяет принимать его даже ниже уровня шумов. Передадим такой сигнал с помощью GNU Radio.
Для начала сигнал надо записать. Для этого возьмем программу WSJT, выставим все необходимые параметры (мощность, позывной, местоположение и пр). Укажем в настройках в качестве выходного аудиоустройства Virtual Audio Cable, и запишем сигнал в WAV. Паузы по краям нужно обрезать в любом редакторе (например Cool Edit), в итоге у нас должен получиться файл длительностью примерно в 2 минуты.
Теперь создаем граф в GNU Radio Companion.

Данный способ не претендует на максимальную эффективность, зато он довольно простой и понятный. Сигнал WSPR изначально находится на частоте 1500Гц, записанный wav-файл имеет частоту дискретизации 22050с/сек. Сначала мы ресемплируем сигнал в 57/5 раз, чтобы привести частоту дискретизации к требуемым 250.000с/сек. Затем мы сдвигаем частоту вверх на 10КГц (полезный сигнал при этом будет на частоте 11.5КГц), переводим сигнал в комплексный вид, требуемый приемнику, и вырезаем фильтром лишнее, оставляя частоты 11–12КГц.
Сигналы WSPR привязаны ко времени, и передаются каждую четную минуту (0:00, 0:02 и пр). Я запускал передачу в GNU Radio вручную, «на глаз» определяя интервал по часам, желающие могут дописать скрипт в Python для автоматического начала передачи.
Ждем нужное время, включаем передатчик, приемник, и проверяем результат.

При желании к программе также можно добавить автоматическую генерацию WSPR-файла на основе входных данных (позывного, местоположения, мощности передачи), примеры генерации WSPR для Python можно взять на github.
Интересно заметить, что на 432МГц дрифт частоты уже довольно заметен, хотя сигнал еще декодируется. А вот на частоте 1.3ГГц дрифт становится настолько большим, что WSPR уже не принимается — к SDR нужен внешний опорный генератор с более стабильным сигналом (или хотя бы программная коррекция частоты при передаче, хотя это менее удобно).
Если SDR позволяет передавать на низких частотах, то можно попробовать передачу и на КВ-диапазоне. Таким образом с HackRF удавалось передать сигнал на 1000 км на 14МГц с комнатной антенны, что можно считать неплохим результатом. Высокие частоты (433МГц и 1.3ГГц) пожалуй даже более интересны для опытов, но сигналы передаются только в прямой видимости, так что для таких экспериментов нужен второй участник на приемной стороне. Второй плюс таких тестов — на КВ в wspr не передавал только ленивый, а вот высокие частоты гораздо менее освоены.
QAM Модем
Пойдем дальше. WSPR достаточно несложный формат, сделаем что-нибудь поинтереснее — полноценный (ну почти) модем. При квадратурно-амплитудной модуляции одновременно изменяется и амплитуда, и фаза сигнала, что позволяет передавать данные с большей скоростью (но и занимаемая полоса также больше).
Рассмотрим первую часть — передатчик.

Как можно видеть, мы читаем данные из файла data.txt, затем с частотой семплирования 25КГц посылаем данные на энкодер пакетов, который преобразует поток в 4х-битный код. Этот поток поступает на квадратурный модулятор, затем частота семплирования увеличивается до частоты передатчика 250КГц, и сигнал сдвигается вверх на 80КГц (у многих приемников есть пик на нулевой частоте, и это будет мешать). Компонент Constellation Rect задает параметры модуляции — количество символов и сдвиг фаз и амплитуд.
Первая часть готова. Запускаем «передачу» и видим наш сигнал.

Мы можем протестировать наш передатчик, даже не имея никакой аппаратуры — для этого есть специальный блок Channel Model — модель канала связи. Там можно задать шум, сдвиг частоты и пр.
Вот так выглядит наш сигнал до и после передачи. Кстати, как можно видеть все «гармоники» передатчика ниже 120Дб ушли гораздо ниже уровня шумов.

Теперь прием. Фактически то же самое, только в обратном порядке.

Отдельно можно остановиться на последнем блоке UDP Sink. Непонятно почему, но в GNU Radio нет никакого компонента для просмотра текстовых данных. Поэтому мы просто посылаем данные по UDP на любой локальный порт (я выбрал 999).
Для приема напишем несложную программу на Python.
import socket
UDP_IP = "127.0.0.1"
UDP_PORT = 999
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP
sock.bind((UDP_IP, UDP_PORT))
sock.settimeout(1.0)
while True:
try:
data, addr = sock.recvfrom(64) # buffer size is 64 bytes
print("Msg:", data)
except socket.timeout:
pass
Результат: запускаем скрипт, запускаем GNU Radio, и видим в консоли принятые сообщения.

Как можно видеть, все работает, и ни приемника, ни антенн можно не иметь :)
Для тех, кто захочет повторить эксперименты, grc-файл проекта под спойлером. Должно работать и под Linux и под Windows.
Mon May 27 21:52:42 2019
options
author
window_size
category
[GRC Hier Blocks]
comment
description
_enabled
True
_coordinate
(8, 8)
_rotation
0
generate_options
qt_gui
hier_block_src_path
.:
id
top_block
max_nouts
0
qt_qss_theme
realtime_scheduling
run_command
{python} -u {filename}
run_options
prompt
run
True
thread_safe_setters
title
variable
comment
_enabled
True
_coordinate
(1144, 172)
_rotation
0
id
excess_bw
value
0.35
variable
comment
_enabled
True
_coordinate
(1104, 436)
_rotation
0
id
nfilts
value
32
variable
comment
_enabled
True
_coordinate
(1096, 588)
_rotation
0
id
nfilts_0
value
32
variable_constellation_rect
comment
const_points
[0.707+0.707j, -0.707+0.707j, -0.707-0.707j, 0.707-0.707j]
_enabled
True
_coordinate
(1104, 16)
_rotation
0
id
qpsk
imag_sect
2
real_sect
2
rot_sym
4
soft_dec_lut
None
precision
8
sym_map
[0, 1, 2, 3]
w_imag_sect
1
w_real_sect
1
variable
comment
_enabled
True
_coordinate
(1104, 372)
_rotation
0
id
rrc_taps
value
firdes.root_raised_cosine(nfilts, nfilts, 1.0/float(sps), 0.35, 45*nfilts)
variable
comment
_enabled
True
_coordinate
(1112, 508)
_rotation
0
id
rrc_taps_0
value
firdes.root_raised_cosine(nfilts, nfilts, 1.0/float(sps), 0.35, 45*nfilts)
variable
comment
_enabled
True
_coordinate
(168, 12)
_rotation
0
id
samp_rate
value
250000
variable
comment
_enabled
True
_coordinate
(1144, 244)
_rotation
0
id
sps
value
4
variable
comment
_enabled
True
_coordinate
(1104, 308)
_rotation
0
id
timing_loop_bw
value
6.28/100.0
analog_sig_source_x
amp
1
alias
comment
affinity
_enabled
True
freq
80000
_coordinate
(664, 20)
_rotation
0
id
analog_sig_source_x_0
maxoutbuf
0
minoutbuf
0
offset
0
type
complex
samp_rate
samp_rate
waveform
analog.GR_COS_WAVE
analog_sig_source_x
amp
1
alias
comment
affinity
_enabled
True
freq
-80000
_coordinate
(48, 540)
_rotation
0
id
analog_sig_source_x_1
maxoutbuf
0
minoutbuf
0
offset
0
type
complex
samp_rate
samp_rate
waveform
analog.GR_COS_WAVE
blks2_packet_decoder
access_code
alias
comment
affinity
_enabled
1
_coordinate
(296, 676)
_rotation
0
id
blks2_packet_decoder_0
maxoutbuf
0
minoutbuf
0
type
byte
threshold
-1
blks2_packet_encoder
access_code
bits_per_symbol
4
alias
comment
affinity
_enabled
1
_coordinate
(224, 76)
_rotation
0
id
blks2_packet_encoder_0
type
byte
maxoutbuf
0
minoutbuf
0
pad_for_usrp
True
payload_length
0
preamble
samples_per_symbol
4
blocks_file_source
alias
comment
affinity
_enabled
1
file
D:\MyProjects\GNURadio\data.txt
_coordinate
(8, 92)
_rotation
0
id
blocks_file_source_0
maxoutbuf
0
minoutbuf
0
type
byte
repeat
True
vlen
1
blocks_multiply_xx
alias
comment
affinity
_enabled
True
_coordinate
(920, 88)
_rotation
0
id
blocks_multiply_xx_0
type
complex
maxoutbuf
0
minoutbuf
0
num_inputs
2
vlen
1
blocks_multiply_xx
alias
comment
affinity
_enabled
True
_coordinate
(224, 496)
_rotation
0
id
blocks_multiply_xx_1
type
complex
maxoutbuf
0
minoutbuf
0
num_inputs
2
vlen
1
blocks_throttle
alias
comment
affinity
_enabled
1
_coordinate
(96, 196)
_rotation
0
id
blocks_throttle_1
ignoretag
True
maxoutbuf
0
minoutbuf
0
samples_per_second
25000
type
byte
vlen
1
blocks_udp_sink
alias
comment
affinity
ipaddr
127.0.0.1
port
999
_enabled
1
_coordinate
(680, 660)
_rotation
0
id
blocks_udp_sink_0
type
byte
psize
64
eof
True
vlen
1
channels_channel_model
alias
block_tags
False
comment
affinity
_enabled
1
epsilon
1.0
freq_offset
0.0
_coordinate
(504, 284)
_rotation
180
id
channels_channel_model_0
maxoutbuf
0
minoutbuf
0
noise_voltage
0.1
seed
0
taps
1.0 + 1.0j
digital_qam_demod
alias
comment
affinity
differential
True
_enabled
1
excess_bw
0.35
freq_bw
6.28/100.0
_coordinate
(672, 456)
_rotation
0
mod_code
"gray"
id
digital_qam_demod_0
log
False
maxoutbuf
0
minoutbuf
0
constellation_points
4
phase_bw
6.28/100.0
samples_per_symbol
4
timing_bw
6.28/100.0
verbose
False
digital_qam_mod
alias
comment
affinity
differential
True
_enabled
True
excess_bw
0.35
_coordinate
(384, 116)
_rotation
0
mod_code
"gray"
id
digital_qam_mod_0
log
False
maxoutbuf
0
minoutbuf
0
constellation_points
4
samples_per_symbol
4
verbose
False
low_pass_filter
beta
6.76
alias
comment
affinity
cutoff_freq
12000
decim
1
_enabled
True
type
fir_filter_ccf
_coordinate
(320, 460)
_rotation
0
gain
1
id
low_pass_filter_0
interp
1
maxoutbuf
0
minoutbuf
0
samp_rate
samp_rate
width
1000
win
firdes.WIN_HAMMING
qtgui_const_sink_x
autoscale
False
axislabels
True
alias
comment
affinity
_enabled
1
_coordinate
(456, 20)
gui_hint
_rotation
0
grid
False
id
qtgui_const_sink_x_0
legend
True
alpha1
1.0
color1
"blue"
label1
marker1
0
style1
0
width1
1
alpha10
1.0
color10
"red"
label10
marker10
0
style10
0
width10
1
alpha2
1.0
color2
"red"
label2
marker2
0
style2
0
width2
1
alpha3
1.0
color3
"red"
label3
marker3
0
style3
0
width3
1
alpha4
1.0
color4
"red"
label4
marker4
0
style4
0
width4
1
alpha5
1.0
color5
"red"
label5
marker5
0
style5
0
width5
1
alpha6
1.0
color6
"red"
label6
marker6
0
style6
0
width6
1
alpha7
1.0
color7
"red"
label7
marker7
0
style7
0
width7
1
alpha8
1.0
color8
"red"
label8
marker8
0
style8
0
width8
1
alpha9
1.0
color9
"red"
label9
marker9
0
style9
0
width9
1
name
""
nconnections
1
size
1024
tr_chan
0
tr_level
0.0
tr_mode
qtgui.TRIG_MODE_FREE
tr_slope
qtgui.TRIG_SLOPE_POS
tr_tag
""
type
complex
update_time
0.10
xmax
2
xmin
-2
ymax
2
ymin
-2
rational_resampler_xxx
alias
comment
affinity
decim
1
_enabled
True
fbw
0
_coordinate
(648, 148)
_rotation
0
id
rational_resampler_xxx_0
interp
10
maxoutbuf
0
minoutbuf
0
taps
type
ccc
rational_resampler_xxx
alias
comment
affinity
decim
10
_enabled
True
fbw
0
_coordinate
(480, 484)
_rotation
0
id
rational_resampler_xxx_0_0
interp
1
maxoutbuf
0
minoutbuf
0
taps
type
ccc
analog_sig_source_x_0
blocks_multiply_xx_0
0
0
analog_sig_source_x_1
blocks_multiply_xx_1
0
1
blks2_packet_decoder_0
blocks_udp_sink_0
0
0
blks2_packet_encoder_0
digital_qam_mod_0
0
0
blocks_file_source_0
blocks_throttle_1
0
0
blocks_multiply_xx_0
channels_channel_model_0
0
0
blocks_multiply_xx_1
low_pass_filter_0
0
0
blocks_throttle_1
blks2_packet_encoder_0
0
0
channels_channel_model_0
blocks_multiply_xx_1
0
0
digital_qam_demod_0
blks2_packet_decoder_0
0
0
digital_qam_mod_0
qtgui_const_sink_x_0
0
0
digital_qam_mod_0
rational_resampler_xxx_0
0
0
low_pass_filter_0
rational_resampler_xxx_0_0
0
0
rational_resampler_xxx_0
blocks_multiply_xx_0
0
1
rational_resampler_xxx_0_0
digital_qam_demod_0
0
0
Хорошее руководство по созданию более сложного модема есть на сайте GNU Radio, но они используют custom block для демодуляции, запустить который удалось только под Linux. В вышеприведенном примере с этим проблем нет.
Заключение
Как можно видеть, GNU Radio — довольно интересная программа для разной работы с сигналами, в которой можно делать много разных интересных вещей. Если у аудитории не пропадет интерес (есть чувство, что я углубляюсь в узкие темы, которые большинству уже мало интересны), можно попробовать рассмотреть передачу чего-то более интересного, например видео.
