1. 从算法到硬件的根升余弦滤波器落地场景
根升余弦滤波器在数字通信里几乎绕不开,QPSK、16QAM 这类调制在 I/Q 两路成型时都要用它来压缩带宽、抑制码间串扰。Matlab 里画个幅频响应曲线很容易,但真正把它搬到 FPGA 上跑起来,中间会卡住不少人:系数怎么导出、FIR IP 怎么配、采样率对不上、仿真出不来波形。这篇就围绕 Vivado 与 Matlab 联合实现根升余弦滤波器这条主线,把系数生成、RTL 实现、Testbench 验证串成一个能跟做的闭环。
适合谁看:正在做通信基带、毕设涉及成型滤波、或者第一次用 Vivado FIR Compiler 的人。你不需要很深的 DSP 背景,但至少要能打开 Vivado、会跑一次综合。整篇的操作路径是:Matlab 生成 48 阶、滚降系数 0.5 的 RRC 系数并导出 coe 文件,Vivado 里用 FIR IP 核加载系数,配好采样率与时钟,再用 Testbench 灌入双极性非归零码观察滤波输出。
我试过把 Fs 和系统时钟设成不一致,结果输出幅度直接塌掉,所以下面每个参数都会说明它和谁对应。先把工具链版本说清楚:Matlab R2020b 及以上、Vivado 2019.2 及以上都能复现,FIR Compiler 用的是较新的 AXI 流接口版本,老版本 IP 界面字段名略有差异,但核心参数一致。
2. TaoToken 前置:把模型对话和接入文档备在手边
写这类联合工程时,最耗时间的往往不是写代码,而是查 IP 核某个字段到底填什么、报错信息怎么解。我习惯把 TaoToken 的模型对话和接入文档放在旁边,遇到 FIR Compiler 的采样率配置、coe 文件格式这类问题直接问,比翻手册快。
TaoToken 在这里的角色是辅助你查参数、读报错、生成脚本骨架,不是替代 Vivado 本身。你需要先拿到 API Key,再按文档接入。具体入口:
- 模型对话(问参数、问报错):https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=model_chat
- 接入文档(看接口格式):https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=doc
- API Keys 管理:https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=api_keys
拿到 Key 之后,你可以让它帮你把 Matlab 的 fdatool 参数翻译成 FIR Compiler 的字段,或者把一段报错贴进去问原因。下面第 3 节的系数脚本和第 4 节的 IP 配置,都可以先让模型生成初稿再自己核对。
3. Matlab 生成 48 阶根升余弦系数并导出 coe
3.1 用 rcosdesign 生成系数
老教程喜欢用 fdatool 点界面,但那个工具在新版 Matlab 里已经换成 filterDesigner,而且点出来的系数不好版本管理。直接用rcosdesign更干净,参数一目了然。48 阶、滚降系数 0.5、每符号 4 个采样点,脚本如下:
% rrc_coeff_gen.m % 生成根升余弦滤波器系数并导出 Vivado coe 文件 close all; clear; clc; N = 48; % 滤波器阶数(抽头数) beta = 0.5; % 滚降系数 sps = 4; % 每符号采样点数(samples per symbol) % rcosdesign 的 span 是符号数,抽头数 = span*sps + 1 span = N / sps; % 48/4 = 12 h = rcosdesign(beta, span, sps, 'sqrt'); fprintf('实际抽头数: %d\n', length(h)); fprintf('系数和: %.6f\n', sum(h)); % 归一化,避免 FPGA 里溢出 h = h / max(abs(h)); % 查看幅频响应 fvtool(h, 'Analysis', 'magnitude');跑完你会看到实际抽头数: 49,因为rcosdesign生成的是span*sps+1个点。如果你严格要求 48 阶,可以把最后一个点截掉,或者把 span 调成 11.75 再取整。我一般保留 49 个抽头,FIR IP 核支持非 2 的幂次抽头数,不影响。
3.2 量化成定点并写 coe 文件
FPGA 里跑的是定点数,所以要把浮点系数量化。这里选 16 位有符号、12 位小数,动态范围够用又不至于浪费资源:
% 定点量化:1 位符号 + 3 位整数 + 12 位小数 = 16 bit frac_bits = 12; coeff_fi = fi(h, 1, 16, frac_bits); % 转成整数 coeff_int = round(coeff_fi * 2^frac_bits); coeff_int = double(coeff_int); % 写 Vivado coe 文件 fid = fopen('rrc_48tap.coe', 'w'); fprintf(fid, 'radix=10;\n'); fprintf(fid, 'coefdata=\n'); for k = 1:length(coeff_int) if k < length(coeff_int) fprintf(fid, '%d,\n', coeff_int(k)); else fprintf(fid, '%d;\n', coeff_int(k)); end end fclose(fid); disp('coe 文件已生成: rrc_48tap.coe');生成的 coe 文件长这样,第一行声明进制,后面是逗号分隔、分号结尾的系数:
radix=10; coefdata= -3, 5, ... 127;注意:
radix=10表示十进制,如果你用radix=16就要写十六进制。FIR IP 核两种都认,但十进制更直观,排错时一眼能看出系数对不对。
4. Vivado FIR Compiler 配置与 RTL 骨架
4.1 新建工程与 IP 配置
新建一个 Vivado 工程,器件按你手头的板子选。然后 IP Catalog 里搜FIR Compiler,双击打开配置界面。关键字段对照如下:
| FIR Compiler 字段 | 填写值 | 说明 |
|---|---|---|
| Filter Type | Single Rate | 单速率,输入输出同采样率 |
| Coefficient Source | COE File | 加载上一步的 coe |
| Number of Coefficients | 49 | 与 coe 文件一致 |
| Coefficient Width | 16 | 与量化位宽一致 |
| Input Data Width | 16 | 输入信号位宽 |
| Input Sampling Frequency | 1 | 单位 MHz,对应 1MHz 符号率 |
| Clock Frequency | 100 | 系统时钟 100MHz |
| Output Rounding | Full Precision | 保留全精度,后续再截位 |
这里最容易错的是Input Sampling Frequency和Clock Frequency的关系。前者是数据有效速率,后者是 IP 核工作时钟。我把它设成 1MHz 和 100MHz,意味着每 100 个时钟周期才有一个有效输入样本,IP 核会自动做多速率处理。
注意:Matlab 里的 Fs 要等于这里的 Input Sampling Frequency。如果你 Matlab 里 Fs 设的是 4MHz(因为 sps=4),那这里也要填 4,否则滤波器的频响会整体偏移,输出波形完全不对。
4.2 例化 FIR IP 的 RTL 骨架
IP 配置完生成后,在顶层模块里例化。下面是一个最小可用的骨架,输入是双极性非归零码,输出是滤波后的 16 位有符号数:
module rrc_filter_top ( input wire clk, // 100MHz 系统时钟 input wire rst_n, // 低电平复位 input wire din_valid, // 输入有效 input wire signed [15:0] din, // 双极性非归零码 +1/-1 output wire dout_valid, output wire signed [15:0] dout ); // 例化 FIR Compiler IP fir_compiler_0 u_fir ( .aclk (clk), .s_axis_data_tvalid(din_valid), .s_axis_data_tready(), .s_axis_data_tdata (din), .m_axis_data_tvalid(dout_valid), .m_axis_data_tdata (dout) ); endmodule注意din声明成signed,因为双极性非归零码是 +1 和 -1,对应 16 位有符号数就是16'sd1和-16'sd1。如果你忘了 signed,综合工具会按无符号处理,-1 会变成 65535,滤波结果直接爆掉。
4.3 生成 1MHz 双极性非归零码
系统时钟 100MHz,要生成 1MHz 的符号率,用计数器分频:
// 100MHz -> 1MHz,计数 0~49 翻转 reg [5:0] div_cnt; reg symbol_clk; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin div_cnt <= 6'd0; symbol_clk <= 1'b0; end else if (div_cnt == 6'd49) begin div_cnt <= 6'd0; symbol_clk <= ~symbol_clk; end else begin div_cnt <= div_cnt + 1'b1; end end // 双极性非归零码:高电平 +1,低电平 -1 wire signed [15:0] nrz_data = symbol_clk ? 16'sd1 : -16'sd1; wire nrz_valid = (div_cnt == 6'd49);nrz_valid在每次符号切换时拉高一个时钟,作为 FIR 的输入有效信号。这样 IP 核就知道每 50 个时钟采一个样。
5. Testbench 验证与 Modelsim 联合仿真
5.1 写一个灌数据的 Testbench
Testbench 的目标是产生一段随机双极性码,喂给滤波器,观察输出是否收敛。核心片段:
`timescale 1ns / 1ps module tb_rrc_filter; reg clk; reg rst_n; reg signed [15:0] din; reg din_valid; wire dout_valid; wire signed [15:0] dout; // 100MHz 时钟 initial clk = 0; always #5 clk = ~clk; // 例化 DUT rrc_filter_top u_dut ( .clk (clk), .rst_n (rst_n), .din_valid (din_valid), .din (din), .dout_valid(dout_valid), .dout (dout) ); // 产生随机双极性码 integer i; initial begin rst_n = 0; din = 0; din_valid = 0; #100; rst_n = 1; #100; for (i = 0; i < 200; i = i + 1) begin @(posedge clk); din <= (i % 2 == 0) ? 16'sd1 : -16'sd1; din_valid <= 1'b1; @(posedge clk); din_valid <= 1'b0; repeat (49) @(posedge clk); // 等待 50 个周期 end #1000; $stop; end endmodule5.2 用 Modelsim 观察波形
Vivado 自带的 simulator 在跑有符号定点 FIR 时经常出不来正确波形,原因多半是 radix 显示设置和无符号/有符号解释的问题。用 Modelsim 联合仿真更稳。流程是:Vivado 里把仿真工具设成 Modelsim,生成仿真脚本,然后在 Modelsim 里跑。
跑起来后,把dout的 radix 改成 signed decimal,din也改成 signed decimal。你会看到din在 +1 和 -1 之间跳,dout是一条平滑的成型波形,峰值大约在 1.0 附近,过冲被滚降系数 0.5 控制住了。
注意:如果
dout一直是 0 或者全是 X,先检查din_valid有没有拉高,再检查 FIR IP 的s_axis_data_tready有没有接出来。很多人忘了接 ready,IP 核一直不接收数据。
6. 常见报错与排查清单
6.1 coe 文件加载失败
报错Invalid coefficient file或Number of coefficients mismatch。原因通常是 coe 文件里系数个数和 IP 里填的Number of Coefficients不一致。用length(coeff_int)打印一下实际个数,49 就填 49。另外 coe 文件最后一行必须是分号结尾,逗号结尾会解析失败。
6.2 输出幅度异常小或为零
先查Input Sampling Frequency和Clock Frequency的比值。如果两者相等,IP 核会认为每个时钟都有有效数据,而你的din_valid每 50 个周期才拉高一次,大部分时间在采零,输出自然小。正确做法是让 IP 核知道真实的有效速率,或者用s_axis_data_tvalid严格对齐。
6.3 仿真波形全是 X
多半是复位没接好,或者din在复位期间是未知态。检查rst_n的时序,确保复位释放后才开始灌数据。另外 FIR IP 核内部有流水线延迟,输出会比输入晚若干周期,别在第一个周期就找结果。
6.4 综合时报位宽不匹配
din声明成signed [15:0],但 IP 核的s_axis_data_tdata可能是[15:0]无符号接口。Vivado 会自动做位宽匹配,但符号位可能丢。稳妥做法是在例化时显式转换,或者把 IP 的输入数据类型设成 signed。
7. 继续深入的方向
跑通这条链路之后,你可以把单路扩展成 I/Q 两路,各例化一个 FIR 核,输入分别是 QPSK 映射后的 I 路和 Q 路。再往后可以加一个 CORDIC 做载波恢复,或者把系数换成可重载的 AXI 接口,实现动态滚降系数切换。
如果你在配 FIR IP 时拿不准某个字段,或者 Modelsim 报错看不懂,可以直接把报错贴到模型对话里问,比翻文档快很多。接入文档里有完整的接口说明,API Keys 页面拿到 Key 就能用。整条链路的关键就三件事:Matlab 系数和 IP 参数对齐、采样率比值设对、仿真工具选 Modelsim。这三步过了,根升余弦滤波器在 FPGA 上就跑起来了。