目录

Sequence、Sequencer、Driver大局观

Sequence和item

item与sequence的关系

flat sequence

hierarchical sequence

sequence与driver的关系

事务传输实例

实例

flat_seq

事务传输过程分析


对于IC验证的新晋工程师进入到一个公司,大概率不会让你上来就写验证环境,以及验证环境的修改。但是会让你一上来就写一些sequence,以及一些test。因此,sequence这一部分是很重要的。

Sequence、Sequencer、Driver大局观

整个序列组件之间的数据传输可以如下描述:

① sequence 对象会产生目标数量的sequence item对象。借助于SV的随机化和sequence item对随机化的支持,使得产生的每个sequence item对象中的数据内容都不相同。产生的sequence item会经过sequencer再流向driver。

② driver陆续得到每一个sequence item,经过数据解析,将数据按照与DUT的物理接口协议写入到接口上,对DUT形成有效激励。

③ 如果需要,driver在解析完一个sequence item后,它可以将最后的状态信息歇写回sequence item对象再返回给sequencer,最终抵达sequence对象一侧。这样就可以让sequence知道driver和DUT互动的状态(如果有需要的话)。


sequence item是driver与DUT每一次互动的最小粒度内容。例如DUT如果是一个slave端,driver扮演一个master去访问DUT的寄存器,那么sequence item需要定义的数据信息至少包括访问地址、命令码、数据和状态值,这样的信息在driver取得后,会通过时序方式在interface一侧发起激励送至DUT。

用户除了可以在声明sequence item时添加必要的成员变量,也可以添加对这些成员变量进行操作的成员方法。这些添加了的成员变量,需要充分考虑在通过sequence传递到driver前是否需要随机化。

对于一个sequence而言,它会产生多个sequence item,也可以产生多个sequence。从产生层次上来看,sequence item是最小粒度,它可以由sequence生成,而相关sequence也可以进一步组织继而实现层次化,最终由更上层的sequence进行调度。

sequence与driver之间起到桥梁作用的是sequence。由于sequence和driver均是component组件,它们之间的通信也是通过TLM端口实现的。TLM端口是实现组件和组件之间的通信,driver和sequencer之间的TLM通信参数是sequence item类。由于这一限制,使得sequencer到driver的传输数据类型不能改变,同时与sequencer连接的sequence创建的sequence item类型也应该为指定类型。 也就是说,sequencer从sequence拿到的item的类型,和sequencer发送给drver,以及driver接收到的数据类型,必须是严格一致的。

driver不应该轻易修改item中的值,它会把item中的数据按照与DUT的物理协议时序关系驱动到接口上面。

uvm_component_item和uvm_sequence都是基于uvm_object,它们不同于uvm_component只应当在build阶段作为UVM环境进行创建和配置,而是可以在任何阶段创建

由于无法判定环境在run阶段什么时间点会创建sequence和将其产生的sequence item 挂载(attach)到sequencer上面,所以无法通过UVM环境结构或者phase机制来识别sequence的运行阶段。也正是因为uvm_object是独立于build阶段之外的,所以用户可以有选择地动态地在合适时间点挂载所需要的sequence和item

uvm_sequence和uvm_sequence_item不是组件,所以无法通过config_db按照层次关系对其进行配置。因此要用一个trick:sequence一旦活动起来,它必须挂载到一个sequencer上(发送item),也就是sequence能够获取sequencer的句柄,通过句柄来访问sequencer中的成员变量或等信息,那么这样sequence可以依赖于sequencer的结构关系,间接通过sequencer来获取顶层的配置和更多信息。

明确划分模块职责的话,sequence应该只负责生成item的内容, 而不应该控制item的时序,而驱动激励时序的任务应当由driver完成。

sequencer之所以作为一个组件,设立在sequence和driver之间,主要有两个原因:

① sequencer作为一个组件,它可以通过TLM端口与driver传送driver对象。

② sequencer在面向多个并行sequence时,它有充分的仲裁机制来合理分配和传送item,继而实现并行item数据传送至driver的测试场景。

数据传送机制:(数据传输,get还是put)

数据传送采用的是get模式不是put模式。如果是put模式,那么应该是sequence将数据put至driver,而如果是get模式,那么应该是driver从sequencer获取item。

选择get模式的原因:

① 如果是get模式,当item从sequence产生,穿过sequencer到达driver时,我们就可以结束该传输。如果是put模式,则必须是sequencer将item传送至driver,同时必须收到返回值才可以发起下一次传输,从效率上看,两者具有差别。

② 如果需要让sequencer具有仲裁特性,可以使得多个sequence同时挂载到sequencer上面,那么get模式更符合设计。这是因为driver作为initiator,一旦发出get请求,它会先通过sequencer,然后获得仲裁后的item


Sequence和item

sequence指的是uvm_sequence类,而item指的是uvm_sequence_item类,简称为sequence和item。item是基于uvm_object类,这表明了它具备UVM核心基类所必要的数据操作方法,例如copy()、clone()、compare()、record()等。

item通常应该具备一下数据成员:

① 控制类:总线协议上的读写类型、数据长度、传送模式等。

② 负载类:一般指数据总线上的数据包。

③ 配置类:用来控制driver的驱动行为,例如命令driver的发送间隔或者有无错误插入。

④ 调试类:用来标记一些额外信息方便调试,例如对象的实例序号,创建时间等。

item使用注意事项:

※ 如果数据域属于需要用来做驱动,那么用户应考虑定义为rand类型,同时按照驱动协议给出合适的constraint。

※ 由于item本身的数据属性,为了充分利用UVM域声明的特性,建议将必要的数据成员都通过`uvm_field_automation机制来声明,以便后续uvm_object基本数据方法的自动实现。

※ UVM要求item的创建和随机化都应该发生在sequence的body()任务中,而不是在sequencer和driver中。

※ 按照item的周期来说,它应该始于sequence的body()方法,而后经过随机化,穿越sequencer到达driver,直到被driver吸收,到此就结束了。如果要对item进行修改数据,不应当直接进行修改,这会无形的增加item的寿命,正确做法是利用copy或者clone函数来复制一份再做处理。

item与sequence的关系

一个sequence可以包含一些有序组织起来的item实例,考虑到item在创建后需要被随机化,sequence在声明时也需要预留一些可供外部随机化的变量。

sequence可以被区分为常见的三类:

扁平类(flat sequence):这一类往往只用来组织更小的粒度,即item实例构成的组织。

    层次类(hierarchical sequence):这一类往往是由更高层的sequence用来组织底层的sequence,进而让这些sequence或者按照顺序方式,或者按照并行方式,挂载到同一个sequencer上。

虚拟类(virtual sequence):这个类是最重要的,它是最终控制整个测试场景的方式,由于整个环境中往往存在不用种类的sequencer和其对应的sequence,我们需要一个虚拟的sequence来协调顶层的测试场景。之所以称这个方式为virtual sequence,是因为该序列本身并不会固定挂载于某一种sequencer类型上,而是将其内部不同类型的sequence最终挂载到不同的目标sequencer上面。

flat sequence

一般对于flat sequence而言,它包含的信息有:

※ sequence item以及相关的constraint用来关联生成的item之间的关系,从而完善出一个flat sequence的时序形态。

※ 除了限制sequence item的内容,各个item之间的时序信息也需要由flat sequence给定,例如何时生成下一个item并且发送至driver。

※ 对于需要与driver握手的情况(读写操作),或者等待monitor事件从而做出反应。都需要相应具体事件,从而创建对应的item并且发送出去。

class flat_seq extends uvm_sequence;rand int length;  //随机变量rand int addr;rand int data[];rand bit write;rand int delay;  constraint cstr{  //随机变量constraintdata.size() == length;foreach(data[i]) soft data[i] == i;soft addr == 'h100;soft write == 1;delay inside {[1:5]};};`uvm_object_utils(flat_seq) //factory注册...                         //省略的new函数task body();bus_trans tmp;foreach(data[i]) begintmp = new();tmp.randomize() with {data == local::data[i];addr == local::addr + i<<2;write == local::write;delay == local::delay;};tmp.print();endendtask
endclassclass bus_trans extends uvm_sequence_item;rand bit write;rand int data;rand int addr;rand int delay;static int id_num;`uvm_object_utils_begin(bus_trans)`uvm_field_int...`uvm_object_utils_end...
endclass

在uvm_sequence中写的task body,当sequence挂载到sequencer之后,body任务会自动运行,就像组件里面的run一样。

上面的代码,是在flat_sequence中,不断去new一个bus_trans,然后给他赋值data、addr、write和delay,事实上这样做的话,flat_sequence不仅要考虑数据包的长度和地址,还要考虑数据包的内容,要做的事情太多了。

我们可以将一段完整发生在数据传输中的、更长的数据都“收编”在一个bus_trans类中(item 中),提高这个item粒度的抽象层次,一旦有了更成熟的、更合适切割的item,上层的flat sequence在使用过程中也更加方便。

hierarchical sequence

Hierarchical sequence区别于flat sequence的地方在于,它可以使用其他sequence,还有item,这么做是为了创建更丰富的激励场景。

通过层次嵌套关系,可以让hierarchical sequence使用其他hierarchical sequence、flat sequence和sequence item,如果底层的sequence和item粒度合适,那么就可以充分复用他们,来实现更为丰富的激励场景。

class hier_seq extends uvm_sequence;`uvm_object_utils(hier_seq)function new(string name = "hier_seq");super.new(name);endfunctiontask body();bus_trans t1,t2;flat_seq s1,s2;`uvm_do_with(t1,{length == 2;})fork`uvm_do_with(s1, {length == 5;})`uvm_do_with(s2, {length == 8;})join`uvm_do_with(t2, {length == 3;})endtask
endclass

这里用了uvm_do_with宏,这个宏定义出来,主要做了三件事:① 创建sequence或者item;② 对里面的成员变量进行随机化; ③ 传送数据到sequencer上。

sequence与driver的关系

为了便于item传输,UVM专门定义了匹配的TLM端口供sequencer和driver使用:

※ uvm_seq_item_pull_port #(type REQ=int, type RSP = REQ)

※ uvm_seq_item_pull_export #(type REQ=int, type RSP = REQ)

※ uvm_seq_item_pull_imp #(type REQ=int, type RSP = REQ, type imp=int)

由于driver是请求发起端,所以在driver一侧例化了两种端口:

※ uvm_seq_item_pull_port #(REP, RSP ) seq_item_port

※ uvm_analysis_port #(RSP) rsp_port   // 专门广播response的,一对多端口

而sequencer一侧则为请求的响应端,在sequencer一侧例化了对应的两种端口:

※ uvm_seq_item_pull_imp #(REQ, RSP, this_type) seq_item_export

※ uvm_analysis_export #(RSP) rsp_export

对于第三种sequence部分的端口需要详解一下,虽然sequence_item_export叫做export,但是它是被定义为imp,也就是TLM传输的终点。而sequence作为TLM通信数据传输的终点,为什么要给uvm_analysis端口定义为export呢?因为在我们的理解中,export是TLM通信数据传输的中间节点而不是终点。这是因为uvm_analysis端口内置了一个存储RSP的FIFO而FIFO上是有imp端口的,因此uvm_analysis端口的export接到内置的FIFO的imp端口上就形成了终点。这也是为什么我们成sequencer就是TLM通信传输的终点的原因。

显然,从上面我们可以看到,sequence和driver的各自的两个端口其实是和对方的端口成对的。uvm_seq_item_pull_port #(REP, RSP ) seq_item_port 和 uvm_seq_item_pull_imp #(REQ, RSP, this_type) seq_item_export成对;uvm_analysis_port #(RSP) rsp_port 和 uvm_analysis_export #(RSP) rsp_export成对。因此需要在connect_phase中将端口进行连接。

通常情况下,只需要连接 driver::seq_item_portsequence::seq_item_export 就行了。即在connect_phase中通过:driver::seq_item_port.connect(sequencer::seq_item_export) 完成。这一对端口功能主要用来实现driver与sequence的request获取和response返回。

seq_item_port可以调用很多方法:

※ task get_next_item(output REQ req_arg): 采取blocking的方式等待从sequenne获取下一个item。

※ task try_next_item(output REQ reg_arg): 采取nonblocking的方式从sequence获取item,如果立即返回的结果req_arg为null,则表示sequence还没有准备好。

  ※ function void item_done(input RSP rsp_arg=null): 用来通知sequence当前的sequence item已经消化完毕,可以选择性的传递RSP参数,返回状态值。

※ task wait_for_sequence(): 等待当前的sequence直到产生下一个有效的item。此任务往往和try_next_item一起使用。

  ※ function bit has_do_available(): 如果当前的sequence准备好而且可以获取下一个有效的item,则返回1,否则返回0。

※ function void put_response(input RSP rsp_arg): 采取nonblocking方式发送response,如果成功则返回1,否则返回0。

平时用的比较多的任务和方法,就是上面加粗的任务和方法。

driver消化完当前的request后,可以通过item_done(input RSP rsp_arg=null)方法来告知sequence 此次传输已经结束,参数中的RSP可以选择填入,返回相应的状态值。

事务传输实例

实例


//bus transaction item definition
class bus_trans extends uvm_sequence_item;rand int data;`uvm_object_utils_begin(bus_trans)`uvm_field_int(data, UVM_ALL_ON)`uvm_object_utils_end...
endclassclass flat_seq extends uvm_sequence;`uvm_object_utils(flat_seq)...task body();//创建了一个sequence item tmpuvm_sequence_item tmp;bus_trans req,rsp;//调用函数 create_item, 接下来生成的item, 挂载到m_sequence上tmp = create_item(bus_trans::get_type(), m_sequencer,"req");//create_item返回的是一个父类的句柄,需要通过cast转换成子类void'($cast(req, tmp));//若在创建的时候没有上面声明item挂载的sequence,则在start_item阶段,默认挂载至sequence所在的sequencer下start_item(req);//倘若没有做子类父类的转化,那么不能访问子类对象且不说,在父类进行randomize的时候,由于randomize是虚方法,父类调用该方法同时也会对子类的成员变量进行随机化。req.randomize with {data == 10;};`uvm_info("SEQ", $sformat("sent a item \n %s", req.sprint()), UVM_LOW)finish_item(req);//ret_response方法原型中,get的变量是父类句柄,所以为了能访问子类方法sprint(),下面还需要做父类到子类的转换get_response(tmp); //driver消化完item,发送一个item_done的response,若item_done(rsp)有句柄送到sequence的fifo中就可以通过get_response来获取句柄,否则不能获取(会被get_response blocking住)。void'($cast(rsp, tmp));`uvm_info("SEQ", $sformat("got a item \n %s", rsp.sprint()), UVM_LOW)endtask
endclassclass sequencer extends uvm_sequence;`uvm_component_utils(sequencer)...
endclass
class driver extends uvm_driver;`uvm_component_utils(driver)...task run_phase(uvm_phase phase);REQ tmp;bus_trans req,rsp;seq_item_port.get_next_item(tmp);//拿到父类的句柄,需要转化成子类的对象void'($cast(req, tmp));`uvm_info("DRV", $sformatf("got a item \n %s", req.sprint()), UVM_LOW)void'(cast(rsp, req.clone()));//子类方法的clone,克隆完返回的还是父类的句柄,需要转化成子类rsp.set_sequence_id(req.get_sequence_id());rsp.data += 100;seq_item_port.item_done(rsp);`uvm_info("DRV", $sformatf("sent a item \n %s", rsp.sprint()), UVM_LOW)endtask
endclassclass env extends uvm_env;//在顶层声明sequencer sqr;driver drv;`uvm_component_utils(env)...function void build_phase(uvm_phase phase);//在build_phase创建sqr = sequencer::type_id::create("sqr", this);drv = driver::type_id::create("drv", this);endfunctionfunction void connect_phase(uvm_phase phase);//在connect_phase连接,连接第一对端口,第二对端口可以不连drv.seq_item_port.connect(sqr.seq_item_export);endfunction
endclassclass test1 extends uvm_test;env e;`uvm_component_utils(test1)...function void build_phase(uvm_phase phase);//例化enve = env::type_id::create("e", this); endfunction task run_phase(uvm_phase phase);flat_seq seq;phase.raise_objection(phase);seq = new();  //创建一个sequenceseq.start(e.sqr); //将sequence挂载到env中的sequencer上//sequence挂载到sequencer上之后,就会自动运行其中的body任务了phase.drop_objection(phase);endtask
endclass

在sequence发送item的时候,就会给item记录一个sequence_id,表示该item是由哪个sequence发送的,这样在一个sequencer同时收到两个不同的sequence发送的item,然后发送给driver,driver再返回这两个item的response给sequencer,让sequencer把对应的response送给对应的sequence,就需要利用这个sequence_id。rsp.set_sequence_id(req.get_sequence_id())这个就是通过获取request的id,给response,来保证发送对应request的sequence能得到对应自己的response。

sequence_id不做域的自动化的话,在使用clone函数的时候是不会进行clone的,默认为0。

注意一下,在声明drvier和sequence时,没有给他们下类型的定义,所以默认是sequence_item类型属于父类的句柄,因此在get_next_item之后,需要将父类的句柄转换成子类的句柄,这步可以通过在声明class时,定义类型#(RSP)来改变。但是req.clone()获得的句柄默认是uvm_object类型,因此必须通过父类到子类的转换。

flat_seq

class flat_seq extends uvm_sequence;`uvm_object_utils(flat_seq)...task body();//创建了一个sequence item tmpuvm_sequence_item tmp;bus_trans req,rsp;//调用函数 create_item, 接下来生成的item, 挂载到m_sequence上tmp = create_item(bus_trans::get_type(), m_sequencer,"req");//create_item返回的是一个父类的句柄,需要通过cast转换成子类void'($cast(req, tmp));//若在创建的时候没有上面声明item挂载的sequence,则在start_item阶段,默认挂载至sequence所在的sequencer下start_item(req);//倘若没有做子类父类的转化,那么不能访问子类对象且不说,在父类进行randomize的时候,由于randomize是虚方法,父类调用该方法同时也会对子类的成员变量进行随机化。req.randomize with {data == 10;};`uvm_info("SEQ", $sformat("sent a item \n %s", req.sprint()), UVM_LOW)finish_item(req);//ret_response方法原型中,get的变量是父类句柄,所以为了能访问子类方法sprint(),下面还需要做父类到子类的转换get_response(tmp); //driver消化完item,发送一个item_done的response,若item_done(rsp)有句柄送到sequence的fifo中就可以通过get_response来获取句柄,否则不能获取(会被get_response blocking住)。void'($cast(rsp, tmp));`uvm_info("SEQ", $sformat("got a item \n %s", rsp.sprint()), UVM_LOW)endtask
endclass

flat_seq作为动态创建的数据生成载体,它的主任务flat_seq::body()做了如下的几件事情:

※ 通过方法create_item()创建了request item对象;

※ 调用start_item()准备发送item;

※ 在完成发送item之前对item进行随机处理;

※ 调用finish_item()完成item发送;

※ 有必要的情况下,可以从driver处获得response item。

事务传输过程分析

在定义driver时,它的主任务driver::run_phase()需要做如下处理:

① 通过seq_item_port.get_next_item(REQ)从sequencer获取有效的request item。

② 从request item中获取数据,进而产生数据激励。

③ 对request item进行克隆生成新的对象response item

④ 修改response item中的数据成员,最终通过seq_item_port.item_done(RSP)将response item对象返回给sequence

对于uvm_sequence::get_response(RSP)和uvm_driver::item_done(RSP)这种成对的操作,是可选的,可以选择获取或者不获取,但是要成对出现。

在高层环境中,应该在connect_phase中完成driver到sequencer的TLM端口连接,比如上面代码中env::connect_phase()中通过drv.seq_item_port.connect(sqr.seq_item_export)完成了driver与sequencer之间的连接。

在完成了flat_seq、sequencer、driver、env的定义之后,到了test1层,需要考虑挂起objection防止仿真在run_phase的时候提前退出。

使用uvm_sequence::start(SEQUENCER)来完成sequence到sequencer的挂载操作。当多个sequence试图挂载到同一个sequencer时,需要在sequencer上添加仲裁功能


在sequence创建item之前,首先需要将sequence挂载到sequencer上,上面整个sequence从create_item到最后和driver握手成功的get_response的运作都在sequence的body()中。Driver的get_next_item到item_done都在driver的run_phase()中。

sequencer做仲裁,是在driver发起get_next_item()时,才开始仲裁选择哪个item。如果只有一个sequence挂载到sequencer上,那直接用就行了。在sequencer将通过的权限交给某一个底层的sequence之前,目标sequence中的item应该完成随机化,继而在获取sequencer的通过权限后,执行finish_item()。

finish_item()会等到driver发挥item_done()才会结束。

对每个item而言,它起始于create_item(),继而通过start_item()尝试从sequencer获取可以通过的权限。如果driver没有了item可用,将调用get_next_item()来尝试从sequencer一侧获取item。

为了统一起见,用户可以不在定义sequencer或者driver时指定sequence item类型, 使用默认的REQ = uvm_sequence_item,但是用户需要注意在driver一侧的类型转换,例如对get_next_item(REQ)的返回值REQ句柄做出动态类型转换,等到正确类型之后再进行接下来的操作。

UVM基础-Sequence、Sequencer(一)相关推荐

  1. UVM基础-Sequence、Sequencer(二)

    目录 sequence和sequencer 将sequence挂载到sequencer 将item挂载到sequencer 宏定义使用实例 sequencer仲裁特性 实例 sequencer的锁定机 ...

  2. 【UVM基础】虚序列器与虚序列(virtual sequencer与virtual sequence)快速上手指南

    文章目录 一.virtual sequence与virtual sequencer 二.virtual sequence and sequencer的产生 2.1.嵌入序列器 2.2.嵌入序列,控制序 ...

  3. 【UVM基础】两种启动 sequence 的方式

    显式启动 所谓显式启动:即在sequence中直接调用uvm_do函数,然后在env中调用objection以及start函数来显式启动. 文件:src\ch2\section2.4\2.4.2\my ...

  4. UVM基础-Seq-Sqr-Driver交互详解

    一.Sequence机制的使用方法 1.1 seq.sqr与driver 熟悉UVM的朋友都知道,在一个基于UVM搭建的验证环境中,Sequence负责产生环境所需的数据包:Transaction,而 ...

  5. UVM中的sequencer

    一. p_sequencer 与m_sequencer 的关系 m_sequencer是在UVM 原码中定义的, p_sequencer 需要我们在代码中进行宏定义才能使用,通过如下定义后, 可以直接 ...

  6. UVM基础-TLM通信机制(二)

    目录 TLM 2.0 通信 端口定义 传送数据 时间标记 同步通信元件 uvm_event uvm_event 总结 uvm_barrier uvm_callback TLM 2.0 通信 TLM 2 ...

  7. uvm中sequence和virtual sequence中objection的控制

    sequence中的objection的控制策略 在整颗UVM树中,树的结点很多,理论来说在任何组件中都可以控制objection.一般在sequence和virtual sequence中,也有在s ...

  8. UVM layering sequence for layered protocol

    在OSI(Open System Interconnect )模型中,将网络结构分为7层,自上而下:应用层,表示层,会话层,传输层,网络层,链路层,物理层.每层都有相应的协议和对应的数据结构. IC中 ...

  9. [UVM] kill sequence

    默认条件下,stop_sequence 会递归kill 掉子sequence,前提是已经建立了父子关系. 如何建立sequence的父子关系呢? 1. 使用uvm_do/uvm_do_on启动sequ ...

最新文章

  1. The application does not contain a valid bundle identifier.解决方法
  2. android根据项目把文件编译到文件系统中
  3. 数据加载很慢_Vaex真香!几秒钟就能处理数十亿行数据,比Pandas、Dask更好用
  4. javaweb实现单点登录,防止重复登录,获取sessionid,对session及时销毁回收,只允许一个用户登录,结合struts2实现
  5. GIS实战应用案例100篇(二)-元胞自动机模拟城市扩张过程
  6. 【PyTorch】中view()==>相当于numpy中resize()、reshape()的功能
  7. hibernate中的一对多和多对多的映射关系
  8. 使用Free Spire.Presentation生成PowerPoint文件
  9. linux测试接口配置,linux can 总线socket接口测试使用
  10. CentOS6.2安装LAMP+DRUPAL网站(1)
  11. 关于N82后摄像头拍照无法启动的超强技术解决方案
  12. C# 获取PDF文档的字体信息及指定文字的坐标,宽度和高度
  13. 计算机开机画面怎么有2个用户,win7开机画面有两个用户登录 ,怎么样隐藏其中一个(管理员帐号)...
  14. Windows系统优化软件 | 这10款功能超级强大!界面优美!值得收藏
  15. 【深度学习】9:CNN实现olivettifaces人脸数据库识别
  16. ethtool修改网卡mac地址流程
  17. Android 点击键盘外 非输入框 关闭软键盘
  18. 如何实现H5可视化编辑器的实时预览和真机扫码预览功能
  19. canvas坐标转换屏幕坐标_Canvas坐标系转换
  20. linux下的plc软件下载,基于Linux系统的软PLC的实现

热门文章

  1. CleanMyMac最新版V4.11.4版MAC电脑系统加速器
  2. [Aizu]-0558 Cheese [BFS]
  3. 【课程】03 Richards方程数值解
  4. 正版Mincraft登录问题:微软账号不能登录
  5. 如何使用NodeJS发送邮件
  6. 新注册的邮箱如何群发邮件
  7. 玩转AWS CloudWatch微信告警
  8. c语言移动光标到指定坐标,C语言实现光标移动
  9. java 兔子生兔子
  10. pikachu漏洞搭建平台