201 lines
5.0 KiB
JavaScript
201 lines
5.0 KiB
JavaScript
function pinTypeFor(model, ref, pin) {
|
|
const instance = model.instances.find((x) => x.ref === ref);
|
|
if (!instance) {
|
|
return "passive";
|
|
}
|
|
const symbol = model.symbols[instance.symbol];
|
|
const p = symbol.pins.find((x) => x.name === pin);
|
|
return p?.type ?? "passive";
|
|
}
|
|
|
|
function buildSignalPaths(model) {
|
|
const directedEdges = [];
|
|
|
|
for (const net of model.nets) {
|
|
if (net.class === "power" || net.class === "ground") {
|
|
continue;
|
|
}
|
|
|
|
const sources = net.nodes
|
|
.filter((n) => {
|
|
const t = pinTypeFor(model, n.ref, n.pin);
|
|
return t === "output" || t === "power_out";
|
|
})
|
|
.map((n) => n.ref);
|
|
|
|
const sinks = net.nodes
|
|
.filter((n) => {
|
|
const t = pinTypeFor(model, n.ref, n.pin);
|
|
return t === "input" || t === "power_in" || t === "analog";
|
|
})
|
|
.map((n) => n.ref);
|
|
|
|
for (const s of sources) {
|
|
for (const d of sinks) {
|
|
if (s !== d) {
|
|
directedEdges.push([s, d]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const uniqEdges = new Map();
|
|
for (const edge of directedEdges) {
|
|
uniqEdges.set(`${edge[0]}->${edge[1]}`, edge);
|
|
}
|
|
|
|
const adj = new Map();
|
|
for (const [a, b] of uniqEdges.values()) {
|
|
const list = adj.get(a) ?? [];
|
|
list.push(b);
|
|
adj.set(a, list);
|
|
}
|
|
|
|
const starts = [...adj.keys()].sort();
|
|
const result = [];
|
|
|
|
for (const start of starts) {
|
|
const queue = [{ node: start, path: [start] }];
|
|
const seen = new Set();
|
|
|
|
while (queue.length > 0) {
|
|
const current = queue.shift();
|
|
const key = current.path.join(">>");
|
|
if (seen.has(key)) {
|
|
continue;
|
|
}
|
|
seen.add(key);
|
|
|
|
const next = adj.get(current.node) ?? [];
|
|
if (next.length === 0 && current.path.length > 1) {
|
|
result.push(current.path);
|
|
}
|
|
|
|
for (const n of next) {
|
|
if (!current.path.includes(n)) {
|
|
queue.push({ node: n, path: [...current.path, n] });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const dedup = new Map();
|
|
for (const p of result) {
|
|
dedup.set(p.join(" -> "), p);
|
|
}
|
|
|
|
return [...dedup.values()].sort((a, b) => a.join("/").localeCompare(b.join("/")));
|
|
}
|
|
|
|
function extractTopology(model) {
|
|
const powerDomains = model.nets
|
|
.filter((n) => n.class === "power" || n.class === "ground")
|
|
.map((n) => n.name)
|
|
.sort();
|
|
|
|
const clockSources = new Set();
|
|
for (const net of model.nets) {
|
|
if (net.class !== "clock") {
|
|
continue;
|
|
}
|
|
for (const node of net.nodes) {
|
|
const type = pinTypeFor(model, node.ref, node.pin);
|
|
if (type === "output" || type === "power_out") {
|
|
clockSources.add(node.ref);
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
power_domains: powerDomains,
|
|
clock_sources: [...clockSources].sort(),
|
|
signal_paths: buildSignalPaths(model)
|
|
};
|
|
}
|
|
|
|
function ercChecks(model) {
|
|
const issues = [];
|
|
const hasGroundNet = model.nets.some((n) => n.class === "ground");
|
|
|
|
if (!hasGroundNet) {
|
|
issues.push({
|
|
code: "ground_net_missing",
|
|
message: "No ground net defined.",
|
|
severity: "error",
|
|
path: "nets"
|
|
});
|
|
}
|
|
|
|
const connectedPins = new Set();
|
|
|
|
for (const net of model.nets) {
|
|
const types = net.nodes.map((n) => ({
|
|
node: n,
|
|
type: pinTypeFor(model, n.ref, n.pin)
|
|
}));
|
|
|
|
for (const t of types) {
|
|
connectedPins.add(`${t.node.ref}.${t.node.pin}`);
|
|
}
|
|
|
|
const powerOut = types.filter((t) => t.type === "power_out");
|
|
if (powerOut.length > 1) {
|
|
issues.push({
|
|
code: "multi_power_out",
|
|
message: `Net '${net.name}' has multiple power_out pins.`,
|
|
severity: "error",
|
|
path: `nets.${net.name}`
|
|
});
|
|
}
|
|
|
|
const outputs = types.filter((t) => t.type === "output");
|
|
if (outputs.length > 1) {
|
|
issues.push({
|
|
code: "output_conflict",
|
|
message: `Net '${net.name}' directly connects multiple output pins.`,
|
|
severity: "error",
|
|
path: `nets.${net.name}`
|
|
});
|
|
}
|
|
}
|
|
|
|
for (const instance of model.instances) {
|
|
const symbol = model.symbols[instance.symbol];
|
|
for (const pin of symbol.pins) {
|
|
const key = `${instance.ref}.${pin.name}`;
|
|
if ((pin.type === "power_in" || pin.type === "ground") && !connectedPins.has(key)) {
|
|
issues.push({
|
|
code: "required_power_unconnected",
|
|
message: `Required pin '${key}' is unconnected.`,
|
|
severity: "warning",
|
|
path: `instances.${instance.ref}`
|
|
});
|
|
}
|
|
if (pin.type === "input" && !connectedPins.has(key)) {
|
|
issues.push({
|
|
code: "floating_input",
|
|
message: `Input pin '${key}' is floating.`,
|
|
severity: "warning",
|
|
path: `instances.${instance.ref}`
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return issues;
|
|
}
|
|
|
|
export function analyzeModel(model, validationIssues = []) {
|
|
const ercIssues = ercChecks(model);
|
|
const all = [...validationIssues, ...ercIssues];
|
|
const errors = all.filter((x) => x.severity === "error");
|
|
const warnings = all.filter((x) => x.severity === "warning");
|
|
|
|
return {
|
|
ok: errors.length === 0,
|
|
errors,
|
|
warnings,
|
|
topology: extractTopology(model)
|
|
};
|
|
}
|