| // Copyright lowRISC contributors. |
| // Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| // SPDX-License-Identifier: Apache-2.0 |
| |
| class i2c_driver extends dv_base_driver #(i2c_item, i2c_agent_cfg); |
| `uvm_component_utils(i2c_driver) |
| |
| `uvm_component_new |
| |
| rand bit [7:0] rd_data[256]; // max length of read transaction |
| byte wr_data; |
| int scl_spinwait_timeout_ns = 1_000_000; // 1ms |
| bit scl_pause = 0; |
| |
| // get an array with unique read data |
| constraint rd_data_c { unique { rd_data }; } |
| |
| virtual task reset_signals(); |
| forever begin |
| @(negedge cfg.vif.rst_ni); |
| `uvm_info(`gfn, "\ndriver in reset progress", UVM_DEBUG) |
| release_bus(); |
| @(posedge cfg.vif.rst_ni); |
| `uvm_info(`gfn, "\ndriver out of reset", UVM_DEBUG) |
| end |
| endtask : reset_signals |
| |
| virtual task run_phase(uvm_phase phase); |
| fork |
| reset_signals(); |
| get_and_drive(); |
| begin |
| if (cfg.if_mode == Host) drive_scl(); |
| end |
| begin |
| if (cfg.if_mode == Host) host_scl_pause_ctrl(); |
| end |
| proc_hot_glitch(); |
| join_none |
| endtask |
| |
| virtual task get_and_drive(); |
| i2c_item req; |
| @(posedge cfg.vif.rst_ni); |
| forever begin |
| if (cfg.if_mode == Device) release_bus(); |
| // driver drives bus per mode |
| seq_item_port.get_next_item(req); |
| fork |
| begin: iso_fork |
| fork |
| begin |
| if (cfg.if_mode == Device) drive_device_item(req); |
| else drive_host_item(req); |
| end |
| // handle on-the-fly reset |
| begin |
| process_reset(); |
| req.clear_all(); |
| end |
| begin |
| // Agent hot reset. It only resets I2C agent. |
| // The DUT funtions normally without reset. |
| // This event only happens in directed test case so cannot set the timeout. |
| // It will be killed by disable fork when 'drive_*_item' is finished. |
| wait(cfg.agent_rst); |
| `uvm_info(`gfn, "drvdbg agent reset", UVM_MEDIUM) |
| req.clear_all(); |
| end |
| join_any |
| disable fork; |
| end: iso_fork |
| join |
| seq_item_port.item_done(); |
| |
| // When agent reset happens, flush all sequence items from sequencer request queue, |
| // before it starts a new sequence. |
| if (cfg.agent_rst) begin |
| i2c_item dummy; |
| do begin |
| seq_item_port.try_next_item(dummy); |
| if (dummy != null) seq_item_port.item_done(); |
| end while (dummy != null); |
| |
| repeat(2) @(cfg.vif.cb); |
| cfg.agent_rst = 0; |
| end |
| end |
| endtask : get_and_drive |
| |
| virtual task drive_host_item(i2c_item req); |
| // During pause period, let drive_scl control scl |
| `DV_WAIT(scl_pause == 1'b0,, scl_spinwait_timeout_ns, "drive_host_item") |
| `uvm_info(`gfn, $sformatf("drv: %s", req.drv_type.name), UVM_MEDIUM) |
| if (cfg.allow_bad_addr & !cfg.valid_addr) begin |
| if (req.drv_type inside {HostAck, HostNAck} & cfg.is_read) return; |
| end |
| case (req.drv_type) |
| HostStart: begin |
| cfg.vif.drv_phase = DrvAddr; |
| cfg.vif.host_start(cfg.timing_cfg); |
| cfg.host_scl_start = 1; |
| end |
| HostRStart: begin |
| cfg.vif.host_rstart(cfg.timing_cfg); |
| end |
| HostData: begin |
| `uvm_info(`gfn, $sformatf("Driving host item 0x%x", req.wdata), UVM_MEDIUM) |
| for (int i = $bits(req.wdata) -1; i >= 0; i--) begin |
| cfg.vif.host_data(cfg.timing_cfg, req.wdata[i]); |
| end |
| // Wait one more cycle for ack |
| cfg.vif.wait_scl(.iter(1), .tc(cfg.timing_cfg)); |
| end |
| HostAck: begin |
| // Wait for read data and send ack |
| cfg.vif.wait_scl(.iter(8), .tc(cfg.timing_cfg)); |
| cfg.vif.host_data(cfg.timing_cfg, 0); |
| end |
| HostNAck: begin |
| // Wait for read data and send nack |
| cfg.vif.wait_scl(.iter(8), .tc(cfg.timing_cfg)); |
| cfg.vif.host_data(cfg.timing_cfg, 1); |
| end |
| HostStop: begin |
| cfg.vif.drv_phase = DrvStop; |
| cfg.host_scl_stop = 1; |
| cfg.vif.host_stop(cfg.timing_cfg); |
| if (cfg.allow_bad_addr & !cfg.valid_addr)cfg.got_stop = 1; |
| end |
| default: begin |
| `uvm_fatal(`gfn, $sformatf("\n host_driver, received invalid request")) |
| end |
| endcase |
| endtask : drive_host_item |
| |
| virtual task drive_device_item(i2c_item req); |
| bit [7:0] rd_data_cnt = 8'd0; |
| bit [7:0] rdata; |
| |
| case (req.drv_type) |
| DevAck: begin |
| cfg.timing_cfg.tStretchHostClock = gen_num_stretch_host_clks(cfg.timing_cfg); |
| `uvm_info(`gfn, $sformatf("sending an ack"), UVM_MEDIUM) |
| fork |
| // host clock stretching allows a high-speed host to communicate |
| // with a low-speed device by setting TIMEOUT_CTRL.EN bit |
| // the device asks host stretching its scl_i by pulling down scl_o |
| // the host clock pulse is extended until device scl_o is pulled up |
| // once scl_o is pulled down longer than TIMEOUT_CTRL.VAL field, |
| // intr_stretch_timeout_o is asserted (ref. https://www.i2c-bus.org/clock-stretching) |
| cfg.vif.device_stretch_host_clk(cfg.timing_cfg); |
| cfg.vif.device_send_ack(cfg.timing_cfg); |
| join |
| end |
| RdData: begin |
| `uvm_info(`gfn, $sformatf("Send readback data %0x", req.rdata), UVM_MEDIUM) |
| for (int i = 7; i >= 0; i--) begin |
| cfg.vif.device_send_bit(cfg.timing_cfg, req.rdata[i]); |
| end |
| `uvm_info(`gfn, $sformatf("\n device_driver, trans %0d, byte %0d %0x", |
| req.tran_id, req.num_data+1, rd_data[rd_data_cnt]), UVM_DEBUG) |
| // rd_data_cnt is rollled back (no overflow) after reading 256 bytes |
| rd_data_cnt++; |
| end |
| WrData: begin |
| // nothing to do |
| end |
| default: begin |
| `uvm_fatal(`gfn, $sformatf("\n device_driver, received invalid request")) |
| end |
| endcase |
| endtask : drive_device_item |
| |
| function int gen_num_stretch_host_clks(ref timing_cfg_t tc); |
| // By randomly pulling down scl_o "offset" within [0:2*tc.tTimeOut], |
| // intr_stretch_timeout_o interrupt would be generated uniformly |
| // To test this feature more regressive, there might need a dedicated vseq (V2) |
| // in which TIMEOUT_CTRL.EN is always set. |
| |
| // If Stretch value is greater than 2*tTimeOut, it will create 2 interrupt events. |
| // Which can cause faluse error in 'host_stretch_testmode'. |
| // So, this value should be associated with tTimeout in host stretch testmode |
| if (cfg.host_stretch_test_mode) return (tc.tTimeOut + 1); |
| else return $urandom_range(tc.tClockPulse, tc.tClockPulse + 2*tc.tTimeOut); |
| endfunction : gen_num_stretch_host_clks |
| |
| virtual task process_reset(); |
| @(negedge cfg.vif.rst_ni); |
| release_bus(); |
| `uvm_info(`gfn, "\n driver is reset", UVM_DEBUG) |
| endtask : process_reset |
| |
| virtual task release_bus(); |
| `uvm_info(`gfn, "Driver released the bus", UVM_HIGH) |
| cfg.vif.scl_o = 1'b1; |
| cfg.vif.sda_o = 1'b1; |
| endtask : release_bus |
| |
| task drive_scl(); |
| // This timeout is extremely long since read trasnactions will stretch |
| // whenever there are unhanded write commands or format bytes. |
| int scl_spinwait_timeout_ns = 100_000_000; // 100ms |
| forever begin |
| @(cfg.vif.cb); |
| wait(cfg.host_scl_start); |
| fork begin |
| fork |
| // Original scl driver thread |
| while(!cfg.host_scl_stop) begin |
| cfg.vif.scl_o <= 1'b0; |
| cfg.vif.wait_for_dly(cfg.timing_cfg.tClockLow); |
| cfg.vif.wait_for_dly(cfg.timing_cfg.tSetupBit); |
| cfg.vif.scl_o <= 1'b1; |
| `DV_WAIT(cfg.vif.scl_i === 1'b1,, scl_spinwait_timeout_ns, "i2c_drv_scl") |
| cfg.vif.wait_for_dly(cfg.timing_cfg.tClockPulse); |
| |
| // There is a corner case s.t. |
| // pause req -> drv got stop back to back. |
| // if that happens, skip to the next txn cycle to pause |
| // to avoid unsuccessful host timeout |
| if (cfg.host_scl_pause_ack & !cfg.host_scl_stop) begin |
| scl_pause = 1; |
| cfg.vif.wait_for_dly(cfg.host_scl_pause_cyc); |
| scl_pause = 0; |
| cfg.host_scl_pause_ack = 0; |
| end |
| if (!cfg.host_scl_stop) cfg.vif.scl_o = 1'b0; |
| cfg.vif.wait_for_dly(cfg.timing_cfg.tHoldBit); |
| end |
| // Force quit thread |
| begin |
| wait(cfg.host_scl_force_high | cfg.host_scl_force_low); |
| cfg.host_scl_stop = 1; |
| if (cfg.host_scl_force_high) begin |
| cfg.vif.scl_o <= 1'b1; |
| cfg.vif.sda_o <= 1'b1; |
| end else begin |
| cfg.vif.scl_o <= 1'b0; |
| cfg.vif.sda_o <= 1'b0; |
| end |
| end |
| join_any |
| disable fork; |
| end join |
| cfg.host_scl_start = 0; |
| cfg.host_scl_stop = 0; |
| end |
| endtask |
| |
| task host_scl_pause_ctrl(); |
| forever begin |
| @(cfg.vif.cb); |
| if (cfg.host_scl_pause_req & cfg.host_scl_start & !cfg.host_scl_stop) begin |
| cfg.host_scl_pause_ack = 1; |
| `DV_WAIT(cfg.host_scl_pause_ack == 0,, |
| scl_spinwait_timeout_ns, "host_scl_pause_ctrl") |
| cfg.host_scl_pause_req = 0; |
| end |
| end |
| endtask |
| |
| // When 'cfg.hot_glitch' is triggered, it wait for data read state |
| // then add 'start' or 'stop' during data read state. |
| // Agent reset (without dut reset) is asserted after this event to |
| // clear driver and monitor state. |
| task proc_hot_glitch(); |
| forever begin |
| @(cfg.vif.cb); |
| if (cfg.hot_glitch) begin |
| wait_for_read_data_state(); |
| randcase |
| 1: add_start(); |
| 1: add_stop(); |
| endcase |
| cfg.agent_rst = 1; |
| cfg.hot_glitch = 0; |
| cfg.host_scl_force_high = 0; |
| cfg.host_scl_force_low = 0; |
| wait(!cfg.agent_rst); |
| end |
| end |
| endtask |
| |
| // Task looking for data read state. |
| task wait_for_read_data_state(); |
| int wait_timeout_ns = 500_000_000; // 500 ms |
| `DV_WAIT(cfg.vif.drv_phase == DrvRd,, wait_timeout_ns, "wait_for_read_data_state"); |
| repeat(2) @(posedge cfg.vif.scl_i); |
| endtask |
| |
| // Force quit scl stuck at high. |
| // 'start' will be added by the next sequence. |
| task add_start(); |
| `uvm_info(`gfn, "proc_hot_glitch: add start", UVM_MEDIUM) |
| cfg.host_scl_force_high = 1; |
| cfg.vif.wait_for_dly(cfg.timing_cfg.tSetupStart); |
| endtask // add_start |
| |
| // Force quit scl stuck at low. |
| // 'stop' is manually added here. |
| task add_stop(); |
| `uvm_info(`gfn, "proc_hot_glitch: add stop", UVM_MEDIUM) |
| cfg.host_scl_force_low = 1; |
| cfg.host_scl_stop = 1; |
| cfg.vif.host_stop(cfg.timing_cfg); |
| endtask |
| endclass : i2c_driver |