def basic_simulation(dut): """ A simple functional test to provide a Wavedrom file :param dut: Veriog module under test """ with cocotb.wavedrom.trace(dut.i_a, dut.i_b, dut.o_sum, clk=dut.i_clk) as waves: # start the clock cocotb.fork(Clock(dut.i_clk, 10, units="ns").start()) clkedge = RisingEdge(dut.i_clk) # provide an input sequence manually # cycle-I dut.i_a <= 1 dut.i_b <= 2 yield clkedge yield ReadWrite() print(dut.o_sum.value) # cycle-II dut.i_a <= 2 dut.i_b <= 3 yield clkedge yield ReadWrite() print(dut.o_sum.value) # cycle-II dut.i_a <= 10 dut.i_b <= -15 yield clkedge yield ReadWrite() print(dut.o_sum.value)
async def test_singleton_isinstance(dut): """ Test that the result of trigger expression have a predictable type """ assert isinstance(NextTimeStep(), NextTimeStep) assert isinstance(ReadOnly(), ReadOnly) assert isinstance(ReadWrite(), ReadWrite)
def ping(self): timeout_count = 0 while timeout_count < self.timeout: yield RisingEdge(self.dut.clk) timeout_count += 1 yield ReadOnly() if self.master_ready.value.get_value() == 0: continue else: break if timeout_count == self.timeout: self.dut.log.error("Timed out while waiting for master to be ready") return yield ReadWrite() timeout_count = 0 while timeout_count < self.timeout: yield RisingEdge(self.dut.clk) timeout_count += 1 yield ReadOnly() #if self.dut.out_en.value.get_value() == 0: # continue #else: # break break if timeout_count == self.timeout: self.dut.log.error("Timed out while waiting for master to respond") return self.dut.log.info("Master Responded to ping") self.dut.log.info("\t0x%08X" % self.out_status.value.get_value())
def do_test_readwrite_in_readonly(dut): global exited yield RisingEdge(dut.clk) yield ReadOnly() dut.clk <= 0 yield ReadWrite() exited = True
def gr_test(dut): """ Randmized test for o_sum = i_a + i_b (unsigned addition/subtraction) using GNU Radio """ # start the clock cocotb.fork(Clock(dut.i_clk, 10, units="ns").start()) clkedge = RisingEdge(dut.i_clk) yield clkedge # synchronize ourselves with the clock a_lst = [] b_lst = [] sum_lst = [] # start the simulation for _ in range(10): # randomize the input data (+ve) A = random.randint(0, 100) B = random.randint(0, 100) dut.i_a <= A dut.i_b <= B # wait for posedge and let the output be resolved yield clkedge yield ReadWrite() # there seems some "updating" issue with cocotb here! ... without this statement, this test won't pass # binary to integer i_A = int(dut.i_a) i_B = int(dut.i_b) o_SUM = int(dut.o_sum) # collect test i/p and o/p data a_lst.append(i_A) b_lst.append(i_B) sum_lst.append(o_SUM) # pass the data to GNU Radio and get the return data np.array(a_lst, dtype=np.float32).tofile("data_a.bin") np.array(b_lst, dtype=np.float32).tofile("data_b.bin") main() sum_lst_gr = np.fromfile("data_sum.bin", dtype=np.float32) # compare the returned data with verilator output if np.array_equal(sum_lst_gr, np.array(sum_lst)): dut._log.info("gr_test passed") else: raise TestFailure("gr_test failed") # print for convinience print(np.fromfile("data_a.bin", dtype=np.float32)) print(np.fromfile("data_b.bin", dtype=np.float32)) print(np.fromfile("data_sum.bin", dtype=np.float32))
def multiply_gr_test(dut): """ Randmized test for o_prod = i_a * i_b (signed multiplication) using GNU Radio """ # start the clock cocotb.fork(Clock(dut.i_clk, 10, units='ns').start()) clkedge = RisingEdge(dut.i_clk) yield clkedge # synchronize ourselves with the clock # start the simulation a_lst = [] b_lst = [] prod_lst = [] for _ in range(10): # randomize the input data A = random.randint(0, 100) B = random.randint(0, 100) dut.i_a <= A dut.i_b <= B # wait for posedge and let the output be resolved yield clkedge yield ReadWrite( ) # there seems some "updating" issue with cocotb here! ... without this statement, this test won't pass # binary to integer i_A = int(dut.i_a) i_B = int(dut.i_b) o_PROD = int(dut.o_prod) # collect test i/p and o/p datas a_lst.append(i_A) b_lst.append(i_B) prod_lst.append(o_PROD) # pass the data to Gnu Radio and get the return data np.array(a_lst, dtype=np.float32).tofile("data_a.bin") np.array(b_lst, dtype=np.float32).tofile("data_b.bin") main() # call the main Gnu Radio flowgraph prod_lst_gr = np.fromfile("data_prod.bin", dtype=np.float32) # compare the returned data with verilator output if np.array_equal(prod_lst_gr, np.array(prod_lst)): dut._log.info("gr_test passed") else: raise TestFailure("gr_test failed") # print for convinience print(np.fromfile("data_a.bin", dtype=np.float32)) print(np.fromfile("data_b.bin", dtype=np.float32)) print(np.fromfile("data_prod.bin", dtype=np.float32))
def test_singleton_isinstance(dut): """ Test that the result of trigger expression have a predictable type """ assert isinstance(RisingEdge(dut.clk), RisingEdge) assert isinstance(FallingEdge(dut.clk), FallingEdge) assert isinstance(Edge(dut.clk), Edge) assert isinstance(NextTimeStep(), NextTimeStep) assert isinstance(ReadOnly(), ReadOnly) assert isinstance(ReadWrite(), ReadWrite) yield Timer(1)
def _do_writes(self): """ An internal coroutine that performs pending writes """ while True: yield self._writes_pending.wait() if self._mode != Scheduler._MODE_NORMAL: yield NextTimeStep() yield ReadWrite() while self._writes: handle, value = self._writes.popitem() handle.setimmediatevalue(value) self._writes_pending.clear()
def basic_test(dut): """ Randmized test for o_sum = i_a + i_b (signed addition/subtraction) """ # start the clock cocotb.fork(Clock(dut.i_clk, 10, units="ns").start()) clkedge = RisingEdge(dut.i_clk) yield clkedge # synchronize ourselves with the clock # start the simulation for _ in range(10): # randomize the input data A = random.randint(-10, 15) B = random.randint(-10, 15) dut.i_a <= A dut.i_b <= B # wait for posedge and let the output be resolved yield clkedge yield ReadWrite( ) # there seems some "updating" issue with cocotb here! ... without this statement, this test won't pass # format A if it is a negative number if A < 0: i_A = bin2sign(dut.i_a) else: i_A = int(dut.i_a) # format B if it is a negative number if B < 0: i_B = bin2sign(dut.i_b) else: i_B = int(dut.i_b) # format the output if it is a negative number if adder_model(A, B) < 0: o_SUM = bin2sign(dut.o_sum) else: o_SUM = int(dut.o_sum) # compare with the reference model if o_SUM != adder_model(A, B): raise TestFailure("Randomised test failed with: %s + %s = %s" % (i_A, i_B, o_SUM)) else: dut._log.info("Randomised test passed with: %s + %s = %s" % (i_A, i_B, o_SUM))
def ping(self): timeout_count = 0 while timeout_count < self.timeout: yield RisingEdge(self.dut.clk) timeout_count += 1 yield ReadOnly() if self.master_ready.value.get_value() == 0: continue else: break if timeout_count == self.timeout: cocotb.log.error("Timed out while waiting for master to be ready") return yield ReadWrite() self.dut.in_ready <= 1 self.dut.in_command <= 0 self.dut.in_data <= 0 self.dut.in_address <= 0 self.dut.in_data_count <= 0 self.dut.out_ready <= 1 timeout_count = 0 while timeout_count < self.timeout: yield RisingEdge(self.dut.clk) timeout_count += 1 yield ReadOnly() if self.dut.out_en.value.get_value() == 0: continue else: break if timeout_count == self.timeout: cocotb.log.error("Timed out while waiting for master to respond") return self.dut.in_ready <= 0 cocotb.log.info("Master Responded to ping") cocotb.log.info("\t0x%08X" % self.out_status.value.get_value())
def simulator_timing_triggers(dut): """Playing with the Simulator Timing Triggers""" cocotb.fork(Clock(dut.clk_i, 2).start()) yield reset(dut) # yield Timer( 2) # Fires after the specified simulation time period has elapsed print_fired(dut, "Timer") yield Timer(2, "ns") print_fired(dut, "Timer") yield Timer(2000, "ps") print_fired(dut, "Timer") yield Timer(0.002, "us") print_fired(dut, "Timer") # yield NextTimeStep() # Fires when the next time step is started print_fired(dut, "NextTimeStep") yield ReadWrite( ) # Fires when the readwrite portion of the sim cycles is reached print_fired(dut, "ReadWrite") yield ReadOnly( ) # Fires when the current simulation timestep moves to the readonly phase print_fired(dut, "ReadOnly")
class Scheduler(object): """The main scheduler. Here we accept callbacks from the simulator and schedule the appropriate coroutines. A callback fires, causing the :any:`react` method to be called, with the trigger that caused the callback as the first argument. We look up a list of coroutines to schedule (indexed by the trigger) and schedule them in turn. NB implementors should not depend on the scheduling order! Some additional management is required since coroutines can return a list of triggers, to be scheduled when any one of the triggers fires. To ensure we don't receive spurious callbacks, we have to un-prime all the other triggers when any one fires. Due to the simulator nuances and fun with delta delays we have the following modes: Normal mode - Callbacks cause coroutines to be scheduled - Any pending writes are cached and do not happen immediately ReadOnly mode - Corresponds to cbReadOnlySynch (VPI) or vhpiCbLastKnownDeltaCycle (VHPI). In this state we are not allowed to perform writes. Write mode - Corresponds to cbReadWriteSynch (VPI) or vhpiCbEndOfProcesses (VHPI) In this mode we play back all the cached write updates. We can legally transition from normal->write by registering a ReadWrite callback, however usually once a simulator has entered the ReadOnly phase of a given timestep then we must move to a new timestep before performing any writes. The mechanism for moving to a new timestep may not be consistent across simulators and therefore we provide an abstraction to assist with compatibility. Unless a coroutine has explicitly requested to be scheduled in ReadOnly mode (for example wanting to sample the finally settled value after all delta delays) then it can reasonably be expected to be scheduled during "normal mode" i.e. where writes are permitted. """ _MODE_NORMAL = 1 # noqa _MODE_READONLY = 2 # noqa _MODE_WRITE = 3 # noqa _MODE_TERM = 4 # noqa # Singleton events, recycled to avoid spurious object creation _readonly = ReadOnly() # TODO[gh-759]: For some reason, the scheduler requires that these triggers # are _not_ the same instances used by the tests themselves. This is risky, # because it can lead to them overwriting each other's callbacks. We should # try to remove this `copy.copy` in future. _next_timestep = copy.copy(NextTimeStep()) _readwrite = copy.copy(ReadWrite()) _timer1 = Timer(1) _timer0 = Timer(0) def __init__(self): self.log = SimLog("cocotb.scheduler") if _debug: self.log.setLevel(logging.DEBUG) # A dictionary of pending coroutines for each trigger, # indexed by trigger self._trigger2coros = collections.defaultdict(list) # A dictionary mapping coroutines to the trigger they are waiting for self._coro2trigger = {} # Our main state self._mode = Scheduler._MODE_NORMAL # A dictionary of pending writes self._writes = {} self._pending_coros = [] self._pending_callbacks = [] self._pending_triggers = [] self._pending_threads = [] self._pending_events = [] # Events we need to call set on once we've unwound self._terminate = False self._test_result = None self._entrypoint = None self._main_thread = threading.current_thread() # Select the appropriate scheduling algorithm for this simulator self.advance = self.default_scheduling_algorithm self._is_reacting = False def default_scheduling_algorithm(self): """ Decide whether we need to schedule our own triggers (if at all) in order to progress to the next mode. This algorithm has been tested against the following simulators: Icarus Verilog """ if not self._terminate and self._writes: if self._mode == Scheduler._MODE_NORMAL: if not self._readwrite.primed: self._readwrite.prime(self.react) elif not self._next_timestep.primed: self._next_timestep.prime(self.react) elif self._terminate: if _debug: self.log.debug("Test terminating, scheduling Timer") for t in self._trigger2coros: t.unprime() for t in [self._readwrite, self._readonly, self._next_timestep, self._timer1, self._timer0]: if t.primed: t.unprime() self._timer1.prime(self.begin_test) self._trigger2coros = collections.defaultdict(list) self._coro2trigger = {} self._terminate = False self._mode = Scheduler._MODE_TERM def begin_test(self, trigger=None): """Called to initiate a test. Could be called on start-up or from a callback. """ if _debug: self.log.debug("begin_test called with trigger: %s" % (str(trigger))) if _profiling: ps = pstats.Stats(_profile).sort_stats('cumulative') ps.dump_stats("test_profile.pstat") ctx = profiling_context() else: ctx = nullcontext() with ctx: self._mode = Scheduler._MODE_NORMAL if trigger is not None: trigger.unprime() # Issue previous test result, if there is one if self._test_result is not None: if _debug: self.log.debug("Issue test result to regression object") cocotb.regression_manager.handle_result(self._test_result) self._test_result = None if self._entrypoint is not None: test = self._entrypoint self._entrypoint = None self.schedule(test) self.advance() def react(self, trigger): """ Called when a trigger fires. We ensure that we only start the event loop once, rather than letting it recurse. """ if self._is_reacting: # queue up the trigger, the event loop will get to it self._pending_triggers.append(trigger) return # start the event loop self._is_reacting = True try: self._event_loop(trigger) finally: self._is_reacting = False def _event_loop(self, trigger): """ Run an event loop triggered by the given trigger. The loop will keep running until no further triggers fire. This should be triggered by only: * The beginning of a test, when there is no trigger to react to * A GPI trigger """ if _profiling: ctx = profiling_context() else: ctx = nullcontext() with ctx: # When a trigger fires it is unprimed internally if _debug: self.log.debug("Trigger fired: %s" % str(trigger)) # trigger.unprime() if self._mode == Scheduler._MODE_TERM: if _debug: self.log.debug("Ignoring trigger %s since we're terminating" % str(trigger)) return if trigger is self._readonly: self._mode = Scheduler._MODE_READONLY # Only GPI triggers affect the simulator scheduling mode elif isinstance(trigger, GPITrigger): self._mode = Scheduler._MODE_NORMAL # We're the only source of ReadWrite triggers which are only used for # playing back any cached signal updates if trigger is self._readwrite: if _debug: self.log.debug("Writing cached signal updates") while self._writes: handle, value = self._writes.popitem() handle.setimmediatevalue(value) self._readwrite.unprime() return # Similarly if we've scheduled our next_timestep on way to readwrite if trigger is self._next_timestep: if not self._writes: self.log.error( "Moved to next timestep without any pending writes!") else: self.log.debug( "Priming ReadWrite trigger so we can playback writes") self._readwrite.prime(self.react) return # work through triggers one by one is_first = True self._pending_triggers.append(trigger) while self._pending_triggers: trigger = self._pending_triggers.pop(0) if not is_first and isinstance(trigger, GPITrigger): self.log.warning( "A GPI trigger occurred after entering react - this " "should not happen." ) assert False # this only exists to enable the warning above is_first = False if trigger not in self._trigger2coros: # GPI triggers should only be ever pending if there is an # associated coroutine waiting on that trigger, otherwise it would # have been unprimed already if isinstance(trigger, GPITrigger): self.log.critical( "No coroutines waiting on trigger that fired: %s" % str(trigger)) trigger.log.info("I'm the culprit") # For Python triggers this isn't actually an error - we might do # event.set() without knowing whether any coroutines are actually # waiting on this event, for example elif _debug: self.log.debug( "No coroutines waiting on trigger that fired: %s" % str(trigger)) continue # Scheduled coroutines may append to our waiting list so the first # thing to do is pop all entries waiting on this trigger. scheduling = self._trigger2coros.pop(trigger) if _debug: debugstr = "\n\t".join([coro.__name__ for coro in scheduling]) if len(scheduling): debugstr = "\n\t" + debugstr self.log.debug("%d pending coroutines for event %s%s" % (len(scheduling), str(trigger), debugstr)) # This trigger isn't needed any more trigger.unprime() for coro in scheduling: if _debug: self.log.debug("Scheduling coroutine %s" % (coro.__name__)) self.schedule(coro, trigger=trigger) if _debug: self.log.debug("Scheduled coroutine %s" % (coro.__name__)) # Schedule may have queued up some events so we'll burn through those while self._pending_events: if _debug: self.log.debug("Scheduling pending event %s" % (str(self._pending_events[0]))) self._pending_events.pop(0).set() # no more pending triggers self.advance() if _debug: self.log.debug("All coroutines scheduled, handing control back" " to simulator") def unschedule(self, coro): """Unschedule a coroutine. Unprime any pending triggers""" # Unprime the trigger this coroutine is waiting on try: trigger = self._coro2trigger.pop(coro) except KeyError: # coroutine probably finished pass else: if coro in self._trigger2coros[trigger]: self._trigger2coros[trigger].remove(coro) if not self._trigger2coros[trigger]: trigger.unprime() del self._trigger2coros[trigger] if Join(coro) in self._trigger2coros: self._pending_triggers.append(Join(coro)) else: try: # throws an error if the background coroutine errored # and no one was monitoring it coro.retval except Exception as e: self._test_result = TestError( "Forked coroutine {} raised exception {}" .format(coro, e) ) self._terminate = True def save_write(self, handle, value): if self._mode == Scheduler._MODE_READONLY: raise Exception("Write to object {0} was scheduled during a read-only sync phase.".format(handle._name)) self._writes[handle] = value def _coroutine_yielded(self, coro, trigger): """Prime the trigger and update our internal mappings.""" self._coro2trigger[coro] = trigger self._trigger2coros[trigger].append(coro) if not trigger.primed: try: trigger.prime(self.react) except Exception as e: # Convert any exceptions into a test result self.finish_test( create_error(self, "Unable to prime trigger %s: %s" % (str(trigger), str(e)))) def queue(self, coroutine): """Queue a coroutine for execution""" self._pending_coros.append(coroutine) def queue_function(self, coroutine): """Queue a coroutine for execution and move the containing thread so that it does not block execution of the main thread any longer. """ # We should be able to find ourselves inside the _pending_threads list for t in self._pending_threads: if t.thread == threading.current_thread(): t.thread_suspend() self._pending_coros.append(coroutine) return t def run_in_executor(self, func, *args, **kwargs): """Run the coroutine in a separate execution thread and return a yieldable object for the caller. """ # Create a thread # Create a trigger that is called as a result of the thread finishing # Create an Event object that the caller can yield on # Event object set when the thread finishes execution, this blocks the # calling coroutine (but not the thread) until the external completes def execute_external(func, _waiter): _waiter._outcome = outcomes.capture(func, *args, **kwargs) if _debug: self.log.debug("Execution of external routine done %s" % threading.current_thread()) _waiter.thread_done() waiter = external_waiter() thread = threading.Thread(group=None, target=execute_external, name=func.__name__ + "_thread", args=([func, waiter]), kwargs={}) waiter.thread = thread; self._pending_threads.append(waiter) return waiter def add(self, coroutine): """Add a new coroutine. Just a wrapper around self.schedule which provides some debug and useful error messages in the event of common gotchas. """ if isinstance(coroutine, cocotb.decorators.coroutine): self.log.critical( "Attempt to schedule a coroutine that hasn't started") coroutine.log.error("This is the failing coroutine") self.log.warning( "Did you forget to add parentheses to the @test decorator?") self._test_result = TestError( "Attempt to schedule a coroutine that hasn't started") self._terminate = True return elif not isinstance(coroutine, cocotb.decorators.RunningCoroutine): self.log.critical( "Attempt to add something to the scheduler which isn't a " "coroutine") self.log.warning( "Got: %s (%s)" % (str(type(coroutine)), repr(coroutine))) self.log.warning("Did you use the @coroutine decorator?") self._test_result = TestError( "Attempt to schedule a coroutine that hasn't started") self._terminate = True return if _debug: self.log.debug("Adding new coroutine %s" % coroutine.__name__) self.schedule(coroutine) self.advance() return coroutine def new_test(self, coroutine): self._entrypoint = coroutine def schedule(self, coroutine, trigger=None): """Schedule a coroutine by calling the send method. Args: coroutine (cocotb.decorators.coroutine): The coroutine to schedule. trigger (cocotb.triggers.Trigger): The trigger that caused this coroutine to be scheduled. """ if trigger is None: send_outcome = outcomes.Value(None) else: send_outcome = trigger._outcome if _debug: self.log.debug("Scheduling with {}".format(send_outcome)) try: result = coroutine._advance(send_outcome) if _debug: self.log.debug("Coroutine %s yielded %s (mode %d)" % (coroutine.__name__, str(result), self._mode)) # TestComplete indication is game over, tidy up except TestComplete as test_result: # Tag that close down is needed, save the test_result # for later use in cleanup handler self.log.debug("TestComplete received: %s" % test_result.__class__.__name__) self.finish_test(test_result) return # Normal coroutine completion except cocotb.decorators.CoroutineComplete as exc: if _debug: self.log.debug("Coroutine completed: %s" % str(coroutine)) self.unschedule(coroutine) return # Don't handle the result if we're shutting down if self._terminate: return # convert lists into `First` Waitables. if isinstance(result, list): result = cocotb.triggers.First(*result) # convert waitables into coroutines if isinstance(result, cocotb.triggers.Waitable): result = result._wait() # convert coroutinues into triggers if isinstance(result, cocotb.decorators.RunningCoroutine): if not result.has_started(): self.queue(result) if _debug: self.log.debug("Scheduling nested coroutine: %s" % result.__name__) else: if _debug: self.log.debug("Joining to already running coroutine: %s" % result.__name__) result = result.join() if isinstance(result, Trigger): if _debug: self.log.debug("%s: is instance of Trigger" % result) self._coroutine_yielded(coroutine, result) else: msg = ("Coroutine %s yielded something the scheduler can't handle" % str(coroutine)) msg += ("\nGot type: %s repr: %s str: %s" % (type(result), repr(result), str(result))) msg += "\nDid you forget to decorate with @cocotb.coroutine?" try: raise_error(self, msg) except Exception as e: self.finish_test(e) # We do not return from here until pending threads have completed, but only # from the main thread, this seems like it could be problematic in cases # where a sim might change what this thread is. def unblock_event(ext): @cocotb.coroutine def wrapper(): ext.event.set() yield PythonTrigger() if self._main_thread is threading.current_thread(): for ext in self._pending_threads: ext.thread_start() if _debug: self.log.debug("Blocking from %s on %s" % (threading.current_thread(), ext.thread)) state = ext.thread_wait() if _debug: self.log.debug("Back from wait on self %s with newstate %d" % (threading.current_thread(), state)) if state == external_state.EXITED: self._pending_threads.remove(ext) self._pending_events.append(ext.event) # Handle any newly queued coroutines that need to be scheduled while self._pending_coros: self.add(self._pending_coros.pop(0)) while self._pending_callbacks: self._pending_callbacks.pop(0)() def finish_test(self, test_result): """Cache the test result and set the terminate flag.""" self.log.debug("finish_test called with %s" % (repr(test_result))) if not self._terminate: self._terminate = True self._test_result = test_result self.cleanup() def finish_scheduler(self, test_result): """Directly call into the regression manager and end test once we return the sim will close us so no cleanup is needed. """ self.log.debug("Issue sim closedown result to regression object") cocotb.regression_manager.handle_result(test_result) def cleanup(self): """Clear up all our state. Unprime all pending triggers and kill off any coroutines stop all externals. """ for trigger, waiting in dict(self._trigger2coros).items(): for coro in waiting: if _debug: self.log.debug("Killing %s" % str(coro)) coro.kill() if self._main_thread is not threading.current_thread(): raise Exception("Cleanup() called outside of the main thread") for ext in self._pending_threads: self.log.warn("Waiting for %s to exit", ext.thread)
def write_manually(): yield ReadWrite() # this should overwrite the write written below dut.stream_in_data.setimmediatevalue(2)
def test_readwrite(dut): """ Test that ReadWrite can be waited on """ # gh-759 yield Timer(1) dut.clk <= 1 yield ReadWrite()
if simulator.is_running(): sim_product = simulator.get_simulator_product() print("SIM_PRODUCT IS |" + sim_product + "|") if sim_product == "Verilator": verilator = True UVM_POUND_ZERO_COUNT = 1000 #UVM_NO_WAIT_FOR_NBA = True UVM_NO_WAIT_FOR_NBA = False UVM_AFTER_NBA_WAIT = 10 if hasattr(cocotb, 'SIM_NAME') and getattr(cocotb, 'SIM_NAME') == 'Verilator': UVM_POUND_ZERO_COUNT = 100 rw_event = ReadWrite() async def uvm_wait_for_nba_region(): if verilator is True: await rw_event else: if UVM_NO_WAIT_FOR_NBA is False: await rw_event else: for _ in range(0, UVM_POUND_ZERO_COUNT): await Timer(0) # Returns UVM coreservice def get_cs():
print("uvm-python: Used simulator is |" + sim_product + "|") if sim_product == "Verilator": verilator = True def uvm_has_verilator(): return verilator UVM_POUND_ZERO_COUNT = 1000 UVM_NO_WAIT_FOR_NBA = False if hasattr(cocotb, 'SIM_NAME') and getattr(cocotb, 'SIM_NAME') == 'Verilator': UVM_POUND_ZERO_COUNT = 100 rw_event = ReadWrite() async def uvm_wait_for_nba_region(): """ Task: uvm_wait_for_nba_region Callers of this task will not return until the `ReadWrite` region, thus allowing other processes any number of `NullTrigger`s to settle out before continuing. See `UVMSequencerBase.wait_for_sequences` for example usage. """ if verilator is True: await rw_event else: if UVM_NO_WAIT_FOR_NBA is False:
class Scheduler: """The main scheduler. Here we accept callbacks from the simulator and schedule the appropriate coroutines. A callback fires, causing the :any:`react` method to be called, with the trigger that caused the callback as the first argument. We look up a list of coroutines to schedule (indexed by the trigger) and schedule them in turn. .. attention:: Implementors should not depend on the scheduling order! Some additional management is required since coroutines can return a list of triggers, to be scheduled when any one of the triggers fires. To ensure we don't receive spurious callbacks, we have to un-prime all the other triggers when any one fires. Due to the simulator nuances and fun with delta delays we have the following modes: Normal mode - Callbacks cause coroutines to be scheduled - Any pending writes are cached and do not happen immediately ReadOnly mode - Corresponds to :any:`cbReadOnlySynch` (VPI) or :any:`vhpiCbLastKnownDeltaCycle` (VHPI). In this state we are not allowed to perform writes. Write mode - Corresponds to :any:`cbReadWriteSynch` (VPI) or :c:macro:`vhpiCbEndOfProcesses` (VHPI) In this mode we play back all the cached write updates. We can legally transition from Normal to Write by registering a :class:`~cocotb.triggers.ReadWrite` callback, however usually once a simulator has entered the ReadOnly phase of a given timestep then we must move to a new timestep before performing any writes. The mechanism for moving to a new timestep may not be consistent across simulators and therefore we provide an abstraction to assist with compatibility. Unless a coroutine has explicitly requested to be scheduled in ReadOnly mode (for example wanting to sample the finally settled value after all delta delays) then it can reasonably be expected to be scheduled during "normal mode" i.e. where writes are permitted. """ _MODE_NORMAL = 1 # noqa _MODE_READONLY = 2 # noqa _MODE_WRITE = 3 # noqa _MODE_TERM = 4 # noqa # Singleton events, recycled to avoid spurious object creation _next_time_step = NextTimeStep() _read_write = ReadWrite() _read_only = ReadOnly() _timer1 = Timer(1) def __init__(self): self.log = SimLog("cocotb.scheduler") if _debug: self.log.setLevel(logging.DEBUG) # Use OrderedDict here for deterministic behavior (gh-934) # A dictionary of pending coroutines for each trigger, # indexed by trigger self._trigger2coros = _py_compat.insertion_ordered_dict() # Our main state self._mode = Scheduler._MODE_NORMAL # A dictionary of pending (write_func, args), keyed by handle. Only the last scheduled write # in a timestep is performed, all the rest are discarded in python. self._write_calls = _py_compat.insertion_ordered_dict() self._pending_coros = [] self._pending_triggers = [] self._pending_threads = [] self._pending_events = [ ] # Events we need to call set on once we've unwound self._terminate = False self._test = None self._main_thread = threading.current_thread() self._current_task = None self._is_reacting = False self._write_coro_inst = None self._writes_pending = Event() async def _do_writes(self): """ An internal coroutine that performs pending writes """ while True: await self._writes_pending.wait() if self._mode != Scheduler._MODE_NORMAL: await self._next_time_step await self._read_write while self._write_calls: handle, (func, args) = self._write_calls.popitem() func(*args) self._writes_pending.clear() def _check_termination(self): """ Handle a termination that causes us to move onto the next test. """ if self._terminate: if _debug: self.log.debug("Test terminating, scheduling Timer") if self._write_coro_inst is not None: self._write_coro_inst.kill() self._write_coro_inst = None for t in self._trigger2coros: t.unprime() if self._timer1.primed: self._timer1.unprime() self._timer1.prime(self._test_completed) self._trigger2coros = _py_compat.insertion_ordered_dict() self._terminate = False self._write_calls = _py_compat.insertion_ordered_dict() self._writes_pending.clear() self._mode = Scheduler._MODE_TERM def _test_completed(self, trigger=None): """Called after a test and its cleanup have completed """ if _debug: self.log.debug("begin_test called with trigger: %s" % (str(trigger))) if _profiling: ps = pstats.Stats(_profile).sort_stats('cumulative') ps.dump_stats("test_profile.pstat") ctx = profiling_context() else: ctx = _py_compat.nullcontext() with ctx: self._mode = Scheduler._MODE_NORMAL if trigger is not None: trigger.unprime() # extract the current test, and clear it test = self._test self._test = None if test is None: raise InternalError( "_test_completed called with no active test") if test._outcome is None: raise InternalError( "_test_completed called with an incomplete test") # Issue previous test result if _debug: self.log.debug("Issue test result to regression object") # this may scheduler another test cocotb.regression_manager.handle_result(test) # if it did, make sure we handle the test completing self._check_termination() def react(self, trigger): """ Called when a trigger fires. We ensure that we only start the event loop once, rather than letting it recurse. """ if self._is_reacting: # queue up the trigger, the event loop will get to it self._pending_triggers.append(trigger) return if self._pending_triggers: raise InternalError( "Expected all triggers to be handled but found {}".format( self._pending_triggers)) # start the event loop self._is_reacting = True try: self._event_loop(trigger) finally: self._is_reacting = False def _event_loop(self, trigger): """ Run an event loop triggered by the given trigger. The loop will keep running until no further triggers fire. This should be triggered by only: * The beginning of a test, when there is no trigger to react to * A GPI trigger """ if _profiling: ctx = profiling_context() else: ctx = _py_compat.nullcontext() with ctx: # When a trigger fires it is unprimed internally if _debug: self.log.debug("Trigger fired: %s" % str(trigger)) # trigger.unprime() if self._mode == Scheduler._MODE_TERM: if _debug: self.log.debug( "Ignoring trigger %s since we're terminating" % str(trigger)) return if trigger is self._read_only: self._mode = Scheduler._MODE_READONLY # Only GPI triggers affect the simulator scheduling mode elif isinstance(trigger, GPITrigger): self._mode = Scheduler._MODE_NORMAL # work through triggers one by one is_first = True self._pending_triggers.append(trigger) while self._pending_triggers: trigger = self._pending_triggers.pop(0) if not is_first and isinstance(trigger, GPITrigger): self.log.warning( "A GPI trigger occurred after entering react - this " "should not happen.") assert False # this only exists to enable the warning above is_first = False # Scheduled coroutines may append to our waiting list so the first # thing to do is pop all entries waiting on this trigger. try: scheduling = self._trigger2coros.pop(trigger) except KeyError: # GPI triggers should only be ever pending if there is an # associated coroutine waiting on that trigger, otherwise it would # have been unprimed already if isinstance(trigger, GPITrigger): self.log.critical( "No coroutines waiting on trigger that fired: %s" % str(trigger)) trigger.log.info("I'm the culprit") # For Python triggers this isn't actually an error - we might do # event.set() without knowing whether any coroutines are actually # waiting on this event, for example elif _debug: self.log.debug( "No coroutines waiting on trigger that fired: %s" % str(trigger)) del trigger continue if _debug: debugstr = "\n\t".join( [coro._coro.__qualname__ for coro in scheduling]) if len(scheduling): debugstr = "\n\t" + debugstr self.log.debug("%d pending coroutines for event %s%s" % (len(scheduling), str(trigger), debugstr)) # This trigger isn't needed any more trigger.unprime() for coro in scheduling: if coro._outcome is not None: # coroutine was killed by another coroutine waiting on the same trigger continue if _debug: self.log.debug("Scheduling coroutine %s" % (coro._coro.__qualname__)) self.schedule(coro, trigger=trigger) if _debug: self.log.debug("Scheduled coroutine %s" % (coro._coro.__qualname__)) # Schedule may have queued up some events so we'll burn through those while self._pending_events: if _debug: self.log.debug("Scheduling pending event %s" % (str(self._pending_events[0]))) self._pending_events.pop(0).set() # remove our reference to the objects at the end of each loop, # to try and avoid them being destroyed at a weird time (as # happened in gh-957) del trigger del coro del scheduling # no more pending triggers self._check_termination() if _debug: self.log.debug("All coroutines scheduled, handing control back" " to simulator") def unschedule(self, coro): """Unschedule a coroutine. Unprime any pending triggers""" # Unprime the trigger this coroutine is waiting on trigger = coro._trigger if trigger is not None: coro._trigger = None if coro in self._trigger2coros.setdefault(trigger, []): self._trigger2coros[trigger].remove(coro) if not self._trigger2coros[trigger]: trigger.unprime() del self._trigger2coros[trigger] assert self._test is not None if coro is self._test: if _debug: self.log.debug("Unscheduling test {}".format(coro)) if not self._terminate: self._terminate = True self.cleanup() elif Join(coro) in self._trigger2coros: self.react(Join(coro)) else: try: # throws an error if the background coroutine errored # and no one was monitoring it coro._outcome.get() except (TestComplete, AssertionError) as e: coro.log.info("Test stopped by this forked coroutine") e = remove_traceback_frames(e, ['unschedule', 'get']) self._test.abort(e) except Exception as e: coro.log.error("Exception raised by this forked coroutine") e = remove_traceback_frames(e, ['unschedule', 'get']) self._test.abort(e) def _schedule_write(self, handle, write_func, *args): """ Queue `write_func` to be called on the next ReadWrite trigger. """ if self._mode == Scheduler._MODE_READONLY: raise Exception( "Write to object {0} was scheduled during a read-only sync phase." .format(handle._name)) # TODO: we should be able to better keep track of when this needs to # be scheduled if self._write_coro_inst is None: self._write_coro_inst = self.add(self._do_writes()) self._write_calls[handle] = (write_func, args) self._writes_pending.set() def _resume_coro_upon(self, coro, trigger): """Schedule `coro` to be resumed when `trigger` fires.""" coro._trigger = trigger trigger_coros = self._trigger2coros.setdefault(trigger, []) if coro is self._write_coro_inst: # Our internal write coroutine always runs before any user coroutines. # This preserves the behavior prior to the refactoring of writes to # this coroutine. trigger_coros.insert(0, coro) else: # Everything else joins the back of the queue trigger_coros.append(coro) if not trigger.primed: if trigger_coros != [coro]: # should never happen raise InternalError( "More than one coroutine waiting on an unprimed trigger") try: trigger.prime(self.react) except Exception as e: # discard the trigger we associated, it will never fire self._trigger2coros.pop(trigger) # replace it with a new trigger that throws back the exception self._resume_coro_upon( coro, NullTrigger(name="Trigger.prime() Error", outcome=outcomes.Error(e))) def queue(self, coroutine): """Queue a coroutine for execution""" self._pending_coros.append(coroutine) def queue_function(self, coro): """Queue a coroutine for execution and move the containing thread so that it does not block execution of the main thread any longer. """ # We should be able to find ourselves inside the _pending_threads list matching_threads = [ t for t in self._pending_threads if t.thread == threading.current_thread() ] if len(matching_threads) == 0: raise RuntimeError( "queue_function called from unrecognized thread") # Raises if there is more than one match. This can never happen, since # each entry always has a unique thread. t, = matching_threads async def wrapper(): # This function runs in the scheduler thread try: _outcome = outcomes.Value(await coro) except BaseException as e: _outcome = outcomes.Error(e) event.outcome = _outcome # Notify the current (scheduler) thread that we are about to wake # up the background (`@external`) thread, making sure to do so # before the background thread gets a chance to go back to sleep by # calling thread_suspend. # We need to do this here in the scheduler thread so that no more # coroutines run until the background thread goes back to sleep. t.thread_resume() event.set() event = threading.Event() self._pending_coros.append(cocotb.decorators.RunningTask(wrapper())) # The scheduler thread blocks in `thread_wait`, and is woken when we # call `thread_suspend` - so we need to make sure the coroutine is # queued before that. t.thread_suspend() # This blocks the calling `@external` thread until the coroutine finishes event.wait() return event.outcome.get() def run_in_executor(self, func, *args, **kwargs): """Run the coroutine in a separate execution thread and return an awaitable object for the caller. """ # Create a thread # Create a trigger that is called as a result of the thread finishing # Create an Event object that the caller can await on # Event object set when the thread finishes execution, this blocks the # calling coroutine (but not the thread) until the external completes def execute_external(func, _waiter): _waiter._outcome = outcomes.capture(func, *args, **kwargs) if _debug: self.log.debug("Execution of external routine done %s" % threading.current_thread()) _waiter.thread_done() async def wrapper(): waiter = external_waiter() thread = threading.Thread(group=None, target=execute_external, name=func.__qualname__ + "_thread", args=([func, waiter]), kwargs={}) waiter.thread = thread self._pending_threads.append(waiter) await waiter.event.wait() return waiter.result # raises if there was an exception return wrapper() @staticmethod def create_task(coroutine: Any) -> RunningTask: """ Checks to see if the given object is a schedulable coroutine object and if so, returns it """ if isinstance(coroutine, RunningTask): return coroutine if inspect.iscoroutine(coroutine): return RunningTask(coroutine) if inspect.iscoroutinefunction(coroutine): raise TypeError( "Coroutine function {} should be called prior to being " "scheduled.".format(coroutine)) if isinstance(coroutine, cocotb.decorators.coroutine): raise TypeError( "Attempt to schedule a coroutine that hasn't started: {}.\n" "Did you forget to add parentheses to the @cocotb.test() " "decorator?".format(coroutine)) if sys.version_info >= (3, 6) and inspect.isasyncgen(coroutine): raise TypeError( "{} is an async generator, not a coroutine. " "You likely used the yield keyword instead of await.".format( coroutine.__qualname__)) raise TypeError( "Attempt to add an object of type {} to the scheduler, which " "isn't a coroutine: {!r}\n" "Did you forget to use the @cocotb.coroutine decorator?".format( type(coroutine), coroutine)) def add(self, coroutine: Union[RunningTask, Coroutine]) -> RunningTask: """Add a new coroutine. Just a wrapper around self.schedule which provides some debug and useful error messages in the event of common gotchas. """ task = self.create_task(coroutine) if _debug: self.log.debug("Adding new coroutine %s" % task._coro.__qualname__) self.schedule(task) self._check_termination() return task def start_soon(self, coro: Union[Coroutine, RunningTask]) -> RunningTask: """ Schedule a coroutine to be run concurrently, starting after the current coroutine yields control. In contrast to :func:`~cocotb.fork` which starts the given coroutine immediately, this function starts the given coroutine only after the current coroutine yields control. This is useful when the coroutine to be forked has logic before the first :keyword:`await` that may not be safe to execute immediately. """ task = self.create_task(coro) if _debug: self.log.debug("Queueing a new coroutine %s" % task._coro.__qualname__) self.queue(task) return task def add_test(self, test_coro): """Called by the regression manager to queue the next test""" if self._test is not None: raise InternalError("Test was added while another was in progress") self._test = test_coro self._resume_coro_upon( test_coro, NullTrigger(name="Start {!s}".format(test_coro), outcome=outcomes.Value(None))) # This collection of functions parses a trigger out of the object # that was yielded by a coroutine, converting `list` -> `Waitable`, # `Waitable` -> `RunningTask`, `RunningTask` -> `Trigger`. # Doing them as separate functions allows us to avoid repeating unencessary # `isinstance` checks. def _trigger_from_started_coro( self, result: cocotb.decorators.RunningTask) -> Trigger: if _debug: self.log.debug("Joining to already running coroutine: %s" % result._coro.__qualname__) return result.join() def _trigger_from_unstarted_coro( self, result: cocotb.decorators.RunningTask) -> Trigger: self.queue(result) if _debug: self.log.debug("Scheduling nested coroutine: %s" % result._coro.__qualname__) return result.join() def _trigger_from_waitable(self, result: cocotb.triggers.Waitable) -> Trigger: return self._trigger_from_unstarted_coro( cocotb.decorators.RunningTask(result._wait())) def _trigger_from_list(self, result: list) -> Trigger: return self._trigger_from_waitable(cocotb.triggers.First(*result)) def _trigger_from_any(self, result) -> Trigger: """Convert a yielded object into a Trigger instance""" # note: the order of these can significantly impact performance if isinstance(result, Trigger): return result if isinstance(result, cocotb.decorators.RunningTask): if not result.has_started(): return self._trigger_from_unstarted_coro(result) else: return self._trigger_from_started_coro(result) if inspect.iscoroutine(result): return self._trigger_from_unstarted_coro( cocotb.decorators.RunningTask(result)) if isinstance(result, list): return self._trigger_from_list(result) if isinstance(result, cocotb.triggers.Waitable): return self._trigger_from_waitable(result) if sys.version_info >= (3, 6) and inspect.isasyncgen(result): raise TypeError( "{} is an async generator, not a coroutine. " "You likely used the yield keyword instead of await.".format( result.__qualname__)) raise TypeError( "Coroutine yielded an object of type {}, which the scheduler can't " "handle: {!r}\n" "Did you forget to decorate with @cocotb.coroutine?".format( type(result), result)) @contextmanager def _task_context(self, task): """Context manager for the currently running task.""" old_task = self._current_task self._current_task = task try: yield finally: self._current_task = old_task def schedule(self, coroutine, trigger=None): """Schedule a coroutine by calling the send method. Args: coroutine (cocotb.decorators.coroutine): The coroutine to schedule. trigger (cocotb.triggers.Trigger): The trigger that caused this coroutine to be scheduled. """ with self._task_context(coroutine): if trigger is None: send_outcome = outcomes.Value(None) else: send_outcome = trigger._outcome if _debug: self.log.debug("Scheduling with {}".format(send_outcome)) coro_completed = False try: coroutine._trigger = None result = coroutine._advance(send_outcome) if _debug: self.log.debug("Coroutine %s yielded %s (mode %d)" % (coroutine._coro.__qualname__, str(result), self._mode)) except cocotb.decorators.CoroutineComplete: if _debug: self.log.debug("Coroutine {} completed with {}".format( coroutine, coroutine._outcome)) coro_completed = True # this can't go in the else above, as that causes unwanted exception # chaining if coro_completed: self.unschedule(coroutine) # Don't handle the result if we're shutting down if self._terminate: return if not coro_completed: try: result = self._trigger_from_any(result) except TypeError as exc: # restart this coroutine with an exception object telling it that # it wasn't allowed to yield that result = NullTrigger(outcome=outcomes.Error(exc)) self._resume_coro_upon(coroutine, result) # We do not return from here until pending threads have completed, but only # from the main thread, this seems like it could be problematic in cases # where a sim might change what this thread is. if self._main_thread is threading.current_thread(): for ext in self._pending_threads: ext.thread_start() if _debug: self.log.debug( "Blocking from %s on %s" % (threading.current_thread(), ext.thread)) state = ext.thread_wait() if _debug: self.log.debug( "Back from wait on self %s with newstate %d" % (threading.current_thread(), state)) if state == external_state.EXITED: self._pending_threads.remove(ext) self._pending_events.append(ext.event) # Handle any newly queued coroutines that need to be scheduled while self._pending_coros: self.add(self._pending_coros.pop(0)) def finish_test(self, exc): self._test.abort(exc) self._check_termination() def finish_scheduler(self, exc): """Directly call into the regression manager and end test once we return the sim will close us so no cleanup is needed. """ # If there is an error during cocotb initialization, self._test may not # have been set yet. Don't cause another Python exception here. if self._test: self.log.debug("Issue sim closedown result to regression object") self._test.abort(exc) cocotb.regression_manager.handle_result(self._test) def cleanup(self): """Clear up all our state. Unprime all pending triggers and kill off any coroutines stop all externals. """ # copy since we modify this in kill items = list(self._trigger2coros.items()) # reversing seems to fix gh-928, although the order is still somewhat # arbitrary. for trigger, waiting in items[::-1]: for coro in waiting: if _debug: self.log.debug("Killing %s" % str(coro)) coro.kill() if self._main_thread is not threading.current_thread(): raise Exception("Cleanup() called outside of the main thread") for ext in self._pending_threads: self.log.warning("Waiting for %s to exit", ext.thread)