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" };
}
