traceforge-pcb 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- traceforge/__init__.py +3 -0
- traceforge/__main__.py +3 -0
- traceforge/acp_server.py +570 -0
- traceforge/agent.py +680 -0
- traceforge/altium/__init__.py +6 -0
- traceforge/altium/cfb.py +70 -0
- traceforge/altium/cli.py +60 -0
- traceforge/altium/model.py +163 -0
- traceforge/altium/outputs.py +24 -0
- traceforge/altium/pcb.py +301 -0
- traceforge/altium/project.py +328 -0
- traceforge/altium/schematic.py +400 -0
- traceforge/altium/tools.py +166 -0
- traceforge/approval.py +219 -0
- traceforge/attachments.py +346 -0
- traceforge/background.py +253 -0
- traceforge/bench/__init__.py +5 -0
- traceforge/bench/adapters.py +337 -0
- traceforge/bench/checks.py +410 -0
- traceforge/bench/cli.py +121 -0
- traceforge/bench/design.py +439 -0
- traceforge/bench/kicad.py +99 -0
- traceforge/bench/partscheck.py +159 -0
- traceforge/bench/review.py +100 -0
- traceforge/bench/runner.py +226 -0
- traceforge/bench/score.py +204 -0
- traceforge/bench/tasks.py +102 -0
- traceforge/chat.py +1185 -0
- traceforge/checks.py +328 -0
- traceforge/checks_sides.py +118 -0
- traceforge/cli.py +2224 -0
- traceforge/clipboard.py +107 -0
- traceforge/config.py +299 -0
- traceforge/context.py +276 -0
- traceforge/design/__init__.py +1 -0
- traceforge/design/constraints.py +397 -0
- traceforge/design/generate.py +1789 -0
- traceforge/design/outline.py +905 -0
- traceforge/design/schematic_layout.py +932 -0
- traceforge/design/schematic_lint.py +329 -0
- traceforge/design/schematic_route.py +207 -0
- traceforge/design/sheets.py +854 -0
- traceforge/design/sides.py +328 -0
- traceforge/design/spec.py +462 -0
- traceforge/design/specsheet.py +2351 -0
- traceforge/design/specsheet_render.py +183 -0
- traceforge/desktop/__init__.py +109 -0
- traceforge/desktop/keys.py +90 -0
- traceforge/desktop/project.py +443 -0
- traceforge/desktop/review.py +20 -0
- traceforge/desktop/server.py +1277 -0
- traceforge/desktop/static/app.css +223 -0
- traceforge/desktop/static/app.js +683 -0
- traceforge/desktop/static/highlight.js +77 -0
- traceforge/desktop/static/index.html +89 -0
- traceforge/desktop/static/md.js +90 -0
- traceforge/desktop/static/viewer.js +213 -0
- traceforge/desktop/symbols.py +212 -0
- traceforge/desktop/update.py +65 -0
- traceforge/diagnostics.py +67 -0
- traceforge/errata.py +219 -0
- traceforge/fab.py +108 -0
- traceforge/fab_layers.py +44 -0
- traceforge/features/__init__.py +6 -0
- traceforge/features/candidates.py +117 -0
- traceforge/features/cli.py +200 -0
- traceforge/features/package.py +433 -0
- traceforge/features/revdiff.py +224 -0
- traceforge/features/sarif.py +145 -0
- traceforge/features/viewer.py +265 -0
- traceforge/firmware/__init__.py +3 -0
- traceforge/firmware/cli.py +174 -0
- traceforge/firmware/drivers.py +372 -0
- traceforge/firmware/drivers_data.py +256 -0
- traceforge/firmware/mcus.py +177 -0
- traceforge/firmware/netlist.py +55 -0
- traceforge/firmware/pinmap.py +296 -0
- traceforge/firmware/scaffold.py +466 -0
- traceforge/firmware/toolchain.py +262 -0
- traceforge/hooks.py +237 -0
- traceforge/kicad/__init__.py +0 -0
- traceforge/kicad/cli.py +519 -0
- traceforge/kicad/diffpair.py +174 -0
- traceforge/kicad/edgegeom.py +191 -0
- traceforge/kicad/edit.py +405 -0
- traceforge/kicad/escape.py +1410 -0
- traceforge/kicad/formats.py +1073 -0
- traceforge/kicad/impedance.py +320 -0
- traceforge/kicad/install.py +75 -0
- traceforge/kicad/libraries.py +490 -0
- traceforge/kicad/models3d.py +942 -0
- traceforge/kicad/outputs.py +81 -0
- traceforge/kicad/pcb.py +787 -0
- traceforge/kicad/planes.py +1242 -0
- traceforge/kicad/project.py +136 -0
- traceforge/kicad/route.py +2698 -0
- traceforge/kicad/routegeom.py +164 -0
- traceforge/kicad/schematic.py +268 -0
- traceforge/kicad/sides.py +34 -0
- traceforge/kicad/stackup.py +424 -0
- traceforge/kicad_plugin/__init__.py +12 -0
- traceforge/kicad_plugin/actions.py +92 -0
- traceforge/kicad_plugin/cli.py +66 -0
- traceforge/kicad_plugin/client.py +86 -0
- traceforge/kicad_plugin/context.py +257 -0
- traceforge/kicad_plugin/discovery.py +81 -0
- traceforge/kicad_plugin/install.py +203 -0
- traceforge/kicad_plugin/overlay.py +60 -0
- traceforge/kicad_plugin/payload/action_plugin.py +65 -0
- traceforge/kicad_plugin/payload/ipc_main.py +42 -0
- traceforge/kicad_plugin/payload/plugin.json +90 -0
- traceforge/kicad_plugin/payload/requirements.txt +1 -0
- traceforge/kicad_plugin/views.py +202 -0
- traceforge/langchain.py +57 -0
- traceforge/local.py +580 -0
- traceforge/mcp_server.py +304 -0
- traceforge/models.py +310 -0
- traceforge/next_steps.py +110 -0
- traceforge/parts/__init__.py +52 -0
- traceforge/parts/alternates.py +488 -0
- traceforge/parts/bom.py +423 -0
- traceforge/parts/brief.py +705 -0
- traceforge/parts/catalog.py +297 -0
- traceforge/parts/cli_sourcing.py +156 -0
- traceforge/parts/data/duty_rates.yaml +37 -0
- traceforge/parts/data/hs_codes.yaml +103 -0
- traceforge/parts/datasheet.py +278 -0
- traceforge/parts/fx.py +100 -0
- traceforge/parts/model.py +185 -0
- traceforge/parts/net.py +187 -0
- traceforge/parts/offers.py +46 -0
- traceforge/parts/packages.py +397 -0
- traceforge/parts/pinout.py +714 -0
- traceforge/parts/providers/__init__.py +17 -0
- traceforge/parts/providers/base.py +100 -0
- traceforge/parts/providers/jlc.py +364 -0
- traceforge/parts/providers/keyed.py +661 -0
- traceforge/parts/report.py +508 -0
- traceforge/parts/supply.py +330 -0
- traceforge/parts/verify.py +455 -0
- traceforge/parts/web.py +575 -0
- traceforge/plans.py +70 -0
- traceforge/preferences.py +127 -0
- traceforge/prices.py +130 -0
- traceforge/prices.yaml +28 -0
- traceforge/prompts/__init__.py +54 -0
- traceforge/prompts/system.md +103 -0
- traceforge/providers.py +764 -0
- traceforge/remote/__init__.py +33 -0
- traceforge/remote/client.py +422 -0
- traceforge/remote/oidc.py +240 -0
- traceforge/remote/orgs.py +510 -0
- traceforge/remote/server.py +690 -0
- traceforge/remote/store.py +100 -0
- traceforge/review_changes.py +187 -0
- traceforge/routing.py +99 -0
- traceforge/runtime.py +435 -0
- traceforge/sessions.py +324 -0
- traceforge/sexpr.py +279 -0
- traceforge/sim/__init__.py +17 -0
- traceforge/sim/acmath.py +234 -0
- traceforge/sim/circuit.py +499 -0
- traceforge/sim/cli.py +162 -0
- traceforge/sim/engine.py +172 -0
- traceforge/sim/ltspice.py +175 -0
- traceforge/sim/measure.py +123 -0
- traceforge/sim/models.py +76 -0
- traceforge/sim/project.py +218 -0
- traceforge/sim/rawfile.py +123 -0
- traceforge/sim/results.py +298 -0
- traceforge/sim/tune.py +172 -0
- traceforge/skill_commands.py +165 -0
- traceforge/skill_library/circuit-simulation/SKILL.md +30 -0
- traceforge/skill_library/compliance-review/SKILL.md +51 -0
- traceforge/skill_library/design-review/SKILL.md +47 -0
- traceforge/skill_library/errata-hunt/SKILL.md +30 -0
- traceforge/skill_library/firmware/SKILL.md +26 -0
- traceforge/skill_library/power-budget/SKILL.md +27 -0
- traceforge/skill_library/release-package/SKILL.md +29 -0
- traceforge/skill_library/sourcing-brief/SKILL.md +24 -0
- traceforge/skill_library/thermal-review/SKILL.md +15 -0
- traceforge/skills.py +131 -0
- traceforge/thermal.py +557 -0
- traceforge/tools/__init__.py +48 -0
- traceforge/tools/agent_tools.py +213 -0
- traceforge/tools/base.py +303 -0
- traceforge/tools/batch.py +155 -0
- traceforge/tools/connections.py +245 -0
- traceforge/tools/files.py +777 -0
- traceforge/tools/firmware_tools.py +218 -0
- traceforge/tools/kicad_tools.py +1072 -0
- traceforge/tools/models3d_tools.py +141 -0
- traceforge/tools/parts_tools.py +473 -0
- traceforge/tools/processes.py +246 -0
- traceforge/tools/review_tools.py +178 -0
- traceforge/tools/sim_tools.py +230 -0
- traceforge/tui.py +620 -0
- traceforge/updates.py +124 -0
- traceforge/validate/__init__.py +77 -0
- traceforge/validate/builtins.py +1091 -0
- traceforge/validate/circuits.py +1240 -0
- traceforge/validate/config.py +410 -0
- traceforge/validate/default.yaml +113 -0
- traceforge/validate/design.py +191 -0
- traceforge/validate/explain.py +112 -0
- traceforge/validate/grounding.py +169 -0
- traceforge/validate/ipc2221.py +350 -0
- traceforge/validate/multilayer.py +805 -0
- traceforge/validate/netmodel.py +261 -0
- traceforge/validate/partcheck.py +472 -0
- traceforge/validate/report.py +280 -0
- traceforge/validate/review.py +322 -0
- traceforge/validate/runner.py +662 -0
- traceforge/workspace.py +171 -0
- traceforge_pcb-1.0.0.dist-info/METADATA +463 -0
- traceforge_pcb-1.0.0.dist-info/RECORD +219 -0
- traceforge_pcb-1.0.0.dist-info/WHEEL +4 -0
- traceforge_pcb-1.0.0.dist-info/entry_points.txt +2 -0
- traceforge_pcb-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""MCU pin tables (data). Package pin number -> pin name, and what each pin can do.
|
|
2
|
+
|
|
3
|
+
Sources are cited per entry. ``functions`` use these tokens: GPIO, ADC, PWM, I2C_SDA,
|
|
4
|
+
I2C_SCL, SPI_MOSI, SPI_MISO, SPI_SCK, UART_TX, UART_RX. Pins a design may use even
|
|
5
|
+
though the datasheet warns about them go in ``caution``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Mcu:
|
|
16
|
+
key: str
|
|
17
|
+
family: str # avr | esp32 | stm32
|
|
18
|
+
name: str
|
|
19
|
+
package: str
|
|
20
|
+
source: str # datasheet table the data was taken from
|
|
21
|
+
match: str # regex over "<symbol> <value> <footprint>"
|
|
22
|
+
pins: dict[str, str] = field(default_factory=dict) # package pin number -> pin name
|
|
23
|
+
functions: dict[str, tuple[str, ...]] = field(default_factory=dict) # pin name -> tokens
|
|
24
|
+
arduino: dict[str, int] = field(default_factory=dict) # pin name -> Arduino number
|
|
25
|
+
adc: dict[str, int] = field(default_factory=dict) # pin name -> ADC channel
|
|
26
|
+
caution: dict[str, str] = field(default_factory=dict) # pin name -> warning
|
|
27
|
+
power_pins: tuple[str, ...] = ()
|
|
28
|
+
platformio: dict[str, str] = field(default_factory=dict)
|
|
29
|
+
fqbn: str = ""
|
|
30
|
+
f_cpu: int = 0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
_ATTINY85 = Mcu(
|
|
34
|
+
key="attiny85",
|
|
35
|
+
family="avr",
|
|
36
|
+
name="ATtiny85",
|
|
37
|
+
package="SOIC-8 / DIP-8",
|
|
38
|
+
source="Microchip ATtiny25/45/85 datasheet (DS40002586), section 1.1 Pin Descriptions, "
|
|
39
|
+
"section 10.3 Alternate Port Functions",
|
|
40
|
+
match=r"attiny85",
|
|
41
|
+
pins={
|
|
42
|
+
"1": "PB5",
|
|
43
|
+
"2": "PB3",
|
|
44
|
+
"3": "PB4",
|
|
45
|
+
"4": "GND",
|
|
46
|
+
"5": "PB0",
|
|
47
|
+
"6": "PB1",
|
|
48
|
+
"7": "PB2",
|
|
49
|
+
"8": "VCC",
|
|
50
|
+
},
|
|
51
|
+
functions={
|
|
52
|
+
# PB0: MOSI/DI/SDA/AIN0/OC0A; PB1: MISO/DO/AIN1/OC0B/OC1A; PB2: SCK/USCK/SCL/ADC1
|
|
53
|
+
"PB0": ("GPIO", "PWM", "I2C_SDA", "SPI_MOSI"),
|
|
54
|
+
"PB1": ("GPIO", "PWM", "SPI_MISO"),
|
|
55
|
+
"PB2": ("GPIO", "ADC", "I2C_SCL", "SPI_SCK"),
|
|
56
|
+
"PB3": ("GPIO", "ADC", "PWM"),
|
|
57
|
+
"PB4": ("GPIO", "ADC", "PWM"),
|
|
58
|
+
"PB5": ("GPIO", "ADC"),
|
|
59
|
+
},
|
|
60
|
+
arduino={"PB0": 0, "PB1": 1, "PB2": 2, "PB3": 3, "PB4": 4, "PB5": 5},
|
|
61
|
+
adc={"PB5": 0, "PB2": 1, "PB4": 2, "PB3": 3},
|
|
62
|
+
caution={"PB5": "RESET pin: using it as I/O needs the RSTDISBL fuse and loses ISP"},
|
|
63
|
+
power_pins=("VCC", "GND"),
|
|
64
|
+
platformio={"platform": "atmelavr", "board": "attiny85", "framework": "arduino"},
|
|
65
|
+
fqbn="ATTinyCore:avr:attinyx5",
|
|
66
|
+
f_cpu=8_000_000,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
_ESP32S3_PINS = {
|
|
70
|
+
"1": "GND",
|
|
71
|
+
"2": "3V3",
|
|
72
|
+
"3": "EN",
|
|
73
|
+
"4": "IO4",
|
|
74
|
+
"5": "IO5",
|
|
75
|
+
"6": "IO6",
|
|
76
|
+
"7": "IO7",
|
|
77
|
+
"8": "IO15",
|
|
78
|
+
"9": "IO16",
|
|
79
|
+
"10": "IO17",
|
|
80
|
+
"11": "IO18",
|
|
81
|
+
"12": "IO8",
|
|
82
|
+
"13": "IO19",
|
|
83
|
+
"14": "IO20",
|
|
84
|
+
"15": "IO3",
|
|
85
|
+
"16": "IO46",
|
|
86
|
+
"17": "IO9",
|
|
87
|
+
"18": "IO10",
|
|
88
|
+
"19": "IO11",
|
|
89
|
+
"20": "IO12",
|
|
90
|
+
"21": "IO13",
|
|
91
|
+
"22": "IO14",
|
|
92
|
+
"23": "IO21",
|
|
93
|
+
"24": "IO47",
|
|
94
|
+
"25": "IO48",
|
|
95
|
+
"26": "IO45",
|
|
96
|
+
"27": "IO0",
|
|
97
|
+
"28": "IO35",
|
|
98
|
+
"29": "IO36",
|
|
99
|
+
"30": "IO37",
|
|
100
|
+
"31": "IO38",
|
|
101
|
+
"32": "IO39",
|
|
102
|
+
"33": "IO40",
|
|
103
|
+
"34": "IO41",
|
|
104
|
+
"35": "IO42",
|
|
105
|
+
"36": "IO44",
|
|
106
|
+
"37": "IO43",
|
|
107
|
+
"38": "IO2",
|
|
108
|
+
"39": "IO1",
|
|
109
|
+
"40": "GND",
|
|
110
|
+
"41": "GND",
|
|
111
|
+
}
|
|
112
|
+
_ESP32S3_GPIO = [n for n in range(49) if n not in range(22, 26)] # GPIO22-25 do not exist
|
|
113
|
+
_ESP32S3_FUNCS: dict[str, tuple[str, ...]] = {}
|
|
114
|
+
for _n in _ESP32S3_GPIO:
|
|
115
|
+
# The GPIO matrix routes any digital peripheral to any pin; ADC1 = 1-10, ADC2 = 11-20.
|
|
116
|
+
_f = ["GPIO", "PWM", "I2C_SDA", "I2C_SCL", "SPI_MOSI", "SPI_MISO", "SPI_SCK"]
|
|
117
|
+
_f += ["UART_TX", "UART_RX"]
|
|
118
|
+
if 1 <= _n <= 20:
|
|
119
|
+
_f.append("ADC")
|
|
120
|
+
_ESP32S3_FUNCS[f"IO{_n}"] = tuple(_f)
|
|
121
|
+
_ESP32S3_ADC = {f"IO{n}": n - 1 for n in range(1, 21)} # ADC1 ch0-9, ADC2 ch0-9 (n 11-20)
|
|
122
|
+
_ESP32S3_ADC.update({f"IO{n}": n - 11 for n in range(11, 21)})
|
|
123
|
+
_ESP32S3_CAUTION = {
|
|
124
|
+
**{f"IO{n}": "strapping pin: keep its boot level" for n in (0, 3, 45, 46)},
|
|
125
|
+
**{f"IO{n}": "connected to the module's flash/PSRAM: not usable" for n in range(26, 33)},
|
|
126
|
+
**{
|
|
127
|
+
f"IO{n}": "octal PSRAM on N8R8/N16R8 modules: not usable there"
|
|
128
|
+
for n in (33, 34, 35, 36, 37)
|
|
129
|
+
},
|
|
130
|
+
"IO19": "USB D- (native USB); using it as GPIO disables USB",
|
|
131
|
+
"IO20": "USB D+ (native USB); using it as GPIO disables USB",
|
|
132
|
+
}
|
|
133
|
+
_ESP32S3 = Mcu(
|
|
134
|
+
key="esp32s3",
|
|
135
|
+
family="esp32",
|
|
136
|
+
name="ESP32-S3-WROOM-1",
|
|
137
|
+
package="module, 41 pads",
|
|
138
|
+
source="Espressif ESP32-S3-WROOM-1 datasheet, Table 3-1 Pin Definitions; ESP32-S3 datasheet "
|
|
139
|
+
"section 2.2 (IO MUX, GPIO matrix, strapping pins)",
|
|
140
|
+
match=r"esp32-s3|esp32s3",
|
|
141
|
+
pins=_ESP32S3_PINS,
|
|
142
|
+
functions=_ESP32S3_FUNCS,
|
|
143
|
+
arduino={f"IO{n}": n for n in _ESP32S3_GPIO},
|
|
144
|
+
adc=_ESP32S3_ADC,
|
|
145
|
+
caution=_ESP32S3_CAUTION,
|
|
146
|
+
power_pins=("3V3", "GND", "EN"),
|
|
147
|
+
platformio={
|
|
148
|
+
"platform": "espressif32",
|
|
149
|
+
"board": "esp32-s3-devkitc-1",
|
|
150
|
+
"framework": "arduino",
|
|
151
|
+
},
|
|
152
|
+
fqbn="esp32:esp32:esp32s3",
|
|
153
|
+
f_cpu=240_000_000,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
_STM32F103 = Mcu(
|
|
157
|
+
key="stm32f103",
|
|
158
|
+
family="stm32",
|
|
159
|
+
name="STM32F103C8T6",
|
|
160
|
+
package="LQFP-48",
|
|
161
|
+
source="ST STM32F103x8/xB datasheet (DS5319), Table 5 pin definitions. Only pin names "
|
|
162
|
+
"are used (e.g. PA5); package pin numbers are not tabulated here",
|
|
163
|
+
match=r"stm32f103",
|
|
164
|
+
functions={},
|
|
165
|
+
power_pins=("VDD", "VSS", "VBAT", "VDDA", "VSSA", "NRST", "BOOT0"),
|
|
166
|
+
platformio={"platform": "ststm32", "board": "bluepill_f103c8", "framework": "stm32cube"},
|
|
167
|
+
f_cpu=8_000_000,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
MCUS: dict[str, Mcu] = {m.key: m for m in (_ATTINY85, _ESP32S3, _STM32F103)}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def find_mcu(text: str) -> Mcu | None:
|
|
174
|
+
for mcu in MCUS.values():
|
|
175
|
+
if re.search(mcu.match, text, re.I):
|
|
176
|
+
return mcu
|
|
177
|
+
return None
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Read the connections of a design: nets -> (reference, pin) and what each part is."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tempfile
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NetlistError(Exception):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Netlist:
|
|
16
|
+
nets: dict[str, list[tuple[str, str]]]
|
|
17
|
+
parts: dict[str, str] = field(default_factory=dict) # reference -> "symbol value footprint"
|
|
18
|
+
source: str = ""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_netlist(path: str | Path) -> Netlist:
|
|
22
|
+
"""Load a design spec (.yaml/.json), a schematic (.kicad_sch) or a KiCad netlist (.net)."""
|
|
23
|
+
path = Path(path)
|
|
24
|
+
if not path.exists():
|
|
25
|
+
raise NetlistError(f"{path} does not exist")
|
|
26
|
+
suffix = path.suffix.lower()
|
|
27
|
+
if suffix in {".yaml", ".yml", ".json"}:
|
|
28
|
+
from traceforge.design.spec import load_spec
|
|
29
|
+
|
|
30
|
+
spec = load_spec(path)
|
|
31
|
+
parts = {p.ref: f"{p.symbol} {p.value} {p.footprint}" for p in spec.parts}
|
|
32
|
+
return Netlist(spec.all_nets(), parts, str(path))
|
|
33
|
+
if suffix == ".kicad_sch":
|
|
34
|
+
from traceforge.kicad.cli import KicadCli, KicadCliError
|
|
35
|
+
from traceforge.kicad.schematic import read_schematic
|
|
36
|
+
|
|
37
|
+
cli = KicadCli()
|
|
38
|
+
if not cli.available:
|
|
39
|
+
raise NetlistError("exporting a netlist from a schematic needs kicad-cli")
|
|
40
|
+
try:
|
|
41
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
42
|
+
nets = cli.netlist(path, Path(tmp))
|
|
43
|
+
except KicadCliError as exc:
|
|
44
|
+
raise NetlistError(str(exc)) from exc
|
|
45
|
+
sch = read_schematic(path)
|
|
46
|
+
parts = {
|
|
47
|
+
ref: f"{units[0].lib_id} {units[0].value} {units[0].footprint}"
|
|
48
|
+
for ref, units in sch.components().items()
|
|
49
|
+
}
|
|
50
|
+
return Netlist(nets, parts, str(path))
|
|
51
|
+
if suffix in {".net", ".xml"}:
|
|
52
|
+
from traceforge.kicad.cli import parse_netlist
|
|
53
|
+
|
|
54
|
+
return Netlist(parse_netlist(path.read_text(encoding="utf-8")), {}, str(path))
|
|
55
|
+
raise NetlistError(f"unsupported design file {path.name} (use .yaml, .kicad_sch or .net)")
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""Signal-to-pin assignments for an MCU from the netlist, and the pin headers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
from traceforge.firmware.mcus import MCUS, Mcu, find_mcu
|
|
9
|
+
from traceforge.firmware.netlist import Netlist, NetlistError
|
|
10
|
+
|
|
11
|
+
_POWER = re.compile(
|
|
12
|
+
r"^(\+?\d+V\d*|\+?\d+\.\d+V|VCC\w*|VDD\w*|VBUS\w*|VIN|VBAT|GND\w*|AGND|VSS\w*)$", re.I
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# (regex over the upper-case net name, peripheral function, direction)
|
|
16
|
+
_ROLES: list[tuple[str, str, str]] = [
|
|
17
|
+
(r"(^|_)(SDA|SIOD)(\d*)($|_)", "I2C_SDA", "io"),
|
|
18
|
+
(r"(^|_)(SCL|SIOC)(\d*)($|_)", "I2C_SCL", "io"),
|
|
19
|
+
(r"(^|_)(MOSI|COPI|SDI)($|_)", "SPI_MOSI", "out"),
|
|
20
|
+
(r"(^|_)(MISO|CIPO|SDO)($|_)", "SPI_MISO", "in"),
|
|
21
|
+
(r"(^|_)(SCK|SCLK|SPI_CLK)($|_)", "SPI_SCK", "out"),
|
|
22
|
+
(r"(^|_)(TX|TXD|UART_TX)\d*($|_)", "UART_TX", "out"),
|
|
23
|
+
(r"(^|_)(RX|RXD|UART_RX)\d*($|_)", "UART_RX", "in"),
|
|
24
|
+
(r"(^|_)(PWM|BL|BACKLIGHT)($|_)", "PWM", "out"),
|
|
25
|
+
(r"(^|_)(ADC|AIN|VSENSE|VBAT_SENSE|VMON|NTC)\d*($|_)", "ADC", "in"),
|
|
26
|
+
(r"(^|_)(BTN|BUTTON|SW|KEY|IRQ|INT|DRDY|PIR|BOOT|PCLK|VSYNC|HREF|Y\d)($|_)", "GPIO", "in"),
|
|
27
|
+
(r"(^|_)(LED|EN|CS|SS|NSS|CSN|RST|RESET|PWDN|XCLK|BUZZER|RELAY)($|_)", "GPIO", "out"),
|
|
28
|
+
]
|
|
29
|
+
_SPI_CS = re.compile(r"(^|_)(CS|SS|NSS|CSN)($|_)")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class PinAssignment:
|
|
34
|
+
net: str
|
|
35
|
+
macro: str # identifier-safe upper-case name, unique in the map
|
|
36
|
+
ref: str
|
|
37
|
+
pin: str # as written in the netlist (number or name)
|
|
38
|
+
pin_name: str # the MCU pin name (PB3, IO4, PA5)
|
|
39
|
+
function: str # GPIO, ADC, PWM, I2C_SDA, ...
|
|
40
|
+
direction: str # in | out | io
|
|
41
|
+
pull_up: bool = False
|
|
42
|
+
capabilities: tuple[str, ...] = ()
|
|
43
|
+
port: str | None = None # AVR/STM32 port letter
|
|
44
|
+
bit: int | None = None
|
|
45
|
+
number: int | None = None # the number code uses (GPIO number, Arduino pin)
|
|
46
|
+
adc_channel: int | None = None
|
|
47
|
+
warnings: list[str] = field(default_factory=list)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class PinMap:
|
|
52
|
+
mcu: Mcu
|
|
53
|
+
ref: str
|
|
54
|
+
assignments: list[PinAssignment]
|
|
55
|
+
unconnected: list[str] = field(default_factory=list)
|
|
56
|
+
warnings: list[str] = field(default_factory=list)
|
|
57
|
+
|
|
58
|
+
def by_net(self, net: str) -> PinAssignment | None:
|
|
59
|
+
return next((a for a in self.assignments if a.net == net), None)
|
|
60
|
+
|
|
61
|
+
def as_dict(self) -> dict:
|
|
62
|
+
return {
|
|
63
|
+
"mcu": self.mcu.name,
|
|
64
|
+
"ref": self.ref,
|
|
65
|
+
"source": self.mcu.source,
|
|
66
|
+
"pins": [
|
|
67
|
+
{
|
|
68
|
+
"net": a.net,
|
|
69
|
+
"macro": f"PIN_{a.macro}",
|
|
70
|
+
"pin": a.pin,
|
|
71
|
+
"pin_name": a.pin_name,
|
|
72
|
+
"function": a.function,
|
|
73
|
+
"direction": a.direction,
|
|
74
|
+
"number": a.number,
|
|
75
|
+
"adc_channel": a.adc_channel,
|
|
76
|
+
"warnings": a.warnings,
|
|
77
|
+
}
|
|
78
|
+
for a in self.assignments
|
|
79
|
+
],
|
|
80
|
+
"warnings": self.warnings,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def classify(net: str) -> tuple[str, str, bool]:
|
|
85
|
+
"""Guess (function, direction, pull-up) from a net name."""
|
|
86
|
+
name = net.upper().lstrip("/")
|
|
87
|
+
for pattern, function, direction in _ROLES:
|
|
88
|
+
if re.search(pattern, name):
|
|
89
|
+
pull = function == "GPIO" and direction == "in" and "BTN" in name
|
|
90
|
+
if function == "SPI_SCK" and re.search(r"(^|_)SCL($|_)", name):
|
|
91
|
+
continue
|
|
92
|
+
return function, direction, pull
|
|
93
|
+
if _SPI_CS.search(name):
|
|
94
|
+
return "GPIO", "out", False
|
|
95
|
+
return "GPIO", "io", False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def macro_name(net: str) -> str:
|
|
99
|
+
return re.sub(r"[^A-Z0-9]+", "_", net.upper().replace("+", "P").replace("~", "N")).strip("_")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _pin_name(mcu: Mcu, pin: str) -> str | None:
|
|
103
|
+
if pin in mcu.pins:
|
|
104
|
+
return mcu.pins[pin]
|
|
105
|
+
upper = pin.upper()
|
|
106
|
+
if mcu.family == "esp32":
|
|
107
|
+
upper = {"RXD0": "IO44", "TXD0": "IO43"}.get(upper, re.sub(r"^GPIO", "IO", upper))
|
|
108
|
+
names = set(mcu.pins.values())
|
|
109
|
+
return upper if upper in names else None
|
|
110
|
+
return (
|
|
111
|
+
upper if re.fullmatch(r"P[A-K]\d{1,2}", upper) or upper in set(mcu.pins.values()) else None
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _split(mcu: Mcu, name: str) -> tuple[str | None, int | None, int | None]:
|
|
116
|
+
"""Return (port, bit, number) for a pin name."""
|
|
117
|
+
if mcu.family == "avr":
|
|
118
|
+
m = re.fullmatch(r"P([A-F])(\d)", name)
|
|
119
|
+
return (m.group(1), int(m.group(2)), mcu.arduino.get(name)) if m else (None, None, None)
|
|
120
|
+
if mcu.family == "esp32":
|
|
121
|
+
m = re.fullmatch(r"IO(\d+)", name)
|
|
122
|
+
return (None, None, int(m.group(1))) if m else (None, None, None)
|
|
123
|
+
m = re.fullmatch(r"P([A-K])(\d{1,2})", name)
|
|
124
|
+
if m:
|
|
125
|
+
return m.group(1), int(m.group(2)), (ord(m.group(1)) - 65) * 16 + int(m.group(2))
|
|
126
|
+
return None, None, None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def find_mcu_ref(netlist: Netlist, mcu: Mcu | None = None) -> tuple[str, Mcu]:
|
|
130
|
+
for ref, text in sorted(netlist.parts.items()):
|
|
131
|
+
found = find_mcu(text)
|
|
132
|
+
if found is not None and (mcu is None or found.key == mcu.key):
|
|
133
|
+
return ref, found
|
|
134
|
+
raise NetlistError(
|
|
135
|
+
"no known MCU in the design (supported: "
|
|
136
|
+
+ ", ".join(m.name for m in MCUS.values())
|
|
137
|
+
+ "); pass --mcu and --ref"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def build_pinmap(netlist: Netlist, ref: str | None = None, mcu: str | None = None) -> PinMap:
|
|
142
|
+
chosen = MCUS.get(mcu) if mcu else None
|
|
143
|
+
if mcu and chosen is None:
|
|
144
|
+
raise NetlistError(f"unknown MCU {mcu!r} (known: {', '.join(MCUS)})")
|
|
145
|
+
if ref is None:
|
|
146
|
+
ref, chosen = find_mcu_ref(netlist, chosen)
|
|
147
|
+
elif chosen is None:
|
|
148
|
+
chosen = find_mcu(netlist.parts.get(ref, ""))
|
|
149
|
+
if chosen is None:
|
|
150
|
+
raise NetlistError(f"{ref} is not a known MCU; pass --mcu")
|
|
151
|
+
pm = PinMap(chosen, ref, [])
|
|
152
|
+
used: set[str] = set()
|
|
153
|
+
for net, members in netlist.nets.items():
|
|
154
|
+
if _POWER.match(net.lstrip("/")) or net.lstrip("/").startswith("unconnected-"):
|
|
155
|
+
continue
|
|
156
|
+
for member_ref, pin in members:
|
|
157
|
+
if member_ref != ref:
|
|
158
|
+
continue
|
|
159
|
+
name = _pin_name(chosen, str(pin))
|
|
160
|
+
if name is None:
|
|
161
|
+
pm.warnings.append(f"{ref}.{pin} (net {net}): not a pin of {chosen.name}")
|
|
162
|
+
continue
|
|
163
|
+
if name in chosen.power_pins or name == "GND":
|
|
164
|
+
continue
|
|
165
|
+
function, direction, pull = classify(net)
|
|
166
|
+
port, bit, number = _split(chosen, name)
|
|
167
|
+
caps = chosen.functions.get(name, ())
|
|
168
|
+
macro = macro_name(net)
|
|
169
|
+
base, k = macro, 2
|
|
170
|
+
while macro in used:
|
|
171
|
+
macro, k = f"{base}_{k}", k + 1
|
|
172
|
+
used.add(macro)
|
|
173
|
+
item = PinAssignment(
|
|
174
|
+
net, macro, ref, str(pin), name, function, direction, pull, caps,
|
|
175
|
+
port, bit, number, chosen.adc.get(name),
|
|
176
|
+
) # fmt: skip
|
|
177
|
+
if caps and function not in caps and function != "GPIO":
|
|
178
|
+
item.warnings.append(
|
|
179
|
+
f"{net} looks like {function} but {name} cannot do it (can: {', '.join(caps)})"
|
|
180
|
+
)
|
|
181
|
+
if function == "GPIO" and caps and "GPIO" not in caps:
|
|
182
|
+
item.warnings.append(f"{name} has no GPIO function")
|
|
183
|
+
if name in chosen.caution:
|
|
184
|
+
item.warnings.append(f"{name}: {chosen.caution[name]}")
|
|
185
|
+
pm.assignments.append(item)
|
|
186
|
+
pm.assignments.sort(key=lambda a: (a.number is None, a.number or 0, a.net))
|
|
187
|
+
mapped = {a.pin_name for a in pm.assignments}
|
|
188
|
+
pm.unconnected = sorted(
|
|
189
|
+
n
|
|
190
|
+
for n in set(chosen.pins.values())
|
|
191
|
+
if n not in mapped and n not in chosen.power_pins and n != "GND"
|
|
192
|
+
)
|
|
193
|
+
for a in pm.assignments:
|
|
194
|
+
pm.warnings.extend(a.warnings)
|
|
195
|
+
_check_peripherals(pm)
|
|
196
|
+
return pm
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _check_peripherals(pm: PinMap) -> None:
|
|
200
|
+
funcs: dict[str, list[PinAssignment]] = {}
|
|
201
|
+
for a in pm.assignments:
|
|
202
|
+
funcs.setdefault(a.function, []).append(a)
|
|
203
|
+
for function, items in funcs.items():
|
|
204
|
+
if function.startswith(("I2C", "SPI", "UART")) and len(items) > 1:
|
|
205
|
+
pm.warnings.append(
|
|
206
|
+
f"{function} appears on several nets: " + ", ".join(i.net for i in items)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ---- headers -------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
_HEADER_NOTE = "Generated by traceforge firmware pins from {src}. Do not edit; regenerate."
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _entries(pm: PinMap) -> list[tuple[str, str, PinAssignment]]:
|
|
216
|
+
"""(macro, value expression, assignment) for every pin."""
|
|
217
|
+
out = []
|
|
218
|
+
for a in pm.assignments:
|
|
219
|
+
value = str(a.number) if a.number is not None else f'"{a.pin_name}"'
|
|
220
|
+
out.append((f"PIN_{a.macro}", value, a))
|
|
221
|
+
return out
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _aliases(pm: PinMap) -> list[tuple[str, str]]:
|
|
225
|
+
"""Peripheral aliases (PIN_I2C_SDA = PIN_<net>) and camera names for esp_camera."""
|
|
226
|
+
aliases: list[tuple[str, str]] = []
|
|
227
|
+
seen: set[str] = set()
|
|
228
|
+
for a in pm.assignments:
|
|
229
|
+
if a.function.startswith(("I2C_", "SPI_", "UART_")) and a.function not in seen:
|
|
230
|
+
seen.add(a.function)
|
|
231
|
+
aliases.append((f"PIN_{a.function}", f"PIN_{a.macro}"))
|
|
232
|
+
if a.macro.startswith("CAM_"):
|
|
233
|
+
aliases.append((f"CAM_PIN_{a.macro[4:]}", f"PIN_{a.macro}"))
|
|
234
|
+
return aliases
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def render_header(pm: PinMap, style: str = "c", source: str = "the netlist") -> str:
|
|
238
|
+
"""Render ``pins.h`` (style c), ``pins.hpp`` (cpp) or ``board_pins.rs`` (rust)."""
|
|
239
|
+
note = _HEADER_NOTE.format(src=source)
|
|
240
|
+
entries = _entries(pm)
|
|
241
|
+
aliases = _aliases(pm)
|
|
242
|
+
if style == "rust":
|
|
243
|
+
lines = [f"//! {note}", f"//! MCU {pm.mcu.name} ({pm.ref}); pin data: {pm.mcu.source}", ""]
|
|
244
|
+
for macro, value, a in entries:
|
|
245
|
+
lines.append(f"/// {a.net}: {pm.ref} pin {a.pin} = {a.pin_name}, {a.function}")
|
|
246
|
+
if a.number is not None:
|
|
247
|
+
lines.append(f"pub const {macro}: u8 = {value};")
|
|
248
|
+
else:
|
|
249
|
+
lines.append(f"pub const {macro}: &str = {value};")
|
|
250
|
+
if a.port is not None:
|
|
251
|
+
lines.append(f"pub const {macro}_PORT: char = '{a.port}';")
|
|
252
|
+
lines.append(f"pub const {macro}_BIT: u8 = {a.bit};")
|
|
253
|
+
if a.adc_channel is not None and a.function == "ADC":
|
|
254
|
+
lines.append(f"pub const {macro}_ADC: u8 = {a.adc_channel};")
|
|
255
|
+
for alias, target in aliases:
|
|
256
|
+
lines.append(f"pub const {alias}: u8 = {target};")
|
|
257
|
+
return "\n".join(lines) + "\n"
|
|
258
|
+
cpp = style == "cpp"
|
|
259
|
+
guard = "TRACEFORGE_PINS_HPP" if cpp else "TRACEFORGE_PINS_H"
|
|
260
|
+
lines = [f"/* {note} */", f"/* MCU {pm.mcu.name} ({pm.ref}); pin data: {pm.mcu.source} */"]
|
|
261
|
+
lines += [f"#ifndef {guard}", f"#define {guard}", ""]
|
|
262
|
+
if pm.mcu.family == "avr":
|
|
263
|
+
lines += ["#include <avr/io.h>", ""]
|
|
264
|
+
for macro, value, a in entries:
|
|
265
|
+
lines.append(f"/* {a.net}: {pm.ref} pin {a.pin} = {a.pin_name}, {a.function} */")
|
|
266
|
+
if cpp:
|
|
267
|
+
lines.append(f"constexpr int {macro} = {value};")
|
|
268
|
+
else:
|
|
269
|
+
lines.append(f"#define {macro} {value}")
|
|
270
|
+
extras: list[tuple[str, str]] = []
|
|
271
|
+
if a.port is not None:
|
|
272
|
+
extras.append((f"{macro}_BIT", str(a.bit)))
|
|
273
|
+
if pm.mcu.family == "avr":
|
|
274
|
+
extras += [
|
|
275
|
+
(f"{macro}_DDR", f"DDR{a.port}"),
|
|
276
|
+
(f"{macro}_PORT", f"PORT{a.port}"),
|
|
277
|
+
(f"{macro}_PINREG", f"PIN{a.port}"),
|
|
278
|
+
]
|
|
279
|
+
else:
|
|
280
|
+
extras += [(f"{macro}_PORT", f"'{a.port}'"), (f"{macro}_PIN", str(a.bit))]
|
|
281
|
+
if a.adc_channel is not None and a.function == "ADC":
|
|
282
|
+
extras.append((f"{macro}_ADC", str(a.adc_channel)))
|
|
283
|
+
for name, val in extras:
|
|
284
|
+
if cpp and val.lstrip("-").isdigit():
|
|
285
|
+
lines.append(f"constexpr int {name} = {val};")
|
|
286
|
+
else:
|
|
287
|
+
lines.append(f"#define {name} {val}")
|
|
288
|
+
if aliases:
|
|
289
|
+
lines.append("")
|
|
290
|
+
for alias, target in aliases:
|
|
291
|
+
lines.append(f"constexpr int {alias} = {target};" if cpp else f"#define {alias} {target}")
|
|
292
|
+
lines += ["", f"#endif /* {guard} */", ""]
|
|
293
|
+
return "\n".join(lines)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
HEADER_FILES = {"c": "pins.h", "cpp": "pins.hpp", "rust": "board_pins.rs"}
|