diff --git a/include/ccalix/ccalix.h b/include/ccalix/ccalix.h
index 9a9d3131f77fd79ef84dda5fed9eafd7c9f0639d..7f8d061934d2d7e42c6f970d532946036888e406 100644
--- a/include/ccalix/ccalix.h
+++ b/include/ccalix/ccalix.h
@@ -1,4 +1,5 @@
 #include "ccalix/genpybind.h"
+#include "ccalix/hagen/multiplication.h"
 #include "ccalix/helpers.h"
 
 GENPYBIND_MANUAL({
diff --git a/include/ccalix/hagen/multiplication.h b/include/ccalix/hagen/multiplication.h
new file mode 100644
index 0000000000000000000000000000000000000000..d1019a62ae8c607448e0dc00276a9a04e36c733f
--- /dev/null
+++ b/include/ccalix/hagen/multiplication.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#include "ccalix/genpybind.h"
+#include "halco/hicann-dls/vx/v3/synapse_driver.h"
+#include "halco/hicann-dls/vx/v3/synram.h"
+#include "haldls/vx/v3/event.h"
+#include "haldls/vx/v3/synapse.h"
+#include "stadls/vx/v3/playback_program_builder.h"
+#include <pybind11/numpy.h>
+
+
+namespace ccalix GENPYBIND_TAG_CCALIX {
+namespace hagen GENPYBIND_MODULE {
+namespace multiplication {
+
+/**
+ * Generate events for the given vector in hagen mode.
+ *
+ * @param builder Builder to append writes to
+ * @param vector Array containing the input vector
+ * @param num_sends Number of repeats of the whole vector
+ * @param wait_period Wait time between two successive events
+ * @param synram_coord Coordinate of synapse array to target with the events
+ * @param synram_selection_bit Determines which bit in the event label selects the synram
+ */
+SYMBOL_VISIBLE GENPYBIND(visible) void send_vectors(
+    stadls::vx::v3::PlaybackProgramBuilder& builder,
+    const pybind11::array_t<uint_fast16_t>& vector,
+    const size_t num_sends,
+    const size_t wait_period,
+    const halco::hicann_dls::vx::v3::SynramOnDLS synram_coord,
+    const size_t synram_selection_bit);
+
+namespace detail {
+/**
+ * Return a spike pack to chip, containing an event reaching the desired
+ * synapse driver on the desired synram.
+ *
+ * @param address Address that is sent to the driver. The MSB reaches the synapses, the lower 5 bit
+ * encode the desired activation.
+ * @param target_driver Coordinate of target synapse driver.
+ * @param synram_coord Coordinate of target synapse array.
+ * @param synram_selection_bit Bit position that selects synapse array.
+ *
+ * @return Spike packet to chip.
+ */
+haldls::vx::v3::SpikePack1ToChip prepare_event(
+    const haldls::vx::v3::SynapseQuad::Label address,
+    const halco::hicann_dls::vx::v3::SynapseDriverOnSynapseDriverBlock target_driver,
+    const halco::hicann_dls::vx::v3::SynramOnDLS synram_coord,
+    const size_t synram_selection_bit);
+} // namespace detail
+
+} // namespace multiplication
+} // namespace hagen
+} // namespace ccalix
diff --git a/src/cc/ccalix/hagen/multiplication.cpp b/src/cc/ccalix/hagen/multiplication.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..318d5f0d0b8397c351ef48d643caa27777953658
--- /dev/null
+++ b/src/cc/ccalix/hagen/multiplication.cpp
@@ -0,0 +1,98 @@
+#include "ccalix/hagen/multiplication.h"
+
+#include "halco/hicann-dls/vx/v3/timing.h"
+#include "haldls/vx/v3/timer.h"
+#include <sstream>
+#include <stdexcept>
+#include <pybind11/numpy.h>
+
+
+namespace ccalix::hagen::multiplication {
+
+namespace detail {
+haldls::vx::v3::SpikePack1ToChip prepare_event(
+    const haldls::vx::v3::SynapseQuad::Label address,
+    const halco::hicann_dls::vx::v3::SynapseDriverOnSynapseDriverBlock target_driver,
+    const halco::hicann_dls::vx::v3::SynramOnDLS synram_coord,
+    const size_t synram_selection_bit)
+{
+	halco::hicann_dls::vx::v3::SpikeLabel label;
+
+	// select target PADI bus
+	label.set_spl1_address(
+	    halco::hicann_dls::vx::SPL1Address(target_driver.toPADIBusOnPADIBusBlock()));
+
+	// select target synram
+	label.set_neuron_label(
+	    halco::hicann_dls::vx::NeuronLabel(synram_coord << synram_selection_bit));
+
+	// select target driver on the PADI bus
+	label.set_row_select_address(
+	    halco::hicann_dls::vx::PADIRowSelectAddress(target_driver.toSynapseDriverOnPADIBus()));
+
+	// set address sent to the driver (MSB + hagen activation)
+	label.set_synapse_label(address);
+
+	haldls::vx::v3::SpikePack1ToChip::labels_type labels;
+	labels[0] = label;
+	return haldls::vx::v3::SpikePack1ToChip(labels);
+}
+} // namespace detail
+
+void send_vectors(
+    stadls::vx::v3::PlaybackProgramBuilder& builder,
+    const pybind11::array_t<uint_fast16_t>& vector,
+    const size_t num_sends,
+    const size_t wait_period,
+    const halco::hicann_dls::vx::v3::SynramOnDLS synram_coord,
+    const size_t synram_selection_bit)
+{
+	if (vector.size() != halco::hicann_dls::vx::v3::SynapseRowOnSynram::size) {
+		std::stringstream ss;
+		ss << "Length of vector (" << vector.size()
+		   << ") does not match number of synapse rows in hemisphere ("
+		   << halco::hicann_dls::vx::v3::SynapseRowOnSynram::size << ").";
+		throw std::runtime_error(ss.str());
+	}
+
+	const size_t num_loops = (num_sends > 1 and wait_period <= 1) ? 1 : num_sends;
+	const size_t num_copies = (num_sends > 1 and wait_period <= 1) ? num_sends : 1;
+
+	size_t entry_counter = 0;
+	builder.write(halco::hicann_dls::vx::v3::TimerOnDLS(), haldls::vx::v3::Timer());
+
+	stadls::vx::v3::PlaybackProgramBuilder vector_builder;
+	for (size_t i = 0; i < num_loops; ++i) {
+		for (size_t row = 0; row < halco::hicann_dls::vx::v3::SynapseRowOnSynram::size; ++row) {
+			uint_fast16_t entry = vector.at(row);
+			if (entry == 0)
+				continue;
+
+			// send event on a different address in order to
+			// select one of the two rows connected to a driver
+			if ((row % halco::hicann_dls::vx::v3::SynapseRowOnSynapseDriver::size) == 0)
+				entry += 32;
+
+			vector_builder.write(
+			    halco::hicann_dls::vx::v3::SpikePack1ToChipOnDLS(),
+			    detail::prepare_event(
+			        halco::hicann_dls::vx::v3::SynapseLabel(entry),
+			        halco::hicann_dls::vx::v3::SynapseDriverOnSynapseDriverBlock(
+			            row / halco::hicann_dls::vx::v3::SynapseRowOnSynapseDriver::size),
+			        synram_coord, synram_selection_bit));
+
+			// wait only if needed:
+			if (wait_period > 1) {
+				vector_builder.block_until(
+				    halco::hicann_dls::vx::v3::TimerOnDLS(),
+				    haldls::vx::v3::Timer::Value(wait_period * entry_counter));
+			}
+			entry_counter += 1;
+		}
+	}
+
+	for (size_t i = 0; i < num_copies; ++i)
+		builder.copy_back(vector_builder);
+}
+
+} // namespace ccalix::hagen::multiplication
diff --git a/src/py/calix/hagen/multiplication.py b/src/py/calix/hagen/multiplication.py
index 722433ba064471577970ec5b2694454f5146913a..6d89db66657bcc08c6a6cb60faed76f7e9d032d9 100644
--- a/src/py/calix/hagen/multiplication.py
+++ b/src/py/calix/hagen/multiplication.py
@@ -10,6 +10,8 @@ import quantities as pq
 
 from dlens_vx_v3 import lola, halco, hal, sta, logger, hxcomm
 
+import pyccalix
+
 from calix.common import base, helpers
 from calix.hagen import neuron_helpers
 from calix import constants
@@ -291,41 +293,6 @@ class Multiplication:
 
         return synapses
 
-    def prepare_event(
-            self, address: hal.SynapseQuad.Label,
-            target_driver: halco.SynapseDriverOnSynapseDriverBlock
-    ) -> hal.SpikePack1ToChip:
-        """
-        Return a spike pack to chip, containing an event reaching
-        the desired synapse driver on the synram in use.
-
-        :param address: Address that is sent to the driver. The
-            MSB reaches the synapses, the lower 5 bit encode the desired
-            activation.
-        :param target_driver: Coordinate of the targeted driver.
-
-        :return: Spike packet to chip.
-        """
-
-        label = halco.SpikeLabel()
-
-        # select target PADI bus
-        label.spl1_address = int(
-            target_driver.toPADIBusOnPADIBusBlock().toEnum())
-
-        # select target synram
-        label.neuron_label = (int(
-            self._synram_coord.toEnum()) << self.synram_selection_bit)
-
-        # select target driver on the PADI bus
-        label.row_select_address = int(
-            target_driver.toSynapseDriverOnPADIBus().toEnum())
-
-        # set address sent to the driver (MSB + hagen activation)
-        label.synapse_label = address
-
-        return hal.SpikePack1ToChip([label])
-
     def reset_synin(self,
                     builder: sta.PlaybackProgramBuilder):
         """
@@ -402,42 +369,11 @@ class Multiplication:
             builder.copy_back(self.cached_reset_synin)
             helpers.wait(builder, 10 * pq.us)
 
-            # Optimize runtime if wait_period is 1 and multiple sends
-            # are requested by copying a vector_builder for each send
-            if self.num_sends > 1 and self.wait_period <= 1:  # pylint: disable=chained-comparison
-                num_loops = 1
-                num_copies = self.num_sends
-            else:
-                num_loops = self.num_sends
-                num_copies = 1
-
-            entry_counter = 0
-            builder.write(halco.TimerOnDLS(), hal.Timer())
-
-            vector_builder = sta.PlaybackProgramBuilder()
-            for _ in range(num_loops):
-                for row, entry in enumerate(vector):
-                    if entry == 0:
-                        continue
-
-                    # send event on a different address in order to
-                    # select one of the two rows connected to a driver
-                    entry += 32 if row % 2 == 1 else 0
-                    vector_builder.write(
-                        halco.SpikePack1ToChipOnDLS(),
-                        self.prepare_event(
-                            halco.SynapseLabel(entry),
-                            halco.SynapseDriverOnSynapseDriverBlock(row // 2)))
-
-                    # wait only if needed:
-                    if self.wait_period > 1:
-                        vector_builder.block_until(
-                            halco.TimerOnDLS(), hal.Timer.Value(
-                                int(self.wait_period * entry_counter)))
-                    entry_counter += 1
-
-            for _ in range(num_copies):
-                builder.copy_back(vector_builder)
+            pyccalix.hagen.send_vectors(
+                builder, vector, num_sends=self.num_sends,
+                wait_period=self.wait_period,
+                synram_coord=self.synram_coord,
+                synram_selection_bit=self.synram_selection_bit)
 
             # Read amplitudes
             coord = halco.CADCSampleRowOnDLS(