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

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, 8.6), rotation: spec.rotation };
	const passives: Passive[] = [
		{
			name: "C_OSCI",
			kind: "capacitor",
			value: "18pF",
			a: "HOSCI",
			b: "GND",
			...rotate(-1.2, 7.725),
			rotation: spec.rotation + 180,
		},
		{
			name: "C_OSCO",
			kind: "capacitor",
			value: "18pF",
			a: "HOSCO",
			b: "GND",
			...rotate(4.4, 9.475),
			rotation: spec.rotation,
		},
	];
	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 < 10.65
					)
						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));
	// Pin 1 of the 0402 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;
	}
	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,
	};
}