[dv/otp_ctrl] Support ecc correctable error in otp check

This PR supports ECC correctable error in otp_checks.
This error not trigger any alert, but just trigger an interrupt and
update error code accordingly.

Signed-off-by: Cindy Chen <chencindy@google.com>
diff --git a/hw/ip/otp_ctrl/dv/env/otp_ctrl_env_cfg.sv b/hw/ip/otp_ctrl/dv/env/otp_ctrl_env_cfg.sv
index e96a944..353966a 100644
--- a/hw/ip/otp_ctrl/dv/env/otp_ctrl_env_cfg.sv
+++ b/hw/ip/otp_ctrl/dv/env/otp_ctrl_env_cfg.sv
@@ -24,6 +24,9 @@
   // This value is updated in sequence when backdoor inject ECC error
   otp_ecc_err_e ecc_err = OtpNoEccErr;
 
+  // Check ECC errors
+  otp_ecc_err_e ecc_chk_err [NumPart] = '{default:OtpNoEccErr};
+
   `uvm_object_utils_begin(otp_ctrl_env_cfg)
   `uvm_object_utils_end
 
diff --git a/hw/ip/otp_ctrl/dv/env/otp_ctrl_scoreboard.sv b/hw/ip/otp_ctrl/dv/env/otp_ctrl_scoreboard.sv
index 151fb3c..770be56 100644
--- a/hw/ip/otp_ctrl/dv/env/otp_ctrl_scoreboard.sv
+++ b/hw/ip/otp_ctrl/dv/env/otp_ctrl_scoreboard.sv
@@ -638,6 +638,12 @@
           if (`gmv(ral.check_timeout) > 0 && `gmv(ral.check_timeout) <= CHK_TIMEOUT_CYC) begin
             set_exp_alert("fatal_check_error", 1, `gmv(ral.check_timeout));
             predict_err(OtpTimeoutErrIdx);
+          end else begin
+            if (get_field_val(ral.check_trigger.consistency, item.a_data)) begin
+              foreach(cfg.ecc_chk_err[i]) begin
+                if (cfg.ecc_chk_err[i] == OtpEccCorrErr) predict_err(i, OtpMacroEccCorrError);
+              end
+            end
           end
         end
       end
diff --git a/hw/ip/otp_ctrl/dv/env/seq_lib/otp_ctrl_base_vseq.sv b/hw/ip/otp_ctrl/dv/env/seq_lib/otp_ctrl_base_vseq.sv
index cafb8a5..d46cdc3 100644
--- a/hw/ip/otp_ctrl/dv/env/seq_lib/otp_ctrl_base_vseq.sv
+++ b/hw/ip/otp_ctrl/dv/env/seq_lib/otp_ctrl_base_vseq.sv
@@ -109,7 +109,7 @@
     bit backdoor_wr;
     addr = randomize_dai_addr(addr);
     if (cfg.ecc_err != OtpEccUncorrErr && addr < LifeCycleOffset) begin
-      backdoor_rd_val = backdoor_inject_ecc_err(addr, ecc_err_mask);
+      backdoor_rd_val = backdoor_inject_ecc_err(addr, ecc_err_mask, cfg.ecc_err);
       backdoor_wr = 1;
     end
 
@@ -212,21 +212,22 @@
   // This function will output original backdoor read data for the given address.
   // TODO: move it to mem_bkdr_if once #4794 landed
   virtual function bit [TL_DW-1:0] backdoor_inject_ecc_err(bit [TL_DW-1:0] addr,
-                                                           bit [TL_DW-1:0] err_mask);
+                                                           bit [TL_DW-1:0] err_mask,
+                                                           ref otp_ecc_err_e ecc_err_type);
     bit [TL_DW-1:0] val, backdoor_val;
-    addr = {addr[TL_DW-3:2], 2'b00};
+    addr = {addr[TL_DW-1:2], 2'b00};
     val = cfg.mem_bkdr_vif.read32(addr);
     if (err_mask == 0 || addr >= LifeCycleOffset) begin
-      cfg.ecc_err = OtpNoEccErr;
+      ecc_err_type = OtpNoEccErr;
       return val;
     end
 
     // If every byte at most has one ECC error bit, it is a correctable error
     // If any byte at more than one ECC error bit, it is a uncorrectable error
-    cfg.ecc_err = OtpEccCorrErr;
+    ecc_err_type = OtpEccCorrErr;
     for (int i = 0; i < 2; i++) begin
       if (!$onehot(err_mask[i*16+:16]) && err_mask[i*16+:16]) begin
-        cfg.ecc_err = OtpEccUncorrErr;
+        ecc_err_type = OtpEccUncorrErr;
         break;
       end
     end
@@ -235,14 +236,35 @@
     foreach (err_mask[i]) backdoor_val[i] = err_mask[i] ? ~val[i] : val[i];
     cfg.mem_bkdr_vif.write32(addr, backdoor_val);
     `uvm_info(`gfn, $sformatf("original val %0h, backdoor val %0h, err_mask %0h err_type %0s",
-                              val, backdoor_val, err_mask, cfg.ecc_err.name), UVM_HIGH)
+                              val, backdoor_val, err_mask, ecc_err_type.name), UVM_HIGH)
 
     return val;
   endfunction
 
-  virtual task trigger_checks(bit [1:0] val, bit wait_done = 1);
+  virtual task trigger_checks(bit [1:0] val, bit wait_done = 1, bit [TL_DW-1:0] ecc_err_mask = 0);
+    bit [TL_DW-1:0] backdoor_rd_val, addr;
+    // Backdoor write ECC errors
+    if (ecc_err_mask) begin
+      int part_idx = $urandom_range(HwCfgIdx, LifeCycleIdx);
+
+      // Only HW cfgs check digest correctness
+      if (part_idx != LifeCycleIdx) begin
+        addr = $urandom_range(0, 1) ? PART_OTP_DIGEST_ADDRS[part_idx] << 2 :
+                                      (PART_OTP_DIGEST_ADDRS[part_idx] + 1) << 2;
+      end else begin
+        addr = $urandom_range(LifeCycleOffset, LifeCycleOffset + LifeCycleSize - 1);
+        addr = {addr[TL_DW-1:2], 2'b00};
+      end
+      backdoor_rd_val = backdoor_inject_ecc_err(addr, ecc_err_mask, cfg.ecc_chk_err[part_idx]);
+    end
+
     csr_wr(ral.check_trigger, val);
     if (wait_done && val) csr_spinwait(ral.status.check_pending, 0);
+
+    if (ecc_err_mask) begin
+      cfg.mem_bkdr_vif.write32(addr, backdoor_rd_val);
+      cfg.ecc_chk_err = '{default: OtpNoEccErr};
+    end
   endtask
 
   // For a DAI interface operation to finish, either way until status dai_idle is set, or check
diff --git a/hw/ip/otp_ctrl/dv/env/seq_lib/otp_ctrl_check_fail_vseq.sv b/hw/ip/otp_ctrl/dv/env/seq_lib/otp_ctrl_check_fail_vseq.sv
index cd9f21f..cac8774 100644
--- a/hw/ip/otp_ctrl/dv/env/seq_lib/otp_ctrl_check_fail_vseq.sv
+++ b/hw/ip/otp_ctrl/dv/env/seq_lib/otp_ctrl_check_fail_vseq.sv
@@ -4,15 +4,25 @@
 
 // This sequence creates the following check failure scenarios:
 // 1. Check timeout
-// 2. TODO: add when support
+// 2. Correctable ECC check error
+// 3. TODO: add when support
 class otp_ctrl_check_fail_vseq extends otp_ctrl_dai_errs_vseq;
   `uvm_object_utils(otp_ctrl_check_fail_vseq)
 
   `uvm_object_new
 
+  constraint ecc_chk_err_c {
+    // TODO: currently only max to 1 error bits, once implemetned ECC in mem_bkdr_if, we can
+    // fully randomize num of error bits
+    $countones(ecc_chk_err_mask) dist {0 :/ 1,
+                                       1 :/ 1};
+  }
+
   // 50% chance of having a check timeout
   constraint check_timeout_val_c {
-    check_timeout_val dist {[1 : CHK_TIMEOUT_CYC] :/ 1,
-                            [100_000 :'1]         :/ 1};
+    $countones(ecc_chk_err_mask) == 0 -> check_timeout_val dist {[1 : CHK_TIMEOUT_CYC] :/ 1,
+                                                                 [100_000 :'1]         :/ 1};
+    $countones(ecc_chk_err_mask) == 1 -> check_timeout_val inside {[100_000 :'1]};
   }
+
 endclass
diff --git a/hw/ip/otp_ctrl/dv/env/seq_lib/otp_ctrl_smoke_vseq.sv b/hw/ip/otp_ctrl/dv/env/seq_lib/otp_ctrl_smoke_vseq.sv
index f0d9668..8b8b142 100644
--- a/hw/ip/otp_ctrl/dv/env/seq_lib/otp_ctrl_smoke_vseq.sv
+++ b/hw/ip/otp_ctrl/dv/env/seq_lib/otp_ctrl_smoke_vseq.sv
@@ -22,7 +22,7 @@
   rand bit                           check_regwen_val, check_trigger_regwen_val;
   rand bit [TL_DW-1:0]               check_timeout_val;
   rand bit [1:0]                     check_trigger_val;
-  rand bit [TL_DW-1:0]               ecc_err_mask;
+  rand bit [TL_DW-1:0]               ecc_err_mask, ecc_chk_err_mask;
 
   constraint no_access_err_c {access_locked_parts == 0;}
 
@@ -61,6 +61,8 @@
 
   constraint ecc_err_c {ecc_err_mask == 0;}
 
+  constraint ecc_chk_err_c {ecc_chk_err_mask == 0;}
+
   virtual task dut_init(string reset_kind = "HARD");
     if (do_reset_in_seq && do_apply_reset) begin
       super.dut_init(reset_kind);
@@ -109,7 +111,7 @@
       if (check_trigger_val && `gmv(ral.check_trigger_regwen)) begin
         csr_wr(ral.check_timeout, check_timeout_val);
       end
-      trigger_checks(.val(check_trigger_val), .wait_done(1));
+      trigger_checks(.val(check_trigger_val), .wait_done(1), .ecc_err_mask(ecc_chk_err_mask));
 
       if (do_req_keys) begin
         req_otbn_key();