# -*- coding: utf-8 -*-
# The MIT License (MIT) - Copyright (c) 2016-2021 Dave Vandenbout.
"""
Handles part pins.
"""
from __future__ import ( # isort:skip
absolute_import,
division,
print_function,
unicode_literals,
)
import re
from builtins import range, super
from collections import defaultdict
from copy import copy
from enum import IntEnum
from future import standard_library
from .alias import *
from .logger import active_logger
from .skidlbaseobj import ERROR, OK, WARNING, SkidlBaseObject
from .utilities import *
standard_library.install_aliases()
[docs]class Pin(SkidlBaseObject):
"""
A class for storing data about pins for a part.
Args:
attribs: Key/value pairs of attributes to add to the library.
Attributes:
nets: The electrical nets this pin is connected to (can be >1).
part: Link to the Part object this pin belongs to.
func: Pin function such as PinType.types.INPUT.
do_erc: When false, the pin is not checked for ERC violations.
"""
# Various types of pins.
types = IntEnum(
"types",
(
"INPUT",
"OUTPUT",
"BIDIR",
"TRISTATE",
"PASSIVE",
"UNSPEC",
"PWRIN",
"PWROUT",
"OPENCOLL",
"OPENEMIT",
"PULLUP",
"PULLDN",
"NOCONNECT",
),
)
[docs] @classmethod
def add_type(cls, *pin_types):
"""
Add new pin type identifiers to the list of pin types.
Args:
pin_types: Strings identifying zero or more pin types.
"""
cls.types = IntEnum("types", [m.name for m in cls.types] + list(pin_types))
# Also add the pin types as attributes of the Pin class so
# existing SKiDL part libs will still work (e.g. Pin.INPUT
# still works as well as the newer Pin.types.INPUT).
for m in cls.types:
setattr(cls, m.name, m)
# Various drive levels a pin can output.
# The order of these is important! The first entry has the weakest
# drive and the drive increases for each successive entry.
drives = IntEnum(
"drives",
(
"NOCONNECT", # NC pin drive.
"NONE", # No drive capability (like an input pin).
"PASSIVE", # Small drive capability, but less than a pull-up or pull-down.
"PULLUPDN", # Pull-up or pull-down capability.
"ONESIDE", # Can pull high (open-emitter) or low (open-collector).
"TRISTATE", # Can pull high/low and be in high-impedance state.
"PUSHPULL", # Can actively drive high or low.
"POWER", # A power supply or ground line.
),
)
# Information about the various types of pins:
# function: A string describing the pin's function.
# drive: The drive capability of the pin.
# rcv_min: The minimum amount of drive the pin must receive to function.
# rcv_max: The maximum amount of drive the pin can receive and still function.
pin_info = {
types.INPUT: {
"function": "INPUT",
"func_str": "INPUT",
"drive": drives.NONE,
"max_rcv": drives.POWER,
"min_rcv": drives.PASSIVE,
},
types.OUTPUT: {
"function": "OUTPUT",
"func_str": "OUTPUT",
"drive": drives.PUSHPULL,
"max_rcv": drives.PASSIVE,
"min_rcv": drives.NONE,
},
types.BIDIR: {
"function": "BIDIRECTIONAL",
"func_str": "BIDIR",
"drive": drives.TRISTATE,
"max_rcv": drives.POWER,
"min_rcv": drives.NONE,
},
types.TRISTATE: {
"function": "TRISTATE",
"func_str": "TRISTATE",
"drive": drives.TRISTATE,
"max_rcv": drives.TRISTATE,
"min_rcv": drives.NONE,
},
types.PASSIVE: {
"function": "PASSIVE",
"func_str": "PASSIVE",
"drive": drives.PASSIVE,
"max_rcv": drives.POWER,
"min_rcv": drives.NONE,
},
types.PULLUP: {
"function": "PULLUP",
"func_str": "PULLUP",
"drive": drives.PULLUPDN,
"max_rcv": drives.POWER,
"min_rcv": drives.NONE,
},
types.PULLDN: {
"function": "PULLDN",
"func_str": "PULLDN",
"drive": drives.PULLUPDN,
"max_rcv": drives.POWER,
"min_rcv": drives.NONE,
},
types.UNSPEC: {
"function": "UNSPECIFIED",
"func_str": "UNSPEC",
"drive": drives.NONE,
"max_rcv": drives.POWER,
"min_rcv": drives.NONE,
},
types.PWRIN: {
"function": "POWER-IN",
"func_str": "PWRIN",
"drive": drives.NONE,
"max_rcv": drives.POWER,
"min_rcv": drives.POWER,
},
types.PWROUT: {
"function": "POWER-OUT",
"func_str": "PWROUT",
"drive": drives.POWER,
"max_rcv": drives.PASSIVE,
"min_rcv": drives.NONE,
},
types.OPENCOLL: {
"function": "OPEN-COLLECTOR",
"func_str": "OPENCOLL",
"drive": drives.ONESIDE,
"max_rcv": drives.TRISTATE,
"min_rcv": drives.NONE,
},
types.OPENEMIT: {
"function": "OPEN-EMITTER",
"func_str": "OPENEMIT",
"drive": drives.ONESIDE,
"max_rcv": drives.TRISTATE,
"min_rcv": drives.NONE,
},
types.NOCONNECT: {
"function": "NO-CONNECT",
"func_str": "NOCONNECT",
"drive": drives.NOCONNECT,
"max_rcv": drives.NOCONNECT,
"min_rcv": drives.NOCONNECT,
},
}
def __init__(self, **attribs):
super().__init__()
self.nets = []
self.part = None
self.name = ""
self.num = ""
self.do_erc = True
self.func = self.types.UNSPEC # Pin function defaults to unspecified.
# Attach additional attributes to the pin.
for k, v in list(attribs.items()):
setattr(self, k, v)
[docs] def copy(self, num_copies=None, **attribs):
"""
Return copy or list of copies of a pin including any net connection.
Args:
num_copies: Number of copies to make of pin.
Keyword Args:
attribs: Name/value pairs for setting attributes for the pin.
Notes:
An instance of a pin can be copied just by calling it like so::
p = Pin() # Create a pin.
p_copy = p() # This is a copy of the pin.
"""
# If the number of copies is None, then a single copy will be made
# and returned as a scalar (not a list). Otherwise, the number of
# copies will be set by the num_copies parameter or the number of
# values supplied for each part attribute.
num_copies_attribs = find_num_copies(**attribs)
return_list = (num_copies is not None) or (num_copies_attribs > 1)
if num_copies is None:
num_copies = max(1, num_copies_attribs)
# Check that a valid number of copies is requested.
if not isinstance(num_copies, int):
active_logger.raise_(
ValueError,
"Can't make a non-integer number ({}) of copies of a pin!".format(
num_copies
),
)
if num_copies < 0:
active_logger.raise_(
ValueError,
"Can't make a negative number ({}) of copies of a pin!".format(
num_copies
),
)
copies = []
for _ in range(num_copies):
# Make a shallow copy of the pin.
cpy = copy(self)
# The copy is not on a net, yet.
cpy.nets = []
# Connect the new pin to the same net as the original.
if self.nets:
self.nets[0] += cpy
# Copy the aliases for the pin if it has them.
cpy.aliases = self.aliases
# Attach additional attributes to the pin.
for k, v in list(attribs.items()):
setattr(cpy, k, v)
copies.append(cpy)
# Return a list of the copies made or just a single copy.
if return_list:
return copies
return copies[0]
# Make copies with the multiplication operator or by calling the object.
__call__ = copy
def __mul__(self, num_copies):
if num_copies is None:
num_copies = 0
return self.copy(num_copies=num_copies)
__rmul__ = __mul__
def __getitem__(self, *ids):
"""
Return the pin if the indices resolve to a single index of 0.
Args:
ids: A list of indices. These can be individual
numbers, net names, nested lists, or slices.
Returns:
The pin, otherwise None or raises an Exception.
"""
# Resolve the indices.
indices = list(set(expand_indices(0, self.width - 1, False, *ids)))
if indices is None or len(indices) == 0:
return None
if len(indices) > 1:
active_logger.raise_(ValueError, "Can't index a pin with multiple indices.")
if indices[0] != 0:
active_logger.raise_(ValueError, "Can't use a non-zero index for a pin.")
return self
def __setitem__(self, ids, *pins_nets_buses):
"""
You can't assign to Pins. You must use the += operator.
This method is a work-around that allows the use of the += for making
connections to pins while prohibiting direct assignment. Python
processes something like net[0] += Net() as follows::
1. Pin.__getitem__ is called with '0' as the index. This
returns a single Pin.
2. The Pin.__iadd__ method is passed the pin and
the thing to connect to it (a Net in this case). This
method makes the actual connection to the net. Then
it creates an iadd_flag attribute in the object it returns.
3. Finally, Pin.__setitem__ is called. If the iadd_flag attribute
is true in the passed argument, then __setitem__ was entered
as part of processing the += operator. If there is no
iadd_flag attribute, then __setitem__ was entered as a result
of using a direct assignment, which is not allowed.
"""
# If the iadd_flag is set, then it's OK that we got
# here and don't issue an error. Also, delete the flag.
if from_iadd(pins_nets_buses):
rmv_iadd(pins_nets_buses)
return
# No iadd_flag or it wasn't set. This means a direct assignment
# was made to the pin, which is not allowed.
active_logger.raise_(TypeError, "Can't assign to a Net! Use the += operator.")
def __iter__(self):
"""
Return an iterator for stepping through the pin.
"""
# You can only iterate a Pin one time.
return (self for i in [0]) # Return generator expr.
[docs] def is_connected(self):
"""Return true if a pin is connected to a net (but not a no-connect net)."""
from .net import NCNet, Net
if not self.nets:
# This pin is not connected to any nets.
return False
# Get the types of things this pin is connected to.
net_types = set([type(n) for n in self.nets])
if set([NCNet]) == net_types:
# This pin is only connected to no-connect nets.
return False
if set([Net]) == net_types:
# This pin is only connected to normal nets.
return True
if set([Net, NCNet]) == net_types:
# Can't be connected to both normal and no-connect nets!
active_logger.raise_(
ValueError,
"{} is connected to both normal and no-connect nets!".format(
self.erc_desc()
),
)
# This is just strange...
active_logger.raise_(
ValueError,
"{} is connected to something strange: {}.".format(
self.erc_desc(), self.nets
),
)
[docs] def is_attached(self, pin_net_bus):
"""Return true if this pin is attached to the given pin, net or bus."""
from .net import Net
from .pin import Pin
if not self.is_connected():
return False
if isinstance(pin_net_bus, Pin):
if pin_net_bus.is_connected():
return pin_net_bus.net.is_attached(self.net)
return False
if isinstance(pin_net_bus, Net):
return pin_net_bus.is_attached(self.net)
if isinstance(pin_net_bus, Bus):
for net in pin_net_bus[:]:
if self.net.is_attached(net):
return True
return False
active_logger.raise_(
ValueError,
"Pins can't be attached to {}!".format(type(pin_net_bus)),
)
[docs] def split_name(self, delimiters):
"""Use chars in divider to split a pin name and add substrings to aliases."""
# Split pin name and add subnames as aliases.
self.aliases += re.split("[" + re.escape(delimiters) + "]", self.name)
# Remove any empty aliases.
self.aliases.clean()
[docs] def connect(self, *pins_nets_buses):
"""
Return the pin after connecting it to one or more nets or pins.
Args:
pins_nets_buses: One or more Pin, Net or Bus objects or
lists/tuples of them.
Returns:
The updated pin with the new connections.
Notes:
You can connect nets or pins to a pin like so::
p = Pin() # Create a pin.
n = Net() # Create a net.
p += net # Connect the net to the pin.
"""
from .net import Net
from .protonet import ProtoNet
# Go through all the pins and/or nets and connect them to this pin.
for pn in expand_buses(flatten(pins_nets_buses)):
if isinstance(pn, ProtoNet):
pn += self
elif isinstance(pn, Pin):
# Connecting pin-to-pin.
if self.is_connected():
# If self is already connected to a net, then add the
# other pin to the same net.
self.nets[0] += pn
elif pn.is_connected():
# If self is unconnected but the other pin is, then
# connect self to the other pin's net.
pn.nets[0] += self
else:
# Neither pin is connected to a net, so create a net
# in the same circuit as the pin and attach both to it.
Net(circuit=self.part.circuit).connect(self, pn)
elif isinstance(pn, Net):
# Connecting pin-to-net, so just connect the pin to the net.
pn += self
else:
active_logger.raise_(
TypeError,
"Cannot attach non-Pin/non-Net {} to {}.".format(
type(pn), self.erc_desc()
),
)
# Set the flag to indicate this result came from the += operator.
set_iadd(self, True)
return self
# Connect a net to a pin using the += operator.
__iadd__ = connect
[docs] def disconnect(self):
"""Disconnect this pin from all nets."""
if not self.net:
return
for n in self.nets:
n.disconnect(self)
n.merge_names() # Clean-up the net after removing a pin.
self.nets = []
[docs] def get_nets(self):
"""Return a list containing the Net objects connected to this pin."""
return self.nets
[docs] def get_pins(self):
"""Return a list containing this pin."""
return to_list(self)
[docs] def create_network(self):
"""Create a network from a single pin."""
from .network import Network
ntwk = Network()
ntwk.append(self)
return ntwk
def __and__(self, obj):
"""Attach a pin and another part/pin/net in serial."""
from .network import Network
return Network(self) & obj
def __rand__(self, obj):
"""Attach a pin and another part/pin/net in serial."""
from .network import Network
return obj & Network(self)
def __or__(self, obj):
"""Attach a pin and another part/pin/net in parallel."""
from .network import Network
return Network(self) | obj
def __ror__(self, obj):
"""Attach a pin and another part/pin/net in parallel."""
from .network import Network
return obj | Network(self)
[docs] def chk_conflict(self, other_pin):
"""Check for electrical rule conflicts between this pin and another."""
if not self.do_erc or not other_pin.do_erc:
return
[erc_result, erc_msg] = conflict_matrix[self.func][other_pin.func]
# Return if the pins are compatible.
if erc_result == OK:
return
# Otherwise, generate an error or warning message.
if not erc_msg:
erc_msg = " ".join(
(
self.pin_info[self.func]["function"],
"connected to",
other_pin.pin_info[other_pin.func]["function"],
)
)
n = self.net.name
p1 = self.erc_desc()
p2 = other_pin.erc_desc()
msg = "Pin conflict on net {n}, {p1} <==> {p2} ({erc_msg})".format(**locals())
if erc_result == WARNING:
active_logger.warning(msg)
else:
active_logger.error(msg)
[docs] def erc_desc(self):
"""Return a string describing this pin for ERC."""
desc = "{func} pin {num}/{name} of {part}".format(
part=self.part.erc_desc(),
num=self.num,
name=self.name,
func=Pin.pin_info[self.func]["function"],
)
return desc
[docs] def get_pin_info(self):
num = getattr(self, "num", "???")
names = [getattr(self, "name", "???")]
names.extend(self.aliases)
names = ",".join(names)
func = Pin.pin_info[self.func]["function"]
return num, names, func
def __str__(self):
"""Return a description of this pin as a string."""
ref = getattr(self.part, "ref", "???")
num, names, func = self.get_pin_info()
return "Pin {ref}/{num}/{names}/{func}".format(**locals())
__repr__ = __str__
[docs] def export(self):
"""Return a string to recreate a Pin object."""
attribs = []
for k in ["num", "name", "func", "do_erc"]:
v = getattr(self, k, None)
if v:
if k == "func":
# Assign the pin function using the actual name of the
# function, not its numerical value (in case that changes
# in the future if more pin functions are added).
v = "Pin.types." + Pin.pin_info[v]["func_str"]
else:
v = repr(v)
attribs.append("{}={}".format(k, v))
return "Pin({})".format(",".join(attribs))
@property
def net(self):
"""Return one of the nets the pin is connected to."""
if self.nets:
return self.nets[0]
return None
@property
def width(self):
"""Return width of a Pin, which is always 1."""
return 1
@property
def drive(self):
"""
Get, set and delete the drive strength of this pin.
"""
try:
return self._drive
except AttributeError:
# Drive unspecified, so use default drive for this type of pin.
return self.pin_info[self.func]["drive"]
@drive.setter
def drive(self, drive):
self._drive = drive
@drive.deleter
def drive(self):
try:
del self._drive
except AttributeError:
pass
@property
def ref(self):
"""Return the reference of the part the pin belongs to."""
return self.part.ref
@property
def circuit(self):
"""Return the circuit of the part the pin belongs to."""
return self.part.circuit
def __bool__(self):
"""Any valid Pin is True."""
return True
__nonzero__ = __bool__ # Python 2 compatibility.
##############################################################################
[docs]class PhantomPin(Pin):
"""
A pin type that exists solely to tie two pinless nets together.
It will not participate in generating any netlists.
"""
def __init__(self, **attribs):
super().__init__(**attribs)
self.nets = []
self.part = None
self.do_erc = False
##############################################################################
[docs]class PinList(list):
"""
A list of Pin objects that's meant to look something like a Pin to a Part.
This is used for vector I/O of XSPICE parts.
"""
def __init__(self, num, name, part):
super().__init__()
# The list needs the following attributes to behave like a Pin.
self.num = num
self.name = name
self.part = part
def __getitem__(self, i):
"""
Get a Pin from the list. Add Pin objects to the list if they don't exist.
"""
if i >= len(self):
self.extend([Pin(num=j, part=self.part) for j in range(len(self), i + 1)])
return super().__getitem__(i)
[docs] def copy(self):
"""
Return a copy of a PinList for use when a Part is copied.
"""
cpy = PinList(self.num, self.name, self.part)
for pin in self:
cpy += pin.copy()
return cpy
[docs] def disconnect(self):
"""Disconnect all the pins in the list."""
for pin in self:
pin.disconnect()
[docs] def is_connected(self):
for pin in self:
if pin.is_connected():
return True
return False
##############################################################################
# This will make all the Pin.drive members into attributes of the Pin class
# so things like Pin.INPUT will work as well as Pin.types.INPUT.
Pin.add_type()
# Create the pin conflict matrix as a defaultdict of defaultdicts which
# returns OK if the given element is not in the matrix. This would indicate
# the pin types used to index that element have no contention if connected.
conflict_matrix = defaultdict(lambda: defaultdict(lambda: [OK, ""]))
# Add the non-OK pin connections to the matrix.
conflict_matrix[Pin.types.OUTPUT][Pin.types.OUTPUT] = [ERROR, ""]
conflict_matrix[Pin.types.TRISTATE][Pin.types.OUTPUT] = [WARNING, ""]
conflict_matrix[Pin.types.UNSPEC][Pin.types.INPUT] = [WARNING, ""]
conflict_matrix[Pin.types.UNSPEC][Pin.types.OUTPUT] = [WARNING, ""]
conflict_matrix[Pin.types.UNSPEC][Pin.types.BIDIR] = [WARNING, ""]
conflict_matrix[Pin.types.UNSPEC][Pin.types.TRISTATE] = [WARNING, ""]
conflict_matrix[Pin.types.UNSPEC][Pin.types.PASSIVE] = [WARNING, ""]
conflict_matrix[Pin.types.UNSPEC][Pin.types.PULLUP] = [WARNING, ""]
conflict_matrix[Pin.types.UNSPEC][Pin.types.PULLDN] = [WARNING, ""]
conflict_matrix[Pin.types.UNSPEC][Pin.types.UNSPEC] = [WARNING, ""]
conflict_matrix[Pin.types.PWRIN][Pin.types.TRISTATE] = [WARNING, ""]
conflict_matrix[Pin.types.PWRIN][Pin.types.UNSPEC] = [WARNING, ""]
conflict_matrix[Pin.types.PWROUT][Pin.types.OUTPUT] = [ERROR, ""]
conflict_matrix[Pin.types.PWROUT][Pin.types.BIDIR] = [WARNING, ""]
conflict_matrix[Pin.types.PWROUT][Pin.types.TRISTATE] = [ERROR, ""]
conflict_matrix[Pin.types.PWROUT][Pin.types.UNSPEC] = [WARNING, ""]
conflict_matrix[Pin.types.PWROUT][Pin.types.PWROUT] = [ERROR, ""]
conflict_matrix[Pin.types.OPENCOLL][Pin.types.OUTPUT] = [ERROR, ""]
conflict_matrix[Pin.types.OPENCOLL][Pin.types.TRISTATE] = [ERROR, ""]
conflict_matrix[Pin.types.OPENCOLL][Pin.types.UNSPEC] = [WARNING, ""]
conflict_matrix[Pin.types.OPENCOLL][Pin.types.PWROUT] = [ERROR, ""]
conflict_matrix[Pin.types.OPENEMIT][Pin.types.OUTPUT] = [ERROR, ""]
conflict_matrix[Pin.types.OPENEMIT][Pin.types.BIDIR] = [WARNING, ""]
conflict_matrix[Pin.types.OPENEMIT][Pin.types.TRISTATE] = [WARNING, ""]
conflict_matrix[Pin.types.OPENEMIT][Pin.types.UNSPEC] = [WARNING, ""]
conflict_matrix[Pin.types.OPENEMIT][Pin.types.PWROUT] = [ERROR, ""]
conflict_matrix[Pin.types.NOCONNECT][Pin.types.INPUT] = [ERROR, ""]
conflict_matrix[Pin.types.NOCONNECT][Pin.types.OUTPUT] = [ERROR, ""]
conflict_matrix[Pin.types.NOCONNECT][Pin.types.BIDIR] = [ERROR, ""]
conflict_matrix[Pin.types.NOCONNECT][Pin.types.TRISTATE] = [ERROR, ""]
conflict_matrix[Pin.types.NOCONNECT][Pin.types.PASSIVE] = [ERROR, ""]
conflict_matrix[Pin.types.NOCONNECT][Pin.types.PULLUP] = [ERROR, ""]
conflict_matrix[Pin.types.NOCONNECT][Pin.types.PULLDN] = [ERROR, ""]
conflict_matrix[Pin.types.NOCONNECT][Pin.types.UNSPEC] = [ERROR, ""]
conflict_matrix[Pin.types.NOCONNECT][Pin.types.PWRIN] = [ERROR, ""]
conflict_matrix[Pin.types.NOCONNECT][Pin.types.PWROUT] = [ERROR, ""]
conflict_matrix[Pin.types.NOCONNECT][Pin.types.OPENCOLL] = [ERROR, ""]
conflict_matrix[Pin.types.NOCONNECT][Pin.types.OPENEMIT] = [ERROR, ""]
conflict_matrix[Pin.types.NOCONNECT][Pin.types.NOCONNECT] = [ERROR, ""]
conflict_matrix[Pin.types.PULLUP][Pin.types.PULLUP] = [
WARNING,
"Multiple pull-ups connected.",
]
conflict_matrix[Pin.types.PULLDN][Pin.types.PULLDN] = [
WARNING,
"Multiple pull-downs connected.",
]
conflict_matrix[Pin.types.PULLUP][Pin.types.PULLDN] = [
ERROR,
"Pull-up connected to pull-down.",
]
# Fill-in the other half of the symmetrical contention matrix by looking
# for entries that != OK at position (r,c) and copying them to position
# (c,r).
cols = list(conflict_matrix.keys())
for c in cols:
for r in list(conflict_matrix[c].keys()):
conflict_matrix[r][c] = conflict_matrix[c][r]