Design Compiler综合实战:从Verilog到门级网表的全流程避坑指南

简介:本资源是一份面向数字电路设计初学者与EDA工程师的Design Compiler(DC)中文入门教程,聚焦电路综合核心原理与全流程实操。内容系统讲解综合的转换、优化、映射三阶段机制,深入剖析时序约束设置(如时钟定义、输入/输出延迟、多周期路径)、静态时序分析驱动的路径优化、工艺库映射规范及可重用综合脚本编写方法,并结合Verilog语言成分(如if语句)到门级单元的具体映射实例展开说明。资源为单文件Word文档(.doc),全文约954KB,结构清晰,含第四章完整教学内容,覆盖综合定位、约束要求、结果验证及前端设计流程衔接等关键环节。目前已有465人学习下载,适合高校IC设计课程辅助学习、FPGA/ASIC前端工程师快速掌握Synopsys DC综合实战要点。

1. 为什么你写的 Verilog 能仿真通过,却在 DC 综合后面积翻倍、时序崩盘?——一份真正能落地的 Design Compiler 中文实战方案

这不是一份“DC 命令速查表”,也不是照着 GUI 点几下就完事的入门指南。我带过三轮某高校微处理器课程设计、参与过两个 SoC 模块的前端交付,最常被 A 同学深夜发来截图问:“老师,我这个 8 位加法器明明只用了 4 行 assign,怎么综合出来占了 320 个标准单元,还报了 17 条 setup violation?”——答案往往不在代码里,而在你漏掉的那行set_wire_load_model,或误用的//synopsys full_case编译指令。Design Compiler(DC)不是翻译器,它是带着工艺库、时序模型和优化策略的“电路建筑师”:它听懂你的 Verilog,但更忠于你没写清楚的约束。本方案专为需要交毕设/课设、要跑通全流程、要能答辩讲清每一步“为什么”的同学和工程师准备。全文基于 DC 2000.11(工业界长期稳定版本),覆盖从.synopsys_dc.setup初始化、RTL 到门级网表的完整链路、Verilog 语句到电路结构的映射逻辑、典型翻车点排查,以及最关键的——如何用一份脚本应对 16 位→32 位模块升级、如何让报告里的report_timing结果成为你答辩时的硬底气。不讲虚的“综合原理”,只讲你打开终端后敲下的每一行命令背后的真实意图。

2. DC 工作环境搭建与初始化:别让.synopsys_dc.setup成为第一个拦路虎

DC 的启动不是简单输入dc_shell-t就完事。它依赖一套分层加载的初始化机制,而.synopsys_dc.setup文件就是整个流程的“总开关”。很多同学卡在第一步:read_db报错unresolved design reference,或者link后找不到ALU.db,根源几乎都出在这个文件配置上。下面拆解真实项目中必须写死的四类路径,并给出可直接复用的最小可行模板。

2.1 三层初始化路径优先级与实操要点

DC 启动时会按固定顺序搜索.synopsys_dc.setup,且后加载的文件会覆盖前者的同名设置:

  • 第一层:$SYNOPSYS/admin/setup/.synopsys_dc.setup—— Synopsys 安装包自带,定义全局变量(如$SYNOPSYS路径),禁止修改;
  • 第二层:$HOME/.synopsys_dc.setup—— 适合存放个人常用别名(如alias dc='dc_shell-t')和通用库路径,建议在此统一管理search_path;
  • 第三层:当前工作目录下的.synopsys_dc.setup—— 这是本方案的核心战场。每个项目必须有独立文件,用于绑定该设计专属的工艺库、DesignWare 路径和环境变量。

提示:用ls -a查看隐藏文件。若当前目录无此文件,DC 会跳过第三层,直接使用$HOME下的配置——这正是跨项目复用脚本时出现“库找不到”错误的元凶。

2.2.synopsys_dc.setup关键参数详解(附可运行模板)

以下是一个经某实验室验证、适配core_slow.db工艺库的最小化模板。所有路径请按你实际环境替换(/path/to/...需改为绝对路径):

# .synopsys_dc.setup - 项目级初始化文件(保存在 risc_design/ 目录下) # ================================================ # 1. 库搜索路径:DC 会按此顺序查找 .db 文件 set search_path [list \ "/path/to/your/tech_lib" \ "/usr/app/synopsys/2000.11/libraries/syn" \ "/path/to/your/designware" \ ] # 2. 核心库声明(必须!否则 link 失败) set link_library [list "*" "core_slow.db"] set target_library "core_slow.db" set synthetic_library [list "standard.sldb" "dw_foundation.sldb"] # 3. 符号库(仅用于 design_analyzer 图形界面) set symbol_library "/usr/app/synopsys/2000.11/libraries/syn/syn.sldb" # 4. 工作环境变量(影响 report 输出格式) set hdlin_input_policy "strict" set verilogout_no_tri "true"

参数说明与避坑点:

  • search_path必须包含工艺库所在目录(如core_slow.db的父目录)和Synopsys 自带库路径(/syn/下的gtech.db,standard.sldb)。漏掉后者会导致read_verilog时无法解析assign或reg;
  • link_library中的"*"是强制要求。它告诉 DC:先查内存中已加载的模块,再查core_slow.db。没有"*",DC 会忽略你read_db进来的RISC_CORE.db,导致link时报unresolved design reference;
  • synthetic_library若需高性能算术单元(如流水线乘法器),必须显式添加dw_foundation.sldb并确保search_path包含其路径,否则+运算仍走基础standard.sldb,面积与时序均劣化。

2.3 启动模式选择:为什么推荐dc_shell-t而非design_analyzer

模式启动命令适用场景关键限制
dc_shelldc_shell旧版脚本兼容语法不兼容 TCL,无法调用get_nets等现代命令
dc_shell-tdc_shell-t推荐!全流程自动化、脚本可复用需用 TCL 语法,但report_*命令完全一致
design_analyzerdesign_analyzer调试电路结构、查看网表图形不能编辑电路,仅显示;生成command.log供回溯,但无法批量执行

实战经验:某同学用design_analyzer手动点选时钟引脚设create_clock,结果导出脚本时发现命令是set_clock(已废弃),导致后续report_timing无时序路径。所有约束必须用dc_shell-t的 TCL 命令施加,GUI 仅作验证。

2.4 验证环境是否就绪:三步快速诊断

在risc_design/目录下执行以下命令,确认环境正确:

# 1. 启动并加载初始化文件 dc_shell-t # 2. 检查库是否识别(应输出 core_slow.db 路径) dc_shell-t> echo $target_library # 3. 查看搜索路径(确认包含工艺库父目录) dc_shell-t> echo $search_path # 4. 尝试读取示例库(成功则无报错,且提示 "Loading db file ...") dc_shell-t> read_db unmapped/RISC_CORE.db

若第 4 步失败,立即检查:

  • unmapped/RISC_CORE.db文件是否存在?权限是否为rw-?
  • .synopsys_dc.setup中search_path是否指向unmapped/的父目录?(例如search_path是/path/to/risc_design,则RISC_CORE.db必须在/path/to/risc_design/unmapped/下)

3. RTL 读取、链接与设计对象构建:从 Verilog 到可优化网表的临门一脚

读取 RTL 并构建设计对象(Design Object)是综合的起点。这一步看似简单,却是后续所有约束生效的基础。很多“时序不收敛”问题,根源在于link后的设计对象层级混乱,或elaborate未正确展开层次结构。本节聚焦 TCL 模式下的标准流程,并揭示analyze/elaborate与read_db的本质区别。

3.1 两种读取方式的本质差异:何时用read_db,何时用analyze+elaborate

方式命令输入格式适用场景关键特性
直接读库read_db file.dbSynopsys 二进制库(.db)推荐!已验证的 RTL 网表、IP 模块加载快,对象结构清晰,link后顶层模块名即为.db文件名
HDL 分析analyze -format verilog file.v
elaborate top_module
Verilog/VHDL 源码需调试 RTL 语法、或模块未编译成.dbanalyze生成中间文件(默认存work/),elaborate创建内存对象;若elaborate失败,需检查work/下是否有对应模块

血泪经验:某同学将ALU.v放入work/后直接elaborate ALU,但 DC 报错module ALU not found。原因:analyze默认将模块名转为小写,而ALU.v中模块名为alu。解决方案:analyze -f verilog -lib_name work ALU.v显式指定库名,或统一命名规范。

3.2link命令的深层逻辑:解决unresolved design reference的唯一正解

link不是简单的“连接文件”,而是 DC 构建设计层次(Hierarchy)的关键动作。它根据link_library中的库,解析 RTL 中所有module实例化语句,将子模块(如REG_FILE,ALU)与工艺库中的单元(如DFFQX1)或已读取的.db模块进行绑定。失败时的典型报错:

Warning: unresolved design reference 'REG_FILE' Warning: unresolved design reference 'ALU'

这不是路径错误,而是链接策略缺失。正确做法分三步:

# 步骤1:确保所有子模块.db均已读取(按依赖顺序!) read_db unmapped/REG_FILE.db read_db unmapped/ALU.db read_db unmapped/RISC_CORE.db # 顶层 # 步骤2:设置 link_library 包含所有已读库(关键!) set link_library [list "*" "REG_FILE.db" "ALU.db" "core_slow.db"] # 步骤3:执行 link(此时 DC 会遍历 RISC_CORE.db 中的实例,匹配 REG_FILE.db 等) link

为什么必须按依赖顺序读取?
DC 的link是单向解析:顶层RISC_CORE引用REG_FILE,则REG_FILE.db必须在RISC_CORE.db之前读入内存。若顺序颠倒,link时REG_FILE尚未加载,自然报 unresolved。

3.3 设计对象(Design Object)的层级与命名规范

link成功后,DC 内存中会生成一个树状设计对象。理解其命名规则是后续施加约束的前提:

# link 后查看当前设计对象 dc_shell-t> current_design # 输出:/path/to/risc_design/unmapped/RISC_CORE.db:RISC_CORE # 查看设计层次(关键!) dc_shell-t> list_designs # 输出: # RISC_CORE # REG_FILE # ALU # CONTROL # ... # 获取某个模块的完整路径(用于 set_* 命令) dc_shell-t> get_cells -hier -of [get_designs REG_FILE] # 输出:RISC_CORE/REG_FILE/uut (uut 是实例名)

命名规则铁律:

  • current_design的格式为file_path:top_module_name;
  • 子模块路径为top_module_name/sub_module_instance_name;
  • 所有set_*约束必须作用于具体对象,如set_input_delay 2 [get_ports {clk rst_n}],而非模糊的set_input_delay 2 clk(DC 会报错ambiguous object)。

3.4 避坑:RTL 读取与链接阶段的五大高频翻车点

  1. 现象:read_db成功,但link后list_designs只显示顶层,无子模块。
    原因:子模块.db文件未读取,或link_library未包含其文件名。
    解决:执行echo $link_library确认列表,用read_db补全缺失库,再link。

  2. 现象:elaborate top_module报错module not found in library 'work'。
    原因:analyze未成功,或work/下无对应.v文件。
    解决:先analyze -f verilog your_module.v,再elaborate your_module;检查work/目录是否存在。

  3. 现象:get_ports返回空列表,无法对端口施加set_input_delay。
    原因:RTL 中端口未声明为input/output,或elaborate未展开顶层模块。
    解决:用report_cell [current_design]查看端口列表;确保elaborate后current_design指向顶层模块。

  4. 现象:report_area显示面积为 0,report_timing无路径。
    原因:设计对象未正确构建,或link后未执行current_design RISC_CORE切换上下文。
    解决:link后手动执行current_design RISC_CORE,再运行报告命令。

  5. 现象:design_analyzer中电路图显示为黑框,无内部结构。
    原因:未设置symbol_library,或.db文件不含符号信息。
    解决:在.synopsys_dc.setup中添加set symbol_library ...,重启design_analyzer。

4. 约束驱动的综合策略:时钟、时序、面积三大核心约束的工程化实现

综合不是“让 DC 自己猜”,而是用精确的约束告诉它:“我要什么”。本节直击 DC 综合的三大支柱——时钟定义、时序约束、面积/功耗约束,并给出每条命令在真实项目中的物理意义和参数设定依据。拒绝“抄命令”,只讲“为什么这么设”。

4.1 时钟约束:create_clock是时序收敛的基石

DC 的所有时序分析(STA)都围绕时钟网络展开。create_clock不是可选项,而是强制项。未定义时钟,report_timing将无有效路径,综合结果必然时序违规。

# 基础时钟定义(假设顶层端口 clk 频率 100MHz,周期 10ns) create_clock -name CLK -period 10 [get_ports clk] # 虚拟时钟(用于约束输入/输出延迟,当芯片外接 FPGA 时常用) create_clock -name VCLK -period 8 [get_ports {ext_clk}] # 多时钟域:主时钟 + 衍生时钟(如 PLL 输出) create_clock -name CLK_MAIN -period 10 [get_ports clk_main] create_generated_clock -name CLK_PLL -source [get_pins CLK_MAIN] \ -divide_by 2 [get_ports clk_pll]

参数深挖:

  • -period:单位为ns,必须与目标频率严格对应(100MHz → 10ns)。误写为100(意为 100ns=10MHz)将导致综合过度优化,时序余量虚假达标;
  • -name:时钟名将出现在report_clock和report_timing中,必须全局唯一;
  • [get_ports clk]:必须用get_ports获取端口对象,不可写clk(DC 会报错ambiguous object)。

设计习惯:某导师要求学生在create_clock后立即执行report_clock,确认Period,Waveform,Source三列数据正确。这是防止后续所有时序约束失效的第一道防线。

4.2 时序约束:set_input_delay/set_output_delay的物理世界映射

输入/输出延迟约束,本质是描述芯片与外部器件(如 FPGA、ADC)的电气接口时序。set_input_delay对应建立时间(Setup Time),set_output_delay对应保持时间(Hold Time)。

# 输入约束:外部器件在 clk 上升沿后 2ns 发送数据,数据到达芯片引脚需 ≤ 3ns set_input_delay -clock CLK 3 [get_ports {data_in[3:0]}] set_input_delay -clock CLK -max 3 [get_ports {data_in[3:0]}] set_input_delay -clock CLK -min 1 [get_ports {data_in[3:0]}] # Hold time = 1ns # 输出约束:芯片需在 clk 上升沿后 1ns 内稳定输出,且保持 ≥ 0.5ns set_output_delay -clock CLK 1 [get_ports {data_out[3:0]}] set_output_delay -clock CLK -max 1 [get_ports {data_out[3:0]}] set_output_delay -clock CLK -min 0.5 [get_ports {data_out[3:0]}]

关键参数解读:

  • -max:最大允许延迟(对应 Setup),是时序分析的主路径;
  • -min:最小允许延迟(对应 Hold),DC 默认不分析,必须显式指定;
  • 数值来源:必须查阅外部器件 datasheet 的tSU(Setup Time)、tH(Hold Time)、tCO(Clock-to-Out)参数,结合 PCB 走线延迟计算得出。

4.3 面积与功耗约束:set_max_area与set_max_transition的协同艺术

面积约束不是“越小越好”,而是与性能的平衡。set_max_area设定总面积上限,set_max_transition控制信号跳变时间,二者共同抑制功耗与串扰。

# 面积约束:目标不超过 5000 标准单元(根据工艺库 cell 计数) set_max_area 5000 # 转换时间约束:防止信号边沿过缓导致功耗激增 set_max_transition 0.8 [current_design] # 单位:ns,典型值 0.5~1.0 # 电容负载约束:避免驱动能力不足 set_max_capacitance 0.1 [all_outputs] # 单位:pF,根据 IO 标准设定

为什么set_max_transition比set_max_capacitance更关键?
在 130nm 及以下工艺,互连电容主导延迟。set_max_transition强制 DC 插入缓冲器(Buffer)加速慢信号,而set_max_capacitance仅限制扇出。实践中,某 32 位 ALU 模块因未设set_max_transition,综合后关键路径transition达 1.5ns,导致静态功耗超标 40%。添加set_max_transition 0.8后,DC 自动插入BUFHX4,功耗回归规格。

4.4 高级路径约束:set_false_path与set_multicycle_path的慎用原则

非法路径(False Path)和多周期路径(Multicycle Path)是处理异步交互、复位网络的利器,但滥用将导致时序分析失效。

# 复位网络:异步复位信号不参与时序分析 set_false_path -from [get_ports rst_n] -to [all_registers] # 多周期路径:ALU 的 carry-chain 路径天然需要 2 个周期 set_multicycle_path 2 -setup -from [get_cells ALU_UUT] -to [get_cells ALU_UUT] set_multicycle_path 1 -hold -from [get_cells ALU_UUT] -to [get_cells ALU_UUT] # 时钟域交叉(CDC):src_clk → dst_clk 的握手信号 set_false_path -from [get_clocks src_clk] -to [get_clocks dst_clk]

慎用警告:

  • set_false_path会完全关闭路径分析,切勿用于同步逻辑路径。某同学为“快速收敛”对所有rst_n路径设 false,结果综合后复位释放时序违例,芯片无法启动;
  • set_multicycle_path的-hold值必须为-setup值减 1(如 setup=2,则 hold=1),否则 hold check 将过于宽松。

5. Verilog 语句到门级电路的映射真相:从 if/case 到锁存器的“玄学”规避

DC 不是 Verilog 编译器,它是硬件电路生成器。同一段 Verilog,在不同约束下可能映射出面积差 3 倍、时序差 2 倍的电路。本节用真实波形图和网表结构,揭示if/case/always语句背后的硬件真相,并给出可验证的规避方案。

5.1if语句:优先级编码器的隐式生成与锁存器陷阱

if语句天然映射为优先级编码器(Priority Encoder),这是其硬件本质。但未完全译码时,DC 会插入锁存器(Latch),这是 DFT(可测试性)的大忌。

// 【反面案例】隐式锁存器(DC 会报 Warning: inferring latch) always @(a or b or sel) begin if (sel == 1'b1) y = a; // else 分支缺失!DC 推断:sel=0 时 y 保持原值 → 需锁存器 end // 【正面案例】显式赋值消除锁存器 always @(a or b or sel) begin y = b; // 默认赋值,覆盖所有未分支情况 if (sel == 1'b1) y = a; end // 【正面案例】完整 if-else 链 always @(a or b or sel) begin if (sel == 1'b1) y = a; else y = b; end

网表验证:

  • 反面案例综合后,y输出端会连接一个LATCHX1单元;
  • 正面案例中,y = b作为默认值,DC 生成纯组合逻辑(AND2X1,OR2X1),无锁存器。

玄学提醒:某同学坚持用if实现 4 选 1,但未写else,综合后面积比case大 2.3 倍。原因:优先级编码器需额外逻辑判断优先级,而case平行结构更简洁。

5.2case语句:平行结构与full_case/parallel_case编译指令的双刃剑

case默认映射为平行比较结构(Parallel Comparator),面积优、时序稳。但未完全译码时,DC 仍可能生成优先级结构。此时需//synopsys full_case指令,但必须极度谨慎。

// 【安全写法】显式覆盖所有 case always @(posedge clk) begin case (state) 2'b00: next_state = 2'b01; 2'b01: next_state = 2'b10; 2'b10: next_state = 2'b11; 2'b11: next_state = 2'b00; endcase end // 【危险写法】依赖 full_case 指令(仿真与综合行为不一致!) // synopsys full_case // DC 忽略未列出状态,但仿真器不识别! always @(posedge clk) begin case (state) 2'b00: next_state = 2'b01; 2'b01: next_state = 2'b10; endcase end

血泪教训:A 同学用full_case省略了 2 个状态,仿真通过,DC 综合出精简电路。但流片后芯片在特定温度下进入未定义状态,因硬件实际执行了next_state = x(不定态),导致系统死锁。永远用显式枚举替代full_case。

5.3always块:组合逻辑与时序逻辑的严格分界

always块的敏感列表(Sensitivity List)和赋值方式,直接决定 DC 生成组合电路还是时序电路。

// 【组合逻辑】敏感列表必须包含所有输入(推荐用 *) always @(*) begin // DC 2000.11 支持 *,自动推导 y = a & b | c; end // 【时序逻辑】必须用 posedge/negedge,且仅对 reg 赋值 always @(posedge clk or negedge rst_n) begin if (!rst_n) q <= 1'b0; else q <= d; end // 【致命错误】混合敏感列表(DC 会报错) always @(posedge clk or a or b) begin // 错误!时序块中不能有组合信号 if (a) q <= b; end

验证方法:

  • 执行report_cell [get_cells y],若类型为AND2X1,则为组合逻辑;
  • 执行report_cell [get_cells q],若类型为DFFQX1,则为触发器。

6. 综合执行、报告分析与验收:用report_*命令构建你的答辩证据链

综合不是compile一按就完。真正的验收,是读懂 DC 生成的每一份报告,并用它们证明:我的设计满足时序、面积、功耗所有指标。本节教你如何用report_timing、report_area、report_power构建无可辩驳的证据链,并给出答辩时最常被问的三个问题及应答逻辑。

6.1compile命令的三种模式与选择依据

compile是 DC 的核心优化命令,但不同模式适用于不同阶段:

模式命令适用阶段特点
基础编译compile初次综合、快速验证执行转换+优化+映射,但不深度优化,速度快
深度编译compile -map_effort high时序收敛关键阶段启用高级映射算法,面积/时序优化更强,耗时长
增量编译compile -incremental_mapping修改约束后快速迭代仅重映射变化部分,节省 70% 时间

实战技巧:某 SoC 项目中,A 同学首次compile后report_timing显示 12 条 setup violation。他未改约束,直接compile -map_effort high,耗时 42 分钟后仍剩 3 条 violation。导师指导:先report_constraint -all_violators定位最差路径,发现是set_input_delay设为 1ns(过严),调整为 2.5ns 后,compile一次通过。优化前先诊断,而非盲目加力。

6.2 关键报告解读:从report_timing到report_power的证据链

6.2.1report_timing:时序收敛的黄金标准

执行report_timing -delay max -max_paths 5,重点关注三列:

列名含义合格标准示例问题
Slack时序余量≥ 0(Setup)
≥ 0(Hold)
Slack = -0.85→ 违例,需优化
Path Type路径类型setup(主路径)
hold(保持路径)
若hold为负,需检查set_output_delay -min
Endpoint路径终点应为寄存器Q端或输出端口若终点是组合逻辑输出,说明时钟未正确传播

答辩话术:“本设计report_timing显示最大负 Slack 为 -0.02ns,位于 ALU 的 carry-out 路径。我们通过set_max_transition 0.6强制插入缓冲器,将 Slack 提升至 +0.15ns,满足 100MHz 时序要求。”

6.2.2report_area:面积合规性的量化证明

report_area -hierarchy按模块输出面积(单位:标准单元数):

Design Library Area ----------------------------------------- RISC_CORE core_slow 4820 REG_FILE core_slow 1250 ALU core_slow 1890 CONTROL core_slow 980 ...

验收标准:

  • 总面积 ≤set_max_area设定值(如 5000);
  • 关键模块(如 ALU)面积与 RTL 规格书偏差 < 10%;
  • 若REG_FILE面积异常大,检查是否误用reg [31:0] mem[0:1023](DC 会综合为寄存器堆,非 RAM)。
6.2.3report_power:功耗的初步评估

report_power -hierarchy输出各模块动态功耗(单位:mW):

Design Power(mW) Cells ----------------------------------------- RISC_CORE 12.5 4820 ALU 5.2 1890 ← 占比 41.6%,合理 REG_FILE 3.1 1250

关键判断:

  • ALU 功耗占比 > 50%?检查是否未启用set_max_transition导致信号边沿过缓;
  • 总功耗 > 15mW?检查set_max_capacitance是否过松,导致驱动单元过大。

6.3 答辩高频问题应答指南:用报告数据直击要害

问题应答逻辑(基于报告数据)答辩话术示例
Q1:为什么你的时序能收敛,而别人不行?指出关键约束的差异化设定“我们针对 ALU 的 carry-chain 路径,设定了set_multicycle_path 2,并配合set_max_transition 0.6,使关键路径 Slack 从 -1.2ns 提升至 +0.18ns。report_timing第 3 行详细列出了该路径的优化前后对比。”
Q2:面积超了怎么办?展示report_area -hierarchy定位热点“report_area显示CONTROL模块占 1520 单元,远超预期。我们发现其状态机用了 8 个case分支,改为if-else优先级结构后,面积降至 980,符合规格。”
Q3:这个锁存器是怎么产生的?用report_cell和 RTL 源码双重验证“report_cell LATCHX1显示其驱动data_out。我们检查 RTL,发现always @(a or b)中未覆盖b==0的分支,已补全else y=0。compile后report_cell确认锁存器消失。”

从那以后我每次compile前,都强制执行三步:

  1. report_constraint -all_violators扫描所有约束是否生效;
  2. report_clock确认时钟周期与波形无误;
  3. check_design检查设计完整性(如无未连接端口)。
    这三步耗时不到 10 秒,却能避免 80% 的返工。希望帮到你。

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