astra/f1c100s

The code defines the physical pin layout, labels, and footprint for the F1C100S system-on-chip (SoC) component used in electronic devices.

Version
0.9.2
License
unset
Stars
0

tests/module.test.tsx

import { checkPreviewPours } from "../scripts/check-preview-pours";
import { checkCapacitorOrientation } from "../scripts/check-capacitor-orientation";
import { checkPlatedExits } from "../scripts/check-plated-exits";
import { checkConventionalRouting } from "../scripts/check-conventions";
import { test, expect } from "bun:test";
import {
	Circuit,
	type GenericLocalAutorouter,
	type SimpleRouteJson,
	type SimplifiedPcbTrace,
} from "tscircuit";
import { F1C100SModule, LAYOUT_PROFILES, getF1C100SCircuitJson } from "../src";
import { SCHEMATIC_BANK_PINS } from "../src/schematic";
import { PIN_NETS } from "../src/pin-map";
import { makeLayout } from "../src/layout";
import { MODULE_SIZE, TERMINAL_EDGE } from "../src/profiles";
import { validateCircuit } from "../scripts/validate";

for (const profile of LAYOUT_PROFILES)
	test(`${profile}: stored copper loads and passes DRC`, async () => {
		const c = new Circuit();
		c.add(
			<board
				width={MODULE_SIZE}
				height={MODULE_SIZE}
				layers={4}
				minViaPadDiameter={0.45}
				minViaHoleDiameter={0.2}
				autorouter={{
					local: true,
					algorithmFn: async () => {
						throw new Error(
							"Loading stored copper must not invoke an autorouter",
						);
					},
				}}
			>
				<F1C100SModule name="SOC" layoutProfile={profile} />
			</board>,
		);
		await c.renderUntilSettled();
		const json = c.getCircuitJson();
		// Check physical copper coverage independently of path orientation and
		// of shared power branches repeated in several pin-to-exit paths.
		const segments = (data: any[]) => {
			const names = new Map(
				data
					.filter((e) => e.type === "source_trace")
					.map((e) => [e.source_trace_id, e.name]),
			);
			return data
				.filter((e) => e.type === "pcb_trace")
				.flatMap((t) =>
					t.route.flatMap((b: any, i: number) => {
						const a = t.route[i - 1];
						return a?.route_type === "wire" &&
							b.route_type === "wire" &&
							a.layer === b.layer &&
							Math.hypot(a.x - b.x, a.y - b.y) > 1e-6
							? [{ a, b, net: names.get(t.source_trace_id) }]
							: [];
					}),
				);
		};
		const stored = getF1C100SCircuitJson(profile);
		const loadedSegments = segments(json),
			storedSegments = segments(stored);
		for (const [from, to] of [
			[loadedSegments, storedSegments],
			[storedSegments, loadedSegments],
		]) {
			for (const s of from!)
				for (const fraction of [0, 0.25, 0.5, 0.75, 1]) {
					const x = s.a.x + (s.b.x - s.a.x) * fraction,
						y = s.a.y + (s.b.y - s.a.y) * fraction;
					expect(
						to!.some((t) => {
							if (t.net !== s.net || t.a.layer !== s.a.layer) return false;
							const dx = t.b.x - t.a.x,
								dy = t.b.y - t.a.y;
							const f = Math.max(
								0,
								Math.min(
									1,
									((x - t.a.x) * dx + (y - t.a.y) * dy) / (dx * dx + dy * dy),
								),
							);
							return Math.hypot(x - t.a.x - f * dx, y - t.a.y - f * dy) < 1e-5;
						}),
					).toBe(true);
				}
		}
		const viaKeys = (data: any[]) =>
			data
				.filter((e) => e.type === "pcb_via")
				.map(
					(v) =>
						`${v.x.toFixed(6)},${v.y.toFixed(6)},${v.hole_diameter},${v.outer_diameter}`,
				)
				.sort();
		expect(viaKeys(json)).toEqual(viaKeys(stored));
		expect(
			json
				.filter((e) => e.type === "pcb_trace")
				.every((e: any) => e.pcb_trace_id.startsWith("saved_fanout_")),
		).toBe(true);

		const capacitors = json.filter(
			(e: any) =>
				e.type === "source_component" && e.ftype === "simple_capacitor",
		);
		expect(capacitors).toHaveLength(32);
		for (const capacitor of capacitors as any[]) {
			const placed = json.find(
				(e: any) =>
					e.type === "pcb_component" &&
					e.source_component_id === capacitor.source_component_id,
			) as any;
			expect(placed.layer).toBe("top");
			const pads = json.filter(
				(e: any) =>
					e.type === "pcb_smtpad" &&
					e.pcb_component_id === placed.pcb_component_id,
			) as any[];
			expect(pads).toHaveLength(2);
			expect(pads.every((p) => p.layer === "top")).toBe(true);
		}
		expect(checkConventionalRouting(json)).toEqual([]);
		expect(checkCapacitorOrientation(json)).toEqual([]);
		expect(checkPlatedExits(json)).toEqual([]);
		expect(
			json.filter(
				(e: any) =>
					e.type === "source_component" && e.ftype === "simple_resistor",
			),
		).toHaveLength(13);
		const crystals = json.filter(
			(e: any) => e.type === "source_component" && e.ftype === "simple_crystal",
		) as any[];
		expect(crystals).toHaveLength(1);
		expect(crystals[0].frequency).toBe(24e6);
		const sourcePorts = json.filter(
			(e: any) => e.type === "source_port",
		) as any[];
		const sources = json.filter(
			(e: any) => e.type === "source_component",
		) as any[];
		const netOf = (name: string, pin: number) => {
			const source = sources.find((s) => s.name === name);
			const port = sourcePorts.find(
				(p) =>
					p.source_component_id === source?.source_component_id &&
					p.pin_number === pin,
			);
			return (
				json.find(
					(e: any) =>
						e.type === "source_trace" &&
						e.connected_source_port_ids.includes(port?.source_port_id),
				) as any
			)?.name;
		};
		expect(netOf("Y1", 1)).toBe("N_HOSCI");
		expect(netOf("Y1", 3)).toBe("N_HOSCO");
		for (const pin of [2, 4]) expect(netOf("Y1", pin)).toBe("N_GND");
		for (const [name, net] of [
			["C_OSCI", "HOSCI"],
			["C_OSCO", "HOSCO"],
		]) {
			expect(
				sources.find((s) => s.name === name).manufacturer_part_number,
			).toBe("GRM1555C1H180JA01D");
			expect(sources.find((s) => s.name === name).max_voltage_rating).toBe(50);
			expect(netOf(name!, 1)).toBe(`N_${net}`);
			expect(netOf(name!, 2)).toBe("N_GND");
			expect(sources.find((s) => s.name === name).capacitance).toBeCloseTo(
				18e-12,
				15,
			);
		}
		expect(netOf("C_TV_REF", 1)).toBe("N_TV_VRP");
		expect(netOf("C_TV_REF", 2)).toBe("N_TV_VRN");
		for (const net of [
			"SDMMC0_CMD",
			"SDMMC0_D0",
			"SDMMC0_D1",
			"SDMMC0_D2",
			"SDMMC0_D3",
			"SPI0_CS",
			"TWI0_SDA",
			"TWI0_SCL",
		]) {
			expect(netOf(`R_PU_${net}`, 1)).toBe("N_VCC_IO");
			expect(netOf(`R_PU_${net}`, 2)).toBe(`N_${net}`);
			expect(sources.find((s) => s.name === `R_PU_${net}`).resistance).toBe(
				net.startsWith("SDMMC") ? 47000 : net === "SPI0_CS" ? 10000 : 4700,
			);
		}
		const chip = json.find(
			(e: any) => e.type === "source_component" && e.name === "U1",
		) as any;
		const physicalPorts = json.filter(
			(e: any) =>
				e.type === "source_port" &&
				e.source_component_id === chip.source_component_id,
		) as any[];
		const renderedPorts = json.filter(
			(e: any) => e.type === "schematic_port",
		) as any[];
		expect(physicalPorts).toHaveLength(89);
		for (const port of physicalPorts) {
			expect(
				renderedPorts.filter((p) => p.source_port_id === port.source_port_id),
			).toHaveLength(1);
		}
		expect(json.filter((e) => e.type === "schematic_component")).toHaveLength(
			53,
		);
		expect(json.filter((e) => e.type.endsWith("_error"))).toEqual([]);
		const errors = await validateCircuit(json);
		if (errors.length) console.log(errors.slice(0, 5));
		expect(errors).toEqual([]);
		expect(json.filter((e) => e.type === "pcb_trace").length).toBeGreaterThan(
			100,
		);
		expect(json.filter((e: any) => e.type === "pcb_autorouting_error")).toEqual(
			[],
		);
		expect(
			json.filter(
				(e: any) => e.type === "source_component" && /^C_D\d+$/.test(e.name),
			),
		).toHaveLength(15);
		if (profile !== "native") {
			const [, lcdSide, , storageSide] = profile.split("_");
			for (const [prefix, side] of [
				["LCD_", lcdSide],
				["SPI0_", storageSide],
				["SDMMC0_", storageSide],
			]) {
				const sources = json.filter(
					(e: any) =>
						e.type === "source_component" && e.name.startsWith(prefix),
				) as any[];
				expect(sources.length).toBe(
					prefix === "LCD_" ? 22 : prefix === "SPI0_" ? 4 : 6,
				);
				for (const source of sources) {
					const placed = json.find(
						(e: any) =>
							e.type === "pcb_component" &&
							e.source_component_id === source.source_component_id,
					) as any;
					const coord =
						side === "left" || side === "right"
							? placed.center.x
							: placed.center.y;
					expect(coord).toBeCloseTo(
						(side === "left" || side === "bottom" ? -1 : 1) * TERMINAL_EDGE,
						5,
					);
				}
			}
		}
	}, 120000);

for (const parentLayer of ["top", "inner1", "inner2", "bottom"] as const)
	test(`parent routing connects to a plated exit on ${parentLayer}`, async () => {
		const terminal = makeLayout("lcd_top_storage_right").terminals.find(
			(t) => t.name === "SPI0_CLK",
		)!;
		let captured: SimpleRouteJson | undefined;
		const factory = async (
			input: SimpleRouteJson,
		): Promise<GenericLocalAutorouter> => {
			captured = input;
			const handlers: Record<string, ((e: any) => void)[]> = {
				complete: [],
				progress: [],
				error: [],
			};
			const traces: SimplifiedPcbTrace[] = input.connections.map((c, i) => ({
				type: "pcb_trace",
				pcb_trace_id: `parent_${i}`,
				connection_name: c.name,
				route: c.pointsToConnect.map((p) => ({
					route_type: "wire",
					x: p.x,
					y: p.y,
					layer: parentLayer,
					width: 0.12,
				})),
			}));
			return {
				input,
				isRouting: false,
				on(e: string, f: (e: any) => void) {
					handlers[e]!.push(f);
				},
				start() {
					queueMicrotask(() =>
						handlers.complete!.forEach((f) => f({ type: "complete", traces })),
					);
				},
				stop() {},
				solveSync() {
					return traces;
				},
			};
		};
		const c = new Circuit();
		c.add(
			<board
				width={52}
				height={36}
				layers={4}
				autorouter={{ local: true, algorithmFn: factory }}
			>
				<F1C100SModule
					name="SOC"
					layoutProfile="lcd_top_storage_right"
					connections={{ SPI0_CLK: ".OUT > .pin1" }}
				/>
				<chip
					name="OUT"
					pinLabels={{ pin1: "pin1" }}
					pcbX={22}
					pcbY={terminal.y}
					footprint={
						<footprint>
							<platedhole
								portHints={["pin1"]}
								shape="circle"
								holeDiameter={0.2}
								outerDiameter={0.45}
								pcbX={0}
								pcbY={0}
							/>
						</footprint>
					}
				/>
			</board>,
		);
		await c.renderUntilSettled();
		expect(captured?.connections).toHaveLength(1);
		for (const p of captured!.connections[0]!.pointsToConnect) {
			expect(p.layers).toContain(parentLayer);
			const hole = captured!.obstacles.find(
				(o) =>
					o.circuitJsonMetadata?.pcb_plated_hole_id &&
					Math.hypot(o.center.x - p.x, o.center.y - p.y) < 1e-6,
			);
			expect(hole?.layers).toContain(parentLayer);
		}
		const exit = captured!.connections[0]!.pointsToConnect.find(
			(p) => Math.abs(p.x - TERMINAL_EDGE) < 1e-6,
		)!;
		expect(
			new Set(
				captured!.obstacles.find(
					(o) =>
						o.circuitJsonMetadata?.pcb_plated_hole_id &&
						Math.hypot(o.center.x - exit.x, o.center.y - exit.y) < 1e-6,
				)?.layers,
			),
		).toEqual(new Set(["top", "inner1", "inner2", "bottom"]));
		const xs = captured!.connections[0]!.pointsToConnect.map((p) => p.x).sort(
			(a, b) => a - b,
		);
		expect(xs).toEqual([TERMINAL_EDGE, 22]);
		const errors = await validateCircuit(c.getCircuitJson());
		if (errors.length) console.log(errors.slice(0, 5));
		expect(errors).toEqual([]);
	}, 120000);

test("the pin map covers every physical pin once", () => {
	expect([...SCHEMATIC_BANK_PINS].sort((a, b) => a - b)).toEqual(
		Array.from({ length: 89 }, (_, i) => i + 1),
	);
	expect(
		Object.keys(PIN_NETS)
			.map(Number)
			.sort((a, b) => a - b),
	).toEqual(Array.from({ length: 89 }, (_, i) => i + 1));
	expect(
		new Set(
			Array.from(
				{ length: 22 },
				(_, i) =>
					Object.values(PIN_NETS)
						.filter((n) => n.startsWith("LCD_"))
						.sort()[i],
			),
		).size,
	).toBe(22);
});

test("data is cloned; unsupported profiles and legacy props fail explicitly", () => {
	expect(() =>
		F1C100SModule({ name: "SOC", busProfile: "native" } as any),
	).toThrow("Use layoutProfile");
	const a = getF1C100SCircuitJson("native"),
		b = getF1C100SCircuitJson("native");
	a.splice(0);
	expect(b.length).toBeGreaterThan(100);
	expect(() => getF1C100SCircuitJson("typo" as any)).toThrow("Unsupported");
	expect(() =>
		F1C100SModule({ name: "SOC", variant: "native" } as any),
	).toThrow("Use layoutProfile");
	expect(() =>
		F1C100SModule({ name: "SOC", connections: { TYPO: "net.GND" } }),
	).toThrow("Unknown");
});

test("two rotated instances retain independent nets and copper", async () => {
	const c = new Circuit();
	c.add(
		<board width={82} height={42} layers={4}>
			<F1C100SModule
				name="A"
				layoutProfile="lcd_top_storage_right"
				pcbX={-20}
			/>
			<F1C100SModule
				name="B"
				layoutProfile="lcd_right_storage_left"
				pcbX={20}
				pcbRotation={90}
			/>
		</board>,
	);
	await c.renderUntilSettled();
	const json = c.getCircuitJson();
	expect(checkCapacitorOrientation(json)).toEqual([]);
	const errors = await validateCircuit(json);
	if (errors.length) console.log(errors.slice(0, 5));
	expect(errors).toEqual([]);
	expect(
		json.filter(
			(e: any) =>
				e.type === "source_component" &&
				e.manufacturer_part_number === "F1C100S",
		),
	).toHaveLength(2);
	const ids = json
		.filter((e: any) => e.type === "source_trace")
		.map((e: any) => e.source_trace_id);
	expect(new Set(ids).size).toBe(ids.length);
	const copperIds = json
		.filter((e: any) => e.type === "pcb_trace")
		.map((e: any) => e.pcb_trace_id);
	expect(new Set(copperIds).size).toBe(copperIds.length);
}, 120000);

test("exported schematic boxes share one processor across two A4 sheets", async () => {
	const { ProfilePreview } = await import("../src/ProfilePreview");
	const c = new Circuit();
	c.add(<ProfilePreview layoutProfile="lcd_top_storage_right" />);
	await c.renderUntilSettled();
	const json = c.getCircuitJson() as any[];
	expect(checkPreviewPours(json)).toEqual([]);
	expect(json.filter((e) => e.type.endsWith("_error"))).toEqual([]);
	expect(
		json.filter((e) => e.type === "schematic_element_outside_sheet_warning"),
	).toEqual([]);
	const sheets = json.filter((e) => e.type === "schematic_sheet");
	expect(sheets).toHaveLength(2);
	expect(sheets.every((e) => e.sheet_size === "a4")).toBe(true);
	const chips = json.filter(
		(e) =>
			e.type === "source_component" && e.manufacturer_part_number === "F1C100S",
	);
	expect(chips).toHaveLength(1);
	const chip = chips[0];
	const symbols = json.filter(
		(e) =>
			e.type === "schematic_component" &&
			e.source_component_id === chip.source_component_id,
	);
	expect(symbols).toHaveLength(7);
	expect(new Set(symbols.map((e) => e.schematic_sheet_id)).size).toBe(2);
	expect(symbols.every((e) => e.size.width < 3 && e.size.height < 9)).toBe(
		true,
	);
	const ports = json.filter(
		(e) =>
			e.type === "source_port" &&
			e.source_component_id === chip.source_component_id,
	);
	expect(ports).toHaveLength(89);
	for (const port of ports) {
		expect(
			json.filter(
				(e) =>
					e.type === "schematic_port" &&
					e.source_port_id === port.source_port_id,
			),
		).toHaveLength(1);
	}
	const sheetIds = new Set(sheets.map((e) => e.schematic_sheet_id));
	expect(json.filter((e) => e.type === "schematic_component")).toHaveLength(53);
	expect(
		json
			.filter((e) =>
				["schematic_component", "schematic_trace", "schematic_port"].includes(
					e.type,
				),
			)
			.every((e) => sheetIds.has(e.schematic_sheet_id)),
	).toBe(true);
	const errors = await validateCircuit(json);
	expect(errors).toEqual([]);
}, 120000);

test("angle audit rejects oblique runs including via approaches", () => {
	const route = (x: number, y: number, route_type = "wire") =>
		[
			{
				type: "pcb_trace",
				pcb_trace_id: "angle_fixture",
				route: [
					{ route_type: "wire", x: 0, y: 0, layer: "top" },
					{
						route_type,
						x,
						y,
						layer: "top",
						from_layer: "top",
						to_layer: "bottom",
					},
				],
			},
		] as any;
	for (const [x, y] of [
		[1, 0],
		[0, 1],
		[1, 1],
		[-1, 1],
		[0, 0],
	])
		expect(checkConventionalRouting(route(x!, y!))).toEqual([]);
	expect(checkConventionalRouting(route(1, 0.5))).toHaveLength(1);
	expect(checkConventionalRouting(route(1, 0.5, "via"))).toHaveLength(1);
});

test("saved external-net paths end at their named plated exit", async () => {
	for (const profile of LAYOUT_PROFILES) {
		const json = getF1C100SCircuitJson(profile) as any[];
		const paths = await Bun.file(
			`src/generated/${profile}.trace-paths.json`,
		).json();
		for (const path of paths) {
			const [name, pin] = path.connection.split(".pin");
			const component = json.find(
				(e) => e.type === "source_component" && e.name === name,
			);
			const port = json.find(
				(e) =>
					e.type === "source_port" &&
					e.source_component_id === component.source_component_id &&
					e.pin_number === Number(pin),
			);
			const net = json.find(
				(e) =>
					e.type === "source_trace" &&
					e.connected_source_port_ids.includes(port.source_port_id),
			);
			const terminal = makeLayout(profile).terminals.find(
				(t) => `N_${t.name}` === net.name,
			);
			if (!terminal) continue;
			expect(path.route.at(-1).x).toBeCloseTo(terminal.x, 6);
			expect(path.route.at(-1).y).toBeCloseTo(terminal.y, 6);
		}
	}
});

test("orientation audit catches reversed and sideways decoupling pads", () => {
	const data: any[] = [
		{
			type: "source_component",
			source_component_id: "chip",
			source_group_id: "module",
			name: "U1",
			ftype: "simple_chip",
		},
		{
			type: "source_component",
			source_component_id: "cap",
			source_group_id: "module",
			name: "C_D5",
			ftype: "simple_capacitor",
		},
		{
			type: "source_port",
			source_port_id: "supply",
			source_component_id: "chip",
			pin_number: 5,
		},
		{
			type: "source_port",
			source_port_id: "positive",
			source_component_id: "cap",
			pin_number: 1,
		},
		{
			type: "source_port",
			source_port_id: "ground",
			source_component_id: "cap",
			pin_number: 2,
		},
		{ type: "source_trace", connected_source_port_ids: ["supply", "positive"] },
		{ type: "pcb_port", source_port_id: "supply", x: 0, y: 0 },
		{ type: "pcb_port", source_port_id: "positive", x: 1, y: 0 },
		{ type: "pcb_port", source_port_id: "ground", x: 2, y: 0 },
	];
	expect(checkCapacitorOrientation(data)).toEqual([]);
	data[7].x = 2;
	data[8].x = 1;
	expect(checkCapacitorOrientation(data)).toHaveLength(1);
	data[7].x = data[8].x = 1;
	data[7].y = 0.5;
	data[8].y = -0.5;
	expect(checkCapacitorOrientation(data)).toHaveLength(1);
});