
学信号与系统这门课时域分析是第一个绕不开的坎。每逢实验周最常见的情况是大家齐刷刷打开 MATLAB结果要么机器上没装授权要么大作业提交前才发现导出的图片惨不忍睹。其实用 Python 加 matplotlib 就能把这一套实验做得很顺手波形生成、时移反转、卷积运算、LTI 系统零状态响应全都能在几十行代码内完成而且免费、跨平台、方便复盘。这篇文章不是课程讲义而是我从完成一次“信号的时域分析”实验里整理出的完整实操记录覆盖环境搭建、典型信号生成、时域运算、卷积与系统响应以及大量课本上不会写的细节。无论你是正在做实验抓耳挠腮的同学还是想快速回顾时域分析工程实现的人都可以照着跑一遍再换成自己的参数去试。1. 时域分析到底要做什么1.1 先理清时域分析的核心脉络信号与系统里的“时域”字面意思就是以时间为自变量去看信号和系统。输入信号用 x(t) 表示输出用 y(t) 表示系统的作用被抽象成一种对时间波形的“加工”。时域分析的本质就是直接研究这种加工过程信号长什么样、做了哪些运算、系统冲激响应如何、零状态响应怎么算。听起来很理论但一旦落到图和代码里就特别直观。在这个实验里我习惯把任务拆成四层。第一层是典型信号生成比如单位阶跃、单位冲激、正弦、指数衰减、矩形脉冲这些是最基础的构建块。第二层是信号的基本运算时移、反转、尺度变换、加减乘目的是理解数学表达式和波形之间的一一对应关系。第三层是卷积这是时域分析的重中之重两个信号相卷积的结果经常就是某个系统对输入的响应。第四层是线性时不变系统分析给定系统冲激响应 h(t)求输入 x(t) 作用下的零状态响应 y(t)卷积的计算价值在这里体现得最明显。很多同学做实验时容易陷入“只要画出图就算完成”的状态。其实画图只是验证手段真正要掌握的是怎么把连续时间数学公式变成计算机里可处理的数组。这里存在一个关键的思维切换计算机里没有真正的连续信号只有密集采样后的离散序列。所以做时域分析实验本质上是在用离散数值方法近似连续时间问题理解这一点后面很多参数设置就顺理成章了。1.2 实验任务拆解与工具链选择具体到实验报告常见要求无非这几项画出常见连续时间信号和离散时间信号的波形。验证信号时移、反转、尺度变换对波形的影响。计算信号的能量与平均功率。用卷积求两个信号的卷积结果画出卷积波形。给定系统冲激响应或微分方程求输入信号作用下的零状态响应。每一项用 Python 都有对应的库。NumPy 负责数组运算和信号采样SciPy 提供卷积、滤波、线性系统仿真等高级工具matplotlib 负责出图。用过 MATLAB 的人会发现映射关系很直接MATLAB 的plot对应plt.plotstem对应plt.stemconv对应np.convolvelsim对应scipy.signal.lsim。基本不需要额外学什么复杂的语法上手阻力很小。工具选型上我坚持用 Python主要有三个原因。一是免费学生自己电脑上没有授权限制实验室批量安装也容易二是过程透明每一步中间变量都是普通数组你可以随时print出来检查不像黑盒工具箱那样难以调试三是复用性好同样的代码稍微改改就能继续做后续的频谱分析、离散系统实验不需要换语言重新学习一套绘图接口。当然MATLAB 在专业工具箱和文档完整性上仍然有优势但针对本科阶段的时域分析实验Python 已经绰绰有余。2. 环境准备与典型信号生成2.1 环境搭建与连续信号仿真思路动手写代码前先把环境弄干净。我用的是 Python 3.9 以上版本依赖就三个numpy、matplotlib、scipy。安装命令很常规pip install numpy matplotlib scipy如果你用的是 Anaconda 这类集成环境这三个包一般已经预装。版本方面不用追新numpy 1.24 以上、matplotlib 3.7 以上、scipy 1.10 以上足够覆盖实验需求。连续信号仿真的思路我在前面已经点破了用密集的离散时间点近似连续时间。比如想模拟从 0 秒到 10 秒的时间范围可以这样定义时间轴import numpy as np dt 0.01 # 采样间隔越小波形越平滑 t np.arange(0, 10 dt, dt)这里dt是仿真采样间隔可以把它看成相邻两个采样点之间的时间步长。dt越小曲线越光滑但计算量也越大。对于普通实验波形dt 0.01已经是很好的精度画出来人眼看不出折线感。如果要模拟更高频的信号就得相应减小dt保证一个周期里至少有 10 个以上采样点否则波形会明显失真。有一个很容易被忽略的细节用np.arange(start, stop, dt)时stop通常不会刚好落在你想要的终点上。我用10 dt作为终点就是为了让最后一个点尽量接近 10。如果直接写成np.arange(0, 10, 0.01)最后一个点可能是 9.99虽然差别极小但计算信号周期或做积分时端点差异会带来少量误差。2.2 典型信号的 Python 实现各类典型信号是时域分析的基石。我在这里给出最常用的一组写法你可以直接写成一个函数库后续所有实验都复用。单位阶跃信号最容易写也有很多种表达u (t 0).astype(float) # t0 为 True赋值成 1.0 # 或者用 numpy 自带函数 u2 np.heaviside(t, 0.5) # 在 t0 处取 0.5两种写法在多数场景下等价只有 t0 这点不同。严格讲连续单位阶跃信号在原点的定义有多种约定常见教材取 1也有取 0.5 的。做数值仿真时(t 0)这种写法让 t0 处为 1适合大多数时域响应计算np.heaviside(t, 0.5)则更符合某些定义。到底用哪个取决于你的教材约定但不要两种混用否则报告里会出现莫名其妙的 0.5 偏差。矩形脉冲可以基于阶跃信号构造也可以用布尔索引rect ((t 0) (t 2)).astype(float) # 0~2 秒内为 1正弦信号和指数衰减信号则更直接A, f, phi 1.0, 1.0, 0.0 x_sin A * np.sin(2 * np.pi * f * t phi) alpha 1.0 x_exp np.exp(-alpha * t) * (t 0)冲激信号是时域实验里最常出问题的一个。连续时间单位冲激 δ(t) 是一个理想化信号计算机里无法直接生成常规做法是用一个宽度为dt、高度为1/dt的窄矩形脉冲去近似它。这样做的目的是保证脉冲面积为 11/dt * dt 1。代码长这样delta np.zeros_like(t) delta[np.abs(t) dt / 2] 1.0 / dt画出来是一根近似“针”的矩形。如果你把dt取得很小高度会非常大这容易让人误以为是程序 bug。其实这是正常现象连续冲激的“强度”体现在面积而不是某一点的高度近似脉冲的高度由采样间隔决定dt越小越接近理想冲激。抽样信号sinc在时域分析里也常用用 numpy 写特别方便T 1.0 s np.sinc(t / T)注意np.sinc(x)定义为sin(pi*x)/(pi*x)和通信领域的Sa(x)sin(x)/x差了一个 π 因子。用之前务必看公式对应的定义否则画出来的曲线位置会跟教材对不上。2.3 画图细节连续曲线与离散杆图信号生成后接下来就是画图。matplotlib 的plot适合连续时间信号画出来是光滑折线离散时间信号则要画成离散序列用stem更专业。以一组典型信号对比图为例import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] # 防止中文乱码按系统调整 plt.rcParams[axes.unicode_minus] False # 负号正常显示 fig, axs plt.subplots(2, 3, figsize(12, 6)) axs[0, 0].plot(t, u, linewidth1.2) axs[0, 0].set_title(单位阶跃信号) axs[0, 1].plot(t, rect, linewidth1.2) axs[0, 1].set_title(矩形脉冲信号) axs[0, 2].plot(t, x_sin, linewidth1.2) axs[0, 2].set_title(正弦信号) axs[1, 0].plot(t, x_exp, linewidth1.2) axs[1, 0].set_title(指数衰减信号) axs[1, 1].plot(t, delta, linewidth1.2) axs[1, 1].set_title(冲激信号近似) # 离散信号示例 n np.arange(-5, 6) xn np.cos(0.5 * n) axs[1, 2].stem(n, xn, linefmt-, markerfmto, basefmt ) axs[1, 2].set_title(离散时间余弦信号) for ax in axs.flat: ax.grid(True, linestyle--, alpha0.6) ax.set_xlabel(t/s) plt.tight_layout() plt.show()画离散信号时plt.stem的三个参数值得留意linefmt控制竖线样式markerfmt控制圆点样式basefmt控制基线。基线的默认样式是一条实线但隔很远看就很碍眼所以用basefmt 去掉。新版 matplotlib 的stem已经不需要use_line_collectionTrue这个历史参数加了反而可能收到警告直接删掉即可。绘图规范看起来是小事但实验报告里的图是否专业很多时候就差在这些细节。坐标轴标签、网格线、图例、标题一个都不能少否则老师拿到图根本不知道你画的是什么参数下的信号。3. 信号的基本时域运算实现与可视化3.1 时移、反转与尺度变换的向量化实现时域运算里最基础的是时移、反转和尺度变换。用 Python 实现时最大的坑在于“数组索引对应时间点”不能像数学公式那样随意替换自变量。先说时移。若原信号是 x(t)时移后的信号是 y(t)x(t-t₀)向右平移时 t₀ 为正。核心思路是在新时间轴 t 上的每个点取原信号在 t-t₀ 处的值。如果原信号在同一个离散时间轴 t 上已经定义好可以直接用np.interp插值t0 1.0 y_shift np.interp(t - t0, t, x, left0, right0)np.interp的四个参数依次是查询点、原时间轴、原信号值、越界时的填充值。这里把越界部分填成 0避免时移出边界时出现意想不到的“卷绕”。如果不写left和rightnumpy 会默认用最边上的值填充导致波形两端出现不该有的平台这是非常隐蔽的错误。反转信号的实现同理y_reverse np.interp(-t, t, x, left0, right0)尺度变换 y(t)x(a·t)关键是理解时间轴压缩和拉伸的方向。a1 时波形被压缩a1 时波形被拉伸。代码还是插值a 2.0 y_scale np.interp(a * t, t, x, left0, right0)在实际实验报告里最好把原始信号和运算后的信号画在同一张图上用不同颜色区分再在标题里注明参数。比如“若 a2波形在时间轴上压缩一半”这类结论直接看波形就能记住。3.2 加减乘运算与时间轴对齐信号相加、相乘看起来简单实际中却容易因为时间轴没对齐而出错。如果两个信号共用同一个t数组那直接做数组加减乘就可以x1 np.sin(2 * np.pi * 1.0 * t) x2 (t 0).astype(float) x_add x1 x2 x_prod x1 * x2但如果你从两个不同的实验里拿到了长度不同的信号想加在一起就必须先把它们插值到公共时间轴上。一个稳妥做法是以较密的那个时间轴为基准用np.interp把另一个信号重采样过去。千万不要直接x1[:len(x2)] x2那样会悄悄丢弃采样点而且时间戳完全错位。乘法的典型应用是“信号调制”。比如用一个高频余弦信号去乘矩形脉冲就得到了一个“门控”后的振荡波形x_mod rect * np.cos(2 * np.pi * 10.0 * t)这类运算在信号与系统后续的频谱搬移章节里会反复出现。时域实验里先看明白波形等到频域实验里再理解频移规律会顺很多。3.3 信号能量与功率的数值计算时域分析里除了波形还经常要算能量和平均功率。连续时间信号的能量定义是平方信号在全时域上的积分数值上用梯形法或简单矩形法都能实现E np.trapezoid(np.abs(x_sin) ** 2, t)注意 numpy 的接口有过一次调整老版本是np.trapz新版本推荐np.trapezoid。如果你用的版本比较老写成np.trapz也能跑但会提示 DeprecationWarning。对于有限能量的非周期信号平均功率按总能量除以持续时间算。比如前面那个矩形脉冲如果高度为 1、宽度为 2 秒那么能量约为 2平均功率约为 1。对周期信号来说平均功率要在一个周期上积分再除以周期。以正弦信号 A·sin(2πft) 为例理论平均功率是 A²/2有效值是 A/√2。写代码时可以用足够多的整周期样本去验证这个公式进一步确认数值积分的正确性。我自己做实验时会习惯先理论手算出结果再用代码跑一遍做交叉验证。比如正弦幅值 A2理论功率就是 2数字积分结果应该非常接近 2.00。如果差异超过 1%通常不是计算问题而是时间轴没有取到整数周期或者dt太大导致的数值误差。4. 卷积与系统时域响应分析4.1 卷积的直观理解与离散实现卷积是时域分析里最核心的运算。它的数学形式是积分但如果只看公式很容易迷失在求和符号里。我更愿意把它理解成“一个信号翻转后从另一个信号上滑过去在每个时刻求重叠区域加权面积”。这个比喻虽然不严格但对理解波形形状非常有帮助。离散卷积在 numpy 里一行就好x np.array([2.0, 1.0]) h np.array([1.0, 3.0, 1.0]) y np.convolve(x, h, modefull)输出长度等于len(x) len(h) - 1结果是 [2, 7, 5, 1]。为什么是这个长度因为卷积本质上是在做多项式乘法两个序列“错位相乘相加”后横向跨度必然是两个原始跨度之和减 1。如果只是在课堂上做验证modefull最直观。但如果你想得到跟输入长度一样的输出可以用modesame它会从 full 结果里剪出一段。选取的起点规则有点绕我的建议是先用 full 把完整波形看清楚再考虑要不要 same。4.2 连续卷积近似为什么要乘以 dt连续卷积定义是积分而计算机只能做求和。把积分中的微元 dt 显式地提出来这是连续卷积实验里最容易丢分的地方。例如宽度为 2 秒、高度为 1 的矩形脉冲与自身卷积会得到一个三角形脉冲顶点高度应该是 2。如果你直接写np.convolve(rect, rect)得到的峰值大约是 200因为两个 rect 各有约 200 个采样点。只有写成y dt * np.convolve(rect, rect, modefull)峰值才会回到 2 附近。这里乘dt的本质是用离散和近似连续积分时要补上时间步长。忘记这一步所有卷积结果的纵轴都会整体放大而且放大倍数还跟采样密度有关属于最容易暴露“照抄代码却没理解原理”的错误。除了乘 dt连续卷积结果的时间轴也要重新计算。如果两个序列都是从 t0 开始采样卷积结果的第 0 个点对应 t0之后每个点相差 dt所以时间轴是t_conv np.arange(len(y)) * dt但如果输入序列的起点不是 0比如 x 从 t-2 开始、h 从 t1 开始那么卷积输出的起点理论上要相加而不是默认为 0。对于大多数课程实验建议大家把信号统一设成从 0 开始省掉这个坑。4.3 完整案例矩形脉冲通过一阶 RC 系统的响应下面给一个可以直接跑、能写进实验报告的综合案例。假设输入是一个 0 到 2 秒的矩形脉冲幅度为 1系统是一阶 RC 低通网络时间常数 RC1 秒。这个系统的冲激响应为h(t) (1/RC) * e^(-t/RC) * u(t)用卷积求零状态响应再和微分方程数值解做个对比能同时验证两个方法。代码框架如下import numpy as np import matplotlib.pyplot as plt RC 1.0 dt 0.01 t np.arange(0, 12 dt, dt) # 输入矩形脉冲 x ((t 0) (t 2)).astype(float) # 冲激响应 h (1 / RC) * np.exp(-t / RC) * (t 0) # 卷积求响应 y_conv dt * np.convolve(x, h, modefull) t_conv np.arange(len(y_conv)) * dt # 直接用欧拉法解微分方程RC * y y x y_euler np.zeros_like(t) for i in range(1, len(t)): dy (x[i - 1] - y_euler[i - 1]) / RC y_euler[i] y_euler[i - 1] dy * dt # 绘图 plt.figure(figsize(8, 5)) plt.plot(t, x, label输入 x(t), linestyle--, linewidth1.5) plt.plot(t_conv, y_conv, label卷积结果, linewidth1.5) plt.plot(t, y_euler, label欧拉法数值解, linewidth1.5) plt.axvline(RC, colorgray, linestyle:, labelRC 时间常数) plt.xlim(0, 8) plt.ylim(0, 1.2) plt.xlabel(t / s) plt.ylabel(幅度) plt.title(矩形脉冲通过一阶 RC 系统的时域响应) plt.grid(True, linestyle--, alpha0.6) plt.legend() plt.tight_layout() plt.show()这里要特别留意卷积结果只画到 0~8 秒。为什么不全画因为全np.convolve的结果长度接近 2400 个点时间轴到了 24 秒但系统响应在 8 秒之后已经非常接近零画出来只是给图面右侧加一条长尾巴反而看不出细节。做实验时你应该主动控制横轴范围让关注的信息尽量占满画布。从结果波形上能看到几个典型特征矩形脉冲的上升沿经过 RC 系统后变成了指数上升不再陡峭脉冲结束后输出没有立刻归零而是按时间常数 RC 指数衰减峰值大约出现在 2 秒附近但比输入跳变略有延迟。这些现象都可以在实验报告里定性解释再和理论冲激响应卷积结果对照。5. 常见问题与排查技巧实录5.1 中文乱码与负号显示新手画图最常遇到的就是中文变方框、坐标轴负号显示出乱码。在代码开头做好全局设置即可plt.rcParams[font.sans-serif] [SimHei, Noto Sans CJK SC] plt.rcParams[axes.unicode_minus] False第一行让 matplotlib 支持中文字体第二行让负号用 ASCII 方式显示。Windows 下一般用SimHeiLinux 或 macOS 可能要用系统中文字体名。如果设置完还是乱码就换英文标签实验报告里也能接受毕竟目标是表达清楚波形含义。5.2 波形失真与数值坑数值实验最大的敌人是采样率不足。画正弦信号时如果dt太大一个周期里只有 4、5 个点折线会明显变成“锯齿”更严重的是视觉上周期可能被看错。我的经验是一个周期至少采样 20 个点画面才足够平滑也基本不会引起误解。对于频率为 f 的信号时间步长至少要小于 1/(20 f)。卷积计算慢是另一个高频问题。np.convolve用的是直接法当两个序列长度都上万时能明显感觉到卡顿。如果实验中需要长序列卷积换成 FFT 加速版from scipy.signal import fftconvolve y dt * fftconvolve(x, h, modefull)结果和直接卷积在数值误差范围内一致但速度快很多。长序列尤其是做系统长时响应时这个替换几乎感觉不到差别。关于连续卷积的幅值和时间轴我再啰嗦一遍别忘了乘dt别忘了重新构造卷积时间轴。这两个问题我见过太多次几乎成了实验报告的标准扣分点。5.3 实验报告与出图技巧实验报告里的图不仅要“能看”最好还要“好看且信息完整”。我的常规做法是保存成 PDF 矢量图而不是 PNG 截图这样在文档里放大也不会糊。保存代码一行就行plt.savefig(result.pdf, bbox_inchestight)bbox_inchestight会自动去掉四周多余空白让图片紧凑。如果保存 PNG建议dpi300。多信号对比时用不同线型和透明度区分。实线画结果虚线画输入点线画理论参考图例放在不遮挡曲线位置。必要时用plt.axvline或plt.axhline标记特殊时间点和幅值比如 RC 时间常数、峰值点。一张图容纳三四条曲线已经接近极限再多就乱不如拆成子图。6. 这些实验做完后的一点体会6.1 我踩过最值得说的一次坑第一次跑一阶 RC 系统响应时我没有乘dt直接拿矩形脉冲和冲激响应做卷积结果输出波形幅度大得离谱。当时的第一个念头是“代码写错了”检查半天却发现np.convolve的调用完全正确。后来才反应过来连续卷积的离散近似必须要带时间步长这个问题不是语法错误而是数学模型到数值模型的映射问题。从那次以后我每写一步代码都会先在纸上把离散化公式写出来再对照着填参数。另一个让我印象深刻的坑是时间轴偏移。离散卷积输出长度等于两序列长度之和减一这个结论大家都知道但真正画图时很多人会草率地沿用输入的时间轴结果图形右移了一大截。如果你把输入 x(t) 和卷积结果 y(t) 画在同一张图上横轴必须用各自的正确时间轴不能偷懒共用一个t。6.2 这套方案还能怎么扩展做完时域分析实验之后后面的很多内容都可以复用这套代码。最简单的扩展是把信号生成函数封装成一个模块之后做频域分析时继续调用省得每个实验都从开头矩阵慢慢敲。进阶一点可以用scipy.signal.lsim替代手写欧拉法直接仿真任意传递函数的系统响应也可以用np.correlate或scipy.signal.correlate做相关运算理解相关和卷积之间的镜像关系。如果你正在做信号与系统实验我的建议是不必急着抄完整代码而是先自己手算一个简单例子的理论值再用代码验证。这样虽然多花十几分钟但波形图上的每一条曲线在你眼里都会“透明”起来。后面再遇到奇怪的结果你也能更快判断是参数问题、采样问题还是模型本身的问题。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。