├── doc ├── readme.md ├── contributing.md ├── index.rst ├── Makefile ├── small_parts.rst ├── netlistsvg.rst ├── api.rst └── conf.py ├── examples ├── voltage_divider.refdes_mapping ├── voltage_divider.py ├── class_a.py ├── servo_micro.refdes_mapping └── servo_micro.py ├── requirements.txt ├── .gitignore ├── LICENSE ├── pcbdl ├── __init__.py ├── defined_at.py ├── allegro.py ├── small_parts.py ├── context.py ├── html.py ├── netlistsvg.py └── base.py ├── test ├── integration │ ├── load_example.py │ ├── netlist_normalize.py │ ├── netlist.py │ └── servo_original.rpt ├── small_parts.py └── base.py ├── CONTRIBUTING.md ├── setup.py ├── Makefile └── README.md /doc/readme.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /doc/contributing.md: -------------------------------------------------------------------------------- 1 | ../CONTRIBUTING.md -------------------------------------------------------------------------------- /examples/voltage_divider.refdes_mapping: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e . # install pcbdl in as a package while still being editable 2 | 3 | coverage 4 | 5 | # documentation 6 | sphinx 7 | sphinx_rtd_theme 8 | recommonmark 9 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | PCBDL Documentation 2 | =================== 3 | 4 | .. toctree:: 5 | :maxdepth: 3 6 | :caption: Table of Contents: 7 | 8 | readme 9 | contributing 10 | netlistsvg 11 | 12 | api 13 | small_parts 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | *$py.class 4 | 5 | venv 6 | 7 | *.egg-info/ 8 | dist/ 9 | build/ 10 | 11 | htmlcov/ 12 | .coverage 13 | 14 | doc/_build/ 15 | 16 | # output files 17 | *.svg 18 | *.json 19 | *.html 20 | *allegro_third_party/ 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Google LLC 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = pcbdl 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /doc/small_parts.rst: -------------------------------------------------------------------------------- 1 | Small Parts 2 | =========== 3 | 4 | JellyBean 5 | --------- 6 | .. autoclass:: pcbdl.small_parts.JellyBean 7 | :show-inheritance: 8 | :members: 9 | :special-members: __rxor__, __xor__ 10 | 11 | Test Point 12 | ---------- 13 | .. autoclass:: pcbdl.small_parts.TP 14 | :show-inheritance: 15 | :members: 16 | 17 | .. autoclass:: pcbdl.small_parts.OnePinPart 18 | :show-inheritance: 19 | :members: 20 | 21 | 22 | Predefined Parts 23 | ---------------- 24 | Using the previous simple classes a ton of predefined parts are available: 25 | 26 | .. automodule:: pcbdl.small_parts 27 | :exclude-members: JellyBean, TP, OnePinPart 28 | :show-inheritance: 29 | :members: 30 | -------------------------------------------------------------------------------- /examples/voltage_divider.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2019 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """ 18 | Super trivial voltage divider circuit. 19 | """ 20 | 21 | from pcbdl import * 22 | 23 | vin, gnd = Net("PP3300"), Net("gnd") 24 | 25 | vin ^ R("30k") ^ Net("Vout_tap1") ^ R("20k") ^ Net("Vout_tap2") ^ R("10k") ^ gnd 26 | -------------------------------------------------------------------------------- /pcbdl/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2019 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from pcbdl.base import * 18 | 19 | from pcbdl.small_parts import * 20 | from pcbdl.defined_at import * 21 | from pcbdl.context import * 22 | 23 | from pcbdl.allegro import * 24 | from pcbdl.html import * 25 | 26 | from pcbdl.netlistsvg import * 27 | -------------------------------------------------------------------------------- /test/integration/load_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2020 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import importlib 18 | import pathlib 19 | import os 20 | import sys 21 | 22 | def run(example_name): 23 | examples_dir = pathlib.Path(__file__).absolute().parent.parent.parent / "examples" 24 | sys.path.insert(0, str(examples_dir)) 25 | saved_cwd = os.getcwd() 26 | try: 27 | os.chdir(examples_dir) # so the refdes_mapping file is in the right spot 28 | return importlib.import_module(example_name) 29 | finally: 30 | os.chdir(saved_cwd) 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Developing PCBDL 2 | 3 | This command will setup the development enviroment so it will be easy to make changes to PCBDL: 4 | ```bash 5 | sudo pip3 install -r requirements.txt 6 | ``` 7 | 8 | You probably also want to [install netlistsvg](https://google.github.io/pcbdl/doc/_build/html/netlistsvg.html#installation). 9 | 10 | ## Tests 11 | 12 | To test pcbdl framework functionality (not the schematics themselves), one can run: 13 | ```bash 14 | make test 15 | ``` 16 | 17 | If everything passes the code coverage can be seen with: 18 | ```bash 19 | make show-coverage 20 | ``` 21 | 22 | ## Contributor License Agreement 23 | 24 | Contributions to this project must be accompanied by a Contributor License 25 | Agreement. You (or your employer) retain the copyright to your contribution; 26 | this simply gives us permission to use and redistribute your contributions as 27 | part of the project. Head over to to see 28 | your current agreements on file or to sign a new one. 29 | 30 | You generally only need to submit a CLA once, so if you've already submitted one 31 | (even if it was for a different project), you probably don't need to do it 32 | again. 33 | -------------------------------------------------------------------------------- /examples/class_a.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2019 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """ 18 | Simple Class A amplifier example. 19 | 20 | https://www.electronics-tutorials.ws/amplifier/amp_5.html 21 | """ 22 | 23 | from pcbdl import * 24 | 25 | ac_coupling_value = "1000u" 26 | 27 | vcc, gnd = Net("vcc"), Net("gnd") 28 | 29 | q = BJT("2n3904") 30 | 31 | C = C_POL 32 | 33 | q.BASE << ( 34 | C(ac_coupling_value, to=Net("vin")), 35 | R("1k", to=vcc), 36 | R("1k", to=gnd), 37 | ) 38 | 39 | q.COLLECTOR << ( 40 | C(ac_coupling_value, to=Net("vout")), 41 | R("100", to=vcc), 42 | ) 43 | 44 | q.EMITTER << ( 45 | R("100", "Rc", to=gnd), 46 | C("1u", "C10", to=gnd), 47 | ) 48 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2019 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import setuptools 18 | 19 | with open("README.md", "r", encoding="utf8") as readme_file: 20 | long_description = readme_file.read() 21 | 22 | setuptools.setup( 23 | name="pcbdl", 24 | version="0.1.1", 25 | author="Google LLC", 26 | description="A programming way to design schematics.", 27 | long_description=long_description, 28 | long_description_content_type="text/markdown", 29 | license="Apache-2.0", 30 | url="https://github.com/google/pcbdl", 31 | packages=setuptools.find_packages(), 32 | keywords=["eda", "hdl", "electronics", "netlist", "hardware", "schematics"], 33 | install_requires=["pygments"], 34 | classifiers=[ 35 | "Intended Audience :: Developers", 36 | "License :: OSI Approved :: Apache Software License", 37 | "Operating System :: OS Independent", 38 | "Programming Language :: Python :: 3", 39 | "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)", 40 | "Topic :: System :: Hardware", 41 | ], 42 | ) 43 | -------------------------------------------------------------------------------- /doc/netlistsvg.rst: -------------------------------------------------------------------------------- 1 | NetlistSVG 2 | ========== 3 | 4 | Introduction 5 | ------------ 6 | `netlistsvg `_ is used by pcbdl to render a graphical output of the schematics. 7 | 8 | It uses the "analog skin" to get something similar to what an engineer would draw for a board/pcb level design: 9 | 10 | .. figure:: https://raw.githubusercontent.com/nturley/netlistsvg/master/doc/and.svg?sanitize=true 11 | 12 | Netlistsvg's Analog Example 13 | (TODO: replace drawing with a compact pcbdl example) 14 | 15 | 16 | Installation 17 | ------------ 18 | 19 | netlistsvg is written in javascript, to run it one needs both nodejs or npm. 20 | For debian style systems the following should do: 21 | 22 | .. code-block:: bash 23 | 24 | sudo apt install nodejs npm 25 | 26 | One should plan for a location to install netlisvg, I recomend next to the pcbdl folder. 27 | 28 | I recommend grabbing the https://github.com/amstan/netlistsvg/tree/for-pcbdl branch. 29 | It has a few tweaks that make netlistsvg outputs so much better, but the changes still need to be 30 | merged with the upstream project. 31 | 32 | .. code-block:: bash 33 | 34 | git clone -b for-pcbdl https://github.com/amstan/netlistsvg.git 35 | 36 | Then it can be installed with npm: 37 | 38 | .. code-block:: bash 39 | 40 | cd netlistsvg 41 | npm install . 42 | 43 | Finally pcbdl neets to be told where netlistsvg is, this is done using an `env variable `_. 44 | The default is assumed to be `~/netlistsvg`. The following can be added to .bashrc or where the user normally stores env variables:: 45 | 46 | export NETLISTSVG_LOCATION=~/path/to/installed/netlistsvg 47 | 48 | It can be tested by running :code:`make gh-pages` in `pcbdl/`, a bunch of `.svg` files will be created in `examples/`. 49 | -------------------------------------------------------------------------------- /test/small_parts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2019 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import unittest 18 | from pcbdl import * 19 | 20 | class JellyBeanTest(unittest.TestCase): 21 | def test_value(self): 22 | class WeirdPart(JellyBean): 23 | UNITS = "furlong" 24 | 25 | p = WeirdPart("100u") 26 | self.assertIn("100", p.value, "where's the value we gave it?") 27 | self.assertIn("furlong", p.value, "where's the units?") 28 | 29 | p = WeirdPart("100kfurlong") 30 | self.assertNotIn("furlongfurlong", p.value, "unit should not show twice") 31 | 32 | def test_connections(self): 33 | class TestPart(JellyBean): 34 | PINS = ["PRIMARY", "SECONDARY"] 35 | 36 | primary_net = Net("PRIMARY_NET") 37 | secondary_net = Net("SECONDARY_NET") 38 | 39 | p = TestPart(to=secondary_net) 40 | primary_net << p 41 | 42 | self.assertIs(p.PRIMARY.net, primary_net, "<< failed") 43 | self.assertIs(p.SECONDARY.net, secondary_net, "to= failed") 44 | 45 | def test_reverse_polarized(self): 46 | """Can we reverse bias a diode and still connect it right?""" 47 | vcc, gnd = Net("VCC"), Net("GND") 48 | 49 | dn = D(to=gnd) 50 | dr = D(to=gnd, reversed=True) 51 | vcc << (dn, dr) 52 | 53 | self.assertIs(dn.A.net, vcc) 54 | self.assertIs(dn.K.net, gnd) 55 | self.assertIs(dr.A.net, gnd) 56 | self.assertIs(dr.K.net, vcc) 57 | 58 | class OnePinPartTest(unittest.TestCase): 59 | def test_connections(self): 60 | n = Net() 61 | 62 | tp = OnePinPart() 63 | n << tp 64 | self.assertIs(tp.net, n) 65 | 66 | self.assertIs(OnePinPart(to=n).net, n) 67 | 68 | tp = OnePinPart() 69 | tp.net = n 70 | self.assertIs(tp.net, n) 71 | 72 | if __name__ == "__main__": 73 | unittest.main() 74 | -------------------------------------------------------------------------------- /pcbdl/defined_at.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from .base import Net, Part, PinFragment, Plugin 16 | 17 | import inspect 18 | import os 19 | 20 | __all__ = [] 21 | 22 | source_code = {} 23 | def grab_nearby_lines(defined_at, range_): 24 | filename, lineno = defined_at.rsplit(":", 1) 25 | lineno = int(lineno) 26 | 27 | if filename not in source_code: 28 | with open(filename) as file: 29 | source_code[filename] = tuple(file.read().split("\n")) 30 | 31 | range_ = slice(lineno - range_, lineno + range_ - 1) 32 | 33 | return source_code[filename][range_] 34 | 35 | cwd = os.getcwd() 36 | 37 | @Plugin.register((Net, Part, PinFragment)) 38 | class DefinedAt(Plugin): 39 | def __init__(self, instance): 40 | stack_trace = inspect.stack() 41 | 42 | # Escape from this function 43 | stack_trace.pop(0) 44 | 45 | # Escape the plugin architecture 46 | stack_trace.pop(0) 47 | stack_trace.pop(0) 48 | 49 | # Escape the caller function (probably the __init__ of the class that has the plugin) 50 | stack_trace.pop(0) 51 | 52 | # Skip #defined_at: not here code 53 | while (stack_trace[0].code_context is not None and 54 | "#defined_at: not here" in stack_trace[0].code_context[0]): 55 | stack_trace.pop(0) 56 | 57 | # Escape all the inheritances of that class 58 | while (stack_trace[0].code_context is not None and 59 | "super()" in stack_trace[0].code_context[0]): 60 | stack_trace.pop(0) 61 | 62 | # Skip #defined_at: not here code again 63 | while (stack_trace[0].code_context is not None and 64 | "#defined_at: not here" in stack_trace[0].code_context[0]): 65 | stack_trace.pop(0) 66 | 67 | self.frame = stack_trace[0] 68 | 69 | label_locals_with_variable_names(self.frame.frame.f_locals) 70 | 71 | filename = os.path.relpath(self.frame.filename, cwd) 72 | instance.defined_at = '%s:%d' % (filename, self.frame.lineno) 73 | 74 | def label_locals_with_variable_names(locals_dict): 75 | for variable_name, instance in locals_dict.items(): 76 | if not isinstance(instance, (Net, Part)): 77 | continue 78 | 79 | if hasattr(instance, variable_name): 80 | continue 81 | 82 | instance.variable_name = variable_name 83 | -------------------------------------------------------------------------------- /test/integration/netlist_normalize.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2020 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """Normalizes a third party Allegro netlist file so it's easier to diff.""" 18 | 19 | import re 20 | 21 | def _sort_lines_and_components(lines): 22 | lines = lines.split("\n") # split lines 23 | lines = [line.rsplit(";", 1) for line in lines] # look at the components 24 | for line in lines: # split and sort components 25 | components = line[1].strip().split() 26 | components.sort() 27 | line[1] = ' '.join(components) 28 | lines.sort() 29 | return '\n'.join('; '.join(line) for line in lines) 30 | 31 | def _normalize_nets(netlist): 32 | # normalizes jelly bean components: removes pin numbers 33 | netlist = re.sub(r"(([RLC]|FB)\d+\w*)\.\w+", r"\1", netlist) 34 | 35 | # unnamed nets 36 | netlist = re.sub(r"UNNAMED\w+\s*;", "ZZZUNNAMED ;", netlist) # google style 37 | netlist = re.sub(r"N\d{5,}\s*;", "ZZZUNNAMED ;", netlist) # odm style 38 | netlist = re.sub(r"ANON_NET\w+\s*;", "ZZZUNNAMED ;", netlist) # pcbdl style 39 | 40 | return _sort_lines_and_components(netlist) 41 | 42 | def _add_quote(s): 43 | """Add quotes on a package unless already quoted.""" 44 | s = re.sub("'(.*)'",r"\1", s) 45 | return f"'{s}'" 46 | 47 | def _normalize_packages(packages, normalize_props): 48 | packages = [package.split(";") for package in packages.split("\n")] 49 | for package in packages: 50 | props = package[0].split("!") 51 | props = [_add_quote(prop.strip()) for prop in props] 52 | if normalize_props: 53 | # cut the rest (part number, values?, tolerance?) 54 | props = props[:1] 55 | package[0] = ' ! '.join(props) 56 | packages = '\n'.join(' ; '.join(package) for package in packages) 57 | return _sort_lines_and_components(packages) 58 | 59 | def normalize(contents, normalize_header=False, normalize_part_props=False): 60 | # unwordwrap 61 | contents = re.sub(r",\n?\s*", r"", contents) 62 | 63 | header, packages, netlist = [group.strip() for group in 64 | re.match(r"(.*)\$PACKAGES*(.*)\$NETS(.*)\$END.*", contents, flags=re.DOTALL).groups() 65 | ] 66 | 67 | if normalize_header: 68 | header = "(NETLIST)\n(normalized header)" 69 | netlist = _normalize_nets(netlist) 70 | packages = _normalize_packages(packages, normalize_part_props) 71 | 72 | return f"{header}\n\n$PACKAGES\n{packages}\n\n$NETS\n{netlist}\n$END." 73 | 74 | if __name__=="__main__": 75 | import sys 76 | contents = open(sys.argv[1], "r").read() 77 | print(normalize(contents)) 78 | -------------------------------------------------------------------------------- /test/integration/netlist.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2020 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | import difflib 19 | import pathlib 20 | import tempfile 21 | import shutil 22 | import unittest 23 | 24 | import load_example 25 | import netlist_normalize 26 | 27 | class NetlistIntegration(unittest.TestCase): 28 | def test_netlist(self): 29 | """Integration test to compare the third party Allegro netlist file that we output vs what the project originally used.""" 30 | servo_micro = load_example.run("servo_micro") 31 | print(f"Import of examples/servo_micro was successful: {servo_micro.ec}") 32 | 33 | tmp_dir = pathlib.Path(tempfile.mkdtemp("_netlist.test")) 34 | try: 35 | print(f"Temporary dir {tmp_dir!r}") 36 | netlist_dir = tmp_dir / "allegro_third_party" 37 | 38 | servo_micro.generate_netlist(netlist_dir) 39 | 40 | with open(netlist_dir / "frompcbdl.netlist.rpt", "r") as f: 41 | pcbdl_netlist_contents = f.read() 42 | pcbdl_normalized = netlist_normalize.normalize( 43 | pcbdl_netlist_contents, 44 | normalize_header=True, 45 | normalize_part_props=True, 46 | ) 47 | pcbdl_normalized_file = tmp_dir / "pcbdl.normalized.rpt" 48 | with open(pcbdl_normalized_file, "w") as f: 49 | f.write(pcbdl_normalized) 50 | 51 | with open(pathlib.Path(__file__).absolute().parent / "servo_original.rpt", "r") as f: 52 | original_netlist_contents = f.read() 53 | original_normalized = netlist_normalize.normalize( 54 | original_netlist_contents, 55 | normalize_header=True, 56 | normalize_part_props=True, 57 | ) 58 | original_normalized_file = tmp_dir / "original.normalized.rpt" 59 | with open(original_normalized_file, "w") as f: 60 | f.write(original_normalized) 61 | 62 | delta = '\n'.join(difflib.unified_diff( 63 | pcbdl_normalized.splitlines(), 64 | original_normalized.splitlines(), 65 | str(pcbdl_normalized_file), 66 | str(original_normalized_file), 67 | n=3 68 | )) 69 | if delta: 70 | raise self.failureException(f"Netlists differ:\n{delta}") 71 | 72 | # Delete the tmp_dir if we got so far successfully 73 | shutil.rmtree(str(tmp_dir)) 74 | except Exception: 75 | print(f"Not deleting {tmp_dir!r} for further investigations:") 76 | raise 77 | 78 | if __name__ == "__main__": 79 | unittest.main() 80 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT: usage 2 | .PHONY: usage 3 | usage: 4 | @echo "This makefile could be used for automating pcbdl exporting:" 5 | @echo " make yourcircuit.html" 6 | @echo " make yourcircuit.svg" 7 | @echo " make yourcircuit.allegro_third_party/" 8 | @echo " make yourcircuit.shell" 9 | @echo 10 | @echo "yourcircuit could be stored anywhere, could be an absolute path." 11 | @echo "If the circuit is outside the pcbdl folder (suggested), make needs a -f flag to point it back to this file:" 12 | @echo " make -f /path/to/pcbdl/Makefile /another/path/to/yourcircuit.html" 13 | @echo 14 | @echo 15 | @echo "Other pcbdl project centric options:" 16 | @echo " make doc" 17 | @echo " make test" 18 | @echo " make show-coverage" 19 | @echo " make gh-pages" 20 | @echo " make clean" 21 | 22 | 23 | EXECUTE_SCHEMATIC = cd $(dir $<); python3 $2 -c "from $(basename $(notdir $<)) import *; $1" 24 | EXECUTE_SCHEMATIC_TO_FILE = $(call EXECUTE_SCHEMATIC, output=open('$(@F)', 'w'); output.write($1); output.close(), $2) 25 | 26 | # we shouldn't care if refdes mapping files are missing, so here's a dummy rule: 27 | %.refdes_mapping: %.py ; 28 | 29 | %.allegro_third_party/: %.py %.refdes_mapping 30 | $(call EXECUTE_SCHEMATIC,generate_netlist('$(basename $(` can be connected to nets using the ``<<`` and ``>>`` operators:: 32 | 33 | gnd << device1.GND 34 | 35 | A :term:`list` of pins can also be given (and they will be added in the same 36 | group):: 37 | 38 | gnd << (device1.GND, device2.GND) 39 | gnd >> ( 40 | device3.GND, 41 | device4.GND, 42 | ) 43 | 44 | The direction of the arrows is stored, but it doesn't really mean 45 | anything yet. You're free to use it as a hint on which way the signal 46 | is flowing (low impedance toward high impedance). 47 | 48 | :returns: A special version of this Net that will actually 49 | :attr:`remember the group` that we're 50 | currently attaching to. This way Pins added in the same line, 51 | even if alternating operators, will remember 52 | they were grouped:: 53 | 54 | gnd << device1.GND >> regulator1.GND 55 | gnd << device2.GND >> regulator2.GND 56 | 57 | Now device1 and regulator1's GND pin are remembered that they were 58 | grouped. The grouping might be significant, maybe as a hint, to the 59 | reader. This is a little bit of metadata that allows prettier 60 | exports (eg: SVG output will probably draw a real wire between 61 | those pins instead of airwires). 62 | 63 | .. autoproperty:: connections 64 | 65 | .. autoproperty:: grouped_connections 66 | 67 | Parts 68 | ----- 69 | .. autoclass:: pcbdl.Part 70 | 71 | .. autoproperty:: refdes 72 | .. autoattribute:: PINS 73 | .. automethod:: _postprocess_pin 74 | .. autoattribute:: pins 75 | .. autoattribute:: REFDES_PREFIX 76 | .. autoattribute:: pin_names_match_nets 77 | .. autoattribute:: pin_names_match_nets_prefix 78 | 79 | 80 | Pins 81 | ---- 82 | .. autoclass:: pcbdl.Pin 83 | 84 | .. autoclass:: pcbdl.base.PinFragment 85 | 86 | .. automethod:: second_name_important 87 | 88 | .. autoclass:: pcbdl.base.PartClassPin 89 | 90 | .. autoclass:: pcbdl.base.PartInstancePin 91 | 92 | .. autoproperty:: net 93 | 94 | .. automethod:: __lshift__(another pin or pins) 95 | .. automethod:: __rshift__(another pin or pins) 96 | 97 | Syntactic sugar for ``pin.net << another_pin`` or ``pin.net >> another_pin``:: 98 | 99 | # Instead of 100 | Net() >> somepart.BUTTON << somebutton.OUTPUT 101 | 102 | # Or 103 | Somepart.BUTTON.net << somebutton.OUTPUT 104 | 105 | # One can just write 106 | somepart.BUTTON << somebutton.OUTPUT 107 | 108 | 109 | Other 110 | ----- 111 | 112 | .. .. automodule:: pcbdl.base 113 | .. :members: 114 | -------------------------------------------------------------------------------- /pcbdl/allegro.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | Allegro(R) "third party" netlist format exporter. 17 | """ 18 | 19 | from .base import Part, PartInstancePin, Net, Plugin 20 | from .context import * 21 | 22 | import collections 23 | from datetime import datetime 24 | import itertools 25 | import os 26 | import shutil 27 | import pprint 28 | 29 | """Allegro "third party" format""" 30 | __all__ = ["generate_netlist"] 31 | 32 | def join_across_lines(iterator, count=10): 33 | iterator = tuple(iterator) 34 | grouped_generator = (iterator[i:i + count] for i in range(0, len(iterator), count)) 35 | return ' ,\n'.join(' '.join(line) for line in grouped_generator) 36 | 37 | @Plugin.register(Net) 38 | class NetlistNet(Plugin): 39 | @property 40 | def line(self): 41 | net = self.instance 42 | name = net.name 43 | pins = (f"{pin.part!r}.{number}" for pin in net.connections for number in pin.numbers) 44 | return "%s ; %s" % ( 45 | name, 46 | join_across_lines(pins), 47 | ) 48 | 49 | def netlist_generator(context, grouped_parts): 50 | yield "(NETLIST)" 51 | yield "(CREATED BY PCBDL)" 52 | yield "(%s)" % (datetime.now().strftime("%a %b %d %H:%M:%S %Y")) 53 | yield "" 54 | 55 | yield "$PACKAGES" 56 | for (package, part_number), parts_list in grouped_parts.items(): 57 | yield "'%s' ! '%s' ; %s" % ( 58 | package, part_number, 59 | " ".join(map(repr, parts_list)) 60 | ) 61 | yield "" 62 | 63 | yield "$NETS" 64 | for net in context.net_list: 65 | yield net.plugins[NetlistNet].line 66 | 67 | yield "$END" 68 | 69 | def generate_device_file_contents(part): 70 | hardware_pins = [] 71 | for pin in part.pins: 72 | all_name = pin.name 73 | numbers = pin.numbers 74 | 75 | for i, number in enumerate(numbers): 76 | if len(numbers) == 1: 77 | name = all_name 78 | else: 79 | # in case there's more than 1 pin, add numbers after every one of them 80 | name = "%s%d" % (all_name, i) 81 | hardware_pins.append((name, number)) 82 | 83 | pin_count = len(hardware_pins) 84 | pin_names, pin_numbers = zip(*hardware_pins) 85 | 86 | contents="""(PCBDL generated stub device file) 87 | PACKAGE %s 88 | PINCOUNT %d 89 | 90 | PINORDER 'MAIN' %s 91 | FUNCTION G1 'MAIN' %s 92 | 93 | END 94 | """ % (part.package, pin_count, ' '.join(pin_names), ' '.join(pin_numbers)) 95 | 96 | return contents 97 | 98 | def generate_netlist(output_location, context=global_context): 99 | # Clear it and make a new one 100 | try: 101 | shutil.rmtree(output_location) 102 | except FileNotFoundError: 103 | pass 104 | os.mkdir(output_location) 105 | 106 | grouped_parts = collections.defaultdict(list) 107 | for part in context.parts_list: 108 | key = (part.package, part.part_number) 109 | grouped_parts[key].append(part) 110 | 111 | netlist_contents = "\n".join(netlist_generator(context, grouped_parts)) 112 | 113 | netlist_filename = os.path.join(output_location, "frompcbdl.netlist.rpt") 114 | with open(netlist_filename, "w") as f: 115 | f.write(netlist_contents) 116 | 117 | # Generate device files 118 | device_location = os.path.join(output_location, "devices") 119 | os.mkdir(device_location) 120 | for parts in grouped_parts.values(): 121 | # Not sure if this is ok 122 | # we're assuming that parts with the same package and part 123 | # number have the same part class yielding in the same 124 | # ammount of pin_count 125 | part = parts[0] 126 | 127 | device_file_contents = generate_device_file_contents(part) 128 | 129 | device_filename = os.path.join(device_location, part.part_number + ".txt") 130 | with open(device_filename, "w") as f: 131 | f.write(device_file_contents) 132 | -------------------------------------------------------------------------------- /test/base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2019 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import unittest 18 | from pcbdl import * 19 | 20 | class TestNet(unittest.TestCase): 21 | def test_create(self): 22 | """Net creation test""" 23 | n = Net("test_create") 24 | 25 | def test_duplicate(self): 26 | Net("create_duplicate") 27 | with self.assertRaises(Exception): 28 | Net("create_duplicate") 29 | 30 | def test_str(self): 31 | """Net naming, both uppercasing and anonymous nets""" 32 | n = Net("test_str") 33 | self.assertEqual(str(n), "TEST_STR") 34 | 35 | n = Net() 36 | self.assertIn("anon", str(n).lower()) 37 | 38 | def test_repr(self): 39 | """Net representations should pretty and contain some info about what's connected""" 40 | n = Net() 41 | self.assertIn("unconnected", repr(n)) 42 | 43 | r0 = R() 44 | n << r0 45 | self.assertIn("connected", repr(n)) 46 | self.assertIn(r0.refdes, repr(n)) 47 | 48 | r1 = R() 49 | n << r1 50 | self.assertIn(",", repr(n), "there should be a comma separated list of components") 51 | self.assertIn(r1.refdes, repr(n)) 52 | 53 | for i in range(100): 54 | n << R() 55 | self.assertIn(str(len(n.connections)), repr(n), 56 | "we should show the number of connections after we can't display them all") 57 | 58 | def test_connections(self): 59 | """Various Net connection tests""" 60 | n = Net() 61 | rs = R(), R() 62 | 63 | # Adding by part, chaining 64 | n << rs[0] << rs[1] 65 | 66 | self.assertEqual({c.part for c in n.connections}, set(rs), "<< failed") 67 | 68 | # Adding by specific pin 69 | n << R().P1 70 | self.assertEqual(len(n.connections), 3, "<< on a pin failed") 71 | 72 | # Adding a whole list of things instead of chaining 73 | n << (R() for i in range(10)) 74 | 75 | self.assertEqual(len(n.connections), 13, "<< on a list failed") 76 | 77 | with self.assertRaises(TypeError, msg="this would be silly to work, connecting something of a random type to a net"): 78 | n << 2 79 | 80 | class DefinedAtTest(unittest.TestCase): 81 | """Make sure all the part/net .defined_at point to this file, not something inside the library proper.""" 82 | 83 | def check_defined_at(self, o): 84 | self.assertIn(__file__, o.defined_at, "%r.defined_at should point to this file, not something inside the library proper." % o) 85 | 86 | def test_created_net(self): 87 | n = Net() 88 | self.check_defined_at(n) 89 | 90 | def test_part(self): 91 | p = Part() 92 | self.check_defined_at(p) 93 | 94 | def test_implicit_net(self): 95 | r = R() 96 | n = r.P1.net 97 | self.check_defined_at(n) 98 | 99 | tp = TP() 100 | n = tp.net 101 | self.check_defined_at(n) 102 | 103 | class PartTest(unittest.TestCase): 104 | def test_automatic_name_collision(self): 105 | """Can we make a lot of uniquely named resistors""" 106 | parts = [] 107 | names = set() 108 | for i in range(1000): 109 | r = R() 110 | parts.append(r) 111 | refdes = r.refdes 112 | self.assertNotIn(refdes, names) 113 | names.add(refdes) 114 | 115 | def test_duplicate(self): 116 | """We shouldn't allow 2 parts with the same refdes.""" 117 | Part(refdes="duplicate_part") 118 | with self.assertRaises(Exception): 119 | Part(refdes="duplicate_part") 120 | 121 | def test_part_naming(self): 122 | p = Part() 123 | p.refdes = "naming_test" 124 | self.assertEqual(p.refdes, "NAMING_TEST") 125 | 126 | def test_repr_str(self): 127 | """Part __repr__ and __str__ contains part refdes""" 128 | p = Part() 129 | self.assertIn(p.refdes, str(p)) 130 | self.assertIn(p.refdes, repr(p)) 131 | 132 | if __name__ == "__main__": 133 | unittest.main() 134 | -------------------------------------------------------------------------------- /examples/servo_micro.refdes_mapping: -------------------------------------------------------------------------------- 1 | refdes code nets variable_name class value part_number 2 | CN1 c4bbf014a n2e9df3aa usb 1981568-1 1981568-1 3 | D1 c14d5125b n5cfb9168 usb_esd TPD2E001DRLR TPD2E001DRLR 4 | U3 c98bf87d0 n8d3b0f2c reg3300 MIC5504-3.3YMT MIC5504-3.3YMT 5 | C9 cc19d6a5f n002ad366 2.2uF CY2.2u 6 | C10 c6f489d26 n3be07f19 10uF CY10u 7 | C12 c0ddc584e n3be07f19 100nF CY100n 8 | C15 ce21e0300 n3be07f19 1000pF CY1000p 9 | U7 cc41b298a na68a2e39 reg1800 TLV70018DSER TLV70018DSER 10 | D2 c8bcbe793 n29740cdc drop_diode 240-800MV 240-800MV 11 | C22 cfb0b2182 n3d4e6bc1 100nF CY100n 12 | C19 c53b34eb7 ndf667ade 1uF CY1u 13 | U6 cacb50b8e n8b9aabc0 ec STM32F072CBU6TR STM32F072CBU6TR 14 | C2 c4f535fb1 n3be07f19 100nF CY100n 15 | C5 cccbe1cdd n3be07f19 100nF CY100n 16 | C1 cc5a9879e n3be07f19 4.7uF CY4.7u 17 | FB1 c4b9d1722 nc98cb2ba 600@100MHzH FERRITE_BEAD-185-00019-00 18 | C4 c831c03e4 ncb5ff657 1uF CY1u 19 | C7 cdc7d2ef9 ncb5ff657 100pF CY100p 20 | C6 c252b2f94 n3be07f19 100nF CY100n 21 | C13 cd0f94be7 n3be07f19 4.7uF CY4.7u 22 | CN2 c0e88b23e n10782a46 prog FH34SRJ-8S-0.5SH(50) FH34SRJ-8S-0.5SH(50) 23 | C8 c5701139b n324b81f1 100nF CY100n 24 | Q4 cd6a64bd1 nfc1d6b38 boot0_q CSD13381F4 CSD13381F4 25 | R22 c8ded2287 nfda1c818 51.1kΩ R51.1k 26 | R23 c6b4c3288 nab4a6662 51.1kΩ R51.1k 27 | J3 cb7557647 nd48e6922 dut AXK850145WG AXK850145WG 28 | U9 cc3af7d3d n222cf016 io TCA6416ARTWR TCA6416ARTWR 29 | C25 c29b6c7a2 n3be07f19 100nF CY100n 30 | R502 c0bffc37f n1d3e9f65 4.7kΩ R4.7k 31 | R501 cd4cc91b7 nbcfcdf6a 4.7kΩ R4.7k 32 | C20 cf7d99302 ndf667ade 100nF CY100n 33 | U14 c26c5927c n681bfbea mfg_mode_shifter SN74AVC1T45DRLR SN74AVC1T45DRLR 34 | C33 c6021be37 n819da06e 100nF CY100n 35 | R48 c6021be37 n819da06e 4.7kΩ R4.7k 36 | TP1 c6f2b349e n9d97ccf9 TP TP 37 | TP2 c429d8f7c n9d97ccf9 TP TP 38 | C29 cb8c64f5e n27a30867 100nF CY100n 39 | R46 cf05a5f62 ne80feabd 0Ω R0 40 | J4 cc3943ce5 nb878de91 jtag_connector HDR_2X5_50MIL-210-00939-00-SAMTEC_FTSH-105-01 HDR_2X5_50MIL-210-00939-00-SAMTEC_FTSH-105-01 41 | U56 c63765086 n7d7f524f shifter1 SN74AVC4T774RSVR SN74AVC4T774RSVR 42 | C109 c3e11e232 n3be07f19 100nF CY100n 43 | C108 c16ae1cca ne53160cf 100nF CY100n 44 | U55 c1558101b n020fba6d shifter2 SN74AVC4T774RSVR SN74AVC4T774RSVR 45 | C111 c870507ac n3be07f19 100nF CY100n 46 | C110 c1b34cc9d ne53160cf 100nF CY100n 47 | U1 ca63c2dc4 n5202b4d7 jtag_mux 313-00929-00 313-00929-00 48 | C11 c7fd822d2 n3be07f19 100nF CY100n 49 | U10 c1d0b4c92 na5140a5e jtag_output_buffer SN74LVC1G126YZPR SN74LVC1G126YZPR 50 | C3 c0f31962d n3be07f19 100nF CY100n 51 | U57 cc2875acb n070fdc6e s SN74AVC4T774RSVR SN74AVC4T774RSVR 52 | U59 ce683b0e0 n9c79a1e8 s SN74AVC4T774RSVR SN74AVC4T774RSVR 53 | U43 c7d19a4f6 n958bec8c power_switch ADP194ACBZ-R7 ADP194ACBZ-R7 54 | U42 c128a14f0 n4ac5ff35 power_switch ADP194ACBZ-R7 ADP194ACBZ-R7 55 | R95 ccd81fd2a n676b6954 4.7kΩ R4.7k 56 | R96 ccd81fd2a n4b0df73f 4.7kΩ R4.7k 57 | C107 c28cc4a35 n3be07f19 100nF CY100n 58 | C106 c4e20e726 n2679f586 100nF CY100n 59 | U23 c7d19a4f6 n4b8d5137 power_switch ADP194ACBZ-R7 ADP194ACBZ-R7 60 | U24 c128a14f0 n034da286 power_switch ADP194ACBZ-R7 ADP194ACBZ-R7 61 | R61 ccd81fd2a nf27ea2b2 4.7kΩ R4.7k 62 | R60 ccd81fd2a ne273c382 4.7kΩ R4.7k 63 | C120 c28cc4a35 n3be07f19 100nF CY100n 64 | C119 c4e20e726 n27a30867 100nF CY100n 65 | U5 c69f7b575 n01971292 spi1_mux TS3A24159 TS3A24159 66 | C14 c0f9a9104 n3be07f19 100nF CY100n 67 | U45 c5374ffe6 n9ed9f3d4 s SN74AVC2T245RSWR SN74AVC2T245RSWR 68 | U4 c5b22328d nd1ba133f s SN74AVC2T245RSWR SN74AVC2T245RSWR 69 | C76 cd2273c52 n3be07f19 100nF CY100n 70 | C77 c415c632e n32e455a2 100nF CY100n 71 | C91 cd2273c52 n3be07f19 100nF CY100n 72 | C90 c415c632e nce5564f6 100nF CY100n 73 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | sys.path.insert(0, os.path.abspath('../pcbdl')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'pcbdl' 23 | copyright = '2019, Google' 24 | author = 'Google' 25 | 26 | # The short X.Y version 27 | version = '' 28 | # The full version, including alpha/beta/rc tags 29 | release = '' 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # 36 | # needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | 'recommonmark', 43 | 'sphinx.ext.autodoc', 44 | 'sphinx.ext.doctest', 45 | 'sphinx.ext.intersphinx', 46 | 'sphinx.ext.todo', 47 | 'sphinx.ext.coverage', 48 | 'sphinx.ext.viewcode', 49 | 'sphinx.ext.githubpages', 50 | ] 51 | 52 | # Add any paths that contain templates here, relative to this directory. 53 | templates_path = ['_templates'] 54 | 55 | # The suffix(es) of source filenames. 56 | # You can specify multiple suffix as a list of string: 57 | # 58 | source_suffix = ['.rst', '.md'] 59 | 60 | # The master toctree document. 61 | master_doc = 'index' 62 | 63 | # The language for content autogenerated by Sphinx. Refer to documentation 64 | # for a list of supported languages. 65 | # 66 | # This is also used if you do content translation via gettext catalogs. 67 | # Usually you set "language" from the command line for these cases. 68 | language = None 69 | 70 | # List of patterns, relative to source directory, that match files and 71 | # directories to ignore when looking for source files. 72 | # This pattern also affects html_static_path and html_extra_path . 73 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 74 | 75 | # The name of the Pygments (syntax highlighting) style to use. 76 | pygments_style = 'sphinx' 77 | 78 | 79 | # -- Options for HTML output ------------------------------------------------- 80 | 81 | # The theme to use for HTML and HTML Help pages. See the documentation for 82 | # a list of builtin themes. 83 | # 84 | html_theme = 'sphinx_rtd_theme' 85 | 86 | # Theme options are theme-specific and customize the look and feel of a theme 87 | # further. For a list of options available for each theme, see the 88 | # documentation. 89 | # 90 | # html_theme_options = {} 91 | 92 | # Add any paths that contain custom static files (such as style sheets) here, 93 | # relative to this directory. They are copied after the builtin static files, 94 | # so a file named "default.css" will overwrite the builtin "default.css". 95 | html_static_path = ['_static'] 96 | 97 | # Custom sidebar templates, must be a dictionary that maps document names 98 | # to template names. 99 | # 100 | # The default sidebars (for documents that don't match any pattern) are 101 | # defined by theme itself. Builtin themes are using these templates by 102 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 103 | # 'searchbox.html']``. 104 | # 105 | # html_sidebars = {} 106 | 107 | 108 | # -- Options for HTMLHelp output --------------------------------------------- 109 | 110 | # Output file base name for HTML help builder. 111 | htmlhelp_basename = 'pcbdldoc' 112 | 113 | 114 | # -- Options for LaTeX output ------------------------------------------------ 115 | 116 | latex_elements = { 117 | # The paper size ('letterpaper' or 'a4paper'). 118 | # 119 | # 'papersize': 'letterpaper', 120 | 121 | # The font size ('10pt', '11pt' or '12pt'). 122 | # 123 | # 'pointsize': '10pt', 124 | 125 | # Additional stuff for the LaTeX preamble. 126 | # 127 | # 'preamble': '', 128 | 129 | # Latex figure (float) alignment 130 | # 131 | # 'figure_align': 'htbp', 132 | } 133 | 134 | # Grouping the document tree into LaTeX files. List of tuples 135 | # (source start file, target name, title, 136 | # author, documentclass [howto, manual, or own class]). 137 | latex_documents = [ 138 | (master_doc, 'pcbdl.tex', 'pcbdl Documentation', 139 | 'Google', 'manual'), 140 | ] 141 | 142 | 143 | # -- Options for manual page output ------------------------------------------ 144 | 145 | # One entry per manual page. List of tuples 146 | # (source start file, name, description, authors, manual section). 147 | man_pages = [ 148 | (master_doc, 'pcbdl', 'pcbdl Documentation', 149 | [author], 1) 150 | ] 151 | 152 | 153 | # -- Options for Texinfo output ---------------------------------------------- 154 | 155 | # Grouping the document tree into Texinfo files. List of tuples 156 | # (source start file, target name, title, author, 157 | # dir menu entry, description, category) 158 | texinfo_documents = [ 159 | (master_doc, 'pcbdl', 'pcbdl Documentation', 160 | author, 'pcbdl', 'One line description of project.', 161 | 'Miscellaneous'), 162 | ] 163 | 164 | 165 | # -- Extension configuration ------------------------------------------------- 166 | autodoc_default_options = { 167 | 'member-order': 'bysource', 168 | #'show-inheritance': True, 169 | #'members': True, 170 | 'undoc-members': True, 171 | } 172 | 173 | # -- Options for intersphinx extension --------------------------------------- 174 | 175 | # Example configuration for intersphinx: refer to the Python standard library. 176 | intersphinx_mapping = {"python": ('https://docs.python.org/', None)} 177 | 178 | # -- Options for todo extension ---------------------------------------------- 179 | 180 | # If true, `todo` and `todoList` produce output, else they produce nothing. 181 | todo_include_todos = True 182 | -------------------------------------------------------------------------------- /pcbdl/small_parts.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from .base import * 16 | 17 | class JellyBean(Part): 18 | """2 pin Jelly Bean components (this includes :class:`Resistors` 19 | and :class:`Capacitors`). 20 | 21 | The main attraction to subclass this is because of the ``to=`` argument, it's very easy to chain connections 22 | with such a part because its pins are pretty flexible therefore easy tochoose automatically. 23 | The use of this class is to allow quick connections of 2 pin devices (both pins at the same time). 24 | 25 | One way to do this is the ``to=`` argument, allows to connect the SECONDARY pin (usually in the same expression) 26 | easily without having to save the JellyBean part to a variable first:: 27 | 28 | supply >> IC.VCC << C("100nF", to=gnd) # note how decoupling cap is fully connected 29 | 30 | The other way these types of parts are cool and unique is the 31 | :meth:`^ operator`; it allows us to describe series connections 32 | through Jellybean components. Here's an example with a resistor divider:: 33 | 34 | some_net ^ R("1k") ^ attenuated_net ^ R("1k") ^ gnd 35 | """ 36 | PINS = [ 37 | ("P1", "1"), 38 | ("P2", "2"), 39 | ] 40 | 41 | UNITS = "" 42 | 43 | def __init__(self, value=None, refdes=None, package=None, part_number=None, populated=True, reversed=False, to=None): 44 | if value is not None: 45 | self.value = value 46 | try: 47 | if not value.endswith(self.UNITS): 48 | value += self.UNITS 49 | except AttributeError: 50 | pass # whatever, we don't even have a value 51 | 52 | super().__init__(value, refdes, package, part_number, populated) 53 | 54 | self._connect_pins_reversed = reversed 55 | 56 | if to is not None: 57 | to.connect(self, ConnectDirection.OUT, pin_type=PinType.SECONDARY) 58 | 59 | def get_pin_to_connect(self, pin_type, net=None): 60 | assert isinstance(pin_type, PinType) 61 | 62 | mapping = [0, 1] 63 | if self._connect_pins_reversed: 64 | mapping.reverse() 65 | 66 | if pin_type == PinType.PRIMARY: 67 | return self.pins[mapping[0]] 68 | elif pin_type == PinType.SECONDARY: 69 | return self.pins[mapping[1]] 70 | else: # pragma: no cover 71 | raise ValueError("Don't know how to get %s pin from %r" % (pin_type.name, self)) 72 | 73 | def __rxor__(self, left_net): 74 | """ 75 | Implements ``left_net ^ jellybean``. Connects the left_net to the PRIMARY pin, 76 | then returns the JellyBean part so it can keep chaining. 77 | 78 | .. note:: This is similar to :meth:`left_net \<\< JellyBean()`, but 79 | with the slight difference in how it can chain. 80 | """ 81 | assert isinstance(left_net, Net) 82 | left_net << self.get_pin_to_connect(PinType.PRIMARY) 83 | return self 84 | 85 | def __xor__(self, right_net): 86 | """ 87 | Implements ``jellybean ^ right_net``. Connects the right_net to the SECONDARY pin, 88 | then returns the JellyBean part so it can keep chaining. 89 | 90 | .. note:: This is similar to ``JellyBean(to=right_net)`` but a little cleaner looking. 91 | """ 92 | assert isinstance(right_net, Net) 93 | grouped_right_net = right_net << self.get_pin_to_connect(PinType.SECONDARY) 94 | return grouped_right_net 95 | 96 | 97 | class OnePinPart(Part): 98 | PINS = [("PIN", "P")] 99 | 100 | def __init__(self, value=None, refdes=None, package=None, part_number=None, populated=True, to=None): 101 | super().__init__(value, refdes, package, part_number, populated) 102 | if to is not None: 103 | to.connect(self, ConnectDirection.OUT, pin_type=PinType.PRIMARY) 104 | 105 | def get_pin_to_connect(self, pin_type, net=None): 106 | if pin_type == PinType.PRIMARY: 107 | # Perhaps we can name ourselves too after the net 108 | if net is not None and net._name is not None: 109 | if self.value == self.part_number: 110 | self.value = net.name 111 | 112 | return self.PIN 113 | else: # pragma: no cover 114 | assert isinstance(pin_type, PinType) 115 | raise ValueError("Don't know how to get %s pin from %r" % (pin_type.name, self)) 116 | 117 | @property 118 | def net(self): 119 | return self.PIN.net #defined_at: not here 120 | @net.setter 121 | def net(self, new_net): 122 | self.PIN.net = new_net 123 | 124 | class TP(OnePinPart): 125 | """Test Point""" 126 | REFDES_PREFIX = "TP" 127 | package = "TP" 128 | part_number = "TP" 129 | 130 | PINS_PLUS_MINUS = [ 131 | ("+", "P", "PLUS", "P2"), 132 | ("-", "M", "MINUS", "P1"), 133 | ] 134 | 135 | PINS_BJT = [ 136 | ("B", "BASE"), 137 | ("E", "EMITTER"), 138 | ("C", "COLLECTOR"), 139 | ] 140 | 141 | PINS_FET = [ 142 | ("G", "GATE"), 143 | ("S", "SOURCE"), 144 | ("D", "DRAIN"), 145 | ] 146 | 147 | class R(JellyBean): 148 | """Resistor""" 149 | REFDES_PREFIX = "R" 150 | UNITS = u"\u03A9" 151 | 152 | class C(JellyBean): 153 | """Capacitor""" 154 | REFDES_PREFIX = "C" 155 | UNITS = "F" 156 | 157 | class C_POL(C): 158 | """Polarized Capacitor""" 159 | PINS = PINS_PLUS_MINUS 160 | 161 | class L(JellyBean): 162 | """Inductor""" 163 | REFDES_PREFIX = "L" 164 | UNITS = "H" 165 | 166 | class D(JellyBean): 167 | """Diode""" 168 | REFDES_PREFIX = "D" 169 | PINS = [ 170 | ("A", "ANODE", "P1"), 171 | ("K", "CATHODE", "KATHODE", "P2"), 172 | ] 173 | 174 | class LED(D): 175 | """Light Emitting Diode""" 176 | REFDES_PREFIX = "LED" 177 | PINS = [ 178 | ("A", "+"), 179 | ("K", "-"), 180 | ] 181 | 182 | class BJT(Part): 183 | """BJT Transistor""" 184 | REFDES_PREFIX = "Q" 185 | PINS = PINS_BJT 186 | 187 | class FET(Part): 188 | """FET Transistor""" 189 | REFDES_PREFIX = "Q" 190 | PINS = PINS_FET 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PCB Design Language 2 | A programming way to design schematics. 3 | 4 | [Sphinx Documentation](https://google.github.io/pcbdl/doc/_build/html/) 5 | 6 | ## Installing 7 | 8 | [![PyPI version](https://badge.fury.io/py/pcbdl.svg)](https://pypi.org/project/pcbdl/) 9 | 10 | ```bash 11 | sudo apt-get install python3 python3-pip python3-pygments 12 | 13 | sudo pip3 install pcbdl 14 | ``` 15 | 16 | ## Interactive terminal 17 | 18 | A good way to try various features without having to write a file separately. 19 | 20 | ```bash 21 | python3 -i -c "from pcbdl import *" 22 | ``` 23 | 24 | ## Language 25 | 26 | PCBDL's goal is to allow designing schematics via Code. Similar to how VHDL or Verilog are ways to represent Logic Diagrams in code form, PCBDL the analogous of that but for EDA schematics. 27 | 28 | To start one should define a couple of nets: 29 | 30 | ```python 31 | >>> vcc, gnd = Net("vcc"), Net("gnd") 32 | >>> vin = Net("vin") 33 | >>> base = Net("base") 34 | ``` 35 | 36 | We then can connect various components between those nets with the `<<` operator and the `to=` argument (for the other side): 37 | 38 | ```python 39 | >>> base << C("1000u", to=vin) 40 | >>> base << ( 41 | R("1k", to=vcc), 42 | R("1k", to=gnd), 43 | ) 44 | ``` 45 | 46 | 2 pin devices (aka JellyBean in pcbdl) like capacitors and resistors are one of the easiest things to connect. Internally they have a primary (connected with `>>`) and secondary, other side, pin (connected with `to=`). 47 | 48 | Let's try to get more complicated by defining a transistor and connect some of its pins. 49 | 50 | ```python 51 | >>> class Transistor(Part): 52 | REFDES_PREFIX = "Q" 53 | PINS = ["B", "C", "E"] 54 | ... 55 | >>> q = Transistor() 56 | >>> base << q.B 57 | ``` 58 | 59 | Nets can also be created anonymously if started from a part's pins: 60 | 61 | ```python 62 | >>> q.E << ( 63 | R("100", to=gnd), 64 | C("1u", to=gnd), 65 | ) 66 | ``` 67 | 68 | Let's finish our class A amplifier (note how we created the "vout" net in place, and how we gave a name ("Rc") to one of our resistors): 69 | 70 | ```python 71 | >>> q.C << ( 72 | C("100u", to=Net("vout")), 73 | R("100", "Rc", to=vcc), 74 | ) 75 | ``` 76 | 77 | Note: One can find a completed version of this amplifier in `examples/class_a.py`: 78 | 79 | ```bash 80 | python3 -i examples/class_a.py 81 | ``` 82 | 83 | 84 | One can now give automatic consecutive reference designators to components that haven't been named manually already: 85 | 86 | ```python 87 | >>> global_context.autoname() 88 | ``` 89 | 90 | Then one can explore the circuit: 91 | 92 | ```python 93 | >>> global_context.parts_list 94 | [R1, R2, R3, Rc, C10, C11, Q1, C12] 95 | 96 | >>> global_context.parts_list[0].refdes 97 | 'R1' 98 | >>> global_context.parts_list[0].value 99 | '1kΩ' 100 | >>> global_context.parts_list[0].pins 101 | (R1.P1, R1.P2) 102 | >>> global_context.parts_list[0].pins[1].net 103 | VCC(connected to R1.P2, R2.P2) 104 | 105 | >>> nets 106 | OrderedDict([('VCC', VCC(connected to R1.P2, R2.P2)), ('GND', GND(connected to R3.P2, Rc.P2, C10.-)), ('VIN', VIN(connected to C11.-)), ('VOUT', VOUT(connected to C12.-))]) 107 | 108 | >>> nets["GND"] 109 | GND(connected to R3.P2, Rc.P2, C10.-) 110 | ``` 111 | 112 | ## Examples 113 | 114 | Found in the `examples/` folder. Another way to make sure the environment is sane. 115 | One can just "run" any example schematic with python, add -i to do more analysis operations on the schematic. 116 | 117 | * `voltage_divider.py`: Very simple voltage divider 118 | * `class_a.py`: Class A transistor amplifier, a good example to how a complicatedish analog circuit would look like. 119 | * `servo_micro.py`: **Servo micro schematics**, reimplementation in pcbdl, [originally an 8 page pdf schematic](https://chromium.googlesource.com/chromiumos/third_party/hdctools/+/refs/heads/master/docs/servo_micro.md#overview). 120 | 121 | ## Exporting 122 | 123 | ### Netlists 124 | 125 | The main goal of this language is to aid in creating PCBs. The intermediate file format that layout programs (where one designs physical boards) is called a netlist. We must support outputting to that (or several of such formats). 126 | 127 | We have a an exporter for the Cadence Allegro Third Party netlists (found in `netlist.py`): 128 | 129 | ```python 130 | >>> generate_netlist("/tmp/some_export_location") 131 | ``` 132 | 133 | ### HTML 134 | 135 | This produces a standalone html page with everything cross-linked: 136 | 137 | * List of nets with links to the parts and pins on each 138 | * List of parts with the part properties and a list of pins linking to the nets connected 139 | * Highlighted source code, with every variable and object linked to the previous 2 lists 140 | 141 | Here's an example of such a [html output for servo micro](https://google.github.io/pcbdl/examples/servo_micro.html). 142 | 143 | ### Schematics / Graphical representation of the circuit 144 | 145 | In order for schematics to be more easily parsable, we want to graphically display them. 146 | The [netlistsvg](https://github.com/nturley/netlistsvg) project has been proven to be an excellent tool to solve the hard problems of this. See `pcbdl/netlistsvg.py` for the implementation. 147 | To use the following commands you'll have to [install netlistsvg](https://google.github.io/pcbdl/doc/_build/html/netlistsvg.html#installation). 148 | 149 | 1. Convert a pcbdl schematic back into a traditional schematic 150 | ```python 151 | generate_svg('svg_filename') 152 | ``` 153 | 154 | Here's the [svg output for the servo_micro example](https://google.github.io/pcbdl/examples/servo_micro.svg). 155 | 156 | 2. Isolated schematics for how a particular thing is hooked up: 157 | * I2C map, ([servo_micro's](https://google.github.io/pcbdl/examples/servo_micro.i2c.svg)) 158 | ```python 159 | generate_svg('svg_filename', net_regex='.*(SDA|SCL).*', airwires=0) 160 | ``` 161 | 162 | * Power connections ([servo_micro's](https://google.github.io/pcbdl/examples/servo_micro.power.svg)) 163 | ```python 164 | generate_svg('svg_filename', net_regex='.*(PP|GND|VIN|VBUS).*') 165 | ``` 166 | 167 | 3. Block diagrams of the overall system 168 | * This depends on how the schematics is declared, if it's not hierarchical enough, it won't have many "blocks" to display 169 | * This task is dependent on allowing hierarchies in pcbdl 170 | 171 | ### BOM 172 | 173 | Bill of Materials would be a trivial thing to implement in pcbdl. 174 | 175 | ## ERC 176 | 177 | Electrical Rule Checking. How to unit test a schematic? 178 | 179 | This is a big **TODO** item. The basic idea is that the pins will get annotated more heavily than normal EDA software. 180 | 181 | Pins will have beyond just simple input/output/open drain/power properties, but will go into detail with things like: 182 | * Power well for both inputs and outputs 183 | * ViH, ViL 184 | * Output voltages 185 | 186 | With this information it should be possible to make isolated spice circuits to check for current leaks. 187 | For every net, for every combination of output pins on that net, are all the input pins receiving proper voltages? 188 | 189 | ## Importing from traditional EDA schematics 190 | 191 | Given that graphical exporting might be impossible, and in lieu of the language being slightly more unreadable than normal schematics, perhaps we should just use pcbdl as an intermediate data format or a library. 192 | 193 | The way one would use it would be to import a kicad schematic, annotate it with a few more classes (for BOM and ERC purposes, unless we can find a way to put all metadata in the kicad schematics). Then all exporting and analysis features of pcbdl can still be used. 194 | 195 | A kicad importer should be pretty trivial to implement. **TODO** 196 | 197 | ## Support 198 | 199 | This is not an officially supported Google product. 200 | 201 | The language itself is still in flux, things might change. A lot of the syntax was added as a demo of what could be possible, might still need polishing. Please don't use this language without expecting some tinkering to keep your schematics up to date to the language. 202 | 203 | ## Credits / Thanks 204 | 205 | * [CBOLD](http://cbold.com/) for the idea 206 | * [netlistsvg](https://github.com/nturley/netlistsvg) for svg output support 207 | * Chrome OS Hardware Design Team for feedback 208 | -------------------------------------------------------------------------------- /test/integration/servo_original.rpt: -------------------------------------------------------------------------------- 1 | (NETLIST) 2 | (FOR DRAWING: C:/Users/rajsrinivasan/projects/servo/06222018.brd) 3 | (Tue Apr 14 14:01:35 2020) 4 | $PACKAGES 5 | AXK850145WG ! 'PANASONIC_P4_50P-210-01017-00-AXK850145WG' ; J3 6 | BGA4C40P2X2_80X80X56 ! 'LOAD_SWITCH_WLCSP-4-313-00729-00-BGA4C40P2X2_80X80X5, 7 | 6' ; U23 U24 U42 U43 8 | BGA5C50P3X2_141X91X50L ! 'SN74LVC1G126_BGA5-313-00911-00,DEFAULT-BGA5C50P3X2, 9 | _141X91X50L' ; U10 10 | BGA10C50P4X3_186X136X50L ! 'TS3A24159_BGA-313-00071-00-BGA10C50P4X3_186X136X, 11 | 50L' ; U5 12 | CAPC0603X33L ! 'CAP_0201-150-00004-00-(0201_MICRO,CAPC05025X13N),CAPC0603X33, 13 | L' ! '1.0uF' ; C19 14 | CAPC0603X33L ! 'CAP_0201-150-00006-00-(0201_MICRO,CAPC05025X13N),CAPC0603X33, 15 | L' ! '0.1uF' ; C3 C14 C20 C22 C25 C29 C33 C76 C77 C90 C91 C106 C107 , 16 | C119 C120 C2 C5 C6 C8 C11 C12 C108 C109 C110 C111 17 | CAPC0603X33L ! 'CAP_0201-150-00316-00-(0201_MICRO,CAPC05025X13N),CAPC0603X33, 18 | L' ! 100pF ; C7 19 | CAPC0603X33L ! 'CAP_0201-150-00461-00-(0201_MICRO,CAPC05025X13N),CAPC0603X33, 20 | L' ! 1000pF ; C15 21 | CAPC1005X71L ! 'CAP_0402-150-00024-00-(CAPC1005X71N,0402_LARGE_MICRO,0402_MI, 22 | CRO,CAPC1005X55L,CAPC1005X60L,CAPC1005X65L,CAPC1005X52L),CAPC1005X71, 23 | L' ! '2.2uF' ; C9 24 | CAPC1005X71L ! 'CAP_0402-150-00039-00-(CAPC1005X71N,0402_LARGE_MICRO,0402_MI, 25 | CRO,CAPC1005X55L,CAPC1005X60L,CAPC1005X65L,CAPC1005X52L),CAPC1005X71, 26 | L' ! '4.7uF' ; C1 C13 27 | CAPC1005X71L ! 'CAP_0402-150-00189-00-(CAPC1005X71N,0402_LARGE_MICRO,0402_MI, 28 | CRO,CAPC1005X55L,CAPC1005X60L,CAPC1005X65L,CAPC1005X52L),CAPC1005X71, 29 | L' ! 1uF ; C4 30 | CAPC1608X80L ! 'CAP_0603-150-00191-00-(CAPC1608X80N,0603_LARGE_MICRO,0603_MI, 31 | CRO,CAPC1608X95L,CAPC1608X100L),CAPC1608X80L' ! 10uF ; C10 32 | 'DFN100X60X35-3L' ! 'NMOS_0402-480-00231-00-(DFN98X58X50-3M),DFN100X60X35-3L, 33 | ' ; Q4 34 | 'HRS_FH34SRJ-8S-0-5SH' ! 'FFC_8PIN_HRS_FH34-210-00794-00-HRS_FH34SRJ-8S-0-5S, 35 | H' ; CN2 36 | INDC1005L ! 'FERRITE_BEAD-185-00019-00' ! '600@100Mhz' ; FB1 37 | 'QFN05P_7-1X7-1_0-6_49N' ! 'STM32F072XX_QFN48-313-00875-00-QFN05P_7-1X7-1_0-, 38 | 6_49N' ; U6 39 | 'QFN40P145X185X55-10N' ! 'SN74AVC2T245_RSW-000-49438-00-QFN40P145X185X55-10N, 40 | ' ; U4 U45 41 | 'QFN40P265X185X55-16N' ! 'SN74AVC4T774_RSV16-313-01231-00-QFN40P265X185X55-1, 42 | 6N' ; U55 U56 U57 U59 43 | 'QFN50P400X400X080-25N' ! 'SMBUS_IO_XPANDER_QFN24-313-00942-00,DEFAULT-QFN50, 44 | P400X400X080-25N' ; U9 45 | RESC0603X23L ! 'RES_0201-470-00012-00-(RESC05025X23N, 0201_MICRO),RESC0603X2, 46 | 3L' ! '4.7K' ; R48 R60 R61 R95 R96 R501 R502 47 | RESC0603X23L ! 'RES_0201-470-00484-00-(RESC05025X23N, 0201_MICRO),RESC0603X2, 48 | 3L' ! '51.1K' ; R22 R23 49 | RESC0603X23L ! 'RES_0201-470-02017-00-(RESC05025X23N, 0201_MICRO),RESC0603X2, 50 | 3L' ! 220 ; R46 51 | 'SAMTEC_FTSH-105-01-L-DV-K' ! 'HDR_2X5_50MIL-210-00939-00-SAMTEC_FTSH-105-01, 52 | -L-DV-K' ; J4 53 | 'SON50P150X150X80-6L' ! 'LDO_FIXED_TLV700XX_SON-313-01342-00-SON50P150X150X8, 54 | 0-6L' ; U7 55 | 'SON65P100X100X40-5T48X48N' ! 'MIC550X_DFN4-313-01318-00-SON65P100X100X40-5T, 56 | 48X48N' ! ! '2.0%' ; U3 57 | 'SOP50P170X60-5N' ! 'ESD_ARRAY_DRL-430-00008-00-SOP50P170X60-5N' ; D1 58 | 'SOP50P170X60-6N' ! 'SN74AVC1T45_DRL-313-01037-00-SOP50P170X60-6N' ; U14 59 | 'SOT65P210X110-6L' ! '74LVC1G97_SOT363-313-00929-00-SOT65P210X110-6L' ; U1 60 | 'SOT95P247X115-3L' ! 'SCHOTTKY_BAT54C_SOT23-480-00317-00-(),SOT95P247X115-3L, 61 | ' ; D2 62 | 'TE_1981568-1' ! 'USB_TYPE_MICRO_B_RA_SMT-210-00266-00-(TE-1981568-5N),TE_19, 63 | 81568-1' ; CN1 64 | TP075 ! 'TESTPOINT_30X30MIL_SMT-000-47086-00-TP075' ; TP1 TP2 65 | $NETS 66 | DUT_COLD_RESET_L ; J3.14 U9.6 67 | DUT_DEV_MODE ; J3.43 U9.15 68 | DUT_GOOG_REC_MODE_L ; J3.30 U9.10 69 | DUT_JTAG_RTCK ; J3.26 U55.11 70 | DUT_JTAG_TCK ; J3.21 J4.4 U55.9 71 | DUT_JTAG_TDI ; J3.24 J4.8 U55.10 U56.9 72 | DUT_JTAG_TDO ; J3.25 J4.6 U55.12 73 | DUT_JTAG_TMS ; J3.23 J4.2 U56.10 74 | DUT_JTAG_TRST_L ; J3.27 U56.11 75 | DUT_LID_OPEN ; J3.44 U9.14 76 | DUT_MFG_MODE ; J3.40 R46.2 U9.3 U9.11 77 | DUT_MFG_MODE_BUF ; R46.1 U14.4 78 | DUT_PWR_BUTTON ; J3.22 U9.7 79 | DUT_SPI1_CLK ; J3.9 U57.9 80 | DUT_SPI1_CS ; J3.10 U57.10 81 | DUT_SPI1_MISO ; J3.12 U57.12 82 | DUT_SPI1_MOSI ; J3.11 U57.11 83 | DUT_SPI2_CLK ; J3.2 U59.9 84 | DUT_SPI2_CS ; J3.3 U59.10 85 | DUT_SPI2_MISO ; J3.5 U59.12 86 | DUT_SPI2_MOSI ; J3.4 U59.11 87 | DUT_WARM_RESET_L ; J3.28 J4.10 U9.8 88 | EC_UART_RX ; CN2.3 U6.31 89 | EC_UART_TX ; CN2.2 U6.30 90 | FTDI_MFG_MODE ; U9.2 U14.3 91 | FW_UP_L ; J3.41 U9.13 92 | FW_WP_EN ; C33.1 R48.1 U9.1 U14.1 U14.5 93 | GND ; C1.2 C2.2 C3.2 C4.2 C5.2 C6.2 C7.2 C8.2 C9.2 C10.2 C11.2 C12.2 C13.2 , 94 | C14.2 C15.2 C19.2 C20.2 C22.2 C25.2 C29.2 C33.2 C76.2 C77.2 C90.2 , 95 | C91.2 C106.2 C107.2 C108.2 C109.2 C110.2 C111.2 C119.2 C120.2 CN1.5 , 96 | CN1.G1 CN1.G2 CN1.G3 CN1.G4 CN2.1 CN2.G1 CN2.G2 D1.4 J3.1 J3.8 , 97 | J3.15 J3.20 J3.31 J3.36 J3.42 J4.3 J4.5 Q4.S R48.2 R60.2 R61.2 , 98 | R95.2 R96.2 U1.2 U3.2 U3.G1 U4.1 U4.3 U5.A2 U6.8 U6.23 U6.35 U6.47 , 99 | U6.49 U7.2 U9.9 U9.18 U9.25 U10.C1 U14.2 U23.B2 U24.B2 U42.B2 , 100 | U43.B2 U45.1 U45.3 U55.5 U55.8 U55.15 U55.16 U56.8 U57.8 U57.15 , 101 | U59.8 U59.15 102 | HPD ; J3.39 U9.12 103 | I2C_REMOTE_ADC_SCL ; J3.38 R501.2 U6.45 U9.19 104 | I2C_REMOTE_ADC_SDA ; J3.37 R502.2 U6.46 U9.20 105 | JTAG_BUFFER_TO_SERVO_TDO ; U5.A1 U6.11 U10.C2 106 | JTAG_BUFIN_EN_L ; U6.6 U55.7 107 | JTAG_BUFOUT_EN_L ; U6.5 U56.7 108 | PCH_DISABLE_L ; J3.45 U9.16 109 | PD_BOOT0 ; CN2.8 Q4.D R23.2 U6.44 110 | PD_NRST_L ; C8.1 CN2.6 U6.7 111 | PP1800_VIN ; C22.1 D2.3 U7.1 U7.6 112 | PP3300_PD_VDDA ; C4.1 C7.1 FB1.2 U6.9 113 | PP1800 ; C19.1 C20.1 U7.3 U9.21 U23.A1 U43.A1 114 | PP3300 ; C1.1 C2.1 C3.1 C5.1 C6.1 C10.1 C11.1 C12.1 C13.1 C14.1 C15.1 C25.1 , 115 | C76.1 C91.1 C107.1 C109.1 C111.1 C120.1 D2.1 D2.2 FB1.1 J3.35 , 116 | R501.1 R502.1 U1.5 U3.1 U4.7 U4.10 U5.D2 U6.1 U6.24 U6.36 U6.48 , 117 | U9.23 U10.A2 U24.A1 U42.A1 U45.7 U45.10 U55.6 U55.14 U56.1 U56.14 , 118 | U56.15 U57.5 U57.6 U57.14 U57.16 U59.5 U59.6 U59.14 U59.16 119 | PPDUT_JTAG_VREF ; C108.1 C110.1 J3.29 J4.1 U55.13 U56.13 120 | PPDUT_SPI1_VREF ; C106.1 J3.13 U42.A2 U43.A2 U57.13 121 | PPDUT_SPI2_VREF ; C29.1 C119.1 J3.6 U14.6 U23.A2 U24.A2 U59.13 122 | PPDUT_UART1_VREF ; C77.1 J3.34 U45.6 123 | PPDUT_UART2_VREF ; C90.1 J3.18 U4.6 124 | RESET_L ; U6.2 U9.24 125 | SERVO_JTAG_MUX_TDO ; U1.4 U10.B1 126 | SERVO_JTAG_RTCK ; U6.19 U55.2 127 | SERVO_JTAG_SWDIO ; U1.1 U55.3 128 | SERVO_JTAG_TCK ; U5.A3 U6.10 U55.4 129 | SERVO_JTAG_TDI ; U6.17 U56.4 130 | SERVO_JTAG_TDI_DIR ; U6.43 U56.6 131 | SERVO_JTAG_TDO ; U1.3 U55.1 132 | SERVO_JTAG_TDO_BUFFER_EN ; U6.16 U10.A1 133 | SERVO_JTAG_TDO_SEL ; U1.6 U6.4 134 | SERVO_JTAG_TMS ; U6.14 U56.3 135 | SERVO_JTAG_TMS_DIR ; U6.3 U56.5 136 | SERVO_JTAG_TRST_DIR ; U6.42 U56.16 137 | SERVO_JTAG_TRST_L ; U6.34 U56.2 138 | SERVO_SPI_CS ; U6.25 U57.3 U59.3 139 | SERVO_SPI_MOSI ; U6.28 U57.2 U59.2 140 | SERVO_TO_SPI1_MUX_CLK ; U5.D3 U6.26 U59.4 141 | SERVO_TO_SPI1_MUX_MISO ; U5.D1 U6.27 U59.1 142 | SPI1_BUF_EN_L ; U6.37 U57.7 143 | SPI1_MUX_SEL ; U5.B1 U5.B3 U6.15 144 | SPI1_VREF_18 ; R95.1 U6.39 U43.B1 145 | SPI1_VREF_33 ; R96.1 U6.20 U42.B1 146 | SPI2_BUF_EN_L ; U6.38 U59.7 147 | SPI2_VREF_18 ; R61.1 U6.41 U23.B1 148 | SPI2_VREF_33 ; R60.1 U6.40 U24.B1 149 | SPI_HOLD_L ; J3.7 U9.5 150 | SPI_MUX_TODUT_SPI1_MISO ; U5.C1 U57.1 151 | SPI_MUX_TO_DUT_SPI1_CLK ; U5.C3 U57.4 152 | UART1_DUT_SERVO_TX ; J3.33 U45.4 153 | UART1_EN_L ; U6.29 U45.2 154 | UART1_RX ; U6.13 U45.9 155 | UART1_SERVO_DUT_TX ; J3.32 U45.5 156 | UART1_TX ; U6.12 U45.8 157 | UART2_DUT_SERVO_TX ; J3.17 U4.4 158 | UART2_EN_L ; U4.2 U6.18 159 | UART2_RX ; U4.9 U6.22 160 | UART2_SERVO_DUT_TX ; J3.16 U4.5 161 | UART2_TX ; U4.8 U6.21 162 | UNNAMED_9_SMBUSIOXPANDER_I81_P0 ; TP1.1 U9.4 163 | UNNAMED_9_SMBUSIOXPANDER_I81_P1 ; TP2.1 U9.17 164 | USB_DM ; CN1.2 D1.2 D1.5 U6.32 165 | USB_DP ; CN1.3 D1.3 U6.33 166 | USB_ID ; CN1.4 Q4.G R22.2 167 | VBUS_IN ; C9.1 CN1.1 D1.1 R22.1 R23.1 U3.3 U3.4 168 | $END 169 | -------------------------------------------------------------------------------- /pcbdl/context.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from .base import Net, Part, Plugin 16 | from .defined_at import grab_nearby_lines 17 | import collections 18 | import csv 19 | import hashlib 20 | 21 | __all__ = [ 22 | "Context", 23 | "global_context", "nets", 24 | ] 25 | 26 | class RefdesRememberer: 27 | """ 28 | Remembers refdeses from old executions of the schematic. This tries to guarantee that as the schematic 29 | evolves, automatically generated refdefs are deterministic. It does this by storing the refdefs in a 30 | file (.refdes_mapping), in part definition order, together with some information about the part (called 31 | anchors). Even partial matches of the anchors will be enough to remember the name. 32 | 33 | .. warning:: This class is very stateful, successful matches consume the entries from the internal state. 34 | """ 35 | 36 | anchor_names = ("code", "nets", "variable_name", "class", "value", "part_number") 37 | 38 | """[(refdes, {anchor_name: anchor})]""" 39 | _mapping = [] 40 | 41 | csv.register_dialect("pcbdl", delimiter="\t", lineterminator="\n", strict=True) #TODO: strict=False 42 | 43 | class MatchNotFound(Exception): 44 | pass 45 | 46 | def __init__(self, filename): 47 | self.filename = filename 48 | self.read() 49 | 50 | def read(self): 51 | """ 52 | Read in the existing .refdes_mapping file and populate the internal state 53 | """ 54 | self._mapping = [] 55 | try: 56 | with open(self.filename, "r") as f: 57 | reader = csv.DictReader(f, dialect="pcbdl") 58 | for row in reader: 59 | refdes = row.pop("refdes") 60 | self._mapping.append((refdes, row)) 61 | except FileNotFoundError: 62 | pass # We'll start fresh! 63 | 64 | def find_match(self, part, score_threshold=0.6, debug=False): 65 | """ 66 | Given a part, finds a match in the older mapping based on the current values of the anchors. 67 | 68 | If most of them match (given the score) with an older entry the refdes of that entry is returned and the entry 69 | is removed from the matches so it's not matched in the future with another part. 70 | """ 71 | current_anchors = self.get_part_anchors(part) 72 | max_score = len(self.anchor_names) 73 | 74 | if not self._mapping: 75 | raise self.MatchNotFound("Empty state.") 76 | 77 | scored_others = [] # score, (refdes, anchors) 78 | for refdes, older_anchors in self._mapping: 79 | score = 0 80 | 81 | for anchor_name in current_anchors.keys(): 82 | if anchor_name not in older_anchors: 83 | continue # change of schema? 84 | 85 | if current_anchors[anchor_name] == older_anchors[anchor_name]: 86 | score += 1 87 | 88 | scored_others.append((score, (refdes, older_anchors))) 89 | 90 | scored_others.sort(key=(lambda other: other[0]), reverse=True) 91 | first_match = scored_others[0] 92 | score, row = first_match 93 | if score < max_score * score_threshold: 94 | raise self.MatchNotFound("Score %d/%d too low." % (score, max_score)) 95 | 96 | refdes, older_anchors = row 97 | 98 | # some logging if it's inexact 99 | if debug and (score != max_score): 100 | print(f"RefdesRememberer: Inexact match ({score}/{max_score}) for {part}:") 101 | for anchor_name in current_anchors.keys(): 102 | if anchor_name not in older_anchors: 103 | print(" [%r] %r not found" % (anchor_name, current_anchors[anchor_name])) 104 | continue # change of schema? 105 | if current_anchors[anchor_name] != older_anchors[anchor_name]: 106 | print(" [%r] %r!=%r" % (anchor_name, current_anchors[anchor_name], older_anchors[anchor_name])) 107 | 108 | #make sure nobody else matches with this row again, since we already found the instance matching it 109 | self._mapping.remove(row) 110 | 111 | return refdes 112 | 113 | def get_part_anchors(self, part): 114 | """ 115 | Generates a dict of anchors (keys being anchor_names) for a given part. 116 | """ 117 | anchors = {} 118 | anchors["code"] = part.plugins[PartContext]._anchor_code 119 | anchors["nets"] = part.plugins[PartContext]._anchor_nets 120 | try: 121 | anchors["variable_name"] = part.variable_name 122 | except AttributeError: 123 | anchors["variable_name"] = "" 124 | anchors["class"] = repr(part.__class__) 125 | anchors["value"] = part.value 126 | anchors["part_number"] = part.part_number 127 | 128 | assert(set(anchors.keys()) == set(self.anchor_names)) 129 | return anchors 130 | 131 | def overwrite(self, context): 132 | """ 133 | Writes the context (all the refdeses and new computed anchors) to a file, 134 | ready to read for next time. 135 | """ 136 | with open(self.filename, "w") as f: 137 | writer = csv.DictWriter(f, dialect="pcbdl", fieldnames=("refdes",) + self.anchor_names) 138 | writer.writeheader() 139 | for refdes, part in context.named_parts.items(): 140 | row = self.get_part_anchors(part) 141 | row["refdes"] = refdes 142 | writer.writerow(row) 143 | 144 | class Context(object): 145 | def __init__(self, name = ""): 146 | self.name = name 147 | 148 | self.net_list = [] 149 | self.parts_list = [] 150 | self.named_nets = collections.OrderedDict() 151 | 152 | def new_part(self, part): 153 | assert(part not in self.parts_list) 154 | 155 | if part.refdes in (other_part.refdes for other_part in self.parts_list): 156 | raise Exception("Cannot have more than one part with the refdes %s in %s" % (part.refdes, self)) 157 | 158 | # Add to the part list 159 | self.parts_list.append(part) 160 | 161 | def new_net(self, net): 162 | assert(net not in self.net_list) 163 | 164 | if net.name in self.named_nets: 165 | raise Exception("Cannot have more than one net called %s in %s" % (net.name, self)) 166 | 167 | # Add to the net list 168 | self.net_list.append(net) 169 | self.named_nets[net.name] = net 170 | 171 | def autoname(self, mapping_file=None): 172 | self.named_parts = collections.OrderedDict() 173 | 174 | if mapping_file: 175 | refdes_rememberer = RefdesRememberer(mapping_file) 176 | 177 | # Do a pass trying to remember it 178 | for part in self.parts_list: 179 | original_name = part.refdes 180 | prefix = part.REFDES_PREFIX 181 | if original_name.startswith(prefix): 182 | number = original_name[len(prefix):] 183 | 184 | if number.startswith("?"): 185 | try: 186 | refdes = refdes_rememberer.find_match(part) 187 | except RefdesRememberer.MatchNotFound: 188 | continue 189 | part.refdes = refdes 190 | number = refdes[len(prefix):] 191 | #print("Remembering refdes %s -> %s" % (original_name, part.refdes)) 192 | 193 | if part.refdes in (other_part.refdes for other_part in self.parts_list if other_part != part): 194 | raise Exception("Cannot have more than one part with the refdes %s in %s" % (part.refdes, self)) 195 | 196 | # Another pass by naming things with the autoincrement 197 | self.refdes_counters = collections.defaultdict(lambda:1) 198 | for part in self.parts_list: 199 | original_name = part.refdes 200 | prefix = part.REFDES_PREFIX 201 | if original_name.startswith(prefix): 202 | number = original_name[len(prefix):] 203 | 204 | if number.startswith("?"): 205 | while True: 206 | part.refdes = "%s%d" % (prefix, self.refdes_counters[prefix]) 207 | self.refdes_counters[prefix] += 1 208 | if part.refdes not in self.named_parts: 209 | break 210 | print("New refdes %s -> %s" % (original_name, part.refdes)) 211 | else: 212 | # Yay, there's a part that's already named 213 | # Let's remember the number for it and save it 214 | try: 215 | number = int(number) 216 | except ValueError: 217 | pass 218 | else: 219 | self.refdes_counters[prefix] = number 220 | #print("Skipping ahead to %s%d+1" % (prefix, self.refdes_counters[prefix])) 221 | self.refdes_counters[prefix] += 1 222 | self.named_parts[part.refdes] = part 223 | 224 | if mapping_file: 225 | refdes_rememberer.overwrite(self) 226 | del refdes_rememberer 227 | 228 | for net in self.net_list: 229 | # Look only for unnamed nets 230 | if net.has_name: 231 | continue 232 | 233 | old_name = net.name 234 | new_name = "ANON_NET_%s" % str(net.connections[0]).replace(".","_") 235 | net.name = new_name 236 | 237 | # Update named_nets list with the new name 238 | if new_name in self.named_nets: 239 | raise Exception("Cannot have more than one net called %s in %s" % (net.name, self)) 240 | self.named_nets[new_name] = net 241 | del self.named_nets[old_name] 242 | 243 | 244 | @Plugin.register(Net) 245 | class NetContext(Plugin): 246 | def __init__(self, instance): 247 | global_context.new_net(instance) 248 | 249 | @Plugin.register(Part) 250 | class PartContext(Plugin): 251 | def __init__(self, instance): 252 | self.instance = instance 253 | global_context.new_part(instance) 254 | 255 | def _generate_anchor_code(self): 256 | if not hasattr(self.instance, "defined_at"): 257 | self._context_ref_value = None 258 | raise Exception("No defined_at") 259 | 260 | if self.instance.defined_at.startswith(""): 261 | self._context_ref_value = None 262 | raise Exception("Can't get context from stdin") 263 | 264 | 265 | tohash = repr(( 266 | grab_nearby_lines(self.instance.defined_at, 3), 267 | )) 268 | 269 | h = hashlib.md5(tohash.encode("utf8")).hexdigest() 270 | 271 | ret = "c" + h[:8] 272 | self._context_ref_value = ret 273 | return ret 274 | 275 | @property 276 | def _anchor_code(self): 277 | try: 278 | return self._anchor_code_value 279 | except AttributeError: 280 | pass 281 | 282 | self._anchor_code_value = self._generate_anchor_code() 283 | return self._anchor_code_value 284 | 285 | def _generate_anchor_nets(self): 286 | tohash = repr(( 287 | sorted(pin.net.name for pin in self.instance.pins if (pin._net is not None and "ANON_NET" not in pin.net.name)), 288 | )) 289 | 290 | h = hashlib.md5(tohash.encode("utf8")).hexdigest() 291 | 292 | ret = "n" + h[:8] 293 | self._context_ref_value = ret 294 | return ret 295 | 296 | @property 297 | def _anchor_nets(self): 298 | try: 299 | return self._anchor_nets_value 300 | except AttributeError: 301 | pass 302 | 303 | self._anchor_nets_value = self._generate_anchor_nets() 304 | return self._anchor_nets_value 305 | 306 | global_context = Context() 307 | nets = global_context.named_nets 308 | -------------------------------------------------------------------------------- /pcbdl/html.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from .base import Part, PartInstancePin, Net, Plugin 16 | from .context import * 17 | from .netlistsvg import generate_svg 18 | import pcbdl.defined_at 19 | 20 | import collections 21 | from datetime import datetime 22 | import html 23 | import inspect 24 | import itertools 25 | import os 26 | import textwrap 27 | 28 | import pygments 29 | import pygments.lexers 30 | import pygments.formatters 31 | 32 | """HTML output format""" 33 | __all__ = ["generate_html"] 34 | 35 | _PCBDL_BUILTINS_PATH = os.path.dirname(pcbdl.__file__) 36 | 37 | @Plugin.register((Net, Part)) 38 | class HTMLDefinedAt(Plugin): 39 | def register(self): 40 | self.defined_at = self.instance.defined_at 41 | 42 | self.filename, self.line = self.defined_at.rsplit(":", 1) 43 | self.line = int(self.line) 44 | 45 | self.code_manager.instanced_here(self.instance, self.filename, self.line) 46 | 47 | @property 48 | def href_line(self): 49 | return "

Defined at: %s

" % (self.filename, self.line, self.defined_at) 50 | 51 | @Plugin.register(Part) 52 | class HTMLPart(Plugin): 53 | def class_list_generator(self): 54 | l = self.instance.__class__.__mro__ 55 | l = l[:l.index(Part) + 1] 56 | for cls in l: 57 | filename, line = inspect.getsourcelines(cls) 58 | filename = os.path.relpath(inspect.getsourcefile(cls), pcbdl.defined_at.cwd) 59 | if filename in self.code_manager.file_database: 60 | yield "%s" % (filename, line, html.escape(repr(cls))) 61 | else: 62 | yield "%s" % html.escape(repr(cls)) 63 | 64 | @property 65 | def part_li(self): 66 | part = self.instance 67 | 68 | class_str = "" 69 | if not part.populated: 70 | class_str = " class=\"not-populated\"" 71 | 72 | yield "

%s

" % (class_str, part.refdes, part.refdes) 73 | 74 | yield part.plugins[HTMLDefinedAt].href_line 75 | yield "

%s

" % ", ".join(self.class_list_generator()) 76 | try: 77 | yield "

Variable Name: %s

" % part.variable_name 78 | except AttributeError: 79 | pass 80 | 81 | if part.__doc__: 82 | yield "
%s
" % textwrap.dedent(part.__doc__.rstrip()) 83 | 84 | yield "

See in SVG

" % part.refdes 85 | 86 | yield "

Value: %s

" % part.value 87 | yield "

Part Number: %s

" % part.part_number 88 | if not part.populated: 89 | yield "

Do Not Populate!

" 90 | try: 91 | yield "

Package: %s

" % part.package 92 | except AttributeError: 93 | yield "Package not defined" 94 | 95 | real_pin_count = len({number for pin in part.pins for number in pin.numbers}) 96 | yield "

%d logical pins (%d real pins):

    " % (len(part.pins), real_pin_count) 97 | for pin in part.pins: 98 | yield "
  • %s (%s)" % (pin.part.refdes, pin.name, " / ".join(pin.names), ', '.join(pin.numbers)) 99 | 100 | try: 101 | net_name = pin._net.name 102 | yield "net: %s" % (net_name, net_name) 103 | except AttributeError: 104 | pass 105 | 106 | try: 107 | yield "well: %s" % (pin.well.plugins[HTMLPin].short_anchor) 108 | except AttributeError: 109 | pass 110 | 111 | yield "
  • " 112 | yield "
" 113 | yield "" 114 | 115 | @Plugin.register(Net) 116 | class HTMLNet(Plugin): 117 | @property 118 | def net_li(self): 119 | net = self.instance 120 | name = net.name 121 | 122 | yield "
  • %s

    " % (name, name) 123 | yield net.plugins[HTMLDefinedAt].href_line 124 | 125 | try: 126 | yield "

    Variable Name: %s

    " % net.variable_name 127 | except AttributeError: 128 | pass 129 | 130 | yield "

    %d connections:

      " % len(net.connections) 131 | for pin in net.connections: 132 | yield "
    • %s
    • " % (pin.plugins[HTMLPin].full_anchor) 133 | yield "
    " 134 | 135 | yield "
  • " 136 | 137 | @Plugin.register(PartInstancePin) 138 | class HTMLPin(Plugin): 139 | @property 140 | def short_anchor(self): 141 | pin = self.instance 142 | return "%s" % (pin.part.refdes, pin.name, pin.name) 143 | 144 | @property 145 | def full_anchor(self): 146 | pin = self.instance 147 | part_anchor = "%s." % (pin.part.refdes, pin.part.refdes) 148 | return part_anchor + self.short_anchor 149 | 150 | class Code: 151 | class CodeHtmlFormatter(pygments.formatters.HtmlFormatter): 152 | def _wrap_linespans(self, inner): 153 | s = self.linespans 154 | line_no = self.linenostart - 1 155 | for t, line in inner: 156 | if t: 157 | line_no += 1 158 | variables = self.fill_variables_for_line(line_no) 159 | line = line.rstrip("\n") 160 | yield 1, '%s%s\n' % (s, line_no, line, variables) 161 | else: 162 | yield 0, line 163 | 164 | def set_source_file(self, filename, fileinstances): 165 | self.linespans = filename 166 | self.lineanchors = filename 167 | self.fileinstances = fileinstances 168 | 169 | def fill_variables_for_line(self, line_no): 170 | variables_on_this_line = self.fileinstances[line_no] 171 | 172 | if not variables_on_this_line: 173 | return "" 174 | 175 | links = [] 176 | for variable in variables_on_this_line: 177 | if isinstance(variable, Net): 178 | net_name = variable.name 179 | links.append("%s" % (net_name, net_name)) 180 | continue 181 | 182 | if isinstance(variable, Part): 183 | part = variable 184 | links.append("%s" % (part.refdes, part.refdes)) 185 | continue 186 | 187 | raise Exception("No idea how to make link for %r of type %r" % (variable, type(variable))) 188 | 189 | return "# %s" % ", ".join(links) 190 | 191 | def __init__(self): 192 | # {filename: {line: [instance]}} 193 | self.file_database = collections.defaultdict(lambda: collections.defaultdict(set)) 194 | self._instances = set() 195 | 196 | self.lexer = pygments.lexers.PythonLexer() 197 | self.formatter = self.CodeHtmlFormatter( 198 | linenos=True, 199 | linespans="undefined", 200 | 201 | anchorlinenos = True, 202 | lineanchors="undefined", 203 | 204 | cssclass="code", 205 | ) 206 | self.formatter.file_database = self.file_database 207 | 208 | def instanced_here(self, instance, filename, line): 209 | self.file_database[filename][line].add(instance) 210 | self._instances.add(instance) 211 | 212 | def css_generator(self): 213 | yield self.formatter.get_style_defs() 214 | 215 | def code_generator(self): 216 | file_list = self.file_database.keys() 217 | for filename in file_list: 218 | yield "

    %s

    " % (filename, filename) 219 | 220 | with open(filename) as f: 221 | source_code = f.read() 222 | 223 | self.formatter.set_source_file(filename, self.file_database[filename]) 224 | result = pygments.highlight(source_code, self.lexer, self.formatter) 225 | 226 | for instance in self._instances: 227 | try: 228 | variable_name = instance.variable_name 229 | except AttributeError: 230 | continue 231 | 232 | if isinstance(instance, Net): 233 | net = instance 234 | href = "#net-%s" % net.name 235 | title = "Net %s" % net 236 | 237 | if isinstance(instance, Part): 238 | part = instance 239 | href = "#part-%s" % part.refdes 240 | title = "Part %s" % part 241 | 242 | original_span = "%s" % variable_name 243 | modified_span = "%s" % (href, title, variable_name) 244 | 245 | if isinstance(instance, Part): 246 | # Linkify all the pins too 247 | for pin in part.pins: 248 | for name in pin.names: 249 | prepend = original_span + "." 250 | original_pin_span = prepend + "%s" % name 251 | 252 | title = repr(pin) 253 | href = "#pin-%s.%s" % (pin.part.refdes, pin.name) 254 | modified_pin_span = prepend + "%s" % (href, title, name) 255 | result = result.replace(original_pin_span, modified_pin_span) 256 | 257 | result = result.replace(original_span, modified_span) 258 | 259 | yield result 260 | 261 | 262 | def html_generator(context=global_context, include_svg=False): 263 | code_manager = Code() 264 | 265 | HTMLDefinedAt.code_manager = code_manager 266 | HTMLPart.code_manager = code_manager 267 | 268 | # Make sure the code_manager knows about everything already 269 | for instance in context.parts_list + context.net_list: 270 | instance.plugins[HTMLDefinedAt].register() 271 | for part in context.parts_list: 272 | l = part.__class__.__mro__ 273 | l = l[:l.index(Part) + 1] 274 | for cls in l: 275 | filename = inspect.getsourcefile(cls) 276 | if _PCBDL_BUILTINS_PATH not in filename: # we don't want pcbdl builtin files in the list 277 | filename = os.path.relpath(filename, pcbdl.defined_at.cwd) 278 | _, line = inspect.getsourcelines(cls) 279 | code_manager.instanced_here(part, filename, line) 280 | 281 | yield "" 282 | yield "" 283 | yield "" 284 | yield "PCBDL %s" % (list(code_manager.file_database.keys())[0]) 285 | yield "" 286 | 287 | yield "" 311 | yield "" 312 | yield "" 313 | 314 | yield "

    PCBDL HTML Output

    " 315 | yield "

    Contents

      " 316 | yield "
    • Parts
    • " 317 | yield "
    • Nets
    • " 318 | yield "
    • Code" 319 | yield "
        " 320 | for filename in code_manager.file_database.keys(): 321 | yield "
      • %s
      • " % (filename, filename) 322 | yield "
      " 323 | yield "
    • " 324 | if include_svg: 325 | yield "
    • SVG
    • " 326 | yield "
    " 327 | 328 | yield "

    Parts

      " 329 | for part in context.parts_list: 330 | yield from part.plugins[HTMLPart].part_li 331 | yield "
    " 332 | 333 | yield "

    Nets

      " 334 | for net in context.net_list: 335 | yield from net.plugins[HTMLNet].net_li 336 | yield "
    " 337 | 338 | yield "

    Code

    " 339 | yield from code_manager.code_generator() 340 | 341 | if include_svg: 342 | yield "

    SVG

    " 343 | yield from generate_svg(context=context, max_pin_count=50) 344 | 345 | yield "" 346 | yield "" 347 | 348 | def generate_html(*args, **kwargs): 349 | return "\n".join(html_generator(*args, **kwargs)) 350 | -------------------------------------------------------------------------------- /pcbdl/netlistsvg.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from .base import Part, PartInstancePin, Net 16 | from .context import * 17 | import pcbdl.small_parts as small_parts 18 | 19 | import collections 20 | import json 21 | import os 22 | import re 23 | import subprocess 24 | import tempfile 25 | 26 | """Renders our circuit into svg with the help of netlistsvg.""" 27 | __all__ = ["generate_svg", "SVGPage"] 28 | 29 | NETLISTSVG_LOCATION = os.path.expanduser( 30 | os.environ.get("NETLISTSVG_LOCATION", "~/netlistsvg")) 31 | 32 | NET_REGEX_ALL = ".*" 33 | 34 | class SVGNet(object): 35 | def __init__(self, instance, schematic_page): 36 | self.instance = instance 37 | self.schematic_page = schematic_page 38 | 39 | current_node_number = -1 40 | @classmethod 41 | def get_next_node_number(cls): 42 | cls.current_node_number += 1 43 | return cls.current_node_number 44 | 45 | def categorize_groups(self): 46 | self.grouped_connections = [] 47 | 48 | for original_group in self.instance.grouped_connections: 49 | group = list(original_group) # make a copy so we can fragment it 50 | group_pin_count = sum(len(pin.part.pins) for pin in group) 51 | self.grouped_connections.append(group) 52 | 53 | if self.schematic_page.airwires < 2: 54 | continue 55 | 56 | #if group_pin_count < 40: 57 | #continue 58 | 59 | first_big_part = None 60 | for pin in original_group: 61 | if len(pin.part.pins) <= 3: 62 | # part is too small, probably should stay here 63 | continue 64 | 65 | if first_big_part is None: 66 | first_big_part = pin.part 67 | continue 68 | 69 | if pin.part is not first_big_part: 70 | # too many big parts here, move this one out 71 | #print("Too many big parts in %s group %r, moving %r out" % (self.instance.name, group, pin)) 72 | group.remove(pin) 73 | self.grouped_connections.append((pin,)) 74 | 75 | 76 | self.node_numbers = [self.get_next_node_number() 77 | for group in self.grouped_connections] 78 | 79 | def _find_group(self, pin): 80 | if not hasattr(self, "node_numbers"): 81 | self.categorize_groups() 82 | 83 | for i, group in enumerate(self.grouped_connections): 84 | if pin in group: 85 | return i, group 86 | 87 | raise ValueError("Can't find pin %s on %s" % (pin, self.instance)) 88 | 89 | def get_other_pins_in_group(self, pin): 90 | _, group = self._find_group(pin) 91 | return group 92 | 93 | def get_node_number(self, pin): 94 | group_idx, _ = self._find_group(pin) 95 | 96 | if self.schematic_page.airwires == 0: 97 | return self.node_numbers[0] 98 | return self.node_numbers[group_idx] 99 | 100 | class SVGPart(object): 101 | SKIN_MAPPING = { # pcbdl_class: (skin_alias_name, pin_names, is_symmetric, is_rotatable) 102 | # more specific comes first 103 | small_parts.R: ("r_", "AB", True, True), 104 | small_parts.C: ("c_", "AB", True, True), 105 | small_parts.L: ("l_", "AB", True, True), 106 | small_parts.LED: ("d_led_", "+-", False, True), 107 | small_parts.D: ("d_", "+-", False, True), 108 | } 109 | SKIN_MAPPING_INSTANCES = tuple(SKIN_MAPPING.keys()) 110 | 111 | def __init__(self, part, schematic_page): 112 | self.part = part 113 | self.schematic_page = schematic_page 114 | 115 | self.svg_type = part.refdes 116 | self.is_skinned = False 117 | self.skin_pin_names = () 118 | self.is_symmetric = False 119 | self.is_rotatable = False 120 | 121 | for possible_class, skin_properties in self.SKIN_MAPPING.items(): 122 | if isinstance(part, possible_class): 123 | self.is_skinned = True 124 | self.svg_type, self.skin_pin_names, self.is_symmetric, self.is_rotatable = skin_properties 125 | break 126 | 127 | def attach_net_name_port(self, net, net_node_number, direction): 128 | self.schematic_page.ports_dict["%s_node%s" % (net.name, str(net_node_number))] = { 129 | "bits": [net_node_number], 130 | "direction": direction 131 | } 132 | 133 | def attach_net_name(self, net, net_node_number, display=True): 134 | netname_entry = self.schematic_page.netnames_dict[net.name] 135 | if net_node_number not in netname_entry["bits"]: # avoid duplicates 136 | netname_entry["bits"].append(net_node_number) 137 | if display: 138 | netname_entry["hide_name"] = 0 139 | 140 | def attach_power_symbol(self, net, net_node_number): 141 | name = net.name 142 | if len(name) > 10: 143 | name = name.replace("PP","") 144 | name = name.replace("_VREF","") 145 | 146 | power_symbol = { 147 | "connections": {"A": [net_node_number]}, 148 | "attributes": {"name": name}, 149 | "type": "gnd" if net.is_gnd else "vcc", 150 | } 151 | 152 | if name == "GND": 153 | # redundant 154 | del power_symbol["attributes"]["name"] 155 | 156 | cell_name = "power_symbol_%d" % (net_node_number) 157 | self.schematic_page.cells_dict[cell_name] = power_symbol 158 | 159 | def should_draw_pin(self, pin): 160 | if not pin._net: 161 | # if we don't have a pin name, do it conditionally based on if a regex is set 162 | return self.schematic_page.net_regex.pattern == NET_REGEX_ALL 163 | else: 164 | return self.schematic_page.net_regex.match(str(pin.net.name)) 165 | 166 | def add_parts(self, indent_depth=""): 167 | # Every real part might yield multiple smaller parts (eg: airwires, gnd/vcc connections) 168 | part = self.part 169 | self.schematic_page.parts_to_draw.remove(part) 170 | 171 | connections = {} 172 | port_directions = {} 173 | 174 | parts_to_bring_on_page = [] 175 | 176 | pin_count = len(part.pins) 177 | for i, pin in enumerate(part.pins): 178 | name = "%s (%s)" % (pin.name, ", ".join(pin.numbers)) 179 | if self.skin_pin_names: 180 | # if possible we need to match pin names with the skin file in netlistsvg 181 | name = self.skin_pin_names[i] 182 | 183 | DIRECTIONS = ["output", "input"] # aka right, left 184 | port_directions[name] = DIRECTIONS[i < pin_count/2] 185 | 186 | # TODO: Make this depend on pin type, instead of such a rough heuristic 187 | if "OUT" in pin.name: 188 | port_directions[name] = "output" 189 | if "IN" in pin.name: 190 | port_directions[name] = "input" 191 | if "EN" in pin.name: 192 | port_directions[name] = "input" 193 | 194 | is_connector = part.refdes.startswith("J") or part.refdes.startswith("CN") 195 | if is_connector: 196 | try: 197 | pin_number = int(pin.number) 198 | except ValueError: 199 | pass 200 | else: 201 | port_directions[name] = DIRECTIONS[pin_number % 2] 202 | 203 | pin_net = pin._net 204 | if pin_net: 205 | if hasattr(pin_net, "parent"): 206 | pin_net = pin_net.parent 207 | 208 | pin_net_helper = self.schematic_page.net_helpers[pin_net] 209 | 210 | net_node_number = pin_net_helper.get_node_number(pin) 211 | connections[name] = [net_node_number] 212 | 213 | for other_pin in pin_net_helper.get_other_pins_in_group(pin): 214 | other_part = other_pin.part 215 | parts_to_bring_on_page.append(other_part) 216 | else: 217 | # Make up a new disposable connection 218 | connections[name] = [SVGNet.get_next_node_number()] 219 | 220 | skip_drawing_pin = not self.should_draw_pin(pin) 221 | 222 | if self.is_skinned or part.refdes.startswith("Q"): 223 | # if any other pins good on a small part, we should draw the whole part (all pins) 224 | for other_pin in set(part.pins) - set((pin,)): 225 | if self.should_draw_pin(other_pin): 226 | skip_drawing_pin = False 227 | 228 | if pin in self.schematic_page.pins_to_skip: 229 | skip_drawing_pin = True 230 | 231 | if skip_drawing_pin: 232 | del connections[name] 233 | continue 234 | 235 | self.schematic_page.pins_drawn.append(pin) 236 | self.schematic_page.pin_count += 1 237 | 238 | if pin_net: 239 | display_net_name = pin.net.has_name 240 | if pin.net.is_gnd or pin.net.is_power: 241 | self.attach_power_symbol(pin.net, net_node_number) 242 | display_net_name = False 243 | self.attach_net_name(pin.net, net_node_number, display=display_net_name) 244 | 245 | if not connections: 246 | return 247 | 248 | if self.is_rotatable: 249 | suffix = "h" 250 | swap_pins = False 251 | for i, pin in enumerate(part.pins): 252 | if pin.net.is_power: 253 | suffix = "v" 254 | if i != 0: 255 | swap_pins = True 256 | if pin.net.is_gnd: 257 | suffix = "v" 258 | if i != 1: 259 | swap_pins = True 260 | #TODO maybe keep it horizontal if both sides .is_power 261 | 262 | if swap_pins and self.is_symmetric: 263 | mapping = {"A": "B", "B": "A"} 264 | connections = {mapping[name]:v 265 | for name, v in connections.items()} 266 | 267 | if self.is_rotatable: 268 | self.svg_type += suffix 269 | 270 | self.schematic_page.cells_dict[self.part.refdes] = { 271 | "connections": connections, 272 | "port_directions": port_directions, 273 | "attributes": {"value": part.value}, 274 | "type": self.svg_type 275 | } 276 | 277 | print(indent_depth + str(part)) 278 | 279 | # Make sure the other related parts are squeezed on this page 280 | for other_part in parts_to_bring_on_page: 281 | if other_part not in self.schematic_page.parts_to_draw: 282 | # we already drew it earlier 283 | continue 284 | 285 | self.schematic_page.part_helpers[other_part].add_parts(indent_depth + " ") 286 | 287 | class SVGPage(object): 288 | """Represents single .svg page""" 289 | 290 | def __init__(self, net_regex=NET_REGEX_ALL, airwires=2, pins_to_skip=[], max_pin_count=None, context=global_context): 291 | self.net_regex = re.compile(net_regex) 292 | self.airwires = airwires 293 | self.context = context 294 | 295 | self.max_pin_count = max_pin_count 296 | self.pin_count = 0 297 | 298 | self.pins_to_skip = pins_to_skip 299 | self.pins_drawn = [] 300 | 301 | self.cells_dict = {} 302 | self.netnames_dict = collections.defaultdict(lambda: {"bits": [], "hide_name": 1}) 303 | self.ports_dict = {} 304 | 305 | # start helper classes 306 | self.net_helpers = {} 307 | for net in self.context.net_list: 308 | self.net_helpers[net] = SVGNet(net, self) 309 | 310 | self.part_helpers = {} 311 | for part in self.context.parts_list: 312 | self.part_helpers[part] = SVGPart(part, self) 313 | 314 | class PageEmpty(Exception): 315 | pass 316 | 317 | def write_json(self, fp): 318 | """Generate the json input required for netlistsvg and dumps it to a file.""" 319 | self.parts_to_draw = collections.deque(self.context.parts_list) 320 | while self.parts_to_draw: 321 | 322 | if self.max_pin_count and self.pin_count > self.max_pin_count: 323 | # stop drawing, this page is too cluttered 324 | break 325 | 326 | part = self.parts_to_draw[0] 327 | self.part_helpers[part].add_parts() 328 | 329 | if not self.pins_drawn: 330 | raise self.PageEmpty 331 | 332 | big_dict = {"modules": {"SVG Output": { 333 | "cells": self.cells_dict, 334 | "netnames": self.netnames_dict, 335 | "ports": self.ports_dict, 336 | }}} 337 | 338 | json.dump(big_dict, fp, indent=4) 339 | fp.flush() 340 | 341 | def generate(self): 342 | """Calls netlistsvg to generate the page and returns the svg contents as a string.""" 343 | with tempfile.NamedTemporaryFile("w", prefix="netlistsvg_input_", suffix=".json", delete=False) as json_file, \ 344 | tempfile.NamedTemporaryFile("r", prefix="netlistsvg_output_", suffix=".svg", delete=False) as netlistsvg_output: 345 | self.write_json(json_file) 346 | netlistsvg_command = [ 347 | "/usr/bin/env", "node", 348 | os.path.join(NETLISTSVG_LOCATION, "bin", "netlistsvg.js"), 349 | 350 | "--skin", 351 | os.path.join(NETLISTSVG_LOCATION, "lib", "analog.svg"), 352 | 353 | json_file.name, 354 | 355 | "-o", 356 | netlistsvg_output.name 357 | ] 358 | print(netlistsvg_command) 359 | subprocess.call(netlistsvg_command) 360 | 361 | svg_contents = netlistsvg_output.read() 362 | 363 | # When a net appears in a few places (when we have airwires), we need to disambiguage the parts of the net 364 | # so netlistsvg doesn't think they're actually the same net and should connect them together. 365 | # Remove the extra decoration: 366 | svg_contents = re.sub("_node\d+", "", svg_contents) 367 | 368 | return svg_contents 369 | 370 | 371 | def generate_svg(*args, **kwargs): 372 | pins_to_skip = [] 373 | while True: 374 | n = SVGPage(*args, **kwargs, pins_to_skip=pins_to_skip) 375 | try: 376 | svg_contents = n.generate() 377 | except SVGPage.PageEmpty: 378 | break 379 | pins_to_skip += n.pins_drawn 380 | 381 | yield svg_contents 382 | -------------------------------------------------------------------------------- /pcbdl/base.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import collections 16 | import copy 17 | import enum 18 | import itertools 19 | __all__ = [ 20 | "PinType", "ConnectDirection", 21 | "Net", "Part", "Pin" 22 | ] 23 | 24 | class Plugin(object): 25 | def __new__(cls, instance): 26 | self = super(Plugin,cls).__new__(cls) 27 | self.instance = instance 28 | return self 29 | 30 | @staticmethod 31 | def register(plugin_targets): 32 | if not isinstance(plugin_targets, collections.abc.Iterable): 33 | plugin_targets = (plugin_targets,) 34 | 35 | def wrapper(plugin): 36 | for target_cls in plugin_targets: 37 | try: 38 | target_cls.plugins 39 | except AttributeError: 40 | target_cls.plugins = set() 41 | target_cls.plugins.add(plugin) 42 | return plugin 43 | 44 | return wrapper 45 | 46 | @staticmethod 47 | def init(instance): 48 | """Init plugins associated with this instance""" 49 | try: 50 | factories = instance.plugins 51 | except AttributeError: 52 | return 53 | assert type(instance.plugins) is not dict 54 | instance.plugins = {plugin: plugin(instance) for plugin in factories} 55 | 56 | class ConnectDirection(enum.Enum): 57 | UNKNOWN = 0 58 | IN = 1 59 | OUT = 2 60 | 61 | class PinType(enum.Enum): 62 | UNKNOWN = 0 63 | PRIMARY = 1 64 | SECONDARY = 2 65 | POWER_INPUT = 3 66 | POWER_OUTPUT = 4 67 | GROUND = 5 68 | INPUT = 6 69 | OUTPUT = 7 70 | 71 | def _maybe_single(o): 72 | if isinstance(o, collections.abc.Iterable): 73 | yield from o 74 | else: 75 | yield o 76 | 77 | class _PinList(collections.OrderedDict): 78 | def __getitem__(self, pin_name): 79 | if isinstance(pin_name, int): 80 | return tuple(self.values())[pin_name] 81 | pin_name = pin_name.upper() 82 | try: 83 | return super().__getitem__(pin_name) 84 | except KeyError: 85 | # try looking slowly through the other names 86 | for pin in self.values(): 87 | if pin_name.upper() in pin.names: 88 | return pin 89 | else: 90 | raise 91 | 92 | def __iter__(self): 93 | yield from self.values() 94 | 95 | def __repr__(self): 96 | return repr(tuple(self.values())) 97 | 98 | class Net(object): 99 | _name = None 100 | has_name = False 101 | 102 | def __init__(self, name=None): 103 | if name is not None: 104 | self.name = name.upper() 105 | self._connections = [] 106 | 107 | Plugin.init(self) 108 | 109 | def connect(self, others, direction=ConnectDirection.UNKNOWN, pin_type=PinType.PRIMARY): 110 | try: 111 | connection_group = self.group 112 | except AttributeError: 113 | connection_group = collections.OrderedDict() 114 | self._connections.append(connection_group) 115 | 116 | for other in _maybe_single(others): 117 | pin = None 118 | 119 | if isinstance(other, Part): 120 | pin = other.get_pin_to_connect(pin_type, self) 121 | 122 | if isinstance(other, PartInstancePin): 123 | pin = other 124 | 125 | if isinstance(other, Net): 126 | raise NotImplementedError("Can't connect nets together yet.") 127 | 128 | if pin is None: 129 | raise TypeError("Don't know how to get %s pin from %r." % (pin_type.name, other)) 130 | 131 | connection_group[pin] = direction 132 | pin.net = self 133 | 134 | self._last_connection_group = connection_group 135 | 136 | def _shift(self, direction, others): 137 | self.connect(others, direction, PinType.PRIMARY) 138 | 139 | if hasattr(self, "group"): 140 | return self 141 | 142 | # Return a copy that acts just like us, but already knows the group 143 | grouped_net = copy.copy(self) 144 | grouped_net.parent = self 145 | grouped_net.group = self._last_connection_group 146 | return grouped_net 147 | 148 | def __lshift__(self, others): 149 | return self._shift(ConnectDirection.IN, others) 150 | 151 | def __rshift__(self, others): 152 | return self._shift(ConnectDirection.OUT, others) 153 | 154 | _MAX_REPR_CONNECTIONS = 10 155 | def __repr__(self): 156 | connected = self.connections 157 | if len(connected) >= self._MAX_REPR_CONNECTIONS: 158 | inside_str = "%d connections" % (len(connected)) 159 | elif len(connected) == 0: 160 | inside_str = "unconnected" 161 | elif len(connected) == 1: 162 | inside_str = "connected to " + repr(connected[0]) 163 | else: 164 | inside_str = "connected to " + repr(connected)[1:-1] 165 | return "%s(%s)" % (self, inside_str) 166 | 167 | def __str__(self): 168 | return self.name 169 | 170 | @property 171 | def name(self): 172 | if hasattr(self, "parent"): 173 | return self.parent.name 174 | 175 | if not self.has_name: 176 | # This path should be rare, only if the user really wants trouble 177 | return "ANON_NET?m%05x" % (id(self) // 32 & 0xfffff) 178 | 179 | return self._name 180 | 181 | @name.setter 182 | def name(self, new_name): 183 | self._name = new_name.upper() 184 | self.has_name = True 185 | 186 | @property 187 | def connections(self): 188 | """ 189 | A :class:`tuple` of pins connected to this net. 190 | 191 | Useful in the interpreter and/or when you want to inspect your schematic:: 192 | 193 | >>> gnd.connections 194 | (U1.GND, VREG1.GND, U2.GND, VREG2.GND) 195 | 196 | """ 197 | return sum(self.grouped_connections, ()) 198 | 199 | @property 200 | def grouped_connections(self): 201 | """ 202 | Similar to :attr:`connections`, but this time pins that were connected together stay in groups:: 203 | 204 | >>> pp1800.grouped_connections 205 | ((U1.GND, VREG1.GND), (U2.GND, VREG2.GND)) 206 | """ 207 | return tuple(tuple(group.keys()) for group in self._connections) 208 | 209 | def is_net_of_class(self, keywords): 210 | for keyword in keywords: 211 | if keyword in self.name: 212 | return True 213 | 214 | @property 215 | def is_power(self): 216 | return self.is_net_of_class(("VCC", "PP", "VBUS")) 217 | 218 | @property 219 | def is_gnd(self): 220 | return self.is_net_of_class(("GND",)) 221 | 222 | class PinFragment(object): 223 | """ 224 | This is the fully featured (as opposed to just a tuple of parameters) 225 | element of :attr:`PINS` at the time of writing 226 | a :class:`Part`. Saves all parameters it's given, 227 | merges later once the Part is fully defined. 228 | 229 | .. warning:: Just like the name implies this is just a fragment of the 230 | information we need for the pin. It's possible the Part needs to be 231 | overlayed on top of its parents before we can have a complete picture. 232 | Ex: this could be the pin labeled "PA2" of a microcontroller, but until 233 | the part is told what package it is, we don't really know the pin 234 | number. 235 | """ 236 | def __init__(self, names_or_numbers=(), names_if_numbers=None, *args, **kwargs): 237 | # Check if short form for the positional arguments 238 | if names_if_numbers is None: 239 | names, numbers = names_or_numbers, () 240 | else: 241 | names, numbers = names_if_numbers, names_or_numbers 242 | 243 | if isinstance(names, str): 244 | names = (names,) 245 | names += kwargs.pop("names", ()) 246 | try: 247 | names += (kwargs.pop("name"),) 248 | except KeyError: 249 | pass 250 | names = tuple(name.upper() for name in names) 251 | self.names = names 252 | 253 | if isinstance(numbers, (str, int)): 254 | numbers = (numbers,) 255 | numbers += kwargs.pop("numbers", ()) 256 | try: 257 | numbers += (kwargs.pop("number"),) 258 | except KeyError: 259 | pass 260 | numbers = tuple(str(maybe_int) for maybe_int in numbers) 261 | self.numbers = numbers 262 | 263 | self.args = args 264 | self.kwargs = kwargs 265 | 266 | Plugin.init(self) 267 | 268 | def __repr__(self): 269 | def arguments(): 270 | yield repr(self.names) 271 | if self.numbers: 272 | yield "numbers=" + repr(self.numbers) 273 | for arg in self.args: 274 | yield repr(arg) 275 | for name, value in self.kwargs.items(): 276 | yield "%s=%r" % (name, value) 277 | return "PinFragment(%s)" % (", ".join(arguments())) 278 | 279 | def __eq__(self, other): 280 | """If any names match between two fragments, we're talking about the same pin. This is associative, so it chains through other fragments.""" 281 | for my_name in self.names: 282 | if my_name in other.names: 283 | return True 284 | return False 285 | 286 | @staticmethod 287 | def part_superclasses(part): 288 | for cls in type(part).__mro__: 289 | if cls is Part: 290 | return 291 | yield cls 292 | 293 | @staticmethod 294 | def gather_fragments(cls_list): 295 | all_fragments = [pin for cls in cls_list for pin in cls.PINS] 296 | while len(all_fragments) > 0: 297 | same_pin_fragments = [] 298 | same_pin_fragments.append(all_fragments.pop(0)) 299 | pin_index = 0 300 | while True: 301 | try: 302 | i = all_fragments.index(same_pin_fragments[pin_index]) 303 | same_pin_fragments.append(all_fragments.pop(i)) 304 | except ValueError: 305 | pin_index += 1 # try following the chain of names, maybe there's another one we need to search by 306 | except IndexError: 307 | break # probably no more fragments for this pin 308 | yield same_pin_fragments 309 | 310 | @staticmethod 311 | def resolve(fragments): 312 | # union the names, keep order 313 | name_generator = (n for f in fragments for n in f.names) 314 | seen_names = set() 315 | deduplicated_names = [n for n in name_generator if not (n in seen_names or seen_names.add(n))] 316 | 317 | pin_numbers = [number for fragment in fragments for number in fragment.numbers] 318 | 319 | # union the args and kwargs, stuff near the front has priority to override 320 | args = [] 321 | kwargs = {} 322 | for fragment in reversed(fragments): 323 | args[:len(fragment.args)] = fragment.args 324 | kwargs.update(fragment.kwargs) 325 | 326 | return PartClassPin(deduplicated_names, pin_numbers, *args, **kwargs) 327 | 328 | @staticmethod 329 | def second_name_important(pin): 330 | """ 331 | Swap the order of the pin names so the functional (second) name is first. 332 | 333 | Used as a :func:`Part._postprocess_pin filter`. 334 | """ 335 | pin.names = pin.names[1:] + (pin.names[0],) 336 | Pin = PinFragment 337 | 338 | class PartClassPin(object): 339 | """ 340 | Pin of a Part, but no particular Part instance. 341 | Contains general information about the pin (but it could be for any 342 | part of that type), nothing related to a specific part instance. 343 | """ 344 | well_name = None 345 | 346 | def __init__(self, names, numbers, type=PinType.UNKNOWN, well=None): 347 | self.names = names 348 | self.numbers = numbers 349 | self.type = type 350 | self.well_name = well 351 | 352 | Plugin.init(self) 353 | 354 | @property 355 | def name(self): 356 | return self.names[0] 357 | 358 | @property 359 | def number(self): 360 | return self.numbers[0] 361 | 362 | def __str__(self): 363 | return "Pin %s" % (self.name) 364 | __repr__ = __str__ 365 | 366 | class PartInstancePin(PartClassPin): 367 | """Particular pin of a particular part instance. Can connect to nets. Knows the refdes of its part.""" 368 | _net = None 369 | 370 | def __init__(self, part_instance, part_class_pin, inject_number=None): 371 | # copy state of the Pin to be inherited, then continue as if the parent class always existed that way 372 | self.__dict__.update(part_class_pin.__dict__.copy()) 373 | # no need to call PartClassPin.__init__ 374 | 375 | self._part_class_pin = part_class_pin 376 | 377 | # save arguments 378 | self.part = part_instance 379 | 380 | if inject_number is not None: 381 | self.numbers = (inject_number,) 382 | assert self.numbers is not None, "this Pin really should have had real pin numbers assigned by now" 383 | 384 | well_name = self.well_name 385 | if well_name is not None: 386 | try: 387 | self.well = self.part.pins[well_name] 388 | except KeyError: 389 | raise KeyError("Couldn't find voltage well pin %s on part %r" % (well_name, part_instance)) 390 | if self.well.type not in (PinType.POWER_INPUT, PinType.POWER_OUTPUT): 391 | raise ValueError("The chosen well pin %s is not a power pin (but is %s)" % (self.well, self.well.type)) 392 | 393 | Plugin.init(self) 394 | 395 | @property 396 | def net(self): 397 | """ 398 | The :class:`Net` that this pin is connected to. 399 | 400 | If it's not connected to anything yet, we'll get a fresh net. 401 | """ 402 | if self._net is None: 403 | fresh_net = Net() #defined_at: not here 404 | return fresh_net << self 405 | #fresh_net.connect(self, direction=ConnectDirection.UNKNOWN) # This indirectly sets self.netf 406 | return self._net 407 | @net.setter 408 | def net(self, new_net): 409 | if self._net is not None: 410 | # TODO: Maybe just unify the existing net and the new 411 | # net and allow this. 412 | raise ValueError("%s pin is already connected to a net (%s). Can't connect to %s too." % 413 | (self, self._net, new_net)) 414 | 415 | self._net = new_net 416 | 417 | def connect(self, *args, **kwargs): 418 | self.net.connect(*args, **kwargs) 419 | 420 | def __lshift__(self, others): 421 | net = self._net 422 | if net is None: 423 | # don't let the net property create a new one, 424 | # we want to dictate the direction to that Net 425 | net = Net() #defined_at: not here 426 | net >>= self 427 | return net << others 428 | 429 | def __rshift__(self, others): 430 | net = self._net 431 | if net is None: 432 | # don't let the net property create a new one, 433 | # we want to dictate the direction to that Net 434 | net = Net() #defined_at: not here 435 | net <<= self 436 | return net >> others 437 | 438 | def __str__(self): 439 | return "%r.%s" % (self.part, self.name) 440 | __repr__ = __str__ 441 | 442 | class PinFragmentList(list): 443 | """Used as a marker that we have visited Part.PINS and converted all the elements to PinFragment.""" 444 | def __init__(self, part_cls): 445 | self.part_cls = part_cls 446 | list.__init__(self, part_cls.PINS) 447 | for i, maybenames in enumerate(self): 448 | # syntactic sugar, .PIN list might have only names instead of the long form Pin instances 449 | if not isinstance(maybenames, Pin): 450 | self[i] = PinFragment(maybenames) 451 | 452 | if part_cls._postprocess_pin.__code__ == Part._postprocess_pin.__code__: 453 | # Let's not waste our time with a noop 454 | return 455 | for i, _ in enumerate(self): 456 | # do user's postprocessing 457 | part_cls._postprocess_pin(self[i]) 458 | 459 | class Part(object): 460 | """ 461 | This is the :ref:`base class` for any new Part the writer of a schematic or a part librarian has to make. :: 462 | 463 | class Transistor(Part): 464 | REFDES_PREFIX = "Q" 465 | PINS = ["B", "C", "E"] 466 | """ 467 | 468 | PINS = [] 469 | """ 470 | This is how the pins of a part are defined, as a :class:`list` of pins. 471 | 472 | Each pin entry can be one of: 473 | 474 | * :class:`Pin` 475 | * :class:`tuple` of names which will automatically be turned into a :class:`Pin` 476 | * just one :class:`string`, representing a pin name, if one cares about nothing else. 477 | 478 | So these are all valid ways to define a pin (in decreasing order of detail), and mean about the same thing:: 479 | 480 | PINS = [ 481 | Pin("1", ("GND", "GROUND"), type=PinType.POWER_INPUT), 482 | ("GND", "GROUND"), 483 | "GND", 484 | ] 485 | 486 | See the :class:`Pins Section` for the types of properties that can be 487 | defined on each Pin entry. 488 | """ 489 | 490 | pins = _PinList() 491 | """ 492 | Once the Part is instanced (aka populated on the schematic), our pins become real too (they turn into :class:`PartInstancePins`). 493 | This is a :class:`dict` like object where the pins are stored. One can look up pins by any of its names:: 494 | 495 | somechip.pins["VCC"] 496 | 497 | Though most pins are also directly populated as a attributes to the part, so this is equivalent:: 498 | 499 | somechip.VCC 500 | 501 | The pins list can still be used to view all of the pins at once, like on the console: 502 | 503 | >>> diode.pins 504 | (D1.VCC, D1.NC, D1.P1, D1.GND, D1.P2) 505 | """ 506 | 507 | REFDES_PREFIX = "UNK" 508 | """ 509 | The prefix that every reference designator of this part will have. 510 | 511 | Example: :attr:`"R"` for resistors, 512 | :attr:`"C"` for capacitors. 513 | 514 | The auto namer system will eventually put numbers after the prefix to get the complete :attr:`refdes`. 515 | """ 516 | 517 | pin_names_match_nets = False 518 | """ 519 | Sometimes when connecting nets to a part, the pin names become very redundant:: 520 | 521 | Net("GND") >> somepart.GND 522 | Net("VCC") >> somepart.VCC 523 | Net("RESET") >> somepart.RESET 524 | 525 | We can use this variable tells the part to pick the right pin depending on 526 | the variable name, at that point the part itself can be used in lieu of 527 | the pin:: 528 | 529 | Net("GND") >> somepart 530 | Net("VCC") >> somepart 531 | Net("RESET") >> somepart 532 | """ 533 | 534 | pin_names_match_nets_prefix = "" 535 | """ 536 | When :attr:`pin_names_match_nets` is active, it strips a 537 | little bit of the net name in case it's part of a bigger net group:: 538 | 539 | class SPIFlash(Part): 540 | pin_names_match_nets = True 541 | pin_names_match_nets_prefix = "SPI1" 542 | PINS = ["MOSI", "MISO", "SCK", "CS", ...] 543 | ... 544 | Net("SPI1_MOSI") >> spi_flash # autoconnects to the pin called only "MOSI" 545 | Net("SPI1_MISO") << spi_flash # "MISO" 546 | Net("SPI1_SCK") >> spi_flash # "SCK" 547 | Net("SPI1_CS") >> spi_flash # "CS" 548 | """ 549 | 550 | def __init__(self, value=None, refdes=None, package=None, part_number=None, populated=True): 551 | if part_number is not None: 552 | self.part_number = part_number 553 | if value is not None: 554 | self.value = value 555 | 556 | # if we don't have a value xor a package, use one of them for both 557 | if not hasattr(self, "value") and hasattr(self, "part_number"): 558 | self.value = self.part_number 559 | if not hasattr(self, "part_number") and hasattr(self, "value"): 560 | self.part_number = self.value 561 | # if we don't have either, then there's not much we can do 562 | if not hasattr(self, "value") and not hasattr(self, "part_number"): 563 | self.value = "" 564 | self.part_number = "" 565 | 566 | self._refdes = refdes 567 | if package is not None: 568 | self.package = package 569 | self.populated = populated 570 | 571 | self._generate_pin_instances() 572 | 573 | Plugin.init(self) 574 | 575 | def _generate_pin_instances(self): 576 | cls_list = list(PinFragment.part_superclasses(self)) 577 | 578 | # process the pin lists a little bit 579 | for cls in cls_list: 580 | # but only if we didn't already do it 581 | if isinstance(cls.PINS, PinFragmentList): 582 | continue 583 | cls.PINS = PinFragmentList(cls) 584 | 585 | self.__class__.pins = [PinFragment.resolve(f) for f in PinFragment.gather_fragments(cls_list)] 586 | 587 | self.pins = _PinList() 588 | for i, part_class_pin in enumerate(self.__class__.pins): 589 | # if we don't have an assigned pin number, generate one 590 | inject_pin_number = str(i + 1) if not part_class_pin.numbers else None 591 | 592 | pin = PartInstancePin(self, part_class_pin, inject_pin_number) 593 | self.pins[pin.name] = pin 594 | 595 | # save the pin as an attr for this part too 596 | for name in pin.names: 597 | self.__dict__[name] = pin 598 | 599 | @property 600 | def _refdes_from_memory_address(self): 601 | return "%s?m%05x" % (self.REFDES_PREFIX, id(self) // 32 & 0xfffff) 602 | 603 | @property 604 | def refdes(self): 605 | """ 606 | Reference designator of the part. Example: R1, R2. 607 | 608 | It's essentially the unique id for the part that will be used to 609 | refer to it in most output methods. 610 | """ 611 | if self._refdes is not None: 612 | return self._refdes 613 | 614 | # make up a refdes based on memory address 615 | return self._refdes_from_memory_address 616 | 617 | @refdes.setter 618 | def refdes(self, new_value): 619 | self._refdes = new_value.upper() 620 | 621 | def __repr__(self): 622 | return self.refdes 623 | 624 | def __str__(self): 625 | return "%s - %s%s" % (self.refdes, self.value, " DNS" if not self.populated else "") 626 | 627 | def get_pin_to_connect(self, pin_type, net=None): # pragma: no cover 628 | assert isinstance(pin_type, PinType) 629 | 630 | if self.pin_names_match_nets and net is not None: 631 | prefix = self.pin_names_match_nets_prefix 632 | net_name = net.name 633 | for pin in self.pins: 634 | for pin_name in pin.names: 635 | if pin_name == net_name: 636 | return pin 637 | if prefix + pin_name == net_name: 638 | return pin 639 | raise ValueError("Couldn't find a matching named pin on %r to connect the net %s" % (self, net_name)) 640 | 641 | raise NotImplementedError("Don't know how to get %s pin from %r" % (pin_type.name, self)) 642 | 643 | @classmethod 644 | def _postprocess_pin(cls, pin): 645 | """ 646 | It's sometimes useful to process the pins from the source code before the part gets placed down. 647 | This method will be called for each pin by each subclass of a Part. 648 | 649 | Good uses for this: 650 | 651 | * :func:`Raise the importance of the second name` in a connector, so the more semantic name is the primary name, not the pin number:: 652 | 653 | PINS = [ 654 | ("P1", "Nicer name"), 655 | ] 656 | _postprocess_pin = Pin.second_name_important 657 | 658 | * Populate alternate functions of a pin if they follow an easy pattern. 659 | * A simple programmatic alias on pin names without subclassing the part itself. 660 | """ 661 | raise TypeError("This particular implementation of _postprocess_pin should be skipped by PinFragmentList()") 662 | -------------------------------------------------------------------------------- /examples/servo_micro.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2019 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """ 18 | Full reimplementation of servo micro in pcbdl. 19 | 20 | Servo Micro's information page (including pdf schematics made in orthodox tools) can be found at: 21 | https://chromium.googlesource.com/chromiumos/third_party/hdctools/+/refs/heads/master/docs/servo_micro.md 22 | """ 23 | 24 | from pcbdl import * 25 | 26 | # Start of things that should really be in a generic library 27 | # It's a TODO to make a library. Until then, 300 lines to start a new schematic from scratch with no library is probably not bad. 28 | def make_connector(pin_count): 29 | class Connector(Part): 30 | REFDES_PREFIX = "CN" 31 | 32 | PINS = [] 33 | 34 | for i in range(pin_count): 35 | i += 1 # 1 indexed 36 | pin = Pin(i, "P%d" % i) 37 | Connector.PINS.append(pin) 38 | 39 | return Connector 40 | 41 | class UsbConnector(Part): 42 | REFDES_PREFIX = "CN" 43 | part_number = "1981568-1" 44 | package = "TE_1981568-1" 45 | PINS = [ 46 | "VBUS", 47 | ("DM", "D-"), 48 | ("DP", "D+"), 49 | "ID", 50 | "GND", 51 | Pin("G", numbers=("G1", "G2", "G3", "G4")), 52 | ] 53 | 54 | class FET(Part): 55 | """FET Transistor""" 56 | REFDES_PREFIX = "Q" 57 | 58 | PINS = [ 59 | Pin("D", "D"), 60 | Pin("G", "G"), 61 | Pin("S", "S"), 62 | ] 63 | 64 | class Regulator(Part): 65 | REFDES_PREFIX = "U" 66 | 67 | PINS = [ 68 | Pin("IN", type=PinType.POWER_INPUT), 69 | Pin("OUT", type=PinType.POWER_OUTPUT), 70 | Pin("GND", type=PinType.POWER_INPUT), 71 | ] 72 | 73 | class MIC5504(Regulator): 74 | part_number = "MIC5504-3.3YMT" 75 | package = "SON65P100X100X40-5T48X48N" 76 | 77 | PINS = [ 78 | Pin("4", "IN"), 79 | Pin("1", "OUT"), 80 | Pin("3", "EN"), 81 | Pin(("2", "G1"), ("GND", "PAD")), 82 | ] 83 | 84 | class TLV70018DSER(Regulator): 85 | part_number = "TLV70018DSER" 86 | package = "SON50P150X150X80-6L" 87 | 88 | PINS = [ 89 | Pin("1", "IN"), 90 | Pin("3", "OUT"), 91 | Pin("6", "EN"), 92 | Pin("4", "NC1"), 93 | Pin("5", "NC2"), 94 | Pin("2", "GND"), 95 | ] 96 | 97 | class UsbEsdDiode(Part): 98 | REFDES_PREFIX = "D" 99 | part_number = "TPD2E001DRLR" 100 | package = "SOP50P170X60-5N" 101 | PINS = [ 102 | Pin("1", "VCC", type=PinType.POWER_INPUT), 103 | Pin("4", "GND", type=PinType.POWER_INPUT), 104 | Pin("3", "P1"), 105 | Pin("5", "P2"), 106 | Pin("2", "NC"), 107 | ] 108 | 109 | class DoubleDiode(Part): 110 | REFDES_PREFIX = "D" 111 | part_number = "240-800MV" 112 | package = "SOT95P247X115-3L" 113 | PINS = ["A1", "A2", "K"] 114 | 115 | class STM32F072(Part): 116 | REFDES_PREFIX = "U" 117 | 118 | part_number = "STM32F072CBU6TR" 119 | package = "QFN05P_7-1X7-1_0-6_49N" 120 | 121 | PINS = [ 122 | Pin(("24", "48"), "VDD", type=PinType.POWER_INPUT), 123 | Pin("1", "VBAT", type=PinType.POWER_INPUT), 124 | Pin("9", "VDDA", type=PinType.POWER_INPUT), 125 | Pin("36", "VDDIO2", type=PinType.POWER_INPUT), 126 | 127 | Pin(("23", "35", "47"), "VSS"), 128 | Pin("8", "VSSA"), 129 | Pin("49", "PAD"), 130 | 131 | Pin("44", "BOOT0"), 132 | Pin("7", "NRST"), 133 | ] 134 | 135 | for i in range(8): 136 | PINS.append(Pin(i + 10, "PA%d" % i)) 137 | 138 | PINS += [ 139 | Pin("29", "PA8"), 140 | Pin("30", "PA9"), 141 | Pin("31", "PA10"), 142 | Pin("32", "PA11"), 143 | Pin("33", "PA12"), 144 | Pin("34", "PA13"), 145 | Pin("37", "PA14"), 146 | Pin("38", "PA15"), 147 | 148 | Pin("18", "PB0"), 149 | Pin("19", "PB1"), 150 | Pin("20", "PB2"), 151 | Pin("39", "PB3"), 152 | Pin("40", "PB4"), 153 | Pin("41", "PB5"), 154 | Pin("42", "PB6"), 155 | Pin("43", "PB7"), 156 | 157 | Pin("45", "PB8"), 158 | Pin("46", "PB9"), 159 | Pin("21", "PB10"), 160 | Pin("22", "PB11"), 161 | Pin("25", "PB12"), 162 | Pin("26", "PB13"), 163 | Pin("27", "PB14"), 164 | Pin("28", "PB15"), 165 | 166 | Pin("2", "PC13"), 167 | Pin("3", ("PC14", "OSC32_IN")), 168 | Pin("4", ("PC15", "OSC32_OUT")), 169 | 170 | Pin("5", ("PF0", "OSC_IN")), 171 | Pin("6", ("PF1", "OSC_OUT")), 172 | ] 173 | 174 | for pin in PINS: 175 | if pin.names[0].startswith("PA"): 176 | pin.well_name = "VDD" 177 | 178 | if pin.names[0].startswith("PB"): 179 | pin.well_name = "VDDA" 180 | 181 | if pin.names[0].startswith("PC"): 182 | pin.well_name = "VDDA" 183 | 184 | if pin.names[0].startswith("PF"): 185 | pin.well_name = "VDDA" 186 | 187 | class I2cIoExpander(Part): 188 | REFDES_PREFIX = "U" 189 | 190 | part_number = "TCA6416ARTWR" 191 | package = "QFN50P400X400X080-25N" 192 | 193 | PINS = [ 194 | Pin("23", "VCCI"), 195 | Pin("21", "VCCP"), 196 | Pin("9", "GND"), 197 | Pin("25", "PAD"), 198 | 199 | Pin("19", "SCL"), 200 | Pin("20", "SDA"), 201 | Pin("22", "INT_L"), 202 | Pin("24", "RESET_L"), 203 | 204 | Pin("18", "A0"), 205 | ] 206 | 207 | for i in range(8): 208 | PINS.append(Pin(i + 1, "P0%d" % i)) 209 | 210 | for i in range(8): 211 | PINS.append(Pin(i + 10, "P1%d" % i)) 212 | 213 | class Mux(Part): 214 | REFDES_PREFIX = "U" 215 | 216 | part_number = "313-00929-00" 217 | package = "SOT65P210X110-6L" 218 | 219 | PINS = [ 220 | Pin("5", "VCC"), 221 | Pin("2", "GND"), 222 | 223 | Pin("3", ("0", "IN0")), 224 | Pin("1", ("1", "IN1")), 225 | 226 | Pin("6", ("S0", "SEL")), 227 | Pin("4", ("Y", "OUT")), 228 | ] 229 | 230 | class OutputBuffer(Part): 231 | REFDES_PREFIX = "U" 232 | 233 | part_number = "SN74LVC1G126YZPR" 234 | package = "BGA5C50P3X2_141X91X50L" 235 | 236 | PINS = [ 237 | Pin("A2", "VCC"), 238 | Pin("C1", "GND"), 239 | 240 | Pin("B1", ("A", "IN")), 241 | Pin("A1", ("OE", "SEL")), 242 | Pin("C2", ("Y", "OUT")), 243 | ] 244 | 245 | class LevelShifter(Part): 246 | """ 247 | Bidirectional Level Shifter 248 | 249 | DIR=0 : B->A 250 | DIR=1 : A->B 251 | """ 252 | REFDES_PREFIX = "U" 253 | 254 | PINS = [ 255 | "VCCA", 256 | "VCCB", 257 | "GND", 258 | ] 259 | 260 | @property 261 | def direction_AB(self): 262 | return self.VCCA.net 263 | 264 | @property 265 | def direction_BA(self): 266 | return self.GND.net 267 | 268 | class LevelShifter1(LevelShifter): 269 | __doc__ = LevelShifter.__doc__ 270 | part_number = "SN74AVC1T45DRLR" 271 | package = "SOP50P170X60-6N" 272 | 273 | PINS = [ 274 | Pin("1", "VCCA", type=PinType.POWER_INPUT), 275 | Pin("6", "VCCB", type=PinType.POWER_INPUT), 276 | Pin("3", "A"), 277 | Pin("4", "B"), 278 | Pin("5", "DIR"), 279 | Pin("2", "GND", type=PinType.POWER_INPUT), 280 | ] 281 | 282 | class LevelShifter2(LevelShifter): 283 | __doc__ = LevelShifter.__doc__ 284 | part_number = "SN74AVC2T245RSWR" 285 | package = "QFN40P145X185X55-10N" 286 | 287 | PINS = [ 288 | Pin("7", "VCCA", type=PinType.POWER_INPUT), 289 | Pin("6", "VCCB", type=PinType.POWER_INPUT), 290 | Pin("2", "OE_L"), 291 | Pin("8", "A1"), 292 | Pin("9", "A2"), 293 | Pin("5", "B1"), 294 | Pin("4", "B2"), 295 | Pin("10", "DIR1"), 296 | Pin("1", "DIR2"), 297 | Pin("3", "GND", type=PinType.POWER_INPUT), 298 | ] 299 | 300 | class LevelShifter4(LevelShifter): 301 | __doc__ = LevelShifter.__doc__ 302 | part_number = "SN74AVC4T774RSVR" 303 | package = "QFN40P265X185X55-16N" 304 | 305 | PINS = [ 306 | Pin("14", "VCCA", type=PinType.POWER_INPUT), 307 | Pin("13", "VCCB", type=PinType.POWER_INPUT), 308 | Pin("7", "OE_L"), 309 | Pin("1", "A1"), 310 | Pin("2", "A2"), 311 | Pin("3", "A3"), 312 | Pin("4", "A4"), 313 | Pin("12", "B1"), 314 | Pin("11", "B2"), 315 | Pin("10", "B3"), 316 | Pin("9", "B4"), 317 | Pin("15", "DIR1"), 318 | Pin("16", "DIR2"), 319 | Pin("5", "DIR3"), 320 | Pin("6", "DIR4"), 321 | Pin("8", "GND", type=PinType.POWER_INPUT), 322 | ] 323 | 324 | class AnalogSwitch(Part): 325 | """ 326 | Dual Analog Switch 327 | 328 | IN DIRECTION 329 | L NC -> COM 330 | H NO -> COM 331 | """ 332 | REFDES_PREFIX = "U" 333 | 334 | part_number = "TS3A24159" 335 | package = "BGA10C50P4X3_186X136X50L" 336 | 337 | PINS = [ 338 | Pin("D2", ("V+", "VCC")), 339 | Pin("A2", "GND"), 340 | 341 | Pin("B1", ("IN1", "SEL1")), 342 | Pin("C1", "COM1"), 343 | Pin("A1", "NC1"), 344 | Pin("D1", "NO1"), 345 | 346 | Pin("B3", ("IN2", "SEL2")), 347 | Pin("C3", "COM2"), 348 | Pin("A3", "NC2"), 349 | Pin("D3", "NO2"), 350 | ] 351 | 352 | class PowerSwitch(Part): 353 | REFDES_PREFIX = "U" 354 | 355 | part_number = "ADP194ACBZ-R7" 356 | package = "BGA4C40P2X2_80X80X56" 357 | 358 | PINS = [ 359 | Pin("A1", ("IN", "IN1")), 360 | Pin("A2", ("OUT", "OUT1")), 361 | Pin("B1", "EN"), 362 | Pin("B2", "GND"), 363 | ] 364 | # End of things that should be in a generic library 365 | 366 | # Maybe this connector could be in a library too, since it's not too specific to this servo schematic 367 | class ServoConnector(make_connector(pin_count=50)): 368 | part_number = "AXK850145WG" 369 | package = "AXK850145WG" 370 | 371 | pin_names_match_nets = True 372 | pin_names_match_nets_prefix = "DUT_" 373 | PINS = [ 374 | ("P1", "GND"), 375 | ("P2", "SPI2_CLK", "SPI2_SK"), 376 | ("P3", "SPI2_CS"), 377 | ("P4", "SPI2_MOSI", "SPI2_DI"), 378 | ("P5", "SPI2_MISO", "SPI2_DO"), 379 | ("P6", "SPI2_VREF"), 380 | ("P7", "SPI2_HOLD_L"), 381 | ("P8", "GND"), 382 | ("P9", "SPI1_CLK", "SPI1_SK"), 383 | ("P10", "SPI1_CS"), 384 | ("P11", "SPI1_MOSI", "SPI1_DI"), 385 | ("P12", "SPI1_MISO", "SPI1_DO"), 386 | ("P13", "SPI1_VREF"), 387 | ("P14", "EC_RESET_L", "COLD_RESET_L"), 388 | ("P15", "GND"), 389 | ("P16", "UART2_SERVO_DUT_TX", "UART2_RXD"), 390 | ("P17", "UART2_DUT_SERVO_TX", "UART2_TXD"), 391 | ("P18", "UART2_VREF"), 392 | ("P19", "SD_DETECT_L"), 393 | ("P20", "GND"), 394 | ("P21", "JTAG_TCK"), 395 | ("P22", "PWR_BUTTON"), 396 | ("P23", "JTAG_TMS"), 397 | ("P24", "JTAG_TDI"), 398 | ("P25", "JTAG_TDO"), 399 | ("P26", "JTAG_RTCK"), 400 | ("P27", "JTAG_TRST_L"), 401 | ("P28", "JTAG_SRST_L", "WARM_RESET_L"), 402 | ("P29", "JTAG_VREF"), 403 | ("P30", "REC_MODE_L", "GOOG_REC_MODE_L"), 404 | ("P31", "GND"), 405 | ("P32", "UART1_SERVO_DUT_TX", "UART1_RXD"), 406 | ("P33", "UART1_DUT_SERVO_TX", "UART1_TXD"), 407 | ("P34", "UART1_VREF"), 408 | ("P35", "I2C_3.3V"), 409 | ("P36", "GND"), 410 | ("P37", "I2C_SDA"), 411 | ("P38", "I2C_SCL"), 412 | ("P39", "HPD"), 413 | ("P40", "FW_WP", "MFG_MODE"), 414 | ("P41", "PROC_HOT_L", "FW_UPDATE_L", "FW_UP_L"), 415 | ("P42", "GND"), 416 | ("P43", "DEV_MODE"), 417 | ("P44", "LID_OPEN"), 418 | ("P45", "PCH_DISABLE_L", "CPU_NMI"), 419 | ("P46", "KBD_COL1"), 420 | ("P47", "KBD_COL2"), 421 | ("P48", "KBD_ROW1"), 422 | ("P49", "KBD_ROW2"), 423 | ("P50", "KBD_ROW3"), 424 | ] 425 | _postprocess_pin = Pin.second_name_important 426 | 427 | # The following part definitions are only related to this circuit 428 | class ProgrammingConnector(make_connector(8)): 429 | part_number = "FH34SRJ-8S-0.5SH(50)" 430 | package = "HRS_FH34SRJ-8S-0-5SH" 431 | 432 | PINS = [ 433 | ("P1", "GND"), 434 | ("P2", "UART_TX"), 435 | ("P3", "UART_RX"), 436 | ("P6", "NRST"), 437 | ("P8", "BOOT0"), 438 | Pin("G", numbers=("G1", "G2")), 439 | ] 440 | _postprocess_pin = Pin.second_name_important 441 | 442 | class JtagConnector(make_connector(10)): 443 | part_number = "HDR_2X5_50MIL-210-00939-00-SAMTEC_FTSH-105-01" 444 | package = "SAMTEC_FTSH-105-01-L-DV-K" 445 | 446 | 447 | pin_names_match_nets = True 448 | pin_names_match_nets_prefix = "DUT_JTAG_" 449 | PINS = [ 450 | ("P1", "VCC"), 451 | ("P2", "TMS", "SWDIO"), 452 | ("P3", "GND"), 453 | ("P4", "TCK", "SWDCLK"), 454 | ("P5", "GND"), 455 | ("P6", "TDO", "SWO"), 456 | ("P7", "KEY"), 457 | ("P8", "TDI"), 458 | ("P9", "GNDDetect"), 459 | ("P10", "RESET"), 460 | ] 461 | _postprocess_pin = Pin.second_name_important 462 | 463 | class ServoEC(STM32F072): 464 | pin_names_match_nets = True 465 | PINS = [ 466 | Pin(("PA0", "UART3_TX")), 467 | Pin(("PA1", "UART3_RX")), 468 | Pin(("PA2", "UART1_TX")), 469 | Pin(("PA3", "UART1_RX")), 470 | Pin(("PA4", "SERVO_JTAG_TMS")), 471 | Pin(("PA5", "SPI1_MUX_SEL")), 472 | Pin(("PA6", "SERVO_JTAG_TDO_BUFFER_EN")), 473 | Pin(("PA7", "SERVO_JTAG_TDI")), 474 | 475 | Pin(("PA8", "UART1_EN_L")), 476 | Pin(("PA9", "EC_UART_TX")), 477 | Pin(("PA10", "EC_UART_RX")), 478 | Pin(("PA11", "USB_DM")), 479 | Pin(("PA12", "USB_DP")), 480 | Pin(("PA13", "SERVO_JTAG_TRST_L")), 481 | Pin(("PA14", "SPI1_BUF_EN_L")), 482 | Pin(("PA15", "SPI2_BUF_EN_L")), 483 | 484 | Pin(("PB0", "UART2_EN_L")), 485 | Pin(("PB1", "SERVO_JTAG_RTCK")), 486 | Pin(("PB2", "SPI1_VREF_33")), 487 | Pin(("PB3", "SPI1_VREF_18")), 488 | Pin(("PB4", "SPI2_VREF_33")), 489 | Pin(("PB5", "SPI2_VREF_18")), 490 | Pin(("PB6", "SERVO_JTAG_TRST_DIR")), 491 | Pin(("PB7", "SERVO_JTAG_TDI_DIR")), 492 | 493 | Pin(("PB8", "MASTER_I2C_SCL")), 494 | Pin(("PB9", "MASTER_I2C_SDA")), 495 | Pin(("PB10", "UART2_TX")), 496 | Pin(("PB11", "UART2_RX")), 497 | Pin(("PB12", "SERVO_SPI_CS")), 498 | Pin(("PB13", "SERVO_TO_SPI1_MUX_CLK")), 499 | Pin(("PB14", "SERVO_TO_SPI1_MUX_MISO")), 500 | Pin(("PB15", "SERVO_SPI_MOSI")), 501 | 502 | Pin(("PC13", "RESET_L")), 503 | Pin(("PC14", "SERVO_JTAG_TMS_DIR")), 504 | Pin(("PC15", "SERVO_JTAG_TDO_SEL")), 505 | 506 | Pin(("PF0", "JTAG_BUFOUT_EN_L")), 507 | Pin(("PF1", "JTAG_BUFIN_EN_L")), 508 | ] 509 | _postprocess_pin = Pin.second_name_important 510 | 511 | # Start of actual schematic 512 | vbus_in = Net("VBUS_IN") 513 | gnd = Net("GND") 514 | def decoupling(value="100n", package=None): 515 | if package is None: 516 | package = "CAPC0603X33L" 517 | 518 | if "u" in value: 519 | package = "CAPC1005X71L" 520 | 521 | if "0u" in value: 522 | package = "CAPC1608X80L" 523 | 524 | return C(value, to=gnd, package=package, part_number="CY" + value) #defined_at: not here 525 | old_R = R 526 | def R(value, to): 527 | return old_R(value, package="RESC0603X23L", part_number="R" + value, to=to) #defined_at: not here 528 | 529 | # usb stuff 530 | usb = UsbConnector() 531 | usb_esd = UsbEsdDiode() 532 | Net("USB_DP") << usb.DP << usb_esd.P1 533 | Net("USB_DM") << usb.DM << usb_esd.P2 >> usb_esd.NC 534 | vbus_in << usb.VBUS << usb_esd.VCC 535 | gnd << usb.GND << usb.G << usb_esd.GND 536 | # We could make this type-c instead! 537 | 538 | # 3300 regulator 539 | pp3300 = Net("PP3300") 540 | reg3300 = MIC5504() 541 | vbus_in << ( 542 | reg3300.IN, decoupling("2.2u"), 543 | reg3300.EN, 544 | ) 545 | gnd << reg3300.GND 546 | pp3300 << ( 547 | reg3300.OUT, 548 | decoupling("10u"), 549 | decoupling(), 550 | decoupling("1000p"), 551 | ) 552 | 553 | # 1800 regulator 554 | pp1800 = Net("PP1800") 555 | reg1800 = TLV70018DSER() 556 | drop_diode = DoubleDiode() 557 | pp3300 << drop_diode.A1 << drop_diode.A2 558 | Net("PP1800_VIN") << ( 559 | drop_diode.K, 560 | reg1800.IN, decoupling(), 561 | reg1800.EN 562 | ) 563 | gnd << reg1800.GND 564 | pp1800 << reg1800.OUT << decoupling("1u", "CAPC0603X33L") 565 | 566 | ec = ServoEC() 567 | usb.DP << ec 568 | usb.DM << ec 569 | 570 | # ec power 571 | pp3300 << ( 572 | ec.VBAT, decoupling(), 573 | ec.VDD, decoupling(), 574 | decoupling("4.7u"), 575 | ) 576 | Net("PP3300_PD_VDDA") << ( 577 | ec.VDDA, 578 | L("600@100MHz", to=pp3300, package="INDC1005L", part_number="FERRITE_BEAD-185-00019-00"), 579 | decoupling("1u"), 580 | decoupling("100p"), 581 | ) 582 | pp3300 << ( 583 | ec.VDDIO2, decoupling(), 584 | decoupling("4.7u"), 585 | ) 586 | gnd << ec.VSS << ec.VSSA << ec.PAD 587 | 588 | # ec programming/debug 589 | prog = ProgrammingConnector() 590 | gnd << prog.GND << prog.G 591 | Net("PD_NRST_L") << ( 592 | ec.NRST, 593 | prog.NRST, 594 | decoupling(), 595 | ) 596 | boot0 = Net("PD_BOOT0") 597 | boot0_q = FET("CSD13381F4", package="DFN100X60X35-3L") 598 | # Use OTG + A-TO-A cable to go to bootloader mode 599 | Net("USB_ID") >> boot0_q.G >> R("51.1k", to=vbus_in) << usb.ID 600 | boot0 << boot0_q.D << R("51.1k", to=vbus_in) << ec.BOOT0 << prog.BOOT0 601 | gnd << boot0_q.S 602 | Net("EC_UART_TX") << ec << prog.UART_TX 603 | Net("EC_UART_RX") << ec << prog.UART_RX 604 | 605 | ppdut_spi_vrefs = { 606 | 1: Net("PPDUT_SPI1_VREF"), 607 | 2: Net("PPDUT_SPI2_VREF"), 608 | } 609 | 610 | jtag_buffer_to_servo_tdo = Net("JTAG_BUFFER_TO_SERVO_TDO") >> ec.UART3_RX # also Net("UART3_TX") 611 | servo_jtag_tck = Net("SERVO_JTAG_TCK") << ec.UART3_TX # also Net("UART3_TX") 612 | 613 | dut = ServoConnector() 614 | gnd << dut.GND 615 | pp3300 >> dut.pins["I2C_3.3V"] 616 | 617 | io = I2cIoExpander() 618 | pp3300 << io.VCCI << decoupling() 619 | gnd << io.GND << io.PAD 620 | gnd << io.A0 # i2c addr 7'H=0x20 621 | Net("I2C_REMOTE_ADC_SDA") << R("4.7k", to=pp3300) << ec.MASTER_I2C_SDA << io.SDA << dut.I2C_SDA 622 | Net("I2C_REMOTE_ADC_SCL") << R("4.7k", to=pp3300) << ec.MASTER_I2C_SCL << io.SCL << dut.I2C_SCL 623 | Net("RESET_L") << io.RESET_L << ec 624 | pp1800 << io.VCCP << decoupling() 625 | 626 | dut_mfg_mode = Net("DUT_MFG_MODE") << dut 627 | mfg_mode_shifter = LevelShifter1() 628 | gnd << mfg_mode_shifter.GND 629 | 630 | Net("FW_WP_EN") << mfg_mode_shifter.VCCA << io.P00 << decoupling() << R("4.7k", to=gnd) 631 | Net("FTDI_MFG_MODE") << io.P01 << mfg_mode_shifter.A 632 | dut_mfg_mode << io.P02 633 | io.P03 << TP(package="TP075") # spare 634 | Net("SPI_HOLD_L") << io.P04 >> dut.SPI2_HOLD_L 635 | Net("DUT_COLD_RESET_L") << io.P05 >> dut 636 | Net("DUT_PWR_BUTTON") << io.P06 >> dut 637 | 638 | Net("DUT_GOOG_REC_MODE_L") << io.P10 >> dut 639 | dut_mfg_mode << io.P11 640 | Net("HPD") << io.P12 >> dut 641 | Net("FW_UP_L") << io.P13 >> dut 642 | Net("DUT_LID_OPEN") << io.P14 >> dut 643 | Net("DUT_DEV_MODE") << io.P15 >> dut 644 | Net("PCH_DISABLE_L") << io.P16 >> dut 645 | io.P17 << TP(package="TP075") # spare 646 | 647 | mfg_mode_shifter.direction_AB << mfg_mode_shifter.DIR 648 | ppdut_spi_vrefs[2] >> mfg_mode_shifter.VCCB << decoupling() 649 | Net("DUT_MFG_MODE_BUF") << R("0", to=dut_mfg_mode) >> mfg_mode_shifter.B 650 | 651 | # JTAG 652 | jtag_connector = JtagConnector() 653 | gnd >> jtag_connector.GND 654 | Net("DUT_WARM_RESET_L") << io.P07 >> dut << jtag_connector.RESET 655 | 656 | jtag_vref = Net("PPDUT_JTAG_VREF") 657 | jtag_vref << dut.JTAG_VREF >> jtag_connector.VCC 658 | 659 | shifter1 = LevelShifter4() 660 | pp3300 >> shifter1.VCCA << decoupling() 661 | jtag_vref >> shifter1.VCCB << decoupling() 662 | gnd >> shifter1.GND 663 | 664 | shifter2 = LevelShifter4() 665 | pp3300 >> shifter2.VCCA << decoupling() 666 | jtag_vref >> shifter2.VCCB << decoupling() 667 | gnd >> shifter2.GND 668 | 669 | jtag_mux = Mux() 670 | pp3300 >> jtag_mux.VCC << decoupling() 671 | gnd >> jtag_mux.GND 672 | Net("SERVO_JTAG_TDO_SEL") << ec >> jtag_mux.SEL 673 | 674 | jtag_output_buffer = OutputBuffer() 675 | pp3300 >> jtag_output_buffer.VCC << decoupling() 676 | gnd >> jtag_output_buffer.GND 677 | Net("SERVO_JTAG_TDO_BUFFER_EN") << ec >> jtag_output_buffer.OE 678 | Net("SERVO_JTAG_MUX_TDO") << jtag_mux.OUT >> jtag_output_buffer.IN 679 | jtag_buffer_to_servo_tdo << jtag_output_buffer.OUT 680 | 681 | Net("JTAG_BUFOUT_EN_L") << ec >> shifter1.OE_L 682 | Net("JTAG_BUFIN_EN_L") << ec >> shifter2.OE_L 683 | 684 | pp3300 >> shifter1.A1 # spare 685 | Net("SERVO_JTAG_TRST_L") << ec << shifter1.A2 686 | Net("SERVO_JTAG_TMS") << ec << shifter1.A3 687 | Net("SERVO_JTAG_TDI") << ec << shifter1.A4 688 | 689 | shifter1.direction_AB >> shifter1.DIR1 # spare 690 | Net("SERVO_JTAG_TRST_DIR") << ec >> shifter1.DIR2 691 | Net("SERVO_JTAG_TMS_DIR") << ec >> shifter1.DIR3 692 | Net("SERVO_JTAG_TDI_DIR") << ec >> shifter1.DIR4 693 | 694 | shifter1.B1 # spare 695 | Net("DUT_JTAG_TRST_L") << dut << shifter1.B2 696 | Net("DUT_JTAG_TMS") >> dut << shifter1.B3 << jtag_connector 697 | Net("DUT_JTAG_TDI") << dut << shifter1.B4 >> shifter2.B3 >> jtag_connector 698 | 699 | Net("DUT_JTAG_TDO") << dut >> shifter2.B1 >> jtag_connector 700 | Net("DUT_JTAG_RTCK") << dut >> shifter2.B2 701 | Net("DUT_JTAG_TCK") << dut >> shifter2.B4 >> jtag_connector 702 | 703 | shifter2.direction_BA >> shifter2.DIR1 704 | shifter2.direction_BA >> shifter2.DIR2 705 | shifter2.direction_BA >> shifter2.DIR3 706 | shifter2.direction_AB >> shifter2.DIR4 707 | 708 | Net("SERVO_JTAG_TDO") << shifter2.A1 >> jtag_mux.IN0 709 | Net("SERVO_JTAG_RTCK") >> ec << shifter2.A2 710 | Net("SERVO_JTAG_SWDIO") << shifter2.A3 >> jtag_mux.IN1 711 | servo_jtag_tck << shifter2.A4 712 | 713 | # SPI1 & 2 714 | # TODO SERVO_TO_SPI1_MUX_CLK 715 | servo_spi_mosi = Net("SERVO_SPI_MOSI") << ec 716 | servo_spi_cs = Net("SERVO_SPI_CS") << ec 717 | 718 | # Since the circuits look so similar, we'll just have a loop 719 | spi_shifters = { 720 | 1: LevelShifter4(), 721 | 2: LevelShifter4(), 722 | } 723 | for i, s in spi_shifters.items(): 724 | # Power supply 725 | vref = ppdut_spi_vrefs[i] 726 | vref << dut.pins["SPI%d_VREF" % i] 727 | 728 | power_switches = [ 729 | ("18", pp1800, PowerSwitch()), 730 | ("33", pp3300, PowerSwitch()), 731 | ] 732 | for voltage, input_rail, power_switch in power_switches: 733 | gnd << power_switch.GND 734 | Net("SPI%d_VREF_%s" % (i, voltage)) << ec >> power_switch.EN << R("4.7k", to=gnd) 735 | input_rail << power_switch.IN 736 | vref << power_switch.OUT 737 | 738 | # Level shifter setup 739 | pp3300 >> s.VCCA << decoupling() 740 | vref >> s.VCCB << decoupling() 741 | gnd >> s.GND 742 | Net("SPI%d_BUF_EN_L" % i) << ec >> s.OE_L 743 | 744 | # MISO 745 | Net("DUT_SPI%d_MISO" % i) << dut >> s.B1 746 | s.direction_BA >> s.DIR1 747 | # A side connected after this loop 748 | 749 | # MOSI 750 | servo_spi_mosi >> s.A2 751 | s.direction_AB >> s.DIR2 752 | Net("DUT_SPI%d_MOSI" % i) << dut >> s.B2 753 | 754 | # CS 755 | servo_spi_cs >> s.A3 756 | s.direction_AB >> s.DIR3 757 | Net("DUT_SPI%d_CS" % i) << dut >> s.B3 758 | 759 | # CLK 760 | # A side connected after this loop 761 | s.direction_AB >> s.DIR4 762 | Net("DUT_SPI%d_CLK" % i) << dut >> s.B4 763 | 764 | spi1_mux = AnalogSwitch() 765 | pp3300 >> spi1_mux.VCC >> decoupling() 766 | gnd >> spi1_mux.GND 767 | Net("SPI1_MUX_SEL") << ec >> spi1_mux.SEL1 >> spi1_mux.SEL2 768 | 769 | Net("SPI_MUX_TODUT_SPI1_MISO") >> spi1_mux.COM1 << spi_shifters[1].A1 770 | Net("SPI_MUX_TO_DUT_SPI1_CLK") << spi1_mux.COM2 >> spi_shifters[1].A4 771 | 772 | Net("SERVO_TO_SPI1_MUX_MISO") << spi1_mux.NO1 << spi_shifters[2].A1 >> ec 773 | Net("SERVO_TO_SPI1_MUX_CLK") >> spi1_mux.NO2 >> spi_shifters[2].A4 << ec 774 | 775 | jtag_buffer_to_servo_tdo << spi1_mux.NC1 776 | servo_jtag_tck >> spi1_mux.NC2 777 | 778 | # UART 1 & 2 779 | uart_shifters = { 780 | 1: LevelShifter2(), 781 | 2: LevelShifter2(), 782 | } 783 | for i, s in uart_shifters.items(): 784 | vref = Net("PPDUT_UART%d_VREF" % i) 785 | vref << dut.pins["UART%d_VREF" % i] 786 | 787 | # Power off to VCCA or VCCB provides isolation 788 | pp3300 >> s.VCCA << decoupling() 789 | vref >> s.VCCB << decoupling() 790 | gnd >> s.GND 791 | Net("UART%d_EN_L" % i) << ec >> s.OE_L 792 | 793 | Net("UART%d_TX" % i) << ec >> s.A1 794 | s.direction_AB >> s.DIR1 795 | Net("UART%d_SERVO_DUT_TX" % i) >> dut << s.B1 796 | 797 | Net("UART%d_DUT_SERVO_TX" % i) << dut >> s.B2 798 | s.direction_BA >> s.DIR2 799 | Net("UART%d_RX" % i) >> ec << s.A2 800 | 801 | global_context.autoname("servo_micro.refdes_mapping") 802 | --------------------------------------------------------------------------------