diff --git a/src/finn/custom_op/fpgadataflow/__init__.py b/src/finn/custom_op/fpgadataflow/__init__.py
index 9af5dfc3b2c8b7dc899520a9c49725fc975351bf..17a55e519ed0440f68e295aecaab179e6adf632f 100644
--- a/src/finn/custom_op/fpgadataflow/__init__.py
+++ b/src/finn/custom_op/fpgadataflow/__init__.py
@@ -40,6 +40,7 @@ from finn.util.basic import (
 from finn.util.fpgadataflow import (
     IPGenBuilder,
     pyverilate_get_liveness_threshold_cycles,
+    rtlsim_multi_io,
 )
 from . import templates
 
@@ -318,16 +319,18 @@ Found no codegen dir for this node, did you run the prepare_cppsim transformatio
             )
 
     def npy_to_dynamic_output(self, context):
-        """Reads the output from a .npy file and saves it at the right place in
-        the context dictionary."""
+        """Reads the output from an output.npy file generated from cppsim and
+        places its content into the context dictionary."""
         node = self.onnx_node
         code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim")
         output = np.load("{}/output.npy".format(code_gen_dir))
         context[node.output[0]] = output
 
     def npy_to_dynamic_outputs(self, context, npy_list):
-        """Reads the output from .npy files and saves it at the right place in
-        the context dictionary."""
+        """Reads the output from .npy files generated from cppsim and places
+        their content into the context dictionary.
+        npy_list is a list specifying which files to read, and its order must
+        match the order of node outputs."""
         node = self.onnx_node
         code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim")
         for i in range(len(npy_list)):
@@ -430,85 +433,14 @@ compilation transformations?
         return outputs
 
     def rtlsim_multi_io(self, sim, io_dict):
-        """Runs the pyverilator simulation by passing the input values to the simulation,
-        toggle the clock and observing the execution time. Function contains also an
-        observation loop that can abort the simulation if no output value is produced
-        after a set number of cycles. Accepts multiple inputs and outputs."""
+        "Run rtlsim for this node, supports multiple i/o streams."
 
         trace_file = self.get_nodeattr("rtlsim_trace")
-        if trace_file != "":
-            if trace_file == "default":
-                trace_file = self.onnx_node.name + ".vcd"
-            sim.start_vcd_trace(trace_file)
-
-        for outp in io_dict["outputs"]:
-            sim.io[outp + "_V_V_TREADY"] = 1
-
-        # observe if output is completely calculated
-        # total_cycle_count will contain the number of cycles the calculation ran
+        if trace_file == "default":
+            trace_file = self.onnx_node.name + ".vcd"
         num_out_values = self.get_number_output_values()
-        output_done = False
-        total_cycle_count = 0
-        output_count = 0
-        old_output_count = 0
-
-        # avoid infinite looping of simulation by aborting when there is no change in
-        # output values after 100 cycles
-        no_change_count = 0
-        liveness_threshold = pyverilate_get_liveness_threshold_cycles()
-
-        while not (output_done):
-            for inp in io_dict["inputs"]:
-                inputs = io_dict["inputs"][inp]
-                sim.io[inp + "_V_V_TVALID"] = 1 if len(inputs) > 0 else 0
-                sim.io[inp + "_V_V_TDATA"] = inputs[0] if len(inputs) > 0 else 0
-                if (
-                    sim.io[inp + "_V_V_TREADY"] == 1
-                    and sim.io[inp + "_V_V_TVALID"] == 1
-                ):
-                    inputs = inputs[1:]
-                io_dict["inputs"][inp] = inputs
-
-            for outp in io_dict["outputs"]:
-                outputs = io_dict["outputs"][outp]
-                if (
-                    sim.io[outp + "_V_V_TVALID"] == 1
-                    and sim.io[outp + "_V_V_TREADY"] == 1
-                ):
-                    outputs = outputs + [sim.io[outp + "_V_V_TDATA"]]
-                    output_count += 1
-                io_dict["outputs"][outp] = outputs
-
-            sim.io.ap_clk = 1
-            sim.io.ap_clk = 0
-
-            total_cycle_count = total_cycle_count + 1
-
-            if output_count == old_output_count:
-                no_change_count = no_change_count + 1
-            else:
-                no_change_count = 0
-                old_output_count = output_count
-
-            # check if all expected output words received
-            if output_count == num_out_values:
-                self.set_nodeattr("sim_cycles", total_cycle_count)
-                output_done = True
-
-            # end sim on timeout
-            if no_change_count == liveness_threshold:
-                if trace_file != "":
-                    sim.flush_vcd_trace()
-                    sim.stop_vcd_trace()
-                raise Exception(
-                    "Error in simulation! Takes too long to produce output. "
-                    "Consider setting the LIVENESS_THRESHOLD env.var. to a "
-                    "larger value."
-                )
-
-        if trace_file != "":
-            sim.flush_vcd_trace()
-            sim.stop_vcd_trace()
+        total_cycle_count = rtlsim_multi_io(sim, io_dict, num_out_values, trace_file)
+        self.set_nodeattr("sim_cycles", total_cycle_count)
 
     def execute_node(self, context, graph):
         """Executes single node using cppsim or rtlsim."""