はじめに
セットアップ編 で、Jetson Nano でソフトウェア無線(SDR: Software Defined Radio)を利用するための環境(cuSignal + SoapySDR)構築について紹介しました。受信機には RTL-SDR BLOG V.3 を使います。今回の動作確認編では、構築した環境上で、FM 放送の受信を例に、動作確認してみます。
cuSignal に付属する Jupyter ノートブック
cuSignal には、その使い方を示したいくつかの Jupyter ノートブックが付属し、ソフトウェア無線関連のノートブックもあります。
ノートブック | 内容 | Jetson Nano で動作 |
---|---|---|
online_signal_processing_tools.ipynb | cuSignal をソフトウェア無線で利用する際の基本的なサンプル | |
sdr_integration.ipynb | rtlsdr を利用した FM 放送受信サンプル | 部分的(設定を変更すれば動作) |
sdr_wfm_demod.ipynb | SoapySDR を利用した FM 放送受信サンプル |
動作確認を解説
cuSignal 付属のノートブックもとても役に立ちましたが、自分自身の理解のためにノートブックを作成したので、それを以下に解説します。なお、作成したノートブックは Gist で公開 しています。
ライブラリのインポート
import SoapySDR
from SoapySDR import * #SOAPY_SDR_ constants
import matplotlib.pyplot as plt
import numpy
import importlib
sample_rate は RTL-SDR がデータをサンプルするサンプリングレートです。FM 放送音声のサンプリングレートである audio_fs との違いに注意。復調後にダウンサンプリングするための比率を resample_factor で定義しています。
sample_rate = int(2.4e6)
audio_fs = int(48e3)
resample_factor = sample_rate // audio_fs
read_elements = 1024
channel = 0
fm_freq は FM 放送局の周波数です。ここでは、80.0MHz の TOKYO FM を設定しています。
fm_freq = 80.0e6 # FM Station Frequency
gain = 40
sample_time = 1 # Sampling time in sec
メモリ消費は大きいですが、動作が分かりやすいので、まず、受信データを sample_time 秒間蓄えてから、変調します。total_elements はそのサンプル数です。SoapySDR の readStream 関数は(私のやり方が間違っている可能性もありますが)一度に多くのサンプルを読み出すと動作が不安定だったので、複数回に分けて読み出し、num_reads がその回数。
total_elements = int(sample_rate * sample_time)
num_reads = total_elements // read_elements
受信デバイス(ここでは RTL-SDR)の初期設定を行います。このページ を参考にしました。
#enumerate devices
results = SoapySDR.Device.enumerate()
for result in results: print(result)
#create device instance
#args can be user defined or from the enumeration result
args = dict(driver="rtlsdr")
sdr = SoapySDR.Device(args)
#query device info
print(sdr.listAntennas(SOAPY_SDR_RX, channel))
print(sdr.listGains(SOAPY_SDR_RX, channel))
freqs = sdr.getFrequencyRange(SOAPY_SDR_RX, channel)
for freqRange in freqs: print(freqRange)
#apply settings
sdr.setSampleRate(SOAPY_SDR_RX, channel, sample_rate)
sdr.setFrequency(SOAPY_SDR_RX, channel, fm_freq)
sdr.setGain(SOAPY_SDR_RX, channel, gain)
{driver=rtlsdr, label=Generic RTL2832U OEM :: 00000001, manufacturer=Realtek, product=RTL2838UHIDIR, serial=00000001, tuner=Rafael Micro R820T}
('RX',)
('TUNER',)
2.3999e+07, 1.764e+09
受信データ読み込み用のバッファ・メモリを用意します。
buff_len = total_elements
cpu_buff = numpy.array([0] * buff_len, numpy.complex64)
print('buff_len={}'.format(buff_len))
buff_len=2400000
受信機からデータを読み込みます。先ほど述べたように、readStream 関数で一度に多くのデータを読み出すと動作が不安定だったため、複数回に分けて読み出しています。最初の読み出しに失敗することもあったので、空読みしてから、本当の読み出しを行っています。
#setup a stream (complex floats)
rxStream = sdr.setupStream(SOAPY_SDR_RX, SOAPY_SDR_CF32)
sdr.activateStream(rxStream) #start streaming
while(True):
sr = sdr.readStream(rxStream, [cpu_buff], 1024)
if sr.ret == 1024:
break
offset = 0
for i in range(num_reads):
sr = sdr.readStream(
rxStream, [cpu_buff[offset:offset + read_elements]], read_elements, timeoutUs=int(8e12))
if sr.ret != read_elements:
print('readStream error sr.ret={}'.format(sr.ret))
break
offset += read_elements
#shutdown the stream
sdr.deactivateStream(rxStream) #stop streaming
sdr.closeStream(rxStream)
パワースペクトラム密度を求める関数。sig を cusignal の別名と、scipy.signal の別名に切り替えることで、両方のライブラリでの実行に対応します。
def welch(buff, sample_rate):
return sig.welch(buff, sample_rate, nfft=1024, scaling='density', return_onesided=False)
FM 復調を行う関数。こちらも、cupy と numpy に対応。受信機から読み出せるデータは I-Q 表現と呼ばれる複素数形式で、np.angle 関数で位相角が求まり、np.unwrap 関数で位相角のシフトを求めます。np.diff 関数で微分します。次に、ダウンサンプリングで、受信機のサンプリングレートから、FM 放送の音声サンプリングレートへ変換。最後に正規化を行って完了です。
def demodulate(buff, resample_factor):
b = buff
b = np.diff(np.unwrap(np.angle(b)))
b = sig.resample_poly(b, 1, resample_factor, window='flattop')
b /= np.pi
return b
私は、説明できる程の知見がないので、FM 変調の仕組みはこの辺でご勘弁を。もう少し詳しく知りたい方は、Interface 2021年5月号 の特集「Pythonで無線信号処理」をお勧めします。
CuPy + cuSignal
まずは、CuPy と cuSignal で、FM 復調を実行します。
cupy の別名を np へ、cusignal の別名を sig に設定します。
np = importlib.import_module('cupy')
print(np.__name__)
sig = importlib.import_module('cusignal')
print(sig.__name__)
cupy
cusignal
GPU が利用できるメモリを割り当て、そこへ、先程、受信機から読み出したデータをコピーします。
gpu_buff = sig.get_shared_mem(buff_len, dtype=np.complex64)
gpu_buff[:] = cpu_buff
パワースペクトラム密度を計算。
f, Pxx_den = welch(gpu_buff, sample_rate)
計算結果をグラフ表示
plt.semilogy(np.asnumpy(np.fft.fftshift(f/1e4)), np.asnumpy(np.fft.fftshift(Pxx_den)))
plt.show()
なぜ中心周波数に凹みがあるのか?不明。私のコードに問題があるのかも知れません。
FM 復調を行います。
%%time
b = demodulate(gpu_buff, resample_factor)
処理時間は後の方でまとめてあります。
音声波形を表示。
b = np.asnumpy(b).astype(np.float32)
x = [i for i in range(len(b))]
plt.plot(x, b, label="test")
[<matplotlib.lines.Line2D at 0x7f311a5550>]
NumPy + SciPy Signal
次に、NumPy と SciPy Signal で、FM 復調を実行します。
numpy の別名を np へ、scipy.signal の別名を sig に設定します。
np = importlib.import_module('numpy')
print(np.__name__)
sig = importlib.import_module('scipy.signal')
print(sig.__name__)
numpy
scipy.signal
パワースペクトラム密度を計算。
f, Pxx_den = welch(cpu_buff, sample_rate)
計算結果をグラフ表示
plt.semilogy(np.fft.fftshift(f/1e4), np.fft.fftshift(Pxx_den))
plt.show()
データが同じなので、CuPy + cuSignal で計算した場合と同じ結果です。
FM 復調を行います。
%%time
b = demodulate(cpu_buff, resample_factor)
処理時間は後の方でまとめてあります。
音声波形を表示。
x = [i for i in range(len(b))]
plt.plot(x, b, label="test")
[<matplotlib.lines.Line2D at 0x7f311da5e0>]
こちらも、データが同じなので、CuPy + cuSignal で復調した場合と同じ結果です。
最後に音声データをファイルに保存します。
from scipy.io import wavfile
wavfile.write('demod.wav', rate=audio_fs, data=1e2*b)
処理時間
音声1秒間分のFM復調処理(10回計測した平均値)
動作電力モード:MAXN(sudo jetson_clocksも実行)
CPU Time User [msec] |
CPU Time Sys [msec] |
CPU Time Total [msec] |
Wall Time [msec] | |
---|---|---|---|---|
CuPy + cuSignal (GPU利用) |
13.6 | 2.4 | 16.0 | 126.0 |
NumPy + SciPy Signal (CPUのみ) |
530.0 | 86.8 | 616.0 | 619.0 |
最後に
前回の セットアップ編 と、今回の動作確認編で、Jetson Nano でソフトウェア無線を試す環境が整いました。しかし、まだ、FM 復調を試しただけで、ソフトウェア無線の目的である、ハードウェアを変更せずに、いろいろな通信方式に対応することには、ほぼ遠いです。Jetson Nano を使うからには、ディープラーニング推論も組み合わせて、面白い使い方を探ってみたいです。例えば、AI ノイズ除去など。私の勉強不足で、そこまで行くのは大変ですが、もし、何かできたら、続きの Qiita 記事を作成したいと思います。