add alternative pc_reset argument to issuer_verilog.py
[soc.git] / src / soc / simple / core.py
1 """simple core
2
3 not in any way intended for production use. connects up FunctionUnits to
4 Register Files in a brain-dead fashion that only permits one and only one
5 Function Unit to be operational.
6
7 the principle here is to take the Function Units, analyse their regspecs,
8 and turn their requirements for access to register file read/write ports
9 into groupings by Register File and Register File Port name.
10
11 under each grouping - by regfile/port - a list of Function Units that
12 need to connect to that port is created. as these are a contended
13 resource a "Broadcast Bus" per read/write port is then also created,
14 with access to it managed by a PriorityPicker.
15
16 the brain-dead part of this module is that even though there is no
17 conflict of access, regfile read/write hazards are *not* analysed,
18 and consequently it is safer to wait for the Function Unit to complete
19 before allowing a new instruction to proceed.
20 (update: actually this is being added now:
21 https://bugs.libre-soc.org/show_bug.cgi?id=737)
22 """
23
24 from nmigen import (Elaboratable, Module, Signal, ResetSignal, Cat, Mux,
25 Const)
26 from nmigen.cli import rtlil
27
28 from openpower.decoder.power_decoder2 import PowerDecodeSubset
29 from openpower.decoder.power_regspec_map import regspec_decode
30 from openpower.sv.svp64 import SVP64Rec
31
32 from nmutil.picker import PriorityPicker
33 from nmutil.util import treereduce
34 from nmutil.singlepipe import ControlBase
35
36 from soc.fu.compunits.compunits import AllFunctionUnits, LDSTFunctionUnit
37 from soc.regfile.regfiles import RegFiles
38 from openpower.decoder.power_decoder2 import get_rdflags
39 from soc.experiment.l0_cache import TstL0CacheBuffer # test only
40 from soc.config.test.test_loadstore import TestMemPspec
41 from openpower.decoder.power_enums import MicrOp, Function
42 from soc.simple.core_data import CoreInput, CoreOutput
43
44 from collections import defaultdict, namedtuple
45 import operator
46
47 from nmutil.util import rising_edge
48
49 FUSpec = namedtuple("FUSpec", ["funame", "fu", "idx"])
50 ByRegSpec = namedtuple("ByRegSpec", ["okflag", "regport", "wid", "specs"])
51
52 # helper function for reducing a list of signals down to a parallel
53 # ORed single signal.
54 def ortreereduce(tree, attr="o_data"):
55 return treereduce(tree, operator.or_, lambda x: getattr(x, attr))
56
57
58 def ortreereduce_sig(tree):
59 return treereduce(tree, operator.or_, lambda x: x)
60
61
62 # helper function to place full regs declarations first
63 def sort_fuspecs(fuspecs):
64 res = []
65 for (regname, fspec) in fuspecs.items():
66 if regname.startswith("full"):
67 res.append((regname, fspec))
68 for (regname, fspec) in fuspecs.items():
69 if not regname.startswith("full"):
70 res.append((regname, fspec))
71 return res # enumerate(res)
72
73
74 # a hazard bitvector "remap" function which returns an AST expression
75 # that remaps read/write hazard regfile port numbers to either a full
76 # bitvector or a reduced subset one. SPR for example is reduced to a
77 # single bit.
78 # CRITICALLY-IMPORTANT NOTE: these bitvectors *have* to match up per
79 # regfile! therefore the remapping is per regfile, *NOT* per regfile
80 # port and certainly not based on whether it is a read port or write port.
81 # note that any reductions here will result in degraded performance due
82 # to conflicts, but at least it keeps the hazard matrix sizes down to "sane"
83 def bitvector_remap(regfile, rfile, port):
84 # 8-bits (at the moment, no SVP64), CR is unary: no remap
85 if regfile == 'CR':
86 return port
87 # 3 bits, unary alrady: return the port
88 if regfile == 'XER':
89 return port
90 # 3 bits, unary: return the port
91 if regfile == 'XER':
92 return port
93 # 5 bits, unary: return the port
94 if regfile == 'STATE':
95 return port
96 # 9 bits (9 entries), might be unary already
97 if regfile == 'FAST':
98 if rfile.unary: # FAST might be unary already
99 return port
100 else:
101 return 1 << port
102 # 10 bits (!!) - reduce to one
103 if regfile == 'SPR':
104 if rfile.unary: # FAST might be unary already
105 return port
106 else:
107 return 1 << port
108 if regfile == 'INT':
109 if rfile.unary: # INT, check if unary/binary
110 return port
111 else:
112 return 1 << port
113
114
115 # derive from ControlBase rather than have a separate Stage instance,
116 # this is simpler to do
117 class NonProductionCore(ControlBase):
118 def __init__(self, pspec):
119 self.pspec = pspec
120
121 # test is SVP64 is to be enabled
122 self.svp64_en = hasattr(pspec, "svp64") and (pspec.svp64 == True)
123
124 # test to see if regfile ports should be reduced
125 self.regreduce_en = (hasattr(pspec, "regreduce") and
126 (pspec.regreduce == True))
127
128 # test to see if overlapping of instructions is allowed
129 # (not normally enabled for TestIssuer FSM but useful for checking
130 # the bitvector hazard detection, before doing In-Order)
131 self.allow_overlap = (hasattr(pspec, "allow_overlap") and
132 (pspec.allow_overlap == True))
133
134 # test core type
135 self.make_hazard_vecs = self.allow_overlap
136 self.core_type = "fsm"
137 if hasattr(pspec, "core_type"):
138 self.core_type = pspec.core_type
139
140 super().__init__(stage=self)
141
142 # single LD/ST funnel for memory access
143 self.l0 = l0 = TstL0CacheBuffer(pspec, n_units=1)
144 pi = l0.l0.dports[0]
145
146 # function units (only one each)
147 # only include mmu if enabled in pspec
148 self.fus = AllFunctionUnits(pspec, pilist=[pi])
149
150 # link LoadStore1 into MMU
151 mmu = self.fus.get_fu('mmu0')
152 ldst0 = self.fus.get_fu('ldst0')
153 print ("core pspec", pspec.ldst_ifacetype)
154 print ("core mmu", mmu)
155 if mmu is not None:
156 lsi = l0.cmpi.lsmem.lsi # a LoadStore1 Interface object
157 print ("core lsmem.lsi", lsi)
158 mmu.alu.set_ldst_interface(lsi)
159 # urr store I-Cache in core so it is easier to get at
160 self.icache = lsi.icache
161
162 # alternative reset values for STATE regs
163 self.msr_at_reset = 0x0
164 self.pc_at_reset = 0x0
165 if hasattr(pspec, "msr_reset") and isinstance(pspec.msr_reset, int):
166 self.msr_at_reset = pspec.msr_reset
167 if hasattr(pspec, "pc_reset") and isinstance(pspec.pc_reset, int):
168 self.pc_at_reset = pspec.pc_reset
169 state_resets = [self.pc_at_reset, # PC at reset
170 self.msr_at_reset, # MSR at reset
171 0x0, # SVSTATE at reset
172 0x0, # DEC at reset
173 0x0] # TB at reset
174
175 # register files (yes plural)
176 self.regs = RegFiles(pspec, make_hazard_vecs=self.make_hazard_vecs,
177 state_resets=state_resets)
178
179 # set up input and output: unusual requirement to set data directly
180 # (due to the way that the core is set up in a different domain,
181 # see TestIssuer.setup_peripherals
182 self.p.i_data, self.n.o_data = self.new_specs(None)
183 self.i, self.o = self.p.i_data, self.n.o_data
184
185 # actual internal input data used (captured)
186 self.ireg = self.ispec()
187
188 # create per-FU instruction decoders (subsetted). these "satellite"
189 # decoders reduce wire fan-out from the one (main) PowerDecoder2
190 # (used directly by the trap unit) to the *twelve* (or more)
191 # Function Units. we can either have 32 wires (the instruction)
192 # to each, or we can have well over a 200 wire fan-out (to 12
193 # ALUs). it's an easy choice to make.
194 self.decoders = {}
195 self.des = {}
196
197 # eep, these should be *per FU* i.e. for FunctionUnitBaseMulti
198 # they should be shared (put into the ALU *once*).
199
200 for funame, fu in self.fus.fus.items():
201 f_name = fu.fnunit.name
202 fnunit = fu.fnunit.value
203 opkls = fu.opsubsetkls
204 if f_name == 'TRAP':
205 # TRAP decoder is the *main* decoder
206 self.trapunit = funame
207 continue
208 assert funame not in self.decoders
209 self.decoders[funame] = PowerDecodeSubset(None, opkls, f_name,
210 final=True,
211 state=self.ireg.state,
212 svp64_en=self.svp64_en,
213 regreduce_en=self.regreduce_en)
214 self.des[funame] = self.decoders[funame].do
215 print ("create decoder subset", funame, opkls, self.des[funame])
216
217 # create per-Function Unit write-after-write hazard signals
218 # yes, really, this should have been added in ReservationStations
219 # but hey.
220 for funame, fu in self.fus.fus.items():
221 fu._waw_hazard = Signal(name="waw_%s" % funame)
222
223 # share the SPR decoder with the MMU if it exists
224 if "mmu0" in self.decoders:
225 self.decoders["mmu0"].mmu0_spr_dec = self.decoders["spr0"]
226
227 # allow pausing of the DEC/TB FSM back in Issuer, by spotting
228 # if there is an MTSPR instruction
229 self.pause_dec_tb = Signal()
230
231 # next 3 functions are Stage API Compliance
232 def setup(self, m, i):
233 pass
234
235 def ispec(self):
236 return CoreInput(self.pspec, self.svp64_en, self.regreduce_en)
237
238 def ospec(self):
239 return CoreOutput()
240
241 # elaborate function to create HDL
242 def elaborate(self, platform):
243 m = super().elaborate(platform)
244
245 # for testing purposes, to cut down on build time in coriolis2
246 if hasattr(self.pspec, "nocore") and self.pspec.nocore == True:
247 x = Signal() # dummy signal
248 m.d.sync += x.eq(~x)
249 return m
250 comb = m.d.comb
251
252 m.submodules.fus = self.fus
253 m.submodules.l0 = l0 = self.l0
254 self.regs.elaborate_into(m, platform)
255 regs = self.regs
256 fus = self.fus.fus
257
258 # amalgamate write-hazards into a single top-level Signal
259 self.waw_hazard = Signal()
260 whaz = []
261 for funame, fu in self.fus.fus.items():
262 whaz.append(fu._waw_hazard)
263 comb += self.waw_hazard.eq(Cat(*whaz).bool())
264
265 # connect decoders
266 self.connect_satellite_decoders(m)
267
268 # ssh, cheat: trap uses the main decoder because of the rewriting
269 self.des[self.trapunit] = self.ireg.e.do
270
271 # connect up Function Units, then read/write ports, and hazard conflict
272 self.issue_conflict = Signal()
273 fu_bitdict, fu_selected = self.connect_instruction(m)
274 raw_hazard = self.connect_rdports(m, fu_bitdict, fu_selected)
275 self.connect_wrports(m, fu_bitdict, fu_selected)
276 if self.allow_overlap:
277 comb += self.issue_conflict.eq(raw_hazard)
278
279 # note if an exception happened. in a pipelined or OoO design
280 # this needs to be accompanied by "shadowing" (or stalling)
281 el = []
282 for exc in self.fus.excs.values():
283 el.append(exc.happened)
284 if len(el) > 0: # at least one exception
285 comb += self.o.exc_happened.eq(Cat(*el).bool())
286
287 return m
288
289 def connect_satellite_decoders(self, m):
290 comb = m.d.comb
291 for k, v in self.decoders.items():
292 # connect each satellite decoder and give it the instruction.
293 # as subset decoders this massively reduces wire fanout given
294 # the large number of ALUs
295 m.submodules["dec_%s" % k] = v
296 comb += v.dec.raw_opcode_in.eq(self.ireg.raw_insn_i)
297 comb += v.dec.bigendian.eq(self.ireg.bigendian_i)
298 # sigh due to SVP64 RA_OR_ZERO detection connect these too
299 comb += v.sv_a_nz.eq(self.ireg.sv_a_nz)
300 if not self.svp64_en:
301 continue
302 comb += v.pred_sm.eq(self.ireg.sv_pred_sm)
303 comb += v.pred_dm.eq(self.ireg.sv_pred_dm)
304 if k == self.trapunit:
305 continue
306 comb += v.sv_rm.eq(self.ireg.sv_rm) # pass through SVP64 RM
307 comb += v.is_svp64_mode.eq(self.ireg.is_svp64_mode)
308 # only the LDST PowerDecodeSubset *actually* needs to
309 # know to use the alternative decoder. this is all
310 # a terrible hack
311 if not k.lower().startswith("ldst"):
312 continue
313 comb += v.use_svp64_ldst_dec.eq( self.ireg.use_svp64_ldst_dec)
314
315 def connect_instruction(self, m):
316 """connect_instruction
317
318 uses decoded (from PowerOp) function unit information from CSV files
319 to ascertain which Function Unit should deal with the current
320 instruction.
321
322 some (such as OP_ATTN, OP_NOP) are dealt with here, including
323 ignoring it and halting the processor. OP_NOP is a bit annoying
324 because the issuer expects busy flag still to be raised then lowered.
325 (this requires a fake counter to be set).
326 """
327 comb, sync = m.d.comb, m.d.sync
328 fus = self.fus.fus
329
330 # indicate if core is busy
331 busy_o = self.o.busy_o
332 any_busy_o = self.o.any_busy_o
333
334 # connect up temporary copy of incoming instruction. the FSM will
335 # either blat the incoming instruction (if valid) into self.ireg
336 # or if the instruction could not be delivered, keep dropping the
337 # latched copy into ireg
338 ilatch = self.ispec()
339 self.instr_active = Signal()
340
341 # enable/busy-signals for each FU, get one bit for each FU (by name)
342 fu_enable = Signal(len(fus), reset_less=True)
343 fu_busy = Signal(len(fus), reset_less=True)
344 fu_bitdict = {}
345 fu_selected = {}
346 for i, funame in enumerate(fus.keys()):
347 fu_bitdict[funame] = fu_enable[i]
348 fu_selected[funame] = fu_busy[i]
349
350 # identify function units and create a list by fnunit so that
351 # PriorityPickers can be created for selecting one of them that
352 # isn't busy at the time the incoming instruction needs passing on
353 by_fnunit = defaultdict(list)
354 for fname, member in Function.__members__.items():
355 for funame, fu in fus.items():
356 fnunit = fu.fnunit.value
357 if member.value & fnunit: # this FU handles this type of op
358 by_fnunit[fname].append((funame, fu)) # add by Function
359
360 # ok now just print out the list of FUs by Function, because we can
361 for fname, fu_list in by_fnunit.items():
362 print ("FUs by type", fname, fu_list)
363
364 # now create a PriorityPicker per FU-type such that only one
365 # non-busy FU will be picked
366 issue_pps = {}
367 fu_found = Signal() # take a note if no Function Unit was available
368 for fname, fu_list in by_fnunit.items():
369 i_pp = PriorityPicker(len(fu_list))
370 m.submodules['i_pp_%s' % fname] = i_pp
371 i_l = []
372 for i, (funame, fu) in enumerate(fu_list):
373 # match the decoded instruction (e.do.fn_unit) against the
374 # "capability" of this FU, gate that by whether that FU is
375 # busy, and drop that into the PriorityPicker.
376 # this will give us an output of the first available *non-busy*
377 # Function Unit (Reservation Statio) capable of handling this
378 # instruction.
379 fnunit = fu.fnunit.value
380 en_req = Signal(name="issue_en_%s" % funame, reset_less=True)
381 fnmatch = (self.ireg.e.do.fn_unit & fnunit).bool()
382 comb += en_req.eq(fnmatch & ~fu.busy_o &
383 self.instr_active)
384 i_l.append(en_req) # store in list for doing the Cat-trick
385 # picker output, gated by enable: store in fu_bitdict
386 po = Signal(name="o_issue_pick_"+funame) # picker output
387 comb += po.eq(i_pp.o[i] & i_pp.en_o)
388 comb += fu_bitdict[funame].eq(po)
389 comb += fu_selected[funame].eq(fu.busy_o | po)
390 # if we don't do this, then when there are no FUs available,
391 # the "p.o_ready" signal will go back "ok we accepted this
392 # instruction" which of course isn't true.
393 with m.If(i_pp.en_o):
394 comb += fu_found.eq(1)
395 # for each input, Cat them together and drop them into the picker
396 comb += i_pp.i.eq(Cat(*i_l))
397
398 # rdmask, which is for registers needs to come from the *main* decoder
399 for funame, fu in fus.items():
400 rdmask = get_rdflags(m, self.ireg.e, fu)
401 comb += fu.rdmaskn.eq(~rdmask)
402
403 # sigh - need a NOP counter
404 counter = Signal(2)
405 with m.If(counter != 0):
406 sync += counter.eq(counter - 1)
407 comb += busy_o.eq(1)
408
409 # default to reading from incoming instruction: may be overridden
410 # by copy from latch when "waiting"
411 comb += self.ireg.eq(self.i)
412 # always say "ready" except if overridden
413 comb += self.p.o_ready.eq(1)
414
415 with m.FSM():
416 with m.State("READY"):
417 with m.If(self.p.i_valid): # run only when valid
418 with m.Switch(self.ireg.e.do.insn_type):
419 # check for ATTN: halt if true
420 with m.Case(MicrOp.OP_ATTN):
421 m.d.sync += self.o.core_terminate_o.eq(1)
422
423 # fake NOP - this isn't really used (Issuer detects NOP)
424 with m.Case(MicrOp.OP_NOP):
425 sync += counter.eq(2)
426 comb += busy_o.eq(1)
427
428 with m.Default():
429 comb += self.instr_active.eq(1)
430 comb += self.p.o_ready.eq(0)
431 # connect instructions. only one enabled at a time
432 for funame, fu in fus.items():
433 do = self.des[funame]
434 enable = fu_bitdict[funame]
435
436 # run this FunctionUnit if enabled route op,
437 # issue, busy, read flags and mask to FU
438 with m.If(enable):
439 # operand comes from the *local* decoder
440 # do not actually issue, though, if there
441 # is a waw hazard. decoder has to still
442 # be asserted in order to detect that, tho
443 comb += fu.oper_i.eq_from(do)
444 if funame == 'mmu0':
445 # URRR this is truly dreadful.
446 # OP_FETCH_FAILED is a "fake" op.
447 # no instruction creates it. OP_TRAP
448 # uses the *main* decoder: this is
449 # a *Satellite* decoder that reacts
450 # on *insn_in*... not fake ops. gaah.
451 main_op = self.ireg.e.do
452 with m.If(main_op.insn_type ==
453 MicrOp.OP_FETCH_FAILED):
454 comb += fu.oper_i.insn_type.eq(
455 MicrOp.OP_FETCH_FAILED)
456 comb += fu.oper_i.fn_unit.eq(
457 Function.MMU)
458 # issue when valid (and no write-hazard)
459 comb += fu.issue_i.eq(~self.waw_hazard)
460 # instruction ok, indicate ready
461 comb += self.p.o_ready.eq(1)
462
463 if self.allow_overlap:
464 with m.If(~fu_found | self.waw_hazard):
465 # latch copy of instruction
466 sync += ilatch.eq(self.i)
467 comb += self.p.o_ready.eq(1) # accept
468 comb += busy_o.eq(1)
469 m.next = "WAITING"
470
471 with m.State("WAITING"):
472 comb += self.instr_active.eq(1)
473 comb += self.p.o_ready.eq(0)
474 comb += busy_o.eq(1)
475 # using copy of instruction, keep waiting until an FU is free
476 comb += self.ireg.eq(ilatch)
477 with m.If(fu_found): # wait for conflict to clear
478 # connect instructions. only one enabled at a time
479 for funame, fu in fus.items():
480 do = self.des[funame]
481 enable = fu_bitdict[funame]
482
483 # run this FunctionUnit if enabled route op,
484 # issue, busy, read flags and mask to FU
485 with m.If(enable):
486 # operand comes from the *local* decoder,
487 # which is asserted even if not issued,
488 # so that WaW-detection can check for hazards.
489 # only if the waw hazard is clear does the
490 # instruction actually get issued
491 comb += fu.oper_i.eq_from(do)
492 # issue when valid
493 comb += fu.issue_i.eq(~self.waw_hazard)
494 with m.If(~self.waw_hazard):
495 comb += self.p.o_ready.eq(1)
496 comb += busy_o.eq(0)
497 m.next = "READY"
498
499 print ("core: overlap allowed", self.allow_overlap)
500 # true when any FU is busy (including the cycle where it is perhaps
501 # to be issued - because that's what fu_busy is)
502 comb += any_busy_o.eq(fu_busy.bool())
503 if not self.allow_overlap:
504 # for simple non-overlap, if any instruction is busy, set
505 # busy output for core.
506 comb += busy_o.eq(any_busy_o)
507 else:
508 # sigh deal with a fun situation that needs to be investigated
509 # and resolved
510 with m.If(self.issue_conflict):
511 comb += busy_o.eq(1)
512 # make sure that LDST, SPR, MMU, Branch and Trap all say "busy"
513 # and do not allow overlap. these are all the ones that
514 # are non-forward-progressing: exceptions etc. that otherwise
515 # change CoreState for some reason (MSR, PC, SVSTATE)
516 for funame, fu in fus.items():
517 if (funame.lower().startswith('ldst') or
518 funame.lower().startswith('branch') or
519 funame.lower().startswith('mmu') or
520 funame.lower().startswith('spr') or
521 funame.lower().startswith('trap')):
522 with m.If(fu.busy_o):
523 comb += busy_o.eq(1)
524 # for SPR pipeline pause dec/tb FSM to avoid race condition
525 # TODO: really this should be much more sophisticated,
526 # spot MTSPR, spot that DEC/TB is what is to be updated.
527 # a job for PowerDecoder2, there
528 if funame.lower().startswith('spr'):
529 with m.If(fu.busy_o #& fu.oper_i.insn_type == OP_MTSPR
530 ):
531 comb += self.pause_dec_tb.eq(1)
532
533 # return both the function unit "enable" dict as well as the "busy".
534 # the "busy-or-issued" can be passed in to the Read/Write port
535 # connecters to give them permission to request access to regfiles
536 return fu_bitdict, fu_selected
537
538 def connect_rdport(self, m, fu_bitdict, fu_selected,
539 rdpickers, regfile, regname, fspec):
540 comb, sync = m.d.comb, m.d.sync
541 fus = self.fus.fus
542 regs = self.regs
543
544 rpidx = regname
545
546 # select the required read port. these are pre-defined sizes
547 rfile = regs.rf[regfile.lower()]
548 rport = rfile.r_ports[rpidx]
549 print("read regfile", rpidx, regfile, regs.rf.keys(),
550 rfile, rfile.unary)
551
552 # for checking if the read port has an outstanding write
553 if self.make_hazard_vecs:
554 wv = regs.wv[regfile.lower()]
555 wvchk = wv.q_int # write-vec bit-level hazard check
556
557 # if a hazard is detected on this read port, simply blithely block
558 # every FU from reading on it. this is complete overkill but very
559 # simple for now.
560 hazard_detected = Signal(name="raw_%s_%s" % (regfile, rpidx))
561
562 fspecs = fspec
563 if not isinstance(fspecs, list):
564 fspecs = [fspecs]
565
566 rdflags = []
567 pplen = 0
568 ppoffs = []
569 for i, fspec in enumerate(fspecs):
570 # get the regfile specs for this regfile port
571 print ("fpsec", i, fspec, len(fspec.specs))
572 name = "%s_%s_%d" % (regfile, regname, i)
573 ppoffs.append(pplen) # record offset for picker
574 pplen += len(fspec.specs)
575 rdflag = Signal(name="rdflag_"+name, reset_less=True)
576 comb += rdflag.eq(fspec.okflag)
577 rdflags.append(rdflag)
578
579 print ("pplen", pplen)
580
581 # create a priority picker to manage this port
582 rdpickers[regfile][rpidx] = rdpick = PriorityPicker(pplen)
583 m.submodules["rdpick_%s_%s" % (regfile, rpidx)] = rdpick
584
585 rens = []
586 addrs = []
587 wvens = []
588
589 for i, fspec in enumerate(fspecs):
590 (rf, _read, wid, fuspecs) = \
591 (fspec.okflag, fspec.regport, fspec.wid, fspec.specs)
592 # connect up the FU req/go signals, and the reg-read to the FU
593 # and create a Read Broadcast Bus
594 for pi, fuspec in enumerate(fspec.specs):
595 (funame, fu, idx) = (fuspec.funame, fuspec.fu, fuspec.idx)
596 pi += ppoffs[i]
597 name = "%s_%s_%s_%i" % (regfile, rpidx, funame, pi)
598 fu_active = fu_selected[funame]
599 fu_issued = fu_bitdict[funame]
600
601 # get (or set up) a latched copy of read register number
602 # and (sigh) also the read-ok flag
603 # TODO: use nmutil latchregister
604 rhname = "%s_%s_%d" % (regfile, regname, i)
605 rdflag = Signal(name="rdflag_%s_%s" % (funame, rhname),
606 reset_less=True)
607 if rhname not in fu.rf_latches:
608 rfl = Signal(name="rdflag_latch_%s_%s" % (funame, rhname))
609 fu.rf_latches[rhname] = rfl
610 with m.If(fu.issue_i):
611 sync += rfl.eq(rdflags[i])
612 else:
613 rfl = fu.rf_latches[rhname]
614
615 # now the register port
616 rname = "%s_%s_%s_%d" % (funame, regfile, regname, pi)
617 read = Signal.like(_read, name="read_"+rname)
618 if rname not in fu.rd_latches:
619 rdl = Signal.like(_read, name="rdlatch_"+rname)
620 fu.rd_latches[rname] = rdl
621 with m.If(fu.issue_i):
622 sync += rdl.eq(_read)
623 else:
624 rdl = fu.rd_latches[rname]
625
626 # make the read immediately available on issue cycle
627 # after the read cycle, otherwies use the latched copy.
628 # this captures the regport and okflag on issue
629 with m.If(fu.issue_i):
630 comb += read.eq(_read)
631 comb += rdflag.eq(rdflags[i])
632 with m.Else():
633 comb += read.eq(rdl)
634 comb += rdflag.eq(rfl)
635
636 # connect request-read to picker input, and output to go-rd
637 addr_en = Signal.like(read, name="addr_en_"+name)
638 pick = Signal(name="pick_"+name) # picker input
639 rp = Signal(name="rp_"+name) # picker output
640 delay_pick = Signal(name="dp_"+name) # read-enable "underway"
641 rhazard = Signal(name="rhaz_"+name)
642
643 # exclude any currently-enabled read-request (mask out active)
644 # entirely block anything hazarded from being picked
645 comb += pick.eq(fu.rd_rel_o[idx] & fu_active & rdflag &
646 ~delay_pick & ~rhazard)
647 comb += rdpick.i[pi].eq(pick)
648 comb += fu.go_rd_i[idx].eq(delay_pick) # pass in *delayed* pick
649
650 # if picked, select read-port "reg select" number to port
651 comb += rp.eq(rdpick.o[pi] & rdpick.en_o)
652 sync += delay_pick.eq(rp) # delayed "pick"
653 comb += addr_en.eq(Mux(rp, read, 0))
654
655 # the read-enable happens combinatorially (see mux-bus below)
656 # but it results in the data coming out on a one-cycle delay.
657 if rfile.unary:
658 rens.append(addr_en)
659 else:
660 addrs.append(addr_en)
661 rens.append(rp)
662
663 # use the *delayed* pick signal to put requested data onto bus
664 with m.If(delay_pick):
665 # connect regfile port to input, creating fan-out Bus
666 src = fu.src_i[idx]
667 print("reg connect widths",
668 regfile, regname, pi, funame,
669 src.shape(), rport.o_data.shape())
670 # all FUs connect to same port
671 comb += src.eq(rport.o_data)
672
673 if not self.make_hazard_vecs:
674 continue
675
676 # read the write-hazard bitvector (wv) for any bit that is
677 wvchk_en = Signal(len(wvchk), name="wv_chk_addr_en_"+name)
678 issue_active = Signal(name="rd_iactive_"+name)
679 # XXX combinatorial loop here
680 comb += issue_active.eq(fu_active & rdflag)
681 with m.If(issue_active):
682 if rfile.unary:
683 comb += wvchk_en.eq(read)
684 else:
685 comb += wvchk_en.eq(1<<read)
686 # if FU is busy (which doesn't get set at the same time as
687 # issue) and no hazard was detected, clear wvchk_en (i.e.
688 # stop checking for hazards). there is a loop here, but it's
689 # via a DFF, so is ok. some linters may complain, but hey.
690 with m.If(fu.busy_o & ~rhazard):
691 comb += wvchk_en.eq(0)
692
693 # read-hazard is ANDed with (filtered by) what is actually
694 # being requested.
695 comb += rhazard.eq((wvchk & wvchk_en).bool())
696
697 wvens.append(wvchk_en)
698
699 # or-reduce the muxed read signals
700 if rfile.unary:
701 # for unary-addressed
702 comb += rport.ren.eq(ortreereduce_sig(rens))
703 else:
704 # for binary-addressed
705 comb += rport.addr.eq(ortreereduce_sig(addrs))
706 comb += rport.ren.eq(Cat(*rens).bool())
707 print ("binary", regfile, rpidx, rport, rport.ren, rens, addrs)
708
709 if not self.make_hazard_vecs:
710 return Const(0) # declare "no hazards"
711
712 # enable the read bitvectors for this issued instruction
713 # and return whether any write-hazard bit is set
714 wvchk_and = Signal(len(wvchk), name="wv_chk_"+name)
715 comb += wvchk_and.eq(wvchk & ortreereduce_sig(wvens))
716 comb += hazard_detected.eq(wvchk_and.bool())
717 return hazard_detected
718
719 def connect_rdports(self, m, fu_bitdict, fu_selected):
720 """connect read ports
721
722 orders the read regspecs into a dict-of-dicts, by regfile, by
723 regport name, then connects all FUs that want that regport by
724 way of a PriorityPicker.
725 """
726 comb, sync = m.d.comb, m.d.sync
727 fus = self.fus.fus
728 regs = self.regs
729 rd_hazard = []
730
731 # dictionary of lists of regfile read ports
732 byregfiles_rdspec = self.get_byregfiles(m, True)
733
734 # okaay, now we need a PriorityPicker per regfile per regfile port
735 # loootta pickers... peter piper picked a pack of pickled peppers...
736 rdpickers = {}
737 for regfile, fuspecs in byregfiles_rdspec.items():
738 rdpickers[regfile] = {}
739
740 # argh. an experiment to merge RA and RB in the INT regfile
741 # (we have too many read/write ports)
742 if self.regreduce_en:
743 if regfile == 'INT':
744 fuspecs['rabc'] = [fuspecs.pop('rb')]
745 fuspecs['rabc'].append(fuspecs.pop('rc'))
746 fuspecs['rabc'].append(fuspecs.pop('ra'))
747 if regfile == 'FAST':
748 fuspecs['fast1'] = [fuspecs.pop('fast1')]
749 if 'fast2' in fuspecs:
750 fuspecs['fast1'].append(fuspecs.pop('fast2'))
751 if 'fast3' in fuspecs:
752 fuspecs['fast1'].append(fuspecs.pop('fast3'))
753
754 # for each named regfile port, connect up all FUs to that port
755 # also return (and collate) hazard detection)
756 for (regname, fspec) in sort_fuspecs(fuspecs):
757 print("connect rd", regname, fspec)
758 rh = self.connect_rdport(m, fu_bitdict, fu_selected,
759 rdpickers, regfile,
760 regname, fspec)
761 rd_hazard.append(rh)
762
763 return Cat(*rd_hazard).bool()
764
765 def make_hazards(self, m, regfile, rfile, wvclr, wvset,
766 funame, regname, idx,
767 addr_en, wp, fu, fu_active, wrflag, write,
768 fu_wrok):
769 """make_hazards: a setter and a clearer for the regfile write ports
770
771 setter is at issue time (using PowerDecoder2 regfile write numbers)
772 clearer is at regfile write time (when FU has said what to write to)
773
774 there is *one* unusual case here which has to be dealt with:
775 when the Function Unit does *NOT* request a write to the regfile
776 (has its data.ok bit CLEARED). this is perfectly legitimate.
777 and a royal pain.
778 """
779 comb, sync = m.d.comb, m.d.sync
780 name = "%s_%s_%d" % (funame, regname, idx)
781
782 # connect up the bitvector write hazard. unlike the
783 # regfile writeports, a ONE must be written to the corresponding
784 # bit of the hazard bitvector (to indicate the existence of
785 # the hazard)
786
787 # the detection of what shall be written to is based
788 # on *issue*. it is delayed by 1 cycle so that instructions
789 # "addi 5,5,0x2" do not cause combinatorial loops due to
790 # fake-dependency on *themselves*. this will totally fail
791 # spectacularly when doing multi-issue
792 print ("write vector (for regread)", regfile, wvset)
793 wviaddr_en = Signal(len(wvset), name="wv_issue_addr_en_"+name)
794 issue_active = Signal(name="iactive_"+name)
795 sync += issue_active.eq(fu.issue_i & fu_active & wrflag)
796 with m.If(issue_active):
797 if rfile.unary:
798 comb += wviaddr_en.eq(write)
799 else:
800 comb += wviaddr_en.eq(1<<write)
801
802 # deal with write vector clear: this kicks in when the regfile
803 # is written to, and clears the corresponding bitvector entry
804 print ("write vector", regfile, wvclr)
805 wvaddr_en = Signal(len(wvclr), name="wvaddr_en_"+name)
806 if rfile.unary:
807 comb += wvaddr_en.eq(addr_en)
808 else:
809 with m.If(wp):
810 comb += wvaddr_en.eq(1<<addr_en)
811
812 # XXX ASSUME that LDSTFunctionUnit always sets the data it intends to
813 # this may NOT be the case when an exception occurs
814 if isinstance(fu, LDSTFunctionUnit):
815 return wvaddr_en, wviaddr_en
816
817 # okaaay, this is preparation for the awkward case.
818 # * latch a copy of wrflag when issue goes high.
819 # * when the fu_wrok (data.ok) flag is NOT set,
820 # but the FU is done, the FU is NEVER going to write
821 # so the bitvector has to be cleared.
822 latch_wrflag = Signal(name="latch_wrflag_"+name)
823 with m.If(~fu.busy_o):
824 sync += latch_wrflag.eq(0)
825 with m.If(fu.issue_i & fu_active):
826 sync += latch_wrflag.eq(wrflag)
827 with m.If(fu.alu_done_o & latch_wrflag & ~fu_wrok):
828 if rfile.unary:
829 comb += wvaddr_en.eq(write) # addr_en gated with wp, don't use
830 else:
831 comb += wvaddr_en.eq(1<<addr_en) # binary addr_en not gated
832
833 return wvaddr_en, wviaddr_en
834
835 def connect_wrport(self, m, fu_bitdict, fu_selected,
836 wrpickers, regfile, regname, fspec):
837 comb, sync = m.d.comb, m.d.sync
838 fus = self.fus.fus
839 regs = self.regs
840
841 rpidx = regname
842
843 # select the required write port. these are pre-defined sizes
844 rfile = regs.rf[regfile.lower()]
845 wport = rfile.w_ports[rpidx]
846
847 print("connect wr", regname, "unary", rfile.unary, fspec)
848 print(regfile, regs.rf.keys())
849
850 # select the write-protection hazard vector. note that this still
851 # requires to WRITE to the hazard bitvector! read-requests need
852 # to RAISE the bitvector (set it to 1), which, duh, requires a WRITE
853 if self.make_hazard_vecs:
854 wv = regs.wv[regfile.lower()]
855 wvset = wv.s # write-vec bit-level hazard ctrl
856 wvclr = wv.r # write-vec bit-level hazard ctrl
857 wvchk = wv.q # write-after-write hazard check
858
859 fspecs = fspec
860 if not isinstance(fspecs, list):
861 fspecs = [fspecs]
862
863 pplen = 0
864 writes = []
865 ppoffs = []
866 wrflags = []
867 for i, fspec in enumerate(fspecs):
868 # get the regfile specs for this regfile port
869 (wf, _write, wid, fuspecs) = \
870 (fspec.okflag, fspec.regport, fspec.wid, fspec.specs)
871 print ("fpsec", i, "wrflag", wf, fspec, len(fuspecs))
872 ppoffs.append(pplen) # record offset for picker
873 pplen += len(fuspecs)
874
875 name = "%s_%s_%d" % (regfile, regname, i)
876 wrflag = Signal(name="wr_flag_"+name)
877 if wf is not None:
878 comb += wrflag.eq(wf)
879 else:
880 comb += wrflag.eq(0)
881 wrflags.append(wrflag)
882
883 # create a priority picker to manage this port
884 wrpickers[regfile][rpidx] = wrpick = PriorityPicker(pplen)
885 m.submodules["wrpick_%s_%s" % (regfile, rpidx)] = wrpick
886
887 wsigs = []
888 wens = []
889 wvsets = []
890 wvseten = []
891 wvclren = []
892 #wvens = [] - not needed: reading of writevec is permanently held hi
893 addrs = []
894 for i, fspec in enumerate(fspecs):
895 # connect up the FU req/go signals and the reg-read to the FU
896 # these are arbitrated by Data.ok signals
897 (wf, _write, wid, fuspecs) = \
898 (fspec.okflag, fspec.regport, fspec.wid, fspec.specs)
899 for pi, fuspec in enumerate(fspec.specs):
900 (funame, fu, idx) = (fuspec.funame, fuspec.fu, fuspec.idx)
901 fu_requested = fu_bitdict[funame]
902 pi += ppoffs[i]
903 name = "%s_%s_%s_%d" % (funame, regfile, regname, idx)
904 # get (or set up) a write-latched copy of write register number
905 write = Signal.like(_write, name="write_"+name)
906 rname = "%s_%s_%s_%d" % (funame, regfile, regname, idx)
907 if rname not in fu.wr_latches:
908 wrl = Signal.like(_write, name="wrlatch_"+rname)
909 fu.wr_latches[rname] = write
910 # do not depend on fu.issue_i here, it creates a
911 # combinatorial loop on waw checking. using the FU
912 # "enable" bitdict entry for this FU is sufficient,
913 # because the PowerDecoder2 read/write nums are
914 # valid continuously when the instruction is valid
915 with m.If(fu_requested):
916 sync += wrl.eq(_write)
917 comb += write.eq(_write)
918 with m.Else():
919 comb += write.eq(wrl)
920 else:
921 write = fu.wr_latches[rname]
922
923 # write-request comes from dest.ok
924 dest = fu.get_out(idx)
925 fu_dest_latch = fu.get_fu_out(idx) # latched output
926 name = "%s_%s_%d" % (funame, regname, idx)
927 fu_wrok = Signal(name="fu_wrok_"+name, reset_less=True)
928 comb += fu_wrok.eq(dest.ok & fu.busy_o)
929
930 # connect request-write to picker input, and output to go-wr
931 fu_active = fu_selected[funame]
932 pick = fu.wr.rel_o[idx] & fu_active
933 comb += wrpick.i[pi].eq(pick)
934 # create a single-pulse go write from the picker output
935 wr_pick = Signal(name="wpick_%s_%s_%d" % (funame, regname, idx))
936 comb += wr_pick.eq(wrpick.o[pi] & wrpick.en_o)
937 comb += fu.go_wr_i[idx].eq(rising_edge(m, wr_pick))
938
939 # connect the regspec write "reg select" number to this port
940 # only if one FU actually requests (and is granted) the port
941 # will the write-enable be activated
942 wname = "waddr_en_%s_%s_%d" % (funame, regname, idx)
943 addr_en = Signal.like(write, name=wname)
944 wp = Signal()
945 comb += wp.eq(wr_pick & wrpick.en_o)
946 comb += addr_en.eq(Mux(wp, write, 0))
947 if rfile.unary:
948 wens.append(addr_en)
949 else:
950 addrs.append(addr_en)
951 wens.append(wp)
952
953 # connect regfile port to input
954 print("reg connect widths",
955 regfile, regname, pi, funame,
956 dest.shape(), wport.i_data.shape())
957 wsigs.append(fu_dest_latch)
958
959 # now connect up the bitvector write hazard
960 if not self.make_hazard_vecs:
961 continue
962 res = self.make_hazards(m, regfile, rfile, wvclr, wvset,
963 funame, regname, idx,
964 addr_en, wp, fu, fu_active,
965 wrflags[i], write, fu_wrok)
966 wvaddr_en, wv_issue_en = res
967 wvclren.append(wvaddr_en) # set only: no data => clear bit
968 wvseten.append(wv_issue_en) # set data same as enable
969
970 # read the write-hazard bitvector (wv) for any bit that is
971 fu_requested = fu_bitdict[funame]
972 wvchk_en = Signal(len(wvchk), name="waw_chk_addr_en_"+name)
973 issue_active = Signal(name="waw_iactive_"+name)
974 whazard = Signal(name="whaz_"+name)
975 if wf is None:
976 # XXX EEK! STATE regfile (branch) does not have an
977 # write-active indicator in regspec_decode_write()
978 print ("XXX FIXME waw_iactive", issue_active,
979 fu_requested, wf)
980 else:
981 # check bits from the incoming instruction. note (back
982 # in connect_instruction) that the decoder is held for
983 # us to be able to do this, here... *without* issue being
984 # held HI. we MUST NOT gate this with fu.issue_i or
985 # with fu_bitdict "enable": it would create a loop
986 comb += issue_active.eq(wf)
987 with m.If(issue_active):
988 if rfile.unary:
989 comb += wvchk_en.eq(write)
990 else:
991 comb += wvchk_en.eq(1<<write)
992 # if FU is busy (which doesn't get set at the same time as
993 # issue) and no hazard was detected, clear wvchk_en (i.e.
994 # stop checking for hazards). there is a loop here, but it's
995 # via a DFF, so is ok. some linters may complain, but hey.
996 with m.If(fu.busy_o & ~whazard):
997 comb += wvchk_en.eq(0)
998
999 # write-hazard is ANDed with (filtered by) what is actually
1000 # being requested. the wvchk data is on a one-clock delay,
1001 # and wvchk_en comes directly from the main decoder
1002 comb += whazard.eq((wvchk & wvchk_en).bool())
1003 with m.If(whazard):
1004 comb += fu._waw_hazard.eq(1)
1005
1006 #wvens.append(wvchk_en)
1007
1008 # here is where we create the Write Broadcast Bus. simple, eh?
1009 comb += wport.i_data.eq(ortreereduce_sig(wsigs))
1010 if rfile.unary:
1011 # for unary-addressed
1012 comb += wport.wen.eq(ortreereduce_sig(wens))
1013 else:
1014 # for binary-addressed
1015 comb += wport.addr.eq(ortreereduce_sig(addrs))
1016 comb += wport.wen.eq(ortreereduce_sig(wens))
1017
1018 if not self.make_hazard_vecs:
1019 return [], []
1020
1021 # return these here rather than set wvclr/wvset directly,
1022 # because there may be more than one write-port to a given
1023 # regfile. example: XER has a write-port for SO, CA, and OV
1024 # and the *last one added* of those would overwrite the other
1025 # two. solution: have connect_wrports collate all the
1026 # or-tree-reduced bitvector set/clear requests and drop them
1027 # in as a single "thing". this can only be done because the
1028 # set/get is an unary bitvector.
1029 print ("make write-vecs", regfile, regname, wvset, wvclr)
1030 return (wvclren, # clear (regfile write)
1031 wvseten) # set (issue time)
1032
1033 def connect_wrports(self, m, fu_bitdict, fu_selected):
1034 """connect write ports
1035
1036 orders the write regspecs into a dict-of-dicts, by regfile,
1037 by regport name, then connects all FUs that want that regport
1038 by way of a PriorityPicker.
1039
1040 note that the write-port wen, write-port data, and go_wr_i all need to
1041 be on the exact same clock cycle. as there is a combinatorial loop bug
1042 at the moment, these all use sync.
1043 """
1044 comb, sync = m.d.comb, m.d.sync
1045 fus = self.fus.fus
1046 regs = self.regs
1047 # dictionary of lists of regfile write ports
1048 byregfiles_wrspec = self.get_byregfiles(m, False)
1049
1050 # same for write ports.
1051 # BLECH! complex code-duplication! BLECH!
1052 wrpickers = {}
1053 wvclrers = defaultdict(list)
1054 wvseters = defaultdict(list)
1055 for regfile, fuspecs in byregfiles_wrspec.items():
1056 wrpickers[regfile] = {}
1057
1058 if self.regreduce_en:
1059 # argh, more port-merging
1060 if regfile == 'INT':
1061 fuspecs['o'] = [fuspecs.pop('o')]
1062 fuspecs['o'].append(fuspecs.pop('o1'))
1063 if regfile == 'FAST':
1064 fuspecs['fast1'] = [fuspecs.pop('fast1')]
1065 if 'fast2' in fuspecs:
1066 fuspecs['fast1'].append(fuspecs.pop('fast2'))
1067 if 'fast3' in fuspecs:
1068 fuspecs['fast1'].append(fuspecs.pop('fast3'))
1069
1070 # collate these and record them by regfile because there
1071 # are sometimes more write-ports per regfile
1072 for (regname, fspec) in sort_fuspecs(fuspecs):
1073 wvclren, wvseten = self.connect_wrport(m,
1074 fu_bitdict, fu_selected,
1075 wrpickers,
1076 regfile, regname, fspec)
1077 wvclrers[regfile.lower()] += wvclren
1078 wvseters[regfile.lower()] += wvseten
1079
1080 if not self.make_hazard_vecs:
1081 return
1082
1083 # for write-vectors: reduce the clr-ers and set-ers down to
1084 # a single set of bits. otherwise if there are two write
1085 # ports (on some regfiles), the last one doing comb += on
1086 # the reg.wv[regfile] instance "wins" (and all others are ignored,
1087 # whoops). if there was only one write-port per wv regfile this would
1088 # not be an issue.
1089 for regfile in wvclrers.keys():
1090 wv = regs.wv[regfile]
1091 wvset = wv.s # write-vec bit-level hazard ctrl
1092 wvclr = wv.r # write-vec bit-level hazard ctrl
1093 wvclren = wvclrers[regfile]
1094 wvseten = wvseters[regfile]
1095 comb += wvclr.eq(ortreereduce_sig(wvclren)) # clear (regfile write)
1096 comb += wvset.eq(ortreereduce_sig(wvseten)) # set (issue time)
1097
1098 def get_byregfiles(self, m, readmode):
1099
1100 mode = "read" if readmode else "write"
1101 regs = self.regs
1102 fus = self.fus.fus
1103 e = self.ireg.e # decoded instruction to execute
1104
1105 # dictionary of dictionaries of lists/tuples of regfile ports.
1106 # first key: regfile. second key: regfile port name
1107 byregfiles_spec = defaultdict(dict)
1108
1109 for (funame, fu) in fus.items():
1110 # create in each FU a receptacle for the read/write register
1111 # hazard numbers (and okflags for read). to be latched in
1112 # connect_rd/write_ports
1113 if readmode:
1114 fu.rd_latches = {} # read reg number latches
1115 fu.rf_latches = {} # read flag latches
1116 else:
1117 fu.wr_latches = {}
1118
1119 # construct regfile specs: read uses inspec, write outspec
1120 print("%s ports for %s" % (mode, funame))
1121 for idx in range(fu.n_src if readmode else fu.n_dst):
1122 (regfile, regname, wid) = fu.get_io_spec(readmode, idx)
1123 print(" %d %s %s %s" % (idx, regfile, regname, str(wid)))
1124
1125 # the PowerDecoder2 (main one, not the satellites) contains
1126 # the decoded regfile numbers. obtain these now
1127 decinfo = regspec_decode(m, readmode, e, regfile, regname)
1128 okflag, regport = decinfo.okflag, decinfo.regport
1129
1130 # construct the dictionary of regspec information by regfile
1131 if regname not in byregfiles_spec[regfile]:
1132 byregfiles_spec[regfile][regname] = \
1133 ByRegSpec(okflag, regport, wid, [])
1134
1135 # here we start to create "lanes" where each Function Unit
1136 # requiring access to a given [single-contended resource]
1137 # regfile port is appended to a list, so that PriorityPickers
1138 # can be created to give uncontested access to it
1139 fuspec = FUSpec(funame, fu, idx)
1140 byregfiles_spec[regfile][regname].specs.append(fuspec)
1141
1142 # ok just print that all out, for convenience
1143 for regfile, fuspecs in byregfiles_spec.items():
1144 print("regfile %s ports:" % mode, regfile)
1145 for regname, fspec in fuspecs.items():
1146 [okflag, regport, wid, fuspecs] = fspec
1147 print(" rf %s port %s lane: %s" % (mode, regfile, regname))
1148 print(" %s" % regname, wid, okflag, regport)
1149 for (funame, fu, idx) in fuspecs:
1150 fusig = fu.src_i[idx] if readmode else fu.dest[idx]
1151 print(" ", funame, fu.__class__.__name__, idx, fusig)
1152 print()
1153
1154 return byregfiles_spec
1155
1156 def __iter__(self):
1157 yield from self.fus.ports()
1158 yield from self.i.e.ports()
1159 yield from self.l0.ports()
1160 # TODO: regs
1161
1162 def ports(self):
1163 return list(self)
1164
1165
1166 if __name__ == '__main__':
1167 pspec = TestMemPspec(ldst_ifacetype='testpi',
1168 imem_ifacetype='',
1169 addr_wid=64,
1170 allow_overlap=True,
1171 mask_wid=8,
1172 reg_wid=64)
1173 dut = NonProductionCore(pspec)
1174 vl = rtlil.convert(dut, ports=dut.ports())
1175 with open("test_core.il", "w") as f:
1176 f.write(vl)