【IC验证】一文速通多通道数据整型器(MCDF)

news2024/11/16 23:31:26

目录

01 README

02 MCDF设计结构

2.1 功能描述

2.2 设计结构

2.3 接口与时序

2.3.1 系统信号接口

2.3.2 通道从端接口

2.3.3 整形器接口

2.3.4 控制寄存器接口

2.3.4.1 接口时序图

2.3.4.2 各数据位信息

03 验证框图

3.1 reg_pkg

3.1.1 reg_trans

3.1.2 reg_driver

3.1.3 reg_generator

3.1.4 reg_monitor

3.1.5 reg_agent

3.2 fmt_pkg

3.2.1 fmt_trans

3.2.2 fmt_driver

3.2.3 fmt_generator

3.2.4 fmt_monitor

3.2.5 fmt_agent

3.3 mcdf_pkg

3.3.1 mcdf_refmod

3.3.2 mcdf_checker

3.3.3 mcdf_env

3.3.4 mcdf_base_test

3.3.5 mcdf_reg_write_read_test

3.3.6 mcdf_reg_illegal_access_tets

3.3.7 mcdf_arbiter_priority_test

3.3.8 mcdf_channel_disbale_test

3.3.9 mcdf_coverage

04 覆盖率收集


01 README

        最最开始系统地学习芯片领域相关只是大概追溯到23年12月初,此前只是有一些Verilog和数电基础,做过两个小的设计项目。整个12月和1月几乎都在学习FPGA的相关课程,仍记得在跨年夜在宿舍敲UART串口通信那篇文章@UART串口通信_uart串口通讯-CSDN博客,但同时也感受到在国外没有开发板的情况下确实很难把这些东西学好。因此年后转而开始学习更偏向于软件的数字IC验证。期间也是断断续续,国内学校的事情太多了,因此总是不能全身心投入,加上自己软件基础并不好,C++,python这些语言学的都不好。大概花了3个月的时间基本学完了SV的语法,验证的基本流程和熟悉验证软件的使用,但也不敢说能够熟练掌握。目前算是进入了下一个时间节点,开始学习UVM验证环境。因此基于MCDF项目对此前学习进行一个总结。大佬们轻喷。

02 MCDF设计结构

2.1 功能描述

        设计模块的全称叫做multi-channel data formatter,他可以将上行多个数据通道的数据通过内部的SLAVE, FIFO给到仲裁器Arbiter, Arbiter选择从不同的FIFO中读取数据,给到下端的Formatter, 对数据进行整形,以数据包的形式送给下行的数据接收端。整个设计通过Register接受外部命令,对MCDF的功能进行修改。

2.2 设计结构

2.3 接口与时序

2.3.1 系统信号接口

clk(0)  :时钟信号
rstn(0):复位信号

2.3.2 通道从端接口

CHx_DATA(31:0):通道数据输入。

CHx_VALID(0):通道数据有效标志信号,高位有效。

CHx_READY(0):通道数据接收信号,高位表示接收成功

当valid为高时,表示要写入数据。如果该时钟周期ready为高,则表示已经将数据写入;如果该时钟周期ready为低,则需要等到ready为高的时钟周期才可以将数据写入。

2.3.3 整形器接口

FMT_CHID(1:0):整形数据包的通道ID号。
FMT_LENGTH(4:0):整形数据包长度信号。
FMT_REQ(0):整形数据包发送请求。
FMT_GRANT(0):整形数据包被允许发送的接受标示。
FMT_DATA(31:0):数据输出端口。
FMT_START(0):数据包起始标示。
FMT_END(0):数据包结束标示。

  • 整形器的作用是对数据进行打包,以数据包的形式向下行数据接收端传递数据,可以选择的数据包长度为4,8,16,32,这是由FMT_LENGTH信号来进行控制。
  • 在发送数据包时,首先应该将FMT_REQ置为高,同时等待接收端的FMT_GRANT拉高,此时表示接收端可以进行数据接受。当FMT_GRANT变为高时,应该在下一个周期将FMT_REQ置为低。FMT_START也必须在接收到FMT_GRANT高有效的下一个时钟被置为高,且需要维持一个时钟周期。在FMT_START被置为高有效的同一个周期,数据也开始传送,数据之间不允许有空闲周期,即应该连续发送数据,直到发送完最后一个数据时,FMT_END也应当被置为高并保持一个时钟周期。在数据包传输过程中FMT_CHID和FMT_LENGTH应该保持不变,直到数据包发送完毕。
  • 相邻的数据包之间应该至少有一个时钟周期的空闲,即FMT_END从高位被拉低以后,至少需要经过一个时钟周期,FMT_REQ才可以被再次置为高。

2.3.4 控制寄存器接口

CMD(1:0):寄存器读写命令。

CMD_ADDR(7:0):寄存器地址。

CMD_DATA_IN(31:0):寄存器写入数据。

CMD_DATA_OUT(31:0):寄存器读出数据。

2.3.4.1 接口时序图

当cmd为写指令时,需要把数据cmd_data_in写入到cmd_addr对应的寄存器中;当cmd为读指令时,即需要从cmd_addr对应的寄存器中读取数据,并在下一个周期,将数据驱动至cmd_data_out接口。

2.3.4.2 各数据位信息
  • 地址0x00 通道控制寄存器 32bits 读写寄存器

bit(0):通道使能信号。1为打开,0位关闭。复位值为1。

bit(2:1):优先级。0为最高,3为最低。复位值为3。
bit(5:3):数据包长度,解码对应表为, 0对应长度4,1对应长度8,2对应长度16,3对应长度32,其它数值(4-7)均暂时对应长度32。复位值为0。
bit(31:6):保留位,无法写入。复位值为0。

  • 地址0x10 通道状态寄存器 32bits 只读寄存器

bit(7:0):上行数据从端FIFO的可写余量,同FIFO的数据余量保持同步变化。复位值为FIFO的深度数。
bit(31:8):保留位,复位值为0。

03 验证框图

一共分为chnl_pkg, reg_pkg, fmt_pkg和mcdf_pkg, 各个包之间通过interface进行连接

3.1 reg_pkg

3.1.1 reg_trans

寄存器传输类,包含地址、命令、数据和响应字段

class reg_trans;
		rand bit[7:0]  addr;
		rand bit[1:0]  cmd ;
		rand bit[31:0] data;
		bit rsp;
	
		constraint cstr{
			soft cmd  inside{'WRITE, 'READ, 'IDLE};
			soft addr inside{'SLV0_RW_ADDR, 'SLV1_RW_ADDR, 'SLV2_RW_ADDR, 'SLV0_R_ADDR, 'SLV1_R_ADDR, 'SLV2_R_ADDR};
			addr[7:4] ==0 && cmd == 'WRITE -> soft data [31:6] == 0;//读写寄存器要求高26位为0
			soft addr [7:5] == 0;
			addr[4] == 1 -> soft cmd == 'READ;//地址为0x10,0x14,0x18的是通道1,2,3的只读存储器
		};
		
		function reg_trans clone() ;
			reg_trans c = new()    ;
			c.addr      = this.addr;
			c.cmd       = this.cmd ;
			c.data      = this.data;
			c.rsp       = this.rsp ;
			return c               ;
		endfunction
		
		function string sprint();
			string s ;
			s = {s, $sformatf("========================n")};
			s = {s, $sformatf("reg_trans object content is as below:\n")};
			s = {s, $sformatf("addr =  %2x:\n", this.addr)};//两位16进制数
			s = {s, $sformatf("cmd  =  %2b:\n", this.cmd )};//2位2进制数
			s = {s, $sformatf("data =  %8x:\n", this.data)};//8位16进制数
			s = {s, $sformatf("rsp  =  %0d:\n", this.rsp )};//10进制数,无字符宽度
			s = {s, $sformatf("=======================\n")};
			return s;
		endfunction
	endclass

3.1.2 reg_driver

寄存器驱动器,用于生成寄存器传输事务并将其发送到接口。
该类包括了一个 do_drive 任务来驱动事务的发送,一个 do_reset 任务来处理复位信号,以及一个 reg_write 任务来处理寄存器的读写操作。

	class reg_driver;
		local string name;
		local virtual reg_intf intf;
		mailbox #(reg_trans) reg_mb;
		mailbox #(reg_trans) rsp_mb;
		
		function new(string name = "reg_driver");
			this.name = name;
		endfunction
		
		function void set_interface (virtual reg_intf intf);
			if(intf == null)
				$error("interface is null");
			else
				this.intf == intf;
		endfunction
		
		task run();
			fork
				this.do_drive();
				this.do_reset();
			join
		endtask
		
		task do_reset();
			forever begin
				@(negedge intf.rstn);
				intf.cmd_addr     <= 0    ;
				intf.cmd          <= 'IDLE;
				intf.cmd_data_m2s <= 0    ;
			end
			
		task do_drive();
			reg_trans req, rsp;
			@(posedge intf.rstn);
			forever begin
				this.req_mb.get(req);
				this.reg_write(req);
				rsp = req.clone();
				rsp.rsp = 1;
				this.rsp_mb.put(rsp);
			end
		endtask
		
		task reg_write (reg_trans t);
			@posedge (intf.clk iff intf.rstn);//这是一个复合条件,iff 表示 "if and only if",即当且仅当。
											  //所以整个条件表示:只有当 intf.rstn 为逻辑真时,才触发在 intf.clk 的上升沿执行操作。
			case(t.cmd)
				'WRITE:begin
						intf.drv_ck.cmd_addr <= t.addr;
						intf.drv_ck.cmd <= t.cmd;
						intf.drv.cmd_data_m2s <= t.data;						
						end
				'READ :begin
						intf.drv_ck.cmd_addr <= t.addr;
						intf.drv_ck.cmd <= t.cmd;
						repeat(2) @(negedge intf.clk);//为何要等两个下降沿?
						t.data = intf.cmd_data_s2m;
						end
				'IDLE :begin
						this.reg_idle();
						end
				default: $error("command %b is illegal", t.cmd);
			endcase 
			$display("%0t reg driver [%s] sent addr %2x,cmd %2b,data %8x",$time,name,t.addr,t.cmd,t.data);
		endtask
		
		task reg_idle();
			@(posedge intf.clk);
			intf.drv_ck.cmd_addr <= 0    ;
			intf.drv_ck.cmd_addr <= 0    ;
			intf.drv_ck.cmd      <= 'IDLE;
		endtask
	endclass

3.1.3 reg_generator

生成寄存器传输事务,并提供一个 send_trans 任务来发送事务并处理响应。

	class reg_generator;
		rand bit [7:0] addr = -1;
		rand bit [1:0] cmd  = -1;
		rand bit [31:0]data = -1;
		
		mailbox #(reg_trans) req_mb;
		mailbox #(reg_trans) rsp_mb;
		
		reg_trans reg_req[$];
		
		constraint cstr{
			soft addr == -1;
			soft cmd  == -1;
			soft data == -1;
		}
		
		function new();
			this.req_mb = new();
			this.rsp_mb = new();
		endfunction
		
		task start();
			send_trans();
		endtask
		
		//generate transaction and put into local mailbox
		task send_trans();
			reg_tans req,rsp;
			req = new();
			assert(req.randomize with{local::addr >= 0 -> addr == local::addr;
									  local::cmd  >= 0 -> cmd  == local::cmd;
									  local::data >= 0 -> data == local::data;
										})
			else 
				$fatal("register packet randomization fail");
			$display(req.sprint());
			this.req_mb.put(req);
			this.rsp_mb.get(rsp);
			$display(rsp.sprint());
			if(req.cmd == 'READ)
				this.data = rsp.data;
			assert(rsp.rsp)//确保收到的响应中包含有效的响应标志。如果响应标志无效,则通过 $error 报错。
				else $error("[RSPERR] 0%t error response received!", $time);
		endtask
		
		function string sprint();
			string s ;
			s = {s, $sformatf("========================n")};
			s = {s, $sformatf("reg_trans object content is as below:\n")};
			s = {s, $sformatf("addr =  %2x:\n", this.addr)};//两位16进制数
			s = {s, $sformatf("cmd  =  %2b:\n", this.cmd )};//2位2进制数
			s = {s, $sformatf("data =  %8x:\n", this.data)};//8位16进制数
			s = {s, $sformatf("rsp  =  %0d:\n", this.rsp )};//10进制数,无字符宽度
			s = {s, $sformatf("=======================\n")};
			return s;
		endfunction
		
		//用于在随机化后显示对象的内容。
		function void post_randomize();
			string s;
			s = {"AFTER RANDOMIZE \n",this.sprint()};
			$display (s);
		endfunction
	endclass

3.1.4 reg_monitor

寄存器监视器,用于监视寄存器传输事务并将其存储到邮箱中。

	class reg_monitor;
		local string name;
		local virtual reg_intf intf;
		mailbox #(reg_trans) mon_mb;
		
		function new(string name = "reg_monitor");
		this.name = name;
		endfunction
		
		function void set_interface(virtual reg_intf intf);
			if(intf == null)
				$error("interface handle is null");
			else
				this.intf = intf;
		endfunction 
		
		task run();
			this.mon_trans();
		endtask
		
		//其功能是监视寄存器接口的传输事务,并将监视到的信息存储到邮箱 mon_mb 中
		task mon_trans();
			reg_trans m;
			forever begin
				@(posedge intf.clk iff(intf.rstn && intf.mon_ck.cmd != 'IDLE));
				m = new();
				m.addr = intf.mon_ck.cmd_addr;
				m.cmd  = intf.mon_ck.cmd;
				if(intf.mon_ck.cmd == 'READ)begin
					@(posedge intf.clk);
					m.data = intf.mon_ck.cmd_data_s2m;
				end
				mon_mb.put(m);
				$display("%0t reg driver [%s] sent addr %2x,cmd %2b,data %8x",$time,this.name,m.addr,m.cmd,m.data);
			end
		endtask
	endclass

3.1.5 reg_agent

组合 reg_driver 和 reg_monitor,并提供了一个 run 任务来同时运行驱动器和监视器。

	class reg_agent;
		local string name;
		reg_driver driver;
		reg_monitor monitor;
		local virtual reg_intf vif;
		function new(string name = "reg_agent");
			this.name = name;
			this.driver = new({name,".driver"});
			this.monitor = new({name,".monitor"});
		endfunction
		
		function void set_interface(virtual reg_intf vif);
			this.vif = vif;
			driver.set_interface(vif);
			monitor.set_interface(vif);
		endfunction
		
		task run();
			fork
				driver.run();
				monitor.run();
			join
		endtask
	endclass	

3.2 fmt_pkg

	import rpt_pkg::*;
	
	typedef enum {SHORT_FIFO, MED_FIFO, LONG_FIOF, ULTRA_FIFO} fmt_fifo_t;
	typedef enum {LOW_WIDTH, MED_WIDTH, HIGH_WIDTH, ULTRA_WIDTH} fmt_bandwidth_t;
	typedef enum bit[2:0]{
		FMT_REQ = 3'b000
		,FMT_WIDTH_GRANT = 3'b001
		,FMT_START       = 3'b011
		,FMT_SEND        = 3'b010
		,FMT_END         = 3'b110
		,FMT_IDLE        = 3'b111
	} fmt_state_t;
	

3.2.1 fmt_trans

模拟数据的比较和打印

	class fmt_trans;
		rand fmt_fifo_t fifo;
		rand fmt_bandwidth_t bandwidth;
		bit [9:0] length;
		bit [31:0]data[];
		bit [1:0] ch_id;
		bit rsp;
		constraint cstr{
			soft fifo      == MED_FIFO ;
			soft bandwidth == MED_WIDTH;
		};
		
		function fmt_trans clone();
			fmt_trans c = new();
			c.fifo      = this.fifo;
			c.bandwidth = this.bandwidth;
			c.length    = this.length;
			c.data      = this.data;
			c.ch_id     = this.ch_id;
			c.rsp       = this.rsp;
			return c;
		endfunction
		
		function string sprint();
			string s ;
			s = {s, $sformatf("========================\n")};
			s = {s, $sformatf("fmt_trans object content is as below:\n")};
			s = {s, $sformatf("fifo =  %s:\n", this.fifo)};
			s = {s, $sformatf("bandwidth  =  %s:\n", this.bandwidth )};
			s = {s, $sformatf("length =  %s:\n", this.length)};
			s = {s, $sformatf("rsp  =  %0d:\n", this.rsp )};
			foreach (data[i]) s = {s,$sformatf("data[%0d] = %8x \n", i, this.data[i])};
			s = {s, $sformatf("ch_id =  %0d:\n", this.ch_id)};
			s = {s, $sformatf("rsp  =  %0d:\n", this.rsp )};			
			s = {s, $sformatf("=======================\n")};
			return s;
		endfunction
		
		
//??为啥要做这两个比较呢,fmt_trans例化后的内容不应该就和fmt_trans中的一样吗		
//当你创建一个类(如 fmt_trans)的实例(对象)时,每个实例通常拥有自己的一套成员变量。
//这意味着,即使两个对象是相同类的不同实例,它们的成员变量也可能拥有不同的值。
//检查它们是否具有相同的长度和通道ID,并且它们的数据数组是否相同		
		function bit compare(fmt_trans t);
			string s;
			compare = 1;//假设对象最初是相等的。
			s = "\n============================\n";
			s = {s,$sformatf("COMPARING fmt_trans object at time %0d \n", $time)};
			if(this.length != t.length)begin//this.lengh指的是18行的length
				compare = 0;
				s = {s, $sformatf("sobj length %0d != tobj length %0d \n", this.length, t.length)};
			end
			if(thi.ch_id != t.ch_id)begin
				compare = 0;
				s = {s, $sformatf("sobj ch_id %0d != tobj ch_id %0d \n", this.ch_id, t.ch_id)};
			end
			foreach(this.data[i])begin
				if(this.data[i] != t.data[i])begin
					compare = 0;
					s = {s, $sformatf("sobj data[%0d] %8x != tobj data[%0d] %8x \n",i,  this.data[i], i, t.data[i])};
				end
			end
			if(compare == 1) s = {s, "COMPARED SUCCESS!\n"};
			else s = {s, "COMPARED FAILURED! \n "};
			rpt_pkg::rpt_msg("[CMPOBJ]", s, rpt_pkg::INFO, rpt_pkg::MEDIUM);
		endfunction
	endclass

3.2.2 fmt_driver

模拟数据发送和接受行为

	class fmt_driver;
		local string name;
		local virtual fmt_intf intf;
		mailbox #(fmt_trans) req_mb;
		mailbox #(fmt_trans) rsp_mb;//与generator通信 
		
		local mailbox #(logical[31:0]) fifo;//模拟formatter的下行数据
		local int fifo_bound; //模拟的下行通道最多能接受多少数据
		local int data_consum_period;// 反应下行数据输出的快慢。反比
		
		function new(string name = "fmt_driver");
			this.name = name;
			this.fifo = new();
			this.fifo_bound = 4096;
			this.data_consum_period = 1;
		endfunction
		
		function void set_interface(virtual fmt_intf intf);
			if(intf == null)
				$error("interface handle is null");
			else
				this.intf = intf;
		endfunction
		
		task run();
			fork
				this.do_receive();
				this.do_consume();
				this.do_config();
				this.do_reset();
			join
			
		//配置driver	
		task do_config();
			fmt_trans req, rsp;
			forever begin
				this.req_mb.get(req);
				case(req.fifo)
					SHORT_FIFO  : this.fifo_bound          = 64  ;
					MED_FIFO    : this.fifo_bound          = 256 ;
					LONG_FIOF   : this.fifo_bound          = 512 ;
					ULTRA_FIFO  : this.fifo_bound          = 2048;
				endcase
				this.fifo = new(this.fifo_bound);
				case(req.bandwidth) //为啥位宽低data_consum_period大呢?
									//可能是因为在低带宽条件下,数据传输速率较慢,
									//因此需要更长的周期来消费数据,以匹配数据的传输速率。
					LOW_WIDTH   : this.data_consum_period  = 8   ;
					MED_WIDTH   : this.data_consum_period  = 4   ;
					HIGH_WIDTH  : this.data_consum_period  = 2   ;
					ULTRA_WIDTH : this.data_consum_period  = 1   ;
				endcase
				rsp = req.clone();
				rsp.rsp = 1;//这里的 rsp 成员变量是一个标志位,用于表示某种状态或响应的有效性。
				this.rsp_mb.put(rsp);
			end                                                   
		endtask
		
		task do_reset();
			forever begin
				@(negedge intf.rstn)
				intf.fmt_grant <= 0;
			end
		endtask
		
//模拟具有随机延迟的数据消费场景。按照随机的时间间隔取出数据
		task do_consume();
			logic[31:0] data;
			forever begin
				void'(this.fifo.try_get(data));// 使用 try_get 方法尝试从FIFO中取出一个数据项并赋值给 data 变量
				//void'() 是SystemVerilog中的一个特性,它用来忽略 try_get 方法返回的状态值
				repeat($urandom_range(1, this.data_consum_period)) @(posedge intf.clk);//这里反应消耗数据的快慢
				//等待intf.clk的上升沿,repeat重复一次
			end
		endtask 
	endclass
	
	//当数据接受的请求信号fmt_req拉高时,且FIFO深度足够时,fmt_grant拉高,表示接收方准备好接受数据了
//

		task do_receive();//模拟接受数据
			forever begin
				@(posedge intf.fmt_req);
				forever begin
					@(posedge intf.clk);
					if((this.fifo_bound-this.fifo.num()) >= intf.fmt_length)
					//fifo_bound是选择的fifo的总深度
					//检查当前FIFO的剩余空间是否足够存放即将到来的数据。
					//如果剩余空间大于或等于 intf.fmt_length 指定的长度,则跳出内部循环。
						break;//只跳出内部的这个forever。当fifo余量足够时,拉高grant
				end
				intf.drv_ck.fmt_grant <= 1;//表示接收方准备好接受数据了
				@(posedge intf.fmt_start);//等待 intf.fmt_start 信号的下一个上升沿,
										 //这表示发送方已经准备好发送数据。
				fork 
					begin
						@(posedge intf.clk);
						intf.drv_ck.fmt_grant <= 0;//维持一拍后就拉低,不再授权数据传输
					end
				join_none
				repeat(intf.fmt_length)begin
					@(nedge intf.clk);
					this.fifo.put(intf.fmt_data);//连续放入数据
				end
			end
		endtask

3.2.3 fmt_generator

创建、随机化、发送和接收 fmt_trans 类型的数据事务

	class fmt_generator;
		rand fmt_fifo_t fifo = MED_FIFO;
		rand fmt_bandwidth_t bandwidth = MED_WIDTH;
		
		mailbox #(fmt_trans) req_mb;//在 fmt_generator 类中,req_mb 用于发送 fmt_trans 类型的请求事务,
									//而 rsp_mb 用于接收响应事务。这种方式使得 fmt_generator 可以作为一个独立的组件,
									//模拟发送请求并等待响应,而不依赖于特定的通信协议实现细节。
		mailbox #(fmt_trans) rsp_mb;
		
		constraint cstr{
			soft fifo == MED_FIFO;
			soft bandwidth == MED_WIDTH;
		}
		
		function new();
			this.req.mb = new();
			this.rsp.mb = new();
		endfunction
		
		task start();
			send_trans();
		endtask
		
		//产生事务并放到本地信箱
		task send_trans();
			fmt_trans req, rsp;
			req = new();//为何这里是MED_FIFO呢,不等于就随机化失败?什么道理
						//因为192行就是这样给的
			assert (req.randomize with {local::fifo != MED_FIFO -> fifo == local::fifo
										local::bandwidth != MED_WIDTH -> bandwidth == local::bandwidth;
									   })
				else $fatal("[RNDFALL] formatter packet randomize failure!");
			$display(req.sprint());
			this.req_mb.put(req);//放入请求
			this.rsp_mb.get(rsp);//取出响应
			$display(rsp.sprint());
			assert(rsp.rsp)//检查响应是否有效
				else $error("[RSPERR] %0t error response received!", $time);
		endtask
		
		function string sprint();
			string s;
			s = {s, $sformatf("========================\n")};
			s = {s, $sformatf("fmt_generator object content is as below:\n")};
			s = {s, $sformatf("fifo =  %s:\n", this.fifo)};
			s = {s, $sformatf("bandwidth  =  %s:\n", this.bandwidth )};
			s = {s, $sformatf("=======================\n" )};
			return s;
		endfunction
		
		function void post_randomize();
			string s;
			s = {"AFTER RANDOMIZE \n", this.sprint()};
			$display(s);
		endfunction
		
	endclass

3.2.4 fmt_monitor

用于观察和记录通过接口 fmt_intf 传输的数据事务

	class fmt_monitor;
		local string name;
		local virtual fmt_intf intf;
		mailbox #(fmt_trans) mon_mb;//用于存储监视到的 fmt_trans 类型的事务。
		function new(string name = "fmt_generator");
			this.name = name;
		endfunction
		
		function void set_interface(virtual fmt_intf intf);
			if(intf == null)
				$error("interface handle is null")
			else 
				this.intf = intf;
		endfunction//设置监视器要监视的接口
		
		task run();
			this.mon_trans();
		endtask
		
		task mon_trans();
			fmt_trans m;
			string s;
			forever begin
				@(posedge intf.mon_ck.fmt_start);//等待 intf.mon_ck.fmt_start 信号的上升沿,表示一个新事务的开始。
				m = new();
				m.length = intf.mon_ck.fmt_length;
				m.ch_id  = intf.mon_ck.fmt_chid;
				m.data   = new[m.length];//接口中获取事务的长度、通道ID和数据,并将它们赋值给 m 对象。
				foreach(m.data[i])begin
					@(posedge intf.clk);//在每个时钟上升沿从接口中获取数据,并将其存储在 m.data 数组中。
					m.data[i] = intf.mon_ck.fmt_data;
				end
				mon_mb.put(m);
				s = {s, $sformatf("========================\n")};
				s = {s, $sformatf("%0t %s monitored a packet:\n", $time, this.name)};
				s = {s, $sformatf("length =  %0d:\n", m.length)};
				s = {s, $sformatf("chid  =  %0d:\n", m.ch_id )};
				foreach (m.data[i] s = {s,$sformatf("data[%0d] = %8x \n", i, m.data[i])};	
				s = {s, $sformatf("=======================\n" )};
				$display(s);
			end
		endtask
	endclass
	

3.2.5 fmt_agent

组合驱动器(fmt_driver)和监视器(fmt_monitor)组件

	class fmt_agent;
		local string name;
		fmt_driver driver;
		fmt_monitor monitor;
		local virtual fmt_intf vif;
		
		function new(string name = "fmt_agent");
			this.name   = name;
			this.driver = new({name, ".driver"});
			this.monitor= new({name, ".monitor"});
		endfunction
		
		function void set_interface(virtual fmt_intf vif);
			this.vif = vif;
			driver.set_interface(vif);
			monitor.set_interface(vif);
		endfunction
		
		task run();
			fork
				driver.run();
				monitor.run();
			join
		endtask 
	endclass

3.3 mcdf_pkg

	import chnl_pkg::*;
	import reg_pkg ::*;
	import arb_pkg ::*;
	import fmt_pkg ::*;
	import rpt_pkg ::*;
	
	typedef struct packed{
		bit[2:0] lenl ;//后面用来规定各寄存器通道的数据包长度,优先级,使能信号,可写余量 
		bit[1:0] prio ;
		bit      en   ;
		bit[7:0] avail;
	}mcdf_reg_t
	
	typedef  enum{RW_LEN, RW_PRIO, RW_EN, RD_AVAIL}mcdf_field_t;//寄存器的不同比特位代表不同的功能,详见蓝皮书376页

3.3.1 mcdf_refmod

用于测试或模拟MCDF模块的寄存器行为,形成一个参考模型

	class mcdf_refmod;
		local virtual mcdf_intf intf;
		local string  name;
		mcdf_reg_t regs[3];
		mailbox #(reg_trans)  reg_mb;
		mailbox #(mon_data_t) in_mbs [3];
		mailbox #(fmt_trans)  out_mbs[3];
		
		function new(string name = "mcdf_refmod");
			this.name = name;
			foreach (this.out_mbs[i]) this.out_mbs[i] = new();
		endfunction
		
		task run();
			fork
				do_reset();
				this.do_reg_update();
				do_packet(0);
				do_packet(1);
				do_packet(2);				
			join
		endtask
		
		task do_reg_update();
			reg_trans t;
			forever begin
				this.reg_mb.get(t);
				if(t.addr[31 :4] == 0 && t.cmd == 'WRITE)begin
					this.regs[t.addr[3:2]].en   = t.data[0]   ;
					this.regs[t.addr[3:2]].prio = t.data[2:1] ;
					this.regs[t.addr[3:2]].len  = t.data[5:3] ;
				end
				else if(t.addr[7:4] == 1 && t.cmd == 'READ)begin
					this.regs[t.addr[3:2]].avail = t.data[7:0];
				end
			end
		endtask
		
//处理数据包
		task do_packet(int id);
			fmt_trans  ot;
			mon_data_t it;
			bit[2:0] len;
			forever begin
				this.in_mbs[id].peek(it);
				ot = new();
				len = this.get_field_value(id, RW_LEN);
				ot.length = len > 3 ? 32:4 << len;//根据len的值计算数据包的长度。
				//为什么数据包长度这样定义        //如果len大于3,则长度为32,否则长度len左移4位。
				//因为第50行规定第4到6位存放数据包长度
				ot.data = new[ot.length];//根据计算出的长度,动态分配一个整型数组给ot.data,
										 //用于存储数据包的数据。
				ot.ch_id = id;
				foreach(ot.data[i])begin
					this.in_mbs[id].get(it);//从信箱中取数据,给到it
					ot.data[i] = it.data;
				end
				this.out_mbs[id].put(ot);//将格式化后的事务ot放入到输出mailbox(out_mbs)中
			end
		endtask 
		
		function int get_field_value(int id, mcdf_field_t f);//mcdf_field_t是18行的枚举类型
			case(f)
				RW_LEN  : return regs[id].len  ;//这四个case分别代表什么?
				RW_PRIO : return regs[id].prio ;
				RW_EN   : return regs[id].en   ;
				RD_AVAIL: return regs[id].avail;
			endcase
		endfunction
		
		task do_reset();
			forever begin
				@(negedge intf.rstn);
				foreach(regs[i])begin
					regs[i].len  = 'h0;
					regs[i].prio = 'h3;
					regs[i].en   = 'h1;//为什么复位信号要给这些值?
					regs[i].avail= 'h20;//应该还是和task do_reg_update有关
				end
			end
		endtask
		
		function void set_interface(virtual mcdf_intf intf);
			if(intf == null)
				$error("interface handle is null");
			else
				this.intf = intf;
		endfunction

	endclass

3.3.2 mcdf_checker

将refmod的数据与fmt的数据进行对比

	class mcdf_checker;
		local string name;
		local int err_count;
		local int total_count;
		local int chl_count[3];
		local virtual mcdf_intf intf;
		local refmod mcdf_refmod;
		
		mailbox #(mon_data_t) chnl_mbs[3];
		mailbox #(fmt_trans) fmt_mb;
		mailbox #(reg_trans) reg_mb;
		mailbox #(fmt_trans) exp_mbs[3];
		
		function new(string name = "mcdf_checker");
			this.name = name;
			foreach(this.chnl_mbs[i]) this.chnl_mbs[i] = new();
			this.fmt_mb = new();
			this.reg_mb = new();
			this.refmod = new();
			foreach (this.refomd.in_mbs[i])begin
				this.refmod.in_mbs[i] = this.chnl_mbs[i];
				this.exp_mbs[i] = this.refmod.out_mbs[i];
			end
			this.refmod.reg_mb = this.reg_mb;
			this.err_count = 0;
			this.total_count = 0;
			foreach(this.chnl_count[i]) this.chnl_count[i] = 0;
		endfunction

		function void set_interface (virtual mcdf_intf intf);
			if(intf == null)
				$error("interface handle is null");
			else
				this.intf = intf;
				this.refmod.set_interface(intf);
		endfunction
		
		task run();
			fork
				this.do_compare();
				this.refmod.run();
			join
		endtask
		
	//比较收集到的数据是否和参考数据一样	
		task do_compare();
			fmt_trans expt, mont;
			bit cmp;
			forever begin
				this.fmt_mb.get(mont);//formatter中的数据
				this.exp_mbs[mont.ch_id].get(expt);//期望的数据
				cmp = mont.compare(expt);//调用的是fmt_pkg里面的compare函数
				this.total_count++;//是拿来计数什么的?
				this.chnl_count[mont.ch_id]++;//是拿来计数什么的?
				if(cmp == 0)begin//不相等
					this.err_count++;
					rpt_pkg::rpt_msg("[CMP FAIL]", $sformatf("%0t %0dth times comparing but failed!MCDF monitored output packet is different with reference model output", $time, this.total_count),
					rpt_pkg::ERROR,
					rpt_pkg::TOP,
					rpt_pkg::LOG);
				end
				else begin
					rpt_pkg::rpt_msg("[CMP SUCD]", $sformatf("%0t %0dth times comparing and succeeded! MCDF monitored output packet is different with reference model output", $time, this.total_count),
					rpt_pkg::INFO,
					rpt_pkg::HIGH,
				end
			end
		endtask
		
		function void do_report();
			string s;
			s = "\n--------------------------------------------\n";
			s = {s, " CHECKER SUMMARY \n"};
			s = {s, $sformatf("total comparison count: %0d \n", this.total_count)};
			foreach (this.chnl_count[i]) begin
				if(this.chnl_mbs[i].num() != 0 )//判断是否把数据比较完
				s = {s, $sformatf("WARNING:: chnl_mbs[%0d] is not empty! size = %0d \n", i, this.chnl_mbs[i].num())};	
			end
			if(this.fmt_mb.num() != 0)
				s = {s, $sformatf("WARNING:: fmt_mb is not empty! size = %0d \n", this.fmt_mb.num())};
			s = "\n--------------------------------------------\n";
			rpt_pkg::rpt_msg($sformatf("[%s]",this.name), s, rpt_pkg::INFO, rpt_pkg::TOP);
		endfunction
	endclass

3.3.3 mcdf_env

用于构建和运行MCDF模块的测试环境

	class mcdf_env;
		chnl_agent chnl_agts[3];
		reg_agent reg_agt;
		fmt_agent fmt_agt;
		mcdf_checker chker;
		mcdf_coverage cvrg;
		protected string name;
		
		function new(string name = "mcdf_env");
			this.name  = name;
			this.chker = new();
			foreach(chnl_agts[i])begin
				this.chnl_agts[i] = new($sformatf("chnl_agts[%0d]", i));
				this.chnl_agts[i].monitor.mon_mb = this.chker.chnl_mbs[i];
			end
			this.reg_agt = new("reg_agt");
			this.reg_agt.monitor.mon_mb = this.chker_reg_mb;
			this.fmt_agt = new("fmt_agt");		
			this.fmt_agt.monitor.mon_mb = this.chker.fmt.mb;
			this.cvrg = new();
			$display("%s instantiated and connected objects", this.name);
		endfunction
		
		virtual task run();
			$display($sformatf("*****************%s started***********", this.name));
			this.do_config();
			fork
				this.chnl_agts[0].run)();
				this.chnl_agts[1].run)();
				this.chnl_agts[2].run)();		
				this.reg_agt.run)();	
				this.fmt_agt.run)();
				this.chker.run();
				this.cvrg.run();
			join
		endtask
		
		virtual function void do_config();//之前式用do_config来配置generator。但是
		endfunction                       //现在将其移植到mcdf_base_test中来进行配置了
		
		virtual function void do_report();
		this.chker.do_report();
		this.cvrg.do_reporet();
		endfunction
	endclass

3.3.4 mcdf_base_test

测试模板

	class mcdf_base_test;
		chnl_generator chnl_gens[3];
		reg_generator reg_gen;
		fmt_generator fmt_gen;
		mcdf_env env;
		protected string name;
		
		function new(string name = "mcdf_base_test");
			this.name = name;
			this.env = new(env);
			
			//将每个gens给到env进行代理
			foreach(this.chnl_gens[i])begin
				this.chnl_gens[i] = new();
				this.env.chnl_agts[i].driver.req_mb = this.chnl_gens[i].req_mb;
				this.env.chnl_agts[i].driver.rsq_mb = this.chnl_gens[i].rsq_mb;
			end
			
			this.reg_gen = new();
			this.env.reg_agt.driver.req_mb = this.reg_gen.req_mb;
			this.env.reg_agt.driver.rsp_mb = this.reg_gen.rsp_mb;
			
			this.fmt_gen = new();
			this.env.fmt_agt.driver.req_mb = this.fmt_gen.req_mb;
			this.env.fmt_agt.driver.rsp_mb = this.fmt_gen.rsp_mb;
			
			$display("%s instantiated and connected objects", this.name);
		endfunction
		
		virtual task run();
			fork 
				env.run();
			join_none
			rpt_pkg::rpt_msg("[TEST]",
				$sformatf("===========%s AT TIME 0%t STARTED=======", this.name,$time),
				rpt_pkg::INFO;
				rpt_pkg::HIGH);
			this.do_reg();
			this.do_formatter();
			this.do_data();
			rpt_pkg::rpt_msg("[TEST]",
				$sformatf("===========%s AT TIME 0%t FINISHED=======", this.name,$time),
				rpt_pkg::INFO;
				rpt_pkg::HIGH);
			this.do_report();
			$finish();
		endtask
		
		//do register configuration
		virtual task do_reg();
		endtask
		
		//do external formatter down stream slave configuration
		virtual task do_formatter();
		endtask
		
		//do data transition from 3 channel slaves
		virtual task do_data();
		endtask
		
		//do simulation SUMMARY
		virtual function void do_report();
			this.env.do_report();
			rpt_pkg::do_report();
		endfunction
		
		virtual function void set_interface( virtual chnl_intf ch0_vif
											,virtual chnl_agent ch1_vif
											,virtual chnl_agent ch2_vif
											,virtual reg_intf reg_vif
											,virtual fmt_intf fmt_vif
											,virtual mcdf_intf mcdf_vif);
			this.env.chnl_agts[0].set_interface(ch0_vif);
			this.env.chnl_agts[1].set_interface(ch1_vif);
			this.env.chnl_agts[2].set_interface(ch2_vif);		
			this.env.reg_agt.set_interface(reg_vif);
			this.env.fmt_agt.set_interface(fmt_vif);
			this.env.chker.set_interface(mcdf_vif);	
			this.env.cvrg.set_interface('{ch0_vif, ch1_vif, ch2_vif,}, reg_vif, arb_vif, fmt_vif, mcdf_vif);
		endfunction
		
		virtual function bit diff_value(int val1, int val2, string id ="value_compare");
			if(val1 != val2)begin
				rpt_pkg::rpt_msg("[CMP ERR]",
				$sformatf("ERROR! %s val1 %8x != val2 %8x", id, val1, val2),
				rpt_pkg::ERROR;
				rpt_pkg::TOP);
				return 0;
			end
			else begin
				rpt_pkg::rpt_msg("[CMP SUC]",
				$sformatf("SUCCESS! %s val1 %8x == val2 %8x", id, val1, val2),
				rpt_pkg::INFO;
				rpt_pkg::HIGH);
				return 1;
			end
		endfunction
		
		virtual task idle_reg();
			void'(reg_gen.randomize() with {cmd == 'IDLE; addr == 0; data == 0;});
			reg_gen.start();
		endtask
		
		virtual task write_reg(bit[7:0]addr, bit[31:0] data);
			void'(reg_gen.randomize() with {cmd == 'WRITE; addr == local::addr; data == local::data;});	
			reg_gen.start();	
		endtask			
			
		virtual task read_reg(bit[7:0]addr, output bit[31:0] data);
			void'(reg_gen.randomize() with {cmd == 'READ; addr == local::addr;});	
			reg_gen.start();	
			data = reg_gen.data;
		endtask			
	endclass

3.3.5 mcdf_data_consistence_basic_test

读写数据一致性检测,检查寄存器读进去和写出来的数据是否一致

	class mcdf_data_consistence_basic_test extends mcdf_base_test;
		function new(string name = "mcdf_data_consistence_basic_test");
			super.new(name);
		endfunction
		
		//检查读写是否一致
		task do_reg();
			bit[31:0] wr_val, rd_val;
			//slv0 with len=8,prior=0,en=1
			wr_val = (1<<3)+(0<<1)+1;//为什么要这样设置呢
			this.wrtie_reg('SLV0_RW_ADDR, wr_val);//写进去
			this.read_reg('SLV0_RW_ADDR, rd_val);//读回来
			void'(this.diff_value(wr_val, rd_val, "SLV0_WR_REG"));//把写进去的值和读回来的值做一个比较
																  //看看写进去的值是否合法
			//slv1 with len=16,prior=1,en=1
			wr_val = (2<<3)+(1<<1)+1;
			this.wrtie_reg('SLV1_RW_ADDR, wr_val);
			this.read_reg('SLV1_RW_ADDR, rd_val);
			void'(this.diff_value(wr_val, rd_val, "SLV1_WR_REG"));	

			//slv2 with len=32,prior=2,en=1
			wr_val = (3<<3)+(2<<1)+1;
			this.wrtie_reg('SLV2_RW_ADDR, wr_val);
			this.read_reg('SLV2_RW_ADDR, rd_val);
			void'(this.diff_value(wr_val, rd_val, "SLV2_WR_REG"));

			//send IDLE command
			this.idle_reg();
		endtask 
		
		//配置格式化器(fmt_gen)的随机值,包括 FIFO 大小和带宽。
		task do_formatter();
			void'(fmt_gen.randomize()with{fifo == LONG_FIFO; bandwidth == HIGH_WIDTH;});
			fmt_gen.start();
		endtask
		
		//生成数据
		task do_data();
			void'(chnl_gens[0].randomize() with {ntrans == 100; ch_id == 0; data_nidles == 0; pkt_nidles == 1; data_size ==  8;});
			void'(chnl_gens[1].randomize() with {ntrans == 100; ch_id == 1; data_nidles == 1; pkt_nidles == 1; data_size == 16;});
			void'(chnl_gens[2].randomize() with {ntrans == 100; ch_id == 2; data_nidles == 2; pkt_nidles == 1; data_size == 32;});	
			fork
				chnl_gens[0].start();
				chnl_gens[1].start();
				chnl_gens[2].start();
			join
			#10us;//wait until all data haven been transfered through MCDF
		endtask
	endclass

3.3.5 mcdf_reg_write_read_test

寄存器读写测试,包括对控制读写寄存器和状态只读寄存器的测试

	class mcdf_reg_write_read_test extends mcdf_base_test;
	
	
		function new (string name = "mcdf_reg_write_read_test");
			super.new(name);
		endfunction
		
		virtual task do_reg();
			bit [31:0]wr_val, rd_val;
			bit [7:0] chnl_rw_addrs[] = '{'SLV0_RW_ADDR, 'SLV1_RW_ADDR, 'SLV2_RW_ADDR};//读写寄存器
			bit [7:0] chnl_ro_addrs[] = '{'SLV0_R_ADDR, 'SLV1_R_ADDR, 'SLV2_R_ADDR};//只读寄存器
			int pwidth = 'PAC_LEN_WIDTH + 'PRIO_WIDTH + 1;//PAC_LEN_WIDTH是定义在param_def中的常数
			bit[31:0] check_pattern[] = '{32'h0000_FFC0, 32'hFFFF_0000};//类似于地址检查库
		
			foreach (chnl_rw_addrs[i])begin
				foreach(check_pattern[j])begin
					wr_val = check_pattern[j];
					this.write_reg(chnl_rw_addrs[i], wr_val);
					this.read_reg(chnl_rw_addr[i], rd_val);
					void'((this.diff_value(wr_val & ((1<<pwidth)-1), rd_val));//为什么,(1<<pwidth)-1=31
				end//按位与操作。这种掩码结果的二进制形式会有pwidth个低位是1。即保留wr_val的低pwidth位,其余位清零
			end//如果wr_val是32'h0000_FFC0,且pwidth是5,那么结果是32'h0000_0010。
			//仅比较有效位(低pwidth位),忽略无关的高位。这在寄存器操作中非常重要,
			//因为某些寄存器的高位可能是未定义或保留位,比较时应避免这些位的影响。
			
			foreach(chnl_ro_addrs[i])begin
				wr_val = 32'hFFFF_FF00;//由于是只读寄存器,因此写进入的值是确定的
				this.write_reg(chnl_ro_addrs[i],wr_val);
				this.read_reg(chnl_ro_addrs[1], rd_val);
				void'(this.diff_value(0, rd_val & wr_val));//这用于将函数调用的返回值转换为 void,实际上是忽略函数的返回值。
			end//对于只读寄存器,我们期望写入操作不会改变寄存器的值。
			//因此,我们预期从只读寄存器读取到的值应该是0(这就是为什么第一个参数是0)。

			
			this.idle_reg();
		endtask 
	endclass

3.3.6 mcdf_reg_illegal_access_tets

寄存器稳定性测试。

	class mcdf_reg_illegal_access_tets extends mcdf_base_test;
		function new(string name = "mcdf_reg_illegal_access_tets");
			super.new(name);
		endfunction
		
		task do_reg();
			bit [31:0]wr_val, rd_val;
			bit [7:0] chnl_rw_addrs[] = '{'SLV0_RW_ADDR, 'SLV1_RW_ADDR, 'SLV2_RW_ADDR};
			bit [7:0] chnl_ro_addrs[] = '{'SLV0_R_ADDR, 'SLV1_R_ADDR, 'SLV2_R_ADDR};
			int pwidth = 'PAC_LEN_WIDTH + 'PRIO_WIDTH + 1;
			bit[31:0] check_pattern[] = '{((1<<pwidth)-1), 0, ((1<<pwidth)-1)};	
			
			foreach (chnl_rw_addrs[i])begin
				foreach(check_pattern[j])begin
					wr_val = check_pattern[j];
					this.write_reg(chnl_rw_addrs[i],wr_val);
					this.read_reg(chnl_rw_addrs[i],rd_val);
					void'(this.diff_value(wr_val, rd_val));
				end
			end
			
			foreach(chnl_ro_addrs[i])begin
					this.read_reg(chnl_ro_addrs[i],rd_val);			
			end
			
			this.idle_reg();
		endtask
	endclass

3.3.7 mcdf_arbiter_priority_test

arbiter优先级测试

如果各数据通道优先级相同,那么arbiter应该采取轮询机制从各个通道接受数据,如果优先级不同,先从高优先级的通道接收数据

	class mcdf_arbiter_priority_test extends mcdf_base_test;
		function new (string name  = "mcdf_arbiter_priority_test");
			super.new(name) 
		endfunction 
		
		task do_arbiter_priority_check();
			int id;
			forever begin
				@(posedge this.arb_vif.clk iff (this.arb_vif.rstn && this.arb_vif.mon_ck.f2a_id_req===1));
				id = this.get_slave_id_with_prio();//得到三个slave的最高优先级
				if(id >= 0) begin
				@(posedge this.arb_vif.clk);
				if(this.arb_vif.mon_ck.a2s_acks[id] !== 1)//acks【i】是设计的硬件信号的slvae通道选择
					rpt_pkg::rpt_msg("[CHKERR]",
					$sformatf("ERROR! %0t arbiter received f2a_id_req===1 and channel[%0d] raising request with high priority, but is not granted by arbiter", $time, id),
					rpt_pkg::ERROR,
					rpt_pkg::TOP);
				end
			end
		endtask
		
//模拟arbiter仲裁逻辑
		function int get_slave_id_with_prio();
			int id=-1;
			int prio=999;
			//寻找最高优先级
			foreach(this.arb_vif.mon_ck.slv_prios[i]) begin
				if(this.arb_vif.mon_ck.slv_prios[i] < prio && this.arb_vif.mon_ck.slv_reqs[i]===1) begin
				id = i;
				prio = this.arb_vif.mon_ck.slv_prios[i];
				end
			end
			return id;
		endfunction


	endclass

3.3.8 mcdf_channel_disbale_test

数据通道开关检测

在数据通道关闭的情况下检测数据写入是否通过

	class mcdf_channel_disbale_test extends mcdf_base_tese;
		function new (string name  = "mcdf_channel_disbale_test");
			super.new(name) 
		endfunction 		
	
		task do_mcdf_channel_disable_test (int id)
			forever begin 
				@(posedge this.mcdf_vif.clk iff(this.mcdf_vif.rstn && this.mcdf_vif.mon_ck.chnl_en[id] == 0));
				if (this.chnl_vifs[id].mon_ck.ch_valid == 1 && this.chnl_vifs[id].mon_ck.ch_ready == 1)
					rpt_pkg::rpt_msg("[CHECKER]",
									$sformatf("ERROR! %0t whern channel disabled, ready signal raised while valid signal is high", $time),
									rpt_pkg::ERROR,
									rpt_pkg::TOP);
			end
		endtask 
	
	endclass

3.3.9 mcdf_coverage

覆盖率收集

	class mcdf_coverage;
		local virtual chnl_intf chnl_vifs[3];	
		local virtual arb_intf arb_vif;
		local virtual mcdf_intf mcdf_vif;
		local virtual reg_intf reg_vif;
		local virtual fmt_intf reg_vif;
		local string  name;
		local int     delay_req_to_grant;
		
		//对所有控制、状态寄存器的读写进行测试。
		covergroup cg_mcdf_reg_write_read;
			addr: coverpoint reg_vif.mon_ck.cmd_addr{
				type_option.weight = 0;//权重设置为0,表示不关心此coverpoint的覆盖率,否则cross以后会出现很多的bin
				bins slv0_rw_addr = {'SLV0_RW_ADDR};
				bins slv1_rw_addr = {'SLV1_RW_ADDR};				
				bins slv2_rw_addr = {'SLV2_RW_ADDR};				
				bins slv0_r_addr  = {'SLV0_R_ADDR };				
				bins slv1_r_addr  = {'SLV1_R_ADDR };				
				bins slv2_r_addr  = {'SLV2_R_ADDR };				
			}//8bit地址实际上会生成256个bin,256-6=250会被平均分配到这6个bin中,
             //但默认max为64个bin,所以剩下的250要分配到64-6=58个bin里面
			//建议如果要做cross,那需要被cross的这些功能点都weight=0,然后在cross中重新bin
			cmd: coverpoint reg_vif.mon_ck.cmd{
				type_option.weight = 0;
				bins write = {'WRITE};
				bins read  = {'READ};
				bins idle  = {'IDLE};
			}//cmd是2bit的,所以还有一个默认的bin
			
			cmdXaddr: cross cmd, addr{
				bins slv0_rw_addr       = binsof (addr.slv0_rw_addr);
				bins slv1_rw_addr       = binsof (addr.slv1_rw_addr);				
				bins slv2_rw_addr       = binsof (addr.slv2_rw_addr);				
				bins slv0_r_addr        = binsof (addr.slv0_r_addr );				
				bins slv1_r_addr        = binsof (addr.slv1_r_addr );					
				bins slv2_r_addr        = binsof (addr.slv2_r_addr );					
				bins write              = (cmd.write);				
				bins read               = (cmd.read );				
				bins idle               = (cmd.idle );			
				//因为之前设置权重为0,所以需要再一次的声明,这样可以只关心所需要的bin即可,不会生成没用的bin				
				bins write_slv0_rw_addr = binsof (cmd.write) && binsof (addr.slv0_rw_addr);		  	
				bins write_slv1_rw_addr = binsof (cmd.write) && binsof (addr.slv1_rw_addr);			
				bins write_slv2_rw_addr = binsof (cmd.write) && binsof (addr.slv2_rw_addr);			
				bins read_slv0_rw_addr  = binsof (cmd.read ) && binsof (addr.slv0_rw_addr);				
				bins read_slv1_rw_addr  = binsof (cmd.read ) && binsof (addr.slv1_rw_addr);				
				bins read_slv2_rw_addr  = binsof (cmd.read ) && binsof (addr.slv2_rw_addr);			
				bins read_slv0_r_addr   = binsof (cmd.read ) && binsof (addr.slv0_r_addr );				
				bins read_slv1_r_addr   = binsof (cmd.read ) && binsof (addr.slv1_r_addr );				
				bins read_slv2_r_addr   = binsof (cmd.read ) && binsof (addr.slv2_r_addr );				
			}
		endgroup

		//对非法地址进行读写测试,对控制寄存器的保留域进行读写测试,对状态寄存器进行写操作测试。
		covergroup cg_mcdf_reg_illegal_access;
			addr: coverpoint reg_vif.mon_ck.cmd_addr {
				type_option.weight = 0;
				bins legal_rw = {`SLV0_RW_ADDR, `SLV1_RW_ADDR, `SLV2_RW_ADDR};//合法读写寄存器地址
				bins legal_r  = {`SLV0_R_ADDR, `SLV1_R_ADDR, `SLV2_R_ADDR};   //合法读地址范围
				/*这里做了一些简化,这里对任意一个通道的读写寄存器进行了操作就满足覆盖率要求*/
				/**但是实际上如果要对每一个通道的寄存器都进行覆盖率检测,那应该写为数组形式*/
				/*bins legal_rw[] = {`SLV0_RW_ADDR, `SLV1_RW_ADDR, `SLV2_RW_ADDR};
				  bins legal_r[]  = {`SLV0_R_ADDR, `SLV1_R_ADDR, `SLV2_R_ADDR};   */			
				bins illegal  = {[8'h20:$], 8'hC, 8'h1C};				      //非法地址范围
			}
			cmd: coverpoint reg_vif.mon_ck.cmd {
				type_option.weight = 0;
				bins write = {`WRITE};
				bins read  = {`READ};
			}
			//写数据覆盖点
			wdata: coverpoint reg_vif.mon_ck.cmd_data_m2s {
				type_option.weight = 0;
				//数据合法范围,0~111111,即bit(0)使能信号、bit(2:1)优先级、bit(5:3)数据包长度都可以写
				bins legal = {[0:'h3F]};
				//数据非法范围,bit(31:6)保留位,无法写入
				bins illegal = {['h40:$]};
			}
			//读数据覆盖点
			rdata: coverpoint reg_vif.mon_ck.cmd_data_s2m {
				type_option.weight = 0;
				//读数据合法范围,0~11111111,即bit(7:0)上行数据从端FIFO的可写余量
				bins legal = {[0:'hFF]};
				illegal_bins illegal = default;
			}
			//交叉覆盖
			cmdXaddrXdata: cross cmd, addr, wdata, rdata {
				bins addr_legal_rw         = binsof(addr.legal_rw);
				bins addr_legal_r          = binsof(addr.legal_r);
				bins addr_illegal          = binsof(addr.illegal);
				bins cmd_write             = binsof(cmd.write);
				bins cmd_read              = binsof(cmd.read);
				bins wdata_legal           = binsof(wdata.legal);
				bins wdata_illegal         = binsof(wdata.illegal);
				bins rdata_legal           = binsof(rdata.legal);
				bins write_illegal_addr    = binsof(cmd.write) && binsof(addr.illegal);//对非法地址进行写操作
				bins read_illegal_addr     = binsof(cmd.read)  && binsof(addr.illegal);//对非法地址进行写操作
				bins write_illegal_rw_data = binsof(cmd.write) && binsof(addr.legal_rw) && binsof(wdata.illegal);//对读写寄存器合法地址写入非法数据
				bins write_illegal_r_data  = binsof(cmd.write) && binsof(addr.legal_r)  && binsof(wdata.illegal);//对只读寄存器合法地址写入非法数据
			}
		endgroup
		
	    //对每一个数据通道对应的控制寄存器域en配置为0,在关闭状态下测试数据写入是否通过。
		covergroup cg_channel_disable;
			ch0_en: coverpoint mcdf_vif.mon_ck.chnl_en[0] {
				type_option.weight = 0;
				wildcard bins en  = {1'b1};//这里为什么要用wildcard来修饰
				wildcard bins dis = {1'b0};
				//wildcard修饰符用于位宽不定的信号,它使得值的匹配忽略位数上的差异。
				//具体来说,wildcard可以用来简化匹配条件,特别是在信号中有不关心的位时
				//wildcard可以让表达式中的任何x、z或?被当作0或1的通配符
			}
			ch1_en: coverpoint mcdf_vif.mon_ck.chnl_en[1] {
				type_option.weight = 0;
				wildcard bins en  = {1'b1};//对应使能
				wildcard bins dis = {1'b0};//对于禁用
			}
			ch2_en: coverpoint mcdf_vif.mon_ck.chnl_en[2] {
				type_option.weight = 0;
				wildcard bins en  = {1'b1};
				wildcard bins dis = {1'b0};
			}
			ch0_vld: coverpoint chnl_vifs[0].mon_ck.ch_valid {
				type_option.weight = 0;
				bins hi = {1'b1};
				bins lo = {1'b0};
			}
			ch1_vld: coverpoint chnl_vifs[1].mon_ck.ch_valid {
				type_option.weight = 0;
				bins hi = {1'b1};
				bins lo = {1'b0};
			}
			ch2_vld: coverpoint chnl_vifs[2].mon_ck.ch_valid {
				type_option.weight = 0;
				bins hi = {1'b1};
				bins lo = {1'b0};
			}
			//采集当valid拉高时,代表要进行写操作,en拉低代表数据通道关闭
			chenXchvld: cross ch0_en, ch1_en, ch2_en, ch0_vld, ch1_vld, ch2_vld {
				bins ch0_en      = binsof(ch0_en.en );
				bins ch0_dis     = binsof(ch0_en.dis);
				bins ch1_en      = binsof(ch1_en.en );
				bins ch1_dis     = binsof(ch1_en.dis);
				bins ch2_en      = binsof(ch2_en.en );
				bins ch2_dis     = binsof(ch2_en.dis);
				bins ch0_hi      = binsof(ch0_vld.hi);
				bins ch0_lo      = binsof(ch0_vld.lo);
				bins ch1_hi      = binsof(ch1_vld.hi);
				bins ch1_lo      = binsof(ch1_vld.lo);
				bins ch2_hi      = binsof(ch2_vld.hi);
				bins ch2_lo      = binsof(ch2_vld.lo);
				bins ch0_en_vld  = binsof(ch0_en.en ) && binsof(ch0_vld.hi);
				bins ch0_dis_vld = binsof(ch0_en.dis) && binsof(ch0_vld.hi);
				bins ch1_en_vld  = binsof(ch1_en.en ) && binsof(ch1_vld.hi);
				bins ch1_dis_vld = binsof(ch1_en.dis) && binsof(ch1_vld.hi);
				bins ch2_en_vld  = binsof(ch2_en.en ) && binsof(ch2_vld.hi);
				bins ch2_dis_vld = binsof(ch2_en.dis) && binsof(ch2_vld.hi);
			}
		endgroup
		
		//将不同数据通道配置为相同或者不同的优先级,在数据通道使能的情况下进行测试。
		covergroup cg_arbiter_priority;
			ch0_prio: coverpoint arb_vif.mon_ck.slv_prios[0] {
				bins ch_prio0 = {0}; 
				bins ch_prio1 = {1}; 
				bins ch_prio2 = {2}; 
				bins ch_prio3 = {3}; //为什么会有4个优先级呢
			}
			ch1_prio: coverpoint arb_vif.mon_ck.slv_prios[1] {
				bins ch_prio0 = {0}; 
				bins ch_prio1 = {1}; 
				bins ch_prio2 = {2}; 
				bins ch_prio3 = {3}; 
			}
			ch2_prio: coverpoint arb_vif.mon_ck.slv_prios[2] {
				bins ch_prio0 = {0}; 
				bins ch_prio1 = {1}; 
				bins ch_prio2 = {2}; 
				bins ch_prio3 = {3}; 
			}
			endgroup
		
		//测试从formatter发送出来的数据包长度是否同对应通道寄存器的配置一致
		covergroup cg_formatter_length;
			id: coverpoint fmt_vif.mon_ck.fmt_chid {
				bins ch0 = {0};
				bins ch1 = {1};
				bins ch2 = {2};
				illegal_bins illegal = default; 
			}
			length: coverpoint fmt_vif.mon_ck.fmt_length {
				bins len4  = {4};
				bins len8  = {8};
				bins len16 = {16};
				bins len32 = {32};
				illegal_bins illegal = default;
			}
		endgroup
	
		//在req拉高以后,grant至少应该在2个时钟周期后拉高,以此来模拟下行从端数据余量不足的情况
		covergroup cg_formatter_grant();
			delay_req_to_grant: coverpoint this.delay_req_to_grant {
				bins delay1 = {1};// delay_req_to_grant是一个自定义的软件信号,在task do_formater_sample()中被定义
				bins delay2 = {2};//数字代表req与grant信号之间延迟的拍数字
				bins delay3_or_more = {[3:10]};
				illegal_bins illegal = {0};//req与grant信号之间没有延迟则非法
			}
			endgroup
		
		function new(string name="mcdf_coverage");
			this.name = name;
			this.cg_mcdf_reg_write_read     = new();
			this.cg_mcdf_reg_illegal_access = new();
			this.cg_channel_disable         = new();
			this.cg_arbiter_priority        = new();
			this.cg_formatter_length        = new();
			this.cg_formatter_grant         = new();
		endfunction
		
		task run();
			fork 
				this.do_reg_sample();
				this.do_channel_sample();
				this.do_arbiter_sample();
				this.do_formater_sample();
			join
		endtask
		
		//寄存器采样
		task do_reg_sample();
			forever begin
				@(posedge reg_vif.clk iff reg_vif.rstn);
				this.cg_mcdf_reg_write_read.sample();
				this.cg_mcdf_reg_illegal_access.sample();
			end
		endtask
		
		//数据通道采样
		task do_channel_sample();
			forever begin对于每一个时钟上升沿,至少有一个channel的valid信号为1时,即有数据发送进来时,才对disable做采样
				@(posedge mcdf_vif.clk iff mcdf_vif.rstn);
				if(chnl_vifs[0].mon_ck.ch_valid===1
				|| chnl_vifs[1].mon_ck.ch_valid===1
				|| chnl_vifs[2].mon_ck.ch_valid===1)
				this.cg_channel_disable.sample();
			end
		endtask
		
		//对优先级做采样
		task do_arbiter_sample();
			forever begin对于每一个时钟的上升沿,至少有一个channel的req为1,即至少有一个channel要发送数据了,才对其优先级做采样
				@(posedge arb_vif.clk iff arb_vif.rstn);
				if(arb_vif.slv_reqs[0]!==0 || arb_vif.slv_reqs[1]!==0 || arb_vif.slv_reqs[2]!==0)
				this.cg_arbiter_priority.sample();
			end
		endtask
		
		//对grant信号做采样
		task do_formater_sample();
			fork
				forever begin//对于每一个时钟上升沿,当req为1时,对数据的长度做采样
					@(posedge fmt_vif.clk iff fmt_vif.rstn);
					if(fmt_vif.mon_ck.fmt_req === 1)
						this.cg_formatter_length.sample();
				end
				forever begin
				//当req拉高时,要通过变量delay_req_to_grant来计算延迟,对grant做采样
					@(posedge fmt_vif.mon_ck.fmt_req);
					this.delay_req_to_grant = 0;/*req信号与grant信号之间的延迟*/
					forever begin//只有grant拉高时,才对grant做采样,进而通过delay_req_to_grant的值得到延迟
						if(fmt_vif.fmt_grant === 1) begin
							this.cg_formatter_grant.sample();
							break;/*当fmt_vif.mon_ck.fmt_req信号和fmt_vif.fmt_grant信号都被拉高时,delay_req_to_grant为0,此时为非法状态,被采样*/
						end
						else begin
							@(posedge fmt_vif.clk);
							this.delay_req_to_grant++;/*每经过一拍,delay_req_to_grant加1*/
						end
					end
				end
			join
		endtask
	
		function void do_report();
			string s;
			s = "\n---------------------------------------------------------------\n";
			s = {s, "COVERAGE SUMMARY \n"}; 
			s = {s, $sformatf("  total coverage: %.1f \n", $get_coverage())}; //$get_coverage可以得到总体的全局覆盖率
			s = {s, $sformatf("  cg_mcdf_reg_write_read coverage: %.1f \n"    , this.cg_mcdf_reg_write_read.get_coverage())}; //get_coverage获取单个covergroup实例的覆盖率
			s = {s, $sformatf("  cg_mcdf_reg_illegal_access coverage: %.1f \n", this.cg_mcdf_reg_illegal_access.get_coverage())}; 
			s = {s, $sformatf("  cg_channel_disable_test coverage: %.1f \n"   , this.cg_channel_disable.get_coverage())}; 
			s = {s, $sformatf("  cg_arbiter_priority_test coverage: %.1f \n"  , this.cg_arbiter_priority.get_coverage())}; 
			s = {s, $sformatf("  cg_formatter_length_test coverage: %.1f \n"  , this.cg_formatter_length.get_coverage())}; 
			s = {s, $sformatf("  cg_formatter_grant_test coverage: %.1f \n"   , this.cg_formatter_grant.get_coverage())}; 
			s = {s, "---------------------------------------------------------------\n"};
			rpt_pkg::rpt_msg($sformatf("[%s]",this.name), s, rpt_pkg::INFO, rpt_pkg::TOP);
		endfunction
	
		virtual function void set_interface(virtual chnl_intf ch_vifs[3] 
											,virtual reg_intf reg_vif
											,virtual arb_intf arb_vif
											,virtual fmt_intf fmt_vif
											,virtual mcdf_intf mcdf_vif
										);
			this.chnl_vifs  = ch_vifs;
			this.arb_vif    = arb_vif;
			this.reg_vif    = reg_vif;
			this.fmt_vif    = fmt_vif;
			this.mcdf_vif   = mcdf_vif;
			if(chnl_vifs[0] == null || chnl_vifs[1] == null || chnl_vifs[2] == null)
				$error("chnl interface handle is NULL, please check if target interface has been intantiated");
			if(arb_vif == null)
				$error("arb interface handle is NULL, please check if target interface has been intantiated");
			if(reg_vif == null)
				$error("reg interface handle is NULL, please check if target interface has been intantiated");
			if(fmt_vif == null)
				$error("fmt interface handle is NULL, please check if target interface has been intantiated");
			if(mcdf_vif == null)
				$error("mcdf interface handle is NULL, please check if target interface has been intantiated");
		endfunction
	endclass	

04 覆盖率收集

运行完这几个test之后,得到覆盖率收集表如下

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1792750.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

uniPush2.0消息推送(云对象)

1.创建uniCloud云开发环境 关联云服务空间&#xff08;没有云空间到官网上创建&#xff09;步骤如下 2. index.obj.js代码 &#xff0c;然后上传部署 // 云对象教程: https://uniapp.dcloud.net.cn/uniCloud/cloud-obj // jsdoc语法提示教程&#xff1a;https://ask.dc…

YOLOv9改进策略 | Conv篇 | 利用YOLOv10提出的SCDown魔改YOLOv9进行下采样(附代码 + 结构图 + 添加教程)

一、本文介绍 本文给大家带来的改进机制是利用YOLOv10提出的SCDown魔改YOLOv9进行下采样,其是更高效的下采样。具体而言,其首先利用点卷积调整通道维度,然后利用深度卷积进行空间下采样。这将计算成本减少到和参数数量减少到。同时,这最大限度地保留了下采样过程中的信息,…

Windows Server FTP详解

搭建&#xff1a; Windows Server 2012R2 FTP服务介绍及搭建_windows2012server r2ftp怎么做&#xff1f;-CSDN博客 问题&#xff1a; https://www.cnblogs.com/123525-m/p/17448357.html Java使用 被动FTP&#xff08;PASV&#xff09; 被动FTP模式在数据连接建立过程中…

数学建模之MATLAB入门教程(上)

前言&#xff1a; • MATLAB是美国Math Works公司出品的商业数学软件&#xff0c;用于数据分析、无线通信、深度学习、图像处理与计算机视觉、信号处理、量化金融与风险管理、机器人&#xff0c;控制系统等领域。 • MATLAB将数值分析、矩阵计算、科学数据可视化以及非线性动…

深入分析 Android BroadcastReceiver (一)

文章目录 深入分析 Android BroadcastReceiver (一)1. Android BroadcastReceiver 设计说明1.1 BroadcastReceiver 的主要用途 2. BroadcastReceiver 的工作机制2.1 注册 BroadcastReceiver2.1.1 静态注册2.1.2 动态注册 3. BroadcastReceiver 的生命周期4. 实现和使用 Broadca…

【面试笔记】嵌入式软件工程师,汽车电子软件相关

文章目录 1. C语言基础1.1 const1.2 static1.3 回调函数的用法1.4 宏定义1.5 编译、链接过程1.6 堆与栈的区别&#xff1f;1.7 简单的字符串算法题&#xff0c;C语言实现1.7.1 给定一个字符串&#xff0c;按顺序筛选出不重复的字符组成字符串&#xff0c;输出该字符串1.7.2 给定…

三相五柱变压器饱和Simulink仿真

此示例说明了三相五柱变压器的饱和情况&#xff0c;并将专用电力系统模型与基于 Simscape 的物理模型进行了比较。 顶部电路使用 SimPowerSytems 模块来实现连接到 315 kV 电网的 300 MVA、315 kV/120 kV/25 kV、Yg/Yg/D 变压器。在不平衡电压条件下&#xff0c;与三柱变压器相…

opencv进阶 ——(十一)基于RMBG实现生活照生成寸照

实现步骤 1、检测人脸&#xff0c;可以使用opencv自带的级联分类器或者dlib实现人脸检测 2、放大人脸范围&#xff0c;调整到正常寸照尺寸 3、基于RMGB算法得到人像掩码 4、生成尺寸相同的纯色背景与当前人像进行ALPHA融合即可 alpha融合实现 void alphaBlend(cv::Mat&…

Python SMTP配置示例中如何处理发送失败?

Python SMTP的加密方式怎么设置&#xff1f;如何设置SMTP服务器&#xff1f; 在Python中&#xff0c;SMTP常被用于发送电子邮件。然而&#xff0c;SMTP配置和使用过程中&#xff0c;难免会遇到发送失败的情况。Aok将探讨在Python SMTP配置示例中如何处理这些发送失败的情况&am…

Redis实战篇——搭建主从复制

Redis实战篇——搭建主从复制 1.Redis主从1.1.主从集群结构1.2.搭建主从集群1.2.1.启动多个Redis实例1.2.2.建立集群1.2.3.测试 1.Redis主从 单节点Redis的并发能力是有上限的&#xff0c;要进一步提高Redis的并发能力&#xff0c;就需要搭建主从集群&#xff0c;实现读写分离…

探索 LLM 预训练的挑战,GPU 集群架构实战

万卡 GPU 集群实战&#xff1a;探索 LLM 预训练的挑战 一、背景 在过往的文章中&#xff0c;我们详细阐述了LLM预训练的数据集、清洗流程、索引格式&#xff0c;以及微调、推理和RAG技术&#xff0c;并介绍了GPU及万卡集群的构建。然而&#xff0c;LLM预训练的具体细节尚待进一…

安卓Zygote进程详解

目录 一、概述二、Zygote如何被启动的&#xff1f;2.1 init.zygote64_32.rc2.2 Zygote进程在什么时候会被重启2.3 Zygote 启动后做了什么2.4 Zygote启动相关主要函数 三、Zygote进程启动源码分析3.1 Nativate-C世界的Zygote启动要代码调用流程3.1.1 [app_main.cpp] main()3.1.2…

0605-JavaSE-单例模式-饿懒汉模式

​​​​​​​ 不能放在方法里面&#xff08;因为每个线程调用都会在方法里面实例化一个locker对象&#xff0c;但不属于同一个对象&#xff09;&#xff0c;然后要用static修饰成静态变量才会起到效果 //单例设计模式 //饿汉模式&#xff1a;在加载类的时候就已经开始创建 /…

Python3数据类型(新)详细介绍

文章目录 数据类型类型查看同时多个变量赋值标准数据类型1.数字(Number)2.字符串3.bool(布尔类型)4.元组元组的运算 运算运算* 运算元组的删除 5.列表(List)查找列表修改列表列表的切片列表是可以修改的列表的追加列表的插入列表的连接列表的删除列表的清空列表的复制 6.字典查…

创建常规DLL的动态链接库

本文仅供学习交流&#xff0c;严禁用于商业用途&#xff0c;如本文涉及侵权请及时联系本人将于及时删除 【例9.3】创建一个MFC 常规DLL的动态链接库Areadll&#xff0c;在该动态链接库中添加一个导出类CArea&#xff0c;通过该类获取正方形和圆的面积。 (1) 使用“MFC动态链接…

水泵选型指南

在现代暖通空调&#xff08;HVAC&#xff09;系统中&#xff0c;冷冻水泵是关键组件之一&#xff0c;它在提供冷却和空调效果方面起着至关重要的作用。选择合适的冷冻水泵不仅可以提高系统效率&#xff0c;还能节省能源和维护成本。本文将介绍冷冻水泵选型的关键因素和步骤。 …

深度学习之非极大值抑制NMS介绍

1. 基本介绍 非极大值抑制&#xff08;Non-Maximum Suppression&#xff0c;NMS&#xff09;是深度学习中一种常用的目标检测算法&#xff0c;用于在检测结果中去除冗余的边界框。 在目标检测任务中&#xff0c;通常会使用候选框&#xff08;bounding boxes&#xff09;来表示可…

wincc7.5在现有Report中增加页面

已有报告&#xff0c;页面是3页&#xff0c;需要新加1页数据。 在布局文件中&#xff0c;看到有3个项&#xff0c;这个版本的Wincc就是这种布置&#xff1a; 需要在报告内容中加入新内容&#xff0c;分页依据内容上下间的缝隙&#xff08;gap &#xff09;。 这里插入嵌入la…

SwiftUI中Menu和ControlGroup的使用

本篇文章主要介绍一下Menu组件和ControlGroup组件的使用。Menu组件是在iOS 14&#xff08;tvOS 17&#xff09;推出的一个组件&#xff0c;点击后提供一个可选择的操作列表。ControlGroup组件是一个容器视图&#xff0c;以视觉上适当的方式为给定的上下文显示语义相关的控件&am…

【设计模式深度剖析】【3】【行为型】【职责链模式】| 以购物中心客户服务流程为例加深理解

&#x1f448;️上一篇:命令模式 设计模式-专栏&#x1f448;️ 文章目录 职责链模式定义英文原话直译如何理解呢&#xff1f; 职责链模式的角色1. Handler&#xff08;抽象处理者&#xff09;2. ConcreteHandler&#xff08;具体处理者&#xff09;3. Client&#xff08;客户…