简介:本资源是一份面向数字电路设计初学者与EDA工程师的Design Compiler(DC)中文入门教程,聚焦IC前端综合核心流程与实操要点。内容系统讲解综合原理、约束设置、脚本编写及优化策略,涵盖转换/优化/映射三阶段机制、基于路径的时序驱动优化、可重用参数化脚本设计,以及时钟定义、线负载模型、多周期路径等关键约束实践,直击综合质量保障难点。资源为单文件Word文档(.doc),全文约954KB,结构清晰,含第四章完整内容:从综合定位与设计流程地位,到Verilog语言成分映射实例(如if语句生成选择器的门级实现),辅以图示与DC 2000.11版本实操说明。目前已有465人学习下载,适合高校微电子/集成电路专业学生、FPGA开发工程师及转岗EDA工具使用者快速掌握DC综合规范与工程落地要点。
1. Design Compiler 不是“翻译器”,而是带时序感知的电路重构引擎:为什么你写的 Verilog 综合出来像一锅乱炖,而别人却能稳稳压在 500MHz?
你有没有遇到过这种玄学时刻:同一段 Verilog,A 同学综合后时序收敛、面积干净、功耗可控;你照着抄过去,DC 却报出一堆setup violation、hold violation,网表里还莫名其妙冒出几个锁存器(latch),后端布局布线直接卡死?更诡异的是,你改了一行always @(posedge clk)的敏感列表,综合结果的路径延迟居然跳变 200ps——这根本不是“编译”,更像是 DC 在暗地里重写了你的电路逻辑。
真相是:Design Compiler 从不“忠实翻译”你的代码。它干的是一场精密的、受约束驱动的电路结构重构:把你的 RTL 当作一份模糊的需求说明书,再从工艺库中成百上千个单元(AND2X1、CLKBUF_X4、DFFHQX1…)里,挑出最能满足时序、面积、功耗三重目标的组合,并动态插入缓冲器、复制寄存器、拆分关键路径、甚至重排逻辑层级。这个过程里,if和case的语义差异会直接决定是否引入优先级编码器;assign a = b >> c中c是常量还是变量,会让 DC 在“硬连线移位”和“全定制移位器”之间做完全不同的架构选择;而你漏写的一个else分支,DC 就会默默给你补上一个电平敏感的锁存器——这不是 bug,是它在告诉你:“你没说清楚,我只能按组合逻辑未定义行为来兜底”。
这份中文版《dc使用中文版(比较详细)》之所以值得深挖,正因为它没停留在“怎么敲命令”的操作手册层面,而是用大量真实综合前后对比图(图4-5 vs 图4-6)、Verilog 源码与门级网表的逐层映射(例1~例13),把 DC 内部那套“约束→分析→重构→验证”的黑匣子,一层层剥开给你看。它让你明白:综合不是终点,而是你第一次真正“看见”自己代码在硅片上物理形态的起点。搞懂它,你才能从“被 DC 牵着鼻子走”的被动者,变成能预判、能引导、能反向调试综合结果的主动设计者。
2. 综合的本质:三阶段重构——转换、优化、映射,每一步都在重写你的电路基因
DC 的综合流程绝非线性流水线,而是一个闭环的、以静态时序分析(STA)为指挥中枢的重构循环。它把你的 RTL 当作待解构的文本,用工艺库作为字典,最终输出的不是“等价电路”,而是“满足约束的最优近似解”。理解这三阶段,就是掌握 DC 的底层心跳。
2.1 转换(Translation):从行为描述到布尔网络,gtech.db 是它的“思维草稿纸”
转换阶段是 DC 的“破题”环节。它不关心时序、不考虑面积,只做一件事:将 Verilog 的语法糖,无损地展开为最基础的布尔逻辑网络。此时 DC 使用的是 Synopsys 自带的gtech.db库——一个仅含纯逻辑功能(AND/OR/NOT/XOR)、不含任何物理属性(延迟、驱动能力、面积)的抽象单元库。
// 【例1】简单 if 语句 module Set_If_AEqB(A,B,Z); input [1:0] A,B; output Z; reg Z; always @ (A or B) if (A==B) Z = 1; else Z = 0; endmoduleDC 在转换阶段会将其展开为:
A==B→ 生成(A[1]==B[1]) && (A[0]==B[0])的布尔表达式树;if-else→ 构建一个二选一多路选择器(MUX2X1)结构,Z = MUX2X1(sel=A==B, in0=0, in1=1);- 整个网络由
gtech.db中的AND2,XNOR2,OR2等单元拼接而成,形成一张“纯逻辑图”。
关键机制说明:
gtech.db是 DC 的“思想实验场”。它确保转换过程绝对可逆、无歧义。你看到图4-4的“转换前结构图”,就是这张纯逻辑网络的可视化。它不包含任何物理信息,所以无法用于后端实现——这只是 DC 开始思考的第一步草稿。
2.2 优化(Optimization):STA 驱动的手术刀,删冗余、切关键路径、插缓冲器
转换后的网络是“正确但低效”的。优化阶段才是 DC 展现其智能的地方:它调用内置的DesignTimer工具,对网络中所有有效路径(valid paths)进行静态时序分析,然后像外科医生一样精准干预。
什么是“有效路径”?DC 定义极其严格:
- 起点:输入端口(primary input)或触发器(FF)的 D 端;
- 终点:输出端口(primary output)或触发器(FF)的 D 端;
- 必须经过:至少一个组合逻辑块(combinational logic block);
- 排除:纯时钟网络、异步复位路径(除非显式声明为
set_ideal_network false)。
# DC 中启用 STA 分析的关键命令 create_clock -name clk -period 2.0 [get_ports clk] set_input_delay 0.5 -clock clk [all_inputs] set_output_delay 0.8 -clock clk [all_outputs] check_design # 检查设计完整性 report_constraint -all_violators # 报告所有违反约束的路径一旦 STA 发现某条路径slack = required_time - arrival_time < 0(即建立时间违例),DC 就启动优化策略:
- 逻辑重构:将
a & b & c & d拆分为(a & b) & (c & d),降低扇出,缩短关键路径; - 寄存器复制:对扇出大的 FF 输出,复制一个同功能 FF,让不同分支走不同副本,避免长线负载;
- 缓冲器插入:在长连线中间插入
BUF_X2,改善信号完整性,但增加面积; - 门级优化:用
NAND2 + INV替代AND2(若工艺库中 NAND 延迟更低)。
关键机制说明:优化不是“越快越好”,而是在约束框架内做帕累托最优。图4-3的“速度-面积折中曲线”揭示了本质:当路径延迟减小到某阈值,再优化带来的时序收益会急剧衰减,而面积代价却线性上升。DC 的算法(如 DAG-based mapping)正是在寻找这个拐点。
2.3 映射(Mapping):从布尔网络到工艺库单元,core_slow.db 是它的“施工蓝图”
映射是落地的最后一步:将优化后的布尔网络,一对一绑定到目标工艺库(如core_slow.db)中的具体物理单元。此时,gtech.db的抽象门被替换为带完整 PDK 参数的实体:
| gtech.db 抽象单元 | core_slow.db 实体单元 | 关键物理参数 |
|---|---|---|
| AND2 | AND2X1 | delay=0.12ns, area=12.5um², drive=4 |
| DFF | DFFHQX1 | clk-to-q=0.18ns, setup=0.15ns, hold=0.05ns |
| BUF | CLKBUF_X4 | drive=16, max_fanout=32 |
# 映射阶段的核心设置 set target_library [list "core_slow.db"] set link_library [list "*" "core_slow.db" "standard.sldb"] read_db unmapped/RISC_CORE.db link # 此时 DC 才真正将 RTL 模块与 core_slow.db 中的单元关联映射不是简单替换。DC 会根据set_max_transition(最大转换时间)、set_max_capacitance(最大电容负载)等规则,为每个 net 选择驱动能力匹配的单元。例如,一个驱动 50pF 负载的时钟 net,DC 不会选BUF_X1(驱动能力仅 0.5pF),而会强制选用CLKBUF_X16。
关键机制说明:
link_library中的"*"至关重要。它告诉 DC:“先查内存中已加载的设计模块,再查工艺库”。若遗漏"*",DC 在链接RISC_CORE时找不到其子模块ALU.db,就会报unresolved design reference——这是新手最常踩的坑,根源在于没理解 DC 的符号解析顺序。
3. Verilog 到门级:语言成分如何被 DC “翻译”成硅片上的物理结构?
Verilog 的每一行代码,在 DC 眼里都不是“指令”,而是对硬件结构的概率性暗示。DC 会根据语句类型、上下文、约束条件,推断你最可能想要的电路拓扑。本节用原文中 6 个典型例题,拆解 DC 的“心证”逻辑。
3.1 If 语句:优先级编码器的默认构造者,缺省赋值是防 latch 的后悔药
DC 对if的解读是隐含优先级。它假设你写if (a) out=x; else if (b) out=y;的意图,就是构建一个 a 优先于 b 的编码器。
// 【例2】条件不完全译码 → latch 诞生 module if_bad(num,xval,yval,zval,dec); input [1:0] num; input xval,yval,zval; output dec; reg dec; always@(num or xval or yval or zval) begin if (num == 2'b00) dec <= xval; else if (num == 2'b01) dec <= yval; else if (num == 2'b10) dec <= zval; // 缺少 num==2'b11 的分支! end endmoduleDC 的推理链:
always @(num...)→ 这是时序逻辑(因有<=);- 但
num的 4 种状态只覆盖 3 种 → 剩余 1 种状态(2'b11)下dec的值未定义; - 数字电路中,未定义的时序输出必须保持原值 →电平敏感锁存器(latch)是唯一能实现“保持”的结构;
- 因此,DC 自动生成一个
LATCH单元,其使能端(enable)由num!=2'b00 && num!=2'b01 && num!=2'b10控制。
避坑 / 常见问题 / 排查
- 现象:综合报告中出现
Warning: Latch inferred for signal 'dec',且网表里多出LATCH单元。
原因:if-else if链未覆盖所有输入组合,DC 为保“功能等价”而插入 latch。
解决:强制补全分支else dec <= 1'b0;,或用always @(*)+if-else全覆盖。- 现象:
if语句综合后关键路径延迟异常高。
原因:DC 构建了深度优先级树(如 8 输入需 7 级 MUX),扇出大、负载重。
解决:改用case+parallel_case指令,或手动展平为二叉树结构。- 现象:仿真通过,但综合后功能错误。
原因:if的敏感列表遗漏信号(如always @(a or b)却用了c),DC 转换时忽略c变化,导致组合逻辑“锁存”。
解决:永远用always @(*)或always_comb(SystemVerilog),让工具自动推导敏感列表。
3.2 Case 语句:平行结构的天然盟友,但 full_case/parallel_case 是双刃剑
case默认被 DC 解读为并行比较:所有分支条件同时计算,通过多路选择器(MUX)选出结果。这比if的串行优先级结构更利于时序收敛。
// 【例4】全译码 case → 干净的平行 MUX module CASE_BEST(num, xval, yval, zval, aval, dec); input [1:0] num; input xval, yval, zval, aval; output dec; reg dec; always@(num or xval or yval or zval or aval) begin case (num) 2'b00: dec <= xval; 2'b01: dec <= yval; 2'b10: dec <= zval; 2'b11: dec <= aval; // 全覆盖! endcase end endmoduleDC 生成一个 4:1 MUX,4 个输入直接连xval/yval/zval/aval,选择线num直连 MUX 的 sel 端——结构扁平,延迟固定。
但当你用case处理不完全译码时,DC 的默认行为是保守的优先级结构(为兼容旧代码)。这时就需要编译指导(synopsys directives)强行干预:
// 【例5】强制平行结构 always @(*) begin case (1'b1) // synopsys parallel_case w: b <= 2'b10; x: b <= 2'b01; endcase end// synopsys parallel_case告诉 DC:“忽略 w/x 的实际值,把它们当作互斥的并行条件处理”,DC 便生成两个独立的比较器 + OR 门。
避坑 / 常见问题 / 排查
- 现象:加了
// synopsys full_case后,仿真与综合结果不一致。
原因:full_case让 DC 忽略未列出的状态(如num==2'b11),综合时直接剪枝,但仿真器仍会执行 default 行为。
解决:full_case只用于确定“所有状态均已覆盖”的场景;否则用default: $display("ERROR");辅助调试。- 现象:
parallel_case导致面积暴增。
原因:DC 为每个分支生成独立比较器(如 8 分支需 8 个 2-input comparator),而非共享逻辑。
解决:仅在关键路径上用;或改用if-else if+force_top_down等高级指令。- 现象:DC 报错
Error: Unknown directive 'parallel_case'。
原因:指令写在case语句之后,或.v文件未用read_verilog -autoread加载(DC 2000.11 需显式支持)。
解决:指令必须紧贴case关键字后;检查 DC 版本是否支持该 directive。
3.3 Always 语句:组合与时序的分水岭,敏感列表是 DC 的“宪法”
always是 DC 解析电路性质的最关键线索。它不看=还是<=,而看敏感列表(sensitivity list):
always @(a or b or c)→组合逻辑:DC 要求所有输入出现在列表中,否则推断为“锁存器”;always @(posedge clk)→同步时序逻辑:DC 插入 DFF,时钟端连clk;always @(posedge clk or negedge rst_n)→异步复位时序逻辑:DC 插入带rst_n异步清零端的 DFF。
// 【例7】组合逻辑:敏感列表必须完备 module always_1 (y, a, b, c, d); output y; input a, b, c, d; reg y, tmp1, tmp2; always @(a or b or c or d) // ✅ 全部输入都列出了 begin tmp1 = a & b; tmp2 = c & d; y = tmp1 | tmp2; end endmodule若误写为always @(a or b),DC 会认为c/d变化不影响y,于是tmp2的值被“锁存”,生成 latch。
避坑 / 常见问题 / 排查
- 现象:
always @(*)综合后仍有 latch。
原因:@(*)仅自动推导显式读取的信号,若y被if (flag) y = a;赋值,但flag未在if条件中出现,DC 仍视为未定义。
解决:用verilator --lint-only或 DC 的check_design检查未覆盖分支。- 现象:
always @(posedge clk)综合出异步复位电路。
原因:代码中有if (!rst_n) q <= 0;,但rst_n不在敏感列表中,DC 无法识别其异步性。
解决:严格按always @(posedge clk or negedge rst_n)书写,rst_n必须出现在列表中。- 现象:综合报告提示
Warning: Unintended latches for signal 'tmp1'。
原因:tmp1在if分支中被赋值,但else分支未赋值,DC 推断其需保持原值。
解决:所有reg型变量,在always块中必须有全覆盖赋值(包括else或default)。
4. DC 工作流实战:从启动、读取、约束到编译,一条命令背后的千层逻辑
DC 的命令行(dc_shell-t)看似简单,但每个命令背后都牵扯到环境初始化、对象解析、数据库加载、约束传播等复杂机制。本节以原文中 RISC_CORE 示例为蓝本,还原一次真实综合的完整脉络。
4.1 启动与初始化:.synopsys_dc.setup是 DC 的“操作系统内核”
DC 启动时,会按固定顺序搜索三个位置的.synopsys_dc.setup文件:
$SYNOPSYS/admin/setup—— 全局安装配置(公司级 PDK 路径、license server);$HOME/.synopsys_dc.setup—— 用户级配置(常用别名、search_path);- 当前目录下的
.synopsys_dc.setup—— 项目级配置(本项目专用库路径、target_library)。
# 典型 .synopsys_dc.setup 内容 search_path = ". ./lib /usr/synopsys/libraries" target_library = "core_slow.db" link_library = "* core_slow.db standard.sldb" synthetic_library = "dw_foundation.sldb" set_operating_conditions -library core_slow.db "slow"关键机制说明:
search_path是 DC 的“文件系统根目录”。当执行read_db unmapped/RISC_CORE.db时,DC 会依次在.、./lib、/usr/synopsys/libraries下查找该文件。若RISC_CORE.db在~/DC_example/risc_design/unmapped/,而search_path未包含此路径,DC 就会报file not found——这是环境配置错误的头号原因。
4.2 读取与链接:read_db+link是构建设计数据库的原子操作
DC 的设计数据库(Design Database)是一个内存中的层次化对象树。read_db只是把.db文件加载进内存,link才是真正建立模块间引用关系的步骤。
# TCL 模式下标准流程 dc_shell-t> read_db unmapped/RISC_CORE.db # 输出:Loading db file '/.../RISC_CORE.db' dc_shell-t> link # 输出:Linking design: RISC_CORE # Using the following designs and libraries: RISC_CORE, core_slow.db, class.db...link命令执行时,DC 做了什么?
- 解析
RISC_CORE.db中的module定义; - 查找
link_library中所有库,匹配其submodule实例(如ALU,REG_FILE); - 若
ALU.db在link_library指定的路径中存在,则将其加载并关联; - 若不存在,报
unresolved design reference: ALU。
避坑 / 常见问题 / 排查
- 现象:
link后报Warning: Unresolved design reference 'ALU'。
原因:ALU.db文件不在search_path的任何目录下,或link_library未包含其所在库。
解决:用ls -l ~/DC_example/risc_design/unmapped/ALU.db确认文件存在;在.synopsys_dc.setup中添加link_library = "* core_slow.db ALU.db"。- 现象:
read_db成功,但report_cell显示 0 个单元。
原因:.db文件是“未映射”版本(unmapped),只含逻辑结构,不含工艺库单元。link后需compile才生成门级网表。
解决:link后执行compile,再report_cell。- 现象:
dc_shell-t启动后立即退出,无提示。
原因:.synopsys_dc.setup中有语法错误(如set target_library = "core_slow.db"多了=)。
解决:用dc_shell-t -f /dev/null启动空会话,再逐行 source setup 文件定位错误行。
4.3 约束施加:set_*命令不是魔法,而是对物理世界的精确建模
DC 的所有set_*命令,本质都是在给设计数据库中的对象(port/net/cell)打标签,这些标签会被DesignTimer读取,用于 STA 计算。
# 核心约束命令解析 create_clock -name clk -period 2.0 -waveform {0 1.0} [get_ports clk] # → 在 clk port 上创建一个周期 2.0ns、占空比 50% 的时钟对象 set_input_delay 0.5 -clock clk [all_inputs] # → 告诉 DC:所有输入端口的数据,在 clk 上升沿到来前 0.5ns 就已稳定(即 input arrival time) set_output_delay 0.8 -clock clk [all_outputs] # → 告诉 DC:所有输出端口的数据,必须在 clk 上升沿到来后 0.8ns 内稳定(即 output required time) set_max_transition 0.3 [all_outputs] # → 告诉 DC:所有输出端口的信号跳变时间不能超过 0.3ns(防止串扰、EMI)关键机制说明:
set_*命令的对象必须精确。set_load 3 Clk是错的,因为Clk可能是 port 名、net 名、pin 名。DC 会报Ambiguous object name。正确写法是set_load 3 [get_nets Clk],明确指定作用于 net 对象。
4.4 编译与报告:compile是重构引擎的总开关,report_*是你的 X 光机
compile命令触发 DC 的完整三阶段流程:转换 → 优化 → 映射。它不是“一键编译”,而是启动一个复杂的迭代优化循环。
dc_shell-t> compile # DC 输出大量日志: # ... Translation completed. # ... Optimizing... # ... Mapping to library 'core_slow.db'... # ... Final optimization pass... # ... Compile completed. # 关键报告命令 report_area # 报告总面积(um²)、各模块面积 report_timing -path full # 报告所有路径的时序(slack, delay) report_power # 报告功耗(若启用了 power library) report_constraint -all_violators # 报告所有约束违例(setup/hold/voltage)report_timing的输出是 DC 的核心诊断书。其中slack值为负,表示违例;path_type为max表示建立时间路径,min表示保持时间路径。
避坑 / 常见问题 / 排查
- 现象:
compile后report_timing显示No paths found。
原因:未定义时钟(create_clock缺失),或时钟端口名与 RTL 中不一致(如 RTL 中是sys_clk,但create_clock写了clk)。
解决:report_clock查看已定义时钟;get_ports确认端口名。- 现象:
report_area显示面积为 0。
原因:compile未成功执行(可能因约束冲突中断),或target_library未正确设置。
解决:检查compile日志末尾是否有Compile completed.;运行echo $target_library。- 现象:
report_power报错Power analysis not enabled。
原因:core_slow.db未包含功耗模型(需power_library),或未执行set_power_analysis_mode -enabled true。
解决:确认工艺库是否为power-aware;添加set_power_analysis_mode -enabled true。
5. 设计取舍的十字路口:为什么 DC 有时“不听话”,以及如何让它听懂你的话
DC 从不犯错,它只是太忠于你的约束,忠于工艺库的物理极限,忠于布尔代数的数学本质。当你觉得它“不听话”,往往是你的约束表述与物理现实之间,存在一道你没察觉的鸿沟。真正的高手,不是命令 DC 做什么,而是学会用 DC 的语言,描述世界本来的样子。
5.1 约束冲突:当set_max_area和set_max_delay同时存在,DC 会选谁?
这是最经典的取舍困境。DC 的官方文档说它会“在约束间做折中”,但折中不是平均主义,而是有明确优先级的:
- 时序约束(Timing Constraints)拥有最高优先级:
create_clock、set_input_delay、set_output_delay是硬性红线。如果set_max_delay 1.0与create_clock -period 0.8冲突(即要求路径比时钟周期还短),DC 会直接报error,拒绝综合。 - 面积与功耗约束是软性目标(Soft Constraints):
set_max_area、set_max_power是优化目标,不是强制门槛。DC 会在满足所有时序约束的前提下,尽可能靠近这些目标。如果set_max_area 10000与set_max_delay 1.0冲突,DC 会选择牺牲面积,插入更多缓冲器、复制寄存器,只为保住时序。 - 物理约束(Physical Constraints)是次级硬约束:
set_max_transition、set_max_capacitance是为了保证信号完整性。若某路径transition > set_max_transition,DC 会强制插入缓冲器,哪怕面积翻倍。
验证方法:用
report_constraint -all_violators查看所有违例。若只有area违例而无timing违例,说明 DC 已尽全力在保时序;若两者都有,说明你的set_max_delay设置本身就不合理(比如要求 1GHz 下 10 级逻辑完成)。
5.2 RTL 编写哲学:从“功能正确”到“综合友好”,三原则重塑你的编码习惯
DC 的“脾气”源于它对 RTL 的解读逻辑。改变 DC 很难,改变自己的 RTL 却立竿见影。我总结出三条血泪经验:
| 原则 | 错误写法 | 正确写法 | DC 解读差异 |
|---|---|---|---|
| 全覆盖赋值 | if (sel) y=a;(缺else) | if (sel) y=a; else y=b;或y = sel ? a : b; | 前者 → latch;后者 → clean MUX |
| 显式时钟域 | always @(posedge clk) if (rst) q<=0; else q<=d; | always @(posedge clk or negedge rst) if (!rst) q<=0; else q<=d; | 前者 → DC 无法识别异步复位,可能综合出同步复位(时序差 1 cycle);后者 → 正确插入DFFAR单元 |
| 分离控制与数据 | always @(posedge clk) case (state) ... endcase | always @(posedge clk) state <= next_state;+always @(*) case (state) ... endcase | 前者 → 状态机与输出耦合,DC 难以优化输出路径;后者 → 清晰的两级结构,DC 可分别优化状态转移与输出逻辑 |
5.3 进阶延伸:从 DC 到物理实现,为什么set_wire_load_model是连接前端与后端的脐带?
set_wire_load_model命令常被新手忽略,但它却是 DC 综合结果能否在后端顺利布局布线的生死线。它的作用,是让 DC在综合阶段就“预见”连线延迟。
- 问题:DC 综合时,需要估算 net 的延迟(wire delay)来判断时序。但此时还没有布局(placement),不知道 net 有多长。若假设 wire delay=0,DC 会过度乐观,综合出的网表在后端布线后必然时序违例。
- 解法:
set_wire_load_model加载一个“线负载模型”(Wire Load Model, WLM),它是一个经验公式,根据 fanout(扇出数)预测 net 的平均长度和电容。例如wlm_cmos25模型规定:fanout=1 → cap=0.01pF;fanout=10 → cap=0.15pF。 - 取舍:WLM 是近似模型。过于乐观(如用
wlm_fast)会导致综合后端违例;过于悲观(如用wlm_slow)会导致 DC 过度插入缓冲器,面积浪费。最佳实践是:先用保守 WLM 综合,拿到初步网表后,让后端工具(如 IC Compiler)提取真实 RC 参数,再反馈给 DC 做“re-compile”。
从那以后我每次启动 DC,第一件事就是source .synopsys_dc.setup,第二件事是check_design,第三件事是report_constraint -all_violators—— 不为别的,就为在compile前,亲手摸一遍设计的“脉搏”,确认我的约束没有自相矛盾,确认我的 RTL 没有埋下 latch 的雷。DC 不是黑匣子,它是你硅片上第一个真正的合作伙伴。你给它清晰的指令,它还你可预测的物理实现。希望帮到你。
本文还有配套的精品资源,点击获取