C54x DSP上FIR低通滤波器的设计与CCS实现验证
2026/9/13 19:36:17 网站建设 项目流程

简介:面向数字信号处理与嵌入式开发学习者的FIR滤波实验资源,基于CCS开发环境编写,完整演示了有限冲激响应滤波器的设计、实现与调试流程,适合正在学习数字信号处理或CCS开发的学生作为参考。压缩包共16个文件,涵盖C源程序、头文件、链接命令文件、预编译库、输入输出数据文件以及工程配置等,整体体积仅26KB,结构清晰便于查阅。已有268人学习下载。实验围绕低通滤波场景展开,完整包含滤波器指标规划、系数生成、量化转换、卷积运算、边界处理以及时域频域性能评估等关键环节,并附带波形数据以便对照验证。通过学习该资源,既能理解有限冲激响应滤波器的原理与逐点卷积实现,也能熟悉CCS工程管理、编译链接和调试分析方法,为后续独立开展数字信号处理开发打下基础。

1. 为什么这份 C54x 的 lpassfir 工程值得从 FIR 底层看起

在 TMS320C5416 这类 C54x DSP 上做数字滤波,很多人第一次是直接从 CCS 里把 lpassfir.pjt 拖进工作区,编译、load、运行,然后盯着图形窗口里的两条曲线发呆。这份 fir.rar 实验包表面上只是 exp5 的一次作业,但它把 FIR 滤波器从设计到上板验证的每个环节都压缩在了不到十个文件里:test.c 负责主逻辑,c54fir.H 放系数,C54FIR.LIB 提供运算例程,wave1k+500.dat 是叠加了 500Hz 和 1kHz 的混合信号,fwave1k+500.dat 是经过低通后的输出。对于想搞懂数字信号处理但又不满足于在桌面软件里画框图的人来说,这个工程是很好的解剖对象。本文按设计、编译、验证、量化的顺序拆一遍。

2. FIR 低通滤波器的参数推导与 C54FIR.LIB 调用

2.1 为什么是 FIR 而不是 IIR 或滑动平均

FIR 滤波器的输出只是输入信号的加权平均,没有反馈项,因此必然稳定。这个实验的数据在 wave1k+500.dat 中,按常见课程设计的假设,采样率是 8000Hz,输入由 500Hz 和 1kHz 两个正弦叠加,目标是保留 500Hz、滤除 1kHz。此时选择一个通带截止 750Hz、阻带截止 1kHz 的低通滤波器即可。相比一阶低通滤波和滑动窗口滤波,FIR 能用有限阶数做出线性相位,不破坏信号到达时间的相对关系;而滑动窗口滤波本质上是系数均为 1/N 的 FIR,阻带衰减固定只有约 13dB,无法同时满足过渡带和阻带的要求。

窗函数法的选型理由很直接:Hamming 窗过渡带约为 3.3/N(归一化频率),在 8kHz 采样下,750Hz 到 1kHz 的过渡带为 250Hz,归一化后是 0.03125,所以 N 大约需要 106。不过实验包里 c54fir.H 中的系数是提前生成的,课程演示常常取 64 或 65 阶,会让阻带衰减略有下降。我一般会先跑一遍脚本观察频谱,再决定阶数,而不是直接套默认值。

2.2 用 Python 生成定点系数

在 C54x 这种 16 位定点 DSP 上,浮点系数必须转为 Q15 格式。用 Python 的 scipy.signal.firwin 可以快速得到一组 Hamming 窗低通系数并导出为 C 数组。在设计系数之前,先确认采样率和目标频率,下面这段代码能直接带出 Q15 数组。

import numpy as np from scipy.signal import firwin fs = 8000.0 # 采样率,实验包通常按 8kHz 处理 cutoff = 750.0 # 低通截止频率 numtaps = 65 # 阶数取 65,奇数,便于线性相位 h = firwin(numtaps, cutoff, fs=fs, window='hamming') h_q15 = np.round(h * 32768).astype(np.int16) print(f"// {numtaps} taps, fs={fs}, cutoff={cutoff}") for i in range(0, numtaps, 8): row = ", ".join(f"{v:6d}" for v in h_q15[i:i+8]) print(f" {row},")

这段代码先用 firwin 计算浮点系数,h 是长度为 65 的数组;乘上 32768 并取整,就得到了 Q15 定点表示。打印出来的数组可以直接粘贴到 c54fir.H 中替换原来的系数。参数上,numtaps 越大过渡带越窄,但在 C54x 上计算开销线性增加;cutoff 取 750Hz 是为了让 500Hz 在通带内、1kHz 在阻带内。如果实验包里原始波形是其它频率,只需改这两项,后面编译和调试流程完全不受影响。导出后检查系数和,低通滤波器系数和应接近 256(Q15 下约 0.9922),偏离明显说明量化误差较大。

2.3 C54FIR.LIB 的调用约定与非零初值的坑

C54FIR.LIB 是 TI 为 C54x 提供的 DSP 例程库,里面包含优化过的 FIR 函数。test.c 中通常这样调用:

#include "c54fir.H" short x[NSAMP]; short y[NSAMP]; short h[Nh]; fir(x, y, h, NSAMP, Nh); /* 实际函数名以 DSPLIB 头文件为准 */

这个调用把 x 的 NSAMP 个样点与 h 的 Nh 个系数做卷积,输出写入 y。C54x 的库函数内部使用循环寻址,要求 h 所在缓冲区起始地址按 Nh 对齐,否则会进入慢速路径甚至产生错误结果。我一般会在链接命令文件 test.cmd 中对 .coeff 段做对齐:

.coeff : {} > RAM PAGE 1, align(64)

另外要注意,DSPLIB 的 fir 函数不会自动清空内部延迟线。如果程序连续多次调用 fir,第二次调用时上次残留的延迟线数据会叠加到新输出上。常见做法是在调用前用 memset 把状态数组清零。test.c 如果只是单次处理 wave1k+500.dat,这个坑不容易被发现,但一旦放到实时采集循环里,输出前几十个点会带有明显偏置。定点 C54x 做 FIR 不用浮点,不是因为浮点不能跑,而是库函数和编译器的定点路径快很多,这也解释了为什么实验包的系数表全部是整型。

3. 在 CCS 中复现实验:工程文件、编译配置与边界处理

3.1 解压 fir.rar 之后,这些文件各自干什么

拿到 fir.rar 后先别急着全选复制到工程目录,先理解每个文件在编译链中的位置。下面这张表是这套实验最常见的文件角色。

文件类型在本工程中的作用
lpassfir.pjt工程文件记录编译选项和源文件列表
test.c源文件主程序,读取输入、调用 fir、输出结果
test.cmd链接命令文件定义 C54x 内存页与段分配
c54fir.H头文件FIR 阶数定义和系数表
C54FIR.LIB库文件TI DSPLIB,提供 fir 等函数
RTS.LIB库文件C 运行支持库
TMS320.H头文件C54x 寄存器和外设定义
test.dat数据文件可能是输入激励或初始化数据
wave1k+500.dat数据文件输入混合信号,常见为 CCS 格式
fwave1k+500.dat输出文件滤波后的结果
Debug.lkv / Debug.lkf中间文件编译生成的链接映射,不必手工编辑
cc_build_Debug.log日志编译日志,排错时查看

lpassfir.paf 是工程参数文件,记录编译器版本、优化级别等。wave1k+500.dat 和 test.dat 的格式需要留意:C54x 的 .dat 文件常见第一行是1651 1 8000 0 0这样的头部,后面每行是“地址 数据”。如果直接用文本编辑器的二进制视图或者普通 load 命令读取,只会读到一串地址,无法得到正确的样点序列。

3.2 导入、编译和加载的步骤

CCS 版本不同,导入方式略有变化。CCS 3.x 直接 File->Open Project,指定 lpassfir.pjt;CCS 4 及以上用 Project->Import Legacy CCS/Eclipse Project,选中 .pjt 后 Toolchain 选择 TI C5000/C5400。导入后会看到源文件和库,右键工程选择 Build Project,console 里应生成 lpassfir.out。如果编译失败,cc_build_Debug.log 是最直接的线索,通常错误集中在 c54fir.H 的系数表超范围、test.cmd 的内存段重叠,以及 C54FIR.LIB 和 RTS.LIB 的路径。

加载 .out 前需要配置目标。纯软件验证用 CCS 内置的 C54x Simulator 即可,不用连开发板。在 Target Configuration 中新建一个 C5416 Simulator 配置,然后 Debug->Connect,最后 Debug->Load Program。加载后打开 Memory 窗口,观察 .bss 段中的 x 缓冲区,用 File->Load Data 加载 wave1k+500.dat 到数组起始地址。这一步容易出错:.dat 文件中第一个地址必须与 CCS 里加载到的地址一致,否则数据会被写到随机位置。

3.3 test.cmd 与生成的 Debug.lkf

test.cmd 是用户写的链接脚本,决定代码和数据落在哪个内存区。C54x 有两套独立地址空间:PAGE 0 是程序空间,PAGE 1 是数据空间。一个典型的 test.cmd 片段如下:

MEMORY { PAGE 0: VEC: org = 0x0080, len = 0x0080 PROG: org = 0x0100, len = 0x3F00 PAGE 1: DATA: org = 0x1000, len = 0x1000 } SECTIONS { .text : > PROG PAGE 0 .data : > DATA PAGE 1 .bss : > DATA PAGE 1 .stack : > DATA PAGE 1 .coeff : > DATA PAGE 1, align(64) }

这里把程序段 .text 放到 0x0100 开始的 0x3F00 字,数据段放到 0x1000 开始的数据区。Debug.lkf 是 CCS 从工程设置自动生成的链接信息文件,等价于一个由图形界面生成的 cmd;当你在工程属性里改了堆栈大小,CCS 会同步改写 .lkf。调试时如果程序跑到异常地址,第一步对比 .map 文件里的实际段地址与 test.cmd 期望值,通常问题出在 .stack 和数据缓冲区重叠。

3.4 边界处理:前 64 个输出点为什么不可信

FIR 的卷积要求完整的历史输入。在第 n 个输出点,需要 x[n]、x[n-1]...x[n-Nh+1],起始阶段 n < Nh-1 时历史样点不存在。test.c 里如果直接写双层循环,最直接的做法是补零:

#define NX 256 #define NH 65 short x[NX], y[NX]; short h[NH]; void fir_naive(int nx, int nh) { int i, j; for (i = 0; i < nx; i++) { long acc = 0; for (j = 0; j < nh; j++) { if (i - j >= 0) acc += (long)x[i - j] * h[j]; } y[i] = (short)(acc >> 15); } }

当 i-j<0 时跳过累加,相当于给输入补零。这样前 64 点幅度偏小,后面逐步正常。观察频域波形时如果看到起始段有诡异变化,不是滤波器坏了,而是补零效应。C54FIR.LIB 的 fir 函数用循环缓冲维护 Nh-1 个历史样点,首次调用前需要把延迟线清零,否则第 0 个输出会带入上次残留。实验包里只有一次滤波过程,所以效果不明显;把同一段输入连续滤波两遍,差异立刻可见。

4. 从 wave1k+500.dat 到 fwave1k+500.dat:波形与频域验证

4.1 解析 CCS 的 .dat 文件

C54x 的 .dat 文件常见格式如下:

1651 1 8000 0 0 0x1200 0x1234 0x1201 0x5678

第一行以 1651 开头,表示十六进制整数数据;第二列 1 表示数据宽度为一个字;第三列 8000 是起始地址。读取时不能直接当作纯样点数组,需要跳过第一行并提取第二列。用 Python 解析:

import numpy as np def read_ccs_dat(path): with open(path, 'r', encoding='ascii') as f: lines = f.readlines() samples = [] for line in lines[1:]: line = line.strip() if not line: continue parts = line.split() if len(parts) >= 2: try: val = int(parts[1], 0) samples.append(val) except ValueError: continue return np.array(samples, dtype=np.int16)

这里 int(parts[1], 0) 能同时识别十六进制和十进制。如果文件中只有一列纯数据,则直接读取。拿到样点后,把 Q15 值除以 32768 得到浮点归一化信号,然后做 FFT。对于这个低通实验,1kHz 分量应下降 40dB 以上,而 500Hz 分量保持原样。

4.2 验证频域指标的数值标准

下表列出一组调 FIR 实验时常用的最低标准,低于这个需要回头检查系数或输入数据格式。

指标期望值检查方法
通带增益0.95 ~ 1.0(-0.5 ~ 0 dB)500Hz 峰幅度与输入比较
阻带衰减> 35 dB1kHz 峰幅度差
过渡带中心750Hz 附近频谱峰值位置
群延迟(Nh-1)/2 个采样点时域波形峰值位置差
直流偏移小于 -60 dB0Hz 处频谱幅度

群延迟对 FIR 非常关键。65 阶线性相位 FIR 的群延迟是 32 个采样点,在 8kHz 采样下正好是 4ms。对比输入和输出波形时,如果只看峰值对齐,会发现两者差 32 个点,这不是滤波器引入的相位失真,而是线性相位 FIR 的正常延迟。验证方法是对齐后取前 512 点做互相关,峰值位置应在 32 附近。

4.3 在 CCS 图形窗口里直接看信号

如果不想导出到 Python,CCS 自带图形工具也能定位问题。View->Graph->Time/Frequency,选择 Single Time,起始地址填 x 数组的地址,Acquisition Buffer Size 填 256,Display Data Size 填 256,DSP Data Type 选 16-bit signed integer,Q-Value 设 15。此时看到的输入波形是两条正弦叠加;再启一个 Graph 窗口指向 y 数组,运行后就能看到 500Hz 正弦。如果波形出现截顶,说明 Q15 系数或输入数据超出 ±1 范围,需要缩放。

频域验证用 FFT 图形,同样指向 x 和 y 地址,FFT Frame Size 取 256。对比两个频谱窗口的峰值,能更直观地看到 1kHz 分量被抑制。若发现频谱底噪很高,先检查 .dat 文件是否被解析成了有符号数,C54x 的 0x8000 到 0xFFFF 代表负数,如果按无符号读,频谱会产生镜像。

5. 定点实现里容易忽略的直流偏置与系数和偏差

5.1 为什么系数和不是 1

Q15 量化后,低通滤波器的直流增益是 h_q15 之和除以 32768。由于每个系数都被四舍五入到整数,累加和可能偏大或偏小。比如 65 阶 Hamming 窗设计出的浮点系数和为 0.9998,量化后乘 32768 取整,和可能变成 32766 或 32770。对应直流增益分别是 0.99994 和 1.00006,单看不严重,但级联多级滤波或进行 DC 去除时,误差会被放大。更麻烦的是量化后不再严格对称,线性相位特性会被轻微破坏。解决方法是设计时保证系数数组左右对称,然后用一个短循环强制系数和为 2^15:

long sum = 0; int i; for (i = 0; i < Nh; i++) sum += h[i]; h[Nh/2] += (short)(32768 - sum);

这段代码把误差集中到中心系数上。中心系数对应的是输入信号的当前样点,对它做微小修正对频响影响最小,因为它位于冲激响应的对称中心。修完后再跑一次频谱检查,阻带通常不会受影响,直流增益则精确等于 1.0。

5.2 用模拟器对照定点与浮点结果

CCS 的 C54x Simulator 支持 C/C++ 调试,但默认会忽略整数溢出标志。在 test.c 中把累加器 long acc 强制赋给 short y[i] 时,如果 acc 超过 16 位范围,低 16 位会被截断而不报错。调试时用 CCS 的 Watch 窗口监视 acc 的最大值,或者用断点记录每个样点的最大值。若 acc 最大值接近 32767*Nh,说明系数和输入动态范围重叠,需要对输入数据右移一位。正确的做法是先用 Python 计算理论峰值,再反推软件里的移位量。

5.3 关键操作:滤波前检查单位冲激响应

无论是修改窗函数还是调整阶数,先不要急着看输出波形,而是把系数数组当作输入滤波一个单位脉冲,观察输出是否对称、峰值是否在第 32 个样点。这一步能快速暴露系数粘贴错误、数组越界和延迟线未清零三类问题。单位冲激响应正常后,再用 wave1k+500.dat 做全流程验证。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询