AMBA总线-结合axi-vip对axi4协议的理解1

一直想写一个axi-vip的理解,但是介于个人水平有限,一直没能做出很好的总结,这个系列的内容将对这方面内容做出一个阐述。另外,个人水平有限,仅参考,有什么问题希望大家能够批评指正,共同进步!

这个vip同样是参考github上的一个项目,这个vip支持如下的特性:

(1)支持axi4、axi4-lite的协议;

(2)地址位宽、数据位宽、ID位宽可以配置;

(3)支持延迟写入数据和响应;

(4)支持间隙写入数据和读取响应;

(5)响应的顺序支持乱序和保序;

(6)支持读操作的interleave

首先分析tvip_axi_item,需要注意的是在这个item中存在一些新的数据类型time。对于SV来说,time数据类型如下,四值逻辑,用来反映当前时间。

AMBA总线-结合axi-vip对axi4协议的理解1_第1张图片

 除此以外还需要注意数据类型real,用于表示浮点数。

        uvm_event             address_begin_event;
        time                  address_begin_time;
        uvm_event             address_end_event;
        time                  address_end_time;
        uvm_event             write_data_begin_event;
        time                  write_data_begin_time;
        uvm_event             write_data_end_event;
        time                  write_data_end_time;
        uvm_event             response_begin_event;
        time                  response_begin_time;
        uvm_event             response_end_event;
        time                  response_end_time;

和configuration相关的约束:

constraint c_valid_id {
    (id >> this.configuration.id_width) == 0;
  }

  constraint c_valid_address {
    (address >> this.configuration.address_width) == 0;
  }


  constraint c_valid_memory_type {
    if (this.configuration.protocol == TVIP_AXI4LITE) {
      memory_type == TVIP_AXI_DEVICE_NON_BUFFERABLE;
    }
  }

  constraint c_valid_qos {
    qos inside {[
      this.configuration.qos_range[0]:
      this.configuration.qos_range[1]
    ]};
  }

上面的约束中需要注意区分<<的作用,<<有两部分作用,一种是在verilog中作为左移右移使用,另一种是在sv中作为流操作符使用。对于前者来说,使用的格式是b=a<<2这种形式,也就是将a左移两位后赋给b;而对于流操作来说使用的格式是h={>>{j}}的方式,也就是将j按照从左到右的打包方式打包。

目前还没有理解 (id >> this.configuration.id_width) == 0的意思,个人认为是创建这么多位的0,也就是>>是当作左移右移符号使用的。

除此以外需要注意和configuration相关的几个约束,第一个busrt传输的长度;第二个burst传输的size;第三个4k边界地址;

constraint c_valid_burst_length {
    if (this.configuration.protocol == TVIP_AXI4) {
      burst_length inside {[1:this.configuration.max_burst_length]};
    }
    else {
      burst_length == 1;
    }
  }

  constraint c_valid_burst_size {
    if (this.configuration.protocol == TVIP_AXI4) {
      burst_size inside {1, 2, 4, 8, 16, 32, 64, 128};
      (8 * burst_size) <= this.configuration.data_width;
    }
    else {
      (8 * burst_size) == this.configuration.data_width;
    }
  }

  constraint c_4kb_boundary {
    (
      (address & `tvip_axi_4kb_boundary_mask(burst_size)) +
      (burst_length * burst_size)
    ) <= 4096;
  }

关于中间变量的约束: 

  constraint c_valid_write_data {
    solve access_type  before data;
    solve burst_length before data;

    (access_type == TVIP_AXI_WRITE_ACCESS) -> data.size() == burst_length;
    (access_type == TVIP_AXI_READ_ACCESS ) -> data.size() == 0;

    foreach (data[i]) {
      (data[i] >> this.configuration.data_width) == 0;
    }
  }

这里的strobe是和access_type和burst_length密切相关的。对于strobe信号需要注意的是:字节选通的信号是仅仅在写操作时才会起作用,在读操作的时候时不存在的,因此读操作的时候对strobe的size直接约束为0。随后按照字节选通的方式进行创建位数。

  constraint c_valid_strobe {
    solve access_type  before strobe;
    solve burst_length before strobe;

    (access_type == TVIP_AXI_WRITE_ACCESS) -> strobe.size() == burst_length;
    (access_type == TVIP_AXI_READ_ACCESS ) -> strobe.size() == 0;

    foreach (strobe[i]) {
      (strobe[i] >> this.configuration.strobe_width) == 0;
    }
  }

这个约束目前没搞清楚作用。

 constraint c_address_start_delay {
    `tvip_delay_constraint(start_delay, this.configuration.request_start_delay)
  }

对于写操作来说,实际上会出现写数据的delay,这里对delay进行约束,这里是指写数据中间可能存在延迟。

  constraint c_write_data_delay {
    solve access_type, burst_length  before write_data_delay;

    if (access_type == TVIP_AXI_WRITE_ACCESS) {
      write_data_delay.size() == burst_length;
    }
    else {
      write_data_delay.size() == 0;
    }

    foreach (write_data_delay[i]) {
      `tvip_delay_constraint(write_data_delay[i], this.configuration.write_data_delay)
    }
  }

对于写操作和读操作存在ready的delay,这里对ready的delay进行约束。注意这里对于写操作和读操作的区分,对于写操作来说,其B通道中会存在slave给master回应的过程,这个response收到master给slave的ready信号的影响,这里对于B通道的ready信号制作了一个size。原因在于一个response是表示这个burst传输的响应,因此对于写操作的一个burst操作来说,其size也只为1。而对于读操作来说,其size是burst_length,原因在于这里的ready是针对读操作返回给master的每一个数据的ready。

  constraint c_response_ready_delay {
    solve access_type, burst_length before response_ready_delay;

    if (access_type == TVIP_AXI_WRITE_ACCESS) {
      response_ready_delay.size() == 1;
    }
    else {
      response_ready_delay.size() == burst_length;
    }

    foreach (response_ready_delay[i]) {
      if (access_type == TVIP_AXI_WRITE_ACCESS) {
        `tvip_delay_constraint(response_ready_delay[i], this.configuration.bready_delay)
      }
      else {
        `tvip_delay_constraint(response_ready_delay[i], this.configuration.rready_delay)
      }
    }
  }

此外需要注意几个函数:

这个函数用来拿到item中的burst的长度。

function int get_burst_length();
    if ((configuration != null) && (configuration.protocol == TVIP_AXI4LITE)) begin
      return 1;
    end
    else begin
      return burst_length;
    end
  endfunction

接下来分析tvip_axi_master_sub_driver:

driver的整体框架如下,也就是主要有三个任务,分别是address_thread、write_data_thread、response_thread,接下来具体分析这三个任务。

protected task main();
    fork
      address_thread();
      write_data_thread();
      response_thread();
    join
  endtask

对于address_thread来说,整体框架如下:

 protected task address_thread();
    tvip_axi_item item;

    forever begin
      get_item_from_queue(address_queue, item);
      consume_delay(item.start_delay);
      begin_address(item);
      drive_address(1, item);
      wait_for_address_ready();
      drive_address(0, null);
      end_address(item);
    end
  endtask

get_item_from_queue函数原型如下:

注意在这个函数中下面的if条件语句没有看懂。

protected task get_item_from_queue(
    input tvip_axi_request_item_queue queue,
    ref   tvip_axi_item               item
  );
    queue.get(item);
    if (!vif.at_master_cb_edge.triggered) begin
      @(vif.at_master_cb_edge);
    end
  endtask

consume_delay函数原型如下:

protected task consume_delay(int delay);
    repeat (delay) begin
      @(vif.master_cb);
    end
  endtask

drive_address函数原型如下:

针对写操作:

protected task drive_address(
    bit           valid,
    tvip_axi_item item
  );
    vif.master_cb.awvalid <= valid;
    if (valid) begin
      vif.master_cb.awaddr  <= item.address;
      vif.master_cb.awid    <= item.id;
      vif.master_cb.awlen   <= item.get_packed_burst_length();
      vif.master_cb.awsize  <= item.get_packed_burst_size();
      vif.master_cb.awburst <= item.burst_type;
      vif.master_cb.awcache <= item.get_cache();
      vif.master_cb.awprot  <= item.protection;
      vif.master_cb.awqos   <= item.qos;
    end
  endtask

针对读操作:

protected task drive_address(
    bit           valid,
    tvip_axi_item item
  );
    vif.master_cb.arvalid <= valid;
    if (valid) begin
      vif.master_cb.araddr  <= item.address;
      vif.master_cb.arid    <= item.id;
      vif.master_cb.arlen   <= item.get_packed_burst_length();
      vif.master_cb.arsize  <= item.get_packed_burst_size();
      vif.master_cb.arburst <= item.burst_type;
      vif.master_cb.arcache <= item.get_cache();
      vif.master_cb.arprot  <= item.protection;
      vif.master_cb.arqos   <= item.qos;
    end
  endtask

 wait_for_write_data_ready函数原型:

protected task wait_for_write_data_ready();
    do begin
      @(vif.master_cb);
    end while (!vif.master_cb.wready);
  endtask

wait_for_address_ready任务:

protected task wait_for_address_ready();
    do begin
      @(vif.master_cb);
    end while (!get_address_ready());
  endtask

这个任务也就是只要get_address_ready函数返回的内容不ready,那么就会一直打拍,get_address_ready任务内容如下:

protected function bit get_address_ready();
    return vif.master_cb.awready;
  endfunction

protected function bit get_address_ready();
    return vif.master_cb.arready;
  endfunction

总的来说,整个address_thread的层次结构如下,首先通过get_item_from_queue拿到item,然后根据拿到的item中的相关设定判断是否需要延迟,随后驱动地址信息,最后等aw通道的ready信号拉高;

AMBA总线-结合axi-vip对axi4协议的理解1_第2张图片

 对于write_data_thread()任务来说,整体的框架如下:

protected task write_data_thread();
    tvip_axi_item item;

    if (is_read_component()) begin
      return;
    end

    forever begin
      get_item_from_queue(write_data_queue, item);

      for (int i = 0;i < item.get_burst_length();++i) begin
        consume_delay(item.write_data_delay[i]);
        if (i == 0) begin
          begin_write_data(item);
        end
        drive_write_data(1, item, i);
        wait_for_write_data_ready();
        drive_write_data(0, null, 0);
      end

      end_write_data(item);
    end
  endtask

get_item_from_queue函数和上面的函数是一样的,对于写操作来说,一个burst传输过程中每一个数据都是有可能存在延迟的,因此这里实际需要对burst传输过程中每一个数据进行delay操作,因此上面会存在for循环加consume_delay的操作。

drive_write_data函数原型如下,给数据和strobe,这里的strobe是每个数据都有相应的strobe,数据的最后一拍给wlast。

protected virtual task drive_write_data(
    bit           valid,
    tvip_axi_item item,
    int           index
  );
    vif.master_cb.wvalid  <= valid;
    if (valid) begin
      vif.master_cb.wdata <= item.data[index];
      vif.master_cb.wstrb <= item.strobe[index];
      if (configuration.protocol == TVIP_AXI4) begin
        vif.master_cb.wlast <= index == (item.get_burst_length() - 1);
      end
    end
  endtask

wait_for_write_data_ready函数原型如下:

protected task wait_for_write_data_ready();
    do begin
      @(vif.master_cb);
    end while (!vif.master_cb.wready);
  endtask

最后一部分是response_thread,在了解response_thread之前需要了解一个别的类tvip_axi_payload_store,这个类主要实现的功能如下:

*************************************tvip_axi_payload_store*********************************************

内部定义了item,data,strobe,response的队列,除此以外还存在一些任务,具体任务如下:

store_write_data,如果是写操作的化,会把这个函数输入的写数据和strobe推到内部定义的data和strobe里;
store_response,将这个函数输入的response存到自己的队列中,如果是读操作,还会将这个函数输入的数据推到相应的队列中;

接下来分析pack的几个任务,在分析之前需要需要分析item中的几个函数:主要有put_data、put_response、put_strobe三个函数,这三个函数的结构均如下(也就是将输入的response给到外面的response):

function void put_response(const ref tvip_axi_response response[$]);
    this.response = new[response.size()];
    foreach (response[i]) begin
      this.response[i]  = response[i];
    end
  endfunction

接下来看:pack_write_data和pack_response。对于pack_write_data任务来说,是调用item中的put_data和put_strobe任务,这个item就是tvip_axi_payload_store中定义的item。对于pack_response是调用item中的put_response,如果是读操作的化,还会调用item中的put_data任务。

最后看两个任务,分别是get_stored_write_data_count和get_stored_response_count任务,这两个任务内部定义的data和response的size。

*************************************tvip_axi_payload_store*********************************************

在分析response_thread的任务之前,先分析一个sample_response任务,这个任务

protected task sample_response(
    input tvip_axi_id id,
    ref   bit         busy
  );
    tvip_axi_payload_store  store;

    store = response_stores[id][0];
    store.store_response(get_response_status(), get_response_data());
    if (get_response_last()) begin
      store.pack_response();
      end_response(store.item);
      void'(response_stores[id].pop_front());
      busy  = 0;
    end
  endtask

也就是说response_thread主要包含的内容是把外部的response_stores给到内部的store,内部的store调用store_response将相关的数据和response存储到store内部中,如果最后一个数据,需要调用store的pack_response,也就是最后一笔数据的时候,将store中华所有的response整合一下到item中去。

这个函数任务的任务如下:

protected function tvip_axi_response get_response_status();
    return vif.master_cb.bresp;
  endfunction

protected function tvip_axi_response get_response_status();
    return vif.master_cb.rresp;
  endfunction

protected function tvip_axi_data get_response_data();
    return '0;
  endfunction

protected function tvip_axi_data get_response_data();
    return vif.master_cb.rdata;
  endfunction

protected function logic get_response_last();
    return '1;
  endfunction

protected function logic get_response_last();
    return (configuration.protocol == TVIP_AXI4LITE) || vif.master_cb.rlast;
  endfunction

最后看下整个response_thread任务所执行的内容:

protected task response_thread();
    bit         busy;
    tvip_axi_id id;
    tvip_axi_id current_id;
    int         delay;

    forever begin
      wait_for_response_valid();//等待B通道的valid信号

      id  = get_response_id();//拿到ID信号
      if ((!busy) || (id != current_id)) begin//一开始循环过程中busy为0,因此执行下面语句,下次循环的时候不会执行这个if语句
        if (is_valid_response(id)) begin//response_stores是否存在对应id的response
          busy        = 1;//把busy变为1,id变为相同的id
          current_id  = id;
          if (!response_stores[current_id][0].item.response_began()) begin
            begin_response(response_stores[current_id][0].item);
          end
        end
        else begin
          busy  = 0;
          `uvm_warning("UNEXPECTED_RESPONSE", $sformatf("unexpected response: id %h", id))
          continue;
        end
      end

      delay = get_response_ready_delay(current_id);//获得delay;
      if (default_response_ready) begin//一开始的一拍是ready的;
        sample_response(current_id, busy);//调用sample_response;
        if (delay > 0) begin//delay大于0,拉低ready,延迟几拍;
          drive_response_ready(0);
          consume_delay(delay);
          drive_response_ready(1);
        end
      end
      else begin
        consume_delay(delay);//一开始的一拍是不ready的,直接延迟一拍,再继续考虑后续延迟情况
        drive_response_ready(1);
        consume_delay(1);
        sample_response(current_id, busy);
        drive_response_ready(0);
      end
    end
  endtask

对于整个任务的框架就是等B通道valid,拿到id,第一次循环的时候执行上面的循环语句,变化busy状态和id号;获取delay;根据第一拍是否有delay进行延迟,最后调用sample_response任务,这个任务结束的时候会将busy置为0,为下一次循环做准备。

你可能感兴趣的:(AMBA总线,开发语言)