astra/f1c100s

This code defines and renders a four-layer, top-mounted F1C100S module with detailed pin assignments, schematic annotations, and layout profiles, facilitating design, validation, and routing of the hardware including support for decoupling capacitors, crystal oscillators, and external connectors.

Version
0.11.0
License
unset
Stars
0

src/layout.ts

import { EXTERNAL_NETS, PIN_NETS, RAILS, pinPosition } from "./pin-map";
import {
	PROFILE_LAYOUTS,
	EXIT_DIAMETER,
	TERMINAL_EDGE,
	type LayoutProfile,
	type Side,
} from "./profiles";
export interface Passive {
	name: string;
	kind: "capacitor" | "resistor";
	value: string;
	x: number;
	y: number;
	rotation: number;
	a: string;
	b: string;
}
export interface Terminal {
	name: string;
	x: number;
	y: number;
	side: Side;
	width: number;
	height: number;
}
export function makeLayout(profile: LayoutProfile) {
	const spec = PROFILE_LAYOUTS[profile];
	const source = (net: string) =>
		pinPosition(
			Number(Object.keys(PIN_NETS).find((p) => PIN_NETS[+p] === net)),
			spec.rotation,
		);
	const sideFor = (p: { x: number; y: number }): Side =>
		Math.abs(p.x) > Math.abs(p.y)
			? p.x > 0
				? "right"
				: "left"
			: p.y > 0
				? "top"
				: "bottom";
	const groups: Record<Side, string[]> = {
		top: [],
		right: [],
		bottom: [],
		left: [],
	};
	for (const net of EXTERNAL_NETS) {
		let side = sideFor(source(net));
		if (net.startsWith("LCD_") && spec.lcd) side = spec.lcd;
		if ((net.startsWith("SPI0_") || net.startsWith("SDMMC0_")) && spec.storage)
			side = spec.storage;
		groups[side].push(net);
	}
	const terminals: Terminal[] = [];
	for (const side of Object.keys(groups) as Side[]) {
		// Keep each bus contiguous; retain physical pin order within a bus.
		const category = (n: string) =>
			n.startsWith("LCD_")
				? 0
				: n.startsWith("SPI0_")
					? 1
					: n.startsWith("SDMMC0_")
						? 2
						: n.startsWith("USB_")
							? 3
							: 4;
		const usb: string[] = groups[side].filter(
			(n) => n === "USB_DP" || n === "USB_DM",
		);
		const blocked = usb.map((n) =>
			side === "top" || side === "bottom" ? source(n).x : -source(n).y,
		);
		for (const name of usb) {
			const p = { ...source(name) };
			const axis = side === "left" || side === "right" ? "y" : "x";
			const middle =
				usb.reduce((sum, n) => sum + source(n)[axis], 0) / usb.length;
			p[axis] += Math.sign(p[axis] - middle) * 0.125;
			const horizontal = side === "left" || side === "right";
			terminals.push({
				name,
				side,
				x: horizontal
					? side === "left"
						? -TERMINAL_EDGE
						: TERMINAL_EDGE
					: p.x,
				y: horizontal ? p.y : side === "top" ? TERMINAL_EDGE : -TERMINAL_EDGE,
				width: EXIT_DIAMETER,
				height: EXIT_DIAMETER,
			});
		}
		const list = groups[side]
			.filter((n) => !usb.includes(n))
			.sort(
				(a, b) =>
					category(a) - category(b) ||
					(side === "top" || side === "bottom"
						? source(a).x - source(b).x
						: source(b).y - source(a).y),
			);
		const slots = Array.from({ length: 37 }, (_, i) => -11.7 + i * 0.65).filter(
			(t) => blocked.every((v) => Math.abs(t - v) > 0.6),
		);
		if (slots.length < list.length)
			throw new Error(`Too many terminals on ${side}`);
		const start = Math.floor((slots.length - list.length) / 2);
		list.forEach((name, i) => {
			const t = slots[start + i]!;
			terminals.push({
				name,
				side,
				x:
					side === "left"
						? -TERMINAL_EDGE
						: side === "right"
							? TERMINAL_EDGE
							: t,
				y:
					side === "top"
						? TERMINAL_EDGE
						: side === "bottom"
							? -TERMINAL_EDGE
							: -t,
				width: EXIT_DIAMETER,
				height: EXIT_DIAMETER,
			});
		});
	}

	const angle = (spec.rotation * Math.PI) / 180;
	const rotate = (x: number, y: number) => ({
		x: +(x * Math.cos(angle) - y * Math.sin(angle)).toFixed(6),
		y: +(x * Math.sin(angle) + y * Math.cos(angle)).toFixed(6),
	});
	const crystal = { ...rotate(1.6, 10.1), rotation: spec.rotation + 180 };
	const passives: Passive[] = [
		{
			name: "C_OSCI",
			kind: "capacitor",
			value: "18pF",
			a: "HOSCI",
			b: "GND",
			...rotate(4.4, 10.975),
			rotation: spec.rotation,
		},
		{
			name: "C_OSCO",
			kind: "capacitor",
			value: "18pF",
			a: "HOSCO",
			b: "GND",
			...rotate(-1.2, 9.225),
			rotation: spec.rotation + 180,
		},
	];
	function add(
		name: string,
		kind: Passive["kind"],
		value: string,
		a: string,
		b: string,
		near: { x: number; y: number },
	) {
		const preferred = sideFor(near);
		let chosen: { x: number; y: number } | undefined;
		for (const side of [
			preferred,
			...(["top", "right", "bottom", "left"] as Side[]).filter(
				(s) => s !== preferred,
			),
		]) {
			const horizontal = side === "left" || side === "right";
			const tangent = horizontal ? near.y : near.x;
			for (const r of [8, 10.45]) {
				for (const offset of [
					0, 1.8, -1.8, 3.6, -3.6, 5.4, -5.4, 7.2, -7.2, 9, -9, 12, -12, 15,
					-15, 18, -18,
				]) {
					const t = Math.max(-10.7, Math.min(10.7, tangent + offset));
					const p = {
						x: horizontal ? (side === "right" ? r : -r) : t,
						y: horizontal ? t : side === "top" ? r : -r,
					};
					// Reserve the complete crystal body/land pattern and local clock wiring.
					const unrot = rotateBack(p.x, p.y);
					if (
						unrot.x > -2.3 &&
						unrot.x < 5.5 &&
						unrot.y > 6.9 &&
						unrot.y < 12.15
					)
						continue;
					if (
						terminals.some((terminal) => {
							if (
								Math.abs(p.x - terminal.x) < 0.88 + terminal.width / 2 &&
								Math.abs(p.y - terminal.y) < 0.42 + terminal.height / 2
							)
								return true;
							if (!terminal.name.startsWith("USB_")) return false;
							const start = source(terminal.name);
							return (
								p.x > Math.min(start.x, terminal.x) - 0.95 &&
								p.x < Math.max(start.x, terminal.x) + 0.95 &&
								p.y > Math.min(start.y, terminal.y) - 0.5 &&
								p.y < Math.max(start.y, terminal.y) + 0.5
							);
						})
					)
						continue;
					if (
						passives.every(
							(q) => Math.abs(q.x - p.x) > 2.35 || Math.abs(q.y - p.y) > 1.65,
						)
					) {
						chosen = p;
						break;
					}
				}
				if (chosen) break;
			}
			if (chosen) break;
		}
		if (!chosen) throw Error(`Cannot place ${name}`);
		passives.push({ name, kind, value, ...chosen, rotation: 0, a, b });
	}
	function rotateBack(x: number, y: number) {
		return {
			x: x * Math.cos(angle) + y * Math.sin(angle),
			y: -x * Math.sin(angle) + y * Math.cos(angle),
		};
	}

	for (const [pin, net] of Object.entries(PIN_NETS)) {
		if ((RAILS as readonly string[]).includes(net))
			add(
				`C_D${pin}`,
				"capacitor",
				"100nF",
				net,
				"GND",
				pinPosition(+pin, spec.rotation),
			);
	}
	for (const net of RAILS)
		add(
			`C_B_${net}`,
			"capacitor",
			net === "VCC_DRAM" ? "1uF" : "10uF",
			net,
			"GND",
			source(net),
		);
	for (const net of ["VRA1", "VRA2"]) {
		add(`C_${net}`, "capacitor", "1uF", net, "GND", source(net));
		add(`R_${net}`, "resistor", "200k", net, "GND", source(net));
	}
	add("R_SV_H", "resistor", "2k", "VCC_DRAM", "SVREF", source("SVREF"));
	add("R_SV_L", "resistor", "2k", "SVREF", "GND", source("SVREF"));
	add("C_SV_H", "capacitor", "100nF", "VCC_DRAM", "SVREF", source("SVREF"));
	add("C_SV_L", "capacitor", "100nF", "SVREF", "GND", source("SVREF"));
	for (const net of ["TV_VRN", "TV_VRP"])
		add(`C_${net}`, "capacitor", "10uF", net, "GND", source(net));
	add("C_TV_REF", "capacitor", "10uF", "TV_VRP", "TV_VRN", source("TV_VRP"));
	add("R_RESET", "resistor", "47k", "VCC_IO", "RESET", source("RESET"));
	add("C_RESET", "capacitor", "100nF", "RESET", "GND", source("RESET"));
	for (const net of [
		"SDMMC0_CMD",
		"SDMMC0_D0",
		"SDMMC0_D1",
		"SDMMC0_D2",
		"SDMMC0_D3",
	])
		add(`R_PU_${net}`, "resistor", "47k", "VCC_IO", net, source(net));
	add(
		"R_PU_SPI0_CS",
		"resistor",
		"10k",
		"VCC_IO",
		"SPI0_CS",
		source("SPI0_CS"),
	);
	for (const net of ["TWI0_SDA", "TWI0_SCL"])
		add(`R_PU_${net}`, "resistor", "4.7k", "VCC_IO", net, source(net));
	// Keep support placements stable while bringing assigned decouplers together
	// in pin order. Positions are authored in the native physical frame.
	const decouplers: Record<number, [number, number]> = {
		4: [-8, 3.5],
		5: [-8, 2.5],
		20: [-8, -3.4],
		22: [-8, -4.4],
		30: [-2.9, -8],
		31: [-1.8, -8],
		32: [-0.7, -8],
		34: [0.4, -8],
		35: [1.5, -8],
		36: [2.6, -8],
		50: [6.9, -2.2],
		67: [4.5, 8],
		71: [2.6, 8],
		73: [1.5, 8],
		80: [-1, 8],
	};
	for (const p of passives) {
		const match = p.name.match(/^C_D(\d+)$/);
		if (match) {
			const [x, y] = decouplers[Number(match[1])]!;
			Object.assign(p, rotate(-y, x));
		}
	}
	// Bulk reservoirs use the underside so their larger courtyards do not
	// displace the short top-side processor decoupling branches.
	const bulkNative: Record<string, [number, number]> = {
		C_B_VCC_HP: [-8, 4],
		C_B_VCC_IO: [-8, 0],
		C_B_VDD_CORE: [-8, -6.5],
		C_B_VCC_DRAM: [-3, -8],
		C_B_VCC_USB: [3, 8],
		C_B_VCC_TV: [-1, 8],
		C_B_AVCC: [-5, 8],
		C_TV_VRN: [8, 4],
		C_TV_VRP: [8, 0],
		C_TV_REF: [8, -4],
	};
	for (const p of passives) {
		const xy = bulkNative[p.name];
		if (xy) Object.assign(p, rotate(-xy[1], xy[0]));
	}
	// Pin 1 of the top-side passive footprint is on the left at 0 degrees. Aim the
	// supply/reference side at its actual processor pin, not merely at U1's
	// center. Dedicated C_D capacitors retain their individual supply pin.
	// The two oscillator loads keep their crystal-oriented placement.
	for (const p of passives) {
		if (p.kind !== "capacitor" || p.name.startsWith("C_OSC")) continue;
		const dedicated = p.name.match(/^C_D(\d+)$/);
		const pins = dedicated
			? [Number(dedicated[1])]
			: Object.entries(PIN_NETS)
					.filter(([, net]) => net === p.a)
					.map(([pin]) => Number(pin));
		const targets = pins.map((pin) => pinPosition(pin, spec.rotation));
		const target = targets.sort(
			(a, b) =>
				Math.hypot(a.x - p.x, a.y - p.y) - Math.hypot(b.x - p.x, b.y - p.y),
		)[0];
		if (!target) throw Error(`No processor target for ${p.name}`);
		const dx = target.x - p.x,
			dy = target.y - p.y;
		p.rotation =
			Math.abs(dx) > Math.abs(dy) ? (dx < 0 ? 0 : 180) : dy < 0 ? 90 : 270;
		// Bottom-side footprints mirror the pad direction.
		if (bulkNative[p.name]) p.rotation = (p.rotation + 180) % 360;
	}
	const nets: Record<string, string[]> = {};
	const connect = (net: string, port: string) => (nets[net] ??= []).push(port);
	for (const [pin, net] of Object.entries(PIN_NETS))
		connect(net, `.U1 > .pin${pin}`);
	for (const p of passives) {
		connect(p.a, `.${p.name} > .pin1`);
		connect(p.b, `.${p.name} > .pin2`);
	}
	connect("HOSCI", ".Y1 > .pin1");
	connect("HOSCO", ".Y1 > .pin3");
	connect("GND", ".Y1 > .pin2");
	connect("GND", ".Y1 > .pin4");
	for (const t of terminals) connect(t.name, `.${t.name} > .pin1`);
	return {
		profile,
		rotation: spec.rotation,
		crystal,
		terminals,
		passives,
		nets,
	};
}