diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..74901050917c7aa4fc8ae54d8f032961dc9c5f4e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,164 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + hdl: + name: RTL lint and simulation + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Install simulators + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends iverilog verilator + - name: Compact accelerator + run: make lint sim + - name: Compact accelerator, extra random seeds + run: for s in 2 3 4; do make sim-icarus SEED=$s OPS=3000; done + - name: bit_accelerator v2 + run: make -C bit_accelerator lint sim + + formal: + name: Why3 proofs + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Install Why3 and Z3 + run: | + sudo apt-get update && sudo apt-get install -y --no-install-recommends why3 z3 + why3 config detect + - name: Compact accelerator proofs + run: make formal + - name: bit_accelerator v2 proofs + run: make -C bit_accelerator formal + + spice: + name: SPICE timing model + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends ngspice + - run: make spice + + gpu: + name: Rust OpenCL crate (gpu/) + runs-on: ubuntu-24.04 + defaults: + run: + working-directory: gpu + steps: + - uses: actions/checkout@v4 + - name: Install OpenCL (PoCL CPU device) + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends pocl-opencl-icd ocl-icd-opencl-dev clinfo + clinfo -l + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2 + with: + workspaces: gpu + - run: cargo fmt --check + - run: cargo clippy --all-targets -- -D warnings + # Serial: PoCL 5.0 can abort (pocl_release_dlhandle_cache assertion) when + # several OpenCL contexts are released from different threads at once. + - run: cargo test + env: + RUST_TEST_THREADS: "1" + - run: cargo run --quiet + + fpga: + name: FPGA build (lint, UART testbench, host co-simulation, bitstreams) + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Install FPGA toolchain + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends iverilog verilator yosys \ + nextpnr-ice40 nextpnr-ecp5 fpga-icestorm fpga-trellis + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2 + with: + workspaces: fpga/host + - run: make -C fpga lint sim host cosim icebreaker ulx3s + - uses: actions/upload-artifact@v4 + with: + name: bitstreams + path: | + fpga/build/icebreaker.bin + fpga/build/ulx3s.bit + fpga/build/*_pnr.log + + fpga-gate-level: + name: FPGA gate-level simulation (${{ matrix.arch }}) + runs-on: ubuntu-24.04 + strategy: + matrix: + arch: [ice40, ecp5] + steps: + - uses: actions/checkout@v4 + - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends iverilog yosys + - run: make -C fpga gl-sim-${{ matrix.arch }} + + asic: + name: sky130 synthesis and equivalence + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends yosys + - run: make -C asic all + - uses: actions/upload-artifact@v4 + with: + name: sky130-netlist + path: | + asic/build/bit_accelerator_sky130.v + asic/build/area.rpt + + pcie: + name: PCIe engine (lint, equivalence vs reference core) + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends iverilog verilator + # The simulation runs are independent make targets; the largest geometry takes ~6 minutes. + - run: make -C pcie -j$(nproc) lint sim sim-verilator + + pcie-cosim: + name: PCIe SoC generation + host co-simulation + throughput + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends verilator + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install LiteX + run: | + for r in m-labs/migen enjoy-digital/litex litex-hub/litex-boards enjoy-digital/litepcie \ + enjoy-digital/litedram enjoy-digital/liteeth enjoy-digital/litescope; do + git clone --depth 1 https://github.com/$r /tmp/$(basename $r) + pip install -e /tmp/$(basename $r) + done + - run: make -C pcie soc cosim perf + + tapeout: + name: Tiny Tapeout project (sync, RTL test) + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends iverilog + - run: make -C tapeout check-sync test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..b0bf6851ab016e4798b739be78996394d01df7a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +target/ +build/ +obj_dir/ +simv +*.vcd diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..e48e0963459bf4c4e175956b657a8f4a91967880 --- /dev/null +++ b/LICENSE @@ -0,0 +1,277 @@ +Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + +"Contributor" means any person or entity that Distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions Distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement +or any Secondary License (as applicable), including Contributors. + +"Derivative Works" shall mean any work, whether in Source Code or other +form, that is based on (or derived from) the Program and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. + +"Modified Works" shall mean any work in Source Code or other form that +results from an addition to, deletion from, or modification of the +contents of the Program, including, for purposes of clarity any new file +in Source Code form that contains any contents of the Program. Modified +Works shall not include works that contain only declarations, +interfaces, types, classes, structures, or files of the Program solely +in each case in order to link to, bind by name, or subclass the Program +or Modified Works thereof. + +"Distribute" means the acts of a) distributing or b) making available +in any manner that enables the transfer of a copy. + +"Source Code" means the form of a Program preferred for making +modifications, including but not limited to software source code, +documentation source, and configuration files. + +"Secondary License" means either the GNU General Public License, +Version 2.0, or any later versions of that license, including any +exceptions or additional permissions as identified by the initial +Contributor. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + +3. REQUIREMENTS + +3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + +3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + +3.3 Contributors may not remove or alter any copyright, patent, +trademark, attribution notices, disclaimers of warranty, or limitations +of liability ("notices") contained within the Program from any copy of +the Program which they Distribute, provided that Contributors may add +their own appropriate notices. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, +the Contributor who includes the Program in a commercial product +offering should do so in a manner which does not create potential +liability for other Contributors. Therefore, if a Contributor includes +the Program in a commercial product offering, such Contributor +("Commercial Contributor") hereby agrees to defend and indemnify every +other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits +and other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the Program +in a commercial product offering. The obligations in this section do not +apply to any claims or Losses relating to any actual or alleged +intellectual property infringement. In order to qualify, an Indemnified +Contributor must: a) promptly notify the Commercial Contributor in +writing of such claim, and b) allow the Commercial Contributor to control, +and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those performance +claims and warranties, and if a court requires any other Contributor to +pay any damages as a result, the Commercial Contributor must pay +those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF +TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR +PURPOSE. Each Recipient is solely responsible for determining the +appropriateness of using and distributing the Program and assumes all +risks associated with its exercise of rights under this Agreement, +including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs +or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS +SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further +action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other software +or hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it +fails to comply with any of the material terms or conditions of this +Agreement and does not cure such failure in a reasonable period of +time after becoming aware of such noncompliance. If all Recipient's +rights under this Agreement terminate, Recipient agrees to cease use +and distribution of the Program as soon as reasonably practicable. +However, Recipient's obligations under this Agreement and any licenses +granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and +may only be modified in the following manner. The Agreement Steward +reserves the right to publish new versions (including revisions) of +this Agreement from time to time. No one other than the Agreement +Steward has the right to modify this Agreement. The Eclipse Foundation +is the initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +Distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is published, +Contributor may elect to Distribute the Program (including its +Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient +receives no rights or licenses to the intellectual property of any +Contributor under this Agreement, whether expressly, by implication, +estoppel or otherwise. All rights in the Program not expressly granted +under this Agreement are reserved. Nothing in this Agreement is intended +to be enforceable by any entity that is not a Contributor or Recipient. +No third-party beneficiary rights are created under this Agreement. + +Exhibit A - Form of Secondary Licenses Notice + +"This Source Code may also be made available under the following +Secondary Licenses when the conditions for such availability set forth +in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), +version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..9358d09f37061edff9fae0107702c553f6ad6862 --- /dev/null +++ b/Makefile @@ -0,0 +1,91 @@ +# bit-string-accelerator: build and verification entry points. +# +# make test everything below (what CI runs) +# make lint verilator -Wall on rtl/bit_accelerator.sv +# make sim self-checking testbench under Icarus and Verilator +# make formal Why3 proofs in formal/ (every goal must be proved by Z3) +# make spice ngspice timing model, fails if the delay is not measured +# make accel the bit_accelerator/ v2 design (lint, sim, formal) +# make gpu the Rust OpenCL crate in gpu/ (fmt, clippy, tests) +# make fpga FPGA wrapper: lint, UART testbench, host co-simulation, bitstreams +# make fpga-gl FPGA wrapper on post-synthesis netlists (slow, ~10 min) +# make asic sky130 synthesis + proof that the netlist equals the RTL +# make pcie parallel engine + PCIe core: lint, equivalence vs the reference core +# make tapeout Tiny Tapeout project: sources in sync, RTL test at both divisors + +IVERILOG ?= iverilog +VVP ?= vvp +VERILATOR ?= verilator +NGSPICE ?= ngspice +WHY3_PROVER ?= z3 +WHY3_TIME ?= 30 +BUILD ?= build +SEED ?= 1 +OPS ?= 2000 + +RTL := rtl/bit_accelerator.sv +TB := verification/tb_bit_accelerator.sv +FORMAL := formal/bit_address.mlw formal/bit_extract.mlw formal/lane_map.mlw +PROVE := bit_accelerator/scripts/prove.sh + +.PHONY: all test lint sim sim-icarus sim-verilator formal spice accel gpu fpga fpga-gl asic pcie tapeout clean +.DEFAULT_GOAL := test + +all test: lint sim formal spice accel gpu fpga asic pcie tapeout + +lint: + $(VERILATOR) --lint-only -Wall $(RTL) + +sim: sim-icarus sim-verilator + +sim-icarus: + mkdir -p $(BUILD) + $(IVERILOG) -g2012 -o $(BUILD)/tb.vvp $(RTL) $(TB) + $(VVP) -n $(BUILD)/tb.vvp +seed=$(SEED) +ops=$(OPS) + +sim-verilator: + mkdir -p $(BUILD)/verilator + $(VERILATOR) --binary --timing --timescale 1ns/1ps -Wno-fatal -Wno-lint --top-module tb_bit_accelerator \ + -Mdir $(BUILD)/verilator $(RTL) $(TB) >/dev/null + $(BUILD)/verilator/Vtb_bit_accelerator +seed=$(SEED) +ops=$(OPS) + +formal: + $(PROVE) -P $(WHY3_PROVER) -t $(WHY3_TIME) $(FORMAL) + +spice: + mkdir -p $(BUILD) + $(NGSPICE) -b spice/bit_extract.sp > $(BUILD)/spice.log 2>&1 + @grep -E '^tpd *= *[0-9.e+-]+' $(BUILD)/spice.log || { cat $(BUILD)/spice.log; echo "tpd not measured"; exit 1; } + +accel: + $(MAKE) -C bit_accelerator test + +gpu: + cd gpu && cargo fmt --check + cd gpu && cargo clippy --all-targets -- -D warnings + # Serial: PoCL 5.0 can abort when several contexts are released concurrently. + cd gpu && RUST_TEST_THREADS=1 cargo test + +fpga: + $(MAKE) -C fpga lint sim cosim icebreaker ulx3s + +fpga-gl: + $(MAKE) -C fpga gl-sim + +asic: + $(MAKE) -C asic all + +pcie: + $(MAKE) -C pcie lint sim sim-verilator + +tapeout: + $(MAKE) -C tapeout check-sync test + +clean: + rm -rf $(BUILD) simv + $(MAKE) -C bit_accelerator clean + $(MAKE) -C fpga clean + $(MAKE) -C asic clean + $(MAKE) -C pcie clean + $(MAKE) -C tapeout clean + cd gpu && cargo clean diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e70ab52f993339f976899c90342b77c1896079ba --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +--- +license: epl-2.0 +tags: +- snapkitty +- october-2026-drop +- systemverilog +- rtl +- fpga +- asic +- formal-verification +--- + +> Mirrored from [https://github.com/SNAPKITTYAGENT9NOVA/bit-string-accelerator](https://github.com/SNAPKITTYAGENT9NOVA/bit-string-accelerator) at commit `d2b5cfe`. Part of the **SnapKitty October 2026 main drop**. + +# bit-string-accelerator + +[![CI](https://github.com/SNAPKITTYAGENT9NOVA/bit-string-accelerator/actions/workflows/ci.yml/badge.svg)](https://github.com/SNAPKITTYAGENT9NOVA/bit-string-accelerator/actions/workflows/ci.yml) + +Synthesizable SystemVerilog bit-string accelerator for deterministic 64-bit-word bit addressing. + + effective_bit_address = (base_address * 8) + bit_offset (mod 2^64) + word_address = (effective_bit_address / 64) * 8 + bit_index = effective_bit_address mod 64 + +Operations: GET, TEST, SET, CLEAR, TOGGLE. Interface contract and verification +evidence: [`docs/verification.md`](docs/verification.md). + +## Layout + +| Path | Contents | +|------|----------| +| `rtl/`, `verification/`, `formal/`, `isa/`, `spice/`, `docs/` | Compact accelerator, its self-checking testbench, Why3 proofs, ISA and timing model | +| `bit_accelerator/` | Multi-stage v2 accelerator with its own testbench and proofs (also in `rust-opencl-gpu`) | +| `fpga/` | Hardware build: UART + block-RAM wrapper, iCEBreaker and ULX3S bitstreams, host tool `bitacc` ([`fpga/README.md`](fpga/README.md)) | +| `asic/` | sky130 standard-cell synthesis, equivalence proof, timing ([`asic/README.md`](asic/README.md)) | +| `pcie/` | Parallel engine with range operations (count, find, fill, bulk logic, pattern match) + PCIe core for the LiteFury, LiteX SoC, host program, open-source timing flow ([`pcie/README.md`](pcie/README.md)) | +| `tapeout/` | Tiny Tapeout (ttsky26d, sky130A) project, hardened and prechecked ([`tapeout/README.md`](tapeout/README.md), [`tapeout/SIGNOFF.md`](tapeout/SIGNOFF.md)) | +| `gpu/` | Rust/OpenCL (`ocl`) crate from `rust-opencl-gpu` | +| `.github/workflows/ci.yml` | CI: lint, simulation, proofs, SPICE, Rust | + +## Requirements + +Ubuntu 24.04: + +```sh +sudo apt-get install iverilog verilator why3 z3 ngspice pocl-opencl-icd ocl-icd-opencl-dev \ + yosys nextpnr-ice40 nextpnr-ecp5 fpga-icestorm fpga-trellis +why3 config detect +``` + +plus a Rust toolchain (1.85 or newer) for `gpu/`. `pocl-opencl-icd` provides a +CPU OpenCL device; any other OpenCL driver works too. + +## Build and test + +```sh +make test # everything CI runs +make lint # verilator -Wall +make sim # testbench under Icarus and Verilator (SEED=n OPS=n to vary) +make formal # Why3 proofs, every goal must be proved by Z3 +make spice # ngspice timing model +make accel # bit_accelerator/ v2: lint, sim, proofs +make gpu # gpu/: cargo fmt, clippy, tests +make fpga # FPGA wrapper: lint, testbench, host co-simulation, both bitstreams +make fpga-gl # FPGA wrapper on post-synthesis netlists (slow) +make asic # sky130 synthesis + RTL/netlist equivalence proof +make pcie # parallel engine + PCIe core vs the reference core (make -j helps) +make tapeout # Tiny Tapeout project: RTL test at both divisor settings +``` + +## Roadmap + +Single-op core (reference, proofs) → parallel engine (`pcie/`, identical +results) → DMA work queue → range operations (one descriptor, up to the whole +bit store) → LiteFury PCIe prototype → custom PCIe board, with the Tiny +Tapeout chip (`tapeout/`) as a separate silicon proof of the core. +The PCIe design does not depend on the transport, so the same card works in +the NUC's M.2 slot or in a USB4/Thunderbolt enclosure. + +## Running on hardware + +`make -C fpga icebreaker prog-icebreaker` (or `ulx3s prog-ulx3s`) builds and +loads a bitstream. Then `bitacc --port /dev/ttyUSB1 selftest` runs the on-board +acceptance test. See [`fpga/README.md`](fpga/README.md). + +| Target | Size | Speed | +|---|---|---| +| iCE40UP5K (iCEBreaker), core + UART + RAM | 1138 LC (21%), 4 BRAM | 39.06 MHz (12 MHz board clock) | +| ECP5 25F (ULX3S), core + UART + RAM | 1171 LUT4 (4%), 2 BRAM | 73.22 MHz (25 MHz board clock) | +| sky130 HD standard cells, core only | 1538 cells, 10,063 µm² | 500 MHz pre-layout, typical corner | + +GPU tests run serially (`RUST_TEST_THREADS=1`): PoCL 5.0 can abort with a +`pocl_release_dlhandle_cache` assertion when several OpenCL contexts are released +from different threads at the same time. With one test thread it passed 40 of +40 runs, against 26 of 40 with parallel threads. + +## Known limits + +- BIT_TEST behaves like BIT_GET; there is no separate condition flag output. +- `base_address << 3` drops the top 3 bits of `base_address` (64-bit wrap). +- The bitstreams have not been run on a physical board yet (no board was available); pin + assignments come from the boards' reference constraint files. +- `asic/` numbers are pre-layout for the bare core. `tapeout/` has a full layout (GDS) of the + Tiny Tapeout top that passes DRC, LVS, antenna, timing and the Tiny Tapeout precheck; it has + not been submitted or fabricated. The SPICE deck is an RC model, not extracted silicon. +- The PCIe design has not run on a LiteFury: the bitstream needs Vivado. An open-source place + and route (nextpnr-xilinx) reaches 88 MHz (84 MHz with a 2-unit MATCH), not 125 MHz; + Vivado's result is unknown. The card is slower than one CPU core for COUNT/BULK; MATCH + (pattern search) with 2 units is expected to be about 2x a 4-thread CPU at 125 MHz and about + 1.2-1.5x at 84 MHz; see `pcie/README.md`. + +The Windows OpenCL SDK and `OpenCL.lib` from `rust-opencl-gpu` are not included +here; the `ocl` crate locates the system OpenCL library. diff --git a/asic/Makefile b/asic/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..cc4314172a94d1be5cf0bbe2e5af28383f2982aa --- /dev/null +++ b/asic/Makefile @@ -0,0 +1,64 @@ +# ASIC synthesis of rtl/bit_accelerator.sv to the SkyWater sky130 high-density +# standard cells (sky130_fd_sc_hd, typical corner 25 C / 1.80 V). +# +# make synth Yosys + ABC (buffering and gate sizing) -> build/bit_accelerator_sky130.v +# make equiv prove the gate netlist equivalent to the RTL (Yosys equiv_*) +# make sta OpenSTA at PERIOD ns (needs `sta` on PATH or STA=/path/to/sta) +# make all synth + equiv +# +# Pre-layout only: no floorplan, placement, clock tree or wire parasitics. + +YOSYS ?= yosys +STA ?= sta +PERIOD ?= 2.0 +BUILD ?= build + +ORFS_COMMIT := 2aeb23bcc3b5aa925abb991c4347ce73827b8b82 +LIB_URL := https://raw.githubusercontent.com/The-OpenROAD-Project/OpenROAD-flow-scripts/$(ORFS_COMMIT)/flow/platforms/sky130hd/lib/sky130_fd_sc_hd__tt_025C_1v80.lib +LIB_SHA256 := ec0e1067a35c8bf20b11e58d1e8ac53326067e4dac84a125cc1b917a3518d0d9 +LIB := $(BUILD)/sky130_fd_sc_hd__tt_025C_1v80.lib +RTL := ../rtl/bit_accelerator.sv +NETLIST := $(BUILD)/bit_accelerator_sky130.v + +.PHONY: all synth equiv sta clean +.DEFAULT_GOAL := all + +all: synth equiv + +$(LIB): + mkdir -p $(BUILD) + curl -sSfL -o $@.tmp $(LIB_URL) + echo "$(LIB_SHA256) $@.tmp" | sha256sum -c --quiet + mv $@.tmp $@ + +synth: $(NETLIST) + +# -nofsm keeps the RTL state encoding so the equivalence check can match registers. +$(NETLIST): $(RTL) $(LIB) abc.script + $(YOSYS) -q -l $(BUILD)/synth.log -p "read_verilog -sv $(RTL); \ + synth -nofsm -flatten -top bit_accelerator; \ + dfflibmap -liberty $(LIB); \ + abc -liberty $(LIB) -D 2000 -script abc.script; \ + opt_clean; tee -o $(BUILD)/area.rpt stat -liberty $(LIB); \ + write_verilog -noattr -noexpr $@" 2>&1 | grep -v "unsupported expression" || true + @test -s $@ + @grep -E "Number of cells:|Chip area" $(BUILD)/area.rpt + +equiv: $(NETLIST) + $(YOSYS) -q -l $(BUILD)/equiv.log -p " \ + read_verilog -sv $(RTL); prep -flatten -top bit_accelerator; \ + rename bit_accelerator gold; design -stash gold; \ + read_liberty -ignore_miss_func $(LIB); read_verilog $(NETLIST); \ + hierarchy -top bit_accelerator; flatten; rename bit_accelerator gate; design -stash gate; \ + design -copy-from gold -as gold gold; design -copy-from gate -as gate gate; \ + equiv_make gold gate equiv; hierarchy -top equiv; async2sync; \ + equiv_simple -seq 5; equiv_induct -seq 5; equiv_status -assert" 2>&1 | grep -v "unsupported expression" || true + @grep -q "Equivalence successfully proven" $(BUILD)/equiv.log + @grep -E "proven and" $(BUILD)/equiv.log | tail -1 + +sta: $(NETLIST) + LIB=$(LIB) NETLIST=$(NETLIST) PERIOD=$(PERIOD) $(STA) -no_init -exit sta.tcl | tee $(BUILD)/sta_$(PERIOD)ns.rpt + @! grep -q VIOLATED $(BUILD)/sta_$(PERIOD)ns.rpt + +clean: + rm -rf $(BUILD) diff --git a/asic/README.md b/asic/README.md new file mode 100644 index 0000000000000000000000000000000000000000..572ec725e9cb484b581c520a9278d05fd1a70c78 --- /dev/null +++ b/asic/README.md @@ -0,0 +1,32 @@ +# ASIC synthesis (sky130) + +Synthesizes `rtl/bit_accelerator.sv` to the SkyWater 130 nm high-density standard +cells (`sky130_fd_sc_hd`, typical corner, 25 °C, 1.80 V). The liberty file is +downloaded from OpenROAD-flow-scripts at a pinned commit and checked by SHA-256. + +```sh +make # synth + equiv +make sta STA=/path/to/sta PERIOD=2.0 +``` + +Needs Yosys. `make sta` needs [OpenSTA](https://github.com/The-OpenROAD-Project/OpenSTA) +(build it from source with CUDD; it is not packaged for Ubuntu). + +## Results + +| | | +|---|---| +| Cells | 1538 (139 flip-flops, plus buffers inserted for fanout) | +| Cell area | 10,063 µm² (about 0.010 mm²) | +| Equivalence to RTL | proved: 223/223 `$equiv` points (Yosys `equiv_simple` + `equiv_induct`) | +| Timing, 2.0 ns (500 MHz) | met: WNS 0.00 ns, no slew/capacitance/fanout violations | +| Timing, 1.9 ns | fails: WNS -0.03 ns | + +These are **pre-layout** numbers: no floorplan, placement, clock tree or wire +parasitics. Inputs are assumed registered on the same clock. Post-layout Fmax +will be lower. Getting to a GDS for fabrication (OpenLane or +OpenROAD-flow-scripts, then a shuttle) is not done here. + +Synthesis uses `-nofsm` so the gate netlist keeps the RTL state encoding, which +lets the equivalence check match registers. With FSM re-encoding, Yosys could +not prove 135 of 220 points without a sequential proof across encodings. diff --git a/asic/abc.script b/asic/abc.script new file mode 100644 index 0000000000000000000000000000000000000000..f049443522033008a9c37a548efa4488746c3742 --- /dev/null +++ b/asic/abc.script @@ -0,0 +1,11 @@ +# ABC mapping script: map to sky130 cells, then buffer high-fanout nets and +# resize gates against the liberty timing (as OpenLane does before placement). +strash +dch +map -B 0.9 +topo +stime -c +buffer -c -N 8 +upsize -c +dnsize -c +stime -p diff --git a/asic/sta.tcl b/asic/sta.tcl new file mode 100644 index 0000000000000000000000000000000000000000..a8c7cff54b6eabd5e523f4706ee7a945054ae7cc --- /dev/null +++ b/asic/sta.tcl @@ -0,0 +1,16 @@ +# Pre-layout static timing for the sky130 netlist (OpenSTA). +# Environment: LIB (liberty), NETLIST (gate-level Verilog), PERIOD (ns). +read_liberty $::env(LIB) +read_verilog $::env(NETLIST) +link_design bit_accelerator +create_clock -name clk -period $::env(PERIOD) [get_ports clk] +# Inputs come from, and outputs go to, registers on the same clock. +set_input_delay 0 -clock clk [delete_from_list [all_inputs] [get_ports clk]] +set_output_delay 0 -clock clk [all_outputs] +set_input_transition 0.1 [all_inputs] +set_load 0.005 [all_outputs] +report_checks -path_delay max -digits 3 +report_check_types -max_slew -max_capacitance -max_fanout -violators +report_wns +report_tns +exit diff --git a/bit_accelerator/.gitignore b/bit_accelerator/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..3a8e5e31ddf66be9322fbba2082470ba47cc9769 --- /dev/null +++ b/bit_accelerator/.gitignore @@ -0,0 +1,2 @@ +build/ +obj_dir/ diff --git a/bit_accelerator/IMPLEMENTATION_REPORT.md b/bit_accelerator/IMPLEMENTATION_REPORT.md new file mode 100644 index 0000000000000000000000000000000000000000..087744c5811fbc43e6d615e68b1fbddb887edc95 --- /dev/null +++ b/bit_accelerator/IMPLEMENTATION_REPORT.md @@ -0,0 +1,47 @@ +# bit_accelerator_v2 - Verified Status + +Everything below was measured by running `make test` (Verilator 5.020, Icarus Verilog 12.0, +Why3 1.6.0 with Z3 4.8.12). + +## Result +- Simulation: `checks=182 fails=0` under both Verilator and Icarus. +- Lint: `verilator --lint-only -Wall` clean. +- Formal: 33/33 Why3 goals valid with Z3. + +## Observed cycle timing (1-cycle-latency memory, no stalls) +a = cycle in which `op_valid && op_ready` is sampled. + +| Op | Timeline | +|----|----------| +| BIT_GET | READ_REQUEST a+1, READ_RESPONSE a+2, RESULT_VALID a+3 (one cycle) | +| SET/CLEAR/TOGGLE | READ_REQUEST a+1, READ_RESPONSE a+2, MODIFY a+3, WRITE accepted a+4, RESULT_VALID a+6 | +| Write stalled (`mem_ready=0` a+4..a+6) | request and wdata held, accepted a+7, RESULT_VALID a+9 | +| Read stalled (`mem_ready=0` a+1..a+2) | accepted a+3, response a+4, RESULT_VALID a+5 | + +## Covered by the testbench +- Result value for GET (set/clear bits, cross-word offsets, non-zero base). +- Memory contents after SET/CLEAR/TOGGLE, including word 1 targets. +- Exactly one `result_valid` pulse per op; `op_ready` low while busy. +- Write and read backpressure: signals held stable, no early result, write committed once. +- Reset asserted in each of READ_REQUEST, READ_WAIT, MODIFY, WRITE_REQUEST, WRITE_WAIT: + no `result_valid`, no write after reset, DUT idle, and a fresh op completes normally. + +## Defects found by simulation and fixed +1. `result_valid` was registered, so it appeared one cycle later than specified (a+4 instead of a+3). + It is now combinational from `ST_RESULT`. +2. A reset in the same cycle as a write handshake let the memory commit the write. + `mem_valid/mem_write/mem_wstrb/mem_wdata` are now forced low while `reset` is asserted. +3. `error` was set on any `mem_fault`; it is now cleared on accept and set only in `ST_READ_WAIT`. +4. Read-fault tests were added (GET/SET/TOGGLE): `error` is high in the result cycle, no write + is issued, and the next operation clears it. The testbench now exits non-zero on failure. +5. Lint fixes: explicit zero-extension of `mem_addr`, bit extraction reads `read_word[bit_index]` directly. +6. The previous testbench did not compile, sampled after the clock edge (racy), and modeled + 2-cycle read latency. It was rewritten to log pre-edge values per cycle. + +## Not verified / known limits +- Only read faults exist; there is no write-fault input path. +- `BIT_TEST` (3'b001) behaves like GET; no flag output exists. +- `base_address << 3` truncates the top 3 bits of `base_address`, and `mem_addr` is 61 significant bits. +- The Why3 proofs cover the specification; RTL-to-specification agreement is checked by simulation only. +- Undefined opcodes (101-111) execute as a read with no write and no error. +- No synthesis was run; area/power/frequency numbers elsewhere in this repo are estimates. diff --git a/bit_accelerator/Makefile b/bit_accelerator/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..e064a1421adf070b0718d4345c1f87710751c7d7 --- /dev/null +++ b/bit_accelerator/Makefile @@ -0,0 +1,55 @@ +# bit_accelerator build and verification. +# +# The verified design is bit_accelerator_v2 (rtl/bit_accelerator_v2.sv). +# The v1 sources (bit_accelerator.sv and its submodules) are kept for reference +# only and are not built: v1 multiply-drives result_valid/result_bit and its +# testbench does not compile. See README.md, "Design versions". + +VERILATOR ?= verilator +IVERILOG ?= iverilog +VVP ?= vvp +WHY3_PROVER ?= z3 +WHY3_TIME ?= 30 +BUILD ?= build + +V2_RTL := rtl/bit_accelerator_v2.sv +V2_TB := testbenches/tb_bit_accelerator_v2.sv +FORMAL := formal/bit_addressing.mlw formal/bit_addressing_proofs.mlw + +.PHONY: help all test lint sim sim-verilator sim-icarus formal formal-cvc4 clean +.DEFAULT_GOAL := help + +all test: lint sim formal + +lint: + $(VERILATOR) --lint-only -Wall $(V2_RTL) + +sim: sim-verilator sim-icarus + +sim-verilator: + OUT=$(abspath $(BUILD))/verilator ./run_v2_sim.sh + +sim-icarus: + mkdir -p $(BUILD) + $(IVERILOG) -g2012 -o $(BUILD)/tb_v2.vvp $(V2_RTL) $(V2_TB) + $(VVP) -n $(BUILD)/tb_v2.vvp + +formal: + ./scripts/prove.sh -P $(WHY3_PROVER) -t $(WHY3_TIME) -L formal $(FORMAL) + +# Cross-check with CVC4. Informational: CVC4 does not discharge every goal +# within the time limit, Z3 does. +formal-cvc4: + -./scripts/prove.sh -P cvc4 -t $(WHY3_TIME) -L formal $(FORMAL) + +clean: + rm -rf $(BUILD) obj_dir + +help: + @echo "make test lint + both simulators + Why3 proofs" + @echo "make lint verilator -Wall on the v2 RTL" + @echo "make sim v2 testbench under Verilator and Icarus" + @echo "make formal prove formal/*.mlw with $(WHY3_PROVER) (every goal must be Valid)" + @echo "make formal-cvc4 informational CVC4 cross-check" + @echo "make clean remove $(BUILD)/" + @echo "Needs: verilator >= 5, iverilog >= 12, why3 >= 1.6 with z3 (why3 config detect)" diff --git a/bit_accelerator/README.md b/bit_accelerator/README.md new file mode 100644 index 0000000000000000000000000000000000000000..7dc365ab551ccb6d604b57903bc81ee2f841859f --- /dev/null +++ b/bit_accelerator/README.md @@ -0,0 +1,200 @@ +# Bit-String Hardware Accelerator + +A dedicated hardware accelerator for bit-string operations with formal verification. + +## Architecture Overview + +``` +request (base_address + bit_offset) + ↓ +STAGE 1: Address Generator (base + offset) + ↓ +STAGE 2: Word & Bit Index Calculator (÷64, mod 64) + ↓ +STAGE 3: Memory Access (TLB → L1 Cache) + ↓ +STAGE 4: Bit Extraction (barrel shifter + AND) + ↓ +STAGE 5: Writeback (register result) + ↓ +result (0 or 1) +``` + +## Supported Operations + +- **BIT_GET** (0b000): Read single bit value +- **BIT_TEST** (0b001): Test bit and set condition flag +- **BIT_SET** (0b010): Set single bit to 1 (read-modify-write) +- **BIT_CLEAR** (0b011): Clear single bit to 0 (read-modify-write) +- **BIT_TOGGLE** (0b100): Toggle single bit (read-modify-write) + +## Design versions + +| Version | Files | Status | +|---------|-------|--------| +| **v2** | `rtl/bit_accelerator_v2.sv` | Verified design. Lint-clean (`verilator -Wall`), 182 testbench checks pass under Verilator and Icarus. | +| v1 | `rtl/bit_accelerator.sv` + submodules | Reference only, not built. `result_valid`/`result_bit` are driven from two processes, and its testbench does not compile. | + +## Interface (v2) + +```systemverilog +// Operation interface +input logic op_valid; output logic op_ready; +input logic [63:0] base_address; // byte address +input logic [63:0] bit_offset; // bit offset from base +input logic [2:0] operation; // 000 GET, 001 TEST, 010 SET, 011 CLEAR, 100 TOGGLE +output logic result_valid; // one-cycle pulse +output logic result_bit; // valid with result_valid +output logic error; // set by a read fault, valid with result_valid, + // held until the next operation is accepted +// Memory interface (valid/ready) +output logic mem_valid, mem_write; +output logic [63:0] mem_addr; // 8-byte-aligned byte address +output logic [63:0] mem_wdata; output logic [7:0] mem_wstrb; +input logic mem_ready, mem_rvalid, mem_fault; +input logic [63:0] mem_rdata; +``` + +Opcodes 101-111 are undefined; v2 executes them as a read with no write. +Write faults are not modelled. + +### Address calculation + +``` +absolute_bit = (base_address * 8) + bit_offset (mod 2^64) +word_address = absolute_bit / 64 +bit_index = absolute_bit mod 64 +result = (memory[word_address] >> bit_index) & 1 +``` + +## Directory structure + +``` +bit_accelerator/ +├── rtl/bit_accelerator_v2.sv # verified design +├── rtl/*.sv # v1 (reference only) +├── testbenches/tb_bit_accelerator_v2.sv +├── formal/bit_addressing.mlw # specification + lemmas (Why3) +├── formal/bit_addressing_proofs.mlw # derived lemmas +├── scripts/prove.sh # proves every goal; fails unless all are Valid +├── isa/BIT_ISA.md, docs/DATAPATH.md +├── Makefile, build.sh, run_v2_sim.sh +└── IMPLEMENTATION_REPORT.md # measured results +``` + +## Build and verification + +Prerequisites (Ubuntu 24.04): `apt-get install verilator iverilog why3 z3`, then `why3 config detect`. + +```bash +make test # lint + Verilator + Icarus simulation + Why3 proofs +make lint # verilator --lint-only -Wall on v2 +make sim # v2 testbench under both simulators +make formal # every Why3 goal must be proved by Z3 +make formal-cvc4 # informational cross-check with CVC4 +./build.sh # same as make test, fails if a tool is missing +``` + +## Formal verification + +`formal/` is checked by Why3 1.6 with Z3 4.8.12: **33/33 goals valid**, no +axioms beyond the Why3 standard library. The lemmas cover: + +1. Address decomposition: `addr = word * 64 + bit`, `0 <= bit < 64`, and (word, bit) determines the address. +2. Boundaries: offset 64 reaches the next word; offsets 0-63 stay in one word **when the base is word-aligned** (`base mod 8 = 0`). +3. Bit operations: GET returns 0 or 1 and is deterministic; GET after SET/CLEAR returns 1/0. +4. Non-interference: SET or CLEAR on one word does not change a GET from another word. + +An earlier draft of these files was not valid Why3 and stated three theorems +that are false (`bit_63_same_word`, `bit_index_wraps_at_64` without the +alignment condition, and `within_word_uniqueness`). Z3 proves their negations; +they were corrected. + +The proofs are about the specification. Agreement between the specification +and the RTL is checked by simulation, not proved. + +## Testing + +`tb_bit_accelerator_v2.sv` is cycle-accurate and self-checking (exits non-zero +on any failure). It checks: + +- GET results (set and clear bits, offset 64, non-zero base) and exact cycle timing +- SET/CLEAR/TOGGLE memory contents, including cross-word offsets +- one `result_valid` pulse per operation; `op_ready` low while busy +- read and write backpressure: request and data held stable, write committed once +- reset in each of READ_REQUEST, READ_WAIT, MODIFY, WRITE_REQUEST, WRITE_WAIT +- read faults on GET/SET/TOGGLE: `error` in the result cycle, no write, cleared by the next op +- undefined opcodes: no write + +## Hardware estimates + +The figures below are design targets, **not measured**: no synthesis, +place-and-route or power analysis has been run. + +| Scenario | Latency | +|----------|---------| +| L1 cache hit | 4 cycles | +| L2 cache miss | 10-15 cycles | +| Memory miss | 50+ cycles | + +- Gate count ~50k (7nm), area ~0.6 mm², power ~2.5 mW active, 1+ GHz + +## Performance Comparison + +### Traditional LOAD-SHIFT-AND Sequence + +``` +LOAD r1, [base] (3-50 cycles: cache/memory) +SHIFT r1, r1, offset (1 cycle) +AND r1, r1, 1 (1 cycle) +TOTAL: 5-52 cycles +``` + +### Dedicated BIT_GET Instruction + +``` +BIT_GET r1, base, offset (4-15 cycles: includes memory) +TOTAL: 4-15 cycles +IMPROVEMENT: 20-80% latency reduction +``` + +## Limitations & Future Work + +### Current Scope +- 64-bit word size (fixed) +- Single-bit operations only (no multi-bit extract yet) +- No atomic multiword operations +- No GPU integration + +### Future Extensions +- Variable-width field extraction +- Atomic compare-and-swap for multiword fields +- SIMD bit-parallel operations +- Hardware-assisted population count pipeline + +## Design Philosophy + +This accelerator prioritizes: + +1. **Correctness**: Formal verification, not testing alone +2. **Determinism**: No undefined behavior, no race conditions +3. **Simplicity**: Minimal instruction set, orthogonal operations +4. **Performance**: Single-digit cycle latency on hits +5. **Verification**: Machine-checkable proofs, not documentation + +## References + +### Intel 64 & IA-32 Architecture +- Bit Manipulation Instructions (BMI, BMI2) +- BITFIELD, BIT_SET, BIT_CLEAR semantics + +### Hardware Design +- Kogge-Stone parallel-prefix adder (address gen) +- Logarithmic barrel shifter (bit extraction) +- Standard pipelined memory interface + +### Formal Methods +- Why3 platform for machine verification +- Euclidean division properties +- Non-interference proofs for memory operations + diff --git a/bit_accelerator/build.sh b/bit_accelerator/build.sh new file mode 100644 index 0000000000000000000000000000000000000000..384662a274e1ecf0784fd599ecfbf56cc5ed3be1 --- /dev/null +++ b/bit_accelerator/build.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Runs every check (lint, Verilator + Icarus simulation, Why3 proofs) and exits +# non-zero if any fails. Missing tools are reported as failures, not passes. +set -euo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")" +for tool in verilator iverilog vvp why3; do + command -v "$tool" >/dev/null || { echo "missing tool: $tool" >&2; exit 1; } +done +make test diff --git a/bit_accelerator/docs/DATAPATH.md b/bit_accelerator/docs/DATAPATH.md new file mode 100644 index 0000000000000000000000000000000000000000..70a20497ca6ec010f67bdf9322dd76b1035e2797 --- /dev/null +++ b/bit_accelerator/docs/DATAPATH.md @@ -0,0 +1,437 @@ +# Bit-Accelerator Datapath Architecture + +## 1. High-Level Datapath Diagram + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ BIT ACCELERATOR DATAPATH │ +└─────────────────────────────────────────────────────────────────────┘ + +Stage 0: DECODE & OPERAND FETCH +┌──────────────┐ +│ Opcode │──────────► Opcode Decoder (6-to-12 decoder) +│ Operands │──────────► Register File (3R1W) +│ │ +└──────────────┘ + │ + ▼ +Stage 1: ADDRESS GENERATION +┌──────────────────────────────────────────┐ +│ Adder (Rs1 + Rs2) ◄─── 64-bit Carry │ +│ eff_bit_address = base + offset │ +│ │ +│ Output: 64-bit effective bit address │ +└──────────────────────────────────────────┘ + │ + ▼ +Stage 2: WORD & BIT INDEX CALCULATION +┌──────────────────────────────────────┐ +│ Divider / Shifter (÷64 & mod 64) │ +│ word_index = eff_addr >> 6 │ Combinational or pipelined +│ bit_index = eff_addr & 0x3F │ +│ │ +│ Output: (word_index[57:0], bit_idx) │ +└──────────────────────────────────────┘ + │ + ▼ +Stage 3: MEMORY ACCESS & TLB LOOKUP +┌──────────────────────────────────────────┐ +│ TLB Lookup (word_index → phys_address) │ +│ L1 Cache Tag/Index Match │ +│ Memory Request to Load/Store Queue │ +│ │ +│ Output: 64-bit word from L1/L2/Memory │ +└──────────────────────────────────────────┘ + │ + ▼ +Stage 4: BIT EXTRACTION +┌──────────────────────────────────────┐ +│ Barrel Shifter │ +│ (word >> bit_index) │ +│ │ +│ AND Gate (extract LSB) │ +│ result = shifted_word & 1 │ +│ │ +│ Output: 1-bit result │ +└──────────────────────────────────────┘ + │ + ▼ +Stage 5: WRITEBACK +┌──────────────────────────────────────┐ +│ Register File Write │ +│ result ──► Rd │ +└──────────────────────────────────────┘ +``` + +--- + +## 2. Component Specifications + +### 2.1 Address Generator (Stage 1) + +**Component**: 64-bit Ripple-Carry Adder or Kogge-Stone Parallel-Prefix Adder + +**Inputs**: +- `base_address[63:0]` from Rs1 register +- `bit_offset[63:0]` from Rs2 register +- `carry_in` = 0 + +**Outputs**: +- `effective_bit_address[63:0]` + +**Latency**: 1 cycle (Kogge-Stone) or 2 cycles (simpler adder) + +**Control Signals**: +- `enable_add`: 1 if instruction requires address computation +- `select_operand`: 0 = Rs1+Rs2, 1 = Rs3+Rs4 (for multi-register ops) + +--- + +### 2.2 Word and Bit Index Calculator (Stage 2) + +**Component**: Fixed-Point Divider (÷ 64) and Modulo-64 Unit + +**Inputs**: +- `effective_bit_address[63:0]` from Address Generator + +**Outputs**: +- `word_index[57:0]` = `effective_bit_address >> 6` +- `bit_index[5:0]` = `effective_bit_address[5:0]` + +**Hardware Implementation**: +``` +word_index[i] = effective_bit_address[i+6], for i = 0 to 57 +bit_index[i] = effective_bit_address[i], for i = 0 to 5 +``` +(This is purely combinational wiring - no logic!) + +**Latency**: 0 cycles (combinational) + +--- + +### 2.3 TLB & Cache Lookup (Stage 3) + +**Component**: TLB (Translation Lookaside Buffer) + L1 Cache Controller + +**Inputs**: +- `word_index[57:0]` (virtual word address, 6-bit aligned = 6 bits from effective address) +- Control: `is_load`, `is_store` + +**Outputs**: +- `cache_hit` (1-bit flag) +- `data_word[63:0]` (from L1 cache on hit) +- `phys_address[40:0]` (for miss → L2/Memory) + +**Behavior**: + +**On Hit** (L1 cache, TLB translate): +- Return data directly from cache (fastest path) +- Latency: 1 cycle + +**On Miss** (TLB or L1 cache miss): +- Initiate memory request to L2 +- Stall pipeline until data returns +- Latency: variable (10+ cycles to L2, 50+ to main memory) + +**Special Case: Page Boundary**: +- If `bit_address` and `(bit_address + 63)` span different pages: + - Hardware may need two memory accesses + - OR: Load both words and merge results (dual-load architecture) + +--- + +### 2.4 Barrel Shifter (Stage 4) + +**Component**: 64-bit Logarithmic Shifter + +**Inputs**: +- `word[63:0]` from memory/cache +- `bit_index[5:0]` from Index Calculator +- `shift_direction` (0=right, 1=left, from decoder) +- `rotate_mode` (0=shift, 1=rotate) + +**Implementation** (Rotating 64-bit barrel shifter): +``` +Level 0: Shift by 32: word[i] ← (shift_by_32 ? word[(i+32)%64] : word[i]) +Level 1: Shift by 16: word[i] ← (shift_by_16 ? word[(i+16)%64] : word[i]) +Level 2: Shift by 8: word[i] ← (shift_by_8 ? word[(i+8)%64] : word[i]) +Level 3: Shift by 4: word[i] ← (shift_by_4 ? word[(i+4)%64] : word[i]) +Level 4: Shift by 2: word[i] ← (shift_by_2 ? word[(i+2)%64] : word[i]) +Level 5: Shift by 1: word[i] ← (shift_by_1 ? word[(i+1)%64] : word[i]) + +bit_index[5:0] is decoded into 6 control signals: {shift_by_32, ..., shift_by_1} +``` + +**Outputs**: +- `shifted_word[63:0]` + +**Latency**: 1 cycle (combinational through 6 levels of muxes) + +--- + +### 2.5 Bit Extractor (Stage 4, after shifter) + +**Component**: AND gate + optional sign-extension logic + +**Inputs**: +- `shifted_word[63:0]` from Barrel Shifter +- `mask[63:0]` (computed from field width) + - Single bit: `mask = 64'h0000_0000_0000_0001` + - Multi-bit (width w): `mask = (1 << w) - 1` + +**Outputs**: +- `extracted_field[63:0]` = `shifted_word & mask` + +**For BITLOAD** (single-bit extract): +- Output: `result[63:0]` with `result[0] = extracted_field[0]`, `result[63:1] = 0` + +**For BITFIELD** (multi-bit extract): +- Output: `result[63:0]` = extracted field, zero-extended + +**Latency**: 1 cycle (combinational AND + logic) + +--- + +### 2.6 Control Unit (Combinational + Sequential) + +**Combinational Decoder**: +``` +Opcode[5:0] → 12 control signals: + - enable_addr_gen + - enable_mem_load + - enable_mem_store + - enable_shift + - enable_extract + - enable_writeback + - shift_direction (L/R) + - rotate_mode + - field_width[5:0] + - is_signed_extend +``` + +**Sequential State Machine** (for cache misses, exceptions): +``` +State 0: IDLE → await instruction +State 1: EXECUTE → run datapath +State 2: MEMORY_WAIT → stall on L1 miss +State 3: EXCEPTION → handle fault +State 4: WRITEBACK → commit result +``` + +--- + +### 2.7 Register File (3-Read, 1-Write Port) + +**Capacity**: 32 × 64-bit registers + +**Read Ports**: +- Port A: Rs1 (base address) +- Port B: Rs2 (offset/secondary operand) +- Port C: Rs3 (value to write for BITSTORE) + +**Write Port**: +- Port W: Rd (destination register) + +**Access Time**: 1 cycle (combinational read, synchronous write) + +--- + +## 3. Data Flow for BITLOAD Instruction + +**Instruction**: `BITLOAD R5, R10, R15` + +``` +Clock | Stage | Operation +------|-------|------------------------------------------- + 0 | 0 | Decode BITLOAD, fetch R10=0x1000, R15=0x42 + | | + 1 | 1 | Address generator: eff_addr = 0x1000 + 0x42 = 0x1042 + | | + 2 | 2 | Index calc (combinational): + | | word_idx = 0x1042 >> 6 = 0x41 + | | bit_idx = 0x1042 & 0x3F = 0x02 + | | + 3 | 3 | TLB lookup: word_index → phys_addr = 0x41000 + | | L1 cache hit: word = 0xABCD_EF01_2345_6789 + | | + 4 | 4 | Shifter: shifted = word >> 2 = 0x2AF37_BC04_8D15_9E + | | Extractor: result = shifted & 1 = 0 + | | + 5 | 5 | Writeback: R5 ← 0 +``` + +--- + +## 4. Pipeline Depth Analysis + +**Best Case** (L1 hit): +- Stage 0: Decode (1 cycle) +- Stage 1: Address gen (1 cycle) +- Stage 2: Index calc (0 cycles, combinational) +- Stage 3: Memory access (1 cycle hit) +- Stage 4: Extract (1 cycle) +- Stage 5: Writeback (committed) +- **Total: 4 cycles latency** + +**Worst Case** (L2 miss, then main memory): +- Stages 0-3: same as above +- Stage 3: Memory miss, initiate L2 request (10+ cycles) +- Stage 4: Extract (1 cycle) +- **Total: 15+ cycles latency** + +--- + +## 5. Throughput Analysis + +**Instruction Issue Rate**: 1 instruction per cycle (assuming no structural hazards) + +**Bottlenecks**: +- Memory bandwidth: If high % of instructions are BITLOAD/BITSTORE +- Cache behavior: Working set fit in L1 cache is critical +- Register contention: Low (3 reads, 1 write per cycle is feasible) + +**Optimization**: Dual-issue for independent bit operations: +- Issue BITLOAD + BITCOUNT in same cycle (different functional units) + +--- + +## 6. Critical Path (for clock frequency) + +The critical path is the longest combinational delay in the pipeline: + +``` +TDC = T_mux(operand select) + T_adder(64-bit) + T_setup(register) + ≈ 0.3ns + 1.2ns + 0.2ns = 1.7ns + ≈ 588 MHz (f_clock ≤ 1/1.7ns) +``` + +For 1+ GHz target, require pipelined adder (Kogge-Stone): +``` +T_KS_adder ≈ 0.8ns → allows ~1.2 GHz +``` + +--- + +## 7. Physical Implementation Details + +### 7.1 Gate Counts (Estimates) + +| Component | Gates | +|-----------|-------| +| 64-bit Adder (Kogge-Stone) | 1,500 | +| Barrel Shifter (6 levels) | 4,000 | +| TLB (16-32 entries) | 5,000 | +| L1 Cache Interface | 10,000 | +| Register File (32×64) | 20,000 | +| Control Decoder + FSM | 3,000 | +| Data Muxes, AND gates, misc | 5,000 | +| **Total estimate** | **~50k gates** | + +### 7.2 Area Estimate + +- Technology: 7nm FinFET +- Gate density: ~80M gates/mm² +- **Area ≈ 50k / 80M ≈ 0.6 mm²** (sub-mm² functional unit) + +### 7.3 Power Estimate + +- Dynamic power: ~2mW (1 GHz, 1.2V, 50k gates, 80% switching activity) +- Static power: ~0.5mW (leakage) +- **Total: ~2.5mW** (idle or active, instruction-dependent) + +--- + +## 8. Memory System Integration + +### 8.1 Memory Hierarchy + +``` +CPU Pipeline + ↓ +L1 I-Cache (32 KB) ←── 1 cycle (instructions) +L1 D-Cache (32 KB) ←── 1 cycle (data for BITLOAD) + ↓ (miss) +L2 Cache (256 KB) ←── 10 cycles + ↓ (miss) +L3 Cache (8 MB) ←── 30 cycles + ↓ (miss) +Main Memory (DDR4) ←── 50+ cycles +``` + +### 8.2 TLB Integration + +``` +Virtual Bit Address (64-bit) + ↓ +Extract word_index[57:0] = bit_address[63:6] + ↓ +TLB lookup (16-entry, 4-way associative) + ├─► Hit: phys_word_addr = phys_base[40:0] | word_index[13:0] + ├─► Miss: page walk (25+ cycles) + ↓ +L1 Cache tag lookup +``` + +### 8.3 Boundary Conditions + +**Case 1**: Bit 0-63 fit in single 64-bit word +- Single memory access (normal path) + +**Case 2**: Extract bits spanning two words (e.g., bits 60-65) +- Hardware loads both words +- Shifts, masks, combines +- Transparent to ISA + +**Case 3**: Bits span two pages (e.g., word 0x1FFFF to 0x20000) +- May require two separate TLB lookups + memory accesses +- Hardware handles by stalling until both words available +- Or: Software constraint to avoid (less common) + +--- + +## 9. Write-Path (BITSTORE) + +``` +BITSTORE Rs1, Rs2, Rs3 + +Stage 1: Address gen → eff_addr = Rs1 + Rs2 +Stage 2: Index calc → word_idx, bit_idx +Stage 3: TLB lookup + load current word +Stage 4: Bit mask & merge: + mask = 1 << bit_idx + new_word = (old_word & ~mask) | ((Rs3 & 1) << bit_idx) +Stage 5: Store new_word back to memory (writeback cache) +``` + +**Read-Modify-Write**: +- Load old word: 3 cycles +- Compute new value: 1 cycle +- Store: pipelined (doesn't wait for commit) +- **Total latency: 4-5 cycles** + +--- + +## 10. Register Transfer Language (RTL) Skeleton + +```verilog +// Stage 1: Address Generation +reg [63:0] eff_addr; +always @(posedge clk) + eff_addr <= rs1_data + rs2_data; + +// Stage 2: Index Calculation (combinational) +wire [57:0] word_index = eff_addr[63:6]; +wire [5:0] bit_index = eff_addr[5:0]; + +// Stage 3: Memory Access +wire [63:0] memory_word = l1_cache_read(word_index); + +// Stage 4: Bit Extraction +wire [63:0] shifted = memory_word >> bit_index; +wire [63:0] extracted = shifted & 64'h1; + +// Stage 5: Writeback +always @(posedge clk) + rd_data <= extracted; +``` + diff --git a/bit_accelerator/formal/bit_addressing.mlw b/bit_accelerator/formal/bit_addressing.mlw new file mode 100644 index 0000000000000000000000000000000000000000..e20287a21a0b1e8f745b0b77303e38a5ec42db4e --- /dev/null +++ b/bit_accelerator/formal/bit_addressing.mlw @@ -0,0 +1,155 @@ +(* Bit addressing and single-bit operation specification for bit_accelerator_v2. + + Memory is a map from word index to an integer word value. Addresses: + absolute bit address = base_byte_address * 8 + bit_offset + word index = bit address / 64 + bit index = bit address mod 64 (Euclidean division) + + Corrections relative to the earlier draft (each disproved by Z3): + - bit_63_same_word and bit_index_wraps_at_64 only hold for a word-aligned + base (base mod 8 = 0); base = 7 and base = 1 are counterexamples. + - within_word_uniqueness claimed two different offsets never share a bit + index; offsets 0 and 64 do. It now states that (word, bit) determines the + bit address. + - disjoint_words_independent used a function where a bit result was meant. *) + +module BitAddressing + + use int.Int + use int.EuclideanDivision + use int.Power + + constant word_bits : int = 64 + constant max_address : int = 281474976710656 (* 2^48 words *) + + predicate is_valid_bit_address (addr : int) = + 0 <= addr < max_address * word_bits + + predicate word_aligned (addr : int) = mod addr word_bits = 0 + + function absolute_bit_address (base_byte_addr bit_offset : int) : int = + base_byte_addr * 8 + bit_offset + + function word_index (bit_addr : int) : int = div bit_addr word_bits + + function bit_index_in_word (bit_addr : int) : int = mod bit_addr word_bits + + (* ---------------------------------------------------- address calculation *) + + lemma absolute_bit_address_valid : + forall base offset : int. + 0 <= base -> 0 <= offset -> absolute_bit_address base offset >= 0 + + lemma word_index_positive : + forall bit_addr : int. is_valid_bit_address bit_addr -> + 0 <= word_index bit_addr < max_address + + lemma bit_index_in_range : + forall bit_addr : int. + 0 <= bit_index_in_word bit_addr < word_bits + + lemma euclidean_division : + forall bit_addr : int. + bit_addr = word_index bit_addr * word_bits + bit_index_in_word bit_addr + + lemma word_and_bit_determine_address : + forall a1 a2 : int. + word_index a1 = word_index a2 -> + bit_index_in_word a1 = bit_index_in_word a2 -> + a1 = a2 + + (* --------------------------------------------------------------- boundaries *) + + lemma bit_0_maps_to_word_0 : + forall base : int. word_index (absolute_bit_address base 0) = div base 8 + + lemma bit_63_same_word : + forall base : int. mod base 8 = 0 -> + word_index (absolute_bit_address base 0) = + word_index (absolute_bit_address base 63) + + lemma bit_64_next_word : + forall base : int. + word_index (absolute_bit_address base 64) = + word_index (absolute_bit_address base 0) + 1 + + lemma bit_index_wraps_at_64 : + forall base : int. mod base 8 = 0 -> + bit_index_in_word (absolute_bit_address base 63) = 63 /\ + bit_index_in_word (absolute_bit_address base 64) = 0 + + lemma within_word_uniqueness : + forall base offset1 offset2 : int. + word_index (absolute_bit_address base offset1) = + word_index (absolute_bit_address base offset2) -> + bit_index_in_word (absolute_bit_address base offset1) = + bit_index_in_word (absolute_bit_address base offset2) -> + offset1 = offset2 + + (* -------------------------------------------------------- bit operations *) + + function bit_of (w b : int) : int = mod (div w (power 2 b)) 2 + + lemma bit_of_binary : + forall w b : int. 0 <= b -> bit_of w b = 0 \/ bit_of w b = 1 + + predicate bit_get_correct (memory : int -> int) (base offset result : int) = + let a = absolute_bit_address base offset in + is_valid_bit_address a /\ + result = bit_of (memory (word_index a)) (bit_index_in_word a) + + predicate frame (old_mem new_mem : int -> int) (w : int) = + forall i : int. i <> w -> new_mem i = old_mem i + + predicate bit_set_correct (old_mem new_mem : int -> int) (base offset : int) = + let a = absolute_bit_address base offset in + is_valid_bit_address a /\ + bit_of (new_mem (word_index a)) (bit_index_in_word a) = 1 /\ + frame old_mem new_mem (word_index a) + + predicate bit_clear_correct (old_mem new_mem : int -> int) (base offset : int) = + let a = absolute_bit_address base offset in + is_valid_bit_address a /\ + bit_of (new_mem (word_index a)) (bit_index_in_word a) = 0 /\ + frame old_mem new_mem (word_index a) + + predicate bit_toggle_correct (old_mem new_mem : int -> int) (base offset : int) = + let a = absolute_bit_address base offset in + is_valid_bit_address a /\ + bit_of (new_mem (word_index a)) (bit_index_in_word a) = + 1 - bit_of (old_mem (word_index a)) (bit_index_in_word a) /\ + frame old_mem new_mem (word_index a) + + lemma bit_get_binary : + forall memory : int -> int, base offset result : int. + bit_get_correct memory base offset result -> result = 0 \/ result = 1 + + let lemma bit_get_deterministic (memory : int -> int) (base offset r1 r2 : int) + requires { bit_get_correct memory base offset r1 } + requires { bit_get_correct memory base offset r2 } + ensures { r1 = r2 } + = let a = absolute_bit_address base offset in + assert { r1 = bit_of (memory (word_index a)) (bit_index_in_word a) = r2 } + + (* A SET to one word does not change a GET from a different word. *) + let lemma disjoint_words_independent (old_mem new_mem : int -> int) + (base1 offset1 base2 offset2 r : int) + requires { word_index (absolute_bit_address base1 offset1) <> + word_index (absolute_bit_address base2 offset2) } + requires { bit_set_correct old_mem new_mem base1 offset1 } + requires { bit_get_correct new_mem base2 offset2 r } + ensures { bit_get_correct old_mem base2 offset2 r } + = let a2 = absolute_bit_address base2 offset2 in + assert { new_mem (word_index a2) = old_mem (word_index a2) } + + (* After SET / CLEAR, GET of the same bit returns 1 / 0. *) + lemma get_after_set : + forall old_mem new_mem : int -> int, base offset : int. + bit_set_correct old_mem new_mem base offset -> + bit_get_correct new_mem base offset 1 + + lemma get_after_clear : + forall old_mem new_mem : int -> int, base offset : int. + bit_clear_correct old_mem new_mem base offset -> + bit_get_correct new_mem base offset 0 +end diff --git a/bit_accelerator/formal/bit_addressing_proofs.mlw b/bit_accelerator/formal/bit_addressing_proofs.mlw new file mode 100644 index 0000000000000000000000000000000000000000..30145510bfaa2345495a688154846ce792b29c39 --- /dev/null +++ b/bit_accelerator/formal/bit_addressing_proofs.mlw @@ -0,0 +1,45 @@ +(* Derived properties of BitAddressing, proved from the definitions in + bit_addressing.mlw (run with `why3 prove -L formal ...`). *) + +module BitAddressingProofs + + use int.Int + use int.EuclideanDivision + use bit_addressing.BitAddressing + + (* Every valid bit address has a valid word and an in-range bit index. *) + lemma valid_address_decomposes : + forall a : int. is_valid_bit_address a -> + 0 <= word_index a < max_address /\ 0 <= bit_index_in_word a < 64 /\ + a = word_index a * 64 + bit_index_in_word a + + (* The 64 offsets from a word-aligned base cover exactly one word, in order. *) + lemma aligned_base_covers_one_word : + forall base k : int. mod base 8 = 0 -> 0 <= k < 64 -> + word_index (absolute_bit_address base k) = div base 8 /\ + bit_index_in_word (absolute_bit_address base k) = k + + (* Offset k + 64 reaches the same bit of the next word. *) + lemma offset_plus_64 : + forall base k : int. + word_index (absolute_bit_address base (k + 64)) = + word_index (absolute_bit_address base k) + 1 /\ + bit_index_in_word (absolute_bit_address base (k + 64)) = + bit_index_in_word (absolute_bit_address base k) + + (* Offsets are interchangeable with base bytes: 8 bits per byte. *) + lemma byte_offset_equivalence : + forall base offset : int. + absolute_bit_address (base + 1) offset = absolute_bit_address base (offset + 8) + + (* A CLEAR to one word does not change a GET from a different word. *) + let lemma clear_disjoint_words_independent (old_mem new_mem : int -> int) + (base1 offset1 base2 offset2 r : int) + requires { word_index (absolute_bit_address base1 offset1) <> + word_index (absolute_bit_address base2 offset2) } + requires { bit_clear_correct old_mem new_mem base1 offset1 } + requires { bit_get_correct new_mem base2 offset2 r } + ensures { bit_get_correct old_mem base2 offset2 r } + = let a2 = absolute_bit_address base2 offset2 in + assert { new_mem (word_index a2) = old_mem (word_index a2) } +end diff --git a/bit_accelerator/isa/BIT_ISA.md b/bit_accelerator/isa/BIT_ISA.md new file mode 100644 index 0000000000000000000000000000000000000000..6f9527b788caf56ad86d3fe85612d846f02694b6 --- /dev/null +++ b/bit_accelerator/isa/BIT_ISA.md @@ -0,0 +1,416 @@ +# Bit-String Hardware Accelerator ISA Specification + +## 1. ISA Overview + +**Architecture**: RISC-style bit-manipulation ISA +**Operand Width**: 64-bit integer registers +**Word Size**: 64 bits +**Addressing**: Bit-addressed linear address space + +--- + +## 2. Register File + +``` +Registers: R0-R31 (32 × 64-bit GPRs) +Special Registers: + - R0: Hard-wired zero + - R31: Return value / implicit accumulator +``` + +--- + +## 3. Instruction Format + +### 3.1 R-Type (Register-Register) +``` +┌─────────┬─────┬─────┬─────┬────────────┐ +│ Opcode │ Rd │ Rs1 │ Rs2 │ Reserved │ +│ 6 bits │ 5 │ 5 │ 5 │ 13 bits │ +└─────────┴─────┴─────┴─────┴────────────┘ +``` + +### 3.2 I-Type (Register-Immediate) +``` +┌─────────┬─────┬─────┬──────────────┐ +│ Opcode │ Rd │ Rs1 │ Immediate │ +│ 6 bits │ 5 │ 5 │ 18 bits │ +└─────────┴─────┴─────┴──────────────┘ +``` + +### 3.3 L-Type (Load Immediate) +``` +┌─────────┬─────┬──────────────────────┐ +│ Opcode │ Rd │ Immediate │ +│ 6 bits │ 5 │ 53 bits (sign-ext) │ +└─────────┴─────┴──────────────────────┘ +``` + +--- + +## 4. Opcode Assignments + +| Mnemonic | Opcode | Type | Semantics | +|----------------|--------|------|-----------| +| BITLOAD | 0x01 | R | rd ← mem[rs1 + rs2 bit offset] & 1 | +| BITSTORE | 0x02 | R | mem[rs1 + rs2 bit offset] ← rs3 & 1 | +| BITFIELD | 0x03 | R | rd ← (mem[rs1 + rs2] >> rs3) & ((1 << rs4) - 1) | +| BITTEST | 0x04 | R | flag ← (mem[rs1 + rs2] >> bit_offset) & 1 | +| BITCOUNT | 0x05 | R | rd ← popcount(mem[rs1]) | +| BITSCANFWD | 0x06 | R | rd ← first_set_bit(rs1) | +| BITSCANREV | 0x07 | R | rd ← last_set_bit(rs1) | +| LOADIMM64 | 0x08 | L | rd ← immediate (53-bit sign-extended) | +| BITEXTRACT | 0x09 | R | rd ← extract_field(rs1, rs2, rs3) | +| BITINSERT | 0x0A | R | rd ← insert_field(rs1, rs2, rs3, rs4) | +| BITROTL | 0x0B | R | rd ← rotate_left(rs1, rs2) | +| BITROTR | 0x0C | R | rd ← rotate_right(rs1, rs2) | + +--- + +## 5. Instruction Semantics + +### 5.1 BITLOAD (Single-Bit Load) + +**Encoding**: `BITLOAD Rd, Rs1, Rs2` + +**Operation**: +``` +effective_bit_address = Rs1 + Rs2 +word_index = effective_bit_address / 64 +bit_index = effective_bit_address mod 64 +Rd ← (memory[word_index] >> bit_index) & 1 +``` + +**Constraints**: +- `Rs1`: Bit base address (64-bit) +- `Rs2`: Bit offset (64-bit) +- `Rd`: Destination register (bit 0 set, bits [63:1] = 0) +- No privilege required +- May fault: page fault if effective address invalid + +**Latency**: 3 cycles (address generation + memory access + extract) + +--- + +### 5.2 BITSTORE (Single-Bit Write) + +**Encoding**: `BITSTORE Rs1, Rs2, Rs3` + +**Operation**: +``` +effective_bit_address = Rs1 + Rs2 +word_index = effective_bit_address / 64 +bit_index = effective_bit_address mod 64 +bit_value = Rs3 & 1 +old_word = memory[word_index] +mask = 1 << bit_index +new_word = (old_word & ~mask) | (bit_value << bit_index) +memory[word_index] ← new_word +``` + +**Constraints**: +- `Rs1`: Bit base address +- `Rs2`: Bit offset +- `Rs3`: Bit value (only bit 0 is meaningful) +- No destination register +- Requires write permission on target memory +- May fault: segmentation fault, write-protect fault + +**Latency**: 3 cycles (address generation + RMW cycle) + +--- + +### 5.3 BITFIELD (Multi-Bit Extract) + +**Encoding**: `BITFIELD Rd, Rs1, Rs2, Rs3, Rs4` + +**Operation**: +``` +effective_bit_address = Rs1 + Rs2 +word_index = effective_bit_address / 64 +bit_index = effective_bit_address mod 64 +field_width = Rs3 +bit_offset_in_word = Rs4 + +word = memory[word_index] +extracted = (word >> bit_offset_in_word) & ((1 << field_width) - 1) +Rd ← extracted +``` + +**Constraints**: +- `field_width` must be in range [1, 64] +- If `bit_offset + field_width > 64`, behavior is undefined (may trap) +- No cross-boundary extraction in this instruction (use software loop) + +**Latency**: 3 cycles + +--- + +### 5.4 BITTEST (Test and Branch) + +**Encoding**: `BITTEST Rs1, Rs2` + +**Operation**: +``` +effective_bit_address = Rs1 + Rs2 +word_index = effective_bit_address / 64 +bit_index = effective_bit_address mod 64 +condition_flag ← (memory[word_index] >> bit_index) & 1 +``` + +**Latency**: 3 cycles + +**Conditional Execution**: Sets zero flag (ZF) in status register: +- `ZF = 0` if bit is set +- `ZF = 1` if bit is clear + +--- + +### 5.5 BITCOUNT (Population Count) + +**Encoding**: `BITCOUNT Rd, Rs1` + +**Operation**: +``` +Rd ← popcount(Rs1) +``` + +**Latency**: 2 cycles (specialized hardware) + +--- + +### 5.6 BITSCANFWD (Find First Set Bit) + +**Encoding**: `BITSCANFWD Rd, Rs1` + +**Operation**: +``` +If Rs1 = 0: + Rd ← 64 (or undefined, behavior determined by hardware) +Else: + Rd ← position of rightmost set bit (0-indexed) + Range: Rd ∈ [0, 63] +``` + +**Latency**: 2-3 cycles + +--- + +### 5.7 BITSCANREV (Find Last Set Bit) + +**Encoding**: `BITSCANREV Rd, Rs1` + +**Operation**: +``` +If Rs1 = 0: + Rd ← 64 (or undefined) +Else: + Rd ← position of leftmost set bit (0-indexed) + Range: Rd ∈ [0, 63] +``` + +**Latency**: 2-3 cycles + +--- + +### 5.8 LOADIMM64 (Load 53-Bit Immediate) + +**Encoding**: `LOADIMM64 Rd, immediate` + +**Operation**: +``` +Rd ← sign_extend(immediate[52:0], 64) +``` + +**Latency**: 1 cycle + +--- + +### 5.9 BITEXTRACT (Field Extraction with Width) + +**Encoding**: `BITEXTRACT Rd, Rs1, Rs2, Rs3` + +**Operation**: +``` +value = Rs1 +start_bit = Rs2 +field_width = Rs3 + +extracted = (value >> start_bit) & ((1 << field_width) - 1) +Rd ← extracted +``` + +**Latency**: 1 cycle (register-only, no memory) + +--- + +### 5.10 BITINSERT (Field Insertion) + +**Encoding**: `BITINSERT Rd, Rs1, Rs2, Rs3, Rs4` + +**Operation**: +``` +dest_value = Rs1 +source_value = Rs2 +start_bit = Rs3 +field_width = Rs4 + +mask = ((1 << field_width) - 1) << start_bit +shifted_source = (source_value & ((1 << field_width) - 1)) << start_bit +Rd ← (dest_value & ~mask) | shifted_source +``` + +**Latency**: 2 cycles + +--- + +### 5.11 BITROTL (Rotate Left) + +**Encoding**: `BITROTL Rd, Rs1, Rs2` + +**Operation**: +``` +value = Rs1 +rotate_amount = Rs2 mod 64 +Rd ← (value << rotate_amount) | (value >> (64 - rotate_amount)) +``` + +**Latency**: 2 cycles + +--- + +### 5.12 BITROTR (Rotate Right) + +**Encoding**: `BITROTR Rd, Rs1, Rs2` + +**Operation**: +``` +value = Rs1 +rotate_amount = Rs2 mod 64 +Rd ← (value >> rotate_amount) | (value << (64 - rotate_amount)) +``` + +**Latency**: 2 cycles + +--- + +## 6. Memory Semantics + +### 6.1 Byte Ordering (Little-Endian) + +All operations assume little-endian byte ordering: +``` +Memory layout (64-bit word at address 0x00): +Bit 0 at byte 0, bit 0 +Bit 7 at byte 0, bit 7 +Bit 8 at byte 1, bit 0 +Bit 63 at byte 7, bit 7 +``` + +### 6.2 Alignment Requirements + +- No alignment requirement for bit addresses +- Memory accesses are naturally granular to 64-bit words +- Unaligned word access: hardware loads containing word, extracts bits + +### 6.3 Virtual Memory Integration + +- Bit addresses translate to physical addresses via TLB +- `word_address = bit_address >> 6` (divide by 64) +- `word_address` is then translated through MMU +- Page boundaries: if `bit_address` and `bit_address+63` span different pages, may require two memory accesses +- Hardware handles transparently for single-bit operations +- Multi-word operations may trap if boundary conditions not met + +### 6.4 Cache Coherency + +- Follows processor's existing cache coherency protocol +- BITSTORE is treated as a regular write for coherency purposes +- No special coherency instruction needed + +--- + +## 7. Exception Behavior + +| Exception | Opcode | Trigger | Action | +|-----------|--------|---------|--------| +| PAGE_FAULT | Any memory op | Address not in TLB | Interrupt handler, retry | +| PROTECTION_FAULT | BITSTORE | Page not writable | Interrupt handler | +| INVALID_ADDRESS | Any memory op | Address > max physical | Trap | +| UNDEFINED_FIELD_WIDTH | BITFIELD | width = 0 or width > 64 | Trap | + +--- + +## 8. Status Register (Implicit) + +``` +Bit Name Meaning +0 ZF Zero Flag (set by BITTEST) +1 CF Carry Flag (unused in this ISA) +2-63 Reserved +``` + +--- + +## 9. Privilege Levels + +All bit-string instructions are **user-mode executable**. No privilege escalation. + +Exception: Bit addresses pointing to kernel memory may trigger protection faults. + +--- + +## 10. Formal Semantics (First-Order Logic) + +### Correctness Property: Single-Bit Load + +``` +∀ base, offset ∈ ℕ : + let eff_addr = base + offset + let word_idx = eff_addr >> 6 + let bit_idx = eff_addr mod 64 + BITLOAD base offset = (mem[word_idx] >> bit_idx) & 1 +``` + +### Correctness Property: Single-Bit Store + +``` +∀ base, offset, value ∈ ℕ : + let eff_addr = base + offset + let word_idx = eff_addr >> 6 + let bit_idx = eff_addr mod 64 + let mask = 1 << bit_idx + + after BITSTORE base offset value: + mem[word_idx] = (mem[word_idx] & ~mask) | ((value & 1) << bit_idx) + ∀ j ≠ word_idx : mem[j] unchanged +``` + +### Correctness Property: Field Extraction + +``` +∀ value, start, width ∈ ℕ, width ≤ 64 : + BITEXTRACT value start width = (value >> start) & ((1 << width) - 1) +``` + +--- + +## 11. Implementation Constraints + +- **Max memory size**: 2^64 bits (addressable) +- **Max single extraction**: 64 bits per instruction +- **Max rotation**: Full 64-bit rotation with any offset +- **Frequency**: Target 1+ GHz (1-3 cycle latency operations) +- **Power**: < 2mW per operation (estimated) +- **Area**: ~100-200k gates (estimates as sub-module) + +--- + +## 12. Future Extensions (Out of Scope) + +- SIMD bit-parallel operations +- GPU integration +- Vector bit strings +- Conditional bit operations +- Atomic bit operations (hardware locks) + diff --git a/bit_accelerator/rtl/behavioral_memory.sv b/bit_accelerator/rtl/behavioral_memory.sv new file mode 100644 index 0000000000000000000000000000000000000000..6b83e7e8e76d867769542dfd16543ffe883dca1c --- /dev/null +++ b/bit_accelerator/rtl/behavioral_memory.sv @@ -0,0 +1,102 @@ +// Behavioral Memory Model for Simulation +// Provides a simple synchronous memory with configurable read latency + +module behavioral_memory #( + parameter int ADDR_WIDTH = 64, + parameter int WORD_WIDTH = 64, + parameter int WORD_BYTES = WORD_WIDTH / 8, + parameter int READ_LATENCY = 2, + parameter string INIT_FILE = "" +) ( + input logic clk, + input logic reset, + + // Memory Request Interface + input logic req_valid, + output logic req_ready, + input logic [ADDR_WIDTH-1:0] addr, + input logic is_read, + input logic is_write, + input logic [WORD_BYTES-1:0] wstrb, + input logic [WORD_WIDTH-1:0] wdata, + + // Memory Response Interface + output logic rvalid, + output logic [WORD_WIDTH-1:0] rdata, + output logic fault +); + + // Memory array: 64K words (512KB total) + logic [WORD_WIDTH-1:0] memory [0:65535]; + + // Read pipeline for latency emulation + logic [READ_LATENCY-1:0] read_valid_pipe; + logic [WORD_WIDTH-1:0] read_data_pipe [0:READ_LATENCY-1]; + logic [ADDR_WIDTH-1:0] read_addr_pipe [0:READ_LATENCY-1]; + + initial begin + if (INIT_FILE != "") begin + $readmemh(INIT_FILE, memory); + end else begin + for (int i = 0; i < 65536; i++) begin + memory[i] = 64'h0; + end + end + end + + // Request handling + always_ff @(posedge clk or negedge reset) begin + if (!reset) begin + req_ready <= 1'b1; + rvalid <= 1'b0; + rdata <= 64'h0; + fault <= 1'b0; + for (int i = 0; i < READ_LATENCY; i++) begin + read_valid_pipe[i] <= 1'b0; + read_data_pipe[i] <= 64'h0; + end + end else begin + // Always ready to accept requests + req_ready <= 1'b1; + + // Handle write operations immediately + if (req_valid && is_write) begin + // Word-aligned write at address / 8 + logic [ADDR_WIDTH-1:0] word_addr = addr >> 3; + if (word_addr < 65536) begin + // Apply write strobes (byte enables) + for (int i = 0; i < WORD_BYTES; i++) begin + if (wstrb[i]) begin + memory[word_addr][i*8 +: 8] <= wdata[i*8 +: 8]; + end + end + end + end + + // Handle read operations (pipeline) + read_valid_pipe[0] <= req_valid && is_read; + if (req_valid && is_read) begin + logic [ADDR_WIDTH-1:0] word_addr = addr >> 3; + read_addr_pipe[0] <= word_addr; + if (word_addr < 65536) begin + read_data_pipe[0] <= memory[word_addr]; + end else begin + read_data_pipe[0] <= 64'hDEADBEEFDEADBEEF; // Fault marker + end + end + + // Pipeline shift for READ_LATENCY cycles + for (int i = 1; i < READ_LATENCY; i++) begin + read_valid_pipe[i] <= read_valid_pipe[i-1]; + read_data_pipe[i] <= read_data_pipe[i-1]; + read_addr_pipe[i] <= read_addr_pipe[i-1]; + end + + // Output from pipeline + rvalid <= read_valid_pipe[READ_LATENCY-1]; + rdata <= read_data_pipe[READ_LATENCY-1]; + fault <= 1'b0; // No faults in this simple model + end + end + +endmodule : behavioral_memory diff --git a/bit_accelerator/rtl/bit_accelerator.sv b/bit_accelerator/rtl/bit_accelerator.sv new file mode 100644 index 0000000000000000000000000000000000000000..9dba1f4118affdcae93ed43628636d2c8f99af97 --- /dev/null +++ b/bit_accelerator/rtl/bit_accelerator.sv @@ -0,0 +1,210 @@ +// Top-Level Bit-String Hardware Accelerator +// Implements: BIT_BASE + BIT_OFFSET addressing with atomic read-modify-write + +module bit_accelerator #( + parameter int ADDR_WIDTH = 64, + parameter int WORD_WIDTH = 64, + parameter int WORD_BYTES = WORD_WIDTH / 8 +) ( + input logic clk, + input logic reset, + + // Operation Interface + input logic op_valid, + output logic op_ready, + input logic [63:0] base_address, + input logic [63:0] bit_offset, + input logic [2:0] operation, // 3'b000=GET, 3'b001=TEST, 3'b010=SET, 3'b011=CLEAR, 3'b100=TOGGLE + + // Response Interface + output logic result_valid, + output logic result_bit, + output logic error, + + // Memory Interface (AXI-like, word-addressed) + output logic mem_req_valid, + input logic mem_req_ready, + output logic [ADDR_WIDTH-1:0] mem_addr, + output logic mem_read, + output logic mem_write, + output logic [WORD_BYTES-1:0] mem_wstrb, + output logic [WORD_WIDTH-1:0] mem_wdata, + + input logic mem_rvalid, + input logic [WORD_WIDTH-1:0] mem_rdata, + input logic mem_fault +); + + // ===== State Machine ===== + typedef enum logic [2:0] { + ST_IDLE, + ST_READ_REQUEST, + ST_READ_WAIT, + ST_MODIFY, + ST_WRITE_REQUEST, + ST_WRITE_WAIT, + ST_DONE + } state_t; + + state_t current_state, next_state; + + // ===== Internal Registers ===== + logic [63:0] base_addr_reg, bit_offset_reg; + logic [2:0] operation_reg; + logic [63:0] absolute_bit_address; + logic [57:0] word_address; // byte address / 8 + logic [5:0] bit_index_in_word; // bit position within 64-bit word + logic [63:0] read_word; + logic [63:0] modified_word; + logic is_modify_op; // SET, CLEAR, TOGGLE + + // ===== Combinational: Address Calculation ===== + + // absolute_bit_address = (base_address × 8) + bit_offset + logic [127:0] temp_addr; + assign temp_addr = {1'b0, base_addr_reg} * 8 + {1'b0, bit_offset_reg}; + + // Truncate to 64-bit (overflow handling) + assign absolute_bit_address = temp_addr[63:0]; + + // word_address = absolute_bit_address / 64 (right-shift by 6) + assign word_address = absolute_bit_address[63:6]; + + // bit_index_in_word = absolute_bit_address % 64 + assign bit_index_in_word = absolute_bit_address[5:0]; + + // Convert word address to byte address for memory interface + // word_address is already in 64-bit word units, multiply by 8 for byte address + assign mem_addr = {word_address, 3'b000}; // Shift left by 3 (multiply by 8) + + // Check if operation is a modify operation + assign is_modify_op = (operation_reg == 3'b010) || // SET + (operation_reg == 3'b011) || // CLEAR + (operation_reg == 3'b100); // TOGGLE + + // ===== Submodules ===== + + bit_extractor bit_extractor_inst ( + .word_data (read_word), + .bit_index (bit_index_in_word), + .field_width (6'h1), + .is_multibit (1'b0), + .result () // Not used directly, result_bit comes from shift+AND + ); + + bit_modifier bit_modifier_inst ( + .word_data (read_word), + .bit_index (bit_index_in_word), + .operation (operation_reg), + .result (modified_word) + ); + + // ===== Result Extraction (Combinational) ===== + logic [63:0] shifted_for_result; + assign shifted_for_result = read_word >> bit_index_in_word; + + // ===== State Machine: Sequential ===== + + always_ff @(posedge clk or negedge reset) begin + if (!reset) begin + current_state <= ST_IDLE; + base_addr_reg <= 64'h0; + bit_offset_reg <= 64'h0; + operation_reg <= 3'h0; + read_word <= 64'h0; + result_valid <= 1'b0; + result_bit <= 1'b0; + error <= 1'b0; + end else begin + current_state <= next_state; + result_valid <= 1'b0; + error <= 1'b0; + + if (op_valid && op_ready) begin + base_addr_reg <= base_address; + bit_offset_reg <= bit_offset; + operation_reg <= operation; + end + + if (mem_rvalid && current_state == ST_READ_WAIT) begin + read_word <= mem_rdata; + end + + if (mem_fault) begin + error <= 1'b1; + result_valid <= 1'b1; + end + end + end + + // ===== State Machine: Combinational Next-State Logic ===== + + always_comb begin + next_state = current_state; + op_ready = 1'b0; + mem_req_valid = 1'b0; + mem_read = 1'b0; + mem_write = 1'b0; + mem_wstrb = 8'h00; + mem_wdata = 64'h0; + + case (current_state) + ST_IDLE: begin + op_ready = 1'b1; + if (op_valid) begin + next_state = ST_READ_REQUEST; + end + end + + ST_READ_REQUEST: begin + mem_req_valid = 1'b1; + mem_read = 1'b1; + if (mem_req_ready) begin + next_state = ST_READ_WAIT; + end + end + + ST_READ_WAIT: begin + if (mem_rvalid) begin + if (is_modify_op) begin + next_state = ST_MODIFY; + end else begin + next_state = ST_DONE; + end + end + if (mem_fault) begin + next_state = ST_DONE; + end + end + + ST_MODIFY: begin + next_state = ST_WRITE_REQUEST; + end + + ST_WRITE_REQUEST: begin + mem_req_valid = 1'b1; + mem_write = 1'b1; + mem_wstrb = 8'hFF; // All 8 bytes of the 64-bit word + mem_wdata = modified_word; + if (mem_req_ready) begin + next_state = ST_WRITE_WAIT; + end + end + + ST_WRITE_WAIT: begin + // For write operations, we don't wait for a write response in this simple model + // Assume write completes immediately after acceptance + next_state = ST_DONE; + end + + ST_DONE: begin + result_valid = 1'b1; + result_bit = shifted_for_result[0]; + next_state = ST_IDLE; + end + + default: next_state = ST_IDLE; + endcase + end + +endmodule : bit_accelerator diff --git a/bit_accelerator/rtl/bit_accelerator_pkg.sv b/bit_accelerator/rtl/bit_accelerator_pkg.sv new file mode 100644 index 0000000000000000000000000000000000000000..fe95e769b136b441d4e7fde7909ce4ab62eb64fa --- /dev/null +++ b/bit_accelerator/rtl/bit_accelerator_pkg.sv @@ -0,0 +1,140 @@ +// Bit Accelerator Package - Type Definitions and Constants + +package bit_accelerator_pkg; + + // Opcode Definitions + typedef enum logic [5:0] { + BITLOAD = 6'h01, + BITSTORE = 6'h02, + BITFIELD = 6'h03, + BITTEST = 6'h04, + BITCOUNT = 6'h05, + BITSCANFWD = 6'h06, + BITSCANREV = 6'h07, + LOADIMM64 = 6'h08, + BITEXTRACT = 6'h09, + BITINSERT = 6'h0A, + BITROTL = 6'h0B, + BITROTR = 6'h0C + } opcode_t; + + // Register Specifier (5 bits) + typedef logic [4:0] reg_addr_t; + + // Immediate Values + typedef logic [17:0] imm18_t; + typedef logic [52:0] imm53_t; + + // 64-bit Word/Address + typedef logic [63:0] word64_t; + + // Bit Index (0-63) + typedef logic [5:0] bit_idx_t; + + // Word Index (for 64-bit memory word, within addressable space) + typedef logic [57:0] word_idx_t; + + // Effective Bit Address + typedef logic [63:0] eff_addr_t; + + // Instruction Format + typedef struct packed { + opcode_t opcode; // [5:0] + reg_addr_t rd; // [10:6] + reg_addr_t rs1; // [15:11] + reg_addr_t rs2; // [20:16] + logic [43:0] unused; // [63:21] + } instr_r_t; + + typedef struct packed { + opcode_t opcode; // [5:0] + reg_addr_t rd; // [10:6] + reg_addr_t rs1; // [15:11] + imm18_t imm; // [33:16] + logic [29:0] unused; // [63:34] + } instr_i_t; + + typedef struct packed { + opcode_t opcode; // [5:0] + reg_addr_t rd; // [10:6] + imm53_t imm; // [58:11] + logic [4:0] unused; // [63:59] + } instr_l_t; + + // Control Signals + typedef struct packed { + logic enable_addr_gen; + logic enable_mem_load; + logic enable_mem_store; + logic enable_shift; + logic enable_extract; + logic enable_writeback; + logic shift_direction; // 0=right, 1=left + logic rotate_mode; // 0=shift, 1=rotate + logic is_signed; + bit_idx_t field_width; + } ctrl_signals_t; + + // Exception Codes + typedef enum logic [3:0] { + EXC_NONE = 4'h0, + EXC_PAGE_FAULT = 4'h1, + EXC_PROTECTION = 4'h2, + EXC_INVALID_ADDR = 4'h3, + EXC_INVALID_WIDTH = 4'h4 + } exception_t; + + // Pipeline Stage Signals + typedef struct packed { + opcode_t opcode; + reg_addr_t rd; + word64_t rs1_data; + word64_t rs2_data; + word64_t rs3_data; + ctrl_signals_t ctrl; + exception_t exc; + } stage_s1_t; + + typedef struct packed { + opcode_t opcode; + reg_addr_t rd; + word64_t rs3_data; + eff_addr_t eff_addr; + ctrl_signals_t ctrl; + exception_t exc; + } stage_s2_t; + + typedef struct packed { + opcode_t opcode; + reg_addr_t rd; + word64_t rs3_data; + word_idx_t word_idx; + bit_idx_t bit_idx; + ctrl_signals_t ctrl; + exception_t exc; + } stage_s3_t; + + typedef struct packed { + opcode_t opcode; + reg_addr_t rd; + word64_t memory_word; + bit_idx_t bit_idx; + ctrl_signals_t ctrl; + exception_t exc; + } stage_s4_t; + + typedef struct packed { + opcode_t opcode; + reg_addr_t rd; + word64_t extracted_value; + exception_t exc; + } stage_s5_t; + + // Control Status Register + typedef struct packed { + logic zf; // Zero flag (from BITTEST) + logic cf; // Carry flag (unused) + logic [61:0] reserved; + } csr_t; + +endpackage : bit_accelerator_pkg diff --git a/bit_accelerator/rtl/bit_accelerator_v2.sv b/bit_accelerator/rtl/bit_accelerator_v2.sv new file mode 100644 index 0000000000000000000000000000000000000000..b57ef9e54d8bbf87fe0a1685836f3561b44b6ccb --- /dev/null +++ b/bit_accelerator/rtl/bit_accelerator_v2.sv @@ -0,0 +1,209 @@ +// Bit-String Hardware Accelerator v2 +// Corrected implementation with explicit write completion semantics +// Reset and in-flight operation cancellation + +module bit_accelerator_v2 #( + parameter int ADDR_WIDTH = 64, + parameter int WORD_WIDTH = 64, + parameter int WORD_BYTES = WORD_WIDTH / 8 +) ( + input logic clk, + input logic reset, + + // Operation Interface + input logic op_valid, + output logic op_ready, + input logic [63:0] base_address, + input logic [63:0] bit_offset, + input logic [2:0] operation, // 3'b000=GET, 3'b001=TEST, 3'b010=SET, 3'b011=CLEAR, 3'b100=TOGGLE + + // Result Interface (one-cycle pulse) + output logic result_valid, + output logic result_bit, + output logic error, + + // Memory Interface (AXI-like handshaking) + output logic mem_valid, // Read or write valid + output logic mem_write, // 0=read, 1=write + output logic [ADDR_WIDTH-1:0] mem_addr, // Byte address (8-byte aligned) + output logic [WORD_WIDTH-1:0] mem_wdata, // Write data + output logic [WORD_BYTES-1:0] mem_wstrb, // Write strobes + input logic mem_ready, // Ready to accept + + input logic mem_rvalid, // Read valid + input logic [WORD_WIDTH-1:0] mem_rdata, // Read data + input logic mem_fault // Read fault +); + + // ===== State Machine ===== + typedef enum logic [3:0] { + ST_IDLE, + ST_READ_REQUEST, + ST_READ_WAIT, + ST_MODIFY, + ST_WRITE_REQUEST, + ST_WRITE_WAIT, + ST_RESULT + } state_t; + + state_t current_state, next_state; + + // ===== Stored Operation State ===== + logic [63:0] base_addr_reg, bit_offset_reg; + logic [2:0] operation_reg; + logic is_modify_op; + + // ===== Calculated Address Components ===== + logic [63:0] absolute_bit_address; + logic [57:0] word_address; + logic [5:0] bit_index_in_word; + + // ===== Data Path ===== + logic [63:0] read_word; + logic [63:0] modified_word; + logic result_data; + + // ===== Address Calculation (Combinational) ===== + assign absolute_bit_address = (base_addr_reg << 3) + bit_offset_reg; + assign word_address = absolute_bit_address[63:6]; + assign bit_index_in_word = absolute_bit_address[5:0]; + assign mem_addr = ADDR_WIDTH'({word_address, 3'b000}); // word address -> byte address (zero-extended) + + // Check if operation is a modify operation + assign is_modify_op = (operation_reg == 3'b010) || // SET + (operation_reg == 3'b011) || // CLEAR + (operation_reg == 3'b100); // TOGGLE + + + // ===== Bit Modification ===== + always_comb begin + case (operation_reg) + 3'b010: modified_word = read_word | (64'h1 << bit_index_in_word); // SET + 3'b011: modified_word = read_word & ~(64'h1 << bit_index_in_word); // CLEAR + 3'b100: modified_word = read_word ^ (64'h1 << bit_index_in_word); // TOGGLE + default: modified_word = read_word; + endcase + end + + // ===== Result Extraction ===== + assign result_data = read_word[bit_index_in_word]; // bit extraction + + // Result is a one-cycle pulse, combinational from ST_RESULT + assign result_valid = (current_state == ST_RESULT); + assign result_bit = (current_state == ST_RESULT) ? result_data : 1'b0; + + // ===== State Machine: Sequential ===== + always_ff @(posedge clk) begin + if (reset) begin + current_state <= ST_IDLE; + base_addr_reg <= 64'h0; + bit_offset_reg <= 64'h0; + operation_reg <= 3'h0; + read_word <= 64'h0; + error <= 1'b0; + end else begin + current_state <= next_state; + + // Capture operation on acceptance + if (op_valid && op_ready) begin + base_addr_reg <= base_address; + bit_offset_reg <= bit_offset; + operation_reg <= operation; + end + + // Capture read data + if (mem_rvalid && current_state == ST_READ_WAIT) begin + read_word <= mem_rdata; + end + + // Error status: cleared on accept, set on read fault while waiting + if (op_valid && op_ready) begin + error <= 1'b0; + end else if (mem_fault && current_state == ST_READ_WAIT) begin + error <= 1'b1; + end + end + end + + // ===== State Machine: Combinational Next-State Logic ===== + always_comb begin + next_state = current_state; + op_ready = 1'b0; + mem_valid = 1'b0; + mem_write = 1'b0; + mem_wdata = 64'h0; + mem_wstrb = 8'h00; + + case (current_state) + ST_IDLE: begin + op_ready = 1'b1; + if (op_valid) begin + next_state = ST_READ_REQUEST; + end + end + + ST_READ_REQUEST: begin + mem_valid = 1'b1; + mem_write = 1'b0; + if (mem_ready) begin + next_state = ST_READ_WAIT; + end + // else stay in READ_REQUEST (stall) + end + + ST_READ_WAIT: begin + if (mem_rvalid) begin + if (is_modify_op) begin + next_state = ST_MODIFY; + end else begin + // BIT_GET or BIT_TEST: go directly to result + next_state = ST_RESULT; + end + end + if (mem_fault) begin + next_state = ST_RESULT; + end + end + + ST_MODIFY: begin + // One cycle to calculate modified value + next_state = ST_WRITE_REQUEST; + end + + ST_WRITE_REQUEST: begin + mem_valid = 1'b1; + mem_write = 1'b1; + mem_wdata = modified_word; + mem_wstrb = 8'hFF; // All bytes + if (mem_ready) begin + next_state = ST_WRITE_WAIT; + end + // else stay in WRITE_REQUEST (stall) + end + + ST_WRITE_WAIT: begin + // Write accepted, no separate write-response in this model + // Go to result state + next_state = ST_RESULT; + end + + ST_RESULT: begin + // result_valid asserted this cycle + // Next cycle return to IDLE + next_state = ST_IDLE; + end + + default: next_state = ST_IDLE; + endcase + + // A request presented in a cycle where reset is asserted is never issued, + // so reset cannot race with a write handshake at the same clock edge. + if (reset) begin + mem_valid = 1'b0; + mem_write = 1'b0; + mem_wdata = 64'h0; + mem_wstrb = 8'h00; + end + end + +endmodule : bit_accelerator_v2 diff --git a/bit_accelerator/rtl/bit_address_generator.sv b/bit_accelerator/rtl/bit_address_generator.sv new file mode 100644 index 0000000000000000000000000000000000000000..e21778b0f4acbcb6c53c1ffcdda3e53872d0f868 --- /dev/null +++ b/bit_accelerator/rtl/bit_address_generator.sv @@ -0,0 +1,31 @@ +// Bit Address Generator +// Computes: effective_bit_address = base_bit_address + bit_offset +// Implementation: 64-bit Kogge-Stone parallel-prefix adder + +module bit_address_generator ( + input logic clk, + input logic rst_n, + + // Operands + input logic [63:0] base_addr, + input logic [63:0] bit_offset, + + // Control + input logic enable, + + // Output + output logic [63:0] eff_bit_addr, + output logic carry_out +); + + import bit_accelerator_pkg::*; + + // Stage 1: Ripple-carry adder (simplified for synthesis) + // For timing closure, use built-in + operator which synthesizes to optimized adder + logic [64:0] sum; + + assign sum = {1'b0, base_addr} + {1'b0, bit_offset}; + assign eff_bit_addr = sum[63:0]; + assign carry_out = sum[64]; + +endmodule : bit_address_generator diff --git a/bit_accelerator/rtl/bit_extractor.sv b/bit_accelerator/rtl/bit_extractor.sv new file mode 100644 index 0000000000000000000000000000000000000000..b83702be46c49b24975f34bdef93044976f97c28 --- /dev/null +++ b/bit_accelerator/rtl/bit_extractor.sv @@ -0,0 +1,36 @@ +// Bit Extractor - Single-bit and multi-bit extraction from a 64-bit word + +module bit_extractor ( + input logic [63:0] word_data, + input logic [5:0] bit_index, + input logic [5:0] field_width, // For multi-bit extraction (1-64) + input logic is_multibit, + + output logic [63:0] result +); + + logic [63:0] shifted_word; + logic [63:0] mask; + + // Barrel shifter: rotate right by bit_index positions + // This is combinational and synthesizes to mux tree + assign shifted_word = word_data >> bit_index; + + // Generate mask for field width + // For width=1: mask = 64'h0000_0000_0000_0001 + // For width=2: mask = 64'h0000_0000_0000_0003 + // For width=64: mask = 64'hFFFF_FFFF_FFFF_FFFF + + always_comb begin + if (field_width == 6'h0) + mask = 64'h0; + else if (field_width == 6'd64) + mask = 64'hFFFFFFFFFFFFFFFF; + else + mask = (64'h1 << field_width) - 1'b1; + end + + // Extract field + assign result = is_multibit ? (shifted_word & mask) : {63'h0, shifted_word[0]}; + +endmodule : bit_extractor diff --git a/bit_accelerator/rtl/bit_modifier.sv b/bit_accelerator/rtl/bit_modifier.sv new file mode 100644 index 0000000000000000000000000000000000000000..eddc6e644cf77b50411550afd4118e54d06ca56c --- /dev/null +++ b/bit_accelerator/rtl/bit_modifier.sv @@ -0,0 +1,29 @@ +// Bit Modifier - Set, clear, and toggle operations on a 64-bit word + +module bit_modifier ( + input logic [63:0] word_data, + input logic [5:0] bit_index, + input logic [2:0] operation, // SET=3'b010, CLEAR=3'b011, TOGGLE=3'b100 + + output logic [63:0] result +); + + logic [63:0] bit_mask; + + // Generate mask with single bit set at bit_index + // For bit_index=0: mask = 64'h0000_0000_0000_0001 + // For bit_index=63: mask = 64'h8000_0000_0000_0000 + + assign bit_mask = 64'h1 << bit_index; + + // Apply operation + always_comb begin + case (operation) + 3'b010: result = word_data | bit_mask; // BIT_SET + 3'b011: result = word_data & ~bit_mask; // BIT_CLEAR + 3'b100: result = word_data ^ bit_mask; // BIT_TOGGLE + default: result = word_data; // No-op + endcase + end + +endmodule : bit_modifier diff --git a/bit_accelerator/run_v2_sim.sh b/bit_accelerator/run_v2_sim.sh new file mode 100644 index 0000000000000000000000000000000000000000..672b9356b90fcd89e86d3d580018e0cb6aba1a37 --- /dev/null +++ b/bit_accelerator/run_v2_sim.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Build and run the cycle-accurate v2 testbench with Verilator (>= 5.0). +# Exits non-zero if any check fails ($fatal in the testbench). +set -euo pipefail +cd "$(dirname "$0")" +OUT=${OUT:-build/verilator} +rm -rf "$OUT" && mkdir -p "$OUT" +verilator --binary --timing -Wall -Wno-fatal --top-module tb_bit_accelerator_v2 \ + -Mdir "$OUT" rtl/bit_accelerator_v2.sv testbenches/tb_bit_accelerator_v2.sv >/dev/null +"$OUT"/Vtb_bit_accelerator_v2 diff --git a/bit_accelerator/scripts/prove.sh b/bit_accelerator/scripts/prove.sh new file mode 100644 index 0000000000000000000000000000000000000000..476a9c01e0a6dcf41d39caa1b5504be7aaa2409f --- /dev/null +++ b/bit_accelerator/scripts/prove.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Prove every goal of the given Why3 files with one prover. +# Usage: scripts/prove.sh [-P prover] [-t seconds] [-L dir] file.mlw... +# Goals are split with the split_vc transformation before proving. +# Prints one line per goal and exits non-zero unless every goal is Valid. +set -euo pipefail +prover=z3; timelimit=30; loadpath=() +while getopts "P:t:L:" opt; do + case "$opt" in + P) prover=$OPTARG ;; + t) timelimit=$OPTARG ;; + L) loadpath+=(-L "$OPTARG") ;; + *) exit 2 ;; + esac +done +shift $((OPTIND - 1)) +[ $# -gt 0 ] || { echo "usage: $0 [-P prover] [-t s] [-L dir] file.mlw..." >&2; exit 2; } +command -v why3 >/dev/null || { echo "why3 not found" >&2; exit 2; } + +total=0; bad=0 +for f in "$@"; do + out=$(why3 prove "${loadpath[@]}" -a split_vc -P "$prover" -t "$timelimit" "$f" 2>&1) || true + results=$(printf '%s\n' "$out" | awk ' + /^Goal / { goal = $2; sub(/\.$/, "", goal) } + /^Sub-goal / { kind = $2; goal = $0; sub(/.* of goal /, "", goal); sub(/\.$/, "", goal); goal = goal ":" kind } + /Prover result is:/ { r = $0; sub(/.*Prover result is: /, "", r); print goal "\t" r }') + if [ -z "$results" ]; then + echo "ERROR $f: no goals reported"; printf '%s\n' "$out" | head -20; bad=$((bad + 1)); continue + fi + while IFS=$'\t' read -r goal res; do + total=$((total + 1)) + case "$res" in + Valid*) printf ' ok %-40s %s\n' "$(basename "$f"):$goal" "$res" ;; + *) printf ' FAIL %-40s %s\n' "$(basename "$f"):$goal" "$res"; bad=$((bad + 1)) ;; + esac + done <<< "$results" +done +echo "why3/$prover: $((total - bad))/$total goals valid" +[ "$bad" -eq 0 ] diff --git a/bit_accelerator/testbenches/tb_bit_accelerator.sv b/bit_accelerator/testbenches/tb_bit_accelerator.sv new file mode 100644 index 0000000000000000000000000000000000000000..81a50ac6fb793d9ae6c8fd739f3a727eeec8debe --- /dev/null +++ b/bit_accelerator/testbenches/tb_bit_accelerator.sv @@ -0,0 +1,322 @@ +// Testbench for Bit-String Hardware Accelerator +// Comprehensive functional verification of all operations and edge cases + +module tb_bit_accelerator (); + + // Parameters + localparam int ADDR_WIDTH = 64; + localparam int WORD_WIDTH = 64; + localparam int WORD_BYTES = WORD_WIDTH / 8; + localparam int READ_LATENCY = 2; + + // Clock and Reset + logic clk; + logic reset; + + // DUT: Operation Interface + logic op_valid; + logic op_ready; + logic [63:0] base_address; + logic [63:0] bit_offset; + logic [2:0] operation; + + // DUT: Response Interface + logic result_valid; + logic result_bit; + logic error; + + // DUT: Memory Interface + logic mem_req_valid; + logic mem_req_ready; + logic [ADDR_WIDTH-1:0] mem_addr; + logic mem_read; + logic mem_write; + logic [WORD_BYTES-1:0] mem_wstrb; + logic [WORD_WIDTH-1:0] mem_wdata; + logic mem_rvalid; + logic [WORD_WIDTH-1:0] mem_rdata; + logic mem_fault; + + // DUT Instantiation + bit_accelerator dut ( + .clk (clk), + .reset (reset), + .op_valid (op_valid), + .op_ready (op_ready), + .base_address(base_address), + .bit_offset (bit_offset), + .operation (operation), + .result_valid(result_valid), + .result_bit (result_bit), + .error (error), + .mem_req_valid(mem_req_valid), + .mem_req_ready(mem_req_ready), + .mem_addr (mem_addr), + .mem_read (mem_read), + .mem_write (mem_write), + .mem_wstrb (mem_wstrb), + .mem_wdata (mem_wdata), + .mem_rvalid (mem_rvalid), + .mem_rdata (mem_rdata), + .mem_fault (mem_fault) + ); + + // Memory Model + behavioral_memory #( + .ADDR_WIDTH (ADDR_WIDTH), + .WORD_WIDTH (WORD_WIDTH), + .READ_LATENCY(READ_LATENCY) + ) memory_model ( + .clk (clk), + .reset (reset), + .req_valid(mem_req_valid), + .req_ready(mem_req_ready), + .addr (mem_addr), + .is_read (mem_read), + .is_write(mem_write), + .wstrb (mem_wstrb), + .wdata (mem_wdata), + .rvalid (mem_rvalid), + .rdata (mem_rdata), + .fault (mem_fault) + ); + + // Clock generation (10ns period = 100 MHz) + initial begin + clk = 1'b0; + forever #5 clk = ~clk; + end + + // Test vector structure + typedef struct packed { + logic [63:0] base_addr; + logic [63:0] bit_offset; + logic [2:0] op; + logic [63:0] expected_word; + logic expected_bit; + string test_name; + } test_vec_t; + + // Test counters + int test_count = 0; + int pass_count = 0; + int fail_count = 0; + + // Helper tasks + + task automatic write_memory(logic [63:0] byte_addr, logic [63:0] data); + logic [63:0] word_addr = byte_addr >> 3; + // Direct write to memory model's memory array + // (In real simulation, use the memory interface) + endtask + + task automatic init_memory(); + // Initialize memory with test pattern + // Word at byte address 0x00: 64'h0123456789ABCDEF + // Word at byte address 0x08: 64'hFEDCBA9876543210 + // Word at byte address 0x10: 64'hAAAAAAAAAAAAAAAA + // Word at byte address 0x18: 64'h5555555555555555 + + automatic logic [63:0] init_data[4]; + init_data[0] = 64'h0123456789ABCDEF; + init_data[1] = 64'hFEDCBA9876543210; + init_data[2] = 64'hAAAAAAAAAAAAAAAA; + init_data[3] = 64'h5555555555555555; + + // Write to memory via operation (requires memory to be functional) + // For now, directly access memory_model.memory + for (int i = 0; i < 4; i++) begin + memory_model.memory[i] = init_data[i]; + end + endtask + + task automatic run_operation( + input logic [63:0] base_addr, + input logic [63:0] bit_off, + input logic [2:0] op, + output logic result_bit_out, + output logic error_out + ); + logic done = 1'b0; + int cycles = 0; + + // Assert operation request + op_valid <= 1'b1; + base_address <= base_addr; + bit_offset <= bit_off; + operation <= op; + + // Wait for acceptance + @(posedge clk); + while (!op_ready && cycles < 100) begin + @(posedge clk); + cycles++; + end + + op_valid <= 1'b0; + + // Wait for result + cycles = 0; + while (!result_valid && cycles < 200) begin + @(posedge clk); + cycles++; + end + + if (result_valid) begin + result_bit_out = result_bit; + error_out = error; + end else begin + result_bit_out = 1'bX; + error_out = 1'b1; + end + endtask + + task automatic test_bit_get(logic [63:0] base_addr, logic [63:0] bit_off, logic expected_bit, string test_name); + logic result_bit_out; + logic error_out; + + $display("[%0d] Testing BIT_GET: %s", test_count, test_name); + run_operation(base_addr, bit_off, 3'b000, result_bit_out, error_out); + + if (error_out) begin + $display(" FAIL: Operation returned error"); + fail_count++; + end else if (result_bit_out === expected_bit) begin + $display(" PASS: Got expected bit value %b", expected_bit); + pass_count++; + end else begin + $display(" FAIL: Expected %b, got %b", expected_bit, result_bit_out); + fail_count++; + end + + test_count++; + @(posedge clk); + endtask + + task automatic test_bit_set(logic [63:0] base_addr, logic [63:0] bit_off, string test_name); + logic result_bit_out; + logic error_out; + + $display("[%0d] Testing BIT_SET: %s", test_count, test_name); + run_operation(base_addr, bit_off, 3'b010, result_bit_out, error_out); + + if (error_out) begin + $display(" FAIL: Operation returned error"); + fail_count++; + end else begin + $display(" PASS: BIT_SET completed, result=%b", result_bit_out); + pass_count++; + end + + test_count++; + @(posedge clk); + endtask + + task automatic test_bit_clear(logic [63:0] base_addr, logic [63:0] bit_off, string test_name); + logic result_bit_out; + logic error_out; + + $display("[%0d] Testing BIT_CLEAR: %s", test_count, test_name); + run_operation(base_addr, bit_off, 3'b011, result_bit_out, error_out); + + if (error_out) begin + $display(" FAIL: Operation returned error"); + fail_count++; + end else begin + $display(" PASS: BIT_CLEAR completed, result=%b", result_bit_out); + pass_count++; + end + + test_count++; + @(posedge clk); + endtask + + // Main Testbench + initial begin + // Initialize signals + op_valid = 1'b0; + base_address = 64'h0; + bit_offset = 64'h0; + operation = 3'h0; + reset = 1'b0; + + // Reset sequence + #20; + reset = 1'b1; + #20; + + $display("\n========================================"); + $display(" BIT ACCELERATOR FUNCTIONAL TEST "); + $display("========================================\n"); + + // Initialize memory + init_memory(); + + // Test 1: BIT_GET at various offsets (single word) + $display("\n--- Test Group 1: Single-Word Bit Extraction ---"); + + // Word 0 = 0x0123456789ABCDEF + // Bit 0 (LSB of byte 0): 1 + test_bit_get(64'h00, 64'h0, 1'b1, "bit 0 of word 0"); + + // Bit 1: 1 + test_bit_get(64'h00, 64'h1, 1'b1, "bit 1 of word 0"); + + // Bit 7: 0 + test_bit_get(64'h00, 64'h7, 1'b0, "bit 7 of word 0"); + + // Bit 8 (LSB of byte 1): 1 + test_bit_get(64'h00, 64'h8, 1'b1, "bit 8 of word 0"); + + // Bit 31: 0 + test_bit_get(64'h00, 64'h1F, 1'b0, "bit 31 of word 0"); + + // Bit 32: 1 + test_bit_get(64'h00, 64'h20, 1'b1, "bit 32 of word 0"); + + // Bit 63 (MSB of word): 0 + test_bit_get(64'h00, 64'h3F, 1'b0, "bit 63 of word 0"); + + // Test 2: Cross-word boundary + $display("\n--- Test Group 2: Cross-Word Boundary ---"); + + // Word 1 = 0xFEDCBA9876543210 + // Bit 64 (bit 0 of word 1): 0 + test_bit_get(64'h00, 64'h40, 1'b0, "bit 64 (bit 0 of word 1)"); + + // Bit 65: 0 + test_bit_get(64'h00, 64'h41, 1'b0, "bit 65 (bit 1 of word 1)"); + + // Test 3: Base address offset + $display("\n--- Test Group 3: Non-Zero Base Address ---"); + + // Base address = 0x08 (word 1) + // Bit offset = 0 (bit 0 of word 1) + test_bit_get(64'h08, 64'h0, 1'b0, "base=0x08, offset=0"); + + // Test 4: Modify operations + $display("\n--- Test Group 4: Modify Operations ---"); + + test_bit_set(64'h10, 64'h0, "BIT_SET at bit 0 of word 2"); + test_bit_clear(64'h10, 64'h0, "BIT_CLEAR at bit 0 of word 2"); + + // Summary + $display("\n========================================"); + $display(" TEST SUMMARY "); + $display("========================================"); + $display("Total Tests: %0d", test_count); + $display("Passed: %0d", pass_count); + $display("Failed: %0d", fail_count); + + if (fail_count == 0) begin + $display("\n✓ All tests PASSED!"); + end else begin + $display("\n✗ %0d tests FAILED!", fail_count); + end + $display("========================================\n"); + + #100; + $finish; + end + +endmodule : tb_bit_accelerator diff --git a/bit_accelerator/testbenches/tb_bit_accelerator_v2.sv b/bit_accelerator/testbenches/tb_bit_accelerator_v2.sv new file mode 100644 index 0000000000000000000000000000000000000000..5294d5d92f111aefb34c94e09a40878f490d5f04 --- /dev/null +++ b/bit_accelerator/testbenches/tb_bit_accelerator_v2.sv @@ -0,0 +1,311 @@ +// Cycle-accurate testbench for bit_accelerator_v2. +// Inputs are driven on negedge; every posedge logs the pre-edge value of each +// signal, so log[c] is exactly what the DUT/memory saw during cycle c. +// Spec (1-cycle-latency memory): accept@a, READ_REQ@a+1, READ_RESP@a+2, RESULT_VALID@a+3. + +module tb_bit_accelerator_v2; + + localparam int MAXC = 4096; + + logic clk = 1'b0; + logic reset; + logic op_valid, op_ready; + logic [63:0] base_address, bit_offset; + logic [2:0] operation; + logic result_valid, result_bit, error; + logic mem_valid, mem_write, mem_ready; + logic [63:0] mem_addr, mem_wdata, mem_rdata; + logic [7:0] mem_wstrb; + logic mem_rvalid = 1'b0, mem_fault = 1'b0; + + bit_accelerator_v2 dut (.*); + + always #5 clk = ~clk; + + // ---------------- memory model (1-cycle read latency) ---------------- + logic [63:0] memory [0:1023]; + bit stall_en; + int stall_lo, stall_hi; + int cyc = 0; + + assign mem_ready = !(stall_en && cyc >= stall_lo && cyc <= stall_hi); + bit fault_next_read = 1'b0; // answer the next accepted read with mem_fault + + always_ff @(posedge clk) begin + mem_rvalid <= 1'b0; + mem_fault <= 1'b0; + if (mem_valid && mem_ready) begin + if (mem_write) begin + for (int i = 0; i < 8; i++) + if (mem_wstrb[i]) memory[mem_addr[12:3]][i*8 +: 8] <= mem_wdata[i*8 +: 8]; + end else if (fault_next_read) begin + mem_fault <= 1'b1; + fault_next_read <= 1'b0; + end else begin + mem_rvalid <= 1'b1; + mem_rdata <= memory[mem_addr[12:3]]; + end + end + end + + // ---------------- per-cycle log ---------------- + logic l_opv [0:MAXC-1], l_opr [0:MAXC-1]; + logic l_mv [0:MAXC-1], l_mw [0:MAXC-1], l_mr [0:MAXC-1]; + logic l_rv [0:MAXC-1], l_res [0:MAXC-1], l_bit [0:MAXC-1], l_err [0:MAXC-1]; + logic [63:0] l_wd [0:MAXC-1]; + + always @(posedge clk) begin + l_opv[cyc] = op_valid; l_opr[cyc] = op_ready; + l_mv[cyc] = mem_valid; l_mw[cyc] = mem_write; l_mr[cyc] = mem_ready; + l_rv[cyc] = mem_rvalid; l_res[cyc] = result_valid; l_bit[cyc] = result_bit; + l_err[cyc] = error; + l_wd[cyc] = mem_wdata; + cyc <= cyc + 1; + end + + // ---------------- checking helpers ---------------- + int fails = 0, checks = 0; + + task automatic check(input bit cond, input string msg); + checks++; + if (!cond) begin + fails++; + $display(" FAIL: %s", msg); + end + endtask + + task automatic init_mem(); + for (int i = 0; i < 1024; i++) memory[i] = {32'hA5A50000 | i[15:0], 32'h0000F00D | (i[15:0] << 16)}; + memory[0] = 64'h0000_0000_0000_0020; // bit 5 set only + memory[1] = 64'h0000_0000_0000_0000; + endtask + + task automatic do_reset(); + @(negedge clk); + reset = 1'b1; op_valid = 1'b0; + @(negedge clk); + reset = 1'b0; + @(negedge clk); + endtask + + // Issue one op; returns the cycle index at which it was accepted. + task automatic issue(input logic [2:0] op, input logic [63:0] base, input logic [63:0] off, + output int acc); + @(negedge clk); + op_valid = 1'b1; operation = op; base_address = base; bit_offset = off; + acc = cyc; // op_valid/op_ready are sampled at the next posedge, cycle == cyc + @(negedge clk); + op_valid = 1'b0; + endtask + + task automatic wait_cycles(input int n); + repeat (n) @(negedge clk); + endtask + + function automatic int count_results(input int lo, input int hi); + int n = 0; + for (int c = lo; c <= hi; c++) if (l_res[c]) n++; + return n; + endfunction + + // ---------------- tests ---------------- + task automatic t_get(input string name, input logic [63:0] base, input logic [63:0] off, input bit exp_bit); + int a; + $display("TEST %s", name); + do_reset(); + issue(3'b000, base, off, a); + wait_cycles(10); + check(l_opv[a] && l_opr[a], "op accepted at a"); + check(l_mv[a+1] && !l_mw[a+1], "READ_REQUEST at a+1"); + check(!l_mv[a+2], "no second request at a+2"); + check(l_rv[a+2], "READ_RESPONSE at a+2"); + check(l_res[a+3], "RESULT_VALID at a+3"); + check(l_bit[a+3] == exp_bit, "result_bit value"); + check(!l_res[a+4], "result_valid is one-cycle (low at a+4)"); + check(count_results(a, a+10) == 1, "exactly one result pulse"); + check(l_opr[a+4] && !l_opr[a+1] && !l_opr[a+3], "op_ready low while busy, high after result"); + endtask + + task automatic t_modify(input string name, input logic [2:0] op, input logic [63:0] base, + input logic [63:0] off, input logic [63:0] exp_word); + int a; + logic [63:0] widx; + widx = ((base << 3) + off) >> 6; + $display("TEST %s", name); + do_reset(); + issue(op, base, off, a); + wait_cycles(12); + check(l_mv[a+1] && !l_mw[a+1], "READ_REQUEST at a+1"); + check(l_rv[a+2], "READ_RESPONSE at a+2"); + check(!l_mv[a+3], "MODIFY: no mem request at a+3"); + check(l_mv[a+4] && l_mw[a+4] && l_mr[a+4], "WRITE accepted (valid&&write&&ready) at a+4"); + check(!l_mv[a+5], "no request after accepted write at a+5"); + check(l_res[a+6], "RESULT_VALID at a+6"); + check(count_results(a, a+12) == 1, "exactly one result pulse"); + check(memory[widx[9:0]] == exp_word, "memory word after RMW"); + endtask + + task automatic t_write_stall(); + int a; + logic [63:0] mem0_pre; + $display("TEST write stall (mem_ready low a+4..a+6)"); + do_reset(); + mem0_pre = memory[0]; + @(negedge clk); + stall_en = 1'b1; stall_lo = cyc + 1 + 4; stall_hi = cyc + 1 + 6; // issue() accepts at cyc+1 + issue(3'b010, 64'd0, 64'd0, a); + check(a + 4 == stall_lo, "stall window aligned to write request"); + wait_cycles(14); + stall_en = 1'b0; + check(l_mv[a+4] && l_mw[a+4] && !l_mr[a+4], "write request held, not accepted a+4"); + check(l_mv[a+5] && l_mw[a+5] && !l_mr[a+5], "write request held a+5"); + check(l_mv[a+6] && l_mw[a+6] && !l_mr[a+6], "write request held a+6"); + check(l_wd[a+4] == l_wd[a+5] && l_wd[a+5] == l_wd[a+6], "wdata stable during stall"); + check(l_mv[a+7] && l_mw[a+7] && l_mr[a+7], "write accepted at a+7"); + check(!l_mv[a+8], "no request after accept"); + check(count_results(a, a+7) == 0, "no result mem0_pre write accepted"); + check(l_res[a+9], "RESULT_VALID at a+9 (2 cycles after accept)"); + check(count_results(a, a+14) == 1, "exactly one result pulse"); + check(memory[0] == (mem0_pre | 64'h1), "write committed once"); + endtask + + task automatic t_read_stall(); + int a; + $display("TEST read stall (mem_ready low a+1..a+2)"); + do_reset(); + @(negedge clk); + stall_en = 1'b1; stall_lo = cyc + 1 + 1; stall_hi = cyc + 1 + 2; + issue(3'b000, 64'd0, 64'd5, a); + wait_cycles(10); + stall_en = 1'b0; + check(l_mv[a+1] && !l_mr[a+1] && l_mv[a+2] && !l_mr[a+2], "read request held during stall"); + check(l_mv[a+3] && l_mr[a+3], "read accepted at a+3"); + check(l_rv[a+4], "read response at a+4"); + check(l_res[a+5] && l_bit[a+5], "RESULT_VALID at a+5 with bit=1"); + check(count_results(a, a+10) == 1, "exactly one result pulse"); + endtask + + // Reset asserted for one cycle at cycle (a + k): operation must be cancelled. + task automatic t_reset_at(input int k, input string where); + int a; + logic [63:0] mem0_pre; + $display("TEST reset cancels op during %s (reset@a+%0d)", where, k); + do_reset(); + mem0_pre = memory[0]; + issue(3'b010, 64'd0, 64'd0, a); + while (cyc < a + k) @(negedge clk); + reset = 1'b1; + @(negedge clk); + reset = 1'b0; + wait_cycles(12); + check(count_results(a, a+16) == 0, "no result_valid for cancelled op"); + check(!mem_valid && !result_valid, "idle outputs after reset"); + check(op_ready, "op_ready after reset"); + if (k <= 4) + check(memory[0] == mem0_pre, "no write committed (cancelled before write accepted)"); + // a fresh op must work normally afterwards + begin + int b; + issue(3'b000, 64'd0, 64'd5, b); + wait_cycles(8); + check(l_res[b+3] && l_bit[b+3], "fresh op after reset completes normally"); + end + endtask + + // Read fault: the op ends at a+3 with error=1 in the result cycle and never writes. + task automatic t_read_fault(input string name, input logic [2:0] op); + int a, b; + logic [63:0] mem0_pre; + $display("TEST read fault on %s", name); + do_reset(); + mem0_pre = memory[0]; + @(negedge clk); + fault_next_read = 1'b1; + issue(op, 64'd0, 64'd5, a); + wait_cycles(10); + check(l_mv[a+1] && !l_mw[a+1], "READ_REQUEST at a+1"); + check(!l_rv[a+2], "no read data on a faulted read"); + check(l_res[a+3], "RESULT_VALID at a+3"); + check(l_err[a+3], "error high in the result cycle"); + check(count_results(a, a+10) == 1, "exactly one result pulse"); + for (int c = a + 1; c <= a + 10; c++) + check(!(l_mv[c] && l_mw[c]), "no write after a read fault"); + check(memory[0] == mem0_pre, "memory unchanged after a read fault"); + // The next operation clears error. + issue(3'b000, 64'd0, 64'd5, b); + wait_cycles(8); + check(l_res[b+3] && !l_err[b+3] && l_bit[b+3], "next op completes with error=0"); + endtask + + // Opcodes 101..111 are not defined; v2 executes them as a read with no write. + task automatic t_undefined_opcode(input logic [2:0] op); + int a; + logic [63:0] mem0_pre; + $display("TEST undefined opcode %b reads and never writes", op); + do_reset(); + mem0_pre = memory[0]; + issue(op, 64'd0, 64'd5, a); + wait_cycles(10); + check(l_res[a+3] && l_bit[a+3] && !l_err[a+3], "completes like GET at a+3"); + for (int c = a + 1; c <= a + 10; c++) + check(!(l_mv[c] && l_mw[c]), "no write for an undefined opcode"); + check(memory[0] == mem0_pre, "memory unchanged"); + endtask + + initial begin + reset = 1'b1; op_valid = 1'b0; operation = '0; base_address = '0; bit_offset = '0; + stall_en = 1'b0; stall_lo = 0; stall_hi = 0; + init_mem(); + + t_get("GET bit5 (set)", 64'd0, 64'd5, 1'b1); + t_get("GET bit4 (clear)", 64'd0, 64'd4, 1'b0); + t_get("GET word1 bit 0 via off=64", 64'd0, 64'd64, 1'b0); + + t_modify("SET bit0", 3'b010, 64'd0, 64'd0, 64'h21); + init_mem(); + t_modify("CLEAR bit5", 3'b011, 64'd0, 64'd5, 64'h00); + init_mem(); + t_modify("TOGGLE bit5", 3'b100, 64'd0, 64'd5, 64'h00); + init_mem(); + t_modify("SET cross-word (off=70 -> word1 bit6)", 3'b010, 64'd0, 64'd70, 64'h40); + init_mem(); + t_modify("SET base=8 off=3 -> word1 bit3", 3'b010, 64'd8, 64'd3, 64'h08); + init_mem(); + + t_write_stall(); + init_mem(); + t_read_stall(); + init_mem(); + + t_reset_at(1, "READ_REQUEST"); + init_mem(); + t_reset_at(2, "READ_WAIT"); + init_mem(); + t_reset_at(3, "MODIFY"); + init_mem(); + t_reset_at(4, "WRITE_REQUEST"); + init_mem(); + t_reset_at(5, "WRITE_WAIT"); + init_mem(); + + t_read_fault("GET", 3'b000); + init_mem(); + t_read_fault("SET", 3'b010); + init_mem(); + t_read_fault("TOGGLE", 3'b100); + init_mem(); + t_undefined_opcode(3'b101); + t_undefined_opcode(3'b111); + + $display("\nchecks=%0d fails=%0d", checks, fails); + if (fails != 0) $fatal(1, "FAILED"); + $display("ALL PASS"); + $finish; + end + + initial begin + #2000000; + $fatal(1, "TIMEOUT"); + end + +endmodule diff --git a/docs/verification.md b/docs/verification.md new file mode 100644 index 0000000000000000000000000000000000000000..1063a49d8d7383a0956ae5672860f408ba137856 --- /dev/null +++ b/docs/verification.md @@ -0,0 +1,58 @@ +# Verification Contract + +Applies to `rtl/bit_accelerator.sv`. + +## Interface + +- Reset is synchronous and active-high. Reset cancels any uncompleted operation + and suppresses its result. `op_ready` is low while `reset` is high. +- An operation is accepted on a cycle with `op_valid && op_ready`. `op_ready` + stays low until the operation's result pulse. +- A read request completes on `mem_valid && !mem_write && mem_ready`; its data + completes on `mem_rvalid`. A read fault is signalled with `mem_fault` instead + of `mem_rvalid`. +- A write request completes on `mem_valid && mem_write && mem_ready`. The cycle + after the write handshake, `mem_fault` reports a write fault. +- `mem_addr` is `((base_address << 3) + bit_offset) >> 6 << 3` (64-bit + arithmetic); `mem_wstrb` is all ones. +- `result_valid` is a one-cycle pulse. `error` and `result_bit` are valid in the + same cycle. On `error`, `result_bit` keeps its previous value. + +## Operations + +| Opcode | Op | Writes | `result_bit` | +|---|---|---|---| +| 000 | GET | no | bit value | +| 001 | TEST | no | bit value | +| 010 | SET | yes | 1 | +| 011 | CLEAR | yes | 0 | +| 100 | TOGGLE | yes | new bit value | +| 101-111 | undefined | no | `error = 1` | + +A read fault ends the operation with `error = 1` and no write. A write fault +reports `error = 1`; whether the memory kept the write is up to the memory. + +## Evidence + +| Check | Tool | Result | +|---|---|---| +| Lint | `verilator --lint-only -Wall` | clean | +| Simulation | `verification/tb_bit_accelerator.sv`, Icarus 12 and Verilator 5.020 | 0 failures; 23 directed operations, 12 reset-cancellation cases, 2000 random operations per seed with backpressure, read latency 0-3 and fault injection | +| Mutation | 7 hand-made RTL mutants (wrong bit index, wrong address, SET as XOR, ...) | all detected | +| Proofs | `formal/*.mlw`, Why3 1.6 + Z3 4.8.12 | 38/38 goals valid | +| Timing model | `spice/bit_extract.sp`, ngspice 42 | tpd ≈ 94 ps (RC model, not extracted silicon) | +| FPGA wrapper | `fpga/tb/tb_fpga_top.sv` over the UART: RTL (Icarus, Verilator), iCE40 and ECP5 gate-level netlists | 0 failures on each | +| Host tool | `fpga/sim/cosim.sh`: `bitacc selftest` against the RTL through a pseudo-terminal | 2000 operations, 0 mismatches | +| FPGA place-and-route | nextpnr, 5 seeds | iCE40UP5K 39.06 MHz, ECP5-25F 73.22 MHz; both meet board clocks | +| ASIC synthesis | Yosys + ABC on sky130_fd_sc_hd, OpenSTA | 1538 cells, 10,063 µm², netlist proved equal to RTL, 500 MHz pre-layout | + +`formal/bit_address.mlw` proves that the RTL's shift/mask datapath equals the +integer address specification when `(base << 3) + offset` does not overflow. +Its trusted base is four facts from the Why3 bit-vector library (listed in the +file), restated because the SMT drivers do not pass them to the solver. +`formal/bit_extract.mlw` proves GET/SET/CLEAR/TOGGLE on 64-bit words change +exactly the target bit. + +The proofs are about the datapath expressions; the state machine is checked by +simulation. Random seeds reproduce within one simulator only (Icarus and +Verilator implement `$random` differently). diff --git a/dual license b/dual license new file mode 100644 index 0000000000000000000000000000000000000000..f337192145bede5da6dbdf8ad28f985f328017f6 --- /dev/null +++ b/dual license @@ -0,0 +1,171 @@ +SNAPKITTY BIT HARDWARE SOURCE LICENSE + +Version 1.0 + +Copyright © 2026 SnapKitty Collective. All rights reserved. + +1. Definitions + +“Software” means software distributed with this project. + +“Hardware” means RTL, HDL, netlists, hardware descriptions, circuit descriptions, formal specifications, verification artifacts, and other hardware-design materials contained in this project. + +“Core” means the bit-string accelerator implementation and its directly derived hardware implementations. + +“Licensee” means any person or organization exercising rights granted by this License. + +“Modified Core” means a modified or derivative version of the Core. + +2. Copyright Permission + +Subject to the terms of this License, SnapKitty Collective grants Licensee a worldwide, non-exclusive, royalty-free license to: + +- use the Hardware; +- study the Hardware; +- reproduce the Hardware; +- modify the Hardware; +- create derivative works; +- compile, synthesize, simulate, and manufacture products incorporating the Hardware; +- use the Hardware commercially. + +3. Commercial Use + +Commercial use is expressly permitted. + +Licensee may: + +- manufacture integrated circuits; +- manufacture FPGA implementations; +- manufacture ASIC implementations; +- sell hardware incorporating the Core; +- license products incorporating the Core; +- provide commercial services using the Core; +- integrate the Core into commercial systems. + +No additional royalty is owed to SnapKitty Collective solely because the Core is used commercially. + +4. Modification and Redistribution + +Modified versions of the Core may be created. + +If a Modified Core is distributed publicly, the distributor must: + +1. preserve this License; +2. retain existing copyright notices; +3. identify material modifications; +4. provide the corresponding modified Core source; +5. provide the same rights granted by this License for the Modified Core. + +A distributor may not impose additional restrictions that prevent recipients from exercising the rights granted by this License. + +5. Proprietary Systems + +Use of the Core inside a larger system does not require the larger system to be licensed under this License. + +A proprietary product may contain or interface with the Core. + +For example: + +Proprietary Product + │ + ├── Proprietary Logic + ├── Proprietary Firmware + ├── Proprietary Software + │ + └── SnapKitty Bit Accelerator + │ + └── licensed under this License + +Only the Core and Modified Core remain subject to the source-sharing requirements of this License. + +6. Generated Hardware + +Synthesis products generated from the Core, including: + +- FPGA bitstreams; +- ASIC netlists; +- gate-level representations; +- placed-and-routed designs; +- manufactured chips; + +may be commercially distributed. + +Distribution of a generated hardware artifact does not by itself require distribution of unrelated proprietary portions of the containing system. + +7. Attribution + +Redistributions of the Core must preserve reasonable attribution to: + +SnapKitty Collective + +Copyright notices and this License must remain included with source distributions. + +8. Patent Grant + +Each contributor grants Licensee a worldwide, non-exclusive, royalty-free patent license to make, have made, use, sell, offer for sale, import, and otherwise transfer the Core, but only for patent claims necessarily infringed by that contributor's contribution to the Core. + +If Licensee initiates patent litigation alleging that the Core itself infringes a patent, the patent license granted to that Licensee under this section terminates as of the date the litigation is initiated. + +9. No Trademark License + +This License does not grant permission to use: + +SnapKitty +SnapKitty Collective +SnapKitty West + +or associated trademarks, logos, or branding except as reasonably necessary to identify the origin of the licensed work. + +10. No Warranty + +THE HARDWARE AND SOFTWARE ARE PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, OR NON-INFRINGEMENT. + +LICENSEE ASSUMES ALL RISK ASSOCIATED WITH THE USE, SYNTHESIS, MANUFACTURE, DEPLOYMENT, AND OPERATION OF THE HARDWARE. + +11. Limitation of Liability + +TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE COPYRIGHT HOLDERS AND CONTRIBUTORS SHALL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ARISING FROM USE OF THE HARDWARE OR SOFTWARE. + +12. No Certification + +Use of this Hardware does not imply that SnapKitty Collective certifies: + +- manufactured chips; +- FPGA implementations; +- safety-critical systems; +- security properties; +- regulatory compliance; +- performance characteristics; +- suitability for any particular application. + +13. License Termination + +The rights granted under this License terminate automatically if Licensee materially violates its terms. + +Rights are reinstated automatically if the violation is cured within thirty (30) days after discovery or notification of the violation. + +Termination does not terminate licenses already properly granted to recipients who are not themselves in violation of this License. + +14. Entire License + +This License constitutes the complete license governing the Core unless a separate written agreement expressly supersedes it. + +Nothing in this License limits rights that Licensee may have independently under applicable copyright, patent, or other law. + +--- + +SNAPKITTY BIT HARDWARE SOURCE LICENSE v1.0 + +Commercial use: PERMITTED + +Modification: PERMITTED + +Commercial manufacturing: PERMITTED + +FPGA/ASIC implementation: PERMITTED + +Commercial sale: PERMITTED + +Modified Core redistribution: SOURCE + SAME LICENSE REQUIRED + +Larger proprietary systems: PERMITTED \ No newline at end of file diff --git a/formal/bit_address.mlw b/formal/bit_address.mlw new file mode 100644 index 0000000000000000000000000000000000000000..242c705f1b4ef215e802b1f1d68bb316260bd165 --- /dev/null +++ b/formal/bit_address.mlw @@ -0,0 +1,111 @@ +(* Address mapping of rtl/bit_accelerator.sv. + + Part 1 states the mapping over unbounded integers. + Part 2 states the RTL's 64-bit datapath over bit-vectors and proves it + implements Part 1 whenever (base << 3) + offset does not overflow. *) + +module BitAddress + use int.Int + use int.EuclideanDivision + + constant word_bits : int = 64 + + (* base is a byte address, offset a bit offset; result is a bit address. *) + function effective_bit (base offset : int) : int = base * 8 + offset + function word_index (b : int) : int = div b word_bits + function bit_index (b : int) : int = mod b word_bits + function word_byte_address (b : int) : int = word_index b * 8 + + lemma decomposition : + forall b : int. b = word_index b * word_bits + bit_index b + + lemma bit_index_bounds : + forall b : int. 0 <= bit_index b < word_bits + + lemma word_byte_address_aligned : + forall b : int. mod (word_byte_address b) 8 = 0 + + (* (word, bit) identifies a bit address uniquely. *) + lemma decomposition_injective : + forall b1 b2 : int. + word_index b1 = word_index b2 -> bit_index b1 = bit_index b2 -> b1 = b2 + + (* Offsets 0..63 from a word-aligned base stay in that word, in order. *) + lemma aligned_base_same_word : + forall base k : int. mod base 8 = 0 -> 0 <= k < word_bits -> + word_index (effective_bit base k) = div base 8 /\ + bit_index (effective_bit base k) = k + + (* Adding 64 to the bit address moves to the next word, same bit. *) + lemma next_word : + forall b : int. + word_index (b + word_bits) = word_index b + 1 /\ + bit_index (b + word_bits) = bit_index b + + (* Offsets 0 and 64 from any base land in adjacent words. *) + lemma offset_64_next_word : + forall base : int. + word_index (effective_bit base 64) = word_index (effective_bit base 0) + 1 +end + +module RtlDatapath + use int.Int + use int.EuclideanDivision + use bv.BV64 + use BitAddress + + (* The RTL expressions, verbatim in bit-vector form. *) + function eff_bit (base offset : t) : t = add (lsl_bv base (3 : t)) offset + function bit_idx (e : t) : t = bw_and e (63 : t) + function req_addr (e : t) : t = lsl_bv (lsr_bv e (6 : t)) (3 : t) + + lemma bit_idx_lt_64 : + forall e : t. ult (bit_idx e) (64 : t) + + lemma req_addr_aligned : + forall e : t. bw_and (req_addr e) (7 : t) = (0 : t) + + (* The word address and bit index recompose the effective bit address. *) + lemma rtl_decomposition : + forall e : t. add (lsl_bv (lsr_bv e (6 : t)) (6 : t)) (bit_idx e) = e + + (* The shift/mask forms equal division, remainder and multiplication. *) + lemma bit_idx_is_urem : + forall e : t. bit_idx e = urem e (64 : t) + + lemma req_addr_is_udiv_mul : + forall e : t. req_addr e = mul (udiv e (64 : t)) (8 : t) + + lemma shl3_is_mul8 : + forall b : t. lsl_bv b (3 : t) = mul b (8 : t) + + (* Trusted base: four specialisations of facts in the Why3 standard library + module bv.BV64 (axioms to_uint_urem and to_uint_udiv at divisor 64, lemmas + to_uint_mul_bounded at factor 8 and to_uint_add_bounded). The SMT drivers + map urem/udiv/mul/add to native bit-vector operations and do not pass the + library's integer axioms on, so these instances are restated here. Each is + true in SMT-LIB bit-vector semantics. Everything else in this file is + proved, not assumed. *) + axiom urem_64 : forall e : t. t'int (urem e (64 : t)) = mod (t'int e) 64 + axiom udiv_64 : forall e : t. t'int (udiv e (64 : t)) = div (t'int e) 64 + axiom mul_8_bounded : + forall a : t. t'int a * 8 < two_power_size -> t'int (mul a (8 : t)) = t'int a * 8 + axiom add_bounded : + forall a b : t. t'int a + t'int b < two_power_size -> + t'int (add a b) = t'int a + t'int b + + lemma udiv_64_bound : forall e : t. 0 <= div (t'int e) 64 * 8 < two_power_size + + (* Bridge to the integer specification. *) + lemma bit_idx_is_mod : + forall e : t. t'int (bit_idx e) = bit_index (t'int e) + + lemma req_addr_is_word_byte_address : + forall e : t. t'int (req_addr e) = word_byte_address (t'int e) + + (* Without overflow the 64-bit sum equals the integer effective address. *) + lemma eff_bit_no_overflow : + forall base offset : t. + t'int base * 8 + t'int offset < two_power_size -> + t'int (eff_bit base offset) = effective_bit (t'int base) (t'int offset) +end diff --git a/formal/bit_extract.mlw b/formal/bit_extract.mlw new file mode 100644 index 0000000000000000000000000000000000000000..362bd8d0d32a94a24dd0cb9ad26df455d5b41ba7 --- /dev/null +++ b/formal/bit_extract.mlw @@ -0,0 +1,50 @@ +(* Single-bit read and read-modify-write semantics of rtl/bit_accelerator.sv, + stated over 64-bit words. k is the bit index (bit_idx_q), k < 64. *) + +module BitExtract + use bv.BV64 + + function one_hot (k : t) : t = lsl_bv one k (* shifted_one *) + function op_set (w k : t) : t = bw_or w (one_hot k) + function op_clear (w k : t) : t = bw_and w (bw_not (one_hot k)) + function op_toggle (w k : t) : t = bw_xor w (one_hot k) + + (* GET/TEST: mem_rdata[k] is the k-th bit of the word. *) + lemma get_is_bit : + forall w k : t. ult k (64 : t) -> + (nth_bv w k <-> bw_and (lsr_bv w k) one = one) + + lemma one_hot_single_bit : + forall k j : t. ult k (64 : t) -> ult j (64 : t) -> + (nth_bv (one_hot k) j <-> j = k) + + (* SET makes bit k one and leaves every other bit unchanged. *) + lemma set_target : + forall w k : t. ult k (64 : t) -> nth_bv (op_set w k) k + lemma set_frame : + forall w k j : t. ult k (64 : t) -> ult j (64 : t) -> j <> k -> + (nth_bv (op_set w k) j <-> nth_bv w j) + + (* CLEAR makes bit k zero and leaves every other bit unchanged. *) + lemma clear_target : + forall w k : t. ult k (64 : t) -> not (nth_bv (op_clear w k) k) + lemma clear_frame : + forall w k j : t. ult k (64 : t) -> ult j (64 : t) -> j <> k -> + (nth_bv (op_clear w k) j <-> nth_bv w j) + + (* TOGGLE inverts bit k and leaves every other bit unchanged. *) + lemma toggle_target : + forall w k : t. ult k (64 : t) -> + (nth_bv (op_toggle w k) k <-> not (nth_bv w k)) + lemma toggle_frame : + forall w k j : t. ult k (64 : t) -> ult j (64 : t) -> j <> k -> + (nth_bv (op_toggle w k) j <-> nth_bv w j) + + (* Algebraic laws. *) + lemma set_idempotent : + forall w k : t. ult k (64 : t) -> op_set (op_set w k) k = op_set w k + lemma clear_idempotent : + forall w k : t. ult k (64 : t) -> op_clear (op_clear w k) k = op_clear w k + lemma toggle_involution : + forall w k : t. ult k (64 : t) -> op_toggle (op_toggle w k) k = w +end diff --git a/formal/lane_map.mlw b/formal/lane_map.mlw new file mode 100644 index 0000000000000000000000000000000000000000..423316e67b011e8a8e57d3f3023a631dd3c6f623 --- /dev/null +++ b/formal/lane_map.mlw @@ -0,0 +1,176 @@ +(* Lane mapping of BULK in pcie/rtl/bitacc_engine.sv. + + The bit store has L lanes; word w lives in lane (w mod L) at local address + (w div L). BULK pairs dst word D + k with src word S + k for k = 0 .. n-1, + L elements per step j. Per lane i the RTL computes, once per operation: + + r i = (i - S) mod L src element offset (RTL: LW'(i) - S[LW-1:0]) + sa i = (S + r i) div L src local base (RTL: sa) + q m = (m - D) mod L dst element offset (RTL: q_off) + da m = (D + q m) div L dst local base (RTL: da) + delta = (D - S) mod L rotation (RTL: r_delta) + + In step j, lane i reads src local address j + sa i, which holds element + k = j*L + r i; dst lane m reads and writes local address j + da m, holding + element j*L + q m, and takes its src word from lane (m - delta) mod L. + + Proved here, for every L > 0 (the RTL uses L = 2^LW, where LW-bit + subtraction is subtraction mod L, Part 1): + - lane i's src read is word S + k of lane i (Part 2); + - every element k >= 0 is read by exactly one lane, in step k div L (Part 2); + - the rotation delivers element k to the lane and local address of word + D + k, i.e. dst lane m and src lane (m - delta) mod L hold the same k + (Part 3). + Bounds (k < n, addresses inside the store) are checked by the RTL's kvalid + and the decode-time range checks, and tested in pcie/tb. *) + +module LaneMap + use int.Int + use int.EuclideanDivision + + constant l : int + axiom l_pos : l > 0 + + (* Each lemma is a lemma function whose assertions name the quotient + witnesses, so that every step is linear arithmetic plus one instance of + the standard library's Div_mod, Div_unique, Div_mult or Mod_mult. *) + + (* ---------------------------------------------------------------- Part 1: residues *) + let lemma mod_add_multiple (a t : int) + ensures { mod (a + t * l) l = mod a l } + = assert { a + t * l = l * t + a } + + (* Linear step of rotation_pairs_elements, kept apart so that the prover + sees it without the residue lemmas in its context. *) + let lemma rotation_arith (m d s t e : int) + requires { d - s = l * t + e } + ensures { m - e + (- s) = (m - d) + t * l } + = () + + let lemma rotation_mod (m d s t e : int) + requires { d - s = l * t + e } + ensures { mod (m - e + (- s)) l = mod (m - d) l } + = rotation_arith m d s t e; + mod_add_multiple (m - d) t + + let lemma div_add_multiple (a t : int) + ensures { div (a + t * l) l = div a l + t } + = assert { a + t * l = l * t + a } + + let lemma mod_small (a : int) + requires { 0 <= a < l } + ensures { mod a l = a } + = assert { div a l = 0 }; + assert { a = l * div a l + mod a l } + + let lemma mod_sum_left (a b : int) + ensures { mod (mod a l + b) l = mod (a + b) l } + = assert { a = l * div a l + mod a l }; + assert { a + b = (mod a l + b) + div a l * l }; + mod_add_multiple (mod a l + b) (div a l) + + let lemma mod_sum_right (a b : int) + ensures { mod (a + mod b l) l = mod (a + b) l } + = mod_sum_left b a; + assert { mod b l + a = a + mod b l /\ b + a = a + b } + + (* LW-bit subtraction of residues (the RTL's r, q_off, r_delta) is the + residue of the difference when L = 2^LW. *) + let lemma mod_sub_residues (a b : int) + ensures { mod (mod a l - mod b l) l = mod (a - b) l } + = assert { a = l * div a l + mod a l }; + assert { b = l * div b l + mod b l }; + assert { a - b = (mod a l - mod b l) + (div a l - div b l) * l }; + mod_add_multiple (mod a l - mod b l) (div a l - div b l) + + (* ---------------------------------------------------------------- Part 2: src side *) + function r (s i : int) : int = mod (i - s) l + function sa (s i : int) : int = div (s + r s i) l + function k_src (s i j : int) : int = j * l + r s i + + lemma r_bounds : forall s i : int. 0 <= r s i < l + + (* Lane i's element of step j lives in lane i ... *) + let lemma src_lane (s i j : int) + requires { 0 <= i < l } + ensures { mod (s + k_src s i j) l = i } + = assert { i - s = l * div (i - s) l + r s i }; + assert { s + k_src s i j = i + (j - div (i - s) l) * l }; + mod_add_multiple i (j - div (i - s) l); + mod_small i + + (* ... at local address j + sa i. *) + let lemma src_local (s i j : int) + ensures { div (s + k_src s i j) l = j + sa s i } + = assert { s + k_src s i j = (s + r s i) + j * l }; + div_add_multiple (s + r s i) j + + (* Every element k is the element of lane (s + k) mod L in step k div L ... *) + let lemma src_covers (s k : int) + ensures { k_src s (mod (s + k) l) (div k l) = k } + = mod_sum_left (s + k) (- s); + assert { s + k + (- s) = k }; + assert { k = l * div k l + mod k l } + + (* ... and of no other lane in that step. *) + let lemma src_unique (s i1 i2 j : int) + requires { 0 <= i1 < l /\ 0 <= i2 < l } + requires { k_src s i1 j = k_src s i2 j } + ensures { i1 = i2 } + = src_lane s i1 j; + src_lane s i2 j + + (* The element of step j is in [j L, j L + L): different steps hold + different elements. *) + let lemma src_step (s i j : int) + ensures { div (k_src s i j) l = j } + = assert { j * l <= k_src s i j < j * l + l } + + let lemma src_steps_disjoint (s i1 i2 j1 j2 : int) + requires { k_src s i1 j1 = k_src s i2 j2 } + ensures { j1 = j2 } + = src_step s i1 j1; + src_step s i2 j2 + + (* ---------------------------------------------------------------- Part 3: dst side, rotation *) + (* The dst offsets have the same form as the src offsets, with d for s. *) + function q (d m : int) : int = r d m + function da (d m : int) : int = sa d m + function k_dst (d m j : int) : int = k_src d m j + function delta (d s : int) : int = mod (d - s) l + function src_lane_for (d s m : int) : int = mod (m - delta d s) l + + (* Dst lane m's element of step j is word d + k of lane m, at j + da m. *) + let lemma dst_lane (d m j : int) + requires { 0 <= m < l } + ensures { mod (d + k_dst d m j) l = m } + = src_lane d m j + + let lemma dst_local (d m j : int) + ensures { div (d + k_dst d m j) l = j + da d m } + = src_local d m j + + (* The src lane the rotation selects is a lane ... *) + lemma rotation_in_range : + forall d s m : int. 0 <= src_lane_for d s m < l + + (* ... and it holds the same element k as dst lane m, in every step. *) + let lemma rotation_pairs_elements (d s m j : int) + ensures { k_src s (src_lane_for d s m) j = k_dst d m j } + = let t = div (d - s) l in + let e = delta d s in + assert { r s (src_lane_for d s m) = mod (mod (m - e) l + (- s)) l }; + mod_sum_left (m - e) (- s); + assert { r s (src_lane_for d s m) = mod (m - e + (- s)) l }; + assert { d - s = l * t + e }; + rotation_mod m d s t e + + (* Element k from src lane i goes to dst lane (i + delta) mod L. *) + let lemma rotation_target (d s i j : int) + requires { 0 <= i < l } + ensures { mod (d + k_src s i j) l = mod (i + delta d s) l } + = assert { i - s = l * div (i - s) l + r s i }; + assert { d + k_src s i j = (i + (d - s)) + (j - div (i - s) l) * l }; + mod_add_multiple (i + (d - s)) (j - div (i - s) l); + mod_sum_right i (d - s) +end diff --git a/fpga/Makefile b/fpga/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..73897792cb65e991e2fca2cb92151f99daa85d00 --- /dev/null +++ b/fpga/Makefile @@ -0,0 +1,115 @@ +# FPGA build for the bit-string accelerator. +# +# make sim UART-level testbench on the RTL (Icarus + Verilator) +# make lint verilator -Wall on both board tops +# make icebreaker bitstream for iCEBreaker (iCE40UP5K) -> build/icebreaker.bin +# make ulx3s bitstream for ULX3S (ECP5 25F) -> build/ulx3s.bit +# make gl-sim testbench on post-synthesis netlists (iCE40, ECP5) +# make cosim host tool (host/) against the RTL through a pseudo-terminal +# make prog-icebreaker / prog-ulx3s program a connected board +# make all lint, sim, cosim, gl-sim and both bitstreams +# +# Tools: yosys, nextpnr-ice40, nextpnr-ecp5, icepack (icestorm), ecppack +# (prjtrellis), iverilog, verilator; iceprog / openFPGALoader to program. + +YOSYS ?= yosys +IVERILOG ?= iverilog +VVP ?= vvp +VERILATOR ?= verilator +BUILD ?= build +SEED ?= 1 +ULX3S_DEVICE ?= 25k # 12k, 25k, 45k or 85k to match the board +YOSYS_DAT ?= /usr/share/yosys # Yosys data dir with ice40/ and ecp5/ cell models + +CORE := ../rtl/bit_accelerator.sv rtl/uart_rx.sv rtl/uart_tx.sv rtl/word_memory.sv rtl/fpga_top.sv +TB := tb/tb_fpga_top.sv + +.PHONY: all sim sim-icarus sim-verilator lint icebreaker ulx3s gl-sim gl-sim-ice40 gl-sim-ecp5 \ + cosim host prog-icebreaker prog-ulx3s clean +.DEFAULT_GOAL := all + +all: lint sim cosim gl-sim icebreaker ulx3s + +$(BUILD): + mkdir -p $@ + +# ------------------------------------------------------------------ RTL checks +sim: sim-icarus sim-verilator + +sim-icarus: | $(BUILD) + $(IVERILOG) -g2012 -o $(BUILD)/tb_fpga.vvp $(CORE) $(TB) + $(VVP) -n $(BUILD)/tb_fpga.vvp + +sim-verilator: | $(BUILD) + $(VERILATOR) --binary --timing --timescale 1ns/1ps -Wno-fatal -Wno-lint --top-module tb_fpga_top \ + -Mdir $(BUILD)/verilator $(CORE) $(TB) >/dev/null + $(BUILD)/verilator/Vtb_fpga_top + +lint: + $(VERILATOR) --lint-only -Wall $(CORE) rtl/power_on_reset.sv boards/icebreaker_top.sv --top-module icebreaker_top + $(VERILATOR) --lint-only -Wall $(CORE) rtl/power_on_reset.sv boards/ulx3s_top.sv --top-module ulx3s_top + +# ------------------------------------------------------------------ host tool +host: + cd host && cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test + +cosim: host + BUILD=$(BUILD) sim/cosim.sh 2000 1 + +# ------------------------------------------------------------------ iCEBreaker +icebreaker: $(BUILD)/icebreaker.bin + +$(BUILD)/icebreaker.json: $(CORE) rtl/power_on_reset.sv boards/icebreaker_top.sv | $(BUILD) + $(YOSYS) -q -l $(BUILD)/icebreaker_synth.log -p "read_verilog -sv $^; synth_ice40 -top icebreaker_top -json $@" + +$(BUILD)/icebreaker.asc: $(BUILD)/icebreaker.json boards/icebreaker.pcf + nextpnr-ice40 --up5k --package sg48 --json $< --pcf boards/icebreaker.pcf --asc $@ \ + --freq 12 --seed $(SEED) -l $(BUILD)/icebreaker_pnr.log -q + +$(BUILD)/icebreaker.bin: $(BUILD)/icebreaker.asc + icepack $< $@ + @grep -E "Max frequency|ICESTORM_LC:|ICESTORM_RAM:" $(BUILD)/icebreaker_pnr.log | tail -3 + +prog-icebreaker: $(BUILD)/icebreaker.bin + iceprog $< + +# ------------------------------------------------------------------ ULX3S +ulx3s: $(BUILD)/ulx3s.bit + +$(BUILD)/ulx3s.json: $(CORE) rtl/power_on_reset.sv boards/ulx3s_top.sv | $(BUILD) + $(YOSYS) -q -l $(BUILD)/ulx3s_synth.log -p "read_verilog -sv $^; synth_ecp5 -top ulx3s_top -json $@" + +$(BUILD)/ulx3s.config: $(BUILD)/ulx3s.json boards/ulx3s.lpf + nextpnr-ecp5 --$(strip $(ULX3S_DEVICE)) --package CABGA381 --json $< --lpf boards/ulx3s.lpf \ + --textcfg $@ --seed $(SEED) -l $(BUILD)/ulx3s_pnr.log -q + +$(BUILD)/ulx3s.bit: $(BUILD)/ulx3s.config + ecppack --compress $< $@ + @grep -E "Max frequency|TRELLIS_COMB:|DP16KD:" $(BUILD)/ulx3s_pnr.log | tail -3 + +prog-ulx3s: $(BUILD)/ulx3s.bit + openFPGALoader --board ulx3s $< + +# ------------------------------------------------------------------ gate level +# The netlists are built with CLKS_PER_BIT = 8 so the UART testbench runs fast. +gl-sim: gl-sim-ice40 gl-sim-ecp5 + +gl-sim-ice40: | $(BUILD) + $(YOSYS) -q -p "read_verilog -sv $(CORE); chparam -set CLKS_PER_BIT 8 fpga_top; \ + synth_ice40 -top fpga_top; write_verilog -noattr $(BUILD)/fpga_top_ice40_gl.v" + $(IVERILOG) -g2012 -DGATE_LEVEL -o $(BUILD)/gl_ice40.vvp \ + $(BUILD)/fpga_top_ice40_gl.v $(strip $(YOSYS_DAT))/ice40/cells_sim.v $(TB) + $(VVP) -n $(BUILD)/gl_ice40.vvp + +# Yosys ships DP16KD (ECP5 block RAM) only as a black box, so this netlist +# keeps the memory in LUT RAM (-nobram); everything else is the real mapping. +gl-sim-ecp5: | $(BUILD) + $(YOSYS) -q -p "read_verilog -sv $(CORE); chparam -set CLKS_PER_BIT 8 fpga_top; \ + synth_ecp5 -nobram -top fpga_top; write_verilog -noattr $(BUILD)/fpga_top_ecp5_gl.v" + $(IVERILOG) -g2012 -DGATE_LEVEL -I$(strip $(YOSYS_DAT))/ecp5 -o $(BUILD)/gl_ecp5.vvp \ + $(BUILD)/fpga_top_ecp5_gl.v $(strip $(YOSYS_DAT))/ecp5/cells_sim.v $(TB) + $(VVP) -n $(BUILD)/gl_ecp5.vvp + +clean: + rm -rf $(BUILD) + cd host && cargo clean diff --git a/fpga/README.md b/fpga/README.md new file mode 100644 index 0000000000000000000000000000000000000000..a3766b27f4d4b22fa50640e9fb52e4b4fd7b1286 --- /dev/null +++ b/fpga/README.md @@ -0,0 +1,99 @@ +# FPGA build + +Runs `rtl/bit_accelerator.sv` on real FPGA boards. The core's 64-bit buses do +not fit on package pins, so `rtl/fpga_top.sv` wraps it with: + +- 256 × 64-bit words of block RAM (byte addresses 0-2047). Any other address + answers with `mem_fault`, so the error path works on hardware. +- A UART command interface, 115200 baud 8N1. + +| Board | FPGA | Clock | Top | Constraints | +|---|---|---|---|---| +| iCEBreaker | iCE40UP5K-SG48 | 12 MHz | `boards/icebreaker_top.sv` | `boards/icebreaker.pcf` | +| ULX3S | ECP5 LFE5U-25F CABGA381 | 25 MHz | `boards/ulx3s_top.sv` | `boards/ulx3s.lpf` | + +LEDs: iCEBreaker red = last operation reported an error, green = heartbeat. +ULX3S `led[0]` = error, `led[1]` = busy, `led[7]` = heartbeat. The button resets. + +## Build and program + +Ubuntu 24.04: `sudo apt-get install yosys nextpnr-ice40 nextpnr-ecp5 fpga-icestorm fpga-trellis iverilog verilator`. + +```sh +make icebreaker # build/icebreaker.bin +make prog-icebreaker # iceprog +make ulx3s # build/ulx3s.bit (ULX3S_DEVICE=12k|25k|45k|85k to match your board) +make prog-ulx3s # openFPGALoader --board ulx3s +``` + +Then, from the host (`cd host && cargo build --release`): + +```sh +bitacc --port /dev/ttyUSB1 ping # pong +bitacc --port /dev/ttyUSB1 write 0 0x8000000000000001 +bitacc --port /dev/ttyUSB1 exec get 0 63 # 1 +bitacc --port /dev/ttyUSB1 exec toggle 0 5 # 1 +bitacc --port /dev/ttyUSB1 read 0 # 0x8000000000000021 +bitacc --port /dev/ttyUSB1 exec get 4096 0 # error (outside the RAM) +bitacc --port /dev/ttyUSB1 selftest 10000 # acceptance test, must report 0 mismatches +``` + +The iCEBreaker's UART is the FTDI's second interface (usually `/dev/ttyUSB1`). +On the ULX3S it is the only FTDI port (usually `/dev/ttyUSB0`). + +`selftest` writes random data to all 256 words. It then runs random operations +(including out-of-range addresses and undefined opcodes) and checks every +status and the final RAM contents against a host model of the RTL. + +## Protocol + +All multi-byte fields are little-endian. + +| Host → FPGA | FPGA → host | | +|---|---|---| +| `'P'` | `'B'` | ping | +| `'W'` idx[1] data[8] | `'K'` | write word | +| `'R'` idx[1] | data[8] | read word | +| `'X'` op[1] base[8] offset[8] | `0xA0 \| error<<1 \| bit` | run one operation | +| anything else | `'?'` | | + +## Results + +Yosys 0.33, nextpnr 0.6, place-and-route seeds 1-5: + +| Board | Logic | Block RAM | Fmax (worst of 5 seeds) | Board clock | +|---|---|---|---|---| +| iCEBreaker | 1138 / 5280 LC (21%) | 4 / 30 | 39.06 MHz | 12 MHz | +| ULX3S 25F | 1171 / 24288 LUT4 (4%), 725 FF | 2 / 56 DP16KD | 73.22 MHz | 25 MHz | + +## Verification + +| Check | Command | Result | +|---|---|---| +| Lint, both board tops | `make lint` | `verilator -Wall` clean | +| UART-level testbench on RTL, Icarus + Verilator | `make sim` | 0 failures (7554 checks under Icarus, 7407 under Verilator; their random streams differ) | +| Same testbench on the iCE40 gate-level netlist (with the `SB_RAM40_4K` model) | `make gl-sim-ice40` | 7554 checks, 0 failures | +| Same testbench on the ECP5 gate-level netlist | `make gl-sim-ecp5` | 7554 checks, 0 failures | +| Host tool against the RTL through a pseudo-terminal | `make cosim` | `selftest` 2000 ops, 0 mismatches | + +The testbench drives the serial line bit by bit. It checks pings, unknown +commands, all 256 words, directed operations (word crossing, last bit of RAM, +out-of-range reads, undefined opcodes) and 300 random operations against a +reference model. It also checks that TX idles high from power-up. Six of seven +hand-made faults in `fpga_top`/`uart_tx` are detected. The seventh (no +write-fault response) cannot occur: a write only follows a successful read of +the same word. + +Two problems were found and fixed on the way: +- **TX powered up low.** FPGA flip-flops power up low, so TX started low and a + host would read a junk byte. +- **Testbench race.** The testbench released reset on a clock edge, so the + gate-level netlist saw a partial reset. + +Limits: +- **Not run on a physical board.** Pin assignments are taken from each board's + reference constraints and have not been checked against a board. +- **ECP5 block RAM is not covered at gate level.** Yosys 0.33 ships `DP16KD` + only as a black box, so `gl-sim-ecp5` maps the memory to LUT RAM (`-nobram`). + The `DP16KD` mapping is covered by place-and-route only. The iCE40 + gate-level run does include block RAM. diff --git a/fpga/boards/icebreaker.pcf b/fpga/boards/icebreaker.pcf new file mode 100644 index 0000000000000000000000000000000000000000..3b5858bd4860d1b6f0bd6b288dc4cd70b9806043 --- /dev/null +++ b/fpga/boards/icebreaker.pcf @@ -0,0 +1,7 @@ +# iCEBreaker v1.0 (iCE40UP5K-SG48). Pin numbers from the iCEBreaker examples. +set_io -nowarn CLK 35 +set_io -nowarn BTN_N 10 +set_io -nowarn RX 6 +set_io -nowarn TX 9 +set_io -nowarn LEDR_N 11 +set_io -nowarn LEDG_N 37 diff --git a/fpga/boards/icebreaker_top.sv b/fpga/boards/icebreaker_top.sv new file mode 100644 index 0000000000000000000000000000000000000000..044a74358dc7934d254b2b714422a02e2a013bb2 --- /dev/null +++ b/fpga/boards/icebreaker_top.sv @@ -0,0 +1,22 @@ +// iCEBreaker (Lattice iCE40UP5K-SG48, 12 MHz). UART on the FTDI second channel +// at 115200 baud. Red LED: last operation reported an error. Green LED: heartbeat. +module icebreaker_top ( + input logic CLK, + input logic BTN_N, + input logic RX, + output logic TX, + output logic LEDR_N, + output logic LEDG_N +); + logic reset, led_busy, led_error; + logic [23:0] beat = '0; + + power_on_reset u_por (.clk(CLK), .button(!BTN_N), .reset); + + fpga_top #(.CLKS_PER_BIT(104)) u_top ( // 12 MHz / 115200 = 104.2 + .clk(CLK), .reset, .uart_rx(RX), .uart_tx(TX), .led_busy, .led_error); + + always_ff @(posedge CLK) beat <= beat + 1'b1; + assign LEDR_N = !led_error; + assign LEDG_N = !(beat[23] || led_busy); +endmodule diff --git a/fpga/boards/ulx3s.lpf b/fpga/boards/ulx3s.lpf new file mode 100644 index 0000000000000000000000000000000000000000..0b649b59064d886c15cc6d64fc5d4fdeb29efa48 --- /dev/null +++ b/fpga/boards/ulx3s.lpf @@ -0,0 +1,35 @@ +# ULX3S v3.x. Sites from the ULX3S reference constraint file (ulx3s_v20.lpf). +BLOCK RESETPATHS; +BLOCK ASYNCPATHS; + +LOCATE COMP "clk_25mhz" SITE "G2"; +IOBUF PORT "clk_25mhz" PULLMODE=NONE IO_TYPE=LVCMOS33; +FREQUENCY PORT "clk_25mhz" 25 MHZ; + +LOCATE COMP "ftdi_rxd" SITE "L4"; # FPGA transmits to FTDI +LOCATE COMP "ftdi_txd" SITE "M1"; # FPGA receives from FTDI +IOBUF PORT "ftdi_rxd" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4; +IOBUF PORT "ftdi_txd" PULLMODE=UP IO_TYPE=LVCMOS33; + +LOCATE COMP "btn[0]" SITE "D6"; # BTN_PWRn, active low +IOBUF PORT "btn[0]" PULLMODE=UP IO_TYPE=LVCMOS33; + +LOCATE COMP "led[0]" SITE "B2"; +LOCATE COMP "led[1]" SITE "C2"; +LOCATE COMP "led[2]" SITE "C1"; +LOCATE COMP "led[3]" SITE "D2"; +LOCATE COMP "led[4]" SITE "D1"; +LOCATE COMP "led[5]" SITE "E2"; +LOCATE COMP "led[6]" SITE "E1"; +LOCATE COMP "led[7]" SITE "H3"; +IOBUF PORT "led[0]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4; +IOBUF PORT "led[1]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4; +IOBUF PORT "led[2]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4; +IOBUF PORT "led[3]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4; +IOBUF PORT "led[4]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4; +IOBUF PORT "led[5]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4; +IOBUF PORT "led[6]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4; +IOBUF PORT "led[7]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4; + +LOCATE COMP "wifi_gpio0" SITE "L2"; +IOBUF PORT "wifi_gpio0" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4; diff --git a/fpga/boards/ulx3s_top.sv b/fpga/boards/ulx3s_top.sv new file mode 100644 index 0000000000000000000000000000000000000000..94d9fd47fc39fa4d8f72433195d8bfa86b9d9023 --- /dev/null +++ b/fpga/boards/ulx3s_top.sv @@ -0,0 +1,23 @@ +// ULX3S (Lattice ECP5 LFE5U-25F/45F/85F, 25 MHz). UART through the on-board FTDI +// at 115200 baud. led[0]: last operation reported an error, led[1]: busy, +// led[7]: heartbeat. btn[0] (PWR, active low) resets. +module ulx3s_top ( + input logic clk_25mhz, + input logic [0:0] btn, + input logic ftdi_txd, // FTDI -> FPGA + output logic ftdi_rxd, // FPGA -> FTDI + output logic [7:0] led, + output logic wifi_gpio0 // held high so the ESP32 does not take over +); + logic reset, led_busy, led_error; + logic [24:0] beat = '0; + + power_on_reset u_por (.clk(clk_25mhz), .button(!btn[0]), .reset); + + fpga_top #(.CLKS_PER_BIT(217)) u_top ( // 25 MHz / 115200 = 217.0 + .clk(clk_25mhz), .reset, .uart_rx(ftdi_txd), .uart_tx(ftdi_rxd), .led_busy, .led_error); + + always_ff @(posedge clk_25mhz) beat <= beat + 1'b1; + assign led = {beat[24], 5'd0, led_busy, led_error}; + assign wifi_gpio0 = 1'b1; +endmodule diff --git a/fpga/host/Cargo.lock b/fpga/host/Cargo.lock new file mode 100644 index 0000000000000000000000000000000000000000..ebc87be8e5d844b2e943fa9f37442fdcb9e28908 --- /dev/null +++ b/fpga/host/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitacc" +version = "0.1.0" diff --git a/fpga/host/Cargo.toml b/fpga/host/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..c7ba9e8cdfc5d2e83b37792dad93eb8891e8624b --- /dev/null +++ b/fpga/host/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "bitacc" +version = "0.1.0" +edition = "2024" +rust-version = "1.85" +description = "Host tool for the bit-string accelerator FPGA build (UART protocol)" +license = "EPL-2.0" +publish = false + +[dependencies] diff --git a/fpga/host/src/main.rs b/fpga/host/src/main.rs new file mode 100644 index 0000000000000000000000000000000000000000..c33e7d459f3088ae303eb3477f8368a2c9eebebc --- /dev/null +++ b/fpga/host/src/main.rs @@ -0,0 +1,283 @@ +//! bitacc: drive the bit-string accelerator FPGA build over its UART. +//! +//! bitacc --port /dev/ttyUSB1 ping +//! bitacc --port /dev/ttyUSB1 write +//! bitacc --port /dev/ttyUSB1 read +//! bitacc --port /dev/ttyUSB1 exec +//! bitacc --port /dev/ttyUSB1 selftest [operations] [seed] +//! +//! Options: --baud N (default 115200), --words N (RAM size: 256 for the FPGA +//! build, 4 for the Tiny Tapeout chip). +//! Numbers are decimal or 0x-prefixed hex. The port is configured with `stty` +//! (115200 8N1 raw, 1 s read timeout); Linux and macOS. + +mod protocol; + +use protocol::*; +use std::fs::{File, OpenOptions}; +use std::io::{Read, Write}; +use std::process::{Command, ExitCode}; + +struct Port { + file: File, +} + +impl Port { + fn open(path: &str, baud: u32) -> Result { + let flag = if cfg!(target_os = "macos") { + "-f" + } else { + "-F" + }; + let status = Command::new("stty") + .args([ + flag, + path, + &baud.to_string(), + "raw", + "-echo", + "cs8", + "-cstopb", + "-parenb", + ]) + .args(["-crtscts", "min", "0", "time", "10"]) + .status() + .map_err(|e| format!("running stty: {e}"))?; + if !status.success() { + return Err(format!("stty could not configure {path}")); + } + let file = OpenOptions::new() + .read(true) + .write(true) + .open(path) + .map_err(|e| format!("opening {path}: {e}"))?; + let mut port = Port { file }; + port.drain(); + Ok(port) + } + + /// Discard bytes left over from a previous session. + fn drain(&mut self) { + let mut buf = [0u8; 256]; + while matches!(self.file.read(&mut buf), Ok(n) if n > 0) {} + } + + fn send(&mut self, bytes: &[u8]) -> Result<(), String> { + self.file + .write_all(bytes) + .map_err(|e| format!("write: {e}")) + } + + fn recv(&mut self, n: usize) -> Result, String> { + let mut out = vec![0u8; n]; + let mut got = 0; + while got < n { + match self.file.read(&mut out[got..]) { + Ok(0) => return Err(format!("timeout: got {got} of {n} reply bytes")), + Ok(k) => got += k, + Err(e) => return Err(format!("read: {e}")), + } + } + Ok(out) + } + + fn ping(&mut self) -> Result<(), String> { + self.send(&[PING])?; + match self.recv(1)?[0] { + PONG => Ok(()), + b => Err(unexpected("ping", b)), + } + } + + fn write_word(&mut self, index: u8, value: u64) -> Result<(), String> { + self.send(&write_frame(index, value))?; + match self.recv(1)?[0] { + WRITE_ACK => Ok(()), + b => Err(unexpected("write", b)), + } + } + + fn read_word(&mut self, index: u8) -> Result { + self.send(&read_frame(index))?; + let b = self.recv(8)?; + Ok(u64::from_le_bytes(b.try_into().expect("8 bytes"))) + } + + fn exec(&mut self, opcode: u8, base: u64, offset: u64) -> Result { + self.send(&exec_frame(opcode, base, offset))?; + parse_status(self.recv(1)?[0]) + } +} + +fn num(s: &str) -> Result { + let r = match s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) { + Some(h) => u64::from_str_radix(&h.replace('_', ""), 16), + None => s.replace('_', "").parse(), + }; + r.map_err(|_| format!("not a number: {s}")) +} + +fn opcode(s: &str) -> Result { + if let Some(op) = Op::parse(s) { + return Ok(op as u8); + } + match num(s)? { + n @ 0..=7 => Ok(n as u8), + _ => Err(format!( + "opcode must be get/test/set/clear/toggle or 0..7: {s}" + )), + } +} + +fn index(s: &str, words: usize) -> Result { + let n = num(s)?; + u8::try_from(n) + .ok() + .filter(|&i| (i as usize) < words) + .ok_or_else(|| format!("index must be 0..{}: {s}", words - 1)) +} + +struct Rng(u64); +impl Rng { + fn next(&mut self) -> u64 { + self.0 ^= self.0 << 13; + self.0 ^= self.0 >> 7; + self.0 ^= self.0 << 17; + self.0 + } +} + +/// Fill the RAM, run random operations, and compare every status and the final +/// RAM contents against the host model. This is the on-hardware acceptance test. +fn selftest(port: &mut Port, words: usize, ops: u64, seed: u64) -> Result<(), String> { + let mut rng = Rng(seed.max(1)); + let mut ram = vec![0u64; words]; + port.ping()?; + for (i, w) in ram.iter_mut().enumerate() { + *w = rng.next(); + port.write_word(i as u8, *w)?; + } + let mut mismatches = 0u64; + for n in 0..ops { + let r = rng.next(); + let mut op = (r % 8) as u8; + if op > 4 && r % 3 != 0 { + op -= 4; // mostly defined opcodes + } + let (base, offset) = if r % 10 == 0 { + (rng.next(), rng.next()) // mostly out of range: exercises faults + } else { + ( + rng.next() % (words as u64 * 8), + rng.next() % (words as u64 * 64), + ) + }; + let want = model_exec(&mut ram, op, base, offset); + let got = port.exec(op, base, offset)?; + if got.error != want.error || (!want.error && got.bit != want.bit) { + mismatches += 1; + eprintln!( + "op {n}: exec {op} base {base:#x} offset {offset:#x}: got {got:?}, expected {want:?}" + ); + } + } + for (i, &w) in ram.iter().enumerate() { + let got = port.read_word(i as u8)?; + if got != w { + mismatches += 1; + eprintln!("word {i}: got {got:#018x}, expected {w:#018x}"); + } + } + println!("selftest: {ops} operations, {words} words checked, {mismatches} mismatches"); + if mismatches == 0 { + Ok(()) + } else { + Err("selftest FAILED".into()) + } +} + +fn run(args: &[String]) -> Result<(), String> { + let mut port_path = None; + let mut baud = 115_200u32; + let mut words = WORDS; + let mut rest = Vec::new(); + let mut it = args.iter(); + while let Some(a) = it.next() { + match a.as_str() { + "--port" => port_path = it.next().cloned(), + "--baud" => baud = num(it.next().ok_or("--baud needs a value")?)? as u32, + "--words" => { + words = num(it.next().ok_or("--words needs a value")?)? as usize; + if !(1..=256).contains(&words) { + return Err("--words must be 1..256".into()); + } + } + "-h" | "--help" => { + println!( + "{}", + include_str!("main.rs") + .lines() + .take(13) + .map(|l| l.trim_start_matches("//!")) + .collect::>() + .join("\n") + ); + return Ok(()); + } + _ => rest.push(a.as_str()), + } + } + let path = port_path.ok_or("missing --port ")?; + let mut port = Port::open(&path, baud)?; + match rest.as_slice() { + ["ping"] => { + port.ping()?; + println!("pong"); + } + ["write", i, v] => port.write_word(index(i, words)?, num(v)?)?, + ["read", i] => println!("{:#018x}", port.read_word(index(i, words)?)?), + ["exec", op, base, offset] => { + let s = port.exec(opcode(op)?, num(base)?, num(offset)?)?; + if s.error { + println!("error"); + } else { + println!("{}", s.bit as u8); + } + } + ["selftest"] => selftest(&mut port, words, 1000, 1)?, + ["selftest", n] => selftest(&mut port, words, num(n)?, 1)?, + ["selftest", n, seed] => selftest(&mut port, words, num(n)?, num(seed)?)?, + _ => return Err("unknown command; see --help".into()), + } + Ok(()) +} + +fn main() -> ExitCode { + let args: Vec = std::env::args().skip(1).collect(); + match run(&args) { + Ok(()) => ExitCode::SUCCESS, + Err(e) => { + eprintln!("bitacc: {e}"); + ExitCode::FAILURE + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_numbers_and_opcodes() { + assert_eq!(num("0x10").unwrap(), 16); + assert_eq!(num("1_000").unwrap(), 1000); + assert!(num("zz").is_err()); + assert_eq!(opcode("set").unwrap(), 2); + assert_eq!(opcode("7").unwrap(), 7); + assert!(opcode("8").is_err()); + assert_eq!(index("255", 256).unwrap(), 255); + assert!(index("256", 256).is_err()); + assert_eq!(index("3", 4).unwrap(), 3); + assert!(index("4", 4).is_err()); + } +} diff --git a/fpga/host/src/protocol.rs b/fpga/host/src/protocol.rs new file mode 100644 index 0000000000000000000000000000000000000000..2490bbedbd539a7517ef0c8c548b3a1dc09c524d --- /dev/null +++ b/fpga/host/src/protocol.rs @@ -0,0 +1,204 @@ +//! Byte-level protocol of `fpga/rtl/fpga_top.sv`. Multi-byte fields are little-endian. + +/// Opcodes accepted by the accelerator. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Op { + Get = 0, + Test = 1, + Set = 2, + Clear = 3, + Toggle = 4, +} + +impl Op { + pub fn parse(s: &str) -> Option { + match s.to_ascii_lowercase().as_str() { + "get" => Some(Op::Get), + "test" => Some(Op::Test), + "set" => Some(Op::Set), + "clear" => Some(Op::Clear), + "toggle" => Some(Op::Toggle), + _ => None, + } + } +} + +pub const PING: u8 = b'P'; +pub const PONG: u8 = b'B'; +pub const WRITE: u8 = b'W'; +pub const WRITE_ACK: u8 = b'K'; +pub const READ: u8 = b'R'; +pub const EXEC: u8 = b'X'; +pub const UNKNOWN: u8 = b'?'; +pub const STATUS_TAG: u8 = 0xA0; +pub const STATUS_TAG_MASK: u8 = 0xFC; + +/// Number of 64-bit words in the FPGA build's RAM; byte addresses 0..WORDS*8 are in +/// range. The Tiny Tapeout chip has 4 (`--words 4`). +pub const WORDS: usize = 256; + +pub fn write_frame(index: u8, value: u64) -> Vec { + let mut f = vec![WRITE, index]; + f.extend_from_slice(&value.to_le_bytes()); + f +} + +pub fn read_frame(index: u8) -> Vec { + vec![READ, index] +} + +/// `opcode` is sent as given (3 bits), so undefined opcodes 5..=7 can be exercised. +pub fn exec_frame(opcode: u8, base: u64, offset: u64) -> Vec { + let mut f = vec![EXEC, opcode & 0x07]; + f.extend_from_slice(&base.to_le_bytes()); + f.extend_from_slice(&offset.to_le_bytes()); + f +} + +/// Result of one accelerator operation. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Status { + pub error: bool, + /// Meaningful only when `error` is false. + pub bit: bool, +} + +/// Describe a reply byte that was not the one expected. +pub fn unexpected(what: &str, byte: u8) -> String { + if byte == UNKNOWN { + format!("{what}: the FPGA did not recognize the command (reply '?')") + } else { + format!("{what}: unexpected reply 0x{byte:02x}") + } +} + +pub fn parse_status(byte: u8) -> Result { + if byte & STATUS_TAG_MASK != STATUS_TAG { + return Err(format!("unexpected status byte 0x{byte:02x}")); + } + Ok(Status { + error: byte & 0x02 != 0, + bit: byte & 0x01 != 0, + }) +} + +/// Host-side reference model of one operation on a RAM image, matching the RTL: +/// effective bit = (base << 3) + offset (mod 2^64), byte address = (bit >> 6) << 3, +/// out-of-range addresses and undefined opcodes report an error and do not write. +pub fn model_exec(ram: &mut [u64], opcode: u8, base: u64, offset: u64) -> Status { + let eff = (base << 3).wrapping_add(offset); + let word = eff >> 6; + let bit = (eff & 63) as u32; + if word >= ram.len() as u64 || opcode > 4 { + return Status { + error: true, + bit: false, + }; + } + let w = &mut ram[word as usize]; + let old = (*w >> bit) & 1 == 1; + let mask = 1u64 << bit; + let new_bit = match opcode { + 2 => { + *w |= mask; + true + } + 3 => { + *w &= !mask; + false + } + 4 => { + *w ^= mask; + !old + } + _ => old, + }; + Status { + error: false, + bit: new_bit, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn frames_are_little_endian() { + assert_eq!( + write_frame(3, 0x0102_0304_0506_0708), + vec![b'W', 3, 8, 7, 6, 5, 4, 3, 2, 1] + ); + assert_eq!(read_frame(255), vec![b'R', 255]); + let x = exec_frame(Op::Toggle as u8, 0x11, 0x2233); + assert_eq!(x.len(), 18); + assert_eq!(&x[..2], &[b'X', 4]); + assert_eq!(&x[2..10], &0x11u64.to_le_bytes()); + assert_eq!(&x[10..], &0x2233u64.to_le_bytes()); + } + + #[test] + fn status_byte() { + assert_eq!( + parse_status(0xA0), + Ok(Status { + error: false, + bit: false + }) + ); + assert_eq!( + parse_status(0xA1), + Ok(Status { + error: false, + bit: true + }) + ); + assert!(parse_status(0xA2).unwrap().error); + assert!(parse_status(b'K').is_err()); + assert!(parse_status(0xB1).is_err()); + } + + #[test] + fn model_matches_rtl_rules() { + let mut ram = [0u64; WORDS]; + ram[0] = 0x8000_0000_0000_0001; + assert_eq!( + model_exec(&mut ram, 0, 0, 63), + Status { + error: false, + bit: true + } + ); + assert_eq!( + model_exec(&mut ram, 2, 8, 6), + Status { + error: false, + bit: true + } + ); + assert_eq!(ram[1], 0x40); + // (7 << 3) + 9 = bit 65 = word 1, bit 1: was 0, toggles to 1. + assert_eq!( + model_exec(&mut ram, 4, 7, 9), + Status { + error: false, + bit: true + } + ); + assert_eq!(ram[1], 0x42); + assert!(model_exec(&mut ram, 0, 2048, 0).error); // byte 2048 is past the RAM + assert!(model_exec(&mut ram, 5, 0, 0).error); // undefined opcode + assert!(model_exec(&mut ram, 0, u64::MAX, 0).error); + + let mut small = [0u64; 4]; // Tiny Tapeout RAM: bytes 0..31 + assert!(!model_exec(&mut small, 2, 31, 7).error); + assert_eq!(small[3], 1 << 63); + assert!(model_exec(&mut small, 0, 32, 0).error); + } + + #[test] + fn op_names() { + assert_eq!(Op::parse("TOGGLE"), Some(Op::Toggle)); + assert_eq!(Op::parse("nope"), None); + } +} diff --git a/fpga/rtl/fpga_top.sv b/fpga/rtl/fpga_top.sv new file mode 100644 index 0000000000000000000000000000000000000000..8da78ab4e0f1a741c1b6cfd98c48d72df203c474 --- /dev/null +++ b/fpga/rtl/fpga_top.sv @@ -0,0 +1,162 @@ +// Board-independent hardware top: bit_accelerator + 256 x 64-bit block RAM, +// controlled over a UART (8N1). All multi-byte fields are little-endian. +// +// host -> FPGA FPGA -> host +// 'P' 'B' ping +// 'W' idx[1] data[8] 'K' write word idx +// 'R' idx[1] data[8] read word idx +// 'X' op[1] base[8] offset[8] 0xA0 | error<<1 | bit run one operation +// anything else '?' +// +// The accelerator sees the RAM at byte addresses 0 .. DEPTH*8-1. Any other +// address answers with mem_fault, so the error path works on hardware. +module fpga_top #( + parameter int CLKS_PER_BIT = 104, // clock Hz / baud + parameter int DEPTH = 256 // words; power of two +)( + input logic clk, + input logic reset, // synchronous, active high + input logic uart_rx, + output logic uart_tx, + output logic led_busy, // operation in progress + output logic led_error // last operation reported error +); + localparam int AW = $clog2(DEPTH); + + // ------------------------------------------------------------- UART + logic rx_valid; + logic [7:0] rx_data; + logic tx_start, tx_ready; + logic [7:0] tx_data; + + localparam logic [9:0] CPB = 10'(CLKS_PER_BIT); + uart_rx u_rx ( + .clk, .reset, .cpb(CPB), .rx(uart_rx), .valid(rx_valid), .data(rx_data)); + uart_tx u_tx ( + .clk, .reset, .cpb(CPB), .start(tx_start), .data(tx_data), .ready(tx_ready), .tx(uart_tx)); + + // ------------------------------------------------------------- accelerator + logic op_valid, op_ready; + logic [63:0] base_q, offset_q; + logic [2:0] opcode_q; + logic result_valid, result_bit, acc_error; + logic mem_valid, mem_write, mem_ready, mem_rvalid, mem_fault; + logic [63:0] mem_addr, mem_wdata, mem_rdata; + logic [7:0] mem_wstrb; + + bit_accelerator u_acc ( + .clk, .reset, + .op_valid, .op_ready, + .base_address(base_q), .bit_offset(offset_q), .operation(opcode_q), + .result_valid, .result_bit, .error(acc_error), + .mem_valid, .mem_write, .mem_addr, .mem_wdata, .mem_wstrb, + .mem_ready, .mem_rvalid, .mem_rdata, .mem_fault); + + // ------------------------------------------------------------- memory + logic ram_we; + logic [AW-1:0] ram_addr; + logic [63:0] ram_wdata, ram_rdata; + + word_memory #(.DEPTH(DEPTH)) u_ram ( + .clk, .we(ram_we), .addr(ram_addr), .wdata(ram_wdata), .rdata(ram_rdata)); + + typedef enum logic [3:0] { + S_CMD, S_ARGS, S_HOST_WRITE, S_HOST_READ, S_HOST_READ_DATA, + S_LOAD, S_START, S_EXEC, S_SEND + } state_t; + state_t state; + + logic [7:0] cmd; + logic [4:0] nargs, argi; // argument bytes expected / received + logic [7:0] args [0:16]; + logic [7:0] reply [0:7]; + logic [3:0] nreply, replyi; + logic in_range; + + logic unused_ok; + assign unused_ok = &{1'b0, mem_wstrb, mem_addr[2:0]}; // whole-word access, strobes all ones + + // The accelerator owns the RAM port only in S_EXEC; the host commands use it otherwise. + assign in_range = (mem_addr[63:AW+3] == '0); + assign mem_ready = (state == S_EXEC); + assign mem_rdata = ram_rdata; + assign ram_we = (state == S_EXEC) ? (mem_valid && mem_write && in_range) + : (state == S_HOST_WRITE); + assign ram_addr = (state == S_EXEC) ? mem_addr[AW+2:3] : args[0][AW-1:0]; + assign ram_wdata = (state == S_EXEC) ? mem_wdata + : {args[8], args[7], args[6], args[5], + args[4], args[3], args[2], args[1]}; + assign op_valid = (state == S_START); + assign tx_data = reply[replyi[2:0]]; + assign tx_start = (state == S_SEND) && tx_ready && (replyi < nreply); + assign led_busy = (state == S_START) || (state == S_EXEC); + + always_ff @(posedge clk) begin + if (reset) begin + state <= S_CMD; cmd <= '0; nargs <= '0; argi <= '0; + nreply <= '0; replyi <= '0; led_error <= 1'b0; + base_q <= '0; offset_q <= '0; opcode_q <= '0; + mem_rvalid <= 1'b0; mem_fault <= 1'b0; + end else begin + mem_rvalid <= 1'b0; + mem_fault <= 1'b0; + case (state) + S_CMD: if (rx_valid) begin + cmd <= rx_data; argi <= '0; replyi <= '0; + case (rx_data) + 8'h50: begin reply[0] <= 8'h42; nreply <= 4'd1; state <= S_SEND; end // 'P' + 8'h57: begin nargs <= 5'd9; state <= S_ARGS; end // 'W' + 8'h52: begin nargs <= 5'd1; state <= S_ARGS; end // 'R' + 8'h58: begin nargs <= 5'd17; state <= S_ARGS; end // 'X' + default: begin reply[0] <= 8'h3F; nreply <= 4'd1; state <= S_SEND; end + endcase + end + S_ARGS: if (rx_valid) begin + args[argi] <= rx_data; + argi <= argi + 1'b1; + if (argi + 1'b1 == nargs) begin + case (cmd) + 8'h57: state <= S_HOST_WRITE; + 8'h52: state <= S_HOST_READ; + default: state <= S_LOAD; + endcase + end + end + S_HOST_WRITE: begin // ram_we is high this cycle + reply[0] <= 8'h4B; nreply <= 4'd1; state <= S_SEND; + end + S_HOST_READ: state <= S_HOST_READ_DATA; // address presented this cycle + S_HOST_READ_DATA: begin + for (int i = 0; i < 8; i++) reply[i] <= ram_rdata[8*i +: 8]; + nreply <= 4'd8; state <= S_SEND; + end + S_LOAD: begin // operands are stable before op_valid + opcode_q <= args[0][2:0]; + base_q <= {args[8], args[7], args[6], args[5], args[4], args[3], args[2], args[1]}; + offset_q <= {args[16], args[15], args[14], args[13], args[12], args[11], args[10], args[9]}; + state <= S_START; + end + S_START: if (op_ready) state <= S_EXEC; // op_valid is high in S_START + S_EXEC: begin + // Memory slave: always ready, one-cycle read latency, fault out of range. + if (mem_valid && !mem_write) begin + if (in_range) mem_rvalid <= 1'b1; else mem_fault <= 1'b1; + end + // Unreachable here (a write always follows a successful read of the same + // word) but kept so the slave honours the interface contract. + if (mem_valid && mem_write && !in_range) mem_fault <= 1'b1; + if (result_valid) begin + reply[0] <= 8'hA0 | {6'd0, acc_error, result_bit}; + led_error <= acc_error; + nreply <= 4'd1; state <= S_SEND; + end + end + S_SEND: begin + if (tx_start) replyi <= replyi + 1'b1; + else if (replyi == nreply && tx_ready) state <= S_CMD; + end + default: state <= S_CMD; + endcase + end + end +endmodule diff --git a/fpga/rtl/power_on_reset.sv b/fpga/rtl/power_on_reset.sv new file mode 100644 index 0000000000000000000000000000000000000000..786dca8b348fb26bde257a7c6d8fae8a1dc56613 --- /dev/null +++ b/fpga/rtl/power_on_reset.sv @@ -0,0 +1,19 @@ +// Synchronous reset: held for 2^CW cycles after configuration and while the +// (already active-high) button input is pressed. The button is synchronized. +module power_on_reset #( + parameter int CW = 8 +)( + input logic clk, + input logic button, + output logic reset +); + logic [CW-1:0] count = '0; + logic b_m = 1'b0, b_s = 1'b0; + always_ff @(posedge clk) begin + b_m <= button; + b_s <= b_m; + if (b_s) count <= '0; + else if (!(&count)) count <= count + 1'b1; + end + assign reset = !(&count); +endmodule diff --git a/fpga/rtl/uart_rx.sv b/fpga/rtl/uart_rx.sv new file mode 100644 index 0000000000000000000000000000000000000000..33dcb2ada54ea01b5c8a0b717a0ef8423da6679c --- /dev/null +++ b/fpga/rtl/uart_rx.sv @@ -0,0 +1,49 @@ +// 8N1 UART receiver. cpb = clocks per bit = clock frequency / baud rate (>= 4). +// valid pulses for one cycle with the received byte in data. +module uart_rx #( + parameter int CW = 10 // width of cpb +)( + input logic clk, + input logic reset, + input logic [CW-1:0] cpb, + input logic rx, + output logic valid, + output logic [7:0] data +); + logic [CW-1:0] FULL, HALF; + assign FULL = cpb - 1'b1; + assign HALF = cpb >> 1; + + logic rx_m, rx_s; // two-flop synchronizer + logic [1:0] state; // 0 idle, 1 start, 2 data, 3 stop + logic [CW-1:0] cnt; + logic [2:0] bitn; + + always_ff @(posedge clk) begin + rx_m <= rx; + rx_s <= rx_m; + valid <= 1'b0; + if (reset) begin + state <= 2'd0; cnt <= '0; bitn <= '0; data <= '0; + rx_m <= 1'b1; rx_s <= 1'b1; + end else begin + case (state) + 2'd0: if (!rx_s) begin state <= 2'd1; cnt <= HALF; end + 2'd1: if (cnt == 0) begin + if (!rx_s) begin state <= 2'd2; cnt <= FULL; bitn <= '0; end + else state <= 2'd0; // glitch, not a start bit + end else cnt <= cnt - 1'b1; + 2'd2: if (cnt == 0) begin + data <= {rx_s, data[7:1]}; + cnt <= FULL; + if (bitn == 3'd7) state <= 2'd3; + bitn <= bitn + 1'b1; + end else cnt <= cnt - 1'b1; + default: if (cnt == 0) begin + state <= 2'd0; + valid <= rx_s; // drop bytes with a bad stop bit + end else cnt <= cnt - 1'b1; + endcase + end + end +endmodule diff --git a/fpga/rtl/uart_tx.sv b/fpga/rtl/uart_tx.sv new file mode 100644 index 0000000000000000000000000000000000000000..e4d5620169fad9206a60fe1ba4083db552066439 --- /dev/null +++ b/fpga/rtl/uart_tx.sv @@ -0,0 +1,48 @@ +// 8N1 UART transmitter. Load with start when ready is high. +module uart_tx #( + parameter int CW = 10 // width of cpb +)( + input logic clk, + input logic reset, + input logic [CW-1:0] cpb, // clocks per bit (>= 4) + input logic start, + input logic [7:0] data, + output logic ready, + output logic tx +); + logic [CW-1:0] FULL; + assign FULL = cpb - 1'b1; + + // LSB first: start, data[7:0], stop, then one guard bit so ready rises only + // after the stop bit has lasted a full bit time. + logic [10:0] shreg; + logic [3:0] left; // bit times still to send + logic [CW-1:0] cnt; + // The line is stored inverted: FPGA flip-flops power up low, so tx is idle + // high from configuration, before the first reset edge (a low line would be a + // start bit, i.e. a junk byte, to the host). No initial value is needed. + logic tx_n; + assign tx = !tx_n; + + assign ready = (left == 0); + + always_ff @(posedge clk) begin + if (reset) begin + tx_n <= 1'b0; left <= '0; cnt <= '0; shreg <= '1; + end else if (left == 0) begin + tx_n <= 1'b0; + if (start) begin + shreg <= {2'b11, data, 1'b0}; + left <= 4'd11; + cnt <= '0; + end + end else if (cnt == 0) begin + tx_n <= !shreg[0]; + shreg <= {1'b1, shreg[10:1]}; + left <= left - 1'b1; + cnt <= FULL; + end else begin + cnt <= cnt - 1'b1; + end + end +endmodule diff --git a/fpga/rtl/word_memory.sv b/fpga/rtl/word_memory.sv new file mode 100644 index 0000000000000000000000000000000000000000..99ced79821a7df0da9535dd3e82eb7db4b1dd239 --- /dev/null +++ b/fpga/rtl/word_memory.sv @@ -0,0 +1,16 @@ +// Single-port 64-bit word memory with synchronous read (maps to block RAM). +module word_memory #( + parameter int DEPTH = 256 +)( + input logic clk, + input logic we, + input logic [$clog2(DEPTH)-1:0] addr, + input logic [63:0] wdata, + output logic [63:0] rdata +); + logic [63:0] mem [0:DEPTH-1]; + always_ff @(posedge clk) begin + if (we) mem[addr] <= wdata; + rdata <= mem[addr]; + end +endmodule diff --git a/fpga/sim/cosim.sh b/fpga/sim/cosim.sh new file mode 100644 index 0000000000000000000000000000000000000000..937c6836e3827d5eebe045af392d16b2626ee895 --- /dev/null +++ b/fpga/sim/cosim.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# End-to-end check of the host tool against the RTL: builds fpga_top with +# Verilator, bridges its UART to a pseudo-terminal (sim/uart_pty.cpp), and runs +# `bitacc selftest` through that terminal exactly as it would run on a board. +# Usage: sim/cosim.sh [operations] [seed] +set -euo pipefail +cd "$(dirname "$0")/.." +OPS=${1:-2000} +SEED=${2:-1} +BUILD=${BUILD:-build} +mkdir -p "$BUILD" +rm -rf "$BUILD/pty" +verilator --cc --exe --build -O2 --timescale 1ns/1ps -Wno-fatal -Wno-lint -GCLKS_PER_BIT=8 --top-module fpga_top \ + -Mdir "$BUILD/pty" -CFLAGS -DCPB=8 -LDFLAGS -lutil \ + ../rtl/bit_accelerator.sv rtl/uart_rx.sv rtl/uart_tx.sv rtl/word_memory.sv rtl/fpga_top.sv \ + "$PWD/sim/uart_pty.cpp" > "$BUILD/pty_build.log" +(cd host && cargo build --quiet --release) + +rm -f "$BUILD/pty.path" +"$BUILD/pty/Vfpga_top" "$BUILD/pty.path" & +SIM=$! +trap 'kill $SIM 2>/dev/null || true; wait $SIM 2>/dev/null || true' EXIT +for _ in $(seq 100); do [ -s "$BUILD/pty.path" ] && break; sleep 0.1; done +PORT=$(cat "$BUILD/pty.path") +BITACC=host/target/release/bitacc + +"$BITACC" --port "$PORT" ping +test "$("$BITACC" --port "$PORT" exec 6 0 0)" = error +"$BITACC" --port "$PORT" selftest "$OPS" "$SEED" diff --git a/fpga/sim/uart_pty.cpp b/fpga/sim/uart_pty.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a499b6483bbcf4af4a0e7d7e5acacb1332207b50 --- /dev/null +++ b/fpga/sim/uart_pty.cpp @@ -0,0 +1,126 @@ +// Runs fpga_top (or, with -DTT_TOP, the Tiny Tapeout top) in Verilator and +// bridges its UART to a pseudo-terminal, so the real host tool (fpga/host) can +// talk to the simulated hardware. +// +// ./V +// +// The UART is bit-banged at CLKS_PER_BIT clocks per bit, exactly as on the +// board. The simulation runs until it receives SIGTERM. +#ifdef TT_TOP +#include "Vtt_um_snapkittyagent9nova_bitacc.h" +using Top = Vtt_um_snapkittyagent9nova_bitacc; +// ui_in[3] = RX, ui_in[7:4],[2:0] = divisor d with 4*d = CPB; uo_out[4] = TX. +static void set_rx(Top& t, int v) { + const unsigned d = CPB / 4; + t.ui_in = ((d >> 3) & 0xF) << 4 | (v & 1) << 3 | (d & 7); +} +static int get_tx(Top& t) { return (t.uo_out >> 4) & 1; } +static void set_reset(Top& t, int r) { t.rst_n = !r; t.ena = 1; t.uio_in = 0; } +#else +#include "Vfpga_top.h" +using Top = Vfpga_top; +static void set_rx(Top& t, int v) { t.uart_rx = v; } +static int get_tx(Top& t) { return t.uart_tx; } +static void set_reset(Top& t, int r) { t.reset = r; } +#endif +#include "verilated.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef CPB +#define CPB 8 +#endif + +static volatile std::sig_atomic_t stop = 0; +static void on_signal(int) { stop = 1; } + +int main(int argc, char** argv) { + if (argc < 2) { std::fprintf(stderr, "usage: %s \n", argv[0]); return 2; } + int master, slave; + char name[256]; + struct termios tio; + cfmakeraw(&tio); + if (openpty(&master, &slave, name, &tio, nullptr) != 0) { std::perror("openpty"); return 1; } + fcntl(master, F_SETFL, fcntl(master, F_GETFL) | O_NONBLOCK); + FILE* f = std::fopen(argv[1], "w"); + std::fprintf(f, "%s\n", name); + std::fclose(f); + std::signal(SIGTERM, on_signal); + std::signal(SIGINT, on_signal); + + Verilated::commandArgs(argc, argv); + Top top; + top.clk = 0; set_reset(top, 1); set_rx(top, 1); + + std::deque to_fpga; + int rx_phase = -1, rx_cnt = 0; // host -> FPGA serializer + unsigned rx_frame = 0; + int tx_state = 0, tx_cnt = 0, tx_bit = 0; // FPGA -> host deserializer + unsigned char tx_byte = 0; + unsigned long long cycle = 0; + + while (!stop) { + // Poll the pty only every few hundred cycles to keep the simulation fast. + if ((cycle & 255) == 0) { + unsigned char buf[256]; + ssize_t n = read(master, buf, sizeof buf); + for (ssize_t i = 0; i < n; i++) to_fpga.push_back(buf[i]); + } + if (cycle == 10) set_reset(top, 0); + + // Serializer: start bit, 8 data bits LSB first, stop bit. + if (rx_phase < 0 && !to_fpga.empty()) { + rx_frame = (1u << 9) | (unsigned(to_fpga.front()) << 1); + to_fpga.pop_front(); + rx_phase = 0; rx_cnt = 0; + } + if (rx_phase >= 0) { + set_rx(top, (rx_frame >> rx_phase) & 1); + if (++rx_cnt == CPB) { rx_cnt = 0; if (++rx_phase == 10) rx_phase = -1; } + } else { + set_rx(top, 1); + } + + top.clk = 1; top.eval(); + top.clk = 0; top.eval(); + cycle++; + + // Deserializer: sample in the middle of each bit. + switch (tx_state) { + case 0: + if (!get_tx(top)) { tx_state = 1; tx_cnt = CPB / 2; } + break; + case 1: + if (--tx_cnt == 0) { + if (get_tx(top)) tx_state = 0; // glitch + else { tx_state = 2; tx_cnt = CPB; tx_bit = 0; tx_byte = 0; } + } + break; + case 2: + if (--tx_cnt == 0) { + tx_byte |= (unsigned char)(get_tx(top) << tx_bit); + tx_cnt = CPB; + if (++tx_bit == 8) tx_state = 3; + } + break; + default: + if (--tx_cnt == 0) { + if (get_tx(top)) { while (write(master, &tx_byte, 1) < 0 && errno == EAGAIN) {} } + else std::fprintf(stderr, "uart_pty: framing error\n"); + tx_state = 0; + } + } + } + top.final(); + close(slave); + close(master); + return 0; +} diff --git a/fpga/tb/tb_fpga_top.sv b/fpga/tb/tb_fpga_top.sv new file mode 100644 index 0000000000000000000000000000000000000000..07e180cbb5a2c952b8bc15c85ac23563b10b4498 --- /dev/null +++ b/fpga/tb/tb_fpga_top.sv @@ -0,0 +1,230 @@ +// UART-level testbench: drives the serial line exactly as a host would and +// checks every reply against a reference model of the RAM and the accelerator. +// Exits non-zero on any failure. +// +// Default: fpga_top (256 words). With +define+TT_TOP it tests the Tiny Tapeout +// top tt_um_snapkittyagent9nova_bitacc (4 words, divisor from ui_in pins). +// +define+GATE_LEVEL instantiates a synthesized netlist without parameters. +`timescale 1ns/1ps +module tb_fpga_top; +`ifdef TT_TOP + localparam int DEPTH = 4; +`ifdef TT_DEFAULT_BAUD + localparam int CPB = 104; // divisor pins all low -> 104 clocks per bit +`else + localparam int CPB = 8; // ui_in divisor d = 2 -> 4*d clocks per bit +`endif +`else + localparam int DEPTH = 256; + localparam int CPB = 8; // clocks per UART bit (small to keep simulation fast) +`endif + + logic clk = 1'b0, reset = 1'b1, uart_rx = 1'b1; + logic uart_tx, led_busy, led_error; + +`ifdef TT_TOP + logic [7:0] uo_out, uio_out, uio_oe; +`ifdef TT_DEFAULT_BAUD + logic [6:0] div = 7'd0; +`else + logic [6:0] div = 7'(CPB / 4); +`endif +`ifdef GL_TEST + supply1 vpwr; // the powered post-layout netlist has inout supply pins + supply0 vgnd; +`endif + tt_um_snapkittyagent9nova_bitacc dut ( +`ifdef GL_TEST + .VPWR(vpwr), .VGND(vgnd), +`endif + .ui_in({div[6:3], uart_rx, div[2:0]}), .uo_out, .uio_in(8'h00), .uio_out, .uio_oe, + .ena(1'b1), .clk, .rst_n(!reset)); + assign uart_tx = uo_out[4]; + assign led_busy = uo_out[0]; + assign led_error = uo_out[1]; +`elsif GATE_LEVEL + fpga_top dut (.*); // synthesized netlist, built with CLKS_PER_BIT = 8 +`else + fpga_top #(.CLKS_PER_BIT(CPB), .DEPTH(DEPTH)) dut (.*); +`endif + + always #5 clk = ~clk; + + integer checks = 0, fails = 0; + task automatic check(input bit cond, input string what); + checks++; + if (!cond) begin fails++; $display("FAIL @%0t: %s", $time, what); end + endtask + + // ------------------------------------------------ receiver: tx line -> FIFO + logic [7:0] rxq [0:1023]; + integer rxq_wr = 0, rxq_rd = 0; + initial begin : receiver + logic [7:0] b; + forever begin + @(negedge uart_tx); + repeat (CPB / 2) @(posedge clk); + if (uart_tx !== 1'b0) check(0, "false start bit on tx"); + else begin + for (int i = 0; i < 8; i++) begin + repeat (CPB) @(posedge clk); + b[i] = uart_tx; + end + repeat (CPB) @(posedge clk); + check(uart_tx === 1'b1, "tx stop bit"); + rxq[rxq_wr % 1024] = b; + rxq_wr++; + end + end + end + + // ------------------------------------------------ host side + // Inputs change on the falling edge so no flip-flop samples them mid-update + // (a reset released exactly on a rising edge is seen by some flip-flops of a + // gate-level netlist and not by others). + task automatic send_byte(input logic [7:0] b); + @(negedge clk); + uart_rx = 1'b0; repeat (CPB) @(negedge clk); + for (int i = 0; i < 8; i++) begin uart_rx = b[i]; repeat (CPB) @(negedge clk); end + uart_rx = 1'b1; repeat (CPB) @(negedge clk); + endtask + + task automatic recv_byte(output logic [7:0] b, output bit ok); + integer waited = 0; + while (rxq_rd == rxq_wr && waited < 400 * CPB) begin @(posedge clk); waited++; end + ok = (rxq_rd != rxq_wr); + b = ok ? rxq[rxq_rd % 1024] : 8'hxx; + if (ok) rxq_rd++; + check(ok, "reply byte timeout"); + endtask + + task automatic send_u64(input logic [63:0] v); + for (int i = 0; i < 8; i++) send_byte(v[8*i +: 8]); + endtask + + // ------------------------------------------------ reference model + logic [63:0] model [0:DEPTH-1]; + + task automatic cmd_write(input logic [7:0] idx, input logic [63:0] v); + logic [7:0] r; bit ok; + send_byte("W"); send_byte(idx); send_u64(v); + recv_byte(r, ok); + check(r == "K", $sformatf("W %0d reply %h", idx, r)); + model[idx] = v; + endtask + + task automatic cmd_read_check(input logic [7:0] idx); + logic [7:0] r; logic [63:0] v; bit ok; + send_byte("R"); send_byte(idx); + for (int i = 0; i < 8; i++) begin recv_byte(r, ok); v[8*i +: 8] = r; end + check(v === model[idx], $sformatf("R %0d = %h, expected %h", idx, v, model[idx])); + endtask + + task automatic cmd_exec(input logic [2:0] op, input logic [63:0] base, input logic [63:0] off); + logic [63:0] eb, addr, word, onehot; + logic [5:0] bi; + bit in_range, exp_err, exp_bit, ok; + logic [7:0] r; + eb = (base << 3) + off; + addr = (eb >> 6) << 3; + bi = eb[5:0]; + onehot = 64'd1 << bi; + in_range = (addr >> 3) < DEPTH; + word = in_range ? model[addr >> 3] : '0; + exp_err = !in_range || op > 3'd4; + exp_bit = 1'b0; + if (!exp_err) begin + case (op) + 3'd2: begin model[addr >> 3] = word | onehot; exp_bit = 1'b1; end + 3'd3: begin model[addr >> 3] = word & ~onehot; exp_bit = 1'b0; end + 3'd4: begin model[addr >> 3] = word ^ onehot; exp_bit = !word[bi]; end + default: exp_bit = word[bi]; + endcase + end + send_byte("X"); send_byte({5'd0, op}); send_u64(base); send_u64(off); + recv_byte(r, ok); + check(r[7:2] == 6'b101000, $sformatf("X status tag %h", r)); + check(r[1] == exp_err, $sformatf("X op %0d base %h off %h: error %b expected %b", op, base, off, r[1], exp_err)); + if (!exp_err) check(r[0] == exp_bit, $sformatf("X op %0d base %h off %h: bit %b expected %b", op, base, off, r[0], exp_bit)); + check(led_error == exp_err, "led_error follows the last operation"); + endtask + + // ------------------------------------------------ stimulus + integer seed = 5; + logic [7:0] r; bit ok; + logic [63:0] rb, ro; + logic [2:0] rop; + + initial begin +`ifdef TT_TOP + // ASIC flip-flops have no power-up value; TX must be high while in reset. + repeat (2) @(negedge clk); + check(uart_tx === 1'b1, "tx high during reset"); + repeat (3) @(negedge clk); +`elsif GATE_LEVEL + // FPGA netlist: flip-flops power up low (the cell models encode this), so TX + // must idle high from power-up, before any reset edge; a low line here is a + // start bit, i.e. a junk byte, to the host. + #1 check(uart_tx === 1'b1, "tx idle-high at power-up"); + repeat (5) @(negedge clk); +`else + // RTL has no power-up value (X); TX must be high while in reset. The + // power-up property is checked on the gate-level netlists. + repeat (2) @(negedge clk); + check(uart_tx === 1'b1, "tx high during reset"); + repeat (3) @(negedge clk); +`endif + reset = 1'b0; + repeat (5) @(negedge clk); + + send_byte("P"); recv_byte(r, ok); check(r == "B", $sformatf("ping reply %h", r)); + send_byte("Z"); recv_byte(r, ok); check(r == "?", $sformatf("unknown command reply %h", r)); + + for (int i = 0; i < DEPTH; i++) cmd_write(i[7:0], {$random(seed), $random(seed)}); + for (int i = 0; i < DEPTH; i += 17) cmd_read_check(i[7:0]); + cmd_read_check(8'(DEPTH - 1)); + + cmd_write(0, 64'h8000_0000_0000_0001); + cmd_write(1, 64'h0000_0000_0000_0040); + cmd_exec(3'd0, 0, 0); // GET word0 bit0 + cmd_exec(3'd0, 0, 63); // GET word0 bit63 + cmd_exec(3'd1, 0, 70); // TEST word1 bit6 + cmd_exec(3'd2, 0, 5); cmd_read_check(0); + cmd_exec(3'd3, 0, 63); cmd_read_check(0); + cmd_exec(3'd4, 8, 6); cmd_read_check(1); // base 8 = word 1 + cmd_exec(3'd4, 7, 9); cmd_read_check(1); // crosses into word 1 + cmd_exec(3'd2, DEPTH * 8 - 1, 7); cmd_read_check(8'(DEPTH - 1)); // last bit of RAM + cmd_exec(3'd0, DEPTH * 8, 0); // first byte past RAM: read fault + cmd_exec(3'd2, 0, DEPTH * 64); // first bit past RAM: fault, no write + cmd_exec(3'd0, 64'hFFFF_FFFF_FFFF_FFFF, 0); + cmd_exec(3'd5, 0, 0); // undefined opcode + cmd_exec(3'd7, 0, 0); + cmd_exec(3'd0, 0, 0); // clean op after errors + for (int i = 0; i < 4; i++) cmd_read_check(i[7:0]); + + for (int n = 0; n < 300; n++) begin + rop = $unsigned($random(seed)) % 8; + if (rop > 4 && ($unsigned($random(seed)) % 3 != 0)) rop = rop - 3'd4; + rb = $unsigned($random(seed)) % (DEPTH * 8); + ro = $unsigned($random(seed)) % (DEPTH * 64); + if ($unsigned($random(seed)) % 10 == 0) rb = {$random(seed), $random(seed)}; + cmd_exec(rop, rb, ro); + if (n % 25 == 0) cmd_read_check(8'((rb >> 3) % DEPTH)); + end + for (int i = 0; i < DEPTH; i++) cmd_read_check(i[7:0]); + + $display("checks=%0d fails=%0d", checks, fails); + if (fails != 0) $fatal(1, "FAIL"); +`ifdef TT_TOP + $display("PASS: tt_um_snapkittyagent9nova_bitacc UART tests"); +`else + $display("PASS: fpga_top UART tests"); +`endif + $finish; + end + + initial begin + #2_000_000_000; + $fatal(1, "global timeout"); + end +endmodule diff --git a/gpu/Cargo.lock b/gpu/Cargo.lock new file mode 100644 index 0000000000000000000000000000000000000000..9793845354f031d8b98b530aee23c7312665421b --- /dev/null +++ b/gpu/Cargo.lock @@ -0,0 +1,260 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cl-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4febd824a957638c066180fbf72b2bed5bcee33740773f3dc59fe91f0a3e6595" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "enum_primitive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4551092f4d519593039259a9ed8daedf0da12e5109c5280338073eaeb81180" +dependencies = [ + "num-traits 0.1.43", +] + +[[package]] +name = "futures" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" + +[[package]] +name = "gpu" +version = "0.1.0" +dependencies = [ + "ocl", +] + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits 0.2.19", +] + +[[package]] +name = "num-traits" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" +dependencies = [ + "num-traits 0.2.19", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "ocl" +version = "0.19.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4297afb442d411793e4e24ee5a2977d15b6c95c743418f1c0ce0a2397d7ec8a3" +dependencies = [ + "futures", + "nodrop", + "num-traits 0.2.19", + "ocl-core", + "qutex", + "thiserror", +] + +[[package]] +name = "ocl-core" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c145dd9f205b86611a5df15eb89517417b03005441cf6cec245c65a4b9248c52" +dependencies = [ + "bitflags", + "cl-sys", + "enum_primitive", + "num-complex", + "num-traits 0.2.19", + "ocl-core-vector", + "rustc_version", + "thiserror", +] + +[[package]] +name = "ocl-core-vector" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f562279e046ca160aeed5eaf6f7c4eb9fa56cb8fd9d038dbdbf56225caeb8074" +dependencies = [ + "num-traits 0.2.19", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "qutex" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11778238e7d8b0e3ca62033fdc69e01ef5cdb08809cdc2398b2ce5ec873a1757" +dependencies = [ + "crossbeam", + "futures", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/gpu/Cargo.toml b/gpu/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..08c35be5e85aec9ffa8af07fc3c586896955e777 --- /dev/null +++ b/gpu/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "gpu" +version = "0.1.0" +edition = "2024" +rust-version = "1.85" +description = "Composable element-wise OpenCL kernels over canonical u8 Boolean buffers" +repository = "https://github.com/SNAPKITTYAGENT9NOVA/bit-string-accelerator" +license = "EPL-2.0" +publish = false + +[dependencies] +ocl = "0.19" diff --git a/gpu/benchmark.cu b/gpu/benchmark.cu new file mode 100644 index 0000000000000000000000000000000000000000..0418d6ea9091a1af0f75be1a1a4086038ae0ace6 --- /dev/null +++ b/gpu/benchmark.cu @@ -0,0 +1,305 @@ +#include +#include +#include +#include + +// Cache sizes for H100 (accurate) +#define L1_CACHE_SIZE (256 * 1024) // 256KB L1 cache per SM +#define L2_CACHE_SIZE (50 * 1024 * 1024) // 50MB L2 cache total + +// Test sizes - adjusted for H100's larger L1 cache +#define FREQ_DATA_SIZE (32 * 1024) // 32KB - data that should stay in L1 cache +#define STREAM_DATA_SIZE (40 * 1024 * 1024) // 40MB - streaming data + +// Number of iterations +#define WARM_UP_ITERATIONS 5 +#define BENCHMARK_ITERATIONS 20 + +// Access frequency for hot data +#define ACCESS_FREQUENCY 50 + +// Enum for different load types +enum LoadType { + STANDARD_LOAD, + SPECIALIZED_LOAD +}; + +// Kernel that demonstrates cache pollution effects +template +__global__ void cachePollutionKernel( + float* frequentlyAccessed, // Data that should stay in L1 cache + const float* streamingData, // Large data to stream through once + float* results, // Output for frequently accessed data + int freqSize, // Size of frequently accessed data + int streamSize, // Size of streaming data + int accessFrequency) // How many times to access the frequent data +{ + int tid = blockIdx.x * blockDim.x + threadIdx.x; + + // Thread-local accumulator + float sum = 0.0f; + + // First, access the frequently accessed data and read it into registers + // This should get the data into L1 cache + for (int i = tid; i < freqSize; i += blockDim.x * gridDim.x) { + sum += frequentlyAccessed[i]; + } + + // Now stream through the large data array once + // This could potentially evict the frequently accessed data from L1 cache + for (int i = tid; i < streamSize; i += blockDim.x * gridDim.x) { + float val; + + if (streamingLoadType == STANDARD_LOAD) { + // Standard load for streaming data + val = streamingData[i]; + } + else { + #if __CUDA_ARCH__ >= 900 + asm volatile("ld.global.nc.L1::no_allocate.L2::256B.f32 %0, [%1];" : "=f"(val) : "l"(&streamingData[i])); + #else + val = streamingData[i]; + #endif + } + + // Do something with the value so it doesn't get optimized away + sum += val * 0.0001f; + } + + // Now access the frequent data multiple times + // If it was evicted from L1 cache, this will be slower + for (int freq = 0; freq < accessFrequency; freq++) { + for (int i = tid; i < freqSize; i += blockDim.x * gridDim.x) { + sum += frequentlyAccessed[i] * 1.01f; + } + } + + // Store the result + if (tid < freqSize) { + results[tid] = sum; + } +} + +// Function to run the benchmark with a specific load type +float runBenchmark(LoadType loadType, const char* benchmarkName) { + float *d_freqData, *d_streamData, *d_results; + float totalTime = 0.0f; + cudaEvent_t start, stop; + + // Allocate device memory + cudaMalloc(&d_freqData, FREQ_DATA_SIZE * sizeof(float)); + cudaMalloc(&d_streamData, STREAM_DATA_SIZE * sizeof(float)); + cudaMalloc(&d_results, FREQ_DATA_SIZE * sizeof(float)); + + // Initialize data + cudaMemset(d_freqData, 0xAA, FREQ_DATA_SIZE * sizeof(float)); + cudaMemset(d_streamData, 0xBB, STREAM_DATA_SIZE * sizeof(float)); + + // Create timing events + cudaEventCreate(&start); + cudaEventCreate(&stop); + + // Set up kernel launch parameters + dim3 blockSize(256); + dim3 gridSize(512); // Use many blocks to increase parallelism + + // Warm-up runs + printf("Warming up %s kernel (%d iterations)...\n", benchmarkName, WARM_UP_ITERATIONS); + + for (int i = 0; i < WARM_UP_ITERATIONS; i++) { + if (loadType == STANDARD_LOAD) { + cachePollutionKernel<<>>( + d_freqData, d_streamData, d_results, + FREQ_DATA_SIZE, STREAM_DATA_SIZE, ACCESS_FREQUENCY); + } + else { + cachePollutionKernel<<>>( + d_freqData, d_streamData, d_results, + FREQ_DATA_SIZE, STREAM_DATA_SIZE, ACCESS_FREQUENCY); + } + } + + cudaDeviceSynchronize(); + + // Main benchmark runs + printf("Running %s benchmark (%d iterations)...\n", benchmarkName, BENCHMARK_ITERATIONS); + + for (int i = 0; i < BENCHMARK_ITERATIONS; i++) { + float milliseconds = 0.0f; + cudaDeviceSynchronize(); // Ensure GPU is idle + + // Record start time + cudaEventRecord(start); + + // Launch appropriate kernel + if (loadType == STANDARD_LOAD) { + cachePollutionKernel<<>>( + d_freqData, d_streamData, d_results, + FREQ_DATA_SIZE, STREAM_DATA_SIZE, ACCESS_FREQUENCY); + } + else { + cachePollutionKernel<<>>( + d_freqData, d_streamData, d_results, + FREQ_DATA_SIZE, STREAM_DATA_SIZE, ACCESS_FREQUENCY); + } + + // Record end time + cudaEventRecord(stop); + cudaEventSynchronize(stop); + + // Check for errors + cudaError_t err = cudaGetLastError(); + if (err != cudaSuccess) { + printf("Error in %s kernel: %s\n", benchmarkName, cudaGetErrorString(err)); + continue; + } + + // Calculate elapsed time + cudaEventElapsedTime(&milliseconds, start, stop); + totalTime += milliseconds; + + printf(" Iteration %2d: %.3f ms\n", i+1, milliseconds); + } + + // Calculate average time + float avgTime = totalTime / BENCHMARK_ITERATIONS; + + // Clean up + cudaFree(d_freqData); + cudaFree(d_streamData); + cudaFree(d_results); + cudaEventDestroy(start); + cudaEventDestroy(stop); + + return avgTime; +} + +// Function to verify the results from both implementations are equivalent +bool verifyResults() { + float *d_freqData, *d_streamData; + float *d_standardResults, *d_specializedResults; + float *h_standardResults, *h_specializedResults; + bool resultsMatch = true; + + // Allocate device memory + cudaMalloc(&d_freqData, FREQ_DATA_SIZE * sizeof(float)); + cudaMalloc(&d_streamData, STREAM_DATA_SIZE * sizeof(float)); + cudaMalloc(&d_standardResults, FREQ_DATA_SIZE * sizeof(float)); + cudaMalloc(&d_specializedResults, FREQ_DATA_SIZE * sizeof(float)); + + // Allocate host memory for results + h_standardResults = (float*)malloc(FREQ_DATA_SIZE * sizeof(float)); + h_specializedResults = (float*)malloc(FREQ_DATA_SIZE * sizeof(float)); + + // Initialize data with fixed values for reproducibility + cudaMemset(d_freqData, 0x42, FREQ_DATA_SIZE * sizeof(float)); + cudaMemset(d_streamData, 0x43, STREAM_DATA_SIZE * sizeof(float)); + + // Set up kernel launch parameters + dim3 blockSize(256); + dim3 gridSize(512); + + // Run both kernels once + cachePollutionKernel<<>>( + d_freqData, d_streamData, d_standardResults, + FREQ_DATA_SIZE, STREAM_DATA_SIZE, 1); // Just one access for verification + + cachePollutionKernel<<>>( + d_freqData, d_streamData, d_specializedResults, + FREQ_DATA_SIZE, STREAM_DATA_SIZE, 1); // Just one access for verification + + // Copy results back to host + cudaMemcpy(h_standardResults, d_standardResults, FREQ_DATA_SIZE * sizeof(float), cudaMemcpyDeviceToHost); + cudaMemcpy(h_specializedResults, d_specializedResults, FREQ_DATA_SIZE * sizeof(float), cudaMemcpyDeviceToHost); + + // Compare results (allow for small floating-point differences) + const float epsilon = 1e-5f; + for (int i = 0; i < FREQ_DATA_SIZE; i++) { + float diff = fabs(h_standardResults[i] - h_specializedResults[i]); + if (diff > epsilon) { + printf("Results mismatch at index %d: Standard=%.6f, Specialized=%.6f\n", + i, h_standardResults[i], h_specializedResults[i]); + resultsMatch = false; + break; + } + } + + // Clean up + cudaFree(d_freqData); + cudaFree(d_streamData); + cudaFree(d_standardResults); + cudaFree(d_specializedResults); + free(h_standardResults); + free(h_specializedResults); + + return resultsMatch; +} + +int main() { + // Check CUDA device + cudaDeviceProp prop; + cudaError_t err = cudaGetDeviceProperties(&prop, 0); + + if (err != cudaSuccess) { + printf("Error getting device properties: %s\n", cudaGetErrorString(err)); + return 1; + } + + printf("\n=== CACHE POLLUTION BENCHMARK ===\n\n"); + + printf("GPU: %s (Compute Capability %d.%d)\n", + prop.name, prop.major, prop.minor); + + printf("L1 Cache per SM: ~256KB\n"); // Updated for H100 + printf("L2 Cache Total: ~50MB\n"); // Updated for H100 + printf("Benchmark Data Sizes:\n"); + printf(" - Frequently accessed data: %d KB\n", FREQ_DATA_SIZE / 1024); + printf(" - Streaming data: %d MB\n", STREAM_DATA_SIZE / (1024 * 1024)); + printf(" - Access frequency for hot data: %d times\n\n", ACCESS_FREQUENCY); + + // Verify that both implementations produce the same results + printf("Verifying both implementations produce equivalent results...\n"); + bool resultsMatch = verifyResults(); + if (!resultsMatch) { + printf("ERROR: Results don't match! This indicates a problem with the implementation.\n"); + return 1; + } + printf("Results verified: Both implementations produce equivalent results.\n\n"); + + // Run standard load benchmark + float standardTime = runBenchmark(STANDARD_LOAD, "Standard Load"); + + // Run specialized load benchmark + float specializedTime = runBenchmark(SPECIALIZED_LOAD, "Specialized Load"); + + // Print results + printf("\n=== BENCHMARK RESULTS ===\n"); + printf("Standard Load: %.3f ms\n", standardTime); + printf("Specialized Load: %.3f ms\n", specializedTime); + + double speedup = standardTime / specializedTime; + printf("Speedup: %.3fx\n", speedup); + + if (speedup > 1.05) { + printf("\nRESULT: Specialized load is FASTER (%.1f%% improvement)\n", + (speedup - 1.0) * 100.0); + } else if (speedup < 0.95) { + printf("\nRESULT: Specialized load is SLOWER (%.1f%% slower)\n", + (1.0 - speedup) * 100.0); + } else { + printf("\nRESULT: Performance is EQUIVALENT (within 5%%)\n"); + } + + printf("\n"); + printf("This benchmark demonstrates cache pollution effects:\n"); + printf("1. First, we load small data that should ideally stay in L1 cache\n"); + printf("2. Then, we stream through large data that could evict the small data\n"); + printf(" - Standard loads may pollute L1 cache with streaming data\n"); + printf(" - Specialized loads with L1::no_allocate bypass L1 cache\n"); + printf("3. Finally, we access the small data again multiple times\n"); + printf("\n"); + printf("If specialized loads are faster, it indicates they're preserving \n"); + printf("the important data in L1 cache by not polluting it with streaming data.\n"); + + return 0; +} \ No newline at end of file diff --git a/gpu/notes.txt b/gpu/notes.txt new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/gpu/src/demo.rs b/gpu/src/demo.rs new file mode 100644 index 0000000000000000000000000000000000000000..497556d1c78a27194791e2405123e9b91a480d07 --- /dev/null +++ b/gpu/src/demo.rs @@ -0,0 +1,70 @@ +// Functor-based GPU architecture demonstration +// This shows the composable kernel design without requiring actual GPU hardware + +trait Kernel: Sized { + fn name() -> &'static str; + fn local_size() -> usize; + fn compute(&self, gid: usize, lid: usize) -> f32; +} + +struct LocalIdKernel; + +impl Kernel for LocalIdKernel { + fn name() -> &'static str { "local_id" } + fn local_size() -> usize { 4 } + + fn compute(&self, _gid: usize, lid: usize) -> f32 { + lid as f32 + } +} + +struct GlobalIdKernel; + +impl Kernel for GlobalIdKernel { + fn name() -> &'static str { "global_id" } + fn local_size() -> usize { 4 } + + fn compute(&self, gid: usize, _lid: usize) -> f32 { + gid as f32 + } +} + +fn execute(kernel: K, work_size: usize) -> Vec { + let local_size = K::local_size(); + (0..work_size) + .map(|gid| { + let local_id = gid % local_size; + kernel.compute(gid, local_id) + }) + .collect() +} + +fn format_output(data: &[f32]) -> String { + data.iter() + .enumerate() + .fold(String::new(), |acc, (i, &val)| { + if i > 0 && i % 16 == 0 { + format!("{}\n{:>3} ", acc, val as i32) + } else { + format!("{}{:>3} ", acc, val as i32) + } + }) +} + +fn main() { + println!("=== Functor-Based GPU Kernel Architecture Demo ===\n"); + + println!("Kernel: {} (local_size={})", LocalIdKernel::name(), LocalIdKernel::local_size()); + let result1 = execute(LocalIdKernel, 128); + println!("Output: {}\n", format_output(&result1)); + + println!("Kernel: {} (local_size={})", GlobalIdKernel::name(), GlobalIdKernel::local_size()); + let result2 = execute(GlobalIdKernel, 128); + println!("Output: {}\n", format_output(&result2)); + + println!("=== Architecture Proof ==="); + println!("✓ Kernel trait abstracts GPU operations"); + println!("✓ execute() is a higher-order function parametrized on Kernel"); + println!("✓ Immutable data flow: kernel.compute() → Vec"); + println!("✓ Type-safe kernel composition via trait bounds"); +} diff --git a/gpu/src/functor/add.rs b/gpu/src/functor/add.rs new file mode 100644 index 0000000000000000000000000000000000000000..15b2fc45d3e9a397453673ff1f731b530e5f94f9 --- /dev/null +++ b/gpu/src/functor/add.rs @@ -0,0 +1,52 @@ +use super::{GpuContext, Result}; +use ocl::Buffer; + +pub(super) const KERNEL_SRC: &str = r#" +__kernel void add_u8(__global const uchar* a, + __global const uchar* b, + __global uchar* output, + const uint count) { + size_t i = get_global_id(0); + if (i >= count) { + return; + } + output[i] = (uchar)(a[i] + b[i]); +} +"#; + +/// Element-wise wrapping `u8` addition: `output[i] = a[i] + b[i]`. +/// On Booleans this is a counting sum (0, 1 or 2), so `Negate(Add(x, y))` is NOR. +#[derive(Clone, Copy, Debug, Default)] +pub struct Add; + +impl Add { + pub fn launch( + &self, + ctx: &GpuContext, + a: &Buffer, + b: &Buffer, + output: &Buffer, + count: usize, + ) -> Result<()> { + let (global, local) = ctx.work_sizes( + count, + &[("a", a.len()), ("b", b.len()), ("output", output.len())], + )?; + if count == 0 { + return Ok(()); + } + let kernel = ctx + .proque() + .kernel_builder("add_u8") + .global_work_size(global) + .local_work_size(local) + .arg(a) + .arg(b) + .arg(output) + .arg(count as u32) + .build()?; + // SAFETY: see Negate::launch. + unsafe { kernel.enq()? }; + Ok(()) + } +} diff --git a/gpu/src/functor/agents.rs b/gpu/src/functor/agents.rs new file mode 100644 index 0000000000000000000000000000000000000000..94cc5f7054543134086602a250cf1c151031c4ec --- /dev/null +++ b/gpu/src/functor/agents.rs @@ -0,0 +1,170 @@ +use super::{GpuContext, GpuError, Result, UnaryOp}; +use ocl::Buffer; + +pub struct ValidationAgent; + +impl ValidationAgent { + pub fn validate_input(count: usize) -> Result<()> { + if count == 0 { + return Err(GpuError::EmptyBuffer); + } + if count > u32::MAX as usize { + return Err(GpuError::InvalidCount(count)); + } + Ok(()) + } + + pub fn validate_buffers(bufs: &[(&'static str, usize)], count: usize) -> Result<()> { + for &(which, len) in bufs { + if len < count { + return Err(GpuError::BufferTooShort { which, len, count }); + } + } + Ok(()) + } + + pub fn allocate(ctx: &GpuContext, count: usize) -> Result> { + Self::validate_input(count)?; + ctx.alloc(count) + } +} + +pub struct ExecutionAgent; + +impl ExecutionAgent { + pub fn execute( + ctx: &GpuContext, + op: &Op, + input: &Buffer, + output: &Buffer, + count: usize, + ) -> Result<()> { + ValidationAgent::validate_input(count)?; + ValidationAgent::validate_buffers( + &[("input", input.len()), ("output", output.len())], + count, + )?; + op.launch(ctx, input, output, count) + } + + pub fn execute_composed( + ctx: &GpuContext, + f: &F, + g: &G, + input: &Buffer, + output: &Buffer, + count: usize, + ) -> Result<()> { + ValidationAgent::validate_input(count)?; + ValidationAgent::validate_buffers( + &[("input", input.len()), ("output", output.len())], + count, + )?; + let tmp = ctx.alloc(count)?; + f.launch(ctx, input, &tmp, count)?; + g.launch(ctx, &tmp, output, count) + } +} + +pub struct VerificationAgent; + +impl VerificationAgent { + pub fn download(ctx: &GpuContext, buf: &Buffer) -> Result> { + ctx.download_raw(buf) + } + + pub fn download_bools(ctx: &GpuContext, buf: &Buffer, count: usize) -> Result> { + ctx.download_bools(buf, count) + } + + pub fn verify_equal(actual: &[u8], expected: &[u8]) -> Result<()> { + if actual != expected { + return Err(GpuError::Ocl(ocl::Error::from( + "verification failed: output mismatch", + ))); + } + Ok(()) + } + + pub fn verify_bools_equal(actual: &[bool], expected: &[bool]) -> Result<()> { + if actual != expected { + return Err(GpuError::Ocl(ocl::Error::from( + "verification failed: boolean output mismatch", + ))); + } + Ok(()) + } +} + +pub struct Workflow { + ctx: GpuContext, +} + +impl Workflow { + pub fn new() -> Result { + let ctx = GpuContext::new()?; + Ok(Workflow { ctx }) + } + + pub fn run_unary(&self, op: &Op, input: &[u8]) -> Result> { + let count = input.len(); + ValidationAgent::validate_input(count)?; + + let inp_buf = self.ctx.upload_raw(input)?; + let out_buf = ValidationAgent::allocate(&self.ctx, count)?; + + ExecutionAgent::execute(&self.ctx, op, &inp_buf, &out_buf, count)?; + + VerificationAgent::download(&self.ctx, &out_buf) + } + + pub fn run_unary_bools(&self, op: &Op, input: &[bool]) -> Result> { + let count = input.len(); + ValidationAgent::validate_input(count)?; + + let inp_buf = self.ctx.upload_bools(input)?; + let out_buf = ValidationAgent::allocate(&self.ctx, count)?; + + ExecutionAgent::execute(&self.ctx, op, &inp_buf, &out_buf, count)?; + + VerificationAgent::download_bools(&self.ctx, &out_buf, count) + } + + pub fn run_composed( + &self, + f: &F, + g: &G, + input: &[u8], + ) -> Result> { + let count = input.len(); + ValidationAgent::validate_input(count)?; + + let inp_buf = self.ctx.upload_raw(input)?; + let out_buf = ValidationAgent::allocate(&self.ctx, count)?; + + ExecutionAgent::execute_composed(&self.ctx, f, g, &inp_buf, &out_buf, count)?; + + VerificationAgent::download(&self.ctx, &out_buf) + } + + pub fn run_composed_bools( + &self, + f: &F, + g: &G, + input: &[bool], + ) -> Result> { + let count = input.len(); + ValidationAgent::validate_input(count)?; + + let inp_buf = self.ctx.upload_bools(input)?; + let out_buf = ValidationAgent::allocate(&self.ctx, count)?; + + ExecutionAgent::execute_composed(&self.ctx, f, g, &inp_buf, &out_buf, count)?; + + VerificationAgent::download_bools(&self.ctx, &out_buf, count) + } + + pub fn device_name(&self) -> Result { + self.ctx.device_name() + } +} diff --git a/gpu/src/functor/canonical.rs b/gpu/src/functor/canonical.rs new file mode 100644 index 0000000000000000000000000000000000000000..8424eb75a89d6473f42ff743e951524d138d2ee9 --- /dev/null +++ b/gpu/src/functor/canonical.rs @@ -0,0 +1,64 @@ +use super::{GpuError, Result}; + +pub const CANONICAL_FALSE: u8 = 0x00; +pub const CANONICAL_TRUE: u8 = 0x01; + +pub fn is_canonical(value: u8) -> bool { + value == CANONICAL_FALSE || value == CANONICAL_TRUE +} + +pub fn validate_buffer(buf: &[u8]) -> Result<()> { + for (i, &byte) in buf.iter().enumerate() { + if !is_canonical(byte) { + return Err(GpuError::Ocl(ocl::Error::from(format!( + "non-canonical boolean at index {}: expected 0x00 or 0x01, got 0x{:02x}", + i, byte + )))); + } + } + Ok(()) +} + +pub fn bools_to_gpu(values: &[bool]) -> Vec { + values + .iter() + .map(|&v| if v { CANONICAL_TRUE } else { CANONICAL_FALSE }) + .collect() +} + +pub fn gpu_to_bools(values: &[u8]) -> Result> { + validate_buffer(values)?; + Ok(values.iter().map(|&v| v != CANONICAL_FALSE).collect()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn is_canonical_exactly_zero_and_one() { + for v in 0..=u8::MAX { + assert_eq!(is_canonical(v), v <= 1, "value 0x{v:02x}"); + } + } + + #[test] + fn validate_buffer_reports_first_non_canonical() { + assert!(validate_buffer(&[]).is_ok()); + assert!(validate_buffer(&[0, 1, 1, 0]).is_ok()); + for bad in 2..=u8::MAX { + let err = validate_buffer(&[0, 1, bad, 7]).unwrap_err().to_string(); + assert!(err.contains("index 2"), "{err}"); + assert!(err.contains(&format!("0x{bad:02x}")), "{err}"); + } + } + + #[test] + fn bools_round_trip() { + let values = [true, false, false, true, true]; + let bytes = bools_to_gpu(&values); + assert_eq!(bytes, vec![1, 0, 0, 1, 1]); + assert_eq!(gpu_to_bools(&bytes).unwrap(), values); + assert!(gpu_to_bools(&[1, 2]).is_err()); + } +} diff --git a/gpu/src/functor/expr.rs b/gpu/src/functor/expr.rs new file mode 100644 index 0000000000000000000000000000000000000000..bc2850c45f675f04de00aeb59cb94345a4508094 --- /dev/null +++ b/gpu/src/functor/expr.rs @@ -0,0 +1,113 @@ +use super::{Add, GpuContext, GpuError, Negate, Result, UnaryOp}; +use ocl::Buffer; + +/// Expression tree of GPU operations, e.g. `Negate(Add(x, y))`. +/// Every node executes as an OpenCL kernel; nothing is computed on the host. +#[derive(Clone, Debug)] +pub enum Expr { + Input(usize), + Negate(Box), + Add(Box, Box), +} + +enum Val<'a> { + Borrowed(&'a Buffer), + Owned(Buffer), +} + +impl Val<'_> { + fn buf(&self) -> &Buffer { + match self { + Val::Borrowed(b) => b, + Val::Owned(b) => b, + } + } +} + +impl Expr { + pub fn input(i: usize) -> Expr { + Expr::Input(i) + } + pub fn negate(e: Expr) -> Expr { + Expr::Negate(Box::new(e)) + } + // Constructor named after the node it builds; `Expr` is not an arithmetic type. + #[allow(clippy::should_implement_trait)] + pub fn add(a: Expr, b: Expr) -> Expr { + Expr::Add(Box::new(a), Box::new(b)) + } + + /// Evaluate on the device over the first `count` elements of `inputs`. + /// Inputs are never mutated; the result is a fresh buffer of `count` elements. + pub fn eval( + &self, + ctx: &GpuContext, + inputs: &[&Buffer], + count: usize, + ) -> Result> { + if count == 0 { + return Err(GpuError::InvalidCount(0)); + } + match self.eval_val(ctx, inputs, count)? { + Val::Owned(b) => Ok(b), + Val::Borrowed(src) => { + let out = ctx.alloc(count)?; + Identity.launch(ctx, src, &out, count)?; + Ok(out) + } + } + } + + fn eval_val<'a>( + &self, + ctx: &GpuContext, + inputs: &[&'a Buffer], + count: usize, + ) -> Result> { + match self { + Expr::Input(i) => { + inputs + .get(*i) + .map(|b| Val::Borrowed(b)) + .ok_or(GpuError::MissingInput { + index: *i, + supplied: inputs.len(), + }) + } + Expr::Negate(e) => { + let v = e.eval_val(ctx, inputs, count)?; + let out = ctx.alloc(count)?; + Negate.launch(ctx, v.buf(), &out, count)?; + Ok(Val::Owned(out)) + } + Expr::Add(a, b) => { + let va = a.eval_val(ctx, inputs, count)?; + let vb = b.eval_val(ctx, inputs, count)?; + let out = ctx.alloc(count)?; + Add.launch(ctx, va.buf(), vb.buf(), &out, count)?; + Ok(Val::Owned(out)) + } + } + } +} + +/// Device-side copy, used only when an expression is a bare `Input`. +struct Identity; + +impl UnaryOp for Identity { + fn launch( + &self, + ctx: &GpuContext, + input: &Buffer, + output: &Buffer, + count: usize, + ) -> Result<()> { + ctx.work_sizes(count, &[("input", input.len()), ("output", output.len())])?; + input + .cmd() + .queue(ctx.proque().queue()) + .copy(output, Some(0), Some(count)) + .enq()?; + Ok(()) + } +} diff --git a/gpu/src/functor/kernels.rs b/gpu/src/functor/kernels.rs new file mode 100644 index 0000000000000000000000000000000000000000..57133dc28178c8914144d70a0f6103d5af6e8217 --- /dev/null +++ b/gpu/src/functor/kernels.rs @@ -0,0 +1,139 @@ +//! Explicit OpenCL kernel signatures and metadata. +//! +//! Each kernel is formally declared with its name, parameter types, work item constraints, +//! and memory access patterns. + +pub struct KernelSignature { + pub name: &'static str, + pub params: &'static [(&'static str, &'static str)], + pub min_work_items: usize, + pub safety_notes: &'static str, +} + +pub const NEGATE_U8_SIGNATURE: KernelSignature = KernelSignature { + name: "negate_u8", + params: &[ + ("input", "__global const uchar*"), + ("output", "__global uchar*"), + ("element_count", "const ulong"), + ], + min_work_items: 1, + safety_notes: "Kernel guards gid >= count. Input and output must not overlap. \ + Canonical representation: input values must be 0x00 or 0x01. \ + Output always produces 0x00 or 0x01.", +}; + +pub const ADD_U8_SIGNATURE: KernelSignature = KernelSignature { + name: "add_u8", + params: &[ + ("a", "__global const uchar*"), + ("b", "__global const uchar*"), + ("output", "__global uchar*"), + ("count", "const uint"), + ], + min_work_items: 1, + safety_notes: "Kernel guards gid >= count. Input buffers a, b must not overlap with output. \ + Performs wrapping u8 addition: output[i] = (a[i] + b[i]) mod 256. \ + On canonical boolean buffers (0x00, 0x01), produces values 0, 1, or 2.", +}; + +/// Registry of all OpenCL kernels in the functor system. +pub const KERNEL_REGISTRY: &[&KernelSignature] = &[&NEGATE_U8_SIGNATURE, &ADD_U8_SIGNATURE]; + +/// Validate kernel signature match at runtime. +/// Returns Ok if all registered kernels have valid signatures. +pub fn validate_registry() -> Result<(), String> { + for kernel_sig in KERNEL_REGISTRY { + if kernel_sig.name.is_empty() { + return Err("kernel name cannot be empty".to_string()); + } + if kernel_sig.params.is_empty() { + return Err(format!("kernel '{}' must have parameters", kernel_sig.name)); + } + for (param_name, param_type) in kernel_sig.params { + if param_name.is_empty() || param_type.is_empty() { + return Err(format!("kernel '{}': invalid parameter", kernel_sig.name)); + } + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn kernel_registry_is_valid() { + validate_registry().expect("kernel registry validation failed"); + } + + #[test] + fn negate_signature_is_correct() { + assert_eq!(NEGATE_U8_SIGNATURE.name, "negate_u8"); + assert_eq!(NEGATE_U8_SIGNATURE.params.len(), 3); + assert_eq!( + NEGATE_U8_SIGNATURE.params[0], + ("input", "__global const uchar*") + ); + assert_eq!(NEGATE_U8_SIGNATURE.params[1], ("output", "__global uchar*")); + assert_eq!( + NEGATE_U8_SIGNATURE.params[2], + ("element_count", "const ulong") + ); + } + + #[test] + fn add_signature_is_correct() { + assert_eq!(ADD_U8_SIGNATURE.name, "add_u8"); + assert_eq!(ADD_U8_SIGNATURE.params.len(), 4); + assert_eq!(ADD_U8_SIGNATURE.params[0], ("a", "__global const uchar*")); + assert_eq!(ADD_U8_SIGNATURE.params[1], ("b", "__global const uchar*")); + assert_eq!(ADD_U8_SIGNATURE.params[2], ("output", "__global uchar*")); + assert_eq!(ADD_U8_SIGNATURE.params[3], ("count", "const uint")); + } + + /// Every registered signature must describe the OpenCL source that is compiled: + /// the kernel name and each `type name` parameter, in declaration order. + #[test] + fn registry_matches_kernel_sources() { + let sources = [ + (&NEGATE_U8_SIGNATURE, super::super::negate::KERNEL_SRC), + (&ADD_U8_SIGNATURE, super::super::add::KERNEL_SRC), + ]; + assert_eq!( + sources.len(), + KERNEL_REGISTRY.len(), + "a registered kernel has no source check" + ); + for (sig, src) in sources { + // Compare with all whitespace removed: "const uint count" == "constuintcount". + let squashed: String = src.chars().filter(|c| !c.is_whitespace()).collect(); + let header = format!("__kernelvoid{}(", sig.name); + let start = squashed + .find(&header) + .unwrap_or_else(|| panic!("kernel '{}' not found in its source", sig.name)); + let rest = &squashed[start + header.len()..]; + let declared: Vec<&str> = rest[..rest.find(')').expect("unterminated parameter list")] + .split(',') + .collect(); + let expected: Vec = sig + .params + .iter() + .map(|(name, ty)| format!("{ty}{name}").replace(' ', "")) + .collect(); + assert_eq!(declared, expected, "kernel '{}' parameters", sig.name); + } + } + + #[test] + fn all_kernels_have_min_work_items() { + for kernel_sig in KERNEL_REGISTRY { + assert!( + kernel_sig.min_work_items >= 1, + "kernel '{}' must allow at least 1 work item", + kernel_sig.name + ); + } + } +} diff --git a/gpu/src/functor/mod.rs b/gpu/src/functor/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..38440cd91b535ef2744f04ca7364bbb97496d0cb --- /dev/null +++ b/gpu/src/functor/mod.rs @@ -0,0 +1,268 @@ +//! Composable element-wise GPU operations over a Boolean/u8 buffer representation. +//! +//! Representation: one `u8` per element; canonical Booleans are `0` (false) and `1` (true). +//! `Negate` accepts only canonical input (it returns an error on any other byte) and +//! writes canonical output. `Add` is wrapping `u8` addition, so its output is canonical +//! only when no position has both inputs true. + +mod add; +mod agents; +mod canonical; +mod expr; +mod kernels; +mod negate; + +pub use add::Add; +pub use agents::{ExecutionAgent, ValidationAgent, VerificationAgent, Workflow}; +pub use canonical::{ + CANONICAL_FALSE, CANONICAL_TRUE, bools_to_gpu, gpu_to_bools, is_canonical, validate_buffer, +}; +pub use expr::Expr; +pub use kernels::{ADD_U8_SIGNATURE, KERNEL_REGISTRY, NEGATE_U8_SIGNATURE, validate_registry}; +pub use negate::Negate; + +use ocl::{Buffer, ProQue}; +use std::fmt; + +const DEFAULT_LOCAL_SIZE: usize = 64; + +static INIT_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + +#[derive(Debug)] +pub enum GpuError { + Ocl(ocl::Error), + /// Element count cannot be represented as the kernel's `uint` argument. + InvalidCount(usize), + /// A buffer is shorter than the requested element count. + BufferTooShort { + which: &'static str, + len: usize, + count: usize, + }, + /// `Expr::Input(index)` refers to an input that was not supplied. + MissingInput { + index: usize, + supplied: usize, + }, + /// A zero-length device buffer was requested (OpenCL cannot allocate one). + EmptyBuffer, + /// A zero local work-group size was requested. + InvalidLocalSize, +} + +impl fmt::Display for GpuError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + GpuError::Ocl(e) => write!(f, "OpenCL error: {e}"), + GpuError::InvalidCount(n) => write!(f, "invalid element count {n} (must fit in u32)"), + GpuError::BufferTooShort { which, len, count } => { + write!(f, "{which} buffer has {len} elements but count is {count}") + } + GpuError::MissingInput { index, supplied } => { + write!(f, "expression input {index} missing ({supplied} supplied)") + } + GpuError::EmptyBuffer => write!(f, "zero-length device buffers cannot be allocated"), + GpuError::InvalidLocalSize => write!(f, "local work-group size must be non-zero"), + } + } +} + +impl std::error::Error for GpuError {} + +impl From for GpuError { + fn from(e: ocl::Error) -> Self { + GpuError::Ocl(e) + } +} + +pub type Result = std::result::Result; + +/// OpenCL context, queue and compiled program for all functor kernels. +pub struct GpuContext { + proque: ProQue, + local_size: usize, +} + +impl GpuContext { + /// Create a context on the default OpenCL device and compile all functor kernels. + pub fn new() -> Result { + Self::with_source(&Self::kernel_source()) + } + + /// Like [`GpuContext::new`] but with a requested work-group size (clamped to the device limit). + pub fn with_local_size(local_size: usize) -> Result { + let mut ctx = Self::new()?; + ctx.set_local_size(local_size)?; + Ok(ctx) + } + + /// Build a context from arbitrary OpenCL source (compile errors are returned, not swallowed). + pub fn with_source(src: &str) -> Result { + // Validate kernel registry before creating context. + validate_registry().map_err(|e| GpuError::Ocl(ocl::Error::from(e)))?; + + // Observed with pocl: concurrent first-time platform/device discovery from several + // threads makes clGetDeviceIDs fail intermittently, so creation is serialised. + let _guard = INIT_LOCK.lock().unwrap_or_else(|p| p.into_inner()); + let proque = ProQue::builder().src(src.to_string()).build()?; + let max = proque.device().max_wg_size()?; + Ok(GpuContext { + proque, + local_size: DEFAULT_LOCAL_SIZE.min(max), + }) + } + + pub fn set_local_size(&mut self, local_size: usize) -> Result<()> { + if local_size == 0 { + return Err(GpuError::InvalidLocalSize); + } + let max = self.proque.device().max_wg_size()?; + self.local_size = local_size.min(max); + Ok(()) + } + + pub fn local_size(&self) -> usize { + self.local_size + } + + pub fn device_name(&self) -> Result { + Ok(self.proque.device().name()?) + } + + /// All functor kernels in one program. + pub fn kernel_source() -> String { + format!("{}\n{}", negate::KERNEL_SRC, add::KERNEL_SRC) + } + + /// Allocate a zero-initialised device buffer of `len` bytes (len must be > 0). + pub fn alloc(&self, len: usize) -> Result> { + self.alloc_filled(len, 0) + } + + /// Allocate a device buffer filled with `val` (used to detect stale/out-of-bounds writes). + pub fn alloc_filled(&self, len: usize, val: u8) -> Result> { + if len == 0 { + return Err(GpuError::EmptyBuffer); + } + Ok(Buffer::::builder() + .queue(self.proque.queue().clone()) + .len(len) + .fill_val(val) + .build()?) + } + + /// Upload raw bytes to a new device buffer. + pub fn upload_raw(&self, data: &[u8]) -> Result> { + if data.is_empty() { + return Err(GpuError::EmptyBuffer); + } + Ok(Buffer::::builder() + .queue(self.proque.queue().clone()) + .len(data.len()) + .copy_host_slice(data) + .build()?) + } + + pub fn upload_bools(&self, data: &[bool]) -> Result> { + let raw: Vec = data.iter().map(|&b| b as u8).collect(); + self.upload_raw(&raw) + } + + pub fn download_raw(&self, buf: &Buffer) -> Result> { + let mut out = vec![0u8; buf.len()]; + buf.cmd().queue(self.proque.queue()).read(&mut out).enq()?; + Ok(out) + } + + pub fn download_bools(&self, buf: &Buffer, count: usize) -> Result> { + let raw = self.download_raw(buf)?; + let slice = raw.get(..count).ok_or(GpuError::BufferTooShort { + which: "download target", + len: raw.len(), + count, + })?; + validate_buffer(slice)?; + Ok(slice.iter().map(|&b| b != 0).collect()) + } + + pub(crate) fn proque(&self) -> &ProQue { + &self.proque + } + + /// Validate `count` against the buffers and return `(global, local)` work sizes. + /// Global size is rounded up to a multiple of local size, so kernels MUST guard `gid < count`. + pub(crate) fn work_sizes( + &self, + count: usize, + bufs: &[(&'static str, usize)], + ) -> Result<(usize, usize)> { + if count > u32::MAX as usize { + return Err(GpuError::InvalidCount(count)); + } + for &(which, len) in bufs { + if len < count { + return Err(GpuError::BufferTooShort { which, len, count }); + } + } + let local = self.local_size; + Ok((count.div_ceil(local) * local, local)) + } +} + +/// A unary device operation `F(A) -> F(A)` that is composable. +pub trait UnaryOp { + /// Enqueue `output[i] = op(input[i])` for `i < count`. Elements at `i >= count` are untouched. + /// `count == 0` is a no-op. + fn launch( + &self, + ctx: &GpuContext, + input: &Buffer, + output: &Buffer, + count: usize, + ) -> Result<()>; + + /// Sequential composition: `self.then(g)` computes `g(self(x))`. + fn then(self, g: G) -> Compose + where + Self: Sized, + { + Compose { + first: self, + second: g, + } + } + + /// Convenience: run on host bools, executing on the device, returning a fresh Vec. + fn run_bools(&self, ctx: &GpuContext, input: &[bool]) -> Result> { + if input.is_empty() { + return Ok(Vec::new()); + } + let inp = ctx.upload_bools(input)?; + let out = ctx.alloc(input.len())?; + self.launch(ctx, &inp, &out, input.len())?; + ctx.download_bools(&out, input.len()) + } +} + +/// `Compose { first: F, second: G }` computes `G(F(x))` through a device temporary. +pub struct Compose { + first: F, + second: G, +} + +impl UnaryOp for Compose { + fn launch( + &self, + ctx: &GpuContext, + input: &Buffer, + output: &Buffer, + count: usize, + ) -> Result<()> { + if count == 0 { + return Ok(()); + } + let tmp = ctx.alloc(count)?; + self.first.launch(ctx, input, &tmp, count)?; + self.second.launch(ctx, &tmp, output, count) + } +} diff --git a/gpu/src/functor/negate.rs b/gpu/src/functor/negate.rs new file mode 100644 index 0000000000000000000000000000000000000000..d83614434e3f350c82944ba6780c87e3b01270a0 --- /dev/null +++ b/gpu/src/functor/negate.rs @@ -0,0 +1,113 @@ +use super::{GpuContext, GpuError, Result, UnaryOp}; +use ocl::Buffer; +use std::fmt; + +pub(super) const KERNEL_SRC: &str = r#" +__kernel void negate_u8(__global const uchar* input, + __global uchar* output, + const ulong element_count) { + const ulong i = (ulong)get_global_id(0); + if (i >= element_count) { + return; + } + output[i] = input[i] ^ (uchar)1; +} +"#; + +/// Error type for Negate validation and execution failures. +#[derive(Debug, Clone)] +pub enum NegateError { + /// Non-canonical Boolean value found at the given index. + InvalidBoolean { index: usize, value: u8 }, + /// OpenCL error during device operations. + OpenCl(String), +} + +impl fmt::Display for NegateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + NegateError::InvalidBoolean { index, value } => { + write!( + f, + "invalid boolean at index {}: expected 0x00 or 0x01, got 0x{:02x}", + index, value + ) + } + NegateError::OpenCl(msg) => write!(f, "OpenCL error: {}", msg), + } + } +} + +impl std::error::Error for NegateError {} + +impl From for NegateError { + fn from(e: ocl::Error) -> Self { + NegateError::OpenCl(e.to_string()) + } +} + +/// Logical negation `¬x`: `output[i] = !input[i]`, executed on the OpenCL device. +#[derive(Clone, Copy, Debug, Default)] +pub struct Negate; + +impl Negate { + /// Reference semantics of the functor on a single Boolean. + pub fn denote(x: bool) -> bool { + !x + } + + /// Validate that all elements in the input buffer are canonical (0x00 or 0x01). + /// Returns the first invalid (index, value) pair found, or Ok(()) if all canonical. + fn validate_input(input_data: &[u8]) -> std::result::Result<(), NegateError> { + for (i, &byte) in input_data.iter().enumerate() { + if byte != 0x00 && byte != 0x01 { + return Err(NegateError::InvalidBoolean { + index: i, + value: byte, + }); + } + } + Ok(()) + } +} + +impl UnaryOp for Negate { + fn launch( + &self, + ctx: &GpuContext, + input: &Buffer, + output: &Buffer, + count: usize, + ) -> Result<()> { + let (global, local) = + ctx.work_sizes(count, &[("input", input.len()), ("output", output.len())])?; + if count == 0 { + return Ok(()); + } + + // Download and validate input data before kernel submission + let input_data = ctx.download_raw(input)?; + let input_slice = input_data.get(..count).ok_or(GpuError::BufferTooShort { + which: "input", + len: input_data.len(), + count, + })?; + + Self::validate_input(input_slice) + .map_err(|e| GpuError::Ocl(ocl::Error::from(e.to_string())))?; + + let kernel = ctx + .proque() + .kernel_builder("negate_u8") + .global_work_size(global) + .local_work_size(local) + .arg(input) + .arg(output) + .arg(count as u64) + .build()?; + // SAFETY: arguments match the kernel signature; the kernel guards gid < count and + // count <= both buffer lengths was validated above. + unsafe { kernel.enq()? }; + Ok(()) + } +} diff --git a/gpu/src/lib.rs b/gpu/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..4a2a41b20fd8bad4674d888e584079b5cc3e5ac3 --- /dev/null +++ b/gpu/src/lib.rs @@ -0,0 +1 @@ +pub mod functor; diff --git a/gpu/src/main.rs b/gpu/src/main.rs new file mode 100644 index 0000000000000000000000000000000000000000..a702ee04a6433b33816ae916a1c5aad5b332c65f --- /dev/null +++ b/gpu/src/main.rs @@ -0,0 +1,140 @@ +use ocl::{ProQue, Result}; + +trait Kernel: Sized { + fn src() -> &'static str; + fn name() -> &'static str; + fn local_size() -> usize; + fn run(&self, proque: &ProQue) -> Result>; +} + +struct Add; + +impl Kernel for Add { + fn src() -> &'static str { + r#" + __kernel void add(__global float* c) { + c[get_global_id(0)] = get_local_id(0); + } + "# + } + fn name() -> &'static str { + "add" + } + fn local_size() -> usize { + 4 + } + + fn run(&self, proque: &ProQue) -> Result> { + let buffer = proque.create_buffer::()?; + let kernel = proque.kernel_builder(Self::name()).arg(&buffer).build()?; + unsafe { + kernel.cmd().local_work_size(Self::local_size()).enq()?; + } + let mut result = vec![0.0f32; 128]; + buffer.cmd().read(&mut result).enq()?; + Ok(result) + } +} + +fn format_output(data: &[f32]) -> String { + data.iter() + .enumerate() + .fold(String::new(), |acc, (i, &val)| { + if i > 0 && i % 16 == 0 { + format!("{}\n{:>3} ", acc, val as i32) + } else { + format!("{}{:>3} ", acc, val as i32) + } + }) +} + +fn execute(kernel: K, proque: &ProQue) -> Result> { + kernel.run(proque) +} + +fn main() -> Result<()> { + let proque = ProQue::builder().src(Add::src()).dims(128).build()?; + + let result = execute(Add, &proque)?; + println!("{}", format_output(&result)); + + negate_demo(); + agent_workflow_demo(); + + Ok(()) +} + +fn negate_demo() { + use gpu::functor::{GpuContext, Negate, UnaryOp}; + + let ctx = match GpuContext::new() { + Ok(c) => c, + Err(e) => { + eprintln!("negate demo: {e}"); + std::process::exit(1); + } + }; + let input = [true, false, true, true, false]; + let once = Negate.run_bools(&ctx, &input).expect("negate failed"); + let twice = Negate + .then(Negate) + .run_bools(&ctx, &input) + .expect("negate∘negate failed"); + println!("\ndevice : {}", ctx.device_name().unwrap_or_default()); + println!("input : {input:?}"); + println!("¬input : {once:?}"); + println!("¬¬input: {twice:?}"); + assert_eq!(twice, input); +} + +fn agent_workflow_demo() { + use gpu::functor::{Negate, ValidationAgent, VerificationAgent, Workflow}; + + println!("\n=== 3-Agent Workflow Demo ==="); + + let workflow = match Workflow::new() { + Ok(w) => w, + Err(e) => { + eprintln!("workflow error: {e}"); + return; + } + }; + + println!("device: {}", workflow.device_name().unwrap_or_default()); + + let input = [true, false, true, true, false]; + println!("\n[ValidationAgent] validating input: {input:?}"); + + if let Err(e) = ValidationAgent::validate_input(input.len()) { + eprintln!("validation failed: {e}"); + return; + } + println!("[ValidationAgent] ✓ input valid (count={})", input.len()); + + println!("[ExecutionAgent] running Negate kernel on device..."); + match workflow.run_unary_bools(&Negate, &input) { + Ok(negate_result) => { + println!("[ExecutionAgent] ✓ Negate computed: {negate_result:?}"); + let expected = [false, true, false, false, true]; + if let Ok(()) = VerificationAgent::verify_bools_equal(&negate_result, &expected) { + println!("[VerificationAgent] ✓ Output verified against expected {expected:?}"); + } else { + println!("[VerificationAgent] ✗ Output mismatch"); + } + } + Err(e) => eprintln!("[ExecutionAgent] ✗ failed: {e}"), + } + + println!("\n[ExecutionAgent] running Negate∘Negate composition..."); + match workflow.run_composed_bools(&Negate, &Negate, &input) { + Ok(double_negated) => { + println!("[ExecutionAgent] ✓ Negate∘Negate computed: {double_negated:?}"); + if let Ok(()) = VerificationAgent::verify_bools_equal(&double_negated, &input) { + println!("[VerificationAgent] ✓ Involution verified: input == ¬¬input"); + } else { + println!("[VerificationAgent] ✗ Involution failed"); + } + } + Err(e) => eprintln!("[ExecutionAgent] ✗ composition failed: {e}"), + } +} diff --git a/gpu/tests/negate_gpu.rs b/gpu/tests/negate_gpu.rs new file mode 100644 index 0000000000000000000000000000000000000000..e5ce09f26be7cf3b42be72586518bc9906528b35 --- /dev/null +++ b/gpu/tests/negate_gpu.rs @@ -0,0 +1,701 @@ +//! Executable OpenCL tests for the Negate functor. Every negation runs as an OpenCL kernel. +//! Tests FAIL (never silently skip) when no OpenCL device is available. + +use gpu::functor::{Add, Expr, GpuContext, GpuError, Negate, UnaryOp}; + +const SENTINEL: u8 = 0xAA; + +fn ctx() -> GpuContext { + GpuContext::new() + .expect("OpenCL context creation / kernel compilation failed (no usable OpenCL device?)") +} + +fn ctx_local(n: usize) -> GpuContext { + GpuContext::with_local_size(n).expect("OpenCL context creation failed") +} + +/// Deterministic xorshift so no extra dependencies are needed. +struct Rng(u64); +impl Rng { + fn next(&mut self) -> u64 { + self.0 ^= self.0 << 13; + self.0 ^= self.0 >> 7; + self.0 ^= self.0 << 17; + self.0 + } + fn bools(&mut self, n: usize) -> Vec { + (0..n).map(|_| self.next() & 1 == 1).collect() + } + fn bytes(&mut self, n: usize) -> Vec { + (0..n).map(|_| (self.next() >> 8) as u8).collect() + } +} + +fn expected_not(input: &[bool]) -> Vec { + input.iter().map(|&b| !b).collect() +} + +#[test] +fn device_is_reported() { + let c = ctx(); + let name = c.device_name().expect("device name"); + println!("OpenCL device: {name} (local size {})", c.local_size()); + assert!(!name.is_empty()); +} + +#[test] +fn scalar_laws_on_device() { + let c = ctx(); + assert_eq!( + Negate.run_bools(&c, &[true]).unwrap(), + vec![false], + "¬true = false" + ); + assert_eq!( + Negate.run_bools(&c, &[false]).unwrap(), + vec![true], + "¬false = true" + ); + assert!(!Negate::denote(true)); + assert!(Negate::denote(false)); + for x in [true, false] { + let twice = Negate.then(Negate).run_bools(&c, &[x]).unwrap(); + assert_eq!(twice, vec![x], "¬¬x = x for {x}"); + } +} + +#[test] +fn small_buffers() { + let c = ctx(); + let cases: [&[bool]; 5] = [&[], &[true], &[false], &[true, false], &[false, true]]; + for case in cases { + let got = Negate.run_bools(&c, case).unwrap(); + assert_eq!(got, expected_not(case), "input {case:?}"); + } +} + +#[test] +fn non_power_of_two_sizes_every_element() { + let c = ctx(); + let mut rng = Rng(0x9E3779B97F4A7C15); + for &n in &[1usize, 3, 7, 17, 31, 63, 65, 127, 257, 1000, 4096] { + for input in [rng.bools(n), vec![true; n], vec![false; n]] { + let got = Negate.run_bools(&c, &input).unwrap(); + assert_eq!(got.len(), n); + assert_eq!(got, expected_not(&input), "n = {n}"); + } + } +} + +#[test] +fn large_buffers() { + let c = ctx(); + let mut rng = Rng(12345); + for &n in &[1usize << 20, 1_000_003] { + let input = rng.bools(n); + let got = Negate.run_bools(&c, &input).unwrap(); + assert_eq!(got, expected_not(&input), "n = {n}"); + } +} + +#[test] +fn canonical_bytes_negation() { + let c = ctx(); + // Test with canonical bytes only (0x00 and 0x01) + let raw: Vec = vec![0, 1, 0, 1, 1, 0, 1, 0, 0]; + let inp = c.upload_raw(&raw).unwrap(); + let out = c.alloc_filled(raw.len(), SENTINEL).unwrap(); + Negate.launch(&c, &inp, &out, raw.len()).unwrap(); + let got = c.download_raw(&out).unwrap(); + let want: Vec = raw.iter().map(|&b| b ^ 1).collect(); + assert_eq!(got, want); +} + +#[test] +fn involution_two_gpu_launches_with_temporary() { + let c = ctx(); + let mut rng = Rng(777); + for &n in &[1usize, 3, 7, 17, 31, 63, 65, 127, 257, 1000, 4096, 100_003] { + let input = rng.bools(n); + let inp = c.upload_bools(&input).unwrap(); + let tmp = c.alloc_filled(n, SENTINEL).unwrap(); + let out = c.alloc_filled(n, SENTINEL).unwrap(); + Negate.launch(&c, &inp, &tmp, n).unwrap(); + Negate.launch(&c, &tmp, &out, n).unwrap(); + + let tmp_raw = c.download_raw(&tmp).unwrap(); + let want_tmp: Vec = input.iter().map(|&b| (!b) as u8).collect(); + assert_eq!(tmp_raw, want_tmp, "temporary after first negation, n = {n}"); + + let out_raw = c.download_raw(&out).unwrap(); + let want: Vec = input.iter().map(|&b| b as u8).collect(); + assert_eq!( + out_raw, want, + "output == input after two negations, n = {n}" + ); + } +} + +#[test] +fn involution_via_compose_and_expr() { + let c = ctx(); + let mut rng = Rng(4242); + for &n in &[1usize, 17, 65, 1000, 4096] { + let input = rng.bools(n); + assert_eq!( + Negate.then(Negate).run_bools(&c, &input).unwrap(), + input, + "Compose n={n}" + ); + assert_eq!( + Negate + .then(Negate) + .then(Negate) + .run_bools(&c, &input) + .unwrap(), + expected_not(&input), + "¬¬¬x = ¬x, n={n}" + ); + + let inp = c.upload_bools(&input).unwrap(); + let e = Expr::negate(Expr::negate(Expr::input(0))); + let out = e.eval(&c, &[&inp], n).unwrap(); + assert_eq!(c.download_bools(&out, n).unwrap(), input, "Expr n={n}"); + } +} + +#[test] +fn negate_does_not_mutate_input() { + let c = ctx(); + let mut rng = Rng(99); + for &n in &[1usize, 63, 4096] { + let raw: Vec = rng.bools(n).iter().map(|&b| b as u8).collect(); + let inp = c.upload_raw(&raw).unwrap(); + let out = c.alloc(n).unwrap(); + + Negate.launch(&c, &inp, &out, n).unwrap(); + assert_eq!( + c.download_raw(&inp).unwrap(), + raw, + "input unchanged after Negate, n={n}" + ); + + Negate.then(Negate).launch(&c, &inp, &out, n).unwrap(); + assert_eq!( + c.download_raw(&inp).unwrap(), + raw, + "input unchanged after Compose, n={n}" + ); + + let e = Expr::negate(Expr::input(0)); + e.eval(&c, &[&inp], n).unwrap(); + assert_eq!( + c.download_raw(&inp).unwrap(), + raw, + "input unchanged after Expr, n={n}" + ); + } +} + +#[test] +fn negate_of_add_matches_host_oracle() { + // Add is wrapping u8 addition, so true + true = 2, which Negate rejects as + // non-canonical. Where x and y are never both true, Negate(Add(x, y)) is NOR. + let c = ctx(); + let mut rng = Rng(2024); + let expr = Expr::negate(Expr::add(Expr::input(0), Expr::input(1))); + for &n in &[1usize, 7, 65, 1000, 4096] { + let x = rng.bools(n); + let y: Vec = rng + .bools(n) + .iter() + .zip(&x) + .map(|(&b, &a)| b && !a) + .collect(); + let (bx, by) = (c.upload_bools(&x).unwrap(), c.upload_bools(&y).unwrap()); + let out = expr.eval(&c, &[&bx, &by], n).unwrap(); + let want: Vec = x.iter().zip(&y).map(|(&a, &b)| !(a || b)).collect(); + assert_eq!(c.download_bools(&out, n).unwrap(), want, "n = {n}"); + } + // A position where both inputs are true yields 2 after Add; Negate must refuse it. + let both = c.upload_bools(&[false, true, false]).unwrap(); + assert!(expr.eval(&c, &[&both, &both], 3).is_err()); +} + +#[test] +fn negate_rejects_non_canonical_bytes() { + let c = ctx(); + let mut rng = Rng(77); + for &n in &[1usize, 9, 300] { + let mut raw = rng.bytes(n); + // Guarantee at least one non-canonical byte. + raw[n / 2] |= 0x02; + let inp = c.upload_raw(&raw).unwrap(); + let out = c.alloc_filled(n, SENTINEL).unwrap(); + assert!(Negate.launch(&c, &inp, &out, n).is_err(), "n = {n}"); + assert_eq!( + c.download_raw(&out).unwrap(), + vec![SENTINEL; n], + "output written on rejection" + ); + } +} + +#[test] +fn expr_input_passthrough_and_missing_input() { + let c = ctx(); + let input = vec![true, false, true]; + let inp = c.upload_bools(&input).unwrap(); + let out = Expr::input(0).eval(&c, &[&inp], 3).unwrap(); + assert_eq!(c.download_bools(&out, 3).unwrap(), input); + match Expr::input(1).eval(&c, &[&inp], 3) { + Err(GpuError::MissingInput { + index: 1, + supplied: 1, + }) => {} + other => panic!("expected MissingInput, got {:?}", other.map(|_| ())), + } +} + +#[test] +fn add_functor_on_device() { + let c = ctx(); + let (a, b) = (vec![1u8, 2, 250, 0, 255], vec![1u8, 3, 10, 0, 1]); + let (ba, bb) = (c.upload_raw(&a).unwrap(), c.upload_raw(&b).unwrap()); + let out = c.alloc(5).unwrap(); + Add.launch(&c, &ba, &bb, &out, 5).unwrap(); + let want: Vec = a.iter().zip(&b).map(|(x, y)| x.wrapping_add(*y)).collect(); + assert_eq!(c.download_raw(&out).unwrap(), want); +} + +#[test] +fn boundary_guard_no_write_past_count() { + // Output/input buffers are LONGER than `count`; global size is rounded up past `count`. + // Any thread with gid >= count that wrote would clobber the sentinel tail. + for &local in &[1usize, 3, 8, 64, 256] { + let c = ctx_local(local); + let l = c.local_size(); + let mut counts = vec![1usize, 2, 5, 17, 100]; + for d in [l.saturating_sub(1), l, l + 1, 2 * l - 1, 2 * l + 1] { + if d > 0 { + counts.push(d); + } + } + for count in counts { + let pad = 3 * l + 7; + let len = count + pad; + let mut raw = vec![0u8; len]; + for (i, r) in raw.iter_mut().enumerate() { + *r = (i % 2) as u8; // tail holds data too: a stray read/write would be visible + } + let inp = c.upload_raw(&raw).unwrap(); + let out = c.alloc_filled(len, SENTINEL).unwrap(); + Negate.launch(&c, &inp, &out, count).unwrap(); + + let got = c.download_raw(&out).unwrap(); + for i in 0..count { + assert_eq!( + got[i], + (raw[i] == 0) as u8, + "local={l} count={count} i={i} (stale or wrong)" + ); + } + for (i, &byte) in got.iter().enumerate().take(len).skip(count) { + assert_eq!( + byte, SENTINEL, + "OOB write at i={i}, local={l} count={count}" + ); + } + assert_eq!( + c.download_raw(&inp).unwrap(), + raw, + "input mutated, local={l} count={count}" + ); + } + } +} + +#[test] +fn boundary_guard_for_add_too() { + let c = ctx_local(8); + let count = 13usize; + let len = count + 40; + let a = c.upload_raw(&vec![1u8; len]).unwrap(); + let b = c.upload_raw(&vec![2u8; len]).unwrap(); + let out = c.alloc_filled(len, SENTINEL).unwrap(); + Add.launch(&c, &a, &b, &out, count).unwrap(); + let got = c.download_raw(&out).unwrap(); + assert!(got[..count].iter().all(|&v| v == 3)); + assert!( + got[count..].iter().all(|&v| v == SENTINEL), + "Add wrote past count" + ); +} + +#[test] +fn stale_output_values_are_all_overwritten() { + let c = ctx(); + for &n in &[1usize, 3, 7, 17, 31, 63, 65, 127, 257, 1000, 4096] { + let inp = c.upload_bools(&vec![true; n]).unwrap(); + let out = c.alloc_filled(n, SENTINEL).unwrap(); + Negate.launch(&c, &inp, &out, n).unwrap(); + assert!( + c.download_raw(&out).unwrap().iter().all(|&v| v == 0), + "stale value survived, n={n}" + ); + + // Reuse the same output buffer for the opposite input: no stale 0s may remain. + let inp2 = c.upload_bools(&vec![false; n]).unwrap(); + Negate.launch(&c, &inp2, &out, n).unwrap(); + assert!( + c.download_raw(&out).unwrap().iter().all(|&v| v == 1), + "stale value survived on reuse, n={n}" + ); + } +} + +#[test] +fn zero_count_is_noop_and_leaves_output_untouched() { + let c = ctx(); + let inp = c.upload_raw(&[1, 0, 1, 0]).unwrap(); + let out = c.alloc_filled(4, SENTINEL).unwrap(); + Negate.launch(&c, &inp, &out, 0).unwrap(); + assert_eq!(c.download_raw(&out).unwrap(), vec![SENTINEL; 4]); + assert_eq!( + Negate.then(Negate).run_bools(&c, &[]).unwrap(), + Vec::::new() + ); +} + +// ---------------- error handling ---------------- + +#[test] +fn error_kernel_compilation_failure_is_reported() { + match GpuContext::with_source("__kernel void broken( { this is not OpenCL C") { + Err(GpuError::Ocl(e)) => println!("compile error surfaced as expected: {e}"), + Err(other) => panic!("wrong error variant: {other}"), + Ok(_) => panic!("invalid OpenCL source compiled"), + } +} + +#[test] +fn error_buffer_too_short() { + let c = ctx(); + let inp = c.upload_raw(&[1, 0, 1]).unwrap(); + let out = c.alloc_filled(8, SENTINEL).unwrap(); + match Negate.launch(&c, &inp, &out, 5) { + Err(GpuError::BufferTooShort { + which: "input", + len: 3, + count: 5, + }) => {} + other => panic!("expected BufferTooShort(input), got {other:?}"), + } + let short_out = c.alloc_filled(2, SENTINEL).unwrap(); + let inp5 = c.upload_raw(&[1; 5]).unwrap(); + match Negate.launch(&c, &inp5, &short_out, 5) { + Err(GpuError::BufferTooShort { + which: "output", + len: 2, + count: 5, + }) => {} + other => panic!("expected BufferTooShort(output), got {other:?}"), + } + assert_eq!( + c.download_raw(&out).unwrap(), + vec![SENTINEL; 8], + "failed launch must not write" + ); + assert_eq!(c.download_raw(&short_out).unwrap(), vec![SENTINEL; 2]); +} + +#[test] +fn error_invalid_element_count() { + let c = ctx(); + let inp = c.upload_raw(&[1]).unwrap(); + let out = c.alloc(1).unwrap(); + let too_big = u32::MAX as usize + 1; + match Negate.launch(&c, &inp, &out, too_big) { + Err(GpuError::InvalidCount(n)) if n == too_big => {} + other => panic!("expected InvalidCount, got {other:?}"), + } + match Expr::input(0).eval(&c, &[&inp], 0) { + Err(GpuError::InvalidCount(0)) => {} + other => panic!( + "expected InvalidCount(0) from Expr::eval, got {:?}", + other.map(|_| ()) + ), + } +} + +#[test] +fn error_invalid_local_size() { + let mut c = ctx(); + assert!(matches!( + c.set_local_size(0), + Err(GpuError::InvalidLocalSize) + )); + assert!(matches!( + GpuContext::with_local_size(0), + Err(GpuError::InvalidLocalSize) + )); +} + +#[test] +fn error_buffer_allocation_failure_is_reported() { + let c = ctx(); + // Zero-length buffers cannot exist in OpenCL (the ocl crate would panic): typed error instead. + assert!(matches!(c.alloc(0), Err(GpuError::EmptyBuffer))); + assert!(matches!(c.upload_raw(&[]), Err(GpuError::EmptyBuffer))); + // Far beyond any device's max allocation size. + assert!(matches!(c.alloc(usize::MAX / 4), Err(GpuError::Ocl(_)))); +} + +#[test] +fn canonical_representation_false_is_0x00() { + use gpu::functor::CANONICAL_FALSE; + assert_eq!(CANONICAL_FALSE, 0x00); +} + +#[test] +fn canonical_representation_true_is_0x01() { + use gpu::functor::CANONICAL_TRUE; + assert_eq!(CANONICAL_TRUE, 0x01); +} + +#[test] +fn canonical_validation_accepts_0x00_and_0x01() { + use gpu::functor::is_canonical; + assert!(is_canonical(0x00)); + assert!(is_canonical(0x01)); +} + +#[test] +fn canonical_validation_rejects_non_canonical() { + use gpu::functor::is_canonical; + for byte in 2u8..=255 { + assert!( + !is_canonical(byte), + "byte 0x{:02x} should be non-canonical", + byte + ); + } +} + +#[test] +fn canonical_buffer_validation_accepts_valid() { + use gpu::functor::validate_buffer; + assert!(validate_buffer(&[]).is_ok()); + assert!(validate_buffer(&[0x00]).is_ok()); + assert!(validate_buffer(&[0x01]).is_ok()); + assert!(validate_buffer(&[0x00, 0x01]).is_ok()); + assert!(validate_buffer(&[0x01, 0x00]).is_ok()); + assert!(validate_buffer(&[0x00, 0x01, 0x00, 0x01]).is_ok()); +} + +#[test] +fn canonical_buffer_validation_rejects_non_canonical() { + use gpu::functor::validate_buffer; + assert!(validate_buffer(&[0x02]).is_err(), "0x02 should be rejected"); + assert!(validate_buffer(&[0xFF]).is_err(), "0xFF should be rejected"); + assert!( + validate_buffer(&[0x00, 0x02]).is_err(), + "0x02 in position 1 should be rejected" + ); + assert!( + validate_buffer(&[0x01, 0x01, 0x80]).is_err(), + "0x80 should be rejected" + ); +} + +#[test] +fn canonical_bools_to_gpu_conversion() { + use gpu::functor::bools_to_gpu; + assert_eq!(bools_to_gpu(&[]), vec![]); + assert_eq!(bools_to_gpu(&[false]), vec![0x00]); + assert_eq!(bools_to_gpu(&[true]), vec![0x01]); + assert_eq!( + bools_to_gpu(&[true, false, true, true, false]), + vec![0x01, 0x00, 0x01, 0x01, 0x00] + ); +} + +#[test] +fn canonical_gpu_to_bools_conversion() { + use gpu::functor::gpu_to_bools; + assert_eq!(gpu_to_bools(&[]).unwrap(), vec![]); + assert_eq!(gpu_to_bools(&[0x00]).unwrap(), vec![false]); + assert_eq!(gpu_to_bools(&[0x01]).unwrap(), vec![true]); + assert_eq!( + gpu_to_bools(&[0x01, 0x00, 0x01, 0x01, 0x00]).unwrap(), + vec![true, false, true, true, false] + ); +} + +#[test] +fn canonical_gpu_to_bools_rejects_non_canonical() { + use gpu::functor::gpu_to_bools; + assert!(gpu_to_bools(&[0x02]).is_err()); + assert!(gpu_to_bools(&[0xFF]).is_err()); + assert!(gpu_to_bools(&[0x00, 0x02]).is_err()); +} + +#[test] +fn negate_output_is_canonical() { + let c = ctx(); + let inputs = vec![ + vec![0x00], + vec![0x01], + vec![0x00, 0x01], + vec![0x01, 0x00], + vec![0x00, 0x01, 0x00, 0x01], + ]; + for input in inputs { + let count = input.len(); + let inp_buf = c.upload_raw(&input).unwrap(); + let out_buf = c.alloc(count).unwrap(); + Negate.launch(&c, &inp_buf, &out_buf, count).unwrap(); + let result = c.download_raw(&out_buf).unwrap(); + for (i, &byte) in result.iter().enumerate() { + assert!( + byte == 0x00 || byte == 0x01, + "output[{}] = 0x{:02x} is non-canonical", + i, + byte + ); + } + } +} + +#[test] +fn double_negation_byte_equality() { + let c = ctx(); + let inputs = vec![ + vec![0x00], + vec![0x01], + vec![0x00, 0x01], + vec![0x01, 0x00], + vec![0x00, 0x01, 0x00, 0x01, 0x00], + (0..1000) + .map(|i| if i % 2 == 0 { 0x00 } else { 0x01 }) + .collect::>(), + ]; + for input in inputs { + let count = input.len(); + let inp_buf = c.upload_raw(&input).unwrap(); + let tmp_buf = c.alloc(count).unwrap(); + let out_buf = c.alloc(count).unwrap(); + + Negate.launch(&c, &inp_buf, &tmp_buf, count).unwrap(); + Negate.launch(&c, &tmp_buf, &out_buf, count).unwrap(); + + let result = c.download_raw(&out_buf).unwrap(); + assert_eq!( + result, input, + "double negation must be byte-for-byte identity" + ); + } +} + +#[test] +fn negate_rejects_non_canonical_input() { + let c = ctx(); + let non_canonical = vec![0x00, 0x02, 0x01]; + let inp_buf = c.upload_raw(&non_canonical).unwrap(); + let out_buf = c.alloc(3).unwrap(); + + // Negate should now reject non-canonical input + match Negate.launch(&c, &inp_buf, &out_buf, 3) { + Err(GpuError::Ocl(_)) => {} // Validation error wrapped in GpuError::Ocl + other => panic!("expected validation error, got {other:?}"), + } + + // Output should remain untouched after failed validation + let sentinel = c.download_raw(&out_buf).unwrap(); + assert!( + sentinel.iter().all(|&b| b == 0), + "output should be untouched after validation failure" + ); +} + +#[test] +fn negate_validates_input_single_invalid_byte() { + let c = ctx(); + // Single invalid byte at different positions + let cases = vec![ + (vec![0x02], 0, 0x02), + (vec![0xFF], 0, 0xFF), + (vec![0x00, 0x02], 1, 0x02), + (vec![0x01, 0xFF], 1, 0xFF), + (vec![0x00, 0x01, 0x80], 2, 0x80), + ]; + + for (input, expected_index, expected_value) in cases { + let count = input.len(); + let inp_buf = c.upload_raw(&input).unwrap(); + let out_buf = c.alloc(count).unwrap(); + + match Negate.launch(&c, &inp_buf, &out_buf, count) { + Err(GpuError::Ocl(e)) => { + let err_msg = e.to_string(); + assert!( + err_msg.contains(&format!("index {}", expected_index)) + && err_msg.contains(&format!("0x{:02x}", expected_value)), + "error message should contain index {} and value 0x{:02x}, got: {}", + expected_index, + expected_value, + err_msg + ); + } + other => panic!("expected validation error for {:?}, got {other:?}", input), + } + + // Verify kernel was never submitted (output untouched) + let sentinel = c.download_raw(&out_buf).unwrap(); + assert!( + sentinel.iter().all(|&b| b == 0), + "output should be untouched after validation failure" + ); + } +} + +#[test] +fn negate_validates_input_accepts_canonical() { + let c = ctx(); + let cases = vec![ + vec![], + vec![0x00], + vec![0x01], + vec![0x00, 0x01], + vec![0x01, 0x00], + vec![0x00, 0x01, 0x00, 0x01], + vec![0x01; 100], + vec![0x00; 100], + ]; + + for input in cases { + let count = input.len(); + if count == 0 { + continue; // Skip empty input for this validation test + } + let inp_buf = c.upload_raw(&input).unwrap(); + let out_buf = c.alloc(count).unwrap(); + + // Should not error on canonical input + match Negate.launch(&c, &inp_buf, &out_buf, count) { + Ok(()) => { + let result = c.download_raw(&out_buf).unwrap(); + let expected: Vec = input.iter().map(|&b| b ^ 1).collect(); + assert_eq!( + result, expected, + "output should be correct for input {:?}", + input + ); + } + Err(e) => panic!( + "should accept canonical input {:?}, got error: {}", + input, e + ), + } + } +} diff --git a/isa/isa.md b/isa/isa.md new file mode 100644 index 0000000000000000000000000000000000000000..f794b6eb084421aaf551322877dd5ba7ea6c7571 --- /dev/null +++ b/isa/isa.md @@ -0,0 +1,16 @@ +# Bit Accelerator ISA + +64-bit base address, 64-bit bit offset. + +| Opcode | Operation | +|---|---| +| 000 | BIT_GET | +| 001 | BIT_TEST | +| 010 | BIT_SET | +| 011 | BIT_CLEAR | +| 100 | BIT_TOGGLE | +| 101-111 | undefined: completes with `error = 1`, no write | + +Inputs: `rs_base`, `rs_offset` or an immediate offset. Output: one-bit architectural result plus condition/result-valid status. + +Bit numbering is least-significant-bit first within each 64-bit word. Memory addresses are byte addresses. The effective bit address is `(base << 3) + offset` modulo 2^64. The containing word address is `(effective_bit_address >> 6) << 3` and bit index is `effective_bit_address mod 64`. diff --git a/pcie/.gitignore b/pcie/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..c3d308ec2a026a3ad0b9eb2ef2d81a71c237a797 --- /dev/null +++ b/pcie/.gitignore @@ -0,0 +1,4 @@ +build/ +host/build_cosim/ +host/*.o +host/bitacc_pcie diff --git a/pcie/Makefile b/pcie/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..8e6f3047e12a007be0a4e56f4de7dfc7727f5043 --- /dev/null +++ b/pcie/Makefile @@ -0,0 +1,153 @@ +# PCIe accelerator: parallel engine, PCIe-facing core, LiteX SoC for the +# LiteFury, and the host program. +# +# make lint verilator -Wall on the engine and the PCIe core +# make sim engine vs the single-op reference core: several +# geometries and seeds (Icarus); core with 128-bit streams. +# The runs are independent targets: use make -j. +# make sim-verilator the engine testbench under Verilator +# make soc generate the LiteX SoC sources and Linux driver (no Vivado) +# make cosim host program (host/bitacc_pcie.c) against the Verilated core +# through a liblitepcie stand-in (needs `make soc` first) +# make perf engine throughput in clock cycles at the LiteFury geometry +# (8 lanes x 2048 words): host bench against the Verilated core +# make timing open-source place and route for the XC7A100T (nextpnr-xilinx; +# set NEXTPNR and CHIPDB, see timing/run.sh): Fmax estimate +# make bitstream full build with Vivado (not available in CI) +# +# The engine testbench fails unless results and final memory equal the +# reference, and unless out-of-order completion, a full reorder buffer, both +# directions of the range barrier and every operation class (single-bit and +# range; in range, out of range, wrapping, undefined) occurred. +# +# A testbench passes only if vvp exits 0 and prints PASS. (Piping vvp into +# grep would report the grep's status, so a failing run would pass.) + +IVERILOG ?= iverilog +VVP ?= vvp +VERILATOR ?= verilator +PYTHON ?= python3 +BUILD ?= build +OPS ?= 4000 + +REF := ../rtl/bit_accelerator.sv +ENGINE := rtl/bitacc_engine.sv +CORE := rtl/bitacc_pcie_core.sv +SOC_DIR := $(abspath $(BUILD))/litefury +DRIVER := $(SOC_DIR)/driver +GEOMETRIES := 2x4 4x16 8x16 8x512 +SEEDS := 1 7 +TB_INC := tb/gold_ops.svh tb/range_stim.svh +# plus 16 lanes, a build without the MATCH unit (opcode 15 must be an error), and +# matchers with fewer units than lanes (run-ml-x-) +ENGINE_RUNS := $(foreach g,$(GEOMETRIES),$(foreach s,$(SEEDS),run-$(g)-$(s))) run-16x8-1 + +# $(call vvp_check,command,log): run, show the summary, fail unless exit 0 and PASS. +define vvp_check +$(1) > $(2) 2>&1; rc=$$?; grep -E '^(stimulus|range coverage|match coverage|checks|PASS|FAIL)' $(2); \ + test $$rc -eq 0 && grep -q '^PASS' $(2) +endef + +.PHONY: all lint sim sim-engine sim-core sim-verilator soc cosim perf timing bitstream clean $(ENGINE_RUNS) run-nomatch $(ML_RUNS) +.DEFAULT_GOAL := all +all: lint sim sim-verilator + +lint: + $(VERILATOR) --lint-only -Wall $(ENGINE) + $(VERILATOR) --lint-only -Wall $(ENGINE) $(CORE) --top-module bitacc_pcie_core + +sim: sim-engine sim-core + +ML_RUNS := run-ml-4x16-1 run-ml-8x16-2 +sim-engine: $(ENGINE_RUNS) run-nomatch $(ML_RUNS) + +$(ML_RUNS): run-ml-%: $(BUILD)/tb_ml_%.vvp + @echo "engine $* (lanes x words - MATCH units)" + @$(call vvp_check,$(VVP) -n $< +seed=3 +ops=$(OPS),$(BUILD)/run-ml-$*.log) + +$(BUILD)/tb_ml_%.vvp: $(REF) $(ENGINE) tb/tb_engine.sv $(TB_INC) + @mkdir -p $(BUILD) + c=$*; g=$${c%-*}; m=$${c#*-}; \ + $(IVERILOG) -g2012 -Itb -DLANES=$${g%x*} -DWPL=$${g#*x} -DMLANES=$$m -o $@ $(REF) $(ENGINE) tb/tb_engine.sv + +# run-x- +.SECONDEXPANSION: +$(ENGINE_RUNS): run-%: $(BUILD)/tb_$$(word 1,$$(subst -, ,$$*)).vvp + @echo "engine $*" + @$(call vvp_check,$(VVP) -n $< +seed=$(word 2,$(subst -, ,$*)) +ops=$(OPS),$(BUILD)/run-$*.log) + +run-nomatch: $(BUILD)/tb_nomatch.vvp + @echo "engine 4x16 without MATCH" + @$(call vvp_check,$(VVP) -n $< +seed=1 +ops=$(OPS),$(BUILD)/run-nomatch.log) + +$(BUILD)/tb_nomatch.vvp: $(REF) $(ENGINE) tb/tb_engine.sv $(TB_INC) + @mkdir -p $(BUILD) + $(IVERILOG) -g2012 -Itb -DLANES=4 -DWPL=16 -DMATCH=0 -o $@ $(REF) $(ENGINE) tb/tb_engine.sv + +$(BUILD)/tb_%.vvp: $(REF) $(ENGINE) tb/tb_engine.sv $(TB_INC) + @mkdir -p $(BUILD) + g=$*; $(IVERILOG) -g2012 -Itb -DLANES=$${g%x*} -DWPL=$${g#*x} -o $@ $(REF) $(ENGINE) tb/tb_engine.sv + +sim-core: + @mkdir -p $(BUILD) + $(IVERILOG) -g2012 -Itb -o $(BUILD)/tb_core.vvp $(REF) $(ENGINE) $(CORE) tb/tb_pcie_core.sv + @$(call vvp_check,$(VVP) -n $(BUILD)/tb_core.vvp,$(BUILD)/core.log) + +# --output-split-cfuncs 0: Verilator 5.020 splits the testbench's main initial +# block (a coroutine with a fork) across C++ functions and drops the code that +# waits for the join; the binary then crashes (illegal instruction). +sim-verilator: + @mkdir -p $(BUILD) + $(VERILATOR) --binary --timing --timescale 1ns/1ps -Wno-fatal -Wno-lint -Wno-style -Itb --output-split-cfuncs 0 \ + --top-module tb_engine -Mdir $(BUILD)/verilator $(REF) $(ENGINE) tb/tb_engine.sv >/dev/null + @$(call vvp_check,$(BUILD)/verilator/Vtb_engine +ops=$(OPS),$(BUILD)/verilator.log) + +soc: + cd litex && $(PYTHON) bitacc_litefury.py --driver --output-dir $(SOC_DIR) + +# $(call cosim_build,dir,lanes,words per lane[,MATCH units]): host program + Verilated core + mock. +define cosim_build + @test -d $(DRIVER) || { echo "run 'make soc' first"; exit 1; } + @mkdir -p $(1) + cc -O2 -Wall -Wextra -Werror -I$(DRIVER)/user/liblitepcie -I$(DRIVER)/kernel \ + -c -o $(abspath $(1))/bitacc_pcie.o host/bitacc_pcie.c + $(VERILATOR) --cc --exe --build -O2 --timescale 1ns/1ps -Wno-fatal -Wno-lint \ + -GLANES=$(2) -GWORDS_PER_LANE=$(3) $(if $(4),-GMATCH_LANES=$(4)) --top-module bitacc_pcie_core -Mdir $(1)/obj \ + -CFLAGS "-I$(DRIVER)/user/liblitepcie -I$(DRIVER)/kernel -DMOCK_LANES=$(2) -DMOCK_WPL=$(3) $(if $(4),-DMOCK_MATCH_LANES=$(4))" \ + -LDFLAGS "$(abspath $(1))/bitacc_pcie.o" \ + $(ENGINE) $(CORE) $(abspath host/sim/mock_litepcie.cpp) -o bitacc_pcie_cosim >/dev/null +endef + +# Small geometry keeps the co-simulation fast; the host reads it from CSRs. +# Arguments: operations, seed, percentage of range operations. +cosim: + $(call cosim_build,$(BUILD)/cosim,4,64) + $(BUILD)/cosim/obj/bitacc_pcie_cosim selftest 200000 1 5 + $(BUILD)/cosim/obj/bitacc_pcie_cosim selftest 50000 9 50 + +# The LiteFury configuration with MATCH: 8 lanes x 2048 words, 2 MATCH units. +# Fails if a bench needs more cycles per descriptor than the design intends: +# single-bit 1 (plus 2%), COUNT/FIND over 16384 words 16384/8 steps of 1 +# cycle, BULK over 8192 words 8192/8 steps of 2 cycles, MATCH over 16384 +# words 16384/2 sub-steps of 1 cycle (plus 1% and a fixed allowance for +# pipeline fill and the MATCH pattern load). +perf: + $(call cosim_build,$(BUILD)/perf,8,2048,2) + $(BUILD)/perf/obj/bitacc_pcie_cosim selftest 20000 3 20 + @for kl in get:1.02 count:2084 find:2084 xor:2084 match:8300; do \ + k=$${kl%:*}; limit=$${kl#*:}; n=$$( [ $$k = get ] && echo 200000 || echo 2048 ); \ + out=$$($(BUILD)/perf/obj/bitacc_pcie_cosim bench $$k $$n 2>&1) || { echo "$$out"; exit 1; }; \ + echo "$$out" | grep -E '^(bench|mock)'; \ + echo "$$out" | awk -v l=$$limit '/^mock:/ { c = $$(NF-1) } END { if (c == "" || c + 0 > l) { print "cycles/descriptor " c " above " l; exit 1 } }' || exit 1; \ + done + +TIMING_CONFIGS ?= 8:2048:1:2 8:2048:0 +timing: + @set -e; cd timing; for c in $(TIMING_CONFIGS); do \ + echo "== $$c (lanes:words per lane:MATCH)"; ./run.sh $$(echo $$c | tr ':' ' '); done + +bitstream: + cd litex && $(PYTHON) bitacc_litefury.py --build --driver --output-dir $(SOC_DIR) + +clean: + rm -rf $(BUILD) host/build_cosim diff --git a/pcie/README.md b/pcie/README.md new file mode 100644 index 0000000000000000000000000000000000000000..1ea4364f6cad249668c154486d54ab8606d226f5 --- /dev/null +++ b/pcie/README.md @@ -0,0 +1,365 @@ +# PCIe accelerator (LiteFury) + +The parallel version of the bit accelerator, attached to a host over PCIe. +`rtl/bit_accelerator.sv` is the golden functional reference, and the +parallel engine must produce identical results. + +``` +host RAM ──DMA reader──▶ descriptors ─▶ dispatch ─┬▶ lane 0 … lane N-1 ─┬▶ reorder buffer ─▶ results ──DMA writer──▶ host RAM + (128-bit) │ (word w → lane w mod N)│ (descriptor order) (64-bit, 2 per beat) + └▶ range unit ──────────┘ + (all lanes at once) +CSRs (BAR0): word access to the bit store, status, counters, geometry, format version, features +``` + +| Path | Contents | +|---|---| +| `rtl/bitacc_engine.sv` | N lanes, each owning one memory bank; range unit with pattern matcher; reorder buffer; host word port | +| `rtl/bitacc_pcie_core.sv` | 128-bit descriptor/result streams, result packing, register interface | +| `tb/gold_ops.svh` | reference semantics of every operation, executed on the reference core | +| `tb/range_stim.svh` | random range-operation stimulus and coverage goals | +| `tb/tb_engine.sv` | engine vs the reference core | +| `tb/tb_pcie_core.sv` | PCIe core vs the reference core | +| `../formal/lane_map.mlw` | Why3 proof of the BULK lane-mapping arithmetic | +| `litex/bitacc_litefury.py` | LiteX SoC: LitePCIe Gen2 x4, one DMA channel, CSRs, DDR3 controller | +| `host/bitacc_pcie.c` | Linux host program (`info`, `selftest`, `bench`) on the generated liblitepcie | +| `host/sim/mock_litepcie.cpp` | liblitepcie stand-in backed by the Verilated core, for co-simulation | +| `host/cpu_bench.c` | the same work on the host CPU, for comparison (`make -C host cpu_bench`) | +| `timing/` | open-source place and route for the XC7A100T: wrapper, pins, `run.sh` | + +## Operations + +One 16-byte descriptor per operation (format version 3, readable from the +`version` CSR; the `features` CSR says whether the MATCH unit is built in, +and with how many units): + +| Bits | Field | +|---|---| +| `[63:0]` | `a`: bit address (single-bit and bit-range operations), or destination word (BULK) | +| `[67:64]` | opcode | +| `[68]` | `dry`: BULK computes and counts without writing | +| `[71:69]` | `fn`: BULK function; MATCH result kind | +| `[103:72]` | `len`: range length in bits, or in words for BULK | +| `[127:104]` | `src`: source word (BULK); pattern word (MATCH, mask in `src + 1`) | + +| Opcode | Operation | Result value | Result bit | +|---|---|---|---| +| 0 GET, 1 TEST | read bit `a` | 0 | the bit | +| 2 SET, 3 CLEAR, 4 TOGGLE | write bit `a` | 0 | the new bit | +| 8 COUNT | count set bits in `[a, a+len)` | the count | count ≠ 0 | +| 9 FIND1, 10 FIND0 | first set / clear bit in `[a, a+len)` | its index (0 if none) | found | +| 11 SETR, 12 CLEARR, 13 FLIPR | set / clear / invert every bit of `[a, a+len)` | set bits before the operation | value ≠ 0 | +| 14 BULK | `dst[k] = fn(dst[k], src[k])` for words `k < len`; fn 0 COPY, 1 AND, 2 OR, 3 XOR, 4 ANDN (`dst & ~src`) | set bits in the results | value ≠ 0 | +| 15 MATCH | find the 64-bit pattern (word `src`) under its mask (word `src + 1`) at every bit position of `[a, a+len)` | fn 0: number of matches; fn 1: first matching position (0 if none) | fn 0: value ≠ 0; fn 1: found | +| 5–7 | undefined | error | | + +A position `s` matches when every mask bit `j` lies inside the range +(`s + j < a + len`) and `bit(s + j) == pattern[j]`. The mask can be any +64-bit value: a run of low bits is a pattern of that length, and other masks +give patterns with gaps. A zero mask matches every position. The host writes +the pattern and mask words like any other data (word writes, or SETR/CLEARR +and BULK COPY in the descriptor stream). + +One 8-byte result per descriptor, in descriptor order: +`[7:0] = 0xA0 | error << 1 | bit` (the UART builds' status byte), `[63:8] = value`. + +These are errors, with no write and a result of bit 0, value 0: +- A single-bit address outside the store. +- A bit range with `a + len > words × 64`, including 64-bit wrap. +- A BULK region outside the store. +- A BULK with partially overlapping regions. Identical regions are allowed. +- BULK with `fn > 4`; MATCH with `fn > 1`, with `src + 2 > words`, or on a + build without the MATCH unit. +- An undefined opcode. + +`len = 0` is valid: the value is 0 and FIND and MATCH find nothing. + +Each range descriptor does up to a whole bit store's worth of work: 1 Mbit +at the default 8 lanes × 2,048 words. That is the point. Single-bit +descriptors are limited by the link, and range descriptors are not. + +## Why the results equal the reference + +**Single-bit operations.** Word `w` lives in lane `w mod N`, so every +operation on a word runs in one lane, in arrival order. Operations on +different words touch disjoint state and commute. Errors never write. + +**Range operations.** A range operation is a barrier: +- It starts only when every earlier operation has completed in its lane. +- It runs on all lanes at once. +- Nothing after it dispatches until it has finished. + +Inside a range operation every word is read once and written at most once. +BULK regions are either disjoint or identical, so no word is both a source +and a different destination. The final memory and every result therefore +equal executing the stream one operation at a time, and the reorder buffer +returns results in descriptor order. + +**BULK lane mapping.** BULK pairs `dst + k` with `src + k`, which usually +live in different lanes. In step `j`: +- Lane `i` reads source element `j·N + ((i − src) mod N)`. +- Destination lane `m` takes its source word from lane `(m − delta) mod N`, where `delta = (dst − src) mod N`. + +`formal/lane_map.mlw` proves the index identities this relies on, for any +`N > 0`: every element is read exactly once, by the lane that owns it, at +the right local address, and the rotation hands it to the lane that owns +its destination word. Why3 1.6 with Z3 proves all 58 goals. A deliberately +wrong rotation fails to prove. + +**Range semantics are defined by the reference core.** The testbenches +execute every range operation as single-bit operations on +`rtl/bit_accelerator.sv` (`tb/gold_ops.svh`). For example, COUNT is GETs of +every bit, and BULK is GET src, GET dst, then SET or CLEAR dst for every +bit. MATCH GETs the 64 pattern bits, the 64 mask bits and every haystack bit, +then applies the match rule above. Which range operations are errors is a specification choice, and it is +cross-checked against the reference core: the core must reject the last bit +of every invalid non-empty bit range. The host program's model +(`host/bitacc_pcie.c`) is a second, independent word-level implementation, +checked against the RTL in co-simulation. + +The argument above is backed by testing and by the lane-mapping proof. It +is not a formal proof of the whole engine. + +## Verification + +`tb/tb_engine.sv` runs the engine and the reference on the same random stream: +- Hot-spot bursts that queue in one lane while others overtake them. +- Random, past-the-end and 64-bit-wrapping addresses, and undefined opcodes. +- About 10% range operations in every class: short (word-crossing), + multi-step, empty, ending exactly at the end of the store, past the end, + wrapping, and whole-store. +- BULK with disjoint, identical, overlapping and out-of-range regions, + every function, and dry runs. +- FIND over regions a fill just emptied. +- MATCH, with the mask built in the stream right before it (by CLEARR/SETR): + - contiguous runs of mask bits; + - zero masks; + - two far-apart bits, so windows cross words; + - a full mask whose pattern is a BULK COPY of a haystack word; + - whatever the memory holds. + + Both result kinds and the error cases are covered. +- Input gaps, and result backpressure with stalls long enough to fill the + reorder buffer. + +It compares every result (error, bit and value) and every final memory +word. It fails unless all of these actually occurred: +- every class above; +- out-of-order completion and a full reorder buffer; +- both barrier directions: a range operation waiting for dispatched + single-bit operations, and an operation waiting for a running range + operation; +- FIND found and not found; +- MATCH with matches, with none, found, not found, and over several steps. + +Coverage is counted from what was executed, not from the generator's intent. + +| Check | Result | +|---|---| +| Engine, 2×4, 4×16, 8×16, 8×512 lanes × words, seeds 1 and 7, plus 16×8, a build without MATCH, and MATCH with fewer units than lanes (4 lanes/1 unit, 8 lanes/2 units); 4,000 operations each (Icarus) | 0 mismatches. 8,068–24,425 checks per run; up to 1.41 M reference-core operations per run. | +| Engine under Verilator | 0 mismatches, all coverage goals met | +| PCIe core, 3,200 descriptors including 469 range operations (Icarus) | 9,897 checks, 0 failures | +| Host program against the Verilated core (`make cosim`) | 200,704 descriptors (12,855 range, 1,732 valid MATCH) and 50,176 (27,821 range, 3,726 valid MATCH): 0 mismatches | +| Host program at the LiteFury geometry, 8×2,048 (`make perf`) | 20,480 descriptors (4,876 range, 676 valid MATCH): 0 mismatches; 16,384 words checked | +| Injected faults, range unit | 22/22 detected against the final testbench. Examples: mask edges, rotation direction, BULK bound and last step, src/dst base, barrier in either direction, FIND lane order and first hit, overlap check, dry run, ANDN, fills of partial words, counting after instead of before, range end bound, missing dst read, stale value on an error result. | +| Injected faults, MATCH and the pipelined front end | 18/18 detected: inverted compare, wrong neighbour lane, lost previous-step word, validity not shifted by a word, mask top bit ignored, last step missing, index not shifted, pattern and mask swapped, result kind ignored, pattern word bound, empty match range, pattern not awaited; skid buffer ignoring reorder space or overfilling, wrong skid slot, barrier counter, stale queue-full flag, write register not cleared | +| Injected faults, MATCH sub-steps (`MATCH_LANES` < `LANES`) | 6/6 detected: unit word ignoring the sub-step, mask ignoring it, result index ignoring it, group ending before its last sub-step, previous group's word from the wrong lane, sub-step counter not reset | +| Injected faults, PCIe core | 9/9 detected (result order in a beat, value position, error/bit swap, opcode bit 3, len and src fields, fn/dry fields, beat size, counter step) | +| Injected faults, host co-simulation | FIND lane order, BULK rotation and fill count all detected (780–9,386 mismatches) | +| Lane mapping (`formal/lane_map.mlw`) | 58/58 goals proved by Z3 | +| Lint | `verilator -Wall` clean, with and without MATCH, 2 to 16 lanes | + +Problems found and fixed while building this: +- **MATCH compared the wrong way.** The first matcher reported matches + where masked bits *differed*, `&((w ^ p) | ~m)` instead of + `~|((w ^ p) & m)`. The reference model caught it on the first run. +- **The 65-bit end-address sum didn't wrap.** Casting the whole sum to 65 + bits widened `(base << 3) + offset` too, so the effective address no + longer wrapped mod 2⁶⁴. Verilator's lint flagged it; `eff` is now an + explicit 64-bit value. +- **An empty range returned a stale value.** One restructuring sent empty + ranges straight to the result, skipping the state that clears the + accumulator. I found it on review; every range now passes through that + state. +- **Verilator 5.020 crashed the engine testbench** (illegal instruction). + It splits the main initial block, a coroutine with a fork, across C++ + functions and drops the wait for the join. `--output-split-cfuncs 0` + avoids it. +- From the previous round: the CI gate that didn't gate (vvp piped into + grep), Verilator's degenerate seeded `$random`, and the result packer's + stall every third cycle. + +## Build + +```sh +make lint # RTL lint +make -j8 sim sim-verilator # equivalence runs (the 8x512 runs take ~6 minutes each) +make soc # LiteX sources + Linux driver (needs LiteX, no Vivado) +make cosim # host program against the Verilated core +make perf # cycle-level throughput at 8 lanes x 2048 words, with limits +make timing # open-source place and route (needs nextpnr-xilinx, see below) +make bitstream # needs Vivado (free edition covers the XC7A100T) +make -C host cpu_bench # the same work on the host CPU +``` + +On the NUC, load the kernel driver from the generated +`build/litefury/driver/kernel`, then build and run the host program: + +```sh +make -C host LITEPCIE=../build/litefury/driver +./host/bitacc_pcie info +./host/bitacc_pcie selftest 10000000 1 5 # operations, seed, % range operations +./host/bitacc_pcie bench match 100000 # also: get, count, find, xor +./host/cpu_bench # the NUC's CPU on the same work +``` + +The SoC leaves out the MATCH unit by default. `litex/bitacc_litefury.py --match` +includes it with 2 units; `--match-lanes N` changes the unit count. See +Resources for the sizes. + +LiteX supports the LiteFury as the SQRL Acorn CLE-101, which litex-boards +documents as equivalent (`litex_boards/platforms/sqrl_acorn.py`). The same +bitstream works in the NUC's M.2 Key-M slot. It also works behind a USB4/ +Thunderbolt enclosure that tunnels PCIe; nothing in the design depends on the +transport. I expect, but have not verified, that an ASM2464PD-based M.2 +enclosure presents the card as a normal PCIe device in USB4/TB mode. It is +NVMe-only in USB 3 mode. + +## Timing (open-source estimate) + +Vivado is the tool that decides whether the design meets 125 MHz, and it +hasn't been run. As an early signal, `timing/` places and routes the engine +and PCIe core for the LiteFury's `xc7a100t-fgg484-2` with +**nextpnr-xilinx** (openXC7) and the Project X-Ray database. A +register-bounded wrapper (`timing/timing_top.sv`) stands in for LitePCIe; +LitePCIe itself isn't included. + +Toolchain, built from source in this session: +- nextpnr-xilinx from `github.com/openXC7/nextpnr-xilinx`; +- the chip database exported with its `bbaexport.py` from `prjxray-db/artix7`. + +Point `NEXTPNR` and `CHIPDB` at them and run `make timing`. + +| Design | Fmax (nextpnr-xilinx) | LUTs (Yosys) | Flip-flops | RAMB36 | +|---|---|---|---|---| +| Before the timing work (range operations only) | 72.1 MHz | 9,948 | 2,670 | 32 | +| 8 lanes, no MATCH | 88.3 MHz | 9,879 (+50 RAM32M) | 5,505 | 32 | +| 8 lanes, MATCH with 2 units (`MATCH_LANES = 2`) | 83.6 MHz | 18,447 (+50 RAM32M) | 6,168 | 32 | +| 8 lanes, MATCH with 8 units | did not route: stopped after 52 minutes in the router's first pass, 63,127 overused wires | 41,446 (+50 RAM32M) | 5,615 | 32 | +| 16 lanes × 1,024 words, no MATCH | 70.8 MHz | 19,864 (+74 RAM32M) | 9,254 | 32 | + +**No configuration meets 125 MHz in this flow.** The runs found real +structural paths, which are fixed: +- the descriptor decode, a 64-bit add then compares in one cycle; +- the combinational ready chain from dispatch back to the descriptor + source; +- block-RAM ports driven through logic; +- 64-deep popcount and lowest-set-bit chains; +- cross-lane crossbars ending in logic; +- control registers with about 1,000 loads each. + +The fixes: +- registered sums; +- a 2-entry circular skid buffer with registered ready, so taking a + descriptor only moves a read pointer; +- a registered outstanding-operation counter for the barrier; +- registered queue-full flags; +- registered addresses, write ports and masks; +- balanced trees; +- crossbars that end in registers; +- multi-cycle range and MATCH set-up; +- per-lane copies of the step's control registers and per-unit copies of + the MATCH pattern and mask. These carry `keep`; without it Yosys merges + them back. + +What remains is mostly routing between lanes that the placer spreads over +the chip: +- **8 lanes:** about 1.7 ns of logic and 9.6 ns of routing, from a lane's + result word through the popcount tree. With MATCH it is 0.8 ns of logic + and 11.1 ns of routing, from a unit's window register into its compare. +- **16 lanes:** 0.6 ns of logic and 13.6 ns of routing, on the BULK + crossbar. Its lanes are far apart. + +nextpnr-xilinx's placer is weaker than Vivado's, which also replicates +high-fanout drivers and pipelines placement-critical nets itself. So these +numbers are pessimistic, but by how much is unknown. If Vivado also misses +125 MHz: +- run the engine from its own slower clock (LitePCIe stays at 125 MHz); +- or add a pipeline stage after the popcount. + +## Resources (estimate) + +Yosys `synth_xilinx -abc9 -nowidelut` (the flow above), 128 KiB bit store, +engine and PCIe core only (no LitePCIe, no DDR3 controller): + +| Lanes × words per lane | MATCH units | LUTs | of XC7A100T (63,400) | Flip-flops | RAMB36 | +|---|---|---|---|---|---| +| 8 × 2,048 (default) | none | 9,879 | 16% | 5,505 | 32 | +| 8 × 2,048 | 2 | 18,447 | 29% | 6,168 | 32 | +| 8 × 2,048 | 8 | 41,446 | 65% | 5,615 | 32 | +| 16 × 1,024 | none | 19,864 | 31% | 9,254 | 32 | + +A MATCH unit checks 64 positions × 64 mask bits per cycle and costs about +4,000 LUTs. `MATCH_LANES` sets the number of units independently of +`LANES`, which must be a multiple of it. With fewer units, each group of +`LANES` words is matched in `LANES / MATCH_LANES` sub-steps. 8 units do not +fit in practice; **2 units are the recommended LiteFury setting**, and the +LiteX default when `--match` is given (`--match-lanes N` changes it). + +16 lanes double COUNT/FIND/BULK throughput per cycle, at twice the LUTs and +a lower clock in this flow: 70.8 against 88.3 MHz. At these clocks that is +still about 1.6× the 8-lane throughput. + +## Performance + +Cycle counts are measured by `make perf`: the host program's `bench` +against the Verilated core at 8 × 2,048 with 2 MATCH units. The DMA +stand-in offers one descriptor and takes one result beat per cycle, with no +PCIe latency. The counts are therefore the engine's own throughput. Rates +are given at 125 MHz, the target; this design doesn't reach it in the +open-source flow (see Timing). At the 83.6 MHz measured there for this +configuration, they scale by 0.67. + +| Descriptor | Cycles each | Work | At 125 MHz | +|---|---|---|---| +| single-bit GET (random) | 1.003 | 1 bit | 125 M descriptors/s | +| COUNT, 1 Mbit | 2,057 | 1,048,576 bits | 16.5 µs, 64 Gbit/s | +| FIND1, 1 Mbit, not found | 2,057 | 1,048,576 bits | 16.5 µs | +| BULK XOR (dry), 8,192 words | 2,057 | 524,288 bits of dst | 16.5 µs, 32 Gbit/s of dst | +| MATCH (2 units), 16-bit pattern, 1 M positions | 8,213 | 1,048,576 positions | 65.7 µs, 16 G positions/s | + +With 8 units MATCH took 2,066 cycles (63 G positions/s at 125 MHz), but that +configuration doesn't fit (see Resources). + +**Against the CPU.** `host/cpu_bench.c` does the same work on the host: +popcount loops, and a MATCH that compilers vectorize (checked against a +naive one). Measured on this repository's build machine (Intel Xeon VM, +2.8 GHz, 4 vCPUs, AVX-512, `-O3 -march=native`), 128 KiB store: + +| Work (128 KiB store) | CPU, 1 thread | CPU, 4 threads (total) | Card at 125 MHz | Card at 83.6 MHz | +|---|---|---|---|---| +| COUNT, 1 Mbit | 10.5 µs (100 Gbit/s) | 210 Gbit/s | 64 Gbit/s | 43 Gbit/s | +| XOR + count, 2 × 64 KiB | 8.9 µs (59 Gbit/s of dst) | 217 Gbit/s | 32 Gbit/s | 21 Gbit/s | +| MATCH, 16-bit pattern | 2.0 G positions/s | 7.0 G positions/s | 16 G positions/s (2 units) | 10.7 G positions/s | +| MATCH, 64-bit pattern | 4.0 G positions/s | 8.9 G positions/s | 16 G positions/s (2 units) | 10.7 G positions/s | + +**Verdict:** +- **COUNT and BULK:** the card is slower than one CPU core. +- **MATCH with 2 units:** + - at 125 MHz, about 2× this machine's 4 threads and 4–8× one thread; + - at the 83.6 MHz of the open-source estimate, about 1.2–1.5× the 4 + threads. + + It is the only operation where the card is expected to beat the CPU, and + only by a small margin on a 4-core host. These are cycle counts times an + assumed clock, not hardware measurements. + +I haven't measured the NUC; run `host/cpu_bench` there to compare with its +real CPU. + +## Generated-code note + +LiteX's generated `liblitepcie/litepcie_dma.c` (at the LiteX revision used +here) checks `if (poll < 0)` instead of the return value of `poll()`, so poll +errors are ignored. The host program builds that file as shipped, without +`-Werror`. diff --git a/pcie/host/.gitignore b/pcie/host/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..0991b7f065b5d9f88a81d0fde1e11610499c0170 --- /dev/null +++ b/pcie/host/.gitignore @@ -0,0 +1 @@ +cpu_bench diff --git a/pcie/host/Makefile b/pcie/host/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..fc1c3cc200cea596408bc6f5332ac55686711635 --- /dev/null +++ b/pcie/host/Makefile @@ -0,0 +1,25 @@ +# Host program for the LitePCIe build. LITEPCIE points at the driver directory +# that pcie/litex/bitacc_litefury.py --driver generates (/driver). +LITEPCIE ?= ../../build/litefury/driver +CC ?= cc +CFLAGS ?= -O2 -Wall -Wextra +LIB := $(LITEPCIE)/user/liblitepcie +INC := -I$(LIB) -I$(LITEPCIE)/kernel + +# Our code builds with -Werror; the generated library is compiled as shipped. +bitacc_pcie: bitacc_pcie.o litepcie_dma.o litepcie_helpers.o + $(CC) -o $@ $^ -lm + +bitacc_pcie.o: bitacc_pcie.c + $(CC) $(CFLAGS) -Werror $(INC) -c -o $@ $< + +litepcie_%.o: $(LIB)/litepcie_%.c + $(CC) $(CFLAGS) $(INC) -c -o $@ $< + +# CPU baseline for the range operations (no driver needed). +cpu_bench: cpu_bench.c + $(CC) -O3 -march=native -Wall -Wextra -Werror -o $@ $< -lpthread + +clean: + rm -f bitacc_pcie cpu_bench *.o +.PHONY: clean diff --git a/pcie/host/bitacc_pcie.c b/pcie/host/bitacc_pcie.c new file mode 100644 index 0000000000000000000000000000000000000000..26fcaf5a5600f817c177bf41841eb2edbeef0cc1 --- /dev/null +++ b/pcie/host/bitacc_pcie.c @@ -0,0 +1,561 @@ +/* + * bitacc_pcie: host program for the bit accelerator on a LitePCIe card + * (pcie/litex/bitacc_litefury.py). Built against the driver LiteX generates: + * + * make LITEPCIE=/driver + * + * bitacc_pcie [-c /dev/litepcie0] info + * bitacc_pcie [-c /dev/litepcie0] selftest [operations] [seed] [range-percent] + * bitacc_pcie [-c /dev/litepcie0] bench get|count|find|xor|match [descriptors] + * + * selftest loads random data into the whole bit store (CSRs), streams random + * descriptors (single-bit and range operations) through the DMA reader, checks + * every result returned by the DMA writer against a model of the engine, then + * reads the bit store back and compares it. It prints the descriptor + * throughput of the DMA phase. + * + * bench streams one kind of descriptor and prints descriptors/s and the bits + * of the bit store they cover per second: + * get random single-bit GETs + * count COUNT over the whole bit store + * find FIND1 over the whole bit store, which bench clears first (not found) + * xor BULK XOR, dry run, of the lower half of the store with the upper half + * match MATCH (count) of a 16-bit pattern over the whole bit store + * + * Descriptor (16 bytes, little-endian), format version 3: + * [63:0] a [67:64] op [68] dry [71:69] fn [103:72] len [127:104] src + * Result (8 bytes, little-endian): [7:0] 0xA0 | error << 1 | bit, [63:8] value. + * Semantics: pcie/rtl/bitacc_engine.sv. The DMA moves DMA_BUFFER_SIZE bytes + * per buffer, so a run is padded with no-op descriptors (opcode 7: no write, + * result "error") to whole buffers in both directions. + */ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "liblitepcie.h" + +#define FORMAT_VERSION 3 +#define DESC_BYTES 16 +#define RESULT_BYTES 8 +#define DESCS_PER_BUFFER (DMA_BUFFER_SIZE / DESC_BYTES) +#define RESULTS_PER_BUFFER (DMA_BUFFER_SIZE / RESULT_BYTES) +/* A run fills whole buffers in both directions. */ +#define RUN_QUANTUM (DESCS_PER_BUFFER > RESULTS_PER_BUFFER ? DESCS_PER_BUFFER : RESULTS_PER_BUFFER) + +enum { OP_GET, OP_TEST, OP_SET, OP_CLEAR, OP_TOGGLE, OP_NOP = 7, + OP_COUNT = 8, OP_FIND1, OP_FIND0, OP_SETR, OP_CLEARR, OP_FLIPR, OP_BULK, OP_MATCH }; +enum { FN_COPY, FN_AND, FN_OR, FN_XOR, FN_ANDN }; +#define RESULT_ERROR 0xA2ull + +struct desc { + uint64_t a; /* bit address, or dst word (BULK) */ + uint8_t op, fn, dry; + uint32_t len; /* bits, or words (BULK) */ + uint32_t src; /* src word (BULK), pattern word (MATCH; mask in src + 1), 24 bits */ +}; + +static const char *device = "/dev/litepcie0"; +static int has_match; /* gateware has the MATCH unit (features CSR) */ + +/* ---------------------------------------------------------------- CSRs */ +static uint32_t rd(int fd, uint32_t a) { return litepcie_readl(fd, a); } +static void wr(int fd, uint32_t a, uint32_t v) { litepcie_writel(fd, a, v); } + +static uint32_t status(int fd) { return rd(fd, CSR_BITACC_STATUS_ADDR); } +#define ST_IDLE (1u << CSR_BITACC_STATUS_IDLE_OFFSET) +#define ST_HOST_BUSY (1u << CSR_BITACC_STATUS_HOST_BUSY_OFFSET) +#define ST_RDATA_VALID (1u << CSR_BITACC_STATUS_RDATA_VALID_OFFSET) + +static int wait_status(int fd, uint32_t mask, uint32_t want) +{ + for (int i = 0; i < 1000000; i++) + if ((status(fd) & mask) == want) + return 0; + fprintf(stderr, "bitacc: timeout waiting for status 0x%x (status 0x%x)\n", want, status(fd)); + return -1; +} + +/* 64-bit CSRs: most significant word at the lower address (CSR_ORDERING_BIG). */ +static int word_write(int fd, uint32_t index, uint64_t v) +{ + wr(fd, CSR_BITACC_HOST_WORD_ADDR, index); + wr(fd, CSR_BITACC_HOST_WDATA_ADDR, (uint32_t)(v >> 32)); + wr(fd, CSR_BITACC_HOST_WDATA_ADDR + 4, (uint32_t)v); + wr(fd, CSR_BITACC_HOST_CTRL_ADDR, 1u << CSR_BITACC_HOST_CTRL_WRITE_OFFSET); + return wait_status(fd, ST_HOST_BUSY, 0); +} + +static int word_read(int fd, uint32_t index, uint64_t *v) +{ + wr(fd, CSR_BITACC_HOST_WORD_ADDR, index); + wr(fd, CSR_BITACC_HOST_CTRL_ADDR, 1u << CSR_BITACC_HOST_CTRL_READ_OFFSET); + if (wait_status(fd, ST_HOST_BUSY | ST_RDATA_VALID, ST_RDATA_VALID)) + return -1; + *v = ((uint64_t)rd(fd, CSR_BITACC_HOST_RDATA_ADDR) << 32) | rd(fd, CSR_BITACC_HOST_RDATA_ADDR + 4); + return 0; +} + +static void geometry(int fd, uint32_t *lanes, uint32_t *wpl) +{ + uint32_t c = rd(fd, CSR_BITACC_CONFIG_ADDR); + *lanes = (c >> CSR_BITACC_CONFIG_LANES_OFFSET) & 0xff; + *wpl = c >> CSR_BITACC_CONFIG_WORDS_PER_LANE_OFFSET; +} + +static int check_version(int fd) +{ + uint32_t v = rd(fd, CSR_BITACC_VERSION_ADDR); + if (v != FORMAT_VERSION) { + fprintf(stderr, "bitacc: gateware descriptor format %u, this program speaks %u\n", v, FORMAT_VERSION); + return -1; + } + has_match = (rd(fd, CSR_BITACC_FEATURES_ADDR) >> CSR_BITACC_FEATURES_MATCH_OFFSET) & 1; + return 0; +} + +/* ---------------------------------------------------------------- descriptors */ +static void encode(uint8_t *d, const struct desc *x) +{ + for (int i = 0; i < 8; i++) d[i] = (uint8_t)(x->a >> (8 * i)); + d[8] = (uint8_t)((x->op & 15) | (x->dry & 1) << 4 | (x->fn & 7) << 5); + for (int i = 0; i < 4; i++) d[9 + i] = (uint8_t)(x->len >> (8 * i)); + for (int i = 0; i < 3; i++) d[13 + i] = (uint8_t)(x->src >> (8 * i)); +} + +static uint64_t decode_result(const char *p) +{ + uint64_t r = 0; + for (int i = 0; i < 8; i++) r |= (uint64_t)(uint8_t)p[i] << (8 * i); + return r; +} + +/* ---------------------------------------------------------------- model */ +/* + * Word-level model of pcie/rtl/bitacc_engine.sv: returns the expected 64-bit + * result and applies the operation to mem. It is written independently of + * the RTL testbenches, which expand range operations bit by bit on the + * reference core. + */ +static uint64_t result(int bit, uint64_t value) { return 0xA0ull | (uint64_t)(bit != 0) | value << 8; } + +static uint64_t model(uint64_t *mem, uint64_t words, const struct desc *x) +{ + uint64_t bits = words * 64; + if (x->op <= OP_TOGGLE) { + uint64_t w = x->a >> 6; + unsigned b = x->a & 63; + if (w >= words) + return RESULT_ERROR; + uint64_t m = 1ull << b, old = (mem[w] >> b) & 1, nb; + switch (x->op) { + case OP_SET: mem[w] |= m; nb = 1; break; + case OP_CLEAR: mem[w] &= ~m; nb = 0; break; + case OP_TOGGLE: mem[w] ^= m; nb = !old; break; + default: nb = old; break; + } + return result((int)nb, 0); + } + if (x->op >= OP_COUNT && x->op <= OP_FLIPR) { + if (x->a > bits || x->len > bits - x->a) + return RESULT_ERROR; + int find = x->op == OP_FIND1 || x->op == OP_FIND0, found = 0; + uint64_t cnt = 0, idx = 0, end = x->a + x->len; + for (uint64_t b = x->a; b < end && !found; ) { + uint64_t w = b >> 6; + unsigned lo = b & 63; + uint64_t n = 64 - lo < end - b ? 64 - lo : end - b; + uint64_t m = (n == 64 ? ~0ull : (1ull << n) - 1) << lo; + uint64_t v = mem[w], hit; + switch (x->op) { + case OP_COUNT: cnt += (uint64_t)__builtin_popcountll(v & m); break; + case OP_FIND1: + case OP_FIND0: + hit = (x->op == OP_FIND1 ? v : ~v) & m; + if (hit) { found = 1; idx = w * 64 + (uint64_t)__builtin_ctzll(hit); } + break; + case OP_SETR: cnt += (uint64_t)__builtin_popcountll(v & m); mem[w] = v | m; break; + case OP_CLEARR: cnt += (uint64_t)__builtin_popcountll(v & m); mem[w] = v & ~m; break; + default: cnt += (uint64_t)__builtin_popcountll(v & m); mem[w] = v ^ m; break; + } + b += n; + } + return find ? result(found, found ? idx : 0) : result(cnt != 0, cnt); + } + if (x->op == OP_MATCH) { + uint64_t a = x->a, n = x->len, p = x->src; + if (!has_match || x->fn > 1 || a > bits || n > bits - a || p + 2 > words) + return RESULT_ERROR; + uint64_t pat = mem[p], msk = mem[p + 1]; + /* position s is valid if every mask bit lies inside the haystack */ + uint64_t top = msk ? 63 - (uint64_t)__builtin_clzll(msk) : 0, cnt = 0, first = 0; + int found = 0; + for (uint64_t s = a; s < a + n && s + top < a + n; s++) { + uint64_t w = s >> 6, o = s & 63; + uint64_t lo = mem[w], hi = w + 1 < words ? mem[w + 1] : 0; + uint64_t win = o ? (lo >> o) | (hi << (64 - o)) : lo; + if (((win ^ pat) & msk) == 0) { + cnt++; + if (!found) { found = 1; first = s; } + } + } + return x->fn ? result(found, first) : result(cnt != 0, cnt); + } + if (x->op == OP_BULK) { + uint64_t d = x->a, s = x->src, n = x->len; + if (x->fn > FN_ANDN || d > words || n > words - d || s > words || n > words - s) + return RESULT_ERROR; + if (n && d != s && d < s + n && s < d + n) + return RESULT_ERROR; /* overlapping, not identical */ + uint64_t cnt = 0; + for (uint64_t k = 0; k < n; k++) { + uint64_t a = mem[d + k], b = mem[s + k], r; + switch (x->fn) { + case FN_COPY: r = b; break; + case FN_AND: r = a & b; break; + case FN_OR: r = a | b; break; + case FN_XOR: r = a ^ b; break; + default: r = a & ~b; break; + } + cnt += (uint64_t)__builtin_popcountll(r); + if (!x->dry) mem[d + k] = r; + } + return result(cnt != 0, cnt); + } + return RESULT_ERROR; /* 5-7 */ +} + +static uint64_t rng_state; +static uint64_t rng(void) +{ + rng_state ^= rng_state << 13; + rng_state ^= rng_state >> 7; + rng_state ^= rng_state << 17; + return rng_state; +} + +static uint64_t umin(uint64_t a, uint64_t b) { return a < b ? a : b; } + +/* A MATCH preceded by operations that build its mask (and sometimes its + * pattern) in the bit store; returns the number of descriptors (<= 4). */ +static int match_seq(struct desc *x, uint64_t words) +{ + uint64_t bits = words * 64, pw = rng() % (words - 1), mw = (pw + 1) * 64; + uint64_t lc = rng() % 100, l = lc < 45 ? rng() % 300 : lc < 95 ? rng() % (64 * umin(words, 64) + 1) : 0; + uint64_t a = rng() % (bits - l + 1), m = rng() % 100; + int n = 0; + memset(x, 0, 4 * sizeof *x); + if (m < 40) { /* contiguous k bits at offset o */ + uint64_t k = 1 + rng() % 12, o = rng() % (64 - k + 1); + x[n].op = OP_CLEARR; x[n].a = mw; x[n++].len = 64; + x[n].op = OP_SETR; x[n].a = mw + o; x[n++].len = (uint32_t)k; + } else if (m < 50) { /* zero mask */ + x[n].op = OP_CLEARR; x[n].a = mw; x[n++].len = 64; + } else if (m < 65 && l >= 64) { /* pattern = a haystack word, full mask */ + uint64_t h = (a + 63) / 64 + rng() % (l / 64); + if (h >= words) h = words - 1; + x[n].op = OP_BULK; x[n].fn = FN_COPY; x[n].a = pw; x[n].src = (uint32_t)h; x[n++].len = 1; + x[n].op = OP_SETR; x[n].a = mw; x[n++].len = 64; + } else if (m < 80) { /* two far-apart bits */ + x[n].op = OP_CLEARR; x[n].a = mw; x[n++].len = 64; + x[n].op = OP_SETR; x[n].a = mw + rng() % 8; x[n++].len = 1; + x[n].op = OP_SETR; x[n].a = mw + 56 + rng() % 8; x[n++].len = 1; + } /* else the mask word as it is */ + x[n].op = OP_MATCH; x[n].a = a; x[n].len = (uint32_t)l; + x[n].src = rng() % 25 == 0 ? (uint32_t)(words - 1) : (uint32_t)pw; /* past the end: error */ + x[n].fn = rng() % 12 == 0 ? (uint8_t)(2 + rng() % 6) : (uint8_t)(rng() % 2); + return n + 1; +} + +/* One or more descriptors (a MATCH sequence); returns how many (<= 4). */ +static int random_desc(struct desc *x, uint64_t words, unsigned range_pct) +{ + uint64_t bits = words * 64, r = rng(); + memset(x, 0, sizeof *x); + if (r % 100 >= range_pct) { /* single-bit */ + x->op = (r >> 8) % 12 == 0 ? (uint8_t)(5 + (r >> 16) % 3) : (uint8_t)((r >> 16) % 5); + if (r % 10 == 0) x->a = rng(); /* anywhere, mostly out of range */ + else if (r % 10 == 1) x->a = bits + rng() % 4096; /* just past the end */ + else x->a = rng() % bits; + return 1; + } + uint64_t c = rng() % 100, l; + if (c < 20) + return match_seq(x, words); + if (c < 23) { /* undefined */ + x->op = (uint8_t)(5 + rng() % 3); x->a = rng() % bits; x->len = (uint32_t)(rng() % 64); + } else if (c < 60) { /* bit range */ + x->op = (uint8_t)(OP_COUNT + rng() % 6); + uint64_t lc = rng() % 100; + l = lc < 50 ? rng() % 130 : lc < 92 ? rng() % (64 * umin(words, 64) + 1) : lc < 96 ? 0 : bits; + x->len = (uint32_t)l; + x->a = rng() % 100 < 94 ? rng() % (bits - l + 1) : bits - l + 1 + rng() % 64; + } else { /* BULK */ + x->op = OP_BULK; + x->fn = rng() % 20 == 0 ? (uint8_t)(5 + rng() % 3) : (uint8_t)(rng() % 5); + x->dry = rng() % 5 == 0; + l = rng() % (umin(words / 2, 256) + 1); + x->len = (uint32_t)l; + x->src = (uint32_t)(rng() % (words - l + 1)); + uint64_t pc = rng() % 100, d = rng() % (words - l + 1); + if (pc < 70) { + for (int t = 0; t < 50 && l && d < x->src + l && x->src < d + l; t++) + d = rng() % (words - l + 1); + } else if (pc < 85) { + d = x->src; + } else if (pc < 95) { + d = x->src + 1 + rng() % (l > 1 ? l - 1 : 1); /* overlapping: error */ + } else { + d = words - l + 1 + rng() % 8; /* past the end: error */ + } + x->a = d; + } + return 1; +} + +/* ---------------------------------------------------------------- streaming */ +/* + * Streams total descriptors (a multiple of RUN_QUANTUM) and stores the 64-bit + * results. Returns the elapsed milliseconds, or -1 on timeout. + */ +static int64_t stream(struct litepcie_dma_ctrl *dma, const uint8_t *desc, uint64_t total, uint64_t *res) +{ + uint64_t sent = 0, got = 0; + int64_t t0 = get_time_ms(), ms; + dma->reader_enable = 1; + dma->writer_enable = 1; + while (got < total) { + litepcie_dma_process(dma); + char *buf; + while (sent < total && (buf = litepcie_dma_next_write_buffer(dma))) { + memcpy(buf, desc + sent * DESC_BYTES, DMA_BUFFER_SIZE); + sent += DESCS_PER_BUFFER; + } + while ((buf = litepcie_dma_next_read_buffer(dma))) + for (uint64_t i = 0; i < RESULTS_PER_BUFFER && got < total; i++, got++) + res[got] = decode_result(buf + i * RESULT_BYTES); + if (get_time_ms() - t0 > 120000) { + fprintf(stderr, "bitacc: timeout after %" PRIu64 " of %" PRIu64 " results\n", got, total); + break; + } + } + ms = get_time_ms() - t0; + dma->reader_enable = 0; + dma->writer_enable = 0; + litepcie_dma_process(dma); + return got == total ? ms : -1; +} + +static int open_engine(struct litepcie_dma_ctrl *dma, uint64_t *words) +{ + uint32_t lanes, wpl; + if (litepcie_dma_init(dma, device, 0)) + return -1; + int fd = dma->fds.fd; + if (check_version(fd)) + return -1; + geometry(fd, &lanes, &wpl); + *words = (uint64_t)lanes * wpl; + printf("bit store: %u lanes x %u words = %" PRIu64 " KiB\n", lanes, wpl, *words * 8 / 1024); + if (!(status(fd) & ST_IDLE)) { + fprintf(stderr, "bitacc: engine not idle; reload the FPGA\n"); + return -1; + } + return fd; +} + +/* ---------------------------------------------------------------- selftest */ +static int selftest(uint64_t ops, uint64_t seed, unsigned range_pct) +{ + int rc = 1; + uint64_t words; + struct litepcie_dma_ctrl dma = {.use_reader = 1, .use_writer = 1}; + uint64_t *mem = NULL, *expect = NULL, *res = NULL; + uint8_t *desc = NULL; + + int fd = open_engine(&dma, &words); + if (fd < 0) + goto out; + + uint64_t total = (ops + RUN_QUANTUM - 1) / RUN_QUANTUM * RUN_QUANTUM; + mem = calloc(words, sizeof *mem); + expect = malloc(total * sizeof *expect); + res = malloc(total * sizeof *res); + desc = malloc(total * DESC_BYTES); + if (!mem || !expect || !res || !desc) { perror("malloc"); goto out; } + + /* Load the bit store and a shadow copy. */ + rng_state = seed ? seed : 1; + for (uint64_t w = 0; w < words; w++) { + mem[w] = rng(); + if (word_write(fd, (uint32_t)w, mem[w])) goto out; + } + + /* Build descriptors and expected results (model runs in descriptor order). */ + uint64_t n_range = 0, n_range_ok = 0, n_match = 0, n_match_hit = 0; + uint32_t ops_before = rd(fd, CSR_BITACC_OPS_ACCEPTED_ADDR); + struct desc xs[4]; + int nx = 0, ix = 0; + for (uint64_t n = 0; n < total; n++) { + struct desc x; + if (ix == nx && n < ops) { + nx = random_desc(xs, words, range_pct); + ix = 0; + } + if (ix < nx) { + x = xs[ix++]; /* may run past ops: padding then starts later */ + } else { + memset(&x, 0, sizeof x); + x.op = OP_NOP; + } + expect[n] = model(mem, words, &x); + if (x.op >= OP_COUNT) { + n_range++; + n_range_ok += expect[n] != RESULT_ERROR; + n_match += x.op == OP_MATCH && expect[n] != RESULT_ERROR; + n_match_hit += x.op == OP_MATCH && (expect[n] & 0xFF) == 0xA1; + } + encode(desc + n * DESC_BYTES, &x); + } + + uint64_t mismatches = 0; + int64_t ms = stream(&dma, desc, total, res); + if (ms < 0) mismatches++; + for (uint64_t n = 0; n < total && ms >= 0; n++) + if (res[n] != expect[n] && mismatches++ < 10) + fprintf(stderr, "result %" PRIu64 ": got 0x%016" PRIx64 ", expected 0x%016" PRIx64 "\n", + n, res[n], expect[n]); + + uint32_t accepted = rd(fd, CSR_BITACC_OPS_ACCEPTED_ADDR) - ops_before; + if (accepted != (uint32_t)total) { + fprintf(stderr, "ops_accepted advanced by %u, expected %" PRIu64 "\n", accepted, total); + mismatches++; + } + + /* Compare the bit store. */ + if (wait_status(fd, ST_IDLE, ST_IDLE)) mismatches++; + for (uint64_t w = 0; w < words; w++) { + uint64_t v; + if (word_read(fd, (uint32_t)w, &v)) { mismatches++; break; } + if (v != mem[w] && mismatches++ < 20) + fprintf(stderr, "word %" PRIu64 ": 0x%016" PRIx64 ", expected 0x%016" PRIx64 "\n", w, v, mem[w]); + } + + printf("selftest: %" PRIu64 " descriptors (%" PRIu64 " range, %" PRIu64 " of them valid, %" PRIu64 + " valid MATCH with %" PRIu64 " hits), %" PRIu64 " words checked, %" PRIu64 " mismatches\n", + total, n_range, n_range_ok, n_match, n_match_hit, words, mismatches); + if (ms > 0) + printf("DMA phase: %.2f M descriptors/s (%" PRId64 " ms)\n", (double)total / ms / 1000.0, ms); + rc = mismatches ? 1 : 0; +out: + free(mem); free(expect); free(res); free(desc); + litepcie_dma_cleanup(&dma); + return rc; +} + +/* ---------------------------------------------------------------- bench */ +static int bench(const char *kind, uint64_t n) +{ + int rc = 1; + uint64_t words; + struct litepcie_dma_ctrl dma = {.use_reader = 1, .use_writer = 1}; + uint64_t *res = NULL; + uint8_t *desc = NULL; + + int fd = open_engine(&dma, &words); + if (fd < 0) + goto out; + uint64_t total = (n + RUN_QUANTUM - 1) / RUN_QUANTUM * RUN_QUANTUM; + res = malloc(total * sizeof *res); + desc = malloc(total * DESC_BYTES); + if (!res || !desc) { perror("malloc"); goto out; } + + struct desc x = {0}; + uint64_t bits_per_desc; + if (!strcmp(kind, "get")) { + x.op = OP_GET; bits_per_desc = 1; + } else if (!strcmp(kind, "count")) { + x.op = OP_COUNT; x.len = (uint32_t)(words * 64); bits_per_desc = words * 64; + } else if (!strcmp(kind, "find")) { + x.op = OP_FIND1; x.len = (uint32_t)(words * 64); bits_per_desc = words * 64; + for (uint64_t w = 0; w < words; w++) + if (word_write(fd, (uint32_t)w, 0)) goto out; + } else if (!strcmp(kind, "match")) { + if (!has_match) { fprintf(stderr, "bench: the gateware has no MATCH unit\n"); goto out; } + x.op = OP_MATCH; x.fn = 0; x.src = 0; x.len = (uint32_t)(words * 64); bits_per_desc = words * 64; + if (word_write(fd, 0, 0xBEEF) || word_write(fd, 1, 0xFFFF)) goto out; /* 16-bit pattern */ + } else if (!strcmp(kind, "xor")) { + x.op = OP_BULK; x.fn = FN_XOR; x.dry = 1; x.a = words / 2; x.src = 0; x.len = (uint32_t)(words / 2); + bits_per_desc = words / 2 * 64; + } else { + fprintf(stderr, "bench: unknown kind '%s' (get, count, find, xor, match)\n", kind); + goto out; + } + rng_state = 1; + for (uint64_t i = 0; i < total; i++) { + if (x.op == OP_GET) x.a = rng() % (words * 64); + encode(desc + i * DESC_BYTES, &x); + } + + int64_t ms = stream(&dma, desc, total, res); + uint64_t errors = 0; + for (uint64_t i = 0; i < total && ms >= 0; i++) + errors += (res[i] & 0xFE) != 0xA0; + if (ms < 0 || errors) { + fprintf(stderr, "bench: %" PRIu64 " error results%s\n", errors, ms < 0 ? ", timeout" : ""); + goto out; + } + printf("bench %s: %" PRIu64 " descriptors in %" PRId64 " ms", kind, total, ms); + if (ms > 0) + printf(": %.3f M descriptors/s, %.3f Gbit/s of bit store covered", + (double)total / ms / 1000.0, (double)total * (double)bits_per_desc / ms / 1e6); + printf("\n"); + rc = 0; +out: + free(res); free(desc); + litepcie_dma_cleanup(&dma); + return rc; +} + +static int info(void) +{ + int fd = open(device, O_RDWR | O_CLOEXEC); + if (fd < 0) { perror(device); return 1; } + uint32_t lanes, wpl; + geometry(fd, &lanes, &wpl); + uint32_t s = status(fd); + uint32_t f = rd(fd, CSR_BITACC_FEATURES_ADDR); + printf("format %u, MATCH units %u, lanes %u, words per lane %u, idle %u, ops accepted %u, results sent %u\n", + rd(fd, CSR_BITACC_VERSION_ADDR), + (f >> CSR_BITACC_FEATURES_MATCH_OFFSET) & 1 ? (f >> CSR_BITACC_FEATURES_MATCH_LANES_OFFSET) & 0xff : 0, + lanes, wpl, !!(s & ST_IDLE), + rd(fd, CSR_BITACC_OPS_ACCEPTED_ADDR), rd(fd, CSR_BITACC_RESULTS_SENT_ADDR)); + close(fd); + return 0; +} + +int main(int argc, char **argv) +{ + int i = 1; + if (i + 1 < argc && !strcmp(argv[i], "-c")) { device = argv[i + 1]; i += 2; } + if (i < argc && !strcmp(argv[i], "info")) + return info(); + if (i < argc && !strcmp(argv[i], "selftest")) { + uint64_t ops = (i + 1 < argc) ? strtoull(argv[i + 1], NULL, 0) : 1000000; + uint64_t seed = (i + 2 < argc) ? strtoull(argv[i + 2], NULL, 0) : 1; + unsigned pct = (i + 3 < argc) ? (unsigned)strtoul(argv[i + 3], NULL, 0) : 5; + return selftest(ops, seed, pct > 100 ? 100 : pct); + } + if (i + 1 < argc && !strcmp(argv[i], "bench")) { + uint64_t n = (i + 2 < argc) ? strtoull(argv[i + 2], NULL, 0) : 100000; + return bench(argv[i + 1], n); + } + fprintf(stderr, "usage: %s [-c device] info | selftest [operations] [seed] [range-percent]" + " | bench get|count|find|xor|match [descriptors]\n", argv[0]); + return 2; +} diff --git a/pcie/host/cpu_bench.c b/pcie/host/cpu_bench.c new file mode 100644 index 0000000000000000000000000000000000000000..cefd464747cc8120d7d43d334b2dd29ce502a6dd --- /dev/null +++ b/pcie/host/cpu_bench.c @@ -0,0 +1,151 @@ +/* + * cpu_bench: the host CPU doing the work of the card's range operations, for + * comparison with `bitacc_pcie bench` and `make perf`. + * + * make -C pcie/host cpu_bench && ./pcie/host/cpu_bench [threads] + * + * Bit store: 16384 64-bit words (128 KiB), the LiteFury default geometry. + * count COUNT over the whole store (popcount) + * xor BULK XOR dry run: count of (lower half ^ upper half) + * match MATCH count of a 16-bit pattern and of a 64-bit pattern at every + * bit position (shift-outer, word-inner loop, which compilers + * vectorize; the naive per-position loop is checked against it) + * Each kind runs on 1 thread and on [threads] threads (default: all online + * CPUs), each thread on its own copy of the store. Build with -O3 + * -march=native for the machine's vector units. + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +#define WORDS 16384 + +static double now(void) +{ + struct timespec t; + clock_gettime(CLOCK_MONOTONIC, &t); + return (double)t.tv_sec + (double)t.tv_nsec * 1e-9; +} + +static uint64_t count_ones(const uint64_t *m) +{ + uint64_t c = 0; + for (int i = 0; i < WORDS; i++) c += (uint64_t)__builtin_popcountll(m[i]); + return c; +} + +static uint64_t xor_count(const uint64_t *m) +{ + uint64_t c = 0; + for (int i = 0; i < WORDS / 2; i++) c += (uint64_t)__builtin_popcountll(m[i] ^ m[WORDS / 2 + i]); + return c; +} + +/* Positions s in [0, WORDS*64 - 64]: ((bits s..s+63) ^ pat) & mask == 0. */ +static uint64_t match_count(const uint64_t *restrict m, uint64_t pat, uint64_t mask) +{ + uint64_t c = 0; + for (int w = 0; w + 1 < WORDS; w++) c += ((m[w] ^ pat) & mask) == 0; + for (int p = 1; p < 64; p++) { + uint64_t cp = 0; + for (int w = 0; w + 1 < WORDS; w++) { + uint64_t win = (m[w] >> p) | (m[w + 1] << (64 - p)); + cp += ((win ^ pat) & mask) == 0; + } + c += cp; + } + return c; +} + +static uint64_t match_naive(const uint64_t *m, uint64_t pat, uint64_t mask) +{ + uint64_t c = 0; + for (int w = 0; w + 1 < WORDS; w++) + for (int p = 0; p < 64; p++) { + uint64_t win = p ? (m[w] >> p) | (m[w + 1] << (64 - p)) : m[w]; + c += ((win ^ pat) & mask) == 0; + } + return c; +} + +enum { K_COUNT, K_XOR, K_MATCH16, K_MATCH64, K_N }; +static const char *names[K_N] = {"count (1 Mbit)", "xor (2 x 64 KiB)", "match 16-bit (1 M positions)", + "match 64-bit (1 M positions)"}; +static const double units[K_N] = {WORDS * 64.0, WORDS * 32.0, (WORDS - 1) * 64.0, (WORDS - 1) * 64.0}; + +struct job { + int kind, reps; + uint64_t *m; + uint64_t sink; +}; + +static void *worker(void *arg) +{ + struct job *j = arg; + uint64_t s = 0; + for (int r = 0; r < j->reps; r++) { + j->m[r & (WORDS - 1)] ^= 1; /* keep each repetition from being hoisted */ + switch (j->kind) { + case K_COUNT: s += count_ones(j->m); break; + case K_XOR: s += xor_count(j->m); break; + case K_MATCH16: s += match_count(j->m, j->m[7] & 0xFFFF, 0xFFFF); break; + default: s += match_count(j->m, j->m[7], ~0ull); break; + } + } + j->sink = s; + return NULL; +} + +/* Seconds per operation per thread, with n threads running at once. */ +static double run(int kind, int n, int reps) +{ + pthread_t t[256]; + struct job j[256]; + for (int i = 0; i < n; i++) { + j[i].kind = kind; j[i].reps = reps; + j[i].m = aligned_alloc(64, WORDS * sizeof(uint64_t)); + uint64_t x = 88172645463325252ull + (uint64_t)i; + for (int w = 0; w < WORDS; w++) { x ^= x << 13; x ^= x >> 7; x ^= x << 17; j[i].m[w] = x; } + } + double t0 = now(); + for (int i = 0; i < n; i++) pthread_create(&t[i], NULL, worker, &j[i]); + for (int i = 0; i < n; i++) pthread_join(t[i], NULL); + double dt = now() - t0; + for (int i = 0; i < n; i++) free(j[i].m); + return dt / reps; +} + +int main(int argc, char **argv) +{ + int threads = argc > 1 ? atoi(argv[1]) : (int)sysconf(_SC_NPROCESSORS_ONLN); + if (threads < 1) threads = 1; + if (threads > 256) threads = 256; + + /* The vectorized MATCH must agree with the naive one. */ + uint64_t *m = aligned_alloc(64, WORDS * sizeof(uint64_t)), x = 1; + for (int w = 0; w < WORDS; w++) { x ^= x << 13; x ^= x >> 7; x ^= x << 17; m[w] = x; } + for (int k = 0; k < 4; k++) { + uint64_t mask = k == 3 ? ~0ull : (1ull << (8 << k)) - 1, pat = m[11 * k + 3] & mask; + if (match_count(m, pat, mask) != match_naive(m, pat, mask)) { + fprintf(stderr, "cpu_bench: vectorized and naive MATCH disagree\n"); + return 1; + } + } + free(m); + + printf("128 KiB bit store; time per operation; rates per thread and for all threads\n"); + for (int k = 0; k < K_N; k++) { + int reps = k >= K_MATCH16 ? 40 : 4000; + double t1 = run(k, 1, reps), tn = threads > 1 ? run(k, threads, reps) : t1; + printf("%-30s 1 thread: %9.1f us (%6.1f G/s) %d threads: %9.1f us each (%6.1f G/s total)\n", + names[k], t1 * 1e6, units[k] / t1 / 1e9, threads, tn * 1e6, units[k] * threads / tn / 1e9); + } + printf("G/s: Gbit/s for count and xor, G positions/s for match.\n"); + return 0; +} diff --git a/pcie/host/sim/mock_litepcie.cpp b/pcie/host/sim/mock_litepcie.cpp new file mode 100644 index 0000000000000000000000000000000000000000..7186ce68200675b82aaa3a9bc3e06f2a2610cb57 --- /dev/null +++ b/pcie/host/sim/mock_litepcie.cpp @@ -0,0 +1,205 @@ +// liblitepcie stand-in backed by a Verilator model of bitacc_pcie_core. +// +// Links with the unmodified host program (bitacc_pcie.c) so its CSR accesses, +// descriptor encoding, padding and result checking run against the RTL: +// - litepcie_readl / litepcie_writel act on the CSR map the LiteX SoC +// generates (64-bit registers split most-significant word first); +// - DMA write buffers are streamed into the core's 128-bit input, result beats +// are collected into DMA_BUFFER_SIZE read buffers. +// The DMA stand-in offers one descriptor per cycle and takes one result beat per +// cycle, with no PCIe latency. At exit it reports the clock cycles the DMA +// phases took (MOCK_REPORT_CYCLES), i.e. the engine's own throughput. +// Only the subset of the library the host program uses is implemented. +#include "Vbitacc_pcie_core.h" +#include "verilated.h" + +#include +#include +#include +#include +#include +#include + +extern "C" { +#include "liblitepcie.h" +} + +#ifndef MOCK_LANES +#define MOCK_LANES 4 +#endif +#ifndef MOCK_WPL +#define MOCK_WPL 64 +#endif +#ifndef MOCK_MATCH +#define MOCK_MATCH 1 +#endif +#ifndef MOCK_MATCH_LANES +#define MOCK_MATCH_LANES MOCK_LANES +#endif + +static Vbitacc_pcie_core *core; +static uint64_t cycles; +static uint64_t dma_cycles; // cycles spent in litepcie_dma_process +static uint64_t dma_in, dma_out; // descriptors in, result beats out + +static uint32_t host_word; +static uint64_t host_wdata; + +static void tick() +{ + core->clk = 0; core->eval(); + core->clk = 1; core->eval(); + cycles++; +} + +static void ensure_core() +{ + if (core) return; + core = new Vbitacc_pcie_core; + core->reset = 1; core->in_valid = 0; core->out_ready = 0; + core->host_write_stb = 0; core->host_read_stb = 0; + for (int i = 0; i < 4; i++) tick(); + core->reset = 0; + tick(); +} + +// ------------------------------------------------------------------ CSRs +extern "C" uint32_t litepcie_readl(int, uint32_t addr) +{ + ensure_core(); + tick(); + switch (addr) { + case CSR_BITACC_HOST_RDATA_ADDR: return (uint32_t)(core->host_rdata >> 32); + case CSR_BITACC_HOST_RDATA_ADDR + 4: return (uint32_t)core->host_rdata; + case CSR_BITACC_STATUS_ADDR: + return (core->idle << CSR_BITACC_STATUS_IDLE_OFFSET) + | (core->host_busy << CSR_BITACC_STATUS_HOST_BUSY_OFFSET) + | (core->host_rdata_valid << CSR_BITACC_STATUS_RDATA_VALID_OFFSET); + case CSR_BITACC_OPS_ACCEPTED_ADDR: return core->ops_accepted; + case CSR_BITACC_RESULTS_SENT_ADDR: return core->results_sent; + case CSR_BITACC_CONFIG_ADDR: + return (MOCK_LANES << CSR_BITACC_CONFIG_LANES_OFFSET) + | (MOCK_WPL << CSR_BITACC_CONFIG_WORDS_PER_LANE_OFFSET); + case CSR_BITACC_VERSION_ADDR: return 3; // FORMAT_VERSION in bitacc_litefury.py + case CSR_BITACC_FEATURES_ADDR: + return MOCK_MATCH << CSR_BITACC_FEATURES_MATCH_OFFSET + | (MOCK_MATCH ? MOCK_MATCH_LANES : 0) << CSR_BITACC_FEATURES_MATCH_LANES_OFFSET; + default: + fprintf(stderr, "mock: read of unmapped CSR 0x%x\n", addr); + return 0xdeadbeef; + } +} + +extern "C" void litepcie_writel(int, uint32_t addr, uint32_t val) +{ + ensure_core(); + switch (addr) { + case CSR_BITACC_HOST_WORD_ADDR: host_word = val; break; + case CSR_BITACC_HOST_WDATA_ADDR: host_wdata = (host_wdata & 0xffffffffull) | ((uint64_t)val << 32); break; + case CSR_BITACC_HOST_WDATA_ADDR + 4: host_wdata = (host_wdata & ~0xffffffffull) | val; break; + case CSR_BITACC_HOST_CTRL_ADDR: + // CSRStorage pulse fields: one-cycle strobes with the stored word/data. + core->host_word = host_word; + core->host_wdata = host_wdata; + core->host_write_stb = (val >> CSR_BITACC_HOST_CTRL_WRITE_OFFSET) & 1; + core->host_read_stb = (val >> CSR_BITACC_HOST_CTRL_READ_OFFSET) & 1; + tick(); + core->host_write_stb = 0; + core->host_read_stb = 0; + break; + default: + fprintf(stderr, "mock: write of unmapped CSR 0x%x\n", addr); + } + tick(); +} + +// ------------------------------------------------------------------ DMA +static std::deque> to_card; // filled write buffers +static std::vector pending_write; // buffer handed to the user +static bool write_handed; +static std::deque> to_host; // completed result buffers +static std::vector current_read; +static std::vector results; // partial result buffer +static size_t in_pos; // byte position in to_card.front() + +extern "C" int litepcie_dma_init(struct litepcie_dma_ctrl *dma, const char *, uint8_t) +{ + ensure_core(); + dma->fds.fd = 3; + return 0; +} + +extern "C" void litepcie_dma_cleanup(struct litepcie_dma_ctrl *) +{ + if (dma_cycles) + fprintf(stderr, "mock: DMA phases %llu cycles, %llu descriptors in, %llu result beats out, " + "%.3f cycles/descriptor\n", (unsigned long long)dma_cycles, + (unsigned long long)dma_in, (unsigned long long)dma_out, (double)dma_cycles / (double)dma_in); +} + +extern "C" void litepcie_dma_process(struct litepcie_dma_ctrl *dma) +{ + if (write_handed) { // the user filled the last write buffer + to_card.push_back(pending_write); + write_handed = false; + } + for (int c = 0; c < 20000; c++) { + // input: one 128-bit descriptor per beat, little-endian + core->in_valid = 0; + if (dma->reader_enable && !to_card.empty()) { + const unsigned char *d = (const unsigned char *)to_card.front().data() + in_pos; + for (int w = 0; w < 4; w++) + core->in_data[w] = d[4*w] | d[4*w+1] << 8 | d[4*w+2] << 16 | (uint32_t)d[4*w+3] << 24; + core->in_valid = 1; + } + core->out_ready = dma->writer_enable ? 1 : 0; + core->clk = 0; core->eval(); + bool in_fire = core->in_valid && core->in_ready; + bool out_fire = core->out_valid && core->out_ready; + unsigned char beat[16]; + if (out_fire) + for (int w = 0; w < 4; w++) + for (int b = 0; b < 4; b++) beat[4*w + b] = (core->out_data[w] >> (8*b)) & 0xff; + core->clk = 1; core->eval(); + cycles++; + dma_cycles++; + dma_in += in_fire; + dma_out += out_fire; + if (in_fire) { + in_pos += 16; + if (in_pos == DMA_BUFFER_SIZE) { to_card.pop_front(); in_pos = 0; dma->reader_sw_count++; } + } + if (out_fire) { + results.insert(results.end(), (char *)beat, (char *)beat + 16); + if (results.size() == DMA_BUFFER_SIZE) { to_host.push_back(results); results.clear(); } + } + if (to_card.empty() && !core->out_valid && core->idle) break; + } +} + +extern "C" char *litepcie_dma_next_write_buffer(struct litepcie_dma_ctrl *) +{ + if (write_handed) { to_card.push_back(pending_write); write_handed = false; } + if (to_card.size() >= 8) return nullptr; // bounded like the real ring + pending_write.assign(DMA_BUFFER_SIZE, 0); + write_handed = true; + return pending_write.data(); +} + +extern "C" char *litepcie_dma_next_read_buffer(struct litepcie_dma_ctrl *dma) +{ + if (to_host.empty()) return nullptr; + current_read = to_host.front(); + to_host.pop_front(); + dma->writer_sw_count++; + return current_read.data(); +} + +extern "C" int64_t get_time_ms(void) +{ + struct timeval tv; + gettimeofday(&tv, nullptr); + return (int64_t)tv.tv_sec * 1000 + tv.tv_usec / 1000; +} + +extern "C" int mock_cycles(void) { return (int)cycles; } diff --git a/pcie/litex/bitacc_litefury.py b/pcie/litex/bitacc_litefury.py new file mode 100644 index 0000000000000000000000000000000000000000..2bddd0ef4a577d12dd3746b5167ded8a570eb5d8 --- /dev/null +++ b/pcie/litex/bitacc_litefury.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +# +# LiteX SoC for the LiteFury (Xilinx Artix-7 XC7A100T, PCIe Gen2 x4, M.2 2280 Key-M). +# LiteX supports it as the SQRL Acorn CLE-101, its electrical equivalent +# (litex_boards/platforms/sqrl_acorn.py). +# +# The bit-operation engine (pcie/rtl) is attached to one LitePCIe DMA channel: +# DMA reader (host -> card) -> 128-bit descriptors -> engine +# engine -> 128-bit result beats (2 results of 64 bits) -> DMA writer (card -> host) +# Descriptor and result formats: pcie/rtl/bitacc_pcie_core.sv (format version 2, +# readable from the version CSR). +# Word access to the engine's bit store and status counters are CSRs on BAR0. +# +# Build (needs Vivado for the bitstream; the free edition covers the XC7A100T): +# ./bitacc_litefury.py --build # gateware + bitstream +# ./bitacc_litefury.py --driver # Linux driver + liblitepcie in build/.../driver +# Generate sources only (no Vivado): +# ./bitacc_litefury.py --no-compile +# +# The same SoC serves the M.2 slot of the NUC directly or a USB4/Thunderbolt +# enclosure that tunnels PCIe; nothing here depends on the transport. + +import os + +from migen import * + +from litex.gen import * +from litex.soc.interconnect.csr import * +from litex.soc.integration.builder import Builder +from litex.build.generic_platform import Pins + +from litex_boards.targets.sqrl_acorn import BaseSoC + +FORMAT_VERSION = 3 + +RTL = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "rtl") + + +class BitAcc(LiteXModule): + """bitacc_pcie_core with CSR access; connect dma_source/dma_sink to a LitePCIe DMA.""" + + def __init__(self, platform, dma, lanes=8, words_per_lane=2048, with_match=True, match_lanes=2): + word_bits = (lanes * words_per_lane - 1).bit_length() + + # Host word access. + self.host_word = CSRStorage(word_bits, description="Bit-store word index for host access.") + self.host_wdata = CSRStorage(64, description="Word to write.") + self.host_ctrl = CSRStorage(fields=[ + CSRField("write", size=1, offset=0, pulse=True, description="Write host_wdata to host_word."), + CSRField("read", size=1, offset=1, pulse=True, description="Read host_word into host_rdata."), + ]) + self.host_rdata = CSRStatus(64, description="Word read by the last read.") + self.status = CSRStatus(fields=[ + CSRField("idle", size=1, offset=0, description="No operation in flight, no result buffered."), + CSRField("host_busy", size=1, offset=1, description="A host word access is pending."), + CSRField("rdata_valid", size=1, offset=2, description="host_rdata holds the last read."), + ]) + self.ops_accepted = CSRStatus(32, description="Descriptors accepted since reset.") + self.results_sent = CSRStatus(32, description="Results sent since reset (multiple of 2).") + self.config = CSRStatus(fields=[ + CSRField("lanes", size=8, offset=0), + CSRField("words_per_lane", size=24, offset=8), + ]) + self.version = CSRStatus(8, reset=FORMAT_VERSION, + description="Descriptor/result format version (see bitacc_pcie_core.sv).") + self.features = CSRStatus(fields=[ + CSRField("match", size=1, offset=0, reset=int(with_match), description="MATCH unit present."), + CSRField("match_lanes", size=8, offset=8, reset=match_lanes if with_match else 0, + description="MATCH units (64 positions per cycle each)."), + ]) + self.comb += [ + self.config.fields.lanes.eq(lanes), + self.config.fields.words_per_lane.eq(words_per_lane), + ] + + self.specials += Instance("bitacc_pcie_core", + p_LANES = lanes, + p_WORDS_PER_LANE = words_per_lane, + p_WITH_MATCH = int(with_match), + p_MATCH_LANES = match_lanes, + i_clk = ClockSignal("sys"), + i_reset = ResetSignal("sys"), + # host -> card + i_in_valid = dma.source.valid, + o_in_ready = dma.source.ready, + i_in_data = dma.source.data, + # card -> host + o_out_valid = dma.sink.valid, + i_out_ready = dma.sink.ready, + o_out_data = dma.sink.data, + # registers + i_host_word = self.host_word.storage, + i_host_wdata = self.host_wdata.storage, + i_host_write_stb = self.host_ctrl.fields.write, + i_host_read_stb = self.host_ctrl.fields.read, + o_host_rdata = self.host_rdata.status, + o_host_rdata_valid = self.status.fields.rdata_valid, + o_host_busy = self.status.fields.host_busy, + o_idle = self.status.fields.idle, + o_ops_accepted = self.ops_accepted.status, + o_results_sent = self.results_sent.status, + ) + self.comb += dma.sink.last.eq(0) + for f in ("bitacc_engine.sv", "bitacc_pcie_core.sv"): + platform.add_source(os.path.join(RTL, f)) + + +class BitAccSoC(BaseSoC): + def __init__(self, lanes=8, words_per_lane=2048, with_match=False, match_lanes=2, **kwargs): + BaseSoC.__init__(self, + variant = "cle-101", # LiteFury-equivalent XC7A100T + with_pcie = True, + pcie_ndmas = 1, + **kwargs) + self.bitacc = BitAcc(self.platform, self.pcie_dma0, + lanes=lanes, words_per_lane=words_per_lane, with_match=with_match, match_lanes=match_lanes) + + +def main(): + from litex.build.parser import LiteXArgumentParser + from litex_boards.platforms import sqrl_acorn + parser = LiteXArgumentParser(platform=sqrl_acorn.Platform, description="Bit accelerator on LiteFury.") + parser.add_target_argument("--sys-clk-freq", default=125e6, type=float, help="System clock frequency.") + parser.add_target_argument("--lanes", default=8, type=int, help="Engine lanes (power of two).") + parser.add_target_argument("--words-per-lane", default=2048, type=int, help="64-bit words per lane (power of two).") + # MATCH at 8 lanes uses ~65% of the XC7A100T's LUTs and did not route in the + # open-source flow (pcie/README.md, Resources), so it is opt-in. + parser.add_target_argument("--match", action="store_true", help="Include the MATCH unit.") + parser.add_target_argument("--match-lanes", default=2, type=int, help="MATCH units (power of two, <= lanes; ~4,000 LUTs each).") + parser.add_target_argument("--driver", action="store_true", help="Generate the PCIe driver.") + # The host drives the engine over PCIe; no soft CPU or UART is needed. + parser.set_defaults(cpu_type="None", no_uart=True) + args = parser.parse_args() + + soc = BitAccSoC( + sys_clk_freq = args.sys_clk_freq, + lanes = args.lanes, + words_per_lane = args.words_per_lane, + with_match = args.match, + match_lanes = args.match_lanes, + **parser.soc_argdict) + builder = Builder(soc, **parser.builder_argdict) + if args.build: + builder.build(**parser.toolchain_argdict) + else: + builder.build(run=False) + if args.driver: + from litepcie.software import generate_litepcie_software + generate_litepcie_software(soc, os.path.join(builder.output_dir, "driver")) + + +if __name__ == "__main__": + main() diff --git a/pcie/rtl/bitacc_engine.sv b/pcie/rtl/bitacc_engine.sv new file mode 100644 index 0000000000000000000000000000000000000000..82d3359e9d2c08844989256a358c5f6fe3d9dedc --- /dev/null +++ b/pcie/rtl/bitacc_engine.sv @@ -0,0 +1,870 @@ +// Parallel bit-operation engine. +// +// A stream of operation descriptors is executed by LANES lanes, each owning +// one bank of the bit store. Word w lives in lane w % LANES at local address +// w / LANES. Results leave in descriptor order through a reorder buffer, and +// equal those of executing the stream one operation at a time (checked against +// the reference core rtl/bit_accelerator.sv by pcie/tb/tb_engine.sv). +// +// Single-bit operations (op 0-4, as in the reference core): +// effective bit a = (base << 3) + offset (mod 2^64) +// word = a >> 6, bit index = a[5:0] +// error = word >= WORDS (no write) +// 0 GET, 1 TEST: bit; 2 SET, 3 CLEAR, 4 TOGGLE: write, return the new bit +// All operations on one word run in one lane, in arrival order. Operations on +// different words commute, so lanes can complete out of order. +// +// Range operations (op 8-15). Each is a barrier: it starts when every earlier +// operation has completed, runs on all lanes at once, and later operations +// start after it. The bit store is therefore always in the state that +// sequential execution gives. +// bit ranges, bits [a, a + len): +// 8 COUNT value = number of set bits +// 9 FIND1 value = index of the first set bit; bit = found +// 10 FIND0 value = index of the first clear bit; bit = found +// 11 SETR, 12 CLEARR, 13 FLIPR: set/clear/invert every bit; +// value = number of set bits before the operation +// 15 MATCH pattern = word src, mask = word src+1. Position s in [a, a+len) +// matches if every mask bit j has s + j < a + len and +// bit(s + j) == pattern[j]. fn 0: value = number of matches; +// fn 1: value = first matching position, bit = found. +// Needs WITH_MATCH = 1; otherwise opcode 15 is an error. +// error = a + len > WORDS * 64; MATCH also: fn > 1 || src + 2 > WORDS +// word regions, words dst = a .. a+len-1 and src = src .. src+len-1: +// 14 BULK dst[k] = fn(dst[k], src[k]) for every k, unless dry; +// fn 0 COPY, 1 AND, 2 OR, 3 XOR, 4 ANDN (dst & ~src); +// value = number of set bits in the results +// error = fn > 4 || dst + len > WORDS || src + len > WORDS +// || the regions overlap without being identical (no write) +// For range operations without a find result, bit = (value != 0). len = 0 +// is valid (value 0, not found). Undefined opcodes (5-7) are errors. An +// error result has bit = 0 and value = 0. +// +// A range operation processes LANES words per step. Bit ranges step through +// groups of LANES consecutive words (one per lane, same local address). BULK +// pairs dst[k] with src[k], which may live in different lanes: lane i reads +// src element k = j*LANES + ((i - src) mod LANES) at local address +// j + (src + ((i - src) mod LANES)) / LANES, and dst lane m receives it from +// lane (m - delta) mod LANES, delta = (dst - src) mod LANES. formal/lane_map.mlw +// proves these index identities. Each BULK step is a src read, a dst read and +// a write: LANES words per 2 cycles. MATCH steps like a bit range; lane i +// checks the 64 positions of the word before its own (word w - 1), with the +// window {word w, word w - 1}; lane 0 takes word w - 1 from the last lane of +// the previous step. With MATCH_LANES < LANES, each group of LANES words is +// matched in LANES / MATCH_LANES sub-steps (the group is read again in each; +// MATCH does not write): unit u takes word sub * MATCH_LANES + u. That is +// MATCH_LANES * 64 positions per cycle. +// +// Timing: every block-RAM port is driven by a register (through the port +// mux), the descriptor's 64-bit sums are registered before they are compared, +// and popcount, lowest-set-bit and the cross-lane sums are balanced trees. +// Range pipeline: S0 issue -> S1 address register (bank read) -> S2 data +// (rdata, masks registered) -> S3 compute from registered data (write and +// result word registered) -> S4 write, popcount -> S5 reduce. +// +// The host port reads and writes whole words while the engine is idle +// (host_ready is low while any operation is in flight). +module bitacc_engine #( + parameter int LANES = 4, // power of two, >= 2 + parameter int WORDS_PER_LANE = 512, // power of two, >= 2 + parameter int ROB_DEPTH = 32, // power of two; max operations in flight + parameter int FIFO_DEPTH = 16, // per-lane queue; power of two + parameter bit WITH_MATCH = 1'b1, // MATCH unit + parameter int MATCH_LANES = LANES // matcher units (power of two, 1 .. LANES; ~4,000 LUTs each on XC7) +)( + input logic clk, + input logic reset, + + // descriptors in + input logic cmd_valid, + output logic cmd_ready, + input logic [3:0] cmd_op, + input logic [63:0] cmd_base, + input logic [63:0] cmd_offset, + input logic [31:0] cmd_len, // range length: bits, or words for BULK + input logic [23:0] cmd_src, // BULK source word, MATCH pattern word + input logic [2:0] cmd_fn, // BULK function, MATCH result kind + input logic cmd_dry, // BULK: count only, no write + + // results out, in descriptor order + output logic res_valid, + input logic res_ready, + output logic res_error, + output logic res_bit, + output logic [55:0] res_value, + + // host word access (only while idle) + input logic host_valid, + output logic host_ready, + input logic host_write, + input logic [$clog2(LANES*WORDS_PER_LANE)-1:0] host_word, + input logic [63:0] host_wdata, + output logic host_rvalid, + output logic [63:0] host_rdata, + + output logic idle +); + localparam int LW = $clog2(LANES); + localparam int AW = $clog2(WORDS_PER_LANE); + localparam int GW = AW + 1; // step counter (MATCH reads one step past the end) + localparam int WW = LW + AW; // global word index width + localparam int BW = WW + 6; // global bit index width + localparam int CW = BW + 1; // bit count width (0 .. WORDS*64) + localparam int SW = $clog2(ROB_DEPTH); + localparam int FW = $clog2(FIFO_DEPTH); + localparam int MK = LANES / MATCH_LANES; // MATCH sub-steps per group + localparam int SBW = MK > 1 ? $clog2(MK) : 1; + + localparam logic [3:0] OP_SET = 4'd2, OP_CLEAR = 4'd3, OP_TOGGLE = 4'd4; + localparam logic [3:0] OP_COUNT = 4'd8, OP_FIND1 = 4'd9, OP_FIND0 = 4'd10, + OP_SETR = 4'd11, OP_CLEARR = 4'd12, OP_FLIPR = 4'd13, + OP_BULK = 4'd14, OP_MATCH = 4'd15; + localparam logic [2:0] FN_COPY = 3'd0, FN_AND = 3'd1, FN_OR = 3'd2, FN_XOR = 3'd3, FN_ANDN = 3'd4; + + localparam logic [64:0] STORE_WORDS = 65'(LANES * WORDS_PER_LANE); + localparam logic [64:0] STORE_BITS = STORE_WORDS << 6; + + // Balanced trees (a loop that accumulates bit by bit synthesizes a 64-deep chain). + function automatic logic [6:0] popcount64(input logic [63:0] x); + logic [6:0] s [64]; + for (int i = 0; i < 64; i++) s[i] = 7'(x[i]); + for (int w = 32; w >= 1; w = w / 2) + for (int i = 0; i < w; i++) s[i] = s[2*i] + s[2*i+1]; + popcount64 = s[0]; + endfunction + + function automatic logic [5:0] ctz64(input logic [63:0] x); // index of the lowest set bit + logic [7:0] any; + logic [2:0] pos [8]; + logic [2:0] grp; + for (int k = 0; k < 8; k++) begin + any[k] = |x[8*k +: 8]; + pos[k] = '0; + for (int b = 7; b >= 0; b--) if (x[8*k + b]) pos[k] = 3'(b); + end + grp = '0; + for (int k = 7; k >= 0; k--) if (any[k]) grp = 3'(k); + ctz64 = {grp, pos[grp]}; + endfunction + + function automatic logic [5:0] msb64(input logic [63:0] x); // index of the highest set bit (0 if none) + logic [7:0] any; + logic [2:0] pos [8]; + logic [2:0] grp; + for (int k = 0; k < 8; k++) begin + any[k] = |x[8*k +: 8]; + pos[k] = '0; + for (int b = 0; b < 8; b++) if (x[8*k + b]) pos[k] = 3'(b); + end + grp = '0; + for (int k = 0; k < 8; k++) if (any[k]) grp = 3'(k); + msb64 = {grp, pos[grp]}; + endfunction + + // ---------------------------------------------------------------- stage P: accept + // A 2-entry skid buffer: cmd_ready is a register (computed from next-state + // counts), so no combinational path runs from the dispatch logic back to the + // descriptor source. The 64-bit sums are registered here and compared in D. + logic p_valid, p_take; + logic [1:0] p_cnt; // entries held (0..2); entry 0 is the head + logic p_rdy; + logic [SW-1:0] pe_seq [2]; + logic [3:0] pe_op [2]; + logic [2:0] pe_fn [2]; + logic pe_dry [2]; + logic [63:0] pe_eff [2]; + logic [31:0] pe_len [2]; + logic [23:0] pe_src [2]; + logic [64:0] pe_end [2]; // a + len: bit range end, or dst region end + logic [32:0] pe_send [2]; // src + len: src region end + + logic [SW:0] inflight; // descriptors accepted and not yet emitted + logic [SW:0] inflight_next; + logic [SW-1:0] next_seq; + logic d_valid, d_take; + logic accept, emit; + logic [1:0] p_cnt_next; + logic p_wp, p_rp; // write and read slots of the circular 2-entry buffer + + logic [63:0] c_eff; // effective bit address, mod 2^64 + assign c_eff = (cmd_base << 3) + cmd_offset; + assign cmd_ready = p_rdy; + assign accept = cmd_valid && p_rdy; + assign p_valid = p_cnt != 2'd0; + assign p_take = p_valid && (!d_valid || d_take); + assign p_cnt_next = p_cnt + 2'(accept) - 2'(p_take); + // inflight counts the descriptors held in stages P and D too. + assign inflight_next = inflight + (SW+1)'(accept) - (SW+1)'(emit); + + // the head entry, as stage D sees it (taking it only moves p_rp, so the + // dispatch decision drives no wide enable here) + logic [SW-1:0] p_seq; + logic [3:0] p_op; + logic [2:0] p_fn; + logic p_dry; + logic [63:0] p_eff; + logic [31:0] p_len; + logic [23:0] p_src; + logic [64:0] p_end; + logic [32:0] p_send; + assign p_seq = pe_seq[p_rp]; assign p_op = pe_op[p_rp]; assign p_fn = pe_fn[p_rp]; assign p_dry = pe_dry[p_rp]; + assign p_eff = pe_eff[p_rp]; assign p_len = pe_len[p_rp]; assign p_src = pe_src[p_rp]; + assign p_end = pe_end[p_rp]; assign p_send = pe_send[p_rp]; + + always_ff @(posedge clk) begin + if (reset) begin + p_cnt <= '0; p_rdy <= 1'b0; next_seq <= '0; p_wp <= 1'b0; p_rp <= 1'b0; + for (int i = 0; i < 2; i++) begin + pe_seq[i] <= '0; pe_op[i] <= '0; pe_fn[i] <= '0; pe_dry[i] <= 1'b0; pe_eff[i] <= '0; + pe_len[i] <= '0; pe_src[i] <= '0; pe_end[i] <= '0; pe_send[i] <= '0; + end + end else begin + p_cnt <= p_cnt_next; + p_rdy <= p_cnt_next < 2'd2 && inflight_next < (SW+1)'(ROB_DEPTH); + if (p_take) p_rp <= !p_rp; + if (accept) begin + pe_seq[p_wp] <= next_seq; + pe_op[p_wp] <= cmd_op; + pe_fn[p_wp] <= cmd_fn; + pe_dry[p_wp] <= cmd_dry; + pe_eff[p_wp] <= c_eff; + pe_len[p_wp] <= cmd_len; + pe_src[p_wp] <= cmd_src; + pe_end[p_wp] <= {1'b0, c_eff} + 65'(cmd_len); + pe_send[p_wp] <= 33'(cmd_src) + 33'(cmd_len); + p_wp <= !p_wp; + next_seq <= next_seq + 1'b1; + end + end + end + + // ---------------------------------------------------------------- stage D: decode + logic [64:0] a_x, src_x, send_x; + logic c_range, c_err; + assign a_x = {1'b0, p_eff}; + assign src_x = 65'(p_src); + assign send_x = 65'(p_send); + assign c_range = p_op >= OP_COUNT; + always_comb begin + if (p_op <= OP_TOGGLE) c_err = (p_eff >> (WW + 6)) != '0; + else if (p_op < OP_COUNT) c_err = 1'b1; // 5-7 + else if (p_op < OP_BULK) c_err = p_end > STORE_BITS; + else if (p_op == OP_BULK) c_err = p_fn > FN_ANDN || p_end > STORE_WORDS || send_x > STORE_WORDS + || (p_len != '0 && a_x != src_x && a_x < send_x && src_x < p_end); + else c_err = !WITH_MATCH || p_fn > 3'd1 || p_end > STORE_BITS + || src_x + 65'd2 > STORE_WORDS; + end + + logic [SW-1:0] d_seq; + logic d_err, d_range; + logic [3:0] d_op; + logic [LW-1:0] d_lane; + logic [AW-1:0] d_local; + logic [5:0] d_bit; + logic [2:0] d_fn; + logic d_dry, d_empty; + logic [BW-1:0] d_lo, d_hi; // first and last bit (bit ranges); d_lo[WW-1:0] = dst (BULK) + logic [WW-1:0] d_src; + logic [WW:0] d_n; // BULK word count (<= WORDS when valid) + + always_ff @(posedge clk) begin + if (reset) begin + d_valid <= 1'b0; + d_seq <= '0; d_err <= 1'b0; d_range <= 1'b0; d_op <= '0; d_lane <= '0; d_local <= '0; d_bit <= '0; + d_fn <= '0; d_dry <= 1'b0; d_empty <= 1'b0; d_lo <= '0; d_hi <= '0; d_src <= '0; d_n <= '0; + end else begin + if (d_take) d_valid <= 1'b0; + if (p_take) begin + d_valid <= 1'b1; + d_seq <= p_seq; + d_err <= c_err; + d_range <= c_range; + d_op <= p_op; + d_lane <= p_eff[6 +: LW]; + d_local <= p_eff[6 + LW +: AW]; + d_bit <= p_eff[5:0]; + d_fn <= p_fn; + d_dry <= p_dry; + d_empty <= p_len == '0; + d_lo <= p_eff[BW-1:0]; + d_hi <= BW'(p_end - 65'd1); + d_src <= src_x[WW-1:0]; + d_n <= (WW+1)'(p_len); + end + end + end + + // ---------------------------------------------------------------- dispatch + logic [LANES-1:0] q_full, q_empty; + logic [LANES-1:0] q_push, q_pop; + logic [SW-1:0] q_seq [LANES]; + logic [AW-1:0] q_local [LANES]; + logic [5:0] q_bit [LANES]; + logic [2:0] q_op [LANES]; + logic [LANES-1:0] l_busy; + logic r_busy, r_start; + logic [SW:0] ops_out; // single-bit operations dispatched and not completed + logic [LANES-1:0] l_done; + + // Errors bypass the lanes and go straight to the reorder buffer. A range + // operation waits until every dispatched single-bit operation has completed + // (ops_out is a register; it reaches 0 the cycle after the last completion + // pulse, after that operation's write); nothing dispatches while it runs. + assign r_start = d_valid && !d_err && d_range && !r_busy && ops_out == '0; + assign d_take = d_valid && (d_err || r_start || (!d_range && !r_busy && !q_full[d_lane])); + always_comb begin + q_push = '0; + if (d_valid && !d_err && !d_range && !r_busy && !q_full[d_lane]) q_push[d_lane] = 1'b1; + end + + always_ff @(posedge clk) begin + if (reset) ops_out <= '0; + else begin + logic [SW:0] done_n; + done_n = '0; + for (int i = 0; i < LANES; i++) done_n = done_n + (SW+1)'(l_done[i]); + ops_out <= ops_out + (SW+1)'(|q_push) - done_n; + end + end + + // ---------------------------------------------------------------- range unit: control + // R_IDLE -> R_INIT (derived constants from registered parameters) -> R_ISSUE + // -> R_DRAIN -> R_IDLE; MATCH loads its pattern first: + // R_INIT -> R_PAT -> R_PWAIT -> R_PSET1 -> R_PSET2 -> R_PSET3 -> R_ISSUE. + typedef enum logic [3:0] {R_IDLE, R_INIT, R_PAT, R_PWAIT, R_PSET1, R_PSET2, R_PSET3, + R_ISSUE, R_DRAIN} rstate_t; + localparam logic [2:0] K_BITS = 3'd0, K_SRC = 3'd1, K_DST = 3'd2, K_PAT = 3'd3, K_MAT = 3'd4; + + rstate_t r_state; + logic [SW-1:0] r_seq; + logic [3:0] r_op; + logic [2:0] r_fn; + logic r_dry, r_empty; + logic [BW-1:0] r_lo, r_hi; // first and last bit; MATCH: first and last position + logic [WW-1:0] r_src; + logic [WW:0] r_n; + logic [GW-1:0] r_g, r_glast; // step: group (bit ranges, MATCH) or j (BULK) + logic [SBW-1:0] r_sub; // MATCH sub-step within the group + logic r_phase; // BULK: 0 src read, 1 dst read + logic [63:0] r_lowm, r_highm; // bit masks of the first and last word of the range + logic [CW-1:0] r_acc; + logic r_found; + logic [BW-1:0] r_idx; + logic r_done; + logic r_find, r_bulk, r_match; + logic [63:0] r_pat, r_msk; // MATCH pattern and mask + logic [LW-1:0] r_pat_lane, r_msk_lane; + logic [5:0] m_top; // MATCH: highest mask bit (0 if the mask is 0) + logic m_neg; // MATCH: last position below 0 + + assign r_busy = r_state != R_IDLE; + assign r_bulk = r_op == OP_BULK; + assign r_match = r_op == OP_MATCH; + assign r_find = r_op == OP_FIND1 || r_op == OP_FIND0 || (r_match && r_fn[0]); + + // S0 issue: one read per lane per cycle + logic s0_valid; + logic [2:0] s0_kind; + assign s0_valid = r_state == R_PAT || (r_state == R_ISSUE && !(r_find && r_found)); + always_comb begin + if (r_state == R_PAT) s0_kind = K_PAT; + else if (r_bulk) s0_kind = r_phase ? K_DST : K_SRC; + else if (r_match) s0_kind = K_MAT; + else s0_kind = K_BITS; + end + + // central pipeline registers + logic s1_valid, s2_valid, s3_valid, s4_valid, s5_valid, s6_valid; + logic [2:0] s1_kind, s2_kind, s3_kind; + logic [GW-1:0] s1_g, s2_g, s3_g, s4_g, s5_g; + logic [SBW-1:0] s1_sub, s2_sub, s3_sub, s4_sub, s5_sub; + logic [6:0] s5_cnt [LANES]; + logic [LANES-1:0] s5_hit; + logic [5:0] s5_pos [LANES]; + + // S5 reduce across lanes, registered into S6 + logic [LW+6:0] s5_sum, s6_sum; // set bits (or matches) in this step, all lanes + logic s5_any, s6_any; + logic [LW-1:0] s5_first; // lowest lane with a hit + logic [WW:0] s5_word; // word of that lane's result bits + logic [BW-1:0] s6_idx; // first hit of this step + always_comb begin + logic [LW+6:0] t [LANES]; + for (int i = 0; i < LANES; i++) t[i] = (LW+7)'(s5_cnt[i]); + for (int lvl = 0; lvl < LW; lvl++) + for (int i = 0; i < LANES / 2; i++) + if (i < (LANES >> (lvl + 1))) t[i] = t[2*i] + t[2*i+1]; + s5_sum = t[0]; + s5_any = 1'b0; s5_first = '0; + for (int i = LANES - 1; i >= 0; i--) if (s5_hit[i]) begin s5_any = 1'b1; s5_first = LW'(i); end + // MATCH results of unit i belong to word g * LANES + sub * MATCH_LANES + i - 1 + if (r_match) + s5_word = (WW+1)'({s5_g, LW'(0)}) + (WW+1)'(s5_sub) * (WW+1)'(MATCH_LANES) + + (WW+1)'(s5_first) - (WW+1)'(1); + else + s5_word = {s5_g, s5_first}; + end + + always_ff @(posedge clk) begin + r_done <= 1'b0; + if (reset) begin + r_state <= R_IDLE; r_seq <= '0; r_op <= '0; r_fn <= '0; r_dry <= 1'b0; r_empty <= 1'b0; + r_lo <= '0; r_hi <= '0; r_src <= '0; r_n <= '0; r_g <= '0; r_glast <= '0; + r_phase <= 1'b0; r_lowm <= '0; r_highm <= '0; r_acc <= '0; r_found <= 1'b0; r_idx <= '0; + r_sub <= '0; + s1_sub <= '0; s2_sub <= '0; s3_sub <= '0; s4_sub <= '0; s5_sub <= '0; + r_pat_lane <= '0; r_msk_lane <= '0; m_top <= '0; m_neg <= 1'b0; + s1_valid <= 1'b0; s1_kind <= K_BITS; s1_g <= '0; + s2_valid <= 1'b0; s2_kind <= K_BITS; s2_g <= '0; + s3_valid <= 1'b0; s3_kind <= K_BITS; s3_g <= '0; + s4_valid <= 1'b0; s4_g <= '0; s5_valid <= 1'b0; s5_g <= '0; + s6_valid <= 1'b0; s6_sum <= '0; s6_any <= 1'b0; s6_idx <= '0; + end else begin + case (r_state) + R_IDLE: begin + // follow the decode register while idle, so starting needs no wide enable + r_seq <= d_seq; r_op <= d_op; r_fn <= d_fn; r_dry <= d_dry; r_empty <= d_empty; + r_lo <= d_lo; r_hi <= d_hi; r_src <= d_src; r_n <= d_n; + if (r_start) r_state <= R_INIT; + end + R_INIT: begin + r_g <= r_bulk ? '0 : GW'(r_lo[BW-1:6+LW]); + r_glast <= r_bulk ? GW'((r_n - 1'b1) >> LW) : GW'(r_hi[BW-1:6+LW]); + r_lowm <= ~64'd0 << r_lo[5:0]; + r_highm <= ~64'd0 >> (6'd63 - r_hi[5:0]); + r_pat_lane <= r_src[LW-1:0]; + r_msk_lane <= r_src[LW-1:0] + 1'b1; + r_phase <= 1'b0; r_sub <= '0; + r_acc <= '0; r_found <= 1'b0; r_idx <= '0; + r_state <= r_empty ? R_DRAIN : (r_match ? R_PAT : R_ISSUE); + end + R_PAT: r_state <= R_PWAIT; + R_PWAIT: if (!s1_valid && !s2_valid && !s3_valid) r_state <= R_PSET1; // pattern captured + R_PSET1: begin + m_top <= r_msk == '0 ? 6'd0 : msb64(r_msk); + r_state <= R_PSET2; + end + R_PSET2: begin + // positions [r_lo, r_hi - m_top] + {m_neg, r_hi} <= {1'b0, r_hi} - (BW+1)'(m_top); + r_state <= R_PSET3; + end + R_PSET3: begin + // steps from the group of word r_lo to the group of word + // (last position / 64) + 1, whose lane 0 computes the last word + r_highm <= ~64'd0 >> (6'd63 - r_hi[5:0]); + r_glast <= GW'(((WW+1)'(r_hi[BW-1:6]) + 1'b1) >> LW); + r_state <= (m_neg || r_hi < r_lo) ? R_DRAIN : R_ISSUE; + end + R_ISSUE: begin + if (r_bulk) begin + r_phase <= !r_phase; + if (r_phase) begin + if (r_g == r_glast) r_state <= R_DRAIN; + r_g <= r_g + 1'b1; + end + end else if (r_match && MK > 1) begin + if (r_sub == SBW'(MK - 1)) begin + if (r_g == r_glast || (r_find && r_found)) r_state <= R_DRAIN; + r_g <= r_g + 1'b1; + r_sub <= '0; + end else begin + if (r_find && r_found) r_state <= R_DRAIN; + r_sub <= r_sub + 1'b1; + end + end else begin + if (r_g == r_glast || (r_find && r_found)) r_state <= R_DRAIN; + r_g <= r_g + 1'b1; + end + end + R_DRAIN: if (!s1_valid && !s2_valid && !s3_valid && !s4_valid && !s5_valid && !s6_valid) begin + r_state <= R_IDLE; + r_done <= 1'b1; + end + default: r_state <= R_IDLE; + endcase + + s1_valid <= s0_valid; s1_kind <= s0_kind; s1_g <= r_g; s1_sub <= r_sub; + s2_valid <= s1_valid; s2_kind <= s1_kind; s2_g <= s1_g; s2_sub <= s1_sub; + s3_valid <= s2_valid; s3_kind <= s2_kind; s3_g <= s2_g; s3_sub <= s2_sub; + s4_sub <= s3_sub; s5_sub <= s4_sub; + s4_valid <= s3_valid && (s3_kind == K_BITS || s3_kind == K_DST || s3_kind == K_MAT); s4_g <= s3_g; + s5_valid <= s4_valid; s5_g <= s4_g; + s6_valid <= s5_valid; + s6_sum <= s5_sum; + s6_any <= s5_any; + s6_idx <= {s5_word[WW-1:0], s5_pos[s5_first]}; + + if (s6_valid) begin + r_acc <= r_acc + CW'(s6_sum); + if (r_find && !r_found && s6_any) begin + r_found <= 1'b1; + r_idx <= s6_idx; + end + end + end + end + + // ---------------------------------------------------------------- lanes + logic [SW-1:0] l_seq [LANES]; + logic l_bit [LANES]; + logic [63:0] rq [LANES]; // S3: registered bank data + logic [63:0] lane_rdata [LANES]; + + // host access: global word -> lane, local address + logic [LW-1:0] h_lane; + logic [AW-1:0] h_local; + assign h_lane = host_word[LW-1:0]; + assign h_local = host_word[WW-1:LW]; + logic h_pending; + logic [LW-1:0] h_lane_q; + + // MATCH pattern capture (S3 of the K_PAT step) + always_ff @(posedge clk) begin + if (reset) begin + r_pat <= '0; r_msk <= '0; + end else begin + if (WITH_MATCH && s3_valid && s3_kind == K_PAT) begin + r_pat <= rq[r_pat_lane]; + r_msk <= rq[r_msk_lane]; + end + end + end + + genvar g; + generate + for (g = 0; g < LANES; g++) begin : lane + // queue + logic [SW+AW+6+3-1:0] fifo [FIFO_DEPTH]; + logic [FW:0] wp, rp; + logic full_q; // registered from the next-state pointers + assign q_full[g] = full_q; + assign q_empty[g] = (wp == rp); + always_ff @(posedge clk) begin + if (reset) begin + wp <= '0; rp <= '0; full_q <= 1'b0; + end else begin + full_q <= ((wp + (FW+1)'(q_push[g])) - (rp + (FW+1)'(q_pop[g]))) == (FW+1)'(FIFO_DEPTH); + if (q_push[g]) begin + fifo[wp[FW-1:0]] <= {d_seq, d_local, d_bit, d_op[2:0]}; + wp <= wp + 1'b1; + end + if (q_pop[g]) rp <= rp + 1'b1; + end + end + assign {q_seq[g], q_local[g], q_bit[g], q_op[g]} = fifo[rp[FW-1:0]]; + + // bank: one write port, one synchronous read port (block RAM) + logic [63:0] bank [WORDS_PER_LANE]; + logic we; + logic [AW-1:0] raddr, waddr; + logic [63:0] wdata, rdata; + always_ff @(posedge clk) begin + if (we) bank[waddr] <= wdata; + rdata <= bank[raddr]; + end + + // ---------------- single-bit execute: READ (address presented) -> MODIFY (data back, write) + logic st_mod; + logic [SW-1:0] x_seq; + logic [AW-1:0] x_local; + logic [5:0] x_bit; + logic [2:0] x_op; + logic [63:0] onehot, newword; + logic oldbit, newbit, writes; + + assign onehot = 64'd1 << x_bit; + assign oldbit = rdata[x_bit]; + always_comb begin + case ({1'b0, x_op}) + OP_SET: begin newword = rdata | onehot; newbit = 1'b1; writes = 1'b1; end + OP_CLEAR: begin newword = rdata & ~onehot; newbit = 1'b0; writes = 1'b1; end + OP_TOGGLE: begin newword = rdata ^ onehot; newbit = !oldbit; writes = 1'b1; end + default: begin newword = rdata; newbit = oldbit; writes = 1'b0; end + endcase + end + + assign q_pop[g] = !q_empty[g] && !st_mod; + assign l_busy[g] = st_mod || !q_empty[g]; + + always_ff @(posedge clk) begin + l_done[g] <= 1'b0; + if (reset) begin + st_mod <= 1'b0; + x_seq <= '0; x_local <= '0; x_bit <= '0; x_op <= '0; + l_seq[g] <= '0; l_bit[g] <= 1'b0; + end else if (st_mod) begin + st_mod <= 1'b0; + l_done[g] <= 1'b1; + l_seq[g] <= x_seq; + l_bit[g] <= newbit; + end else if (q_pop[g]) begin + st_mod <= 1'b1; + x_seq <= q_seq[g]; + x_local <= q_local[g]; + x_bit <= q_bit[g]; + x_op <= q_op[g]; + end + end + + // ---------------- range datapath + // Per-operation constants (see the header): BULK dst element offset q + // and the local base addresses of the src and dst regions; MATCH: the + // local address of this lane's pattern-or-mask word. + logic [LW-1:0] q_off, src_sel; + logic [AW-1:0] sa, da, pa; + always_ff @(posedge clk) begin + if (reset) begin + q_off <= '0; src_sel <= '0; sa <= '0; da <= '0; pa <= '0; + end else if (r_state == R_INIT) begin + q_off <= LW'(g) - r_lo[LW-1:0]; + // the src lane this dst lane takes its word from: (g - delta) mod LANES + src_sel <= LW'(LW'(g) - LW'(r_lo[LW-1:0] - r_src[LW-1:0])); + sa <= AW'(((WW+1)'(r_src) + (WW+1)'(LW'(LW'(g) - r_src[LW-1:0]))) >> LW); + da <= AW'(((WW+1)'(r_lo[WW-1:0]) + (WW+1)'(LW'(LW'(g) - r_lo[LW-1:0]))) >> LW); + // word src (pattern) or src + 1 (mask), whichever lives in this lane + pa <= AW'(((WW+1)'(r_src) + (WW+1)'(LW'(g) != r_src[LW-1:0])) >> LW); + end + end + + // S1: registered read address + logic [AW-1:0] s1_raddr; + always_ff @(posedge clk) begin + if (reset) s1_raddr <= '0; + else case (s0_kind) + K_SRC: s1_raddr <= r_g[AW-1:0] + sa; + K_DST: s1_raddr <= r_g[AW-1:0] + da; + K_PAT: s1_raddr <= pa; + default: s1_raddr <= r_g[AW-1:0]; + endcase + end + + // S1 -> S2: where this lane's word lies in the range, from s1_g; + // S2 -> S3: its bit mask. Also the BULK element bound and write addresses. + logic [WW:0] wv; // word this lane's mask refers to + logic [WW:0] lo_w, hi_w; + logic s1_kvalid; + // MATCH: unit g checks the positions of word g*LANES + sub*MATCH_LANES + g - 1 + assign wv = (s1_kind == K_MAT) + ? (WW+1)'({s1_g, LW'(0)}) + (WW+1)'(s1_sub) * (WW+1)'(MATCH_LANES) + + (WW+1)'(g) - (WW+1)'(1) + : {s1_g, LW'(g)}; + assign lo_w = (WW+1)'(r_lo[BW-1:6]); + assign hi_w = (WW+1)'(r_hi[BW-1:6]); + assign s1_kvalid = ((WW+1)'({s1_g[AW-1:0], LW'(0)}) + (WW+1)'(q_off)) < r_n; + + logic s2_in, s2_first, s2_last; + logic [63:0] s3_mask; + logic s2_kvalid, s3_kvalid; + logic [AW-1:0] s2_waddr, s3_waddr; + always_ff @(posedge clk) begin + if (reset) begin + s2_in <= 1'b0; s2_first <= 1'b0; s2_last <= 1'b0; s2_kvalid <= 1'b0; s2_waddr <= '0; + s3_mask <= '0; s3_kvalid <= 1'b0; s3_waddr <= '0; rq[g] <= '0; + end else begin + s2_in <= wv >= lo_w && wv <= hi_w; + s2_first <= wv == lo_w; + s2_last <= wv == hi_w; + s2_kvalid <= s1_kvalid; + s2_waddr <= s1_kind == K_DST ? s1_g[AW-1:0] + da : s1_g[AW-1:0]; + s3_mask <= s2_in ? ((s2_first ? r_lowm : ~64'd0) & (s2_last ? r_highm : ~64'd0)) : 64'd0; + s3_kvalid <= s2_kvalid; + s3_waddr <= s2_waddr; + rq[g] <= rdata; // S2 -> S3: bank data registered + end + end + + // Per-lane copies of the step's control (kept apart, so that no single + // control net fans out to every lane's 64-bit muxes). l_op/l_fn/l_dry + // follow r_op/r_fn/r_dry one cycle later, long before S3 uses them. + (* keep *) logic [2:0] l3_kind; + (* keep *) logic l3_valid; + (* keep *) logic [3:0] l_op; + (* keep *) logic [2:0] l_fn; + (* keep *) logic l_dry; + (* keep *) always_ff @(posedge clk) begin + if (reset) begin + l3_kind <= K_BITS; l3_valid <= 1'b0; l_op <= '0; l_fn <= '0; l_dry <= 1'b0; + end else begin + l3_kind <= s2_kind; l3_valid <= s2_valid; l_op <= r_op; l_fn <= r_fn; l_dry <= r_dry; + end + end + + // S3 compute, from registered data + logic [63:0] src_q; // BULK: this dst lane's src word of the current step + logic [63:0] sel, fillword, srcw, bulkword, mvec; + assign sel = (l_op == OP_FIND0 ? ~rq[g] : rq[g]) & s3_mask; + always_comb begin + case (l_op) + OP_SETR: fillword = rq[g] | s3_mask; + OP_CLEARR: fillword = rq[g] & ~s3_mask; + default: fillword = rq[g] ^ s3_mask; // FLIPR + endcase + end + assign srcw = src_q; + always_comb begin + case (l_fn) + FN_COPY: bulkword = srcw; + FN_AND: bulkword = rq[g] & srcw; + FN_OR: bulkword = rq[g] | srcw; + FN_XOR: bulkword = rq[g] ^ srcw; + default: bulkword = rq[g] & ~srcw; // ANDN + endcase + end + if (WITH_MATCH && g < MATCH_LANES) begin : matcher + // positions p of word w - 1: window bits p .. p+63 of {word w, word w - 1}, + // w = this sub-step's word idx of the group. Both are registered here at + // S2. For idx 0, word w - 1 is the last lane's word of the previous + // group (sub-steps issue on consecutive cycles), still in rq[LANES - 1]. + logic [LW-1:0] idx; + logic [63:0] m_hi, m_lo; + logic [127:0] win; + // (s2_sub is 0 when MATCH_LANES == LANES, where LW'(MATCH_LANES) wraps to 0) + assign idx = LW'(LW'(s2_sub) * LW'(MATCH_LANES) + LW'(g)); + always_ff @(posedge clk) begin + if (reset) begin + m_hi <= '0; m_lo <= '0; + end else begin + m_hi <= lane_rdata[idx]; + m_lo <= (idx == '0) ? rq[LANES-1] : lane_rdata[LW'(idx - 1'b1)]; + end + end + assign win = {m_hi, m_lo}; + // this unit's copy of the pattern and mask (loaded in R_PSET1, before any + // MATCH step), so their bits do not fan out across all units + (* keep *) logic [63:0] u_pat, u_msk; + (* keep *) always_ff @(posedge clk) begin + if (reset) begin + u_pat <= '0; u_msk <= '0; + end else if (r_state == R_PSET1) begin + u_pat <= r_pat; u_msk <= r_msk; + end + end + always_comb + for (int p = 0; p < 64; p++) mvec[p] = ~|((win[p +: 64] ^ u_pat) & u_msk); + end else begin : no_matcher + assign mvec = '0; + end + + // S3 -> S4: registered bank write and result word + logic w_we; + logic [AW-1:0] w_waddr; + logic [63:0] w_wdata, s4_word; + always_ff @(posedge clk) begin + if (reset) begin + w_we <= 1'b0; w_waddr <= '0; w_wdata <= '0; s4_word <= '0; src_q <= '0; + end else begin + w_we <= 1'b0; + w_waddr <= s3_waddr; + // the cross-lane rotation ends in this register + if (l3_valid && l3_kind == K_SRC) src_q <= rq[src_sel]; + case (l3_kind) + K_DST: begin + w_we <= l3_valid && s3_kvalid && !l_dry; + w_wdata <= bulkword; + s4_word <= s3_kvalid ? bulkword : 64'd0; + end + K_MAT: s4_word <= (g < MATCH_LANES) ? (mvec & s3_mask) : 64'd0; + default: begin + w_we <= l3_valid && l3_kind == K_BITS && s3_mask != '0 + && (l_op == OP_SETR || l_op == OP_CLEARR || l_op == OP_FLIPR); + w_wdata <= fillword; + s4_word <= sel; + end + endcase + if (s4_valid) begin + s5_cnt[g] <= popcount64(s4_word); + s5_hit[g] <= s4_word != '0; + s5_pos[g] <= ctz64(s4_word); + end + end + end + + // ---------------- bank port: range unit, else lane, else host (engine idle) + logic [AW-1:0] q_addr; + logic q_idle; + assign q_addr = q_local[g]; + assign q_idle = q_empty[g]; + always_comb begin + raddr = q_addr; we = 1'b0; waddr = x_local; wdata = newword; + if (r_busy) begin + raddr = s1_raddr; we = w_we; waddr = w_waddr; wdata = w_wdata; + end else if (st_mod) begin + we = writes; + end else if (q_idle && host_valid && host_ready && h_lane == LW'(g)) begin + raddr = h_local; waddr = h_local; we = host_write; wdata = host_wdata; + end + end + end + endgenerate + + // ---------------------------------------------------------------- reorder buffer + logic [ROB_DEPTH-1:0] rob_full, rob_err, rob_bit, rob_rng; + logic [55:0] rob_val [ROB_DEPTH]; // written only by the range unit + logic [SW-1:0] head; + logic r_bitout; + logic [55:0] r_valout; + + assign r_bitout = r_find ? r_found : (r_acc != '0); + assign r_valout = r_find ? (r_found ? 56'(r_idx) : 56'd0) : 56'(r_acc); + + assign res_valid = rob_full[head]; + assign res_error = rob_err[head]; + assign res_bit = rob_bit[head]; + assign res_value = rob_rng[head] ? rob_val[head] : 56'd0; + assign emit = res_valid && res_ready; + + always_ff @(posedge clk) begin + if (r_done) rob_val[r_seq] <= r_valout; + end + + always_ff @(posedge clk) begin + if (reset) begin + rob_full <= '0; rob_err <= '0; rob_bit <= '0; rob_rng <= '0; head <= '0; inflight <= '0; + end else begin + if (emit) begin + rob_full[head] <= 1'b0; + head <= head + 1'b1; + end + if (d_take && d_err) begin + rob_full[d_seq] <= 1'b1; rob_err[d_seq] <= 1'b1; rob_bit[d_seq] <= 1'b0; rob_rng[d_seq] <= 1'b0; + end + for (int i = 0; i < LANES; i++) begin + if (l_done[i]) begin + rob_full[l_seq[i]] <= 1'b1; rob_err[l_seq[i]] <= 1'b0; rob_bit[l_seq[i]] <= l_bit[i]; + rob_rng[l_seq[i]] <= 1'b0; + end + end + if (r_done) begin + rob_full[r_seq] <= 1'b1; rob_err[r_seq] <= 1'b0; rob_bit[r_seq] <= r_bitout; rob_rng[r_seq] <= 1'b1; + end + inflight <= inflight_next; + end + end + + // ---------------------------------------------------------------- host port + assign idle = (inflight == '0) && !p_valid && !d_valid && (l_busy == '0) && !r_busy && !h_pending; + assign host_ready = idle; + + // A read's data is in the bank's output register in the cycle after the + // handshake, which is exactly when h_pending is high. + assign host_rvalid = h_pending; + always_ff @(posedge clk) begin + if (reset) begin + h_pending <= 1'b0; h_lane_q <= '0; + end else begin + h_pending <= host_valid && host_ready && !host_write; + if (host_valid && host_ready) h_lane_q <= h_lane; + end + end + + generate + for (g = 0; g < LANES; g++) begin : rd + assign lane_rdata[g] = lane[g].rdata; + end + endgenerate + assign host_rdata = lane_rdata[h_lane_q]; + + logic unused_ok; + assign unused_ok = &{1'b0, src_x[64:WW], p_end[64:BW], s5_word[WW], r_pat}; // MATCH regs unused when WITH_MATCH = 0 +endmodule diff --git a/pcie/rtl/bitacc_pcie_core.sv b/pcie/rtl/bitacc_pcie_core.sv new file mode 100644 index 0000000000000000000000000000000000000000..1cf81c22f4b154f0dd7842078e8c529faba63e68 --- /dev/null +++ b/pcie/rtl/bitacc_pcie_core.sv @@ -0,0 +1,150 @@ +// PCIe-facing wrapper around bitacc_engine: 128-bit DMA streams and a small +// register interface. Transport-independent: LiteX/LitePCIe connects the +// streams to its DMA reader (host -> card) and writer (card -> host), but any +// 128-bit valid/ready stream source works. +// +// Descriptor (one per 128-bit input beat, little-endian), format version 3: +// [63:0] a: bit address (single-bit and bit-range operations; the host +// computes (base << 3) + offset), or dst word (BULK) +// [67:64] opcode: 0 GET, 1 TEST, 2 SET, 3 CLEAR, 4 TOGGLE, +// 8 COUNT, 9 FIND1, 10 FIND0, 11 SETR, 12 CLEARR, 13 FLIPR, 14 BULK, +// 15 MATCH (only with WITH_MATCH; otherwise an error), +// 5-7 undefined (no write, result "error") +// [68] dry: BULK computes and counts without writing +// [71:69] fn: BULK function (0 COPY, 1 AND, 2 OR, 3 XOR, 4 ANDN); +// MATCH result (0 number of matches, 1 first match) +// [103:72] len: range length in bits, or in words for BULK +// [127:104] src word (BULK source; MATCH pattern, with the mask in src + 1) +// Semantics and errors: see bitacc_engine.sv. +// +// Results: 8 bytes per descriptor, in descriptor order, 2 per output beat +// (bytes 0-7 of a beat = the first descriptor of the pair): +// [7:0] 0xA0 | error << 1 | bit (same status byte as the UART builds) +// [63:8] value (range operations; 0 otherwise) +// A batch must hold an even number of descriptors; hosts pad with opcode 7 +// (no write, result "error") and drop the padding results. +// +// Registers (driven by CSRs in the SoC): +// host_word / host_wdata / host_write_stb / host_read_stb : word access to +// the bit store, accepted only while idle; host_rdata valid when +// host_rdata_valid rises (sticky until the next access). +module bitacc_pcie_core #( + parameter int LANES = 8, + parameter int WORDS_PER_LANE = 2048, + parameter int ROB_DEPTH = 32, + parameter bit WITH_MATCH = 1'b1, + parameter int MATCH_LANES = LANES +)( + input logic clk, + input logic reset, + + input logic in_valid, + output logic in_ready, + input logic [127:0] in_data, + + output logic out_valid, + input logic out_ready, + output logic [127:0] out_data, + + input logic [$clog2(LANES*WORDS_PER_LANE)-1:0] host_word, + input logic [63:0] host_wdata, + input logic host_write_stb, + input logic host_read_stb, + output logic [63:0] host_rdata, + output logic host_rdata_valid, + output logic host_busy, // an access is pending + + output logic idle, + output logic [31:0] ops_accepted, + output logic [31:0] results_sent +); + localparam int WW = $clog2(LANES * WORDS_PER_LANE); + + // ---------------------------------------------------------------- engine + logic res_valid, res_ready, res_error, res_bit; + logic [55:0] res_value; + logic h_valid, h_ready, h_write, h_rvalid; + logic [WW-1:0] h_word; + logic [63:0] h_wdata, h_rdata; + logic eng_idle; + + bitacc_engine #(.LANES(LANES), .WORDS_PER_LANE(WORDS_PER_LANE), .ROB_DEPTH(ROB_DEPTH), + .WITH_MATCH(WITH_MATCH), .MATCH_LANES(MATCH_LANES)) u_engine ( + .clk, .reset, + .cmd_valid(in_valid), .cmd_ready(in_ready), + .cmd_op(in_data[67:64]), .cmd_base(64'd0), .cmd_offset(in_data[63:0]), + .cmd_len(in_data[103:72]), .cmd_src(in_data[127:104]), .cmd_fn(in_data[71:69]), .cmd_dry(in_data[68]), + .res_valid, .res_ready, .res_error, .res_bit, .res_value, + .host_valid(h_valid), .host_ready(h_ready), .host_write(h_write), + .host_word(h_word), .host_wdata(h_wdata), + .host_rvalid(h_rvalid), .host_rdata(h_rdata), + .idle(eng_idle)); + + // ---------------------------------------------------------------- result packer + logic [127:0] pack; + logic fill; // results already in pack + logic full; // pack holds 2 results waiting for out_ready + + // A beat leaving this cycle frees the pack for the next result. + assign res_ready = !full || out_ready; + assign out_valid = full; + assign out_data = pack; + + always_ff @(posedge clk) begin + if (reset) begin + pack <= '0; fill <= '0; full <= 1'b0; + end else begin + if (full && out_ready) full <= 1'b0; + if (res_valid && res_ready) begin + pack[{fill, 6'b000000} +: 64] <= {res_value, 6'b101000, res_error, res_bit}; + fill <= !fill; + if (fill) full <= 1'b1; + end + end + end + + // ---------------------------------------------------------------- host word access + // A strobe is latched and issued when the engine is idle. + logic pend, pend_write; + logic [WW-1:0] pend_word; + logic [63:0] pend_wdata; + + assign h_valid = pend; + assign h_write = pend_write; + assign h_word = pend_word; + assign h_wdata = pend_wdata; + assign host_busy = pend; + + always_ff @(posedge clk) begin + if (reset) begin + pend <= 1'b0; pend_write <= 1'b0; pend_word <= '0; pend_wdata <= '0; + host_rdata <= '0; host_rdata_valid <= 1'b0; + end else begin + if (!pend && (host_write_stb || host_read_stb)) begin + pend <= 1'b1; + pend_write <= host_write_stb; + pend_word <= host_word; + pend_wdata <= host_wdata; + host_rdata_valid <= 1'b0; + end else if (pend && h_ready) begin + pend <= 1'b0; + end + if (h_rvalid) begin + host_rdata <= h_rdata; + host_rdata_valid <= 1'b1; + end + end + end + + // ---------------------------------------------------------------- status + assign idle = eng_idle && !full && !fill && !pend; + + always_ff @(posedge clk) begin + if (reset) begin + ops_accepted <= '0; results_sent <= '0; + end else begin + if (in_valid && in_ready) ops_accepted <= ops_accepted + 1'b1; + if (out_valid && out_ready) results_sent <= results_sent + 32'd2; + end + end +endmodule diff --git a/pcie/tb/gold_ops.svh b/pcie/tb/gold_ops.svh new file mode 100644 index 0000000000000000000000000000000000000000..418919c06f6adfdc997be085c9caa5a9fb22ce5b --- /dev/null +++ b/pcie/tb/gold_ops.svh @@ -0,0 +1,148 @@ +// Reference semantics of every engine operation, executed on the single-op +// reference core rtl/bit_accelerator.sv. +// +// Included inside a testbench module that provides: clk; the reference core's +// inputs g_op_valid, g_base, g_off, g_opc and outputs g_op_ready, g_res_valid, +// g_res_bit, g_error; WORDS (bit-store size in 64-bit words); WITH_MATCH +// (whether the engine under test has the MATCH unit); and +// task check(cond, what). +// +// Range operations are defined as sequences of single-bit operations on the +// reference core, bit by bit in ascending order: +// COUNT GET every bit, add them up +// FIND1/FIND0 GET bits until one equals 1/0 +// SETR/CLEARR/FLIPR GET each bit (counted), then SET/CLEAR/TOGGLE it +// BULK per bit: GET src, GET dst, compute, SET/CLEAR dst if it changes +// MATCH GET the 64 pattern bits, the 64 mask bits and every haystack +// bit; then position k matches if every mask bit j has +// k + j < len and haystack[k + j] == pattern[j] +// Which range operations are errors is a specification choice (range_error); +// for bit ranges it is cross-checked against the reference core, which must +// reject the last bit of every invalid non-empty range. + +localparam logic [2:0] G_GET = 3'd0, G_SET = 3'd2, G_CLEAR = 3'd3, G_TOGGLE = 3'd4; +localparam logic [3:0] R_COUNT = 4'd8, R_FIND1 = 4'd9, R_FIND0 = 4'd10, + R_SETR = 4'd11, R_CLEARR = 4'd12, R_FLIPR = 4'd13, R_BULK = 4'd14, + R_MATCH = 4'd15; + +integer gold_ops_issued = 0; +logic gold_hay [WORDS * 64]; // MATCH haystack, as read from the reference core + +// One operation on the reference core. +task automatic gold_op(input logic [63:0] base, input logic [63:0] off, input logic [2:0] op, + output logic err, output logic b); + @(negedge clk); + g_base = base; g_off = off; g_opc = op; g_op_valid = 1'b1; + while (!g_op_ready) @(negedge clk); + @(negedge clk); g_op_valid = 1'b0; + while (!g_res_valid) @(negedge clk); + err = g_error; + b = g_res_bit; + gold_ops_issued++; +endtask + +// GET/SET/CLEAR/TOGGLE at an absolute bit address that must be valid. +task automatic gold_bit(input logic [63:0] bitaddr, input logic [2:0] op, output logic b); + logic err; + gold_op(64'd0, bitaddr, op, err, b); + if (err) check(1'b0, $sformatf("reference core accepts bit %h inside a valid range", bitaddr)); +endtask + +function automatic logic range_error(input logic [3:0] op, input logic [63:0] a, input logic [31:0] len, + input logic [23:0] src, input logic [2:0] fn); + logic [64:0] e, se, words; + e = {1'b0, a} + 65'(len); + se = 65'(src) + 65'(len); + words = 65'(WORDS); + if (op >= R_COUNT && op <= R_FLIPR) range_error = e > (words << 6); + else if (op == R_BULK) + range_error = fn > 3'd4 || e > words || se > words + || (len != 0 && {1'b0, a} != 65'(src) && {1'b0, a} < se && 65'(src) < e); + else if (op == R_MATCH) + range_error = !WITH_MATCH || fn > 3'd1 || e > (words << 6) || 65'(src) + 65'd2 > words; + else range_error = 1'b1; +endfunction + +function automatic logic bulk_fn(input logic [2:0] fn, input logic d, input logic s); + case (fn) + 3'd0: bulk_fn = s; + 3'd1: bulk_fn = d & s; + 3'd2: bulk_fn = d | s; + 3'd3: bulk_fn = d ^ s; + default: bulk_fn = d & !s; + endcase +endfunction + +// A range operation (opcode 8-15) on the reference core. +task automatic gold_range(input logic [3:0] op, input logic [63:0] a, input logic [31:0] len, + input logic [23:0] src, input logic [2:0] fn, input logic dry, + output logic err, output logic b, output logic [55:0] val); + logic x, y, r, e, done, ok; + logic [64:0] last; + logic [63:0] k, bitaddr, sbit, pat, msk, cnt, j; + err = range_error(op, a, len, src, fn); + b = 1'b0; + val = '0; + if (err) begin + last = {1'b0, a} + 65'(len) - 65'd1; + if (((op >= R_COUNT && op <= R_FLIPR) || (op == R_MATCH && WITH_MATCH && fn <= 3'd1 + && 65'(src) + 65'd2 <= 65'(WORDS))) + && len != 0 && !last[64]) begin + gold_op(64'd0, last[63:0], G_GET, e, x); + check(e, $sformatf("reference core rejects the last bit %h of invalid range op %0d", last[63:0], op)); + end + end else if (op == R_COUNT) begin + for (k = 0; k < 64'(len); k++) begin + gold_bit(a + k, G_GET, x); + val = val + 56'(x); + end + end else if (op == R_FIND1 || op == R_FIND0) begin + k = 0; done = 1'b0; + while (!done && k < 64'(len)) begin + gold_bit(a + k, G_GET, x); + if (x == (op == R_FIND1)) begin done = 1'b1; b = 1'b1; val = 56'(a + k); end + k++; + end + end else if (op == R_SETR || op == R_CLEARR || op == R_FLIPR) begin + for (k = 0; k < 64'(len); k++) begin + gold_bit(a + k, G_GET, x); + val = val + 56'(x); + gold_bit(a + k, op == R_SETR ? G_SET : op == R_CLEARR ? G_CLEAR : G_TOGGLE, y); + end + end else if (op == R_MATCH) begin + for (k = 0; k < 64; k++) begin + gold_bit((64'(src) << 6) + k, G_GET, x); pat[k] = x; + gold_bit((64'(src) << 6) + 64 + k, G_GET, y); msk[k] = y; + end + for (k = 0; k < 64'(len); k++) begin + gold_bit(a + k, G_GET, x); + gold_hay[k] = x; + end + cnt = 0; done = 1'b0; + for (k = 0; k < 64'(len); k++) begin + ok = 1'b1; j = 0; + while (ok && j < 64) begin + if (msk[j] && (k + j >= 64'(len) || gold_hay[k + j] != pat[j])) ok = 1'b0; + j++; + end + if (ok) begin + cnt++; + if (!done) begin done = 1'b1; val = 56'(a + k); end + end + end + if (fn[0]) b = done; + else begin val = 56'(cnt); b = cnt != 0; end + end else begin // BULK: a = dst word + for (k = 0; k < 64'(len) * 64; k++) begin + bitaddr = (a << 6) + k; + sbit = (64'(src) << 6) + k; + gold_bit(sbit, G_GET, x); + gold_bit(bitaddr, G_GET, y); + r = bulk_fn(fn, y, x); + val = val + 56'(r); + if (!dry && r != y) gold_bit(bitaddr, r ? G_SET : G_CLEAR, y); + end + end + if (!err && op != R_FIND1 && op != R_FIND0 && op != R_MATCH) b = val != 0; + if (!err && op == R_MATCH && fn[0] && !b) val = '0; +endtask diff --git a/pcie/tb/range_stim.svh b/pcie/tb/range_stim.svh new file mode 100644 index 0000000000000000000000000000000000000000..3cf3bc18d02ed6670c90953ba6cf36bd717f9b88 --- /dev/null +++ b/pcie/tb/range_stim.svh @@ -0,0 +1,197 @@ +// Random range-operation stimulus and coverage, shared by tb_engine and +// tb_pcie_core. Included after gold_ops.svh in a module that provides WORDS, +// LANES, rnd(n), rnd32() and check(cond, what). +// +// Classes: every range opcode and the undefined opcodes; bit ranges that are +// short (word-crossing), multi-step, empty, at the end of the store, past the +// end and 64-bit wrapping; BULK regions that are disjoint, identical, +// overlapping (error), out of range, with every function, undefined functions +// and dry runs; FIND over a region the last fill emptied (not found) or past it; +// MATCH (gen_match_seq) with masks built in the stream: contiguous runs, zero, +// two far-apart bits, a full word whose pattern is a copy of a haystack word, +// or whatever the memory holds; fn 0/1 and the error cases. +// Coverage is counted from what was executed (operation and reference result), +// not from the generator's intent. + +logic [63:0] rs_fill_a = '0; +logic [31:0] rs_fill_len = '0; +logic [3:0] rs_fill_op = '0; + +function automatic logic [63:0] rs_min(input logic [63:0] x, input logic [63:0] y); + rs_min = x < y ? x : y; +endfunction + +task automatic gen_range(output logic [3:0] op, output logic [63:0] a, output logic [31:0] len, + output logic [23:0] src, output logic [2:0] fn, output logic dry); + integer c, lc, pc, tries; + logic [63:0] bits, dst, l; + bits = 64'(WORDS) * 64; + src = '0; fn = '0; dry = 1'b0; len = '0; a = '0; op = R_COUNT; + c = rnd(100); + if (c < 4) begin // undefined opcode (5-7) as a range + op = 4'(5 + rnd(3)); a = rnd(WORDS * 64); len = rnd(64); + end else if (c < 14 && rs_fill_len != 0 && rs_fill_op != R_FLIPR) begin + // FIND the value the last fill removed: not found in the region, maybe found past it + op = rs_fill_op == R_SETR ? R_FIND0 : R_FIND1; + a = rs_fill_a; + len = 32'(rs_min(64'(rs_fill_len) + (rnd(2) == 0 ? 0 : rnd(130)), bits - rs_fill_a)); + end else if (c < 60) begin // bit range + op = 4'(8 + rnd(6)); + lc = rnd(100); + if (lc < 45) len = rnd(130); + else if (lc < 88) len = rnd(64 * rs_min(WORDS, 3 * LANES + 3) + 1); + else len = 0; + pc = rnd(100); + if (pc < 82) a = rnd(bits - len + 1); + else if (pc < 86) a = bits - len; // ends exactly at the end of the store + else if (pc < 93) a = bits - len + 1 + rnd(64); // past the end + else if (pc < 97) a = {rnd32(), rnd32()}; + else a = ~64'd0 - rnd(64); // a + len wraps 64 bits + if ((op == R_SETR || op == R_CLEARR || op == R_FLIPR) && len != 0 + && !range_error(op, a, len, src, fn)) begin + rs_fill_a = a; rs_fill_len = len; rs_fill_op = op; + end + end else begin // BULK + op = R_BULK; + fn = rnd(10) == 0 ? 3'(5 + rnd(3)) : 3'(rnd(5)); + dry = rnd(4) == 0; + lc = rnd(100); + if (lc < 50) l = rnd(3 * LANES + 3); + else if (lc < 92) l = rnd(rs_min(WORDS / 2, 64) + 1); + else l = 0; + len = 32'(l); + src = 24'(rnd(WORDS - l + 1)); + pc = rnd(100); + if (pc < 60) begin // disjoint + dst = rnd(WORDS - l + 1); tries = 0; + while (tries < 50 && l != 0 && dst < 64'(src) + l && 64'(src) < dst + l) begin + dst = rnd(WORDS - l + 1); tries++; + end + end else if (pc < 75) dst = 64'(src); // identical + else if (pc < 87) dst = 64'(src) + 1 + rnd(l > 1 ? l - 1 : 1); // overlapping + else if (pc < 95) dst = WORDS - l + 1 + rnd(8); // past the end + else dst = {rnd32(), rnd32()}; + a = dst; + end +endtask + +// MATCH sequence: set up the mask (and sometimes the pattern) with ordinary +// operations, then MATCH. gen_match_seq fills rs_q_*[0 .. rs_q_n-1]. +logic [3:0] rs_q_op [4]; +logic [63:0] rs_q_a [4]; +logic [31:0] rs_q_len [4]; +logic [23:0] rs_q_src [4]; +logic [2:0] rs_q_fn [4]; +integer rs_q_n = 0; + +task automatic rs_push(input logic [3:0] op, input logic [63:0] a, input logic [31:0] len, + input logic [23:0] src, input logic [2:0] fn); + rs_q_op[rs_q_n] = op; rs_q_a[rs_q_n] = a; rs_q_len[rs_q_n] = len; + rs_q_src[rs_q_n] = src; rs_q_fn[rs_q_n] = fn; + rs_q_n++; +endtask + +task automatic gen_match_seq(); + integer c, m, k, o; + logic [63:0] bits, pw, mw, a, h; + logic [31:0] len; + logic [2:0] fn; + bits = 64'(WORDS) * 64; + rs_q_n = 0; + pw = rnd(WORDS - 1); // pattern word; mask word pw + 1 + mw = (pw + 1) << 6; + c = rnd(100); + if (c < 45) len = rnd(300); + else if (c < 92) len = rnd(64 * rs_min(WORDS, 3 * LANES + 3) + 1); + else len = 0; + a = rnd(bits - len + 1); + fn = rnd(12) == 0 ? 3'(2 + rnd(6)) : 3'(rnd(2)); + m = rnd(100); + if (m < 40) begin // contiguous k bits at offset o + k = 1 + rnd(10); o = rnd(64 - k + 1); + rs_push(R_CLEARR, mw, 64, 0, 0); + rs_push(R_SETR, mw + o, k, 0, 0); + end else if (m < 52) begin // zero mask: every position matches + rs_push(R_CLEARR, mw, 64, 0, 0); + end else if (m < 66 && len >= 64) begin // pattern = a haystack word, full mask + h = (a + 63) / 64 + rnd(len / 64); // a word that may lie inside the haystack + if (h >= WORDS) h = WORDS - 1; + rs_push(R_BULK, pw, 1, 24'(h), 3'd0); // COPY word h to the pattern word + rs_push(R_SETR, mw, 64, 0, 0); + end else if (m < 82) begin // two far-apart bits: windows across words + rs_push(R_CLEARR, mw, 64, 0, 0); + rs_push(R_SETR, mw + rnd(8), 1, 0, 0); + rs_push(R_SETR, mw + 56 + rnd(8), 1, 0, 0); + end // else: the mask word as it is + if (rnd(25) == 0) pw = WORDS - 1; // mask word past the end: error + rs_push(R_MATCH, a, len, 24'(pw), fn); +endtask + +integer cov_op [16]; +integer cov_fn [8]; +integer cov_dry = 0, cov_same = 0, cov_overlap = 0, cov_empty = 0, cov_rerr = 0, cov_whole = 0; +integer cov_multi_bits = 0, cov_multi_bulk = 0, cov_found = 0, cov_notfound = 0, cov_end = 0; +integer cov_mcount = 0, cov_mnone = 0, cov_mfound = 0, cov_mnf = 0, cov_merr = 0, cov_mmulti = 0; +initial begin + for (int i = 0; i < 16; i++) cov_op[i] = 0; + for (int i = 0; i < 8; i++) cov_fn[i] = 0; +end + +task automatic range_cover(input logic [3:0] op, input logic [63:0] a, input logic [31:0] len, + input logic [23:0] src, input logic [2:0] fn, input logic dry, + input logic err, input logic b); + logic [64:0] e; + e = {1'b0, a} + 65'(len); + if (op < R_COUNT) begin + // single-bit operation: not a range class + end else if (err) begin + cov_rerr++; + if (op == R_MATCH) cov_merr++; + if (op == R_BULK && fn <= 3'd4 && e <= 65'(WORDS) && 65'(src) + 65'(len) <= 65'(WORDS)) cov_overlap++; + end else begin + cov_op[op]++; + if (len == 0) cov_empty++; + if (op == R_MATCH) begin + if (len > 64 * 2 * LANES) cov_mmulti++; + if (fn[0]) begin if (b) cov_mfound++; else if (len != 0) cov_mnf++; end + else begin if (b) cov_mcount++; else if (len != 0) cov_mnone++; end + end else if (op == R_BULK) begin + cov_fn[fn]++; + if (dry) cov_dry++; + if (a == 64'(src) && len != 0) cov_same++; + if (len > 2 * LANES) cov_multi_bulk++; + end else begin + if (len > 64 * 2 * LANES) cov_multi_bits++; + if (len != 0 && e == 65'(WORDS) * 64) cov_end++; + if (a == 0 && e == 65'(WORDS) * 64) cov_whole++; + if ((op == R_FIND1 || op == R_FIND0) && len != 0) begin + if (b) cov_found++; else cov_notfound++; + end + end + end +endtask + +task automatic range_cover_check(); + for (int op = 8; op <= (WITH_MATCH ? 15 : 14); op++) + check(cov_op[op] > 0, $sformatf("range opcode %0d executed without error", op)); + if (WITH_MATCH) + check(cov_mcount > 0 && cov_mnone > 0 && cov_mfound > 0 && cov_mnf > 0 && cov_mmulti > 0, + "MATCH counted matches, no matches, found, not found, and spanned several steps"); + check(cov_merr > 0, "invalid MATCH rejected"); + for (int f = 0; f <= 4; f++) + check(cov_fn[f] > 0, $sformatf("BULK function %0d executed", f)); + check(cov_dry > 0, "BULK dry run executed"); + check(cov_same > 0, "BULK with identical regions executed"); + check(cov_overlap > 0, "BULK with overlapping regions rejected"); + check(cov_empty > 0, "empty range executed"); + check(cov_multi_bits > 0 && cov_multi_bulk > 0, "multi-step bit range and BULK executed"); + check(cov_end > 0, "bit range ending at the end of the store executed"); + check(cov_found > 0 && cov_notfound > 0, "FIND found and not found"); + check(cov_rerr > 0, "invalid range operations rejected"); + $display("range coverage: count=%0d find1=%0d find0=%0d setr=%0d clearr=%0d flipr=%0d bulk=%0d (copy %0d and %0d or %0d xor %0d andn %0d dry %0d same %0d) overlap_err=%0d errors=%0d empty=%0d whole=%0d found=%0d not_found=%0d", + cov_op[8], cov_op[9], cov_op[10], cov_op[11], cov_op[12], cov_op[13], cov_op[14], + cov_fn[0], cov_fn[1], cov_fn[2], cov_fn[3], cov_fn[4], cov_dry, cov_same, + cov_overlap, cov_rerr, cov_empty, cov_whole, cov_found, cov_notfound); + $display("match coverage: valid=%0d with_matches=%0d none=%0d first_found=%0d not_found=%0d multi_step=%0d errors=%0d", + cov_op[15], cov_mcount, cov_mnone, cov_mfound, cov_mnf, cov_mmulti, cov_merr); +endtask diff --git a/pcie/tb/tb_engine.sv b/pcie/tb/tb_engine.sv new file mode 100644 index 0000000000000000000000000000000000000000..dccde75d257b7d2bb8a87e78f0b4a7adec0bd8eb --- /dev/null +++ b/pcie/tb/tb_engine.sv @@ -0,0 +1,358 @@ +// Equivalence testbench: the parallel engine (pcie/rtl/bitacc_engine.sv) against +// the single-op reference core (rtl/bit_accelerator.sv). +// +// Both start from the same random memory image and receive the same operation +// stream. The reference core runs the stream one operation at a time, range +// operations expanded into single-bit operations (tb/gold_ops.svh); the +// engine gets it as a stream with random gaps and random result backpressure. +// Every result (error flag; bit and value when there is no error) and every +// word of the final memory must match. Exits non-zero on any mismatch, or if +// a coverage goal (operation classes, out-of-order completion, full reorder +// buffer, range barriers in both directions) was not reached. +// +// +ops=N +seed=N stream length and seed +// -DLANES=n -DWPL=n engine geometry (default 4 lanes x 16 words: many conflicts) +`timescale 1ns/1ps +module tb_engine; +`ifndef LANES + `define LANES 4 +`endif +`ifndef WPL + `define WPL 16 +`endif +`ifndef MATCH + `define MATCH 1 +`endif +`ifndef MLANES + `define MLANES `LANES +`endif + localparam int LANES = `LANES; + localparam int WPL = `WPL; + localparam int WORDS = LANES * WPL; + localparam bit WITH_MATCH = `MATCH; + localparam int MLANES = `MLANES; // MATCH units + localparam int MAXOPS = 20000; + + logic clk = 1'b0, reset = 1'b1; + always #5 clk = ~clk; + + integer checks = 0, fails = 0; + task automatic check(input bit cond, input string what); + checks++; + if (!cond) begin + fails++; + if (fails <= 20) $display("FAIL @%0t: %s", $time, what); + end + endtask + + // ------------------------------------------------------------ stimulus + integer ops = 5000, seed = 1, seed0; + logic [3:0] s_op [MAXOPS]; + logic [63:0] s_base[MAXOPS], s_off[MAXOPS]; + logic [31:0] s_len [MAXOPS]; + logic [23:0] s_src [MAXOPS]; + logic [2:0] s_fn [MAXOPS]; + logic s_dry [MAXOPS]; + logic [63:0] init [WORDS]; + + // xorshift32, well distributed in every simulator. (Verilator 5.020's + // seeded system random function is degenerate: the seed roughly doubles on + // every call.) Simulators may evaluate operands such as rnd(a) + rnd(b) in + // different orders, so the same seed can give different streams in Icarus + // and Verilator; each is checked the same way. + function automatic logic [31:0] rnd32(); + logic [31:0] x; + x = (seed == 0) ? 32'd1 : 32'(seed); + x = x ^ (x << 13); x = x ^ (x >> 17); x = x ^ (x << 5); + seed = x; + rnd32 = x; + endfunction + function automatic integer rnd(input integer n); + rnd = (n <= 1) ? 0 : integer'(rnd32() % 32'(n)); + endfunction + + // ------------------------------------------------------------ reference: core + memory + logic g_op_valid = 1'b0, g_op_ready; + logic [63:0] g_base = '0, g_off = '0; + logic [2:0] g_opc = '0; + logic g_res_valid, g_res_bit, g_error; + logic g_mem_valid, g_mem_write; + logic [63:0] g_mem_addr, g_mem_wdata; + logic [7:0] g_mem_wstrb; + logic g_mem_rvalid = 1'b0, g_mem_fault = 1'b0; + logic [63:0] g_mem_rdata = '0; + logic [63:0] gmem [WORDS]; + + bit_accelerator gold ( + .clk, .reset, + .op_valid(g_op_valid), .op_ready(g_op_ready), + .base_address(g_base), .bit_offset(g_off), .operation(g_opc), + .result_valid(g_res_valid), .result_bit(g_res_bit), .error(g_error), + .mem_valid(g_mem_valid), .mem_write(g_mem_write), .mem_addr(g_mem_addr), + .mem_wdata(g_mem_wdata), .mem_wstrb(g_mem_wstrb), + .mem_ready(1'b1), .mem_rvalid(g_mem_rvalid), .mem_rdata(g_mem_rdata), + .mem_fault(g_mem_fault)); + + // Memory slave for the reference: byte addresses 0 .. WORDS*8-1, else fault. + always @(posedge clk) begin + g_mem_rvalid <= 1'b0; + g_mem_fault <= 1'b0; + if (!reset && g_mem_valid) begin + if ((g_mem_addr >> 3) >= WORDS) g_mem_fault <= 1'b1; + else if (g_mem_write) gmem[g_mem_addr >> 3] <= g_mem_wdata; + else begin g_mem_rvalid <= 1'b1; g_mem_rdata <= gmem[g_mem_addr >> 3]; end + end + end + + logic g_err_r [MAXOPS]; + logic g_bit_r [MAXOPS]; + logic [55:0] g_val_r [MAXOPS]; + bit gold_done = 0; + + `include "gold_ops.svh" + `include "range_stim.svh" + + task automatic run_gold(); + logic e, b; + logic [55:0] v; + for (int n = 0; n < ops; n++) begin + if (s_op[n] < 4'd8) begin + gold_op(s_base[n], s_off[n], s_op[n][2:0], e, b); + v = '0; + end else + gold_range(s_op[n], (s_base[n] << 3) + s_off[n], s_len[n], s_src[n], s_fn[n], s_dry[n], e, b, v); + g_err_r[n] = e; + g_bit_r[n] = b; + g_val_r[n] = v; + end + gold_done = 1; + endtask + + // ------------------------------------------------------------ engine + logic cmd_valid = 1'b0, cmd_ready; + logic [3:0] cmd_op = '0; + logic [63:0] cmd_base = '0, cmd_offset = '0; + logic [31:0] cmd_len = '0; + logic [23:0] cmd_src = '0; + logic [2:0] cmd_fn = '0; + logic cmd_dry = 1'b0; + logic res_valid, res_ready = 1'b0, res_error, res_bit; + logic [55:0] res_value; + logic host_valid = 1'b0, host_ready, host_write = 1'b0, host_rvalid; + logic [$clog2(WORDS)-1:0] host_word = '0; + logic [63:0] host_wdata = '0, host_rdata; + logic idle; + + bitacc_engine #(.LANES(LANES), .WORDS_PER_LANE(WPL), .WITH_MATCH(WITH_MATCH), .MATCH_LANES(MLANES)) dut (.*); + + logic e_err_r [MAXOPS]; + logic e_bit_r [MAXOPS]; + logic [55:0] e_val_r [MAXOPS]; + integer e_results = 0; + bit eng_done = 0; + + // result sink with random backpressure + always @(posedge clk) begin + if (!reset && res_valid && res_ready) begin + e_err_r[e_results] <= res_error; + e_bit_r[e_results] <= res_bit; + e_val_r[e_results] <= res_value; + e_results <= e_results + 1; + end + end + // Random backpressure, plus a 200-cycle stall every 2000 cycles that fills + // the reorder buffer and must hold the engine at its in-flight limit. + integer sink_cyc = 0; + always @(negedge clk) begin + sink_cyc <= sink_cyc + 1; + res_ready <= ((sink_cyc % 2000) >= 200) && ((rnd32() % 4) != 0); + end + + task automatic host_write_word(input integer w, input logic [63:0] v); + @(negedge clk); + host_valid = 1'b1; host_write = 1'b1; host_word = w; host_wdata = v; + while (!host_ready) @(negedge clk); + @(negedge clk); host_valid = 1'b0; host_write = 1'b0; + endtask + + task automatic host_read_word(input integer w, output logic [63:0] v); + @(negedge clk); + host_valid = 1'b1; host_write = 1'b0; host_word = w; + while (!host_ready) @(negedge clk); + @(negedge clk); host_valid = 1'b0; + check(host_rvalid === 1'b1, "host_rvalid one cycle after a read handshake"); + v = host_rdata; + endtask + + task automatic run_engine(); + integer n = 0; + while (n < ops) begin + @(negedge clk); + if (rnd(20) == 0) cmd_valid = 1'b0; // input gap + else begin + // cmd_ready does not depend on cmd_valid and only changes on a rising + // edge, so its value now is the one the engine samples at the next edge. + cmd_valid = 1'b1; cmd_op = s_op[n]; cmd_base = s_base[n]; cmd_offset = s_off[n]; + cmd_len = s_len[n]; cmd_src = s_src[n]; cmd_fn = s_fn[n]; cmd_dry = s_dry[n]; + if (cmd_ready) n++; + end + end + @(negedge clk); cmd_valid = 1'b0; + while (e_results < ops) @(negedge clk); + eng_done = 1; + endtask + + // ------------------------------------------------------------ coverage, watchdog + // A completion is out of order when an older operation (between the reorder + // head and its own slot) has not completed yet. Shadow completion flags per + // reorder slot: set when a result is written, cleared when it is emitted. + integer ooo = 0, idle_cycles = 0, last_results = 0, rob_full_cycles = 0; + integer range_waits = 0, single_waits = 0; // barrier: range behind lanes, single behind range + bit older_pending; + bit cdone [32]; + initial for (int i = 0; i < 32; i++) cdone[i] = 0; + // Sampled mid-cycle: the engine's registers are stable and hold exactly the + // values the next rising edge consumes, independent of simulator ordering. + always @(negedge clk) begin + if (!reset) begin + if (dut.inflight == 32) rob_full_cycles <= rob_full_cycles + 1; + if (dut.d_valid && !dut.d_err && dut.d_range && dut.ops_out != '0) range_waits <= range_waits + 1; + if (dut.d_valid && !dut.d_err && !dut.d_range && dut.r_busy) single_waits <= single_waits + 1; + for (int i = 0; i < LANES; i++) + if (dut.l_done[i]) begin + older_pending = 0; + for (int t = dut.head; (t % 32) != dut.l_seq[i]; t++) + if (!cdone[t % 32]) older_pending = 1; + if (older_pending) ooo = ooo + 1; + end + for (int i = 0; i < LANES; i++) if (dut.l_done[i]) cdone[dut.l_seq[i]] = 1; + if (dut.d_take && dut.d_err) cdone[dut.d_seq] = 1; + if (dut.r_done) cdone[dut.r_seq] = 1; + if (res_valid && res_ready) cdone[dut.head] = 0; + if (e_results != last_results || dut.idle) idle_cycles <= 0; + else idle_cycles <= idle_cycles + 1; + last_results <= e_results; + if (idle_cycles > 20000) $fatal(1, "engine stalled: no result for 20000 cycles"); + if (dut.inflight > 32) $fatal(1, "more operations in flight than reorder slots"); + end + end + + // ------------------------------------------------------------ main + logic [63:0] v; + integer kind; // per-operation stimulus class + integer pend_i = 0; // next entry of a pending MATCH sequence + integer n_hot = 0, n_any = 0, n_past = 0, n_wrap = 0, n_undef = 0, n_range = 0; + logic [63:0] ra; + + // Bit-store-wide operations at fixed points of the stream. + task automatic gen_whole(input int n, input int which); + s_base[n] = 0; s_off[n] = 0; s_src[n] = 0; s_fn[n] = 3'd3; s_dry[n] = 1'b0; + case (which) + 0: begin s_op[n] = R_COUNT; s_len[n] = WORDS * 64; end + 1: begin s_op[n] = R_FLIPR; s_len[n] = WORDS * 64; end + default: begin s_op[n] = R_BULK; s_len[n] = WORDS / 2; s_off[n] = WORDS / 2; end // upper ^= lower + endcase + endtask + initial begin + if (!$value$plusargs("ops=%d", ops)) ops = 5000; + if (!$value$plusargs("seed=%d", seed)) seed = 1; + seed0 = seed; + if (ops > MAXOPS) ops = MAXOPS; + + for (int w = 0; w < WORDS; w++) begin + init[w] = {rnd32(), rnd32()}; + gmem[w] = init[w]; + end + for (int n = 0; n < ops; n++) begin + // Blocks of 24: 16 back-to-back operations on 4 words of lane 0 (its queue + // builds up), then 8 mixed ones, many of which overtake them in other lanes. + kind = ((n % 24) < 16) ? 0 : 40 + rnd(60); + s_op[n] = (rnd(12) == 0) ? 4'(5 + rnd(3)) : 4'(rnd(5)); + s_len[n] = '0; s_src[n] = '0; s_fn[n] = '0; s_dry[n] = 1'b0; + if (s_op[n] > 4'd4) n_undef++; + if (n == ops / 4 || n == ops / 2 || n == 3 * ops / 4) begin + n_range++; + gen_whole(n, n == ops / 4 ? 0 : n == ops / 2 ? 1 : 2); + end else if (pend_i < rs_q_n || (kind >= 40 && rnd(4) == 0)) begin // range operation + n_range++; + if (pend_i < rs_q_n) begin // rest of a MATCH sequence + s_op[n] = rs_q_op[pend_i]; ra = rs_q_a[pend_i]; s_len[n] = rs_q_len[pend_i]; + s_src[n] = rs_q_src[pend_i]; s_fn[n] = rs_q_fn[pend_i]; pend_i++; + end else if (rnd(4) == 0) begin + gen_match_seq(); + s_op[n] = rs_q_op[0]; ra = rs_q_a[0]; s_len[n] = rs_q_len[0]; + s_src[n] = rs_q_src[0]; s_fn[n] = rs_q_fn[0]; pend_i = 1; + end else + gen_range(s_op[n], ra, s_len[n], s_src[n], s_fn[n], s_dry[n]); + if (rnd(2) == 0) begin s_base[n] = 0; s_off[n] = ra; end + else begin s_base[n] = ra >> 3; s_off[n] = ra & 64'd7; end + end else if (kind < 40) begin // hot spot: 4 words, all in lane 0 + n_hot++; + s_base[n] = rnd(4) * LANES * 8; s_off[n] = rnd(64); + end else if (kind < 80) begin // anywhere in memory + n_any++; + s_base[n] = rnd(WORDS * 8); s_off[n] = rnd(64 * 4); + if (rnd(2) == 0) s_base[n] = (rnd(WPL) * LANES + 1 + rnd(LANES - 1)) * 8; // not lane 0 + end else if (kind < 95) begin // just past the end + n_past++; + s_base[n] = WORDS * 8 + rnd(16); s_off[n] = rnd(64); + end else begin // arbitrary 64-bit values (wraps) + n_wrap++; + s_base[n] = {rnd32(), rnd32()}; s_off[n] = {rnd32(), rnd32()}; + end + end + + $display("stimulus: hot=%0d anywhere=%0d past_end=%0d wrap=%0d undefined_op=%0d range=%0d", + n_hot, n_any, n_past, n_wrap, n_undef, n_range); + check(n_hot > ops / 3 && n_any > ops / 20 && n_past > ops / 80 && n_wrap > ops / 400 + && n_undef > ops / 40 && n_range > ops / 40, "stimulus covers every operation class"); + repeat (3) @(negedge clk); + reset = 1'b0; + for (int w = 0; w < WORDS; w++) host_write_word(w, init[w]); + for (int w = 0; w < WORDS; w++) begin + host_read_word(w, v); + check(v === init[w], $sformatf("host read back word %0d", w)); + end + + fork + run_gold(); + run_engine(); + join + + for (int n = 0; n < ops; n++) begin + check(e_err_r[n] === g_err_r[n], + $sformatf("op %0d (%0d, base %h, off %h, len %0d, src %0d, fn %0d): error %b, reference %b", + n, s_op[n], s_base[n], s_off[n], s_len[n], s_src[n], s_fn[n], e_err_r[n], g_err_r[n])); + if (!g_err_r[n]) + check(e_bit_r[n] === g_bit_r[n] && e_val_r[n] === g_val_r[n], + $sformatf("op %0d (%0d, base %h, off %h, len %0d, src %0d, fn %0d): bit %b value %0d, reference %b %0d", + n, s_op[n], s_base[n], s_off[n], s_len[n], s_src[n], s_fn[n], + e_bit_r[n], e_val_r[n], g_bit_r[n], g_val_r[n])); + else + check(e_bit_r[n] === 1'b0 && e_val_r[n] === '0, $sformatf("op %0d: error result has bit 0, value 0", n)); + range_cover(s_op[n], (s_base[n] << 3) + s_off[n], s_len[n], s_src[n], s_fn[n], s_dry[n], + g_err_r[n], g_bit_r[n]); + end + range_cover_check(); + check(range_waits > 0, "a range operation waited for busy lanes"); + check(single_waits > 0, "an operation waited for a running range operation"); + check(idle, "engine idle after the stream"); + check(ooo > ops / 50, $sformatf("out-of-order completions exercised (%0d)", ooo)); + check(rob_full_cycles > 0, "reorder buffer filled (in-flight limit exercised)"); + for (int w = 0; w < WORDS; w++) begin + host_read_word(w, v); + check(v === gmem[w], $sformatf("final word %0d: %h, reference %h", w, v, gmem[w])); + end + + $display("checks=%0d fails=%0d lanes=%0d words=%0d ops=%0d seed=%0d out_of_order=%0d rob_full=%0d range_waits=%0d single_waits=%0d reference_ops=%0d", + checks, fails, LANES, WORDS, ops, seed0, ooo, rob_full_cycles, range_waits, single_waits, gold_ops_issued); + if (fails != 0) $fatal(1, "FAIL"); + $display("PASS: engine matches the reference core"); + $finish; + end + + initial begin + repeat (20) #1_000_000_000; + $fatal(1, "global timeout"); + end +endmodule diff --git a/pcie/tb/tb_pcie_core.sv b/pcie/tb/tb_pcie_core.sv new file mode 100644 index 0000000000000000000000000000000000000000..8f270faf75fbaa92efa3a175b602dfa580d7d372 --- /dev/null +++ b/pcie/tb/tb_pcie_core.sv @@ -0,0 +1,275 @@ +// bitacc_pcie_core (128-bit descriptor/result streams + register interface) +// against the single-op reference core rtl/bit_accelerator.sv. +// +// The reference executes each single-bit descriptor as base = 0, offset = bit +// address, and each range descriptor as its expansion into single-bit +// operations (tb/gold_ops.svh). The stream side sees input gaps and output +// backpressure with long stalls. Every 8-byte result, the counters, the idle +// flag and the final memory (read through the register interface) must +// match, and every range class must occur (tb/range_stim.svh). Exits non-zero +// on any mismatch. +`timescale 1ns/1ps +module tb_pcie_core; +`ifndef LANES + `define LANES 4 +`endif +`ifndef WPL + `define WPL 16 +`endif +`ifndef MATCH + `define MATCH 1 +`endif +`ifndef MLANES + `define MLANES `LANES +`endif + localparam int LANES = `LANES; + localparam int WPL = `WPL; + localparam int WORDS = LANES * WPL; + localparam bit WITH_MATCH = `MATCH; + localparam int MLANES = `MLANES; // MATCH units + localparam int MAXOPS = 16384; + + logic clk = 1'b0, reset = 1'b1; + always #4 clk = ~clk; // 125 MHz + + integer checks = 0, fails = 0; + task automatic check(input bit cond, input string what); + checks++; + if (!cond) begin + fails++; + if (fails <= 20) $display("FAIL @%0t: %s", $time, what); + end + endtask + + integer seed = 3, seed0, ops = 3200; + // xorshift32, well distributed in every simulator. (Verilator 5.020's + // seeded system random function is degenerate: the seed roughly doubles on + // every call.) Simulators may evaluate operands such as rnd(a) + rnd(b) in + // different orders, so the same seed can give different streams in Icarus + // and Verilator; each is checked the same way. + function automatic logic [31:0] rnd32(); + logic [31:0] x; + x = (seed == 0) ? 32'd1 : 32'(seed); + x = x ^ (x << 13); x = x ^ (x >> 17); x = x ^ (x << 5); + seed = x; + rnd32 = x; + endfunction + function automatic integer rnd(input integer n); + rnd = (n <= 1) ? 0 : integer'(rnd32() % 32'(n)); + endfunction + + // ------------------------------------------------------------ stimulus + logic [3:0] s_op [MAXOPS]; + logic [63:0] s_bit [MAXOPS]; // a: bit address, or dst word for BULK + logic [31:0] s_len [MAXOPS]; + logic [23:0] s_src [MAXOPS]; + logic [2:0] s_fn [MAXOPS]; + logic s_dry [MAXOPS]; + logic [63:0] init [WORDS]; + + // ------------------------------------------------------------ reference + logic g_op_valid = 1'b0, g_op_ready; + logic [63:0] g_base = '0, g_off = '0; + logic [2:0] g_opc = '0; + logic g_res_valid, g_res_bit, g_error; + logic g_mem_valid, g_mem_write; + logic [63:0] g_mem_addr, g_mem_wdata, g_mem_rdata = '0; + logic [7:0] g_mem_wstrb; + logic g_mem_rvalid = 1'b0, g_mem_fault = 1'b0; + logic [63:0] gmem [WORDS]; + + bit_accelerator gold ( + .clk, .reset, + .op_valid(g_op_valid), .op_ready(g_op_ready), + .base_address(g_base), .bit_offset(g_off), .operation(g_opc), + .result_valid(g_res_valid), .result_bit(g_res_bit), .error(g_error), + .mem_valid(g_mem_valid), .mem_write(g_mem_write), .mem_addr(g_mem_addr), + .mem_wdata(g_mem_wdata), .mem_wstrb(g_mem_wstrb), + .mem_ready(1'b1), .mem_rvalid(g_mem_rvalid), .mem_rdata(g_mem_rdata), + .mem_fault(g_mem_fault)); + + always @(posedge clk) begin + g_mem_rvalid <= 1'b0; + g_mem_fault <= 1'b0; + if (!reset && g_mem_valid) begin + if ((g_mem_addr >> 3) >= WORDS) g_mem_fault <= 1'b1; + else if (g_mem_write) gmem[g_mem_addr >> 3] <= g_mem_wdata; + else begin g_mem_rvalid <= 1'b1; g_mem_rdata <= gmem[g_mem_addr >> 3]; end + end + end + + logic g_err_r [MAXOPS], g_bit_r [MAXOPS]; + logic [55:0] g_val_r [MAXOPS]; + + `include "gold_ops.svh" + `include "range_stim.svh" + + task automatic run_gold(); + logic e, b; + logic [55:0] v; + for (int n = 0; n < ops; n++) begin + if (s_op[n] < 4'd8) begin + gold_op(64'd0, s_bit[n], s_op[n][2:0], e, b); + v = '0; + end else + gold_range(s_op[n], s_bit[n], s_len[n], s_src[n], s_fn[n], s_dry[n], e, b, v); + g_err_r[n] = e; g_bit_r[n] = b; g_val_r[n] = v; + end + endtask + + // ------------------------------------------------------------ DUT + logic in_valid = 1'b0, in_ready; + logic [127:0] in_data = '0; + logic out_valid, out_ready = 1'b0; + logic [127:0] out_data; + logic [$clog2(WORDS)-1:0] host_word = '0; + logic [63:0] host_wdata = '0, host_rdata; + logic host_write_stb = 1'b0, host_read_stb = 1'b0, host_rdata_valid, host_busy; + logic idle; + logic [31:0] ops_accepted, results_sent; + + bitacc_pcie_core #(.LANES(LANES), .WORDS_PER_LANE(WPL), .WITH_MATCH(WITH_MATCH), .MATCH_LANES(MLANES)) dut (.*); + + // result sink: random backpressure plus a long stall every 1500 cycles + logic [63:0] r_res [MAXOPS]; + integer nres = 0, sink_cyc = 0; + always @(negedge clk) begin + sink_cyc <= sink_cyc + 1; + out_ready <= ((sink_cyc % 1500) >= 300) && (rnd(3) != 0); + end + always @(posedge clk) begin + if (!reset && out_valid && out_ready) begin + for (int i = 0; i < 2; i++) r_res[nres + i] <= out_data[64*i +: 64]; + nres <= nres + 2; + end + end + + // Watchdog: fail fast instead of hanging if results stop arriving. + // Armed only while the stream runs; any new result beat resets it. + integer stall = 0, last_nres = 0; + bit streaming = 0; + always @(posedge clk) begin + if (!reset) begin + if (!streaming || nres != last_nres) stall <= 0; else stall <= stall + 1; + last_nres <= nres; + if (stall > 20000) $fatal(1, "core stalled: no result beat for 20000 cycles"); + end + end + + task automatic run_stream(); + integer n = 0; + streaming = 1; + while (n < ops) begin + @(negedge clk); + if (rnd(10) == 0) in_valid = 1'b0; + else begin + in_valid = 1'b1; + in_data = {s_src[n], s_len[n], s_fn[n], s_dry[n], s_op[n], s_bit[n]}; + if (in_ready) n++; // in_ready is stable until the edge + end + end + @(negedge clk); in_valid = 1'b0; + while (nres < ops) @(negedge clk); + streaming = 0; + endtask + + task automatic host_write(input integer w, input logic [63:0] v); + @(negedge clk); + host_word = w; host_wdata = v; host_write_stb = 1'b1; + @(negedge clk); host_write_stb = 1'b0; + while (host_busy) @(negedge clk); + endtask + + task automatic host_read(input integer w, output logic [63:0] v); + integer waited = 0; + @(negedge clk); + host_word = w; host_read_stb = 1'b1; + @(negedge clk); host_read_stb = 1'b0; + while (!host_rdata_valid && waited < 100) begin @(negedge clk); waited++; end + check(host_rdata_valid, $sformatf("host read of word %0d completes", w)); + v = host_rdata; + endtask + + // ------------------------------------------------------------ main + logic [63:0] v; + integer pend_i = 0; + integer kind, n_hot = 0, n_any = 0, n_past = 0, n_wrap = 0, n_undef = 0, n_range = 0; + initial begin + if (!$value$plusargs("seed=%d", seed)) seed = 3; + if (!$value$plusargs("ops=%d", ops)) ops = 3200; + ops = (ops / 2) * 2; + seed0 = seed; + for (int w = 0; w < WORDS; w++) begin init[w] = {rnd32(), rnd32()}; gmem[w] = init[w]; end + for (int n = 0; n < ops; n++) begin + kind = ((n % 24) < 16) ? 0 : 40 + rnd(60); + s_op[n] = (rnd(12) == 0) ? 4'(5 + rnd(3)) : 4'(rnd(5)); + s_len[n] = '0; s_src[n] = '0; s_fn[n] = '0; s_dry[n] = 1'b0; + if (s_op[n] > 4'd4) n_undef++; + if (pend_i < rs_q_n) begin // rest of a MATCH sequence + n_range++; + s_op[n] = rs_q_op[pend_i]; s_bit[n] = rs_q_a[pend_i]; s_len[n] = rs_q_len[pend_i]; + s_src[n] = rs_q_src[pend_i]; s_fn[n] = rs_q_fn[pend_i]; pend_i++; + end else if (kind >= 40 && rnd(3) == 0) begin + n_range++; + if (rnd(4) == 0) begin + gen_match_seq(); + s_op[n] = rs_q_op[0]; s_bit[n] = rs_q_a[0]; s_len[n] = rs_q_len[0]; + s_src[n] = rs_q_src[0]; s_fn[n] = rs_q_fn[0]; pend_i = 1; + end else + gen_range(s_op[n], s_bit[n], s_len[n], s_src[n], s_fn[n], s_dry[n]); + end else if (kind < 40) begin n_hot++; s_bit[n] = 64'(rnd(4) * LANES) * 64 + rnd(64); end + else if (kind < 80) begin n_any++; s_bit[n] = rnd(WORDS * 64); end + else if (kind < 95) begin n_past++; s_bit[n] = WORDS * 64 + rnd(1024); end + else begin n_wrap++; s_bit[n] = {rnd32(), rnd32()}; end + end + $display("stimulus: hot=%0d anywhere=%0d past_end=%0d wrap=%0d undefined_op=%0d range=%0d", + n_hot, n_any, n_past, n_wrap, n_undef, n_range); + + repeat (3) @(negedge clk); + reset = 1'b0; + for (int w = 0; w < WORDS; w++) host_write(w, init[w]); + for (int w = 0; w < WORDS; w++) begin + host_read(w, v); + check(v === init[w], $sformatf("host read back word %0d", w)); + end + + fork + run_gold(); + run_stream(); + join + + for (int n = 0; n < ops; n++) begin + check(r_res[n][7:2] == 6'b101000, $sformatf("result %0d tag %h", n, r_res[n])); + check(r_res[n][1] === g_err_r[n], + $sformatf("result %0d (op %0d, a %h, len %0d): error %b, reference %b", + n, s_op[n], s_bit[n], s_len[n], r_res[n][1], g_err_r[n])); + if (!g_err_r[n]) + check(r_res[n][0] === g_bit_r[n] && r_res[n][63:8] === g_val_r[n], + $sformatf("result %0d (op %0d, a %h, len %0d, src %0d, fn %0d): bit %b value %0d, reference %b %0d", + n, s_op[n], s_bit[n], s_len[n], s_src[n], s_fn[n], + r_res[n][0], r_res[n][63:8], g_bit_r[n], g_val_r[n])); + else + check(r_res[n][0] === 1'b0 && r_res[n][63:8] === '0, $sformatf("result %0d: error has bit 0, value 0", n)); + range_cover(s_op[n], s_bit[n], s_len[n], s_src[n], s_fn[n], s_dry[n], g_err_r[n], g_bit_r[n]); + end + range_cover_check(); + repeat (5) @(negedge clk); + check(idle, "core idle after the stream"); + check(ops_accepted == ops, $sformatf("ops_accepted %0d", ops_accepted)); + check(results_sent == ops, $sformatf("results_sent %0d", results_sent)); + for (int w = 0; w < WORDS; w++) begin + host_read(w, v); + check(v === gmem[w], $sformatf("final word %0d: %h, reference %h", w, v, gmem[w])); + end + + $display("checks=%0d fails=%0d lanes=%0d words=%0d ops=%0d seed=%0d", checks, fails, LANES, WORDS, ops, seed0); + if (fails != 0) $fatal(1, "FAIL"); + $display("PASS: PCIe core matches the reference core"); + $finish; + end + + initial begin + #2_000_000_000; + $fatal(1, "global timeout"); + end +endmodule diff --git a/pcie/timing/run.sh b/pcie/timing/run.sh new file mode 100644 index 0000000000000000000000000000000000000000..d4c04edb109ace4ebce51a345031d322860b87ba --- /dev/null +++ b/pcie/timing/run.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# run.sh LANES WORDS_PER_LANE WITH_MATCH [MATCH_LANES]: synthesize timing_top and place & route it +# for the XC7A100T-FGG484-2 at 125 MHz with nextpnr-xilinx; prints utilisation and Fmax. +set -euo pipefail +L=$1; W=$2; M=$3; ML=${4:-$1}; tag=${L}x${W}_m${M}_u${ML} +NEXTPNR=${NEXTPNR:-nextpnr-xilinx}; CHIPDB=${CHIPDB:?set CHIPDB to the xc7a100t chip database} +mkdir -p build +yosys -q -l build/synth_$tag.log -p "read_verilog -sv ../rtl/bitacc_engine.sv ../rtl/bitacc_pcie_core.sv timing_top.sv; \ + chparam -set LANES $L -set WORDS_PER_LANE $W -set WITH_MATCH $M -set MATCH_LANES $ML timing_top; \ + synth_xilinx -flatten -nowidelut -abc9 -arch xc7 -top timing_top; tee -q -o build/stat_$tag.txt stat; \ + write_json build/$tag.json" > /dev/null 2>&1 +$NEXTPNR --chipdb "$CHIPDB" --xdc timing_top.xdc --json build/$tag.json --freq 125 \ + --report build/report_$tag.json > build/pnr_$tag.log 2>&1 || { tail -20 build/pnr_$tag.log; exit 1; } +grep -E "SLICE_LUTX:|SLICE_FFX:|RAMB36E1:" build/pnr_$tag.log | tail -3 +grep "Max frequency" build/pnr_$tag.log | tail -1 diff --git a/pcie/timing/timing_top.sv b/pcie/timing/timing_top.sv new file mode 100644 index 0000000000000000000000000000000000000000..c04e7a3d4ca65e37e385e3b884f688dee0307a0a --- /dev/null +++ b/pcie/timing/timing_top.sv @@ -0,0 +1,47 @@ +// Register-bounded wrapper for timing bitacc_pcie_core with the open-source +// Artix-7 flow (pcie/timing/Makefile). The core's ~470 I/O bits do not fit a +// package, and in the SoC they connect to LitePCIe registers anyway, so here +// every input comes from a register (a serial shift chain) and every output +// goes into a register (XOR-folded to a few pins). The register-to-register +// paths inside the core are the ones that matter for 125 MHz. +module timing_top #( + parameter int LANES = 8, + parameter int WORDS_PER_LANE = 2048, + parameter bit WITH_MATCH = 1'b1, + parameter int MATCH_LANES = LANES +)( + input logic clk, + input logic rst, + input logic sin, + input logic [3:0] ctl, + output logic [7:0] sout +); + localparam int WW = $clog2(LANES * WORDS_PER_LANE); + + logic [191+WW:0] sh; + logic r_rst, in_valid, out_ready, wstb, rstb; + always_ff @(posedge clk) begin + sh <= {sh[190+WW:0], sin}; + r_rst <= rst; + {in_valid, out_ready, wstb, rstb} <= ctl; + end + + logic in_ready, out_valid, host_rdata_valid, host_busy, idle; + logic [127:0] out_data; + logic [63:0] host_rdata; + logic [31:0] ops_accepted, results_sent; + + bitacc_pcie_core #(.LANES(LANES), .WORDS_PER_LANE(WORDS_PER_LANE), .WITH_MATCH(WITH_MATCH), + .MATCH_LANES(MATCH_LANES)) core ( + .clk, .reset(r_rst), + .in_valid, .in_ready, .in_data(sh[127:0]), + .out_valid, .out_ready, .out_data, + .host_word(sh[192 +: WW]), .host_wdata(sh[191:128]), + .host_write_stb(wstb), .host_read_stb(rstb), + .host_rdata, .host_rdata_valid, .host_busy, + .idle, .ops_accepted, .results_sent); + + always_ff @(posedge clk) + sout <= {^out_data[127:64], ^out_data[63:0], out_valid, in_ready, + ^host_rdata, host_rdata_valid, host_busy, idle ^ (^ops_accepted) ^ (^results_sent)}; +endmodule diff --git a/pcie/timing/timing_top.xdc b/pcie/timing/timing_top.xdc new file mode 100644 index 0000000000000000000000000000000000000000..1508ebaf47dbf506ff76686abc8a532d5042d1c9 --- /dev/null +++ b/pcie/timing/timing_top.xdc @@ -0,0 +1,31 @@ +# Pins are arbitrary bank-16 I/Os of the XC7A100T-FGG484; only internal timing is of interest. +set_property LOC C18 [get_ports clk] +set_property LOC A13 [get_ports rst] +set_property LOC A14 [get_ports sin] +set_property LOC A15 [get_ports ctl[0]] +set_property LOC A16 [get_ports ctl[1]] +set_property LOC A18 [get_ports ctl[2]] +set_property LOC A19 [get_ports ctl[3]] +set_property LOC A20 [get_ports sout[0]] +set_property LOC A21 [get_ports sout[1]] +set_property LOC B13 [get_ports sout[2]] +set_property LOC B15 [get_ports sout[3]] +set_property LOC B16 [get_ports sout[4]] +set_property LOC B17 [get_ports sout[5]] +set_property LOC B18 [get_ports sout[6]] +set_property LOC B20 [get_ports sout[7]] +set_property IOSTANDARD LVCMOS33 [get_ports clk] +set_property IOSTANDARD LVCMOS33 [get_ports rst] +set_property IOSTANDARD LVCMOS33 [get_ports sin] +set_property IOSTANDARD LVCMOS33 [get_ports ctl[0]] +set_property IOSTANDARD LVCMOS33 [get_ports ctl[1]] +set_property IOSTANDARD LVCMOS33 [get_ports ctl[2]] +set_property IOSTANDARD LVCMOS33 [get_ports ctl[3]] +set_property IOSTANDARD LVCMOS33 [get_ports sout[0]] +set_property IOSTANDARD LVCMOS33 [get_ports sout[1]] +set_property IOSTANDARD LVCMOS33 [get_ports sout[2]] +set_property IOSTANDARD LVCMOS33 [get_ports sout[3]] +set_property IOSTANDARD LVCMOS33 [get_ports sout[4]] +set_property IOSTANDARD LVCMOS33 [get_ports sout[5]] +set_property IOSTANDARD LVCMOS33 [get_ports sout[6]] +set_property IOSTANDARD LVCMOS33 [get_ports sout[7]] diff --git a/rtl/bit_accelerator.sv b/rtl/bit_accelerator.sv new file mode 100644 index 0000000000000000000000000000000000000000..55abb6e554fa371c185c02ce6d363c4677e75047 --- /dev/null +++ b/rtl/bit_accelerator.sv @@ -0,0 +1,99 @@ +// Bit-string accelerator: one GET/TEST/SET/CLEAR/TOGGLE per operation on a +// 64-bit-word memory. See docs/verification.md for the interface contract. +// +// effective_bit = (base_address << 3) + bit_offset (mod 2^64) +// mem_addr = (effective_bit >> 6) << 3 (8-byte aligned) +// bit index = effective_bit[5:0] (LSB-first) +// +// result_valid is a one-cycle pulse; error and result_bit are valid in that +// same cycle. On error, result_bit holds its previous value. +module bit_accelerator #( + parameter int ADDR_W=64, + parameter int WORD_W=64 +)( + input logic clk,input logic reset, + input logic op_valid, output logic op_ready, + input logic [ADDR_W-1:0] base_address, + input logic [63:0] bit_offset, + input logic [2:0] operation, + output logic result_valid, output logic result_bit, output logic error, + output logic mem_valid, output logic mem_write, + output logic [ADDR_W-1:0] mem_addr, + output logic [WORD_W-1:0] mem_wdata, + output logic [WORD_W/8-1:0] mem_wstrb, + input logic mem_ready, input logic mem_rvalid, + input logic [WORD_W-1:0] mem_rdata, input logic mem_fault +); + localparam logic [2:0] OP_GET=3'b000, OP_TEST=3'b001, OP_SET=3'b010, + OP_CLEAR=3'b011, OP_TOGGLE=3'b100; + typedef enum logic [2:0] {IDLE,READ_REQ,READ_WAIT,WRITE_REQ,WRITE_WAIT,RESP} state_t; + state_t state; + logic [ADDR_W-1:0] req_addr; + logic [63:0] eff_bit; + logic [5:0] bit_idx_q; + logic err_q; // error latched during the operation, reported with result_valid + logic [2:0] op_q; + logic old_bit_q; + logic [WORD_W-1:0] new_word_q; + logic [WORD_W-1:0] shifted_one; + + assign op_ready = (state == IDLE) && !reset; + assign mem_valid = (state == READ_REQ || state == WRITE_REQ); + assign mem_write = (state == WRITE_REQ); + assign mem_addr = req_addr; + assign mem_wdata = new_word_q; + assign mem_wstrb = {WORD_W/8{1'b1}}; + assign shifted_one = ({{(WORD_W-1){1'b0}},1'b1} << bit_idx_q); + assign eff_bit = (64'(base_address) << 3) + bit_offset; + + always_ff @(posedge clk) begin + if (reset) begin + state <= IDLE; result_valid <= 1'b0; result_bit <= 1'b0; error <= 1'b0; + req_addr <= '0; bit_idx_q <= '0; err_q <= 1'b0; + op_q <= OP_GET; old_bit_q <= 1'b0; new_word_q <= '0; + end else begin + result_valid <= 1'b0; error <= 1'b0; + case (state) + IDLE: if (op_valid) begin + op_q <= operation; + err_q <= 1'b0; + bit_idx_q <= eff_bit[5:0]; + req_addr <= ADDR_W'((eff_bit >> 6) << 3); + state <= READ_REQ; + end + READ_REQ: if (mem_ready) state <= READ_WAIT; + READ_WAIT: if (mem_fault) begin err_q <= 1'b1; state <= RESP; end + else if (mem_rvalid) begin + old_bit_q <= mem_rdata[bit_idx_q]; + case (op_q) + OP_GET, OP_TEST: state <= RESP; + OP_SET: begin new_word_q <= mem_rdata | shifted_one; state <= WRITE_REQ; end + OP_CLEAR: begin new_word_q <= mem_rdata & ~shifted_one; state <= WRITE_REQ; end + OP_TOGGLE: begin new_word_q <= mem_rdata ^ shifted_one; state <= WRITE_REQ; end + default: begin err_q <= 1'b1; state <= RESP; end + endcase + end + WRITE_REQ: if (mem_ready) state <= WRITE_WAIT; + WRITE_WAIT: begin + if (mem_fault) err_q <= 1'b1; + state <= RESP; + end + RESP: begin + result_valid <= 1'b1; + error <= err_q; + if (!err_q) begin + case (op_q) + OP_GET, OP_TEST: result_bit <= old_bit_q; + OP_SET: result_bit <= 1'b1; + OP_CLEAR: result_bit <= 1'b0; + OP_TOGGLE: result_bit <= ~old_bit_q; + default: result_bit <= 1'b0; + endcase + end + state <= IDLE; + end + default: state <= IDLE; + endcase + end + end +endmodule diff --git a/spice/bit_extract.sp b/spice/bit_extract.sp new file mode 100644 index 0000000000000000000000000000000000000000..4dbbb8305fd9274fdb980c4cdcb3a9053e001760 --- /dev/null +++ b/spice/bit_extract.sp @@ -0,0 +1,14 @@ +* Behavioral SPICE timing model for one-bit extraction datapath +.param VDD=1.0 +Vdd vdd 0 {VDD} +* Address adder / decoder / mux / extractor modeled as cascaded RC stages. +Raddr in addr 1k +Caddr addr 0 20f +Rsel addr sel 1k +Csel sel 0 25f +Rext sel out 800 +Cout out 0 20f +Vin in 0 PULSE(0 {VDD} 0 5p 5p 500p 1n) +.tran 1p 5n +.measure tran TPD TRIG v(in) VAL='VDD/2' RISE=1 TARG v(out) VAL='VDD/2' RISE=1 +.end diff --git a/tapeout/.github/workflows/docs.yaml b/tapeout/.github/workflows/docs.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2690b308db1a50265f46aba7d045793371f34824 --- /dev/null +++ b/tapeout/.github/workflows/docs.yaml @@ -0,0 +1,17 @@ +name: docs + +on: + push: + workflow_dispatch: + +jobs: + docs: + runs-on: ubuntu-24.04 + steps: + - name: Checkout repo + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Build docs + uses: TinyTapeout/tt-gds-action/docs@ttsky26d diff --git a/tapeout/.github/workflows/fpga.yaml b/tapeout/.github/workflows/fpga.yaml new file mode 100644 index 0000000000000000000000000000000000000000..22c812d35f2aa7260cb1669d926971bf0be7632b --- /dev/null +++ b/tapeout/.github/workflows/fpga.yaml @@ -0,0 +1,19 @@ +name: fpga + +on: + push: + # Comment out (or remove) the following line to run the FPGA workflow on every push: + branches: none + workflow_dispatch: + +jobs: + fpga: + runs-on: ubuntu-24.04 + steps: + - name: checkout repo + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: FPGA bitstream for TT ASIC Sim (ICE40UP5K) + uses: TinyTapeout/tt-gds-action/fpga/ice40up5k@ttsky26d diff --git a/tapeout/.github/workflows/gds.yaml b/tapeout/.github/workflows/gds.yaml new file mode 100644 index 0000000000000000000000000000000000000000..0bb3c975cb67c4b0b5bb5feb9cad75d3ef87f461 --- /dev/null +++ b/tapeout/.github/workflows/gds.yaml @@ -0,0 +1,47 @@ +name: gds + +on: + push: + workflow_dispatch: + +jobs: + gds: + runs-on: ubuntu-24.04 + steps: + - name: checkout repo + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Build GDS + uses: TinyTapeout/tt-gds-action@ttsky26d + with: + pdk: sky130A + + precheck: + needs: gds + runs-on: ubuntu-24.04 + steps: + - name: Run Tiny Tapeout Precheck + uses: TinyTapeout/tt-gds-action/precheck@ttsky26d + + gl_test: + needs: gds + runs-on: ubuntu-24.04 + steps: + - name: checkout repo + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: GL test + uses: TinyTapeout/tt-gds-action/gl_test@ttsky26d + + viewer: + needs: gds + runs-on: ubuntu-24.04 + permissions: + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source + steps: + - uses: TinyTapeout/tt-gds-action/viewer@ttsky26d diff --git a/tapeout/.github/workflows/test.yaml b/tapeout/.github/workflows/test.yaml new file mode 100644 index 0000000000000000000000000000000000000000..cf171b6728fe4509a056417e3b44ea19a184ac35 --- /dev/null +++ b/tapeout/.github/workflows/test.yaml @@ -0,0 +1,34 @@ +name: test +on: [push, workflow_dispatch] +jobs: + test: + runs-on: ubuntu-24.04 + steps: + - name: Checkout repo + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Install iverilog + shell: bash + run: sudo apt-get update && sudo apt-get install -y iverilog + + # The testbench is plain SystemVerilog (no cocotb); test/Makefile writes + # results.xml and exits non-zero on any failure. + - name: Run tests (divisor pins d=2, then default divisor) + run: | + cd test + make clean + make + ! grep -E '<(failure|error)' results.xml + make TB_DEFINES=-DTT_DEFAULT_BAUD + ! grep -E '<(failure|error)' results.xml + + - name: upload test results + if: success() || failure() + uses: actions/upload-artifact@v7 + with: + name: test-results + path: | + test/results.xml + test/sim_build/*/sim.log diff --git a/tapeout/.gitignore b/tapeout/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..66cd149b0a5feec16304569d2c64255e56fa287a --- /dev/null +++ b/tapeout/.gitignore @@ -0,0 +1,11 @@ +.DS_Store +*.vcd +*.fst +runs +tt +tt_submission +src/user_config.json +src/config_merged.json +test/sim_build +test/results.xml +test/gate_level_netlist.v diff --git a/tapeout/Makefile b/tapeout/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..84f4f1136f1d7fc4f17791414e55654add9c3ba0 --- /dev/null +++ b/tapeout/Makefile @@ -0,0 +1,50 @@ +# Tiny Tapeout submission (ttsky26d, sky130A). +# +# This directory has the layout of a Tiny Tapeout project repository +# (info.yaml, src/, test/, docs/, .github/workflows/). src/ and test/ hold +# copies of shared sources so the directory can be pushed as its own repository. +# +# make sync copy the shared RTL and testbench into src/ and test/ +# make check-sync fail if a copy differs from its original (CI runs this) +# make test UART testbench on the RTL (test/, divisor 2 and default) +# make harden GDS with LibreLane 3.0.14 via tt-support-tools (needs the +# LibreLane Nix shell; see README.md) +# make gl-test the same testbench on the hardened gate-level netlist + +SHARED := ../rtl/bit_accelerator.sv:src/bit_accelerator.sv \ + ../fpga/rtl/uart_rx.sv:src/uart_rx.sv \ + ../fpga/rtl/uart_tx.sv:src/uart_tx.sv \ + ../fpga/tb/tb_fpga_top.sv:test/tb_fpga_top.sv + +.PHONY: sync check-sync test harden gl-test clean + +sync: + @for p in $(SHARED); do cp $${p%%:*} $${p##*:}; echo "copied $${p%%:*} -> $${p##*:}"; done + +check-sync: + @for p in $(SHARED); do cmp -s $${p%%:*} $${p##*:} || { echo "$${p##*:} differs from $${p%%:*}; run make sync"; exit 1; }; done + @echo "src/ and test/ match their originals" + +test: check-sync + $(MAKE) -C test + $(MAKE) -C test TB_DEFINES=-DTT_DEFAULT_BAUD + +TT_TOOLS ?= tt +TT_TOOLS_REF ?= main + +$(TT_TOOLS): + git clone --depth 1 -b $(TT_TOOLS_REF) https://github.com/TinyTapeout/tt-support-tools $@ + +harden: check-sync | $(TT_TOOLS) + ./$(TT_TOOLS)/tt_tool.py --create-user-config + ./$(TT_TOOLS)/tt_tool.py --harden --no-docker + ./$(TT_TOOLS)/tt_tool.py --print-stats + ./$(TT_TOOLS)/tt_tool.py --print-warnings + +gl-test: + cp runs/wokwi/final/nl/tt_um_snapkittyagent9nova_bitacc.nl.v test/gate_level_netlist.v + $(MAKE) -C test GATES=yes + +clean: + rm -rf runs tt_submission src/user_config.json src/config_merged.json + $(MAKE) -C test clean diff --git a/tapeout/README.md b/tapeout/README.md new file mode 100644 index 0000000000000000000000000000000000000000..6c7e2ee66586a9c62b87730096a6f16a451f80fd --- /dev/null +++ b/tapeout/README.md @@ -0,0 +1,71 @@ +# Tiny Tapeout submission + +This directory is a complete [Tiny Tapeout](https://tinytapeout.com) project for +the **ttsky26d** shuttle (SkyWater sky130A). It is laid out like Tiny Tapeout's +`ttsky-verilog-template` repository, so its contents can be pushed as the root +of the submission repository unchanged. + +| | | +|---|---| +| Top module | `tt_um_snapkittyagent9nova_bitacc` | +| Size | 2×2 tiles | +| Design | `bit_accelerator` core + 4 × 64-bit RAM + UART (`src/`) | +| Datasheet | [`docs/info.md`](docs/info.md) | +| Flow | LibreLane 3.0.14 + tt-support-tools, the versions `tt-gds-action@ttsky26d` uses | + +`src/bit_accelerator.sv`, `src/uart_rx.sv`, `src/uart_tx.sv` and +`test/tb_fpga_top.sv` are copies of `../rtl/` and `../fpga/` sources. +`make check-sync` (run by the monorepo CI) fails if they drift; `make sync` +refreshes them. + +## Results + +Hardened locally with the same tool versions as Tiny Tapeout's action. The +details are in [`SIGNOFF.md`](SIGNOFF.md): +- DRC, LVS and antenna clean. +- Setup and hold met at all 9 corners. +- The Tiny Tapeout precheck passes. +- The post-layout gate-level test passes (2,183 checks). +- Known residual: max-slew warnings, documented with their cause. + +## Testing + +`make test` runs the shared UART testbench (`test/tb_fpga_top.sv`) on the RTL +twice: with divisor pins d = 2, and with the default divisor (all pins low). +The testbench is plain SystemVerilog under Icarus Verilog. It writes +`results.xml`, which is what Tiny Tapeout's gate-level action checks, so no +cocotb or Python is involved. `make gl-test` runs the same testbench on the +post-layout netlist. + +The host tool also passes its acceptance test against this top level through a +pseudo-terminal (`../fpga/sim/uart_pty.cpp` built with `-DTT_TOP`): +`bitacc --words 4 selftest 3000` reports 0 mismatches. + +## Hardening locally + +Tiny Tapeout's action runs LibreLane in Docker. Without Docker, use LibreLane's +Nix shell: + +```sh +git clone -b 3.0.14 https://github.com/librelane/librelane && cd librelane +nix develop # OpenROAD, Magic, KLayout, Netgen, Yosys, LibreLane 3.0.14 +cd && make harden +``` + +`tt_tool.py` expects to run from the root of a git repository with a remote, +which the submission repository is. + +## Submitting (owner's steps) + +Submitting creates a public repository under your account and buys silicon +area, so these steps are yours: + +1. Create a repository from + (for example `tt-bit-string-accelerator`). Replace its contents with this + directory and push. Enable GitHub Pages (Settings → Pages → GitHub Actions). +2. Check that the `gds`, `test` and `docs` actions pass. `gds` runs hardening, + precheck and the gate-level test on GitHub's runners. +3. Buy a 2×2 slot (4 tiles) on the ttsky26d shuttle at and + submit the repository there before the shuttle's deadline. +4. When the chips arrive, test the design with a USB-UART adapter on + `ui[3]`/`uo[4]` and `bitacc --words 4 selftest`. See `docs/info.md`. diff --git a/tapeout/SIGNOFF.md b/tapeout/SIGNOFF.md new file mode 100644 index 0000000000000000000000000000000000000000..62b9dffd5b1b63c423ad1cfe5b6bc73df34441a4 --- /dev/null +++ b/tapeout/SIGNOFF.md @@ -0,0 +1,46 @@ +# Signoff report — tt_um_snapkittyagent9nova_bitacc + +Hardened locally with the same tools and versions as `tt-gds-action@ttsky26d`. +The GDS has not been fabricated, and has not been through Tiny Tapeout's own +CI. That happens when the submission repository is pushed. + +| | | +|---|---| +| Flow | LibreLane 3.0.14 (Nix), tt-support-tools `main` @ 01d5d28, sky130A @ 8afc834 (ciel) | +| Size | 2×2 tiles, die 75,602 µm², core 72,565 µm² | +| Cells | 4,821 standard cells (12,927 instances with fill and tap), 516 hold buffers | +| Utilization | 62.9% | +| Clock target | 20 ns (50 MHz); the UART divisor pins adapt to the board clock | +| Power (nominal, from STA) | 2.98 mW | + +## Checks + +| Check | Result | +|---|---| +| Magic DRC | 0 errors | +| Routing DRC | 0 errors | +| LVS (Netgen) | 0 errors, 0 device differences | +| Antenna | 0 violating nets, 0 violating pins | +| Setup, all 9 PVT corners | met; worst slack +1.95 ns (max_ss_100C_1v60), +10.12 ns at nom_tt | +| Hold, all 9 PVT corners | met; worst slack +0.105 ns (min_ff_n40C_1v95) | +| Tiny Tapeout precheck (KLayout 0.30.8, Magic 8.3.568 as pinned by the precheck) | **passed**: Magic DRC, KLayout FEOL/BEOL/off-grid/pin-label/zero-area, top macro name, forbidden layers, prBoundary, pins, n-well, Verilog syntax | +| Gate-level simulation of the powered post-layout netlist (`test/`, Icarus 12, sky130 cell models) | 2,183 checks, 0 failures | + +## Known residuals + +| Item | Count | Assessment | +|---|---|---| +| Max slew violations | 121 at nom_tt, 897 at max_ss, 11 at nom_ff | Nets driven by resizer-inserted fanout buffers (`clkdlybuf4s25_1`) reach ~1.0 ns transitions against a 0.75 ns limit. Setup and hold are met with these slews included, but delays beyond the library's characterized range are extrapolated. Excluding `clkdlybuf4s*` from repair made it worse (274 at nom_tt, 12 cap violations), so that run was discarded. | +| Max fanout violations | 33 | 32 are clock-tree leaf buffers driving 12 flip-flops against the generic limit of 10; one data net has fanout 11. | +| Max capacitance violations | 3 (max_ss), 2 (nom_ss), 0 at tt/ff | Slow corners only. | +| Disconnected pins | 9 | `uio_in[7:0]` and `ena`, which the design does not use (0 connections in the netlist). | +| Floating nets during repair | 2 | `VPWR`/`VGND` before power connection. | + +Tiny Tapeout's precheck does not gate on slew, fanout or capacitance; they are +reported here because they affect timing accuracy, not functionality. + +## Reproduce + +See [`README.md`](README.md), "Hardening locally". The run produced +`runs/wokwi/final/` and `tt_submission/`; neither is committed (Tiny Tapeout's +action regenerates them). diff --git a/tapeout/docs/info.md b/tapeout/docs/info.md new file mode 100644 index 0000000000000000000000000000000000000000..1e8b8d66e73c5eebc9b5c8f7bd89db3af146e711 --- /dev/null +++ b/tapeout/docs/info.md @@ -0,0 +1,74 @@ +## How it works + +A small hardware accelerator for single-bit operations on 64-bit words. It +computes the bit's address and performs a read-modify-write when needed. + + effective bit = (base * 8) + offset (64-bit arithmetic) + word = effective bit / 64 + bit index = effective bit mod 64 + +| Opcode | Operation | Writes | Result bit | +|---|---|---|---| +| 0 | GET | no | the bit | +| 1 | TEST | no | the bit | +| 2 | SET | yes | 1 | +| 3 | CLEAR | yes | 0 | +| 4 | TOGGLE | yes | the new bit | +| 5-7 | undefined | no | error | + +The chip holds 4 words of RAM (byte addresses 0-31). An address outside that +range ends the operation with an error and never writes. + +Everything is driven over a UART (8 data bits, no parity, 1 stop bit). All +multi-byte fields are little-endian: + +| Host sends | Chip replies | | +|---|---|---| +| `P` | `B` | ping | +| `W` idx data[8] | `K` | write word idx (0-3) | +| `R` idx | data[8] | read word idx | +| `X` op base[8] offset[8] | `0xA0` + 2·error + bit | run one operation | +| anything else | `?` | | + +The design is the `bit_accelerator` core from +https://github.com/SNAPKITTYAGENT9NOVA/bit-string-accelerator. Before +hardening it was checked as follows: +- a self-checking testbench, with random backpressure, read latency and fault injection +- Why3 proofs of the address arithmetic +- the same RTL running on iCE40 and ECP5 FPGAs +- a formal equivalence proof of its sky130 netlist + +## How to test + +1. Set the clock, and set the UART divisor pins to match it. The chip uses + `4 × d` clocks per bit, where `d = {ui[7:4], ui[2:0]}`. With all divisor + pins low it uses 104 clocks per bit, which is 115200 baud at a 12 MHz clock. + Other examples: + + | Clock | Baud | d | ui[7:4] | ui[2:0] | + |---|---|---|---|---| + | 12 MHz | 115200 | 0 (default) | 0000 | 000 | + | 25 MHz | 115200 | 54 (216 clocks) | 0110 | 110 | + | 50 MHz | 115200 | 108 (432 clocks) | 1101 | 100 | + +2. Connect a 3.3 V USB-UART adapter: adapter TX to `ui[3]` (RX), adapter RX to + `uo[4]` (TX). +3. Reset the design. `uo[0]` is high while an operation runs. `uo[1]` is the + last operation's error flag. `uo[2]` is its result bit. +4. Use the host tool from the repository (`fpga/host`, Rust): + + bitacc --port /dev/ttyUSB0 --words 4 ping # pong + bitacc --port /dev/ttyUSB0 --words 4 write 0 0x8000000000000001 + bitacc --port /dev/ttyUSB0 --words 4 exec get 0 63 # 1 + bitacc --port /dev/ttyUSB0 --words 4 exec toggle 0 5 # 1 + bitacc --port /dev/ttyUSB0 --words 4 read 0 # 0x8000000000000021 + bitacc --port /dev/ttyUSB0 --words 4 exec get 32 0 # error (outside RAM) + bitacc --port /dev/ttyUSB0 --words 4 selftest 10000 # 0 mismatches + +`selftest` runs random operations, including faults and undefined opcodes, +and checks every reply and the final RAM contents against a model of the +design. + +## External hardware + +A 3.3 V USB-UART adapter (for example an FTDI or CP2102 cable) on `ui[3]`/`uo[4]`. diff --git a/tapeout/info.yaml b/tapeout/info.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c8a40518aba3b915a21d4942cb589a36db1aea32 --- /dev/null +++ b/tapeout/info.yaml @@ -0,0 +1,59 @@ +# Tiny Tapeout project information +project: + title: "Bit-String Accelerator" + author: "Ahmad Ali Parr" + discord: "" + description: "Single-bit GET/TEST/SET/CLEAR/TOGGLE on 64-bit words over a UART" + language: "SystemVerilog" + clock_hz: 12000000 # Any clock works: set the UART divisor pins to match + + # How many tiles your design occupies? A single tile is about 167x108 uM. + tiles: "2x2" # Valid values: 1x1, 1x2, 2x2, 3x2, 4x2, 6x2 or 8x2 + + # Your top module name must start with "tt_um_". Make it unique by including your github username: + top_module: "tt_um_snapkittyagent9nova_bitacc" + + # List your project's source files here. + # Source files must be in ./src and you must list each source file separately, one per line. + # Don't forget to also update `PROJECT_SOURCES` in test/Makefile. + source_files: + - "bit_accelerator.sv" + - "uart_rx.sv" + - "uart_tx.sv" + - "tt_um_snapkittyagent9nova_bitacc.sv" + +# The pinout of your project. Leave unused pins blank. DO NOT delete or add any pins. +# This section is for the datasheet/website. Use descriptive names (e.g., RX, TX, MOSI, SCL, SEG_A, etc.). +pinout: + # Inputs + ui[0]: "DIV0" + ui[1]: "DIV1" + ui[2]: "DIV2" + ui[3]: "RX" + ui[4]: "DIV3" + ui[5]: "DIV4" + ui[6]: "DIV5" + ui[7]: "DIV6" + + # Outputs + uo[0]: "BUSY" + uo[1]: "ERROR" + uo[2]: "RESULT" + uo[3]: "" + uo[4]: "TX" + uo[5]: "" + uo[6]: "" + uo[7]: "" + + # Bidirectional pins + uio[0]: "" + uio[1]: "" + uio[2]: "" + uio[3]: "" + uio[4]: "" + uio[5]: "" + uio[6]: "" + uio[7]: "" + +# Do not change! +yaml_version: 6 diff --git a/tapeout/src/bit_accelerator.sv b/tapeout/src/bit_accelerator.sv new file mode 100644 index 0000000000000000000000000000000000000000..55abb6e554fa371c185c02ce6d363c4677e75047 --- /dev/null +++ b/tapeout/src/bit_accelerator.sv @@ -0,0 +1,99 @@ +// Bit-string accelerator: one GET/TEST/SET/CLEAR/TOGGLE per operation on a +// 64-bit-word memory. See docs/verification.md for the interface contract. +// +// effective_bit = (base_address << 3) + bit_offset (mod 2^64) +// mem_addr = (effective_bit >> 6) << 3 (8-byte aligned) +// bit index = effective_bit[5:0] (LSB-first) +// +// result_valid is a one-cycle pulse; error and result_bit are valid in that +// same cycle. On error, result_bit holds its previous value. +module bit_accelerator #( + parameter int ADDR_W=64, + parameter int WORD_W=64 +)( + input logic clk,input logic reset, + input logic op_valid, output logic op_ready, + input logic [ADDR_W-1:0] base_address, + input logic [63:0] bit_offset, + input logic [2:0] operation, + output logic result_valid, output logic result_bit, output logic error, + output logic mem_valid, output logic mem_write, + output logic [ADDR_W-1:0] mem_addr, + output logic [WORD_W-1:0] mem_wdata, + output logic [WORD_W/8-1:0] mem_wstrb, + input logic mem_ready, input logic mem_rvalid, + input logic [WORD_W-1:0] mem_rdata, input logic mem_fault +); + localparam logic [2:0] OP_GET=3'b000, OP_TEST=3'b001, OP_SET=3'b010, + OP_CLEAR=3'b011, OP_TOGGLE=3'b100; + typedef enum logic [2:0] {IDLE,READ_REQ,READ_WAIT,WRITE_REQ,WRITE_WAIT,RESP} state_t; + state_t state; + logic [ADDR_W-1:0] req_addr; + logic [63:0] eff_bit; + logic [5:0] bit_idx_q; + logic err_q; // error latched during the operation, reported with result_valid + logic [2:0] op_q; + logic old_bit_q; + logic [WORD_W-1:0] new_word_q; + logic [WORD_W-1:0] shifted_one; + + assign op_ready = (state == IDLE) && !reset; + assign mem_valid = (state == READ_REQ || state == WRITE_REQ); + assign mem_write = (state == WRITE_REQ); + assign mem_addr = req_addr; + assign mem_wdata = new_word_q; + assign mem_wstrb = {WORD_W/8{1'b1}}; + assign shifted_one = ({{(WORD_W-1){1'b0}},1'b1} << bit_idx_q); + assign eff_bit = (64'(base_address) << 3) + bit_offset; + + always_ff @(posedge clk) begin + if (reset) begin + state <= IDLE; result_valid <= 1'b0; result_bit <= 1'b0; error <= 1'b0; + req_addr <= '0; bit_idx_q <= '0; err_q <= 1'b0; + op_q <= OP_GET; old_bit_q <= 1'b0; new_word_q <= '0; + end else begin + result_valid <= 1'b0; error <= 1'b0; + case (state) + IDLE: if (op_valid) begin + op_q <= operation; + err_q <= 1'b0; + bit_idx_q <= eff_bit[5:0]; + req_addr <= ADDR_W'((eff_bit >> 6) << 3); + state <= READ_REQ; + end + READ_REQ: if (mem_ready) state <= READ_WAIT; + READ_WAIT: if (mem_fault) begin err_q <= 1'b1; state <= RESP; end + else if (mem_rvalid) begin + old_bit_q <= mem_rdata[bit_idx_q]; + case (op_q) + OP_GET, OP_TEST: state <= RESP; + OP_SET: begin new_word_q <= mem_rdata | shifted_one; state <= WRITE_REQ; end + OP_CLEAR: begin new_word_q <= mem_rdata & ~shifted_one; state <= WRITE_REQ; end + OP_TOGGLE: begin new_word_q <= mem_rdata ^ shifted_one; state <= WRITE_REQ; end + default: begin err_q <= 1'b1; state <= RESP; end + endcase + end + WRITE_REQ: if (mem_ready) state <= WRITE_WAIT; + WRITE_WAIT: begin + if (mem_fault) err_q <= 1'b1; + state <= RESP; + end + RESP: begin + result_valid <= 1'b1; + error <= err_q; + if (!err_q) begin + case (op_q) + OP_GET, OP_TEST: result_bit <= old_bit_q; + OP_SET: result_bit <= 1'b1; + OP_CLEAR: result_bit <= 1'b0; + OP_TOGGLE: result_bit <= ~old_bit_q; + default: result_bit <= 1'b0; + endcase + end + state <= IDLE; + end + default: state <= IDLE; + endcase + end + end +endmodule diff --git a/tapeout/src/config.json b/tapeout/src/config.json new file mode 100644 index 0000000000000000000000000000000000000000..db4188882fa366a97ef53b9de0670438329dbedf --- /dev/null +++ b/tapeout/src/config.json @@ -0,0 +1,74 @@ +{ + "//": "DO NOT EDIT THIS FILE before reading the comments below:", + + "//": "This is the default configuration for Tiny Tapeout projects. It should fit most designs.", + "//": "If you change it, please make sure you understand what you are doing. We are not responsible", + "//": "if your project fails because of a bad configuration.", + + "//": "!!! DO NOT EDIT THIS FILE unless you know what you are doing !!!", + + "//": "If you get stuck with this config, please open an issue or get in touch via the discord.", + + "//": "Here are some of the variables you may want to change:", + + "//": "PL_TARGET_DENSITY_PCT - You can increase this if Global Placement fails with error GPL-0302.", + "//": "Users have reported that values up to 80 worked well for them.", + "PL_TARGET_DENSITY_PCT": 60, + + "//": "CLOCK_PERIOD - Increase this in case you are getting setup time violations.", + "//": "The value is in nanoseconds, so 20ns == 50MHz.", + "CLOCK_PERIOD": 20, + + "//": "Hold slack margin - Increase them in case you are getting hold violations.", + "PL_RESIZER_HOLD_SLACK_MARGIN": 0.1, + "GRT_RESIZER_HOLD_SLACK_MARGIN": 0.05, + + "//": "RUN_LINTER, LINTER_INCLUDE_PDK_MODELS - Disabling the linter is not recommended!", + "RUN_LINTER": 1, + "LINTER_INCLUDE_PDK_MODELS": 1, + + "//": "If you need a custom clock configuration, read the following documentation first:", + "//": "https://tinytapeout.com/faq/#how-can-i-map-an-additional-external-clock-to-one-of-the-gpios", + "CLOCK_PORT": "clk", + + "//": "Configuration docs: https://librelane.readthedocs.io/en/latest/reference/configuration.html", + + "//": "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", + "//": "!!! DO NOT CHANGE ANYTHING BELOW THIS POINT !!!", + "//": "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", + + "//": "Save some time", + "RUN_KLAYOUT_XOR": 0, + "RUN_KLAYOUT_DRC": 0, + + "//": "Don't put clock buffers on the outputs", + "DESIGN_REPAIR_BUFFER_OUTPUT_PORTS": 0, + + "//": "Reduce wasted space", + "TOP_MARGIN_MULT": 1, + "BOTTOM_MARGIN_MULT": 1, + "LEFT_MARGIN_MULT": 6, + "RIGHT_MARGIN_MULT": 6, + + "//": "Absolute die size", + "FP_SIZING": "absolute", + + "GRT_ALLOW_CONGESTION": 1, + + "FP_IO_HLENGTH": 2, + "FP_IO_VLENGTH": 2, + + "FP_PDN_VPITCH": 38.87, + + "//": "Clock", + "RUN_CTS": 1, + + "//": "Don't generate power rings", + "FP_PDN_MULTILAYER": 0, + + "//": "MAGIC_DEF_LABELS may cause issues with LVS", + "MAGIC_DEF_LABELS": 0, + + "//": "Only export pin area in LEF (without any connected nets)", + "MAGIC_WRITE_LEF_PINONLY": 1 +} diff --git a/tapeout/src/tt_um_snapkittyagent9nova_bitacc.sv b/tapeout/src/tt_um_snapkittyagent9nova_bitacc.sv new file mode 100644 index 0000000000000000000000000000000000000000..9c2be3173cc882af176090d451828cc3502f6fb1 --- /dev/null +++ b/tapeout/src/tt_um_snapkittyagent9nova_bitacc.sv @@ -0,0 +1,163 @@ +// Tiny Tapeout top: bit_accelerator + 4 x 64-bit words of RAM, driven over a +// UART with the same protocol as fpga/rtl/fpga_top.sv (see fpga/README.md): +// +// 'P' -> 'B' ping +// 'W' idx[1] data[8] -> 'K' write word idx +// 'R' idx[1] -> data[8] read word idx +// 'X' op[1] base[8] offset[8] -> 0xA0 | error<<1 | bit run one operation +// anything else -> '?' +// +// Multi-byte fields are little-endian. The RAM holds byte addresses 0..31; any +// other address answers with mem_fault (error). idx is taken modulo 4. +// +// Pins: +// ui_in[3] UART RX (115200 8N1 at the default divisor) +// ui_in[7:4], ui_in[2:0] baud divisor d = {ui_in[7:4], ui_in[2:0]}: +// clocks per bit = 4*d, or 104 when d = 0 +// uo_out[4] UART TX +// uo_out[0] busy (operation in progress) +// uo_out[1] error (last operation) +// uo_out[2] result bit (last operation) +// +// Compared with fpga_top, operands are received straight into the registers +// the core reads, and read replies are streamed from the RAM, so no byte is +// stored twice. +module tt_um_snapkittyagent9nova_bitacc ( + input wire [7:0] ui_in, + output wire [7:0] uo_out, + input wire [7:0] uio_in, + output wire [7:0] uio_out, + output wire [7:0] uio_oe, + input wire ena, + input wire clk, + input wire rst_n +); + localparam int DEPTH = 4; + + logic reset; + assign reset = !rst_n; + + // ------------------------------------------------------------------ UART + logic [6:0] div; + logic [9:0] cpb; + assign div = {ui_in[7:4], ui_in[2:0]}; + assign cpb = (div == 7'd0) ? 10'd104 : {1'b0, div, 2'b00}; + + logic rx_valid, tx_start, tx_ready, tx; + logic [7:0] rx_data, tx_data; + + uart_rx u_rx (.clk, .reset, .cpb, .rx(ui_in[3]), .valid(rx_valid), .data(rx_data)); + uart_tx u_tx (.clk, .reset, .cpb, .start(tx_start), .data(tx_data), .ready(tx_ready), .tx); + + // ------------------------------------------------------------------ registers + typedef enum logic [2:0] {S_CMD, S_ARGS, S_HOST_WRITE, S_START, S_EXEC, S_SEND} state_t; + state_t state; + + logic [7:0] cmd; + logic [4:0] argi, nargs; + logic [2:0] arg0; // word index (W, R; low 2 bits) or opcode (X) + logic [63:0] base; // X: base address; W: data + logic [63:0] offset; // X: bit offset + logic [7:0] status; // one-byte reply + logic [3:0] replyi, nreply; + logic last_error, last_bit; + + // ------------------------------------------------------------------ core + logic op_ready, result_valid, result_bit, acc_error; + logic mem_valid, mem_write, mem_rvalid, mem_fault; + logic [63:0] mem_addr, mem_wdata, mem_rdata; + logic [7:0] mem_wstrb; + + bit_accelerator u_acc ( + .clk, .reset, + .op_valid(state == S_START), .op_ready, + .base_address(base), .bit_offset(offset), .operation(arg0[2:0]), + .result_valid, .result_bit, .error(acc_error), + .mem_valid, .mem_write, .mem_addr, .mem_wdata, .mem_wstrb, + .mem_ready(state == S_EXEC), .mem_rvalid, .mem_rdata, .mem_fault); + + // ------------------------------------------------------------------ RAM (flip-flops) + logic [63:0] ram [0:DEPTH-1]; + logic in_range; + logic [1:0] acc_idx; + assign in_range = (mem_addr[63:5] == '0); + assign acc_idx = mem_addr[4:3]; + assign mem_rdata = ram[acc_idx]; // address is held stable by the core + + always_ff @(posedge clk) begin + if (state == S_HOST_WRITE) ram[arg0[1:0]] <= base; + else if (state == S_EXEC && mem_valid && mem_write && in_range) ram[acc_idx] <= mem_wdata; + end + + // ------------------------------------------------------------------ reply stream + logic [63:0] rd_word; + assign rd_word = ram[arg0[1:0]]; + assign tx_data = (cmd == 8'h52) ? rd_word[{replyi[2:0], 3'b000} +: 8] : status; + assign tx_start = (state == S_SEND) && tx_ready && (replyi < nreply); + + // ------------------------------------------------------------------ control + always_ff @(posedge clk) begin + if (reset) begin + state <= S_CMD; cmd <= '0; argi <= '0; nargs <= '0; arg0 <= '0; + base <= '0; offset <= '0; status <= '0; replyi <= '0; nreply <= '0; + last_error <= 1'b0; last_bit <= 1'b0; mem_rvalid <= 1'b0; mem_fault <= 1'b0; + end else begin + mem_rvalid <= 1'b0; + mem_fault <= 1'b0; + case (state) + S_CMD: if (rx_valid) begin + cmd <= rx_data; argi <= '0; replyi <= '0; + case (rx_data) + 8'h50: begin status <= 8'h42; nreply <= 4'd1; state <= S_SEND; end // 'P' + 8'h57: begin nargs <= 5'd9; state <= S_ARGS; end // 'W' + 8'h52: begin nargs <= 5'd1; state <= S_ARGS; end // 'R' + 8'h58: begin nargs <= 5'd17; state <= S_ARGS; end // 'X' + default: begin status <= 8'h3F; nreply <= 4'd1; state <= S_SEND; end + endcase + end + S_ARGS: if (rx_valid) begin + // byte 0 -> arg0, bytes 1..8 -> base, bytes 9..16 -> offset + if (argi == 5'd0) arg0 <= rx_data[2:0]; + else if (argi <= 5'd8) base[{argi[2:0] - 3'd1, 3'b000} +: 8] <= rx_data; + else offset[{argi[2:0] - 3'd1, 3'b000} +: 8] <= rx_data; + argi <= argi + 1'b1; + if (argi + 1'b1 == nargs) begin + case (cmd) + 8'h57: state <= S_HOST_WRITE; + 8'h52: begin nreply <= 4'd8; state <= S_SEND; end + default: state <= S_START; + endcase + end + end + S_HOST_WRITE: begin // RAM written this cycle + status <= 8'h4B; nreply <= 4'd1; state <= S_SEND; + end + S_START: if (op_ready) state <= S_EXEC; // operands are already stable + S_EXEC: begin + if (mem_valid && !mem_write) begin + if (in_range) mem_rvalid <= 1'b1; else mem_fault <= 1'b1; + end + if (mem_valid && mem_write && !in_range) mem_fault <= 1'b1; + if (result_valid) begin + status <= 8'hA0 | {6'd0, acc_error, result_bit}; + last_error <= acc_error; + if (!acc_error) last_bit <= result_bit; + nreply <= 4'd1; state <= S_SEND; + end + end + S_SEND: begin + if (tx_start) replyi <= replyi + 1'b1; + else if (replyi == nreply && tx_ready) state <= S_CMD; + end + default: state <= S_CMD; + endcase + end + end + + assign uo_out = {3'b000, tx, 1'b0, last_bit, last_error, (state == S_START || state == S_EXEC)}; + assign uio_out = 8'h00; + assign uio_oe = 8'h00; + + logic _unused; + assign _unused = &{1'b0, ena, uio_in, mem_wstrb, mem_addr[2:0]}; +endmodule diff --git a/tapeout/src/uart_rx.sv b/tapeout/src/uart_rx.sv new file mode 100644 index 0000000000000000000000000000000000000000..33dcb2ada54ea01b5c8a0b717a0ef8423da6679c --- /dev/null +++ b/tapeout/src/uart_rx.sv @@ -0,0 +1,49 @@ +// 8N1 UART receiver. cpb = clocks per bit = clock frequency / baud rate (>= 4). +// valid pulses for one cycle with the received byte in data. +module uart_rx #( + parameter int CW = 10 // width of cpb +)( + input logic clk, + input logic reset, + input logic [CW-1:0] cpb, + input logic rx, + output logic valid, + output logic [7:0] data +); + logic [CW-1:0] FULL, HALF; + assign FULL = cpb - 1'b1; + assign HALF = cpb >> 1; + + logic rx_m, rx_s; // two-flop synchronizer + logic [1:0] state; // 0 idle, 1 start, 2 data, 3 stop + logic [CW-1:0] cnt; + logic [2:0] bitn; + + always_ff @(posedge clk) begin + rx_m <= rx; + rx_s <= rx_m; + valid <= 1'b0; + if (reset) begin + state <= 2'd0; cnt <= '0; bitn <= '0; data <= '0; + rx_m <= 1'b1; rx_s <= 1'b1; + end else begin + case (state) + 2'd0: if (!rx_s) begin state <= 2'd1; cnt <= HALF; end + 2'd1: if (cnt == 0) begin + if (!rx_s) begin state <= 2'd2; cnt <= FULL; bitn <= '0; end + else state <= 2'd0; // glitch, not a start bit + end else cnt <= cnt - 1'b1; + 2'd2: if (cnt == 0) begin + data <= {rx_s, data[7:1]}; + cnt <= FULL; + if (bitn == 3'd7) state <= 2'd3; + bitn <= bitn + 1'b1; + end else cnt <= cnt - 1'b1; + default: if (cnt == 0) begin + state <= 2'd0; + valid <= rx_s; // drop bytes with a bad stop bit + end else cnt <= cnt - 1'b1; + endcase + end + end +endmodule diff --git a/tapeout/src/uart_tx.sv b/tapeout/src/uart_tx.sv new file mode 100644 index 0000000000000000000000000000000000000000..e4d5620169fad9206a60fe1ba4083db552066439 --- /dev/null +++ b/tapeout/src/uart_tx.sv @@ -0,0 +1,48 @@ +// 8N1 UART transmitter. Load with start when ready is high. +module uart_tx #( + parameter int CW = 10 // width of cpb +)( + input logic clk, + input logic reset, + input logic [CW-1:0] cpb, // clocks per bit (>= 4) + input logic start, + input logic [7:0] data, + output logic ready, + output logic tx +); + logic [CW-1:0] FULL; + assign FULL = cpb - 1'b1; + + // LSB first: start, data[7:0], stop, then one guard bit so ready rises only + // after the stop bit has lasted a full bit time. + logic [10:0] shreg; + logic [3:0] left; // bit times still to send + logic [CW-1:0] cnt; + // The line is stored inverted: FPGA flip-flops power up low, so tx is idle + // high from configuration, before the first reset edge (a low line would be a + // start bit, i.e. a junk byte, to the host). No initial value is needed. + logic tx_n; + assign tx = !tx_n; + + assign ready = (left == 0); + + always_ff @(posedge clk) begin + if (reset) begin + tx_n <= 1'b0; left <= '0; cnt <= '0; shreg <= '1; + end else if (left == 0) begin + tx_n <= 1'b0; + if (start) begin + shreg <= {2'b11, data, 1'b0}; + left <= 4'd11; + cnt <= '0; + end + end else if (cnt == 0) begin + tx_n <= !shreg[0]; + shreg <= {1'b1, shreg[10:1]}; + left <= left - 1'b1; + cnt <= FULL; + end else begin + cnt <= cnt - 1'b1; + end + end +endmodule diff --git a/tapeout/test/Makefile b/tapeout/test/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..d6b2659ce1215a86fc76023ffbb4f342ed9c1b0d --- /dev/null +++ b/tapeout/test/Makefile @@ -0,0 +1,45 @@ +# Runs test/tb_fpga_top.sv (the shared UART testbench, Tiny Tapeout top mode) +# with Icarus Verilog and writes results.xml (JUnit) for the Tiny Tapeout +# gate-level test action. No cocotb or Python is needed. +# +# make RTL simulation +# make GATES=yes gate-level simulation of gate_level_netlist.v +# (needs PDK_ROOT with sky130A installed) + +SHELL := /bin/bash +SRC_DIR = $(abspath ../src) +PROJECT_SOURCES = bit_accelerator.sv uart_rx.sv uart_tx.sv tt_um_snapkittyagent9nova_bitacc.sv +TB_DEFINES ?= +IVERILOG ?= iverilog +VVP ?= vvp + +ifneq ($(GATES),yes) +SIM_BUILD = sim_build/rtl +SOURCES = $(addprefix $(SRC_DIR)/,$(PROJECT_SOURCES)) +DEFINES = -DTT_TOP $(TB_DEFINES) +else +SIM_BUILD = sim_build/gl +PDK ?= sky130A +SOURCES = $(PDK_ROOT)/$(PDK)/libs.ref/sky130_fd_sc_hd/verilog/primitives.v \ + $(PDK_ROOT)/$(PDK)/libs.ref/sky130_fd_sc_hd/verilog/sky130_fd_sc_hd.v \ + $(abspath gate_level_netlist.v) +DEFINES = -DTT_TOP -DGL_TEST -DGATE_LEVEL -DFUNCTIONAL -DUSE_POWER_PINS -DSIM -DUNIT_DELAY=\#1 $(TB_DEFINES) +endif + +.PHONY: all clean +all: + mkdir -p $(SIM_BUILD) + rm -f results.xml + $(IVERILOG) -g2012 $(DEFINES) -o $(SIM_BUILD)/tb.vvp $(SOURCES) tb_fpga_top.sv + @set -o pipefail; $(VVP) -n $(SIM_BUILD)/tb.vvp | tee $(SIM_BUILD)/sim.log; rc=$$?; \ + name=$$(if [ "$(GATES)" = yes ]; then echo gate_level; else echo rtl; fi)$(TB_DEFINES); \ + checks=$$(grep -oE 'checks=[0-9]+ fails=[0-9]+' $(SIM_BUILD)/sim.log | tail -1); \ + if [ $$rc -eq 0 ] && grep -q '^PASS:' $(SIM_BUILD)/sim.log; then \ + printf '%s\n' "$$name" "$$checks" > results.xml; \ + else \ + printf '\n' "$$name" "$$checks" > results.xml; \ + exit 1; \ + fi + +clean: + rm -rf sim_build results.xml diff --git a/tapeout/test/tb_fpga_top.sv b/tapeout/test/tb_fpga_top.sv new file mode 100644 index 0000000000000000000000000000000000000000..07e180cbb5a2c952b8bc15c85ac23563b10b4498 --- /dev/null +++ b/tapeout/test/tb_fpga_top.sv @@ -0,0 +1,230 @@ +// UART-level testbench: drives the serial line exactly as a host would and +// checks every reply against a reference model of the RAM and the accelerator. +// Exits non-zero on any failure. +// +// Default: fpga_top (256 words). With +define+TT_TOP it tests the Tiny Tapeout +// top tt_um_snapkittyagent9nova_bitacc (4 words, divisor from ui_in pins). +// +define+GATE_LEVEL instantiates a synthesized netlist without parameters. +`timescale 1ns/1ps +module tb_fpga_top; +`ifdef TT_TOP + localparam int DEPTH = 4; +`ifdef TT_DEFAULT_BAUD + localparam int CPB = 104; // divisor pins all low -> 104 clocks per bit +`else + localparam int CPB = 8; // ui_in divisor d = 2 -> 4*d clocks per bit +`endif +`else + localparam int DEPTH = 256; + localparam int CPB = 8; // clocks per UART bit (small to keep simulation fast) +`endif + + logic clk = 1'b0, reset = 1'b1, uart_rx = 1'b1; + logic uart_tx, led_busy, led_error; + +`ifdef TT_TOP + logic [7:0] uo_out, uio_out, uio_oe; +`ifdef TT_DEFAULT_BAUD + logic [6:0] div = 7'd0; +`else + logic [6:0] div = 7'(CPB / 4); +`endif +`ifdef GL_TEST + supply1 vpwr; // the powered post-layout netlist has inout supply pins + supply0 vgnd; +`endif + tt_um_snapkittyagent9nova_bitacc dut ( +`ifdef GL_TEST + .VPWR(vpwr), .VGND(vgnd), +`endif + .ui_in({div[6:3], uart_rx, div[2:0]}), .uo_out, .uio_in(8'h00), .uio_out, .uio_oe, + .ena(1'b1), .clk, .rst_n(!reset)); + assign uart_tx = uo_out[4]; + assign led_busy = uo_out[0]; + assign led_error = uo_out[1]; +`elsif GATE_LEVEL + fpga_top dut (.*); // synthesized netlist, built with CLKS_PER_BIT = 8 +`else + fpga_top #(.CLKS_PER_BIT(CPB), .DEPTH(DEPTH)) dut (.*); +`endif + + always #5 clk = ~clk; + + integer checks = 0, fails = 0; + task automatic check(input bit cond, input string what); + checks++; + if (!cond) begin fails++; $display("FAIL @%0t: %s", $time, what); end + endtask + + // ------------------------------------------------ receiver: tx line -> FIFO + logic [7:0] rxq [0:1023]; + integer rxq_wr = 0, rxq_rd = 0; + initial begin : receiver + logic [7:0] b; + forever begin + @(negedge uart_tx); + repeat (CPB / 2) @(posedge clk); + if (uart_tx !== 1'b0) check(0, "false start bit on tx"); + else begin + for (int i = 0; i < 8; i++) begin + repeat (CPB) @(posedge clk); + b[i] = uart_tx; + end + repeat (CPB) @(posedge clk); + check(uart_tx === 1'b1, "tx stop bit"); + rxq[rxq_wr % 1024] = b; + rxq_wr++; + end + end + end + + // ------------------------------------------------ host side + // Inputs change on the falling edge so no flip-flop samples them mid-update + // (a reset released exactly on a rising edge is seen by some flip-flops of a + // gate-level netlist and not by others). + task automatic send_byte(input logic [7:0] b); + @(negedge clk); + uart_rx = 1'b0; repeat (CPB) @(negedge clk); + for (int i = 0; i < 8; i++) begin uart_rx = b[i]; repeat (CPB) @(negedge clk); end + uart_rx = 1'b1; repeat (CPB) @(negedge clk); + endtask + + task automatic recv_byte(output logic [7:0] b, output bit ok); + integer waited = 0; + while (rxq_rd == rxq_wr && waited < 400 * CPB) begin @(posedge clk); waited++; end + ok = (rxq_rd != rxq_wr); + b = ok ? rxq[rxq_rd % 1024] : 8'hxx; + if (ok) rxq_rd++; + check(ok, "reply byte timeout"); + endtask + + task automatic send_u64(input logic [63:0] v); + for (int i = 0; i < 8; i++) send_byte(v[8*i +: 8]); + endtask + + // ------------------------------------------------ reference model + logic [63:0] model [0:DEPTH-1]; + + task automatic cmd_write(input logic [7:0] idx, input logic [63:0] v); + logic [7:0] r; bit ok; + send_byte("W"); send_byte(idx); send_u64(v); + recv_byte(r, ok); + check(r == "K", $sformatf("W %0d reply %h", idx, r)); + model[idx] = v; + endtask + + task automatic cmd_read_check(input logic [7:0] idx); + logic [7:0] r; logic [63:0] v; bit ok; + send_byte("R"); send_byte(idx); + for (int i = 0; i < 8; i++) begin recv_byte(r, ok); v[8*i +: 8] = r; end + check(v === model[idx], $sformatf("R %0d = %h, expected %h", idx, v, model[idx])); + endtask + + task automatic cmd_exec(input logic [2:0] op, input logic [63:0] base, input logic [63:0] off); + logic [63:0] eb, addr, word, onehot; + logic [5:0] bi; + bit in_range, exp_err, exp_bit, ok; + logic [7:0] r; + eb = (base << 3) + off; + addr = (eb >> 6) << 3; + bi = eb[5:0]; + onehot = 64'd1 << bi; + in_range = (addr >> 3) < DEPTH; + word = in_range ? model[addr >> 3] : '0; + exp_err = !in_range || op > 3'd4; + exp_bit = 1'b0; + if (!exp_err) begin + case (op) + 3'd2: begin model[addr >> 3] = word | onehot; exp_bit = 1'b1; end + 3'd3: begin model[addr >> 3] = word & ~onehot; exp_bit = 1'b0; end + 3'd4: begin model[addr >> 3] = word ^ onehot; exp_bit = !word[bi]; end + default: exp_bit = word[bi]; + endcase + end + send_byte("X"); send_byte({5'd0, op}); send_u64(base); send_u64(off); + recv_byte(r, ok); + check(r[7:2] == 6'b101000, $sformatf("X status tag %h", r)); + check(r[1] == exp_err, $sformatf("X op %0d base %h off %h: error %b expected %b", op, base, off, r[1], exp_err)); + if (!exp_err) check(r[0] == exp_bit, $sformatf("X op %0d base %h off %h: bit %b expected %b", op, base, off, r[0], exp_bit)); + check(led_error == exp_err, "led_error follows the last operation"); + endtask + + // ------------------------------------------------ stimulus + integer seed = 5; + logic [7:0] r; bit ok; + logic [63:0] rb, ro; + logic [2:0] rop; + + initial begin +`ifdef TT_TOP + // ASIC flip-flops have no power-up value; TX must be high while in reset. + repeat (2) @(negedge clk); + check(uart_tx === 1'b1, "tx high during reset"); + repeat (3) @(negedge clk); +`elsif GATE_LEVEL + // FPGA netlist: flip-flops power up low (the cell models encode this), so TX + // must idle high from power-up, before any reset edge; a low line here is a + // start bit, i.e. a junk byte, to the host. + #1 check(uart_tx === 1'b1, "tx idle-high at power-up"); + repeat (5) @(negedge clk); +`else + // RTL has no power-up value (X); TX must be high while in reset. The + // power-up property is checked on the gate-level netlists. + repeat (2) @(negedge clk); + check(uart_tx === 1'b1, "tx high during reset"); + repeat (3) @(negedge clk); +`endif + reset = 1'b0; + repeat (5) @(negedge clk); + + send_byte("P"); recv_byte(r, ok); check(r == "B", $sformatf("ping reply %h", r)); + send_byte("Z"); recv_byte(r, ok); check(r == "?", $sformatf("unknown command reply %h", r)); + + for (int i = 0; i < DEPTH; i++) cmd_write(i[7:0], {$random(seed), $random(seed)}); + for (int i = 0; i < DEPTH; i += 17) cmd_read_check(i[7:0]); + cmd_read_check(8'(DEPTH - 1)); + + cmd_write(0, 64'h8000_0000_0000_0001); + cmd_write(1, 64'h0000_0000_0000_0040); + cmd_exec(3'd0, 0, 0); // GET word0 bit0 + cmd_exec(3'd0, 0, 63); // GET word0 bit63 + cmd_exec(3'd1, 0, 70); // TEST word1 bit6 + cmd_exec(3'd2, 0, 5); cmd_read_check(0); + cmd_exec(3'd3, 0, 63); cmd_read_check(0); + cmd_exec(3'd4, 8, 6); cmd_read_check(1); // base 8 = word 1 + cmd_exec(3'd4, 7, 9); cmd_read_check(1); // crosses into word 1 + cmd_exec(3'd2, DEPTH * 8 - 1, 7); cmd_read_check(8'(DEPTH - 1)); // last bit of RAM + cmd_exec(3'd0, DEPTH * 8, 0); // first byte past RAM: read fault + cmd_exec(3'd2, 0, DEPTH * 64); // first bit past RAM: fault, no write + cmd_exec(3'd0, 64'hFFFF_FFFF_FFFF_FFFF, 0); + cmd_exec(3'd5, 0, 0); // undefined opcode + cmd_exec(3'd7, 0, 0); + cmd_exec(3'd0, 0, 0); // clean op after errors + for (int i = 0; i < 4; i++) cmd_read_check(i[7:0]); + + for (int n = 0; n < 300; n++) begin + rop = $unsigned($random(seed)) % 8; + if (rop > 4 && ($unsigned($random(seed)) % 3 != 0)) rop = rop - 3'd4; + rb = $unsigned($random(seed)) % (DEPTH * 8); + ro = $unsigned($random(seed)) % (DEPTH * 64); + if ($unsigned($random(seed)) % 10 == 0) rb = {$random(seed), $random(seed)}; + cmd_exec(rop, rb, ro); + if (n % 25 == 0) cmd_read_check(8'((rb >> 3) % DEPTH)); + end + for (int i = 0; i < DEPTH; i++) cmd_read_check(i[7:0]); + + $display("checks=%0d fails=%0d", checks, fails); + if (fails != 0) $fatal(1, "FAIL"); +`ifdef TT_TOP + $display("PASS: tt_um_snapkittyagent9nova_bitacc UART tests"); +`else + $display("PASS: fpga_top UART tests"); +`endif + $finish; + end + + initial begin + #2_000_000_000; + $fatal(1, "global timeout"); + end +endmodule diff --git a/verification/tb_bit_accelerator.sv b/verification/tb_bit_accelerator.sv new file mode 100644 index 0000000000000000000000000000000000000000..bb744ba7d3e941cffa0bfe126a86aeedded581f0 --- /dev/null +++ b/verification/tb_bit_accelerator.sv @@ -0,0 +1,285 @@ +// Self-checking testbench for rtl/bit_accelerator.sv. +// +// A memory slave with randomized backpressure, read latency and fault injection +// drives the DUT. Every accepted operation is checked against a reference model: +// - mem_addr of every request equals ((base<<3)+offset)>>6<<3 (64-bit wrap) +// - write data equals the expected modified word, with all byte strobes set +// - GET/TEST/illegal opcodes never write +// - exactly one result_valid pulse per operation, with error and result_bit +// valid in that same cycle +// - op_ready is low from acceptance until the result pulse +// - reset cancels an in-flight operation: no result, no write after reset +// Directed cases run first, then a seeded random phase (+seed=N, +ops=N). +`timescale 1ns/1ps +module tb_bit_accelerator; + localparam int DEPTH = 64; // memory words, indexed by mem_addr[8:3] + localparam int TIMEOUT = 200; // cycles before an operation is declared hung + + logic clk = 1'b0; + logic reset = 1'b1; + logic op_valid = 1'b0, op_ready; + logic [63:0] base_address = '0, bit_offset = '0; + logic [2:0] operation = '0; + logic result_valid, result_bit, error; + logic mem_valid, mem_write; + logic [63:0] mem_addr, mem_wdata; + logic [7:0] mem_wstrb; + logic mem_ready = 1'b0, mem_rvalid = 1'b0, mem_fault = 1'b0; + logic [63:0] mem_rdata = '0; + + bit_accelerator dut (.*); + + always #5 clk = ~clk; + + // ---------------------------------------------------------------- memory slave + logic [63:0] mem [0:DEPTH-1]; + integer seed = 1; + integer ready_pct = 100; // probability (%) that mem_ready is high + integer max_lat = 0; // read response latency, 0..max_lat extra cycles + bit fault_read_next = 1'b0; // fault the next accepted read + bit fault_write_next = 1'b0; // fault the next accepted write + bit rd_pending = 1'b0, rd_fault = 1'b0; + integer rd_delay = 0; + logic [63:0] rd_addr = '0; + integer writes_seen = 0; + + function automatic integer rnd(input integer n); // 0..n-1 + rnd = (n <= 1) ? 0 : ($unsigned($random(seed)) % n); + endfunction + + always @(posedge clk) begin + mem_rvalid <= 1'b0; + mem_fault <= 1'b0; + mem_ready <= (rnd(100) < ready_pct); + if (reset) begin + rd_pending <= 1'b0; + end else begin + if (mem_valid && mem_ready && !mem_write) begin + rd_pending <= 1'b1; + rd_delay <= rnd(max_lat + 1); + rd_addr <= mem_addr; + rd_fault <= fault_read_next; + fault_read_next <= 1'b0; + end + if (rd_pending) begin + if (rd_delay == 0) begin + rd_pending <= 1'b0; + if (rd_fault) mem_fault <= 1'b1; + else begin + mem_rvalid <= 1'b1; + mem_rdata <= mem[rd_addr[8:3]]; + end + end else rd_delay <= rd_delay - 1; + end + if (mem_valid && mem_ready && mem_write) begin + writes_seen <= writes_seen + 1; + if (fault_write_next) begin + mem_fault <= 1'b1; // seen by the DUT in WRITE_WAIT + fault_write_next <= 1'b0; + end else begin + mem[mem_addr[8:3]] <= mem_wdata; + end + end + end + end + + // -------------------------------------------------------------- bookkeeping + integer checks = 0, fails = 0; + task automatic check(input bit cond, input string what); + checks++; + if (!cond) begin + fails++; + $display("FAIL @%0t: %s", $time, what); + end + endtask + + // Request-level checks against the operation currently in flight. + bit in_flight = 1'b0; + logic [63:0] exp_addr; + logic [63:0] exp_wdata; + bit exp_write; + integer results_seen = 0; + + always @(posedge clk) begin + if (!reset) begin + if (mem_valid) begin + check(in_flight, "memory request while no operation is in flight"); + check(mem_addr === exp_addr, + $sformatf("mem_addr %h, expected %h", mem_addr, exp_addr)); + if (mem_write) begin + check(exp_write, "write issued for an operation that must not write"); + check(mem_wstrb === 8'hFF, $sformatf("mem_wstrb %h, expected ff", mem_wstrb)); + check(mem_wdata === exp_wdata, + $sformatf("mem_wdata %h, expected %h", mem_wdata, exp_wdata)); + end + end + if (in_flight && !result_valid) check(!op_ready, "op_ready high while busy"); + if (result_valid) results_seen <= results_seen + 1; + end + end + + // ------------------------------------------------------------- reference model + function automatic logic [63:0] eff_bit(input logic [63:0] b, input logic [63:0] o); + eff_bit = (b << 3) + o; + endfunction + + // Run one operation and check its result. fault_rd / fault_wr inject a fault on + // the read or write of this operation. Returns when result_valid has been seen. + task automatic run_op(input logic [63:0] b, input logic [63:0] o, input logic [2:0] op, + input bit fault_rd, input bit fault_wr); + logic [63:0] eb, word, onehot; + logic [5:0] idx; + bit exp_bit, exp_err, writes; + integer waited, w0; + eb = eff_bit(b, o); + idx = eb[5:0]; + onehot = 64'd1 << idx; + word = mem[(eb >> 6) % DEPTH]; + writes = (op == 3'b010 || op == 3'b011 || op == 3'b100); + exp_err = fault_rd || (!fault_rd && op > 3'b100) || (writes && fault_wr); + case (op) + 3'b010: begin exp_wdata = word | onehot; exp_bit = 1'b1; end + 3'b011: begin exp_wdata = word & ~onehot; exp_bit = 1'b0; end + 3'b100: begin exp_wdata = word ^ onehot; exp_bit = !word[idx]; end + default: begin exp_wdata = word; exp_bit = word[idx]; end + endcase + exp_addr = (eb >> 6) << 3; + exp_write = writes && !fault_rd; + + @(negedge clk); + fault_read_next = fault_rd; + fault_write_next = fault_wr; + base_address = b; bit_offset = o; operation = op; op_valid = 1'b1; + waited = 0; + while (!op_ready) begin @(negedge clk); waited++; end + @(posedge clk); w0 = writes_seen; // accepted on this edge + @(negedge clk); op_valid = 1'b0; in_flight = 1'b1; + base_address = '1; bit_offset = '1; operation = 3'b111; // must be ignored now + waited = 0; + while (!result_valid && waited < TIMEOUT) begin @(negedge clk); waited++; end + check(result_valid, $sformatf("timeout: op %0d base %h off %h", op, b, o)); + if (result_valid) begin + check(error === exp_err, + $sformatf("op %0d base %h off %h: error %b, expected %b (sampled with result_valid)", + op, b, o, error, exp_err)); + if (!exp_err) + check(result_bit === exp_bit, + $sformatf("op %0d base %h off %h: result_bit %b, expected %b", + op, b, o, result_bit, exp_bit)); + check(writes_seen - w0 == (exp_write ? 1 : 0), + $sformatf("op %0d: %0d writes, expected %0d", op, writes_seen - w0, exp_write)); + if (exp_write && !fault_wr) + check(mem[(eb >> 6) % DEPTH] === exp_wdata, "memory word not updated as expected"); + if (!exp_write || fault_wr) + check(mem[(eb >> 6) % DEPTH] === word, "memory word changed by a non-writing op"); + end + @(negedge clk); + check(!result_valid, "result_valid longer than one cycle"); + in_flight = 1'b0; + endtask + + // Start an operation and assert reset `after` cycles into it. + task automatic reset_during_op(input logic [2:0] op, input integer after); + integer w0, r0; + logic [63:0] snapshot [0:DEPTH-1]; + for (int k = 0; k < DEPTH; k++) snapshot[k] = mem[k]; + exp_addr = 64'd0; exp_wdata = mem[0] ^ 64'd1; exp_write = (op >= 3'b010 && op <= 3'b100); + if (op == 3'b010) exp_wdata = mem[0] | 64'd1; + if (op == 3'b011) exp_wdata = mem[0] & ~64'd1; + @(negedge clk); + base_address = 0; bit_offset = 0; operation = op; op_valid = 1'b1; + while (!op_ready) @(negedge clk); + @(posedge clk); + @(negedge clk); op_valid = 1'b0; in_flight = 1'b1; + repeat (after) @(negedge clk); + if (result_valid) begin + in_flight = 1'b0; // finished before the reset point; nothing to cancel + end else begin + reset = 1'b1; + @(negedge clk); + w0 = writes_seen; r0 = results_seen; + reset = 1'b0; in_flight = 1'b0; + // Restore memory so a write committed before reset does not affect later checks. + for (int k = 0; k < DEPTH; k++) mem[k] = snapshot[k]; + repeat (10) @(negedge clk); + check(results_seen == r0, $sformatf("result_valid after reset cancelled op %0d", op)); + check(writes_seen == w0, $sformatf("write after reset cancelled op %0d", op)); + check(op_ready, "DUT not idle after reset"); + end + endtask + + // --------------------------------------------------------------------- stimulus + integer ops = 2000; + integer seed0; + logic [63:0] rb, ro; + logic [2:0] rop; + + initial begin + if (!$value$plusargs("seed=%d", seed)) seed = 1; + if (!$value$plusargs("ops=%d", ops)) ops = 2000; + seed0 = seed; + for (int k = 0; k < DEPTH; k++) mem[k] = '0; + mem[0] = 64'h8000_0000_0000_0001; + mem[1] = 64'h0000_0000_0000_0040; + repeat (3) @(negedge clk); + reset = 1'b0; + + // Directed: the original eight cases (base 0, ideal memory). + run_op(0, 0, 3'b000, 0, 0); // GET word0 bit0 -> 1 + run_op(0, 63, 3'b000, 0, 0); // GET word0 bit63 -> 1 + run_op(0, 64, 3'b000, 0, 0); // GET word1 bit0 -> 0 + run_op(0, 7, 3'b001, 0, 0); // TEST word0 bit7 -> 0 + run_op(0, 1, 3'b010, 0, 0); // SET + run_op(0, 1, 3'b001, 0, 0); // TEST -> 1 + run_op(0, 1, 3'b011, 0, 0); // CLEAR + run_op(0, 1, 3'b100, 0, 0); // TOGGLE + + // Directed: non-zero base (byte address), word crossing, high/wrapping addresses. + run_op(8, 6, 3'b000, 0, 0); // byte 8 = word 1, bit 6 -> 1 + run_op(7, 7, 3'b010, 0, 0); // bit 63 of word 0 via base 7 + run_op(7, 8, 3'b100, 0, 0); // crosses into word 1 + run_op(64'h1FFF_FFFF_FFFF_FFFF, 64'd8, 3'b000, 0, 0); // (base<<3)+8 wraps to 0 + run_op(64'hFFFF_FFFF_FFFF_FFF8, 0, 3'b000, 0, 0); // base<<3 drops top 3 bits + run_op(0, 64'hFFFF_FFFF_FFFF_FFFF, 3'b010, 0, 0); // last bit of address space + + // Directed: illegal opcodes report error and never write. + run_op(0, 3, 3'b101, 0, 0); + run_op(0, 3, 3'b110, 0, 0); + run_op(0, 3, 3'b111, 0, 0); + + // Directed: faults. A read fault aborts before any write; a write fault + // reports error on the same cycle as result_valid. + run_op(0, 5, 3'b000, 1, 0); + run_op(0, 5, 3'b010, 1, 0); + run_op(0, 5, 3'b010, 0, 1); + run_op(0, 5, 3'b011, 0, 1); + run_op(0, 5, 3'b100, 0, 1); + run_op(0, 5, 3'b000, 0, 0); // a clean op right after faults has error=0 + + // Directed: reset at every point of a read-modify-write and of a read. + for (int a = 0; a < 8; a++) reset_during_op(3'b100, a); + for (int a = 0; a < 4; a++) reset_during_op(3'b000, a); + + // Random: backpressure, variable read latency, faults, all opcodes. + ready_pct = 60; max_lat = 3; + for (int n = 0; n < ops; n++) begin + rb = {$random(seed), $random(seed)}; + ro = {$random(seed), $random(seed)}; + if (rnd(2) == 0) begin rb = rnd(64); ro = rnd(1024); end // dense region + rop = rnd(8); + if (rnd(10) == 0) rop = 3'b101 + rnd(3); + else rop = rnd(5); + run_op(rb, ro, rop, rnd(20) == 0, rnd(20) == 0); + end + + $display("checks=%0d fails=%0d seed=%0d ops=%0d", checks, fails, seed0, ops); + if (fails == 0) $display("PASS: bit accelerator tests"); + else $fatal(1, "FAIL: %0d of %0d checks failed", fails, checks); + $finish; + end + + initial begin + #50_000_000; + $fatal(1, "global timeout"); + end +endmodule