astra-abse/t113-s3

Allwinner T113-S3 / JLCPCB C5197687: native saved fanout, LCD top, storage right, 127 perimeter exits, 30 decoupling capacitors, connected GND planes; clean DRC and shorts, all 29 vias connected.

Version
1.2.0
License
MIT
Stars
0

src/T113S3Module.tsx

import { attachSavedFanout, joinSavedFanoutExits } from "./saved-fanout";
import type { FanoutTracePath } from "@tscircuit/props";
import savedPaths from "./generated/lcd_top_storage_right.trace-paths.json";
import { Fragment } from "react";
import { Decoupling } from "./decoupling";
import type { ChipProps } from "@tscircuit/props";
import { T113_S3 } from "../imports/T113_S3";
import { pins } from "./pins";
import { resolveTerminal, type TerminalName } from "./terminals";
import {
	CHIP_ROTATION,
	FANOUT_SIZE,
	TRACE_WIDTH,
	LCD_PINS,
	STORAGE_PINS,
	type RoutingProfile,
} from "./routing-profile";

export interface T113S3ModuleProps {
	name: string;
	routingProfile?: RoutingProfile;
	/** Profile selector matching the current astra/f1c100s reference. */
	layoutProfile?: RoutingProfile;
	/** Alias for compatibility with the first release. */
	busProfile?: RoutingProfile;
	/** Module center in board coordinates, mm. +X right, +Y top. */
	pcbX?: number;
	pcbY?: number;
	/** Rotate the complete module and exits; directions remain module-relative. */
	pcbRotation?: 0 | 90 | 180 | 270;
	schX?: number;
	schY?: number;
	connections?: Partial<Record<TerminalName, string>>;
}

const pinAttributes: NonNullable<ChipProps["pinAttributes"]> =
	Object.fromEntries(
		pins.map((pin) => [
			`pin${pin.pin}`,
			pin.pin === 106
				? { doNotConnect: true }
				: {
						mustBeConnected: true,
						...([91, 129].includes(pin.pin) ? { requiresGround: true } : {}),
						...(/^(VCC|VDD|AVCC|HPVCC|LDO_IN)/.test(pin.label)
							? { requiresPower: true }
							: {}),
					},
		]),
	);

/** Reusable, saved-fanout module for JLCPCB C5197687.
 * This is a fanout primitive, not a powered or bootable SoC reference design.
 * Public chip selectors: `.SOC .U1 > .PD0`, `.SOC .U1 > .PF0`, etc.
 */
export function T113S3Module({
	name,
	routingProfile,
	layoutProfile,
	busProfile,
	pcbX = 0,
	pcbY = 0,
	pcbRotation = 0,
	schX = 0,
	schY = 0,
	connections = {},
}: T113S3ModuleProps) {
	const profile =
		layoutProfile ?? routingProfile ?? busProfile ?? "lcd_top_storage_right";
	if (profile !== "lcd_top_storage_right") {
		throw new Error(`Unknown routing profile: ${profile}`);
	}
	if (
		[layoutProfile, routingProfile, busProfile]
			.filter(Boolean)
			.some((p) => p !== profile)
	)
		throw new Error("Conflicting layoutProfile, routingProfile or busProfile");
	if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
		throw new Error("Module name must be a selector-safe identifier");
	if (![0, 90, 180, 270].includes(pcbRotation))
		throw new Error("pcbRotation must be 0, 90, 180 or 270");
	const externalConnections = Object.entries(connections).map(
		([terminal, target]) => {
			const pin = resolveTerminal(terminal);
			if (typeof target !== "string" || !target.trim())
				throw new Error(`Empty connection for ${terminal}`);
			return { terminal, pin, target };
		},
	);

	return (
		<>
			<subcircuit
				name={name}
				{...{ ref: attachSavedFanout }}
				autorouter={{ local: true, algorithmFn: joinSavedFanoutExits }}
				schTraceAutoLabelEnabled
				schMaxTraceDistance={3}
				pcbX={pcbX}
				pcbY={pcbY}
				pcbRotation={pcbRotation}
				schX={schX}
				schY={schY}
				pcbStyle={{ viaHoleDiameter: 0.2, viaPadDiameter: 0.45 }}
			>
				<fanout
					name="FANOUT"
					width={FANOUT_SIZE}
					height={FANOUT_SIZE}
					pcbTracePaths={structuredClone(savedPaths) as FanoutTracePath[]}
				>
					<T113_S3
						name="U1"
						pcbRotation={CHIP_ROTATION}
						pinAttributes={pinAttributes}
						schSectionName="soc"
						schX={-14}
						schY={0}
						schWidth={5}
						schHeight={20.5}
						schPinStyle={Object.fromEntries(
							pins.map((p) => [
								`pin${p.pin}`,
								{ topMargin: 0.05, bottomMargin: 0.05 },
							]),
						)}
						schPinArrangement={{
							leftSide: {
								pins: Array.from({ length: 65 }, (_, i) => i + 1),
								direction: "top-to-bottom",
							},
							rightSide: {
								pins: Array.from({ length: 64 }, (_, i) => i + 66),
								direction: "top-to-bottom",
							},
						}}
					/>
					<Decoupling />
				</fanout>
				<bus name="LCD" connections={LCD_PINS.map((p) => `ESC_${p.label}`)} />
				<bus
					name="STORAGE"
					connections={STORAGE_PINS.map((p) => `ESC_${p.label}`)}
				/>

				{pins
					.filter((p) => p.pin !== 106)
					.map((pin) => {
						const connection = `U1.pin${pin.pin}`;
						const net = pin.pin === 91 || pin.pin === 129 ? "GND" : pin.label;
						return (
							<Fragment key={pin.pin}>
								<trace
									name={`ESC_${pin.label}`}
									schDisplayLabel={net}
									from={connection}
									to={`net.${net}`}
									thickness={TRACE_WIDTH}
								/>
							</Fragment>
						);
					})}

				<Decoupling connectionsOnly />
				{(["bottom", "inner1"] as const).map((layer) => (
					<Fragment key={layer}>
						<copperpour
							name={`GROUND_${layer}`}
							layer={layer}
							connectsTo="net.GND"
							coveredWithSolderMask
							clearance={0.15}
							useThermalReliefs
							outline={[
								{ x: -17, y: -17 },
								{ x: 17, y: -17 },
								{ x: 17, y: 17 },
								{ x: -17, y: 17 },
							].map((p) => ({ x: p.x + pcbX, y: p.y + pcbY }))}
						/>
					</Fragment>
				))}
			</subcircuit>
			{externalConnections.map(({ terminal, pin, target }) => (
				<Fragment key={terminal}>
					<trace
						name={`${name}_${terminal}_external`}
						from={`.${name} .U1 > .${pin}`}
						to={target}
					/>
				</Fragment>
			))}
		</>
	);
}