ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

MATLAB数字信号处理随书代码实战:从脚本复现到滤波器设计

MATLAB数字信号处理随书代码实战:从脚本复现到滤波器设计

简介:这是维纳·K·英格尔与约翰·G·普罗克斯合著、刘树堂翻译的《数字信号处理(MATLAB版)》一书的随书代码,适用于原书第2至第4版,服务数字信号处理课程学习及MATLAB仿真实践。原代码下载站点关闭、配套工具包pwsk_dsp改名,导致互联网上很难获取,这套整理后的代码包便显得尤为难得。压缩包共含192个文件,以191个.m脚本为主,另附1个txt说明文件,整体仅105KB;文件按章节和示例编号命名,便于与教材内容逐一对应对照运行。代码内容覆盖教材中的典型算法实例,可直接在MATLAB中执行,有助于理解滤波器设计、频域分析、随机信号处理等核心知识点,也便于教师备课与学生自学。目前已有437人学习下载,适合正在学习或讲授数字信号处理、需要随书代码辅助实践的读者使用。

1. 数字信号处理(MATLAB版)随书代码:先跑通一个脚本,再回头啃公式

数字信号处理的教材公式推起来是一回事,写成能在MATLAB里出图的代码又是另一回事。我最初学DSP时最卡的位置不是傅里叶变换的推导,而是"书上说X(k)=sum(x(n)e^{-j2πkn/N}),我怎么把它变成一段能跑的MATLAB脚本"。这份随书代码的价值在于,它把离散时间信号、Z变换、DFT/FFT、滤波器设计、随机信号处理这些章节的算法全部拆成了可运行的m文件,拿来就能改参数、跑实验、看波形。适合正在上DSP课程的学生、做课程设计需要仿真支撑的同学,以及工作中突然要补数字滤波器基础的工程人员。目标只有一条:让你先用最小成本把代码跑起来,再回头看教材里的公式时,知道每一行在算什么。

2. 随书代码包的结构:按章节拆解的m文件与它们对应的DSP知识点

这套代码包不是单个孤立的脚本,而是按教材章节组织的一组工程文件。常见的组织方式是每个章节对应一个子文件夹,或者文件名带章节编号。拿到手别急着全部打开,先扫一眼文件名列表,你会发现规律非常清楚——信号与系统、Z变换、DFT、FFT、滤波器设计、随机信号处理,章节顺序和教材目录几乎一一对应。我一般会先打开信号生成那个脚本确认运行入口,再按章节顺序逐个跑,这样后面遇到滤波器设计里的参数时,脑海里已经有前面信号波形的印象。

2.1 章节与文件的对应关系:先找准入口文件再动手

下面是我拆这类随书代码包时整理出的常见映射关系,注意不同版本命名可能不同,但功能模块基本一致:

章节主题典型m文件涉及的核心函数这个脚本在验证什么
离散时间信号与系统chap02_sig_gen.m、conv_demo.msin、conv、stem采样率、时间向量、卷积与系统响应
Z变换chap03_ztrans.m、pzmap_demo.mztrans、iztrans、tf2zp极点零点与因果稳定性
离散傅里叶变换chap04_dft_demo.mdft实现、abs、angleDFT与IDFT的编程实现
FFT与频谱分析chap05_fft_spec.mfft、fftshift、freqz频率分辨率、单边谱、补零
FIR/IIR滤波器设计chap06_fir_lp.m、chap06_iir_bp.mfir1、butter、filter、filtfilt截至频率归一化、幅频响应、相位响应

第三节的Z变换脚本我建议后置,因为它的输出主要是零极点图,需要先理解s平面和z平面的映射关系,否则看图像看天书。常规路径是先跑信号生成和卷积,把MATLAB的矩阵运算手感找回来,再进FFT和滤波器设计。这套顺序对我帮助最大的是:前面章节生成了基础信号,后面滤波器设计时就拿它做输入,不需要额外造数据。

2.2 信号生成与卷积脚本:采样率、时间向量和频率怎么搭配才不会混叠

随书代码里最常出现的片段就是正弦信号生成。我剥开一个典型脚本给大家看:

% chap02_sig_gen.m Fs = 1000; % 采样率 1000 Hz T = 1 / Fs; % 采样周期 1 ms L = 1000; % 采样点数 t = (0:L-1) * T; % 时间向量,总长 1 秒 f = 50; % 信号频率 50 Hz x = sin(2 * pi * f * t); % 生成正弦序列 figure; plot(t, x(1:200)); % 只画前 200 个点,波形更清晰 xlabel('时间/s'); ylabel('幅度'); title('50Hz 正弦信号(前 200 点)');

逻辑说明:t = (0:L-1)*T是构造离散时间轴的标准写法,相邻样本间隔为T=1/Fs,样本总数是L,所以最后一个样本对应的物理时间是(L-1)*T,不是L*T。2*pi*f*t算出的是每个样本点的相位数,sin逐点计算得到序列。

参数说明:这里最关键的量是f与Fs的关系。能满足f < Fs/2时才不会混叠,50Hz 对 1000Hz 采样率来说远低于奈奎斯特频率 500Hz,所以画出来的波形肉眼可见是光滑正弦。如果你把f改成 600Hz,波形会变成 400Hz 的假信号,这就是混叠,理论上无法还原。做实验时我习惯把Fs固定,先算好f再生成,避免采样率不够导致后面分析全错。

卷积部分的脚本一般长这样:

% conv_demo.m n = 0:40; h = ones(1, 5) / 5; % 五点滑动平均,等效一个低通 imp = [1, zeros(1, 40)]; % 单位脉冲序列 y = conv(imp, h); % 卷积验证系统输出 figure; stem(0:44, y); title('五点滑动平均对单位脉冲的响应');

逻辑说明:h是长度为 5 的滑动平均滤波器,每个系数1/5,目的是让直流增益为 1。对单位脉冲做卷积后得到的就是系统的脉冲响应,stem画离散序列比plot更合适。conv默认输出长度为length(imp)+length(h)-1=45,所以横轴要写成0:44。

参数说明:把5换成21,滑动窗口变长,脉冲响应展宽,频域上低通截至频率会降低。这个脚本的价值是让你直观看到"时域卷积 = 平滑"这件事,后面学滤波器设计时随时回来对照。

2.3 滤波器设计脚本:FIR 与 IIR 的两种调用套路

滤波器设计章节的脚本是所有随书代码里实用性最高的,工作后你大概率会回头翻它。先看 FIR 低通设计:

% chap06_fir_lp.m Fs = 1000; % 采样率 1000 Hz fc = 100; % 截至频率 100 Hz Wn = fc / (Fs/2); % 归一化截至频率,必须除以奈奎斯特频率 N = 32; % 滤波器阶数 b = fir1(N, Wn); % 返回 FIR 系数 a = 1; % FIR 滤波器分母为 1 % 查看幅频响应 freqz(b, a, 512, Fs);

逻辑说明:fir1设计的系数 b 是按归一化角频率给出的,Wn=fc/(Fs/2)把物理截至频率映射到 0~1 区间,其中 1 对应奈奎斯特频率Fs/2。N=32表示滤波器阶数,实际系数个数是N+1=33,阶数越高过渡带越窄,但计算量也越大。

参数说明:写freqz(b, a, 512, Fs)的时候,第四个参数Fs让横轴直接以 Hz 显示,否则横轴是归一化频率 π 的倍数,新手很容易看懵。把fc从 100 改成 300,截至点沿频率轴右移,但注意fc不能超过Fs/2,否则Wn > 1,fir1直接报错,这是后文避坑的重点之一。

IIR 设计的套路几乎在每套代码里都会出现:

% chap06_iir_butter.m [b, a] = butter(4, Wn); % 4 阶巴特沃斯低通 y = filter(b, a, x); % 普通滤波,有相位延迟 y2 = filtfilt(b, a, x); % 零相位滤波,无延迟但只能离线用

逻辑说明:butter返回分子系数 b 和分母系数 a,直接用filter是常规因果滤波,会有与阶数匹配的群延迟;filtfilt对信号正反各滤一次,相位为零,适合做离线分析,但不适合实时处理。随书代码里两个写法都会出现,我建议都跑一遍,对比看滤波后的波形与原始信号在时间轴上的位置差异。

3. 把代码跑起来:环境准备、路径设置与三个典型实验复现

代码包拿到手,首要任务不是逐行读,而是让它先跑起来。很多同学卡在第一步就放弃了,其实准备工作和运行技巧非常固定。我给这一步总结了三个字:先环境,再示例,后调参。环境不对后面全是玄学报错,示例跑通了你对代码包的整体信任感就建立起来了。

3.1 环境准备:工具箱检查、工作目录与路径设置

MATLAB 版本不是越新越好,对于这套随书代码,只要不是远古版本基本都能跑。重点检查两个工具箱,Signal Processing Toolbox 和 DSP System Toolbox,因为fir1、butter、freqz都在这些工具箱里。先跑一条命令确认:

% 检查工具箱是否完整 ver('signal') ver('dsp')

逻辑说明:ver('signal')会返回 Signal Processing Toolbox 的版本号,如果提示找不到该工具箱,后面所有滤波器相关脚本都会报Undefined function 'fir1'。这种情况多半是安装 MATLAB 时没有勾选组件,不在代码包里,需要重新运行安装程序补装。

工作目录设置有个硬性建议:全套代码解压到纯英文路径。比如D:\DSP_matlab_codes\而不是D:\下载\数字信号处理代码\。这是因为 MATLAB 对中文路径支持时好时坏,尤其是旧版的 figure 保存、export 功能,中文路径会引发无法保存图片的诡异问题。设置方式:

% 切换到代码根目录,并添加各章节子目录 cd('D:/DSP_matlab_codes'); addpath('chap02', 'chap04', 'chap05', 'chap06');

参数说明:addpath把子目录加入搜索路径,这样你在任意目录下都能直接调用chap02_sig_gen这些脚本。如果你运行的脚本内部又调用了run('chap02/xxx.m'),那路径带中文会直接报错,所以一开始就养成纯英文路径的习惯,能省掉后续大量排查时间。

3.2 复现实验A:混合正弦信号的 FFT 频谱分析

这套代码里最值得第一个复现的实验,就是用 FFT 分析混合正弦信号。它同时覆盖了 2.2 节的信号生成和 2.3 节之前的频谱知识点。我自己重写的版本如下:

% expt_A_mix_sine_fft.m Fs = 2000; % 采样率 2000 Hz t = (0:1023) / Fs; % 1024 个样本,总时长为 0.512 秒 x = 0.8*sin(2*pi*100*t) + 0.5*sin(2*pi*300*t) + 0.3*sin(2*pi*700*t); X = fft(x); % 1024 点 FFT P2 = abs(X / 1024); % 双边幅度谱,除以点数归一化 P1 = P2(1:513); % 取单边前半部分 P1(2:end-1) = 2 * P1(2:end-1); % 除直流外,单边幅值加倍 f = Fs * (0:512) / 1024; % 对应频率轴 plot(f, P1); xlim([0 1000]); xlabel('频率/Hz'); ylabel('幅度'); title('混合正弦信号的单边频谱');

逻辑说明:fft的输出是复数,取绝对值后再除以样本点数 1024 得到真实幅度。因为 FFT 结果关于奈奎斯特频率对称,单边谱只取前N/2+1=513点,除直流分量外幅度乘 2 才能还原真实幅值。频率轴f = Fs*(0:512)/1024每个点对应一个频点,相邻频点的间隔是Δf = Fs/N ≈ 1.95Hz。

参数说明:三个信号的幅度 0.8、0.5、0.3 会在频谱上对应三个峰值,频率 100、300、700Hz 的位置你可以在图上直接读到。如果把采样点数从 1024 改成 256,频率分辨率变成Fs/256=7.8Hz,相距较近的 100Hz 和 105Hz 两个分量就会糊在一起分不开,这就是频率分辨率的直观演示。补零不能提高真实分辨率,只能让谱线更平滑,这一点很多人搞混。

3.3 复现实验B:窗函数对频谱泄漏的抑制

做 FFT 的人迟早会撞上频谱泄漏这个坑,随书代码通常都会专门放一个窗函数对比脚本。真实信号不可能是无限长的,截断就相当于加了矩形窗,频谱上表现为主瓣变宽、旁瓣出现。试验方法是对同一段信号分别加矩形窗和汉宁窗,看旁瓣衰减效果:

% expt_B_window_compare.m N = 256; n = 0:N-1; fs = 1000; x = sin(2*pi*80*n/fs) + 0.1*sin(2*pi*82*n/fs); % 两个接近的频率分量 w_rect = rectwin(N)'; w_hamm = hamming(N)'; x_rect = x .* w_rect; x_hamm = x .* w_hamm; X_rect = abs(fft(x_rect, 2048)); % 补零到 2048,让频谱曲线更平滑 X_hamm = abs(fft(x_hamm, 2048)); f = linspace(0, fs, 2048); subplot(2,1,1); plot(f(1:1024), X_rect(1:1024)); title('矩形窗'); subplot(2,1,2); plot(f(1:1024), X_hamm(1:1024)); title('汉宁窗');

逻辑说明:矩形窗等效于直接把信号拦腰截断,两个相隔 2Hz 的频点 80Hz 和 82Hz 在矩形窗下很难分清,旁瓣泄漏会把小信号淹没;汉宁窗通过让信号两端平滑过渡到零,旁瓣衰减大幅提升,但主瓣变宽了,这就是分辨率与泄漏的权衡。

参数说明:rectwin(N)产生长度 256 的全 1 列向量,转置成行向量才能与x逐点相乘。fft(x_rect, 2048)第二个参数是补零后的点数,补零本身不增加信息,只是把频谱采样点加密。要观察不同窗的效果,把hamming换成hanning、blackman都行,对比旁瓣即可。

3.4 复现实验C:用 FIR 低通滤波器分离信号中的低频分量

滤波实验是承上启下的关键一步——前面生成信号、看频谱,到这里终于要动真格把不需要的频率分量滤掉。我常做的验证是生成一个包含低频和高频的信号,设计 FIR 低通滤波器,滤波后再看频谱:

% expt_C_fir_lowpass_filter.m Fs = 1000; t = (0:1999) / Fs; x = sin(2*pi*80*t) + 0.8*sin(2*pi*400*t); % 80Hz 有用 + 400Hz 噪声 fc = 150; % 截至频率设为 150 Hz Wn = fc / (Fs/2); % 归一化截至频率 b = fir1(32, Wn); % 32 阶 FIR x_filt = filter(b, 1, x); % 滤波 % 滤波前后频谱对比 X_before = abs(fft(x)); X_after = abs(fft(x_filt)); f = Fs * (0:999) / 2000; subplot(2,1,1); plot(f, X_before(1:1000)); title('滤波前'); subplot(2,1,2); plot(f, X_after(1:1000)); title('滤波后');

逻辑说明:滤波后 400Hz 的幅度会明显下降,而 80Hz 分量基本保持不变。filter(b, 1, x)中 b 是 FIR 系数,1 是分母系数,FIR 滤波没有递归反馈,所以分母固定为 1。

参数说明:阶数 32 对应的过渡带宽度大约是4/(N+1)个归一化频率,换算成实际频率为4*Fs/(2*(N+1)) ≈ 61Hz。如果你把阶数改成 128,过渡带明显变窄,但滤波延迟和计算量也变大。这里最容易被忽略的是Wn的归一化,我见过不少人直接把 150 写进fir1,结果滤波器设计出来的截至点根本不是想象中的位置。

4. 随书代码避坑指南:版本兼容、路径与参数设置的五个常见问题

代码跑不通的原因十有八九不在代码本身,而在环境、路径和参数理解上。下面这五条是我拆过的 DSP 代码包里出现频率最高的坑,每一条都按"现象 → 原因 → 解决"的方式写清楚,照着排能省下不少时间。

4.1 报错 Undefined function or variable 'fir1'

现象:运行滤波器设计脚本时,MATLAB 直接提示Undefined function or variable 'fir1',但代码文件明明在路径里。

原因:这个报错九成是 Signal Processing Toolbox 没有安装或没有激活。fir1、butter、freqz都依赖该工具箱,基础 MATLAB 只带核心数学函数。

解决:先跑ver('signal')确认工具箱是否可用。如果返回空或提示不存在,需要用 MATLAB 安装包里的 Add-On Explorer 补装。装完重启 MATLAB,重新执行脚本即可。还有一种隐蔽情况:你的工作目录里恰好有某个自定义文件叫fir1.m,它会遮蔽工具箱同名函数,用which fir1就能看出 MATLAB 实际调用了哪个路径下的文件。

4.2 freqz 图的横轴是 0 到 1 而不是 0 到 500

现象:按照 2.3 节的代码调用freqz(b, a, 512),出来的幅频响应横轴是从 0 到 1 的归一化频率,怎么看都不像物理频率。

原因:freqz默认横轴是归一化频率,单位是 π rad/sample,范围 0 到 1。这是最简单也最容易看走眼的地方——教材里公式用 ω 表示数字角频率,而实际工程里我们关心的是 Hz。

解决:调用时补上采样率参数,写成freqz(b, a, 512, Fs),横轴就会自动以 Hz 为单位显示,范围 0 到Fs/2。我习惯上所有滤波设计脚本里都写上Fs,这样回头读图不用再做心算。

4.3 滤波器截止点不对:把 100Hz 写成了归一化值

现象:设计截至频率 100Hz 的低通滤波器,用fir1(32, 100)或butter(4, 100)直接跑,结果幅频响应在归一化频率 0.1 附近就掉下来了,物理上完全对不上。

原因:fir1和butter的第二参数是归一化截至频率,范围必须落在 0~1 之间。100 没有除以奈奎斯特频率Fs/2=500,导致设计点位置差了 5 倍。这是随书代码里最容易踩的坑之一,因为刚看完教材的模拟频率概念,容易把模拟角频率直接套进数字滤波器设计函数。

解决:统一用Wn = fc / (Fs/2)先把物理频率转换成归一化值,再用fir1(N, Wn)。写脚本时把这个转换单独放一行并注释清楚,后面调参只需要改fc和Fs两个值。

4.4 FFT 频谱中直流分量超大,其他频率分量看不清

现象:对一段有明显偏置的信号做 FFT,0Hz 处的谱线高度远大于其他分量,导致图上其他峰几乎贴着横轴,什么都看不出来。

原因:信号里有直流偏置,即均值不为零。FFT 中直流分量是所有样本的平均值乘以 N,数值大得离谱,显示时把其他分量都压扁了。这通常不是代码 bug,而是信号预处理没有做。

解决:在做 FFT 前先执行x = x - mean(x);去掉直流分量,再跑fft。如果处理后的频谱依然有少量直流残留,大概率是前面窗函数没有完全对称,可以再检查一下x .* w里的窗函数是否与信号长度匹配。这个预处理习惯在随书代码的 2.2 节信号脚本里其实隐约出现过,只是很多同学没注意。

4.5 中文路径下无法保存图片或脚本自动退出

现象:代码跑完,执行saveas(gcf, 'result.png')弹出警告甚至直接退出,更诡异的是换个纯英文目录就一切正常。

原因:MATLAB 在部分系统环境下对含中文或特殊符号的路径处理不够严谨,尤其是在保存 figure 和调用外部绘图渲染器时。代码包里如果没用绝对路径,靠相对路径保存图片也会因为路径解析失败而中断。

解决:把整套代码解压到纯英文目录,比如D:\DSP_Project\codes\,并保证该目录下没有空格。代码里写绘图保存时,最好先用cd明确指定保存目录。这个事看似和环境无关,实际排错时最容易翻车——我见过有人换了三台电脑排查一周,最后发现就是路径里的中文问题。

5. 从复现到自用:把随书脚本改造成自己的 DSP 工具函数

复现到第三遍的时候,你一定会觉得每次改参数翻脚本太烦了。这一步就是把随书代码从"演示脚本"升级成"自己的工具箱"。我一般会挑两个改造点:信号频谱分析和滤波器设计。

5.1 把 FFT 频谱脚本封装成函数

function [f, P1] = spec_analyze(x, Fs) % 输入: x 为待分析信号,Fs 为采样率 % 输出: f 频率轴,P1 单边幅度谱 N = length(x); X = fft(x); P2 = abs(X / N); P1 = P2(1:floor(N/2)+1); P1(2:end-1) = 2 * P1(2:end-1); f = Fs * (0:floor(N/2)) / N; plot(f, P1); xlabel('频率/Hz'); ylabel('幅度'); end

封装后,任何信号进来一行[f, P1] = spec_analyze(x, Fs);就能出图。这个函数把 3.2 节的实验变成了固定工具,核心在于长度 N 用了floor(N/2),兼容奇数长度信号,算是改造时最值得注意的小细节。

5.2 批量扫描滤波器阶数,观察过渡带变化

滤波器设计参数调整靠肉眼一个个试效率太低,写个循环批量出图:

figure; hold on; for N = [16 32 64 128] b = fir1(N, 0.2); [H, f] = freqz(b, 1, 512, 1000); plot(f, 20*log10(abs(H))); end legend('N=16', 'N=32', 'N=64', 'N=128'); xlabel('频率/Hz'); ylabel('幅度/dB');

这一段跑下来,你能清晰看到阶数越高过渡带越窄,但旁瓣也更多。从那以后我每次拿到新的随书代码包,都强制自己走一遍"先跑最小例子、再封装成函数、批量扫参数"的三步流程,这习惯帮我避开了无数个看着玄学其实是参数没用对的坑。希望帮到你。

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

RELATED READING

延伸阅读

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