Skip to content

AXI4 VIP development and integration - #688

Open
tchilikov-semify wants to merge 25 commits into
lowRISC:mainfrom
tchilikov-semify:axi_driver_pr_new
Open

AXI4 VIP development and integration#688
tchilikov-semify wants to merge 25 commits into
lowRISC:mainfrom
tchilikov-semify:axi_driver_pr_new

Conversation

@tchilikov-semify

@tchilikov-semify tchilikov-semify commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

This PR does a few things:

  1. Pulls @rswarbrick's AXI driver from his Caliptra repository as a local copy and expands on its functionality with multi-beat support
  2. Merges it with Csaba's AXI monitor from Axi4 passive vip #391, creating one unified agent for Active (manager) and Passive (monitor) modes
  3. Applies various bug and compilation fixes to the agent
  4. Integrates the agent in the top TB as a monitor on each port of the crossbar
  5. creates an environment level scoreboard

This was tested with the top level CVA6-based test cases.

Will close issue #168

@rswarbrick rswarbrick left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much for doing this (and getting my completely untested code working...)

Do you think you could cherry-pick the various fixes in "axi agent compilation fixes" to the commits that made the mistakes? Since we don't use squash merge in Mocha, this is a way to avoid putting Rupert's silly mistakes into the history :-)

@tchilikov-semify

Copy link
Copy Markdown
Collaborator Author

Thank you very much for doing this (and getting my completely untested code working...)

Do you think you could cherry-pick the various fixes in "axi agent compilation fixes" to the commits that made the mistakes? Since we don't use squash merge in Mocha, this is a way to avoid putting Rupert's silly mistakes into the history :-)

Hey Rupert,

Sure, I can merge the patches into your commits. This would also encompass the handshaking bugfix commit, for a total of two commits :)

rswarbrick and others added 8 commits July 22, 2026 14:40
This is the first step towards a simple AXI agent. It will only have
to handle the subset of AXI that is in use in the blocks in
question (which work by translating to TLUL, so they aren't doing
anything particularly exciting).

In this commit, we're just defining interfaces for the five
channels (which are AW, W, B, AR and R).

Co-authored-by: tchilikov-semify <christian.tchilikov@semify-eda.com>
The axi_read_request_item and axi_read_data_item classes are intended
to be randomised (when a sequence wishes to send either read requests
or data responses). The axi_read_item class can be used by a monitor
that sees a read request and then one or more responses.

Co-authored-by: tchilikov-semify <christian.tchilikov@semify-eda.com>
When the agent is representing an AXI Manager, this will be useful for
the B and R channels.

Co-authored-by: tchilikov-semify <christian.tchilikov@semify-eda.com>
These drive write requests (AW) and write data (W) and also drive the
write response channel (B).

Co-authored-by: tchilikov-semify <christian.tchilikov@semify-eda.com>
Co-authored-by: tchilikov-semify <christian.tchilikov@semify-eda.com>
Co-authored-by: tchilikov-semify <christian.tchilikov@semify-eda.com>
For an example use-case, suppose you are reading with AR / R
transfers. You send an AR transfer with some ARID that will have a
burst with k beats. Now you want consume read data with that many (k)
R transfers using the some ID.

To do so:

  - Run k axi_mgr_read_data_seq sequences, calling on_response() after
    each finishes.

  - (In parallel) call wait_for_response() k times.

The trick is that these sequences might respond with different IDs,
but that doesn't matter: the sequences are just actings as tokens to
allow *something* to come back.
Comment thread hw/ip/dv/axi_agent/axi_mgr_read_data_driver.svh Outdated
Comment thread hw/ip/dv/axi_agent/axi_mgr_read_data_driver.svh Outdated
Comment thread hw/ip/dv/axi_agent/axi_mgr_read_data_driver.svh Outdated
Comment thread hw/ip/dv/axi_agent/axi_mgr_write_response_driver.svh Outdated
Comment thread hw/ip/dv/axi_agent/seq_lib/axi_mgr_read_burst_vseq.svh Outdated
Comment thread hw/ip/dv/axi_agent/axi_agent_cfg.svh Outdated
Comment thread hw/ip/dv/axi_agent/axi_mgr_agent.svh Outdated
Comment thread hw/ip/dv/axi_agent/axi_mgr_agent.svh Outdated
Comment thread hw/ip/dv/axi_agent/axi_mgr_agent.svh Outdated
Comment thread hw/ip/dv/axi_agent/axi_mon_item.svh Outdated
@tchilikov-semify

Copy link
Copy Markdown
Collaborator Author

Hey @rswarbrick,
Thanks a lot for the in-depth review! I'll go through all of the comments and address them.

In the meanwhile, I've folded the old compilation bugfixes commit into the respective commits that you made, so the commit history should be clear of bugs now.

@tchilikov-semify
tchilikov-semify force-pushed the axi_driver_pr_new branch 9 times, most recently from a5c0dab to 6550b3a Compare July 23, 2026 09:27
@tchilikov-semify
tchilikov-semify force-pushed the axi_driver_pr_new branch 3 times, most recently from 36382cb to f1c39f1 Compare August 6, 2026 15:32
@tchilikov-semify

Copy link
Copy Markdown
Collaborator Author

The AXI driver has been ran past the ARM axi4 protocol assertions using the axi_sram.sv of mocha as a DUT as a preliminary validity check. The driver exercised the majority of the testcases from #667 and the assertions did not fire or indicate incorrect behavior, suggesting correct functionality thus far.

rswarbrick and others added 2 commits August 12, 2026 18:01
To do so, we add a (pretty trivial) reg_adapter and a sequencer in a
layered vseq that runs the resulting translated items.

Co-authored-by: tchilikov-semify <christian.tchilikov@semify-eda.com>
@tchilikov-semify
tchilikov-semify force-pushed the axi_driver_pr_new branch 3 times, most recently from 1851eb5 to c90d65e Compare August 17, 2026 09:55
Merge per-channel reset monitors into a unified reset monitor
by introducing a shared clock and reset interface to the agent.
Add AXI transaction monitor which snoops the 5 channels and rebuilds
AXI transactions, then broadcasts them. Monitor is built
unconditionally.

@rswarbrick rswarbrick left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are some notes about the "fix axi handshaking" and "multi-beat transaction support" commits.

These both look really good: I like the code, and it's just some nitty notes about comments.


// Drive rready and watch for the actual transfer. We track the value we drive in
// rather than reading it back from the clocking block, and we sample on that exact
// handshake edge. This is what makes the accept protocol-correct:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think this is a stray hyphen. (Currently looks like "protocol-correct" is an adjective)

@(m_vif.mgr_cb);
if (m_vif.mgr_cb.rvalid === 1'b1) begin
if (rready) break; // rvalid && rready on this edge: beat transferred
delay++; // counts the cycle just elapsed, including the first rvalid one

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Probably better to phrase this as "count the ..." (to avoid it sounding like it's a number of counts)

// When we get here, we have finished accepting a response and we are at the end of a cycle
// where rvalid and rready were asserted. Write a sequence item to represent the response
// that we have seen to our read_data_item output argument.
// The beat transferred on the edge that just finished: sample the value.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An edge is a zero-time event, so doesn't really "finish". Probably "just happened" would be better.


// Drive bready and watch for the actual transfer. We track the value we drive in
// rather than reading it back from the clocking block, and we sample on that exact
// handshake edge. This is what makes the accept protocol-correct:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As with the read driver, I think this should drop the hyphen.

@(m_vif.mgr_cb);
if (m_vif.mgr_cb.bvalid === 1'b1) begin
if (bready) break; // bvalid && bready on this edge: response accepted
delay++; // counts the cycle just elapsed, including the first bvalid one

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As with the driver, I'd suggest "count" here.

Comment on lines +21 to +22
// the request illegal. The burst length comes from m_ar_req.m_len, so the sequence expects
// m_len + 1 beats.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I don't think the second half of the sentence follows from the first. I just checked the spec quickly for phrasing. How about "The m_len variable controls ARLEN and the burst has ARLEN+1 beats." ?

// the request illegal. The burst length comes from m_ar_req.m_len, so the sequence expects
// m_len + 1 beats.
// - Optionally configure m_r_accept to control rready timing (see axi_mgr_read_data_seq). Its
// m_use_fixed_*/m_fixed_* pins are copied to every beat; unpinned fields are randomised

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I don't think "pins" is the right noun here. Maybe just "values"?

// response into the router. Spawned once per beat by body(); beat_num only names the seq.
extern local task accept_one_beat(int unsigned beat_num);

// Take n_beats responses for our AR's ID back out of the router, in beat order, appending each

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what "AR" means here. I don't think that ARID splits up as "AR"+"ID".

$sformatf({"m_read_beats already holds %0d beats. Has this sequence ",
"been started twice?"}, m_read_beats.size()))
end
m_read_beats.delete();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd move this into the if block above: equivalent and it might make the code slightly more obvious.

axi_mgr_read_data_seq r_seq =
axi_mgr_read_data_seq::type_id::create($sformatf("r_seq_%0d", beat_num));

r_seq.m_use_fixed_ready_without_valid_pct = m_r_accept.m_use_fixed_ready_without_valid_pct;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this have been done by clone()'ing m_r_accept?

@rswarbrick rswarbrick left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two more minor notes about "add multi-beat transaction support"


// A subclass of axi_mgr_write_data_seq that sends a caller-supplied list of write data items, one
// per beat, in order. Used for multi-beat write bursts. The beats are not randomised: each item is
// driven verbatim as supplied, except WLAST.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be made more explicit. How about "... except that WLAST is asserted when driving the final item." ? (Phrasing it like this avoids it sounding like we're changing the contents of the item).


extern function new(string name="");

// Overrides axi_mgr_write_data_seq::populate_item to "produce" each beat by copying the next

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd stick to the imperative, so this could just be "Override".

@rswarbrick rswarbrick left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A minor note about error messages in "add clk_rst_if to axi interfaces". (I really like this change though!)

task axi_mgr_read_data_driver::run_phase(uvm_phase phase);
if (m_vif == null) begin
if (m_vif == null || m_clk_rst_vif == null) begin
`uvm_fatal(get_full_name(), "Cannot drive interface: vif is null.")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I think this should probably now be something more like "Cannot run driver: either m_vif or m_clk_rst_vif is null."

task axi_mgr_read_request_driver::run_phase(uvm_phase phase);
if (m_vif == null) begin
if (m_vif == null || m_clk_rst_vif == null) begin
`uvm_fatal(get_full_name(), "Cannot drive interface: vif is null.")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As with the read data driver, this probably needs to mention both interfaces.

task axi_mgr_write_data_driver::run_phase(uvm_phase phase);
if (m_vif == null) begin
if (m_vif == null || m_clk_rst_vif == null) begin
`uvm_fatal(get_full_name(), "Cannot drive interface: vif is null.")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As with the read data driver, this probably needs to mention both interfaces.

task axi_mgr_write_request_driver::run_phase(uvm_phase phase);
if (m_vif == null) begin
if (m_vif == null || m_clk_rst_vif == null) begin
`uvm_fatal(get_full_name(), "Cannot drive interface: vif is null.")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As with the read data driver, this probably needs to mention both interfaces.

task axi_mgr_write_response_driver::run_phase(uvm_phase phase);
if (m_vif == null) begin
if (m_vif == null || m_clk_rst_vif == null) begin
`uvm_fatal(get_full_name(), "Cannot drive interface: vif is null.")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As with the read data driver, this probably needs to mention both interfaces.

@rswarbrick rswarbrick left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some notes about the (excellent!) change that adds axi_widths_pkg. They're all about alignment: sorry - very boring.


bit m_use_fixed_id;
bit [31:0] m_fixed_id;
bit [AxiMaxIdWidth-1:0] m_fixed_id;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a big fan of this change, but could you realign the adjacent variables?

I think that's probably better than switching to e.g. bit m_use_fixed_id;, but either seems reasonable. It's just that "aligned to something that used to be there" is definitely not right!

// This is sent as AWID, whose width is configurable (and will be checked against the
// corresponding width in the interface by the driver).
rand bit [31:0] m_id;
rand bit [AxiMaxIdWidth-1:0] m_id;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The alignment of the variables seems to have gone a bit wiggly in this file.

// This is sent as AWID, whose width is configurable (and will be checked against the
// corresponding width in the interface by the driver).
rand bit [31:0] m_id;
rand bit [AxiMaxIdWidth-1:0] m_id;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As with axi_fixed_read_req_item, I think the alignment is a bit wrong in this file.

wire [AxiMaxIdWidth-1:0] rid;
wire [AxiMaxDataWidth-1:0] rdata;
wire [AxiRespWidth-1:0] rresp;
wire rlast;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you indent rvalid, rready and rlast to match the other variables that have moved outwards?

@@ -64,22 +65,22 @@ interface axi_read_data_if (input clk_i, input rst_ni);
// these, but take masks into account for signals with configurable length and are also cleared on
// reset.
logic rvalid_driven, rvalid_internal;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you indent the variables to line up in this block?

// Copies of the signals that are driven by mgr_cb (only used if if_mode == Host). The "*_driven"
// signals are directly driven by the clocking block. The "*_internal" signals track these, but
// take masks into account for signals with configurable length and are also cleared on reset.
logic awvalid_driven, awvalid_internal;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you fix the indentation for these variables?

logic [31:0] awid_mask;
logic [63:0] awaddr_mask;
logic [127:0] awuser_mask;
logic [AxiMaxIdWidth-1:0] awid_mask;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you fix the indentation for these variables?

@@ -42,9 +43,9 @@ interface axi_write_response_if (input clk_i, input rst_ni);
// - BTAGMATCH (MTE_Support is false)
wire bvalid;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you fix the indentation for these variables?

@@ -56,18 +57,18 @@ interface axi_write_response_if (input clk_i, input rst_ni);
// these, but take masks into account for signals with configurable length and are also cleared on
// reset.
logic bvalid_driven, bvalid_internal;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you fix the indentation for these variables?

logic [31:0] bid_mask;
logic [2:0] bresp_mask;
logic [15:0] buser_mask;
logic [AxiMaxIdWidth-1:0] bid_mask;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you fix the indentation for these variables?

class axi_monitor extends uvm_monitor;
`uvm_component_utils(axi_monitor)

local axi_agent_cfg m_cfg;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm really sorry for not replying to this more quickly. Personally, I'd get rid of the config class and just give the monitor a set_interfaces() function that takes all the interfaces at once.

But that can be a follow-up if you don't want to delay this PR further.

// Request-phase address (awaddr / araddr).
pure virtual function axi_addr_t get_addr();

// Transaction direction (implied by the concrete type).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I know what the parenthesised comment means here. Is it needed?


obj = clone();
// A $cast from a null handle succeeds, so a failed clone would return null silently.
`DV_CHECK_FATAL(obj != null, "clone() returned null")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I claim that the macros make this less clear. Any reason not to be explicit?

  if (obj == null)             `uvm_fatal(get_name(), "clone() returned null.")
  if (!$cast(item_clone, obj)) `uvm_fatal(get_name(), "Failed to cast self to axi_mon_item.")

// Read address (AR)
axi_id_t m_arid;
axi_addr_t m_araddr;
logic [AxiLenWidth-1:0] m_arlen;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Why are these variables logic not bit? What would m_arlen = 'x mean? How about m_arlen = 'b10110xx01? I'm not convinced we want to represent that.

Update: This is really clever! Could the explanation in the monitor be moved/copied here?


function void axi_mon_read_item::do_print(uvm_printer printer);
super.do_print(printer);
printer.print_field_int("m_arid", m_arid, 32, UVM_HEX);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the widths passed to the printer be the named widths from the package?

The same applies to compare below.

local virtual axi_read_data_if m_r_vif;
local virtual clk_rst_if m_clk_rst_vif; // shared ACLK/ARESETn

uvm_analysis_port #(axi_mon_item) tx_ap;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this have a documentation comment?

// match B/R to their request by ID, so the B and R collectors are the ones that complete a
// transaction and publish it on tx_ap.
//
// Each collector reports a sampled X/Z as MON_X. The items are 4-state so an unknown does reach

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah: this answers questions I was asking earlier. That's really clever!

Could you move/copy the explanation to the item types?

Comment thread hw/ip/dv/axi_agent/axi_monitor.svh
Comment on lines +119 to +120
@(m_aw_vif.mon_cb);
if (m_aw_vif.mon_cb.awvalid && m_aw_vif.mon_cb.awready) begin

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be wait(m_aw_vif.mon_cb.awvalid && m_aw_vif.mon_cb.awready);? I think it's a bit simpler.

The same applies to the other collection tasks too.

Comment on lines +282 to +283
`DV_CHECK_FATAL(obj != null, "clone() returned null")
`DV_CHECK_FATAL($cast(write_item, obj))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As earlier, I think it's better to be explicit: the macros probably make it worse.

@rswarbrick rswarbrick left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One specific question: Is there a good reason to use dv_base_agent for the IP? It's designed to a very specific OpenTitan shape, as chosen by the DV lead at the time.

It's also actively against my design for this agent. If someone more important than me mandates that we use the class, I guess we have to. If not, I'd prefer we didn't.

parameter int UserWidth = 1,
parameter uvm_pkg::uvm_active_passive_enum IsActive = uvm_pkg::UVM_ACTIVE,
parameter string InstId = "axi_mgr", // names the published axi_agent_cfg
parameter string CfgScope = "*" // config_db publish scope glob

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this design following a pattern for which you have a reference? I'm slightly surprised that you don't set things in the config db from the testbench, rather than the interface that gets instantiated.

parameter int AddrWidth = 64,
parameter int DataWidth = 64,
parameter int UserWidth = 1,
parameter uvm_pkg::uvm_active_passive_enum IsActive = uvm_pkg::UVM_ACTIVE,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest moving an import earlier to avoid this crazy line.

Couldn't the interface start with something like this?

interface axi_vip_if
  import uvm_pkg::uvm_active_pass_enum;
#(
  ...
) (
  ...
);

Comment on lines +5 to +8
// Reusable AXI VIP connection interface: instantiates the five axi_agent channel
// interfaces, bridges them to a packed AXI req/resp struct pair, and publishes them
// to the UVM config_db as one axi_agent_cfg. Parameterized on the req/resp struct
// types + widths, and on IsActive:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is clever, but could you explicitly mention the structure of the req_t and resp_t types?

I could only figure out what was going on by grepping in the rest of the project for uses of the interface until I found AXI_TYPEDEF_REQ_T.

If I understand correctly, this is a pulp type (so we can't move the definition to the agent!), but I think we need to explain what it is.

// clear, the payload is randomised instead: still meaningless, but defined.
//
// Defaults set, so an idle channel drives X.
bit drive_x_when_idle = 1'b1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm repeating myself here, but I think this should be set by calling a setter on the agent, which calls a setter on each of the drivers.

Note that you wrote earlier that the agent config just contained a list of virtual interfaces. I wasn't convinced. I'm still not :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants