Julia で LimeSDR を制御するまでの道のり

この記事のまとめ:
  • LimeSDR 用の C++ のサンプルコードを動かします
  • Julia で ccall を使ったサンプルコードとして RTLSDR.jl を動かします
  • Julia で Lime Suite の C/C++ 用のライブラリー関数を ccall で呼び出します
  • Clang.jl を使って Lime Suite の C/C++ 用のライブラリー関数のラッパー関数を自動生成し、パッケージ化します
  • 自作のラッパー関数のパッケージを読み込み LimeSDR mini で信号受信のサンプルコードを動かします
背景

普段仕事で MATLAB を使うことがあるのですが、処理の遅さやライブスクリプトの使い勝手の悪さが以前から気になっていました。MATLAB の代替言語と考えたときに数年前から Julia 言語が気になっておりまして、ようやく重い腰を上げて、まずはプライベートで Julia を本格的に学び始めているところです。 それと同時に、せっかく LimeSDR mini を(だいぶ前に)購入したのに、これまでは誰かが書いたコードを動かすだけでした。SDR (Software Defined Radio) なのに自分でプログラミングできないのはもったいないかと思い、自分である程度使えるようになっておこうと思ったところでした。

そういった背景があり、Julia 言語の勉強と SDR の勉強を兼ねて、LimeSDR の C 言語用 API の Julia 用のラッパーを作ってみました。

正直、Julia は初心者ですし、C 言語のポインターも高校から大学のときに覚えたくらいの知識ですし、LimeSDR でプログラミングの経験もないので間違いなどあればご指摘いただけると幸いです。

はじめに

次の5ステップで LimeSDR mini を Julia でプログラミングしていきます。

  • Step 1: C++ で LimeSDR 用のライブラリー関数を呼び出す
  • Step 2: Julia で C ライブラリー関数を呼び出す
  • Step 3: Julia で LimeSDR 用のライブラリー関数を呼び出す
  • Step 4: Clang.jl で LimeSDR 用のライブラリー関数の Julia 用のラッパー関数を自動生成し、パッケージ化する
  • Step 5: Julia で LimeSDR 用ラッパー関数を使用して LimeSDR を制御する

ここで紹介するサンプルコードはすべて次のリポジトリーにあります。 サンプルコードはすべて Linux 上で確認しています。 Step 1 - 5 をそれぞれ異なる Docker コンテナーを立ち上げ、動かしています。 コンテナーの立ち上げには docker-compose を利用しています。 Docker 関連の操作方法の説明は、本記事では十分にはしませんので、ご了承ください。

Step 1: C++ で LimeSDR 用のライブラリー関数を呼び出す

まず Step 1 として、LimeSDR mini を使って自分でコーディングする感覚をつかんでみます。

LimeSDR を制御するためには、Myriad RF から提供されている Lime Suite を使います。 Lime Suite には LimeSDR 用のドライバーや C 言語と C++ 用のライブラリー関数 (API) などが含まれています。 まずはこの API を C++ でサンプルコードを使って動かしてみます。

C 言語や C++ で書かれたサンプルコードを探してみると次のようなものがありました。

  1. Myriad RF のディスカッションで共有されていたサンプルコード
  2. csete/limerx.c
  3. LMS API - Quick start guide - (Myriad RF のページでは出てこないのが不思議…)

ここでは細かい動作を確認するつもりはありませんので、一番シンプルな 1. のサンプルコードを実行してみます。 なお、このサンプルコードは送信機用であるため、実行すると電波発射をします。 そのまま空間に電波発射すると電波法違反になりますので適切な環境を用意して実行してください。

サンプルコードを実行するために必要なパッケージは Lime Suite と g++ くらいです。 それぞれ Ubuntu であれば apt パッケージマネージャーでインストールできます。

サンプルコードを /apps/lime_test.cpp としたとき、コンパイルは次のようにできます。 ここでは、オプションとして -lLimeSuite で Lime Suite を読み出していることがポイントです。

$ g++ -pthread /apps/lime_test.cpp -lLimeSuite -o /apps/lime_test.bin

なお、このサンプルコードでは、送信ストリームの処理にスレッドを使っているため -pthread オプションを付けています。

生成されたバイナリーファイルを次のように実行すれば、LimeSDR が電波発射します。

$ /apps/lime_test.bin

そのままでは動いているのかわからないので、RTL-SDR に有線接続してスペクトルを確認してみました。 どうやら指定した周波数で何かを送信していることは確認できたので、とりあえず C++ でのサンプルコードの実行は完了です。

Step 2: Julia で C ライブラリー関数を呼び出す

続いて Step 2 として、Julia で C 言語用のライブラリー関数を呼び出すコードを見てみます。

Julia で C ライブラリー関数を呼び出すのは比較的簡単だといわれています。 ただ、私はその経験がないのでその感覚からつかんでみようと思います。 C ライブラリー関数を呼び出す良いサンプルがないか探してみたところ、Julia 用の RTL-SDR ラッパーがあったのでそれを試してみます。

Julia を簡単にテストするには Jupyter Notebook を使うのが便利です。 以前、Docker で Julia が使える Jupyter Notebook コンテナーの立ち上げ方を紹介しました。

基本的には以後のステップでもこの jupyter/datascience-notebook をベースとして Dokerfiledocker-compose.yml 編集してステップごとの用途に合わせて使っていきます。

RTL-SDR を使える Julia 環境の準備

まずは、Jupyter Notebook が使える Docker イメージに RTLSDR.jl を使える環境を整えます。 Dockerfile 内で RTL-SDR 用のライブラリーのインストールを記述し、packages.jlRTLSDR.jl のインストールする記述を追記します。

また、この Docker イメージを立ち上げる際、RTL-SDR の USB ドングルをコンテナー内からアクセスできるように docker-compose.ymldevices のオプションを追加します。

そして、この docker-compose.yml で Docker コンテナーを立ち上げると RTLSDR.jl を使える Jupyter が立ち上がります。 なお、 Docker コンテナーを立ち上げる際は、必ず PC に RTL-SDR を接続をしてから立ち上げるようにします。

RTLSDR.jl の動作確認

ブラウザーで Jupyter Notebook を開きます。

まずは RTL-SDR 用のライブラリーである libstlsdr が読み込めるか確認します。 Libdl モジュールは共有ライブラリーを読み込むためのモジュールです。これを使って libstlsdr が読めるか確認します。

using Libdl
find_library("librtlsdr")

次のように表示されれば、ライブラリーが読み込めています。

"librtlsdr"

早速、RTL-SDR を使ってスペクトルを確認してみます。帯域幅が 2 MHz で、中心周波数が 88.5 MHz の設定だと次のとおりです。

using RTLSDR
 
r = RtlSdr()
 
set_rate(r, 2.0e6)
set_freq(r, 88.5e6)                # if we wanted the center freq on NPR
 
samples = read_samples(r, 1024)
 
# plot power spectral density
using PyPlot
psd(samples)
 
close(r)

うまく動作すればこのようにグラフが表示されます。

ccall の使い方を確認する

RTLSDR.jl がどのように C のライブラリー関数を呼び出しているか確認するためにソースコードをのぞいてみます。

ソースコードはこちらにあり、 RTLSDR.jlc_interface.jl の2つのファイルで構成されています。c_interface.jl で C 言語用に用意された librtlsdrccall メソッドで呼び出す Julia 用の C ラッパーの関数群です。RTLSDR.jl がそれらの関数を利用して使いやすい関数として定義しているようです。

たとえば、RTLSDR のデバイスポインターを取得する関数がこちら。

# returns a pointer to 
function rtlsdr_open()
    index = 0
    rd = Array{Ptr{rtlsdr_dev},1}(undef,1)
    ret = ccall( (:rtlsdr_open, "librtlsdr"),
                 Cint,
                 (Ptr{Ptr{rtlsdr_dev}}, UInt32),
                 rd,
                 index
               )
 
    if ret != 0; throw(RTLSDRError("RTLSDR.jl reports: Error opening device.")); end
 
    rf = rd[1]
 
    # resetting buffers is critical to reading bytes
    rtlsdr_reset_buffer(rf)
 
    return rf
end

ccall で librtlsdr の rtlsdr_open の関数を呼び出していることがわかります。 ccall の用法は公式マニュアルを読み込む必要がありますがこんな感じで C ライブラリーを呼び出せることがわかりました。

Step 3: Julia で LimeSDR 用のライブラリー関数を呼び出す

ここまでで、Lime Suite の使い方と、Julia で C ライブラリーの呼び出し方がわかってきました。 ここからは、本来やりたいことであった Julia で Lime Suite の C/C++ 用 API を呼び出していきます。

Step 3 では、もっともシンプルな方法で Julia で Lime Suite の C/C++ 用 API を呼び出します。

Lime Suite を使える Julia 環境の準備

まずは、Docker で Julia と Lime Suite を使える環境を用意します。

Step 2 と同様にjupyter/datascience-notebook をベースに Dockerfile 内で Lime Suite をインストールします。

さらに Step 2 と同様に Docker コンテナーを立ち上げる際にコンテナー内から USB で接続された LimeSDR mini にアクセスできるように docker-compose.yml 内に devices オプションを追加します。

準備ができたら docker-compose コマンドでコンテナーを立ち上げます。

ccall を使って API を呼び出す

まずは、RTL-SDR のときと同様に Lime Suite のライブラリーが正しく読み込めるか確認します。

using Libdl
find_library("libLimeSuite")

"libLimeSuite" と表示されればライブラリーが正しくインストールされていることがわかります。

それでは LimeSDR の API を呼び出していきます。

Step 1 で最初に呼び出す API でもある LMS_GetDeviceList を呼び出すラッパーを作ってみます。

マニュアルを見るとこの API の型は次のように定義されています。

int LMS_GetDeviceList(lms_info_str_t * dev_list)

lms_info_str_t 型は char (8 ビット) 型の 256 要素の配列で、そのポインターを渡すということは配列の配列を扱うことを意味しています。 公式マニュアルによると Julia で C に引き渡す変数を定義する際、C のサイズが決まっている配列に相当する Julia の型は NTuple として定義するようです。 また、その配列の配列のポインターを渡すために Array 型で定義します。Array 型を ccall で引き渡すと自動的にポインターを示す Ptr{T} の型に変換してくれるようです。

これらを記述すると次のとおりです。

dev_list = Array{NTuple{256, UInt8}}(undef,1)
 
function LMS_GetDeviceList(dev_list)
    ccall((:LMS_GetDeviceList, "libLimeSuite"), Cint, (Ptr{NTuple{256, UInt8}},), dev_list)
end

それでは実行してみます。

dev_list = Array{NTuple{256, UInt8}}(undef,1)
num_dev = LMS_GetDeviceList(dev_list)
println(String(collect(dev_list[1])))

num_dev には利用可能なデバイス数が入ります。 dev_list も値が更新され、最初のデバイスを指す dev_list[1] を文字列として表示させています。 正しく実行できていれば、次のように表示されます。

これでようやく自分で書いた Julia のコードで LimeSDR を制御できてきました!

なお、この実行時に次のような USB へのアクセス権に関するエラーが出る場合があります。

libusb: error [_get_usbfs_fd] libusb couldn't open USB device /dev/bus/usb/004/006: Permission denied
libusb: error [_get_usbfs_fd] libusb requires write access to USB device nodes.

これは、Jupyter 用の Docker コンテナー内では、root ユーザーではなく jovyan ユーザーでコマンド実行をしているためです。 そのため、あらかじめホスト側で対象のデバイスに対してアクセス権を与えるようにしてください。

Step 4: Clang.jl で LimeSDR 用のライブラリー関数の Julia 用のラッパー関数を自動生成し、パッケージ化する

Step 3 で時間のかかるタスクの1つは、API を記述した C ヘッダーファイルから ccall でラップする Julia 関数を作成することです。 Clang.jl を使うと、この作業を自動化できます。 Clang.jl モジュールには、C ヘッダーファイルから Julia ラッパーを作成するジェネレーターが含まれています。 現在、以下の宣言がサポートされています。

  • function: Julia に翻訳された ccall (va_list と vararg 引数はサポートされていません)
  • struct: Julia に翻訳された構造体
  • enum: CEnum と訳されます
  • union: Julia 構造体に翻訳されています
  • typedef: Julia に翻訳された typealias をもとにした固有型
  • marco: 限定的なサポート (src/wrap_c.jl を参照)

Step 4 では、Clang.jl を使って Lime Suite のラッパーモジュールを含むパッケージを作ります。

なお、Clang.jl のドキュメントは多くないですが公式ドキュメントや次のサイトを参考すれば、概要はわかるでしょう。

Clang.jlを使うにあたってリファレンスコードになるようなプロジェクトを探したのですが、私が探した限りでは、Clang.jl の古いハイレベル API を使ったコードばかりで最新の Clang.jlを使った実装は見つかりませんでした(たとえば、SCIP.jl )。 こちらの issue にあるとおり、Clang.initClang.run といった関数で実装を行っているものは古い Clang.jl の実装を利用しています。 現状、動作に問題はありませんでしたが、将来的に廃止される可能性もありますので、最新の実装を行ったほうがよいと思います。

Clang.jl を使える Julia 環境の準備

Julia の実行環境として、引き続き jupyter/datascience-notebook をベースにしますが、今回は Jupyter は使わず REPL を主に使います。

まずは、Clang.jl をインストールするために、Package.jlPkg.add("Clang") と追加してあります。

また、Clang を実行する際、stddef.h がないというエラーが出たため、 Dockerfile に次の部分を追加しています。

# Install Clang
RUN apt-get install -yq clang llvm
ENV C_INCLUDE_PATH /usr/lib/llvm-6.0/lib/clang/6.0.0/include

準備ができたら docker-compose コマンドでコンテナーを立ち上げます。

Clang.jl を使うときのパッケージ構成

μβ の記事にも書いてありますが、Clang.jl でパッケージ化する場合、ディレクトリー構成として次のように gen ディレクトリーに Clang.jl の実行コードを保存します。 今回は、パッケージのビルドや、テストコードは用意しませんので、それ以外のものを作っていきます。

$ tree
.
└── LimeSuite
    ├── Project.toml
    ├── README.md
    ├── deps
    ├── gen
    ├── src
    └── test
Clang.jl 用プロジェクトの作成

パッケージの初期化は Julia の REPL からパッケージ管理モードに入り、次のように generate コマンドで作成できます。

$ julia
pkg > generate LimeSuite

コマンドを実行すると次のようなファイル類が作成されます。

$ tree
.
└── LimeSuite
    ├── Manifest.toml
    ├── Project.toml
    └── src
        └── LimeSuite.jl

ここから gen ディレクトリーを作成し、そのディレクトリーで Clang.jl 用の作業をしていきます。 Clang.jl は LimeSuite 用のパッケージを動かすためには不要なため、gen ディレクトリー内だけで Clang.jl を動かすだけのためのプロジェクトを作成します。

gen ディレクトリーでパッケージ管理モードに入り、次のように打てばカレントディレクトリーでプロジェクトを作れます。

pkg> activate .

Project.toml が生成されますので、ファイルを開き、プロジェクト名として次のように記述します。

name = "gen"

Julia の REPL を立ち上げる際、次のようにオプションを付けるとカレントディレクトリーの Project.toml を読み込んで Julia の REPL を立ち上げてくれます。

julia --project

Project.toml には依存モジュールなどの情報が記載されます。 たとえば、gen プロジェクトで次のように Clang.jl をモジュールを読み込むと、自動的に Project.toml に依存モジュールとして書き込まれ、管理されます。

(gen) pkg> add Clang

次回以降 gen プロジェクトを読み込むと依存モジュールを自動的に読み込んでくれます。

Clang.jl を使ったラッパーの自動生成

あとは Clang.jl を使ったラッパー生成のコードを作成します。 リファレンスとして、上記で紹介した公式ドキュメントのサンプルコードをほとんどそのまま使います。

コードは generate_wrapper.jl としてここにあるとおりですが、Lime Suite のヘッダーファイル群のパスを変更したり、ファイル名やパスを変更したくらいでほぼサンプルコードのままです。 なお、複合型 (struct) は C の構造体のように使うため、mutable として定義するためのオプションを次のように設定しています。

ctx.options["is_struct_mutable"] = true

REPL でこのコードを実行すれば、src/wrapper ディレクトリーにラッパー関数が自動生成されます。 なお、REPL でソースコードをのファイルを実行する場合は、次のように include を使えばよいです。

julia> include("generate_wrapper.jl")

これで Clang.jl でパースできた構文については自動的に Julia 用のラッパー関数のファイルを作成してくれます。生成されるファイルは2つあり、関数定義ファイルである libLimeSuite_api.jl と変数や型の定義ファイルである common.jl です。 ただし、次のようなものは生成してくれませんでした。

  • タグなし enum 型
  • enum 型を含む構造体
  • static 定数

これらについては、手動で記述が必要です。 今回は manual_common.jl として定義しています。 なお、C の enum 型に対応する型を扱う場合は、CEnum.jl モジュールを読み込む必要があります。

パッケージを完成させる

パッケージを初期化した際に生成されるパッケージ名の Julia ファイル(今回の場合は LimeSuite.jl)にパッケージとして使用するファイルを記述します。

Step 5: Julia で LimeSDR 用ラッパー関数を使用して LimeSDR を制御する

Step 4 で作成した LimeSuite.jl パッケージは GitHub で公開しています。

これをモジュールとして追加して、LimeSDR のテストを行います。

LimeSuite.jl を使える Julia 環境の準備

Docker イメージのベースはほかのステップと同様 jupyter/datascience-notebook を使います。 Packege.jl に次のように記述して、step 4 で作成したパッケージをインストールした Docker イメージを作成します。

Pkg.add(PackageSpec(url="https://github.com/hassiweb/LimeSuite.jl", rev="master"))

自分が作ったパッケージをこうも手軽に公開して利用できるって本当に便利ですね。

LimeSuite.jl の動作確認

Step 1 で紹介した LMS API - Quick start guide - に記載されているテスト用の 8-PSK 信号を受信するコードを Jupyter Notebook で書いていきます。

ただ、ひとつひとつ説明すると長くなるので、書いたコードはこちらに載せておきます。

最終的にはこのように PyPlot を使って表示できました。

Julia での C 用のポインターの扱いが難解で苦労したとことが多々あり、これはこれでいつか記事にまとめようと思います。


だいーーぶ長くなりましたが、今回は以上です。 最後まで読んでいただき、ありがとうございます。
関連記事



コメント

このブログの人気の投稿

LinuxでのnVidia GPUのオーバークロック・電力チューニング方法