MustafaMulla29/rp2040-bldc-motor-controller-new

A USB‑C PD–powered RP2040 BLDC motor controller PCB with 3.3 V logic, MCT8329A sensorless commutation, three MOSFET half-bridges, protected power switching, USB data, controls, and motor connectors.

Version
0.3.82
License
unset
Stars
0

scripts/evaluate-u11.mjs

/** Datasheet/DC screening only. No circuit edits, SPICE, or hardware qualification. */
import { readFileSync, writeFileSync } from 'node:fs'
import { createHash } from 'node:crypto'
import assert from 'node:assert/strict'

const pcb = readFileSync('dist/index/circuit.json')
const hash = createHash('sha256').update(pcb).digest('hex')
const baseline = JSON.parse(readFileSync('dist/pd-electrical-review.json'))
assert.equal(hash, baseline.board_sha256, 'Re-review the power topology before reusing this analysis')
const parts = new Map(JSON.parse(pcb).filter(x => x.type === 'source_component').map(x => [x.name, x]))
assert.equal(parts.get('U11').manufacturer_part_number, 'TPS1H100AQPWPRQ1')
assert.equal(parts.get('R13').resistance, .020)
assert.equal(parts.get('D4').manufacturer_part_number, 'SMBJ22A')
for (const name of ['Q1', 'Q2']) assert.equal(parts.get(name).manufacturer_part_number, 'SI4427BDY-T1-E3')

// Unchanged assumptions from the prior sensitivity study, NOT a full worst case.
const otherSeriesOhms = .039 + .020 + .034 // Q1/Q2 at 25 C + shunt + nominal cold fuse
const driverMinimumV = 4.5
const provisionalStopV = 4.65
const candidates = [
  { name: 'TPS1H100-Q1 existing', minInputV: 3.5, hotOhms: .166 * 1.2,
    roomOhms: .120, floorV: 0, disposition: 'Baseline; cannot guarantee the adverse 5 V scenario.' },
  { name: 'TPS259470L screening', minInputV: 2.7, hotOhms: .045, floorV: .0169,
    disposition: 'Not accepted: 28 V absolute maximum is not protected by the present 35.5 V TVS clamp specification; also insufficient 5 V margin. Active current limiting and active-high enable exist, but footprint/support circuitry differ.' },
  { name: 'TPS259480L screening', minInputV: 3.5, hotOhms: .020, floorV: .0245,
    disposition: 'Not accepted: same transient-voltage mismatch and insufficient 5 V margin. Active current limiting and active-high enable exist; new footprint, current-limit resistor, inrush capacitor, OVLO network and routing would be needed.' },
  { name: 'TPS26630 screening', minInputV: 4.5, hotOhms: .053, floorV: 0,
    disposition: 'Not accepted as a robust 5 V fix: local input falls below its own 4.5 V operating minimum in the adverse case. 60 V range, active limiting and SHDN are useful, but do not solve cable drop.' },
  { name: 'Ideal zero-loss U11 (mathematical bound only)', minInputV: 0, hotOhms: 0, floorV: 0,
    disposition: 'Not a part or bypass proposal. Establishes whether ANY passive U11-only substitution can meet the voltage requirement.' },
]
const cases = [
  { name: '5.0 V / short cable / room baseline', sourceV: 5, cableOhms: .1, motorA: .8, logicA: .1, idealDiodeV: .020, room: true },
  { name: '5.0 V / short cable / elevated switch loss', sourceV: 5, cableOhms: .1, motorA: .8, logicA: .1, idealDiodeV: .029 },
  { name: '4.75 V / higher cable resistance / elevated switch loss', sourceV: 4.75, cableOhms: .25, motorA: .8, logicA: .1, idealDiodeV: .029 },
]
const round = x => Number(x.toFixed(6))
const rows = cases.map(s => {
  const cableDropV = (s.motorA + s.logicA) * s.cableOhms
  const inputV = s.sourceV - cableDropV - s.motorA * otherSeriesOhms - s.idealDiodeV
  return {
    ...s, cableDropV: round(cableDropV), U11InputV: round(inputV),
    values: candidates.map(c => {
      const resistance = s.room && c.roomOhms !== undefined ? c.roomOhms : c.hotOhms
      // Approximate ORing regulation as a forward-drop floor. This is a DC
      // screening envelope, not a characterization at our exact 0.8 A load.
      const switchDropV = Math.max(s.motorA * resistance, c.floorV)
      const driverV = inputV - switchDropV
      return { candidate: c.name, switchDropV: round(switchDropV), driverV: round(driverV),
        switchConductionW: round(s.motorA * switchDropV),
        candidateInputWithinRange: inputV >= c.minInputV,
        driverMarginV: round(driverV - driverMinimumV),
        aboveProvisionalStop: driverV >= provisionalStopV }
    }),
  }
})

// Check reproduction of the earlier estimates and the independent ideal bound.
for (let i = 0; i < rows.length; i++)
  assert.ok(Math.abs(rows[i].values[0].driverV - baseline.scenarios[i].estimated_VMOTOR_V) < .0001)
const adverse = rows.at(-1)
assert.equal(adverse.U11InputV, 4.4216)
assert.ok(adverse.values.every(v => v.driverV <= adverse.U11InputV))
assert.ok(adverse.values.every(v => v.driverV < driverMinimumV))

const results = {
  board_sha256: hash,
  decision: 'DO_NOT_REPLACE_U11_ALONE_FOR_GUARANTEED_5V_OPERATION',
  hardware_modified: false,
  scope: 'DC component screening, not measured hardware or a qualified current rating. Candidate maximum RON values use their datasheet test conditions (3 A for TPS25947/48); floor model is approximate. No candidate accepted for installation.',
  assumptions: { otherSeriesOhms, driverMinimumV, provisionalStopV,
    omitted: 'PCB/connector resistance, switch auxiliary current, hot fuse/PMOS increases, ADC errors, ripple, dynamic startup and motor regeneration. Logic 0.1 A is assumed. Each candidate temperature limit differs; these are individual screening bounds, not matched-temperature predictions.' },
  candidates, cases: rows,
  independent_bounds: {
    idealU11DriverV: adverse.U11InputV,
    idealEntireBoardAfterCableV: round(4.75 - (.8 + .1) * .25),
    explanation: 'Even an entirely lossless PCB gives only 4.525 V after this cable, below the provisional 4.65 V stop threshold. A passive switch cannot restore the lost voltage.'
  },
  sources: {
    existing: 'https://www.ti.com/lit/ds/symlink/tps1h100-q1.pdf',
    tps25947: 'https://www.ti.com/lit/ds/symlink/tps25947.pdf',
    tps25948: 'https://www.ti.com/lit/ds/symlink/tps25948.pdf',
    tps2663: 'https://www.ti.com/lit/ds/symlink/tps2663.pdf',
    motorDriver: 'https://www.ti.com/lit/ds/symlink/mct8329a.pdf',
  },
}
writeFileSync('dist/u11-evaluation.json', JSON.stringify(results, null, 2) + '\n')
console.log(JSON.stringify({ decision: results.decision, scenarios: rows.length,
  candidateCases: rows.length * candidates.length, idealU11DriverV: adverse.U11InputV,
  adverseDriverV: adverse.values.map(x => ({ candidate: x.candidate, V: x.driverV })),
  report: 'dist/u11-evaluation.json' }, null, 2))