From 18d54fd1e7bed3d5087a9dc2003b4e1ba54c5b80 Mon Sep 17 00:00:00 2001 From: seveibar Date: Thu, 30 Jul 2026 11:52:21 -0700 Subject: [PATCH] Add RK3326 bus and power-plane fanout --- README.md | 50 +++ bun.lock | 16 +- index.circuit.tsx | 15 +- index.tsx | 24 ++ package.json | 2 +- scripts/validate-rk3326.mjs | 122 +++++++- src/RK3326Breakout.tsx | 592 ++++++++++++++++++++++++++++++++++++ tests/rk3326.test.ts | 127 ++++++++ 8 files changed, 924 insertions(+), 24 deletions(-) create mode 100644 src/RK3326Breakout.tsx diff --git a/README.md b/README.md index 5fb2c01..5959e4b 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,56 @@ Every populated ball is available as a selector such as `.U1 > .A1` or `.U1 > .AA21`. The full Rev 1.4 pin-function table is included, so functional selectors such as `.U1 > .USB_OTG_DP` and `.U1 > .GPIO0_A0` also work. +### Initial bus fanout + +`RK3326Breakout` places the chip inside tscircuit's native ``. Named +`` groups route signals to 122 stable boundary handoff pads: + +- DDR data/masks/strobes on the left +- DDR command/address/clock on the top +- eMMC, SDMMC0, SDMMC1, and USB OTG on the right +- MIPI DSI/LVDS transmit on the top +- MIPI CSI receive on the right + +The default build starts with 25 representative eMMC, USB OTG, and MIPI DSI +signals. SDMMC0/1, MIPI CSI, DDR data, and DDR command/address are mapped as +opt-in follow-on groups so the fanout can be validated in deterministic stages. + +All 205 supply balls are included in every stage: + +- 154 ground balls terminate on `inner1` +- 51 power balls terminate on `inner2` +- replicated CPU, logic, and DDR supply pins share their named rail +- unrelated I/O, PLL, MIPI, USB, ADC, OTP, and PMU rails remain electrically + distinct + +The power rails share a routing layer, not a net. A downstream board design must +create the appropriate isolated copper regions for the 23 named power nets on +`inner2`. + +```tsx +import { RK3326Breakout } from "@tsci/tscircuit.rk3326" + +export default () => ( + + + +) +``` + +The exported `RK3326_FANOUT_BUSES` metadata can be used to build connectors or +downstream routing one interface at a time. Pass a subset with +`buses={["emmc", "usb-otg"]}` when iterating on a smaller escape problem, or +add `"ddr-data"` and `"ddr-command-address"` for the DDR stage. + +The fanout phase reserves `inner1` and `inner2` for the supply planes. Signal +escapes default to `top`, `inner3`, `inner4`, and `bottom`; a deeper stack can +override those with `signalFanoutLayers`. + +The included 0.09 mm traces and 0.25/0.10 mm via pad/hole are preliminary HDI +constraints for the 0.65 mm-pitch BGA. Confirm the microvia stack and +fabrication tolerances with the board manufacturer before production. + `RK3326_SUPPLIER_PART_NUMBERS` exports the known LCSC catalog reference separately. It is not attached to the component by default because the tscircuit/JLCPCB importer does not currently return a usable footprint for diff --git a/bun.lock b/bun.lock index 42241d2..6862c62 100644 --- a/bun.lock +++ b/bun.lock @@ -7,7 +7,7 @@ "devDependencies": { "@types/bun": "^1.3.14", "@types/react": "^19.2.17", - "tscircuit": "^0.0.2177", + "tscircuit": "0.0.2180", "typescript": "^5.0.0", }, }, @@ -159,17 +159,17 @@ "@tscircuit/checks": ["@tscircuit/checks@0.0.146", "", { "peerDependencies": { "@flatten-js/core": "*", "@tscircuit/math-utils": "*", "circuit-json": "*", "circuit-json-to-connectivity-map": "*", "typescript": "^5.5.3" } }, "sha512-zQOCjeLoSvq/my1ojSIkG3SHWYxZC0qq5MFocd2Y90Li/gvHVpEu+uBPMsBdy9Jb7tO16+UJOjmM5AKqrWYXpQ=="], - "@tscircuit/circuit-json-util": ["@tscircuit/circuit-json-util@0.0.101", "", { "dependencies": { "parsel-js": "^1.1.2" }, "peerDependencies": { "circuit-json": "*", "transformation-matrix": "*", "zod": "3" } }, "sha512-sYdBri1kkV2BiOTE9KOvyYs7g8PxhQKXOp9HxzMwsYB0NQXcvnNU2M0eLvaLD6P7gKtQEM0tFGTZvilypoTk3g=="], + "@tscircuit/circuit-json-util": ["@tscircuit/circuit-json-util@0.0.102", "", { "dependencies": { "parsel-js": "^1.1.2" }, "peerDependencies": { "circuit-json": "*", "transformation-matrix": "*", "zod": "3" } }, "sha512-qn9L7bcVmDFGXUMb5NKaegdotJ4GiKVgzF8nA3gwBFauOEfumDjK/80AR6BnabsfuavvjV/KuQZHXNeaeNL4QQ=="], - "@tscircuit/cli": ["@tscircuit/cli@0.1.1774", "", { "peerDependencies": { "circuit-json": "^0.0.446", "tscircuit": "*" }, "bin": { "tscircuit-cli": "cli/entrypoint.js" } }, "sha512-qEiaGTtzQkZvXTYiJ3U0G61UFFQC5qhZwBVYBlK9NHj0FXH0SH/RuG191sRylYjG9u0Oj/8r6hLq5PjjQw750Q=="], + "@tscircuit/cli": ["@tscircuit/cli@0.1.1777", "", { "peerDependencies": { "circuit-json": "^0.0.446", "tscircuit": "*" }, "bin": { "tscircuit-cli": "cli/entrypoint.js" } }, "sha512-nz50RlrUUFY+6C+AN6rfmnrOOaLXoOBomquYUrUzl+j05HrhvQJWN8XxSUSsahyIZOACKjIEo2s3LXein/qcnQ=="], "@tscircuit/copper-pour-solver": ["@tscircuit/copper-pour-solver@0.0.42", "", { "dependencies": { "@tscircuit/manifold-2d": "^0.0.6" }, "peerDependencies": { "typescript": "^5" } }, "sha512-FHsX32w/8upaPad+SkFe82JEWKCuRLzduDxJAZITPL9IdX9v/uMXiKGp2DQgoQ9kBgRHxyRcoRiaC6qCoEaq6Q=="], - "@tscircuit/core": ["@tscircuit/core@0.0.1546", "", { "dependencies": { "@flatten-js/core": "^1.6.2", "@lume/kiwi": "^0.4.3", "calculate-cell-boundaries": "^0.0.13", "calculate-packing": "^0.0.82", "css-select": "5.1.0", "format-si-unit": "^0.0.7", "nanoid": "^5.0.7", "performance-now": "^2.1.0", "react-reconciler": "^0.32.0", "svg-path-commander": "^2.1.11", "transformation-matrix": "^2.16.1", "zod": "^3.25.67" }, "peerDependencies": { "@tscircuit/capacity-autorouter": "*", "@tscircuit/checks": "*", "@tscircuit/circuit-json-util": "*", "@tscircuit/create-fdm-enclosure": "*", "@tscircuit/footprinter": "*", "@tscircuit/infgrid-ijump-astar": "*", "@tscircuit/matchpack": "*", "@tscircuit/math-utils": "*", "@tscircuit/props": "*", "@tscircuit/schematic-match-adapt": "*", "bpc-graph": "*", "circuit-json": "*", "circuit-json-to-bpc": "*", "circuit-json-to-connectivity-map": "*", "circuit-json-to-spice": "*", "schematic-symbols": "*", "spicets": "*", "typescript": "^5.0.0" } }, "sha512-D7h/oDzOI31rdzkXANbgYnIuj1xG9bkrOd1BlX/JW/X8QJGzDMBVVaZC1PEpChB75bi0ALRZX5/if24vTT6Hvw=="], + "@tscircuit/core": ["@tscircuit/core@0.0.1549", "", { "dependencies": { "@flatten-js/core": "^1.6.2", "@lume/kiwi": "^0.4.3", "calculate-cell-boundaries": "^0.0.13", "calculate-packing": "^0.0.82", "css-select": "5.1.0", "format-si-unit": "^0.0.7", "nanoid": "^5.0.7", "performance-now": "^2.1.0", "react-reconciler": "^0.32.0", "svg-path-commander": "^2.1.11", "transformation-matrix": "^2.16.1", "zod": "^3.25.67" }, "peerDependencies": { "@tscircuit/capacity-autorouter": "*", "@tscircuit/checks": "*", "@tscircuit/circuit-json-util": "*", "@tscircuit/create-fdm-enclosure": "*", "@tscircuit/footprinter": "*", "@tscircuit/infgrid-ijump-astar": "*", "@tscircuit/matchpack": "*", "@tscircuit/math-utils": "*", "@tscircuit/props": "*", "@tscircuit/schematic-match-adapt": "*", "bpc-graph": "*", "circuit-json": "*", "circuit-json-to-bpc": "*", "circuit-json-to-connectivity-map": "*", "circuit-json-to-spice": "*", "schematic-symbols": "*", "spicets": "*", "typescript": "^5.0.0" } }, "sha512-bh8TehXW/WdPKrTx0ZacNkOTruIRZRb+M9adt42NOhoTKieYgfzHNYWlsjtBoJLk846ZyAeEvxU1nT4YIcbNGw=="], "@tscircuit/create-fdm-enclosure": ["@tscircuit/create-fdm-enclosure@0.0.3", "", { "dependencies": { "@tscircuit/solver-utils": "^0.0.19", "graphics-debug": "^0.0.96", "jscad-planner": "^0.0.14" }, "peerDependencies": { "typescript": "^5" } }, "sha512-uEjUz/8mH5GZ7wHOtNE/vk0Jisn1AeXmLWQ1kuGHsl8ndbjH11+gBnqMrOjEIFDzMChJFOIlIcYpFYRbG0nj/A=="], - "@tscircuit/eval": ["@tscircuit/eval@0.0.1092", "", { "peerDependencies": { "@tscircuit/core": "*", "circuit-json": "*", "typescript": "^5.0.0", "zod": "3" } }, "sha512-0Cmvwr+SuCJCySpqzBC9LijT6T9GhNaKG+aH3pAFQkrHzaOxBZTsQASWRoeD2KjOoZ07k+luHi8slNMjBf/iGQ=="], + "@tscircuit/eval": ["@tscircuit/eval@0.0.1095", "", { "peerDependencies": { "@tscircuit/core": "*", "circuit-json": "*", "typescript": "^5.0.0", "zod": "3" } }, "sha512-9QtfHK6UDsiLuPKq30LQs1JIc7703Ppnb3p+829KJRUDe+nz1I5lnmjKunKChgLJCC3ESnBGIcmI2NYG+ttcQQ=="], "@tscircuit/fanout-solver": ["@tscircuit/fanout-solver@0.0.10", "", { "dependencies": { "@tscircuit/capacity-autorouter": "0.0.718", "@tscircuit/solver-utils": "0.0.19", "graphics-debug": "0.0.96" }, "peerDependencies": { "typescript": "^5" } }, "sha512-JqHpS0Uflu7vZeSAO7MLHRsuEfAxwSPAyMiFCcaRGGDJsQZRSJBIevKmQDR23FHmFAdWzAFoCaEjHkx714DNyg=="], @@ -199,7 +199,7 @@ "@tscircuit/props": ["@tscircuit/props@0.0.598", "", { "peerDependencies": { "circuit-json": "*", "react": "*", "zod": "*" } }, "sha512-4hH1SMIkS8ukJ8t3+qjzHT3cJxLGAjpOofZIvcIIdpOJ5NkJpfK7CyTMOba9uzXJSSIveI+ZpsJXsZMX/JAB3A=="], - "@tscircuit/runframe": ["@tscircuit/runframe@0.0.2311", "", { "dependencies": { "@tscircuit/eval": "^0.0.1092", "@tscircuit/solver-utils": "^0.0.7", "debug": "^4.4.0" } }, "sha512-TlvNj3t0EtQUFC9xxknA4HptVNKVCbNlQTLTGxl3fBETHjZq6goxKhpp2u7A/ZHX/L+00TW3xEjgbw5B0WsNqw=="], + "@tscircuit/runframe": ["@tscircuit/runframe@0.0.2314", "", { "dependencies": { "@tscircuit/eval": "^0.0.1095", "@tscircuit/solver-utils": "^0.0.7", "debug": "^4.4.0" } }, "sha512-I0ijoPCKKvTu4arsNwg6eBLanYAgkXJnD2O2yTiXAr8DUfmNZDsM3NTng8Xn42WSAI/TRmZxezoTXQYY8EAX9g=="], "@tscircuit/schematic-corpus": ["@tscircuit/schematic-corpus@0.0.114", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-jDQX+XDXP6PklB39X8AwUSuiqGRdn78esy1wD/qG8U3cCXkT5rtE3ousDVaKRxclCeED8FxJlD+ZvRtAe8mw8w=="], @@ -427,7 +427,7 @@ "kicad-to-circuit-json": ["kicad-to-circuit-json@0.0.117", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-JXIGIix7t39kcpqFEQP9m/eWqN/2MqeCFME628xbhrXRu8nNBSGD54vozKNh9GsFVqIpzGxciS6SGVxWrA+osw=="], - "kicadts": ["kicadts@0.0.51", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-QQjMRad5zcfvoj8+8UI11bc5vLvboru9jbNYvsJ/9e35QyFJ5x3DmyJCDJp7GE8s7UdUmwct5QRT13VbSCbLOA=="], + "kicadts": ["kicadts@0.0.53", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-qXBtGBc11LhG93dF4iXlVeWbVjk5UUmeUANpP+cgvbRxZa+w7p2UvSCK7/4uv7KzGKhxzSJouEPSAZdastlXag=="], "kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], @@ -593,7 +593,7 @@ "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], - "tscircuit": ["tscircuit@0.0.2177", "", { "dependencies": { "@flatten-js/core": "^1.6.2", "@lume/kiwi": "^0.4.3", "@resvg/resvg-js": "^2.6.2", "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-typescript": "^12.3.0", "@tscircuit/alphabet": "0.0.25", "@tscircuit/capacity-autorouter": "^0.0.722", "@tscircuit/checks": "0.0.146", "@tscircuit/circuit-json-util": "^0.0.101", "@tscircuit/cli": "^0.1.1774", "@tscircuit/copper-pour-solver": "0.0.42", "@tscircuit/core": "^0.0.1546", "@tscircuit/create-fdm-enclosure": "0.0.3", "@tscircuit/eval": "^0.0.1092", "@tscircuit/fanout-solver": "0.0.10", "@tscircuit/footprinter": "^0.0.387", "@tscircuit/image-utils": "^0.0.8", "@tscircuit/infer-cable-insertion-point": "^0.0.2", "@tscircuit/infgrid-ijump-astar": "^0.0.35", "@tscircuit/internal-dynamic-import": "^0.0.11", "@tscircuit/krt-wasm": "^0.1.1", "@tscircuit/matchpack": "^0.0.60", "@tscircuit/math-utils": "^0.0.36", "@tscircuit/miniflex": "^0.0.4", "@tscircuit/ngspice-spice-engine": "^0.0.20", "@tscircuit/props": "^0.0.598", "@tscircuit/runframe": "^0.0.2311", "@tscircuit/schematic-match-adapt": "^0.0.18", "@tscircuit/schematic-trace-solver": "^0.0.115", "@tscircuit/simple-3d-svg": "^0.0.41", "@tscircuit/solver-utils": "^0.0.16", "@tscircuit/soup-util": "^0.0.41", "bpc-graph": "^0.0.57", "calculate-cell-boundaries": "^0.0.13", "calculate-elbow": "^0.0.12", "calculate-packing": "^0.0.82", "circuit-json": "^0.0.455", "circuit-json-to-bpc": "^0.0.13", "circuit-json-to-connectivity-map": "^0.0.23", "circuit-json-to-gltf": "^0.0.107", "circuit-json-to-pnp-csv": "^0.0.8", "circuit-json-to-simple-3d": "^0.0.9", "circuit-json-to-spice": "^0.0.45", "circuit-to-svg": "^0.0.393", "comlink": "^4.4.2", "connectivity-map": "^1.0.0", "css-select": "5.1.0", "debug": "^4.3.6", "flatbush": "^4.5.0", "format-si-unit": "^0.0.7", "graphics-debug": "^0.0.95", "jscad-planner": "^0.0.13", "kicad-component-converter": "^0.1.40", "kicad-to-circuit-json": "^0.0.117", "kicadts": "^0.0.51", "minicssgrid": "^0.0.9", "performance-now": "^2.1.0", "poppygl": "^0.0.26", "react": "^19.1.0", "react-dom": "^19.1.0", "rollup": "^4.53.2", "rollup-plugin-dts": "^6.2.3", "s-expression": "^3.1.1", "schematic-symbols": "^0.0.238", "spicets": "^0.0.4", "spicey": "^0.0.14", "sucrase": "^3.35.0", "svg-path-commander": "^2.1.11", "transformation-matrix": "^2.16.1", "tslib": "^2.8.1", "zod": "^3.25.67" }, "peerDependencies": { "typescript": "^5.0.0" }, "bin": { "tsci": "cli.mjs", "tscircuit": "cli.mjs" } }, "sha512-oVM83ty4pFZIIwNMsyGhwQ2tGl0ju583tGGc73Rw3CLC6+JUcUMDdt7svR2K9ksMmMkjqtlflk2C6XhAwE7ONA=="], + "tscircuit": ["tscircuit@0.0.2180", "", { "dependencies": { "@flatten-js/core": "^1.6.2", "@lume/kiwi": "^0.4.3", "@resvg/resvg-js": "^2.6.2", "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-typescript": "^12.3.0", "@tscircuit/alphabet": "0.0.25", "@tscircuit/capacity-autorouter": "^0.0.722", "@tscircuit/checks": "0.0.146", "@tscircuit/circuit-json-util": "^0.0.102", "@tscircuit/cli": "^0.1.1777", "@tscircuit/copper-pour-solver": "0.0.42", "@tscircuit/core": "^0.0.1549", "@tscircuit/create-fdm-enclosure": "0.0.3", "@tscircuit/eval": "^0.0.1095", "@tscircuit/fanout-solver": "0.0.10", "@tscircuit/footprinter": "^0.0.387", "@tscircuit/image-utils": "^0.0.8", "@tscircuit/infer-cable-insertion-point": "^0.0.2", "@tscircuit/infgrid-ijump-astar": "^0.0.35", "@tscircuit/internal-dynamic-import": "^0.0.11", "@tscircuit/krt-wasm": "^0.1.1", "@tscircuit/matchpack": "^0.0.60", "@tscircuit/math-utils": "^0.0.36", "@tscircuit/miniflex": "^0.0.4", "@tscircuit/ngspice-spice-engine": "^0.0.20", "@tscircuit/props": "^0.0.598", "@tscircuit/runframe": "^0.0.2314", "@tscircuit/schematic-match-adapt": "^0.0.18", "@tscircuit/schematic-trace-solver": "^0.0.115", "@tscircuit/simple-3d-svg": "^0.0.41", "@tscircuit/solver-utils": "^0.0.16", "@tscircuit/soup-util": "^0.0.41", "bpc-graph": "^0.0.57", "calculate-cell-boundaries": "^0.0.13", "calculate-elbow": "^0.0.12", "calculate-packing": "^0.0.82", "circuit-json": "^0.0.455", "circuit-json-to-bpc": "^0.0.13", "circuit-json-to-connectivity-map": "^0.0.23", "circuit-json-to-gltf": "^0.0.107", "circuit-json-to-pnp-csv": "^0.0.8", "circuit-json-to-simple-3d": "^0.0.9", "circuit-json-to-spice": "^0.0.45", "circuit-to-svg": "^0.0.393", "comlink": "^4.4.2", "connectivity-map": "^1.0.0", "css-select": "5.1.0", "debug": "^4.3.6", "flatbush": "^4.5.0", "format-si-unit": "^0.0.7", "graphics-debug": "^0.0.95", "jscad-planner": "^0.0.13", "kicad-component-converter": "^0.1.40", "kicad-to-circuit-json": "^0.0.117", "kicadts": "^0.0.53", "minicssgrid": "^0.0.9", "performance-now": "^2.1.0", "poppygl": "^0.0.26", "react": "^19.1.0", "react-dom": "^19.1.0", "rollup": "^4.53.2", "rollup-plugin-dts": "^6.2.3", "s-expression": "^3.1.1", "schematic-symbols": "^0.0.238", "spicets": "^0.0.4", "spicey": "^0.0.14", "sucrase": "^3.35.0", "svg-path-commander": "^2.1.11", "transformation-matrix": "^2.16.1", "tslib": "^2.8.1", "zod": "^3.25.67" }, "peerDependencies": { "typescript": "^5.0.0" }, "bin": { "tsci": "cli.mjs", "tscircuit": "cli.mjs" } }, "sha512-332krsPi/CzFnUjYfN6lNC6kwk4hHJ7MmVNeEkjRtLer0GvMP7j4ELh6gV5GERhetuYMeguYVaSVwoYm0cxpMw=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], diff --git a/index.circuit.tsx b/index.circuit.tsx index 59874cd..6fd4f18 100644 --- a/index.circuit.tsx +++ b/index.circuit.tsx @@ -1,7 +1,16 @@ -import { RK3326 } from "./src/RK3326" +import { RK3326Breakout } from "./src/RK3326Breakout" export default () => ( - - + + ) diff --git a/index.tsx b/index.tsx index 83bc77d..b691321 100644 --- a/index.tsx +++ b/index.tsx @@ -7,3 +7,27 @@ export { RK3326_SUPPLIER_PART_NUMBERS, type RK3326Props, } from "./src/RK3326" + +export { + RK3326Breakout, + RK3326_BREAKOUT_GEOMETRY, + RK3326_BREAKOUT_RULES, + RK3326_DEFAULT_SIGNAL_FANOUT_LAYERS, + RK3326_FANOUT_BUSES, + RK3326_FANOUT_BUS_IDS, + RK3326_GROUND_PLANE_LAYER, + RK3326_INITIAL_FANOUT_BUS_IDS, + RK3326_PLANE_FANOUT_CONNECTIONS, + RK3326_POWER_PLANE_LAYER, + RK3326_SUPPLY_PINS, + getRK3326FanoutBreakoutPoints, + getRK3326PlaneFanoutConnections, + type RK3326BreakoutProps, + type RK3326FanoutDirection, + type RK3326FanoutBreakoutPoint, + type RK3326FanoutBus, + type RK3326FanoutBusId, + type RK3326FanoutSide, + type RK3326PlaneFanoutConnection, + type RK3326SignalFanoutLayer, +} from "./src/RK3326Breakout" diff --git a/package.json b/package.json index d478689..d38d393 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "devDependencies": { "@types/bun": "^1.3.14", "@types/react": "^19.2.17", - "tscircuit": "^0.0.2177", + "tscircuit": "0.0.2180", "typescript": "^5.0.0" } } diff --git a/scripts/validate-rk3326.mjs b/scripts/validate-rk3326.mjs index 261a85c..10b2e59 100644 --- a/scripts/validate-rk3326.mjs +++ b/scripts/validate-rk3326.mjs @@ -10,9 +10,49 @@ const pinFunctions = JSON.parse( ), ) -const pads = circuitJson.filter((element) => element.type === "pcb_smtpad") -const padByBall = new Map( - pads.map((pad) => [pad.port_hints?.[0], pad]), +const rk3326SourceComponent = circuitJson.find( + (element) => + element.type === "source_component" && + element.manufacturer_part_number === "RK3326", +) +const rk3326PcbComponent = circuitJson.find( + (element) => + element.type === "pcb_component" && + element.source_component_id === rk3326SourceComponent?.source_component_id, +) +const pads = circuitJson.filter( + (element) => + element.type === "pcb_smtpad" && + element.pcb_component_id === rk3326PcbComponent?.pcb_component_id, +) +const padByBall = new Map(pads.map((pad) => [pad.port_hints?.[0], pad])) +const sourceTraceById = new Map( + circuitJson + .filter((element) => element.type === "source_trace") + .map((trace) => [trace.source_trace_id, trace]), +) +const pcbTraceById = new Map( + circuitJson + .filter((element) => element.type === "pcb_trace") + .map((trace) => [trace.pcb_trace_id, trace]), +) +const planeSourceTraces = [...sourceTraceById.values()].filter((trace) => + trace.name?.startsWith("PLANE_TRACE_"), +) +const planePcbTraces = [...pcbTraceById.values()].filter((trace) => + sourceTraceById.get(trace.source_trace_id)?.name?.startsWith("PLANE_TRACE_"), +) +const planeVias = circuitJson.filter( + (element) => + element.type === "pcb_via" && + sourceTraceById + .get(pcbTraceById.get(element.pcb_trace_id)?.source_trace_id) + ?.name?.startsWith("PLANE_TRACE_"), +) +const fanoutSignalPcbTraces = [...pcbTraceById.values()].filter( + (trace) => + trace.pcb_trace_id.startsWith("fanout:") && + sourceTraceById.get(trace.source_trace_id)?.name?.startsWith("BUS_"), ) const missingBalls = [ @@ -45,11 +85,10 @@ const assert = (condition, message) => { if (!condition) throw new Error(message) } +assert(rk3326SourceComponent, "RK3326 source component is missing") +assert(rk3326PcbComponent, "RK3326 PCB component is missing") assert(pads.length === 418, `Expected 418 pads, found ${pads.length}`) -assert( - new Set(padByBall.keys()).size === 418, - "Ball names are not unique", -) +assert(new Set(padByBall.keys()).size === 418, "Ball names are not unique") assert( Object.keys(pinFunctions).length === 418, `Expected 418 pin functions, found ${Object.keys(pinFunctions).length}`, @@ -70,13 +109,72 @@ const aa21 = padByBall.get("AA21") assert(a1 && a2 && aa21, "Corner/reference balls are missing") assert(a1.x === -6.5 && a1.y === 6.5, "A1 is not at the upper-left") assert(aa21.x === 6.5 && aa21.y === -6.5, "AA21 is not at the lower-right") -assert( - Math.abs(a2.x - a1.x - 0.65) < 1e-9, - "Horizontal pitch is not 0.65 mm", -) +assert(Math.abs(a2.x - a1.x - 0.65) < 1e-9, "Horizontal pitch is not 0.65 mm") assert( pads.every((pad) => pad.shape === "circle" && pad.radius === 0.15), "Pads are not 0.30 mm circles", ) -console.log("RK3326 footprint validation passed: 418 pads, 0.65 mm pitch") +const circuitErrors = circuitJson.filter((element) => + element.type.endsWith("_error"), +) +assert( + circuitErrors.length === 0, + `Expected no circuit errors, found ${circuitErrors.length}`, +) +assert( + planeSourceTraces.length === 205, + `Expected 205 supply-plane source traces, found ${planeSourceTraces.length}`, +) +assert( + planePcbTraces.length === 205, + `Expected 205 supply-plane PCB traces, found ${planePcbTraces.length}`, +) +assert( + planeVias.length === 205, + `Expected 205 supply-plane vias, found ${planeVias.length}`, +) + +const groundVias = planeVias.filter((via) => via.to_layer === "inner1") +const powerVias = planeVias.filter((via) => via.to_layer === "inner2") +assert( + groundVias.length === 154, + `Expected 154 ground drops to inner1, found ${groundVias.length}`, +) +assert( + powerVias.length === 51, + `Expected 51 power drops to inner2, found ${powerVias.length}`, +) +assert( + groundVias.every( + (via) => via.from_layer === "top" && via.layers.join(",") === "top,inner1", + ), + "At least one ground via has the wrong layer span", +) +assert( + powerVias.every( + (via) => + via.from_layer === "top" && via.layers.join(",") === "top,inner1,inner2", + ), + "At least one power via has the wrong layer span", +) + +const signalFanoutLayers = new Set( + fanoutSignalPcbTraces.flatMap((trace) => + trace.route.flatMap((point) => + point.route_type === "wire" + ? [point.layer] + : point.route_type === "via" + ? [point.from_layer, point.to_layer] + : [], + ), + ), +) +assert( + !signalFanoutLayers.has("inner1") && !signalFanoutLayers.has("inner2"), + "Signal fanout uses a reserved supply-plane layer", +) + +console.log( + "RK3326 validation passed: 418 pads; 154 ground drops to inner1; 51 power drops to inner2", +) diff --git a/src/RK3326Breakout.tsx b/src/RK3326Breakout.tsx new file mode 100644 index 0000000..9064cf1 --- /dev/null +++ b/src/RK3326Breakout.tsx @@ -0,0 +1,592 @@ +import type { BreakoutProps } from "@tscircuit/props" +import { Fragment } from "react" +import { + RK3326, + RK3326_BALLS, + RK3326_PACKAGE, + RK3326_PIN_FUNCTIONS, +} from "./RK3326" + +export const RK3326_BREAKOUT_GEOMETRY = { + sizeMm: 36, + paddingMm: 11, + boundaryPointInsetMm: 0.0001, + usableHalfSpanMm: 17, +} as const + +/** + * Preliminary HDI rules for escaping the 0.65 mm-pitch BGA. + * + * These are routing constraints, not a claim that every board house supports + * the resulting stackup. Confirm the microvia structure and annular-ring rules + * with the selected fabricator before production. + */ +export const RK3326_BREAKOUT_RULES = { + defaultTraceWidth: 0.09, + minTraceWidth: 0.09, + minTraceToPadEdgeClearance: 0.075, + minViaPadDiameter: 0.25, + minViaHoleDiameter: 0.1, + minViaEdgeToPadEdgeClearance: 0.075, + minViaHoleEdgeToViaHoleEdgeClearance: 0.1, +} as const + +export const RK3326_FANOUT_BUS_IDS = [ + "ddr-data", + "ddr-command-address", + "emmc", + "sdmmc0", + "sdmmc1", + "usb-otg", + "mipi-dsi", + "mipi-csi", +] as const + +export type RK3326FanoutBusId = (typeof RK3326_FANOUT_BUS_IDS)[number] +export type RK3326FanoutSide = "top" | "right" | "bottom" | "left" +export type RK3326FanoutDirection = "left" | "right" | "up" | "down" +export type RK3326SignalFanoutLayer = + | "top" + | "inner3" + | "inner4" + | "inner5" + | "inner6" + | "inner7" + | "inner8" + | "bottom" + +export const RK3326_GROUND_PLANE_LAYER = "inner1" as const +export const RK3326_POWER_PLANE_LAYER = "inner2" as const +export const RK3326_DEFAULT_SIGNAL_FANOUT_LAYERS = [ + "top", + "inner3", + "inner4", + "bottom", +] as const satisfies readonly RK3326SignalFanoutLayer[] + +/** + * The interfaces routed by the default, buildable first fanout stage. + * DDR is mapped and available as an opt-in follow-on stage. + */ +export const RK3326_INITIAL_FANOUT_BUS_IDS = [ + "emmc", + "usb-otg", + "mipi-dsi", +] as const satisfies readonly RK3326FanoutBusId[] + +export interface RK3326FanoutBus { + id: RK3326FanoutBusId + label: string + side: RK3326FanoutSide + balls: readonly string[] +} + +const getBallFunctions = (ball: string) => + RK3326_PIN_FUNCTIONS[ball].split("/").filter(Boolean) + +const getBallsMatching = (pattern: RegExp) => + RK3326_BALLS.filter((ball) => + getBallFunctions(ball).some((pinFunction) => pattern.test(pinFunction)), + ) + +const getMatchingFunction = (ball: string, pattern: RegExp) => + getBallFunctions(ball).find((pinFunction) => pattern.test(pinFunction)) ?? + ball + +const sortByMatchingFunction = (balls: readonly string[], pattern: RegExp) => + [...balls].sort((ballA, ballB) => + getMatchingFunction(ballA, pattern).localeCompare( + getMatchingFunction(ballB, pattern), + undefined, + { numeric: true }, + ), + ) + +const ddrDataPattern = /^DDR_(?:DQ\d+|DQS\d+_[NP]|DM\d+|D_[Mm]\d+)$/ +const ddrCommandAddressPattern = /^DDR(?:3|4)_/ +const emmcPattern = /^EMMC_/ +const sdmmc0Pattern = /^SDMMC0_/ +const sdmmc1Pattern = /^SDMMC1_/ +const usbOtgPattern = /^(?:USB_OTG_D[PM]|USB_ID|USB_VBUS)$/ +const mipiDsiPattern = /^MIPI_TX_(?:D[0-3][PN]|CLK[PN])$/ +const mipiCsiPattern = /^MIPI_CSI_(?:D[PN][0-3]|CLK[PN])$/ + +const busFunctionPatterns: Record = { + "ddr-data": ddrDataPattern, + "ddr-command-address": ddrCommandAddressPattern, + emmc: emmcPattern, + sdmmc0: sdmmc0Pattern, + sdmmc1: sdmmc1Pattern, + "usb-otg": usbOtgPattern, + "mipi-dsi": mipiDsiPattern, + "mipi-csi": mipiCsiPattern, +} + +export const RK3326_FANOUT_BUSES: readonly RK3326FanoutBus[] = [ + { + id: "ddr-data", + label: "DDR data, masks, and strobes", + side: "left", + balls: sortByMatchingFunction( + getBallsMatching(ddrDataPattern), + ddrDataPattern, + ), + }, + { + id: "ddr-command-address", + label: "DDR command, address, and clock", + side: "top", + balls: sortByMatchingFunction( + getBallsMatching(ddrCommandAddressPattern), + ddrCommandAddressPattern, + ), + }, + { + id: "emmc", + label: "eMMC", + side: "right", + balls: sortByMatchingFunction(getBallsMatching(emmcPattern), emmcPattern), + }, + { + id: "sdmmc0", + label: "SDMMC0", + side: "right", + balls: sortByMatchingFunction( + getBallsMatching(sdmmc0Pattern), + sdmmc0Pattern, + ), + }, + { + id: "sdmmc1", + label: "SDMMC1", + side: "right", + balls: sortByMatchingFunction( + getBallsMatching(sdmmc1Pattern), + sdmmc1Pattern, + ), + }, + { + id: "usb-otg", + label: "USB OTG", + side: "right", + balls: sortByMatchingFunction( + getBallsMatching(usbOtgPattern), + usbOtgPattern, + ), + }, + { + id: "mipi-dsi", + label: "MIPI DSI / LVDS transmit", + side: "top", + balls: sortByMatchingFunction( + getBallsMatching(mipiDsiPattern), + mipiDsiPattern, + ), + }, + { + id: "mipi-csi", + label: "MIPI CSI receive", + side: "right", + balls: sortByMatchingFunction( + getBallsMatching(mipiCsiPattern), + mipiCsiPattern, + ), + }, +] + +export interface RK3326FanoutBreakoutPoint { + ball: string + busId: RK3326FanoutBusId + endpointName: string + netName: string + traceName: string + side: RK3326FanoutSide + pcbX: number + pcbY: number +} + +export interface RK3326PlaneFanoutConnection { + ball: string + category: "ground" | "power" + pinFunction: string + netName: string + traceName: string + busName: string + layer: typeof RK3326_GROUND_PLANE_LAYER | typeof RK3326_POWER_PLANE_LAYER + direction: RK3326FanoutDirection +} + +const getBreakoutNetName = (busId: RK3326FanoutBusId, ball: string) => { + const pinFunction = getMatchingFunction(ball, busFunctionPatterns[busId]) + const normalizedFunction = pinFunction + .replace(/[^a-zA-Z0-9]+/g, "_") + .replace(/^_+|_+$/g, "") + .toUpperCase() + + return `RK3326_${normalizedFunction}` +} + +const getPointPosition = ( + side: RK3326FanoutSide, + index: number, + pointCount: number, +) => { + const { sizeMm, boundaryPointInsetMm, usableHalfSpanMm } = + RK3326_BREAKOUT_GEOMETRY + const boundaryCoordinate = sizeMm / 2 - boundaryPointInsetMm + const alongEdge = + pointCount === 1 + ? 0 + : -usableHalfSpanMm + (index * usableHalfSpanMm * 2) / (pointCount - 1) + + switch (side) { + case "top": + return { pcbX: alongEdge, pcbY: boundaryCoordinate } + case "right": + return { pcbX: boundaryCoordinate, pcbY: -alongEdge } + case "bottom": + return { pcbX: alongEdge, pcbY: -boundaryCoordinate } + case "left": + return { pcbX: -boundaryCoordinate, pcbY: -alongEdge } + } +} + +export const getRK3326FanoutBreakoutPoints = ( + selectedBusIds: readonly RK3326FanoutBusId[] = RK3326_FANOUT_BUS_IDS, +): RK3326FanoutBreakoutPoint[] => { + const selectedBusIdSet = new Set(selectedBusIds) + const selectedBuses = RK3326_FANOUT_BUSES.filter((bus) => + selectedBusIdSet.has(bus.id), + ) + const breakoutPoints: RK3326FanoutBreakoutPoint[] = [] + + for (const side of ["top", "right", "bottom", "left"] as const) { + const sidePoints = selectedBuses + .filter((bus) => bus.side === side) + .flatMap((bus) => + bus.balls.map((ball, signalIndex) => ({ + ball, + busId: bus.id, + signalIndex, + side, + })), + ) + + for (const [index, point] of sidePoints.entries()) { + const normalizedBusId = point.busId.replace(/-/g, "_").toUpperCase() + + breakoutPoints.push({ + ball: point.ball, + busId: point.busId, + endpointName: `FANOUT_${point.ball}`, + netName: getBreakoutNetName(point.busId, point.ball), + traceName: `BUS_${normalizedBusId}_${String( + point.signalIndex + 1, + ).padStart(2, "0")}`, + side: point.side, + ...getPointPosition(side, index, sidePoints.length), + }) + } + } + + return breakoutPoints +} + +const groundPinFunctionPattern = /^(?:VSS_\d+|AVSS|TVSS)$/ +const powerPinFunctionPattern = /(?:VDD|VCC|AVDD)|^PMUIO\d+$/ + +const getSupplyPin = (ball: string) => { + const pinFunction = getBallFunctions(ball).find( + (candidate) => + groundPinFunctionPattern.test(candidate) || + powerPinFunctionPattern.test(candidate), + ) + if (!pinFunction) return null + + const category = groundPinFunctionPattern.test(pinFunction) + ? ("ground" as const) + : ("power" as const) + const netName = + category === "ground" + ? "GND" + : pinFunction.replace(/^(CPU_VDD|LOGIC_VDD|DDRIO_VDD)_\d+$/, "$1") + + return { ball, category, pinFunction, netName } +} + +const getBallGridPosition = (ball: string) => { + const match = ball.match(/^([A-Z]+)(\d+)$/) + if (!match) throw new Error(`Invalid RK3326 ball name: ${ball}`) + + const row = RK3326_PACKAGE.rowNames.indexOf( + match[1] as (typeof RK3326_PACKAGE.rowNames)[number], + ) + if (row < 0) throw new Error(`Unknown RK3326 ball row: ${match[1]}`) + + return { row, column: Number(match[2]) - 1 } +} + +const getViaCornerKey = ( + ball: string, + direction: RK3326FanoutDirection, + handedness: -1 | 1, +) => { + const { row, column } = getBallGridPosition(ball) + const x2 = column * 2 + const y2 = row * 2 + + switch (direction) { + case "left": + return `${x2 - 1}:${y2 - handedness}` + case "right": + return `${x2 + 1}:${y2 - handedness}` + case "up": + return `${x2 + handedness}:${y2 - 1}` + case "down": + return `${x2 + handedness}:${y2 + 1}` + } +} + +const sideToDirection: Record = { + top: "up", + right: "right", + bottom: "down", + left: "left", +} + +const directionToAnchor = { + left: "center_left", + right: "center_right", + up: "top_center", + down: "bottom_center", +} as const satisfies Record + +const allocatePlaneFanoutDirections = ( + planeBalls: readonly string[], + selectedSignalBusIds: readonly RK3326FanoutBusId[], +): ReadonlyMap => { + const planeBallSet = new Set(planeBalls) + const selectedSignalBusIdSet = new Set(selectedSignalBusIds) + // The solver attempts positive handedness first for each signal bus. Reserve + // those preferred interstices while leaving enough alternatives for the + // dense DDR and supply populations to coexist. + const reservedSignalCorners = new Set( + RK3326_FANOUT_BUSES.filter((bus) => + selectedSignalBusIdSet.has(bus.id), + ).flatMap((bus) => + bus.balls.map((ball) => + getViaCornerKey(ball, sideToDirection[bus.side], 1), + ), + ), + ) + const cornerOwner = new Map() + const directionByBall = new Map() + + const tryAssign = (ball: string, visited: Set): boolean => { + if (visited.has(ball)) return false + visited.add(ball) + + const { row, column } = getBallGridPosition(ball) + const directionOrder = [ + "right", + "down", + "left", + "up", + ] as const satisfies readonly RK3326FanoutDirection[] + const offset = (column * 3 + row) % directionOrder.length + const candidates = directionOrder + .map( + (_, index) => directionOrder[(index + offset) % directionOrder.length]!, + ) + .filter( + (direction) => + !reservedSignalCorners.has(getViaCornerKey(ball, direction, 1)), + ) + + for (const direction of candidates) { + const cornerKey = getViaCornerKey(ball, direction, 1) + const currentOwner = cornerOwner.get(cornerKey) + if ( + currentOwner === undefined || + (planeBallSet.has(currentOwner) && tryAssign(currentOwner, visited)) + ) { + cornerOwner.set(cornerKey, ball) + directionByBall.set(ball, direction) + return true + } + } + + return false + } + + for (const ball of planeBalls) { + if (!tryAssign(ball, new Set())) { + throw new Error( + `RK3326 could not allocate a unique plane escape via for ${ball}`, + ) + } + } + + return directionByBall +} + +export const RK3326_SUPPLY_PINS = RK3326_BALLS.flatMap((ball) => { + const supplyPin = getSupplyPin(ball) + return supplyPin ? [supplyPin] : [] +}) + +export const getRK3326PlaneFanoutConnections = ( + selectedSignalBusIds: readonly RK3326FanoutBusId[] = RK3326_INITIAL_FANOUT_BUS_IDS, +): readonly RK3326PlaneFanoutConnection[] => { + const planeFanoutDirectionByBall = allocatePlaneFanoutDirections( + RK3326_SUPPLY_PINS.map((pin) => pin.ball), + selectedSignalBusIds, + ) + + return RK3326_SUPPLY_PINS.map(({ ball, category, pinFunction, netName }) => ({ + ball, + category, + pinFunction, + netName, + traceName: `PLANE_TRACE_${ball}`, + busName: `plane-${category}-${ball}`, + layer: + category === "ground" + ? RK3326_GROUND_PLANE_LAYER + : RK3326_POWER_PLANE_LAYER, + direction: planeFanoutDirectionByBall.get(ball)!, + })) +} + +export const RK3326_PLANE_FANOUT_CONNECTIONS = getRK3326PlaneFanoutConnections() + +type FixedBreakoutProps = + | "children" + | "autorouter" + | "width" + | "height" + | "pcbWidth" + | "pcbHeight" + | "padding" + | "paddingLeft" + | "paddingRight" + | "paddingTop" + | "paddingBottom" + | keyof typeof RK3326_BREAKOUT_RULES + +export interface RK3326BreakoutProps extends Omit< + BreakoutProps, + FixedBreakoutProps +> { + chipName?: string + buses?: readonly RK3326FanoutBusId[] + signalFanoutLayers?: readonly RK3326SignalFanoutLayer[] +} + +/** + * Initial RK3326 bus fanout. + * + * The breakout points are stable physical handoff locations for downstream + * signal routing. Every ground ball drops to inner1 and every power ball drops + * to its named rail on inner2. Signals are restricted to the remaining + * configured fanout layers. + */ +export const RK3326Breakout = ({ + chipName = "U1", + buses = RK3326_INITIAL_FANOUT_BUS_IDS, + signalFanoutLayers = RK3326_DEFAULT_SIGNAL_FANOUT_LAYERS, + name = "RK3326_BREAKOUT", + ...props +}: RK3326BreakoutProps) => { + const breakoutPoints = getRK3326FanoutBreakoutPoints(buses) + const planeFanoutConnections = getRK3326PlaneFanoutConnections(buses) + const selectedSignalBuses = RK3326_FANOUT_BUSES.filter((bus) => + buses.includes(bus.id), + ) + const busFanoutDirections = Object.fromEntries([ + ...selectedSignalBuses.map( + (bus) => [bus.id, directionToAnchor[sideToDirection[bus.side]]] as const, + ), + ...planeFanoutConnections.map( + (connection) => + [connection.busName, directionToAnchor[connection.direction]] as const, + ), + ]) + const { sizeMm, paddingMm } = RK3326_BREAKOUT_GEOMETRY + const breakoutEndpointFootprint = ( + + + + ) + + return ( + + + + {planeFanoutConnections.map((connection) => ( + + .${connection.ball}`} + to={`net.${connection.netName}`} + /> + + + ))} + {breakoutPoints.map( + ({ ball, busId, endpointName, netName, traceName, pcbX, pcbY }) => ( + + + .${ball}`} + to={`.${endpointName} > .1`} + /> + + ), + )} + {selectedSignalBuses.map((bus) => ( + + point.busId === bus.id) + .map((point) => point.traceName)} + /> + + ))} + + ) +} diff --git a/tests/rk3326.test.ts b/tests/rk3326.test.ts index 24bf491..b16086c 100644 --- a/tests/rk3326.test.ts +++ b/tests/rk3326.test.ts @@ -5,6 +5,17 @@ import { RK3326_PIN_FUNCTIONS, RK3326_PIN_LABELS, } from "../src/RK3326" +import { + getRK3326FanoutBreakoutPoints, + getRK3326PlaneFanoutConnections, + RK3326_DEFAULT_SIGNAL_FANOUT_LAYERS, + RK3326_FANOUT_BUSES, + RK3326_FANOUT_BUS_IDS, + RK3326_GROUND_PLANE_LAYER, + RK3326_INITIAL_FANOUT_BUS_IDS, + RK3326_POWER_PLANE_LAYER, + RK3326_SUPPLY_PINS, +} from "../src/RK3326Breakout" describe("RK3326 package", () => { test("contains the complete 418-ball population", () => { @@ -32,3 +43,119 @@ describe("RK3326 package", () => { expect(labels.some((aliases) => aliases.includes("GPIO0_A0"))).toBe(true) }) }) + +describe("RK3326 initial fanout", () => { + test("groups the selected high-speed and storage buses", () => { + const expectedBallCounts = { + "ddr-data": 44, + "ddr-command-address": 30, + emmc: 11, + sdmmc0: 7, + sdmmc1: 6, + "usb-otg": 4, + "mipi-dsi": 10, + "mipi-csi": 10, + } + + expect(RK3326_FANOUT_BUSES).toHaveLength(RK3326_FANOUT_BUS_IDS.length) + + for (const bus of RK3326_FANOUT_BUSES) { + expect(bus.balls).toHaveLength(expectedBallCounts[bus.id]) + } + }) + + test("creates one breakout point per selected bus ball", () => { + const breakoutPoints = getRK3326FanoutBreakoutPoints() + const balls = breakoutPoints.map((point) => point.ball) + const endpointNames = breakoutPoints.map((point) => point.endpointName) + const netNames = breakoutPoints.map((point) => point.netName) + const traceNames = breakoutPoints.map((point) => point.traceName) + + expect(breakoutPoints).toHaveLength(122) + expect(new Set(balls).size).toBe(122) + expect(new Set(endpointNames).size).toBe(122) + expect(new Set(netNames).size).toBe(122) + expect(new Set(traceNames).size).toBe(122) + + for (const ball of balls) { + expect(RK3326_BALLS).toContain(ball) + } + }) + + test("keeps the default fanout to a deterministic first stage", () => { + const initialPoints = getRK3326FanoutBreakoutPoints( + RK3326_INITIAL_FANOUT_BUS_IDS, + ) + + expect(RK3326_INITIAL_FANOUT_BUS_IDS).toEqual([ + "emmc", + "usb-otg", + "mipi-dsi", + ]) + expect(initialPoints).toHaveLength(25) + }) + + test("drops every supply ball into its assigned plane layer", () => { + const planeConnections = getRK3326PlaneFanoutConnections() + const allBusPlaneConnections = getRK3326PlaneFanoutConnections( + RK3326_FANOUT_BUS_IDS, + ) + const groundConnections = planeConnections.filter( + (connection) => connection.category === "ground", + ) + const powerConnections = planeConnections.filter( + (connection) => connection.category === "power", + ) + + expect(RK3326_SUPPLY_PINS).toHaveLength(205) + expect(planeConnections).toHaveLength(205) + expect(allBusPlaneConnections).toHaveLength(205) + expect(new Set(planeConnections.map(({ ball }) => ball)).size).toBe(205) + expect( + new Set(planeConnections.map(({ traceName }) => traceName)).size, + ).toBe(205) + expect(new Set(planeConnections.map(({ busName }) => busName)).size).toBe( + 205, + ) + + expect(groundConnections).toHaveLength(154) + expect( + groundConnections.every( + ({ layer, netName }) => + layer === RK3326_GROUND_PLANE_LAYER && netName === "GND", + ), + ).toBe(true) + + expect(powerConnections).toHaveLength(51) + expect( + powerConnections.every(({ layer }) => layer === RK3326_POWER_PLANE_LAYER), + ).toBe(true) + expect( + powerConnections.filter(({ netName }) => netName === "CPU_VDD"), + ).toHaveLength(9) + expect( + powerConnections.filter(({ netName }) => netName === "LOGIC_VDD"), + ).toHaveLength(9) + expect( + powerConnections.filter(({ netName }) => netName === "DDRIO_VDD"), + ).toHaveLength(13) + expect(new Set(powerConnections.map(({ netName }) => netName)).size).toBe( + 23, + ) + }) + + test("reserves both plane layers from signal fanout", () => { + expect(RK3326_DEFAULT_SIGNAL_FANOUT_LAYERS).toEqual([ + "top", + "inner3", + "inner4", + "bottom", + ]) + expect(RK3326_DEFAULT_SIGNAL_FANOUT_LAYERS).not.toContain( + RK3326_GROUND_PLANE_LAYER, + ) + expect(RK3326_DEFAULT_SIGNAL_FANOUT_LAYERS).not.toContain( + RK3326_POWER_PLANE_LAYER, + ) + }) +})