X-Git-Url: https://git.libre-soc.org/?a=blobdiff_plain;f=c4m%2Fnmigen%2Fjtag%2Ftap.py;h=f5bfd8cc676e02f0e9e53b882a41c2cac25e6bf7;hb=HEAD;hp=c10ff3a7c59dc476a09a7146fa65d9f1b00ee5cf;hpb=1186750991cca4ce5d22b187bbabe41fef642359;p=c4m-jtag.git diff --git a/c4m/nmigen/jtag/tap.py b/c4m/nmigen/jtag/tap.py index c10ff3a..f5bfd8c 100755 --- a/c4m/nmigen/jtag/tap.py +++ b/c4m/nmigen/jtag/tap.py @@ -1,4 +1,9 @@ #!/usr/bin/env python3 +#!/bin/env python3 +# Copyright (C) 2019,2020,2021 Staf Verhaegen +# Copyright (C) 2021,2022 Luke Kenneth Casson Leighton +# Funded by NLnet and NGI POINTER under EU Grants 871528 and 957073 + import os, textwrap from enum import Enum, auto @@ -9,7 +14,7 @@ from nmigen.tracer import get_var_name from nmigen_soc.wishbone import Interface as WishboneInterface -from .bus import Interface +from .bus import Interface, DMIInterface __all__ = [ "TAP", "ShiftReg", "IOType", "IOConn", @@ -161,8 +166,15 @@ class IOType(Enum): class IOConn(Record): + lengths = { + IOType.In: 1, + IOType.Out: 1, + IOType.TriOut: 2, + IOType.InTriOut: 3, + } + """TAP subblock representing the interface for an JTAG IO cell. - It contains signal to connect to the core and to the pad + It contains signals to connect to the core and to the pad This object is normally only allocated and returned from ``TAP.add_io`` It is a Record subclass. @@ -172,20 +184,24 @@ class IOConn(Record): core: subrecord with signals for the core i: Signal(1), present only for IOType.In and IOType.InTriOut. Signal input to core with pad input value. - o: Signal(1), present only for IOType.Out, IOType.TriOut and IOType.InTriOut. + o: Signal(1), present only for IOType.Out, IOType.TriOut and + IOType.InTriOut. Signal output from core with the pad output value. oe: Signal(1), present only for IOType.TriOut and IOType.InTriOut. Signal output from core with the pad output enable value. pad: subrecord with for the pad i: Signal(1), present only for IOType.In and IOType.InTriOut Output from pad with pad input value for core. - o: Signal(1), present only for IOType.Out, IOType.TriOut and IOType.InTriOut. + o: Signal(1), present only for IOType.Out, IOType.TriOut and + IOType.InTriOut. Input to pad with pad output value. oe: Signal(1), present only for IOType.TriOut and IOType.InTriOut. Input to pad with pad output enable value. + + bank select, pullup and pulldown may also be optionally added """ @staticmethod - def layout(iotype): + def layout(iotype, banksel=0, pullup=False, pulldown=False): sigs = [] if iotype in (IOType.In, IOType.InTriOut): sigs.append(("i", 1)) @@ -193,13 +209,25 @@ class IOConn(Record): sigs.append(("o", 1)) if iotype in (IOType.TriOut, IOType.InTriOut): sigs.append(("oe", 1)) + if banksel > 0: + sigs.append(("sel", banksel)) + if pullup: + sigs.append(("pu", 1)) + if pulldown: + sigs.append(("pd", 1)) return Layout((("core", sigs), ("pad", sigs))) - def __init__(self, *, iotype, name=None, src_loc_at=0): - super().__init__(self.__class__.layout(iotype), name=name, src_loc_at=src_loc_at+1) + def __init__(self, *, iotype, name=None, banksel=0, + pullup=False, pulldown=False, + src_loc_at=0): + layout = self.__class__.layout(iotype, banksel, pullup, pulldown) + super().__init__(layout, name=name, src_loc_at=src_loc_at+1) self._iotype = iotype + self._banksel = banksel + self._pullup = pullup + self._pulldown = pulldown class _IDBypassBlock(Elaboratable): @@ -208,7 +236,8 @@ class _IDBypassBlock(Elaboratable): tdi, capture, shift, update, bypass, name): self.name = name - if not isinstance(manufacturer_id, Const) and len(manufacturer_id) != 11: + if (not isinstance(manufacturer_id, Const) and + len(manufacturer_id) != 11): raise ValueError("manufacturer_id has to be Const of length 11") if not isinstance(part_number, Const) and len(manufacturer_id) != 16: raise ValueError("part_number has to be Const of length 16") @@ -302,13 +331,13 @@ class ShiftReg(Record): class TAP(Elaboratable): #TODO: Document TAP - def __init__( - self, *, with_reset=False, ir_width=None, - manufacturer_id=Const(0b10001111111, 11), part_number=Const(1, 16), - version=Const(0, 4), - name=None, src_loc_at=0 - ): - assert((ir_width is None) or (isinstance(ir_width, int) and ir_width >= 2)) + def __init__(self, *, with_reset=False, ir_width=None, + manufacturer_id=Const(0b10001111111, 11), + part_number=Const(1, 16), + version=Const(0, 4), + name=None, src_loc_at=0): + assert((ir_width is None) or (isinstance(ir_width, int) and + ir_width >= 2)) assert(len(version) == 4) if name is None: @@ -324,11 +353,12 @@ class TAP(Elaboratable): self._part_number = part_number self._version = version - self._ircodes = [0, 1, 2] # Already taken codes, all ones added at the end + self._ircodes = [0, 1, 2] # Already taken codes, all ones added at end self._ios = [] self._srs = [] self._wbs = [] + self._dmis = [] def elaborate(self, platform): m = Module() @@ -337,7 +367,8 @@ class TAP(Elaboratable): ir_max = max(self._ircodes) + 1 # One extra code needed with all ones ir_width = len("{:b}".format(ir_max)) if self._ir_width is not None: - assert self._ir_width >= ir_width, "Specified JTAG IR width not big enough for allocated shiift registers" + assert self._ir_width >= ir_width, "Specified JTAG IR width " \ + "not big enough for allocated shiift registers" ir_width = self._ir_width # TODO: Make commands numbers configurable @@ -348,13 +379,13 @@ class TAP(Elaboratable): cmd_preload = 2 cmd_bypass = 2**ir_width - 1 # All ones - m.submodules._fsm = fsm = _FSM(bus=self.bus) + m.submodules.fsm = fsm = _FSM(bus=self.bus) m.domains.posjtag = fsm.posjtag m.domains.negjtag = fsm.negjtag # IR block select_ir = fsm.isir - m.submodules._irblock = irblock = _IRBlock( + m.submodules.irblock = irblock = _IRBlock( ir_width=ir_width, cmd_idcode=cmd_idcode, tdi=self.bus.tdi, capture=(fsm.isir & fsm.capture), shift=(fsm.isir & fsm.shift), @@ -364,14 +395,19 @@ class TAP(Elaboratable): ir = irblock.ir # ID block - select_id = fsm.isdr & ((ir == cmd_idcode) | (ir == cmd_bypass)) - m.submodules._idblock = idblock = _IDBypassBlock( - manufacturer_id=self._manufacturer_id, part_number=self._part_number, + select_id = Signal() + id_bypass = Signal() + m.d.comb += select_id.eq(fsm.isdr & + ((ir == cmd_idcode) | (ir == cmd_bypass))) + m.d.comb += id_bypass.eq(ir == cmd_bypass) + m.submodules.idblock = idblock = _IDBypassBlock( + manufacturer_id=self._manufacturer_id, + part_number=self._part_number, version=self._version, tdi=self.bus.tdi, capture=(select_id & fsm.capture), shift=(select_id & fsm.shift), update=(select_id & fsm.update), - bypass=(ir == cmd_bypass), + bypass=id_bypass, name=self.name+"_id", ) @@ -385,7 +421,8 @@ class TAP(Elaboratable): preload = (ir == cmd_preload) select_io = fsm.isdr & (sample | preload) m.d.comb += [ - io_capture.eq(sample & fsm.capture), # Don't capture if not sample (like for PRELOAD) + io_capture.eq(sample & fsm.capture), # Don't capture if not sample + # (like for PRELOAD) io_shift.eq(select_io & fsm.shift), io_update.eq(select_io & fsm.update), io_bd2io.eq(ir == cmd_extest), @@ -415,10 +452,104 @@ class TAP(Elaboratable): # wishbone self._elaborate_wishbones(m) + # DMI (Debug Memory Interface) + self._elaborate_dmis(m) + return m + def add_dmi(self, *, ircodes, address_width=8, data_width=64, + domain="sync", name=None): + """Add a DMI interface + + * writing to DMIADDR will automatically trigger a DMI READ. + the DMI address does not alter (so writes can be done at that addr) + * reading from DMIREAD triggers a DMI READ at the current DMI addr + the address is automatically incremented by 1 after. + * writing to DMIWRITE triggers a DMI WRITE at the current DMI addr + the address is automatically incremented by 1 after. + + Parameters: + ----------- + ircodes: sequence of three integer for the JTAG IR codes; + they represent resp. DMIADDR, DMIREAD and DMIWRITE. + First code has a shift register of length 'address_width', + the two other codes share a shift register of length + data_width. + + address_width: width of the address + data_width: width of the data + + Returns: + dmi: soc.debug.dmi.DMIInterface + The DMI interface + """ + if len(ircodes) != 3: + raise ValueError("3 IR Codes have to be provided") + + if name is None: + name = "dmi" + str(len(self._dmis)) + + # add 2 shift registers: one for addr, one for data. + sr_addr = self.add_shiftreg(ircode=ircodes[0], length=address_width, + domain=domain, name=name+"_addrsr") + sr_data = self.add_shiftreg(ircode=ircodes[1:], length=data_width, + domain=domain, name=name+"_datasr") + + dmi = DMIInterface(name=name) + self._dmis.append((sr_addr, sr_data, dmi, domain)) + + return dmi + + def _elaborate_dmis(self, m): + for sr_addr, sr_data, dmi, domain in self._dmis: + cd = m.d[domain] + m.d.comb += sr_addr.i.eq(dmi.addr_i) + + with m.FSM(domain=domain) as ds: - def add_io(self, *, iotype, name=None, src_loc_at=0): + # detect mode based on whether jtag addr or data read/written + with m.State("IDLE"): + with m.If(sr_addr.oe): # DMIADDR code + cd += dmi.addr_i.eq(sr_addr.o) + m.next = "READ" + with m.Elif(sr_data.oe[0]): # DMIREAD code + # If data is + cd += dmi.addr_i.eq(dmi.addr_i + 1) + m.next = "READ" + with m.Elif(sr_data.oe[1]): # DMIWRITE code + cd += dmi.din.eq(sr_data.o) + m.next = "WRRD" + + # req_i raises for 1 clock + with m.State("READ"): + m.next = "READACK" + + # wait for read ack + with m.State("READACK"): + with m.If(dmi.ack_o): + # Store read data in sr_data.i hold till next read + cd += sr_data.i.eq(dmi.dout) + m.next = "IDLE" + + # req_i raises for 1 clock + with m.State("WRRD"): + m.next = "WRRDACK" + + # wait for write ack + with m.State("WRRDACK"): + with m.If(dmi.ack_o): + cd += dmi.addr_i.eq(dmi.addr_i + 1) + m.next = "READ" # for readwrite + + # set DMI req and write-enable based on ongoing FSM states + m.d.comb += [ + dmi.req_i.eq(ds.ongoing("READ") | ds.ongoing("WRRD")), + dmi.we_i.eq(ds.ongoing("WRRD")), + ] + + def add_io(self, *, iotype, name=None, banksel=0, + pullup=False, pulldown=False, + src_loc_at=0): """Add a io cell to the boundary scan chain Parameters: @@ -430,18 +561,30 @@ class TAP(Elaboratable): if name is None: name = "ioconn" + str(len(self._ios)) - ioconn = IOConn(iotype=iotype, name=name, src_loc_at=src_loc_at+1) + ioconn = IOConn(iotype=iotype, banksel=banksel, + pullup=pullup, pulldown=pulldown, + name=name, src_loc_at=src_loc_at+1) self._ios.append(ioconn) return ioconn def _elaborate_ios(self, *, m, capture, shift, update, bd2io, bd2core): - connlength = { - IOType.In: 1, - IOType.Out: 1, - IOType.TriOut: 2, - IOType.InTriOut: 3, - } - length = sum(connlength[conn._iotype] for conn in self._ios) + # note: the starting points where each IOConn is placed into + # the Shift Register depends *specifically* on the type (parameters) + # of each IOConn, and therefore on all IOConn(s) that came before it + # [prior calls to add_io]. this function consistently follows + # the exact same pattern in the exact same sequence every time, + # to compute consistent offsets. developers must do the same: + # note that each length depends on *all* parameters: + # IOtype, banksel, pullup *and* pulldown. + + # pre-compute the length of the IO shift registers needed. + length = 0 + for conn in self._ios: + length += IOConn.lengths[conn._iotype] + conn._banksel + if conn._pullup: + length += 1 + if conn._pulldown: + length += 1 if length == 0: return self.bus.tdi @@ -450,30 +593,31 @@ class TAP(Elaboratable): # Boundary scan "capture" mode. makes I/O status available via SR with m.If(capture): + iol = [] idx = 0 for conn in self._ios: - if conn._iotype == IOType.In: - m.d.posjtag += io_sr[idx].eq(conn.pad.i) + # in appropriate sequence: In/TriOut has pad.i, + # Out.InTriOut has everything, Out and TriOut have core.o + if conn._iotype in [IOType.In, IOType.InTriOut]: + iol.append(conn.pad.i) + if conn._iotype in [IOType.Out, IOType.InTriOut]: + iol.append(conn.core.o) + if conn._iotype in [IOType.TriOut, IOType.InTriOut]: + iol.append(conn.core.oe) + # now also banksel, pullup and pulldown from core are added + if conn._banksel != 0: + iol.append(conn.core.sel) + idx += conn._banksel + if conn._pullup: + iol.append(conn.core.pu) idx += 1 - elif conn._iotype == IOType.Out: - m.d.posjtag += io_sr[idx].eq(conn.core.o) + if conn._pulldown: + iol.append(conn.core.pd) idx += 1 - elif conn._iotype == IOType.TriOut: - m.d.posjtag += [ - io_sr[idx].eq(conn.core.o), - io_sr[idx+1].eq(conn.core.oe), - ] - idx += 2 - elif conn._iotype == IOType.InTriOut: - m.d.posjtag += [ - io_sr[idx].eq(conn.pad.i), - io_sr[idx+1].eq(conn.core.o), - io_sr[idx+2].eq(conn.core.oe), - ] - idx += 3 - else: - raise("Internal error") - assert idx == length, "Internal error" + # help with length double-check + idx += IOConn.lengths[conn._iotype] # fails if wrong type + assert idx == length, "Internal error, length mismatch" + m.d.posjtag += io_sr.eq(Cat(*iol)) # assigns all io_sr in one hit # "Shift" mode (sends out captured data on tdo, sets incoming from tdi) with m.Elif(shift): @@ -485,8 +629,13 @@ class TAP(Elaboratable): # sets up IO (pad<->core) or in testing mode depending on requested # mode, via Muxes controlled by bd2core and bd2io + # for each IOConn, the number of bits needed from io_bd will vary + # and is computed on-the-fly, here. it is up to the developer to + # keep track of where each IO pad configuration starts and ends + # in the Shift Register (TODO: provide a dictionary of starting points) idx = 0 for conn in self._ios: + # mux the I/O/OE depending on IOType if conn._iotype == IOType.In: m.d.comb += conn.core.i.eq(Mux(bd2core, io_bd[idx], conn.pad.i)) idx += 1 @@ -508,16 +657,31 @@ class TAP(Elaboratable): idx += 3 else: raise("Internal error") + # optional mux of banksel, pullup and pulldown. note that idx + # advances each time, so that io_bd[idx] comes from the right point + comb = m.d.comb + if conn._banksel != 0: + s, e = (idx, idx+conn._banksel) # banksel can be multi-bit + comb += conn.pad.sel.eq(Mux(bd2io, io_bd[s:e], conn.core.sel)) + idx = e + if conn._pullup: + comb += conn.pad.pu.eq(Mux(bd2io, io_bd[idx], conn.core.pu)) + idx += 1 + if conn._pulldown: + comb += conn.pad.pd.eq(Mux(bd2io, io_bd[idx], conn.core.pd)) + idx += 1 assert idx == length, "Internal error" + # return the last bit of the shift register, for output on tdo return io_sr[-1] - def add_shiftreg(self, *, ircode, length, domain="sync", name=None, src_loc_at=0): + def add_shiftreg(self, *, ircode, length, domain="sync", name=None, + src_loc_at=0): """Add a shift register to the JTAG interface Parameters: - - ircode: code(s) for the IR; int or sequence of ints. In the latter case this - shiftreg is shared between different IR codes. + - ircode: code(s) for the IR; int or sequence of ints. In the latter + case this shiftreg is shared between different IR codes. - length: the length of the shift register - domain: the domain on which the signal will be used""" @@ -528,7 +692,8 @@ class TAP(Elaboratable): ir_it = ircodes = (ircode,) for _ircode in ir_it: if not isinstance(_ircode, int) or _ircode <= 0: - raise ValueError("IR code '{}' is not an int greater than 0".format(_ircode)) + raise ValueError("IR code '{}' is not an int " + "greater than 0".format(_ircode)) if _ircode in self._ircodes: raise ValueError("IR code '{}' already taken".format(_ircode)) @@ -536,7 +701,8 @@ class TAP(Elaboratable): if name is None: name = "sr{}".format(len(self._srs)) - sr = ShiftReg(sr_length=length, cmds=len(ircodes), name=name, src_loc_at=src_loc_at+1) + sr = ShiftReg(sr_length=length, cmds=len(ircodes), name=name, + src_loc_at=src_loc_at+1) self._srs.append((ircodes, domain, sr)) return sr @@ -559,13 +725,18 @@ class TAP(Elaboratable): sr_update.eq((isir != 0) & update), ] - # update signal is on the JTAG clockdomain, sr.oe is on `domain` clockdomain - # latch update in `domain` clockdomain and see when it has falling edge. + # update signal is on the JTAG clockdomain, sr.oe is on `domain` + # clockdomain latch update in `domain` clockdomain and see when + # it has falling edge. # At that edge put isir in sr.oe for one `domain` clockdomain + # Using this custom sync <> JTAG domain synchronization avoids + # the use of more generic but also higher latency CDC solutions + # like FFSynchronizer. update_core = Signal(name=sr.name+"_update_core") update_core_prev = Signal(name=sr.name+"_update_core_prev") m.d[domain] += [ - update_core.eq(sr_update), # This is CDC from JTAG domain to given domain + update_core.eq(sr_update), # This is CDC from JTAG domain + # to given domain update_core_prev.eq(update_core) ] with m.If(update_core_prev & ~update_core): @@ -582,7 +753,6 @@ class TAP(Elaboratable): # tdo = reg[0], tdo_en = shift tdos.append((reg[0], sr_shift)) - # Assign the right tdo to the bus tdo for i, (tdo, tdo_en) in enumerate(tdos): if i == 0: @@ -596,16 +766,17 @@ class TAP(Elaboratable): with m.Else(): m.d.comb += self.bus.tdo.eq(tdo_jtag) else: - # Always connect tdo_jtag to + # Always connect tdo_jtag to m.d.comb += self.bus.tdo.eq(tdo_jtag) - - def add_wishbone(self, *, ircodes, address_width, data_width, granularity=None, domain="sync", + def add_wishbone(self, *, ircodes, address_width, data_width, + granularity=None, domain="sync", features=None, name=None, src_loc_at=0): """Add a wishbone interface - In order to allow high JTAG clock speed, data will be cached. This means that if data is - output the value of the next address will be read automatically. + In order to allow high JTAG clock speed, data will be cached. + This means that if data is output the value of the next address + will be read automatically. Parameters: ----------- @@ -615,6 +786,7 @@ class TAP(Elaboratable): share a shift register of length data_width. address_width: width of the address data_width: width of the data + features: features required. defaults to stall, lock, err, rty Returns: wb: nmigen_soc.wishbone.bus.Interface @@ -623,6 +795,8 @@ class TAP(Elaboratable): if len(ircodes) != 3: raise ValueError("3 IR Codes have to be provided") + if features is None: + features={"stall", "lock", "err", "rty"} if name is None: name = "wb" + str(len(self._wbs)) sr_addr = self.add_shiftreg( @@ -635,7 +809,7 @@ class TAP(Elaboratable): ) wb = WishboneInterface(data_width=data_width, addr_width=address_width, - granularity=granularity, features={"stall", "lock", "err", "rty"}, + granularity=granularity, features=features, name=name, src_loc_at=src_loc_at+1) self._wbs.append((sr_addr, sr_data, wb, domain)) @@ -663,23 +837,42 @@ class TAP(Elaboratable): m.d[domain] += wb.dat_w.eq(sr_data.o) m.next = "WRITEREAD" with m.State("READ"): - with m.If(~wb.stall): + if not hasattr(wb, "stall"): m.next = "READACK" + else: + with m.If(~wb.stall): + m.next = "READACK" with m.State("READACK"): with m.If(wb.ack): - # Store read data in sr_data.i and keep it there til next read + # Store read data in sr_data.i + # and keep it there til next read. + # This is enough to synchronize between sync and JTAG + # clock domain and no higher latency solutions like + # FFSynchronizer is needed. m.d[domain] += sr_data.i.eq(wb.dat_r) m.next = "IDLE" with m.State("WRITEREAD"): - with m.If(~wb.stall): + if not hasattr(wb, "stall"): m.next = "WRITEREADACK" + else: + with m.If(~wb.stall): + m.next = "WRITEREADACK" with m.State("WRITEREADACK"): with m.If(wb.ack): m.d[domain] += wb.adr.eq(wb.adr + 1) m.next = "READ" - m.d.comb += [ - wb.cyc.eq(~fsm.ongoing("IDLE")), - wb.stb.eq(fsm.ongoing("READ") | fsm.ongoing("WRITEREAD")), - wb.we.eq(fsm.ongoing("WRITEREAD")), - ] + if hasattr(wb, "stall"): + m.d.comb += wb.stb.eq(fsm.ongoing("READ") | + fsm.ongoing("WRITEREAD")) + m.d.comb += wb.we.eq(fsm.ongoing("WRITEREAD")) + else: + # non-stall is single-cycle (litex), must assert stb + # until ack is sent + m.d.comb += wb.stb.eq(fsm.ongoing("READ") | + fsm.ongoing("WRITEREAD") | + fsm.ongoing("READACK") | + fsm.ongoing("WRITEREADACK")) + m.d.comb += wb.we.eq(fsm.ongoing("WRITEREAD") | + fsm.ongoing("WRITEREADACK")) + m.d.comb += wb.cyc.eq(~fsm.ongoing("IDLE"))