diff --git a/README.md b/README.md index 8394447..39a7259 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ and output an array of arrays for any issues found. | --- | --- | | [`checkConnectorAccessibleOrientation`](./lib/check-connector-accessible-orientation.ts) | Returns `pcb_accessibility_error` for connectors whose orientation makes them inaccessible. | | [`checkAllPinsInComponentAreUnderspecified`](./lib/check-all-pins-in-component-are-underspecified.ts) | Returns `source_component_pins_underspecified_warning` when every pin on a chip lacks pin attributes. | +| [`checkChipPowerPinsHaveDecouplingCapacitors`](./lib/check-chip-power-pins-have-decoupling-capacitors.ts) | Returns `source_pin_missing_trace_warning` when a connected chip power pin has no decoupling capacitor to ground. | | [`checkNoPowerPinDefined`](./lib/check-no-power-pin-defined.ts) | Returns `source_no_power_pin_defined_warning` when a chip has no pin with `requires_power=true`. | | [`checkNoGroundPinDefined`](./lib/check-no-ground-pin-defined.ts) | Returns `source_no_ground_pin_defined_warning` when a chip has no pin with `requires_ground=true`. | | [`checkDifferentNetViaSpacing`](./lib/check-different-net-via-spacing.ts) | Returns `pcb_via_clearance_error` if vias on different nets are too close together. | @@ -32,7 +33,7 @@ and output an array of arrays for any issues found. | Function | Description | | --- | --- | | [`runAllPlacementChecks`](./lib/run-all-checks.ts) | Runs placement checks (`checkViasOffBoard`, `checkPcbComponentsOutOfBoard`, `checkPcbComponentOverlap`, `checkPadPadClearance`, and `checkConnectorAccessibleOrientation`). | -| [`runAllNetlistChecks`](./lib/run-all-checks.ts) | Runs netlist connectivity checks (currently `checkPinMustBeConnected`). | +| [`runAllNetlistChecks`](./lib/run-all-checks.ts) | Runs netlist connectivity checks (`checkPinMustBeConnected` and `checkChipPowerPinsHaveDecouplingCapacitors`). | | [`runAllPinSpecificationChecks`](./lib/run-all-checks.ts) | Runs pin specification checks (e.g. `checkAllPinsInComponentAreUnderspecified`, `checkNoPowerPinDefined`, and `checkNoGroundPinDefined`). | | [`runAllRoutingChecks`](./lib/run-all-checks.ts) | Runs all routing checks currently enabled (`checkEachPcbPortConnectedToPcbTraces`, `checkSourceTracesHavePcbTraces`, `checkEachPcbTraceNonOverlapping`, `checkPadTraceClearance`, `checkViaTraceClearance`, same/different net via spacing, and `checkPcbTracesOutOfBoard`). Trace-obstacle pairs are classified before aggregation, so each pair produces one overlap or clearance diagnostic, never both. | | [`runAllChecks`](./lib/run-all-checks.ts) | Runs placement, netlist, pin specification, and routing checks and returns a combined list of issues. | diff --git a/index.ts b/index.ts index 8330c1c..1bd9fa4 100644 --- a/index.ts +++ b/index.ts @@ -17,6 +17,7 @@ export { checkViaTraceClearance } from "./lib/check-via-trace-clearance" export { dedupePcbDrcErrors } from "./lib/dedupe-pcb-drc-errors" export { checkPinMustBeConnected } from "./lib/check-pin-must-be-connected" export { checkAllPinsInComponentAreUnderspecified } from "./lib/check-all-pins-in-component-are-underspecified" +export { checkChipPowerPinsHaveDecouplingCapacitors } from "./lib/check-chip-power-pins-have-decoupling-capacitors" export { checkNoPowerPinDefined } from "./lib/check-no-power-pin-defined" export { checkNoGroundPinDefined } from "./lib/check-no-ground-pin-defined" export { diff --git a/lib/check-chip-power-pins-have-decoupling-capacitors.ts b/lib/check-chip-power-pins-have-decoupling-capacitors.ts new file mode 100644 index 0000000..5e2a7fa --- /dev/null +++ b/lib/check-chip-power-pins-have-decoupling-capacitors.ts @@ -0,0 +1,10 @@ +import type { + AnyCircuitElement, + SourcePinMissingTraceWarning, +} from "circuit-json" +import { DecouplingCapacitorChecker } from "./check-chip-power-pins-have-decoupling-capacitors/decoupling-capacitor-checker" + +export const checkChipPowerPinsHaveDecouplingCapacitors = ( + circuitJson: AnyCircuitElement[], +): SourcePinMissingTraceWarning[] => + new DecouplingCapacitorChecker(circuitJson).getWarnings() diff --git a/lib/check-chip-power-pins-have-decoupling-capacitors/decoupling-capacitor-checker-capacitor-connects-power-source-port-to-ground.ts b/lib/check-chip-power-pins-have-decoupling-capacitors/decoupling-capacitor-checker-capacitor-connects-power-source-port-to-ground.ts new file mode 100644 index 0000000..ec316e5 --- /dev/null +++ b/lib/check-chip-power-pins-have-decoupling-capacitors/decoupling-capacitor-checker-capacitor-connects-power-source-port-to-ground.ts @@ -0,0 +1,28 @@ +import type { SourcePort, SourceSimpleCapacitor } from "circuit-json" +import type { DecouplingCapacitorChecker } from "./decoupling-capacitor-checker" + +export const capacitorConnectsPowerSourcePortToGround = ( + checker: DecouplingCapacitorChecker, + capacitorSourceComponent: SourceSimpleCapacitor, + chipPowerSourcePort: SourcePort, +): boolean => { + const capacitorSourcePorts = checker.getSourcePorts( + capacitorSourceComponent.source_component_id, + ) + if (capacitorSourcePorts.length !== 2) return false + + const [firstCapacitorSourcePort, secondCapacitorSourcePort] = + capacitorSourcePorts + return ( + (checker.sourceConnectivityMap.areIdsConnected( + chipPowerSourcePort.source_port_id, + firstCapacitorSourcePort.source_port_id, + ) && + checker.sourcePortIsConnectedToGround(secondCapacitorSourcePort)) || + (checker.sourceConnectivityMap.areIdsConnected( + chipPowerSourcePort.source_port_id, + secondCapacitorSourcePort.source_port_id, + ) && + checker.sourcePortIsConnectedToGround(firstCapacitorSourcePort)) + ) +} diff --git a/lib/check-chip-power-pins-have-decoupling-capacitors/decoupling-capacitor-checker-get-warnings.ts b/lib/check-chip-power-pins-have-decoupling-capacitors/decoupling-capacitor-checker-get-warnings.ts new file mode 100644 index 0000000..bc10e37 --- /dev/null +++ b/lib/check-chip-power-pins-have-decoupling-capacitors/decoupling-capacitor-checker-get-warnings.ts @@ -0,0 +1,48 @@ +import type { SourcePinMissingTraceWarning } from "circuit-json" +import type { DecouplingCapacitorChecker } from "./decoupling-capacitor-checker" +import { getSourcePortDisplayLabel } from "./get-source-port-display-label" + +export const getDecouplingCapacitorWarnings = ( + checker: DecouplingCapacitorChecker, +): SourcePinMissingTraceWarning[] => { + const warnings: SourcePinMissingTraceWarning[] = [] + + for (const chipSourceComponent of checker.sourceComponents) { + if (chipSourceComponent.ftype !== "simple_chip") continue + + for (const chipSourcePort of checker.getSourcePorts( + chipSourceComponent.source_component_id, + )) { + if (!checker.sourcePortShouldHaveDecouplingCapacitor(chipSourcePort)) { + continue + } + if (!checker.sourcePortHasConnection(chipSourcePort)) continue + + const hasDecouplingCapacitor = checker.capacitorSourceComponents.some( + (capacitorSourceComponent) => + checker.capacitorConnectsPowerSourcePortToGround( + capacitorSourceComponent, + chipSourcePort, + ), + ) + if (hasDecouplingCapacitor) continue + + const recommendedCapacitance = + chipSourcePort.recommended_decoupling_capacitor_capacitance + const capacitanceDescription = + recommendedCapacitance === undefined ? "" : ` ${recommendedCapacitance}` + + warnings.push({ + type: "source_pin_missing_trace_warning", + source_pin_missing_trace_warning_id: `source_pin_missing_trace_warning_decoupling_${chipSourcePort.source_port_id}`, + warning_type: "source_pin_missing_trace_warning", + message: `Power pin ${getSourcePortDisplayLabel(chipSourcePort)} on ${chipSourceComponent.name} should have a${capacitanceDescription} decoupling capacitor connected to ground`, + source_component_id: chipSourceComponent.source_component_id, + source_port_id: chipSourcePort.source_port_id, + subcircuit_id: chipSourcePort.subcircuit_id, + }) + } + } + + return warnings +} diff --git a/lib/check-chip-power-pins-have-decoupling-capacitors/decoupling-capacitor-checker-source-port-has-connection.ts b/lib/check-chip-power-pins-have-decoupling-capacitors/decoupling-capacitor-checker-source-port-has-connection.ts new file mode 100644 index 0000000..b8889d1 --- /dev/null +++ b/lib/check-chip-power-pins-have-decoupling-capacitors/decoupling-capacitor-checker-source-port-has-connection.ts @@ -0,0 +1,15 @@ +import type { SourcePort } from "circuit-json" +import type { DecouplingCapacitorChecker } from "./decoupling-capacitor-checker" + +export const sourcePortHasConnection = ( + checker: DecouplingCapacitorChecker, + sourcePort: SourcePort, +): boolean => { + const connectedNetId = checker.sourceConnectivityMap.getNetConnectedToId( + sourcePort.source_port_id, + ) + if (!connectedNetId) return false + return checker.sourceConnectivityMap + .getIdsConnectedToNet(connectedNetId) + .some((connectedId) => connectedId !== sourcePort.source_port_id) +} diff --git a/lib/check-chip-power-pins-have-decoupling-capacitors/decoupling-capacitor-checker-source-port-is-connected-to-ground.ts b/lib/check-chip-power-pins-have-decoupling-capacitors/decoupling-capacitor-checker-source-port-is-connected-to-ground.ts new file mode 100644 index 0000000..3392309 --- /dev/null +++ b/lib/check-chip-power-pins-have-decoupling-capacitors/decoupling-capacitor-checker-source-port-is-connected-to-ground.ts @@ -0,0 +1,19 @@ +import type { SourcePort } from "circuit-json" +import type { DecouplingCapacitorChecker } from "./decoupling-capacitor-checker" + +export const sourcePortIsConnectedToGround = ( + checker: DecouplingCapacitorChecker, + sourcePort: SourcePort, +): boolean => + checker.groundSourcePorts.some((groundSourcePort) => + checker.sourceConnectivityMap.areIdsConnected( + sourcePort.source_port_id, + groundSourcePort.source_port_id, + ), + ) || + checker.groundSourceNets.some((groundSourceNet) => + checker.sourceConnectivityMap.areIdsConnected( + sourcePort.source_port_id, + groundSourceNet.source_net_id, + ), + ) diff --git a/lib/check-chip-power-pins-have-decoupling-capacitors/decoupling-capacitor-checker-source-port-should-have-decoupling-capacitor.ts b/lib/check-chip-power-pins-have-decoupling-capacitors/decoupling-capacitor-checker-source-port-should-have-decoupling-capacitor.ts new file mode 100644 index 0000000..6f00c45 --- /dev/null +++ b/lib/check-chip-power-pins-have-decoupling-capacitors/decoupling-capacitor-checker-source-port-should-have-decoupling-capacitor.ts @@ -0,0 +1,12 @@ +import type { SourcePort } from "circuit-json" + +export const sourcePortShouldHaveDecouplingCapacitor = ( + sourcePort: SourcePort, +): boolean => { + if (sourcePort.should_have_decoupling_capacitor !== undefined) { + return sourcePort.should_have_decoupling_capacitor + } + return ( + sourcePort.requires_power === true && sourcePort.provides_power !== true + ) +} diff --git a/lib/check-chip-power-pins-have-decoupling-capacitors/decoupling-capacitor-checker.ts b/lib/check-chip-power-pins-have-decoupling-capacitors/decoupling-capacitor-checker.ts new file mode 100644 index 0000000..93fda23 --- /dev/null +++ b/lib/check-chip-power-pins-have-decoupling-capacitors/decoupling-capacitor-checker.ts @@ -0,0 +1,96 @@ +import { cju } from "@tscircuit/circuit-json-util" +import type { + AnyCircuitElement, + SourceComponentBase, + SourceNet, + SourcePinMissingTraceWarning, + SourcePort, + SourceSimpleCapacitor, +} from "circuit-json" +import { + type ConnectivityMap, + getSourcePortConnectivityMapFromCircuitJson, +} from "circuit-json-to-connectivity-map" +import { capacitorConnectsPowerSourcePortToGround } from "./decoupling-capacitor-checker-capacitor-connects-power-source-port-to-ground" +import { getDecouplingCapacitorWarnings } from "./decoupling-capacitor-checker-get-warnings" +import { sourcePortHasConnection } from "./decoupling-capacitor-checker-source-port-has-connection" +import { sourcePortIsConnectedToGround } from "./decoupling-capacitor-checker-source-port-is-connected-to-ground" +import { sourcePortShouldHaveDecouplingCapacitor } from "./decoupling-capacitor-checker-source-port-should-have-decoupling-capacitor" +import type { SourceComponentId } from "./types" + +export class DecouplingCapacitorChecker { + readonly sourceComponents: SourceComponentBase[] + readonly sourcePorts: SourcePort[] + readonly groundSourcePorts: SourcePort[] + readonly groundSourceNets: SourceNet[] + readonly capacitorSourceComponents: SourceSimpleCapacitor[] + readonly sourcePortsBySourceComponentId = new Map< + SourceComponentId, + SourcePort[] + >() + readonly sourceConnectivityMap: ConnectivityMap + + constructor(circuitJson: AnyCircuitElement[]) { + const db = cju(circuitJson) + this.sourceComponents = db.source_component.list() as SourceComponentBase[] + this.sourcePorts = db.source_port.list() + this.groundSourcePorts = this.sourcePorts.filter( + (sourcePort) => + sourcePort.requires_ground === true || + sourcePort.provides_ground === true, + ) + this.groundSourceNets = db.source_net + .list() + .filter((sourceNet) => sourceNet.is_ground) + this.capacitorSourceComponents = this.sourceComponents.filter( + (sourceComponent): sourceComponent is SourceSimpleCapacitor => + sourceComponent.ftype === "simple_capacitor", + ) + this.sourceConnectivityMap = + getSourcePortConnectivityMapFromCircuitJson(circuitJson) + + for (const sourcePort of this.sourcePorts) { + if (!sourcePort.source_component_id) continue + const sourceComponentPorts = + this.sourcePortsBySourceComponentId.get( + sourcePort.source_component_id, + ) ?? [] + sourceComponentPorts.push(sourcePort) + this.sourcePortsBySourceComponentId.set( + sourcePort.source_component_id, + sourceComponentPorts, + ) + } + } + + getWarnings(): SourcePinMissingTraceWarning[] { + return getDecouplingCapacitorWarnings(this) + } + + getSourcePorts(sourceComponentId: SourceComponentId): SourcePort[] { + return this.sourcePortsBySourceComponentId.get(sourceComponentId) ?? [] + } + + sourcePortShouldHaveDecouplingCapacitor(sourcePort: SourcePort): boolean { + return sourcePortShouldHaveDecouplingCapacitor(sourcePort) + } + + sourcePortHasConnection(sourcePort: SourcePort): boolean { + return sourcePortHasConnection(this, sourcePort) + } + + sourcePortIsConnectedToGround(sourcePort: SourcePort): boolean { + return sourcePortIsConnectedToGround(this, sourcePort) + } + + capacitorConnectsPowerSourcePortToGround( + capacitorSourceComponent: SourceSimpleCapacitor, + chipPowerSourcePort: SourcePort, + ): boolean { + return capacitorConnectsPowerSourcePortToGround( + this, + capacitorSourceComponent, + chipPowerSourcePort, + ) + } +} diff --git a/lib/check-chip-power-pins-have-decoupling-capacitors/get-source-port-display-label.ts b/lib/check-chip-power-pins-have-decoupling-capacitors/get-source-port-display-label.ts new file mode 100644 index 0000000..1df34b1 --- /dev/null +++ b/lib/check-chip-power-pins-have-decoupling-capacitors/get-source-port-display-label.ts @@ -0,0 +1,19 @@ +import type { SourcePort } from "circuit-json" + +const sourcePortLabelIsGenericPinName = (sourcePortLabel: string): boolean => { + const normalizedSourcePortLabel = sourcePortLabel.trim().toLowerCase() + const possiblePinNumber = normalizedSourcePortLabel.startsWith("pin") + ? normalizedSourcePortLabel.slice(3) + : normalizedSourcePortLabel + return ( + possiblePinNumber.length > 0 && + [...possiblePinNumber].every( + (character) => character >= "0" && character <= "9", + ) + ) +} + +export const getSourcePortDisplayLabel = (sourcePort: SourcePort): string => + sourcePort.port_hints?.find( + (sourcePortHint) => !sourcePortLabelIsGenericPinName(sourcePortHint), + ) ?? sourcePort.name diff --git a/lib/check-chip-power-pins-have-decoupling-capacitors/types.ts b/lib/check-chip-power-pins-have-decoupling-capacitors/types.ts new file mode 100644 index 0000000..c803ae6 --- /dev/null +++ b/lib/check-chip-power-pins-have-decoupling-capacitors/types.ts @@ -0,0 +1,4 @@ +import type { SourceComponentBase } from "circuit-json" + +// circuit-json does not currently export a standalone source-component ID type. +export type SourceComponentId = SourceComponentBase["source_component_id"] diff --git a/lib/run-all-checks.ts b/lib/run-all-checks.ts index b398c3e..9817b3d 100644 --- a/lib/run-all-checks.ts +++ b/lib/run-all-checks.ts @@ -1,5 +1,6 @@ import type { AnyCircuitElement } from "circuit-json" import { checkAllPinsInComponentAreUnderspecified } from "./check-all-pins-in-component-are-underspecified" +import { checkChipPowerPinsHaveDecouplingCapacitors } from "./check-chip-power-pins-have-decoupling-capacitors" import { checkConnectorAccessibleOrientation } from "./check-connector-accessible-orientation" import { checkCourtyardOverlap } from "./check-courtyard-overlap/checkCourtyardOverlap" import { checkDifferentNetViaSpacing } from "./check-different-net-via-spacing" @@ -32,7 +33,10 @@ export async function runAllPlacementChecks(circuitJson: AnyCircuitElement[]) { } export async function runAllNetlistChecks(circuitJson: AnyCircuitElement[]) { - return [...checkPinMustBeConnected(circuitJson)] + return [ + ...checkPinMustBeConnected(circuitJson), + ...checkChipPowerPinsHaveDecouplingCapacitors(circuitJson), + ] } export async function runAllPinSpecificationChecks( diff --git a/tests/lib/check-chip-power-pins-have-decoupling-capacitors.test.ts b/tests/lib/check-chip-power-pins-have-decoupling-capacitors.test.ts new file mode 100644 index 0000000..ad68aab --- /dev/null +++ b/tests/lib/check-chip-power-pins-have-decoupling-capacitors.test.ts @@ -0,0 +1,214 @@ +import { expect, test } from "bun:test" +import type { AnyCircuitElement } from "circuit-json" +import { checkChipPowerPinsHaveDecouplingCapacitors } from "lib/check-chip-power-pins-have-decoupling-capacitors" +import { runAllNetlistChecks } from "lib/run-all-checks" + +test("warns only for connected chip power pins missing a decoupling capacitor", async () => { + const circuitJson: AnyCircuitElement[] = [ + { + type: "source_component", + ftype: "simple_chip", + source_component_id: "missing_chip", + name: "U_MISSING", + }, + { + type: "source_component", + ftype: "simple_chip", + source_component_id: "decoupled_chip", + name: "U_WITH_CAP", + }, + { + type: "source_component", + ftype: "simple_chip", + source_component_id: "opt_out_chip", + name: "U_OPT_OUT", + }, + { + type: "source_component", + ftype: "simple_chip", + source_component_id: "power_source_chip", + name: "U_POWER_SOURCE", + }, + { + type: "source_component", + ftype: "simple_chip", + source_component_id: "unconnected_chip", + name: "U_UNCONNECTED", + }, + { + type: "source_component", + ftype: "simple_capacitor", + source_component_id: "decoupling_capacitor", + name: "C1", + capacitance: 1e-7, + }, + { + type: "source_port", + source_port_id: "missing_vcc", + source_component_id: "missing_chip", + name: "pin1", + port_hints: ["pin1", "VCC"], + requires_power: true, + should_have_decoupling_capacitor: true, + recommended_decoupling_capacitor_capacitance: "100nF", + }, + { + type: "source_port", + source_port_id: "missing_gnd", + source_component_id: "missing_chip", + name: "pin2", + port_hints: ["pin2", "GND"], + requires_ground: true, + }, + { + type: "source_port", + source_port_id: "decoupled_vdd", + source_component_id: "decoupled_chip", + name: "VDD", + should_have_decoupling_capacitor: true, + }, + { + type: "source_port", + source_port_id: "decoupled_gnd", + source_component_id: "decoupled_chip", + name: "GND", + requires_ground: true, + }, + { + type: "source_port", + source_port_id: "opt_out_vbat", + source_component_id: "opt_out_chip", + name: "VBAT", + requires_power: true, + should_have_decoupling_capacitor: false, + }, + { + type: "source_port", + source_port_id: "opt_out_gnd", + source_component_id: "opt_out_chip", + name: "GND", + requires_ground: true, + }, + { + type: "source_port", + source_port_id: "power_source_vcc", + source_component_id: "power_source_chip", + name: "VCC", + provides_power: true, + should_have_decoupling_capacitor: false, + }, + { + type: "source_port", + source_port_id: "power_source_gnd", + source_component_id: "power_source_chip", + name: "GND", + provides_ground: true, + }, + { + type: "source_port", + source_port_id: "unconnected_vcc", + source_component_id: "unconnected_chip", + name: "VCC", + requires_power: true, + should_have_decoupling_capacitor: true, + }, + { + type: "source_port", + source_port_id: "unconnected_gnd", + source_component_id: "unconnected_chip", + name: "GND", + requires_ground: true, + }, + { + type: "source_port", + source_port_id: "capacitor_power", + source_component_id: "decoupling_capacitor", + name: "pin1", + }, + { + type: "source_port", + source_port_id: "capacitor_ground", + source_component_id: "decoupling_capacitor", + name: "pin2", + }, + { + type: "source_net", + source_net_id: "ground_net", + name: "GND", + member_source_group_ids: [], + is_ground: true, + }, + { + type: "source_net", + source_net_id: "missing_power_net", + name: "VCC_MISSING", + member_source_group_ids: [], + }, + { + type: "source_net", + source_net_id: "opt_out_power_net", + name: "VBAT", + member_source_group_ids: [], + }, + { + type: "source_net", + source_net_id: "provided_power_net", + name: "VCC_SOURCE", + member_source_group_ids: [], + }, + { + type: "source_trace", + source_trace_id: "ground_trace", + connected_source_port_ids: [ + "missing_gnd", + "decoupled_gnd", + "opt_out_gnd", + "power_source_gnd", + "unconnected_gnd", + "capacitor_ground", + ], + connected_source_net_ids: ["ground_net"], + }, + { + type: "source_trace", + source_trace_id: "missing_power_trace", + connected_source_port_ids: ["missing_vcc"], + connected_source_net_ids: ["missing_power_net"], + }, + { + type: "source_trace", + source_trace_id: "decoupled_power_trace", + connected_source_port_ids: ["decoupled_vdd", "capacitor_power"], + connected_source_net_ids: [], + }, + { + type: "source_trace", + source_trace_id: "opt_out_power_trace", + connected_source_port_ids: ["opt_out_vbat"], + connected_source_net_ids: ["opt_out_power_net"], + }, + { + type: "source_trace", + source_trace_id: "provided_power_trace", + connected_source_port_ids: ["power_source_vcc"], + connected_source_net_ids: ["provided_power_net"], + }, + ] + + const warnings = checkChipPowerPinsHaveDecouplingCapacitors(circuitJson) + + expect(warnings).toEqual([ + { + type: "source_pin_missing_trace_warning", + source_pin_missing_trace_warning_id: + "source_pin_missing_trace_warning_decoupling_missing_vcc", + warning_type: "source_pin_missing_trace_warning", + message: + "Power pin VCC on U_MISSING should have a 100nF decoupling capacitor connected to ground", + source_component_id: "missing_chip", + source_port_id: "missing_vcc", + subcircuit_id: undefined, + }, + ]) + expect(await runAllNetlistChecks(circuitJson)).toContainEqual(warnings[0]) +})