diff --git a/ebpfcat/ebpfcat.py b/ebpfcat/ebpfcat.py
index 510b0633fa45e59bd0f6ed134244b181b8048bc2..648243eb7fc14748da45b27b5398f46619420e87 100644
--- a/ebpfcat/ebpfcat.py
+++ b/ebpfcat/ebpfcat.py
@@ -19,98 +19,143 @@ class PacketDesc:
     def __get__(self, instance, owner):
         if instance is None:
             return self
-        return PacketVar(instance, self)
-
+        offset = instance.position_offset[self.position[0]]
+        if isinstance(instance, Struct):
+            terminal = instance.terminal
+            device = instance.device
+        else:
+            terminal = instance
+            device = None
+        ret = PacketVar(terminal, (self.position[0],
+                                   self.position[1] + offset), self.size)
+        if device is None:
+            return ret
+        else:
+            return ret.get(device)
 
-class PacketVar:
-    def __init__(self, terminal, desc):
-        self.terminal = terminal
-        self.desc = desc
+    def __set__(self, instance, value):
+        offset = instance.position_offset[self.position[0]]
+        ret = PacketVar(instance.terminal,
+                        (self.position[0], self.position[1] + offset),
+                        self.size)
+        return ret.set(instance.device, value)
 
 
-class TerminalVar(MemoryDesc):
+class PacketVar(MemoryDesc):
     base_register = 9
 
-    def __init__(self):
-        pass  # do not call parent to set self.fmt
-
     def fmt(self):
-        pv = instance.__dict__[self.name]
-        if isinstance(pv.desc.size, int):
+        if isinstance(self.size, int):
             return "B"
         else:
-            return pv.desc.size
+            return self.size
 
-    def __set__(self, instance, value):
-        if isinstance(value, PacketVar):
-            instance.__dict__[self.name] = value
-        elif instance.sync_group.current_data is None:
-            fmt, _ = self._fmt_start(instance)
-            if isinstance(fmt, int):
+    def __init__(self, terminal, position, size):
+        self.terminal = terminal
+        self.position = position
+        self.size = size
+
+    def set(self, device, value):
+        if device.sync_group.current_data is None:
+            if isinstance(self.size, int):
                 try:
                     bool(value)
                 except RuntimeError:
-                    e = instance.sync_group
+                    e = device.sync_group
                     with e.wtmp:
-                        e.wtmp = super().__get__(instance, None)
+                        e.wtmp = super().__get__(device, None)
                         with value as cond:
-                            e.wtmp |= 1 << fmt
+                            e.wtmp |= 1 << self.size
                         with cond.Else():
-                            e.wtmp &= ~(1 << fmt)
-                        super().__set__(instance, e.wtmp)
+                            e.wtmp &= ~(1 << self.size)
+                        super().__set__(device, e.wtmp)
                     return
                 else:
-                    old = super().__get__(instance, None)
+                    old = super().__get__(device, None)
                     if value:
-                        value = old | (1 << fmt)
+                        value = old | (1 << self.size)
                     else:
-                        value = old & ~(1 << fmt)
-            super().__set__(instance, value)
+                        value = old & ~(1 << self.size)
+            super().__set__(device, value)
         else:
-            data = instance.sync_group.current_data
-            fmt, start = self._fmt_start(instance)
-            if isinstance(fmt, int):
+            data = device.sync_group.current_data
+            start = self._start(device)
+            if isinstance(self.size, int):
                 if value:
-                    data[start] |= 1 << fmt
+                    data[start] |= 1 << self.size
                 else:
-                    data[start] &= ~(1 << fmt)
+                    data[start] &= ~(1 << self.size)
             else:
-                pack_into("<" + fmt, data, start, value)
+                pack_into("<" + self.size, data, start, value)
+
+    def get(self, device):
+        if device.sync_group.current_data is None:
+            if isinstance(self.size, int):
+                return super().__get__(device, None) & (1 << self.size)
+            else:
+                return super().__get__(device, None)
+        else:
+            data = device.sync_group.current_data
+            start = self._start(device)
+            if isinstance(self.size, int):
+                return bool(data[start] & (1 << self.size))
+            else:
+                return unpack_from("<" + self.size, data, start)[0]
+
+    def _start(self, device):
+        base, offset = self.position
+        return device.sync_group.terminals[self.terminal][base] + offset
+
+    def fmt_addr(self, device):
+        return ("B" if isinstance(self.size, int) else self.size,
+                self._start(device) + Packet.ETHERNET_HEADER)
+
+
+class Struct:
+    device = None
+
+    def __new__(cls, *args):
+        return StructDesc(cls, *args)
+
+
+class StructDesc:
+    def __init__(self, struct, *position_offset):
+        self.struct = struct
+        self.position_offset = position_offset
 
     def __get__(self, instance, owner):
         if instance is None:
             return self
-        elif self.name not in instance.__dict__:
-            return None
-        elif instance.sync_group.current_data is None:
-            fmt, _ = self._fmt_start(instance)
-            if isinstance(fmt, int):
-                return super().__get__(instance, owner) & (1 << fmt)
-            else:
-                return super().__get__(instance, owner)
+        ret = object.__new__(self.struct)
+        ret.position_offset = self.position_offset
+        ret.terminal = instance
+        return ret
+
+
+class TerminalVar:
+    def __set__(self, instance, value):
+        if isinstance(value, PacketVar):
+            instance.__dict__[self.name] = value
+        elif isinstance(value, Struct):
+            instance.__dict__[self.name] = value
+            value.device = instance
         else:
-            data = instance.sync_group.current_data
-            fmt, start = self._fmt_start(instance)
-            if isinstance(fmt, int):
-                return bool(data[start] & (1 << fmt))
-            else:
-                return unpack_from("<" + fmt, data, start)[0]
+            return instance.__dict__[self.name].set(instance, value)
 
-    def _fmt_start(self, instance):
-        pv = instance.__dict__[self.name]
-        base, offset = pv.desc.position
-        start = pv.terminal.bases[base] + offset
-        return pv.desc.size, start
+    def __get__(self, instance, owner):
+        if instance is None:
+            return self
+        var = instance.__dict__.get(self.name)
+        if var is None:
+            return None
+        elif isinstance(var, Struct):
+            return var
+        else:
+            return instance.__dict__[self.name].get(instance)
 
     def __set_name__(self, owner, name):
         self.name = name
 
-    def fmt_addr(self, instance):
-        fmt, start = self._fmt_start(instance)
-        if isinstance(fmt, int):
-            fmt = "B"
-        return fmt, start + Packet.ETHERNET_HEADER
-
 
 class DeviceVar(ArrayGlobalVarDesc):
     def __init__(self, size="I"):
@@ -122,7 +167,7 @@ class DeviceVar(ArrayGlobalVarDesc):
         elif instance.sync_group.current_data is None:
             return super().__get__(instance, owner)
         else:
-            return instance.__dict__[self.name]
+            return instance.__dict__.get(self.name, 0)
 
     def __set__(self, instance, value):
         if instance.sync_group.current_data is None:
@@ -140,13 +185,14 @@ class Device(SubProgram):
     def get_terminals(self):
         ret = set()
         for pv in self.__dict__.values():
-            if isinstance(pv, PacketVar):
+            if isinstance(pv, (PacketVar, Struct)):
                 ret.add(pv.terminal)
         return ret
 
 
 class EBPFTerminal(Terminal):
     compatibility = None
+    position_offset = 0, 0
 
     def __init_subclass__(cls):
         cls.pdo = {}
@@ -163,15 +209,16 @@ class EBPFTerminal(Terminal):
 
     def allocate(self, packet):
         if self.pdo_in_sz:
-            self.bases = [packet.size + packet.DATAGRAM_HEADER]
+            bases = [packet.size + packet.DATAGRAM_HEADER]
             packet.append(ECCmd.FPRD, b"\0" * self.pdo_in_sz, 0,
                           self.position, self.pdo_in_off)
         else:
-            self.bases = [None]
+            bases = [None]
         if self.pdo_out_sz:
-            self.bases.append(packet.size + packet.DATAGRAM_HEADER)
+            bases.append(packet.size + packet.DATAGRAM_HEADER)
             packet.append(ECCmd.FPWR, b"\0" * self.pdo_out_sz, 0,
                           self.position, self.pdo_out_off)
+        return bases
 
     def update(self, data):
         pass
@@ -239,15 +286,9 @@ class FastEtherCat(EtherCat):
         self.ebpf.programs = self.programs
         self.fd = await self.ebpf.attach(self.addr[0])
 
-
-class SyncGroup:
-    """A group of devices communicating at the same time"""
-
-    packet_index = 1000
-
-    current_data = False  # None is used to indicate FastSyncGroup
-
+class SyncGroupBase:
     def __init__(self, ec, devices, **kwargs):
+        super().__init__(**kwargs)
         self.ec = ec
         self.devices = devices
 
@@ -256,7 +297,20 @@ class SyncGroup:
             terminals.update(dev.get_terminals())
             dev.sync_group = self
         # sorting is only necessary for test stability
-        self.terminals = sorted(terminals, key=lambda t: t.position)
+        self.terminals = {t: None for t in
+                          sorted(terminals, key=lambda t: t.position)}
+
+    def allocate(self):
+        self.packet = Packet()
+        self.terminals = {t: t.allocate(self.packet) for t in self.terminals}
+
+
+class SyncGroup(SyncGroupBase):
+    """A group of devices communicating at the same time"""
+
+    packet_index = 1000
+
+    current_data = False  # None is used to indicate FastSyncGroup
 
     async def run(self):
         await gather(*[t.to_operational() for t in self.terminals])
@@ -269,16 +323,14 @@ class SyncGroup:
                 dev.update()
 
     def start(self):
-        self.packet = Packet()
-        for term in self.terminals:
-            term.allocate(self.packet)
+        self.allocate()
         self.packet_index = SyncGroup.packet_index
         SyncGroup.packet_index += 1
         self.asm_packet = self.packet.assemble(self.packet_index)
         return ensure_future(self.run())
 
 
-class FastSyncGroup(XDP):
+class FastSyncGroup(SyncGroupBase, XDP):
     license = "GPL"
 
     current_data = None
@@ -286,16 +338,7 @@ class FastSyncGroup(XDP):
     properties = ArrayMap()
 
     def __init__(self, ec, devices, **kwargs):
-        super().__init__(subprograms=devices, **kwargs)
-        self.ec = ec
-        self.devices = devices
-
-        terminals = set()
-        for dev in self.devices:
-            terminals.update(dev.get_terminals())
-            dev.sync_group = self
-        # sorting is only necessary for test stability
-        self.terminals = sorted(terminals, key=lambda t: t.position)
+        super().__init__(ec, devices, subprograms=devices, **kwargs)
 
     def program(self):
         with self.packetSize >= self.packet.size + Packet.ETHERNET_HEADER as p:
@@ -304,9 +347,7 @@ class FastSyncGroup(XDP):
         self.exit(XDPExitCode.TX)
 
     def start(self):
-        self.packet = Packet()
-        for term in self.terminals:
-            term.allocate(self.packet)
+        self.allocate()
         index = self.ec.register_sync_group(self)
         self.ec.send_packet(self.packet.assemble(index))
         self.monitor = ensure_future(gather(*[t.to_operational()
diff --git a/ebpfcat/ethercat_test.py b/ebpfcat/ethercat_test.py
index a6502152e05ccbb9e211ad4e4bd0a28409284f9a..c5e885bb72f74e823defbb05ec4379793997e971 100644
--- a/ebpfcat/ethercat_test.py
+++ b/ebpfcat/ethercat_test.py
@@ -2,9 +2,10 @@ from asyncio import CancelledError, Future, get_event_loop, sleep, gather
 from unittest import TestCase, main
 
 from .devices import AnalogInput, AnalogOutput
-from .terminals import EL4104, EL3164
+from .terminals import EL4104, EL3164, EK1814
 from .ethercat import ECCmd
-from .ebpfcat import SyncGroup
+from .ebpfcat import FastSyncGroup, SyncGroup, TerminalVar, Device
+from .ebpf import Instruction, Opcode as O
 
 
 class MockEtherCat:
@@ -26,18 +27,22 @@ class MockEtherCat:
                 await sleep(0)
         return self.results.pop(0)
 
+    def register_sync_group(self, sg):
+        self.rsg = sg
+        return 0x33
+
 
 class Tests(TestCase):
     def test_input(self):
         ti = EL3164()
         ti.pdo_in_sz = 4
-        ti.pdo_in_off = 0xABCD 
+        ti.pdo_in_off = 0xABCD
         ti.position = 0x77
         ti.pdo_out_sz = 3
         ti.pdo_out_off = 0x4321
         ec = MockEtherCat(self)
         ti.ec = ec
-        ai = AnalogInput(ti.ch1_value)
+        ai = AnalogInput(ti.channel1.value)
         SyncGroup.packet_index = 1000
         sg = SyncGroup(ec, [ai])
         self.task = sg.start()
@@ -73,7 +78,7 @@ class Tests(TestCase):
     def test_output(self):
         ti = EL4104()
         ti.pdo_in_sz = 4
-        ti.pdo_in_off = 0xABCD 
+        ti.pdo_in_off = 0xABCD
         ti.position = 0x77
         ti.pdo_out_sz = 3
         ti.pdo_out_off = 0x4321
@@ -120,6 +125,97 @@ class Tests(TestCase):
         with self.assertRaises(CancelledError):
             get_event_loop().run_until_complete(self.task)
 
+    def test_ebpf(self):
+        ti = EL3164()
+        ti.pdo_in_sz = 4
+        ti.pdo_in_off = 0xABCD
+        ti.position = 0x77
+        ti.pdo_out_sz = None
+        ti.pdo_out_off = None
+        to = EL4104()
+        to.pdo_in_sz = None
+        to.pdo_in_off = None
+        to.position = 0x55
+        to.pdo_out_sz = 2
+        to.pdo_out_off = 0x5678
+        td = EK1814()
+        td.pdo_in_sz = 1
+        td.pdo_in_off = 0x7777
+        td.position = 0x44
+        td.pdo_out_sz = 1
+        td.pdo_out_off = 0x8888
+
+        class D(Device):
+            ai = TerminalVar()
+            ao = TerminalVar()
+            di = TerminalVar()
+            do = TerminalVar()
+
+            def program(self):
+                self.do = False
+                self.do = True
+                self.do = self.ai
+                self.ao = self.di
+                with self.di:
+                    self.ao = self.ai
+
+        d = D()
+        d.ai = ti.channel1.value
+        d.ao = to.ch1_value
+        d.di = td.ch1
+        d.do = td.ch5
+
+        ec = MockEtherCat(self)
+        sg = FastSyncGroup(ec, [d])
+        ec.expected = [
+            bytes.fromhex("4610"  # EtherCAT Header, length & type
+                          "000033000000008000000000"  # ID datagram
+                          "04004400777701800000000000"  # digi in
+                          "05004400888801800000000000"  # digi out
+                          "0500550078560280000000000000"  # ana out
+                          "04007700cdab04000000000000000000")  # ana in
+            ]
+        task = sg.start()
+        self.assertEqual(ec.rsg, sg)
+        task.cancel()
+        with self.assertRaises(CancelledError):
+            get_event_loop().run_until_complete(task)
+        sg.program()
+        self.maxDiff = None
+        self.assertEqual(sg.opcodes, [
+            Instruction(opcode=O.W+O.LD, dst=9, src=1, off=0, imm=0),
+            Instruction(opcode=O.W+O.LD, dst=0, src=1, off=4, imm=0),
+            Instruction(opcode=O.W+O.LD, dst=2, src=1, off=0, imm=0),
+            Instruction(opcode=O.ADD+O.LONG, dst=2, src=0, off=0, imm=83),
+            Instruction(opcode=O.JLE+O.REG, dst=0, src=2, off=21, imm=0),
+
+            Instruction(opcode=O.B+O.LD, dst=0, src=9, off=51, imm=0),
+            Instruction(opcode=O.AND, dst=0, src=0, off=0, imm=-2),
+            Instruction(opcode=O.STX+O.B, dst=9, src=0, off=51, imm=0),
+            Instruction(opcode=O.B+O.LD, dst=0, src=9, off=51, imm=0),
+            Instruction(opcode=O.OR, dst=0, src=0, off=0, imm=1),
+            Instruction(opcode=O.STX+O.B, dst=9, src=0, off=51, imm=0),
+
+            Instruction(opcode=O.LD+O.B, dst=0, src=9, off=51, imm=0),
+            Instruction(opcode=O.REG+O.LD, dst=2, src=9, off=80, imm=0),
+            Instruction(opcode=O.JEQ, dst=2, src=0, off=2, imm=0),
+            Instruction(opcode=O.OR, dst=0, src=0, off=0, imm=1),
+            Instruction(opcode=O.JMP, dst=0, src=0, off=1, imm=0),
+            Instruction(opcode=O.AND, dst=0, src=0, off=0, imm=-2),
+            Instruction(opcode=O.STX+O.B, dst=9, src=0, off=51, imm=0),
+
+            Instruction(opcode=O.B+O.LD, dst=0, src=9, off=38, imm=0),
+            Instruction(opcode=O.AND, dst=0, src=0, off=0, imm=1),
+            Instruction(opcode=O.STX+O.REG, dst=9, src=0, off=64, imm=0),
+
+            Instruction(opcode=O.LD+O.B, dst=0, src=9, off=38, imm=0),
+            Instruction(opcode=O.JSET, dst=0, src=0, off=1, imm=1),
+            Instruction(opcode=O.JMP, dst=0, src=0, off=2, imm=0),
+            Instruction(opcode=O.LD+O.REG, dst=0, src=9, off=80, imm=0),
+            Instruction(opcode=O.STX+O.REG, dst=9, src=0, off=64, imm=0),
+
+            Instruction(opcode=O.MOV+O.LONG, dst=0, src=0, off=0, imm=3),
+            Instruction(opcode=O.EXIT, dst=0, src=0, off=0, imm=0)])
 
 if __name__ == "__main__":
     main()
diff --git a/ebpfcat/terminals.py b/ebpfcat/terminals.py
index 721d7bc24520684e5f360c9dba232ada0bc126e5..620a43461d0dfc11fbdd082078aa97c1aa849e30 100644
--- a/ebpfcat/terminals.py
+++ b/ebpfcat/terminals.py
@@ -1,4 +1,4 @@
-from .ebpfcat import EBPFTerminal, PacketDesc
+from .ebpfcat import EBPFTerminal, PacketDesc, Struct
 
 
 class Generic(EBPFTerminal):
@@ -13,14 +13,15 @@ class EL4104(EBPFTerminal):
 
 
 class EL3164(EBPFTerminal):
-    ch1_attrs = PacketDesc((0, 0), 'H')
-    ch2_attrs = PacketDesc((0, 4), 'H')
-    ch3_attrs = PacketDesc((0, 8), 'H')
-    ch4_attrs = PacketDesc((0, 12), 'H')
-    ch1_value = PacketDesc((0, 2), 'H')
-    ch2_value = PacketDesc((0, 6), 'H')
-    ch3_value = PacketDesc((0, 10), 'H')
-    ch4_value = PacketDesc((0, 14), 'H')
+    class Channel(Struct):
+        attrs = PacketDesc((0, 0), 'H')
+        value = PacketDesc((0, 2), 'H')
+
+    channel1 = Channel(0)
+    channel2 = Channel(4)
+    channel3 = Channel(8)
+    channel4 = Channel(12)
+
 
 class EK1814(EBPFTerminal):
     ch1 = PacketDesc((0, 0), 0)