#!/usr/bin/env python # coding: utf-8 # # System on Chip design auxiliaries # # For mass generation of bus decoders, a register bit map must be associated with corresponding control, status or data signals. # This SoC concept follows the [*MaSoCist*](https://github.com/hackfin/MaSoCisthttps://github.com/hackfin/MaSoCist) register map design rules: # # * Registers are mapped into memory space and are accessed by an address, hence. # * They can be flagged read-only, write-only or volatile: # * READONLY: Writing to the register has no effect # * WRITEONLY: Reading from this register returns an undefined value # * VOLATILE: Write or read access triggers a pulse on the corresponding `select` lines. # This allows to implement `W1C` (write one to clear) behaviour, or optimized data in/out transfers. # * Registers contain bit fields that can be READONLY or WRITEONLY # * Two register definitions (one READONLY, one WRITEONLY) can be mapped to one address. This is used for data I/O. # # To skip the entire elaboration on *myIRL* auxiliaries, jump to the actual [register map decoder implementation](#Register-map-decoder) # ## Class enhancements # # The `NamedBitfield` and `Register` class is extended with flags: # In[1]: import sys sys.path.insert(0, "../../") # In[2]: from myirl.library.registers import Register, NamedBitfield from myirl.kernel.components import ContainerExtension, ContainerBase, Signal, ChildAlias from myirl.library.bulksignals import BulkWrapperSig from myhdl import intbv from myirl.kernel.sensitivity import hdlmacro from myirl import targets class BF(NamedBitfield): READONLY = 0x01 WRITEONLY = 0x02 def __init__(self, name, msb, lsb, flags = 0, default = None): super().__init__(name, msb, lsb) self.init = default # Initialization value when reset self.flags = flags # Access flags class Reg(Register): VOLATILE = 0x04 READONLY = 0x01 WRITEONLY = 0x02 def __init__(self, size, bitfields, flags = 0): super().__init__(size, bitfields) self.flags = flags def has_select(self, flag): if (self.flags & Reg.VOLATILE) and not (self.flags & flag): return True return False # We then create a `RegisterSignal` derived from the `ContainerExtension` that takes a register as argument. To allow aliasing, we also need to be able to pass a signal dictionary, optionally. # # This class also adds a few special assignment methods for its signal members, according to their WRITEONLY/READONLY configuration. # In[3]: class RegisterSignal(ContainerExtension): def __init__(self, name, reg, signals = None, virtual = None): if name is None: name = kernel.utils.get_uuid('reg_') # Hack to allow cloning/aliasing if signals is not None: super().__init__(name, signals, template = reg, virtual = True, twoway = True) return if not isinstance(reg, Register): raise TypeError("expects register as argument") bitfields = reg.members().items() signals = { n : Signal(intbv(ival := i.init if i.init is not None else 0)[i.msb + 1-i.lsb:] if i.msb != i.lsb else bool(ival), name = i.name) for n, i in bitfields } # Selection signals ('W1C' etc.) select_sigs = {} if reg.flags & reg.VOLATILE: if not reg.flags & reg.READONLY: select_sigs["sel_w"] = Signal(bool(0)) if not reg.flags & reg.WRITEONLY: select_sigs["sel_r"] = Signal(bool(0)) inputs = filter(lambda t: (t[1].flags & BF.WRITEONLY) == False, bitfields) outputs = filter(lambda t: (t[1].flags & BF.READONLY) == False, bitfields) input_container = self.create(name, 'read', { n : signals[n] for n, _ in inputs }, ()) output_container = self.create(name, 'write', { n : signals[n] for n, _ in outputs }, ()) select_container = self.create(name, 'sel', select_sigs, ()) sigs = {} if input_container is not None: sigs['read'] = input_container if output_container is not None: output_container.driven = True output_container._aux = False sigs['write'] = output_container if select_container is not None: select_container.driven = True sigs['select'] = select_container for n, s in sigs.items(): s.rename(name + '_' + n) super().__init__(name, sigs, template = reg, virtual = True, twoway = True) def create(self, name, mname, children, bases = tuple()): "Dynamically create a subclass from self._type" if len(children) == 0: return None d = { '_templ' : children, '_virtual' : True, '_rank' : ContainerBase._rank } sign = name + '_' + mname container = type(sign, (BulkWrapperSig, *bases), d) targets.vhdl.register_type(container, sign) inst = container() inst._populate(children) return inst def alias(self, name): "We need a different .alias() method, as we pass a Register for a new object" signals = {} for n, s in self._members.items(): nn = n sig = ChildAlias(self, s, n, is_member = False) signals[nn] = sig new = type(self)(name, self._template, signals) new.rename(name) # Need explicit rename here XXX return new # Here we don't need a @hdlmacro, because Register.assign() already returns # a generator def assign(self, data): d = {} try: readport_members = self.get_children()['read'].members().items() for n, i in readport_members: bf = self._template.bfmap[n] d[n] = i gen = self._template.assign(data, **d) return gen except KeyError: return None @hdlmacro def select_reset(self): try: members = self.get_children()['select'].members().items() gen = [] for n, i in members: gen.append(i.set(False)) yield gen except KeyError: yield [] @hdlmacro def set(self, other): gen = [] try: w = self.get_children()['write'] m = w.members() for n, i in m.items(): bf = self._template.bfmap[n] if bf.msb == bf.lsb: gen.append(i.set(other[bf.msb])) else: gen.append(i.set(other[bf.msb + 1: bf.lsb])) yield gen except KeyError: yield [] # ## Register definitions # # Add a few register with bit fields and flags: # In[4]: reg01 = Reg(16, [ BF("im", 3, 1, flags = BF.READONLY), BF("ex", 7, 6), BF("inv", 4, 4, flags = BF.WRITEONLY), BF("mode", 14, 10, default = 2) ] ) reg02 = Reg(16, [ BF("gna", 6, 1, default = 8), BF("reset", 7, 7, default = True) ], flags = Reg.VOLATILE | Reg.WRITEONLY ) # This is a description for an address map regdesc = { 0x01: ['stat', reg01], 0x02: ['ctrl', reg02], 0x04: ['TXD', Reg(16, [ BF("DATA", 15, 0)], flags = Reg.WRITEONLY | Reg.VOLATILE) ], 0x05: ['RXD', Reg(16, [ BF("DATA", 15, 0)], flags = Reg.READONLY | Reg.VOLATILE)] } # ## Register decoder table generator # # To turn this into a register decoder circuit, we create a factory function, returning a `worker` process: # In[5]: from myirl.kernel import sensitivity from myirl.kernel.sig import ConstSig from myirl import simulation def gen_regdec(regmap, port, clk, reset, wr, addr, idata, odata, REPORT_ILLEGAL_ACCESS = False, RESET_DEFAULTS = False): kwargs = { 'EDGE' : clk.POS } if RESET_DEFAULTS: kwargs['RESET'] = reset @sensitivity.process(clk, **kwargs) def worker(logic): """Creates a case/when flow the procedural way""" cw, cr = [ logic.Case(addr) for _ in range(2) ] N = addr.size() _reset = [] for k, rdesc in regmap.items(): name, rd = rdesc[0], rdesc[1] if rd.has_select(Reg.READONLY): maybe_write_sel = port[name].select.sel_w.set(True) else: maybe_write_sel = None if rd.has_select(Reg.WRITEONLY): maybe_read_sel = port[name].select.sel_r.set(True) else: maybe_read_sel = None reg = port[name] _reset += [ reg.select_reset(), ] s = ConstSig(k, N) if not rd.flags & Reg.READONLY: cw = cw.When(s)(reg.set(idata), maybe_write_sel) if not rd.flags & Reg.WRITEONLY: cr = cr.When(s)(reg.assign(odata), maybe_read_sel) if REPORT_ILLEGAL_ACCESS: cw = cw.Other(sim.raise_(ValueError("Illegal WRITE address"))) cr = cr.Other(sim.raise_(ValueError("Illegal READ address"))) else: cw = cw.Other(None) cr = cr.Other(None) _if = logic.If(wr == True).Then(cw).Else(cr) logic += _reset logic += [_if ] return worker # The register decoder for this specific memory mapped register has a dynamic `registerbank` dictionary passed to the interface, containing the register in/out wires. This variable argument construct is inferred to a HDL description. # ## Register map decoder # # The actual register map decoder consists of the code below. # In[6]: from myirl.emulation.myhdl import * from myirl.library.portion import * SigType = Signal.Type Bool = SigType(bool) Addr = SigType(intbv, 12) Data = SigType(intbv, 16) @block def mmr_decode( clk : ClkSignal, reset : ResetSignal, addr : Addr, wr : Bool, data_in : Data, data_out : Data.Output, REGDESC, **registerbank ): # We use a partially assigneable signal: idata = PASignal(intbv()[len(data_out):]) # Then generate the decoder from the register map description passed: wk = gen_regdec(REGDESC, registerbank, clk, reset, wr, addr, data_in, idata, RESET_DEFAULTS = True) @always(clk.posedge) def drive(): data_out.next = idata return instances() # ## Test bench # # We define an interface generation function that creates a signal dictionary out of the register description: # In[7]: # Interface generation: def gen_interface(rd): d = {} for k, rdesc in rd.items(): n, reg = rdesc[0], rdesc[1] sig = RegisterSignal(n, reg) sig.rename(n) d[n] = sig return d # We might pack all MMR signals into a port structure including auxiliary methods. We need to decorate them with `@hdlmacro` in order to return a generator element usable within the myHDL `@instance`. # In[8]: @container() class MMRPort: _inputs = ['din', 'wr', 'addr'] _outputs = ['dout'] _other = ['clk', 'rst'] def __init__(self): self.clk = ClkSignal() self.wr = Signal(bool()) self.addr = Addr() self.rst = ResetSignal(0, 1) self.din, self.dout = [ Data() for _ in range(2) ] @hdlmacro def reset_sequence(self): p = self yield [ p.rst.set(True), simulation.wait(2 * (p.clk.posedge, )), p.rst.set(False) ] @hdlmacro def write_sequence(self, a, d): p = self yield [ p.addr.set(a), p.din.set(d), simulation.wait(p.clk.posedge), p.wr.set(True), simulation.wait(p.clk.posedge), p.wr.set(False), ] @hdlmacro def assert_read(self, addr, data): yield [ self.addr.set(addr), self.wr.set(False), simulation.wait(2 * (self.clk.posedge,)), simulation.assert_(self.dout == data, "Read mismatch") ] # ### The test bench # # Finally, we run a reset/write on the decoder: # In[9]: @block def testbench(regdesc): p = MMRPort() clk = ClkSignal('clk') mon_gna = Signal(intbv()[6:]) mon_select = Signal(bool()) debug = Signal(bool()) interface = gen_interface(regdesc) wires = [ mon_gna.wireup(interface['ctrl'].read.gna), mon_select.wireup(interface['ctrl'].select.sel_w), p.clk.wireup(clk) ] inst = mmr_decode(clk, p.rst, p.addr, p.wr, p.din, p.dout, regdesc, **interface ) @always(delay(2)) def clkgen(): clk.next = ~clk ctrl = interface['ctrl'] stat = interface['stat'] @instance def stimulus(): print("START") debug.next = False p.wr.next = False p.addr.next = 0x001 p.reset_sequence() stat.read.ex.next = 0 stat.read.mode.next = 4 stat.read.im.next = 2 p.assert_read(0x001, 0x1004) p.write_sequence(0x002, 0xfa) debug.next = True yield clk.posedge assert ctrl.select.sel_w == True assert ctrl.write.gna == 0x3d yield clk.negedge assert ctrl.select.sel_w == False p.write_sequence(0x001, 0x10) assert stat.write.inv == True yield 2 * (clk.posedge, ) print("DONE") raise StopSimulation return instances() def test(): tb = testbench(regdesc) f = tb.elab(targets.VHDL, elab_all = True) # Turn 'debug' on for simulation output run_ghdl(f, tb, debug = True, vcdfile = 'testbench.vcd') return f f = test() # In[10]: get_ipython().system(' cat -n {f[0]}') # In[11]: get_ipython().system(' cat {f[1]}') # In[12]: # ! cat {mmr_decode.ctx.path_prefix}module_defs.vhdl # ## Waveform display # # The `*.vcd` format hides the `MMRPort` record members from the trace. Therefore we need a few monitoring auxiliary signals. # In[13]: import wavedraw import nbwavedrom # In[14]: TB = "testbench" waveform = wavedraw.vcd2wave(TB+ ".vcd", TB + '.clk', None) nbwavedrom.draw(waveform) # In[ ]: