[otp_ctrl] First cut implementation of the OTP controller

This adds an initial implementation of the OTP controller.

Still missing are:
- LFSR timer for triggering integrity/consistency checks
- Life cycle interface
- Scrambling key derivation

Signed-off-by: Michael Schaffner <msf@opentitan.org>
diff --git a/hw/ip/otp_ctrl/rtl/otp_ctrl.sv b/hw/ip/otp_ctrl/rtl/otp_ctrl.sv
index 9ec1cd1..889bb3a 100644
--- a/hw/ip/otp_ctrl/rtl/otp_ctrl.sv
+++ b/hw/ip/otp_ctrl/rtl/otp_ctrl.sv
@@ -20,8 +20,8 @@
   input  tlul_pkg::tl_h2d_t         tl_i,
   output tlul_pkg::tl_d2h_t         tl_o,
   // Interrupt Requests
-  output logic                      intr_otp_access_done_o,
-  output logic                      intr_otp_ctrl_err_o,
+  output logic                      intr_otp_operation_done_o,
+  output logic                      intr_otp_error_o,
   // Alerts
   input  prim_alert_pkg::alert_rx_t [NumAlerts-1:0] alert_rx_i,
   output prim_alert_pkg::alert_tx_t [NumAlerts-1:0] alert_tx_o,
@@ -33,6 +33,7 @@
   input  lc_otp_program_req_t       lc_otp_program_req_i,
   output lc_otp_program_rsp_t       lc_otp_program_rsp_o,
   // Lifecycle broadcast inputs
+  input  lc_tx_t                    lc_escalate_en_i,
   input  lc_tx_t                    lc_provision_en_i,
   input  lc_tx_t                    lc_test_en_i,
   // OTP broadcast outputs
@@ -42,227 +43,546 @@
   // TODO: other hardware broadcast outputs
 );
 
-  //////////////////////////////////
-  // Regfile Breakout and Mapping //
-  //////////////////////////////////
+  import prim_util_pkg::vbits;
 
-  tlul_pkg::tl_h2d_t tl_win_h2d[2];
-  tlul_pkg::tl_d2h_t tl_win_d2h[2];
+  ////////////////////////
+  // Integration Checks //
+  ////////////////////////
+
+  // This ensures that we can transfer scrambler data blocks in and out of OTP atomically.
+  `ASSERT_INIT(OtpIfWidth_A, OtpIfWidth == ScrmblBlockWidth)
+
+  /////////////
+  // Regfile //
+  /////////////
+
+  tlul_pkg::tl_h2d_t tl_win_h2d[3];
+  tlul_pkg::tl_d2h_t tl_win_d2h[3];
 
   otp_ctrl_reg_pkg::otp_ctrl_reg2hw_t reg2hw;
   otp_ctrl_reg_pkg::otp_ctrl_hw2reg_t hw2reg;
 
   otp_ctrl_reg_top u_reg (
-    .clk_i ,
+    .clk_i,
     .rst_ni,
     .tl_i,
     .tl_o,
-    .tl_win_o ( tl_win_h2d ),
-    .tl_win_i ( tl_win_d2h ),
-    .reg2hw   ( reg2hw   ),
-    .hw2reg   ( hw2reg   ),
-    .devmode_i( 1'b1     )
+    .tl_win_o  ( tl_win_h2d ),
+    .tl_win_i  ( tl_win_d2h ),
+    .reg2hw    ( reg2hw     ),
+    .hw2reg    ( hw2reg     ),
+    .devmode_i ( 1'b1       )
   );
 
-  ////////////////
-  // Interrupts //
-  ////////////////
+  ///////////////////////////////
+  // Digests and LC State CSRs //
+  ///////////////////////////////
 
-  logic otp_access_done;
-  logic otp_ctrl_err;
+  logic [ScrmblBlockWidth-1:0] unused_digest;
+  logic [NumPart-1:0][ScrmblBlockWidth-1:0] part_digest;
+  assign hw2reg.creator_sw_cfg_digest = part_digest[CreatorSwCfgIdx];
+  assign hw2reg.owner_sw_cfg_digest   = part_digest[OwnerSwCfgIdx];
+  assign hw2reg.hw_cfg_digest         = part_digest[HwCfgIdx];
+  assign hw2reg.secret0_digest        = part_digest[Secret0Idx];
+  assign hw2reg.secret1_digest        = part_digest[Secret1Idx];
+  assign hw2reg.secret2_digest        = part_digest[Secret2Idx];
+  // LC partition has no digest
+  assign unused_digest                = part_digest[LifeCycleIdx];
 
-  // dummy connections
-  assign otp_access_done = reg2hw.direct_access_size.q[0];
-  assign otp_ctrl_err    = reg2hw.direct_access_size.q[1];
+  // TODO: connect these
+  assign hw2reg.lc_state          = '0;
+  assign hw2reg.lc_transition_cnt = '0;
+
+  //////////////////////////////
+  // Access Defaults and CSRs //
+  //////////////////////////////
+
+  part_access_t [NumPart-1:0] part_access_csrs;
+  always_comb begin : p_access_control
+    // Default (this will be overridden by partition-internal settings).
+    part_access_csrs = {{32'(2*NumPart)}{Unlocked}};
+    // Propagate CSR read enables down to the SW_CFG partitions.
+    if (!reg2hw.creator_sw_cfg_read_lock) part_access_csrs[CreatorSwCfgIdx].read_lock = Locked;
+    if (!reg2hw.owner_sw_cfg_read_lock) part_access_csrs[OwnerSwCfgIdx].read_lock = Locked;
+    // The SECRET2 partition can only be accessed when provisioning is enabled.
+    if (lc_provision_en_i != On) part_access_csrs[Secret2Idx].read_lock = Locked;
+    // Permanently lock DAI write access to the life cycle partition
+    part_access_csrs[LifeCycleIdx].write_lock = Locked;
+  end
+
+  //////////////////////
+  // DAI-related CSRs //
+  //////////////////////
+
+  logic                         dai_idle;
+  logic                         dai_req;
+  dai_cmd_e                     dai_cmd;
+  logic [OtpByteAddrWidth-1:0]  dai_addr;
+  logic [NumDaiWords-1:0][31:0] dai_wdata, dai_rdata;
+  logic                         unused_wdata_qe;
+  logic                         unused_addr_qe;
+
+  // Any write to this register triggers a DAI command.
+  assign dai_req = reg2hw.direct_access_cmd.digest.qe |
+                   reg2hw.direct_access_cmd.write.qe  |
+                   reg2hw.direct_access_cmd.read.qe;
+
+  assign dai_cmd = {reg2hw.direct_access_cmd.digest.q,
+                    reg2hw.direct_access_cmd.write.q,
+                    reg2hw.direct_access_cmd.read.q};
+
+  assign dai_addr  = reg2hw.direct_access_address.q;
+  assign dai_wdata = reg2hw.direct_access_wdata;
+  assign hw2reg.direct_access_rdata = dai_rdata;
+  // This write-protects all DAI regs during pending operations.
+  assign hw2reg.direct_access_regwen.d = dai_idle;
+
+  // The DAI and the LCI can initiate write transactions, which
+  // are critical and we must not power down if such transactions
+  // are pending. Hence, we signal the LCI/DAI idle state to the
+  // power manager
+  logic lci_idle;
+  assign otp_pwr_state_o.idle = lci_idle & dai_idle;
+
+  //////////////////////////////////////
+  // Ctrl/Status CSRs, Errors, Alerts //
+  //////////////////////////////////////
+
+  // Status and error reporting CSRs, error interrupt generation and alerts.
+  otp_err_e [NumPart+1:0] part_error;
+  logic [NumPart+1:0] part_errors_reduced;
+  logic otp_operation_done, otp_error;
+  logic otp_fatal_error, otp_check_failed;
+  always_comb begin : p_errors_alerts
+    hw2reg.err_code = part_error;
+    otp_fatal_error = 1'b0;
+    otp_check_failed = 1'b0;
+    // Aggregate all the errors from the partitions and the DAI/LCI
+    for (int k = 0; k < NumPart+2; k++) begin
+      // Set the error bit if the error status of the corresponding partition is nonzero.
+      // Note that due to the enumeration of the fields in the otp_ctrl_hw2reg_status_reg_t
+      // we have to reverse the bitpositions here such that the mapping is correct.
+      part_errors_reduced[NumPart+1-k] = |part_error[k];
+      // Filter for critical error codes that should not occur in the field.
+      otp_fatal_error |= part_error[k] inside {OtpCmdInvErr,
+                                               OtpInitErr,
+                                               OtpReadUncorrErr,
+                                               OtpReadErr,
+                                               OtpWriteErr};
+
+      // Filter for integrity and consistency check failures.
+      otp_check_failed |= part_error[k] inside {ParityErr,
+                                                IntegErr,
+                                                CnstyErr,
+                                                FsmErr};
+    end
+  end
+
+  // Assign these to the status register.
+  assign hw2reg.status = {part_errors_reduced, dai_idle};
+  // If we got an error, we trigger an interrupt.
+  assign otp_error = |part_errors_reduced;
+
+  //////////////////////////////////
+  // Interrupts and Alert Senders //
+  //////////////////////////////////
 
   prim_intr_hw #(
     .Width(1)
-  ) i_intr_esc0 (
+  ) u_intr_esc0 (
     .clk_i,
     .rst_ni,
-    .event_intr_i           ( otp_access_done                       ),
-    .reg2hw_intr_enable_q_i ( reg2hw.intr_enable.otp_access_done.q  ),
-    .reg2hw_intr_test_q_i   ( reg2hw.intr_test.otp_access_done.q    ),
-    .reg2hw_intr_test_qe_i  ( reg2hw.intr_test.otp_access_done.qe   ),
-    .reg2hw_intr_state_q_i  ( reg2hw.intr_state.otp_access_done.q   ),
-    .hw2reg_intr_state_de_o ( hw2reg.intr_state.otp_access_done.de  ),
-    .hw2reg_intr_state_d_o  ( hw2reg.intr_state.otp_access_done.d   ),
-    .intr_o                 ( intr_otp_access_done_o                )
+    .event_intr_i           ( otp_operation_done                      ),
+    .reg2hw_intr_enable_q_i ( reg2hw.intr_enable.otp_operation_done.q ),
+    .reg2hw_intr_test_q_i   ( reg2hw.intr_test.otp_operation_done.q   ),
+    .reg2hw_intr_test_qe_i  ( reg2hw.intr_test.otp_operation_done.qe  ),
+    .reg2hw_intr_state_q_i  ( reg2hw.intr_state.otp_operation_done.q  ),
+    .hw2reg_intr_state_de_o ( hw2reg.intr_state.otp_operation_done.de ),
+    .hw2reg_intr_state_d_o  ( hw2reg.intr_state.otp_operation_done.d  ),
+    .intr_o                 ( intr_otp_operation_done_o               )
   );
 
   prim_intr_hw #(
     .Width(1)
-  ) i_intr_esc1 (
+  ) u_intr_esc1 (
     .clk_i,
     .rst_ni,
-    .event_intr_i           ( otp_ctrl_err                       ),
-    .reg2hw_intr_enable_q_i ( reg2hw.intr_enable.otp_ctrl_err.q  ),
-    .reg2hw_intr_test_q_i   ( reg2hw.intr_test.otp_ctrl_err.q    ),
-    .reg2hw_intr_test_qe_i  ( reg2hw.intr_test.otp_ctrl_err.qe   ),
-    .reg2hw_intr_state_q_i  ( reg2hw.intr_state.otp_ctrl_err.q   ),
-    .hw2reg_intr_state_de_o ( hw2reg.intr_state.otp_ctrl_err.de  ),
-    .hw2reg_intr_state_d_o  ( hw2reg.intr_state.otp_ctrl_err.d   ),
-    .intr_o                 ( intr_otp_ctrl_err_o                )
+    .event_intr_i           ( otp_error                      ),
+    .reg2hw_intr_enable_q_i ( reg2hw.intr_enable.otp_error.q ),
+    .reg2hw_intr_test_q_i   ( reg2hw.intr_test.otp_error.q   ),
+    .reg2hw_intr_test_qe_i  ( reg2hw.intr_test.otp_error.qe  ),
+    .reg2hw_intr_state_q_i  ( reg2hw.intr_state.otp_error.q  ),
+    .hw2reg_intr_state_de_o ( hw2reg.intr_state.otp_error.de ),
+    .hw2reg_intr_state_d_o  ( hw2reg.intr_state.otp_error.d  ),
+    .intr_o                 ( intr_otp_error_o               )
   );
 
-  ///////////////////
-  // Alert Senders //
-  ///////////////////
-
-  logic parity_mismatch;
-  logic digest_mismatch;
-
-  // dummy connections
-  assign parity_mismatch = reg2hw.direct_access_cmd.read.q &&
-                           reg2hw.direct_access_cmd.read.qe;
-  assign digest_mismatch = reg2hw.direct_access_cmd.write.q &&
-                           reg2hw.direct_access_cmd.write.qe;
-
   prim_alert_sender #(
     .AsyncOn(AlertAsyncOn[0])
-  ) i_prim_alert_sender0 (
+  ) u_prim_alert_sender0 (
     .clk_i,
     .rst_ni,
-    .alert_i    ( parity_mismatch ),
+    .alert_i    ( otp_fatal_error ),
     .alert_rx_i ( alert_rx_i[0] ),
     .alert_tx_o ( alert_tx_o[0] )
   );
 
   prim_alert_sender #(
     .AsyncOn(AlertAsyncOn[1])
-  ) i_prim_alert_sender1 (
+  ) u_prim_alert_sender1 (
     .clk_i,
     .rst_ni,
-    .alert_i    ( digest_mismatch ),
+    .alert_i    ( otp_check_failed ),
     .alert_rx_i ( alert_rx_i[1] ),
     .alert_tx_o ( alert_tx_o[1] )
   );
 
-  ///////////////
-  // OTP Macro //
-  ///////////////
+  ////////////////
+  // LFSR Timer //
+  ////////////////
 
-  localparam int OtpWidth     = 8;
-  localparam int OtpDepth     = 1024;
-  localparam int OtpAddrWidth = $clog2(OtpDepth);
-  localparam int OtpErrWidth  = 8;
+  // TBD: should we incorporate a timeout in the LFSR counter to cover the case where a
+  // partition check never completes due to wedged arbitration (or some other condition
+  // induced due to a tampering attempt)? This will likely be constructed in a similar
+  // way as the ping timer inside the alert handler.
 
-  logic otp_init_req, otp_init_done;
-  logic otp_err_valid;
-  logic [OtpErrWidth-1:0] otp_err_code;
-  logic otp_valid, otp_ready;
-  logic [OtpAddrWidth-1:0] otp_addr;
-  logic [OtpWidth-1:0] otp_wdata, otp_rdata;
-  logic otp_wren, otp_rvalid;
+  logic [NumPart-1:0] integ_chk_req, integ_chk_ack;
+  logic [NumPart-1:0] cnsty_chk_req, cnsty_chk_ack;
 
-  // a couple of dummy connections to the OTP macro
-  assign otp_valid = reg2hw.direct_access_wdata[1].qe | reg2hw.direct_access_cmd.write.qe;
-  assign otp_wren  = reg2hw.direct_access_wdata[1].qe;
-  assign otp_addr  = reg2hw.direct_access_address.q;
-  assign otp_wdata = reg2hw.direct_access_wdata[1].q[OtpWidth-1:0];
+  ///////////////////////////////
+  // OTP Macro and Arbitration //
+  ///////////////////////////////
 
-  assign otp_init_req = 1'b1;
+  typedef struct packed {
+    prim_otp_cmd_e               cmd;
+    logic [OtpSizeWidth-1:0]     size; // Number of native words to write.
+    logic [OtpIfWidth-1:0]       wdata;
+    logic [OtpAddrWidth-1:0]     addr; // Halfword address.
+  } otp_bundle_t;
+
+  logic [NumPart+1:0]          part_otp_arb_req, part_otp_arb_gnt;
+  otp_bundle_t                 part_otp_arb_bundle [NumPart+2];
+  logic                        otp_arb_valid, otp_arb_ready;
+  logic [vbits(NumPart+2)-1:0] otp_arb_idx;
+  otp_bundle_t                 otp_arb_bundle;
+
+  // The OTP interface is arbitrated on a per-cycle basis, meaning that back-to-back
+  // transactions can be completely independent.
+  prim_arbiter_tree #(
+    .N(NumPart+2),
+    .DW($bits(otp_bundle_t))
+  ) u_otp_arb (
+    .clk_i,
+    .rst_ni,
+    .req_i   ( part_otp_arb_req    ),
+    .data_i  ( part_otp_arb_bundle ),
+    .gnt_o   ( part_otp_arb_gnt    ),
+    .idx_o   ( otp_arb_idx         ),
+    .valid_o ( otp_arb_valid       ),
+    .data_o  ( otp_arb_bundle      ),
+    .ready_i ( otp_arb_ready       )
+  );
+
+  otp_err_e              part_otp_err;
+  logic [OtpIfWidth-1:0] part_otp_rdata;
+  logic                  otp_rvalid;
+  tlul_pkg::tl_h2d_t     tl_win_h2d_gated;
+  tlul_pkg::tl_d2h_t     tl_win_d2h_gated;
+
+  // Life cycle qualification of TL-UL test interface.
+  assign tl_win_h2d_gated              = (lc_test_en_i == On) ? tl_win_h2d[$high(tl_win_h2d)] : '0;
+  assign tl_win_d2h[$high(tl_win_h2d)] = (lc_test_en_i == On) ? tl_win_d2h_gated : '0;
 
   prim_otp #(
     .Width(OtpWidth),
     .Depth(OtpDepth),
+    .CmdWidth(OtpCmdWidth),
     .ErrWidth(OtpErrWidth)
-  ) i_prim_otp (
+  ) u_otp (
     .clk_i,
     .rst_ni,
-    // Test inerface
-    .test_tl_i   ( tl_win_h2d[1] ),
-    .test_tl_o   ( tl_win_d2h[1] ),
-    // Init and error signals
-    .init_req_i  ( otp_init_req  ),
-    .init_done_o ( otp_init_done ),
-    .err_valid_o ( otp_err_valid ),
-    .err_code_o  ( otp_err_code  ),
+    // Test interface
+    .test_tl_i   ( tl_win_h2d_gated     ),
+    .test_tl_o   ( tl_win_d2h_gated     ),
     // Read / Write command interface
-    .ready_o     ( otp_ready     ),
-    .valid_i     ( otp_valid     ),
-    .addr_i      ( otp_addr      ),
-    .wdata_i     ( otp_wdata     ),
-    .wren_i      ( otp_wren      ),
+    .ready_o     ( otp_arb_ready        ),
+    .valid_i     ( otp_arb_valid        ),
+    .cmd_i       ( otp_arb_bundle.cmd   ),
+    .size_i      ( otp_arb_bundle.size  ),
+    .addr_i      ( otp_arb_bundle.addr  ),
+    .wdata_i     ( otp_arb_bundle.wdata ),
     // Read data out
-    .rdata_o     ( otp_rdata     ),
-    .rvalid_o    ( otp_rvalid    )
+    .valid_o     ( otp_rvalid           ),
+    .rdata_o     ( part_otp_rdata       ),
+    .err_o       ( part_otp_err         )
   );
 
-  ////////////////////
-  // OTP Ctrl Logic //
-  ////////////////////
+  logic otp_fifo_valid;
+  logic [NumPartWidth-1:0] otp_part_idx;
+  logic [NumPart+1:0] part_otp_rvalid;
 
-  logic [31:0] gate_gen_out;
-  logic gate_gen_out_valid;
-
-  // gate generator with dummy connection to regfile
-  prim_gate_gen #(
-    .NumGates(30000)
-  ) i_prim_gate_gen (
+  // We can have up to two OTP commands in flight, hence we size this to be 2 deep.
+  // The partitions can unconditionally sink requested data.
+  prim_fifo_sync #(
+    .Width(NumPartWidth),
+    .Depth(2)
+  ) u_otp_rsp_fifo (
     .clk_i,
     .rst_ni,
-    .data_i  ( reg2hw.direct_access_wdata[0].q  ),
-    .valid_i ( reg2hw.direct_access_wdata[0].qe ),
-    .data_o  ( gate_gen_out                     ),
-    .valid_o ( gate_gen_out_valid               )
+    .clr_i    ( 1'b0           ),
+    .wvalid_i ( otp_arb_valid & otp_arb_ready ),
+    .wready_o (                ),
+    .wdata_i  ( otp_arb_idx    ),
+    .rvalid_o ( otp_fifo_valid ),
+    .rready_i ( otp_rvalid     ),
+    .rdata_o  ( otp_part_idx   ),
+    .depth_o  (                )
   );
 
-  /////////////////////
-  // PRESENT ENC/DEC //
-  /////////////////////
-
-  logic [63:0] otp_scrambler_out;
-  logic otp_scrambler_out_valid;
-
-  otp_ctrl_scrmbl i_otp_ctrl_scrmbl (
-    .clk_i,
-    .rst_ni,
-    .data_i  ( {reg2hw.direct_access_wdata[1].q,
-                reg2hw.direct_access_wdata[0].q}    ),
-    .cmd_i   ( otp_scrmbl_cmd_e'(reg2hw.direct_access_wdata[1].q[2:0]) ),
-    .valid_i ( reg2hw.direct_access_wdata[1].qe     ),
-    .ready_o (                                      ),
-    .data_o  ( otp_scrambler_out                    ),
-    .valid_o ( otp_scrambler_out_valid              )
-  );
-
-  ///////////////////////////////////
-  // Dummy Connections and Tie Off //
-  ///////////////////////////////////
-
-  always_comb begin : p_tie_off_or_connect
-    // tie off
-    hw2reg.status                  = '0;
-    hw2reg.err_code                = '0;
-    hw2reg.direct_access_rdata     = '0;
-    hw2reg.lc_state                = '0;
-    hw2reg.id_state                = '0;
-    hw2reg.test_xxx_cnt            = '0;
-    hw2reg.transition_cnt          = '0;
-    hw2reg.secret_integrity_digest = '0;
-    hw2reg.hw_cfg_lock             = '0;
-    hw2reg.hw_cfg                  = '0;
-    hw2reg.hw_cfg_integrity_digest = '0;
-    hw2reg.sw_cfg_integrity_digest = '0;
-
-    // TODO: initialization should only performed once after reset. subsequent
-    // init requests should just be immediately ack'ed without performing the complete
-    // OTP boot sequence.
-    pwr_otp_init_rsp_o   = '0;
-    otp_pwr_state_o      = '0;
-    lc_otp_program_rsp_o = '0;
-    otp_lc_data_o        = '0;
-    otp_keymgr_key_o     = '0;
-    otp_flash_key_o      = '0;
-
-    // dummy connections
-    {hw2reg.direct_access_rdata[0].d,
-     hw2reg.direct_access_rdata[1].d} = otp_scrambler_out ^ {gate_gen_out, gate_gen_out};
-    hw2reg.direct_access_rdata[0].de = gate_gen_out_valid ^ gate_gen_out_valid;
-    hw2reg.direct_access_rdata[1].de = gate_gen_out_valid ^ gate_gen_out_valid;
-    hw2reg.lc_state[0]               = otp_rdata ^ {8{otp_rvalid}};
+  // Steer response back to the partition where this request originated.
+  always_comb begin : p_rvalid
+    part_otp_rvalid = '0;
+    part_otp_rvalid[otp_part_idx] = otp_rvalid & otp_fifo_valid;
   end
 
+  // Note that this must be true by construction.
+  `ASSERT(OtpRespFifoUnderflow_A, otp_rvalid |-> otp_fifo_valid)
+
+  /////////////////////////////////////////
+  // Scrambling Datapath and Arbitration //
+  /////////////////////////////////////////
+
+  // Note: as opposed to the OTP arbitration above, we do not perform cycle-wise arbitration, but
+  // transaction-wise arbitration. This is implemented using a RR arbiter that acts as a mutex.
+  // I.e., each agent (e.g. the DAI or a partition) can request a lock on the mutex. Once granted,
+  // the partition can keep the the lock as long as needed for the transaction to complete. The
+  // partition must yield its lock by deasserting the request signal for the arbiter to proceed.
+  // Since this scheme does not have built-in preemtion, it must be ensured that the agents
+  // eventually release their locks for this to be fair.
+  typedef struct packed {
+    otp_scrmbl_cmd_e             cmd;
+    logic  [ConstSelWidth-1:0]   sel;
+    logic [ScrmblBlockWidth-1:0] data;
+    logic                        valid;
+  } scrmbl_bundle_t;
+
+  logic [NumPart:0]            part_scrmbl_mtx_req, part_scrmbl_mtx_gnt;
+  scrmbl_bundle_t              part_scrmbl_req_bundle [NumPart+1];
+  scrmbl_bundle_t              scrmbl_req_bundle;
+  logic [vbits(NumPart+1)-1:0] scrmbl_mtx_idx;
+  logic                        scrmbl_mtx_valid;
+
+  // Note that arbiter decisions do not change when backpressured.
+  // Hence, the idx_o signal is guaranteed to remain stable until ack'ed.
+  prim_arbiter_tree #(
+    .N(NumPart+1),
+    .DW($bits(scrmbl_bundle_t))
+  ) u_scrmbl_mtx (
+    .clk_i,
+    .rst_ni,
+    .req_i   ( part_scrmbl_mtx_req  ),
+    .data_i  ( part_scrmbl_req_bundle ),
+    .gnt_o   (                        ),
+    .idx_o   ( scrmbl_mtx_idx         ),
+    .valid_o ( scrmbl_mtx_valid       ),
+    .data_o  ( scrmbl_req_bundle      ),
+    .ready_i ( 1'b0                   )
+  );
+
+  // Since the ready_i signal of the arbiter is statically set to 1'b0 above, we are always in a
+  // "backpressure" situation, where the RR arbiter will automatically advance the internal RR state
+  // to give the current winner max priority in subsequent cycles in order to keep the decision
+  // stable. Rearbitration occurs once the winning agent deasserts its request.
+  always_comb begin : p_mutex
+    part_scrmbl_mtx_gnt = '0;
+    part_scrmbl_mtx_gnt[scrmbl_mtx_idx] = scrmbl_mtx_valid;
+  end
+
+  logic [ScrmblBlockWidth-1:0] part_scrmbl_rsp_data;
+  logic scrmbl_arb_req_ready, scrmbl_arb_rsp_valid;
+  logic [NumPart:0] part_scrmbl_req_ready, part_scrmbl_rsp_valid;
+
+  otp_ctrl_scrmbl u_scrmbl (
+    .clk_i,
+    .rst_ni,
+    .cmd_i   ( scrmbl_req_bundle.cmd   ),
+    .sel_i   ( scrmbl_req_bundle.sel   ),
+    .data_i  ( scrmbl_req_bundle.data  ),
+    .valid_i ( scrmbl_req_bundle.valid ),
+    .ready_o ( scrmbl_arb_req_ready    ),
+    .data_o  ( part_scrmbl_rsp_data    ),
+    .valid_o ( scrmbl_arb_rsp_valid    )
+  );
+
+  // steer back responses
+  always_comb begin : p_scmrbl_resp
+    part_scrmbl_req_ready = '0;
+    part_scrmbl_rsp_valid = '0;
+    part_scrmbl_req_ready[scrmbl_mtx_idx] = scrmbl_arb_req_ready;
+    part_scrmbl_rsp_valid[scrmbl_mtx_idx] = scrmbl_arb_rsp_valid;
+  end
+
+  /////////////////////////////
+  // Direct Access Interface //
+  /////////////////////////////
+
+  logic                           part_init_req;
+  logic [NumPart-1:0]             part_init_done;
+  part_access_t [NumPart-1:0]     part_access_dai;
+
+  otp_ctrl_dai u_otp_ctrl_dai (
+    .clk_i,
+    .rst_ni,
+    .init_req_i       ( pwr_otp_init_req_i.init               ),
+    .init_done_o      ( pwr_otp_init_rsp_o.done               ),
+    .part_init_req_o  ( part_init_req                         ),
+    .part_init_done_i ( part_init_done                        ),
+    .escalate_en_i    ( lc_escalate_en_i                      ),
+    .error_o          ( part_error[DaiIdx]                    ),
+    .part_access_i    ( part_access_dai                       ),
+    .dai_addr_i       ( dai_addr                              ),
+    .dai_cmd_i        ( dai_cmd                               ),
+    .dai_req_i        ( dai_req                               ),
+    .dai_wdata_i      ( dai_wdata                             ),
+    .dai_idle_o       ( dai_idle                              ),
+    .dai_cmd_done_o   ( otp_operation_done                    ),
+    .dai_rdata_o      ( dai_rdata                             ),
+    .otp_req_o        ( part_otp_arb_req[DaiIdx]              ),
+    .otp_cmd_o        ( part_otp_arb_bundle[DaiIdx].cmd       ),
+    .otp_size_o       ( part_otp_arb_bundle[DaiIdx].size      ),
+    .otp_wdata_o      ( part_otp_arb_bundle[DaiIdx].wdata     ),
+    .otp_addr_o       ( part_otp_arb_bundle[DaiIdx].addr      ),
+    .otp_gnt_i        ( part_otp_arb_gnt[DaiIdx]              ),
+    .otp_rvalid_i     ( part_otp_rvalid[DaiIdx]               ),
+    .otp_rdata_i      ( part_otp_rdata                        ),
+    .otp_err_i        ( part_otp_err                          ),
+    .scrmbl_mtx_req_o ( part_scrmbl_mtx_req[DaiIdx]           ),
+    .scrmbl_mtx_gnt_i ( part_scrmbl_mtx_gnt[DaiIdx]           ),
+    .scrmbl_cmd_o     ( part_scrmbl_req_bundle[DaiIdx].cmd    ),
+    .scrmbl_sel_o     ( part_scrmbl_req_bundle[DaiIdx].sel    ),
+    .scrmbl_data_o    ( part_scrmbl_req_bundle[DaiIdx].data   ),
+    .scrmbl_valid_o   ( part_scrmbl_req_bundle[DaiIdx].valid  ),
+    .scrmbl_ready_i   ( part_scrmbl_req_ready[DaiIdx]         ),
+    .scrmbl_valid_i   ( part_scrmbl_rsp_valid[DaiIdx]         ),
+    .scrmbl_data_i    ( part_scrmbl_rsp_data                  )
+  );
+
+  ////////////////////////////////////
+  // Lifecycle Transition Interface //
+  ////////////////////////////////////
+
+  otp_ctrl_lci #(
+    .Info(PartInfo[LifeCycleIdx])
+  ) u_otp_ctrl_lci (
+    .clk_i,
+    .rst_ni,
+    .escalate_en_i ( lc_escalate_en_i                  ),
+    .error_o       ( part_error[LciIdx]                ),
+    .lci_idle_o    ( lci_idle                          ),
+    // TODO: add LC interface
+    .otp_req_o     ( part_otp_arb_req[LciIdx]          ),
+    .otp_cmd_o     ( part_otp_arb_bundle[LciIdx].cmd   ),
+    .otp_size_o    ( part_otp_arb_bundle[LciIdx].size  ),
+    .otp_wdata_o   ( part_otp_arb_bundle[LciIdx].wdata ),
+    .otp_addr_o    ( part_otp_arb_bundle[LciIdx].addr  ),
+    .otp_gnt_i     ( part_otp_arb_gnt[LciIdx]          ),
+    .otp_rvalid_i  ( part_otp_rvalid[LciIdx]           ),
+    .otp_rdata_i   ( part_otp_rdata                    ),
+    .otp_err_i     ( part_otp_err                      )
+  );
+
+  /////////////////////////
+  // Partition Instances //
+  /////////////////////////
+
+  for (genvar k = 0; k < NumPart; k ++) begin : gen_partitions
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+    if (PartInfo[k].variant == Unbuffered) begin : gen_unbuffered
+      otp_ctrl_part_unbuf #(
+        .Info(PartInfo[k])
+      ) u_part_unbuf (
+        .clk_i,
+        .rst_ni,
+        .init_req_i    ( part_init_req                ),
+        .init_done_o   ( part_init_done[k]            ),
+        .escalate_en_i ( lc_escalate_en_i             ),
+        .error_o       ( part_error[k]                ),
+        .access_i      ( part_access_csrs[k]          ),
+        .access_o      ( part_access_dai[k]           ),
+        .digest_o      ( part_digest[k]               ),
+        // Note: this assumes that partitions with windows always come first.
+        .tl_i          ( tl_win_h2d[k]                ),
+        .tl_o          ( tl_win_d2h[k]                ),
+        .otp_req_o     ( part_otp_arb_req[k]          ),
+        .otp_cmd_o     ( part_otp_arb_bundle[k].cmd   ),
+        .otp_size_o    ( part_otp_arb_bundle[k].size  ),
+        .otp_wdata_o   ( part_otp_arb_bundle[k].wdata ),
+        .otp_addr_o    ( part_otp_arb_bundle[k].addr  ),
+        .otp_gnt_i     ( part_otp_arb_gnt[k]          ),
+        .otp_rvalid_i  ( part_otp_rvalid[k]           ),
+        .otp_rdata_i   ( part_otp_rdata               ),
+        .otp_err_i     ( part_otp_err                 )
+      );
+
+      // Tie off unused connections.
+      assign part_scrmbl_mtx_req[k]    = '0;
+      assign part_scrmbl_req_bundle[k] = '0;
+      // These checks do not exist in this partition type,
+      // so we always acknowledge the request.
+      assign integ_chk_ack[k]          = 1'b1;
+      assign cnsty_chk_ack[k]          = 1'b1;
+
+      // This stops lint from complaining about unused signals.
+      logic unused_part_scrmbl_req_ready, unused_part_scrmbl_rsp_valid, unused_part_scrmbl_mtx_gnt;
+      logic unused_integ_chk_req, unused_cnsty_chk_req;
+      assign unused_part_scrmbl_mtx_gnt   = part_scrmbl_mtx_gnt[k];
+      assign unused_part_scrmbl_req_ready = part_scrmbl_req_ready[k];
+      assign unused_part_scrmbl_rsp_valid = part_scrmbl_rsp_valid[k];
+      assign unused_integ_chk_req         = integ_chk_req[k];
+      assign unused_cnsty_chk_req         = cnsty_chk_req[k];
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+    end else if (PartInfo[k].variant == Buffered) begin : gen_buffered
+      otp_ctrl_part_buf #(
+        .Info(PartInfo[k])
+      ) u_part_buf (
+        .clk_i,
+        .rst_ni,
+        .init_req_i       ( part_init_req                   ),
+        .init_done_o      ( part_init_done[k]               ),
+        .integ_chk_req_i  ( integ_chk_req[k]                ),
+        .integ_chk_ack_o  ( integ_chk_ack[k]                ),
+        .cnsty_chk_req_i  ( cnsty_chk_req[k]                ),
+        .cnsty_chk_ack_o  ( cnsty_chk_ack[k]                ),
+        .escalate_en_i    ( lc_escalate_en_i                ),
+        .error_o          ( part_error[k]                   ),
+        .access_i         ( part_access_csrs[k]             ),
+        .access_o         ( part_access_dai[k]              ),
+        .digest_o         ( part_digest[k]                  ),
+        .data_o           (                                 ), // TODO: make breakout connections
+        .otp_req_o        ( part_otp_arb_req[k]             ),
+        .otp_cmd_o        ( part_otp_arb_bundle[k].cmd      ),
+        .otp_size_o       ( part_otp_arb_bundle[k].size     ),
+        .otp_wdata_o      ( part_otp_arb_bundle[k].wdata    ),
+        .otp_addr_o       ( part_otp_arb_bundle[k].addr     ),
+        .otp_gnt_i        ( part_otp_arb_gnt[k]             ),
+        .otp_rvalid_i     ( part_otp_rvalid[k]              ),
+        .otp_rdata_i      ( part_otp_rdata                  ),
+        .otp_err_i        ( part_otp_err                    ),
+        .scrmbl_mtx_req_o ( part_scrmbl_mtx_req[k]          ),
+        .scrmbl_mtx_gnt_i ( part_scrmbl_mtx_gnt[k]          ),
+        .scrmbl_cmd_o     ( part_scrmbl_req_bundle[k].cmd   ),
+        .scrmbl_sel_o     ( part_scrmbl_req_bundle[k].sel   ),
+        .scrmbl_data_o    ( part_scrmbl_req_bundle[k].data  ),
+        .scrmbl_valid_o   ( part_scrmbl_req_bundle[k].valid ),
+        .scrmbl_ready_i   ( part_scrmbl_req_ready[k]        ),
+        .scrmbl_valid_i   ( part_scrmbl_rsp_valid[k]        ),
+        .scrmbl_data_i    ( part_scrmbl_rsp_data            )
+      );
+    end else begin : gen_invalid
+      // This is invalid and should break elaboration
+      assert_static_in_generate_invalid assert_static_in_generate_invalid();
+    end
+  end
 
 endmodule : otp_ctrl
diff --git a/hw/ip/otp_ctrl/rtl/otp_ctrl_dai.sv b/hw/ip/otp_ctrl/rtl/otp_ctrl_dai.sv
new file mode 100644
index 0000000..054b702
--- /dev/null
+++ b/hw/ip/otp_ctrl/rtl/otp_ctrl_dai.sv
@@ -0,0 +1,682 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// Direct access interface for OTP controller.
+//
+
+`include "prim_assert.sv"
+
+module otp_ctrl_dai
+  import otp_ctrl_pkg::*;
+  import otp_ctrl_reg_pkg::*;
+(
+  input                                  clk_i,
+  input                                  rst_ni,
+  // Init reqest from power manager
+  input                                  init_req_i,
+  output logic                           init_done_o,
+  // Init request going to partitions
+  output logic                           part_init_req_o,
+  input  [NumPart-1:0]                   part_init_done_i,
+  // Escalation input. This moves the FSM into a terminal state and locks down
+  // the DAI.
+  input lc_tx_t                          escalate_en_i,
+  // Output error state of DAI, to be consumed by OTP error/alert logic.
+  // Note that most errors are not recoverable and move the DAI FSM into
+  // a terminal error state.
+  output otp_err_e                       error_o,
+  // Access/lock status from partitions
+  input  part_access_t [NumPart-1:0]     part_access_i,
+  // CSR interface
+  input        [OtpByteAddrWidth-1:0]    dai_addr_i,
+  input dai_cmd_e                        dai_cmd_i,
+  input logic                            dai_req_i,
+  input        [NumDaiWords-1:0][31:0]   dai_wdata_i,
+  output logic                           dai_idle_o,     // wired to the status CSRs
+  output logic                           dai_cmd_done_o, // this is used to raise an IRQ
+  output logic [NumDaiWords-1:0][31:0]   dai_rdata_o,
+  // OTP interface
+  output logic                           otp_req_o,
+  output prim_otp_cmd_e                  otp_cmd_o,
+  output logic [OtpSizeWidth-1:0]        otp_size_o,
+  output logic [OtpIfWidth-1:0]          otp_wdata_o,
+  output logic [OtpAddrWidth-1:0]        otp_addr_o,
+  input                                  otp_gnt_i,
+  input                                  otp_rvalid_i,
+  input  [ScrmblBlockWidth-1:0]          otp_rdata_i,
+  input  otp_err_e                       otp_err_i,
+  // Scrambling mutex request
+  output logic                           scrmbl_mtx_req_o,
+  input                                  scrmbl_mtx_gnt_i,
+  // Scrambling datapath interface
+  output otp_scrmbl_cmd_e                scrmbl_cmd_o,
+  output logic [ConstSelWidth-1:0]       scrmbl_sel_o,
+  output logic [ScrmblBlockWidth-1:0]    scrmbl_data_o,
+  output logic                           scrmbl_valid_o,
+  input  logic                           scrmbl_ready_i,
+  input  logic                           scrmbl_valid_i,
+  input  logic [ScrmblBlockWidth-1:0]    scrmbl_data_i
+);
+
+  ////////////////////////
+  // Integration Checks //
+  ////////////////////////
+
+  import prim_util_pkg::vbits;
+
+  localparam int CntWidth = OtpByteAddrWidth - $clog2(ScrmblBlockWidth/8);
+
+  // Integration checks for parameters.
+  `ASSERT_INIT(CheckNativeOtpWidth0_A, ScrmblBlockWidth % OtpWidth == 0)
+  `ASSERT_INIT(CheckNativeOtpWidth1_A, 32 % OtpWidth == 0)
+
+  /////////////////////
+  // DAI Control FSM //
+  /////////////////////
+
+  // Encoding generated with ./sparse-fsm-encode -d 5 -m 20 -n 12
+  // Hamming distance histogram:
+  //
+  // 0:  --
+  // 1:  --
+  // 2:  --
+  // 3:  --
+  // 4:  --
+  // 5:  ||||||||||||||| (30.53%)
+  // 6:  |||||||||||||||||||| (38.42%)
+  // 7:  |||||||| (15.79%)
+  // 8:  |||| (8.42%)
+  // 9:  | (3.68%)
+  // 10:  (1.05%)
+  // 11: | (2.11%)
+  // 12: --
+  //
+  // Minimum Hamming distance: 5
+  // Maximum Hamming distance: 11
+  //
+  typedef enum logic [11:0] {
+    ResetSt       = 12'b000000011100,
+    InitOtpSt     = 12'b010011000101,
+    InitPartSt    = 12'b101011010111,
+    IdleSt        = 12'b000010001011,
+    ErrorSt       = 12'b010010110000,
+    ReadSt        = 12'b111110000001,
+    ReadWaitSt    = 12'b001111100011,
+    DescrSt       = 12'b100101000110,
+    DescrWaitSt   = 12'b011000000010,
+    WriteSt       = 12'b001100111010,
+    WriteWaitSt   = 12'b110111101111,
+    ScrSt         = 12'b101010101000,
+    ScrWaitSt     = 12'b111110011110,
+    DigClrSt      = 12'b011101110100,
+    DigReadSt     = 12'b001101001101,
+    DigReadWaitSt = 12'b111001111001,
+    DigSt         = 12'b100111011000,
+    DigPadSt      = 12'b100000100101,
+    DigFinSt      = 12'b010101010011,
+    DigWaitSt     = 12'b010100101001
+  } state_e;
+
+  typedef enum logic [1:0] {
+    OtpData = 2'b00,
+    DaiData = 2'b01,
+    ScrmblData = 2'b10
+  } data_sel_e;
+
+
+  typedef enum logic {
+    PartOffset = 1'b0,
+    DaiOffset = 1'b1
+  } addr_sel_e;
+
+  state_e state_d, state_q;
+  logic [CntWidth-1:0] cnt_d, cnt_q;
+  logic cnt_en, cnt_clr;
+  otp_err_e error_d, error_q;
+  logic data_en, data_clr;
+  data_sel_e data_sel;
+  addr_sel_e base_sel_d, base_sel_q;
+  logic [ScrmblBlockWidth-1:0] data_q;
+  logic [NumPartWidth-1:0] part_idx;
+  logic [NumPart-1:0][OtpAddrWidth-1:0] digest_addr_lut;
+
+  // Output partition error state.
+  assign error_o       = error_q;
+  // Working register is connected to data outputs.
+  assign dai_rdata_o   = data_q;
+  assign otp_wdata_o   = data_q;
+  assign scrmbl_data_o = data_q;
+
+  always_comb begin : p_fsm
+    state_d = state_q;
+
+    // Init signals
+    init_done_o = 1'b1;
+    part_init_req_o = 1'b0;
+
+    // DAI signals
+    dai_idle_o = 1'b0;
+    dai_cmd_done_o = 1'b0;
+
+    // OTP signals
+    otp_req_o = 1'b0;
+    otp_cmd_o = OtpInit;
+
+    // Scrambling mutex
+    scrmbl_mtx_req_o = 1'b0;
+
+    // Scrambling datapath
+    scrmbl_cmd_o   = LoadShadow;
+    scrmbl_sel_o   = '0;
+    scrmbl_valid_o = 1'b0;
+
+    // Counter
+    cnt_en  = 1'b0;
+    cnt_clr = 1'b0;
+    base_sel_d = base_sel_q;
+
+    // Temporary data register
+    data_en = 1'b0;
+    data_clr = 1'b0;
+    data_sel = OtpData;
+
+    // Error Register
+    error_d = error_q;
+
+    unique case (state_q)
+      ///////////////////////////////////////////////////////////////////
+      // We get here after reset and wait until the power manager
+      // requests OTP initialization. If initialization is requested,
+      // an init command is written to the OTP macro, and we move on
+      // to the InitOtpSt waiting state.
+      ResetSt: begin
+        init_done_o = 1'b0;
+        data_clr = 1'b1;
+        if (init_req_i) begin
+          otp_req_o = 1'b1;
+          if (otp_gnt_i) begin
+            state_d = InitOtpSt;
+          end
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // We wait here unitl the OTP macro has initialized without
+      // error. If an error occurred during this stage, we latch that
+      // error and move into a terminal error  state.
+      InitOtpSt: begin
+        init_done_o = 1'b0;
+        if (otp_rvalid_i) begin
+          if ((!(otp_err_i inside {NoErr, OtpReadCorrErr}))) begin
+            state_d = ErrorSt;
+            error_d = otp_err_i;
+          end else begin
+            state_d = InitPartSt;
+          end
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Since the OTP macro is now functional, we can send out an
+      // initialization request to all partitions and wait until they
+      // all have initialized.
+      InitPartSt: begin
+        init_done_o = 1'b0;
+        part_init_req_o = 1'b1;
+        if (part_init_done_i == {NumPart{1'b1}}) begin
+          state_d = IdleSt;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Idle state where we wait for incoming commands.
+      // Invalid commands trigger a CmdInvErr, which is recoverable.
+      IdleSt: begin
+        dai_idle_o  = 1'b1;
+        if (dai_req_i) begin
+          // This clears previous (recoverable) errors.
+          error_d = NoErr;
+          // Clear the temporary data register.
+          data_clr = 1'b1;
+          unique case (dai_cmd_i)
+            DaiRead:  begin
+              state_d = ReadSt;
+              base_sel_d = DaiOffset;
+            end
+            DaiWrite: begin
+              // Fetch data block.
+              data_sel = DaiData;
+              data_en = 1'b1;
+              base_sel_d = DaiOffset;
+              // If this partition is scrambled, directly go to write scrambling first.
+              if (PartInfo[part_idx].scrambled) begin
+                state_d = ScrSt;
+              end else begin
+                state_d = WriteSt;
+              end
+            end
+            DaiDigest: begin
+              state_d = DigClrSt;
+              scrmbl_mtx_req_o = 1'b1;
+              base_sel_d = PartOffset;
+            end
+            default: begin
+              // Invalid commands get caught here. This is a recoverable error.
+              error_d = CmdInvErr;
+            end
+          endcase // dai_cmd_i
+        end // dai_req_i
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Each time we request a block of data from OTP, we re-check
+      // whether read access has been locked for this partition. If
+      // that is the case, we immediately bail out. Otherwise, we
+      // request a block of data from OTP.
+      ReadSt: begin
+        if (part_access_i[part_idx].read_lock == Unlocked) begin
+          otp_req_o = 1'b1;
+          otp_cmd_o = OtpRead;
+          if (otp_gnt_i) begin
+            state_d = ReadWaitSt;
+          end
+        end else begin
+          state_d = IdleSt;
+          error_d = AccessErr; // Signal this error, but do not go into terminal error state.
+          dai_cmd_done_o = 1'b1;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Wait for OTP response and write to readout register. Check
+      // whether descrambling is  required or not. In case an OTP
+      // transaction fails, latch the OTP error code, and jump to
+      // terminal error state.
+      ReadWaitSt: begin
+        if (otp_rvalid_i) begin
+          // Check OTP return code.
+          if ((!(otp_err_i inside {NoErr, OtpReadCorrErr}))) begin
+            state_d = ErrorSt;
+            error_d = otp_err_i;
+          end else begin
+            data_en = 1'b1;
+            if (PartInfo[part_idx].scrambled) begin
+              state_d = DescrSt;
+            end else begin
+              state_d = IdleSt;
+              dai_cmd_done_o = 1'b1;
+            end
+            // Signal soft ECC errors, but do not go into terminal error state.
+            if (otp_err_i == OtpReadCorrErr) begin
+              error_d = otp_err_i;
+            end
+          end
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Descrambling state. This first acquires the scrambling
+      // datapath mutex. Note that once the mutex is acquired, we have
+      // exclusive access to the scrambling datapath until we release
+      // the mutex by deasserting scrmbl_mtx_req_o.
+      DescrSt: begin
+        scrmbl_mtx_req_o = 1'b1;
+        scrmbl_valid_o = 1'b1;
+        scrmbl_cmd_o = Decrypt;
+        scrmbl_sel_o = PartInfo[part_idx].key_idx;
+        if (scrmbl_mtx_gnt_i && scrmbl_ready_i) begin
+          state_d = DescrWaitSt;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Wait for the descrambled data to return. Note that we release
+      // the mutex lock upon leaving this state.
+      DescrWaitSt: begin
+        scrmbl_mtx_req_o = 1'b1;
+        scrmbl_sel_o = PartInfo[part_idx].key_idx;
+        data_sel = ScrmblData;
+        if (scrmbl_valid_i) begin
+          state_d = IdleSt;
+          data_en = 1'b1;
+          dai_cmd_done_o = 1'b1;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // First, check whether write accesses are allowed to this
+      // partition, and error out otherwise. Note that for buffered
+      // partitions, we do not allow DAI writes to the digest offset.
+      // Unbuffered partitions have SW managed digests, hence that
+      // check is not needed in that case. The LC partition is
+      // permanently write locked and can hence not be written via the DAI.
+      WriteSt: begin
+        if (part_access_i[part_idx].write_lock == Unlocked &&
+            // If this is a HW digest write to a buffered partition.
+            ((PartInfo[part_idx].variant == Buffered && PartInfo[part_idx].hw_digest &&
+              base_sel_q == PartOffset && otp_addr_o == digest_addr_lut[part_idx]) ||
+             // If this is a non HW digest write to a buffered partition.
+             (PartInfo[part_idx].variant == Buffered && PartInfo[part_idx].hw_digest &&
+              base_sel_q == DaiOffset && otp_addr_o < digest_addr_lut[part_idx]) ||
+             // If this is a write to an unbuffered partition
+             (PartInfo[part_idx].variant != Buffered && base_sel_q == DaiOffset))) begin
+          otp_req_o = 1'b1;
+          otp_cmd_o = OtpWrite;
+          if (otp_gnt_i) begin
+            state_d = WriteWaitSt;
+          end
+        end else begin
+          state_d = IdleSt;
+          error_d = AccessErr; // Signal this error, but do not go into terminal error state.
+          dai_cmd_done_o = 1'b1;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Wait for OTP response, and then go back to idle. In case an
+      // OTP transaction fails, latch the OTP error code, and jump to
+      // terminal error state.
+      WriteWaitSt: begin
+        if (otp_rvalid_i) begin
+          // Check OTP return code. Note that non-blank errors are recoverable.
+          if ((!(otp_err_i inside {NoErr, OtpWriteBlankErr}))) begin
+            state_d = ErrorSt;
+            error_d = otp_err_i;
+          end else begin
+            state_d = IdleSt;
+            dai_cmd_done_o = 1'b1;
+            // Signal non-blank state, but do not go to terminal error state.
+            if (otp_err_i == OtpWriteBlankErr) begin
+              error_d = otp_err_i;
+            end
+          end
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Scrambling state. This first acquires the scrambling
+      // datapath mutex. Note that once the mutex is acquired, we have
+      // exclusive access to the scrambling datapath until we release
+      // the mutex by deasserting scrmbl_mtx_req_o.
+      ScrSt: begin
+        scrmbl_mtx_req_o = 1'b1;
+        scrmbl_valid_o = 1'b1;
+        scrmbl_cmd_o = Encrypt;
+        scrmbl_sel_o = PartInfo[part_idx].key_idx;
+        if (scrmbl_mtx_gnt_i && scrmbl_ready_i) begin
+          state_d = ScrWaitSt;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Wait for the scrambled data to return. Note that we release
+      // the mutex lock upon leaving this state.
+      ScrWaitSt: begin
+        scrmbl_mtx_req_o = 1'b1;
+        data_sel = ScrmblData;
+        if (scrmbl_valid_i) begin
+          state_d = WriteSt;
+          data_en = 1'b1;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // First, acquire the mutex for the digest and clear the digest state.
+      DigClrSt: begin
+        scrmbl_mtx_req_o = 1'b1;
+        scrmbl_valid_o = 1'b1;
+        // Need to reset the digest state and set digest mode to "standard".
+        scrmbl_sel_o = StandardMode;
+        scrmbl_cmd_o = DigestInit;
+        if (scrmbl_mtx_gnt_i && scrmbl_ready_i) begin
+          state_d = DigReadSt;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // This requests a 64bit block to be pushed into the digest datapath.
+      // We also check here whether the partition has been write locked.
+      DigReadSt: begin
+        scrmbl_mtx_req_o = 1'b1;
+        if (part_access_i[part_idx].read_lock == Unlocked &&
+            part_access_i[part_idx].write_lock == Unlocked) begin
+          otp_req_o = 1'b1;
+          otp_cmd_o = OtpRead;
+          if (otp_gnt_i) begin
+            state_d = DigReadWaitSt;
+          end
+        end else begin
+          state_d = IdleSt;
+          error_d = AccessErr; // Signal this error, but do not go into terminal error state.
+          dai_cmd_done_o = 1'b1;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Wait for OTP response and write to readout register. Check
+      // whether descrambling is required or not. In case an OTP
+      // transaction fails, latch the OTP error code, and jump to
+      // terminal error state.
+      DigReadWaitSt: begin
+        scrmbl_mtx_req_o = 1'b1;
+        if (otp_rvalid_i) begin
+          cnt_en = 1'b1;
+          // Check OTP return code.
+          if ((!(otp_err_i inside {NoErr, OtpReadCorrErr}))) begin
+            state_d = ErrorSt;
+            error_d = otp_err_i;
+          end else begin
+            data_en = 1'b1;
+            state_d = DigSt;
+            // Signal soft ECC errors, but do not go into terminal error state.
+            if (otp_err_i == OtpReadCorrErr) begin
+              error_d = otp_err_i;
+            end
+          end
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Push the word read into the scrambling datapath.  The last
+      // block is repeated in case the number blocks in this partition
+      // is odd.
+      DigSt: begin
+        scrmbl_mtx_req_o = 1'b1;
+        scrmbl_valid_o = 1'b1;
+        // No need to digest the digest value itself
+        if (otp_addr_o == digest_addr_lut[part_idx]) begin
+          // Trigger digest round in case this is the second block in a row.
+          if (cnt_q[0]) begin
+            scrmbl_cmd_o = Digest;
+            if (scrmbl_ready_i) begin
+              state_d = DigFinSt;
+            end
+          // Otherwise, just load low word and go to padding state.
+          end else if (scrmbl_ready_i) begin
+            state_d = DigPadSt;
+          end
+        end else begin
+          // Trigger digest round in case this is the second block in a row.
+          if (cnt_q[0]) begin
+            scrmbl_cmd_o = Digest;
+          end
+          // Go back and fetch more data blocks.
+          if (scrmbl_ready_i) begin
+            state_d = DigReadSt;
+          end
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Padding state, just repeat the last block and go to digest
+      // finalization.
+      DigPadSt: begin
+        scrmbl_mtx_req_o = 1'b1;
+        scrmbl_valid_o = 1'b1;
+        scrmbl_cmd_o = Digest;
+        if (scrmbl_ready_i) begin
+          state_d = DigFinSt;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Trigger digest finalization and go wait for the result.
+      DigFinSt: begin
+        scrmbl_mtx_req_o = 1'b1;
+        scrmbl_valid_o = 1'b1;
+        scrmbl_cmd_o = DigestFinalize;
+        if (scrmbl_ready_i) begin
+          state_d = DigWaitSt;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Wait for the digest to return, and write the result to OTP.
+      // Note that the write address will be correct in this state,
+      // since the counter has been stepped to the correct address as
+      // part of the readout sequence, and the correct size for this
+      // access has been loaded before.
+      DigWaitSt: begin
+        scrmbl_mtx_req_o = 1'b1;
+        data_sel = ScrmblData;
+        if (scrmbl_valid_i) begin
+          state_d = WriteSt;
+          data_en = 1'b1;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Terminal Error State. This locks access to the DAI. Make sure
+      // an FsmErr error code is assigned here, in case no error code has
+      // been assigned yet.
+      ErrorSt: begin
+        if (!error_q) begin
+          error_d = FsmErr;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // We should never get here. If we do (e.g. via a malicious
+      // glitch), error out immediately.
+      default: begin
+        state_d = ErrorSt;
+      end
+      ///////////////////////////////////////////////////////////////////
+    endcase // state_q
+
+    if (state_q != ErrorSt) begin
+      // Unconditionally jump into the termninal error state in case of
+      // escalation, and lock access to the DAI down.
+      if (escalate_en_i != Off) begin
+        state_d = ErrorSt;
+        error_d = EscErr;
+      end
+    end
+  end
+
+  ////////////////////////////
+  // Partition Select Logic //
+  ////////////////////////////
+
+  // This checks which partition the address belongs to by comparing
+  // the incoming address to the partition address ranges. The onehot
+  // bitvector generated by the parallel comparisons is fed into a
+  // binary tree that determines the partition index with O(log(N)) delay.
+
+  logic [NumPart-1:0] part_sel_oh;
+  for (genvar k = 0; k < NumPart; k++) begin : gen_part_sel
+    localparam logic [OtpByteAddrWidth:0] PartEnd = (OtpByteAddrWidth+1)'(PartInfo[k].offset) +
+                                                    (OtpByteAddrWidth+1)'(PartInfo[k].size);
+    assign part_sel_oh[k] = (dai_addr_i >= PartInfo[k].offset) & ({1'b0, dai_addr_i} < PartEnd);
+    // Needed for other address and access checks.
+    localparam logic [OtpByteAddrWidth-1:0] DigestOffset = OtpByteAddrWidth'(PartEnd -
+                                                                             ScrmblBlockWidth/8);
+    assign digest_addr_lut[k] = DigestOffset >> OtpAddrShift;
+  end
+
+  `ASSERT(PartSelMustBeOnehot_A, $onehot0(part_sel_oh))
+
+  prim_arbiter_fixed #(
+    .N(NumPart),
+    .EnDataPort(0)
+  ) i_prim_arbiter_fixed (
+    .clk_i,
+    .rst_ni,
+    .req_i   ( part_sel_oh    ),
+    .data_i  ( '{default: '0} ),
+    .gnt_o   (                ), // unused
+    .idx_o   ( part_idx       ),
+    .valid_o (                ), // unused
+    .data_o  (                ), // unused
+    .ready_i ( 1'b0           )
+  );
+
+  /////////////////////////////////////
+  // Address Calculations for Digest //
+  /////////////////////////////////////
+
+  // Depending on whether this is a 32bit or 64bit partition, we cut off the lower address bits.
+  logic [OtpByteAddrWidth-1:0] addr_base;
+  assign addr_base = (base_sel_q == PartOffset)     ? PartInfo[part_idx].offset :
+                     (PartInfo[part_idx].scrambled) ? {dai_addr_i[OtpByteAddrWidth-1:3], 3'h0} :
+                                                      {dai_addr_i[OtpByteAddrWidth-1:2], 2'h0};
+
+  // OTP transaction sizes are 64bit for scrambled partitions, and when digesting.
+  // Otherwise, they are 32bit.
+  assign otp_size_o = (PartInfo[part_idx].scrambled || base_sel_q == PartOffset) ?
+                      OtpSizeWidth'(unsigned'(ScrmblBlockWidth / OtpWidth - 1)) :
+                      OtpSizeWidth'(unsigned'(32 / OtpWidth - 1));
+
+  // Address counter - this is only used for computing a digest, hence the increment is
+  // fixed to 8 byte.
+  assign cnt_d = (cnt_clr) ? '0           :
+                 (cnt_en)  ? cnt_q + 1'b1 : cnt_q;
+
+  // Note that OTP works on halfword (16bit) addresses, hence need to
+  // shift the addresses appropriately.
+  logic [OtpByteAddrWidth-1:0] addr_calc;
+  assign addr_calc = {cnt_q, {$clog2(ScrmblBlockWidth/8){1'b0}}} + addr_base;
+  assign otp_addr_o = addr_calc >> OtpAddrShift;
+
+  ///////////////
+  // Registers //
+  ///////////////
+
+  always_ff @(posedge clk_i or negedge rst_ni) begin : p_regs
+    if (!rst_ni) begin
+      state_q        <= ResetSt;
+      error_q        <= NoErr;
+      cnt_q         <= '0;
+      data_q         <= '0;
+      base_sel_q     <= DaiOffset;
+    end else begin
+      state_q        <= state_d;
+      error_q        <= error_d;
+      cnt_q          <= cnt_d;
+      base_sel_q     <= base_sel_d;
+
+      // Working register
+      if (data_clr) begin
+        data_q <= '0;
+      end else if (data_en) begin
+        if (data_sel == ScrmblData) begin
+          data_q <= scrmbl_data_i;
+        end else if (data_sel == DaiData) begin
+          data_q <= dai_wdata_i;
+        end else begin
+          data_q <= otp_rdata_i;
+        end
+      end
+    end
+  end
+
+  ////////////////
+  // Assertions //
+  ////////////////
+
+  // Known assertions
+  `ASSERT_KNOWN(InitDoneKnown_A,     init_done_o)
+  `ASSERT_KNOWN(PartInitReqKnown_A,  part_init_req_o)
+  `ASSERT_KNOWN(ErrorKnown_A,        error_o)
+  `ASSERT_KNOWN(DaiIdleKnown_A,      dai_idle_o)
+  `ASSERT_KNOWN(DaiRdataKnown_A,     dai_rdata_o)
+  `ASSERT_KNOWN(OtpReqKnown_A,       otp_req_o)
+  `ASSERT_KNOWN(OtpCmdKnown_A,       otp_cmd_o)
+  `ASSERT_KNOWN(OtpSizeKnown_A,      otp_size_o)
+  `ASSERT_KNOWN(OtpWdataKnown_A,     otp_wdata_o)
+  `ASSERT_KNOWN(OtpAddrKnown_A,      otp_addr_o)
+  `ASSERT_KNOWN(ScrmblMtxReqKnown_A, scrmbl_mtx_req_o)
+  `ASSERT_KNOWN(ScrmblCmdKnown_A,    scrmbl_cmd_o)
+  `ASSERT_KNOWN(ScrmblSelKnown_A,    scrmbl_sel_o)
+  `ASSERT_KNOWN(ScrmblDataKnown_A,   scrmbl_data_o)
+  `ASSERT_KNOWN(ScrmblValidKnown_A,  scrmbl_valid_o)
+
+  // OTP error response
+  `ASSERT(OtpErrorState_A,
+      state_q inside {InitOtpSt, ReadWaitSt, WriteWaitSt, DigReadWaitSt} && otp_rvalid_i &&
+      !(otp_err_i inside {NoErr, OtpReadCorrErr})
+      |=>
+      state_q == ErrorSt && error_o == $past(otp_err_i))
+
+endmodule : otp_ctrl_dai
diff --git a/hw/ip/otp_ctrl/rtl/otp_ctrl_lci.sv b/hw/ip/otp_ctrl/rtl/otp_ctrl_lci.sv
new file mode 100644
index 0000000..7e47aa4
--- /dev/null
+++ b/hw/ip/otp_ctrl/rtl/otp_ctrl_lci.sv
@@ -0,0 +1,41 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// Life cycle interface for performing life cycle transitions in OTP.
+//
+
+`include "prim_assert.sv"
+
+module otp_ctrl_lci
+  import otp_ctrl_pkg::*;
+  import otp_ctrl_reg_pkg::*;
+#(
+  // Lifecycle partition information
+  parameter part_info_t Info
+) (
+  input                               clk_i,
+  input                               rst_ni,
+  // Escalation input. This moves the FSM into a terminal state and locks down
+  // the partition.
+  input  lc_tx_t                      escalate_en_i,
+
+  // TODO: add lifecycle transition interface
+  // Output error state of partition, to be consumed by OTP error/alert logic.
+  // Note that most errors are not recoverable and move the partition FSM into
+  // a terminal error state.
+  output otp_err_e                    error_o,
+  output logic                        lci_idle_o,
+  // OTP interface
+  output logic                        otp_req_o,
+  output prim_otp_cmd_e               otp_cmd_o,
+  output logic [OtpSizeWidth-1:0]     otp_size_o,
+  output logic [OtpIfWidth-1:0]       otp_wdata_o,
+  output logic [OtpAddrWidth-1:0]     otp_addr_o,
+  input                               otp_gnt_i,
+  input                               otp_rvalid_i,
+  input  [ScrmblBlockWidth-1:0]       otp_rdata_i,
+  input  otp_err_e                    otp_err_i
+);
+
+endmodule : otp_ctrl_lci
diff --git a/hw/ip/otp_ctrl/rtl/otp_ctrl_parity_reg.sv b/hw/ip/otp_ctrl/rtl/otp_ctrl_parity_reg.sv
new file mode 100644
index 0000000..34bd250
--- /dev/null
+++ b/hw/ip/otp_ctrl/rtl/otp_ctrl_parity_reg.sv
@@ -0,0 +1,88 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// Parity-protected register file for buffered OTP partitions.
+//
+// TODO: discuss whether reset is allowed here. We may also need a secure wiping feature.
+
+`include "prim_assert.sv"
+
+module otp_ctrl_parity_reg #(
+  parameter  int Width = 32, // bit
+  parameter  int Depth = 128,
+  localparam int Aw    = prim_util_pkg::vbits(Depth) // derived parameter
+) (
+  input  logic                        clk_i,
+  input  logic                        rst_ni,
+
+  input  logic                        wren_i,
+  input  logic [Aw-1:0]               addr_i,
+  input  logic [Width-1:0]            wdata_i,
+
+  // Concurrent output of the register state.
+  output logic [Depth-1:0][Width-1:0] data_o,
+  // Concurrent parity check error is flagged via this signal.
+  output logic                        parity_err_o
+);
+
+  // Integration checks for parameters.
+  `ASSERT_INIT(WidthMustBeByteAligned_A, Width % 8 == 0)
+
+  logic [Depth-1:0][Width-1:0]   data_d, data_q;
+  logic [Depth-1:0][Width/8-1:0] parity_d, parity_q;
+
+  if (Depth == 1) begin : gen_one_word_only
+    always_comb begin : p_write
+      data_o = data_q;
+      data_d = data_q;
+      parity_d = parity_q;
+
+      if (wren_i && 32'(addr_i) < Depth) begin
+        data_d[0] = wdata_i;
+        // Calculate EVEN parity bit for each Byte written.
+        for (int k = 0; k < Width/8; k ++) begin
+          parity_d[0][k] = ^wdata_i[k*8 +: 8];
+        end
+      end
+    end
+  end else begin : gen_multiple_words
+    always_comb begin : p_write
+      data_o = data_q;
+      data_d = data_q;
+      parity_d = parity_q;
+
+      if (wren_i && 32'(addr_i) < Depth) begin
+        data_d[addr_i] = wdata_i;
+        // Calculate EVEN parity bit for each Byte written.
+        for (int k = 0; k < Width/8; k ++) begin
+          parity_d[addr_i][k] = ^wdata_i[k*8 +: 8];
+        end
+      end
+    end
+  end
+
+  always_comb begin : p_parity
+    // Concurrent parity check.
+    parity_err_o = 1'b0;
+    for (int j = 0; j < Depth; j ++) begin
+      for (int k = 0; k < Width/8; k ++) begin
+        parity_err_o |= ^{data_q[j][k*8 +: 8], parity_q[j][k]};
+      end
+    end
+  end
+
+  always_ff @(posedge clk_i or negedge rst_ni) begin : p_regs
+    if (!rst_ni) begin
+      parity_q <= '0;
+      data_q   <= '0;
+    end else begin
+      parity_q <= parity_d;
+      data_q   <= data_d;
+    end
+  end
+
+  `ASSERT_KNOWN(ParityKnown_A, parity_q)
+  `ASSERT_KNOWN(DataKnown_A, data_q)
+
+endmodule : otp_ctrl_parity_reg
diff --git a/hw/ip/otp_ctrl/rtl/otp_ctrl_part_buf.sv b/hw/ip/otp_ctrl/rtl/otp_ctrl_part_buf.sv
new file mode 100644
index 0000000..170cc84
--- /dev/null
+++ b/hw/ip/otp_ctrl/rtl/otp_ctrl_part_buf.sv
@@ -0,0 +1,656 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// Buffered partition for OTP controller.
+//
+
+`include "prim_assert.sv"
+
+module otp_ctrl_part_buf
+  import otp_ctrl_pkg::*;
+  import otp_ctrl_reg_pkg::*;
+#(
+  // Partition information.
+  parameter part_info_t Info
+) (
+  input                               clk_i,
+  input                               rst_ni,
+  // Pulse to start partition initialisation (required once per power cycle).
+  input                               init_req_i,
+  output logic                        init_done_o,
+  // Integrity check requests
+  input                               integ_chk_req_i,
+  output logic                        integ_chk_ack_o,
+  // Consistency check requests
+  input                               cnsty_chk_req_i,
+  output logic                        cnsty_chk_ack_o,
+  // Escalation input. This moves the FSM into a terminal state and locks down
+  // the partition.
+  input  lc_tx_t                      escalate_en_i,
+  // Output error state of partition, to be consumed by OTP error/alert logic.
+  // Note that most errors are not recoverable and move the partition FSM into
+  // a terminal error state.
+  output otp_err_e                    error_o,
+  // Access/lock status
+  input  part_access_t                access_i, // runtime lock from CSRs
+  output part_access_t                access_o,
+  // Buffered 64bit digest output.
+  output logic [ScrmblBlockWidth-1:0] digest_o,
+  output logic [Info.size*8-1:0]      data_o,
+  // OTP interface
+  output logic                        otp_req_o,
+  output prim_otp_cmd_e               otp_cmd_o,
+  output logic [OtpSizeWidth-1:0]     otp_size_o,
+  output logic [OtpIfWidth-1:0]       otp_wdata_o,
+  output logic [OtpAddrWidth-1:0]     otp_addr_o,
+  input                               otp_gnt_i,
+  input                               otp_rvalid_i,
+  input  [ScrmblBlockWidth-1:0]       otp_rdata_i,
+  input  otp_err_e                    otp_err_i,
+  // Scrambling mutex request
+  output logic                        scrmbl_mtx_req_o,
+  input                               scrmbl_mtx_gnt_i,
+  // Scrambling datapath interface
+  output otp_scrmbl_cmd_e             scrmbl_cmd_o,
+  output logic [ConstSelWidth-1:0]    scrmbl_sel_o,
+  output logic [ScrmblBlockWidth-1:0] scrmbl_data_o,
+  output logic                        scrmbl_valid_o,
+  input  logic                        scrmbl_ready_i,
+  input  logic                        scrmbl_valid_i,
+  input  logic [ScrmblBlockWidth-1:0] scrmbl_data_i
+);
+
+  ////////////////////////
+  // Integration Checks //
+  ////////////////////////
+
+  import prim_util_pkg::vbits;
+
+  localparam int DigestOffset = Info.offset + Info.size - ScrmblBlockWidth/8;
+  localparam int NumScrmblBlocks = Info.size / (ScrmblBlockWidth/8);
+  localparam int CntWidth = vbits(NumScrmblBlocks);
+
+  // Integration checks for parameters.
+  `ASSERT_INIT(OffsetMustBeBlockAligned_A, Info.offset % ScrmblBlockWidth/8 == 0)
+  `ASSERT_INIT(SizeMustBeBlockAligned_A, Info.size % ScrmblBlockWidth/8 == 0)
+  `ASSERT(ScrambledImpliesDigest_A, Info.scrambled |-> Info.hw_digest)
+  `ASSERT(WriteLockImpliesDigest_A, Info.read_lock |-> Info.hw_digest)
+  `ASSERT(ReadLockImpliesDigest_A, Info.write_lock |-> Info.hw_digest)
+
+  ///////////////////////
+  // OTP Partition FSM //
+  ///////////////////////
+
+  // Encoding generated with ./sparse-fsm-encode -d 5 -m 16 -n 12
+  // Hamming distance histogram:
+  //
+  // 0:  --
+  // 1:  --
+  // 2:  --
+  // 3:  --
+  // 4:  --
+  // 5:  |||||||||||||||| (29.17%)
+  // 6:  |||||||||||||||||||| (35.83%)
+  // 7:  ||||||||||| (20.83%)
+  // 8:  |||| (7.50%)
+  // 9:  | (2.50%)
+  // 10: | (3.33%)
+  // 11:  (0.83%)
+  // 12: --
+  //
+  // Minimum Hamming distance: 5
+  // Maximum Hamming distance: 11
+  //
+  typedef enum logic [11:0] {
+    ResetSt         = 12'b110001101001,
+    InitSt          = 12'b000100100000,
+    InitWaitSt      = 12'b011101011010,
+    InitDescrSt     = 12'b111010110000,
+    InitDescrWaitSt = 12'b110111000011,
+    IdleSt          = 12'b000000011100,
+    IntegScrSt      = 12'b001111101011,
+    IntegScrWaitSt  = 12'b000001000111,
+    IntegDigClrSt   = 12'b100110001101,
+    IntegDigSt      = 12'b101000110111,
+    IntegDigPadSt   = 12'b110101010100,
+    IntegDigFinSt   = 12'b101011001000,
+    IntegDigWaitSt  = 12'b010010101111,
+    CnstyReadSt     = 12'b001110000110,
+    CnstyReadWaitSt = 12'b011011011101,
+    ErrorSt         = 12'b000111110101
+  } state_e;
+
+  typedef enum logic {
+    ScrmblData,
+    OtpData
+  } data_sel_e;
+
+  typedef enum logic {
+    PartOffset,
+    DigOffset
+  } base_sel_e;
+
+  state_e state_d, state_q;
+  otp_err_e error_d, error_q;
+  data_sel_e data_sel;
+  base_sel_e base_sel;
+  access_e dout_gate_d, dout_gate_q;
+  logic [CntWidth-1:0] cnt_d, cnt_q;
+  logic cnt_en, cnt_clr;
+  logic parity_err;
+  logic buffer_reg_en;
+  logic [ScrmblBlockWidth-1:0] data_mux;
+
+  // Output partition error state.
+  assign error_o = error_q;
+
+  // This partition cannot do any write accesses, hence we tie this
+  // constantly off.
+  assign otp_wdata_o = '0;
+  assign otp_cmd_o   = OtpRead;
+
+  always_comb begin : p_fsm
+    state_d = state_q;
+
+    // Redundantly encoded lock signal for buffer regs.
+    dout_gate_d = dout_gate_q;
+
+    // OTP signals
+    otp_req_o = 1'b0;
+
+    // Scrambling mutex
+    scrmbl_mtx_req_o = 1'b0;
+
+    // Scrambling datapath
+    scrmbl_cmd_o   = LoadShadow;
+    scrmbl_sel_o   = '0;
+    scrmbl_valid_o = 1'b0;
+
+    // Counter
+    cnt_en   = 1'b0;
+    cnt_clr  = 1'b0;
+    base_sel = PartOffset;
+
+    // Buffer register
+    buffer_reg_en = 1'b0;
+    data_sel = OtpData;
+
+    // Error Register
+    error_d = error_q;
+
+    // Integrity/Consistency check responses
+    cnsty_chk_ack_o = 1'b0;
+    integ_chk_ack_o = 1'b0;
+
+    unique case (state_q)
+      ///////////////////////////////////////////////////////////////////
+      // State right after reset. Wait here until we get a an
+      // initialization request.
+      ResetSt: begin
+        if (init_req_i) begin
+          state_d = InitSt;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Initialization reads out the digest only in unbuffered
+      // partitions. Wait here until the OTP request has been granted.
+      // And then wait until the OTP word comes back.
+      InitSt: begin
+        otp_req_o = 1'b1;
+        if (otp_gnt_i) begin
+          state_d = InitWaitSt;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Wait for OTP response and write to buffer register, then go to
+      // descrambling state. In case an OTP transaction fails, latch the
+      // OTP error code and jump to a
+      // terminal error state.
+      InitWaitSt: begin
+        if (otp_rvalid_i) begin
+          buffer_reg_en = 1'b1;
+          // The only error we tolerate is an ECC soft error. However,
+          // we still signal that error via the error state output.
+          if (!(otp_err_i inside {NoErr, OtpReadCorrErr})) begin
+            state_d = ErrorSt;
+            error_d = otp_err_i;
+          end else begin
+            // Once we've read and descrambled the whole partition, we can go to integrity
+            // verification. Note that the last block is the digest value, which does not
+            // have to be descrambled.
+            if (cnt_q == NumScrmblBlocks-1) begin
+              state_d = IntegDigClrSt;
+            // Only need to descramble if this is a scrambled partition.
+            // Otherwise, we can just go back to InitSt and read the next block.
+            end else if (Info.scrambled) begin
+              state_d = InitDescrSt;
+            end else begin
+              state_d = InitSt;
+              cnt_en = 1'b1;
+            end
+            // Signal ECC soft errors, but do not go into terminal error state.
+            if (otp_err_i == OtpReadCorrErr) begin
+              error_d = otp_err_i;
+            end
+          end
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Descrambling state. This first acquires the scrambling
+      // datapath mutex. Note that once the mutex is acquired, we have
+      // exclusive access to the scrambling datapath until we release
+      // the mutex by deasserting scrmbl_mtx_req_o.
+      InitDescrSt: begin
+        scrmbl_mtx_req_o = 1'b1;
+        scrmbl_valid_o = 1'b1;
+        scrmbl_cmd_o = Decrypt;
+        scrmbl_sel_o = Info.key_idx;
+        if (scrmbl_mtx_gnt_i && scrmbl_ready_i) begin
+          state_d = InitDescrWaitSt;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Wait for the descrambled data to return. Note that we release
+      // the mutex lock upon leaving this state.
+      InitDescrWaitSt: begin
+        scrmbl_mtx_req_o = 1'b1;
+        scrmbl_sel_o = Info.key_idx;
+        data_sel = ScrmblData;
+        if (scrmbl_valid_i) begin
+          state_d = InitSt;
+          buffer_reg_en = 1'b1;
+          cnt_en = 1'b1;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Idle state. We basically wait for integrity and consistency check
+      // triggers in this state.
+      IdleSt: begin
+        if (integ_chk_req_i) begin
+          state_d = IntegDigClrSt;
+        end else if (cnsty_chk_req_i) begin
+          state_d = CnstyReadSt;
+          cnt_clr = 1'b1;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Read the digest. Wait here until the OTP request has been granted.
+      // And then wait until the OTP word comes back.
+      CnstyReadSt: begin
+        otp_req_o = 1'b1;
+        // In case this partition has a hardware digest, we only have to read
+        // and compare the digest value. In that case we select the digest offset here.
+        // Otherwise we have to read and compare the whole partition, in which case we
+        // select the partition offset, which is the default assignment of base_sel.
+        if (Info.hw_digest) begin
+          base_sel = DigOffset;
+        end
+        if (otp_gnt_i) begin
+          state_d = CnstyReadWaitSt;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Wait for OTP response and and compare the digest. In case there is
+      // a mismatch, lock down the partition and go into the terminal error
+      // state. In case an OTP transaction fails, latch the OTP error code
+      // and jump to a terminal error state.
+      CnstyReadWaitSt: begin
+        if (otp_rvalid_i) begin
+          // The only error we tolerate is an ECC soft error. However,
+          // we still signal that error via the error state output.
+          if (!(otp_err_i inside {NoErr, OtpReadCorrErr})) begin
+            state_d = ErrorSt;
+            error_d = otp_err_i;
+          end else begin
+            // Check whether we need to compare the digest or the full partition
+            // contents here.
+            if (Info.hw_digest) begin
+              // Note that we ignore this check if the digest is still blank.
+              if (digest_o == data_mux || data_mux == '0 && digest_o == '0) begin
+                state_d = IdleSt;
+                cnsty_chk_ack_o = 1'b1;
+              // Error out and lock the partition if this check fails.
+              end else begin
+                state_d = ErrorSt;
+                error_d = CnstyErr;
+              end
+            end else begin
+              // Check whether the read data corresponds with the data buffered in regs.
+              if (scrmbl_data_o == data_mux) begin
+                // Can go back to idle and acknowledge the
+                // request if this is the last block.
+                if (cnt_q == NumScrmblBlocks-1) begin
+                  state_d = IdleSt;
+                  cnsty_chk_ack_o = 1'b1;
+                // Need to go back and read out more blocks.
+                end else begin
+                  state_d = CnstyReadSt;
+                  cnt_en = 1'b1;
+                end
+              end else begin
+                state_d = ErrorSt;
+                error_d = CnstyErr;
+              end
+            end
+          end
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // First, acquire the mutex for the digest and clear the digest state.
+      IntegDigClrSt: begin
+        // Check whether this partition requires checking at all.
+        if (Info.hw_digest) begin
+          scrmbl_mtx_req_o = 1'b1;
+          scrmbl_valid_o = 1'b1;
+          cnt_clr = 1'b1;
+          // Need to reset the digest state and set it to chained
+          // mode if this partition is scrambled.
+          scrmbl_cmd_o = DigestInit;
+          if (Info.scrambled) begin
+            scrmbl_sel_o = ChainedMode;
+            if (scrmbl_mtx_gnt_i && scrmbl_ready_i) begin
+              state_d = IntegScrSt;
+            end
+          // If this partition is not scrambled, we can just directly
+          // jump to the digest state.
+          end else begin
+            scrmbl_sel_o = StandardMode;
+            if (scrmbl_mtx_gnt_i && scrmbl_ready_i) begin
+              state_d = IntegDigSt;
+            end
+          end
+        // Otherwise, if this partition is not digest protected,
+        // we can just acknowledge the request and return to idle,
+        // since there is nothing to check.
+        end else begin
+          state_d = IdleSt;
+          integ_chk_ack_o = 1'b1;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Scramble buffered data (which is held in plaintext form).
+      // This moves the previous scrambling result into the shadow reg
+      // for later use.
+      IntegScrSt: begin
+          scrmbl_mtx_req_o = 1'b1;
+          scrmbl_valid_o = 1'b1;
+          scrmbl_cmd_o = Encrypt;
+          scrmbl_sel_o = Info.key_idx;
+          if (scrmbl_ready_i) begin
+            state_d = IntegScrWaitSt;
+          end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Wait for the scrambled data to return.
+      IntegScrWaitSt: begin
+        scrmbl_mtx_req_o = 1'b1;
+        scrmbl_sel_o = Info.key_idx;
+        if (scrmbl_valid_i) begin
+          state_d = IntegDigSt;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Push the word read into the scrambling datapath. The last
+      // block is repeated in case the number blocks in this partition
+      // is odd.
+      IntegDigSt: begin
+        scrmbl_mtx_req_o = 1'b1;
+        scrmbl_valid_o = 1'b1;
+        if (scrmbl_ready_i) begin
+          cnt_en = 1'b1;
+          // No need to digest the digest value itself
+          if (cnt_q == NumScrmblBlocks-2) begin
+            // Note that the digest operates on 128bit blocks since the data is fed in via the
+            // PRESENT key input. Therefore, we only trigger a digest update on every second
+            // 64bit block that is pushed into the scrambling datapath.
+            if (cnt_q[0]) begin
+              scrmbl_cmd_o = Digest;
+              state_d = IntegDigFinSt;
+            end else begin
+              state_d = IntegDigPadSt;
+            end
+          end else begin
+            // Trigger digest round in case this is the second block in a row.
+            if (cnt_q[0]) begin
+              scrmbl_cmd_o = Digest;
+            end
+            // Go back and scramble the next data block if this is
+            // a scrambled partition. Otherwise just stay here.
+            if (Info.scrambled) begin
+              state_d = IntegScrSt;
+            end
+          end
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Padding state. When we get here, we've copied the last encryption
+      // result into the shadow register such that we've effectively
+      // repeated the last block twice in order to pad the data to 128bit.
+      IntegDigPadSt: begin
+        scrmbl_mtx_req_o = 1'b1;
+        scrmbl_valid_o = 1'b1;
+        scrmbl_cmd_o = Digest;
+        if (scrmbl_ready_i) begin
+          state_d = IntegDigFinSt;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Trigger digest finalization and go wait for the result.
+      IntegDigFinSt: begin
+        scrmbl_mtx_req_o = 1'b1;
+        scrmbl_valid_o = 1'b1;
+        scrmbl_cmd_o = DigestFinalize;
+        if (scrmbl_ready_i) begin
+          state_d = IntegDigWaitSt;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Wait for the digest to return, and double check whether the digest
+      // matches. If yes, unlock the partition. Otherwise, go into the terminal
+      // error state, where the partition will be locked down.
+      IntegDigWaitSt: begin
+        scrmbl_mtx_req_o = 1'b1;
+        data_sel = ScrmblData;
+        if (scrmbl_valid_i) begin
+          // This is the only way the buffer regs can get unlocked.
+          // Note that we ignore this check if the digest is still blank.
+          if (digest_o == data_mux || digest_o == '0) begin
+            state_d = IdleSt;
+            // If the partition is still locked, this is the first integrity check after
+            // initialization. This is the only way the buffer regs can get unlocked.
+            if (dout_gate_q != Unlocked) begin
+              dout_gate_d = Unlocked;
+            // Otherwise, this integrity check has requested by the LFSR timer, and we have
+            // to acknowledge its completion.
+            end else begin
+              integ_chk_ack_o = 1'b1;
+            end
+          // Error out and lock the partition if this check fails.
+          end else begin
+            state_d = ErrorSt;
+            error_d = IntegErr;
+          end
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Terminal Error State. This locks access to the partition.
+      // Make sure the partition signals an error state if no error
+      // code has been latched so far, and lock the buffer regs down.
+      ErrorSt: begin
+        dout_gate_d = Locked;
+        if (!error_q) begin
+          error_d = FsmErr;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // We should never get here. If we do (e.g. via a malicious
+      // glitch), error out immediately.
+      default: begin
+        state_d = ErrorSt;
+      end
+      ///////////////////////////////////////////////////////////////////
+    endcase // state_q
+
+    if (state_q != ErrorSt) begin
+      // Unconditionally jump into the termninal error state in case of
+      // a parity error or escalation, and lock access to the partition down.
+      if (parity_err) begin
+        state_d = ErrorSt;
+        error_d = ParityErr;
+      end
+      if (escalate_en_i != Off) begin
+        state_d = ErrorSt;
+        error_d = EscErr;
+      end
+    end
+  end
+
+  ////////////////////////////
+  // Address Calc and Muxes //
+  ////////////////////////////
+
+  // Address counter - this is only used for computing a digest, hence the increment is
+  // fixed to 8 byte.
+  assign cnt_d = (cnt_clr) ? '0           :
+                 (cnt_en)  ? cnt_q + 1'b1 : cnt_q;
+
+  logic [OtpByteAddrWidth-1:0] addr_base;
+  assign addr_base = (base_sel == DigOffset) ? DigestOffset : Info.offset;
+
+  // Note that OTP works on halfword (16bit) addresses, hence need to
+  // shift the addresses appropriately.
+  logic [OtpByteAddrWidth-1:0] addr_calc;
+  assign addr_calc = OtpByteAddrWidth'({cnt_q, {$clog2(ScrmblBlockWidth/8){1'b0}}}) + addr_base;
+  assign otp_addr_o = addr_calc >> OtpAddrShift;
+
+  // Always transfer 64bit blocks.
+  assign otp_size_o = OtpSizeWidth'(unsigned'(ScrmblBlockWidth / OtpWidth) - 1);
+
+  assign scrmbl_data_o = data_o[cnt_q << $clog2(ScrmblBlockWidth) +: ScrmblBlockWidth];
+
+  assign data_mux = (data_sel == ScrmblData) ? scrmbl_data_i : otp_rdata_i;
+
+  /////////////////
+  // Buffer Regs //
+  /////////////////
+
+  // TODO: need to add secure erase feature here.
+  logic [Info.size*8-1:0] data;
+  otp_ctrl_parity_reg #(
+    .Width ( ScrmblBlockWidth ),
+    .Depth ( NumScrmblBlocks  )
+  ) u_otp_ctrl_parity_reg (
+    .clk_i,
+    .rst_ni,
+    .wren_i       ( buffer_reg_en ),
+    .addr_i       ( cnt_q         ),
+    .wdata_i      ( data_mux      ),
+    .data_o       ( data          ),
+    .parity_err_o ( parity_err    )
+  );
+
+  // Hardware output gating.
+  // Note that this is decoupled from the DAI access rules further below.
+  // TODO: This may need a data-specific default.
+  assign data_o = (dout_gate_q == Unlocked) ? data : '0;
+  // The digest does not have to be gated.
+  assign digest_o = data[$high(data_o) -: ScrmblBlockWidth];
+  // We have successfully initialized the partition once it has been unlocked.
+  assign init_done_o = (dout_gate_q == Unlocked);
+
+
+  ////////////////////////
+  // DAI Access Control //
+  ////////////////////////
+
+  // Aggregate all possible DAI write locks. The partition is also locked when uninitialized.
+  // Note that the locks are redundantly encoded values.
+  if (Info.write_lock) begin : gen_digest_write_lock
+    assign access_o.write_lock = ((dout_gate_q != Unlocked) ||
+                                  (access_i.write_lock != Unlocked) ||
+                                  (digest_o != '0))  ? Locked : Unlocked;
+    `ASSERT(DigestWriteLocksPartition_A, digest_o |-> access_o.write_lock == Locked)
+  end else begin : gen_no_digest_write_lock
+    assign access_o.write_lock = ((dout_gate_q != Unlocked) ||
+                                  (access_i.write_lock != Unlocked)) ? Locked : Unlocked;
+  end
+
+  // Aggregate all possible DAI read locks. The partition is also locked when uninitialized.
+  // Note that the locks are redundantly encoded 16bit values.
+  if (Info.read_lock) begin : gen_digest_read_lock
+    assign access_o.read_lock = ((dout_gate_q != Unlocked) ||
+                                 (access_i.read_lock != Unlocked) ||
+                                 (digest_o != '0)) ? Locked : Unlocked;
+    `ASSERT(DigestReadLocksPartition_A, digest_o |-> access_o.read_lock == Locked)
+  end else begin : gen_no_digest_read_lock
+    assign access_o.read_lock = ((dout_gate_q != Unlocked) ||
+                                 (access_i.read_lock != Unlocked)) ? Locked : Unlocked;
+  end
+
+  ///////////////
+  // Registers //
+  ///////////////
+
+  always_ff @(posedge clk_i or negedge rst_ni) begin : p_regs
+    if (!rst_ni) begin
+      state_q     <= ResetSt;
+      error_q     <= NoErr;
+      cnt_q       <= '0;
+      dout_gate_q <= Locked;
+    end else begin
+      state_q     <= state_d;
+      error_q     <= error_d;
+      cnt_q       <= cnt_d;
+      dout_gate_q <= dout_gate_d;
+    end
+  end
+
+  ////////////////
+  // Assertions //
+  ////////////////
+
+  // Known assertions
+  `ASSERT_KNOWN(InitDoneKnown_A,  init_done_o)
+  `ASSERT_KNOWN(ErrorKnown_A,     error_o)
+  `ASSERT_KNOWN(AccessKnown_A,    access_o)
+  `ASSERT_KNOWN(DataKnown_A,      data_o)
+  `ASSERT_KNOWN(DigestKnown_A,    digest_o)
+
+  `ASSERT_KNOWN(OtpReqKnown_A,    otp_req_o)
+  `ASSERT_KNOWN(OtpCmdKnown_A,    otp_cmd_o)
+  `ASSERT_KNOWN(OtpSizeKnown_A,   otp_size_o)
+  `ASSERT_KNOWN(OtpWdataKnown_A,  otp_wdata_o)
+  `ASSERT_KNOWN(OtpAddrKnown_A,   otp_addr_o)
+
+  // Uninitialized partitions should always be locked, no matter what.
+  `ASSERT(InitWriteLocksPartition_A,
+      dout_gate_q != Unlocked
+      |->
+      access_o.write_lock == Locked)
+  `ASSERT(InitReadLocksPartition_A,
+      dout_gate_q != Unlocked
+      |->
+      access_o.read_lock == Locked)
+  // Incoming Lock propagation
+  `ASSERT(WriteLockPropagation_A,
+      access_i.write_lock != Unlocked
+      |->
+      access_o.write_lock == Locked)
+  `ASSERT(ReadLockPropagation_A,
+      access_i.read_lock != Unlocked
+      |->
+      access_o.read_lock == Locked)
+  // Parity error
+  `ASSERT(ParityErrorState_A,
+      parity_err
+      |=>
+      state_q == ErrorSt)
+  // OTP error response
+  `ASSERT(OtpErrorState_A,
+      state_q inside {InitWaitSt, CnstyReadWaitSt} && otp_rvalid_i &&
+      !(otp_err_i inside {NoErr, OtpReadCorrErr}) && !parity_err
+      |=>
+      state_q == ErrorSt && error_o == $past(otp_err_i))
+
+endmodule : otp_ctrl_part_buf
diff --git a/hw/ip/otp_ctrl/rtl/otp_ctrl_part_unbuf.sv b/hw/ip/otp_ctrl/rtl/otp_ctrl_part_unbuf.sv
new file mode 100644
index 0000000..dde86e4
--- /dev/null
+++ b/hw/ip/otp_ctrl/rtl/otp_ctrl_part_unbuf.sv
@@ -0,0 +1,418 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// Unbuffered partition for OTP controller.
+//
+
+`include "prim_assert.sv"
+
+module otp_ctrl_part_unbuf
+  import otp_ctrl_pkg::*;
+  import otp_ctrl_reg_pkg::*;
+#(
+  // Partition information.
+  parameter part_info_t Info
+) (
+  input                               clk_i,
+  input                               rst_ni,
+  // Pulse to start partition initialisation (required once per power cycle).
+  input                               init_req_i,
+  output logic                        init_done_o,
+  // Escalation input. This moves the FSM into a terminal state and locks down
+  // the partition.
+  input  lc_tx_t                      escalate_en_i,
+  // Output error state of partition, to be consumed by OTP error/alert logic.
+  // Note that most errors are not recoverable and move the partition FSM into
+  // a terminal error state.
+  output otp_err_e                    error_o,
+  // Access/lock status
+  input  part_access_t                access_i, // runtime lock from CSRs
+  output part_access_t                access_o,
+  // Buffered 64bit digest output.
+  output logic [ScrmblBlockWidth-1:0] digest_o,
+  // Bus Interface (device) for window
+  input  tlul_pkg::tl_h2d_t           tl_i,
+  output tlul_pkg::tl_d2h_t           tl_o,
+  // OTP interface
+  output logic                        otp_req_o,
+  output prim_otp_cmd_e               otp_cmd_o,
+  output logic [OtpSizeWidth-1:0]     otp_size_o,
+  output logic [OtpIfWidth-1:0]       otp_wdata_o,
+  output logic [OtpAddrWidth-1:0]     otp_addr_o,
+  input                               otp_gnt_i,
+  input                               otp_rvalid_i,
+  input  [ScrmblBlockWidth-1:0]       otp_rdata_i,
+  input  otp_err_e                    otp_err_i
+);
+
+  ////////////////////////
+  // Integration Checks //
+  ////////////////////////
+
+  import prim_util_pkg::vbits;
+
+  localparam int PartAddrWidth = prim_util_pkg::vbits(Info.size/4);
+  localparam int DigestOffset = (Info.offset + (Info.size - (ScrmblBlockWidth/8)));
+
+  // Integration checks for parameters.
+  `ASSERT_INIT(OffsetMustBeBlockAligned_A, Info.offset % ScrmblBlockWidth/8 == 0)
+  `ASSERT_INIT(SizeMustBeBlockAligned_A, Info.size % ScrmblBlockWidth/8 == 0)
+
+  ///////////////////////
+  // OTP Partition FSM //
+  ///////////////////////
+
+  // Encoding generated with ./sparse-fsm-encode -d 5 -m 7 -n 10
+  // Hamming distance histogram:
+  //
+  // 0: --
+  // 1: --
+  // 2: --
+  // 3: --
+  // 4: --
+  // 5: |||||||||||||||||||| (42.86%)
+  // 6: |||||||||||||||||||| (42.86%)
+  // 7: |||||| (14.29%)
+  // 8: --
+  // 9: --
+  // 10: --
+  //
+  // Minimum Hamming distance: 5
+  // Maximum Hamming distance: 7
+  //
+  typedef enum logic [9:0] {
+    ResetSt    = 10'b1000011000,
+    InitSt     = 10'b1001110011,
+    InitWaitSt = 10'b0100000111,
+    IdleSt     = 10'b1101100100,
+    ReadSt     = 10'b1110101011,
+    ReadWaitSt = 10'b0011011101,
+    ErrorSt    = 10'b0111010010
+  } state_e;
+
+  typedef enum logic {
+    DigestAddr = 1'b0,
+    DataAddr = 1'b1
+  } addr_sel_e;
+
+  state_e state_d, state_q;
+  addr_sel_e otp_addr_sel;
+  otp_err_e error_d, error_q;
+
+  logic digest_reg_en;
+  logic tlul_req, tlul_gnt, tlul_rvalid;
+  logic [PartAddrWidth-1:0] tlul_addr_d, tlul_addr_q;
+  logic [1:0]  tlul_rerror;
+  logic parity_err;
+
+  // Output partition error state.
+  assign error_o = error_q;
+
+  // This partition cannot do any write accesses, hence we tie this
+  // constantly off.
+  assign otp_wdata_o = '0;
+  assign otp_cmd_o   = OtpRead;
+
+  `ASSERT_KNOWN(FsmStateKnown_A, state_q)
+  always_comb begin : p_fsm
+    // Default assignments
+    state_d = state_q;
+
+    // Response to init request
+    init_done_o = 1'b0;
+
+    // OTP signals
+    otp_req_o   = 1'b0;
+    otp_addr_sel = DigestAddr;
+
+    // TL-UL signals
+    tlul_gnt      = 1'b0;
+    tlul_rvalid   = 1'b0;
+    tlul_rerror   = '0;
+
+    // Enable for buffered digest register
+    digest_reg_en = 1'b0;
+
+    // Error Register
+    error_d = error_q;
+
+    unique case (state_q)
+      ///////////////////////////////////////////////////////////////////
+      // State right after reset. Wait here until we get a an
+      // initialization request.
+      ResetSt: begin
+        if (init_req_i) begin
+          state_d = InitSt;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Initialization reads out the digest only in unbuffered
+      // partitions. Wait here until the OTP request has been granted.
+      // And then wait until the OTP word comes back.
+      InitSt: begin
+        otp_req_o = 1'b1;
+        if (otp_gnt_i) begin
+          state_d = InitWaitSt;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Wait for OTP response and write to digest buffer register. In
+      // case an OTP transaction fails, latch the  OTP error code and
+      // jump to a terminal error state.
+      InitWaitSt: begin
+        if (otp_rvalid_i) begin
+          digest_reg_en = 1'b1;
+          // The only error we tolerate is an ECC soft error. However,
+          // we still signal that error via the error state output.
+          if (!(otp_err_i inside {NoErr, OtpReadCorrErr})) begin
+            state_d = ErrorSt;
+            error_d = otp_err_i;
+          end else begin
+            state_d = IdleSt;
+            // Signal ECC soft errors, but do not go into terminal error state.
+            if (otp_err_i == OtpReadCorrErr) begin
+              error_d = otp_err_i;
+            end
+          end
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Wait for TL-UL requests coming in.
+      // Then latch address and go to readout state.
+      IdleSt: begin
+        init_done_o = 1'b1;
+        if (tlul_req) begin
+          error_d = NoErr; // clear recoverable soft errors.
+          state_d = ReadSt;
+          tlul_gnt = 1'b1;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // If the address is out of bounds, or if the partition is
+      // locked, signal back a bus error. Note that such an error does
+      // not cause the partition to go into error state. Otherwise if
+      // these checks pass, an OTP word is requested.
+      ReadSt: begin
+        init_done_o = 1'b1;
+        if (OtpByteAddrWidth'({tlul_addr_q, 2'b00} < Info.size) &&
+            access_o.read_lock == Unlocked) begin
+          otp_req_o = 1'b1;
+          otp_addr_sel = DataAddr;
+          if (otp_gnt_i) begin
+            state_d = ReadWaitSt;
+          end
+        end else begin
+          state_d = IdleSt;
+          error_d = AccessErr; // Signal this error, but do not go into terminal error state.
+          tlul_rvalid = 1'b1;
+          tlul_rerror = 2'b11; // This causes the TL-UL adapter to return a bus error.
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Wait for OTP response and and release the TL-UL response. In
+      // case an OTP transaction fails, latch the OTP error code,
+      // signal a TL-Ul bus error and jump to a terminal error state.
+      ReadWaitSt: begin
+        init_done_o = 1'b1;
+        if (otp_rvalid_i) begin
+          // Check OTP return code.
+          if ((!(otp_err_i inside {NoErr, OtpReadCorrErr}))) begin
+            state_d = ErrorSt;
+            error_d = otp_err_i;
+            tlul_rvalid = 1'b1;
+            // This causes the TL-UL adapter to return a bus error.
+            tlul_rerror = 2'b11;
+          end else begin
+            state_d = IdleSt;
+            // Signal soft ECC errors, but do not go into terminal error state.
+            if (otp_err_i == OtpReadCorrErr) begin
+              error_d = otp_err_i;
+            end
+          end
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Terminal Error State. This locks access to the partition.
+      // Make sure the partition signals an error state if no error
+      // code has been latched so far.
+      ErrorSt: begin
+        if (!error_q) begin
+          error_d = FsmErr;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // We should never get here. If we do (e.g. via a malicious
+      // glitch), error out immediately.
+      default: begin
+        state_d = ErrorSt;
+      end
+      ///////////////////////////////////////////////////////////////////
+    endcase // state_q
+
+    if (state_q != ErrorSt) begin
+      // Unconditionally jump into the termninal error state in case of
+      // a parity error or escalation, and lock access to the partition down.
+      if (parity_err) begin
+        state_d = ErrorSt;
+        error_d = ParityErr;
+      end
+      if (escalate_en_i != Off) begin
+        state_d = ErrorSt;
+        error_d = EscErr;
+      end
+    end
+  end
+
+  ///////////////////
+  // TL-UL Adapter //
+  ///////////////////
+
+  tlul_adapter_sram #(
+    .SramAw      ( PartAddrWidth ),
+    .SramDw      ( 32            ),
+    .Outstanding ( 1             ),
+    .ByteAccess  ( 0             ),
+    .ErrOnWrite  ( 1             ) // No write accesses allowed here.
+  ) u_tlul_adapter_sram (
+    .clk_i,
+    .rst_ni,
+    .tl_i,
+    .tl_o,
+    .req_o    ( tlul_req          ),
+    .gnt_i    ( tlul_gnt          ),
+    .we_o     (                   ), // unused
+    .addr_o   ( tlul_addr_d       ),
+    .wdata_o  (                   ), // unused
+    .wmask_o  (                   ), // unused
+    .rdata_i  ( otp_rdata_i[31:0] ),
+    .rvalid_i ( tlul_rvalid       ),
+    .rerror_i ( tlul_rerror       )
+  );
+
+  // Note that OTP works on halfword (16bit) addresses, hence need to
+  // shift the addresses appropriately.
+  assign otp_addr_o = (otp_addr_sel == DigestAddr) ? (DigestOffset >> OtpAddrShift) :
+                                                     {tlul_addr_q, 2'b00} >> OtpAddrShift;
+  // Request 32bit except in case of the digest.
+  assign otp_size_o = (otp_addr_sel == DigestAddr) ?
+                      OtpSizeWidth'(unsigned'(32 / OtpWidth - 1)) :
+                      OtpSizeWidth'(unsigned'(ScrmblBlockWidth / OtpWidth - 1));
+
+  ////////////////
+  // Digest Reg //
+  ////////////////
+
+  otp_ctrl_parity_reg #(
+    .Width ( ScrmblBlockWidth ),
+    .Depth ( 1                )
+  ) u_otp_ctrl_parity_reg (
+    .clk_i,
+    .rst_ni,
+    .wren_i        ( digest_reg_en ),
+    .addr_i        ( '0            ),
+    .wdata_i       ( otp_rdata_i   ),
+    .data_o        ( digest_o      ),
+    .parity_err_o  ( parity_err    )
+  );
+
+  ////////////////////////
+  // DAI Access Control //
+  ////////////////////////
+
+  // Aggregate all possible DAI write locks. The partition is also locked when uninitialized.
+  // Note that the locks are redundantly encoded values.
+  if (Info.write_lock) begin : gen_digest_write_lock
+    assign access_o.write_lock =
+        (~init_done_o || access_i.write_lock != Unlocked || digest_o != '0) ? Locked : Unlocked;
+
+    `ASSERT(DigestWriteLocksPartition_A, digest_o |-> access_o.write_lock == Locked)
+
+  end else begin : gen_no_digest_write_lock
+      assign access_o.write_lock =
+          (~init_done_o || access_i.write_lock != Unlocked) ? Locked : Unlocked;
+  end
+
+  // Aggregate all possible DAI read locks. The partition is also locked when uninitialized.
+  // Note that the locks are redundantly encoded 16bit values.
+  if (Info.read_lock) begin : gen_digest_read_lock
+    assign access_o.read_lock =
+        (~init_done_o || access_i.read_lock != Unlocked || digest_o != '0) ? Locked : Unlocked;
+
+    `ASSERT(DigestReadLocksPartition_A, digest_o |-> access_o.read_lock == Locked)
+
+  end else begin : gen_no_digest_read_lock
+      assign access_o.read_lock =
+          (~init_done_o || access_i.read_lock != Unlocked) ? Locked : Unlocked;
+  end
+
+  ///////////////
+  // Registers //
+  ///////////////
+
+  always_ff @(posedge clk_i or negedge rst_ni) begin : p_regs
+    if (!rst_ni) begin
+      state_q       <= ResetSt;
+      error_q       <= NoErr;
+      tlul_addr_q   <= '0;
+    end else begin
+      state_q       <= state_d;
+      error_q       <= error_d;
+      if (tlul_gnt) begin
+        tlul_addr_q <= tlul_addr_d;
+      end
+    end
+  end
+
+  ////////////////
+  // Assertions //
+  ////////////////
+
+  // Known assertions
+  `ASSERT_KNOWN(InitDoneKnown_A,  init_done_o)
+  `ASSERT_KNOWN(ErrorKnown_A,     error_o)
+  `ASSERT_KNOWN(AccessKnown_A,    access_o)
+  `ASSERT_KNOWN(DigestKnown_A,    digest_o)
+  `ASSERT_KNOWN(TlKnown_A,        tl_o)
+  `ASSERT_KNOWN(OtpReqKnown_A,    otp_req_o)
+  `ASSERT_KNOWN(OtpCmdKnown_A,    otp_cmd_o)
+  `ASSERT_KNOWN(OtpSizeKnown_A,   otp_size_o)
+  `ASSERT_KNOWN(OtpWdataKnown_A,  otp_wdata_o)
+  `ASSERT_KNOWN(OtpAddrKnown_A,   otp_addr_o)
+
+  // Uninitialized partitions should always be locked, no matter what.
+  `ASSERT(InitWriteLocksPartition_A,
+      ~init_done_o
+      |->
+      access_o.write_lock == Locked)
+  `ASSERT(InitReadLocksPartition_A,
+      ~init_done_o
+      |->
+      access_o.read_lock == Locked)
+  // Incoming Lock propagation
+  `ASSERT(WriteLockPropagation_A,
+      access_i.write_lock != Unlocked
+      |->
+      access_o.write_lock == Locked)
+  `ASSERT(ReadLockPropagation_A,
+      access_i.read_lock != Unlocked
+      |->
+      access_o.read_lock == Locked)
+  // If the partition is read locked, the TL-UL access must error out
+  `ASSERT(TlulReadOnReadLock_A,
+      tlul_req && tlul_gnt ##1 access_o.read_lock != Unlocked
+      |=>
+      tlul_rerror > '0 && tlul_rvalid)
+  // Parity error
+  `ASSERT(ParityErrorState_A,
+      parity_err
+      |=>
+      state_q == ErrorSt)
+  // OTP error response
+  `ASSERT(OtpErrorState_A,
+      state_q inside {InitWaitSt, ReadWaitSt} && otp_rvalid_i &&
+      !(otp_err_i inside {NoErr, OtpReadCorrErr}) && !parity_err
+      |=>
+      state_q == ErrorSt && error_o == $past(otp_err_i))
+
+endmodule : otp_ctrl_part_unbuf
diff --git a/hw/ip/otp_ctrl/rtl/otp_ctrl_pkg.sv b/hw/ip/otp_ctrl/rtl/otp_ctrl_pkg.sv
index 383f4c3..8e5c685 100644
--- a/hw/ip/otp_ctrl/rtl/otp_ctrl_pkg.sv
+++ b/hw/ip/otp_ctrl/rtl/otp_ctrl_pkg.sv
@@ -6,6 +6,36 @@
 package otp_ctrl_pkg;
 
   import prim_util_pkg::vbits;
+  import otp_ctrl_reg_pkg::*;
+
+  ////////////////////////
+  // General Parameters //
+  ////////////////////////
+
+  parameter int NumPart = 7;
+  parameter int NumPartWidth = vbits(NumPart);
+
+  // TODO: may need to tune this and make sure that this encoding not optimized away.
+  // Redundantly encoded and complementary values are used to for signalling to the partition
+  // controller FSMs and the DAI whether a partition is locked or not. Any other value than
+  // "Unlocked" is interpreted as "Locked" in those FSMs.
+  typedef enum logic [7:0] {
+    Unlocked = 8'h5A,
+    Locked   = 8'hA5
+  } access_e;
+
+  // Partition access type
+  typedef struct packed {
+    access_e read_lock;
+    access_e write_lock;
+  } part_access_t;
+
+  parameter int DaiCmdWidth = 3;
+  typedef enum logic [DaiCmdWidth-1:0] {
+    DaiRead   = 3'b001,
+    DaiWrite  = 3'b010,
+    DaiDigest = 3'b100
+  } dai_cmd_e;
 
   //////////////////////////////////////
   // Typedefs for OTP Macro Interface //
@@ -13,12 +43,11 @@
 
   // OTP-macro specific
   parameter int OtpWidth         = 16;
-  parameter int OtpDepth         = 1024;
+  parameter int OtpAddrWidth     = OtpByteAddrWidth - $clog2(OtpWidth/8);
+  parameter int OtpDepth         = 2**OtpAddrWidth;
   parameter int OtpCmdWidth      = 2;
   parameter int OtpSizeWidth     = 2; // Allows to transfer up to 4 native OTP words at once.
   parameter int OtpErrWidth      = 4;
-  parameter int OtpAddrWidth     = vbits(OtpDepth);
-  parameter int OtpByteAddrWidth = vbits(OtpWidth/8 * OtpDepth);
   parameter int OtpIfWidth       = 2**OtpSizeWidth*OtpWidth;
   // Number of Byte address bits to cut off in order to get the native OTP word address.
   parameter int OtpAddrShift     = OtpByteAddrWidth - OtpAddrWidth;
@@ -30,22 +59,21 @@
   } prim_otp_cmd_e;
 
   typedef enum logic [OtpErrWidth-1:0] {
-    None                 = 4'h0,
-    OtpCmdInvErr         = 4'h1,
-    OtpSizeInvErr        = 4'h2,
-    OtpInitErr           = 4'h3,
-    OtpReadErrEccCorr    = 4'h4,
-    OtpReadErrEccUncorr  = 4'h5,
-    OtpReadErrOther      = 4'h6,
-    OtpWriteErrNotBlank  = 4'h7,
-    OtpWriteErrOther     = 4'h8,
-    ParityErr            = 4'h9,
-    IntegErr             = 4'hA,
-    CnstyErr             = 4'hB,
-    FsmErr               = 4'hC,
-    CmdInvErr            = 4'hD,
-    AccessErr            = 4'hE
-    // TODO: populate with error codes
+    NoErr            = 4'h0,
+    OtpCmdInvErr     = 4'h1,
+    OtpInitErr       = 4'h2,
+    OtpReadCorrErr   = 4'h3,
+    OtpReadUncorrErr = 4'h4,
+    OtpReadErr       = 4'h5,
+    OtpWriteBlankErr = 4'h6,
+    OtpWriteErr      = 4'h7,
+    CmdInvErr        = 4'h8,
+    AccessErr        = 4'h9,
+    ParityErr        = 4'hA,
+    IntegErr         = 4'hB,
+    CnstyErr         = 4'hC,
+    FsmErr           = 4'hD,
+    EscErr           = 4'hE
   } otp_err_e;
 
   /////////////////////////////////
@@ -104,11 +132,65 @@
     ChainedMode
   } otp_digest_mode_e;
 
+  ////////////////////////
+  // Partition Metadata //
+  ////////////////////////
+
+  typedef enum logic {
+    Unbuffered,
+    Buffered
+  } part_variant_e;
+
+  typedef struct packed {
+    part_variant_e variant;
+    // Offset and size within the OTP array, in Bytes.
+    logic [OtpByteAddrWidth-1:0] offset;
+    logic [OtpByteAddrWidth-1:0] size;
+    // Key index to use for scrambling.
+    logic [ConstSelWidth-1:0] key_idx;
+    // Attributes
+    logic scrambled;  // Whether the partition is scrambled
+    logic hw_digest;  // Whether the partition has a hardware digest
+    logic write_lock; // Whether the partition is write lockable (via digest)
+    logic read_lock;  // Whether the partition is read lockable (via digest)
+  } part_info_t;
+
+  // TODO: need to parse this somehow from an hjson
+  localparam part_info_t PartInfo [NumPart] = '{
+    // Variant    | offset | size | key_idx | scrambled | HW digest | write_lock | read_lock
+    // CREATOR_SW_CFG
+    '{Unbuffered,   32'h0,   768,      0,       1'b0,      1'b0,      1'b1,       1'b0},
+    // OWNER_SW_CFG
+    '{Unbuffered,   32'h300, 768,      0,       1'b0,      1'b0,      1'b1,       1'b0},
+    // HW_CFG
+    '{Buffered,     32'h600, 176,      0,       1'b0,      1'b1,      1'b1,       1'b0},
+    // SECRET0
+    '{Buffered,     32'h6B0, 40,       0,       1'b1,      1'b1,      1'b1,       1'b1},
+    // SECRET1
+    '{Buffered,     32'h6D8, 88,       1,       1'b1,      1'b1,      1'b1,       1'b1},
+    // SECRET2
+    '{Buffered,     32'h730, 120,      2,       1'b1,      1'b1,      1'b1,       1'b1},
+    // LIFE_CYCLE
+    '{Buffered,     32'h7A8, 88,       0,       1'b0,      1'b0,      1'b0,       1'b0}
+  };
+
+  parameter int CreatorSwCfgIdx = 0;
+  parameter int OwnerSwCfgIdx   = 1;
+  parameter int HwCfgIdx        = 2;
+  parameter int Secret0Idx      = 3;
+  parameter int Secret1Idx      = 4;
+  parameter int Secret2Idx      = 5;
+  parameter int LifeCycleIdx    = 6;
+  // These are not "real partitions", but in terms of implementation it is convenient to
+  // add these at the end of certain arrays.
+  parameter int DaiIdx = 7;
+  parameter int LciIdx = 8;
+
   ///////////////////////////////
   // Typedefs for LC Interface //
   ///////////////////////////////
 
-  // TODO: move to lc_ctrl_pkg
+  // TODO: update this encoding and move to lc_ctrl_pkg
   typedef enum logic [7:0] {
     Value0 = 8'h 00,
     Value1 = 8'h 6D,
@@ -207,21 +289,15 @@
   ////////////////////////////////
 
   typedef struct packed {
-    logic  init;
+    logic init;
   } pwr_otp_init_req_t;
 
   typedef struct packed {
-    logic  done;
+    logic done;
   } pwr_otp_init_rsp_t;
 
-  typedef enum logic [1:0] {
-    Idle    = 2'b00,
-    Reading = 2'b01,
-    Writing = 2'b10
-  } otp_pwr_state_e;
-
   typedef struct packed {
-    otp_pwr_state_e  state;
+    logic idle;
   } otp_pwr_state_t;
 
 endpackage : otp_ctrl_pkg
diff --git a/hw/ip/otp_ctrl/rtl/otp_ctrl_scrmbl.sv b/hw/ip/otp_ctrl/rtl/otp_ctrl_scrmbl.sv
index 19e77d3..39f270d 100644
--- a/hw/ip/otp_ctrl/rtl/otp_ctrl_scrmbl.sv
+++ b/hw/ip/otp_ctrl/rtl/otp_ctrl_scrmbl.sv
@@ -84,9 +84,15 @@
   // Decryption Key LUT //
   ////////////////////////
 
+  logic [NumPresentRounds-1:0][4:0] present_round_lut;
   logic [NumScrmblKeys-1:0][ScrmblKeyWidth-1:0] otp_dec_key_lut;
 
   // This pre-calculates the inverse scrambling keys at elab time.
+  for (genvar k = 0; k < NumPresentRounds; k++) begin : gen_round_lut
+    assign present_round_lut[k] = 5'(unsigned'(k+1));
+  end
+  `ASSERT_INIT(NumMaxPresentRounds_A, NumPresentRounds <= 31)
+
   always_comb begin : p_inv_keys
     for (int k = 0; k < NumScrmblKeys; k++) begin
       // Initialize with encryption key
@@ -94,11 +100,12 @@
       for (int j = 0; j < NumPresentRounds; j++) begin
         // Due to the PRESENT key schedule, we have to step the key schedule function by
         // NumPresentRounds forwards to get the decryption key.
-        otp_dec_key_lut[k] = prim_cipher_pkg::present_update_key128(otp_dec_key_lut[k], 5'(j + 1));
+        otp_dec_key_lut[k] = prim_cipher_pkg::present_update_key128(otp_dec_key_lut[k],
+                                                                    present_round_lut[j]);
       end
     end
   end
-  `ASSERT_KNOWN(DecKeyLutKnown_A, otp_dec_key_lut);
+  `ASSERT_KNOWN(DecKeyLutKnown_A, otp_dec_key_lut)
 
   //////////////
   // Datapath //
@@ -129,7 +136,7 @@
   data_state_sel_e  data_state_sel;
   key_state_sel_e   key_state_sel;
   logic data_state_en, data_shadow_copy, data_shadow_load, digest_state_en, key_state_en;
-  logic sel_d, sel_q;
+  logic [ConstSelWidth-1:0] sel_d, sel_q;
   otp_digest_mode_e digest_mode_d, digest_mode_q;
 
   assign otp_enc_key_mux      = (sel_d < NumScrmblKeys) ? OtpKey[sel_d]          : '0;
@@ -151,7 +158,7 @@
 
   assign key_state_d     = (key_state_sel == SelDecKeyOut)      ? dec_key_out          :
                            (key_state_sel == SelEncKeyOut)      ? enc_key_out          :
-                           (key_state_sel == SelDecKeyInit)     ? otp_dec_key_lut      :
+                           (key_state_sel == SelDecKeyInit)     ? otp_dec_key_mux      :
                            (key_state_sel == SelEncKeyInit)     ? otp_enc_key_mux      :
                            (key_state_sel == SelDigestConst)    ? otp_digest_const_mux :
                            (key_state_sel == SelDigestChained)  ? {data_state_q, data_shadow_q} :
@@ -165,11 +172,27 @@
   // FSM //
   /////////
 
-  typedef enum logic [1:0] {
-    IdleSt,
-    DecryptSt,
-    EncryptSt,
-    DigestSt
+  // Encoding generated with ./sparse-fsm-encode -d 5 -m 4 -n 8
+  // Hamming distance histogram:
+  //
+  // 0: --
+  // 1: --
+  // 2: --
+  // 3: --
+  // 4: --
+  // 5: |||||||||||||||||||| (66.67%)
+  // 6: |||||||||| (33.33%)
+  // 7: --
+  // 8: --
+  //
+  // Minimum Hamming distance: 5
+  // Maximum Hamming distance: 6
+  //
+  typedef enum logic [7:0] {
+    IdleSt    = 8'b11100001,
+    DecryptSt = 8'b01011010,
+    EncryptSt = 8'b10010100,
+    DigestSt  = 8'b00101111
   } state_e;
 
   localparam int CntWidth = $clog2(NumPresentRounds+1);
@@ -218,12 +241,14 @@
               key_state_sel = SelDecKeyInit;
               data_state_en = 1'b1;
               key_state_en  = 1'b1;
+              sel_d         = sel_i;
             end
             Encrypt: begin
               state_d       = EncryptSt;
               key_state_sel = SelEncKeyInit;
               data_state_en = 1'b1;
               key_state_en  = 1'b1;
+              sel_d         = sel_i;
             end
             LoadShadow: begin
               if (digest_mode_q == ChainedMode) begin
@@ -239,6 +264,7 @@
               data_state_en  = 1'b1;
               key_state_en   = 1'b1;
               is_first_d     = 1'b0;
+              sel_d          = sel_i;
             end
             DigestInit: begin
               digest_mode_d  = otp_digest_mode_e'(sel_i);
@@ -252,6 +278,7 @@
               key_state_en   = 1'b1;
               is_first_d     = 1'b1;
               digest_mode_d  = StandardMode;
+              sel_d          = sel_i;
             end
             default: ; // ignore
           endcase // cmd_i
diff --git a/hw/ip/prim_generic/rtl/prim_generic_otp.sv b/hw/ip/prim_generic/rtl/prim_generic_otp.sv
index e0141d9..c6ed031 100644
--- a/hw/ip/prim_generic/rtl/prim_generic_otp.sv
+++ b/hw/ip/prim_generic/rtl/prim_generic_otp.sv
@@ -46,15 +46,33 @@
   // Control logic //
   ///////////////////
 
-  typedef enum logic [2:0] {
-    ResetSt,
-    InitSt,
-    IdleSt,
-    ReadSt,
-    ReadWaitSt,
-    WriteCheckSt,
-    WriteWaitSt,
-    WriteSt
+  // Encoding generated with ./sparse-fsm-encode -d 5 -m 8 -n 10
+  // Hamming distance histogram:
+  //
+  // 0: --
+  // 1: --
+  // 2: --
+  // 3: --
+  // 4: --
+  // 5: |||||||||||||||||||| (53.57%)
+  // 6: ||||||||||||| (35.71%)
+  // 7: | (3.57%)
+  // 8: || (7.14%)
+  // 9: --
+  // 10: --
+  //
+  // Minimum Hamming distance: 5
+  // Maximum Hamming distance: 8
+  //
+  typedef enum logic [9:0] {
+    ResetSt      = 10'b1100000011,
+    InitSt       = 10'b1100110100,
+    IdleSt       = 10'b1010111010,
+    ReadSt       = 10'b0011100000,
+    ReadWaitSt   = 10'b1001011111,
+    WriteCheckSt = 10'b0111010101,
+    WriteWaitSt  = 10'b0000001100,
+    WriteSt      = 10'b0110101111
   } state_e;
 
   state_e state_d, state_q;
@@ -80,8 +98,8 @@
     // Default
     state_d = state_q;
     ready_o = 1'b0;
-    valid_d = 1'b1;
-    err_d   = otp_ctrl_pkg::None;
+    valid_d = 1'b0;
+    err_d   = otp_ctrl_pkg::NoErr;
     req     = 1'b0;
     wren    = 1'b0;
     cnt_clr = 1'b0;
@@ -92,14 +110,13 @@
       ResetSt: begin
         ready_o = 1'b1;
         if (valid_i) begin
-          unique case (cmd_i)
-            otp_ctrl_pkg::OtpInit:  state_d = InitSt;
-            default:  begin
-              // Invalid commands get caught here
-              valid_d = 1'b1;
-              err_d = otp_ctrl_pkg::OtpCmdInvErr;
-            end
-          endcase // cmd_i
+          if (cmd_i == otp_ctrl_pkg::OtpInit) begin
+            state_d = InitSt;
+          end else begin
+            // Invalid commands get caught here
+            valid_d = 1'b1;
+            err_d = otp_ctrl_pkg::OtpCmdInvErr;
+          end
         end
       end
       // Wait for some time until the OTP macro is ready.
@@ -113,7 +130,7 @@
         ready_o = 1'b1;
         if (valid_i) begin
           cnt_clr = 1'b1;
-          err_d = otp_ctrl_pkg::None;
+          err_d = otp_ctrl_pkg::NoErr;
           unique case (cmd_i)
             otp_ctrl_pkg::OtpRead:  state_d = ReadSt;
             otp_ctrl_pkg::OtpWrite: state_d = WriteCheckSt;
@@ -139,7 +156,7 @@
           if (rerror[1]) begin
             state_d = IdleSt;
             valid_d = 1'b1;
-            err_d = otp_ctrl_pkg::OtpReadErrEccUncorr;
+            err_d = otp_ctrl_pkg::OtpReadUncorrErr;
           end else begin
             if (cnt_q == size_q) begin
               state_d = IdleSt;
@@ -149,7 +166,7 @@
             end
             // Correctable error, carry on but signal back.
             if (rerror[0]) begin
-              err_d = otp_ctrl_pkg::OtpReadErrEccCorr;
+              err_d = otp_ctrl_pkg::OtpReadCorrErr;
             end
           end
         end
@@ -169,7 +186,7 @@
           if (rerror[1] || rdata_d != '0) begin
             state_d = IdleSt;
             valid_d = 1'b1;
-            err_d = otp_ctrl_pkg::OtpWriteErrNotBlank;
+            err_d = otp_ctrl_pkg::OtpWriteBlankErr;
           end else begin
             if (cnt_q == size_q) begin
               cnt_clr = 1'b1;