Add first version of IO test vehicle.
diff --git a/dodo.py b/dodo.py
new file mode 100644
index 0000000..ae069a8
--- /dev/null
+++ b/dodo.py
@@ -0,0 +1,30 @@
+# SPDX-License-Identifier: CERN-OHL-S-2.0+
+from os.path import dirname
+from pathlib import Path
+
+DOIT_CONFIG = {
+    "default_tasks": ["gds"],
+}
+
+
+top_dir = Path(dirname(__file__))
+
+code_dir = top_dir.joinpath("code")
+code_py_files = tuple(code_dir.rglob("*.py"))
+
+gds_dir = top_dir.joinpath("gds")
+gds_empty_file = gds_dir.joinpath("user_analog_project_wrapper_empty.gds")
+gds_out_file = gds_dir.joinpath("user_analog_project_wrapper.gds.gz")
+
+def task_gds():
+    """Generating GDS file with joined difftap"""
+    def run():
+        from doitcode.generate import gen_gds
+
+        gen_gds(gds_out=gds_out_file, gds_empty=gds_empty_file)
+
+    return {
+        "file_dep": code_py_files,
+        "targets": (gds_out_file,),
+        "actions": (run,),
+    }
\ No newline at end of file
diff --git a/doitcode/__init__.py b/doitcode/__init__.py
new file mode 100644
index 0000000..99aa209
--- /dev/null
+++ b/doitcode/__init__.py
@@ -0,0 +1 @@
+# SPDX-License-Identifier: CERN-OHL-S-2.0+
diff --git a/doitcode/frame.py b/doitcode/frame.py
new file mode 100644
index 0000000..a3a11fe
--- /dev/null
+++ b/doitcode/frame.py
@@ -0,0 +1,22 @@
+# SPDX-License-Identifier: CERN-OHL-S-2.0+
+from pdkmaster.technology import geometry as _geo
+
+
+__all__ = ["boundary", "ioanalog"]
+
+
+# The metal3 pins for the IOs
+boundary = _geo.Rect(left=0.0, bottom=0.0, right=2920.0, top=3520.0)
+ioanalog = (
+    _geo.Rect(left=2911.50, bottom=3389.92, right=2924.00, top=3414.92), # 0
+    _geo.Rect(left=2832.97, bottom=3511.50, right=2857.97, top=3524.00), # 1
+    _geo.Rect(left=2326.97, bottom=3511.50, right=2351.97, top=3524.00), # 2
+    _geo.Rect(left=2066.97, bottom=3511.50, right=2091.97, top=3524.00), # 3
+    _geo.Rect(left=1594.97, bottom=3511.50, right=1619.97, top=3524.00), # 4
+    _geo.Rect(left=1086.47, bottom=3511.50, right=1111.47, top=3524.00), # 5
+    _geo.Rect(left=827.97, bottom=3511.50, right=852.97, top=3524.00), # 6
+    _geo.Rect(left=600.97, bottom=3511.50, right=625.97, top=3524.00), # 7
+    _geo.Rect(left=340.97, bottom=3511.50, right=365.97, top=3524.00), # 8
+    _geo.Rect(left=80.97, bottom=3511.50, right=105.97, top=3524.00), # 9
+    _geo.Rect(left=-4.00, bottom=3401.21, right=8.50, top=3426.21), # 10
+)
\ No newline at end of file
diff --git a/doitcode/generate.py b/doitcode/generate.py
new file mode 100644
index 0000000..caf2575
--- /dev/null
+++ b/doitcode/generate.py
@@ -0,0 +1,158 @@
+# SPDX-License-Identifier: CERN-OHL-S-2.0+
+from pathlib import Path
+from typing import Any, cast
+
+import pya as _pya
+# Disable type checking
+pya = cast(Any, _pya)
+
+from pdkmaster.technology import (geometry as _geo, primitive as _prm)
+from pdkmaster.design import library as _lib
+from pdkmaster.io.klayout import export as _klexp
+
+from c4m.pdk import sky130
+
+from . import frame as _frm
+
+def gen_gds(*, gds_out: Path, gds_empty: Path):
+    top_name = "user_analog_project_wrapper"
+    prims = sky130.tech.primitives
+
+    lib = _lib.Library(
+        name="MPW4IOTest", tech=sky130.tech, cktfab=sky130.cktfab, layoutfab=sky130.layoutfab,
+    )
+
+    # Create the IOBlock
+    ioblock = lib.new_cell(name="IOBlock")
+    ckt = ioblock.new_circuit()
+    layouter = ioblock.new_circuitlayouter()
+
+    x = 0.0
+    lay = None
+    for cell_name in (
+        "IOPadIOVss", "IOPadIOVdd", "IOPadIn", "IOPadInOut", "IOPadOut",
+        "IOPadVdd", "IOPadVss",
+    ):
+        cell = sky130.iolib.cells[cell_name]
+        inst = ckt.new_instance(name=cell_name, object_=cell)
+        lay = layouter.place(inst, x=x, y=0.0)
+        assert lay.boundary is not None
+        x = lay.boundary.right
+    assert lay is not None
+
+    assert isinstance(lay.boundary, _geo.Rect)
+    layouter.layout.boundary = _geo.Rect.from_rect(rect=lay.boundary, left=0.0)
+
+    # Create the IORO
+    ioro = lib.new_cell(name="IORO")
+    ckt = ioro.new_circuit()
+    layouter = ioro.new_circuitlayouter()
+
+    x = 0.0
+    lay = None
+    # First DC cells
+    for cell_name in ("IOPadVdd", "IOPadVss", "IOPadIOVdd", "IOPadIOVss"):
+        cell = sky130.iolib.cells[cell_name]
+        inst = ckt.new_instance(name=f"{cell_name}0", object_=cell)
+        lay = layouter.place(object_=inst, x=x, y=0.0)
+        assert lay.boundary is not None
+        x = lay.boundary.right
+    # 5 In/Out pairs
+    cell_in = sky130.iolib.cells["IOPadIn"]
+    cell_out = sky130.iolib.cells["IOPadOut"]
+    for i in range(5):
+        inst = ckt.new_instance(name=f"in{i}", object_=cell_in)
+        lay = layouter.place(object_=inst, x=x, y=0.0)
+        assert lay.boundary is not None
+        x = lay.boundary.right
+
+        inst = ckt.new_instance(name=f"out{i}", object_=cell_out)
+        lay = layouter.place(object_=inst, x=x, y=0.0)
+        assert lay.boundary is not None
+        x = lay.boundary.right
+    # Last DC cells
+    for cell_name in ("IOPadVdd", "IOPadVss", "IOPadIOVdd", "IOPadIOVss"):
+        cell = sky130.iolib.cells[cell_name]
+        inst = ckt.new_instance(name=f"{cell_name}1", object_=cell)
+        lay = layouter.place(object_=inst, x=x, y=0.0)
+        assert lay.boundary is not None
+        x = lay.boundary.right
+    assert lay is not None
+
+    assert isinstance(lay.boundary, _geo.Rect)
+    layouter.layout.boundary = _geo.Rect.from_rect(rect=lay.boundary, left=0.0)
+
+    # Create connected out cell
+    ioconn = lib.new_cell(name="IOConnected")
+    ckt = ioconn.new_circuit()
+    layouter = ioconn.new_circuitlayouter()
+
+    inst = ckt.new_instance(name="block", object_=ioblock)
+    bound = ioblock.layout.boundary
+    assert isinstance(bound, _geo.Rect)
+    x = _frm.boundary.center.x - bound.center.x
+    y = _frm.boundary.top - 500.0
+    layouter.place(object_=inst, x=x, y=y)
+
+    inst = ckt.new_instance(name="ro", object_=ioro)
+    bound = ioro.layout.boundary
+    assert isinstance(bound, _geo.Rect)
+    x = _frm.boundary.center.x - bound.center.x
+    y = _frm.boundary.top - 1500.0
+    layouter.place(object_=inst, x=x, y=y)
+
+    layouter.layout.boundary = _frm.boundary
+
+    # Create user_analog_project_wrapper top cell
+    top = lib.new_cell(name=top_name)
+    ckt = top.new_circuit()
+    layouter = top.new_circuitlayouter()
+
+    inst = ckt.new_instance(name="ioconn", object_=ioconn)
+    layouter.place(inst, x=0.0, y=0.0)
+
+    layouter.layout.boundary = _frm.boundary
+
+    # Convert to klayout
+    klay = _klexp.export2db(
+        obj=lib, add_pin_label=True, gds_layers=sky130.gds_layers, cell_name=None, merge=True,
+    )
+
+    # Split tapdiff into tap and diff layers
+    nwell_idx = klay.layer(64, 20)
+    diff_idx = klay.layer(65, 20)
+    tap_idx = klay.layer(65, 44)
+    nsdm_idx = klay.layer(93, 44)
+    psdm_idx = klay.layer(94, 20)
+
+    for cell in klay.each_cell():
+        nwell = pya.Region(cell.shapes(nwell_idx))
+        nsdm = pya.Region(cell.shapes(nsdm_idx))
+        psdm = pya.Region(cell.shapes(psdm_idx))
+
+        tap_cover = (nsdm & nwell) + (psdm - nwell)
+
+        diff_shapes = cell.shapes(diff_idx)
+        tap_shapes = cell.shapes(tap_idx)
+
+        difftap = pya.Region(cell.shapes(diff_idx)) # Original difftap layer to be split
+
+        tap = difftap & tap_cover
+        diff = difftap - tap
+
+        diff_shapes.clear()
+        diff_shapes.insert(diff)
+        tap_shapes.insert(tap)
+
+    # Instantiate the empty cell in top cell
+    emptylib = pya.Library()
+    emptylib.register("empty")
+    emptylayout = emptylib.layout()
+    emptylayout.read(str(gds_empty))
+    emptylibcell_idx = emptylayout.cell("user_analog_project_wrapper_empty").cell_index()
+    emptycell_idx = klay.add_lib_cell(emptylib, emptylibcell_idx)
+
+    ktop = klay.cell(top_name)
+    ktop.insert(pya.CellInstArray(emptycell_idx, pya.Trans.R0))
+
+    klay.write(str(gds_out))
diff --git a/gds/user_analog_project_wrapper.gds.gz b/gds/user_analog_project_wrapper.gds.gz
new file mode 100644
index 0000000..d1001b8
--- /dev/null
+++ b/gds/user_analog_project_wrapper.gds.gz
Binary files differ