imrishabh18/rp2040-motor-controller

This circuit module provides a microcontroller-based NEMA17 stepper motor driver with integrated USB-C Power Delivery, temperature sensing, and safety interlocks.

Version
1.0.16
License
unset
Stars
0

tests/helpers/assertThermalProtection.ts

import type { CircuitJson } from "circuit-json";

/** Recompute electrical connectivity; never trust cached connectivity keys. */
export function assertThermalProtection(json: CircuitJson) {
  const parent = new Map<string, string>();
  const root = (id: string): string => {
    if (!parent.has(id)) parent.set(id, id);
    if (parent.get(id) !== id) parent.set(id, root(parent.get(id)!));
    return parent.get(id)!;
  };
  for (const e of json) {
    const ids = e.type === "source_trace"
      ? [...e.connected_source_port_ids, ...e.connected_source_net_ids]
      : e.type === "source_component_internal_connection" ? e.source_port_ids : [];
    for (const id of ids.slice(1)) parent.set(root(id), root(ids[0]!));
  }
  const component = (name: string) => {
    const e = json.find(e => e.type === "source_component" && e.name === name);
    if (e?.type !== "source_component") throw new Error(`Missing ${name}`);
    return e;
  };
  const pin = (name: string, label: string) => {
    const c = component(name);
    const p = json.find(e => e.type === "source_port" && e.source_component_id === c.source_component_id &&
      (e.name === label || e.port_hints?.includes(label)));
    if (p?.type !== "source_port") throw new Error(`Missing ${name}.${label}`);
    return root(p.source_port_id);
  };
  const same = (a: string[], b: string[]) => {
    if (pin(a[0]!, a[1]!) !== pin(b[0]!, b[1]!)) throw new Error(`Disconnected ${a.join('.')} / ${b.join('.')}`);
  };
  for (const [part, mpn] of [["U_TEMP", "TMP102AIDRLR"], ["U_THERM_GATE", "SN74LVC1G08DBVR"]]) {
    if (component(part!).manufacturer_part_number !== mpn) throw new Error(`Wrong ${part} part`);
  }
  // Physical package pin numbers, independently checked against TI datasheets.
  const groups = [
    // Preserve the existing motor firmware pinout through the MCU escape vias.
    [["U1", "GPIO18"], ["DRIVER", "pin16"]],
    [["U1", "GPIO19"], ["DRIVER", "pin15"]],
    [["U1", "GPIO20"], ["DRIVER", "pin9"]],
    [["U1", "GPIO21"], ["DRIVER", "pin10"]],
    [["U_TEMP", "pin6"], ["U1", "GPIO26_ADC0"], ["R_TEMP_SDA", "pin1"]],
    [["U_TEMP", "pin1"], ["U1", "GPIO27_ADC1"], ["R_TEMP_SCL", "pin1"]],
    [["U_TEMP", "pin3"], ["U1", "GPIO24"], ["U_THERM_GATE", "pin2"], ["R_TEMP_ALERT", "pin1"]],
    [["U_THERM_GATE", "pin1"], ["Q_PD_ENABLE", "drain"], ["R_ENABLE_PD", "pin1"]],
    [["U_THERM_GATE", "pin4"], ["DRIVER", "pin1"], ["R_SLEEP_PD", "pin1"]],
    [["U1", "GPIO22"], ["Q_PD_ENABLE", "source"], ["R_MCU_ENABLE_PD", "pin1"]],
    [["Q_PD_ENABLE", "gate"], ["U_PD", "PG"]],
    [["U_TEMP", "pin5"], ["U_THERM_GATE", "pin5"], ["L_AVDD", "pin1"],
      ...["C_TEMP", "C_THERM_GATE"].map(n => [n, "pin1"]),
      ...["R_TEMP_SDA", "R_TEMP_SCL", "R_TEMP_ALERT"].map(n => [n, "pin2"])],
    [["DRIVER", "GND2"], ["U_TEMP", "pin2"], ["U_TEMP", "pin4"], ["U_THERM_GATE", "pin3"],
      ...["C_TEMP", "C_THERM_GATE", "R_ENABLE_PD", "R_MCU_ENABLE_PD"].map(n => [n, "pin2"])],
  ];
  for (const group of groups) for (const other of group.slice(1)) same(group[0]!, other);
  // The gate and transistor must not be bypassed by a stray old trace.
  const separated = groups.map(group => pin(group[0]![0]!, group[0]![1]!));
  if (new Set(separated).size !== separated.length) throw new Error("Thermal interlock bypass or signal/power short");
  for (const [name, ohms] of [["R_TEMP_SDA", 4700], ["R_TEMP_SCL", 4700], ["R_TEMP_ALERT", 10000],
    ["R_ENABLE_PD", 100000], ["R_MCU_ENABLE_PD", 100000]] as const) {
    const c = component(name);
    if (!("resistance" in c) || c.resistance !== ohms) throw new Error(`Wrong ${name} resistance`);
  }
  const pcbFor = (name: string) => json.find(e => e.type === "pcb_component" &&
    e.source_component_id === component(name).source_component_id);
  const driver = pcbFor("DRIVER"), sensor = pcbFor("U_TEMP");
  let sensorDistanceMm: number | null = null;
  if (driver?.type === "pcb_component" && sensor?.type === "pcb_component") {
    sensorDistanceMm = Math.hypot(driver.center.x - sensor.center.x, driver.center.y - sensor.center.y);
    if (sensorDistanceMm > 5 || driver.layer !== sensor.layer) throw new Error("Temperature sensor too far from driver");
  }
  return { i2cAddress: "0x48", sdaGpio: 26, sclGpio: 27, alertGpio: 24, enableGpio: 22, sensorDistanceMm,
    checkedElectricalGroups: groups.length, hardwareInterlock: "PD-gated MCU enable AND TMP102 ALERT", status: "pass" };
}