AnasSarkiz/usb-c-pd-brushed-motor

USB-C PD-powered motor controller with protected input, selectable 5/9/12 V buck supply, MCU qualification, PWM H-bridge drive, overvoltage sensing, regenerative energy-dump resistors, and safety interlocks.

Version
0.0.1-a48-cloud-pipeline9-unrouted-c41d79a-305b1e7a
License
unset
Stars
0

scripts/power-prototype-envelope.ts

import {
  dumpResistorEnvelope,
  reviewMotorContracts,
} from "./motor-validation-calculations"

/** Static tolerance screen; rail gain is shared with the buck feedback loop.
 * Comparator input current directions are widened deliberately to ±400nA.
 * Reference bounds include 1% initial, 34mV full-temperature deviation and
 * 5mV cathode-current allowance. Resistors reserve 1% initial +1% TCR.
 */
export function analogRailCorners(branchOhms?: number) {
  const limits = {
    uvFbMin: Infinity,
    uvFbMax: -Infinity,
    ovFbMin: Infinity,
    ovFbMax: -Infinity,
    tripMin: Infinity,
    tripMax: -Infinity,
    releaseMin: Infinity,
    releaseMax: -Infinity,
    minHysteresis: Infinity,
    minReferenceBiasA: Infinity,
  }
  const nominal = [
    105100,
    20000,
    branchOhms ?? Infinity,
    27000,
    10000,
    20000,
    11000,
    1000000,
    20000,
  ]
  for (let mask = 0; mask < 2 ** 9; mask++) {
    const r = nominal.map(
      (resistance, index) => resistance * (mask & (1 << index) ? 1.02 : 0.98),
    )
    const [
      top,
      bottom,
      branch,
      uvTop,
      uvBottom,
      ovTop,
      ovBottom,
      hysteresis,
      pullup,
    ] = r
    const gain = 1 + top / bottom + top / branch
    const fbResistance = 1 / (1 / top + 1 / bottom + 1 / branch)
    for (const ref of [2.47 - 0.034 - 0.005, 2.52 + 0.034 + 0.005])
      for (const offset of [-0.009, 0.009])
        for (const bias of [-400e-9, 400e-9])
          for (const fbBias of [-900e-9, 900e-9])
            for (const outputLowV of [0, 0.7]) {
              const uv =
                (ref * uvBottom) / (uvTop + uvBottom) +
                offset +
                bias / (1 / uvTop + 1 / uvBottom)
              const conductance =
                1 / ovTop + 1 / ovBottom + 1 / (hysteresis + pullup)
              const feedbackErrorV = fbBias * fbResistance
              const trip =
                (ref / ovTop +
                  bias +
                  offset * conductance -
                  feedbackErrorV * conductance) /
                (conductance / gain - 1 / (hysteresis + pullup))
              const release =
                (ref / ovTop + outputLowV / hysteresis + bias) /
                  (1 / ovTop + 1 / ovBottom + 1 / hysteresis) +
                offset -
                feedbackErrorV
              const uvFb = uv - feedbackErrorV
              const tripFb = trip / gain
              limits.uvFbMin = Math.min(limits.uvFbMin, uvFb)
              limits.uvFbMax = Math.max(limits.uvFbMax, uvFb)
              limits.ovFbMin = Math.min(limits.ovFbMin, tripFb)
              limits.ovFbMax = Math.max(limits.ovFbMax, tripFb)
              limits.tripMin = Math.min(limits.tripMin, trip)
              limits.tripMax = Math.max(limits.tripMax, trip)
              limits.releaseMin = Math.min(limits.releaseMin, release * gain)
              limits.releaseMax = Math.max(limits.releaseMax, release * gain)
              limits.minHysteresis = Math.min(
                limits.minHysteresis,
                trip - release * gain,
              )
              // Reference bias is valid only once the independently qualified rail minimum is reached.
              const minimumQualifiedVmV =
                branchOhms === undefined
                  ? 4.75
                  : branchOhms === 21000
                    ? 8.55
                    : 11.4
              const biasSupply =
                (minimumQualifiedVmV - ref) / 2040 - ref / 26460 - ref / 19600
              limits.minReferenceBiasA = Math.min(
                limits.minReferenceBiasA,
                biasSupply,
              )
            }
  }
  return {
    ...limits,
    cases: 512 * 32,
    referenceMinBiasRequiredA: 0.0007,
    commonModeMaximumInputV: 1.1,
    minimumQualifiedRailV:
      branchOhms === undefined ? 4.75 : branchOhms === 21000 ? 8.55 : 11.4,
    uvBelowBuckMinimumFb: limits.uvFbMax < 0.788,
    ovAboveBuckMaximumFb: limits.ovFbMin > 0.812,
    hysteresisPositive: limits.minHysteresis > 0,
    referenceBiasScreen: limits.minReferenceBiasA >= 0.0007,
    status:
      "conditional component-bound screen; no response-delay maximum guaranteed",
  }
}

/** Loss allowances are requirements for the routed prototype and thermal tests,
 * not guaranteed hot values or use of datasheet θJA as our board's θJA.
 */
export function prototypeThermalRequirements(motorVoltageV: 5 | 9 | 12) {
  const source = reviewMotorContracts({
    motorVoltageV,
    peakMotorCurrentA: 2.423,
    sourceCapabilities: [
      { voltageV: 15, currentA: 3 },
      { voltageV: 20, currentA: 3 },
    ],
  }).selectedContract
  if (!source) throw new Error("No acceptable prototype source")
  const inputVoltageV = source.voltageV - 0.55
  const duty = motorVoltageV / inputVoltageV
  const outputCurrentA = 2
  const rippleA = ((inputVoltageV - motorVoltageV) * duty) / (6.56e-6 * 480000)
  const inductorRmsSquared = outputCurrentA ** 2 + rippleA ** 2 / 12
  const losses = {
    buck:
      0.19 * inductorRmsSquared * duty +
      (inputVoltageV * outputCurrentA * 100e-9 * 600000) / 2 +
      0.03,
    catchDiode: 0.55 * outputCurrentA * (1 - duty),
    inductorCopper: 0.03 * inductorRmsSquared,
    efuse: 0.045 * source.requiredInputCurrentA ** 2,
    seriesDiode: 0.55 * source.requiredInputCurrentA,
    bridge: 0.36 * outputCurrentA ** 2,
    ldos:
      (source.voltageV * 1.05 - 3.3) * 0.015 +
      (source.voltageV * 1.05 - 5) * 0.005,
    bleeder: (motorVoltageV * 1.05) ** 2 / 940,
  }
  return {
    motorVoltageV,
    contractVoltageV: source.voltageV,
    ambientMaximumC: 40,
    junctionScreenC: 125,
    assumptions:
      "buck100ns switching edge; bridge0.36ohm hot; diode0.55V;15mA3V3/5mA5V aggregate; inductor core loss absent; measured effective θ/efficiency required",
    lossesW: losses,
    maximumEffectiveThetaCPerW: Object.fromEntries(
      Object.entries(losses).map(([part, lossW]) => [part, 85 / lossW]),
    ),
    totalSemiconductorAllowanceW: Object.values(losses).reduce(
      (sum, loss) => sum + loss,
      0,
    ),
    continuous2AStatus: "target; not thermally qualified",
  }
}
export function prototypeRegenScreen() {
  const railCeilingV = 18
  const pulseS = 0.02
  const periodS = 10
  return {
    railCeilingV,
    pulseS,
    periodS,
    energyCeilingJ: 0.5,
    initialMotorEnergyCeilingJ: 0.001,
    initialCapacitorOnlyMaximumV: Math.sqrt(12.6 ** 2 + (2 * 0.001) / 264e-6),
    bank: dumpResistorEnvelope({
      railV: railCeilingV,
      ambientC: 40,
      pulseS,
      periodS,
    }),
    minimumBankCurrentAtCeilingA: railCeilingV / 5.225,
    mosfetFullyOnLossW: (railCeilingV / 4.775) ** 2 * 0.06,
    mosfetHotResistanceAssumptionOhms: 0.06,
    capacitorEnergyUntilDumpJ: 0.5 * 264e-6 * (railCeilingV ** 2 - 12.6 ** 2),
    status:
      "conditional expanded test ceiling only after measured clamp timing/SOA; initial motor stored energy <=1mJ; no repetitive braking guarantee",
    exclusions: [
      "externally powered back-drive",
      "unmeasured motor inertia",
      "operation beyond20ms dump event",
      "TVS clamp as steady voltage regulation",
      "unqualified5V current regulation",
    ],
  }
}