Software Defined Radio — как это работает? Часть 7
Привет, Хабр.
В предыдущей части про передачу в GNU Radio был задан вопрос о том, можно ли декодировать протокол LoRa (передача данных для устройств с низким энергопотреблением) с помощью SDR. Мне эта тема показалась интересной, тем более что и сам сигнал у LoRa довольно-таки необычный — так называемая Chirp Spread Spectrum modulation, или «модуляция чирпами».
Как это работает, продолжение под катом.
Кстати, дословный перевод Chirp modulation звучал бы как «модуляция чириканием», но это звучит уж совсем сюрреалистично, так что лучше оставлю простое слово «чирп».
Модуляция LoRa
Как было сказано выше, при передаче LoRa используется способ модуляции при помощи «чирпов», кстати запатентованный компанией Semtech. Если кто хочет подробностей реализации с формулами, можно почитать PDF на сайте semtech или здесь, ну, а если совсем грубо, то один «чирп» — это одно изменение частоты, такими изменениями и кодируется битовый поток, как показано на картинке выше. Параметрами сигнала в LoRa являются SF (spreading factor — по сути, длительность одного «чирпа») и bandwidth — ширина полосы передачи. Параметр SF задается предопределенными значениями SF7 — SF12, где 7 самый быстрый, а 12 — самый медленный режим (для примера можно посмотреть картинку с иллюстрацией разных скоростей «чирпования» с researchgate).
Очевидно, чем меньше длина «чирпа» и чем шире полоса, тем больше можно получить скорость передачи. Все это связано примерно такой таблицей:
С точки зрения дальности и помехозащищенности выгодно передавать медленно и печально, но при этом мы во-первых, теряем в скорости, во-вторых, проигрываем во времени, а по правилам LoRa, устройство может передавать не более 1% времени, чтобы не мешать другим устройствам. Так что выбор оптимальной скорости передачи для малопотребляющих устройств задача тоже не простая.
С общим принципом надеюсь, ясно, теперь перейдем к SDR и декодированию.
Железо
Для тестирования я использовал LoRa Click RN2483 и Arduino M0, просто потому что они были в наличии.
Это достаточно удобный форм-фактор для прототипирования, т.к. позволяет легко заменить плату одну на другую без пайки (в этом формате, называемом MikroBUS доступно много разной периферии).
Код в черновом варианте, не претендующий на production, добавлен под спойлер. В качестве теста передается значение »1234».
// RN2483 Modem and LoRa Click test TX. Tested with Arduino M0
int reset = A2;
int rts = 9; // CS
int cts = 3; // INT
// the setup routine runs once when you press reset:
void setup()
{
Serial1.begin(57600); // Serial port to radio
// output LED pin
pinMode(LED_BUILTIN, OUTPUT);
pinMode(cts, INPUT);
pinMode(rts, OUTPUT);
digitalWrite(rts, HIGH);
// Reset rn2483
pinMode(reset, OUTPUT);
digitalWrite(reset, LOW);
delay(100);
digitalWrite(reset, HIGH);
delay(100);
sendCommand("sys get ver\r\n");
sendCommand("sys get hweui\r\n");
sendCommand("mac pause\r\n");
sendCommand("radio set mod lora\r\n");
sendCommand("radio set pwr -3\r\n"); // the transceiver output power, from -3 to 15
sendCommand("radio set sf sf8\r\n"); // sf7..sf12, sf7 the fastest spreading factor but gives the shortest range
// sendCommand("mac set dr 0\r\n"); // data rate: 0-4, 4 faster
sendCommand("radio set freq 869100000\r\n");
// sendCommand("radio set afcbw 41.7\r\n");
sendCommand("radio set rxbw 125\r\n");
// sendCommand("radio set prlen 8\r\n");
sendCommand("radio set crc on\r\n");
// sendCommand("radio set iqi off\r\n");
sendCommand("radio set cr 4/8\r\n");
// sendCommand("radio set wdt 60000\r\n"); // disable for continuous reception
// sendCommand("radio set sync 12\r\n");
sendCommand("radio set bw 125\r\n");
}
void sendCommand(const char *cmd)
{
Serial1.print(cmd);
String incoming = Serial1.readString();
// SerialUSB.print(cmd);
// SerialUSB.println(incoming);
}
// the loop routine runs over and over again forever:
void loop()
{
char data[64] = {0};
// hexadecimal value representing the data to be transmitted, from 0 to 255 bytes for LoRa modulation and from 0 to 64 bytes for FSK modulation.
sprintf(data, "radio tx 1234\r\n");
sendCommand(data);
if (msg_num > 10000) msg_num=0;
digitalWrite(LED_BUILTIN, 1);
delay(400);
digitalWrite(LED_BUILTIN, 0);
delay(600);
}
Кстати, максимальная заявленная дальность передачи для RN2483 составляет до 15 км, на практике, при наличии одноэтажной застройки сигнал пропадает уже за 1 км, и может быть не более 100 м в городских «муравейниках».
Запускаем модем, и приступаем к декодированию.
Декодирование
В самом GNU Radio поддержки LoRa нет, так что придется использовать сторонние компоненты. Их нашлось всего два, и к сожалению, оба автора не проявили никакой фантазии в названии, и назвали их совершенно одинаково — gr-lora (https://github.com/rpp0/gr-lora и https://github.com/BastilleResearch/gr-lora соответственно).
rpp0/gr-lora
Скачать исходники декодера можно с github, сборка стандартна и затруднений не вызывает:
git clone https://github.com/rpp0/gr-lora.git
cd gr-lora
mkdir build
cd build
cmake ..
make && sudo make install && sudo ldconfig
После установки в GNU Radio появляются дополнительные блоки, из которых несложно собрать декодер. В качестве параметров декодера необходимо указать ширину полосы передачи, spreading factor, центральную частоту SDR и частоту приема.
Блоки в GNU Radio стандартизированы, так что можно использовать любой приемник, например RTL-SDR. Я использовал SDRPlay. Для вывода данных в консоль использовалась простая программа на Python.
import socket
UDP_IP = "127.0.0.1"
UDP_PORT = 40868
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((UDP_IP, UDP_PORT))
sock.settimeout(0.5)
while True:
try:
data, addr = sock.recvfrom(128) # buffer size is 1024 bytes
print("Msg:", ' '.join('{:02x}'.format(x) for x in data))
except socket.timeout:
pass
Результат работы показан на рисунке.
Как можно видеть, в строке есть блоки заголовка и окончания передачи, а в середине мы видим наши данные »1234».
BastilleResearch/gr-lora
Этот модуль примечателен тем, что может работать не только на прием, но и на передачу. Установка примерно такая же: компонент нужно собрать из исходников.
git clone git://github.com/BastilleResearch/gr-lora.git
cd gr-lora
mkdir build
cd build
cmake ..
make && sudo make install && sudo ldconfig
Connection Graph для данного декодера показан на рисунке.
Как можно видеть, блоков тут побольше. Rotator и Polyphase Resampler выделяют нужную частоту и обрезают лишнее, Demodulator преобразует «чирпы» в бинарный код (на выходе получается последовательность вроде »17 00 3e 00 38 00 2f 00 01 00 39 00 2c 00 30 00 c6 00 18 00 7e 00 d5 00 85 00 e9 00 d8 00 67 00 c4 00»), а Decoder формирует окончательный пакет.
К сожалению, нормально оно так и не заработало. Декодирование определенно работает, во время работы модема данные появляются, но принимаемые сообщения не имеют ничего общего с передаваемыми.
Причину я так и не понял, то ли где-то ошибся в настройках, то ли этот декодер совместим только со своим же кодером. Желающие могут проверить самостоятельно с помощью Channel Model.
LoRaWAN
Как можно видеть, выше рассматривался нижний, физический уровень передачи. В более высокоуровневом протоколе LoRaWAN поверх помещается еще один логический уровень — с шифрованием, ключами и прочими сервисами. Желающие посмотреть как устроено кодирование, могут попробовать онлайн-декодер здесь.
Заключение
Как можно видеть, декодирование LoRa средствами SDR вполне возможно. Конечно, реальный gateway делать на базе SDR вряд ли целесообразно — его чувствительность будет хуже чувствительности «настоящих» модемов, которые специально рассчитаны на прием слабых сигналов, и имеют более узкополосные фильтры и LNA. Но для тестирования или исследования это может быть вполне интересно.
Для тех, кто захочет попробовать самостоятельно, исходные grc-файлы GNU Radio под спойлером.
Mon Jun 3 09:39:45 2019
options
author
window_size
category
[GRC Hier Blocks]
comment
description
_enabled
True
_coordinate
(8, 8)
_rotation
0
generate_options
wx_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
(760, 12)
_rotation
0
id
bw
value
125000.0
variable
comment
_enabled
1
_coordinate
(936, 12)
_rotation
0
id
code_rate
value
4
variable
comment
_enabled
1
_coordinate
(848, 12)
_rotation
0
id
header
value
True
variable
comment
_enabled
1
_coordinate
(680, 12)
_rotation
0
id
ldr
value
True
variable
comment
_enabled
True
_coordinate
(400, 12)
_rotation
0
id
offset
value
-100000.0
variable
comment
_enabled
True
_coordinate
(8, 76)
_rotation
0
id
samp_rate
value
1000000
variable
comment
_enabled
1
_coordinate
(544, 12)
_rotation
0
id
spreading_factor
value
8
blocks_rotator_cc
alias
comment
affinity
_enabled
1
_coordinate
(416, 252)
_rotation
0
id
blocks_rotator_cc_0
maxoutbuf
0
minoutbuf
0
phase_inc
(2 * math.pi * offset) / samp_rate
blocks_socket_pdu
alias
comment
affinity
_enabled
1
_coordinate
(944, 452)
_rotation
0
host
127.0.0.1
id
blocks_socket_pdu_0
mtu
10000
maxoutbuf
0
minoutbuf
0
port
40868
tcp_no_delay
False
type
"UDP_CLIENT"
import
alias
comment
_enabled
True
_coordinate
(296, 12)
_rotation
0
id
import_0
import
import math
lora_decode
alias
code_rate
code_rate
comment
affinity
_enabled
1
header
header
_coordinate
(648, 452)
_rotation
0
id
lora_decode_0
low_data_rate
ldr
maxoutbuf
0
minoutbuf
0
spreading_factor
spreading_factor
lora_demod
alias
comment
affinity
_enabled
1
fft_factor
2
beta
25.0
_coordinate
(384, 452)
_rotation
0
id
lora_demod_0
low_data_rate
ldr
maxoutbuf
0
minoutbuf
0
spreading_factor
spreading_factor
pfb_arb_resampler_xxx
alias
comment
affinity
_enabled
1
_coordinate
(656, 292)
_rotation
0
id
pfb_arb_resampler_xxx_0
maxoutbuf
0
minoutbuf
0
nfilts
32
rrate
bw/samp_rate
samp_delay
0
atten
100
taps
type
ccf
sdrplay_rsp2_source
agc_enabled
False
antenna
'A'
bw
400
alias
comment
affinity
dc_offset_mode
True
debug_enabled
False
device_serial
'0'
_enabled
True
_coordinate
(144, 196)
_rotation
0
id
sdrplay_rsp2_source_0
if_atten_db
30
ifType
0
iq_balance_mode
True
lna_atten_step
3
lo_mode
1
maxoutbuf
0
minoutbuf
0
rf_freq
869.0e6
sample_rate
samp_rate
wxgui_fftsink2
avg_alpha
0
average
False
baseband_freq
0
alias
comment
affinity
_enabled
True
fft_size
1024
freqvar
None
_coordinate
(1000, 116)
_rotation
0
grid_pos
id
wxgui_fftsink2_0
notebook
peak_hold
False
ref_level
0
ref_scale
2.0
fft_rate
15
samp_rate
samp_rate
title
FFT Plot
type
complex
win_size
win
None
y_divs
10
y_per_div
10
blocks_rotator_cc_0
pfb_arb_resampler_xxx_0
0
0
blocks_rotator_cc_0
wxgui_fftsink2_0
0
0
lora_decode_0
blocks_socket_pdu_0
out
pdus
lora_demod_0
lora_decode_0
out
in
pfb_arb_resampler_xxx_0
lora_demod_0
0
0
sdrplay_rsp2_source_0
blocks_rotator_cc_0
0
0
Mon Jun 3 09:39:45 2019
options
author
window_size
category
[GRC Hier Blocks]
comment
description
_enabled
True
_coordinate
(8, 8)
_rotation
0
generate_options
wx_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
(184, 12)
_rotation
0
id
samp_rate
value
1000000
lora_lora_receiver
bandwidth
125000
alias
crc
True
center_freq
869e6
channel_list
[869.1e6]
cr
4
comment
conj
False
affinity
decimation
1
disable_channelization
False
disable_drift_correction
False
_enabled
True
_coordinate
(456, 332)
_rotation
0
id
lora_lora_receiver_0
implicit
False
maxoutbuf
0
minoutbuf
0
reduced_rate
False
samp_rate
1e6
sf
8
lora_message_socket_sink
alias
comment
affinity
_enabled
True
_coordinate
(696, 364)
_rotation
0
id
lora_message_socket_sink_0
ip
127.0.0.1
layer
1
port
40868
sdrplay_rsp2_source
agc_enabled
False
antenna
'A'
bw
400
alias
comment
affinity
dc_offset_mode
True
debug_enabled
False
device_serial
'0'
_enabled
True
_coordinate
(72, 148)
_rotation
0
id
sdrplay_rsp2_source_0
if_atten_db
30
ifType
0
iq_balance_mode
True
lna_atten_step
3
lo_mode
1
maxoutbuf
0
minoutbuf
0
rf_freq
869.0e6
sample_rate
samp_rate
wxgui_fftsink2
avg_alpha
0
average
True
baseband_freq
0
alias
comment
affinity
_enabled
True
fft_size
1024
freqvar
None
_coordinate
(688, 108)
_rotation
0
grid_pos
id
wxgui_fftsink2_0
notebook
peak_hold
True
ref_level
0
ref_scale
2.0
fft_rate
15
samp_rate
samp_rate
title
FFT Plot
type
complex
win_size
win
None
y_divs
10
y_per_div
10
lora_lora_receiver_0
lora_message_socket_sink_0
frames
in
sdrplay_rsp2_source_0
lora_lora_receiver_0
0
0
sdrplay_rsp2_source_0
wxgui_fftsink2_0
0
0
Всем удачных экспериментов.