[reggen] Define classes to represent parameters in a block

This is supposed to represent the thing at obj['params_list']. The
Params class is essentially just a list of the parameters. These
parameters are now split into 3 classes, inheriting from BaseParam.

LocalParam and Parameter are for "normal" parameters and localparams.
RandParameter is used for random netlist constants.

This change has a small impact on topgen.py: the code that pulls
parameters up to the top-level actually gets a bit cleaner, but it now
needs to convert back to dict explicitly. That shuffling, in turn,
slightly changes the ordering of fields in (the checked-in)
top_earlgrey.gen.hjson.

Signed-off-by: Rupert Swarbrick <rswarbrick@lowrisc.org>
diff --git a/hw/top_earlgrey/data/autogen/top_earlgrey.gen.hjson b/hw/top_earlgrey/data/autogen/top_earlgrey.gen.hjson
index 184eb20..723005d 100644
--- a/hw/top_earlgrey/data/autogen/top_earlgrey.gen.hjson
+++ b/hw/top_earlgrey/data/autogen/top_earlgrey.gen.hjson
@@ -2875,33 +2875,26 @@
           type: ""
           default: '''""'''
           expose: "true"
-          local: "false"
-          randcount: "0"
-          randtype: none
           name_top: OtpCtrlMemInitFile
         }
         {
           name: RndCnstLfsrSeed
           desc: Compile-time random bits for initial LFSR seed
           type: otp_ctrl_pkg::lfsr_seed_t
-          randcount: "40"
+          randcount: 40
           randtype: data
-          local: "false"
-          default: 0xf45def7861
-          expose: "false"
           name_top: RndCnstOtpCtrlLfsrSeed
+          default: 0xf45def7861
           randwidth: 40
         }
         {
           name: RndCnstLfsrPerm
           desc: Compile-time random permutation for LFSR output
           type: otp_ctrl_pkg::lfsr_perm_t
-          randcount: "40"
+          randcount: 40
           randtype: perm
-          local: "false"
-          default: 0x5d294061e29a7c404f4593035a19097666e37072064153623855022d39e0
-          expose: "false"
           name_top: RndCnstOtpCtrlLfsrPerm
+          default: 0x5d294061e29a7c404f4593035a19097666e37072064153623855022d39e0
           randwidth: 240
         }
       ]
@@ -3224,36 +3217,30 @@
           name: RndCnstLcKeymgrDivInvalid
           desc: Compile-time random bits for lc state group diversification value
           type: lc_ctrl_pkg::lc_keymgr_div_t
-          randcount: "64"
+          randcount: 64
           randtype: data
-          local: "false"
-          default: 0xf4c3471c5def7861
-          expose: "false"
           name_top: RndCnstLcCtrlLcKeymgrDivInvalid
+          default: 0xf4c3471c5def7861
           randwidth: 64
         }
         {
           name: RndCnstLcKeymgrDivTestDevRma
           desc: Compile-time random bits for lc state group diversification value
           type: lc_ctrl_pkg::lc_keymgr_div_t
-          randcount: "64"
+          randcount: 64
           randtype: data
-          local: "false"
-          default: 0x83d0550b80e84eb1
-          expose: "false"
           name_top: RndCnstLcCtrlLcKeymgrDivTestDevRma
+          default: 0x83d0550b80e84eb1
           randwidth: 64
         }
         {
           name: RndCnstLcKeymgrDivProduction
           desc: Compile-time random bits for lc state group diversification value
           type: lc_ctrl_pkg::lc_keymgr_div_t
-          randcount: "64"
+          randcount: 64
           randtype: data
-          local: "false"
-          default: 0x2d73930d4cac3785
-          expose: "false"
           name_top: RndCnstLcCtrlLcKeymgrDivProduction
+          default: 0x2d73930d4cac3785
           randwidth: 64
         }
       ]
@@ -3715,24 +3702,20 @@
           name: RndCnstLfsrSeed
           desc: Compile-time random bits for initial LFSR seed
           type: alert_pkg::lfsr_seed_t
-          randcount: "32"
+          randcount: 32
           randtype: data
-          local: "false"
-          default: 0x5def7861
-          expose: "false"
           name_top: RndCnstAlertHandlerLfsrSeed
+          default: 0x5def7861
           randwidth: 32
         }
         {
           name: RndCnstLfsrPerm
           desc: Compile-time random permutation for LFSR output
           type: alert_pkg::lfsr_perm_t
-          randcount: "32"
+          randcount: 32
           randtype: perm
-          local: "false"
-          default: 0x5f00c4cafd73fc4ac479a61068375f38956d84b3
-          expose: "false"
           name_top: RndCnstAlertHandlerLfsrPerm
+          default: 0x5f00c4cafd73fc4ac479a61068375f38956d84b3
           randwidth: 160
         }
       ]
@@ -5040,35 +5023,28 @@
           name: RndCnstSramKey
           desc: Compile-time random reset value for SRAM scrambling key.
           type: otp_ctrl_pkg::sram_key_t
-          randcount: "128"
+          randcount: 128
           randtype: data
-          local: "false"
-          default: 0x83d0550b80e84eb1f4c3471c5def7861
-          expose: "false"
           name_top: RndCnstSramCtrlRetAonSramKey
+          default: 0x83d0550b80e84eb1f4c3471c5def7861
           randwidth: 128
         }
         {
           name: RndCnstSramNonce
           desc: Compile-time random reset value for SRAM scrambling nonce.
           type: otp_ctrl_pkg::sram_nonce_t
-          randcount: "64"
+          randcount: 64
           randtype: data
-          local: "false"
-          default: 0x2d73930d4cac3785
-          expose: "false"
           name_top: RndCnstSramCtrlRetAonSramNonce
+          default: 0x2d73930d4cac3785
           randwidth: 64
         }
         {
           name: InstrExec
           desc: Support execution from SRAM
           type: bit
-          local: "false"
-          expose: "true"
           default: "1"
-          randcount: "0"
-          randtype: none
+          expose: "true"
           name_top: SramCtrlRetAonInstrExec
         }
       ]
@@ -5268,48 +5244,40 @@
           name: RndCnstAddrKey
           desc: Compile-time random bits for default address key
           type: flash_ctrl_pkg::flash_key_t
-          randcount: "128"
+          randcount: 128
           randtype: data
-          local: "false"
-          default: 0x83d0550b80e84eb1f4c3471c5def7861
-          expose: "false"
           name_top: RndCnstFlashCtrlAddrKey
+          default: 0x83d0550b80e84eb1f4c3471c5def7861
           randwidth: 128
         }
         {
           name: RndCnstDataKey
           desc: Compile-time random bits for default data key
           type: flash_ctrl_pkg::flash_key_t
-          randcount: "128"
+          randcount: 128
           randtype: data
-          local: "false"
-          default: 0xfabd19450b238d4c2d73930d4cac3785
-          expose: "false"
           name_top: RndCnstFlashCtrlDataKey
+          default: 0xfabd19450b238d4c2d73930d4cac3785
           randwidth: 128
         }
         {
           name: RndCnstLfsrSeed
           desc: Compile-time random bits for initial LFSR seed
           type: flash_ctrl_pkg::lfsr_seed_t
-          randcount: "32"
+          randcount: 32
           randtype: data
-          local: "false"
-          default: 0xd89f9dfc
-          expose: "false"
           name_top: RndCnstFlashCtrlLfsrSeed
+          default: 0xd89f9dfc
           randwidth: 32
         }
         {
           name: RndCnstLfsrPerm
           desc: Compile-time random permutation for LFSR output
           type: flash_ctrl_pkg::lfsr_perm_t
-          randcount: "32"
+          randcount: 32
           randtype: perm
-          local: "false"
-          default: 0x26ff203d990d87c5e8a98bafec7506855aa99c54
-          expose: "false"
           name_top: RndCnstFlashCtrlLfsrPerm
+          default: 0x26ff203d990d87c5e8a98bafec7506855aa99c54
           randwidth: 160
         }
       ]
@@ -5673,145 +5641,116 @@
       [
         {
           name: AES192Enable
+          desc: Disable (0) or enable (1) support for 192-bit key lengths (AES-192).
           type: bit
           default: 1'b1
-          desc: Disable (0) or enable (1) support for 192-bit key lengths (AES-192).
-          local: "false"
           expose: "false"
-          randcount: "0"
-          randtype: none
           name_top: AesAES192Enable
         }
         {
           name: Masking
-          type: bit
-          default: 1'b1
           desc:
             '''
             Disable (0) or enable (1) first-order masking of the AES cipher core.
             Masking requires the use of a masked S-Box, see SBoxImpl parameter.
             '''
-          local: "false"
+          type: bit
+          default: 1'b1
           expose: "true"
-          randcount: "0"
-          randtype: none
           name_top: AesMasking
         }
         {
           name: SBoxImpl
+          desc: Selection of the S-Box implementation. See aes_pkg.sv.
           type: aes_pkg::sbox_impl_e
           default: aes_pkg::SBoxImplDom
-          desc: Selection of the S-Box implementation. See aes_pkg.sv.
-          local: "false"
           expose: "true"
-          randcount: "0"
-          randtype: none
           name_top: AesSBoxImpl
         }
         {
           name: SecStartTriggerDelay
-          type: int unsigned
-          default: "0"
           desc:
             '''
             Manual start trigger delay, useful for SCA measurements.
             A value of e.g. 40 allows the processor to go into sleep before AES starts operation.
             '''
-          local: "false"
+          type: int unsigned
+          default: "0"
           expose: "true"
-          randcount: "0"
-          randtype: none
           name_top: SecAesStartTriggerDelay
         }
         {
           name: SecAllowForcingMasks
-          type: bit
-          default: 1'b0
           desc:
             '''
             Forbid (0) or allow (1) forcing the mask to zero via FORCE_ZERO_MASK bit in the Control Register.
             Useful for SCA measurements.
             Meaningful only if masking is enabled.
             '''
-          local: "false"
+          type: bit
+          default: 1'b0
           expose: "true"
-          randcount: "0"
-          randtype: none
           name_top: SecAesAllowForcingMasks
         }
         {
           name: SecSkipPRNGReseeding
-          type: bit
-          default: 1'b0
           desc:
             '''
             Perform (0) or skip (1) PRNG reseeding requests, useful for SCA measurements only.
             The current SCA setup doesn't provide sufficient resources to implement the infrastructure required for PRNG reseeding (CSRNG, EDN).
             To enable SCA resistance evaluations, we need to skip reseeding requests on the SCA platform.
             '''
-          local: "false"
+          type: bit
+          default: 1'b0
           expose: "true"
-          randcount: "0"
-          randtype: none
           name_top: SecAesSkipPRNGReseeding
         }
         {
           name: AlertAsyncOn
+          desc: One bit per alert specifying whether the corresponding sender in the AES module and the receiver in the alert handler are in the same clock domain (0) or whether there is an asynchronous boundary in between (1).
           type: logic [aes_reg_pkg::NumAlerts-1:0]
           default: "{aes_reg_pkg::NumAlerts{1'b1}}"
-          desc: One bit per alert specifying whether the corresponding sender in the AES module and the receiver in the alert handler are in the same clock domain (0) or whether there is an asynchronous boundary in between (1).
-          local: "false"
           expose: "false"
-          randcount: "0"
-          randtype: none
           name_top: AesAlertAsyncOn
         }
         {
           name: RndCnstClearingLfsrSeed
-          type: aes_pkg::clearing_lfsr_seed_t
           desc: Default seed of the PRNG used for register clearing.
-          randcount: "64"
+          type: aes_pkg::clearing_lfsr_seed_t
+          randcount: 64
           randtype: data
-          local: "false"
-          default: 0xf4c3471c5def7861
-          expose: "false"
           name_top: RndCnstAesClearingLfsrSeed
+          default: 0xf4c3471c5def7861
           randwidth: 64
         }
         {
           name: RndCnstClearingLfsrPerm
-          type: aes_pkg::clearing_lfsr_perm_t
           desc: Permutation applied to the LFSR of the PRNG used for clearing.
-          randcount: "64"
+          type: aes_pkg::clearing_lfsr_perm_t
+          randcount: 64
           randtype: perm
-          local: "false"
-          default: 0x26ac29e186c1f4dc6f959d6ed08dc044a0f3f1519e8dca131275df1e48bbf964ac772e613d0320adaebf38552dd822e6
-          expose: "false"
           name_top: RndCnstAesClearingLfsrPerm
+          default: 0x26ac29e186c1f4dc6f959d6ed08dc044a0f3f1519e8dca131275df1e48bbf964ac772e613d0320adaebf38552dd822e6
           randwidth: 384
         }
         {
           name: RndCnstMaskingLfsrSeed
-          type: aes_pkg::masking_lfsr_seed_t
           desc: Default seed of the PRNG used for masking.
-          randcount: "160"
+          type: aes_pkg::masking_lfsr_seed_t
+          randcount: 160
           randtype: data
-          local: "false"
-          default: 0x19e5a91389b9fe0d3b818e46ce7d846469a3b8e3
-          expose: "false"
           name_top: RndCnstAesMaskingLfsrSeed
+          default: 0x19e5a91389b9fe0d3b818e46ce7d846469a3b8e3
           randwidth: 160
         }
         {
           name: RndCnstMskgChunkLfsrPerm
-          type: aes_pkg::mskg_chunk_lfsr_perm_t
           desc: Permutation applied to the LFSR chunks of the PRNG used for masking.
-          randcount: "32"
+          type: aes_pkg::mskg_chunk_lfsr_perm_t
+          randcount: 32
           randtype: perm
-          local: "false"
-          default: 0x23f6ba7ea92aa0e8e3b900f826cee835bc1648fa
-          expose: "false"
           name_top: RndCnstAesMskgChunkLfsrPerm
+          default: 0x23f6ba7ea92aa0e8e3b900f826cee835bc1648fa
           randwidth: 160
         }
       ]
@@ -6031,24 +5970,19 @@
       [
         {
           name: EnMasking
-          type: bit
-          default: "0"
           desc:
             '''
             Disable(0) or enable(1) first-order masking of Keccak round.
 
             If masking is enabled, ReuseShare parameter will impact the design.
             '''
-          local: "false"
+          type: bit
+          default: "0"
           expose: "true"
-          randcount: "0"
-          randtype: none
           name_top: KmacEnMasking
         }
         {
           name: ReuseShare
-          type: int
-          default: "0"
           desc:
             '''
             If enabled (1), the internal Keccak round logic will re-use the
@@ -6058,10 +5992,9 @@
 
             This feature is not implemented yet.
             '''
-          local: "false"
+          type: int
+          default: "0"
           expose: "true"
-          randcount: "0"
-          randtype: none
           name_top: KmacReuseShare
         }
       ]
@@ -6207,156 +6140,130 @@
           name: RndCnstLfsrSeed
           desc: Compile-time random bits for initial LFSR seed
           type: keymgr_pkg::lfsr_seed_t
-          randcount: "64"
+          randcount: 64
           randtype: data
-          local: "false"
-          default: 0xf4c3471c5def7861
-          expose: "false"
           name_top: RndCnstKeymgrLfsrSeed
+          default: 0xf4c3471c5def7861
           randwidth: 64
         }
         {
           name: RndCnstLfsrPerm
           desc: Compile-time random permutation for LFSR output
           type: keymgr_pkg::lfsr_perm_t
-          randcount: "64"
+          randcount: 64
           randtype: perm
-          local: "false"
-          default: 0x26ac29e186c1f4dc6f959d6ed08dc044a0f3f1519e8dca131275df1e48bbf964ac772e613d0320adaebf38552dd822e6
-          expose: "false"
           name_top: RndCnstKeymgrLfsrPerm
+          default: 0x26ac29e186c1f4dc6f959d6ed08dc044a0f3f1519e8dca131275df1e48bbf964ac772e613d0320adaebf38552dd822e6
           randwidth: 384
         }
         {
           name: RndCnstRandPerm
           desc: Compile-time random permutation for entropy used in share overriding
           type: keymgr_pkg::rand_perm_t
-          randcount: "32"
+          randcount: 32
           randtype: perm
-          local: "false"
-          default: 0x86cb85d79423d404d9d717e683782c975a389f3a
-          expose: "false"
           name_top: RndCnstKeymgrRandPerm
+          default: 0x86cb85d79423d404d9d717e683782c975a389f3a
           randwidth: 160
         }
         {
           name: RndCnstRevisionSeed
           desc: Compile-time random bits for revision seed
           type: keymgr_pkg::seed_t
-          randcount: "256"
+          randcount: 256
           randtype: data
-          local: "false"
-          default: 0xa917f063c414964e46ce0fe8ddb271067884ebcc4f232c841702ef7147e71b5c
-          expose: "false"
           name_top: RndCnstKeymgrRevisionSeed
+          default: 0xa917f063c414964e46ce0fe8ddb271067884ebcc4f232c841702ef7147e71b5c
           randwidth: 256
         }
         {
           name: RndCnstCreatorIdentitySeed
           desc: Compile-time random bits for creator identity seed
           type: keymgr_pkg::seed_t
-          randcount: "256"
+          randcount: 256
           randtype: data
-          local: "false"
-          default: 0x84b1a5a63af5fa0aaf8c6a6b90f868a3f2590e4a1004f9cf620d8c56137d9287
-          expose: "false"
           name_top: RndCnstKeymgrCreatorIdentitySeed
+          default: 0x84b1a5a63af5fa0aaf8c6a6b90f868a3f2590e4a1004f9cf620d8c56137d9287
           randwidth: 256
         }
         {
           name: RndCnstOwnerIntIdentitySeed
           desc: Compile-time random bits for owner intermediate identity seed
           type: keymgr_pkg::seed_t
-          randcount: "256"
+          randcount: 256
           randtype: data
-          local: "false"
-          default: 0x32949cc3635d1f47c8f05affc85f7d889ecd94b67eb674bbdf38d62d3622493
-          expose: "false"
           name_top: RndCnstKeymgrOwnerIntIdentitySeed
+          default: 0x32949cc3635d1f47c8f05affc85f7d889ecd94b67eb674bbdf38d62d3622493
           randwidth: 256
         }
         {
           name: RndCnstOwnerIdentitySeed
           desc: Compile-time random bits for owner identity seed
           type: keymgr_pkg::seed_t
-          randcount: "256"
+          randcount: 256
           randtype: data
-          local: "false"
-          default: 0xeec5e43d4b16446726a27b8f0b30ad5048bae844c87b69111a24d5e4442bcfb7
-          expose: "false"
           name_top: RndCnstKeymgrOwnerIdentitySeed
+          default: 0xeec5e43d4b16446726a27b8f0b30ad5048bae844c87b69111a24d5e4442bcfb7
           randwidth: 256
         }
         {
           name: RndCnstSoftOutputSeed
           desc: Compile-time random bits for software generation seed
           type: keymgr_pkg::seed_t
-          randcount: "256"
+          randcount: 256
           randtype: data
-          local: "false"
-          default: 0xab174a27f3e9a7da096f534d1a07eb42a37dbfb7be9bb6e69a7d3c5f369ae283
-          expose: "false"
           name_top: RndCnstKeymgrSoftOutputSeed
+          default: 0xab174a27f3e9a7da096f534d1a07eb42a37dbfb7be9bb6e69a7d3c5f369ae283
           randwidth: 256
         }
         {
           name: RndCnstHardOutputSeed
           desc: Compile-time random bits for hardware generation seed
           type: keymgr_pkg::seed_t
-          randcount: "256"
+          randcount: 256
           randtype: data
-          local: "false"
-          default: 0x8fefbc18548dfcb2fd145395d417df2f55255e182efed724cf8370466a266421
-          expose: "false"
           name_top: RndCnstKeymgrHardOutputSeed
+          default: 0x8fefbc18548dfcb2fd145395d417df2f55255e182efed724cf8370466a266421
           randwidth: 256
         }
         {
           name: RndCnstAesSeed
           desc: Compile-time random bits for generation seed when aes destination selected
           type: keymgr_pkg::seed_t
-          randcount: "256"
+          randcount: 256
           randtype: data
-          local: "false"
-          default: 0xe390fce7275f3bce5145a3b8edb5f33fd64e96a5b7a45278102f4c28f70a5b08
-          expose: "false"
           name_top: RndCnstKeymgrAesSeed
+          default: 0xe390fce7275f3bce5145a3b8edb5f33fd64e96a5b7a45278102f4c28f70a5b08
           randwidth: 256
         }
         {
           name: RndCnstHmacSeed
           desc: Compile-time random bits for generation seed when hmac destination selected
           type: keymgr_pkg::seed_t
-          randcount: "256"
+          randcount: 256
           randtype: data
-          local: "false"
-          default: 0x5f0e50f0c4cdf307b6cf85bd7669c05b0e5dbc9627c050e4936e54cc761467e
-          expose: "false"
           name_top: RndCnstKeymgrHmacSeed
+          default: 0x5f0e50f0c4cdf307b6cf85bd7669c05b0e5dbc9627c050e4936e54cc761467e
           randwidth: 256
         }
         {
           name: RndCnstKmacSeed
           desc: Compile-time random bits for generation seed when kmac destination selected
           type: keymgr_pkg::seed_t
-          randcount: "256"
+          randcount: 256
           randtype: data
-          local: "false"
-          default: 0xc19175299f2746ea860d6c0dd7a42245f8e1d251d66cef31bce5f18f586239c4
-          expose: "false"
           name_top: RndCnstKeymgrKmacSeed
+          default: 0xc19175299f2746ea860d6c0dd7a42245f8e1d251d66cef31bce5f18f586239c4
           randwidth: 256
         }
         {
           name: RndCnstNoneSeed
           desc: Compile-time random bits for generation seed when no destination selected
           type: keymgr_pkg::seed_t
-          randcount: "256"
+          randcount: 256
           randtype: data
-          local: "false"
-          default: 0x350bb6b68440934dcb834f93689fe9e88ebd53404a1d7296f0cbdb8b82989aa7
-          expose: "false"
           name_top: RndCnstKeymgrNoneSeed
+          default: 0x350bb6b68440934dcb834f93689fe9e88ebd53404a1d7296f0cbdb8b82989aa7
           randwidth: 256
         }
       ]
@@ -6570,13 +6477,10 @@
       [
         {
           name: SBoxImpl
+          desc: Selection of the S-Box implementation. See aes_pkg.sv.
           type: aes_pkg::sbox_impl_e
           default: aes_pkg::SBoxImplCanright
-          desc: Selection of the S-Box implementation. See aes_pkg.sv.
-          local: "false"
           expose: "true"
-          randcount: "0"
-          randtype: none
           name_top: CsrngSBoxImpl
         }
       ]
@@ -7162,35 +7066,28 @@
           name: RndCnstSramKey
           desc: Compile-time random reset value for SRAM scrambling key.
           type: otp_ctrl_pkg::sram_key_t
-          randcount: "128"
+          randcount: 128
           randtype: data
-          local: "false"
-          default: 0xb6d4b556d89f9dfcfabd19450b238d4c
-          expose: "false"
           name_top: RndCnstSramCtrlMainSramKey
+          default: 0xb6d4b556d89f9dfcfabd19450b238d4c
           randwidth: 128
         }
         {
           name: RndCnstSramNonce
           desc: Compile-time random reset value for SRAM scrambling nonce.
           type: otp_ctrl_pkg::sram_nonce_t
-          randcount: "64"
+          randcount: 64
           randtype: data
-          local: "false"
-          default: 0x160733e752cad615
-          expose: "false"
           name_top: RndCnstSramCtrlMainSramNonce
+          default: 0x160733e752cad615
           randwidth: 64
         }
         {
           name: InstrExec
           desc: Support execution from SRAM
           type: bit
-          local: "false"
-          expose: "true"
           default: "1"
-          randcount: "0"
-          randtype: none
+          expose: "true"
           name_top: SramCtrlMainInstrExec
         }
       ]
@@ -7363,13 +7260,10 @@
       [
         {
           name: RegFile
+          desc: Selection of the register file implementation. See otbn_pkg.sv.
           type: otbn_pkg::regfile_e
           default: otbn_pkg::RegFileFF
-          desc: Selection of the register file implementation. See otbn_pkg.sv.
-          local: "false"
           expose: "true"
-          randcount: "0"
-          randtype: none
           name_top: OtbnRegFile
         }
       ]
diff --git a/hw/top_earlgrey/data/top_earlgrey.sv.tpl b/hw/top_earlgrey/data/top_earlgrey.sv.tpl
index b507b29..de9342e 100644
--- a/hw/top_earlgrey/data/top_earlgrey.sv.tpl
+++ b/hw/top_earlgrey/data/top_earlgrey.sv.tpl
@@ -40,7 +40,7 @@
   % if not lib.is_inst(m):
 <% continue %>
   % endif
-  % for p_exp in filter(lambda p: p["expose"] == "true", m["param_list"]):
+  % for p_exp in filter(lambda p: p.get("expose") == "true", m["param_list"]):
   parameter ${p_exp["type"]} ${p_exp["name_top"]} = ${p_exp["default"]},
   % endfor
 % endfor
@@ -592,7 +592,7 @@
   % if m["param_list"]:
   ${m["type"]} #(
     % for i in m["param_list"]:
-    .${i["name"]}(${i["name_top" if i["expose"] == "true" or i["randtype"] != "none" else "default"]})${"," if not loop.last else ""}
+    .${i["name"]}(${i["name_top" if i.get("expose") == "true" or i.get("randtype", "none") != "none" else "default"]})${"," if not loop.last else ""}
     % endfor
   ) u_${m["name"]} (
   % else:
diff --git a/hw/top_earlgrey/data/top_earlgrey_rnd_cnst_pkg.sv.tpl b/hw/top_earlgrey/data/top_earlgrey_rnd_cnst_pkg.sv.tpl
index 6165266..9700c3d 100644
--- a/hw/top_earlgrey/data/top_earlgrey_rnd_cnst_pkg.sv.tpl
+++ b/hw/top_earlgrey/data/top_earlgrey_rnd_cnst_pkg.sv.tpl
@@ -26,7 +26,7 @@
 package top_${top["name"]}_rnd_cnst_pkg;
 
 % for m in top["module"]:
-  % for p in filter(lambda p: p["randtype"] in ["data", "perm"], m["param_list"]):
+  % for p in filter(lambda p: p.get("randtype") in ["data", "perm"], m["param_list"]):
     % if loop.first:
   ////////////////////////////////////////////
   // ${m['name']}
diff --git a/util/build_docs.py b/util/build_docs.py
index ead2c6a..71e88db 100755
--- a/util/build_docs.py
+++ b/util/build_docs.py
@@ -154,7 +154,7 @@
         regs = hjson.load(hardware_file,
                           use_decimal=True,
                           object_pairs_hook=validate.checking_dict)
-        if validate.validate(regs) == 0:
+        if validate.validate(regs, params=[]) == 0:
             logging.info("Parsed %s" % (hardware))
         else:
             logging.fatal("Failed to parse %s" % (hardware))
diff --git a/util/reggen/bits.py b/util/reggen/bits.py
index d1fd99e..d3cb3b3 100644
--- a/util/reggen/bits.py
+++ b/util/reggen/bits.py
@@ -4,10 +4,10 @@
 
 '''Support code for bit ranges in reggen'''
 
-import re
-from typing import List, Dict, Tuple
+from typing import Tuple
 
-from .lib import check_str, expand_parameter
+from .lib import check_str
+from .params import Params
 
 
 class Bits:
@@ -31,7 +31,7 @@
     @staticmethod
     def from_raw(where: str,
                  reg_width: int,
-                 params: List[Dict[str, object]],
+                 params: Params,
                  raw: object) -> 'Bits':
         # Bits should be specified as msb:lsb or as just a single bit index.
         if isinstance(raw, int):
@@ -56,7 +56,7 @@
 
     @staticmethod
     def _parse_str(where: str,
-                   params: List[Dict[str, object]],
+                   params: Params,
                    str_val: str) -> Tuple[int, int]:
         try:
             idx = int(str_val)
@@ -70,54 +70,10 @@
             raise ValueError('bits field for {} is not an '
                              'integer or of the form msb:lsb. Saw {!r}.'
                              .format(where, str_val))
-        return (Bits._parse_index('msb of bits field for {}'.format(where),
-                                  params, parts[0]),
-                Bits._parse_index('lsb of bits field for {}'.format(where),
-                                  params, parts[1]))
-
-    @staticmethod
-    def _parse_index(where: str,
-                     params: List[Dict[str, object]],
-                     str_val: str) -> int:
-        # Here, we want to support arithmetic expressions with + and -. We
-        # don't support other operators, or parentheses (so can parse with just
-        # a regex).
-        #
-        # Use re.split, capturing the operators. This turns e.g. "a + b-c" into
-        # ['a ', '+', ' b', '-', 'c']. If there's a leading operator ("+a"),
-        # the first element of the results is an empty string: elements with
-        # odd positions are always operators and elements with even positions
-        # are values.
-        acc = 0
-        is_neg = False
-
-        for idx, tok in enumerate(re.split(r'([+-])', str_val)):
-            if idx == 0 and not tok:
-                continue
-            if idx % 2:
-                is_neg = (tok == '-')
-                continue
-
-            term = Bits._parse_value('term {} of {}'.format(idx // 2, where),
-                                     params,
-                                     tok.strip())
-            acc += -term if is_neg else term
-
-        return acc
-
-    @staticmethod
-    def _parse_value(where: str,
-                     params: List[Dict[str, object]],
-                     str_val: str) -> int:
-        # If str_val is an integer, return it.
-        try:
-            return int(str_val)
-        except ValueError:
-            pass
-
-        # Otherwise, search through params for a matching parameter.
-        return expand_parameter(params, str_val,
-                                'expanding {}'.format(where))
+        return (params.expand(parts[0],
+                              'msb of bits field for {}'.format(where)),
+                params.expand(parts[1],
+                              'lsb of bits field for {}'.format(where)))
 
     def make_translated(self, bit_offset: int) -> 'Bits':
         assert 0 <= bit_offset
diff --git a/util/reggen/field.py b/util/reggen/field.py
index 1a8791a..4d302e5 100644
--- a/util/reggen/field.py
+++ b/util/reggen/field.py
@@ -9,6 +9,7 @@
 from .enum_entry import EnumEntry
 from .lib import (check_keys, check_str, check_name,
                   check_list, check_str_list, check_xint)
+from .params import Params
 
 REQUIRED_FIELDS = {
     'bits': ['b', "bit or bit range (msb:lsb)"]
@@ -75,7 +76,7 @@
                  reg_width: int,
                  reg_hwqe: bool,
                  reg_hwre: bool,
-                 params: List[Dict[str, object]],
+                 params: Params,
                  raw: object) -> 'Field':
         where = 'field {} of {} register'.format(field_idx, reg_name)
         rd = check_keys(raw, where,
diff --git a/util/reggen/gen_cheader.py b/util/reggen/gen_cheader.py
index 6fdbf7b..b087b76 100644
--- a/util/reggen/gen_cheader.py
+++ b/util/reggen/gen_cheader.py
@@ -169,25 +169,19 @@
 
 
 def gen_cdefines_module_param(outstr, param, module_name, existing_defines):
-    param_type = param['type']
-
-    # Do not generate C defines for parameters that are not localparams defined
-    # in the corresponding SystemVerilog package.
-    if param["local"].lower() == "false":
-        return
-
     # Presently there is only one type (int), however if the new types are
     # added, they potentially need to be handled differently.
     known_types = ["int"]
-    if param_type not in known_types:
-        warnings.warn(
-            "Cannot generate a module define of type {}".format(param_type))
+    if param.param_type not in known_types:
+        warnings.warn("Cannot generate a module define of type {}"
+                      .format(param.param_type))
         return
 
-    genout(outstr, format_comment(first_line(param['desc'])))
-    define_name = as_define(module_name + '_PARAM_' + param['name'])
-    if param_type == "int":
-        define = gen_define(define_name, [], param['default'],
+    if param.desc is not None:
+        genout(outstr, format_comment(first_line(param.desc)))
+    define_name = as_define(module_name + '_PARAM_' + param.name)
+    if param.param_type == "int":
+        define = gen_define(define_name, [], param.value,
                             existing_defines)
 
     genout(outstr, define)
@@ -201,7 +195,7 @@
     if 'param_list' in module_data:
         module_params = module_data['param_list']
 
-    for param in module_params:
+    for param in module_params.get_localparams():
         gen_cdefines_module_param(outstr, param, module_name, existing_defines)
 
     genout(outstr, format_comment(first_line("Register width")))
diff --git a/util/reggen/gen_json.py b/util/reggen/gen_json.py
index 18c1b5a..2e65f70 100644
--- a/util/reggen/gen_json.py
+++ b/util/reggen/gen_json.py
@@ -8,11 +8,13 @@
 
 
 def gen_json(obj, outfile, format):
-    # Temporary hack to deal with the fact that the 'registers' field is a list
-    # rather than a dictionary. When we convert the top-level object to a class
-    # (with its own _as_dict method), this logic can go in there.
+    # Temporary hack to deal with the fact that the 'registers' and
+    # 'param_list' fields are lists rather than dictionaries. When we convert
+    # the top-level object to a class (with its own _as_dict method), this
+    # logic can go in there.
     obj = obj.copy()
     obj['registers'] = obj['registers'].as_dicts()
+    obj['param_list'] = obj['param_list'].as_dicts()
 
     if format == 'json':
         hjson.dumpJSON(obj,
diff --git a/util/reggen/lib.py b/util/reggen/lib.py
index e4af727..d2e076e 100644
--- a/util/reggen/lib.py
+++ b/util/reggen/lib.py
@@ -232,47 +232,3 @@
 
     raise ValueError('{} is of type {}, not an integer.'
                      .format(what, type(obj).__name__))
-
-
-def expand_parameter(params: List[Dict[str, object]],
-                     value: str,
-                     when: str) -> int:
-    # Check whether the 'parameter' is already an integer: if so, return that.
-    try:
-        return int(value, 0)
-    except ValueError:
-        pass
-
-    found = None
-    for param in params:
-        if param['name'] == value:
-            found = param
-            break
-    if found is None:
-        raise ValueError('Cannot find a parameter called {} when {}. '
-                         'Known parameters: {}.'
-                         .format(value,
-                                 when,
-                                 ', '.join(str(p['name'])
-                                           for p in params)))
-
-    # Only allow localparams in the expansion (because otherwise we're at
-    # the mercy of whatever instantiates the block)
-    if param['local'] != 'true':
-        raise ValueError("When {}, {} is a parameter, "
-                         "not a localparam."
-                         .format(when, value))
-
-    default = param['default']
-    if isinstance(default, int):
-        return default
-
-    default_str = check_str(default,
-                            'default field in {!r} parameter'
-                            .format(value))
-    try:
-        return int(default_str, 0)
-    except ValueError:
-        raise ValueError("When {}, the {} value expanded as "
-                         "{}, which doesn't parse as an integer."
-                         .format(when, value, param['default'])) from None
diff --git a/util/reggen/multi_register.py b/util/reggen/multi_register.py
index 37bc9ee..818701b 100644
--- a/util/reggen/multi_register.py
+++ b/util/reggen/multi_register.py
@@ -6,8 +6,8 @@
 
 from reggen import register
 from .field import Field
-from .lib import (check_keys, check_str, check_name,
-                  check_bool, expand_parameter)
+from .lib import check_keys, check_str, check_name, check_bool
+from .params import Params
 from .reg_base import RegBase
 from .register import Register
 
@@ -47,7 +47,7 @@
                  offset: int,
                  addrsep: int,
                  reg_width: int,
-                 params: List[Dict[str, object]],
+                 params: Params,
                  raw: object):
         super().__init__(offset)
 
@@ -86,9 +86,8 @@
         count_str = check_str(rd['count'],
                               'count field of multireg {}'
                               .format(self.reg.name))
-        self.count = expand_parameter(params, count_str,
-                                      'expanding count field of multireg {}'
-                                      .format(self.reg.name))
+        self.count = params.expand(count_str,
+                                   'count field of multireg ' + self.reg.name)
         if self.count <= 0:
             raise ValueError("Multireg {} has a count of {}, "
                              "which isn't positive."
diff --git a/util/reggen/params.py b/util/reggen/params.py
new file mode 100644
index 0000000..14f52a0
--- /dev/null
+++ b/util/reggen/params.py
@@ -0,0 +1,332 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+import re
+from typing import Dict, List, Optional
+
+from .lib import check_keys, check_str, check_int, check_bool, check_list
+
+REQUIRED_FIELDS = {
+    'name': ['s', "name of the item"],
+}
+
+OPTIONAL_FIELDS = {
+    'desc': ['s', "description of the item"],
+    'type': ['s', "item type. int by default"],
+    'default': ['s', "item default value"],
+    'local': ['pb', "to be localparam"],
+    'expose': ['pb', "to be exposed to top"],
+    'randcount': [
+        's', "number of bits to randomize in the parameter. 0 by default."
+    ],
+    'randtype': ['s', "type of randomization to perform. none by default"],
+}
+
+
+class BaseParam:
+    def __init__(self, name: str, desc: Optional[str], param_type: str):
+        self.name = name
+        self.desc = desc
+        self.param_type = param_type
+
+    def apply_default(self, value: str) -> None:
+        if self.param_type[:3] == 'int':
+            check_int(value,
+                      'default value for parameter {} '
+                      '(which has type {})'
+                      .format(self.name, self.param_type))
+        self.default = value
+
+    def as_dict(self) -> Dict[str, object]:
+        rd = {}  # type: Dict[str, object]
+        rd['name'] = self.name
+        if self.desc is not None:
+            rd['desc'] = self.desc
+        rd['type'] = self.param_type
+        return rd
+
+
+class LocalParam(BaseParam):
+    def __init__(self,
+                 name: str,
+                 desc: Optional[str],
+                 param_type: str,
+                 value: str):
+        super().__init__(name, desc, param_type)
+        self.value = value
+
+    def expand_value(self, when: str) -> int:
+        try:
+            return int(self.value, 0)
+        except ValueError:
+            raise ValueError("When {}, the {} value expanded as "
+                             "{}, which doesn't parse as an integer."
+                             .format(when, self.name, self.value)) from None
+
+    def as_dict(self) -> Dict[str, object]:
+        rd = super().as_dict()
+        rd['local'] = True
+        rd['default'] = self.value
+        return rd
+
+
+class Parameter(BaseParam):
+    def __init__(self,
+                 name: str,
+                 desc: Optional[str],
+                 param_type: str,
+                 default: str,
+                 expose: bool):
+        super().__init__(name, desc, param_type)
+        self.default = default
+        self.expose = expose
+
+    def as_dict(self) -> Dict[str, object]:
+        rd = super().as_dict()
+        rd['default'] = self.default
+        rd['expose'] = 'true' if self.expose else 'false'
+        return rd
+
+
+class RandParameter(BaseParam):
+    def __init__(self,
+                 name: str,
+                 desc: Optional[str],
+                 param_type: str,
+                 randcount: int,
+                 randtype: str):
+        assert randcount > 0
+        assert randtype in ['perm', 'data']
+        super().__init__(name, desc, param_type)
+        self.randcount = randcount
+        self.randtype = randtype
+
+    def apply_default(self, value: str) -> None:
+        raise ValueError('Cannot apply a default value of {!r} to '
+                         'parameter {}: it is a random netlist constant.'
+                         .format(self.name, value))
+
+    def as_dict(self) -> Dict[str, object]:
+        rd = super().as_dict()
+        rd['randcount'] = self.randcount
+        rd['randtype'] = self.randtype
+        return rd
+
+
+def _parse_parameter(where: str, raw: object) -> BaseParam:
+    rd = check_keys(raw, where,
+                    list(REQUIRED_FIELDS.keys()),
+                    list(OPTIONAL_FIELDS.keys()))
+
+    # TODO: Check if PascalCase or ALL_CAPS
+    name = check_str(rd['name'], 'name field of ' + where)
+
+    r_desc = rd.get('desc')
+    if r_desc is None:
+        desc = None
+    else:
+        desc = check_str(r_desc, 'desc field of ' + where)
+
+    # TODO: We should probably check that any register called RndCnstFoo has
+    #       randtype and randcount.
+    if name.lower().startswith('rndcnst') and 'randtype' in rd:
+        # This is a random netlist constant and should be parsed as a
+        # RandParameter.
+        randtype = check_str(rd.get('randtype', 'none'),
+                             'randtype field of ' + where)
+        if randtype not in ['perm', 'data']:
+            raise ValueError('At {}, parameter {} has a name that implies it '
+                             'is a random netlist constant, which means it '
+                             'must specify a randtype of "perm" or "data", '
+                             'rather than {!r}.'
+                             .format(where, name, randtype))
+
+        r_randcount = rd.get('randcount')
+        if r_randcount is None:
+            raise ValueError('At {}, the random netlist constant {} has no '
+                             'randcount field.'
+                             .format(where, name))
+        randcount = check_int(r_randcount, 'randcount field of ' + where)
+        if randcount <= 0:
+            raise ValueError('At {}, the random netlist constant {} has a '
+                             'randcount of {}, which is not positive.'
+                             .format(where, name, randcount))
+
+        r_type = rd.get('type')
+        if r_type is None:
+            raise ValueError('At {}, parameter {} has no type field (which is '
+                             'required for random netlist constants).'
+                             .format(where, name))
+        param_type = check_str(r_type, 'type field of ' + where)
+
+        local = check_bool(rd.get('local', 'false'), 'local field of ' + where)
+        if local:
+            raise ValueError('At {}, the parameter {} specifies local = true, '
+                             'meaning that it is a localparam. This is '
+                             'incompatible with being a random netlist '
+                             'constant (how would it be set?)'
+                             .format(where, name))
+
+        r_default = rd.get('default')
+        if r_default is not None:
+            raise ValueError('At {}, the parameter {} specifies a value for '
+                             'the "default" field. This is incompatible with '
+                             'being a random netlist constant: the value will '
+                             'be set by the random generator.'
+                             .format(where, name))
+
+        expose = check_bool(rd.get('expose', 'false'),
+                            'expose field of ' + where)
+        if expose:
+            raise ValueError('At {}, the parameter {} specifies expose = '
+                             'true, meaning that the parameter is exposed to '
+                             'the top-level. This is incompatible with being '
+                             'a random netlist constant.'
+                             .format(where, name))
+
+        return RandParameter(name, desc, param_type, randcount, randtype)
+
+    # This doesn't have a name like a random netlist constant. Check that it
+    # doesn't define randcount or randtype.
+    for fld in ['randcount', 'randtype']:
+        if fld in rd:
+            raise ValueError("At {where}, the parameter {name} specifies "
+                             "{fld} but the name doesn't look like a random "
+                             "netlist constant. To use {fld}, prefix the name "
+                             "with RndCnst."
+                             .format(where=where, name=name, fld=fld))
+
+    r_type = rd.get('type')
+    if r_type is None:
+        param_type = 'int'
+    else:
+        param_type = check_str(r_type, 'type field of ' + where)
+
+    local = check_bool(rd.get('local', 'true'), 'local field of ' + where)
+    expose = check_bool(rd.get('expose', 'false'), 'expose field of ' + where)
+
+    r_default = rd.get('default')
+    if r_default is None:
+        raise ValueError('At {}, the {} param has no default field.'
+                         .format(where, name))
+    else:
+        default = check_str(r_default, 'default field of ' + where)
+        if param_type[:3] == 'int':
+            check_int(default,
+                      'default field of {}, (an integer parameter)'
+                      .format(name))
+
+    if local:
+        if expose:
+            raise ValueError('At {}, the localparam {} cannot be exposed to '
+                             'the top-level.'
+                             .format(where, name))
+        return LocalParam(name, desc, param_type, value=default)
+    else:
+        return Parameter(name, desc, param_type, default, expose)
+
+
+class Params:
+    def __init__(self) -> None:
+        self.by_name = {}  # type: Dict[str, BaseParam]
+
+    @staticmethod
+    def from_raw(where: str, raw: object) -> 'Params':
+        ret = Params()
+        rl = check_list(raw, where)
+        for idx, r_param in enumerate(rl):
+            entry_where = 'entry {} in {}'.format(idx + 1, where)
+            param = _parse_parameter(entry_where, r_param)
+            if ret.get(param.name) is not None:
+                raise ValueError('At {}, found a duplicate parameter with '
+                                 'name {}.'
+                                 .format(entry_where, param.name))
+            ret.add(param)
+        return ret
+
+    def add(self, param: BaseParam) -> None:
+        assert param.name not in self.by_name
+        self.by_name[param.name] = param
+
+    def get(self, name: str) -> Optional[BaseParam]:
+        return self.by_name.get(name)
+
+    def _apply_default(self, name: str, value: str) -> None:
+        param = self.by_name.get(name)
+        if param is None:
+            raise KeyError('Cannot find parameter '
+                           '{} to set default value.'
+                           .format(name))
+
+        param.apply_default(value)
+
+    def apply_defaults(self, defaults: List[str]) -> None:
+        for idx, entry in enumerate(defaults):
+            tokens = entry.split('=')
+            if len(tokens) != 2:
+                raise ValueError('Entry {} in list of parameter defaults to '
+                                 'apply is {!r}, which is not of the form '
+                                 'param=value.'
+                                 .format(idx, entry))
+            self._apply_default(tokens[0], tokens[1])
+
+    def _expand_one(self, value: str, when: str) -> int:
+        # Check whether value is already an integer: if so, return that.
+        try:
+            return int(value, 0)
+        except ValueError:
+            pass
+
+        param = self.by_name.get(value)
+        if param is None:
+            raise ValueError('Cannot find a parameter called {} when {}. '
+                             'Known parameters: {}.'
+                             .format(value,
+                                     when,
+                                     ', '.join(self.by_name.keys())))
+
+        # Only allow localparams in the expansion (because otherwise we're at
+        # the mercy of whatever instantiates the block).
+        if not isinstance(param, LocalParam):
+            raise ValueError("When {}, {} is a not a local parameter."
+                             .format(when, value))
+
+        return param.expand_value(when)
+
+    def expand(self, value: str, where: str) -> int:
+        # Here, we want to support arithmetic expressions with + and -. We
+        # don't support other operators, or parentheses (so can parse with just
+        # a regex).
+        #
+        # Use re.split, capturing the operators. This turns e.g. "a + b-c" into
+        # ['a ', '+', ' b', '-', 'c']. If there's a leading operator ("+a"),
+        # the first element of the results is an empty string. This means
+        # elements with odd positions are always operators and elements with
+        # even positions are values.
+        acc = 0
+        is_neg = False
+
+        for idx, tok in enumerate(re.split(r'([+-])', value)):
+            if idx == 0 and not tok:
+                continue
+            if idx % 2:
+                is_neg = (tok == '-')
+                continue
+
+            term = self._expand_one(tok.strip(),
+                                    'expanding term {} of {}'
+                                    .format(idx // 2, where))
+            acc += -term if is_neg else term
+
+        return acc
+
+    def as_dicts(self) -> List[Dict[str, object]]:
+        return [p.as_dict() for p in self.by_name.values()]
+
+    def get_localparams(self) -> List[LocalParam]:
+        ret = []
+        for param in self.by_name.values():
+            if isinstance(param, LocalParam):
+                ret.append(param)
+        return ret
diff --git a/util/reggen/reg_block.py b/util/reggen/reg_block.py
index 5b7ebcc..54cf784 100644
--- a/util/reggen/reg_block.py
+++ b/util/reggen/reg_block.py
@@ -9,15 +9,13 @@
 
 from .lib import check_int, check_list, check_str_dict
 from .multi_register import MultiRegister
+from .params import Params
 from .register import Register
 from .window import Window
 
 
 class RegBlock:
-    def __init__(self,
-                 addrsep: int,
-                 reg_width: int,
-                 params: List[Dict[str, object]]):
+    def __init__(self, addrsep: int, reg_width: int, params: Params):
 
         self._addrsep = addrsep
         self._reg_width = reg_width
diff --git a/util/reggen/reg_pkg.sv.tpl b/util/reggen/reg_pkg.sv.tpl
index cef644a..f97b476 100644
--- a/util/reggen/reg_pkg.sv.tpl
+++ b/util/reggen/reg_pkg.sv.tpl
@@ -12,14 +12,15 @@
   flat_regs = block.reg_block.flat_regs
   num_regs = len(flat_regs)
   max_regs_char = len("{}".format(num_regs - 1))
+  localparams = block.params.get_localparams()
 %>\
 package ${block.name}_reg_pkg;
-% if len(block.params) != 0:
+% if localparams:
 
   // Param list
 % endif
-% for param in [p for p in block.params if p["local"] == "true"]:
-  parameter ${param["type"]} ${param["name"]} = ${param["default"]};
+% for param in localparams:
+  parameter ${param.param_type} ${param.name} = ${param.value};
 % endfor
 
   // Address width within the block
diff --git a/util/reggen/register.py b/util/reggen/register.py
index 28ff4fc..8f08ab9 100644
--- a/util/reggen/register.py
+++ b/util/reggen/register.py
@@ -8,6 +8,7 @@
 from .field import Field
 from .lib import (check_keys, check_str, check_name, check_bool,
                   check_list, check_str_list, check_int)
+from .params import Params
 from .reg_base import RegBase
 
 REQUIRED_FIELDS = {
@@ -176,7 +177,7 @@
     @staticmethod
     def from_raw(reg_width: int,
                  offset: int,
-                 params: List[Dict[str, object]],
+                 params: Params,
                  raw: object) -> 'Register':
         rd = check_keys(raw, 'register',
                         list(REQUIRED_FIELDS.keys()),
diff --git a/util/reggen/validate.py b/util/reggen/validate.py
index dd07b86..4389a1b 100644
--- a/util/reggen/validate.py
+++ b/util/reggen/validate.py
@@ -7,10 +7,12 @@
 
 import logging as log
 from collections import OrderedDict
+from typing import List
 
 from .access import SWAccess, HWAccess
 from .bits import Bits
 from .field import Field
+from .params import LocalParam, Params
 from .reg_block import RegBlock
 from .register import Register
 
@@ -99,115 +101,6 @@
     return error
 
 
-def check_lp(obj, x, err_prefix):
-    error = 0
-    if not isinstance(obj[x], list):
-        log.error(err_prefix + ' element ' + x + ' not a list')
-        return 1
-
-    for y in obj[x]:
-        error += check_keys(y, lp_required, lp_optional, {},
-                            err_prefix + ' element ' + x)
-
-        # If this is a random netlist constant, other attributes like local, default and expose
-        # are automatically set. Throw an error if they already exist in the dict.
-        randcount = int(y.setdefault('randcount', "0"))
-        randtype = y.setdefault('randtype', "none")
-        if randtype != "none":
-
-            if randcount <= 0:
-                log.error(err_prefix + ' randwith for parameter ' + y['name'] +
-                          ' must be greater > 0.')
-                return error + 1
-
-            if randtype not in ['perm', 'data']:
-                log.error(err_prefix + ' parameter ' + y['name'] +
-                          ' has unknown randtype ' + randtype)
-                return error + 1
-
-            if y.get('type') is None:
-                log.error(
-                    err_prefix + ' parameter ' + y['name'] +
-                    ' has undefined type. '
-                    'It is required to define the type in the IP package.')
-                return error + 1
-
-            if not y.get('name').lower().startswith('rndcnst'):
-                log.error(
-                    err_prefix + ' parameter ' + y['name'] +
-                    ' is defined as a compile-time '
-                    'random netlist constant. The name must therefore start with RndCnst.'
-                )
-                return error + 1
-
-            overrides = [('local', 'false'), ('default', ''),
-                         ('expose', 'false')]
-
-            for key, value in overrides:
-                if y.setdefault(key, value) != value:
-                    log.error(
-                        err_prefix + ' ' + key + ' for parameter ' +
-                        y['name'] +
-                        ' must not be set since it will be defined automatically.'
-                    )
-                    return error + 1
-
-        # TODO: Check if PascalCase or ALL_CAPS
-        y.setdefault('type', 'int')
-
-        y.setdefault('local', 'true')
-        local, ierr = check_bool(y["local"], err_prefix + " local")
-        if ierr:
-            error += 1
-            y["local"] = "true"
-
-        y.setdefault('expose', 'false')
-        local, ierr = check_bool(y["expose"], err_prefix + " expose")
-        if ierr:
-            error += 1
-            y["expose"] = "false"
-
-        if y["local"] == "true" and y["expose"] == "true":
-            log.error(err_prefix + ' element ' + x + '["' + y["name"] + '"]' +
-                      ' cannot be local and exposed to top level')
-            return error + 1
-
-        if "default" in y:
-            if y["type"][:3] == "int":
-                default, ierr = check_int(y["default"],
-                                          err_prefix + " default")
-                if ierr:
-                    error += 1
-                    y["default"] = "1"
-        elif y["randtype"] != "none":
-            # Don't make assumptions for exposed parameters. These must have
-            # a default.
-            if y["expose"] == "true":
-                log.error(err_prefix + ' element ' + x + '["' + y["name"] +
-                          '"]' + ' has no defined default value')
-            elif y["type"][:3] == "int":
-                y["default"] = "1"
-            elif y["type"] == "string":
-                y["default"] = ""
-            else:
-                log.error(err_prefix + ' element ' + x + '["' + y["name"] +
-                          '"]' + ' type is not supported')
-                return error + 1
-
-    return error
-
-
-def search_param(obj, key):
-    """return the param object if found, else return non zero error
-    """
-    for p in obj:
-        if p["name"] == key:
-            return p, 0
-
-    log.error("Param {} cannot be found".format(key))
-    return None, 1
-
-
 def check_keys(obj, required_keys, optional_keys, added_keys, err_prefix):
     error = 0
     for x in required_keys:
@@ -225,8 +118,6 @@
         if type is not None:
             if type[:2] == 'ln':
                 error += check_ln(obj, x, type == 'lnw', err_prefix)
-            if type == 'lp':
-                error += check_lp(obj, x, err_prefix)
 
     return error
 
@@ -331,21 +222,6 @@
     'width': ['d', "bit width of the item (if not 1)"],
 }
 
-# lp type
-lp_required = {
-    'name': ['s', "name of the item"],
-}
-lp_optional = {
-    'desc': ['s', "description of the item"],
-    'type': ['s', "item type. int by default"],
-    'default': ['s', "item default value"],
-    'local': ['pb', "to be localparam"],
-    'expose': ['pb', "to be exposed to top"],
-    'randcount':
-    ['s', "number of bits to randomize in the parameter. 0 by default."],
-    'randtype': ['s', "type of randomization to perform. none by default"],
-}
-
 # Registers list may have embedded keys
 list_optone = {
     'reserved': ['d', "number of registers to reserve space for"],
@@ -492,20 +368,13 @@
     return alert_regs, 0
 
 
-def validate(regs, **kwargs):
-    if "params" in kwargs:
-        params = kwargs["params"]
-    else:
-        params = []
-
+def validate(regs, params: List[str]):
     if 'name' not in regs:
         log.error("Component has no name. Aborting.")
         return 1
 
     component = regs['name']
 
-    regs.setdefault('param_list', [])
-
     error = check_keys(regs, top_required, top_optional, top_added, component)
     if (error > 0):
         log.error("Component has top level errors. Aborting.")
@@ -529,7 +398,11 @@
     else:
         addrsep = fullwidth // 8
 
-    reg_block = RegBlock(addrsep, fullwidth, regs.get('param_list', []))
+    param_list = Params.from_raw('block parameter list',
+                                 regs.get('param_list', []))
+    regs['param_list'] = param_list
+
+    reg_block = RegBlock(addrsep, fullwidth, param_list)
 
     autoregs = []
 
@@ -592,53 +465,24 @@
                 error += 1
 
         if num_alerts != 0:
-            param = ''
-            for p in regs['param_list']:
-                if p['name'] == 'NumAlerts':
-                    param = p
-            if param:
-                # We already have an NumAlerts parameter.
-                if (param['type'] != 'int' or
-                        param['default'] != str(num_alerts) or
-                        param['local'] != 'true'):
-                    log.error(
-                        'Conflicting definition of NumAlerts parameter found.')
+            existing_param = param_list.get('NumAlerts')
+            if existing_param is not None:
+                if ((not isinstance(existing_param, LocalParam) or
+                     existing_param.param_type != 'int' or
+                     existing_param.value != str(num_alerts))):
+                    log.error('Conflicting definition of NumAlerts parameter.')
                     error += 1
             else:
-                # Generate the NumAlerts parameter.x
-                regs['param_list'].append({
-                    'name': 'NumAlerts',
-                    'type': 'int',
-                    'default': str(num_alerts),
-                    'desc': 'Number of alerts',
-                    'local': 'true',
-                    'expose': 'false',
-                })
+                param_list.add(LocalParam(name='NumAlerts',
+                                          desc='Number of alerts',
+                                          param_type='int',
+                                          value=str(num_alerts)))
 
-    # Change default param value if exists.
-    #   Assumed param list is already validated in above `check_keys` function
-    if "param_list" in regs and len(regs["param_list"]) != 0:
-        for p in params:
-            if p == '':
-                continue
-
-            tokens = p.split('=')
-            if len(tokens) != 2:
-                error += 1
-                log.error("Parameter format isn't correct. {}".format(p))
-            key, value = tokens[0], tokens[1]
-            param, err = search_param(regs["param_list"], key)
-            if err != 0:
-                error += err
-                continue
-
-            value, err = check_int(
-                value, component + " param[{}]".format(param["name"]))
-            if err != 0:
-                error += err
-                continue
-
-            param["default"] = value
+    try:
+        param_list.apply_defaults(params)
+    except (ValueError, KeyError) as err:
+        log.error(str(err))
+        return error + 1
 
     if "scan" in regs:
         scan, err = check_bool(regs["scan"], component + " scan")
diff --git a/util/reggen/window.py b/util/reggen/window.py
index 9a21d5f..114971c 100644
--- a/util/reggen/window.py
+++ b/util/reggen/window.py
@@ -2,11 +2,12 @@
 # Licensed under the Apache License, Version 2.0, see LICENSE for details.
 # SPDX-License-Identifier: Apache-2.0
 
-from typing import Dict, List
+from typing import Dict
 
 from .access import SWAccess
-from .lib import (check_keys, check_str, check_bool, check_int,
-                  expand_parameter)
+from .lib import check_keys, check_str, check_bool, check_int
+from .params import Params
+
 
 REQUIRED_FIELDS = {
     'name': ['s', "name of the window"],
@@ -72,7 +73,7 @@
     @staticmethod
     def from_raw(offset: int,
                  reg_width: int,
-                 params: List[Dict[str, object]],
+                 params: Params,
                  raw: object) -> 'Window':
         rd = check_keys(raw, 'window',
                         list(REQUIRED_FIELDS.keys()),
@@ -100,8 +101,7 @@
                              .format(wind_desc, validbits, reg_width))
 
         r_items = check_str(rd['items'], 'items field for ' + wind_desc)
-        items = expand_parameter(params, r_items,
-                                 'expanding items field for ' + wind_desc)
+        items = params.expand(r_items, 'items field for ' + wind_desc)
         if items <= 0:
             raise ValueError("Items field for {} is {}, "
                              "which isn't positive."
diff --git a/util/regtool.py b/util/regtool.py
index 791f6cb..aec4fd5 100755
--- a/util/regtool.py
+++ b/util/regtool.py
@@ -132,7 +132,7 @@
         format = 'hjson'
 
     infile = args.input
-    params = args.param.split(';')
+    params = args.param.split(';') if args.param else []
 
     # Define either outfile or outdir (but not both), depending on the output
     # format.
diff --git a/util/topgen.py b/util/topgen.py
index ad53b1f..79529b4 100755
--- a/util/topgen.py
+++ b/util/topgen.py
@@ -191,7 +191,7 @@
     hjson_obj = hjson.loads(out,
                             use_decimal=True,
                             object_pairs_hook=validate.checking_dict)
-    validate.validate(hjson_obj)
+    validate.validate(hjson_obj, params=[])
     gen_rtl.gen_rtl(hjson_obj, str(rtl_path))
 
 
@@ -250,7 +250,7 @@
     hjson_obj = hjson.loads(out,
                             use_decimal=True,
                             object_pairs_hook=OrderedDict)
-    validate.validate(hjson_obj)
+    validate.validate(hjson_obj, params=[])
     gen_rtl.gen_rtl(hjson_obj, str(rtl_path))
 
     # Generate RV_PLIC Top Module
@@ -468,7 +468,7 @@
     hjson_obj = hjson.loads(out,
                             use_decimal=True,
                             object_pairs_hook=validate.checking_dict)
-    validate.validate(hjson_obj)
+    validate.validate(hjson_obj, params=[])
     gen_rtl.gen_rtl(hjson_obj, str(rtl_path))
 
 
@@ -573,7 +573,7 @@
         hjson_obj = hjson.load(out,
                                use_decimal=True,
                                object_pairs_hook=OrderedDict)
-    validate.validate(hjson_obj)
+    validate.validate(hjson_obj, params=[])
     gen_rtl.gen_rtl(hjson_obj, str(rtl_path))
 
 
@@ -630,7 +630,7 @@
         hjson_obj = hjson.load(out,
                                use_decimal=True,
                                object_pairs_hook=OrderedDict)
-    validate.validate(hjson_obj)
+    validate.validate(hjson_obj, params=[])
     gen_rtl.gen_rtl(hjson_obj, str(rtl_path))
 
 
@@ -719,7 +719,7 @@
         hjson_obj = hjson.load(out,
                                use_decimal=True,
                                object_pairs_hook=OrderedDict)
-    validate.validate(hjson_obj)
+    validate.validate(hjson_obj, params=[])
     gen_rtl.gen_rtl(hjson_obj, str(rtl_path))
 
 
@@ -778,7 +778,7 @@
         hjson_obj = hjson.load(out,
                                use_decimal=True,
                                object_pairs_hook=OrderedDict)
-    validate.validate(hjson_obj)
+    validate.validate(hjson_obj, params=[])
     gen_rtl.gen_rtl(hjson_obj, str(rtl_path))
 
 
@@ -798,7 +798,7 @@
             hjson_obj = hjson.load(out,
                                    use_decimal=True,
                                    object_pairs_hook=OrderedDict)
-            validate.validate(hjson_obj)
+            validate.validate(hjson_obj, params=[])
             gen_rtl.gen_rtl(hjson_obj, str(genrtl_dir))
 
 
@@ -940,7 +940,7 @@
             obj = hjson.load(hjson_file.open('r'),
                              use_decimal=True,
                              object_pairs_hook=OrderedDict)
-            if validate.validate(obj) != 0:
+            if validate.validate(obj, params=[]) != 0:
                 log.info("Parsing IP %s configuration failed. Skip" % x)
                 continue
             ip_objs.append(obj)
diff --git a/util/topgen/merge.py b/util/topgen/merge.py
index 85aeb29..9cc9fb9 100644
--- a/util/topgen/merge.py
+++ b/util/topgen/merge.py
@@ -10,6 +10,7 @@
 from math import ceil, log2
 
 from topgen import c, lib
+from reggen.params import LocalParam, Parameter, RandParameter
 
 
 def _get_random_data_hex_literal(width):
@@ -118,43 +119,53 @@
             ip_module["available_inout_list"] = []
 
         # param_list
-        if "param_list" in ip:
-            ip_module["param_list"] = deepcopy(ip["param_list"])
-            # Removing local parameters.
-            for i in ip["param_list"]:
-                if i["local"] == "true":
-                    ip_module["param_list"].remove(i)
-            # Checking for security-relevant parameters
-            # that are not exposed, adding a top-level name.
-            for i in ip_module["param_list"]:
-                par_name = i["name"]
-                if par_name.lower().startswith("sec") and not i["expose"]:
-                    log.warning("{} has security-critical parameter {} "
-                                "not exposed to top".format(
-                                    mod_name, par_name))
-                # Move special prefixes to the beginnining of the parameter name.
-                param_prefixes = ["Sec", "RndCnst"]
-                cc_mod_name = c.Name.from_snake_case(mod_name).as_camel_case()
-                for prefix in param_prefixes:
-                    if par_name.lower().startswith(prefix.lower()):
-                        i["name_top"] = prefix + cc_mod_name + par_name[
-                            len(prefix):]
-                        break
-                else:
-                    i["name_top"] = cc_mod_name + par_name
+        new_params = []
+        for param in ip['param_list'].by_name.values():
+            if isinstance(param, LocalParam):
+                # Remove local parameters.
+                continue
 
-                # Generate random bits or permutation, if needed
-                if i["randtype"] == "data":
-                    i["default"] = _get_random_data_hex_literal(i["randcount"])
+            new_param = param.as_dict()
+
+            param_expose = param.expose if isinstance(param, Parameter) else False
+
+            # Check for security-relevant parameters that are not exposed,
+            # adding a top-level name.
+            if param.name.lower().startswith("sec") and not param_expose:
+                log.warning("{} has security-critical parameter {} "
+                            "not exposed to top".format(
+                                mod_name, param.name))
+
+            # Move special prefixes to the beginnining of the parameter name.
+            param_prefixes = ["Sec", "RndCnst"]
+            cc_mod_name = c.Name.from_snake_case(mod_name).as_camel_case()
+            name_top = cc_mod_name + param.name
+            for prefix in param_prefixes:
+                if param.name.lower().startswith(prefix.lower()):
+                    name_top = (prefix + cc_mod_name +
+                                param.name[len(prefix):])
+                    break
+
+            new_param['name_top'] = name_top
+
+            # Generate random bits or permutation, if needed
+            if isinstance(param, RandParameter):
+                if param.randtype == 'data':
+                    new_default = _get_random_data_hex_literal(param.randcount)
                     # Effective width of the random vector
-                    i["randwidth"] = int(i["randcount"])
-                elif i["randtype"] == "perm":
-                    i["default"] = _get_random_perm_hex_literal(i["randcount"])
+                    randwidth = param.randcount
+                else:
+                    assert param.randtype == 'perm'
+                    new_default = _get_random_perm_hex_literal(param.randcount)
                     # Effective width of the random vector
-                    i["randwidth"] = int(i["randcount"]) * int(
-                        ceil(log2(float(i["randcount"]))))
-        else:
-            ip_module["param_list"] = []
+                    randwidth = param.randcount * ceil(log2(param.randcount))
+
+                new_param['default'] = new_default
+                new_param['randwidth'] = randwidth
+
+            new_params.append(new_param)
+
+        ip_module["param_list"] = new_params
 
         # interrupt_list
         if "interrupt_list" in ip: