基于FPGA与Verilog的DDS信号发生器:从相位累加到扫频上板实践
简介:一套面向FPGA数字逻辑学习者的DDS信号发生器工程包,围绕直接数字频率合成的Verilog实现展开,可用于理解相位累加、频率控制字、分频调频、MIF查找表与DAC接口等关键环节。压缩包共315个文件,约5.7MB,包含Verilog与SystemVerilog源码、MIF波形数据、Quartus工程文件(qpf/qsf/qip),以及综合布局布线产生的cdb、hdb数据库和rpt报告,另附说明文档,工程链条相对完整。已有902人学习下载。读者可据此查看相位累加器、分频器、波形查表和DAC输出模块的组织方式,结合正弦查找表理解频率与相位调整思路,并借助现有工程进行仿真验证和板级调试,适合作为课程设计、实验项目或DDS入门实践的参考。
1. 手头缺一台能扫频的信号源时,我用一块 FPGA 板子跑通了 Verilog 版 DDS
去年调一个传感器激励电路,需要一路频率可调、步进细、波形能切的信号。桌面信号发生器倒是有,但改频率靠旋钮,自动扫频还要另接上位机脚本,来回折腾。后来把一块闲置的 FPGA 开发板翻出来,写了一版 Verilog 的 DDS 信号发生器,频率字一个寄存器直接控制,1 Hz 步进都稳得住,扫频逻辑塞进同一个工程里,配合一个 14 位并行 DAC,输出正弦、方波、三角波都能切换。这件事让我更确信:DDS 这种靠相位累加加查找表出波形的结构,是 FPGA 练手和实战之间最划算的一类小工程——原理一页纸能写完,写起来却能把时钟、存储、时序、DAC 接口这些坑一个一个踩出来。这篇文章把我从参数计算到上板调试的完整路径摊开,适合刚碰 Verilog 不久、想拿一个真实可跑的小系统练手的人,也适合手边需要一个灵活激励源、又不想被专用仪器绑住的从业者。
2. DDS 的两笔账:频率字怎么算、查找表怎么选
DDS 的核心并不神秘,一个相位累加器加一张波形查找表,就能把一个固定时钟变成任意频率的周期波形。但真正动手前,有两笔账必须先算清楚:频率分辨率由累加器位宽和系统时钟共同决定,输出频谱的干净程度则由查找表深度和幅度位宽把守。这两笔账算错了,后面代码写得再漂亮,波形也是歪的。
2.1 从相位累加器到输出频率:一条公式决定分辨率
DDS 的数学骨架是:每个时钟周期把相位值加上一个固定的步进量,累加器自然溢出,取模 2^N,输出的高位作为查表地址。设系统时钟为 f_clk,累加器位宽为 N,频率控制字为 K,则输出频率为:
f_out = K × f_clk / 2^N
反过来,要输出目标频率 f_out,频率字这样算:
K = round(f_out × 2^N / f_clk)
以 100 MHz 时钟、32 位累加器、目标 1 kHz 为例:
K = 1000 × 2^32 / 100,000,000 ≈ 42949.67,取 42950
实际输出频率为 42950 × 100e6 / 2^32 ≈ 1000.0005 Hz,误差在 0.5 mHz 量级。这个误差来源不是代码写错,而是频率字只能取整数,属于 DDS 固有的量化误差,工程上一般可以忽略。
频率分辨率等于 f_clk / 2^N,100 MHz 配 32 位时约 0.023 Hz。这就是为什么我倾向于把累加器位宽开到 32 位:分辨率够细,改频率时不会出现“想设 1 kHz 结果只能跳到 1.02 kHz”的尴尬。但位宽越大,后面查表截断时丢掉的相位位也越多,杂散指标会受影响,取舍放在下一节说。
有一个细节容易被忽略:累加器寄存器本身承担了 2^N 取模,不需要额外写取模运算。Verilog 里phase <= phase + freq_word这一句,溢出位直接丢弃,正好就是相位回绕。硬要写成if (phase >= 2**N) phase <= 0反而是错的,还多占了资源。
2.2 查找表位宽与相位截断:SFDR 为什么上不去
理想 DDS 的输出频谱只有一根主谱线,实际输出会有杂散,主要来源有三个:相位截断、幅度量化、DAC 非线性。其中前两个是设计阶段就能控制的。
相位截断杂散来自“累加器 32 位,但 ROM 只有 2^A 个地址”。如果 A 取 12,相当于只用了相位的高 12 位,低 20 位被直接扔掉。粗略估算,截断 D 位相位时,无杂散动态范围 SFDR 大约在 6.02D dB 附近,12 位地址对应约 72 dB 的理论上限,实测通常还要低一些。想再往上抬,要么加 ROM 深度,要么在截断前加抖动(dithering),把周期性杂散打散成噪声。
幅度量化杂散由 ROM 输出位宽决定。B 位幅度数据下,理论信噪比约 6.02B + 1.76 dB。12 位幅度大约 74 dB,14 位约 86 dB。很多入门开发板上的 DAC 是 10 位或 12 位并行接口,ROM 输出位宽做到 12 位到 14 位就够用,再往上加,瓶颈会转移到 DAC 芯片本身。
参数怎么组合,我一般按这张表先框定范围,再动手写代码:
| 系统时钟 | 累加器位宽 | 频率分辨率 | 查找表深度 | 地址位宽 | 输出频率上限(工程建议) |
|---|---|---|---|---|---|
| 50 MHz | 24 | 3 Hz | 1024 | 10 | 5 MHz |
| 100 MHz | 32 | 0.023 Hz | 4096 | 12 | 10 MHz |
| 125 MHz | 32 | 0.029 Hz | 4096 | 12 | 12 MHz |
输出频率上限为什么远低于奈奎斯特频率 f_clk/2?因为 DDS 输出是阶梯状采样序列,高频段每周期采样点太少,DAC 后面的重建滤波器很难把镜像压干净。经验值是输出频率不超过时钟的 1/10 到 1/8,波形失真才在可接受范围内。
2.3 选型理由:为什么不用直接计数分频
有人会问,输出方波直接拿计数器分频不就行了,何必上 DDS。分频法的问题是频率只能取 f_clk 除以整数,分辨率在低频段很差,而且换波形、扫频、调相位都不方便。DDS 的相位是连续累加的,改频率字立即生效且相位不跳变,扫频时波形连续,这是分频法给不了的。另一个常见方案是查表加插值,用低深度 ROM 配线性插值器换精度,代价是乘法器资源和时序压力,除非对 SFDR 有很高考量,一般小系统用不着。
3. Verilog 落地:三个模块把纸面公式变成波形
参数算完,接下来是把公式翻译成可综合的 Verilog。我习惯拆成三块:相位累加器、波形查找表、顶层与 DAC 接口。拆开的好处是每一块都能单独仿真、单独换实现,后面调参时改动面小。
3.1 相位累加器模块:频率字的加载与流水线处理
累加器是最简单也最容易出问题的一块。频率字如果直接从外部端口接进来参与累加,一旦输入在累加过程中变化,那一拍的相位增量就不确定,输出会出现相位毛刺。稳妥的做法是加一个更新脉冲,只在脉冲有效时锁存频率字:
module phase_acc #( parameter integer PHASE_W = 32, // 累加器位宽,决定频率分辨率 parameter integer FREQ_W = 32 // 频率字位宽,通常与累加器一致 )( input wire clk, input wire rst_n, input wire [FREQ_W-1:0] freq_word, // 频率控制字 K input wire freq_upd, // 单拍更新脉冲 output reg [PHASE_W-1:0] phase // 当前相位值 ); reg [FREQ_W-1:0] freq_word_r; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin freq_word_r <= {FREQ_W{1'b0}}; phase <= {PHASE_W{1'b0}}; end else begin // 只在新频率字到来时锁存,避免异步输入在累加中途变化 if (freq_upd) freq_word_r <= freq_word; // 溢出位自动丢弃,等价于对 2^PHASE_W 取模 phase <= phase + freq_word_r; end end endmodule逻辑说明:freq_upd起到了一个写使能的作用,把频率字和累加动作隔离开。如果工程里频率字由另一个时钟域产生,这个脉冲还要先做跨时钟域同步,不能直接把异步信号当使能。参数说明:PHASE_W决定分辨率和查找表地址来源,FREQ_W一般与PHASE_W保持一致,若频率字位宽更小,综合工具会自动补零,但建议显式对齐,避免位宽不匹配的警告淹没其他真正的问题。
3.2 查找表模块:ROM IP 核与纯逻辑写法的取舍
查找表有两种实现路径:调用厂商提供的 ROM IP 核,挂初始化文件;或者用case语句在逻辑里展开。前者的优点是深度大、时序好、改数据只换初始化文件;后者只适合小深度示意,深度一过几百行,综合时间和可读性都会崩。实际工程我一律走 IP 核,初始化文件用脚本生成。
下面是查找表的接口示意,数据源换成 IP 核后接口保持不变:
module sin_rom #( parameter integer ADDR_W = 12, // 地址位宽,深度 = 2^ADDR_W parameter integer DATA_W = 12 // 输出位宽,决定幅度量化精度 )( input wire clk, input wire [ADDR_W-1:0] addr, output reg [DATA_W-1:0] dout ); // 实际工程由厂商 ROM IP 核替换本段,并加载 .coe/.mif 初始化文件 always @(posedge clk) begin case (addr) 12'd0 : dout <= 12'd2048; // 中点,对应相位 0 12'd1024 : dout <= 12'd2048; // 半周期处 12'd2048 : dout <= 12'd2048; // 溢出边界 12'd3072 : dout <= 12'd2048; default : dout <= 12'd2048; endcase end endmodule需要强调的是,上面case只是接口示例,真实数据由生成脚本产出。以 12 位幅度、4096 点正弦表为例,数据生成用几行 MATLAB 就够了:
N = 4096; % 查找表深度 B = 12; % 幅度位宽 addr = 0:N-1; data = round((2^(B-1)-1) * sin(2*pi*addr/N) + 2^(B-1)); % 生成 .coe 文件,格式按综合工具厂商的要求调整 fid = fopen('sin_rom.coe', 'w'); fprintf(fid, 'memory_initialization_radix=10;\n'); fprintf(fid, 'memory_initialization_vector=\n'); fprintf(fid, '%d,\n', data(1:end-1)); fprintf(fid, '%d;\n', data(end)); fclose(fid);参数说明:2^(B-1)的偏移是为了把有符号正弦搬到无符号区间,方便直接驱动单极性 DAC。如果 DAC 是差分或双极性接法,这个偏移要不要保留要看后级电路,不能照搬。生成时务必保证首尾点连续,第 N 个点不写进表里,否则查表到边界会多跳一格,波形上出现一个固定位置的毛刺。
3.3 顶层与 DAC 接口:地址映射和时序收敛
顶层负责把相位高位接到 ROM 地址、把 ROM 输出打到 DAC 数据口,同时产生 DAC 时钟。相位到地址的映射写法是addr = phase[PHASE_W-1 -: ADDR_W],取高位、丢低位,这正是相位截断动作。如果开发板上 DAC 是源同步接口,还要注意数据与时钟的对齐关系,常见做法是把 DAC 时钟反相输出,让数据在时钟上升沿稳定。
module dds_top #( parameter integer PHASE_W = 32, parameter integer ADDR_W = 12, parameter integer DATA_W = 12 )( input wire clk, input wire rst_n, input wire [PHASE_W-1:0] freq_word, input wire freq_upd, output wire [DATA_W-1:0] dac_data, output wire dac_clk ); wire [PHASE_W-1:0] phase; phase_acc #( .PHASE_W (PHASE_W), .FREQ_W (PHASE_W) ) u_acc ( .clk (clk), .rst_n (rst_n), .freq_word (freq_word), .freq_upd (freq_upd), .phase (phase) ); sin_rom #( .ADDR_W (ADDR_W), .DATA_W (DATA_W) ) u_rom ( .clk (clk), .addr (phase[PHASE_W-1 -: ADDR_W]), // 取高位做地址,低位截断 .dout (dac_data) ); // DAC 时钟反相输出,给外部器件留出建立保持余量 assign dac_clk = ~clk; endmodule逻辑说明:相位高位直接进 ROM 地址,ROM 读延迟一拍,输出数据相对相位晚一个时钟周期,对连续波形没有影响,只是整体相位平移,做相位同步的系统里要记得把这个延迟算进去。参数说明:ADDR_W和DATA_W必须与 ROM IP 核配置一致,改了一处忘了另一处,综合会报位宽不匹配或者悄悄截断,是最隐蔽的一类错误。
4. 仿真不是走流程:用 Testbench 提前暴露频谱问题
代码写完直接上板,示波器上看到波形就以为成了,这是新手最常见的心态。DDS 的很多问题——频率偏差、波形不连续、幅度台阶——在仿真阶段就能抓出来,前提是 Testbench 写到位,并且把数据导出来做频域分析。
4.1 行为级仿真确认频率与幅度
Testbench 的核心是给一个确定的频率字,跑够时长,观察输出的周期数是否与理论一致。下面这版可以直接用:
`timescale 1ns / 1ps module tb_dds_top; reg clk; reg rst_n; reg [31:0] freq_word; reg freq_upd; wire [11:0] dac_data; wire dac_clk; // 100 MHz 系统时钟,周期 10 ns initial clk = 0; always #5 clk = ~clk; dds_top #( .PHASE_W (32), .ADDR_W (12), .DATA_W (12) ) u_dds ( .clk (clk), .rst_n (rst_n), .freq_word (freq_word), .freq_upd (freq_upd), .dac_data (dac_data), .dac_clk (dac_clk) ); initial begin rst_n = 1'b0; freq_word = 32'd42950; // 对应 1 kHz freq_upd = 1'b0; #100; rst_n = 1'b1; #20; freq_upd = 1'b1; #10; freq_upd = 1'b0; #2000000; // 仿真 2 ms,覆盖两个完整周期 $stop; end // 每个时钟把 DAC 数据写入文本,供后续频谱分析 integer fd; initial fd = $fopen("dds_out.txt", "w"); always @(posedge clk) begin if (rst_n) $fdisplay(fd, "%d", dac_data); end endmodule逻辑说明:#2000000对应 2 ms 仿真时长,1 kHz 信号刚好走两个周期,肉眼在波形窗口里能数出峰峰值。freq_upd只拉高一拍,模拟真实控制逻辑的写入动作。参数说明:仿真时长和时钟频率要匹配,如果改到 50 MHz 时钟,#5要改成#10,否则频率字算出来的结果和仿真显示对不上,这类错配在实际项目里并不少见。
4.2 把仿真数据导出来算 SFDR
时域波形只能看个大概,频谱干不干净要把数据导出来做 FFT。用 Octave 或 MATLAB 都行,下面这段脚本读文本文件、去直流、加窗、找基波和最大杂散:
% 读取仿真导出的 DAC 数据并计算 SFDR x = load('dds_out.txt'); x = x(:) - mean(x); % 去掉直流分量 N = length(x); w = hann(N); X = abs(fft(x .* w)); X = X(1:floor(N/2)); X_db = 20*log10(X / max(X) + eps); [~, fund_bin] = max(X_db); % 基波左右各 3 个 bin 视为同一根谱线,其余区域找最大杂散 mask = true(size(X_db)); mask(max(1, fund_bin-3):min(end, fund_bin+3)) = false; sfdr = X_db(fund_bin) - max(X_db(mask)); fprintf('SFDR = %.2f dB\n', sfdr);参数说明:加窗是为了压住频谱泄漏,矩形窗在非整周期采样时会把主谱线能量摊到邻近 bin,SFDR 读数会偏低,hann 窗是通用选择。掩码范围 3 个 bin 取决于频率字是否落在整数 bin 上,如果频率字按整周期设置,可以收窄到 1 个 bin。脚本跑出来的值不代表 DAC 实测指标,它只反映数字部分的截断和量化杂散,上板后还要叠加 DAC 非线性和时钟抖动。
4.3 时序仿真:查找表读延迟的影响
行为级仿真通过不代表上板一定通过。ROM IP 核通常有一到两拍读延迟,综合后的实际延迟取决于器件原语。如果系统里还有其他模块依赖 DAC 输出做反馈,这个延迟必须算进环路。我一般在综合后跑一次门级时序仿真,重点看两件事:ROM 输出在 DAC 时钟沿前是否稳定,以及freq_upd脉冲有没有因为布线延迟出现毛刺。前者决定波形是否失真,后者决定频率切换瞬间是否出现跳变。
时序仿真里如果发现 DAC 数据在时钟沿附近变化,解决办法不是改代码逻辑,而是在 ROM 输出和 DAC 端口之间插一级输出寄存器,把路径约束在一个时钟周期内。这一级寄存器的代价是再增加一拍延迟,但换来的是建立时间余量,上板稳定性提升明显。
5. 上板排查:四类翻车现场和对应的后悔药
仿真再漂亮,第一次上板能一次点亮的概率也不高。DDS 这类系统的问题往往不是代码语法错误,而是初始化、时钟、接口这些“外围”环节。下面几条是我自己踩过、也见过别人踩的坑,按现象、原因、解决三段写,方便对照。
5.1 输出恒定不动:ROM 初始化和 DAC 配置的连环问题
现象:上电后示波器测 DAC 输出,一直是中间电平,波形不动。原因多半是 ROM 没有正确加载初始化文件。综合工具找不到.coe文件时,默认把存储内容填零,查表地址怎么变,输出都是零,再经 DAC 偏移后就是固定电平。解决:先翻综合日志里关于 ROM 初始化的 warning,确认文件路径是相对工程路径还是绝对路径;再打开生成的比特流报告,看 ROM 是否被推断成分布式 RAM 或块 RAM,有没有初始化内容。另一个可能是 DAC 配置没完成,有些并行 DAC 需要先写控制寄存器进入正常模式,如果配置状态机卡住,数据口输入什么都不会体现在输出上。我一般会在配置完成信号上挂一个 LED,先确认 DAC 已经退出复位,再看波形。
5.2 频率偏差:频率字按错时钟算,或者位宽搞混
现象:目标 1 kHz,实测频率是 1.07 kHz 左右,偏差固定。原因通常是把频率字按 50 MHz 时钟算好了,工程实际跑在 100 MHz,或者反过来。另一种情况是累加器位宽在顶层参数和子模块参数里不一致,综合工具按其中一处生效,频率字算出来的结果和实际累加宽度对不上。解决:先拿示波器或逻辑分析仪测系统时钟的实际频率,不要信晶振标称值;再把频率字公式在纸上重算一遍,确认时钟频率、位宽、频率字三个数都对上。改参数时顶层和子模块的参数必须同步修改,用parameter传递而不是各自写死,能从源头避免这类问题。
5.3 波形毛刺与台阶:数据格式、时序余量、滤波器三处嫌疑
现象:低频段波形看起来正常,频率一升高,波形上出现规律性台阶或毛刺,幅度也开始塌。原因分三层:第一层是 ROM 输出到 DAC 的路径没有输出寄存器,建立时间不够;第二层是 DAC 建立时间本身不够,芯片选型时没核对数据手册里的 settling time;第三层是后级重建滤波器截止频率设得太低,把有用信号的高频分量也削掉了。解决:先加输出寄存器排除时序问题,再核对 DAC 建立时间与采样时钟的关系,最后用扫频方式测一遍滤波器幅频响应,截止频率一般取输出最高频率的 1.2 到 1.5 倍,留够过渡带又不过度衰减。
提示:幅度塌陷不一定是 DDS 的错,先断开滤波器直接测 DAC 输出,能把问题范围缩小一半。
5.4 换波形数据后输出异常:初始化文件格式和首尾连续性问题
现象:把正弦表换成三角波表,重新综合后输出波形周期性跳变,或者整体幅度只剩一半。原因多半是初始化文件的进制格式和工具要求不一致,比如工具要十进制而文件里写了十六进制,读进去的数据全错;另一种是波形数据首尾不连续,三角波生成时第 N 个点写成了和第 1 个点相同的值,查表回绕时多走一格,输出上就出现固定位置的尖刺。解决:生成脚本里固定进制声明,生成后人工抽查前几个点和最后几个点;首尾连续性用数值方式验证,确认data(N) - data(1)的差值等于一个正常步进,而不是零或跳变。
5.5 频率切换瞬间相位跳变:跨时钟域与更新时序
现象:静态输出时波形干净,一旦通过控制接口改频率,切换瞬间波形相位跳一下。原因是频率字来自另一个时钟域,freq_upd没有同步就直接当使能用,锁存时刻不确定;或者频率字和更新脉冲的先后关系不固定,有时旧字配新脉冲,有时新字配旧脉冲。解决:频率字先过两级触发器同步到 DDS 时钟域,更新脉冲用脉冲同步器传递;控制端保证先稳定频率字、再发更新脉冲,中间留至少两个时钟周期的间隔。这类问题在仿真里不容易复现,因为仿真器的跨时钟域行为和实际布线延迟不同,只能靠设计规范提前规避。
6. 从单频到扫频:把 DDS 做成可调信号源的三个扩展
单频输出只是起点,真正让这块板子变得好用,是在同一个工程里加上扫频、多波形切换和幅度调制。这三个扩展共用同一套相位累加和查找表,增加的主要是控制逻辑,代码量不大,但能让一个练习工程变成手边常备的工具。
6.1 扫频:频率字的步进更新与换向边界
扫频的本质是按固定时间间隔改频率字。下面的片段把更新周期设为 1 秒,频率字线性递增,到上限后回绕:
localparam integer SWEEP_PERIOD = 100_000_000; // 100 MHz 下约 1 秒 localparam [31:0] FREQ_START = 32'd42950; // 1 kHz localparam [31:0] FREQ_STOP = 32'd429496; // 约 10 kHz localparam [31:0] FREQ_STEP = 32'd4295; // 每步约 100 Hz reg [31:0] cnt; reg [31:0] freq_cur; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 32'd0; freq_cur <= FREQ_START; end else begin cnt <= cnt + 1'b1; if (cnt >= SWEEP_PERIOD) begin cnt <= 32'd0; // 到达上限后回到起点,形成循环扫频 if (freq_cur + FREQ_STEP > FREQ_STOP) freq_cur <= FREQ_START; else freq_cur <= freq_cur + FREQ_STEP; end end end这段逻辑的输出freq_cur接到相位累加器的频率字端口,配合前面说的更新脉冲使用。参数说明:SWEEP_PERIOD决定扫频速度,想扫得更慢就加大计数上限,注意位宽要能装下这个值,32 位在 100 MHz 下最多约 43 秒,更长周期需要扩位。FREQ_STEP决定步进间隔,和SWEEP_PERIOD一起决定扫频斜率,调的时候两个参数要一起看,别只改一个。
6.2 多波形切换:一张 ROM 分块存多张表
把正弦、三角、方波各存一段,用高地址位选波形,是资源最省的多波形方案。地址映射改成addr = {wave_sel, phase[PHASE_W-1 -: ADDR_W-1]},wave_sel两位就能选四种波形。代价是每张表的深度变成原来的四分之一,频率分辨率不变但相位截断变多,杂散会略微上升。对激励源这类应用足够用,对频谱纯度要求高的场合还是建议一种波形一张 ROM,用片选切换。
| 波形 | 每周期点数 | 幅度位宽 | 主要杂散来源 | 适用场景 |
|---|---|---|---|---|
| 正弦 | 1024 | 12 | 相位截断 | 本振、传感器激励 |
| 三角 | 1024 | 12 | 拐点量化 | 扫描源、线性测试 |
| 方波 | 2 | 1 | 边沿抖动 | 时钟测试、逻辑激励 |
6.3 用 DDS 做 AM 调制:乘法器资源的取舍
幅度调制无非是把基带信号和载波乘起来。FPGA 里可以直接用乘法运算符,综合工具会推断出 DSP 块或逻辑乘法器:
// 载波与调制信号各 12 位,乘积截高 12 位输出 wire [11:0] carrier; wire [11:0] mod_sig; wire [23:0] am_mix = carrier * mod_sig; assign dac_data = am_mix[23:12]; // 截位时直接取高位,注意舍入误差参数说明:截位直接取高 12 位等价于向下取整,会引入一个小的直流偏移,对直流精度敏感的应用要在截位前加半个最低位做舍入。乘法器资源在低端器件上可能紧张,如果只做单频载波加低频调制,也可以把调制信号量化成几位,用移位加法替代乘法器。
写到这里,我最想说的一个习惯是:每次改完频率字参数或者换 ROM 数据,先跑一遍行为级仿真导数据算 SFDR,再上板。有一次偷懒跳过这步,上板后波形看着正常,扫频到高频段才发现幅度塌了一半,回头查是滤波器截止频率设低了,白搭进去一个下午。从那以后我每次动查找表或者改时钟参数,都强制走一遍仿真导出加频谱检查,这个习惯替我省下的时间远多于它花掉的时间。希望帮到你。
本文还有配套的精品资源,点击获取