mirror of
https://github.com/TinyTapeout/tt-support-tools.git
synced 2026-11-03 12:46:08 +01:00
feat(precheck): initial version
perform presubmission checks on custom GDS files submitted to Tiny Tapeout
This commit is contained in:
@@ -0,0 +1,41 @@
|
|||||||
|
name: Precheck Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-precheck:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
PDK_ROOT: /home/runner/pdk
|
||||||
|
PDK_VERSION: dd7771c384ed36b91a25e9f8b314355fc26561be
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
cache: 'pip'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: precheck
|
||||||
|
run: pip install -r requirements.txt
|
||||||
|
|
||||||
|
- name: Install Sky130 PDK
|
||||||
|
uses: TinyTapeout/volare-action@v1
|
||||||
|
with:
|
||||||
|
pdk_name: sky130
|
||||||
|
pdk_version: ${{ env.PDK_VERSION }}
|
||||||
|
pdk_root: ${{ env.PDK_ROOT }}
|
||||||
|
|
||||||
|
- name: Install nix
|
||||||
|
uses: cachix/install-nix-action@v20
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
working-directory: precheck
|
||||||
|
run: nix-shell --run "pytest"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
__pycache__
|
||||||
|
.pytest_cache
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{ pkgs ? import
|
||||||
|
(builtins.fetchGit {
|
||||||
|
url = "https://github.com/NixOS/nixpkgs.git";
|
||||||
|
rev = "f2bd8adf7b78d7616b52d0ef08865c7c2fcf189d";
|
||||||
|
})
|
||||||
|
{ }
|
||||||
|
, magic ? import ./nix/magic.nix { inherit pkgs; }
|
||||||
|
,
|
||||||
|
}:
|
||||||
|
|
||||||
|
pkgs.mkShell {
|
||||||
|
buildInputs = [
|
||||||
|
pkgs.klayout
|
||||||
|
magic
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
|
class LayerInfo:
|
||||||
|
def __init__(self, name: str, source: str, layer: int, data_type: int):
|
||||||
|
self.name = name
|
||||||
|
self.source = source
|
||||||
|
self.layer = layer
|
||||||
|
self.data_type = data_type
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"LayerInfo(name={self.name}, source={self.source}, layer={self.layer}, data_type={self.data_type})"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_lyp_layers(lyp_file: str):
|
||||||
|
with open(lyp_file) as f:
|
||||||
|
xml_content = f.read()
|
||||||
|
root = ET.fromstring(xml_content)
|
||||||
|
|
||||||
|
layers_dict: Dict[str, LayerInfo] = {}
|
||||||
|
|
||||||
|
for properties in root.findall("properties"):
|
||||||
|
name = properties.find("name")
|
||||||
|
source = properties.find("source")
|
||||||
|
valid = properties.find("valid")
|
||||||
|
|
||||||
|
if valid is not None and valid.text == "false":
|
||||||
|
continue
|
||||||
|
|
||||||
|
if name is not None and source is not None:
|
||||||
|
name_key = name.text.split("-")[0].strip()
|
||||||
|
layer, data_type = source.text.split("@")[0].split("/")
|
||||||
|
# Add the 'source' text as the value in the dictionary
|
||||||
|
layers_dict[name_key] = LayerInfo(
|
||||||
|
name=name.text,
|
||||||
|
source=source.text,
|
||||||
|
layer=int(layer),
|
||||||
|
data_type=int(data_type),
|
||||||
|
)
|
||||||
|
|
||||||
|
return layers_dict
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2020 Efabless Corporation
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
|
||||||
|
set GDS_UT_PATH [lindex $argv 6]
|
||||||
|
set DESIGN_NAME [lindex $argv 7]
|
||||||
|
set PDK_PATH [lindex $argv 8]
|
||||||
|
set DRC_REPORT [lindex $argv 9]
|
||||||
|
set DRC_MAG [lindex $argv 10]
|
||||||
|
|
||||||
|
gds read $GDS_UT_PATH
|
||||||
|
|
||||||
|
set fout [open $DRC_REPORT w]
|
||||||
|
set oscale [cif scale out]
|
||||||
|
set cell_name $DESIGN_NAME
|
||||||
|
magic::suspendall
|
||||||
|
puts stdout "\[INFO\]: Loading $cell_name\n"
|
||||||
|
flush stdout
|
||||||
|
load $cell_name
|
||||||
|
select top cell
|
||||||
|
expand
|
||||||
|
drc euclidean on
|
||||||
|
drc style drc(full)
|
||||||
|
drc check
|
||||||
|
set drc_result [drc listall why]
|
||||||
|
|
||||||
|
|
||||||
|
set count 0
|
||||||
|
puts $fout "$cell_name"
|
||||||
|
puts $fout "----------------------------------------"
|
||||||
|
foreach {errtype coordlist} $drc_result {
|
||||||
|
puts $fout $errtype
|
||||||
|
puts $fout "----------------------------------------"
|
||||||
|
foreach coord $coordlist {
|
||||||
|
set bllx [expr {$oscale * [lindex $coord 0]}]
|
||||||
|
set blly [expr {$oscale * [lindex $coord 1]}]
|
||||||
|
set burx [expr {$oscale * [lindex $coord 2]}]
|
||||||
|
set bury [expr {$oscale * [lindex $coord 3]}]
|
||||||
|
set coords [format " %.3f %.3f %.3f %.3f" $bllx $blly $burx $bury]
|
||||||
|
puts $fout "$coords"
|
||||||
|
set count [expr {$count + 1} ]
|
||||||
|
}
|
||||||
|
puts $fout "----------------------------------------"
|
||||||
|
}
|
||||||
|
|
||||||
|
puts $fout "\[INFO\]: COUNT: $count"
|
||||||
|
puts $fout "\[INFO\]: Should be divided by 3 or 4"
|
||||||
|
|
||||||
|
puts $fout ""
|
||||||
|
close $fout
|
||||||
|
|
||||||
|
puts stdout "\[INFO\]: DRC Checking DONE ($DRC_REPORT)"
|
||||||
|
flush stdout
|
||||||
|
|
||||||
|
if {$count > 0} {
|
||||||
|
puts stderr "\[ERROR\]: $count DRC errors found"
|
||||||
|
puts stdout "\[INFO\]: Saving mag view with DRC errors($DRC_MAG)"
|
||||||
|
# WARNING: changes the name of the cell; keep as last step
|
||||||
|
save $DRC_MAG
|
||||||
|
puts stdout "\[INFO\]: Saved"
|
||||||
|
flush stdout
|
||||||
|
exit -1
|
||||||
|
} else {
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# Copyright 2023 Efabless Corporation
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
# Copyright (c) 2003-2023 Eelco Dolstra and the Nixpkgs/NixOS contributors
|
||||||
|
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
# a copy of this software and associated documentation files (the
|
||||||
|
# "Software"), to deal in the Software without restriction, including
|
||||||
|
# without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
# permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
# the following conditions:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
{ pkgs ? import <nixpkgs> { }
|
||||||
|
,
|
||||||
|
}:
|
||||||
|
|
||||||
|
with pkgs; clangStdenv.mkDerivation rec {
|
||||||
|
name = "magic-vlsi";
|
||||||
|
rev = "11080465ad3759d4bdb3936026c797644c6f7f0b";
|
||||||
|
|
||||||
|
src = fetchFromGitHub {
|
||||||
|
owner = "RTimothyEdwards";
|
||||||
|
repo = "magic";
|
||||||
|
inherit rev;
|
||||||
|
sha256 = "sha256-zRIS8tFT52soPsySZdxVOX+5Pu8xriZeur0Gh8kdaXk=";
|
||||||
|
};
|
||||||
|
|
||||||
|
nativeBuildInputs = [ python3 tcsh gnused ];
|
||||||
|
|
||||||
|
# So here's the situation:
|
||||||
|
## * Magic will not compile without X11 libraries, even for headless use.
|
||||||
|
## * Magic with Cairo will use Cairo-X11 headers that are not available on
|
||||||
|
## pre-built Mac versions of Cairo which presume you're going to use Quartz.
|
||||||
|
## -> Unintuitively, that means X11 libraries must be present on Mac, but not
|
||||||
|
## Cairo.
|
||||||
|
buildInputs = [
|
||||||
|
xorg.libX11
|
||||||
|
m4
|
||||||
|
mesa_glu
|
||||||
|
ncurses
|
||||||
|
tcl
|
||||||
|
tcsh
|
||||||
|
tk
|
||||||
|
] ++ lib.optionals stdenv.isLinux [ cairo ];
|
||||||
|
|
||||||
|
configureFlags = [
|
||||||
|
"--with-tcl=${tcl}"
|
||||||
|
"--with-tk=${tk}"
|
||||||
|
"--disable-werror"
|
||||||
|
];
|
||||||
|
|
||||||
|
preConfigure = ''
|
||||||
|
# nix shebang fix
|
||||||
|
patchShebangs ./scripts
|
||||||
|
|
||||||
|
# "Precompute" git rev-parse HEAD
|
||||||
|
sed -i 's@`git rev-parse HEAD`@${rev}@' ./scripts/defs.mak.in
|
||||||
|
'';
|
||||||
|
|
||||||
|
NIX_CFLAGS_COMPILE = "-Wno-implicit-function-declaration -Wno-parentheses -Wno-macro-redefined";
|
||||||
|
|
||||||
|
# Fairly sure this is deprecated?
|
||||||
|
enableParallelBuilding = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import klayout.db as pya
|
||||||
|
import klayout.rdb as rdb
|
||||||
|
from klayout_tools import parse_lyp_layers
|
||||||
|
|
||||||
|
PDK_ROOT = os.getenv("PDK_ROOT")
|
||||||
|
PDK_NAME = os.getenv("PDK_NAME") or "sky130A"
|
||||||
|
LYP_FILE = f"{PDK_ROOT}/{PDK_NAME}/libs.tech/klayout/tech/{PDK_NAME}.lyp"
|
||||||
|
REPORTS_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "reports")
|
||||||
|
|
||||||
|
|
||||||
|
def magic_drc(gds: str, toplevel: str):
|
||||||
|
logging.info(f"Running magic DRC on {gds} (module={toplevel})")
|
||||||
|
|
||||||
|
magic = subprocess.run(
|
||||||
|
[
|
||||||
|
"magic",
|
||||||
|
"-noconsole",
|
||||||
|
"-dnull",
|
||||||
|
"-rcfile",
|
||||||
|
f"{PDK_ROOT}/{PDK_NAME}/libs.tech/magic/{PDK_NAME}.magicrc",
|
||||||
|
"magic_drc.tcl",
|
||||||
|
gds,
|
||||||
|
toplevel,
|
||||||
|
PDK_ROOT,
|
||||||
|
f"{REPORTS_PATH}/magic_drc.txt",
|
||||||
|
f"{REPORTS_PATH}/magic_drc.mag",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
if magic.returncode != 0:
|
||||||
|
logging.error("Magic DRC failed")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def klayout_drc(gds: str, check: str):
|
||||||
|
logging.info(f"Running klayout {check} on {gds}")
|
||||||
|
report_file = f"{REPORTS_PATH}/drc_{check}.xml"
|
||||||
|
klayout = subprocess.run(
|
||||||
|
[
|
||||||
|
"klayout",
|
||||||
|
"-b",
|
||||||
|
"-r",
|
||||||
|
f"tech-files/{PDK_NAME}_mr.drc",
|
||||||
|
"-rd",
|
||||||
|
f"{check}=true",
|
||||||
|
"-rd",
|
||||||
|
f"input={gds}",
|
||||||
|
"-rd",
|
||||||
|
f"report={report_file}",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
if klayout.returncode != 0:
|
||||||
|
logging.error(f"Klayout {check} failed")
|
||||||
|
return False
|
||||||
|
|
||||||
|
report = rdb.ReportDatabase().load(report_file)
|
||||||
|
|
||||||
|
if report.num_items() > 0:
|
||||||
|
logging.error(
|
||||||
|
f"Klayout {check} failed with {report.num_items()} DRC violations"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def klayout_checks(gds: str):
|
||||||
|
layout = pya.Layout()
|
||||||
|
layout.read(gds)
|
||||||
|
layers = parse_lyp_layers(LYP_FILE)
|
||||||
|
|
||||||
|
logging.info("Running forbidden layer check...")
|
||||||
|
forbidden_layers = [
|
||||||
|
"met5.drawing",
|
||||||
|
"met5.pin",
|
||||||
|
"met5.label",
|
||||||
|
]
|
||||||
|
|
||||||
|
had_error = False
|
||||||
|
for layer in forbidden_layers:
|
||||||
|
layer_info = layers[layer]
|
||||||
|
logging.info(f"* Checking {layer_info.name}")
|
||||||
|
layer_index = layout.find_layer(layer_info.layer, layer_info.data_type)
|
||||||
|
if layer_index is not None:
|
||||||
|
logging.error(f"Forbidden layer {layer} found in {gds}")
|
||||||
|
had_error = True
|
||||||
|
|
||||||
|
if had_error:
|
||||||
|
logging.error("Klayout checks failed")
|
||||||
|
return not had_error
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--gds", required=True)
|
||||||
|
parser.add_argument("--toplevel", required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
||||||
|
logging.info(f"PDK_ROOT: {PDK_ROOT}")
|
||||||
|
|
||||||
|
assert magic_drc(args.gds, args.toplevel)
|
||||||
|
|
||||||
|
assert klayout_drc(args.gds, "feol")
|
||||||
|
assert klayout_drc(args.gds, "beol")
|
||||||
|
assert klayout_drc(args.gds, "offgrid")
|
||||||
|
|
||||||
|
assert klayout_checks(args.gds)
|
||||||
|
|
||||||
|
logging.info(f"Precheck passed for {args.gds}! 🎉")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
magic_drc.mag
|
||||||
|
magic_drc.txt
|
||||||
|
drc_feol.xml
|
||||||
|
drc_beol.xml
|
||||||
|
drc_offgrid.xml
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
klayout==0.28.12
|
||||||
|
pytest==7.4.3
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
sky130A_mr.drc from https://github.com/efabless/mpw_precheck revision 4efb33553ee6c2acdb0fa9a6a5f87af099f5c1d5
|
||||||
@@ -0,0 +1,798 @@
|
|||||||
|
# DRC for SKY130 according to :
|
||||||
|
# https://skywater-pdk.readthedocs.io/en/latest/rules/periphery.html
|
||||||
|
# https://skywater-pdk.readthedocs.io/en/latest/rules/layers.html
|
||||||
|
#
|
||||||
|
# Distributed under GNU GPLv3: https://www.gnu.org/licenses/
|
||||||
|
#
|
||||||
|
# History :
|
||||||
|
# 2022-6-22 : 2022.6.30_01.07 release
|
||||||
|
#
|
||||||
|
# 2023-6-14 : 2023.6.14_01.08 release
|
||||||
|
#
|
||||||
|
##########################################################################################
|
||||||
|
release = "2023.6.14_01.08"
|
||||||
|
|
||||||
|
require 'time'
|
||||||
|
require "logger"
|
||||||
|
|
||||||
|
exec_start_time = Time.now
|
||||||
|
|
||||||
|
logger = Logger.new(STDOUT)
|
||||||
|
|
||||||
|
logger.formatter = proc do |severity, datetime, progname, msg|
|
||||||
|
"#{msg}
|
||||||
|
"
|
||||||
|
end
|
||||||
|
# optionnal for a batch launch : klayout -b -rd input=my_layout.gds -rd report=sky130_drc.txt -r drc_sky130.drc
|
||||||
|
if $input
|
||||||
|
source($input, $top_cell)
|
||||||
|
end
|
||||||
|
|
||||||
|
if $report
|
||||||
|
report("SKY130 DRC runset", $report)
|
||||||
|
else
|
||||||
|
report("SKY130 DRC runset", File.join(File.dirname(RBA::CellView::active.filename), "sky130_drc.txt"))
|
||||||
|
end
|
||||||
|
|
||||||
|
AL = true # do not change
|
||||||
|
CU = false # do not change
|
||||||
|
# choose betwen only one of AL or CU back-end flow here :
|
||||||
|
backend_flow = AL
|
||||||
|
|
||||||
|
FEOL = false
|
||||||
|
BEOL = false
|
||||||
|
OFFGRID = false
|
||||||
|
SEAL = false
|
||||||
|
FLOATING_MET = false
|
||||||
|
|
||||||
|
# enable / disable rule groups
|
||||||
|
if $feol == "1" || $feol == "true"
|
||||||
|
FEOL = true # front-end-of-line checks
|
||||||
|
else
|
||||||
|
FEOL = false
|
||||||
|
end
|
||||||
|
|
||||||
|
if $beol == "1" || $beol == "true"
|
||||||
|
BEOL = true # back-end-of-line checks
|
||||||
|
else
|
||||||
|
BEOL = false
|
||||||
|
end
|
||||||
|
|
||||||
|
if $offgrid == "1" || $offgrid == "true"
|
||||||
|
OFFGRID = true # manufacturing grid/angle checks
|
||||||
|
else
|
||||||
|
OFFGRID = false
|
||||||
|
end
|
||||||
|
|
||||||
|
if $seal == "1" || $seal == "true"
|
||||||
|
SEAL = true # SEAL RING checks
|
||||||
|
else
|
||||||
|
SEAL = false
|
||||||
|
end
|
||||||
|
|
||||||
|
if $floating_met == "1" || $floating_met == "true"
|
||||||
|
FLOATING_MET = true # back-end-of-line checks
|
||||||
|
else
|
||||||
|
FLOATING_MET = false
|
||||||
|
end
|
||||||
|
|
||||||
|
# klayout setup
|
||||||
|
########################
|
||||||
|
# use a tile size of 1mm - not used in deep mode-
|
||||||
|
# tiles(1000.um)
|
||||||
|
# use a tile border of 10 micron:
|
||||||
|
# tile_borders(1.um)
|
||||||
|
#no_borders
|
||||||
|
|
||||||
|
# hierachical
|
||||||
|
deep
|
||||||
|
|
||||||
|
if $thr
|
||||||
|
threads($thr)
|
||||||
|
else
|
||||||
|
threads(4)
|
||||||
|
end
|
||||||
|
|
||||||
|
# if more inof is needed, set true
|
||||||
|
# verbose(true)
|
||||||
|
verbose(true)
|
||||||
|
|
||||||
|
# layers definitions
|
||||||
|
########################
|
||||||
|
|
||||||
|
# all except purpose (datatype) 5 -- label and 44 -- via
|
||||||
|
li_wildcard = "67/20"
|
||||||
|
mcon_wildcard = "67/44"
|
||||||
|
|
||||||
|
m1_wildcard = "68/20"
|
||||||
|
via_wildcard = "68/44"
|
||||||
|
|
||||||
|
m2_wildcard = "69/20"
|
||||||
|
via2_wildcard = "69/44"
|
||||||
|
|
||||||
|
m3_wildcard = "70/20"
|
||||||
|
via3_wildcard = "70/44"
|
||||||
|
|
||||||
|
m4_wildcard = "71/20"
|
||||||
|
via4_wildcard = "71/44"
|
||||||
|
|
||||||
|
m5_wildcard = "72/20"
|
||||||
|
|
||||||
|
nsdm_wildcard = "93/44"
|
||||||
|
|
||||||
|
psdm_wildcard = "94/20"
|
||||||
|
nwell_wildcard = "64/20"
|
||||||
|
|
||||||
|
diff = input(65, 20)
|
||||||
|
tap = polygons(65, 44)
|
||||||
|
nwell = polygons(nwell_wildcard)
|
||||||
|
dnwell = polygons(64, 18)
|
||||||
|
pwbm = polygons(19, 44)
|
||||||
|
pwde = polygons(124, 20)
|
||||||
|
natfet = polygons(124, 21)
|
||||||
|
hvtr = polygons(18, 20)
|
||||||
|
hvtp = polygons(78, 44)
|
||||||
|
ldntm = polygons(11, 44)
|
||||||
|
hvi = polygons(75, 20)
|
||||||
|
tunm = polygons(80, 20)
|
||||||
|
lvtn = polygons(125, 44)
|
||||||
|
poly = polygons(66, 20)
|
||||||
|
hvntm = polygons(125, 20)
|
||||||
|
nsdm = polygons(nsdm_wildcard)
|
||||||
|
psdm = polygons(psdm_wildcard)
|
||||||
|
rpm = polygons(86, 20)
|
||||||
|
urpm = polygons(79, 20)
|
||||||
|
npc = polygons(95, 20)
|
||||||
|
licon = polygons(66, 44)
|
||||||
|
|
||||||
|
li = polygons(li_wildcard)
|
||||||
|
mcon = polygons(mcon_wildcard)
|
||||||
|
|
||||||
|
m1 = polygons(m1_wildcard)
|
||||||
|
via = polygons(via_wildcard)
|
||||||
|
|
||||||
|
m2 = polygons(m2_wildcard)
|
||||||
|
via2 = polygons(via2_wildcard)
|
||||||
|
|
||||||
|
m3 = polygons(m3_wildcard)
|
||||||
|
via3 = polygons(via3_wildcard)
|
||||||
|
|
||||||
|
m4 = polygons(m4_wildcard)
|
||||||
|
via4 = polygons(via4_wildcard)
|
||||||
|
|
||||||
|
m5 = polygons(m5_wildcard)
|
||||||
|
|
||||||
|
pad = polygons(76, 20)
|
||||||
|
nsm = polygons(61, 20)
|
||||||
|
capm = polygons(89, 44)
|
||||||
|
cap2m = polygons(97, 44)
|
||||||
|
vhvi = polygons(74, 21)
|
||||||
|
uhvi = polygons(74, 22)
|
||||||
|
npn = polygons(82, 20)
|
||||||
|
inductor = polygons(82, 24)
|
||||||
|
vpp = polygons(82, 64)
|
||||||
|
pnp = polygons(82, 44)
|
||||||
|
lvs_prune = polygons(84, 44)
|
||||||
|
ncm = polygons(92, 44)
|
||||||
|
padcenter = polygons(81, 20)
|
||||||
|
mf = polygons(76, 44)
|
||||||
|
areaid_sl = polygons(81, 1)
|
||||||
|
areaid_ce = polygons(81, 2)
|
||||||
|
areaid_fe = polygons(81, 3)
|
||||||
|
areaid_sc = polygons(81, 4)
|
||||||
|
areaid_sf = polygons(81, 6)
|
||||||
|
areaid_sw = polygons(81, 7)
|
||||||
|
areaid_sr = polygons(81, 8)
|
||||||
|
areaid_mt = polygons(81, 10)
|
||||||
|
areaid_dt = polygons(81, 11)
|
||||||
|
areaid_ft = polygons(81, 12)
|
||||||
|
areaid_ww = polygons(81, 13)
|
||||||
|
areaid_ld = polygons(81, 14)
|
||||||
|
areaid_ns = polygons(81, 15)
|
||||||
|
areaid_ij = polygons(81, 17)
|
||||||
|
areaid_zr = polygons(81, 18)
|
||||||
|
areaid_ed = polygons(81, 19)
|
||||||
|
areaid_de = polygons(81, 23)
|
||||||
|
areaid_rd = polygons(81, 24)
|
||||||
|
areaid_dn = polygons(81, 50)
|
||||||
|
areaid_cr = polygons(81, 51)
|
||||||
|
areaid_cd = polygons(81, 52)
|
||||||
|
areaid_st = polygons(81, 53)
|
||||||
|
areaid_op = polygons(81, 54)
|
||||||
|
areaid_en = polygons(81, 57)
|
||||||
|
areaid_en20 = polygons(81, 58)
|
||||||
|
areaid_le = polygons(81, 60)
|
||||||
|
areaid_hl = polygons(81, 63)
|
||||||
|
areaid_sd = polygons(81, 70)
|
||||||
|
areaid_po = polygons(81, 81)
|
||||||
|
areaid_it = polygons(81, 84)
|
||||||
|
areaid_et = polygons(81, 101)
|
||||||
|
areaid_lvt = polygons(81, 108)
|
||||||
|
areaid_re = polygons(81, 125)
|
||||||
|
areaid_ag = polygons(81, 79)
|
||||||
|
poly_rs = polygons(66, 13)
|
||||||
|
diff_rs = polygons(65, 13)
|
||||||
|
pwell_rs = polygons(64, 13)
|
||||||
|
li_rs = polygons(67, 13)
|
||||||
|
cfom = polygons(22, 20)
|
||||||
|
|
||||||
|
|
||||||
|
# Define a new custom function that selects polygons by their number of holes:
|
||||||
|
# It will return a new layer containing those polygons with min to max holes.
|
||||||
|
# max can be nil to omit the upper limit.
|
||||||
|
class DRC::DRCLayer
|
||||||
|
def with_holes(min, max)
|
||||||
|
new_data = RBA::Region::new
|
||||||
|
self.data.each do |p|
|
||||||
|
if p.holes >= (min || 0) && (!max || p.holes <= max)
|
||||||
|
new_data.insert(p)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
DRC::DRCLayer::new(@engine, new_data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# DRC section
|
||||||
|
########################
|
||||||
|
log("DRC section")
|
||||||
|
|
||||||
|
if FEOL
|
||||||
|
log("FEOL section")
|
||||||
|
# dnwell
|
||||||
|
log("START: 64/18 (dnwell)")
|
||||||
|
dnwell.width(3.0, euclidian).output("dnwell.2", "dnwell.2 : min. dnwell width : 3.0um")
|
||||||
|
log("END: 64/18 (dnwell)")
|
||||||
|
|
||||||
|
not_sram = layout(source.cell_obj).select("-*sky130_sram_*kbyte_*")
|
||||||
|
not_sram_nsdm = not_sram.input(nsdm_wildcard)
|
||||||
|
not_sram_psdm = not_sram.input(psdm_wildcard)
|
||||||
|
not_sram_nwell = not_sram.input(nwell_wildcard)
|
||||||
|
|
||||||
|
# This is a hack, should be reverted
|
||||||
|
|
||||||
|
not_io = layout(source.cell_obj).select("-*sky130_fd_io__gpiov2_amux", "-*sky130_fd_io__simple_pad_and_busses")
|
||||||
|
not_io_nwell = not_io.input(nwell_wildcard)
|
||||||
|
|
||||||
|
# nwell
|
||||||
|
log("START: 64/20 (nwell)")
|
||||||
|
nwell.width(0.84, euclidian).output("nwell.1", "nwell.1 : min. nwell width : 0.84um")
|
||||||
|
nwell.space(1.27, euclidian).output("nwell.2a", "nwell.2a : min. nwell spacing (merged if less) : 1.27um")
|
||||||
|
nwell_interact = not_sram_nwell.and(not_io_nwell).merge
|
||||||
|
dnwell.enclosing(nwell_interact.holes, 1.03, euclidian).output("nwell.6", "nwell.6 : min enclosure of nwellHole by dnwell : 1.03um")
|
||||||
|
log("END: 64/20 (nwell)")
|
||||||
|
|
||||||
|
# hvtp
|
||||||
|
log("START: 78/44 (hvtp)")
|
||||||
|
hvtp.width(0.38, euclidian).output("hvtp.1", "hvtp.1 : min. hvtp width : 0.38um")
|
||||||
|
hvtp.space(0.38, euclidian).output("hvtp.2", "hvtp.2 : min. hvtp spacing : 0.38um")
|
||||||
|
log("END: 78/44 (hvtp)")
|
||||||
|
|
||||||
|
# hvtr
|
||||||
|
log("START: 18/20 (htvr)")
|
||||||
|
hvtr.width(0.38, euclidian).output("hvtr.1", "hvtr.1 : min. hvtr width : 0.38um")
|
||||||
|
hvtr.separation(hvtp, 0.38, euclidian).output("hvtr.2", "hvtr.2 : min. hvtr spacing : 0.38um")
|
||||||
|
hvtr.and(hvtp).output("hvtr.2_a", "hvtr.2_a : hvtr must not overlap hvtp")
|
||||||
|
log("END: 18/20 (htvr)")
|
||||||
|
|
||||||
|
# lvtn
|
||||||
|
log("START: 25/44 (lvtn)")
|
||||||
|
lvtn.width(0.38, euclidian).output("lvtn.1a", "lvtn.1a : min. lvtn width : 0.38um")
|
||||||
|
lvtn.space(0.38, euclidian).output("lvtn.2", "lvtn.2 : min. lvtn spacing : 0.38um")
|
||||||
|
log("END: 25/44 (lvtn)")
|
||||||
|
|
||||||
|
# ncm
|
||||||
|
log("START: 92/44 (ncm)")
|
||||||
|
ncm.width(0.38, euclidian).output("ncm.1", "ncm.1 : min. ncm width : 0.38um")
|
||||||
|
ncm.space(0.38, euclidian).output("ncm.2a", "ncm.2a : min. ncm spacing : 0.38um")
|
||||||
|
log("END: 92/44 (ncm)")
|
||||||
|
|
||||||
|
# diff-tap
|
||||||
|
log("START: 65/20 (diff)")
|
||||||
|
difftap = diff.or(tap)
|
||||||
|
diff_width = diff.rectangles.width(0.15, euclidian).polygons
|
||||||
|
diff_cross_areaid_ce = diff_width.edges.outside_part(areaid_ce).not(diff_width.outside(areaid_ce).edges)
|
||||||
|
diff_cross_areaid_ce.output("difftap.1", "difftap.1 : min. diff width across areaid:ce : 0.15um")
|
||||||
|
diff.outside(areaid_ce).width(0.15, euclidian).output("difftap.1_a", "difftap.1_a : min. diff width in periphery : 0.15um")
|
||||||
|
log("END: 65/20 (diff)")
|
||||||
|
|
||||||
|
log("START: 65/44 (tap)")
|
||||||
|
tap_width = tap.rectangles.width(0.15, euclidian).polygons
|
||||||
|
tap_cross_areaid_ce = tap_width.edges.outside_part(areaid_ce).not(tap_width.outside(areaid_ce).edges)
|
||||||
|
tap_cross_areaid_ce.output("difftap.1_b", "difftap.1_b : min. tap width across areaid:ce : 0.15um")
|
||||||
|
tap.not(areaid_ce).width(0.15, euclidian).output("difftap.1_c", "difftap.1_c : min. tap width in periphery : 0.15um")
|
||||||
|
log("END: 65/44 (tap)")
|
||||||
|
|
||||||
|
difftap.space(0.27, euclidian).output("difftap.3", "difftap.3 : min. difftap spacing : 0.27um")
|
||||||
|
|
||||||
|
# tunm
|
||||||
|
log("START: 80/20 (tunm)")
|
||||||
|
tunm.width(0.41, euclidian).output("tunm.1", "tunm.1 : min. tunm width : 0.41um")
|
||||||
|
tunm.space(0.5, euclidian).output("tunm.2", "tunm.2 : min. tunm spacing : 0.5um")
|
||||||
|
log("END: 80/20 (tunm)")
|
||||||
|
|
||||||
|
# poly
|
||||||
|
log("START: 66/20 (poly)")
|
||||||
|
poly.width(0.15, euclidian).output("poly.1a", "poly.1a : min. poly width : 0.15um")
|
||||||
|
poly.not(areaid_ce).space(0.21, euclidian).output("poly.2", "poly.2 : min. poly spacing : 0.21um")
|
||||||
|
|
||||||
|
|
||||||
|
# rpm
|
||||||
|
log("START: 86/20 (rpm)")
|
||||||
|
rpm.width(1.27, euclidian).output("rpm.1a", "rpm.1a : min. rpm width : 1.27um")
|
||||||
|
rpm.space(0.84, euclidian).output("rpm.2", "rpm.2 : min. rpm spacing : 0.84um")
|
||||||
|
log("END: 86/20 (rpm)")
|
||||||
|
|
||||||
|
# urpm
|
||||||
|
log("START: 79/20 (urpm)")
|
||||||
|
urpm.width(1.27, euclidian).output("urpm.1a", "urpm.1a : min. rpm width : 1.27um")
|
||||||
|
urpm.space(0.84, euclidian).output("urpm.2", "urpm.2 : min. rpm spacing : 0.84um")
|
||||||
|
log("END: 79/20 (urpm)")
|
||||||
|
|
||||||
|
# npc
|
||||||
|
log("START: 95/20 (npc)")
|
||||||
|
npc.width(0.27, euclidian).output("npc.1", "npc.1 : min. npc width : 0.27um")
|
||||||
|
npc.space(0.27, euclidian).output("npc.2", "npc.2 : min. npc spacing, should be manually merged if less than : 0.27um")
|
||||||
|
log("END: 95/20 (npc)")
|
||||||
|
|
||||||
|
# nsdm
|
||||||
|
log("START: 93/44 (nsdm)")
|
||||||
|
not_sram_nsdm.outside(areaid_ce).width(0.38, euclidian).output("nsd.1", "nsd.1 : min. nsdm width : 0.38um")
|
||||||
|
not_sram_nsdm.not(areaid_ce).space(0.38, euclidian).output("nsd.2", "nsd.2 : min. nsdm spacing, should be manually merged if less than : 0.38um")
|
||||||
|
log("END: 93/44 (nsdm)")
|
||||||
|
|
||||||
|
# psdm
|
||||||
|
log("START: 94/20 (psdm)")
|
||||||
|
not_sram_psdm.outside(areaid_ce).width(0.38, euclidian).output("psd.1", "psd.1 : min. psdm width : 0.38um")
|
||||||
|
not_sram_psdm.not(areaid_ce).space(0.38, euclidian).output("psd.2", "psd.2 : min. psdm spacing, should be manually merged if less than : 0.38um")
|
||||||
|
log("END: 94/20 (psdm)")
|
||||||
|
|
||||||
|
# licon
|
||||||
|
log("START: 66/44 (licon)")
|
||||||
|
if SEAL
|
||||||
|
ringLICON = licon.drc(with_holes > 0)
|
||||||
|
rectLICON = licon.not(ringLICON)
|
||||||
|
else
|
||||||
|
rectLICON = licon
|
||||||
|
end
|
||||||
|
xfom = difftap.not(poly)
|
||||||
|
licon1ToXfom = licon.interacting(licon.and(xfom))
|
||||||
|
licon1ToXfom_PERI = licon1ToXfom.not(areaid_ce)
|
||||||
|
rectLICON.non_rectangles.output("licon.1", "licon.1 : licon should be rectangle")
|
||||||
|
rectLICON.not(rpm.or(urpm)).edges.without_length(0.17).output("licon.1_a/b", "licon.1_a/b : minimum/maximum width of licon : 0.17um")
|
||||||
|
licon1ToXfom_PERI.separation(npc, 0.09, euclidian).output("licon.13", "licon.13 : min. difftap licon spacing to npc : 0.09um")
|
||||||
|
licon1ToXfom_PERI.and(npc).output("licon.13_a", "licon.13_a : licon of diffTap in periphery must not overlap npc")
|
||||||
|
licon.interacting(poly).and(licon.interacting(difftap)).output("licon.17", "licon.17 : Licons may not overlap both poly and (diff or tap)")
|
||||||
|
log("END: 66/44 (licon)")
|
||||||
|
|
||||||
|
# CAPM
|
||||||
|
log("START: 89/44 (capm)")
|
||||||
|
capm.width(1.0, euclidian).output("capm.1", "capm.1 : min. capm width : 1.0um")
|
||||||
|
capm.space(0.84, euclidian).output("capm.2a", "capm.2a : min. capm spacing : 0.84um")
|
||||||
|
m3.interacting(capm).isolated(1.2, euclidian).output("capm.2b", "capm.2b : min. capm spacing : 1.2um")
|
||||||
|
(m3.interacting(capm)).isolated(1.2, euclidian).output("capm.2b_a", "capm.2b_a : min. spacing of m3_bot_plate : 1.2um")
|
||||||
|
capm.and(m3).enclosing(m3, 0.14, euclidian).output("capm.3", "capm.3 : min. capm and m3 enclosure of m3 : 0.14um")
|
||||||
|
m3.enclosing(capm, 0.14, euclidian).output("capm.3_a", "capm.3_a : min. m3 enclosure of capm : 0.14um")
|
||||||
|
capm.enclosing(via3, 0.14, euclidian).output("capm.4", "capm.4 : min. capm enclosure of via3 : 0.14um")
|
||||||
|
capm.separation(via3, 0.14, euclidian).output("capm.5", "capm.5 : min. capm spacing to via3 : 0.14um")
|
||||||
|
(m3.not_interacting(capm)).separation(capm, 0.5, euclidian).output("capm.11", "capm.11 : Min spacing of capm and met3 not overlapping capm : 0.5um")
|
||||||
|
log("END: 89/44 (capm)")
|
||||||
|
|
||||||
|
# CAP2M
|
||||||
|
log("START: 97/44 (cap2m)")
|
||||||
|
cap2m.width(1.0, euclidian).output("cap2m.1", "cap2m.1 : min. cap2m width : 1.0um")
|
||||||
|
cap2m.space(0.84, euclidian).output("cap2m.2a", "cap2m.2a : min. cap2m spacing : 0.84um")
|
||||||
|
m4.interacting(cap2m).isolated(1.2, euclidian).output("cap2m.2b", "cap2m.2b : min. cap2m spacing : 1.2um")
|
||||||
|
(m4.interacting(cap2m)).isolated(1.2, euclidian).output("cap2m.2b_a", "cap2m.2b_a : min. spacing of m4_bot_plate : 1.2um")
|
||||||
|
cap2m.and(m4).enclosing(m4, 0.14, euclidian).output("cap2m.3", "cap2m.3 : min. m4 enclosure of cap2m : 0.14um")
|
||||||
|
m4.enclosing(cap2m, 0.14, euclidian).output("cap2m.3_a", "cap2m.3_a : min. m4 enclosure of cap2m : 0.14um")
|
||||||
|
cap2m.enclosing(via4, 0.2, euclidian).output("cap2m.4", "cap2m.4 : min. cap2m enclosure of via4 : 0.14um")
|
||||||
|
cap2m.separation(via4, 0.2, euclidian).output("cap2m.5", "cap2m.5 : min. cap2m spacing to via4 : 0.14um")
|
||||||
|
(m4.not_interacting(cap2m)).separation(cap2m, 0.5, euclidian).output("cap2m.11", "cap2m.11 : Min spacing of cap2m and met4 not overlapping cap2m : 0.5um")
|
||||||
|
log("END: 97/44 (cap2m)")
|
||||||
|
end #FEOL
|
||||||
|
|
||||||
|
if BEOL
|
||||||
|
log("BEOL section")
|
||||||
|
|
||||||
|
# li
|
||||||
|
log("START: 67/20 (li)")
|
||||||
|
linotace = li.not(li.interacting(areaid_ce))
|
||||||
|
linotace.width(0.17, euclidian).output("li.1", "li.1 : min. li width : 0.17um")
|
||||||
|
# This rule is taking a long time in some slots
|
||||||
|
linotace.edges.space(0.17, euclidian).output("li.3", "li.3 : min. li spacing : 0.17um")
|
||||||
|
licon_peri = licon.not(areaid_ce)
|
||||||
|
li_edges_with_less_enclosure = li.enclosing(licon_peri, 0.08, projection).second_edges
|
||||||
|
error_corners = li_edges_with_less_enclosure.width(angle_limit(100.0), 1.dbu)
|
||||||
|
li_interact = licon_peri.interacting(error_corners.polygons(1.dbu))
|
||||||
|
li_interact.output("li.5", "li.5 : min. li enclosure of licon of 2 adjacent edges : 0.08um")
|
||||||
|
linotace.with_area(nil, 0.0561).output("li.6", "li.6 : min. li area : 0.0561um²")
|
||||||
|
log("END: 67/20 (li)")
|
||||||
|
|
||||||
|
# ct
|
||||||
|
log("START: 67/44 (mcon)")
|
||||||
|
mconnotace = mcon.not(areaid_ce)
|
||||||
|
if SEAL
|
||||||
|
ringMCON = mcon.drc(with_holes > 0)
|
||||||
|
rectMCON = mcon.not(ringMCON)
|
||||||
|
else
|
||||||
|
rectMCON = mcon
|
||||||
|
end
|
||||||
|
rectMCON_peri = rectMCON.not(areaid_ce)
|
||||||
|
rectMCON.non_rectangles.output("ct.1", "ct.1: non-ring mcon should be rectangular")
|
||||||
|
# rectMCON_peri.edges.without_length(0.17).output("ct.1_a/b", "ct.1_a/b : minimum/maximum width of mcon : 0.17um")
|
||||||
|
rectMCON_peri.drc(width < 0.17).output("ct.1_a", "ct.1_a : minimum width of mcon : 0.17um")
|
||||||
|
rectMCON_peri.drc(length > 0.17).output("ct.1_b", "ct.1_b : maximum length of mcon : 0.17um")
|
||||||
|
mcon.space(0.19, euclidian).output("ct.2", "ct.2 : min. mcon spacing : 0.19um")
|
||||||
|
if SEAL
|
||||||
|
ringMCON.width(0.17, euclidian).output("ct.3", "ct.3 : min. width of ring-shaped mcon : 0.17um")
|
||||||
|
ringMCON.drc(width >= 0.175).output("ct.3_a", "ct.3_a : max. width of ring-shaped mcon : 0.175um")
|
||||||
|
ringMCON.not(areaid_sl).output("ct.3_b", "ct.3_b: ring-shaped mcon must be enclosed by areaid_sl")
|
||||||
|
end
|
||||||
|
mconnotace.not(li).output("ct.4", "ct.4 : mcon should covered by li")
|
||||||
|
log("END: 67/44 (mcon)")
|
||||||
|
|
||||||
|
# m1
|
||||||
|
log("START: 68/20 (m1)")
|
||||||
|
m1.width(0.14, euclidian).output("m1.1", "m1.1 : min. m1 width : 0.14um")
|
||||||
|
huge_m1 = m1.sized(-1.5).sized(1.5).snap(0.005) & m1
|
||||||
|
non_huge_m1 = m1.edges - huge_m1
|
||||||
|
huge_m1 = huge_m1.edges.outside_part(m1.merged)
|
||||||
|
|
||||||
|
non_huge_m1.space(0.14, euclidian).output("m1.2", "m1.2 : min. m1 spacing : 0.14um")
|
||||||
|
|
||||||
|
(huge_m1.separation(non_huge_m1, 0.28, euclidian) + huge_m1.space(0.28, euclidian)).output("m1.3ab", "m1.3ab : min. 3um.m1 spacing m1 : 0.28um")
|
||||||
|
|
||||||
|
#not_in_cell6 = layout(source.cell_obj).select("-s8cell_ee_plus_sseln_a", "-s8cell_ee_plus_sseln_b", "-s8cell_ee_plus_sselp_a", "-s8cell_ee_plus_sselp_b", "-s8fpls_pl8", "-s8fs_cmux4_fm")
|
||||||
|
not_in_cell6 = layout(source.cell_obj).select("-s8cell_ee_plus_sseln_a", "-s8cell_ee_plus_sseln_b", "-s8cell_ee_plus_sselp_a", "-s8cell_ee_plus_sselp_b", "-s8fs_cmux4_fm")
|
||||||
|
not_in_cell6_m1 = not_in_cell6.input(m1_wildcard)
|
||||||
|
|
||||||
|
not_in_cell6_m1.enclosing(mconnotace, 0.03, euclidian).output("791_m1.4", "791_m1.4 : min. m1 enclosure of mcon : 0.03um")
|
||||||
|
mconnotace.not(m1).output("m1.4", "m1.4 : mcon periphery must be enclosed by m1")
|
||||||
|
in_cell6 = layout(source.cell_obj).select("-*", "+s8cell_ee_plus_sseln_a", "+s8cell_ee_plus_sseln_b", "+s8cell_ee_plus_sselp_a", "+s8cell_ee_plus_sselp_b", "+s8fpls_pl8", "+s8fs_cmux4_fm")
|
||||||
|
in_cell6_m1 = in_cell6.input(m1_wildcard)
|
||||||
|
in_cell6_m1.enclosing(mcon, 0.005, euclidian).output("m1.4a", "m1.4a : min. m1 enclosure of mcon for specific cells : 0.005um")
|
||||||
|
|
||||||
|
in_cell6_m1.not(m1).output('m1.4a_a', 'm1.4a_a : mcon periph must be enclosed by met1 for specific cells')
|
||||||
|
|
||||||
|
m1.with_area(0..0.083).output("m1.6", "m1.6 : min. m1 area : 0.083um²")
|
||||||
|
|
||||||
|
m1.holes.with_area(0..0.14).output("m1.7", "m1.7 : min. m1 with holes area : 0.14um²")
|
||||||
|
if FLOATING_MET
|
||||||
|
m1.not_interacting(via.or(mcon)).output("m1.x", "floating met1, must interact with via1")
|
||||||
|
end
|
||||||
|
|
||||||
|
if backend_flow = AL
|
||||||
|
#Could flag false positive, fix would be to add .rectangles for m1
|
||||||
|
mconnotace_edges_with_less_enclosure_m1 = m1.enclosing(mconnotace, 0.06, projection).second_edges
|
||||||
|
error_corners_m1 = mconnotace_edges_with_less_enclosure_m1.width(angle_limit(100.0), 1.dbu)
|
||||||
|
mconnotace_interact_m1 = mconnotace.interacting(error_corners_m1.polygons(1.dbu))
|
||||||
|
mconnotace_interact_m1.output("m1.5", "m1.5 : min. m1 enclosure of mcon of 2 adjacent edges : 0.06um")
|
||||||
|
end
|
||||||
|
log("END: 68/20 (m1)")
|
||||||
|
|
||||||
|
# via
|
||||||
|
log("START: 68/44 (via)")
|
||||||
|
if backend_flow = AL
|
||||||
|
if SEAL
|
||||||
|
ringVIA = via.drc(with_holes > 0)
|
||||||
|
rectVIA = via.not(ringVIA)
|
||||||
|
else
|
||||||
|
rectVIA = via
|
||||||
|
end
|
||||||
|
|
||||||
|
via_not_mt = rectVIA.not(areaid_mt)
|
||||||
|
|
||||||
|
via_not_mt.non_rectangles.output("via.1a", "via.1a : via outside of moduleCut should be rectangular")
|
||||||
|
via_not_mt.width(0.15, euclidian).output("via.1a_a", "via.1a_a : min. width of via outside of moduleCut : 0.15um")
|
||||||
|
# via_not_mt.edges.without_length(nil, 0.15 + 1.dbu).output("via.1a_b", "via.1a_b : maximum length of via : 0.15um")
|
||||||
|
via_not_mt.drc(length > 0.15).output("via.1a_b", "via.1a_b : maximum length of via : 0.15um")
|
||||||
|
|
||||||
|
via.space(0.17, euclidian).output("via.2", "via.2 : min. via spacing : 0.17um")
|
||||||
|
|
||||||
|
if SEAL
|
||||||
|
ringVIA.width(0.2, euclidian).output("via.3", "via.3 : min. width of ring-shaped via : 0.2um")
|
||||||
|
ringVIA.drc(width >= 0.205).output("via.3_a", "via.3_a : max. width of ring-shaped via : 0.205um")
|
||||||
|
ringVIA.not(areaid_sl).output("via.3_b", "via.3_b: ring-shaped via must be enclosed by areaid_sl")
|
||||||
|
end
|
||||||
|
|
||||||
|
m1.edges.enclosing(rectVIA.drc(width == 0.15), 0.055, euclidian).output("via.4a", "via.4a : min. m1 enclosure of 0.15um via : 0.055um")
|
||||||
|
rectVIA.squares.drc(width == 0.15).not(m1).output("via.4a_a", "via.4a_a : 0.15um via must be enclosed by met1")
|
||||||
|
|
||||||
|
via1_edges_with_less_enclosure_m1 = m1.edges.enclosing(rectVIA.drc(width == 0.15), 0.085, projection).second_edges
|
||||||
|
error_corners_via1 = via1_edges_with_less_enclosure_m1.width(angle_limit(100.0), 1.dbu)
|
||||||
|
via2_interact = via.interacting(error_corners_via1.polygons(1.dbu))
|
||||||
|
via2_interact.output("via.5a", "via.5a : min. m1 enclosure of 0.15um via of 2 adjacent edges : 0.085um")
|
||||||
|
|
||||||
|
end
|
||||||
|
log("END: 68/44 (via)")
|
||||||
|
|
||||||
|
# m2
|
||||||
|
log("START: 69/20 (m2)")
|
||||||
|
m2.width(0.14, euclidian).output("m2.1", "m2.1 : min. m2 width : 0.14um")
|
||||||
|
|
||||||
|
huge_m2 = m2.sized(-1.5).sized(1.5).snap(0.005) & m2
|
||||||
|
non_huge_m2 = m2.edges - huge_m2
|
||||||
|
huge_m2 = huge_m2.edges.outside_part(m2.merged)
|
||||||
|
via_outside_periphery = via.not(areaid_ce)
|
||||||
|
|
||||||
|
non_huge_m2.space(0.14, euclidian).output("m2.2", "m2.2 : min. m2 spacing : 0.14um")
|
||||||
|
|
||||||
|
(huge_m2.separation(non_huge_m2, 0.28, euclidian) + huge_m2.space(0.28, euclidian)).output("m2.3ab", "m2.3ab : min. 3um.m2 spacing m2 : 0.28um")
|
||||||
|
|
||||||
|
m2.with_area(0..0.0676).output("m2.6", "m2.6 : min. m2 area : 0.0676um²")
|
||||||
|
m2.holes.with_area(0..0.14).output("m2.7", "m2.7 : min. m2 holes area : 0.14um²")
|
||||||
|
if FLOATING_MET
|
||||||
|
m2.not_interacting(via.or(via2)).output("m2.x", "floating met2, must interact with via1 or via2")
|
||||||
|
end
|
||||||
|
if backend_flow = AL
|
||||||
|
m2.enclosing(via_outside_periphery, 0.055, euclidian).output("m2.4", "m2.4 : min. m2 enclosure of via : 0.055um")
|
||||||
|
via_outside_periphery.not(m2).output("m2.4_a", "m2.4_a : via in periphery must be enclosed by met2")
|
||||||
|
via_edges_with_less_enclosure_m2 = m2.enclosing(via, 0.085, projection).second_edges
|
||||||
|
error_corners = via_edges_with_less_enclosure_m2.width(angle_limit(100.0), 1.dbu)
|
||||||
|
via_interact = via.interacting(error_corners.polygons(1.dbu))
|
||||||
|
via_interact.output("m2.5", "m2.5 : min. m2 enclosure of via of 2 adjacent edges : 0.085um")
|
||||||
|
|
||||||
|
end
|
||||||
|
log("END: 69/20 (m2)")
|
||||||
|
|
||||||
|
# via2
|
||||||
|
log("START: 69/44 (via2)")
|
||||||
|
if backend_flow = AL
|
||||||
|
if SEAL
|
||||||
|
ringVIA2 = via2.drc(with_holes > 0)
|
||||||
|
rectVIA2 = via2.not(ringVIA2)
|
||||||
|
else
|
||||||
|
rectVIA2 = via2
|
||||||
|
end
|
||||||
|
|
||||||
|
via2_not_mt = rectVIA2.not(areaid_mt)
|
||||||
|
via2_not_mt.non_rectangles.output("via2.1a", "via2.1a : via2 outside of moduleCut should be rectangular")
|
||||||
|
via2_not_mt.width(0.2, euclidian).output("via2.1a_a", "via2.1a_a : min. width of via2 outside of moduleCut : 0.2um")
|
||||||
|
via2_not_mt.edges.without_length(nil, 0.2 + 1.dbu).output("via2.1a_b", "via2.1a_b : maximum length of via2 : 0.2um")
|
||||||
|
via2.space(0.2, euclidian).output("via2.2", "via2.2 : min. via2 spacing : 0.2um")
|
||||||
|
|
||||||
|
if SEAL
|
||||||
|
ringVIA2.width(0.2, euclidian).output("via2.3", "via2.3 : min. width of ring-shaped via2 : 0.2um")
|
||||||
|
ringVIA2.drc(width >= 0.205).output("via2.3_a", "via2.3_a : max. width of ring-shaped via2 : 0.205um")
|
||||||
|
ringVIA2.not(areaid_sl).output("via2.3_b", "via2.3_b: ring-shaped via2 must be enclosed by areaid_sl")
|
||||||
|
end
|
||||||
|
|
||||||
|
m2.enclosing(via2, 0.04, euclidian).output("via2.4", "via2.4 : min. m2 enclosure of via2 : 0.04um")
|
||||||
|
via2.not(m2).output("via2.4_a", "via2.4_a : via must be enclosed by met2")
|
||||||
|
|
||||||
|
via2_edges_with_less_enclosure = m2.enclosing(via2, 0.085, projection).second_edges
|
||||||
|
error_corners = via2_edges_with_less_enclosure.width(angle_limit(100.0), 1.dbu)
|
||||||
|
via2_interact = via2.interacting(error_corners.polygons(1.dbu))
|
||||||
|
via2_interact.output("via2.5", "via2.5 : min. m3 enclosure of via2 of 2 adjacent edges : 0.085um")
|
||||||
|
end
|
||||||
|
log("END: 69/44 (via2)")
|
||||||
|
|
||||||
|
# m3
|
||||||
|
log("START: 70/20 (m3)")
|
||||||
|
m3.width(0.3, euclidian).output("m3.1", "m3.1 : min. m3 width : 0.3um")
|
||||||
|
|
||||||
|
huge_m3 = m3.sized(-1.5).sized(1.5).snap(0.005) & m3
|
||||||
|
non_huge_m3 = m3.edges - huge_m3
|
||||||
|
huge_m3 = huge_m3.edges.outside_part(m3.merged)
|
||||||
|
|
||||||
|
non_huge_m3.space(0.3, euclidian).output("m3.2", "m3.2 : min. m3 spacing : 0.3um")
|
||||||
|
|
||||||
|
m3.with_area(0..0.240).output("m3.6", "m3.6 : min. m3 area : 0.240um²")
|
||||||
|
|
||||||
|
(huge_m3.separation(non_huge_m3, 0.4, euclidian) + huge_m3.space(0.4, euclidian)).output("m3.3cd", "m3.3cd : min. 3um.m3 spacing m3 : 0.4um")
|
||||||
|
if FLOATING_MET
|
||||||
|
m3.not_interacting(via2.or(via3)).output("m3.x", "floating met3, must interact with via2 or via3")
|
||||||
|
end
|
||||||
|
if backend_flow = AL
|
||||||
|
m3.enclosing(via2, 0.065, euclidian).output("m3.4", "m3.4 : min. m3 enclosure of via2 : 0.065um")
|
||||||
|
via2.not(m3).output("m3.4_a", "m3.4_a : via2 must be enclosed by met3")
|
||||||
|
end
|
||||||
|
log("END: 70/20 (m3)")
|
||||||
|
|
||||||
|
# via3
|
||||||
|
log("START: 70/44 (via3)")
|
||||||
|
if backend_flow = AL
|
||||||
|
if SEAL
|
||||||
|
ringVIA3 = via3.drc(with_holes > 0)
|
||||||
|
rectVIA3 = via3.not(ringVIA3)
|
||||||
|
else
|
||||||
|
rectVIA3 = via3
|
||||||
|
end
|
||||||
|
|
||||||
|
via3_not_mt = rectVIA3.not(areaid_mt)
|
||||||
|
via3_not_mt.non_rectangles.output("via3.1", "via3.1 : via3 outside of moduleCut should be rectangular")
|
||||||
|
via3_not_mt.width(0.2, euclidian).output("via3.1_a", "via3.1_a : min. width of via3 outside of moduleCut : 0.2um")
|
||||||
|
via3_not_mt.edges.without_length(nil, 0.2 + 1.dbu).output("via3.1_b", "via3.1_b : maximum length of via3 : 0.2um")
|
||||||
|
|
||||||
|
via3.space(0.2, euclidian).output("via3.2", "via3.2 : min. via3 spacing : 0.2um")
|
||||||
|
m3.enclosing(via3, 0.06, euclidian).output("via3.4", "via3.4 : min. m3 enclosure of via3 : 0.06um")
|
||||||
|
rectVIA3.not(m3).output("via3.4_a", "via3.4_a : non-ring via3 must be enclosed by met3")
|
||||||
|
|
||||||
|
via_edges_with_less_enclosure = m3.enclosing(via3, 0.09, projection).second_edges
|
||||||
|
error_corners = via_edges_with_less_enclosure.width(angle_limit(100.0), 1.dbu)
|
||||||
|
via3_interact = via3.interacting(error_corners.polygons(1.dbu))
|
||||||
|
via3_interact.output("via3.5", "via3.5 : min. m3 enclosure of via3 of 2 adjacent edges : 0.09um")
|
||||||
|
end
|
||||||
|
log("END: 70/44 (via3)")
|
||||||
|
|
||||||
|
# m4
|
||||||
|
log("START: 71/20 (m4)")
|
||||||
|
m4.width(0.3, euclidian).output("m4.1", "m4.1 : min. m4 width : 0.3um")
|
||||||
|
|
||||||
|
huge_m4 = m4.sized(-1.5).sized(1.5).snap(0.005) & m4
|
||||||
|
non_huge_m4 = m4.edges - huge_m4
|
||||||
|
huge_m4 = huge_m4.edges.outside_part(m4.merged)
|
||||||
|
|
||||||
|
non_huge_m4.space(0.3, euclidian).output("m4.2", "m4.2 : min. m4 spacing : 0.3um")
|
||||||
|
|
||||||
|
m4.with_area(0..0.240).output("m4.4a", "m4.4a : min. m4 area : 0.240um²")
|
||||||
|
|
||||||
|
(huge_m4.separation(non_huge_m4, 0.4, euclidian) + huge_m4.space(0.4, euclidian)).output("m4.5ab", "m4.5ab : min. 3um.m4 spacing m4 : 0.4um")
|
||||||
|
if FLOATING_MET
|
||||||
|
m4.not_interacting(via3.or(via4)).output("m4.x", "floating met3, must interact with via3 or via4")
|
||||||
|
end
|
||||||
|
if backend_flow = AL
|
||||||
|
m4.enclosing(via3, 0.065, euclidian).output("m4.3", "m4.3 : min. m4 enclosure of via3 : 0.065um")
|
||||||
|
via3.not(m4).output("m4.3_a", "m4.3_a : via3 must be enclosed by met4")
|
||||||
|
end
|
||||||
|
log("END: 71/20 (m4)")
|
||||||
|
|
||||||
|
# via4
|
||||||
|
log("START: 71/44 (via4)")
|
||||||
|
if SEAL
|
||||||
|
ringVIA4 = via4.drc(with_holes > 0)
|
||||||
|
rectVIA4 = via4.not(ringVIA4)
|
||||||
|
else
|
||||||
|
rectVIA4 = via4
|
||||||
|
end
|
||||||
|
|
||||||
|
via4_not_mt = rectVIA4.not(areaid_mt)
|
||||||
|
via4_not_mt.non_rectangles.output("via4.1", "via4.1 : via4 outside of moduleCut should be rectangular")
|
||||||
|
rectVIA4.width(0.8, euclidian).output("via4.1_a", "via4.1_a : min. width of via4 outside of moduleCut : 0.8um")
|
||||||
|
rectVIA4.drc(length > 0.8).output("via4.1_b", "via4.1_b : maximum length of via4 : 0.8um")
|
||||||
|
|
||||||
|
via4.space(0.8, euclidian).polygons.output("via4.2", "via4.2 : min. via4 spacing : 0.8um")
|
||||||
|
|
||||||
|
if SEAL
|
||||||
|
ringVIA4.width(0.8, euclidian).output("via4.3", "via4.3 : min. width of ring-shaped via4 : 0.8um")
|
||||||
|
ringVIA4.drc(width >= 0.805).output("via4.3_a", "via4.3_a : max. width of ring-shaped via4 : 0.805um")
|
||||||
|
ringVIA4.not(areaid_sl).output("via4.3_b", "via4.3_b: ring-shaped via4 must be enclosed by areaid_sl")
|
||||||
|
end
|
||||||
|
|
||||||
|
m4.enclosing(via4, 0.19, euclidian).output("via4.4", "via4.4 : min. m4 enclosure of via4 : 0.19um")
|
||||||
|
rectVIA4.not(m4).output("via4.4_a", "via4.4_a : m4 must enclose all via4")
|
||||||
|
log("END: 71/44 (via4)")
|
||||||
|
|
||||||
|
# m5
|
||||||
|
log("START: 72/20 (m5)")
|
||||||
|
m5.width(1.6, euclidian).output("m5.1", "m5.1 : min. m5 width : 1.6um")
|
||||||
|
|
||||||
|
m5.space(1.6, euclidian).output("m5.2", "m5.2 : min. m5 spacing : 1.6um")
|
||||||
|
|
||||||
|
m5.enclosing(via4, 0.31, euclidian).output("m5.3", "m5.3 : min. m5 enclosure of via4 : 0.31um")
|
||||||
|
via4.not(m5).output("m5.3_a", "m5.3_a : via must be enclosed by m5")
|
||||||
|
if FLOATING_MET
|
||||||
|
m5.not_interacting(via4).output("m5.x", "floating met5, must interact with via4")
|
||||||
|
end
|
||||||
|
m5.with_area(0..4.0).output("m5.4", "m5.4 : min. m5 area : 4.0um²")
|
||||||
|
log("END: 72/20 (m5)")
|
||||||
|
|
||||||
|
# pad
|
||||||
|
log("START: 76/20 (pad)")
|
||||||
|
pad.space(1.27, euclidian).output("pad.2", "pad.2 : min. pad spacing : 1.27um")
|
||||||
|
log("END: 76/20 (pad)")
|
||||||
|
|
||||||
|
end #BEOL
|
||||||
|
|
||||||
|
if FEOL
|
||||||
|
log("FEOL section")
|
||||||
|
|
||||||
|
# hvi
|
||||||
|
log("START: 75/20 (hvi)")
|
||||||
|
hvi_peri = hvi.not(areaid_ce)
|
||||||
|
hvi_peri.width(0.6, euclidian).output("hvi.1", "hvi.1 : min. hvi width : 0.6um")
|
||||||
|
hvi_peri.space(0.7, euclidian).output("hvi.2a", "hvi.2a : min. hvi spacing : 0.7um")
|
||||||
|
log("END: 75/20 (hvi)")
|
||||||
|
|
||||||
|
# hvntm
|
||||||
|
log("START: 125/20 (hvntm)")
|
||||||
|
hvntm_peri = hvntm.not(areaid_ce)
|
||||||
|
hvntm_peri.width(0.7, euclidian).output("hvntm.1", "hvntm.1 : min. hvntm width : 0.7um")
|
||||||
|
hvntm_peri.space(0.7, euclidian).output("hvntm.2", "hvntm.2 : min. hvntm spacing : 0.7um")
|
||||||
|
log("END: 125/20 (hvntm)")
|
||||||
|
|
||||||
|
end #FEOL
|
||||||
|
|
||||||
|
|
||||||
|
if OFFGRID
|
||||||
|
log("OFFGRID-ANGLES section")
|
||||||
|
|
||||||
|
dnwell.ongrid(0.005).output("dnwell_OFFGRID", "x.1b : OFFGRID vertex on dnwell")
|
||||||
|
dnwell.with_angle(0 .. 45).output("dnwell_angle", "x.3a : non 45 degree angle dnwell")
|
||||||
|
nwell.ongrid(0.005).output("nwell_OFFGRID", "x.1b : OFFGRID vertex on nwell")
|
||||||
|
nwell.with_angle(0 .. 45).output("nwell_angle", "x.3a : non 45 degree angle nwell")
|
||||||
|
pwbm.ongrid(0.005).output("pwbm_OFFGRID", "x.1b : OFFGRID vertex on pwbm")
|
||||||
|
pwbm.with_angle(0 .. 45).output("pwbm_angle", "x.3a : non 45 degree angle pwbm")
|
||||||
|
pwde.ongrid(0.005).output("pwde_OFFGRID", "x.1b : OFFGRID vertex on pwde")
|
||||||
|
pwde.with_angle(0 .. 45).output("pwde_angle", "x.3a : non 45 degree angle pwde")
|
||||||
|
hvtp.ongrid(0.005).output("hvtp_OFFGRID", "x.1b : OFFGRID vertex on hvtp")
|
||||||
|
hvtp.with_angle(0 .. 45).output("hvtp_angle", "x.3a : non 45 degree angle hvtp")
|
||||||
|
hvtr.ongrid(0.005).output("hvtr_OFFGRID", "x.1b : OFFGRID vertex on hvtr")
|
||||||
|
hvtr.with_angle(0 .. 45).output("hvtr_angle", "x.3a : non 45 degree angle hvtr")
|
||||||
|
lvtn.ongrid(0.005).output("lvtn_OFFGRID", "x.1b : OFFGRID vertex on lvtn")
|
||||||
|
lvtn.with_angle(0 .. 45).output("lvtn_angle", "x.3a : non 45 degree angle lvtn")
|
||||||
|
ncm.ongrid(0.005).output("ncm_OFFGRID", "x.1b : OFFGRID vertex on ncm")
|
||||||
|
ncm.with_angle(0 .. 45).output("ncm_angle", "x.3a : non 45 degree angle ncm")
|
||||||
|
diff.ongrid(0.005).output("diff_OFFGRID", "x.1b : OFFGRID vertex on diff")
|
||||||
|
tap.ongrid(0.005).output("tap_OFFGRID", "x.1b : OFFGRID vertex on tap")
|
||||||
|
diff.not(areaid_en.and(uhvi)).with_angle(0 .. 90).output("diff_angle", "x.2 : non 90 degree angle diff")
|
||||||
|
diff.and(areaid_en.and(uhvi)).with_angle(0 .. 45).output("diff_angle", "x.2c : non 45 degree angle diff")
|
||||||
|
tap.not(areaid_en.and(uhvi)).with_angle(0 .. 90).output("tap_angle", "x.2 : non 90 degree angle tap")
|
||||||
|
tap.and(areaid_en.and(uhvi)).with_angle(0 .. 45).output("tap_angle", "x.2c : non 45 degree angle tap")
|
||||||
|
tunm.ongrid(0.005).output("tunm_OFFGRID", "x.1b : OFFGRID vertex on tunm")
|
||||||
|
tunm.with_angle(0 .. 45).output("tunm_angle", "x.3a : non 45 degree angle tunm")
|
||||||
|
poly.ongrid(0.005).output("poly_OFFGRID", "x.1b : OFFGRID vertex on poly")
|
||||||
|
poly.with_angle(0 .. 90).output("poly_angle", "x.2 : non 90 degree angle poly")
|
||||||
|
rpm.ongrid(0.005).output("rpm_OFFGRID", "x.1b : OFFGRID vertex on rpm")
|
||||||
|
rpm.with_angle(0 .. 45).output("rpm_angle", "x.3a : non 45 degree angle rpm")
|
||||||
|
npc.ongrid(0.005).output("npc_OFFGRID", "x.1b : OFFGRID vertex on npc")
|
||||||
|
npc.with_angle(0 .. 45).output("npc_angle", "x.3a : non 45 degree angle npc")
|
||||||
|
nsdm.ongrid(0.005).output("nsdm_OFFGRID", "x.1b : OFFGRID vertex on nsdm")
|
||||||
|
nsdm.with_angle(0 .. 45).output("nsdm_angle", "x.3a : non 45 degree angle nsdm")
|
||||||
|
psdm.ongrid(0.005).output("psdm_OFFGRID", "x.1b : OFFGRID vertex on psdm")
|
||||||
|
psdm.with_angle(0 .. 45).output("psdm_angle", "x.3a : non 45 degree angle psdm")
|
||||||
|
licon.ongrid(0.005).output("licon_OFFGRID", "x.1b : OFFGRID vertex on licon")
|
||||||
|
licon.with_angle(0 .. 90).output("licon_angle", "x.2 : non 90 degree angle licon")
|
||||||
|
li.ongrid(0.005).output("li_OFFGRID", "x.1b : OFFGRID vertex on li")
|
||||||
|
li.with_angle(0 .. 45).output("li_angle", "x.3a : non 45 degree angle li")
|
||||||
|
mcon.ongrid(0.005).output("ct_OFFGRID", "x.1b : OFFGRID vertex on mcon")
|
||||||
|
mcon.with_angle(0 .. 90).output("ct_angle", "x.2 : non 90 degree angle mcon")
|
||||||
|
vpp.ongrid(0.005).output("vpp_OFFGRID", "x.1b : OFFGRID vertex on vpp")
|
||||||
|
vpp.with_angle(0 .. 45).output("vpp_angle", "x.3a : non 45 degree angle vpp")
|
||||||
|
m1.ongrid(0.005).output("m1_OFFGRID", "x.1b : OFFGRID vertex on m1")
|
||||||
|
m1.with_angle(0 .. 45).output("m1_angle", "x.3a : non 45 degree angle m1")
|
||||||
|
via.ongrid(0.005).output("via_OFFGRID", "x.1b : OFFGRID vertex on via")
|
||||||
|
via.with_angle(0 .. 90).output("via_angle", "x.2 : non 90 degree angle via")
|
||||||
|
m2.ongrid(0.005).output("m2_OFFGRID", "x.1b : OFFGRID vertex on m2")
|
||||||
|
m2.with_angle(0 .. 45).output("m2_angle", "x.3a : non 45 degree angle m2")
|
||||||
|
via2.ongrid(0.005).output("via2_OFFGRID", "x.1b : OFFGRID vertex on via2")
|
||||||
|
via2.with_angle(0 .. 90).output("via2_angle", "x.2 : non 90 degree angle via2")
|
||||||
|
m3.ongrid(0.005).output("m3_OFFGRID", "x.1b : OFFGRID vertex on m3")
|
||||||
|
m3.with_angle(0 .. 45).output("m3_angle", "x.3a : non 45 degree angle m3")
|
||||||
|
via3.ongrid(0.005).output("via3_OFFGRID", "x.1b : OFFGRID vertex on via3")
|
||||||
|
via3.with_angle(0 .. 90).output("via3_angle", "x.2 : non 90 degree angle via3")
|
||||||
|
nsm.ongrid(0.005).output("nsm_OFFGRID", "x.1b : OFFGRID vertex on nsm")
|
||||||
|
nsm.with_angle(0 .. 45).output("nsm_angle", "x.3a : non 45 degree angle nsm")
|
||||||
|
m4.ongrid(0.005).output("m4_OFFGRID", "x.1b : OFFGRID vertex on m4")
|
||||||
|
m4.with_angle(0 .. 45).output("m4_angle", "x.3a : non 45 degree angle m4")
|
||||||
|
via4.ongrid(0.005).output("via4_OFFGRID", "x.1b : OFFGRID vertex on via4")
|
||||||
|
via4.with_angle(0 .. 90).output("via4_angle", "x.2 : non 90 degree angle via4")
|
||||||
|
m5.ongrid(0.005).output("m5_OFFGRID", "x.1b : OFFGRID vertex on m5")
|
||||||
|
m5.with_angle(0 .. 45).output("m5_angle", "x.3a : non 45 degree angle m5")
|
||||||
|
pad.ongrid(0.005).output("pad_OFFGRID", "x.1b : OFFGRID vertex on pad")
|
||||||
|
pad.with_angle(0 .. 45).output("pad_angle", "x.3a : non 45 degree angle pad")
|
||||||
|
mf.ongrid(0.005).output("mf_OFFGRID", "x.1b : OFFGRID vertex on mf")
|
||||||
|
mf.with_angle(0 .. 90).output("mf_angle", "x.2 : non 90 degree angle mf")
|
||||||
|
hvi.ongrid(0.005).output("hvi_OFFGRID", "x.1b : OFFGRID vertex on hvi")
|
||||||
|
hvi.with_angle(0 .. 45).output("hvi_angle", "x.3a : non 45 degree angle hvi")
|
||||||
|
hvntm.ongrid(0.005).output("hvntm_OFFGRID", "x.1b : OFFGRID vertex on hvntm")
|
||||||
|
hvntm.with_angle(0 .. 45).output("hvntm_angle", "x.3a : non 45 degree angle hvntm")
|
||||||
|
vhvi.ongrid(0.005).output("vhvi_OFFGRID", "x.1b : OFFGRID vertex on vhvi")
|
||||||
|
vhvi.with_angle(0 .. 45).output("vhvi_angle", "x.3a : non 45 degree angle vhvi")
|
||||||
|
uhvi.ongrid(0.005).output("uhvi_OFFGRID", "x.1b : OFFGRID vertex on uhvi")
|
||||||
|
uhvi.with_angle(0 .. 45).output("uhvi_angle", "x.3a : non 45 degree angle uhvi")
|
||||||
|
pwell_rs.ongrid(0.005).output("pwell_rs_OFFGRID", "x.1b : OFFGRID vertex on pwell_rs")
|
||||||
|
pwell_rs.with_angle(0 .. 45).output("pwell_rs_angle", "x.3a : non 45 degree angle pwell_rs")
|
||||||
|
areaid_re.ongrid(0.005).output("areaid_re_OFFGRID", "x.1b : OFFGRID vertex on areaid.re")
|
||||||
|
|
||||||
|
end #OFFGRID
|
||||||
|
logger.info(" ")
|
||||||
|
logger.info("Cell exclusion list:")
|
||||||
|
logger.info(" rule | cell")
|
||||||
|
logger.info(" nwell.6 | sky130_fd_io__gpiov2_amux, sky130_fd_io__simple_pad_and_busses, sram")
|
||||||
|
logger.info(" nsd.1 | sram")
|
||||||
|
logger.info(" nsd.2 | sram")
|
||||||
|
logger.info(" psd.1 | sram")
|
||||||
|
logger.info(" psd.2 | sram")
|
||||||
|
logger.info(" ")
|
||||||
|
logger.info("release #{release}")
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
import klayout.db as pya
|
||||||
|
import klayout_tools
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import precheck
|
||||||
|
|
||||||
|
PDK_ROOT = os.getenv("PDK_ROOT")
|
||||||
|
PDK_NAME = os.getenv("PDK_NAME") or "sky130A"
|
||||||
|
LYP_FILE = f"{PDK_ROOT}/{PDK_NAME}/libs.tech/klayout/tech/{PDK_NAME}.lyp"
|
||||||
|
gds_layers = klayout_tools.parse_lyp_layers(LYP_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def gds_empty(tmp_path_factory):
|
||||||
|
"""Creates a GDS with an empty cell that should pass DRC."""
|
||||||
|
gds_file = tmp_path_factory.mktemp("gds") / "gds_empty.gds"
|
||||||
|
layout = pya.Layout()
|
||||||
|
layout.create_cell("TEST_empty")
|
||||||
|
layout.write(str(gds_file))
|
||||||
|
return str(gds_file)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def gds_fail_met1_poly(tmp_path_factory):
|
||||||
|
"""Creates a GDS that fails Magic DRC and BEOL because the met1 rect is too small."""
|
||||||
|
gds_file = tmp_path_factory.mktemp("gds") / "gds_met1_fail.gds"
|
||||||
|
layout = pya.Layout()
|
||||||
|
met1_info = gds_layers["met1.drawing"]
|
||||||
|
met1 = layout.layer(met1_info.layer, met1_info.data_type)
|
||||||
|
top_cell = layout.create_cell("TEST_met1_error")
|
||||||
|
# Should fail "Metal1 minimum area < 0.083um^2 (met1.6)" (magic)
|
||||||
|
# and "m1.1 : min. m1 width : 0.14um" (klayout):
|
||||||
|
rect = pya.DBox(0, 0, 0.005, 0.005)
|
||||||
|
top_cell.shapes(met1).insert(rect)
|
||||||
|
layout.write(str(gds_file))
|
||||||
|
return str(gds_file)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def gds_fail_nwell_poly(tmp_path_factory):
|
||||||
|
"""Creates a GDS that fails FEOL because the nwell is too small."""
|
||||||
|
gds_file = tmp_path_factory.mktemp("gds") / "gds_nwell_fail.gds"
|
||||||
|
layout = pya.Layout()
|
||||||
|
nwell_info = gds_layers["nwell.drawing"]
|
||||||
|
nwell = layout.layer(nwell_info.layer, nwell_info.data_type)
|
||||||
|
top_cell = layout.create_cell("TEST_nwell_error")
|
||||||
|
# Should fail "nwell.1 : min. nwell width : 0.84um" (klayout):
|
||||||
|
rect = pya.DBox(0, 0, 0.005, 0.005)
|
||||||
|
top_cell.shapes(nwell).insert(rect)
|
||||||
|
layout.write(str(gds_file))
|
||||||
|
return str(gds_file)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def gds_fail_metal5_poly(tmp_path_factory):
|
||||||
|
"""Creates a GDS with drawings on layer5, should fail our precheck."""
|
||||||
|
gds_file = tmp_path_factory.mktemp("gds") / "gds_metal5_fail.gds"
|
||||||
|
layout = pya.Layout()
|
||||||
|
met5_info = gds_layers["met5.drawing"]
|
||||||
|
met5 = layout.layer(met5_info.layer, met5_info.data_type)
|
||||||
|
top_cell = layout.create_cell("TEST_met5_error")
|
||||||
|
rect = pya.DBox(0, 0, 5, 5)
|
||||||
|
top_cell.shapes(met5).insert(rect)
|
||||||
|
layout.write(str(gds_file))
|
||||||
|
return str(gds_file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_magic_drc_pass(gds_empty):
|
||||||
|
result = precheck.magic_drc(gds_empty, "TEST_empty")
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_magic_drc_fail(gds_fail_met1_poly):
|
||||||
|
result = precheck.magic_drc(gds_fail_met1_poly, "TEST_met1_error")
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_klayout_feol_pass(gds_empty):
|
||||||
|
result = precheck.klayout_drc(gds_empty, "feol")
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_klayout_feol_fail(gds_fail_nwell_poly):
|
||||||
|
result = precheck.klayout_drc(gds_fail_nwell_poly, "feol")
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_klayout_beol_pass(gds_empty):
|
||||||
|
result = precheck.klayout_drc(gds_empty, "beol")
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_klayout_beol_fail(gds_fail_met1_poly):
|
||||||
|
result = precheck.klayout_drc(gds_fail_met1_poly, "beol")
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_klayout_checks_pass(gds_empty):
|
||||||
|
result = precheck.klayout_checks(gds_empty)
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_klayout_checks_fail(gds_fail_metal5_poly):
|
||||||
|
result = precheck.klayout_checks(gds_fail_metal5_poly)
|
||||||
|
assert result is False
|
||||||
Reference in New Issue
Block a user