panda's tech note

Advent Calendar 2020: ソフトウェア無線

注意LimeSDR などのソフトウェア無線は技術基準適合証明(通称,技適)を受けていないため,これらの機器を日本国内で無線機として利用することは電波法により禁じられています。無線機として使用するためには,実験試験局免許を取得するか電波暗室・シールドボックスなどの設備を使用する必要があります。または,アンテナの代わりにケーブルとアッテネータを用い有線接続をし電波を発しないようにすることで,無線通信ではなく有線通信とはなりますが実験することができます(この場合も電波が漏れないように注意してください)。このページを参照される方は,実験される国や地域の法令などを遵守するようにご注意ください。また,実験等はご自身の責任でお願いします。

Day 4: BPSKによる通信(送信)

今日からの数日間で最も単純な2相の位相偏移変調であるBPSKの送受信を実装していきます。そのため,今日はまずディジタル信号をBPSKで変調し,送信するところを実装します。BPSKの送信は,

  1. SDRデバイスの初期化
  2. 送信機・アンテナの設定(搬送波周波数・帯域幅・ゲイン・サンプルレート)
  3. 送信ストリームの有効化
  4. 送信データの変調・信号サンプルの構築
  5. 送信

という流れで行います。

2日目に説明したように,I-Qダイアグラムを複素平面として扱うことでシンボルを複素数で表すことができます。そのため,ここからはPythonで複素数を簡単に扱うためにNumPyを使います。また,送信機・アンテナの設定パラメータを引数で設定できるように argparse を使います。プログラムの先頭は以下の通りです。main() 関数に実装していきます。

import SoapySDR
import sys
import argparse
import math
import numpy as np

# Arguments
parser = argparse.ArgumentParser()
parser.add_argument('--sample-rate', type=int, default=1e6)
parser.add_argument('--samples-per-symbol', type=int, default=10)
parser.add_argument('--bandwidth', type=int, default=5e6)
parser.add_argument('--rf', type=int, default=2420e6)

"""
Call the main routine
"""
if __name__ == "__main__":
    # Parse the arguments
    args = parser.parse_args()
    main(args)

SDRデバイスの初期化

3日目で扱ったように,まずはSDRデバイスを初期化し,インスタンスを取得します。

"""
Main routine
"""
def main(args):
    # Create an SDR device instance
    try:
        sdr = SoapySDR.Device(dict(driver="lime"))
    except:
        sys.stderr.write("Failed to create an SDR device instance.\n")
        return False

    if not sdr:
        sys.stderr.write("Could not find any SDR devices.\n")
        return False

送信機・アンテナの設定

次に送信機・アンテナの設定をします。サンプルレート,帯域幅,アンテナ,ゲイン,搬送波周波数を設定します。このうち,サンプルレート,帯域幅,搬送波周波数は引数で変更でき,そのパラメータを argparse を通じて取得しています。

    sdr.setSampleRate(SoapySDR.SOAPY_SDR_TX, 0, args.sample_rate)
    sdr.setBandwidth(SoapySDR.SOAPY_SDR_TX, 0, args.bandwidth)
    sdr.setAntenna(SoapySDR.SOAPY_SDR_TX, 0, "BAND1")
    sdr.setGain(SoapySDR.SOAPY_SDR_TX, 0, 50.0)
    sdr.setFrequency(SoapySDR.SOAPY_SDR_TX, 0, args.rf)

setSampleRate() では搬送波に乗せる信号サンプルのレート (samples per second) を指定します。信号処理を習った方は,シャノン・染谷の定理でサンプリングレートは搬送波周波数の2倍以上にする必要があると感じられるかもしれませんが,BPSKではディジタル信号を位相により変調し搬送波に乗せて送受信するため,搬送波の信号・周波数成分を完全に復元する必要はありません。そのため,サンプルレートはシンボルレートを基準に考えます。送信機と受信機の間で基準信号が同期していない場合,受信時に位相が変化する瞬間の信号をサンプルしてしまう可能性があるためシンボルレートの2倍以上のサンプルが必要となりますが,詳しくは受信の際に説明します。

今回はデフォルトのサンプルレート値として1 Msps (Mega-samples per second)とし,シンボルあたり10サンプルとしています。送信については,1シンボルあたり1サンプルで送信することも可能ですが,サンプル間の振幅および位相が滑らかに補間されるため,復調性能に影響します。

例えば,01010101... をBPSKで変調し,1シンボルあたり10サンプルで送信する場合,

10 samples per symbol

のようになりますが,1シンボルあたり1サンプルで送信する場合,

1 sample per symbol

のようにサンプルする点によっては振幅・位相が正しく復調できなくなります。そのため,今回は1シンボルあたり10サンプルを使用します。

setBandwidth() では送信アンテナのバンドパスフィルタの帯域幅 (bandwidth) を指定しています。この帯域幅は,変調により現れる搬送波周波数の周囲(サイドバンド)のスペクトラムの幅を意味します。

setAntenna() では送信に使用するアンテナを指定しています。今回は TX1_1 を使うので BAND1 を指定しています。送信アンテナは LimeSDR Quick Start によると,全周波数に対応していますが,TX1_1 (BAND1) と TX1_2 (BAND2) で若干特性が異なるようです。

setGain() ではゲインを設定します。実験に使った環境ではアッテネータを入れいているので,今回は(強めの) 50 dB を使いました。

setFrequency() では搬送波の中心周波数を指定しています。今回は2.42 GHzを指定しています。周波数はどこでもよかったのですが,手持ちの2.4 GHz帯のスペアナで電波が漏れていないかを念のため確認するためにこの周波数にしました。

送信ストリームの有効化

次に送信アンテナから信号サンプルを出力する送信ストリームを設定します。今回は1つのアンテナ(シングルチャネル)のみを使うため,以下のようにシングルチャネルのストリームを準備しています。

    # Setup a transmit stream
    txStream = sdr.setupStream(SoapySDR.SOAPY_SDR_TX, SoapySDR.SOAPY_SDR_CF32, [0])
    # Activate the stream
    sdr.activateStream(txStream)

setupStream() で送信ストリームを作成しています。第2引数は信号サンプルの表現形式です。今回はI-Q信号を指定するため,32ビット複素数であるCF32(CF: Complex Float)を指定しています。第3引数はチャネルのリストですが,今回はシングルチャネルなので0を指定しています。送信ストリームを作成したら activateStream() でストリームを有効化します。

送信データの変調・信号サンプルの構築

ストリームの準備が終わったら送信データの変調を行い,信号サンプルを作成します。

BPSKでは基準信号に対する位相差により変調を行うため,送信機と受信機で基準信号が完全に同期されている必要があります。基準信号を同期して検波(復調)を行う方式を同期検波と呼びますが,基準信号の位相を送信機と受信機で同期するにはパイロットキャリアと呼ばれる基準信号などを使用する必要があります。今回は,基準信号の位相を同期せずに,ユニークワードと呼ばれる情報を埋め込むことで,位相のずれを検出・補正する方法を用います。このような方式は準同期検波とも呼ばれます。また,プリアンブルと呼ばれる信号(2値の場合,010101...のように0と1が交互に続くものがよく使われます)により,信号の開始とシンボルのエッジを判定します。

このため,送信機からはプリアンブルとユニークワードに続き,データを送信します。ここではプリアンブルを 0101010101010111,ユニークワードを 0010101111010100 として,その後ろにデータを送信する。本来はユニークワードの後ろにはフレーム長などのプロトコルヘッダが付与されるが,今回は簡単のためにデータを直接送るだけにします。

プリアンブル,ユニークワード,データは以下の通りとします。

PREAMBLE = [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1]
SYNC = [0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0] # Unique word
DATA = [0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1]

BPSKでは,1シンボルで1ビットを送信し,送信ストリームのサンプルレートはシンボルレートの args.samples_per_symbol 倍に設定しているため,これらをつなぎ合わせ,各ビットを10回繰り返すものを data 変数に格納し,信号サンプルに変換して送信します。送信ストリームの最大送信単位(MTU: Maximum Transmit Unit)よりも十分小さくなると考えられますが,以下のコードでは念のためサイズの確認を行っています。

    # Data to send
    data = []
    for d in PREAMBLE + SYNC + DATA:
        data += [d] * args.samples_per_symbol

    # Check the maximum transmit unit
    mtu = sdr.getStreamMTU(txStream)
    if mtu < len(data):
        sys.stderr.write("MTU is too small {} < {}\n".format(mtu, len(data)))
        return False

信号サンプルは,各ビット \(b\) に対して \( \exp( \pi i b ) \) とすることで得られます。単純に,ビットが 0 であれば 1 + 0i1 であれば -1 + 0i ということなので,別の表現でも表すことができますが,今回は複素指数関数(オイラーの公式)で,以下のコードように NumPy を用いることで簡単に信号サンプルを得ることができます。

    # Build preamble, sync code (unique word) and data
    samples = np.exp( 1j * math.pi * np.array(data, np.complex64) ).astype(np.complex64)

上記の信号を送るのですが,この信号をいきなり送っても受信機の周波数が同期されておらず検波(復調)が正しくできないため,位相が大きくずれます。受信機のPhase Locked Loop (PLL)と呼ばれる同期回路を動作させるために以下のように搬送波周波数の正弦波(ビット0の連続信号)を生成して,データの信号サンプルの前後に付与します。

    # Build 0...0 (1 + 0j)s
    base = np.ones(mtu, np.complex64)

送信

上記で得た正弦波とデータの信号サンプル base および sampleswriteStream() により送信することができます。返り値の .ret には正常に送信できたサンプル数またはエラーコードが格納されるので,指定した信号サンプル数分送信できたかを確認しています。

    # Transmit the base signal (to warm up)
    for i in range(5000):
        status = sdr.writeStream(txStream, [base], base.size, timeoutUs=1000000)
        if status.ret != samples.size:
            sys.stderr.write("Failed to transmit all samples in writeStream(): {}\n".format(status.ret))
            return False

    # Transmit the samples
    print("Sending data...")
    status = sdr.writeStream(txStream, [samples], samples.size, SoapySDR.SOAPY_SDR_END_BURST, timeoutUs=1000000)
    if status.ret != samples.size:
        sys.stderr.write("Failed to transmit all samples in writeStream(): {}\n".format(status.ret))
        return False

    # Transmit the base signal
    for i in range(5000):
        status = sdr.writeStream(txStream, [base], base.size, timeoutUs=1000000)
        if status.ret != samples.size:
            sys.stderr.write("Failed to transmit all samples in writeStream(): {}\n".format(status.ret))
            return False

送信が完了したら以下のコードでストリームを閉じます。

    # Deactivate and close the stream
    sdr.deactivateStream(txStream)
    sdr.closeStream(txStream)

今日のまとめと明日の予定

今日はBPSKでの通信を行うために信号サンプルの送信を実装しました。コードは bpsk_tx.py に置きました。明日はこの信号の受信部分を実装します。