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/current-telemetry.test.tsx

import { expect, test } from "bun:test";
import { Circuit } from "tscircuit";
import Board from "../index.circuit";

test("two inline current sensors, safe ADC nets, RGB and independent voltage probes", async () => {
  const circuit = new Circuit({platform:{pcbDisabled:true}});
  circuit.add(<Board routingDisabled/>);
  await circuit.renderUntilSettled();
  const json:any[]=circuit.getCircuitJson();
  const parent=new Map<string,string>();
  const root=(id:string):string=>{if(!parent.has(id))parent.set(id,id);const p=parent.get(id)!;if(p!==id)parent.set(id,root(p));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 comp=(name:string)=>{const c=json.find(e=>e.type==='source_component'&&e.name===name);if(!c)throw new Error(name);return c};
  const net=(name:string,pin:string)=>{const c=comp(name);const p=json.find(e=>e.type==='source_port'&&e.source_component_id===c.source_component_id&&(e.name===pin||e.port_hints?.includes(pin)));if(!p)throw new Error(name+'.'+pin);return root(p.source_port_id)};
  const common=net('U1','GND'),v33=net('L_AVDD','pin1');
  const currentNets:string[]=[];
  for(const [phase,i] of [['A',0],['B',1]] as const){
    const amp=`U_CURRENT_${phase}`,shunt=`R_PHASE_${phase}`;
    // Check physical pin numbers, independent of imported aliases.
    expect(net(amp,'pin8')).toBe(net(shunt,'pin1'));
    expect(net(amp,'pin1')).toBe(net(shunt,'pin2'));
    expect(net(shunt,'pin1')).toBe(net('DRIVER',`${phase}OUT1`));
    expect(net(shunt,'pin2')).toBe(net('J1',`pin${i*2+1}`));
    expect(comp(shunt).resistance).toBe(0.05);
    expect(comp(shunt).supplier_part_numbers.jlcpcb).toEqual(["C7467249"]);
    expect(comp(amp).manufacturer_part_number).toBe("INA240A1DR");
    expect(comp(amp).supplier_part_numbers.jlcpcb).toEqual(["C2060769"]);
    expect(comp(`R_ISEN_${phase}`).resistance).toBe(0.3);
    expect(net(amp,'pin5')).toBe(net(`R_ADC_${phase}`,'pin1'));
    const adc=net('U1',`GPIO${28+i}_ADC${2+i}`);
    expect(adc).toBe(net(`R_ADC_${phase}`,'pin2'));
    expect(adc).toBe(net(`C_ADC_${phase}`,'pin1'));
    for(const pin of ['pin2','pin3','pin4'])expect(net(amp,pin)).toBe(common);
    for(const pin of ['pin6','pin7'])expect(net(amp,pin)).toBe(v33);
    expect(net(`C_CURRENT_${phase}`,'pin1')).toBe(v33);
    expect(net(`C_CURRENT_${phase}`,'pin2')).toBe(common);
    expect(net(`C_ADC_${phase}`,'pin2')).toBe(common);
    currentNets.push(adc,net(amp,'pin5'),net(shunt,'pin1'),net(shunt,'pin2'));
  }
  expect(new Set([...currentNets,common,v33]).size).toBe(10);
  // CH224K power feed remains sized for a 20 V request. GPIO selection has its own regression test.
  expect(comp('R_PD_VDD').resistance).toBe(1000);
  expect(comp('R_PD_VDD').manufacturer_part_number).toBe('CRCW12061K00FKEAHP');
  expect(comp('R_PD_VDD').supplier_part_numbers.jlcpcb).toEqual(['C844867']);
  expect(comp('D_MOTOR_TVS').manufacturer_part_number).toBe('SMBJ22CAQ-13-F');
  expect(comp('D_MOTOR_TVS').supplier_part_numbers.jlcpcb).toEqual(['C1975329']);
  expect(comp('DRIVER').manufacturer_part_number).toBe('DRV8825PWPR');
  expect(json.some(e => e.type === 'source_component' && /TMC2209/.test(e.manufacturer_part_number ?? ''))).toBe(false);
  expect(net('C_MOTOR_BULK','pin1')).toBe(net('DRIVER','VM'));
  expect(net('C_MOTOR_BULK','pin2')).toBe(common);
  const raw=net('F_MOTOR_VBUS','pin1'),protectedRail=net('D_MOTOR_BLOCK','cathode');
  // Supplier pin 1 is the cathode on both polarized parts; the rail and
  // ground connections must follow those physical pin numbers.
  expect(net('D_MOTOR_TVS','pin1')).toBe(protectedRail);
  expect(net('D_MOTOR_TVS','pin2')).toBe(common);
  expect(net('D_PWR','pin1')).toBe(common);
  expect(net('D_PWR','pin2')).toBe(net('R_PWR_LED','pin2'));
  expect(net('D_MOTOR_BLOCK','anode')).toBe(net('F_MOTOR_VBUS','pin2'));
  expect(net('D_MOTOR_BLOCK','anode')).not.toBe(protectedRail);
  expect(net('TP_PD','pin1')).toBe(raw);
  expect(net('TP_VMOTOR','pin1')).toBe(protectedRail);
  expect(net('TP_PD_GND','pin1')).toBe(common);
  expect(new Set([raw,protectedRail,common,v33,...currentNets]).size).toBe(12);
  expect(net('D_STATUS','pin4')).toBe(net('J_USB','A4B9'));
  expect(net('D_STATUS','pin4')).not.toBe(raw);
  for(const [color,i] of [['R',0],['G',1],['B',2]] as const){
    expect(net(`Q_STATUS_${color}`,'pin1')).toBe(common);
    expect(net(`Q_STATUS_${color}`,'pin2')).toBe(net('U1',`GPIO${[25,2,3][i]}`));
    expect(net(`Q_STATUS_${color}`,'pin3')).toBe(net(`R_STATUS_${color}`,'pin2'));
    expect(net(`R_STATUS_${color}`,'pin1')).toBe(net('D_STATUS',`pin${i+1}`));
    expect(comp(`R_STATUS_${color}`).resistance).toBe(1000);
  }
  expect(net("U_ENCODER", "SDA")).toBe(net("U1", "GPIO0"));
  expect(net("U_ENCODER", "SCL")).toBe(net("U1", "GPIO1"));
},60000);

test("each sense lead terminates directly at its shunt pad without a via", async () => {
  const circuit = new Circuit();
  circuit.add(<Board routingDisabled/>);
  await circuit.renderUntilSettled();
  const json:any[]=circuit.getCircuitJson();
  for(const phase of ['A','B']) for(const polarity of ['POS','NEG']) {
    const source = json.find(e=>e.type==='source_trace'&&e.name===`CURRENT_${phase}_SENSE_${polarity}`);
    const copper = json.filter(e=>e.type==='pcb_trace'&&e.source_trace_id===source.source_trace_id);
    expect(copper).toHaveLength(1);
    const route = copper[0].route;
    expect(route.every((p:any)=>p.route_type==='wire'&&p.layer==='top')).toBe(true);
    expect(route.length).toBeGreaterThanOrEqual(2);
    const first=route[0],last=route.at(-1);
    const r = json.find(e=>e.type==='source_component'&&e.name===`R_PHASE_${phase}`);
    const sp = json.find(e=>e.type==='source_port'&&e.source_component_id===r.source_component_id&&e.pin_number===(polarity==='POS'?1:2));
    const pp = json.find(e=>e.type==='pcb_port'&&e.source_port_id===sp.source_port_id);
    expect(first.start_pcb_port_id).toBe(pp.pcb_port_id);
    expect(first.x).toBeCloseTo(pp.x,6);
    expect(first.y).toBeCloseTo(pp.y,6);
    const length=route.slice(1).reduce((sum:number,p:any,i:number)=>sum+Math.hypot(p.x-route[i].x,p.y-route[i].y),0);
    expect(length).toBeLessThan(6);
  }
},60000);