GNURadio-Manual中文文档

个人整理的GNURadio中文文档以及学习笔记。GNURadio版本为3.7.10,https://www.gnuradio.org/doc/doxygen-v3.7.10.0/dir_05c3040209450f1dcdb347a8089b09d4.html

AN Zhenlin, 2020

个人主页:https://www4.comp.polyu.edu.hk/~cszan/

GNURadio源码手册

构建说明

依赖

全局依赖

Python

构建文档

grc: The GNU Radio Companion

gr-wavelet:小波变换

gsl (>= 1.10) http://gnuwin32.sourceforge.net/packages/gsl.htm

gr-qtgui: QT图形接口

qt4 (>= 4.4.0) http://qt.nokia.com/downloads/ qwt (>= 5.2.0) http://sourceforge.net/projects/qwt/ pyqt (>= 4.10.0) http://www.riverbankcomputing.co.uk/software/pyqt/download

gr-wxgui: WX图形接口

wxpython (>= 2.8) http://www.wxpython.org/ python-lxml (>= 1.3.6) http://lxml.de/

gr-audio: 语音子系统

audio-alsa (>= 0.9) http://www.alsa-project.org audio-jack (>= 0.8) http://jackaudio.org/ portaudio (>= 19) http://www.portaudio.com/ audio-oss (>= 1.0) http://www.opensound.com/oss.html audio-osx audio-windows

额外推荐的依赖

uhd: The Ettus USRP Hardware Driver Interface

gr-video-sdl: PAL and NTSC display

gr-comedi: Comedi hardware interface

gr-log: Logging Tools (Optional)

构建

GNURadio使用的是Cmake的构建系统,标准的构建过程是:

$ mkdir $(builddir)
$ cd $(builddir)
$ cmake [OPTIONS] $(srcdir)
$ make
$ make test
$ sudo make install

$(builddir)通常是$(srcdir)/build,是代码构建后的目录。

CMake的选项

CMake的选项用来指定那些不会被自动化找到的,需要依赖的库和include,(-DCMAKE_PREFIX_PATH)。或者,设置安装目录(-DCMAKE_INSTALL_PREFIX=(dir))。

GNURadio的几个例子

这些例子安装在$prefix/share/doc/gnuradio-$version/。

Dial Tone

使用两种模块

  • gr::analog::sig_source_f,生成 \(350Hz,440Hz\)
  • gr::audio::sink,将输出连接到语音系统,以采样率生成输出信号。audio sink可以设置两个输出。
sig_source_f (freq = 350) -->
                          audio.sink
sig_source_f (freq = 440) -->

dial_tone.py文件:

from gnuradio import gr
from gnuradio import audio
from gnuradio.eng_option import eng_option
from optparse import OptionParser

try:
    from gnuradio import analog
except ImportError:
    sys.stderr.write("Error: Program requires gr-analog.\n")
    sys.exit(1)

class my_top_block(gr.top_block):

    def __init__(self):
        gr.top_block.__init__(self)

        parser = OptionParser(option_class=eng_option)
        parser.add_option("-O", "--audio-output", type="string", default="",
                        help="pcm output device name.  E.g., hw:0,0 or /dev/dsp")
        parser.add_option("-r", "--sample-rate", type="eng_float", default=48000,
                        help="set sample rate to RATE (48000)")
        (options, args) = parser.parse_args()
        if len(args) != 0:
            parser.print_help()
            raise SystemExit, 1

        sample_rate = int(options.sample_rate)
        ampl = 0.1

        src0 = analog.sig_source_f(sample_rate, analog.GR_SIN_WAVE, 350, ampl)
        src1 = analog.sig_source_f(sample_rate, analog.GR_SIN_WAVE, 440, ampl)
        dst = audio.sink(sample_rate, options.audio_output)
        self.connect(src0, (dst, 0))
        self.connect(src1, (dst, 1))

if __name__ == '__main__':
    try:
        my_top_block().run()
    except KeyboardInterrupt:
        pass

FM Modulator

这个例子用GRC或者GRC和Python完成。我们用GRC生成一个FM信号,然后用GRC程序或者Python程序解码。

Modulator

首先用控制台命令 "gnuradio-companion"运行GRC。用图形接口创建我们的流程图。这里先不详细介绍GRC接口,仅仅利用GRC程序生成数据。 GRC运行之后,打开“fm_tx.grc”。流程图中,生成了电话音频率,求和,重采样,以便我们可以通过整数倍的上采样得到宽带FM信号,然后输出。

实际例子只是为了生成FM电话音信号,保存到文件中。不需要文件太大,只要能说明问题即可。

  • gr::blocks::head,模块限制进入文件的采样数量。一旦有了N个采样点,流程图就被终止了。
  • gr::blocks::skiphead忽略最初的M个采样点,避免滤波器的跳变和群延时。

运行程序,可以使用"Build->Execute",或者点击工具栏上的齿轮。程序运行一会儿,一旦控制台显示了"nitems"参数设置的item数量。 在一个"dummy.dat"文件中有FM的复数采样点。

Demodulator

GRC程序"fm_rx.grc",对应着python脚本,"fm_demod.py"。

  • gr::blocks::file_source,读取文件
  • gr::analog::quadrature_demod_cf,把文件复数的FM信号转换成浮点信号。

我们把200bps输入信号重采样到44.1kbps(语音速率)。因为这个重采样不能使用整数倍的抽取速率,所以我们使用了任意速率重采样器, gr::filter::pfb_arb_resampler_fff。将输出的信号以 \(44.1Khz\) 的采样率滤波到 \(15Khz\) 的带宽,然后利用 gr::audio::sink 输出。

Flowchart

操作流程图

GNURadio基本的结构就是flowchart。flowchart是有向无环图,有一个或者多个source(输入采样点),一个或者多个sin(输出采样点或者终止)。

每个程序必须至少创建一个'top_blcok'作为flowchart的顶层结构。这个结构提供了很多全局的方法,'start,' 'stop,' and 'wait'。

GNU Radio的应用创建gr_top_block来实例化blocks,连接模块,然后开始gr_top_block。下面给出一个FIR滤波器的例子。

from gnuradio import gr, blocks, filter, analog
class my_topblock(gr.top_block):
    def __init__(self):
        gr.top_block.__init__(self)
        amp = 1
        taps = filter.firdes.low_pass(1, 1, 0.1, 0.01)
        self.src = analog.noise_source_c(analog.GR_GAUSSIAN, amp)
        self.flt = filter.fir_filter_ccf(1, taps)
        self.snk = blocks.null_sink(gr.sizeof_gr_complex)
        self.connect(self.src, self.flt, self.snk)
if __name__ == "__main__":
    tb = my_topblock()
    tb.start()
    tb.wait()

'tb.start()'开始了数据流流过flowchart,'tb.wait()'等价于知道gr_top_block结束后等待线程'join'。 可以利用 'run' 方法来替换这两个方法的先后调用。

延迟和吞吐量

GNU Radio运行一个调度器来优化吞吐量。动态调度器使得成块的数据通过blocks从source流到sink。数据块的大小和信号处理的速度有关。 对于每个block,能够处理的数据量和输出buffer的空间和输入buffer中已经收到的数据量有关。

这样操作的结果就是,一个模块可能申请了很多数据来处理(规模可能到几千个采样点)。 从速度的角度来看,这样使得大部分的处理时间都用在了处理数据使得系统更有效率。 申请小的数据块就意味着会向调度器多次申请。 这样做的副作用就是当block处理大量数据的时候会出现延迟。

为了解决这个问题,gr_top_block可以限制block可以收到的数据量,也就是上一个block的需要输出的数据量。 一个block只能得到比这个数量少的采样点输入,所以相当于一个block的最大延迟。 通过限制每次调用申请的数据量,我们相当于增加了调度器的负担,所以降低了全局效率。

可以按照如下方式限制输出数据量:

tb.start(1000)
tb.wait()
# or
tb.run(1000)

使用这个方法,我们设置了一个全局的item数量限制。每个block可以通过'set_max_noutput_items(m)',重写这个限制。

tb.flt.set_max_noutput_items(2000)
tb.run(1000)

在一些情况下,可能想要限制输出缓存的大小。这个能够防止要输出的数据量过大超过了上限,而使得新的输出延迟。 你可以为每个block的每个输出端口设置输出延迟。

tb.blk0.set_max_output_buffer(2000)
tb.blk1.set_max_output_buffer(1, 2000)
tb.start()
print tb.blk1.max_output_buffer(0)
print tb.blk1.max_output_buffer(1)

上面的接口blk0所有端口被设置成缓存为2000个items,而blk1只有端口1被设置了,其余为默认值。

注意:

  • 在运行的开始,缓存的大小就被配置好了。
  • 一旦flowgraph开始,缓存长度对于一个block的值是不能被更改的,即使是lock()/unlock()。如果要改变缓存大小,必须删除block再重新建立。
  • 这可能影响到吞吐量。
  • 真实的缓存大小实际上是依赖于最小系统粒度。理论上就是一个页的大小,通常是4096bytes。这就意味着,由指令设置的缓存大小最终会四舍五入到最接近的系统粒度上。

动态配置流程图

在通信系统运行的时候,经常需要根据输入信号改变系统的状态,这时候需要更新流图。更新意味着改变结构,不独立的参数设置。 例如, gr::blocks::add_const_cc中改变加的常量大小可以由调用'set_k(k)'完成。

更新流图有三步:

  • 锁定,停止运行,处理数据
  • 更新
  • 解锁

下面的例子展示了一个流图,首先加入两个gr::analog::noise_source_c,然后由gr::blocks::sub_cc替代gr::blocks::add_cc。

from gnuradio import gr, analog, blocks
import time
class mytb(gr.top_block):
    def __init__(self):
        gr.top_block.__init__(self)
        self.src0 = analog.noise_source_c(analog.GR_GAUSSIAN, 1)
        self.src1 = analog.noise_source_c(analog.GR_GAUSSIAN, 1)
        self.add = blocks.add_cc()
        self.sub = blocks.sub_cc()
        self.head = blocks.head(gr.sizeof_gr_complex, 1000000)
        self.snk = blocks.file_sink(gr.sizeof_gr_complex, "output.32fc")
        self.connect(self.src0, (self.add,0))
        self.connect(self.src1, (self.add,1))
        self.connect(self.add, self.head)
        self.connect(self.head, self.snk)
    def main():
        tb = mytb()
        tb.start()
        time.sleep(0.01)
        # Stop flowgraph and disconnect the add block
        tb.lock()
        tb.disconnect(tb.add, tb.head)
        tb.disconnect(tb.src0, (tb.add,0))
        tb.disconnect(tb.src1, (tb.add,1))
        # Connect the sub block and restart
        tb.connect(tb.sub, tb.head)
        tb.connect(tb.src0, (tb.sub,0))
        tb.connect(tb.src1, (tb.sub,1))
        tb.unlock()
        tb.wait()
    if __name__ == "__main__":
        main()

在更新flowchart的时候,最大输出items数量也可以被更改。一个block也可以调用'unset_max_noutput_items()' 来解锁限制恢复到全局值。 下面的例子扩展了上面的例子,增加了设置最大输出items数量。

from gnuradio import gr, analog, blocks
import time
class mytb(gr.top_block):
    def __init__(self):
        gr.top_block.__init__(self)
        self.src0 = analog.noise_source_c(analog.GR_GAUSSIAN, 1)
        self.src1 = analog.noise_source_c(analog.GR_GAUSSIAN, 1)
        self.add = blocks.add_cc()
        self.sub = blocks.sub_cc()
        self.head = blocks.head(gr.sizeof_gr_complex, 1000000)
        self.snk = blocks.file_sink(gr.sizeof_gr_complex, "output.32fc")
        self.connect(self.src0, (self.add,0))
        self.connect(self.src1, (self.add,1))
        self.connect(self.add, self.head)
        self.connect(self.head, self.snk)
    def main():
        # Start the gr_top_block after setting some max noutput_items.
        tb = mytb()
        tb.src1.set_max_noutput_items(2000)
        tb.start(100)
        time.sleep(0.01)
        # Stop flowgraph and disconnect the add block
        tb.lock()
        tb.disconnect(tb.add, tb.head)
        tb.disconnect(tb.src0, (tb.add,0))
        tb.disconnect(tb.src1, (tb.add,1))
        # Connect the sub block
        tb.connect(tb.sub, tb.head)
        tb.connect(tb.src0, (tb.sub,0))
        tb.connect(tb.src1, (tb.sub,1))
        # Set new max_noutput_items for the gr_top_block
        # and unset the local value for src1
        tb.set_max_noutput_items(1000)
        tb.src1.unset_max_noutput_items()
        tb.unlock()
        tb.wait()
    if __name__ == "__main__":
        main()

模块类型

概述

利用gnuradio的框架,用户可以创建多种类型的模块来实现特定的数据处理。

  • Synchronous Blocks (1:1)
  • Decimation Blocks (N:1)
  • Interpolation Blocks (1:M)
  • Basic (a.k.a. General) Blocks (N:M)

Synchronous Blocks (1:1)

同步模块一个端口的的消耗和输出的点数量是一样的。零输入的同步模块叫做source,零输出的同步模块叫做sink。

#include <gr_sync_block.h>
class my_sync_block : public gr_sync_block
{
    public:
    my_sync_block(...):
        gr_sync_block("my block",
                    gr_make_io_signature(1, 1, sizeof(int32_t)),
                    gr_make_io_signature(1, 1, sizeof(int32_t)))
    {
        //constructor stuff
    }

    int work(int noutput_items,
            gr_vector_const_void_star &input_items,
            gr_vector_void_star &output_items)
    {
        //work stuff...
        return noutput_items;
    }
};
  • noutput_items是输入和输出的缓冲区大小
  • 输入的签名gr_make_io_signature(0, 0, 0)使得模块为source
  • 输出的签名gr_make_io_signature(0, 0, 0)使得模块为sink

gr_make_io_signature(int min_streams, int max_streams, int sizeof_stream_item)控制了接口的数量,以及接口的数据大小。下面给出了python的模块样例:

class my_sync_block(gr.sync_block):
    def __init__(self):
        gr.sync_block.__init__(self,
            name = "my sync block",
            in_sig = [numpy.float32, numpy.float32],
            out_sig = [numpy.float32],
        )
    def work(self, input_items, output_items):
        output_items[0][:] = input_items[0] + input_items[1]
        return len(output_items[0])

input_items和output_items是包含列表的列表。input_items的每个端口有一个输入采样点向量。output_items也是一个向量可以将输出点存起来。output_items[0]的长度等于C++中noutput_items。

  • in_sig=None的时候,模块为source
  • out_sig=None的时候,模块为sink。这时候要用len(input_items[0])
  • 不像C++中的gr::io_signature类,python可以直接创建指定数据类型的list

Basic Block

#include <gr_block.h>
class my_basic_block : public gr_block
{
public:
my_basic_adder_block(...):
    gr_block("another adder block",
            in_sig,
            out_sig)
{
    //constructor stuff
}

int general_work(int noutput_items,
                gr_vector_int &ninput_items,
                gr_vector_const_void_star &input_items,
                gr_vector_void_star &output_items)
{
    //cast buffers
    const float* in0 = reinterpret_cast(input_items[0]);
    const float* in1 = reinterpret_cast(input_items[1]);
    float* out = reinterpret_cast(output_items[0]);

    //process data
    for(size_t i = 0; i < noutput_items; i++) {
    out[i] = in0[i] + in1[i];
    }

    //consume the inputs
    this->consume(0, noutput_items); //consume port 0 input
    this->consume(1, noutput_items); //consume port 1 input
    //this->consume_each(noutput_items); //or shortcut to consume on all inputs

    //return produced
    return noutput_items;
}
};

GNURadio scheduler

GNURadio的scheduler是数据流调度的核心。gnuradio的官方文档和教程关于这个部分的介绍很少。为了解释清楚调度器,本文参考了gnuradio的其中一个作者Tom Rondeau的slides: gnuradio-note ,以及笔者的一些源码阅读。

首先看一个例子,后面的讨论都会基于这个简单的例子。创建两个数据流经过一个同步模块,再经过一个十倍欠采样模块,最后输出。

_images/scheduler-1.png

在gnuradio里,对于每个模块之间,调度器都会维护一个buffer。对于一个block输入是input buffer,输出是output buffer。在output区,block利用Wptr指针写数据;在input区,block利用Rptr指针读取数据。

_images/scheduler-2.png

对于模块Decimator,我们需要足够的输入来计算输出。

_images/scheduler-3.png

接着我们复习一下block的工作函数。

general_work()和work()

general_work()和work()是block工作的核心函数,数据流的操作都在这里完成。

int block::general_work(int noutput_items,
    gr_vector_int &ninput_items,
    gr_vector_const_void_star &input_items,
    gr_vector_void_star &output_items)

input_items 是一个vector包含一组指针指向input buffer。output_items 是一个vector包含一组指针指向output buffer。general_work()方法不指定输入输出的关系,只是指定输入和输出的数量。noutput_items是最小的output数量。ninput_items是input buffer。

int block::work(int noutput_items,
    gr_vector_const_void_star &input_items,
    gr_vector_void_star &output_items)

work函数指定了input和output的关系。通过noutput_items确定ninput_items。有了这些知识,我们开始看scheduler的工作方式。

Scheduler的基本功能

GNURadio的调度器会处理block的需求也就是对于数据流和数据指针的调度,以及控制buffer缓冲区的大小。除此之外,buffer,messages流和stream tags也会由调度器控制。Block之间会传递三种类型的数据:采样点数据data,消息messages,数据标签tags。下面我们分别看一下,对于三种类型,调度器需要作什么。

Data调度

对于Data,blocks有几个需求:alignment,output multiple,forecast,history。alignment和output multiple都是为了控制输出的数据量要满足一定的倍数。forecase和history都是控制buffer的数据满足读取的需求。调度器调度数据主要就是满足alignment,output multiple,forecast,history的需求。

_images/scheduler-4.png
  • alignment: 将输出对齐到一定倍数,不一定保证。
  • output multiple:将输出对齐到一定倍数,保证实现。如不满足会等待。
  • forecast:利用ninput_items_required[i]告诉调度器,对于每个输出需要多少输入。
  • history:利用set_history()方法,高速scheduler进一步调整buffer的长度。如果我们将history设置为N,那么buffer里的前N个数据中的N-1个数据为历史数据(即使你已经用过了)。history保证了buffer里至少有N-1个数据。
_images/scheduler-history.png

当我们给定输出的数据数量noutput_items,那么我们可以计算输入数据量ninput_items_required[i]:

//forecast()
ninput_items_required[i]=noutput_items+history()-1; // default
ninput_items_required[i]=noutput_items*decimation()+history()-1; // Decim
ninput_items_required[i]=noutput_items/interpolation()+history()-1; // Interp

经过这样的forecast设置,可以保证输入满足输出的需求。

Buffer和latency调度

调度器也会控制缓冲区大小和延迟。又一下几个方法完成。

// Caps the maximum noutput_items.
// Will round down to nearest output multiple, if set.
// Does not change the size of any buffers.
set_max_noutput_items(int)
// Sets the maximum buer size for all output buers.
// Buffer calculations are based on a number of factors, this limits overall size.
// On most systems, will round to nearest page size.
set_max_output_buffer(long)
// Sets the minimum buer size for all output buers.
// On most systems, will round to nearest page size.
set_min_output_buffer(long)
Messages调度

Message可以用来传递一些控制信息,或者数据包Packet Unit Data。每个block可以创建自己的Messages queue。当messages传递的时候,messages会放到subscriber的queue里。Messags的优先级是高于data的,在后面的整体操作流程中,优先处理messages。调度器dispatch处理messags是通过调用block的handler实现的。Messags的queue大小是由max_nmsgs控制的。

_images/scheduler-msg.png
Stream Tags 调度

Steam tags是帮助block标记和识别处理过的数据。对于一个指定的samples,我们打上一些tag。tag会逐级传递。随着data rate的变化,tag的位置会更新。tag_propagation_policy标签的传递规则是有block的构造器控制的。tag的处理是在general_work后面。tag_propagation_policy有两种TPP_ALL_TO_ALL和TPP_ONE_TO_ONE。第一种会把所有Tag都标上每一个samples,后一种是一对一的。

_images/scheduler-tag.png

综上,调度器需要完成以下的任务:

  • 计算input有多少可用的点
  • 计算output有多空间
  • 确定限制条件: history, alignment, forecast
  • 必要的调整或者重试
  • call general_work,给block恰当的指针和数据
  • 从general_work的返回值更新指针

Scheduler Flow Chart

有了上面的基础,我们就做好了了解scheduler如何调度一个完整的gnuradio flow chart的准备。起初,调度器会为每个模块初始化创建一个线程。tpb_container为block的线程池。

_images/scheduler-init.png

tpb_thread_body会控制所有线程。首先设置线程优先级。如果block就绪了,就可以处理传递的messages。如果input的数据量不够,会将block设置为BLKD_IN。直到数据流满足了需求,进入核心函数run_one_iteration()。这个函数在block_executor.cc文件中实现。如果函数结束,ready状态的时候,会通知与这个block相邻的其他block。告诉他们,input和output缓冲区的状态。如果是READY_NO_OUTPUT,则说明没有数据输出,通知上一block。如果DONE,传递DONE的消息到其他所有block。

_images/scheduler-thread.png

AGC自动增益控制

这里整理一下GNURadio的自动增益控制是如何实现的。自动增益控制模块是在analog大类里实现,并一共定义了三种自动增益控制: agc,agc2,agc3。agc是最普通的自动增益控制,agc,agc2增加了attack和decay控制。attack指的是agc可以多快的响应功率迅速增加的信号,decay指的是agc可以多快的响应功率迅速减小的信号。这两个时间决定了AGC的带宽。通常来说,我们要求AGC的带宽要小于信号的最小频率,这样才不会影响信号的解调。

class ANALOG_API agc_cc
{
public:
    /*!
    * Construct a complex value AGC loop implementation object.
    *
    * \param rate the update rate of the loop.
    * \param reference reference value to adjust signal power to.
    * \param gain initial gain value.
    * \param max_gain maximum gain value (0 for unlimited).
    */
    agc_cc(float rate = 1e-4,
        float reference = 1.0,
        float gain = 1.0,
        float max_gain = 0.0)
        : _rate(rate), _reference(reference), _gain(gain), _max_gain(max_gain){};

    virtual ~agc_cc(){};

    float rate() const { return _rate; }
    float reference() const { return _reference; }
    float gain() const { return _gain; }
    float max_gain() const { return _max_gain; }

    void set_rate(float rate) { _rate = rate; }
    void set_reference(float reference) { _reference = reference; }
    void set_gain(float gain) { _gain = gain; }
    void set_max_gain(float max_gain) { _max_gain = max_gain; }

    gr_complex scale(gr_complex input)
    {
        gr_complex output = input * _gain;

        _gain += _rate * (_reference - std::sqrt(output.real() * output.real() +
                                                output.imag() * output.imag()));
        if (_max_gain > 0.0 && _gain > _max_gain) {
            _gain = _max_gain;
        }
        return output;
    }

    void scaleN(gr_complex output[], const gr_complex input[], unsigned n)
    {
        for (unsigned i = 0; i < n; i++) {
            output[i] = scale(input[i]);
        }
    }

protected:
    float _rate;      // adjustment rate
    float _reference; // reference value
    float _gain;      // current gain
    float _max_gain;  // max allowable gain
};

Polymorphic Types

介绍

Polymorphic Types(多态)是一种高级的数据类型,被设计成通用类型用来在block和thread之间传递数据。 在stream tags和message passed接口中使用的很多。下面由一段python代码看pmt的使用方法:

>>> import pmt
>>> P = pmt.from_long(23)
>>> type(P)
<class 'pmt.pmt_swig.swig_int_ptr'>
>>> print P
23
>>> P2 = pmt.from_complex(1j)
>>> type(P2)
<class 'pmt.pmt_swig.swig_int_ptr'>
>>> print P2
0+1i
>>> pmt.is_complex(P2)
True

我们利用from_long和from_complex导入了一个长整数和一个复数。但是他们的类型是一样的,都是pmt。 这样我们就可以把这些变量利用swig传入C++。 同样C++代码如下:

#include <pmt/pmt.h>
// [...]
pmt::pmt_t P = pmt::from_long(23);
std::cout << P << std::endl;
pmt::pmt_t P2 = pmt::from_complex(gr_complex(0, 1)); // Alternatively: pmt::from_complex(0, 1)
std::cout << P2 << std::endl;
std::cout << pmt::is_complex(P2) << std::endl;

有两个特点在C++和python都很重要。首先,我们可以很容易的打印pmt的内容。PMT内置了把值转化成string的方法(某些类型的数据不行)。 而且,PMT必须显式的知道他们的类型,所以我们可以查询他们的类型,比如调用is_complex方法。

non-PMT和PMT的转化使用 from_x和to_x方法。

pmt::pmt_t P_int = pmt::from_long(42);
int i = pmt::to_long(P_int);
pmt::pmt_t P_double = pmt::from_double(0.2);
double d = pmt::to_double(P_double);

string是一个比较特殊的类型,他的转化是特殊的方法。

pmt::pmt_t P_str = pmt::string_to_symbol("spam");
pmt::pmt_t P_str2 = pmt::intern("spam");
std::string
str = pmt::symbol_to_string(P_str);

pmt::intern是symbol_to_string的另外一种方法。

在python中,我们可以使用弱类型。

Metadata Information

Introduction

元数据文件在文件头有额外的元数据存储着有关采样点类型的信息。 原始文件,二进制文件不带有任何额外信息。所以这类文件必须被特殊处理。 系统中的任何改变,比如采样率或者接受机的频率都没有在文件中体现,元数据的文件头解决了这类问题。

我们利用gr::blocks::file_meta_sink写入元数据文件,利用gr::blocks::file_meta_source读取元数据文件。

元数据文件的文件头描述了一个数据分片的信息。比如,item size,数据类型(comples),采样率,首个采样点的时间戳, 文件头的大小和分片大小。

第一个静态区保存着:

  • version: (char) version number (usually set to METADATA_VERSION)
  • rx_rate: (double) Stream's sample rate
  • rx_time: (pmt::pmt_t pair - (uint64_t, double)) Time stamp (format from UHD)
  • size: (int) item size in bytes - reflects vector length if any.
  • type: (int) data type (enum below)
  • cplx: (bool) true if data is complex
  • strt: (uint64_t) start of data relative to current header
  • bytes: (uint64_t) size of following data segment in bytes

额外的分局存储在每一个收到的tags里。

  • rx_rate: the sample rate of the stream.
  • rx_time: the time stamp of the first item in the segment.

在一个文件中的数据类型是不会变得。因为GNU Radio的block只能在构造函数的IO signature设置数据额类型,所以之后的数据类型改变不会被接受。

元数据文件的类型

GNU Radio支持两种:

  • inline:headers和数据在同一行
  • detached:headers在一个单独的header file里

inline是标准的方法。如果使用detached方法,headers简单地插入到detached header file;数据文件是标准的无中断的原始二进制格式。

更新headers
实现

结构

Header Information
额外信息

使用

例子

例子在

  • gr-blocks/examples/metadata

GRC例子

  • file_metadata_sink: create a metadata file from UHD samples.
  • file_metadata_source: read the metadata file as input to a simple graph.
  • file_metadata_vector_sink: create a metadata file from UHD samples.
  • file_metadata_vector_source: read the metadata file as input to a simple graph.

message passing

Introduction

GNURadio的最初设计是为了处理数据流,比特和采样点为基本的处理单位。为了传输控制信息,元数据,包结构,Gnuradio引入了标签流。 标签流和数据流是并行处理的。标签流是为了存储metadata,控制信息的。标签和采样点是关联的,和数据流一起传输。这个模型使得模块可以识别一些特殊的事件,采取一定的措施。 缺点是:标签流只能单向流动,而且只能在模块的work函数访问。优点是,标签流和数据流是等同步的。

设计这个机制的两个目的是:

  • 下行的模块可以回传数据给上行模块
  • 外部程序可以用这个接口和GNURADIO通信

这个模块严重依赖多态类型实现(PMT)。

Message Passing API

message passing的接口在gr::basic_block中得到了实现,gr::basic_block是所有block的父类。 每个block都有一个消息队列可以存储消息,并向下传输消息。 而且可以区分输入和输出端口。

端口是在构造器中声明的:

void message_port_register_in(pmt::pmt_t port_id)
void message_port_register_out(pmt::pmt_t port_id)

每个接口有一个端口id。其他block要和这个端口通信,接收或者发送message,必须订阅这个端口。subscribe的API如下:

void message_port_pub(pmt::pmt_t port_id,pmt::pmt_t msg);
void message_port_sub(pmt::pmt_t port_id,pmt::pmt_t target);
void message_port_unsub(pmt::pmt_t port_id,pmt::pmt_t target);

任何block订阅了另一个block的输出端口后,会在发布消息的时候收到消息。 在一个block内部,当他要发布消息的时候,他会向每个订阅了他输出的端口的block的消息队列发送消息。

Message Handler Functions

订阅了block的消息的端口后, 必须声明一个处理方法。 利用gr::basic_block::message_port_register_in订阅了一个端口后,我们必须把这个端口绑定到一个消息处理器上。 这个部分利用的是boost的bind函数:

set_msg_handler(pmt::pmt_t port_id,
                boost::bind(&block_class::message_handler_function, this, _1));
  • 'port_id' 是输入的端口id
  • 'block_class::message_handler_function'是处理这个端口消息的函数
  • this和_1是boost绑定函数的
void block_class::message_handler_function(pmt::pmt_t msg);

Connecting Messages through the Flowgraph

这个机制的接口是独立于数据流的,所以创建模块的时候不需要

Code Examples

下面利用gr::blocks::message_debug 和 gr::blocks::tagged_stream_to_pdu 。 gr::blocks::message_debug模块是用来调试消息传递的模块。有三个输入口:

  • print,打印所有信息到标准输出流。
  • store,把消息存储到list里,和gr::blocks::message_debug::get_message(int i)连接,取出第i个消息。
  • pdu_print,把PDU消息转化成标准流。
{
    message_port_register_in(pmt::mp("print"));
    set_msg_handler(pmt::mp("print"),
    boost::bind(&message_debug_impl::print, this, _1));
    message_port_register_in(pmt::mp("store"));
    set_msg_handler(pmt::mp("store"),
    boost::bind(&message_debug_impl::store, this, _1));
    message_port_register_in(pmt::mp("print_pdu"));
    set_msg_handler(pmt::mp("print_pdu"),
    boost::bind(&message_debug_impl::print_pdu, this, _1));
}

GNURadio使用教程

USRP环境配置

这里主要介绍使用USRP平台,上位机是ubuntu的环境配置。从容易到难列出配置环境的几种方式,最后针对几种不同的软件无线电平台给出特有的步骤。

SDR Live

最简单的方式,利用官方的SDR Live镜像,直接安装配有完整环境的系统盘。需要注意的是,这样安装的系统只能在线读写,每次启动都会复原。

https://wiki.gnuradio.org/index.php/GNU_Radio_Live_SDR_Environment

使用自动化脚本

这种方式也很方便。前人写好的自动化脚本完成了从源代码的下载,编译和安装几个步骤。过程很慢,要有心理准备。

wget http://www.sbrac.org/files/build-gnuradio && chmod a+x ./build-gnuradio && ./build-gnuradio

其中有可能遇到几种问题:

  • 路径访问权限报错。
chmod: changing permissions of './build-gnuradio': Operation not permitted

需要在一个有完全访问权限的路径使用脚本。

  • 依赖的包找不到,笔者遇到了两个。
Failed to find package 'libzmq1-dev' in known package repositories
Failed to find package 'python-wxgtk2.8' in known package repositorie

wkgtk是wx-gui的包,如果这里没有装成功,之后wx-gui的组件就不能使用了,如果使用了wx-gui,会报出如下错误。

wxgui-python
TypeError: Error when calling the metaclass bases
    multiple bases have instance lay-out conflict

之所以找不到这个包是因为ubuntu16.04不再有这个包了,是wkgtk3.0。 这里首先获取apt源的列表,安装历史版本2.8。 关于libzmq1-dev这个包没有找到合适的解决方案,但是这个不影响使用。

Pybombs

Gnuradio推荐的方式是利用官方发布的pybombs工具安装gnuradio及其依赖。 但是实际操作的时候,pybombs的bug还是很多,这里并不推荐。

E310/E312的环境配置

  • 需要重新针对E310/312构建UHD驱动。

https://kb.ettus.com/Software_Development_on_the_E310_and_E312

git clone https://github.com/EttusResearch/uhd.git
cd ./host
mkdir build
cmake ./ -DENABLE_E300=ON
make install -j8

-j8是为了使用多核,速度会快些。

  • Remote login E312
ssh root@192.168.10.10
  • 切换网络模式
usrp_e3x0_network_mode
  • 开启另一个终端,查找设备。
uhd_find_devices --args="addr=192.168.10.10"

如果上面的构建失败就会出现

No UHD Devices Found
FPGA版本不兼容

E31x系列比较烦人的是内部有一个linux系统,也要配置环境。 如果内部系统用的FPGA版本和外部控制电脑不一致,虽然UHD驱动仍然可以找到设备,调试的时候就会报错。

RuntimeError: RuntimeError: Expected FPGA compatibility number 16.x, but got 14.0:
The FPGA build is not compatible with the host code build.
Please run:

    "/usr/local/lib/uhd/utils/uhd_images_downloader.py"

当然按照他给的方案,直接下载uhd镜像是肯定不行的。用UHD工具查看FPGA版本。

uhd_find_devices
linux; GNU C++ version 5.4.0 20160609; Boost_105800; UHD_003.010.002.000-3-g122bfae1

--------------------------------------------------
-- UHD Device 0
--------------------------------------------------
Device Address:
    type: e3x0
    addr: 192.168.10.10
    name:
    serial: 30CCCC1
    product: 30675

E310,E312,E313的FPGA的硬件版本都是e3x0。

  • 方案一,降低本机的UHD版本

注意到本机的UHD版本是3.11.1,与E312内部的版本不同。 这里选择将本机的UHD版本降低到与E312一致,这时候运行程序的时候会出现GnuRadio Companion的UHD组件冲突,需要重新编译GnuRadio。

_uhd.swig: undefined symbol: _ZN3uhd4usrp10multi_usrp7ALL_LOSB5cxx11E

再利用E312的测试程序。

rx_ascii_art_dft ­­--freq 88.1e6 ­­--rate 400e3 ­­--gain 30 ­­--ref­-lvl ­-30
  • 方案二,升级USRP的UHD版本。

首先格式化SD卡

sudo umount /dev/sdb1
sudo mkdosfs -F 32 -v /dev/sdb1

Read more: http://www.arthurtoday.com/2013/10/ubuntu-mkdosfs-format-sd-card.html#ixzz52HCPfTVQ

然后写入SD卡的镜像文件

sudo dd if=sdimage-gnuradio-dev.direct of=/dev/<yoursdcard> bs=1M

<yoursdcard>可以用fdisk -l或者df看到。

配置ip信息

USB串口进入设备,在设备内更新网络配置文件。

sudo screen /dev/ttyUSB0 115200
cd etc/network
vi interface

在auto eth0后面加入

iface eth0 inet static
    address 192.168.10.10
    netmask 255.255.255.0
    gateway 192.168.10.1

BladeRF环境配置

BladeRF有详细的官方windows教程,很难做错,这里就毋庸赘言了。主要介绍BladeRF在ubuntu的环境配置。

同样官方给了easy安装版本。 https://github.com/Nuand/bladeRF/wiki/Getting-Started:-Linux#Easy_installation_for_Ubuntu_The_bladeRF_PPA

遇到的问题可能有:

  • FPGA not laoded
bladeRF-cli -i
bladeRF> info

Serial #:                 19a3df66ec02993409cd516b0ef169ff
VCTCXO DAC calibration:   0x925f
FPGA size:                115 KLE
FPGA loaded:              no
USB bus:                  3
USB address:              2
USB speed:                SuperSpeed
Backend:                  libusb
Instance:                 0

从BladeRF官网下载对应的FPGA镜像 https://www.nuand.com/fpga.php

$ bladeRF-cli -L hostedx115-latest.rbf
Writing FPGA to flash for autoloading...
[INFO @ usb.c:498] Erasing 55 blocks starting at block 4
...
[INFO @ usb.c:617] Done reading 13952 pages
Done.
$ bladeRF-cli -l hostedx115-latest.rbf
Loading fpga...
Done.

在VSCode调试GNURadio Block

Prerequisite

旧版本的VScode必须用sudo才能用GDB调试。GDB调试是Linux的标准调试器。$TMPDIR是VScode的用户临时文件。

类型转换

  • ishort_to_complex模块:interleaved_short_to_complex ,交叉short类型转complex

控制接口

为GNURadio创建分布式应用。这个模块使得内部Blocks可以连接到一个输出流,远程画图; 以及输出可以被设置,监听,绘制的变量。模块在ctrlport命名空间内,可以被如下方式访问。

from gnuradio import ctrlport

注意

SBX子板的DC控制

SBX子板的自动DC控制,在直接做ASK调制的时候,有长连1的时候, SBX子板会将长1调整成直流分量,从而使得ASK调制在接受端看起来是相反的。

self.source.set_auto_dc_offset(False)

SBX全双工

SBX子板有两个天线:TX/RX和RX2,但是子板上只有一个transmit和一个receive。

  • 做全双工的时候,必须由RX2做接收,而且收发的工作频率可以不一致
  • 不支持两路都接收

OOT模块开发

UHD开发

Multiple USRP Connection and Configuration

Network Connection

  • 如果有两张网卡,可以将两张网卡配置在不同网段,然后两个USRP分别配置在两个网段里,然后在一个gnuradio脚本里直接访问。
  • 如果多台PC用交换机访问每个USRP,需要将多张网卡配置在一个网段里,还要指定每个网卡的UHD通信端口,端口不能冲突,否则UHD只能找到一个设备。

MIMO Wire Connection

USRP可以利用MIMO线将两台USRP同步。

Clock Source

Tuning Notes

Two-stage tuning process

一个USRP设备有两级变音:

  • RF front-end:从射频前端到中频
  • DSP:中频到基频

Self-Calibration

UHD提供了软件矫正IQ不平衡和直流偏差的工具。偏差的结果被保存在用户home路径下的一个CSV文件。 当用户重新启动UHD,GnuRadio会先读取矫正文件。

完成自矫正,首先需要断开所有USRP射频连接。 .. code:: bash

uhd_cal_rx_iq_balance: - minimizes RX IQ imbalance vs. LO frequency uhd_cal_tx_dc_offset: - minimizes TX DC offset vs. LO frequency uhd_cal_tx_iq_balance: - minimizes TX IQ imbalance vs. LO frequency

第一部分,GNURadio的指导手册和源码阅读。整理了GNURadio的基本结构和常用的模块。第二部分整理了笔者实际做GNURadio开发过程中遇到的问题,包括环境的搭建和代码调试。第三部分是UHD的开发部分。UHD是ettus公司开发的USRP的驱动。射频的低层配置需要有UHD完成。