流图基础:信号源、信号处理模块、信号汇的构建与连接方式

GNU Radio 的流图(Flowgraph)是信号处理链路的载体,它由三类基本节点构成:信号源(Source)、信号处理模块(Processing Block)和信号汇(Sink)。理解这三类节点的构建规则与连接方式,是设计任何软件无线电系统(从简单的波形发生器到复杂的频谱感知链路)的前提。本章将围绕流图的数据流语义、模块端口匹配规则、连接操作流程以及常见设计误区展开,帮助您建立对 GNU Radio 运行时架构的底层认知。

信号源层 处理层 信号汇层 信号源 (如 Signal Source) 文件源 (如 File Source) 硬件源 (如 UHD: USRP Source) 处理模块 (如 Low Pass Filter) 变换模块 (如 FFT / Resampler) 控制模块 (如 Throttle / Head) 图形汇 (如 QT GUI Sink) 文件汇 (如 File Sink) 硬件汇 (如 UHD: USRP Sink) 采样数据流 (item 流) 处理后数据流 (item 流) out:0 in:0 out:0 in:0 流图构建核心:端口类型匹配 + 采样率一致 + 数据类型一致

2.1 三类节点的核心原理与数据流语义

在 GNU Radio 中,流图是一个有向无环图(DAG),数据以 item 为单位在模块间流动。一个 item 可以是复数采样点(gr_complex)、浮点数(float)、整数(int)或字节(byte)等。信号源是流图的起点,它只拥有输出端口(output port),负责产生或读取数据;信号汇是流图的终点,只拥有输入端口(input port),负责消费或输出数据;处理模块则同时拥有输入和输出端口,对数据执行变换、滤波、同步等操作。

流图的数据流遵循 推拉结合 机制:调度器根据下游模块的消费需求,向上游模块请求数据。每个模块内部维护一个缓冲区(buffer),上游模块将数据写入缓冲区,下游模块从缓冲区读取。这种设计使得模块之间无需全局时钟同步,而是通过缓冲区水位和调度器策略实现背压(backpressure)控制。

关键概念:流图中的连接(connect)操作本质上是将上游模块的输出端口与下游模块的输入端口绑定,并创建一个共享缓冲区。缓冲区的容量由 GNU Radio 运行时自动分配,但可以通过 set_max_output_buffer 等方法手动调整,以优化吞吐或延迟。

2.2 端口匹配与连接规则

构建流图时,必须确保连接的两个端口满足以下三个条件,否则流图启动时会抛出 RuntimeError:

  1. 数据类型一致:输出端口的数据类型必须与输入端口的数据类型完全相同。例如,复数信号源不能直接连接到浮点滤波器,需要插入 complex_to_float 转换模块。
  2. 端口编号有效:每个模块的端口从 0 开始编号。连接时必须指定正确的端口索引,例如 tb.connect(source, 0, sink, 0)。
  3. 流图无环:GNU Radio 不允许存在反馈回路(除非使用专门的消息端口或延迟模块),否则调度器无法确定执行顺序。

以下是一个典型的连接代码示例,构建一个从信号源到频谱显示再到文件存储的链路:

from gnuradio import gr, blocks, analog, qtgui, fft

class my_flowgraph(gr.top_block):
    def __init__(self):
        gr.top_block.__init__(self)
        
        # 1. 创建信号源:1kHz 正弦波,采样率 32kHz
        self.src = analog.sig_source_f(32000, analog.GR_SIN_WAVE, 1000, 0.5)
        
        # 2. 创建处理模块:FFT 变换(用于频谱显示)
        self.fft = fft.fft_vcc(1024, True, window=fft.window.blackmanharris(1024))
        
        # 3. 创建信号汇:QT 频谱显示 + 文件存储
        self.sink_qt = qtgui.sink_c(1024, fft.window.blackmanharris(1024),
                                    32000, "Spectrum", True, True, True, False, None)
        self.sink_file = blocks.file_sink(gr.sizeof_gr_complex, "output.bin")
        
        # 4. 连接模块:注意数据类型转换
        self.connect(self.src, self.fft)          # float -> complex (fft_vcc 内部处理)
        self.connect(self.fft, self.sink_qt)      # complex -> complex
        self.connect(self.fft, self.sink_file)    # complex -> complex

if __name__ == '__main__':
    tb = my_flowgraph()
    tb.start()
    input("Press Enter to stop...")
    tb.stop()

上述代码展示了三个关键操作:实例化模块、指定参数(采样率、频率、窗口类型等)、调用 connect 方法。注意 sig_source_f 输出 float 类型,而 fft_vcc 要求输入 complex 类型,因此 GNU Radio 会自动插入一个隐式的 float_to_complex 转换模块(在 GRC 中可见,在 Python 中由调度器自动处理)。

2.3 案例拆解:多速率流图的构建与采样率匹配

在实际系统中,信号源、处理模块和信号汇往往工作在不同的采样率下。例如,USRP 硬件源以 10 MS/s 输出,而基带解调器需要 1 MS/s 的符号速率。此时必须在流图中插入 任意重采样器(Rational Resampler)或 多相滤波器组(Polyphase Filterbank)。

考虑一个典型的窄带 FM 接收机流图:

模块 类型 采样率 (MS/s) 数据类型 作用
USRP Source 信号源 10 complex 从射频前端采集 IQ 数据
Low Pass Filter 处理 10 complex 提取 200 kHz 带宽内的 FM 信号
Rational Resampler 处理 10 → 1 complex 将采样率降低 10 倍,减少后续计算量
FM Demodulator 处理 1 complex → float 解调 FM 信号,输出音频波形
Audio Sink 信号汇 1 float 将音频数据发送至声卡播放

构建该流图时,需要特别注意 重采样器的插值/抽取因子 设置。Rational Resampler 的插值因子(interpolation)和抽取因子(decimation)必须满足:输出采样率 = 输入采样率 × interpolation / decimation。在本例中,设置 interpolation=1, decimation=10 即可实现 10 MS/s 到 1 MS/s 的转换。

常见陷阱:如果信号源和信号汇的采样率不匹配,且没有插入重采样器,流图虽然能启动,但会产生 数据溢出(overflow) 或 欠载(underflow)。在 USRP 硬件汇中,这会导致缓冲区溢出并丢包;在文件汇中,则会导致采样点时间戳错乱。务必在流图设计阶段就明确各级采样率。

案例复盘:在一次实际调试中,我们构建了一个从文件读取 IQ 数据、经过 FIR 滤波、然后输出到 QT 频谱显示的流图。初始现象是频谱显示出现明显的“锯齿”状混叠。通过检查发现,文件源的数据速率为 2 MS/s,而 QT GUI Sink 的采样率参数误设为 5 MS/s。修正 Sink 的采样率参数后,频谱恢复正常。这个案例说明,信号汇的采样率参数必须与上游实际数据速率一致,否则显示时间轴和频率轴都会失真。

2.4 常见误区与设计建议

  • 误区一:忽略数据类型转换。 在 GRC 中,连接线颜色代表数据类型(紫色为 complex,黄色为 float,绿色为 int)。如果连接线颜色不匹配,GRC 会提示错误。在 Python 中,务必使用 blocks.float_to_complex 等显式转换模块,避免依赖隐式转换带来的性能开销。
  • 误区二:信号源没有限流模块。 当信号源(如信号发生器)直接连接到文件汇时,如果没有 head 模块限制样本数量,流图将无限运行,导致磁盘空间耗尽。建议在调试阶段使用 blocks.head(gr.sizeof_gr_complex, 1000000) 限制样本数。
  • 误区三:处理模块的输入输出速率不一致。 某些模块(如 fft_vcc)的输出样本数不等于输入样本数(FFT 输出帧大小固定)。此时需要理解模块的 相对速率(relative rate),并在下游使用 keep_one_in_n 或 interleave 等模块调整流形。
  • 误区四:在信号汇后继续连接模块。 信号汇是流图的终点,其输出端口不存在。如果尝试从信号汇引出连接,GNU Radio 会抛出 AttributeError。
设计建议:在构建复杂流图前,先在 GRC 中绘制草图,并利用 GRC 的“类型检查”功能(连接线颜色)快速发现端口不匹配问题。对于采样率变化,使用 probe_signal 模块实时监测各级采样率,确保链路速率一致。

最后,请记住流图构建的核心原则:数据从源流向汇,端口类型必须匹配,采样率逐级衔接,缓冲区大小按需调整。掌握这些基础后,您就可以开始设计具有实际吞吐需求的流图,并进一步探索 GNU Radio 的调度器行为和性能优化技巧。