[lc_ctrl] Add first cut implementation

Signed-off-by: Michael Schaffner <msf@opentitan.org>
diff --git a/hw/ip/lc_ctrl/lc_ctrl.core b/hw/ip/lc_ctrl/lc_ctrl.core
new file mode 100644
index 0000000..47193c8
--- /dev/null
+++ b/hw/ip/lc_ctrl/lc_ctrl.core
@@ -0,0 +1,71 @@
+CAPI=2:
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+name: "lowrisc:ip:lc_ctrl:0.1"
+description: "LC Controller"
+
+filesets:
+  files_rtl:
+    depend:
+      - lowrisc:prim:all
+      - lowrisc:prim:lc_sync
+      - lowrisc:ip:lc_ctrl_pkg
+      - lowrisc:ip:tlul
+      - lowrisc:ip:pwrmgr_pkg
+      - lowrisc:ip:otp_ctrl_pkg
+    files:
+      - rtl/lc_ctrl_reg_top.sv
+      - rtl/lc_ctrl_state_decode.sv
+      - rtl/lc_ctrl_state_transition.sv
+      - rtl/lc_ctrl_signal_decode.sv
+      - rtl/lc_ctrl_fsm.sv
+      - rtl/lc_ctrl.sv
+    file_type: systemVerilogSource
+
+  files_verilator_waiver:
+    depend:
+      # common waivers
+      - lowrisc:lint:common
+      - lowrisc:lint:comportable
+    files:
+      - lint/lc_ctrl.vlt
+    file_type: vlt
+
+  files_ascentlint_waiver:
+    depend:
+      # common waivers
+      - lowrisc:lint:common
+      - lowrisc:lint:comportable
+    files:
+      - lint/lc_ctrl.waiver
+    file_type: waiver
+
+parameters:
+  SYNTHESIS:
+    datatype: bool
+    paramtype: vlogdefine
+
+
+targets:
+  default: &default_target
+    filesets:
+      - tool_verilator  ? (files_verilator_waiver)
+      - tool_ascentlint ? (files_ascentlint_waiver)
+      - files_rtl
+    toplevel: lc_ctrl
+
+  lint:
+    <<: *default_target
+    default_tool: verilator
+    parameters:
+      - SYNTHESIS=true
+    tools:
+      ascentlint:
+        ascentlint_options:
+          - "-wait_license"
+          - "-stop_on_error"
+      verilator:
+        mode: lint-only
+        verilator_options:
+          - "-Wall"
diff --git a/hw/ip/lc_ctrl/lc_ctrl_pkg.core b/hw/ip/lc_ctrl/lc_ctrl_pkg.core
index 09d879f..281aa3e 100644
--- a/hw/ip/lc_ctrl/lc_ctrl_pkg.core
+++ b/hw/ip/lc_ctrl/lc_ctrl_pkg.core
@@ -8,7 +8,6 @@
   files_rtl:
     depend:
       - lowrisc:tlul:headers
-
     files:
       - rtl/lc_ctrl_reg_pkg.sv
       - rtl/lc_ctrl_pkg.sv
diff --git a/hw/ip/lc_ctrl/lint/lc_ctrl.vlt b/hw/ip/lc_ctrl/lint/lc_ctrl.vlt
new file mode 100644
index 0000000..ddeca7e
--- /dev/null
+++ b/hw/ip/lc_ctrl/lint/lc_ctrl.vlt
@@ -0,0 +1,6 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// waiver file for LC controller
+
diff --git a/hw/ip/lc_ctrl/lint/lc_ctrl.waiver b/hw/ip/lc_ctrl/lint/lc_ctrl.waiver
new file mode 100644
index 0000000..cbd0a96
--- /dev/null
+++ b/hw/ip/lc_ctrl/lint/lc_ctrl.waiver
@@ -0,0 +1,6 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+#
+# waiver file for LC controller
+
diff --git a/hw/ip/lc_ctrl/rtl/lc_ctrl.sv b/hw/ip/lc_ctrl/rtl/lc_ctrl.sv
new file mode 100644
index 0000000..3383cba
--- /dev/null
+++ b/hw/ip/lc_ctrl/rtl/lc_ctrl.sv
@@ -0,0 +1,446 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// Life cycle controller top.
+//
+
+`include "prim_assert.sv"
+
+module lc_ctrl
+  import lc_ctrl_pkg::*;
+  import lc_ctrl_reg_pkg::*;
+#(
+  // Enable asynchronous transitions on alerts.
+  parameter logic [NumAlerts-1:0] AlertAsyncOn      = {NumAlerts{1'b1}}
+) (
+  input                                              clk_i,
+  input                                              rst_ni,
+  // Bus Interface (device)
+  input  tlul_pkg::tl_h2d_t                          tl_i,
+  output tlul_pkg::tl_d2h_t                          tl_o,
+  // Alert outputs.
+  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,
+  // Escalation inputs (severity 1 and 2).
+  // These need not be synchronized since the alert handler is
+  // in the same clock domain as the LC controller.
+  input  prim_esc_pkg::esc_rx_t                      esc_wipe_secrets_tx_i,
+  output prim_esc_pkg::esc_tx_t                      esc_wipe_secrets_rx_o,
+  input  prim_esc_pkg::esc_rx_t                      esc_scrap_state_tx_i,
+  output prim_esc_pkg::esc_tx_t                      esc_scrap_state_rx_o,
+  // Power manager interface (inputs are synced to lifecycle clock domain).
+  input  pwrmgr_pkg::pwr_lc_req_t                    pwr_lc_i,
+  output pwrmgr_pkg::pwr_lc_rsp_t                    pwr_lc_o,
+  // Life cycle transition command interface.
+  // No sync required since LC and OTP are in the same clock domain.
+  output otp_ctrl_pkg::lc_otp_program_req_t          lc_otp_program_o,
+  input  otp_ctrl_pkg::lc_otp_program_rsp_t          lc_otp_program_i,
+  // Life cycle hashing interface for raw unlock
+  // No sync required since LC and OTP are in the same clock domain.
+  output otp_ctrl_pkg::lc_otp_token_req_t            lc_otp_token_o,
+  input  otp_ctrl_pkg::lc_otp_token_rsp_t            lc_otp_token_i,
+  // OTP broadcast outputs
+  // No sync required since LC and OTP are in the same clock domain.
+  input  otp_ctrl_pkg::otp_lc_data_t                 otp_lc_data_i,
+  // Life cycle broadcast outputs (all of them are registered).
+  output lc_tx_t                                     lc_dft_en_o,
+  output lc_tx_t                                     lc_nvm_debug_en_o,
+  output lc_tx_t                                     lc_hw_debug_en_o,
+  output lc_tx_t                                     lc_cpu_en_o,
+  output lc_tx_t                                     lc_provision_wr_en_o,
+  output lc_tx_t                                     lc_provision_rd_en_o,
+  output lc_tx_t                                     lc_keymgr_en_o,
+  output lc_tx_t                                     lc_escalate_en_o,
+  // Request and feedback to/from clock manager and AST.
+  // The ack is synced to the lc clock domain using prim_lc_sync.
+  output lc_tx_t                                     lc_clk_byp_req_o,
+  input  lc_tx_t                                     lc_clk_byp_ack_i,
+  // Request and feedback to/from flash controller.
+  // The ack is synced to the lc clock domain using prim_lc_sync.
+  output lc_flash_rma_seed_t                         lc_flash_rma_seed_o,
+  output lc_tx_t                                     lc_flash_rma_req_o,
+  input  lc_tx_t                                     lc_flash_rma_ack_i
+);
+
+  ////////////////////////
+  // Integration Checks //
+  ////////////////////////
+
+  // Check that the CSR parameters correspond with the ones used in the design.
+  `ASSERT_INIT(DecLcStateWidthCheck_A, CsrLcStateWidth == DecLcStateWidth)
+  `ASSERT_INIT(DecLcCountWidthCheck_A, CsrLcCountWidth == DecLcCountWidth)
+  `ASSERT_INIT(DecLcIdStateWidthCheck_A, CsrLcIdStateWidth == DecLcIdStateWidth)
+  `ASSERT_INIT(NumTokenWordsCheck_A, NumTokenWords == LcTokenWidth/32)
+
+  /////////////
+  // Regfile //
+  /////////////
+
+  lc_ctrl_reg_pkg::lc_ctrl_reg2hw_t reg2hw;
+  lc_ctrl_reg_pkg::lc_ctrl_hw2reg_t hw2reg;
+
+  lc_ctrl_reg_top u_reg (
+    .clk_i,
+    .rst_ni,
+    .tl_i,
+    .tl_o,
+    .reg2hw    ( reg2hw ),
+    .hw2reg    ( hw2reg ),
+    .devmode_i ( 1'b1   )
+  );
+
+  ////////////////////
+  // Life Cycle TAP //
+  ////////////////////
+
+  tlul_pkg::tl_h2d_t tap_tl_h2d;
+  tlul_pkg::tl_d2h_t tap_tl_d2h, unused_tap_tl_d2h;
+  lc_ctrl_reg_pkg::lc_ctrl_reg2hw_t tap_reg2hw;
+  lc_ctrl_reg_pkg::lc_ctrl_hw2reg_t tap_hw2reg;
+
+  lc_ctrl_reg_top u_reg_tap (
+    .clk_i,
+    .rst_ni,
+    .tl_i      ( tap_tl_h2d ),
+    .tl_o      ( tap_tl_d2h ),
+    .reg2hw    ( tap_reg2hw ),
+    .hw2reg    ( tap_hw2reg ),
+    .devmode_i ( 1'b1       )
+  );
+
+  // TODO: implement TAP
+  assign tap_tl_h2d = '0;
+  assign unused_tap_tl_d2h = tap_tl_d2h;
+
+  ///////////////////////////////////////
+  // Transition Interface and HW Mutex //
+  ///////////////////////////////////////
+
+  // TODO: expose device ID
+  // TODO: expose other info to expose via CSRs / TAP?
+
+  // All registers are HWext
+  logic          trans_success_d, trans_success_q;
+  logic          trans_cnt_oflw_error_d, trans_cnt_oflw_error_q;
+  logic          trans_invalid_error_d, trans_invalid_error_q;
+  logic          token_invalid_error_d, token_invalid_error_q;
+  logic          flash_rma_error_d, flash_rma_error_q;
+  logic          otp_prog_error_d, otp_prog_error_q;
+  logic          state_invalid_error_d, state_invalid_error_q;
+  logic          sw_claim_transition_if_d, sw_claim_transition_if_q;
+  logic          tap_claim_transition_if_d, tap_claim_transition_if_q;
+  logic          transition_cmd;
+  lc_token_t     transition_token_d, transition_token_q;
+  dec_lc_state_e transition_target_d, transition_target_q;
+  // No need to register these.
+  dec_lc_state_e    dec_lc_state;
+  dec_lc_cnt_t      dec_lc_cnt;
+  dec_lc_id_state_e dec_lc_id_state;
+
+  logic lc_idle_d;
+
+  always_comb begin : p_csr_assign_outputs
+    hw2reg = '0;
+    hw2reg.status.ready                  = lc_idle_d;
+    hw2reg.status.transition_successful  = trans_success_q;
+    hw2reg.status.transition_count_error = trans_cnt_oflw_error_q;
+    hw2reg.status.transition_error       = trans_invalid_error_q;
+    hw2reg.status.token_error            = token_invalid_error_q;
+    hw2reg.status.flash_rma_error        = flash_rma_error_q;
+    hw2reg.status.otp_error              = otp_prog_error_q;
+    hw2reg.status.state_error            = state_invalid_error_q;
+    hw2reg.transition_regwen             = lc_idle_d;
+    hw2reg.lc_state                      = dec_lc_state;
+    hw2reg.lc_transition_cnt             = dec_lc_cnt;
+    hw2reg.lc_id_state                   = dec_lc_id_state;
+    // The assignments above are identical for the TAP.
+    tap_hw2reg = hw2reg;
+
+    // Assignments gated by mutex.
+    hw2reg.claim_transition_if = sw_claim_transition_if_q;
+    if (sw_claim_transition_if_q) begin
+      hw2reg.transition_token  = transition_token_q;
+      hw2reg.transition_target = transition_target_q;
+    end
+
+    tap_hw2reg.claim_transition_if = tap_claim_transition_if_q;
+    if (tap_claim_transition_if_q) begin
+      tap_hw2reg.transition_token  = transition_token_q;
+      tap_hw2reg.transition_target = transition_target_q;
+    end
+  end
+
+  always_comb begin : p_csr_assign_inputs
+    sw_claim_transition_if_d  = sw_claim_transition_if_q;
+    tap_claim_transition_if_d = tap_claim_transition_if_q;
+    transition_token_d        = transition_token_q;
+    transition_target_d       = transition_target_q;
+    transition_cmd            = 1'b0;
+
+    // SW mutex claim.
+    if (!tap_claim_transition_if_q &&
+        reg2hw.claim_transition_if.qe) begin
+      sw_claim_transition_if_d = reg2hw.claim_transition_if.q;
+    end
+    // TAP mutex claim. This has prio over SW above.
+    if (!sw_claim_transition_if_q &&
+        tap_reg2hw.claim_transition_if.qe) begin
+      tap_claim_transition_if_d = tap_reg2hw.claim_transition_if.q;
+    end
+
+    // The idle signal serves as the REGWEN in this case.
+    if (lc_idle_d) begin
+      if (tap_claim_transition_if_q) begin
+        transition_cmd = tap_reg2hw.transition_cmd.q &
+                         tap_reg2hw.transition_cmd.qe;
+
+        for (int k = 0; k < LcTokenWidth/32; k++) begin
+          if (tap_reg2hw.transition_token[k].qe) begin
+            transition_token_d[k*32 +: 32] = tap_reg2hw.transition_token[k].q;
+          end
+        end
+
+        if (tap_reg2hw.transition_target.qe) begin
+          transition_target_d = tap_reg2hw.transition_target.q;
+        end
+      end else if (sw_claim_transition_if_q) begin
+        transition_cmd = reg2hw.transition_cmd.q &
+                         reg2hw.transition_cmd.qe;
+
+        for (int k = 0; k < LcTokenWidth/32; k++) begin
+          if (reg2hw.transition_token[k].qe) begin
+            transition_token_d[k*32 +: 32] = reg2hw.transition_token[k].q;
+          end
+        end
+
+        if (reg2hw.transition_target.qe) begin
+          transition_target_d = reg2hw.transition_target.q;
+        end
+      end
+    end
+  end
+
+  always_ff @(posedge clk_i or negedge rst_ni) begin : p_csrs
+    if (!rst_ni) begin
+      trans_success_q           <= 1'b0;
+      trans_invalid_error_q     <= 1'b0;
+      token_invalid_error_q     <= 1'b0;
+      flash_rma_error_q         <= 1'b0;
+      otp_prog_error_q          <= 1'b0;
+      state_invalid_error_q     <= 1'b0;
+      sw_claim_transition_if_q  <= '0;
+      tap_claim_transition_if_q <= '0;
+      transition_token_q        <= '0;
+      transition_target_q       <= '0;
+    end else begin
+      // All status and error bits are terminal and require a reset cycle.
+      trans_success_q           <= trans_success_d        | trans_success_q;
+      trans_cnt_oflw_error_q    <= trans_cnt_oflw_error_d | trans_cnt_oflw_error_q;
+      trans_invalid_error_q     <= trans_invalid_error_d  | trans_invalid_error_q;
+      token_invalid_error_q     <= token_invalid_error_d  | token_invalid_error_q;
+      flash_rma_error_q         <= flash_rma_error_d      | flash_rma_error_q;
+      otp_prog_error_q          <= otp_prog_error_d       | otp_prog_error_q;
+      state_invalid_error_q     <= state_invalid_error_d  | state_invalid_error_q;
+      // Other regs, gated by mutex further below.
+      sw_claim_transition_if_q  <= sw_claim_transition_if_d;
+      tap_claim_transition_if_q <= tap_claim_transition_if_d;
+      transition_token_q        <= transition_token_d;
+      transition_target_q       <= transition_target_d;
+    end
+  end
+
+  //////////////////
+  // Alert Sender //
+  //////////////////
+
+  logic [NumAlerts-1:0] alerts;
+  logic [NumAlerts-1:0] alert_test;
+  logic [NumAlerts-1:0] tap_alert_test;
+
+  assign alerts = {
+    otp_prog_error_q,
+    state_invalid_error_q
+  };
+
+  assign alert_test = {
+    reg2hw.alert_test.lc_programming_failure.q &
+    reg2hw.alert_test.lc_programming_failure.qe,
+    reg2hw.alert_test.lc_state_failure.q &
+    reg2hw.alert_test.lc_state_failure.qe
+  };
+
+   assign tap_alert_test = {
+    tap_reg2hw.alert_test.lc_programming_failure.q &
+    tap_reg2hw.alert_test.lc_programming_failure.qe,
+    tap_reg2hw.alert_test.lc_state_failure.q &
+    tap_reg2hw.alert_test.lc_state_failure.qe
+  };
+
+  for (genvar k = 0; k < NumAlerts; k++) begin : gen_alert_tx
+    prim_alert_sender #(
+      .AsyncOn(AlertAsyncOn[k])
+    ) u_prim_alert_sender (
+      .clk_i,
+      .rst_ni,
+      .alert_req_i ( alerts[k]     |
+                     alert_test[k] |
+                     tap_alert_test[k] ),
+      .alert_ack_o (                 ),
+      .alert_rx_i  ( alert_rx_i[k]   ),
+      .alert_tx_o  ( alert_tx_o[k]   )
+    );
+  end
+
+  //////////////////////////
+  // Escalation Receivers //
+  //////////////////////////
+
+  // This escalation action triggers the
+  // lc_escalate_en life cycle control signal.
+  logic esc_wipe_secrets;
+  prim_esc_receiver u_prim_esc_receiver1 (
+    .clk_i,
+    .rst_ni,
+    .esc_en_o (esc_wipe_secrets),
+    .esc_rx_o (esc_wipe_secrets_rx_o),
+    .esc_tx_i (esc_wipe_secrets_tx_i)
+  );
+
+  // This escalation action moves the life cycle
+  // state into a temporary "SCRAP" state named "ESCALATE".
+  logic esc_scrap_state;
+  prim_esc_receiver u_prim_esc_receiver2 (
+    .clk_i,
+    .rst_ni,
+    .esc_en_o (esc_scrap_state),
+    .esc_rx_o (esc_scrap_state_rx_o),
+    .esc_tx_i (esc_scrap_state_tx_i)
+  );
+
+  ////////////////////////////
+  // Synchronization of IOs //
+  ////////////////////////////
+
+  // Signals going to and coming from power manager.
+  logic lc_init;
+  prim_flop_2sync #(
+    .Width(1)
+  ) u_prim_flop_2sync_init (
+    .clk_i,
+    .rst_ni,
+    .d_i(pwr_lc_i.lc_init),
+    .q_o(lc_init)
+  );
+
+  logic lc_done_d, lc_done_q;
+  logic lc_idle_q;
+
+  always_ff @(posedge clk_i or negedge rst_ni) begin : p_sync_regs
+    if (!rst_ni) begin
+      lc_done_q <= 1'b0;
+      lc_idle_q <= 1'b0;
+    end else begin
+      lc_done_q <= lc_done_d;
+      lc_idle_q <= lc_idle_d;
+    end
+  end
+
+  assign pwr_lc_o.lc_done = lc_done_q;
+  assign pwr_lc_o.lc_idle = lc_idle_q;
+
+  // Life cycle ACK signals.
+  lc_tx_t lc_clk_byp_ack;
+  prim_lc_sync u_prim_lc_sync_clk_byp_ack (
+    .clk_i,
+    .rst_ni,
+    .lc_en_i(lc_clk_byp_ack_i),
+    .lc_en_o(lc_clk_byp_ack)
+  );
+
+  lc_tx_t lc_flash_rma_ack;
+  prim_lc_sync u_prim_lc_sync_flash_rma_ack (
+    .clk_i,
+    .rst_ni,
+    .lc_en_i(lc_flash_rma_ack_i),
+    .lc_en_o(lc_flash_rma_ack)
+  );
+
+  ////////////
+  // LC FSM //
+  ////////////
+
+  assign lc_otp_token_o.token_input = transition_token_q;
+  assign lc_flash_rma_seed_o = transition_token_q[RmaSeedWidth-1:0];
+
+  lc_ctrl_fsm u_lc_ctrl_fsm (
+    .clk_i,
+    .rst_ni,
+    .init_req_i             ( lc_init                         ),
+    .init_done_o            ( lc_done_d                       ),
+    .idle_o                 ( lc_idle_d                       ),
+    .esc_scrap_state_i      ( esc_scrap_state                 ),
+    .esc_wipe_secrets_i     ( esc_wipe_secrets                ),
+    .lc_state_valid_i       ( otp_lc_data_i.valid             ),
+    .lc_state_i             ( otp_lc_data_i.state             ),
+    .lc_id_state_i          ( otp_lc_data_i.id_state          ),
+    .lc_cnt_i               ( otp_lc_data_i.count             ),
+    .test_unlock_token_i    ( otp_lc_data_i.test_unlock_token ),
+    .test_exit_token_i      ( otp_lc_data_i.test_exit_token   ),
+    .rma_token_i            ( otp_lc_data_i.rma_token         ),
+    .trans_cmd_i            ( transition_cmd                  ),
+    .trans_target_i         ( transition_target_q             ),
+    .dec_lc_state_o         ( dec_lc_state                    ),
+    .dec_lc_cnt_o           ( dec_lc_cnt                      ),
+    .dec_lc_id_state_o      ( dec_lc_id_state                 ),
+    .token_hash_req_o       ( lc_otp_token_o.req              ),
+    .token_hash_ack_i       ( lc_otp_token_i.ack              ),
+    .hashed_token_i         ( lc_otp_token_i.hashed_token     ),
+    .otp_prog_req_o         ( lc_otp_program_o.req            ),
+    .otp_prog_lc_state_o    ( lc_otp_program_o.state          ),
+    .otp_prog_lc_cnt_o      ( lc_otp_program_o.count          ),
+    .otp_prog_ack_i         ( lc_otp_program_i.ack            ),
+    .otp_prog_err_i         ( lc_otp_program_i.err            ),
+    .trans_success_o        ( trans_success_d                 ),
+    .trans_cnt_oflw_error_o ( trans_cnt_oflw_error_d          ),
+    .trans_invalid_error_o  ( trans_invalid_error_d           ),
+    .token_invalid_error_o  ( token_invalid_error_d           ),
+    .flash_rma_error_o      ( flash_rma_error_d               ),
+    .otp_prog_error_o       ( otp_prog_error_d                ),
+    .state_invalid_error_o  ( state_invalid_error_d           ),
+    .lc_dft_en_o,
+    .lc_nvm_debug_en_o,
+    .lc_hw_debug_en_o,
+    .lc_cpu_en_o,
+    .lc_provision_wr_en_o,
+    .lc_provision_rd_en_o,
+    .lc_keymgr_en_o,
+    .lc_escalate_en_o,
+    .lc_clk_byp_req_o,
+    .lc_clk_byp_ack_i      ( lc_clk_byp_ack                  ),
+    .lc_flash_rma_req_o,
+    .lc_flash_rma_ack_i    ( lc_flash_rma_ack                )
+  );
+
+  ////////////////
+  // Assertions //
+  ////////////////
+
+  `ASSERT_KNOWN(TlOKnown,               tl_o                 )
+  `ASSERT_KNOWN(AlertTxKnown_A,         alert_tx_o           )
+  `ASSERT_KNOWN(PwrLcKnown_A,           pwr_lc_o             )
+  `ASSERT_KNOWN(LcOtpProgramKnwon_A,    lc_otp_program_o     )
+  `ASSERT_KNOWN(LcOtpTokenKnown_A,      lc_otp_token_o       )
+  `ASSERT_KNOWN(LcDftEnKnown_A,         lc_dft_en_o          )
+  `ASSERT_KNOWN(LcNvmDebugEnKnown_A,    lc_nvm_debug_en_o    )
+  `ASSERT_KNOWN(LcHwDebugEnKnown_A,     lc_hw_debug_en_o     )
+  `ASSERT_KNOWN(LcCpuEnKnown_A,         lc_cpu_en_o          )
+  `ASSERT_KNOWN(LcProvisionWrEnKnown_A, lc_provision_wr_en_o )
+  `ASSERT_KNOWN(LcProvisionRdEnKnown_A, lc_provision_rd_en_o )
+  `ASSERT_KNOWN(LcKeymgrEnKnown_A,      lc_keymgr_en_o       )
+  `ASSERT_KNOWN(LcEscalateEnKnown_A,    lc_escalate_en_o     )
+  `ASSERT_KNOWN(LcClkBypReqKnown_A,     lc_clk_byp_req_o     )
+  `ASSERT_KNOWN(LcFlashRmaSeedKnown_A,  lc_flash_rma_seed_o  )
+  `ASSERT_KNOWN(LcFlashRmaReqKnown_A,   lc_flash_rma_req_o   )
+
+endmodule : lc_ctrl
diff --git a/hw/ip/lc_ctrl/rtl/lc_ctrl_fsm.sv b/hw/ip/lc_ctrl/rtl/lc_ctrl_fsm.sv
new file mode 100644
index 0000000..2fe44c7
--- /dev/null
+++ b/hw/ip/lc_ctrl/rtl/lc_ctrl_fsm.sv
@@ -0,0 +1,447 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// Main Life Cycle Controller FSM.
+
+module lc_ctrl_fsm
+  import lc_ctrl_pkg::*;
+(
+  // This module is combinational, but we
+  // need the clock and reset for the assertions.
+  input                         clk_i,
+  input                         rst_ni,
+  // Initialization request from power manager.
+  input                         init_req_i,
+  output logic                  init_done_o,
+  output logic                  idle_o,
+  // Escalatio input
+  input                         esc_scrap_state_i,
+  input                         esc_wipe_secrets_i,
+  // Life cycle state vector from OTP.
+  input                         lc_state_valid_i,
+  input  lc_state_e             lc_state_i,
+  input  lc_id_state_e          lc_id_state_i,
+  input  lc_cnt_e               lc_cnt_i,
+  // Token input from OTP.
+  input  lc_token_t             test_unlock_token_i,
+  input  lc_token_t             test_exit_token_i,
+  input  lc_token_t             rma_token_i,
+  // Transition trigger interface.
+  input                         trans_cmd_i,
+  input  dec_lc_state_e         trans_target_i,
+  // Decoded life cycle state for CSRs.
+  output dec_lc_state_e         dec_lc_state_o,
+  output dec_lc_cnt_t           dec_lc_cnt_o,
+  output dec_lc_id_state_e      dec_lc_id_state_o,
+  // Token hashing interface
+  output logic                  token_hash_req_o,
+  input                         token_hash_ack_i,
+  input  lc_token_t             hashed_token_i,
+  // OTP programming interface
+  output logic                  otp_prog_req_o,
+  output lc_state_e             otp_prog_lc_state_o,
+  output lc_cnt_e               otp_prog_lc_cnt_o,
+  input                         otp_prog_ack_i,
+  input                         otp_prog_err_i,
+  // Error outputs going to CSRs
+  output logic                  trans_success_o,
+  output logic                  trans_cnt_oflw_error_o,
+  output logic                  trans_invalid_error_o,
+  output logic                  token_invalid_error_o,
+  output logic                  flash_rma_error_o,
+  output logic                  otp_prog_error_o,
+  output logic                  state_invalid_error_o,
+  // Life cycle broadcast outputs.
+  output lc_tx_t                lc_dft_en_o,
+  output lc_tx_t                lc_nvm_debug_en_o,
+  output lc_tx_t                lc_hw_debug_en_o,
+  output lc_tx_t                lc_cpu_en_o,
+  output lc_tx_t                lc_provision_wr_en_o,
+  output lc_tx_t                lc_provision_rd_en_o,
+  output lc_tx_t                lc_keymgr_en_o,
+  output lc_tx_t                lc_escalate_en_o,
+    // Request and feedback to/from clock manager and AST.
+  output lc_tx_t                lc_clk_byp_req_o,
+  input  lc_tx_t                lc_clk_byp_ack_i,
+  // Request and feedback to/from flash controller
+  output lc_tx_t                lc_flash_rma_req_o,
+  input  lc_tx_t                lc_flash_rma_ack_i
+);
+
+  ///////////////
+  // FSM Logic //
+  ///////////////
+  fsm_state_e fsm_state_d, fsm_state_q;
+
+  // Continously feed in valid signal for LC state.
+  logic lc_state_valid_d, lc_state_valid_q;
+  assign lc_state_valid_d = lc_state_valid_i;
+
+  // Encoded state vector.
+  lc_state_e    lc_state_d, lc_state_q, next_lc_state;
+  lc_cnt_e      lc_cnt_d, lc_cnt_q, next_lc_cnt;
+  lc_id_state_e lc_id_state_d, lc_id_state_q;
+
+  // Working register for hashed token.
+  lc_token_t hashed_token_d, hashed_token_q;
+
+  // Feed the lc state reg back to the programming interface of OTP.
+  assign otp_prog_lc_state_o = lc_state_q;
+  assign otp_prog_lc_cnt_o   = lc_cnt_q;
+
+  // Conditional LC signal outputs
+  lc_tx_t lc_clk_byp_req_d, lc_clk_byp_req_q;
+  lc_tx_t lc_flash_rma_req_d, lc_flash_rma_req_q;
+
+  `ASSERT_KNOWN(LcStateKnown_A,   lc_state_q   )
+  `ASSERT_KNOWN(LcCntKnown_A,     lc_cnt_q     )
+  `ASSERT_KNOWN(LcIdStateKnown_A, lc_id_state_q)
+  `ASSERT_KNOWN(FsmStateKnown_A,  fsm_state_q  )
+
+  // Hashed token to compare against.
+  logic [LcTokenWidth-1:0] hashed_token_mux;
+
+  always_comb begin : p_fsm
+    // FSM default state assignments.
+    fsm_state_d   = fsm_state_q;
+    lc_state_d    = lc_state_q;
+    lc_cnt_d      = lc_cnt_q;
+    lc_id_state_d = lc_id_state_q;
+
+    // Token hashing.
+    token_hash_req_o = 1'b0;
+    hashed_token_d   = hashed_token_q;
+
+    // OTP Interface
+    otp_prog_req_o = 1'b0;
+
+    // Defaults for status/error signals.
+    token_invalid_error_o = 1'b0;
+    otp_prog_error_o      = 1'b0;
+    flash_rma_error_o     = 1'b0;
+    trans_success_o       = 1'b0;
+
+    // Status indication going to power manager.
+    init_done_o = 1'b1;
+    idle_o      = 1'b1;
+
+    // The clock bypass and RMA signals remain asserted once set to ON.
+    // Note that the remaining life cycle signals are decoded in
+    // the lc_ctrl_signal_decode submodule.
+    lc_clk_byp_req_d   = lc_clk_byp_req_q;
+    lc_flash_rma_req_d = lc_flash_rma_req_q;
+
+    unique case (fsm_state_q)
+      ///////////////////////////////////////////////////////////////////
+      // Wait here until OTP has initialized and the
+      // power manager sends an initialization request.
+      ResetSt: begin
+        init_done_o = 1'b0;
+        if (init_req_i && lc_state_valid_q) begin
+          fsm_state_d = IdleSt;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Idle state where life cycle control signals are broadcast.
+      // Note that the life cycle signals are decoded and broadcast
+      // in the lc_ctrl_signal_decode submodule.
+      IdleSt: begin
+        idle_o = 1'b1;
+        // Continuously fetch LC state from OTP.
+        lc_state_d    = lc_state_i;
+        lc_cnt_d      = lc_cnt_i;
+        lc_id_state_d = lc_id_state_i;
+
+        // Initiate a transition. This will first increment the
+        // life cycle counter before hashing and checking the token.
+        if (trans_cmd_i) begin
+          fsm_state_d = ClkMuxSt;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Clock mux state. If in RAW or TEST_LOCKED the bypass request is
+      // asserted and we have to wait until the clock mux and clock manager
+      // have switched the mux and the clock divider.
+      ClkMuxSt: begin
+        if (lc_state_q inside {LcStRaw,
+                               LcStTestLocked0,
+                               LcStTestLocked1,
+                               LcStTestLocked2}) begin
+          lc_clk_byp_req_d = On;
+          if (lc_clk_byp_ack_i == On) begin
+            fsm_state_d = CntIncrSt;
+          end
+        end else begin
+          fsm_state_d = CntIncrSt;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // This increments the life cycle counter state.
+      CntIncrSt: begin
+        // If the counter has reached the maximum, bail out.
+        if (trans_cnt_oflw_error_o) begin
+          fsm_state_d = PostTransSt;
+        end else begin
+          fsm_state_d = CntProgSt;
+          lc_cnt_d = next_lc_cnt;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // This programs the life cycle counter state.
+      CntProgSt: begin
+        otp_prog_req_o = 1'b1;
+        // Check return value and
+        if (otp_prog_ack_i) begin
+          if (otp_prog_err_i) begin
+            fsm_state_d = PostTransSt;
+            otp_prog_error_o = 1'b1;
+          end else begin
+            fsm_state_d = TransCheckSt;
+          end
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // First transition valid check. This will be repeated several
+      // times below.
+      TransCheckSt: begin
+        if (trans_invalid_error_o) begin
+          fsm_state_d = PostTransSt;
+        end else begin
+          fsm_state_d = TokenHashSt;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Hash and compare the token, no matter whether this transition
+      // is conditional or not. Unconditional transitions just use a known
+      // all-zero token value. That way, we always compare a hashed token
+      // and guarantee that no other control flow path exists that could
+      // bypass the token check.
+      TokenHashSt: begin
+        token_hash_req_o = 1'b1;
+        if (token_hash_ack_i) begin
+          // This is the first comparison.
+          // The token is registered and then
+          // compared two more times further below.
+          hashed_token_d = hashed_token_i;
+          if (hashed_token_i == hashed_token_mux) begin
+            fsm_state_d = FlashRmaSt;
+          end else begin
+            fsm_state_d = PostTransSt;
+            token_invalid_error_o = 1'b1;
+          end
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Flash RMA state. Note that we check the flash response again
+      // two times later below.
+      FlashRmaSt: begin
+        if (trans_target_i == DecLcStRma) begin
+          lc_flash_rma_req_d = On;
+          if (lc_flash_rma_ack_i == On) begin
+            fsm_state_d = TokenCheck0St;
+          end
+        end else begin
+          fsm_state_d = TokenCheck0St;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Check again two times whether this transition and the hashed
+      // token are valid. Also check again whether the flash RMA
+      // response is valid.
+      TokenCheck0St,
+      TokenCheck1St: begin
+        if (trans_invalid_error_o) begin
+          fsm_state_d = PostTransSt;
+        end else begin
+          // If any of these RMA are conditions are true,
+          // all of them must be true at the same time.
+          if ((trans_target_i != DecLcStRma &&
+               lc_flash_rma_req_q == Off    &&
+               lc_flash_rma_ack_i == Off)   ||
+              (trans_target_i == DecLcStRma &&
+               lc_flash_rma_req_q == On     &&
+               lc_flash_rma_ack_i == On)) begin
+            if (hashed_token_i == hashed_token_mux) begin
+              if (fsm_state_q == TokenCheck1St) begin
+                // This is the only way we can get into the
+                // programming state.
+                fsm_state_d = TransProgSt;
+                lc_state_d = next_lc_state;
+              end else begin
+                fsm_state_d = TokenCheck1St;
+              end
+            end else begin
+              fsm_state_d = PostTransSt;
+              token_invalid_error_o = 1'b1;
+            end
+          // The flash RMA process failed.
+          end else begin
+              fsm_state_d = PostTransSt;
+              flash_rma_error_o = 1'b1;
+          end
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Initiate OTP transaction. Note that the concurrent
+      // LC state check is continuously checking whether the
+      // new LC state remains valid. Once the ack returns we are
+      // done with the transition and can go into the terminal PosTransSt.
+      TransProgSt: begin
+        otp_prog_req_o = 1'b1;
+        if (otp_prog_ack_i) begin
+          fsm_state_d = PostTransSt;
+          otp_prog_error_o = otp_prog_err_i;
+          trans_success_o  = ~otp_prog_err_i;
+        end
+      end
+      ///////////////////////////////////////////////////////////////////
+      // Terminal error states.
+      PostTransSt,
+      EscalateSt,
+      InvalidSt: ;
+      ///////////////////////////////////////////////////////////////////
+      // Go to terminal error state if we get here.
+      default: fsm_state_d = InvalidSt;
+      ///////////////////////////////////////////////////////////////////
+    endcase
+
+    // If at any time the life cycle state encoding is not valid,
+    // we jump into the terminal error state right away.
+    if (state_invalid_error_o) begin
+      fsm_state_d = InvalidSt;
+    end else if (esc_scrap_state_i) begin
+      fsm_state_d = EscalateSt;
+    end
+  end
+
+  /////////////////
+  // State Flops //
+  /////////////////
+
+  // This primitive is used to place a size-only constraint on the
+  // flops in order to prevent FSM state encoding optimizations.
+  logic [FsmStateWidth-1:0] fsm_state_raw_q;
+  assign fsm_state_q = fsm_state_e'(fsm_state_raw_q);
+  prim_flop #(
+    .Width(FsmStateWidth),
+    .ResetValue(FsmStateWidth'(ResetSt))
+  ) u_state_regs (
+    .clk_i,
+    .rst_ni,
+    .d_i ( fsm_state_d ),
+    .q_o ( fsm_state_raw_q )
+  );
+
+  always_ff @(posedge clk_i or negedge rst_ni) begin : p_regs
+    if (!rst_ni) begin
+      lc_state_q         <= LcStScrap;
+      lc_cnt_q           <= LcCnt16;
+      lc_id_state_q      <= LcIdPersonalized;
+      lc_state_valid_q   <= 1'b0;
+      hashed_token_q     <= {LcTokenWidth{1'b1}};
+      lc_clk_byp_req_q   <= Off;
+      lc_flash_rma_req_q <= Off;
+    end else begin
+      lc_state_q         <= lc_state_d;
+      lc_cnt_q           <= lc_cnt_d;
+      lc_id_state_q      <= lc_id_state_d;
+      lc_state_valid_q   <= lc_state_valid_d;
+      hashed_token_q     <= hashed_token_d;
+      lc_clk_byp_req_q   <= lc_clk_byp_req_d;
+      lc_flash_rma_req_q <= lc_flash_rma_req_d;
+    end
+  end
+
+  ///////////////
+  // Token mux //
+  ///////////////
+
+  // This indexes the correct token, based on the transition arc.
+  // Note that we always perform a token comparison, even in case of
+  // unconditional transitions. In the case of unconditional tokens
+  // we just pass an all-zero constant through the hashing function.
+  logic [2**TokenIdxWidth-1:0][LcTokenWidth-1:0] hashed_tokens;
+  logic [TokenIdxWidth-1:0] token_idx;
+  always_comb begin : p_token_assign
+    hashed_tokens = '0;
+    hashed_tokens[ZeroTokenIdx]       = AllZeroTokenHashed;
+    hashed_tokens[RawUnlockTokenIdx]  = RawUnlockTokenHashed;
+    hashed_tokens[TestUnlockTokenIdx] = test_unlock_token_i;
+    hashed_tokens[TestExitTokenIdx]   = test_exit_token_i;
+    hashed_tokens[RmaTokenIdx]        = rma_token_i;
+    hashed_tokens[InvalidTokenIdx]    = '0;
+  end
+
+  assign token_idx = TransTokenIdxMatrix[dec_lc_state_o][trans_target_i];
+  assign hashed_token_mux = hashed_tokens[token_idx];
+
+  ////////////////////////////////////////////////////////////////////
+  // Decoding and transition logic for redundantly encoded LC state //
+  ////////////////////////////////////////////////////////////////////
+
+  // This decodes the state into a format that can be exposed in the CSRs,
+  // and flags any errors in the state encoding. Errors will move the
+  // main FSM into INVALID right away.
+  lc_ctrl_state_decode u_lc_ctrl_state_decode (
+    .lc_state_valid_i  ( lc_state_valid_q ),
+    .lc_state_i        ( lc_state_q       ),
+    .lc_id_state_i     ( lc_id_state_q    ),
+    .lc_cnt_i          ( lc_cnt_q         ),
+    .fsm_state_i       ( fsm_state_q      ),
+    .dec_lc_state_o,
+    .dec_lc_id_state_o,
+    .dec_lc_cnt_o,
+    .state_invalid_error_o
+  );
+
+  // LC transition checker logic and next state generation.
+  lc_ctrl_state_transition u_lc_ctrl_state_transition (
+    .lc_state_i            ( lc_state_q     ),
+    .lc_cnt_i              ( lc_cnt_q       ),
+    .dec_lc_state_i        ( dec_lc_state_o ),
+    .trans_target_i,
+    .next_lc_state_o       ( next_lc_state  ),
+    .next_lc_cnt_o         ( next_lc_cnt    ),
+    .trans_cnt_oflw_error_o,
+    .trans_invalid_error_o
+  );
+
+  // LC signal decoder and broadcasting logic.
+  lc_ctrl_signal_decode u_lc_ctrl_signal_decode (
+    .clk_i,
+    .rst_ni,
+    .lc_state_valid_i   ( lc_state_valid_q ),
+    .lc_state_i         ( lc_state_q       ),
+    .lc_id_state_i      ( lc_id_state_q    ),
+    .fsm_state_i        ( fsm_state_q      ),
+    .esc_wipe_secrets_i,
+    .lc_dft_en_o,
+    .lc_nvm_debug_en_o,
+    .lc_hw_debug_en_o,
+    .lc_cpu_en_o,
+    .lc_provision_wr_en_o,
+    .lc_provision_rd_en_o,
+    .lc_keymgr_en_o,
+    .lc_escalate_en_o
+  );
+
+  // Conditional signals set by main FSM.
+  assign lc_clk_byp_req_o   = lc_clk_byp_req_q;
+  assign lc_flash_rma_req_o = lc_flash_rma_req_q;
+
+  ////////////////
+  // Assertions //
+  ////////////////
+
+  `ASSERT(ClkBypStaysOnOnceAsserted_A,
+      lc_escalate_en_q == On
+      |=>
+      lc_escalate_en_q == On)
+
+  `ASSERT(FlashRmaStaysOnOnceAsserted_A,
+      lc_flash_rma_req_o == On
+      |=>
+      lc_flash_rma_req_o == On)
+
+endmodule : lc_ctrl_fsm
diff --git a/hw/ip/lc_ctrl/rtl/lc_ctrl_pkg.sv b/hw/ip/lc_ctrl/rtl/lc_ctrl_pkg.sv
index 1f024c0..85ca004 100644
--- a/hw/ip/lc_ctrl/rtl/lc_ctrl_pkg.sv
+++ b/hw/ip/lc_ctrl/rtl/lc_ctrl_pkg.sv
@@ -5,6 +5,76 @@
 
 package lc_ctrl_pkg;
 
+  import prim_util_pkg::vbits;
+
+  // TODO: need to generate these randomly, based on ECC
+  // polynomial used inside the OTP macro.
+  // The A/B values are used for the encoded LC state.
+  parameter logic [15:0] A0 = 16'h0000;
+  parameter logic [15:0] A1 = 16'h0000;
+  parameter logic [15:0] A2 = 16'h0000;
+  parameter logic [15:0] A3 = 16'h0000;
+  parameter logic [15:0] A4 = 16'h0000;
+  parameter logic [15:0] A5 = 16'h0000;
+  parameter logic [15:0] A6 = 16'h0000;
+  parameter logic [15:0] A7 = 16'h0000;
+  parameter logic [15:0] A8 = 16'h0000;
+  parameter logic [15:0] A9 = 16'h0000;
+  parameter logic [15:0] A10 = 16'h0000;
+  parameter logic [15:0] A11 = 16'h0000;
+
+  parameter logic [15:0] B0 = 16'hFFFF;
+  parameter logic [15:0] B1 = 16'hFFFF;
+  parameter logic [15:0] B2 = 16'hFFFF;
+  parameter logic [15:0] B3 = 16'hFFFF;
+  parameter logic [15:0] B4 = 16'hFFFF;
+  parameter logic [15:0] B5 = 16'hFFFF;
+  parameter logic [15:0] B6 = 16'hFFFF;
+  parameter logic [15:0] B7 = 16'hFFFF;
+  parameter logic [15:0] B8 = 16'hFFFF;
+  parameter logic [15:0] B9 = 16'hFFFF;
+  parameter logic [15:0] B10 = 16'hFFFF;
+  parameter logic [15:0] B11 = 16'hFFFF;
+
+  // The C/D values are used for the encoded LC transition counter.
+  parameter logic [15:0] C0 = 16'h0000;
+  parameter logic [15:0] C1 = 16'h0000;
+  parameter logic [15:0] C2 = 16'h0000;
+  parameter logic [15:0] C3 = 16'h0000;
+  parameter logic [15:0] C4 = 16'h0000;
+  parameter logic [15:0] C5 = 16'h0000;
+  parameter logic [15:0] C6 = 16'h0000;
+  parameter logic [15:0] C7 = 16'h0000;
+  parameter logic [15:0] C8 = 16'h0000;
+  parameter logic [15:0] C9 = 16'h0000;
+  parameter logic [15:0] C10 = 16'h0000;
+  parameter logic [15:0] C11 = 16'h0000;
+  parameter logic [15:0] C12 = 16'h0000;
+  parameter logic [15:0] C13 = 16'h0000;
+  parameter logic [15:0] C14 = 16'h0000;
+  parameter logic [15:0] C15 = 16'h0000;
+
+  parameter logic [15:0] D0 = 16'hFFFF;
+  parameter logic [15:0] D1 = 16'hFFFF;
+  parameter logic [15:0] D2 = 16'hFFFF;
+  parameter logic [15:0] D3 = 16'hFFFF;
+  parameter logic [15:0] D4 = 16'hFFFF;
+  parameter logic [15:0] D5 = 16'hFFFF;
+  parameter logic [15:0] D6 = 16'hFFFF;
+  parameter logic [15:0] D7 = 16'hFFFF;
+  parameter logic [15:0] D8 = 16'hFFFF;
+  parameter logic [15:0] D9 = 16'hFFFF;
+  parameter logic [15:0] D10 = 16'hFFFF;
+  parameter logic [15:0] D11 = 16'hFFFF;
+  parameter logic [15:0] D12 = 16'hFFFF;
+  parameter logic [15:0] D13 = 16'hFFFF;
+  parameter logic [15:0] D14 = 16'hFFFF;
+  parameter logic [15:0] D15 = 16'hFFFF;
+
+  // The E/F values are used for the encoded ID state.
+  parameter logic [15:0] E0 = 16'h0000;
+  parameter logic [15:0] F0 = 16'hFFFF;
+
   /////////////////////////////////
   // General Typedefs and Params //
   /////////////////////////////////
@@ -15,30 +85,108 @@
   parameter int LcStateWidth = NumLcStateValues * LcValueWidth;
   parameter int NumLcCountValues = 16;
   parameter int LcCountWidth = NumLcCountValues * LcValueWidth;
+  parameter int NumLcStates = 13;
+  parameter int DecLcStateWidth = vbits(NumLcStates);
+  parameter int DecLcCountWidth = vbits(NumLcCountValues+1);
+  parameter int LcIdStateWidth = LcValueWidth;
+  parameter int DecLcIdStateWidth = 2;
 
-  typedef enum logic [LcValueWidth-1:0] {
-    Blk = 16'h0000, // blank
-    Set = 16'hF5FA  // programmed
-  } lc_value_e;
+  typedef logic [LcTokenWidth-1:0] lc_token_t;
 
+  // TODO: make this secret and generate randomly, given a specific ECC polynomial.
   typedef enum logic [LcStateWidth-1:0] {
     // Halfword idx   :  11 | 10 |  9 |  8 |  7 |  6 |  5 |  4 |  3 |  2 |  1 |  0
-    LcStRaw           = {Blk, Blk, Blk, Blk, Blk, Blk, Blk, Blk, Blk, Blk, Blk, Blk},
-    LcStTestUnlocked0 = {Blk, Blk, Blk, Blk, Blk, Blk, Blk, Blk, Blk, Blk, Blk, Set},
-    LcStTestLocked0   = {Blk, Blk, Blk, Blk, Blk, Blk, Blk, Blk, Blk, Blk, Set, Set},
-    LcStTestUnlocked1 = {Blk, Blk, Blk, Blk, Blk, Blk, Blk, Blk, Blk, Set, Set, Set},
-    LcStTestLocked1   = {Blk, Blk, Blk, Blk, Blk, Blk, Blk, Blk, Set, Set, Set, Set},
-    LcStTestUnlocked2 = {Blk, Blk, Blk, Blk, Blk, Blk, Blk, Set, Set, Set, Set, Set},
-    LcStTestLocked2   = {Blk, Blk, Blk, Blk, Blk, Blk, Set, Set, Set, Set, Set, Set},
-    LcStTestUnlocked3 = {Blk, Blk, Blk, Blk, Blk, Set, Set, Set, Set, Set, Set, Set},
-    LcStDev           = {Blk, Blk, Blk, Blk, Set, Set, Set, Set, Set, Set, Set, Set},
-    LcStProd          = {Blk, Blk, Blk, Set, Blk, Set, Set, Set, Set, Set, Set, Set},
-    LcStProdEnd       = {Blk, Blk, Set, Blk, Blk, Set, Set, Set, Set, Set, Set, Set},
-    LcStRma           = {Set, Set, Blk, Set, Set, Set, Set, Set, Set, Set, Set, Set},
-    LcStScrap         = {Set, Set, Set, Set, Set, Set, Set, Set, Set, Set, Set, Set}
+    LcStRaw           = '0,
+    LcStTestUnlocked0 = {A11, A10, A9, A8, A7, A6, A5, A4, A3, A2, A1, B0},
+    LcStTestLocked0   = {A11, A10, A9, A8, A7, A6, A5, A4, A3, A2, B1, B0},
+    LcStTestUnlocked1 = {A11, A10, A9, A8, A7, A6, A5, A4, A3, B2, B1, B0},
+    LcStTestLocked1   = {A11, A10, A9, A8, A7, A6, A5, A4, B3, B2, B1, B0},
+    LcStTestUnlocked2 = {A11, A10, A9, A8, A7, A6, A5, B4, B3, B2, B1, B0},
+    LcStTestLocked2   = {A11, A10, A9, A8, A7, A6, B5, B4, B3, B2, B1, B0},
+    LcStTestUnlocked3 = {A11, A10, A9, A8, A7, B6, B5, B4, B3, B2, B1, B0},
+    LcStDev           = {A11, A10, A9, A8, B7, B6, B5, B4, B3, B2, B1, B0},
+    LcStProd          = {A11, A10, A9, B8, A7, B6, B5, B4, B3, B2, B1, B0},
+    LcStProdEnd       = {A11, A10, B9, A8, A7, B6, B5, B4, B3, B2, B1, B0},
+    LcStRma           = {B11, B10, A9, B8, B7, B6, B5, B4, B3, B2, B1, B0},
+    LcStScrap         = {B11, B10, B9, B8, B7, B6, B5, B4, B3, B2, B1, B0}
   } lc_state_e;
 
-  typedef lc_value_e [NumLcCountValues-1:0] lc_cnt_t;
+  // Decoded life cycle state, used to interface with CSRs and TAP.
+  typedef enum logic [DecLcStateWidth-1:0] {
+    DecLcStRaw            = 4'h0,
+    DecLcStTestUnlocked0  = 4'h1,
+    DecLcStTestLocked0    = 4'h2,
+    DecLcStTestUnlocked1  = 4'h3,
+    DecLcStTestLocked1    = 4'h4,
+    DecLcStTestUnlocked2  = 4'h5,
+    DecLcStTestLocked2    = 4'h6,
+    DecLcStTestUnlocked3  = 4'h7,
+    DecLcStDev            = 4'h8,
+    DecLcStProd           = 4'h9,
+    DecLcStProdEnd        = 4'hA,
+    DecLcStRma            = 4'hB,
+    DecLcStScrap          = 4'hC,
+    DecLcStPostTrans      = 4'hD,
+    DecLcStEscalate       = 4'hE,
+    DecLcStInvalid        = 4'hF
+  } dec_lc_state_e;
+
+  typedef enum logic [LcIdStateWidth-1:0] {
+    LcIdBlank        = E0,
+    LcIdPersonalized = F0
+  } lc_id_state_e;
+
+  typedef enum logic [DecLcIdStateWidth-1:0] {
+    DecLcIdBlank        = 2'd0,
+    DecLcIdPersonalized = 2'd1,
+    DecLcIdInvalid      = 2'd2
+  } dec_lc_id_state_e;
+
+  typedef enum logic [LcCountWidth-1:0] {
+    LcCntRaw = '0,
+    LcCnt1   = {C15, C14, C13, C12, C11, C10, C9, C8, C7, C6, C5, C4, C3, C2, C1, D0},
+    LcCnt2   = {C15, C14, C13, C12, C11, C10, C9, C8, C7, C6, C5, C4, C3, C2, D1, D0},
+    LcCnt3   = {C15, C14, C13, C12, C11, C10, C9, C8, C7, C6, C5, C4, C3, D2, D1, D0},
+    LcCnt4   = {C15, C14, C13, C12, C11, C10, C9, C8, C7, C6, C5, C4, D3, D2, D1, D0},
+    LcCnt5   = {C15, C14, C13, C12, C11, C10, C9, C8, C7, C6, C5, D4, D3, D2, D1, D0},
+    LcCnt6   = {C15, C14, C13, C12, C11, C10, C9, C8, C7, C6, D5, D4, D3, D2, D1, D0},
+    LcCnt7   = {C15, C14, C13, C12, C11, C10, C9, C8, C7, D6, D5, D4, D3, D2, D1, D0},
+    LcCnt8   = {C15, C14, C13, C12, C11, C10, C9, C8, D7, D6, D5, D4, D3, D2, D1, D0},
+    LcCnt9   = {C15, C14, C13, C12, C11, C10, C9, D8, D7, D6, D5, D4, D3, D2, D1, D0},
+    LcCnt10  = {C15, C14, C13, C12, C11, C10, D9, D8, D7, D6, D5, D4, D3, D2, D1, D0},
+    LcCnt11  = {C15, C14, C13, C12, C11, D10, D9, D8, D7, D6, D5, D4, D3, D2, D1, D0},
+    LcCnt12  = {C15, C14, C13, C12, D11, D10, D9, D8, D7, D6, D5, D4, D3, D2, D1, D0},
+    LcCnt13  = {C15, C14, C13, D12, D11, D10, D9, D8, D7, D6, D5, D4, D3, D2, D1, D0},
+    LcCnt14  = {C15, C14, D13, D12, D11, D10, D9, D8, D7, D6, D5, D4, D3, D2, D1, D0},
+    LcCnt15  = {C15, D14, D13, D12, D11, D10, D9, D8, D7, D6, D5, D4, D3, D2, D1, D0},
+    LcCnt16  = {D15, D14, D13, D12, D11, D10, D9, D8, D7, D6, D5, D4, D3, D2, D1, D0}
+  } lc_cnt_e;
+
+  typedef logic [DecLcCountWidth-1:0] dec_lc_cnt_t;
+
+
+  ///////////////////////////////////////
+  // Netlist Constants (Hashed Tokens) //
+  ///////////////////////////////////////
+
+  parameter int NumTokens = 6;
+  parameter int TokenIdxWidth = vbits(NumTokens);
+  typedef enum logic [TokenIdxWidth-1:0] {
+    // This is the index for the hashed all-zero constant.
+    // All unconditional transitions use this token.
+    ZeroTokenIdx       = 3'h0,
+    RawUnlockTokenIdx  = 3'h1,
+    TestUnlockTokenIdx = 3'h2,
+    TestExitTokenIdx   = 3'h3,
+    RmaTokenIdx        = 3'h4,
+    // This is the index for an all-zero value (i.e., hashed value = '0).
+    // This is used as an additional blocker for some invalid state transition edges.
+    InvalidTokenIdx    = 3'h5
+  } token_idx_e;
+
+  // TODO: precompute these values (probably have to do that in OTP at elab time).
+  parameter logic [TokenIdxWidth-1:0] RawUnlockTokenHashed = '0;
+  parameter logic [TokenIdxWidth-1:0] AllZeroTokenHashed = '0;
 
   ////////////////////////////////
   // Typedefs for LC Interfaces //
@@ -50,10 +198,149 @@
     Off = 4'b0101
   } lc_tx_e;
 
-  typedef struct packed {
-    lc_tx_e state;
-  } lc_tx_t;
+  typedef lc_tx_e lc_tx_t;
 
   parameter lc_tx_t LC_TX_DEFAULT = Off;
 
+  parameter int RmaSeedWidth = 32;
+  typedef logic [RmaSeedWidth-1:0] lc_flash_rma_seed_t;
+
+  ////////////////////
+  // Main FSM State //
+  ////////////////////
+
+  // Encoding generated with:
+  // $ ./sparse-fsm-encode.py -d 5 -m 14 -n 16 \
+  //      -s 2934212379 --language=sv
+  //
+  // Hamming distance histogram:
+  //
+  //  0: --
+  //  1: --
+  //  2: --
+  //  3: --
+  //  4: --
+  //  5: |||||| (6.59%)
+  //  6: |||||||||| (10.99%)
+  //  7: |||||||||||||||| (17.58%)
+  //  8: |||||||||||||||||||| (20.88%)
+  //  9: |||||||||||||||| (17.58%)
+  // 10: |||||||||||||| (15.38%)
+  // 11: |||||| (6.59%)
+  // 12: ||| (3.30%)
+  // 13: | (1.10%)
+  // 14: --
+  // 15: --
+  // 16: --
+  //
+  // Minimum Hamming distance: 5
+  // Maximum Hamming distance: 13
+  //
+  localparam int FsmStateWidth = 16;
+  typedef enum logic [FsmStateWidth-1:0] {
+    ResetSt       = 16'b1100000001111011,
+    IdleSt        = 16'b1111011010111100,
+    ClkMuxSt      = 16'b0000011110101101,
+    CntIncrSt     = 16'b1100111011001001,
+    CntProgSt     = 16'b0011001111000111,
+    TransCheckSt  = 16'b0000110001010100,
+    TokenHashSt   = 16'b1110100010001111,
+    FlashRmaSt    = 16'b0110111010110000,
+    TokenCheck0St = 16'b0010000011000000,
+    TokenCheck1St = 16'b1101010101101111,
+    TransProgSt   = 16'b1000000110101011,
+    PostTransSt   = 16'b0110110100101100,
+    EscalateSt    = 16'b1010100001010001,
+    InvalidSt     = 16'b1011110110011011
+  } fsm_state_e;
+
+  ///////////////////////////////////////////
+  // Manufacturing State Transition Matrix //
+  ///////////////////////////////////////////
+
+  // The token index matrix below encodes 1) which transition edges are valid and 2) which token
+  // to use for a given transition edge. Note that unconditional but otherwise valid transitions
+  // are assigned the ZeroTokenIdx, whereas invalid transitions are assigned an InvalidTokenIdx.
+  parameter token_idx_e [NumLcStates-1:0][NumLcStates-1:0] TransTokenIdxMatrix = {
+    // SCRAP
+    {13{InvalidTokenIdx}}, // -> TEST_LOCKED0-2, TEST_UNLOCKED0-3, DEV, PROD, PROD_END, RMA, SCRAP
+    // RMA
+    ZeroTokenIdx,          // -> SCRAP
+    {12{InvalidTokenIdx}}, // -> TEST_LOCKED0-2, TEST_UNLOCKED0-3, DEV, PROD, PROD_END, RMA
+    // PROD_END
+    ZeroTokenIdx,          // -> SCRAP
+    {12{InvalidTokenIdx}}, // -> TEST_LOCKED0-2, TEST_UNLOCKED0-3, DEV, PROD, PROD_END, RMA
+    // PROD
+    ZeroTokenIdx,          // -> SCRAP
+    RmaTokenIdx,           // -> RMA
+    {11{InvalidTokenIdx}}, // -> TEST_LOCKED0-2, TEST_UNLOCKED0-3, DEV, PROD, PROD_END
+    // DEV
+    ZeroTokenIdx,          // -> SCRAP
+    RmaTokenIdx,           // -> RMA
+    {11{InvalidTokenIdx}}, // -> TEST_LOCKED0-2, TEST_UNLOCKED0-3, DEV, PROD, PROD_END
+    // TEST_UNLOCKED2
+    {2{ZeroTokenIdx}},     // -> SCRAP, RMA
+    {3{TestExitTokenIdx}}, // -> PROD, PROD_END, DEV
+    {8{InvalidTokenIdx}},  // -> TEST_LOCKED0-2, TEST_UNLOCKED0-3, RAW
+    // TEST_LOCKED2
+    ZeroTokenIdx,          // -> SCRAP
+    InvalidTokenIdx,       // -> RMA
+    {3{TestExitTokenIdx}}, // -> PROD, PROD_END, DEV
+    TestUnlockTokenIdx,    // -> TEST_UNLOCKED3
+    {7{InvalidTokenIdx}},  // -> TEST_LOCKED0-2, TEST_UNLOCKED0-2, RAW
+    // TEST_UNLOCKED2
+    {2{ZeroTokenIdx}},     // -> SCRAP, RMA
+    {3{TestExitTokenIdx}}, // -> PROD, PROD_END, DEV
+    InvalidTokenIdx,       // -> TEST_UNLOCKED3
+    ZeroTokenIdx,          // -> TEST_LOCKED2
+    {6{InvalidTokenIdx}},  // -> TEST_LOCKED0-1, TEST_UNLOCKED0-2, RAW
+    // TEST_LOCKED1
+    ZeroTokenIdx,          // -> SCRAP
+    InvalidTokenIdx,       // -> RMA
+    {3{TestExitTokenIdx}}, // -> PROD, PROD_END, DEV
+    TestUnlockTokenIdx,    // -> TEST_UNLOCKED3
+    InvalidTokenIdx  ,     // -> TEST_LOCKED2
+    TestUnlockTokenIdx,    // -> TEST_UNLOCKED2
+    {5{InvalidTokenIdx}},  // -> TEST_LOCKED0-1, TEST_UNLOCKED0-1, RAW
+    // TEST_UNLOCKED1
+    {2{ZeroTokenIdx}},     // -> SCRAP, RMA
+    {3{TestExitTokenIdx}}, // -> PROD, PROD_END, DEV
+    InvalidTokenIdx,       // -> TEST_UNLOCKED3
+    ZeroTokenIdx,          // -> TEST_LOCKED2
+    InvalidTokenIdx,       // -> TEST_UNLOCKED2
+    ZeroTokenIdx,          // -> TEST_LOCKED1
+    {4{InvalidTokenIdx}},  // -> TEST_LOCKED0, TEST_UNLOCKED0-1, RAW
+    // TEST_LOCKED0
+    ZeroTokenIdx,          // -> SCRAP
+    InvalidTokenIdx,       // -> RMA
+    {3{TestExitTokenIdx}}, // -> PROD, PROD_END, DEV
+    TestUnlockTokenIdx,    // -> TEST_UNLOCKED3
+    InvalidTokenIdx,       // -> TEST_LOCKED2
+    TestUnlockTokenIdx,    // -> TEST_UNLOCKED2
+    InvalidTokenIdx,       // -> TEST_LOCKED1
+    TestUnlockTokenIdx,    // -> TEST_UNLOCKED1
+    {3{InvalidTokenIdx}},  // -> TEST_LOCKED0, TEST_UNLOCKED0, RAW
+    // TEST_UNLOCKED0
+    {2{ZeroTokenIdx}},     // -> SCRAP, RMA
+    {3{TestExitTokenIdx}}, // -> PROD, PROD_END, DEV
+    InvalidTokenIdx,       // -> TEST_UNLOCKED3
+    ZeroTokenIdx,          // -> TEST_LOCKED2
+    InvalidTokenIdx,       // -> TEST_UNLOCKED2
+    ZeroTokenIdx,          // -> TEST_LOCKED1
+    InvalidTokenIdx,       // -> TEST_UNLOCKED1
+    ZeroTokenIdx,          // -> TEST_LOCKED0
+    {2{InvalidTokenIdx}},  // -> TEST_UNLOCKED0, RAW
+    // RAW
+    ZeroTokenIdx,          // -> SCRAP
+    {4{InvalidTokenIdx}},  // -> RMA, PROD, PROD_END, DEV
+    RawUnlockTokenIdx,     // -> TEST_UNLOCKED3
+    InvalidTokenIdx,       // -> TEST_LOCKED2
+    RawUnlockTokenIdx,     // -> TEST_UNLOCKED2
+    InvalidTokenIdx,       // -> TEST_LOCKED1
+    RawUnlockTokenIdx,     // -> TEST_UNLOCKED1
+    InvalidTokenIdx,       // -> TEST_LOCKED0
+    RawUnlockTokenIdx,     // -> TEST_UNLOCKED0
+    InvalidTokenIdx        // -> RAW
+  };
+
 endpackage : lc_ctrl_pkg
diff --git a/hw/ip/lc_ctrl/rtl/lc_ctrl_signal_decode.sv b/hw/ip/lc_ctrl/rtl/lc_ctrl_signal_decode.sv
new file mode 100644
index 0000000..75507d3
--- /dev/null
+++ b/hw/ip/lc_ctrl/rtl/lc_ctrl_signal_decode.sv
@@ -0,0 +1,183 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// Life cycle signal decoder and sender module.
+
+module lc_ctrl_signal_decode
+  import lc_ctrl_pkg::*;
+(
+  input                  clk_i,
+  input                  rst_ni,
+  // Life cycle state vector.
+  input  logic           lc_state_valid_i,
+  input  lc_state_e      lc_state_i,
+  input  lc_id_state_e   lc_id_state_i,
+  input  fsm_state_e     fsm_state_i,
+  // Escalation enable from escalation receiver.
+  input                  esc_wipe_secrets_i,
+  // Life cycle broadcast outputs.
+  output lc_tx_t         lc_dft_en_o,
+  output lc_tx_t         lc_nvm_debug_en_o,
+  output lc_tx_t         lc_hw_debug_en_o,
+  output lc_tx_t         lc_cpu_en_o,
+  output lc_tx_t         lc_provision_wr_en_o,
+  output lc_tx_t         lc_provision_rd_en_o,
+  output lc_tx_t         lc_keymgr_en_o,
+  output lc_tx_t         lc_escalate_en_o
+);
+
+  //////////////////////////
+  // Signal Decoder Logic //
+  //////////////////////////
+
+  lc_tx_t lc_dft_en_d, lc_dft_en_q;
+  lc_tx_t lc_nvm_debug_en_d, lc_nvm_debug_en_q;
+  lc_tx_t lc_hw_debug_en_d, lc_hw_debug_en_q;
+  lc_tx_t lc_cpu_en_d, lc_cpu_en_q;
+  lc_tx_t lc_provision_wr_en_d, lc_provision_wr_en_q;
+  lc_tx_t lc_provision_rd_en_d, lc_provision_rd_en_q;
+  lc_tx_t lc_keymgr_en_d, lc_keymgr_en_q;
+  lc_tx_t lc_escalate_en_d, lc_escalate_en_q;
+
+  always_comb begin : p_lc_signal_decode
+    // Life cycle control signal defaults
+    lc_dft_en_d          = Off;
+    lc_nvm_debug_en_d    = Off;
+    lc_hw_debug_en_d     = Off;
+    lc_cpu_en_d          = Off;
+    lc_provision_wr_en_d = Off;
+    lc_provision_rd_en_d = Off;
+    lc_keymgr_en_d       = Off;
+    lc_escalate_en_d     = Off;
+
+    // The escalation life cycle signal is always decoded, no matter
+    // which state we currently are in.
+    if (esc_wipe_secrets_i) begin
+      lc_escalate_en_d = On;
+    end
+
+    // Only broadcast during the following main FSM states
+    if (lc_state_valid_i && fsm_state_i inside {IdleSt,
+                                                ClkMuxSt,
+                                                CntIncrSt,
+                                                CntProgSt,
+                                                TransCheckSt,
+                                                FlashRmaSt,
+                                                TokenHashSt,
+                                                TokenCheck0St,
+                                                TokenCheck1St,
+                                                TransProgSt}) begin
+      unique case (lc_state_i)
+        ///////////////////////////////////////////////////////////////////
+        // Enable DFT and debug functionality, including the CPU in the
+        // test unlocked states.
+        LcStTestUnlocked0,
+        LcStTestUnlocked1,
+        LcStTestUnlocked2,
+        LcStTestUnlocked3: begin
+          lc_dft_en_d       = On;
+          lc_nvm_debug_en_d = On;
+          lc_hw_debug_en_d  = On;
+          lc_cpu_en_d       = On;
+        end
+        ///////////////////////////////////////////////////////////////////
+        // Enable production functions
+        LcStProd, LcStProdEnd: begin
+          lc_cpu_en_d          = On;
+          lc_keymgr_en_d       = On;
+          lc_provision_rd_en_d = On;
+          // Only allow provisioning if the defice has not yet been personalized.
+          if (lc_id_state_i == LcIdBlank) begin
+            lc_provision_wr_en_d = On;
+          end
+        end
+        ///////////////////////////////////////////////////////////////////
+        // Same functions as PROD, but with additional debug functionality.
+        LcStDev: begin
+          lc_hw_debug_en_d     = On;
+          lc_cpu_en_d          = On;
+          lc_keymgr_en_d       = On;
+          lc_provision_rd_en_d = On;
+          // Only allow provisioning if the defice has not yet been personalized.
+          if (lc_id_state_i == LcIdBlank) begin
+            lc_provision_wr_en_d = On;
+          end
+        end
+        ///////////////////////////////////////////////////////////////////
+        // Enable all test and production functions.
+        LcStRma: begin
+          lc_dft_en_d          = On;
+          lc_nvm_debug_en_d    = On;
+          lc_hw_debug_en_d     = On;
+          lc_cpu_en_d          = On;
+          lc_keymgr_en_d       = On;
+          lc_provision_rd_en_d = On;
+          // Only allow provisioning if the defice has not yet been personalized.
+          if (lc_id_state_i == LcIdBlank) begin
+            lc_provision_wr_en_d = On;
+          end
+        end
+        ///////////////////////////////////////////////////////////////////
+        // Invalid or scrapped life cycle state, do not assert
+        // any signals other than escalate_en and clk_byp_en.
+        default: ;
+      endcase // lc_state_i
+    end
+  end
+
+  /////////////////////////////////
+  // Control signal output flops //
+  /////////////////////////////////
+
+  assign lc_dft_en_o          = lc_dft_en_q;
+  assign lc_nvm_debug_en_o    = lc_nvm_debug_en_q;
+  assign lc_hw_debug_en_o     = lc_hw_debug_en_q;
+  assign lc_cpu_en_o          = lc_cpu_en_q;
+  assign lc_provision_wr_en_o = lc_provision_wr_en_q;
+  assign lc_provision_rd_en_o = lc_provision_rd_en_q;
+  assign lc_keymgr_en_o       = lc_keymgr_en_q;
+  assign lc_escalate_en_o     = lc_escalate_en_q;
+
+  always_ff @(posedge clk_i or negedge rst_ni) begin : p_regs
+    if (!rst_ni) begin
+      lc_dft_en_q          <= Off;
+      lc_nvm_debug_en_q    <= Off;
+      lc_hw_debug_en_q     <= Off;
+      lc_cpu_en_q          <= Off;
+      lc_provision_wr_en_q <= Off;
+      lc_provision_rd_en_q <= Off;
+      lc_keymgr_en_q       <= Off;
+      lc_escalate_en_q     <= Off;
+    end else begin
+      lc_dft_en_q          <= lc_dft_en_d;
+      lc_nvm_debug_en_q    <= lc_nvm_debug_en_d;
+      lc_hw_debug_en_q     <= lc_hw_debug_en_d;
+      lc_cpu_en_q          <= lc_cpu_en_d;
+      lc_provision_wr_en_q <= lc_provision_wr_en_d;
+      lc_provision_rd_en_q <= lc_provision_rd_en_d;
+      lc_keymgr_en_q       <= lc_keymgr_en_d;
+      lc_escalate_en_q     <= lc_escalate_en_d;
+    end
+  end
+
+  ////////////////
+  // Assertions //
+  ////////////////
+
+  `ASSERT(SignalsAreOffWhenNotEnabled_A,
+      !lc_state_valid_i
+      |=>
+      lc_dft_en_o == Off &&
+      lc_nvm_debug_en_o == Off &&
+      lc_hw_debug_en_o == Off &&
+      lc_cpu_en_o == Off &&
+      lc_provision_wr_en_o == Off &&
+      lc_provision_rd_en_o == Off &&
+      lc_keymgr_en_o == Off &&
+      lc_dft_en_o == Off)
+
+  `ASSERT(EscalationAlwaysDecoded_A,
+      (lc_escalate_en_o == On) == $past(esc_wipe_secrets_i))
+
+endmodule : lc_ctrl_signal_decode
diff --git a/hw/ip/lc_ctrl/rtl/lc_ctrl_state_decode.sv b/hw/ip/lc_ctrl/rtl/lc_ctrl_state_decode.sv
new file mode 100644
index 0000000..d684c29
--- /dev/null
+++ b/hw/ip/lc_ctrl/rtl/lc_ctrl_state_decode.sv
@@ -0,0 +1,125 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// Life cycle state decoder. This is a purely combinational module.
+
+module lc_ctrl_state_decode
+  import lc_ctrl_pkg::*;
+(
+  // Life cycle state vector.
+  input  logic              lc_state_valid_i,
+  input  lc_state_e         lc_state_i,
+  input  lc_id_state_e      lc_id_state_i,
+  input  lc_cnt_e           lc_cnt_i,
+  // Main FSM state.
+  input  fsm_state_e        fsm_state_i,
+  // Decoded state vector.
+  output dec_lc_state_e     dec_lc_state_o,
+  output dec_lc_id_state_e  dec_lc_id_state_o,
+  output dec_lc_cnt_t       dec_lc_cnt_o,
+  output logic              state_invalid_error_o
+);
+
+  //////////////////////////
+  // Signal Decoder Logic //
+  //////////////////////////
+
+  // The decoder logic below decodes the life cycle state vector and counter
+  // into a format that can be exposed in the CSRs. If the state is invalid,
+  // this will be flagged as well.
+
+  always_comb begin : p_lc_state_decode
+    // Decoded state defaults
+    dec_lc_state_o        = DecLcStInvalid;
+    dec_lc_cnt_o          = {DecLcCountWidth{1'b1}};
+    dec_lc_id_state_o     = DecLcIdInvalid;
+    state_invalid_error_o = 1'b0;
+
+    unique case (fsm_state_i)
+      // Don't decode anything in ResetSt
+      ResetSt: ;
+      // These are temporary, terminal states that are not encoded
+      // in the persistenc LC state vector from OTP, hence we decode them first.
+      EscalateSt:  dec_lc_state_o = DecLcStEscalate;
+      PostTransSt: dec_lc_state_o = DecLcStPostTrans;
+      InvalidSt:   dec_lc_state_o = DecLcStInvalid;
+      // Otherwise check and decode the life cycle state continously.
+      default: begin
+        // Note that we require that the valid signal from OTP is
+        // asserted at all times except when the LC controller is in ResetSt.
+        // This will trigger an invalid_state_error when the OTP partition
+        // is corrupt and moved into an error state, where the valid bit is
+        // deasserted.
+        state_invalid_error_o = ~lc_state_valid_i;
+
+        unique case (lc_state_i)
+          LcStRaw:           dec_lc_state_o = DecLcStRaw;
+          LcStTestUnlocked0: dec_lc_state_o = DecLcStTestUnlocked0;
+          LcStTestLocked0:   dec_lc_state_o = DecLcStTestLocked0;
+          LcStTestUnlocked1: dec_lc_state_o = DecLcStTestUnlocked1;
+          LcStTestLocked1:   dec_lc_state_o = DecLcStTestLocked1;
+          LcStTestUnlocked2: dec_lc_state_o = DecLcStTestUnlocked2;
+          LcStTestLocked2:   dec_lc_state_o = DecLcStTestLocked2;
+          LcStTestUnlocked3: dec_lc_state_o = DecLcStTestUnlocked3;
+          LcStDev:           dec_lc_state_o = DecLcStDev;
+          LcStProd:          dec_lc_state_o = DecLcStProd;
+          LcStProdEnd:       dec_lc_state_o = DecLcStProdEnd;
+          LcStRma:           dec_lc_state_o = DecLcStRma;
+          LcStScrap:         dec_lc_state_o = DecLcStScrap;
+          default:           state_invalid_error_o = 1'b1;
+        endcase // lc_state_i
+
+        unique case (lc_cnt_i)
+          LcCntRaw: dec_lc_cnt_o = 5'd0;
+          LcCnt1:   dec_lc_cnt_o = 5'd1;
+          LcCnt2:   dec_lc_cnt_o = 5'd2;
+          LcCnt3:   dec_lc_cnt_o = 5'd3;
+          LcCnt4:   dec_lc_cnt_o = 5'd4;
+          LcCnt5:   dec_lc_cnt_o = 5'd5;
+          LcCnt6:   dec_lc_cnt_o = 5'd6;
+          LcCnt7:   dec_lc_cnt_o = 5'd7;
+          LcCnt8:   dec_lc_cnt_o = 5'd8;
+          LcCnt9:   dec_lc_cnt_o = 5'd9;
+          LcCnt10:  dec_lc_cnt_o = 5'd10;
+          LcCnt11:  dec_lc_cnt_o = 5'd11;
+          LcCnt12:  dec_lc_cnt_o = 5'd12;
+          LcCnt13:  dec_lc_cnt_o = 5'd13;
+          LcCnt14:  dec_lc_cnt_o = 5'd14;
+          LcCnt15:  dec_lc_cnt_o = 5'd15;
+          LcCnt16:  dec_lc_cnt_o = 5'd16;
+          default:  state_invalid_error_o = 1'b1;
+        endcase // lc_cnt_i
+
+        unique case (lc_id_state_i)
+          LcIdBlank:        dec_lc_id_state_o = DecLcIdBlank;
+          LcIdPersonalized: dec_lc_id_state_o = DecLcIdPersonalized;
+          default:          state_invalid_error_o = 1'b1;
+        endcase // lc_id_state_i
+
+        // Require that any non-raw state has a valid, nonzero
+        // transition count.
+        if (lc_state_i != LcStRaw && lc_cnt_i != LcCntRaw) begin
+          state_invalid_error_o = 1'b1;
+        end
+
+        // We can't have a personalized device that is
+        // still in RAW or any of the test states.
+        if ((lc_id_state_i == LcIdPersonalized) &&
+            !(lc_state_i inside {LcStDev,
+                                 LcStProd,
+                                 LcStProdEnd,
+                                 LcStRma,
+                                 LcStScrap})) begin
+          state_invalid_error_o = 1'b1;
+        end
+      end
+    endcase // lc_id_state_i
+  end
+
+  ////////////////
+  // Assertions //
+  ////////////////
+
+
+endmodule : lc_ctrl_state_decode
diff --git a/hw/ip/lc_ctrl/rtl/lc_ctrl_state_transition.sv b/hw/ip/lc_ctrl/rtl/lc_ctrl_state_transition.sv
new file mode 100644
index 0000000..036f70b
--- /dev/null
+++ b/hw/ip/lc_ctrl/rtl/lc_ctrl_state_transition.sv
@@ -0,0 +1,95 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// Life cycle state transition function. Checks whether a transition is valid
+// and computes the target state. This module is purely combinational.
+
+module lc_ctrl_state_transition
+  import lc_ctrl_pkg::*;
+(
+  // Life cycle state vector.
+  input  lc_state_e        lc_state_i,
+  input  lc_cnt_e          lc_cnt_i,
+  // Decoded lc state input
+  input  dec_lc_state_e    dec_lc_state_i,
+  // Transition target.
+  input  dec_lc_state_e    trans_target_i,
+  // Updated state vector.
+  output lc_state_e        next_lc_state_o,
+  output lc_cnt_e          next_lc_cnt_o,
+  // If the transition counter is maxed out
+  output logic             trans_cnt_oflw_error_o,
+  output logic             trans_invalid_error_o
+);
+
+  //////////////////////////
+  // Signal Decoder Logic //
+  //////////////////////////
+
+  // The decoder logic below checks whether a given transition edge
+  // is valid and computes the next lc counter ans state vectors.
+  always_comb begin : p_lc_state_transition
+    // Decoded state defaults
+    next_lc_cnt_o = lc_cnt_i;
+    next_lc_state_o = lc_state_i;
+    trans_cnt_oflw_error_o = 1'b0;
+    trans_invalid_error_o = 1'b0;
+
+    // In this state, the life cycle counter is incremented.
+    // Throw an error if the counter is already maxed out.
+    unique case (lc_cnt_i)
+      LcCntRaw: next_lc_cnt_o = LcCnt1;
+      LcCnt1:   next_lc_cnt_o = LcCnt2;
+      LcCnt2:   next_lc_cnt_o = LcCnt3;
+      LcCnt3:   next_lc_cnt_o = LcCnt4;
+      LcCnt4:   next_lc_cnt_o = LcCnt5;
+      LcCnt5:   next_lc_cnt_o = LcCnt6;
+      LcCnt6:   next_lc_cnt_o = LcCnt7;
+      LcCnt7:   next_lc_cnt_o = LcCnt8;
+      LcCnt8:   next_lc_cnt_o = LcCnt9;
+      LcCnt9:   next_lc_cnt_o = LcCnt10;
+      LcCnt10:  next_lc_cnt_o = LcCnt11;
+      LcCnt11:  next_lc_cnt_o = LcCnt12;
+      LcCnt12:  next_lc_cnt_o = LcCnt13;
+      LcCnt13:  next_lc_cnt_o = LcCnt14;
+      LcCnt14:  next_lc_cnt_o = LcCnt15;
+      LcCnt15:  next_lc_cnt_o = LcCnt16;
+      LcCnt16:  trans_cnt_oflw_error_o = 1'b1;
+      default:  trans_cnt_oflw_error_o = 1'b1;
+    endcase // lc_cnt_i
+
+    // Check that the decoded transition indexes are valid
+    // before indexing the state transition matrix.
+    if (dec_lc_state_i <= DecLcStScrap ||
+        trans_target_i <= DecLcStScrap) begin
+      // Check the state transition token matrix in order to see whether this
+      // transition is valid. All transitions have a token index value different
+      // from InvalidTokenIdx.
+      if (TransTokenIdxMatrix[dec_lc_state_i][trans_target_i] != InvalidTokenIdx) begin
+        // Encode the target state.
+        unique case (trans_target_i)
+          DecLcStRaw:           next_lc_state_o = LcStRaw;
+          DecLcStTestUnlocked0: next_lc_state_o = LcStTestUnlocked0;
+          DecLcStTestLocked0:   next_lc_state_o = LcStTestLocked0;
+          DecLcStTestUnlocked1: next_lc_state_o = LcStTestUnlocked1;
+          DecLcStTestLocked1:   next_lc_state_o = LcStTestLocked1;
+          DecLcStTestUnlocked2: next_lc_state_o = LcStTestUnlocked2;
+          DecLcStTestLocked2:   next_lc_state_o = LcStTestLocked2;
+          DecLcStTestUnlocked3: next_lc_state_o = LcStTestUnlocked3;
+          DecLcStDev:           next_lc_state_o = LcStDev;
+          DecLcStProd:          next_lc_state_o = LcStProd;
+          DecLcStProdEnd:       next_lc_state_o = LcStProdEnd;
+          DecLcStRma:           next_lc_state_o = LcStRma;
+          DecLcStScrap:         next_lc_state_o = LcStScrap;
+          default: ;
+        endcase // trans_target_i
+      end else begin
+        trans_invalid_error_o = 1'b1;
+      end
+    end else begin
+      trans_invalid_error_o = 1'b1;
+    end
+  end
+
+endmodule : lc_ctrl_state_transition
diff --git a/hw/ip/prim/rtl/prim_lc_sync.sv b/hw/ip/prim/rtl/prim_lc_sync.sv
index 1de7450..516e2cb 100644
--- a/hw/ip/prim/rtl/prim_lc_sync.sv
+++ b/hw/ip/prim/rtl/prim_lc_sync.sv
@@ -25,10 +25,10 @@
 
   `ASSERT_INIT(NumCopiesMustBeGreaterZero_A, NumCopies > 0)
 
-  lc_ctrl_pkg::lc_tx_t lc_en;
+  logic [lc_ctrl_pkg::TxWidth-1:0] lc_en;
   prim_flop_2sync #(
     .Width(lc_ctrl_pkg::TxWidth),
-    .ResetValue(int'(lc_ctrl_pkg::Off))
+    .ResetValue(lc_ctrl_pkg::TxWidth'(lc_ctrl_pkg::Off))
   ) u_prim_flop_2sync (
     .clk_i,
     .rst_ni,
@@ -36,16 +36,19 @@
     .q_o(lc_en)
   );
 
+  logic [NumCopies-1:0][lc_ctrl_pkg::TxWidth-1:0] lc_en_copies;
   for (genvar j = 0; j < NumCopies; j++) begin : gen_buffs
     for (genvar k = 0; k < lc_ctrl_pkg::TxWidth; k++) begin : gen_bits
       // TODO: replace this with a normal buffer primitive, once available.
       prim_clock_buf u_prim_clock_buf (
         .clk_i(lc_en[k]),
-        .clk_o(lc_en_o[j][k])
+        .clk_o(lc_en_copies[j][k])
       );
     end
   end
 
+  assign lc_en_o = lc_en_copies;
+
   ////////////////
   // Assertions //
   ////////////////
diff --git a/hw/top_earlgrey/lint/top_earlgrey_lint_cfgs.hjson b/hw/top_earlgrey/lint/top_earlgrey_lint_cfgs.hjson
index 1ca9e4b..1d2caa6 100644
--- a/hw/top_earlgrey/lint/top_earlgrey_lint_cfgs.hjson
+++ b/hw/top_earlgrey/lint/top_earlgrey_lint_cfgs.hjson
@@ -68,6 +68,11 @@
                   import_cfgs: ["{proj_root}/hw/lint/tools/dvsim/common_lint_cfg.hjson"]
                   rel_path: "hw/ip/i2c/lint/{tool}"
              },
+             {    name: lc_ctrl
+                  fusesoc_core: lowrisc:ip:lc_ctrl
+                  import_cfgs: ["{proj_root}/hw/lint/tools/dvsim/common_lint_cfg.hjson"]
+                  rel_path: "hw/ip/lc_ctrl/lint/{tool}"
+             },
              {    name: pattgen
                   fusesoc_core: lowrisc:ip:pattgen
                   import_cfgs: ["{proj_root}/hw/lint/tools/dvsim/common_lint_cfg.hjson"]