├── .gitignore ├── LICENSE ├── README.md ├── demo.py ├── microscope ├── __init__.py ├── config.py ├── core.py ├── globals.py ├── inserts.py ├── microscope.py └── uart.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | *.egg-info 4 | build 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2017 M-Labs Limited. 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Microscope 2 | ========== 3 | 4 | A simple FPGA logic analyzer for Migen designs. 5 | 6 | Microscope only requires two RS232 UART pins and a clock to work, and is highly 7 | portable. It is the tool of choice when everything else in your FPGA is falling 8 | apart. It is more feature-limited than C...scope, but it uses kilobytes instead 9 | of gigabytes, and it will work without involving Intellectual Poverty (IP) or 10 | drivers with more bugs than a rain forest. 11 | 12 | Probes can be inserted anywhere in the target design using the ``add_probe_*`` 13 | global functions. Those functions return submodules that you must add to the 14 | current module (this enables the probes to see the clock domains of the current 15 | module). 16 | 17 | The logic analyzer component ``Microscope`` can be instantiated anywhere in 18 | the design, typically at the top-level. If ``Microscope`` is not instantiated, 19 | or the probes are filtered out, then the probes generate no logic and can be 20 | left in their respective cores without consuming FPGA resources. 21 | 22 | Use the communication program ``microscope.py`` to read back data from the 23 | probes. 24 | 25 | See ``demo.py`` for an example design. 26 | -------------------------------------------------------------------------------- /demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from migen import * 4 | from migen.build.platforms import kc705 5 | from migen.genlib.io import CRG 6 | 7 | from microscope import * 8 | 9 | 10 | class MicroscopeDemo(Module): 11 | def __init__(self, serial_pads, sys_clk_freq): 12 | counter = Signal(32) 13 | toggle1 = Signal() 14 | toggle2 = Signal() 15 | self.comb += [ 16 | toggle1.eq(counter[29]), 17 | toggle2.eq(counter[28]) 18 | ] 19 | self.sync += counter.eq(counter + 1) 20 | 21 | self.submodules += [ 22 | add_probe_single("demo", "toggle1", toggle1), 23 | add_probe_single("demo", "toggle2", toggle2), 24 | add_probe_buffer("demo", "counter", counter) 25 | ] 26 | 27 | self.submodules += Microscope(serial_pads, sys_clk_freq) 28 | 29 | 30 | def main(): 31 | platform = kc705.Platform() 32 | top = MicroscopeDemo(platform.request("serial"), 1e9/platform.default_clk_period) 33 | clock = platform.request(platform.default_clk_name) 34 | top.submodules += CRG(clock) 35 | platform.build(top) 36 | 37 | 38 | if __name__ == "__main__": 39 | main() 40 | -------------------------------------------------------------------------------- /microscope/__init__.py: -------------------------------------------------------------------------------- 1 | from microscope.globals import add_probe_async, add_probe_single, add_probe_buffer 2 | from microscope.core import Microscope 3 | -------------------------------------------------------------------------------- /microscope/config.py: -------------------------------------------------------------------------------- 1 | import msgpack 2 | 3 | from microscope.inserts import * 4 | 5 | 6 | __all__ = ["get_config_from_inserts"] 7 | 8 | 9 | def get_config_from_inserts(inserts): 10 | config_groups = [] 11 | for insert in inserts: 12 | if insert.group not in config_groups: 13 | config_groups.append(insert.group) 14 | 15 | config_inserts = [] 16 | for insert in inserts: 17 | element = [config_groups.index(insert.group), 18 | insert.name] 19 | if isinstance(insert, (ProbeAsync, ProbeSingle)): 20 | element += [len(insert.data), 1] 21 | elif isinstance(insert, ProbeBuffer): 22 | element += [len(insert.data), insert.depth] 23 | else: 24 | raise ValueError 25 | config_inserts.append(element) 26 | 27 | config = { 28 | "grp": config_groups, 29 | "ins": config_inserts 30 | } 31 | return msgpack.packb(config, use_bin_type=True) 32 | -------------------------------------------------------------------------------- /microscope/core.py: -------------------------------------------------------------------------------- 1 | from migen import * 2 | from migen.genlib.fsm import * 3 | 4 | from microscope.globals import registry as global_registry 5 | from microscope.config import * 6 | from microscope.uart import UART 7 | 8 | 9 | class ConfigROM(Module): 10 | def __init__(self, data): 11 | self.data = Signal(8) 12 | self.next = Signal() 13 | self.reset = Signal() 14 | self.last = Signal() 15 | 16 | mem = Memory(8, len(data), init=data) 17 | port = mem.get_port() 18 | self.specials += mem, port 19 | 20 | current_address = Signal(max=len(data)) 21 | self.sync += current_address.eq(port.adr) 22 | self.comb += [ 23 | self.data.eq(port.dat_r), 24 | port.adr.eq(current_address), 25 | If(self.next, 26 | port.adr.eq(current_address + 1) 27 | ), 28 | If(self.reset, 29 | port.adr.eq(0) 30 | ), 31 | self.last.eq(current_address == len(data)-1) 32 | ] 33 | 34 | 35 | class InsertMux(Module): 36 | def __init__(self, inserts): 37 | max_depth = max(getattr(insert, "depth", 1) for insert in inserts) 38 | max_width = max(len(insert.data) for insert in inserts) 39 | 40 | if len(inserts) > 1: 41 | self.sel = Signal(max=len(inserts)) 42 | self.arm = Signal() 43 | self.pending = Signal() 44 | self.address = Signal(max=max(max_depth, 2)) 45 | self.data = Signal(max_width) 46 | 47 | self.last_address = Signal(max=max(max_depth, 2)) 48 | self.last_byte = Signal(max=max((max_width+7)//8, 2)) 49 | 50 | # # # 51 | 52 | if len(inserts) > 1: 53 | sel = self.sel 54 | else: 55 | sel = 0 56 | self.comb += [ 57 | self.pending.eq(Array(getattr(insert, "pending", 0) for insert in inserts)[sel]), 58 | self.data.eq(Array(insert.data for insert in inserts)[sel]) 59 | ] 60 | for n, insert in enumerate(inserts): 61 | if hasattr(insert, "arm"): 62 | self.comb += insert.arm.eq(self.arm & (sel == n)) 63 | if hasattr(insert, "address"): 64 | self.comb += insert.address.eq(self.address) 65 | self.comb += [ 66 | self.last_address.eq(Array(getattr(insert, "depth", 1)-1 for insert in inserts)[sel]), 67 | self.last_byte.eq(Array((len(insert.data)+7)//8-1 for insert in inserts)[sel]) 68 | ] 69 | 70 | 71 | class SerialProtocolEngine(Module): 72 | def __init__(self, config_rom, imux, timeout_cycles): 73 | self.rx_data = Signal(8) 74 | self.rx_stb = Signal() 75 | 76 | self.tx_data = Signal(8) 77 | self.tx_stb = Signal() 78 | self.tx_ack = Signal() 79 | 80 | # # # 81 | 82 | timeout = Signal() 83 | timeout_counter = Signal(max=timeout_cycles + 1, reset=timeout_cycles) 84 | self.sync += [ 85 | timeout.eq(0), 86 | If(self.tx_stb | self.rx_stb, 87 | timeout_counter.eq(timeout_cycles) 88 | ).Else( 89 | If(timeout_counter == 0, 90 | timeout.eq(1), 91 | timeout_counter.eq(timeout_cycles) 92 | ).Else( 93 | timeout_counter.eq(timeout_counter - 1) 94 | ) 95 | ) 96 | ] 97 | 98 | next_address = Signal() 99 | reset_address = Signal() 100 | last_address = Signal() 101 | current_address = Signal.like(imux.address) 102 | self.sync += current_address.eq(imux.address) 103 | self.comb += [ 104 | imux.address.eq(current_address), 105 | If(next_address, 106 | imux.address.eq(current_address + 1) 107 | ), 108 | If(reset_address, 109 | imux.address.eq(0) 110 | ), 111 | last_address.eq(current_address == imux.last_address) 112 | ] 113 | 114 | next_byte = Signal() 115 | reset_byte = Signal() 116 | last_byte = Signal() 117 | current_byte = Signal.like(imux.last_byte) 118 | data = Signal(8) 119 | nbytes = (len(imux.data)+7)//8 120 | self.sync += [ 121 | If(next_byte, 122 | Case(current_byte, { 123 | i: data.eq(imux.data[((i+1)*8):]) for i in range(nbytes-1) 124 | }), 125 | current_byte.eq(current_byte + 1) 126 | ), 127 | If(reset_byte, 128 | data.eq(imux.data), 129 | current_byte.eq(0) 130 | ) 131 | ] 132 | self.comb += last_byte.eq(current_byte == imux.last_byte) 133 | 134 | imux_sel_load = Signal() 135 | if hasattr(imux, "sel"): 136 | self.sync += If(imux_sel_load, imux.sel.eq(self.rx_data)) 137 | 138 | fsm = ResetInserter()(FSM()) 139 | self.submodules += fsm 140 | self.comb += fsm.reset.eq(timeout) 141 | 142 | fsm.act("MAGIC1", 143 | If(self.rx_stb, 144 | If(self.rx_data == 0x1a, 145 | NextState("MAGIC2") 146 | ).Else( 147 | NextState("MAGIC1") 148 | ) 149 | ) 150 | ) 151 | fsm.act("MAGIC2", 152 | If(self.rx_stb, 153 | If(self.rx_data == 0xe5, 154 | NextState("MAGIC3") 155 | ).Else( 156 | NextState("MAGIC1") 157 | ) 158 | ) 159 | ) 160 | fsm.act("MAGIC3", 161 | If(self.rx_stb, 162 | If(self.rx_data == 0x52, 163 | NextState("MAGIC4") 164 | ).Else( 165 | NextState("MAGIC1") 166 | ) 167 | ) 168 | ) 169 | fsm.act("MAGIC4", 170 | If(self.rx_stb, 171 | If(self.rx_data == 0x9c, 172 | NextState("COMMAND") 173 | ).Else( 174 | NextState("MAGIC1") 175 | ) 176 | ) 177 | ) 178 | fsm.act("COMMAND", 179 | config_rom.reset.eq(1), 180 | reset_address.eq(1), 181 | reset_byte.eq(1), 182 | If(self.rx_stb, 183 | Case(self.rx_data, { 184 | 0x00: NextState("SEND_CONFIG"), 185 | 0x01: NextState("SET_SEL"), 186 | 0x02: imux.arm.eq(1), 187 | 0x03: NextState("SEND_PENDING"), 188 | 0x04: NextState("SEND_DATA") 189 | }) 190 | ) 191 | ) 192 | fsm.act("SEND_CONFIG", 193 | self.tx_stb.eq(1), 194 | self.tx_data.eq(config_rom.data), 195 | If(self.tx_ack, 196 | config_rom.next.eq(1), 197 | If(config_rom.last, NextState("MAGIC1")) 198 | ) 199 | ) 200 | fsm.act("SET_SEL", 201 | If(self.rx_stb, 202 | imux_sel_load.eq(1), 203 | NextState("MAGIC1") 204 | ) 205 | ) 206 | fsm.act("SEND_PENDING", 207 | self.tx_stb.eq(1), 208 | self.tx_data.eq(imux.pending), 209 | If(self.tx_ack, NextState("MAGIC1")) 210 | ) 211 | fsm.act("SEND_DATA", 212 | self.tx_stb.eq(1), 213 | self.tx_data.eq(data), 214 | If(self.tx_ack, 215 | next_byte.eq(1), 216 | If(last_byte, 217 | next_address.eq(1), 218 | If(last_address, 219 | NextState("MAGIC1") 220 | ).Else( 221 | NextState("RESET_BYTE") 222 | ) 223 | ) 224 | ) 225 | ) 226 | fsm.act("RESET_BYTE", 227 | reset_byte.eq(1), 228 | NextState("SEND_DATA") 229 | ) 230 | 231 | 232 | class Microscope(Module): 233 | def __init__(self, serial_pads, sys_clk_freq, registry=None): 234 | self.serial_pads = serial_pads 235 | self.sys_clk_freq = sys_clk_freq 236 | if registry is None: 237 | registry = global_registry 238 | self.registry = registry 239 | 240 | self.clock_domains.cd_microscope = ClockDomain(reset_less=True) 241 | self.comb += self.cd_microscope.clk.eq(ClockSignal()) 242 | 243 | def do_finalize(self): 244 | inserts = [insert for insert in self.registry.inserts 245 | if self.registry.is_enabled(insert)] 246 | if not inserts: 247 | return 248 | for insert in inserts: 249 | insert.create_insert_logic() 250 | 251 | config_rom = ConfigROM(list(get_config_from_inserts(inserts))) 252 | imux = InsertMux(inserts) 253 | spe = SerialProtocolEngine(config_rom, imux, round(self.sys_clk_freq*50e-3)) 254 | uart = UART(self.serial_pads, round((115200/self.sys_clk_freq)*2**32)) 255 | self.submodules += config_rom, imux, spe, uart 256 | 257 | self.comb += [ 258 | spe.rx_data.eq(uart.rx_data), 259 | spe.rx_stb.eq(uart.rx_stb), 260 | uart.tx_data.eq(spe.tx_data), 261 | uart.tx_stb.eq(spe.tx_stb), 262 | spe.tx_ack.eq(uart.tx_ack) 263 | ] 264 | -------------------------------------------------------------------------------- /microscope/globals.py: -------------------------------------------------------------------------------- 1 | from microscope.inserts import * 2 | 3 | 4 | registry = InsertRegistry() 5 | 6 | 7 | def add_probe_async(*args, **kwargs): 8 | return ProbeAsync(registry, *args, **kwargs) 9 | 10 | 11 | def add_probe_single(*args, **kwargs): 12 | return ProbeSingle(registry, *args, **kwargs) 13 | 14 | 15 | def add_probe_buffer(*args, **kwargs): 16 | return ProbeBuffer(registry, *args, **kwargs) 17 | -------------------------------------------------------------------------------- /microscope/inserts.py: -------------------------------------------------------------------------------- 1 | from migen import * 2 | from migen.genlib.cdc import PulseSynchronizer, MultiReg 3 | 4 | 5 | __all__ = ["InsertRegistry", "ProbeAsync", "ProbeSingle", "ProbeBuffer"] 6 | 7 | 8 | class InsertRegistry: 9 | def __init__(self): 10 | self.filter = None 11 | self.inserts = [] 12 | 13 | def is_enabled(self, insert): 14 | if self.filter is None: 15 | return True 16 | else: 17 | return insert.group in self.filter 18 | 19 | def register(self, insert): 20 | self.inserts.append(insert) 21 | 22 | 23 | class Insert(Module): 24 | def __init__(self, registry, group, name): 25 | self.group = group 26 | self.name = name 27 | registry.register(self) 28 | 29 | def create_insert_logic(self): 30 | raise NotImplementedError 31 | 32 | 33 | class ProbeAsync(Insert): 34 | def __init__(self, registry, group, name, target): 35 | Insert.__init__(self, registry, group, name) 36 | self.target = target 37 | 38 | def create_insert_logic(self): 39 | self.data = Signal.like(self.target) 40 | self.specials += MultiReg(self.target, self.data, "microscope") 41 | 42 | 43 | class ProbeSingle(Insert): 44 | def __init__(self, registry, group, name, target, clock_domain="sys"): 45 | Insert.__init__(self, registry, group, name) 46 | self.target = target 47 | self.clock_domain = clock_domain 48 | 49 | def create_insert_logic(self): 50 | self.arm = Signal() 51 | self.pending = Signal() 52 | self.data = Signal.like(self.target) 53 | 54 | buf = Signal.like(self.target) 55 | buf.attr.add("no_retiming") 56 | self.specials += MultiReg(buf, self.data, "microscope") 57 | 58 | ps_arm = PulseSynchronizer("microscope", self.clock_domain) 59 | ps_done = PulseSynchronizer(self.clock_domain, "microscope") 60 | self.submodules += ps_arm, ps_done 61 | self.comb += ps_arm.i.eq(self.arm) 62 | self.sync.microscope += [ 63 | If(ps_done.o, self.pending.eq(0)), 64 | If(self.arm, self.pending.eq(1)) 65 | ] 66 | 67 | sync = getattr(self.sync, self.clock_domain) 68 | sync += [ 69 | ps_done.i.eq(0), 70 | If(ps_arm.o, 71 | buf.eq(self.target), 72 | ps_done.i.eq(1) 73 | ) 74 | ] 75 | 76 | 77 | class ProbeBuffer(Insert): 78 | def __init__(self, registry, group, name, target, trigger=1, depth=256, clock_domain="sys"): 79 | Insert.__init__(self, registry, group, name) 80 | self.target = target 81 | self.trigger = trigger 82 | self.depth = depth 83 | self.clock_domain = clock_domain 84 | 85 | def create_insert_logic(self): 86 | self.arm = Signal() 87 | self.pending = Signal() 88 | self.address = Signal(max=self.depth) 89 | self.data = Signal(len(self.target)) 90 | self.specials.memory = Memory(len(self.target), self.depth) 91 | 92 | rdport = self.memory.get_port(clock_domain="microscope") 93 | self.specials += rdport 94 | self.comb += [ 95 | rdport.adr.eq(self.address), 96 | self.data.eq(rdport.dat_r) 97 | ] 98 | 99 | ps_arm = PulseSynchronizer("microscope", self.clock_domain) 100 | ps_done = PulseSynchronizer(self.clock_domain, "microscope") 101 | self.submodules += ps_arm, ps_done 102 | self.comb += ps_arm.i.eq(self.arm) 103 | self.sync.microscope += [ 104 | If(ps_done.o, self.pending.eq(0)), 105 | If(self.arm, self.pending.eq(1)) 106 | ] 107 | 108 | port = self.memory.get_port(write_capable=True, 109 | clock_domain=self.clock_domain) 110 | self.specials += port 111 | 112 | running = Signal() 113 | wait_trigger = Signal() 114 | sync = getattr(self.sync, self.clock_domain) 115 | sync += [ 116 | ps_done.i.eq(0), 117 | If(running, 118 | port.adr.eq(port.adr + 1), 119 | If(port.adr == self.depth-1, 120 | running.eq(0), 121 | ps_done.i.eq(1) 122 | ) 123 | ), 124 | If(wait_trigger & self.trigger, 125 | running.eq(1), 126 | wait_trigger.eq(0) 127 | ), 128 | If(ps_arm.o, 129 | wait_trigger.eq(1) 130 | ) 131 | ] 132 | self.comb += port.we.eq(running) 133 | sync += port.dat_w.eq(self.target) 134 | -------------------------------------------------------------------------------- /microscope/microscope.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import argparse 5 | import struct 6 | 7 | import serial 8 | import msgpack 9 | import prettytable 10 | 11 | 12 | class Comm: 13 | magic = b"\x1a\xe5\x52\x9c" 14 | 15 | def __init__(self, port_url): 16 | self.ser = serial.serial_for_url(port_url, baudrate=115200) 17 | 18 | def close(self): 19 | self.ser.close() 20 | 21 | def get_config(self): 22 | self.ser.write(Comm.magic + b"\x00") 23 | return next(msgpack.Unpacker(self.ser, read_size=1)) 24 | 25 | def select(self, insert): 26 | self.ser.write(Comm.magic + b"\x01" + struct.pack("B", insert)) 27 | 28 | def arm(self): 29 | self.ser.write(Comm.magic + b"\x02") 30 | 31 | def pending(self): 32 | self.ser.write(Comm.magic + b"\x03") 33 | return struct.unpack("?", self.ser.read(1))[0] 34 | 35 | def data(self, length): 36 | self.ser.write(Comm.magic + b"\x04") 37 | return self.ser.read(length) 38 | 39 | 40 | def display_inserts(comm): 41 | config = comm.get_config() 42 | table = prettytable.PrettyTable(["Group", "Name", "Width", "Depth"]) 43 | for group, name, width, depth in config["ins"]: 44 | group = config["grp"][group] 45 | table.add_row([group, name, width, depth]) 46 | print(table) 47 | 48 | 49 | def display_singles(comm): 50 | config = comm.get_config() 51 | table = prettytable.PrettyTable(["Group", "Name", "Value"]) 52 | for i, (group, name, width, depth) in enumerate(config["ins"]): 53 | if depth == 1: 54 | comm.select(i) 55 | comm.arm() 56 | while comm.pending(): 57 | pass 58 | data = comm.data((width+7)//8) 59 | value = int.from_bytes(data, "little") 60 | group = config["grp"][group] 61 | table.add_row([group, name, hex(value)]) 62 | print(table) 63 | 64 | 65 | def monitor_single(comm, q_group, q_name, q_n): 66 | config = comm.get_config() 67 | try: 68 | q_group = config["grp"].index(q_group) 69 | except IndexError: 70 | raise SystemExit("Group not found") 71 | found = None 72 | n = 0 73 | for i, (group, name, width, depth) in enumerate(config["ins"]): 74 | if group == q_group and name == q_name and depth == 1: 75 | if q_n is None or n == q_n: 76 | if found is not None: 77 | raise SystemExit("More than one insert matches") 78 | found = i 79 | if found is None: 80 | raise SystemExit("Insert not found") 81 | 82 | _, _, width, _ = config["ins"][found] 83 | fmtstring = "{:0" + str((width+3)//4) + "x}" 84 | toggle = False 85 | comm.select(found) 86 | while True: 87 | comm.arm() 88 | while comm.pending(): 89 | pass 90 | data = comm.data((width+7)//8) 91 | value = int.from_bytes(data, "little") 92 | print(("/ " if toggle else "\\ ") + fmtstring.format(value), 93 | end="\r", flush=True) 94 | toggle = not toggle 95 | 96 | 97 | def display_buffer(comm, q_group, q_name, q_n): 98 | config = comm.get_config() 99 | try: 100 | q_group = config["grp"].index(q_group) 101 | except IndexError: 102 | raise SystemExit("Group not found") 103 | found = False 104 | n = 0 105 | for i, (group, name, width, depth) in enumerate(config["ins"]): 106 | if group == q_group and name == q_name: 107 | if q_n is None or n == q_n: 108 | found = True 109 | comm.select(i) 110 | comm.arm() 111 | print("waiting for trigger...", file=sys.stderr) 112 | while comm.pending(): 113 | pass 114 | print("done", file=sys.stderr) 115 | 116 | word_len = (width+7)//8 117 | data = comm.data(depth*word_len) 118 | print("[") 119 | for j in range(depth): 120 | print(hex(int.from_bytes(data[j*word_len:(j+1)*word_len], "little")) + ",") 121 | print("]") 122 | n += 1 123 | if not found: 124 | raise SystemExit("Insert not found") 125 | 126 | 127 | def main(): 128 | parser = argparse.ArgumentParser(description="Microscope FPGA logic analyzer client") 129 | parser.add_argument("port", help="serial port URL (see open_for_url in pyserial)") 130 | subparsers = parser.add_subparsers(dest="action") 131 | subparsers.add_parser("inserts", help="list inserts available on the target device") 132 | subparsers.add_parser("singles", help="show current values of single-value inserts") 133 | parser_monitor = subparsers.add_parser("monitor", help="continously monitor the value of a single-value insert") 134 | parser_monitor.add_argument("group", metavar="GROUP") 135 | parser_monitor.add_argument("name", metavar="NAME") 136 | parser_monitor.add_argument("-n", type=int, default=None, 137 | help="index (in case of multiple matches)") 138 | parser_buffer = subparsers.add_parser("buffer", help="show values of a buffering insert") 139 | parser_buffer.add_argument("group", metavar="GROUP") 140 | parser_buffer.add_argument("name", metavar="NAME") 141 | parser_buffer.add_argument("-n", type=int, default=None, 142 | help="index (in case of multiple matches)") 143 | args = parser.parse_args() 144 | 145 | comm = Comm(args.port) 146 | try: 147 | if args.action is None or args.action == "inserts": 148 | display_inserts(comm) 149 | elif args.action == "singles": 150 | display_singles(comm) 151 | elif args.action == "monitor": 152 | monitor_single(comm, args.group, args.name, args.n) 153 | elif args.action == "buffer": 154 | display_buffer(comm, args.group, args.name, args.n) 155 | finally: 156 | comm.close() 157 | 158 | 159 | if __name__ == "__main__": 160 | main() 161 | -------------------------------------------------------------------------------- /microscope/uart.py: -------------------------------------------------------------------------------- 1 | from migen import * 2 | from migen.genlib.cdc import MultiReg 3 | 4 | 5 | class UART(Module): 6 | def __init__(self, pads, tuning_word): 7 | self.rx_data = Signal(8) 8 | self.rx_stb = Signal() 9 | 10 | self.tx_data = Signal(8) 11 | self.tx_stb = Signal() 12 | self.tx_ack = Signal() 13 | 14 | # # # 15 | 16 | # 17 | # RX 18 | # 19 | 20 | uart_clk_rxen = Signal() 21 | phase_accumulator_rx = Signal(32) 22 | 23 | rx = Signal() 24 | self.specials += MultiReg(pads.rx, rx) 25 | rx_r = Signal() 26 | rx_reg = Signal(8) 27 | rx_bitcount = Signal(4) 28 | rx_busy = Signal() 29 | rx_done = self.rx_stb 30 | rx_data = self.rx_data 31 | self.sync += [ 32 | rx_done.eq(0), 33 | rx_r.eq(rx), 34 | If(~rx_busy, 35 | If(~rx & rx_r, # look for start bit 36 | rx_busy.eq(1), 37 | rx_bitcount.eq(0), 38 | ) 39 | ).Else( 40 | If(uart_clk_rxen, 41 | rx_bitcount.eq(rx_bitcount + 1), 42 | If(rx_bitcount == 0, 43 | If(rx, # verify start bit 44 | rx_busy.eq(0) 45 | ) 46 | ).Elif(rx_bitcount == 9, 47 | rx_busy.eq(0), 48 | If(rx, # verify stop bit 49 | rx_data.eq(rx_reg), 50 | rx_done.eq(1) 51 | ) 52 | ).Else( 53 | rx_reg.eq(Cat(rx_reg[1:], rx)) 54 | ) 55 | ) 56 | ) 57 | ] 58 | self.sync += \ 59 | If(rx_busy, 60 | Cat(phase_accumulator_rx, uart_clk_rxen).eq(phase_accumulator_rx + tuning_word) 61 | ).Else( 62 | Cat(phase_accumulator_rx, uart_clk_rxen).eq(2**31) 63 | ) 64 | 65 | # 66 | # TX 67 | # 68 | uart_clk_txen = Signal() 69 | phase_accumulator_tx = Signal(32) 70 | 71 | pads.tx.reset = 1 72 | 73 | tx_reg = Signal(8) 74 | tx_bitcount = Signal(4) 75 | tx_busy = Signal() 76 | self.sync += [ 77 | self.tx_ack.eq(0), 78 | If(self.tx_stb & ~tx_busy & ~self.tx_ack, 79 | tx_reg.eq(self.tx_data), 80 | tx_bitcount.eq(0), 81 | tx_busy.eq(1), 82 | pads.tx.eq(0) 83 | ).Elif(uart_clk_txen & tx_busy, 84 | tx_bitcount.eq(tx_bitcount + 1), 85 | If(tx_bitcount == 8, 86 | pads.tx.eq(1) 87 | ).Elif(tx_bitcount == 9, 88 | pads.tx.eq(1), 89 | tx_busy.eq(0), 90 | self.tx_ack.eq(1), 91 | ).Else( 92 | pads.tx.eq(tx_reg[0]), 93 | tx_reg.eq(Cat(tx_reg[1:], 0)) 94 | ) 95 | ) 96 | ] 97 | self.sync += \ 98 | If(tx_busy, 99 | Cat(phase_accumulator_tx, uart_clk_txen).eq(phase_accumulator_tx + tuning_word) 100 | ).Else( 101 | Cat(phase_accumulator_tx, uart_clk_txen).eq(0) 102 | ) 103 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from setuptools import setup 5 | from setuptools import find_packages 6 | 7 | 8 | if sys.version_info[:3] < (3, 5): 9 | raise SystemExit("You need Python 3.5+") 10 | 11 | 12 | setup( 13 | name="microscope", 14 | version="1.4", 15 | description="A simple FPGA logic analyzer for Migen designs", 16 | author="Sebastien Bourdeauducq", 17 | author_email="sb@m-labs.hk", 18 | url="https://m-labs.hk", 19 | download_url="https://github.com/m-labs/microscope", 20 | license="BSD", 21 | platforms=["Any"], 22 | keywords="HDL ASIC FPGA hardware design", 23 | classifiers=[ 24 | "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)", 25 | "Environment :: Console", 26 | "Development Status :: Beta", 27 | "Intended Audience :: Developers", 28 | "License :: OSI Approved :: BSD License", 29 | "Operating System :: OS Independent", 30 | "Programming Language :: Python", 31 | ], 32 | packages=find_packages(), 33 | install_requires=["migen", "pyserial", "msgpack>=1.0.0", "prettytable"], 34 | include_package_data=True, 35 | entry_points={ 36 | "console_scripts": ["microscope = microscope.microscope:main"], 37 | }, 38 | ) 39 | --------------------------------------------------------------------------------