From 9bf5cd1e6833af41b332b5a4e48337ab34d9133d Mon Sep 17 00:00:00 2001 From: Arnav Sacheti <36746504+arnavsacheti@users.noreply.github.com> Date: Fri, 10 Oct 2025 22:28:36 -0700 Subject: [PATCH] Initial Commit - Forked from PeakRDL-regblock @ a440cc19769069be831d267505da4f3789a26695 --- .github/ISSUE_TEMPLATE/bug_report.md | 26 + .github/ISSUE_TEMPLATE/feature_request.md | 22 + .github/ISSUE_TEMPLATE/question.md | 10 + .github/pull_request_template.md | 11 + .github/workflows/build.yml | 178 ++++++ .gitignore | 19 + .readthedocs.yaml | 17 + CONTRIBUTING.md | 53 ++ LICENSE | 165 +++++ MANIFEST.in | 2 + README.md | 12 + docs/Makefile | 20 + docs/api.rst | 50 ++ docs/architecture.rst | 59 ++ docs/conf.py | 89 +++ docs/configuring.rst | 45 ++ docs/cpuif/apb.rst | 59 ++ docs/cpuif/avalon.rst | 33 + docs/cpuif/axi4lite.rst | 32 + docs/cpuif/customizing.rst | 114 ++++ docs/cpuif/internal_protocol.rst | 232 +++++++ docs/cpuif/introduction.rst | 36 ++ docs/cpuif/passthrough.rst | 10 + docs/dev_notes/Alpha-Beta Versioning | 10 + docs/dev_notes/Hierarchy-and-Indexing | 67 ++ docs/dev_notes/Program Flow | 23 + docs/dev_notes/Resets | 11 + docs/dev_notes/Signal Dereferencer | 22 + docs/dev_notes/Validation Needed | 183 ++++++ .../template-layers/1-port-declaration | 51 ++ .../template-layers/1.1.hardware-interface | 103 ++++ docs/dev_notes/template-layers/2-CPUIF | 72 +++ .../template-layers/3-address-decode | 51 ++ docs/dev_notes/template-layers/4-fields | 163 +++++ docs/dev_notes/template-layers/5-readback-mux | 49 ++ .../template-layers/6-output-port-mapping | 9 + docs/diagrams/arch.png | Bin 0 -> 151692 bytes docs/diagrams/diagrams.odg | Bin 0 -> 19782 bytes docs/diagrams/rbuf.png | Bin 0 -> 124933 bytes docs/diagrams/readback.png | Bin 0 -> 90728 bytes docs/diagrams/wbuf.png | Bin 0 -> 125800 bytes docs/faq.rst | 131 ++++ docs/hwif.rst | 61 ++ docs/img/err.svg | 53 ++ docs/img/ok.svg | 53 ++ docs/img/warn.svg | 53 ++ docs/index.rst | 94 +++ docs/licensing.rst | 50 ++ docs/limitations.rst | 53 ++ docs/props/addrmap.rst | 28 + docs/props/field.rst | 491 +++++++++++++++ docs/props/reg.rst | 14 + docs/props/rhs_props.rst | 182 ++++++ docs/props/signal.rst | 28 + docs/rdl_features/external.rst | 155 +++++ docs/requirements.txt | 3 + docs/udps/extended_swacc.rst | 49 ++ docs/udps/fixedpoint.rst | 103 ++++ docs/udps/intro.rst | 85 +++ docs/udps/read_buffering.rst | 164 +++++ docs/udps/signed.rst | 74 +++ docs/udps/write_buffering.rst | 183 ++++++ hdl-src/README.md | 9 + hdl-src/apb3_intf.sv | 40 ++ hdl-src/apb4_intf.sv | 46 ++ hdl-src/avalon_mm_intf.sv | 46 ++ hdl-src/axi4lite_intf.sv | 80 +++ hdl-src/regblock_udps.rdl | 54 ++ pyproject.toml | 51 ++ src/peakrdl_regblock/__about__.py | 2 + src/peakrdl_regblock/__init__.py | 3 + src/peakrdl_regblock/__peakrdl__.py | 205 +++++++ src/peakrdl_regblock/addr_decode.py | 219 +++++++ src/peakrdl_regblock/cpuif/__init__.py | 1 + src/peakrdl_regblock/cpuif/apb3/__init__.py | 33 + src/peakrdl_regblock/cpuif/apb3/apb3_tmpl.sv | 48 ++ src/peakrdl_regblock/cpuif/apb4/__init__.py | 35 ++ src/peakrdl_regblock/cpuif/apb4/apb4_tmpl.sv | 51 ++ src/peakrdl_regblock/cpuif/avalon/__init__.py | 40 ++ .../cpuif/avalon/avalon_tmpl.sv | 41 ++ .../cpuif/axi4lite/__init__.py | 70 +++ .../cpuif/axi4lite/axi4lite_tmpl.sv | 254 ++++++++ src/peakrdl_regblock/cpuif/base.py | 76 +++ .../cpuif/passthrough/__init__.py | 22 + .../cpuif/passthrough/passthrough_tmpl.sv | 12 + src/peakrdl_regblock/dereferencer.py | 264 ++++++++ src/peakrdl_regblock/exporter.py | 288 +++++++++ src/peakrdl_regblock/external_acks.py | 54 ++ src/peakrdl_regblock/field_logic/__init__.py | 501 +++++++++++++++ src/peakrdl_regblock/field_logic/bases.py | 114 ++++ .../field_logic/generators.py | 393 ++++++++++++ .../field_logic/hw_interrupts.py | 162 +++++ .../field_logic/hw_interrupts_with_write.py | 187 ++++++ .../field_logic/hw_set_clr.py | 72 +++ src/peakrdl_regblock/field_logic/hw_write.py | 95 +++ src/peakrdl_regblock/field_logic/sw_onread.py | 45 ++ .../field_logic/sw_onwrite.py | 129 ++++ .../field_logic/sw_singlepulse.py | 19 + .../field_logic/templates/counter_macros.sv | 48 ++ .../field_logic/templates/external_block.sv | 31 + .../field_logic/templates/external_reg.sv | 46 ++ .../field_logic/templates/field_storage.sv | 86 +++ src/peakrdl_regblock/forloop_generator.py | 98 +++ src/peakrdl_regblock/hwif/__init__.py | 249 ++++++++ src/peakrdl_regblock/hwif/generators.py | 385 ++++++++++++ src/peakrdl_regblock/identifier_filter.py | 52 ++ src/peakrdl_regblock/module_tmpl.sv | 293 +++++++++ src/peakrdl_regblock/package_tmpl.sv | 14 + src/peakrdl_regblock/parity.py | 34 ++ .../read_buffering/__init__.py | 59 ++ .../implementation_generator.py | 59 ++ .../read_buffering/storage_generator.py | 18 + .../read_buffering/template.sv | 5 + src/peakrdl_regblock/readback/__init__.py | 72 +++ src/peakrdl_regblock/readback/generators.py | 381 ++++++++++++ .../readback/templates/readback.sv | 79 +++ src/peakrdl_regblock/scan_design.py | 119 ++++ src/peakrdl_regblock/struct_generator.py | 292 +++++++++ src/peakrdl_regblock/sv_int.py | 17 + src/peakrdl_regblock/udps/__init__.py | 17 + src/peakrdl_regblock/udps/extended_swacc.py | 23 + src/peakrdl_regblock/udps/fixedpoint.py | 73 +++ src/peakrdl_regblock/udps/rw_buffering.py | 130 ++++ src/peakrdl_regblock/udps/signed.py | 33 + src/peakrdl_regblock/utils.py | 104 ++++ src/peakrdl_regblock/validate_design.py | 207 +++++++ .../write_buffering/__init__.py | 81 +++ .../implementation_generator.py | 59 ++ .../write_buffering/storage_generator.py | 32 + .../write_buffering/template.sv | 31 + tests/.coveragerc | 22 + tests/README.md | 119 ++++ tests/__init__.py | 0 tests/conftest.py | 48 ++ tests/lib/__init__.py | 0 tests/lib/base_testcase.py | 140 +++++ tests/lib/cpuifs/__init__.py | 17 + tests/lib/cpuifs/apb3/__init__.py | 18 + tests/lib/cpuifs/apb3/apb3_intf_driver.sv | 116 ++++ tests/lib/cpuifs/apb3/tb_inst.sv | 32 + tests/lib/cpuifs/apb4/__init__.py | 18 + tests/lib/cpuifs/apb4/apb4_intf_driver.sv | 128 ++++ tests/lib/cpuifs/apb4/tb_inst.sv | 36 ++ tests/lib/cpuifs/avalon/__init__.py | 18 + .../cpuifs/avalon/avalon_mm_intf_driver.sv | 138 +++++ tests/lib/cpuifs/avalon/tb_inst.sv | 36 ++ tests/lib/cpuifs/axi4lite/__init__.py | 18 + .../cpuifs/axi4lite/axi4lite_intf_driver.sv | 263 ++++++++ tests/lib/cpuifs/axi4lite/tb_inst.sv | 54 ++ tests/lib/cpuifs/base.py | 87 +++ tests/lib/cpuifs/passthrough/__init__.py | 11 + .../cpuifs/passthrough/passthrough_driver.sv | 123 ++++ tests/lib/cpuifs/passthrough/tb_inst.sv | 32 + tests/lib/external_block.sv | 73 +++ tests/lib/external_reg.sv | 79 +++ tests/lib/sim_testcase.py | 107 ++++ tests/lib/simulators/__init__.py | 39 ++ tests/lib/simulators/base.py | 35 ++ tests/lib/simulators/questa.py | 84 +++ tests/lib/simulators/stub.py | 17 + tests/lib/simulators/xcelium.py | 86 +++ tests/lib/simulators/xilinx.py | 74 +++ tests/lib/sv_line_anchor.py | 10 + tests/lib/synth_testcase.py | 39 ++ tests/lib/synthesizers/__init__.py | 30 + tests/lib/synthesizers/base.py | 17 + tests/lib/synthesizers/vivado.py | 29 + .../synthesizers/vivado_scripts/constr.xdc | 8 + tests/lib/synthesizers/vivado_scripts/run.tcl | 34 ++ tests/lib/tb_base.sv | 128 ++++ tests/lib/test_params.py | 7 + tests/mypy.ini | 7 + tests/pylint.rc | 571 ++++++++++++++++++ tests/pytest.ini | 2 + tests/requirements.txt | 11 + tests/run.sh | 28 + tests/test_bitwise_reduce/__init__.py | 0 tests/test_bitwise_reduce/regblock.rdl | 8 + tests/test_bitwise_reduce/tb_template.sv | 44 ++ tests/test_bitwise_reduce/testcase.py | 5 + tests/test_buffered_swacc_swmod/__init__.py | 0 tests/test_buffered_swacc_swmod/regblock.rdl | 60 ++ .../test_buffered_swacc_swmod/tb_template.sv | 197 ++++++ tests/test_buffered_swacc_swmod/testcase.py | 5 + tests/test_counter_basics/__init__.py | 0 tests/test_counter_basics/regblock.rdl | 69 +++ tests/test_counter_basics/tb_template.sv | 147 +++++ tests/test_counter_basics/testcase.py | 5 + tests/test_counter_saturate/__init__.py | 0 tests/test_counter_saturate/regblock.rdl | 88 +++ tests/test_counter_saturate/tb_template.sv | 214 +++++++ tests/test_counter_saturate/testcase.py | 5 + tests/test_counter_threshold/__init__.py | 0 tests/test_counter_threshold/regblock.rdl | 36 ++ tests/test_counter_threshold/tb_template.sv | 115 ++++ tests/test_counter_threshold/testcase.py | 5 + tests/test_enum/__init__.py | 0 tests/test_enum/regblock.rdl | 12 + tests/test_enum/tb_template.sv | 22 + tests/test_enum/testcase.py | 5 + tests/test_extended_swacc_swmod/__init__.py | 0 tests/test_extended_swacc_swmod/regblock.rdl | 17 + .../test_extended_swacc_swmod/tb_template.sv | 68 +++ tests/test_extended_swacc_swmod/testcase.py | 5 + tests/test_external/__init__.py | 0 tests/test_external/regblock.rdl | 56 ++ tests/test_external/tb_template.sv | 311 ++++++++++ tests/test_external/testcase.py | 29 + tests/test_field_types/__init__.py | 0 tests/test_field_types/regblock.rdl | 64 ++ tests/test_field_types/tb_template.sv | 131 ++++ tests/test_field_types/testcase.py | 5 + tests/test_fixedpoint/__init__.py | 0 tests/test_fixedpoint/regblock.rdl | 39 ++ tests/test_fixedpoint/tb_template.sv | 77 +++ tests/test_fixedpoint/testcase.py | 5 + tests/test_hw_access/__init__.py | 0 tests/test_hw_access/regblock.rdl | 73 +++ tests/test_hw_access/tb_template.sv | 105 ++++ tests/test_hw_access/testcase.py | 5 + tests/test_interrupts/__init__.py | 0 tests/test_interrupts/regblock.rdl | 229 +++++++ tests/test_interrupts/tb_template.sv | 288 +++++++++ tests/test_interrupts/testcase.py | 5 + tests/test_map_size/__init__.py | 0 tests/test_map_size/regblock.rdl | 5 + tests/test_map_size/tb_template.sv | 12 + tests/test_map_size/testcase.py | 5 + tests/test_onread_onwrite/__init__.py | 0 tests/test_onread_onwrite/regblock.rdl | 61 ++ tests/test_onread_onwrite/tb_template.sv | 30 + tests/test_onread_onwrite/testcase.py | 5 + tests/test_parity/__init__.py | 0 tests/test_parity/regblock.rdl | 14 + tests/test_parity/tb_template.sv | 37 ++ tests/test_parity/testcase.py | 5 + tests/test_pipelined_cpuif/__init__.py | 0 tests/test_pipelined_cpuif/regblock.rdl | 8 + tests/test_pipelined_cpuif/tb_template.sv | 50 ++ tests/test_pipelined_cpuif/testcase.py | 14 + tests/test_pkg_params/__init__.py | 0 tests/test_pkg_params/regblock.rdl | 11 + tests/test_pkg_params/tb_template.sv | 8 + tests/test_pkg_params/testcase.py | 27 + tests/test_precedence/__init__.py | 0 tests/test_precedence/regblock.rdl | 26 + tests/test_precedence/tb_template.sv | 26 + tests/test_precedence/testcase.py | 5 + tests/test_read_buffer/__init__.py | 0 tests/test_read_buffer/regblock.rdl | 117 ++++ tests/test_read_buffer/tb_template.sv | 146 +++++ tests/test_read_buffer/testcase.py | 5 + tests/test_read_fanin/__init__.py | 0 tests/test_read_fanin/regblock.rdl | 10 + tests/test_read_fanin/tb_template.sv | 34 ++ tests/test_read_fanin/testcase.py | 37 ++ tests/test_reset_signals/__init__.py | 0 tests/test_reset_signals/regblock.rdl | 80 +++ tests/test_reset_signals/tb_template.sv | 128 ++++ tests/test_reset_signals/testcase.py | 5 + tests/test_singlepulse/__init__.py | 0 tests/test_singlepulse/regblock.rdl | 8 + tests/test_singlepulse/tb_template.sv | 56 ++ tests/test_singlepulse/testcase.py | 5 + tests/test_structural_sw_rw/__init__.py | 0 tests/test_structural_sw_rw/regblock.rdl | 44 ++ tests/test_structural_sw_rw/tb_template.sv | 80 +++ tests/test_structural_sw_rw/testcase.py | 51 ++ tests/test_swacc_swmod/__init__.py | 0 tests/test_swacc_swmod/regblock.rdl | 43 ++ tests/test_swacc_swmod/tb_template.sv | 198 ++++++ tests/test_swacc_swmod/testcase.py | 5 + tests/test_swwe/__init__.py | 0 tests/test_swwe/regblock.rdl | 66 ++ tests/test_swwe/tb_template.sv | 63 ++ tests/test_swwe/testcase.py | 5 + tests/test_user_cpuif/__init__.py | 0 tests/test_user_cpuif/regblock.rdl | 7 + tests/test_user_cpuif/testcase.py | 49 ++ tests/test_user_cpuif/user_apb3_tmpl.sv | 1 + tests/test_validation_errors/__init__.py | 0 tests/test_validation_errors/external_ref.rdl | 15 + .../fixedpoint_counter.rdl | 9 + .../fixedpoint_enum.rdl | 15 + .../fixedpoint_inconsistent_width.rdl | 9 + .../inconsistent_accesswidth.rdl | 9 + .../multiple_unconditional_assigns.rdl | 40 ++ tests/test_validation_errors/sharedextbus.rdl | 7 + .../test_validation_errors/signed_counter.rdl | 14 + tests/test_validation_errors/signed_enum.rdl | 20 + tests/test_validation_errors/testcase.py | 121 ++++ .../test_validation_errors/unaligned_reg.rdl | 8 + .../unaligned_stride.rdl | 8 + .../unbuffered_wide_fields.rdl | 21 + .../test_validation_errors/unsynth_reset1.rdl | 19 + .../test_validation_errors/unsynth_reset2.rdl | 13 + tests/test_wide_regs/__init__.py | 0 tests/test_wide_regs/regblock.rdl | 111 ++++ tests/test_wide_regs/tb_template.sv | 121 ++++ tests/test_wide_regs/testcase.py | 5 + tests/test_write_buffer/__init__.py | 0 tests/test_write_buffer/regblock.rdl | 106 ++++ tests/test_write_buffer/tb_template.sv | 303 ++++++++++ tests/test_write_buffer/testcase.py | 8 + tests/test_write_strobes/__init__.py | 0 tests/test_write_strobes/regblock.rdl | 54 ++ tests/test_write_strobes/tb_template.sv | 31 + tests/test_write_strobes/testcase.py | 9 + 308 files changed, 19414 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/question.md create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 .readthedocs.yaml create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.md create mode 100644 docs/Makefile create mode 100644 docs/api.rst create mode 100644 docs/architecture.rst create mode 100644 docs/conf.py create mode 100644 docs/configuring.rst create mode 100644 docs/cpuif/apb.rst create mode 100644 docs/cpuif/avalon.rst create mode 100644 docs/cpuif/axi4lite.rst create mode 100644 docs/cpuif/customizing.rst create mode 100644 docs/cpuif/internal_protocol.rst create mode 100644 docs/cpuif/introduction.rst create mode 100644 docs/cpuif/passthrough.rst create mode 100644 docs/dev_notes/Alpha-Beta Versioning create mode 100644 docs/dev_notes/Hierarchy-and-Indexing create mode 100644 docs/dev_notes/Program Flow create mode 100644 docs/dev_notes/Resets create mode 100644 docs/dev_notes/Signal Dereferencer create mode 100644 docs/dev_notes/Validation Needed create mode 100644 docs/dev_notes/template-layers/1-port-declaration create mode 100644 docs/dev_notes/template-layers/1.1.hardware-interface create mode 100644 docs/dev_notes/template-layers/2-CPUIF create mode 100644 docs/dev_notes/template-layers/3-address-decode create mode 100644 docs/dev_notes/template-layers/4-fields create mode 100644 docs/dev_notes/template-layers/5-readback-mux create mode 100644 docs/dev_notes/template-layers/6-output-port-mapping create mode 100644 docs/diagrams/arch.png create mode 100644 docs/diagrams/diagrams.odg create mode 100644 docs/diagrams/rbuf.png create mode 100644 docs/diagrams/readback.png create mode 100644 docs/diagrams/wbuf.png create mode 100644 docs/faq.rst create mode 100644 docs/hwif.rst create mode 100644 docs/img/err.svg create mode 100644 docs/img/ok.svg create mode 100644 docs/img/warn.svg create mode 100644 docs/index.rst create mode 100644 docs/licensing.rst create mode 100644 docs/limitations.rst create mode 100644 docs/props/addrmap.rst create mode 100644 docs/props/field.rst create mode 100644 docs/props/reg.rst create mode 100644 docs/props/rhs_props.rst create mode 100644 docs/props/signal.rst create mode 100644 docs/rdl_features/external.rst create mode 100644 docs/requirements.txt create mode 100644 docs/udps/extended_swacc.rst create mode 100644 docs/udps/fixedpoint.rst create mode 100644 docs/udps/intro.rst create mode 100644 docs/udps/read_buffering.rst create mode 100644 docs/udps/signed.rst create mode 100644 docs/udps/write_buffering.rst create mode 100644 hdl-src/README.md create mode 100644 hdl-src/apb3_intf.sv create mode 100644 hdl-src/apb4_intf.sv create mode 100644 hdl-src/avalon_mm_intf.sv create mode 100644 hdl-src/axi4lite_intf.sv create mode 100644 hdl-src/regblock_udps.rdl create mode 100644 pyproject.toml create mode 100644 src/peakrdl_regblock/__about__.py create mode 100644 src/peakrdl_regblock/__init__.py create mode 100644 src/peakrdl_regblock/__peakrdl__.py create mode 100644 src/peakrdl_regblock/addr_decode.py create mode 100644 src/peakrdl_regblock/cpuif/__init__.py create mode 100644 src/peakrdl_regblock/cpuif/apb3/__init__.py create mode 100644 src/peakrdl_regblock/cpuif/apb3/apb3_tmpl.sv create mode 100644 src/peakrdl_regblock/cpuif/apb4/__init__.py create mode 100644 src/peakrdl_regblock/cpuif/apb4/apb4_tmpl.sv create mode 100644 src/peakrdl_regblock/cpuif/avalon/__init__.py create mode 100644 src/peakrdl_regblock/cpuif/avalon/avalon_tmpl.sv create mode 100644 src/peakrdl_regblock/cpuif/axi4lite/__init__.py create mode 100644 src/peakrdl_regblock/cpuif/axi4lite/axi4lite_tmpl.sv create mode 100644 src/peakrdl_regblock/cpuif/base.py create mode 100644 src/peakrdl_regblock/cpuif/passthrough/__init__.py create mode 100644 src/peakrdl_regblock/cpuif/passthrough/passthrough_tmpl.sv create mode 100644 src/peakrdl_regblock/dereferencer.py create mode 100644 src/peakrdl_regblock/exporter.py create mode 100644 src/peakrdl_regblock/external_acks.py create mode 100644 src/peakrdl_regblock/field_logic/__init__.py create mode 100644 src/peakrdl_regblock/field_logic/bases.py create mode 100644 src/peakrdl_regblock/field_logic/generators.py create mode 100644 src/peakrdl_regblock/field_logic/hw_interrupts.py create mode 100644 src/peakrdl_regblock/field_logic/hw_interrupts_with_write.py create mode 100644 src/peakrdl_regblock/field_logic/hw_set_clr.py create mode 100644 src/peakrdl_regblock/field_logic/hw_write.py create mode 100644 src/peakrdl_regblock/field_logic/sw_onread.py create mode 100644 src/peakrdl_regblock/field_logic/sw_onwrite.py create mode 100644 src/peakrdl_regblock/field_logic/sw_singlepulse.py create mode 100644 src/peakrdl_regblock/field_logic/templates/counter_macros.sv create mode 100644 src/peakrdl_regblock/field_logic/templates/external_block.sv create mode 100644 src/peakrdl_regblock/field_logic/templates/external_reg.sv create mode 100644 src/peakrdl_regblock/field_logic/templates/field_storage.sv create mode 100644 src/peakrdl_regblock/forloop_generator.py create mode 100644 src/peakrdl_regblock/hwif/__init__.py create mode 100644 src/peakrdl_regblock/hwif/generators.py create mode 100644 src/peakrdl_regblock/identifier_filter.py create mode 100644 src/peakrdl_regblock/module_tmpl.sv create mode 100644 src/peakrdl_regblock/package_tmpl.sv create mode 100644 src/peakrdl_regblock/parity.py create mode 100644 src/peakrdl_regblock/read_buffering/__init__.py create mode 100644 src/peakrdl_regblock/read_buffering/implementation_generator.py create mode 100644 src/peakrdl_regblock/read_buffering/storage_generator.py create mode 100644 src/peakrdl_regblock/read_buffering/template.sv create mode 100644 src/peakrdl_regblock/readback/__init__.py create mode 100644 src/peakrdl_regblock/readback/generators.py create mode 100644 src/peakrdl_regblock/readback/templates/readback.sv create mode 100644 src/peakrdl_regblock/scan_design.py create mode 100644 src/peakrdl_regblock/struct_generator.py create mode 100644 src/peakrdl_regblock/sv_int.py create mode 100644 src/peakrdl_regblock/udps/__init__.py create mode 100644 src/peakrdl_regblock/udps/extended_swacc.py create mode 100644 src/peakrdl_regblock/udps/fixedpoint.py create mode 100644 src/peakrdl_regblock/udps/rw_buffering.py create mode 100644 src/peakrdl_regblock/udps/signed.py create mode 100644 src/peakrdl_regblock/utils.py create mode 100644 src/peakrdl_regblock/validate_design.py create mode 100644 src/peakrdl_regblock/write_buffering/__init__.py create mode 100644 src/peakrdl_regblock/write_buffering/implementation_generator.py create mode 100644 src/peakrdl_regblock/write_buffering/storage_generator.py create mode 100644 src/peakrdl_regblock/write_buffering/template.sv create mode 100644 tests/.coveragerc create mode 100644 tests/README.md create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/lib/__init__.py create mode 100644 tests/lib/base_testcase.py create mode 100644 tests/lib/cpuifs/__init__.py create mode 100644 tests/lib/cpuifs/apb3/__init__.py create mode 100644 tests/lib/cpuifs/apb3/apb3_intf_driver.sv create mode 100644 tests/lib/cpuifs/apb3/tb_inst.sv create mode 100644 tests/lib/cpuifs/apb4/__init__.py create mode 100644 tests/lib/cpuifs/apb4/apb4_intf_driver.sv create mode 100644 tests/lib/cpuifs/apb4/tb_inst.sv create mode 100644 tests/lib/cpuifs/avalon/__init__.py create mode 100644 tests/lib/cpuifs/avalon/avalon_mm_intf_driver.sv create mode 100644 tests/lib/cpuifs/avalon/tb_inst.sv create mode 100644 tests/lib/cpuifs/axi4lite/__init__.py create mode 100644 tests/lib/cpuifs/axi4lite/axi4lite_intf_driver.sv create mode 100644 tests/lib/cpuifs/axi4lite/tb_inst.sv create mode 100644 tests/lib/cpuifs/base.py create mode 100644 tests/lib/cpuifs/passthrough/__init__.py create mode 100644 tests/lib/cpuifs/passthrough/passthrough_driver.sv create mode 100644 tests/lib/cpuifs/passthrough/tb_inst.sv create mode 100644 tests/lib/external_block.sv create mode 100644 tests/lib/external_reg.sv create mode 100644 tests/lib/sim_testcase.py create mode 100644 tests/lib/simulators/__init__.py create mode 100644 tests/lib/simulators/base.py create mode 100644 tests/lib/simulators/questa.py create mode 100644 tests/lib/simulators/stub.py create mode 100644 tests/lib/simulators/xcelium.py create mode 100644 tests/lib/simulators/xilinx.py create mode 100644 tests/lib/sv_line_anchor.py create mode 100644 tests/lib/synth_testcase.py create mode 100644 tests/lib/synthesizers/__init__.py create mode 100644 tests/lib/synthesizers/base.py create mode 100644 tests/lib/synthesizers/vivado.py create mode 100644 tests/lib/synthesizers/vivado_scripts/constr.xdc create mode 100644 tests/lib/synthesizers/vivado_scripts/run.tcl create mode 100644 tests/lib/tb_base.sv create mode 100644 tests/lib/test_params.py create mode 100644 tests/mypy.ini create mode 100644 tests/pylint.rc create mode 100644 tests/pytest.ini create mode 100644 tests/requirements.txt create mode 100755 tests/run.sh create mode 100644 tests/test_bitwise_reduce/__init__.py create mode 100644 tests/test_bitwise_reduce/regblock.rdl create mode 100644 tests/test_bitwise_reduce/tb_template.sv create mode 100644 tests/test_bitwise_reduce/testcase.py create mode 100644 tests/test_buffered_swacc_swmod/__init__.py create mode 100644 tests/test_buffered_swacc_swmod/regblock.rdl create mode 100644 tests/test_buffered_swacc_swmod/tb_template.sv create mode 100644 tests/test_buffered_swacc_swmod/testcase.py create mode 100644 tests/test_counter_basics/__init__.py create mode 100644 tests/test_counter_basics/regblock.rdl create mode 100644 tests/test_counter_basics/tb_template.sv create mode 100644 tests/test_counter_basics/testcase.py create mode 100644 tests/test_counter_saturate/__init__.py create mode 100644 tests/test_counter_saturate/regblock.rdl create mode 100644 tests/test_counter_saturate/tb_template.sv create mode 100644 tests/test_counter_saturate/testcase.py create mode 100644 tests/test_counter_threshold/__init__.py create mode 100644 tests/test_counter_threshold/regblock.rdl create mode 100644 tests/test_counter_threshold/tb_template.sv create mode 100644 tests/test_counter_threshold/testcase.py create mode 100644 tests/test_enum/__init__.py create mode 100644 tests/test_enum/regblock.rdl create mode 100644 tests/test_enum/tb_template.sv create mode 100644 tests/test_enum/testcase.py create mode 100644 tests/test_extended_swacc_swmod/__init__.py create mode 100644 tests/test_extended_swacc_swmod/regblock.rdl create mode 100644 tests/test_extended_swacc_swmod/tb_template.sv create mode 100644 tests/test_extended_swacc_swmod/testcase.py create mode 100644 tests/test_external/__init__.py create mode 100644 tests/test_external/regblock.rdl create mode 100644 tests/test_external/tb_template.sv create mode 100644 tests/test_external/testcase.py create mode 100644 tests/test_field_types/__init__.py create mode 100644 tests/test_field_types/regblock.rdl create mode 100644 tests/test_field_types/tb_template.sv create mode 100644 tests/test_field_types/testcase.py create mode 100644 tests/test_fixedpoint/__init__.py create mode 100644 tests/test_fixedpoint/regblock.rdl create mode 100644 tests/test_fixedpoint/tb_template.sv create mode 100644 tests/test_fixedpoint/testcase.py create mode 100644 tests/test_hw_access/__init__.py create mode 100644 tests/test_hw_access/regblock.rdl create mode 100644 tests/test_hw_access/tb_template.sv create mode 100644 tests/test_hw_access/testcase.py create mode 100644 tests/test_interrupts/__init__.py create mode 100644 tests/test_interrupts/regblock.rdl create mode 100644 tests/test_interrupts/tb_template.sv create mode 100644 tests/test_interrupts/testcase.py create mode 100644 tests/test_map_size/__init__.py create mode 100644 tests/test_map_size/regblock.rdl create mode 100644 tests/test_map_size/tb_template.sv create mode 100644 tests/test_map_size/testcase.py create mode 100644 tests/test_onread_onwrite/__init__.py create mode 100644 tests/test_onread_onwrite/regblock.rdl create mode 100644 tests/test_onread_onwrite/tb_template.sv create mode 100644 tests/test_onread_onwrite/testcase.py create mode 100644 tests/test_parity/__init__.py create mode 100644 tests/test_parity/regblock.rdl create mode 100644 tests/test_parity/tb_template.sv create mode 100644 tests/test_parity/testcase.py create mode 100644 tests/test_pipelined_cpuif/__init__.py create mode 100644 tests/test_pipelined_cpuif/regblock.rdl create mode 100644 tests/test_pipelined_cpuif/tb_template.sv create mode 100644 tests/test_pipelined_cpuif/testcase.py create mode 100644 tests/test_pkg_params/__init__.py create mode 100644 tests/test_pkg_params/regblock.rdl create mode 100644 tests/test_pkg_params/tb_template.sv create mode 100644 tests/test_pkg_params/testcase.py create mode 100644 tests/test_precedence/__init__.py create mode 100644 tests/test_precedence/regblock.rdl create mode 100644 tests/test_precedence/tb_template.sv create mode 100644 tests/test_precedence/testcase.py create mode 100644 tests/test_read_buffer/__init__.py create mode 100644 tests/test_read_buffer/regblock.rdl create mode 100644 tests/test_read_buffer/tb_template.sv create mode 100644 tests/test_read_buffer/testcase.py create mode 100644 tests/test_read_fanin/__init__.py create mode 100644 tests/test_read_fanin/regblock.rdl create mode 100644 tests/test_read_fanin/tb_template.sv create mode 100644 tests/test_read_fanin/testcase.py create mode 100644 tests/test_reset_signals/__init__.py create mode 100644 tests/test_reset_signals/regblock.rdl create mode 100644 tests/test_reset_signals/tb_template.sv create mode 100644 tests/test_reset_signals/testcase.py create mode 100644 tests/test_singlepulse/__init__.py create mode 100644 tests/test_singlepulse/regblock.rdl create mode 100644 tests/test_singlepulse/tb_template.sv create mode 100644 tests/test_singlepulse/testcase.py create mode 100644 tests/test_structural_sw_rw/__init__.py create mode 100644 tests/test_structural_sw_rw/regblock.rdl create mode 100644 tests/test_structural_sw_rw/tb_template.sv create mode 100644 tests/test_structural_sw_rw/testcase.py create mode 100644 tests/test_swacc_swmod/__init__.py create mode 100644 tests/test_swacc_swmod/regblock.rdl create mode 100644 tests/test_swacc_swmod/tb_template.sv create mode 100644 tests/test_swacc_swmod/testcase.py create mode 100644 tests/test_swwe/__init__.py create mode 100644 tests/test_swwe/regblock.rdl create mode 100644 tests/test_swwe/tb_template.sv create mode 100644 tests/test_swwe/testcase.py create mode 100644 tests/test_user_cpuif/__init__.py create mode 100644 tests/test_user_cpuif/regblock.rdl create mode 100644 tests/test_user_cpuif/testcase.py create mode 100644 tests/test_user_cpuif/user_apb3_tmpl.sv create mode 100644 tests/test_validation_errors/__init__.py create mode 100644 tests/test_validation_errors/external_ref.rdl create mode 100644 tests/test_validation_errors/fixedpoint_counter.rdl create mode 100644 tests/test_validation_errors/fixedpoint_enum.rdl create mode 100644 tests/test_validation_errors/fixedpoint_inconsistent_width.rdl create mode 100644 tests/test_validation_errors/inconsistent_accesswidth.rdl create mode 100644 tests/test_validation_errors/multiple_unconditional_assigns.rdl create mode 100644 tests/test_validation_errors/sharedextbus.rdl create mode 100644 tests/test_validation_errors/signed_counter.rdl create mode 100644 tests/test_validation_errors/signed_enum.rdl create mode 100644 tests/test_validation_errors/testcase.py create mode 100644 tests/test_validation_errors/unaligned_reg.rdl create mode 100644 tests/test_validation_errors/unaligned_stride.rdl create mode 100644 tests/test_validation_errors/unbuffered_wide_fields.rdl create mode 100644 tests/test_validation_errors/unsynth_reset1.rdl create mode 100644 tests/test_validation_errors/unsynth_reset2.rdl create mode 100644 tests/test_wide_regs/__init__.py create mode 100644 tests/test_wide_regs/regblock.rdl create mode 100644 tests/test_wide_regs/tb_template.sv create mode 100644 tests/test_wide_regs/testcase.py create mode 100644 tests/test_write_buffer/__init__.py create mode 100644 tests/test_write_buffer/regblock.rdl create mode 100644 tests/test_write_buffer/tb_template.sv create mode 100644 tests/test_write_buffer/testcase.py create mode 100644 tests/test_write_strobes/__init__.py create mode 100644 tests/test_write_strobes/regblock.rdl create mode 100644 tests/test_write_strobes/tb_template.sv create mode 100644 tests/test_write_strobes/testcase.py diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..18f9e56 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,26 @@ +--- +name: Bug report +about: The tool is not doing what I expected +title: "[BUG]" +labels: '' +assignees: '' + +--- + +- [ ] I have reviewed this project's [contribution guidelines](https://github.com/SystemRDL/PeakRDL-regblock/blob/main/CONTRIBUTING.md) + +**Describe the bug** +A clear and concise description of what the bug is. + +Details like these can be helpful: +* Sample SystemRDL code +* Error message, simulation waveform, etc. +* Version numbers for the tool, Python, and OS + + +**Expected behavior** +A clear and concise description of what you expected to happen. + + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..34f370a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,22 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[FEATURE]" +labels: '' +assignees: '' + +--- + +- [ ] I have reviewed this project's [contribution guidelines](https://github.com/SystemRDL/PeakRDL-regblock/blob/main/CONTRIBUTING.md) + +**Describe the problem/limitation you think should be addressed** +A clear and concise description of what the problem is. + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..1f7917d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,10 @@ +--- +name: Question +about: I have a question +title: '' +labels: '' +assignees: '' + +--- + +Please consider using the discussion board for more open-ended questions: https://github.com/orgs/SystemRDL/discussions diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..6f0db28 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,11 @@ +# Description of change + +Describe what bug or feature your pull request addresses. +If applicable, provide a link to the relevant issue ticket or discussion about +this change. + +# Checklist + +- [ ] I have reviewed this project's [contribution guidelines](https://github.com/SystemRDL/PeakRDL-regblock/blob/main/CONTRIBUTING.md) +- [ ] This change has been tested and does not break any of the existing unit tests. (if unable to run the tests, let us know) +- [ ] If this change adds new features, I have added new unit tests that cover them. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..1111ec1 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,178 @@ +name: build + +on: + push: + branches: + - main + - 'dev/**' + pull_request: + branches: [ main ] + release: + types: + - published + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + test: + strategy: + matrix: + python-version: + - "3.7" + - "3.8" + - "3.9" + - "3.10" + - "3.11" + - "3.12" + include: + - os: ubuntu-latest + + # older versions need older OS + - python-version: "3.7" + os: ubuntu-22.04 + + - python-version: "3.8" + os: ubuntu-22.04 + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install -r tests/requirements.txt + + - name: Install + run: | + python -m pip install ".[cli]" + + - name: Test + run: | + cd tests + pytest --cov=peakrdl_regblock --synth-tool skip --sim-tool stub + + - name: Coveralls + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERALLS_PARALLEL: true + run: | + cd tests + coveralls --service=github + + finish_coveralls: + needs: test + runs-on: ubuntu-latest + steps: + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: 3.x + + - name: Coveralls + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERALLS_PARALLEL: true + run: | + python -m pip install coveralls>=3.0.0 + coveralls --service=github --finish + +#------------------------------------------------------------------------------- + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install dependencies + run: | + python -m pip install -r tests/requirements.txt + + - name: Install + run: | + python -m pip install ".[cli]" + + - name: Run Lint + run: | + pylint --rcfile tests/pylint.rc peakrdl_regblock + +#------------------------------------------------------------------------------- + mypy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install dependencies + run: | + python -m pip install -r tests/requirements.txt + + - name: Install + run: | + python -m pip install ".[cli]" + + - name: Type Check + run: | + mypy --config-file tests/mypy.ini src/peakrdl_regblock + +#------------------------------------------------------------------------------- + build: + needs: + - test + - lint + - mypy + name: Build distributions + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-python@v4 + name: Install Python + with: + python-version: "3.10" + + - name: Install dependencies + run: | + python -m pip install build + + - name: Build sdist + run: python -m build + + - uses: actions/upload-artifact@v4 + with: + name: dist + path: | + dist/*.tar.gz + dist/*.whl + +#------------------------------------------------------------------------------- + deploy: + needs: + - build + + runs-on: ubuntu-latest + environment: release + permissions: + id-token: write + + # Only publish when a GitHub Release is created. + if: github.event_name == 'release' + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist + + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac5e70c --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +**/__pycache__ +**/.vscode +**/.venv +**/.coverage +**/*.rpt +**/.pytest_cache +**/_build +**/*.out +**/transcript +**/htmlcov +**/*.log +**/*.pb +**/.Xil +**/.coverage.* + +build/ +dist/ +*.egg-info/ +.eggs/ diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..354c013 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,17 @@ +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +sphinx: + configuration: docs/conf.py + +python: + install: + - requirements: docs/requirements.txt + - method: pip + path: . diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b82b0d8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,53 @@ +# Contributing to the PeakRDL-regblock code generator +We love your input! We want to make contributing to this project as easy and +transparent as possible, whether it's: + +- Reporting a bug +- Discussing the current state of the code +- Submitting a fix +- Proposing new features +- Becoming a maintainer + + +## Open an issue using the [Issue Tracker](https://github.com/SystemRDL/PeakRDL-regblock/issues) +Talking to us is the easiest way to contribute! Report a bug or feature request by +[opening a new issue](https://github.com/SystemRDL/PeakRDL-regblock/issues). + +Issue submission expectations: +* Please keep each issue submission limited to one topic. This helps us stay organized. +* Before opening an issue, check if one already exists for your topic. It may have already been discussed. +* If submitting a bug, provide enough details so we can reproduce it on our end. (version number, example SystemRDL, etc...) +* If submitting a feature request, please make sure ... + * ... it does not violate the semantics of the SystemRDL standard. + Submissions that would change the interpretation of the SystemRDL language + and are not faithful to the [Accellera SystemRDL specification](http://accellera.org/downloads/standards/systemrdl) will be rejected. + Additional notes on the spec's interpretation can be found in [our unofficial errata page](https://systemrdl-compiler.readthedocs.io/en/latest/dev_notes/rdl_spec_errata.html). +* Please be patient! This project is run by volunteers that are passionate about + improving the state of register automation. Much of the work is done in their free time. + + +## Contribute code using a pull request +Pull requests are the best way to propose changes to the codebase. We actively +welcome your pull requests. To maximize the chance of your pull request getting accepted, +please review the expectations below. + +Pull request expectations: +* Before starting a pull request, please consider discussing the change with us + first by **opening an issue ticket**. Unfortunately many of the PRs that get rejected + are because they implement changes that do not align with the mission of this + compiler project. +* PRs shall only contain only one feature/bug/concept change. **Bulk PRs that change numerous unrelated things will be rejected**. +* Your PR should provide proof that it works correctly and does not break the existing unit tests. +* Use meaningful commit messages, squash commits as appropriate. + +How to submit a PR: +1. Fork the repo and create your feature/bugfix branch from `main`. +2. If you've added code that should be tested, add tests. +3. Ensure the test suite passes. +4. Submit the pull request! + + +## Any contributions you make will be under the GNU LGPL-3.0 Software License +In short, when you submit code changes, your submissions are understood to be +under the same [LGPL-3.0 License](https://choosealicense.com/licenses/lgpl-3.0/) that +covers this project. Feel free to contact the maintainers if that's a concern. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2a7f2ad --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..eafe2d0 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +recursive-include src/peakrdl_regblock *.sv +prune tests diff --git a/README.md b/README.md new file mode 100644 index 0000000..8b554c1 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +[![Documentation Status](https://readthedocs.org/projects/peakrdl-regblock/badge/?version=latest)](http://peakrdl-regblock.readthedocs.io) +[![build](https://github.com/SystemRDL/PeakRDL-regblock/workflows/build/badge.svg)](https://github.com/SystemRDL/PeakRDL-regblock/actions?query=workflow%3Abuild+branch%3Amain) +[![Coverage Status](https://coveralls.io/repos/github/SystemRDL/PeakRDL-regblock/badge.svg?branch=main)](https://coveralls.io/github/SystemRDL/PeakRDL-regblock?branch=main) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/peakrdl-regblock.svg)](https://pypi.org/project/peakrdl-regblock) + +# PeakRDL-regblock +Compile SystemRDL into a SystemVerilog control/status register (CSR) block. + +For the command line tool, see the [PeakRDL project](https://peakrdl.readthedocs.io). + +## Documentation +See the [PeakRDL-regblock Documentation](https://peakrdl-regblock.readthedocs.io) for more details diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..69b809d --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,50 @@ +Exporter API +============ + +If you are not using the `PeakRDL command-line tool `_, +you can still generate regblocks programmatically using the exporter API: + +.. autoclass:: peakrdl_regblock.RegblockExporter + :members: + +Example +------- +Below is a simple example that demonstrates how to generate a SystemVerilog +implementation from SystemRDL source. + +.. code-block:: python + :emphasize-lines: 2-4, 29-33 + + from systemrdl import RDLCompiler, RDLCompileError + from peakrdl_regblock import RegblockExporter + from peakrdl_regblock.cpuif.axi4lite import AXI4Lite_Cpuif + from peakrdl_regblock.udps import ALL_UDPS + + input_files = [ + "PATH/TO/my_register_block.rdl" + ] + + # Create an instance of the compiler + rdlc = RDLCompiler() + + # Register all UDPs that 'regblock' requires + for udp in ALL_UDPS: + rdlc.register_udp(udp) + + try: + # Compile your RDL files + for input_file in input_files: + rdlc.compile_file(input_file) + + # Elaborate the design + root = rdlc.elaborate() + except RDLCompileError: + # A compilation error occurred. Exit with error code + sys.exit(1) + + # Export a SystemVerilog implementation + exporter = RegblockExporter() + exporter.export( + root, "path/to/output_dir", + cpuif_cls=AXI4Lite_Cpuif + ) diff --git a/docs/architecture.rst b/docs/architecture.rst new file mode 100644 index 0000000..daa88f8 --- /dev/null +++ b/docs/architecture.rst @@ -0,0 +1,59 @@ +Register Block Architecture +=========================== + +The generated register block RTL is organized into several sections. +Each section is automatically generated based on the source register model and +is rendered into the output register block SystemVerilog RTL. + +.. figure:: diagrams/arch.png + +Although it is not completely necessary to know the inner workings of the +generated RTL, it can be helpful to understand the implications of various +exporter configuration options. + + +CPU Interface +------------- +The CPU interface logic layer provides an abstraction between the +application-specific bus protocol and the internal register file logic. +This logic layer normalizes external CPU read & write transactions into a common +:ref:`cpuif_protocol` that is used to interact with the register file. + + +Address Decode +-------------- +A common address decode operation is generated which computes individual access +strobes for each software-accessible register in the design. +This operation is performed completely combinationally. + + +Field Logic +----------- +This layer of the register block implements the storage elements and state-change +logic for every field in the design. Field state is updated based on address +decode strobes from software read/write actions, as well as events from the +hardware interface input struct. +This section also assigns any hardware interface outputs. + + +Readback +-------- +The readback layer aggregates and reduces all readable registers into a single +read response. During a read operation, the same address decode strobes are used +to select the active register that is being accessed. +This allows for a simple OR-reduction operation to be used to compute the read +data response. + +For designs with a large number of software-readable registers, an optional +fanin re-timing stage can be enabled. This stage is automatically inserted at a +balanced point in the read-data reduction so that fanin and logic-levels are +optimally reduced. + +.. figure:: diagrams/readback.png + :width: 65% + :align: center + +A second optional read response retiming register can be enabled in-line with the +path back to the CPU interface layer. This can be useful if the CPU interface protocol +used has a fully combinational response path, and the design's complexity requires +this path to be retimed further. diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..6a3f472 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,89 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('../src/')) + +import datetime + +# -- Project information ----------------------------------------------------- + +project = 'PeakRDL-regblock' +copyright = '%d, Alex Mykyta' % datetime.datetime.now().year +author = 'Alex Mykyta' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.napoleon', + "sphinxcontrib.wavedrom", +] +render_using_wavedrompy = True + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_book_theme" + +html_theme_options = { + "repository_url": "https://github.com/SystemRDL/PeakRDL-regblock", + "path_to_docs": "docs", + "use_download_button": False, + "use_source_button": True, + "use_repository_button": True, + "use_issues_button": True, +} + + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = [] + + +rst_epilog = """ +.. |iERR| image:: /img/err.svg + :width: 18px + :class: no-scaled-link + +.. |iWARN| image:: /img/warn.svg + :width: 18px + :class: no-scaled-link + +.. |iOK| image:: /img/ok.svg + :width: 18px + :class: no-scaled-link + +.. |NO| replace:: |iERR| Not Supported + +.. |EX| replace:: |iWARN| Experimental + +.. |OK| replace:: |iOK| Supported + +""" diff --git a/docs/configuring.rst b/docs/configuring.rst new file mode 100644 index 0000000..6be6f63 --- /dev/null +++ b/docs/configuring.rst @@ -0,0 +1,45 @@ +.. _peakrdl_cfg: + +Configuring PeakRDL-regblock +============================ + +If using the `PeakRDL command line tool `_, +some aspects of the ``regblock`` command have additional configuration options +available via the PeakRDL TOML file. + +All regblock-specific options are defined under the ``[regblock]`` TOML heading. + +.. data:: cpuifs + + Mapping of additional CPU Interface implementation classes to load. + The mapping's key indicates the cpuif's name. + The value is a string that describes the import path and cpuif class to + load. + + For example: + + .. code-block:: toml + + [regblock] + cpuifs.my-cpuif-name = "my_cpuif_module:MyCPUInterfaceClass" + + +.. data:: default_reset + + Choose the default style of reset signal if not explicitly + specified by the SystemRDL design. If unspecified, the default reset + is active-high and synchronous. + + Choice of: + + * ``rst`` (default) + * ``rst_n`` + * ``arst`` + * ``arst_n`` + + For example: + + .. code-block:: toml + + [regblock] + default_reset = "arst" diff --git a/docs/cpuif/apb.rst b/docs/cpuif/apb.rst new file mode 100644 index 0000000..efec7d8 --- /dev/null +++ b/docs/cpuif/apb.rst @@ -0,0 +1,59 @@ +AMBA APB +======== + +Both APB3 and APB4 standards are supported. + +.. warning:: + Some IP vendors will incorrectly implement the address signalling + assuming word-addresses. (that each increment of ``PADDR`` is the next word) + + For this exporter, values on the interface's ``PADDR`` input are interpreted + as byte-addresses. (an APB interface with 32-bit wide data increments + ``PADDR`` in steps of 4 for every word). Even though APB protocol does not + allow for unaligned transfers, this is in accordance to the official AMBA + specification. + + Be sure to double-check the interpretation of your interconnect IP. A simple + bit-shift operation can be used to correct this if necessary. + + +APB3 +---- + +Implements the register block using an +`AMBA 3 APB `_ +CPU interface. + +The APB3 CPU interface comes in two i/o port flavors: + +SystemVerilog Interface + * Command line: ``--cpuif apb3`` + * Interface Definition: :download:`apb3_intf.sv <../../hdl-src/apb3_intf.sv>` + * Class: :class:`peakrdl_regblock.cpuif.apb3.APB3_Cpuif` + +Flattened inputs/outputs + Flattens the interface into discrete input and output ports. + + * Command line: ``--cpuif apb3-flat`` + * Class: :class:`peakrdl_regblock.cpuif.apb3.APB3_Cpuif_flattened` + + +APB4 +---- + +Implements the register block using an +`AMBA 4 APB `_ +CPU interface. + +The APB4 CPU interface comes in two i/o port flavors: + +SystemVerilog Interface + * Command line: ``--cpuif apb4`` + * Interface Definition: :download:`apb4_intf.sv <../../hdl-src/apb4_intf.sv>` + * Class: :class:`peakrdl_regblock.cpuif.apb4.APB4_Cpuif` + +Flattened inputs/outputs + Flattens the interface into discrete input and output ports. + + * Command line: ``--cpuif apb4-flat`` + * Class: :class:`peakrdl_regblock.cpuif.apb4.APB4_Cpuif_flattened` diff --git a/docs/cpuif/avalon.rst b/docs/cpuif/avalon.rst new file mode 100644 index 0000000..2ae8a04 --- /dev/null +++ b/docs/cpuif/avalon.rst @@ -0,0 +1,33 @@ +Intel Avalon +============ + +Implements the register block using an +`Intel Avalon MM `_ +CPU interface. + +The Avalon interface comes in two i/o port flavors: + +SystemVerilog Interface + * Command line: ``--cpuif avalon-mm`` + * Interface Definition: :download:`avalon_mm_intf.sv <../../hdl-src/avalon_mm_intf.sv>` + * Class: :class:`peakrdl_regblock.cpuif.avalon.Avalon_Cpuif` + +Flattened inputs/outputs + Flattens the interface into discrete input and output ports. + + * Command line: ``--cpuif avalon-mm-flat`` + * Class: :class:`peakrdl_regblock.cpuif.avalon.Avalon_Cpuif_flattened` + + +Implementation Details +---------------------- +This implementation of the Avalon protocol has the following features: + +* Interface uses word addressing. +* Supports `pipelined transfers `_ +* Responses may have variable latency + + In most cases, latency is fixed and is determined by how many retiming + stages are enabled in your design. + However if your design contains external components, access latency is + not guaranteed to be uniform. diff --git a/docs/cpuif/axi4lite.rst b/docs/cpuif/axi4lite.rst new file mode 100644 index 0000000..255be14 --- /dev/null +++ b/docs/cpuif/axi4lite.rst @@ -0,0 +1,32 @@ +.. _cpuif_axi4lite: + +AMBA AXI4-Lite +============== + +Implements the register block using an +`AMBA AXI4-Lite `_ +CPU interface. + +The AXI4-Lite CPU interface comes in two i/o port flavors: + +SystemVerilog Interface + * Command line: ``--cpuif axi4-lite`` + * Interface Definition: :download:`axi4lite_intf.sv <../../hdl-src/axi4lite_intf.sv>` + * Class: :class:`peakrdl_regblock.cpuif.axi4lite.AXI4Lite_Cpuif` + +Flattened inputs/outputs + Flattens the interface into discrete input and output ports. + + * Command line: ``--cpuif axi4-lite-flat`` + * Class: :class:`peakrdl_regblock.cpuif.axi4lite.AXI4Lite_Cpuif_flattened` + + +Pipelined Performance +--------------------- +This implementation of the AXI4-Lite interface supports transaction pipelining +which can significantly improve performance of back-to-back transfers. + +In order to support transaction pipelining, the CPU interface will accept multiple +concurrent transactions. The number of outstanding transactions allowed is automatically +determined based on the register file pipeline depth (affected by retiming options), +and influences the depth of the internal transaction response skid buffer. diff --git a/docs/cpuif/customizing.rst b/docs/cpuif/customizing.rst new file mode 100644 index 0000000..dc806c5 --- /dev/null +++ b/docs/cpuif/customizing.rst @@ -0,0 +1,114 @@ +Customizing the CPU interface +============================= + +Use your own existing SystemVerilog interface definition +-------------------------------------------------------- + +This exporter comes pre-bundled with its own SystemVerilog interface declarations. +What if you already have your own SystemVerilog interface declaration that you prefer? + +Not a problem! As long as your interface definition is similar enough, it is easy +to customize and existing CPUIF definition. + + +As an example, let's use the SystemVerilog interface definition for +:ref:`cpuif_axi4lite` that is bundled with this project. This interface uses +the following style and naming conventions: + +* SystemVerilog interface type name is ``axi4lite_intf`` +* Defines modports named ``master`` and ``slave`` +* Interface signals are all upper-case: ``AWREADY``, ``AWVALID``, etc... + +Lets assume your preferred SV interface definition uses a slightly different naming convention: + +* SystemVerilog interface type name is ``axi4_lite_interface`` +* Modports are capitalized and use suffixes ``Master_mp`` and ``Slave_mp`` +* Interface signals are all lower-case: ``awready``, ``awvalid``, etc... + +Rather than rewriting a new CPU interface definition, you can extend and adjust the existing one: + +.. code-block:: python + + from peakrdl_regblock.cpuif.axi4lite import AXI4Lite_Cpuif + + class My_AXI4Lite(AXI4Lite_Cpuif): + @property + def port_declaration(self) -> str: + # Override the port declaration text to use the alternate interface name and modport style + return "axi4_lite_interface.Slave_mp s_axil" + + def signal(self, name:str) -> str: + # Override the signal names to be lowercase instead + return "s_axil." + name.lower() + +Then use your custom CPUIF during export: + +.. code-block:: python + + exporter = RegblockExporter() + exporter.export( + root, "path/to/output_dir", + cpuif_cls=My_AXI4Lite + ) + + + +Custom CPU Interface Protocol +----------------------------- + +If you require a CPU interface protocol that is not included in this project, +you can define your own. + +1. Create a SystemVerilog CPUIF implementation template file. + + This contains the SystemVerilog implementation of the bus protocol. The logic + in this shall implement a translation between your custom protocol and the + :ref:`cpuif_protocol`. + + Reminder that this template will be preprocessed using + `Jinja `_, so you can use + some templating tags to dynamically render content. See the implementations of + existing CPU interfaces as an example. + +2. Create a Python class that defines your CPUIF + + Extend your class from :class:`peakrdl_regblock.cpuif.CpuifBase`. + Define the port declaration string, and provide a reference to your template file. + +3. Use your new CPUIF definition when exporting. +4. If you think the CPUIF protocol is something others might find useful, let me + know and I can add it to PeakRDL! + + +Loading into the PeakRDL command line tool +------------------------------------------ +There are two ways to make your custom CPUIF class visible to the +`PeakRDL command-line tool `_. + +Via the PeakRDL TOML +^^^^^^^^^^^^^^^^^^^^ +The easiest way to add your cpuif is via the TOML config file. See the +:ref:`peakrdl_cfg` section for more details. + +Via a package's entry point definition +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +If you are publishing a collection of PeakRDL plugins as an installable Python +package, you can advertise them to PeakRDL using an entry point. +This advertises your custom CPUIF class to the PeakRDL-regblock tool as a plugin +that should be loaded, and made available as a command-line option in PeakRDL. + +.. code-block:: toml + + [project.entry-points."peakrdl_regblock.cpuif"] + my-cpuif = "my_package.my_module:MyCPUIF" + + +* ``my_package``: The name of your installable Python module +* ``peakrdl-regblock.cpuif``: This is the namespace that PeakRDL-regblock will + search. Any cpuif plugins you create must be enclosed in this namespace in + order to be discovered. +* ``my_package.my_module:MyCPUIF``: This is the import path that + points to your CPUIF class definition. +* ``my-cpuif``: The lefthand side of the assignment is your cpuif's name. This + text is what the end-user uses in the command line interface to select your + CPUIF implementation. diff --git a/docs/cpuif/internal_protocol.rst b/docs/cpuif/internal_protocol.rst new file mode 100644 index 0000000..75f4515 --- /dev/null +++ b/docs/cpuif/internal_protocol.rst @@ -0,0 +1,232 @@ +.. _cpuif_protocol: + +Internal CPUIF Protocol +======================= + +Internally, the regblock generator uses a common CPU interface handshake +protocol. This strobe-based protocol is designed to add minimal overhead to the +regblock implementation, while also being flexible enough to support advanced +features of a variety of bus interface standards. + + +Signal Descriptions +------------------- + +Request +^^^^^^^ +cpuif_req + When asserted, a read or write transfer will be initiated. + Denotes that the following signals are valid: ``cpuif_addr``, + ``cpuif_req_is_wr``, and ``cpuif_wr_data``. + + A transfer will only initiate if the relevant stall signal is not asserted. + If stalled, the request shall be held until accepted. A request's parameters + (type, address, etc) shall remain static throughout the stall. + +cpuif_addr + Byte-address of the transfer. + +cpuif_req_is_wr + If ``1``, denotes that the current transfer is a write. Otherwise transfer is + a read. + +cpuif_wr_data + Data to be written for the write transfer. This signal is ignored for read + transfers. + +cpuif_wr_biten + Active-high bit-level write-enable strobes. + Only asserted bit positions will change the register value during a write + transfer. + +cpuif_req_stall_rd + If asserted, and the next pending request is a read operation, then the + transfer will not be accepted until this signal is deasserted. + +cpuif_req_stall_wr + If asserted, and the next pending request is a write operation, then the + transfer will not be accepted until this signal is deasserted. + + +Read Response +^^^^^^^^^^^^^ +cpuif_rd_ack + Single-cycle strobe indicating a read transfer has completed. + Qualifies that the following signals are valid: ``cpuif_rd_err`` and + ``cpuif_rd_data`` + +cpuif_rd_err + If set, indicates that the read transaction failed and the CPUIF logic + should return an error response if possible. + +cpuif_rd_data + Read data. Is sampled on the same cycle that ``cpuif_rd_ack`` is asserted. + +Write Response +^^^^^^^^^^^^^^ +cpuif_wr_ack + Single-cycle strobe indicating a write transfer has completed. + Qualifies that the ``cpuif_wr_err`` signal is valid. + +cpuif_wr_err + If set, indicates that the write transaction failed and the CPUIF logic + should return an error response if possible. + + +Transfers +--------- + +Transfers have the following characteristics: + +* Only one transfer can be initiated per clock-cycle. This is implicit as there + is only one set of request signals. +* The register block implementation shall guarantee that only one response can be + asserted in a given clock cycle. Only one ``cpuif_*_ack`` signal can be + asserted at a time. +* Responses shall arrive in the same order as their corresponding request was + dispatched. + + +Basic Transfer +^^^^^^^^^^^^^^ + +Depending on the configuration of the exported register block, transfers can be +fully combinational or they may require one or more clock cycles to complete. +Both are valid and CPU interface logic shall be designed to anticipate either. + +.. wavedrom:: + + { + "signal": [ + {"name": "clk", "wave": "p...."}, + {"name": "cpuif_req", "wave": "010.."}, + {"name": "cpuif_req_is_wr", "wave": "x2x.."}, + {"name": "cpuif_addr", "wave": "x2x..", "data": ["A"]}, + {}, + {"name": "cpuif_*_ack", "wave": "010.."}, + {"name": "cpuif_*_err", "wave": "x2x.."} + ], + "foot": { + "text": "Zero-latency transfer" + } + } + +.. wavedrom:: + + { + "signal": [ + {"name": "clk", "wave": "p..|..."}, + {"name": "cpuif_req", "wave": "010|..."}, + {"name": "cpuif_req_is_wr", "wave": "x2x|..."}, + {"name": "cpuif_addr", "wave": "x2x|...", "data": ["A"]}, + {}, + {"name": "cpuif_*_ack", "wave": "0..|10."}, + {"name": "cpuif_*_err", "wave": "x..|2x."} + ], + "foot": { + "text": "Transfer with non-zero latency" + } + } + + +Read & Write Transactions +------------------------- + +Waveforms below show the timing relationship of simple read/write transactions. +For brevity, only showing non-zero latency transfers. + +.. wavedrom:: + + { + "signal": [ + {"name": "clk", "wave": "p..|..."}, + {"name": "cpuif_req", "wave": "010|..."}, + {"name": "cpuif_req_is_wr", "wave": "x0x|..."}, + {"name": "cpuif_addr", "wave": "x3x|...", "data": ["A"]}, + {}, + {"name": "cpuif_rd_ack", "wave": "0..|10."}, + {"name": "cpuif_rd_err", "wave": "x..|0x."}, + {"name": "cpuif_rd_data", "wave": "x..|5x.", "data": ["D"]} + ], + "foot": { + "text": "Read Transaction" + } + } + + +.. wavedrom:: + + { + "signal": [ + {"name": "clk", "wave": "p..|..."}, + {"name": "cpuif_req", "wave": "010|..."}, + {"name": "cpuif_req_is_wr", "wave": "x1x|..."}, + {"name": "cpuif_addr", "wave": "x3x|...", "data": ["A"]}, + {"name": "cpuif_wr_data", "wave": "x5x|...", "data": ["D"]}, + {}, + {"name": "cpuif_wr_ack", "wave": "0..|10."}, + {"name": "cpuif_wr_err", "wave": "x..|0x."} + ], + "foot": { + "text": "Write Transaction" + } + } + + +Transaction Pipelining & Stalls +------------------------------- +If the CPU interface supports it, read and write operations can be pipelined. + +.. wavedrom:: + + { + "signal": [ + {"name": "clk", "wave": "p......"}, + {"name": "cpuif_req", "wave": "01..0.."}, + {"name": "cpuif_req_is_wr", "wave": "x0..x.."}, + {"name": "cpuif_addr", "wave": "x333x..", "data": ["A1", "A2", "A3"]}, + {}, + {"name": "cpuif_rd_ack", "wave": "0.1..0."}, + {"name": "cpuif_rd_err", "wave": "x.0..x."}, + {"name": "cpuif_rd_data", "wave": "x.555x.", "data": ["D1", "D2", "D3"]} + ] + } + +It is very likely that the transfer latency of a read transaction will not +be the same as a write for a given register block configuration. Typically read +operations will be more deeply pipelined. This latency asymmetry would create a +hazard for response collisions. + +In order to eliminate this hazard, additional stall signals (``cpuif_req_stall_rd`` +and ``cpuif_req_stall_wr``) are provided to delay the next incoming transfer +request if necessary. When asserted, the CPU interface shall hold the next pending +request until the stall is cleared. + +For non-pipelined CPU interfaces that only allow one outstanding transaction at a time, +these stall signals can be safely ignored. + +In the following example, the regblock is configured such that: + +* A read transaction takes 1 clock cycle to complete +* A write transaction takes 0 clock cycles to complete + +.. wavedrom:: + + { + "signal": [ + {"name": "clk", "wave": "p......."}, + {"name": "cpuif_req", "wave": "01.....0"}, + {"name": "cpuif_req_is_wr", "wave": "x1.0.1.x"}, + {"name": "cpuif_addr", "wave": "x33443.x", "data": ["W1", "W2", "R1", "R2", "W3"]}, + {"name": "cpuif_req_stall_wr", "wave": "0...1.0."}, + {}, + {"name": "cpuif_rd_ack", "wave": "0...220.", "data": ["R1", "R2"]}, + {"name": "cpuif_wr_ack", "wave": "0220..20", "data": ["W1", "W2", "W3"]} + ] + } + +In the above waveform, observe that: + +* The ``R2`` read request is not affected by the assertion of the write stall, + since the write stall only applies to write requests. +* The ``W3`` write request is stalled for one cycle, and is accepted once the stall is cleared. diff --git a/docs/cpuif/introduction.rst b/docs/cpuif/introduction.rst new file mode 100644 index 0000000..126bc0e --- /dev/null +++ b/docs/cpuif/introduction.rst @@ -0,0 +1,36 @@ +Introduction +============ + +The CPU interface logic layer provides an abstraction between the +application-specific bus protocol and the internal register file logic. +When exporting a design, you can select from a variety of popular CPU interface +protocols. These are described in more detail in the pages that follow. + + +Bus Width +^^^^^^^^^ +The CPU interface bus width is automatically determined from the contents of the +design being exported. The bus width is equal to the widest ``accesswidth`` +encountered in the design. + + +Addressing +^^^^^^^^^^ + +The regblock exporter will always generate its address decoding logic using local +address offsets. The absolute address offset of your device shall be +handled by your system interconnect, and present addresses to the regblock that +only include the local offset. + +For example, consider a fictional AXI4-Lite device that: + +- Consumes 4 kB of address space (``0x000``-``0xFFF``). +- The device is instantiated in your system at global address range ``0x30_0000 - 0x50_0FFF``. +- After decoding transactions destined to the device, the system interconnect shall + ensure that AxADDR values are presented to the device as relative addresses - within + the range of ``0x000``-``0xFFF``. +- If care is taken to align the global address offset to the size of the device, + creating a relative address is as simple as pruning down address bits. + +By default, the bit-width of the address bus will be the minimum size to span the contents +of the register block. If needed, the address width can be overridden to a larger range. diff --git a/docs/cpuif/passthrough.rst b/docs/cpuif/passthrough.rst new file mode 100644 index 0000000..0bfb2d7 --- /dev/null +++ b/docs/cpuif/passthrough.rst @@ -0,0 +1,10 @@ +CPUIF Passthrough +================= + +This CPUIF mode bypasses the protocol converter stage and directly exposes the +internal CPUIF handshake signals to the user. + +* Command line: ``--cpuif passthrough`` +* Class: :class:`peakrdl_regblock.cpuif.passthrough.PassthroughCpuif` + +For more details on the protocol itself, see: :ref:`cpuif_protocol`. diff --git a/docs/dev_notes/Alpha-Beta Versioning b/docs/dev_notes/Alpha-Beta Versioning new file mode 100644 index 0000000..c8a089d --- /dev/null +++ b/docs/dev_notes/Alpha-Beta Versioning @@ -0,0 +1,10 @@ +Holy smokes this is complicated + +Keep this exporter in Alpha/Beta for a while +Add some text in the readme or somewhere: + - No guarantees of correctness! This is always true with open source software, + but even more here! + Be sure to do your own validation before using this in production. + - Alpha means the implementation may change drastically! + Unlike official sem-ver, I am not making any guarantees on compatibility + - I need your help! Validating, finding edge cases, etc... diff --git a/docs/dev_notes/Hierarchy-and-Indexing b/docs/dev_notes/Hierarchy-and-Indexing new file mode 100644 index 0000000..9819b31 --- /dev/null +++ b/docs/dev_notes/Hierarchy-and-Indexing @@ -0,0 +1,67 @@ +-------------------------------------------------------------------------------- +Preserve Hierarchy +-------------------------------------------------------------------------------- +I *reaaaally* want to be able to make deferred RDL parameters a reality in the +future. (https://github.com/SystemRDL/systemrdl-compiler/issues/58) + +Proactively design templates to retain "real" hierarchy. This means: +- Do not flatten/unroll signals. Use SV structs & arrays +- Do not flatten/unroll logic. Use nested for loops + +Sticking to the above should make adding parameter support somewhat less painful. + +-------------------------------------------------------------------------------- +Indexing & references +-------------------------------------------------------------------------------- +Need to define a consistent scheme for referencing hierarchical elements. + +When inside a nesting of for loops, and array indexes are intended to increment, +always use an incrementing indexing scheme when generating iterators: + i0, i1, i2, i3, ... i9, i10, i11, etc... +For example: + access_strb.2d_array[i0][i1].array[i3] + +Sometimes, an RDL input may create the need to reference an element with +partially constant indexes. +For example, given this RDL: + + addrmap top { + regfile { + reg { + field {} f1; + } x[8]; + + reg { + field {} f2; + } y; + + y.f2->next = x[3].f1; + + } rf_loop[16]; + }; + +The 'next' assignment will have a reference that has the following hierarchical +path: + top.rf_loop[].x[3].f1 + | | + | +--- known index + +--- unknown index + +It is provable that any RDL references will always follow these truths: + - a reference may have a mix of known/unknown indexes in its path + - unknown indexes (if any) will always precede known indexes + - unknown indexes are not actually part of the relative reference path, and + represent replication of the reference. + It is impossible for the reference itself to introduce unknown indexes. + +When generating SystemVerilog, be sure to generate code such that "unknown" indexes +are always implicitly known due to the reference being used from within a for loop. +For example: + + for(int i0=0; i0<16; i0++) begin : rf_loop_array + top.rf_loop[i0].y.f2 = top.rf_loop[i0].x[3].f1 + end + +This should be a reasonable thing to accomplish, since unknown indexes should +only show up in situations where the consumer of the reference is being +replicated as well, and is therefore implicitly going to be inside a for loop. diff --git a/docs/dev_notes/Program Flow b/docs/dev_notes/Program Flow new file mode 100644 index 0000000..a07ff23 --- /dev/null +++ b/docs/dev_notes/Program Flow @@ -0,0 +1,23 @@ + +1. Scan design. Collect information + - Check for unsupported constructs. Throw errors as appropriate + - Uniform regwidth, accesswidth, etc. + + - Collect reset signals + cpuif_reset, field_reset + explicitly assigned to field->resetsignal + + - Collect any other misc user signals that are referenced in the design + + - Top-level interrupts + Collect X & Y: + X = set of all registers that have an interrupt field + Y = set of all interrupt registers that are referenced by a field + Top level interrupt registers are the set in X, but not in Y + (and probably other caveats. See notes) + +2. Create intermediate template objects + +3. Render top-level IO struct package (if applicable) + +4. Render top-level module template diff --git a/docs/dev_notes/Resets b/docs/dev_notes/Resets new file mode 100644 index 0000000..2b7ad14 --- /dev/null +++ b/docs/dev_notes/Resets @@ -0,0 +1,11 @@ +================================================================================ +Resets +================================================================================ +use whatever is defined in RDL based on cpuif_reset and field_reset signals +Otherwise, provide configuration that defines what the default is: + a single reset that is active high/low, or sync/async + +If cpuif_reset is specified, what do fields use? + I assume they still use the default reset separately? + YES. Agnisys appears to be wrong. + cpuif_reset has no influence on the fields' reset according to the spec diff --git a/docs/dev_notes/Signal Dereferencer b/docs/dev_notes/Signal Dereferencer new file mode 100644 index 0000000..6b8922c --- /dev/null +++ b/docs/dev_notes/Signal Dereferencer @@ -0,0 +1,22 @@ +I need some sort of signal "dereferencer" that can be easily used to translate references +to stuff via a normalized interface. + +For example, if RDL defines: + my_field->next = my_other_field +Then in Python (or a template) I could do: + x = my_field.get_property('next') + y = dereferencer.get(x) +and trust that I'll get a value/identifier/whatever that accurately represents +the value being referenced + +Values: + If X is a field reference: + ... that implements storage, return its DFF value reference + ... no storage, but has a hw input, grab from the hwif input + ... no storage, and no hw input, return its constant reset value? + If X is a property reference... do whats right... + my_field->anded === (&path.to.my_field) + if X is a static value, return the literal + + +See `Hierarchy and Indexing` on details on how to build path references to stuff diff --git a/docs/dev_notes/Validation Needed b/docs/dev_notes/Validation Needed new file mode 100644 index 0000000..6377ec2 --- /dev/null +++ b/docs/dev_notes/Validation Needed @@ -0,0 +1,183 @@ + +================================================================================ +Things that need validation by the compiler +================================================================================ +Many of these are probably already covered, but being paranoid. +Make a list of things as I think of them. +Keep them here in case I forget and re-think of them. + +Mark these as follows: + X = Yes, confirmed that the compiler covers this + ! = No! Confirmed that the compiler does not check this, and should. + ? = TBD + +-------------------------------------------------------------------------------- + +X resetsignal width + reset signals shall have width of 1 + +X Field has no knowable value + - does not implement storage + - hw is not writable + - sw is readable + - No reset value specified + + --> emit a warning? + +X References to a component or component property must use unambiguous array indexing + For example, if "array_o_regs" is an array... + The following is illegal: + my_reg.my_field->next = array_o_regs.thing + my_reg.my_field->next = array_o_regs.thing->anded + This is ok: + my_reg.my_field->next = array_o_regs[2].thing + my_reg.my_field->next = array_o_regs[2].thing->anded + + NEVERMIND - compiler does not allow indefinite array references at all! + References are guaranteed to be unambiguous: + "Incompatible number of index dimensions after 'CTRL'. Expected 1, found 0." + +X Clause 10.6.1-f (wide registers cannot have access side-effects) + +X multiple field_reset in the same hierarchy + there can only be one signal declared with field_reset + in a given hierarchy + +X multiple cpuif_reset in the same hierarchy + there can only be one signal declared with cpuif_reset + in a given hierarchy + +X Mutually-exclusive property checking + --> Yes. compiler now auto-clears mutex partners on assign, so it is + implicitly handled + +X incrwidth/incrvalue & decrvalue/decrwidth + these pairs are mutually exclusive. + Make sure they are not both set after elaboration + Compiler checks for mutex within the same scope, but + i dont think I check for mutexes post-elaborate + + ... or, make these properties clear each-other on assignment + +X Illegal property references: + - reference any of the counter property references to something that isn't a counter + decrsaturate / incrsaturate / saturate + overflow / underflow + - reference hwclr or hwset, but the owner node has them set to False + means that the actual inferred signal doesnt exist! + - reference swwe/swwel or we/wel, but the owner node has them, AND their complement set to False + means that the actual inferred signal doesnt exist! + - only valid to reference if owner has this prop set + enable/mask + haltenable/haltmask + hwenable + hwmask + decr/incr, decr../incrthreshold/..value + - others references that may not always make sense: + intr/halt - target must contain interrupt/halt fields + next + is this ever illegal? + +X If a node ispresent=true, and any of its properties are a reference, + then those references' ispresent shall also be true + This is an explicit clause in the spec: 5.3.1-i + +X Flag illegal sw actions if not readable/writable + The following combinations dont get flagged currently: + sw=w; rclr; + sw=w; rset; + sw=r; woset; + sw=r; woclr; + their counterparts do get flagged. such as: + sw=w; onread=rclr; + +X Signals marked as field_reset or cpuif_reset need to have activehigh/activelow + specified. (8.2.1-d states that activehigh/low does not have an implied default state if unset!) + Also applies to signals referenced by resetsignal + +X incrvalue/decrvalue needs to be the same or narrower than counter itself + +X field shall be hw writable if "next" is assigned. + +X sticky=true + "(posedge|negedge|bothedge) intr" + Edge-sensitivty doesnt make sense for full-field stickiness + +X we/wel + implied or explicit "sticky"/"stickybit" + we/wel modifier doesn't make sense here. + +X sticky/stickybit shall be hw writable + +X Illegal to use enable/mask/haltenable/haltmask on non-intr fields + +X incrwidth/decrwidth must be between 1 and the width of the counter + +X counter field that saturates should not set overflow + counter; incrsaturate; overflow; + counter; decrsaturate; underflow; + + Flag this as an error on the overflow/underflow property. + overflow/underflow property is meaningless since it can never happen. + + Same goes to prop references to overflow/underflow + +! hwclr/hwset/we/wel probably shouldn't be able to reference itself + y->hwclr = y; + y->we = y; + ... it works, but should it be allowed? Seems like user-error + +================================================================================ +Things that need validation by this exporter +================================================================================ +List of stuff in case I forget. + X = Yes! I already implemented this. + ! = No! exporter does not enforce this yet + +-------------------------------------------------------------------------------- + +X Contents of target are all internal. No external regs + +X Does not contain any mem components + +X Warn/error on any signal with cpuif_reset set, that is not in the top-level + addrmap. At the very least, warn that it will be ignored + + +X "bridge" addrmap not supported + export shall refuse to process an addrmap marked as a "bridge" + Only need to check top-level. Compiler will enforce that child nodes arent bridges + +X regwidth/accesswidth is sane + X accesswidth == regwidth + Enforce this for now. Dont feel like supporting fancy modes yet + X regwidth < accesswidth + This is illegal and is enforced by the compiler. + X regwidth > accesswidth + Need to extend address decode strobes to have multiple bits + this is where looking at endianness matters to determine field placement + Dont feel like supporting this yet + X constant regwidth? + For now, probably limit to only allow the same regwidth everywhere? + + +X Do not allow unaligned addresses + All offsets & strides shall be a multiple of the regwidth used + + X each reg needs to be aligned to its width + X each regfile/addrmap/stride shall be aligned to the largest regwidth it encloses + +X Error if a property is a reference to something that is external, or enclosed + in an external component. + Limit this check to child nodes inside the export hierarchy + +! async data signals + Only supporting async signals if they are exclusively used in resets. + Anything else declared as "async" shall emit a warning that it is ignored + I have zero interest in implementing resynchronizers + +! Error if a property references a non-signal component, or property reference from + outside the export hierarchy + +! Add warning for sticky race condition + stickybit and other similar situations generally should use hw precedence. + Emit a warning as appropriate + Or should this be a compiler warning?? diff --git a/docs/dev_notes/template-layers/1-port-declaration b/docs/dev_notes/template-layers/1-port-declaration new file mode 100644 index 0000000..f4df391 --- /dev/null +++ b/docs/dev_notes/template-layers/1-port-declaration @@ -0,0 +1,51 @@ +-------------------------------------------------------------------------------- +Port Declaration +-------------------------------------------------------------------------------- +Generates the port declaration of the module: + - Parameters + - rd/wr error response/data behavior + Do missed accesses cause a SLVERR? + Do reads respond with a magic value? + - Pipeline enables + Enable reg stages in various places + + - RDL-derived Parameters: + Someday in the future if i ever get around to this: https://github.com/SystemRDL/systemrdl-compiler/issues/58 + + - Clock/Reset + Single clk + One or more resets + + - CPU Bus Interface + Given the bus interface object, emits the IO + This can be flattened ports, or a SV Interface + Regardless, it shall be malleable so that the user can use their favorite + declaration style + + - Hardware interface + Two options: + - 2-port struct interface + Everything is rolled into two unpacked structs - inputs and outputs + - Flattened --> NOT DOING + Flatten/Unroll everything + No. not doing. I hate this and dont want to waste time implementing this. + This will NEVER be able to support parameterized regmaps, and just + creates a ton of corner cases i dont care to deal with. + +Other IO Signals I need to be aware of: + any signals declared, and used in any references: + field.resetsignal + field.next + ... etc ... + any signals declared and marked as cpuif_reset, or field_reset + These override the default rst + If both are defined, be sure to not emit the default + Pretty straightforward (see 17.1) + Also have some notes on this in my general Logbook + Will have to make a call on how these propagate if multiple defined + in different hierarchies + interrupt/halt outputs + See "Interrupts" logbook for explanation + addrmap.errextbus, regfile.errextbus, reg.errextbus + ??? + Apparently these are inputs diff --git a/docs/dev_notes/template-layers/1.1.hardware-interface b/docs/dev_notes/template-layers/1.1.hardware-interface new file mode 100644 index 0000000..e23442a --- /dev/null +++ b/docs/dev_notes/template-layers/1.1.hardware-interface @@ -0,0 +1,103 @@ +================================================================================ +Summary +================================================================================ + +RTL interface that provides access to per-field context signals + +Regarding signals: + RDL-declared signals are part of the hwif input structure. + Only include them if they are referenced by the design (need to scan the + full design anyways, so may as well filter out unreferenced ones) + + It is possible to use signals declared in a parent scope. + This means that not all signals will be discovered by a hierarchical listener alone + Need to scan ALL assigned properties for signal references too. + - get signal associated with top node's cpuif_reset helper property, if any + - collect all field_resets + X check all signal instances in the hier tree + - search parents of top node for the first field_reset signal, if any + This is WAY less expensive than querying EACH field's resetsignal property + X Check all explicitly assigned properties + only need to do this for fields + Collect all of these into the following: + - If inside the hier, add to a list of paths + - if outside the hier, add to a dict of path:SignalNode + These are all the signals in-use by the design + + Pass list into the hwif generator + If the hwif generator encounters a signal during traversal: + check if it exists in the signal path list + + out-of-hier signals are inserted outside of the hwif_in as standalone signals. + For now, just use their plain inst names. If I need to uniquify them i can add that later. + I should at least check against a list of known "dirty words". Seems very likely someone will choose + a signal called "rst". + Prefix with usersig_ if needed + + + + +================================================================================ +Naming Scheme +================================================================================ + +hwif_out + .my_regblock + .my_reg[X][Y] + .my_field + .value + .anded + +hwif_in + .my_regblock + .my_reg[X][Y] + .my_field + .value + .we + .my_signal + .my_fieldreset_signal + +================================================================================ +Flattened mode? --> NO +================================================================================ +If user wants a flattened list of ports, +still use the same hwif_in/out struct internally. +Rather than declaring hwif_in and hwif_out in the port list, declare it internally + +Add a mapping layer in the body of the module that performs a ton of assign statements +to map flat signals <-> struct + +Alternatively, don't do this at all. +If I want to add a flattened mode, generate a wrapper module instead. + +Marking this as YAGNI for now. + + +================================================================================ +IO Signals +================================================================================ + +Outputs: + field value + If hw readable + bitwise reductions + if anded, ored, xored == True, output a signal + swmod/swacc + event strobes + +Inputs: + field value + If hw writable + we/wel + if either is boolean, and true + not part of external hwif if reference + mutually exclusive + hwclr/hwset + if either is boolean, and true + not part of external hwif if reference + incr/decr + if counter=true, generate BOTH + incrvalue/decrvalue + if either incrwidth/decrwidth are set + signals! + any signal instances instantiated in the scope diff --git a/docs/dev_notes/template-layers/2-CPUIF b/docs/dev_notes/template-layers/2-CPUIF new file mode 100644 index 0000000..6eb025e --- /dev/null +++ b/docs/dev_notes/template-layers/2-CPUIF @@ -0,0 +1,72 @@ +-------------------------------------------------------------------------------- +CPU Bus interface layer +-------------------------------------------------------------------------------- +Provides an abstraction layer between the outside SoC's bus interface, and the +internal register block's implementation. +Converts a user-selectable bus protocol to generic register file signals. + +Upstream Signals: + Signal names are defined in the bus interface class and shall be malleable + to the user. + User can choose a flat signal interface, or a SV interface. + SV interface shall be easy to tweak since various orgs will use different + naming conventions in their library of interface definitions + +Downstream Signals: + - cpuif_req + - Single-cycle pulse + - Qualifies the following child signals: + - cpuif_req_is_wr + 1 denotes this is a write transfer + - cpuif_addr + Byte address + - cpuif_wr_data + - cpuif_wr_biten + per-bit strobes + some protocols may opt to tie this to all 1's + - cpuif_rd_ack + - Single-cycle pulse + - Qualifies the following child signals: + - cpuif_rd_data + - cpuif_rd_err + + - cpuif_wr_ack + - Single-cycle pulse + - Qualifies the following child signals: + - cpuif_wr_err + + +Misc thoughts +- Internal cpuif_* signals use a strobe-based protocol: + - Unknown, but fixed latency + - Makes for easy pipelining if needed +- Decided to keep cpuif_req signals common for read write: + This will allow address decode logic to be shared for read/write + Downside is split protocols like axi-lite can't have totally separate rd/wr + access lanes, but who cares? +- separate response strobes + Not necessary to use, but this lets me independently pipeline read/write paths. + read path will need more time if readback mux is large +- On multiple outstanding transactions + Currently, cpuif doesnt really support this. Goal was to make it easily pipelineable + without having to backfeed stall logic. + Could still be possible to do a "fly-by" pipeline with a more intelligent cpuif layer + Not worrying about this now. + + +Implementation: + Implement this mainly as a Jinja template. + Upstream bus intf signals are fetched via busif class properties. Ex: + {{busif.signal('pready')}} <= '1; + This allows the actual SV or flattened signal to be emitted + +What protocols do I care about? + - AXI4 Lite + - Ignore AxPROT? + - APB3 + - APB4 + - Ignore pprot? + - AHB? + - Wishbone + - Generic + breakout the above signals as-is (reassign with a prefix or something) diff --git a/docs/dev_notes/template-layers/3-address-decode b/docs/dev_notes/template-layers/3-address-decode new file mode 100644 index 0000000..b835c1c --- /dev/null +++ b/docs/dev_notes/template-layers/3-address-decode @@ -0,0 +1,51 @@ + +-------------------------------------------------------------------------------- +Address Decode layer +-------------------------------------------------------------------------------- +A bunch of combinational address decodes that generate individual register +req strobes + +Possible decode logic styles: + - Big case statement + + Probably more sim-efficient + - Hard to do loop parameterization + - More annoying to do multiple regs per address + - Big always_comb + One if/else chain + + Easy to nest loops & parameterize if needed + - sim has a lot to evaluate each time + - More annoying to do multiple regs per address + - implies precedence? Synth tools should be smart enough? + - Big always_comb + inline conditionals <---- DO THIS + + Easy to nest loops & parameterize if needed + - sim has a lot to evaluate each time + + Multiple regs per address possible + + implies address decode parallelism. + ?? Should I try using generate loops + assigns? + This would be more explicit parallelism, however some tools may + get upset at multiple assignments to a common struct + +Implementation: + Jinja is inappropriate here + Very logic-heavy. Jinja may end up being annoying + Also, not much need for customization here + This may even make sense as a visitor that dumps lines + - visit each reg + - upon entering an array, create for loops + - upon exiting an array, emit 'end' + Make the strobe struct declared locally + No need for it to leave the block + Error handling + If no strobe generated, respond w error? + This is actually pretty expensive to do for writes. + Hold off on this for now. + Reads get this effectively for free in the readback mux. + Implement write response strobes back upstream to cpuif + Eventually allow for optional register stage for strobe struct + Will need to also pipeline the other cpuif signals + ok to discard the cpuif_addr. no longer needed + + +Downstream Signals: + - access strobes + Encase these into a struct datatype + - is_write + wr_data/wr_bitstrobe diff --git a/docs/dev_notes/template-layers/4-fields b/docs/dev_notes/template-layers/4-fields new file mode 100644 index 0000000..3721266 --- /dev/null +++ b/docs/dev_notes/template-layers/4-fields @@ -0,0 +1,163 @@ +-------------------------------------------------------------------------------- +Field storage / next value layer +-------------------------------------------------------------------------------- + +Where all the magic happens!! + +Any field that implements storage is defined here. +Bigass struct that only contains storage elements + +Each field consists of: + - Entries in the storage element struct + - if implements storage - field value + - user extensible values? + - Entries in the combo struct + - if implements storage: + - Field's "next" value + - load-enable strobe + - If counter + various event strobes (overflow/overflow). + These are convenient to generate alongside the field next state logic + - user extensible values? + - an always_comb block: + - generates the "next value" combinational signal + - May generate other intermediate strobes? + incr/decr? + - series of if/else statements that assign the next value in the storage element + Think of this as a flat list of "next state" conditons, ranked by their precedence as follows: + - reset + Actually, handle this in the always_ff + - sw access (if sw precedence) + - onread/onwrite + - hw access + - Counter + beware of clear events and incr/decr events happening simultaneously + - next + - etc + - sw access (if hw precedence) + - onread/onwrite + - always_comb block to also generate write-enable strobes for the actual + storage element + This is better for low-power design + - an always_ff block + Implements the actual storage element + Also a tidy place to abstract the specifics of activehigh/activelow field reset + selection. + +TODO: + Scour the RDL spec. + Does this "next state" precedence model hold true in all situations? + +TODO: + Think about user-extensibility + Provide a mechanism for users to extend/override field behavior + +TODO: + Does the endianness the user sets matter anywhere? + +Implementation + Makes sense to use a listener class + +Be sure to skip alias registers + +-------------------------------------------------------------------------------- + +NextStateConditional Class + Describes a single conditional action that determines the next state of a field + Provides information to generate the following content: + if() begin + + end + + - is_match(self, field: FieldNode) -> bool: + Returns True if this conditional is relevant to the field. If so, + it instructs the FieldBuider that code for this conditional shall be emitted + TODO: better name than "is_match"? More like "is this relevant" + + - get_predicate(self, field: FieldNode) -> str: + Returns the rendered conditional text + + - get_assignments(self, field: FieldNode) -> List[str]: + Returns a list of rendered assignment strings + This will basically always be two: + .next = + .load_next = '1; + + - get_extra_combo_signals(self, field: FieldNode) -> List[TBD]: + Some conditionals will need to set some extra signals (eg. counter underflow/overflow strobes) + Compiler needs to know to: + - declare these inthe combo struct + - initialize them in the beginning of always_comb + + Return something that denotes the following information: (namedtuple?) + - signal name: str + - width: int + - default value assignment: str + + Multiple NextStateConditional can declare the same extra combo signal + as long as their definitions agree + --> Assert this + + +FieldBuilder Class + Describes how to build fields + + Contains NextStateConditional definitions + Nested inside the class namespace, define all the NextStateConditional classes + that apply + User can override definitions or add own to extend behavior + + NextStateConditional objects are stored in a dictionary as follows: + _conditionals { + assignment_precedence: [ + conditional_option_1, + conditional_option_2, + conditional_option_3, + ] + } + + add_conditional(self, conditional, assignment_precedence): + Inserts the NextStateConditional into the given assignment precedence bin + The first one added to a precedence bin is first in that bin's search order + + init_conditionals(self) -> None: + Called from __init__. + loads all possible conditionals into self.conditionals list + This function is to provide a hook for the user to add their own. + + Do not do fancy class introspection. Load them explicitly by name like so: + self.add_conditional(MyNextState(), AssignmentPrecedence.SW_ACCESS) + + If user wants to extend this class, they can pile onto the bins of conditionals freely! + +-------------------------------------------------------------------------------- +Misc +-------------------------------------------------------------------------------- +What about complex behaviors like a read-clear counter? + if({{software read}}) + next = 0 + elif({{increment}}) + next = prev + 1 + + --> Implement this by stacking multiple NextStateConditional in the same assignment precedence. + In this case, there would be a special action on software read that would be specific to read-clear counters + this would get inserted ahead of the search order. + + +Precedence & Search order + There are two layers of priority I need to keep track of: + - Assignment Precedence + RTL precedence of the assignment conditional + - Search order (sp?) + Within an assignment precedence, order in which the NextStateConditional classes are + searched for a match + + For assignment precedence, it makes sense to use an integer enumeration for this + since there really aren't too many precedence levels that apply here. + Space out the integer enumerations so that user can reliably insert their own actions, ie: + my_precedence = AssignmentPrecedence.SW_ACCESS + 1 + + For search order, provide a user API to load a NextStateConditional into + a precedence 'bin'. Pushing into a bin always inserts into the front of the search order + This makes sense since user overrides will always want to be highest priority - and + rule themselves out before falling back to builtin behavior diff --git a/docs/dev_notes/template-layers/5-readback-mux b/docs/dev_notes/template-layers/5-readback-mux new file mode 100644 index 0000000..fa53102 --- /dev/null +++ b/docs/dev_notes/template-layers/5-readback-mux @@ -0,0 +1,49 @@ +-------------------------------------------------------------------------------- +Readback mux layer +-------------------------------------------------------------------------------- + +Implementation: + - Big always_comb block + - Initialize default rd_data value + - Lotsa if statements that operate on reg strb to assign rd_data + - Merges all fields together into reg + - pulls value from storage element struct, or input struct + - Provision for optional flop stage? + +Mux Strategy: + Flat case statement: + -- Cant parameterize + + better performance? + + Flat 1-hot array then OR reduce: + - Create a bus-wide flat array + eg: 32-bits x N readable registers + - Assign each element: + the readback value of each register + ... masked by the register's access strobe + - I could also stuff an extra bit into the array that denotes the read is valid + A missed read will OR reduce down to a 0 + - Finally, OR reduce all the elements in the array down to a flat 32-bit bus + - Retiming the large OR fanin can be done by chopping up the array into stages + for 2 stages, sqrt(N) gives each stage's fanin size. Round to favor + more fanin on 2nd stage + 3 stages uses cube-root. etc... + - This has the benefit of re-using the address decode logic. + synth can choose to replicate logic if fanout is bad + + +WARNING: + Beware of read/write flop stage asymmetry & race conditions. + Eg. If a field is rclr, dont want to sample it after it gets read: + addr --> strb --> clear + addr --> loooong...retime --> sample rd value + Should guarantee that read-sampling happens at the same cycle as any read-modify + + +Forwards response strobe back up to cpu interface layer + +TODO: + Dont forget about alias registers here + +TODO: + Does the endinness the user sets matter anywhere? diff --git a/docs/dev_notes/template-layers/6-output-port-mapping b/docs/dev_notes/template-layers/6-output-port-mapping new file mode 100644 index 0000000..013330b --- /dev/null +++ b/docs/dev_notes/template-layers/6-output-port-mapping @@ -0,0 +1,9 @@ +-------------------------------------------------------------------------------- +Output Port mapping layer +-------------------------------------------------------------------------------- + +Assign to output struct port + +Still TBD if this will actually be a distinct layer. +Cosmetically, this might be nicer to interleave with the field section above +Assign storage element & other derived values as requested by properties diff --git a/docs/diagrams/arch.png b/docs/diagrams/arch.png new file mode 100644 index 0000000000000000000000000000000000000000..0aad955f20dc57e23e406459ceec4df2e8d36e42 GIT binary patch literal 151692 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

bqNp@#}q9rMJ1Wl3RS z#}wL^0cFUec7Ui6V*C*<;XoCKdqdYMxzf~#WT!i}Fm7P1^`dhX8%6$;q z2MaaSzU@^@NjZf@g%+to&nryJ-`e}AcK}J}CBaI(F+xBaYOP!m;^T|a%d;;+CeJ5J zDz_o_iM}O;%JP8>xjm}!$(ji{59my6{b>B?8=Q9xen?obi`NhBn6sOXo+lhEQ)D+~ zR9t*QXnAv@Hz#V38@ux5ZCBf6ZfoVRjc`BhI9c41mug3VQqKo)4qhIS70BPy53R|r zdWX10l5EEDE6G#7z1)t*X;Z{JCU&rJ?o+=b4dlp;whSmJIYSiu>p)e0zMfu)zDKKHa`M_v#7 zBg^6|nGvG=VNyXj(yjl)<;>p-(|xZ*<4=`OmVc*dV@i={XYO#O$v|C7bjvNC)~Zr5 zvP~P7z2?^q;epV+Gw!Z5lzAm(rh}g9rUFW<8txhyKn;H#qB$HqVE^q1czjapUXj<+zMSn1c zZ*r5x}~cyAJz zfkhM)n(qlOh!v_Q{Zf>7eR>l8=m|oP$s=s3w>_H7%B;Ys*4n2qo@Q|IYOq*bX<+o; zU|$7SUR_%LOm++9Ld}(iu^hO4jXV?+^>T0)GwXmh`Y<8Nt9iipH1V0-Z_=DX;@edF zaW;2@7(7%8x;nfJ{i3f|POR&v&bQqf%broLHGXgWt9yT;GIn{|Q(muWDdg4|1ZVAI zZz5#9=Wv}yH>?D+1J%nh7fwP$84a3JNc|^)a52d6YS9tT8P8mXa@X0Svn_~EQeEJP zn*87~-*j7lKROD^wY$3&qm;dWA6j(rx+5~RNXn#bj4__cYka5|sXy9~pRGU+ExH-y zgmWI(wn#aU;FIUL!vZ%`v3Zka)Fw8PA57KMs+X&0LR)eFIgcuQHsI!V+vI}Ojr^5Q z9?yx%Dy&ZzxY?}6%&UbhhX!~+RJ&$_hJ&iLg4qK?(PcN(%dc6{ipE-FjYiE=*AD>| z#{^<<;)v($H;9zef_m>i(xQ4#&4i8S0&yDd3nhg8zS7_gN&(e%%P7<}XlBoG}arm;&gNo7XP zCX1O8=V5tfJyl3ttHyn2#KYWUp7%a;)wgyg3ek737P%2x2flEym{v|vRI94CK1cb| zCb{{2xLiOpDBEZ+S3V@!RcSJ>v(L9A!h%Dz-$&Hgy;G3{u=W|8oRiJcHd+fC|lO& z{7z5t#MF7pOG+;`$JzwUl;qOmaealcW7lzk&#FEB_m`GE3u)|Ww>No+bRQKORx20i zvKJ4+jpvqQEcX^8FFU3fyRSZi3M%|gKIzY~EFurILW|(bnPM`nJ$dbT3n5h){9A@2$J{CvKRr=y7xYB@IuE;S4zA98Q=P1N|c>B%cz#}{0c&oWx>gI<6w7xo?AkCU-Ebtm?pGmj0mp9h-RKRb_xy`%e zMgC}GbV%ZO<%m^(8$}=Ol5f6*UeCT3w~c#=aUmbpxZ5oM?ivPBVyqDeRH{C;w<{Gs zU%W$S^%Y&+lH;npLkiAe3;xMz>&>&bcr8?=dSD!Tn=kLAZU?m(WFrmmpC?HveZ75uiUef zSFm?y2|Emp?l0);WiX=rDW|2h#*pH48f_WkwboijUu@c!h1!Wvf|0)-5m7JvAM@G4G+z!UtxTxXuxFUJ-#0r;pu;=c1YR%V`0xLzIVDm9f z7oQ*xdWsM+HYH$tS33n*jnWq-cR!FEgNyksIgST^Q8)3C7|NtoY?#@iTex=FzANGT zo~R?gv@bphrMVO@FdLMjFI0m@8C1Vxxz5laRMfFw%dE>lT*L>Y`Ss-CCRu?i(90?0eTzP0W z;PV5_mII$s&&1_BKhDXisA$W{$?f-#*`>Tsf0{!pH2@+hAjJ$Wv>u4EvB^)i#MjO* zxIdudG%Qmy`}VdAn2lBs_To)^)d8_K*uvxikC}_}m-G73<67YZZxZ`C{7;bH<+r&v z_URuF{Q+j0EZp;38o4ie!XF-5FU<0*6`kkkeHFR@B7ww42@%2=CX?(M54f z*zwbbhdt>Ss6#S)_;+JC)NNqu5z-l zdNhmbU0JX-KKw|M;NLeva@=0PmK6YL*~<;N{=Jjg8W3GcY}To^q}<_QL(;eYS00fp zHkrfrr8WE8aA50#frv{d-^&38@djWJ4`+UkOU%c6!Ml^E@x9tq4J3fK4p^K(9Cm6x z%eNr3uku4kjA<{a)4#1KA!>ItG&D$l)4*_{0-}DO{G#|qyZsFa@nZVQ>0FXE-X>S8 zC_juuCSk4A(9~S*Z;)`AxpeFpgm44$gO7w`2jfA4A@IG5ygZx0cuDWgeEh&GADa7E^(P&#OVph(E!3%P|lg1s54P(fIv(R zduY48zJUH3a=m52)yu*QiA0j9$A>eb_6)SR7eu<)#p)CoBGc1vr_pbKu#o(oYu22) zA0BeWhuk6uUWt++$99WIW)R9GkOI&X;gurhb~g_+2I6>AsizlHI*J16OWb{iCPc>og}WmM^kyaCtOPb9hR zA0R*idXOC}Yk9n34zRjOO_2yWc4)8he|D9MDhFihBE^MHEu7)GqtEop0W3`bX$s;4v_i5LW}bv)IchqL28IzD&Ph8we6?l zOr$Jb7vNQs`kn_o{tewpQu^@h$DxJo{@VR)5Y!hALbOx>^GIVCscU>?yZ~m*Gq5k0 z27xrYf$>(i{_xbK@LgGG#Mc6x62o z{`~@_gZ`WbW0BaWd<03`#`uw0bp=7^4lXGn*Y8AlWck{9|I4~TASKo>zz^;Z9onB)t)%O zkmdr>lb?bL9KUt}#ACDSWuGG{{u)%rNZJv64M~58a~LxQVXXTdYWH%c)BKD-efqSr zv7rN^Bb*`8sjJ#$!1!fULZfP zfMx3qQs|6-h3gqR7%nxQ_T8?19K(huZ+80Fra-h`S2v`@b9Pz8nP+ zczE+^=@1e04hhKbQA|Pgk4QuFW0my$!(!nN!U`P*O2C?=Jq2H-_dW$Y_rK-;$%-iO z$EfK4*r{@4umXV&yCrf*9tNU<-=j)p0B9Yg>h+POQBBUq#-?I6CuybDxZH^pCh`nv z7yiLK%=MT04vbF!O+#g~|Js@h4hG^}o0^_Ge_oSh+XnUdcNASFi74s8X&wB^%q%Pi zlW_2cnCkN8t?DoomB#tmSosyma=c1 zfYiM{j6R^zN-M5X?NyZ_Fbm=zP+q!n+u}V(eCOfq;LrXs(!~4^82U&R)Jc-40T2bA z@CHVC-dhwR7k-iEhf)aaRixbpqG(cK6)|KQ=lz9+W3wYFxsdd|pb`o8&h0^zklId60cduL zn%xhe;s~(Gsaa2JK8@cAps1$PiTAd^4yh7Bx+fS9%j?UAa7{L)CNbFh6BN>_{kHIm z+x~|E^+^3gY;sXXv^{L`ZCsknL`d&s3UyzuT<+0|z|e&skPMA0I4UvKi6%2r>j_TF zvjpG|^=V)Y&I9GS@WhE_w>g#bCySh14z)+4{6+`oI4BR_c#ZWayZ&gMMGs0o?SDAD zcryR6eECJ=L})Lia1E0cQbJPylzHAVa!gWLhaaPKL5^KZd4S~puK9q?$>GhUF2M^$ z;k_om{!Bc(5c{)LE4V4O`Wk`96*VvjT2g|k4{~?WUdFGIoRpit3K8F)QCIzAd|b`0 zh=Cuz&z9WGi;=@=kCm1)*DpqCMYKT}nhLtQ=Dl9dR1oT3s3#_+`m)@s5ID9|ATd;D za05eIKub!bd2c>?_?!%#t3E;{u~#u4V}ZTf)hU>b?ECkHl@tCVQEQ;_9B>s~K#e-OXAa$yz( z8#13WA=@!TBoi}3LgA#v6*3gh5>#6Ir1cW(yi0@$$}cCEy0v_vCj7v-`Gz%iyQlJo zM$x{$1wx>9#PIaMEY-=j9M_dI@$v`_4LpjXb-jmWBnYHElRhXwAbE2o?|aa6)Y707 zy746UtwOw_k}A~ehrRD{iwfeU8SBB;xr4z4!xl_lpLKM@FYi+|2Z^k(9uc<9oO(r7 zm99@(&59D$diQAZxju}FR2@OZ`QfRfI<`#9)>~Mf0>jgwYzaLjZ3VgKf+2=z*wSFp zU;=pN*ZA15yO8v! z!`OwD5oAk5ot75OQU-U8GndT-r`2D%@8$zpvmx(04bA`ekv8X(w-vDFA@Sk8DJkD5 zm(l>Papb!`UJ+MT-R35ExBM+b4oOcJ>jp|M`^^(cEUhfLyYUh~S#-vPgzxj+TShs= z8G#~U$8uVSeLITv(bd;n?-4YFQCi>~?Ze3&FE*^d(`+BEsIxa%T4hFPH|@Sx$f~zr zsOB!e=EVD)@>iom1^2f=liBs;t#$k zQP0k^{f%gm_@RgW?yupdJa4M?+DyAKYu)6h<}ortkITLG%YCgi91HTyg_xu7L^J>kSwz~RfdTwzM$4gst=`)3M@v{l2qhMuS z=yb70;BxQN6FGmxO9a#4Z>#3YFgmCO0T?!!-cJR`?kg#u9 z>3O<@TYo$Fi?NxcR1tZzO35mzgMV#olMh=IyDB2d^2(}dlLRE22TWfU-~N^wQR1Uo zzI+;|H(dCQ`^c*T-u&2HNE90{U2qx|b!>I3#l7cL%&EY3li7Ije1RgS&$p&;5Xg)m zE39`B^q86E!*ln6(R%LOxr>)7A9^}>JLx=HHq!eXRnYfS$j=%qc_5&$ z8W|`Y0l(zr_7Ci|?*79QeUOCoTRv4_GZmMmLJ4B|=i?N01r2s3sj?Q5q{^vy)NTY+ zXx=_k2#WkE_(0|V9}p?aGlc;UBB_6aXjl!S=0A=`u=gkADtLTtlYeTX)z)(QbCo*@w8WfO#x!;qkEdH->&!V&l>A z+`*au5|q*y{cGi+1O|fC(3EhNX;Uevg7HpABkM`KmI)N6*Y8qnWh>ZAT|% zNwl3J520`}JQl9ukg)0DQ+#Z9H@AW&@Z8LcpI;?ZcMBwfbr`BJw0TP(OOB`U2}5>YKl2rS zC|C2^0~f~E`xb`M-$D+)|xTAi_Wopl5O>S`@-JtgdAN#Lc2^VD4~J2hD8qj2O#J28ITiO{1=cDCgT2TEoR=t zfiKd9F)vE2)9xMO$;5!3dnc)c2x-`5A> z2RUuPrP4y#k41%tORM{r6h5o635@C7m%B#Otcf!YIo)2ok?c4)`Y2GqzNpW}wtB~p)M};Hn1Tmh9l+OVSYEK%5`&mSE2Ej(`&U1R>{K%z@a+?!}1_Cp6|(|r-8q91MWf^%lzHFeF_ zDt!e%BqPA{UfO+CKakyI5gHM}b3^rBm~CJ7o@Rvk{qNSdH4I4=Ql>@Ax6Hmb$Jz{X z>7_2u{#1rn7pm|I6E-_Lg}}$BtM`<~IKxqR6P_T@=#G=l?zIN$R{aY@a621AGRTX@ zg1_LM2;mx~?nF5H*>Qc=@rXOybSEzCVaNz2jv}$JYM&8Wu6W%#2bGN}aQhyy^h^1; z)88tPrQ`fLu>7uzoS}-iVx1iQTZU_V^n;wf0>)LG9?Zka17VT!FXw9a54@lyE$>q$ zxgzuWXtS+%rp|NjJG(6)_q}H#%p80YMWC&RapjP_G;0AF%|bW(QAGaMge9Yu?2MUR z8w4Ak)4HmV49{oZZeg#b^j^D^AI>qmoS0A6 zWGk8&l-SbIVngpOXek)IQ_t7V`t_vP(0P6>Tt5QcZC9ODo%L~gM-Dw#bd+#XBtiKq zhxNIbNY#uiE@U)o*+leAza09F;W3K;lA6& zZ>X?GYygfbbEqa_>)u^n4n5~9xc=PaDjp8HYP(6(eH&tOIzfaf@Q12ML``#Hr*n=c zUg}k!M)-GUIkBZ*0z?Gb69Ze>FY$(5aiRFq^l+J{jj+ITi%wk9JEtQ_<%iSV3Y*og z!|k6`IoQEs&(lA%h^Fn1(vVrcQ4xt6%&O<=4LhZ__S3qhHhT^abfQ=Aa9VG%w<}|Q zA?vG#zm^uc_Z@~Ij$>UCPuyf$#!fzsADdt{=e=5m^9q~;^0w^iT@F48ZcF9LMksj; zo**DVKU;vg{FO!QWe$w(>q)m-c#V@e1zq*ZqP%NC`NA}(6MF8vKtJv6tZU-PW@|

V5=|Gl-V4D5;FJL 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

    qZP`qF24Vf-MRRk+i9{ozeV_tlZ+r zFVV)DrEf9;|HS7c!xOTbcj@t-78t{kamDxgs%&Z7+iH!=(ff}J^pB!AOL_3&g>U;m z2<^^}BA5)0GqJ$?pN{bVG0pw2Ux2^=(O-`+1{Xeyll*#s8q}(YjBDkrVS_ZV^$p7Oh8aaQ1xdx^a@AZk zPnD_fep*yGOOH3|UVr>(P@$`M3A~4fDfcpr+M9VnwUReXUQzXy6eNA+eV=M}*M~B@m;U zsIxloGFB{%pRx3-#f#71S>|uUA<12$NJp5&Bq&fmR{cex_EPV-l~%b>|4in&%B$~l zTPq8Ow+cO5yeD?JQMqH3$e9>=UFcL@5$;f_T~&dZvg2yBMHQe_sDES!z#@KIXZjC; zfoS?L?VE*6HO9HW$9kijo3v3elKM&lAjiDYrPs2_rFyg2ed}?#qglCw7LF;lWkode zlEY{r!OHV=U>bC)Re{mk1vXyR>pC{kFBIkIIPUq?HU0A5cJReDzQuZ}$Ur~c z)cIP^Dew4BQO5zX<1SgijagRT#O(B@fAoe!62wHl?#YOEmuFc=U61S~PtUzh)R%Uu z4~o`uOqi;U)I6Iu!sbvW4nl^0y%4q)5SCHO)jYQ8!+jUzn=Pu}oaz4{X2hl0QbA5J zj4HrJ--vC!lljrfwLg2%=HQi$)5$bEbXuLch@q1e{d^+jnC4ZZZ$J# z1M|EPmC80&&q1no{u~G&lbHSGD(!`@EqH8Q&QUgR`!2B3KEWsGsocvqHfO}u$8XoN zkJ0i=vY5YCoc+J5yYg?g*01ljRh{tdEmhibOU*@9Q8l)xF@_SUF%$`6s+cw0PPE1r zQ8TR}W;KtA4%ZYSh)4~giWpLHkx+tsqwQVqeb@UJyyv%P?RB1W_FCsW&$B;!@6TtI zO)#z0<();wbjH)P36r~P3`n6kH6*xhY%rl%5_Wh?euEj4m45oH*z}&z02cDPMiCOQ z>2w$8w|Yu3fj}ot(Usxog}`oDCI%srJxEI4gJd!nw0uW-u>}fgze7+p_NGbD7`3GV zY%%#pSYEVexj#xRENN@xmKw^H{;5SvVL?=Pr?D53cN3*8dB^Z#-888!VDoivgmd(^ z|K6AOCgt0>X!BiX%ycJmLp{P3ZWxbxp708**^Lb#B+wCSj)ODdnf-!emVekeY7S_Ahq}H6fQtz6)kW?AHoeIXY)ydAp80p=z zri;)X`$d^)P`cjWOe2CLZ0gosPvYo@oMt`YG*uJ(fbojI|E>qIr6^oHE)@rxnKqw{R}I}tp7$er!%l(>%}W0yh(vW zDa8Geh?S4-J!NGpJ4=u58`M*}tLx`6!15X?dIl7qO5i;Yjx!@V! z|5=3bmt(c(%uIZA!k|oR?WZ6wi%YjR>&3=v0~;f4;fB6SwpkM7Y8r9bYpf3tumh>f zdzV@d{p7(AJXa9qfF!SrmEw8*}b0W1F}#s&iw+8_`-;A@7+YA*|V zBOBt6q)|}RGl0A$MP7x_0q?WPu;@3Z-Hvjp^~;1rdR3{@lmPhC8(RxmS#Me(F#A4j zw*kCzQO*1A#csIcy#>)QNQXn+O>U(^@;NwYQ;!j-1nHSC{?voe^uc0sCuMV$U=sm3 zPX;NJlW|G&6^CB(xh7&Tg`T~dZG=3fdOYFWMOJZ}?G@Om3Sz$jMMS$sYzje1Rqulc zqg?Fzc&|qF5*t^0P?B`3Yo-P`X8&q1)X`f@X&?UvLX|S90&>3XSI1!QS$=4WN)EGs zk(LfQ6!YwJ(^l^ZnxU3rJv_CP5p5ZBN&o_)$6@!5 zdR?J7Nc$ovQ><~J!p`F51F3dWGCmMhl*w-9g>I!}LRt~~pZ6WIJ5R0DHFxbP>N0O* zeyp%hB7Qbg{wazR$SXOK6jhy3uwRf>?Ne$~>1Q#~I>Y9w?tcwl#8;emK0nhs^NI;1 zAz`i{N6o)K17hn9lS}J?R@Z~jO@ljL;i7@m3D=Rf;OD1ofnUdO2~AcO)}FZziM^}q zM|igtQEpap$u^FW&zozDm^^Vp_ z7rq%P(faGF0^>f6%ex25njhrc11EaM?C7MMKGOo_e>u92$cIcw`Z-Hi3V)g!t>phU z=5zM{U`pHddwq&KOCF=C4sphlDy9XirLSP(zs1FZ8SPW&?()q_?B zdxgeZVjpuGH70v8M%u#PmTE5Swvb?qor*CAr(n#-hjN0oYT4|RBy?*wum|$pm8lbE zz8UjA0?f{*KTGO<=dmQ5Ww^7vbBC|xz8O?;Br3{~rk-S#g&l6| zVkM`AIUJ9T+cI*m{4FH@S&;lkuO`yP26*m*k&(r>M+jracQ@m8iOc|qNPH=M&aFjU z4L567=M`6LV#{v2@Q#|7-mA0exTQKO%nm)t`kUbi6*i8Ya_|PcWN#Z_l{~3;zHPcR z%MM&h!Bty)^0Ev_twN<0;VbgedV-h7H|InP9T_+4t)7uw>p#gFXEcy_&B@DMJ`%3 zC#5e94OF(x0R!J$26*$sO6+JEPp|Qv=9i9p6W#Fq5=sTxJKJ8aqSWWSNqtO!zgQ_jj$+Hc0GR9JSa$# zY;^DTAJ4BGZ!sIL$@!Vc>Me2|I`L=vGS{AzVS0#% zJg;g&=TyjF^jT6pR!=RzGlZHhnJ~*Jc+t1n+auK+Rd*KG4vvEu*E+;{lC30pg^N!Y z$P z50gjP-t$NJR0bPUB9_yIN=rm`5vs&ofh~#$o~uD?FYvc>Y7G7NizJ(;Iw^#c+J3m{R~ue8l?XDAm1u7; zGLEl~D(4zyy2rD1pav5Kb@}+ET|>stQC;O9kA?52wblEhI=ngG z&0ArcZFfQ^rfTgI&x=KkB9>>5Z?z1q--(IN)R(z(LLi#oD=M}V|OK>PgQ;!Yu(8LiyDk9=?D!@BhToLEI3rTY|)Rgy1C4`{eh z$(7fBcTfGgRp9oC$uCoyJ8F~bCZMCm+A@aWYLXI}4#w|=^<^Eb;X z6W1@7!VJs}bKvz4;NKC79R~+da_k#tyyrAW>+r0T0OX2fy0g= zQE%;5O{k#KjyFj|wtJ^&Q$=}?k>$#cl@sNTNM@q|4A&ONw{y>t^NBki)R-w#6oSHX z;4TctKI;P_TmpK>o369YN4eqM-&wRwj4q{!Zy;ONbPP|vzCTVMekn^MP^%})gt@f; z2-Q&!{-!xsMt5fGuz@WtR>*hVO%~)va)s0t zhEaZU7H0Yp_C`+ztX121sG-e4&r{*jA6qh&L3OdQ)W@wzSS@lq-$#f!r@HA(zFR4mr zdx1yW-iRN5ZX{7Hiu9x}s_Iw(ZVDNG)hBb5DHbZf2VTt8!VzWE^d-5~zzYcx#;|he z(6w=2PFerPx(I;{dYRMIlsLpsI{R^@2*e;k}4#TA$3`vx)mL zVMyPRPkWOFQ&UEu3VHO1*j(DoqgAKjoRALGstML>pMaGN+zzU1=vz_yt42`z;9sz4 znoj-s=?F=+bti;}?t15J%pK)FFWn5SkvO$j)MUV8wVEc5b=qv4vS(Utdrf&Tnq6Xj z&5m!k{DsWtS#fnQExf|P?M;9eFg-WESS!n|l-8?Cz?=({hGzBxIn7A8J1&ozR!2A+*K7u9>`0(AECU*xxZtCa1|tCIBVvB*`# zf)$NW#a9LgZ*@znUAog4)e-Ewb5Agiuvz7p2zP*r|(n}_^5I48B)w`@Qw_|?&z zG5h13S@qt%G6>KbhqsBMWU=SRF#vX-7#?42im;Nc&O}}OP0XrQzfQr3Sc~F<=`7rv z_uJFUkZtk;=eMI}olsA|TRD=nuZ>keIc(%a$LlPV7$nMM=<+Xr>*Zk1CRhh0B>8ddezZr(a zm=DpzwKGAPC0&NrW|@QID2Xas<+W$KS#ZY&JH8B6g+%4i5BN>G;udt98W_(|BNvWr{vZ%P58-g2%_vyoxb+|>IT&z$u#$k<;%GADE z4bE_O;F^YuxbE+GdLAM6oICT`Wy**O%WU)Vloaf}RzvPeht~0ojN?mw<4q5)bxz=*|oCZ5vK|)Y>C$t0bz*Gjh~hff_h z_MJ)TzE}xaxvNZHHlWj>CiEnNoz%=n=}b+}r{NMiZWHcN+VC%mv2~Y2y8Es}fyKq- z>kf#U^)KIPXPN%+$WMU4I?sIBpDd2l$+%PbqL0YjP_|p;1Y`B{xJSWI!5Pn}0g zL6%g_hEc1>N4Q<%4$yI^Xy*f8LF$7##`aE8c;s;2^$I^|gYWNh6YjDy@|HP?+9t2f zva48;4CEAMc21@#xmwAbT;7q1x2)-wKaCA=O>sXA@IvOS3v313-aJs++79@jcP zaYA<9C!eR`yWyj|9PW_gYntv3gH5%tZHcJiY7t=w*7U2NG>)TIXH!t zfRFrQlu0lhk>2uJ5;((mqhGw)Ca^uRvGh#P!3MD`^5|z>ppNUTi3PLp$g)n(!Q9=m zg8yQp;V`KE*XthGv!nU{mtFMN3F5%UapU)2ll~aJe|7BNn0=z&ugvEUqyObYzY+!q z!#~AN|Ksez@WKn7fAIl*G5p7!0~?3I|KmsQ@pgrvkKBCVYIxv@k-nK;)otg;{{?lG Bbs7Kw literal 0 HcmV?d00001 diff --git a/docs/diagrams/diagrams.odg b/docs/diagrams/diagrams.odg new file mode 100644 index 0000000000000000000000000000000000000000..5b42eb6c76134be390521086f32cee3d6a577d49 GIT binary patch literal 19782 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
  1. gaHFuEMYl3cso1lN0pX#IO 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 literal 0 HcmV?d00001 diff --git a/docs/diagrams/readback.png b/docs/diagrams/readback.png new file mode 100644 index 0000000000000000000000000000000000000000..95086500e5e10b1908a4d7dec4121a37ca58923a GIT binary patch literal 90728 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%0DQ`vM>AV zP~n5(pEr5x1_UR2k5axCnjCQR2#IK|$|d!uZ$95y;uL=Fx8c%sZ}*qi*G2mda~@rq zQzL7?B^peLYBuq?h$EI!xa+oDNyRE6{atMa3OPwuQ)y+L zK;wSm0DZTI)A+1F_W>R@Yg^m-5uXJGAEh5Hc^Qo{g)RI$R(G&8Rm+LBO~jQ7-CoZLTrPDwIy_%x@R+Ys=x?$Fjtai1%r$&q=UzNAE5xYM)_w`uOjGgY3NQ zT?d{Rv6!iw{V<5cg!ikSH~+8EiBWAD`?1V;*fw8rE}|1Kb?BF3{@4*Q@Jo#ZXM;aK z)v`KE8f%p}^ArlI=_2x0^Y))zr(HTNJoqe=x*NBF=UtpfuPPqRZ}O#2nY~^|HO?yY z?e*zrN=YyC>?3|se=+KBYYuCaxwNtLDra-`S94G5jbAhAm!2?poQODet2(IailIIA z4=wK`>+oq|iy;q~0zq9?x#a;bBGLscC_K9n5 zqVBGl2p_8-TCCi#WC^YAakg8M`AXi_*w-EZO8-Xk%U0w89LwsNG42cisi1uGv!dKr z+jwDrj<>yQ9W6Vg71Wr;%=4*wN6w!7HJ6oJen5p)Dnn{e*pNilb~U#1?ey+!ZQ0SA zCchf$pj4UuWFycK@&h`wiGxLIV>iJn#Y_4VHg&%4`K zUBi4&P_b3X?s_O!bIT{5PLSMQ^kx!h0e{!ZC%YnE2m)yk?k!&Qthhb5%7~ZnRujjm+ff1`LaK%r0?k+`D_}~T&wQj@%+hlr&crGln4UWjZQIjs03!Ngys1+A)VE0n zUSqG2-Y@EBJ`Abeu(CRtd4Pf_Oi}n$+hBB9@2a-l=67ux0r9d=BhKzEtvQZ-O*wZO z)~0M}S~+TXt{sk+&X6nJnAE`e753R6bdG8VGH*>L@HaxUwD6{kz{eiyA zB?gz3qut!baJ@iGx8o#b-A+J7@c=@EOkcB`MiqQl(r0g^%f>>poOpJ@k z@LY;{D919Vt9RQ_LJGQrOBtGJ`P(UYjK2r}1uoI4v6e^chf3|ITostyCG*d8S^jKo z5s0_eXKG$?;aU5&I@rf;4hPJijqO;&27u@z)q^{1=%(sv&bB%qp}jnQTZaiyH=MgL zv?R`i8JVI!9^0mpk=!jA&F|bDIoVA^nY|{udOYG)lGJ-P`x;gAQseX6cmf!jQkpY` zC|WXEr^Zvx7Mg#$-EvOu!dP?MZOf*srS=~JsdZ9@gQD{7r@l|7AH{+DuQ^<4-X(irUeo211_gO#-T^YTGS@@5)5yA_T@Aey{ zvJW)ynA5x0!Lp;a>6@XbrcG>mxA`57myDeEI0EvmS`>u z@>mZJ4o)yz&#EkRocP5OmZ(u2u4(SD37(IWN`Gm-*y{K43sYyDH3n@~NTkVa&D7yp zf2ASD)3>O?*1lR@%Gcv`>975qKbUt;Q08qEzoi$bt**Gu*?!(w?6=s_&iZVI)>HY} z`$TfXqFX@?E)8v%@1n0vlJ|;=>KXN$kw2E+pRAzJj2>~#cvN+;r&!!VMNnt3SO(bigIR}Ay({LJV`prx&2hHXK;{b@6C7*n3fu!$_czHKuV zJA8g&VRgFz``XF=GWUw9wyIqirAulbrlee&X?wi>{&{HVtJG9QN1>1k2*kF1wvS;E zlP~cIV~O0V8Ff1*zo#Lcg5<;@s>-8LuLADyUthoAlG8RN5-wigGSQNeWJvQdWnovu zgm&jXF)HU%h;`ZEF$epIu$+`Eed>8q$_r2+vd_}0 zl!-cJ*7};xTC44`z9XC5$%8SJl$6lxttUOSAySe2Nm-J8srg3W%VbBjl|M(Iwa<3l zc|%UQjXTw0Sc_Bb;>U3D5~V>+{10YXwWE8q1hu@=(?#v`&z#f^dqv+n<8kfdPEcx25qChcg_-81 zgyM&!Y$s1_7DThop};mA`^AdBn8&=>?E;nR$2`O%xA8c|hV6 z`^zYABIe@(gVfZNy|mV1Hu3vf8&`XnhRqavGxmi!s}8JtE|e`)yMSL)HU2Z;ot}Fd z-hy>cffU$3!3jCq;O(cUr$d4+NEw5uIam=TeHf1lv3^C9rnej=a(m)KqM&!*CgU!$VG<9ycnPow$V;zYVG)cq zl66!f6)yJbZ7z&-S70^~o{9)c4-JT64eyGJ`js8*A!KNNwT&^qzWX=OszoO5BPI$bHLf~dki_woF>X&O!ihP47$9281yMg5T zfrz)72ooNjpkij!gcxgQ9MJQm`}#2%4hK92Wx?-gRxe%3THWZ*x_|#1TTWG)g<#gHtA$2IEOlGK z+v36MQn}t}mN(I|I@+BNM)3|=*4tU$8IM!Dz`b5`w9JT)pC4^xc$yKGRB^-Esd%P8 zcl=wF(aX;0Ho8~NGcnB#$rOv4v)&}nvo8c(*$sIn-SQ;d1pd}O|CTP%!l%Z>b9VWH z2I(ekzUPDVbPGr%9gH)cUoZxt_+fj4DF7&zG&+e}iQ4wV#}{^%t8VaSFUOaZtgknv z^1iuA<;Gm`wLy&W^Wbu$&%NG}zy=EUORN{MCp5TOk|E#u(AE|@eXh=8X5{tV`oE}V z*3w$iHGfCyB(fw4cXHe6@HI44oYSBP4Jydi2DU|_U^73z9_!3zWnv{kYutK|r5bVw zR@T~mled{XZaV)^6?(m~clCvHXnJHvMZvz!Lf7R6x+b@Zg`xbB40biK_aB#km^Lev zdc4M>KQSe)3sK67KyhyTD ze#yWc8eH*>X2ix{?q*&ejjQI+9;$|C%wd|JV_Tqtf3k=X3d`ZPVn_QJ<(gET_Z#`b`;@$Zu~vcdE2LPwZ1R;*{}ao_>z` z=#3r|D||e^b;mM7g(JL6*>2{WHtCrRO#8mj(0qBHhN94m;YxI=q2ZcJ2Bm7Z4J!Hi zPs^aR#fUpU;0omu-B2~Qevs1LMu!HGYy6iqS&N)XHq#XI+DS`&IvWY$!pCOzW=shL zvKEz|*~E>NbdH=ZAaIcUzwd8nPQ6b{3mPWC>u(ow3cmVl3yN zuP;7&Zv2c|63sTbpSwluq8FIST58fpM@%U#*`h@6_!#Apjk&~DiuDY_Fn&>n!S6lHp{gwsnb_eC70)z+lMkK}Dui`$$ zK7FcSXZLhe5s^lz34R>FZ{b(Z1C006r%P&Tj0Ps4xxaby2JBlZdx+#N9}=J+xjFHB zy;euH+*HG)YdPM-1CLM*CK9s?S|cj`l>rQDK#1j_%u8Ox4ztsG1Qo3AQ7k_-%e`SK-H8c)UN=jZXM z!eIjJLTCyhY(Mm4Zgv(#GyKaj@`)cmK)Qkhs5-NLi?8;bXYK+P9jC;_%fPsZBDd(u z{XxudYR@XaJt&7CqM*X5-w5MWNTY^_hUg{SKTm`G(+*HiVGz1BOiQ9BhJSh5svF+N z$?SiD?ZgQ*SIq$Q-i*Rir3Jng@{z6;@Dxqg$!}*}!~*9*#d+QSAB+ml<=#N5_WO0_27Ym}6fw5Rmwc$M6e)3f;b zH4|rs_wnbc?0$Uhb6Gxm=wxBKd{hLCWVE%3zA7o-S(}_@ze$nZhN*R@80(7xnnqKg z-qO6`etxQwl9Euq0*5N})`VK@bCB6NqlTFEIWhLJC$S0R%z zjjBI#vM|5Kfc~Ys-m3MDQ&6YMpBphuFFY<`)|B{yJ^T&pG(W_*s0QWI-g!l z!8Olj;ipx`ht9}`n4)WA4g1(h=l6W5pKRQ3%ffr6*=*(8bdW?j-(@q7=GpGm@_A!{ zdA}a4p+C+a3e%@&hd&xp_8!j%dycH&(#cNKBd6j&-Vb@URfQpYbgNV8!F*pgXJ);B zip$~#|1B=dF@y?}tU+FL;pa~@aBK?lEP4S4<$AYSV-Vpw!Ybuk5-2I>8*7hyBjF9v zf55NTReWImB+Pm18#i6{KlJwAJ zwN~lbept28a;y2G@q{L0cX_c>)3wGpy6=0g2x)aW&^NhBy$ij>k{0dPJ2EgtN4t1u zQbXs)S+T%(?yokOwmj3GoZCG^vhB*v7WG~yi{lep{CLyesu&1)EI*Aq!Lq3*N|ERM zy^sBAR`=N1@;Sqqp4;NZXC4L$Sapc_O|)(7;>l#%^z1&OoU|NzY?$;a=5|A|x147j zbm)_;#1iRUi5}AFx|uZ>=+@abd7>x7+*W_yXukIdykw*0Cv-(B?lrM~JyD|bNvBSq zOuW?oR@`O8rh)d(2iwZK6c0ZB6sEcJv}EdrU^LAYmzZ^jaPQ5vvj#QYdEF-JIsTL7 z*P7(!O*FC|ERB47G~}_p)#KR8xPEyMPN=P_=l7VupLb>+YvWpWMnY2J7e9~6Srl#*?85piec65qO{xIHnAsl!XkL= z2`hTCLN39<6mJ>yKMPb$lYi>4Z0LUWYsblJ`3gRR+Ff(I85$n_9BY2Mu4Xy$Td(Fo zYiLkV@6n&J%OQ{Cu=YCeOk(&X-UnGKE9f0 z-yWT;U;L{5S?4F?wII(AtL*P;GshaQH_M?8mSVIA0x^AWKvpG!F6~*oD;|`rhCnLPXabV=3Oy9 zg|?4n+_@IgbQEKUV;|QVX75U@nNgd4HF^JIQgAE!`k=T$%zcGefZYuclh-uQ?Dgaa%j&`_ zA#%N+&fkBZ34C+iN^HC(k6XZ6W}e<@HGY$hB^-*(S4qD*ot{-?`^}UEeDEri^^+um zml4jsFP>>eUSaSmu_=!J_8k`& zs>6pt&>$(Bh4vc!iIZB{4|81>$A>2-*1lD2lQMRNN!=gAip^(44eOH0`9S4pI#w8~UnzeAu`=WZ}u8Nkzvz1Bwti>%P6uwTL++NY~ zNkYbRFt}PL_vq4{sFiFOMLd5qahL-v9Ka z2>#|6Vf#8XX?4Zvn=#=vno2J}e01zI5}sGyAz&6N{cuAG^MQWQ82( zCt2wyZWONXz5aa?<^)5+?L@<2OoVsEE0(GSoBf>b)p@4{EAT%vVI#%5{*Mp*4doI~ zFMaJ8V4yG%X}k|P5%c$?!7aOwMLa6ny_1mY$!nMg)ueyinO^%un5CS{nlb5e6p#JA z^4orDfEuXgvK3Wta$8@;?ZLW7_qmqVBn+;6j+M!yI7~s6(Z?xc8C|?Mf1vP`r6B{; z$S}|1W%Yt^QO6puLXqTA?Vf_e`G&vRUbeTLwYYfRpA|Zwi> zl=p`w!ggl%ihqi-mw29^wfSKW6hPnro6O7=$6e$lBO^O^uHeZvm}YcRX?xu|xdkK7 z`Y7Z)559=PVT}d8F`q>f7X>LxE?hWJyTed!>M#YR*wYIJ_wET7TPX|K8Es124yzNOvag6qx_` z$$UNowlO!$$wGcmUK=jf`^tu z=WRGb_VCuGtx0_Z>%1bNj#*SR$|4=@&2!jEq-jya;LpzQ%PAe z=}XzpXZJvyDg@W;fy>y;o?@*e`tpy*Lk%$so9Hp=FShsH;NZkKtk>c3gd;uE)J|W1 zM~2PDm$7!;jwMIkCyf+syhDYDEWb4D8_IK1itZ3OE69FA{Dv0Br`?fxnQ)eIb}qU= zvg>rA9k2N(@wi$i!{#d8@`O)rptf?B9nE|h&qukBT|y&ra@VbC($`6Qfv=ZE?esNQ zdY@QLU_1>EZ6cA0HyqmC1bFe7YGl3m(6mVYBQw6F&eak^{R2mj9tE+R{fC!fdUiu! zeRgcW+1bV&LFUP+t(o25E-Ui>NVrdY=%C+dAi<--X#O^Qq{HJ$^||({jvQL zPnOT1B`|Gu9oNv+jk-H`xaxet=8hGHEA$~F`B&7`t{E96#>IUp2^l-WC!bw9ADG)8F3R?oWY@JiA+H5~ zZ4}N-1VZj9K0f3Cq!iu#ceHY!K60;mbTzPpe>@o$O}h0@ZoiLfvSMKP1Ih8P=bc5H(O0-kNF{&|h-;2kuglU%=(l|z)!Cz6A*M*s6HYx$z zweYJjx- zw+oRTD4Fw^(HHTkz8X8cD>*60_E{qcaLw&Ece?c1p4QPYM>D32dp5AbX;<@r=vn3A zabW7hKLd8>=MB1a*~K^L<&5ds>AV4s74<2ASauI_KD%G7XsG22Ji!d+>tFfWv#PG7 zWmeiol$7Ys^)gk~)Qp)pJ1E3nbS%ECq4Bxy6Zyxk9Wp!eYE0JT-#Ol%FLRiV8PpK5 zjAqkNd-+>lebr|@U?3ul)NIo9mqWzUg$}$RtQ6l8GLIj2{PrfKPCJCgwZGO$1#9`< zS)Pu@%6gDkF@56-Ex#3K*Q2mq_W9i~xc~(^x&+BK1!+Lr*X>s)bbUy8}o;$ zQg_RtX1yz2dygQIonc2aXV3o)o3IAOb8>efjK}3hv{6QIB!ae(4M5$0_iJ0a2jM9308UF&;_k` z9XF7$m58xk-DwgjrB_NhBXaasMMFiUWxZhP=EZdfs`E2d1#oIu6R^^@ZhRHg^8Tif zr9}47&j4Bx*Jpg;yifzYrc>-*21ZY)J$OU&#%rtY735vqym|A&ksi01r@P-|dVflP z%XfN@KY0l@8r5uzm<7XQ1m&uP zN1)-&n;d0%{=+9YIl(0MvZf$@2^n)?x?Dn1(pl4{1QgTPwZ*k;&^91xf&JC+HiA62 ztZd@ivO!McOX7zekJSx(*A+^46ufiXM+uSoC$wfTYHSR9M^8!54in1?6+8M$+42`!Z_hBDLCnMk6E}8lg7=RfgB-dUNPsy zzy*~!c9YmYN$E}jPZxjw+`4TWF3jWr?)f^7zk-Qg)!_bv!ZjRq4Ztx94+IfzAlVI3 zKjC3vS4!xcO#jJQpW_M?n}a14HcHyLZdn)?l)XhZ@)ba-hFEStX#e`!a7}&;pvDb;bQ1&>(;; zJgUWV8^UWaKHc#Md&+lytG`Lriw{8qc9?2AG4$HI5AJ~U3v~^Wu%R1**IJIKl7o%S zvF|>H0;)vwb8D-fg0J=UzA}=mtgQ1t;AW;65fChOy$3;Ha(?ud*RH8u3_!EZfA(x+ zVpwdbKm(&k7#?Y}S)Z5VJVMJws+ z_n{~M`ZYjo_Ct<5%#!BBPHj6>JMYuA}K=X@>ZnNL+eaitN9H|g7d&nS zviN80`1CGCL*<6>#j?~?GqA26P~`m6dG)Dj33&uq# zQJ+ImfarktjVtGBvV)=`Y2&d@r7_5P?qgSJaMhU2VvM-e+BoJi%Tfa%5=|c}Ee*+H77|gsXK8jaJv`)CwT@Cj) zeAQ*TKh_riv#G>?oFMq(KW4LmjLgi^V#Cq=lmD_KmBl|$K<_2Lm7;}XH_`s$zO?3p z?Ef@g)0dMQLFR$+pSU0(eaFQ9C!0qzUEdO3!4DmR|JzZ+!uG0mMn^?KlA;BBDQG1l;b~@PYs-8{4Z>B|B7ACr#wz@cz{Us3Gypvr(s;C}EH2^CpZhxiQ@M4E7z?`E zcrKCu_bz=bLQWz%`2idS5jBK-Ho)*;v#2rER##W!90O%Jfq%a3^Jk07ha`P{eR!Bq zy4>e+g#j3%YG8NAGzENX_17=)JF{^ug-{V75lSHyW|U4~Id`5ugu$#0=&5}TQ9^vK zE4Ro#35wu=QP9Qc$sCq_NK|SSf>;G(jkMHMl=r+>i5SvVb@i{Ut*s3WDzgP`NvaIA zw2=s@2weI%hUA@|3rQu;QH+&|Yfo(zqmHwXP9Rw`_~#Es$_9F6<$Bx$x0jBJIR~4c zT>7i-BG2JO8ltb$ElRmJv~Pt(M5qiHvDJ3qX!5{qgZC~WDyrLaLJ`S3BqZO7adjGM zY6n+|B`^gx%wamYbiLvwiOqH9s*-bib@OfneE!$JXX2|skZ)@+~y}W#UV5Q^t{644o zzyp9XCny!{>>q1tI8L7q_%q*zb%2+;nwq>khwuYS6nsQmR)Sh9(aY>kOa;-KG4PMi z_y36}G>a6JfzI12L48IZMGOprzxlw&O&Zr@h=~(~hfMqMdK-S@gL5ceIS(C6;PC@QYhp%f8=L#T z6_`_0VCiV7`d7lwk53D7!5vFRXw!O&bRFT zu&7t{%lskHyOvr|M8pL^P;qe(_S>1(RcsUtPmv50SU$>wQ&! z&N6jYAua&wP-Lr3K_VB(#&`B?65HC` z;4YVKS6qE@q4&N!Cc{b~Y=x-lT0SB-g=hgI;nGF8!e6sunrcTIslBa-F(x@pJmDx+ zqXa8#g{se{Mj5G{*H%is_pjfx7q-sBh(#HuU%CUIXuuY&ouHehW~-JX3*qTO^EcVq z!Zt&mkm;D_EU<KYe3o>^!1KEG;Zx{;)dH&co!jg>XnESlHMQHW4&#(V7|pVF?f-8yMbTxEhH^=)n48`~pD@Y?Nj3H5 zNrF1e1VH@bB9X4Bs32Y>tHwZ`aIe63ack#Q^d3k{kUM`KD=B7D8k;bBVQfoh6b1y) zA$wqiKE&Nf?PN9V3A?<&g*#R@-qso^8N(>m-PczRQBc?~j$htV?k(?enUMoNfZwsA zH9m9xZZEsjh>C$aUFyzw22vda_Sw^)XxDX(r(I*l?IuM=Kg!R_9lNmvN3Fz@2MOCX z)~z{KN6V2n#IjG+?N{*j{o1MV0XaE*#bOU0@xxIyIYympujbNqRZ0ei#=L_|*7oQ% zT#bi*4d>+N!rqnDQpm|lV|7_=YM_6|O$doLVXOY;dNnEFsJoxY`UDKFfjxX1U*A|eYk!DCRkZO#b?YGEr|=)*;I`h&?>w%VYaM4w zyKFy{H4AD6oI{=5>lQ7-2Wo!Xr8Lh*d8Z{|KO05FFiBbx)KXC(Gfh7du^sswNG~aL z^9v&@Yobm2j&qf5*%!}grj6H#KTk~^#~ml_$V5>4%Rf%eVfJA$PV-=jY?@Oj`(gc< z4;N`c1X{a2e9}nJurey@rAoriAqtTvCd!=_9_L8#^XHQ4>ky(I{+8asDy8|L{6jY8 zil5)Ti%ZxQLLj_uO;#TSPPn(NjhTbP6BisYfMVr9wsaJ#W7t_68bWK!jCsOc(r<)M zz;94Tv9mw%&?Lz0^+>t93tiX}^C%&X?8J{0nRCxS9ZOBzLbNjnH?0-1rA#q8L`mrF z!`ld=sH731k&BZ6m38VtBCYiAl9r%mkHN&w{E;pGHqbU9Lj%N^I~IrTRuOp7d$5K;_m=@^%2OkCf@%`}eb1mzI{+{`_|#ywp^+5J^bh z3km9vlP}H<^mKu7^Tb?72FJTORMH8zQJvu!Yg*Y@%SZAq`0v=Vou5``zf0r#BBpU! zN;oytnJmP_qk}LyxTS)Sh+G+q(?aqfG{HHsOd(qJ&{)A;e zCc=SRYj%1Y$~1k`jdY_-WyVQV6++Qr;{<=#;n+3nosoNqYH-E3YQqMrET!@_|qzczU%M6DVaUvm6?%IZ%eZjLaMvb5l6UdsHQ_3(;bgb48;pH z#D##FG*n?R$oPqi_p;K_$Haoz1ZeT}V3LMD~HJT=KGoQS{lG2O>dxC2Pa zv*--$eeV=Q9c#CLdQ@Wga#GVSMSxt;SuJ2Rn|>9@Qh zBkh&|s!LK7{gglcp3BRe%w}cz;bl>e!Mx0?1R9*glTJj9^~3}%71eo532)-7g>(HB zZ2ECGrQsUD)ytVzIWRG71A7igGcM{(qD+kucPoO@e|WS0ZZeJ!Yb-t*KI6UZ)Kx=S z!kxF0Q&I$Q*oufmjz_^r2cqZ8moKMOETMU#rlrM23Xk2uXbGl)F2ubdt5`Br@~sTM zYEbz51K;#aG&_dpztq){qHqhu{Z^3^J6Qe+LzYgr z*0wg(Wh^4Gn6nRsEezF?<61E5!!gpHcE!&^17V?x5pPyB!?I1A*|Pb0_rxBQo5eZ{ zJcOD1)g%=<-cAr954I%i%ci+ijIcRm=qb@?GgMV}0(DimPiN8ylhDGE>pZ@Vo?@ft zyR{U#LnbsW8yXr!!*&v`UshB5f=V6|{ic{Rmf8yJC0E#@bZB>Gb<4d z;&0boN&Abm9OEHH<3Wu+x;nk93!^#R8kK_v`g4o(|2F8QU#ikIpk#qN(r;OZ5zX|g zOw)NREG%ka;)hvmhfflANT1+`w;sDjw;F{M375%a&ofr{rgAH z)jhwbIdB<8?0B$WK%Hbr0yo+r7@>Q=Y; zW(z^>D^{@$9M8^uzIsv3998Zr=T?Fh5yB}ciDLSM2j=DBl~YFG%!k)7>0Hm&Y$H!^ zZ}_4dcc)(&U4@#oeT&WLjE;cU!df6$F$H5lf-g z8o8)xZGvP8@>LmSG;^}E2Z~k)wosx=5cSzD2~V-RX91D@k-zUC$iD=tH9Hx5);i|l zi7m;bL}>+Z@_%L#B^SXn<(wl4L5&o5=iSs3jEs_=YiaxFgf)&<69}q*U!BnSCl+tN z%LzaA|7~|kack8m!>i;&Rn-w{Y7+6Vd-p0(wIF2zLFjcwa)e~b!<7iyO z{(Yp+vuV1$=Zp=>{{8gWQ7#Wiace`%q7X}Z z(I-CsI@%W^DQ;*8)Qp1+41f@bMlS)OB$9pzkMM(`;54UXFvhQkX=t>Yk}kcv(D~-Z zu1!{4YlWb;<9f!Ks-moH*_1#Mq9v<$d^aiH+P#FMTasA*w6hu$8>6uvb%hU65Q8@L z@X@1yw=kMUUETH8^2Gf*Lc)WrtgM4tzUwXqZ>Gzi!#KgA2-;$qceTAW#=2W2Ry|ym!>yu;%bnOs(qaSx28vbXq8O(>S2D<4+gME{2mb!3Wfg zfHQe=|3jZVpqm#O+U7&ehY-N)*RP*rySBc0stUD0XZ{8h=~Kw{gpoc=kpa8pBTSJq zNdsDI`p0l;6R&?D6bI-r3dZGi9(ERpK`BYX8}DdUm+g4`06(?8MVpoYGK zYQxg7k@oOmBA*1p@Od8On`PjV29$ZY|BQ`gCrYLd*DQ@eXjWC^)MwY3o6^S{TAu8@ zVQ&5@M(&|YgBm^Y#=;o{Xm)yfI)UhFVqfOIQG~M_WzLYbc&(YMG5fuzlT{0Q&4N)r zff~2>$f$4V>`&v!k~G!%pEy zxR?ZfH*YJBXLxsBOrH0eX=y;3>g;tInr+15@$sz4mEDj+!wfsWcK%}`0hb>MJG{iA$u?tf(MivlS5_>zl zz5Dm;XB(2#nqliAjk5YI*WNbGhPE~bZ15c8`s=e@E&f11Xpdt+!7>_B#5z={f0EUb zw(zZm;b=G)NR`QVqSu!-I@>9G%G@Gd>BeK|hRY-9>u{UjSy^ArLGgz@?QLz~wG8Q5 zOa;wUQXkB$)SGYh-SZKnirq)K#=M2+ke{F5=s0zCf3=Ur{n|V8%zxv(h%pn*iL!ss_N6~AB7j;PVsvXpZR1C10k--+so_NW_*|l z37`VQml!HqF6pSZ>>z9cwk^lvA=W1MD3EMauu^&!MVqqT_B!&$oa?e=94YT9E5&Z4 zuAA?Q>s^@}d?XlGh%*yTIOWcZsW9@=%`e{)Q&)6|{JO~TxM(hXE$P^OxQ?KFCfQh4 z^L)o2CEH_usQ>y7A&o6+h01@uD8|4dU5A$GZP5KA^~b7w1Ut^5V_jMPE4unG15!d= z0w%Hu$5ZP>3;K0x1z8NW`tvAL3dAeR4oePe#++U(C@28C!M69xB<_cnjomNmDpksA zPAlrpKsH z+-(E;1yMx23e(M<6Soy0Zai{n%RvlgzP-SjWfeP1PPjYTdB`;;VLM^?PyXV+FzeJz zT)Azjm%R}ic9-E#u>l_yMYMe%s;e*m{FuK@gYoZb1K?Z1iv#~`)QM%w-=T=IQPI(u zb39mdt1d+yMStrlIlkrAtaUOT_rKAG|Me#RPf&b( z_5X`l$p4SOIZPL#!`?qw3-MhMG(-hSN&oyG{=>M}C!P47|NPm#joJL)|4*oVSM!I< zK>Ullr%q)P=Nfc`u&Fqtz+bsKG<0~&r@%l^)tg4o!8Oc@Z%ORO?*dVT!x z0o8=J1NXo|YU)Cp;W|ui1M5bl(V+F#DjE*GqwRww+H z$(_=}jN$e7hqSI<_20Mk!R3-~%;p^l1j&@YKXpdg0QkHUbbI}h z=S6}XxFFFCVf6IZ>jnk|W#!>0H(~;qYD_{GU=yJCPf$&)AoL5?K1hc<~Tu(^Y1MoAAlhmj_QUORKZrp zZ{EHwyY>A(AUa6uQhFvoQBUxu>;gpBtOy>N2d@s9>l@Pq*U%Z$DPO{T{bp6WebL)`KUCy&YN! z6tamuT(0XYUqR7BPc|6?Q09zTi>GTIEf>U7%+Z(+VxCQYeboZ6P{Ilx_Uk8mbbzcT zD3I?oKb$S9w385b%@F)Kf~1$fn|$^SFaU3iPhnJ0YsF-**^YQcKcH$B9j~E+&qUPl zM?Z!HTm0-vy_-mNOFabbyTjzw>_$XMb;0XKGfO|(Zn~#1Xj0=6xxd~-AnB3!fp7`Q z%2Hj#;6zSF#`8NE<~{7Or~W#Z(qG!L1Lb~1Z)jK;)$PK~YD44#GfuU%Xk1La+Gui* zGi~0I5@na*M^Z3nI<82O4b5o4*OW+=Z~|=I$naWvD=_+tMqHpHng=pctmP@T2 zuNAi3z4+I^-Wf`+$kNRVxmLZ--`?HB_12~<7yZD% zE+Gm18({nb+tK}<#mIYRy>v0m*|OvHJ#A$CWw6BZaR^G4xhOz@S+gD69HSNuKg=f9 zs*av!+GNH$&x8476CfQtxffYk*4S;Z;$v)EJa_rh>X|(lEt=$`K&}x=mve*Dpu`f1 z9omKErKMMox3cbxJPcz(xi;c4n&1fy=oP>0Y~JE}_nrN~S`kpCtk)ys5Kh2hWI;W#wNly&Ky*-mktSSKs4G@mOMn9oy+FF3#6{l*Nk zl|$3g^Q-qO-5j$;xNDp{*&pxby`jJ`7oNB>zeOgo$(qs=2R1OMVEdB@IHIT5Z%Ufn zC2|W|@dli*cidUNEj?5ZRrWd+RSYlhbXS+sL}u9N+WDVcXeoeXn#>l+LaklL=3OQ z7Ij!|-D8SdGjg)g+mPb#OMY+?oKO<26nV=1YHu_=>(4piqah?#{#_esa3aSBApC$% z8M*gLC`NObC=l=6OP7NBcyUw_e*dd*x{rawC+ti(vLUqxMeWm!{e%P=1Y#CGs-|l* z@DLEl!!rlE5j7nTuZ_R&bi!^xCC>^5L;jRng zyH>N_Gb2ljl-$~ABP@Cf0;ZerGybc9`;ztg|KjZ}qpIG%@8P4UD2kY*fQTTH(p?4! zNOyyDcc+1>nJ!0-TzGQ7@1GyhwPL;n35wThfgT2~k#0nw!H& zS!OkSu~X9fliz82GyGZ4`e zn%1*6=4JXyb zVuPqkHrB(r5V%&Lc{n>ceaMjmV$grNa1hL)EYSLXw%gF8%V@P&i&C~YQ%ozWvUqwi z!8=>lI|r~s$kYIbYiu?_2weX6kUZ~0LnK8d4$Fh&_uPM9OOHpdwg4d(B4v~_cFDOY>9jcNPetHPOS?6`L&c}R z>H8D%Ir#1YP&0vMi|Uiz8HDBEeS$!|hyKVb`StOEYz+!C@RjoZWQV&)I6`QPRS{O{ zy7uvX(0~lhe@XCeGS8 zJfT47SixaHp?LuzcmBT>@ze2mKQB-^rLFA>g`Qr`H(5Vc)h_uDSAxr6avYaadtIFl z5DNfQHSCH#qrQU>(DVAY4zIm}GxzA|C|HQS)jgYr{inJ_V9kS^b^_F|@hmusfNTN` zq8{~I2s!G1-{)!V1mjgJc2_-IQ7|RG#Jn-E-wa0g{;c^Zp)qpnt0QVHC`>05MzumN z00}tcF+J*d2)P^o{s3Z7T%4wzqYjoyi1Dn_Ar>Atx*$un2 z;A{qVM_dIJbfo`V!?h6EgAOC%xU^UZ$?Dz*(Sh)t*AO1EKJPlwC=bxtRza+({@L1hfzD`vpW>+5IS75g2i z3J{r|I!Gg=Uj6q{Tc86s68psa0U+P%Pvo<5bG1%q;?8!^Jb1wGI%=ChKlV&X(p2up zq}vD*Y2C^a3S#C$QWT6l9h{ipCnJl4jNl&aB}Dqwf4fRx$d-vIT_a0Hd0rIG{iddi z#w{Ew3;vd z>XOI^*%uhfL!WSc(E*^ zt(Sn4043r=Hs~7=i5LI=xMKk5vPkEix_6_F$CrxkBbPYM%*?v^ft?5K2T*s4-=Se; zTyqRHmjPCzZt)U+9_y_7c=CH>8u` z67D|xyZdDtQ^g#RPaJMP4uaW*9M)$Kn%kaF6uT?`sF#K6mGVTV!FO|S4;UG76$0>! z*9iVT#rIy;MN3<}uFahl>xc_sfvwwnO0G2w}(x}tMTi_w^ z|H*yIGg7U==5yHk*A=6u6P1-lJNNLuK;8gcHE`RRJZEtTE zK{M>Y*bVJ>?~IV<>ZxmJ&^wu!o1acM?g94?1mL#N zy$7P>_dkiIXJQfzsdk6~TU*s`bQuJEK#=o42Ww~;C5T=g1pn^i@t+;Wfn0Lx2J4Y9 zR+BG)*jHBajKP77@C<-=e-_R2hKLApV1fSJkH><7+Kb&uP{<@zT!SBJ`C!*K{|csc z&CcN_r2!xXP)Q*uBWwrGBkuO)lBz7qe^QtB&3q+*OQ-zx8tS=-a$K6ilX81~1nLFsWq>&ee@{87djE4+ znEWOCXN>tkMrAXAT?|%z_t>1emV_@oM7#DOq|$T~QJb12EK}AT(2-vc6==BJ-vj!Da z;uyq8bS-6~*g>N!NLcB1QmZ$LD9;GcnAv-R?(f_EJtqaE*2gP1>|;*uB`thvg`5uP z@C4BO9D63Hmz=J^yV{7Ooa}3C_pIC`P$S*U4v>N`^y;E$!r<+{e)doO>Z>)H}ngz3Q86o3gOEYRWqZV~!*-u5KA z{G001Y$7W9vAmp1{26cwlK^siq9Lo991sIn8Q@_EqtX}Y(cpF>F+6qwN9f!SgRegD zHGrN2=oWk_zY|gy`l866Kto06R6zcPw?VH`xBz9+7)pO9 z|5o(Ml9KDdwqaysgsz7#D!Kr?U;}PGkdO*CGBgYrfIkL}|oN7I=fJqq|d3@|qU z&ClxNy+zg6IVY%w2alz4Xzq$?HGO!(e1y@ z4o_QjrTiJZXtk}anclEIt@mhwQ%UJ-baaO<6R2B- zcF-hdlX1YPgB2keTB5VNp$zT)0o@x!&g2u%G&T8QlG7ikib9+c*s9fw-Z-SNN}3Ns zY;0_r83gY^ZHWGO_M*>;i$LH_1|%-l46ko&wBeJy%f!@OUytU1 z_hJmZ)X^Cm9sT-Z4muZUm!K=rv zVp<3KdULh_zk^CNSW=`DG=0F`VgW!D`Xp@TqhGgyIFba-(q2V$J_T1teG#&xyH`L( zx5NjBoH;C0C{aLc0r^E4Ov8ZwfJ^>E*<`dxVOtYAF7Wb!;)f>tMRMAt%J#j7o2YWy z(g$c5dI+ekdC6c(aZr3Yfj|UyNPw4^zN@&RA^_KWkVXJI6Ka*@Xl!FGtzYZw6EMPY zR~NSF9S+N1P~F0=L>QgYz_!F(o0Oq>eCnt@AVHH#53q!!!7}X(XGJ_Xpo6dUV--!h(rnd_{EUb^g^QFNtduch9Uplb#zstuXBLAjA@bZpXx=pKRzY%noLZ> z>XVxBRJF+14g^U{wrrl%m;b>$h`ZMcNZgumj;N*4taMH&B!N-~&G~M1Vg&a8{+`}9 zr;vs6qymZDU)0j6;s5)GTEoO4SLS8vpQ-PCA{3_KA3y#%y+cC>05Jp!%PN`%h4$rw zx(E&;ShoXkFn~;RAv4GKqX#iuEg~9#s;1@X#^%*4TwUsy<@7sace2oPdK4A)l0(_++{f2mBsD;KdZy22Tt4HZX`8GJ<|k2?0_MlpR@-wtz_wAm=j$VXRc; z0uV#YL2^`KyO@ z%Uv^d2iTQYi9cqWkU)hGakQ_u7r+jI-&IE~L=g~TfiY=HMFn>S^Zom}Ag;QMOS`eN zlSFUFV>R^yGD0Z5gv4_p@q_{s(0AzU8bU^1o8=7AdIqRMSx=nf9~4U*7?;OC-co$RE?;P2ar?b)C4S<)erGy!k?>>(I)D)81{SDG-`3 z$rngtc${t!5*jxn;i#FaK?6Vm?#x1aNAt@D45(``+IGfFTYEBAI`}J8{R8rBfYQ66 zc844~FO^@u9>!ne!8IK5P%)z#wY_TG=Y^vPlU{;jOy(ucH`F$!788<|?ce7Ad2vG3oG|H@> zEd&-JiFh0z%?sSHJ39vVRz~s$JsV9!q^P$^#nF+&32nwO&(hG)03>`^!Lw=e&@zPf zzu{1R5;OqA!jyqk51YtDOREcbqu=qmp^a`}Fb~KSEWaM$>;jDtkim`ZZD5}MfR-BM z)M9-=7c)kR-nil-ls$nyU!T`#8u9-I7nZW1TMCN0tlJ?&;PUQZu4X(|tk(Idt+kbg zmKLC*Yx455+negtPG%OmD$&}k#V}n4{Y)muB9IKC>AhJK?+GQ6vTr0sQ=nS`a6HHg zXP%+H10l|H13mUZQi^9rx}fQ^{fnYrics&@wH{M=cFs{?@c}6Oew#cPqZIH%Op4rD zSXu&mv@m06OzlH5d6V)*Dfr4mivRNEz4;HO#{df%17{I7E2K}Fc`v=t#^)gBMZs-? z6WcS~r1W&cJ9k!o{p!lo5#Ci(`?C!62RdS0caATGe7?G}5fT#2T(odP#rJO6xNBqj zJ2!jgT{1Md#JV$Vp~2%8bSoz|b!M_6B`WSxCj}nvtx|DP_`?cfbxoNI=J=a?=B(YO z-*vw@wb5^$s_zYbN^I&k!zN?_x%+d;N)LvU&YX@h=dzp$5z6xE62=I)YREp<+2=}r|iGIJpru*QmZ=+11yskjmat$W~2_wR^l zF)}d$5$6HJkDe`lgJJM3fSe~V^vNtH@Wg>a!3sPEsCyKMWMu2404IgqY#6plH@{z) z9yyc_K)8iO%2ZWVb!l5bq6rwI1BLJ2z6J4fL_~zohYx{=AvB8SgSpy(JoI`c0WF;M z5R>+);0Y;9??Gh%$%ET=CmW4$Arud z<=1N9A5%#vQf1$gvp(_8H~f);hk|6g$2$JePR|iVkB}*o`Y(sAOG9J&WmC^gUxeW~ zE-xMM?)pwD4Rl-Ytw|%d@GqBH2l`}Q>yzZOu^ObzY?F`2`)V0N#X{c%bUSuX3#TR~ zf`i69WGisuSy^qu8459Dvd(*=)ap$iTAm5jIH1Of;$UA5N*Z**XE1{%Gh6_F0D!%r zr3tYtkugew-(fBC^=q_+09eSPY{QjS;h+M!LU3BzB4o_)JIOFb0r==Z41wV=ppdvi z$nxG8TKIziz<`|!obAE-(DQ{_fCy?NIBbUoFjMK3m50!UMg+Tm+bt0b0E9c5n;~KO z2`3nr!`jN^q;%H!^KUZ`m>`k@#C?NW{^8xbxggL&)=kaNLnwKMCbq4kJ!%PyA_RTK zR;%4kpU?U#p)pvUrVvjon2_<$!__?%10sD`S_#vyup4Z?ydV;! zBwONTIou{qOyez7$K3gXqEfV$TflCp71yEcq?P4=87a73SFvZI83FH3K7rYk#YM$0 zKv7Qg;Bc|`I(tNs4-A4Ej5&;odUpFI$vB1o6Jbf(o`WHs=or3A?LJGe@w+wC$nQ6- zSD~Tw)b{kFT}b7Nj=<*0N<9-vm<7ZPbBcHL>lLyxDpwMu6S8l#A4Hi|S??}~GC+|6 z6!zY9c^Zwz^HQw3{RmI7;uj$$fk8os#|JhP*iF81ZJe3_6wF2_Gv*D<%+6ZfS*%Iz zt8N|0d6|B!1V<+@w?5~jNT}s}VPY`2ENjP|764BEb}pq%)W_Nc&3G`r8 zGdlGbRqgPP?^)Hp-q}Fb1W$6hRUd1rAoAC>pj+rb|Ti%u!8T7`_2t0YH#u*OD=Wa~~9 z68mm+}9ls zQ}dE|UEHi6n+m#DJNxt9*2o^L$my7F%5@QMUo6iQFZboOVC##`@JW8&(D$_~IK_PRCD#*gU|EFwh7uEMJ03rgQ0wHk(B7>b*uF`r4tXu(RQXr~{~8RvZC*9t4a-kj}ddy?)UJE#B_ z(qtitpR$ue^u1{#M6j5Hh+uM~hn}SrHf6Y3H;XRlyz=Q zhh;isvFH7)jGkR}F`bQ&S5mG{&P@$CCJYR;_&&)!Q93hsylXjpi_kS}I0~e4kZ6~d zI?f88VR-?ORUk^KQw|5CT{qqP)#?3v_g+(%R#Xf)&+~ZL0WMPk;Z)+0bOo?{RY3I* zNe*yworv&R=<(L7v?ik1JUEX&=)WEBb<)kKy!nRHXJ~zvf$IK@{jqh{TXvJUOGDHa zq&=vLz1=MGOY!mT}h9|DZ4) z9C;(vkgr7-Lan+Rcl}^vx38l;*+R1>kum3x#~L}aGIaaC$QeR%bgUu+R)rA*+c~M2 z4bocHV#9|^Mag!4U7~tD}^ZNC`h}QNSyojA&`y=SlGP}hrKP=WZLyy&(@7bv3JJAMn!Cm*mp`}XMI=O zE@&K|>hIe^V$m5eutn*~)l;itt7({bxX-tjW^c zg_Tw3ZT;1;<@@vP`n?}Bz-L9+)os2lOqQGM zEQ=iaZsF6**DqfJzW~Sr=pBJ$S&4m#kerN68=0D&{i#4aP6ApCeC~H-?JD@GnmV~l z8&41GyD7+_gLN$bU$Bh`b0j{G5Ev_WTdjQ+r3yKNVcV_Tni`K1^E>LSSGsiX7N83i z^rvrQ@x%nK$Rg;bQJcvZ9Ad6QV76*tp8rPl_mTj-=z|e+S69dE={BwuafA2~kmcvk zpKq{HBgP-=qpLymhq}(a&j2b2NNfQB?tnE-cn;RrpHJ*rz3jtnBvPf^_F>MOvxZ84 zuN96paC({{9jV!hkHIPO$>2Lf{R+b6pUAwv6`Szy z6NjsF3E6jrs0PUsx_O{h8Za=4V7dC=)gSj=xv~ufEDyjz4h{|=yVCH+==%EKBiK+J zY5K8Dg&?H(?`jc<(*Ioq0>S>@#Uc>DF8ue!9z|d%1RjmR5`n}P$%X4Fbm8wNMtIf? z@2+x_@PGOMlZn{a%m=cg0FHe3?gIMe@BHuYg&QA9@Uj%bDVq&4Zu^uMVKDLjI(Uyj zD$x~7_V+dI+~=HpGx?TIZpgUe=i9|6OL5-h*{_7vG18EJjYkwoKQg$fzG*&|dTzKF z-mZHTh^#jXRz?KJ#{L4_7C2l^H`Q~ph5avZvrnA-yv=5)Y8yl`@@BSzP)c}x_F)m#BWoL`PzWnzoNL~Jt z$@qvRTFHs1Ucovd5^q3~2O(v1g6YTMMqXZ8y8m`y4KeWsP@F_-0O=t;gidn7o z0)H3Ik_g3swL9udx%4q?8OMy=tP0)c+#@mB3X9% z^wZs<)OJde0Sr=}D}Gh`S~&q5q%}3?qN@vRwiwfS#wOfYH14#3%Gm@ zVN9u>&mpoeUA)+) zVJcc2^H4L2EkeEA7Up_LNJ&{jZwFxR$@!cqEOXD~Gk+K5ue{SY6=PM`pI})8^V1Z5 z4*c@4-RtM#Ht9p7d}KE?Uqbe}vPx8xnsj8IJD;x2@7X$e&B?imTsj%{+TglU_31V) zZa^cXrO<`bXgn*f3<_lNtD%Z=FKa}5zoY@9-re0DAibO<{4<19f1j6UL}I&s?T{B8toXVG%7md+6rZ(9lipn1MV2*?!TasNX?)nsE1tI)w_9 zekdj;vg3OVW7B9gF5ivENT}RxV?vd#!m>C^UTDgBbS+RC5Ys3n3{X(Ou zx|^plOzflUQ;T2Il%l(di#xPT+ynuLgMx~xwCP8U$V(~|twc?8P^H@ot8HOR?IH%z+_(P5vDmpUI2K4Z&vSr{sr)*#(t3L)P) z4EEc)OH`fNW}BmK?@#JAd)%R+S4Ouw;ID(JM%nR5t=#$Md{iXA(|&42!gz04>%fp) zx9Ql_Ly^tSNZwA{#UVp_;n|I(z8^WH57#6jqWPUZ|99|@?)*LY>29g|g>#rC$1!RK zi*Yv7P;zSxyMn>9W6?aq=jFoO${blT3u=m zw=L2yU>-8ptjHbdIJT2HrhL$u?r#)%xOj!%?PQ|pDdtsO*M+6UMW{IdCE4WS@+ABn zxMGJvtbz8m;^!s0d9!YBB8MGqjNs>&byONldo)=nl0x7$KVd{>VMsp0*fJ1AP;qW( zjPG@zGI``$53QYO4!&MgZof!0TMA~vU7B5sm|_Q`l<9O5XD>=KUqZ80Y!!1d9S13* z;gvPt*Gx7)YvXSWd)5^$F!<)j-=Fs7WK|vx&S6Q}=yr^>Gn(61N^uU$t{oi z4O5VEojog`xq&EJSWdy2$xE5@Buws8y6_3V1a7l6Wv{4P^~vtKO8i6(r-d@{ty}jQ z82n)k=lx#8vBN;VfN=pk+l#Uiguw^*!Z`Y^o=W9?h{88`C4DJ#3`dOFgq!p;pGo$U z<)NU!Ei4yFvCahI_sLl+Vn<&ED>hKGKT~h^8y2EQ6X=dH=scLkOVihL6Z-J|708mp zisM18<9jQO>P8MHk3_fPT}6oZt(xGCSBuip z(^Jk)M^~IXcOLT!!EF>vdRt$t;Jj;<{RkkdCdN2s^fahbJDI7il-hgoY!z4cy1LSH z4V*g}QYa-S-x{CSip$87;@8ZX(5#!u?fzFP zlHyqRNkfV^JL5^mu_@sQCK)lWk}+7omkGPJ$8p?YC(W4m=Cnk2_O=ouU^(Uck?VArOHYF1VNA6e-IaB;fJY9L_^CEL-nLUSXG}n0T9Wlx96XDSHq3_NV ziw2X_G*E^qhveLd$zcmu8@tOJ$)>|07luM zF6lKZAc|r%W--$ne&g?p9KWcdEn-wvk3!w!t0vFNI7@fRy(&()I%g6-f?_twyxHG! z?Pj`Hs>=Hwcf3bf#50Br;>K}LJP&W(CaH1$E|FfT>-Q>d^XYg)aOB2!%UaxY)1ooM z+HJB*Kks?-3+DqGEn@iB@=%hsskxq;E)Qlz1-!zFg!k41grg>P25I`Z9AwQV7RQzo zjn7*iIp1}*Jf4GvsWD?ob|$1Iht6FrA^of=Ww5Gx2WDp^@XeO z01w|w#K7>puUMv`8!AqGLGJRC%crPMj0_$pDdXDb%&tyHFwh;~P1i~qjCCJgzQlsT z-%y`|oGN@@Uue%%bPYowSnyk-d zkw%0*aV^!Tld5yy{9;;R*ciSOFDyfR&4%;o+;s#*6`RC{r#;5=GaQ~5D5!Z(Tgr0h zB{h;x+h(Q{nwvx%mdD(WKMT5H+S5&NKxN|Jm}DT$P*7lfOQq#1L6UZ<@l=nn2(H+k z40gxFNYbF_Cxt#v6N-8Yc6z>xmp;Du=n)gLsruq^p<82r0ws1=1Y2s__>RZSewyIC z#JOlbV^gC}jhM)5ssS-M;|Y>pp^Uq08Cx7oD;m$c3J+S9LnKMYQ2T3kb{P0v&QpFq z?uut!@c`hH7aFZ7Yq$1t-s3~3?O*4;7>6UdzmLBwwQBZn;FvIzvBRWWKV8kB>9@Fz zdtEPw%-_vU)T7?!ItJ%kQ-Gk+D6d6CpYzBVtAB7qW%Jno5WdA%;)p1TniY?>k-D6@LD*NgCMR zHWo=3vF!O~d{-wUz}QA*D*a>3Swm;j0p|;vA7fjLms)fvO@eEyBK$2hKHVjHxGYYj zTcvZo?=Clu!`$=MLd~#V^NCmLE<3B+v(x^0gc-WCNlI=X#ol*QKda8mVYL2GPAa*2 zwSToIEn;b8rn4l6&zUe}rm|UB$FqF-Qmcwv9Cj_zPF1zUXufDjJ=DSk{KF1tW*F0!Vce_9ayk!D4PK= zUWLmIkg_o7?->~ALDfm!)EU9y_Wep-NZ&{8HOH*&-WXN2tGh=Yq7r}%IzbEdA+!0x zm&o30UeCP18=DD9XN*iIikf+=1aO8XKl9qaT{=2wVW$D3Y=esmVg9?~my!i$B&hR7 z^OLrHKR|B|Y~sR8$j%^G?(8n(k#LPv~? zbnI&4E83)9?(=eC7<5Q!*X(eQCx~OWBN?Jz|Do0KamApb+NNUqf@#MzCEHBNh?Q_lZ2xQXu&^2i=N*rJ?f6}Px)7i>%D<7HOGo=Ndw6)L z?OYPaKwr@}B8)=Jr#z;wJ&N66LQJfd?2#k!Bdu@1*Fz&$EDA`hZqkX_z}n8w+f0%n zw?Y$7+hSX+>B42tH|xLb@n0OoKD8lHu5l7|=w0Uc?nZ~H!aDdy?I7-o*dmydte zHnWLzPDt~EmT*nUoLz-=B=A(lV) zxTxqYn;3wc9q6EIY+J>Z+W8)O@2snZ72g{5a#)0dQ$X#2B>$cnw_b7Dc6qIj!Q&|Q z1F2bJ%omu;Y_Br}FJmDUiNxpZZm|?~jByz07v--|M2g=odOd$BXH0jx{N|Fnn=NXW zd@lbh&wVN?mYyahSr@~s?JrRwlS^7Vzt+SFx8C-EvYdmH^U@`4I}*d!{~#%PVl=$r z;k;}7Iim1I!T1T+gu8p!cVtr*O&~;*d#Yjj1-O2U#o^z}ErzcoNarJEicP8(Y@L6l zaqA{5RcxD<48A~ZRA0W+z-IblqV;UPU-7B(qVyL<0kPrv(+LV_)QR8W0ZaYuawTjr!)4)TA zJ;kf5>RfcR5xBe2#j{t%)!xH6I>S2FP)yWZhYXz&)5|-&=Mj-j9W7B@wr&ay7KF?c z29FFJD?Vj;xJy>dIP5K_#IVe%#8miaPuqP(d<)p(9;vgM9t)u9T>}a$PAHK*w-*S99!kAxp3bk#JSkml+R--DFQ?*S5AC7k>0+T6^8dFkj`jI~PI#DiyK1UL*Z7I#~yIE9!;_k!|q@w0i zRiXSuc_7uNs8!6bn{84I7g!WtUo{+>b9M5@jJM(3ST9pN!wd0@|VcIJTgr1C13C+6?f=vh;{ zH^Gu}l%yx3Jsf^UZI`)yQ@E?Ug)MEfr5%SO2D1bK^0 zs6iBJq?GxsN9?Rc@}n69OH02}pTooiITu)3fuZ(9Q`7qrX*idz`DXA%v5i#` zJY9wHJm7sR|Mc8t;8Fj^hQ$L}5= zLX{Pl7_sn0d>nbCphe|C(C%!O*tZm-GH>oug#ogXm13Q`$CTLEeo&SG-F<_L1)5N$ zZmX}ox8{y3d1TglQvAVvAv~NAja>`;c>>9jPXqcT-W3DM^=U}|-ra;bbuP1My5VR> zU#5|VFAIA|QFr#Ki|o%Dl1Uu3mtM!tdUIzrNrd(4t&E&e(p;i=?pLOqH3fTvsyvlM znd#}&ckjY*94&3_?~U%FWC($tox@q5s0eX>H%H+X;DB#!?nZOkP@=EkJzRmO&^M4| z0;&uK9KhL<6E`*np1X*pr6qVchlYZ)p0;{pEZ92(zZ@5r&GOebLbk1)9q1I2n6Oh* zi^|GwN&1eG1fkagL*YYr)Y?^fI=XGC;}UB)XWl$3#J$17 z6_uy-y?Jn$U>HgT2}DA`v5(bcN`bl2iPyRCSESQONhd3nA1{RtaR{#h3gq|cNQCTg za!j3VIrA%UyKPU858(Z6aB(3#xdH(b8#C$|`_KRuxBS9Bx*r#NJ-4t> zt;U@fLS9BPFmIp>coiF)m`V2{4o1cY7*^us?A%`&1Gee_A%V~bXqe#g1EP5((B*;T z4TU_Q5h0IJoPlo$luyZUmWQAvaf^kL4(<&4Hj%C;8TFHdnJ^u@ z>NMG@?0m2J9-ndaX+-)+^Ywvm3E8jY4jQIUf@G$OMo5;vH`%k4V}~|MEIy_gSJ{;9 zp*ieq(z~UT(G?RdVB;N0f_|BxChw%QZ8!ih>J*e*8V*>wRP#}YwGYDNj+YWWIG zq=0!3>?)vT19=w24WO$6qA}2P09&C5NffXH%>_7QilP4@-(ej1G|-*t*9%%vd;^h& zhmEZ`^}W^7#Y>)7vcOg6aQ?nNSCj-L76>UQg5Lp;hs~tl*B1#e8j>3Hov8)o@XnjS zxB}FBFprXDPDC3$QNu>z?1Ogjum#(}<_z+jP~TK=V70Kw$;wja%LRM~#`6T@;orK| z3rc-ZCx9CA+n_TBS8?ew%v;p{t&3l;K+!esDETEPIiyysKTo``?e0^y}b70T89_@;& zRc@=1fEo(ipJ3RS$LWE=NU_-{HZQdGkPB;T64l)SSAo35$-fKIAfR3Vt$GNA|4Jdi zQ(Nkady}1Q4%SOUtdAaDynGq$F$0`)WiwEIw2Wf;1?xY4>`PMt4@^czhT1>axk4hE z=U}o9v%iE1IgXxnP?-T*Ufrat%}32#)XdCVK$e1X3Fyr(|VbfsY>E-X4-wTIU#W+^UgDhGHo4Jyv)dN zsQHQ@oAKzUFWDx-sAI0$w{KVX{|2f~WNr4$t0A#KmfqPs*6##|?9V6>4ABg>wWpws2kOz7GJhirOkN{RIj z8+r1Juv+ayv?^h%#8EMB4RWsw!t7#5>OSXog6&<7<^JZ6C+O z)06mSwqE}rUz0}$rSDTWQXC$J9zP~uV?6cq3(Kq;(laVjv8Dr#1WEss&$wC3*H7)i z=C$zf(1=ar_3OUsG4*A(Ro>3g*wD|ByY~EUz3Pggd3^Q2(FS8JnJj)3JXir8bm!GF z2V7Z@7iK#@$6ZZBim8Y2+88tikinAzg_9XP+6*Q-I&lSsFGXTW4_XY8QWeIjAkdk#hKB|<)Hs`2F$*ARK|&W zAw|y2?```Q(vB2q>}Jyp;SA}!zYn3T*w)I4d~B0m*cF%BL?VA8Vtl$Z`vC;i(y(tt62+Nl~cyvyYvCde*RIJ`?Favf1`yFnVc|gs8SY>zJ z{>6(cPHF}E*e@_3jq6I*B1tF`;v|%IW;-RyJnz z=BSxi)EJNLf)p|^E;jZX`hih@iH>%Mm;~z70#q{r;`ga*ISousI3&P%3QU)wgMSSV z&)?U#t*HqgoVWo&RBDHTS1`W>Ec_&g6O)qEt6e$S*#{uEs(XhmhXDqO3=9kw7VAHL zJnjA}krKo0@X2)(Xv}huZ)ItAKW_-Y^?3|WbZZ0IfgVa+S`+n+@zutXki0zQ@)@FiS6 zS+8ZF7wN57!pnXAXa2zF<|fvo!w*&j`T6NnVb@Yxa2~v1@3=?g=VY&8J=OSjki9cL z2(S?My(OPFv)cJl<#`F>E=k5k=RBR6^^q^~^%7IKEZ8n%V*`YL>-u#o2M5hcM@GP+ z5mY8W-Us@VGM`=qUtL4!n_daSbxAPbui;H35>*a`K zdVt4mq~@NA4S!7vx6+;XmJ>`n1G=lM=>NAKX6JiW!faECWKY?-#? zTZ%>m%}>s#syr;AdKjmgW9b&P{cyHba>R|s+kbT=&4eglJerl3p+w8IM!r`iHD)}8 zs>G~WTMW`y$H(PmnQw@u=Mo((H0($6v@Zq2X&@jV032XPXo-CDUbKcpv$Yg@j!+ylQLAUOOOf-{4qMFZKgg9^0D+k6k0wzhrCt zI^OB=2hK=DtKGAAHauFx6&?mMbCnbb5U7t`(rrD6ssbk4WguaK~MxQJgmVAV`cXY=imC zQYM8?jDe5kl6ERgZdrvzCP*s+aEuCQ2zE*Cp%9|SGN$n7*+Tgrj?}nN&;k; zGw+WHX>HK>&~N@7nUys1y5f^v9Z7D zfa?$O_wTH&eJznO>*9MBVVPCt8+pPv4i9m8;B=pDMot-cP zrXnQUz$ti5nS-oY%mbDv<+JBveO55>4?8M1_x(@7-{gY1>K6pzT;e;ye5c0Qxhwz% zUl$cWoRvfGWG$(`DA27grMw(1DKZ#_2rm6F03g`IC{r}&jfNd53_5y(x5v@#oIBn{jbJy$9mJT`S%D zUH@h)4yZXh9bM3pd%3GbY%8$RAn2pHR?)w)0@i1<9WbgE#sou60AjTzPS4bh{l3fg z3gbda>SD{pKF)@=sWAa?l@0p8Szz5EMvpq>2R%YgDJE^XVt_N+UG8~kY@4&SL zcs@N3sHo(E>j)G-tnI`x_TO;JwJ}+gkB2*iYUK`_snY7(i(MPPZ&dtC?t3Zdk~Awo zTBKF`g`I&P`mqdA#vW5k#rDLivSxtKs@E9qEhl32%iuFByCn3ybbr-7vZuTCdT2Ug7_ACCGuG8{LJgI@&8BG?S z(QY3d%}rUS1hX{YolmK)?~qE;3ToM(%cVXHtY1$HCK}l z-gj7+!Cf`C6v?@1Pvf{6PxOjL=Ya4~eobydf?r^T4;7q+SWcZPn|w9Eo@Fy182i(- zKFYLUsb$#V>{|Cs&HM7jcHh5NdaK`VowYuj-ceMmjJv6+ zj=t1GntsIlUDH>tW)J0E6atAPm)Y+xW>`w=iHhXwgRJ~k^5xK?PIeL~iTuglZQ{t&0qc)AoR~77H*LCRyK>$(` zxb?je#V{Nck{CV`&HYWd)*C;+=nQX`jIP;oO6>huaEC#L_CnD5ff>gil7BH3$!?`I zDm`+D;l5ev8_2EcY%cz>Y4XUsN4E|X)5ww#XWDvfR{B}e$+nV{k+Je-uvVHzguO|Q z&uP5H$@CXRs`Iq)5W@(2RPpM@;JO$yNy_!8OU1EG7Tlu`->AISw#~`#tBCbm+URYV z9bX`+OI-6mw3f4Pa76Fmn0`B%qM{iGd~#@@^rpRgwD7whTho>l=80JgBp@->>Sh&u%Gv*o z=JGXuhtnKeQgV*LD)yZ;lsn_tHC{KZh}Ae>?%9{`w$hROc?>O!)Ie2-AY>?-jum)m9`YO+CPwJ0h&3%Q-6WZi-c73WNJLG5<9G$e@S0@>^N9}R6+59=P z+@|Zr{cg8CO?l#}v>&#*((;>3@vry|B0etdVu{QPeb`@OQ`#Qyw0*aggh7CAC1UpD zgN}-@@kE0|{;b+e2Pk+HZL4*}>FDC?Ou+du^DazuZ=?FKci`J*><6yPEW}Y87V?{` zy{4s$LzU~DniM3g+jN7nVTJE%b7eQ zV%uOoniZW(-$CTbFw*kc@^1XG5PARf_hA&Ji4o+NG%i;5IgETbSggr|p$uDFo~+zY z1*HqL?d?XNe{PAaJ8rDaUq)l2=XiN>dBwz}RagFlBU8FQKBm^bK2i+5a>uXpYUIhc zUz_eP+Hm7K^CmXb&UPHYQEwRr#a^46*T9cD;>L6@F#bT^_cm?ofydX-Kg>%PS(}Sn zdUv)d^lI5bsz@?f+7}li^LeFQd7k(!t!&k`Tateo(dIV2Bz-T%rXvTE?IMDL1!O`g zekMQm_j6rqT|VQ%1!7!YsnWz)eCIK93k!(Xs!aOjRS^sErmQWDi!j0J^cx`4ExHAN zOk6gbqerTxeI~cdY0CeWy(Xoh;#5(pB{@Cg=S;r!IfhQ7k|83GjDZS+GoPdd^*0Et99gTV@~PgS4l>rTL01aB8E{ z@lT#U5ps?)Zx@f8U}X3fwOrkK;nPX4hTh;H^@J~e{8LX4zj@;=^W$xH#k&%pC4T1| zEq#FO?SP-pCwq#4fbp=F-)=pzV!jCOb9`R&iL57?wNkI}rt`WTa_+xXw%>A(Q_`cQ z?5{>!gLn3`dDUxm%~HjSb$8vCi%%2L981|`x_xzSBwLeW+>%#l9|=eBrKcX0Ir^BxNKJ0gp~uLEE+PsaC0A96ftS>!Oibmi>@CzvRGqXEgVD7-Ko6ZE1GhY z7o_?9KdsL)GnY`_5IcS5o93;XKc`KV>gc(5C-@#u=czv*U#k_N?ZnPVHGb~9RmUP@ z`t$d@N%qd=DFo8>PBc)+T`;iik??p!e5Vh zNF3@Nw7dDp22J9y6qD!SGL>Rl-6EGYt4!2%rg%ma)U_=(4J^bzTAIt;7cM%&#r`2EWk2xU3|tu=oU{p7;Z{mK>+|7bcOt^-$?>Nl3`Dp+O!~HxG~ndD{(v61>qaKQ;6?c}zYy@JK+G zzkd^E1TY7(V7cO1K%fZ{nuW$BBnu+CR`=Msf=5Q6Ed1`>=SWY_F=WohBS)1y+_zU& zcQR#6z=N^BRLW%9-y<_=TBz7HQpLGMLNjNx!YRGK)V-?jXrx?2Lk+#i)o_MA)Q^5s_czplJ*q&OYijhOYfL7v__~(xic2Y$_?1`jAkuJ zmXjI9I97!h0em|0RI_=MT`$Anj)QAVQ{+_1oz?R++M9Gz1t!^x6S8uMdRlq6s{t-T znv^{A_$a5Ipve4cfUI1U*8R&j?QToDOxScga2(X<-gy47oET*lKAum%lwX|c$r5B; zW@K`j>X-)`4_$vkvGLY~ozuaoYxsTDt%k4rZn7RR_x=*s%o`e57o^cK%N54g=h%D5 zwcRk~?d}qHms0)KqG+*_{52h`Zt*)HfLfAG*k)@D4miHC&%4GwdZ&#y?d1;!fxAH- znHk-MF0V`OFO!(Ed$jF(J(!nHD>Tp~le9`=AKpd!q%6(iUckAydTqxJhxPipWG7k? zb~ZXG%2>BmqkMz7gwWeLGoA8=XQ{V(%@$51j%Eq1L>uWkNXLf^k8sWyh^I21V5omT zUFI8@j+|d~D`|EbnB991@;Ywq%vooi&(AA7EO4VQi${fYb$oc#$hSEMOp4WWHx3W#!s`Jk3DeD`Hi@SeYWMsB(O$t|wQQIprm{a&Z)Xu*PUJ;m-yp4@j zNtGj_-0=%0kJ{}N-`EfE+6SG|0=z7WY?OL!GKcuSv?*+306K+S$W`e{K*n zRmSbm`B?Ao|m1WdvI&g>No1FfwOLy8lh)=<4gjKYn1EIlb{_ za?POFvYKCmMz&eKe=87WxB9XKdDrwE9pm08O|I6o%SRc1ZqRf2)L!?>emqfZ-HgG^o9ijhN3X(e*&FT?%F0yFYKt^#PjwQ@Qtb<0Y5WTEsH|S3eoBh6uGeR0 z^&qNbv2Xxuau?3I$GP1(NX2z)Db_*TDU(?lSP!|!nB-dZCw4Jr?x*H?VHL!EEG79e zZ(CZH#X8CT==>W;I}@@V_V$S_kAg<(_SW;nsRQR(ZMCFoNPJoK!U7_{d{=%o(Q^TZ z?3(Lh!X78{_Y;R-lKrzOt{NN1J$w8Z7Uyi*x!&!Wh%tS|SN|d{sgLmuPdP;42_X54jcc) zmwbnzKgMd_)Iw^Iz$ZLdt4gaNRgy!R*WKFb6+fRb{T4QGE-d=0J`5UGndI74L{kcT zP>y6d@JuE(z$Dq1f-7!xXa_g}(;KaMZ#idXiLO2>6{PhncqS zk;YC+M}M@ne0yCiQsv#h!jeVz9H4Q--!0i)lQqkmFfE781lN zQ^cDU{q)}3b|*@2IY~c7>-q!(TS2u;`LO0To5^nocRjN!jhKU9(EWp@=f~f4x%Yic z+y9o2p~LROtqhy!1uwrV8+Yy{W;GaN4K&oCs*2Cc=e0}kio4I!JnVnX(Y^+btb?81 zL5pL_+(a$$3{jeeK-8cSg!#s!1GMgwZLHV4lto~Ca$+7F-$*f!s6c0GIf=aXt>I2X zzQnAA(8{8bA!Io@N;ZlrDjVavR~tLeEm+ye_0`u^ICe(x-_*CPV6(1X%J_vgFWq50cUO0w6b@Kmmqi!aV{mQGyWo zcfYPxjfQ|SoC}npg5#h=B;0JoWeMFX^nIc6HpT`u`#D}3HU$n-6O-r11^m%i0O$iv z!*+2V1;zu{jNi77riW%&^&V+chPH{d(1u}_K11jJS&`^y^@POcOK($^g6>@9Jsh6r zR;07fd{0f8QC>#I_t6nAAMam_J$sVbWO`uK)od2UT+#Dq zt`?Dn6cpNm8mSpZn2XF{6l3?unnHcYr-Xa&ADnU3O7;GdOq$faSXXl}CCkOvQRnk- zE^gy(<9|cGN!9P{P>CHkYJ)!DGK0)s3C{QL6Jt<29PgYK9^_dwzNe z%dRe%*HE6)BVc%Nm>>(VAo%XlNgPtbR1kVc1oWaFy_<@%dRj*3?34~2ow)vsr`rW5 z>6jSU=&l9j6alVZ1&?-TUXs1{&b+}yFRGfcUu1Qha>6%ub{9A}|AZ++S zN2Ioz$LiAEz^c4@$f5Yqibb`H|G}-}Bb1G3^(H+%8Uwm&FcU&;I_OTE_%QiuA~+#5 zv~K1UWDu4P<|$v+$;rvV^aRp?qt>%H8*+&ZomC#z98 zL|Q)6lJz>WAd!aj9aHmlG@+(gj;R=L&oIat8KZ0A00zo%ehY+X0eqp62lq$1Qg$yQ+y@ zLWwTo;*xPW2??Ra^CXMr_93m`<=JkM)XDdDWZp{BbyTy#f&G(JnR4-|A|O>oJv&x{ zfpxT6Ih$Uk@#`Up9DLR$F2*-w;ckr9QlLM2?Xrrp;^2G=`=gO3Po7n?@Xj{H^n#gL zva=>CSg`>m5{(0CZ1wQ7)Yw3NAG^_kJ!Qn&30IS+CBc0hl(Hf{?Y%qgg>f6^c90&P zn4Xq3j3}lrv8&kX+uXAtc;;FYjo{iw@6R8$*K9PchpP!fq5H_$u1HHS3UbqQ3>Sr3 zQH*!0@0__Tc6)wwB))cO>dhyEe6pr#!5o@%v19x@BLR8H;ZzG>xyl!H{7fbLU&K%U z+UFc`WLs2(=b3k_PHtFMl0mwAfcV+LdRjJ$YH17W+N`H z=x9=3k=I}8ICRo5XYC2sBMrf0^K9qMdMIU0SP?-tZWlj(?8QAwT`~1z?VXYuvQg4Y z;f~b4x+D!KyR}P~0FOZDTU}MP*nQIln1|ccln1Wxg+%0-{ng2D-lHCG`ICh79IY zL4-$$l{36Yym%_{VJk^o?E7JiRq4cENX^e?bGkp1dYn7k;nEWOhfDKrRp%I1&c>|q z?uDC+QqJFx$-1f=x^FRGyL7m1FQ+9WTw^4CwiZUXm_U!~8Xjh1`?$I0E31Ku6&#F^ zz$32ww)u8n(=kkktN{qZ?>m1V6G)X$@h!fvxc1VN(0CSf&^GANRtgRZ;p7Z!De(DL zLq2<_I1N;7`k?Y)E)#YNX_DD%cf$DG4?D2i=G0eP=GGql`^bCB8{1%P8^M7^2v-wu z*(!MgrHdxd-)$^!ADsj3di-_ge2o{aA&=SNCX>0l z|8baQPu8x;#pNzTD3)B_I^30I{LIIP6r<#UsF;`-M5(Z_PKf9vzJy8I;llyQk6;~( z@jGM>ua&D)15XBg9~6pr?rB@?d=80QAo~rcB`;k<2db_22BumBAzM`%PNOSkW@f!U z1Q~jw4|ICz$$v0j&?tmn1`r>bg@7js9Y`H-%BIfwZ&;7WuHF9=)}upJHC0uk=_{w9 zPiuRa;H^yr`0-n7b`2-?fGN0%-JJ5S>+K%bks~jaM;vk&O?-1reB+DStdGd=5mFkt z6c?Z;?3xWo151rc`y^N&wf46Wtp4n$jhrOC4T<=2dkm|y2a)?PV{Fg zHL6czVw9eA_$(Czvpd^&*+}VJsG@YA4e92TjTYU6$;ru+s+_S;9y+v5cK7*`&oa@cHRT%9b8M?p zs+q8giFwQvIgiyx(HmEpG-*4!0ISp7%%ZbDGS#P%GI{3C+Ip|s0)f3cea)0FyZ@AoWLjst zymz-3Q!;ZDvE7<-w3t%U==0tyK^5}U?>^ccoJ{1PM22A8ARZ8S2)b@kB<3UjdQ5uC zEWX?8?Rry%h@+ZeZ#R|6Gu8NxC5|2vD5Xd&2RS6@K%JcB4X1FC8WE?+eZmf7>+LOH zi`8lNWvQ!LesMk^t13}NaB#MUMmj*jZ#lMBbMv|;zMJ`sWW9? zsl6@)8`RWR0Ko@{vuJAm)G9bHaffsa2ti-Ei<$)vDqC8 zC7SdJn7$s;K)LvaP>vyjL7YcZ3FJbDABsV@yeM<+|?c02>_jh!B)PgE*V-~fsVmegC0QckS{wNWBM0D)a?fWTxF#H>y?1JH8CCp=vH>Q$1l zor-{+VIJ%_(cV|P>Y%W&)s<;&w2KMI zlW{*m)oGlLh*GX!fx3u3zHvj#yJBNLysva?O-RTotDcgPJ^St{mwf53E$yX?GQZD@_KVdOFUD|#n-_cho_mYrA0Lb?#{+OlBZXvaw zM+%9Xz5G%?#a}qQKP3IQpd{DSTc=eQ1Hh$>Eee*bDrmN&cYhN+rsrDZUKt5!+e4hkL?~Eb!Epmp^g~H~x$L^t)dZejY(R;`cE;w})37#~{Agl~w-z zOh9n3nt}o>MUSUnFkrR2a67X1)@Y@VPqMlAslfo*ST)v|xs-nP?Y4>OPt>Xp~*Ocx6TLzCJSAk*%5 z>dXP9@iY2~A}M3_5|+pAM-^n(to)giYe!*R|E16yl8AG3CCIl+4em?9#%~AvV1e~`#oGd>-j}Vrt($WDzL8-C!GtUi} zw(S!Ex~+q|2t>n|0rP)OEtDm8DOJmpe&4UNH!!P`9+m^~Wx%O)i(P^O!=-ro3nmrD zY>FpMzklEJy3nc}a*ygir(PW#QnS5z=pd`e{h=wl2FTGckISY#{KnfpRGl;}y!K)%Lwr{9?B>Q+-HoR6NkwDdAEITc8OgF4bMt$T_5RC7)YqJ;*b?5UE)xG$ zQ(v8tr$KY@ARRk$B%PTMN7yNi?HL5xS0!LrbasBK^^FsswRgP>FH@}69RJ*T)mv^?F0P$uIKz0c$#~t zpbP0-wi69Gc}Qre2DyGsRTZ+qdI$xdKK;47ySt|+LeRl-q&kR-*9ta01Z#00=Tq4) zecvW0yTVun)FpyYFulVp?}`cH`mh}+TvI{GYC&mUvhmHDnpaj%Q;%J4C8MGW2NA=c z?%w_FIvylK%9goZ-QDdozcXkep9>;)xVEuvzJE205;N5yYK_yqTv*(PVGwplQBTho z<(u12xRWaj68&z8phE;Eyk!xxI`8#EcYxpB-u7Pdjq{Mvpo zv2D1y0PLx}c$dIA1bmzGGctMPZj#q(ph@I#>sDpsFe@_?lSsphq7AGev}623*iW6p zoB#>s1sX(tS{fR;K0c*VBpSrTPrzh*vCRu_@=VtsHgeCiGBOgRHjLHI0oyN!?xTYP zMX4Cz)nH%&TP^M$H#`;|5m7ssfAd$IAd;1dG;OO4nYJ6Mh~F|eo%?S0q*llDL9&CX zlj5BMHaei+L#4pl>4&k$)q?yf<{Im4vX^cy+i{livf^~F#g-JGM(bVg%gTA7pVwC` zce2pV_mrhbcDeTOE=ui=7p%iqJ-eTiLVER7&5{@ut|TB z=H?Db9eC~?D>5d|7G|*ht<#cn(W=l#?pco4a-g441QE}j_dp743lPHw!Taj&)YO5= zuz%yg)27&N-@A9uSWcdckB_~-_UV&ixI6d`UD=knaN$j@Pt}>DB@rVsJ*214o|VyM z(##i-8JWacY$yqjjI1kx@$vz==ghP6>5n&)lt|V|b{_!^qQA5zt>d=};~ZadS#1|8 z+7LEvwc4VX#t3@QwO_XMYIs~=XL|DFjBcr8k@y!&)yf=&;kiEro5Fv%n=+?L{zc1` z&ud`ySn_%F1p*mvWwa(DG&a^bx+3w=TnnA)si#k$L>5x3mz2mkjK9vT zzS+&zO=ts$QD`m;3yx_qu@t$@M;$>Mjwv?efq{(`%RCeu zB_2Q}l)7&mye-Jh=Vh1HjHW2$>ROC?>U&j;hhv-Ejr|vh8(2=hIe9g8?tlz zPDc*KLcpzlBj#;lYqE9{ZA4a*7X#g^G{TwOI{)YDCo_KieX6-#HYy|EUMo0FObqG3 zSG|@F2?G1!1GyM z0&(*i>2XDqVqv)%mo}LK0o9)nV_7xrKV5nqk?v?U-mcON5ma8Z z-2%HmI3Pc}L)kF-p4svH1{v*-XV7BY_!+!1XA_b1(jVfE|4#s)6Qy?O<|VFny^QsG zb)_BIlpJ-zir{!Nal@;XNGH|z57Ol z&L&*saD(j&HLLv+0w07Nr?QYNZh7N}mO3KRSKgZO4OWZfLjKfn>Qtr#N*nsVctvSh$HK1V8{|dbCDm)PFtO#^&5K`AFjCrBPx0=bKaH|aqrD^0iNHHY|$m0P6v2Zb0c zGqEXtdFgjc@Q%+5>6XnV%X&g*yG3>xWf}%6Sk5^|fug5CBrgr2Z1}Vc%=-YxIZA7ek=;SP$=anUuT$pUL`UA%p3w zZd)yVZc>siluYvAz`~aY9N8k0-M4>(AYZmC63u!4GcVonQn200Bl)sd$gSb_m&r3P zIkESEiJWUl@9XI7wC*j)S>T23$~j)%YgtB5V5Y1Z^ZIN2446}%f~KWgCsFw*AzF=qF z?xJYUH4I7)uS#EuFD$rP)d`w6)UUL2;9<8B4FrWnS|;dr!fg21b8?zB@Mo;aTwIVs*lhL8P$b0G)TKXMfa8}oSb9lxaf;(m7%hg zD8`kL1s0Twcvjdkhk7j+Eyu7G)GZOdEdNa_M`vK(+yJ%T9rWGc^BBma4Bv<`?`cMs z^y>fyb?=`jB27uGuCMMe$(&kMF6TJ`kC+1YO~J5U-`M&BHc5F~wq zyM5H^OmxUrHnk^Anl}OGRUp4fMs_5u-ig7Nd$j-h*<1>x>E$u+8xH)JNSO58r+aSJ z_SpQPO6$gS^1~0Zv7xlAsC+O_ncOy)?E7<-O~Or-m&qF*2-+^Zt`^!Ky1(s9>#LkN zaUD-IDV-s5i}Q_40ga~6+UOIfPZ#w$=GA9#pdUN_b8aQ5uInK{ZFI)0tf{YFoe&Vn zXZF3vte^cOi_>F!sL%TL6dZD1pE!kpz-#&Sm|-$S z%6eSgIZApKcH_dfXL9j-q6#EShe>gQ)oxjO|lW z`6nUT{Ey%`&-n0Q??9w>`;p2mE8Q7UvNnXU3{ zcXjexr3;Ba!^FW!gZ4EEm~KWf)j<)%bS&%++>LQsuP?}QzpACIxf$2k*ogKQFmD%f z=?V_xvLQQi=-V4UDDUXei=wlpu5N3j9?Ed~G+38@KlAHTA_Z$TPTVW6)uKUXc>dd$ z916XmyKkTW9%{a#JEeCie!ng>-NMUdswiqH`qB(1qx+kYR&B10Gy3GWjm^YN-lk?y zc|~W^c1FT$zpT8x2f{NdZRn54gMXUB4 zYyO}=-=N~(d@reViF!biqed^0{ih&_ei;T*xbx^<4&LUuSTK+3wQIh@pEf6`xau~O1ke$19@#6RK z@kqdsbN`T#4C_XLL^kMPwjBQ}tV|=U9QVT@0zK9^+3<3}B&h!V5JWT320AAZF*!ckLr%Mhe#$YDTo;6OCLoq>e~b9!0g zi+lInC@8`}P~Hz09l}+C%R<*MC247JIq&W6Zf9y@?!C7DVsnDH{u5-HYf}`V#{#>u zWsDtvQ`mvUvW6cqJc2`oodE_ulx|2kQl4vYdoM*`Mml_L&koW{@JEEZBn&b?6KILA z3}gkeC80H`m3goE5*<|_O#677Du4Z^&`>BrKaB{Sz;V-mY478X*g;_7{PXD{D6hZV z3*mZ8OG_@UY#8?`@jzDqc0#b;!|VyBLU@Tq-sx=l5G4u^hg3*mVIg2}zy5${DaQUp zjTbevw9@oSqX1G-ZT^o5D(3^QyT9YYdF6)$$(MtmtKGY|y&Aa!yViw-*RK_$_0b;> z4h)2DO3T=*-%s`7Fz|clsk<;rf~zLB@orMe)2B~^Z2clQm=|_=m@$HnjWl zpr*{q%zSeBG!f@JNL!4TR(leR^d={zKZOfBbE+<`EnD*N@El?l*gz*bB^a)G0v=lg z<2UrUL7bGjefm1~Jw_0&uC8$FmFuGwj1+}|EvU-=JF)ZN)5f*%G)nZ&)xJz9+u#e` z{3#bayNd2PCU3UgxzliQ!>}K()en{9|QhWktmpL{K_&t*M+OaAE=3Ei6Rr4)~>kB76fjc9;S} z4nC!p@`;|7)*Afs@QC{l9EfkSha>{rdugkdCR$mPhDJxxvH7%mJ`h7}RN0mgegQ)8 zx~l(vpU+w( zK&f&YF4fM|gl~4fnV|U75(L9tzX~*nvefHM4AQ^0x0^z25Wr1{yAcx;4+>_N^T0G5 z7&2EEmp*ty&JBD-!vijL?;|67Q#7-oC*U~MaS-AnhH=}KF>oZA`~XEq6~0tSQrsaz zup3+O2Z*pUy1KIRdXRt{BY^Q0-W2x851?>2)mL0t03KS;{l{mndHvdK3L=EP*2@g1 z>!Q1!p64HW&CSoZW|+=cb?4SWf2*_884r=0n|lE;x($Tu}64->h=)boNp+6+{h$hTK)1e1J1%@%K}xC$ zhHb5_4j(-K`1b7^Kq!#b!cH!=8`_;5cK5D0Ox<7?ClwAUBy44PZodo*v+l^aYjNGc z;2VB2JslYudWEuieCa>ikfBp;F2m5;I6aD{)#Cg-DAmeAJ9g|qJw?wB1e^!_lda*z zzIuMxp)OJgh^wF?HiTWAEHs@LequMm;cnrPp9`4x`}?JWK^Pw$9)_+!Jgz2~PNg^< z`u$xj$Pu;_Na3%(GC$v$XRC)=9l-)yh{e{nM#Y(mi7651NWJO2fs7A`cu$0x z!u9t*`#}0{?3w#J;gutTb1AZT&t zks`8FB_)74lo}*j%HIF}y9xw%7zM9f7y`p_9+#K+{oROixX&P~A{)U!4>L`uC*ULn z2M6PlRZr9TilFlKYgKJ6bc46nmRS-{?@Bt1D;IH%@Pik{i%`vnbNl6A5U-{!SDx?l zLltOiprds~6`=3&^KLM^PgjokCJIELL)|2*5{)1{!*t|n&c4_cl8jG1Lgw$ z9q?{EL_@3r?mXaNeSO!owZqHuD$B~sAn$}<$-l48$iCaDt_Ykas=g*pOG zK6p@l;2;%M&GNq-+c4F@1EJ3E}mGWg-cFBoQ`nQvCRMC*L(=1l`?e&gyRY)#rN&d%^i zV#+SKfYG({Y1vadsHi9+ zG7|IxD@#k3)2H8Ib1K>XPjlF{jORdvj2fJsND+YzI-W$W^Zh5f_f!Orc6=;$5jNs~ zjQ%iA4_Bo)b}RueH#L+6O2`BGP|WOVsaF7WGr`qM&POY5aDMCv1p7ok;! z?7d*Gww&Da0%159{~rA50AkUSu9JTg(m+CvQ-1#bCkqKqp0qW{NqT!Di^O2mKDCTy zHNlczmpR(;7vzh`^AX`74@hEL1xZCi=NGdh2<7D$6zo5C?!1nUFuEeAP8t538-ya$ zbsnyB=e~Dj#z67i9OHDy)rFCs9vvjBq*S~Lq(JK9MHLknLzzg*5VV7t9)VrE9xBF2 z?%J~_pjXSo;eWkRhL^D$gNu%X?{Snx4Qj1%qdRe1Li@9-rDX|BDtsI8R93-0ixhGK zxJ~LWLaDj>_wTo<;6$K?JRcq#TVGv;SZML>`Flyk`}eoCwx({J(T=O`z9JxGIt#Nn5v#Y5e~89|%eu8`BAefg;GEs&n{j z(0q%V8PPk8P>+Eoiff)JuYkM!=Dc9)m-CQu3(B6g{uO-ZLSWS~V0-y(Gqm$GRaBN> zIZktVXJbnuv{R&|`wJYNwcq{qe>u-eh}XwJ=Q&DEEgOV{ZFYG076!uAA-zj;a|t(h z{S($b8UDhupRqh~Sal0w3zxjEt}Z5mpSlx%KQ0j^B_YS)WYv0?VRX_Mi-{P82_rB^ z0a*zNXVPQ-`AY)?doE99R1-0%arXZH?eq74k%s^IQ+)p-@Zi7y!>0?fCI9EQi|m8{ z2E(NUE)OMnRdx0Lf*(V7ROl6Q|m|9w=QlZ zrFjhv${Za?zlTeb`=A=46Vkz94gmo`4uda?Bg5d;nicPs;EWX_*I97hd*BuV+ph1;r>arX}&`;>SXpyq=UGI{hk;g z>p*s#3(-rAwliq!2Zx`QjUP`$h4)l1!hJA5%vHzmYC zq^&W>Ql0bd3EEYFa0YG6lK2*^=^(sV%LPFn0kPnhAN>RxF?GV7l28pKRb3bt(uENm zIHQU4hS$WoYvKH`W6)p zji|xry$e?Z{QV&cr_qa(I$$eNayr0Q%fZ1x%_MckuVDnZJ@g&$mR_Y*+>=cDM67}3 z!(KZNax$CvxJP|%AtP%~)xI2O>bf%Z+DRuO9-nXABIR^56aV_eztK*kd*UDNaW(V|lW5J6>S_mAKl zqrpv>ewp3sFcmctyg2f@@^j*+a*UB)lSIg*CGkW~qN(!mE-L2dWd{YKYra|tM-C0Y zeW9RN>~e`@Vq(I$OW{c=r<3AY55tSlomxPO{b{vf*TC$rq=^{G*+P|U+!3VV@%60n zeg}q75u^CewrN(_a;$9GBNj%YP}tIcu9KWSrbT-~7KO3QElefX(AH79-Wniq72DLo zl?ud6fjNSTrr@pXs-m~&Jc~)kX8RrxAFhuOK*G@mpat?5PA)EP?i8CVg!_e*G;F`a zY@Y}l+YAz2oYWMUF9al8$c`oCpDBbvc4LQ0yy}0fjx6d(TpgzeIOqRXd6|O6KEA)= zzrG8hF|st>t*)oH-Ij``mKOJDFTJGqv3V%_t}OqF^95h&V$%ki37TY7Xwbzo`ZNe+ zG*9E#uXMgRF1?r`Ih3^{>PrPMsAe6~OwG>B)P(4*Hw70Ba$rcW_`O8V@8ACEk~+Kr zoi~1RILm7&XJW&yt*_%yn3$R(PK8ZGmpS>$lvc(-6B79?s8zlq^uhM-4dt;U8!vKSutcLjlGC^v z?dnSJVYK{lHxh6_><=Eunw&oo+_U$kjwSOFSrB5ptZmK3DZX)soNh2R?-dwSuc)&WE0+sKr;A>v%N zG8`@zX645|F+AkG*+TQP`xg4r85wf`4;X#B$Z(~FG7yDM7ajqJQL~O80hBYcrml|O zcX$rXBfrE^fUW_Iu9~`qcAt@B^;HKueglWGo7^_>_7O$zMDMIYdl~JZFHKFH%*;4qHoLvMPsT{CQ*V zmX>;iReO38t?05atWeJQ+th?rw*wkTecd2k*mdPBxkM;uP-0748@i5c%9>S$c~OZn z^kH0Jw%<62F3kB}y2DQv@~jO)!otRb z2We>PCka*XdqV$F#fp%VZ*3N$ahRMu2}09ts-uM8B1MDZaA-m=5Zq9j$MHMI%IcFx zXzHsFTwfJ<0HBg)P>O3FM`!EwKkiwwR#o zA|&}P$QLEK2<{5VD6U}|0DM?Bcg;8vdT(kt0W9-@N$*Z%oWE z2+7mGKL2Avzv1xPA{rJ6@)7)s?S=$0Rzkv+ix-E{DNmdvjA$kZt3dcDc#*MRJ5Ej> z|N8YoYU;Yle=kwvMSej+Ewn_!!lru*Lz1LqWqCL_Q0&#CMYFKLi3{OB^8elYk84au z5=xZwb(!)V|E&*3i8E1ss$^x@B!YjhfOegVZ?%cUwk3?ja zL|9OUdl1?t2!S=bZ*54?@NjcmG+Cnu0JPTQ$5(vrC1;Y9nVrbQdJw6Q$8qp{J>MrH zqwvQWLO14L1C%Pp$^Rp=5ugvqWnEldJvP^ZRQz|e;_&=$WTTijZ}4Kxw~Hy4aKw#Uc8H-;v5jF7G(eB6a?} z=d-*ozdB71jthZMj|LRdB?~|Jx=j932f750X zDVsveh`SNxP z`B&47&-*Gf;UrO(a5f*?KXyst(O>xpe3(`PkgE{{f{AgKVT61~bx%adL!Ry78y6`} zxs;$~-xP#w3F%26aL#}KtuR7&^wXb5|0%PpVg0rrM(Q5h?ki$-|KBG&X?i=JrKQb4 znH!-PO9F>6+di2qR}yU0;R}x{XIO;J{_M(H0&G8ugcs^>I9UP?;c4;p*R`Rjt{z`s z_{RV-zRqwz^KaGjaMffr#a%FxF#gORoe_y+)$7)oo%nq0c<%B_Z^YZoAe1&Ci7D0OP8w$$4bk}h*)a(y=Al)lX~;@DOz87p=pV-g!vJ< zJ3+s`VGA%0M7a$i5=S4O(@9CUVEFo8yWyR_iOtDDfm$|JV`%8|3g5%=aZT)*d=(V$ z6Fd@K5%>neY&fBoXNW~%m4wvPwy(Et4WjuJXvrm3xrj5{MtRf?y}1h#@jikrbrM3D z;fVf4LLtHqr@HW07<;l+%zV8)My1u7=s%l%3^))%O#HV%bIHRiY#v?B(CWvN0_jRz zs)twiUqC{m>TCg>2>^u_S*fkg*Z<+OOav#yd7B3h;z($h1Yh!ZrW@{lV46=INr53htL^$c66sidTYDH~y*sgv&b#B9@B;_gc- ziY5ZLBOH#itHnkFx70|6kw=D$ar4^=+%X_RepO)NF1#+d4(P-7@83TkEOEt=qlQfQOpNL)%n1RqlGwpy3;9S-~Iz zU@nDHP+oozY?9ZFsif#wp_X-%oZc^kG6?e}QZGlslEN9!#>0SZ1GgkSQC~j>@C=3n zIj7&AJyAf+kaA=A)*KCu%&aWeJu@Z=K)J%j{%y`jA&XsuX6tz5?fX!NfLJT?gL&%V z(iPpywq4)Yh-O38m6h5lkQnJ!q0l@Q+rq@R84aVokHZwfRcv#JC;gq5Yb-R`>YxDdR%}1+u1KA5_=T@mFJT6s;EFD4b@9OVT;kQ z9v~~;D8>yVlfh^!uqS&F2H@>MgyLVbplT7VruKmBLQalW=P6TvwN5=QJ=gNoPV0j~ zAc%T6@GTrQp?dQ1i!+cQe=6n9l*i(08}<)boYa5nB%UQ@08RjH1B$h!ngFxG(LAX1 zu>}ZwPhApJ+}NEM*M_NA*KgiLJ(D?-6w&k9##^ng`QCbNKL30w`&ll>vBfh^q$E^L z=WvRIbsg0UBbO&jA~Oc2Fyv~0Erd*`w-x&f1o&W^fFeM&ua`Sg;$CJE+%to3h&-Nq z6CJ{k!e(RYg+(92F+g#8rV2DcA2cBQY+dV*9y``6k?{CQ$nYgMTtaBHlC0%{pUO0> z-Eu6#j)@Ez3{1e2j^)*}dDipB{<95WvQGeCT~T z076xPd89FsB3ixAH7QwLd9A1=($06>7;hZV4Bag?&=V^ce1q_e^ZNDi#nl%tPK>=b zlg;G@BmDxXzrhC)<_?PmD_9r8#z)!oAFx|?GULQ)u*P_~$|@?j+(lrqjI${z>asTZ z;04FoGiL&SbbE3@YTFwIKdhsmtg6cEg509JDS@|9k*@+&hSxN+B-`v{H04@TlYQcx zwS{afam=GB?XK(S`S(ZccPEGG;$ma@t>ER%wSEpuy{;@0??yQyD7sQ6O?7W*DAtP0 z#wbpQDR_A42ERFAnoBqJS-8@i@(b-_GcxW0uxc6Ag`wLv7IH5Qwg&1IlL>vmWZV- z)>=M+iY!3n;qZJhcB^OMdJhkN^J;ck@;A?>MczY}0w=p54ggwOAQVULWmj3jk7n^7 zzv$~rRTgKM__i1)h(rKh(OagN8q=>3D6013YY;%`2EW=_Kcw?`x@3Ts$T0<0N2(xn z<=CUyl%t$_UQ+3_rKRooOFq=D|=3Cv*nXo2rWhWjC^>hxD<*-$`y+cT5qeR-1608;^p zNa;;*-;7n$I;IC79t|jTJk=+W?8Ay9^kY_GXt}2`G1QjK1R9U=}WlU976?@){ zHZ|ICAC|MH=R*kf?31Y;8yy@ARqkevI-8n>10+0s{?_wiDtCyjYvOY)76A<87%23$ z_40ZI@Vd3NGPUC;W%}FkX}mQgxr+;X4!s^c@ZFA%t6}{RYs^k!Tgos_3>M>NLyq;< zt@j|q|81btwu_%e|9e?!Y0J4w$N=wJ+uJ9sPE8@kgSlR;Tx?*#hb@pqXO^r&tt%Y& zE%eX;3pz16q%M%jh8D6HhjqBYq!AY4kwtrUAo|;a?!zkW`MMR*knlWm+X55Q#2)f# zX$n=Vs_1@G%PMf!~z!F2fku70v}asUueCEBB{5&N9y(jg5~lhqesx zf7`cU{Nv&j`TR=vvpuYJYLcYt3GdiNy1GMP2q3M|)h&Qkd|V%yZt?4}T^R%Cw_lj< z%p?-AVQez4KwWmA3znw3I#h2lz>K2zJGOdwa6L8yC4>bNo(pEN7Q8>D(IPr>U;}T2 z!{K0h6I9xSJ@KfDUlFNI;letDY-zybW7X!BmeMV+!~0H?k*i!24;Mu29kz79_HH}#}h}}Df0eHqv$Tqhp7(75Z#%uCucQ=(I>qJk*4 z8HQHbyj4m`N9O+Ir(f(TqaSLTX_RYL?r-SY38)52iB0iK1_!eTem2h;^!c!86s1E$ zn5zyMOiARfqwxkSSDuU#7H>k#;wIg<`H|Y@elYI>&odbJNv_Z+%AUk7D?>4E;>f?m zV>#0}X=!1AP!MWdiJr(=IK|pEM(7ns1cvj8wkV(d;r*Dx_#H5{Sym}h(O-}fx(N`*}b6p>Rx9uzZA=`B2 z%I10kN!1V_28+-vsjTEQXYWS3$lN z2ndLH6gs?9juK+v3aK^J*&*_`m*N|k4g{?t1RPhA5i#Yp6M^{I^|Z~;|Cr$nIlU5A zWtVQpi6rtTVYxLUR4yvNG$=LiL0VzCqs7PBbLS>ra-h8ORa7{1)nja;aOx96AMQK_ zVYa5eM#!t%*!kU|E0F~h_Fyr>q7R3M+?B#JN?O+4ghOFvB4SKN$|W@fy+L|S+>0A} z9{5_FB3c|`6@syXSLxkD+)3jK1APc@BIXz^TC~B=?rf^kf<$HHLV$(v?f7jAvE3tP zsN+}Z6K=9E5Pc=#gDMJe@*({a1YCG1&*b7oDlv2D_v52Za5SvweN0J73B>s^av}cm zZ}3)#zp|c(2Aa@eh+A8~mrrrVXEj6){NecDibDKVqqjfg8^D2JkpV<39bzCY5xRQ* z9YT2ni2n+uz;S{9ozXTn7BP!(2KfWT9DNGB6&P0Ox z`|;<6Do?(T&G?K>j~I8fzs12woKua#O!=3z<~e)2yjfEnxNEAn9oIu4c{P!IeldB0 zM(=5lZBK6|{Pyq1$LB`;`!DD#Kb-#|XN0};29HX7ysYJMgj$`d?x806b8qx%Cq8|$ zOpdh&WyqhmIgAtz>g0{6Vaeb~kUxn3<7rdgsECTYa$N|#k+jjyab3C1_LzSFxj@a| literal 0 HcmV?d00001 diff --git a/docs/faq.rst b/docs/faq.rst new file mode 100644 index 0000000..751de34 --- /dev/null +++ b/docs/faq.rst @@ -0,0 +1,131 @@ +Frequently Asked Questions +========================== + +Why isn't there an option for a flat non-struct hardware interface? +------------------------------------------------------------------- +SystemRDL is inherently a very hierarchical language. +For small register blocks, flattening the hardware interface may be acceptable, +but this ends up scaling very poorly as the design becomes larger and has more +complex hierarchy. +Using struct compositions for the hardware interface has the benefit of +preserving conceptual hierarchy and arrays exactly as defined in the original +SystemRDL. + +How do I know I connected everything? Structs are harder to review +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Initially this can be daunting, but fortunately the tool has an option to generate a +flattened hardware interface report upon export. Try using the ``--hwif-report`` +command line option when exporting. This is the easiest way to quickly +understand the structure of the hardware interface. + + + +Why does the tool generate un-packed structs? I prefer packed structs. +---------------------------------------------------------------------- +Packed structs are great when describing vectors that have bit-level structure. +In this tool, the use of un-packed structs is intentional since the hardware +interface is not something that is meant to be bit-accessible. In the case of +the hardware interface structs, using a packed struct is semantically inappropriate. + +... Then how can I initialize the struct? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +We get this request most often because designers want to initialize the ``hwif_in`` +struct with a simple assignment: + +.. code:: systemverilog + + always_comb begin + hwif_in = '0; + end + +Of course since the struct actually is **unpacked**, this will result in a +compile error which usually leads to the inappropriate assumption that it ought +to be packed. (See this amusing blog post about `X/Y problems `_) + +If your goal is to initialize the packed struct, fortunately SystemVerilog already +has syntax to do this: + +.. code:: systemverilog + + always_comb begin + hwif_in = '{default: '0}; + end + +This is lesser-known syntax, but still very well supported by synthesis +tools, and is the recommended way to handle this. + +... What if I want to assign it to a bit-vector? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Assigning the hwif struct to a bit-vector is strongly discouraged. This tool makes +no guarantees regarding the field ordering of the hwif structure, so doing so +should be considered functionally dangerous. + +That said, if you still need to do this, it is still trivially possible to +without requiring packed structs. Instead, use the SystemVerilog streaming operator: + +.. code:: systemverilog + + my_packed_vector = {<<{hwif_out}}; + + +... Why are unpacked structs preferred? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +In the case of the hardware interface ports, unpacked structs help prevent +mistakes that are very easy to make. +Consider the following situation - a designer has a field that sets the following +properties: ``sw=rw; hw=rw; we;``, and wants to assign the hardware input value, +so they erroneously do the following assignment in Verilog: + +.. code:: systemverilog + + assign hwif_in.my_register.my_field = ; + +This is actually a bug since the ``my_field`` member is actually a struct that +has two members: ``we`` and ``next``. If this were a packed struct, this would +silently compile and you would potentially have a bug that may not be noticed +(depending on how thorough the test campaign is). +With an unpacked struct, this gets flagged immediately as a compile error since +the assignment is invalid. + +The designer may have simply forgotten that the field is an aggregate of multiple +members and intended to do the following: + +.. code:: systemverilog + + assign hwif.my_register.my_field.next = ; + assign hwif.my_register.my_field.we = ; + + +The generated output does not match our organization's coding style +------------------------------------------------------------------- +SystemVerilog coding styles vary wildly, and unfortunately there is little +consensus on this topic within the digital design community. + +The output generated by PeakRDL-regblock strives to be as human-readable as possible, +and follow consistent indentation and styling. We do our best to use the most +widely accepted coding style, but since this is a very opinionated space, it is +impossible to satisfy everyone. + +In general, we strive to follow the +`SystemVerilog style guide by lowRISC `_, +but may deviate in some areas if not practical or would impose excessive complexity on the code generator. + + +The lint tool I am using is flagging violations in generated code +----------------------------------------------------------------- +Code linting tools are a great way to check for user-error, flag inconsistencies, +and enforce best-practices within an organization. In many cases, linter tools +may be configured to also enforce stylistic preferences. +Unfortunately just like coding styles, lint rules can often be more +opinionated than practical. + +In general, we will not address lint violations unless they flag actual +structural issues or semantically dangerous code. +Stylistic violations that pose no actual danger to the correctness of the design +will rarely be addressed, especially if the change would add unreasonable +complexity to the tool. + +If you encounter a lint violation, please carefully review and consider waiving +it if it does not pose an actual danger. If you still believe it is a problem, +please let us know by `submitting an issue `_ +that describes the problem. diff --git a/docs/hwif.rst b/docs/hwif.rst new file mode 100644 index 0000000..3bf50db --- /dev/null +++ b/docs/hwif.rst @@ -0,0 +1,61 @@ +Hardware Interface +------------------ + +The generated register block will present the entire hardware interface to the user +using two struct ports: + +* ``hwif_in`` +* ``hwif_out`` + +All field inputs and outputs as well as signals are consolidated into these +struct ports. The presence of each depends on the specific contents of the design +being exported. + + +Using structs for the hardware interface has the following benefits: + +* Preserves register map component grouping, arrays, and hierarchy. +* Avoids naming collisions and cumbersome signal name flattening. +* Allows for more natural mapping and distribution of register block signals to a design's hardware components. +* Use of unpacked arrays/structs prevents common assignment mistakes as they are enforced by the compiler. + + +Structs are organized as follows: ``hwif_out..`` + +For example, a simple design such as: + +.. code-block:: systemrdl + + addrmap my_design { + reg { + field { + sw = rw; + hw = rw; + we; + } my_field; + } my_reg[2]; + }; + +... results in the following struct members: + +.. code-block:: text + + hwif_out.my_reg[0].my_field.value + hwif_in.my_reg[0].my_field.next + hwif_in.my_reg[0].my_field.we + hwif_out.my_reg[1].my_field.value + hwif_in.my_reg[1].my_field.next + hwif_in.my_reg[1].my_field.we + +For brevity in this documentation, hwif features will be described using shorthand +notation that omits the hierarchical path: ``hwif_out..`` + + +.. important:: + + The PeakRDL tool makes no guarantees on the field order of the hwif structs. + For this reason, it is strongly recommended to always access struct members + directly, by name. + + If using the SystemVerilog streaming operator to assign the hwif struct to a + packed vector, be extremely careful to avoid assumptions on the resulting bit-position of a field. diff --git a/docs/img/err.svg b/docs/img/err.svg new file mode 100644 index 0000000..6ce297c --- /dev/null +++ b/docs/img/err.svg @@ -0,0 +1,53 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/docs/img/ok.svg b/docs/img/ok.svg new file mode 100644 index 0000000..defc966 --- /dev/null +++ b/docs/img/ok.svg @@ -0,0 +1,53 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/docs/img/warn.svg b/docs/img/warn.svg new file mode 100644 index 0000000..1debe8a --- /dev/null +++ b/docs/img/warn.svg @@ -0,0 +1,53 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..e21236a --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,94 @@ +Introduction +============ + +PeakRDL-regblock is a free and open-source control & status register (CSR) compiler. +This code generator translates your SystemRDL register description into +a synthesizable SystemVerilog RTL module that can be easily instantiated into +your hardware design. + +* Generates fully synthesizable SystemVerilog RTL (IEEE 1800-2012) +* Options for many popular CPU interface protocols (AMBA APB, AXI4-Lite, and more) +* Configurable pipelining options for designs with fast clock rates. +* Broad support for SystemRDL 2.0 features + + +Quick Start +----------- +The easiest way to use PeakRDL-regblock is via the `PeakRDL command line tool `_: + +.. code-block:: bash + + # Install PeakRDL-regblock along with the command-line tool + python3 -m pip install peakrdl-regblock[cli] + + # Export! + peakrdl regblock atxmega_spi.rdl -o regblock/ --cpuif axi4-lite + + +Looking for VHDL? +----------------- +This project generates SystemVerilog RTL. If you prefer using VHDL, check out +the sister project which aims to be a feature-equivalent fork of +PeakRDL-regblock: `PeakRDL-regblock-VHDL `_ + + +Links +----- + +- `Source repository `_ +- `Release Notes `_ +- `Issue tracker `_ +- `PyPi `_ +- `SystemRDL Specification `_ + + +.. toctree:: + :hidden: + + self + architecture + hwif + configuring + limitations + faq + licensing + api + +.. toctree:: + :hidden: + :caption: CPU Interfaces + + cpuif/introduction + cpuif/apb + cpuif/axi4lite + cpuif/avalon + cpuif/passthrough + cpuif/internal_protocol + cpuif/customizing + +.. toctree:: + :hidden: + :caption: SystemRDL Properties + + props/field + props/reg + props/addrmap + props/signal + props/rhs_props + +.. toctree:: + :hidden: + :caption: Other SystemRDL Features + + rdl_features/external + +.. toctree:: + :hidden: + :caption: Extended Properties + + udps/intro + udps/read_buffering + udps/write_buffering + udps/extended_swacc + udps/signed + udps/fixedpoint diff --git a/docs/licensing.rst b/docs/licensing.rst new file mode 100644 index 0000000..c9424b6 --- /dev/null +++ b/docs/licensing.rst @@ -0,0 +1,50 @@ +Licensing +========= + +Re-distribution of the PeakRDL-regblock code generator tool shall adhere to the +terms outlined by the GNU LGPL v3 license. For a copy of the license, see: +https://github.com/SystemRDL/PeakRDL-regblock/blob/main/LICENSE + + +Why LGPLv3? +----------- +LGPLv3 was chosen because my intent is to promote a thriving ecosystem of free and +open source register automation tools. The license terms discourage this tool from +being bundled into some commercially sold closed-source software, as that would +be contrary to this project's philosophy. + + +What is covered by the LGPL v3 license? +-------------------------------------- +The LGPL license is intended for the code generator itself. This includes all +Python sources, Jinja template files, as well as testcase infrastructure not +explicitly mentioned in the exemptions below. + + +What is exempt from the LGPLv3 license? +--------------------------------------- +Don't worry. Not everything that the PeakRDL-regblock project touches is +considered LGPLv3 code. + +The following are exempt and are free to use with no restrictions: + +* Any code that is generated using PeakRDL-regblock is 100% yours. Since it + was derived from your regblock definition, it remains yours. You can + distribute it freely, use it in a proprietary ASIC, sell it as part of an + IP, whatever. +* Any code snippets in this documentation can be freely copy/pasted. These are + examples that are intended for this purpose. +* All reference files that are downloadable from this documentation, which are + also available in the `hdl-src folder in the repository `_ + + +Can I use this as part of my company's internally developed tools? +------------------------------------------------------------------ +Absolutely! + +Sometimes it may be necessary to integrate this into a larger toolchain at your +workplace. This is totally OK, as long as you don't start distributing it +outside your workplace in ways that violate the LGPLv3 license. + +That said, I'd encourage you to check out the `PeakRDL command line tool `_. +It may already do everything you need. diff --git a/docs/limitations.rst b/docs/limitations.rst new file mode 100644 index 0000000..2ae08b9 --- /dev/null +++ b/docs/limitations.rst @@ -0,0 +1,53 @@ +Known Limitations +================= + +Not all SystemRDL features are supported by this exporter. For a listing of +supported properties, see the appropriate property listing page in the sections +that follow. + + +Alias Registers +--------------- +Registers instantiated using the ``alias`` keyword are not supported yet. + + +Unaligned Registers +------------------- +All address offsets & strides shall be a multiple of the cpuif bus width used. Specifically: + +* Bus width is inferred by the maximum accesswidth used in the regblock. +* Each component's address and array stride shall be aligned to the bus width. + + +Uniform accesswidth +------------------- +All registers within a register block shall use the same accesswidth. + +One exception is that registers with regwidth that is narrower than the cpuif +bus width are permitted, provided that their regwidth is equal to their accesswidth. + +For example: + +.. code-block:: systemrdl + + // (Largest accesswidth used is 32, therefore the CPUIF bus width is 32) + + reg { + regwidth = 32; + accesswidth = 32; + } reg_a @ 0x00; // OK. Regular 32-bit register + + reg { + regwidth = 64; + accesswidth = 32; + } reg_b @ 0x08; // OK. "Wide" register of 64-bits, but is accessed using 32-bit subwords + + reg { + regwidth = 8; + accesswidth = 8; + } reg_c @ 0x10; // OK. Is aligned to the cpuif bus width + + reg { + regwidth = 32; + accesswidth = 8; + } bad_reg @ 0x14; // NOT OK. accesswidth conflicts with cpuif width diff --git a/docs/props/addrmap.rst b/docs/props/addrmap.rst new file mode 100644 index 0000000..770cd3f --- /dev/null +++ b/docs/props/addrmap.rst @@ -0,0 +1,28 @@ +Addrmap/Regfile Properties +========================== + +.. note:: Any properties not explicitly listed here are either implicitly + supported, or are not relevant to the regblock exporter and are ignored. + + +errextbus +--------- +|NO| + +sharedextbus +------------ +|NO| + + +-------------------------------------------------------------------------------- + +Addrmap Properties +================== + +bigendian/littleendian +---------------------- +|NO| + +rsvdset +------- +|NO| diff --git a/docs/props/field.rst b/docs/props/field.rst new file mode 100644 index 0000000..fc8e35a --- /dev/null +++ b/docs/props/field.rst @@ -0,0 +1,491 @@ +Field Properties +================ + +.. note:: Any properties not explicitly listed here are either implicitly + supported, or are not relevant to the regblock exporter and are ignored. + +Software Access Properties +-------------------------- + +onread/onwrite +^^^^^^^^^^^^^^ + +All onread/onwrite actions are supported (except for ruser/wuser) + +rclr/rset +^^^^^^^^^ + +See ``onread``. These are effectively aliases of the onread property. + +singlepulse +^^^^^^^^^^^ + +If set, field will get cleared back to zero after being written. + +.. wavedrom:: + + {"signal": [ + {"name": "clk", "wave": "p....."}, + {"name": "", "wave": "0.10.."}, + {"name": "hwif_out..value", "wave": "0..10."} + ]} + +sw +^^^ +All sw access modes are supported except for ``w1`` and ``rw1``. + +swacc +^^^^^ + +If true, infers an output signal ``hwif_out..swacc`` that is asserted when +accessed by software. Specifically, on the same clock cycle that the field is +being sampled during a software read operation, or as it is being written. + +.. wavedrom:: + + {"signal": [ + {"name": "clk", "wave": "p...."}, + {"name": "hwif_in..next", "wave": "x.=x.", "data": ["D"]}, + {"name": "hwif_out..swacc", "wave": "0.10."} + ]} + + +swmod +^^^^^ + +If true, infers an output signal ``hwif_out..swmod`` that is asserted as the +field is being modified by software. This can be due to a software write +operation, or a software read operation that has clear/set side-effects. + + +.. wavedrom:: + + {"signal": [ + {"name": "clk", "wave": "p....."}, + {"name": "hwif_out..value", "wave": "=..=..", "data": ["old", "new"]}, + {"name": "hwif_out..swmod", "wave": "0.10.."} + ]} + + +swwe/swwel +^^^^^^^^^^ + +Provides a mechanism that allows hardware to override whether the field is +writable by software. + +boolean + If True, infers an input signal ``hwif_in..swwe`` or ``hwif_in..swwel``. + +reference + Single-bit reference controls field's behavior. + + +woclr/woset +^^^^^^^^^^^ +See ``onwrite``. These are effectively aliases of the onwrite property. + +-------------------------------------------------------------------------------- + +Hardware Access Properties +-------------------------- + +anded/ored/xored +^^^^^^^^^^^^^^^^ +If true, infers the existence of output signal: ``hwif_out..anded``, +``hwif_out..ored``, ``hwif_out..xored`` + + +hw +^^^ +Controls hardware access to the field. + +If readable, enables output signal ``hwif_out..value``. If writable, enables +input ``hwif_in..next``. + +Hardware-writable fields can optionally define the ``next`` property which replaces +the inferred ``hwif_in..next`` input with an alternate reference. + + +hwclr/hwset +^^^^^^^^^^^ +If both ``hwclr`` and ``hwset`` properties are used, and both are asserted at +the same clock cycle, then ``hwset`` will take precedence. + +boolean + If true, infers the existence of input signal: ``hwif_in..hwclr``, ``hwif_in..hwset`` + +reference + Reference to any single-bit internal object to drive this control. + + +hwenable/hwmask +^^^^^^^^^^^^^^^ +Reference to a component that provides bit-level control of hardware writeability. + + +we/wel +^^^^^^ +Write-enable control from hardware interface. + +If true, infers the existence of input signal: ``hwif_in..we``, ``hwif_in..wel`` + +.. wavedrom:: + + {"signal": [ + {"name": "clk", "wave": "p...."}, + {"name": "hwif_in..next", "wave": "x.=x.", "data": ["D"]}, + {"name": "hwif_in..we", "wave": "0.10."}, + {"name": "hwif_in..wel", "wave": "1.01."}, + {"name": "", "wave": "x..=.", "data": ["D"]} + ]} + +boolean + If true, infers the existence of input signal ``hwif_in..we`` or ``hwif_in..wel`` + +reference + Reference to any single-bit internal object to drive this control. + +-------------------------------------------------------------------------------- + +Counter Properties +------------------ + +counter +^^^^^^^ +If true, marks this field as a counter. The counter direction is inferred based +based on which properties are assigned. By default, an up-counter is implemented. +If any of the properties associated with an up-counter are used, then up-counting +capabilities will be implemented. The same is true for down-counters and up/down +counters. + +Unless alternate control signals are specified, the existence of input signals +``hwif_in..incr`` and ``hwif_in..decr`` will be inferred depending on the type +of counter described. + + +incr +^^^^ +Assign a reference to an alternate control signal to increment the counter. +If assigned, the inferred ``hwif_in..incr`` input will not be generated. + +incrsaturate/saturate +^^^^^^^^^^^^^^^^^^^^^ +If assigned, indicates that the counter will saturate instead of wrapping. +If an alternate saturation point is specified, the counter value will be +adjusted so that it does not exceed that limit, even after non-increment actions. + +boolean + If true, saturation point is at the counter's maximum count value. (2^width - 1) + +integer + Specify a static saturation value. + +reference + Specify a dynamic saturation value. + + +incrthreshold/threshold +^^^^^^^^^^^^^^^^^^^^^^^ +If assigned, infers a ``hwif_out..incrthreshold`` output signal. This signal is +asserted if the counter value is greater or equal to the threshold. + +.. wavedrom:: + + { + "signal": [ + {"name": "clk", "wave": "p......"}, + {"name": "hwif_in..incr", "wave": "01...0."}, + {"name": "", "wave": "=.=3==..", "data": [4,5,6,7,8,9]}, + {"name": "hwif_out..incrthreshold", "wave": "0..1...."} + ], + "foot": { + "text": "Example where incrthreshold = 6" + } + } + + +boolean + If true, threshold is the counter's maximum count value. (2^width - 1) + +integer + Specify a static threshold value. + +reference + Specify a dynamic threshold value. + + +incrvalue +^^^^^^^^^ +Override the counter's increment step size. + +integer + Specify a static increment step size. + +reference + Reference a component that controls the step size. + +incrwidth +^^^^^^^^^ +If assigned, infers an input signal ``hwif_in..incrvalue``. The value of this +property defines the signal's width. + + +overflow +^^^^^^^^ +If true, infers an output signal ``hwif_out..overflow`` that is asserted when +the counter is about to wrap. + +.. wavedrom:: + + { + "signal": [ + {"name": "clk", "wave": "p......."}, + {"name": "hwif_in..incr", "wave": "0101010."}, + {"name": "", "wave": "=.=.=.=.", "data": [14,15,0,1]}, + {"name": "hwif_out..overflow", "wave": "0..10..."} + ], + "foot": { + "text": "A 4-bit counter overflowing" + } + } + + +decr +^^^^ +Assign a reference to an alternate control signal to decrement the counter. +If assigned, the inferred ``hwif_in..decr`` input will not be generated. + + +decrsaturate +^^^^^^^^^^^^ +If assigned, indicates that the counter will saturate instead of wrapping. +If an alternate saturation point is specified, the counter value will be +adjusted so that it does not exceed that limit, even after non-decrement actions. + +boolean + If true, saturation point is when the counter reaches 0. + +integer + Specify a static saturation value. + +reference + Specify a dynamic saturation value. + + +decrthreshold +^^^^^^^^^^^^^ +If assigned, infers a ``hwif_out..decrthreshold`` output signal. This signal is +asserted if the counter value is less than or equal to the threshold. + +.. wavedrom:: + + { + "signal": [ + {"name": "clk", "wave": "p......"}, + {"name": "hwif_in..decr", "wave": "01...0."}, + {"name": "", "wave": "=.=3==.", "data": [9,8,7,6,5,4]}, + {"name": "hwif_out..decrthreshold", "wave": "0..1..."} + ], + "foot": { + "text": "Example where incrthreshold = 7" + } + } + + +boolean + If true, threshold is 0. + +integer + Specify a static threshold value. + +reference + Specify a dynamic threshold value. + + +decrvalue +^^^^^^^^^ +Override the counter's decrement step size. + +integer + Specify a static step size. + +reference + Reference to a component that controls the step size. + + +decrwidth +^^^^^^^^^ +If assigned, infers an input signal ``hwif_in..decrvalue``. The value of this +property defines the signal's width. + + +underflow +^^^^^^^^^ +If true, infers an output signal ``hwif_out..underflow`` that is asserted when +the counter is about to wrap. + +.. wavedrom:: + + { + "signal": [ + {"name": "clk", "wave": "p......."}, + {"name": "hwif_in..decr", "wave": "0101010."}, + {"name": "", "wave": "=.=.=.=.", "data": [1,0,15,14]}, + {"name": "hwif_out..underflow", "wave": "0..10..."} + ], + "foot": { + "text": "A 4-bit counter underflowing" + } + } + +-------------------------------------------------------------------------------- + +Interrupt Properties +-------------------- + +intr +^^^^ + +If set, this field becomes an interrupt field. +The enclosing register infers an output signal ``hwif_out..intr`` which denotes +that an interrupt is active. This is an or-reduction of all interrupt fields +after applying the appropriate ``enable`` or ``mask`` to the field value. + +level (default) + Interrupt is level-sensitive. If a bit on the field's ``hwif_in..next`` input + is '1', it will trigger an interrupt event. + +posedge + If a bit on the field's ``hwif_in..next`` input transitions from '0' to '1', + it will trigger an interrupt event. This transition shall still be synchronous + to the register block's clock. + +negedge + If a bit on the field's ``hwif_in..next`` input transitions from '1' to '0', + it will trigger an interrupt event. This transition shall still be synchronous + to the register block's clock. + +bothedge + If a bit on the field's ``hwif_in..next`` input transitions from '0' to '1' or '1' to '0', + it will trigger an interrupt event. This transition shall still be synchronous + to the register block's clock. + +nonsticky + Interrupt event is not sticky. + + +enable +^^^^^^ +Reference to a field or signal that, if set to 1, define which bits in the field +are used to assert an interrupt. + + +mask +^^^^ +Reference to a field or signal that, if set to 1, define which bits in the field +are *not* used to assert an interrupt. + + +haltenable +^^^^^^^^^^ +Reference to a field or signal that, if set to 1, define which bits in the field +are used to assert the halt output. + +If this property is set, the enclosing register will infer a ``hwif_out..halt`` output. + + +haltmask +^^^^^^^^ +Reference to a field or signal that, if set to 1, define which bits in the field +are *not* used to assert the halt output. + +If this property is set, the enclosing register will infer a ``hwif_out..halt`` output. + + +stickybit +^^^^^^^^^ +When an interrupt trigger occurs, a stickybit field will set the corresponding +bit to '1' and hold it until it is cleared by a software access. + +The interrupt trigger depends on the interrupt type. By default, interrupts are +level-sensitive, but the interrupt modifiers allow for edge-sensitive triggers as +well. + +The waveform below demonstrates a level-sensitive interrupt: + +.. wavedrom:: + + { + "signal": [ + {"name": "clk", "wave": "p....."}, + {"name": "hwif_in..next", "wave": "010..."}, + {"name": "", "wave": "0.1..."} + ] + } + + +sticky +^^^^^^ +Unlike ``stickybit`` fields, a sticky field will latch an entire value. The +value is latched as soon as ``hwif_in..next`` is nonzero, and is held until the +field contents are cleared back to 0 by a software access. + +.. wavedrom:: + + { + "signal": [ + {"name": "clk", "wave": "p....."}, + {"name": "hwif_in..next", "wave": "23.22.", "data": [0,10,20,30]}, + {"name": "", "wave": "2.3...", "data": [0, 10]} + ] + } + + +-------------------------------------------------------------------------------- + +Misc +---- + +encode +^^^^^^ +If assigned a user-defined enumeration, the resulting package file will include +its definition. Due to limitations from type-strictness rules in SystemVerilog, +the field will remain as a ``logic`` datatype. + + +next +^^^^ +If assigned, replaces the inferred ``hwif_in..next`` input with an explicit reference. + + +paritycheck +^^^^^^^^^^^ +If set, enables parity checking for this field. + +Adds a ``parity_error`` output signal to the module. + +.. note:: + + If this field does not implement storage, the ``partycheck`` property is ignored. + + + +precedence +^^^^^^^^^^ +Control whether hardware or software has precedence when field value update +contention occurs. Software has precedence by default. + +reset +^^^^^ +Control the reset value of the field's storage element. +If not specified, the field will not be reset. + +integer + Static reset value + +reference + Reference to a dynamic reset value. + +resetsignal +^^^^^^^^^^^ +Provide an alternate reset trigger for this field. diff --git a/docs/props/reg.rst b/docs/props/reg.rst new file mode 100644 index 0000000..bbef409 --- /dev/null +++ b/docs/props/reg.rst @@ -0,0 +1,14 @@ +Register Properties +=================== + +.. note:: Any properties not explicitly listed here are either implicitly + supported, or are not relevant to the regblock exporter and are ignored. + +accesswidth +----------- +Control the software access width. The register block's CPUIF bus width is +determined by the maximum accesswidth encountered. + +regwidth +-------- +Control the bit-width of the register. diff --git a/docs/props/rhs_props.rst b/docs/props/rhs_props.rst new file mode 100644 index 0000000..6234bd5 --- /dev/null +++ b/docs/props/rhs_props.rst @@ -0,0 +1,182 @@ +RHS Property References +======================= + +SystemRDL allows some properties to be referenced in the righthand-side of +property assignment expressions: + + .. code-block:: systemrdl + + some_property = my_reg.my_field -> some_property; + +The official SystemRDL spec refers to these as "Ref targets" in Table G1, but +unfortunately does not describe their semantics in much detail. + +The text below describes the interpretations used for this exporter. + +-------------------------------------------------------------------------------- + +Field +----- + +field -> swacc +^^^^^^^^^^^^^^ +Single-cycle strobe that indicates the field is being accessed by software +(read or write). + + +field -> swmod +^^^^^^^^^^^^^^^ +Single-cycle strobe that indicates the field is being modified during a software +access operation. + + +field -> swwe/swwel +^^^^^^^^^^^^^^^^^^^ +Represents the signal that controls the field's swwe/swwel behavior. + + +field -> anded/ored/xored +^^^^^^^^^^^^^^^^^^^^^^^^^ +Represents the current and/or/xor reduction of the field's value. + + +field -> hwclr/hwset +^^^^^^^^^^^^^^^^^^^^ +|EX| + +Represents the signal that controls the field's hwclr/hwset behavior. + + +field -> hwenable/hwmask +^^^^^^^^^^^^^^^^^^^^^^^^ +Represents the signal that controls the field's hwenable/hwmask behavior. + +field -> we/wel +^^^^^^^^^^^^^^^ +Represents the signal that controls the field's we/wel behavior. + +field -> next +^^^^^^^^^^^^^ +|EX| + +field -> reset +^^^^^^^^^^^^^^ +Represents the value that was assigned to this property. + +field -> resetsignal +^^^^^^^^^^^^^^^^^^^^ +Represents the value that was assigned to this property. + +-------------------------------------------------------------------------------- + +Field Counter Properties +------------------------ + +field -> incr +^^^^^^^^^^^^^ +Represents the signal that controls the field's counter increment control. + + +field -> incrsaturate/saturate +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Represents the internal 1-bit event signal that indicates whether the counter is saturated +at its saturation value. + +.. wavedrom:: + + { + "signal": [ + {"name": "clk", "wave": "p......"}, + {"name": "hwif_in..decr", "wave": "0101010"}, + {"name": "", "wave": "=.=....", "data": [1,0]}, + {"name": "", "wave": "0.1...."} + ], + "foot": { + "text": "A 4-bit counter saturating" + } + } + + +field -> incrthreshold/threshold +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Represents the 1-bit event signal that indicates whether the counter has met or +exceeded its incrthreshold. + +field -> incrvalue +^^^^^^^^^^^^^^^^^^ +Represents the value that was assigned to this property. + +field -> overflow +^^^^^^^^^^^^^^^^^ +Represents the event signal that is asserted when the counter is about to wrap. + +field -> decr +^^^^^^^^^^^^^ +Represents the signal that controls the field's counter decrement control. + +field -> decrsaturate +^^^^^^^^^^^^^^^^^^^^^ +Represents the internal 1-bit event signal that indicates whether the counter is saturated +at its saturation value. + +.. wavedrom:: + + { + "signal": [ + {"name": "clk", "wave": "p......"}, + {"name": "hwif_in..incr", "wave": "0101010"}, + {"name": "", "wave": "=.=....", "data": [14,15]}, + {"name": "", "wave": "0.1...."} + ], + "foot": { + "text": "A 4-bit counter saturating" + } + } + +field -> decrthreshold +^^^^^^^^^^^^^^^^^^^^^^ +Represents the 1-bit event signal that indicates whether the counter has met or +exceeded its incrthreshold. + +field -> decrvalue +^^^^^^^^^^^^^^^^^^ +Represents the value that was assigned to this property. + +field -> underflow +^^^^^^^^^^^^^^^^^^ +Represents the event signal that is asserted when the counter is about to wrap. + +-------------------------------------------------------------------------------- + +Field Interrupt Properties +-------------------------- + +field -> enable +^^^^^^^^^^^^^^^ +Represents the value that was assigned to this property. + +field -> mask +^^^^^^^^^^^^^ +Represents the value that was assigned to this property. + +field -> haltenable +^^^^^^^^^^^^^^^^^^^ +Represents the value that was assigned to this property. + +field -> haltmask +^^^^^^^^^^^^^^^^^ +Represents the value that was assigned to this property. + + +-------------------------------------------------------------------------------- + +Register +-------- + +reg -> intr +^^^^^^^^^^^ +References the register's ``hwif_out..intr`` signal. + +reg -> halt +^^^^^^^^^^^ +References the register's ``hwif_out..halt`` signal. diff --git a/docs/props/signal.rst b/docs/props/signal.rst new file mode 100644 index 0000000..7171019 --- /dev/null +++ b/docs/props/signal.rst @@ -0,0 +1,28 @@ +Signal Properties +================= + +.. note:: Any properties not explicitly listed here are either implicitly + supported, or are not relevant to the regblock exporter and are ignored. + + +activehigh/activelow +-------------------- +Only relevant for signals used as resets. Defines the reset signal's polarity. + + +sync/async +---------- +Only supported for signals used as resets to infer edge-sensitive reset. +Ignored in all other contexts. + + +cpuif_reset +----------- +Specify that this signal shall be used as alternate reset signal for the CPU +interface for this regblock. + + +field_reset +----------- +Specify that this signal is used as an alternate reset signal for all fields +instantiated in sub-hierarchies relative to this signal. diff --git a/docs/rdl_features/external.rst b/docs/rdl_features/external.rst new file mode 100644 index 0000000..15d275e --- /dev/null +++ b/docs/rdl_features/external.rst @@ -0,0 +1,155 @@ +External Components +=================== +SystemRDL allows some component instances to be defined as "external" elements +of an address space definition. In the context of this regblock generator, +the implementation of an external component is left up to the designer. When +generating the RTL for a regblock, the implementations of external components +are omitted and instead a user-interface is presented on the +``hwif_in``/``hwif_out`` i/o structs. + +External component signals on the hardware interface closely follow the semantics +of the :ref:`cpuif_protocol`. + + +Things you should know +---------------------- + +* By default external ``hwif_out`` signals are driven combinationally. An + optional output retiming stage can be enabled if needed. +* Due to the uncertain access latency of external components, the regblock will + only issue one outstanding transaction to an external component at a time. + This is enforced even if the CPUIF is capable of pipelined accesses such as + AXI4-Lite. + + +External Registers +------------------ +External registers can be useful if it is necessary to implement a register that +cannot easily be expressed using SystemRDL semantics. This could be a unique +access policy, or FIFO-like push/pop registers. + +External registers are annotated as such by using the ``external`` keyword: + +.. code-block:: systemrdl + + // An internal register + my_reg int_reg; + + // An external register + external my_reg ext_reg; + +Request +^^^^^^^ +hwif_out..req + When asserted, a read or write transfer will be initiated. + Qualifies all other request signals. + + If the register is wide (``regwidth`` > ``accesswidth``), then the + ``hwif_out..req`` will consist of multiple bits, representing the access + strobe for each sub-word of the register. + + If the register does not contain any readable fields, this strobe will be + suppressed for read operations. + + If the register does not contain any writable readable fields, this strobe + will be suppressed for write operations. + +hwif_out..req_is_wr + If ``1``, denotes that the current transfer is a write. Otherwise transfer is + a read. + +hwif_out..wr_data + Data to be written for the write transfer. This signal is ignored for read + transfers. + + The bit-width of this signal always matches the CPUIF's bus width, + regardless of the regwidth. + + If the register does not contain any writable fields, this signal is omitted. + +hwif_out..wr_biten + Active-high bit-level write-enable strobes. + Only asserted bit positions will change the register value during a write + transfer. + + If the register does not contain any writable fields, this signal is omitted. + + +Read Response +^^^^^^^^^^^^^ +hwif_in..rd_ack + Single-cycle strobe indicating a read transfer has completed. + Qualifies all other read response signals. + + If the transfer is always completed in the same cycle, it is acceptable to + tie this signal to ``hwif_out..req && !hwif_out..req_is_wr``. + + If the register does not contain any readable fields, this signal is omitted. + +hwif_in..rd_data + Read response data. + + If the register does not contain any readable fields, this signal is omitted. + +Write Response +^^^^^^^^^^^^^^ +hwif_in..wr_ack + Single-cycle strobe indicating a write transfer has completed. + + If the transfer is always completed in the same cycle, it is acceptable to + tie this signal to ``hwif_out..req && hwif_out..req_is_wr``. + + If the register does not contain any writable fields, this signal is omitted. + + + +External Blocks +--------------- +Broader external address regions can be represented by external block-like +components such as ``addrmap``, ``regfile`` or ``mem`` elements. + +To ensure address decoding for external blocks is simple (only requires simple bit-pruning), +blocks that are external to an exported regblock shall be aligned to their size. + +Request +^^^^^^^ +hwif_out..req + When asserted, a read or write transfer will be initiated. + Qualifies all other request signals. + +hwif_out..addr + Byte-address of the transfer. + + Address is always relative to the block's local addressing. i.e: The first + byte within an external block is represented as ``hwif_out..addr`` == 0, + regardless of the absolute address of the block. + +hwif_out..req_is_wr + If ``1``, denotes that the current transfer is a write. Otherwise transfer is + a read. + +hwif_out..wr_data + Data to be written for the write transfer. This signal is ignored for read + transfers. + + The bit-width of this signal always matches the CPUIF's bus width, + regardless of the contents of the block. + +hwif_out..wr_biten + Active-high bit-level write-enable strobes. + Only asserted bit positions will change the register value during a write + transfer. + +Read Response +^^^^^^^^^^^^^ +hwif_in..rd_ack + Single-cycle strobe indicating a read transfer has completed. + Qualifies all other read response signals. + +hwif_in..rd_data + Read response data. + +Write Response +^^^^^^^^^^^^^^ +hwif_in..wr_ack + Single-cycle strobe indicating a write transfer has completed. diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..d37f57f --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +pygments-systemrdl +sphinxcontrib-wavedrom +sphinx-book-theme diff --git a/docs/udps/extended_swacc.rst b/docs/udps/extended_swacc.rst new file mode 100644 index 0000000..f760275 --- /dev/null +++ b/docs/udps/extended_swacc.rst @@ -0,0 +1,49 @@ +.. _extended_swacc: + +Read/Write-specific swacc +========================= + +SystemRDL defines the ``swacc`` property, but it does not distinguish between +read and write operations - it is asserted on *all* software accesses. +Similarly, the spec defines ``swmod`` which gets asserted on software writes, +but can also get asserted if the field has on-read side-effects. + +What if you just wanted a plain and simple strobe that is asserted when software +reads or writes a field? The ``rd_swacc`` and ``wr_swacc`` UDPs provide this +functionality. + + +Properties +---------- +These UDP definitions, along with others supported by PeakRDL-regblock can be +enabled by compiling the following file along with your design: +:download:`regblock_udps.rdl <../../hdl-src/regblock_udps.rdl>`. + +.. describe:: rd_swacc + + If true, infers an output signal ``hwif_out..rd_swacc`` that is asserted + when accessed by a software read operation. The output signal is asserted + on the same clock cycle that the field is being sampled during the software + read operation. + + .. wavedrom:: + + {"signal": [ + {"name": "clk", "wave": "p...."}, + {"name": "hwif_in..next", "wave": "x.=x.", "data": ["D"]}, + {"name": "hwif_out..rd_swacc", "wave": "0.10."} + ]} + + +.. describe:: wr_swacc + + If true, infers an output signal ``hwif_out..wr_swacc`` that is asserted + as the field is being modified by a software write operation. + + .. wavedrom:: + + {"signal": [ + {"name": "clk", "wave": "p....."}, + {"name": "hwif_out..value", "wave": "=..=..", "data": ["old", "new"]}, + {"name": "hwif_out..wr_swacc", "wave": "0.10.."} + ]} diff --git a/docs/udps/fixedpoint.rst b/docs/udps/fixedpoint.rst new file mode 100644 index 0000000..bbcd990 --- /dev/null +++ b/docs/udps/fixedpoint.rst @@ -0,0 +1,103 @@ +.. _fixedpoint: + +Fixed-Point Fields +================== + +`Fixed-point `_ numbers +can be used to efficiently represent real numbers using integers. Fixed-point +numbers consist of some combination of integer bits and fractional bits. The +number of integer/fractional bits is usually implicitly tracked (not stored) +for each number, unlike for floating-point numbers. + +For this SystemVerilog exporter, these properties only affect the signal type in +the the ``hwif`` structs. There is no special handling in the internals of +the regblock. + +Properties +---------- +Fields can be declared as fixed-point numbers using the following two properties: + +.. literalinclude:: ../../hdl-src/regblock_udps.rdl + :lines: 46-54 + +The :ref:`is_signed` property can be used in conjunction with these +properties to declare signed fixed-point fields. + +These UDP definitions, along with others supported by PeakRDL-regblock, can be +enabled by compiling the following file along with your design: +:download:`regblock_udps.rdl <../../hdl-src/regblock_udps.rdl>`. + +.. describe:: intwidth + + * The ``intwidth`` property defines the number of integer bits in the + fixed-point representation (including the sign bit, if present). + +.. describe:: fracwidth + + * The ``fracwidth`` property defines the number of fractional bits in the + fixed-point representation. + +Representable Numbers +^^^^^^^^^^^^^^^^^^^^^ + +The range of representable real numbers is summarized in the table below. + +.. list-table:: Representable Numbers + :header-rows: 1 + + * - Signedness + - Minimum Value + - Maximum Value + - Step Size + + * - Unsigned + - :math:`0` + - :math:`2^{\mathrm{intwidth}} - 2^{-\mathrm{fracwidth}}` + - :math:`2^{-\mathrm{fracwidth}}` + + * - Signed + - :math:`-2^{\mathrm{intwidth}-1}` + - :math:`2^{\mathrm{intwidth}-1} - 2^{-\mathrm{fracwidth}}` + - :math:`2^{-\mathrm{fracwidth}}` + +SystemVerilog Types +^^^^^^^^^^^^^^^^^^^ + +When either ``intwidth`` or ``fracwidth`` are defined for a field, that field's +type in the generated SystemVerilog ``hwif`` struct is +``logic (signed) [intwidth-1:-fracwidth]``. The bit at index :math:`i` contributes +a weight of :math:`2^i` to the real number represented. + +Other Rules +^^^^^^^^^^^ +* Only one of ``intwidth`` or ``fracwidth`` need be defined. The other is + inferred from the field bit width. +* The bit width of the field shall be equal to ``intwidth`` + ``fracwidth``. +* If both ``intwidth`` and ``fracwidth`` are defined for a field, it is an + error if their sum does not equal the bit width of the field. +* Either ``fracwidth`` or ``intwidth`` can be a negative integer. Because + SystemRDL does not have a signed integer type, the only way to achieve + this is to define one of the widths as larger than the bit width of the + component so that the other width is inferred as a negative number. +* The properties defined above are mutually exclusive with the ``counter`` + property. +* The properties defined above are mutually exclusive with the ``encode`` + property. + +Examples +-------- + +A 12-bit signed fixed-point field with 4 integer bits and 8 fractional bits +can be declared with + +.. code-block:: systemrdl + :emphasize-lines: 3, 4 + + field { + sw=rw; hw=r; + intwidth = 4; + is_signed; + } fixedpoint_num[11:0] = 0; + +This field can represent values from -8.0 to 7.99609375 +in steps of 0.00390625. diff --git a/docs/udps/intro.rst b/docs/udps/intro.rst new file mode 100644 index 0000000..63658ca --- /dev/null +++ b/docs/udps/intro.rst @@ -0,0 +1,85 @@ +Introduction +============ + +Although the official SystemRDL spec defines numerous properties that allow you +to define complex register map structures, sometimes they are not enough to +accurately describe a necessary feature. Fortunately the SystemRDL spec allows +the language to be extended using "User Defined Properties" (UDPs). The +PeakRDL-regblock tool understands several UDPs that are described in this +section. + +To enable these UDPs, compile this RDL file prior to the rest of your design: +:download:`regblock_udps.rdl <../../hdl-src/regblock_udps.rdl>`. + +.. list-table:: Summary of UDPs + :header-rows: 1 + + * - Name + - Component + - Type + - Description + + * - buffer_reads + - reg + - boolean + - If set, reads from the register are double-buffered. + + See: :ref:`read_buffering`. + + * - rbuffer_trigger + - reg + - reference + - Defines the buffered read load trigger. + + See: :ref:`read_buffering`. + + * - buffer_writes + - reg + - boolean + - If set, writes to the register are double-buffered. + + See: :ref:`write_buffering`. + + * - wbuffer_trigger + - reg + - reference + - Defines the buffered write commit trigger. + + See: :ref:`write_buffering`. + + * - rd_swacc + - field + - boolean + - Enables an output strobe that is asserted on sw reads. + + See: :ref:`extended_swacc`. + + * - wr_swacc + - field + - boolean + - Enables an output strobe that is asserted on sw writes. + + See: :ref:`extended_swacc`. + + * - is_signed + - field + - boolean + - Defines the signedness of a field. + + See: :ref:`signed`. + + * - intwidth + - field + - unsigned integer + - Defines the number of integer bits in the fixed-point representation + of a field. + + See: :ref:`fixedpoint`. + + * - fracwidth + - field + - unsigned integer + - Defines the number of fractional bits in the fixed-point representation + of a field. + + See: :ref:`fixedpoint`. diff --git a/docs/udps/read_buffering.rst b/docs/udps/read_buffering.rst new file mode 100644 index 0000000..41a175e --- /dev/null +++ b/docs/udps/read_buffering.rst @@ -0,0 +1,164 @@ +.. _read_buffering: + +Read-buffered Registers +======================= + +Read buffering is a mechanism that allows for software accesses to read a +snapshot of one or more registers atomically. When enabled on a register, a +read buffer will latch the state of its fields when triggered such that software +can read a coherent snapshot of one or more registers' value. + +Some examples of when this is useful: + * A wide 64-bit status register needs to be read atomically, but the CPU + interface is only 32-bits. + * Software needs to be able to read the state of multiple registers + atomically. + * A hardware event latches the software-visible state of one or more + registers. + +.. figure:: ../diagrams/rbuf.png + + +Properties +---------- +The behavior of read-buffered registers is defined using the following two +properties: + +.. literalinclude:: ../../hdl-src/regblock_udps.rdl + :lines: 10-18 + +These UDP definitions, along with others supported by PeakRDL-regblock can be +enabled by compiling the following file along with your design: +:download:`regblock_udps.rdl <../../hdl-src/regblock_udps.rdl>`. + +.. describe:: buffer_reads + + * Assigned value is a boolean. + * If true, enables double-buffering of software reads of this register. + * The read buffer will load the register's field values when its trigger + event is asserted. + * Unless specified otherwise, the buffer trigger occurs when the lowest + address of the buffered register is read. + * When read by software the data returned is from the buffer contents, not + directly from the register's fields. + +.. describe:: rbuffer_trigger + + * Assigned value is a reference to a register, single-bit field, signal, or + single-bit property. + * Controls when the double-buffer loads the register's field vaues into the + buffer storage element. + * If reference is a single-bit value (signal, field, property reference), + then the assertion of that value triggers the buffer to be evicted. + * Signal references shall have either activehigh/activelow property set to + define the polarity. + * If the reference is a reg, then buffer is loaded when the register's + lowest address is read. + +Other Rules +^^^^^^^^^^^ +* It is an error to set ``buffer_reads`` if the register does not contain any + readable fields +* If ``buffer_reads`` is false, then anything assigned to ``rbuffer_trigger`` + is ignored. +* The buffered register and the trigger reference shall both be within the same + internal device. ie: one cannot be in an external scope with respect to the + other. +* Unless it is a register, the reference assigned to ``rbuffer_trigger`` shall + represent a single bit. +* The software read operation considered to take place when the buffer is loaded. + This influences the behavior of properties like ``swmod`` and ``swacc`` - + they are not asserted until the register's fields are actually sampled by the + buffer. +* If a read-buffered register is wide (accesswidth < regwidth) and is its own + trigger, the first sub-word's buffer is bypassed to ensure the first read + operation is atomically coherent with the rest of the sampled register. + + +Examples +-------- +Below are several examples of what you can do with registers that are +read-buffered. + +Wide Atomic Register +^^^^^^^^^^^^^^^^^^^^ +In this example, a wide 64-bit read-clear counter is implemented. +Without read-buffering, it is impossible to coherently read the state of the +counter using a 32-bit CPU interface without risking a discontinuity. With +read-buffering enabled, the read of the lower half of the register will trigger +the upper half's value to be latched. A subsequent software access can then +coherently read the rest of the register's buffered value. + +.. code-block:: systemrdl + :emphasize-lines: 4 + + reg { + regwidth = 64; + accesswidth = 32; + buffer_reads = true; + field { + sw=r; hw=na; + counter; + incr; + } my_counter[63:0] = 0; + }; + + +Atomic Group of Registers +^^^^^^^^^^^^^^^^^^^^^^^^^ +Perhaps you have a group of registers that monitor some rapidly-changing state +within your design. Using the ``rbuffer_trigger`` property, you can define which +register read operation triggers the buffered registers' values to be latched. + +.. code-block:: systemrdl + :emphasize-lines: 11-14 + + reg my_status_reg { + field { + sw=r; hw=w; + } value[31:0]; + }; + + my_status_reg status1; + my_status_reg status2; + my_status_reg status3; + + status2->buffer_reads = true; + status2->rbuffer_trigger = status1; + status3->buffer_reads = true; + status3->rbuffer_trigger = status1; + +In this example, when software reads status1, this triggers status2-status3 +registers to latch their values into their respective read buffers. Subsequent +reads to status2 and status3 return the value that these registers contained at +the moment that status1 was read. This makes it possible for software to read +the state of multiple registers atomically. + + +Externally Triggered Register Sampling +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +If needed, an external trigger can be used to load a read buffer. +This can be useful if precise timing of software's view of the register state is +required. + +.. code-block:: systemrdl + :emphasize-lines: 14-15 + + reg my_status_reg { + buffer_reads = true; + field { + sw=r; hw=w; + } value[31:0]; + }; + + my_status_reg status1; + my_status_reg status2; + + signal { + activehigh; + } trigger_signal; + status1->rbuffer_trigger = trigger_signal; + status2->rbuffer_trigger = trigger_signal; + +When ``hwif_in..trigger_signal`` is asserted, the state of registers ``status1`` +and ``status2`` is buffered. diff --git a/docs/udps/signed.rst b/docs/udps/signed.rst new file mode 100644 index 0000000..b2c710c --- /dev/null +++ b/docs/udps/signed.rst @@ -0,0 +1,74 @@ +.. _signed: + +Signed Fields +============= + +SystemRDL does not natively provide a way to mark fields as signed or unsigned. +The ``is_signed`` user-defined property fills this need. + +For this SystemVerilog exporter, marking a field as signed only affects the +signal type in the ``hwif`` structs. There is no special handling in the internals +of the regblock. + +Properties +---------- +A field can be marked as signed using the following user-defined property: + +.. literalinclude:: ../../hdl-src/regblock_udps.rdl + :lines: 40-44 + +This UDP definition, along with others supported by PeakRDL-regblock, can be +enabled by compiling the following file along with your design: +:download:`regblock_udps.rdl <../../hdl-src/regblock_udps.rdl>`. + +.. describe:: is_signed + + * Assigned value is a boolean. + * If true, the hardware interface field will have the type + ``logic signed [width-1:0]``. + * If false or not defined for a field, the hardware interface field will + have the type ``logic [width-1:0]``, which is unsigned by definition. + +Other Rules +^^^^^^^^^^^ + +* ``is_signed=true`` is mutually exclusive with the ``counter`` property. +* ``is_signed=true`` is mutually exclusive with the ``encode`` property. + +Examples +-------- +Below are some examples of fields with different signedness. + +Signed Fields +^^^^^^^^^^^^^ +.. code-block:: systemrdl + :emphasize-lines: 3, 8 + + field { + sw=rw; hw=r; + is_signed; + } signed_num[63:0] = 0; + + field { + sw=r; hw=w; + is_signed = true; + } another_signed_num[19:0] = 20'hFFFFF; // -1 + +SystemRDL's own integer type is always unsigned. In order to specify a negative +reset value, the two's complement value must be used as shown in the second +example above. + +Unsigned Fields +^^^^^^^^^^^^^^^ +.. code-block:: systemrdl + :emphasize-lines: 3, 8 + + field { + sw=rw; hw=r; + // fields are unsigned by default + } unsigned_num[63:0] = 0; + + field { + sw=r; hw=w; + is_signed = false; + } another_unsigned_num[19:0] = 0; diff --git a/docs/udps/write_buffering.rst b/docs/udps/write_buffering.rst new file mode 100644 index 0000000..1d5d2ba --- /dev/null +++ b/docs/udps/write_buffering.rst @@ -0,0 +1,183 @@ +.. _write_buffering: + +Write-buffered Registers +======================== + +In order to support larger software write accesses that are atomic, the +regblock generator understands several UDPs that implement write-buffering to +specific registers. This causes the regblock to delay the effect of a software +write operation until a defined trigger event. + +Some examples of when this is useful: + * You need to have software update a wide 64-bit register atomically, but + the CPU interface is only 32-bits. + * Software needs to be able to write multiple registers such that the + hardware is updated atomically. + * Software can pre-load one or more registers with their next value, and + trigger the update via an external hardware signal. + +If a register is write-buffered, a holding buffer stage is inserted between the +decode logic and the field logic. This effectively defers any software write +operations to that register until a trigger event occurs that releases it. +Write buffering storage is unique to each register that enables it. +If a register is not write buffered, this buffer stage is bypassed. + +.. figure:: ../diagrams/wbuf.png + + +Properties +---------- +The behavior of write-buffered registers is defined using the following two +properties: + +.. literalinclude:: ../../hdl-src/regblock_udps.rdl + :lines: 20-28 + +These UDP definitions, along with others supported by PeakRDL-regblock can be +enabled by compiling the following file along with your design: +:download:`regblock_udps.rdl <../../hdl-src/regblock_udps.rdl>`. + +.. describe:: buffer_writes + + * Assigned value is a boolean. + * If true, enables double-buffering of writes to this register. + * Any software write operation to a buffered register is held back in a + storage element unique to the register. + * The software write operation is committed to the register once triggered + to do so. + * Unless specified otherwise, the buffer trigger occurs when the highest + address of the buffered register is written. + +.. describe:: wbuffer_trigger + + * Assigned value is a reference to a register, single-bit field, signal, + or single-bit property. + * Controls when the double-buffer commits the software write operation to + the register's fields. + * If reference is a single-bit value (signal, field, property reference), + then the assertion of that value triggers the buffer to be evicted. + * Signal references shall have either activehigh/activelow property set to + define the polarity. + * If the reference is a reg, then buffer is evicted when the register's + highest address is written. + + +Other Rules +^^^^^^^^^^^ +* It is an error to set ``buffer_writes`` if the register does not contain any + writable fields +* If ``buffer_writes`` is false, then anything assigned to ``wbuffer_trigger`` + is ignored. +* The buffered register and the trigger reference shall both be within the + same internal device. ie: one cannot be in an external scope with respect to + the other. +* Unless it is a register, the reference assigned to ``wbuffer_trigger`` shall + represent a single bit. +* If a buffered register was not written, any trigger events are ignored. +* It is valid for a buffered register to be partially written (either via + write strobes, or partial addressing). +* The software write operation is not considered to take place until the + buffer is evicted by the trigger. This influences the behavior of properties + like ``swmod`` and ``swacc`` - they are not asserted until the register's + fields are actually written by the buffer. + + + +Examples +-------- +Below are several examples of what you can do with registers that are +write-buffered. + +Wide Atomic Register +^^^^^^^^^^^^^^^^^^^^ +Without write-buffering, it is impossible to update the state of a 64-bit +register using a 32-bit CPU interface in a single clock-cycle. +In this example, it still requires two write-cycles to update the register, but +the register's storage element is not updated until both sub-words are written. +Upon writing the 2nd sub-word (the higher byte address), the write data for both +write cycles are committed to the register's storage element together on the +same clock cycle. The register is updated atomically. + +.. code-block:: systemrdl + :emphasize-lines: 4 + + reg { + regwidth = 64; + accesswidth = 32; + buffer_writes = true; + field { + sw=rw; hw=r; + } my_field[63:0] = 0; + }; + + +Atomic Group of Registers +^^^^^^^^^^^^^^^^^^^^^^^^^ +Perhaps you have a group of registers that need their state to be updated +atomically. Using the ``wbuffer_trigger`` property, you can define which +register write operation triggers the group to be updated. + + +.. code-block:: systemrdl + :emphasize-lines: 2, 18-20 + + reg my_buffered_reg { + buffer_writes = true; + field { + sw=rw; hw=r; + } my_field[31:0] = 0; + }; + + my_buffered_reg reg1; + my_buffered_reg reg2; + my_buffered_reg reg3; + + reg { + field { + sw=rw; hw=r; + } my_field[31:0] = 0; + } reg4; + + reg1->wbuffer_trigger = reg4; + reg2->wbuffer_trigger = reg4; + reg3->wbuffer_trigger = reg4; + + +In this example software may pre-write information into reg1-reg3, but the +register write operations do not take effect until software also writes to reg4. +The write operation to reg4 triggers the buffered data to be committed to +reg1-reg3. This is guaranteed to occur on the same clock-cycle. + + +Externally Triggered Register Update +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Some applications may require precise timing for when a register (or group of +registers) update their value. Often software cannot offer such timing +precision. + +In this example, the trigger event is bound to an external signal. When +asserted, any pending write operation the buffered register will be committed. +The hwif_out value presents the new register state on the clock cycle after the +trigger is asserted. + +.. code-block:: systemrdl + :emphasize-lines: 2, 11-13 + + reg my_buffered_reg { + buffer_writes = true; + field { + sw=rw; hw=r; + } my_field[31:0] = 0; + }; + + my_buffered_reg reg1; + my_buffered_reg reg2; + + signal { + activehigh; + } trigger_signal; + reg1->wbuffer_trigger = trigger_signal; + reg2->wbuffer_trigger = trigger_signal; + +After software writes to ``reg1`` & ``reg2``, the written data is held back in +the write buffer until ``hwif_in..trigger_signal`` is asserted by the hardware. diff --git a/hdl-src/README.md b/hdl-src/README.md new file mode 100644 index 0000000..f699d99 --- /dev/null +++ b/hdl-src/README.md @@ -0,0 +1,9 @@ +# HDL Source Files +This folder contains some SystemVerilog definitions that are useful collateral +to be used alongside this project. + +These reference files are free to use for any purpose and are not covered by +this project's LGPLv3 license. + +If for whatever reason you feel the need to reference a license when using +these, then lets go with the [MIT License](https://choosealicense.com/licenses/mit/) diff --git a/hdl-src/apb3_intf.sv b/hdl-src/apb3_intf.sv new file mode 100644 index 0000000..d18d3a0 --- /dev/null +++ b/hdl-src/apb3_intf.sv @@ -0,0 +1,40 @@ +interface apb3_intf #( + parameter DATA_WIDTH = 32, + parameter ADDR_WIDTH = 32 +); + // Command + logic PSEL; + logic PENABLE; + logic PWRITE; + logic [ADDR_WIDTH-1:0] PADDR; + logic [DATA_WIDTH-1:0] PWDATA; + + // Response + logic [DATA_WIDTH-1:0] PRDATA; + logic PREADY; + logic PSLVERR; + + modport master ( + output PSEL, + output PENABLE, + output PWRITE, + output PADDR, + output PWDATA, + + input PRDATA, + input PREADY, + input PSLVERR + ); + + modport slave ( + input PSEL, + input PENABLE, + input PWRITE, + input PADDR, + input PWDATA, + + output PRDATA, + output PREADY, + output PSLVERR + ); +endinterface diff --git a/hdl-src/apb4_intf.sv b/hdl-src/apb4_intf.sv new file mode 100644 index 0000000..4a554f8 --- /dev/null +++ b/hdl-src/apb4_intf.sv @@ -0,0 +1,46 @@ +interface apb4_intf #( + parameter DATA_WIDTH = 32, + parameter ADDR_WIDTH = 32 +); + // Command + logic PSEL; + logic PENABLE; + logic PWRITE; + logic [2:0] PPROT; + logic [ADDR_WIDTH-1:0] PADDR; + logic [DATA_WIDTH-1:0] PWDATA; + logic [DATA_WIDTH/8-1:0] PSTRB; + + // Response + logic [DATA_WIDTH-1:0] PRDATA; + logic PREADY; + logic PSLVERR; + + modport master ( + output PSEL, + output PENABLE, + output PWRITE, + output PPROT, + output PADDR, + output PWDATA, + output PSTRB, + + input PRDATA, + input PREADY, + input PSLVERR + ); + + modport slave ( + input PSEL, + input PENABLE, + input PWRITE, + input PPROT, + input PADDR, + input PWDATA, + input PSTRB, + + output PRDATA, + output PREADY, + output PSLVERR + ); +endinterface diff --git a/hdl-src/avalon_mm_intf.sv b/hdl-src/avalon_mm_intf.sv new file mode 100644 index 0000000..1d3d0c3 --- /dev/null +++ b/hdl-src/avalon_mm_intf.sv @@ -0,0 +1,46 @@ +interface avalon_mm_intf #( + parameter DATA_WIDTH = 32, + parameter ADDR_WIDTH = 32 // Important! Avalon uses word addressing +); + // Command + logic read; + logic write; + logic waitrequest; + logic [ADDR_WIDTH-1:0] address; + logic [DATA_WIDTH-1:0] writedata; + logic [DATA_WIDTH/8-1:0] byteenable; + + // Response + logic readdatavalid; + logic writeresponsevalid; + logic [DATA_WIDTH-1:0] readdata; + logic [1:0] response; + + modport host ( + output read, + output write, + input waitrequest, + output address, + output writedata, + output byteenable, + + input readdatavalid, + input writeresponsevalid, + input readdata, + input response + ); + + modport agent ( + input read, + input write, + output waitrequest, + input address, + input writedata, + input byteenable, + + output readdatavalid, + output writeresponsevalid, + output readdata, + output response + ); +endinterface diff --git a/hdl-src/axi4lite_intf.sv b/hdl-src/axi4lite_intf.sv new file mode 100644 index 0000000..b0a232d --- /dev/null +++ b/hdl-src/axi4lite_intf.sv @@ -0,0 +1,80 @@ +interface axi4lite_intf #( + parameter DATA_WIDTH = 32, + parameter ADDR_WIDTH = 32 +); + logic AWREADY; + logic AWVALID; + logic [ADDR_WIDTH-1:0] AWADDR; + logic [2:0] AWPROT; + + logic WREADY; + logic WVALID; + logic [DATA_WIDTH-1:0] WDATA; + logic [DATA_WIDTH/8-1:0] WSTRB; + + logic BREADY; + logic BVALID; + logic [1:0] BRESP; + + logic ARREADY; + logic ARVALID; + logic [ADDR_WIDTH-1:0] ARADDR; + logic [2:0] ARPROT; + + logic RREADY; + logic RVALID; + logic [DATA_WIDTH-1:0] RDATA; + logic [1:0] RRESP; + + modport master ( + input AWREADY, + output AWVALID, + output AWADDR, + output AWPROT, + + input WREADY, + output WVALID, + output WDATA, + output WSTRB, + + output BREADY, + input BVALID, + input BRESP, + + input ARREADY, + output ARVALID, + output ARADDR, + output ARPROT, + + output RREADY, + input RVALID, + input RDATA, + input RRESP + ); + + modport slave ( + output AWREADY, + input AWVALID, + input AWADDR, + input AWPROT, + + output WREADY, + input WVALID, + input WDATA, + input WSTRB, + + input BREADY, + output BVALID, + output BRESP, + + output ARREADY, + input ARVALID, + input ARADDR, + input ARPROT, + + input RREADY, + output RVALID, + output RDATA, + output RRESP + ); +endinterface diff --git a/hdl-src/regblock_udps.rdl b/hdl-src/regblock_udps.rdl new file mode 100644 index 0000000..f6ded7b --- /dev/null +++ b/hdl-src/regblock_udps.rdl @@ -0,0 +1,54 @@ +/* + * This file defines several property extensions that are understood by the + * PeakRDL-Regblock SystemVerilog code generator. + * + * Compile this file prior to your other SystemRDL sources. + * + * For more details, see: https://peakrdl-regblock.readthedocs.io/en/latest/udps/intro.html + */ + +property buffer_reads { + component = reg; + type = boolean; +}; + +property rbuffer_trigger { + component = reg; + type = ref; +}; + +property buffer_writes { + component = reg; + type = boolean; +}; + +property wbuffer_trigger { + component = reg; + type = ref; +}; + +property rd_swacc { + component = field; + type = boolean; +}; + +property wr_swacc { + component = field; + type = boolean; +}; + +property is_signed { + type = boolean; + component = field; + default = true; +}; + +property intwidth { + type = longint unsigned; + component = field; +}; + +property fracwidth { + type = longint unsigned; + component = field; +}; diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..db6f4f1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,51 @@ +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "peakrdl-regblock" +dynamic = ["version"] +requires-python = ">=3.7" +dependencies = [ + "systemrdl-compiler ~= 1.29", + "Jinja2>=2.11", +] + +authors = [ + {name="Alex Mykyta"}, +] +description = "Compile SystemRDL into a SystemVerilog control/status register (CSR) block" +readme = "README.md" +license = {text = "LGPLv3"} +keywords = [ + "SystemRDL", "PeakRDL", "CSR", "compiler", "tool", "registers", "generator", + "Verilog", "SystemVerilog", "register abstraction layer", + "FPGA", "ASIC", +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", + "Operating System :: OS Independent", + "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)", +] + +[project.optional-dependencies] +cli = [ + "peakrdl-cli >= 1.2.3", +] + +[project.urls] +Source = "https://github.com/SystemRDL/PeakRDL-regblock" +Tracker = "https://github.com/SystemRDL/PeakRDL-regblock/issues" +Changelog = "https://github.com/SystemRDL/PeakRDL-regblock/releases" +Documentation = "https://peakrdl-regblock.readthedocs.io/" + +[tool.setuptools.dynamic] +version = {attr = "peakrdl_regblock.__about__.__version__"} + +[project.entry-points."peakrdl.exporters"] +regblock = "peakrdl_regblock.__peakrdl__:Exporter" diff --git a/src/peakrdl_regblock/__about__.py b/src/peakrdl_regblock/__about__.py new file mode 100644 index 0000000..edebc2e --- /dev/null +++ b/src/peakrdl_regblock/__about__.py @@ -0,0 +1,2 @@ +version_info = (1, 1, 1) +__version__ = ".".join([str(n) for n in version_info]) diff --git a/src/peakrdl_regblock/__init__.py b/src/peakrdl_regblock/__init__.py new file mode 100644 index 0000000..c64c756 --- /dev/null +++ b/src/peakrdl_regblock/__init__.py @@ -0,0 +1,3 @@ +from .__about__ import __version__ + +from .exporter import RegblockExporter diff --git a/src/peakrdl_regblock/__peakrdl__.py b/src/peakrdl_regblock/__peakrdl__.py new file mode 100644 index 0000000..c67bdb5 --- /dev/null +++ b/src/peakrdl_regblock/__peakrdl__.py @@ -0,0 +1,205 @@ +from typing import TYPE_CHECKING, Dict, Type +import functools +import sys + +from peakrdl.plugins.exporter import ExporterSubcommandPlugin +from peakrdl.config import schema +from peakrdl.plugins.entry_points import get_entry_points + +from .exporter import RegblockExporter +from .cpuif import CpuifBase, apb3, apb4, axi4lite, passthrough, avalon +from .udps import ALL_UDPS + +if TYPE_CHECKING: + import argparse + from systemrdl.node import AddrmapNode + +class Exporter(ExporterSubcommandPlugin): + short_desc = "Generate a SystemVerilog control/status register (CSR) block" + + udp_definitions = ALL_UDPS + + cfg_schema = { + "cpuifs": {"*": schema.PythonObjectImport()}, + "default_reset": schema.Choice(["rst", "rst_n", "arst", "arst_n"]), + } + + @functools.lru_cache() + def get_cpuifs(self) -> Dict[str, Type[CpuifBase]]: + + # All built-in CPUIFs + cpuifs = { + "passthrough": passthrough.PassthroughCpuif, + "apb3": apb3.APB3_Cpuif, + "apb3-flat": apb3.APB3_Cpuif_flattened, + "apb4": apb4.APB4_Cpuif, + "apb4-flat": apb4.APB4_Cpuif_flattened, + "axi4-lite": axi4lite.AXI4Lite_Cpuif, + "axi4-lite-flat": axi4lite.AXI4Lite_Cpuif_flattened, + "avalon-mm": avalon.Avalon_Cpuif, + "avalon-mm-flat": avalon.Avalon_Cpuif_flattened, + } + + # Load any cpuifs specified via entry points + for ep, dist in get_entry_points("peakrdl_regblock.cpuif"): + name = ep.name + cpuif = ep.load() + if name in cpuifs: + raise RuntimeError(f"A plugin for 'peakrdl-regblock' tried to load cpuif '{name}' but it already exists") + if not issubclass(cpuif, CpuifBase): + raise RuntimeError(f"A plugin for 'peakrdl-regblock' tried to load cpuif '{name}' but it not a CpuifBase class") + cpuifs[name] = cpuif + + # Load any CPUIFs via config import + for name, cpuif in self.cfg['cpuifs'].items(): + if name in cpuifs: + raise RuntimeError(f"A plugin for 'peakrdl-regblock' tried to load cpuif '{name}' but it already exists") + if not issubclass(cpuif, CpuifBase): + raise RuntimeError(f"A plugin for 'peakrdl-regblock' tried to load cpuif '{name}' but it not a CpuifBase class") + cpuifs[name] = cpuif + + return cpuifs + + + def add_exporter_arguments(self, arg_group: 'argparse._ActionsContainer') -> None: + cpuifs = self.get_cpuifs() + + arg_group.add_argument( + "--cpuif", + choices=cpuifs.keys(), + default="apb3", + help="Select the CPU interface protocol to use [apb3]" + ) + + arg_group.add_argument( + "--module-name", + metavar="NAME", + default=None, + help="Override the SystemVerilog module name" + ) + + arg_group.add_argument( + "--package-name", + metavar="NAME", + default=None, + help="Override the SystemVerilog package name" + ) + + arg_group.add_argument( + "--type-style", + dest="type_style", + choices=['lexical', 'hier'], + default="lexical", + help="""Choose how HWIF struct type names are generated. + The 'lexical' style will use RDL lexical scope & type names where + possible and attempt to re-use equivalent type definitions. + The 'hier' style uses component's hierarchy as the struct type name. [lexical] + """ + ) + + arg_group.add_argument( + "--hwif-report", + action="store_true", + default=False, + help="Generate a HWIF report file" + ) + + arg_group.add_argument( + "--addr-width", + type=int, + default=None, + help="""Override the CPU interface's address width. By default, + address width is sized to the contents of the regblock. + """ + ) + + arg_group.add_argument( + "--rt-read-fanin", + action="store_true", + default=False, + help="Enable additional read path retiming. Good for register blocks with large readback fan-in" + ) + arg_group.add_argument( + "--rt-read-response", + action="store_true", + default=False, + help="Enable additional retiming stage between readback fan-in and cpu interface" + ) + arg_group.add_argument( + "--rt-external", + help="Retime outputs to external components. Specify a comma-separated list of: reg,regfile,mem,addrmap,all" + ) + + arg_group.add_argument( + "--default-reset", + choices=["rst", "rst_n", "arst", "arst_n"], + default=None, + help="""Choose the default style of reset signal if not explicitly + specified by the SystemRDL design. If unspecified, the default reset + is active-high and synchronous [rst]""" + ) + + + def do_export(self, top_node: 'AddrmapNode', options: 'argparse.Namespace') -> None: + cpuifs = self.get_cpuifs() + + retime_external_reg = False + retime_external_regfile = False + retime_external_mem = False + retime_external_addrmap = False + if options.rt_external: + for key in options.rt_external.split(","): + key = key.strip().lower() + if key == "reg": + retime_external_reg = True + elif key == "regfile": + retime_external_regfile = True + elif key == "mem": + retime_external_mem = True + elif key == "addrmap": + retime_external_addrmap = True + elif key == "all": + retime_external_reg = True + retime_external_regfile = True + retime_external_mem = True + retime_external_addrmap = True + else: + print("error: invalid option for --rt-external: '%s'" % key, file=sys.stderr) + + # Get default reset. Favor command-line over cfg. Fall back to 'rst' + default_rst = options.default_reset or self.cfg['default_reset'] or "rst" + if default_rst == "rst": + default_reset_activelow = False + default_reset_async = False + elif default_rst == "rst_n": + default_reset_activelow = True + default_reset_async = False + elif default_rst == "arst": + default_reset_activelow = False + default_reset_async = True + elif default_rst == "arst_n": + default_reset_activelow = True + default_reset_async = True + else: + raise RuntimeError + + + x = RegblockExporter() + x.export( + top_node, + options.output, + cpuif_cls=cpuifs[options.cpuif], + module_name=options.module_name, + package_name=options.package_name, + reuse_hwif_typedefs=(options.type_style == "lexical"), + retime_read_fanin=options.rt_read_fanin, + retime_read_response=options.rt_read_response, + retime_external_reg=retime_external_reg, + retime_external_regfile=retime_external_regfile, + retime_external_mem=retime_external_mem, + retime_external_addrmap=retime_external_addrmap, + generate_hwif_report=options.hwif_report, + address_width=options.addr_width, + default_reset_activelow=default_reset_activelow, + default_reset_async=default_reset_async, + ) diff --git a/src/peakrdl_regblock/addr_decode.py b/src/peakrdl_regblock/addr_decode.py new file mode 100644 index 0000000..1390192 --- /dev/null +++ b/src/peakrdl_regblock/addr_decode.py @@ -0,0 +1,219 @@ +from typing import TYPE_CHECKING, Union, List, Optional + +from systemrdl.node import FieldNode, RegNode +from systemrdl.walker import WalkerAction + +from .utils import get_indexed_path +from .struct_generator import RDLStructGenerator +from .forloop_generator import RDLForLoopGenerator +from .identifier_filter import kw_filter as kwf +from .sv_int import SVInt + +if TYPE_CHECKING: + from .exporter import RegblockExporter + from systemrdl.node import AddrmapNode, AddressableNode + from systemrdl.node import RegfileNode, MemNode + +class AddressDecode: + def __init__(self, exp:'RegblockExporter'): + self.exp = exp + + @property + def top_node(self) -> 'AddrmapNode': + return self.exp.ds.top_node + + def get_strobe_struct(self) -> str: + struct_gen = DecodeStructGenerator() + s = struct_gen.get_struct(self.top_node, "decoded_reg_strb_t") + assert s is not None # guaranteed to have at least one reg + return s + + def get_implementation(self) -> str: + gen = DecodeLogicGenerator(self) + s = gen.get_content(self.top_node) + assert s is not None + return s + + def get_access_strobe(self, node: Union[RegNode, FieldNode], reduce_substrobes: bool=True) -> str: + """ + Returns the Verilog string that represents the register/field's access strobe. + """ + if isinstance(node, FieldNode): + field = node + path = get_indexed_path(self.top_node, node.parent) + + regwidth = node.parent.get_property('regwidth') + accesswidth = node.parent.get_property('accesswidth') + if regwidth > accesswidth: + # Is wide register. + # Determine the substrobe(s) relevant to this field + sidx_hi = field.msb // accesswidth + sidx_lo = field.lsb // accesswidth + if sidx_hi == sidx_lo: + suffix = f"[{sidx_lo}]" + else: + suffix = f"[{sidx_hi}:{sidx_lo}]" + path += suffix + + if sidx_hi != sidx_lo and reduce_substrobes: + return "|decoded_reg_strb." + path + + else: + path = get_indexed_path(self.top_node, node) + + return "decoded_reg_strb." + path + + def get_external_block_access_strobe(self, node: 'AddressableNode') -> str: + assert node.external + assert not isinstance(node, RegNode) + path = get_indexed_path(self.top_node, node) + return "decoded_reg_strb." + path + + +class DecodeStructGenerator(RDLStructGenerator): + + def _enter_external_block(self, node: 'AddressableNode') -> None: + self.add_member( + kwf(node.inst_name), + array_dimensions=node.array_dimensions, + ) + + def enter_Addrmap(self, node: 'AddrmapNode') -> Optional[WalkerAction]: + assert node.external + self._enter_external_block(node) + return WalkerAction.SkipDescendants + + def exit_Addrmap(self, node: 'AddrmapNode') -> None: + assert node.external + + def enter_Regfile(self, node: 'RegfileNode') -> Optional[WalkerAction]: + if node.external: + self._enter_external_block(node) + return WalkerAction.SkipDescendants + super().enter_Regfile(node) + return WalkerAction.Continue + + def exit_Regfile(self, node: 'RegfileNode') -> None: + if node.external: + return + super().exit_Regfile(node) + + def enter_Mem(self, node: 'MemNode') -> Optional[WalkerAction]: + assert node.external + self._enter_external_block(node) + return WalkerAction.SkipDescendants + + def exit_Mem(self, node: 'MemNode') -> None: + assert node.external + + def enter_Reg(self, node: 'RegNode') -> None: + # if register is "wide", expand the strobe to be able to access the sub-words + n_subwords = node.get_property("regwidth") // node.get_property("accesswidth") + + self.add_member( + kwf(node.inst_name), + width=n_subwords, + array_dimensions=node.array_dimensions, + ) + + # Stub out + def exit_Reg(self, node: 'RegNode') -> None: + pass + def enter_Field(self, node: 'FieldNode') -> None: + pass + + +class DecodeLogicGenerator(RDLForLoopGenerator): + + def __init__(self, addr_decode: AddressDecode) -> None: + self.addr_decode = addr_decode + super().__init__() + + # List of address strides for each dimension + self._array_stride_stack = [] # type: List[int] + + + def enter_AddressableComponent(self, node: 'AddressableNode') -> Optional[WalkerAction]: + super().enter_AddressableComponent(node) + + if node.array_dimensions: + assert node.array_stride is not None + # Collect strides for each array dimension + current_stride = node.array_stride + strides = [] + for dim in reversed(node.array_dimensions): + strides.append(current_stride) + current_stride *= dim + strides.reverse() + self._array_stride_stack.extend(strides) + + if node.external and not isinstance(node, RegNode): + # Is an external block + addr_str = self._get_address_str(node) + strb = self.addr_decode.get_external_block_access_strobe(node) + rhs = f"cpuif_req_masked & (cpuif_addr >= {addr_str}) & (cpuif_addr <= {addr_str} + {SVInt(node.size - 1, self.addr_decode.exp.ds.addr_width)})" + self.add_content(f"{strb} = {rhs};") + self.add_content(f"is_external |= {rhs};") + return WalkerAction.SkipDescendants + + return WalkerAction.Continue + + + def _get_address_str(self, node: 'AddressableNode', subword_offset: int=0) -> str: + expr_width = self.addr_decode.exp.ds.addr_width + a = str(SVInt( + node.raw_absolute_address - self.addr_decode.top_node.raw_absolute_address + subword_offset, + expr_width + )) + for i, stride in enumerate(self._array_stride_stack): + a += f" + ({expr_width})'(i{i}) * {SVInt(stride, expr_width)}" + return a + + + def enter_Reg(self, node: RegNode) -> None: + regwidth = node.get_property('regwidth') + accesswidth = node.get_property('accesswidth') + + if regwidth == accesswidth: + rhs = f"cpuif_req_masked & (cpuif_addr == {self._get_address_str(node)})" + s = f"{self.addr_decode.get_access_strobe(node)} = {rhs};" + self.add_content(s) + if node.external: + readable = node.has_sw_readable + writable = node.has_sw_writable + if readable and writable: + self.add_content(f"is_external |= {rhs};") + elif readable and not writable: + self.add_content(f"is_external |= {rhs} & !cpuif_req_is_wr;") + elif not readable and writable: + self.add_content(f"is_external |= {rhs} & cpuif_req_is_wr;") + else: + raise RuntimeError + else: + # Register is wide. Create a substrobe for each subword + n_subwords = regwidth // accesswidth + subword_stride = accesswidth // 8 + for i in range(n_subwords): + rhs = f"cpuif_req_masked & (cpuif_addr == {self._get_address_str(node, subword_offset=(i*subword_stride))})" + s = f"{self.addr_decode.get_access_strobe(node)}[{i}] = {rhs};" + self.add_content(s) + if node.external: + readable = node.has_sw_readable + writable = node.has_sw_writable + if readable and writable: + self.add_content(f"is_external |= {rhs};") + elif readable and not writable: + self.add_content(f"is_external |= {rhs} & !cpuif_req_is_wr;") + elif not readable and writable: + self.add_content(f"is_external |= {rhs} & cpuif_req_is_wr;") + else: + raise RuntimeError + + def exit_AddressableComponent(self, node: 'AddressableNode') -> None: + super().exit_AddressableComponent(node) + + if not node.array_dimensions: + return + + for _ in node.array_dimensions: + self._array_stride_stack.pop() diff --git a/src/peakrdl_regblock/cpuif/__init__.py b/src/peakrdl_regblock/cpuif/__init__.py new file mode 100644 index 0000000..08b2adc --- /dev/null +++ b/src/peakrdl_regblock/cpuif/__init__.py @@ -0,0 +1 @@ +from .base import CpuifBase diff --git a/src/peakrdl_regblock/cpuif/apb3/__init__.py b/src/peakrdl_regblock/cpuif/apb3/__init__.py new file mode 100644 index 0000000..4b1adc1 --- /dev/null +++ b/src/peakrdl_regblock/cpuif/apb3/__init__.py @@ -0,0 +1,33 @@ +from ..base import CpuifBase + +class APB3_Cpuif(CpuifBase): + template_path = "apb3_tmpl.sv" + is_interface = True + + @property + def port_declaration(self) -> str: + return "apb3_intf.slave s_apb" + + def signal(self, name:str) -> str: + return "s_apb." + name.upper() + + +class APB3_Cpuif_flattened(APB3_Cpuif): + is_interface = False + + @property + def port_declaration(self) -> str: + lines = [ + "input wire " + self.signal("psel"), + "input wire " + self.signal("penable"), + "input wire " + self.signal("pwrite"), + f"input wire [{self.addr_width-1}:0] " + self.signal("paddr"), + f"input wire [{self.data_width-1}:0] " + self.signal("pwdata"), + "output logic " + self.signal("pready"), + f"output logic [{self.data_width-1}:0] " + self.signal("prdata"), + "output logic " + self.signal("pslverr"), + ] + return ",\n".join(lines) + + def signal(self, name:str) -> str: + return "s_apb_" + name diff --git a/src/peakrdl_regblock/cpuif/apb3/apb3_tmpl.sv b/src/peakrdl_regblock/cpuif/apb3/apb3_tmpl.sv new file mode 100644 index 0000000..33a3663 --- /dev/null +++ b/src/peakrdl_regblock/cpuif/apb3/apb3_tmpl.sv @@ -0,0 +1,48 @@ +{%- if cpuif.is_interface -%} +`ifndef SYNTHESIS + initial begin + assert_bad_addr_width: assert($bits({{cpuif.signal("paddr")}}) >= {{ds.package_name}}::{{ds.module_name.upper()}}_MIN_ADDR_WIDTH) + else $error("Interface address width of %0d is too small. Shall be at least %0d bits", $bits({{cpuif.signal("paddr")}}), {{ds.package_name}}::{{ds.module_name.upper()}}_MIN_ADDR_WIDTH); + assert_bad_data_width: assert($bits({{cpuif.signal("pwdata")}}) == {{ds.package_name}}::{{ds.module_name.upper()}}_DATA_WIDTH) + else $error("Interface data width of %0d is incorrect. Shall be %0d bits", $bits({{cpuif.signal("pwdata")}}), {{ds.package_name}}::{{ds.module_name.upper()}}_DATA_WIDTH); + end +`endif + +{% endif -%} + +// Request +logic is_active; +always_ff {{get_always_ff_event(cpuif.reset)}} begin + if({{get_resetsignal(cpuif.reset)}}) begin + is_active <= '0; + cpuif_req <= '0; + cpuif_req_is_wr <= '0; + cpuif_addr <= '0; + cpuif_wr_data <= '0; + end else begin + if(~is_active) begin + if({{cpuif.signal("psel")}}) begin + is_active <= '1; + cpuif_req <= '1; + cpuif_req_is_wr <= {{cpuif.signal("pwrite")}}; + {%- if cpuif.data_width_bytes == 1 %} + cpuif_addr <= {{cpuif.signal("paddr")}}[{{cpuif.addr_width-1}}:0]; + {%- else %} + cpuif_addr <= { {{-cpuif.signal("paddr")}}[{{cpuif.addr_width-1}}:{{clog2(cpuif.data_width_bytes)}}], {{clog2(cpuif.data_width_bytes)}}'b0}; + {%- endif %} + cpuif_wr_data <= {{cpuif.signal("pwdata")}}; + end + end else begin + cpuif_req <= '0; + if(cpuif_rd_ack || cpuif_wr_ack) begin + is_active <= '0; + end + end + end +end +assign cpuif_wr_biten = '1; + +// Response +assign {{cpuif.signal("pready")}} = cpuif_rd_ack | cpuif_wr_ack; +assign {{cpuif.signal("prdata")}} = cpuif_rd_data; +assign {{cpuif.signal("pslverr")}} = cpuif_rd_err | cpuif_wr_err; diff --git a/src/peakrdl_regblock/cpuif/apb4/__init__.py b/src/peakrdl_regblock/cpuif/apb4/__init__.py new file mode 100644 index 0000000..45b2961 --- /dev/null +++ b/src/peakrdl_regblock/cpuif/apb4/__init__.py @@ -0,0 +1,35 @@ +from ..base import CpuifBase + +class APB4_Cpuif(CpuifBase): + template_path = "apb4_tmpl.sv" + is_interface = True + + @property + def port_declaration(self) -> str: + return "apb4_intf.slave s_apb" + + def signal(self, name:str) -> str: + return "s_apb." + name.upper() + + +class APB4_Cpuif_flattened(APB4_Cpuif): + is_interface = False + + @property + def port_declaration(self) -> str: + lines = [ + "input wire " + self.signal("psel"), + "input wire " + self.signal("penable"), + "input wire " + self.signal("pwrite"), + "input wire [2:0] " + self.signal("pprot"), + f"input wire [{self.addr_width-1}:0] " + self.signal("paddr"), + f"input wire [{self.data_width-1}:0] " + self.signal("pwdata"), + f"input wire [{self.data_width_bytes-1}:0] " + self.signal("pstrb"), + "output logic " + self.signal("pready"), + f"output logic [{self.data_width-1}:0] " + self.signal("prdata"), + "output logic " + self.signal("pslverr"), + ] + return ",\n".join(lines) + + def signal(self, name:str) -> str: + return "s_apb_" + name diff --git a/src/peakrdl_regblock/cpuif/apb4/apb4_tmpl.sv b/src/peakrdl_regblock/cpuif/apb4/apb4_tmpl.sv new file mode 100644 index 0000000..4293bec --- /dev/null +++ b/src/peakrdl_regblock/cpuif/apb4/apb4_tmpl.sv @@ -0,0 +1,51 @@ +{%- if cpuif.is_interface -%} +`ifndef SYNTHESIS + initial begin + assert_bad_addr_width: assert($bits({{cpuif.signal("paddr")}}) >= {{ds.package_name}}::{{ds.module_name.upper()}}_MIN_ADDR_WIDTH) + else $error("Interface address width of %0d is too small. Shall be at least %0d bits", $bits({{cpuif.signal("paddr")}}), {{ds.package_name}}::{{ds.module_name.upper()}}_MIN_ADDR_WIDTH); + assert_bad_data_width: assert($bits({{cpuif.signal("pwdata")}}) == {{ds.package_name}}::{{ds.module_name.upper()}}_DATA_WIDTH) + else $error("Interface data width of %0d is incorrect. Shall be %0d bits", $bits({{cpuif.signal("pwdata")}}), {{ds.package_name}}::{{ds.module_name.upper()}}_DATA_WIDTH); + end +`endif + +{% endif -%} + +// Request +logic is_active; +always_ff {{get_always_ff_event(cpuif.reset)}} begin + if({{get_resetsignal(cpuif.reset)}}) begin + is_active <= '0; + cpuif_req <= '0; + cpuif_req_is_wr <= '0; + cpuif_addr <= '0; + cpuif_wr_data <= '0; + cpuif_wr_biten <= '0; + end else begin + if(~is_active) begin + if({{cpuif.signal("psel")}}) begin + is_active <= '1; + cpuif_req <= '1; + cpuif_req_is_wr <= {{cpuif.signal("pwrite")}}; + {%- if cpuif.data_width_bytes == 1 %} + cpuif_addr <= {{cpuif.signal("paddr")}}[{{cpuif.addr_width-1}}:0]; + {%- else %} + cpuif_addr <= { {{-cpuif.signal("paddr")}}[{{cpuif.addr_width-1}}:{{clog2(cpuif.data_width_bytes)}}], {{clog2(cpuif.data_width_bytes)}}'b0}; + {%- endif %} + cpuif_wr_data <= {{cpuif.signal("pwdata")}}; + for(int i=0; i<{{cpuif.data_width_bytes}}; i++) begin + cpuif_wr_biten[i*8 +: 8] <= {8{ {{-cpuif.signal("pstrb")}}[i]}}; + end + end + end else begin + cpuif_req <= '0; + if(cpuif_rd_ack || cpuif_wr_ack) begin + is_active <= '0; + end + end + end +end + +// Response +assign {{cpuif.signal("pready")}} = cpuif_rd_ack | cpuif_wr_ack; +assign {{cpuif.signal("prdata")}} = cpuif_rd_data; +assign {{cpuif.signal("pslverr")}} = cpuif_rd_err | cpuif_wr_err; diff --git a/src/peakrdl_regblock/cpuif/avalon/__init__.py b/src/peakrdl_regblock/cpuif/avalon/__init__.py new file mode 100644 index 0000000..20d8a59 --- /dev/null +++ b/src/peakrdl_regblock/cpuif/avalon/__init__.py @@ -0,0 +1,40 @@ +from ..base import CpuifBase +from ...utils import clog2 + +class Avalon_Cpuif(CpuifBase): + template_path = "avalon_tmpl.sv" + is_interface = True + + @property + def port_declaration(self) -> str: + return "avalon_mm_intf.agent avalon" + + def signal(self, name:str) -> str: + return "avalon." + name + + @property + def word_addr_width(self) -> int: + # Avalon agents use word addressing, therefore address width is reduced + return self.addr_width - clog2(self.data_width_bytes) + +class Avalon_Cpuif_flattened(Avalon_Cpuif): + is_interface = False + + @property + def port_declaration(self) -> str: + lines = [ + "input wire " + self.signal("read"), + "input wire " + self.signal("write"), + "output logic " + self.signal("waitrequest"), + f"input wire [{self.word_addr_width-1}:0] " + self.signal("address"), + f"input wire [{self.data_width-1}:0] " + self.signal("writedata"), + f"input wire [{self.data_width_bytes-1}:0] " + self.signal("byteenable"), + "output logic " + self.signal("readdatavalid"), + "output logic " + self.signal("writeresponsevalid"), + f"output logic [{self.data_width-1}:0] " + self.signal("readdata"), + "output logic [1:0] " + self.signal("response"), + ] + return ",\n".join(lines) + + def signal(self, name:str) -> str: + return "avalon_" + name diff --git a/src/peakrdl_regblock/cpuif/avalon/avalon_tmpl.sv b/src/peakrdl_regblock/cpuif/avalon/avalon_tmpl.sv new file mode 100644 index 0000000..fe59b23 --- /dev/null +++ b/src/peakrdl_regblock/cpuif/avalon/avalon_tmpl.sv @@ -0,0 +1,41 @@ +{%- if cpuif.is_interface -%} +`ifndef SYNTHESIS + initial begin + assert_bad_addr_width: assert($bits({{cpuif.signal("address")}}) >= {{cpuif.word_addr_width}}) + else $error("Interface address width of %0d is too small. Shall be at least %0d bits", $bits({{cpuif.signal("address")}}), {{cpuif.word_addr_width}}); + assert_bad_data_width: assert($bits({{cpuif.signal("writedata")}}) == {{ds.package_name}}::{{ds.module_name.upper()}}_DATA_WIDTH) + else $error("Interface data width of %0d is incorrect. Shall be %0d bits", $bits({{cpuif.signal("writedata")}}), {{ds.package_name}}::{{ds.module_name.upper()}}_DATA_WIDTH); + end +`endif + +{% endif -%} + +// Request +always_comb begin + cpuif_req = {{cpuif.signal("read")}} | {{cpuif.signal("write")}}; + cpuif_req_is_wr = {{cpuif.signal("write")}}; + {%- if cpuif.data_width_bytes == 1 %} + cpuif_addr = {{cpuif.signal("address")}}; + {%- else %} + cpuif_addr = { {{-cpuif.signal("address")}}, {{clog2(cpuif.data_width_bytes)}}'b0}; + {%- endif %} + cpuif_wr_data = {{cpuif.signal("writedata")}}; + for(int i=0; i<{{cpuif.data_width_bytes}}; i++) begin + cpuif_wr_biten[i*8 +: 8] = {8{ {{-cpuif.signal("byteenable")}}[i]}}; + end + {{cpuif.signal("waitrequest")}} = (cpuif_req_stall_rd & {{cpuif.signal("read")}}) | (cpuif_req_stall_wr & {{cpuif.signal("write")}}); +end + +// Response +always_comb begin + {{cpuif.signal("readdatavalid")}} = cpuif_rd_ack; + {{cpuif.signal("writeresponsevalid")}} = cpuif_wr_ack; + {{cpuif.signal("readdata")}} = cpuif_rd_data; + if(cpuif_rd_err || cpuif_wr_err) begin + // SLVERR + {{cpuif.signal("response")}} = 2'b10; + end else begin + // OK + {{cpuif.signal("response")}} = 2'b00; + end +end diff --git a/src/peakrdl_regblock/cpuif/axi4lite/__init__.py b/src/peakrdl_regblock/cpuif/axi4lite/__init__.py new file mode 100644 index 0000000..65dead5 --- /dev/null +++ b/src/peakrdl_regblock/cpuif/axi4lite/__init__.py @@ -0,0 +1,70 @@ +from ..base import CpuifBase + +class AXI4Lite_Cpuif(CpuifBase): + template_path = "axi4lite_tmpl.sv" + is_interface = True + + @property + def port_declaration(self) -> str: + return "axi4lite_intf.slave s_axil" + + def signal(self, name:str) -> str: + return "s_axil." + name.upper() + + @property + def regblock_latency(self) -> int: + return max(self.exp.ds.min_read_latency, self.exp.ds.min_write_latency) + + @property + def max_outstanding(self) -> int: + """ + Best pipelined performance is when the max outstanding transactions + is the design's latency + 2. + Anything beyond that does not have any effect, aside from adding unnecessary + logic and additional buffer-bloat latency. + """ + return self.regblock_latency + 2 + + @property + def resp_buffer_size(self) -> int: + """ + Response buffer size must be greater or equal to max outstanding + transactions to prevent response overrun. + """ + return self.max_outstanding + + +class AXI4Lite_Cpuif_flattened(AXI4Lite_Cpuif): + is_interface = False + + @property + def port_declaration(self) -> str: + lines = [ + "output logic " + self.signal("awready"), + "input wire " + self.signal("awvalid"), + f"input wire [{self.addr_width-1}:0] " + self.signal("awaddr"), + "input wire [2:0] " + self.signal("awprot"), + + "output logic " + self.signal("wready"), + "input wire " + self.signal("wvalid"), + f"input wire [{self.data_width-1}:0] " + self.signal("wdata"), + f"input wire [{self.data_width_bytes-1}:0]" + self.signal("wstrb"), + + "input wire " + self.signal("bready"), + "output logic " + self.signal("bvalid"), + "output logic [1:0] " + self.signal("bresp"), + + "output logic " + self.signal("arready"), + "input wire " + self.signal("arvalid"), + f"input wire [{self.addr_width-1}:0] " + self.signal("araddr"), + "input wire [2:0] " + self.signal("arprot"), + + "input wire " + self.signal("rready"), + "output logic " + self.signal("rvalid"), + f"output logic [{self.data_width-1}:0] " + self.signal("rdata"), + "output logic [1:0] " + self.signal("rresp"), + ] + return ",\n".join(lines) + + def signal(self, name:str) -> str: + return "s_axil_" + name diff --git a/src/peakrdl_regblock/cpuif/axi4lite/axi4lite_tmpl.sv b/src/peakrdl_regblock/cpuif/axi4lite/axi4lite_tmpl.sv new file mode 100644 index 0000000..d89113e --- /dev/null +++ b/src/peakrdl_regblock/cpuif/axi4lite/axi4lite_tmpl.sv @@ -0,0 +1,254 @@ +{%- if cpuif.is_interface -%} +`ifndef SYNTHESIS + initial begin + assert_bad_addr_width: assert($bits({{cpuif.signal("araddr")}}) >= {{ds.package_name}}::{{ds.module_name.upper()}}_MIN_ADDR_WIDTH) + else $error("Interface address width of %0d is too small. Shall be at least %0d bits", $bits({{cpuif.signal("araddr")}}), {{ds.package_name}}::{{ds.module_name.upper()}}_MIN_ADDR_WIDTH); + assert_bad_data_width: assert($bits({{cpuif.signal("wdata")}}) == {{ds.package_name}}::{{ds.module_name.upper()}}_DATA_WIDTH) + else $error("Interface data width of %0d is incorrect. Shall be %0d bits", $bits({{cpuif.signal("wdata")}}), {{ds.package_name}}::{{ds.module_name.upper()}}_DATA_WIDTH); + end +`endif + +{% endif -%} + +// Max Outstanding Transactions: {{cpuif.max_outstanding}} +logic [{{clog2(cpuif.max_outstanding+1)-1}}:0] axil_n_in_flight; +logic axil_prev_was_rd; +logic axil_arvalid; +logic [{{cpuif.addr_width-1}}:0] axil_araddr; +logic axil_ar_accept; +logic axil_awvalid; +logic [{{cpuif.addr_width-1}}:0] axil_awaddr; +logic axil_wvalid; +logic [{{cpuif.data_width-1}}:0] axil_wdata; +logic [{{cpuif.data_width_bytes-1}}:0] axil_wstrb; +logic axil_aw_accept; +logic axil_resp_acked; + +// Transaction request acceptance +always_ff {{get_always_ff_event(cpuif.reset)}} begin + if({{get_resetsignal(cpuif.reset)}}) begin + axil_prev_was_rd <= '0; + axil_arvalid <= '0; + axil_araddr <= '0; + axil_awvalid <= '0; + axil_awaddr <= '0; + axil_wvalid <= '0; + axil_wdata <= '0; + axil_wstrb <= '0; + axil_n_in_flight <= '0; + end else begin + // AR* acceptance register + if(axil_ar_accept) begin + axil_prev_was_rd <= '1; + axil_arvalid <= '0; + end + if({{cpuif.signal("arvalid")}} && {{cpuif.signal("arready")}}) begin + axil_arvalid <= '1; + axil_araddr <= {{cpuif.signal("araddr")}}; + end + + // AW* & W* acceptance registers + if(axil_aw_accept) begin + axil_prev_was_rd <= '0; + axil_awvalid <= '0; + axil_wvalid <= '0; + end + if({{cpuif.signal("awvalid")}} && {{cpuif.signal("awready")}}) begin + axil_awvalid <= '1; + axil_awaddr <= {{cpuif.signal("awaddr")}}; + end + if({{cpuif.signal("wvalid")}} && {{cpuif.signal("wready")}}) begin + axil_wvalid <= '1; + axil_wdata <= {{cpuif.signal("wdata")}}; + axil_wstrb <= {{cpuif.signal("wstrb")}}; + end + + // Keep track of in-flight transactions + if((axil_ar_accept || axil_aw_accept) && !axil_resp_acked) begin + axil_n_in_flight <= axil_n_in_flight + 1'b1; + end else if(!(axil_ar_accept || axil_aw_accept) && axil_resp_acked) begin + axil_n_in_flight <= axil_n_in_flight - 1'b1; + end + end +end + +always_comb begin + {{cpuif.signal("arready")}} = (!axil_arvalid || axil_ar_accept); + {{cpuif.signal("awready")}} = (!axil_awvalid || axil_aw_accept); + {{cpuif.signal("wready")}} = (!axil_wvalid || axil_aw_accept); +end + +// Request dispatch +always_comb begin + cpuif_wr_data = axil_wdata; + for(int i=0; i<{{cpuif.data_width_bytes}}; i++) begin + cpuif_wr_biten[i*8 +: 8] = {8{axil_wstrb[i]}}; + end + cpuif_req = '0; + cpuif_req_is_wr = '0; + cpuif_addr = '0; + axil_ar_accept = '0; + axil_aw_accept = '0; + + if(axil_n_in_flight < {{clog2(cpuif.max_outstanding+1)}}'d{{cpuif.max_outstanding}}) begin + // Can safely issue more transactions without overwhelming response buffer + if(axil_arvalid && !axil_prev_was_rd) begin + cpuif_req = '1; + cpuif_req_is_wr = '0; + {%- if cpuif.data_width_bytes == 1 %} + cpuif_addr = axil_araddr; + {%- else %} + cpuif_addr = {axil_araddr[{{cpuif.addr_width-1}}:{{clog2(cpuif.data_width_bytes)}}], {{clog2(cpuif.data_width_bytes)}}'b0}; + {%- endif %} + if(!cpuif_req_stall_rd) axil_ar_accept = '1; + end else if(axil_awvalid && axil_wvalid) begin + cpuif_req = '1; + cpuif_req_is_wr = '1; + {%- if cpuif.data_width_bytes == 1 %} + cpuif_addr = axil_awaddr; + {%- else %} + cpuif_addr = {axil_awaddr[{{cpuif.addr_width-1}}:{{clog2(cpuif.data_width_bytes)}}], {{clog2(cpuif.data_width_bytes)}}'b0}; + {%- endif %} + if(!cpuif_req_stall_wr) axil_aw_accept = '1; + end else if(axil_arvalid) begin + cpuif_req = '1; + cpuif_req_is_wr = '0; + {%- if cpuif.data_width_bytes == 1 %} + cpuif_addr = axil_araddr; + {%- else %} + cpuif_addr = {axil_araddr[{{cpuif.addr_width-1}}:{{clog2(cpuif.data_width_bytes)}}], {{clog2(cpuif.data_width_bytes)}}'b0}; + {%- endif %} + if(!cpuif_req_stall_rd) axil_ar_accept = '1; + end + end +end + + +// AXI4-Lite Response Logic +{%- if cpuif.resp_buffer_size == 1 %} +always_ff {{get_always_ff_event(cpuif.reset)}} begin + if({{get_resetsignal(cpuif.reset)}}) begin + {{cpuif.signal("rvalid")}} <= '0; + {{cpuif.signal("rresp")}} <= '0; + {{cpuif.signal("rdata")}} <= '0; + {{cpuif.signal("bvalid")}} <= '0; + {{cpuif.signal("bresp")}} <= '0; + end else begin + if({{cpuif.signal("rvalid")}} && {{cpuif.signal("rready")}}) begin + {{cpuif.signal("rvalid")}} <= '0; + end + + if({{cpuif.signal("bvalid")}} && {{cpuif.signal("bready")}}) begin + {{cpuif.signal("bvalid")}} <= '0; + end + + if(cpuif_rd_ack) begin + {{cpuif.signal("rvalid")}} <= '1; + {{cpuif.signal("rdata")}} <= cpuif_rd_data; + if(cpuif_rd_err) {{cpuif.signal("rresp")}} <= 2'b10; // SLVERR + else {{cpuif.signal("rresp")}} <= 2'b00; // OKAY + end + + if(cpuif_wr_ack) begin + {{cpuif.signal("bvalid")}} <= '1; + if(cpuif_wr_err) {{cpuif.signal("bresp")}} <= 2'b10; // SLVERR + else {{cpuif.signal("bresp")}} <= 2'b00; // OKAY + end + end +end + +always_comb begin + axil_resp_acked = '0; + if({{cpuif.signal("rvalid")}} && {{cpuif.signal("rready")}}) axil_resp_acked = '1; + if({{cpuif.signal("bvalid")}} && {{cpuif.signal("bready")}}) axil_resp_acked = '1; +end + +{%- else %} +struct { + logic is_wr; + logic err; + logic [{{cpuif.data_width-1}}:0] rdata; +} axil_resp_buffer[{{roundup_pow2(cpuif.resp_buffer_size)}}]; +{%- if not is_pow2(cpuif.resp_buffer_size) %} +// axil_resp_buffer is intentionally padded to the next power of two despite +// only requiring {{cpuif.resp_buffer_size}} entries. +// This is to avoid quirks in some tools that cannot handle indexing into a non-power-of-2 array. +// Unused entries are expected to be optimized away +{% endif %} + +logic [{{clog2(cpuif.resp_buffer_size)}}:0] axil_resp_wptr; +logic [{{clog2(cpuif.resp_buffer_size)}}:0] axil_resp_rptr; + +always_ff {{get_always_ff_event(cpuif.reset)}} begin + if({{get_resetsignal(cpuif.reset)}}) begin + for(int i=0; i<{{cpuif.resp_buffer_size}}; i++) begin + axil_resp_buffer[i].is_wr <= '0; + axil_resp_buffer[i].err <= '0; + axil_resp_buffer[i].rdata <= '0; + end + axil_resp_wptr <= '0; + axil_resp_rptr <= '0; + end else begin + // Store responses in buffer until AXI response channel accepts them + if(cpuif_rd_ack || cpuif_wr_ack) begin + if(cpuif_rd_ack) begin + axil_resp_buffer[axil_resp_wptr[{{clog2(cpuif.resp_buffer_size)-1}}:0]].is_wr <= '0; + axil_resp_buffer[axil_resp_wptr[{{clog2(cpuif.resp_buffer_size)-1}}:0]].err <= cpuif_rd_err; + axil_resp_buffer[axil_resp_wptr[{{clog2(cpuif.resp_buffer_size)-1}}:0]].rdata <= cpuif_rd_data; + + end else if(cpuif_wr_ack) begin + axil_resp_buffer[axil_resp_wptr[{{clog2(cpuif.resp_buffer_size)-1}}:0]].is_wr <= '1; + axil_resp_buffer[axil_resp_wptr[{{clog2(cpuif.resp_buffer_size)-1}}:0]].err <= cpuif_wr_err; + end + {%- if is_pow2(cpuif.resp_buffer_size) %} + axil_resp_wptr <= axil_resp_wptr + 1'b1; + {%- else %} + if(axil_resp_wptr[{{clog2(cpuif.resp_buffer_size)-1}}:0] == {{cpuif.resp_buffer_size-1}}) begin + axil_resp_wptr[{{clog2(cpuif.resp_buffer_size)-1}}:0] <= '0; + axil_resp_wptr[{{clog2(cpuif.resp_buffer_size)}}] <= ~axil_resp_wptr[{{clog2(cpuif.resp_buffer_size)}}]; + end else begin + axil_resp_wptr[{{clog2(cpuif.resp_buffer_size)-1}}:0] <= axil_resp_wptr[{{clog2(cpuif.resp_buffer_size)-1}}:0] + 1'b1; + end + {%- endif %} + end + + // Advance read pointer when acknowledged + if(axil_resp_acked) begin + {%- if is_pow2(cpuif.resp_buffer_size) %} + axil_resp_rptr <= axil_resp_rptr + 1'b1; + {%- else %} + if(axil_resp_rptr[{{clog2(cpuif.resp_buffer_size)-1}}:0] == {{cpuif.resp_buffer_size-1}}) begin + axil_resp_rptr[{{clog2(cpuif.resp_buffer_size)-1}}:0] <= '0; + axil_resp_rptr[{{clog2(cpuif.resp_buffer_size)}}] <= ~axil_resp_rptr[{{clog2(cpuif.resp_buffer_size)}}]; + end else begin + axil_resp_rptr[{{clog2(cpuif.resp_buffer_size)-1}}:0] <= axil_resp_rptr[{{clog2(cpuif.resp_buffer_size)-1}}:0] + 1'b1; + end + {%- endif %} + end + end +end + +always_comb begin + axil_resp_acked = '0; + {{cpuif.signal("bvalid")}} = '0; + {{cpuif.signal("rvalid")}} = '0; + if(axil_resp_rptr != axil_resp_wptr) begin + if(axil_resp_buffer[axil_resp_rptr[{{clog2(cpuif.resp_buffer_size)-1}}:0]].is_wr) begin + {{cpuif.signal("bvalid")}} = '1; + if({{cpuif.signal("bready")}}) axil_resp_acked = '1; + end else begin + {{cpuif.signal("rvalid")}} = '1; + if({{cpuif.signal("rready")}}) axil_resp_acked = '1; + end + end + + {{cpuif.signal("rdata")}} = axil_resp_buffer[axil_resp_rptr[{{clog2(cpuif.resp_buffer_size)-1}}:0]].rdata; + if(axil_resp_buffer[axil_resp_rptr[{{clog2(cpuif.resp_buffer_size)-1}}:0]].err) begin + {{cpuif.signal("bresp")}} = 2'b10; + {{cpuif.signal("rresp")}} = 2'b10; + end else begin + {{cpuif.signal("bresp")}} = 2'b00; + {{cpuif.signal("rresp")}} = 2'b00; + end +end +{%- endif %} diff --git a/src/peakrdl_regblock/cpuif/base.py b/src/peakrdl_regblock/cpuif/base.py new file mode 100644 index 0000000..2031a71 --- /dev/null +++ b/src/peakrdl_regblock/cpuif/base.py @@ -0,0 +1,76 @@ +from typing import TYPE_CHECKING, List +import inspect +import os + +import jinja2 as jj + +from ..utils import clog2, is_pow2, roundup_pow2 + +if TYPE_CHECKING: + from ..exporter import RegblockExporter + +class CpuifBase: + + # Path is relative to the location of the class that assigns this variable + template_path = "" + + def __init__(self, exp:'RegblockExporter'): + self.exp = exp + self.reset = exp.ds.top_node.cpuif_reset + + @property + def addr_width(self) -> int: + return self.exp.ds.addr_width + + @property + def data_width(self) -> int: + return self.exp.ds.cpuif_data_width + + @property + def data_width_bytes(self) -> int: + return self.data_width // 8 + + @property + def port_declaration(self) -> str: + raise NotImplementedError() + + @property + def parameters(self) -> List[str]: + """ + Optional list of additional parameters this CPU interface provides to + the module's definition + """ + return [] + + def _get_template_path_class_dir(self) -> str: + """ + Traverse up the MRO and find the first class that explicitly assigns + template_path. Returns the directory that contains the class definition. + """ + for cls in inspect.getmro(self.__class__): + if "template_path" in cls.__dict__: + class_dir = os.path.dirname(inspect.getfile(cls)) + return class_dir + raise RuntimeError + + + def get_implementation(self) -> str: + class_dir = self._get_template_path_class_dir() + loader = jj.FileSystemLoader(class_dir) + jj_env = jj.Environment( + loader=loader, + undefined=jj.StrictUndefined, + ) + + context = { + "cpuif": self, + "get_always_ff_event": self.exp.dereferencer.get_always_ff_event, + "get_resetsignal": self.exp.dereferencer.get_resetsignal, + "clog2": clog2, + "is_pow2": is_pow2, + "roundup_pow2": roundup_pow2, + "ds": self.exp.ds, + } + + template = jj_env.get_template(self.template_path) + return template.render(context) diff --git a/src/peakrdl_regblock/cpuif/passthrough/__init__.py b/src/peakrdl_regblock/cpuif/passthrough/__init__.py new file mode 100644 index 0000000..0363860 --- /dev/null +++ b/src/peakrdl_regblock/cpuif/passthrough/__init__.py @@ -0,0 +1,22 @@ +from ..base import CpuifBase + +class PassthroughCpuif(CpuifBase): + template_path = "passthrough_tmpl.sv" + + @property + def port_declaration(self) -> str: + lines = [ + "input wire s_cpuif_req", + "input wire s_cpuif_req_is_wr", + f"input wire [{self.addr_width-1}:0] s_cpuif_addr", + f"input wire [{self.data_width-1}:0] s_cpuif_wr_data", + f"input wire [{self.data_width-1}:0] s_cpuif_wr_biten", + "output wire s_cpuif_req_stall_wr", + "output wire s_cpuif_req_stall_rd", + "output wire s_cpuif_rd_ack", + "output wire s_cpuif_rd_err", + f"output wire [{self.data_width-1}:0] s_cpuif_rd_data", + "output wire s_cpuif_wr_ack", + "output wire s_cpuif_wr_err", + ] + return ",\n".join(lines) diff --git a/src/peakrdl_regblock/cpuif/passthrough/passthrough_tmpl.sv b/src/peakrdl_regblock/cpuif/passthrough/passthrough_tmpl.sv new file mode 100644 index 0000000..8e5bb70 --- /dev/null +++ b/src/peakrdl_regblock/cpuif/passthrough/passthrough_tmpl.sv @@ -0,0 +1,12 @@ +assign cpuif_req = s_cpuif_req; +assign cpuif_req_is_wr = s_cpuif_req_is_wr; +assign cpuif_addr = s_cpuif_addr; +assign cpuif_wr_data = s_cpuif_wr_data; +assign cpuif_wr_biten = s_cpuif_wr_biten; +assign s_cpuif_req_stall_wr = cpuif_req_stall_wr; +assign s_cpuif_req_stall_rd = cpuif_req_stall_rd; +assign s_cpuif_rd_ack = cpuif_rd_ack; +assign s_cpuif_rd_err = cpuif_rd_err; +assign s_cpuif_rd_data = cpuif_rd_data; +assign s_cpuif_wr_ack = cpuif_wr_ack; +assign s_cpuif_wr_err = cpuif_wr_err; diff --git a/src/peakrdl_regblock/dereferencer.py b/src/peakrdl_regblock/dereferencer.py new file mode 100644 index 0000000..d80e6ed --- /dev/null +++ b/src/peakrdl_regblock/dereferencer.py @@ -0,0 +1,264 @@ +from typing import TYPE_CHECKING, Union, Optional +from systemrdl.node import AddrmapNode, FieldNode, SignalNode, RegNode, AddressableNode +from systemrdl.rdltypes import PropertyReference + +from .sv_int import SVInt + +if TYPE_CHECKING: + from .exporter import RegblockExporter, DesignState + from .hwif import Hwif + from .field_logic import FieldLogic + from .addr_decode import AddressDecode + +class Dereferencer: + """ + This class provides an interface to convert conceptual SystemRDL references + into Verilog identifiers + """ + def __init__(self, exp:'RegblockExporter'): + self.exp = exp + + @property + def hwif(self) -> 'Hwif': + return self.exp.hwif + + @property + def address_decode(self) -> 'AddressDecode': + return self.exp.address_decode + + @property + def field_logic(self) -> 'FieldLogic': + return self.exp.field_logic + + @property + def ds(self) -> 'DesignState': + return self.exp.ds + + @property + def top_node(self) -> AddrmapNode: + return self.exp.ds.top_node + + def get_value(self, obj: Union[int, FieldNode, SignalNode, PropertyReference], width: Optional[int] = None) -> Union[SVInt, str]: + """ + Returns the Verilog string that represents the readable value associated + with the object. + + If given a simple scalar value, then the corresponding Verilog literal is returned. + + If obj references a structural systemrdl object, then the corresponding Verilog + expression is returned that represents its value. + + The optional width argument can be provided to hint at the expression's desired bitwidth. + """ + if isinstance(obj, int): + # Is a simple scalar value + return SVInt(obj, width) + + if isinstance(obj, FieldNode): + if obj.implements_storage: + return self.field_logic.get_storage_identifier(obj) + + if self.hwif.has_value_input(obj): + return self.hwif.get_input_identifier(obj, width) + + # Field does not have a storage element, nor does it have a HW input + # must be a constant value as defined by its reset value + reset_value = obj.get_property('reset') + if reset_value is not None: + return self.get_value(reset_value, obj.width) + else: + # No reset value defined! + obj.env.msg.warning( + f"Field '{obj.inst_name}' is a constant but does not have a known value (missing reset). Assigning it a value of X.", + obj.inst.inst_src_ref + ) + return "'X" + + if isinstance(obj, SignalNode): + # Signals are always inputs from the hwif + return self.hwif.get_input_identifier(obj, width) + + if isinstance(obj, PropertyReference): + if isinstance(obj.node, FieldNode): + return self.get_field_propref_value(obj.node, obj.name, width) + elif isinstance(obj.node, RegNode): + return self.get_reg_propref_value(obj.node, obj.name) + else: + raise RuntimeError + + raise RuntimeError(f"Unhandled reference to: {obj}") + + + def get_field_propref_value( + self, + field: FieldNode, + prop_name: str, + width: Optional[int] = None, + ) -> Union[SVInt, str]: + # Value reduction properties. + # Wrap with the appropriate Verilog reduction operator + if prop_name == "anded": + val = self.get_value(field) + return f"&({val})" + elif prop_name == "ored": + val = self.get_value(field) + return f"|({val})" + elif prop_name == "xored": + val = self.get_value(field) + return f"^({val})" + + # references that directly access a property value + if prop_name in { + 'decrvalue', + 'enable', + 'haltenable', + 'haltmask', + 'hwenable', + 'hwmask', + 'incrvalue', + 'mask', + 'reset', + 'resetsignal', + }: + return self.get_value(field.get_property(prop_name), width) + + # Field Next + if prop_name == "next": + prop_value = field.get_property(prop_name) + if prop_value is None: + # unset by the user, points to the implied internal signal + return self.field_logic.get_field_combo_identifier(field, "next") + else: + return self.get_value(prop_value, width) + + # References to another component value, or an implied input + if prop_name in {'hwclr', 'hwset'}: + prop_value = field.get_property(prop_name) + if prop_value is True: + # Points to inferred hwif input + return self.hwif.get_implied_prop_input_identifier(field, prop_name) + elif prop_value is False: + # This should never happen, as this is checked by the compiler's validator + raise RuntimeError + else: + return self.get_value(prop_value) + + # References to another component value, or an implied input + # May have a complementary partner property + complementary_pairs = { + "we": "wel", + "wel": "we", + "swwe": "swwel", + "swwel": "swwe", + } + if prop_name in complementary_pairs: + prop_value = field.get_property(prop_name) + if prop_value is True: + # Points to inferred hwif input + return self.hwif.get_implied_prop_input_identifier(field, prop_name) + elif prop_value is False: + # Try complementary property + prop_value = field.get_property(complementary_pairs[prop_name]) + if prop_value is True: + # Points to inferred hwif input + return f"!({self.hwif.get_implied_prop_input_identifier(field, complementary_pairs[prop_name])})" + elif prop_value is False: + # This should never happen, as this is checked by the compiler's validator + raise RuntimeError + else: + return f"!({self.get_value(prop_value)})" + else: + return self.get_value(prop_value, width) + + if prop_name == "swacc": + return self.field_logic.get_swacc_identifier(field) + if prop_name == "swmod": + return self.field_logic.get_swmod_identifier(field) + + + # translate aliases + aliases = { + "saturate": "incrsaturate", + "threshold": "incrthreshold", + } + prop_name = aliases.get(prop_name, prop_name) + + # Counter properties + if prop_name == 'incr': + return self.field_logic.get_counter_incr_strobe(field) + if prop_name == 'decr': + return self.field_logic.get_counter_decr_strobe(field) + + if prop_name in { + 'decrsaturate', + 'decrthreshold', + 'incrsaturate', + 'incrthreshold', + 'overflow', + 'underflow', + }: + return self.field_logic.get_field_combo_identifier(field, prop_name) + + raise RuntimeError(f"Unhandled reference to: {field}->{prop_name}") + + + def get_reg_propref_value(self, reg: RegNode, prop_name: str) -> str: + if prop_name in {'halt', 'intr'}: + return self.hwif.get_implied_prop_output_identifier(reg, prop_name) + raise NotImplementedError + + + def get_access_strobe(self, obj: Union[RegNode, FieldNode], reduce_substrobes: bool=True) -> str: + """ + Returns the Verilog string that represents the register's access strobe + """ + return self.address_decode.get_access_strobe(obj, reduce_substrobes) + + def get_external_block_access_strobe(self, obj: 'AddressableNode') -> str: + """ + Returns the Verilog string that represents the external block's access strobe + """ + return self.address_decode.get_external_block_access_strobe(obj) + + @property + def default_resetsignal_name(self) -> str: + s = "rst" + if self.ds.default_reset_async: + s = f"a{s}" + if self.ds.default_reset_activelow: + s = f"{s}_n" + return s + + + def get_resetsignal(self, obj: Optional[SignalNode] = None) -> str: + """ + Returns a normalized active-high reset signal + """ + if isinstance(obj, SignalNode): + s = self.get_value(obj) + if obj.get_property('activehigh'): + return str(s) + else: + return f"~{s}" + + # No explicit reset signal specified. Fall back to default reset signal + s = self.default_resetsignal_name + if self.ds.default_reset_activelow: + s = f"~{s}" + return s + + def get_always_ff_event(self, resetsignal: Optional[SignalNode] = None) -> str: + if resetsignal is None: + # No explicit reset signal specified. Fall back to default reset signal + if self.ds.default_reset_async: + if self.ds.default_reset_activelow: + return f"@(posedge clk or negedge {self.default_resetsignal_name})" + else: + return f"@(posedge clk or posedge {self.default_resetsignal_name})" + else: + return "@(posedge clk)" + elif resetsignal.get_property('async') and resetsignal.get_property('activehigh'): + return f"@(posedge clk or posedge {self.get_value(resetsignal)})" + elif resetsignal.get_property('async') and not resetsignal.get_property('activehigh'): + return f"@(posedge clk or negedge {self.get_value(resetsignal)})" + return "@(posedge clk)" diff --git a/src/peakrdl_regblock/exporter.py b/src/peakrdl_regblock/exporter.py new file mode 100644 index 0000000..b2c5f29 --- /dev/null +++ b/src/peakrdl_regblock/exporter.py @@ -0,0 +1,288 @@ +import os +from typing import TYPE_CHECKING, Union, Any, Type, Optional, Set, List +from collections import OrderedDict + +import jinja2 as jj +from systemrdl.node import AddrmapNode, RootNode + +from .addr_decode import AddressDecode +from .field_logic import FieldLogic +from .dereferencer import Dereferencer +from .readback import Readback +from .identifier_filter import kw_filter as kwf +from .utils import clog2 +from .scan_design import DesignScanner +from .validate_design import DesignValidator +from .cpuif import CpuifBase +from .cpuif.apb4 import APB4_Cpuif +from .hwif import Hwif +from .write_buffering import WriteBuffering +from .read_buffering import ReadBuffering +from .external_acks import ExternalWriteAckGenerator, ExternalReadAckGenerator +from .parity import ParityErrorReduceGenerator +from .sv_int import SVInt + +if TYPE_CHECKING: + from systemrdl.node import SignalNode + from systemrdl.rdltypes import UserEnum + +class RegblockExporter: + hwif: Hwif + cpuif: CpuifBase + address_decode: AddressDecode + field_logic: FieldLogic + readback: Readback + write_buffering: WriteBuffering + read_buffering: ReadBuffering + dereferencer: Dereferencer + ds: 'DesignState' + + def __init__(self, **kwargs: Any) -> None: + # Check for stray kwargs + if kwargs: + raise TypeError(f"got an unexpected keyword argument '{list(kwargs.keys())[0]}'") + + + loader = jj.ChoiceLoader([ + jj.FileSystemLoader(os.path.dirname(__file__)), + jj.PrefixLoader({ + 'base': jj.FileSystemLoader(os.path.dirname(__file__)), + }, delimiter=":") + ]) + + self.jj_env = jj.Environment( + loader=loader, + undefined=jj.StrictUndefined, + ) + + + def export(self, node: Union[RootNode, AddrmapNode], output_dir:str, **kwargs: Any) -> None: + """ + Parameters + ---------- + node: AddrmapNode + Top-level SystemRDL node to export. + output_dir: str + Path to the output directory where generated SystemVerilog will be written. + Output includes two files: a module definition and package definition. + cpuif_cls: :class:`peakrdl_regblock.cpuif.CpuifBase` + Specify the class type that implements the CPU interface of your choice. + Defaults to AMBA APB4. + module_name: str + Override the SystemVerilog module name. By default, the module name + is the top-level node's name. + package_name: str + Override the SystemVerilog package name. By default, the package name + is the top-level node's name with a "_pkg" suffix. + reuse_hwif_typedefs: bool + By default, the exporter will attempt to re-use hwif struct definitions for + nodes that are equivalent. This allows for better modularity and type reuse. + Struct type names are derived using the SystemRDL component's type + name and declared lexical scope path. + + If this is not desireable, override this parameter to ``False`` and structs + will be generated more naively using their hierarchical paths. + retime_read_fanin: bool + Set this to ``True`` to enable additional read path retiming. + For large register blocks that operate at demanding clock rates, this + may be necessary in order to manage large readback fan-in. + + The retiming flop stage is automatically placed in the most optimal point in the + readback path so that logic-levels and fanin are minimized. + + Enabling this option will increase read transfer latency by 1 clock cycle. + retime_read_response: bool + Set this to ``True`` to enable an additional retiming flop stage between + the readback mux and the CPU interface response logic. + This option may be beneficial for some CPU interfaces that implement the + response logic fully combinationally. Enabling this stage can better + isolate timing paths in the register file from the rest of your system. + + Enabling this when using CPU interfaces that already implement the + response path sequentially may not result in any meaningful timing improvement. + + Enabling this option will increase read transfer latency by 1 clock cycle. + retime_external_reg: bool + Retime outputs to external ``reg`` components. + retime_external_regfile: bool + Retime outputs to external ``regfile`` components. + retime_external_mem: bool + Retime outputs to external ``mem`` components. + retime_external_addrmap: bool + Retime outputs to external ``addrmap`` components. + generate_hwif_report: bool + If set, generates a hwif report that can help designers understand + the contents of the ``hwif_in`` and ``hwif_out`` structures. + address_width: int + Override the CPU interface's address width. By default, address width + is sized to the contents of the regblock. + default_reset_activelow: bool + If overriden to True, default reset is active-low instead of active-high. + default_reset_async: bool + If overriden to True, default reset is asynchronous instead of synchronous. + """ + # If it is the root node, skip to top addrmap + if isinstance(node, RootNode): + top_node = node.top + else: + top_node = node + + self.ds = DesignState(top_node, kwargs) + + cpuif_cls = kwargs.pop("cpuif_cls", None) or APB4_Cpuif # type: Type[CpuifBase] + generate_hwif_report = kwargs.pop("generate_hwif_report", False) # type: bool + + # Check for stray kwargs + if kwargs: + raise TypeError(f"got an unexpected keyword argument '{list(kwargs.keys())[0]}'") + + if generate_hwif_report: + path = os.path.join(output_dir, f"{self.ds.module_name}_hwif.rpt") + hwif_report_file = open(path, "w", encoding='utf-8') # pylint: disable=consider-using-with + else: + hwif_report_file = None + + # Construct exporter components + self.cpuif = cpuif_cls(self) + self.hwif = Hwif(self, hwif_report_file=hwif_report_file) + self.readback = Readback(self) + self.address_decode = AddressDecode(self) + self.field_logic = FieldLogic(self) + self.write_buffering = WriteBuffering(self) + self.read_buffering = ReadBuffering(self) + self.dereferencer = Dereferencer(self) + ext_write_acks = ExternalWriteAckGenerator(self) + ext_read_acks = ExternalReadAckGenerator(self) + parity = ParityErrorReduceGenerator(self) + + # Validate that there are no unsupported constructs + DesignValidator(self).do_validate() + + # Compute readback implementation early. + # Readback has the capability to disable retiming if the fanin is tiny. + # This affects the rest of the design's implementation, and must be known + # before any other templates are rendered + readback_implementation = self.readback.get_implementation() + + # Build Jinja template context + context = { + "cpuif": self.cpuif, + "hwif": self.hwif, + "write_buffering": self.write_buffering, + "read_buffering": self.read_buffering, + "get_resetsignal": self.dereferencer.get_resetsignal, + "default_resetsignal_name": self.dereferencer.default_resetsignal_name, + "address_decode": self.address_decode, + "field_logic": self.field_logic, + "readback_implementation": readback_implementation, + "ext_write_acks": ext_write_acks, + "ext_read_acks": ext_read_acks, + "parity": parity, + "get_always_ff_event": self.dereferencer.get_always_ff_event, + "ds": self.ds, + "kwf": kwf, + "SVInt" : SVInt, + } + + # Write out design + os.makedirs(output_dir, exist_ok=True) + package_file_path = os.path.join(output_dir, self.ds.package_name + ".sv") + template = self.jj_env.get_template("package_tmpl.sv") + stream = template.stream(context) + stream.dump(package_file_path) + + module_file_path = os.path.join(output_dir, self.ds.module_name + ".sv") + template = self.jj_env.get_template("module_tmpl.sv") + stream = template.stream(context) + stream.dump(module_file_path) + + if hwif_report_file: + hwif_report_file.close() + + +class DesignState: + """ + Dumping ground for all sorts of variables that are relevant to a particular + design. + """ + + def __init__(self, top_node: AddrmapNode, kwargs: Any) -> None: + self.top_node = top_node + msg = top_node.env.msg + + #------------------------ + # Extract compiler args + #------------------------ + self.reuse_hwif_typedefs = kwargs.pop("reuse_hwif_typedefs", True) # type: bool + self.module_name = kwargs.pop("module_name", None) or kwf(self.top_node.inst_name) # type: str + self.package_name = kwargs.pop("package_name", None) or (self.module_name + "_pkg") # type: str + user_addr_width = kwargs.pop("address_width", None) # type: Optional[int] + + # Pipelining options + self.retime_read_fanin = kwargs.pop("retime_read_fanin", False) # type: bool + self.retime_read_response = kwargs.pop("retime_read_response", False) # type: bool + self.retime_external_reg = kwargs.pop("retime_external_reg", False) # type: bool + self.retime_external_regfile = kwargs.pop("retime_external_regfile", False) # type: bool + self.retime_external_mem = kwargs.pop("retime_external_mem", False) # type: bool + self.retime_external_addrmap = kwargs.pop("retime_external_addrmap", False) # type: bool + + # Default reset type + self.default_reset_activelow = kwargs.pop("default_reset_activelow", False) # type: bool + self.default_reset_async = kwargs.pop("default_reset_async", False) # type: bool + + #------------------------ + # Info about the design + #------------------------ + self.cpuif_data_width = 0 + + # Collections of signals that were actually referenced by the design + self.in_hier_signal_paths = set() # type: Set[str] + self.out_of_hier_signals = OrderedDict() # type: OrderedDict[str, SignalNode] + + self.has_writable_msb0_fields = False + self.has_buffered_write_regs = False + self.has_buffered_read_regs = False + + self.has_external_block = False + self.has_external_addressable = False + + self.has_paritycheck = False + + # Track any referenced enums + self.user_enums = [] # type: List[Type[UserEnum]] + + # Scan the design to fill in above variables + DesignScanner(self).do_scan() + + if self.cpuif_data_width == 0: + # Scanner did not find any registers in the design being exported, + # so the width is not known. + # Assume 32-bits + msg.warning( + "Addrmap being exported only contains external components. Unable to infer the CPUIF bus width. Assuming 32-bits.", + self.top_node.inst.def_src_ref + ) + self.cpuif_data_width = 32 + + #------------------------ + # Min address width encloses the total size AND at least 1 useful address bit + self.addr_width = max(clog2(self.top_node.size), clog2(self.cpuif_data_width//8) + 1) + + if user_addr_width is not None: + if user_addr_width < self.addr_width: + msg.fatal(f"User-specified address width shall be greater than or equal to {self.addr_width}.") + self.addr_width = user_addr_width + + @property + def min_read_latency(self) -> int: + n = 0 + if self.retime_read_fanin: + n += 1 + if self.retime_read_response: + n += 1 + return n + + @property + def min_write_latency(self) -> int: + n = 0 + return n diff --git a/src/peakrdl_regblock/external_acks.py b/src/peakrdl_regblock/external_acks.py new file mode 100644 index 0000000..9a6c044 --- /dev/null +++ b/src/peakrdl_regblock/external_acks.py @@ -0,0 +1,54 @@ +from typing import TYPE_CHECKING + +from systemrdl.walker import WalkerAction +from systemrdl.node import RegNode + +from .forloop_generator import RDLForLoopGenerator + +if TYPE_CHECKING: + from .exporter import RegblockExporter + from systemrdl.node import AddressableNode + + +class ExternalWriteAckGenerator(RDLForLoopGenerator): + def __init__(self, exp: 'RegblockExporter') -> None: + super().__init__() + self.exp = exp + + def get_implementation(self) -> str: + content = self.get_content(self.exp.ds.top_node) + if content is None: + return "" + return content + + def enter_AddressableComponent(self, node: 'AddressableNode') -> WalkerAction: + super().enter_AddressableComponent(node) + + if node.external: + if not isinstance(node, RegNode) or node.has_sw_writable: + self.add_content(f"wr_ack |= {self.exp.hwif.get_external_wr_ack(node)};") + return WalkerAction.SkipDescendants + + return WalkerAction.Continue + + +class ExternalReadAckGenerator(RDLForLoopGenerator): + def __init__(self, exp: 'RegblockExporter') -> None: + super().__init__() + self.exp = exp + + def get_implementation(self) -> str: + content = self.get_content(self.exp.ds.top_node) + if content is None: + return "" + return content + + def enter_AddressableComponent(self, node: 'AddressableNode') -> WalkerAction: + super().enter_AddressableComponent(node) + + if node.external: + if not isinstance(node, RegNode) or node.has_sw_readable: + self.add_content(f"rd_ack |= {self.exp.hwif.get_external_rd_ack(node)};") + return WalkerAction.SkipDescendants + + return WalkerAction.Continue diff --git a/src/peakrdl_regblock/field_logic/__init__.py b/src/peakrdl_regblock/field_logic/__init__.py new file mode 100644 index 0000000..b44c7f6 --- /dev/null +++ b/src/peakrdl_regblock/field_logic/__init__.py @@ -0,0 +1,501 @@ +from typing import TYPE_CHECKING, Union + +from systemrdl.rdltypes import PrecedenceType, InterruptType + +from .bases import AssignmentPrecedence, NextStateConditional +from . import sw_onread +from . import sw_onwrite +from . import sw_singlepulse +from . import hw_write +from . import hw_set_clr +from . import hw_interrupts +from . import hw_interrupts_with_write + +from ..utils import get_indexed_path +from ..sv_int import SVInt + +from .generators import CombinationalStructGenerator, FieldStorageStructGenerator, FieldLogicGenerator + +if TYPE_CHECKING: + from typing import Dict, List + from systemrdl.node import AddrmapNode, FieldNode + from ..exporter import RegblockExporter, DesignState + +class FieldLogic: + def __init__(self, exp:'RegblockExporter'): + self.exp = exp + + self._hw_conditionals = {} # type: Dict[int, List[NextStateConditional]] + self._sw_conditionals = {} # type: Dict[int, List[NextStateConditional]] + + self.init_conditionals() + + @property + def ds(self) -> 'DesignState': + return self.exp.ds + + @property + def top_node(self) -> 'AddrmapNode': + return self.exp.ds.top_node + + def get_storage_struct(self) -> str: + struct_gen = FieldStorageStructGenerator(self) + s = struct_gen.get_struct(self.top_node, "field_storage_t") + + # Only declare the storage struct if it exists + if s is None: + return "" + + return s + "\nfield_storage_t field_storage;" + + def get_combo_struct(self) -> str: + struct_gen = CombinationalStructGenerator(self) + s = struct_gen.get_struct(self.top_node, "field_combo_t") + + # Only declare the storage struct if it exists + if s is None: + return "" + + return s + "\nfield_combo_t field_combo;" + + def get_implementation(self) -> str: + gen = FieldLogicGenerator(self) + s = gen.get_content(self.top_node) + if s is None: + return "" + return s + + #--------------------------------------------------------------------------- + # Field utility functions + #--------------------------------------------------------------------------- + def get_storage_identifier(self, field: 'FieldNode') -> str: + """ + Returns the Verilog string that represents the storage register element + for the referenced field + """ + assert field.implements_storage + path = get_indexed_path(self.top_node, field) + return f"field_storage.{path}.value" + + def get_next_q_identifier(self, field: 'FieldNode') -> str: + """ + Returns the Verilog string that represents the storage register element + for the delayed 'next' input value + """ + assert field.implements_storage + path = get_indexed_path(self.top_node, field) + return f"field_storage.{path}.next_q" + + def get_field_combo_identifier(self, field: 'FieldNode', name: str) -> str: + """ + Returns a Verilog string that represents a field's internal combinational + signal. + """ + assert field.implements_storage + path = get_indexed_path(self.top_node, field) + return f"field_combo.{path}.{name}" + + def get_counter_incr_strobe(self, field: 'FieldNode') -> str: + """ + Return the Verilog string that represents the field's incr strobe signal. + """ + prop_value = field.get_property('incr') + if prop_value: + return str(self.exp.dereferencer.get_value(prop_value)) + + # unset by the user, points to the implied input signal + return self.exp.hwif.get_implied_prop_input_identifier(field, "incr") + + def get_counter_incrvalue(self, field: 'FieldNode') -> Union[SVInt, str]: + """ + Return the string that represents the field's increment value + """ + incrvalue = field.get_property('incrvalue') + if incrvalue is not None: + return self.exp.dereferencer.get_value(incrvalue, field.width) + if field.get_property('incrwidth'): + return self.exp.hwif.get_implied_prop_input_identifier(field, "incrvalue") + return "1'b1" + + def get_counter_incrsaturate_value(self, field: 'FieldNode') -> Union[SVInt, str]: + prop_value = field.get_property('incrsaturate') + if prop_value is True: + return self.exp.dereferencer.get_value(2**field.width - 1, field.width) + return self.exp.dereferencer.get_value(prop_value, field.width) + + def counter_incrsaturates(self, field: 'FieldNode') -> bool: + """ + Returns True if the counter saturates + """ + return field.get_property('incrsaturate') is not False + + def get_counter_incrthreshold_value(self, field: 'FieldNode') -> Union[SVInt, str]: + prop_value = field.get_property('incrthreshold') + if isinstance(prop_value, bool): + # No explicit value set. use max + return self.exp.dereferencer.get_value(2**field.width - 1, field.width) + return self.exp.dereferencer.get_value(prop_value, field.width) + + def get_counter_decr_strobe(self, field: 'FieldNode') -> str: + """ + Return the Verilog string that represents the field's incr strobe signal. + """ + prop_value = field.get_property('decr') + if prop_value: + return str(self.exp.dereferencer.get_value(prop_value)) + + # unset by the user, points to the implied input signal + return self.exp.hwif.get_implied_prop_input_identifier(field, "decr") + + def get_counter_decrvalue(self, field: 'FieldNode') -> Union[SVInt, str]: + """ + Return the string that represents the field's decrement value + """ + decrvalue = field.get_property('decrvalue') + if decrvalue is not None: + return self.exp.dereferencer.get_value(decrvalue, field.width) + if field.get_property('decrwidth'): + return self.exp.hwif.get_implied_prop_input_identifier(field, "decrvalue") + return "1'b1" + + def get_counter_decrsaturate_value(self, field: 'FieldNode') -> Union[SVInt, str]: + prop_value = field.get_property('decrsaturate') + if prop_value is True: + return f"{field.width}'d0" + return self.exp.dereferencer.get_value(prop_value, field.width) + + def counter_decrsaturates(self, field: 'FieldNode') -> bool: + """ + Returns True if the counter saturates + """ + return field.get_property('decrsaturate') is not False + + def get_counter_decrthreshold_value(self, field: 'FieldNode') -> Union[SVInt, str]: + prop_value = field.get_property('decrthreshold') + if isinstance(prop_value, bool): + # No explicit value set. use min + return f"{field.width}'d0" + return self.exp.dereferencer.get_value(prop_value, field.width) + + def get_swacc_identifier(self, field: 'FieldNode') -> str: + """ + Asserted when field is software accessed (read or write) + """ + buffer_reads = field.parent.get_property('buffer_reads') + buffer_writes = field.parent.get_property('buffer_writes') + if buffer_reads and buffer_writes: + rstrb = self.exp.read_buffering.get_trigger(field.parent) + wstrb = self.exp.write_buffering.get_write_strobe(field) + return f"{rstrb} || {wstrb}" + elif buffer_reads and not buffer_writes: + strb = self.exp.dereferencer.get_access_strobe(field) + rstrb = self.exp.read_buffering.get_trigger(field.parent) + return f"{rstrb} || ({strb} && decoded_req_is_wr)" + elif not buffer_reads and buffer_writes: + strb = self.exp.dereferencer.get_access_strobe(field) + wstrb = self.exp.write_buffering.get_write_strobe(field) + return f"{wstrb} || ({strb} && !decoded_req_is_wr)" + else: + strb = self.exp.dereferencer.get_access_strobe(field) + return strb + + def get_rd_swacc_identifier(self, field: 'FieldNode') -> str: + """ + Asserted when field is software accessed (read) + """ + buffer_reads = field.parent.get_property('buffer_reads') + if buffer_reads: + rstrb = self.exp.read_buffering.get_trigger(field.parent) + return rstrb + else: + strb = self.exp.dereferencer.get_access_strobe(field) + return f"{strb} && !decoded_req_is_wr" + + def get_wr_swacc_identifier(self, field: 'FieldNode') -> str: + """ + Asserted when field is software accessed (write) + """ + buffer_writes = field.parent.get_property('buffer_writes') + if buffer_writes: + wstrb = self.exp.write_buffering.get_write_strobe(field) + return wstrb + else: + strb = self.exp.dereferencer.get_access_strobe(field) + return f"{strb} && decoded_req_is_wr" + + def get_swmod_identifier(self, field: 'FieldNode') -> str: + """ + Asserted when field is modified by software (written or read with a + set or clear side effect). + """ + w_modifiable = field.is_sw_writable + r_modifiable = field.get_property('onread') is not None + buffer_writes = field.parent.get_property('buffer_writes') + buffer_reads = field.parent.get_property('buffer_reads') + accesswidth = field.parent.get_property("accesswidth") + + + astrb = self.exp.dereferencer.get_access_strobe(field) + + conditions = [] + if r_modifiable: + if buffer_reads: + rstrb = self.exp.read_buffering.get_trigger(field.parent) + else: + rstrb = f"{astrb} && !decoded_req_is_wr" + conditions.append(rstrb) + + if w_modifiable: + if buffer_writes: + wstrb = self.exp.write_buffering.get_write_strobe(field) + else: + wstrb = f"{astrb} && decoded_req_is_wr" + + # Due to 10.6.1-f, it is impossible for a field that is sw-writable to + # be split across subwords. + # Therefore it is ok to get the subword idx from only one of the bit offsets + # in order to compute the biten range + sidx = field.low // accesswidth + biten = self.get_wr_biten(field, sidx) + wstrb += f" && |({biten})" + + conditions.append(wstrb) + + if not conditions: + # Not sw modifiable + return "1'b0" + else: + return " || ".join(conditions) + + + def get_parity_identifier(self, field: 'FieldNode') -> str: + """ + Returns the identifier for the stored 'golden' parity value of the field + """ + path = get_indexed_path(self.top_node, field) + return f"field_storage.{path}.parity" + + def get_parity_error_identifier(self, field: 'FieldNode') -> str: + """ + Returns the identifier for whether the field currently has a parity error + """ + path = get_indexed_path(self.top_node, field) + return f"field_combo.{path}.parity_error" + + def has_next_q(self, field: 'FieldNode') -> bool: + """ + Some fields require a delayed version of their 'next' input signal in + order to do edge-detection. + + Returns True if this is the case. + """ + if field.get_property('intr type') in { + InterruptType.posedge, + InterruptType.negedge, + InterruptType.bothedge + }: + return True + + return False + + def get_wbus_bitslice(self, field: 'FieldNode', subword_idx: int = 0) -> str: + """ + Get the bitslice range string of the internal cpuif's data/biten bus + that corresponds to this field + """ + if field.parent.get_property('buffer_writes'): + # register is buffered. + # write buffer is the full width of the register. no need to deal with subwords + high = field.high + low = field.low + if field.msb < field.lsb: + # slice is for an msb0 field. + # mirror it + regwidth = field.parent.get_property('regwidth') + low = regwidth - 1 - low + high = regwidth - 1 - high + low, high = high, low + else: + # Regular non-buffered register + # For normal fields this ends up passing-through the field's low/high + # values unchanged. + # For fields within a wide register (accesswidth < regwidth), low/high + # may be shifted down and clamped depending on which sub-word is being accessed + accesswidth = field.parent.get_property('accesswidth') + + # Shift based on subword + high = field.high - (subword_idx * accesswidth) + low = field.low - (subword_idx * accesswidth) + + # clamp to accesswidth + high = max(min(high, accesswidth), 0) + low = max(min(low, accesswidth), 0) + + if field.msb < field.lsb: + # slice is for an msb0 field. + # mirror it + bus_width = self.exp.cpuif.data_width + low = bus_width - 1 - low + high = bus_width - 1 - high + low, high = high, low + + return f"[{high}:{low}]" + + def get_wr_biten(self, field: 'FieldNode', subword_idx: int=0) -> str: + """ + Get the bit-enable slice that corresponds to this field + """ + if field.parent.get_property('buffer_writes'): + # Is buffered. Use value from write buffer + # No need to check msb0 ordering. Bus is pre-swapped, and bitslice + # accounts for it + bslice = self.get_wbus_bitslice(field) + wbuf_prefix = self.exp.write_buffering.get_wbuf_prefix(field) + return wbuf_prefix + ".biten" + bslice + else: + # Regular non-buffered register + bslice = self.get_wbus_bitslice(field, subword_idx) + + if field.msb < field.lsb: + # Field gets bitswapped since it is in [low:high] orientation + value = "decoded_wr_biten_bswap" + bslice + else: + value = "decoded_wr_biten" + bslice + return value + + def get_wr_data(self, field: 'FieldNode', subword_idx: int=0) -> str: + """ + Get the write data slice that corresponds to this field + """ + if field.parent.get_property('buffer_writes'): + # Is buffered. Use value from write buffer + # No need to check msb0 ordering. Bus is pre-swapped, and bitslice + # accounts for it + bslice = self.get_wbus_bitslice(field) + wbuf_prefix = self.exp.write_buffering.get_wbuf_prefix(field) + return wbuf_prefix + ".data" + bslice + else: + # Regular non-buffered register + bslice = self.get_wbus_bitslice(field, subword_idx) + + if field.msb < field.lsb: + # Field gets bitswapped since it is in [low:high] orientation + value = "decoded_wr_data_bswap" + bslice + else: + value = "decoded_wr_data" + bslice + return value + + #--------------------------------------------------------------------------- + # Field Logic Conditionals + #--------------------------------------------------------------------------- + def add_hw_conditional(self, conditional: NextStateConditional, precedence: AssignmentPrecedence) -> None: + """ + Register a NextStateConditional action for hardware-triggered field updates. + Categorizing conditionals correctly by hw/sw ensures that the RDL precedence + property can be reliably honored. + + The ``precedence`` argument determines the conditional assignment's priority over + other assignments of differing precedence. + + If multiple conditionals of the same precedence are registered, they are + searched sequentially and only the first to match the given field is used. + """ + if precedence not in self._hw_conditionals: + self._hw_conditionals[precedence] = [] + self._hw_conditionals[precedence].append(conditional) + + + def add_sw_conditional(self, conditional: NextStateConditional, precedence: AssignmentPrecedence) -> None: + """ + Register a NextStateConditional action for software-triggered field updates. + Categorizing conditionals correctly by hw/sw ensures that the RDL precedence + property can be reliably honored. + + The ``precedence`` argument determines the conditional assignment's priority over + other assignments of differing precedence. + + If multiple conditionals of the same precedence are registered, they are + searched sequentially and only the first to match the given field is used. + """ + if precedence not in self._sw_conditionals: + self._sw_conditionals[precedence] = [] + self._sw_conditionals[precedence].append(conditional) + + + def init_conditionals(self) -> None: + """ + Initialize all possible conditionals here. + + Remember: The order in which conditionals are added matters within the + same assignment precedence. + """ + + self.add_sw_conditional(sw_onread.ClearOnRead(self.exp), AssignmentPrecedence.SW_ONREAD) + self.add_sw_conditional(sw_onread.SetOnRead(self.exp), AssignmentPrecedence.SW_ONREAD) + + self.add_sw_conditional(sw_onwrite.Write(self.exp), AssignmentPrecedence.SW_ONWRITE) + self.add_sw_conditional(sw_onwrite.WriteSet(self.exp), AssignmentPrecedence.SW_ONWRITE) + self.add_sw_conditional(sw_onwrite.WriteClear(self.exp), AssignmentPrecedence.SW_ONWRITE) + self.add_sw_conditional(sw_onwrite.WriteZeroToggle(self.exp), AssignmentPrecedence.SW_ONWRITE) + self.add_sw_conditional(sw_onwrite.WriteZeroClear(self.exp), AssignmentPrecedence.SW_ONWRITE) + self.add_sw_conditional(sw_onwrite.WriteZeroSet(self.exp), AssignmentPrecedence.SW_ONWRITE) + self.add_sw_conditional(sw_onwrite.WriteOneToggle(self.exp), AssignmentPrecedence.SW_ONWRITE) + self.add_sw_conditional(sw_onwrite.WriteOneClear(self.exp), AssignmentPrecedence.SW_ONWRITE) + self.add_sw_conditional(sw_onwrite.WriteOneSet(self.exp), AssignmentPrecedence.SW_ONWRITE) + + self.add_sw_conditional(sw_singlepulse.Singlepulse(self.exp), AssignmentPrecedence.SW_SINGLEPULSE) + + self.add_hw_conditional(hw_interrupts_with_write.PosedgeStickybitWE(self.exp), AssignmentPrecedence.HW_WRITE) + self.add_hw_conditional(hw_interrupts_with_write.PosedgeStickybitWEL(self.exp), AssignmentPrecedence.HW_WRITE) + self.add_hw_conditional(hw_interrupts_with_write.NegedgeStickybitWE(self.exp), AssignmentPrecedence.HW_WRITE) + self.add_hw_conditional(hw_interrupts_with_write.NegedgeStickybitWEL(self.exp), AssignmentPrecedence.HW_WRITE) + self.add_hw_conditional(hw_interrupts_with_write.BothedgeStickybitWE(self.exp), AssignmentPrecedence.HW_WRITE) + self.add_hw_conditional(hw_interrupts_with_write.BothedgeStickybitWEL(self.exp), AssignmentPrecedence.HW_WRITE) + self.add_hw_conditional(hw_interrupts_with_write.StickyWE(self.exp), AssignmentPrecedence.HW_WRITE) + self.add_hw_conditional(hw_interrupts_with_write.StickyWEL(self.exp), AssignmentPrecedence.HW_WRITE) + self.add_hw_conditional(hw_interrupts_with_write.StickybitWE(self.exp), AssignmentPrecedence.HW_WRITE) + self.add_hw_conditional(hw_interrupts_with_write.StickybitWEL(self.exp), AssignmentPrecedence.HW_WRITE) + self.add_hw_conditional(hw_interrupts.PosedgeStickybit(self.exp), AssignmentPrecedence.HW_WRITE) + self.add_hw_conditional(hw_interrupts.NegedgeStickybit(self.exp), AssignmentPrecedence.HW_WRITE) + self.add_hw_conditional(hw_interrupts.BothedgeStickybit(self.exp), AssignmentPrecedence.HW_WRITE) + self.add_hw_conditional(hw_interrupts.Sticky(self.exp), AssignmentPrecedence.HW_WRITE) + self.add_hw_conditional(hw_interrupts.Stickybit(self.exp), AssignmentPrecedence.HW_WRITE) + self.add_hw_conditional(hw_write.WEWrite(self.exp), AssignmentPrecedence.HW_WRITE) + self.add_hw_conditional(hw_write.WELWrite(self.exp), AssignmentPrecedence.HW_WRITE) + self.add_hw_conditional(hw_write.AlwaysWrite(self.exp), AssignmentPrecedence.HW_WRITE) + + self.add_hw_conditional(hw_set_clr.HWClear(self.exp), AssignmentPrecedence.HWCLR) + + self.add_hw_conditional(hw_set_clr.HWSet(self.exp), AssignmentPrecedence.HWSET) + + + def _get_X_conditionals(self, conditionals: 'Dict[int, List[NextStateConditional]]', field: 'FieldNode') -> 'List[NextStateConditional]': + result = [] + precedences = sorted(conditionals.keys(), reverse=True) + for precedence in precedences: + for conditional in conditionals[precedence]: + if conditional.is_match(field): + result.append(conditional) + break + return result + + + def get_conditionals(self, field: 'FieldNode') -> 'List[NextStateConditional]': + """ + Get a list of NextStateConditional objects that apply to the given field. + + The returned list is sorted in priority order - the conditional with highest + precedence is first in the list. + """ + sw_precedence = field.get_property('precedence') == PrecedenceType.sw + result = [] + + if sw_precedence: + result.extend(self._get_X_conditionals(self._sw_conditionals, field)) + + result.extend(self._get_X_conditionals(self._hw_conditionals, field)) + + if not sw_precedence: + result.extend(self._get_X_conditionals(self._sw_conditionals, field)) + + return result diff --git a/src/peakrdl_regblock/field_logic/bases.py b/src/peakrdl_regblock/field_logic/bases.py new file mode 100644 index 0000000..2a13676 --- /dev/null +++ b/src/peakrdl_regblock/field_logic/bases.py @@ -0,0 +1,114 @@ +from typing import TYPE_CHECKING, List +import enum + +from ..utils import get_indexed_path + +if TYPE_CHECKING: + from systemrdl.node import FieldNode + + from ..exporter import RegblockExporter + +class AssignmentPrecedence(enum.IntEnum): + """ + Enumeration of standard assignment precedence groups. + Each value represents the precedence of a single conditional assignment + category that determines a field's next state. + + Higher value denotes higher precedence + + Important: If inserting custom intermediate assignment rules, do not rely on the absolute + value of the enumeration. Insert your rules relative to an existing precedence: + FieldBuilder.add_hw_conditional(MyConditional, HW_WE + 1) + """ + + # Software access assignment groups + SW_ONREAD = 5000 + SW_ONWRITE = 4000 + SW_SINGLEPULSE = 3000 + + # Hardware access assignment groups + HW_WRITE = 3000 + HWSET = 2000 + HWCLR = 1000 + COUNTER_INCR_DECR = 0 + + + + +class SVLogic: + """ + Represents a SystemVerilog logic signal + """ + def __init__(self, name: str, width: int, default_assignment: str) -> None: + self.name = name + self.width = width + self.default_assignment = default_assignment + + def __eq__(self, o: object) -> bool: + if not isinstance(o, SVLogic): + return False + + return ( + o.name == self.name + and o.width == self.width + and o.default_assignment == self.default_assignment + ) + + +class NextStateConditional: + """ + Describes a single conditional action that determines the next state of a field + Provides information to generate the following content: + if() begin + + end + """ + + # Optional comment to emit next to the conditional + comment = "" + + def __init__(self, exp:'RegblockExporter'): + self.exp = exp + + def is_match(self, field: 'FieldNode') -> bool: + """ + Returns True if this conditional is relevant to the field. If so, + it instructs the FieldBuilder that Verilog for this conditional shall + be emitted + """ + raise NotImplementedError + + def get_field_path(self, field:'FieldNode') -> str: + return get_indexed_path(self.exp.ds.top_node, field) + + def get_predicate(self, field: 'FieldNode') -> str: + """ + Returns the rendered conditional text + """ + raise NotImplementedError + + + def get_assignments(self, field: 'FieldNode') -> List[str]: + """ + Returns a list of rendered assignment strings + This will basically always be two: + .next = + .load_next = '1; + """ + raise NotImplementedError + + def get_extra_combo_signals(self, field: 'FieldNode') -> List[SVLogic]: + """ + Return any additional combinational signals that this conditional + will assign if present. + """ + return [] + +class NextStateUnconditional(NextStateConditional): + """ + Use this class if predicate can never evaluate to false. + This will be generated as an 'else' clause, or a direct assignment + """ + + # Explanation text for use in error message about conflicts + unconditional_explanation = "" diff --git a/src/peakrdl_regblock/field_logic/generators.py b/src/peakrdl_regblock/field_logic/generators.py new file mode 100644 index 0000000..04e474f --- /dev/null +++ b/src/peakrdl_regblock/field_logic/generators.py @@ -0,0 +1,393 @@ +from typing import TYPE_CHECKING, List, Optional + +from collections import OrderedDict + +from systemrdl.walker import WalkerAction +from systemrdl.node import RegNode, RegfileNode, MemNode, AddrmapNode + +from ..struct_generator import RDLStructGenerator +from ..forloop_generator import RDLForLoopGenerator +from ..utils import get_indexed_path, clog2 +from ..identifier_filter import kw_filter as kwf +from .bases import NextStateUnconditional + +if TYPE_CHECKING: + from . import FieldLogic + from systemrdl.node import FieldNode, AddressableNode + from .bases import SVLogic + +class CombinationalStructGenerator(RDLStructGenerator): + + def __init__(self, field_logic: 'FieldLogic'): + super().__init__() + self.field_logic = field_logic + + def enter_AddressableComponent(self, node: 'AddressableNode') -> Optional[WalkerAction]: + super().enter_AddressableComponent(node) + + if node.external: + return WalkerAction.SkipDescendants + return WalkerAction.Continue + + def enter_Field(self, node: 'FieldNode') -> None: + # If a field doesn't implement storage, it is not relevant here + if not node.implements_storage: + return + + # collect any extra combo signals that this field requires + extra_combo_signals = OrderedDict() # type: OrderedDict[str, SVLogic] + for conditional in self.field_logic.get_conditionals(node): + for signal in conditional.get_extra_combo_signals(node): + if signal.name in extra_combo_signals: + # Assert that subsequent declarations of the same signal + # are identical + assert signal == extra_combo_signals[signal.name] + else: + extra_combo_signals[signal.name] = signal + + self.push_struct(kwf(node.inst_name)) + self.add_member("next", node.width) + self.add_member("load_next") + for signal in extra_combo_signals.values(): + self.add_member(signal.name, signal.width) + if node.is_up_counter: + self.add_up_counter_members(node) + if node.is_down_counter: + self.add_down_counter_members(node) + if node.get_property('paritycheck'): + self.add_member("parity_error") + self.pop_struct() + + def add_up_counter_members(self, node: 'FieldNode') -> None: + self.add_member('incrthreshold') + if self.field_logic.counter_incrsaturates(node): + self.add_member('incrsaturate') + else: + self.add_member('overflow') + + def add_down_counter_members(self, node: 'FieldNode') -> None: + self.add_member('decrthreshold') + if self.field_logic.counter_decrsaturates(node): + self.add_member('decrsaturate') + else: + self.add_member('underflow') + + +class FieldStorageStructGenerator(RDLStructGenerator): + + def __init__(self, field_logic: 'FieldLogic') -> None: + super().__init__() + self.field_logic = field_logic + + def enter_AddressableComponent(self, node: 'AddressableNode') -> Optional[WalkerAction]: + super().enter_AddressableComponent(node) + + if node.external: + return WalkerAction.SkipDescendants + return WalkerAction.Continue + + def enter_Field(self, node: 'FieldNode') -> None: + self.push_struct(kwf(node.inst_name)) + + if node.implements_storage: + self.add_member("value", node.width) + if node.get_property('paritycheck'): + self.add_member("parity") + + if self.field_logic.has_next_q(node): + self.add_member("next_q", node.width) + + self.pop_struct() + + +class FieldLogicGenerator(RDLForLoopGenerator): + i_type = "genvar" + def __init__(self, field_logic: 'FieldLogic') -> None: + super().__init__() + self.field_logic = field_logic + self.exp = field_logic.exp + self.ds = self.exp.ds + self.field_storage_template = self.exp.jj_env.get_template( + "field_logic/templates/field_storage.sv" + ) + self.external_reg_template = self.exp.jj_env.get_template( + "field_logic/templates/external_reg.sv" + ) + self.external_block_template = self.exp.jj_env.get_template( + "field_logic/templates/external_block.sv" + ) + self.intr_fields = [] # type: List[FieldNode] + self.halt_fields = [] # type: List[FieldNode] + self.msg = self.ds.top_node.env.msg + + + def enter_AddressableComponent(self, node: 'AddressableNode') -> Optional[WalkerAction]: + super().enter_AddressableComponent(node) + + if node.external and not isinstance(node, RegNode): + # Is an external block + self.assign_external_block_outputs(node) + + # Do not recurse + return WalkerAction.SkipDescendants + + return WalkerAction.Continue + + def enter_Reg(self, node: 'RegNode') -> Optional[WalkerAction]: + self.intr_fields = [] + self.halt_fields = [] + + if node.external: + self.assign_external_reg_outputs(node) + # Do not recurse to fields + return WalkerAction.SkipDescendants + + return WalkerAction.Continue + + + def enter_Field(self, node: 'FieldNode') -> None: + if node.implements_storage: + self.generate_field_storage(node) + + self.assign_field_outputs(node) + + if node.get_property('intr'): + self.intr_fields.append(node) + if node.get_property('haltenable') or node.get_property('haltmask'): + self.halt_fields.append(node) + + + def exit_Reg(self, node: 'RegNode') -> None: + # Assign register's intr output + if self.intr_fields: + strs = [] + for field in self.intr_fields: + enable = field.get_property('enable') + mask = field.get_property('mask') + F = self.exp.dereferencer.get_value(field) + if enable: + E = self.exp.dereferencer.get_value(enable) + s = f"|({F} & {E})" + elif mask: + M = self.exp.dereferencer.get_value(mask) + s = f"|({F} & ~{M})" + else: + s = f"|{F}" + strs.append(s) + + self.add_content( + f"assign {self.exp.hwif.get_implied_prop_output_identifier(node, 'intr')} =" + ) + self.add_content( + " " + + "\n || ".join(strs) + + ";" + ) + + # Assign register's halt output + if self.halt_fields: + strs = [] + for field in self.halt_fields: + enable = field.get_property('haltenable') + mask = field.get_property('haltmask') + F = self.exp.dereferencer.get_value(field) + if enable: + E = self.exp.dereferencer.get_value(enable) + s = f"|({F} & {E})" + elif mask: + M = self.exp.dereferencer.get_value(mask) + s = f"|({F} & ~{M})" + else: + s = f"|{F}" + strs.append(s) + + self.add_content( + f"assign {self.exp.hwif.get_implied_prop_output_identifier(node, 'halt')} =" + ) + self.add_content( + " " + + "\n || ".join(strs) + + ";" + ) + + + def generate_field_storage(self, node: 'FieldNode') -> None: + conditionals = self.field_logic.get_conditionals(node) + extra_combo_signals = OrderedDict() + unconditional: Optional[NextStateUnconditional] = None + new_conditionals = [] + for conditional in conditionals: + for signal in conditional.get_extra_combo_signals(node): + extra_combo_signals[signal.name] = signal + + if isinstance(conditional, NextStateUnconditional): + if unconditional is not None: + # Too inconvenient to validate this early. Easier to validate here in-place generically + self.msg.fatal( + "Field has multiple conflicting properties that unconditionally set its state:\n" + f" * {conditional.unconditional_explanation}\n" + f" * {unconditional.unconditional_explanation}", + node.inst.inst_src_ref + ) + unconditional = conditional + else: + new_conditionals.append(conditional) + conditionals = new_conditionals + + resetsignal = node.get_property('resetsignal') + + reset_value = node.get_property('reset') + if reset_value is not None: + reset_value_str = self.exp.dereferencer.get_value(reset_value, node.width) + else: + # 5.9.1-g: If no reset value given, the field is not reset, even if it has a resetsignal. + reset_value_str = None + resetsignal = None + + context = { + 'node': node, + 'reset': reset_value_str, + 'field_logic': self.field_logic, + 'extra_combo_signals': extra_combo_signals, + 'conditionals': conditionals, + 'unconditional': unconditional, + 'resetsignal': resetsignal, + 'get_always_ff_event': self.exp.dereferencer.get_always_ff_event, + 'get_value': self.exp.dereferencer.get_value, + 'get_resetsignal': self.exp.dereferencer.get_resetsignal, + 'get_input_identifier': self.exp.hwif.get_input_identifier, + 'ds': self.ds, + } + self.add_content(self.field_storage_template.render(context)) + + + def assign_field_outputs(self, node: 'FieldNode') -> None: + # Field value output + if self.exp.hwif.has_value_output(node): + output_identifier = self.exp.hwif.get_output_identifier(node) + value = self.exp.dereferencer.get_value(node) + self.add_content( + f"assign {output_identifier} = {value};" + ) + + # Inferred logical reduction outputs + if node.get_property('anded'): + output_identifier = self.exp.hwif.get_implied_prop_output_identifier(node, "anded") + value = self.exp.dereferencer.get_field_propref_value(node, "anded") + self.add_content( + f"assign {output_identifier} = {value};" + ) + if node.get_property('ored'): + output_identifier = self.exp.hwif.get_implied_prop_output_identifier(node, "ored") + value = self.exp.dereferencer.get_field_propref_value(node, "ored") + self.add_content( + f"assign {output_identifier} = {value};" + ) + if node.get_property('xored'): + output_identifier = self.exp.hwif.get_implied_prop_output_identifier(node, "xored") + value = self.exp.dereferencer.get_field_propref_value(node, "xored") + self.add_content( + f"assign {output_identifier} = {value};" + ) + + # Software access strobes + if node.get_property('swmod'): + output_identifier = self.exp.hwif.get_implied_prop_output_identifier(node, "swmod") + value = self.field_logic.get_swmod_identifier(node) + self.add_content( + f"assign {output_identifier} = {value};" + ) + if node.get_property('swacc'): + output_identifier = self.exp.hwif.get_implied_prop_output_identifier(node, "swacc") + value = self.field_logic.get_swacc_identifier(node) + self.add_content( + f"assign {output_identifier} = {value};" + ) + if node.get_property('rd_swacc'): + output_identifier = self.exp.hwif.get_implied_prop_output_identifier(node, "rd_swacc") + value = self.field_logic.get_rd_swacc_identifier(node) + self.add_content( + f"assign {output_identifier} = {value};" + ) + if node.get_property('wr_swacc'): + output_identifier = self.exp.hwif.get_implied_prop_output_identifier(node, "wr_swacc") + value = self.field_logic.get_wr_swacc_identifier(node) + self.add_content( + f"assign {output_identifier} = {value};" + ) + + # Counter thresholds + if node.get_property('incrthreshold') is not False: # (explicitly not False. Not 0) + output_identifier = self.exp.hwif.get_implied_prop_output_identifier(node, "incrthreshold") + value = self.field_logic.get_field_combo_identifier(node, 'incrthreshold') + self.add_content( + f"assign {output_identifier} = {value};" + ) + if node.get_property('decrthreshold') is not False: # (explicitly not False. Not 0) + output_identifier = self.exp.hwif.get_implied_prop_output_identifier(node, "decrthreshold") + value = self.field_logic.get_field_combo_identifier(node, 'decrthreshold') + self.add_content( + f"assign {output_identifier} = {value};" + ) + + # Counter events + if node.get_property('overflow'): + output_identifier = self.exp.hwif.get_implied_prop_output_identifier(node, "overflow") + value = self.field_logic.get_field_combo_identifier(node, 'overflow') + self.add_content( + f"assign {output_identifier} = {value};" + ) + if node.get_property('underflow'): + output_identifier = self.exp.hwif.get_implied_prop_output_identifier(node, "underflow") + value = self.field_logic.get_field_combo_identifier(node, 'underflow') + self.add_content( + f"assign {output_identifier} = {value};" + ) + + + def assign_external_reg_outputs(self, node: 'RegNode') -> None: + prefix = "hwif_out." + get_indexed_path(self.exp.ds.top_node, node) + strb = self.exp.dereferencer.get_access_strobe(node) + + width = min(self.exp.cpuif.data_width, node.get_property('regwidth')) + if width != self.exp.cpuif.data_width: + bslice = f"[{width - 1}:0]" + else: + bslice = "" + + context = { + "has_sw_writable": node.has_sw_writable, + "has_sw_readable": node.has_sw_readable, + "prefix": prefix, + "strb": strb, + "bslice": bslice, + "retime": self.ds.retime_external_reg, + 'get_always_ff_event': self.exp.dereferencer.get_always_ff_event, + "get_resetsignal": self.exp.dereferencer.get_resetsignal, + "resetsignal": self.exp.ds.top_node.cpuif_reset, + } + self.add_content(self.external_reg_template.render(context)) + + def assign_external_block_outputs(self, node: 'AddressableNode') -> None: + prefix = "hwif_out." + get_indexed_path(self.exp.ds.top_node, node) + strb = self.exp.dereferencer.get_external_block_access_strobe(node) + addr_width = clog2(node.size) + + retime = False + if isinstance(node, RegfileNode): + retime = self.ds.retime_external_regfile + elif isinstance(node, MemNode): + retime = self.ds.retime_external_mem + elif isinstance(node, AddrmapNode): + retime = self.ds.retime_external_addrmap + + context = { + "prefix": prefix, + "strb": strb, + "addr_width": addr_width, + "retime": retime, + 'get_always_ff_event': self.exp.dereferencer.get_always_ff_event, + "get_resetsignal": self.exp.dereferencer.get_resetsignal, + "resetsignal": self.exp.ds.top_node.cpuif_reset, + } + self.add_content(self.external_block_template.render(context)) diff --git a/src/peakrdl_regblock/field_logic/hw_interrupts.py b/src/peakrdl_regblock/field_logic/hw_interrupts.py new file mode 100644 index 0000000..dbebdd3 --- /dev/null +++ b/src/peakrdl_regblock/field_logic/hw_interrupts.py @@ -0,0 +1,162 @@ +from typing import TYPE_CHECKING, List + +from systemrdl.rdltypes import InterruptType + +from .bases import NextStateConditional + +if TYPE_CHECKING: + from systemrdl.node import FieldNode + + +class Sticky(NextStateConditional): + """ + Normal multi-bit sticky + """ + comment = "multi-bit sticky" + def is_match(self, field: 'FieldNode') -> bool: + return ( + field.is_hw_writable + and field.get_property('sticky') + ) + + def get_predicate(self, field: 'FieldNode') -> str: + I = self.exp.hwif.get_input_identifier(field) + R = self.exp.field_logic.get_storage_identifier(field) + return f"({R} == '0) && ({I} != '0)" + + def get_assignments(self, field: 'FieldNode') -> List[str]: + I = self.exp.hwif.get_input_identifier(field) + return [ + f"next_c = {I};", + "load_next_c = '1;", + ] + + +class Stickybit(NextStateConditional): + """ + Normal stickybit + """ + comment = "stickybit" + def is_match(self, field: 'FieldNode') -> bool: + return ( + field.is_hw_writable + and field.get_property('stickybit') + and field.get_property('intr type') in {None, InterruptType.level} + ) + + def get_predicate(self, field: 'FieldNode') -> str: + F = self.exp.hwif.get_input_identifier(field) + if field.width == 1: + return str(F) + else: + return f"{F} != '0" + + def get_assignments(self, field: 'FieldNode') -> List[str]: + if field.width == 1: + return [ + "next_c = '1;", + "load_next_c = '1;", + ] + else: + I = self.exp.hwif.get_input_identifier(field) + R = self.exp.field_logic.get_storage_identifier(field) + return [ + f"next_c = {R} | {I};", + "load_next_c = '1;", + ] + +class PosedgeStickybit(NextStateConditional): + """ + Positive edge stickybit + """ + comment = "posedge stickybit" + def is_match(self, field: 'FieldNode') -> bool: + return ( + field.is_hw_writable + and field.get_property('stickybit') + and field.get_property('intr type') == InterruptType.posedge + ) + + def get_predicate(self, field: 'FieldNode') -> str: + I = self.exp.hwif.get_input_identifier(field) + Iq = self.exp.field_logic.get_next_q_identifier(field) + return f"(~{Iq} & {I}) != '0" + + def get_assignments(self, field: 'FieldNode') -> List[str]: + if field.width == 1: + return [ + "next_c = '1;", + "load_next_c = '1;", + ] + else: + I = self.exp.hwif.get_input_identifier(field) + Iq = self.exp.field_logic.get_next_q_identifier(field) + R = self.exp.field_logic.get_storage_identifier(field) + return [ + f"next_c = {R} | (~{Iq} & {I});", + "load_next_c = '1;", + ] + +class NegedgeStickybit(NextStateConditional): + """ + Negative edge stickybit + """ + comment = "negedge stickybit" + def is_match(self, field: 'FieldNode') -> bool: + return ( + field.is_hw_writable + and field.get_property('stickybit') + and field.get_property('intr type') == InterruptType.negedge + ) + + def get_predicate(self, field: 'FieldNode') -> str: + I = self.exp.hwif.get_input_identifier(field) + Iq = self.exp.field_logic.get_next_q_identifier(field) + return f"({Iq} & ~{I}) != '0" + + def get_assignments(self, field: 'FieldNode') -> List[str]: + if field.width == 1: + return [ + "next_c = '1;", + "load_next_c = '1;", + ] + else: + I = self.exp.hwif.get_input_identifier(field) + Iq = self.exp.field_logic.get_next_q_identifier(field) + R = self.exp.field_logic.get_storage_identifier(field) + return [ + f"next_c = {R} | ({Iq} & ~{I});", + "load_next_c = '1;", + ] + +class BothedgeStickybit(NextStateConditional): + """ + edge-sensitive stickybit + """ + comment = "bothedge stickybit" + def is_match(self, field: 'FieldNode') -> bool: + return ( + field.is_hw_writable + and field.get_property('stickybit') + and field.get_property('intr type') == InterruptType.bothedge + ) + + def get_predicate(self, field: 'FieldNode') -> str: + I = self.exp.hwif.get_input_identifier(field) + Iq = self.exp.field_logic.get_next_q_identifier(field) + return f"{Iq} != {I}" + + def get_assignments(self, field: 'FieldNode') -> List[str]: + if field.width == 1: + return [ + "next_c = '1;", + "load_next_c = '1;", + ] + else: + I = self.exp.hwif.get_input_identifier(field) + Iq = self.exp.field_logic.get_next_q_identifier(field) + R = self.exp.field_logic.get_storage_identifier(field) + return [ + f"next_c = {R} | ({Iq} ^ {I});", + "load_next_c = '1;", + ] diff --git a/src/peakrdl_regblock/field_logic/hw_interrupts_with_write.py b/src/peakrdl_regblock/field_logic/hw_interrupts_with_write.py new file mode 100644 index 0000000..7545533 --- /dev/null +++ b/src/peakrdl_regblock/field_logic/hw_interrupts_with_write.py @@ -0,0 +1,187 @@ +from typing import List, TYPE_CHECKING + +from .hw_interrupts import ( + Sticky, Stickybit, + PosedgeStickybit, NegedgeStickybit, BothedgeStickybit +) +from .hw_write import WEWrite, WELWrite + +if TYPE_CHECKING: + from systemrdl.node import FieldNode + + +class StickyWE(Sticky, WEWrite): + """ + Normal multi-bit sticky with write enable + """ + comment = "multi-bit sticky with WE" + def is_match(self, field: 'FieldNode') -> bool: + return ( + Sticky.is_match(self, field) + and WEWrite.is_match(self, field) + ) + + def get_predicate(self, field: 'FieldNode') -> str: + BASE = Sticky.get_predicate(self, field) + WE = WEWrite.get_predicate(self, field) + return f"{BASE} && {WE}" + + def get_assignments(self, field: 'FieldNode') -> List[str]: + return Sticky.get_assignments(self, field) + +class StickyWEL(Sticky, WELWrite): + """ + Normal multi-bit sticky with write enable low + """ + comment = "multi-bit sticky with WEL" + def is_match(self, field: 'FieldNode') -> bool: + return ( + Sticky.is_match(self, field) + and WELWrite.is_match(self, field) + ) + + def get_predicate(self, field: 'FieldNode') -> str: + BASE = Sticky.get_predicate(self, field) + WEL = WELWrite.get_predicate(self, field) + return f"{BASE} && {WEL}" + + def get_assignments(self, field: 'FieldNode') -> List[str]: + return Sticky.get_assignments(self, field) + +class StickybitWE(Stickybit, WEWrite): + """ + Normal stickybiti with write enable + """ + comment = "stickybit with WE" + def is_match(self, field: 'FieldNode') -> bool: + return ( + Stickybit.is_match(self, field) + and WEWrite.is_match(self, field) + ) + + def get_predicate(self, field: 'FieldNode') -> str: + BASE = Stickybit.get_predicate(self, field) + WE = WEWrite.get_predicate(self, field) + return f"{BASE} && {WE}" + + def get_assignments(self, field: 'FieldNode') -> List[str]: + return Stickybit.get_assignments(self, field) + +class StickybitWEL(Stickybit, WELWrite): + """ + Normal stickybiti with write enable low + """ + comment = "stickybit with WEL" + def is_match(self, field: 'FieldNode') -> bool: + return Stickybit.is_match(self, field) \ + and WELWrite.is_match(self, field) + + def get_predicate(self, field: 'FieldNode') -> str: + BASE = Stickybit.get_predicate(self, field) + WEL = WELWrite.get_predicate(self, field) + return f"{BASE} && {WEL}" + + def get_assignments(self, field: 'FieldNode') -> List[str]: + return Stickybit.get_assignments(self, field) + +class PosedgeStickybitWE(PosedgeStickybit, WEWrite): + """ + Positive edge stickybit with write enable + """ + comment = "posedge stickybit with WE" + def is_match(self, field: 'FieldNode') -> bool: + return PosedgeStickybit.is_match(self, field) \ + and WEWrite.is_match(self, field) + + def get_predicate(self, field: 'FieldNode') -> str: + BASE = PosedgeStickybit.get_predicate(self, field) + WE = WEWrite.get_predicate(self, field) + return f"{BASE} && {WE}" + + def get_assignments(self, field: 'FieldNode') -> List[str]: + return PosedgeStickybit.get_assignments(self, field) + +class PosedgeStickybitWEL(PosedgeStickybit, WELWrite): + """ + Positive edge stickybit with write enable low + """ + comment = "posedge stickybit with WEL" + def is_match(self, field: 'FieldNode') -> bool: + return PosedgeStickybit.is_match(self, field) \ + and WELWrite.is_match(self, field) + + def get_predicate(self, field: 'FieldNode') -> str: + BASE = PosedgeStickybit.get_predicate(self, field) + WEL = WELWrite.get_predicate(self, field) + return f"{BASE} && {WEL}" + + def get_assignments(self, field: 'FieldNode') -> List[str]: + return PosedgeStickybit.get_assignments(self, field) + +class NegedgeStickybitWE(NegedgeStickybit, WEWrite): + """ + Negative edge stickybit with write enable + """ + comment = "negedge stickybit with WE" + def is_match(self, field: 'FieldNode') -> bool: + return NegedgeStickybit.is_match(self, field) \ + and WEWrite.is_match(self, field) + + def get_predicate(self, field: 'FieldNode') -> str: + BASE = NegedgeStickybit.get_predicate(self, field) + WE = WEWrite.get_predicate(self, field) + return f"{BASE} && {WE}" + + def get_assignments(self, field: 'FieldNode') -> List[str]: + return NegedgeStickybit.get_assignments(self, field) + +class NegedgeStickybitWEL(NegedgeStickybit, WELWrite): + """ + Negative edge stickybit with write enable low + """ + comment = "negedge stickybit with WEL" + def is_match(self, field: 'FieldNode') -> bool: + return NegedgeStickybit.is_match(self, field) \ + and WELWrite.is_match(self, field) + + def get_predicate(self, field: 'FieldNode') -> str: + BASE = NegedgeStickybit.get_predicate(self, field) + WEL = WELWrite.get_predicate(self, field) + return f"{BASE} && {WEL}" + + def get_assignments(self, field: 'FieldNode') -> List[str]: + return NegedgeStickybit.get_assignments(self, field) + +class BothedgeStickybitWE(BothedgeStickybit, WEWrite): + """ + edge-sensitive stickybit with write enable + """ + comment = "bothedge stickybit with WE" + def is_match(self, field: 'FieldNode') -> bool: + return BothedgeStickybit.is_match(self, field) \ + and WEWrite.is_match(self, field) + + def get_predicate(self, field: 'FieldNode') -> str: + BASE = BothedgeStickybit.get_predicate(self, field) + WE = WEWrite.get_predicate(self, field) + return f"{BASE} && {WE}" + + def get_assignments(self, field: 'FieldNode') -> List[str]: + return BothedgeStickybit.get_assignments(self, field) + +class BothedgeStickybitWEL(BothedgeStickybit, WELWrite): + """ + edge-sensitive stickybit with write enable low + """ + comment = "bothedge stickybit with WEL" + def is_match(self, field: 'FieldNode') -> bool: + return BothedgeStickybit.is_match(self, field) \ + and WELWrite.is_match(self, field) + + def get_predicate(self, field: 'FieldNode') -> str: + BASE = BothedgeStickybit.get_predicate(self, field) + WEL = WELWrite.get_predicate(self, field) + return f"{BASE} && {WEL}" + + def get_assignments(self, field: 'FieldNode') -> List[str]: + return BothedgeStickybit.get_assignments(self, field) diff --git a/src/peakrdl_regblock/field_logic/hw_set_clr.py b/src/peakrdl_regblock/field_logic/hw_set_clr.py new file mode 100644 index 0000000..982f1b3 --- /dev/null +++ b/src/peakrdl_regblock/field_logic/hw_set_clr.py @@ -0,0 +1,72 @@ +from typing import TYPE_CHECKING, List + +from .bases import NextStateConditional + +if TYPE_CHECKING: + from systemrdl.node import FieldNode + + +class HWSet(NextStateConditional): + comment = "HW Set" + def is_match(self, field: 'FieldNode') -> bool: + return bool(field.get_property('hwset')) + + def get_predicate(self, field: 'FieldNode') -> str: + prop = field.get_property('hwset') + if isinstance(prop, bool): + identifier = self.exp.hwif.get_implied_prop_input_identifier(field, "hwset") + else: + # signal or field + identifier = str(self.exp.dereferencer.get_value(prop)) + return identifier + + def get_assignments(self, field: 'FieldNode') -> List[str]: + hwmask = field.get_property('hwmask') + hwenable = field.get_property('hwenable') + R = self.exp.field_logic.get_storage_identifier(field) + if hwmask is not None: + M = self.exp.dereferencer.get_value(hwmask) + next_val = f"{R} | ~{M}" + elif hwenable is not None: + E = self.exp.dereferencer.get_value(hwenable) + next_val = f"{R} | {E}" + else: + next_val = "'1" + + return [ + f"next_c = {next_val};", + "load_next_c = '1;", + ] + + +class HWClear(NextStateConditional): + comment = "HW Clear" + def is_match(self, field: 'FieldNode') -> bool: + return bool(field.get_property('hwclr')) + + def get_predicate(self, field: 'FieldNode') -> str: + prop = field.get_property('hwclr') + if isinstance(prop, bool): + identifier = self.exp.hwif.get_implied_prop_input_identifier(field, "hwclr") + else: + # signal or field + identifier = str(self.exp.dereferencer.get_value(prop)) + return identifier + + def get_assignments(self, field: 'FieldNode') -> List[str]: + hwmask = field.get_property('hwmask') + hwenable = field.get_property('hwenable') + R = self.exp.field_logic.get_storage_identifier(field) + if hwmask is not None: + M = self.exp.dereferencer.get_value(hwmask) + next_val = f"{R} & {M}" + elif hwenable is not None: + E = self.exp.dereferencer.get_value(hwenable) + next_val = f"{R} & ~{E}" + else: + next_val = "'0" + + return [ + f"next_c = {next_val};", + "load_next_c = '1;", + ] diff --git a/src/peakrdl_regblock/field_logic/hw_write.py b/src/peakrdl_regblock/field_logic/hw_write.py new file mode 100644 index 0000000..5a629ce --- /dev/null +++ b/src/peakrdl_regblock/field_logic/hw_write.py @@ -0,0 +1,95 @@ +from typing import TYPE_CHECKING, List + +from .bases import NextStateConditional, NextStateUnconditional + +if TYPE_CHECKING: + from systemrdl.node import FieldNode + + +class AlwaysWrite(NextStateUnconditional): + """ + hw writable, without any qualifying we/wel + """ + comment = "HW Write" + unconditional_explanation = "A hardware-writable field without a write-enable (we/wel) will always update the field value" + + def is_match(self, field: 'FieldNode') -> bool: + return ( + field.is_hw_writable + and not field.get_property('we') + and not field.get_property('wel') + ) + + def get_assignments(self, field: 'FieldNode') -> List[str]: + hwmask = field.get_property('hwmask') + hwenable = field.get_property('hwenable') + I = str(self.exp.hwif.get_input_identifier(field)) + R = self.exp.field_logic.get_storage_identifier(field) + if hwmask is not None: + M = self.exp.dereferencer.get_value(hwmask) + next_val = f"{I} & ~{M} | {R} & {M}" + elif hwenable is not None: + E = self.exp.dereferencer.get_value(hwenable) + next_val = f"{I} & {E} | {R} & ~{E}" + else: + next_val = I + + return [ + f"next_c = {next_val};", + "load_next_c = '1;", + ] + + +class _QualifiedWrite(NextStateConditional): + def get_assignments(self, field: 'FieldNode') -> List[str]: + hwmask = field.get_property('hwmask') + hwenable = field.get_property('hwenable') + I = str(self.exp.hwif.get_input_identifier(field)) + R = self.exp.field_logic.get_storage_identifier(field) + if hwmask is not None: + M = self.exp.dereferencer.get_value(hwmask) + next_val = f"{I} & ~{M} | {R} & {M}" + elif hwenable is not None: + E = self.exp.dereferencer.get_value(hwenable) + next_val = f"{I} & {E} | {R} & ~{E}" + else: + next_val = I + + return [ + f"next_c = {next_val};", + "load_next_c = '1;", + ] + +class WEWrite(_QualifiedWrite): + comment = "HW Write - we" + def is_match(self, field: 'FieldNode') -> bool: + return ( + field.is_hw_writable + and bool(field.get_property('we')) + ) + + def get_predicate(self, field: 'FieldNode') -> str: + prop = field.get_property('we') + if isinstance(prop, bool): + identifier = self.exp.hwif.get_implied_prop_input_identifier(field, "we") + else: + # signal or field + identifier = str(self.exp.dereferencer.get_value(prop)) + return identifier + +class WELWrite(_QualifiedWrite): + comment = "HW Write - wel" + def is_match(self, field: 'FieldNode') -> bool: + return ( + field.is_hw_writable + and bool(field.get_property('wel')) + ) + + def get_predicate(self, field: 'FieldNode') -> str: + prop = field.get_property('wel') + if isinstance(prop, bool): + identifier = self.exp.hwif.get_implied_prop_input_identifier(field, "wel") + else: + # signal or field + identifier = str(self.exp.dereferencer.get_value(prop)) + return f"!{identifier}" diff --git a/src/peakrdl_regblock/field_logic/sw_onread.py b/src/peakrdl_regblock/field_logic/sw_onread.py new file mode 100644 index 0000000..ba998c7 --- /dev/null +++ b/src/peakrdl_regblock/field_logic/sw_onread.py @@ -0,0 +1,45 @@ +from typing import TYPE_CHECKING, List + +from systemrdl.rdltypes import OnReadType + +from .bases import NextStateConditional + +if TYPE_CHECKING: + from systemrdl.node import FieldNode + +class _OnRead(NextStateConditional): + onreadtype = None # type: OnReadType + def is_match(self, field: 'FieldNode') -> bool: + return field.get_property('onread') == self.onreadtype + + def get_predicate(self, field: 'FieldNode') -> str: + if field.parent.get_property('buffer_reads'): + # Is buffered read. Use alternate strobe + rstrb = self.exp.read_buffering.get_trigger(field.parent) + return rstrb + else: + # is regular register + strb = self.exp.dereferencer.get_access_strobe(field) + return f"{strb} && !decoded_req_is_wr" + + +class ClearOnRead(_OnRead): + comment = "SW clear on read" + onreadtype = OnReadType.rclr + + def get_assignments(self, field: 'FieldNode') -> List[str]: + return [ + "next_c = '0;", + "load_next_c = '1;", + ] + + +class SetOnRead(_OnRead): + comment = "SW set on read" + onreadtype = OnReadType.rset + + def get_assignments(self, field: 'FieldNode') -> List[str]: + return [ + "next_c = '1;", + "load_next_c = '1;", + ] diff --git a/src/peakrdl_regblock/field_logic/sw_onwrite.py b/src/peakrdl_regblock/field_logic/sw_onwrite.py new file mode 100644 index 0000000..7d11945 --- /dev/null +++ b/src/peakrdl_regblock/field_logic/sw_onwrite.py @@ -0,0 +1,129 @@ +from typing import TYPE_CHECKING, List, Optional + +from systemrdl.rdltypes import OnWriteType + +from .bases import NextStateConditional + +if TYPE_CHECKING: + from systemrdl.node import FieldNode + +# TODO: implement sw=w1 "write once" fields + +class _OnWrite(NextStateConditional): + onwritetype: Optional[OnWriteType] = None + def is_match(self, field: 'FieldNode') -> bool: + return field.is_sw_writable and field.get_property('onwrite') == self.onwritetype + + def get_predicate(self, field: 'FieldNode') -> str: + if field.parent.get_property('buffer_writes'): + # Is buffered write. Use alternate strobe + wstrb = self.exp.write_buffering.get_write_strobe(field) + + if field.get_property('swwe') or field.get_property('swwel'): + # dereferencer will wrap swwel complement if necessary + qualifier = self.exp.dereferencer.get_field_propref_value(field, 'swwe') + return f"{wstrb} && {qualifier}" + + return wstrb + else: + # is regular register + strb = self.exp.dereferencer.get_access_strobe(field) + + if field.get_property('swwe') or field.get_property('swwel'): + # dereferencer will wrap swwel complement if necessary + qualifier = self.exp.dereferencer.get_field_propref_value(field, 'swwe') + return f"{strb} && decoded_req_is_wr && {qualifier}" + + return f"{strb} && decoded_req_is_wr" + + def get_assignments(self, field: 'FieldNode') -> List[str]: + accesswidth = field.parent.get_property("accesswidth") + + # Due to 10.6.1-f, it is impossible for a field with an onwrite action to + # be split across subwords. + # Therefore it is ok to get the subword idx from only one of the bit offsets + sidx = field.low // accesswidth + + # field does not get split between subwords + R = self.exp.field_logic.get_storage_identifier(field) + D = self.exp.field_logic.get_wr_data(field, sidx) + S = self.exp.field_logic.get_wr_biten(field, sidx) + lines = [ + f"next_c = {self.get_onwrite_rhs(R, D, S)};", + "load_next_c = '1;", + ] + return lines + + def get_onwrite_rhs(self, reg: str, data: str, strb: str) -> str: + raise NotImplementedError + + +#------------------------------------------------------------------------------- +class WriteOneSet(_OnWrite): + comment = "SW write 1 set" + onwritetype = OnWriteType.woset + + def get_onwrite_rhs(self, reg: str, data: str, strb: str) -> str: + return f"{reg} | ({data} & {strb})" + +class WriteOneClear(_OnWrite): + comment = "SW write 1 clear" + onwritetype = OnWriteType.woclr + + def get_onwrite_rhs(self, reg: str, data: str, strb: str) -> str: + return f"{reg} & ~({data} & {strb})" + +class WriteOneToggle(_OnWrite): + comment = "SW write 1 toggle" + onwritetype = OnWriteType.wot + + def get_onwrite_rhs(self, reg: str, data: str, strb: str) -> str: + return f"{reg} ^ ({data} & {strb})" + +class WriteZeroSet(_OnWrite): + comment = "SW write 0 set" + onwritetype = OnWriteType.wzs + + def get_onwrite_rhs(self, reg: str, data: str, strb: str) -> str: + return f"{reg} | (~{data} & {strb})" + +class WriteZeroClear(_OnWrite): + comment = "SW write 0 clear" + onwritetype = OnWriteType.wzc + + def get_onwrite_rhs(self, reg: str, data: str, strb: str) -> str: + return f"{reg} & ({data} | ~{strb})" + +class WriteZeroToggle(_OnWrite): + comment = "SW write 0 toggle" + onwritetype = OnWriteType.wzt + + def get_onwrite_rhs(self, reg: str, data: str, strb: str) -> str: + return f"{reg} ^ (~{data} & {strb})" + +class WriteClear(_OnWrite): + comment = "SW write clear" + onwritetype = OnWriteType.wclr + + def get_assignments(self, field: 'FieldNode') -> List[str]: + return [ + "next_c = '0;", + "load_next_c = '1;", + ] + +class WriteSet(_OnWrite): + comment = "SW write set" + onwritetype = OnWriteType.wset + + def get_assignments(self, field: 'FieldNode') -> List[str]: + return [ + "next_c = '1;", + "load_next_c = '1;", + ] + +class Write(_OnWrite): + comment = "SW write" + onwritetype = None + + def get_onwrite_rhs(self, reg: str, data: str, strb: str) -> str: + return f"({reg} & ~{strb}) | ({data} & {strb})" diff --git a/src/peakrdl_regblock/field_logic/sw_singlepulse.py b/src/peakrdl_regblock/field_logic/sw_singlepulse.py new file mode 100644 index 0000000..9ed476b --- /dev/null +++ b/src/peakrdl_regblock/field_logic/sw_singlepulse.py @@ -0,0 +1,19 @@ +from typing import TYPE_CHECKING, List + +from .bases import NextStateUnconditional + +if TYPE_CHECKING: + from systemrdl.node import FieldNode + +class Singlepulse(NextStateUnconditional): + comment = "singlepulse clears back to 0" + unconditional_explanation = "The 'singlepulse' property unconditionally clears a field when not written" + + def is_match(self, field: 'FieldNode') -> bool: + return field.get_property('singlepulse') + + def get_assignments(self, field: 'FieldNode') -> List[str]: + return [ + "next_c = '0;", + "load_next_c = '1;", + ] diff --git a/src/peakrdl_regblock/field_logic/templates/counter_macros.sv b/src/peakrdl_regblock/field_logic/templates/counter_macros.sv new file mode 100644 index 0000000..2714bce --- /dev/null +++ b/src/peakrdl_regblock/field_logic/templates/counter_macros.sv @@ -0,0 +1,48 @@ +{% macro up_counter(field) -%} + if({{field_logic.get_counter_incr_strobe(node)}}) begin // increment + {%- if field_logic.counter_incrsaturates(node) %} + if((({{node.width+1}})'(next_c) + {{field_logic.get_counter_incrvalue(node)}}) > {{field_logic.get_counter_incrsaturate_value(node)}}) begin // up-counter saturated + next_c = {{field_logic.get_counter_incrsaturate_value(node)}}; + end else begin + next_c = next_c + {{field_logic.get_counter_incrvalue(node)}}; + end + {%- else %} + {{field_logic.get_field_combo_identifier(node, "overflow")}} = ((({{node.width+1}})'(next_c) + {{field_logic.get_counter_incrvalue(node)}}) > {{get_value(2**node.width - 1, node.width)}}); + next_c = next_c + {{field_logic.get_counter_incrvalue(node)}}; + {%- endif %} + load_next_c = '1; + {%- if not field_logic.counter_incrsaturates(node) %} + end else begin + {{field_logic.get_field_combo_identifier(node, "overflow")}} = '0; + {%- endif %} + end + {{field_logic.get_field_combo_identifier(node, "incrthreshold")}} = ({{field_logic.get_storage_identifier(node)}} >= {{field_logic.get_counter_incrthreshold_value(node)}}); + {%- if field_logic.counter_incrsaturates(node) %} + {{field_logic.get_field_combo_identifier(node, "incrsaturate")}} = ({{field_logic.get_storage_identifier(node)}} >= {{field_logic.get_counter_incrsaturate_value(node)}}); + {%- endif %} +{%- endmacro %} + + +{% macro down_counter(field) -%} + if({{field_logic.get_counter_decr_strobe(node)}}) begin // decrement + {%- if field_logic.counter_decrsaturates(node) %} + if(({{node.width+1}})'(next_c) < ({{field_logic.get_counter_decrvalue(node)}} + {{field_logic.get_counter_decrsaturate_value(node)}})) begin // down-counter saturated + next_c = {{field_logic.get_counter_decrsaturate_value(node)}}; + end else begin + next_c = next_c - {{field_logic.get_counter_decrvalue(node)}}; + end + {%- else %} + {{field_logic.get_field_combo_identifier(node, "underflow")}} = (next_c < ({{field_logic.get_counter_decrvalue(node)}})); + next_c = next_c - {{field_logic.get_counter_decrvalue(node)}}; + {%- endif %} + load_next_c = '1; + {%- if not field_logic.counter_decrsaturates(node) %} + end else begin + {{field_logic.get_field_combo_identifier(node, "underflow")}} = '0; + {%- endif %} + end + {{field_logic.get_field_combo_identifier(node, "decrthreshold")}} = ({{field_logic.get_storage_identifier(node)}} <= {{field_logic.get_counter_decrthreshold_value(node)}}); + {%- if field_logic.counter_decrsaturates(node) %} + {{field_logic.get_field_combo_identifier(node, "decrsaturate")}} = ({{field_logic.get_storage_identifier(node)}} <= {{field_logic.get_counter_decrsaturate_value(node)}}); + {%- endif %} +{%- endmacro %} diff --git a/src/peakrdl_regblock/field_logic/templates/external_block.sv b/src/peakrdl_regblock/field_logic/templates/external_block.sv new file mode 100644 index 0000000..5c5914a --- /dev/null +++ b/src/peakrdl_regblock/field_logic/templates/external_block.sv @@ -0,0 +1,31 @@ +{% if retime -%} + + +always_ff {{get_always_ff_event(resetsignal)}} begin + if({{get_resetsignal(resetsignal)}}) begin + {{prefix}}.req <= '0; + {{prefix}}.addr <= '0; + {{prefix}}.req_is_wr <= '0; + {{prefix}}.wr_data <= '0; + {{prefix}}.wr_biten <= '0; + end else begin + {{prefix}}.req <= {{strb}}; + {{prefix}}.addr <= decoded_addr[{{addr_width-1}}:0]; + {{prefix}}.req_is_wr <= decoded_req_is_wr; + {{prefix}}.wr_data <= decoded_wr_data; + {{prefix}}.wr_biten <= decoded_wr_biten; + end +end + + +{%- else -%} + + +assign {{prefix}}.req = {{strb}}; +assign {{prefix}}.addr = decoded_addr[{{addr_width-1}}:0]; +assign {{prefix}}.req_is_wr = decoded_req_is_wr; +assign {{prefix}}.wr_data = decoded_wr_data; +assign {{prefix}}.wr_biten = decoded_wr_biten; + + +{%- endif %} diff --git a/src/peakrdl_regblock/field_logic/templates/external_reg.sv b/src/peakrdl_regblock/field_logic/templates/external_reg.sv new file mode 100644 index 0000000..1a47515 --- /dev/null +++ b/src/peakrdl_regblock/field_logic/templates/external_reg.sv @@ -0,0 +1,46 @@ +{% if retime -%} + + +always_ff {{get_always_ff_event(resetsignal)}} begin + if({{get_resetsignal(resetsignal)}}) begin + {{prefix}}.req <= '0; + {{prefix}}.req_is_wr <= '0; + {%- if has_sw_writable %} + {{prefix}}.wr_data <= '0; + {{prefix}}.wr_biten <= '0; + {%- endif %} + end else begin + {%- if has_sw_readable and has_sw_writable %} + {{prefix}}.req <= {{strb}}; + {%- elif has_sw_readable and not has_sw_writable %} + {{prefix}}.req <= !decoded_req_is_wr ? {{strb}} : '0; + {%- elif not has_sw_readable and has_sw_writable %} + {{prefix}}.req <= decoded_req_is_wr ? {{strb}} : '0; + {%- endif %} + {{prefix}}.req_is_wr <= decoded_req_is_wr; + {%- if has_sw_writable %} + {{prefix}}.wr_data <= decoded_wr_data{{bslice}}; + {{prefix}}.wr_biten <= decoded_wr_biten{{bslice}}; + {%- endif %} + end +end + + +{%- else -%} + + +{%- if has_sw_readable and has_sw_writable %} +assign {{prefix}}.req = {{strb}}; +{%- elif has_sw_readable and not has_sw_writable %} +assign {{prefix}}.req = !decoded_req_is_wr ? {{strb}} : '0; +{%- elif not has_sw_readable and has_sw_writable %} +assign {{prefix}}.req = decoded_req_is_wr ? {{strb}} : '0; +{%- endif %} +assign {{prefix}}.req_is_wr = decoded_req_is_wr; +{%- if has_sw_writable %} +assign {{prefix}}.wr_data = decoded_wr_data{{bslice}}; +assign {{prefix}}.wr_biten = decoded_wr_biten{{bslice}}; +{%- endif %} + + +{%- endif %} diff --git a/src/peakrdl_regblock/field_logic/templates/field_storage.sv b/src/peakrdl_regblock/field_logic/templates/field_storage.sv new file mode 100644 index 0000000..4e2dca0 --- /dev/null +++ b/src/peakrdl_regblock/field_logic/templates/field_storage.sv @@ -0,0 +1,86 @@ +{%- import 'field_logic/templates/counter_macros.sv' as counter_macros with context -%} +// Field: {{node.get_path()}} +always_comb begin + automatic logic [{{node.width-1}}:0] next_c; + automatic logic load_next_c; + next_c = {{field_logic.get_storage_identifier(node)}}; + load_next_c = '0; + + {%- for signal in extra_combo_signals %} + {{field_logic.get_field_combo_identifier(node, signal.name)}} = {{signal.default_assignment}}; + {%- endfor %} + {% for conditional in conditionals %} + {%- if not loop.first %} else {% endif %}if({{conditional.get_predicate(node)}}) begin // {{conditional.comment}} + {%- for assignment in conditional.get_assignments(node) %} + {{assignment|indent}} + {%- endfor %} + end + {%- endfor %} + {%- if unconditional %} + {%- if conditionals %} else begin // {{unconditional.comment}} + {%- for assignment in unconditional.get_assignments(node) %} + {{assignment|indent}} + {%- endfor %} + end + {%- else %} + // {{unconditional.comment}} + {%- for assignment in unconditional.get_assignments(node) %} + {{assignment|indent}} + {%- endfor %} + {%- endif %} + {%- endif %} + + {%- if node.is_up_counter %} + {{counter_macros.up_counter(node)}} + {%- endif %} + + {%- if node.is_down_counter %} + {{counter_macros.down_counter(node)}} + {%- endif %} + {{field_logic.get_field_combo_identifier(node, "next")}} = next_c; + {{field_logic.get_field_combo_identifier(node, "load_next")}} = load_next_c; + + {%- if node.get_property('paritycheck') %} + {{field_logic.get_parity_error_identifier(node)}} = ({{field_logic.get_parity_identifier(node)}} != ^{{field_logic.get_storage_identifier(node)}}); + {%- endif %} +end + + + +{%- if reset is not none %} +always_ff {{get_always_ff_event(resetsignal)}} begin + if({{get_resetsignal(resetsignal)}}) begin + {{field_logic.get_storage_identifier(node)}} <= {{reset}}; + {%- if node.get_property('paritycheck') %} + {{field_logic.get_parity_identifier(node)}} <= ^{{reset}}; + {%- endif %} + {%- if field_logic.has_next_q(node) %} + {{field_logic.get_next_q_identifier(node)}} <= {{reset}}; + {%- endif %} + end else begin + if({{field_logic.get_field_combo_identifier(node, "load_next")}}) begin + {{field_logic.get_storage_identifier(node)}} <= {{field_logic.get_field_combo_identifier(node, "next")}}; + {%- if node.get_property('paritycheck') %} + {{field_logic.get_parity_identifier(node)}} <= ^{{field_logic.get_field_combo_identifier(node, "next")}}; + {%- endif %} + end + {%- if field_logic.has_next_q(node) %} + {{field_logic.get_next_q_identifier(node)}} <= {{get_input_identifier(node)}}; + {%- endif %} + end +end + + +{%- else %} +always_ff @(posedge clk) begin + if({{field_logic.get_field_combo_identifier(node, "load_next")}}) begin + {{field_logic.get_storage_identifier(node)}} <= {{field_logic.get_field_combo_identifier(node, "next")}}; + {%- if node.get_property('paritycheck') %} + {{field_logic.get_parity_identifier(node)}} <= ^{{field_logic.get_field_combo_identifier(node, "next")}}; + {%- endif %} + end + {%- if field_logic.has_next_q(node) %} + {{field_logic.get_next_q_identifier(node)}} <= {{get_input_identifier(node)}}; + {%- endif %} +end +{%- endif %} diff --git a/src/peakrdl_regblock/forloop_generator.py b/src/peakrdl_regblock/forloop_generator.py new file mode 100644 index 0000000..7ff0f4e --- /dev/null +++ b/src/peakrdl_regblock/forloop_generator.py @@ -0,0 +1,98 @@ +from typing import TYPE_CHECKING, Optional, List, Union +import textwrap + +from systemrdl.walker import RDLListener, RDLWalker, WalkerAction + +if TYPE_CHECKING: + from systemrdl.node import AddressableNode, Node + +class Body: + + def __init__(self) -> None: + self.children = [] # type: List[Union[str, Body]] + + def __str__(self) -> str: + s = '\n'.join((str(x) for x in self.children)) + return s + +class LoopBody(Body): + def __init__(self, dim: int, iterator: str, i_type: str) -> None: + super().__init__() + self.dim = dim + self.iterator = iterator + self.i_type = i_type + + def __str__(self) -> str: + s = super().__str__() + return ( + f"for({self.i_type} {self.iterator}=0; {self.iterator}<{self.dim}; {self.iterator}++) begin\n" + + textwrap.indent(s, " ") + + "\nend" + ) + + +class ForLoopGenerator: + i_type = "int" + loop_body_cls = LoopBody + + def __init__(self) -> None: + self._loop_level = 0 + self._stack = [] # type: List[Body] + + @property + def current_loop(self) -> Body: + return self._stack[-1] + + def push_loop(self, dim: int) -> None: + i = f"i{self._loop_level}" + b = self.loop_body_cls(dim, i, self.i_type) + self._stack.append(b) + self._loop_level += 1 + + def add_content(self, s: str) -> None: + self.current_loop.children.append(s) + + def pop_loop(self) -> None: + b = self._stack.pop() + + if b.children: + # Loop body is not empty. Attach it to the parent + self.current_loop.children.append(b) + self._loop_level -= 1 + + def start(self) -> None: + assert not self._stack + b = Body() + self._stack.append(b) + + def finish(self) -> Optional[str]: + b = self._stack.pop() + assert not self._stack + + if not b.children: + return None + return str(b) + +class RDLForLoopGenerator(ForLoopGenerator, RDLListener): + + def get_content(self, node: 'Node') -> Optional[str]: + self.start() + walker = RDLWalker() + walker.walk(node, self, skip_top=True) + return self.finish() + + def enter_AddressableComponent(self, node: 'AddressableNode') -> Optional[WalkerAction]: + if not node.array_dimensions: + return None + + for dim in node.array_dimensions: + self.push_loop(dim) + return None + + def exit_AddressableComponent(self, node: 'AddressableNode') -> Optional[WalkerAction]: + if not node.array_dimensions: + return None + + for _ in node.array_dimensions: + self.pop_loop() + return None diff --git a/src/peakrdl_regblock/hwif/__init__.py b/src/peakrdl_regblock/hwif/__init__.py new file mode 100644 index 0000000..1462264 --- /dev/null +++ b/src/peakrdl_regblock/hwif/__init__.py @@ -0,0 +1,249 @@ +from typing import TYPE_CHECKING, Union, Optional, TextIO + +from systemrdl.node import AddrmapNode, SignalNode, FieldNode, RegNode, AddressableNode +from systemrdl.rdltypes import PropertyReference + +from ..utils import get_indexed_path +from ..identifier_filter import kw_filter as kwf +from ..sv_int import SVInt + +from .generators import InputStructGenerator_Hier, OutputStructGenerator_Hier +from .generators import InputStructGenerator_TypeScope, OutputStructGenerator_TypeScope +from .generators import EnumGenerator + +if TYPE_CHECKING: + from ..exporter import RegblockExporter, DesignState + +class Hwif: + """ + Defines how the hardware input/output signals are generated: + - Field outputs + - Field inputs + - Signal inputs (except those that are promoted to the top) + """ + + def __init__( + self, exp: 'RegblockExporter', + hwif_report_file: Optional[TextIO] + ): + self.exp = exp + + self.has_input_struct = False + self.has_output_struct = False + + self.hwif_report_file = hwif_report_file + + if not self.ds.reuse_hwif_typedefs: + self._gen_in_cls = InputStructGenerator_Hier + self._gen_out_cls = OutputStructGenerator_Hier + else: + self._gen_in_cls = InputStructGenerator_TypeScope + self._gen_out_cls = OutputStructGenerator_TypeScope + + @property + def ds(self) -> 'DesignState': + return self.exp.ds + + @property + def top_node(self) -> AddrmapNode: + return self.exp.ds.top_node + + + def get_extra_package_params(self) -> str: + lines = [""] + + for param in self.top_node.inst.parameters: + value = param.get_value() + if isinstance(value, int): + lines.append( + f"localparam {param.name} = {SVInt(value)};" + ) + elif isinstance(value, str): + lines.append( + f"localparam {param.name} = {value};" + ) + + return "\n".join(lines) + + + def get_package_contents(self) -> str: + """ + If this hwif requires a package, generate the string + """ + lines = [""] + + gen_in = self._gen_in_cls(self) + structs_in = gen_in.get_struct( + self.top_node, + f"{self.top_node.inst_name}__in_t" + ) + if structs_in is not None: + self.has_input_struct = True + lines.append(structs_in) + else: + self.has_input_struct = False + + gen_out = self._gen_out_cls(self) + structs_out = gen_out.get_struct( + self.top_node, + f"{self.top_node.inst_name}__out_t" + ) + if structs_out is not None: + self.has_output_struct = True + lines.append(structs_out) + else: + self.has_output_struct = False + + gen_enum = EnumGenerator() + enums = gen_enum.get_enums(self.ds.user_enums) + if enums is not None: + lines.append(enums) + + return "\n\n".join(lines) + + + @property + def port_declaration(self) -> str: + """ + Returns the declaration string for all I/O ports in the hwif group + """ + + # Assume get_package_declaration() is always called prior to this + assert self.has_input_struct is not None + assert self.has_output_struct is not None + + lines = [] + if self.has_input_struct: + type_name = f"{self.top_node.inst_name}__in_t" + lines.append(f"input {self.ds.package_name}::{type_name} hwif_in") + if self.has_output_struct: + type_name = f"{self.top_node.inst_name}__out_t" + lines.append(f"output {self.ds.package_name}::{type_name} hwif_out") + + return ",\n".join(lines) + + #--------------------------------------------------------------------------- + # hwif utility functions + #--------------------------------------------------------------------------- + def has_value_input(self, obj: Union[FieldNode, SignalNode]) -> bool: + """ + Returns True if the object infers an input wire in the hwif + """ + if isinstance(obj, FieldNode): + return obj.is_hw_writable + elif isinstance(obj, SignalNode): + # Signals are implicitly always inputs + return True + else: + raise RuntimeError + + + def has_value_output(self, obj: FieldNode) -> bool: + """ + Returns True if the object infers an output wire in the hwif + """ + return obj.is_hw_readable + + + def get_input_identifier( + self, + obj: Union[FieldNode, SignalNode, PropertyReference], + width: Optional[int] = None, + ) -> Union[SVInt, str]: + """ + Returns the identifier string that best represents the input object. + + if obj is: + Field: the fields hw input value port + Signal: signal input value + Prop reference: + could be an implied hwclr/hwset/swwe/swwel/we/wel input + + raises an exception if obj is invalid + """ + if isinstance(obj, FieldNode): + next_value = obj.get_property('next') + if next_value is not None: + # 'next' property replaces the inferred input signal + return self.exp.dereferencer.get_value(next_value, width) + # Otherwise, use inferred + path = get_indexed_path(self.top_node, obj) + return "hwif_in." + path + ".next" + elif isinstance(obj, SignalNode): + if obj.get_path() in self.ds.out_of_hier_signals: + return kwf(obj.inst_name) + path = get_indexed_path(self.top_node, obj) + return "hwif_in." + path + elif isinstance(obj, PropertyReference): + assert isinstance(obj.node, FieldNode) + return self.get_implied_prop_input_identifier(obj.node, obj.name) + + raise RuntimeError(f"Unhandled reference to: {obj}") + + def get_external_rd_data(self, node: AddressableNode) -> str: + """ + Returns the identifier string for an external component's rd_data signal + """ + path = get_indexed_path(self.top_node, node) + return "hwif_in." + path + ".rd_data" + + def get_external_rd_ack(self, node: AddressableNode) -> str: + """ + Returns the identifier string for an external component's rd_ack signal + """ + path = get_indexed_path(self.top_node, node) + return "hwif_in." + path + ".rd_ack" + + def get_external_wr_ack(self, node: AddressableNode) -> str: + """ + Returns the identifier string for an external component's wr_ack signal + """ + path = get_indexed_path(self.top_node, node) + return "hwif_in." + path + ".wr_ack" + + def get_implied_prop_input_identifier(self, field: FieldNode, prop: str) -> str: + assert prop in { + 'hwclr', 'hwset', 'swwe', 'swwel', 'we', 'wel', + 'incr', 'decr', 'incrvalue', 'decrvalue' + } + path = get_indexed_path(self.top_node, field) + return "hwif_in." + path + "." + prop + + + def get_output_identifier(self, obj: Union[FieldNode, PropertyReference]) -> str: + """ + Returns the identifier string that best represents the output object. + + if obj is: + Field: the fields hw output value port + Property ref: this is also part of the struct + + raises an exception if obj is invalid + """ + if isinstance(obj, FieldNode): + path = get_indexed_path(self.top_node, obj) + return "hwif_out." + path + ".value" + elif isinstance(obj, PropertyReference): + # TODO: this might be dead code. + # not sure when anything would call this function with a prop ref + # when dereferencer's get_value is more useful here + assert obj.node.get_property(obj.name) + assert isinstance(obj.node, (RegNode, FieldNode)) + return self.get_implied_prop_output_identifier(obj.node, obj.name) + + raise RuntimeError(f"Unhandled reference to: {obj}") + + + def get_implied_prop_output_identifier(self, node: Union[FieldNode, RegNode], prop: str) -> str: + if isinstance(node, FieldNode): + assert prop in { + "anded", "ored", "xored", "swmod", "swacc", + "incrthreshold", "decrthreshold", "overflow", "underflow", + "rd_swacc", "wr_swacc", + } + elif isinstance(node, RegNode): + assert prop in { + "intr", "halt", + } + path = get_indexed_path(self.top_node, node) + return "hwif_out." + path + "." + prop diff --git a/src/peakrdl_regblock/hwif/generators.py b/src/peakrdl_regblock/hwif/generators.py new file mode 100644 index 0000000..e53f0fe --- /dev/null +++ b/src/peakrdl_regblock/hwif/generators.py @@ -0,0 +1,385 @@ +from typing import TYPE_CHECKING, Optional, List, Type + +from systemrdl.node import FieldNode, RegNode, AddrmapNode, MemNode +from systemrdl.walker import WalkerAction + +from ..struct_generator import RDLFlatStructGenerator +from ..identifier_filter import kw_filter as kwf +from ..sv_int import SVInt +from ..utils import clog2 + +if TYPE_CHECKING: + from systemrdl.node import Node, SignalNode, AddressableNode, RegfileNode + from . import Hwif + from systemrdl.rdltypes import UserEnum + +class HWIFStructGenerator(RDLFlatStructGenerator): + def __init__(self, hwif: 'Hwif', hwif_name: str) -> None: + super().__init__() + self.hwif = hwif + self.top_node = hwif.top_node + + self.hwif_report_stack = [hwif_name] + + def push_struct(self, type_name: str, inst_name: str, array_dimensions: Optional[List[int]] = None, packed: bool = False) -> None: # type: ignore + super().push_struct(type_name, inst_name, array_dimensions, packed) + + if array_dimensions: + array_suffix = "".join([f"[0:{dim-1}]" for dim in array_dimensions]) + segment = inst_name + array_suffix + else: + segment = inst_name + self.hwif_report_stack.append(segment) + + def pop_struct(self) -> None: + super().pop_struct() + self.hwif_report_stack.pop() + + def add_member(self, name: str, width: int = 1, *, lsb: int = 0, signed: bool = False) -> None: # type: ignore # pylint: disable=arguments-differ + super().add_member(name, width, lsb=lsb, signed=signed) + + if width > 1 or lsb != 0: + suffix = f"[{lsb+width-1}:{lsb}]" + else: + suffix = "" + + path = ".".join(self.hwif_report_stack) + if self.hwif.hwif_report_file: + self.hwif.hwif_report_file.write(f"{path}.{name}{suffix}\n") + +#------------------------------------------------------------------------------- + +class InputStructGenerator_Hier(HWIFStructGenerator): + def __init__(self, hwif: 'Hwif') -> None: + super().__init__(hwif, "hwif_in") + + def get_typdef_name(self, node:'Node', suffix: str = "") -> str: + base = node.get_rel_path( + self.top_node.parent, + hier_separator="__", + array_suffix="x", + empty_array_suffix="x" + ) + return f'{base}{suffix}__in_t' + + def enter_Signal(self, node: 'SignalNode') -> None: + # only emit the signal if design scanner detected it is actually being used + path = node.get_path() + if path in self.hwif.ds.in_hier_signal_paths: + self.add_member(kwf(node.inst_name), node.width) + + def _add_external_block_members(self, node: 'AddressableNode') -> None: + self.add_member("rd_ack") + self.add_member("rd_data", self.hwif.ds.cpuif_data_width) + self.add_member("wr_ack") + + def enter_Addrmap(self, node: 'AddrmapNode') -> Optional[WalkerAction]: + super().enter_Addrmap(node) + assert node.external + self._add_external_block_members(node) + return WalkerAction.SkipDescendants + + def enter_Regfile(self, node: 'RegfileNode') -> Optional[WalkerAction]: + super().enter_Regfile(node) + if node.external: + self._add_external_block_members(node) + return WalkerAction.SkipDescendants + return WalkerAction.Continue + + def enter_Mem(self, node: 'MemNode') -> Optional[WalkerAction]: + super().enter_Mem(node) + assert node.external + self._add_external_block_members(node) + return WalkerAction.SkipDescendants + + def enter_Reg(self, node: 'RegNode') -> Optional[WalkerAction]: + super().enter_Reg(node) + if node.external: + width = min(self.hwif.ds.cpuif_data_width, node.get_property('regwidth')) + n_subwords = node.get_property("regwidth") // node.get_property("accesswidth") + if node.has_sw_readable: + self.add_member("rd_ack") + self.add_external_reg_rd_data(node, width, n_subwords) + if node.has_sw_writable: + self.add_member("wr_ack") + return WalkerAction.SkipDescendants + + return WalkerAction.Continue + + def add_external_reg_rd_data(self, node: 'RegNode', width: int, n_subwords: int) -> None: + if n_subwords == 1: + # External reg is 1 sub-word. Add a packed struct to represent it + type_name = self.get_typdef_name(node, "__fields") + self.push_struct(type_name, "rd_data", packed=True) + current_bit = width - 1 + for field in reversed(list(node.fields())): + if not field.is_sw_readable: + continue + if field.high < current_bit: + # Add padding + self.add_member( + f"_reserved_{current_bit}_{field.high + 1}", + current_bit - field.high + ) + self.add_member( + kwf(field.inst_name), + field.width + ) + current_bit = field.low - 1 + + # Add end padding if needed + if current_bit != -1: + self.add_member( + f"_reserved_{current_bit}_0", + current_bit + 1 + ) + self.pop_struct() + else: + # Multiple sub-words. Cannot generate a struct + self.add_member("rd_data", width) + + def enter_Field(self, node: 'FieldNode') -> None: + type_name = self.get_typdef_name(node) + self.push_struct(type_name, kwf(node.inst_name)) + + # Provide input to field's next value if it is writable by hw, and it + # was not overridden by the 'next' property + if node.is_hw_writable and node.get_property('next') is None: + # Get the field's LSB index (can be nonzero for fixed-point values) + fracwidth = node.get_property("fracwidth") + lsb = 0 if fracwidth is None else -fracwidth + + # get the signedness of the field + signed = node.get_property("is_signed") + + self.add_member("next", node.width, lsb=lsb, signed=signed) + + # Generate implied inputs + for prop_name in ["we", "wel", "swwe", "swwel", "hwclr", "hwset"]: + # if property is boolean and true, implies a corresponding input signal on the hwif + if node.get_property(prop_name) is True: + self.add_member(prop_name) + + # Generate any implied counter inputs + if node.is_up_counter: + if not node.get_property('incr'): + # User did not provide their own incr component reference. + # Imply an input + self.add_member('incr') + + width = node.get_property('incrwidth') + if width: + # Implies a corresponding incrvalue input + self.add_member('incrvalue', width) + + if node.is_down_counter: + if not node.get_property('decr'): + # User did not provide their own decr component reference. + # Imply an input + self.add_member('decr') + + width = node.get_property('decrwidth') + if width: + # Implies a corresponding decrvalue input + self.add_member('decrvalue', width) + + def exit_Field(self, node: 'FieldNode') -> None: + self.pop_struct() + + +class OutputStructGenerator_Hier(HWIFStructGenerator): + def __init__(self, hwif: 'Hwif') -> None: + super().__init__(hwif, "hwif_out") + + def get_typdef_name(self, node:'Node', suffix: str = "") -> str: + base = node.get_rel_path( + self.top_node.parent, + hier_separator="__", + array_suffix="x", + empty_array_suffix="x" + ) + return f'{base}{suffix}__out_t' + + def _add_external_block_members(self, node: 'AddressableNode') -> None: + self.add_member("req") + self.add_member("addr", clog2(node.size)) + self.add_member("req_is_wr") + self.add_member("wr_data", self.hwif.ds.cpuif_data_width) + self.add_member("wr_biten", self.hwif.ds.cpuif_data_width) + + def enter_Addrmap(self, node: 'AddrmapNode') -> Optional[WalkerAction]: + super().enter_Addrmap(node) + assert node.external + self._add_external_block_members(node) + return WalkerAction.SkipDescendants + + def enter_Regfile(self, node: 'RegfileNode') -> Optional[WalkerAction]: + super().enter_Regfile(node) + if node.external: + self._add_external_block_members(node) + return WalkerAction.SkipDescendants + return WalkerAction.Continue + + def enter_Mem(self, node: 'MemNode') -> Optional[WalkerAction]: + super().enter_Mem(node) + assert node.external + self._add_external_block_members(node) + return WalkerAction.SkipDescendants + + def enter_Reg(self, node: 'RegNode') -> Optional[WalkerAction]: + super().enter_Reg(node) + if node.external: + width = min(self.hwif.ds.cpuif_data_width, node.get_property('regwidth')) + n_subwords = node.get_property("regwidth") // node.get_property("accesswidth") + self.add_member("req", n_subwords) + self.add_member("req_is_wr") + if node.has_sw_writable: + self.add_external_reg_wr_data("wr_data", node, width, n_subwords) + self.add_external_reg_wr_data("wr_biten", node, width, n_subwords) + return WalkerAction.SkipDescendants + + return WalkerAction.Continue + + def add_external_reg_wr_data(self, name: str, node: 'RegNode', width: int, n_subwords: int) -> None: + if n_subwords == 1: + # External reg is 1 sub-word. Add a packed struct to represent it + type_name = self.get_typdef_name(node, "__fields") + self.push_struct(type_name, name, packed=True) + current_bit = width - 1 + for field in reversed(list(node.fields())): + if not field.is_sw_writable: + continue + if field.high < current_bit: + # Add padding + self.add_member( + f"_reserved_{current_bit}_{field.high + 1}", + current_bit - field.high + ) + self.add_member( + kwf(field.inst_name), + field.width + ) + current_bit = field.low - 1 + + # Add end padding if needed + if current_bit != -1: + self.add_member( + f"_reserved_{current_bit}_0", + current_bit + 1 + ) + self.pop_struct() + else: + # Multiple sub-words. Cannot generate a struct + self.add_member(name, width) + + def enter_Field(self, node: 'FieldNode') -> None: + type_name = self.get_typdef_name(node) + self.push_struct(type_name, kwf(node.inst_name)) + + # Expose field's value if it is readable by hw + if node.is_hw_readable: + # Get the node's LSB index (can be nonzero for fixed-point values) + fracwidth = node.get_property("fracwidth") + lsb = 0 if fracwidth is None else -fracwidth + + # get the signedness of the field + signed = node.get_property("is_signed") + + self.add_member("value", node.width, lsb=lsb, signed=signed) + + # Generate output bit signals enabled via property + for prop_name in ["anded", "ored", "xored", "swmod", "swacc", "overflow", "underflow", "rd_swacc", "wr_swacc"]: + if node.get_property(prop_name): + self.add_member(prop_name) + + if node.get_property('incrthreshold') is not False: # (explicitly not False. Not 0) + self.add_member('incrthreshold') + if node.get_property('decrthreshold') is not False: # (explicitly not False. Not 0) + self.add_member('decrthreshold') + + def exit_Field(self, node: 'FieldNode') -> None: + self.pop_struct() + + def exit_Reg(self, node: 'RegNode') -> None: + if node.is_interrupt_reg: + self.add_member('intr') + if node.is_halt_reg: + self.add_member('halt') + super().exit_Reg(node) + +#------------------------------------------------------------------------------- +class InputStructGenerator_TypeScope(InputStructGenerator_Hier): + def get_typdef_name(self, node:'Node', suffix: str = "") -> str: + scope_path = node.get_global_type_name("__") + if scope_path is None: + # Unable to determine a reusable type name. Fall back to hierarchical path + # Add prefix to prevent collision when mixing namespace methods + scope_path = "xtern__" + super().get_typdef_name(node) + + if node.external: + # Node generates alternate external signals + extra_suffix = "__external" + else: + extra_suffix = "" + + return f'{scope_path}{extra_suffix}{suffix}__in_t' + +class OutputStructGenerator_TypeScope(OutputStructGenerator_Hier): + def get_typdef_name(self, node:'Node', suffix: str = "") -> str: + scope_path = node.get_global_type_name("__") + if scope_path is None: + # Unable to determine a reusable type name. Fall back to hierarchical path + # Add prefix to prevent collision when mixing namespace methods + scope_path = "xtern__" + super().get_typdef_name(node) + + if node.external: + # Node generates alternate external signals + extra_suffix = "__external" + else: + extra_suffix = "" + + return f'{scope_path}{extra_suffix}{suffix}__out_t' + +#------------------------------------------------------------------------------- +class EnumGenerator: + """ + Generator for user-defined enum definitions + """ + + def get_enums(self, user_enums: List[Type['UserEnum']]) -> Optional[str]: + if not user_enums: + return None + + lines = [] + for user_enum in user_enums: + lines.append(self._enum_typedef(user_enum)) + + return '\n\n'.join(lines) + + @staticmethod + def _get_prefix(user_enum: Type['UserEnum']) -> str: + scope = user_enum.get_scope_path("__") + if scope: + return f"{scope}__{user_enum.type_name}" + else: + return user_enum.type_name + + def _enum_typedef(self, user_enum: Type['UserEnum']) -> str: + prefix = self._get_prefix(user_enum) + + lines = [] + max_value = 1 + for enum_member in user_enum: + lines.append(f" {prefix}__{enum_member.name} = {SVInt(enum_member.value)}") + max_value = max(max_value, enum_member.value) + + if max_value.bit_length() == 1: + datatype = "logic" + else: + datatype = f"logic [{max_value.bit_length() - 1}:0]" + + return ( + f"typedef enum {datatype} {{\n" + + ",\n".join(lines) + + f"\n}} {prefix}_e;" + ) diff --git a/src/peakrdl_regblock/identifier_filter.py b/src/peakrdl_regblock/identifier_filter.py new file mode 100644 index 0000000..f701a28 --- /dev/null +++ b/src/peakrdl_regblock/identifier_filter.py @@ -0,0 +1,52 @@ + +# All SystemVerilog 2017 keywords +SV_KEYWORDS = { + 'accept_on', 'alias', 'always', 'always_comb', 'always_ff', 'always_latch', + 'and', 'assert', 'assign', 'assume', 'automatic', 'before', 'begin', 'bind', + 'bins', 'binsof', 'bit', 'break', 'buf', 'bufif0', 'bufif1', 'byte', 'case', + 'casex', 'casez', 'cell', 'chandle', 'checker', 'class', 'clocking', 'cmos', + 'config', 'const', 'constraint', 'context', 'continue', 'cover', 'covergroup', + 'coverpoint', 'cross', 'deassign', 'default', 'defparam', 'design', 'disable', + 'dist', 'do', 'edge', 'else', 'end', 'endcase', 'endchecker', 'endclass', + 'endclocking', 'endconfig', 'endfunction', 'endgenerate', 'endgroup', + 'endinterface', 'endmodule', 'endpackage', 'endprimitive', 'endprogram', + 'endproperty', 'endspecify', 'endsequence', 'endtable', 'endtask', 'enum', + 'event', 'eventually', 'expect', 'export', 'extends', 'extern', 'final', + 'first_match', 'for', 'force', 'foreach', 'forever', 'fork', 'forkjoin', + 'function', 'generate', 'genvar', 'global', 'highz0', 'highz1', 'if', 'iff', + 'ifnone', 'ignore_bins', 'illegal_bins', 'implements', 'implies', 'import', + 'incdir', 'include', 'initial', 'inout', 'input', 'inside', 'instance', + 'int', 'integer', 'interconnect', 'interface', 'intersect', 'join', + 'join_any', 'join_none', 'large', 'let', 'liblist', 'library', 'local', + 'localparam', 'logic', 'longint', 'macromodule', 'matches', 'medium', + 'modport', 'module', 'nand', 'negedge', 'nettype', 'new', 'nexttime', 'nmos', + 'nor', 'noshowcancelled', 'not', 'notif0', 'notif1', 'null', 'or', 'output', + 'package', 'packed', 'parameter', 'pmos', 'posedge', 'primitive', 'priority', + 'program', 'property', 'protected', 'pull0', 'pull1', 'pulldown', 'pullup', + 'pulsestyle_ondetect', 'pulsestyle_onevent', 'pure', 'rand', 'randc', + 'randcase', 'randsequence', 'rcmos', 'real', 'realtime', 'ref', 'reg', + 'reject_on', 'release', 'repeat', 'restrict', 'return', 'rnmos', 'rpmos', + 'rtran', 'rtranif0', 'rtranif1', 's_always', 's_eventually', 's_nexttime', + 's_until', 's_until_with', 'scalared', 'sequence', 'shortint', 'shortreal', + 'showcancelled', 'signed', 'small', 'soft', 'solve', 'specify', 'specparam', + 'static', 'string', 'strong', 'strong0', 'strong1', 'struct', 'super', + 'supply0', 'supply1', 'sync_accept_on', 'sync_reject_on', 'table', 'tagged', + 'task', 'this', 'throughout', 'time', 'timeprecision', 'timeunit', 'tran', + 'tranif0', 'tranif1', 'tri', 'tri0', 'tri1', 'triand', 'trior', 'trireg', + 'type', 'typedef', 'union', 'unique', 'unique0', 'unsigned', 'until', + 'until_with', 'untyped', 'use', 'uwire', 'var', 'vectored', 'virtual', 'void', + 'wait', 'wait_order', 'wand', 'weak', 'weak0', 'weak1', 'while', 'wildcard', + 'wire', 'with', 'within', 'wor', 'xnor', 'xor' +} + + +def kw_filter(s: str) -> str: + """ + Make all user identifiers 'safe' and ensure they do not collide with + SystemVerilog keywords. + + If an SV keyword is encountered, add an underscore suffix + """ + if s in SV_KEYWORDS: + s += "_" + return s diff --git a/src/peakrdl_regblock/module_tmpl.sv b/src/peakrdl_regblock/module_tmpl.sv new file mode 100644 index 0000000..bad1c21 --- /dev/null +++ b/src/peakrdl_regblock/module_tmpl.sv @@ -0,0 +1,293 @@ +// Generated by PeakRDL-regblock - A free and open-source SystemVerilog generator +// https://github.com/SystemRDL/PeakRDL-regblock + +module {{ds.module_name}} + {%- if cpuif.parameters %} #( + {{",\n ".join(cpuif.parameters)}} + ) {%- endif %} ( + input wire clk, + input wire {{default_resetsignal_name}}, + + {%- for signal in ds.out_of_hier_signals.values() %} + {%- if signal.width == 1 %} + input wire {{kwf(signal.inst_name)}}, + {%- else %} + input wire [{{signal.width-1}}:0] {{kwf(signal.inst_name)}}, + {%- endif %} + {%- endfor %} + + {%- if ds.has_paritycheck %} + + output logic parity_error, + {%- endif %} + + {{cpuif.port_declaration|indent(8)}} + {%- if hwif.has_input_struct or hwif.has_output_struct %},{% endif %} + + {{hwif.port_declaration|indent(8)}} + ); + + //-------------------------------------------------------------------------- + // CPU Bus interface logic + //-------------------------------------------------------------------------- + logic cpuif_req; + logic cpuif_req_is_wr; + logic [{{cpuif.addr_width-1}}:0] cpuif_addr; + logic [{{cpuif.data_width-1}}:0] cpuif_wr_data; + logic [{{cpuif.data_width-1}}:0] cpuif_wr_biten; + logic cpuif_req_stall_wr; + logic cpuif_req_stall_rd; + + logic cpuif_rd_ack; + logic cpuif_rd_err; + logic [{{cpuif.data_width-1}}:0] cpuif_rd_data; + + logic cpuif_wr_ack; + logic cpuif_wr_err; + + {{cpuif.get_implementation()|indent}} + + logic cpuif_req_masked; +{%- if ds.has_external_addressable %} + logic external_req; + logic external_pending; + logic external_wr_ack; + logic external_rd_ack; + always_ff {{get_always_ff_event(cpuif.reset)}} begin + if({{get_resetsignal(cpuif.reset)}}) begin + external_pending <= '0; + end else begin + if(external_req & ~external_wr_ack & ~external_rd_ack) external_pending <= '1; + else if(external_wr_ack | external_rd_ack) external_pending <= '0; + `ifndef SYNTHESIS + assert_bad_ext_wr_ack: assert(!external_wr_ack || (external_pending | external_req)) + else $error("An external wr_ack strobe was asserted when no external request was active"); + assert_bad_ext_rd_ack: assert(!external_rd_ack || (external_pending | external_req)) + else $error("An external rd_ack strobe was asserted when no external request was active"); + `endif + end + end +{%- endif %} +{% if ds.min_read_latency == ds.min_write_latency %} + // Read & write latencies are balanced. Stalls not required + {%- if ds.has_external_addressable %} + // except if external + assign cpuif_req_stall_rd = external_pending; + assign cpuif_req_stall_wr = external_pending; + {%- else %} + assign cpuif_req_stall_rd = '0; + assign cpuif_req_stall_wr = '0; + {%- endif %} +{%- elif ds.min_read_latency > ds.min_write_latency %} + // Read latency > write latency. May need to delay next write that follows a read + logic [{{ds.min_read_latency - ds.min_write_latency - 1}}:0] cpuif_req_stall_sr; + always_ff {{get_always_ff_event(cpuif.reset)}} begin + if({{get_resetsignal(cpuif.reset)}}) begin + cpuif_req_stall_sr <= '0; + end else if(cpuif_req && !cpuif_req_is_wr) begin + cpuif_req_stall_sr <= '1; + end else begin + cpuif_req_stall_sr <= (cpuif_req_stall_sr >> 'd1); + end + end + {%- if ds.has_external_addressable %} + assign cpuif_req_stall_rd = external_pending; + assign cpuif_req_stall_wr = cpuif_req_stall_sr[0] | external_pending; + {%- else %} + assign cpuif_req_stall_rd = '0; + assign cpuif_req_stall_wr = cpuif_req_stall_sr[0]; + {%- endif %} +{%- else %} + // Write latency > read latency. May need to delay next read that follows a write + logic [{{ds.min_write_latency - ds.min_read_latency - 1}}:0] cpuif_req_stall_sr; + always_ff {{get_always_ff_event(cpuif.reset)}} begin + if({{get_resetsignal(cpuif.reset)}}) begin + cpuif_req_stall_sr <= '0; + end else if(cpuif_req && cpuif_req_is_wr) begin + cpuif_req_stall_sr <= '1; + end else begin + cpuif_req_stall_sr <= (cpuif_req_stall_sr >> 'd1); + end + end + {%- if ds.has_external_addressable %} + assign cpuif_req_stall_rd = cpuif_req_stall_sr[0] | external_pending; + assign cpuif_req_stall_wr = external_pending; + {%- else %} + assign cpuif_req_stall_rd = cpuif_req_stall_sr[0]; + assign cpuif_req_stall_wr = '0; + {%- endif %} +{%- endif %} + assign cpuif_req_masked = cpuif_req + & !(!cpuif_req_is_wr & cpuif_req_stall_rd) + & !(cpuif_req_is_wr & cpuif_req_stall_wr); + + //-------------------------------------------------------------------------- + // Address Decode + //-------------------------------------------------------------------------- + {{address_decode.get_strobe_struct()|indent}} + decoded_reg_strb_t decoded_reg_strb; +{%- if ds.has_external_addressable %} + logic decoded_strb_is_external; +{% endif %} +{%- if ds.has_external_block %} + logic [{{cpuif.addr_width-1}}:0] decoded_addr; +{% endif %} + logic decoded_req; + logic decoded_req_is_wr; + logic [{{cpuif.data_width-1}}:0] decoded_wr_data; + logic [{{cpuif.data_width-1}}:0] decoded_wr_biten; + + always_comb begin + {%- if ds.has_external_addressable %} + automatic logic is_external; + is_external = '0; + {%- endif %} + {{address_decode.get_implementation()|indent(8)}} + {%- if ds.has_external_addressable %} + decoded_strb_is_external = is_external; + external_req = is_external; + {%- endif %} + end + + // Pass down signals to next stage +{%- if ds.has_external_block %} + assign decoded_addr = cpuif_addr; +{% endif %} + assign decoded_req = cpuif_req_masked; + assign decoded_req_is_wr = cpuif_req_is_wr; + assign decoded_wr_data = cpuif_wr_data; + assign decoded_wr_biten = cpuif_wr_biten; +{% if ds.has_writable_msb0_fields %} + // bitswap for use by fields with msb0 ordering + logic [{{cpuif.data_width-1}}:0] decoded_wr_data_bswap; + logic [{{cpuif.data_width-1}}:0] decoded_wr_biten_bswap; + assign decoded_wr_data_bswap = {<<{decoded_wr_data}}; + assign decoded_wr_biten_bswap = {<<{decoded_wr_biten}}; +{%- endif %} + +{%- if ds.has_buffered_write_regs %} + + //-------------------------------------------------------------------------- + // Write double-buffers + //-------------------------------------------------------------------------- + {{write_buffering.get_storage_struct()|indent}} + + {{write_buffering.get_implementation()|indent}} +{%- endif %} + //-------------------------------------------------------------------------- + // Field logic + //-------------------------------------------------------------------------- + {{field_logic.get_combo_struct()|indent}} + + {{field_logic.get_storage_struct()|indent}} + + {{field_logic.get_implementation()|indent}} + +{%- if ds.has_paritycheck %} + + //-------------------------------------------------------------------------- + // Parity Error + //-------------------------------------------------------------------------- + always_ff {{get_always_ff_event(cpuif.reset)}} begin + if({{get_resetsignal(cpuif.reset)}}) begin + parity_error <= '0; + end else begin + automatic logic err; + err = '0; + {{parity.get_implementation()|indent(12)}} + parity_error <= err; + end + end +{%- endif %} + +{%- if ds.has_buffered_read_regs %} + + //-------------------------------------------------------------------------- + // Read double-buffers + //-------------------------------------------------------------------------- + {{read_buffering.get_storage_struct()|indent}} + + {{read_buffering.get_implementation()|indent}} +{%- endif %} + + //-------------------------------------------------------------------------- + // Write response + //-------------------------------------------------------------------------- +{%- if ds.has_external_addressable %} + always_comb begin + automatic logic wr_ack; + wr_ack = '0; + {{ext_write_acks.get_implementation()|indent(8)}} + external_wr_ack = wr_ack; + end + assign cpuif_wr_ack = external_wr_ack | (decoded_req & decoded_req_is_wr & ~decoded_strb_is_external); +{%- else %} + assign cpuif_wr_ack = decoded_req & decoded_req_is_wr; +{%- endif %} + // Writes are always granted with no error response + assign cpuif_wr_err = '0; + + //-------------------------------------------------------------------------- + // Readback + //-------------------------------------------------------------------------- +{%- if ds.has_external_addressable %} + logic readback_external_rd_ack_c; + always_comb begin + automatic logic rd_ack; + rd_ack = '0; + {{ext_read_acks.get_implementation()|indent(8)}} + readback_external_rd_ack_c = rd_ack; + end + + logic readback_external_rd_ack; + {%- if ds.retime_read_fanin %} + always_ff {{get_always_ff_event(cpuif.reset)}} begin + if({{get_resetsignal(cpuif.reset)}}) begin + readback_external_rd_ack <= '0; + end else begin + readback_external_rd_ack <= readback_external_rd_ack_c; + end + end + + {%- else %} + + assign readback_external_rd_ack = readback_external_rd_ack_c; + {%- endif %} +{%- endif %} + + logic readback_err; + logic readback_done; + logic [{{cpuif.data_width-1}}:0] readback_data; +{{readback_implementation|indent}} +{% if ds.retime_read_response %} + always_ff {{get_always_ff_event(cpuif.reset)}} begin + if({{get_resetsignal(cpuif.reset)}}) begin + cpuif_rd_ack <= '0; + cpuif_rd_data <= '0; + cpuif_rd_err <= '0; + {%- if ds.has_external_addressable %} + external_rd_ack <= '0; + {%- endif %} + end else begin + {%- if ds.has_external_addressable %} + external_rd_ack <= readback_external_rd_ack; + cpuif_rd_ack <= readback_done | readback_external_rd_ack; + {%- else %} + cpuif_rd_ack <= readback_done; + {%- endif %} + cpuif_rd_data <= readback_data; + cpuif_rd_err <= readback_err; + end + end +{% else %} + {%- if ds.has_external_addressable %} + assign external_rd_ack = readback_external_rd_ack; + assign cpuif_rd_ack = readback_done | readback_external_rd_ack; + {%- else %} + assign cpuif_rd_ack = readback_done; + {%- endif %} + assign cpuif_rd_data = readback_data; + assign cpuif_rd_err = readback_err; +{%- endif %} +endmodule +{# (eof newline anchor) #} diff --git a/src/peakrdl_regblock/package_tmpl.sv b/src/peakrdl_regblock/package_tmpl.sv new file mode 100644 index 0000000..b665495 --- /dev/null +++ b/src/peakrdl_regblock/package_tmpl.sv @@ -0,0 +1,14 @@ +// Generated by PeakRDL-regblock - A free and open-source SystemVerilog generator +// https://github.com/SystemRDL/PeakRDL-regblock + +package {{ds.package_name}}; + + localparam {{ds.module_name.upper()}}_DATA_WIDTH = {{ds.cpuif_data_width}}; + localparam {{ds.module_name.upper()}}_MIN_ADDR_WIDTH = {{ds.addr_width}}; + localparam {{ds.module_name.upper()}}_SIZE = {{SVInt(ds.top_node.size)}}; + + {{-hwif.get_extra_package_params()|indent}} + + {{-hwif.get_package_contents()|indent}} +endpackage +{# (eof newline anchor) #} diff --git a/src/peakrdl_regblock/parity.py b/src/peakrdl_regblock/parity.py new file mode 100644 index 0000000..980a2c7 --- /dev/null +++ b/src/peakrdl_regblock/parity.py @@ -0,0 +1,34 @@ +from typing import TYPE_CHECKING + +from systemrdl.walker import WalkerAction + + +from .forloop_generator import RDLForLoopGenerator + +if TYPE_CHECKING: + from .exporter import RegblockExporter + from systemrdl.node import FieldNode, AddressableNode + + +class ParityErrorReduceGenerator(RDLForLoopGenerator): + def __init__(self, exp: 'RegblockExporter') -> None: + super().__init__() + self.exp = exp + + def get_implementation(self) -> str: + content = self.get_content(self.exp.ds.top_node) + if content is None: + return "" + return content + + def enter_AddressableComponent(self, node: 'AddressableNode') -> WalkerAction: + super().enter_AddressableComponent(node) + if node.external: + return WalkerAction.SkipDescendants + return WalkerAction.Continue + + def enter_Field(self, node: 'FieldNode') -> None: + if node.get_property('paritycheck') and node.implements_storage: + self.add_content( + f"err |= {self.exp.field_logic.get_parity_error_identifier(node)};" + ) diff --git a/src/peakrdl_regblock/read_buffering/__init__.py b/src/peakrdl_regblock/read_buffering/__init__.py new file mode 100644 index 0000000..65ccc94 --- /dev/null +++ b/src/peakrdl_regblock/read_buffering/__init__.py @@ -0,0 +1,59 @@ +from typing import TYPE_CHECKING, Union + +from systemrdl.node import AddrmapNode, RegNode, SignalNode + +from .storage_generator import RBufStorageStructGenerator +from .implementation_generator import RBufLogicGenerator +from ..utils import get_indexed_path +from ..sv_int import SVInt + +if TYPE_CHECKING: + from ..exporter import RegblockExporter + + +class ReadBuffering: + def __init__(self, exp:'RegblockExporter'): + self.exp = exp + + @property + def top_node(self) -> 'AddrmapNode': + return self.exp.ds.top_node + + def get_storage_struct(self) -> str: + struct_gen = RBufStorageStructGenerator() + s = struct_gen.get_struct(self.top_node, "rbuf_storage_t") + assert s is not None + return s + "\nrbuf_storage_t rbuf_storage;" + + def get_implementation(self) -> str: + gen = RBufLogicGenerator(self) + s = gen.get_content(self.top_node) + assert s is not None + return s + + def get_trigger(self, node: RegNode) -> str: + trigger = node.get_property('rbuffer_trigger') + + if isinstance(trigger, RegNode): + # Trigger is a register. + # trigger when lowermost address of the register is written + regwidth = trigger.get_property('regwidth') + accesswidth = trigger.get_property('accesswidth') + strb_prefix = self.exp.dereferencer.get_access_strobe(trigger, reduce_substrobes=False) + + if accesswidth < regwidth: + return f"{strb_prefix}[0] && !decoded_req_is_wr" + else: + return f"{strb_prefix} && !decoded_req_is_wr" + elif isinstance(trigger, SignalNode): + s = self.exp.dereferencer.get_value(trigger) + if trigger.get_property('activehigh'): + return str(s) + else: + return f"~{s}" + else: + # Trigger is a field or propref bit + return str(self.exp.dereferencer.get_value(trigger)) + + def get_rbuf_data(self, node: RegNode) -> str: + return "rbuf_storage." + get_indexed_path(self.top_node, node) + ".data" diff --git a/src/peakrdl_regblock/read_buffering/implementation_generator.py b/src/peakrdl_regblock/read_buffering/implementation_generator.py new file mode 100644 index 0000000..ea36d8b --- /dev/null +++ b/src/peakrdl_regblock/read_buffering/implementation_generator.py @@ -0,0 +1,59 @@ +from typing import TYPE_CHECKING + +from systemrdl.component import Reg +from systemrdl.node import RegNode + +from ..forloop_generator import RDLForLoopGenerator + +if TYPE_CHECKING: + from . import ReadBuffering + +class RBufLogicGenerator(RDLForLoopGenerator): + i_type = "genvar" + def __init__(self, rbuf: 'ReadBuffering') -> None: + super().__init__() + self.rbuf = rbuf + self.exp = rbuf.exp + self.template = self.exp.jj_env.get_template( + "read_buffering/template.sv" + ) + + def enter_Reg(self, node: RegNode) -> None: + super().enter_Reg(node) + assert isinstance(node.inst, Reg) + + if not node.get_property('buffer_reads'): + return + + context = { + 'node': node, + 'rbuf': self.rbuf, + 'get_assignments': self.get_assignments, + } + self.add_content(self.template.render(context)) + + + + def get_assignments(self, node: RegNode) -> str: + data = self.rbuf.get_rbuf_data(node) + bidx = 0 + s = [] + for field in node.fields(): + if bidx < field.low: + # zero padding before field + s.append(f"{data}[{field.low-1}:{bidx}] <= '0;") + + value = self.exp.dereferencer.get_value(field) + if field.msb < field.lsb: + # Field gets bitswapped since it is in [low:high] orientation + value = f"{{<<{{{value}}}}}" + s.append(f"{data}[{field.high}:{field.low}] <= {value};") + + bidx = field.high + 1 + + regwidth = node.get_property('regwidth') + if bidx < regwidth: + # zero padding after last field + s.append(f"{data}[{regwidth-1}:{bidx}] <= '0;") + + return "\n".join(s) diff --git a/src/peakrdl_regblock/read_buffering/storage_generator.py b/src/peakrdl_regblock/read_buffering/storage_generator.py new file mode 100644 index 0000000..c6a6d6d --- /dev/null +++ b/src/peakrdl_regblock/read_buffering/storage_generator.py @@ -0,0 +1,18 @@ +from systemrdl.node import FieldNode, RegNode + +from ..struct_generator import RDLStructGenerator + +class RBufStorageStructGenerator(RDLStructGenerator): + + def enter_Field(self, node: FieldNode) -> None: + # suppress parent class's field behavior + pass + + def enter_Reg(self, node: RegNode) -> None: + super().enter_Reg(node) + + if not node.get_property('buffer_reads'): + return + + regwidth = node.get_property('regwidth') + self.add_member("data", regwidth) diff --git a/src/peakrdl_regblock/read_buffering/template.sv b/src/peakrdl_regblock/read_buffering/template.sv new file mode 100644 index 0000000..a97b351 --- /dev/null +++ b/src/peakrdl_regblock/read_buffering/template.sv @@ -0,0 +1,5 @@ +always_ff @(posedge clk) begin + if({{rbuf.get_trigger(node)}}) begin + {{get_assignments(node)|indent(8)}} + end +end diff --git a/src/peakrdl_regblock/readback/__init__.py b/src/peakrdl_regblock/readback/__init__.py new file mode 100644 index 0000000..dafb1e0 --- /dev/null +++ b/src/peakrdl_regblock/readback/__init__.py @@ -0,0 +1,72 @@ +from typing import TYPE_CHECKING +import math + +from .generators import ReadbackAssignmentGenerator + +if TYPE_CHECKING: + from ..exporter import RegblockExporter, DesignState + from systemrdl.node import AddrmapNode + +class Readback: + def __init__(self, exp:'RegblockExporter'): + self.exp = exp + + @property + def ds(self) -> 'DesignState': + return self.exp.ds + + @property + def top_node(self) -> 'AddrmapNode': + return self.exp.ds.top_node + + def get_implementation(self) -> str: + gen = ReadbackAssignmentGenerator(self.exp) + array_assignments = gen.get_content(self.top_node) + array_size = gen.current_offset + + # Enabling the fanin stage doesnt make sense if readback fanin is + # small. This also avoids pesky corner cases + if array_size < 4: + self.ds.retime_read_fanin = False + + context = { + "array_assignments" : array_assignments, + "array_size" : array_size, + 'get_always_ff_event': self.exp.dereferencer.get_always_ff_event, + 'get_resetsignal': self.exp.dereferencer.get_resetsignal, + "cpuif": self.exp.cpuif, + "ds": self.ds, + } + + if self.ds.retime_read_fanin: + # If adding a fanin pipeline stage, goal is to try to + # split the fanin path in the middle so that fanin into the stage + # and the following are roughly balanced. + fanin_target = math.sqrt(array_size) + + # Size of fanin group to consume per fanin element + fanin_stride = math.floor(fanin_target) + + # Number of array elements to reduce to. + # Round up to an extra element in case there is some residual + fanin_array_size = math.ceil(array_size / fanin_stride) + + # leftovers are handled in an extra array element + fanin_residual_stride = array_size % fanin_stride + + if fanin_residual_stride != 0: + # If there is a partial fanin element, reduce the number of + # loops performed in the bulk fanin stage + fanin_loop_iter = fanin_array_size - 1 + else: + fanin_loop_iter = fanin_array_size + + context['fanin_stride'] = fanin_stride + context['fanin_array_size'] = fanin_array_size + context['fanin_residual_stride'] = fanin_residual_stride + context['fanin_loop_iter'] = fanin_loop_iter + + template = self.exp.jj_env.get_template( + "readback/templates/readback.sv" + ) + return template.render(context) diff --git a/src/peakrdl_regblock/readback/generators.py b/src/peakrdl_regblock/readback/generators.py new file mode 100644 index 0000000..87f2969 --- /dev/null +++ b/src/peakrdl_regblock/readback/generators.py @@ -0,0 +1,381 @@ +from typing import TYPE_CHECKING, List + +from systemrdl.node import RegNode, AddressableNode +from systemrdl.walker import WalkerAction + +from ..forloop_generator import RDLForLoopGenerator, LoopBody + +from ..utils import do_bitswap, do_slice + +if TYPE_CHECKING: + from ..exporter import RegblockExporter + +class ReadbackLoopBody(LoopBody): + def __init__(self, dim: int, iterator: str, i_type: str) -> None: + super().__init__(dim, iterator, i_type) + self.n_regs = 0 + + def __str__(self) -> str: + # replace $i#sz token when stringifying + s = super().__str__() + token = f"${self.iterator}sz" + s = s.replace(token, str(self.n_regs)) + return s + +class ReadbackAssignmentGenerator(RDLForLoopGenerator): + i_type = "genvar" + loop_body_cls = ReadbackLoopBody + + def __init__(self, exp:'RegblockExporter') -> None: + super().__init__() + self.exp = exp + + # The readback array collects all possible readback values into a flat + # array. The array width is equal to the CPUIF bus width. Each entry in + # the array represents an aligned read access. + self.current_offset = 0 + self.start_offset_stack = [] # type: List[int] + self.dim_stack = [] # type: List[int] + + @property + def current_offset_str(self) -> str: + """ + Derive a string that represents the current offset being assigned. + This consists of: + - The current integer offset + - multiplied index of any enclosing loop + + The integer offset from "current_offset" is static and is monotonically + incremented as more register assignments are processed. + + The component of the offset from loops is added by multiplying the current + loop index by the loop size. + Since the loop's size is not known at this time, it is emitted as a + placeholder token like: $i0sz, $i1sz, $i2sz, etc + These tokens can be replaced once the loop body has been completed and the + size of its contents is known. + """ + offset_parts = [] + for i in range(self._loop_level): + offset_parts.append(f"i{i} * $i{i}sz") + offset_parts.append(str(self.current_offset)) + return " + ".join(offset_parts) + + def push_loop(self, dim: int) -> None: + super().push_loop(dim) + self.start_offset_stack.append(self.current_offset) + self.dim_stack.append(dim) + + def pop_loop(self) -> None: + start_offset = self.start_offset_stack.pop() + dim = self.dim_stack.pop() + + # Number of registers enclosed in this loop + n_regs = self.current_offset - start_offset + self.current_loop.n_regs = n_regs # type: ignore + + super().pop_loop() + + # Advance current scope's offset to account for loop's contents + self.current_offset = start_offset + n_regs * dim + + + def enter_AddressableComponent(self, node: 'AddressableNode') -> WalkerAction: + super().enter_AddressableComponent(node) + + if node.external and not isinstance(node, RegNode): + # External block + strb = self.exp.hwif.get_external_rd_ack(node) + data = self.exp.hwif.get_external_rd_data(node) + self.add_content(f"assign readback_array[{self.current_offset_str}] = {strb} ? {data} : '0;") + self.current_offset += 1 + return WalkerAction.SkipDescendants + + return WalkerAction.Continue + + def enter_Reg(self, node: RegNode) -> WalkerAction: + if not node.has_sw_readable: + return WalkerAction.SkipDescendants + + if node.external: + self.process_external_reg(node) + return WalkerAction.SkipDescendants + + accesswidth = node.get_property('accesswidth') + regwidth = node.get_property('regwidth') + rbuf = node.get_property('buffer_reads') + if rbuf: + trigger = node.get_property('rbuffer_trigger') + is_own_trigger = (isinstance(trigger, RegNode) and trigger == node) + if is_own_trigger: + if accesswidth < regwidth: + self.process_buffered_reg_with_bypass(node, regwidth, accesswidth) + else: + # bypass cancels out. Behaves like a normal reg + self.process_reg(node) + else: + self.process_buffered_reg(node, regwidth, accesswidth) + elif accesswidth < regwidth: + self.process_wide_reg(node, accesswidth) + else: + self.process_reg(node) + + return WalkerAction.SkipDescendants + + def process_external_reg(self, node: RegNode) -> None: + strb = self.exp.hwif.get_external_rd_ack(node) + data = self.exp.hwif.get_external_rd_data(node) + regwidth = node.get_property('regwidth') + if regwidth < self.exp.cpuif.data_width: + self.add_content(f"assign readback_array[{self.current_offset_str}][{self.exp.cpuif.data_width-1}:{regwidth}] = '0;") + self.add_content(f"assign readback_array[{self.current_offset_str}][{regwidth-1}:0] = {strb} ? {data} : '0;") + else: + self.add_content(f"assign readback_array[{self.current_offset_str}] = {strb} ? {data} : '0;") + + self.current_offset += 1 + + def process_reg(self, node: RegNode) -> None: + current_bit = 0 + rd_strb = f"({self.exp.dereferencer.get_access_strobe(node)} && !decoded_req_is_wr)" + # Fields are sorted by ascending low bit + for field in node.fields(): + if not field.is_sw_readable: + continue + + # insert reserved assignment before this field if needed + if field.low != current_bit: + self.add_content(f"assign readback_array[{self.current_offset_str}][{field.low-1}:{current_bit}] = '0;") + + value = self.exp.dereferencer.get_value(field) + if field.msb < field.lsb: + # Field gets bitswapped since it is in [low:high] orientation + value = do_bitswap(value) + + self.add_content(f"assign readback_array[{self.current_offset_str}][{field.high}:{field.low}] = {rd_strb} ? {value} : '0;") + + current_bit = field.high + 1 + + # Insert final reserved assignment if needed + bus_width = self.exp.cpuif.data_width + if current_bit < bus_width: + self.add_content(f"assign readback_array[{self.current_offset_str}][{bus_width-1}:{current_bit}] = '0;") + + self.current_offset += 1 + + + def process_buffered_reg(self, node: RegNode, regwidth: int, accesswidth: int) -> None: + rbuf = self.exp.read_buffering.get_rbuf_data(node) + + if accesswidth < regwidth: + # Is wide reg + n_subwords = regwidth // accesswidth + astrb = self.exp.dereferencer.get_access_strobe(node, reduce_substrobes=False) + for i in range(n_subwords): + rd_strb = f"({astrb}[{i}] && !decoded_req_is_wr)" + bslice = f"[{(i + 1) * accesswidth - 1}:{i*accesswidth}]" + self.add_content(f"assign readback_array[{self.current_offset_str}] = {rd_strb} ? {rbuf}{bslice} : '0;") + self.current_offset += 1 + + else: + # Is regular reg + rd_strb = f"({self.exp.dereferencer.get_access_strobe(node)} && !decoded_req_is_wr)" + self.add_content(f"assign readback_array[{self.current_offset_str}][{regwidth-1}:0] = {rd_strb} ? {rbuf} : '0;") + + bus_width = self.exp.cpuif.data_width + if regwidth < bus_width: + self.add_content(f"assign readback_array[{self.current_offset_str}][{bus_width-1}:{regwidth}] = '0;") + + self.current_offset += 1 + + + def process_buffered_reg_with_bypass(self, node: RegNode, regwidth: int, accesswidth: int) -> None: + """ + Special case for a buffered register when the register is its own trigger. + First sub-word shall bypass the read buffer and assign directly. + Subsequent subwords assign from the buffer. + Caller guarantees this is a wide reg + """ + astrb = self.exp.dereferencer.get_access_strobe(node, reduce_substrobes=False) + + # Generate assignments for first sub-word + bidx = 0 + rd_strb = f"({astrb}[0] && !decoded_req_is_wr)" + for field in node.fields(): + if not field.is_sw_readable: + continue + + if field.low >= accesswidth: + # field is not in this subword. + break + + if bidx < field.low: + # insert padding before + self.add_content(f"assign readback_array[{self.current_offset_str}][{field.low - 1}:{bidx}] = '0;") + + if field.high >= accesswidth: + # field gets truncated + r_low = field.low + r_high = accesswidth - 1 + f_low = 0 + f_high = accesswidth - 1 - field.low + + if field.msb < field.lsb: + # Field gets bitswapped since it is in [low:high] orientation + # Mirror the low/high indexes + f_low = field.width - 1 - f_low + f_high = field.width - 1 - f_high + f_low, f_high = f_high, f_low + value = do_bitswap(do_slice(self.exp.dereferencer.get_value(field), f_high, f_low)) + else: + value = do_slice(self.exp.dereferencer.get_value(field), f_high, f_low) + + self.add_content(f"assign readback_array[{self.current_offset_str}][{r_high}:{r_low}] = {rd_strb} ? {value} : '0;") + bidx = accesswidth + else: + # field fits in subword + value = self.exp.dereferencer.get_value(field) + if field.msb < field.lsb: + # Field gets bitswapped since it is in [low:high] orientation + value = do_bitswap(value) + self.add_content(f"assign readback_array[{self.current_offset_str}][{field.high}:{field.low}] = {rd_strb} ? {value} : '0;") + bidx = field.high + 1 + + # pad up remainder of subword + if bidx < accesswidth: + self.add_content(f"assign readback_array[{self.current_offset_str}][{accesswidth-1}:{bidx}] = '0;") + self.current_offset += 1 + + # Assign remainder of subwords from read buffer + n_subwords = regwidth // accesswidth + rbuf = self.exp.read_buffering.get_rbuf_data(node) + for i in range(1, n_subwords): + rd_strb = f"({astrb}[{i}] && !decoded_req_is_wr)" + bslice = f"[{(i + 1) * accesswidth - 1}:{i*accesswidth}]" + self.add_content(f"assign readback_array[{self.current_offset_str}] = {rd_strb} ? {rbuf}{bslice} : '0;") + self.current_offset += 1 + + def process_wide_reg(self, node: RegNode, accesswidth: int) -> None: + bus_width = self.exp.cpuif.data_width + + subword_idx = 0 + current_bit = 0 # Bit-offset within the wide register + access_strb = self.exp.dereferencer.get_access_strobe(node, reduce_substrobes=False) + # Fields are sorted by ascending low bit + for field in node.fields(): + if not field.is_sw_readable: + continue + + # insert zero assignment before this field if needed + if field.low >= accesswidth*(subword_idx+1): + # field does not start in this subword + if current_bit > accesswidth * subword_idx: + # current subword had content. Assign remainder + low = current_bit % accesswidth + high = bus_width - 1 + self.add_content(f"assign readback_array[{self.current_offset_str}][{high}:{low}] = '0;") + self.current_offset += 1 + + # Advance to subword that contains the start of the field + subword_idx = field.low // accesswidth + current_bit = accesswidth * subword_idx + + if current_bit != field.low: + # assign zero up to start of this field + low = current_bit % accesswidth + high = (field.low % accesswidth) - 1 + self.add_content(f"assign readback_array[{self.current_offset_str}][{high}:{low}] = '0;") + current_bit = field.low + + + # Assign field + # loop until the entire field's assignments have been generated + field_pos = field.low + while current_bit <= field.high: + # Assign the field + rd_strb = f"({access_strb}[{subword_idx}] && !decoded_req_is_wr)" + if (field_pos == field.low) and (field.high < accesswidth*(subword_idx+1)): + # entire field fits into this subword + low = field.low - accesswidth * subword_idx + high = field.high - accesswidth * subword_idx + + value = self.exp.dereferencer.get_value(field) + if field.msb < field.lsb: + # Field gets bitswapped since it is in [low:high] orientation + value = do_bitswap(value) + + self.add_content(f"assign readback_array[{self.current_offset_str}][{high}:{low}] = {rd_strb} ? {value} : '0;") + + current_bit = field.high + 1 + + if current_bit == accesswidth*(subword_idx+1): + # Field ends at the subword boundary + subword_idx += 1 + self.current_offset += 1 + elif field.high >= accesswidth*(subword_idx+1): + # only a subset of the field can fit into this subword + # high end gets truncated + + # assignment slice + r_low = field_pos - accesswidth * subword_idx + r_high = accesswidth - 1 + + # field slice + f_low = field_pos - field.low + f_high = accesswidth * (subword_idx + 1) - 1 - field.low + + if field.msb < field.lsb: + # Field gets bitswapped since it is in [low:high] orientation + # Mirror the low/high indexes + f_low = field.width - 1 - f_low + f_high = field.width - 1 - f_high + f_low, f_high = f_high, f_low + + value = do_bitswap(do_slice(self.exp.dereferencer.get_value(field), f_high, f_low)) + else: + value = do_slice(self.exp.dereferencer.get_value(field), f_high, f_low) + + self.add_content(f"assign readback_array[{self.current_offset_str}][{r_high}:{r_low}] = {rd_strb} ? {value} : '0;") + + # advance to the next subword + subword_idx += 1 + current_bit = accesswidth * subword_idx + field_pos = current_bit + self.current_offset += 1 + else: + # only a subset of the field can fit into this subword + # finish field + + # assignment slice + r_low = field_pos - accesswidth * subword_idx + r_high = field.high - accesswidth * subword_idx + + # field slice + f_low = field_pos - field.low + f_high = field.high - field.low + + if field.msb < field.lsb: + # Field gets bitswapped since it is in [low:high] orientation + # Mirror the low/high indexes + f_low = field.width - 1 - f_low + f_high = field.width - 1 - f_high + f_low, f_high = f_high, f_low + + value = do_bitswap(do_slice(self.exp.dereferencer.get_value(field), f_high, f_low)) + else: + value = do_slice(self.exp.dereferencer.get_value(field), f_high, f_low) + + self.add_content(f"assign readback_array[{self.current_offset_str}][{r_high}:{r_low}] = {rd_strb} ? {value} : '0;") + + current_bit = field.high + 1 + if current_bit == accesswidth*(subword_idx+1): + # Field ends at the subword boundary + subword_idx += 1 + self.current_offset += 1 + + # insert zero assignment after the last field if needed + if current_bit > accesswidth * subword_idx: + # current subword had content. Assign remainder + low = current_bit % accesswidth + high = bus_width - 1 + self.add_content(f"assign readback_array[{self.current_offset_str}][{high}:{low}] = '0;") + self.current_offset += 1 diff --git a/src/peakrdl_regblock/readback/templates/readback.sv b/src/peakrdl_regblock/readback/templates/readback.sv new file mode 100644 index 0000000..e44c9ed --- /dev/null +++ b/src/peakrdl_regblock/readback/templates/readback.sv @@ -0,0 +1,79 @@ +{% if array_assignments is not none %} +// Assign readback values to a flattened array +logic [{{cpuif.data_width-1}}:0] readback_array[{{array_size}}]; +{{array_assignments}} + + +{%- if ds.retime_read_fanin %} + +// fanin stage +logic [{{cpuif.data_width-1}}:0] readback_array_c[{{fanin_array_size}}]; +for(genvar g=0; g<{{fanin_loop_iter}}; g++) begin + always_comb begin + automatic logic [{{cpuif.data_width-1}}:0] readback_data_var; + readback_data_var = '0; + for(int i=g*{{fanin_stride}}; i<((g+1)*{{fanin_stride}}); i++) readback_data_var |= readback_array[i]; + readback_array_c[g] = readback_data_var; + end +end +{%- if fanin_residual_stride == 1 %} +assign readback_array_c[{{fanin_array_size-1}}] = readback_array[{{array_size-1}}]; +{%- elif fanin_residual_stride > 1 %} +always_comb begin + automatic logic [{{cpuif.data_width-1}}:0] readback_data_var; + readback_data_var = '0; + for(int i={{(fanin_array_size-1) * fanin_stride}}; i<{{array_size}}; i++) readback_data_var |= readback_array[i]; + readback_array_c[{{fanin_array_size-1}}] = readback_data_var; +end +{%- endif %} + +logic [{{cpuif.data_width-1}}:0] readback_array_r[{{fanin_array_size}}]; +logic readback_done_r; +always_ff {{get_always_ff_event(cpuif.reset)}} begin + if({{get_resetsignal(cpuif.reset)}}) begin + for(int i=0; i<{{fanin_array_size}}; i++) readback_array_r[i] <= '0; + readback_done_r <= '0; + end else begin + readback_array_r <= readback_array_c; + {%- if ds.has_external_addressable %} + readback_done_r <= decoded_req & ~decoded_req_is_wr & ~decoded_strb_is_external; + {%- else %} + readback_done_r <= decoded_req & ~decoded_req_is_wr; + {%- endif %} + end +end + +// Reduce the array +always_comb begin + automatic logic [{{cpuif.data_width-1}}:0] readback_data_var; + readback_done = readback_done_r; + readback_err = '0; + readback_data_var = '0; + for(int i=0; i<{{fanin_array_size}}; i++) readback_data_var |= readback_array_r[i]; + readback_data = readback_data_var; +end + +{%- else %} + +// Reduce the array +always_comb begin + automatic logic [{{cpuif.data_width-1}}:0] readback_data_var; + {%- if ds.has_external_addressable %} + readback_done = decoded_req & ~decoded_req_is_wr & ~decoded_strb_is_external; + {%- else %} + readback_done = decoded_req & ~decoded_req_is_wr; + {%- endif %} + readback_err = '0; + readback_data_var = '0; + for(int i=0; i<{{array_size}}; i++) readback_data_var |= readback_array[i]; + readback_data = readback_data_var; +end +{%- endif %} + + + +{%- else %} +assign readback_done = decoded_req & ~decoded_req_is_wr; +assign readback_data = '0; +assign readback_err = '0; +{% endif %} diff --git a/src/peakrdl_regblock/scan_design.py b/src/peakrdl_regblock/scan_design.py new file mode 100644 index 0000000..c3bea91 --- /dev/null +++ b/src/peakrdl_regblock/scan_design.py @@ -0,0 +1,119 @@ +from typing import TYPE_CHECKING, Optional + +from systemrdl.walker import RDLListener, RDLWalker, WalkerAction +from systemrdl.node import SignalNode, RegNode + +if TYPE_CHECKING: + from systemrdl.node import Node, FieldNode, AddressableNode, AddrmapNode + from .exporter import DesignState + + +class DesignScanner(RDLListener): + """ + Scans through the register model and validates that any unsupported features + are not present. + + Also collects any information that is required prior to the start of the export process. + """ + def __init__(self, ds:'DesignState') -> None: + self.ds = ds + self.msg = self.top_node.env.msg + + @property + def top_node(self) -> 'AddrmapNode': + return self.ds.top_node + + def _get_out_of_hier_field_reset(self) -> None: + current_node: Optional[Node] + current_node = self.top_node.parent + while current_node is not None: + for signal in current_node.signals(): + if signal.get_property('field_reset'): + path = signal.get_path() + self.ds.out_of_hier_signals[path] = signal + return + current_node = current_node.parent + + def do_scan(self) -> None: + # Collect cpuif reset, if any. + cpuif_reset = self.top_node.cpuif_reset + if cpuif_reset is not None: + path = cpuif_reset.get_path() + rel_path = cpuif_reset.get_rel_path(self.top_node) + if rel_path.startswith("^"): + self.ds.out_of_hier_signals[path] = cpuif_reset + else: + self.ds.in_hier_signal_paths.add(path) + + # collect out-of-hier field_reset, if any + self._get_out_of_hier_field_reset() + + # Ensure addrmap is not a bridge. This concept does not make sense for + # terminal components. + if self.top_node.get_property('bridge'): + self.msg.error( + "Regblock generator does not support exporting bridge address maps", + self.top_node.inst.property_src_ref.get('bridge', self.top_node.inst.inst_src_ref) + ) + + RDLWalker().walk(self.top_node, self) + if self.msg.had_error: + self.msg.fatal( + "Unable to export due to previous errors" + ) + + def enter_Component(self, node: 'Node') -> Optional[WalkerAction]: + if node.external and (node != self.top_node): + # Do not inspect external components. None of my business + return WalkerAction.SkipDescendants + + # Collect any signals that are referenced by a property + for prop_name in node.list_properties(): + value = node.get_property(prop_name) + if isinstance(value, SignalNode): + path = value.get_path() + rel_path = value.get_rel_path(self.top_node) + if rel_path.startswith("^"): + self.ds.out_of_hier_signals[path] = value + else: + self.ds.in_hier_signal_paths.add(path) + + if prop_name == "encode": + if value not in self.ds.user_enums: + self.ds.user_enums.append(value) + + return WalkerAction.Continue + + def enter_AddressableComponent(self, node: 'AddressableNode') -> None: + if node.external and node != self.top_node: + self.ds.has_external_addressable = True + if not isinstance(node, RegNode): + self.ds.has_external_block = True + + def enter_Reg(self, node: 'RegNode') -> None: + # The CPUIF's bus width is sized according to the largest accesswidth in the design + accesswidth = node.get_property('accesswidth') + self.ds.cpuif_data_width = max(self.ds.cpuif_data_width, accesswidth) + + self.ds.has_buffered_write_regs = self.ds.has_buffered_write_regs or bool(node.get_property('buffer_writes')) + self.ds.has_buffered_read_regs = self.ds.has_buffered_read_regs or bool(node.get_property('buffer_reads')) + + def enter_Signal(self, node: 'SignalNode') -> None: + if node.get_property('field_reset'): + path = node.get_path() + self.ds.in_hier_signal_paths.add(path) + + def enter_Field(self, node: 'FieldNode') -> None: + if node.is_sw_writable and (node.msb < node.lsb): + self.ds.has_writable_msb0_fields = True + + if node.get_property('paritycheck') and node.implements_storage: + self.ds.has_paritycheck = True + + if node.get_property('reset') is None: + self.msg.warning( + f"Field '{node.inst_name}' includes parity check logic, but " + "its reset value was not defined. Will result in an undefined " + "value on the module's 'parity_error' output.", + self.top_node.inst.property_src_ref.get('paritycheck', self.top_node.inst.inst_src_ref) + ) diff --git a/src/peakrdl_regblock/struct_generator.py b/src/peakrdl_regblock/struct_generator.py new file mode 100644 index 0000000..50dcf48 --- /dev/null +++ b/src/peakrdl_regblock/struct_generator.py @@ -0,0 +1,292 @@ +from typing import TYPE_CHECKING, Optional, List +import textwrap +from collections import OrderedDict + +from systemrdl.walker import RDLListener, RDLWalker, WalkerAction + +from .identifier_filter import kw_filter as kwf + +if TYPE_CHECKING: + from typing import Union + + from systemrdl.node import AddrmapNode, RegfileNode, RegNode, FieldNode, Node, MemNode + + +class _StructBase: + def __init__(self) -> None: + self.children = [] # type: List[Union[str, _StructBase]] + + def __str__(self) -> str: + s = '\n'.join((str(x) for x in self.children)) + return textwrap.indent(s, " ") + + +class _AnonymousStruct(_StructBase): + def __init__(self, inst_name: str, array_dimensions: Optional[List[int]] = None): + super().__init__() + self.inst_name = inst_name + self.array_dimensions = array_dimensions + + def __str__(self) -> str: + if self.array_dimensions: + suffix = "[" + "][".join((str(n) for n in self.array_dimensions)) + "]" + else: + suffix = "" + + return ( + "struct {\n" + + super().__str__() + + f"\n}} {self.inst_name}{suffix};" + ) + + +class _TypedefStruct(_StructBase): + def __init__(self, type_name: str, inst_name: Optional[str] = None, array_dimensions: Optional[List[int]] = None, packed: bool = False): + super().__init__() + self.type_name = type_name + self.inst_name = inst_name + self.array_dimensions = array_dimensions + self.packed = packed + + def __str__(self) -> str: + if self.packed: + return ( + "typedef struct packed {\n" + + super().__str__() + + f"\n}} {self.type_name};" + ) + else: + return ( + "typedef struct {\n" + + super().__str__() + + f"\n}} {self.type_name};" + ) + + @property + def instantiation(self) -> str: + if self.array_dimensions: + suffix = "[" + "][".join((str(n) for n in self.array_dimensions)) + "]" + else: + suffix = "" + + return f"{self.type_name} {self.inst_name}{suffix};" + +#------------------------------------------------------------------------------- + +class StructGenerator: + + def __init__(self) -> None: + self._struct_stack = [] # type: List[_StructBase] + + @property + def current_struct(self) -> _StructBase: + return self._struct_stack[-1] + + + def push_struct(self, inst_name: str, array_dimensions: Optional[List[int]] = None) -> None: + s = _AnonymousStruct(inst_name, array_dimensions) + self._struct_stack.append(s) + + + def add_member( + self, + name: str, + width: int = 1, + array_dimensions: Optional[List[int]] = None, + *, + lsb: int = 0, + signed: bool = False, + ) -> None: + if array_dimensions: + suffix = "[" + "][".join((str(n) for n in array_dimensions)) + "]" + else: + suffix = "" + + if signed: + sign = "signed " + else: + # the default 'logic' type is unsigned per SV LRM 6.11.3 + sign = "" + + if width == 1 and lsb == 0: + m = f"logic {sign}{name}{suffix};" + else: + m = f"logic {sign}[{lsb+width-1}:{lsb}] {name}{suffix};" + self.current_struct.children.append(m) + + + def pop_struct(self) -> None: + s = self._struct_stack.pop() + + if s.children: + # struct is not empty. Attach it to the parent + self.current_struct.children.append(s) + + + def start(self, type_name: str) -> None: + assert not self._struct_stack + s = _TypedefStruct(type_name) + self._struct_stack.append(s) + + def finish(self) -> Optional[str]: + s = self._struct_stack.pop() + assert not self._struct_stack + + if not s.children: + return None + return str(s) + + +class RDLStructGenerator(StructGenerator, RDLListener): + """ + Struct generator that naively translates an RDL node tree into a single + struct typedef containing nested anonymous structs + + This can be extended to add more intelligent behavior + """ + + def get_struct(self, node: 'Node', type_name: str) -> Optional[str]: + self.start(type_name) + + walker = RDLWalker() + walker.walk(node, self, skip_top=True) + + return self.finish() + + + def enter_Addrmap(self, node: 'AddrmapNode') -> Optional[WalkerAction]: + self.push_struct(kwf(node.inst_name), node.array_dimensions) + return WalkerAction.Continue + + def exit_Addrmap(self, node: 'AddrmapNode') -> Optional[WalkerAction]: + self.pop_struct() + return WalkerAction.Continue + + def enter_Regfile(self, node: 'RegfileNode') -> Optional[WalkerAction]: + self.push_struct(kwf(node.inst_name), node.array_dimensions) + return WalkerAction.Continue + + def exit_Regfile(self, node: 'RegfileNode') -> Optional[WalkerAction]: + self.pop_struct() + return WalkerAction.Continue + + def enter_Mem(self, node: 'MemNode') -> Optional[WalkerAction]: + self.push_struct(kwf(node.inst_name), node.array_dimensions) + return WalkerAction.Continue + + def exit_Mem(self, node: 'MemNode') -> Optional[WalkerAction]: + self.pop_struct() + return WalkerAction.Continue + + def enter_Reg(self, node: 'RegNode') -> Optional[WalkerAction]: + self.push_struct(kwf(node.inst_name), node.array_dimensions) + return WalkerAction.Continue + + def exit_Reg(self, node: 'RegNode') -> Optional[WalkerAction]: + self.pop_struct() + return WalkerAction.Continue + + def enter_Field(self, node: 'FieldNode') -> Optional[WalkerAction]: + self.add_member(kwf(node.inst_name), node.width) + return WalkerAction.Continue + +#------------------------------------------------------------------------------- + +class FlatStructGenerator(StructGenerator): + + def __init__(self) -> None: + super().__init__() + self.typedefs = OrderedDict() # type: OrderedDict[str, _TypedefStruct] + + def push_struct(self, type_name: str, inst_name: str, array_dimensions: Optional[List[int]] = None, packed = False) -> None: # type: ignore # pylint: disable=arguments-renamed + s = _TypedefStruct(type_name, inst_name, array_dimensions, packed) + self._struct_stack.append(s) + + def pop_struct(self) -> None: + s = self._struct_stack.pop() + assert isinstance(s, _TypedefStruct) + + if s.children: + # struct is not empty. Attach it to the parent + self.current_struct.children.append(s.instantiation) + + # Add to collection of struct definitions + if s.type_name not in self.typedefs: + self.typedefs[s.type_name] = s + + def finish(self) -> Optional[str]: + s = self._struct_stack.pop() + assert isinstance(s, _TypedefStruct) + assert not self._struct_stack + + # no children, no struct. + if not s.children: + return None + + # Add to collection of struct definitions + if s.type_name not in self.typedefs: + self.typedefs[s.type_name] = s + + all_structs = [str(s) for s in self.typedefs.values()] + + return "\n\n".join(all_structs) + + +class RDLFlatStructGenerator(FlatStructGenerator, RDLListener): + """ + Struct generator that naively translates an RDL node tree into a flat list + of typedefs + + This can be extended to add more intelligent behavior + """ + + def get_typdef_name(self, node:'Node') -> str: + raise NotImplementedError + + def get_struct(self, node: 'Node', type_name: str) -> Optional[str]: + self.start(type_name) + + walker = RDLWalker() + walker.walk(node, self, skip_top=True) + + return self.finish() + + def enter_Addrmap(self, node: 'AddrmapNode') -> Optional[WalkerAction]: + type_name = self.get_typdef_name(node) + self.push_struct(type_name, kwf(node.inst_name), node.array_dimensions) + return WalkerAction.Continue + + def exit_Addrmap(self, node: 'AddrmapNode') -> Optional[WalkerAction]: + self.pop_struct() + return WalkerAction.Continue + + def enter_Regfile(self, node: 'RegfileNode') -> Optional[WalkerAction]: + type_name = self.get_typdef_name(node) + self.push_struct(type_name, kwf(node.inst_name), node.array_dimensions) + return WalkerAction.Continue + + def exit_Regfile(self, node: 'RegfileNode') -> Optional[WalkerAction]: + self.pop_struct() + return WalkerAction.Continue + + def enter_Mem(self, node: 'MemNode') -> Optional[WalkerAction]: + type_name = self.get_typdef_name(node) + self.push_struct(type_name, kwf(node.inst_name), node.array_dimensions) + return WalkerAction.Continue + + def exit_Mem(self, node: 'MemNode') -> Optional[WalkerAction]: + self.pop_struct() + return WalkerAction.Continue + + def enter_Reg(self, node: 'RegNode') -> Optional[WalkerAction]: + type_name = self.get_typdef_name(node) + self.push_struct(type_name, kwf(node.inst_name), node.array_dimensions) + return WalkerAction.Continue + + def exit_Reg(self, node: 'RegNode') -> Optional[WalkerAction]: + self.pop_struct() + return WalkerAction.Continue + + def enter_Field(self, node: 'FieldNode') -> Optional[WalkerAction]: + self.add_member(kwf(node.inst_name), node.width) + return WalkerAction.Continue diff --git a/src/peakrdl_regblock/sv_int.py b/src/peakrdl_regblock/sv_int.py new file mode 100644 index 0000000..496eacf --- /dev/null +++ b/src/peakrdl_regblock/sv_int.py @@ -0,0 +1,17 @@ +from typing import Optional + +class SVInt: + def __init__(self, value: int, width: Optional[int] = None) -> None: + self.value = value + self.width = width + + def __str__(self) -> str: + if self.width is not None: + # Explicit width + return f"{self.width}'h{self.value:x}" + elif self.value.bit_length() > 32: + # SV standard only enforces that unsized literals shall be at least 32-bits + # To support larger literals, they need to be sized explicitly + return f"{self.value.bit_length()}'h{self.value:x}" + else: + return f"'h{self.value:x}" diff --git a/src/peakrdl_regblock/udps/__init__.py b/src/peakrdl_regblock/udps/__init__.py new file mode 100644 index 0000000..2eb9c20 --- /dev/null +++ b/src/peakrdl_regblock/udps/__init__.py @@ -0,0 +1,17 @@ +from .rw_buffering import BufferWrites, WBufferTrigger +from .rw_buffering import BufferReads, RBufferTrigger +from .extended_swacc import ReadSwacc, WriteSwacc +from .fixedpoint import IntWidth, FracWidth +from .signed import IsSigned + +ALL_UDPS = [ + BufferWrites, + WBufferTrigger, + BufferReads, + RBufferTrigger, + ReadSwacc, + WriteSwacc, + IntWidth, + FracWidth, + IsSigned, +] diff --git a/src/peakrdl_regblock/udps/extended_swacc.py b/src/peakrdl_regblock/udps/extended_swacc.py new file mode 100644 index 0000000..c982eda --- /dev/null +++ b/src/peakrdl_regblock/udps/extended_swacc.py @@ -0,0 +1,23 @@ +from typing import TYPE_CHECKING, Any + +from systemrdl.udp import UDPDefinition +from systemrdl.component import Field + +if TYPE_CHECKING: + from systemrdl.node import Node + +class ReadSwacc(UDPDefinition): + name = "rd_swacc" + valid_components = {Field} + valid_type = bool + + def get_unassigned_default(self, node: 'Node') -> Any: + return False + +class WriteSwacc(UDPDefinition): + name = "wr_swacc" + valid_components = {Field} + valid_type = bool + + def get_unassigned_default(self, node: 'Node') -> Any: + return False diff --git a/src/peakrdl_regblock/udps/fixedpoint.py b/src/peakrdl_regblock/udps/fixedpoint.py new file mode 100644 index 0000000..cf47534 --- /dev/null +++ b/src/peakrdl_regblock/udps/fixedpoint.py @@ -0,0 +1,73 @@ +from typing import Any + +from systemrdl.component import Field +from systemrdl.node import Node, FieldNode +from systemrdl.udp import UDPDefinition + + +class _FixedpointWidth(UDPDefinition): + valid_components = {Field} + valid_type = int + + def validate(self, node: "Node", value: Any) -> None: + assert isinstance(node, FieldNode) + + intwidth = node.get_property("intwidth") + fracwidth = node.get_property("fracwidth") + assert intwidth is not None + assert fracwidth is not None + prop_ref = node.inst.property_src_ref.get(self.name) + + # incompatible with "counter" fields + if node.get_property("counter"): + self.msg.error( + "Fixed-point representations are not supported for counter fields.", + prop_ref + ) + + # incompatible with "encode" fields + if node.get_property("encode") is not None: + self.msg.error( + "Fixed-point representations are not supported for fields encoded as an enum.", + prop_ref + ) + + # ensure node width = fracwidth + intwidth + if intwidth + fracwidth != node.width: + self.msg.error( + f"Number of integer bits ({intwidth}) plus number of fractional bits ({fracwidth})" + f" must be equal to the width of the component ({node.width}).", + prop_ref + ) + + +class IntWidth(_FixedpointWidth): + name = "intwidth" + + def get_unassigned_default(self, node: "Node") -> Any: + """ + If 'fracwidth' is defined, 'intwidth' is inferred from the node width. + """ + assert isinstance(node, FieldNode) + fracwidth = node.get_property("fracwidth", default=None) + if fracwidth is not None: + return node.width - fracwidth + else: + # not a fixed-point number + return None + + +class FracWidth(_FixedpointWidth): + name = "fracwidth" + + def get_unassigned_default(self, node: "Node") -> Any: + """ + If 'intwidth' is defined, 'fracwidth' is inferred from the node width. + """ + assert isinstance(node, FieldNode) + intwidth = node.get_property("intwidth", default=None) + if intwidth is not None: + return node.width - intwidth + else: + # not a fixed-point number + return None diff --git a/src/peakrdl_regblock/udps/rw_buffering.py b/src/peakrdl_regblock/udps/rw_buffering.py new file mode 100644 index 0000000..37ba6ce --- /dev/null +++ b/src/peakrdl_regblock/udps/rw_buffering.py @@ -0,0 +1,130 @@ +from typing import Any + +from systemrdl.udp import UDPDefinition +from systemrdl.component import Reg +from systemrdl.rdltypes.references import RefType, PropertyReference +from systemrdl.rdltypes import NoValue +from systemrdl.node import Node, RegNode, VectorNode, SignalNode, FieldNode + + +class xBufferTrigger(UDPDefinition): + valid_components = {Reg} + valid_type = RefType + + def validate(self, node: Node, value: Any) -> None: + # TODO: Reference shall not cross an internal/external boundary + + if value is NoValue: + self.msg.error( + "Double-buffer trigger property is missing a value assignment", + self.get_src_ref(node) + ) + elif isinstance(value, VectorNode): + # Trigger can reference a vector, but only if it is a single-bit + if value.width != 1: + self.msg.error( + "%s '%s' references %s '%s' but its width is not 1" + % ( + type(node.inst).__name__.lower(), node.inst_name, + type(value.inst).__name__.lower(), value.inst_name + ), + self.get_src_ref(node) + ) + + if isinstance(value, SignalNode): + if not value.get_property('activehigh') and not value.get_property('activelow'): + self.msg.error( + "Trigger was asigned a signal, but it does not specify whether it is activehigh/activelow", + self.get_src_ref(node) + ) + + elif isinstance(value, PropertyReference) and value.width is not None: + # Trigger can reference a property, but only if it is a single-bit + if value.width != 1: + self.msg.error( + "%s '%s' references property '%s->%s' but its width is not 1" + % ( + type(node.inst).__name__.lower(), node.inst_name, + value.node.inst_name, value.name, + ), + self.get_src_ref(node) + ) + elif isinstance(value, RegNode): + # Trigger can reference a register, which implies access of the + # 'correct half' of the register is the trigger. + # For buffered writes, it is the upper-half. + # For buffered reads, it is the lower-half. + pass + else: + # All other reference types are invalid + self.msg.error( + "Reference to a %s component is incompatible with the '%s' property." + % (type(node.inst).__name__.lower(), self.name), + self.get_src_ref(node) + ) + +#------------------------------------------------------------------------------- +class BufferWrites(UDPDefinition): + name = "buffer_writes" + valid_components = {Reg} + valid_type = bool + + def validate(self, node: 'Node', value: Any) -> None: + assert isinstance(node, RegNode) + if value: + if not node.has_sw_writable: + self.msg.error( + "'buffer_writes' is set to true, but this register does not contain any writable fields.", + self.get_src_ref(node) + ) + + def get_unassigned_default(self, node: 'Node') -> Any: + return False + + +class WBufferTrigger(xBufferTrigger): + name = "wbuffer_trigger" + + def get_unassigned_default(self, node: 'Node') -> Any: + # If buffering is enabled, trigger is the register itself + if node.get_property('buffer_writes'): + return node + return None + + def validate(self, node: Node, value: Any) -> None: + super().validate(node, value) + + if isinstance(value, FieldNode): + if value.parent == node: + self.msg.error( + "Trigger for a write-buffered register cannot be a field " + "within the same register since the buffering makes it impossible to trigger." + ) + + +class BufferReads(UDPDefinition): + name = "buffer_reads" + valid_components = {Reg} + valid_type = bool + + def validate(self, node: 'Node', value: Any) -> None: + assert isinstance(node, RegNode) + if value: + if not node.has_sw_readable: + self.msg.error( + "'buffer_reads' is set to true, but this register does not contain any readable fields.", + self.get_src_ref(node) + ) + + def get_unassigned_default(self, node: 'Node') -> Any: + return False + + +class RBufferTrigger(xBufferTrigger): + name = "rbuffer_trigger" + + def get_unassigned_default(self, node: 'Node') -> Any: + # If buffering is enabled, trigger is the register itself + if node.get_property('buffer_reads'): + return node + return None diff --git a/src/peakrdl_regblock/udps/signed.py b/src/peakrdl_regblock/udps/signed.py new file mode 100644 index 0000000..b342986 --- /dev/null +++ b/src/peakrdl_regblock/udps/signed.py @@ -0,0 +1,33 @@ +from typing import Any + +from systemrdl.component import Field +from systemrdl.node import Node +from systemrdl.udp import UDPDefinition + + +class IsSigned(UDPDefinition): + name = "is_signed" + valid_components = {Field} + valid_type = bool + default_assignment = True + + def validate(self, node: "Node", value: Any) -> None: + # "counter" fields can not be signed + if value and node.get_property("counter"): + self.msg.error( + "The property is_signed=true is not supported for counter fields.", + node.inst.property_src_ref["is_signed"] + ) + + # incompatible with "encode" fields + if value and node.get_property("encode") is not None: + self.msg.error( + "The property is_signed=true is not supported for fields encoded as an enum.", + node.inst.property_src_ref["is_signed"] + ) + + def get_unassigned_default(self, node: "Node") -> Any: + """ + Unsigned by default if not specified. + """ + return False diff --git a/src/peakrdl_regblock/utils.py b/src/peakrdl_regblock/utils.py new file mode 100644 index 0000000..b9d7a7f --- /dev/null +++ b/src/peakrdl_regblock/utils.py @@ -0,0 +1,104 @@ +import re +from typing import Match, Union, Optional + +from systemrdl.rdltypes.references import PropertyReference +from systemrdl.node import Node, AddrmapNode + +from .identifier_filter import kw_filter as kwf +from .sv_int import SVInt + +def get_indexed_path(top_node: Node, target_node: Node) -> str: + """ + TODO: Add words about indexing and why i'm doing this. Copy from logbook + """ + path = target_node.get_rel_path(top_node, empty_array_suffix="[!]") + + # replace unknown indexes with incrementing iterators i0, i1, ... + class ReplaceUnknown: + def __init__(self) -> None: + self.i = 0 + def __call__(self, match: Match) -> str: + s = f'i{self.i}' + self.i += 1 + return s + path = re.sub(r'!', ReplaceUnknown(), path) + + # Sanitize any SV keywords + def kw_filter_repl(m: Match) -> str: + return kwf(m.group(0)) + path = re.sub(r'\w+', kw_filter_repl, path) + + return path + +def clog2(n: int) -> int: + return (n-1).bit_length() + +def is_pow2(x: int) -> bool: + return (x > 0) and ((x & (x - 1)) == 0) + +def roundup_pow2(x: int) -> int: + return 1<<(x-1).bit_length() + +def ref_is_internal(top_node: AddrmapNode, ref: Union[Node, PropertyReference]) -> bool: + """ + Determine whether the reference is internal to the top node. + + For the sake of this exporter, root signals are treated as internal. + """ + current_node: Optional[Node] + if isinstance(ref, Node): + current_node = ref + elif isinstance(ref, PropertyReference): + current_node = ref.node + else: + raise RuntimeError + + while current_node is not None: + if current_node == top_node: + # reached top node without finding any external components + # is internal! + return True + + if current_node.external: + # not internal! + return False + + current_node = current_node.parent + + # A root signal was referenced, which dodged the top addrmap + # This is considered internal for this exporter + return True + + +def do_slice(value: Union[SVInt, str], high: int, low: int) -> Union[SVInt, str]: + if isinstance(value, str): + # If string, assume this is an identifier. Append bit-slice + if high == low: + return f"{value}[{low}]" + else: + return f"{value}[{high}:{low}]" + else: + # it is an SVInt literal. Slice it down + mask = (1 << (high + 1)) - 1 + v = (value.value & mask) >> low + + if value.width is not None: + w = high - low + 1 + else: + w = None + + return SVInt(v, w) + +def do_bitswap(value: Union[SVInt, str]) -> Union[SVInt, str]: + if isinstance(value, str): + # If string, assume this is an identifier. Wrap in a streaming operator + return "{<<{" + value + "}}" + else: + # it is an SVInt literal. bitswap it + assert value.width is not None # width must be known! + v = value.value + vswap = 0 + for _ in range(value.width): + vswap = (vswap << 1) + (v & 1) + v >>= 1 + return SVInt(vswap, value.width) diff --git a/src/peakrdl_regblock/validate_design.py b/src/peakrdl_regblock/validate_design.py new file mode 100644 index 0000000..7b8e9f8 --- /dev/null +++ b/src/peakrdl_regblock/validate_design.py @@ -0,0 +1,207 @@ +from typing import TYPE_CHECKING, Optional, List, Union + +from systemrdl.walker import RDLListener, RDLWalker, WalkerAction +from systemrdl.rdltypes import PropertyReference +from systemrdl.node import Node, RegNode, FieldNode, SignalNode, AddressableNode +from systemrdl.node import RegfileNode, AddrmapNode + +from .utils import roundup_pow2, is_pow2 + +from .utils import ref_is_internal + +if TYPE_CHECKING: + from .exporter import RegblockExporter + +class DesignValidator(RDLListener): + """ + Performs additional rule-checks on the design that check for limitations + imposed by this exporter. + """ + def __init__(self, exp:'RegblockExporter') -> None: + self.exp = exp + self.ds = exp.ds + self.msg = self.top_node.env.msg + + self._contains_external_block_stack = [] # type: List[bool] + self.contains_external_block = False + + @property + def top_node(self) -> 'AddrmapNode': + return self.exp.ds.top_node + + def do_validate(self) -> None: + RDLWalker().walk(self.top_node, self) + if self.msg.had_error: + self.msg.fatal( + "Unable to export due to previous errors" + ) + + def enter_Component(self, node: 'Node') -> Optional[WalkerAction]: + if node.external and (node != self.top_node): + # Do not inspect external components. None of my business + return WalkerAction.SkipDescendants + + # Check if any property references reach across the internal/external boundary + for prop_name in node.list_properties(): + value = node.get_property(prop_name) + if isinstance(value, (PropertyReference, Node)): + if not ref_is_internal(self.top_node, value): + if isinstance(value, PropertyReference): + src_ref = value.src_ref + else: + src_ref = node.inst.property_src_ref.get(prop_name, node.inst.inst_src_ref) + self.msg.error( + "Property is assigned a reference that points to a component not internal to the regblock being exported.", + src_ref + ) + return None + + def enter_Signal(self, node: 'SignalNode') -> None: + # If encountering a CPUIF reset that is nested within the register model, + # warn that it will be ignored. + # Only cpuif resets in the top-level node or above will be honored + if node.get_property('cpuif_reset') and (node.parent != self.top_node): + self.msg.warning( + "Only cpuif_reset signals that are instantiated in the top-level " + "addrmap or above will be honored. Any cpuif_reset signals nested " + "within children of the addrmap being exported will be ignored.", + node.inst.inst_src_ref + ) + + def enter_AddressableComponent(self, node: 'AddressableNode') -> None: + # All registers must be aligned to the internal data bus width + alignment = self.exp.cpuif.data_width_bytes + if (node.raw_address_offset % alignment) != 0: + self.msg.error( + "Unaligned registers are not supported. Address offset of " + f"instance '{node.inst_name}' must be a multiple of {alignment}", + node.inst.inst_src_ref + ) + if node.is_array and (node.array_stride % alignment) != 0: # type: ignore # is_array implies stride is not none + self.msg.error( + "Unaligned registers are not supported. Address stride of " + f"instance array '{node.inst_name}' must be a multiple of {alignment}", + node.inst.inst_src_ref + ) + + if not isinstance(node, RegNode): + # Entering a block-like node + if node == self.top_node: + # Ignore top addrmap's external property when entering + self._contains_external_block_stack.append(False) + else: + self._contains_external_block_stack.append(node.external) + + def enter_Regfile(self, node: RegfileNode) -> None: + self._check_sharedextbus(node) + + def enter_Addrmap(self, node: AddrmapNode) -> None: + self._check_sharedextbus(node) + + def _check_sharedextbus(self, node: Union[RegfileNode, AddrmapNode]) -> None: + if node.get_property('sharedextbus'): + self.msg.error( + "This exporter does not support enabling the 'sharedextbus' property yet.", + node.inst.property_src_ref.get('sharedextbus', node.inst.inst_src_ref) + ) + + def enter_Reg(self, node: 'RegNode') -> None: + # accesswidth of wide registers must be consistent within the register block + accesswidth = node.get_property('accesswidth') + regwidth = node.get_property('regwidth') + + if accesswidth < regwidth: + # register is 'wide' + if accesswidth != self.exp.cpuif.data_width: + self.msg.error( + f"Multi-word registers that have an accesswidth ({accesswidth}) " + "that are inconsistent with this regblock's CPU bus width " + f"({self.exp.cpuif.data_width}) are not supported.", + node.inst.inst_src_ref + ) + + + def enter_Field(self, node: 'FieldNode') -> None: + parent_accesswidth = node.parent.get_property('accesswidth') + parent_regwidth = node.parent.get_property('regwidth') + if ( + (parent_accesswidth < parent_regwidth) + and (node.lsb // parent_accesswidth) != (node.msb // parent_accesswidth) + ): + # field spans multiple sub-words + + if node.is_sw_writable and not node.parent.get_property('buffer_writes'): + # ... and is writable without the protection of double-buffering + # Enforce 10.6.1-f + self.msg.error( + f"Software-writable field '{node.inst_name}' shall not span" + " multiple software-accessible subwords. Consider enabling" + " write double-buffering.\n" + "For more details, see: https://peakrdl-regblock.readthedocs.io/en/latest/udps/write_buffering.html", + node.inst.inst_src_ref + ) + + if node.get_property('onread') is not None and not node.parent.get_property('buffer_reads'): + # ... is modified by an onread action without the atomicity of read buffering + # Enforce 10.6.1-f + self.msg.error( + f"The field '{node.inst_name}' spans multiple software-accessible" + " subwords and is modified on-read, making it impossible to" + " access its value correctly. Consider enabling read" + " double-buffering. \n" + "For more details, see: https://peakrdl-regblock.readthedocs.io/en/latest/udps/read_buffering.html", + node.inst.inst_src_ref + ) + + # Check for unsynthesizable reset + reset = node.get_property("reset") + if not (reset is None or isinstance(reset, int)): + # Has reset that is not a constant value + resetsignal = node.get_property("resetsignal") + if resetsignal: + is_async_reset = resetsignal.get_property("async") + else: + is_async_reset = self.ds.default_reset_async + + if is_async_reset: + self.msg.error( + "A field that uses an asynchronous reset cannot use a dynamic reset value. This is not synthesizable.", + node.inst.inst_src_ref + ) + + + def exit_AddressableComponent(self, node: AddressableNode) -> None: + if not isinstance(node, RegNode): + # Exiting block-like node + contains_external_block = self._contains_external_block_stack.pop() + + if self._contains_external_block_stack: + # Still in the design. Update stack + self._contains_external_block_stack[-1] |= contains_external_block + else: + # Exiting top addrmap. Resolve final answer + self.contains_external_block = contains_external_block + + if contains_external_block: + # Check that addressing follows strict alignment rules to allow + # for simplified address bit-pruning + if node.external: + err_suffix = "is external" + else: + err_suffix = "contains an external addrmap/regfile/mem" + + req_align = roundup_pow2(node.size) + if (node.raw_address_offset % req_align) != 0: + self.msg.error( + f"Address offset +0x{node.raw_address_offset:x} of instance '{node.inst_name}' is not a power of 2 multiple of its size 0x{node.size:x}. " + f"This is required by the regblock exporter if a component {err_suffix}.", + node.inst.inst_src_ref + ) + if node.is_array: + assert node.array_stride is not None + if not is_pow2(node.array_stride): + self.msg.error( + f"Address stride of instance array '{node.inst_name}' is not a power of 2" + f"This is required by the regblock exporter if a component {err_suffix}.", + node.inst.inst_src_ref + ) diff --git a/src/peakrdl_regblock/write_buffering/__init__.py b/src/peakrdl_regblock/write_buffering/__init__.py new file mode 100644 index 0000000..a03ddcb --- /dev/null +++ b/src/peakrdl_regblock/write_buffering/__init__.py @@ -0,0 +1,81 @@ +from typing import TYPE_CHECKING, Union + +from systemrdl.node import AddrmapNode, RegNode, FieldNode, SignalNode + +from .storage_generator import WBufStorageStructGenerator +from .implementation_generator import WBufLogicGenerator +from ..utils import get_indexed_path +from ..sv_int import SVInt + +if TYPE_CHECKING: + from ..exporter import RegblockExporter + + +class WriteBuffering: + def __init__(self, exp:'RegblockExporter'): + self.exp = exp + + @property + def top_node(self) -> 'AddrmapNode': + return self.exp.ds.top_node + + + def get_storage_struct(self) -> str: + struct_gen = WBufStorageStructGenerator(self) + s = struct_gen.get_struct(self.top_node, "wbuf_storage_t") + assert s is not None + return s + "\nwbuf_storage_t wbuf_storage;" + + + def get_implementation(self) -> str: + gen = WBufLogicGenerator(self) + s = gen.get_content(self.top_node) + assert s is not None + return s + + def get_wbuf_prefix(self, node: Union[RegNode, FieldNode]) -> str: + if isinstance(node, FieldNode): + node = node.parent + wbuf_prefix = "wbuf_storage." + get_indexed_path(self.top_node, node) + return wbuf_prefix + + def get_write_strobe(self, node: Union[RegNode, FieldNode]) -> str: + prefix = self.get_wbuf_prefix(node) + return f"{prefix}.pending && {self.get_trigger(node)}" + + def get_raw_trigger(self, node: 'RegNode') -> Union[SVInt, str]: + trigger = node.get_property('wbuffer_trigger') + + if isinstance(trigger, RegNode): + # Trigger is a register. + # trigger when uppermost address of the register is written + regwidth = trigger.get_property('regwidth') + accesswidth = trigger.get_property('accesswidth') + strb_prefix = self.exp.dereferencer.get_access_strobe(trigger, reduce_substrobes=False) + + if accesswidth < regwidth: + n_subwords = regwidth // accesswidth + return f"{strb_prefix}[{n_subwords-1}] && decoded_req_is_wr" + else: + return f"{strb_prefix} && decoded_req_is_wr" + elif isinstance(trigger, SignalNode): + s = self.exp.dereferencer.get_value(trigger) + if trigger.get_property('activehigh'): + return s + else: + return f"~{s}" + else: + # Trigger is a field or propref bit + return self.exp.dereferencer.get_value(trigger) + + def get_trigger(self, node: Union[RegNode, FieldNode]) -> Union[SVInt, str]: + if isinstance(node, FieldNode): + node = node.parent + trigger = node.get_property('wbuffer_trigger') + + if isinstance(trigger, RegNode) and trigger == node: + # register is its own trigger + # use the delayed trigger signal + return self.get_wbuf_prefix(node) + ".trigger_q" + else: + return self.get_raw_trigger(node) diff --git a/src/peakrdl_regblock/write_buffering/implementation_generator.py b/src/peakrdl_regblock/write_buffering/implementation_generator.py new file mode 100644 index 0000000..c2dad95 --- /dev/null +++ b/src/peakrdl_regblock/write_buffering/implementation_generator.py @@ -0,0 +1,59 @@ +from typing import TYPE_CHECKING +from collections import namedtuple + +from systemrdl.component import Reg +from systemrdl.node import RegNode + +from ..forloop_generator import RDLForLoopGenerator + +if TYPE_CHECKING: + from . import WriteBuffering + +class WBufLogicGenerator(RDLForLoopGenerator): + i_type = "genvar" + def __init__(self, wbuf: 'WriteBuffering') -> None: + super().__init__() + self.wbuf = wbuf + self.exp = wbuf.exp + self.template = self.exp.jj_env.get_template( + "write_buffering/template.sv" + ) + + def enter_Reg(self, node: 'RegNode') -> None: + super().enter_Reg(node) + assert isinstance(node.inst, Reg) + + if not node.get_property('buffer_writes'): + return + + regwidth = node.get_property('regwidth') + accesswidth = node.get_property('accesswidth') + strb_prefix = self.exp.dereferencer.get_access_strobe(node, reduce_substrobes=False) + Segment = namedtuple("Segment", ["strobe", "bslice"]) + segments = [] + if accesswidth < regwidth: + n_subwords = regwidth // accesswidth + for i in range(n_subwords): + strobe = strb_prefix + f"[{i}]" + if node.inst.is_msb0_order: + bslice = f"[{regwidth - (accesswidth * i) - 1}: {regwidth - (accesswidth * (i+1))}]" + else: + bslice = f"[{(accesswidth * (i + 1)) - 1}:{accesswidth * i}]" + segments.append(Segment(strobe, bslice)) + else: + segments.append(Segment(strb_prefix, "")) + + trigger = node.get_property('wbuffer_trigger') + is_own_trigger = (isinstance(trigger, RegNode) and trigger == node) + + context = { + 'wbuf': self.wbuf, + 'wbuf_prefix': self.wbuf.get_wbuf_prefix(node), + 'segments': segments, + 'node': node, + 'cpuif': self.exp.cpuif, + 'get_resetsignal': self.exp.dereferencer.get_resetsignal, + 'get_always_ff_event': self.exp.dereferencer.get_always_ff_event, + 'is_own_trigger': is_own_trigger, + } + self.add_content(self.template.render(context)) diff --git a/src/peakrdl_regblock/write_buffering/storage_generator.py b/src/peakrdl_regblock/write_buffering/storage_generator.py new file mode 100644 index 0000000..6b48472 --- /dev/null +++ b/src/peakrdl_regblock/write_buffering/storage_generator.py @@ -0,0 +1,32 @@ +from typing import TYPE_CHECKING + +from systemrdl.node import FieldNode, RegNode + +from ..struct_generator import RDLStructGenerator + +if TYPE_CHECKING: + from . import WriteBuffering + +class WBufStorageStructGenerator(RDLStructGenerator): + def __init__(self, wbuf: 'WriteBuffering') -> None: + super().__init__() + self.wbuf = wbuf + + def enter_Field(self, node: FieldNode) -> None: + # suppress parent class's field behavior + pass + + def enter_Reg(self, node: RegNode) -> None: + super().enter_Reg(node) + + if not node.get_property('buffer_writes'): + return + + regwidth = node.get_property('regwidth') + self.add_member("data", regwidth) + self.add_member("biten", regwidth) + self.add_member("pending") + + trigger = node.get_property('wbuffer_trigger') + if isinstance(trigger, RegNode) and trigger == node: + self.add_member("trigger_q") diff --git a/src/peakrdl_regblock/write_buffering/template.sv b/src/peakrdl_regblock/write_buffering/template.sv new file mode 100644 index 0000000..78bbf46 --- /dev/null +++ b/src/peakrdl_regblock/write_buffering/template.sv @@ -0,0 +1,31 @@ +always_ff {{get_always_ff_event(cpuif.reset)}} begin + if({{get_resetsignal(cpuif.reset)}}) begin + {{wbuf_prefix}}.pending <= '0; + {{wbuf_prefix}}.data <= '0; + {{wbuf_prefix}}.biten <= '0; + {%- if is_own_trigger %} + {{wbuf_prefix}}.trigger_q <= '0; + {%- endif %} + end else begin + if({{wbuf.get_trigger(node)}}) begin + {{wbuf_prefix}}.pending <= '0; + {{wbuf_prefix}}.data <= '0; + {{wbuf_prefix}}.biten <= '0; + end + {%- for segment in segments %} + if({{segment.strobe}} && decoded_req_is_wr) begin + {{wbuf_prefix}}.pending <= '1; + {%- if node.inst.is_msb0_order %} + {{wbuf_prefix}}.data{{segment.bslice}} <= ({{wbuf_prefix}}.data{{segment.bslice}} & ~decoded_wr_biten_bswap) | (decoded_wr_data_bswap & decoded_wr_biten_bswap); + {{wbuf_prefix}}.biten{{segment.bslice}} <= {{wbuf_prefix}}.biten{{segment.bslice}} | decoded_wr_biten_bswap; + {%- else %} + {{wbuf_prefix}}.data{{segment.bslice}} <= ({{wbuf_prefix}}.data{{segment.bslice}} & ~decoded_wr_biten) | (decoded_wr_data & decoded_wr_biten); + {{wbuf_prefix}}.biten{{segment.bslice}} <= {{wbuf_prefix}}.biten{{segment.bslice}} | decoded_wr_biten; + {%- endif %} + end + {%- endfor %} + {%- if is_own_trigger %} + {{wbuf_prefix}}.trigger_q <= {{wbuf.get_raw_trigger(node)}}; + {%- endif %} + end +end diff --git a/tests/.coveragerc b/tests/.coveragerc new file mode 100644 index 0000000..05e3012 --- /dev/null +++ b/tests/.coveragerc @@ -0,0 +1,22 @@ +[run] +branch = True +#relative_files = True + +omit = + # to be covered elsewhere + */__peakrdl__.py + +[paths] +source = + ../src/peakrdl_regblock/ + */site-packages/*/peakrdl_regblock + */site-packages/peakrdl_regblock + +[report] +exclude_lines = + pragma: no cover + raise RuntimeError + raise NotImplementedError + if TYPE_CHECKING: + +precision = 1 diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..bb3996c --- /dev/null +++ b/tests/README.md @@ -0,0 +1,119 @@ + +# Test Dependencies + +## Questa + +Testcases require an installation of the Questa simulator, and for `vlog` & `vsim` +commands to be visible via the PATH environment variable. + +*Questa - Intel FPGA Starter Edition* can be downloaded for free from Intel: +* Go to https://www.intel.com/content/www/us/en/collections/products/fpga/software/downloads.html?edition=pro&q=questa&s=Relevancy +* Select latest version of Questa +* Download Questa files. +* Install + * Be sure to choose "Starter Edition" for the free version. +* Create an account on https://licensing.intel.com + * press "Enroll" to register + * After you confirm your email, go back to this page and press "Enroll" again to finish enrollment +* Go to https://licensing.intel.com/psg/s/sales-signup-evaluationlicenses +* Generate a free *Starter Edition* license file for Questa + * Easiest to use a *fixed* license using your NIC ID (MAC address of your network card via `ifconfig`) +* Download the license file and point the `LM_LICENSE_FILE` environment variable to the folder which contains it. +* (optional) Delete Intel libraries to save some disk space + * Delete `/questa_fse/intel` + * Edit `/questa_fse/modelsim.ini` and remove lines that reference the `intel` libraries + + +## Vivado (optional) + +To run synthesis tests, Vivado needs to be installed and visible via the PATH environment variable. + +Vivado can be downloaded for free from: https://www.xilinx.com/support/download.html + + + +## Python Packages +Install dependencies required for running tests + +```bash +python3 -m pip install -r tests/requirements.txt +``` + + + +# Running tests + +Tests can be launched from the test directory using `pytest`. +Use `pytest --workers auto` to run tests in parallel. + +To run all tests: +```bash +python3 setup.py install +pytest tests +``` + +You can also run a specific testcase. For example: +```bash +pytest tests/test_hw_access +``` + +Command-line arguments can be used to explicitly select which simulator/synthesis tools are used +If unspecified, the tool will be selected automatically based on what you have installed. +```bash +pytest --sim-tool questa --synth-tool vivado +``` + + +Alternatively, launch tests using the helper script. This handles installing +dependencies into a virtual environment automatically. +```bash +cd tests +./run.sh +``` + + + +# Test organization + +The goal for this test infrastructure is to make it easy to add small-standalone +testcases, with minimal repetition/boilerplate code that is usually present in +SystemVerilog testbenches. + +To accomplish this, Jinja templates are used extensively to generate the +resulting `tb.sv` file, as well as assist in dynamic testcase parameterization. + + + +## CPU Interfaces +Each CPU interface type is described in its own folder as follows: + +`lib/cpuifs//__init__.py` +: Definitions for CPU Interface test mode classes. + +`lib/cpuifs//tb_inst.sv` +: Jinja template that defines how the CPU interface is declared & instantiated in the testbench file. + +`lib/cpuifs//*.sv` +: Any other files required for compilation. + + + +## Testcase +Each testcase group has its own folder and contains the following: + +`test_*/__init__.py` +: Empty file required for test discovery. + +`test_*/regblock.rdl` +: Testcase RDL file. Testcase infrastructure will automatically compile this and generate the regblock output SystemVerilog. + +`test_*/tb_template.sv` +: Jinja template that defines the testcase-specific sequence. + +`test_*/testcase.py` +: Defines Python unittest testcase entry point. + + + +## Parameterization +Testcase classes can be parameterized using the [parameterized](https://github.com/wolever/parameterized) extension. This allows the same testcase to be run against multiple permutations of regblock export modes such as CPU interfaces, retiming flop stages, or even RDL parameterizations. diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..13dcb5d --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,48 @@ +def pytest_addoption(parser): + parser.addoption( + "--sim-tool", + choices=["questa", "xsim", "stub", "skip", "auto"], + default="auto", + help=""" + Select the simulator to use. + + stub: run the testcase using a no-op simulator stub + skip: skip all the simulation tests + auto: choose the best simulator based on what is installed + """ + ) + + parser.addoption( + "--gui", + default=False, + action="store_true", + help=""", + Launch sim tool in GUI mode + + Only use this option when running a single test + """ + ) + + + parser.addoption( + "--rerun", + default=False, + action="store_true", + help=""", + Re-run simulation in-place without re-exporting regblock + + Useful if hand-editing a testcase interactively. + """ + ) + + parser.addoption( + "--synth-tool", + choices=["vivado", "skip", "auto"], + default="auto", + help=""" + Select the synthesis tool to use. + + skip: skip all the simulation tests + auto: choose the best tool based on what is installed + """ + ) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/lib/base_testcase.py b/tests/lib/base_testcase.py new file mode 100644 index 0000000..6c9d4b8 --- /dev/null +++ b/tests/lib/base_testcase.py @@ -0,0 +1,140 @@ +from typing import Optional +import unittest +import os +import glob +import shutil +import inspect +import pathlib + +import pytest +from systemrdl import RDLCompiler + +from peakrdl_regblock import RegblockExporter +from peakrdl_regblock.udps import ALL_UDPS + +from .cpuifs.base import CpuifTestMode +from .cpuifs.apb4 import APB4 + + +class BaseTestCase(unittest.TestCase): + #: Path to the testcase's RDL file. + #: Relative to the testcase's dir. If unset, the first RDL file found in the + #: testcase dir will be used + rdl_file = None # type: Optional[str] + + #: RDL type name to elaborate. If unset, compiler will automatically choose + #: the top. + rdl_elab_target = None # type: Optional[str] + + #: Parameters to pass into RDL elaboration + rdl_elab_params = {} + + #: Define what CPUIF to use for this testcase + cpuif = APB4() # type: CpuifTestMode + + # Other exporter args: + retime_read_fanin = False + retime_read_response = False + reuse_hwif_typedefs = True + retime_external = False + default_reset_activelow = False + default_reset_async = False + + #: this gets auto-loaded via the _load_request autouse fixture + request = None # type: pytest.FixtureRequest + + exporter = RegblockExporter() + + @pytest.fixture(autouse=True) + def _load_request(self, request): + self.request = request + + @property + def rerun(self) -> bool: + """ + Re-run without deleting and re-generating prior output directory. + """ + return self.request.config.getoption("--rerun") + + def get_testcase_dir(self) -> str: + class_dir = os.path.dirname(inspect.getfile(self.__class__)) + return class_dir + + def get_run_dir(self) -> str: + this_dir = self.get_testcase_dir() + run_dir = os.path.join(this_dir, "run.out", self.__class__.__name__) + return run_dir + + def _write_params(self) -> None: + """ + Write out the class parameters to a file so that it is easier to debug + how a testcase was parameterized + """ + path = os.path.join(self.get_run_dir(), "params.txt") + + with open(path, 'w') as f: + for k, v in self.__class__.__dict__.items(): + if k.startswith("_") or callable(v): + continue + f.write(f"{k}: {repr(v)}\n") + + + def export_regblock(self): + """ + Call the peakrdl_regblock exporter to generate the DUT + """ + this_dir = self.get_testcase_dir() + + if self.rdl_file: + rdl_file = os.path.join(this_dir, self.rdl_file) + else: + # Find any *.rdl file in testcase dir + rdl_file = glob.glob(os.path.join(this_dir, "*.rdl"))[0] + + rdlc = RDLCompiler() + + # Load the UDPs + for udp in ALL_UDPS: + rdlc.register_udp(udp) + # ... including the definition + udp_file = os.path.join(this_dir, "../../hdl-src/regblock_udps.rdl") + rdlc.compile_file(udp_file) + + rdlc.compile_file(rdl_file) + root = rdlc.elaborate(self.rdl_elab_target, "regblock", self.rdl_elab_params) + + self.exporter.export( + root, + self.get_run_dir(), + module_name="regblock", + package_name="regblock_pkg", + cpuif_cls=self.cpuif.cpuif_cls, + retime_read_fanin=self.retime_read_fanin, + retime_read_response=self.retime_read_response, + reuse_hwif_typedefs=self.reuse_hwif_typedefs, + retime_external_reg=self.retime_external, + retime_external_regfile=self.retime_external, + retime_external_mem=self.retime_external, + retime_external_addrmap=self.retime_external, + default_reset_activelow=self.default_reset_activelow, + default_reset_async=self.default_reset_async, + ) + + def delete_run_dir(self) -> None: + run_dir = self.get_run_dir() + if os.path.exists(run_dir): + shutil.rmtree(run_dir) + + def setUp(self) -> None: + if self.rerun: + return + + # Create fresh build dir + run_dir = self.get_run_dir() + self.delete_run_dir() + pathlib.Path(run_dir).mkdir(parents=True, exist_ok=True) + + self._write_params() + + # Convert testcase RDL file --> SV + self.export_regblock() diff --git a/tests/lib/cpuifs/__init__.py b/tests/lib/cpuifs/__init__.py new file mode 100644 index 0000000..6ea672d --- /dev/null +++ b/tests/lib/cpuifs/__init__.py @@ -0,0 +1,17 @@ +from .passthrough import Passthrough +from .apb3 import APB3, FlatAPB3 +from .apb4 import APB4, FlatAPB4 +from .axi4lite import AXI4Lite, FlatAXI4Lite +from .avalon import Avalon, FlatAvalon + +ALL_CPUIF = [ + Passthrough(), + APB3(), + FlatAPB3(), + APB4(), + FlatAPB4(), + AXI4Lite(), + FlatAXI4Lite(), + Avalon(), + FlatAvalon(), +] diff --git a/tests/lib/cpuifs/apb3/__init__.py b/tests/lib/cpuifs/apb3/__init__.py new file mode 100644 index 0000000..fffb098 --- /dev/null +++ b/tests/lib/cpuifs/apb3/__init__.py @@ -0,0 +1,18 @@ +from ..base import CpuifTestMode + +from peakrdl_regblock.cpuif.apb3 import APB3_Cpuif, APB3_Cpuif_flattened + +class APB3(CpuifTestMode): + cpuif_cls = APB3_Cpuif + rtl_files = [ + "../../../../hdl-src/apb3_intf.sv", + ] + tb_files = [ + "../../../../hdl-src/apb3_intf.sv", + "apb3_intf_driver.sv", + ] + tb_template = "tb_inst.sv" + +class FlatAPB3(APB3): + cpuif_cls = APB3_Cpuif_flattened + rtl_files = [] diff --git a/tests/lib/cpuifs/apb3/apb3_intf_driver.sv b/tests/lib/cpuifs/apb3/apb3_intf_driver.sv new file mode 100644 index 0000000..5533f27 --- /dev/null +++ b/tests/lib/cpuifs/apb3/apb3_intf_driver.sv @@ -0,0 +1,116 @@ +interface apb3_intf_driver #( + parameter DATA_WIDTH = 32, + parameter ADDR_WIDTH = 32 + )( + input wire clk, + input wire rst, + apb3_intf.master m_apb + ); + + timeunit 1ps; + timeprecision 1ps; + + logic PSEL; + logic PENABLE; + logic PWRITE; + logic [ADDR_WIDTH-1:0] PADDR; + logic [DATA_WIDTH-1:0] PWDATA; + logic [DATA_WIDTH-1:0] PRDATA; + logic PREADY; + logic PSLVERR; + + assign m_apb.PSEL = PSEL; + assign m_apb.PENABLE = PENABLE; + assign m_apb.PWRITE = PWRITE; + assign m_apb.PADDR = PADDR; + assign m_apb.PWDATA = PWDATA; + assign PRDATA = m_apb.PRDATA; + assign PREADY = m_apb.PREADY; + assign PSLVERR = m_apb.PSLVERR; + + default clocking cb @(posedge clk); + default input #1step output #1; + output PSEL; + output PENABLE; + output PWRITE; + output PADDR; + output PWDATA; + input PRDATA; + input PREADY; + input PSLVERR; + endclocking + + task automatic reset(); + cb.PSEL <= '0; + cb.PENABLE <= '0; + cb.PWRITE <= '0; + cb.PADDR <= '0; + cb.PWDATA <= '0; + endtask + + semaphore txn_mutex = new(1); + + task automatic write(logic [ADDR_WIDTH-1:0] addr, logic [DATA_WIDTH-1:0] data); + txn_mutex.get(); + ##0; + + // Initiate transfer + cb.PSEL <= '1; + cb.PENABLE <= '0; + cb.PWRITE <= '1; + cb.PADDR <= addr; + cb.PWDATA <= data; + @(cb); + + // active phase + cb.PENABLE <= '1; + @(cb); + + // Wait for response + while(cb.PREADY !== 1'b1) @(cb); + reset(); + txn_mutex.put(); + endtask + + task automatic read(logic [ADDR_WIDTH-1:0] addr, output logic [DATA_WIDTH-1:0] data); + txn_mutex.get(); + ##0; + + // Initiate transfer + cb.PSEL <= '1; + cb.PENABLE <= '0; + cb.PWRITE <= '0; + cb.PADDR <= addr; + cb.PWDATA <= '0; + @(cb); + + // active phase + cb.PENABLE <= '1; + @(cb); + + // Wait for response + while(cb.PREADY !== 1'b1) @(cb); + assert(!$isunknown(cb.PRDATA)) else $error("Read from 0x%0x returned X's on PRDATA", addr); + assert(!$isunknown(cb.PSLVERR)) else $error("Read from 0x%0x returned X's on PSLVERR", addr); + data = cb.PRDATA; + reset(); + txn_mutex.put(); + endtask + + task automatic assert_read(logic [ADDR_WIDTH-1:0] addr, logic [DATA_WIDTH-1:0] expected_data, logic [DATA_WIDTH-1:0] mask = '1); + logic [DATA_WIDTH-1:0] data; + read(addr, data); + data &= mask; + assert(data == expected_data) else $error("Read from 0x%x returned 0x%x. Expected 0x%x", addr, data, expected_data); + endtask + + initial begin + reset(); + end + + initial forever begin + @cb; + if(!rst) assert(!$isunknown(cb.PREADY)) else $error("Saw X on PREADY!"); + end + +endinterface diff --git a/tests/lib/cpuifs/apb3/tb_inst.sv b/tests/lib/cpuifs/apb3/tb_inst.sv new file mode 100644 index 0000000..aec79b1 --- /dev/null +++ b/tests/lib/cpuifs/apb3/tb_inst.sv @@ -0,0 +1,32 @@ +{% sv_line_anchor %} +apb3_intf #( + .DATA_WIDTH({{exporter.cpuif.data_width}}), + .ADDR_WIDTH({{exporter.cpuif.addr_width}}) +) s_apb(); +apb3_intf_driver #( + .DATA_WIDTH({{exporter.cpuif.data_width}}), + .ADDR_WIDTH({{exporter.cpuif.addr_width}}) +) cpuif ( + .clk(clk), + .rst(rst), + .m_apb(s_apb) +); +{% if type(cpuif).__name__.startswith("Flat") %} +{% sv_line_anchor %} +wire s_apb_psel; +wire s_apb_penable; +wire s_apb_pwrite; +wire [{{exporter.cpuif.addr_width - 1}}:0] s_apb_paddr; +wire [{{exporter.cpuif.data_width - 1}}:0] s_apb_pwdata; +wire s_apb_pready; +wire [{{exporter.cpuif.data_width - 1}}:0] s_apb_prdata; +wire s_apb_pslverr; +assign s_apb_psel = s_apb.PSEL; +assign s_apb_penable = s_apb.PENABLE; +assign s_apb_pwrite = s_apb.PWRITE; +assign s_apb_paddr = s_apb.PADDR; +assign s_apb_pwdata = s_apb.PWDATA; +assign s_apb.PREADY = s_apb_pready; +assign s_apb.PRDATA = s_apb_prdata; +assign s_apb.PSLVERR = s_apb_pslverr; +{% endif %} diff --git a/tests/lib/cpuifs/apb4/__init__.py b/tests/lib/cpuifs/apb4/__init__.py new file mode 100644 index 0000000..4a93b67 --- /dev/null +++ b/tests/lib/cpuifs/apb4/__init__.py @@ -0,0 +1,18 @@ +from ..base import CpuifTestMode + +from peakrdl_regblock.cpuif.apb4 import APB4_Cpuif, APB4_Cpuif_flattened + +class APB4(CpuifTestMode): + cpuif_cls = APB4_Cpuif + rtl_files = [ + "../../../../hdl-src/apb4_intf.sv", + ] + tb_files = [ + "../../../../hdl-src/apb4_intf.sv", + "apb4_intf_driver.sv", + ] + tb_template = "tb_inst.sv" + +class FlatAPB4(APB4): + cpuif_cls = APB4_Cpuif_flattened + rtl_files = [] diff --git a/tests/lib/cpuifs/apb4/apb4_intf_driver.sv b/tests/lib/cpuifs/apb4/apb4_intf_driver.sv new file mode 100644 index 0000000..cf5258f --- /dev/null +++ b/tests/lib/cpuifs/apb4/apb4_intf_driver.sv @@ -0,0 +1,128 @@ +interface apb4_intf_driver #( + parameter DATA_WIDTH = 32, + parameter ADDR_WIDTH = 32 + )( + input wire clk, + input wire rst, + apb4_intf.master m_apb + ); + + timeunit 1ps; + timeprecision 1ps; + + logic PSEL; + logic PENABLE; + logic PWRITE; + logic [2:0] PPROT; + logic [ADDR_WIDTH-1:0] PADDR; + logic [DATA_WIDTH-1:0] PWDATA; + logic [DATA_WIDTH/8-1:0] PSTRB; + logic [DATA_WIDTH-1:0] PRDATA; + logic PREADY; + logic PSLVERR; + + assign m_apb.PSEL = PSEL; + assign m_apb.PENABLE = PENABLE; + assign m_apb.PWRITE = PWRITE; + assign m_apb.PPROT = PPROT; + assign m_apb.PADDR = PADDR; + assign m_apb.PWDATA = PWDATA; + assign m_apb.PSTRB = PSTRB; + assign PRDATA = m_apb.PRDATA; + assign PREADY = m_apb.PREADY; + assign PSLVERR = m_apb.PSLVERR; + + default clocking cb @(posedge clk); + default input #1step output #1; + output PSEL; + output PENABLE; + output PWRITE; + output PPROT; + output PADDR; + output PWDATA; + output PSTRB; + input PRDATA; + input PREADY; + input PSLVERR; + endclocking + + task automatic reset(); + cb.PSEL <= '0; + cb.PENABLE <= '0; + cb.PWRITE <= '0; + cb.PPROT <= '0; + cb.PADDR <= '0; + cb.PWDATA <= '0; + cb.PSTRB <= '0; + endtask + + semaphore txn_mutex = new(1); + + task automatic write(logic [ADDR_WIDTH-1:0] addr, logic [DATA_WIDTH-1:0] data, logic [DATA_WIDTH/8-1:0] strb = '1); + txn_mutex.get(); + ##0; + + // Initiate transfer + cb.PSEL <= '1; + cb.PENABLE <= '0; + cb.PWRITE <= '1; + cb.PPROT <= '0; + cb.PADDR <= addr; + cb.PWDATA <= data; + cb.PSTRB <= strb; + @(cb); + + // active phase + cb.PENABLE <= '1; + @(cb); + + // Wait for response + while(cb.PREADY !== 1'b1) @(cb); + reset(); + txn_mutex.put(); + endtask + + task automatic read(logic [ADDR_WIDTH-1:0] addr, output logic [DATA_WIDTH-1:0] data); + txn_mutex.get(); + ##0; + + // Initiate transfer + cb.PSEL <= '1; + cb.PENABLE <= '0; + cb.PWRITE <= '0; + cb.PPROT <= '0; + cb.PADDR <= addr; + cb.PWDATA <= '0; + cb.PSTRB <= '0; + @(cb); + + // active phase + cb.PENABLE <= '1; + @(cb); + + // Wait for response + while(cb.PREADY !== 1'b1) @(cb); + assert(!$isunknown(cb.PRDATA)) else $error("Read from 0x%0x returned X's on PRDATA", addr); + assert(!$isunknown(cb.PSLVERR)) else $error("Read from 0x%0x returned X's on PSLVERR", addr); + data = cb.PRDATA; + reset(); + txn_mutex.put(); + endtask + + task automatic assert_read(logic [ADDR_WIDTH-1:0] addr, logic [DATA_WIDTH-1:0] expected_data, logic [DATA_WIDTH-1:0] mask = '1); + logic [DATA_WIDTH-1:0] data; + read(addr, data); + data &= mask; + assert(data == expected_data) else $error("Read from 0x%x returned 0x%x. Expected 0x%x", addr, data, expected_data); + endtask + + initial begin + reset(); + end + + initial forever begin + @cb; + if(!rst) assert(!$isunknown(cb.PREADY)) else $error("Saw X on PREADY!"); + end + +endinterface diff --git a/tests/lib/cpuifs/apb4/tb_inst.sv b/tests/lib/cpuifs/apb4/tb_inst.sv new file mode 100644 index 0000000..d769854 --- /dev/null +++ b/tests/lib/cpuifs/apb4/tb_inst.sv @@ -0,0 +1,36 @@ +{% sv_line_anchor %} +apb4_intf #( + .DATA_WIDTH({{exporter.cpuif.data_width}}), + .ADDR_WIDTH({{exporter.cpuif.addr_width}}) +) s_apb(); +apb4_intf_driver #( + .DATA_WIDTH({{exporter.cpuif.data_width}}), + .ADDR_WIDTH({{exporter.cpuif.addr_width}}) +) cpuif ( + .clk(clk), + .rst(rst), + .m_apb(s_apb) +); +{% if type(cpuif).__name__.startswith("Flat") %} +{% sv_line_anchor %} +wire s_apb_psel; +wire s_apb_penable; +wire s_apb_pwrite; +wire [2:0] s_apb_pprot; +wire [{{exporter.cpuif.addr_width - 1}}:0] s_apb_paddr; +wire [{{exporter.cpuif.data_width - 1}}:0] s_apb_pwdata; +wire [{{exporter.cpuif.data_width_bytes - 1}}:0] s_apb_pstrb; +wire s_apb_pready; +wire [{{exporter.cpuif.data_width - 1}}:0] s_apb_prdata; +wire s_apb_pslverr; +assign s_apb_psel = s_apb.PSEL; +assign s_apb_penable = s_apb.PENABLE; +assign s_apb_pwrite = s_apb.PWRITE; +assign s_apb_pprot = s_apb.PPROT; +assign s_apb_paddr = s_apb.PADDR; +assign s_apb_pwdata = s_apb.PWDATA; +assign s_apb_pstrb = s_apb.PSTRB; +assign s_apb.PREADY = s_apb_pready; +assign s_apb.PRDATA = s_apb_prdata; +assign s_apb.PSLVERR = s_apb_pslverr; +{% endif %} diff --git a/tests/lib/cpuifs/avalon/__init__.py b/tests/lib/cpuifs/avalon/__init__.py new file mode 100644 index 0000000..79672ab --- /dev/null +++ b/tests/lib/cpuifs/avalon/__init__.py @@ -0,0 +1,18 @@ +from ..base import CpuifTestMode + +from peakrdl_regblock.cpuif.avalon import Avalon_Cpuif, Avalon_Cpuif_flattened + +class Avalon(CpuifTestMode): + cpuif_cls = Avalon_Cpuif + rtl_files = [ + "../../../../hdl-src/avalon_mm_intf.sv", + ] + tb_files = [ + "../../../../hdl-src/avalon_mm_intf.sv", + "avalon_mm_intf_driver.sv", + ] + tb_template = "tb_inst.sv" + +class FlatAvalon(Avalon): + cpuif_cls = Avalon_Cpuif_flattened + rtl_files = [] diff --git a/tests/lib/cpuifs/avalon/avalon_mm_intf_driver.sv b/tests/lib/cpuifs/avalon/avalon_mm_intf_driver.sv new file mode 100644 index 0000000..173be6a --- /dev/null +++ b/tests/lib/cpuifs/avalon/avalon_mm_intf_driver.sv @@ -0,0 +1,138 @@ +interface avalon_mm_intf_driver #( + parameter DATA_WIDTH = 32, + parameter ADDR_WIDTH = 32 + )( + input wire clk, + input wire rst, + avalon_mm_intf.host avalon + ); + timeunit 1ps; + timeprecision 1ps; + + localparam ADDR_PAD = $clog2(DATA_WIDTH/8); + localparam WORD_ADDR_WIDTH = ADDR_WIDTH - ADDR_PAD; + + logic av_read; + logic av_write; + logic av_waitrequest; + logic [WORD_ADDR_WIDTH-1:0] av_address; + logic [DATA_WIDTH-1:0] av_writedata; + logic [DATA_WIDTH/8-1:0] av_byteenable; + logic av_readdatavalid; + logic av_writeresponsevalid; + logic [DATA_WIDTH-1:0] av_readdata; + logic [1:0] av_response; + + assign avalon.read = av_read; + assign avalon.write = av_write; + assign av_waitrequest = avalon.waitrequest; + assign avalon.address = av_address; + assign avalon.writedata = av_writedata; + assign avalon.byteenable = av_byteenable; + assign av_readdatavalid = avalon.readdatavalid; + assign av_writeresponsevalid = avalon.writeresponsevalid; + assign av_readdata = avalon.readdata; + assign av_response = avalon.response; + + default clocking cb @(posedge clk); + default input #1step output #1; + output av_read; + output av_write; + input av_waitrequest; + output av_address; + output av_writedata; + output av_byteenable; + input av_readdatavalid; + input av_writeresponsevalid; + input av_readdata; + input av_response; + endclocking + + task automatic reset(); + cb.av_read <= '0; + cb.av_write <= '0; + cb.av_address <= '0; + cb.av_writedata <= '0; + cb.av_byteenable <= '0; + endtask + + semaphore req_mutex = new(1); + semaphore resp_mutex = new(1); + + task automatic write(logic [ADDR_WIDTH-1:0] addr, logic [DATA_WIDTH-1:0] data, logic [DATA_WIDTH/8-1:0] strb = '1); + fork + begin + req_mutex.get(); + ##0; + // Initiate transfer + cb.av_write <= '1; + cb.av_address <= (addr >> ADDR_PAD); + cb.av_writedata <= data; + cb.av_byteenable <= strb; + @(cb); + + // Wait for transfer to be accepted + while(cb.av_waitrequest == 1'b1) @(cb); + reset(); + req_mutex.put(); + end + + begin + resp_mutex.get(); + @cb; + // Wait for response + while(cb.av_writeresponsevalid !== 1'b1) @(cb); + assert(!$isunknown(cb.av_response)) else $error("Read from 0x%0x returned X's on av_response", addr); + resp_mutex.put(); + end + join + endtask + + task automatic read(logic [ADDR_WIDTH-1:0] addr, output logic [DATA_WIDTH-1:0] data); + fork + begin + req_mutex.get(); + ##0; + // Initiate transfer + cb.av_read <= '1; + cb.av_address <= (addr >> ADDR_PAD); + @(cb); + + // Wait for transfer to be accepted + while(cb.av_waitrequest == 1'b1) @(cb); + reset(); + req_mutex.put(); + end + + begin + resp_mutex.get(); + @cb; + // Wait for response + while(cb.av_readdatavalid !== 1'b1) @(cb); + assert(!$isunknown(cb.av_readdata)) else $error("Read from 0x%0x returned X's on av_response", av_readdata); + assert(!$isunknown(cb.av_response)) else $error("Read from 0x%0x returned X's on av_response", addr); + data = cb.av_readdata; + resp_mutex.put(); + end + join + endtask + + task automatic assert_read(logic [ADDR_WIDTH-1:0] addr, logic [DATA_WIDTH-1:0] expected_data, logic [DATA_WIDTH-1:0] mask = '1); + logic [DATA_WIDTH-1:0] data; + read(addr, data); + data &= mask; + assert(data == expected_data) else $error("Read from 0x%x returned 0x%x. Expected 0x%x", addr, data, expected_data); + endtask + + initial begin + reset(); + end + + initial forever begin + @cb; + if(!rst) assert(!$isunknown(cb.av_waitrequest)) else $error("Saw X on av_waitrequest!"); + if(!rst) assert(!$isunknown(cb.av_readdatavalid)) else $error("Saw X on av_readdatavalid!"); + if(!rst) assert(!$isunknown(cb.av_writeresponsevalid)) else $error("Saw X on av_writeresponsevalid!"); + end + +endinterface diff --git a/tests/lib/cpuifs/avalon/tb_inst.sv b/tests/lib/cpuifs/avalon/tb_inst.sv new file mode 100644 index 0000000..e5b2303 --- /dev/null +++ b/tests/lib/cpuifs/avalon/tb_inst.sv @@ -0,0 +1,36 @@ +{% sv_line_anchor %} +avalon_mm_intf #( + .DATA_WIDTH({{exporter.cpuif.data_width}}), + .ADDR_WIDTH({{exporter.cpuif.word_addr_width}}) +) avalon(); +avalon_mm_intf_driver #( + .DATA_WIDTH({{exporter.cpuif.data_width}}), + .ADDR_WIDTH({{exporter.cpuif.addr_width}}) +) cpuif ( + .clk(clk), + .rst(rst), + .avalon(avalon) +); +{% if type(cpuif).__name__.startswith("Flat") %} +{% sv_line_anchor %} +wire avalon_read; +wire avalon_write; +wire avalon_waitrequest; +wire [{{exporter.cpuif.word_addr_width - 1}}:0] avalon_address; +wire [{{exporter.cpuif.data_width - 1}}:0] avalon_writedata; +wire [{{exporter.cpuif.data_width_bytes - 1}}:0] avalon_byteenable; +wire avalon_readdatavalid; +wire avalon_writeresponsevalid; +wire [{{exporter.cpuif.data_width - 1}}:0] avalon_readdata; +wire [1:0] avalon_response; +assign avalon_read = avalon.read; +assign avalon_write = avalon.write; +assign avalon.waitrequest = avalon_waitrequest; +assign avalon_address = avalon.address; +assign avalon_writedata = avalon.writedata; +assign avalon_byteenable = avalon.byteenable; +assign avalon.readdatavalid = avalon_readdatavalid; +assign avalon.writeresponsevalid = avalon_writeresponsevalid; +assign avalon.readdata = avalon_readdata; +assign avalon.response = avalon_response; +{% endif %} diff --git a/tests/lib/cpuifs/axi4lite/__init__.py b/tests/lib/cpuifs/axi4lite/__init__.py new file mode 100644 index 0000000..ef3147c --- /dev/null +++ b/tests/lib/cpuifs/axi4lite/__init__.py @@ -0,0 +1,18 @@ +from ..base import CpuifTestMode + +from peakrdl_regblock.cpuif.axi4lite import AXI4Lite_Cpuif, AXI4Lite_Cpuif_flattened + +class AXI4Lite(CpuifTestMode): + cpuif_cls = AXI4Lite_Cpuif + rtl_files = [ + "../../../../hdl-src/axi4lite_intf.sv", + ] + tb_files = [ + "../../../../hdl-src/axi4lite_intf.sv", + "axi4lite_intf_driver.sv", + ] + tb_template = "tb_inst.sv" + +class FlatAXI4Lite(AXI4Lite): + cpuif_cls = AXI4Lite_Cpuif_flattened + rtl_files = [] diff --git a/tests/lib/cpuifs/axi4lite/axi4lite_intf_driver.sv b/tests/lib/cpuifs/axi4lite/axi4lite_intf_driver.sv new file mode 100644 index 0000000..856048a --- /dev/null +++ b/tests/lib/cpuifs/axi4lite/axi4lite_intf_driver.sv @@ -0,0 +1,263 @@ +interface axi4lite_intf_driver #( + parameter DATA_WIDTH = 32, + parameter ADDR_WIDTH = 32 + )( + input wire clk, + input wire rst, + axi4lite_intf.master m_axil + ); + + timeunit 1ps; + timeprecision 1ps; + + logic AWREADY; + logic AWVALID; + logic [ADDR_WIDTH-1:0] AWADDR; + logic [2:0] AWPROT; + + logic WREADY; + logic WVALID; + logic [DATA_WIDTH-1:0] WDATA; + logic [DATA_WIDTH/8-1:0] WSTRB; + + logic BREADY; + logic BVALID; + logic [1:0] BRESP; + + logic ARREADY; + logic ARVALID; + logic [ADDR_WIDTH-1:0] ARADDR; + logic [2:0] ARPROT; + + logic RREADY; + logic RVALID; + logic [DATA_WIDTH-1:0] RDATA; + logic [1:0] RRESP; + + assign AWREADY = m_axil.AWREADY; + assign m_axil.AWVALID = AWVALID; + assign m_axil.AWADDR = AWADDR; + assign m_axil.AWPROT = AWPROT; + assign WREADY = m_axil.WREADY; + assign m_axil.WVALID = WVALID; + assign m_axil.WDATA = WDATA; + assign m_axil.WSTRB = WSTRB; + assign m_axil.BREADY = BREADY; + assign BVALID = m_axil.BVALID; + assign BRESP = m_axil.BRESP; + assign ARREADY = m_axil.ARREADY; + assign m_axil.ARVALID = ARVALID; + assign m_axil.ARADDR = ARADDR; + assign m_axil.ARPROT = ARPROT; + assign m_axil.RREADY = RREADY; + assign RVALID = m_axil.RVALID; + assign RDATA = m_axil.RDATA; + assign RRESP = m_axil.RRESP; + + default clocking cb @(posedge clk); + default input #1step output #1; + input AWREADY; + output AWVALID; + output AWADDR; + output AWPROT; + input WREADY; + output WVALID; + output WDATA; + output WSTRB; + inout BREADY; + input BVALID; + input BRESP; + input ARREADY; + output ARVALID; + output ARADDR; + output ARPROT; + inout RREADY; + input RVALID; + input RDATA; + input RRESP; + endclocking + + task automatic reset(); + cb.AWVALID <= '0; + cb.AWADDR <= '0; + cb.AWPROT <= '0; + cb.WVALID <= '0; + cb.WDATA <= '0; + cb.WSTRB <= '0; + cb.ARVALID <= '0; + cb.ARADDR <= '0; + cb.ARPROT <= '0; + endtask + + initial forever begin + cb.RREADY <= $urandom_range(1, 0); + cb.BREADY <= $urandom_range(1, 0); + @cb; + end + + //-------------------------------------------------------------------------- + typedef struct { + logic [1:0] bresp; + } write_response_t; + + class write_request_t; + mailbox #(write_response_t) response_mbx; + logic [ADDR_WIDTH-1:0] addr; + logic [DATA_WIDTH-1:0] data; + logic [DATA_WIDTH/8-1:0] strb; + function new(); + this.response_mbx = new(); + endfunction + endclass + + mailbox #(write_request_t) aw_mbx = new(); + mailbox #(write_request_t) w_mbx = new(); + write_request_t write_queue[$]; + + // Issue AW transfers + initial forever begin + write_request_t req; + aw_mbx.get(req); + ##0; + repeat($urandom_range(2,0)) @cb; + cb.AWVALID <= '1; + cb.AWADDR <= req.addr; + cb.AWPROT <= '0; + @(cb); + while(cb.AWREADY !== 1'b1) @(cb); + cb.AWVALID <= '0; + end + + // Issue W transfers + initial forever begin + write_request_t req; + w_mbx.get(req); + ##0; + repeat($urandom_range(2,0)) @cb; + cb.WVALID <= '1; + cb.WDATA <= req.data; + cb.WSTRB <= req.strb; + @(cb); + while(cb.WREADY !== 1'b1) @(cb); + cb.WVALID <= '0; + cb.WSTRB <= '0; + end + + // Listen for R responses + initial forever begin + @cb; + while(rst || !(cb.BREADY === 1'b1 && cb.BVALID === 1'b1)) @cb; + if(write_queue.size() != 0) begin + // Can match this response with an existing request. + // Send response to requestor + write_request_t req; + write_response_t resp; + req = write_queue.pop_front(); + resp.bresp = cb.BRESP; + req.response_mbx.put(resp); + end else begin + $error("Got unmatched write response"); + end + end + + task automatic write(logic [ADDR_WIDTH-1:0] addr, logic [DATA_WIDTH-1:0] data, logic [DATA_WIDTH/8-1:0] strb = '1); + write_request_t req; + write_response_t resp; + + req = new(); + req.addr = addr; + req.data = data; + req.strb = strb; + + aw_mbx.put(req); + w_mbx.put(req); + write_queue.push_back(req); + + // Wait for response + req.response_mbx.get(resp); + assert(!$isunknown(resp.bresp)) else $error("Read from 0x%0x returned X's on BRESP", addr); + endtask + + //-------------------------------------------------------------------------- + typedef struct { + logic [DATA_WIDTH-1: 0] rdata; + logic [1:0] rresp; + } read_response_t; + + class read_request_t; + mailbox #(read_response_t) response_mbx; + function new(); + this.response_mbx = new(); + endfunction + endclass + + semaphore txn_ar_mutex = new(1); + read_request_t read_queue[$]; + + // Listen for R responses + initial forever begin + @cb; + while(rst || !(cb.RREADY === 1'b1 && cb.RVALID === 1'b1)) @cb; + if(read_queue.size() != 0) begin + // Can match this response with an existing request. + // Send response to requestor + read_request_t req; + read_response_t resp; + req = read_queue.pop_front(); + resp.rdata = cb.RDATA; + resp.rresp = cb.RRESP; + req.response_mbx.put(resp); + end else begin + $error("Got unmatched read response"); + end + end + + task automatic read(logic [ADDR_WIDTH-1:0] addr, output logic [DATA_WIDTH-1:0] data); + read_request_t req; + read_response_t resp; + + txn_ar_mutex.get(); + // Issue read request + ##0; + cb.ARVALID <= '1; + cb.ARADDR <= addr; + cb.ARPROT <= '0; + @(cb); + while(cb.ARREADY !== 1'b1) @(cb); + cb.ARVALID <= '0; + + // Push new request into queue + req = new(); + read_queue.push_back(req); + txn_ar_mutex.put(); + + // Wait for response + req.response_mbx.get(resp); + + assert(!$isunknown(resp.rdata)) else $error("Read from 0x%0x returned X's on RDATA", addr); + assert(!$isunknown(resp.rresp)) else $error("Read from 0x%0x returned X's on RRESP", addr); + data = resp.rdata; + endtask + + task automatic assert_read(logic [ADDR_WIDTH-1:0] addr, logic [DATA_WIDTH-1:0] expected_data, logic [DATA_WIDTH-1:0] mask = '1); + logic [DATA_WIDTH-1:0] data; + read(addr, data); + data &= mask; + assert(data == expected_data) else $error("Read from 0x%x returned 0x%x. Expected 0x%x", addr, data, expected_data); + endtask + + //-------------------------------------------------------------------------- + initial begin + reset(); + end + + initial forever begin + @cb; + if(!rst) assert(!$isunknown(cb.AWREADY)) else $error("Saw X on AWREADY!"); + if(!rst) assert(!$isunknown(cb.WREADY)) else $error("Saw X on WREADY!"); + if(!rst) assert(!$isunknown(cb.BVALID)) else $error("Saw X on BVALID!"); + if(!rst) assert(!$isunknown(cb.ARREADY)) else $error("Saw X on ARREADY!"); + if(!rst) assert(!$isunknown(cb.RVALID)) else $error("Saw X on RVALID!"); + end + +endinterface diff --git a/tests/lib/cpuifs/axi4lite/tb_inst.sv b/tests/lib/cpuifs/axi4lite/tb_inst.sv new file mode 100644 index 0000000..5b3f6fa --- /dev/null +++ b/tests/lib/cpuifs/axi4lite/tb_inst.sv @@ -0,0 +1,54 @@ +{% sv_line_anchor %} +axi4lite_intf #( + .DATA_WIDTH({{exporter.cpuif.data_width}}), + .ADDR_WIDTH({{exporter.cpuif.addr_width}}) +) s_axil(); +axi4lite_intf_driver #( + .DATA_WIDTH({{exporter.cpuif.data_width}}), + .ADDR_WIDTH({{exporter.cpuif.addr_width}}) +) cpuif ( + .clk(clk), + .rst(rst), + .m_axil(s_axil) +); +{% if type(cpuif).__name__.startswith("Flat") %} +{% sv_line_anchor %} +wire s_axil_awready; +wire s_axil_awvalid; +wire [{{exporter.cpuif.addr_width - 1}}:0] s_axil_awaddr; +wire [2:0] s_axil_awprot; +wire s_axil_wready; +wire s_axil_wvalid; +wire [{{exporter.cpuif.data_width - 1}}:0] s_axil_wdata; +wire [{{exporter.cpuif.data_width_bytes - 1}}:0] s_axil_wstrb; +wire s_axil_bready; +wire s_axil_bvalid; +wire [1:0] s_axil_bresp; +wire s_axil_arready; +wire s_axil_arvalid; +wire [{{exporter.cpuif.addr_width - 1}}:0] s_axil_araddr; +wire [2:0] s_axil_arprot; +wire s_axil_rready; +wire s_axil_rvalid; +wire [{{exporter.cpuif.data_width - 1}}:0] s_axil_rdata; +wire [1:0] s_axil_rresp; +assign s_axil.AWREADY = s_axil_awready; +assign s_axil_awvalid = s_axil.AWVALID; +assign s_axil_awaddr = s_axil.AWADDR; +assign s_axil_awprot = s_axil.AWPROT; +assign s_axil.WREADY = s_axil_wready; +assign s_axil_wvalid = s_axil.WVALID; +assign s_axil_wdata = s_axil.WDATA; +assign s_axil_wstrb = s_axil.WSTRB; +assign s_axil_bready = s_axil.BREADY; +assign s_axil.BVALID = s_axil_bvalid; +assign s_axil.BRESP = s_axil_bresp; +assign s_axil.ARREADY = s_axil_arready; +assign s_axil_arvalid = s_axil.ARVALID; +assign s_axil_araddr = s_axil.ARADDR; +assign s_axil_arprot = s_axil.ARPROT; +assign s_axil_rready = s_axil.RREADY; +assign s_axil.RVALID = s_axil_rvalid; +assign s_axil.RDATA = s_axil_rdata; +assign s_axil.RRESP = s_axil_rresp; +{% endif %} diff --git a/tests/lib/cpuifs/base.py b/tests/lib/cpuifs/base.py new file mode 100644 index 0000000..c53358f --- /dev/null +++ b/tests/lib/cpuifs/base.py @@ -0,0 +1,87 @@ +from typing import List, TYPE_CHECKING +import os +import inspect + +import jinja2 as jj + +from peakrdl_regblock.cpuif.base import CpuifBase + +from ..sv_line_anchor import SVLineAnchor + +if TYPE_CHECKING: + from peakrdl_regblock import RegblockExporter + from ..sim_testcase import SimTestCase + +class CpuifTestMode: + cpuif_cls = None # type: CpuifBase + + # Files required by the DUT + # Paths are relative to the class that assigns this + rtl_files = [] # type: List[str] + + # Files required by the sim testbench + # Paths are relative to the class that assigns this + tb_files = [] # type: List[str] + + # Path is relative to the class that assigns this + tb_template = "" + + + def _get_class_dir_of_variable(self, varname:str) -> str: + """ + Traverse up the MRO and find the first class that explicitly assigns + the variable of name varname. Returns the directory that contains the + class definition. + """ + for cls in inspect.getmro(self.__class__): + if varname in cls.__dict__: + class_dir = os.path.dirname(inspect.getfile(cls)) + return class_dir + raise RuntimeError + + + def _get_file_paths(self, varname:str) -> List[str]: + class_dir = self._get_class_dir_of_variable(varname) + files = getattr(self, varname) + cwd = os.getcwd() + + new_files = [] + for file in files: + relpath = os.path.relpath( + os.path.join(class_dir, file), + cwd + ) + new_files.append(relpath) + return new_files + + + def get_sim_files(self) -> List[str]: + files = self._get_file_paths("rtl_files") + self._get_file_paths("tb_files") + unique_files = [] + [unique_files.append(f) for f in files if f not in unique_files] + return unique_files + + + def get_synth_files(self) -> List[str]: + return self._get_file_paths("rtl_files") + + + def get_tb_inst(self, testcase: 'SimTestCase', exporter: 'RegblockExporter') -> str: + class_dir = self._get_class_dir_of_variable("tb_template") + loader = jj.FileSystemLoader(class_dir) + jj_env = jj.Environment( + loader=loader, + undefined=jj.StrictUndefined, + extensions=[SVLineAnchor], + ) + + context = { + "cpuif": self, + "testcase": testcase, + "exporter": exporter, + "type": type, + } + + template = jj_env.get_template(self.tb_template) + + return template.render(context) diff --git a/tests/lib/cpuifs/passthrough/__init__.py b/tests/lib/cpuifs/passthrough/__init__.py new file mode 100644 index 0000000..04ee62f --- /dev/null +++ b/tests/lib/cpuifs/passthrough/__init__.py @@ -0,0 +1,11 @@ +from ..base import CpuifTestMode + +from peakrdl_regblock.cpuif.passthrough import PassthroughCpuif + +class Passthrough(CpuifTestMode): + cpuif_cls = PassthroughCpuif + rtl_files = [] + tb_files = [ + "passthrough_driver.sv", + ] + tb_template = "tb_inst.sv" diff --git a/tests/lib/cpuifs/passthrough/passthrough_driver.sv b/tests/lib/cpuifs/passthrough/passthrough_driver.sv new file mode 100644 index 0000000..9145172 --- /dev/null +++ b/tests/lib/cpuifs/passthrough/passthrough_driver.sv @@ -0,0 +1,123 @@ +interface passthrough_driver #( + parameter DATA_WIDTH = 32, + parameter ADDR_WIDTH = 32 + )( + input wire clk, + input wire rst, + + output logic m_cpuif_req, + output logic m_cpuif_req_is_wr, + output logic [ADDR_WIDTH-1:0] m_cpuif_addr, + output logic [DATA_WIDTH-1:0] m_cpuif_wr_data, + output logic [DATA_WIDTH-1:0] m_cpuif_wr_biten, + input wire m_cpuif_req_stall_wr, + input wire m_cpuif_req_stall_rd, + input wire m_cpuif_rd_ack, + input wire m_cpuif_rd_err, + input wire [DATA_WIDTH-1:0] m_cpuif_rd_data, + input wire m_cpuif_wr_ack, + input wire m_cpuif_wr_err + ); + + timeunit 1ps; + timeprecision 1ps; + + default clocking cb @(posedge clk); + default input #1step output #1; + output m_cpuif_req; + output m_cpuif_req_is_wr; + output m_cpuif_addr; + output m_cpuif_wr_data; + output m_cpuif_wr_biten; + input m_cpuif_req_stall_wr; + input m_cpuif_req_stall_rd; + input m_cpuif_rd_ack; + input m_cpuif_rd_err; + input m_cpuif_rd_data; + input m_cpuif_wr_ack; + input m_cpuif_wr_err; + endclocking + + task automatic reset(); + cb.m_cpuif_req <= '0; + cb.m_cpuif_req_is_wr <= '0; + cb.m_cpuif_addr <= '0; + cb.m_cpuif_wr_data <= '0; + cb.m_cpuif_wr_biten <= '0; + endtask + + semaphore txn_req_mutex = new(1); + semaphore txn_resp_mutex = new(1); + + task automatic write(logic [ADDR_WIDTH-1:0] addr, logic [DATA_WIDTH-1:0] data, logic [DATA_WIDTH-1:0] biten = '1); + fork + begin + // Initiate transfer + txn_req_mutex.get(); + ##0; + cb.m_cpuif_req <= '1; + cb.m_cpuif_req_is_wr <= '1; + cb.m_cpuif_addr <= addr; + cb.m_cpuif_wr_data <= data; + cb.m_cpuif_wr_biten <= biten; + @(cb); + while(cb.m_cpuif_req_stall_wr !== 1'b0) @(cb); + reset(); + txn_req_mutex.put(); + end + + begin + // Wait for response + txn_resp_mutex.get(); + @cb; + while(cb.m_cpuif_wr_ack !== 1'b1) @(cb); + txn_resp_mutex.put(); + end + join + endtask + + task automatic read(logic [ADDR_WIDTH-1:0] addr, output logic [DATA_WIDTH-1:0] data); + fork + begin + // Initiate transfer + txn_req_mutex.get(); + ##0; + cb.m_cpuif_req <= '1; + cb.m_cpuif_req_is_wr <= '0; + cb.m_cpuif_addr <= addr; + @(cb); + while(cb.m_cpuif_req_stall_rd !== 1'b0) @(cb); + reset(); + txn_req_mutex.put(); + end + + begin + // Wait for response + txn_resp_mutex.get(); + @cb; + while(cb.m_cpuif_rd_ack !== 1'b1) @(cb); + assert(!$isunknown(cb.m_cpuif_rd_data)) else $error("Read from 0x%0x returned X's on m_cpuif_rd_data", addr); + data = cb.m_cpuif_rd_data; + txn_resp_mutex.put(); + end + join + endtask + + task automatic assert_read(logic [ADDR_WIDTH-1:0] addr, logic [DATA_WIDTH-1:0] expected_data, logic [DATA_WIDTH-1:0] mask = '1); + logic [DATA_WIDTH-1:0] data; + read(addr, data); + data &= mask; + assert(data == expected_data) else $error("Read from 0x%x returned 0x%x. Expected 0x%x", addr, data, expected_data); + endtask + + initial begin + reset(); + end + + initial forever begin + @cb; + if(!rst) assert(!$isunknown(cb.m_cpuif_rd_ack)) else $error("Saw X on m_cpuif_rd_ack!"); + if(!rst) assert(!$isunknown(cb.m_cpuif_wr_ack)) else $error("Saw X on m_cpuif_wr_ack!"); + end + +endinterface diff --git a/tests/lib/cpuifs/passthrough/tb_inst.sv b/tests/lib/cpuifs/passthrough/tb_inst.sv new file mode 100644 index 0000000..e33b9fe --- /dev/null +++ b/tests/lib/cpuifs/passthrough/tb_inst.sv @@ -0,0 +1,32 @@ +{% sv_line_anchor %} +wire s_cpuif_req; +wire s_cpuif_req_is_wr; +wire [{{exporter.cpuif.addr_width-1}}:0] s_cpuif_addr; +wire [{{exporter.cpuif.data_width-1}}:0] s_cpuif_wr_data; +wire [{{exporter.cpuif.data_width-1}}:0] s_cpuif_wr_biten; +wire s_cpuif_req_stall_wr; +wire s_cpuif_req_stall_rd; +wire s_cpuif_rd_ack; +wire s_cpuif_rd_err; +wire [{{exporter.cpuif.data_width-1}}:0] s_cpuif_rd_data; +wire s_cpuif_wr_ack; +wire s_cpuif_wr_err; +passthrough_driver #( + .DATA_WIDTH({{exporter.cpuif.data_width}}), + .ADDR_WIDTH({{exporter.cpuif.addr_width}}) +) cpuif ( + .clk(clk), + .rst(rst), + .m_cpuif_req(s_cpuif_req), + .m_cpuif_req_is_wr(s_cpuif_req_is_wr), + .m_cpuif_addr(s_cpuif_addr), + .m_cpuif_wr_data(s_cpuif_wr_data), + .m_cpuif_wr_biten(s_cpuif_wr_biten), + .m_cpuif_req_stall_wr(s_cpuif_req_stall_wr), + .m_cpuif_req_stall_rd(s_cpuif_req_stall_rd), + .m_cpuif_rd_ack(s_cpuif_rd_ack), + .m_cpuif_rd_err(s_cpuif_rd_err), + .m_cpuif_rd_data(s_cpuif_rd_data), + .m_cpuif_wr_ack(s_cpuif_wr_ack), + .m_cpuif_wr_err(s_cpuif_wr_err) +); diff --git a/tests/lib/external_block.sv b/tests/lib/external_block.sv new file mode 100644 index 0000000..a1c75c9 --- /dev/null +++ b/tests/lib/external_block.sv @@ -0,0 +1,73 @@ +module external_block #( + parameter WIDTH = 32, + parameter ADDR_WIDTH = 8 +)( + input wire clk, + input wire rst, + + input wire req, + input wire req_is_wr, + input wire [ADDR_WIDTH-1:0] addr, + input wire [WIDTH-1:0] wr_data, + input wire [WIDTH-1:0] wr_biten, + output logic rd_ack, + output logic [WIDTH-1:0] rd_data, + output logic wr_ack +); +timeunit 1ps; +timeprecision 1ps; + +localparam ADDR_SHIFT = $clog2(WIDTH/8); +localparam N_ENTRIES = 2**(ADDR_WIDTH - ADDR_SHIFT); +logic [WIDTH-1:0] mem[N_ENTRIES]; + + +task do_write(int idx, logic [WIDTH-1:0] data, logic [WIDTH-1:0] biten); + automatic int delay; + // Random delay + delay = $urandom_range(3,0); + repeat(delay) @(posedge clk) + $info("Write delay: %d", delay); + + for(int b=0; b> ADDR_SHIFT, wr_data, wr_biten); + else do_read(addr >> ADDR_SHIFT); + end + end +end + +endmodule diff --git a/tests/lib/external_reg.sv b/tests/lib/external_reg.sv new file mode 100644 index 0000000..9a51395 --- /dev/null +++ b/tests/lib/external_reg.sv @@ -0,0 +1,79 @@ +module external_reg #( + parameter WIDTH = 32, + parameter SUBWORDS = 1 +)( + input wire clk, + input wire rst, + + input wire [SUBWORDS-1:0] req, + input wire req_is_wr, + input wire [WIDTH-1:0] wr_data, + input wire [WIDTH-1:0] wr_biten, + output logic rd_ack, + output logic [WIDTH-1:0] rd_data, + output logic wr_ack +); +timeunit 1ps; +timeprecision 1ps; +logic [SUBWORDS-1:0][WIDTH-1:0] value; + + +task do_write(logic [SUBWORDS-1:0] strb, logic [WIDTH-1:0] data, logic [WIDTH-1:0] biten); + automatic int delay; + // Random delay + delay = $urandom_range(3,0); + repeat(delay) @(posedge clk) + $info("Write delay: %d", delay); + + for(int i=0; i List[str]: + paths = [] + for path in self.extra_tb_files: + path = os.path.join(self.get_testcase_dir(), path) + paths.append(path) + return paths + + def _generate_tb(self): + """ + Render the testbench template into actual tb.sv + """ + template_root_path = os.path.join(os.path.dirname(__file__), "..") + loader = jj.FileSystemLoader( + template_root_path + ) + jj_env = jj.Environment( + loader=loader, + undefined=jj.StrictUndefined, + extensions=[SVLineAnchor], + ) + + context = { + "testcase": self, + "exporter": self.exporter, + } + + # template path needs to be relative to the Jinja loader root + template_path = os.path.join(self.get_testcase_dir(), self.tb_template_file) + template_path = os.path.relpath(template_path, template_root_path) + template = jj_env.get_template(template_path) + + output_path = os.path.join(self.get_run_dir(), "tb.sv") + stream = template.stream(context) + stream.dump(output_path) + + + def setUp(self): + name = self.request.config.getoption("--sim-tool") + if name in self.incompatible_sim_tools: + pytest.skip() + simulator_cls = get_simulator_cls(name) + if simulator_cls is None: + pytest.skip() + + super().setUp() + + # Create testbench from template + if not self.rerun: + self._generate_tb() + + simulator = simulator_cls(self) + + # cd into the build directory + cwd = os.getcwd() + os.chdir(self.get_run_dir()) + try: + simulator.compile() + finally: + # cd back + os.chdir(cwd) + + + def run_test(self, plusargs:List[str] = None) -> None: + name = self.request.config.getoption("--sim-tool") + simulator_cls = get_simulator_cls(name) + simulator = simulator_cls(self) + + # cd into the build directory + cwd = os.getcwd() + os.chdir(self.get_run_dir()) + + try: + simulator.run(plusargs) + finally: + # cd back + os.chdir(cwd) diff --git a/tests/lib/simulators/__init__.py b/tests/lib/simulators/__init__.py new file mode 100644 index 0000000..f14227b --- /dev/null +++ b/tests/lib/simulators/__init__.py @@ -0,0 +1,39 @@ +from typing import Type, Optional, List +import functools + +from .base import Simulator +from .questa import Questa +from .xilinx import XilinxXSIM +from .xcelium import Xcelium +from .stub import StubSimulator + +ALL_SIMULATORS: List[Simulator] +ALL_SIMULATORS = [ + Questa, + XilinxXSIM, + Xcelium, + StubSimulator, +] + +@functools.lru_cache() +def get_simulator_cls(name: str) -> Optional[Type[Simulator]]: + if name == "skip": + return None + + if name == "auto": + # Find the first simulator that is installed + for sim_cls in ALL_SIMULATORS: + if sim_cls is StubSimulator: + # Never offer the stub as an automatic option + continue + if sim_cls.is_installed(): + return sim_cls + raise ValueError("Could not find any installed simulators") + + # Look up which explicit simulator name was specified + for sim_cls in ALL_SIMULATORS: + if sim_cls.name == name: + if not sim_cls.is_installed(): + raise ValueError("Simulator '%s' is not installed" % sim_cls.name) + return sim_cls + raise RuntimeError diff --git a/tests/lib/simulators/base.py b/tests/lib/simulators/base.py new file mode 100644 index 0000000..115f6d5 --- /dev/null +++ b/tests/lib/simulators/base.py @@ -0,0 +1,35 @@ +from typing import TYPE_CHECKING, List + +if TYPE_CHECKING: + from ..sim_testcase import SimTestCase + +class Simulator: + name = "" + + @classmethod + def is_installed(cls) -> bool: + raise NotImplementedError + + def __init__(self, testcase: 'SimTestCase' = None) -> None: + self.testcase = testcase + + @property + def gui_mode(self) -> bool: + return self.testcase.request.config.getoption("--gui") + + @property + def tb_files(self) -> List[str]: + files = [] + files.extend(self.testcase.cpuif.get_sim_files()) + files.extend(self.testcase.get_extra_tb_files()) + files.append("regblock_pkg.sv") + files.append("regblock.sv") + files.append("tb.sv") + + return files + + def compile(self) -> None: + raise NotImplementedError + + def run(self, plusargs:List[str] = None) -> None: + raise NotImplementedError diff --git a/tests/lib/simulators/questa.py b/tests/lib/simulators/questa.py new file mode 100644 index 0000000..cf97a47 --- /dev/null +++ b/tests/lib/simulators/questa.py @@ -0,0 +1,84 @@ +from typing import List +import subprocess +import os +import shutil + +from .base import Simulator + +class Questa(Simulator): + name = "questa" + + @classmethod + def is_installed(cls) -> bool: + return ( + shutil.which("vlog") is not None + and shutil.which("vsim") is not None + ) + + def compile(self) -> None: + cmd = [ + "vlog", "-sv", "-quiet", "-l", "build.log", + + "+incdir+%s" % os.path.join(os.path.dirname(__file__), ".."), + + # Use strict LRM conformance + "-svinputport=net", + + # all warnings are errors + "-warning", "error", + + # Ignore noisy warning about vopt-time checking of always_comb/always_latch + "-suppress", "2583", + + # Suppress "error" about use of the `line directive + "-suppress", "13465", + ] + + # Add source files + cmd.extend(self.tb_files) + + # Run command! + subprocess.run(cmd, check=True) + + + def run(self, plusargs:List[str] = None) -> None: + plusargs = plusargs or [] + + test_name = self.testcase.request.node.name + + # call vsim + cmd = [ + "vsim", "-quiet", + "-voptargs=+acc", + "-msgmode", "both", + "-l", "%s.log" % test_name, + "-wlf", "%s.wlf" % test_name, + "tb", + "-do", "set WildcardFilter [lsearch -not -all -inline $WildcardFilter Memory]", + "-do", "log -r /*;", + ] + + if self.gui_mode: + cmd.append("-i") + else: + cmd.extend([ + "-do", "run -all; exit;", + "-c", + ]) + + for plusarg in plusargs: + cmd.append("+" + plusarg) + subprocess.run(cmd, check=True) + + self.assertSimLogPass("%s.log" % test_name) + + + def assertSimLogPass(self, path: str): + self.testcase.assertTrue(os.path.isfile(path)) + + with open(path, encoding="utf-8") as f: + for line in f: + if line.startswith("# ** Error"): + self.testcase.fail(line) + elif line.startswith("# ** Fatal"): + self.testcase.fail(line) diff --git a/tests/lib/simulators/stub.py b/tests/lib/simulators/stub.py new file mode 100644 index 0000000..6e54425 --- /dev/null +++ b/tests/lib/simulators/stub.py @@ -0,0 +1,17 @@ +from typing import List + +from .base import Simulator + +class StubSimulator(Simulator): + name = "stub" + + @classmethod + def is_installed(cls) -> bool: + # Always available! + return True + + def compile(self) -> None: + pass + + def run(self, plusargs: List[str] = None) -> None: + pass diff --git a/tests/lib/simulators/xcelium.py b/tests/lib/simulators/xcelium.py new file mode 100644 index 0000000..fe00e27 --- /dev/null +++ b/tests/lib/simulators/xcelium.py @@ -0,0 +1,86 @@ +from typing import List +import subprocess +import os +import shutil +from .base import Simulator + +class Xcelium(Simulator): + """ + Don't use the Xcelium simulator, it is unable to compile & run any testcases. + + As observed in 25.03.006: + - Using unpacked structs with more than 2 levels of nesting in clocking blocks is not + supported. + """ + name = "xcelium" + + @classmethod + def is_installed(cls) -> bool: + return ( + shutil.which("xrun") is not None + ) + + def compile(self) -> None: + # Compile and elaborate into a snapshot + cmd = [ + "xrun", + "-64bit", + "-elaborate", + "-sv", + "-log build.log", + "-timescale 10ps/1ps", + "-ENABLE_DS_UNPS", # Allow ".*" DUT connection with unpacked structs + "-nowarn LNDER6", # Suppress warning about the `line 2 "lib/tb_base.sv" 0 file location + "-nowarn SPDUSD", # Suppress warning about unused include directory + "-incdir %s" % os.path.join(os.path.dirname(__file__), "..") + ] + + if self.gui_mode: + cmd.append("-access +rwc") + + # Add source files + cmd.extend(self.tb_files) + + # Run command! + subprocess.run(cmd, check=True) + + + def run(self, plusargs:List[str] = None) -> None: + plusargs = plusargs or [] + + test_name = self.testcase.request.node.name + + # Call xrun on the elaborated snapshot + cmd = [ + "xrun", + "-64bit", + "-log %s.log" % test_name, + "-r worklib.tb:sv" + ] + + if self.gui_mode: + cmd.append("-gui") + cmd.append('-input "@database -open waves -into waves.shm -shm -default -event"') + cmd.append('-input "@probe -create tb -depth all -tasks -functions -all -packed 4k \ + -unpacked 16k -memories -dynamic -variables -database waves"') + else: + cmd.extend([ + "-input", "@run", + ]) + + for plusarg in plusargs: + cmd.append("+" + plusarg) + subprocess.run(cmd, check=True) + + self.assertSimLogPass("%s.log" % test_name) + + + def assertSimLogPass(self, path: str): + self.testcase.assertTrue(os.path.isfile(path)) + + with open(path, encoding="utf-8") as f: + for line in f: + if line.startswith("xmsim: *E"): + self.testcase.fail(line) + elif line.startswith("xmsim: *F"): + self.testcase.fail(line) diff --git a/tests/lib/simulators/xilinx.py b/tests/lib/simulators/xilinx.py new file mode 100644 index 0000000..ea69781 --- /dev/null +++ b/tests/lib/simulators/xilinx.py @@ -0,0 +1,74 @@ +from typing import List +import subprocess +import os +import shutil + +from .base import Simulator + +class XilinxXSIM(Simulator): + name = "xsim" + + @classmethod + def is_installed(cls) -> bool: + return ( + shutil.which("xvlog") is not None + and shutil.which("xelab") is not None + and shutil.which("xsim") is not None + ) + + def compile(self) -> None: + cmd = [ + "xvlog", "--sv", + "--log", "compile.log", + "--include", os.path.join(os.path.dirname(__file__), ".."), + "--define", "XILINX_XSIM", + ] + cmd.extend(self.tb_files) + subprocess.run(cmd, check=True) + + cmd = [ + "xelab", + "--log", "elaborate.log", + "--timescale", "1ps/1ps", + "--debug", "all", + "tb", + ] + subprocess.run(cmd, check=True) + + + def run(self, plusargs:List[str] = None) -> None: + plusargs = plusargs or [] + + test_name = self.testcase.request.node.name + + # call xsim + cmd = ["xsim"] + if self.gui_mode: + cmd.append("--gui") + else: + cmd.append("-R") + + cmd.extend([ + "--log", "%s.log" % test_name, + "tb", + ]) + + for plusarg in plusargs: + cmd.append("--testplusarg") + cmd.append(plusarg) + subprocess.run(cmd, check=True) + + self.assertSimLogPass("%s.log" % test_name) + + + def assertSimLogPass(self, path: str): + self.testcase.assertTrue(os.path.isfile(path)) + + with open(path, encoding="utf-8") as f: + for line in f: + if line.startswith("Error:"): + self.testcase.fail(line) + elif line.startswith("Fatal:"): + self.testcase.fail(line) + elif line.startswith("FATAL_ERROR:"): + self.testcase.fail(line) diff --git a/tests/lib/sv_line_anchor.py b/tests/lib/sv_line_anchor.py new file mode 100644 index 0000000..1f09ec1 --- /dev/null +++ b/tests/lib/sv_line_anchor.py @@ -0,0 +1,10 @@ +from jinja2_simple_tags import StandaloneTag + +class SVLineAnchor(StandaloneTag): + """ + Define a custom Jinja tag that emits a SystemVerilog `line directive so that + assertion messages can get properly back-annotated + """ + tags = {"sv_line_anchor"} + def render(self): + return f'`line {self.lineno + 1} "{self.template}" 0' diff --git a/tests/lib/synth_testcase.py b/tests/lib/synth_testcase.py new file mode 100644 index 0000000..552902d --- /dev/null +++ b/tests/lib/synth_testcase.py @@ -0,0 +1,39 @@ +from typing import List +import os + +import pytest + +from .base_testcase import BaseTestCase +from .synthesizers import get_synthesizer_cls + +class SynthTestCase(BaseTestCase): + + def _get_synth_files(self) -> List[str]: + files = [] + files.extend(self.cpuif.get_synth_files()) + files.append("regblock_pkg.sv") + files.append("regblock.sv") + + return files + + def setUp(self) -> None: + name = self.request.config.getoption("--synth-tool") + synth_cls = get_synthesizer_cls(name) + if synth_cls is None: + pytest.skip() + super().setUp() + + def run_synth(self) -> None: + name = self.request.config.getoption("--synth-tool") + synth_cls = get_synthesizer_cls(name) + synth = synth_cls(self) + + # cd into the build directory + cwd = os.getcwd() + os.chdir(self.get_run_dir()) + + try: + synth.run() + finally: + # cd back + os.chdir(cwd) diff --git a/tests/lib/synthesizers/__init__.py b/tests/lib/synthesizers/__init__.py new file mode 100644 index 0000000..58d1c7e --- /dev/null +++ b/tests/lib/synthesizers/__init__.py @@ -0,0 +1,30 @@ +from typing import List, Optional, Type +import functools + +from .base import Synthesizer +from .vivado import Vivado + +ALL_SYNTHESIZERS: List[Synthesizer] +ALL_SYNTHESIZERS = [ + Vivado, +] + +@functools.lru_cache() +def get_synthesizer_cls(name: str) -> Optional[Type[Synthesizer]]: + if name == "skip": + return None + + if name == "auto": + # Find the first tool that is installed + for synth_cls in ALL_SYNTHESIZERS: + if synth_cls.is_installed(): + return synth_cls + raise ValueError("Could not find any installed synthesis tools") + + # Look up which explicit synth tool name was specified + for synth_cls in ALL_SYNTHESIZERS: + if synth_cls.name == name: + if not synth_cls.is_installed(): + raise ValueError("Synthesis tool '%s' is not installed" % synth_cls.name) + return synth_cls + raise RuntimeError diff --git a/tests/lib/synthesizers/base.py b/tests/lib/synthesizers/base.py new file mode 100644 index 0000000..5a3e36e --- /dev/null +++ b/tests/lib/synthesizers/base.py @@ -0,0 +1,17 @@ +from typing import TYPE_CHECKING, List + +if TYPE_CHECKING: + from ..synth_testcase import SynthTestCase + +class Synthesizer: + name = "" + + @classmethod + def is_installed(cls) -> bool: + raise NotImplementedError + + def __init__(self, testcase: 'SynthTestCase' = None) -> None: + self.testcase = testcase + + def run(self) -> None: + raise NotImplementedError diff --git a/tests/lib/synthesizers/vivado.py b/tests/lib/synthesizers/vivado.py new file mode 100644 index 0000000..857fac0 --- /dev/null +++ b/tests/lib/synthesizers/vivado.py @@ -0,0 +1,29 @@ +import os +import subprocess +import shutil + +from .base import Synthesizer + +class Vivado(Synthesizer): + name = "vivado" + + @classmethod + def is_installed(cls) -> bool: + return shutil.which("vivado") is not None + + def run(self) -> None: + script = os.path.join( + os.path.dirname(__file__), + "vivado_scripts/run.tcl" + ) + + cmd = [ + "vivado", "-nojournal", "-notrace", + "-mode", "batch", + "-log", "out.log", + "-source", script, + "-tclargs" + ] + cmd.extend(self.testcase._get_synth_files()) + + subprocess.run(cmd, check=True) diff --git a/tests/lib/synthesizers/vivado_scripts/constr.xdc b/tests/lib/synthesizers/vivado_scripts/constr.xdc new file mode 100644 index 0000000..3c063c1 --- /dev/null +++ b/tests/lib/synthesizers/vivado_scripts/constr.xdc @@ -0,0 +1,8 @@ + +create_clock -period 10.000 -name clk [get_ports clk] + +set_input_delay -clock [get_clocks clk] -min -add_delay 0.000 [get_ports -filter {(DIRECTION == IN) && (NAME != clk)}] +set_input_delay -clock [get_clocks clk] -max -add_delay 0.000 [get_ports -filter {(DIRECTION == IN) && (NAME != clk)}] + +set_output_delay -clock [get_clocks clk] -min -add_delay 0.000 [get_ports -filter {DIRECTION == OUT}] +set_output_delay -clock [get_clocks clk] -max -add_delay 0.000 [get_ports -filter {DIRECTION == OUT}] diff --git a/tests/lib/synthesizers/vivado_scripts/run.tcl b/tests/lib/synthesizers/vivado_scripts/run.tcl new file mode 100644 index 0000000..db793b7 --- /dev/null +++ b/tests/lib/synthesizers/vivado_scripts/run.tcl @@ -0,0 +1,34 @@ +set this_dir [file dirname [file normalize [info script]]] +set files $argv + + +# Multi-driven +set_msg_config -id {[Synth 8-6858]} -new_severity "ERROR" +set_msg_config -id {[Synth 8-6859]} -new_severity "ERROR" + +# Implicit net +set_msg_config -id {[Synth 8-992]} -new_severity "ERROR" + +# Non-combo always_comb +set_msg_config -id {[Synth 8-87]} -new_severity "ERROR" + +# Latch +set_msg_config -id {[Synth 8-327]} -new_severity "ERROR" + +# Timing loop +set_msg_config -id {[Synth 8-295]} -new_severity "ERROR" + +# Promote all critical warnings to errors +set_msg_config -severity {CRITICAL WARNING} -new_severity "ERROR" + + +set_part [lindex [get_parts] 0] +read_verilog -sv $files +read_xdc $this_dir/constr.xdc +synth_design -top regblock -mode out_of_context + +#write_checkpoint -force synth.dcp + +if {[get_msg_config -count -severity {CRITICAL WARNING}] || [get_msg_config -count -severity ERROR]} { + error "Encountered errors" +} diff --git a/tests/lib/tb_base.sv b/tests/lib/tb_base.sv new file mode 100644 index 0000000..a6c93d6 --- /dev/null +++ b/tests/lib/tb_base.sv @@ -0,0 +1,128 @@ +{% sv_line_anchor %} +module tb; + timeunit 10ps; + timeprecision 1ps; + + class bitswap_cls #(W=1); + static function logic [W-1:0] bitswap(logic [W-1:0] x); + logic [W-1:0] result; + result = {<<{x}}; + return result; + endfunction + endclass + `define bitswap(x) (bitswap_cls#($bits(x))::bitswap(x)) + + logic rst = '1; + logic clk = '0; + initial forever begin + #5ns; + clk = ~clk; + end + + logic rst_n, arst, arst_n; + assign rst_n = ~rst; + assign arst = rst; + assign arst_n = ~rst; + + //-------------------------------------------------------------------------- + // DUT Signal declarations + //-------------------------------------------------------------------------- +{%- if exporter.hwif.has_input_struct %} + regblock_pkg::regblock__in_t hwif_in; +{%- endif %} + +{%- if exporter.hwif.has_output_struct %} + regblock_pkg::regblock__out_t hwif_out; +{%- endif %} + +{%- if exporter.ds.has_paritycheck %} + logic parity_error; +{%- endif %} + + +{%- block declarations %} +{%- endblock %} + + //-------------------------------------------------------------------------- + // Clocking + //-------------------------------------------------------------------------- + default clocking cb @(posedge clk); + default input #1step output #1; + output rst; +{%- if exporter.hwif.has_input_struct and testcase.clocking_hwif_in %} + output hwif_in; +{%- endif %} + +{%- if exporter.hwif.has_output_struct and testcase.clocking_hwif_out %} + input hwif_out; +{%- endif %} + +{%- if exporter.ds.has_paritycheck %} + input parity_error; +{%- endif %} + +{%- filter indent %} +{%- block clocking_dirs %} +{%- endblock %} +{%- endfilter %} + endclocking + + //-------------------------------------------------------------------------- + // CPUIF + //-------------------------------------------------------------------------- + {{testcase.cpuif.get_tb_inst(testcase, exporter)|indent}} + + //-------------------------------------------------------------------------- + // DUT + //-------------------------------------------------------------------------- + {% sv_line_anchor %} + regblock dut (.*); + +{%- if exporter.hwif.has_output_struct %} + {% sv_line_anchor %} + initial begin + logic [$bits(hwif_out)-1:0] tmp; + forever begin + ##1; + tmp = {>>{hwif_out}}; // Workaround for Xilinx's xsim - assign to tmp variable + if(!rst) assert(!$isunknown(tmp)) else $error("hwif_out has X's!"); + {%- if exporter.ds.has_paritycheck %} + if(!rst) assert(!$isunknown(parity_error)) else $error("parity_error has X's!"); + {%- endif %} + end + end +{%- endif %} + {% sv_line_anchor %} + +{%- block dut_support %} +{%- endblock %} + + //-------------------------------------------------------------------------- + // Test Sequence + //-------------------------------------------------------------------------- + initial begin + cb.rst <= '1; + {%- if exporter.hwif.has_input_struct and testcase.init_hwif_in %} + cb.hwif_in <= '{default: '0}; + {%- endif %} + + begin + {%- filter indent(8) %} + {%- block seq %} + {%- endblock %} + {%- endfilter %} + end + {% sv_line_anchor %} + ##5; + $finish(); + end + + //-------------------------------------------------------------------------- + // Monitor for timeout + //-------------------------------------------------------------------------- + initial begin + ##{{testcase.timeout_clk_cycles}}; + $fatal(1, "Test timed out after {{testcase.timeout_clk_cycles}} clock cycles"); + end + +endmodule diff --git a/tests/lib/test_params.py b/tests/lib/test_params.py new file mode 100644 index 0000000..3615561 --- /dev/null +++ b/tests/lib/test_params.py @@ -0,0 +1,7 @@ +from itertools import product + +def get_permutations(spec): + param_list = [] + for v in product(*spec.values()): + param_list.append(dict(zip(spec, v))) + return param_list diff --git a/tests/mypy.ini b/tests/mypy.ini new file mode 100644 index 0000000..052d30e --- /dev/null +++ b/tests/mypy.ini @@ -0,0 +1,7 @@ +[mypy] +disallow_incomplete_defs = True +disallow_untyped_defs = True +warn_unused_configs = True +warn_unused_ignores = True +warn_unreachable = True +disallow_untyped_calls = True diff --git a/tests/pylint.rc b/tests/pylint.rc new file mode 100644 index 0000000..b3eb902 --- /dev/null +++ b/tests/pylint.rc @@ -0,0 +1,571 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10.0 + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS, parser, docs, test + +# Add files or directories matching the regex patterns to the ignore-list. The +# regex matches against paths. +ignore-paths= + +# Files or directories matching the regex patterns are skipped. The regex +# matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=0 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable= + # Disable for now during development + fixme, + + # User ignored limits + too-many-lines, + too-many-locals, + too-many-branches, + too-many-return-statements, + too-few-public-methods, + too-many-public-methods, + too-many-statements, + too-many-instance-attributes, + too-many-function-args, + line-too-long, + + # Noise / Don't care + no-else-return, + unused-variable, + invalid-name, + missing-docstring, + abstract-method, + protected-access, + duplicate-code, + unused-argument, + consider-using-f-string + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=no + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: en_GB (aspell), en_AU +# (aspell), en_US (hunspell), en (aspell), en_CA (aspell). +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear and the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=no + +# Signatures are removed from the similarity computation +ignore-signatures=no + +# Minimum lines number of a similarity. +min-similarity-lines=10 + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=110 + +# Maximum number of lines in a module. +max-module-lines=2000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[DESIGN] + +# List of qualified class names to ignore when countint class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=16 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=builtin.BaseException, + builtin.Exception diff --git a/tests/pytest.ini b/tests/pytest.ini new file mode 100644 index 0000000..afd91e9 --- /dev/null +++ b/tests/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +python_files = test_*.py testcase.py diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..440db54 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,11 @@ +# hold back: https://github.com/kevlened/pytest-parallel/issues/118 +pytest<7.2 + +parameterized +pytest-parallel # TODO: deprecated. migrate to pytest-xdist +jinja2-simple-tags +pylint +mypy +pytest-cov +coveralls>=3.0.0 +types-setuptools diff --git a/tests/run.sh b/tests/run.sh new file mode 100755 index 0000000..b8106f7 --- /dev/null +++ b/tests/run.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +set -e + +cd "$(dirname "$0")" + +# Initialize venv +rm -rf .venv +python3 -m venv .venv +source .venv/bin/activate + +# Install test dependencies +pip install -r requirements.txt + +# Install dut +pip install -e "../[cli]" + +# Run lint +pylint --rcfile pylint.rc ../src/peakrdl_regblock + +# Run static type checking +mypy ../src/peakrdl_regblock + +# Run unit tests +pytest --workers auto --cov=peakrdl_regblock --synth-tool skip + +# Generate coverage report +coverage html -i -d htmlcov diff --git a/tests/test_bitwise_reduce/__init__.py b/tests/test_bitwise_reduce/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_bitwise_reduce/regblock.rdl b/tests/test_bitwise_reduce/regblock.rdl new file mode 100644 index 0000000..6d770e1 --- /dev/null +++ b/tests/test_bitwise_reduce/regblock.rdl @@ -0,0 +1,8 @@ +addrmap top { + reg { + field { + sw=rw; hw=r; + anded; ored; xored; + } f[7:0] = 0; + } r1; +}; diff --git a/tests/test_bitwise_reduce/tb_template.sv b/tests/test_bitwise_reduce/tb_template.sv new file mode 100644 index 0000000..9222491 --- /dev/null +++ b/tests/test_bitwise_reduce/tb_template.sv @@ -0,0 +1,44 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + ##1; + cb.rst <= '0; + ##1; + + cpuif.write('h0, 'h00); + @cb; + assert(cb.hwif_out.r1.f.anded == 1'b0); + assert(cb.hwif_out.r1.f.ored == 1'b0); + assert(cb.hwif_out.r1.f.xored == 1'b0); + + cpuif.write('h0, 'h01); + @cb; + assert(cb.hwif_out.r1.f.anded == 1'b0); + assert(cb.hwif_out.r1.f.ored == 1'b1); + assert(cb.hwif_out.r1.f.xored == 1'b1); + + cpuif.write('h0, 'h02); + @cb; + assert(cb.hwif_out.r1.f.anded == 1'b0); + assert(cb.hwif_out.r1.f.ored == 1'b1); + assert(cb.hwif_out.r1.f.xored == 1'b1); + + cpuif.write('h0, 'h03); + @cb; + assert(cb.hwif_out.r1.f.anded == 1'b0); + assert(cb.hwif_out.r1.f.ored == 1'b1); + assert(cb.hwif_out.r1.f.xored == 1'b0); + + cpuif.write('h0, 'hFE); + @cb; + assert(cb.hwif_out.r1.f.anded == 1'b0); + assert(cb.hwif_out.r1.f.ored == 1'b1); + assert(cb.hwif_out.r1.f.xored == 1'b1); + + cpuif.write('h0, 'hFF); + @cb; + assert(cb.hwif_out.r1.f.anded == 1'b1); + assert(cb.hwif_out.r1.f.ored == 1'b1); + assert(cb.hwif_out.r1.f.xored == 1'b0); +{% endblock %} diff --git a/tests/test_bitwise_reduce/testcase.py b/tests/test_bitwise_reduce/testcase.py new file mode 100644 index 0000000..835b5ef --- /dev/null +++ b/tests/test_bitwise_reduce/testcase.py @@ -0,0 +1,5 @@ +from ..lib.sim_testcase import SimTestCase + +class Test(SimTestCase): + def test_dut(self): + self.run_test() diff --git a/tests/test_buffered_swacc_swmod/__init__.py b/tests/test_buffered_swacc_swmod/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_buffered_swacc_swmod/regblock.rdl b/tests/test_buffered_swacc_swmod/regblock.rdl new file mode 100644 index 0000000..f4105ec --- /dev/null +++ b/tests/test_buffered_swacc_swmod/regblock.rdl @@ -0,0 +1,60 @@ +addrmap top { + default regwidth = 16; + default accesswidth = 8; + + reg { + buffer_reads; + field { + sw=r; hw=w; + swacc; + } f[16]; + } r1; + + reg { + buffer_reads; + buffer_writes; + field { + sw=rw; hw=r; + swmod; + } f[16] = 0x4020; + } r2; + + reg { + buffer_reads; + buffer_writes; + field { + sw=rw; hw=r; + swmod; + rclr; + } f[16] = 0x1030; + } r3; + + reg { + buffer_reads; + buffer_writes; + field { + sw=rw; hw=r; + swacc; + swmod; + } f[16] = 0x1234; + } r4; + + reg { + buffer_writes; + field { + sw=rw; hw=r; + swacc; + swmod; + } f[16] = 0xABCD; + } r5; + + reg { + buffer_reads; + field { + sw=r; hw=rw; + we; + swmod; + rclr; + } f[16] = 0x1030; + } r6; +}; diff --git a/tests/test_buffered_swacc_swmod/tb_template.sv b/tests/test_buffered_swacc_swmod/tb_template.sv new file mode 100644 index 0000000..019c669 --- /dev/null +++ b/tests/test_buffered_swacc_swmod/tb_template.sv @@ -0,0 +1,197 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + logic [15:0] counter; + logic [7:0] rd_data_l; + logic [7:0] rd_data_h; + logic [15:0] latched_data; + int event_count; + bit fired; + latched_data = 'x; + + ##1; + cb.rst <= '0; + ##1; + + // Verify that hwif gets sampled at the same cycle as swacc strobe + counter = 'h10; + cb.hwif_in.r1.f.next <= counter; + @cb; + event_count = 0; + fork + begin + ##0; + forever begin + counter++; + cb.hwif_in.r1.f.next <= counter; + @cb; + if(cb.hwif_out.r1.f.swacc) begin + latched_data = counter; + event_count++; + end + end + end + + begin + cpuif.read('h0, rd_data_l); + cpuif.read('h1, rd_data_h); + repeat(3) @cb; + end + join_any + disable fork; + assert({rd_data_h, rd_data_l} == latched_data) else $error("Read returned 0x%0x but swacc strobed during 0x%0x", {rd_data_h, rd_data_l}, latched_data); + assert(event_count == 1) else $error("Observed excess swacc events: %0d", event_count); + + + // Verify that hwif changes 1 cycle after swmod + fired = 0; + fork + begin + ##0; + forever begin + assert(cb.hwif_out.r2.f.value == 'h4020); + if(cb.hwif_out.r2.f.swmod) break; + @cb; + end + fired = 1; + @cb; + forever begin + assert(cb.hwif_out.r2.f.value == 'h4221); + assert(cb.hwif_out.r2.f.swmod == 0); + @cb; + end + end + + begin + cpuif.write('h2, 'h21); + cpuif.write('h3, 'h42); + repeat(3) @cb; + end + join_any + disable fork; + assert(fired); + + // Verify that hwif changes 1 cycle after swmod + fired = 0; + fork + begin + ##0; + forever begin + assert(cb.hwif_out.r3.f.value == 'h1030); + if(cb.hwif_out.r3.f.swmod) break; + @cb; + end + fired = 1; + @cb; + forever begin + assert(cb.hwif_out.r3.f.value == 0); + assert(cb.hwif_out.r3.f.swmod == 0); + @cb; + end + end + + begin + cpuif.assert_read('h4, 'h30); + cpuif.assert_read('h5, 'h10); + repeat(3) @cb; + end + join_any + disable fork; + assert(fired); + + // Verify swacc and swmod assert when written + fired = 0; + fork + begin + ##0; + forever begin + assert(cb.hwif_out.r4.f.value == 'h1234); + if(cb.hwif_out.r4.f.swmod || cb.hwif_out.r4.f.swacc) begin + assert(cb.hwif_out.r4.f.swmod == 1); + assert(cb.hwif_out.r4.f.swacc == 1); + break; + end + @cb; + end + fired = 1; + @cb; + forever begin + assert(cb.hwif_out.r4.f.value == 'h4567); + assert(cb.hwif_out.r4.f.swmod == 0); + assert(cb.hwif_out.r4.f.swacc == 0); + @cb; + end + end + + begin + cpuif.write('h6, 'h67); + cpuif.write('h7, 'h45); + repeat(3) @cb; + end + join_any + disable fork; + assert(fired); + + // Verify swacc and swmod assert when written + fired = 0; + fork + begin + ##0; + forever begin + assert(cb.hwif_out.r5.f.value == 'hABCD); + if(cb.hwif_out.r5.f.swmod || cb.hwif_out.r5.f.swacc) begin + assert(cb.hwif_out.r5.f.swmod == 1); + assert(cb.hwif_out.r5.f.swacc == 1); + break; + end + @cb; + end + fired = 1; + @cb; + forever begin + assert(cb.hwif_out.r5.f.value == 'hEF12); + assert(cb.hwif_out.r5.f.swmod == 0); + assert(cb.hwif_out.r5.f.swacc == 0); + @cb; + end + end + + begin + cpuif.write('h8, 'h12); + cpuif.write('h9, 'hEF); + repeat(3) @cb; + end + join_any + disable fork; + assert(fired); + + // Verify that hwif changes 1 cycle after swmod + fired = 0; + fork + begin + ##0; + forever begin + assert(cb.hwif_out.r6.f.value == 'h1030); + if(cb.hwif_out.r6.f.swmod) break; + @cb; + end + fired = 1; + @cb; + forever begin + assert(cb.hwif_out.r6.f.value == 0); + assert(cb.hwif_out.r6.f.swmod == 0); + @cb; + end + end + + begin + cpuif.assert_read('ha, 'h30); + cpuif.assert_read('hb, 'h10); + repeat(3) @cb; + end + join_any + disable fork; + assert(fired); + +{% endblock %} diff --git a/tests/test_buffered_swacc_swmod/testcase.py b/tests/test_buffered_swacc_swmod/testcase.py new file mode 100644 index 0000000..835b5ef --- /dev/null +++ b/tests/test_buffered_swacc_swmod/testcase.py @@ -0,0 +1,5 @@ +from ..lib.sim_testcase import SimTestCase + +class Test(SimTestCase): + def test_dut(self): + self.run_test() diff --git a/tests/test_counter_basics/__init__.py b/tests/test_counter_basics/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_counter_basics/regblock.rdl b/tests/test_counter_basics/regblock.rdl new file mode 100644 index 0000000..db8286d --- /dev/null +++ b/tests/test_counter_basics/regblock.rdl @@ -0,0 +1,69 @@ +addrmap top { + reg { + field { + sw=r; hw=na; counter; + } implied_up[3:0] = 0xD; + field { + sw=r; hw=na; counter; + incrvalue=1; + } up[7:4] = 0xD; + field { + sw=r; hw=na; counter; + decrvalue=1; + } down[11:8] = 0x4; + field { + sw=r; hw=r; counter; + incrvalue=1; + decrvalue=1; + overflow; underflow; + } updown[15:12] = 0; + + field { + sw=r; hw=na; counter; + incrvalue=3; + decrvalue=3; + } updown2[19:16] = 0; + + field { + sw=r; hw=na; counter; + incrwidth=2; + decrwidth=2; + } updown3[23:20] = 0; + + field { + sw=r; hw=na; counter; + } updown4[27:24] = 0; + + field { + sw=rw; hw=na; + } step[29:28] = 0; + updown4->incrvalue = step; + updown4->decrvalue = step; + + field { + sw=w; hw=r; singlepulse; + } do_count_up[30:30] = 0; + field { + sw=w; hw=r; singlepulse; + } do_count_down[31:31] = 0; + updown2->incr = do_count_up; + updown2->decr = do_count_down; + updown3->incr = do_count_up; + updown3->decr = do_count_down; + updown4->incr = updown3->incr; + updown4->decr = updown3->decr; + } simple @ 0x0; + + + reg { + field { + sw=r; hw=na; rclr; counter; + } overflow_count[8] = 0; + + field { + sw=r; hw=na; rclr; counter; + } underflow_count[8] = 0; + } wrap_counter @ 0x4; + wrap_counter.overflow_count->incr = simple.updown3->overflow; + wrap_counter.underflow_count->incr = simple.updown3->underflow; +}; diff --git a/tests/test_counter_basics/tb_template.sv b/tests/test_counter_basics/tb_template.sv new file mode 100644 index 0000000..0629271 --- /dev/null +++ b/tests/test_counter_basics/tb_template.sv @@ -0,0 +1,147 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + ##1; + cb.rst <= '0; + ##1; + + //-------------------------------------------------------------------------- + // Test simple counters + //-------------------------------------------------------------------------- + // up + cpuif.assert_read('h0, 'hD, 'h000F); + cb.hwif_in.simple.implied_up.incr <= '1; + repeat(4) @cb; + cb.hwif_in.simple.implied_up.incr <= '0; + cpuif.assert_read('h0, 'h1, 'h000F); + + // up + cpuif.assert_read('h0, 'hD0, 'h00F0); + cb.hwif_in.simple.up.incr <= '1; + repeat(4) @cb; + cb.hwif_in.simple.up.incr <= '0; + cpuif.assert_read('h0, 'h10, 'h00F0); + + // down + cpuif.assert_read('h0, 'h400, 'h0F00); + cb.hwif_in.simple.down.decr <= '1; + repeat(6) @cb; + cb.hwif_in.simple.down.decr <= '0; + cpuif.assert_read('h0, 'hE00, 'h0F00); + + // up/down via hw + cpuif.assert_read('h0, 'h0000, 'hF000); + cb.hwif_in.simple.updown.incr <= '1; + repeat(6) @cb; + cb.hwif_in.simple.updown.incr <= '0; + cpuif.assert_read('h0, 'h6000, 'hF000); + cb.hwif_in.simple.updown.decr <= '1; + repeat(6) @cb; + cb.hwif_in.simple.updown.decr <= '0; + cpuif.assert_read('h0, 'h0000, 'hF000); + cb.hwif_in.simple.updown.decr <= '1; + repeat(6) @cb; + cb.hwif_in.simple.updown.decr <= '0; + cpuif.assert_read('h0, 'hA000, 'hF000); + cb.hwif_in.simple.updown.incr <= '1; + repeat(6) @cb; + cb.hwif_in.simple.updown.incr <= '0; + cpuif.assert_read('h0, 'h0000, 'hF000); + + // up/down external underflow + fork + begin + ##0; + forever begin + assert(cb.hwif_out.simple.updown.value == 0); + if(cb.hwif_out.simple.updown.underflow) break; + @cb; + end + @cb; + forever begin + assert(cb.hwif_out.simple.updown.value == 15); + assert(cb.hwif_out.simple.updown.underflow == 0); + @cb; + end + end + + begin + repeat(2) @cb; + cb.hwif_in.simple.updown.decr <= '1; + @cb; + cb.hwif_in.simple.updown.decr <= '0; + repeat(2) @cb; + end + join_any + disable fork; + + // up/down external overflow + fork + begin + ##0; + forever begin + assert(cb.hwif_out.simple.updown.value == 15); + if(cb.hwif_out.simple.updown.overflow) break; + @cb; + end + @cb; + forever begin + assert(cb.hwif_out.simple.updown.value == 0); + assert(cb.hwif_out.simple.updown.overflow == 0); + @cb; + end + end + + begin + repeat(2) @cb; + cb.hwif_in.simple.updown.incr <= '1; + @cb; + cb.hwif_in.simple.updown.incr <= '0; + repeat(2) @cb; + end + join_any + disable fork; + + + // up/down via sw + cpuif.assert_read('h0, 'h00000, 'hF0000); + repeat(3) cpuif.write('h0, 'h4000_0000); // incr + cpuif.assert_read('h0, 'h90000, 'hF0000); + repeat(3) cpuif.write('h0, 'h8000_0000); // decr + cpuif.assert_read('h0, 'h00000, 'hF0000); + repeat(3) cpuif.write('h0, 'h8000_0000); // decr + cpuif.assert_read('h0, 'h70000, 'hF0000); + repeat(3) cpuif.write('h0, 'h4000_0000); // incr + cpuif.assert_read('h0, 'h00000, 'hF0000); + + // up/down via hw + external dynamic stepsize + cpuif.assert_read('h0, 'h000000, 'hF00000); + cb.hwif_in.simple.updown3.incrvalue <= 'h2; + repeat(3) cpuif.write('h0, 'h4000_0000); // incr + cpuif.assert_read('h0, 'h600000, 'hF00000); + cpuif.assert_read('h4, 'h00_00); // no overflows or underflows + cb.hwif_in.simple.updown3.decrvalue <= 'h3; + repeat(3) cpuif.write('h0, 'h8000_0000); // decr + cpuif.assert_read('h0, 'hD00000, 'hF00000); + cpuif.assert_read('h4, 'h01_00); // one underflow + cb.hwif_in.simple.updown3.incrvalue <= 'h1; + repeat(2) cpuif.write('h0, 'h4000_0000); // incr + cpuif.assert_read('h0, 'hF00000, 'hF00000); + cpuif.assert_read('h4, 'h00_00); // no overflows or underflows + repeat(1) cpuif.write('h0, 'h4000_0000); // incr + cpuif.assert_read('h0, 'h000000, 'hF00000); + cpuif.assert_read('h4, 'h00_01); // one overflow + repeat(32) cpuif.write('h0, 'h4000_0000); // incr + cpuif.assert_read('h0, 'h000000, 'hF00000); + cpuif.assert_read('h4, 'h00_02); // overflow + + // up/down via hw + referenced dynamic stepsize + cpuif.assert_read('h0, 'h0000000, 'hF000000); + repeat(4) cpuif.write('h0, 'h4000_0000 + (2'h3 << 28)); // + 3 + cpuif.assert_read('h0, 'hC000000, 'hF000000); + repeat(4) cpuif.write('h0, 'h8000_0000 + (2'h1 << 28)); // - 1 + cpuif.assert_read('h0, 'h8000000, 'hF000000); + repeat(2) cpuif.write('h0, 'h8000_0000 + (2'h3 << 28)); // - 3 + cpuif.assert_read('h0, 'h2000000, 'hF000000); +{% endblock %} diff --git a/tests/test_counter_basics/testcase.py b/tests/test_counter_basics/testcase.py new file mode 100644 index 0000000..835b5ef --- /dev/null +++ b/tests/test_counter_basics/testcase.py @@ -0,0 +1,5 @@ +from ..lib.sim_testcase import SimTestCase + +class Test(SimTestCase): + def test_dut(self): + self.run_test() diff --git a/tests/test_counter_saturate/__init__.py b/tests/test_counter_saturate/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_counter_saturate/regblock.rdl b/tests/test_counter_saturate/regblock.rdl new file mode 100644 index 0000000..8987372 --- /dev/null +++ b/tests/test_counter_saturate/regblock.rdl @@ -0,0 +1,88 @@ +addrmap top { + field strobe_t { + sw=w; hw=r; singlepulse; + }; + + reg { + field { + sw=r; hw=r; counter; + incrsaturate; + decrsaturate; + } count[8] = 0; + + strobe_t increment[9:9] = 0; + strobe_t decrement[10:10] = 0; + strobe_t clear[11:11] = 0; + strobe_t set[12:12] = 0; + + field { + sw=rw; hw=na; + } step[23:16] = 1; + + count->incr = increment; + count->decr = decrement; + count->hwclr = clear; + count->hwset = set; + count->incrvalue = step; + count->decrvalue = step; + } saturate_via_bool @ 0x0; + + + reg { + field { + sw=r; hw=r; counter; + incrsaturate = 250; + decrsaturate = 5; + } count[8] = 0; + + strobe_t increment[9:9] = 0; + strobe_t decrement[10:10] = 0; + strobe_t clear[11:11] = 0; + strobe_t set[12:12] = 0; + + field { + sw=rw; hw=na; + } step[23:16] = 1; + + count->incr = increment; + count->decr = decrement; + count->hwclr = clear; + count->hwset = set; + count->decrvalue = step; + count->incrvalue = count->decrvalue; + } saturate_via_const @ 0x4; + + + reg { + field { + sw=r; hw=r; counter; + } count[8] = 0; + + strobe_t increment[9:9] = 0; + strobe_t decrement[10:10] = 0; + strobe_t clear[11:11] = 0; + strobe_t set[12:12] = 0; + + field { + sw=rw; hw=na; + } step[23:16] = 1; + + count->incr = increment; + count->decr = decrement; + count->hwclr = clear; + count->hwset = set; + count->incrvalue = step; + count->decrvalue = count->incrvalue; + } saturate_via_ref @ 0x8; + + reg { + field { + sw=rw; hw=na; + } min[8] = 0x00; + field { + sw=rw; hw=na; + } max[8] = 0xFF; + } saturate_control @ 0xC; + saturate_via_ref.count -> decrsaturate = saturate_control.min; + saturate_via_ref.count -> incrsaturate = saturate_control.max; +}; diff --git a/tests/test_counter_saturate/tb_template.sv b/tests/test_counter_saturate/tb_template.sv new file mode 100644 index 0000000..7158c07 --- /dev/null +++ b/tests/test_counter_saturate/tb_template.sv @@ -0,0 +1,214 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + ##1; + cb.rst <= '0; + ##1; + + // counter controls are the same for each sub-test + `define incr (1<<9) + `define decr (1<<10) + `define clr (1<<11) + `define set (1<<12) + `define step(n) (n<<16) + + //-------------------------------------------------------------------------- + // Test incrsaturate = true; decrsaturate = true; + //-------------------------------------------------------------------------- + cpuif.assert_read('h0, 'h00, 'hFF); + + // incrsaturate via +1 + cpuif.write('h0, `set); + cpuif.assert_read('h0, 'hFF, 'hFF); + cpuif.write('h0, `decr + `step(1)); + cpuif.assert_read('h0, 'hFE, 'hFF); + cpuif.write('h0, `incr + `step(1)); + cpuif.assert_read('h0, 'hFF, 'hFF); + cpuif.write('h0, `incr + `step(1)); + cpuif.assert_read('h0, 'hFF, 'hFF); + + // decrsaturate via +1 + cpuif.write('h0, `clr); + cpuif.assert_read('h0, 'h00, 'hFF); + cpuif.write('h0, `incr + `step(1)); + cpuif.assert_read('h0, 'h01, 'hFF); + cpuif.write('h0, `decr + `step(1)); + cpuif.assert_read('h0, 'h00, 'hFF); + cpuif.write('h0, `decr + `step(1)); + cpuif.assert_read('h0, 'h00, 'hFF); + + // incrsaturate via larger steps + cpuif.write('h0, `set); + cpuif.assert_read('h0, 'hFF, 'hFF); + cpuif.write('h0, `decr + `step(1)); + cpuif.assert_read('h0, 'hFE, 'hFF); + cpuif.write('h0, `incr + `step(2)); + cpuif.assert_read('h0, 'hFF, 'hFF); + cpuif.write('h0, `incr + `step(3)); + cpuif.assert_read('h0, 'hFF, 'hFF); + cpuif.write('h0, `incr + `step(255)); + cpuif.assert_read('h0, 'hFF, 'hFF); + + // decrsaturate via larger steps + cpuif.write('h0, `clr); + cpuif.assert_read('h0, 'h00, 'hFF); + cpuif.write('h0, `incr + `step(1)); + cpuif.assert_read('h0, 'h01, 'hFF); + cpuif.write('h0, `decr + `step(2)); + cpuif.assert_read('h0, 'h00, 'hFF); + cpuif.write('h0, `decr + `step(3)); + cpuif.assert_read('h0, 'h00, 'hFF); + cpuif.write('h0, `decr + `step(255)); + cpuif.assert_read('h0, 'h00, 'hFF); + + //-------------------------------------------------------------------------- + // Test incrsaturate = 250; decrsaturate = 5; + //-------------------------------------------------------------------------- + cpuif.assert_read('h4, 'h00, 'hFF); + + // incrsaturate via +1 + cpuif.write('h4, `set); + cpuif.assert_read('h4, 'hFF, 'hFF); + cpuif.write('h4, `decr + `step(1)); + cpuif.assert_read('h4, 'hFE, 'hFF); + cpuif.write('h4, `incr + `step(1)); + cpuif.assert_read('h4, 'hFA, 'hFF); + cpuif.write('h4, `incr + `step(1)); + cpuif.assert_read('h4, 'hFA, 'hFF); + + // decrsaturate via +1 + cpuif.write('h4, `clr); + cpuif.assert_read('h4, 'h00, 'hFF); + cpuif.write('h4, `incr + `step(1)); + cpuif.assert_read('h4, 'h01, 'hFF); + cpuif.write('h4, `decr + `step(1)); + cpuif.assert_read('h4, 'h05, 'hFF); + cpuif.write('h4, `decr + `step(1)); + cpuif.assert_read('h4, 'h05, 'hFF); + + // incrsaturate via larger steps + cpuif.write('h4, `set); + cpuif.assert_read('h4, 'hFF, 'hFF); + cpuif.write('h4, `decr + `step(1)); + cpuif.assert_read('h4, 'hFE, 'hFF); + cpuif.write('h4, `incr + `step(2)); + cpuif.assert_read('h4, 'hFA, 'hFF); + cpuif.write('h4, `incr + `step(3)); + cpuif.assert_read('h4, 'hFA, 'hFF); + cpuif.write('h4, `incr + `step(255)); + cpuif.assert_read('h4, 'hFA, 'hFF); + + // decrsaturate via larger steps + cpuif.write('h4, `clr); + cpuif.assert_read('h4, 'h00, 'hFF); + cpuif.write('h4, `incr + `step(1)); + cpuif.assert_read('h4, 'h01, 'hFF); + cpuif.write('h4, `decr + `step(2)); + cpuif.assert_read('h4, 'h05, 'hFF); + cpuif.write('h4, `decr + `step(3)); + cpuif.assert_read('h4, 'h05, 'hFF); + cpuif.write('h4, `decr + `step(255)); + cpuif.assert_read('h4, 'h05, 'hFF); + + //-------------------------------------------------------------------------- + // Test incrsaturate = ; decrsaturate = ; + //-------------------------------------------------------------------------- + cpuif.assert_read('h8, 'h00, 'hFF); + + // incrsaturate via +1 + cpuif.write('h8, `set); + cpuif.assert_read('h8, 'hFF, 'hFF); + cpuif.write('h8, `decr + `step(1)); + cpuif.assert_read('h8, 'hFE, 'hFF); + cpuif.write('h8, `incr + `step(1)); + cpuif.assert_read('h8, 'hFF, 'hFF); + cpuif.write('h8, `incr + `step(1)); + cpuif.assert_read('h8, 'hFF, 'hFF); + + // decrsaturate via +1 + cpuif.write('h8, `clr); + cpuif.assert_read('h8, 'h00, 'hFF); + cpuif.write('h8, `incr + `step(1)); + cpuif.assert_read('h8, 'h01, 'hFF); + cpuif.write('h8, `decr + `step(1)); + cpuif.assert_read('h8, 'h00, 'hFF); + cpuif.write('h8, `decr + `step(1)); + cpuif.assert_read('h8, 'h00, 'hFF); + + // incrsaturate via larger steps + cpuif.write('h8, `set); + cpuif.assert_read('h8, 'hFF, 'hFF); + cpuif.write('h8, `decr + `step(1)); + cpuif.assert_read('h8, 'hFE, 'hFF); + cpuif.write('h8, `incr + `step(2)); + cpuif.assert_read('h8, 'hFF, 'hFF); + cpuif.write('h8, `incr + `step(3)); + cpuif.assert_read('h8, 'hFF, 'hFF); + cpuif.write('h8, `incr + `step(255)); + cpuif.assert_read('h8, 'hFF, 'hFF); + + // decrsaturate via larger steps + cpuif.write('h8, `clr); + cpuif.assert_read('h8, 'h00, 'hFF); + cpuif.write('h8, `incr + `step(1)); + cpuif.assert_read('h8, 'h01, 'hFF); + cpuif.write('h8, `decr + `step(2)); + cpuif.assert_read('h8, 'h00, 'hFF); + cpuif.write('h8, `decr + `step(3)); + cpuif.assert_read('h8, 'h00, 'hFF); + cpuif.write('h8, `decr + `step(255)); + cpuif.assert_read('h8, 'h00, 'hFF); + + //-------------------------------------------------------------------------- + // Test incrsaturate = ; decrsaturate = ; + //-------------------------------------------------------------------------- + cpuif.write('hc, 'hFA_05); + + cpuif.assert_read('h4, 'h05, 'hFF); + + // incrsaturate via +1 + cpuif.write('h8, `set); + cpuif.assert_read('h8, 'hFF, 'hFF); + cpuif.write('h8, `decr + `step(1)); + cpuif.assert_read('h8, 'hFE, 'hFF); + cpuif.write('h8, `incr + `step(1)); + cpuif.assert_read('h8, 'hFA, 'hFF); + cpuif.write('h8, `incr + `step(1)); + cpuif.assert_read('h8, 'hFA, 'hFF); + + // decrsaturate via +1 + cpuif.write('h8, `clr); + cpuif.assert_read('h8, 'h00, 'hFF); + cpuif.write('h8, `incr + `step(1)); + cpuif.assert_read('h8, 'h01, 'hFF); + cpuif.write('h8, `decr + `step(1)); + cpuif.assert_read('h8, 'h05, 'hFF); + cpuif.write('h8, `decr + `step(1)); + cpuif.assert_read('h8, 'h05, 'hFF); + + // incrsaturate via larger steps + cpuif.write('h8, `set); + cpuif.assert_read('h8, 'hFF, 'hFF); + cpuif.write('h8, `decr + `step(1)); + cpuif.assert_read('h8, 'hFE, 'hFF); + cpuif.write('h8, `incr + `step(2)); + cpuif.assert_read('h8, 'hFA, 'hFF); + cpuif.write('h8, `incr + `step(3)); + cpuif.assert_read('h8, 'hFA, 'hFF); + cpuif.write('h8, `incr + `step(255)); + cpuif.assert_read('h8, 'hFA, 'hFF); + + // decrsaturate via larger steps + cpuif.write('h8, `clr); + cpuif.assert_read('h8, 'h00, 'hFF); + cpuif.write('h8, `incr + `step(1)); + cpuif.assert_read('h8, 'h01, 'hFF); + cpuif.write('h8, `decr + `step(2)); + cpuif.assert_read('h8, 'h05, 'hFF); + cpuif.write('h8, `decr + `step(3)); + cpuif.assert_read('h8, 'h05, 'hFF); + cpuif.write('h8, `decr + `step(255)); + cpuif.assert_read('h8, 'h05, 'hFF); + +{% endblock %} diff --git a/tests/test_counter_saturate/testcase.py b/tests/test_counter_saturate/testcase.py new file mode 100644 index 0000000..835b5ef --- /dev/null +++ b/tests/test_counter_saturate/testcase.py @@ -0,0 +1,5 @@ +from ..lib.sim_testcase import SimTestCase + +class Test(SimTestCase): + def test_dut(self): + self.run_test() diff --git a/tests/test_counter_threshold/__init__.py b/tests/test_counter_threshold/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_counter_threshold/regblock.rdl b/tests/test_counter_threshold/regblock.rdl new file mode 100644 index 0000000..4c5e8f8 --- /dev/null +++ b/tests/test_counter_threshold/regblock.rdl @@ -0,0 +1,36 @@ +addrmap top { + reg { + field { + sw=r; hw=r; counter; + incrthreshold; + decrthreshold; + } count[4] = 0; + } threshold_via_bool @ 0x0; + + + reg { + field { + sw=r; hw=r; counter; + incrthreshold = 10; + decrthreshold = 5; + } count[4] = 0; + } threshold_via_const @ 0x4; + + + reg { + field { + sw=r; hw=r; counter; + } count[4] = 0; + } threshold_via_ref @ 0x8; + + reg { + field { + sw=rw; hw=na; + } min[4] = 0x4; + field { + sw=rw; hw=na; + } max[4] = 0xB; + } threshold_control @ 0xC; + threshold_via_ref.count -> decrthreshold = threshold_control.min; + threshold_via_ref.count -> incrthreshold = threshold_control.max; +}; diff --git a/tests/test_counter_threshold/tb_template.sv b/tests/test_counter_threshold/tb_template.sv new file mode 100644 index 0000000..145bc6e --- /dev/null +++ b/tests/test_counter_threshold/tb_template.sv @@ -0,0 +1,115 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + ##1; + cb.rst <= '0; + ##1; + + //-------------------------------------------------------------------------- + // Test incrthreshold = true; incrthreshold = true; + //-------------------------------------------------------------------------- + cpuif.assert_read('h0, 'h0); + + fork + begin + forever begin + @cb; + if(cb.hwif_out.threshold_via_bool.count.value == 0) + assert(cb.hwif_out.threshold_via_bool.count.decrthreshold == 1'b1); + else + assert(cb.hwif_out.threshold_via_bool.count.decrthreshold == 1'b0); + + if(cb.hwif_out.threshold_via_bool.count.value == 15) + assert(cb.hwif_out.threshold_via_bool.count.incrthreshold == 1'b1); + else + assert(cb.hwif_out.threshold_via_bool.count.incrthreshold == 1'b0); + end + end + + begin + @cb; + cb.hwif_in.threshold_via_bool.count.incr <= '1; + repeat(32) @cb; + cb.hwif_in.threshold_via_bool.count.incr <= '0; + cb.hwif_in.threshold_via_bool.count.decr <= '1; + repeat(32) @cb; + cb.hwif_in.threshold_via_bool.count.decr <= '0; + @cb; + @cb; + end + join_any + disable fork; + + //-------------------------------------------------------------------------- + // Test incrthreshold = 10; incrthreshold = 5; + //-------------------------------------------------------------------------- + cpuif.assert_read('h4, 'h0); + + fork + begin + forever begin + @cb; + if(cb.hwif_out.threshold_via_const.count.value <= 5) + assert(cb.hwif_out.threshold_via_const.count.decrthreshold == 1'b1); + else + assert(cb.hwif_out.threshold_via_const.count.decrthreshold == 1'b0); + + if(cb.hwif_out.threshold_via_const.count.value >= 10) + assert(cb.hwif_out.threshold_via_const.count.incrthreshold == 1'b1); + else + assert(cb.hwif_out.threshold_via_const.count.incrthreshold == 1'b0); + end + end + + begin + @cb; + cb.hwif_in.threshold_via_const.count.incr <= '1; + repeat(32) @cb; + cb.hwif_in.threshold_via_const.count.incr <= '0; + cb.hwif_in.threshold_via_const.count.decr <= '1; + repeat(32) @cb; + cb.hwif_in.threshold_via_const.count.decr <= '0; + @cb; + @cb; + end + join_any + disable fork; + + //-------------------------------------------------------------------------- + // Test incrthreshold = ref; incrthreshold =ref; + //-------------------------------------------------------------------------- + cpuif.assert_read('h8, 'h0); + + fork + begin + forever begin + @cb; + if(cb.hwif_out.threshold_via_ref.count.value <= 4) + assert(cb.hwif_out.threshold_via_ref.count.decrthreshold == 1'b1); + else + assert(cb.hwif_out.threshold_via_ref.count.decrthreshold == 1'b0); + + if(cb.hwif_out.threshold_via_ref.count.value >= 11) + assert(cb.hwif_out.threshold_via_ref.count.incrthreshold == 1'b1); + else + assert(cb.hwif_out.threshold_via_ref.count.incrthreshold == 1'b0); + end + end + + begin + @cb; + cb.hwif_in.threshold_via_ref.count.incr <= '1; + repeat(32) @cb; + cb.hwif_in.threshold_via_ref.count.incr <= '0; + cb.hwif_in.threshold_via_ref.count.decr <= '1; + repeat(32) @cb; + cb.hwif_in.threshold_via_ref.count.decr <= '0; + @cb; + @cb; + end + join_any + disable fork; + + +{% endblock %} diff --git a/tests/test_counter_threshold/testcase.py b/tests/test_counter_threshold/testcase.py new file mode 100644 index 0000000..835b5ef --- /dev/null +++ b/tests/test_counter_threshold/testcase.py @@ -0,0 +1,5 @@ +from ..lib.sim_testcase import SimTestCase + +class Test(SimTestCase): + def test_dut(self): + self.run_test() diff --git a/tests/test_enum/__init__.py b/tests/test_enum/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_enum/regblock.rdl b/tests/test_enum/regblock.rdl new file mode 100644 index 0000000..adb4fe4 --- /dev/null +++ b/tests/test_enum/regblock.rdl @@ -0,0 +1,12 @@ +addrmap top { + enum my_enum { + val_1 = 3 {name = "Value 1";}; + val_2 = 4 {desc = "Second value";}; + }; + reg { + field { + encode = my_enum; + sw=rw; hw=na; + } f[2:0] = my_enum::val_2; + } r0; +}; diff --git a/tests/test_enum/tb_template.sv b/tests/test_enum/tb_template.sv new file mode 100644 index 0000000..457bd2b --- /dev/null +++ b/tests/test_enum/tb_template.sv @@ -0,0 +1,22 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + ##1; + cb.rst <= '0; + ##1; + + // check enum values + assert(regblock_pkg::top__my_enum__val_1 == 'd3); + assert(regblock_pkg::top__my_enum__val_2 == 'd4); + + // check initial conditions + cpuif.assert_read('h0, regblock_pkg::top__my_enum__val_2); + + //--------------------------------- + // set r0 = val_1 + cpuif.write('h0, regblock_pkg::top__my_enum__val_1); + + cpuif.assert_read('h0, regblock_pkg::top__my_enum__val_1); + +{% endblock %} diff --git a/tests/test_enum/testcase.py b/tests/test_enum/testcase.py new file mode 100644 index 0000000..835b5ef --- /dev/null +++ b/tests/test_enum/testcase.py @@ -0,0 +1,5 @@ +from ..lib.sim_testcase import SimTestCase + +class Test(SimTestCase): + def test_dut(self): + self.run_test() diff --git a/tests/test_extended_swacc_swmod/__init__.py b/tests/test_extended_swacc_swmod/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_extended_swacc_swmod/regblock.rdl b/tests/test_extended_swacc_swmod/regblock.rdl new file mode 100644 index 0000000..de8be5c --- /dev/null +++ b/tests/test_extended_swacc_swmod/regblock.rdl @@ -0,0 +1,17 @@ +addrmap top { + default regwidth = 8; + + reg { + field { + sw=r; hw=w; + rd_swacc; + } f[8]; + } r1; + + reg { + field { + sw=rw; hw=r; + wr_swacc; + } f[8] = 20; + } r2; +}; diff --git a/tests/test_extended_swacc_swmod/tb_template.sv b/tests/test_extended_swacc_swmod/tb_template.sv new file mode 100644 index 0000000..ba724e6 --- /dev/null +++ b/tests/test_extended_swacc_swmod/tb_template.sv @@ -0,0 +1,68 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + logic [7:0] counter; + logic [7:0] rd_data; + logic [7:0] latched_data; + int event_count; + latched_data = 'x; + + ##1; + cb.rst <= '0; + ##1; + + // Verify that hwif gets sampled at the same cycle as rd_swacc strobe + counter = 'h10; + cb.hwif_in.r1.f.next <= counter; + @cb; + event_count = 0; + fork + begin + ##0; + forever begin + counter++; + cb.hwif_in.r1.f.next <= counter; + @cb; + if(cb.hwif_out.r1.f.rd_swacc) begin + latched_data = counter; + event_count++; + end + end + end + + begin + cpuif.read('h0, rd_data); + @cb; + end + join_any + disable fork; + assert(rd_data == latched_data) else $error("Read returned 0x%0x but rd_swacc strobed during 0x%0x", rd_data, latched_data); + assert(event_count == 1) else $error("Observed excess rd_swacc events: %0d", event_count); + + + // Verify that hwif changes 1 cycle after wr_swacc + fork + begin + ##0; + forever begin + assert(cb.hwif_out.r2.f.value == 20); + if(cb.hwif_out.r2.f.wr_swacc) break; + @cb; + end + @cb; + forever begin + assert(cb.hwif_out.r2.f.value == 21); + assert(cb.hwif_out.r2.f.wr_swacc == 0); + @cb; + end + end + + begin + cpuif.write('h1, 21); + @cb; + end + join_any + disable fork; + +{% endblock %} diff --git a/tests/test_extended_swacc_swmod/testcase.py b/tests/test_extended_swacc_swmod/testcase.py new file mode 100644 index 0000000..835b5ef --- /dev/null +++ b/tests/test_extended_swacc_swmod/testcase.py @@ -0,0 +1,5 @@ +from ..lib.sim_testcase import SimTestCase + +class Test(SimTestCase): + def test_dut(self): + self.run_test() diff --git a/tests/test_external/__init__.py b/tests/test_external/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_external/regblock.rdl b/tests/test_external/regblock.rdl new file mode 100644 index 0000000..dd6fbd4 --- /dev/null +++ b/tests/test_external/regblock.rdl @@ -0,0 +1,56 @@ +addrmap top { + reg my_reg { + field {sw=rw; hw=r;} whatever[32] = 0; + }; + reg my_reg_alt { + field {sw=r; hw=w;} whatever_a[3:2] = 0; + field {sw=w; hw=r;} whatever_b[4:4] = 0; + field {sw=rw; hw=r;} whatever_c[15:8] = 0; + }; + reg my_wide_reg { + regwidth = 64; + accesswidth = 32; + field {sw=rw; hw=r;} whatever = 0; + }; + + external my_reg_alt ext_reg @ 0x00; + my_reg int_reg @ 0x04; + external my_wide_reg wide_ext_reg @ 0x10; + external my_reg ext_reg_array[32] @ 0x100 += 4; + + + external regfile { + my_reg placeholder @ 8*4-4; + } rf @ 0x1000; + + addrmap { + my_reg placeholder @ 8*4-4; + } am @ 0x2000; + + external mem { + memwidth = 32; + mementries = 8; + } mm @ 0x3000; + + reg my_ro_reg { + field {sw=r; hw=w;} whatever[32] = 0; + }; + reg my_wo_reg { + field {sw=w; hw=r;} whatever[32] = 0; + }; + external my_ro_reg ro_reg @ 0x4000; + external my_wo_reg wo_reg @ 0x4004; + + reg my_wide_ro_reg { + regwidth = 64; + accesswidth = 32; + field {sw=r; hw=w;} whatever[32] = 0; + }; + reg my_wide_wo_reg { + regwidth = 64; + accesswidth = 32; + field {sw=w; hw=r;} whatever[32] = 0; + }; + external my_wide_ro_reg wide_ro_reg @ 0x4010; + external my_wide_wo_reg wide_wo_reg @ 0x4018; +}; diff --git a/tests/test_external/tb_template.sv b/tests/test_external/tb_template.sv new file mode 100644 index 0000000..f5a047e --- /dev/null +++ b/tests/test_external/tb_template.sv @@ -0,0 +1,311 @@ +{% extends "lib/tb_base.sv" %} + + + +{%- block dut_support %} + {% sv_line_anchor %} + + external_reg ext_reg_inst ( + .clk(clk), + .rst(rst), + + .req(hwif_out.ext_reg.req), + .req_is_wr(hwif_out.ext_reg.req_is_wr), + .wr_data(hwif_out.ext_reg.wr_data), + .wr_biten(hwif_out.ext_reg.wr_biten), + .rd_ack(hwif_in.ext_reg.rd_ack), + .rd_data(hwif_in.ext_reg.rd_data), + .wr_ack(hwif_in.ext_reg.wr_ack) + ); + + external_reg #( + .SUBWORDS(2) + ) wide_ext_reg_inst ( + .clk(clk), + .rst(rst), + + .req(hwif_out.wide_ext_reg.req), + .req_is_wr(hwif_out.wide_ext_reg.req_is_wr), + .wr_data(hwif_out.wide_ext_reg.wr_data), + .wr_biten(hwif_out.wide_ext_reg.wr_biten), + .rd_ack(hwif_in.wide_ext_reg.rd_ack), + .rd_data(hwif_in.wide_ext_reg.rd_data), + .wr_ack(hwif_in.wide_ext_reg.wr_ack) + ); + + for(genvar i=0; i<32; i++) begin : array + external_reg ext_reg_inst ( + .clk(clk), + .rst(rst), + + .req(hwif_out.ext_reg_array[i].req), + .req_is_wr(hwif_out.ext_reg_array[i].req_is_wr), + .wr_data(hwif_out.ext_reg_array[i].wr_data), + .wr_biten(hwif_out.ext_reg_array[i].wr_biten), + .rd_ack(hwif_in.ext_reg_array[i].rd_ack), + .rd_data(hwif_in.ext_reg_array[i].rd_data), + .wr_ack(hwif_in.ext_reg_array[i].wr_ack) + ); + end + + external_block #( + .ADDR_WIDTH(5) + ) rf_inst ( + .clk(clk), + .rst(rst), + + .req(hwif_out.rf.req), + .req_is_wr(hwif_out.rf.req_is_wr), + .addr(hwif_out.rf.addr), + .wr_data(hwif_out.rf.wr_data), + .wr_biten(hwif_out.rf.wr_biten), + .rd_ack(hwif_in.rf.rd_ack), + .rd_data(hwif_in.rf.rd_data), + .wr_ack(hwif_in.rf.wr_ack) + ); + + external_block #( + .ADDR_WIDTH(5) + ) am_inst ( + .clk(clk), + .rst(rst), + + .req(hwif_out.am.req), + .req_is_wr(hwif_out.am.req_is_wr), + .addr(hwif_out.am.addr), + .wr_data(hwif_out.am.wr_data), + .wr_biten(hwif_out.am.wr_biten), + .rd_ack(hwif_in.am.rd_ack), + .rd_data(hwif_in.am.rd_data), + .wr_ack(hwif_in.am.wr_ack) + ); + + external_block #( + .ADDR_WIDTH(5) + ) mm_inst ( + .clk(clk), + .rst(rst), + + .req(hwif_out.mm.req), + .req_is_wr(hwif_out.mm.req_is_wr), + .addr(hwif_out.mm.addr), + .wr_data(hwif_out.mm.wr_data), + .wr_biten(hwif_out.mm.wr_biten), + .rd_ack(hwif_in.mm.rd_ack), + .rd_data(hwif_in.mm.rd_data), + .wr_ack(hwif_in.mm.wr_ack) + ); + + external_reg wo_reg_inst ( + .clk(clk), + .rst(rst), + + .req(hwif_out.wo_reg.req), + .req_is_wr(hwif_out.wo_reg.req_is_wr), + .wr_data(hwif_out.wo_reg.wr_data), + .wr_biten(hwif_out.wo_reg.wr_biten), + .rd_ack(), + .rd_data(), + .wr_ack(hwif_in.wo_reg.wr_ack) + ); + + external_reg ro_reg_inst ( + .clk(clk), + .rst(rst), + + .req(hwif_out.ro_reg.req), + .req_is_wr(hwif_out.ro_reg.req_is_wr), + .wr_data(32'b0), + .wr_biten(32'b0), + .rd_ack(hwif_in.ro_reg.rd_ack), + .rd_data(hwif_in.ro_reg.rd_data), + .wr_ack() + ); + + external_reg #( + .SUBWORDS(2) + ) wide_wo_reg_inst ( + .clk(clk), + .rst(rst), + + .req(hwif_out.wide_wo_reg.req), + .req_is_wr(hwif_out.wide_wo_reg.req_is_wr), + .wr_data(hwif_out.wide_wo_reg.wr_data), + .wr_biten(hwif_out.wide_wo_reg.wr_biten), + .rd_ack(), + .rd_data(), + .wr_ack(hwif_in.wide_wo_reg.wr_ack) + ); + + external_reg #( + .SUBWORDS(2) + ) wide_ro_reg_inst ( + .clk(clk), + .rst(rst), + + .req(hwif_out.wide_ro_reg.req), + .req_is_wr(hwif_out.wide_ro_reg.req_is_wr), + .wr_data(32'b0), + .wr_biten(32'b0), + .rd_ack(hwif_in.wide_ro_reg.rd_ack), + .rd_data(hwif_in.wide_ro_reg.rd_data), + .wr_ack() + ); +{%- endblock %} + + + +{% block seq %} + logic [31:0] x; + {% sv_line_anchor %} + ##1; + cb.rst <= '0; + ##1; + + //-------------------------------------------------------------------------- + // Simple read/write tests + //-------------------------------------------------------------------------- + + repeat(20) begin + x = $urandom(); + cpuif.write('h00, x); + cpuif.assert_read('h00, x); + assert(ext_reg_inst.value == x); + end + + for(int i=0; i<2; i++) begin + repeat(20) begin + x = $urandom(); + cpuif.write('h10 + i*4, x); + cpuif.assert_read('h10 + i*4, x); + assert(wide_ext_reg_inst.value[i] == x); + end + end + + for(int i=0; i<32; i++) begin + repeat(20) begin + x = $urandom(); + cpuif.write('h100 + i*4, x); + cpuif.assert_read('h100 + i*4, x); + end + end + + for(int i=0; i<8; i++) begin + repeat(20) begin + x = $urandom(); + cpuif.write('h1000 + i*4, x); + cpuif.assert_read('h1000 + i*4, x); + assert(rf_inst.mem[i] == x); + end + end + + for(int i=0; i<8; i++) begin + repeat(20) begin + x = $urandom(); + cpuif.write('h2000 + i*4, x); + cpuif.assert_read('h2000 + i*4, x); + assert(am_inst.mem[i] == x); + end + end + + for(int i=0; i<8; i++) begin + repeat(20) begin + x = $urandom(); + cpuif.write('h3000 + i*4, x); + cpuif.assert_read('h3000 + i*4, x); + assert(mm_inst.mem[i] == x); + end + end + + repeat(20) begin + x = $urandom(); + ro_reg_inst.value <= x; + cpuif.write('h4000, ~x); + cpuif.assert_read('h4000, x); + assert(ro_reg_inst.value == x); + end + + repeat(20) begin + x = $urandom(); + cpuif.write('h4004, x); + cpuif.assert_read('h4004, 0); + assert(wo_reg_inst.value == x); + end + + for(int i=0; i<2; i++) begin + repeat(20) begin + x = $urandom(); + wide_ro_reg_inst.value[i] <= x; + cpuif.write('h4010 + i*4, ~x); + cpuif.assert_read('h4010 + i*4, x); + assert(wide_ro_reg_inst.value[i] == x); + end + end + + for(int i=0; i<2; i++) begin + repeat(20) begin + x = $urandom(); + cpuif.write('h4018 + i*4, x); + cpuif.assert_read('h4018 + i*4, 0); + assert(wide_wo_reg_inst.value[i] == x); + end + end + + //-------------------------------------------------------------------------- + // Pipelined access + //-------------------------------------------------------------------------- + // init array with unique known value + cpuif.write('h4, 'h1234); + for(int i=0; i<32; i++) begin + cpuif.write('h100 + i*4, 'h100 + i); + end + for(int i=0; i<8; i++) begin + cpuif.write('h1000 + i*4, 'h1000 + i); + cpuif.write('h2000 + i*4, 'h2000 + i); + cpuif.write('h3000 + i*4, 'h3000 + i); + end + + // random pipelined read/writes + repeat(256) begin + fork + begin + automatic int i, j; + i = $urandom_range(31, 0); + j = $urandom_range(7, 0); + case($urandom_range(9,0)) + // external reg + 0: cpuif.write('h100 + i*4, 'h100 + i); + 1: cpuif.assert_read('h100 + i*4, 'h100 + i); + // internal reg + 2: cpuif.write('h4, 'h1234); + 3: cpuif.assert_read('h4, 'h1234); + // external regfile + 4: cpuif.write('h1000 + j*4, 'h1000 + j); + 5: cpuif.assert_read('h1000 + j*4, 'h1000 + j); + // external addrmap + 6: cpuif.write('h2000 + j*4, 'h2000 + j); + 7: cpuif.assert_read('h2000 + j*4, 'h2000 + j); + // external mem + 8: cpuif.write('h3000 + j*4, 'h3000 + j); + 9: cpuif.assert_read('h3000 + j*4, 'h3000 + j); + endcase + end + join_none + end + wait fork; + + // Check register struct bit-order + repeat(32) begin + regblock_pkg::top__my_reg_alt__external__fields__in_t fields_in; + regblock_pkg::top__my_reg_alt__external__fields__out_t fields_out; + fields_in = $urandom(); + fields_out = $urandom(); + + assert(fields_in.whatever_a == fields_in[3:2]); + assert(fields_in.whatever_c == fields_in[15:8]); + + assert(fields_out.whatever_b == fields_out[4]); + assert(fields_out.whatever_c == fields_out[15:8]); + end + +{% endblock %} diff --git a/tests/test_external/testcase.py b/tests/test_external/testcase.py new file mode 100644 index 0000000..623ad67 --- /dev/null +++ b/tests/test_external/testcase.py @@ -0,0 +1,29 @@ +from parameterized import parameterized_class + +from ..lib.sim_testcase import SimTestCase +from ..lib.test_params import get_permutations +from ..lib.cpuifs.apb4 import APB4 +from ..lib.cpuifs.axi4lite import AXI4Lite +from ..lib.cpuifs.passthrough import Passthrough + +@parameterized_class(get_permutations({ + "cpuif": [ + APB4(), + AXI4Lite(), + Passthrough(), + ], + "retime_read_fanin": [True, False], + "retime_read_response": [True, False], + "retime_external": [True, False], +})) +class Test(SimTestCase): + extra_tb_files = [ + "../lib/external_reg.sv", + "../lib/external_block.sv", + ] + init_hwif_in = False + clocking_hwif_in = False + timeout_clk_cycles = 30000 + + def test_dut(self): + self.run_test() diff --git a/tests/test_field_types/__init__.py b/tests/test_field_types/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_field_types/regblock.rdl b/tests/test_field_types/regblock.rdl new file mode 100644 index 0000000..0557159 --- /dev/null +++ b/tests/test_field_types/regblock.rdl @@ -0,0 +1,64 @@ +addrmap top { + default regwidth = 8; + + // All the valid combinations from Table 12 + reg { + field { + sw=rw; hw=rw; we; // Storage element + } f[8] = 10; + } r1; + + reg { + field { + sw=rw; hw=r; // Storage element + } f[8] = 20; + } r2; + + reg { + field { + sw=rw; hw=w; wel; // Storage element + } f[8] = 30; + } r3; + + reg { + field { + sw=rw; hw=na; // Storage element + } f[8] = 40; + } r4; + + reg { + field { + sw=r; hw=rw; we; // Storage element + } f[8] = 50; + } r5; + + reg { + field { + sw=r; hw=r; // Wire/Bus - constant value + } f[8] = 60; + } r6; + + reg { + field { + sw=r; hw=w; // Wire/Bus - hardware assigns value + } f[8]; + } r7; + + reg { + field { + sw=r; hw=na; // Wire/Bus - constant value + } f[8] = 80; + } r8; + + reg { + field { + sw=w; hw=rw; we; // Storage element + } f[8] = 90; + } r9; + + reg { + field { + sw=w; hw=r; // Storage element + } f[8] = 100; + } r10; +}; diff --git a/tests/test_field_types/tb_template.sv b/tests/test_field_types/tb_template.sv new file mode 100644 index 0000000..159c115 --- /dev/null +++ b/tests/test_field_types/tb_template.sv @@ -0,0 +1,131 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + cb.hwif_in.r3.f.wel <= 1; + ##1; + cb.rst <= '0; + ##1; + + // r1 - sw=rw; hw=rw; we; // Storage element + cpuif.assert_read('h0, 10); + assert(cb.hwif_out.r1.f.value == 10); + + cpuif.write('h0, 11); + cpuif.assert_read('h0, 11); + assert(cb.hwif_out.r1.f.value == 11); + + cb.hwif_in.r1.f.next <= 9; + cpuif.assert_read('h0, 11); + assert(cb.hwif_out.r1.f.value == 11); + cb.hwif_in.r1.f.next <= 12; + cb.hwif_in.r1.f.we <= 1; + @cb; + cb.hwif_in.r1.f.next <= 0; + cb.hwif_in.r1.f.we <= 0; + cpuif.assert_read('h0, 12); + assert(cb.hwif_out.r1.f.value == 12); + + + // r2 - sw=rw; hw=r; // Storage element + cpuif.assert_read('h1, 20); + assert(cb.hwif_out.r2.f.value == 20); + + cpuif.write('h1, 21); + cpuif.assert_read('h1, 21); + assert(cb.hwif_out.r2.f.value == 21); + + + // r3 - sw=rw; hw=w; wel; // Storage element + cpuif.assert_read('h2, 30); + + cpuif.write('h2, 31); + cpuif.assert_read('h2, 31); + + cb.hwif_in.r3.f.next <= 29; + cpuif.assert_read('h2, 31); + cb.hwif_in.r3.f.next <= 32; + cb.hwif_in.r3.f.wel <= 0; + @cb; + cb.hwif_in.r3.f.next <= 0; + cb.hwif_in.r3.f.wel <= 1; + cpuif.assert_read('h2, 32); + + + // r4 - sw=rw; hw=na; // Storage element + cpuif.assert_read('h3, 40); + cpuif.write('h3, 41); + cpuif.assert_read('h3, 41); + + + // r5 - sw=r; hw=rw; we; // Storage element + cpuif.assert_read('h4, 50); + assert(cb.hwif_out.r5.f.value == 50); + + cpuif.write('h4, 51); + cpuif.assert_read('h4, 50); + assert(cb.hwif_out.r5.f.value == 50); + + cb.hwif_in.r5.f.next <= 9; + cpuif.assert_read('h4, 50); + assert(cb.hwif_out.r5.f.value == 50); + cb.hwif_in.r5.f.next <= 52; + cb.hwif_in.r5.f.we <= 1; + @cb; + cb.hwif_in.r5.f.next <= 0; + cb.hwif_in.r5.f.we <= 0; + cpuif.assert_read('h4, 52); + assert(cb.hwif_out.r5.f.value == 52); + + + // r6 - sw=r; hw=r; // Wire/Bus - constant value + cpuif.assert_read('h5, 60); + assert(cb.hwif_out.r6.f.value == 60); + cpuif.write('h5, 61); + cpuif.assert_read('h5, 60); + assert(cb.hwif_out.r6.f.value == 60); + + + // r7 - sw=r; hw=w; // Wire/Bus - hardware assigns value + cpuif.assert_read('h6, 0); + cb.hwif_in.r7.f.next <= 70; + cpuif.assert_read('h6, 70); + cpuif.write('h6, 71); + cpuif.assert_read('h6, 70); + + + // r8 - sw=r; hw=na; // Wire/Bus - constant value + cpuif.assert_read('h7, 80); + cpuif.write('h7, 81); + cpuif.assert_read('h7, 80); + + + // r9 - sw=w; hw=rw; we; // Storage element + cpuif.assert_read('h8, 0); + assert(cb.hwif_out.r9.f.value == 90); + + cpuif.write('h8, 91); + cpuif.assert_read('h8, 0); + assert(cb.hwif_out.r9.f.value == 91); + + cb.hwif_in.r9.f.next <= 89; + cpuif.assert_read('h8, 0); + assert(cb.hwif_out.r9.f.value == 91); + cb.hwif_in.r9.f.next <= 92; + cb.hwif_in.r9.f.we <= 1; + @cb; + cb.hwif_in.r9.f.next <= 0; + cb.hwif_in.r9.f.we <= 0; + cpuif.assert_read('h8, 0); + assert(cb.hwif_out.r9.f.value == 92); + + + // r10 - sw=w; hw=r; // Storage element + cpuif.assert_read('h9, 0); + assert(cb.hwif_out.r10.f.value == 100); + + cpuif.write('h9, 101); + cpuif.assert_read('h9, 0); + assert(cb.hwif_out.r10.f.value == 101); + +{% endblock %} diff --git a/tests/test_field_types/testcase.py b/tests/test_field_types/testcase.py new file mode 100644 index 0000000..835b5ef --- /dev/null +++ b/tests/test_field_types/testcase.py @@ -0,0 +1,5 @@ +from ..lib.sim_testcase import SimTestCase + +class Test(SimTestCase): + def test_dut(self): + self.run_test() diff --git a/tests/test_fixedpoint/__init__.py b/tests/test_fixedpoint/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_fixedpoint/regblock.rdl b/tests/test_fixedpoint/regblock.rdl new file mode 100644 index 0000000..42f0f48 --- /dev/null +++ b/tests/test_fixedpoint/regblock.rdl @@ -0,0 +1,39 @@ +addrmap top { + default accesswidth = 64; + default regwidth = 64; + reg { + field { + sw = rw; hw = r; + intwidth = 8; + fracwidth = 8; + } f_Q8_8[16] = 0; + field { + sw = r; hw = w; + intwidth = 32; + } f_Q32_n12[20]; + field { + sw = rw; hw = r; + fracwidth = 32; + is_signed; + } f_SQn8_32[24] = 0; + field { + sw = rw; hw = r; + fracwidth = 7; + is_signed; + } f_SQn6_7 = 0; + } r1 @ 0x0; + + reg { + field { + sw = r; hw = w; + is_signed; + } f_signed[16]; + field { + sw = rw; hw = r; + is_signed = false; + } f_unsigned[16] = 0; + field { + sw = r; hw = w; + } f_no_sign[16]; + } r2 @ 0x8; +}; diff --git a/tests/test_fixedpoint/tb_template.sv b/tests/test_fixedpoint/tb_template.sv new file mode 100644 index 0000000..3386efb --- /dev/null +++ b/tests/test_fixedpoint/tb_template.sv @@ -0,0 +1,77 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + ##1; + cb.rst <= '0; + ##1; + + // set all fields to all 1s + cb.hwif_in.r1.f_Q32_n12.next <= '1; + cb.hwif_in.r2.f_signed.next <= '1; + cb.hwif_in.r2.f_no_sign.next <= '1; + cpuif.write('h0, 64'hFFFF_FFFF_FFFF_FFFF); + cpuif.write('h8, 64'hFFFF_FFFF_FFFF_FFFF); + @cb; + + // Q8.8 + // verify bit range + assert(cb.hwif_out.r1.f_Q8_8.value[7:-8] == '1); + // verify bit width + assert($size(hwif_out.r1.f_Q8_8.value) == 16); + // verify unsigned + assert(cb.hwif_out.r1.f_Q8_8.value > 0); + + // Q32.-12 + // verify bit range + assert(hwif_in.r1.f_Q32_n12.next[31:12] == '1); + // verify bit width + assert($size(hwif_in.r1.f_Q32_n12.next) == 20); + // verify unsigned + assert(hwif_in.r1.f_Q32_n12.next > 0); + + // SQ-8.32 + // verify bit range + assert(cb.hwif_out.r1.f_SQn8_32.value[-9:-32] == '1); + // verify bit width + assert($size(hwif_out.r1.f_SQn8_32.value) == 24); + // verify signed + assert(cb.hwif_out.r1.f_SQn8_32.value < 0); + + // SQ-6.7 + // verify bit range + assert(cb.hwif_out.r1.f_SQn6_7.value[-7:-7] == '1); + // verify bit width + assert($size(hwif_out.r1.f_SQn6_7.value) == 1); + // verify signed + assert(cb.hwif_out.r1.f_SQn6_7.value < 0); + + // 16-bit signed integer + // verify bit range + assert(hwif_in.r2.f_signed.next[15:0] == '1); + // verify bit width + assert($size(hwif_in.r2.f_signed.next) == 16); + // verify signed + assert(hwif_in.r2.f_signed.next < 0); + + // 16-bit unsigned integer + // verify bit range + assert(cb.hwif_out.r2.f_unsigned.value[15:0] == '1); + // verify bit width + assert($size(hwif_out.r2.f_unsigned.value) == 16); + // verify unsigned + assert(cb.hwif_out.r2.f_unsigned.value > 0); + + // 16-bit field (no sign) + // verify bit range + assert(hwif_in.r2.f_no_sign.next[15:0] == '1); + // verify bit width + assert($size(hwif_in.r2.f_no_sign.next) == 16); + // verify unsigned (logic is unsigned in SV) + assert(hwif_in.r2.f_no_sign.next > 0); + + // verify readback + cpuif.assert_read('h0, 64'h1FFF_FFFF_FFFF_FFFF); + cpuif.assert_read('h8, 64'h0000_FFFF_FFFF_FFFF); + +{% endblock %} diff --git a/tests/test_fixedpoint/testcase.py b/tests/test_fixedpoint/testcase.py new file mode 100644 index 0000000..835b5ef --- /dev/null +++ b/tests/test_fixedpoint/testcase.py @@ -0,0 +1,5 @@ +from ..lib.sim_testcase import SimTestCase + +class Test(SimTestCase): + def test_dut(self): + self.run_test() diff --git a/tests/test_hw_access/__init__.py b/tests/test_hw_access/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_hw_access/regblock.rdl b/tests/test_hw_access/regblock.rdl new file mode 100644 index 0000000..8784840 --- /dev/null +++ b/tests/test_hw_access/regblock.rdl @@ -0,0 +1,73 @@ +addrmap top { + reg { + field { + sw=rw; hw=na; + } hw_enable[7:0] = 0xFF; + + field { + sw=rw; hw=na; + } hw_mask[15:8] = 0x00; + + field { + sw=rw; hw=na; + } hw_clr[16:16] = 0; + + field { + sw=rw; hw=na; + } hw_set[17:17] = 0; + + field { + sw=rw; hw=na; + } hw_we[18:18] = 0; + + field { + sw=rw; hw=na; + } hw_wel[20:20] = 1; + } hw_ctrl; + + + reg { + field { + sw=r; hw=w; + we; hwclr; hwset; + } f[7:0] = 0x11; + } r1; + r1.f->hwenable = hw_ctrl.hw_enable; + + + reg { + field { + sw=r; hw=w; + we; hwclr; hwset; + } f[7:0] = 0x22; + } r2; + r2.f->hwmask = hw_ctrl.hw_mask; + + + reg { + field { + sw=rw; hw=w; + } f[7:0] = 0x33; + } r3; + r3.f->hwenable = hw_ctrl.hw_enable; + r3.f->hwclr = hw_ctrl.hw_clr; + r3.f->hwset = hw_ctrl.hw_set; + r3.f->we = hw_ctrl.hw_we; + + reg { + field { + sw=rw; hw=w; + } f[7:0] = 0x44; + } r4; + r4.f->wel = hw_ctrl.hw_wel; + + reg { + signal {} f_next_value[8]; + signal {} f_we; + field { + sw=rw; hw=w; + next = f_next_value; + we = f_we; + } f[7:0] = 0x55; + } r5; +}; diff --git a/tests/test_hw_access/tb_template.sv b/tests/test_hw_access/tb_template.sv new file mode 100644 index 0000000..d2cca1d --- /dev/null +++ b/tests/test_hw_access/tb_template.sv @@ -0,0 +1,105 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + ##1; + cb.rst <= '0; + ##1; + + // check initial conditions + cpuif.assert_read('h4, 'h11); + cpuif.assert_read('h8, 'h22); + cpuif.assert_read('hC, 'h33); + + //--------------------------------- + // set hwenable = F0 + cpuif.write('h0, 'h00_F0); + + // test hwenable + we + cb.hwif_in.r1.f.next <= 'hAB; + cb.hwif_in.r1.f.we <= '1; + @cb; + cb.hwif_in.r1.f.we <= '0; + cpuif.assert_read('h4, 'hA1); + + // test hwenable + hwclr + cb.hwif_in.r1.f.hwclr <= '1; + @cb; + cb.hwif_in.r1.f.hwclr <= '0; + cpuif.assert_read('h4, 'h01); + + // test hwenable + hwset + cb.hwif_in.r1.f.hwset <= '1; + @cb; + cb.hwif_in.r1.f.hwset <= '0; + cpuif.assert_read('h4, 'hF1); + + + //--------------------------------- + // set hwmask = F0 + cpuif.write('h0, 'hF0_00); + + // test hwmask + we + cb.hwif_in.r2.f.next <= 'hAB; + cb.hwif_in.r2.f.we <= '1; + @cb; + cb.hwif_in.r2.f.we <= '0; + cpuif.assert_read('h8, 'h2B); + + // test hwmask + hwclr + cb.hwif_in.r2.f.hwclr <= '1; + @cb; + cb.hwif_in.r2.f.hwclr <= '0; + cpuif.assert_read('h8, 'h20); + + // test hwmask + hwset + cb.hwif_in.r2.f.hwset <= '1; + @cb; + cb.hwif_in.r2.f.hwset <= '0; + cpuif.assert_read('h8, 'h2F); + + //--------------------------------- + // test hwenable + hwclr via reference + // toggle hwenable = F0, hwclr=1 + cpuif.write('h0, 'h1_00_F0); + cpuif.write('h0, 'h0_00_00); + cpuif.assert_read('hC, 'h03); + + // test hwenable + hwset via reference + // toggle hwenable = 0F, hwset=1 + cpuif.write('h0, 'h2_00_0F); + cpuif.write('h0, 'h0_00_00); + cpuif.assert_read('hC, 'h0F); + + // test hwenable + we via reference + cb.hwif_in.r3.f.next <= 'hAA; + // toggle hwenable = 0F, we=1 + cpuif.write('h0, 'h4_00_0F); + cpuif.write('h0, 'h0_00_00); + cpuif.assert_read('hC, 'h0A); + + //--------------------------------- + // test wel via reference + cb.hwif_in.r4.f.next <= 'hBB; + // toggle wel + cpuif.write('h0, 'h10_00_00); + cpuif.write('h0, 'h00_00_00); + cpuif.assert_read('h10, 'hBB); + + cb.hwif_in.r4.f.next <= 'hCC; + // toggle wel + cpuif.write('h0, 'h10_00_00); + cpuif.write('h0, 'h00_00_00); + cpuif.assert_read('h10, 'hCC); + + //--------------------------------- + // test we and next via reference + cb.hwif_in.r5.f_next_value <= 'h54; + cpuif.assert_read('h14, 'h55); + cb.hwif_in.r5.f_next_value <= 'h56; + cb.hwif_in.r5.f_we <= '1; + @cb; + cb.hwif_in.r5.f_next_value <= '0; + cb.hwif_in.r5.f_we <= '0; + cpuif.assert_read('h14, 'h56); +{% endblock %} diff --git a/tests/test_hw_access/testcase.py b/tests/test_hw_access/testcase.py new file mode 100644 index 0000000..835b5ef --- /dev/null +++ b/tests/test_hw_access/testcase.py @@ -0,0 +1,5 @@ +from ..lib.sim_testcase import SimTestCase + +class Test(SimTestCase): + def test_dut(self): + self.run_test() diff --git a/tests/test_interrupts/__init__.py b/tests/test_interrupts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_interrupts/regblock.rdl b/tests/test_interrupts/regblock.rdl new file mode 100644 index 0000000..687ba88 --- /dev/null +++ b/tests/test_interrupts/regblock.rdl @@ -0,0 +1,229 @@ +addrmap top { + //--------------------------------- + reg { + field ctrl_t { + sw=rw; hw=na; + }; + ctrl_t irq0[8] = 0; + ctrl_t irq1[1] = 0; + } + ctrl_enable @ 0x100, + ctrl_mask @ 0x104, + ctrl_haltenable @ 0x108, + ctrl_haltmask @ 0x10c; + reg { + field ctrl_t { + sw=rw; hw=na; + }; + ctrl_t irq0[1] = 0; + ctrl_t irq1[1] = 0; + } + ctrl_we @ 0x110, + ctrl_wel @ 0x114; + //--------------------------------- + + reg { + field intr_t { + sw=rw; hw=w; + level intr; + woclr; + }; + + intr_t irq0[8] = 0; + intr_t irq1[1] = 0; + } + level_irqs_1 @ 0x0, + level_irqs_2 @ 0x4, + level_irqs_3 @ 0x8; + + level_irqs_2.irq0->enable = ctrl_enable.irq0; + level_irqs_2.irq1->enable = ctrl_enable.irq1; + level_irqs_2.irq0->haltenable = ctrl_haltenable.irq0; + level_irqs_2.irq1->haltenable = ctrl_haltenable.irq1; + level_irqs_3.irq0->mask = ctrl_mask.irq0; + level_irqs_3.irq1->mask = ctrl_mask.irq1; + level_irqs_3.irq0->haltmask = ctrl_haltmask.irq0; + level_irqs_3.irq1->haltmask = ctrl_haltmask.irq1; + + reg { + field intr_t { + sw=rw; hw=w; + level intr; + woclr; + we; + }; + + intr_t irq0[8] = 0; + intr_t irq1[1] = 0; + } level_irqs_we @ 0x10; + + reg { + field intr_t { + sw=rw; hw=w; + level intr; + woclr; + wel; + }; + + intr_t irq0[8] = 0; + intr_t irq1[1] = 0; + } level_irqs_wel @ 0x14; + + level_irqs_we.irq0->we = ctrl_we.irq0; + level_irqs_we.irq1->we = ctrl_we.irq1; + level_irqs_wel.irq0->wel = ctrl_wel.irq0; + level_irqs_wel.irq1->wel = ctrl_wel.irq1; + + //--------------------------------- + + reg { + field intr_t { + sw=rw; hw=w; + posedge intr; + woclr; + }; + + intr_t irq0[8] = 0; + intr_t irq1[1] = 0; + } posedge_irqs @ 0x20; + + reg { + field intr_t { + sw=rw; hw=w; + posedge intr; + woclr; + }; + + intr_t irq0[8] = 0; + intr_t irq1[1] = 0; + } posedge_we_irqs @ 0x24; + + posedge_we_irqs.irq0->we = ctrl_we.irq0; + posedge_we_irqs.irq1->we = ctrl_we.irq1; + + reg { + field intr_t { + sw=rw; hw=w; + posedge intr; + woclr; + }; + + intr_t irq0[8] = 0; + intr_t irq1[1] = 0; + } posedge_wel_irqs @ 0x28; + + posedge_wel_irqs.irq0->wel = ctrl_wel.irq0; + posedge_wel_irqs.irq1->wel = ctrl_wel.irq1; + + //--------------------------------- + + reg { + field intr_t { + sw=rw; hw=w; + negedge intr; + woclr; + }; + + intr_t irq0[8] = 0; + intr_t irq1[1] = 0; + } negedge_irqs @ 0x30; + + reg { + field intr_t { + sw=rw; hw=w; + negedge intr; + woclr; + }; + + intr_t irq0[8] = 0; + intr_t irq1[1] = 0; + } negedge_we_irqs @ 0x34; + + negedge_we_irqs.irq0->we = ctrl_we.irq0; + negedge_we_irqs.irq1->we = ctrl_we.irq1; + + reg { + field intr_t { + sw=rw; hw=w; + negedge intr; + woclr; + }; + + intr_t irq0[8] = 0; + intr_t irq1[1] = 0; + } negedge_wel_irqs @ 0x38; + + negedge_wel_irqs.irq0->wel = ctrl_wel.irq0; + negedge_wel_irqs.irq1->wel = ctrl_wel.irq1; + + //--------------------------------- + + reg { + field intr_t { + sw=rw; hw=w; + bothedge intr; + woclr; + }; + + intr_t irq0[8] = 0; + intr_t irq1[1] = 0; + } bothedge_irqs @ 0x40; + + reg { + field intr_t { + sw=rw; hw=w; + bothedge intr; + woclr; + }; + + intr_t irq0[8] = 0; + intr_t irq1[1] = 0; + } bothedge_we_irqs @ 0x44; + + bothedge_we_irqs.irq0->we = ctrl_we.irq0; + bothedge_we_irqs.irq1->we = ctrl_we.irq1; + + reg { + field intr_t { + sw=rw; hw=w; + bothedge intr; + woclr; + }; + + intr_t irq0[8] = 0; + intr_t irq1[1] = 0; + } bothedge_wel_irqs @ 0x48; + + bothedge_wel_irqs.irq0->wel = ctrl_wel.irq0; + bothedge_wel_irqs.irq1->wel = ctrl_wel.irq1; + + //--------------------------------- + + reg { + field intr_t { + sw=r; hw=w; + nonsticky intr; + }; + + intr_t level_active[1]; + intr_t posedge_active[1]; + intr_t negedge_active[1]; + intr_t bothedge_active[1]; + intr_t level_halt_active[1]; + } top_irq @ 0x50; + + top_irq.level_active->next = level_irqs_1->intr; + top_irq.posedge_active->next = posedge_irqs->intr; + top_irq.negedge_active->next = negedge_irqs->intr; + top_irq.bothedge_active->next = bothedge_irqs->intr; + top_irq.level_halt_active->next = level_irqs_2->halt; + + //--------------------------------- + reg { + field { + sw=rw; hw=w; + sticky; + } stickyfield[8] = 0; + } stickyreg @ 0x60; + +}; diff --git a/tests/test_interrupts/tb_template.sv b/tests/test_interrupts/tb_template.sv new file mode 100644 index 0000000..87112ba --- /dev/null +++ b/tests/test_interrupts/tb_template.sv @@ -0,0 +1,288 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + ##1; + cb.rst <= '0; + ##1; + + // Enable all interrupts + cpuif.write('h100, 'h1FF); // ctrl_enable + cpuif.write('h104, 'h000); // ctrl_mask + cpuif.write('h108, 'h1FF); // ctrl_haltenable + cpuif.write('h10C, 'h000); // ctrl_haltmask + cpuif.write('h110, 'h0); // ctrl_we + cpuif.write('h114, 'h3); // ctrl_wel + + //-------------------------------------------------------------------------- + // Test level_irqs_1 + cpuif.assert_read('h0, 'h000); + assert(cb.hwif_out.level_irqs_1.intr == 1'b0); + cb.hwif_in.level_irqs_1.irq0.next <= 'h0F; + @cb; + cb.hwif_in.level_irqs_1.irq0.next <= 'h00; + cpuif.assert_read('h0, 'h00F); + assert(cb.hwif_out.level_irqs_1.intr == 1'b1); + cpuif.write('h0, 'h3); + cpuif.assert_read('h0, 'h00C); + assert(cb.hwif_out.level_irqs_1.intr == 1'b1); + cpuif.write('h0, 'hC); + cpuif.assert_read('h0, 'h000); + assert(cb.hwif_out.level_irqs_1.intr == 1'b0); + + cb.hwif_in.level_irqs_1.irq1.next <= 'b1; + @cb; + cb.hwif_in.level_irqs_1.irq1.next <= 'b0; + cpuif.assert_read('h0, 'h100); + assert(cb.hwif_out.level_irqs_1.intr == 1'b1); + cpuif.write('h0, 'h100); + @cb; + assert(cb.hwif_out.level_irqs_1.intr == 1'b0); + cpuif.assert_read('h0, 'h0); + + cb.hwif_in.level_irqs_1.irq1.next <= 'b1; + cpuif.assert_read('h0, 'h100); + assert(cb.hwif_out.level_irqs_1.intr == 1'b1); + cpuif.write('h0, 'h100); + cpuif.assert_read('h0, 'h100); + assert(cb.hwif_out.level_irqs_1.intr == 1'b1); + cb.hwif_in.level_irqs_1.irq1.next <= 'b0; + cpuif.assert_read('h0, 'h100); + assert(cb.hwif_out.level_irqs_1.intr == 1'b1); + cpuif.write('h0, 'h100); + cpuif.assert_read('h0, 'h000); + assert(cb.hwif_out.level_irqs_1.intr == 1'b0); + + //-------------------------------------------------------------------------- + // Test level_irqs_2 + cpuif.assert_read('h4, 'h000); + assert(cb.hwif_out.level_irqs_2.intr == 1'b0); + assert(cb.hwif_out.level_irqs_2.halt == 1'b0); + cb.hwif_in.level_irqs_2.irq0.next <= 'h0F; + @cb; + cb.hwif_in.level_irqs_2.irq0.next <= 'h00; + cpuif.assert_read('h4, 'h00F); + assert(cb.hwif_out.level_irqs_2.intr == 1'b1); + assert(cb.hwif_out.level_irqs_2.halt == 1'b1); + cpuif.write('h100, 'h0); // ctrl_enable + @cb; + assert(cb.hwif_out.level_irqs_2.intr == 1'b0); + assert(cb.hwif_out.level_irqs_2.halt == 1'b1); + cpuif.write('h108, 'h0); // ctrl_haltenable + @cb; + assert(cb.hwif_out.level_irqs_2.intr == 1'b0); + assert(cb.hwif_out.level_irqs_2.halt == 1'b0); + cpuif.write('h100, 'h1FF); // ctrl_enable + cpuif.write('h108, 'h1FF); // ctrl_haltenable + @cb; + assert(cb.hwif_out.level_irqs_2.intr == 1'b1); + assert(cb.hwif_out.level_irqs_2.halt == 1'b1); + cpuif.write('h4, 'h1FF); + @cb; + assert(cb.hwif_out.level_irqs_2.intr == 1'b0); + assert(cb.hwif_out.level_irqs_2.halt == 1'b0); + + //-------------------------------------------------------------------------- + // Test level_irqs_3 + cpuif.assert_read('h8, 'h000); + assert(cb.hwif_out.level_irqs_3.intr == 1'b0); + assert(cb.hwif_out.level_irqs_3.halt == 1'b0); + cb.hwif_in.level_irqs_3.irq0.next <= 'h0F; + @cb; + cb.hwif_in.level_irqs_3.irq0.next <= 'h00; + cpuif.assert_read('h8, 'h00F); + assert(cb.hwif_out.level_irqs_3.intr == 1'b1); + assert(cb.hwif_out.level_irqs_3.halt == 1'b1); + cpuif.write('h104, 'h0F); // ctrl_mask + @cb; + assert(cb.hwif_out.level_irqs_3.intr == 1'b0); + assert(cb.hwif_out.level_irqs_3.halt == 1'b1); + cpuif.write('h10C, 'hF); // ctrl_haltmask + @cb; + assert(cb.hwif_out.level_irqs_3.intr == 1'b0); + assert(cb.hwif_out.level_irqs_3.halt == 1'b0); + cpuif.write('h104, 'h0); // ctrl_mask + cpuif.write('h10C, 'h0); // ctrl_haltmask + @cb; + assert(cb.hwif_out.level_irqs_3.intr == 1'b1); + assert(cb.hwif_out.level_irqs_3.halt == 1'b1); + + //-------------------------------------------------------------------------- + // Test level_irqs with we + cpuif.assert_read('h10, 'h000); + assert(cb.hwif_out.level_irqs_we.intr == 1'b0); + cb.hwif_in.level_irqs_we.irq0.next <= 'h0F; + @cb; + cb.hwif_in.level_irqs_we.irq0.next <= 'h00; + assert(cb.hwif_out.level_irqs_we.intr == 1'b0); + cpuif.assert_read('h10, 'h000); + cpuif.write('h110, 'h1); // enable ctrl_we + @cb; + cpuif.assert_read('h110, 'h1); + assert(cb.hwif_out.level_irqs_we.intr == 1'b0); + cb.hwif_in.level_irqs_we.irq0.next <= 'h0F; + @cb; + cpuif.assert_read('h10, 'h00F); + assert(cb.hwif_out.level_irqs_we.intr == 1'b1); + cpuif.write('h110, 'h0); // disable ctrl_we + cpuif.write('h10, 'h1FF); + @cb; + assert(cb.hwif_out.level_irqs_we.intr == 1'b0); + cpuif.assert_read('h10, 'h000); + cb.hwif_in.level_irqs_we.irq0.next <= 'h00; + + //-------------------------------------------------------------------------- + // Test level_irqs with wel + cpuif.assert_read('h14, 'h000); + assert(cb.hwif_out.level_irqs_wel.intr == 1'b0); + cb.hwif_in.level_irqs_wel.irq0.next <= 'h0F; + @cb; + cb.hwif_in.level_irqs_wel.irq0.next <= 'h00; + cpuif.assert_read('h14, 'h000); + assert(cb.hwif_out.level_irqs_wel.intr == 1'b0); + cpuif.write('h114, 'h2); // enable ctrl_we + @cb; + cpuif.assert_read('h14, 'h000); + assert(cb.hwif_out.level_irqs_wel.intr == 1'b0); + cb.hwif_in.level_irqs_wel.irq0.next <= 'h0F; + @cb; + cpuif.assert_read('h14, 'h00F); + assert(cb.hwif_out.level_irqs_wel.intr == 1'b1); + cpuif.write('h114, 'h3); // disable ctrl_we + cpuif.write('h14, 'h1FF); + @cb; + assert(cb.hwif_out.level_irqs_wel.intr == 1'b0); + cpuif.assert_read('h14, 'h000); + cb.hwif_in.level_irqs_wel.irq0.next <= 'h00; + + //-------------------------------------------------------------------------- + // Test posedge_irqs + cpuif.assert_read('h20, 'h000); + assert(cb.hwif_out.posedge_irqs.intr == 1'b0); + cb.hwif_in.posedge_irqs.irq1.next <= 1'b1; + @cb; + cpuif.assert_read('h20, 'h100); + assert(cb.hwif_out.posedge_irqs.intr == 1'b1); + cpuif.write('h20, 'h100); + cpuif.assert_read('h20, 'h000); + assert(cb.hwif_out.posedge_irqs.intr == 1'b0); + cpuif.assert_read('h20, 'h000); + + cb.hwif_in.posedge_irqs.irq1.next <= 1'b0; + cpuif.assert_read('h20, 'h000); + assert(cb.hwif_out.posedge_irqs.intr == 1'b0); + + //-------------------------------------------------------------------------- + // Test negedge_irqs + cpuif.assert_read('h30, 'h000); + assert(cb.hwif_out.negedge_irqs.intr == 1'b0); + cb.hwif_in.negedge_irqs.irq1.next <= 1'b1; + @cb; + cpuif.assert_read('h30, 'h000); + assert(cb.hwif_out.negedge_irqs.intr == 1'b0); + cb.hwif_in.negedge_irqs.irq1.next <= 1'b0; + cpuif.assert_read('h30, 'h100); + assert(cb.hwif_out.negedge_irqs.intr == 1'b1); + cpuif.write('h30, 'h100); + cpuif.assert_read('h30, 'h000); + assert(cb.hwif_out.negedge_irqs.intr == 1'b0); + cpuif.assert_read('h30, 'h000); + + //-------------------------------------------------------------------------- + // Test bothedge_irqs + cpuif.assert_read('h40, 'h000); + assert(cb.hwif_out.bothedge_irqs.intr == 1'b0); + + cb.hwif_in.bothedge_irqs.irq1.next <= 1'b1; + cpuif.assert_read('h40, 'h100); + assert(cb.hwif_out.bothedge_irqs.intr == 1'b1); + cpuif.write('h40, 'h100); + cpuif.assert_read('h40, 'h000); + assert(cb.hwif_out.bothedge_irqs.intr == 1'b0); + cpuif.assert_read('h40, 'h000); + + cb.hwif_in.bothedge_irqs.irq1.next <= 1'b0; + cpuif.assert_read('h40, 'h100); + assert(cb.hwif_out.bothedge_irqs.intr == 1'b1); + cpuif.write('h40, 'h100); + cpuif.assert_read('h40, 'h000); + assert(cb.hwif_out.bothedge_irqs.intr == 1'b0); + cpuif.assert_read('h40, 'h000); + + + //-------------------------------------------------------------------------- + // Test top_irq + cpuif.assert_read('h50, 'h000); + assert(cb.hwif_out.top_irq.intr == 1'b0); + + cb.hwif_in.level_irqs_1.irq0.next <= 'h01; + @cb; + cb.hwif_in.level_irqs_1.irq0.next <= 'h00; + cpuif.assert_read('h50, 'b0001); + assert(cb.hwif_out.top_irq.intr == 1'b1); + cpuif.write('h0, 'h01); + cpuif.assert_read('h50, 'b0000); + assert(cb.hwif_out.top_irq.intr == 1'b0); + + cb.hwif_in.posedge_irqs.irq0.next <= 'h01; + @cb; + cb.hwif_in.posedge_irqs.irq0.next <= 'h00; + cpuif.assert_read('h50, 'b0010); + assert(cb.hwif_out.top_irq.intr == 1'b1); + cpuif.write('h20, 'h01); + cpuif.assert_read('h50, 'b0000); + assert(cb.hwif_out.top_irq.intr == 1'b0); + + cb.hwif_in.negedge_irqs.irq0.next <= 'h01; + @cb; + cb.hwif_in.negedge_irqs.irq0.next <= 'h00; + @cb; + cpuif.assert_read('h50, 'b0100); + assert(cb.hwif_out.top_irq.intr == 1'b1); + cpuif.write('h30, 'h01); + cpuif.assert_read('h50, 'b0000); + assert(cb.hwif_out.top_irq.intr == 1'b0); + + cb.hwif_in.bothedge_irqs.irq0.next <= 'h01; + @cb; + cb.hwif_in.bothedge_irqs.irq0.next <= 'h00; + cpuif.assert_read('h50, 'b1000); + assert(cb.hwif_out.top_irq.intr == 1'b1); + cpuif.write('h40, 'h01); + cpuif.assert_read('h50, 'b0000); + assert(cb.hwif_out.top_irq.intr == 1'b0); + + cpuif.write('h108, 'h000); // ctrl_haltenable + cb.hwif_in.level_irqs_2.irq0.next <= 'h01; + @cb; + cb.hwif_in.level_irqs_2.irq0.next <= 'h00; + @cb; + cpuif.assert_read('h50, 'b00000); + assert(cb.hwif_out.top_irq.intr == 1'b0); + + cpuif.write('h108, 'h001); // ctrl_haltenable + cpuif.assert_read('h50, 'b10000); + assert(cb.hwif_out.top_irq.intr == 1'b1); + + cpuif.write('h4, 'h01); + cpuif.assert_read('h50, 'b00000); + assert(cb.hwif_out.top_irq.intr == 1'b0); + + //-------------------------------------------------------------------------- + // Test multi-bit sticky reg + cpuif.assert_read('h60, 'h00); + cb.hwif_in.stickyreg.stickyfield.next <= 'h12; + @cb; + cb.hwif_in.stickyreg.stickyfield.next <= 'h34; + @cb; + cb.hwif_in.stickyreg.stickyfield.next <= 'h56; + @cb; + cpuif.assert_read('h60, 'h12); + cpuif.write('h60, 'h00); + @cb; + cb.hwif_in.stickyreg.stickyfield.next <= 'h78; + @cb; + cpuif.assert_read('h60, 'h56); + + +{% endblock %} diff --git a/tests/test_interrupts/testcase.py b/tests/test_interrupts/testcase.py new file mode 100644 index 0000000..835b5ef --- /dev/null +++ b/tests/test_interrupts/testcase.py @@ -0,0 +1,5 @@ +from ..lib.sim_testcase import SimTestCase + +class Test(SimTestCase): + def test_dut(self): + self.run_test() diff --git a/tests/test_map_size/__init__.py b/tests/test_map_size/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_map_size/regblock.rdl b/tests/test_map_size/regblock.rdl new file mode 100644 index 0000000..046949a --- /dev/null +++ b/tests/test_map_size/regblock.rdl @@ -0,0 +1,5 @@ +addrmap top { + reg { + field {} f1[32] = 0; + } my_reg; +}; diff --git a/tests/test_map_size/tb_template.sv b/tests/test_map_size/tb_template.sv new file mode 100644 index 0000000..d86780a --- /dev/null +++ b/tests/test_map_size/tb_template.sv @@ -0,0 +1,12 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + ##1; + cb.rst <= '0; + ##1; + + // check block size + assert(regblock_pkg::REGBLOCK_SIZE == {{exporter.ds.top_node.size}}); + +{% endblock %} diff --git a/tests/test_map_size/testcase.py b/tests/test_map_size/testcase.py new file mode 100644 index 0000000..835b5ef --- /dev/null +++ b/tests/test_map_size/testcase.py @@ -0,0 +1,5 @@ +from ..lib.sim_testcase import SimTestCase + +class Test(SimTestCase): + def test_dut(self): + self.run_test() diff --git a/tests/test_onread_onwrite/__init__.py b/tests/test_onread_onwrite/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_onread_onwrite/regblock.rdl b/tests/test_onread_onwrite/regblock.rdl new file mode 100644 index 0000000..de196e5 --- /dev/null +++ b/tests/test_onread_onwrite/regblock.rdl @@ -0,0 +1,61 @@ +addrmap top { + + reg { + field { + sw=rw; hw=na; + onread = rclr; + } f1[7:0] = 0xF0; + + field { + sw=rw; hw=na; + onread = rset; + } f2[15:8] = 0x0F; + } r1; + + + reg { + field { + sw=rw; hw=na; + onwrite = woset; + } f1[3:0] = 0x0; + + field { + sw=rw; hw=na; + onwrite = woclr; + } f2[7:4] = 0xF; + + field { + sw=rw; hw=na; + onwrite = wot; + } f3[11:8] = 0x0; + } r2; + + reg { + field { + sw=rw; hw=na; + onwrite = wzs; + } f1[3:0] = 0x0; + + field { + sw=rw; hw=na; + onwrite = wzc; + } f2[7:4] = 0xF; + + field { + sw=rw; hw=na; + onwrite = wzt; + } f3[11:8] = 0x0; + } r3; + + reg { + field { + sw=rw; hw=na; + onwrite = wclr; + } f1[7:0] = 0xF0; + + field { + sw=rw; hw=na; + onwrite = wset; + } f2[15:8] = 0x0F; + } r4; +}; diff --git a/tests/test_onread_onwrite/tb_template.sv b/tests/test_onread_onwrite/tb_template.sv new file mode 100644 index 0000000..f3308e7 --- /dev/null +++ b/tests/test_onread_onwrite/tb_template.sv @@ -0,0 +1,30 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + ##1; + cb.rst <= '0; + ##1; + + cpuif.assert_read('h0, 'h0F_F0); + cpuif.assert_read('h0, 'hFF_00); + cpuif.write ('h0, 'h00_FF); + cpuif.assert_read('h0, 'h00_FF); + cpuif.assert_read('h0, 'hFF_00); + + cpuif.assert_read('h4, 'h0_F_0); + cpuif.write ('h4, 'h1_1_1); + cpuif.assert_read('h4, 'h1_E_1); + cpuif.write ('h4, 'h1_2_2); + cpuif.assert_read('h4, 'h0_C_3); + + cpuif.assert_read('h8, 'h0_F_0); + cpuif.write ('h8, 'hE_E_E); + cpuif.assert_read('h8, 'h1_E_1); + cpuif.write ('h8, 'hE_D_D); + cpuif.assert_read('h8, 'h0_C_3); + + cpuif.assert_read('hC, 'h0F_F0); + cpuif.write ('hC, 'h12_34); + cpuif.assert_read('hC, 'hFF_00); +{% endblock %} diff --git a/tests/test_onread_onwrite/testcase.py b/tests/test_onread_onwrite/testcase.py new file mode 100644 index 0000000..835b5ef --- /dev/null +++ b/tests/test_onread_onwrite/testcase.py @@ -0,0 +1,5 @@ +from ..lib.sim_testcase import SimTestCase + +class Test(SimTestCase): + def test_dut(self): + self.run_test() diff --git a/tests/test_parity/__init__.py b/tests/test_parity/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_parity/regblock.rdl b/tests/test_parity/regblock.rdl new file mode 100644 index 0000000..7056baf --- /dev/null +++ b/tests/test_parity/regblock.rdl @@ -0,0 +1,14 @@ +addrmap top { + default paritycheck; + default sw=rw; + default hw=na; + + reg my_reg { + field {} f1[16] = 0; + field {} f2[8] = 0; + field {} f3 = 0; + }; + + my_reg r1 @ 0x000; + my_reg r2[8] @ 0x1000; +}; diff --git a/tests/test_parity/tb_template.sv b/tests/test_parity/tb_template.sv new file mode 100644 index 0000000..392a23a --- /dev/null +++ b/tests/test_parity/tb_template.sv @@ -0,0 +1,37 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + ##1; + cb.rst <= '0; + ##1; + + fork + begin + repeat(50) begin + automatic int i = $urandom_range(7,0); + cpuif.write('h0, $urandom()); + cpuif.write('h1000 + i*4, $urandom()); + end + end + begin + forever begin + assert(cb.parity_error != 1'b1); + @cb; + end + end + join_any + disable fork; + + cpuif.write('h0, 'd0); + assign dut.field_storage.r1.f1.value = 16'd1; + deassign dut.field_storage.r1.f1.value; + @cb; + @cb; + assert(cb.parity_error == 1'b1); + cpuif.write('h0, 'd0); + @cb; + @cb; + assert(cb.parity_error == 1'b0); + +{% endblock %} diff --git a/tests/test_parity/testcase.py b/tests/test_parity/testcase.py new file mode 100644 index 0000000..835b5ef --- /dev/null +++ b/tests/test_parity/testcase.py @@ -0,0 +1,5 @@ +from ..lib.sim_testcase import SimTestCase + +class Test(SimTestCase): + def test_dut(self): + self.run_test() diff --git a/tests/test_pipelined_cpuif/__init__.py b/tests/test_pipelined_cpuif/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_pipelined_cpuif/regblock.rdl b/tests/test_pipelined_cpuif/regblock.rdl new file mode 100644 index 0000000..a208e43 --- /dev/null +++ b/tests/test_pipelined_cpuif/regblock.rdl @@ -0,0 +1,8 @@ +addrmap regblock { + default sw=rw; + default hw=r; + + reg { + field {} x[31:0] = 0; + } x[64] @ 0 += 4; +}; diff --git a/tests/test_pipelined_cpuif/tb_template.sv b/tests/test_pipelined_cpuif/tb_template.sv new file mode 100644 index 0000000..cfee40a --- /dev/null +++ b/tests/test_pipelined_cpuif/tb_template.sv @@ -0,0 +1,50 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + ##1; + cb.rst <= '0; + ##1; + + // Write all regs in parallel burst + for(int i=0; i<64; i++) begin + fork + automatic int i_fk = i; + begin + cpuif.write(i_fk*4, i_fk + 32'h12340000); + end + join_none + end + wait fork; + + // Verify HW value + @cb; + for(int i=0; i<64; i++) begin + assert(cb.hwif_out.x[i].x.value == i + 32'h12340000) + else $error("hwif_out.x[i] == 0x%0x. Expected 0x%0x", cb.hwif_out.x[i].x.value, i + 32'h12340000); + end + + // Read all regs in parallel burst + for(int i=0; i<64; i++) begin + fork + automatic int i_fk = i; + begin + cpuif.assert_read(i_fk*4, i_fk + 32'h12340000); + end + join_none + end + wait fork; + + // Mix read/writes + for(int i=0; i<8; i++) begin + fork + automatic int i_fk = i; + begin + cpuif.write(i_fk*4, i_fk + 32'h56780000); + cpuif.assert_read(i_fk*4, i_fk + 32'h56780000); + end + join_none + end + wait fork; + +{% endblock %} diff --git a/tests/test_pipelined_cpuif/testcase.py b/tests/test_pipelined_cpuif/testcase.py new file mode 100644 index 0000000..27fe9ae --- /dev/null +++ b/tests/test_pipelined_cpuif/testcase.py @@ -0,0 +1,14 @@ +from parameterized import parameterized_class + +from ..lib.sim_testcase import SimTestCase +from ..lib.test_params import get_permutations +from ..lib.cpuifs import ALL_CPUIF + +@parameterized_class(get_permutations({ + "cpuif": ALL_CPUIF, + "retime_read_fanin": [True, False], + "retime_read_response": [True, False], +})) +class Test(SimTestCase): + def test_dut(self): + self.run_test() diff --git a/tests/test_pkg_params/__init__.py b/tests/test_pkg_params/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_pkg_params/regblock.rdl b/tests/test_pkg_params/regblock.rdl new file mode 100644 index 0000000..09b7418 --- /dev/null +++ b/tests/test_pkg_params/regblock.rdl @@ -0,0 +1,11 @@ +addrmap top #( + longint N_REGS = 1, + longint REGWIDTH = 32, + string NAME = "abcd" +) { + reg reg_t { + regwidth = REGWIDTH; + field {sw=rw; hw=r;} f[REGWIDTH] = 1; + }; + reg_t regs[N_REGS]; +}; diff --git a/tests/test_pkg_params/tb_template.sv b/tests/test_pkg_params/tb_template.sv new file mode 100644 index 0000000..b33b3d2 --- /dev/null +++ b/tests/test_pkg_params/tb_template.sv @@ -0,0 +1,8 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + assert(regblock_pkg::N_REGS == {{testcase.n_regs}}); + assert(regblock_pkg::REGWIDTH == {{testcase.regwidth}}); + assert(regblock_pkg::NAME == "{{testcase.name}}"); +{% endblock %} diff --git a/tests/test_pkg_params/testcase.py b/tests/test_pkg_params/testcase.py new file mode 100644 index 0000000..790b21b --- /dev/null +++ b/tests/test_pkg_params/testcase.py @@ -0,0 +1,27 @@ +from parameterized import parameterized_class + +from ..lib.sim_testcase import SimTestCase +from ..lib.test_params import get_permutations + +PARAMS = get_permutations({ + "n_regs" : [1, 2], + "regwidth" : [8, 16], + "name" : ["hello", "world"], +}) +@parameterized_class(PARAMS) +class TestRetimedFanin(SimTestCase): + n_regs = 20 + regwidth = 32 + name = "xyz" + + @classmethod + def setUpClass(cls): + cls.rdl_elab_params = { + "N_REGS": cls.n_regs, + "REGWIDTH": cls.regwidth, + "NAME": f'"{cls.name}"', + } + super().setUpClass() + + def test_dut(self): + self.run_test() diff --git a/tests/test_precedence/__init__.py b/tests/test_precedence/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_precedence/regblock.rdl b/tests/test_precedence/regblock.rdl new file mode 100644 index 0000000..8affd3e --- /dev/null +++ b/tests/test_precedence/regblock.rdl @@ -0,0 +1,26 @@ +addrmap top { + reg { + field { + sw=rw; + hw=w; we; + precedence=sw; + } f_sw = 0; + field { + sw=rw; + hw=w; we; + precedence=hw; + } f_hw = 0; + } r1 @ 0x0; + + reg { + default counter; + default sw=r; + default hw=na; + + field {} f_sw_count[3:0] = 0; + field {} f_hw_count[7:4] = 0; + } r1_events @ 0x4; + + r1_events.f_sw_count->incr = r1.f_sw; + r1_events.f_hw_count->incr = r1.f_hw; +}; diff --git a/tests/test_precedence/tb_template.sv b/tests/test_precedence/tb_template.sv new file mode 100644 index 0000000..b870bef --- /dev/null +++ b/tests/test_precedence/tb_template.sv @@ -0,0 +1,26 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + ##1; + cb.rst <= '0; + ##1; + + // Always write both fields from hardware + cb.hwif_in.r1.f_sw.next <= '0; + cb.hwif_in.r1.f_sw.we <= '1; + cb.hwif_in.r1.f_hw.next <= '0; + cb.hwif_in.r1.f_hw.we <= '1; + @cb; + @cb; + + cpuif.assert_read('h0, 'b00); + cpuif.assert_read('h4, 'h00); + + cpuif.write('h0, 'b11); + cpuif.write('h0, 'b11); + cpuif.write('h0, 'b11); + cpuif.assert_read('h0, 'h00); + cpuif.assert_read('h4, 'h03); + +{% endblock %} diff --git a/tests/test_precedence/testcase.py b/tests/test_precedence/testcase.py new file mode 100644 index 0000000..835b5ef --- /dev/null +++ b/tests/test_precedence/testcase.py @@ -0,0 +1,5 @@ +from ..lib.sim_testcase import SimTestCase + +class Test(SimTestCase): + def test_dut(self): + self.run_test() diff --git a/tests/test_read_buffer/__init__.py b/tests/test_read_buffer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_read_buffer/regblock.rdl b/tests/test_read_buffer/regblock.rdl new file mode 100644 index 0000000..60ffd66 --- /dev/null +++ b/tests/test_read_buffer/regblock.rdl @@ -0,0 +1,117 @@ +addrmap top { + default regwidth = 8; + default accesswidth = 8; + default sw=r; + default hw=r; + + signal {} incr_en; + //-------------------------------------------------------------------------- + // Wide registers + //-------------------------------------------------------------------------- + reg { + regwidth = 32; + default counter; + default incr = incr_en; + buffer_reads; + + field {} f1[3] = 0; + field {} f2[3] = 0; + field {} f3[3] = 0; + field {} f4[3] = 0; + field {} f5[3] = 0; + field {} f6[3] = 0; + field {} f7[3] = 0; + field {} f8[3] = 0; + field {} f9[3] = 0; + field {} fa[3] = 0; + } reg1; + + reg { + regwidth = 32; + default counter; + default incr = incr_en; + buffer_reads; + + field {} f1[28:30] = 0; + field {} f2[3] = 0; + field {} f3[3] = 0; + field {} f4[3] = 0; + field {} f5[3] = 0; + field {} f6[3] = 0; + field {} f7[3] = 0; + field {} f8[3] = 0; + field {} f9[3] = 0; + field {} fa[3] = 0; + } reg1_msb0; + + + reg { + regwidth = 32; + default counter; + default incr = incr_en; + default rclr; + buffer_reads; + + field {} f1[4:0] = 0; + field {} f2[14:10] = 0; + field {} f3[26:22] = 0; + field {} f4[31:27] = 0; + } reg2; + + //-------------------------------------------------------------------------- + // Alternate Triggers + //-------------------------------------------------------------------------- + reg myreg { + buffer_reads; + default counter; + default incr = incr_en; + field {} f1[7:0] = 0; + }; + + reg myreg_wide { + buffer_reads; + default counter; + default incr = incr_en; + regwidth = 16; + field {} f1[15:0] = 0xAAAA; + }; + + // Trigger via another register + myreg g1_r1; + myreg g1_r2; + g1_r2->rbuffer_trigger = g1_r1; + + myreg_wide g2_r1 @ 0x10; + myreg_wide g2_r2; + g2_r2->rbuffer_trigger = g2_r1; + + // triger from signal + signal { + activehigh; + } trigger_sig; + signal { + activelow; + } trigger_sig_n; + reg ro_reg { + buffer_reads; + field { + hw=w; + } f1[7:0]; + }; + ro_reg g3_r1; + ro_reg g3_r2; + g3_r1->rbuffer_trigger = trigger_sig; + g3_r2->rbuffer_trigger = trigger_sig_n; + + // trigger from field/propref + reg { + field { + sw=w; hw=r; singlepulse; + } trig = 0; + } g4_trig; + myreg g4_r1; + myreg g4_r2; + g4_r1->rbuffer_trigger = g4_trig.trig; + g4_r2->rbuffer_trigger = g4_trig.trig->swmod; + +}; diff --git a/tests/test_read_buffer/tb_template.sv b/tests/test_read_buffer/tb_template.sv new file mode 100644 index 0000000..b6b91d7 --- /dev/null +++ b/tests/test_read_buffer/tb_template.sv @@ -0,0 +1,146 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + cb.hwif_in.incr_en <= '1; + cb.hwif_in.trigger_sig_n <= '1; + ##1; + cb.rst <= '0; + ##1; + + //-------------------------------------------------------------------------- + // Wide registers + //-------------------------------------------------------------------------- + + // reg1 + // expect to read all counter values atomically + begin + logic [7:0] subword; + logic [31:0] rdata; + logic [2:0] fdata; + cpuif.read('h0, subword); + fdata = subword[2:0]; + rdata = {10{fdata}}; + assert(subword == rdata[7:0]); + + cpuif.assert_read('h1, rdata[15:8]); + cpuif.assert_read('h2, rdata[23:16]); + cpuif.assert_read('h3, rdata[31:24]); + end + + // reg1_msb0 + // expect to read all counter values atomically + begin + logic [7:0] subword; + logic [31:0] rdata; + logic [2:0] fdata; + cpuif.read('h4, subword); + fdata = subword[3:1]; + rdata = {10{fdata}} << 1; + assert(subword == rdata[7:0]); + + cpuif.assert_read('h5, rdata[15:8]); + cpuif.assert_read('h6, rdata[23:16]); + cpuif.assert_read('h7, rdata[31:24]); + end + + cb.hwif_in.incr_en <= '0; + @cb; + + // check that msb0 ordering is correct + begin + logic [7:0] subword; + logic [31:0] rdata; + logic [2:0] fdata; + cpuif.read('h0, subword); + fdata = subword[2:0]; + rdata = {10{fdata}}; + assert(subword == rdata[7:0]); + + cpuif.assert_read('h1, rdata[15:8]); + cpuif.assert_read('h2, rdata[23:16]); + cpuif.assert_read('h3, rdata[31:24]); + + + fdata = {<<{fdata}}; + rdata = {10{fdata}} << 1; + cpuif.assert_read('h4, rdata[7:0]); + cpuif.assert_read('h5, rdata[15:8]); + cpuif.assert_read('h6, rdata[23:16]); + cpuif.assert_read('h7, rdata[31:24]); + end + + cb.hwif_in.incr_en <= '1; + + // reg2 + // read-clear + repeat(2) begin + logic [7:0] subword; + logic [4:0] fdata; + logic [31:0] rdata; + + cpuif.read('h8, subword); + rdata[7:0] = subword; + cpuif.read('h9, subword); + rdata[15:8] = subword; + cpuif.read('hA, subword); + rdata[23:16] = subword; + cpuif.read('hB, subword); + rdata[31:24] = subword; + + fdata = rdata[4:0]; + assert(rdata[14:10] == fdata); + assert(rdata[26:22] == fdata); + assert(rdata[31:27] == fdata); + end + + + //-------------------------------------------------------------------------- + // Alternate Triggers + //-------------------------------------------------------------------------- + + // Trigger via another register + // g1 + begin + logic [7:0] rdata; + cpuif.read('hC, rdata); + cpuif.assert_read('hD, rdata); + end + + // g2 + begin + logic [7:0] rdata1; + logic [7:0] rdata2; + cpuif.read('h10, rdata1); + cpuif.read('h11, rdata2); + cpuif.assert_read('h12, rdata1); + cpuif.assert_read('h13, rdata2); + end + + // triger from signal + // g3 + cb.hwif_in.g3_r1.f1.next <= 'hAB; + cb.hwif_in.g3_r2.f1.next <= 'hCD; + cb.hwif_in.trigger_sig <= '1; + cb.hwif_in.trigger_sig_n <= '0; + @cb; + cb.hwif_in.g3_r1.f1.next <= 'h00; + cb.hwif_in.g3_r2.f1.next <= 'h00; + cb.hwif_in.trigger_sig <= '0; + cb.hwif_in.trigger_sig_n <= '1; + @cb; + cpuif.assert_read('h14, 'hAB); + cpuif.assert_read('h15, 'hCD); + + // trigger from field/propref + // g4 + begin + logic [7:0] rdata; + cpuif.write('h16, 'h1); + repeat(5) @cb; + cpuif.read('h17, rdata); + repeat(5) @cb; + cpuif.assert_read('h18, rdata - 1); // swmod happens one cycle earlier, so count is -1 + end + +{% endblock %} diff --git a/tests/test_read_buffer/testcase.py b/tests/test_read_buffer/testcase.py new file mode 100644 index 0000000..835b5ef --- /dev/null +++ b/tests/test_read_buffer/testcase.py @@ -0,0 +1,5 @@ +from ..lib.sim_testcase import SimTestCase + +class Test(SimTestCase): + def test_dut(self): + self.run_test() diff --git a/tests/test_read_fanin/__init__.py b/tests/test_read_fanin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_read_fanin/regblock.rdl b/tests/test_read_fanin/regblock.rdl new file mode 100644 index 0000000..5d6bffd --- /dev/null +++ b/tests/test_read_fanin/regblock.rdl @@ -0,0 +1,10 @@ +addrmap top #( + longint N_REGS = 1, + longint REGWIDTH = 32 +) { + reg reg_t { + regwidth = REGWIDTH; + field {sw=rw; hw=na;} f[REGWIDTH] = 1; + }; + reg_t regs[N_REGS]; +}; diff --git a/tests/test_read_fanin/tb_template.sv b/tests/test_read_fanin/tb_template.sv new file mode 100644 index 0000000..1154c3f --- /dev/null +++ b/tests/test_read_fanin/tb_template.sv @@ -0,0 +1,34 @@ +{% extends "lib/tb_base.sv" %} + +{%- block declarations %} + {% sv_line_anchor %} + localparam REGWIDTH = {{testcase.regwidth}}; + localparam STRIDE = REGWIDTH/8; + localparam N_REGS = {{testcase.n_regs}}; +{%- endblock %} + + +{% block seq %} + {% sv_line_anchor %} + bit [REGWIDTH-1:0] data[N_REGS]; + + ##1; + cb.rst <= '0; + ##1; + + foreach(data[i]) data[i] = {$urandom(), $urandom(), $urandom(), $urandom()}; + + for(int i=0; iresetsignal = f2_reset; + r2.f2->resetsignal = f2_reset; + r3.f2->resetsignal = f2_reset; + r4.f2->resetsignal = f2_reset; + r5.f2->resetsignal = f2_reset; +}; diff --git a/tests/test_reset_signals/tb_template.sv b/tests/test_reset_signals/tb_template.sv new file mode 100644 index 0000000..5429b9e --- /dev/null +++ b/tests/test_reset_signals/tb_template.sv @@ -0,0 +1,128 @@ +{% extends "lib/tb_base.sv" %} + +{%- block declarations %} + logic root_cpuif_reset; + logic [15:0] r5f2_resetvalue; +{%- endblock %} + +{%- block clocking_dirs %} + output root_cpuif_reset; + output r5f2_resetvalue; +{%- endblock %} + +{% block seq %} + {% sv_line_anchor %} + cb.root_cpuif_reset <= '1; + cb.hwif_in.r2.my_reset <= '1; + cb.hwif_in.r3.my_areset <= '1; + cb.hwif_in.r4.my_reset_n <= '0; + cb.hwif_in.r5.my_areset_n <= '0; + cb.hwif_in.f2_reset <= '1; + cb.r5f2_resetvalue <= 'hABCD; + ##2; + cb.rst <= '0; + cb.root_cpuif_reset <= '0; + cb.hwif_in.r2.my_reset <= '0; + cb.hwif_in.r3.my_areset <= '0; + cb.hwif_in.r4.my_reset_n <= '1; + cb.hwif_in.r5.my_areset_n <= '1; + cb.hwif_in.f2_reset <= '0; + ##1; + + + cpuif.assert_read('h00, 'h5678_1234); + cpuif.assert_read('h04, 'h5678_1234); + cpuif.assert_read('h08, 'h5678_1234); + cpuif.assert_read('h0c, 'h5678_1234); + cpuif.assert_read('h10, 'hABCD_1234); + + for(int i=0; i<5; i++) cpuif.write(i*4, 0); + + cpuif.assert_read('h00, 'h0000_0000); + cpuif.assert_read('h04, 'h0000_0000); + cpuif.assert_read('h08, 'h0000_0000); + cpuif.assert_read('h0c, 'h0000_0000); + cpuif.assert_read('h10, 'h0000_0000); + + cb.rst <= '1; + @cb; + cb.rst <= '0; + @cb; + + cpuif.assert_read('h00, 'h0000_1234); + cpuif.assert_read('h04, 'h0000_0000); + cpuif.assert_read('h08, 'h0000_0000); + cpuif.assert_read('h0c, 'h0000_0000); + cpuif.assert_read('h10, 'h0000_0000); + + for(int i=0; i<5; i++) cpuif.write(i*4, 0); + + cb.hwif_in.r2.my_reset <= '1; + @cb; + cb.hwif_in.r2.my_reset <= '0; + @cb; + + cpuif.assert_read('h00, 'h0000_0000); + cpuif.assert_read('h04, 'h0000_1234); + cpuif.assert_read('h08, 'h0000_0000); + cpuif.assert_read('h0c, 'h0000_0000); + cpuif.assert_read('h10, 'h0000_0000); + + for(int i=0; i<5; i++) cpuif.write(i*4, 0); + + ##1; + #2ns; + hwif_in.r3.my_areset = '1; + #1ns; + hwif_in.r3.my_areset = '0; + ##1; + + cpuif.assert_read('h00, 'h0000_0000); + cpuif.assert_read('h04, 'h0000_0000); + cpuif.assert_read('h08, 'h0000_1234); + cpuif.assert_read('h0c, 'h0000_0000); + cpuif.assert_read('h10, 'h0000_0000); + + for(int i=0; i<5; i++) cpuif.write(i*4, 0); + + cb.hwif_in.r4.my_reset_n <= '0; + @cb; + cb.hwif_in.r4.my_reset_n <= '1; + @cb; + + cpuif.assert_read('h00, 'h0000_0000); + cpuif.assert_read('h04, 'h0000_0000); + cpuif.assert_read('h08, 'h0000_0000); + cpuif.assert_read('h0c, 'h0000_1234); + cpuif.assert_read('h10, 'h0000_0000); + + for(int i=0; i<5; i++) cpuif.write(i*4, 0); + + ##1; + #2ns; + hwif_in.r5.my_areset_n = '0; + #1ns; + hwif_in.r5.my_areset_n = '1; + ##1; + + cpuif.assert_read('h00, 'h0000_0000); + cpuif.assert_read('h04, 'h0000_0000); + cpuif.assert_read('h08, 'h0000_0000); + cpuif.assert_read('h0c, 'h0000_0000); + cpuif.assert_read('h10, 'h0000_1234); + + for(int i=0; i<5; i++) cpuif.write(i*4, 0); + + @cb; + cb.hwif_in.f2_reset <= '1; + cb.r5f2_resetvalue <= 'h3210; + @cb; + cb.hwif_in.f2_reset <= '0; + + cpuif.assert_read('h00, 'h5678_0000); + cpuif.assert_read('h04, 'h5678_0000); + cpuif.assert_read('h08, 'h5678_0000); + cpuif.assert_read('h0c, 'h5678_0000); + cpuif.assert_read('h10, 'h3210_0000); + +{% endblock %} diff --git a/tests/test_reset_signals/testcase.py b/tests/test_reset_signals/testcase.py new file mode 100644 index 0000000..835b5ef --- /dev/null +++ b/tests/test_reset_signals/testcase.py @@ -0,0 +1,5 @@ +from ..lib.sim_testcase import SimTestCase + +class Test(SimTestCase): + def test_dut(self): + self.run_test() diff --git a/tests/test_singlepulse/__init__.py b/tests/test_singlepulse/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_singlepulse/regblock.rdl b/tests/test_singlepulse/regblock.rdl new file mode 100644 index 0000000..82ff060 --- /dev/null +++ b/tests/test_singlepulse/regblock.rdl @@ -0,0 +1,8 @@ +addrmap top { + reg { + field { + sw=rw; hw=r; + singlepulse; + } f[0:0] = 0; + } r1; +}; diff --git a/tests/test_singlepulse/tb_template.sv b/tests/test_singlepulse/tb_template.sv new file mode 100644 index 0000000..27ff6dd --- /dev/null +++ b/tests/test_singlepulse/tb_template.sv @@ -0,0 +1,56 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + int event_count; + + ##1; + cb.rst <= '0; + ##1; + + // No pulse if writing zero + event_count = 0; + fork + begin + ##0; + forever begin + @cb; + if(cb.hwif_out.r1.f.value) begin + event_count++; + end + end + end + + begin + cpuif.write('h0, 'h0); + repeat(5) @cb; + end + join_any + disable fork; + assert(event_count == 0) else $error("Observed excess singlepulse events: %0d", event_count); + + // single pulse + event_count = 0; + fork + begin + ##0; + forever begin + @cb; + if(cb.hwif_out.r1.f.value) begin + event_count++; + end + end + end + + begin + cpuif.write('h0, 'h1); + repeat(5) @cb; + end + join_any + disable fork; + assert(event_count == 1) else $error("Observed incorrect number of singlepulse events: %0d", event_count); + + // auto-clears + cpuif.assert_read('h0, 'h0); + +{% endblock %} diff --git a/tests/test_singlepulse/testcase.py b/tests/test_singlepulse/testcase.py new file mode 100644 index 0000000..835b5ef --- /dev/null +++ b/tests/test_singlepulse/testcase.py @@ -0,0 +1,5 @@ +from ..lib.sim_testcase import SimTestCase + +class Test(SimTestCase): + def test_dut(self): + self.run_test() diff --git a/tests/test_structural_sw_rw/__init__.py b/tests/test_structural_sw_rw/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_structural_sw_rw/regblock.rdl b/tests/test_structural_sw_rw/regblock.rdl new file mode 100644 index 0000000..30488d1 --- /dev/null +++ b/tests/test_structural_sw_rw/regblock.rdl @@ -0,0 +1,44 @@ +addrmap regblock { + default sw=rw; + default hw=r; + + reg my_reg { + field {} a[8] = 0x23; + field {} b = 0; + field {} c[31:31] = 1; + }; + + my_reg r0 @0x000; + r0.a->reset = 0x42; + + my_reg r1[2][3][4] @0x10 += 8; + + my_reg r2 @0x1000; + r2.a->reset = 0x11; + + + reg subreg { + field {} x[7:4] = 1; + }; + regfile subrf { + subreg r1[4] @ 0x0 += 4; + regfile { + subreg r1 @ 0x0; + subreg r2[2] @ 0x4 += 4; + subreg r3 @ 0xc; + } sub[2] @ 0x10 += 0x10; + subreg r2[4] @ 0x30 += 4; + }; + subrf sub2[2] @ 0x2000 += 0x40; + subreg r3 @ 0x2080; + + reg { + field {} f1[19:12] = 0; + field {} f2[30:20] = 0; + } rw_reg @ 0x3000; + + reg { + field {} f1[12:19] = 0; + field {} f2[20:30] = 0; + } rw_reg_lsb0 @ 0x3004; +}; diff --git a/tests/test_structural_sw_rw/tb_template.sv b/tests/test_structural_sw_rw/tb_template.sv new file mode 100644 index 0000000..591bfe4 --- /dev/null +++ b/tests/test_structural_sw_rw/tb_template.sv @@ -0,0 +1,80 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + ##1; + cb.rst <= '0; + ##1; + + // Assert value via frontdoor + cpuif.assert_read(0, 32'h8000_0042); + for(int i=0; i<2*3*4; i++) begin + cpuif.assert_read('h10+i*8, 32'h8000_0023); + end + cpuif.assert_read('h1000, 32'h8000_0011); + for(int i=0; i<33; i++) begin + cpuif.assert_read('h2000 +i*4, 32'h0000_0010); + end + + // Assert via hwif + assert(cb.hwif_out.r0.a.value == 'h42); + assert(cb.hwif_out.r0.b.value == 'h0); + assert(cb.hwif_out.r0.c.value == 'h1); + foreach(cb.hwif_out.r1[x, y, z]) begin + assert(cb.hwif_out.r1[x][y][z].a.value == 'h23); + assert(cb.hwif_out.r1[x][y][z].b.value == 'h0); + assert(cb.hwif_out.r1[x][y][z].c.value == 'h1); + end + assert(cb.hwif_out.r2.a.value == 'h11); + assert(cb.hwif_out.r2.b.value == 'h0); + assert(cb.hwif_out.r2.c.value == 'h1); + + // Write values + cpuif.write(0, 32'h8000_0002); + for(int i=0; i<2*3*4; i++) begin + cpuif.write('h10+i*8, i+'h110a); + end + cpuif.write('h1000, 32'h0000_0000); + for(int i=0; i<33; i++) begin + cpuif.write('h2000 +i*4, i << 4); + end + + // Assert value via frontdoor + cpuif.assert_read(0, 32'h8000_0002); + for(int i=0; i<2*3*4; i++) begin + cpuif.assert_read('h10+i*8, i+'h10a); + end + cpuif.assert_read('h1000, 32'h0000_0000); + for(int i=0; i<33; i++) begin + cpuif.assert_read('h2000 +i*4, (i << 4) & 'hF0); + end + + // Assert via hwif + assert(cb.hwif_out.r0.a.value == 'h02); + assert(cb.hwif_out.r0.b.value == 'h0); + assert(cb.hwif_out.r0.c.value == 'h1); + foreach(cb.hwif_out.r1[x, y, z]) begin + assert(cb.hwif_out.r1[x][y][z].a.value == x*12+y*4+z+10); + assert(cb.hwif_out.r1[x][y][z].b.value == 'h1); + assert(cb.hwif_out.r1[x][y][z].c.value == 'h0); + end + assert(cb.hwif_out.r2.a.value == 'h0); + assert(cb.hwif_out.r2.b.value == 'h0); + assert(cb.hwif_out.r2.c.value == 'h0); + + // rw_reg + cpuif.assert_read('h3000, 0); + cpuif.write('h3000, 'h4DEAB000); + @cb; + assert(cb.hwif_out.rw_reg.f1.value == 8'hAB); + assert(cb.hwif_out.rw_reg.f2.value == 11'h4DE); + cpuif.assert_read('h3000, 'h4DEAB000); + + // rw_reg_lsb0 + cpuif.assert_read('h3004, 0); + cpuif.write('h3004, 'h4DEAB000); + @cb; + assert(`bitswap(cb.hwif_out.rw_reg_lsb0.f1.value) == 8'hAB); + assert(`bitswap(cb.hwif_out.rw_reg_lsb0.f2.value) == 11'h4DE); + cpuif.assert_read('h3004, 'h4DEAB000); +{% endblock %} diff --git a/tests/test_structural_sw_rw/testcase.py b/tests/test_structural_sw_rw/testcase.py new file mode 100644 index 0000000..afb1d82 --- /dev/null +++ b/tests/test_structural_sw_rw/testcase.py @@ -0,0 +1,51 @@ +import os + +from parameterized import parameterized_class + +from ..lib.sim_testcase import SimTestCase +from ..lib.synth_testcase import SynthTestCase +from ..lib.test_params import get_permutations +from ..lib.cpuifs import ALL_CPUIF + + + + +@parameterized_class(get_permutations({ + "cpuif": ALL_CPUIF, + "retime_read_fanin": [True, False], + "retime_read_response": [True, False], +})) +class TestCPUIFS(SimTestCase): + def test_dut(self): + self.run_test() + + + +@parameterized_class(get_permutations({ + "reuse_hwif_typedefs": [True, False], +})) +class TestTypedefs(SimTestCase): + def test_dut(self): + self.run_test() + + + +@parameterized_class(get_permutations({ + "default_reset_activelow": [True, False], + "default_reset_async": [True, False], +})) +class TestDefaultResets(SimTestCase): + def test_dut(self): + self.run_test() + + + +@parameterized_class(get_permutations({ + "cpuif": ALL_CPUIF, + "retime_read_fanin": [True, False], + "retime_read_response": [True, False], + "reuse_hwif_typedefs": [True, False], +})) +class TestSynth(SynthTestCase): + def test_dut(self): + self.run_synth() diff --git a/tests/test_swacc_swmod/__init__.py b/tests/test_swacc_swmod/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_swacc_swmod/regblock.rdl b/tests/test_swacc_swmod/regblock.rdl new file mode 100644 index 0000000..877098b --- /dev/null +++ b/tests/test_swacc_swmod/regblock.rdl @@ -0,0 +1,43 @@ +addrmap top { + default regwidth = 8; + + reg { + field { + sw=r; hw=w; + swacc; + swmod; + } f[8]; + } r1; + + reg { + field { + sw=rw; hw=r; + swmod; + } f[8] = 20; + } r2; + + reg { + field { + sw=rw; hw=r; + swmod; + rclr; + } f[8] = 30; + } r3; + + reg { + field { + sw=rw; hw=r; + swacc; + swmod; + } f[8] = 0x12; + } r4; + + reg { + field { + sw=r; hw=rw; + we; + swmod; + rclr; + } f[8] = 30; + } r5; +}; diff --git a/tests/test_swacc_swmod/tb_template.sv b/tests/test_swacc_swmod/tb_template.sv new file mode 100644 index 0000000..5a9fef9 --- /dev/null +++ b/tests/test_swacc_swmod/tb_template.sv @@ -0,0 +1,198 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + logic [7:0] counter; + logic [7:0] rd_data; + logic [7:0] latched_data; + int event_count; + bit fired; + latched_data = 'x; + + ##1; + cb.rst <= '0; + ##1; + + // Verify that hwif gets sampled at the same cycle as swacc strobe + counter = 'h10; + cb.hwif_in.r1.f.next <= counter; + @cb; + event_count = 0; + fork + begin + ##0; + forever begin + counter++; + cb.hwif_in.r1.f.next <= counter; + @cb; + if(cb.hwif_out.r1.f.swacc) begin + latched_data = counter; + event_count++; + end + end + end + + begin + cpuif.read('h0, rd_data); + repeat(4) @cb; + end + join_any + disable fork; + assert(rd_data == latched_data) else $error("Read returned 0x%0x but swacc strobed during 0x%0x", rd_data, latched_data); + assert(event_count == 1) else $error("Observed excess swacc events: %0d", event_count); + + // Verify that writing a read-only register with no side effects never asserts swmod + cb.hwif_in.r1.f.next <= 'h99; + @cb; + fork + begin + ##0; + forever begin + assert(cb.hwif_out.r1.f.swmod == 0); + @cb; + end + end + begin + cpuif.write('h0, 'h0); + cpuif.assert_read('h0, 'h99); + repeat(4) @cb; + end + join_any + disable fork; + + + // Verify that hwif changes 1 cycle after swmod + fired = 0; + fork + begin + ##0; + forever begin + assert(cb.hwif_out.r2.f.value == 20); + if(cb.hwif_out.r2.f.swmod) break; + @cb; + end + fired = 1; + @cb; + forever begin + assert(cb.hwif_out.r2.f.value == 21); + assert(cb.hwif_out.r2.f.swmod == 0); + @cb; + end + end + + begin + cpuif.write('h1, 21); + repeat(4) @cb; + end + join_any + disable fork; + assert(fired); + + // Verify that swmod does NOT trigger if strobes not set + fired = 0; + fork + begin + ##0; + forever begin + assert(cb.hwif_out.r2.f.value == 21); + if(cb.hwif_out.r2.f.swmod) break; + @cb; + end + fired = 1; + end + + begin + cpuif.write('h1, 22, 0); + repeat(4) @cb; + end + join_any + disable fork; + assert(!fired); + + // Verify that hwif changes 1 cycle after swmod + fired = 0; + fork + begin + ##0; + forever begin + assert(cb.hwif_out.r3.f.value == 30); + if(cb.hwif_out.r3.f.swmod) break; + @cb; + end + fired = 1; + @cb; + forever begin + assert(cb.hwif_out.r3.f.value == 0); + assert(cb.hwif_out.r3.f.swmod == 0); + @cb; + end + end + + begin + cpuif.assert_read('h2, 30); + repeat(4) @cb; + end + join_any + disable fork; + assert(fired); + + // Verify swacc and swmod assert when written + fired = 0; + fork + begin + ##0; + forever begin + assert(cb.hwif_out.r4.f.value == 'h12); + if(cb.hwif_out.r4.f.swmod || cb.hwif_out.r4.f.swacc) begin + assert(cb.hwif_out.r4.f.swmod == 1); + assert(cb.hwif_out.r4.f.swacc == 1); + break; + end + @cb; + end + fired = 1; + @cb; + forever begin + assert(cb.hwif_out.r4.f.value == 'h34); + assert(cb.hwif_out.r4.f.swmod == 0); + @cb; + end + end + + begin + cpuif.write('h3, 'h34); + repeat(4) @cb; + end + join_any + disable fork; + assert(fired); + + + // Verify that hwif changes 1 cycle after swmod + fired = 0; + fork + begin + ##0; + forever begin + assert(cb.hwif_out.r5.f.value == 30); + if(cb.hwif_out.r5.f.swmod) break; + @cb; + end + fired = 1; + @cb; + forever begin + assert(cb.hwif_out.r5.f.value == 0); + assert(cb.hwif_out.r5.f.swmod == 0); + @cb; + end + end + + begin + cpuif.assert_read('h4, 30); + repeat(4) @cb; + end + join_any + disable fork; + assert(fired); + +{% endblock %} diff --git a/tests/test_swacc_swmod/testcase.py b/tests/test_swacc_swmod/testcase.py new file mode 100644 index 0000000..835b5ef --- /dev/null +++ b/tests/test_swacc_swmod/testcase.py @@ -0,0 +1,5 @@ +from ..lib.sim_testcase import SimTestCase + +class Test(SimTestCase): + def test_dut(self): + self.run_test() diff --git a/tests/test_swwe/__init__.py b/tests/test_swwe/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_swwe/regblock.rdl b/tests/test_swwe/regblock.rdl new file mode 100644 index 0000000..c31fc29 --- /dev/null +++ b/tests/test_swwe/regblock.rdl @@ -0,0 +1,66 @@ +addrmap top { + default regwidth = 8; + + reg { + field { + sw=rw; hw=na; + } r3_swwe[0:0] = 1; + + field { + sw=rw; hw=na; + } r4_swwel[1:1] = 0; + } lock; + + //--------------------------------- + // via inferred signal + //--------------------------------- + + reg { + field { + sw=rw; hw=na; + swwe; + } f[8] = 0x11; + } r1; + + reg { + field { + sw=rw; hw=na; + swwel; + } f[8] = 0x22; + } r2; + + //--------------------------------- + // via lock register + //--------------------------------- + + reg { + field { + sw=rw; hw=na; + } f[8] = 0x33; + } r3; + r3.f->swwe = lock.r3_swwe; + + reg { + field { + sw=rw; hw=na; + } f[8] = 0x44; + } r4; + r4.f->swwel = lock.r4_swwel; + + //--------------------------------- + // via prop ref chaining + //--------------------------------- + reg { + field { + sw=rw; hw=na; + } f[8] = 0x55; + } r5; + r5.f->swwe = r3.f->swwe; + + reg { + field { + sw=rw; hw=na; + } f[8] = 0x66; + } r6; + r6.f->swwe = r4.f->swwel; // intentionally opposite! +}; diff --git a/tests/test_swwe/tb_template.sv b/tests/test_swwe/tb_template.sv new file mode 100644 index 0000000..114816f --- /dev/null +++ b/tests/test_swwe/tb_template.sv @@ -0,0 +1,63 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + ##1; + cb.rst <= '0; + ##1; + + // r1 swwe = true + cpuif.assert_read('h1, 'h11); + cb.hwif_in.r1.f.swwe <= '0; + cpuif.write ('h1, 'h12); + cpuif.assert_read('h1, 'h11); + cb.hwif_in.r1.f.swwe <= '1; + cpuif.write ('h1, 'h13); + cpuif.assert_read('h1, 'h13); + + // r2 swwel = true + cpuif.assert_read('h2, 'h22); + cb.hwif_in.r2.f.swwel <= '1; + cpuif.write ('h2, 'h23); + cpuif.assert_read('h2, 'h22); + cb.hwif_in.r2.f.swwel <= '0; + cpuif.write ('h2, 'h24); + cpuif.assert_read('h2, 'h24); + + // r3 swwe = lock.r3_swwe + cpuif.assert_read('h3, 'h33); + cpuif.write ('h0, 'h0); + cpuif.write ('h3, 'h32); + cpuif.assert_read('h3, 'h33); + cpuif.write ('h0, 'h1); + cpuif.write ('h3, 'h34); + cpuif.assert_read('h3, 'h34); + + // r4 swwel = lock.r4_swwel + cpuif.assert_read('h4, 'h44); + cpuif.write ('h0, 'h2); + cpuif.write ('h4, 'h40); + cpuif.assert_read('h4, 'h44); + cpuif.write ('h0, 'h0); + cpuif.write ('h4, 'h45); + cpuif.assert_read('h4, 'h45); + + // r5 swwe = r3->swwe = lock.r3_swwe + cpuif.assert_read('h5, 'h55); + cpuif.write ('h0, 'h0); + cpuif.write ('h5, 'h52); + cpuif.assert_read('h5, 'h55); + cpuif.write ('h0, 'h1); + cpuif.write ('h5, 'h54); + cpuif.assert_read('h5, 'h54); + + // r6 swwe = r4->swwel = lock.r4_swwel + cpuif.assert_read('h6, 'h66); + cpuif.write ('h0, 'h0); + cpuif.write ('h6, 'h60); + cpuif.assert_read('h6, 'h66); + cpuif.write ('h0, 'h2); + cpuif.write ('h6, 'h65); + cpuif.assert_read('h6, 'h65); + +{% endblock %} diff --git a/tests/test_swwe/testcase.py b/tests/test_swwe/testcase.py new file mode 100644 index 0000000..835b5ef --- /dev/null +++ b/tests/test_swwe/testcase.py @@ -0,0 +1,5 @@ +from ..lib.sim_testcase import SimTestCase + +class Test(SimTestCase): + def test_dut(self): + self.run_test() diff --git a/tests/test_user_cpuif/__init__.py b/tests/test_user_cpuif/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_user_cpuif/regblock.rdl b/tests/test_user_cpuif/regblock.rdl new file mode 100644 index 0000000..edc1829 --- /dev/null +++ b/tests/test_user_cpuif/regblock.rdl @@ -0,0 +1,7 @@ +addrmap top { + reg { + field { + sw=rw; hw=r; + } f = 0; + } r1; +}; diff --git a/tests/test_user_cpuif/testcase.py b/tests/test_user_cpuif/testcase.py new file mode 100644 index 0000000..3e8465e --- /dev/null +++ b/tests/test_user_cpuif/testcase.py @@ -0,0 +1,49 @@ +import os + +from peakrdl_regblock.cpuif.apb3 import APB3_Cpuif +from ..lib.cpuifs.apb3 import APB3 +from ..lib.base_testcase import BaseTestCase + + +#------------------------------------------------------------------------------- + +class ClassOverride_Cpuif(APB3_Cpuif): + @property + def port_declaration(self) -> str: + return "user_apb3_intf.slave s_apb" + +class ClassOverride_cpuiftestmode(APB3): + cpuif_cls = ClassOverride_Cpuif + + +class Test_class_override(BaseTestCase): + cpuif = ClassOverride_cpuiftestmode() + + def test_override_success(self): + output_file = os.path.join(self.get_run_dir(), "regblock.sv") + with open(output_file, "r") as f: + self.assertIn( + "user_apb3_intf.slave s_apb", + f.read() + ) + +#------------------------------------------------------------------------------- + +class TemplateOverride_Cpuif(APB3_Cpuif): + # contains the text "USER TEMPLATE OVERRIDE" + template_path = "user_apb3_tmpl.sv" + +class TemplateOverride_cpuiftestmode(APB3): + cpuif_cls = TemplateOverride_Cpuif + + +class Test_template_override(BaseTestCase): + cpuif = TemplateOverride_cpuiftestmode() + + def test_override_success(self): + output_file = os.path.join(self.get_run_dir(), "regblock.sv") + with open(output_file, "r") as f: + self.assertIn( + "USER TEMPLATE OVERRIDE", + f.read() + ) diff --git a/tests/test_user_cpuif/user_apb3_tmpl.sv b/tests/test_user_cpuif/user_apb3_tmpl.sv new file mode 100644 index 0000000..bea1976 --- /dev/null +++ b/tests/test_user_cpuif/user_apb3_tmpl.sv @@ -0,0 +1 @@ +// USER TEMPLATE OVERRIDE diff --git a/tests/test_validation_errors/__init__.py b/tests/test_validation_errors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_validation_errors/external_ref.rdl b/tests/test_validation_errors/external_ref.rdl new file mode 100644 index 0000000..99778b6 --- /dev/null +++ b/tests/test_validation_errors/external_ref.rdl @@ -0,0 +1,15 @@ +addrmap sub { + reg { + field {} f; + } x; +}; + +addrmap top { + reg { + field {} f; + } x; + + sub sub; + + x.f->reset = sub.x.f; +}; diff --git a/tests/test_validation_errors/fixedpoint_counter.rdl b/tests/test_validation_errors/fixedpoint_counter.rdl new file mode 100644 index 0000000..426d70e --- /dev/null +++ b/tests/test_validation_errors/fixedpoint_counter.rdl @@ -0,0 +1,9 @@ +addrmap top { + reg { + field { + sw = rw; hw = r; + intwidth = 4; + counter; + } fixedpoint_counter[8] = 0; + } r1; +}; diff --git a/tests/test_validation_errors/fixedpoint_enum.rdl b/tests/test_validation_errors/fixedpoint_enum.rdl new file mode 100644 index 0000000..8d3fe84 --- /dev/null +++ b/tests/test_validation_errors/fixedpoint_enum.rdl @@ -0,0 +1,15 @@ +addrmap top { + reg { + enum test_enum { + zero = 2'b00; + one = 2'b01; + two = 2'b10; + three = 2'b11; + }; + field { + sw = rw; hw = r; + fracwidth = 0; + encode = test_enum; + } fixedpoint_enum[2] = 0; + } r1; +}; diff --git a/tests/test_validation_errors/fixedpoint_inconsistent_width.rdl b/tests/test_validation_errors/fixedpoint_inconsistent_width.rdl new file mode 100644 index 0000000..dd49956 --- /dev/null +++ b/tests/test_validation_errors/fixedpoint_inconsistent_width.rdl @@ -0,0 +1,9 @@ +addrmap top { + reg { + field { + sw = rw; hw = r; + intwidth = 4; + fracwidth = 5; + } num[8] = 0; + } r1; +}; diff --git a/tests/test_validation_errors/inconsistent_accesswidth.rdl b/tests/test_validation_errors/inconsistent_accesswidth.rdl new file mode 100644 index 0000000..9a3057b --- /dev/null +++ b/tests/test_validation_errors/inconsistent_accesswidth.rdl @@ -0,0 +1,9 @@ +addrmap top { + reg { + field {}f; + } x; + reg { + accesswidth = 16; + field {}f; + } y; +}; diff --git a/tests/test_validation_errors/multiple_unconditional_assigns.rdl b/tests/test_validation_errors/multiple_unconditional_assigns.rdl new file mode 100644 index 0000000..a087018 --- /dev/null +++ b/tests/test_validation_errors/multiple_unconditional_assigns.rdl @@ -0,0 +1,40 @@ +addrmap top { + reg { + field { + sw = rw; + hw = w; + singlepulse = true; + } a = 0; + field { + sw = rw; + hw = w; + singlepulse = true; + posedge intr; + stickybit = false; + } b = 0; + field { + sw = rw; + hw = w; + singlepulse = true; + negedge intr; + stickybit = false; + } c = 0; + field { + sw = rw; + hw = w; + singlepulse = true; + bothedge intr; + stickybit = false; + } d = 0; + } x; +}; + +/* +stickybit=false + intr posedge +stickybit=false + intr negedge +stickybit=false + intr bothedge +hw=w wel = false +singlepulse + + +*/ diff --git a/tests/test_validation_errors/sharedextbus.rdl b/tests/test_validation_errors/sharedextbus.rdl new file mode 100644 index 0000000..de14091 --- /dev/null +++ b/tests/test_validation_errors/sharedextbus.rdl @@ -0,0 +1,7 @@ +addrmap top { + sharedextbus; + + reg { + field {}f; + } x; +}; diff --git a/tests/test_validation_errors/signed_counter.rdl b/tests/test_validation_errors/signed_counter.rdl new file mode 100644 index 0000000..9910a81 --- /dev/null +++ b/tests/test_validation_errors/signed_counter.rdl @@ -0,0 +1,14 @@ +addrmap top { + reg { + field { + sw = rw; hw = r; + is_signed = false; + counter; + } unsigned_counter[8] = 0; + field { + sw = rw; hw = r; + is_signed; + counter; + } signed_counter[8] = 0; + } r1; +}; diff --git a/tests/test_validation_errors/signed_enum.rdl b/tests/test_validation_errors/signed_enum.rdl new file mode 100644 index 0000000..2b91450 --- /dev/null +++ b/tests/test_validation_errors/signed_enum.rdl @@ -0,0 +1,20 @@ +addrmap top { + reg { + enum test_enum { + zero = 2'b00; + one = 2'b01; + two = 2'b10; + three = 2'b11; + }; + field { + sw = rw; hw = r; + is_signed = false; + encode = test_enum; + } unsigned_enum[2] = 0; + field { + sw = rw; hw = r; + is_signed; + encode = test_enum; + } signed_enum[2] = 0; + } r1; +}; diff --git a/tests/test_validation_errors/testcase.py b/tests/test_validation_errors/testcase.py new file mode 100644 index 0000000..3267dac --- /dev/null +++ b/tests/test_validation_errors/testcase.py @@ -0,0 +1,121 @@ +import io +import contextlib + +from systemrdl.messages import RDLCompileError + +from ..lib.base_testcase import BaseTestCase + +class TestValidationErrors(BaseTestCase): + def setUp(self) -> None: + # Stub usual pre-test setup + pass + + def tearDown(self): + # Delete any cruft that may get generated + self.delete_run_dir() + + def assert_validate_error(self, rdl_file: str, err_regex: str) -> None: + self.rdl_file = rdl_file + f = io.StringIO() + with contextlib.redirect_stderr(f): + with self.assertRaises(RDLCompileError): + self.export_regblock() + stderr = f.getvalue() + self.assertRegex(stderr, err_regex) + + + def test_unaligned_reg(self) -> None: + self.assert_validate_error( + "unaligned_reg.rdl", + "Unaligned registers are not supported. Address offset of instance 'x' must be a multiple of 4", + ) + + def test_unaligned_stride(self) -> None: + self.assert_validate_error( + "unaligned_stride.rdl", + "Unaligned registers are not supported. Address stride of instance array 'x' must be a multiple of 4", + ) + + def test_bad_external_ref(self) -> None: + self.assert_validate_error( + "external_ref.rdl", + "Property is assigned a reference that points to a component not internal to the regblock being exported", + ) + + def test_sharedextbus_not_supported(self) -> None: + self.assert_validate_error( + "sharedextbus.rdl", + "This exporter does not support enabling the 'sharedextbus' property yet", + ) + + def test_inconsistent_accesswidth(self) -> None: + self.assert_validate_error( + "inconsistent_accesswidth.rdl", + r"Multi-word registers that have an accesswidth \(16\) that are inconsistent with this regblock's CPU bus width \(32\) are not supported", + ) + + def test_unbuffered_wide_w_fields(self) -> None: + self.assert_validate_error( + "unbuffered_wide_fields.rdl", + "Software-writable field 'xf' shall not span" + " multiple software-accessible subwords. Consider enabling" + " write double-buffering", + ) + + def test_unbuffered_wide_r_fields(self) -> None: + self.assert_validate_error( + "unbuffered_wide_fields.rdl", + "The field 'yf' spans multiple software-accessible" + " subwords and is modified on-read, making it impossible to" + " access its value correctly. Consider enabling read" + " double-buffering.", + ) + + def test_multiple_unconditional_assigns(self) -> None: + self.assert_validate_error( + "multiple_unconditional_assigns.rdl", + "Field has multiple conflicting properties that unconditionally set its state", + ) + + def test_unsynth_reset1(self) -> None: + self.assert_validate_error( + "unsynth_reset1.rdl", + "A field that uses an asynchronous reset cannot use a dynamic reset value. This is not synthesizable.", + ) + + def test_unsynth_reset2(self) -> None: + self.default_reset_async = True + self.assert_validate_error( + "unsynth_reset2.rdl", + "A field that uses an asynchronous reset cannot use a dynamic reset value. This is not synthesizable.", + ) + + def test_fixedpoint_counter(self) -> None: + self.assert_validate_error( + "fixedpoint_counter.rdl", + "Fixed-point representations are not supported for counter fields.", + ) + + def test_fixedpoint_enum(self) -> None: + self.assert_validate_error( + "fixedpoint_enum.rdl", + "Fixed-point representations are not supported for fields encoded as an enum.", + ) + + def test_fixedpoint_inconsistent_width(self) -> None: + self.assert_validate_error( + "fixedpoint_inconsistent_width.rdl", + r"Number of integer bits \(4\) plus number of fractional bits \(5\) must be equal to the width of the component \(8\).", + ) + + def test_signed_counter(self) -> None: + self.assert_validate_error( + "signed_counter.rdl", + "The property is_signed=true is not supported for counter fields.", + ) + + def test_signed_enum(self) -> None: + self.assert_validate_error( + "signed_enum.rdl", + "The property is_signed=true is not supported for fields encoded as an enum." + ) diff --git a/tests/test_validation_errors/unaligned_reg.rdl b/tests/test_validation_errors/unaligned_reg.rdl new file mode 100644 index 0000000..c30dac2 --- /dev/null +++ b/tests/test_validation_errors/unaligned_reg.rdl @@ -0,0 +1,8 @@ +addrmap top { + default regwidth = 32; + default accesswidth = 32; + + reg { + field {}f; + } x @ 1; +}; diff --git a/tests/test_validation_errors/unaligned_stride.rdl b/tests/test_validation_errors/unaligned_stride.rdl new file mode 100644 index 0000000..da5fb24 --- /dev/null +++ b/tests/test_validation_errors/unaligned_stride.rdl @@ -0,0 +1,8 @@ +addrmap top { + default regwidth = 32; + default accesswidth = 32; + + reg { + field {}f; + } x[4] @ 0 += 5; +}; diff --git a/tests/test_validation_errors/unbuffered_wide_fields.rdl b/tests/test_validation_errors/unbuffered_wide_fields.rdl new file mode 100644 index 0000000..e349a9c --- /dev/null +++ b/tests/test_validation_errors/unbuffered_wide_fields.rdl @@ -0,0 +1,21 @@ +addrmap top { + reg { + regwidth = 64; + accesswidth = 32; + field { + sw=w; + hw=r; + } xf[64]; + } x; + + reg { + regwidth = 64; + accesswidth = 32; + field { + sw=r; + hw=w; + we; + onread=rclr; + } yf[64]; + } y; +}; diff --git a/tests/test_validation_errors/unsynth_reset1.rdl b/tests/test_validation_errors/unsynth_reset1.rdl new file mode 100644 index 0000000..df1e4a4 --- /dev/null +++ b/tests/test_validation_errors/unsynth_reset1.rdl @@ -0,0 +1,19 @@ +signal { + field_reset; + async; + activehigh; +} foo; + +addrmap top { + reg { + field { + sw=rw; hw=na; + } f1; + + field { + sw=rw; hw=na; + } f2; + + f1->reset = f2; + } x; +}; diff --git a/tests/test_validation_errors/unsynth_reset2.rdl b/tests/test_validation_errors/unsynth_reset2.rdl new file mode 100644 index 0000000..9cec0c3 --- /dev/null +++ b/tests/test_validation_errors/unsynth_reset2.rdl @@ -0,0 +1,13 @@ +addrmap top { + reg { + field { + sw=rw; hw=na; + } f1; + + field { + sw=rw; hw=na; + } f2; + + f1->reset = f2; + } x; +}; diff --git a/tests/test_wide_regs/__init__.py b/tests/test_wide_regs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_wide_regs/regblock.rdl b/tests/test_wide_regs/regblock.rdl new file mode 100644 index 0000000..b3d4ca4 --- /dev/null +++ b/tests/test_wide_regs/regblock.rdl @@ -0,0 +1,111 @@ +addrmap top { + + reg { + regwidth = 64; + accesswidth = 16; + default sw=rw; + default hw=r; + + field {} f1[7:0] = 0; + field {} f2[14:12] = 0; + field {} f3[36:36] = 0; + field {} f4[47:40] = 0; + } rw_reg1; + + reg { + regwidth = 64; + accesswidth = 16; + default sw=rw; + default hw=r; + + field {} f1[19:16] = 0; + field {} f2[63:48] = 0; + } rw_reg2; + + reg { + regwidth = 64; + accesswidth = 16; + default sw=rw; + default hw=r; + + field {} f1[0:7] = 0; + field {} f2[12:14] = 0; + field {} f3[36:36] = 0; + field {} f4[40:47] = 0; + } rw_reg1_lsb0; + + reg { + regwidth = 64; + accesswidth = 16; + default sw=rw; + default hw=r; + + field {} f1[16:19] = 0; + field {} f2[48:63] = 0; + } rw_reg2_lsb0; + + reg { + regwidth = 32; + accesswidth = 16; + default sw=r; + default hw=w; + + field { + sw=w; hw=r; + } f0[3:3] = 0; + field {} f1[19:12]; + field {} f2[30:20]; + } r_reg; + + reg { + regwidth = 32; + accesswidth = 16; + default sw=r; + default hw=w; + + field {} f1[12:19]; + field {} f2[20:30]; + } r_reg_lsb0; + + reg { + regwidth = 64; + accesswidth = 16; + default sw=r; + default hw=w; + + field {} f1[31:12]; + field {} f2[49:48]; + } r_reg2; + + reg { + regwidth=16; + field { + sw=r; hw=na; + counter; + } f1_cnt[7:0] = 0; + field { + sw=r; hw=na; + counter; + } f2_cnt[15:8] = 0; + } counter_reg; + counter_reg.f1_cnt->incr = r_reg2.f1->swacc; + counter_reg.f2_cnt->incr = r_reg2.f2->swacc; + + reg { + regwidth = 32; + accesswidth = 16; + default sw=r; + default hw=r; + + field {} f1[31:0] = 0x1234_5678; + } r_reg3; + + reg { + regwidth = 32; + accesswidth = 16; + default sw=r; + default hw=r; + + field {} f1[0:31] = 0x1234_5678; + } r_reg4; +}; diff --git a/tests/test_wide_regs/tb_template.sv b/tests/test_wide_regs/tb_template.sv new file mode 100644 index 0000000..8c261c6 --- /dev/null +++ b/tests/test_wide_regs/tb_template.sv @@ -0,0 +1,121 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + ##1; + cb.rst <= '0; + ##1; + + // rw_reg1 + assert(cb.hwif_out.rw_reg1.f1.value == 0); + assert(cb.hwif_out.rw_reg1.f2.value == 0); + assert(cb.hwif_out.rw_reg1.f3.value == 0); + assert(cb.hwif_out.rw_reg1.f4.value == 0); + cpuif.write('h0, 'h1234); + cpuif.write('h2, 'h5678); + cpuif.write('h4, 'h9ABC); + cpuif.write('h6, 'hDEF1); + @cb; + assert(cb.hwif_out.rw_reg1.f1.value == 8'h34); + assert(cb.hwif_out.rw_reg1.f2.value == 3'h1); + assert(cb.hwif_out.rw_reg1.f3.value == 1'h1); + assert(cb.hwif_out.rw_reg1.f4.value == 8'h9A); + cpuif.assert_read('h0, 'h1034); + cpuif.assert_read('h2, 'h0000); + cpuif.assert_read('h4, 'h9A10); + cpuif.assert_read('h6, 'h0000); + + // rw_reg2 + assert(cb.hwif_out.rw_reg2.f1.value == 0); + assert(cb.hwif_out.rw_reg2.f2.value == 0); + cpuif.write('h8, 'h1234); + cpuif.write('hA, 'h5678); + cpuif.write('hC, 'h9ABC); + cpuif.write('hE, 'hDEF1); + @cb; + assert(cb.hwif_out.rw_reg2.f1.value == 4'h8); + assert(cb.hwif_out.rw_reg2.f2.value == 16'hDEF1); + cpuif.assert_read('h8, 'h0000); + cpuif.assert_read('hA, 'h0008); + cpuif.assert_read('hC, 'h0000); + cpuif.assert_read('hE, 'hDEF1); + + // rw_reg1_lsb0 + assert(cb.hwif_out.rw_reg1_lsb0.f1.value == 0); + assert(cb.hwif_out.rw_reg1_lsb0.f2.value == 0); + assert(cb.hwif_out.rw_reg1_lsb0.f3.value == 0); + assert(cb.hwif_out.rw_reg1_lsb0.f4.value == 0); + cpuif.write('h10, 'h1234); + cpuif.write('h12, 'h5678); + cpuif.write('h14, 'h9ABC); + cpuif.write('h16, 'hDEF1); + @cb; + assert(`bitswap(cb.hwif_out.rw_reg1_lsb0.f1.value) == 8'h34); + assert(`bitswap(cb.hwif_out.rw_reg1_lsb0.f2.value) == 3'h1); + assert(`bitswap(cb.hwif_out.rw_reg1_lsb0.f3.value) == 1'h1); + assert(`bitswap(cb.hwif_out.rw_reg1_lsb0.f4.value) == 8'h9A); + cpuif.assert_read('h10, 'h1034); + cpuif.assert_read('h12, 'h0000); + cpuif.assert_read('h14, 'h9A10); + cpuif.assert_read('h16, 'h0000); + + // rw_reg2_lsb0 + assert(cb.hwif_out.rw_reg2_lsb0.f1.value == 0); + assert(cb.hwif_out.rw_reg2_lsb0.f2.value == 0); + cpuif.write('h18, 'h1234); + cpuif.write('h1A, 'h5678); + cpuif.write('h1C, 'h9ABC); + cpuif.write('h1E, 'hDEF1); + @cb; + assert(`bitswap(cb.hwif_out.rw_reg2_lsb0.f1.value) == 4'h8); + assert(`bitswap(cb.hwif_out.rw_reg2_lsb0.f2.value) == 16'hDEF1); + cpuif.assert_read('h18, 'h0000); + cpuif.assert_read('h1A, 'h0008); + cpuif.assert_read('h1C, 'h0000); + cpuif.assert_read('h1E, 'hDEF1); + + // r_reg + cpuif.assert_read('h20, 0); + cpuif.assert_read('h22, 0); + cb.hwif_in.r_reg.f1.next <= 8'hAB; + cb.hwif_in.r_reg.f2.next <= 11'h4DE; + @cb; + cpuif.assert_read('h20, 'hB000); + cpuif.assert_read('h22, 'h4DEA); + + // r_reg_lsb0 + cpuif.assert_read('h24, 0); + cpuif.assert_read('h26, 0); + cb.hwif_in.r_reg_lsb0.f1.next <= {<<{8'hAB}}; + cb.hwif_in.r_reg_lsb0.f2.next <= {<<{11'h4DE}}; + @cb; + cpuif.assert_read('h24, 'hB000); + cpuif.assert_read('h26, 'h4DEA); + + // r_reg2 + cpuif.assert_read('h28, 0); + cpuif.assert_read('h2a, 0); + cpuif.assert_read('h2c, 0); + cpuif.assert_read('h2e, 0); + cb.hwif_in.r_reg2.f1.next <= 20'hABCDE; + cb.hwif_in.r_reg2.f2.next <= 2'h3; + @cb; + cpuif.assert_read('h28, 'hE000); + cpuif.assert_read('h2a, 'hABCD); + cpuif.assert_read('h2c, 'h0000); + cpuif.assert_read('h2e, 'h0003); + + // counter_reg + cpuif.assert_read('h30, 16'h0204); + + // r_reg3 + cpuif.assert_read('h34, 16'h5678); + cpuif.assert_read('h36, 16'h1234); + assert(cb.hwif_out.r_reg3.f1.value == 32'h12345678); + + // r_reg4 + cpuif.assert_read('h38, 16'h2C48); + cpuif.assert_read('h3A, 16'h1E6A); + assert(cb.hwif_out.r_reg4.f1.value == 32'h12345678); + +{% endblock %} diff --git a/tests/test_wide_regs/testcase.py b/tests/test_wide_regs/testcase.py new file mode 100644 index 0000000..835b5ef --- /dev/null +++ b/tests/test_wide_regs/testcase.py @@ -0,0 +1,5 @@ +from ..lib.sim_testcase import SimTestCase + +class Test(SimTestCase): + def test_dut(self): + self.run_test() diff --git a/tests/test_write_buffer/__init__.py b/tests/test_write_buffer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_write_buffer/regblock.rdl b/tests/test_write_buffer/regblock.rdl new file mode 100644 index 0000000..ca267bc --- /dev/null +++ b/tests/test_write_buffer/regblock.rdl @@ -0,0 +1,106 @@ +addrmap top { + default regwidth = 16; + default accesswidth = 16; + default sw=rw; + default hw=r; + //-------------------------------------------------------------------------- + // Wide registers + //-------------------------------------------------------------------------- + reg { + regwidth = 64; + buffer_writes = true; + field {} f1[63:0] = 0; + } reg1; + + reg { + regwidth = 64; + buffer_writes = true; + field {} f1[0:63] = 0; + } reg1_msb0; + + reg { + regwidth = 32; + buffer_writes = true; + field {} f1[19:8] = 0; + field {} f2[23:20] = 0; + } reg2; + + reg { + regwidth = 32; + buffer_writes = true; + field {} f1[8:19] = 0; + field {} f2[20:23] = 0; + } reg2_msb0; + + //-------------------------------------------------------------------------- + // Alternate Triggers + //-------------------------------------------------------------------------- + reg myreg { + buffer_writes; + field {} f1[15:0] = 0; + }; + + // Trigger via another register + myreg g1_r1; + myreg g1_r2; + g1_r1->buffer_writes = false; + g1_r2->wbuffer_trigger = g1_r1; + + // triger from signal + signal { + activehigh; + } trigger_sig; + signal { + activelow; + } trigger_sig_n; + myreg g2_r1; + myreg g2_r2; + g2_r1->wbuffer_trigger = trigger_sig; + g2_r2->wbuffer_trigger = trigger_sig_n; + + // trigger from field + myreg g3_r1; + reg { + field { + sw=w; hw=r; singlepulse; + } trig = 0; + } g3_trig; + g3_r1->wbuffer_trigger = g3_trig.trig; + + // trigger from propref + myreg g4_r1; + reg { + field { + hw=na; + } trig_vec[3:0] = 0; + } g4_trig; + g4_r1->wbuffer_trigger = g4_trig.trig_vec->anded; + + //-------------------------------------------------------------------------- + // swmod behavior + //-------------------------------------------------------------------------- + myreg g5_r1; + g5_r1->wbuffer_trigger = trigger_sig; + reg { + field{ + sw=rw; + hw=na; + counter; + } c[3:0] = 0; + } g5_modcount; + g5_modcount.c->incr = g5_r1.f1->swmod; + + myreg g6_r1; + g6_r1.f1->rclr; + g6_r1->wbuffer_trigger = trigger_sig; + reg { + field{ + sw=rw; + hw=na; + counter; + } c[3:0] = 0; + } g6_modcount; + g6_modcount.c->incr = g6_r1.f1->swmod; + + +}; diff --git a/tests/test_write_buffer/tb_template.sv b/tests/test_write_buffer/tb_template.sv new file mode 100644 index 0000000..4c281eb --- /dev/null +++ b/tests/test_write_buffer/tb_template.sv @@ -0,0 +1,303 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + cb.hwif_in.trigger_sig_n <= '1; + ##1; + cb.rst <= '0; + ##1; + + //-------------------------------------------------------------------------- + // Wide registers + //-------------------------------------------------------------------------- + + // reg1 + cpuif.assert_read('h0, 'h0); + cpuif.assert_read('h2, 'h0); + cpuif.assert_read('h4, 'h0); + cpuif.assert_read('h6, 'h0); + assert(cb.hwif_out.reg1.f1.value == 0); + cpuif.write('h0, 'h1234); + cpuif.assert_read('h0, 'h0); + cpuif.assert_read('h2, 'h0); + cpuif.assert_read('h4, 'h0); + cpuif.assert_read('h6, 'h0); + assert(cb.hwif_out.reg1.f1.value == 0); + cpuif.write('h2, 'h5678); + cpuif.assert_read('h0, 'h0); + cpuif.assert_read('h2, 'h0); + cpuif.assert_read('h4, 'h0); + cpuif.assert_read('h6, 'h0); + assert(cb.hwif_out.reg1.f1.value == 0); + cpuif.write('h4, 'h9ABC); + cpuif.assert_read('h0, 'h0); + cpuif.assert_read('h2, 'h0); + cpuif.assert_read('h4, 'h0); + cpuif.assert_read('h6, 'h0); + assert(cb.hwif_out.reg1.f1.value == 0); + cpuif.write('h6, 'hDEF1); + @cb; @cb; + assert(cb.hwif_out.reg1.f1.value == 64'hDEF19ABC56781234); + cpuif.assert_read('h0, 'h1234); + cpuif.assert_read('h2, 'h5678); + cpuif.assert_read('h4, 'h9ABC); + cpuif.assert_read('h6, 'hDEF1); + + // reg1_msb0 + cpuif.assert_read('h8, 'h0); + cpuif.assert_read('hA, 'h0); + cpuif.assert_read('hC, 'h0); + cpuif.assert_read('hE, 'h0); + assert(cb.hwif_out.reg1_msb0.f1.value == 0); + cpuif.write('h8, 'h1234); + cpuif.assert_read('h8, 'h0); + cpuif.assert_read('hA, 'h0); + cpuif.assert_read('hC, 'h0); + cpuif.assert_read('hE, 'h0); + assert(cb.hwif_out.reg1_msb0.f1.value == 0); + cpuif.write('hA, 'h5678); + cpuif.assert_read('h8, 'h0); + cpuif.assert_read('hA, 'h0); + cpuif.assert_read('hC, 'h0); + cpuif.assert_read('hE, 'h0); + assert(cb.hwif_out.reg1_msb0.f1.value == 0); + cpuif.write('hC, 'h9ABC); + cpuif.assert_read('h8, 'h0); + cpuif.assert_read('hA, 'h0); + cpuif.assert_read('hC, 'h0); + cpuif.assert_read('hE, 'h0); + assert(cb.hwif_out.reg1_msb0.f1.value == 0); + cpuif.write('hE, 'hDEF1); + @cb; @cb; + assert(`bitswap(cb.hwif_out.reg1_msb0.f1.value) == 64'hDEF19ABC56781234); + cpuif.assert_read('h8, 'h1234); + cpuif.assert_read('hA, 'h5678); + cpuif.assert_read('hC, 'h9ABC); + cpuif.assert_read('hE, 'hDEF1); + + // reg2 + cpuif.assert_read('h10, 'h0); + cpuif.assert_read('h12, 'h0); + assert(cb.hwif_out.reg2.f1.value == 0); + assert(cb.hwif_out.reg2.f2.value == 0); + cpuif.write('h10, 'h34AA); + cpuif.assert_read('h10, 'h0); + cpuif.assert_read('h12, 'h0); + assert(cb.hwif_out.reg2.f1.value == 0); + assert(cb.hwif_out.reg2.f2.value == 0); + cpuif.write('h12, 'hAA12); + @cb; @cb; + assert(cb.hwif_out.reg2.f1.value == 12'h234); + assert(cb.hwif_out.reg2.f2.value == 4'h1); + cpuif.assert_read('h10, 'h3400); + cpuif.assert_read('h12, 'h0012); + + // reg2_msb0 + cpuif.assert_read('h14, 'h0); + cpuif.assert_read('h16, 'h0); + assert(cb.hwif_out.reg2_msb0.f1.value == 0); + assert(cb.hwif_out.reg2_msb0.f2.value == 0); + cpuif.write('h14, 'h34AA); + cpuif.assert_read('h14, 'h0); + cpuif.assert_read('h16, 'h0); + assert(cb.hwif_out.reg2_msb0.f1.value == 0); + assert(cb.hwif_out.reg2_msb0.f2.value == 0); + cpuif.write('h16, 'hAA12); + @cb; @cb; + assert(`bitswap(cb.hwif_out.reg2_msb0.f1.value) == 12'h234); + assert(`bitswap(cb.hwif_out.reg2_msb0.f2.value) == 4'h1); + cpuif.assert_read('h14, 'h3400); + cpuif.assert_read('h16, 'h0012); + + //-------------------------------------------------------------------------- + // Alternate Triggers + //-------------------------------------------------------------------------- + + // g1 + cpuif.assert_read('h18, 'h0); + cpuif.assert_read('h1A, 'h0); + assert(cb.hwif_out.g1_r1.f1.value == 0); + assert(cb.hwif_out.g1_r2.f1.value == 0); + cpuif.write('h1A, 'h1234); + cpuif.assert_read('h18, 'h0); + cpuif.assert_read('h1A, 'h0); + assert(cb.hwif_out.g1_r1.f1.value == 0); + assert(cb.hwif_out.g1_r2.f1.value == 0); + cpuif.write('h18, 'hABCD); + @cb; + assert(cb.hwif_out.g1_r1.f1.value == 'hABCD); + assert(cb.hwif_out.g1_r2.f1.value == 'h1234); + + // g2 + cpuif.assert_read('h1C, 'h0); + cpuif.assert_read('h1E, 'h0); + assert(cb.hwif_out.g2_r1.f1.value == 0); + assert(cb.hwif_out.g2_r2.f1.value == 0); + cpuif.write('h1C, 'h5678); + cpuif.write('h1E, 'h9876); + cpuif.assert_read('h1C, 'h0); + cpuif.assert_read('h1E, 'h0); + assert(cb.hwif_out.g2_r1.f1.value == 0); + assert(cb.hwif_out.g2_r2.f1.value == 0); + cb.hwif_in.trigger_sig <= '1; + cb.hwif_in.trigger_sig_n <= '0; + @cb; + cb.hwif_in.trigger_sig <= '0; + cb.hwif_in.trigger_sig_n <= '1; + @cb; + assert(cb.hwif_out.g2_r1.f1.value == 'h5678); + assert(cb.hwif_out.g2_r2.f1.value == 'h9876); + + // g3 + cpuif.assert_read('h20, 'h0); + assert(cb.hwif_out.g3_r1.f1.value == 0); + cpuif.write('h20, 'hFEDC); + @cb; @cb; + assert(cb.hwif_out.g3_r1.f1.value == 0); + cpuif.assert_read('h20, 'h0); + cpuif.write('h22, 'h0000); + @cb; @cb; + assert(cb.hwif_out.g3_r1.f1.value == 0); + cpuif.assert_read('h20, 'h0); + cpuif.write('h22, 'h0001); + @cb; @cb; + assert(cb.hwif_out.g3_r1.f1.value == 'hFEDC); + cpuif.assert_read('h20, 'hFEDC); + + // g4 + cpuif.assert_read('h24, 'h0); + assert(cb.hwif_out.g4_r1.f1.value == 0); + cpuif.write('h24, 'hCAFE); + @cb; @cb; + assert(cb.hwif_out.g4_r1.f1.value == 0); + cpuif.assert_read('h24, 'h0); + cpuif.write('h26, 'h0000); + @cb; @cb; + assert(cb.hwif_out.g4_r1.f1.value == 0); + cpuif.assert_read('h24, 'h0); + cpuif.write('h26, 'h000E); + @cb; @cb; + assert(cb.hwif_out.g4_r1.f1.value == 0); + cpuif.assert_read('h24, 'h0); + cpuif.write('h26, 'h000F); + @cb; @cb; + assert(cb.hwif_out.g4_r1.f1.value == 'hCAFE); + cpuif.assert_read('h24, 'hCAFE); + + //-------------------------------------------------------------------------- + // swmod behavior + //-------------------------------------------------------------------------- + // g5 + cpuif.assert_read('h28, 'h0); + cpuif.assert_read('h2A, 'h0); + cpuif.write('h28, 'h1234); + cpuif.write('h28, 'h5678); + cpuif.assert_read('h28, 'h0); + cpuif.assert_read('h2A, 'h0); + cb.hwif_in.trigger_sig <= '1; + @cb; + cb.hwif_in.trigger_sig <= '0; + cpuif.assert_read('h28, 'h5678); + cpuif.assert_read('h2A, 'h1); + + // g6 + cpuif.assert_read('h2E, 'h0); + cpuif.assert_read('h2C, 'h0); + cpuif.assert_read('h2E, 'h1); + cpuif.write('h2C, 'h5678); + cpuif.write('h2C, 'h1234); + cpuif.assert_read('h2E, 'h1); + cpuif.assert_read('h2C, 'h0); + cpuif.assert_read('h2E, 'h2); + cb.hwif_in.trigger_sig <= '1; + @cb; + cb.hwif_in.trigger_sig <= '0; + cpuif.assert_read('h2E, 'h3); + cpuif.assert_read('h2C, 'h1234); + cpuif.assert_read('h2E, 'h4); + + //-------------------------------------------------------------------------- + // strobes + //-------------------------------------------------------------------------- + // reg1 + // reset field to known state + cpuif.write('h0, 'h0000); + cpuif.write('h2, 'h0000); + cpuif.write('h4, 'h0000); + cpuif.write('h6, 'h0000); + @cb; + cpuif.assert_read('h0, 'h0); + cpuif.assert_read('h2, 'h0); + cpuif.assert_read('h4, 'h0); + cpuif.assert_read('h6, 'h0); + assert(cb.hwif_out.reg1.f1.value == 0); + + cpuif.write('h0, 'hABCD, 'hF000); + cpuif.write('h2, 'h1234, 'h0F00); + cpuif.write('h4, 'h5678, 'h00F0); + cpuif.write('h6, 'hEF12, 'h000F); + @cb; + cpuif.assert_read('h0, 'hA000); + cpuif.assert_read('h2, 'h0200); + cpuif.assert_read('h4, 'h0070); + cpuif.assert_read('h6, 'h0002); + assert(cb.hwif_out.reg1.f1.value == 'h0002_0070_0200_A000); + + // Check that strobes are cumulative + cpuif.write('h0, 'h0030, 'h00F0); + cpuif.write('h2, 'h0070, 'h00F0); + cpuif.write('h4, 'h000D, 'h000F); + cpuif.write('h4, 'hA000, 'hF000); + cpuif.write('h2, 'h0008, 'h000F); + cpuif.write('h0, 'h0200, 'h0F00); + cpuif.write('h6, 'hA000, 'hF000); + cpuif.write('h6, 'h0F00, 'h0F00); + @cb; + cpuif.assert_read('h0, 'hA230); + cpuif.assert_read('h2, 'h0278); + cpuif.assert_read('h4, 'hA07D); + cpuif.assert_read('h6, 'hAF02); + assert(cb.hwif_out.reg1.f1.value == 'hAF02_A07D_0278_A230); + + // reg1_msb0 + // reset field to known state + cpuif.write('h8, 'h0000); + cpuif.write('hA, 'h0000); + cpuif.write('hC, 'h0000); + cpuif.write('hE, 'h0000); + @cb; + cpuif.assert_read('h8, 'h0); + cpuif.assert_read('hA, 'h0); + cpuif.assert_read('hC, 'h0); + cpuif.assert_read('hE, 'h0); + assert(cb.hwif_out.reg1_msb0.f1.value == 0); + + cpuif.write('h8, 'hABCD, 'hF000); + cpuif.write('hA, 'h1234, 'h0F00); + cpuif.write('hC, 'h5678, 'h00F0); + cpuif.write('hE, 'hEF12, 'h000F); + @cb; + cpuif.assert_read('h8, 'hA000); + cpuif.assert_read('hA, 'h0200); + cpuif.assert_read('hC, 'h0070); + cpuif.assert_read('hE, 'h0002); + assert(`bitswap(cb.hwif_out.reg1_msb0.f1.value) == 'h0002_0070_0200_A000); + + // Check that strobes are cumulative + cpuif.write('h8, 'h0030, 'h00F0); + cpuif.write('hA, 'h0070, 'h00F0); + cpuif.write('hC, 'h000D, 'h000F); + cpuif.write('hC, 'hA000, 'hF000); + cpuif.write('hA, 'h0008, 'h000F); + cpuif.write('h8, 'h0200, 'h0F00); + cpuif.write('hE, 'hA000, 'hF000); + cpuif.write('hE, 'h0F00, 'h0F00); + @cb; + cpuif.assert_read('h8, 'hA230); + cpuif.assert_read('hA, 'h0278); + cpuif.assert_read('hC, 'hA07D); + cpuif.assert_read('hE, 'hAF02); + assert(`bitswap(cb.hwif_out.reg1_msb0.f1.value) == 'hAF02_A07D_0278_A230); + + +{% endblock %} diff --git a/tests/test_write_buffer/testcase.py b/tests/test_write_buffer/testcase.py new file mode 100644 index 0000000..4c1be35 --- /dev/null +++ b/tests/test_write_buffer/testcase.py @@ -0,0 +1,8 @@ +from ..lib.sim_testcase import SimTestCase +from ..lib.cpuifs.passthrough import Passthrough + +class Test(SimTestCase): + cpuif = Passthrough() # test with bit strobes + + def test_dut(self): + self.run_test() diff --git a/tests/test_write_strobes/__init__.py b/tests/test_write_strobes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_write_strobes/regblock.rdl b/tests/test_write_strobes/regblock.rdl new file mode 100644 index 0000000..bfa31bd --- /dev/null +++ b/tests/test_write_strobes/regblock.rdl @@ -0,0 +1,54 @@ +addrmap top { + + reg { + field { + sw=rw; hw=na; + onwrite = woset; + } f1[3:0] = 0x0; + + field { + sw=rw; hw=na; + onwrite = woclr; + } f2[7:4] = 0xF; + + field { + sw=rw; hw=na; + onwrite = wot; + } f3[11:8] = 0x0; + } r1; + + reg { + field { + sw=rw; hw=na; + onwrite = wzs; + } f1[3:0] = 0x0; + + field { + sw=rw; hw=na; + onwrite = wzc; + } f2[7:4] = 0xF; + + field { + sw=rw; hw=na; + onwrite = wzt; + } f3[11:8] = 0x0; + } r2; + + reg { + field { + sw=rw; hw=na; + onwrite = wclr; + } f1[7:0] = 0xF0; + + field { + sw=rw; hw=na; + onwrite = wset; + } f2[15:8] = 0x0F; + } r3; + + reg { + field { + sw=rw; hw=na; + } f3[7:0] = 0x00; + } r4; +}; diff --git a/tests/test_write_strobes/tb_template.sv b/tests/test_write_strobes/tb_template.sv new file mode 100644 index 0000000..12e8e57 --- /dev/null +++ b/tests/test_write_strobes/tb_template.sv @@ -0,0 +1,31 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + ##1; + cb.rst <= '0; + ##1; + + cpuif.assert_read('h0, 'h0_F_0); + cpuif.write ('h0, 'h5_5_5, 'h3_3_3); + cpuif.assert_read('h0, 'h1_E_1); + cpuif.write ('h0, 'h5_A_A, 'h3_3_3); + cpuif.assert_read('h0, 'h0_C_3); + + cpuif.assert_read('h4, 'h0_F_0); + cpuif.write ('h4, 'hA_A_A, 'h3_3_3); + cpuif.assert_read('h4, 'h1_E_1); + cpuif.write ('h4, 'hA_5_5, 'h3_3_3); + cpuif.assert_read('h4, 'h0_C_3); + + cpuif.assert_read('h8, 'h0F_F0); + cpuif.write ('h8, 'h12_34, 'hFF_00); + cpuif.assert_read('h8, 'hFF_00); + + cpuif.assert_read('hC, 'h00); + cpuif.write ('hC, 'hFF, 'hF0); + cpuif.assert_read('hC, 'hF0); + cpuif.write ('hC, 'h00, 'h3C); + cpuif.assert_read('hC, 'hC0); + +{% endblock %} diff --git a/tests/test_write_strobes/testcase.py b/tests/test_write_strobes/testcase.py new file mode 100644 index 0000000..b4c56ed --- /dev/null +++ b/tests/test_write_strobes/testcase.py @@ -0,0 +1,9 @@ +from ..lib.sim_testcase import SimTestCase + +from ..lib.cpuifs.passthrough import Passthrough + +class Test(SimTestCase): + cpuif = Passthrough() + + def test_dut(self): + self.run_test()