UVM实战_5_UVM中的寄存器模型

这篇具有很好参考价值的文章主要介绍了UVM实战_5_UVM中的寄存器模型。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

本章节主要介绍UVM实战第7章寄存器模型的内容


一、寄存器模型简介

通常来说,DUT中会有一组控制端口,通过控制端口,可以配置DUT中的寄存器,DUT可以根据寄存器的值来改变行为,这组控制端口就是寄存器配置总线。
暂时以uvm实战附录B代码为例,后续要改成apb总线控制的rtl代码

module dut(clk,rst_n,bus_cmd_valid,bus_op,bus_addr,bus_wr_data,bus_rd_data,rxd,rx_dv,txd,tx_en);
input          clk;
input          rst_n;
input          bus_cmd_valid;
input          bus_op;
input  [15:0]  bus_addr;
input  [15:0]  bus_wr_data;
output [15:0]  bus_rd_data;
input  [7:0]   rxd;
input          rx_dv;
output [7:0]   txd;
output         tx_en;

reg[7:0] txd;
reg tx_en;
reg invert;

always @(posedge clk) begin
   if(!rst_n) begin
      txd <= 8'b0;
      tx_en <= 1'b0;
   end
   else if(invert) begin
      txd <= ~rxd;
      tx_en <= rx_dv;
   end
   else begin
      txd <= rxd;
      tx_en <= rx_dv;
   end
end

always @(posedge clk) begin
   if(!rst_n) 
      invert <= 1'b0;
   else if(bus_cmd_valid && bus_op) begin
      case(bus_addr)
         16'h9: begin
            invert <= bus_wr_data[0];
         end
         default: begin
         end
      endcase
   end
end

reg [15:0]  bus_rd_data;
always @(posedge clk) begin
   if(!rst_n)
      bus_rd_data <= 16'b0;
   else if(bus_cmd_valid && !bus_op) begin
      case(bus_addr)
         16'h9: begin
            bus_rd_data <= {15'b0, invert};
         end
         default: begin
            bus_rd_data <= 16'b0; 
         end
      endcase
   end
end
endmodule

如上述代码,控制总线即为bus_op,bus_addr,bus_rd_data,bus_wr_data,bus_cmd_valid等,相同的控制总线如apb总线,而寄存器是invert,也就是通过控制总线来配置寄存器的值,从而改变dut的行为(上述代码33-45)

1.引入寄存器模型

上述的rtl代码,invert寄存器用于控制DUT是否将输入的激励按位取反。在取反的情况下,参考模型需要读取此寄存器的值,如果为1,那么其输出的transaction也需要进行反转。参考模型如何去读此寄存器的值呢?

  • 假设没有寄存器模型:利用config机制分别为virtual sequencer和scoreboard设置一个config_object,在此object设置一个事件,然后在scoreboard中触发,随后启动一个sequence,这个sequence发送一个transaction给bus_driver。sequence读取到寄存器后,再通过config_db传递给参考模型,还需要使用wait_modified来更新数据。--------相当麻烦!!!
  • 有寄存器模型:整个过程可以简化为:reg_model.INVERT_REG.read(status, value, UVM_FRONTDOOR);
    UVM实战_5_UVM中的寄存器模型,fpga开发

可以看出寄存器模型的优点:

  1. 可以在任何耗费时间的phase中使用寄存器模型以前门或后门进行访问,还可以在不耗费时间的phase中使用后门访问的方式来读取寄存器的值;(前门后门的概念后面会描述)
  2. 寄存器模型提供一些任务,如mirror,updata,批量完成寄存器模型与DUT中相关寄存器的交互;
  3. 重新定义验证平台与DUT的寄存器接口,使验证人员更好地组织及配置寄存器,简化流程、减少工作量;

2.寄存器模型中的基本概念

UVM实战_5_UVM中的寄存器模型,fpga开发
UVM实战_5_UVM中的寄存器模型,fpga开发

uvm_reg_field:寄存器模型中的最小单位,个人理解就是域,如上的状态寄存器共有四个域,分别是empty、full、overflow、underflow,注意reserved不是一个域,是预留的空位。
uvm_reg:它比uvm_reg_field高一个级别,但是依然是比较小的单位。这里我理解就是上面的FIFO_STATUS,就是常说的寄存器名字,它包含了好多的域。
uvm_reg_block:它是一个比较大的单位,在其中可以加入许多的uvm_reg,也可以加入其他的uvm_reg_block。一个寄存器模型中至少包含一个uvm_reg_block。
uvm_reg_map:每个寄存器在加入寄存器模型时都有其地址,uvm_reg_map就是存储这些地址,并将其转换成可以访问的物理地址(因为加入寄存器模型中的寄存器地址一般都是偏移地址,而不是绝对地址)。当寄存器模型使用前门访问方式来实现读或写操作时,uvm_reg_map就会将地址转换成绝对地址,启动一个读或写的sequence,并将读或写的结果返回。每个reg_block内部,至少有一个(通常也只有一个)uvm_reg_map.

3.简单的寄存器模型

为上节的dut建立一个简单的寄存器模型,虽然只有一个寄存器invert。建造寄存器模型首先要从uvm_reg派生一个invert类

class reg_invert extends uvm_reg;
    rand uvm_reg_field reg_data;
    virtual function void build();
        reg_data = uvm_reg_field::type_id::create("reg_data");
        // parameter: parent, size, lsb_pos, access, volatile, reset value, has_reset, is_rand, individually accessible
        reg_data.configure(this, 1, 0, "RW", 1, 0, 1, 1, 0);
    endfunction
    `uvm_object_utils(reg_invert)
    function new(input string name="reg_invert");
        //parameter: name, size, has_coverage
        super.new(name, 16, UVM_NO_COVERAGE);//16:这个宽度指的是寄存器中总共的位数。这个数字一般与系统总线的宽度一致。
    endfunction
endclass
  • new函数的写法:第二个参数是要将invert寄存器的宽度作为参数传递给super.new函数。第三个参数是是否要加入覆盖率的支持
  • build函数的写法:1) 这个build不会自动执行,需要手动调用;2) 所有的uvm_reg_field都在这里实例化; 3) reg_data实例化后,要调用reg_data.configure函数来配置这个字段,configure的参数解释如下:

1.第一个参数是此域(uvm_reg_field)的父辈,也就是此域位于哪个寄存器中,这里就是填this
2.第二个参数是此域的宽度,在上节的dut中,invert的宽度为1,所以这里就是1
3.第三个参数是此域的最低为在整个寄存器中的位置,从0开始计数
4.第四个参数表示字段的存取方式,一般有这几种:RO-只读,RW-读写,RC-读清,WC-写清,WO-只写
5.第五个参数表示是否易失的(volatile),这个参数一般不会使用
6.第六个参数表示此域上电复位后的默认值
7.第七个参数表示此域是否有复位,一般的寄存器或者寄存器的域都有上电复位值,因此这里一般都填1
8.第八个参数表示这个域是否可以随机化。主要用于对寄存器进行随机写测试
9.第九哥参数表示这个域是否可以单独存取

上面代码定义好invert寄存器后,需要在一个有reg_block派生的类中将invert类实例化

class reg_model extends uvm_reg_block;
   rand reg_invert invert;
   
   virtual function void build();
      //parameter:parameter name, base_addr, bus width(byte),large/small end,address by byte
      default_map = create_map("default_map", 0, 2, UVM_BIG_ENDIAN, 0);//这里必须用create_map来实现

      invert = reg_invert::type_id::create("invert", , get_full_name());
      invert.configure(this, null, "");
      invert.build(); //手动调用invert的build函数,将invert中的域实例化
      default_map.add_reg(invert, 'h9, "RW");//将此寄存器加入default_map中
   endfunction

   `uvm_object_utils(reg_model)
    function new(input string name="reg_model");
        super.new(name, UVM_NO_COVERAGE);
    endfunction 
endclass
  • build函数的写法:1) 实例化default_map(一个uvm_reg_block中一定要对应一个uvm_reg_map,系统已经有一个声明好的default_map);2) 在此build函数中对所有的寄存器进行实例化;

第一步实例化default map

  1. 第一个参数是名字
  2. 第二个参数是基地址
  3. 第三个参数是系统总线的宽度,这里的单位是byte而不是bit
  4. 第四个参数是大小端
  5. 第五个参数表示能否按照byte寻址

第二步实例化寄存器并调用invert.configure函数,目的是指定寄存器进行后门访问操作时的路径

  1. 第一个参数是此寄存器所在uvm_reg_block的指针,这里填this
  2. 第二个参数是reg_file的指针
  3. 第三个参数是此寄存器的后门访问路径

第三步将此寄存器加入default_map中,uvm_reg_map的作用就是存储所有寄存器的地址,因此必读加入default_map

  1. 第一个参数是要加入的寄存器
  2. 第二个参数是寄存器的地址,这里是’h9
  3. 第三个参数是此寄存器的存取方式

总结:uvm_reg_file是最小的单位,是具体存储寄存器数值的变量,可以直接使用这个类(就是上面提的reg_invert),它得继承于uvm_reg,uvm_reg是一个空壳子,就是纯虚类,不能直接使用,所以它需要派生一个新类(reg_invert)。uvm_reg_block则用于组织大量uvm_reg的大容器。

4.将寄存器模型集成到验证平台中

1) 加入adapter
寄存器的读和写本质是需要通过sequence产生一个uvm_reg_bus_op的变量,这个变量里面包含这读写类型、读写地址、写入的数据等信息,然后这些信息要交给bus_sequencer,随后交给driver,由bus_driver实现最终的前门访问读写操作。但是,这个产生的uvm_reg_bus_op是不能直接给到bus_sequencer的,需要转换一下,这就引入一个转换器:adapter

class my_adapter extends uvm_reg_adapter;
    string tID = get_type_name();

    `uvm_object_utils(my_adapter)

   function new(string name="my_adapter");
      super.new(name);
   endfunction : new

   function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
      bus_transaction tr;
      tr = new("tr"); 
      tr.addr = rw.addr;
      tr.bus_op = (rw.kind == UVM_READ) ? BUS_RD: BUS_WR;
      if (tr.bus_op == BUS_WR)
         tr.wr_data = rw.data; 
      return tr;
   endfunction : reg2bus

   function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
      bus_transaction tr;
      if(!$cast(tr, bus_item)) begin
         `uvm_fatal(tID,
          "Provided bus_item is not of the correct type. Expecting bus_transaction")
          return;
      end
      rw.kind = (tr.bus_op == BUS_RD) ? UVM_READ : UVM_WRITE;
      rw.addr = tr.addr;
      rw.byte_en = 'h3;
      rw.data = (tr.bus_op == BUS_RD) ? tr.rd_data : tr.wr_data;
      rw.status = UVM_IS_OK;
   endfunction : bus2reg

endclass : my_adapter

这个转换器定义了两个函数:reg2bus和bus2reg
reg2bus:将寄存器模型通过sequence发出的uvm_reg_bus_op型的变量转换成bus_sequencer能够接受的形式;
bus2reg:为当检测到总线上有操作时,它将收集来的transaction转换成寄存器模型能够接受的形式,以便寄存器模型能够更新相应的寄存器的值
UVM实战_5_UVM中的寄存器模型,fpga开发
实际寄存器的读和写流程如上图所示,写流程比较简单,暂且不说;读操作中,读到的数值是如何返回到寄存器模型的呢?
由于总线的特殊性,bus_driver在驱动总线进行读操作时,它也能顺便获取要读的数值,如果它将此值放入从bus_sequencer获得的bus_transaction中时,那么bus_transaction中就会有读取的值,此值经过adapter的bus2reg函数的传递,最终被寄存器模型获取,如上图读操作的虚线所示,完整流程如下:

一,参考模型调用寄存器模型的读任务;
二,寄存器模型产生sequence,并产生uvm_reg_item:rw;
三,产生driver能够接受的transaction:bus_req=adapter.reg2bus(rw);
四,把bus_req交给bus_sequencer;
五,driver得到bus_req后驱动它,得到读取的值,并将读取值放入bus_req[rep]中,调用item_done;
六,寄存器模型调用adapter.bus2reg(bus_req[rep], rw)将bus_req中的读取值传递给rw;
七,将rw中的读数据返回参考模型

如果driver一直发送应答而sequence不收集应答,那么将会导致sequencer的应答队列溢出,UVM在adpter中设置了provide_responses选项,寄存器在调用bus2reg将目标transaction转换成uvm_reg_item时,其传入的参数是rep,而不是req,见上述步骤中括号内[ ];

2) 加入寄存器模型
在base_test中加入寄存器模型:

class base_test extends uvm_test;

   my_env         env;
   my_vsqr        v_sqr;
   reg_model      rm;  //加入的寄存器模型
   my_adapter     reg_sqr_adapter;  //加入的adapter

   function new(string name = "base_test", uvm_component parent = null);
      super.new(name,parent);
   endfunction
   
   extern virtual function void build_phase(uvm_phase phase);
   extern virtual function void connect_phase(uvm_phase phase);
   extern virtual function void report_phase(uvm_phase phase);
   `uvm_component_utils(base_test)
endclass


function void base_test::build_phase(uvm_phase phase);
   super.build_phase(phase);
   env  =  my_env::type_id::create("env", this); 
   v_sqr =  my_vsqr::type_id::create("v_sqr", this);
   rm = reg_model::type_id::create("rm", this);
   rm.configure(null, "");
   rm.build();
   rm.lock_model();
   rm.reset();
   reg_sqr_adapter = new("reg_sqr_adapter");
   env.p_rm = this.rm;//为env的p_rm赋值
endfunction

function void base_test::connect_phase(uvm_phase phase);
   super.connect_phase(phase);
   v_sqr.p_my_sqr = env.i_agt.sqr;
   v_sqr.p_bus_sqr = env.bus_agt.sqr;
   v_sqr.p_rm = this.rm;
   rm.default_map.set_sequencer(env.bus_agt.sqr, reg_sqr_adapter);
   rm.default_map.set_auto_predict(1);
endfunction

要将一个寄存器模型集成到base_test中,那么至少需要在base_test中定义两个成员变量,reg_model和reg_sqr_adapter

build函数:reg_model和reg_sqr_adapter都要在build_phase中实例化,

reg_model
第一是使用type_id来实例化reg_model
第二个调用configure函数,第一个参数是parent block,由于是最顶层的reg_block,因此填写null,第二个参数是后门访问路径
第三个调用build函数,将所有的寄存器实例化
第四个是调用lock_model函数,调用此函数后,reg_model中就不能再加入新的寄存器了
第五个是调用reset函数,如果不调用此函数,那么reg_model中所有寄存器的值都是0,调用此函数后,所有寄存器的值都将变为设置的复位值

adapter
调用new函数

connect函数:将转换器和bus_sequencer通过set_sequencer函数告知reg_model的default_map,并将default_map设置为自动预测状态

3) 在验证平台中使用寄存器模型
寄存器模型建好后,就可以在sequence或者其他component中使用,下面举例在reference model中使用为例子:

env:上节的base_test已经为env的p_rm赋值,因此在env只需要将p_rm传递给reference model

class my_env extends uvm_env;
   reg_model  p_rm;
   ...
function void my_env::connect_phase(uvm_phase phase);
   super.connect_phase(phase);
   ...
   mdl.p_rm = this.p_rm;//传递给reference model
endfunction
endclass

reference model:设置一个寄存器模型指针

class my_model extends uvm_component;
   reg_model p_rm; //在参考模型中要有一个寄存器模型的指针
   ...
task my_model::main_phase(uvm_phase phase);
   my_transaction tr;
   my_transaction new_tr;
   uvm_status_e status;
   uvm_reg_data_t value;
   super.main_phase(phase);
   p_rm.invert.read(status, value, UVM_FRONTDOOR);//读操作
   while(1) begin
      port.get(tr);
      new_tr = new("new_tr");
      new_tr.copy(tr);
      //`uvm_info("my_model", "get one transaction, copy and print it:", UVM_LOW)
      //new_tr.print();
      if(value)
         invert_tr(new_tr);
      ap.write(new_tr);
   end
endtask
endclass

3) 寄存器模型的两个基本任务:read和write

read任务原型:

extern virtual task read(output uvm_status_e	status,
						output	uvm_reg_data_t	value,
						input	uvm_path_e		path=UVM_DEFAULT_PATH,
						input	uvm_reg_map		map=null,
						input	uvm_sequence_base	parent=null,
						input	int				paior=-1,
						input	uvm_object		extension=null,
						input	string			fname="",
						input	int				lineno=0);

第一个是uvm_status_e型的变量,这是一个输出,用于表明读操作是否成功;
第二个是读取的数值,也是一个输出;
第三个是读取的方式,可选UVM_FRONTDOOR和UVM_BACKDOOR;

write任务原型:
由于参考模型一般不会写寄存器,因此对于write任务,以在virtual sequence进行写操作为例说明:
在sequence中使用寄存器模型,通常通过p_sequencer的形式引用。需要首先在sequencer中有一个寄存器模型的指针,base_test中已经为v_sqr.p_rm赋值了。因此可以直接以如下方式进行写操作:

class case0_cfg_vseq extends uvm_sequence;
   `uvm_object_utils(case0_cfg_vseq)
   `uvm_declare_p_sequencer(my_vsqr)
   ...
   virtual task body();
      uvm_status_e   status;
      uvm_reg_data_t value;
      ...
      p_sequencer.p_rm.invert.read(status, value, UVM_FRONTDOOR);
      `uvm_info("case0_cfg_vseq", $sformatf("invert's initial value is %0h", value), UVM_LOW)
      p_sequencer.p_rm.invert.write(status, 1, UVM_FRONTDOOR);
      p_sequencer.p_rm.invert.read(status, value, UVM_FRONTDOOR);
      `uvm_info("case0_cfg_vseq", $sformatf("after set, invert's value is %0h", value), UVM_LOW)
      ...
   endtask
endclass

第一个是uvm_status_e型的变量,这是一个输出,用于表明写操作是否成功;
第二个是要写的值,也是一个输入;
第三个是写操作的方式,可选UVM_FRONTDOOR和UVM_BACKDOOR;

二、后门访问与前门访问

1.什么叫前门访问?什么叫后门访问

前门访问:通过模拟cpu在总线上发出读指令,进行读写操作。在这个过程中,仿真时间($time函数得到的时间)是一直往前走的。
后门访问:不通过总线进行读写操作,而是直接通过层次化的引用来改变寄存器的值。广义来说,所有不通过DUT的总线而对DUT内部的寄存器或者存储器进行存取的操作都是后门访问操作。

2.后门访问的优缺点

后门访问优点
1.后门访问操作能够更好地完成前门访问操作所做的事情。后门访问不消耗仿真时间,与前门访问操作相比,它消耗的运行时间要远小于前门访问操作的运行时间。在一个大型芯片的验证中,在其正常工作前需要配置众多的寄存器,配置时间可能要达到一个或几个小时,而如果使用后门访问操作,则时间可能缩短为原来的1/100。
2. 后门访问操作能够完成前门访问操作不能完成的事情。如在网络通信系统中,计数器通常都是只读的(有一些会附加清零功能),无法对其指定一个非零的初值。而大部分计数器都是多个加法器的叠加,需要测试它们的进位操作。在实际应用中,可能要几万个或者更多的时钟才会加1,因此需要大量的运行时间。这种情况下,后门访问操作能够完成前门访问操作完成的事情,给只读的寄存器一个初值。

后门访问劣势
所有的前门访问操作都可以在波形文件中找到总线信号变化的波形及所有操作的记录。但是后门访问操作则无法在波形文件中找到操作痕迹。其操作记录只能仰仗验证平台编写者在进行后门访问操作时输出的打印信息,这样便增加了调试的难度。

3.使用interface进行后门访问操作

如果不想使用寄存器模型提供的后门访问,或者没有建立寄存器模型,同时又必须对DUT中的一个寄存器或者一块存储器进行后门访问,可以选择使用interface来进行后门访问操作。

后门interface:

interface backdoor_if(input clk, input rst_n);

   function void poke_counter(input bit[31:0] value);
      top_tb.my_dut.counter = value;
   endfunction

   function void peek_counter(output bit[31:0] value);
      value = top_tb.my_dut.counter;
   endfunction
endinterface

poke_counter为后门写,peek_counter为后门读,测试用例中可以直接调用此函数:

task my_case0::configure_phase(uvm_phase phase);
   phase.raise_objection(this);
   @(posedge vif.rst_n);
   vif.poke_counter(32'hFFFD);
   phase.drop_objection(this);
endtask

4.UVM中后门访问操作的实现:DPI+VPI

首先,在system verilog中需要使用如下的方式,将在C/C++中定义的函数导入:

import “DPI-C” context function int uvm_hdl_read(string path, output uvm_hdl_data_t value);

然后就可以在SV中像普通函数调用uvm_hdl_read函数,如:

uvm_hdl_read("top_tb.my_dut.counter", value);

整个过程如下:

UVM实战_5_UVM中的寄存器模型,fpga开发
总结:
1). 在建立寄存器模型时将路径参数设置好
2). 在进行后门访问的写操作时,寄存器模型调用uvm_hdl_deposit函数
3). 进行后门访问的读操作时,调用uvm_hdl_read函数

5.寄存器模型的后门访问功能

准备一:reg_block中调用uvm_reg的configure函数时,设置好第三个路径参数:

class reg_model extends uvm_reg_block;
   rand reg_invert invert;
   rand reg_counter_high counter_high;
   rand reg_counter_low counter_low;

   virtual function void build();
      default_map = create_map("default_map", 0, 2, UVM_BIG_ENDIAN, 0);

      invert = reg_invert::type_id::create("invert", , get_full_name());
      invert.configure(this, null, "invert");
      invert.build();
      default_map.add_reg(invert, 'h9, "RW");
      counter_high = reg_counter_high::type_id::create("counter_high", , get_full_name());
      counter_high.configure(this, null, "counter[31:16]"); //第三个参数,即为具体到信号的路径
      counter_high.build();
      default_map.add_reg(counter_high, 'h5, "RW");
      counter_low = reg_counter_low::type_id::create("counter_low", , get_full_name());
      counter_low.configure(this, null, "counter[15:0]");//第三个参数,即为具体到信号的路径
      counter_low.build();
      default_map.add_reg(counter_low, 'h6, "RW");
   endfunction

   `uvm_object_utils(reg_model)

    function new(input string name="reg_model");
        super.new(name, UVM_NO_COVERAGE);
    endfunction 

endclass

准备二:在将寄存器模型集成到验证平台时,需要设置好根路径hdl_root:

function void base_test::build_phase(uvm_phase phase);
   super.build_phase(phase);
   env  =  my_env::type_id::create("env", this); 
   v_sqr =  my_vsqr::type_id::create("v_sqr", this);
   rm = reg_model::type_id::create("rm", this);
   rm.configure(null, "");
   rm.build();
   rm.lock_model();
   rm.reset();
   rm.set_hdl_path_root("top_tb.my_dut");//设置好根路径
   reg_sqr_adapter = new("reg_sqr_adapter");
   env.p_rm = this.rm;
endfunction

两种后门访问函数:read和write、peek和poke
read和write:UVM_BACKDOOR形式的read和write,该方法会在进行操作时模仿DUT的行为,如对只读的寄存器进行写操作,是写不进去的
peek和poke:peek对应着写操作,poke对应着读操作,该方法不管DUT的行为,即使是只读的寄存器,也可以把数值写进去

举例如下:

class case0_cfg_vseq extends uvm_sequence;

   `uvm_object_utils(case0_cfg_vseq)
   `uvm_declare_p_sequencer(my_vsqr)
   
   function  new(string name= "case0_cfg_vseq");
      super.new(name);
   endfunction 
   
   virtual task body();
      uvm_status_e   status;
      uvm_reg_data_t value;
      bit[31:0] counter;
      if(starting_phase != null) 
         starting_phase.raise_objection(this);
      p_sequencer.p_rm.invert.read(status, value, UVM_FRONTDOOR);
      `uvm_info("case0_cfg_vseq", $sformatf("invert's initial value is %0h", value), UVM_LOW)
      p_sequencer.p_rm.invert.write(status, 1, UVM_FRONTDOOR);
      p_sequencer.p_rm.invert.read(status, value, UVM_FRONTDOOR);
      `uvm_info("case0_cfg_vseq", $sformatf("after set, invert's value is %0h", value), UVM_LOW)
      p_sequencer.p_rm.counter_low.read(status, value, UVM_FRONTDOOR);
      counter[15:0] = value[15:0];
      p_sequencer.p_rm.counter_high.read(status, value, UVM_FRONTDOOR);
      counter[31:16] = value[15:0];
      `uvm_info("case0_cfg_vseq", $sformatf("counter's initial value(FRONTDOOR) is %0h", counter), UVM_LOW)
      p_sequencer.p_rm.counter_low.poke(status, 16'hFFFD);//这里这里这里,poke操作
      p_sequencer.p_rm.counter_low.read(status, value, UVM_FRONTDOOR);
      counter[15:0] = value[15:0];
      p_sequencer.p_rm.counter_high.read(status, value, UVM_FRONTDOOR);
      counter[31:16] = value[15:0];
      `uvm_info("case0_cfg_vseq", $sformatf("after poke, counter's value(FRONTDOOR) is %0h", counter), UVM_LOW)
      p_sequencer.p_rm.counter_low.peek(status, value);//这里这里这里,peek操作
      counter[15:0] = value[15:0];
      p_sequencer.p_rm.counter_high.peek(status, value);
      counter[31:16] = value[15:0];
      `uvm_info("case0_cfg_vseq", $sformatf("after poke, counter's value(BACKDOOR) is %0h", counter), UVM_LOW)
      if(starting_phase != null) 
         starting_phase.drop_objection(this);
   endtask
endclass

三、复杂的寄存器模型

1.层次的寄存器模型

前面几节介绍的寄存器模型比较简单,只有一个寄存器模型(invert),这个寄存器加入了uvm_reg_block,然后在base_test中实例化。现实中是这样的,举例如下:一个DUT分了三个子模块,1.global模块(0x0000-0x0FFF) 2.buf模块(0x1000-0x1FFF) 3.mac模块(0x2000-0x2FFF),这样的话,会定义mac_block,buf_block,global_block,每个模块都有若干个寄存器,寄存器分别加入到各自的block中,最后这些block再加入到一个大的uvm_reg_block中,最后在base_test实例化此block。如下图所示:
UVM实战_5_UVM中的寄存器模型,fpga开发
代码如下:

class global_blk extends uvm_reg_block;
   rand reg_invert invert;
   
   virtual function void build();
      default_map = create_map("default_map", 0, 2, UVM_BIG_ENDIAN, 0);

      invert = reg_invert::type_id::create("invert", , get_full_name());
      invert.configure(this, null, "");
      invert.build();
      default_map.add_reg(invert, 'h9, "RW");
   endfunction

    `uvm_object_utils(global_blk)

    function new(input string name="global_blk");
        super.new(name, UVM_NO_COVERAGE);
    endfunction 
endclass

class buf_blk extends uvm_reg_block;
...
endclass

class mac_blk extends uvm_reg_block;
...
endclass

class reg_model extends uvm_reg_block;

   rand global_blk gb_ins;
   rand buf_blk    bb_ins;
   rand mac_blk    mb_ins;

   virtual function void build();
      default_map = create_map("default_map", 0, 2, UVM_BIG_ENDIAN, 0);
      gb_ins = global_blk::type_id::create("gb_ins");
      gb_ins.configure(this, "");
      gb_ins.build();
      gb_ins.lock_model();
      default_map.add_submap(gb_ins.default_map, 16'h0);

      bb_ins = buf_blk::type_id::create("bb_ins");
      bb_ins.configure(this, "");
      bb_ins.build();
      bb_ins.lock_model();
      default_map.add_submap(bb_ins.default_map, 16'h1000);

      mb_ins = mac_blk::type_id::create("mb_ins");
      mb_ins.configure(this, "");
      mb_ins.build();
      mb_ins.lock_model();
      default_map.add_submap(mb_ins.default_map, 16'h2000);

   endfunction
   
   `uvm_object_utils(reg_model)
    function new(input string name="reg_model");
        super.new(name, UVM_NO_COVERAGE);
    endfunction 
endclass

上面的这个reg_model相当于一个大的uvm_reg_block,这个类里面是要将子reg_block加入到父reg_block中,有以下几步:

  1. 先实例化子reg_block
  2. 第二调用子reg_block的configure函数,如果需要使用后门访问,需要说明子reg_block相对于父reg_block来说的路径
  3. 第三调用子reg_block的build函数
  4. 第四调用子reg_block的lock_model函数
  5. 第五将子reg_block的default_map以子map的形式加入父reg_block的default_map中,可以和父reg_block的基地址组成完整地址

2.reg_file的作用

reg_file的引用主要是用于区分不同的hdl路径。还是举例子说明:
假设有两个寄存器regA和regB,hdl路径分别是top_tb.mac_reg.fileA.regA和top_tb.mac_reg.fileB.regB,基地址还是0x2000,那么在设置路径的时候是这样设置的:

//顶层reg_block
class reg_model extends uvm_reg_block;
   rand mac_blk    mb_ins;

   virtual function void build();
      default_map = create_map("default_map", 0, 2, UVM_BIG_ENDIAN, 0);
      ...
      mb_ins = mac_blk::type_id::create("mb_ins");
      mb_ins.configure(this, "mac_reg"); //configure中设置“子reg_block相对于父reg_block来说的路径”
      mb_ins.build();
      mb_ins.lock_model();
      default_map.add_submap(mb_ins.default_map, 16'h2000);
   endfunction
   ... 
endclass
//子reg_block
class mac_blk extends uvm_reg_block;
   virtual function void build();
      ...
      regA.configure(this, null, "fileA.regA");
	  ...
	  regB.configure(this, null, "fileA.regA");
	  ...
   endfunction
   ...
endclass

如果寄存器不多,这种写法没问题,但是假如fileA中有几十个寄存器,那么fileA.*会几十次的出现这几十个寄存器的configure函数例。一旦fileA有改动,那么这几十行都要改,这个过程比较容易出错,所以,引入了uvm_reg_file的概念。如下所示:

class regfile extends uvm_reg_file;
   function new(string name = "regfile");
      super.new(name);
   endfunction

   `uvm_object_utils(regfile)
endclass

class mac_blk extends uvm_reg_block;

   rand regfile file_a;//声明fileA
   rand regfile file_b;//声明fileB
   rand reg_regA regA;
   rand reg_regB regB;
   rand reg_vlan vlan;
   
   virtual function void build();
      default_map = create_map("default_map", 0, 2, UVM_BIG_ENDIAN, 0);

      file_a = regfile::type_id::create("file_a", , get_full_name());//实例化fileA
      file_a.configure(this, null, "fileA");
      file_b = regfile::type_id::create("file_b", , get_full_name());//实例化fileB
      file_b.configure(this, null, "fileB");
      
      regA = reg_regA::type_id::create("regA", , get_full_name());
      regA.configure(this, file_a, "regA");
      regA.build();
      default_map.add_reg(regA, 'h31, "RW");
      
      regB = reg_regB::type_id::create("regB", , get_full_name());
      regB.configure(this, file_b, "regB");
      regB.build();
      default_map.add_reg(regB, 'h32, "RW");
      ...
   endfunction
   ...
endclass

第一:先从uvm_reg_file派生一个类
第二:在mac_blk中实例化此类
第三:调用configure函数,第一个参数是其所在reg_block的指针,在这里直接填this;第二个参数是假设此reg_file是另外一个reg_file的父文件,那么这里就填写其父reg_file的指针,由于这里只有这一级reg_file,因此填null;第三个参数是此reg_file的hdl路径,当爸reg_file定义好后,在调用寄存器的configure参数时,就可以将其第二个参数设为reg_file的指针

加入了reg_file的概念后,当fileA名称变化时,只需要将file_a的configure参数值改变一下即可

3.多个域的寄存器

看到这,也很崩溃啊,遇到的寄存器哪有一个域的,这里讲到如果一个寄存器有多个域,它的写法和第一节有些许的不同,具体由哪写地方不同呢,还是举例:

加入一个寄存器有三个域,分别是低两位的filedA,三位的filedB,四位的filedC,其余位未使用。这种情况从逻辑上看是一个寄存器,从物理上看,即在DUT中是三个寄存器,因此这一个寄存器对应着三个不同的hdl路径:fieldA、fieldB、fieldC。(实际中,有三个不同路径的域,还放同一个寄存器里么?后面再研究),代码如下:

class three_field_reg extends uvm_reg;
    rand uvm_reg_field fieldA;
    rand uvm_reg_field fieldB;
    rand uvm_reg_field fieldC;

    virtual function void build();
        fieldA = uvm_reg_field::type_id::create("fieldA");
        fieldB = uvm_reg_field::type_id::create("fieldB");
        fieldC = uvm_reg_field::type_id::create("fieldC");
    endfunction
    ...
endclass

class mac_blk extends uvm_reg_block;

   rand reg_vlan vlan;
   rand three_field_reg tf_reg;
   
   virtual function void build();
      default_map = create_map("default_map", 0, 2, UVM_BIG_ENDIAN, 0);

      vlan = reg_vlan::type_id::create("vlan", , get_full_name());
      vlan.configure(this, null, "vlan");
      vlan.build();
      default_map.add_reg(vlan, 'h40, "RW");
      
      tf_reg = three_field_reg::type_id::create("tf_reg", , get_full_name());
      tf_reg.configure(this, null, "");
      tf_reg.build();
      tf_reg.fieldA.configure(tf_reg, 2, 0, "RW", 1, 0, 1, 1, 1);
      tf_reg.add_hdl_path_slice("fieldA", 0, 2);
      tf_reg.fieldB.configure(tf_reg, 3, 2, "RW", 1, 0, 1, 1, 1);
      tf_reg.add_hdl_path_slice("fieldA", 2, 3);
      tf_reg.fieldC.configure(tf_reg, 4, 5, "RW", 1, 0, 1, 1, 1);
      tf_reg.add_hdl_path_slice("fieldA", 5, 4);
      default_map.add_reg(tf_reg, 'h41, "RW");
   endfunction
   ...
endclass

第一:先从uvm_reg派生一个类,在此类中加入3个uvm_reg_field。
第二:在reg_block中将此类实例化,
第三:调用tf_reg.configure,要注意,最后一个代表hdl路径的参数已经变为了空的字符串,
第四:调用tf_reg.build
第五:调用tf_reg.fieldA的configure函数。
第六:将fieldA的hdl路径加入tf_reg中,此时用到的函数是add_hdl_path_slice。这个函数的第一个参数是要加入的路径,第二个参数则是此路径对应的域在此寄存器中的起始位数,如fieldA是从0开始的,而fieldB是从2开始的,第三个参数则是此路径对应的域的位宽。

4.多个地址的寄存器

什么是多个地址的寄存器?比如前面的counter,总线宽度是16,但是counter的位宽是32bit,占据了两个地址,前面的例子的解决方式是将一个寄存器分成了两份,每次读counter的值时,需要对counter_low和counter_high各进行读一次,然后再拼接。有点不方便,但非常好理解。

这里给出了另一个解决方法:使一个寄存器占据多个地址

class reg_counter extends uvm_reg;

    rand uvm_reg_field reg_data;

    virtual function void build();
        reg_data = uvm_reg_field::type_id::create("reg_data");
        // parameter: parent, size, lsb_pos, access, volatile, reset value, has_reset, is_rand, individually accessible
        reg_data.configure(this, 32, 0, "W1C", 1, 0, 1, 1, 0);//指明寄存器大小是32bit
    endfunction
    ...
endclass

class reg_model extends uvm_reg_block;
   rand reg_counter counter;

   virtual function void build();
      default_map = create_map("default_map", 0, 2, UVM_BIG_ENDIAN, 0);
      ...
      counter= reg_counter::type_id::create("counter", , get_full_name());
      counter.configure(this, null, "counter");
      counter.build();
      default_map.add_reg(counter, 'h5, "RW");//指定一个最小的地址即可
   endfunction
   ...
endclass

需要注意几个点:
第一:定义reg_counter,并在构造函数中要指明此寄存器的大小是32位
第二:再reg_model中调用default_map的add_reg函数时,要指定寄存器的地址,这里只需要指明一个最小的地址即可。因为前面实例化default_map时,已经指明了使用UVM_LITTLE_ENDIAN形式,同时总线宽度是2byte,UVM会自动根据这些信息计算出此寄存器占据两个地址。当使用前门访问读写此寄存器时,寄存器模型会进行了两次读写操作,即发出两个transaction,这两个transaction对应的读写操作的地址从0x05递增到0x06

5.加入存储器

例如,一个DUT的功能是接收一种数据,它经过处理(操作A)后将数据存储在存储器(DUT)中,并没有为其分配地址。当存储器中的数据达到一定量时,将它们读出,并再另外做一些处理(操作B)后发送出去。在验证平台中如果只是将DUT输出接口的数据与期望值相比较,当数据不匹配情况出现时,则无法确定问题是出在操作A还是操作B中,如下图所示。
UVM实战_5_UVM中的寄存器模型,fpga开发
但是,如果在输出接口之前再增加一级比较,就可以快速地定位问题所在了,如下图所示
UVM实战_5_UVM中的寄存器模型,fpga开发
因此就需要在寄存器模型也加一块存储器,用来存放操作A处理得到的数据。比如数据总线宽度是16,要加一块512x32的存储器:

class my_memory extends uvm_mem;
   function new(string name="my_memory");
      super.new(name, 512, 32);//指明宽度是32,总线宽是16
   endfunction
   `uvm_object_utils(my_memory)
endclass

class reg_model extends uvm_reg_block;
   rand my_memory mm;

   virtual function void build();
      default_map = create_map("default_map", 0, 2, UVM_BIG_ENDIAN, 0);
      ...
      mm = my_memory::type_id::create("mm", , get_full_name());
      mm.configure(this, "stat_blk.ram512x32_inst.array");
      default_map.add_mem(mm, 'h100);
   endfunction
   ...
endclass

第一:由uvm_mem派生一个类my_memory,在其new函数中调用super.new函数。这个函数有三个参数,第一个是名字,第二个是存储器的深度,第三个是宽度
第二:在reg_model的build函数中,将存储器实例化,调用其configure函数,第一个参数是所在reg_block的指针,第二个参数是此块存储器的hdl路径
第三:最后调用default_map.add_mem函数,将此块存储器加入default_map中,从而可以对其进行前门访问操作。如果没有对此块存储器分配地址空间,那么这里可以不将其加入default_map中。在这种情况下,只能使用后门访问的方式对其进行访问。

注意:由于总线宽度是16,而这里mem的宽度是32,所以,这里加入的存储器的一个单元占据两个物理地址,共占据1024个地址。那么当使用read、write、peek、poke时,输入的参数offset代表实际的物理地址偏移还是某一个存储单元偏移呢?答案是存储单元偏移。在访问这块512×32的存储器时,offset的最大值是511,而不是1023。当指定一个offset,使用前门访问操作读写时,由于一个offset对应的是两个物理地址,所以寄存器模型会在总线上进行两次读写操作。
UVM实战_5_UVM中的寄存器模型,fpga开发

四、寄存器模型对DUT的模拟

1.期望值和镜像值

镜像值:由于DUT中寄存器的值是实时变更的,对于任意一个寄存器,寄存器模型中都会有一个专门的变量用于最大可能地与DUT保持同步,这个变量在寄存器模型中称为DUT的镜像值(mirrored value)
期望值:如果目前DUT中invert的值为’h0,寄存器模型中的镜像值也为’h0,通过set函数将期望值设置为’h1(此时镜像值依然为0),之后调用update任务,update任务会检查期望值和镜像值是否一致,如果不一致,那么将会把期望值写入DUT中,并且更新镜像值

class case0_cfg_vseq extends uvm_sequence;
   ...
   virtual task body();
      ...
      p_sequencer.p_rm.invert.set(16'h1);//set函数写期望值
      value = p_sequencer.p_rm.invert.get();//get函数得到期望值
      value = p_sequencer.p_rm.invert.get_mirrored_value();//得到镜像值
      p_sequencer.p_rm.invert.update(status, UVM_FRONTDOOR);//updata函数
      value = p_sequencer.p_rm.invert.get();
      value = p_sequencer.p_rm.invert.get_mirrored_value();
      p_sequencer.p_rm.invert.peek(status, value);
      ...
   endtask
endclass

一些操作对期望值和镜像值的影响:
read/write:无论通过后门访问还是前门访问的方式从DUT中读取或写入寄存器的
值,在操作完成后,寄存器模型都会根据读写的结果更新期望值和镜像值(二者相等)
peek/poke:在操作完成后,寄存器模型会根据操作的结果更新期望值和镜像值(二者相等)
get/set:set操作会更新期望值,但是镜像值不会改变。get操作会返回寄存器模型中当前寄存器的期望值
update:updata会检查寄存器的期望值和镜像值是否一致,如果不一致,那么就会将期望值写入DUT中,并且更新镜像值,使其与期望值一致。每个由uvm_reg派生来的类都会有update操作。每个由uvm_reg_block派生来的类也有update操作,它会递归地调用所有加入此reg_block的寄存器的update任务

2.set_auto_predict和reg_predictor

set_auto_predict:上节的读操作的返回值(通过driver),寄存器模型会把返回值更新到镜像值和期望值。这个功能被称为寄存器模型的auto predict功能,在需要打开此功能

rm.default_map.set_auto_predict(1);

reg_predictor:monitor将从总线上收集到的transaction交给寄存器模型,后者更新相应寄存器的值。这种方法具体如何实现,请看UVM实战7.7.1

3.mirror和predict

mirror操作用于读DUT中的寄存器,并将它们更新到寄存器模型中。如果发现DUT中寄存器的值与寄存器模型中的镜像值不一样,那么在更新寄存器模型之前会给出错误提示,函数原型如下:

task uvm_reg::mirror(output uvm_status_e    status,
				input	uvm_check_e			check = UVM_CHECK,//如果是UVM_NO_CHECK,则不会有错误提示
				input	uvm_path_e			path = UVM_DEFAULT_PATH,
				...);

调用位置:uvm_reg级别:check具体的寄存器。uvm_reg_block级别:调用加入其中的所有寄存器的mirror

predict:执行mirror会从DUT读数据,然后和镜像值做对比,那这个镜像值怎么来的?–用predict,函数原型为:

function bit uvm_reg::predict (uvm_reg_data_t		value,
							uvm_reg_byte_en_t 		be = -1,
							uvm_predict_e			kind = UVM_PREDICT_DIRECT,
							uvm_path_e				path = UVM_FRONTDOOR,
							...); 

第一个参数:要预测的值
第二个参数:byte_en,默认-1的意思是全部有效,
第三个参数:预测的类型,有三种,分别是UVM_PREDICT_(DIRECT / REDA / WRITE),默认是DIRECT,即参考模型中更新寄存器模型的镜像值,而不影响DU的值
第四个参数:后门访问后者前门访问

task my_model::main_phase(uvm_phase phase);
   ...
   p_rm.invert.read(status, value, UVM_FRONTDOOR);
   while(1) begin
      port.get(tr);
      new_tr = new("new_tr");
      new_tr.copy(tr);
      //`uvm_info("my_model", "get one transaction, copy and print it:", UVM_LOW)
      //new_tr.print();
      if(value)
         invert_tr(new_tr);
      counter = p_rm.counter.get();
      length = new_tr.pload.size() + 18;
      counter = counter + length;
      p_rm.counter.predict(counter); //将计算后的counter值更新到寄存器模型
      ap.write(new_tr);
   end
endtask

3.寄存器模型的随机化

从uvm_reg_field和uvm_reg的定义上看,都是rand型的,所以,这些可以支持randomize,如下:

assert(rm.randomize());
assert(rm.invert.randomize());
assert(rm.invert.reg_data.randomize());

除了进行assert,还需要在调用configure函数时,在第八个参数写1,还需要这个field中有写的操作,不能是只读的。

3.get_reg_by_offset

如果要使用地址来访问寄存器模型,那么可以使用get_reg_by_offset函数,通过寄存器的地址得到一个uvm_reg的指针,再调用uvm_reg的read/write就可以进行读写操作


总结

提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。文章来源地址https://www.toymoban.com/news/detail-818108.html

到了这里,关于UVM实战_5_UVM中的寄存器模型的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

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

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

相关文章

  • 使用FPGA实现桶形移位寄存器

    我给大家介绍的是逻辑/算术左移移位寄存器。实现的功能是根据输入信号shift将输入信号进行移位,高位移除,低位补0。我建立的工程是由3个独立的桶形移位寄存器组成的。 library ieee; use ieee.std_logic_1164.all; entity barrel is      port( inp : in std_logic_vector(7 downto 0);          

    2024年04月29日
    浏览(31)
  • 【汇编中的寄存器分类与不同寄存器的用途】

    寄存器分类 在计算机体系结构中,8086CPU,寄存器可以分为以下几类: 1. 通用寄存器: 通用寄存器是用于存储数据和执行算术运算的寄存器。在 x86 架构中,这些通用寄存器通常包括 AX、BX、CX、DX、SI、DI、BP 和 SP。其中,AX、BX、CX 和 DX 寄存器可以分别作为累加器(accumulat

    2024年02月09日
    浏览(41)
  • FPGA原理与结构——移位寄存器(Shift Registers)

    系列文章目录:FPGA原理与结构(0)——目录与传送门 目录 一、移位寄存器概述 1、基本概念      2、LUT实现移位寄存器 3、移位寄存器的应用 4、移位寄存器的功能 5、移位寄存器结构 6、移位寄存器级连 二、移位寄存器数据流 1、动态读操作(移位长度不固定) 2、静态读

    2024年02月12日
    浏览(85)
  • FPGA的配置状态字寄存器Status Register

    目录 简介 状态字定义       Unknown Device/Many Unknow Devices 解决办法 一般原因  Xilinx的FPGA有多种配置接口,如SPI,BPI,SeletMAP,Serial,JTAG等;如果从时钟发送者的角度分,还可以分为主动Master(即由FPGA自己发送配置时钟信号CCLK)和被动Slave(即由外部器件提供配置所需要的时

    2024年04月25日
    浏览(27)
  • FPGA的可编程逻辑单元(LUT和寄存器)

    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言 一、pandas是什么? 二、使用步骤 1.引入库 2.读入数据 总结 1.根据PLD器件单片集成度的高低,可将PLD分为低密度可编程逻辑器件和高密度可编程逻辑器件。 2.按器件结构类型划分      

    2024年02月19日
    浏览(38)
  • FPGA原理与结构(5)——移位寄存器(Shift Registers)

    系列文章目录:FPGA原理与结构(0)——目录与传送门 目录 一、移位寄存器概述 1、基本概念      2、LUT实现移位寄存器 3、移位寄存器的应用 4、移位寄存器的功能 5、移位寄存器结构 6、移位寄存器级连 二、移位寄存器数据流 1、动态读操作(移位长度不固定) 2、静态读

    2024年02月08日
    浏览(28)
  • 寄存器模型

    寄存器配置总线:通过控制端口,配置DUT中的寄存器,DUT可以根据寄存器的值来改变其行为。 uvm_reg_field:寄存器模型中最小的单位是具体存储寄存器数值的变量。 uvm_reg:比uvm_reg_field高一个级别,但依然是比较小的单位。下图为uvm_reg_field与uvm_reg_的关系: uvm_reg_block:一个比

    2024年02月02日
    浏览(29)
  • ARM中的寄存器

    ARM工作模式 ARM有8个基本的工作模式 User 非特权模式,一般在执行上层的应用程序时ARM处于该模式 FIQ 当一个高优先级中断产生后ARM将进入这种模式 IRQ 当一个低优先级中断产生后ARM将进入这种模式 SVC 当复位或执行软中断指令后ARM将进入这种模式 Abort 当产生存取异常时ARM将进

    2024年02月03日
    浏览(29)
  • FPGA中锁存器(latch)、触发器(flip-flop)以及寄存器(register)详解

    1 定义 1.1 锁存器(latch)     锁存器是一种由电平触发的存储单元,为异步电路,数据存储的动作取决于输入信号的电平值,只要输入发生变化,输出即随之发生变化。 1.2 触发器(flip-flop)     触发器是边沿敏感的存储单元,数据存储的动作由某一信号的上升或者下降

    2024年02月12日
    浏览(29)
  • ARM编程模型-寄存器组

    Cortex A系列ARM处理器共有40个32位寄存器,其中33个为通用寄存器,7个为状态寄存器。usr模式和sys模式共用同一组寄存器。 通用寄存器包括R0~R15,可以分为3类: 未分组寄存器R0~R7 分组寄存器R8~R14、R13(SP) 、R14(LR) 程序计数器PC(R15)、R8_fiq-R12_fir为快中断独有 在不同模式下,名称相同的

    2024年02月10日
    浏览(35)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包