| // Copyright lowRISC contributors. | |
| // Licensed under the Apache License, Version 2.0, see LICENSE for details. | |
| // SPDX-License-Identifier: Apache-2.0 | |
| // | |
| // SRAM controller. | |
| // | |
| `include "prim_assert.sv" | |
| module sram_ctrl | |
| import sram_ctrl_pkg::*; | |
| import sram_ctrl_reg_pkg::*; | |
| #( | |
| // Number of words stored in the SRAM. | |
| parameter int MemSizeRam = 32'h1000, | |
| // Enable asynchronous transitions on alerts. | |
| parameter logic [NumAlerts-1:0] AlertAsyncOn = {NumAlerts{1'b1}}, | |
| // Enables the execute from SRAM feature. | |
| parameter bit InstrExec = 1, | |
| // Random netlist constants | |
| parameter otp_ctrl_pkg::sram_key_t RndCnstSramKey = RndCnstSramKeyDefault, | |
| parameter otp_ctrl_pkg::sram_nonce_t RndCnstSramNonce = RndCnstSramNonceDefault, | |
| parameter lfsr_seed_t RndCnstLfsrSeed = RndCnstLfsrSeedDefault, | |
| parameter lfsr_perm_t RndCnstLfsrPerm = RndCnstLfsrPermDefault | |
| ) ( | |
| // SRAM Clock | |
| input logic clk_i, | |
| input logic rst_ni, | |
| // OTP Clock (for key interface) | |
| input logic clk_otp_i, | |
| input logic rst_otp_ni, | |
| // Bus Interface (device) for SRAM | |
| input tlul_pkg::tl_h2d_t ram_tl_i, | |
| output tlul_pkg::tl_d2h_t ram_tl_o, | |
| // Bus Interface (device) for CSRs | |
| input tlul_pkg::tl_h2d_t regs_tl_i, | |
| output tlul_pkg::tl_d2h_t regs_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, | |
| // Life-cycle escalation input (scraps the scrambling keys) | |
| // SEC_CM: LC_ESCALATE_EN.INTERSIG.MUBI | |
| input lc_ctrl_pkg::lc_tx_t lc_escalate_en_i, | |
| // SEC_CM: LC_HW_DEBUG_EN.INTERSIG.MUBI | |
| input lc_ctrl_pkg::lc_tx_t lc_hw_debug_en_i, | |
| // Otp configuration for sram execution | |
| // SEC_CM: EXEC.INTERSIG.MUBI | |
| input prim_mubi_pkg::mubi8_t otp_en_sram_ifetch_i, | |
| // Key request to OTP (running on clk_fixed) | |
| // SEC_CM: SCRAMBLE.KEY.SIDELOAD | |
| output otp_ctrl_pkg::sram_otp_key_req_t sram_otp_key_o, | |
| input otp_ctrl_pkg::sram_otp_key_rsp_t sram_otp_key_i, | |
| // config | |
| input prim_ram_1p_pkg::ram_1p_cfg_t cfg_i | |
| ); | |
| import lc_ctrl_pkg::lc_tx_t; | |
| import lc_ctrl_pkg::lc_tx_test_true_loose; | |
| import lc_ctrl_pkg::lc_tx_bool_to_lc_tx; | |
| import lc_ctrl_pkg::lc_tx_or_hi; | |
| import lc_ctrl_pkg::lc_tx_inv; | |
| import lc_ctrl_pkg::lc_to_mubi4; | |
| // This is later on pruned to the correct width at the SRAM wrapper interface. | |
| parameter int unsigned Depth = MemSizeRam >> 2; | |
| parameter int unsigned AddrWidth = prim_util_pkg::vbits(Depth); | |
| `ASSERT_INIT(NonceWidthsLessThanSource_A, NonceWidth + LfsrWidth <= otp_ctrl_pkg::SramNonceWidth) | |
| ///////////////////////////////////// | |
| // Anchor incoming seeds and constants | |
| ///////////////////////////////////// | |
| localparam int TotalAnchorWidth = $bits(otp_ctrl_pkg::sram_key_t) + | |
| $bits(otp_ctrl_pkg::sram_nonce_t); | |
| otp_ctrl_pkg::sram_key_t cnst_sram_key; | |
| otp_ctrl_pkg::sram_nonce_t cnst_sram_nonce; | |
| prim_sec_anchor_buf #( | |
| .Width(TotalAnchorWidth) | |
| ) u_seed_anchor ( | |
| .in_i({RndCnstSramKey, | |
| RndCnstSramNonce}), | |
| .out_o({cnst_sram_key, | |
| cnst_sram_nonce}) | |
| ); | |
| ////////////////////////// | |
| // CSR Node and Mapping // | |
| ////////////////////////// | |
| // We've got two bus interfaces + an lc_gate in this module, | |
| // hence three integ failure sources. | |
| logic [2:0] bus_integ_error; | |
| sram_ctrl_regs_reg2hw_t reg2hw; | |
| sram_ctrl_regs_hw2reg_t hw2reg; | |
| // SEC_CM: CTRL.CONFIG.REGWEN | |
| // SEC_CM: EXEC.CONFIG.REGWEN | |
| sram_ctrl_regs_reg_top u_reg_regs ( | |
| .clk_i, | |
| .rst_ni, | |
| .tl_i (regs_tl_i), | |
| .tl_o (regs_tl_o), | |
| .reg2hw, | |
| .hw2reg, | |
| // SEC_CM: BUS.INTEGRITY | |
| .intg_err_o(bus_integ_error[0]), | |
| .devmode_i (1'b1) | |
| ); | |
| // Key and attribute outputs to scrambling device | |
| logic [otp_ctrl_pkg::SramKeyWidth-1:0] key_d, key_q; | |
| logic [otp_ctrl_pkg::SramNonceWidth-1:0] nonce_d, nonce_q; | |
| // tie-off unused nonce bits | |
| if (otp_ctrl_pkg::SramNonceWidth > NonceWidth) begin : gen_nonce_tieoff | |
| logic unused_nonce; | |
| assign unused_nonce = ^nonce_q[otp_ctrl_pkg::SramNonceWidth-1:NonceWidth]; | |
| end | |
| ////////////////// | |
| // Alert Sender // | |
| ////////////////// | |
| logic alert_test; | |
| assign alert_test = reg2hw.alert_test.q & reg2hw.alert_test.qe; | |
| assign hw2reg.status.bus_integ_error.d = 1'b1; | |
| assign hw2reg.status.bus_integ_error.de = |bus_integ_error; | |
| logic init_error; | |
| assign hw2reg.status.init_error.d = 1'b1; | |
| assign hw2reg.status.init_error.de = init_error; | |
| logic alert_req; | |
| assign alert_req = (|bus_integ_error) | init_error; | |
| prim_alert_sender #( | |
| .AsyncOn(AlertAsyncOn[0]), | |
| .IsFatal(1) | |
| ) u_prim_alert_sender_parity ( | |
| .clk_i, | |
| .rst_ni, | |
| .alert_test_i ( alert_test ), | |
| .alert_req_i ( alert_req ), | |
| .alert_ack_o ( ), | |
| .alert_state_o ( ), | |
| .alert_rx_i ( alert_rx_i[0] ), | |
| .alert_tx_o ( alert_tx_o[0] ) | |
| ); | |
| ///////////////////////// | |
| // Escalation Triggers // | |
| ///////////////////////// | |
| lc_tx_t [1:0] escalate_en; | |
| prim_lc_sync #( | |
| .NumCopies (2) | |
| ) u_prim_lc_sync ( | |
| .clk_i, | |
| .rst_ni, | |
| .lc_en_i (lc_escalate_en_i), | |
| .lc_en_o (escalate_en) | |
| ); | |
| // SEC_CM: KEY.GLOBAL_ESC | |
| logic escalate; | |
| assign escalate = lc_tx_test_true_loose(escalate_en[0]); | |
| assign hw2reg.status.escalated.d = 1'b1; | |
| assign hw2reg.status.escalated.de = escalate; | |
| // SEC_CM: KEY.LOCAL_ESC | |
| // Aggregate external and internal escalation sources. | |
| // This is used in countermeasures further below (key reset and transaction blocking). | |
| logic local_esc, local_esc_reg; | |
| // This signal only aggregates registered escalation signals and is used for transaction | |
| // blocking further below, which is on a timing-critical path. | |
| assign local_esc_reg = reg2hw.status.escalated.q | | |
| reg2hw.status.init_error.q | | |
| reg2hw.status.bus_integ_error.q; | |
| // This signal aggregates all escalation trigger signals, including the ones that are generated in | |
| // the same cycle such as init_error and bus_integ_error. It is used for countermeasures that are | |
| // not on the critical path (such as clearing the scrambling keys). | |
| assign local_esc = escalate | | |
| init_error | | |
| (|bus_integ_error) | | |
| local_esc_reg; | |
| // Convert registered, local escalation sources to a multibit signal and combine this with | |
| // the incoming escalation enable signal before feeding into the TL-UL gate further below. | |
| lc_tx_t lc_tlul_gate_en; | |
| assign lc_tlul_gate_en = lc_tx_inv(lc_tx_or_hi(escalate_en[1], | |
| lc_tx_bool_to_lc_tx(local_esc_reg))); | |
| /////////////////////// | |
| // HW Initialization // | |
| /////////////////////// | |
| // A write to the init register reloads the LFSR seed, resets the init counter and | |
| // sets init_q to flag a pending initialization request. | |
| logic init_trig; | |
| assign init_trig = reg2hw.ctrl.init.q & reg2hw.ctrl.init.qe; | |
| logic init_d, init_q, init_done; | |
| assign init_d = (init_done) ? 1'b0 : | |
| (init_trig) ? 1'b1 : init_q; | |
| always_ff @(posedge clk_i or negedge rst_ni) begin : p_init_reg | |
| if(!rst_ni) begin | |
| init_q <= 1'b0; | |
| end else begin | |
| init_q <= init_d; | |
| end | |
| end | |
| // This waits until the scrambling keys are actually valid (this allows the SW to trigger | |
| // key renewal and initialization at the same time). | |
| logic init_req; | |
| logic [AddrWidth-1:0] init_cnt; | |
| logic key_req_pending_d, key_req_pending_q; | |
| assign init_req = init_q & ~key_req_pending_q; | |
| assign init_done = (init_cnt == AddrWidth'(Depth - 1)) & init_req; | |
| // We employ two redundant counters to guard against FI attacks. | |
| // If any of the two is glitched and the two counter states do not agree, | |
| // we trigger an alert. | |
| // SEC_CM: INIT.CTR.REDUN | |
| prim_count #( | |
| .Width(AddrWidth) | |
| ) u_prim_count ( | |
| .clk_i, | |
| .rst_ni, | |
| .clr_i(init_trig), | |
| .set_i(1'b0), | |
| .set_cnt_i('0), | |
| .incr_en_i(init_req), | |
| .decr_en_i(1'b0), | |
| .step_i(AddrWidth'(1)), | |
| .cnt_o(init_cnt), | |
| .cnt_next_o(), | |
| .err_o(init_error) | |
| ); | |
| // Clear this bit on local escalation. | |
| assign hw2reg.status.init_done.d = init_done & ~init_trig & ~local_esc; | |
| assign hw2reg.status.init_done.de = init_done | init_trig | local_esc; | |
| //////////////////////////// | |
| // Scrambling Key Request // | |
| //////////////////////////// | |
| // The scrambling key and nonce have to be requested from the OTP controller via a req/ack | |
| // protocol. Since the OTP controller works in a different clock domain, we have to synchronize | |
| // the req/ack protocol as described in more details here: | |
| // https://docs.opentitan.org/hw/ip/otp_ctrl/doc/index.html#interfaces-to-sram-and-otbn-scramblers | |
| logic key_req, key_ack; | |
| assign key_req = reg2hw.ctrl.renew_scr_key.q & reg2hw.ctrl.renew_scr_key.qe; | |
| assign key_req_pending_d = (key_req) ? 1'b1 : | |
| (key_ack) ? 1'b0 : key_req_pending_q; | |
| // Clear this bit on local escalation. | |
| assign hw2reg.status.scr_key_valid.d = key_ack & ~key_req & ~local_esc; | |
| assign hw2reg.status.scr_key_valid.de = key_req | key_ack | local_esc; | |
| // Clear this bit on local escalation. | |
| logic key_seed_valid; | |
| assign hw2reg.status.scr_key_seed_valid.d = key_seed_valid & ~local_esc; | |
| assign hw2reg.status.scr_key_seed_valid.de = key_ack | local_esc; | |
| always_ff @(posedge clk_i or negedge rst_ni) begin : p_regs | |
| if (!rst_ni) begin | |
| key_req_pending_q <= 1'b0; | |
| // reset case does not use buffered values as the | |
| // reset value will be directly encoded into flop types | |
| key_q <= RndCnstSramKey; | |
| nonce_q <= RndCnstSramNonce; | |
| end else begin | |
| key_req_pending_q <= key_req_pending_d; | |
| if (key_ack) begin | |
| key_q <= key_d; | |
| nonce_q <= nonce_d; | |
| end | |
| // This scraps the keys. | |
| // SEC_CM: KEY.GLOBAL_ESC | |
| // SEC_CM: KEY.LOCAL_ESC | |
| if (local_esc) begin | |
| key_q <= cnst_sram_key; | |
| nonce_q <= cnst_sram_nonce; | |
| end | |
| end | |
| end | |
| prim_sync_reqack_data #( | |
| .Width($bits(otp_ctrl_pkg::sram_otp_key_rsp_t)-1), | |
| .DataSrc2Dst(1'b0) | |
| ) u_prim_sync_reqack_data ( | |
| .clk_src_i ( clk_i ), | |
| .rst_src_ni ( rst_ni ), | |
| .clk_dst_i ( clk_otp_i ), | |
| .rst_dst_ni ( rst_otp_ni ), | |
| .req_chk_i ( 1'b1 ), | |
| .src_req_i ( key_req_pending_q ), | |
| .src_ack_o ( key_ack ), | |
| .dst_req_o ( sram_otp_key_o.req ), | |
| .dst_ack_i ( sram_otp_key_i.ack ), | |
| .data_i ( {sram_otp_key_i.key, | |
| sram_otp_key_i.nonce, | |
| sram_otp_key_i.seed_valid} ), | |
| .data_o ( {key_d, | |
| nonce_d, | |
| key_seed_valid} ) | |
| ); | |
| logic unused_csr_sigs; | |
| assign unused_csr_sigs = ^{reg2hw.status.init_done.q, | |
| reg2hw.status.scr_key_seed_valid.q}; | |
| //////////////////// | |
| // SRAM Execution // | |
| //////////////////// | |
| import prim_mubi_pkg::mubi4_t; | |
| import prim_mubi_pkg::MuBi4True; | |
| import prim_mubi_pkg::MuBi4False; | |
| import prim_mubi_pkg::mubi8_test_true_strict; | |
| mubi4_t en_ifetch; | |
| if (InstrExec) begin : gen_instr_ctrl | |
| mubi4_t lc_ifetch_en; | |
| mubi4_t reg_ifetch_en; | |
| // SEC_CM: INSTR.BUS.LC_GATED | |
| assign lc_ifetch_en = lc_to_mubi4(lc_hw_debug_en_i); | |
| // SEC_CM: EXEC.CONFIG.MUBI | |
| assign reg_ifetch_en = mubi4_t'(reg2hw.exec.q); | |
| // SEC_CM: EXEC.INTERSIG.MUBI | |
| assign en_ifetch = (mubi8_test_true_strict(otp_en_sram_ifetch_i)) ? reg_ifetch_en : | |
| lc_ifetch_en; | |
| end else begin : gen_tieoff | |
| assign en_ifetch = MuBi4False; | |
| // tie off unused signals | |
| logic unused_sigs; | |
| assign unused_sigs = ^{lc_hw_debug_en_i, | |
| reg2hw.exec.q, | |
| otp_en_sram_ifetch_i}; | |
| end | |
| ///////////////////////// | |
| // Initialization LFSR // | |
| ///////////////////////// | |
| logic [LfsrWidth-1:0] lfsr_out; | |
| prim_lfsr #( | |
| .LfsrDw ( LfsrWidth ), | |
| .EntropyDw ( LfsrWidth ), | |
| .StateOutDw ( LfsrWidth ), | |
| .DefaultSeed ( RndCnstLfsrSeed ), | |
| .StatePermEn ( 1'b1 ), | |
| .StatePerm ( RndCnstLfsrPerm ) | |
| ) u_lfsr ( | |
| .clk_i, | |
| .rst_ni, | |
| .lfsr_en_i(init_req), | |
| .seed_en_i(init_trig), | |
| .seed_i(nonce_q[NonceWidth +: LfsrWidth]), | |
| .entropy_i('0), | |
| .state_o(lfsr_out) | |
| ); | |
| // Compute the correct integrity alongside for the pseudo-random initialization values. | |
| logic [DataWidth - 1 :0] lfsr_out_integ; | |
| tlul_data_integ_enc u_tlul_data_integ_enc ( | |
| .data_i(lfsr_out), | |
| .data_intg_o(lfsr_out_integ) | |
| ); | |
| //////////////////////////// | |
| // SRAM TL-UL Access Gate // | |
| //////////////////////////// | |
| logic tl_gate_resp_pending; | |
| tlul_pkg::tl_h2d_t ram_tl_in_gated; | |
| tlul_pkg::tl_d2h_t ram_tl_out_gated; | |
| // SEC_CM: RAM_TL_LC_GATE.FSM.SPARSE | |
| tlul_lc_gate #( | |
| .NumGatesPerDirection(2) | |
| ) u_tlul_lc_gate ( | |
| .clk_i, | |
| .rst_ni, | |
| .tl_h2d_i(ram_tl_i), | |
| .tl_d2h_o(ram_tl_o), | |
| .tl_h2d_o(ram_tl_in_gated), | |
| .tl_d2h_i(ram_tl_out_gated), | |
| .flush_req_i('0), | |
| .flush_ack_o(), | |
| .resp_pending_o(tl_gate_resp_pending), | |
| .lc_en_i (lc_tlul_gate_en), | |
| .err_o (bus_integ_error[2]) | |
| ); | |
| ///////////////////////////////// | |
| // SRAM with scrambling device // | |
| ///////////////////////////////// | |
| logic tlul_req, tlul_gnt, tlul_we; | |
| logic [AddrWidth-1:0] tlul_addr; | |
| logic [DataWidth-1:0] tlul_wdata, tlul_wmask; | |
| logic sram_intg_error, sram_req, sram_gnt, sram_we, sram_rvalid; | |
| logic [AddrWidth-1:0] sram_addr; | |
| logic [DataWidth-1:0] sram_wdata, sram_wmask, sram_rdata; | |
| tlul_adapter_sram #( | |
| .SramAw(AddrWidth), | |
| .SramDw(DataWidth - tlul_pkg::DataIntgWidth), | |
| .Outstanding(2), | |
| .ByteAccess(1), | |
| .CmdIntgCheck(1), | |
| .EnableRspIntgGen(1), | |
| .EnableDataIntgGen(0), | |
| .EnableDataIntgPt(1), // SEC_CM: MEM.INTEGRITY | |
| .SecFifoPtr (1) // SEC_CM: TLUL_FIFO.CTR.REDUN | |
| ) u_tlul_adapter_sram ( | |
| .clk_i, | |
| .rst_ni, | |
| .tl_i (ram_tl_in_gated), | |
| .tl_o (ram_tl_out_gated), | |
| .en_ifetch_i (en_ifetch), | |
| .req_o (tlul_req), | |
| .req_type_o (), | |
| .gnt_i (tlul_gnt), | |
| .we_o (tlul_we), | |
| .addr_o (tlul_addr), | |
| .wdata_o (tlul_wdata), | |
| .wmask_o (tlul_wmask), | |
| // SEC_CM: BUS.INTEGRITY | |
| .intg_error_o(bus_integ_error[1]), | |
| .rdata_i (sram_rdata), | |
| .rvalid_i (sram_rvalid), | |
| .rerror_i ('0) | |
| ); | |
| // Interposing mux logic for initialization with pseudo random data. | |
| assign sram_req = tlul_req | init_req; | |
| assign tlul_gnt = sram_gnt & ~init_req; | |
| assign sram_we = tlul_we | init_req; | |
| assign sram_intg_error = |bus_integ_error[2:1] & ~init_req; | |
| assign sram_addr = (init_req) ? init_cnt : tlul_addr; | |
| assign sram_wdata = (init_req) ? lfsr_out_integ : tlul_wdata; | |
| assign sram_wmask = (init_req) ? {DataWidth{1'b1}} : tlul_wmask; | |
| // The SRAM scrambling wrapper will not accept any transactions while the | |
| // key req is pending or if we have escalated. Note that we're not using | |
| // the scr_key_valid CSR here, such that the SRAM can be used right after | |
| // reset, where the keys are reset to the default netlist constant. | |
| // | |
| // If we have escalated, but there is a pending request in the TL gate, we | |
| // may have a pending read-modify-write transaction in the SRAM adapter. In | |
| // that case we let a write proceed, since the TL gate won't accept any new | |
| // transactions and the SRAM keys have been clobbered already. | |
| logic key_valid; | |
| assign key_valid = (key_req_pending_q) ? 1'b0 : | |
| (reg2hw.status.escalated.q) ? (tl_gate_resp_pending & tlul_we) : 1'b1; | |
| // SEC_CM: MEM.SCRAMBLE, ADDR.SCRAMBLE | |
| prim_ram_1p_scr #( | |
| .Width(DataWidth), | |
| .Depth(Depth), | |
| .EnableParity(0), | |
| .DataBitsPerMask(DataWidth), | |
| .DiffWidth(DataWidth) | |
| ) u_prim_ram_1p_scr ( | |
| .clk_i, | |
| .rst_ni, | |
| .key_valid_i (key_valid), | |
| .key_i (key_q), | |
| .nonce_i (nonce_q[NonceWidth-1:0]), | |
| .req_i (sram_req), | |
| .intg_error_i(sram_intg_error), | |
| .gnt_o (sram_gnt), | |
| .write_i (sram_we), | |
| .addr_i (sram_addr), | |
| .wdata_i (sram_wdata), | |
| .wmask_i (sram_wmask), | |
| .rdata_o (sram_rdata), | |
| .rvalid_o (sram_rvalid), | |
| .rerror_o ( ), | |
| .raddr_o ( ), | |
| .cfg_i | |
| ); | |
| //////////////// | |
| // Assertions // | |
| //////////////// | |
| `ASSERT_KNOWN(RegsTlOutKnown_A, regs_tl_o) | |
| `ASSERT_KNOWN(RamTlOutKnown_A, ram_tl_o.d_valid) | |
| `ASSERT_KNOWN_IF(RamTlOutPayLoadKnown_A, ram_tl_o, ram_tl_o.d_valid) | |
| `ASSERT_KNOWN(AlertOutKnown_A, alert_tx_o) | |
| `ASSERT_KNOWN(SramOtpKeyKnown_A, sram_otp_key_o) | |
| // Alert assertions for redundant counters. | |
| `ASSERT_PRIM_COUNT_ERROR_TRIGGER_ALERT(CntCheck_A, | |
| u_prim_count, alert_tx_o[0]) | |
| `ASSERT_PRIM_FSM_ERROR_TRIGGER_ERR(LcGateFsmCheck_A, | |
| u_tlul_lc_gate.u_state_regs, alert_tx_o[0]) | |
| // Alert assertions for reg_we onehot check. | |
| `ASSERT_PRIM_REG_WE_ONEHOT_ERROR_TRIGGER_ALERT(RegWeOnehotCheck_A, | |
| u_reg_regs, alert_tx_o[0]) | |
| // Alert assertions for redundant counters. | |
| `ASSERT_PRIM_COUNT_ERROR_TRIGGER_ALERT(FifoWptrCheck_A, | |
| u_tlul_adapter_sram.u_rspfifo.gen_normal_fifo.u_fifo_cnt.gen_secure_ptrs.u_wptr, | |
| alert_tx_o[0]) | |
| `ASSERT_PRIM_COUNT_ERROR_TRIGGER_ALERT(FifoRptrCheck_A, | |
| u_tlul_adapter_sram.u_rspfifo.gen_normal_fifo.u_fifo_cnt.gen_secure_ptrs.u_rptr, | |
| alert_tx_o[0]) | |
| endmodule : sram_ctrl |