diff --git a/applications/rdma_demo/libraries/rdma_packetiser/cocotb/dp_bus.py b/applications/rdma_demo/libraries/rdma_packetiser/cocotb/dp_bus.py
new file mode 100644
index 0000000000000000000000000000000000000000..1b627f866b1bbe80e0327711b30e040ba8021fe3
--- /dev/null
+++ b/applications/rdma_demo/libraries/rdma_packetiser/cocotb/dp_bus.py
@@ -0,0 +1,354 @@
+
+from typing import Iterable, Union, Optional
+
+import cocotb
+from cocotb.utils import hexdump
+from cocotb.triggers import RisingEdge, ReadOnly
+from cocotb.binary import BinaryValue
+from cocotb.result import TestError
+from cocotb_bus.drivers import ValidatedBusDriver
+from cocotb_bus.monitors import BusMonitor
+
+
+class SosiDriver(ValidatedBusDriver):
+    _optional_signals = ['valid', 'sop', 'eop', 'sync', 'bsn', 'data', 're', 'im', 'empty', 'channel', 'err']
+    _signals = []
+    _default_config = {
+        "dataBitsPerSymbol"             : 8,
+        "firstSymbolInHighOrderBits"    : True,
+        "maxChannel"                    : 1,
+        "readyLatency"                  : 1
+    }
+    def __init__(self, dut, name, clk, data_w = None):
+        ValidatedBusDriver.__init__(self, dut, name, clk, bus_separator='.')    
+        self.config = self._default_config.copy()
+        self.use_empty = True
+        self.config["useEmpty"] = self.use_empty
+        if data_w == None:
+            self.data_w = len(self.bus.data)
+        else:
+            self.data_w = data_w
+
+    async def _wait_ready(self):
+        """Wait for a ready cycle on the bus before continuing.
+
+            Can no longer drive values this cycle...
+
+            FIXME assumes readyLatency of 0
+        """
+        await ReadOnly()
+        while False: #not self.bus.ready.value:
+            await RisingEdge(self.clock)
+            await ReadOnly()
+
+    async def _send_string(self, string: bytes, sync: bool = True, channel: Optional[int] = None) -> None:
+        """Args:
+            string: A string of bytes to send over the bus.
+            channel: Channel to send the data on.
+        """
+        # Avoid spurious object creation by recycling
+        clkedge = RisingEdge(self.clock)
+        firstword = True
+
+        # FIXME: buses that aren't an integer numbers of bytes
+        bus_width = int(self.data_w / 8)
+        
+        word = BinaryValue(n_bits=len(self.bus.data),
+                           bigEndian=self.config["firstSymbolInHighOrderBits"])
+
+        single = BinaryValue(n_bits=1, bigEndian=False)
+        if self.use_empty:
+            empty = BinaryValue(n_bits=len(self.bus.empty), bigEndian=False)
+
+        # Drive some defaults since we don't know what state we're in
+        if self.use_empty:
+            self.bus.empty.value = 0
+        self.bus.sop.value = 0
+        self.bus.eop.value = 0
+        self.bus.valid.value = 0
+        if hasattr(self.bus, 'err'):
+            self.bus.err.value = 0
+
+        if hasattr(self.bus, 'channel'):
+            self.bus.channel.value = 0
+        elif channel is not None:
+            raise TestError("%s does not have a channel signal" % self.name)
+
+        while string:
+            if not firstword or (firstword and sync):
+                await clkedge
+
+            # Insert a gap where valid is low
+            if not self.on:
+                self.bus.valid.value = 0
+                for _ in range(self.off):
+                    await clkedge
+
+                # Grab the next set of on/off values
+                self._next_valids()
+
+            # Consume a valid cycle
+            if self.on is not True and self.on:
+                self.on -= 1
+
+            self.bus.valid.value = 1
+            if hasattr(self.bus, 'channel'):
+                if channel is None:
+                    self.bus.channel.value = 0
+                elif channel > self.config['maxChannel'] or channel < 0:
+                    raise TestError("%s: Channel value %d is outside range 0-%d" %
+                                    (self.name, channel, self.config['maxChannel']))
+                else:
+                    self.bus.channel.value = channel
+
+            if firstword:
+                self.bus.sop.value = 1
+                firstword = False
+            else:
+                self.bus.sop.value = 0
+
+            nbytes = min(len(string), bus_width)
+            data = string[:nbytes]
+            
+            # set unused bits of dp_sosi.data field to 0
+            data = bytes(word.n_bits // 8 - bus_width) + data        
+            word.buff = data
+
+            if len(string) <= bus_width:
+                self.bus.eop.value = 1
+                if self.use_empty:
+                    self.bus.empty.value = bus_width - len(string)
+                string = b""
+            else:
+                string = string[bus_width:]
+
+            self.bus.data.value = word
+
+            # If this is a bus with a ready signal, wait for this word to
+            # be acknowledged
+            if hasattr(self.bus, "ready"):
+                await self._wait_ready()
+
+        await clkedge
+        self.bus.valid.value = 0
+        self.bus.eop.value = 0
+        word.binstr   = "x" * self.data_w
+        single.binstr = "x"
+        self.bus.data.value = word
+        self.bus.sop.value = single
+        self.bus.eop.value = single
+
+        if self.use_empty:
+            empty.binstr = "x" * len(self.bus.empty)
+            self.bus.empty.value = empty
+        if hasattr(self.bus, 'channel'):
+            channel_value = BinaryValue(n_bits=len(self.bus.channel), bigEndian=False,
+                                        value="x" * len(self.bus.channel))
+            self.bus.channel.value = channel_value
+
+    async def _send_iterable(self, pkt: Iterable, sync: bool = True) -> None:
+        """Args:
+            pkt: Will yield objects with attributes matching the
+                signal names for each individual bus cycle.
+        """
+        clkedge = RisingEdge(self.clock)
+        firstword = True
+
+        for word in pkt:
+            if not firstword or (firstword and sync):
+                await clkedge
+
+            firstword = False
+
+            # Insert a gap where valid is low
+            if not self.on:
+                self.bus.valid.value = 0
+                for _ in range(self.off):
+                    await clkedge
+
+                # Grab the next set of on/off values
+                self._next_valids()
+
+            # Consume a valid cycle
+            if self.on is not True and self.on:
+                self.on -= 1
+
+            if not hasattr(word, "valid"):
+                self.bus.valid.value = 1
+            else:
+                self.bus.value = word
+
+            # Wait for valid words to be acknowledged
+            if not hasattr(word, "valid") or word.valid:
+                if hasattr(self.bus, "ready"):
+                    await self._wait_ready()
+
+        await clkedge
+        self.bus.valid.value = 0
+
+    async def _driver_send(self, pkt: Union[bytes, Iterable], sync: bool = True, channel: Optional[int] = None):
+        """Send a packet over the bus.
+
+        Args:
+            pkt: Packet to drive onto the bus.
+            channel: Channel attributed to the packet.
+
+        If ``pkt`` is a string, we simply send it word by word
+
+        If ``pkt`` is an iterable, it's assumed to yield objects with
+        attributes matching the signal names.
+        """
+
+        # Avoid spurious object creation by recycling
+        if isinstance(pkt, bytes):
+            self.log.debug("Sending packet of length %d bytes", len(pkt))
+            self.log.debug(hexdump(pkt))
+            await self._send_string(pkt, sync=sync, channel=channel)
+            self.log.debug("Successfully sent packet of length %d bytes", len(pkt))
+        elif isinstance(pkt, str):
+            raise TypeError("pkt must be a bytestring, not a unicode string")
+        else:
+            if channel is not None:
+                self.log.warning("%s is ignoring channel=%d because pkt is an iterable", self.name, channel)
+            await self._send_iterable(pkt, sync=sync)
+
+class SisoDriver(ValidatedBusDriver):
+    _optional_signals = ['ready', 'xon']
+    _signals = []
+    def __init__(self, dut, name, clk):
+        ValidatedBusDriver.__init__(self, dut, name, clk, bus_separator='.')        
+
+class SosiMonitor(BusMonitor):
+    _optional_signals = ['valid', 'sop', 'eop', 'sync', 'bsn', 'data', 're', 'im', 'empty', 'channel', 'err']
+    _signals = []
+
+    _default_config = {
+        "dataBitsPerSymbol"             : 8,
+        "firstSymbolInHighOrderBits"    : True,
+        "maxChannel"                    : 1,
+        "readyLatency"                  : 1,
+        "invalidTimeout"                : 0,
+    }
+    def __init__(self, dut, name, clk, data_w=None, reset=None, reset_n=None, report_channel=False):
+        BusMonitor.__init__(self, dut, name, clk, reset=reset, reset_n=reset_n, bus_separator='.')
+        self.config = self._default_config.copy()
+        self.use_empty = True
+        self.config["useEmpty"] = self.use_empty
+        self.report_channel = report_channel
+        self.data_w = data_w
+    async def _monitor_recv(self):
+        """Watch the pins and reconstruct transactions."""
+
+        # Avoid spurious object creation by recycling
+        clkedge = RisingEdge(self.clock)
+        pkt = b""
+        in_pkt = False
+        invalid_cyclecount = 0
+        channel = None
+
+        def valid():
+            if hasattr(self.bus, 'ready'):
+                return self.bus.valid.value and self.bus.ready.value
+            return self.bus.valid.value
+
+        while True:
+            await clkedge
+
+            if self.in_reset:
+                continue
+
+            if valid():
+                invalid_cyclecount = 0
+
+                if self.bus.sop.value:
+                    if pkt:
+                        raise Exception("Duplicate start-of-packet received on %s" %
+                                                  str(self.bus.sop))
+                    pkt = b""
+                    in_pkt = True
+
+                if not in_pkt:
+                    raise Exception("Data transfer outside of "
+                                              "packet")
+
+                # Handle empty and X's in empty / data
+                vec = BinaryValue()
+                if not self.bus.eop.value:
+                    value = self.bus.data.value.get_binstr()[-1*self.data_w:]
+                    vec.assign(value)
+                else:
+                    value = self.bus.data.value.get_binstr()[-1*self.data_w:]
+                    if self.config["useEmpty"] and self.bus.empty.value.integer:
+                        empty = self.bus.empty.value.integer * self.config["dataBitsPerSymbol"]
+                        if self.config["firstSymbolInHighOrderBits"]:
+                            value = value[:-empty]
+                        else:
+                            value = value[empty:]
+
+                    vec.assign(value)
+                    if not vec.is_resolvable:
+                        raise Exception("After empty masking value is still bad?  "
+                                                  "Had empty {:d}, got value {:s}".format(empty,
+                                                                                          self.bus.data.value.get_binstr()))
+                vec.big_endian = self.config['firstSymbolInHighOrderBits']
+                pkt += vec.buff
+
+                if hasattr(self.bus, 'channel'):
+                    if channel is None:
+                        channel = self.bus.channel.value.integer
+                        if channel > self.config["maxChannel"]:
+                            raise Exception("Channel value (%d) is greater than maxChannel (%d)" %
+                                                      (channel, self.config["maxChannel"]))
+                    elif self.bus.channel.value.integer != channel:
+                        raise Exception("Channel value changed during packet")
+
+                if self.bus.eop.value:
+                    self.log.info("Received a packet of %d bytes", len(pkt))
+                    self.log.debug(hexdump(pkt))
+                    self.channel = channel
+                    if self.report_channel:
+                        self._recv({"data": pkt, "channel": channel})
+                    else:
+                        self._recv(pkt)
+                    pkt = b""
+                    in_pkt = False
+                    channel = None
+            else:
+                if in_pkt:
+                    invalid_cyclecount += 1
+                    if self.config["invalidTimeout"]:
+                        if invalid_cyclecount >= self.config["invalidTimeout"]:
+                            raise Exception(
+                                "In-Packet Timeout. Didn't receive any valid data for %d cycles!" %
+                                invalid_cyclecount)
+
+class SisoMonitor(BusMonitor):
+    _optional_signals = ['ready', 'xon']
+    _signals = []
+    def __init__(self, dut, name, clk, reset=None, reset_n=None):
+        BusMonitor.__init__(self, dut, name, clk, reset=reset, reset_n=reset_n, bus_separator='.')
+        self.clk = clk
+
+    async def _monitor_recv(self):
+        """Watch the pins and reconstruct transactions."""
+
+        # Avoid spurious object creation by recycling
+        clkedge = RisingEdge(self.clock)
+
+        # NB could await on valid here more efficiently?
+        while True:
+            await clkedge            
+            self._recv({"ready": self.bus.ready.value, "xon": self.bus.xon.value})        
+
+class DpStream:
+    # at the time, cocotb_bus does not officially support VHDL record type bus. 
+    # It does work in the following way:
+    # 1. Only use _optional_signals, no _signals.
+    # 2. Use bus_separator='.'
+    def __init__(self, dut, sosi_name, siso_name, clk, rst, data_w = None):
+        self.sosi_drv = SosiDriver(dut, sosi_name, clk, data_w)
+        self.siso_drv = SisoDriver(dut, siso_name, clk)
+        self.sosi_mon = SosiMonitor(dut, sosi_name, clk, data_w, reset=rst)
+        self.siso_mon = SisoMonitor(dut, siso_name, clk, reset=rst)
+        cocotb.start_soon(self.siso_mon._monitor_recv())
+
diff --git a/applications/rdma_demo/libraries/rdma_packetiser/cocotb/mm_bus.py b/applications/rdma_demo/libraries/rdma_packetiser/cocotb/mm_bus.py
new file mode 100644
index 0000000000000000000000000000000000000000..a25d6c79d4a83e0d05555f79b18a5ae1eb6cff0e
--- /dev/null
+++ b/applications/rdma_demo/libraries/rdma_packetiser/cocotb/mm_bus.py
@@ -0,0 +1,183 @@
+
+from typing import Iterable, Union, Optional
+import cocotb.binary
+from cocotb.log import SimLog
+from cocotb.utils import hexdump
+from cocotb.triggers import RisingEdge, ReadOnly
+from cocotb.binary import BinaryValue
+from cocotb.result import TestError
+from cocotb_bus.drivers import BusDriver
+from cocotb.decorators import coroutine
+
+class CopiDriver(BusDriver):
+    _signals = []
+    _optional_signals = ["rd", "wr", "wrdata", "address"]
+
+    def __init__(self, entity, name, clock, **kwargs):
+        BusDriver.__init__(self, entity, name, clock, **kwargs)
+        self._can_read = False
+        self._can_write = False
+
+        # Drive some sensible defaults (setimmediatevalue to avoid x asserts)
+        if hasattr(self.bus, "rd"):
+            self.bus.rd.setimmediatevalue(0)
+            self._can_read = True
+
+        if hasattr(self.bus, "wr"):
+            self.bus.wr.setimmediatevalue(0)
+            v = self.bus.wrdata.value
+            v.binstr = "x" * len(self.bus.wrdata)
+            self.bus.wrdata.value = v
+            self._can_write = True
+
+        if hasattr(self.bus, "address"):
+            v = self.bus.address.value
+            v.binstr = "x" * len(self.bus.address)
+            self.bus.address.setimmediatevalue(v)
+
+    def read(self, address):
+        pass
+
+    def write(self, address, value):
+        pass
+        
+
+class CipoDriver(BusDriver):
+    _signals = []
+    _optional_signals = ["rddata", "waitrequest","rdval"]
+
+    def __init__(self, entity, name, clock, **kwargs):
+        BusDriver.__init__(self, entity, name, clock, **kwargs)
+        self._can_read = True
+        self._can_write = False
+
+        # Drive some sensible defaults (setimmediatevalue to avoid x asserts)
+        if hasattr(self.bus, "rdval"):
+            self.bus.rdval.setimmediatevalue(0)            
+
+        if hasattr(self.bus, "rddata"):            
+            v = self.bus.rddata.value
+            v.binstr = "x" * len(self.bus.rddata)
+            self.bus.rddata.value = v            
+
+        if hasattr(self.bus, "waitrequest"):
+            self.bus.waitrequest.setimmediatevalue(0)
+
+    def read(self, address):
+        pass
+
+    def write(self, address, value):
+        pass
+
+class MMController():
+    """Memory Mapped Interface (MM) Controller."""
+
+    def __init__(self, entity, copi_name, cipo_name, clock):        
+        self._log = SimLog("cocotb.%s.%s" % (entity._name, copi_name))
+        self._log.debug("Memory Mapped Controller created")
+        self.copi = CopiDriver(entity, copi_name, clock, bus_separator='.')
+        self.cipo = CipoDriver(entity, cipo_name, clock, bus_separator='.')
+        self.clock = clock
+
+    def __len__(self):
+        return 2**len(self.copi.bus.address)
+
+    @coroutine
+    async def read(self, address: int, sync: bool = True) -> BinaryValue:
+        """Issue a request to the bus and block until this comes back.
+
+        Simulation time still progresses
+        but syntactically it blocks.
+
+        Args:
+            address: The address to read from.
+            sync: Wait for rising edge on clock initially.
+                Defaults to True.
+
+        Returns:
+            The read data value.
+
+        Raises:
+            :any:`TestError`: If controller is write-only.
+        """
+        if not self.copi._can_read:
+            self._log.error("Cannot read - have no read signal")
+            raise TestError("Attempt to read on a write-only MM Controller")
+
+        await self.copi._acquire_lock()
+
+        # Apply values for next clock edge
+        if sync:
+            await RisingEdge(self.clock)
+        self.copi.bus.address.value = address
+        self.copi.bus.rd.value = 1
+
+
+        # Wait for waitrequest to be low
+        if hasattr(self.cipo.bus, "waitrequest"):
+            await self.cipo._wait_for_nsignal(self.cipo.bus.waitrequest)
+        await RisingEdge(self.clock)
+
+        # Deassert read
+        self.copi.bus.rd.value = 0                
+        #v = self.copi.bus.address.value
+        #v.binstr = "x" * len(self.copi.bus.address)
+        #self.copi.bus.address.value = v
+
+        if hasattr(self.cipo.bus, "rdval"):
+            while True:
+                await ReadOnly()
+                if int(self.cipo.bus.rdval):
+                    break
+                await RisingEdge(self.clock)
+        else:
+            # Assume readLatency = 1 if no readdatavalid
+            # FIXME need to configure this,
+            # should take a dictionary of Avalon properties.
+            await ReadOnly()
+
+        # Get the data
+        data = self.cipo.bus.rddata.value
+        
+        self.copi._release_lock()
+        return data
+
+    @coroutine
+    async def write(self, address: int, value: int) -> None:
+        """Issue a write to the given address with the specified
+        value.
+
+        Args:
+            address: The address to write to.
+            value: The data value to write.
+
+        Raises:
+            :any:`TestError`: If controller is read-only.
+        """
+        if not self.copi._can_write:
+            self._log.error("Cannot write - have no write signal")
+            raise TestError("Attempt to write on a read-only MM Controller")
+
+        await self.copi._acquire_lock()
+
+        # Apply values to bus
+        await RisingEdge(self.clock)
+        self.copi.bus.address.value = address
+        self.copi.bus.wrdata.value = value
+        self.copi.bus.wr.value = 1
+        
+        # Wait for waitrequest to be low
+        if hasattr(self.cipo.bus, "waitrequest"):
+            await self.cipo._wait_for_nsignal(self.cipo.bus.waitrequest)
+
+        # Deassert write
+        await RisingEdge(self.clock)
+        self.copi.bus.wr.value = 0        
+        #v = self.copi.bus.address.value
+        #v.binstr = "x" * len(self.copi.bus.address)
+        #self.copi.bus.address.value = v
+
+        #v = self.copi.bus.wrdata.value
+        #v.binstr = "x" * len(self.copi.bus.wrdata)
+        #self.copi.bus.wrdata.value = v
+        self.copi._release_lock()
diff --git a/applications/rdma_demo/libraries/rdma_packetiser/cocotb/tb_rdma_packetiser.py b/applications/rdma_demo/libraries/rdma_packetiser/cocotb/tb_rdma_packetiser.py
new file mode 100644
index 0000000000000000000000000000000000000000..a92c8b4d271c89df17c726f4db266612d17a556a
--- /dev/null
+++ b/applications/rdma_demo/libraries/rdma_packetiser/cocotb/tb_rdma_packetiser.py
@@ -0,0 +1,156 @@
+import cocotb
+from cocotb.utils import hexdump
+from cocotb.triggers import FallingEdge, Timer, ReadOnly
+from cocotb.clock import Clock
+from cocotb.binary import BinaryValue
+
+from dp_bus import DpStream
+from mm_bus import MMController
+
+
+
+async def perform_rst(rst, clk, cycles):    
+    rst.value = 1
+    await Timer(cycles * clk.period)
+    rst.value = 0
+
+async def read_mm_dict(mm: MMController, read_dict: dict) -> dict:
+    # remove all write-only entries
+    d = {k: v for k, v in read_dict.items() if 'r' in v['access'].lower()}
+    return_dict = {}
+    for k, v in d.items():
+        os = v['offset']        
+        rddata = [(await mm.read(addr)).binstr for addr in range(os, os + v['size'])]
+        # concat to 32 bits
+        rddata = [x[-32:] for x in rddata]
+        # reverse data order to match the write order
+        rddata = [x for x in rddata[::-1]]
+        rddata = BinaryValue(''.join(rddata))
+        return_dict[k] = rddata
+    return return_dict
+
+async def write_mm_dict(mm: MMController, write_dict: dict) -> None:    
+    # remove all read-only entries
+    d = {k: v for k, v in write_dict.items() if 'w' in v['access'].lower()}
+    for k, v in d.items():
+        # get data to write from dictionary  
+        os = v['offset']
+        data_w = 32
+        n_bits = v['size'] * data_w
+        val = BinaryValue(v['value'], n_bits, False).binstr
+        wrdata = [BinaryValue(val[i:i+data_w]).integer for i in range(0, n_bits, data_w)]
+        # reverse data order to match the write order
+        wrdata = [x for x in wrdata[::-1]]
+        # write data
+        for i in range(v['size']):
+            await mm.write(v['offset'] + i, wrdata[i])
+                
+async def send_multi_dp_packet(dp_stream: DpStream, data, n):
+    for i in range(n):
+        await cocotb.start_soon(dp_stream.sosi_drv._driver_send(data))
+
+
+@cocotb.test()
+async def tb_rdma_packetiser(dut):
+    """Try accessing the design. run with < run -a >"""
+
+    n_bytes = dut.c_nof_byte.value
+    c_data_w = dut.c_data_w.value
+    n_words = 120 # = 7680 bytes
+    c_block_len = n_words * n_bytes
+    c_nof_packets_in_msg = 5
+    c_dma_len = c_block_len * c_nof_packets_in_msg    
+    n_hdr_regs = 46
+
+    hdr_dict = {
+        "eth_dst_mac":                     {"access": "RW", "size": 2, "offset": 44, "value": 0xCAFEBABE1996},
+        "eth_src_mac":                     {"access": "RW", "size": 2, "offset": 42, "value": 0x1DECAFC0FFEE},
+        "eth_type":                        {"access": "RO", "size": 1, "offset": 41, "value": 0x0800},
+
+        "ip_version":                      {"access": "RO", "size": 1, "offset": 40, "value": 4},
+        "ip_header_length":                {"access": "RO", "size": 1, "offset": 39, "value": 5},
+        "ip_services":                     {"access": "RO", "size": 1, "offset": 38, "value": 0},
+        "ip_total_length":                 {"access": "RO", "size": 1, "offset": 37, "value": -1},
+        "ip_identification":               {"access": "RO", "size": 1, "offset": 36, "value": 0},
+        "ip_flags":                        {"access": "RO", "size": 1, "offset": 35, "value": 2},
+        "ip_fragment_offset":              {"access": "RO", "size": 1, "offset": 34, "value": 0},
+        "ip_time_to_live":                 {"access": "RO", "size": 1, "offset": 33, "value": 127},
+        "ip_protocol":                     {"access": "RO", "size": 1, "offset": 32, "value": 17},
+        "ip_header_checksum":              {"access": "RO", "size": 1, "offset": 31, "value": -1},
+        "ip_src_addr":                     {"access": "RW", "size": 1, "offset": 30, "value": 0xFACE0FF},
+        "ip_dst_addr":                     {"access": "RW", "size": 1, "offset": 29, "value": 0x7DECADE},
+
+        "udp_src_port":                    {"access": "RW", "size": 1, "offset": 28, "value": 1234},
+        "udp_dst_port":                    {"access": "RW", "size": 1, "offset": 27, "value": 4321},
+        "udp_total_length":                {"access": "RO", "size": 1, "offset": 26, "value": -1},
+        "udp_checksum":                    {"access": "RO", "size": 1, "offset": 25, "value": 0},
+
+        "bth_opcode":                      {"access": "RO", "size": 1, "offset": 24, "value": -1},
+        "bth_se":                          {"access": "RW", "size": 1, "offset": 23, "value": 0},
+        "bth_m":                           {"access": "RW", "size": 1, "offset": 22, "value": 0},
+        "bth_pad":                         {"access": "RW", "size": 1, "offset": 21, "value": 0},
+        "bth_tver":                        {"access": "RW", "size": 1, "offset": 20, "value": 0},
+        "bth_partition_key":               {"access": "RW", "size": 1, "offset": 19, "value": 65535},
+        "bth_fres":                        {"access": "RW", "size": 1, "offset": 18, "value": 0}, 
+        "bth_bres":                        {"access": "RW", "size": 1, "offset": 17, "value": 0}, 
+        "bth_reserved_a":                  {"access": "RO", "size": 1, "offset": 16, "value": 0}, 
+        "bth_dest_qp":                     {"access": "RW", "size": 1, "offset": 15, "value": 0}, 
+        "bth_ack_req":                     {"access": "RW", "size": 1, "offset": 14, "value": 0}, 
+        "bth_reserved_b":                  {"access": "RO", "size": 1, "offset": 13, "value": 0}, 
+        "bth_psn":                         {"access": "RO", "size": 1, "offset": 12, "value": -1}, 
+
+        "reth_virtual_address":            {"access": "RO", "size": 2, "offset": 10, "value": -1},
+        "reth_r_key":                      {"access": "RW", "size": 1, "offset": 9,  "value": 0}, 
+        "reth_dma_length":                 {"access": "RW", "size": 1, "offset": 8,  "value": c_dma_len}, 
+ 
+        "immediate_data":                  {"access": "RW", "size": 1, "offset": 7,  "value": 0xABADCAFE}, 
+ 
+        "config_reserved":                 {"access": "RO", "size": 1, "offset": 6,  "value": 0}, 
+        "config_use_immediate":            {"access": "RW", "size": 1, "offset": 5,  "value": 1}, 
+        "config_use_msg_cnt_as_immediate": {"access": "RW", "size": 1, "offset": 4,  "value": 0}, 
+        "config_nof_packets_in_msg":       {"access": "RW", "size": 1, "offset": 3,  "value": c_nof_packets_in_msg}, 
+        "config_nof_msg":                  {"access": "RW", "size": 1, "offset": 2,  "value": 3}, 
+        "config_start_address":            {"access": "RW", "size": 2, "offset": 0,  "value": 1000000}, 
+    }
+        
+
+    # simple counter value per byte
+    snk_in_data = b''.join([(i % 2**8).to_bytes(1, 'little') for i in range(n_words * n_bytes)])
+    
+    # Create clocks
+    dpClock = Clock(dut.dp_clk, 5, units="ns") 
+    mmClock = Clock(dut.mm_clk, 1, units="ns")
+
+    # DP streams
+    in_stream = DpStream(dut, 'snk_in', 'snk_out', dut.dp_clk, dut.dp_rst, c_data_w)
+    out_stream = DpStream(dut, 'src_out', 'src_in', dut.dp_clk, dut.dp_rst, c_data_w)
+
+    # MM busses
+    reg_hdr_dat = MMController(dut, 'reg_hdr_dat_copi', 'reg_hdr_dat_cipo', dut.mm_clk)
+    
+    cocotb.start_soon(dpClock.start())  # run the dp clock "in the background"
+    cocotb.start_soon(mmClock.start())  # run the mm clock "in the background"
+    cocotb.start_soon(perform_rst(dut.dp_rst, dpClock, 7))
+    cocotb.start_soon(perform_rst(dut.mm_rst, mmClock, 7))
+
+    await Timer(dpClock.period * 10)  # wait a bit for resets to occur
+    await FallingEdge(dut.dp_clk)  # wait for falling edge/"negedge"
+    await write_mm_dict(reg_hdr_dat, hdr_dict)
+    #print(snk_in_data)
+    await Timer(dpClock.period * 10)  # wait a bit for resets to occur
+    await FallingEdge(dut.dp_clk)  # wait for falling edge/"negedge"
+    mm_rd = await read_mm_dict(reg_hdr_dat, hdr_dict)
+    for k, v in mm_rd.items():
+        print(f'{k} = {hex(v.integer)} in hex, {v.integer} in dec')            
+    
+    await FallingEdge(dut.dp_clk)  # wait for falling edge/"negedge"
+    cocotb.start_soon(send_multi_dp_packet(in_stream, snk_in_data, 10))
+    #cocotb.start_soon(in_stream.sosi_drv._driver_send(snk_in_data))
+    
+    # wait for packet to arrive on src_out
+    for i in range(10):
+        data = await out_stream.sosi_mon.wait_for_recv()
+        dut._log.info("src_out data = \n%s", hexdump(data))
+
+
+    
diff --git a/applications/rdma_demo/libraries/rdma_packetiser/hdllib.cfg b/applications/rdma_demo/libraries/rdma_packetiser/hdllib.cfg
index 70a9f1437ef434bb1c44fe5f959ffb0cf9e57f7f..8de4a7e73f9eaf591a7f6b7bce173f33189bff6d 100644
--- a/applications/rdma_demo/libraries/rdma_packetiser/hdllib.cfg
+++ b/applications/rdma_demo/libraries/rdma_packetiser/hdllib.cfg
@@ -16,6 +16,8 @@ test_bench_files =
 regression_test_vhdl = 
     tb/vhdl/tb_tb_rdma_packetiser_assemble_header.vhd
 
+cocotb_files = 
+    cocotb/tb_rdma_packetiser.py # DUT is whatever comes after tb_ (so work.rdma_packetiser)
 
 [modelsim_project_file]