ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

92页MATLAB信号与系统实验报告复现与二次开发指南

92页MATLAB信号与系统实验报告复现与二次开发指南

简介:面向广东工业大学信息工程学院电子信息类专业学生的《信号与系统》实验报告,适合课程实验、MATLAB仿真入门及期末复习场景使用。报告围绕连续与离散时间信号的表示和运算、卷积运算、连续/离散LTI系统时域分析,以及傅里叶级数、傅里叶变换、拉普拉斯变换和Z变换等实验展开,逐章给出实验目的、实验内容、MATLAB代码与结果图,可帮助读者理解采样、量化对信号的影响,掌握卷积与系统响应的求解流程。资源包共1个docx文件,约1.68MB,正文约92页,目录按实验一至实验四及章节模块组织,结构完整,便于按知识点检索与复盘。已有1257人学习下载,适合广工电子信息类学生及需要补强信号处理实验操作的学习者参考,也可作为实验报告撰写与MATLAB实现思路的样例。

1. 从 92 页实验报告的复现说起

实验报告卡壳的时候,问题通常不在公式,而在把教材里的 f(t) 翻译成 MATLAB 里的一个数组:t 取多长、步长给多少、阶跃怎么表示、卷积之后时间轴怎么重建。《信号与系统》实验报告这份资源正好把这条链路完整走了一遍——92 页、四组实验、十四个章节,从连续时间信号的波形到傅里叶变换、拉普拉斯变换、Z 变换,每个知识点都配了能直接跑的 MATLAB 代码、结果图和分析。它适合三类人:正在上信号与系统实验课、要交报告的学生;想把 MATLAB 信号处理捡回来的开发者;以及想搭一套教学演示模板的人。下面按我拆这份资源的顺序,从目录结构讲到核心代码,再讲怎么改。

2. 目录结构拆解:四组实验和十四个章节怎么排

2.1 报告骨架:四组实验串起连续到离散的主线

这份报告不是按教材章节顺序写的,而是按实验分组组织。目录编号本身就带着一点"历史遗留"味道:第五章出现在实验二下面,第十二、十三章又并进了实验二,第十四章单独收尾在 Z 变换。第一次翻的时候容易迷路,我把它按知识点重新对齐了一遍,下面这张表是拆完之后的结构。

实验分组覆盖章节核心知识点关键函数
实验一第二、三、四章连续时间信号的 MATLAB 表示、时移反褶尺度变换、卷积运算cos、exp、square、sinc、fliplr、conv
实验二第五、十二、十三章连续 LTI 系统时域分析、离散时间信号表示与运算、离散 LTI 时域分析conv、filter、stem、impulse、step
实验三第六、七、八章周期信号傅里叶级数、傅里叶变换及其性质、系统频率特性与频域分析fft、abs、angle、freqs、bode
实验四第十、十一、十四章拉普拉斯变换、连续系统零极点分析、Z 变换与 Z 域分析laplace、pzmap、zplane、residue

这张表最大的用处是定位。比如你想找"卷积"的代码,目录里可能出现在第四章,也可能在第十三章的离散卷积里,两者的写法完全不同——连续卷积要乘dt近似积分,离散卷积直接用conv求和就够了。分不清这两处,跑出来的幅值会差一个数量级,这是后面避坑章会展开的坑之一。

从技术选型看,这份报告用的是纯 MATLAB 命令行脚本,没有 App Designer,没有 Simulink,也没有用 Live Script。所有代码都写成>>开头的交互式命令,直接粘进命令窗口就能跑。这种写法在工程上不算优雅,但对实验报告场景特别合适:一段代码对应一个结论,读者可以逐行验证,出错时也能立刻定位到是哪一行。

2.2 每节的五段式结构:目的、内容、代码、结果、分析

这份报告最值得借鉴的不是某段代码,而是每一节的固定结构。打开任意一章,基本都是这个顺序:实验目的、实验内容、实验代码、实验结果图、结果分析。看上去像模板,但它恰好对应了"懂原理、会写码、能验证"三步。

对于二次开发的人来说,这个结构的好处是每一小节天然是一个可独立运行的单元。你完全可以把 2.3.1 里画2cos(3t+pi/4)的那段代码单独拎出来,放进一个函数里,然后批量跑、批量出图。我在拆这类报告时的习惯是,先按这个五段式做个映射表,把"目的"对应到验证条件,"内容"对应到输入参数,"代码"对应到函数体,"结果"对应到断言,这样整份报告就能被自动检查。

提示:把五段式搬进代码时,最省事的做法是在每个脚本开头用注释写清楚这一段要验证什么公式,比如% 验证:f(t)=2cos(3t+pi/4),振幅 2、角频率 3、初相 pi/4。以后改参数时,看注释就知道该盯哪几个量。

2.3 藏在正文里的函数文件:uCT.m 与 functl.m

整份报告有两处关键的自定义函数,正文里只在代码块中间提了一句,很容易被忽略。第一处是uCT(t),用来表示单位阶跃信号;第二处是functl.m,把一个分段线性信号写成了一个独立函数文件。

% uCT.m:单位阶跃信号,等价于教材里的 u(t) function y = uCT(t) y = (t >= 0); % 逻辑数组直接返回,参与运算时自动转成 0/1 end

这段代码只有两行,但它是整份报告里所有门函数的基础。之所以要单独写一个函数,是因为在 MATLAB 里u(t)不是内置函数,如果每处都写(t>=0),一是代码长,二是做时移时容易漏括号。把它封成函数之后,uCT(t-1)就天然带上了时移语义,写起来和教材公式一一对应,可读性立刻上来了。

functl.m的作用类似,只是它描述的是一个具体的分段信号。这两个文件必须放在当前工作目录,或者用addpath把所在文件夹加入搜索路径,否则调用时会报"未定义函数"。这一点报告里没写,但它是新手跑不起来的第一大原因,后面避坑章会专门说。

2.4 运行环境与目录约定

报告没有标注 MATLAB 版本,但从用到的函数推断,R2014b 之后的版本都能跑。square和sinc这两个函数来自 Signal Processing Toolbox,如果装的是精简版 MATLAB,运行到 2.3.3 和 3.3.1(2) 就会报未定义。

碰到这种情况不用换环境,自己补两个替代实现就行:square可以用方波符号函数拼,sinc用sin(pi*x)./(pi*x)并单独处理零点。这属于典型的"环境不对但代码思路没错",先把替代实现写进自己的工具函数里,后面所有章节都能复用。

目录上建议按实验分组建四个文件夹,每个文件夹里放脚本和该组用到的函数文件。原因是不同实验组会用到同名的变量(比如几乎每节都叫t和y),一个文件夹放全部脚本时,运行顺序一乱就容易拿着上一节的y去画图,跑出来的图看着像对的,其实完全错了。

3. 连续时间信号的表示:从时间向量到复信号四联图

3.1 时间向量是一切的底座

报告里所有波形都从一个时间向量开始。第一个例子是画2cos(3t+pi/4),代码只有五行,但每行都有讲究。

% 2.3.1(1) 画出 2cos(3t+pi/4) 的波形 t = -pi/12:0.01:2*pi; % 起点、步长、终点,步长决定波形顺滑度 y = 2*cos(3*t + pi/4); % 逐点计算,y 与 t 长度必须一致 plot(t, y); grid on; % 打开网格,方便读振幅和周期 axis([-pi/12, 2*pi, -2.2, 2.2]); % 手动锁死坐标范围 title('2.3.1(1)');

0.01这个步长在这份报告里反复出现,它不是随便定的。信号里最高角频率是 3,对应周期约 2.09,一个周期内大约 209 个采样点,画出来的曲线足够平滑;如果步长给到 0.5,一个周期只剩 4 个点,plot会把折线画成锯齿,看上去像信号失真,其实是采样不够。

axis那行是另一个容易被忽略的细节。四个参数依次是 x 轴最小、最大,y 轴最小、最大。手动指定范围的好处是同一组实验里的几张图可以直接对比幅值;坏处是如果你改了信号参数忘了改axis,波形可能被裁掉一截,图上什么都看不见。我一般会先plot看一眼自动范围,再决定要不要写死。

3.2 阶跃与门函数:用逻辑数组代替 u(t)

第二个例子画(2-e^(-t))u(t),报告里的代码是这样的:

t = -2:0.01:4; y = (1 - exp(-t)) .* (t >= 0); % (t>=0) 是逻辑数组,乘上去等价于乘 u(t) plot(t, y); grid on; axis([-2, 4, -0.2, 1.2]);

这里有两个语法点值得单独拎出来。第一是.*,它是逐元素乘法,左边是数值数组、右边是逻辑数组,MATLAB 会自动把true/false转成1/0再逐点相乘。第二是不能写成*,矩阵乘法在长度不一致时会直接报错,而长度一致时会算出完全错误的结果——这种错误比报错更可怕,因为图能画出来,只是画的是别的东西。

同一个门函数的写法还有另一种,报告在画矩形脉冲时用的是相减法:(t>=0) - (t>=1)。两个阶跃相减,在[0, 1)区间上得到 1,其他地方是 0,正好是一个宽度为 1 的门。这个技巧在functl.m里被用到了极致,四个分段全靠它拼出来,后面第 4 章会逐行拆。

注意:t>=0返回的是 logical 类型,如果直接拿去做axis之外的索引运算,记得先转 double;混在某些老版本工具箱函数里会报类型不匹配。

3.3 复信号四联图:real、imag、abs、angle

复信号是这份报告里视觉信息密度最高的一节。一个复指数信号要看实部、虚部、模、相角四个量,报告用subplot(2,2,k)排成田字格。

t = -5:0.01:10; y = 2 + exp(1i*(pi/4)*t) + exp(1i*(pi/2)*t); % 1i 是虚数单位,避免和变量 i 冲突 subplot(2,2,1); plot(t, real(y)); title('实部'); grid on; subplot(2,2,2); plot(t, imag(y)); title('虚部'); grid on; subplot(2,2,3); plot(t, abs(y)); title('模'); grid on; subplot(2,2,4); plot(t, angle(y)); title('相角'); grid on;

subplot(m,n,k)的含义是把画布分成 m 行 n 列,当前在第 k 个格子里画,编号从左到右、从上到下。四个函数分别取实部、虚部、模、相角,其中angle返回的是弧度,范围在[-pi, pi],所以相角图会出现跳变——这是数学上的正常现象,不是画错了。如果想让曲线连续,可以在angle外面套一层unwrap,这一点报告里没提,但在做相位分析时几乎是必备操作。

1i而不是i是个小习惯。i和j在 MATLAB 里既是虚数单位也是常见循环变量,一旦你在前面写过for i = 1:10,后面的exp(i*t)就会拿循环残留的值去算,结果莫名其妙。用1i可以彻底避开这个玄学问题。

3.4 矩形脉冲:square 的幅度和占空比

第三节要产生一个幅度为 1、周期为 1、占空比 0.5 的矩形脉冲,报告给的代码是:

t = -0.2:0.01:1.2; y = 0.5 * square(2*pi*t, 50); % 第一参数是角频率,第二参数是占空比百分比 plot(t, y); axis([-0.2, 1.2, -0.7, 0.7]); grid on; title('2.3.3');

square的第一个参数是角频率,2*pi*t对应周期为 1;第二个参数 50 表示占空比 50%,即半个周期高、半个周期低。它的输出是 ±1 的方波,乘 0.5 之后变成 ±0.5,峰峰值正好是 1。

这里有个语义上的分歧值得说清楚:报告要求"幅度为 1",而代码得到的是 ±0.5。如果按单极性脉冲理解,幅度应该是 0 到 1;如果按双极性理解,幅值 0.5、峰峰值 1。两种理解都能自圆其说,但如果老师按单极性判,这张图就要扣分。我的做法是先看实验要求里的坐标范围,如果 y 轴是[-1, 1]就按双极性写,是[0, 1]就加 0.5 抬到单极性。这种细节在报告里不会写,但它决定了你的图能不能对上参考答案。

4. 信号运算与卷积:函数体不动,只改入参

4.1 时移、反褶、尺度变换的四种写法

这一节是整份报告里设计得最巧的部分。它先定义了一个基准信号functl.m,然后所有的变换都不改函数体,只改传进去的自变量。

t = -3:0.01:3; y1 = functl(t - 1); % 右移 1 y2 = functl(2 - t); % 反褶后再右移 2 y3 = functl(2*t + 1); % 时间压缩 2 倍,再左移 1 y4 = functl(4 - t/2); % 时间展宽 2 倍,再反褶、右移

这种写法的核心思想是:functl只认一个自变量,你把什么表达式传进去,它就算什么。t-1代入原函数相当于 f(t-1),波形右移一格;2-t相当于 f(-(t-2)),先反褶再右移两格;2*t+1相当于 f(2(t+0.5)),先压缩再左移半格;4-t/2相当于 f(-(t-8)/2),先展宽两倍、再反褶、再右移八格。

真正容易翻车的是尺度变换之后的显示范围。时间被压缩 2 倍,有效区间也跟着缩了一半,如果t还是原来那个-3:0.01:3,图上会出现大片空白,或者信号被挤在中间一小段。报告在每个子图后面都单独写了axis,就是在处理这个问题。我一般会把axis和变换一起写,改一处参数就顺手改一处范围,省得来回找。

4.2 functl.m 逐段拆解:逻辑数组叠加法

基准信号长这样:在[-2,-1)上是一条斜率 1 的直线,[-1,0)上恒为 1,[0,1)上恒为 2,[1,2]上是一条斜率 -1 的直线。手工画很简单,写成代码就需要一点技巧。

function f = functl(t) % 分段线性信号:用门函数逐段叠加 % 门函数写法 (t>=a) - (t>=b) 在 [a,b) 上取 1,其余为 0 f = (t + 1) .* ((t >= -2) - (t >= -1)) ... % 第 1 段:斜率 1,区间 [-2,-1) + 1 .* ((t >= -1) - (t >= 0)) ... % 第 2 段:常数 1,区间 [-1,0) + 2 .* ((t >= 0) - (t >= 1)) ... % 第 3 段:常数 2,区间 [0,1) + (-t + 2) .* ((t >= 1) - (t >= 2)); % 第 4 段:斜率 -1,区间 [1,2] end

这段代码的精髓在门函数(t>=a) - (t>=b)。两个逻辑数组相减,在[a, b)上得到 1,在区间外得到 0,等价于一个矩形窗。把每段的表达式乘上对应的窗再相加,就得到了完整的分段函数,不需要if-else,也不需要循环,整段代码向量化执行,输入多长的t都能一次性算完。

这种写法的好处在二次开发时特别明显。你要加一段,只需要在末尾再加一行表达式 .* 门函数;要改某段斜率,改那个表达式就行,其他段完全不受影响。比起用for循环逐点判断,向量化写法在点数上万时快得不是一点半点。

4.3 奇偶分量分解:fliplr 的适用条件

奇偶分量这一节,报告给的代码是:

t = -3:0.01:3; y = (t >= 0) .* (t <= 2) .* (1 - t); % 只定义在 [0,2] 上的三角脉冲 y1 = fliplr(y); % 时间反褶:左右翻转数组 ye = (y + y1) / 2; % 偶分量 yo = (y - y1) / 2; % 奇分量

fliplr是左右翻转,它把数组第一个元素和最后一个元素交换,第二个和倒数第二个交换,以此类推。在t关于 0 对称的前提下,数组翻转等价于时间反褶 f(-t)。

这里有个隐含条件:t必须严格关于 0 对称,而且采样点数是奇数,保证 0 正好落在数组正中间。如果t = 0:0.01:3,翻转之后得到的是 f(3-t),不是 f(-t),算出来的"偶分量"根本没有物理意义。报告里恰好用了对称区间,所以没暴露这个问题。

更稳妥的做法是对任意时间轴做重采样:

ye = (y + interp1(t, y, -t, 'linear', 0)) / 2; % -t 落在原区间外时补 0

interp1把y在-t这些点上重新插值,超出范围的部分用 0 填充。比fliplr多两行,但对时间轴没有要求,算是给这个技巧买的一份后悔药。

4.4 卷积:conv 之后时间轴必须重建

连续时间卷积在 MATLAB 里只能用数值近似,报告的写法是把两个信号离散化后做求和卷积,再乘采样间隔还原量纲。

dt = 0.01; t1 = 0:dt:2; x = (t1 >= 0) - (t1 >= 1); % 宽度为 1 的门函数 t2 = 0:dt:3; h = exp(-2*t2); % 指数衰减的冲激响应 y = conv(x, h) * dt; % 数值卷积,乘 dt 近似积分 ty = (t1(1) + t2(1)) : dt : (t1(1) + t2(1)) + (length(y) - 1) * dt; plot(ty, y); grid on;

conv输出的长度是length(x) + length(h) - 1,不是原来两个向量中的任何一个。所以ty不能用t1代替,必须重新生成:起点是两个起点之和,步长还是dt,点数跟卷积结果一致。这一步漏掉,plot会直接报向量长度不一致;如果强行截断,图能画出来但时间轴是错的,波形形状对、位置全歪。

乘dt那一步也不是可有可无。离散卷积算的是求和Σ x[k]h[n-k],连续卷积是积分∫x(τ)h(t-τ)dτ,两者差一个dt因子。不乘的话,幅值会随采样密度变化——你把步长从 0.01 改成 0.001,算出来的峰值会大 10 倍,这种"参数一改结果就变"的现象最容易让人怀疑公式写错了。

验证卷积结果是否正确,除了看图形,还有个偷懒办法:拿解析解比。比如门函数和指数衰减卷积,理论峰值可以手算出来,用max(y)和手算值对一下,误差在 1% 以内就说明数值实现没问题。这个习惯我保持到现在,比盯着图看靠谱得多。

5. 避坑与排查:跑不出图、结果不对的五处血泪经验

5.1 画图和变量类的三个坑

现象一:运行到plot(t,y)报"未定义函数或变量 y"。

原因是报告里有些代码是分两段写的,中间夹着一段用了未定义函数,比如 2.3.1(3) 里先写了y = t.*(uCT(t)-uCT(t-1)),但uCT.m不在路径上,这一行执行失败,y根本没进工作区;后面的plot(t,y)自然找不到它。另一种情况是同一个变量名在前面被算过,你以为它还在,其实中间某行又把它覆盖成了别的东西。

解决办法很简单:运行前先在命令窗口敲whos看一眼工作区里到底有什么,或者干脆在每段代码开头清空clear一次。我一般会在报告脚本最前面加clear; close all; clc;,虽然粗暴,但能避免 90% 的"变量残留"问题。

现象二:四联图里第二个子图只剩一条曲线。

复信号那节的虚部代码里,subplot(2,2,2)下面连着写了plot(t, real(y))和plot(t, imag(y))。MATLAB 的plot默认会清除当前坐标区,第二次调用把第一次画的线擦掉了,所以最后只剩虚部。

改法有两种:一是中间加hold on,二是把两条线写进同一个plot调用:

subplot(2,2,2); plot(t, real(y), t, imag(y)); % 一次画两条,自动分色 legend('实部', '虚部'); grid on;

第二种写法在报告里更常见,因为省了一次hold,图例也能一次配齐。

现象三:坐标范围怎么设都不对,波形被裁掉。

典型例子是模那个子图,先写了axis([-5,10,-2.2,2.2]),紧接着又写axis([-5,10,1,3]),后一行把前一行覆盖了,最终范围是[1,3],如果模的最小值小于 1,曲线下半截就被裁掉了。

排查办法是数一下每个子图里axis出现的次数。正常应该只有一行,写在plot之后、title之前。多行的情况要么是复制粘贴留下的,要么是改参数时忘了删旧的。我现在的习惯是把axis和title写在一起,改范围的时候两行一起动,不容易漏。

5.2 函数和数值类的两个坑

现象四:sinc画出来的过零点位置和教材差一个 pi。

MATLAB 的sinc(x)定义是 sin(πx)/(πx),而教材里常写的抽样函数是 Sa(t) = sin(t)/t。两者相差一个缩放因子,过零点一个在整数处,一个在 π 的整数倍处。直接套用会导致波形"看起来对、刻度全错"。

改法是自己写一个:

function y = sa(t) % 教材定义的抽样函数 sin(t)/t,t=0 处取极限值 1 y = sin(t) ./ (t + (t == 0) * eps); % 分母补一个极小量,避开 0/0 y(t == 0) = 1; end

t==0时补eps是为了避免除零警告,随后再把该点覆盖成 1。这个写法比if判断更向量化,输入数组也能直接用。

现象五:conv之后画图报"向量长度不一致"。

卷积输出长度是N+M-1,很多人直接拿原来的t去画,长度对不上就报错;也有人用linspace重建时间轴,但起止点算错了,图能画出来、波形位置全是歪的。

正确的重建方式是起点相加、步长不变、长度跟卷积结果走,也就是 4.4 节里那段ty。判断有没有算错,看两个特征:波形起点应该等于两个输入起点之和,波形终点应该等于两个输入终点之和。这两个数对不上,说明时间轴重建错了。

注意:卷积前先确认两个信号的采样间隔是否一致。如果x用 0.01、h用 0.02,直接conv得到的结果没有物理意义,必须先统一重采样到同一个dt。

6. 二次开发:把十四节实验改成一个可复用模板库

拆完这份报告,我做的第一件事是把每个实验小节拆成独立的函数,而不是继续用一堆命令行脚本。原因是脚本里的变量全都躺在工作区,跑完一节再跑下一节,t、y、f互相覆盖,想单独复现某张图得从头再跑一遍。

改造思路分三层:最底层是工具函数,uCT、sa、门函数这些放一个文件夹统一addpath;中间层是每个实验对应一个demo_*函数,只负责算和画,不负责保存;最上层是一个批量渲染脚本,跑一遍把所有图导出成 PNG,写报告时直接贴。

% render_all.m:批量跑实验图并导出,写报告时直接取用 figDir = fullfile(pwd, 'figs'); if ~exist(figDir, 'dir'), mkdir(figDir); end cases = {@()demo_cos, @()demo_step, @()demo_complex, @()demo_conv}; names = {'fig_2_3_1_1', 'fig_2_3_1_2', 'fig_2_3_2_1', 'fig_3_3_conv'}; for k = 1:numel(cases) f = figure('Visible', 'off'); % 不弹窗,批量跑的时候窗口不会乱闪 cases{k}(); % 调用对应的绘图函数 print(f, fullfile(figDir, [names{k} '.png']), '-dpng', '-r150'); close(f); end

这段代码的关键在'Visible', 'off'和-r150。前者让图在后台渲染,跑二十个实验也不会满屏弹窗;后者是导出分辨率,150 dpi 足够报告印刷,再高文件会大到不好传。函数句柄用@()demo_cos这种匿名形式包一层,是为了让所有 case 的调用方式统一,加新实验时只改数组、不改循环。

再进一步,可以给每个demo_*加断言。比如卷积那个函数,算完之后用解析解的峰值做一次assert,误差超过阈值就直接报错,而不是等你把图贴进报告之后才发现形状不对。这套东西我一般叫它"图形回归测试"——图对不对肉眼看不出来,但数值对不对一测就知道。

改造过程中最容易忽略的是路径问题。工具函数文件夹加进addpath之后,换一台机器又要重新加一遍,所以我习惯在项目根目录放一个setup.m,里面写死相对路径,别人拿到文件夹先跑一次setup就能用。这一步多花五分钟,能省掉后面无数次"在我电脑上明明能跑"的扯皮。

从那以后我每次接手这类实验报告类的资源,都强制先跑一遍"清空工作区、只留函数文件、从头执行"的流程,确认它不是靠工作区里的残留变量才跑通的。这份报告里那几处y未定义、axis被覆盖的问题,全都是靠这个流程揪出来的。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进