From 3c57904dc02a6b1e237d7f69a8564e169d9139a4 Mon Sep 17 00:00:00 2001 From: doctormin Date: Thu, 8 Aug 2024 20:34:58 +0800 Subject: [PATCH 1/7] minor update & fix --- content-zh/digital_modulation.rst | 2 +- content-zh/frequency_domain.rst | 19 +++++++++---------- content-zh/intro.rst | 3 ++- content-zh/link_budgets.rst | 11 +++++------ content-zh/multipath_fading.rst | 16 ++++++++-------- 5 files changed, 25 insertions(+), 26 deletions(-) diff --git a/content-zh/digital_modulation.rst b/content-zh/digital_modulation.rst index 54d7cec1..97adf0f2 100644 --- a/content-zh/digital_modulation.rst +++ b/content-zh/digital_modulation.rst @@ -4,7 +4,7 @@ 数字调制 ################### -在本章中,我们将讨论如何使用数字调制(Digital Modulation)和无线符号(Wireless Symbols)去 *实际传输数据* ! +在本章中,我们将学习使用数字调制(Digital Modulation)和无线符号(Wireless Symbols)去 *实际传输数据* ! 我们将使用 ASK、PSK、QAM、FSK 等调制方案去设计一些能传达 “信息” 的无线信号,例如 1 和 0。 我们还将讨论 IQ 图和星座图(Constellation),并以一个 Python 示例收尾本章。 diff --git a/content-zh/frequency_domain.rst b/content-zh/frequency_domain.rst index a9bcd046..9740bbb9 100644 --- a/content-zh/frequency_domain.rst +++ b/content-zh/frequency_domain.rst @@ -4,9 +4,8 @@ 频率域 ##################### -本章主要探讨频率域(Frequency Domain, 后文简称 **频域** ), -包括傅里叶级数、傅里叶变换、傅里叶性质、快速傅里叶变换(FFT)、窗函数处理(Windowing)和时频谱/瀑布图(Spectrograms/Waterfall), -同时还提供 Python 实例进行分析。 +在本章中,我们将学习频率域(Frequency Domain, 后文简称 **频域** )、傅里叶级数、傅里叶变换、傅里叶性质、快速傅里叶变换(FFT)、窗函数处理(Windowing)和时频谱/瀑布图(Spectrogram/Waterfall), +同时还将学习 Python 实例分析。 学习数字信号处理(DSP)和无线通信的其中一个有趣的副产品是,能让你习惯于在频域中思考。 然而,多数人对于频域的理解程度,可能仅限于在调节汽车音响的低音、中音和高音旋钮,或者简单的观察一个音频均衡器,如下图所示: @@ -31,7 +30,7 @@ 傅里叶级数 *************** -频域的基本理念是任何信号都可以通过一系列叠加的正弦波来表达。如果我们将一个信号解构为其组成的正弦波,我们称之为傅立叶级数。以下是一个由两个正弦波组合构成的信号的示例: +频域的基本理念是任何信号都可以通过一系列叠加的正弦波来表达。如果我们将一个信号解构为其组成的正弦波,我们称之为傅里叶级数。以下是一个由两个正弦波组合构成的信号的示例: .. image:: ../_images/summing_sinusoids.svg :align: center @@ -53,7 +52,7 @@ :align: center :alt: Animation of the Fourier series decomposition of an arbitrary function made up of square pulses -要理解我们如何将信号分解为正弦波(sine waves, 或称为 sinusoids),我们需要先回顾正弦波的三个属性: +要理解我们如何将信号分解为正弦波(sine wave, 或称为 sinusoid),我们需要先回顾正弦波的三个属性: #. 幅度(Amplitude) #. 频率(Frequency) @@ -143,7 +142,7 @@ 傅里叶变换 ***************** -在数学上,我们用来从时域转换到频域的“变换”称为傅里叶变换(Fourier Transform)。其定义如下: +数学上,傅里叶变换(Fourier Transform)是一种时域到频域的 “变换”。其定义如下: .. math:: X(f) = \int x(t) e^{-j2\pi ft} dt @@ -179,7 +178,7 @@ 如果这些方程对你来说没有多大意义,也没关系。实际上,我们不直接使用它们也可以用 DSP 和 SDR 做一些很酷的事情! ************************* -时间-频率特性 +时间-频率性质 ************************* 我们之前检查了信号在时域和频率域中的表现形式的例子。现在,我们将涵盖五个重要的“傅里叶性质”。 @@ -358,7 +357,7 @@ FFT 令人疑惑的一点在于,其输出总是在频域中的,所以当我 但是实际上,仅仅是因为它低于中心频率而已。当我们学习更多关于采样的知识并且积累了 SDR 设备的使用经验后,你将彻底明白为什么会发生这个转换。 **************************** -时域上的顺序并不重要 +时域中的顺序并不重要 **************************** 在我们开始深入探讨 FFT 之前,还有最后一个性质需要了解。 @@ -609,7 +608,7 @@ FFT 的代码实现 然后在 1965 年被 James Cooley 和 John Tukey 重新发明并普及。 这个算法的基本版本适用于二的幂次方窗口大小的 FFT,旨在处理复数输入,但也可以处理实数输入。 -这个算法的构建模块被称为蝶形运算(butterfly),本质上是一个 N = 2 窗口大小的 FFT,包括两次乘法运算和两次求和运算: +这个算法的构建模块被称为蝶形运算(Butterfly),本质上是一个 N = 2 窗口大小的 FFT,包括两次乘法运算和两次求和运算: .. image:: ../_images/butterfly.svg @@ -628,7 +627,7 @@ FFT 的代码实现 ( :math:`N` 是 FFT 窗口大小,:math:`k` 是索引)。 注意输入和输出都是复数,例如,:math:`x_0` 可能是 0.6123 - 0.5213j,加法与乘法操作都是复数运算。 -该算法是二分递归的,直到最后只剩下一系列的蝶形运算(Butterflies),下面用一个窗口大小为 8 的 FFT 来描述这个过程: +该算法是二分递归的,直到最后只剩下一系列的蝶形运算,下面用一个窗口大小为 8 的 FFT 来描述这个过程: .. image:: ../_images/butterfly2.svg :align: center diff --git a/content-zh/intro.rst b/content-zh/intro.rst index 8d419665..fa073bf3 100644 --- a/content-zh/intro.rst +++ b/content-zh/intro.rst @@ -56,7 +56,7 @@ 参与贡献 *************** -如果你从PySDR中获得了帮助,别忘了与你的同事、学生和其他可能对这些资料感兴趣的终身学习者分享。你还可以通过在PySDR的 `Patreon `_ 上捐款来表达你的支持,你的名字将会显示在每章下面的章节列表的左侧。 +如果你从 PySDR 中获得了帮助,别忘了与你的同事、学生和其他可能对这些资料感兴趣的终身学习者分享。你还可以通过在 PySDR 的 `Patreon `_ 上捐款来表达你的支持,你的名字将会显示在每章下面的章节列表的左侧。 此外,欢迎你通过 pysdr@vt.edu 向我发送问题、评论或者建议,你将为这本教科书的完善做出贡献! 你还可以直接在教科书的 `GitHub页面 `_ 上编辑源代码,欢迎你提交 Issue 或 Pull Request。 @@ -76,5 +76,6 @@ - `Daniel Versluis `_ 将 PySDR 翻译为 `荷兰语 `_ - `mrbloom `_ 将 PySDR 翻译为 `乌克兰语 `_ - `Yimin Zhao `_ 将 PySDR 翻译为 `简体中文 `_ +- `Eduardo Chancay `_ 将 PySDR 翻译为 `西班牙语 `_ 以及所有 `PySDR Patreon `_ 支持者! diff --git a/content-zh/link_budgets.rst b/content-zh/link_budgets.rst index c1f1640f..c4fe26a4 100644 --- a/content-zh/link_budgets.rst +++ b/content-zh/link_budgets.rst @@ -4,7 +4,7 @@ 链路预算 ################## -本章将重点介绍链路预算(Link Budget)这一概念, +在本章中,我们将学习链路预算(Link Budget)这一概念, 其中主要包含发送/接收功率、路径损耗(Path Loss)、 天线增益(Antenna Gain)、噪声(Noise)和信噪比(缩写为 SNR 或 S/N)等细节。 最后,我们通过一个针对 ADS-B 系统的链路预算实例来加深理解。 @@ -50,7 +50,7 @@ ADS-B 是商用飞机使用的一种信号系统,用于实时共享飞机的 发射功率 ##################### -传输功率比较直观易懂,它以 W(瓦特)、dBW 或 dBm(注意 dBm 是 dBmW 的简写)作为单位来衡量。 +传输功率比较直观易懂,它以 W(瓦特)、dBW 或 dBm(dBm 是 dBmW 的简写)作为单位来衡量。 每个发射机都配备一个或多个放大器,而传输功率主要取决于这些放大器的性能。 传输功率可以类比灯泡的功率瓦数,很显然高功率灯泡发出的光线更亮。 以下是不同技术示例的传输功率范围: @@ -128,7 +128,7 @@ Okumura-Hata 模型是城市和郊区(如蜂窝网络)中常用的模型之 L_{path} = 69.55 + 26.16 \log_{10} f - 13.82 \log_{10} h_B - C_H + \left[ 44.9 - 6.55 \log_{10} h_B \right] \log_{10} d 其中 :math:`L_{path}` 是路径损耗(dB),:math:`h_B` 是发射天线相对于地面高度(m), -:math:`f` 是载波频率(MHz),:math:`d` 是发射机(Tx)和接收(Rx)之间的距离(km), +:math:`f` 是载波频率(MHz),:math:`d` 是发射机(TX)和接收(RX)之间的距离(km), :math:`C_H` 被称为 “天线高度修正因子” ,根据城市规模和载波频率范围来具体设定。 对于中小型城市,:math:`C_H` 设置为: @@ -217,7 +217,7 @@ EIRP 是一个代表性数字,指出如果使用理想的全向天线,在天 在讨论完信号功率后,让我们转向讨论接收噪声,有了这两者之后就可以计算 SNR 了。 两者的计算方式也是类似的。 -噪声是如何进入通信链路的?答案是:**接收机!** +噪声是如何进入通信链路的?答案是:**接收机** ! 只有当信号到达接收机后才会受到干扰,理解这一点 **非常** 重要! 许多学生并没有完全理解这一点,结果犯了一些错误。 空气中并没有漂浮的噪声,噪声的来源是接收机中的放大器和其他电子元件,毕竟它们不完美并且在非零开尔文(K)的温度下工作。 @@ -328,5 +328,4 @@ ADS-B 的物理层(PHY)有这些特征: 但在真实情况下,例如使用不合适的天线随意放置在接收机上,并在教室里接收 ADS-B 信号, 附近有一个强大的 FM 广播电台引起干扰,这种情况下损耗可以轻松达到 20-30 dB。 -尽管这个例子只是一个草稿计算,但它展示了创建链路预算和理解通信链路的重要参数的基础知识。 - +尽管这个例子只是一个草稿计算,但它展示了创建链路预算和理解通信链路的重要参数的基础知识。 \ No newline at end of file diff --git a/content-zh/multipath_fading.rst b/content-zh/multipath_fading.rst index 63c7e491..e3025d88 100644 --- a/content-zh/multipath_fading.rst +++ b/content-zh/multipath_fading.rst @@ -4,14 +4,14 @@ 多径衰落 ####################### -在本章中我们将介绍多径(Multipath),它指的是信号从发射端 “经由两条或更多路径” 传播到接收端。 +在本章中,我们将学习多径(Multipath),它指的是信号从发射端 “经由两条或更多路径” 传播到接收端。 这种现象才是真实场景中多见的,而我们之前所讨论的 “AWGN 信道” 只是一种简单地将信号加到噪声上的模型,基本上只适用于有线通信和一些卫星通信系统。 ************************* 多径 ************************* -无线信道中往往存在各种各样的 “反射体(Reflectors)”,它们会反射射频信号。 +无线信道中往往存在各种各样的 “反射体(Reflector)”,它们会反射射频信号。 在发射机(TX)和接收机(RX)之间或附近的任何物体都会通过反射给原信号增加额外的传播路径,而每条路径都将制造不同的延迟(即相位变化)和衰减(即幅度变化)。 来自所有路径的信号将在接收端叠加,这种叠加可能是相加性的(Constructively)、相消性的(Destructively)或者二者兼有。 我们将信号通过多条信号路径到达接收端的概念称为 “多径”。 @@ -21,7 +21,7 @@ .. image:: ../_images/multipath.svg :align: center :target: ../_images/multipath.svg - :alt: 多径的简单描绘,展示了视线路径和一条非视线路径 + :alt: 多径的简单描绘,展示了视线路径和一条非视线路径。 在一些情况下,多径之间会产生相消性干涉。考虑上面的示例(其中只有两条路径),当载波频率或者路径距离变化时,来自这两条路径的信号可能以相似的幅度但是 180 度的相位差到达,这时它们会相互抵消(如下图所示)。当多径之间严重相消时,我们称之为 “深衰落(Deep Fade)”,此时接收端收到的信号会短暂消失。 @@ -34,7 +34,7 @@ .. image:: ../_images/multipath2.svg :align: center :target: ../_images/multipath2.svg - :alt: 多径以及功率延迟分布的可视化 + :alt: 多径以及功率延迟分布的可视化。 在功率时延谱中,最靠近 y 轴的箭头始终描述的是 LOS 路径(假设有的话),因为没有其他路径能比 LOS 路径更快到达接收端。通常,随着时延的增加功率会减小,因为信号到达接收端的路径距离会更远。 @@ -52,7 +52,7 @@ .. image:: ../_images/multipath_fading.png :scale: 100 % :align: center - :alt: 多径衰落有时候会导致 “深衰落(零值)” 现象,此时 SNR 会急剧下降 + :alt: 多径衰落有时候会导致 “深衰落(零值)” 现象,此时 SNR 会急剧下降。 从 **时间** 域的角度看,多径衰落有两种类型: @@ -70,7 +70,7 @@ .. image:: ../_images/flat_vs_freq_selective.png :scale: 70 % :align: center - :alt: 平坦衰落对比频率选择性衰落 + :alt: 平坦衰落对比频率选择性衰落。 下图展示了一个频率选择性衰落的示例。图中,一个带宽为 16 MHz 的信号被传输,在某些时刻出现了因衰落导致的信号缺失。 可以注意到,这些缺失是 “部分” 的,即在一些频率上导致了空洞但是没影响其他频率。 @@ -150,7 +150,7 @@ .. image:: ../_images/rayleigh.svg :align: center :target: ../_images/rayleigh.svg - :alt: 对瑞利衰落的模拟 + :alt: 对瑞利衰落的模拟。 从上图中可以看到两个有意思的点:一是深衰落会短暂出现,二是多径信道在某些时刻得到的接收信号甚至会比没有衰落的直射信号更好。 @@ -176,6 +176,6 @@ OFDM ##### 正交频分复用(OFDM)是 4G 蜂窝、WiFi 等许多技术所使用的一种方案。 -OFDM 使用所谓的 “子载波”(Subcarriers)在频域中将信号分成一堆互相存在交叉的窄信号。 +OFDM 使用所谓的 “子载波”(Subcarrier)在频域中将信号分成一堆互相存在交叉的窄信号。 为了对抗多径衰落,我们可以避免将数据分配给处于深衰落中的子载波(当然,前提是接收端能快速向发射端告知当前信道状态)。 我们也可以将高阶 QAM 调制方案分配给信道质量良好的子载波,以最大化我们的数据传输速率。 \ No newline at end of file From 793e9c35f64e64fe4521c120a9c0010151a7db93 Mon Sep 17 00:00:00 2001 From: doctormin Date: Wed, 11 Sep 2024 16:30:28 +0800 Subject: [PATCH 2/7] update filter.rst --- content-zh/filters.rst | 541 ++++++++++++++++++++++++----------------- 1 file changed, 318 insertions(+), 223 deletions(-) diff --git a/content-zh/filters.rst b/content-zh/filters.rst index 9293eb42..70f1e6ab 100644 --- a/content-zh/filters.rst +++ b/content-zh/filters.rst @@ -4,27 +4,34 @@ 滤波器 ############# -在本章中,我们将学习使用Python的数字滤波器。我们将介绍滤波器的类型(FIR/IIR和低通/高通/带通/带阻)、滤波器的数字表示方法以及设计方法。最后,我们将介绍脉冲整形,并在 :ref:`pulse-shaping-chapter` 一章中进一步探讨。 +在本章中,我们将学习使用 Python 实现数字滤波器(Digital Filter)。 +本章将涵盖不同类别的滤波器(FIR/IIR, 低通/高通/带通/带阻)的设计和实现。 +最后,我们将介绍脉冲整形(Pulse Shaping),并在之后的 :ref:`pulse-shaping-chapter` 章节中进一步探讨。 ************************* 滤波器基础知识 ************************* -滤波器应用于许多学科。例如,图像处理大量使用二维滤波器,此时输入和输出都是图像。你每天早上煮咖啡时可能会用到“滤波器”,它可以过滤掉液体中的固体。在数字信号处理中,滤波器主要用于: - +滤波器(Filters)在许多学科中被使用。 +例如,图像处理领域大量使用以图像为输入输出的 2D 滤波器。 +甚至每天早上冲咖啡时我们也会用到过滤器(译者注:英文里都是 Filter)来从液体中滤除固体。 +在 DSP(数字信号处理)中,滤波器主要用于: + 1. 分离已合并的信号(例如,提取所需的信号) 2. 接收信号后去除多余噪声 3. 恢复以某种方式失真的信号(例如,音频均衡器就是一种滤波器) - + 当然,滤波器还有其他用途,但本章旨在介绍这一概念,而不是解释所有可能用到的过滤器。 - -也许你会认为我们只关心数字滤波器,毕竟本书探讨的是数字信号处理。但实际上很多滤波器都是模拟的,比如我们的SDR中位于接收端模数转换器 (ADC) 之前的那些滤波器。下图并列显示了模拟滤波器的电路原理图和数字滤波算法的流程图。 + +也许你会认为我们只关心数字滤波器,毕竟本书探讨的是数字信号处理。 +但实际上很多滤波器都是模拟(Analog)的,比如我们的 SDR 中位于接收端模数转换器(ADC)之前的那些滤波器。 +下图并列显示了模拟滤波器的电路原理图和数字滤波算法的流程图。 .. image:: ../_images/analog_digital_filter.png - :scale: 70 % + :scale: 70 % :align: center :alt: Analog vs digital filters - + 在数字信号处理中,输入和输出都是信号,每个滤波器都有一个输入信号和一个输出信号: .. tikz:: [font=\sffamily\Large, scale=2] @@ -36,16 +43,18 @@ minimum height=2.4cm ] (filter) {Filter}; \draw[<-, very thick] (filter.west) -- ++(-2,0) node[left,align=center]{Input\\(time domain)} ; - \draw[->, very thick] (filter.east) -- ++(2,0) node[right,align=center]{Output\\(time domain)}; + \draw[->, very thick] (filter.east) -- ++(2,0) node[right,align=center]{Output\\(time domain)}; :libs: positioning :xscale: 80 -所以,一个滤波器不能输入两个不同的信号,除非先将这两个信号相加或进行其他操作。同样,输出始终是一个信号,即一维数组。 - -滤波器有四种基本类型:低通、高通、带通和带阻。每种类型的滤波器都会改变信号,使其集中在不同的频率范围内。下面的图展示了每种类型的滤波器会怎样滤除信号中的频率,首先展示的是对正频率区间的处理(这更容易理解),然后是对负频率区间的处理。 +所以,一个滤波器不能输入两个不同的信号,除非先将这两个信号相加或进行其他操作。 +同样,输出始终是一个信号,即一维数组。 + +滤波器有四种基本类型:低通、高通、带通和带阻。每种类型的滤波器都会改变信号,使其集中在不同的频率范围内。 +下面的图展示了每种类型的滤波器会怎样滤除信号中的频率,首先展示的是对正频率区间的处理(这更容易理解),然后是对负频率区间的处理。 .. image:: ../_images/filter_types.png - :scale: 70 % + :scale: 70 % :align: center :alt: Filter types, including low-pass, high-pass, band-pass, and band-stop filtering in the frequency domain @@ -56,7 +65,7 @@
.. This draw the lowpass filter -.. tikz:: [font=\sffamily\large] +.. tikz:: [font=\sffamily\large] \draw[->, thick] (-5,0) -- (5,0) node[below]{Frequency}; \draw[->, thick] (0,-0.5) node[below]{0 Hz} -- (0,5) node[left=1cm]{\textbf{Low-Pass}}; \draw[red, thick, smooth] plot[tension=0.5] coordinates{(-5,0) (-2.5,0.5) (-1.5,3) (1.5,3) (2.5,0.5) (5,0)}; @@ -67,7 +76,7 @@ .. this draws the highpass filter -.. tikz:: [font=\sffamily\large] +.. tikz:: [font=\sffamily\large] \draw[->, thick] (-5,0) -- (5,0) node[below]{Frequency}; \draw[->, thick] (0,-0.5) node[below]{0 Hz} -- (0,5) node[left=1cm]{\textbf{High-Pass}}; \draw[red, thick, smooth] plot[tension=0.5] coordinates{(-5,3) (-2.5,2.5) (-1.5,0.3) (1.5,0.3) (2.5,2.5) (5,3)}; @@ -78,7 +87,7 @@
.. this draws the bandpass filter -.. tikz:: [font=\sffamily\large] +.. tikz:: [font=\sffamily\large] \draw[->, thick] (-5,0) -- (5,0) node[below]{Frequency}; \draw[->, thick] (0,-0.5) node[below]{0 Hz} -- (0,5) node[left=1cm]{\textbf{Band-Pass}}; \draw[red, thick, smooth] plot[tension=0.5] coordinates{(-5,0) (-4.5,0.3) (-3.5,3) (-2.5,3) (-1.5,0.3) (1.5, 0.3) (2.5,3) (3.5, 3) (4.5,0.3) (5,0)}; @@ -89,10 +98,10 @@ .. and finally the bandstop filter -.. tikz:: [font=\sffamily\large] +.. tikz:: [font=\sffamily\large] \draw[->, thick] (-5,0) -- (5,0) node[below]{Frequency}; \draw[->, thick] (0,-0.5) node[below]{0 Hz} -- (0,5) node[left=1cm]{\textbf{Band-Stop}}; - \draw[red, thick, smooth] plot[tension=0.5] coordinates{(-5,3) (-4.5,2.7) (-3.5,0.3) (-2.5,0.3) (-1.5,2.7) (1.5, 2.7) (2.5,0.3) (3.5, 0.3) (4.5,2.7) (5,3)}; + \draw[red, thick, smooth] plot[tension=0.5] coordinates{(-5,3) (-4.5,2.7) (-3.5,0.3) (-2.5,0.3) (-1.5,2.7) (1.5, 2.7) (2.5,0.3) (3.5, 0.3) (4.5,2.7) (5,3)}; :xscale: 100 .. raw:: html @@ -102,16 +111,22 @@ .. .......................... end of filter plots in tikz -每个滤波器都允许在信号中保留某些频率,同时阻断其他频率。滤波器允许通过的频率范围称为“通带”,而“阻带”指的是被阻断的频率范围。就低通滤波器而言,它让信号的低频部分通过,而阻断高频的部分,因此0 Hz始终在通带内。而对于高通和带通滤波器,0 Hz始终处于阻带之内。 - -请不要将这些滤波类型与滤波器的算法实现(如 IIR 与 FIR)混淆。最常见的滤波器类型是低通滤波器(low-pass filter, LPF),因为我们通常以基带表示信号。LPF 允许我们滤除信号“周围”的其它东西,包括多余的噪声和其他信号。 +每个滤波器都允许在信号中保留某些频率,同时阻断其他频率。 +滤波器允许通过的频率范围称为 “通带”,而 “阻带” 指的是被阻断的频率范围。 +就低通滤波器而言,它让信号的低频部分通过,而阻断高频的部分,因此 0 Hz 始终在通带内。而对于高通和带通滤波器,0 Hz 始终处于阻带之内。 + +请不要将这些滤波类型与滤波器的算法实现(如 IIR 与 FIR)混淆。最常见的滤波器类型是低通滤波器(Low-Pass Filter, LPF),因为我们通常以基带表示信号。 +LPF 允许我们滤除信号 “周围” 的其它东西,包括多余的噪声和其他信号。 ************************* 滤波器的表示 ************************* -对于我们下面要讲到的大多数滤波器(称为FIR或有限脉冲响应滤波器),我们可以用一个浮点数组来表示滤波器本身。对于在频域对称的滤波器,这些浮点数会是实数(而非复数),而且往往是奇数个浮点数构成这个数组。我们称这个浮点数组为“滤波器抽头”。我们通常使用 :math:`h` 作为滤波器抽头的符号。下面是一组滤波器抽头的示例,它们定义了一个滤波器: +在使用中,大部分滤波器都是 FIR(Finite Impulse Response,有限脉冲响应)类型的,我们可以用一个浮点数数组来表示滤波器本身。 +对于在频率域中对称的滤波器,这些浮点数将是实数(而不是复数),通常有奇数个。 +我们称这个浮点数数组为 “滤波器抽头(Filter Taps)”。 +我们通常使用 :math:`h` 作为滤波器抽头的符号。以下是一组定义某个滤波器的滤波器抽头的例子: .. code-block:: python @@ -129,60 +144,73 @@ -2.46268845e-03 -1.01714338e-03 1.64604862e-04 8.51595307e-04 1.08410297e-03 9.92977939e-04] -Example Use-Case +使用案例 ######################## -为了解滤波器的使用方法,让我们来看一个例子:将SDR调到某个信号所在的频率,并希望将其与其他信号区分开来。请记住,我们告诉SDR调谐到哪个频率,但SDR捕获的采样是基带信号,这意味着信号将以0 Hz为中心,所以必须记住我们让SDR调谐到了哪个频率。我们获取到的信号可能是这样的: +为了了解滤波器是如何使用的,让我们看一个例子,假设我们将我们的 SDR 调谐到一个现有信号的频率,并且我们想将其与其他信号隔离。 +请记住,SDR 捕获的样本是在基带的,这意味着信号将以 0 Hz 为中心显示。 +以下是可能接收到的信号: .. image:: ../_images/filter_use_case.png - :scale: 70 % + :scale: 70 % :align: center :alt: GNU Radio frequency domain plot of signal of interest and an interfering signal and noise floor -由于信号已经以直流(0 Hz)为中心,自然我们就需要一个低通滤波器。必须选择一个正确的“截止频率”(又称转折频率),它决定了通带和阻带之间的界限。截止频率始终以赫兹(Hz)为单位。在本例中,3 kHz似乎是一个合适的值: +因为我们的信号已经集中在直流(0 Hz)上,所以需要一个低通滤波器。 +选择滤波器就必须确定一个 “截止频率(Cutoff Frequency)”(也称为转折频率 Conor Frequency),这将决定通带在频谱的哪里过渡到阻带。 +截止频率的单位始终是 Hz,在这个例子中,3 kHz 似乎是一个不错的值: .. image:: ../_images/filter_use_case2.png - :scale: 70 % - :align: center + :scale: 70 % + :align: center -不过,按照大多数低通滤波器的工作模式,此时负频率的边界将是-3 kHz。也就是说,它是以直流(0 Hz)为轴对称的(稍后你会知道为什么)。截止频率看起来会是这样的(通带则是截止频率之间的区域): +然而,按照大多数低通滤波器的工作方式,在负频率处也会有一个 -3 kHz 的边界,即它关于直流会表现出对称性(稍后你会明白为什么)。 +所以,我们的截止频率看起来像这样(两者之间的区域就是通带): .. image:: ../_images/filter_use_case3.png - :scale: 70 % - :align: center + :scale: 70 % + :align: center -建立并对信号使用了截止频率为3 kHz的滤波器后,我们得到了 +在创建并应用截止频率为 3 kHz 的滤波器后,我们将得到: .. image:: ../_images/filter_use_case4.png - :scale: 70 % - :align: center + :scale: 70 % + :align: center :alt: GNU Radio frequency domain plot of signal of interest and an interfering signal and noise floor, with interference filtered out -这个滤波后的信号也许不好理解,但是,回想一下之前信号的噪声本底是在-65 dB左右的绿线处,就会豁然开朗了。尽管我们仍然可以看到以10 kHz为中心的干扰信号,但我们已经大大地降低了该信号的功率。现在它已经低于之前的噪声本底!同时,阻带中的大部分噪声也都被消除了。 - -除了截止频率,低通滤波器的另一个主要参数称为“过渡宽度”。过渡宽度的单位也是赫兹,它表示滤波器在通带和阻带之间的过渡区域的宽度,无过渡的切换通带和阻带(过渡宽度为0)实际上是不可能的。 - -来形象的理解一下过渡宽度。在下图中,:green:`绿色` 线表示理想的通带和阻带之间的过渡,其过渡宽度基本上为零。:red:`红色` 线表示实际滤波器的结果,它有一些纹波和一定的过渡宽度。 +这个滤波后的信号看起来可能仍然让人有些困惑。但是,如果你回看原信号,你会发现噪音功率 *原本* 在 -65 dB 左右(图中绿色虚线),而现在已经被大大降低(滤除)。 +尽管在 10 kHz 处的干扰信号仍然部分存在,但是我们已经 *大幅* 降低了它的功率,使其低于背景噪音功率(绿色虚线)。 +可以说,我们滤除了阻带中存在的大部分噪音。 + +除了截止频率,我们的低通滤波器的另一个主要参数称为 “过渡带宽(Transition Width)”。 +过渡带宽同样以 Hz 为单位,指示滤波器在通带和阻带之间转换的速度(译者注:此速度指的是频域上的频率变化速度),因为瞬时转换是不可能的。 + +让我们可视化过渡带宽。在下图中,:green:`绿色` 线代表了在通带和阻带之间过渡的理想响应,它基本上具有零的过渡带宽。:red:`红色` 线展示了一个现实滤波器的结果,它有一些波纹(Ripple)和一定的过渡带宽。 .. image:: ../_images/realistic_filter.png - :scale: 100 % + :scale: 100 % :align: center :alt: Frequency response of a low-pass filter, showing ripple and transition width -你可能想知道为什么不是把过渡宽度设置得越小越好。这主要是因为过渡宽度越小,抽头就越多,而抽头越多,计算量就越大--我们很快就会知道原因所在。在Raspberry Pi上,一个50个抽头的滤波器只需使用1%的CPU而运行一整天。与此同时,50,000 个抽头的滤波器会导致 CPU 爆炸! -通常情况下,我们使用滤波器设计工具,来估算需要的抽头数量,如果太多(例如超过100个),我们就增加过渡宽度。当然,这完全取决于运行滤波器的软硬件的要求和条件。 - -在上面的滤波示例中,我们使用了3 kHz的截止频率和1 kHz的过渡宽度(仅看这些图很难真正看到过渡宽度)。所用的滤波器实际有77个抽头。 - -回到滤波器的表示。尽管我们可以显示滤波器抽头的这一组数字,通常还是在频域上更直观地表示滤波器。我们称之为滤波器的“频率响应”,它展示了滤波器在频域中将起到的作用。下面是我们刚才使用的滤波器的频率响应: +你可以会好奇,我们能否把这个过渡带宽控制的尽可能窄呢?实际上是困难的,因为更小的过渡带宽需要更长的滤波器抽头 --- 更多的计算量。 +举一个具体的例子:一个 50 抽头的滤波器可以在树莓派上运行一整天而只占用 1% 的 CPU,然而一个 50,000 抽头的滤波器则会让你的 CPU 爆满! +通常我们会使用设计工具设计一个滤波器,然后查看它输出了多少抽头,如果太多(例如,超过 100 个),我们会增加过渡带宽。 +当然,具体的取舍取决于应用场景和运行滤波器的硬件。 + +在上面的滤波示例中,我们使用了 3 kHz 的截止频率和 1 kHz 的过渡带宽(仅通过截图其实不容易看出来)。最终的滤波器有 77 个抽头。 + +回到滤波器的表示。尽管我们可以用抽头列表来表示滤波器,但我们通常在频域中以图形方式表示滤波器。 +我们称之为滤波器的 “频率响应(Frequency Response)”,它展示了滤波器对不同频率的行为。 +以下是我们刚刚使用的滤波器的频率响应: .. image:: ../_images/filter_use_case5.png - :scale: 100 % - :align: center + :scale: 100 % + :align: center -请注意,我在这里显示的不是信号--它只是滤波器在频域的表示。这可能不太好理解,但当我们看完示例和代码后,你就会明白了。 - -一个给定的滤波器也可以在时域里表示;它被称为滤波器的“脉冲响应”,因为如果你把一个脉冲通过这个滤波器的话,这就是你在时域中将看到的结果。(搜索“狄拉克三角函数”可了解更多有关脉冲的信息)。对于FIR型滤波器来说,脉冲响应就是抽头本身。对于我们之前使用的77个抽头的滤波器,抽头为 +请注意,上图并不是一个信号,而是滤波器的频域表示。这可能一开始有点难以理解,但随着我们深入后文的示例和代码,它会变得清晰。 + +一个给定的滤波器也可以在时域里表示;它被称为滤波器的 “脉冲响应”,因为如果你把一个脉冲通过这个滤波器的话,这就是你在时域中将看到的结果。(搜索 “狄拉克三角函数” 可了解更多有关脉冲的信息)。 +对于 FIR 型滤波器来说,脉冲响应就是抽头本身。对于我们之前使用的 77 个抽头的滤波器,抽头为 .. code-block:: python @@ -213,7 +241,7 @@ Example Use-Case 0.000906112720258534, 0.0008378280326724052, 0.0005385575350373983, 0.00013669139298144728, -0.00025604525581002235] -尽管我们还没有开始学习设计滤波器,但下面给出了生成这个滤波器的Python代码: +尽管我们还没有开始讲滤波器设计,但可以先看看生成该滤波器的 Python 代码: .. code-block:: python @@ -221,43 +249,46 @@ Example Use-Case from scipy import signal import matplotlib.pyplot as plt - num_taps = 51 # it helps to use an odd number of taps + num_taps = 51 # 使用奇数个抽头 cut_off = 3000 # Hz sample_rate = 32000 # Hz - # create our low pass filter + # 创建低通滤波器 h = signal.firwin(num_taps, cut_off, fs=sample_rate) - # plot the impulse response + # 绘制脉冲响应 plt.plot(h, '.-') plt.show() 只需把这个浮点数组绘制出来,就得到了这个滤波器的脉冲响应: .. image:: ../_images/impulse_response.png - :scale: 100 % + :scale: 100 % :align: center :alt: Example of impulse response of a filter, plotting the taps in the time domain -下面是用于生成上述频率响应的代码。看起来有点复杂,是因为我们还得创建一个储存频率值(x轴)的数组。 +这里是用于生成之前显示的频率响应的代码。它稍微复杂一些,因为我们需要创建以频率为 x 轴的数组。 .. code-block:: python - # plot the frequency response - H = np.abs(np.fft.fft(h, 1024)) # take the 1024-point FFT and magnitude - H = np.fft.fftshift(H) # make 0 Hz in the center - w = np.linspace(-sample_rate/2, sample_rate/2, len(H)) # x axis + # 绘制频率响应 + H = np.abs(np.fft.fft(h, 1024)) # 进行窗口长度为 1024 的 FFT,记录幅度 + H = np.fft.fftshift(H) # 将 0 Hz 移到中间 + w = np.linspace(-sample_rate/2, sample_rate/2, len(H)) # x 轴 plt.plot(w, H, '.-') plt.show() 实数和复数滤波器 ######################## -之前展示的滤波器具有实数的抽头,但抽头也可以是复数的。抽头是实数还是复数,与通过滤波器的是实信号还是复信号无关,也就是说,你可以将复数信号通过实数抽头的滤波器,反之亦然。当抽头为实数时,滤波器的频率响应将以直流(0 Hz)为轴对称。通常情况下,我们在需要不对称时才会使用复数抽头,但这种情况并不常见。 +上文展示过的滤波器都是实数抽头,但是抽头其实也可以是复数。 +抽头是实数还是复数并不一定要与输入信号匹配,也就是说,你可以将复数信号通过具有实数抽头的滤波器,反之亦然。 +当抽头是实数时,滤波器的频率响应将在直流(0 Hz)周围对称。 +一般而言我们只在需要不对称性时才使用复数抽头,但这种情况并不常见。 .. draw real vs complex filter -.. tikz:: [font=\sffamily\Large,scale=2] - \definecolor{babyblueeyes}{rgb}{0.36, 0.61, 0.83} +.. tikz:: [font=\sffamily\Large,scale=2] + \definecolor{babyblueeyes}{rgb}{0.36, 0.61, 0.83} \draw[->, thick] (-5,0) node[below]{$-\frac{f_s}{2}$} -- (5,0) node[below]{$\frac{f_s}{2}$}; \draw[->, thick] (0,-0.5) node[below]{0 Hz} -- (0,1); \draw[babyblueeyes, smooth, line width=3pt] plot[tension=0.1] coordinates{(-5,0) (-1,0) (-0.5,2) (0.5,2) (1,0) (5,0)}; @@ -267,36 +298,42 @@ Example Use-Case \draw[font=\huge\bfseries] (0,2.5) node[above,align=center]{Example Low-Pass Filter\\with Real Taps}; \draw[font=\huge\bfseries] (11,2.5) node[above,align=center]{Example Low-Pass Filter\\with Complex Taps}; -作为复数抽头的一个例子,让我们回到滤波器用例,只不过这次我们想接收另一个干扰信号(无需重新调谐接收机)。这意味着我们需要一个带通滤波器,但不是对称的。我们只想保留(也就是“通过”)7 kHz到13 kHz之间的频率(并不想让-13 kHz到-7 kHz之间的信号通过): +作为复数抽头滤波器的例子,让我们回到之前的滤波使用案例,这次我们想接收另一个干扰信号(而不必重新调谐收音机)。 +这意味着我们需要一个带通滤波器,但是它并不是关于直流对称的。 +我们只想保留(即 “通过”)大约 7 kHz 到 13 kHz 之间的频率(同时不希望通过 -13 kHz 到 -7 kHz 的频率): .. image:: ../_images/filter_use_case6.png - :scale: 70 % - :align: center + :scale: 70 % + :align: center -设计这种滤波器的一个方法是制作一个截止频率为3 kHz的低通滤波器,然后对其进行移频。请记住,对于信号x(t)(时域),我们可以通过乘以:math:`e^{j2\pi f_0t}` 来对它进行移频。在这种情况下,:math:`f_0` 应该是10 kHz,这就将我们的滤波器频率上移了10 kHz。记得在上面的 Python 代码中,:math:`h` 是低通滤波器的滤波器抽头。在下面的程序中,为了创建带通滤波器,只需将这些抽头乘以 :math:`e^{j2/pi f_0t}` ,同时需要根据采样周期(采样率的倒数)创建一个向量来储存时间值: +一种设计方法是首先建立一个截止频率为 3 kHz 的低通滤波器,然后将它频移。 +如果你还记得,我们可以将 :math:`x(t)` 乘以 :math:`e^{j2\pi f_0t}` 来频移 :math:`f_0` 。 +具体而言这里 :math:`f_0` 取 10 kHz。 +前面 Python 示例代码中,:math:`h` 就是我们创建的原本的低通滤波器的抽头。 +为了创建带通滤波器,只需将这些抽头乘以 :math:`e^{j2/pi f_0t}` ,同时需要根据采样周期(采样率的倒数)创建一个向量来储存时间值: .. code-block:: python - # (h was found using the first code snippet) + # (h 来自前文代码示例) - # Shift the filter in frequency by multiplying by exp(j*2*pi*f0*t) - f0 = 10e3 # amount we will shift - Ts = 1.0/sample_rate # sample period - t = np.arange(0.0, Ts*len(h), Ts) # time vector. args are (start, stop, step) - exponential = np.exp(2j*np.pi*f0*t) # this is essentially a complex sine wave + # 通过乘以 exp(j*2*pi*f0*t) 进行频移 + f0 = 10e3 # 频移的量(Hz) + Ts = 1.0/sample_rate # 采样时间 + t = np.arange(0.0, Ts*len(h), Ts) # 时间向量,(start, stop, step) + exponential = np.exp(2j*np.pi*f0*t) # 这里本质上是一个正弦波 - h_band_pass = h * exponential # do the shift + h_band_pass = h * exponential # 将低通滤波器进行频移,得到带通滤波器 - # plot impulse response + # 绘制脉冲响应 plt.figure('impulse') plt.plot(np.real(h_band_pass), '.-') plt.plot(np.imag(h_band_pass), '.-') plt.legend(['real', 'imag'], loc=1) - # plot the frequency response - H = np.abs(np.fft.fft(h_band_pass, 1024)) # take the 1024-point FFT and magnitude - H = np.fft.fftshift(H) # make 0 Hz in the center - w = np.linspace(-sample_rate/2, sample_rate/2, len(H)) # x axis + # 绘制频率响应 + H = np.abs(np.fft.fft(h_band_pass, 1024)) # 进行窗口长度为 1024 的 FFT,记录幅度 + H = np.fft.fftshift(H) # 将 0 Hz 移到中间 + w = np.linspace(-sample_rate/2, sample_rate/2, len(H)) # x 轴 plt.figure('freq') plt.plot(w, H, '.-') plt.xlabel('Frequency [Hz]') @@ -305,12 +342,16 @@ Example Use-Case 脉冲响应和频率响应图如下所示: .. image:: ../_images/shifted_filter.png - :scale: 60 % - :align: center + :scale: 60 % + :align: center -由于我们的滤波器不是在0 Hz附近对称的,因此必须使用复数抽头,进而我们需要两条线来画出这些复数抽头。在左侧图中看到的仍然是脉冲响应。而从频率响应图中才能确认所创建的滤波器是否是我们想要的,即除了以10 kHz为中心的信号外,它能滤除其他所有信号。请再次记住,上图不是实际的信号:它只是滤波器的表示。这有点容易混淆,因为当你将滤波器应用于信号并在频域中画出输出图时,在很多情况下,它看起来会与滤波器本身的频率响应大致相同。 - -如果复数的抽头让你困惑,也不必太担心,因为99%的情况下你都会只用到实数抽头的低通滤波器。 +此时,我们的滤波器不再是关于 0 Hz 对称的,因此其抽头必然为复数。 +在左图(脉冲响应)中,我们需要分别用两条线绘制实部和虚部。 +而右图(频率响应)则清晰地展示了我们的滤波器确实实现了目标中的带通效果 --- 它有效地滤除了集中在 10 kHz 的信号以外的所有内容。 +再次提醒,上述图示 *并不是* 实际信号;它仅仅是滤波器的表示。 +然而,这一区别可能相当微妙,因为在许多情况下,当你将滤波器应用于信号并在频域中绘制输出时,结果可能与滤波器的频率响应看起来大致相同。 + +如果这个小节让你感到困惑,不用担心,99% 的情况下你只需要处理简单的低通滤波器,它们的抽头都是实数的。 .. _convolution-section: @@ -318,56 +359,66 @@ Example Use-Case 卷积 *********** -插播一下卷积算子的介绍。如果您已经熟悉卷积算子,可以跳过这一部分。 - -将两个信号相加是将两个信号合二为一的一种方法。在 :ref:`freq-domain-chapter` 一章中,我们探讨了将两个信号相加时所具有的线性特点。卷积是将两个信号合二为一的另一种方法,但它与简单的相加截然不同。两个信号的卷积就像是将一个信号滑过另一个信号并进行积分。如果你熟悉互相关的计算,卷积与它非常相似。事实上,它在很多情况下都等同于互相关。我们通常使用 :code:`*` 符号来表示卷积,尤其是在数学公式中。 - -我认为,卷积操作最好通过实例来学习。在第一个例子中,我们将两个方波脉冲卷积在一起: +我们将简短地介绍一下卷积运算。如果你已经熟悉它,可以跳过这一部分。 + +将两个信号相加是将它们合并为一个的简单方法。在 :ref:`freq-domain-chapter` 章节中,我们探讨了如何在线性系统中将两个信号相加。 +卷积是另一种将两个信号结合的方法,但与简单相加有很大的不同。 +卷积运算的本质是将一个信号滑动到另一个信号上,然后进行积分。 +如果你熟悉互相关操作,你会发现卷积和它非常相似。实际上,在许多情况下,卷积和互相关是等价的。 +我们通常用 :code:`*` 符号来表示卷积,特别是在数学表达式中。 + +我认为学会卷积操作的最佳方式是通过示例。在这个第一个例子中,我们将介绍两个方形波的卷积: .. image:: ../_images/rect_rect_conv.gif - :scale: 90 % - :align: center - -我们有两个输入信号(一个红色,一个蓝色),卷积的输出显示为黑色。可以看到,输出是一个信号在另一个信号上滑动时两个信号的积分。因为只是滑动积分,所以结果是一个三角形,最大值位于两个方形脉冲完全重合的位置。 - -让我们再看几个卷积的例子: + :scale: 90 % + :align: center + +我们有两个输入信号(一个红色,一个蓝色),其卷积的输出显示为黑色。 +你可以看到,当一个信号滑过另一个信号时,输出是两个信号的积分。 +由于这只是一个滑动积分,结果是一个三角形,最大值出现在两个方形脉冲完美对齐的点。 + +让我们再看看几个卷积的例子: .. image:: ../_images/rect_fat_rect_conv.gif - :scale: 90 % - :align: center + :scale: 90 % + :align: center | .. image:: ../_images/rect_exp_conv.gif - :scale: 90 % - :align: center + :scale: 90 % + :align: center | .. image:: ../_images/gaussian_gaussian_conv.gif - :scale: 90 % - :align: center + :scale: 90 % + :align: center -可以看到高斯函数与高斯函数的卷积是另一个高斯函数,但脉冲宽度变得更宽,振幅更小。 - -由于这种“滑动”的原因,输出信号的长度实际上比输入信号要长。如果一个信号是 :code:`M` 个采样点,另一个信号是 :code:`N` 个采样点,两者的卷积可以产生 :code:`N+M-1` 个采样点。不过,:code:`numpy.convolve()` 等函数可以指定是要整个输出(:code:`max(M, N)` 个样本),还是只要信号完全重叠的部分(也就是 :code:`max(M, N) - min(M, N) + 1` 个采样点,如果你想知道的话)。其实不必过于纠缠这些细节,关键是记住卷积的输出长度不一定等于输入的长度。 - -那么,为什么卷积在数字信号处理中很重要呢?简单来说,要对信号进行滤波,我们只需将滤波器的脉冲响应与信号进行卷积即可。FIR滤波就是一种简单的卷积操作。 +注意,高斯与高斯的卷积仍然是一个高斯函数,但具有更宽的脉冲和更低的幅度。 + +由于这种 “滑动” 特性,输出的长度实际上比输入的长度更长。如果一个信号是 :code:`M` 个样本,另一个信号是 :code:`N` 个样本,这两个信号的卷积可以产生 :code:`N+M-1` 个样本。 +像 :code:`numpy.convolve()` 这样的函数可以指定您想要整个输出( :code:`max(M, N)` 个样本)还是仅仅是信号完全重叠的样本( :code:`max(M, N) - min(M, N) + 1` )。 +有点复杂,但是你其实不需要纠结于这个细节,只需知道卷积的输出长度不仅仅是输入的长度。 + +那么卷积在数字信号处理(DSP)中为什么重要呢?首先,要对信号进行滤波,我们可以简单地取该滤波器的脉冲响应并与信号进行卷积。FIR 滤波实际上就是一个卷积操作。 .. image:: ../_images/filter_convolve.png - :scale: 70 % - :align: center + :scale: 70 % + :align: center + +说滤波器是卷积操作可能有点不好理解,因为我们在前面提到,卷积计算要输入两个信号并输出一个信号。 +可以试着这么理解:我们将滤波器的脉冲响应视为一个信号,而卷积就是一个数学运算符,它对两个一维数组进行运算。 +如果其中一个一维数组是滤波器的脉冲响应,那么另一个一维数组可以是一段输入信号,而输出则是输入信号的被滤波后的结果。 -说滤波器是卷积操作可能有点不好理解,因为我们在前面提到,卷积计算要输入两个信号并输出一个信号。可以试着这么理解:我们将滤波器的脉冲响应视为一个信号,而卷积就是一个数学运算符,它对两个一维数组进行运算。如果其中一个一维数组是滤波器的脉冲响应,那么另一个一维数组可以是一段输入信号,而输出则是输入信号的被滤波后的结果。 - 让我们再看一个例子来帮助理解。在下面的示例中,三角形代表滤波器的脉冲响应,而 :green:`绿色` 信号则是要被滤波的信号。 .. image:: ../_images/convolution.gif - :scale: 70 % - :align: center + :scale: 70 % + :align: center + +:red:`红色` 是滤波后的信号。 -:red:`红色` 是滤波后的信号。 - 问:三角形的脉冲响应是来自什么类型的滤波器? .. raw:: html @@ -387,75 +438,95 @@ Example Use-Case .. math:: (f * g)(t) = \int f(\tau) g(t - \tau) d\tau - -在上式中,:math:`g(t)` 是时间上颠倒并滑过 :math:`f(t)` 的信号或输入,但 :math:`g(t)` 和 :math:`f(t)` 可以互换,仍然是相同的表达式。通常,较短的数组将被当作 :math:`g(t)`。当 :math:`g(t)` 是对称的,即相对于原点翻转时不变,卷积等于互相关,上式可写为 :math:`\int f(\tau) g(t+/tau)`。 + +在上式中,:math:`g(t)` 是时间上颠倒并滑过 :math:`f(t)` 的信号或输入,但 :math:`g(t)` 和 :math:`f(t)` 可以互换,仍然是相同的表达式。 +通常,较短的数组将被当作 :math:`g(t)` 。当 :math:`g(t)` 是对称的,即相对于原点翻转时不变,卷积等于互相关,上式可写为 :math:`\int f(\tau) g(t+/tau)` 。 ************************* 滤波器的实现 ************************* -我们并不会太深入地探讨滤波器的实现,相反,将专注于介绍滤波器的设计(反正你可以在任何编程语言中找到现成可用的实现)。就目前而言,记住一个要点:使用FIR滤波器滤波信号时,就是把脉冲响应(抽头数组)与输入信号卷积即可。在离散信号的世界中,我们使用离散卷积(如下图所示)。标记着b的那些三角形表示抽头。在这个流程图中,三角形上方标有 :math:`z^{-1}` 的正方形表示延迟一个时间步长。 +我们并不会太深入地探讨滤波器的实现,相反,将专注于介绍滤波器的设计(反正你可以在任何编程语言中找到现成可用的实现)。 +就目前而言,记住一个要点:使用 FIR 滤波器滤波信号时,就是把脉冲响应(抽头数组)与输入信号卷积即可。 +在离散信号的世界中,我们使用离散卷积(如下图所示)。 +标记着 :math:`b` 的那些三角形表示抽头。 +在这个流程图中,三角形上方标有 :math:`z^{-1}` 的正方形表示延迟一个时间步长。 .. image:: ../_images/discrete_convolution.png - :scale: 80 % + :scale: 80 % :align: center :alt: Implementation of a finite impulse response (FIR) filter with delays and taps and summations -从这个图里也许你能理解为什么叫滤波器的“抽头(taps)”,与图里所示的滤波器的实现方式有关。 +从这个图里也许你能理解为什么叫滤波器的 “抽头(taps)”,与图里所示的滤波器的实现方式有关。 -FIR与IIR +FIR 与 IIR ############## 数字滤波器有两大类:FIR 和 IIR - -1. 有限脉冲响应(FIR) -2. 无限脉冲响应(IIR) - -我们不对理论进行太深入的研究,但现在只需记住以下几点:FIR滤波器更容易设计,只要使用足够多的抽头,就能实现任何你想要做的事情。IIR滤波器更为复杂,有可能不稳定,但效率更高(对于给定的滤波器,CPU 和内存的使用量更少)。如果有人丢给你一个抽头的浮点数数组,那应该就是一个FIR滤波器。如果他们开始提到“极点(poles)”,那么他们说的就是IIR 滤波器。在本书中,我们只重点关注FIR滤波器。 - -下面是一个频率响应示例,比较的是两个效果几乎一样的FIR和IIR滤波器;它们具有相似的过渡宽度,而我们前面讲过过渡宽度将决定需要多少个抽头。这个FIR滤波器有50个抽头,而IIR滤波器有12个极点,就所消耗的计算资源而言,这就相当于12个抽头。 + +1. 有限脉冲响应(Finite Impulse Response,FIR) +2. 无限脉冲响应(Infinite Impulse Response,IIR) + +我们不对理论进行太深入的研究,但现在只需记住以下几点:FIR 滤波器更容易设计,只要使用足够多的抽头,就能实现任何你想要做的事情。 +IIR 滤波器更为复杂,有可能不稳定,但效率更高(对于给定的滤波器,CPU 和内存的使用量更少)。 +如果有人丢给你一个抽头的浮点数数组,那应该就是一个FIR滤波器。如果他们开始提到 “极点(poles)”,那么他们说的就是 IIR 滤波器。在本书中,我们只重点关注 FIR 滤波器。 + +下面是一个频率响应示例,比较的是两个效果几乎一样的 FIR 和 IIR 滤波器;它们具有相似的过渡带宽,而我们前面讲过过渡带宽将决定需要多少个抽头。 +这个 FIR 滤波器有 50 个抽头,而 IIR 滤波器有 12 个极点,就所消耗的计算资源而言,这就相当于12个抽头。 .. image:: ../_images/FIR_IIR.png - :scale: 70 % + :scale: 70 % :align: center :alt: Comparing finite impulse response (FIR) and infinite impulse response (IIR) filters by observing frequency response -经验告诉我们,要执行大致相同的滤波操作,FIR滤波器所需的计算资源要比IIR滤波器多得多。 - -下面是一些你可能实际用过的FIR和IIR滤波器的例子。 +经验告诉我们,要执行大致相同的滤波操作,FIR 滤波器所需的计算资源要比IIR滤波器多得多。 + +下面是一些你可能实际用过的 FIR 和 IIR 滤波器的例子。 -如果对一个数组进行“滑动平均”,这就是一个抽头全为1的FIR滤波器: -- h = [1 1 1 1 1 1 1 1 1 1],表示窗口大小为10的滑动平均滤波器。它恰好也是一个低通滤波器,为什么?使用全是1的和逐渐衰减到零的抽头有什么区别? +如果对一个数组进行 “滑动平均”,这就是一个抽头全为 1 的 FIR 滤波器: +:math:`- h = [1 1 1 1 1 1 1 1 1 1]` ,表示窗口大小为 10 的滑动平均滤波器。它恰好也是一个低通滤波器,为什么?使用全是 1 的和逐渐衰减到零的抽头有什么区别? .. raw:: html
Answers -滑动平均滤波器是一种低通滤波器,因为它能平滑掉“高频”变化,这通常也是人们使用这种滤波器的原因。使用两端衰减到零的抽头是为了避免输出突然变化,比如被滤波的信号在一段时间内为零,然后突然跃升这种情况。 +滑动平均滤波器是一种低通滤波器,因为它能平滑掉 “高频” 变化,这通常也是人们使用这种滤波器的原因。 +使用两端衰减到零的抽头是为了避免输出突然变化,比如被滤波的信号在一段时间内为零,然后突然跃升这种情况。 .. raw:: html
-现在举一个IIR例子。你平时有没有做过类似这样的操作? +现在举一个 IIR 例子。你平时有没有做过类似这样的操作? - x = x*0.99 + new_value*0.01 + x = x*0.99 + new_value*0.01 -其中0.99和0.01代表数值更新的速度(或衰减率,是一个意思)。这是可以缓慢更新某个变量的一种简便方法,同时无需记住x的历史数值。这实际上是一种低通IIR滤波器。希望从这个例子你能明白为什么IIR滤波器的稳定性不如FIR滤波器。因为历史数值的影响永远不会完全消失! +其中 0.99 和 0.01 代表数值更新的速度(或衰减率,是一个意思)。 +这是可以缓慢更新某个变量的一种简便方法,同时无需记住 x 的历史数值。这实际上是一种低通 IIR 滤波器。 +希望从这个例子你能明白为什么 IIR 滤波器的稳定性不如 FIR 滤波器。 +因为历史数值的影响永远不会完全消失! ************************* -FIR滤波器设计 +FIR 滤波器设计 ************************* -在实践中,大多数人会使用滤波器设计工具或代码中的函数(如 Python/SciPy)来设计滤波器。我们将首先展示在Python中可以实现的功能,然后再介绍第三方工具。我们的重点将放在FIR滤波器上,因为这是当前数字信号处理中最常用的滤波器。 +在实践中,大多数人会使用滤波器设计工具或代码中的函数(如 Python/SciPy)来设计滤波器。 +我们将首先展示在 Python 中可以实现的功能,然后再介绍第三方工具。 +我们的重点将放在 FIR 滤波器上,因为这是当前数字信号处理中最常用的滤波器。 -Python程序 +Python 代码 ################# -设计滤波器就是要得到满足我们所需的频率相应的抽头,其中关键一环是要确定滤波器的类型(低通、高通、带通或带阻)、截止频率、抽头数量以及过渡宽度。 - -在SciPy中,我们主要使用两个函数来设计FIR滤波器,这两个函数都使用所谓的窗口方法。首先,:code:`scipy.signal.firwin()` 是最直接的;它能给出线性相位FIR滤波器的抽头。该函数需要我们指定抽头数和一个截止频率(用于低通/高通),或者两个截止频率(用于带通/带阻),还可以选择指定过渡宽度。如果通过 :code:`fs` 指定采样率,则截止频率和过渡宽度的单位为Hz,但如果不指定采样率,则单位为归一化的Hz(0 至 1 Hz)。默认情况下,:code:`pass_zero` 参数为:code:`True` ,但如果需要高通或带通滤波器,则必须将其设置为:code:`False` ;它表示通带中是否应包含0 Hz。建议使用奇数个抽头,101个抽头是一个很好的起点。例如,让我们生成一个采样率为1 MHz、通带范围为100 kHz至200 kHz 的带通滤波器: +设计滤波器就是要得到满足我们所需的频率相应的抽头,其中关键一环是要确定滤波器的类型(低通、高通、带通或带阻)、截止频率、抽头数量以及过渡带宽。 + +在 SciPy 中,我们主要使用两个函数来设计 FIR 滤波器,这两个函数都使用所谓的窗口方法。 +首先, :code:`scipy.signal.firwin()` 是最直接的,它能给出线性相位FIR滤波器的抽头。 +该函数需要我们指定抽头数和一个截止频率(用于低通/高通),或者两个截止频率(用于带通/带阻),还可以选择指定过渡带宽。 +如果通过 :code:`fs` 指定采样率,则截止频率和过渡带宽的单位为Hz,但如果不指定采样率,则单位为归一化的 Hz(0 至 1 Hz)。 +默认情况下, :code:`pass_zero` 参数为 :code:`True` ,但如果需要高通或带通滤波器,则必须将其设置为 :code:`False` ;它表示通带中是否应包含 0 Hz。 +建议使用奇数个抽头,101 个抽头是一个很好的起点。 +例如,让我们生成一个采样率为 1 MHz、通带范围为 100 kHz 至 200 kHz 的带通滤波器: .. code-block:: python @@ -464,7 +535,9 @@ Python程序 h = firwin(101, [100e3, 200e3], pass_zero=False, fs=sample_rate) print(h) -第二个函数是 :code:`scipy.signal.firwin2()`,它更加灵活,可用于设计具有自定义频率响应的滤波器,因为可以指定多个频率以及每个频率上所需的增益。它也要求提供抽头数,并支持上一段提到的 :code:`fs` 参数。例如,下面的代码生成的滤波器具有一个低通区域,使100 kHz以下的频率可以通过,还具有一个带通区域,200 kHz至300 kHz,但这里的增益是低通区域的一半,同时要求过渡宽度为 10 kHz: +第二个函数是 :code:`scipy.signal.firwin2()` ,它更加灵活,可用于设计具有自定义频率响应的滤波器,因为可以指定多个频率以及每个频率上所需的增益。 +它也要求提供抽头数,并支持上一段提到的 :code:`fs` 参数。 +例如,下面的代码生成的滤波器具有一个低通区域,使 100 kHz以下的频率可以通过,还具有一个带通区域,200 kHz 至 300 kHz,但这里的增益是低通区域的一半,同时要求过渡带宽为 10 kHz: .. code-block:: python @@ -475,71 +548,77 @@ Python程序 h2 = firwin2(101, freqs, gains, fs=sample_rate) print(h2) -要在信号上实际使用FIR滤波器,有以下几个函数可选择使用,但它们实际上都是在要滤波的采样点和上面生成的滤波器抽头之间进行卷积操作: +要在信号上实际使用 FIR 滤波器,有以下几个函数可选择使用,但它们实际上都是在要滤波的采样点和上面生成的滤波器抽头之间进行卷积操作: - :code:`np.convolve` - :code:`scipy.signal.convolve` - :code:`scipy.signal.fftconvolve` - :code:`scipy.signal.lfilter` -上述基于卷积的函数都有一个 :code:`mode`参数,可以选择 :code:`'full'`、:code:`'valid'` 或 :code:`'same'`,它们的作用是会影响函数输出数据量的大小。因为在执行卷积时,正如我们在本章前面所看到的,在开始和结束时会产生过渡值。使用 :code:`'valid'`选项将不会产生这些过渡值,输出数据的大小就会比输入信号略小。如果使用 :code:`'same'`选项,输出数据将与输入信号大小相同,这在追踪时间或其他时域信号特征时非常有用。最后,:code:`'full'` 选项将包括所有的过渡值,输出完整的卷积结果。 - -现在,我们将基于 :code:`scipy.signal.firwin2()` 生成的抽头,使用上面的四个函数处理一个由高斯白噪声构成的测试信号。请注意,:code:`lfilter` 有一个额外的参数(第二个参数),对于 FIR 滤波器来说,这个参数总是1。 +上述基于卷积的函数都有一个 :code:`mode` 参数,可以选择 :code:`'full'`、:code:`'valid'` 或 :code:`'same'` ,它们的作用是会影响函数输出数据量的大小。 +因为在执行卷积时,正如我们在本章前面所看到的,在开始和结束时会产生过渡值。使用 :code:`'valid'` 选项将不会产生这些过渡值,输出数据的大小就会比输入信号略小。 +如果使用 :code:`'same'` 选项,输出数据将与输入信号大小相同,这在追踪时间或其他时域信号特征时非常有用。最后,:code:`'full'` 选项将包括所有的过渡值,输出完整的卷积结果。 + +现在,我们将基于 :code:`scipy.signal.firwin2()` 生成的抽头,使用上面的四个函数处理一个由高斯白噪声构成的测试信号。 +请注意, :code:`lfilter` 有一个额外的参数(第二个参数),对于 FIR 滤波器来说,这个参数总是1。 .. code-block:: python import numpy as np from scipy.signal import firwin2, convolve, fftconvolve, lfilter - # Create a test signal, we'll use Gaussian noise + # 创建一个信号用于测试,我们在这里采用高斯噪音 sample_rate = 1e6 # Hz N = 1000 # samples to simulate x = np.random.randn(N) + 1j * np.random.randn(N) - # Create an FIR filter, same one as 2nd example above + # 创建一个 FIR 滤波器,与上文第二个示例相同 freqs = [0, 100e3, 110e3, 190e3, 200e3, 300e3, 310e3, 500e3] gains = [1, 1, 0, 0, 0.5, 0.5, 0, 0] h2 = firwin2(101, freqs, gains, fs=sample_rate) - # Apply filter using the four different methods + # 使用四种方法应用滤波器 x_numpy = np.convolve(h2, x) x_scipy = convolve(h2, x) # scipys convolve x_fft_convolve = fftconvolve(h2, x) x_lfilter = lfilter(h2, 1, x) # 2nd arg is always 1 for FIR filters - # Prove they are all giving the same output + # 验证它们的输出均相同 print(x_numpy[0:2]) print(x_scipy[0:2]) print(x_fft_convolve[0:2]) print(x_lfilter[0:2]) -上述代码展示了这四个函数的基本用法,但你可能想知道哪种方法最好。当这四种方法在英特尔酷睿 i9-10900K 上运行时,下面的图显示了使用不同数量的抽头时所耗费的计算时间,两个图分别是处理1k个和100k个采样点的情况。 +上述代码展示了这四个函数的基本用法,但你可能想知道哪种方法最好。 +当这四种方法在英特尔酷睿 i9-10900K 上运行时,下面的图显示了使用不同数量的抽头时所耗费的计算时间,两个图分别是处理 1k 个和 100k 个采样点的情况。 .. image:: ../_images/convolve_comparison_1000.svg - :align: center + :align: center :target: ../_images/convolve_comparison_1000.svg .. image:: ../_images/convolve_comparison_100000.svg - :align: center + :align: center :target: ../_images/convolve_comparison_100000.svg -可以看到,:code:`scipy.signal.convolve` 实际上是在输入数据达到一定大小时自动切换为基于FFT的方法。总的来说,对于给定的抽头和输入数据量(代表射频应用中相当典型的数据量大小), :code:`fftconvolve` 都是明显的赢家。PySDR 中的许多代码实际上都使用 :code:`np.convolve:`,这只是因为它不需要导入scipy库,并且对于低数据率或非实时应用来说,性能差异可以忽略不计。 - -最后,我们将在频域显示输出,以便最终确认firwin2方法是否为我们提供了与设计参数相匹配的滤波器。从上面生成 :code:`h2` 的代码开始: +可以看到,:code:`scipy.signal.convolve` 实际上是在输入数据达到一定大小时自动切换为基于 FFT 的方法。 +总的来说,对于给定的抽头和输入数据量(代表射频应用中相当典型的数据量大小), :code:`fftconvolve` 都是明显的赢家。 +PySDR 中的许多代码实际上都使用 :code:`np.convolve:` ,这只是因为它不需要导入 scipy 库,并且对于低数据率或非实时应用来说,性能差异可以忽略不计。 + +最后,我们将在频域显示输出,以便最终确认 firwin2 方法是否为我们提供了与设计参数相匹配的滤波器。从上面生成 :code:`h2` 的代码开始: .. code-block:: python - # Simulate signal comprising of Gaussian noise - N = 100000 # signal length - x = np.random.randn(N) + 1j * np.random.randn(N) # complex signal + # 创建模拟信号,包含高斯噪音 + N = 100000 # 信号长度 + x = np.random.randn(N) + 1j * np.random.randn(N) # 复数信号 - # Save PSD of the input signal + # 保存输入信号的 PSD PSD_input = 10*np.log10(np.fft.fftshift(np.abs(np.fft.fft(x))**2)/len(x)) - # Apply filter + # 应用滤波器 x = fftconvolve(x, h2, 'same') - # Look at PSD of the output signal + # 查看输出信号的 PSD PSD_output = 10*np.log10(np.fft.fftshift(np.abs(np.fft.fft(x))**2)/len(x)) f = np.linspace(-sample_rate/2/1e6, sample_rate/2/1e6, len(PSD_output)) plt.plot(f, PSD_input, alpha=0.8) @@ -552,48 +631,59 @@ Python程序 plt.savefig('../_images/fftconvolve.svg', bbox_inches='tight') plt.show() -我们可以看到,带通部分比低通部分低3分贝: +我们可以看到,带通部分比低通部分低 3 dB : .. image:: ../_images/fftconvolve.svg - :align: center + :align: center :target: ../_images/fftconvolve.svg -另外,还有一个不起眼的函数用于对信号进行滤波,叫做 :code:`scipy.signal.filtfilt`。它实现的是“零相位滤波”,有助于保留时域波形中的一些特征,使这些特征在滤波后和滤波前的信号中出现的位置保持不变。它通过使用两次滤波器抽头来实现这一点,正向使用一次,然后再反向使用一次。此时,频率响应将是正常情况下的平方。更多信息,请参阅 https://www.mathworks.com/help/signal/ref/filtfilt.html 或 https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.filtfilt.html。 +另外,还有一个不起眼的函数用于对信号进行滤波,叫做 :code:`scipy.signal.filtfilt`。 +它实现的是 “零相位滤波”,有助于保留时域波形中的一些特征,使这些特征在滤波后和滤波前的信号中出现的位置保持不变。 +它通过使用两次滤波器抽头来实现这一点,正向使用一次,然后再反向使用一次。 +此时,频率响应将是正常情况下的平方。 +更多信息,请参阅 https://www.mathworks.com/help/signal/ref/filtfilt.html 或 https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.filtfilt.html。 -状态滤波 -################## +状态滤波(Stateful Filtering) +############################### -如果你正在创建一个实时应用程序,需要依次对采样数据块应用滤波函数,这时,能保存状态的滤波器就会非常有用。使用这种滤波器时,可以用上一次调用滤波器的输出提供这一次调用的初始条件。这样可以消除信号开始和结束时出现的过渡变化(毕竟,在程序连续运行时,输入滤波器的采样数据块实际是连续的)。状态必须在两次调用之间保存,而且还必须在代码一开始就初始化,以便进行第一次滤波器的调用。幸运的是,SciPy中的 :code:`lfilter_zi` 可以为 lfilter 构造初始条件。下面展示一个使用有状态滤波器处理连续采样数据的示例: +如果你正在创建一个实时应用程序,需要依次对采样数据块应用滤波函数,这时,能保存状态的滤波器就会非常有用。 +使用这种滤波器时,可以用上一次调用滤波器的输出提供这一次调用的初始条件。 +这样可以消除信号开始和结束时出现的过渡变化(毕竟,在程序连续运行时,输入滤波器的采样数据块实际是连续的)。 +状态必须在两次调用之间保存,而且还必须在代码一开始就初始化,以便进行第一次滤波器的调用。 +幸运的是,SciPy 中的 :code:`lfilter_zi` 可以为 lfilter 构造初始条件。 +下面展示一个使用有状态滤波器处理连续采样数据的示例: .. code-block:: python b = taps - a = 1 # for FIR, but non-1 for IIR - zi = lfilter_zi(b, a) # calc initial conditions + a = 1 # 对于 FIR 是 1,对于 IIR 不是 1 + zi = lfilter_zi(b, a) # 计算初始条件 while True: - samples = sdr.read_samples(num_samples) # Replace with your SDR's receive samples function - samples_filtered, zi = lfilter(b, a, samples, zi=zi) # apply filter + samples = sdr.read_samples(num_samples) # 请换成你自己的 SDR 设备的采样代码 + samples_filtered, zi = lfilter(b, a, samples, zi=zi) # 应用滤波器 第三方工具 ####################### -还可以使用Python以外的工具来设计自定义FIR滤波器。对于学生,我推荐Peter Isza设计的这款简单易用的网络应用程序,它可以显示脉冲响应和频率响应:http://t-filter.engineerjs.com。使用默认值,至少在撰写本文时,它就会设计一个低通滤波器,通带为 0至 400 Hz,阻带为 500 Hz 及以上。因为采样率为 2 kHz,所以我们能“看到”的最大频率为 1 kHz。 +还可以使用 Python 以外的工具来设计自定义 FIR 滤波器。 +对于学生,我推荐 Peter Isza 设计的这款简单易用的网络应用程序,它可以显示脉冲响应和频率响应:http://t-filter.engineerjs.com。 +使用默认值,至少在撰写本文时,它就会设计一个低通滤波器,通带为 0 至 400 Hz,阻带为 500 Hz 及以上。因为采样率为 2 kHz,所以我们能 “看到” 的最大频率为 1 kHz。 .. image:: ../_images/filter_designer1.png - :scale: 70 % - :align: center + :scale: 70 % + :align: center -点击“设计滤波器”按钮,创建抽头并绘制频率响应图。 +点击 “设计滤波器” 按钮,创建抽头并绘制频率响应图。 .. image:: ../_images/filter_designer2.png - :scale: 70 % - :align: center + :scale: 70 % + :align: center -单击图上方的“脉冲响应”标签,查看脉冲响应,因为这是一个FIR滤波器,所以这里显示的是抽头的图。 +单击图上方的 “脉冲响应” 标签,查看脉冲响应,因为这是一个 FIR 滤波器,所以这里显示的是抽头的图。 .. image:: ../_images/filter_designer3.png - :scale: 70 % - :align: center + :scale: 70 % + :align: center 该应用程序甚至包括实现和使用该滤波器的 C++ 源代码。网络应用程序不包括任何设计 IIR 滤波器的方法,设计 IIR 滤波器一般要困难得多。 @@ -602,13 +692,13 @@ Python程序 **************************** 现在,我们考虑一种用 Python 自行设计 FIR 滤波器的方法,从所需的频率响应开始,倒推找到脉冲响应。最终显示出我们的滤波器(通过画出其抽头的图)。 - + 首先,创建一个所需的频率响应矢量。让我们设计一个任意形状的低通滤波器,如下图所示: .. image:: ../_images/filter_design1.png - :scale: 70 % - :align: center + :scale: 70 % + :align: center 用于创建此滤波器的代码相当简单: @@ -646,8 +736,8 @@ Python程序 plt.show() .. image:: ../_images/filter_design2.png - :scale: 90 % - :align: center + :scale: 90 % + :align: center 我们将使用上面显示的抽头作为滤波器。我们知道画出抽头就是脉冲响应,所以我们在上面看到的就是脉冲响应。让我们对抽头进行 FFT,看看频域的情况。进行 1,024 点 FFT,以获得高分辨率: @@ -658,22 +748,22 @@ Python程序 plt.show() .. image:: ../_images/filter_design3.png - :scale: 70 % - :align: center + :scale: 70 % + :align: center -看,频率响应并不平直......如果你还记得我们最初设想的滤波器,那么它并不符合我们的期望。其中一个重要原因是我们的脉冲响应没有做到衰减,也就是说,左右两边的频率响应没有达到零。我们有两种方法可以让它衰减为零: +看,频率响应并不平直。。。如果你还记得我们最初设想的滤波器,那么它并不符合我们的期望。其中一个重要原因是我们的脉冲响应没有做到衰减,也就是说,左右两边的频率响应没有达到零。我们有两种方法可以让它衰减为零: -**方法 1:** 我们对当前的脉冲响应加窗,使其两边都衰减为 0。这需要将脉冲响应与一个起点和终点均为 0 的“窗口函数”相乘。 +**方法 1:** 我们对当前的脉冲响应加窗,使其两边都衰减为 0。这需要将脉冲响应与一个起点和终点均为 0 的 “窗口函数” 相乘。 .. code-block:: python - # After creating h using the previous code, create and apply the window + # 上文示例中我们创建了 h,现在创建并应用窗口 window = np.hamming(len(h)) h = h * window .. image:: ../_images/filter_design4.png - :scale: 70 % - :align: center + :scale: 70 % + :align: center **方法 2:** 我们使用更多的点重新生成脉冲响应,使其有时间衰减。我们需要在原始频域数组中增加分辨率(称为内插)。 @@ -684,20 +774,20 @@ Python程序 w = np.linspace(-0.5, 0.5, 500) plt.plot(w, H, '.-') plt.show() - # (the rest of the code is the same) + # (剩下代码同上文示例) .. image:: ../_images/filter_design5.png - :scale: 60 % - :align: center + :scale: 60 % + :align: center .. image:: ../_images/filter_design6.png - :scale: 70 % - :align: center + :scale: 70 % + :align: center .. image:: ../_images/filter_design7.png - :scale: 50 % - :align: center + :scale: 50 % + :align: center 两种方法都可行。你会选择哪一种?第二种方法产生了更多的抽头,但第一种方法产生的频率响应不是很尖锐,下降沿也不是很陡峭。可见,设计滤波器的方法有很多,每种方法都有自己的取舍。许多人认为滤波器设计是一门艺术。 @@ -705,52 +795,57 @@ Python程序 脉冲整形简介 ************************* -我们将简要介绍数字信号处理中一个非常有趣的主题--脉冲整形。后面还有专门的章节会深入探讨这一主题,请参见 :ref:`pulse-shaping-chapter`。在滤波器的部分要提一下脉冲整形,是因为它归根结底是一种滤波器,用于特定目的、具有特殊属性的滤波器。 - -前面我们讲到过,数字信号使用符号来表示一个或多个比特的信息。我们使用 ASK、PSK、QAM、FSK 等数字调制方案对载波进行调制,从而以无线方式发送信息。当我们在:ref:`modulation-chapter`一章中模拟 QPSK 时,我们只模拟了每个符号对应一个采样的情况,也就是说,我们创建的每个复数就是星座上的一个点--它就是一个符号。实际上,我们通常会为每个符号生成多个采样,原因就与滤波有关。 - -我们使用滤波器来修整符号的“形状”,因为改变时域的形状也就改变了频域的形状。信号在频域的形状告诉我们信号将使用多少频谱/带宽,我们通常希望将其最小化。需要理解的是,当我们对载波进行调制时,基带符号的频谱特性(频域)并不会改变;它只是将基带的频率上移,而形状保持不变,这意味着它使用的带宽保持不变。当我们使用每个符号 1 个采样点时,就像是在传输方形脉冲。事实上,使用每个符号 1 个采样的 BPSK 只是一个 1 和 -1 随机出现的方波: +我们将简要介绍数字信号处理中一个非常有趣的主题 --- 脉冲整形。后面还有专门的章节会深入探讨这一主题,请参见 :ref:`pulse-shaping-chapter` 。在滤波器的部分要提一下脉冲整形,是因为它归根结底是一种滤波器,用于特定目的、具有特殊属性的滤波器。 + +前面我们讲到过,数字信号使用符号来表示一个或多个比特的信息。 +我们使用 ASK、PSK、QAM、FSK 等数字调制方案对载波进行调制,从而以无线方式发送信息。 +当我们在 :ref:`modulation-chapter` 一章中模拟 QPSK 时,我们只模拟了每个符号对应一个采样的情况,也就是说,我们创建的每个复数就是星座上的一个点 --- 它就是一个符号。 +实际上,我们通常会为每个符号生成多个采样,原因就与滤波有关。 + +我们使用滤波器来修整符号的 “形状”,因为改变时域的形状也就改变了频域的形状。 +信号在频域的形状告诉我们信号将使用多少频谱/带宽,我们通常希望将其最小化。 +需要理解的是,当我们对载波进行调制时,基带符号的频谱特性(频域)并不会改变;它只是将基带的频率上移,而形状保持不变,这意味着它使用的带宽保持不变。 +当我们使用每个符号 1 个采样点时,就像是在传输方形脉冲。 +事实上,使用每个符号 1 个采样的 BPSK 只是一个 1 和 -1 随机出现的方波: .. image:: ../_images/bpsk.svg - :align: center + :align: center :target: ../_images/bpsk.svg 正如我们已经了解到的,方形脉冲的效率不高,因为它们占用了过量的频谱: .. image:: ../_images/square-wave.svg - :align: center + :align: center -因此,我们要做的就是对这些块状符号进行“脉冲整形”,以减少它们在频域中所占的带宽。我们使用低通滤波器进行“脉冲整形”,因为它能剔除符号中的高频成分。下图显示了在应用脉冲整形滤波器之前和之后,符号在时域(上)和频域(下)中的样子: +因此,我们要做的就是对这些块状符号进行 “脉冲整形”,以减少它们在频域中所占的带宽。我们使用低通滤波器进行 “脉冲整形”,因为它能剔除符号中的高频成分。 +下图显示了在应用脉冲整形滤波器之前和之后,符号在时域(上)和频域(下)中的样子: .. image:: ../_images/pulse_shaping.png - :scale: 70 % - :align: center + :scale: 70 % + :align: center | .. image:: ../_images/pulse_shaping_freq.png - :scale: 90 % + :scale: 90 % :align: center :alt: Demonstration of pulse shaping of an RF signal to reduce occupied bandwidth 请注意看一些频率上的信号能量下降的有多快。脉冲整形后,边带降低了约 30 dB,相当于降低了 1000 倍!更重要的是,主频带更窄了,因此每秒相同比特数信息使用的频谱更少了。 - + 目前,常见的脉冲整形滤波器包括 - + 1. 升余弦滤波器 2. 根升余弦滤波器 3. Sinc 滤波器 4. 高斯滤波器 - + 这些滤波器一般都有一个参数,调整它可以减小所使用的带宽。下图展示了升余弦滤波器在时域和频域的形状,当 :math:`\beta` 取不同值的时候,改变了滚降的陡峭程度。 .. image:: ../_images/pulse_shaping_rolloff.png - :scale: 40 % - :align: center + :scale: 40 % + :align: center 可以看到,:math:`\beta` 的值越小(对于相同的数据量),使用的频谱就越少。但是,如果该值太低,时域符号衰减为零的时间会更长。实际上,当 :math:`\beta=0` 时,符号永远不会完全衰减为零,这意味着我们实际上无法传输这样的符号。常用的 :math:`\beta` 值为 0.35 左右。 - -你将在 :ref:`pulse-shaping-chapter` 章节了解到更多有关脉冲整形的信息,包括脉冲整形滤波器必须满足的一些特殊属性。 - - +你将在 :ref:`pulse-shaping-chapter` 章节了解到更多有关脉冲整形的信息,包括脉冲整形滤波器必须满足的一些特殊属性。 From d2fa0437549d04408ad4c2e31938c8e63a7503e3 Mon Sep 17 00:00:00 2001 From: doctormin Date: Tue, 14 Jan 2025 14:14:41 +0800 Subject: [PATCH 3/7] [zh] update --- _templates/homepage_zh.html | 56 +++++++++++++++++++++++++++++++++ content-zh/filters.rst | 5 +-- content-zh/frequency_domain.rst | 16 +++++----- content-zh/sampling.rst | 14 ++++++--- content-zh/usrp.rst | 27 +++++++++++++++- index-zh.rst | 12 ++++--- 6 files changed, 110 insertions(+), 20 deletions(-) create mode 100644 _templates/homepage_zh.html diff --git a/_templates/homepage_zh.html b/_templates/homepage_zh.html new file mode 100644 index 00000000..db4205fc --- /dev/null +++ b/_templates/homepage_zh.html @@ -0,0 +1,56 @@ +

PySDR:使用 Python 玩转 SDR 和 DSP

+ +

+ by + + Dr. Marc Lichtman + + - + + pysdr@vt.edu + +

+ +

+ 欢迎来到 PySDR,这是一本免费的在线教科书(不是一个 Python 库!),它通过丰富的图表、动画和 Python 代码示例,提供了对无线通信和软件定义无线电(SDR)的友好介绍。从 FFT 到滤波器,再到数字调制,以及在 Python 中使用 SDR 设备进行接收和发送,PySDR 都能满足你的需求! +

+ +

+ PySDR 的目标是越过艰深的数学,用最直观易懂的方式让更多人能够接触到这些传统上只在少数大学中涉及的主题。PySDR 的所有内容都是 + 开源的。 +

+ +

+ 请浏览 + 1. 简介 + 以了解更多关于 PySDR 的信息。 +

+ +

+ 为了快速了解无线信号是什么,请尝试使用下面的模拟器,它展示了由一个正弦波和高斯噪声组成的信号的频域和时域。 +

+ +
+ + + +
+
+ + + +
+
+ +
+
+
+
+ +
+ + + +
diff --git a/content-zh/filters.rst b/content-zh/filters.rst index 70f1e6ab..ff6de7fc 100644 --- a/content-zh/filters.rst +++ b/content-zh/filters.rst @@ -310,7 +310,7 @@ LPF 允许我们滤除信号 “周围” 的其它东西,包括多余的噪 如果你还记得,我们可以将 :math:`x(t)` 乘以 :math:`e^{j2\pi f_0t}` 来频移 :math:`f_0` 。 具体而言这里 :math:`f_0` 取 10 kHz。 前面 Python 示例代码中,:math:`h` 就是我们创建的原本的低通滤波器的抽头。 -为了创建带通滤波器,只需将这些抽头乘以 :math:`e^{j2/pi f_0t}` ,同时需要根据采样周期(采样率的倒数)创建一个向量来储存时间值: +为了创建带通滤波器,只需将这些抽头乘以 :math:`e^{j2\pi f_0t}` ,同时需要根据采样周期(采样率的倒数)创建一个向量来储存时间值: .. code-block:: python @@ -440,7 +440,8 @@ LPF 允许我们滤除信号 “周围” 的其它东西,包括多余的噪 (f * g)(t) = \int f(\tau) g(t - \tau) d\tau 在上式中,:math:`g(t)` 是时间上颠倒并滑过 :math:`f(t)` 的信号或输入,但 :math:`g(t)` 和 :math:`f(t)` 可以互换,仍然是相同的表达式。 -通常,较短的数组将被当作 :math:`g(t)` 。当 :math:`g(t)` 是对称的,即相对于原点翻转时不变,卷积等于互相关,上式可写为 :math:`\int f(\tau) g(t+/tau)` 。 +通常,较短的数组将被当作 :math:`g(t)` 。 +当 :math:`g(t)` 是对称的,即相对于原点翻转时不变,卷积等于互相关,上式可写为 :math:`\int f(\tau) g(t+\tau)` 。 ************************* 滤波器的实现 diff --git a/content-zh/frequency_domain.rst b/content-zh/frequency_domain.rst index 9740bbb9..58b24e3e 100644 --- a/content-zh/frequency_domain.rst +++ b/content-zh/frequency_domain.rst @@ -393,8 +393,8 @@ FFT 函数的行为有点像将输入从时域上压扁然后吐出一个频域 如果我们绘制 :code:`s` ,它将看起来像这样: -.. image:: ../_images/fft-python1.png - :scale: 70 % +.. image:: ../_images/fft-python1.svg + :target: ../_images/fft-python1.svg :align: center 接下来,让我们使用 NumPy 中的 FFT 函数: @@ -410,8 +410,8 @@ FFT 函数的行为有点像将输入从时域上压扁然后吐出一个频域 S = array([-0.01865008 +0.00000000e+00j, -0.01171553 -2.79073782e-01j,0.02526446 -8.82681208e-01j, 3.50536075 -4.71354150e+01j, -0.15045671 +1.31884375e+00j, -0.10769903 +7.10452463e-01j, -0.09435855 +5.01303240e-01j, -0.08808671 +3.92187956e-01j, -0.08454414 +3.23828386e-01j, -0.08231753 +2.76337148e-01j, -0.08081535 +2.41078885e-01j, -0.07974909 +2.13663710e-01j,... 一个提示:无论你在何时何地遇到了复数,请尝试计算其幅度和相位看看能否得到一些有意义的信息。 -在大多数编程语言中,:code:`abs()` 是用来计算复数幅度的函数,而计算相位的函数各不相同, -在 Python 中它是 :code:`np.angle()`。让我们画出来看看: +在大多数编程语言中,:code:`abs()` 是用来计算复数幅度的函数,而计算相位的函数可能各不相同, +在 Python 中,Numpy 提供的 :code:`np.angle()` 将返回以弧度为单位的相位。 .. code-block:: python @@ -421,8 +421,8 @@ FFT 函数的行为有点像将输入从时域上压扁然后吐出一个频域 plt.plot(t,S_mag,'.-') plt.plot(t,S_phase,'.-') -.. image:: ../_images/fft-python2.png - :scale: 80 % +.. image:: ../_images/fft-python2.svg + :target: ../_images/fft-python2.svg :align: center 目前我们没有为这个图提供任何 x 轴信息,它仅仅是数组的索引(从 0 开始计数)。 @@ -474,8 +474,8 @@ FFT 函数的行为有点像将输入从时域上压扁然后吐出一个频域 plt.plot(f, S_phase,'.-') plt.show() -.. image:: ../_images/fft-python5.png - :scale: 80 % +.. image:: ../_images/fft-python5.svg + :target: ../_images/fft-python5.svg :align: center 请注意,我们在 0.15 Hz 处看到了峰值,这正是我们在创建正弦波时使用的频率,这意味着我们的 FFT 算法有效! diff --git a/content-zh/sampling.rst b/content-zh/sampling.rst index 0b8c6894..2b147a78 100644 --- a/content-zh/sampling.rst +++ b/content-zh/sampling.rst @@ -262,6 +262,8 @@ FFT的输出是一个复数数组,每个复数对应一个频域分量,自 在开发接收机时,SDR 将会提供给我们 IQ 数据(采样点)。 在开发发射机时,我们将 IQ 数据输入 SDR,数据类型是复数整数/复数浮点数。 +.. _downconversion-figure: + ************************** 载波和下变频 ************************** @@ -371,7 +373,7 @@ B2x0 USRP 和 PlutoSDR 都包含一个射频集成电路(RFIC),采样率 我们将以 0 Hz 为中心的信号称为“基带信号(Baseband)”。 相反,“带通信号”是指信号在远离 0 Hz 的某个频率上,它是为了无线传输而被向上移动过去的。 注意,没有“基带传输”的概念,因为基带信号只能是虚拟的。 -基带信号正好以 0 Hz 为中心(如上一小节的图中右侧部分),但 *非常接近* 0 Hz (如下图的两个信号)其实也仍被视为基带信号。 +基带信号正好以 0 Hz 为中心(如 :ref:`downconversion-figure` 小节里第二张图的右侧部分),但 *非常接近* 0 Hz (如下图的两个信号)其实也仍被视为基带信号。 右图还示例了一个带通信号,它一个非常高的频率 :math:`f_c` 为中心。 .. image:: ../_images/baseband_bandpass.png @@ -382,11 +384,15 @@ B2x0 USRP 和 PlutoSDR 都包含一个射频集成电路(RFIC),采样率 你可能看到过“中频(IF)”这个术语。目前,你只需把它看作基带信号和带通(射频)信号进行转换时的中间状态。 我们倾向于在基带处创建、记录、分析信号,因为这样可以以较低的采样率开展工作(出于前面小节讨论的原因)。 -需要注意的是,基带信号通常是复数信号,而带通信号(比如我们实际上发射传输的无线电波)是实数信号。 -仔细想一想这很合理:通过天线馈入的信号必须是实数,因为传输“复数/虚数无线电波”没有物理意义。 +需要注意的是,基带信号通常是 **复数** 信号,而带通信号(比如我们实际上发射传输的无线电波)是 **实数** 信号。 +仔细想一想这很合理:通过天线馈入的信号必须是实数,因为真实发射出去的无线电波的瞬时电压值只可能是实数。 若信号的 FFT 结果中负频率和正频率部分不完全相同,那么它一定是复数信号。 负频率不是真的意味着频率是负数,而是代表信号频率在载波频率以下。究其根本是我们发现用复数来记录信号(由此产生了正负频率)很方便。 +如果我们的信号中没有任何虚部,那么我们就没有任何 Q 值(或者你可以认为所有的 Q 值都等于零)。 +这反过来意味着我们只有没有任何相位偏移的余弦信号。 +在频域中绘制时,由于具有相同的正负分量,没有相位偏移的余弦信号之和将关于 y 轴对称。 + 前文我们以 0.7-0.4j 作为复数举例过,实际上基带信号的样本就会长成类似这样。 当你看到复数样本(比如 IQ 采样点)时,它们大多数都是来自基带信号。 信号很少在射频处以数字方式表示或存储,因为这将制造大量的数据(采样率会非常高),并且我们通常只对射频频谱中的一小部分感兴趣。 @@ -418,7 +424,7 @@ LO 泄漏是通过频率组合产生的额外能量,但是消除这种额外 许多射频集成电路(RFICs)内置了自动直流偏移消除功能,但通常需要存在真实的接收信号才能工作。 这就是为什么当不存在信号时,直流峰值会非常明显。 -又一个简单而快速的方法可以解决这个问题,那就是调离中心频率并超采样。 +有一个简单而快速的方法可以解决这个问题,那就是调离中心频率并超采样(这个技巧被称为 *Offset Tuning* )。 举个例子,假设我们想在 100 MHz 的中心频率上查看 5 MHz 带宽的频谱(也就意味着采样率需要至少为 10MHz), 那么我们调离中心频率为 95 MHz ,同时以 20 MHz 的采样率超采样(这意味着能覆盖中心频率附近 10MHz 带宽的频谱)。 diff --git a/content-zh/usrp.rst b/content-zh/usrp.rst index bfa15379..f8659772 100644 --- a/content-zh/usrp.rst +++ b/content-zh/usrp.rst @@ -323,4 +323,29 @@ Python 玩转 USRP 如果以上代码没有按照预期运行,但是又没有报错,你可以尝试把 3.0 改为 1.0 到 5.0 之间的任意值试试。你也可以在调用 :code:`recv()` 后检查元数据,即检查 :code:`if metadata.error_code != uhd.types.RXMetadataErrorCode.none:` 。 -为了 Debug,你可以通过检查 :code:`usrp.get_mboard_sensor("ref_locked", 0)` 的返回值来验证 10 MHz 信号是否传递到了 USRP。而对于 PPS 信号而言,如果它没有传递到 USRP,那么上面代码中的第一个 while 循环将永远不会结束。 \ No newline at end of file +为了 Debug,你可以通过检查 :code:`usrp.get_mboard_sensor("ref_locked", 0)` 的返回值来验证 10 MHz 信号是否传递到了 USRP。而对于 PPS 信号而言,如果它没有传递到 USRP,那么上面代码中的第一个 while 循环将永远不会结束。 + +**** +GPIO +**** + +大多数 USRP 都包含一个 GPIO 接头。在 B200/B210 上,它是 J504 接头,而在 X310 上则位于前面板。 + +为了介绍 GPIO,首先我们定义一些 Ettus 的术语: **CTRL** 设置引脚是否 ATR(automatic,自动)控制(1 表示 ATR 控制,0 表示手动控制)。**DDR** (Data Direction Register, 数据方向寄存器)设置 GPIO 是输出(0)还是输入(1)。 **OUT** 用于手动设置引脚的值(仅在手动 CTRL 模式下使用)。 + +以使用 X310 前面的 “AUX I/O” 接头作为 GPIO 输出为例,更多信息请参见 `此文档 `_ 。 + +.. code-block:: python + + import uhd + import time + usrp = uhd.usrp.MultiUSRP() + usrp.set_gpio_attr('FP0A', 'CTRL', 0x000, 0xFFF) + usrp.set_gpio_attr('FP0A', 'DDR', 0xFFF, 0xFFF) + for i in range(10): + print("Off") + usrp.set_gpio_attr('FP0A', 'OUT', 0x000, 0xFFF) + time.sleep(1) + print("On") + usrp.set_gpio_attr('FP0A', 'OUT', 0xFFF, 0xFFF) + time.sleep(1) \ No newline at end of file diff --git a/index-zh.rst b/index-zh.rst index 566b635e..c6de1c4c 100644 --- a/index-zh.rst +++ b/index-zh.rst @@ -1,11 +1,13 @@ -========================================== -PySDR:使用 Python 玩转 SDR 和 DSP -========================================== +.. raw:: html + :file: _templates/homepage_zh.html -作者: :ref:`Dr. Marc Lichtman` +.. raw:: html + +
+

展开目录

.. toctree:: - :maxdepth: 2 + :maxdepth: 3 :numbered: 1 content-zh/intro From 3ba36129c2c1c19745808ae8dc1043a110aed864 Mon Sep 17 00:00:00 2001 From: doctormin Date: Tue, 14 Jan 2025 14:41:07 +0800 Subject: [PATCH 4/7] [zh] update --- content-zh/iq_files.rst | 109 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/content-zh/iq_files.rst b/content-zh/iq_files.rst index 316fe45f..8ab12c52 100644 --- a/content-zh/iq_files.rst +++ b/content-zh/iq_files.rst @@ -459,3 +459,112 @@ SigMF 记录的名称不一定得是 :code:`channel-0` , :code:`channel-1` 等 4. (可选)与他人分享 :code:`.sigmf` 文件! 最后,如果想读取其中任何数据,只需记住你不必提取 tarball 而是可以直接在其中阅读。 + +********************** +Midas Blue 文件格式 +********************** + +Blue 文件,也称为 BLUEFILES 或 Midas Files,是一种可以表示多种数据结构的文件格式,包括一维和二维数据,并被某些组织用于将原始 RF 信号记录到文件中。 +也就是说,在 RF/SDR 的背景下,Blue 文件可以被视为一种 IQ 文件格式。 +Blue 文件用于 X-Midas 信号处理框架及其衍生产品 Midas 2k(C++),NeXtMidas(Java)和 XMPy(Python)中。 +对于那些听说过 REDAWK 的人,NeXtMidas 的一部分嵌入在其中。 +一些应用程序使用文件扩展名 :code:`.blue` 生成 Blue 文件,而其他应用则使用 :code:`.cdif` ,它们的底层格式是相同的。 + +Blue 文件是二进制文件,按以下顺序包含三个组成部分: + +1. 512 字节的头部(Header),包含文件的元数据; +2. 数据,在我们的例子中是二进制 IQ(以 IQIQIQ 的形式表示的整数或浮点数); +3. 可选的“扩展头”(也称为尾随字节),包含辅助元数据,以任意键/值对的形式。 + +头部中包含的字段在 `此页面 `_ 上有描述。对我们来说重要的有: + +- 字节 52:数据格式代码,两个字符。第一个字符表示是实数(S)还是复数(C)。第二个字符指定数据类型,其中 :code:`B` 是 8 位有符号整数,:code:`I` 是 16 位有符号整数,:code:`L` 是 32 位有符号整数,:code:`F` 是 32 位浮点数,:code:`D` 是 64 位浮点数。 +- 字节 8:数据表示,四个字符,其中 :code:`IEEE` 表示大端序,:code:`EEEI` 表示小端序(最常见)。 +- 字节 24:扩展头开始位置,一个 int32,以 512 字节块为单位。 +- 字节 28:扩展头大小,一个 int32,以字节表示。 +- 字节 264:采样之间的时间间隔,即 1/sample_rate,作为 float64 以秒表示。 + +例如,:code:`CI` 相当于 SigMF 的 :code:`ci16_le` , :code:`CF` 是 SigMF 的 :code:`cf32_le` 。 +尽管扩展头(即尾随字节)的长度和起始位置已指定,偷懒的做法是直接忽略文件的最后几千个 IQ 样本,这样就肯定能避开扩展头,从而避免读取错误的 IQ 值。 + +读取上述字段以及 IQ 数据的 Python 代码如下: + +.. code-block:: python + + import numpy as np + import os + import matplotlib.pyplot as plt + + filename = 'yourfile.blue' # or cdif + + filesize = os.path.getsize(filename) + print('File size', filesize, 'bytes') + with open(filename, 'rb') as f: + header = f.read(512) + + # 解码头部 + dtype = header[52:54].decode('utf-8') # eg 'CI' + endianness = header[8:12].decode('utf-8') # 最好是 'EEEI'!从此以后我们假设它是这样的 + extended_header_start = int.from_bytes(header[24:28], byteorder='little') * 512 # in units of bytes + extended_header_size = int.from_bytes(header[28:32], byteorder='little') + if extended_header_size != filesize - extended_header_start: + print('Warning: extended header size seems wrong') + time_interval = np.frombuffer(header[264:272], dtype=np.float64)[0] + sample_rate = 1/time_interval + print('Sample rate', sample_rate/1e6, 'MHz') + + # 读取 IQ 数据 + if dtype == 'CI': + samples = np.fromfile(filename, dtype=np.int16, offset=512, count=(filesize-extended_header_size)) + samples = samples[::2] + 1j*samples[1::2] # 转换为 IQIQIQ... + + # 绘制每 1000 个数据点,确保没有错误读入非 IQ 数据部分 + print(len(samples)) + plt.plot(samples.real[::1000]) + plt.show() + +“扩展头”(也称为尾随字节),以任意键/值对的形式描述,这些格式在 `Blue 文件格式规范 `_ 的第 3.3 节中有描述。 +它通常包含 RF 频率、增益和接收器/SDR 等信息。下面是解码这些键/值对的 Python 代码,修改自 `此代码 `_ : + +.. code-block:: python + + ... + + # 读取文件末尾的扩展头 + with open(filename, 'rb') as f: + f.seek(filesize-extended_header_size) + ext_header = f.read(extended_header_size) + print("length of extended header", len(ext_header), '\n') + + def parse_extended_header(idx): + next_offset = np.frombuffer(ext_header[idx:idx+4], dtype=np.int32)[0] + non_data_length = np.frombuffer(ext_header[idx+4:idx+6], dtype=np.int16)[0] + name_length = ext_header[idx+6] + dataStart = idx + 8 + dataLength = dataStart + next_offset - non_data_length + midas_to_np = {'O' : np.uint8, 'B' : np.int8, 'I' : np.int16, 'L' : np.int32, 'X' : np.int64, 'F' : np.float32, 'D' : np.float64} + format_code = chr(ext_header[idx+7]) + if format_code == 'A': + val = ext_header[dataStart:dataLength].decode('latin_1') + else: + val = np.frombuffer(ext_header[dataStart:dataLength], dtype=midas_to_np[format_code])[0] + key = ext_header[dataLength:dataLength+name_length].decode('latin_1') + print(key, ' ', val) + return idx + next_offset + + next_idx = 0 + while next_idx < extended_header_size: + next_idx = parse_extended_header(next_idx) + +最后补充说明一点:Blue 文件和其他在同一文件中包含元数据和数据的二进制 IQ 格式是 SigMF 包含一种称为不合格数据集(Non-Conforming Datasets,NCDs)变体的原因,该变体允许在开始和/或结束处有额外字节(二进制 IQ 文件用于元数据)被强制转换为 SigMF 类型的格式。 +有关更多信息,请参阅 SigMF 元数据字段:dataset、header_bytes、trailing_bytes。 +也就是说,仅从数据读取的角度来看,我们可以将 Blue 文件视为普通的二进制 IQ 文件,只要我们忽略前 512 个字节和文件末尾的任何扩展头字节。 + +与 Blue 文件相关的外部资源: + +#. https://web.archive.org/web/20150413061156/http://nextmidas.techma.com/nm/nxm/sys/docs/MidasBlueFileFormat.pdf +#. https://sigplot.lgsinnovations.com/html/doc/bluefile.html +#. https://lgsinnovations.github.io/sigfile/bluefile.js.html +#. http://nextmidas.com.s3-website-us-gov-west-1.amazonaws.com/ +#. https://web.archive.org/web/20181020012349/http://nextmidas.techma.com/nm/htdocs/usersguide/BlueFiles.html +#. https://github.com/Geontech/XMidasBlueReader From 1d54fc3e68068c921a732efeaf8721bcdb61e3ec Mon Sep 17 00:00:00 2001 From: doctormin Date: Fri, 14 Feb 2025 00:08:52 +0800 Subject: [PATCH 5/7] [zh] update --- content-zh/channel_coding.rst | 258 ++++++++++++++++++++++++++++++++++ index-zh.rst | 1 + 2 files changed, 259 insertions(+) create mode 100644 content-zh/channel_coding.rst diff --git a/content-zh/channel_coding.rst b/content-zh/channel_coding.rst new file mode 100644 index 00000000..ce7a2e33 --- /dev/null +++ b/content-zh/channel_coding.rst @@ -0,0 +1,258 @@ +.. _channel-coding-chapter: + +##################### +信道编码 +##################### + + +在本章中,我们介绍信道编码(Channel Coding)的基础知识,包含前向纠错(Forwad Error Correction,FEC)、香农极限(Shannon Limit)、汉明码(Hamming Code)、涡轮码(Turbo Code)和低密度奇偶校验码(LDPC Code)等。 +信道编码是无线通信中的一个巨大领域,是“信息论”的一个分支,信息论研究信息的量化、存储和传输。 + + +*************************** +为什么需要信道编码 +*************************** + +正如我们在 :ref:`noise-chapter` 章节中所学到的,无线信道是嘈杂的,我们的数字符号无法完美地到达接收器。如果你上过网络课程,你可能已经知道循环冗余校验(Cyclic Redundancy Checks,CRCs),它可以在接收端 **检测** 错误。 +信道编码的目的是在接收端检测以及 **纠正** 错误。 +带有差错允许的编码可以让我们在更高阶的调制方案下进行传输而不断链。 +以下星座图是一个很好的可视化案例,显示在相同噪声下的 QPSK(左)和 16-QAM(右)调制。 +QPSK 每个符号编码 2 bits 的信息,而 16-QAM 每个符号编码 4 bits(传输数据率是 QPSK 的两倍)。 +如果我们把接收到的 QPSK 信号可视化在星座图中,当且仅当符号没有越过符号决策边界、或 x 轴 y 轴时,符号才能被正确解码。 +对于 16-QAM 而言,其簇与簇之间间隙更小,更容易重叠,因此符号解码更容易出错。 + + +.. image:: ../_images/qpsk_vs_16qam.png + :scale: 90 % + :align: center + :alt: Comparing noisy QPSK and 16QAM to demonstrate why forward error correction, a.k.a. channel coding, is needed + +使用 CRC 时,我们只能检测错误,而不能纠正错误,因此出错后只能重传。 +信道编码的目的是通过多传输一些精心设计的 **冗余** 信息,让接收端能够纠正错误。 + +在上文中,我们讨论了为何需要信道编码,接下来我们看看信道编码在发射-接收链路中的位置: + +.. image:: ../_images/tx_rx_chain.svg + :align: center + :target: ../_images/tx_rx_chain.svg + :alt: The wireless communications transmit receive chain showing both sides of a transceiver + + +在发射-接收链路中有多步编码过程。 +信源编码(Source Coding)是第一步,其目的是尽可能压缩要传输的数据,就像你压缩文件以减少占用空间一样。 +换句话说,信源编码块的输出应该比数据输入 **小**,但信道编码的输出将比其输入大,因为添加了冗余信息。 + +*************************** +信道编码的种类 +*************************** + +为了进行信道编码,我们使用“纠错码(Error Correction Code)”。 +最基本的编码称为“重复编码”,就是简单地将一个比特连续重复 N 次。对于重复-3 编码,每个比特将传输三次: + +.. role:: raw-html(raw) + :format: html + +- 0 :raw-html:`→` 000 +- 1 :raw-html:`→` 111 + +消息 "10010110" 在信道编码后传输为 "111000000111000111111000"。 + +某些编码在“块”输入位上工作,而其他编码则使用流式方法。 +每次处理固定长度数据的编码称为“块编码”,而处理任意长度数据的编码称为 “卷积编码”。 +这两种是主要的编码类型。我们的重复-3 编码是一个块编码,每个块为三位。 + +顺便提一下,纠错码的应用不仅仅限于无线通信中的信道编码。 +你可能曾经把数据存储在机械硬盘或固态硬盘上,然后发现读取时从未出现过比特错误。 +其实,从内存中写入数据再读出的过程,类似于一个通信系统。 +硬盘和固态硬盘的控制器内部都集成了纠错功能,这个过程对操作系统来说是透明的,而且通常是专有的,因为所有功能都在硬盘或固态硬盘内部实现。 +而对于像 CD 这样的便携式存储介质,纠错标准必须统一:例如,Reed-Solomon 码在 CD-ROM 中非常常见。 + +*************************** +编码率 +*************************** + +所有错误纠正都包含某种形式的冗余。这意味着如果我们想要传输 100 位信息,我们实际上需要发送超过 100 个比特。“编码率(Code-rate)”是信息位数与发送的总位数(即信息加冗余位)的比率。回到重复-3 编码的例子,如果我有 100 位信息,那么我们可以确定以下内容: + +- 300 比特被发送 +- 仅 100 比特表示信息 +- Code-rate = 100/300 = 1/3 + +纠错码的编码率将始终小于 1。它代表了冗余程度和吞吐量的权衡,较低的编码率意味着更多的冗余和更少的吞吐量。 + +*************************** +调制与编码 +*************************** + +在 :ref:`modulation-chapter` 章节中,我们讨论了不同噪音下的调制方式选择。 +当信噪比较低时,需要使用低阶调制方案(比如 QPSK)来应对噪声;而当信噪比较高时,可以使用 256QAM 这样的高阶调制方案,以提高数据传输速率。 +信道编码也是类似的情况:在低信噪比下,应选择较低的码率;而在高信噪比下,则可以使用接近 1 的码率。 +现代通信系统采用了一套组合调制(Modulation)和编码(Coding)方案,称为 MCS(Modulation Coding Scheme)。 +每种 MCS 都规定了在特定信噪比条件下应使用的调制和编码方式。 + +现代通信系统能够根据无线信道的实时状况,动态调整 MCS。 +接收端会向发送端反馈信道的质量信息。 +这些反馈必须在信道质量发生变化之前及时传递,通常在几毫秒内完成。 +通过这种自适应机制,通信系统可以实现最高的数据传输效率,这一技术被广泛应用于 LTE、5G 和 WiFi 等现代通信技术中。 +下面是一个蜂窝基站根据用户距离的变化,在传输过程中调整 MCS 的示意图。 + +.. image:: ../_images/adaptive_mcs.svg + :align: center + :target: ../_images/adaptive_mcs.svg + :alt: Modulation and coding scheme (MCS) visualized using a cellular base station where each ring represents the boundary of a MCS scheme to operate without error + +使用自适应 MCS 时,如果你绘制吞吐量与 SNR 的关系图,你会得到一个阶梯形的曲线,如下图所示。 +像 LTE 这样的协议通常有一个表格,指示在什么 SNR 下应该使用哪种调制和编码。 + +.. image:: ../_images/adaptive_mcs2.svg + :align: center + :target: ../_images/adaptive_mcs2.svg + :alt: Plot of throughput over SNR for various modulation and coding schemes (MCS), leading to a staircase or step shape + +*************************** +汉明码 +*************************** + +让我们来学习一个简单但是精巧的纠错码 --- 汉明码(Hamming Code)。 +1940年代末,Richard Hamming 在贝尔实验室工作,使用一台依赖穿孔纸带的机电计算机。 +当计算机检测到错误时,它会停止运行,需要操作员手动修复。 +汉明对每次遇到错误都要从头开始重启程序感到非常沮丧。 +他抱怨说:“真见鬼,既然机器能检测到错误,为什么不能自动定位并修正这些错误呢?” +于是,他在接下来的几年里开发了汉明码,使计算机能够自动检测并纠正错误。 + +在汉明码中,通过添加一些额外的位,即奇偶校验位或检查位,来增加数据的冗余性。 +这些奇偶校验位位于所有 2 的幂次的位置上,比如 1, 2, 4, 8 等等。 +其他位置则用于存储实际的数据。 +下面的表格用绿色标出了奇偶校验位。 +每个奇偶校验位都会“覆盖”那些与奇偶校验位位置进行按位与运算后结果非零的位,这些位在表格中用红色X标记。 +如果我们想要使用某个数据位,就需要知道哪些奇偶校验位覆盖了它。 +例如,要使用数据位 d9,就需要奇偶校验位 p8 以及所有在它之前的奇偶校验位。 +这个表格可以帮助我们确定对于特定数量的数据位,需要多少个奇偶校验位。 +这种模式可以无限扩展。 + +.. image:: ../_images/hamming.svg + :align: center + :target: ../_images/hamming.svg + :alt: Hamming code pattern showing how parity bit coverage works + +汉明码是一种块编码方式,它每次处理 N 个数据位。 通过使用三个校验位,我们可以每次处理四个数据位的块。 这种错误编码方法被称为 Hamming(7,4),其中第一个数字表示传输的总位数,第二个数字表示实际的数据位数。 + +.. image:: ../_images/hamming2.svg + :align: center + :target: ../_images/hamming2.svg + :alt: Example of Hamming 7,4 which has three parity bits + +以下是汉明码的三个重要特点: + +- 从一个码字转换到另一个码字至少需要改变三位 +- 它能够纠正单个位的错误 +- 它能检测但不能纠正两个位的错误 + +从算法的角度来看,编码过程可以通过简单的矩阵乘法来实现,使用的是一种称为“生成矩阵(Generator Matrix)”的工具。 +在下面的例子中,向量 1011 是要编码的数据,也就是我们希望发送给接收者的信息。 二维矩阵就是生成矩阵,它定义了编码的方式。 +通过矩阵乘法得到的结果就是我们要传输的码字。 + +.. image:: ../_images/hamming3.png + :scale: 60 % + :align: center + :alt: Matrix multiplication used to encode bits with a generator matrix, using Hamming codes + +我们深入探讨汉明码的目的,是为了让大家对错误编码的工作原理有一个初步的了解。 +块码通常遵循类似的模式。 +虽然卷积码的工作方式有所不同,但这里不作详细讨论。 +卷积码通常使用 Trellis 风格的解码方法,这种方法可以用一个类似下图的形式来表示: + +.. image:: ../_images/trellis.svg + :align: center + :scale: 80% + :alt: A trellis diagram or graph is used within convolutional coding to show connection between nodes + +*************************** +软判决解码 vs 硬判决解码 +*************************** + +回想一下,在接收端,信号先经过解调再进行解码。解调器可以告诉我们它认为发送的是哪个符号,也可以输出一个“软”值。 +以 BPSK 为例,解调器不会简单地告诉我们 1 或 0,而是会给出 0.3423 或 -1.1234 这样的“软”值。 +通常,解码方法会设计为使用硬值或软值。 + +- 软判决解码(Soft Decision Decoding) – 输出软值 +- 硬判决解码(Hard Decision Decoding) – 只输出 1 和 0 + +软判决解码更加可靠,因为它利用了所有可用的信息,但实现起来也更为复杂。我们之前讨论的汉明码采用硬判决,而卷积码则通常使用软判决。 + +*************************** +香农极限 +*************************** + +香农极限(Shannon Limit)或香农容量(Shannon Capacity)是一个令人难以置信的理论,它告诉我们信道每秒可以发送多少比特无错误的信息: + +.. math:: + C = B \cdot log_2 \left( 1 + \frac{S}{N} \right) + +- C – 信道容量 [比特/秒(bit/sec)] +- B – 信道带宽 [赫兹(Hz)] +- S – 平均接收信号功率 [瓦特(Watt)] +- N – 平均噪声功率 [瓦特(Watt)] + +这个公式表示了在信噪比足够高以实现无误传输的情况下,任何调制编码方案(MCS)所能达到的最佳性能。 +更直观的是以每赫兹每秒比特数(bit/sec/Hz)来表示这个极限: + +.. math:: + \frac{C}{B} = log_2 \left( 1 + \mathrm{SNR} \right) + +这里 SNR 用线性单位表示(而不是 dB)。 但在绘图时,为了方便,我们通常用 dB 表示 SNR: + +.. image:: ../_images/shannon_limit.svg + :align: center + :target: ../_images/shannon_limit.svg + :alt: Plot of the Shannon Limit in bits per second per Hz over SNR in dB + +如果你在其他地方看到的香农极限图有所不同,可能是它们使用了“每比特能量”或 :math:`E_b/N_0` 作为横坐标,这只是另一种表示 SNR 的方式。 + +当信噪比相当高(例如,10 dB 或更高)时,香农极限可以简化为 :math:`log_2 \left( \mathrm{SNR} \right)` ,这大约等于 :math:`\mathrm{SNR_{dB}}/3` (解释在 `这里 `_ )。 +例如,在 24 dB 的信噪比下,每赫兹每秒可以传输 8 bits,所以如果你有 1 MHz 的带宽,那就是 8 Mbps。 +你可能会想,“这只是理论上的极限”,但现代通信技术已经非常接近这个极限,因此至少它可以给你一个大致的概念。 +你也可以把这个数字减半,以考虑数据包/帧开销和非理想的 MCS。 + +根据规范,802.11n WiFi 在 2.4 GHz 频段(使用 20 MHz 宽的信道)的最大吞吐量是 300 Mbps。 +显然,你可以紧挨着路由器坐,获得极高的信噪比,比如 60 dB,但为了可靠性和实用性,最大吞吐量的调制编码方案(回想上面的阶梯曲线)不太可能需要如此高的信噪比。 +你可以查看一下 `802.11n 的 MCS 列表 `_ 。 +802.11n 最高支持 64-QAM,结合信道编码,根据 `这张表 `_ ,它需要约 25 dB 的信噪比。 +这意味着,即使在 60 分贝的信噪比下,你的 WiFi 仍然会使用 64-QAM 。 +因此,在 25 dB 时,香农极限大约是 8.3 bit/sec/Hz,给定 20 MHz 的带宽,就是 166 Mbps。 +然而,当你考虑到 MIMO 技术(我们将在未来的章节中讨论),你可以并行运行四个这样的数据流,从而达到 664 Mbps。 +将这个数字减半,你得到的结果非常接近 802.11n WiFi 在 2.4 GHz 频段宣传的最大速度 300 Mbps。 + +香农极限背后的证明相当复杂,它涉及的数学公式看起来像这样: + +.. image:: ../_images/shannon_limit_proof.png + :scale: 70 % + :align: center + :alt: Example of the math involved in the Shannon Limit proof + +更多信息可以参考 `这里 `_. + +*************************** +当前最先进的编码技术 +*************************** + +目前,最佳的信道编码方案是: + +1. Turbo 编码,广泛应用于 3G、4G 通信系统以及 NASA 的航天器。 +2. LDPC 编码,用于数字视频广播(DVB-S2)、WiMAX 和 IEEE 802.11n 无线网络标准。 + +这两种编码技术都接近了理论上的香农极限(即,在特定的信噪比条件下几乎达到了这一极限)。 +相比之下,汉明编码等较为简单的编码方式距离香农极限还有很大差距。 +从研究角度来看,编码技术本身已经很难再有大的突破。 +当前的研究重点更多放在如何提高解码的计算效率,以及如何更好地适应信道反馈上。 + +低密度奇偶校验(Low-Density Parity-Check,LDPC)编码是一种非常高效的线性分组编码技术。 +这种编码方法最早由 Robert G. Gallager 在 1960 年的 MIT 博士论文中提出。 +由于其实现的计算复杂度较高,直到 1990 年代才开始受到重视! +撰写本文时(2020 年),加拉格尔先生已经 89 岁高龄,依然健在,并因其开创性的研究获得了多项荣誉(这些荣誉是在他完成这项工作数十年后获得的)。 +LDPC 编码不受专利保护,因此可以免费使用(而 Turbo 编码则需要支付专利费),这也是它被广泛应用于多种开放协议的原因。 + +Turbo 编码则是基于卷积编码的一种技术。 +它通过结合两个或多个简单的卷积编码和一个交织器来实现。 +Turbo 编码的基本专利申请于 1991 年 4 月 23 日提交。 +由于其发明者是法国人,因此当高通公司希望在 3G 的 CDMA 系统中使用 Turbo 编码时,不得不与法国电信达成付费专利许可协议。 +该专利的主要部分已于 2013 年 8 月 29 日到期。 \ No newline at end of file diff --git a/index-zh.rst b/index-zh.rst index c6de1c4c..80c5a042 100644 --- a/index-zh.rst +++ b/index-zh.rst @@ -18,6 +18,7 @@ content-zh/noise content-zh/filters content-zh/link_budgets + content-zh/channel_coding content-zh/iq_files content-zh/multipath_fading content-zh/about_author From e7daa6b076483b83464d2f75cc9697f405d34e82 Mon Sep 17 00:00:00 2001 From: doctormin Date: Fri, 20 Jun 2025 15:58:39 +0800 Subject: [PATCH 6/7] update sampling.rst --- content-zh/sampling.rst | 135 ++++++++++++++++++++++------------------ 1 file changed, 75 insertions(+), 60 deletions(-) diff --git a/content-zh/sampling.rst b/content-zh/sampling.rst index 2b147a78..40065c79 100644 --- a/content-zh/sampling.rst +++ b/content-zh/sampling.rst @@ -31,11 +31,11 @@ SDR 与麦克风惊人地相似,只不过使用天线而不是麦克风接收 :alt: Concept of sampling a signal, showing sample period T, the samples are the blue dots 我们会以固定间隔时间 :math:`T` 秒记录 :math:`S(t)` 的值,这个间隔被称为 **采样周期** 。 -我们采样的频率,即每秒采集的样本数量 :math:`\frac{1}{T}` 称为 **采样率** ,它是采样周期的倒数。 -例如,采样率为 10 Hz 相当于采样周期为 0.1 秒,每个样本之间将会有 0.1 秒的间隔。 +我们采样的频率,即每秒采集的采样点数量 :math:`\frac{1}{T}` 称为 **采样率** ,它是采样周期的倒数。 +例如,采样率为 10 Hz 相当于采样周期为 0.1 秒,每个采样点之间将会有 0.1 秒的间隔。 在实际应用中,采样率通常为数百 kHz 、数十 MHz 乃至更高。当我们对信号进行采样时,需要首先注意采样率,这是一个非常重要的参数。 -有些人可能更喜欢看到公式定义:让 :math:`S_n` 代表样本 :math:`n` , +有些人可能更喜欢看到公式定义:让 :math:`S_n` 代表采样点 :math:`n` , 采样过程可以用数学形式表示为 :math:`S_n = S(nT)` , :math:`n` 是从 0 开始的整数。 即,我们在间隔 :math:`nT` 的一系列时间点上评估模拟信号 :math:`S(t)` 。 @@ -53,7 +53,7 @@ SDR 与麦克风惊人地相似,只不过使用天线而不是麦克风接收 :align: center 上图中的红色虚线代表了一种与原函数不同(且错误的)映射。 -这说明我们的采样率过低,在采样过程中产生了歧义,因为同一组样本数据可能源自两个完全不同的函数。 +这说明我们的采样率过低,在采样过程中产生了歧义,因为同一组采样点数据可能源自两个完全不同的函数。 若要准确重建出原始信号,我们必须避免这种采样歧义。 让我们调高一点采样率试试,比如 :math:` Fs = 1.2f` : @@ -61,7 +61,7 @@ SDR 与麦克风惊人地相似,只不过使用天线而不是麦克风接收 .. image:: ../_images/sampling_Fs_0.36.svg :align: center -可以看到老问题仍然存在:采样点拟合出来的函数还是错的。这种信号重建歧义表明,如果某人提供给我们这些样本数据就是这些,我们并不能重建出真实的原始信号。 +可以看到老问题仍然存在:采样点拟合出来的函数还是错的。这种信号重建歧义表明,如果某人提供给我们这些采样点数据就是这些,我们并不能重建出真实的原始信号。 再高一点试试,比如 Fs = 1.5f: @@ -74,7 +74,7 @@ SDR 与麦克风惊人地相似,只不过使用天线而不是麦克风接收 .. image:: ../_images/sampling_Fs_0.6.svg :align: center -这次重建的信号没有误差是因为我们的采样速率足够快,以致于除了你所看到的信号之外,不存在其他任何其他也能与这些样本吻合的信号 +这次重建的信号没有误差是因为我们的采样速率足够快,以致于除了你所看到的信号之外,不存在其他任何其他也能与这些采样点吻合的信号 (当然,如果考虑更 *高* 频率的情况则另当别论,这一点我们稍后再详细讨论)。 在之前的例子中,我们使用的是一个简单正弦波信号,但现实世界的信号大都包含多个不同的频率分量。 @@ -97,7 +97,7 @@ SDR 与麦克风惊人地相似,只不过使用天线而不是麦克风接收 如果采样速度不够快速,会出现一个称为混叠(Aliasing)的现象,我们后面会详细讲解,现在仅需知道我们必须不惜一切代价避免它。 SDR 设备以及大多数接收机都会在执行采样之前过直接滤掉所有超过 :math:`Fs/2` 的频率成分。 所以如果采样率设置的太低,前置的滤波器甚至直接就把我们想要收集的信号给截断了。 -毕竟,所有的 SDR 设备都会努力地确保提供给我们的样本不受混叠和其它因素的影响! +毕竟,所有的 SDR 设备都会努力地确保提供给我们的采样点不受混叠和其它因素的影响! ************************* 正交采样 @@ -109,7 +109,7 @@ SDR 设备以及大多数接收机都会在执行采样之前过直接滤掉所 为了便于理解和使用,我们通常以一个正弦波和一个余弦波来代表两个相位差为 90 度(正交)的正弦波。 我们还需要定义变量去代表正弦函数和余弦函数的 **幅度** (amplitude)。 -我们将以 :math:`I` 来代表余弦函数 :math:`cos()` 的幅度,用 :math:`Q` 来代表正弦函数 :math:`sin()` 的幅度: +我们将以 I 来代表余弦函数 :math:`cos()` 的幅度,用 Q 来代表正弦函数 :math:`sin()` 的幅度: .. math:: I \cos(2\pi ft) @@ -127,17 +127,51 @@ SDR 设备以及大多数接收机都会在执行采样之前过直接滤掉所 而将 :math:`sin()` 函数称作偏移 90 度相位的“正交”分量,简称为 Q (Quadrature)分量。 不过就算你不慎把 :math:`cos()` 当作了 Q,把 :math:`sin()` 当作了 I,大部分情况下也不会导致结果错误。 -从发射信号的角度看,IQ 分解的原理更容易理解。假设我们想发射特定相位的单一正弦波, -根据三角恒等式 :math:`a \cos(x) + b \sin(x) = A \cos(x-\phi)`, -实际上发送 :math:`sin()` 和 :math:`cos()` 的和就行了(两个分量各自相位为 0)。 -假设 :math:`x(t)` 是我们要发送的信号: +正交采样(又称为 IQ 采样)从发射机的角度更好理解。假如我们要发射一个信号,它的频率是 :math:`f` ,幅度是 :math:`A` ,相位是 :math:`\phi` : .. math:: - x(t) = I \cos(2\pi ft) + Q \sin(2\pi ft) + A \cos(2 \pi f t - \phi) + +哦,对了,这里相位前面的负号只是约定俗成。 +真实的信号往往在不同时刻具有不同的幅度和相位,因此,我们可以把幅度、相位都定义为关于时间的函数,如下面公式所示。 + +.. math:: + A(t) \cos(2 \pi f t - \phi(t)) + +在射频电路中,控制正弦波的幅度很简单,但控制相位却很困难,因此我们可以利用三角恒等式: :math:`a \cos(x) + b \sin(x) = A \cos(x - \phi)` , +它告诉我们可以通过两路特定幅度、初始相位为0、同频率的正弦信号和余弦信号来合成一个具有特定初始相位和幅度的余弦信号。 +在无线领域,我们习惯用 I 来代替上式中的 :math:`a` ,用 Q 来代替上式中的 :math:`b` ,同时带入 :math:`x = 2 \pi f t` ,我们就得到了: + +.. math:: + A \cos(2 \pi f t - \phi) + + = I \cos(2 \pi f t) + Q \sin(2 \pi f t) + +其中: -当我们将正弦函数和余弦函数相加时会发生什么? -或者说,当我们将相位差为90度的两个正弦波相加时会发生什么? -在下面的视频中,有一个滑动条用于调整 I 分量和 Q 分量。绘制的是调整后的余弦函数、正弦函数以及它们的和。 +.. math:: + A = \sqrt{I^2 + Q^2} + + \phi = \tan^{-1}\left(\frac{Q}{I}\right) + +这个数学机制意味着,通过控制 I 和 Q 两个分量,我们就可以合成出任意幅度和相位的余弦波。 +下面的电路就落实了这个思路: + +.. image:: ../_images/IQ_diagram.png + :scale: 80% + :align: center + :alt: Diagram showing how I and Q are modulated onto a carrier + +这意味着,假设我们有一个 IQ 采样点(它是一个复数,即 :math:`I + jQ` ),我们可以将它调制到一个余弦波上,其幅度和相位由这个复数所定义: + +.. math:: + x(t) = I \cos(2\pi ft) + Q \sin(2\pi ft) + + \qquad \qquad \qquad \qquad = \left(\sqrt{I^2+Q^2}\right) \cos\left(2\pi ft - \tan^{-1}\left(\frac{Q}{I}\right)\right) + +了解数学原理后,我们通过可视化来直观感受一下如何将两个相位差 90 度的正弦波相加。 +在下面的视频中,有一个滑块用于调整 I ,还有一个滑块用于调整 Q ,即余弦和正弦的幅度。 +绘制的是余弦(红色)、正弦(蓝色)以及两者的和(绿色)。 .. image:: ../_images/IQ3.gif :scale: 100% @@ -145,22 +179,14 @@ SDR 设备以及大多数接收机都会在执行采样之前过直接滤掉所 :target: ../_images/IQ3.gif :alt: GNU Radio animation showing I and Q as amplitudes of sinusoids that get summed together -(上图是基于 pyqt 制作的,源代码可见 `这里 `_ ) +(上图是基于 pyqt 制作的,源代码可见 `这里 `_ ) -从上文可以得到一个重要的结论,当我们将 :math:`cos()` 和 :math:`sin()` 相加时,我们得到的是另一个具有不同相位和幅度的纯正弦波。 +从上文可以得到一个重要的结论,当我们将 :math:`cos()` 和 :math:`sin()` 相加时,我们得到的是另一个具有不同相位和幅度的余弦波。 此外,随着我们慢慢去除或添加其中的一个分量,它们的和的相位、幅度会发生变化。 -其实三角恒等式已经说明了一切: :math:`a \cos(x) + b \sin(x) = A \cos(x-\phi)` ,我们会稍后回到这个式子。 这种构造的“实用性”在于,我们可以通过调整 I 和 Q 的幅度来控制所得到的正弦波的相位和幅度(同时不需要调整余弦或正弦的相位)。 例如,通过调整 I 和 Q 的值,我们可以输出幅度不变,但是具有任意相位的正弦波。 对于信号发射机而言,这种构造方法非常有用,因为调整两个固定相位正余弦波的幅度并进行加法操作比同时调整一个正弦波的幅度和相位要容易得多。 -我们的信号发射机将看起来像这样: - -.. image:: ../_images/IQ_diagram.png - :scale: 80% - :align: center - :alt: Diagram showing how I and Q are modulated onto a carrier - -我们只需要生成一个正弦波(I 分量),并将其相位移动 90 度,就可以得到 Q 分量。 +这也使我们能够更方便地表示基带信号,使其与载波无关。 ************************* 复数 @@ -247,7 +273,7 @@ FFT的输出是一个复数数组,每个复数对应一个频域分量,自 天线接收到实信号,然后将其分解为 IQ 值。 硬件上其实是通过两个 ADC 分别对 I、 Q 分量进行采样,然后将它们以复数的格式存储。 -换句话说,每隔一个采样时间,设备将采样一个 I 值和一个 Q 值,并以 :math:`I + jQ` 的形式组合起来(即每个 IQ 样本对应到一个复数)存储。 +换句话说,每隔一个采样时间,设备将采样一个 I 值和一个 Q 值,并以 :math:`I + jQ` 的形式组合起来(即每个 IQ 采样点对应到一个复数)存储。 这个采样的频率也就是我们常说的“采样率”。 比如,“我有一个采样率为 2 MHz 的 SDR 设备” 的意思是这个 SDR 设备每秒接收并存储两百万个 IQ 采样点(两百万个复数)。 @@ -258,7 +284,7 @@ FFT的输出是一个复数数组,每个复数对应一个频域分量,自 最后,请记住:上文图表展示的是 SDR 的内部机制。 在现实工作中,我们并不需要手动生成正弦波、平移90度、执行乘法或加法。 -因为 SDR 将替我们完成这些内在操作,我们只需要告诉 SDR 以什么频率接收或传输我们的样本。 +因为 SDR 将替我们完成这些内在操作,我们只需要告诉 SDR 以什么频率接收或传输我们的采样点。 在开发接收机时,SDR 将会提供给我们 IQ 数据(采样点)。 在开发发射机时,我们将 IQ 数据输入 SDR,数据类型是复数整数/复数浮点数。 @@ -271,16 +297,10 @@ FFT的输出是一个复数数组,每个复数对应一个频域分量,自 到目前为止,我们还没有讨论过频率,但我们在涉及 :math:`cos()` 和 :math:`sin()` 的方程中能看到它的身影::math:`f` 。 这个频率是我们发送的信号的中心频率(即电磁波的频率)。 我们将其称为“载波”,因为它在特定的射频频率上“运载”我们的信号。 -当我们用 SDR 调谐到一个频率并接收样本时,我们的信号存储在 I 和 Q 中,假设我们调谐到了这个载波,那么这个载波不会在 I 和 Q 中显示出来。 +当我们用 SDR 调谐到一个频率并接收采样点时,我们的信号仅仅存储在 I 和 Q 中(而与 :math:`f` 无关)! +即假设我们调谐到了这个载波,那么这个载波不会在 I 和 Q 中显示出来。 -.. tikz:: [font=\Large\bfseries\sffamily] - \draw (0,0) node[align=center]{$A\cdot cos(2\pi ft+ \phi)$} - (0,-2) node[align=center]{$\left(\sqrt{I^2+Q^2}\right)cos\left(2\pi ft + tan^{-1}(\frac{Q}{I})\right)$}; - \draw[->,red,thick] (-2,-0.5) -- (-2.5,-1.2); - \draw[->,red,thick] (1.9,-0.5) -- (2.4,-1.5); - \draw[->,red,thick] (0,-4) node[red, below, align=center]{This is what we call the carrier} -- (-0.6,-2.7); - -无线电信号(例如 FM 收音机、Wi-Fi、蓝牙、LTE、GPS 等)通常使用 100 MHz 到 6 GHz 之间的频率的载波。 +作为参考,无线电信号(例如 FM 收音机、Wi-Fi、蓝牙、LTE、GPS 等)通常使用 100 MHz 到 6 GHz 之间的频率的载波。 这些频率下的载波在空气中传输效果非常好,不需要特别长的天线或大量功率来进行传输或接收。 微波炉使用 2.4 GHz 的电磁波来加热食物。如果微波炉的门没关好,泄露的电磁波会对 Wi-Fi 信号产生干扰,甚至可能灼伤皮肤。 另一种电磁波是光,可见光的频率大约为 500 THz。它是如此的高,以至于我们没法使用传统的天线,而是使用半导体器件(如 LED)来传输。 @@ -293,14 +313,11 @@ FFT的输出是一个复数数组,每个复数对应一个频域分量,自 当我们快速改变载波的 IQ 值并传输时,这就是在对载波进行“调制”,调制的节奏和方法就是我们想要传输的数据。 改变 IQ 值等价于改变这一瞬间载波的相位和幅度。当然,也可以选择直接改变载波的频率,这就是常见的调频(FM)广播所采用的方法。 -举一个简单的例子,假设我们传输 IQ 样本 1+0j,然后下一个点切换为 0+1j。 -数学上这意味着从发送 :math:`\cos(2\pi ft)` 改为 :math:`\sin(2\pi ft)`,相当于将载波的相位偏移 90 度。 - -我们在上文谈到了两种无线电波,分别是我们想要传输的信号(通常包含许多频率分量)和我们以另一个频率传输的载波,你可能对此感到很困惑。 +我们在上文谈到了两种无线电波,分别是我们想要传输的(通常包含许多频率分量)信号和单一频率的载波,你可能对此感到很困惑。 因此接下来我们将讨论基带信号与带通信号之间的区别,希望能厘清这一点。 -在此之前,我们还需要重提采样的问题。 -与其将信号直接乘以 :math:`cos()` 和 :math:`sin()` 然后记录 IQ 值来接收样本, +现在让我们暂且回到采样的问题。 +与其将信号直接乘以 :math:`cos()` 和 :math:`sin()` 然后记录 IQ 值来接收采样点, 将信号从天线先传入一个单一的 ADC(就像我们先前讨论的直接采样架构中那样)会不会更好? 毕竟,假设载波频率是 2.4 GHz(比如 Wi-Fi 或蓝牙)的,那么根据奈奎斯特采样定理,我们必须以至少 4.8 GHz 的速度采样。 这个采样率高得吓人,能满足这一要求的 ADC 将价值数千美元! @@ -308,11 +325,9 @@ FFT的输出是一个复数数组,每个复数对应一个频域分量,自 这种降频发生在采样之前,我们将从: .. math:: - I \cos(2\pi ft) - - Q \sin(2\pi ft) + I \underbrace{\cos(2\pi ft)}_{carrier} \ + \ \ Q \underbrace{\sin(2\pi ft)}_{carrier} -转为收集单纯的 I、Q 值 +转为收集单纯的 I、Q 值。 让我们在频域可视化这一下变频的过程: @@ -393,8 +408,8 @@ B2x0 USRP 和 PlutoSDR 都包含一个射频集成电路(RFIC),采样率 这反过来意味着我们只有没有任何相位偏移的余弦信号。 在频域中绘制时,由于具有相同的正负分量,没有相位偏移的余弦信号之和将关于 y 轴对称。 -前文我们以 0.7-0.4j 作为复数举例过,实际上基带信号的样本就会长成类似这样。 -当你看到复数样本(比如 IQ 采样点)时,它们大多数都是来自基带信号。 +前文我们以 0.7-0.4j 作为复数举例过,实际上基带信号的采样点就会长成类似这样。 +当你看到复数采样点(比如 IQ 采样点)时,它们大多数都是来自基带信号。 信号很少在射频处以数字方式表示或存储,因为这将制造大量的数据(采样率会非常高),并且我们通常只对射频频谱中的一小部分感兴趣。 *************************** @@ -459,7 +474,7 @@ LO 泄漏是通过频率组合产生的额外能量,但是消除这种额外 ************************* 在进行 DSP 前首先应确定信号的存在性,为此人们常常会先计算信号的功率。 -对于离散复信号,也就是我们常见的采样样本点,我们可以通过求每个采样点的幅度的平方并求取平均来得到平均功率。 +对于离散复信号,也就是我们常见的采样采样点点,我们可以通过求每个采样点的幅度的平方并求取平均来得到平均功率。 .. math:: P = \frac{1}{N} \sum_{n=1}^{N} |x[n]|^2 @@ -473,7 +488,7 @@ LO 泄漏是通过频率组合产生的额外能量,但是消除这种额外 avg_pwr = np.mean(np.abs(x)**2) 计算平均功率还有一个小妙招。 -如果信号均值约等于 0 (SDR 设备采集的通常是这样,你稍后会看到为什么),那么信号功率可以通过计算样本方差得到。 +如果信号均值约等于 0 (SDR 设备采集的通常是这样,你稍后会看到为什么),那么信号功率可以通过计算采样点方差得到。 这种方法用 Python 写出来就是: .. code-block:: python @@ -484,7 +499,7 @@ LO 泄漏是通过频率组合产生的额外能量,但是消除这种额外 背后的数学原理其实很简单::math:`\frac{1}{N}\sum^N_{n=1} |x[n]-\mu|^2` , 其中 :math:`\mu` 是信号的均值。 你会发现这个公式很眼熟!如果 :math:`\mu` 是 0 ,那么计算方差的公式和计算平均功率的公式就等价了。 -对于哪些均值不为 0 的样本点而言,先减去它们的均值再求方差当然也行。 +对于哪些均值不为 0 的采样点点而言,先减去它们的均值再求方差当然也行。 只需要记住,当信号均值不为 0 的时候,直接计算方差并不等于平均功率。 ********************************** @@ -495,7 +510,7 @@ LO 泄漏是通过频率组合产生的额外能量,但是消除这种额外 许多 DSP 算法都是在频域中进行的,而 PSD 是一种可以将频域信息可视化的极有用的工具。 计算 PSD 并绘制出来并不容易,光有 FFT 是不够的,我们必须进行以下六个步骤来得到 PSD: -1. 对样本进行 FFT。如果我们有 x 个样本,默认情况下 FFT 窗口大小就是 x。 +1. 对采样点进行 FFT。如果我们有 x 个采样点,默认情况下 FFT 窗口大小就是 x。 举个例子,假设我们取一段信号的前 1024 个采样点作为 FFT 的输入,那么 FFT 窗口大小就是 1024,得到的输出就是 1024 个复数。 2. 取 FFT 输出序列的幅度,得到 1024 个实数浮点数。 3. 将得到的幅度平方,得到功率。 @@ -525,7 +540,7 @@ LO 泄漏是通过频率组合产生的额外能量,但是消除这种额外 绘制 PSD 图需要知道 x 轴的取值。 在上一章我们学过,对信号进行采样后只能“看到” :math:`-Fs/2` 和 :math:`Fs/2` 之间的频谱。 -频域分辨率取决于 FFT 的窗口大小,其默认等于我们的输入样本数。 +频域分辨率取决于 FFT 的窗口大小,其默认等于我们的输入采样点数。 在本例中,我们的 x 轴是 -0.5 MHz 和 0.5 MHz 之间均匀间隔的 1024 个点构成的。 如果我们将 SDR 调谐到 2.4 GHz,那么频谱上的观察窗口将为 2.3995 GHz 到 2.4005 GHz。 在 Python 中,获取观察窗口并移位的操作如下: @@ -540,14 +555,14 @@ LO 泄漏是通过频率组合产生的额外能量,但是消除这种额外 然后应该就能看到漂亮的 PSD 图像了! -如果你想要计算包含一百万个样本点的 PSD,不要傻乎乎地直接把 FFT 窗口大小设置为一百万,因为这要花很长时间。 +如果你想要计算包含一百万个采样点点的 PSD,不要傻乎乎地直接把 FFT 窗口大小设置为一百万,因为这要花很长时间。 毕竟,它的输出会包含一百万个频率间隔(Frequency Bin),这太密了,画图都够呛。 相反,我建议进行多个较小的 PSD 并将它们平均化,或者使用时频谱来一起显示它们。 -另外,如果你知道你的信号变化不快,只抽样其中几千个样本来算 PSD 也可以。 -在抽样区间内虽然只有几千个样本),但可能也足够表示频谱特性了。 +另外,如果你知道你的信号变化不快,只抽样其中几千个采样点来算 PSD 也可以。 +在抽样区间内虽然只有几千个采样点),但可能也足够表示频谱特性了。 以下是一个包括信号(50 Hz 复指数信号)、噪声生成的完整代码示例。 -注意,N 是用于模拟的样本数,也是 FFT 窗口大小。 +注意,N 是用于模拟的采样点数,也是 FFT 窗口大小。 .. code-block:: python @@ -556,7 +571,7 @@ LO 泄漏是通过频率组合产生的额外能量,但是消除这种额外 Fs = 300 # 采样率 Ts = 1/Fs # 采样间隔 - N = 2048 # 用于模拟的样本数 + N = 2048 # 用于模拟的采样点数 t = Ts*np.arange(N) x = np.exp(1j*2*np.pi*50*t) # 模拟一个 50 Hz 的正弦信号 From 891d73323aa24edde2331f84e8efa0b94f42cad8 Mon Sep 17 00:00:00 2001 From: doctormin Date: Sun, 15 Mar 2026 18:30:27 +0800 Subject: [PATCH 7/7] update & sync --- content-zh/about_author.rst | 9 +- content-zh/digital_modulation.rst | 25 +- content-zh/frequency_domain.rst | 46 +++- content-zh/intro.rst | 4 +- content-zh/iq_files.rst | 12 +- content-zh/noise.rst | 383 +++++++++++++++++++++++++++++- content-zh/pulse_shaping.rst | 263 ++++++++++++++++++++ content-zh/sampling.rst | 2 +- content-zh/usrp.rst | 14 +- index-zh.rst | 1 + 10 files changed, 710 insertions(+), 49 deletions(-) create mode 100644 content-zh/pulse_shaping.rst diff --git a/content-zh/about_author.rst b/content-zh/about_author.rst index a32f2baa..9129f666 100644 --- a/content-zh/about_author.rst +++ b/content-zh/about_author.rst @@ -8,16 +8,15 @@ 专长于 SDR(软件定义无线电),机器学习,LTE/5G-NR,以及频谱感知。 他是马里兰大学的兼职教授,在那里他创立了一门课程,该课程成为了他编写这本教科书的基础。 他的课程是一个面向对 SDR/DSP(数字信号处理)感兴趣的计算机科学本科生的四年级选修课。 -这门课程使他更好地了解了如何深入浅出地吸引并教会那些编程能力很强但几乎不了解物理层知识的学生。 +这门课程使他更好地了解了如何深入浅出地吸引并教会那些编程能力很强但几乎没有无线通信和信号处理背景的学生。 在马克的课程中遇到迷你黑客马拉松并不奇怪,学生们必须利用他们最近学到的知识来找到或解码(由 Marc 发送)的隐藏的信号。 Marc 也是 `GNU Radio项目 `_ 的主要负责人之一。 GNU Radio 是一个开源的 SDR 框架,在学术界和国防相关研究中被广泛应用。 -虽然 Python 非常适合学习、快速实验和开发,但它并不适合大型和计算复杂的应用程序。 -GNU Radio 能够实现更高级的 DSP 应用程序,而且使用 GNU Radio 开发的 App 或单独的 Block 非常容易与他人共享。 +GNU Radio 可以用来实现更高级的 DSP 应用程序,而且使用 GNU Radio 开发的 App 或单独的 Block 非常容易与他人共享。 -Marc 目前和他的妻子 Lindsey 以及他们的多只猫和狗一起住在华盛顿特区地区。 -他的爱好包括木工制作、激光切割、演奏单簧管/萨克斯风、帆船、园艺、制作/操纵无人机、制作/骑行电动滑板,以及钻研高级溜溜球技巧。 +Marc 目前和他的妻子以及他们的多只猫和狗一起住在华盛顿特区地区。 +他的爱好包括木工制作、机加工、激光切割、单簧管/萨克斯风、帆船、园艺和弹球机。 邮箱: marc@pysdr.org diff --git a/content-zh/digital_modulation.rst b/content-zh/digital_modulation.rst index 97adf0f2..ffdd9862 100644 --- a/content-zh/digital_modulation.rst +++ b/content-zh/digital_modulation.rst @@ -100,7 +100,7 @@ 注意,图中无线信号的纵坐标平均值为零。在讨论调制时,人们往往喜欢这样展现数据和绘图。 -我们可以使用多于两个幅度级别,从而让每个符号携带更多比特。以下是 4-ASK 的示例。 +我们可以使用多于两个幅度级别,从而让每个符号携带更多比特。以下是 4-ASK 的示例(幅度为 0 也是四种级别之一)。 在这种情况下,每个符号携带 2 比特的信息。 .. image:: ../_images/ask2.svg @@ -133,7 +133,7 @@ 上图中,顶图展示的是由红色点表示的离散采样点(即我们生成的数字信号),底图展示的是调制后的真正能在空中发出的信号。 在真实的通信系统中,载波的频率通常远远高于符号变化的频率: -在这个例子中,每个符号包含载波(正弦波)的三个周期,但在实际中可能包含数千个周期,具体取决于载波频率有多高。 +在这个例子中,每个符号包含载波(正弦波)的 2.5 个周期,但在实际中可能包含数千个周期,具体取决于载波频率有多高。如需了解更多关于 ASK 的信息,推荐参阅 `这篇资料 `_ 。 ************************ 相移键控(PSK) @@ -227,7 +227,7 @@ IQ 图/星座图 现在,让我们暂时回到 ASK 方案。 我们可以像展示 PSK 一样在 IQ 图上展示 ASK。 -以下是双极(Bipolar) 2-ASK、4-ASK 和 8-ASK,以及单极(Unipolar) 2-ASK 和 4-ASK 的 IQ 图。 +以下是双极(Bipolar) 2-ASK、4-ASK 和 8-ASK,以及单极(Unipolar) 2-ASK 和 4-ASK 的 IQ 图。在这里,双极意味着调制信号可以取正和负的幅度值,而单极 ASK 只使用正幅度。 .. image:: ../_images/ask_set.png :scale: 50 % @@ -286,13 +286,12 @@ FSK 是相当容易理解的---我们在 N 个频率之间切换,每个频率 因为我们是在调制载波,所以实际上是在载波频率上加减这 N 种频率。 例如,我们可能在 1.2 GHz 的载波频率上切换以下四个频率: -1. 1.2005 GHz -2. 1.2010 GHz -3. 1.1995 GHz -4. 1.1990 GHz +1. 1.2001 GHz +2. 1.2003 GHz +3. 1.1999 GHz +4. 1.1997 GHz -上面这个例子就是一个 4-FSK, 每个符号携带 2 比特的信息。 -在频域中,一个 4-FSK 信号可能看起来像这样: +上面这个例子就是一个 4-FSK,每个符号携带 2 比特的信息。频率间距为 200 kHz,整个信号的带宽略大于 600 kHz。这个 4-FSK 信号在基带频域中(对多个符号进行 FFT 后)可能看起来像这样: .. image:: ../_images/fsk.svg :align: center @@ -302,9 +301,8 @@ FSK 是相当容易理解的---我们在 N 个频率之间切换,每个频率 在设计 FSK 时,你一定会遇到一个关键问题:相邻频率之间的频谱间距应该是多少? 我们通常把这个间距用 :math:`\Delta f` 表示(单位是 Hz)。 我们希望在频域中避免频率重叠,这样接收机才能根据不同的频率辨别符号,所以 :math:`\Delta f` 必须足够大。 -而每种频率的宽度则取决于我们的符号速率:每秒需要传输的符号越多,每个符号的持续时间越短,其频率宽度就越宽(回忆一下时间和频率之间的反比关系)。 +而每种频率的宽度则取决于我们的符号速率以及所应用的脉冲整形滤波器:每秒需要传输的符号越多,每个符号的持续时间越短,其频率宽度就越宽(回忆一下时间和频率之间的反比关系)。 那么相应的,:math:`\Delta f` 就需要越大,以避免不同频率之间出现重叠。 -我们暂时不会在本教材中深入讨论 FSK 的设计细节。 IQ 图无法展示不同的频率,它们展示的是幅度和相位。 虽然在时域中展示 FSK 是可能的,但是超过 2 个频率仍然会使符号之间的区分变得困难: @@ -318,9 +316,10 @@ IQ 图无法展示不同的频率,它们展示的是幅度和相位。 不同于 FSK 中不同符号所使用的离散频率,FM 广播使用连续的音频信号来调制载波频率。 下面是调频(FM)和调幅(AM)的示例,顶部的 “信号” 是需要调制到载波上的音频信号。 -.. image:: ../_images/Carrier_Mod_AM_FM.webp +.. image:: ../_images/am_fm_animation.gif :align: center - :target: ../_images/Carrier_Mod_AM_FM.webp + :scale: 75 % + :target: ../_images/am_fm_animation.gif :alt: AM 和 FM 调制后的载波在时域上的示例动画。 在本教材中,我们主要关注数字信号调制方法。 diff --git a/content-zh/frequency_domain.rst b/content-zh/frequency_domain.rst index 58b24e3e..7a9385ec 100644 --- a/content-zh/frequency_domain.rst +++ b/content-zh/frequency_domain.rst @@ -150,23 +150,23 @@ 为信号 :math:`x(t)` 我们可以使用下面的公式得到其频域版本 :math:`X(f)` 。 我们将以 :math:`x(t)` 或 :math:`y(t)` 来表示函数的时域版本, 相应的以 :math:`X(f)` 和 :math:`Y(f)` 来表示其频域版本。 -注意“ :math:`t` ”代表时间,而“ :math:`f` ”代表频率。“ :math:`j` ”只是虚数单位而已。 -你可能在高中数学课上见过用“ :math:`i` ”来表示它。 -在工程和计算机科学中使用“ :math:`j` ”,因为在这些领域中“ :math:`i` ”通常指电流,并且在编程中常常作为循环变量使用。 +注意 :math:`t` 代表时间,而 :math:`f` 代表频率。 :math:`j` 只是虚数单位而已, +你可能在高中数学课上见过用 :math:`i` 来表示它。 +在工程和计算机科学中使用” :math:`j` “,因为在这些领域中” :math:`i` “通常指电流,并且在编程中常常作为循环变量使用。 -要从频域返回到时域几乎是一样的,除了多了一个缩放因子和一个负号: +要从频域返回到时域几乎是一样的,除了一个负号: .. math:: - x(t) = \frac{1}{2 \pi} \int X(f) e^{j2\pi ft} df + x(t) = \int X(f) e^{j2\pi ft} df -请注意,许多教科书和其他资源使用 :math:`w` 代替 :math:`2\pi f`。:math:`w` 是以弧度每秒为单位的角频率, +请注意,许多教科书和其他资源使用 :math:`w` 代替 :math:`2\pi f`,其中 :math:`w` 是以弧度每秒为单位的角频率, 而 :math:`f` 是以 Hz 为单位。你只需要知道: .. math:: \omega = 2 \pi f -即使它在许多方程中增加了一个 :math:`2 \pi` 项,在实际中我们更倾向于使用频率的 Hz 单位。 -最终,你在 SDR 应用中使用的将是 Hz 单位。 +即使它在许多方程中增加了一个 :math:`2 \pi` 项,在实际中我们更倾向于使用频率的 Hz 单位, +因为在大多数 SDR 和射频信号处理应用中我们使用的都是 Hz 单位。 上述傅里叶变换的方程是连续形式,其实你只会在数学问题中看到它。离散形式的方程才更接近于它在代码中实现的形态: @@ -356,6 +356,30 @@ FFT 令人疑惑的一点在于,其输出总是在频域中的,所以当我 虽然接收信号的中心频率被调到了 100MHz,但是在接受结果中,97.5 MHz 的信号实际会显示为 -2.5 MHz,负频率出现了! 但是实际上,仅仅是因为它低于中心频率而已。当我们学习更多关于采样的知识并且积累了 SDR 设备的使用经验后,你将彻底明白为什么会发生这个转换。 +从数学角度来看,通过观察复指数函数 :math:`e^{2j \pi f t}` 可以理解负频率。如果我们代入一个负频率,可以看到它是一个沿相反方向旋转的复正弦波。 + +.. math:: + e^{2j \pi f t} = \cos(2 \pi f t) + j \sin(2 \pi f t) \quad \mathrm{\textcolor{blue}{blue}} + +.. math:: + e^{2j \pi (-f) t} = \cos(2 \pi f t) - j \sin(2 \pi f t) \quad \mathrm{\textcolor{red}{red}} + +.. image:: ../_images/negative_freq_animation.gif + :align: center + :scale: 75 % + :target: ../_images/negative_freq_animation.gif + :alt: Animation of a positive and negative frequency sinusoid on the complex plane + +之所以用复指数来解释,是因为单独的 :math:`cos()` 或 :math:`sin()` 其实同时包含正频率和负频率成分,这可以从欧拉公式应用于频率为 :math:`f` 的正弦波看出来: + +.. math:: + \cos(2 \pi f t) = \underbrace{\frac{1}{2} e^{2j \pi f t}}_\text{positive} + \underbrace{\frac{1}{2} e^{-2j \pi f t}}_\text{negative} + +.. math:: + \sin(2 \pi f t) = \underbrace{\frac{1}{2j} e^{2j \pi f t}}_\text{positive} - \underbrace{\frac{1}{2j} e^{-2j \pi f t}}_\text{negative} + +因此,在射频信号处理中,我们通常使用复指数而不是余弦和正弦函数。 + **************************** 时域中的顺序并不重要 **************************** @@ -586,14 +610,16 @@ FFT 窗口大小设置 for i in range(num_rows): spectrogram[i,:] = 10*np.log10(np.abs(np.fft.fftshift(np.fft.fft(x[i*fft_size:(i+1)*fft_size])))**2) - plt.imshow(spectrogram, aspect='auto', extent = [sample_rate/-2/1e6, sample_rate/2/1e6, 0, len(x)/sample_rate]) + # 时间从顶部开始向下递增,例如 x[0] 会作为显示的第一行(最顶行)的一部分 + plt.imshow(spectrogram, aspect='auto', extent = [sample_rate/-2/1e6, sample_rate/2/1e6, len(x)/sample_rate, 0]) plt.xlabel("Frequency [MHz]") plt.ylabel("Time [s]") plt.show() 该操作应会生成以下结果,这算不上一个很有趣的时频谱,因为没有频率时变行为。 它有两个音调是因为我们模拟了一个真实信号,而真实信号总是有一个与正侧相匹配的负功率谱密度(PSD)。 -想看更多有趣的时频谱示例,请访问 https://www.IQEngine.org! +请注意,在这种实现方式中,最顶行对应的是信号的起始部分。 +想看更多有趣的时频谱示例,请访问 https://www.IQEngine.org ! .. image:: ../_images/spectrogram.svg :align: center diff --git a/content-zh/intro.rst b/content-zh/intro.rst index 233e06e2..7216ea64 100644 --- a/content-zh/intro.rst +++ b/content-zh/intro.rst @@ -56,7 +56,7 @@ 参与贡献 *************** -如果你从 PySDR 中获得了帮助,别忘了与你的同事、学生和其他可能对这些资料感兴趣的终身学习者分享。你还可以通过在 PySDR 的 `Patreon `_ 上捐款来表达你的支持,你的名字将会显示在每章下面的章节列表的左侧。 +如果你从 PySDR 中获得了帮助,别忘了与你的同事、学生和其他可能对这些资料感兴趣的终身学习者分享。你还可以通过在 PySDR 的 `Patreon `_ 上捐款来表达你的支持,你的名字将会显示在每章下面的章节列表的左侧。此外也可以通过 `一次性捐款 `_ 的方式支持本项目。 此外,欢迎你通过 marc@pysdr.org 向我发送问题、评论或者建议,你将为这本教科书的完善做出贡献! 你还可以直接在教科书的 `GitHub页面 `_ 上编辑源代码,欢迎你提交 Issue 或 Pull Request。 @@ -77,5 +77,7 @@ - `mrbloom `_ 将 PySDR 翻译为 `乌克兰语 `_ - `Yimin Zhao `_ 将 PySDR 翻译为 `简体中文 `_ - `Eduardo Chancay `_ 将 PySDR 翻译为 `西班牙语 `_ +- John Marcovici +- `Vishwaksen Reddy Dhareddy `_ 贡献了检测章节中的实时数据包检测部分 以及所有 `PySDR Patreon `_ 支持者! diff --git a/content-zh/iq_files.rst b/content-zh/iq_files.rst index 8ab12c52..0384c306 100644 --- a/content-zh/iq_files.rst +++ b/content-zh/iq_files.rst @@ -19,13 +19,13 @@ IQ 文件与 SigMF 以上数据对应 [I+jQ, I+jQ, I+jQ, I+jQ, I+jQ, I+jQ, I+jQ, ...] 当我们要将复数保存到文件中时,我们会使用 IQIQIQIQIQIQIQIQ 这样的格式进行保存。 -也就是说,我们按顺序存储了一系列浮点数。在读取它们时,我们必须将其重新分离成 [I+jQ, I+jQ, ...] 的形式。 +也就是说,我们按顺序存储了一系列整数或浮点数。在读取它们时,我们必须将其重新分离成 [I+jQ, I+jQ, ...] 的形式。 虽然我们可以将这一长串复数存储在文本文件或 CSV 文件中,但我们更倾向于将它们保存在所谓的 “二进制文件(Binary File)” 中以节省空间。 毕竟在高采样率下,你所记录的信号文件可能轻松超过多个 GB。 如果你直接在文本编辑器中打开一个二进制文件,它看起来可能和下面的截图差不多。 二进制文件包含了一系列字节,所以你必须自己按照约定的格式解析,但是二进制文件通常是存储数据最高效的方式(同时还有各种压缩算法可用)。 -由于我们的信号通常是随机序列般的一串浮点数,所以我们通常不会对其进行压缩。当然,二进制文件也被用于许多其他事情,例如编译过的程序。 +由于我们的信号通常是随机序列般的一串整数或浮点数,所以我们通常不会对其进行压缩。当然,二进制文件也被用于许多其他事情,例如编译过的程序。 用于保存信号时,我们称它们为二进制的 “IQ 文件”,使用文件扩展名 :code:`.iq` 。 .. image:: ../_images/binary_file.png @@ -33,10 +33,12 @@ IQ 文件与 SigMF :align: center 在 Python 中,默认的复数类型是 :code:`np.complex128`,它使用两个 64 位浮点数(:code:`'float64'`)来表示一个复数。 -但是在 DSP/SDR 领域,我们倾向于使用 32 位的浮点数(:code:`'float32'`), +但是在 DSP/SDR 领域,我们倾向于使用 16 位整数或 32 位浮点数, 毕竟我们的 SDR 设备上的 ADC 硬件并不能提供高达 :code:`'float64'` 的精度。 -因此在 Python 代码中,我们实际使用的是 **np.complex64** ,即用两个 :code:`'float32'` 来表示一个复数。 -其实在写代码时,复数到底是哪种类型并不重要。重要的是当你把数据保存到文件时,请确保它是以 :code:`np.complex64` 类型的数组存储的。 +事实上,大多数 SDR 使用 12 位 ADC,因此我们可以通过存储为 16 位整数(Python 中的 :code:`np.int16`)来最小化存储空间, +这意味着每个 IQ 采样点占用 4 字节,射频录制文件的大小(字节)将是采样率的 4 倍,这被称为 "Sean 的 4 倍法则"。 +在下面的 Python 示例中,我们将使用 **np.complex64** ,即用两个 :code:`'float32'` 来表示一个复数,因为 Python 没有原生的复数整数类型(但这并不妨碍我们将 IQ 数据以整数格式保存到文件中,后文会有说明)。 +其实在写代码处理信号时,复数到底是哪种类型并不重要。重要的是当你把数据保存到文件时,请确保它是以 :code:`np.complex64`(或交织 IQ 的 :code:`np.int16`)类型的数组存储的。 ************************* Python 代码示例 diff --git a/content-zh/noise.rst b/content-zh/noise.rst index 0c23be0d..ecd598f0 100644 --- a/content-zh/noise.rst +++ b/content-zh/noise.rst @@ -1,12 +1,15 @@ .. _noise-chapter: -################## -噪声与分贝(dB) -################## +########################## +噪声与随机变量 +########################## 本章将详细讨论噪声的相关主题,特别是噪声在无线通信系统中的建模和处理方式。 涉及的概念包括加性高斯白噪声(AWGN)、复数噪声、信噪比(SNR)、信干噪比(SINR)。 同时,我们还会介绍在无线通信和软件定义无线电(SDR)中广泛使用的分贝(dB)单位。 +最后,我们将深入探讨随机变量和随机过程的基本概念,这些概念对于理解噪声、信道效应以及无线通信中的许多信号处理技术至关重要。 +我们将涵盖概率分布、期望、方差,以及随机过程如何随时间演变。 +这些概念构成了分析噪声以及 SDR 和 DSP 中许多其他主题的数学基础。 ************************ 高斯噪声 @@ -17,6 +20,7 @@ .. image:: ../_images/noise.png :scale: 70 % :align: center + :target: ../_images/noise.png 在时间域中,图上的纵坐标的平均值为零。 如果平均值不为零,我们可以减去平均值来得到一个偏置,从而使剩余部分的平均值为零。 @@ -63,6 +67,7 @@ :scale: 70 % :align: center :alt: Depiction of why it's important to understand dB or decibels, showing a spectrogram using linear vs log scale + :target: ../_images/linear_vs_log.png 给定一个值 x,我们可以使用以下公式将 x 转换为 dB: @@ -77,7 +82,7 @@ Python 代码: 你可能会发现在其他领域中,表达式中的 :code:`10 *` 可能需要改为 :code:`20 *` 。 当处理代表功率的量时,常用系数 10,但当处理代表非功率量如电压或电流时,更适合使用系数 20。 -在DSP领域,我们通常处理代表功率的量。实际上,在本书中我们没有使用过 20,而始终使用 10。 +在 DSP 领域,我们通常处理代表功率的量。 我们使用以下方法将 dB 转换回线性数值(普通数值): @@ -104,6 +109,7 @@ dB 的对数刻度使我们能够在表达数字或绘制图形时拥有更大 .. image:: ../_images/db.png :scale: 80 % :align: center + :target: ../_images/db.png 还需要记住,dB 并不是严格意义上的 “单位”。 一个 dB 的数值本身是无单位的,就像说某物体是 “2 倍大” 一样,在上下文告诉你单位之前,它本身是没有单位的。 @@ -157,6 +163,7 @@ dB 是一个相对的概念。在音频处理中,当人们说 dB 时,他们 :scale: 110 % :align: center :alt: AWGN in the time domain is also Gaussian noise in the frequency domain, although it looks like a flat line when you take the magnitude and perform averaging + :target: ../_images/noise_freq.png 从下方的频谱图可以看到,所有频率上的功率谱密度都大致相同且相对平坦。 这证明,高斯噪声在频域中也是呈高斯分布的。 @@ -179,13 +186,14 @@ dB 是一个相对的概念。在音频处理中,当人们说 dB 时,他们 plt.plot(np.real(X), '.-') plt.show() -请注意,默认情况下,:code:`randn()` 生成的数据符合标准正态分布(均值为 0,方差为 1)。 +请注意,默认情况下, :code:`randn()` 函数生成的数据符合标准正态分布(均值为 0,方差为 1)。 代码生成的两张图像看起来都会类似这样: .. image:: ../_images/noise_python.png :scale: 100 % :align: center :alt: Example of white noise simulated in Python + :target: ../_images/noise_python.png 只需对上面的 FFT 输出取对数并进行平均,你就可以复现上文来自 GNU Radio 的那张平坦的 PSD 图像。 我们生成并进行 FFT 的信号是实信号(而不是复信号),任何实信号的 FFT 都会有相抵的负数和正数部分,所以我们只保存了 FFT 输出的正部分(第二部分)。 @@ -233,6 +241,7 @@ dB 是一个相对的概念。在音频处理中,当人们说 dB 时,他们 :scale: 80 % :align: center :alt: Complex noise simulated in Python + :target: ../_images/noise3.png 从上图可以看出来,实部和虚部是互相独立的。 @@ -249,6 +258,7 @@ dB 是一个相对的概念。在音频处理中,当人们说 dB 时,他们 :scale: 60 % :align: center :alt: Complex noise on an IQ or constellation plot, simulated in Python + :target: ../_images/noise_iq.png 它的形状符合预期:一个以原点为中心的随机斑点分布,即 0+0j。为了增加趣味性,让我们尝试在一个 QPSK 信号中引入噪声,并观察 IQ 图的变化。 @@ -256,12 +266,15 @@ dB 是一个相对的概念。在音频处理中,当人们说 dB 时,他们 :scale: 60 % :align: center :alt: Noisy QPSK simulated in Python + :target: ../_images/noisey_qpsk.png 当噪声更强时,会发生什么呢? .. image:: ../_images/noisey_qpsk2.png :scale: 50 % :align: center + :alt: Noisy QPSK with stronger noise simulated in Python + :target: ../_images/noisey_qpsk2.png 我们逐渐领悟到了无线数据传输的复杂性。 在追求高效率的同时,我们也不得不面对噪声干扰所带来的问题。 @@ -273,7 +286,7 @@ AWGN AWGN (Additive White Gaussian Noise,加性高斯白噪声)是 DSP 和 SDR 领域中经常能听到的缩写。 GN 指的是高斯噪声,我们之前已经讨论过了。Additive (加性)表示噪声是被添加到接收信号中的。 -White (白)在频域上意味着我们整个观测频带上的频谱是平坦的,在实践中,它几乎总是白噪声,或者是近似白噪声。 +White (白)在频域上意味着我们整个观测频带上的频谱是平坦的,在实践中,它几乎总是白噪声,或者近似白噪声。 在本教材中,当处理通信链路和链路预算等问题时,我们将只考虑 AWGN 作为唯一形式的噪声。 非 AWGN 噪声往往是一个专门的课题。 @@ -309,19 +322,363 @@ SNR 在实践中通常以分贝(dB)表示。在无线通信的模拟实验 干扰的构成取决于具体应用和场景,但通常是另一个信号干扰了我们感兴趣的信号(Signal Of Interest,缩写为 SOI), 并且在频率上与 SOI 重叠、或者由于某种原因无法被滤除。 -************************* -参考资料 -************************* +********************************* +深入理解随机变量 +********************************* + +到目前为止,我们一直避免过于数学化,但现在我们需要退后一步,介绍随机变量的概念以及它们在无线通信和 SDR 中的应用。 **随机变量** 是一个数学概念,它将随机实验的结果映射到数值上。随机变量表示那些在被观察或测量之前其值是不确定的量,比如我们的噪声采样点。设想掷一个六面骰子,在掷之前你不知道会出现什么数字。我们可以定义一个随机变量 :math:`X` 来表示掷骰子的结果。 :math:`X` 的值是 {1, 2, 3, 4, 5, 6} 中的一个,但在实际掷之前我们不知道是哪一个。 + +在无线通信和 SDR 的场景中,随机变量无处不在: + +* 接收机中的热噪声在每个时刻都可以建模为随机变量 +* 受多径衰落影响的接收信号的幅度是随机的 +* 变化信道引入的相位偏移可以建模为 :math:`0` 到 :math:`2\pi` 之间的随机变量 +* 甚至我们传输的数据比特也可以被视为随机变量 + +**单个样本 vs. 多个样本** + +这是一个至关重要的区别,经常引起混淆: + +* 随机变量的 **单次实现** 或 **单个样本** 只是一个数字——随机实验的一个结果 +* 要描述一个随机变量的特征(找到其均值、分布范围等),我们需要 **大量实现** ——即多个结果 + +例如,如果你在 Python 中调用 ``np.random.randn()`` 不带任何参数,它返回一个从高斯分布中抽取的随机数。这单个数字几乎无法告诉你分布本身的任何信息。但如果你调用 ``np.random.randn(10000)`` 生成 10,000 个样本,你就可以估计分布的均值和方差等属性了。 + +.. code-block:: python + + import numpy as np + + # 单个样本 - 只是一个数字 + x_single = np.random.randn() + print(x_single) # 可能是 0.534, -1.23, 或其他任何值 + + # 大量样本 - 现在我们可以描述分布的特征 + x_many = np.random.randn(10000) + print(np.mean(x_many)) # 会接近 0 + print(np.var(x_many)) # 会接近 1 + +联合分布 +#################### + +到目前为止,我们关注的是单个随机变量。当同时处理两个或更多随机变量时,我们使用 **联合分布(Joint Distribution)** 。 + +对于连续随机变量 :math:`X` 和 :math:`Y` ,联合分布由 **联合概率密度函数(Joint PDF)** 描述: + +.. math:: + f_{X,Y}(x,y) + +联合概率密度函数告诉我们 :math:`X` 取值 :math:`x` *同时* :math:`Y` 取值 :math:`y` 的可能性。 + +从联合概率密度函数中,我们可以计算: + +* 边缘概率密度函数(例如 :math:`f_X(x)` 或 :math:`f_Y(y)` ) +* 期望值,如 :math:`E[XY]` +* 协方差和相关性 +* 涉及两个变量的概率 + +例如,:math:`X` 的边缘概率密度函数可以通过对 :math:`Y` 积分得到: + +.. math:: + f_X(x) = \int_{-\infty}^{\infty} f_{X,Y}(x,y)\,dy + +联合分布是理解随机变量之间依赖性、相关性和独立性的数学基础。 + + +概率分布 +######################### + +**概率分布** 描述了随机变量取不同值的可能性。对于连续随机变量,我们使用 **概率密度函数(Probability Density Function,PDF)** ,记为 :math:`f_X(x)` 。PDF 告诉我们随机变量取不同值的相对可能性。 + +在 SDR 和通信中最重要的分布是 **高斯(正态)分布** 。均值为 :math:`\mu` 、方差为 :math:`\sigma^2` 的高斯随机变量 :math:`X` 的 PDF 为: + +.. math:: + f_X(x) = \frac{1}{\sqrt{2\pi\sigma^2}} e^{-\frac{(x-\mu)^2}{2\sigma^2}} + +这就是你可能见过的著名 "钟形曲线"。该分布完全由两个参数确定: + +* **均值** :math:`\mu` :分布的中心 +* **方差** :math:`\sigma^2` :分布的展宽程度(标准差 :math:`\sigma` 是方差的平方根) + +在 Python 中,``np.random.randn()`` 生成的样本来自 :math:`\mu = 0` 、 :math:`\sigma^2 = 1` 的 **标准高斯** 分布。我们可以这样可视化: + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + + # 从标准高斯分布生成 10,000 个样本 + x = np.random.randn(10000) + + # 创建直方图以可视化分布 + plt.hist(x, bins=50, density=True, alpha=0.7, edgecolor='black') + plt.xlabel('Value') + plt.ylabel('Probability Density') + plt.title('Gaussian Distribution (μ=0, σ²=1)') + plt.grid(True) + plt.show() + +.. image:: ../_images/gaussian_histogram.png + :scale: 80% + :align: center + :alt: Histogram of Gaussian distributed samples + :target: ../_images/gaussian_histogram.png + +期望(即均值) +######################### + +随机变量的 **期望** 或 **期望值** ,记为 :math:`E[X]` 或 :math:`\mu` ,表示其在大量实现中的平均值。对于具有 PDF :math:`f_X(x)` 的连续随机变量,期望为: + +.. math:: + E[X] = \int_{-\infty}^{\infty} x \cdot f_X(x) \, dx + +在实践中,当我们有从分布中抽取的 :math:`N` 个样本 :math:`x_1, x_2, \ldots, x_N` 时,我们使用 **样本均值** 来估计期望: + +.. math:: + \hat{\mu} = \frac{1}{N} \sum_{n=1}^{N} x_n + +期望是一个 **线性算子** ,这意味着: + +* :math:`E[aX + b] = aE[X] + b` (其中 :math:`a` 和 :math:`b` 为常数) +* :math:`E[X + Y] = E[X] + E[Y]` (对于任意两个随机变量) + +这种线性性质在信号处理中非常有用! + +方差与标准差 +############################### + +随机变量的 **方差** ,记为 :math:`\text{Var}(X)` 或 :math:`\sigma^2` ,衡量其值围绕均值的分散程度。它被定义为偏离均值的平方的期望值: + +.. math:: + \text{Var}(X) = E[(X - \mu)^2] = E[X^2] - (E[X])^2 + +当我们有 :math:`N` 个样本时,我们使用以下公式估计方差: + +.. math:: + \hat{\sigma}^2 = \frac{1}{N} \sum_{n=1}^{N} (x_n - \hat{\mu})^2 + +**标准差** :math:`\sigma` 就是方差的平方根::math:`\sigma = \sqrt{\sigma^2}` 。 + +请注意上面公式中 :math:`\sigma` 和样本均值上方的 :math:`\enspace \hat{} \enspace` 符号(称为 "hat")。这个帽子符号表示我们是在 *估计* 均值/方差,估计值不一定精确等于真实的均值/方差,但随着样本数量的增加,它会越来越接近真实值。 + +**关键性质:** 如果 :math:`X` 是方差为 :math:`\sigma^2` 的随机变量,则: + +* 缩放::math:`\text{Var}(aX) = a^2 \text{Var}(X)` +* 平移::math:`\text{Var}(X + b) = \text{Var}(X)` (加上常数不改变分散程度) + +相应地,标准差 :math:`\sigma` 的性质为: + +* 缩放::math:`\sigma(aX) = a\sigma(X)` +* 平移::math:`\sigma(X+b) = \sigma(X)` + +.. image:: ../_images/gaussian_transformed.png + :scale: 80% + :align: center + :alt: Scaling and shifting the Gaussian Distribution. (notice the scales on x and y axes) + :target: ../_images/gaussian_transformed.png + +对高斯分布进行缩放和平移(注意 x 轴和 y 轴的刻度变化) + +**方差与功率** + +在信号处理中,对于 **零均值** 信号(均值约为 0),方差等于 **平均功率** 。这就是为什么我们经常交替使用这两个术语: + +.. math:: + P = \text{Var}(X) = E[X^2] \quad \text{(当 } E[X] = 0\text{)} + +这个关系对于分析噪声功率、信噪比(SNR)和链路预算至关重要。 + +.. code-block:: python -关于AWGN,SNR和方差的更多资料可参考: + noise_power = 2.0 + n = np.random.randn(N) * np.sqrt(noise_power) + print(np.var(n)) # 会约等于 2.0 -1. https://en.wikipedia.org/wiki/Additive_white_Gaussian_noise -2. https://en.wikipedia.org/wiki/Signal-to-noise_ratio -3. https://en.wikipedia.org/wiki/Variance +协方差 +########## +两个随机变量 :math:`X` 和 :math:`Y` 之间的 **协方差(Covariance)** 定义为: +.. math:: + \text{Cov}(X,Y) = E[(X - E[X])(Y - E[Y])] + +一个等价且通常更方便的形式是: + +.. math:: + \text{Cov}(X,Y) = E[XY] - E[X]E[Y] + +协方差衡量两个变量如何共同变化: + +* 正协方差:它们趋向于一起增大或一起减小 +* 负协方差:一个趋向于在另一个减小时增大 +* 零协方差:它们是不相关的 + +如果两个变量都是零均值的,则简化为: + +.. math:: + \text{Cov}(X,Y) = E[XY] + +协方差有单位(它不是归一化的),这就是为什么在实践中我们经常使用 **相关系数** (Correlation Coefficient): + +.. math:: + \rho_{XY} = \frac{\text{Cov}(X,Y)}{\sigma_X \sigma_Y} + +相关系数是一个介于 -1 和 +1 之间的无量纲值。 + +变量之和的方差 +############################### + +在信号处理中,我们经常处理随机变量之和,比如信号加噪声: + +.. math:: + Z = X + Y + +这个和的方差取决于 :math:`X` 和 :math:`Y` 是否独立(或更一般地说,是否相关)。 + +完整的一般形式为: + +.. math:: + \text{Var}(X + Y) = \text{Var}(X) + \text{Var}(Y) + 2\,\text{Cov}(X,Y) + +其中 :math:`\text{Cov}(X,Y)` 是 :math:`X` 和 :math:`Y` 之间的 **协方差** 。 + +**独立的情况** + +如果 :math:`X` 和 :math:`Y` 是独立的(或仅仅是不相关的),则表达式简化为: + +.. math:: + \text{Var}(X + Y) = \text{Var}(X) + \text{Var}(Y) + +这个结果在通信中极其重要。例如,如果接收信号为: + +.. math:: + R = S + N + +其中 :math:`S` 是信号,:math:`N` 是独立的噪声,那么总功率就是信号功率和噪声功率之和。 + +这就是为什么 SNR 计算如此简单直接。 + +************************ +复数随机变量 +************************ + +在 SDR 中,我们大量使用 **复数值信号** ,这意味着我们也需要处理复数随机变量。复数随机变量的形式为: + +.. math:: + Z = X + jY + +其中 :math:`X` 和 :math:`Y` 都是实数值随机变量,分别代表同相(I)和正交(Q)分量。 + +**复高斯噪声** + +在无线通信中最常见的复数随机变量是 **复高斯噪声** ,其中 :math:`X` 和 :math:`Y` 都是具有相同方差的独立高斯随机变量。 +例如,如果 :math:`X \sim \mathcal{N}(\alpha_1, \sigma_1^2)` 和 :math:`Y \sim \mathcal{N}(\alpha_2, \sigma_2^2)` 是独立的,那么复数随机变量 :math:`Z = X + jY` 具有: + +* 均值::math:`E[Z] = E[X] + jE[Y] = \alpha_1 + j\alpha_2` +* 方差(功率)::math:`\text{Var}(Z) = \text{Var}(X) + \text{Var}(Y) = \sigma_1^2 + \sigma_2^2` + +.. image:: ../_images/gaussian_IQ.png + :scale: 80% + :align: center + :alt: Complex Gaussian noise visualized as two independent Gaussian random variables on the I and Q axes + :target: ../_images/gaussian_IQ.png + +这就是为什么当我们创建单位功率(方差 = 1)的复高斯噪声时,我们使用: + +.. code-block:: python + + N = 10000 + n = (np.random.randn(N) + 1j*np.random.randn(N)) / np.sqrt(2) + print(np.var(n)) # ~ 1 + +除以 :math:`\sqrt{2}` 确保了总功率(I 和 Q 方差之和)等于 1。 + +.. code-block:: python + + # 不做归一化的情况: + n_raw = np.random.randn(N) + 1j*np.random.randn(N) + print(np.var(np.real(n_raw))) # ~ 1 + print(np.var(np.imag(n_raw))) # ~ 1 + print(np.var(n_raw)) # ~ 2 (总功率) + + # 做归一化的情况: + n_norm = n_raw / np.sqrt(2) + print(np.var(n_norm)) # ~ 1 (单位功率) + +**************** +随机过程 +**************** + +到目前为止我们讨论的是随机变量——某个单一时刻的随机值。 **随机过程** (也称为 **随机过程** ,Stochastic Process)是一组按时间索引的随机变量: + +.. math:: + X(t) \quad \text{或} \quad X[n] \text{(离散时间)} + +在每个时刻 :math:`t` ,:math:`X(t)` 都是一个随机变量。可以把随机过程想象成一个随时间随机演变的信号。 + +在无线通信中的例子: + +* 接收机处的噪声::math:`N(t)` 或 :math:`N[n]` +* 经历时变衰落的信号::math:`H(t)S(t)` +* 来自 SDR 的采样数据:每批数据都是随机过程的一次实现 + +**平稳过程** + +如果一个随机过程的统计特性不随时间变化,则称其为 **平稳** 的。特别地,一个 **广义平稳(Wide-Sense Stationary,WSS)** 过程具有: + +* 恒定均值:对所有 :math:`t` ,:math:`E[X(t)] = \mu` +* 自相关仅依赖于时间差::math:`E[X(t)X(t+\tau)]` 仅依赖于 :math:`\tau` ,而不依赖于 :math:`t` + +无线系统中的许多噪声源近似为平稳的,这大大简化了分析过程。 + +**白噪声** + +**白噪声** 是一种在不同时刻的样本之间不相关的随机过程,且其功率谱密度在所有频率上是恒定的。加性高斯白噪声(AWGN)同时具有以下两个特性: + +* **白** :时间上不相关,频谱平坦 +* **高斯** :每个样本都服从高斯分布 + +当我们在 Python 中使用 ``np.random.randn(N)`` 生成噪声时,:math:`N` 个样本中的每一个都是独立的高斯随机变量,共同构成一个白噪声过程。 + + +独立性与相关性 +############################# + +如果知道一个随机变量的值不能提供关于另一个的任何信息,则这两个随机变量 :math:`X` 和 :math:`Y` 是 **独立** 的。数学上,它们的联合 PDF 可以分解为: + +.. math:: + f_{X,Y}(x,y) = f_X(x) \cdot f_Y(y) + +独立性是一个很强的条件。一个较弱的条件是 **不相关** ,即: + +.. math:: + E[XY] = E[X]E[Y] + +对于高斯随机变量,不相关意味着独立(这是高斯分布的一个特殊性质)。 + +在复高斯噪声中,I 和 Q 分量是独立的: + +.. code-block:: python + N = 10000 + I = np.random.randn(N) + Q = np.random.randn(N) + + # 通过相关性检验独立性 + correlation = np.corrcoef(I, Q)[0, 1] + print(f"Correlation between I and Q: {correlation:.4f}") # ~ 0 + +*************************** +拓展阅读 +*************************** + +1. Papoulis, A., & Pillai, S. U. (2002). *Probability, Random Variables, and Stochastic Processes*. McGraw-Hill. +2. Kay, S. M. (2006). *Intuitive Probability and Random Processes using MATLAB®*. Springer. +3. https://en.wikipedia.org/wiki/Random_variable +4. https://en.wikipedia.org/wiki/Normal_distribution +5. https://en.wikipedia.org/wiki/Stochastic_process +6. https://en.wikipedia.org/wiki/Additive_white_Gaussian_noise +7. https://en.wikipedia.org/wiki/Signal-to-noise_ratio diff --git a/content-zh/pulse_shaping.rst b/content-zh/pulse_shaping.rst new file mode 100644 index 00000000..2da00459 --- /dev/null +++ b/content-zh/pulse_shaping.rst @@ -0,0 +1,263 @@ +.. _pulse-shaping-chapter: + +####################### +脉冲整型 +####################### + +在本章中我们将介绍脉冲整形(Pulse Shaping),符号间干扰(Inter-Symbol-Interference, ISI),匹配滤波器(Matched Filter)和升余弦滤波器(Raised-Cosine Filter)。 +最后我们将使用 Python 为 BPSK 符号添加脉冲整形。 +你可以把本章节视为滤波器章节的后续章节,在此我们将深入讨论脉冲整形。 + +********************************** +符号间干扰(ISI) +********************************** + +在 :ref:`filters-chapter` 章节中,我们学习到了方块形状的符号/脉冲会占用相当宽的频谱,我们可以通过"整形"我们的脉冲来大大减少使用的频谱。然而,你不能使用任何低通滤波器,否则可能会出现符号间干扰(Inter-Symbol-Interference,ISI),即符号相互干扰。 + +当我们传输数字符号时,我们是连续地逐个传输它们的(而不是在符号之间等待一段时间)。当你施加脉冲整形滤波器时,它会在时域中拉长脉冲(以便在频域中压缩),从而导致相邻符号彼此重叠。只要你的脉冲整形滤波器满足以下准则,这种重叠就不会有问题:除了其中一个脉冲之外,所有脉冲在符号周期 :math:`T` 的每一个整数倍处的叠加值必须为零。通过下面这幅图可以最直观地理解这个概念: + +.. image:: ../_images/pulse_train.svg + :align: center + :target: ../_images/pulse_train.svg + :alt: A pulse train of sinc pulses + +如图所示,在 :math:`T` 的每一个整数倍处,只有一个脉冲存在峰值,而其余所有脉冲的值都为 0(它们穿过了 x 轴)。当接收机对信号进行采样时,它恰好在完美的时刻(即脉冲的峰值处)进行采样,这意味着只有那个时间点才真正重要。通常,接收机中会有一个符号同步模块来确保在峰值处对符号进行采样。 + +********************************** +匹配滤波器 +********************************** + +在无线通信中,我们使用的一个技巧叫做匹配滤波(Matched Filtering)。要理解匹配滤波,你首先需要理解以下两点: + +1. 上面讨论的那些脉冲只需要在 *接收端* 采样之前完美对齐即可。在那之前,即使存在 ISI 也没关系,也就是说,信号可以带着 ISI 在空中传播,这是完全没问题的。 + +2. 我们希望在发射机中使用低通滤波器来减少信号占用的频谱。但接收机同样需要一个低通滤波器来尽可能消除信号旁边的噪声/干扰。因此,我们在发射机(Tx)端有一个低通滤波器,在接收机(Rx)端也有一个低通滤波器,而采样发生在这两个滤波器(以及无线信道的影响)之后。 + +在现代通信中,我们会将脉冲整形滤波器均等地拆分到 Tx 和 Rx 两端。虽然它们 *不一定* 非得是相同的滤波器,但从理论上讲,在 AWGN 环境下最大化 SNR 的最优线性滤波器就是在 Tx 和 Rx 使用 *相同* 的滤波器。这种策略被称为 "匹配滤波器" 的概念。 + +另一种理解匹配滤波器的方式是:接收机将接收到的信号与已知的模板信号进行相关运算。这里的模板信号本质上就是发射机发送的脉冲,与施加在其上的相位/幅度偏移无关。回忆一下,滤波是通过卷积来完成的,而卷积基本上就是相关运算(事实上,当模板是对称的时候,它们在数学上是完全等价的)。将接收信号与模板进行相关运算,能让我们最大限度地恢复所发送的内容,这也是为什么它在理论上是最优的。打个比方,想象一个图像识别系统,它使用一个人脸模板并通过 2D 相关运算来寻找人脸: + +.. image:: ../_images/face_template.png + :scale: 70 % + :align: center + +********************************** +将滤波器拆分为两半 +********************************** + +我们具体要怎么把一个滤波器拆成两半呢?卷积运算具有结合律,即: + +.. math:: + (f * g) * h = f * (g * h) + +假设 :math:`f` 是我们的输入信号,而 :math:`g` 和 :math:`h` 是两个滤波器。先用 :math:`g` 滤波 :math:`f` ,再用 :math:`h` 滤波,等效于使用一个为 :math:`g * h` 的滤波器进行滤波。 + +另外,回忆一下时域卷积等于频域相乘: + +.. math:: + g(t) * h(t) \leftrightarrow G(f)H(f) + +要将一个滤波器拆分为两半,你可以对其频率响应取平方根。 + +.. math:: + X(f) = X_H(f) X_H(f) \quad \mathrm{where} \quad X_H(f) = \sqrt{X(f)} + +下图展示了一个简化的发射和接收链路图。其中升余弦(RC)滤波器被拆分为两个根升余弦(RRC)滤波器:发射端的一个是脉冲整形滤波器,接收端的一个是匹配滤波器。它们共同作用,使得解调器处的脉冲看起来就像经过了单个 RRC 滤波器进行脉冲整形一样。 + +.. image:: ../_images/splitting_rc_filter.svg + :align: center + :target: ../_images/splitting_rc_filter.svg + :alt: A diagram of a transmit and receive chain, with a Raised Cosine (RC) filter being split into two Root Raised Cosine (RRC) filters + +********************************** +具体的脉冲整形滤波器 +********************************** + +我们已经知道我们需要做到: + +1. 设计一个能降低信号带宽(以减少频谱占用)的滤波器,同时要求在每个符号间隔处,除了一个脉冲之外,其余脉冲的叠加值均为零。 + +2. 将该滤波器拆分为两半,一半放在 Tx,另一半放在 Rx。 + +让我们来看看一些常用的脉冲整形滤波器。 + +升余弦滤波器 +######################### + +最常用的脉冲整形滤波器似乎是 "升余弦" 滤波器。它是一个很好的低通滤波器,能够限制信号占用的带宽,同时它还具有在 :math:`T` 的整数倍处求和为零的特性: + +.. image:: ../_images/raised_cosine.svg + :align: center + :target: ../_images/raised_cosine.svg + :alt: The raised cosine filter in the time domain with a variety of roll-off values + +请注意,上图是时域图,描绘的是滤波器的脉冲响应。参数 :math:`\beta` 是升余弦滤波器唯一的参数,它决定了滤波器在时域中衰减到零的速度,而这与其在频域中衰减的速度成反比: + +.. image:: ../_images/raised_cosine_freq.svg + :align: center + :target: ../_images/raised_cosine_freq.svg + :alt: The raised cosine filter in the frequency domain with a variety of roll-off values + +之所以称之为升余弦滤波器,是因为当 :math:`\beta = 1` 时,其频域响应是一个半周期的余弦波,被抬升到 x 轴上。 + +升余弦滤波器的脉冲响应的数学表达式为: + +.. math:: + h(t) = \mathrm{sinc}\left( \frac{t}{T} \right) \frac{\cos\left(\frac{\pi\beta t}{T}\right)}{1 - \left( \frac{2 \beta t}{T} \right)^2} + +关于 :math:`\mathrm{sinc}()` 函数的更多信息可以参阅 `这里 `_ 。你可能在其他地方看到包含 :math:`\frac{1}{T}` 缩放因子的公式;这个因子使滤波器具有单位增益,即输出信号的功率与输入信号相同(这是设计滤波器时的常见做法)。然而,我们要将其应用于由符号组成的脉冲序列(例如 1 和 -1),我们不希望这些符号的幅度在脉冲整形后发生变化,因此我们省略了这个缩放因子。当我们深入到 Python 示例并绘制输出时,这一点会更加清晰。 + +记住:我们要将这个滤波器均等地拆分到 Tx 和 Rx。接下来介绍根升余弦(RRC)滤波器! + +根升余弦滤波器 +######################### + +根升余弦(Root Raised-Cosine,RRC)滤波器才是我们实际在 Tx 和 Rx 中实现的滤波器。正如前文所述,两者组合起来就构成了一个完整的升余弦滤波器。因为将滤波器拆分为两半涉及到频域上的开平方运算,所以脉冲响应看起来会稍微复杂一些: + +.. image:: ../_images/rrc_filter.png + :scale: 70 % + :align: center + +好在这是一个非常常用的滤波器,有大量现成的实现可以使用,包括 `Python 实现 `_ 。 + +其他脉冲整形滤波器 +########################### + +其他滤波器包括高斯滤波器(Gaussian Filter),其脉冲响应类似高斯函数。还有 sinc 滤波器,它等效于 :math:`\beta = 0` 时的升余弦滤波器。sinc 滤波器更接近理想滤波器,即它能够在几乎没有过渡区域的情况下消除不需要的频率。 + +********************************** +滚降因子 +********************************** + +让我们仔细研究一下参数 :math:`\beta` 。它是一个介于 0 到 1 之间的数值,称为 "滚降因子(Roll-off Factor)",有时也被称为 "过量带宽(Excess Bandwidth)"。它决定了滤波器在时域中衰减到零的速度。回忆一下,要用作滤波器,脉冲响应必须在两侧衰减到零: + +.. image:: ../_images/rrc_rolloff.svg + :align: center + :target: ../_images/rrc_rolloff.svg + :alt: Plot of the raised cosine roll-off parameter + +:math:`\beta` 越小,所需的滤波器抽头数就越多。当 :math:`\beta = 0` 时,脉冲响应永远不会完全衰减到零,因此我们试图将 :math:`\beta` 设置得尽可能低,同时又不会引发其他问题。滚降因子越低,对于给定的符号速率,我们能够在频率上把信号压缩得越紧凑,这一点始终很重要。 + +一个常用的公式用于估算给定符号速率和滚降因子下的带宽(单位为 Hz): + +.. math:: + \mathrm{BW} = R_S(\beta + 1) + +:math:`R_S` 是符号速率(单位为 Hz)。在无线通信中,我们通常选择 0.2 到 0.5 之间的滚降因子。作为经验法则,一个使用符号速率 :math:`R_S` 的数字信号将占用略多于 :math:`R_S` 的频谱,这里包括正频率和负频率部分。一旦我们将信号上变频并发射出去,两侧的频谱都很重要。如果我们以每秒 100 万个符号(MSps)的速率传输 QPSK 信号,它将占用大约 1.3 MHz 的频谱。数据速率将是 2 Mbps(回忆一下 QPSK 每个符号携带 2 比特),其中包括信道编码和帧头等开销。 + +********************************** +Python 练习 +********************************** + +作为 Python 练习,让我们来对一些脉冲进行滤波和整形。我们将使用 BPSK 符号,因为它更容易可视化——在脉冲整形步骤之前,BPSK 就是传输 1 或 -1,其 "Q" 分量等于零。由于 Q 为零,我们只需绘制 I 分量即可,这样看起来更简单。 + +在这个仿真中,我们将使用每个符号 8 个采样点,并且不使用看起来像方波的 1 和 -1 信号,而是使用由脉冲(冲激)组成的脉冲序列。当你把一个脉冲通过滤波器时,输出就是脉冲响应(这也是 "脉冲响应" 这个名称的由来)。因此,如果你想要一系列脉冲,就应该使用中间填充零的冲激序列,从而避免产生方形脉冲。 + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + from scipy import signal + + num_symbols = 10 + sps = 8 + + bits = np.random.randint(0, 2, num_symbols) # Our data to be transmitted, 1's and 0's + + x = np.array([]) + for bit in bits: + pulse = np.zeros(sps) + pulse[0] = bit*2-1 # set the first value to either a 1 or -1 + x = np.concatenate((x, pulse)) # add the 8 samples to the signal + plt.figure(0) + plt.plot(x, '.-') + plt.grid(True) + plt.show() + +.. image:: ../_images/pulse_shaping_python1.png + :scale: 80 % + :align: center + :alt: A pulse train of impulses in the time domain simulated in Python + +此时我们的符号仍然是 1 和 -1。不要纠结于我们使用了冲激这件事。实际上,*不要* 去可视化冲激响应,而是把它当作一个数组来理解可能会更简单: + +.. code-block:: python + + bits: [0, 1, 1, 1, 1, 0, 0, 0, 1, 1] + BPSK symbols: [-1, 1, 1, 1, 1, -1, -1, -1, 1, 1] + Applying 8 samples per symbol: [-1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, ...] + +我们将使用 :math:`\beta` 为 0.35 的升余弦滤波器,并将其设为 101 个抽头长度,以给信号足够的时间衰减到零。虽然升余弦方程需要我们提供符号周期和时间向量 :math:`t` ,但我们可以假设 **采样** 周期为 1 秒来 "归一化" 我们的仿真。这意味着我们的符号周期 :math:`Ts` 为 8,因为我们有每个符号 8 个采样点。于是我们的时间向量将是一个整数列表。根据升余弦方程的特性,我们希望 :math:`t=0` 位于中心。我们将生成从 -51 到 +51 的长度为 101 的时间向量。 + +.. code-block:: python + + # Create our raised-cosine filter + num_taps = 101 + beta = 0.35 + Ts = sps # Assume sample rate is 1 Hz, so sample period is 1, so *symbol* period is 8 + t = np.arange(num_taps) - (num_taps-1)//2 + h = np.sinc(t/Ts) * np.cos(np.pi*beta*t/Ts) / (1 - (2*beta*t/Ts)**2) + plt.figure(1) + plt.plot(t, h, '.') + plt.grid(True) + plt.show() + + +.. image:: ../_images/pulse_shaping_python2.png + :scale: 80 % + :align: center + +注意输出确实衰减到了零。我们使用每个符号 8 个采样点这一设定决定了这个滤波器看起来有多窄以及它衰减到零的速度有多快。上面的脉冲响应看起来就像一个典型的低通滤波器,我们实际上无法仅从外观上判断它是专门的脉冲整形滤波器还是其他普通的低通滤波器。 + +最后,我们可以用该滤波器对信号 :math:`x` 进行滤波并查看结果。不要过分关注代码中引入的 for 循环,我们会在代码块之后讨论它存在的原因。 + +.. code-block:: python + + # Filter our signal, in order to apply the pulse shaping + x_shaped = np.convolve(x, h) + plt.figure(2) + plt.plot(x_shaped, '.-') + for i in range(num_symbols): + plt.plot([i*sps+num_taps//2,i*sps+num_taps//2], [0, x_shaped[i*sps+num_taps//2]]) + plt.grid(True) + plt.show() + +.. image:: ../_images/pulse_shaping_python3.svg + :align: center + :target: ../_images/pulse_shaping_python3.svg + +这个结果信号是由许多脉冲响应叠加而成的,其中大约一半的脉冲响应先乘以了 -1。虽然看起来可能很复杂,但我们会一起来分析。 + +首先,由于滤波器和卷积运算的特性,数据前后会存在一些瞬态采样点。这些额外的采样点会包含在我们的传输中,但它们并不包含真正的脉冲 "峰值"。 + +其次,竖直线是在 for 循环中创建的,用于辅助可视化。它们旨在标示 :math:`Ts` 间隔出现的位置,这些间隔代表接收机对信号进行采样的位置。观察可以发现,在每个 :math:`Ts` 间隔处,曲线的值恰好为 1.0 或 -1.0,这使得它们成为理想的采样时刻。 + +如果我们要将这个信号上变频并发射出去,接收机就需要确定 :math:`Ts` 的边界在哪里,例如使用符号同步算法。这样接收机才能 *精确地* 知道何时采样以获得正确的数据。如果接收机采样稍早或稍晚,由于 ISI 的存在,它会看到略有偏差的值;如果偏差太大,接收到的就会是一堆奇怪的数字。 + +下面是一个使用 GNU Radio 创建的示例,它展示了在正确和错误的时刻采样时 IQ 图(即星座图)的样子。原始脉冲的比特值已标注在图上。 + +.. image:: ../_images/symbol_sync1.png + :scale: 50 % + :align: center + +下图代表理想的采样时刻以及对应的 IQ 图: + +.. image:: ../_images/symbol_sync2.png + :scale: 40 % + :align: center + :alt: GNU Radio simulation showing perfect sampling as far as timing + +与之对比,下图展示了最差的采样时刻。注意星座图中出现了三个聚类。我们恰好在每两个符号的正中间进行采样,因此采样值会有很大偏差。 + +.. image:: ../_images/symbol_sync3.png + :scale: 40 % + :align: center + :alt: GNU Radio simulation showing imperfect sampling as far as timing + +这是另一个不良采样时刻的例子,它介于理想情况和最差情况之间。注意此时出现了四个聚类。在高 SNR 下,我们或许勉强能用这个采样时刻,但这并不可取。 + +.. image:: ../_images/symbol_sync4.png + :scale: 40 % + :align: center + +请记住,时域图中没有显示 Q 值,因为它们近似为零,所以 IQ 图仅在水平方向上展开。 diff --git a/content-zh/sampling.rst b/content-zh/sampling.rst index 40065c79..1f72c9e7 100644 --- a/content-zh/sampling.rst +++ b/content-zh/sampling.rst @@ -18,7 +18,7 @@ IQ 采样是软件定义无线电(SDR)以及许多数字接收机(和发 麦克风是一种传感器,它将声波转换成电信号(电压)。这个电信号被 ADC(模数转换器)转换成声波的数字表示。 简化来说,麦克风捕捉声波,将声转换成电,然后将电转换成数字。可以看出,ADC 是模拟领域和数字领域之间的桥梁。 SDR 与麦克风惊人地相似,只不过使用天线而不是麦克风接收信号,它的内部也使用了 ADC。 -在两种情况中,电压水平都是由 ADC 进行采样的。你可以把 SDR 设备想象成以无线电波而不是声波为输入的麦克风。 +在两种情况中,电压水平都是由 ADC 进行采样的。你可以把 SDR 设备想象成以无线电波为输入、数字为输出的麦克风。 无论是处理声波还是无线电波,如果我们想要数字化捕捉、处理或保存一个信号,就必须对其进行采样。 采样这个过程看似简单,实际上还挺复杂。 diff --git a/content-zh/usrp.rst b/content-zh/usrp.rst index f8659772..7eec9153 100644 --- a/content-zh/usrp.rst +++ b/content-zh/usrp.rst @@ -43,9 +43,15 @@ Python 玩转 USRP .. code-block:: bash - sudo apt-get install git cmake libboost-all-dev libusb-1.0-0-dev python3-docutils python3-mako python3-numpy python3-requests python3-ruamel.yaml python3-setuptools build-essential # 译者注:这里可以提前配置 APT 源为清华源等镜像以加速下载 + sudo apt update + sudo apt install git cmake libboost-all-dev libusb-1.0-0-dev build-essential # 译者注:这里可以提前配置 APT 源为清华源等镜像以加速下载 + sudo pip install pybind11[global] + pip install numpy==1.26.4 docutils mako requests ruamel.yaml setuptools cd ~ git clone https://github.com/EttusResearch/uhd.git # 译者注:这里需要注意网络环境 + cd uhd + git checkout v4.8.0.0 + cd host mkdir build cd build cmake -DENABLE_TESTS=OFF -DENABLE_C_API=OFF -DENABLE_PYTHON_API=ON -DENABLE_MANUAL=OFF .. @@ -325,6 +331,12 @@ Python 玩转 USRP 为了 Debug,你可以通过检查 :code:`usrp.get_mboard_sensor("ref_locked", 0)` 的返回值来验证 10 MHz 信号是否传递到了 USRP。而对于 PPS 信号而言,如果它没有传递到 USRP,那么上面代码中的第一个 while 循环将永远不会结束。 +********************************************** +多台 B210 的相位相干同步(用于 MIMO) +********************************************** + +为了执行到达方向(DOA)估计和相控阵数字波束成形等操作,通常需要所有接收通道实现相位相干,即接收通道之间的相对相位保持恒定且可以通过校准消除。B200 和 B210 USRP 基于 AD9361 射频集成电路,其本振(LO)由芯片内部生成,无法外部馈入 LO。因此即使你为 USRP 提供了 10 MHz 参考信号和 PPS,也只能实现频率和采样时钟的同步,而无法实现相位同步。这是因为每次设备开机或改变频率时,VCO/PLL 链路中的分频器都会引入一个新的随机相位偏移,更多信息请参见 `此页面 `_ 。实现相位同步的一种方法是增加硬件,将校准信号(可以是 USRP 自身生成的信号、宽带噪声源或单音信号)分路后馈入所有接收端口,每次 USRP 开机或重新调谐时进行一次快速校准。请注意,更改增益也会导致相位偏移,但只要 B210 保持相同的增益,相位差不应发生显著变化。 `Techtile 项目 `_ 提供了关于此主题的更多信息,包括可能允许多台 B210 同步重新调谐以保持同步的自定义固件镜像,但每次无线电开机时可能仍需要使用外部硬件进行校准。 + **** GPIO **** diff --git a/index-zh.rst b/index-zh.rst index 80c5a042..52ff3505 100644 --- a/index-zh.rst +++ b/index-zh.rst @@ -21,4 +21,5 @@ content-zh/channel_coding content-zh/iq_files content-zh/multipath_fading + content-zh/pulse_shaping content-zh/about_author