热度 19
2013-8-7 17:12
1570 次阅读|
0 个评论
RTL编码风格所造成的仿真和综合的不匹配 1.0 简介 ASIC或者FPGA设计就是把一个想法或者概念转换成物理实现的过程。这篇文章讨论了HDL编码风格所造成的RTLGate-level仿真的不一致的几种情况。 它的一个基本的判定规则是,符合以下两种情况的编码风格是坏的编码风格。 · 提供给HDL仿真器的关于设计的信息不能传送给综合工具 · 综合开关提供给综合工具的信息在仿真器中不可得 如果上犯了上两条禁忌,就会造成综合前的RTL仿真和综合后的门级仿真不匹配的问题。这些问题很难发现,因为由于门的数量的增多,完备测试是不可能的,而且如果不注意会最终导致ASIC生产的失败。 解决方法就是了解什么样的编码风格或者综合选项会导致RTL到门级的仿真不一致,并避免这些问题。 2.0 敏感列表 当一个always块中并不包含Verilog的关键字posedge或者negedge的时候综合工具会把它综合成组合或者锁存器逻辑。对于一个组合逻辑always块,逻辑直接从块中的等式中推导出,*而与敏感列表没有关系*。综合工具读取敏感列表,并把它与always块中的等式相比较,报告出可能造成pre-和post-synthesis仿真不一致的疏漏的敏感列表。 若一个信号在敏感列表中出现而没有在always块用到,它不会对pre-和post-synthesis造成任何功能上的不同。多余的敏感信号的唯一的后果就是使仿真变慢了。 2.1 不完全敏感信号列表 综合工具总是把always块中的等式按完整敏感信号来综合。但是pre-synthesis的功能仿真就会非常不同。 · module code1a (o, a, b); · output o; · input a, b; · reg o; · always @(a or b) · o = a b; · endmodule 在code1a中敏感列表时完整的,pre-synthesis和post-synthesis都会按一个2输入and门来仿真 · module code1b (o, a, b); · output o; · input a, b; · reg o; · always @(a) · o = a b; · endmodule 在code1b中敏感列表不完整,post-synthesis仍然是个2-input and门,而在pre-synthesis的仿真中只有在a有变化的时候always块才会执行。 · module code1c (o, a, b); · output o; · input a, b; · reg o; · always · o = a b; · endmodule code1c不包括任何敏感列表,在pre-synthesis的时候仿真器在在always块中陷入无限循环,而post-synthesis仍然是个2-input and门 2.2 不同赋值顺序的完整敏感列表 -在pre-synthesis时always块中的赋值语句会按顺序执行。这样,在always块中有局部临时变量时就会产生问题,临时变量可能在if语句的判断条件部分,case语句,或者赋值语句的右端中用到。*如果一个临时变量在被赋值之前用到,就会导致赋值的乱序*。直到temp变量的赋值语句被执行,temp将保持之前传送给它的值。 · module code2a (o, a, b, c, d); · output o; · input a, b, c, d; · reg o, temp; · always @(a or b or c or d) begin · o = a b | temp; · temp = c d; · end · endmodule -在code2a中,temp在被赋值之前被用到,temp将保持之前的值。之后temp被赋新值。在pre-synthesis仿真中,temp将像一个锁存器。而在综合时,它会按正确的顺序赋值。此会造成不一致。 · module code2b (o, a, b, c, d); · output o; · input a, b, c, d; · reg o, temp; · always @(a or b or c or d) begin temp = c d; o = a b | temp; · end · endmodule -code2b中pre-和post-synthesis仿真结果是一致的。 3.0 function -function总是被综合成组合电路,由于这个原因,一些工程师把所有的组合逻辑都用function来实现。如果function按照组合逻辑来仿真使用function是没有什么问题的。当工程师在function中的组合逻辑中犯了错误而使它仿真时像一个锁存器,就会产生错误。当function代码仿真寄存器的行为的时候综合工具是没有警告信息的。用function建模组合逻辑是危险的。 · module code3a (o, a, nrst, en); · output o; · input a, nrst, en; · reg o; · always @(a or nrst or en) · if (!nrst) o = 1'b0; · else if (en) o = a; · endmodule -code3a是设计锁存器的典型方法。 · module code3b (o, a, nrst, en); · output o; · input a, nrst, en; · reg o; · always @(a or nrst or en) · o = latch(a, nrst, en); · function latch; · input a, nrst, en; · if (!nrst) latch = 1'b0; · else if (en) latch = a; · endfunction · endmodule -code3b中把相同的代码用在了一个if语句中,产生了3-input and门。 -如果在function中的代码是用来生成latch的,pre-synthesis仿真将仿真成latch的行为,为post-synthesis仿真将会仿真成组合逻辑。从而造成不匹配。 4.0 case语句 4.1 Full Case *用编译指令//synopsys full_case 给编译工具更多的关于设计的信息,而不能把这些信息提供给仿真工具*。这条特别指令是提醒编译工具,case语句是fully defined的,输出对于没用用到的case是"don't care"的。当用此条指令时,pre-和post-synthesis的仿真可能会不同。此外,尽管此条指令是告诉编译工具把未用的的情况视为“不处理”,这条指令有时会使设计比忽略full_case时更大和更慢 · module code4a (y, a, en); · output y; · input a; · input en; · reg y; · always @(a or en) begin · y = 4'h0; · case ({en,a}) · 3'b1_00: y = 1'b1; · 3'b1_01: y = 1'b1; · 3'b1_10: y = 1'b1; · 3'b1_11: y = 1'b1; · endcase · end · endmodule 在code4a中case没有综合指令,结果是三输入与门和一个反相器。pre-和post-synthesis仿真是一致的。 · module code4b (y, a, en); · output y; · input a; · input en; · reg y; · always @(a or en) begin · y = 4'h0; · case ({en,a}) // synopsys full_case 3'b1_00: y = 1'b1; · 3'b1_01: y = 1'b1; · 3'b1_10: y = 1'b1; · 3'b1_11: y = 1'b1; · endcase · end · endmodule 在code4b中case语句带综合指令full_case,正是因为此,在综合时en输入被优化掉而成为一个悬挂的输入。code4a和code4b综合前的仿真结果以及code4a综合后的结果都是一致的,而它们是与code4b综合后的结果不同的。 4.2 Parael Case *用编译指令//synopsys parallel_case 给编译工具更多的关于设计的信息,而不能把这些信息提供给仿真工具*。这条编译指令是告诉编译器所有的情况是并行执行的,尽管有重叠的情况(这种情况通常会形成一种优先编码器)。当设计中有重叠的case时,pre-和post-synthesis 的仿真结果将会不同。在某些情况,用这条指令会使设计变得更大和更慢。 添加parallel_case的一个目的是优化面积和速度。*优先编码器行为的RTL模型通过了RTL级测试,但是如果在门级测试时忽略了这个漏洞(它其实是没有优先级的并行逻辑),导致的结果就是,设计是错误的,但是bug直到ASIC原型交货前也没有发现,那么在高度的金钱和时间成本下,ASIC必须重新设计。* · module code5a (y, z, a, b, c, d); · output y, z; · input a, b, c, d; · reg y, z; · always @(a or b or c or d) begin · {y, z} = 2'b0; · casez ({a, b, c, d}) · 4'b11??: z = 1; · 4'b??11: y = 1; · endcase · end · endmodule code5a会被被综合成成具有优先编码器的功能的逻辑。但是code5b却被综合成两个and门。 · module code5b (y, z, a, b, c, d); · output y, z; · input a, b, c, d; · reg y, z; · always @(a or b or c or d) begin · {y, z} = 2'b0; · casez ({a, b, c, d}) // synopsys parallel_case · 4'b11??: z = 1; · 4'b??11: y = 1; · endcase · end · endmodule 综合工具指令//synopsys parallel_case的使用将导致具有优先编码器功能的逻辑实现成并行逻辑,从而导致综合前后的不匹配。 4.3 Casex casex语句的使用也会导致设计问题。casex把'X'视为'don't care'(如果它们出现在case表达式和case条目里)。casex所导致的问题会在casex表达式的输入被初始化成不定值时出现。在处理casex语句时,综合前仿真会把不定值视为'don't care'。综合后的仿真把'X'在门级的模型中传递。 有个公司在他们的设计中使用了casex语句。casex语句的输入在复位后成为不定值,因为综合前RTL仿真把未知的输入视为'don't care',casex语句错误地初始化设计在工作状态,而门级仿真并不够复杂或者说详细以致于没有发现这个错误,从而使递交的ASIC带着严重的bug。 · module code6 (memce0, memce1, cs, en, addr); · output memce0, memce1, cs; · input en; · input addr; · reg memce0, memce1, cs; · always @(addr or en) begin · {memce0, memce1, cs} = 3'b0; · casex ({addr, en}) · 3'b101: memce0 = 1'b1; · 3'b111: memce1 = 1'b1; · 3'b0?1: cs = 1'b1; · endcase · end · endmodule code6是一个带着enable信号的简单的地址解码器,有时外部接口的设计错误会导致enable信号初始化后在被设置成一个有效状态之前成为不定值。当enable处于未知状态时,case选择器基于addr信号错误地匹配case选项。这个初始化的问题只有在post-synthesis仿真时出出现。一个类似的情况就是,当en有效,地址线的MSB位成为不定值时。这将会导致memce0或者memce1有效,而不管片选cs信号是否有效。 *使用原则*:出现不定值信号时很容易的,最好用casez语句而不用casex语句。 4.4 casez casez语句会导致和casex相同的设计问题,只不过它在验证时更容易被发现。用casez当输入被初始化成高阻态时就会出现问题。但是casez是设计很多结构的一种简洁和有效的方式,如优先编码器,中断处理器,和地址解码器。因此当设计工程师再用HDL设计某些结构时应该考虑用casez语句。 · module code7 (memce0, memce1, cs, en, addr); · output memce0, memce1, cs; · input en; · input addr; · reg memce0, memce1, cs; · always @(addr or en) begin · {memce0, memce1, cs} = 3'b0; · casez ({addr, en}) · 3'b101: memce0 = 1'b1; · 3'b111: memce1 = 1'b1; · 3'b0?1: cs = 1'b1; · endcase · end · endmodule code7是与code6相同的带enable信号的简单地址解码器,只不过它用casez替换了casex。当某个输入成为高阻态而不是有效值时,它会与code6产生相同的问题。同样,错误的case匹配会发生(取决于case语句输入的状态)。然而,casez语句的匹配(浮置输入(floating input)或者三态驱动信号)比cazex语句匹配(信号成为不定值)可能性要小得多。*但是casez语句在设计地址解码器和优先编码器的时候是很有用的* *使用原则*:在RTL级谨慎使用casez语句。 5.0 初始化 5.1 'X'初始化 当在RTL代码中复制时,有时会被赋给'X'值。'X'被Verilog仿真器解释为不定值(casex例外),但是会被综合工具当做'don't care'。赋值成'X'会导致综合前后仿真的不一致。但是赋值成'X'值也是一种有用的技巧。在FSM设计中,存在未用到的状态时,把状态变量赋值成'X'可以帮助调试状态转换。这是在进入case状态之前把状态寄存器赋值成'X'值(它是为所有的不正确的状态而准备)。切记,综合工具将把任何的'X'值解释为'don't care'。 · module code8a (y, a, b, c, s); · output y; · input a, b, c; · input s; · reg y; · always @(a or b or c or s) begin · y = 1'bx; · case (s) · 2'b00: y = a; · 2'b01: y = b; · 2'b10: y = c; · endcase · end · endmodule · module code8b (y, a, b, c, s); · output y; · input a, b, c; · input s; · reg y; · always @(a or b or c or s) · case (s) · 2'b00: y = a; · 2'b01: y = b; · 2'b10, 2'b11: y = c; · endcase · endmodule code8a和code8b是3选1选择器的简单的Verilog模型。如果一旦case的2'b11情况发生,code8a的编码风格会造成仿真的不一致。而在code8b中则不会发生这种情况。而这种不匹配在2'b11情况永远不会发生时是有用的,因为一旦2'b11情况发生,y的仿真输出就会是不定值。 5.2 用translate_off/translate_on初始化设计 在translate_off和translate_on,对于综合工具是不可见的,而仿真工具可以。这样就会造成综合前后仿真的不一致,有可能在综合前仿真中初始化是正确的,但是在初始化之后,结果就不正确了。 · module code9 (y1, go, clk, nrst); · output y1; · input go, clk, nrst; · reg y1; · parameter IDLE = 1'd0, BUSY = 1'd1; · reg state, next; · // Hiding the initialization of variables from the · // synthesis tool is a very dangerous practice!! · // synopsys translate_off · initial y1 = 1'b1; · // synopsys translate_on · always @(posedge clk or negedge nrst) · if (!nrst) state = IDLE; · else state = next; · always @(state or go) begin · next = 1'bx; · y1 = 1'b0; · case (state) · IDLE: if (go) next = BUSY; · BUSY: begin if (!go) next = IDLE; y1 = 1'b1; end · endcase · end · endmodule translate_off/translate_on的初始化部分,会导致综合前后仿真结果的不一致。 6.0 translate_off/translate_on的一般用法 translate_off/translate_on综合指令应该谨慎使用。它们在显示设计的信息时是非常有用的,但是当它被用来模拟某种功能时是很危险的。一个例外是异步复位和置位的D触发器。他要求用非综合的模型提供正确的综合前仿真模型以和综合后模型匹配。这种条件按以下方式创建:置reset有效,置set有效,置reset无效,使set继续有效。在这种情况,D触发器模型需要一点辅助来在综合前阶段正确建模set条件。这是由于在always块里只有在set/reset的边沿时才会触发。当输入是异步的,一旦reset信号被去除,set就会有效,但是这种情况不会发生,因为always块不能被触发。为了解决这个问题,可以用translate_off/translate_on指令强制输出正确的值。*最好的方法就是尽可能避免异步set/reset触发器的使用。 · // Generally good DFF with asynchronous set and reset · module code10a (q, d, clk, rstn, setn); · output q; · input d, clk, rstn, setn; · reg q; · always @(posedge clk or negedge rstn or negedge setn) · if (!rstn) q = 0; // asynchronous reset · else if (!setn) q = 1; // asynchronous set · else q = d; · endmodule · // synopsys translate_off · // Bad DFF with asynchronous set and reset. This design · // will not compile from Synopsys, and the design will · // not simulate correctly. · module code10b (q, d, clk, rstn, setn); · output q; · input d, clk, rstn, setn; · reg q; · always @(posedge clk or rstn or setn) · if (!rstn) q = 0; // asynchronous reset · else if (!setn) q = 1; // asynchronous set · else q = d; · endmodule · // synopsys translate_on · // Good DFF with asynchronous set and reset and self- · // correcting · // set-reset assignment · module code10c (q, d, clk, rstn, setn); · output q; · input d, clk, rstn, setn; · reg q; · always @(posedge clk or negedge rstn or negedge setn) · if (!rstn) q = 0; // asynchronous reset · else if (!setn) q = 1; // asynchronous set · else q = d; · // synopsys translate_off · always @(rstn or setn) · if (rstn !setn) force q = 1; · else release q; · // synopsys translate_on · endmodule code10a在仿真时%99的情况是正确的,但是当rstn的上升沿来到时,如果setn一直是低电平,always块是不被触发的,这导致了仿真的不正确。而在code10b中不管是综合前后的仿真,还是综合本身,都会是错误的。而code10c会100%仿真正确,综合前后的仿真也会匹配,它使用translate_off/translate_on综合指令,强制使输出输出正确的值。(这确实是很有意思)。 7.0 延时 在always块中,如果不在0时间点上有延时,就会错过RTL或者行为级的触发事件。 · module code11 (out1, out2, in); · output out1, out2; · input in; · reg out1, out2; · always @(in) begin · #25 out1 = ~in; · #40 out2 = ~in; · end · endmodule 在code11中,在赋值语句的左端添加了延时,将导致综合前后仿真结果的不一致。首先,一旦敏感列表中的in变量变化时,就会进入always块,之后的65个时间单元内in的变化将不在重新出发always块。第二,25个时间单元的延迟后,in的值将被读取,取反,传送给out1,再过40个时间单元后,in的值将再一次被读取,取反,传送给out2。在延迟的过程中,所有的发生在in上的事件都将被忽略。如果任何输入的变化频率比每65个时间单元快,在code11的输出端不会有更新。综合之后的门级模型将仿真成两个反相器,而综合前的RTL代码将错失很多输入的变化。*在alway块中的赋值语句的左端加入延时,并不能精确进行RTL和行为级的建模*。 8.0 总结 知道那一种编码风格会导致综合前后的仿真结果不一致,理解为什么不一致会发生,并且避免容易出错的编码风格将大大减少RTL设计中的漏洞和修补这些不匹配所花的调试时间。由于ASIC的尺寸正在持续增加,综合后运行100%覆盖率的回归测试越来越不可能。设计者必须能够在设计过程中的用所有可能的方法尽早减少风险。 在项目进行过程中,根据自己的经验,总结各大公司的相关文档,创建了自己的编码守则,内容不多,也不教条,适合自己用,大家也可以看看。先来说说这几项守则的目标,任何东西都是在一定的目标和评价体系下,才有其真实的意义。 目标 - 方便施加综合约束 - 很好的设计可读性 - 可重用性 - 可测试行 - 有助简化静态时序分析 - 门级电路行为与RTL代码一致 大家注意比较这几点。 文件风格 1. 一个文件内只能有一个模块 不知道为什么有些人要把多个模块放在一个文件里面,这样可以省很多资源吗? 多个模块在一个文件里面造成的结果是,工具既加载了我们需要的模块,也同时加载我们不需要的模块。如果这不是你愿意看到的,那么就不要把多个模块放在一个文件里。 2. 文件名与模块名一致 做工程的人要深刻理解“简单”的美学和哲学。越简单、越土,做出来的东西越稳定。文件名和模块名的一致,就是出于这样的哲学,当两者一致的时候,从代码阅读、理解到EDA工具加载,都具有非常好的直观性。 3. 文件名与模块名大写 这是一个推荐的原则,你完全可以采取小写。但是这样做的好处是,模块名在代码中非常显眼,容易识别。显眼的目的是提高代码的可读性,这样重点突出,方便阅读理解。 4. 撰写文件头 这是每个RTL作者都希望读到,但是自己又懒得去做的部分。你想想是不是?所谓己所不欲,勿施于人。我们都讨厌没有头部说明的文件,那么自己写的东西,也尽量把这些信息补充完整吧。说到头部文件,我个人认为,不要像Xilinx Core Gen之类产生的头文件那么详细,把模块的简介和作者、版本等信息用简单明了的方式补充上即可。 端口风格 1. 端口信号用大写 理由同文件名和模块名大写,也是推荐而已。 2. 输入输出信号用 i_ 和 o_ 前缀明示 读代码的时候,了解信号是内部的还是接口的,及其输入输出方向很有必要。这样对于把握结构和数据流很有帮助。不理解的朋友通过实践,慢慢体会吧。 时钟风格 1. 时钟名称采用频率作为后缀 比如 i_CLK 修改为 i_CLK_40,以表明该时钟工作在40MHz下,这样在仿真或者综合的时候,非常容易比对,可以及时发现时钟与标称值不一致引起的问题。 2. 不要在内部创造门控时钟 3. 只使用时钟上升沿 这个话题我们有专贴讲述,大家可以去看看。主要是违反了DFT设计原则,所以需要慎重。 4. 不能将data作为时钟使用 将普通信号作为时钟使用,这也是不允许的,尽量把这种实现修改成用该信号做使能信号的方式。之所以要避免这种用法,也是因为违反了DFT原则。因为用于test的扫描链,都使用同一个时钟树,这样才能实现测试数据的移位输入与输出,但是采用了普通信号作为时钟的寄存器不在这个时钟树内,因此也无法进入扫描链,造成部分电路不可测试。 5. 不能将时钟用作普通信号参与运算 以下是我个人理解,不一定正确。(这种用法对生成时钟数和生成扫描链造成重大挑战。时钟信号相当于产生了非常大的延迟,而且由于逻辑部分的渗入,使得末端的时钟行为非常复杂。所以要么就是时钟树在此断裂,要么就产生非常复杂的派生时钟,以上种种都破坏了时钟的简单性和纯粹性。) 复位风格 1. 不要在模块内创建门控复位并使用之。 2. 异步复位只使用下降沿 3. 不要将data作为异步复位使用 4. 不要将异步复位信号作为数据使用 5. 明明白白使用异步复位和同步复位 6. 用后缀指示复位是高有效还是低有效 例化风格 1. 例化子模块时,端口映射一定要采用显式名称映射方式,比如 .clk (i_clk), 2. 端口映射时,信号名称尽量相同或者类似 3. 用于端口连接的信号用前缀 w_ 表示 这里,我想推荐一个命名规则,是我一直在用的,但是觉得非常好,大家可以参考一下,但是每个人都可以不一样,有自己的原则就行。 i_ :输入 o_:输出 r_ :内部寄存器 c_:内部组合逻辑 w_:模块间连线 逻辑风格 1. 输入信号都要FF锁存后使用 2. 输出信号都要FF锁存后输出 3. 严禁组合环(组合环的存在可以通过综合警告来确认) 4. case语句必须有default 5. if 语句必须清楚优先顺序 6. 少用if-else,多用case 7. 不要使用latch 8. 硬件实现尽量简单,比如乘以2的运算,可以用左移1位来代替 9. 不要使用不可综合的语句 10. 明明白白使用阻塞和非阻塞语句 存储器风格 1. 时钟用上升沿 2. CEN和WEN用低有效 3. Dual port memory 和 2-port memory 不要同时访问同一地址 4. memory的write mode 必须使用2。也就是read after write,其他的模式,比如read before write,有可能不能实现。 5. memory的命名采用下面规则 类型_depth x width 类型包括 dual port, two port, single port, rom。 FPGA风格 1. FPGA使用的内容要单独规划,方便ASIC的移植。这个方法很多,大家选自己擅长的