【数字设计验证】System Verilog(sv)稍微进阶的笔记(一)

这篇具有很好参考价值的文章主要介绍了【数字设计验证】System Verilog(sv)稍微进阶的笔记(一)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

接触硬件描述语言(HDL)也有几个年头了,由于之后research会偏向Architecture,做偏软件的活,算是走入一个新的阶段,因此想写一篇关于SV的笔记进行总结复习。选择SV的原因在于它目前是业界主流。SV是Verilog的继承扩展版本,类似于Cpp和C的关系,扩展内容可以分为Declaration Enhancement(多了变量类型),和Programming Enhancement(一些写法的shortcut,硬件行为描述的支持,运算符,directive等等),具体参见下图,Coding会在Part 2详细说明。总之sv对design和verification的支持都很显著,本文会挑个人觉得比较有用的点去记,所以这不是一个入门的指导,我会跳过蛮多基础的东西,Bear in mind!!!
【数字设计验证】System Verilog(sv)稍微进阶的笔记(一)

1. EDA工具对代码的处理与输出

在讲Coding前,还是稍微skim一下它的工作步骤,只想看sv的Coding Guide的话直接跳到Part 2,这里不细说底层的运行,因为这与不同EDA工具和FAB的lib有关系 (例如说Synopsy和Cadence的综合程序算法就不大一样),目前还没有能力和兴趣去探究,主要还是说一些已经standardized的东西。我们使用HDL的最终目的在于生成可靠可知的IC layout,我们将layout及其设计步骤抽象为代码,人写完代码后借由一系列Computer Aided Design(CAD)工具(在EE领域我们特指它为EDA)再将代码转变为layout的输出文件。因为我们需要其可靠可知,所以设计流程中的一些中间产物也是很重要的。因此这里将对HDL代码的处理分步为:Compile, Simulation和Synthesis。对应着不同类的EDA工具对应的功能目的和输出结果。

PS: HDL可服务于不同类型的DIC实现:Full Custom, Semi-Custom和Programmable。这更分化了不同的EDA功用,但是大道至简,殊途同归,后文主要还是讲general的部分和一些经验tips方便回忆
【数字设计验证】System Verilog(sv)稍微进阶的笔记(一)

1.1 Compile

第一步是编译,实际上对于这一名词不同工具我们也能看到不同的结果。比如说对于vivado或者dc shell这类需要后续制作netlist等的软件,当我们输入filelist进行编译后除了进行syntax analysis外,还会附带生成对应的RTL Schematic,也就是说我们可以看到电路的hierarchy,甚至有些情况Compile就直接是Synthesis操作本身。而对于仿真器而言,Compile的含义很多只代指对与各个文件的代码分析,当要去执行Simulation的时候才会load各个模块生成Schematic和对应的Hierarchy。

这里仿真软件举的例子为Modelsim(vsim这个软件的组分也是比较多的,包括compiler, linter, simulator, waveform visioner等,支持GUI或者TCL对于各个模块的调用)。它在compile worklib后点击View–Schematic依旧是空的,在Run Simulation过后可以在Sim窗口中看到目标top module 的Hierarchy结构还有schematic。而Dc shell和vivado等可以直接set top module并查看这些东西。Anyway,在这里我还是想将对RTL代码Compile的概念统一描述,就当是Syntax Analyze 和 Linter,进行纠错并整合信息方便后面的程序进行处理。Compile是可以分辨一些directives(主要应用于Syn),挑出基本的组分。此外,关于Tb层面的compile,默认的timescale为1ns/xx(时间单位/仿真精度)。还有package和#include之于compile的区别,会在Part 2提及,Makefile的特性,我们重复compile不需要update没有更改的成员。在编写code的时候利用文本插件(推荐Vim或者VSC等,能装插件就行)的辅助可以很快通过第一次compile(减少很多syntax层面的typo)。

1.2 Simulation

当然在通过1.1的Compile后我们只是独自对各个file进行检查,想要把DUT作为一个整体,还需要各个模块进行完整的interconnection。因此当我们在Modelsim跑某个testbench时,即便Compile全部PASS,也会出现例如组件名字mismatch,端口连接mismatch或不能识别某个名字等的情况。总之,针对DUT部分,我们需要将它作为电路进行编写并进行严格的连接,这些组件都能在Simulation后的hierarchy中单独显示,合理地划分组件和功能能更方便Debug,SV对于Simulation有非常多额外的支持。

这里标记一些用Simulation Tool的小tips。

  • 首先跑Sim后,代码和各组件建立了映射关系,因此我们可以通过看代码添加所需的信号,也可以邮件代码中的一些信号trace driver或reference,这个对于debug一些陌生的代码很有好处,可以快速地了解信号,寄存器之间的关系,反之亦然-go to source。
  • 在写代码的时候,除了一些需要用parameters customize Hardware,尽量添加信号位宽的标注可以方便Debug发现忘记添加变量声明的情况。或者说直接添加以下代码取消掉SV对default type的设置。
`default_nettype none

这样的话如果我们不小心用了未声明的变量就会直接报错,而非默认为logic[0:0],在一些现有的代码中,有人习惯直接用未声明的变量做interconnection,个人认为这不是个好习惯,尽管说这些变量不具备其他的功能,但标注出来对debug而言会更方便。

  • 添加Divider在waveform中可以方便区分不同module的信号,也可以在preference设置代码名称的path深度来简化信号来源的描述。信号的默认名字一般就是main/sub/signal name。我一般习惯设置max path长度为2~3,一般来讲记性越差,工程越大,所需要的path长度更大吧。一次性拉了过多的信号的话,可以通过set property改变wave的颜色使重要成员显著
    【数字设计验证】System Verilog(sv)稍微进阶的笔记(一)

  • 做self check的时候可以多使用 $stop or $fatal(“log”)并fork timeout thread来做debug,step by step的debug是我们所喜欢的方式

  fork
	//Error occurs when pwr_up is never asserted
	begin: timeout
		repeat(30000) @(posedge clk);
		$fatal("Timeout for waiting pwr_up");
	end: timeout1
	//If pwr_up is asserted, disable timeout
	begin
		@(posedge iDUT.iAuth.pwr_up);
		disable timeout;	//Once pwr_up is asserted, disable timeout and keep testing
	end
  join
  • $strobe $display $monitor 在event-driven simulation下发生在哪个queue呢(时刻),参见Stratified Event Queue,它详细描述了Simulation的运行规范,这里就记住strobe和monitor能发生在非阻塞赋值后就行。
  • $stop $finish stop类似于断点,可以continue。finish类似于terminate当前thread,在GUI中它通常会问我们finish后要不要exit。Debug还是stop用的比较多
  • class 中的变量无法生成add到wave,我们需要将它转为interface才可以拉出来看到波形。这不太方便,因此要注意信号尽量例化或连接到外面。
  • 后仿真需要注意精度的问题,会对结果产生出入。而且对Netlist仿真有时候就很玄学,应该和Lib有关系,个人看法还是图个乐呵。除此之外尽量也不要依靠内部信号,很容易出现X value,内部信号名很多也会被优化或者处理掉,尤其是synthesize flatten的情况,DUT变成了一层。如果一定要refer一个内部信号,可以在综合时加入一下代码,或者一些软件可以识别RTL代码中的dont touch,下面一些代码是dc shell一些代码是vivado的,使用的时候参见其TCL文档即可。
# in scripts
set_dont_touch [find pin main/sub/sig_name]
set_property DONT_TOUCH TRUE [~]
set_property MARK_DEBUG TRUE [[get_nets –of [get_pins hier1/hier2/<flop_name>/Q]]]
# or in the RTL codes
(*DONT_TOUCH = "TRUE"*) wire xxx...
(* MARK_DEBUG = "{TRUE|FALSE}" *) logic yyy...

1.3 Synthesis

这里只讲Code->Netlist这一步也就是,后续Netlist->layout的APR部分不在此列,以DC shell为例子。在Compile并进行完成前仿后,我们将RTL代码mapping到对应库的门级网标中,这一步我们实际上无论是Semi-Custom,Full Custom抑或是FPGA,都是必要的,因为从Code到Gate List是比较外层的由抽象到具象的程序,到了具体的implementation再分化。但无论如何,Netlist所依赖的Lib各不相同,对于Full Custom而言,那就不是单纯依赖Fab提供的Lib了,而是需要更多人工的设计。以DC shell为例子的话,Synthesis在以下方面可以额外注意下,基础共通部分我不赘述,随便看一个完整的脚本代码再参照资料就可以理解:

  1. Cost Function = -Slack + Area + power 此为综合trade off的标准,往往不可兼得
  2. Timing Constraints: 就是数电的基础时序约束,Setup和Hold。Tsu+Td+Tco+Tuncertainty < Tclk; Tco + Td-Tuncertainty > Thold。Tuncertainty来源于Skew也就是Clock Tree的不均衡
  3. 一般在DC shell中我们不会touch clk。会在CTS(Clock Tree Syn)中单独去解决它。
  4. Input delay 和 Output delay一般默认是同步的环境,可当作Td的一部分,针对异步的模块我们一般手动去设置,不对它纳入时序分析的整体和优化。
set_multicycle_path 2 -setup -from [find pin Main/Sub/ptch_*_reg*/CLK]
set_multicycle_path 2 -setup -from [find pin Main/Sub/AZ*_reg*/CLK]

set_clock_groups -name async_clk0_clk1 -asynchronous -group {clk0 usrclk itfclk} \
-group {clk1 gtclkrx gtclktx} ...
  1. drive以及Capacitive load决定了门的延时,可以参照那个Ids的方程,信号的传输就是经历一个又一个transition,对应的Slope自然就一定上决定了速度。举个例子,Fanout过大drive弱,因此消耗的delay也就多,我们一般也会加以约束,添加buffer,只不过说这又要增加Area了,正好也对应了1的tradeoff
  2. 多次综合的优势:我们之前说过compile定义的区分,对于综合工具而言,Compile有时可以定义为综合操作本身,这一点可以在对应脚本命令中看出,map effort越高,所用以fix timing的area消耗越大。这里我们统一描述为综合Syn,但是tcl还是照常。多次综合可以把Netlist做Flatten,取缔掉原版设计的hierarchy,尽力去服用Gate本身。也能添加一些额外的约束到Netlist以便于重新组织,例如说之前提到的asynchronous和hold time fix。示例代码如下:
compile -map_effort high 
set_multicycle_path 2 ...
ungroup -all -flatten
set_fix_hold clk
compile -map_effort medium
  1. 还有一个小tip,有些人会告诉你GUI is just children’s work,cmd mode才是硬核。然而我个人的看法是工具不分高低贵贱,好用就行,至少我认为一些GUI的terminal要比直接调用的shell>要好用多了(我说的就是Synopsy!)。而且GUI兼顾脚本的运行,又能够方便调用到一些直观的功能,比如说hierarchy和schematic。所以以偏概全地跟风并不好,自己实践可以得到更优的选择吧,大概!

2. System Verilog Coding Guide

扯了那么多有的没的,终于到正题了,也就是Coding的部分,如前文所说,SV是Verilog的扩展,因此这个PART的内容也会夹杂Verilog本身的东西,只挑选我认为很重要或者比较有意思的内容。开头我先列一点优秀的文字资料,虽说coding是一门实践的技术,但就我的感觉来看,实践过后回归理论,能够很有效的补全知识漏洞。

  • IEEE SV官方指南:IEEE 1800-2017 SV官方链接 有些东西网上搜的不太明白可以查一查。
  • 2013 SNUG会议文章 “Synthesizing systemverilog busting the myth that systemverilog is only for verification”,关于System Verilog的使用,例子很丰富,讲解很详细:PDF link
  • 2000 SNUG会议文章 “Nonblocking Assignments in Verilog Synthesis, Coding Styles That Kill!”,Verilog的Coding Guideline:PDF link

2.1 状态机【Design】

结构上来看,数电状态机就是时序逻辑+组合逻辑形成的环路,最简单的状态机例子就是计数器,counter即是它的状态,一般来讲寄存器保有current state,Comb的输出决定next state。Moore SM和Mealy SM的区别就在于状态机输出的构成,Moore的输出只由current state决定,比如说计数器的counter,告之当前时刻。而Mealy的输出还由外部输入决定,比如说给计数器输出添加一个mask,这样就可以选定时间的scale,从输出性质来讲,Moore为同步,Mealy可能为异步,如果外部输入的是同步信号,那其实输出的也就同步了。
【数字设计验证】System Verilog(sv)稍微进阶的笔记(一)

功能上来看,我认为状态机就是时序逻辑的一个拓展的实例。在数电中,我们用时钟和寄存器来进行timing control,规范在不同时间(CLK)电路进行不同的配置和操作。重点在于规范,这就是我们除开FF和latch以外禁止做comb环路的原因,在组合环路中我们很难准确预测什么时刻电路会做什么,因此引入时钟信号和时序单元来规范一切。而状态机也是如此,这个环路使我们能够设计电路什么时刻会干什么,下一时刻又会发生什么,所以我说他是时序功能的延续,是电路时序控制的一个抽象,抽象为Bubble Diagram能更让人理解功能。Bubble Diagram的规范写法如下图所示。
【数字设计验证】System Verilog(sv)稍微进阶的笔记(一)

Coding来看,我要说的无非就是两段/三段式,Enum type,Latch规避以及Case语法变种。

  1. 二/三段式:只是把输出信号放在Case段里还是外的区别。我个人还是喜欢在case里加中间信号,然后最后assign输出信号的,感觉结构比较清晰一点。第一段就是时序部分current state <= next_state; 下面给一个2-3段的简单代码,虽简单但也有点长,会在之后的叙述中再次提到。
  ///
  // SM 2nd part: decide next state(maybe combine some outputs)//
  ///
  always_comb begin
      next_state = state;
      clr_tmr = 1'b0;   //default to avoild latch
      case (state)
        INIT: begin
            if(sum_gt_min) begin    //ensure that rider is absolutely on (higher than threshold + hysteresis), go to waiting balance
                next_state = WAITING;
                clr_tmr = 1'b1;
            end
        end
        WAITING: begin
            clr_tmr = 1'b0;
            if (sum_lt_min) begin  //rider totally step off, go back to INIT
                next_state = INIT;
            end else if(diff_gt_1_4) begin           //balance wrong, clear the counter, waiting for balance
                clr_tmr = 1'b1;
            end else if (tmr_full) begin    //1.3s for balance is up, we can enable steer
                next_state = NORMAL;
            end
        end

        NORMAL: begin
            if(~sum_gt_min) begin   //CAUSE: first check rider may get knock off the device - high priority
                next_state = INIT;
            end else if(diff_gt_15_16) begin //rider is steppingg off go to waiting for balance(clr counter)
                next_state = WAITING;
                clr_tmr = 1'b1;
            end
        end

        default: begin
            next_state = INIT;
        end
      endcase
  end

  ///
  // SM 3rd part: assign some outputs by sm&input             //
  ///
  assign rider_off = (sum_lt_min) ? 1 : ((state == INIT) & (~sum_gt_min)) ? 1 : 0;
  assign en_steer = (state == NORMAL) ? 1 : 0;
  1. Enum Type: 在2-3段的代码我们看到next_state = INIT看似奇怪的语句,实际上就是引入了eunm变量,enum变量方便在simulation中Debug,但是readability一般,特别是写在package里的时候,enum的值默认为0-n,可以手动赋值。在waveform中enum为显示为其名称而非具体的binary value
  typedef enum reg[1:0] {INIT, WAITING, NORMAL} state_t; //0,1,2
  typedef enum reg[1:0] {INIT = 1, WAITING = 2, NORMAL = 0} state_t; //1,2,0
  x = state_t'(num) 			//type transfer
  state_t state, next_state;
  1. 规避latch:在2-3段的代码中我们看到在comb段开头写了next_state = state; clr_tmr = 1’b0; 这个的目的就是让case中涉及的变量能够有个default value,防止有些situation这些变量没有cover到,编译器默认把这部分当作latch了,从行为上来看就是所有case中的output我们都会先给个值,根据阻塞赋值的定义,这个值会在后面的语句覆盖掉(如果有的话),值得一提的是always模块内的输出变量得是reg,但实际上在硬件中它可以是个wire(比如这里的case块),因此SV引入了logic变量unify了这两者。总之规避latch是很重要的,它可能会引发一些synthesis工具的报错。而默认next = current 也给了next 同current rst的情况。这套思路实际在if else语句中也适用,我们填充default情况的判定来规避自动综合出latch。
  2. case语法的变种casex(),casez(),case() inside,unique case,priority case。前三者,虽然花里胡哨又是xz inside,实际上就是引入了wildcards,也就是dont care。case inside 只能在case item(xx: begin end)里添加z,x或?作为wildcards,若是输入expression中有xz,不认为是wild card。而对于casez 而言,exp中的z设为wildcards而casex则在casez基础上将exp中的zx都设为wildcards。wild card在decoder中还是挺好用的,尤其是那种可能会发生x z信号的情况,一个wild card能让他们避免进入default,case inside还扩展了range作为case item的选择。但对于综合而言,结果就和sim有出入了,实际上这三者xz inside综合出的结果都是一样的,casex 和 casez对于exp的wildcard无法在synthesis后的电路适用,因为在实际电路中我们无法判别输入是否有x或z,因此我们不难看出,之所以在SV引入case inside,其中一个原因就是统一Sim到Syn的功能规范,casex和casez因exp可能产生的sim&syn不一致理应舍去。因此,个人推荐做DUT的话那就只能用case inside,若是testbench代码则爱用啥用啥。priority和unique也直接影响Synthesis,顾名思义,priority所映射的电路就是有优先级的,如果满足第一个就不会去管第二个了。而unique指的是the one and the only one,他的结构会呈现出Flat mux的形态(flat会更节省,因为不需要顺次传递,Td也小),若是实际与这种directive不符合,syn的时候会报warning,但对于Sim来讲,他们皆相同,都是有priority的顺序比较。总之减少didnt cover的情况,并确保code和RTL电路的mapping一致性是很重要的,SV给我们提供了很多帮助
    下面是一个case的代码例子:
always_comb begin
	casex(case_exp)
		3'b0??: $display("one");
		3'b10?: $display("two");
		3'b111: $display("three");
		default:$display("four");
	endcase
end
always_comb begin
	case(case_exp) inside
		3'b0??: $display("one");
		3'b10?: $display("two");
		3'b111: $display("three");
		default:$display("four");
	endcase
end
//distinguish these two different results

当case_exp为000,11x和11z时,x输出的为133,inside输出的结果为144,如是casez则结果为143。从这可以看出Sim中这三者的差异,实际就是exp判定xz的差异,但Post Sim的话他们就全部一样了。

2.2 Behavorial Verilog再到Always模块【Design】

我们将Verilog代码区分为BehavioralStructuralStructural代码就是模块元件之间的连接的形式,比如说Netlist就是纯纯的structural,是比较底层的电路抽象,除此之外的都是Behavioral代码,我们描述具体行为来生成电路,是一种更高的抽象方法,其中always模块便是Behavioral的最大组成部分,我们用它来表示时序和综合电路,包括latch。此外的话多为辅助,比如说用assign表示组合逻辑,是种方便的alternative。下面说一些always模块编写的guideline。

  1. always_ff,always_comb和always_latch:之前我们提到code和RTL电路的mapping一致性,这里SV的拓展也是为它而生,我们用不同的always_来map想要的电路类型,倘若我们的代码行为与硬件mapping不一致,它同理也会报warning,非综合型的代码也无法在这个block仿真,但无法说通过这个directive来决定想要综合出的元件类型,它能做到的也只是辅助。

  2. posedge和negedge:他们并不止是1-0 0-1,在tb中适用更多。
    【数字设计验证】System Verilog(sv)稍微进阶的笔记(一)

  3. 不同变量的行为,我们尽可能将对应always分离,这样可以规避不需要的situation判定,从而减少潜在bug和资源消耗。下面举一个最简单的复位打拍电路为例,对应他们的电路图,我们可以看到Synthesis的差异

// circuit 1 mixed
always_ff@(posedge clk or negedge rst_n) begin
	if(~rst_n)
		q1 <= 1'b0;
	else begin
		q1 <= D;
		q2 <= q1;
	end
end
// circuit 2 seperate
always_ff@(posedge clk or negedge rst_n) begin
	if(~rst_n)
		q1 <= 1'b0;
	else begin
		q1 <= D;
	end
end

always_ff@(posedge clk) begin
		q2 <= q1;
end

【数字设计验证】System Verilog(sv)稍微进阶的笔记(一)

可以看到在复位的情况我们没有定义q2的行为,所以综合工具会给他自动保持q2<=q2,因此除非我们知道要做这种回路,否则的话还是分离或者是定义其对应的行为。

  1. 异步信号输入的亚稳态处理:Meta-Stability的产生来源于异步信号输入到FF,就拿上面circuit2 代码的下面一个FF举例,若输入q1是个相对于clk的异步信号,那我们在采样的过程中可能得到的Z值。原因的话从粗略角度讲,这个异步信号可能不满足setup或hold time,因此在输入的跳变中寄存器采到了一个中间态,即为Z高阻态。细致点讲的话,一般来讲我们的FF由两个锁存器组成,我们会发生亚稳态的原因就是当第一个latch采到Z的时候恰好时钟跳变,第一个latch的值传递到了第二个上,因此我们最后得到了Z(这是setup不满足的情况,hold也类似)。所以说,为了规避采到Z,会多放置一个FF来进行采样(像是异步复位的同步释放),这样的话即便是第一个FF传递出了Z,经由1个clk的调整,它也基本上就会恢复状态,由第二个FF得到正常的同步信号。关于亚稳态为什么维持时间比较短的原因,笼统地说,这个寄存器器件的工艺和环境等有关,在寄存器中维持高阻态这个平衡并不容易,一点串扰和毛刺都可能打破这个平衡。具体的说,如图所示,锁存器有一强一弱两个反相器头尾相连组成,倘若此时亚稳态发生了,也就是第一个latch头尾保持Z值,已知inverter一强一弱,那么所谓的平衡的电压值也就有了偏斜(上下电流是有不同的,若inverter的strength来源于transistor宽长比),再加上器件电流自身的扰动,那么很快,电压就会恢复到一高一低的电平,也就是亚稳态的恢复,传递到下一级的latch上。因此只要恢复时间小于一个CLK,那么加一级同步器就能有效的抵消掉亚稳态阻止它传播。
    【数字设计验证】System Verilog(sv)稍微进阶的笔记(一)

2.3 便捷写法【Design】

在原有的印象中,RTL代码相对于高级编程语言是非常冗余且传统的,但实际上随着版本的更新迭代,SV如今也是提供了丰富的便捷的代码编写用法。SV支持的Oop涵盖很广且通用就不在这个子Part提及了。

  • vector和generate:当需要重复例化模块或者重复一些行为语句时,可以使用向量或者generate语句来实现。若是比较规范的需求,则可以直接用vector来做,若是一些需要不同situation控制的地方,则需要用generate语句。
wire[7:0] A, B, S;
wire[6:0] Carrybits;
fulladder adders[0:7] (A, B, {Carrybits,1'b0}, S)
//如果要用half adder + full adder的形式,generate会更加方便,加个判断index的情况即可
  • 变量拼接(Concatenation):拼接我们经常用,就是利用{},高位到低位,下面就列举它的一些handy usecases
// add signed and unsigned with ov
wire signed[15:0] addend1;
wire [7:0] addend2;
wire [15:0] sum;
wire ov;
assign {ov, addend2} = addend1 + {8'h00, addend2};

// sign extension
assign {ov, addend2} = addend1 + {{8{addend2[7]}}, addend2[6:0]};

// signed shift
assign signedDiv8 = {{3{value[15]}},value[15:3]};
assign signedDiv8 = value <<< 3;		// keep value as signed
assign rotated = {value[0],value[15:1]};
// shift registers are quite prevalent and we can reuse the bits in interesting ways

// struct assignment
struct {
logic [ 7:0] opcode;
logic [31:0] data;
logic status;
} operation;
operation = ’{8’h55, 1024, 1’b0};		// entire assignment
operation.data = 32'd1024;				// seperate assignment

// vector assignment
logic [7:0] a1 [0:1][0:3];
logic [7:0] a2 [2][4];							// C like declaration
a1 = ’{’{7,3,0,5},’{default:’1}};			// first range as the most external {}
a2 = a1;
  • Instantiation:例化模块时,我们需要添加对应模块的IO和parameters,为了方便有这几种方法:
    使用插件直接生成对应例化代码,如VSC Open Command Palette --> system verilog auto instantiate…看具体不同插件的使用方法了,在vsc一般就是在ctrl+shift+p(command palette shortcut)后调用即可。推荐将parameter写在module内部,因为有时候插件没那么聪明哈哈,不过这种生成的脚本挺好写的,自己弄个也不麻烦就是了,还能把变量declaration加上去了。
    【数字设计验证】System Verilog(sv)稍微进阶的笔记(一)
//	另一种方法就是用sv支持的语法
dff dff1 (.*, .q(q[0]), .d(d[0]), .qb());
// 等同于
dff dff1 (.clk, .rst, .pre, .d(d[0]), .q(q[0]));
// 这种方法就是自动与同名变量进行连接,但是私以为

2.4 变量运算【Both DV】

SV提供了丰富的Arithmetic Operator,除了介绍基本的符号和规则外,我同样也会写一些handy usecases

  1. 4-value logic:包含了XZ后,AND OR等波尔运算会稍微复杂一点点,X就是Unknown,如01冲突会给X。Z是高阻态,悬浮信号,易被外来的下拉或者上拉信号改变,因此总线中很常用。
    【数字设计验证】System Verilog(sv)稍微进阶的笔记(一)

  2. Strength Value:这个一般也就在TB中会用,实际的driving force肯定和器件相关,我们可以给primitive添加这些directives来给定输出拉高拉低的strength。
    【数字设计验证】System Verilog(sv)稍微进阶的笔记(一)

not (weak0,weak1) #(1;2;3, 3:2:1, ...) inv2 (Q,n1)	// 1 ns min, 2ns typical, 3ns max delay;  rise, fall, to Z delay
  1. 符号数: SV默认使用的是2s补码,我们可以用快捷方式表达负数如-16‘h3A,也可以直接用对应的补码形式16’FFC6,通过内置函数$signed()来讲值当作符号数来处理,在乘法中运用更加必要一些。
  2. 运算符Arithmetic Operators:就列举一些可能不太熟的运算符
    表1:运算符补充
Operator Name Operator Description
exponent ** 指数运算符 Range of unsigned = 2 ** width-1 或 $pow(2,width)-1
modulus % 求余运算符
Shift <<< >>> >>> 符号右移only works of operand and dest declared as signed. 等同于加了个sign extension
Equality === !== === and !== compare x’s and z’s explicitly not as don’t care. Use in testbench
Bitwise & | ^ ~ Applies bit by bit, ~ is unary ^ is xor,按位与或…就是把dest所有bit用operator连接
set membership inside() if (data inside {[0:255}) … if (data inside {3’b1?1}) … if (data inside array) …
streaming << >> bitstream operator, 会将目标变量当作比特流来处理,>>正序 <<倒序 a = { << byte { b }}; reverse by byte
Wild equality ==? !=? deem xz as wildcards if (data =?= 8’b1xxx_z1xz) begin
assignment *= += similar to cpp like a+=1 ---- a = a + 1

看着比较高级的运算符(斜体)通常就只在TB或parameter中用,也不是禁止在DUT用,但和design规范相关。运算符优先级和其他语言比较类似,不在这赘述,但是我个人的习惯就是加括号,增加可读性,明确优先度。

  1. handy cases:这里就结合代码给一些slick的运算操作,顺带解释一些3中的运算符
// Reduction Operator
assign all_one = &accumulator;    		// are all bits set? all 1 leads to res 1
assign not_zero = |accumulator;			// are any bits set? any 1 leads to res 1
assign zero_exist = ~&accumulator;		// are any bits rst? any 0 leads to res 1
assign all_zero = ~|accumulator;		// are all bits rst? all 0 leads to res 1
assign parity = ^data_out;				// is number of 1 odd? odd 1s leads to res 1

// saturation 16bit signed to 12bit signed
assign sat12 = (~sum[15] & |sum[14:11]) ? 12’h7FF :		//positive sat
(sum[15] & ~&sum[14:11]) ? 12’h800 :					//negative sat
sum[11:0];												//normal

// decide
if (data inside {4'b1??1})
if (data =?= 4'b1zx1)				// same condition effect match with format

//PACK and UNPACK
data = {<< 8{data}}; 				// reverse data by byte
int a, b, c;
{>>{ a, b, c }} = 96'b1; 			// left to right a, b, c = 0,0,1 fit 96bits
{>>{ a, b, c }} = 97'b1;			//left to right a, b, c = 0, 0, 0 fit the MSB 96bits(0)
bit   [99:0] d;
d = {>> { a, b, c }};				//d become all zero ,if d narrower than 96, compile error

【数字设计验证】System Verilog(sv)稍微进阶的笔记(一)

2.5 Verilog Stratified Event Queue【Verification】

这部分来源于SV官方文档 IEEE Standard,定义了软件的Simulation Spec,已知我们的simulation是event driven的,这个Queue就定义了各类event与它们的判定优先级。当我们编写SV时,或多或少会遇到race的情况,因此了解这个优先级和组成有益于我们编写和解读代码,避免看到仿真结果一脸懵的情况。为了详细说明,这里再列举了需要的Simulation Terminology

  • Processes:我们可以在仿真软件的Sim窗口中查看到这些元素。
    Objects that can be evaluated,包括modules tasks functions primitives,tb的inital和always blocks还有procedural assignments
  • Update Event
    LHS assignment,对应变量值的变化更新,可以是net可以是reg
  • Evaluation Event
    RHS computation,计算右边的式子。
  • Scheduling an event
    将Event(上述)放入Event Queue
  • Simulation Time
    仿真用的时间系,不是说我们现实的时间
  • Simulation Cycle
    Complete processing all currently active events。一次simulation time 可以有多个cycle
  • Explicit zero delay #0
    将当前sim time后面event强行放入inactive状态

列举完了Terminologies,下面我们解释这个stratified event queue,event分五个Region,随机执行active region中的events,当一次simulation cycle结束后(当前simulation time的所有active events finish),会依次将其他Region的events加入到active中(例如跳转到下一时刻,将future events加入active并执行,自然是最后去做的,也就是Region 5)。下面我们对各个region的events进行描述。

  • Region 1 Active Events:这个其实就是当前时刻要率先执行的任务,包括内置函数$display,RHS的计算,阻塞赋值,continuous assignment(也就是assign语句),primitive的输入到输出等,Region内部的events执行并不存在固定顺序,但在同一个block中时,阻塞赋值存在顺序,这个我认为也可以延伸到continuous assignment上。
  • Region 2 Inactive Events:#0 的阻塞赋值会放在这里。
  • Region 3 Nonblocking Events:阻塞赋值中更新LHS。
  • Region 4 Monitor Events:$monitor $strobe。
  • Region 5 Future Events:接下时刻的events。

事实上,我们比较在意initial和always模块中的的先后顺序,我一般将组合逻辑作为对应顺序的非阻塞赋值,有单向依赖关系的情况是好排序的,但是遇到两个block又双向依赖的情况,race condition一般就不好判定了。而对于那种trigger events,它在Active的位次处于同一block blocking assignment的后列,当他去monitor trigger的时候内部block的events事实上已经执行完成了,因此阻塞赋值不能够出现自己self-trigger的情况,但非阻塞可以,因为它update LHS是在Active后

initial 
	clk = 0;
	
always@(clk)
```#5 clk = ~clk;	//不成功!只有在clk完成blocking assignment后,always模块才会开启trigger。

always@(clk)
	#5 clk <= ~clk;		//成功!trigger先在Active中开启,在后才会把Non blocking中的赋值放入Active,此时trigger events可以检测到非阻塞赋值的变化

2.6 Fork methods【Verification】

我们使用fork方法来实现TB中的parallel thread。除了方法本身,这里还会介绍一些配合named blocks的usecases

  • fork join:该block下的子线程需要全部完成后,fork join才会结束并返回主线程。
  • fork join_any:任何一个子线程完成后,主线程继续往下跑,但这并不会结束其他的子进程的运行,它们依旧会在后台继续运行。
  • fork join_none:相较于join_any,join_none会直接让主线程继续跑无论是否有子进程结束,同样的子进程会在后台继续运行。
  • wait fork:针对join_any和join_none两种情况,我们可以在主进程中添加wait fork语句,方便在后面等待子进程的完成加以控制。
    下面的代码案例就使用了wait fork 组合 fork join_any。当完成Thread1时它会继续执行join下的语句,并在wait fork时等待其他thread完成,通常我们会给一个timeout的限制,一般报timeout错就$stop。
initial begin
	// Main Thread
	$display("Proc Starts at %0t ns!", $time());
	fork
		//Thread 1
		$display("Thread 1 hits at %0t ns!", $time());
		//Thread 2
		begin: break
			while(1) begin :  continue
				@(posedge clk);
				cnt += 1;
				if(cnt >= 1000) begin
					cnt = 0
					disable continue;	// == continue
				end
				if(rdy == 1'b1 && cnt == 1) begin
					disable timeout_check;	// == break
					disable break;
				end
			end : continue
			//disable timeout_check
		end: break
		//Thread 3
		begin: timeout_check
			repeat(50000) @(posedge clk);
			$display("time out error at %0t", $time);
			disable break;
		end: timeout_check
	join_any
	$display("Wait Fork threads");
	wait fork;
	$display("fork join is over at %0t", $time())
end

2.7 Assertion【Verification】

在testbench中我们经常会进行self-check,而Assertion则是SV支持的一种简化的self-check写法。具体来讲它就是Assert一个condition,这是申明Assertion的系统下所需要遵循的一个条件状态,可以是一个即时或者长期的固有属性。因此有Immediate Assertion和Concurrent Assertion的区分,当判定的条件是永久性触发(如posedge clk)时,我们就用Concurrent Assertion。当Assertion只需判定一次(形同if语句)时,我们就用Immediate Assertion。从可读性和规范上Assertion是有增色的,它统一了self-check的格式。值得详述的还是Concurrent Assertion,我们可以用Keyword “property”来声明concurrent的condition。如果我们没有去指定触发check property的tick,则默认会用simulation的sys tick进行周期性check,这过于频繁。因此我们通常会使用clk来作为property的checking tick。此外若是比较复杂的property,我们可以用sequence组合property blocks来进行层次化, sequence->property->directive。下面就放一些例子,其中也涉及到一些不太常用的operator。

  1. Immediate Assertion
[assertion name] : assert (res == expected) $pass_statement
else	$fail_statement
// equal to
if (res == expected) $pass_statement
else $fail_statement

assert(class.func1());
//默认不加fail success statement的话,assert函数会判定这个返回值,若为0则爆error
  1. Simple Concurrent Assertion
assert property (@(posedge clk) !(rd & wrt))
else $fatal("WRT and RD should not assert at same time")
// property的判定实际上是先于trigger的那个时刻的,因此若有同步信号进来,则只会在下一拍check到

// when req is asserted ack should ///
// be asserted 1 to 2 clocks later ///
assert property (@(posedge clk) req |-> ##[1:2] ack);

operator |-> 是一个implication operator,代指了信号assert的先后关系,这个是LHS先RHS后。
operator ## 表示前后时间间隔的operator,可以是constant或者range,无|->直接使用的话就是前后的范围了。

  1. Multiple level Assertion
//Sequence layer
sequence : reg_gnt_seq
	(~reg & gnt) |-> ##[1:4] (~reg & ~gnt);
endsequence

sequence : rdy_ack_seq
	rdy ##[1:3] ack;
endsequence

//Property layer
property : reg_prop
	@(posedge clk)
	disable iff(~rst_n) set[->3] |-> ##1 rdy_ack_seq.ended ##1 reg_gnt_seq.ended;
endproperty

assert property(reg_prop)
else $fatal("ERROR!");

这里property调用了一个变量和两个sequence。其中set需要拉高3个clk而后第二时间触发rdy_ack_seq,第三时间出发reg_gnt_seq,ended是将他们同步为结束后执行下一步。相对复杂的时序组合通过不同seq制作property是很好的self check编写方法。我们也会通过disable iff语句来对self check进行关停与否。

  1. Event
    self check中我们有时想要手动控制步骤的执行,一种方法是直接定义一个变量如logic flag,通过其他thread对他赋值来控制check。这里介绍event变量,作用类似,我们可以通过监视他的triggered成员,这个成员是一个状态量,也就是说不会因为race condition而不被触发。而倘若我们用类似@()方法,我们在Event Queue中已知trigger方法是在后触发的,因此race condition下不能检测到pulse。wait_order 顾名思义,它会等待event的顺序触发。
event a, b, c;
initial begin
	#20;
	->a;
	#10 ->b;
	#10 ->c;
end

initial
	#20 wait(a.triggered);		//it works triggered is like a state

initial
	#20 @(a);					//it cannot be triggered out of race “a” is like a pulse in that way

initial
	wait_order(a,b,c);

2.8 import package & `include【Both DV】

`include可以理解为直接将文件的文本copy到对应位置,它的好处是合并编译,include的内容可以依赖目标模块的成员,比如说要refer内部信号,include是可以做到的,坏处就在于冗余不方便多文件共享了,他例化的都是各自独立的存在,public variable不好实现。而package是单独编译的,当模块import package的时候,实际上它是会look up这个文件,所有import的都依赖一个package,我们也能指定import的内容,者方便了多文件共享和通信,缺点就是不能反过来依赖模块文件,总体来讲就是1对1和1对多的区别。package是SV特有的extension,搭配面对对象特性,可以做到方便快捷的多文件泛用。

2.9 Random Testing【Verification】

这里主要讲CRV (Constrained Random Verification)。它会调用一些SV的功能对生成的随机变量加以约束以生成我们想要的数据分布,从而实施更有针对性的测试。

  • rand/randc bit[7:0] rand_byte; 这个type有一个built-in method randomize(),可以自己生成随机数。randc的特点是他生成的随机数会耗尽后再出现重复,也就是说相邻的同样随机值之间一定包含该变化域的所有可能值,像这里就有255个。
  • 利用constraint{} directive,我们可以限制rand bits的随机生成,constraint块的分层是{}不同于通常的begin end。
rand bit [31:0] len, src, dst; 
rand bit congestion_test;

constraint c_stim{
   len<3000;
   len>20;
   if(congestion_test){
   	dst inside {[32,128]};
   	src = 233;
   } else {
   	src inside {0, [2:10], [100:107] }
   }
}
//根据congestion test 生成不同限制的随机值

rand bit[7:0] low, med, high;

constraint relate{
   low < med;
   high > med;
}
//constraint可以是相对大小限制
  • control the distribution,用dist directive来控制随机数的分布。通过class的构造我们也能生成随机数,成员也可以通过randomize()变成对应的随机数。
class stim_t
	rand bit[15:0] RF;
	constraint{
		RF dist {[16'h0 : 16'd99]:=99, [16'd99 : 16'hffff]:=1 }; 		// 99:1 distribution
	}
endclass

initial begin
	stim_t stim = new();
	for (int i = 0; i < 10; i++) begin
		stim.randomize();
	end
end
// we can give assignment to feed the value to DUT, otherwise using virtual interface is also advisable. 

2.10 Fuction & Task【Verification】

这里标注了function和task的一些区别,有时候function还是会用在design里的(作为comb logic)。
【数字设计验证】System Verilog(sv)稍微进阶的笔记(一)

这里再额外说一下task/func automatic,它的主要作用有两个,一个是迭代函数,另一个是引入ref输入(类似指针,但只可读),ref输入的作用就是可以输入一些含时的信号,以作monitor。

//Define the factorial function
function automatic integer factorial;
input[31:0] oper;
begin
	if (oper>=2)
		factorial = factorial(oper-1)*oper; 
	else
		factorial = 1;
end
//这里可以看到func的返回可以直接用它的函数名来赋值

task automatic uart_tx_case(input[7:0] tx_in, ref clk);
	@(negedge clk);
	vif.send_cmd = 1;
	vif.cmd = tx_in;
	@(negedge clk);
	vif.send_cmd = 0;		// 1clk trmt, to capture and set txdata;
	wait(vif.cmd_sent == 1'b1); 	// serial data done, otherwise uart_tx is not sent
	$display("tx data, %h, is sent",tx_in);
	wait_bauds(12);   
endtask

2.11 Coding for Synthesis【Design】

虽然说根据script,EDA工具会自动去优化生成对应的Netlist和Layout,但还是需要了解一些与代码相关的部分,和设计的trade off息息相关。

  1. Synthesis of Z and X. 我们可以用高阻态来实现三态门,比如说I2C总线中的开漏结构。而X则代表了dont care,他不会实际出现在电路中,但在表示电路时可以节省area。默认的话它会做成环路,在sequential最多就增加MUX的输入,但在COMB中的话就是latch了。因此手动赋X的话就会直接摈弃掉一些不需要的逻辑判定和资源。总体来讲,在comb中,Area Latch>default 0>default x。
  2. Datapath Chain:这里先说没有conditional control的datapath,比如说Res = abc*d。实际上区别就在是unique还是priority的结构,后者会比前者的delay更大,但是因为logic effort的区别使得priority的Area稍微少一点。当然如若是shrinking后的自然是资源最少的,加入时序模块。
  3. Conditional Datapath。加入控制逻辑后就有不同的说法了,比如说Res = (sel) ? ab : cd;它可以是双multiplier或者单的结构。显然我们从资源角度考虑是希望放两个MUX作为单个multiplier的输入的。但是,具体情况需要具体分析,我们不清楚这里Sel的来源,若是一个同步信号可能带有比较长的input delay,那么出于Critical Path的考虑就要权衡这个conditional control放在哪里了,他可以放在两个Multiplier的后面的MUX选通,这样的话timing问题就会减缓。因此需要根据具体情况去trade off,需要去分析是否有late arriving signal。
  4. 在底层的话EDA工具会应用不同的算法和对应的综合策略来进行优化,这里就不过多地赘述了。

3. Extension

众所周知,Digital Design & Verification的工作流程比较长,不同EDA工具不同的Programming再到Layout等。因此这里再额外塞一些比较有意义的思路或方法,提高设计流程的质量和速度。

3.1 Automatic buildup/Test Script Cases

工业化设计下,使用脚本将作业自动化使我们所希望的,因此这里就列举一些工具下自动化脚本的案例。Digital部分我用的比较多的还是Shell或者Tcl,这两者比较方便与Software和OS进行交互。而若是数据处理等更high level的需求,Python C#等会更加好用。不同脚本语言也是可以混合运行的。通常GUI会把使用的CMD interface打印在terminal里,是我们脚本仿照使用的一个比较好的学习窗口。

// ModelSim sim.tcl
proc compile_files {args} {
      #for .do script 's filelist
      vlog /filespace/demo/Segway/testbench/define.svh
      ...
}

set fp [open "/filespace/demo/Segway/testbench/define.svh" w]
puts $fp "`define TP1"
close $fp

vlib  work
vmap work work
compile_files

vsim -t 1ns work.Segway_tb_seq -voptargs=+acc

add wave -position insertpoint sim:/Segway_tb_seq/Seg_intf/*
add wave -position insertpoint sim:/Segway_tb_seq/iPHYS/theta_*
add wave -position insertpoint sim:/Segway_tb_seq/iDUT/ptch
...

run -all
dataset save sim wave1.wlf
write format wave wave1.do

还有vivado cadence和synopsis工具的case。等之后找到了再更新。

3.2 Code Coverage

根据变量值的变化情况(如SM覆盖率),仿真工具可以生成Code Coverage的报告来评估仿真是否全面,我们再根据报告反推测试代码没有cover的模块,优化测试质量和覆盖率,这是Code Coverage的作用。可以通过GUI或者define对应的directive来使能这个功能。
不同工具虽然界面不同,但基本都遵从Compile Configuration – Enable Code Coverage 和Sim Config 同理,两者都需要去设置。
得到的结果粗糙来看如下所示:

=================================================================================
=== File: /filespace/Segway/rtl_src/A2D_Intf.sv
=================================================================================
    Enabled Coverage              Bins      Hits    Misses  Coverage
    ----------------              ----      ----    ------  --------
    Toggles                        180       130        50    72.22%

具体的话还能在代码界面中查看各个变量,branch等是否cover到。

3.3 TBD

感觉具体的案例还是单独开blog写好点,太臃肿了可读性还是差了点,吃了经验的亏呀<_<文章来源地址https://www.toymoban.com/news/detail-469618.html

到了这里,关于【数字设计验证】System Verilog(sv)稍微进阶的笔记(一)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【数字IC精品文章收录】近500篇文章|学习路线|基础知识|接口|总线|脚本语言|芯片求职|安全|EDA|工具|低功耗设计|Verilog|低功耗|STA|设计|验证|FPGA|架构|AMBA|书籍|

    1.1 索引目的 本篇索引旨在 收藏CSDN全站中有关数字IC领域高价值文章 ,在数字芯片领域中,就算将架构,设计,验证,DFT,后端诸多岗位加在一起的数量,都不及软件类一个细分方向的岗位数量多,反映在社区氛围或是开源资料的丰富度而言,数字IC领域相较于软件/互联网领

    2024年02月03日
    浏览(120)
  • sv,verilog

    在 Verilog 中,敏感列表(Sensitive List)指的是在 always 语句块中使用时钟信号的列表。敏感列表用于指定 always 块所监控的输入信号,在这些信号状态发生变化时, always 块会被触发执行。 在 Verilog 中, always 块有两种敏感列表: 电平敏感列表(Level-Sensitive List):较早的 Ver

    2024年02月04日
    浏览(38)
  • 【数字IC验证进阶】SoC系统验证和IP模块验证的区别及侧重点分析

    芯片验证其实有很多种,现在分工也很细了,包括ip验证和Soc验证等等,像现在还不是很热门的dft,formal,都是。偏的不讲,就说ip和soc。 很多小公司做的都是soc,因为简单,有钱买IP就行。去了就是soc验证 ,个别小公司,会自己开发一些自己的ip, 挂到soc中去 ,也会涉及到

    2024年02月07日
    浏览(38)
  • SV芯片验证之验证环境

    声明:本内容来自于学习路科验证发布在B站上的免费视频课程后的笔记 它是整个验证系统的总称,包括: 1、验证结构中的各个组件、组件之间的连接关系、测试平台的配置和控制; 2、编译仿真的流程、结果分析报告和覆盖率检查; 我们主要关注验证平台的结构和组件部分

    2024年02月16日
    浏览(41)
  • Verilog学习笔记(4):仿真验证与Testbench编写

    仿真,也叫模拟,是通过使用EDA仿真工具,通过输入测试信号,比对输出信号(波形、文本或者VCD文件)和期望值,来确认是否得到与期望所一致的正确的设计结果,验证设计的正确性。 验证是一个证明设计思路如何实现,保证设计在功能上正确的一个过程。 验证在Verilog设

    2024年02月01日
    浏览(48)
  • FPGA(Verilog)时钟无缝切换设计与验证

    时钟切换基本模型,本文围绕“ 基本组合电路切换、解决前毛刺切换、解决后毛刺切换 ”三方面完成时钟无缝切换。 组合逻辑切换,本质就是二选一多路器 如下图,CLK_SEL 0与1分别控制时钟CLK_A CLK_B输出。 组合逻辑输出只跟当前输入状态有关,CLK_SEL异步不可控导致输出毛刺

    2023年04月10日
    浏览(49)
  • SV芯片验证之数据类型(一)

    四值(四态)逻辑: 可以表示0(低电平)、1(高电平)、X(未知值、不定态)和Z(高阻值)四种值。有 logic、reg、integer、time、net-type(wire、tri) 。 注: 真实电路中并无X,它只是用来表示不确定的状态,即不确定它到底是0还是1,但它要么是0要么就是1,不存在所谓的“中间态”。 高阻态

    2024年02月09日
    浏览(37)
  • Verilog设计数字时钟

    目录 一、设计要求 二、模块总和 三、模块设计 1.顶层模块 2.秒分频模块 3.秒计数模块 4.分钟分频模块 5.分钟计数模块 6.小时分频模块 7.小时计数模块 8.数据分配数码管模块 9.数码管显示模块 10.管脚约束代码 四、引脚分配 1.利用 NEXYS4 DDR 开发板设计一款数字时钟,能够正确

    2024年02月06日
    浏览(42)
  • 数字集成电路设计(四、Verilog HDL数字逻辑设计方法)(二)

    所有的是时序逻辑电路都可以拆成组合逻辑电路+存储 (关于组合逻辑电路的理解可以参考我数电的博客https://blog.csdn.net/y_u_yu_yu_/article/details/127592466) 可以分成两个部分,组合逻辑电路和存储电路。组合逻辑电路的输入一个是x信号一个是当前的状态,这两个信号决定了组合

    2024年02月06日
    浏览(55)
  • FPGA自动洗衣机的设计与验证(Verilog编写)

    目的及要求         1.洗衣机的工作步骤为洗衣、漂洗和脱水三个过程,工作时间分别为:洗 衣45秒,漂洗30 秒,脱水15 秒;         2.用一个按键实现洗衣程序的手动选择:A、单洗涤;B、单漂洗;C、单脱水;D、漂洗和脱水;E、洗涤、漂洗和脱水全过程;         

    2024年02月21日
    浏览(49)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包