├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CONTRIBUTORS ├── LICENSE ├── README.md ├── doc ├── architecture.dia ├── architecture.png ├── enjoy_digital.png ├── litescope_logo_full.png └── litescope_logo_full.svg ├── examples └── arty.py ├── litescope ├── __init__.py ├── core.py └── software │ ├── __init__.py │ ├── driver │ ├── __init__.py │ ├── analyzer.py │ └── io.py │ ├── dump │ ├── __init__.py │ ├── common.py │ ├── csv.py │ ├── json.py │ ├── python.py │ ├── sigrok.py │ └── vcd.py │ └── litescope_cli.py ├── setup.py └── test ├── __init__.py ├── test_analyzer.py ├── test_dump.py └── test_examples.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-22.04 8 | steps: 9 | # Checkout Repository 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | 13 | - name: Setup CCache 14 | uses: hendrikmuhs/ccache-action@v1.2 15 | 16 | # Install Tools 17 | - name: Install Tools 18 | run: | 19 | sudo apt-get install wget build-essential python3 ninja-build 20 | pip3 install setuptools 21 | pip3 install requests 22 | pip3 install pexpect 23 | pip3 install meson 24 | 25 | # Install (n)Migen / LiteX / Cores 26 | - name: Install LiteX 27 | run: | 28 | wget https://raw.githubusercontent.com/enjoy-digital/litex/master/litex_setup.py 29 | python3 litex_setup.py --init --install --user 30 | 31 | # Install RISC-V GCC 32 | - name: Install RISC-V GCC 33 | run: | 34 | sudo python3 litex_setup.py --gcc=riscv 35 | 36 | # Install Project 37 | - name: Install Project 38 | run: python3 setup.py develop --user 39 | 40 | # Test 41 | - name: Run Tests 42 | run: | 43 | python3 setup.py test 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | LiteX ecosystem would not exist without the collaborative work of contributors! Here is below the 2 | list of all the LiteScope contributors. 3 | 4 | In the source code, each file list the main authors/contributors: 5 | - author(s) that created the initial content. 6 | - contributor(s) that added essential features/improvements. 7 | 8 | If you think you should be in this list and don't find yourself, write to florent@enjoy-digital.fr 9 | and we'll fix it! 10 | 11 | Contributors: 12 | Copyright (c) 2020-2021 Antmicro 13 | Copyright (c) 2019-2021 Arnaud Durand 14 | Copyright (c) 2018 bunnie 15 | Copyright (c) 2020 Christian Klarhorst 16 | Copyright (c) 2018 Felix Held 17 | Copyright (c) 2015-2025 Florent Kermarrec 18 | Copyright (c) 2022 Jevin Sweval 19 | Copyright (c) 2019 kees.jongenburger 20 | Copyright (c) 2022 Marcus Comstedt 21 | Copyright (c) 2018 Sean Cross 22 | Copyright (c) 2021 Simon Thornington 23 | Copyright (c) 2022 Sylvain Munaut 24 | Copyright (c) 2016 Tim 'mithro' Ansell 25 | Copyright (c) 2020 Vegard Storheil Eriksen 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Unless otherwise noted, LiteScope is Copyright 2012-2024 / EnjoyDigital 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright notice, 9 | this list of conditions and the following disclaimer in the documentation 10 | and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 | 23 | 24 | Other authors retain ownership of their contributions. If a submission can 25 | reasonably be considered independently copyrightable, it's yours and we 26 | encourage you to claim it with appropriate copyright notices. This submission 27 | then falls under the "otherwise noted" category. All submissions are strongly 28 | encouraged to use the two-clause BSD license reproduced above. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | __ _ __ ____ 3 | / / (_) /____ / __/______ ___ ___ 4 | / /__/ / __/ -_)\ \/ __/ _ \/ _ \/ -_) 5 | /____/_/\__/\__/___/\__/\___/ .__/\__/ 6 | /_/ 7 | Copyright 2015-2024 / EnjoyDigital 8 | 9 | A small footprint and configurable Logic Analyzer 10 | core powered by Migen & LiteX 11 | ``` 12 | 13 | [![](https://github.com/enjoy-digital/litescope/workflows/ci/badge.svg)](https://github.com/enjoy-digital/litescope/actions) ![License](https://img.shields.io/badge/License-BSD%202--Clause-orange.svg) 14 | 15 | 16 | [> Intro 17 | -------- 18 | LiteScope provides a small footprint and configurable embedded logic analyzer that you 19 | can use in your FPGA and aims to provide a free, portable and flexible 20 | alternative to vendor's solutions! 21 | 22 | LiteScope is part of LiteX libraries whose aims are to lower entry level of 23 | complex FPGA cores by providing simple, elegant and efficient implementations 24 | of components used in today's SoC such as Ethernet, SATA, PCIe, SDRAM Controller... 25 | 26 | Using Migen to describe the HDL allows the core to be highly and easily configurable. 27 | 28 | LiteScope can be used as LiteX library or can be integrated with your standard 29 | design flow by generating the verilog rtl that you will use as a standard core. 30 | 31 | [> Features 32 | ----------- 33 | - IO peek and poke with LiteScopeIO. 34 | - Logic analyser with LiteScopeAnalyzer: 35 | - Subsampling. 36 | - Data storage in Block RAM. 37 | - Configurable triggers. 38 | - Bridges: 39 | - UART <--> Wishbone (provided by LiteX) 40 | - Ethernet <--> Wishbone ("Etherbone") (provided by LiteEth) 41 | - PCIe <--> Wishbone (provided by LitePCIe) 42 | - Exports formats: .vcd, .sr(sigrok), .csv, .py, etc... 43 | 44 | [> Proven 45 | --------- 46 | LiteScope has already been used to investigate issues on several commercial or 47 | open-source designs. 48 | 49 | [> Possible improvements 50 | ------------------------ 51 | - add standardized interfaces (AXI, Avalon-ST) 52 | - add protocols analyzers 53 | - add signals injection/generation 54 | - add storage in DRAM 55 | - add storage in HDD with LiteSATA core 56 | - ... See below Support and consulting :) 57 | 58 | If you want to support these features, please contact us at florent [AT] 59 | enjoy-digital.fr. 60 | 61 | [> Getting started 62 | ------------------ 63 | 1. Install Python 3.6+ and FPGA vendor's development tools. 64 | 2. Install LiteX and the cores by following the LiteX's wiki [installation guide](https://github.com/enjoy-digital/litex/wiki/Installation). 65 | 3. You can find examples of integration of the core with LiteX in LiteX-Boards and in the examples directory. 66 | 67 | [> Tests 68 | -------- 69 | Unit tests are available in ./test/. 70 | To run all the unit tests: 71 | ```sh 72 | $ ./setup.py test 73 | ``` 74 | 75 | Tests can also be run individually: 76 | ```sh 77 | $ python3 -m unittest test.test_name 78 | ``` 79 | 80 | [> License 81 | ---------- 82 | LiteScope is released under the very permissive two-clause BSD license. Under 83 | the terms of this license, you are authorized to use LiteScope for closed-source 84 | proprietary designs. 85 | Even though we do not require you to do so, those things are awesome, so please 86 | do them if possible: 87 | - tell us that you are using LiteScope 88 | - cite LiteScope in publications related to research it has helped 89 | - send us feedback and suggestions for improvements 90 | - send us bug reports when something goes wrong 91 | - send us the modifications and improvements you have done to LiteScope. 92 | 93 | [> Support and consulting 94 | ------------------------- 95 | We love open-source hardware and like sharing our designs with others. 96 | 97 | LiteScope is developed and maintained by EnjoyDigital. 98 | 99 | If you would like to know more about LiteScope or if you are already a happy 100 | user and would like to extend it for your needs, EnjoyDigital can provide standard 101 | commercial support as well as consulting services. 102 | 103 | So feel free to contact us, we'd love to work with you! (and eventually shorten 104 | the list of the possible improvements :) 105 | 106 | [> Contact 107 | ---------- 108 | E-mail: florent [AT] enjoy-digital.fr -------------------------------------------------------------------------------- /doc/architecture.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enjoy-digital/litescope/3898771a9d1cff274e30fdd81883bb43f837f853/doc/architecture.dia -------------------------------------------------------------------------------- /doc/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enjoy-digital/litescope/3898771a9d1cff274e30fdd81883bb43f837f853/doc/architecture.png -------------------------------------------------------------------------------- /doc/enjoy_digital.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enjoy-digital/litescope/3898771a9d1cff274e30fdd81883bb43f837f853/doc/enjoy_digital.png -------------------------------------------------------------------------------- /doc/litescope_logo_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enjoy-digital/litescope/3898771a9d1cff274e30fdd81883bb43f837f853/doc/litescope_logo_full.png -------------------------------------------------------------------------------- /doc/litescope_logo_full.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 31 | 41 | 51 | 61 | 68 | 72 | 76 | 77 | 87 | 89 | 93 | 97 | 98 | 100 | 104 | 108 | 109 | 110 | 128 | 130 | 131 | 133 | image/svg+xml 134 | 136 | 137 | 138 | 139 | 140 | 144 | LITe 158 | scope 172 | 182 | powered by 197 | 206 | 207 | 208 | -------------------------------------------------------------------------------- /examples/arty.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # 4 | # This file is part of LiteScope. 5 | # 6 | # Copyright (c) 2020 Florent Kermarrec 7 | # SPDX-License-Identifier: BSD-2-Clause 8 | 9 | # Use: 10 | # ./arty.py --build --load 11 | # litex_server --udp (for LiteScope over UDP) 12 | # litescope_cli: will trigger an immediate capture! 13 | # litescope_cli --help: list the available trigger option. 14 | # litescope_cli --list: list the signals that can be used as triggers. 15 | # litescope_cli -v main_count 128: trigger on count value == 128. 16 | # litescope_cli -r litescopesoc_cpu_ibus_stb: trigger in ibus_stb rising edge 17 | # For more information: https://github.com/enjoy-digital/litex/wiki/Use-LiteScope-To-Debug-A-SoC 18 | 19 | import os 20 | import argparse 21 | 22 | from migen import * 23 | 24 | from litex_boards.platforms import digilent_arty 25 | from litex_boards.targets.digilent_arty import * 26 | 27 | from litescope import LiteScopeAnalyzer 28 | 29 | # LiteScopeSoC ------------------------------------------------------------------------------------- 30 | 31 | class LiteScopeSoC(BaseSoC): 32 | def __init__(self): 33 | platform = digilent_arty.Platform() 34 | 35 | # BaseSoC ---------------------------------------------------------------------------------- 36 | BaseSoC.__init__(self, 37 | integrated_rom_size = 0x8000, 38 | with_etherbone = True, 39 | ) 40 | 41 | # LiteScope Analyzer ----------------------------------------------------------------------- 42 | count = Signal(8) 43 | self.sync += count.eq(count + 1) 44 | analyzer_signals = [ 45 | self.cpu.ibus, 46 | count, 47 | ] 48 | self.submodules.analyzer = LiteScopeAnalyzer(analyzer_signals, 49 | depth = 1024, 50 | clock_domain = "sys", 51 | samplerate = self.sys_clk_freq, 52 | csr_csv = "analyzer.csv") 53 | self.add_csr("analyzer") 54 | 55 | # Build -------------------------------------------------------------------------------------------- 56 | 57 | def main(): 58 | parser = argparse.ArgumentParser(description="LiteScope example on Arty A7") 59 | parser.add_argument("--build", action="store_true", help="Build bitstream") 60 | parser.add_argument("--load", action="store_true", help="Load bitstream") 61 | args = parser.parse_args() 62 | 63 | soc = LiteScopeSoC() 64 | builder = Builder(soc, csr_csv="csr.csv") 65 | builder.build(run=args.build) 66 | 67 | if args.load: 68 | prog = soc.platform.create_programmer() 69 | prog.load_bitstream(os.path.join(builder.gateware_dir, soc.build_name + ".bit")) 70 | 71 | if __name__ == "__main__": 72 | main() 73 | -------------------------------------------------------------------------------- /litescope/__init__.py: -------------------------------------------------------------------------------- 1 | from litescope.core import LiteScopeIO, LiteScopeAnalyzer 2 | from litescope.software.driver.io import LiteScopeIODriver 3 | from litescope.software.driver.analyzer import LiteScopeAnalyzerDriver 4 | -------------------------------------------------------------------------------- /litescope/core.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of LiteScope. 3 | # 4 | # Copyright (c) 2016-2024 Florent Kermarrec 5 | # Copyright (c) 2018 bunnie 6 | # Copyright (c) 2016 Tim 'mithro' Ansell 7 | # SPDX-License-Identifier: BSD-2-Clause 8 | 9 | from migen import * 10 | from migen.genlib.cdc import MultiReg, PulseSynchronizer 11 | 12 | from litex.gen import * 13 | from litex.gen.genlib.misc import WaitTimer 14 | 15 | from litex.build.tools import write_to_file 16 | 17 | from litex.soc.interconnect.csr import * 18 | 19 | from litex.soc.cores.gpio import GPIOInOut 20 | from litex.soc.interconnect import stream 21 | 22 | # LiteScope IO ------------------------------------------------------------------------------------- 23 | 24 | class LiteScopeIO(LiteXModule): 25 | def __init__(self, data_width): 26 | self.data_width = data_width 27 | self.input = Signal(data_width) 28 | self.output = Signal(data_width) 29 | 30 | # # # 31 | 32 | self.gpio = GPIOInOut(self.input, self.output) 33 | 34 | def get_csrs(self): 35 | return self.gpio.get_csrs() 36 | 37 | # LiteScope Analyzer Constants/Layouts ------------------------------------------------------------- 38 | 39 | def core_layout(data_width): 40 | return [("data", data_width), ("hit", 1)] 41 | 42 | # LiteScope Analyzer Trigger ----------------------------------------------------------------------- 43 | 44 | class _Trigger(LiteXModule): 45 | def __init__(self, data_width, depth=16): 46 | self.sink = sink = stream.Endpoint(core_layout(data_width)) 47 | self.source = source = stream.Endpoint(core_layout(data_width)) 48 | 49 | self.enable = CSRStorage() 50 | self.done = CSRStatus() 51 | 52 | self.mem_write = CSR() 53 | self.mem_mask = CSRStorage(data_width) 54 | self.mem_value = CSRStorage(data_width) 55 | self.mem_full = CSRStatus() 56 | 57 | # # # 58 | 59 | # Control re-synchronization. 60 | enable = Signal() 61 | enable_d = Signal() 62 | self.specials += MultiReg(self.enable.storage, enable, "scope") 63 | self.sync.scope += enable_d.eq(enable) 64 | 65 | # Status re-synchronization. 66 | done = Signal() 67 | self.specials += MultiReg(done, self.done.status) 68 | 69 | # Memory and configuration. 70 | mem = stream.AsyncFIFO([("mask", data_width), ("value", data_width)], depth) 71 | mem = ClockDomainsRenamer({"write": "sys", "read": "scope"})(mem) 72 | self.submodules += mem 73 | self.comb += [ 74 | mem.sink.valid.eq(self.mem_write.re), 75 | mem.sink.mask.eq(self.mem_mask.storage), 76 | mem.sink.value.eq(self.mem_value.storage), 77 | self.mem_full.status.eq(~mem.sink.ready) 78 | ] 79 | 80 | # Hit and memory read/flush. 81 | hit = Signal() 82 | flush = WaitTimer(2*depth) 83 | flush = ClockDomainsRenamer("scope")(flush) 84 | self.submodules += flush 85 | self.comb += [ 86 | flush.wait.eq(~(~enable & enable_d)), # flush when disabling 87 | hit.eq((sink.data & mem.source.mask) == (mem.source.value & mem.source.mask)), 88 | mem.source.ready.eq((enable & hit) | ~flush.done), 89 | ] 90 | 91 | # Output. 92 | self.comb += [ 93 | sink.connect(source), 94 | # Done when all triggers have been consumed. 95 | done.eq(~mem.source.valid), 96 | source.hit.eq(done) 97 | ] 98 | 99 | # LiteScope Analyzer SubSampler -------------------------------------------------------------------- 100 | 101 | class _SubSampler(LiteXModule): 102 | def __init__(self, data_width): 103 | self.sink = sink = stream.Endpoint(core_layout(data_width)) 104 | self.source = source = stream.Endpoint(core_layout(data_width)) 105 | 106 | self.value = CSRStorage(16) 107 | 108 | # # # 109 | 110 | value = Signal(16) 111 | self.specials += MultiReg(self.value.storage, value, "scope") 112 | 113 | counter = Signal(16) 114 | done = Signal() 115 | self.sync.scope += \ 116 | If(source.ready, 117 | If(done, 118 | counter.eq(0) 119 | ).Elif(sink.valid, 120 | counter.eq(counter + 1) 121 | ) 122 | ) 123 | 124 | self.comb += [ 125 | done.eq(counter == value), 126 | sink.connect(source, omit={"valid"}), 127 | source.valid.eq(sink.valid & done) 128 | ] 129 | 130 | # LiteScope Analyzer Mux --------------------------------------------------------------------------- 131 | 132 | class _Mux(LiteXModule): 133 | def __init__(self, data_width, n): 134 | self.sinks = sinks = [stream.Endpoint(core_layout(data_width)) for i in range(n)] 135 | self.source = source = stream.Endpoint(core_layout(data_width)) 136 | 137 | self.value = CSRStorage(bits_for(n)) 138 | 139 | # # # 140 | 141 | value = Signal(bits_for(n)) 142 | self.specials += MultiReg(self.value.storage, value, "scope") 143 | 144 | cases = {} 145 | for i in range(n): 146 | cases[i] = sinks[i].connect(source) 147 | self.comb += Case(value, cases) 148 | 149 | # LiteScope Analyzer Storage ----------------------------------------------------------------------- 150 | 151 | class _Storage(LiteXModule): 152 | def __init__(self, data_width, depth): 153 | self.sink = sink = stream.Endpoint(core_layout(data_width)) 154 | 155 | self.enable = CSRStorage() 156 | self.done = CSRStatus() 157 | 158 | self.length = CSRStorage(bits_for(depth)) 159 | self.offset = CSRStorage(bits_for(depth)) 160 | 161 | read_width = min(32, data_width) 162 | self.mem_level = CSRStatus(bits_for(depth)) 163 | self.mem_data = CSRStatus(read_width) 164 | 165 | # # # 166 | 167 | # Control re-synchronization. 168 | enable = Signal() 169 | enable_d = Signal() 170 | self.specials += MultiReg(self.enable.storage, enable, "scope") 171 | self.sync.scope += enable_d.eq(enable) 172 | 173 | length = Signal().like(self.length.storage) 174 | offset = Signal().like(self.offset.storage) 175 | self.specials += MultiReg(self.length.storage, length, "scope") 176 | self.specials += MultiReg(self.offset.storage, offset, "scope") 177 | 178 | # Status re-synchronization. 179 | done = Signal() 180 | level = Signal().like(self.mem_level.status) 181 | self.specials += MultiReg(done, self.done.status) 182 | self.specials += MultiReg(level, self.mem_level.status) 183 | 184 | # Memory. 185 | mem = stream.SyncFIFO([("data", data_width)], depth, buffered=True) 186 | mem = ClockDomainsRenamer("scope")(mem) 187 | cdc = stream.AsyncFIFO([("data", data_width)], 4) 188 | cdc = ClockDomainsRenamer({"write": "scope", "read": "sys"})(cdc) 189 | self.submodules += mem, cdc 190 | 191 | self.comb += level.eq(mem.level) 192 | 193 | # Flush. 194 | mem_flush = WaitTimer(depth) 195 | mem_flush = ClockDomainsRenamer("scope")(mem_flush) 196 | self.submodules += mem_flush 197 | 198 | # FSM. 199 | fsm = FSM(reset_state="IDLE") 200 | fsm = ClockDomainsRenamer("scope")(fsm) 201 | self.submodules += fsm 202 | fsm.act("IDLE", 203 | done.eq(1), 204 | If(enable & ~enable_d, 205 | NextState("FLUSH") 206 | ), 207 | sink.ready.eq(1), 208 | mem.source.connect(cdc.sink) 209 | ) 210 | fsm.act("FLUSH", 211 | sink.ready.eq(1), 212 | mem_flush.wait.eq(1), 213 | mem.source.ready.eq(1), 214 | If(mem_flush.done, 215 | NextState("WAIT") 216 | ) 217 | ) 218 | fsm.act("WAIT", 219 | sink.connect(mem.sink, omit={"hit"}), 220 | If(sink.valid & sink.hit, 221 | NextState("RUN") 222 | ), 223 | mem.source.ready.eq(mem.level >= offset) 224 | ) 225 | fsm.act("RUN", 226 | sink.connect(mem.sink, omit={"hit"}), 227 | If(mem.level >= length, 228 | NextState("IDLE"), 229 | ) 230 | ) 231 | 232 | # Memory read. 233 | read_source = stream.Endpoint([("data", data_width)]) 234 | if data_width > read_width: 235 | pad_bits = - data_width % read_width 236 | w_conv = stream.Converter(data_width + pad_bits, read_width) 237 | self.submodules += w_conv 238 | self.comb += cdc.source.connect(w_conv.sink) 239 | self.comb += w_conv.source.connect(read_source) 240 | else: 241 | self.comb += cdc.source.connect(read_source) 242 | 243 | self.comb += [ 244 | read_source.ready.eq(self.mem_data.we | ~self.enable.storage), 245 | self.mem_data.status.eq(read_source.data) 246 | ] 247 | 248 | # LiteScope Analyzer ------------------------------------------------------------------------------- 249 | 250 | class LiteScopeAnalyzer(LiteXModule): 251 | def __init__(self, groups, depth, 252 | samplerate = 1e12, 253 | clock_domain = "sys", 254 | trigger_depth = 16, 255 | register = False, 256 | csr_csv = "analyzer.csv", 257 | ): 258 | self.groups = groups = self.format_groups(groups) 259 | self.depth = depth 260 | self.samplerate = int(samplerate) 261 | 262 | self.data_width = data_width = max([sum([len(s) for s in g]) for g in groups.values()]) 263 | 264 | self.csr_csv = csr_csv 265 | 266 | # # # 267 | 268 | # Create scope clock domain. 269 | self.cd_scope = ClockDomain() 270 | self.comb += self.cd_scope.clk.eq(ClockSignal(clock_domain)) 271 | 272 | # Mux. 273 | # ---- 274 | self.mux = _Mux(data_width, len(groups)) 275 | sd = getattr(self.sync, clock_domain) 276 | for i, signals in groups.items(): 277 | s = Cat(signals) 278 | if len(s) < data_width: 279 | s = Cat(s, Constant(0, data_width - len(s))) 280 | if register: 281 | s_d = Signal(len(s)) 282 | sd += s_d.eq(s) 283 | s = s_d 284 | self.comb += [ 285 | self.mux.sinks[i].valid.eq(1), 286 | self.mux.sinks[i].data.eq(s) 287 | ] 288 | 289 | # Frontend. 290 | # --------- 291 | self.trigger = _Trigger(data_width, depth=trigger_depth) 292 | self.subsampler = _SubSampler(data_width) 293 | 294 | # Storage. 295 | # -------- 296 | self.storage = _Storage(data_width, depth) 297 | 298 | # Pipeline: Mux -> Trigger -> Subsampler -> Storage. 299 | # -------------------------------------------------- 300 | self.pipeline = stream.Pipeline( 301 | self.mux, 302 | self.trigger, 303 | self.subsampler, 304 | self.storage, 305 | ) 306 | 307 | def format_groups(self, groups): 308 | if not isinstance(groups, dict): 309 | groups = {0 : groups} 310 | new_groups = {} 311 | for n, signals in groups.items(): 312 | if not isinstance(signals, list): 313 | signals = [signals] 314 | 315 | split_signals = [] 316 | for s in signals: 317 | if isinstance(s, Record): 318 | split_signals.extend(s.flatten()) 319 | elif isinstance(s, FSM): 320 | s.do_finalize() 321 | s.finalized = True 322 | split_signals.append(s.state) 323 | else: 324 | split_signals.append(s) 325 | split_signals = list(dict.fromkeys(split_signals)) # Remove duplicates. 326 | new_groups[n] = split_signals 327 | return new_groups 328 | 329 | def export_csv(self, vns, filename): 330 | def format_line(*args): 331 | return ",".join(args) + "\n" 332 | r = format_line("config", "None", "data_width", str(self.data_width)) 333 | r += format_line("config", "None", "depth", str(self.depth)) 334 | r += format_line("config", "None", "samplerate", str(self.samplerate)) 335 | for i, signals in self.groups.items(): 336 | for s in signals: 337 | r += format_line("signal", str(i), vns.get_name(s), str(len(s))) 338 | write_to_file(filename, r) 339 | 340 | def do_exit(self, vns): 341 | if self.csr_csv is not None: 342 | self.export_csv(vns, self.csr_csv) 343 | -------------------------------------------------------------------------------- /litescope/software/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enjoy-digital/litescope/3898771a9d1cff274e30fdd81883bb43f837f853/litescope/software/__init__.py -------------------------------------------------------------------------------- /litescope/software/driver/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enjoy-digital/litescope/3898771a9d1cff274e30fdd81883bb43f837f853/litescope/software/driver/__init__.py -------------------------------------------------------------------------------- /litescope/software/driver/analyzer.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of LiteScope. 3 | # 4 | # Copyright (c) 2015-2018 Florent Kermarrec 5 | # Copyright (c) 2019 kees.jongenburger 6 | # Copyright (c) 2018 Sean Cross 7 | # SPDX-License-Identifier: BSD-2-Clause 8 | 9 | import os 10 | import sys 11 | import re 12 | 13 | from migen import * 14 | 15 | from litescope.software.dump.common import * 16 | from litescope.software.dump import * 17 | 18 | import csv 19 | 20 | 21 | class LiteScopeAnalyzerDriver: 22 | def __init__(self, regs, name, config_csv=None, debug=False): 23 | self.regs = regs 24 | self.name = name 25 | self.config_csv = config_csv 26 | if self.config_csv is None: 27 | self.config_csv = name + ".csv" 28 | self.debug = debug 29 | self.get_config() 30 | self.get_layouts() 31 | self.build() 32 | self.group = 0 33 | self.data = DumpData(self.data_width) 34 | 35 | self.offset = 0 36 | self.length = None 37 | 38 | # Disable trigger and storage 39 | self.trigger_enable.write(0) 40 | self.storage_enable.write(0) 41 | 42 | def get_config(self): 43 | csv_reader = csv.reader(open(self.config_csv), delimiter=',', quotechar='#') 44 | for item in csv_reader: 45 | t, g, n, v = item 46 | if t == "config": 47 | setattr(self, n, int(v)) 48 | 49 | def get_layouts(self): 50 | self.layouts = {} 51 | csv_reader = csv.reader(open(self.config_csv), delimiter=',', quotechar='#') 52 | for item in csv_reader: 53 | t, g, n, v = item 54 | if t == "signal": 55 | try: 56 | self.layouts[int(g)].append((n, int(v))) 57 | except: 58 | self.layouts[int(g)] = [(n, int(v))] 59 | 60 | def build(self): 61 | for key, value in self.regs.d.items(): 62 | if self.name == key[:len(self.name)]: 63 | key = key.replace(self.name + "_", "") 64 | setattr(self, key, value) 65 | for signals in self.layouts.values(): 66 | value = 1 67 | for name, length in signals: 68 | setattr(self, name + "_o", value) 69 | value = value*(2**length) 70 | for signals in self.layouts.values(): 71 | value = 0 72 | for name, length in signals: 73 | setattr(self, name + "_m", (2**length-1) << value) 74 | value += length 75 | 76 | def configure_group(self, value): 77 | self.group = value 78 | self.mux_value.write(value) 79 | 80 | def add_trigger(self, value=0, mask=0, cond=None): 81 | if self.trigger_mem_full.read(): 82 | raise ValueError("Trigger memory full, too much conditions") 83 | if cond is not None: 84 | for k, v in cond.items(): 85 | # Check for binary/hexa expressions 86 | mb = re.match("0b([01x]+)", v) 87 | mx = re.match("0x([0-fx]+)", v) 88 | m = mb or mx 89 | if m is not None: 90 | b = m.group(1) 91 | v = 0 92 | m = 0 93 | for c in b: 94 | v <<= 4 if mx is not None else 1 95 | m <<= 4 if mx is not None else 1 96 | if c != "x": 97 | v |= int(c, 16 if mx is not None else 2 ) 98 | m |= 0xf if mx is not None else 0b1 99 | value |= getattr(self, k + "_o")*v 100 | mask |= getattr(self, k + "_m") & (getattr(self, k + "_o")*m) 101 | # Else convert to int 102 | else: 103 | value |= getattr(self, k + "_o")*int(v, 0) 104 | mask |= getattr(self, k + "_m") 105 | self.trigger_mem_mask.write(mask) 106 | self.trigger_mem_value.write(value) 107 | self.trigger_mem_write.write(1) 108 | 109 | def add_rising_edge_trigger(self, name): 110 | self.add_trigger(getattr(self, name + "_o")*0, getattr(self, name + "_m")) 111 | self.add_trigger(getattr(self, name + "_o")*1, getattr(self, name + "_m")) 112 | 113 | def add_falling_edge_trigger(self, name): 114 | self.add_trigger(getattr(self, name + "_o")*1, getattr(self, name + "_m")) 115 | self.add_trigger(getattr(self, name + "_o")*0, getattr(self, name + "_m")) 116 | 117 | def configure_trigger(self, value=0, mask=0, cond=None): 118 | self.add_trigger(value, mask, cond) 119 | 120 | def configure_subsampler(self, value): 121 | self.subsampling = value 122 | self.subsampler_value.write(value-1) 123 | 124 | def run(self, offset=0, length=None): 125 | if length is None: 126 | length = self.depth 127 | assert offset < self.depth 128 | assert length <= self.depth 129 | self.offset = offset 130 | self.length = length 131 | if self.debug: 132 | print("[running]...") 133 | self.storage_offset.write(offset) 134 | self.storage_length.write(length) 135 | self.storage_enable.write(1) 136 | self.trigger_enable.write(1) 137 | 138 | def clear(self): 139 | self.data = DumpData(self.data_width) 140 | self.offset = 0 141 | self.length = None 142 | self.trigger_enable.write(0) 143 | self.storage_enable.write(0) 144 | 145 | def done(self): 146 | return self.storage_done.read() 147 | 148 | def wait_done(self): 149 | while not self.done(): 150 | pass 151 | 152 | def upload(self): 153 | if self.debug: 154 | print("[uploading]...") 155 | 156 | length = self.storage_mem_level.read() 157 | remaining = length 158 | swpw = (self.data_width + 31) // 32 # Sub-Words per word 159 | mwbl = 192 // swpw # Max Burst len (in # of words) 160 | 161 | while remaining > 0: 162 | rdw = min(remaining, mwbl) 163 | rdsw = rdw * swpw 164 | datas = self.storage_mem_data.readfn(self.storage_mem_data.addr, length=rdsw, burst="fixed") 165 | 166 | for i, sv in enumerate(datas): 167 | j = i % swpw 168 | if j == 0: 169 | v = 0 170 | v |= sv << (32 * j) 171 | if j == (swpw - 1): 172 | self.data.append(v) 173 | 174 | remaining -= rdw 175 | 176 | sys.stdout.write("[{}>{}] {}%\r".format( 177 | '=' * (20-20*remaining//length), 178 | ' ' * (20*remaining//length), 179 | 100-(100*remaining//length)) 180 | ) 181 | 182 | if self.debug: 183 | print("") 184 | return self.data 185 | 186 | def save(self, filename, samplerate=None, flatten=False): 187 | if samplerate is None: 188 | samplerate = self.samplerate / self.subsampling 189 | if self.debug: 190 | print("[writing to " + filename + "]...") 191 | name, ext = os.path.splitext(filename) 192 | if ext == ".vcd": 193 | dump = VCDDump(samplerate=samplerate) 194 | elif ext == ".csv": 195 | dump = CSVDump() 196 | elif ext == ".py": 197 | dump = PythonDump() 198 | elif ext == ".json": 199 | dump = JSONDump() 200 | elif ext == ".sr": 201 | dump = SigrokDump(samplerate=samplerate) 202 | else: 203 | raise NotImplementedError 204 | if not flatten: 205 | dump.add_from_layout(self.layouts[self.group], self.data) 206 | else: 207 | dump.add_from_layout_flatten(self.layouts[self.group], self.data) 208 | dump.add_scope_clk() 209 | dump.add_scope_trig(self.offset) 210 | dump.write(filename) 211 | 212 | def get_instant_value(self, group, name): 213 | self.data = DumpData(self.data_width) 214 | self.debug = False 215 | self.configure_group(group) 216 | self.configure_trigger() 217 | self.configure_subsampler(1) 218 | self.run(0, 1) 219 | self.wait_done() 220 | self.upload() 221 | min_idx = log2_int(getattr(self, name + "_o")) 222 | max_idx = min_idx + log2_int((getattr(self, name + "_m") >> min_idx) + 1) 223 | return self.data[min_idx:max_idx][0] 224 | -------------------------------------------------------------------------------- /litescope/software/driver/io.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of LiteScope. 3 | # 4 | # Copyright (c) 2015-2017 Florent Kermarrec 5 | # SPDX-License-Identifier: BSD-2-Clause 6 | 7 | class LiteScopeIODriver: 8 | def __init__(self, regs, name): 9 | self.regs = regs 10 | self.name = name 11 | self.build() 12 | 13 | def build(self): 14 | self.input = getattr(self.regs, self.name + "_in") 15 | self.output = getattr(self.regs, self.name + "_out") 16 | 17 | def write(self, value): 18 | self.output.write(value) 19 | 20 | def read(self): 21 | return self.input.read() 22 | -------------------------------------------------------------------------------- /litescope/software/dump/__init__.py: -------------------------------------------------------------------------------- 1 | from litescope.software.dump.common import DumpData, DumpVariable, Dump 2 | from litescope.software.dump.csv import CSVDump 3 | from litescope.software.dump.python import PythonDump 4 | from litescope.software.dump.json import JSONDump 5 | from litescope.software.dump.sigrok import SigrokDump 6 | from litescope.software.dump.vcd import VCDDump 7 | -------------------------------------------------------------------------------- /litescope/software/dump/common.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of LiteScope. 3 | # 4 | # Copyright (c) 2015-2019 Florent Kermarrec 5 | # Copyright (c) 2019 kees.jongenburger 6 | # SPDX-License-Identifier: BSD-2-Clause 7 | 8 | def dec2bin(d, width=0): 9 | if d == "x": 10 | return "x"*width 11 | elif d == 0: 12 | b = "0" 13 | else: 14 | b = "" 15 | while d != 0: 16 | b = "01"[d&1] + b 17 | d = d >> 1 18 | return b.zfill(width) 19 | 20 | 21 | def get_bits(values, low, high=None): 22 | r = [] 23 | if high is None: 24 | high = low + 1 25 | for val in values: 26 | t = (val >> low) & (2**(high - low) - 1) 27 | r.append(t) 28 | return r 29 | 30 | 31 | class DumpData(list): 32 | def __init__(self, width): 33 | self.width = width 34 | 35 | def __getitem__(self, key): 36 | if isinstance(key, int): 37 | return get_bits(self, key) 38 | elif isinstance(key, slice): 39 | if key.start != None: 40 | start = key.start 41 | else: 42 | start = 0 43 | if key.stop != None: 44 | stop = key.stop 45 | else: 46 | stop = self.width 47 | if stop > self.width: 48 | stop = self.width 49 | if key.step != None: 50 | raise KeyError 51 | return get_bits(self, start, stop) 52 | else: 53 | raise KeyError 54 | 55 | 56 | class DumpVariable: 57 | def __init__(self, name, width, values=[]): 58 | self.name = name 59 | self.width = width 60 | self.values = [int(v)%2**width for v in values] 61 | 62 | def __len__(self): 63 | return len(self.values) 64 | 65 | 66 | class Dump: 67 | def __init__(self): 68 | self.variables = [] 69 | 70 | def add(self, variable): 71 | self.variables.append(variable) 72 | 73 | def add_from_layout(self, layout, variable): 74 | offset = 0 75 | for name, sample_width in layout: 76 | values = variable[offset:offset+sample_width] 77 | values2x = [values[i//2] for i in range(len(values)*2)] 78 | self.add(DumpVariable(name, sample_width, values2x)) 79 | offset += sample_width 80 | 81 | def add_from_layout_flatten(self, layout, variable): 82 | offset = 0 83 | for name, sample_width in layout: 84 | # The samples from the logic analyzer end up in an array of size sample size 85 | # and have n (number of channel) bits. The following does a bit slice on the array 86 | # elements (implemented above) 87 | values = variable[offset:offset+sample_width] 88 | values_flatten = [values[i//sample_width] >> (i % sample_width ) & 1 for i in range(len(values)*sample_width)] 89 | self.add(DumpVariable(name, 1, values_flatten)) 90 | offset += sample_width 91 | 92 | def add_scope_clk(self): 93 | self.add(DumpVariable("scope_clk", 1, [1, 0]*(len(self)//2))) 94 | 95 | def add_scope_trig(self, offset): 96 | self.add(DumpVariable("scope_trig", 1, [0]*offset + [1]*(len(self)-offset))) 97 | 98 | def __len__(self): 99 | l = 0 100 | for variable in self.variables: 101 | l = max(len(variable), l) 102 | return l 103 | -------------------------------------------------------------------------------- /litescope/software/dump/csv.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of LiteScope. 3 | # 4 | # Copyright (c) 2015 Florent Kermarrec 5 | # SPDX-License-Identifier: BSD-2-Clause 6 | 7 | from litescope.software.dump.common import Dump, dec2bin 8 | 9 | 10 | class CSVDump(Dump): 11 | def __init__(self, dump=None): 12 | Dump.__init__(self) 13 | self.variables = [] if dump is None else dump.variables 14 | 15 | def generate_vars(self): 16 | r = "" 17 | for variable in self.variables: 18 | r += variable.name 19 | r += "," 20 | r += "\n" 21 | for variable in self.variables: 22 | r += str(variable.width) 23 | r += "," 24 | r += "\n" 25 | return r 26 | 27 | def generate_dumpvars(self): 28 | r = "" 29 | for i in range(len(self)): 30 | for variable in self.variables: 31 | try: 32 | variable.current_value = variable.values[i] 33 | except: 34 | pass 35 | if variable.current_value == "x": 36 | r += "x" 37 | else: 38 | r += dec2bin(variable.current_value, variable.width) 39 | r += ", " 40 | r += "\n" 41 | return r 42 | 43 | def write(self, filename): 44 | f = open(filename, "w") 45 | f.write(self.generate_vars()) 46 | f.write(self.generate_dumpvars()) 47 | f.close() 48 | 49 | def read(self, filename): 50 | raise NotImplementedError("CSV files can not (yet) be read, please contribute!") 51 | -------------------------------------------------------------------------------- /litescope/software/dump/json.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of LiteScope. 3 | # 4 | # Copyright (c) 2021 Arnaud Durand 5 | # SPDX-License-Identifier: BSD-2-Clause 6 | 7 | import json 8 | 9 | from litescope.software.dump.common import Dump 10 | 11 | 12 | class JSONDump(Dump): 13 | def __init__(self, dump=None): 14 | Dump.__init__(self) 15 | self.variables = [] if dump is None else dump.variables 16 | 17 | def generate_data(self): 18 | return {v.name: v.values for v in self.variables} 19 | 20 | def write(self, filename): 21 | with open(filename, "w") as f: 22 | json.dump(self.generate_data(), f) 23 | 24 | def read(self, filename): 25 | raise NotImplementedError("JSON files can not (yet) be read, please contribute!") 26 | -------------------------------------------------------------------------------- /litescope/software/dump/python.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of LiteScope. 3 | # 4 | # Copyright (c) 2015 Florent Kermarrec 5 | # SPDX-License-Identifier: BSD-2-Clause 6 | 7 | from litescope.software.dump.common import Dump 8 | 9 | 10 | class PythonDump(Dump): 11 | def __init__(self, dump=None): 12 | Dump.__init__(self) 13 | self.variables = [] if dump is None else dump.variables 14 | 15 | def generate_data(self): 16 | r = "dump = {\n" 17 | for variable in self.variables: 18 | r += "\"" + variable.name + "\"" 19 | r += " : " 20 | r += str(variable.values) 21 | r += ",\n" 22 | r += "}" 23 | return r 24 | 25 | def write(self, filename): 26 | f = open(filename, "w") 27 | f.write(self.generate_data()) 28 | f.close() 29 | 30 | def read(self, filename): 31 | raise NotImplementedError("Python files can not (yet) be read, please contribute!") 32 | -------------------------------------------------------------------------------- /litescope/software/dump/sigrok.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of LiteScope. 3 | # 4 | # Copyright (c) 2015-2017 Florent Kermarrec 5 | # SPDX-License-Identifier: BSD-2-Clause 6 | 7 | import os 8 | import math 9 | import shutil 10 | import zipfile 11 | import re 12 | from collections import OrderedDict 13 | 14 | from litescope.software.dump.common import Dump, DumpVariable 15 | 16 | 17 | class SigrokDump(Dump): 18 | def __init__(self, dump=None, samplerate=None): 19 | Dump.__init__(self) 20 | self.variables = [] if dump is None else dump.variables 21 | self.samplerate = 100e6 if samplerate is None else samplerate 22 | 23 | def write_version(self): 24 | f = open("version", "w") 25 | f.write("1") 26 | f.close() 27 | 28 | def write_metadata(self, name): 29 | probe_bits = math.ceil(sum(variable.width for variable in self.variables)/8)*8 30 | f = open("metadata", "w") 31 | r = """ 32 | [global] 33 | sigrok version=0.3.0 34 | [device 1] 35 | capturefile=logic-1-1 36 | total probes={} 37 | samplerate={} MHz 38 | unitsize={} 39 | """.format( 40 | probe_bits, 41 | self.samplerate//1e6*2, 42 | probe_bits//8 43 | ) 44 | i = 1 45 | for variable in self.variables: 46 | if variable.width > 1: 47 | for j in range(variable.width): 48 | r += "probe{}={}[{}]\n".format(i, variable.name, j) 49 | i += 1 50 | else: 51 | r += "probe{}={}\n".format(i, variable.name) 52 | i += 1 53 | f.write(r) 54 | f.close() 55 | 56 | def write_data(self): 57 | data_bits = math.ceil(sum(variable.width for variable in self.variables)/8)*8 58 | data_len = 0 59 | for variable in self.variables: 60 | data_len = max(data_len, len(variable)) 61 | datas = [] 62 | for i in range(data_len): 63 | data = 0 64 | for j, variable in enumerate(reversed(self.variables)): 65 | data = data << variable.width 66 | try: 67 | data |= variable.values[i] 68 | except: 69 | pass 70 | datas.append(data) 71 | f = open("logic-1-1", "wb") 72 | for data in datas: 73 | f.write(data.to_bytes(data_bits//8, "little")) 74 | f.close() 75 | 76 | def zip(self, name): 77 | f = zipfile.ZipFile(name + ".sr", "w") 78 | os.chdir(name) 79 | f.write("version") 80 | f.write("metadata") 81 | f.write("logic-1-1") 82 | os.chdir("..") 83 | f.close() 84 | 85 | def write(self, filename): 86 | name, ext = os.path.splitext(filename) 87 | if os.path.exists(name): 88 | shutil.rmtree(name) 89 | os.makedirs(name) 90 | os.chdir(name) 91 | self.write_version() 92 | self.write_metadata(name) 93 | self.write_data() 94 | os.chdir("..") 95 | self.zip(name) 96 | shutil.rmtree(name) 97 | 98 | def unzip(self, filename, name): 99 | f = open(filename, "rb") 100 | z = zipfile.ZipFile(f) 101 | if os.path.exists(name): 102 | shutil.rmtree(name) 103 | os.makedirs(name) 104 | for file in z.namelist(): 105 | z.extract(file, name) 106 | f.close() 107 | 108 | def read_metadata(self): 109 | probes = OrderedDict() 110 | f = open("metadata", "r") 111 | for l in f: 112 | m = re.search("probe([0-9]+) = (\w+)", l, re.I) 113 | if m is not None: 114 | index = int(m.group(1)) 115 | name = m.group(2) 116 | probes[name] = index 117 | m = re.search("samplerate = ([0-9]+) kHz", l, re.I) 118 | if m is not None: 119 | self.samplerate = int(m.group(1))*1000 120 | m = re.search("samplerate = ([0-9]+) MHz", l, re.I) 121 | if m is not None: 122 | self.samplerate = int(m.group(1))*1000000 123 | f.close() 124 | return probes 125 | 126 | def read_data(self, name, nprobes): 127 | datas = [] 128 | f = open("logic-1-1", "rb") 129 | while True: 130 | data = f.read(math.ceil(nprobes/8)) 131 | if data == bytes('', "utf-8"): 132 | break 133 | data = int.from_bytes(data, "little") 134 | datas.append(data) 135 | f.close() 136 | return datas 137 | 138 | def read(self, filename): 139 | self.variables = [] 140 | name, ext = os.path.splitext(filename) 141 | self.unzip(filename, name) 142 | os.chdir(name) 143 | probes = self.read_metadata() 144 | datas = self.read_data(name, len(probes.keys())) 145 | os.chdir("..") 146 | shutil.rmtree(name) 147 | 148 | for k, v in probes.items(): 149 | probe_data = [] 150 | for data in datas: 151 | probe_data.append((data >> (v-1)) & 0x1) 152 | self.add(DumpVariable(k, 1, probe_data)) 153 | -------------------------------------------------------------------------------- /litescope/software/dump/vcd.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of LiteScope. 3 | # 4 | # Copyright (c) 2015-2018 Florent Kermarrec 5 | # SPDX-License-Identifier: BSD-2-Clause 6 | 7 | from itertools import count 8 | import datetime 9 | import re 10 | from litescope.software.dump.common import Dump, dec2bin 11 | 12 | 13 | def vcd_codes(): 14 | codechars = [chr(i) for i in range(33, 127)] 15 | for n in count(): 16 | q, r = divmod(n, len(codechars)) 17 | code = codechars[r] 18 | while q > 0: 19 | q, r = divmod(q, len(codechars)) 20 | code = codechars[r] + code 21 | yield code 22 | 23 | _si_prefix2exp = { 24 | "": 0, 25 | "m": -3, 26 | "u": -6, 27 | "n": -9, 28 | "p": -12, 29 | "f": -15, 30 | } 31 | 32 | def _timescale_str2num(timescale): 33 | match = re.fullmatch("(\d+)(\w{0,1})s", timescale) 34 | num = int(match.group(1)) 35 | si_prefix = match.group(2) 36 | exp = _si_prefix2exp[si_prefix] 37 | return num * 10**exp, si_prefix 38 | 39 | 40 | class VCDDump(Dump): 41 | def __init__(self, dump=None, samplerate=1e-12, timescale="1ps", comment=""): 42 | Dump.__init__(self) 43 | self.variables = [] if dump is None else dump.variables 44 | self.timescale = timescale 45 | self.comment = comment 46 | self.cnt = -1 47 | # rescale the timescale from the provided one to one where it is equal to the samplerate 48 | # this lets us output sequential change timestamps which helps with software like PulseView 49 | # that slow down if a much smaller timescale than necessary is used 50 | timescale_seconds, si_prefix = _timescale_str2num(timescale) 51 | # factor of 2 scale is because of 2x samples from fake clock 52 | self.count_timescale = int(1 / (timescale_seconds * samplerate * 2)) 53 | self.timescale_unit_str = si_prefix + "s" 54 | 55 | def change(self): 56 | r = "" 57 | c = "" 58 | for v in self.variables: 59 | try: 60 | val = v.values[self.cnt + 1] 61 | if val != v.current_value: 62 | v.current_value = val 63 | c += f"b{dec2bin(val, v.width)} {v.code}\n" 64 | except: 65 | pass 66 | if c != "": 67 | r += f"#{self.cnt + 1}\n{c}" 68 | return r 69 | 70 | def generate_date(self): 71 | now = datetime.datetime.now() 72 | r = "$date\n" 73 | r += "\t" 74 | r += now.strftime("%Y-%m-%d %H:%M") 75 | r += "\n" 76 | r += "$end\n" 77 | return r 78 | 79 | def generate_version(self): 80 | r = "$version\n" 81 | r += "\tlitescope VCD dump\n" 82 | r += "$end\n" 83 | return r 84 | 85 | def generate_timescale(self): 86 | r = "$timescale " 87 | r += str(self.count_timescale) + self.timescale_unit_str 88 | r += " $end\n" 89 | return r 90 | 91 | def generate_vars(self): 92 | r = "$scope dumped_signals $end\n" 93 | for v in self.variables: 94 | r += "$var wire " 95 | r += str(v.width) 96 | r += " " 97 | r += v.code 98 | r += " " 99 | r += v.name 100 | r += " $end\n" 101 | r += "$unscope " 102 | r += " $end\n" 103 | r += "$enddefinitions " 104 | r += " $end\n" 105 | return r 106 | 107 | def generate_dumpvars(self): 108 | r = "$dumpvars\n" 109 | for v in self.variables: 110 | v.current_value = "x" 111 | r += "b" 112 | r += dec2bin(v.current_value, v.width) 113 | r += " " 114 | r += v.code 115 | r += "\n" 116 | r += "$end\n" 117 | return r 118 | 119 | def generate_valuechange(self): 120 | r = "" 121 | for i in range(len(self)): 122 | r += self.change() 123 | self.cnt += 1 124 | return r 125 | 126 | def __repr__(self): 127 | r = "" 128 | return r 129 | 130 | def finalize(self): 131 | codegen = vcd_codes() 132 | for v in self.variables: 133 | v.code = next(codegen) 134 | 135 | def write(self, filename): 136 | self.finalize() 137 | f = open(filename, "w") 138 | f.write(self.generate_date()) 139 | f.write(self.generate_timescale()) 140 | f.write(self.generate_vars()) 141 | f.write(self.generate_dumpvars()) 142 | f.write(self.generate_valuechange()) 143 | f.close() 144 | 145 | def read(self, filename): 146 | raise NotImplementedError("VCD files can not (yet) be read, please contribute!") 147 | -------------------------------------------------------------------------------- /litescope/software/litescope_cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # 4 | # This file is part of LiteScope. 5 | # 6 | # Copyright (c) 2020 Antmicro 7 | # Copyright (c) 2020 Florent Kermarrec 8 | # SPDX-License-Identifier: BSD-2-Clause 9 | 10 | import os 11 | import re 12 | import csv 13 | import sys 14 | import time 15 | import threading 16 | import argparse 17 | 18 | from litex import RemoteClient 19 | from litescope import LiteScopeAnalyzerDriver 20 | 21 | # Helpers ------------------------------------------------------------------------------------------ 22 | 23 | def get_signals(csvname, group): 24 | signals = [] 25 | with open(csvname) as f: 26 | reader = csv.reader(f, delimiter=",", quotechar="#") 27 | for t, g, n, v in reader: 28 | if t == "signal" and g == str(group): 29 | signals.append(n) 30 | return signals 31 | 32 | class Finder: 33 | def __init__(self, signals): 34 | self.signals = signals 35 | 36 | def __getitem__(self, name): 37 | scores = {s: 0 for s in self.signals} 38 | # Exact match 39 | if name in scores: 40 | print("Exact:", name) 41 | return name 42 | # Substring 43 | pattern = re.compile(name) 44 | max_score = 0 45 | for s in self.signals: 46 | match = pattern.search(s) 47 | if match: 48 | scores[s] = match.end() - match.start() 49 | max_score = max(scores.values()) 50 | best = list(filter(lambda kv: kv[1] == max_score, scores.items())) 51 | assert len(best) == 1, f"Found multiple candidates: {best}" 52 | name, score = best[0] 53 | return name 54 | 55 | def add_triggers(args, analyzer, signals): 56 | added = False 57 | finder = Finder(signals) 58 | 59 | for signal in args.rising_edge or []: 60 | name = finder[signal] 61 | analyzer.add_rising_edge_trigger(name) 62 | print(f"Rising edge: {name}") 63 | added = True 64 | for signal in args.falling_edge or []: 65 | name = finder[signal] 66 | analyzer.add_falling_edge_trigger(finder[signal]) 67 | print(f"Falling edge: {name}") 68 | added = True 69 | cond = {} 70 | for signal, value in args.value_trigger or []: 71 | name = finder[signal] 72 | cond[finder[signal]] = value 73 | print(f"Condition: {name} == {value}") 74 | if cond: 75 | analyzer.add_trigger(cond=cond) 76 | added = True 77 | return added 78 | 79 | # Run Batch/GUI ----------------------------------------------------------------------------------- 80 | 81 | def run_batch(args): 82 | bus = RemoteClient(host=args.host, csr_csv=args.csr_csv) 83 | bus.open() 84 | 85 | basename = os.path.splitext(os.path.basename(args.csv))[0] 86 | signals = get_signals(args.csv, args.group) 87 | 88 | # Configure and run LiteScope analyzer. 89 | analyzer = LiteScopeAnalyzerDriver(bus.regs, basename, config_csv=args.csv, debug=True) 90 | analyzer.configure_group(args.group) 91 | analyzer.configure_subsampler(args.subsampling) 92 | if not add_triggers(args, analyzer, signals): 93 | print("No trigger, immediate capture.") 94 | analyzer.run( 95 | offset = int(args.offset, 0), 96 | length = None if args.length is None else int(args.length, 0) 97 | ) 98 | analyzer.wait_done() 99 | analyzer.upload() 100 | analyzer.save(args.dump) 101 | 102 | # Close remove control. 103 | bus.close() 104 | 105 | def run_gui(args): 106 | import dearpygui.dearpygui as dpg 107 | 108 | bus = RemoteClient(host=args.host, port=args.port, csr_csv=args.csr_csv) 109 | bus.open() 110 | 111 | triggers = get_signals(args.csv, args.group) 112 | 113 | def capture_callback(): 114 | basename = os.path.splitext(os.path.basename(args.csv))[0] 115 | analyzer = LiteScopeAnalyzerDriver(bus.regs, basename, config_csv=args.csv, debug=True) 116 | analyzer.configure_group(int(dpg.get_value(item="capture_group"), 0)) 117 | analyzer.configure_subsampler(int(dpg.get_value(item="capture_subsampling"), 0)) 118 | trigger_cond = {} 119 | for trigger in triggers: 120 | trigger_cond[trigger] = dpg.get_value(trigger) 121 | analyzer.add_trigger(cond=trigger_cond) 122 | analyzer.run( 123 | offset = int(dpg.get_value(item="capture_offset"), 0), 124 | length = int(dpg.get_value(item="capture_length"), 0), 125 | ) 126 | dpg.set_value("capture_status", "Running...") 127 | analyzer.wait_done() 128 | dpg.set_value("capture_status", "Uploading...") 129 | analyzer.upload() 130 | dpg.set_value("capture_status", "Writing...") 131 | analyzer.save(dpg.get_value(item="capture_dump")) 132 | dpg.set_value("capture_status", "Idle") 133 | 134 | dpg.create_context() 135 | dpg.create_viewport(title="LiteScope CLI GUI", max_width=400, always_on_top=True) 136 | dpg.setup_dearpygui() 137 | 138 | with dpg.window(label="Capture", autosize=True): 139 | dpg.add_text("Parameters") 140 | dpg.add_input_text(indent=8, label="Offset", tag="capture_offset", default_value=args.offset) 141 | dpg.add_input_text(indent=8, label="Length", tag="capture_length", default_value="128") # FIXME 142 | dpg.add_input_text(indent=8, label="Group", tag="capture_group", default_value="0") # FIXME 143 | dpg.add_input_text(indent=8, label="Subsampling", tag="capture_subsampling", default_value="1") # FIXME 144 | dpg.add_input_text(indent=8, label="Dump", tag="capture_dump", default_value=args.dump) 145 | dpg.add_text("Control/Status") 146 | with dpg.group(horizontal=True): 147 | dpg.add_button(label="Run", callback=capture_callback) 148 | dpg.add_text(tag="capture_status", default_value="Idle") 149 | 150 | with dpg.window(label="Triggers", autosize=True, pos=(0, 250)): 151 | for trigger in triggers: 152 | dpg.add_input_text(indent=8, label=trigger, tag=trigger, default_value="0bx", width=100) 153 | 154 | dpg.show_viewport() 155 | dpg.start_dearpygui() 156 | dpg.destroy_context() 157 | 158 | bus.close() 159 | 160 | # Main --------------------------------------------------------------------------------------------- 161 | 162 | def parse_args(): 163 | parser = argparse.ArgumentParser(description="""LiteScope Client utility""") 164 | parser.add_argument("-r", "--rising-edge", action="append", help="Add rising edge trigger.") 165 | parser.add_argument("-f", "--falling-edge", action="append", help="Add falling edge trigger.") 166 | parser.add_argument("-v", "--value-trigger", action="append", nargs=2, help="Add conditional trigger with given value.", 167 | metavar=("TRIGGER", "VALUE")) 168 | parser.add_argument("-l", "--list", action="store_true", help="List signal choices.") 169 | parser.add_argument("--host", default="localhost", help="Host ip address") 170 | parser.add_argument("--port", default="1234", help="Host bind port.") 171 | parser.add_argument("--csv", default="analyzer.csv", help="Analyzer CSV file.") 172 | parser.add_argument("--csr-csv", default="csr.csv", help="SoC CSV file.") 173 | parser.add_argument("--group", default=0, type=int, help="Capture Group.") 174 | parser.add_argument("--subsampling", default=1, type=int, help="Capture Subsampling.") 175 | parser.add_argument("--offset", default="32", help="Capture Offset.") 176 | parser.add_argument("--length", default=None, help="Capture Length.") 177 | parser.add_argument("--dump", default="dump.vcd", help="Capture Filename.") 178 | parser.add_argument("--gui", action="store_true", help="Run Gui.") 179 | args = parser.parse_args() 180 | return args 181 | 182 | def main(): 183 | args = parse_args() 184 | 185 | # Check if analyzer file is present and exit if not. 186 | if not os.path.exists(args.csv): 187 | raise ValueError("{} not found. This is necessary to load the wires which have been tapped to scope." 188 | "Try setting --csv to value of the csr_csv argument to LiteScopeAnalyzer in the SoC.".format(args.csv)) 189 | sys.exit(1) 190 | 191 | # If in list mode, list signals and exit. 192 | if args.list: 193 | signals = get_signals(args.csv, args.group) 194 | for signal in signals: 195 | print(signal) 196 | sys.exit(0) 197 | 198 | # Create and open remote control. 199 | if not os.path.exists(args.csr_csv): 200 | raise ValueError("{} not found. This is necessary to load the 'regs' of the remote. Try setting --csr-csv here to " 201 | "the path to the --csr-csv argument of the SoC build.".format(args.csr_csv)) 202 | 203 | # Run Batch/Gui. 204 | if args.gui: 205 | run_gui(args) 206 | else: 207 | run_batch(args) 208 | 209 | 210 | if __name__ == "__main__": 211 | main() 212 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from setuptools import setup 4 | from setuptools import find_packages 5 | 6 | 7 | with open("README.md", "r", encoding="utf-8") as fp: 8 | long_description = fp.read() 9 | 10 | 11 | setup( 12 | name = "litescope", 13 | version = "2025.04", 14 | description = "Small footprint and configurable embedded FPGA logic analyzer core", 15 | long_description = long_description, 16 | long_description_content_type = "text/markdown", 17 | author = "Florent Kermarrec", 18 | author_email = "florent@enjoy-digital.fr", 19 | url = "http://enjoy-digital.fr", 20 | download_url = "https://github.com/enjoy-digital/litescope", 21 | test_suite = "test", 22 | license = "BSD", 23 | python_requires = "~=3.7", 24 | install_requires = ["pyyaml", "litex"], 25 | extras_require = { 26 | "develop": [ 27 | "meson" 28 | "pexpect" 29 | "setuptools" 30 | "requests" 31 | ] 32 | }, 33 | packages = find_packages(exclude=("test*", "sim*", "doc*", "examples*")), 34 | include_package_data = True, 35 | keywords = "HDL ASIC FPGA hardware design", 36 | classifiers = [ 37 | "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)", 38 | "Environment :: Console", 39 | "Development Status :: 3 - Alpha", 40 | "Intended Audience :: Developers", 41 | "License :: OSI Approved :: BSD License", 42 | "Operating System :: OS Independent", 43 | "Programming Language :: Python", 44 | ], 45 | entry_points = { 46 | "console_scripts": [ 47 | "litescope_cli=litescope.software.litescope_cli:main", 48 | ], 49 | }, 50 | ) 51 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enjoy-digital/litescope/3898771a9d1cff274e30fdd81883bb43f837f853/test/__init__.py -------------------------------------------------------------------------------- /test/test_analyzer.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of LiteScope. 3 | # 4 | # Copyright (c) 2017-2019 Florent Kermarrec 5 | # SPDX-License-Identifier: BSD-2-Clause 6 | 7 | import unittest 8 | 9 | from migen import * 10 | 11 | from litescope import LiteScopeAnalyzer 12 | 13 | 14 | class TestAnalyzer(unittest.TestCase): 15 | def test_analyzer(self): 16 | def generator(dut): 17 | dut.data = [] 18 | # Configure Trigger 19 | yield from dut.analyzer.trigger.mem_value.write(0x0010) 20 | yield from dut.analyzer.trigger.mem_mask.write(0xffff) 21 | yield from dut.analyzer.trigger.mem_write.write(1) 22 | 23 | # Configure Subsampler 24 | yield from dut.analyzer.subsampler.value.write(2) 25 | 26 | # Configure Storage 27 | yield from dut.analyzer.storage.length.write(256) 28 | yield from dut.analyzer.storage.offset.write(8) 29 | yield from dut.analyzer.storage.enable.write(1) 30 | yield 31 | for i in range(16): 32 | yield 33 | # Wait capture 34 | while not (yield from dut.analyzer.storage.done.read()): 35 | yield 36 | # Read captured datas 37 | while (yield from dut.analyzer.storage.mem_level.read()) > 0: 38 | dut.data.append((yield from dut.analyzer.storage.mem_data.read())) 39 | yield 40 | 41 | class DUT(Module): 42 | def __init__(self): 43 | counter = Signal(32) 44 | self.sync += counter.eq(counter + 1) 45 | self.submodules.analyzer = LiteScopeAnalyzer(counter, 512) 46 | 47 | dut = DUT() 48 | generators = {"sys" : [generator(dut)]} 49 | clocks = {"sys": 10, "scope": 10} 50 | run_simulation(dut, generators, clocks, vcd_name="sim.vcd") 51 | self.assertEqual(dut.data, [524 + 3*i for i in range(len(dut.data))]) 52 | -------------------------------------------------------------------------------- /test/test_dump.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of LiteScope. 3 | # 4 | # Copyright (c) 2017 Florent Kermarrec 5 | # SPDX-License-Identifier: BSD-2-Clause 6 | 7 | import unittest 8 | import os 9 | from math import cos, sin 10 | 11 | from litescope.software.dump import * 12 | 13 | #TODO: 14 | # - find a way to check if files are generated correctly 15 | 16 | dump = Dump() 17 | for i in range(4): 18 | dump.add(DumpVariable("ramp"+str(i), 2**i, [j for j in range(256)])) 19 | pi = 3.1415 20 | dump.add(DumpVariable("sin", 8, [128+128*sin(j/(2*pi*16)) for j in range(1024)])) 21 | dump.add(DumpVariable("cos", 8, [128+128*cos(j/(2*pi*16)) for j in range(1024)])) 22 | 23 | 24 | class TestDump(unittest.TestCase): 25 | def test_csv(self): 26 | filename = "dump.csv" 27 | CSVDump(dump).write(filename) 28 | os.remove(filename) 29 | 30 | def test_py(self): 31 | filename = "dump.py" 32 | PythonDump(dump).write(filename) 33 | os.remove(filename) 34 | 35 | def test_sigrok(self): 36 | filename = "dump.sr" 37 | SigrokDump(dump).write(filename) 38 | SigrokDump(dump).read(filename) 39 | SigrokDump(dump).write(filename) 40 | os.remove(filename) 41 | 42 | def test_vcd(self): 43 | filename = "dump.vcd" 44 | VCDDump(dump).write(filename) 45 | os.remove(filename) 46 | -------------------------------------------------------------------------------- /test/test_examples.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of LiteScope. 3 | # 4 | # Copyright (c) 2019 Florent Kermarrec 5 | # SPDX-License-Identifier: BSD-2-Clause 6 | 7 | import unittest 8 | import os 9 | 10 | from litescope.software.dump import * 11 | 12 | root_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..") 13 | 14 | class TestExamples(unittest.TestCase): 15 | def test_arty(self): 16 | os.system(f"rm -rf {root_dir}/build") 17 | os.system(f"cd {root_dir}/examples && python3 arty.py") 18 | self.assertEqual(os.path.isfile(f"{root_dir}/examples/build/digilent_arty/gateware/digilent_arty.v"), True) 19 | --------------------------------------------------------------------------------