| // Copyright lowRISC contributors. |
| // Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| // SPDX-License-Identifier: Apache-2.0 |
| |
| // A class that provides common RISCV debugging utilities over JTAG. |
| // |
| // The utilities provided here allow the user to mimic an external debugger performing tasks such |
| // as halting the CPU, asserting a non-debug domain reset, accessing the CPU registers and memory |
| // using abstract commands, inserting breakpoints, single-stepping, causing the CPU to execute |
| // arbitrary instructions and accessing the chip resources over the SBA interface. |
| class jtag_rv_debugger extends uvm_object; |
| `uvm_object_utils(jtag_rv_debugger) |
| |
| // A pre-created handle to the jtag_agent_cfg object. |
| protected jtag_agent_cfg cfg; |
| |
| // A pre-created handle to the JTAG DMI reg block with a frontdoor accessor attached. |
| protected jtag_dmi_reg_block ral; |
| |
| // The elf file executed by the CPU. |
| protected string elf_file; |
| |
| // Enable prediction on writes. |
| bit predict_dmi_writes = 1; |
| |
| // Number of harts in the system. TODO: add support for multi-hart system. |
| int num_harts = 1; |
| |
| // Number of breakpoints (triggers) supported. |
| int num_triggers; |
| |
| // Indicates whether aarpostincrement is supported. |
| bit aarpostincrement_supported; |
| |
| // An sba_access_reg_frontdoor instance to provide access to system CSRs via SBA using JTAG. |
| sba_access_reg_frontdoor m_sba_access_reg_frontdoor; |
| |
| // Internal status signals to optimize debugger performance. |
| // TODO: Use these bits effectively. |
| protected bit dmactive_is_set; |
| protected bit cpu_is_halted; |
| protected bit step_entered; |
| protected logic [BUS_AW-1:0] symbol_table[string]; |
| |
| function new (string name = ""); |
| super.new(name); |
| m_sba_access_reg_frontdoor = sba_access_reg_frontdoor::type_id::create( |
| "m_sba_access_reg_frontdoor"); |
| m_sba_access_reg_frontdoor.debugger_h = this; |
| endfunction : new |
| |
| // Sets the jtag_agent_cfg handle. |
| virtual function void set_cfg(jtag_agent_cfg cfg); |
| this.cfg = cfg; |
| endfunction |
| |
| // Returns the jtag_agent_cfg handle. |
| virtual function jtag_agent_cfg get_cfg(); |
| return cfg; |
| endfunction |
| |
| // Sets the jtag_dmi_reg_block handle. |
| virtual function void set_ral(jtag_dmi_reg_block ral); |
| this.ral = ral; |
| endfunction |
| |
| // Returns the jtag_dmi_reg_block handle. |
| virtual function jtag_dmi_reg_block get_ral(); |
| return ral; |
| endfunction |
| |
| // Sets the elf file name. |
| virtual function void set_elf_file(string elf_file); |
| this.elf_file = elf_file; |
| symbol_table.delete(); |
| endfunction |
| |
| // Returns the elf file name. |
| virtual function string get_elf_file(); |
| return elf_file; |
| endfunction |
| |
| // Asserts TRST_N for a few cycles. |
| virtual task do_trst_n(int cycles = $urandom_range(5, 20)); |
| `uvm_info(`gfn, "Asserting TRST_N", UVM_MEDIUM) |
| cfg.vif.do_trst_n(cycles); |
| dmactive_is_set = 0; |
| cpu_is_halted = 0; |
| step_entered = 0; |
| endtask |
| |
| // Enables the debug module. |
| virtual task set_dmactive(bit value); |
| `uvm_info(`gfn, $sformatf("Setting dmactive = %0b", value), UVM_MEDIUM) |
| csr_wr(.ptr(ral.dmcontrol.dmactive), .value(value), .blocking(1), .predict(predict_dmi_writes)); |
| dmactive_is_set = value; |
| if (!value) begin |
| cpu_is_halted = 0; |
| step_entered = 0; |
| end |
| endtask |
| |
| // Issues a CPU halt request. |
| virtual task set_haltreq(bit value, int hart = 0); |
| `uvm_info(`gfn, $sformatf("Setting haltreq = %0b", value), UVM_MEDIUM) |
| csr_wr(.ptr(ral.dmcontrol.haltreq), .value(value), .blocking(1), .predict(predict_dmi_writes)); |
| endtask |
| |
| // Waits for the CPU to be in halted state. |
| virtual task wait_cpu_halted(int hart = 0); |
| uvm_reg_data_t data; |
| `uvm_info(`gfn, "Waiting for CPU to halt", UVM_MEDIUM) |
| `DV_SPINWAIT( |
| do begin |
| csr_rd(.ptr(ral.dmstatus), .value(data), .blocking(1)); |
| end while (dv_base_reg_pkg::get_field_val(ral.dmstatus.anyhalted, data) == 0); |
| ) |
| if (num_harts == 1) begin |
| `DV_CHECK_EQ(dv_base_reg_pkg::get_field_val(ral.dmstatus.allhalted, data), 1) |
| end |
| `uvm_info(`gfn, "CPU has halted", UVM_MEDIUM) |
| endtask |
| |
| // Issues an NDM reset request. |
| virtual task set_ndmreset(bit value); |
| `uvm_info(`gfn, $sformatf("Setting ndmreset = %0b", value), UVM_MEDIUM) |
| csr_wr(.ptr(ral.dmcontrol.ndmreset), .value(value), .blocking(1), .predict(predict_dmi_writes)); |
| endtask |
| |
| // Issues a CPU resume reset request. |
| virtual task set_resumereq(bit value); |
| `uvm_info(`gfn, $sformatf("Setting resumereq = %0b", value), UVM_MEDIUM) |
| csr_wr(.ptr(ral.dmcontrol.resumereq), .value(value), .blocking(1), |
| .predict(predict_dmi_writes)); |
| endtask |
| |
| // Waits for the CPU to be in resumed state. |
| virtual task wait_cpu_resumed(int hart = 0); |
| uvm_reg_data_t data; |
| `uvm_info(`gfn, "Waiting for CPU to resume", UVM_MEDIUM) |
| `DV_SPINWAIT( |
| do begin |
| csr_rd(.ptr(ral.dmstatus), .value(data), .blocking(1)); |
| end while (dv_base_reg_pkg::get_field_val(ral.dmstatus.anyhalted, data) == 1); |
| ) |
| `DV_CHECK_EQ(dv_base_reg_pkg::get_field_val(ral.dmstatus.anyrunning, data), 1) |
| if (num_harts == 1) begin |
| `DV_CHECK_EQ(dv_base_reg_pkg::get_field_val(ral.dmstatus.allhalted, data), 0) |
| `DV_CHECK_EQ(dv_base_reg_pkg::get_field_val(ral.dmstatus.allrunning, data), 1) |
| end |
| `uvm_info(`gfn, "CPU has resumed", UVM_MEDIUM) |
| endtask |
| |
| // Checks if we are ready for executing abstract commands. |
| // |
| // Recommended to be run once at the very start of the debug session. |
| // ready: Returns whether the debug module is ready to accept abstract commands. |
| virtual task abstract_cmd_dm_ready(output bit ready); |
| uvm_reg_data_t data; |
| csr_rd(.ptr(ral.dmcontrol), .value(data), .blocking(1)); |
| ready = 1; |
| ready &= dv_base_reg_pkg::get_field_val(ral.dmcontrol.dmactive, data) == 1; |
| ready &= dv_base_reg_pkg::get_field_val(ral.dmcontrol.ackhavereset, data) == 0; |
| ready &= dv_base_reg_pkg::get_field_val(ral.dmcontrol.resumereq, data) == 0; |
| ready &= dv_base_reg_pkg::get_field_val(ral.dmcontrol.haltreq, data) == 0; |
| csr_rd(.ptr(ral.dmstatus), .value(data), .blocking(1)); |
| ready &= dv_base_reg_pkg::get_field_val(ral.dmstatus.allhalted, data) == 1; |
| csr_rd(.ptr(ral.abstractcs), .value(data), .blocking(1)); |
| ready &= dv_base_reg_pkg::get_field_val(ral.abstractcs.busy, data) == 0; |
| ready &= dv_base_reg_pkg::get_field_val(ral.abstractcs.cmderr, data) == AbstractCmdErrNone; |
| endtask |
| |
| // Waits for an abstract cmd to finish executing. |
| // |
| // status: Returns the status of the previous executed command. |
| // cmderr_clear: Clear the cmd error status (default off). |
| virtual task abstract_cmd_busy_wait(output abstract_cmd_err_e status, |
| input bit cmderr_clear = 0); |
| uvm_reg_data_t data; |
| `uvm_info(`gfn, "Waiting for an abstract command (if any) to complete execution", UVM_MEDIUM) |
| `DV_SPINWAIT( |
| do begin |
| csr_rd(.ptr(ral.abstractcs), .value(data), .blocking(1)); |
| end while (dv_base_reg_pkg::get_field_val(ral.abstractcs.busy, data) == 1); |
| ) |
| status = abstract_cmd_err_e'(dv_base_reg_pkg::get_field_val(ral.abstractcs.cmderr, data)); |
| if (status != AbstractCmdErrNone && cmderr_clear) begin |
| csr_wr(.ptr(ral.abstractcs.cmderr), .value(1), .blocking(1), .predict(predict_dmi_writes)); |
| end |
| `uvm_info(`gfn, "Abstract command completed", UVM_MEDIUM) |
| endtask |
| |
| // Disables the abstract cmd by setting the control field to 0. |
| virtual task abstract_cmd_disable(); |
| uvm_reg_data_t rw_data = '0; |
| rw_data = csr_utils_pkg::get_csr_val_with_updated_field(ral.command.cmdtype, rw_data, |
| AbstractCmdRegAccess); |
| csr_wr(.ptr(ral.command), .value(rw_data), .blocking(1), .predict(predict_dmi_writes)); |
| endtask |
| |
| // Writes the abstractauto register to enable automatic command execution. |
| // |
| // The default value for the args are set to 0 to prioritize disabling of the autoexec feature. |
| virtual task write_abstract_cmd_auto(bit autoexecdata = 0, bit autoexecprogbuf = 0); |
| uvm_reg_data_t rw_data = '0; |
| rw_data = csr_utils_pkg::get_csr_val_with_updated_field( |
| ral.abstractauto.autoexecprogbuf, rw_data, autoexecprogbuf); |
| rw_data = csr_utils_pkg::get_csr_val_with_updated_field( |
| ral.abstractauto.autoexecdata, rw_data, autoexecdata); |
| csr_wr(.ptr(ral.abstractauto), .value(rw_data), .blocking(1), .predict(predict_dmi_writes)); |
| endtask |
| |
| // Reads (a) CPU register(s) via an abstract command. |
| // |
| // regno: The starting CPU register number. |
| // value_q: A queue of returned reads. |
| // size: The Number of successive reads. The regno is incremented this many times. |
| // postexec: Have the CPU execute the progbuf immediately after the command. The progbuf is |
| // assumed to have been loaded already. |
| // TODO: add support for sub-word transfer size. |
| // TODO: Add support for reg read via program buffer. |
| virtual task abstract_cmd_reg_read(input abstract_cmd_regno_t regno, |
| output logic [BUS_DW-1:0] value_q[$], |
| output abstract_cmd_err_e status, |
| input int size = 1, |
| input bit postexec = 0); |
| uvm_reg_data_t rw_data = '0; |
| abstract_cmd_reg_access_t cmd = '0; |
| |
| if (aarpostincrement_supported && size > 1) begin |
| write_abstract_cmd_auto(.autoexecdata(1)); |
| cmd.aarpostincrement = 1; |
| end |
| |
| cmd.aarsize = 2; |
| cmd.postexec = postexec; |
| cmd.transfer = 1; |
| cmd.regno = regno; |
| rw_data = csr_utils_pkg::get_csr_val_with_updated_field(ral.command.cmdtype, rw_data, |
| AbstractCmdRegAccess); |
| if (aarpostincrement_supported) begin |
| // Write command only once at the start. |
| rw_data = csr_utils_pkg::get_csr_val_with_updated_field(ral.command.control, rw_data, |
| cmd); |
| csr_wr(.ptr(ral.command), .value(rw_data), .blocking(1), .predict(predict_dmi_writes)); |
| end |
| |
| for (int i = 0; i < size; i++) begin |
| if (!aarpostincrement_supported) begin |
| // Manually increment regno and write command for each read. |
| cmd.regno = regno + i; |
| rw_data = csr_utils_pkg::get_csr_val_with_updated_field(ral.command.control, rw_data, |
| cmd); |
| csr_wr(.ptr(ral.command), .value(rw_data), .blocking(1), .predict(predict_dmi_writes)); |
| end |
| abstract_cmd_busy_wait(.status(status), .cmderr_clear(1)); |
| // Let the caller handle error cases. |
| if (status != AbstractCmdErrNone) return; |
| // Before the last read, reset the autoexecdata bit. |
| if (aarpostincrement_supported && size > 1 && i == size - 1) write_abstract_cmd_auto(); |
| csr_rd(.ptr(ral.abstractdata[0]), .value(value_q[i]), .blocking(1)); |
| `uvm_info(`gfn, $sformatf("Read CPU register 0x%0h: 0x%0h", regno + i, value_q[i]), |
| UVM_MEDIUM) |
| end |
| endtask |
| |
| // Writes (a) CPU register(s) via an abstract command. |
| // |
| // regno: The starting CPU register number. |
| // value_q: A queue of data written. regno is incremented for each value. |
| // size: The Number of successive reads. |
| // postexec: Have the CPU execute the progbuf immediately after the command. The progbuf is |
| // assumed to have been loaded already. |
| // TODO: Add support for reg read via program buffer. |
| virtual task abstract_cmd_reg_write(input abstract_cmd_regno_t regno, |
| input logic [BUS_DW-1:0] value_q[$], |
| output abstract_cmd_err_e status, |
| input bit postexec = 0); |
| uvm_reg_data_t rw_data = '0; |
| abstract_cmd_reg_access_t cmd = '0; |
| |
| csr_wr(.ptr(ral.abstractdata[0]), .value(value_q[0]), .blocking(1), |
| .predict(predict_dmi_writes)); |
| |
| cmd.aarsize = 2; |
| cmd.postexec = postexec; |
| cmd.transfer = 1; |
| cmd.write = 1; |
| cmd.regno = regno; |
| cmd.aarpostincrement = aarpostincrement_supported && (value_q.size() > 1); |
| rw_data = csr_utils_pkg::get_csr_val_with_updated_field(ral.command.cmdtype, rw_data, |
| AbstractCmdRegAccess); |
| rw_data = csr_utils_pkg::get_csr_val_with_updated_field(ral.command.control, rw_data, |
| cmd); |
| csr_wr(.ptr(ral.command), .value(rw_data), .blocking(1), .predict(predict_dmi_writes)); |
| `uvm_info(`gfn, $sformatf("Wrote 0x%0h to CPU register 0x%0h", value_q[0], cmd.regno), |
| UVM_MEDIUM) |
| |
| if (aarpostincrement_supported && (value_q.size() > 1)) begin |
| write_abstract_cmd_auto(.autoexecdata(1)); |
| end |
| |
| for (int i = 1; i < value_q.size(); i++) begin |
| abstract_cmd_busy_wait(.status(status), .cmderr_clear(1)); |
| // Let the caller handle error cases. |
| if (status != AbstractCmdErrNone) return; |
| csr_wr(.ptr(ral.abstractdata[0]), .value(value_q[i]), .blocking(1), |
| .predict(predict_dmi_writes)); |
| if (!aarpostincrement_supported) begin |
| // Manually increment regno and write command for each write. |
| cmd.regno = regno + i; |
| rw_data = csr_utils_pkg::get_csr_val_with_updated_field(ral.command.control, rw_data, |
| cmd); |
| csr_wr(.ptr(ral.command), .value(rw_data), .blocking(1), .predict(predict_dmi_writes)); |
| end |
| `uvm_info(`gfn, $sformatf("Wrote 0x%0h to CPU register 0x%0h", value_q[i], regno + i), |
| UVM_MEDIUM) |
| end |
| abstract_cmd_busy_wait(.status(status), .cmderr_clear(1)); |
| // After the last write, reset the autoexecdata bit. |
| if (aarpostincrement_supported && (value_q.size() > 1)) write_abstract_cmd_auto(); |
| endtask |
| |
| // Reads (a) system memory location(s) via an abstract command. |
| // |
| // The system memory address space can be accessed either via `AbstractCmdMemAccess` abstract |
| // command type, or indirectly using the CPU using the program buffer. The former is not currently |
| // supported. |
| // addr: The starting system address. |
| // value_q: The read data returned to the caller. |
| // status: The status of the op returned to the caller. |
| // size: The size of the read access in terms of full bus words. |
| // route: The route taken to access the system memory. |
| virtual task abstract_cmd_mem_read(input logic [BUS_AW-1:0] addr, |
| output logic [BUS_DW-1:0] value_q[$], |
| output abstract_cmd_err_e status, |
| input int size = 1, |
| input mem_access_route_e route = MemAccessViaProgbuf); |
| `DV_CHECK(size) |
| case (route) |
| MemAccessViaAbstractCmd: begin |
| `uvm_fatal(`gfn, "Accessing the system memory using AbstractCmdMemAccess is not supproted") |
| end |
| MemAccessViaProgbuf: begin |
| logic [BUS_DW-1:0] progbuf_q[$]; |
| logic [BUS_DW-1:0] rwdata[$]; |
| logic [BUS_DW-1:0] s0, s1; |
| |
| if (size == 1) progbuf_q = '{LwS0S0, Ebreak}; |
| else progbuf_q = '{LwS1S0, AddiS0S04, Ebreak}; |
| |
| abstract_cmd_reg_read(.regno(RvCoreCsrGprS0), .value_q(rwdata), .status(status)); |
| if (status != AbstractCmdErrNone) return; |
| s0 = rwdata[0]; |
| if (size > 1) begin |
| abstract_cmd_reg_read(.regno(RvCoreCsrGprS1), .value_q(rwdata), .status(status)); |
| if (status != AbstractCmdErrNone) return; |
| s1 = rwdata[0]; |
| end |
| `uvm_info(`gfn, $sformatf("Saved CPU registers S0 = 0x%0h and S1 = 0x%0h", s0, s1), |
| UVM_HIGH) |
| |
| value_q.delete(); |
| write_progbuf(progbuf_q); |
| abstract_cmd_reg_write(.regno(RvCoreCsrGprS0), .value_q({addr}), .status(status), |
| .postexec(1)); |
| if (status != AbstractCmdErrNone) return; |
| abstract_cmd_busy_wait(.status(status), .cmderr_clear(1)); |
| if (status != AbstractCmdErrNone) return; |
| if (size == 1) begin |
| abstract_cmd_reg_read(.regno(RvCoreCsrGprS0), .value_q(value_q), .status(status)); |
| if (status != AbstractCmdErrNone) return; |
| end else begin |
| abstract_cmd_reg_read(.regno(RvCoreCsrGprS1), .value_q(rwdata), .status(status), |
| .postexec(1)); |
| if (status != AbstractCmdErrNone) return; |
| value_q[0] = rwdata[0]; |
| `uvm_info(`gfn, $sformatf("Read from system memory: [0x%0h] = 0x%0h", addr, |
| value_q[0]), UVM_MEDIUM) |
| write_abstract_cmd_auto(.autoexecdata(1)); |
| for (int i = 1; i < size; i++) begin |
| abstract_cmd_busy_wait(.status(status), .cmderr_clear(1)); |
| if (status != AbstractCmdErrNone) return; |
| // Before the last read, reset the autoexecdata bit. |
| if (i == size - 1) write_abstract_cmd_auto(); |
| csr_rd(.ptr(ral.abstractdata[0]), .value(value_q[i]), .blocking(1)); |
| `uvm_info(`gfn, $sformatf("Read from system memory: [0x%0h] = 0x%0h", addr + i * 4, |
| value_q[i]), UVM_MEDIUM) |
| end |
| end |
| |
| abstract_cmd_reg_write(.regno(RvCoreCsrGprS0), .value_q({s0}), .status(status)); |
| if (status != AbstractCmdErrNone) return; |
| if (size > 1) begin |
| abstract_cmd_reg_write(.regno(RvCoreCsrGprS1), .value_q({s1}), .status(status)); |
| if (status != AbstractCmdErrNone) return; |
| end |
| `uvm_info(`gfn, $sformatf("Restored CPU registers S0 = 0x%0h and S1 = 0x%0h", s0, s1), |
| UVM_HIGH) |
| end |
| MemAccessViaSba: begin |
| `uvm_fatal(`gfn, "Please invoke sba_read instead") |
| end |
| default: begin |
| `uvm_fatal(`gfn, $sformatf("The mem access route %s is invalid or unsupported", |
| route.name())) |
| end |
| endcase |
| endtask |
| |
| // Writes (a) system memory location(s) via an abstract command. |
| // |
| // The system memory address space can be accessed either via `AbstractCmdMemAccess` abstract |
| // command type, or indirectly using the CPU using the program buffer. The former is not currently |
| // supported. |
| // addr: The starting system address. |
| // value_q: The data set to be written. |
| // status: The status of the op returned to the caller. |
| // route: The route taken to access the system memory. |
| virtual task abstract_cmd_mem_write(input logic [BUS_AW-1:0] addr, |
| input logic [BUS_DW-1:0] value_q[$], |
| output abstract_cmd_err_e status, |
| input mem_access_route_e route = MemAccessViaProgbuf); |
| `DV_CHECK(value_q.size()) |
| case (route) |
| MemAccessViaAbstractCmd: begin |
| `uvm_fatal(`gfn, "Accessing the system memory using AbstractCndMemAccess is not supproted") |
| end |
| MemAccessViaProgbuf: begin |
| logic [BUS_DW-1:0] progbuf_q[$]; |
| logic [BUS_DW-1:0] rwdata[$]; |
| logic [BUS_DW-1:0] s0, s1; |
| |
| if (value_q.size() == 1) progbuf_q = '{SwS1S0, Ebreak}; |
| else progbuf_q = '{SwS1S0, AddiS0S04, Ebreak}; |
| |
| abstract_cmd_reg_read(.regno(RvCoreCsrGprS0), .value_q(rwdata), .status(status)); |
| if (status != AbstractCmdErrNone) return; |
| s0 = rwdata[0]; |
| abstract_cmd_reg_read(.regno(RvCoreCsrGprS1), .value_q(rwdata), .status(status)); |
| if (status != AbstractCmdErrNone) return; |
| s1 = rwdata[0]; |
| `uvm_info(`gfn, $sformatf("Saved CPU registers S0 = 0x%0h and S1 = 0x%0h", s0, s1), |
| UVM_HIGH) |
| |
| write_progbuf(progbuf_q); |
| abstract_cmd_reg_write(.regno(RvCoreCsrGprS0), .value_q({addr}), .status(status)); |
| if (status != AbstractCmdErrNone) return; |
| abstract_cmd_reg_write(.regno(RvCoreCsrGprS1), .value_q({value_q[0]}), .status(status), |
| .postexec(1)); |
| if (status != AbstractCmdErrNone) return; |
| `uvm_info(`gfn, $sformatf("Wrote to system memory: [0x%0h] = 0x%0h", addr, value_q[0]), |
| UVM_MEDIUM) |
| if (value_q.size() > 1) begin |
| write_abstract_cmd_auto(.autoexecdata(1)); |
| for (int i = 1; i < value_q.size(); i++) begin |
| csr_wr(.ptr(ral.abstractdata[0]), .value(value_q[i]), .blocking(1), |
| .predict(predict_dmi_writes)); |
| abstract_cmd_busy_wait(.status(status), .cmderr_clear(1)); |
| if (status != AbstractCmdErrNone) return; |
| `uvm_info(`gfn, $sformatf("Wrote to system memory: [0x%0h] = 0x%0h", addr + i * 4, |
| value_q[i]), UVM_MEDIUM) |
| end |
| write_abstract_cmd_auto(); |
| end |
| |
| abstract_cmd_reg_write(.regno(RvCoreCsrGprS0), .value_q({s0}), .status(status)); |
| if (status != AbstractCmdErrNone) return; |
| abstract_cmd_reg_write(.regno(RvCoreCsrGprS1), .value_q({s1}), .status(status)); |
| if (status != AbstractCmdErrNone) return; |
| `uvm_info(`gfn, $sformatf("Restored CPU registers S0 = 0x%0h and S1 = 0x%0h", s0, s1), |
| UVM_HIGH) |
| end |
| MemAccessViaSba: begin |
| `uvm_fatal(`gfn, "Please invoke sba_write instead") |
| end |
| default: begin |
| `uvm_fatal(`gfn, $sformatf("The mem access route %s is invalid or unsupported", |
| route.name())) |
| end |
| endcase |
| endtask |
| |
| // Have the CPU execute arbitrary instructions in the program buffer. |
| virtual task abstract_cmd_exec_progbuf(input logic [BUS_DW-1:0] progbuf_q[$], |
| output abstract_cmd_err_e status); |
| uvm_reg_data_t rw_data = '0; |
| abstract_cmd_reg_access_t cmd = '0; |
| write_progbuf(progbuf_q); |
| cmd.postexec = 1; |
| cmd.transfer = 0; |
| rw_data = csr_utils_pkg::get_csr_val_with_updated_field(ral.command.cmdtype, rw_data, |
| AbstractCmdRegAccess); |
| rw_data = csr_utils_pkg::get_csr_val_with_updated_field(ral.command.control, rw_data, |
| cmd); |
| csr_wr(.ptr(ral.command), .value(rw_data), .blocking(1), .predict(predict_dmi_writes)); |
| abstract_cmd_busy_wait(.status(status), .cmderr_clear(1)); |
| endtask |
| |
| // Issue a quick access command. |
| virtual task abstract_cmd_quick_access(output abstract_cmd_err_e status, |
| input logic [BUS_DW-1:0] progbuf_q[$] = {}); |
| uvm_reg_data_t rw_data = '0; |
| // TODO: Check if core is not halted. |
| write_progbuf(progbuf_q); |
| rw_data = csr_utils_pkg::get_csr_val_with_updated_field(ral.command.cmdtype, rw_data, |
| AbstractCmdQuickAccess); |
| csr_wr(.ptr(ral.command), .value(rw_data), .blocking(1), .predict(predict_dmi_writes)); |
| abstract_cmd_busy_wait(.status(status), .cmderr_clear(1)); |
| // Note: The external testbench is expected to check the CPU automatically halted, executed the |
| // progbuf and then, resumed. |
| endtask |
| |
| // Write arbitrary commands into progbuf. |
| virtual task write_progbuf(logic [BUS_DW-1:0] progbuf_q[$]); |
| `DV_CHECK_LE_FATAL(progbuf_q.size(), ral.abstractcs.progbufsize.get()) |
| foreach (progbuf_q[i]) begin |
| csr_wr(.ptr(ral.progbuf[i]), .value(progbuf_q[i]), .blocking(1), |
| .predict(predict_dmi_writes)); |
| end |
| `uvm_info(`gfn, $sformatf("Wrote progbuf: %p", progbuf_q), UVM_MEDIUM) |
| endtask |
| |
| // Returns the saved PC via DPC register. |
| virtual task read_pc(output logic [BUS_DW-1:0] pc); |
| abstract_cmd_err_e status; |
| logic [BUS_DW-1:0] cmd_data[$]; |
| abstract_cmd_reg_read(.regno(RvCoreCsrDpc), .value_q(cmd_data), .status(status)); |
| `DV_CHECK_EQ(status, AbstractCmdErrNone) |
| `uvm_info(`gfn, $sformatf("DPC = 0x%0h", cmd_data[0]), UVM_LOW) |
| pc = cmd_data[0]; |
| endtask |
| |
| // Set PC to a new value with address or the symbol |
| virtual task write_pc(logic [BUS_AW-1:0] addr = 'x, string symbol = ""); |
| abstract_cmd_err_e status; |
| logic [BUS_DW-1:0] cmd_data[$]; |
| |
| addr_or_symbol_set(addr, symbol); |
| abstract_cmd_reg_write(.regno(RvCoreCsrDpc), .value_q({addr}), .status(status)); |
| `DV_CHECK_EQ(status, AbstractCmdErrNone) |
| `uvm_info(`gfn, $sformatf("Wrote new DPC = 0x%0h", addr), UVM_LOW) |
| endtask |
| |
| // Returns the DCSR value. |
| virtual task read_dcsr(output rv_core_csr_dcsr_t dcsr); |
| abstract_cmd_err_e status; |
| logic [BUS_DW-1:0] cmd_data[$]; |
| abstract_cmd_reg_read(.regno(RvCoreCsrDcsr), .value_q(cmd_data), .status(status)); |
| `DV_CHECK_EQ(status, AbstractCmdErrNone) |
| dcsr = rv_core_csr_dcsr_t'(cmd_data[0]); |
| `uvm_info(`gfn, $sformatf("DCSR = %p", dcsr), UVM_MEDIUM) |
| endtask |
| |
| // Set the DCSR value. |
| virtual task write_dcsr(rv_core_csr_dcsr_t dcsr); |
| abstract_cmd_err_e status; |
| abstract_cmd_reg_write(.regno(RvCoreCsrDcsr), .value_q({BUS_DW'(dcsr)}), .status(status)); |
| `DV_CHECK_EQ(status, AbstractCmdErrNone) |
| step_entered = dcsr.step; |
| endtask |
| |
| // Internal helper function which returns the address of a symbol from the elf file. |
| protected virtual function logic [BUS_AW-1:0] get_symbol_address(string symbol); |
| longint unsigned addr, size; |
| // Return address if we already looked it up before. |
| if (symbol_table.exists(symbol)) return symbol_table[symbol]; |
| `DV_CHECK(dv_utils_pkg::sw_symbol_get_addr_size(.elf_file(elf_file), .symbol(symbol), |
| .does_not_exist_ok(0), .addr(addr), .size(size))) |
| return BUS_AW'(addr); |
| endfunction |
| |
| // Internal helper function enforces either address or the symbol to be set. |
| // |
| // It throws an error if both, symbol and addr are set to known values. It also throws an error if |
| // neither of them are set. If the symbol is set, then the address is updated to the symbol's |
| // physical address. |
| // |
| // addr: The address. If set to 'x, it is updated to the symbol's physical address and returned. |
| // symbol: The symbolic address. |
| protected virtual function void addr_or_symbol_set(inout logic [BUS_AW-1:0] addr, |
| input string symbol); |
| if (addr === 'x && symbol == "") begin |
| `uvm_fatal(`gfn, "Either addr or symbol expected to be set. Neither are set.") |
| end else if (addr !== 'x && symbol != "") begin |
| `uvm_fatal(`gfn, "Either addr or symbol expected to be set. Both are set.") |
| end |
| if (symbol != "") addr = get_symbol_address(symbol); |
| endfunction |
| |
| // Checks if the halted CPU's PC is at the given address or symbol. |
| // |
| // exp_addr: Expected address. |
| // exp_symbol: Expected symbolic address. |
| // error_if_eq: 0: throw error on mismatch, 1: throw error on match. |
| // NOTE: Either exp_addr or exp_symbol must be set (pseudo-function overloading). |
| virtual task check_pc(logic [BUS_AW-1:0] exp_addr = 'x, |
| string exp_symbol = "", |
| bit error_if_eq = 0); |
| logic [BUS_DW-1:0] pc; |
| addr_or_symbol_set(exp_addr, exp_symbol); |
| read_pc(pc); |
| if (error_if_eq) `DV_CHECK_NE(pc, exp_addr) |
| else `DV_CHECK_EQ(pc, exp_addr) |
| endtask |
| |
| // Checks if the reason for entering the debug mode matches the expected cause. |
| // |
| // exp_debug_cause: The expected debug cause. |
| virtual task check_debug_cause(rv_core_csr_dcsr_cause_e exp_debug_cause); |
| rv_core_csr_dcsr_t dcsr; |
| read_dcsr(dcsr); |
| `DV_CHECK_EQ(dcsr.cause, exp_debug_cause) |
| endtask |
| |
| // Check if trigger select is valid. |
| virtual task is_valid_tselect_index(input int index, output bit valid); |
| abstract_cmd_err_e status; |
| logic [BUS_DW-1:0] cmd_data[$]; |
| abstract_cmd_reg_write(.regno(RvCoreCsrTSelect), .value_q({index}), .status(status)); |
| `DV_CHECK_EQ(status, AbstractCmdErrNone) |
| abstract_cmd_reg_read(.regno(RvCoreCsrTSelect), .value_q(cmd_data), .status(status)); |
| `DV_CHECK_EQ(status, AbstractCmdErrNone) |
| valid = (index == cmd_data[0]); |
| endtask |
| |
| // List breakpoints. |
| // |
| // TODO: Only match type breakpoints on address supported. |
| // For each breakpoint, list the following information: |
| // Breakpoint on addr|data 0x%0d before|after load|store|execute op. |
| // breakpoints: The list of breakpoints returned to the caller. |
| // delete_breakpoints: Optionally, enable deleting all breakpoints to allow further execution. |
| virtual task info_breakpoints(output breakpoint_t breakpoints[$], |
| input bit delete_breakpoints = 0); |
| abstract_cmd_err_e status; |
| logic [BUS_DW-1:0] cmd_data[$]; |
| |
| breakpoints.delete(); |
| for (int i = 0; i >= 0; i++) begin // Infinite loop. |
| bit valid; |
| is_valid_tselect_index(i, valid); |
| if (valid) begin |
| rv_core_csr_trigger_mcontrol_t mcontrol; |
| abstract_cmd_reg_read(.regno(RvCoreCsrTData1), .value_q(cmd_data), .status(status)); |
| `DV_CHECK_EQ(status, AbstractCmdErrNone) |
| mcontrol = rv_core_csr_trigger_mcontrol_t'(cmd_data[0]); |
| if (!(mcontrol.trigger_type == RvTriggerTypeMatch && |
| (mcontrol.load || mcontrol.store || mcontrol.execute))) continue; |
| // This slot has as a valid breakpoint. |
| abstract_cmd_reg_read(.regno(RvCoreCsrTData2), .value_q(cmd_data), .status(status)); |
| `DV_CHECK_EQ(status, AbstractCmdErrNone) |
| breakpoints.push_back( |
| '{trigger_type: RvTriggerTypeMatch, |
| index : i, |
| tdata1 : BUS_DW'(mcontrol), |
| tdata2 : cmd_data[0]}); |
| if (delete_breakpoints) clear_tdata1(0); |
| end else break; |
| end |
| print_breakpoints(breakpoints); |
| endtask |
| |
| // Prints breakpoints in human-friendly way. |
| virtual function void print_breakpoints(ref breakpoint_t breakpoints[$]); |
| string format = {"\n\t%0d: %0s type breakpoint on %0s 0x%0h %0s 3'b%0b(execute|store|load) ", |
| "op, with action %0s"}; |
| string msg = ""; |
| |
| foreach (breakpoints[i]) begin |
| rv_core_csr_trigger_mcontrol_t mcontrol = rv_core_csr_trigger_mcontrol_t'( |
| breakpoints[i].tdata1); |
| msg = {msg, $sformatf(format, |
| breakpoints[i].index, |
| breakpoints[i].trigger_type.name(), |
| mcontrol.select ? "data" : "addr", |
| breakpoints[i].tdata2, |
| mcontrol.timing ? "after" : "before", |
| {mcontrol.execute, mcontrol.store, mcontrol.load}, |
| mcontrol.action.name())}; |
| end |
| if (msg != "") `uvm_info(`gfn, msg, UVM_LOW) |
| else `uvm_info(`gfn, "No breakpoints added yet", UVM_LOW) |
| endfunction |
| |
| // Insert a breakpoint at the given address or symbol. |
| // |
| // addr: The PC address. |
| // symbol: Symbolic address. |
| // TODO: Only trigger on exact address match before execution supported. |
| virtual task add_breakpoint(logic [BUS_AW-1:0] addr = 'x, string symbol = ""); |
| abstract_cmd_err_e status; |
| logic [BUS_DW-1:0] cmd_data[$]; |
| |
| addr_or_symbol_set(addr, symbol); |
| `uvm_info(`gfn, $sformatf("Adding breakpoint on 0x%0h(%0s)", addr, symbol), UVM_LOW) |
| for (int i = 0; i >= 0; i++) begin // Infinite loop. |
| bit valid; |
| is_valid_tselect_index(i, valid); |
| if (valid) begin |
| rv_core_csr_trigger_mcontrol_t mcontrol; |
| abstract_cmd_reg_read(.regno(RvCoreCsrTData1), .value_q(cmd_data), .status(status)); |
| `DV_CHECK_EQ(status, AbstractCmdErrNone) |
| mcontrol = rv_core_csr_trigger_mcontrol_t'(cmd_data[0]); |
| if (mcontrol.trigger_type == RvTriggerTypeMatch && |
| (mcontrol.load || mcontrol.store || mcontrol.execute)) continue; |
| // This slot is available for a new breakpoint. |
| mcontrol.trigger_type = RvTriggerTypeMatch; |
| mcontrol.dmode = 1; |
| mcontrol.select = 0; |
| mcontrol.timing = 0; |
| mcontrol.action = RvTriggerActionEnterDebugMode; |
| mcontrol.match = 0; |
| mcontrol.u = 1; |
| mcontrol.execute = 1; |
| mcontrol.store = 0; |
| mcontrol.load = 0; |
| abstract_cmd_reg_write(.regno(RvCoreCsrTData1), .value_q({BUS_DW'(mcontrol)}), |
| .status(status)); |
| `DV_CHECK_EQ(status, AbstractCmdErrNone) |
| abstract_cmd_reg_write(.regno(RvCoreCsrTData2), .value_q({addr}), .status(status)); |
| `DV_CHECK_EQ(status, AbstractCmdErrNone) |
| return; |
| end else break; |
| end |
| `uvm_error(`gfn, "No free breakpoint slots available") |
| endtask |
| |
| // Restores previously deleted breakpoints. |
| virtual task restore_breakpoints(breakpoint_t breakpoints[$]); |
| `DV_CHECK(breakpoints.size()) |
| foreach (breakpoints[i]) begin |
| abstract_cmd_err_e status; |
| abstract_cmd_reg_write(.regno(RvCoreCsrTSelect), .value_q({breakpoints[i].index}), |
| .status(status)); |
| `DV_CHECK_EQ(status, AbstractCmdErrNone) |
| abstract_cmd_reg_write(.regno(RvCoreCsrTData1), .value_q({breakpoints[i].tdata1}), |
| .status(status)); |
| `DV_CHECK_EQ(status, AbstractCmdErrNone) |
| abstract_cmd_reg_write(.regno(RvCoreCsrTData2), .value_q({breakpoints[i].tdata2}), |
| .status(status)); |
| `DV_CHECK_EQ(status, AbstractCmdErrNone) |
| end |
| endtask |
| |
| // Clear trigger data1 register and optionally, tdata2 as well. |
| // |
| // clear_tdata2: Knob to clear tdata2 as well. |
| // Note: It is upto the the caller to ensure the right trigger is selected first. Also, we are |
| // leaving tdata2 in a stale state. |
| protected virtual task clear_tdata1(bit clear_tdata2); |
| abstract_cmd_err_e status; |
| abstract_cmd_reg_write(.regno(RvCoreCsrTData1), .value_q({0}), .status(status)); |
| `DV_CHECK_EQ(status, AbstractCmdErrNone) |
| if (clear_tdata2) begin |
| abstract_cmd_reg_write(.regno(RvCoreCsrTData2), .value_q({0}), .status(status)); |
| `DV_CHECK_EQ(status, AbstractCmdErrNone) |
| end |
| endtask |
| |
| // Delete an existing breakpoint by index, or on the given address or symbol. |
| // |
| // index: The selected trigger index. |
| // addr: The PC address. |
| // symbol: Symbolic PC address. |
| // clear_tdata2: Knob to clear tdata2 as well. |
| // Note: Only either set index, addr or symbol. |
| virtual task delete_breakpoint(logic [BUS_AW-1:0] addr = 'x, |
| string symbol = "", |
| int index = -1, |
| bit clear_tdata2 = 0); |
| abstract_cmd_err_e status; |
| logic [BUS_DW-1:0] cmd_data[$]; |
| |
| if (index != -1) begin |
| bit valid; |
| if (addr !== 'x || symbol != "") `uvm_fatal(`gfn, "Only set either index, or addr or symbol") |
| is_valid_tselect_index(index, valid); |
| |
| if (valid) begin |
| `uvm_info(`gfn, $sformatf("Deleting breakpoint at index %0d", index), UVM_LOW) |
| clear_tdata1(clear_tdata2); |
| end else begin |
| `uvm_error(`gfn, $sformatf("Index %0d appears to be out of bounds", index)) |
| end |
| return; |
| end |
| |
| addr_or_symbol_set(addr, symbol); |
| for (int i = 0; i >= 0; i++) begin // Infinite loop. |
| bit valid; |
| is_valid_tselect_index(i, valid); |
| if (valid) begin |
| rv_core_csr_trigger_mcontrol_t mcontrol; |
| abstract_cmd_reg_read(.regno(RvCoreCsrTData1), .value_q(cmd_data), .status(status)); |
| `DV_CHECK_EQ(status, AbstractCmdErrNone) |
| mcontrol = rv_core_csr_trigger_mcontrol_t'(cmd_data[0]); |
| if (!(mcontrol.trigger_type == RvTriggerTypeMatch && |
| (mcontrol.load || mcontrol.store || mcontrol.execute))) continue; |
| // This slot has as a valid breakpoint. Check if address matches. |
| abstract_cmd_reg_read(.regno(RvCoreCsrTData2), .value_q(cmd_data), .status(status)); |
| `DV_CHECK_EQ(status, AbstractCmdErrNone) |
| if (addr == cmd_data[0]) begin |
| `uvm_info(`gfn, $sformatf("Deleting breakpoint on 0x%0h(%0s)", addr, symbol), UVM_LOW) |
| clear_tdata1(clear_tdata2); |
| return; |
| end |
| end else break; |
| end |
| `uvm_error(`gfn, $sformatf("No breakpoint found associated with addr 0x%0h (%s)", addr, |
| symbol)) |
| endtask |
| |
| // Single step. |
| // |
| // Read-modify-writes the DCSR register with the step field set to 1. The `step_entered` local |
| // status bit is checked for improved performance. |
| virtual task step(int num = 1); |
| if (!step_entered) begin |
| rv_core_csr_dcsr_t dcsr; |
| read_dcsr(dcsr); |
| dcsr.step = 1; |
| write_dcsr(dcsr); |
| end |
| `uvm_info(`gfn, "Single stepping", UVM_LOW) |
| set_resumereq(1); |
| wait_cpu_halted(); |
| endtask |
| |
| // Resume execution after entering a halted state. |
| // |
| // Equivalent of `continue` gdb command (`continue` is a reserved keyword in SystemVerilog, so we |
| // pick the name `continue_execution()` instead). |
| // If there is a breakpoint on the DPC register, then this task deletes all breakpoints, |
| // single-steps, restores all breakpoints and then resumes. |
| // do_wait_cpu_resumed: Optionally, poll the dmstatus register to wait for the CPU to have |
| // resumed (default off). |
| virtual task continue_execution(bit do_wait_cpu_resumed = 0); |
| logic [BUS_DW-1:0] pc; |
| breakpoint_t breakpoints[$]; |
| rv_core_csr_dcsr_t dcsr; |
| |
| read_pc(pc); |
| info_breakpoints(.breakpoints(breakpoints)); |
| foreach (breakpoints[i]) begin |
| if (breakpoints[i].tdata2 == pc) begin |
| delete_breakpoint(.addr(pc)); |
| step(); |
| restore_breakpoints(.breakpoints({breakpoints[i]})); |
| break; |
| end |
| end |
| |
| read_dcsr(dcsr); |
| dcsr.step = 0; |
| write_dcsr(dcsr); |
| `uvm_info(`gfn, "Continuing execution", UVM_LOW) |
| set_resumereq(1); |
| |
| // NOTE: It may take a short while for the CPU to resume. If there are other upcoming |
| // breakpoints, the CPU may execute and immediately halt again, by the time `wait_cpu_resumed()` |
| // reads the dmstatus register to ascertain the CPU has indeed resumed. That will result in a |
| // timeout error. So, it is left to the caller to synchronize these events. The step below is |
| // hence conditioned on an optional argument which is set to 0 by default. |
| if (do_wait_cpu_resumed) wait_cpu_resumed(); |
| endtask |
| |
| // Execute calls as one instruction. |
| virtual task next(); |
| `uvm_fatal(`gfn, "Not implemented") |
| endtask |
| |
| // Finish executing the current function. |
| // |
| // It is assumed that the CPU is in a halted state at this point, and has just begun executing an |
| // arbitrary function. It reads the CPU's RA register and adds a breakpoint on it before |
| // continuing the execution. |
| // |
| // Note that this task adds a new breakpoint on the return address. After the CPU resumes, it |
| // waits for a few TCK cycles to let the CPU exit the debug mode. Then, it waits for the CPU to |
| // halt again. When it halts, the newly added breakpoint is deleted. |
| // return_address: Use an externally provided return address. If set to X, it reads the return |
| // address from the RA register. |
| // noreturn_function: Indicate that the function does not return. If set to 1, a breakpoint on RA |
| // is not added, and the task exits immediately after having the CPU continue |
| // executing the function. |
| virtual task finish(logic [BUS_AW-1:0] return_address = 'x, bit noreturn_function = 0); |
| if (return_address === 'x) begin |
| abstract_cmd_err_e status; |
| logic [BUS_DW-1:0] cmd_data[$]; |
| abstract_cmd_reg_read(.regno(RvCoreCsrGprRa), .value_q(cmd_data), .status(status)); |
| `DV_CHECK_EQ(status, AbstractCmdErrNone) |
| return_address = BUS_AW'(cmd_data[0]); |
| end |
| `uvm_info(`gfn, $sformatf("Adding a breakpoint on the return address: 0x%0h", |
| return_address), UVM_MEDIUM) |
| |
| if (!noreturn_function) add_breakpoint(.addr(return_address)); |
| continue_execution(); |
| if (!noreturn_function) begin |
| // It is better to wait for a few TCKs than call wait_cpu_resumed(), since the latter takes a |
| // long time, within which the CPU could have re-entered the halted state. |
| cfg.vif.wait_tck(20); |
| wait_cpu_halted(); |
| check_pc(.exp_addr(return_address)); |
| delete_breakpoint(.addr(return_address)); |
| end |
| endtask |
| |
| // Have the CPU execute a function with the specified arguments. |
| // |
| // addr: The function address. |
| // symbol: The function's symbol. |
| // args: The function args. |
| // noreturn_function: Indicate that the CPU does not return from this function. |
| virtual task call(logic [BUS_AW-1:0] addr = 'x, |
| string symbol = "", |
| logic [BUS_DW-1:0] args[$] = {}, |
| bit noreturn_function = 0); |
| byte status; |
| abstract_cmd_err_e abs_status; |
| logic [BUS_DW-1:0] cmd_data[$]; |
| logic [BUS_DW-1:0] gprs[abstract_cmd_regno_t], new_sp; |
| |
| `uvm_info(`gfn, "Saving all necessary GPRS", UVM_MEDIUM) |
| `DV_CHECK(args.size() <= 8) |
| abstract_cmd_reg_read(.regno(RvCoreCsrGprRa), .value_q(cmd_data), .status(abs_status)); |
| `DV_CHECK_EQ(abs_status, AbstractCmdErrNone) |
| gprs[RvCoreCsrGprRa] = cmd_data[0]; |
| abstract_cmd_reg_read(.regno(RvCoreCsrGprSp), .value_q(cmd_data), .status(abs_status)); |
| `DV_CHECK_EQ(abs_status, AbstractCmdErrNone) |
| gprs[RvCoreCsrGprSp] = cmd_data[0]; |
| // Setup a new stack 32 bytes from the current stack pointer, and align it to 32 bytes. |
| new_sp = (cmd_data[0] - 32) & ~('h1f); |
| abstract_cmd_reg_read(.regno(RvCoreCsrGprGp), .value_q(cmd_data), .status(abs_status)); |
| `DV_CHECK_EQ(abs_status, AbstractCmdErrNone) |
| gprs[RvCoreCsrGprGp] = cmd_data[0]; |
| for (int i = 0; i < cmd_data.size(); i++) begin |
| abstract_cmd_reg_read(.regno(RvCoreCsrGprA0 + i), .value_q(cmd_data), .status(abs_status)); |
| `DV_CHECK_EQ(abs_status, AbstractCmdErrNone) |
| gprs[RvCoreCsrGprA0 + i] = cmd_data[0]; |
| end |
| read_pc(gprs[RvCoreCsrDpc]); |
| |
| `uvm_info(`gfn, $sformatf("Setting new SP and RA: 0x%0h", new_sp), UVM_MEDIUM) |
| abstract_cmd_reg_write(.regno(RvCoreCsrGprRa), .value_q({new_sp}), .status(abs_status)); |
| `DV_CHECK_EQ(abs_status, AbstractCmdErrNone) |
| abstract_cmd_reg_write(.regno(RvCoreCsrGprSp), .value_q({new_sp}), .status(abs_status)); |
| `DV_CHECK_EQ(abs_status, AbstractCmdErrNone) |
| `uvm_info(`gfn, "Writing the opcode 'ebreak' at the SP address", UVM_MEDIUM) |
| mem_write(.addr(new_sp), .value_q({Ebreak}), .status(status)); |
| `DV_CHECK_EQ(status, 0) |
| `uvm_info(`gfn, $sformatf("Writing function arguments [%p] to A registers", args), UVM_MEDIUM) |
| foreach (args[i]) begin |
| abstract_cmd_reg_write(.regno(RvCoreCsrGprA0 + i), .value_q({args[i]}), .status(abs_status)); |
| `DV_CHECK_EQ(abs_status, AbstractCmdErrNone) |
| end |
| |
| write_pc(addr, symbol); |
| finish(.return_address(new_sp), .noreturn_function(noreturn_function)); |
| |
| if (!noreturn_function) begin |
| foreach (gprs[i]) begin |
| abstract_cmd_reg_write(.regno(i), .value_q({gprs[i]}), .status(abs_status)); |
| `DV_CHECK_EQ(abs_status, AbstractCmdErrNone) |
| end |
| `uvm_info(`gfn, "Restored all necessary GPRS", UVM_MEDIUM) |
| end |
| endtask |
| |
| // Initiates a single SBA access through JTAG (-> DTM -> DMI -> SBA). |
| // |
| // It writes DMI SBA registers to create a read or write access on the system bus, poll for its |
| // completion and return the response (on reads). |
| // |
| // req: The SBA access request item. It will be updated with the responses. |
| // sba_access_err_clear: Knob to clear the SBA access errors. |
| virtual task sba_access(sba_access_item req, bit sba_access_err_clear = 1'b1); |
| uvm_reg_data_t rdata, wdata; |
| |
| // Update sbcs for the new transaction. |
| wdata = ral.sbcs.get_mirrored_value(); |
| wdata = get_csr_val_with_updated_field(ral.sbcs.sbaccess, wdata, req.size); |
| if (req.bus_op == BusOpRead) begin |
| wdata = get_csr_val_with_updated_field(ral.sbcs.sbreadonaddr, wdata, req.readonaddr); |
| wdata = get_csr_val_with_updated_field(ral.sbcs.sbreadondata, wdata, req.readondata); |
| end else begin |
| // If we set these bits on writes, it complicates things. |
| wdata = get_csr_val_with_updated_field(ral.sbcs.sbreadonaddr, wdata, 0); |
| wdata = get_csr_val_with_updated_field(ral.sbcs.sbreadondata, wdata, 0); |
| end |
| wdata = get_csr_val_with_updated_field(ral.sbcs.sbautoincrement, wdata, |
| (req.autoincrement > 0)); |
| if (wdata != ral.sbcs.sbaccess.get_mirrored_value()) begin |
| csr_wr(.ptr(ral.sbcs), .value(wdata), .blocking(1), .predict(predict_dmi_writes)); |
| end |
| |
| // Update the sbaddress0. |
| csr_wr(.ptr(ral.sbaddress0), .value(req.addr), .blocking(1), .predict(predict_dmi_writes)); |
| |
| // These steps are run in several branches in the following code. Macroize them. |
| `define BUSYWAIT_AND_EXIT_ON_ERR \ |
| sba_access_busy_wait_and_clear_error(.req(req), \ |
| .sba_access_err_clear(sba_access_err_clear)); \ |
| \ |
| // We don't know what to do if any of the following is true - return to the caller: \ |
| // \ |
| // - The request timed out or returned is_busy_err \ |
| // - The access reported non-0 sberror and sba_access_err_clear is set to 0 \ |
| // - Request was malformed (An SBA access is not made in the case) \ |
| // - The access returned SbaErrOther error response (The DUT may need special handling) \ |
| if (req.timed_out || \ |
| req.is_busy_err || \ |
| (req.is_err != SbaErrNone && !sba_access_err_clear) || \ |
| req.is_err inside {SbaErrBadAlignment, SbaErrBadSize, SbaErrOther}) begin \ |
| return; \ |
| end \ |
| |
| // Write / read sbdata, wait for transaction to complete. |
| if (req.bus_op == BusOpRead) begin |
| case ({req.readondata, req.readonaddr}) |
| 2'b00: begin |
| // No SBA read will be triggered. Read sbdata0 and and check status anyway (25% of the |
| // time). The external scoreboard is expected to verify that no SBA transaction is seen. |
| if (!cfg.in_reset && !$urandom_range(0, 3)) begin // 25% |
| csr_rd(.ptr(ral.sbdata0), .value(rdata), .blocking(1)); |
| `BUSYWAIT_AND_EXIT_ON_ERR |
| end |
| end |
| 2'b01: begin |
| // SBA read already triggered on write to sbaddress0. Read sbdata0 to fetch the response |
| // data. If autoincrement is set, then return to the caller - it is not a valid usecase. |
| `BUSYWAIT_AND_EXIT_ON_ERR |
| if (!cfg.in_reset) begin |
| csr_rd(.ptr(ral.sbdata0), .value(rdata), .blocking(1)); |
| req.rdata[0] = rdata; |
| end |
| end |
| 2'b10, 2'b11: begin |
| if (!req.readonaddr) begin |
| // The first read to sbdata returns stale data. Discard it. |
| if (!cfg.in_reset) begin |
| csr_rd(.ptr(ral.sbdata0), .value(rdata), .blocking(1)); |
| end |
| end |
| |
| // Read sbdata req.autoincrement+1 number of times. |
| // |
| // Randomly also inject reads to sbaddress0 to verify address is correctly incremented |
| // (checked by sba_access_monitor or the external scoreboard). |
| for (int i = 0; i < req.autoincrement + 1; i++) begin |
| // The previous step triggered an SBA read. Wait for it to complete. |
| `BUSYWAIT_AND_EXIT_ON_ERR |
| if (!cfg.in_reset && i > 0 && !$urandom_range(0, 3)) begin //25% |
| csr_rd(.ptr(ral.sbaddress0), .value(rdata)); |
| end |
| // Before the last read, set readondata to 0 to prevent new SBA read triggers. |
| if (!cfg.in_reset && i == req.autoincrement) begin |
| wdata = get_csr_val_with_updated_field(ral.sbcs.sbreadondata, wdata, 0); |
| csr_wr(.ptr(ral.sbcs), .value(wdata), .blocking(1), .predict(predict_dmi_writes)); |
| end |
| if (!cfg.in_reset) begin |
| csr_rd(.ptr(ral.sbdata0), .value(rdata)); |
| req.rdata[i] = rdata; |
| end |
| end |
| end |
| default: begin |
| `uvm_fatal(`gfn, "Unreachable!") |
| end |
| endcase |
| end else begin |
| // Write sbdata req.autoincrement+1 number of times. |
| // |
| // Randomly also inject reads to sbaddress0 to verify address is correctly incremented |
| // (done externally by the scoreboard). |
| for (int i = 0; i < req.autoincrement + 1; i++) begin |
| if (!cfg.in_reset) begin |
| csr_wr(.ptr(ral.sbdata0), .value(req.wdata[i]), .blocking(1), |
| .predict(predict_dmi_writes)); |
| end |
| `BUSYWAIT_AND_EXIT_ON_ERR |
| if (i > 0 && !cfg.in_reset && !$urandom_range(0, 3)) begin //25% |
| csr_rd(.ptr(ral.sbaddress0), .value(rdata), .blocking(1)); |
| end |
| end |
| end |
| |
| `undef BUSYWAIT_AND_EXIT_ON_ERR |
| endtask |
| |
| // Read the SBA access status. |
| virtual task sba_access_status(input sba_access_item req, output logic is_busy); |
| uvm_reg_data_t data; |
| csr_rd(.ptr(ral.sbcs), .value(data), .blocking(1)); |
| is_busy = get_field_val(ral.sbcs.sbbusy, data); |
| req.is_busy_err = get_field_val(ral.sbcs.sbbusyerror, data); |
| req.is_err = sba_access_err_e'(get_field_val(ral.sbcs.sberror, data)); |
| if (!is_busy) begin |
| `uvm_info(`gfn, $sformatf("SBA req completed: %0s", req.sprint(uvm_default_line_printer)), |
| UVM_HIGH) |
| end |
| endtask |
| |
| // Reads sbcs register to poll and wait for access to complete. |
| virtual task sba_access_busy_wait(input sba_access_item req); |
| logic is_busy; |
| `DV_SPINWAIT_EXIT( |
| do begin |
| sba_access_status(req, is_busy); |
| if (req.is_err != SbaErrNone) `DV_CHECK_EQ(is_busy, 0) |
| end while (is_busy && !req.is_busy_err);, |
| begin |
| fork |
| // TODO: Provide callbacks to support waiting for custom exit events. |
| wait(cfg.in_reset); |
| begin |
| // TODO: Make this timeout controllable. |
| #(cfg.vif.tck_period_ps * 100000 * 1ps); |
| req.timed_out = 1'b1; |
| `uvm_info(`gfn, $sformatf("SBA req timed out: %0s", |
| req.sprint(uvm_default_line_printer)), UVM_LOW) |
| end |
| join_any |
| end |
| ) |
| endtask |
| |
| // Clear SBA access busy error and access error sticky bits if they are set. |
| // |
| // Note that the req argument will continue to reflect the error bits. |
| virtual task sba_access_error_clear(sba_access_item req, bit clear_sbbusyerror = 1'b1, |
| bit clear_sberror = 1'b1); |
| uvm_reg_data_t data = ral.sbcs.get_mirrored_value(); |
| if (clear_sbbusyerror && req.is_busy_err) begin |
| data = get_csr_val_with_updated_field(ral.sbcs.sbbusyerror, data, 1); |
| end |
| if (clear_sberror && req.is_err != SbaErrNone) begin |
| data = get_csr_val_with_updated_field(ral.sbcs.sberror, data, 1); |
| end |
| csr_wr(.ptr(ral.sbcs), .value(data), .blocking(1), .predict(predict_dmi_writes)); |
| endtask |
| |
| // Wrapper task for busy wait followed by clearing of sberror if an error was reported. |
| virtual task sba_access_busy_wait_and_clear_error(sba_access_item req, bit sba_access_err_clear); |
| sba_access_busy_wait(req); |
| // Only clear sberror - let the caller handle sbbusyerror. |
| if (!cfg.in_reset && sba_access_err_clear && req.is_err != SbaErrNone) begin |
| sba_access_error_clear(.req(req), .clear_sbbusyerror(0)); |
| end |
| endtask |
| |
| // Simplified version of sba_access(), for 32-bit reads. |
| virtual task sba_read(input logic [BUS_AW-1:0] addr, |
| output logic [BUS_DW-1:0] value_q[$], |
| output sba_access_err_e status, |
| input int size = 1); |
| sba_access_item sba_item = sba_access_item::type_id::create("sba_item"); |
| sba_item.bus_op = BusOpRead; |
| sba_item.addr = addr; |
| sba_item.size = SbaAccessSize32b; |
| sba_item.autoincrement = size > 1 ? size : 0; |
| sba_item.readonaddr = size == 1; |
| sba_item.readondata = size > 1; |
| sba_access(sba_item); |
| `DV_CHECK_EQ(sba_item.rdata.size(), size) |
| value_q = sba_item.rdata; |
| status = sba_item.is_err; |
| `DV_CHECK(!sba_item.is_busy_err) |
| `DV_CHECK(!sba_item.timed_out) |
| endtask |
| |
| // Simplified version of sba_access(), for 32-bit writes. |
| virtual task sba_write(input logic [BUS_AW-1:0] addr, |
| input logic [BUS_DW-1:0] value_q[$], |
| output sba_access_err_e status); |
| sba_access_item sba_item = sba_access_item::type_id::create("sba_item"); |
| sba_item.bus_op = BusOpWrite; |
| sba_item.addr = addr; |
| sba_item.wdata = value_q; |
| sba_item.size = SbaAccessSize32b; |
| sba_item.autoincrement = value_q.size() > 1 ? value_q.size() : 0; |
| sba_access(sba_item); |
| status = sba_item.is_err; |
| `DV_CHECK(!sba_item.is_busy_err) |
| `DV_CHECK(!sba_item.timed_out) |
| endtask |
| |
| // Wrapper task for reading the system memory either via SBA or via program buffer. |
| // |
| // addr: The starting system address. |
| // value_q: The read data returned to the caller. |
| // status: The status of the op returned to the caller. |
| // size: The size of the read access in terms of full bus words. |
| // route: The route taken to access the system memory. |
| virtual task mem_read(input logic [BUS_AW-1:0] addr, |
| output logic [BUS_DW-1:0] value_q[$], |
| output byte status, |
| input mem_access_route_e route = randomize_mem_access_route(), |
| input int size = 1); |
| case (route) |
| MemAccessViaAbstractCmd, MemAccessViaProgbuf: begin |
| abstract_cmd_err_e abs_status; |
| abstract_cmd_mem_read(.addr(addr), .value_q(value_q), .status(abs_status), .size(size), |
| .route(route)); |
| status = byte'(abs_status); |
| end |
| MemAccessViaSba: begin |
| sba_access_err_e sba_status; |
| sba_read(.addr(addr), .value_q(value_q), .status(sba_status), .size(size)); |
| status = byte'(sba_status); |
| end |
| default: `uvm_fatal(`gfn, $sformatf("Invalid route: 0x%0h", route)) |
| endcase |
| endtask |
| |
| // Wrapper task for writing to the system memory either via SBA or via program buffer. |
| // |
| // addr: The starting address of the system memory. |
| // value_q: The read data returned to the caller. |
| // status: The status of the op returned to the caller. |
| // size: The size of the read access in terms of full bus words. |
| // route: The route taken to access the system memory. |
| virtual task mem_write(input logic [BUS_AW-1:0] addr, |
| input logic [BUS_DW-1:0] value_q[$], |
| output byte status, |
| input mem_access_route_e route = randomize_mem_access_route()); |
| case (route) |
| MemAccessViaAbstractCmd, MemAccessViaProgbuf: begin |
| abstract_cmd_err_e abs_status; |
| abstract_cmd_mem_write(.addr(addr), .value_q(value_q), .status(abs_status), .route(route)); |
| status = byte'(abs_status); |
| end |
| MemAccessViaSba: begin |
| sba_access_err_e sba_status; |
| sba_write(.addr(addr), .value_q(value_q), .status(sba_status)); |
| status = byte'(sba_status); |
| end |
| default: `uvm_fatal(`gfn, $sformatf("Invalid route: 0x%0h", route)) |
| endcase |
| endtask |
| |
| // Load a memory location in the device with a custom image. |
| // |
| // Reads a VMEM file and writes the contents to the memory. |
| // vmem_file: Path to vmem file. Must be BUS_DW word sized. |
| // addr: The starting address of the system memory. |
| // route: The route taken to access the system memory. |
| virtual task load_image(string vmem_file, |
| logic [BUS_AW-1:0] addr, |
| input mem_access_route_e route = randomize_mem_access_route()); |
| logic [BUS_DW-1:0] vmem_data[$]; |
| byte status; |
| dv_utils_pkg::read_vmem(vmem_file, vmem_data); |
| `DV_CHECK_FATAL(vmem_data.size(), "vmem_data is empty!") |
| mem_write(.addr(addr), .value_q(vmem_data), .status(status), .route(route)); |
| `DV_CHECK_EQ(status, 0) |
| endtask |
| |
| virtual function mem_access_route_e randomize_mem_access_route(); |
| mem_access_route_e route; |
| `DV_CHECK_STD_RANDOMIZE_WITH_FATAL(route, |
| route inside {MemAccessViaProgbuf, MemAccessViaSba};) |
| return route; |
| endfunction |
| |
| endclass |