imrishabh18/rp2040-motor-controller

An RP2040-based NEMA17 stepper-motor controller with USB-C programming and PD motor-power negotiation, DRV8825 dual H-bridge drive, current/temperature telemetry, protected power filtering, status RGB LED, buzzer alarm, and optional magnetic encoder.

Version
1.0.42
License
unset
Stars
3

tests/helpers/assertTemperatureTelemetry.ts

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

/** Recompute electrical connectivity; never trust cached connectivity keys. */
export function assertTemperatureTelemetry(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"]]) {
    if (component(part!).manufacturer_part_number !== mpn) throw new Error(`Wrong ${part} part`);
  }
  for (const name of ["U_THERM_GATE", "C_THERM_GATE", "R_ENABLE_PD", "R_TEMP_ALERT"]) {
    if (json.some(e => e.type === "source_component" && e.name === name)) throw new Error(`Removed thermal/alert part remains: ${name}`);
  }
  // Physical package pin numbers, independently checked against TI datasheets.
  const groups = [
    // STEP/DIR interface; physical pins checked against TI DRV8825 Rev F.
    [["U1", "GPIO18"], ["DRIVER", "pin22"]],
    [["U1", "GPIO19"], ["DRIVER", "pin20"]],
    [["U1", "GPIO20"], ["DRIVER", "pin16"]],
    [["U1", "GPIO21"], ["DRIVER", "pin21"]],
    [["U_TEMP", "pin6"], ["U1", "GPIO26_ADC0"], ["R_TEMP_SDA", "pin1"]],
    [["U_TEMP", "pin1"], ["U1", "GPIO27_ADC1"], ["R_TEMP_SCL", "pin1"]],
    [["Q_PD_ENABLE", "drain"], ["DRIVER", "pin17"], ["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"], ["L_AVDD", "pin1"],
      ...["C_TEMP"].map(n => [n, "pin1"]),
      ...["R_TEMP_SDA", "R_TEMP_SCL"].map(n => [n, "pin2"])],
    [["DRIVER", "EP"], ["U_TEMP", "pin2"], ["U_TEMP", "pin4"],
      ...["C_TEMP", "R_SLEEP_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);
  // Neither hardware ALERT nor the freed GPIO24 may have any routed connection.
  for (const [name, label] of [["U_TEMP", "pin3"], ["U1", "GPIO24"]]) {
    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}`);
    if (!p.do_not_connect) throw new Error(`Missing explicit no-connect: ${name}.${label}`);
    if (json.some(e => e.type === "source_trace" && e.connected_source_port_ids.includes(p.source_port_id))) {
      throw new Error(`Unused temperature ALERT/GPIO24 is connected: ${name}.${label}`);
    }
  }
  // The PD interlock, I2C and motor-control signals must remain separate.
  const separated = groups.map(group => pin(group[0]![0]!, group[0]![1]!));
  if (new Set(separated).size !== separated.length) throw new Error("Temperature alert/enable short or PD interlock bypass");
  for (const [name, ohms] of [["R_TEMP_SDA", 4700], ["R_TEMP_SCL", 4700],
    ["R_SLEEP_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);
    // The 9.7 mm HTSSOP needs a larger center distance than the old 5 mm IC.
    // 6.25 mm keeps the sensor adjacent to this package, on the same side.
    if (sensorDistanceMm > 6.25 || driver.layer !== sensor.layer) throw new Error("Temperature sensor too far from driver");
  }
  return { i2cAddress: "0x48", sdaGpio: 26, sclGpio: 27, alertGpio: null, unusedGpio: 24, alertPolicy: "RP2040 software using I2C temperature", enableGpio: 22, sensorDistanceMm,
    checkedElectricalGroups: groups.length, hardwareInterlock: "PD-gated MCU enable", autonomousBoardTemperatureShutdown: false, status: "pass" };
}