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