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/motor-validation-calculations.ts

import {
  motorInputCurrentA,
  inputBudgetReserveFraction,
} from "./electrical-calculations"

export type MotorVoltageV = 5 | 9 | 12

// A7 engineering screen: approved MCU architecture, 6.98 kOhm eFuse implemented.
// Embedded PD transport and physical qualification remain required.
export function reviewMotorContracts({
  motorVoltageV,
  peakMotorCurrentA,
  sourceCapabilities,
}: {
  motorVoltageV: MotorVoltageV
  peakMotorCurrentA: number
  sourceCapabilities: { voltageV: number; currentA: number }[]
}) {
  const nominalInputLimitA = 18_000 / 6_980
  const minimumInputLimitA = (nominalInputLimitA * 0.9) / 1.02
  const maximumInputLimitA = (nominalInputLimitA * 1.1) / 0.98
  const candidates = sourceCapabilities.map((source) => {
    const requiredInputCurrentA = motorInputCurrentA({
      motorVoltageV,
      motorCurrentA: peakMotorCurrentA,
      contractVoltageV: source.voltageV,
    })
    const maximumTotalFaultCurrentA =
      maximumInputLimitA + 1 / (source.voltageV * 0.95)
    const rejectionReasons: string[] = []
    if (source.voltageV !== 15 && source.voltageV !== 20)
      rejectionReasons.push("Only qualified 15/20 V fixed PDOs are allowed")
    if (source.currentA < 3 || source.currentA < maximumTotalFaultCurrentA)
      rejectionReasons.push("Source current is below worst-case fault demand")
    if (!Number.isFinite(source.currentA) || source.currentA > 5)
      rejectionReasons.push(
        "Source current is outside the supported fixed-PDO range",
      )
    if (
      requiredInputCurrentA * (1 + inputBudgetReserveFraction) >
      minimumInputLimitA
    )
      rejectionReasons.push(
        "Peak load plus 5% reserve exceeds minimum input current limit",
      )
    return {
      ...source,
      requestedOperatingCurrentA: 3,
      expectedMaximumCurrentA: source.currentA,
      requiredInputCurrentA,
      reservedInputCurrentA:
        requiredInputCurrentA * (1 + inputBudgetReserveFraction),
      maximumTotalFaultCurrentA,
      rejectionReasons,
      qualified: rejectionReasons.length === 0,
    }
  })
  const selectedContract =
    candidates
      .filter((candidate) => candidate.qualified)
      .sort((left, right) => left.voltageV - right.voltageV)[0] ?? null
  return {
    implementationStatus:
      "MCU qualification wired; embedded transport/prototype validation pending",
    nominalInputLimitA,
    minimumInputLimitA,
    maximumInputLimitA,
    candidates,
    selectedContract,
    motorMustRemainInhibited: selectedContract === null,
  }
}

export function motorTransientEnvelope({
  motorVoltageV,
  windingResistanceOhms,
  windingInductanceH,
  backEmfV,
  currentRegulationDelayS,
  currentLimitA,
}: {
  motorVoltageV: number
  windingResistanceOhms: number
  windingInductanceH: number
  backEmfV: number
  currentRegulationDelayS: number
  currentLimitA: number
}) {
  const reversingVoltageV = motorVoltageV + Math.abs(backEmfV)
  return {
    unrestrictedStartupCurrentA: motorVoltageV / windingResistanceOhms,
    unrestrictedReversalCurrentA: reversingVoltageV / windingResistanceOhms,
    windingEnergyAtCurrentLimitJ: 0.5 * windingInductanceH * currentLimitA ** 2,
    // First-order upper slope ignoring resistance; delay is an explicit
    // assumed total and is not a manufacturer worst-case timing guarantee.
    possibleReversalOvershootA:
      (reversingVoltageV / windingInductanceH) * currentRegulationDelayS,
  }
}

export function capacitorEnergyMarginJ({
  effectiveCapacitanceF,
  initialRailV,
  maximumRailV,
}: {
  effectiveCapacitanceF: number
  initialRailV: number
  maximumRailV: number
}) {
  return 0.5 * effectiveCapacitanceF * (maximumRailV ** 2 - initialRailV ** 2)
}

export function buckInductorPeakA({
  inputVoltageV,
  outputVoltageV,
  loadCurrentA,
  minimumInductanceH,
  minimumSwitchingFrequencyHz,
}: {
  inputVoltageV: number
  outputVoltageV: number
  loadCurrentA: number
  minimumInductanceH: number
  minimumSwitchingFrequencyHz: number
}) {
  const rippleCurrentA =
    ((inputVoltageV - outputVoltageV) * outputVoltageV) /
    inputVoltageV /
    minimumInductanceH /
    minimumSwitchingFrequencyHz
  return loadCurrentA + rippleCurrentA / 2
}

export function railMonitorThresholdsV(branchOhms?: number) {
  const gain = 1 + 105_100 / 20_000 + (branchOhms ? 105_100 / branchOhms : 0)
  const referenceV = 2.495
  // Inactive PNP: R41 + R42 = 20 kOhm between VM and the open output.
  // R40 then feeds OV_REF. No assumed 0.6 V PNP drop while it is off.
  const highFeedbackConductance = 1 / 1_020_000
  const referenceConductance = 1 / 20_000
  const groundConductance = 1 / 11_000
  const tripV =
    (referenceV * referenceConductance) /
    ((referenceConductance + groundConductance + highFeedbackConductance) /
      gain -
      highFeedbackConductance)
  const releaseFeedbackV =
    (referenceV * referenceConductance + 0.2 / 1_000_000) /
    (referenceConductance + groundConductance + 1 / 1_000_000)
  return {
    nominalRailV: 0.8 * gain,
    uvV: ((referenceV * 10_000) / 37_000) * gain,
    ovTripV: tripV,
    ovReleaseV: releaseFeedbackV * gain,
    referenceBiasAtNominalA: (0.8 * gain - referenceV) / 2_000,
    referenceBiasResistorPowerAtTripW: (tripV - referenceV) ** 2 / 2_000,
    instantaneousDumpPowerAtTripW: tripV ** 2 / 5,
  }
}

// A22: eight unchanged FH 10 ohm / 2 W resistors, four series-pair branches.
// A manufacturer short-time overload test is a component screen, not a
// guarantee of unlimited repeated motor braking or finished-board thermals.
export function dumpResistorEnvelope({
  railV,
  ambientC,
  pulseS,
  periodS,
}: {
  railV: number
  ambientC: number
  pulseS: number
  periodS: number
}) {
  if (
    !Number.isFinite(railV) ||
    railV < 0 ||
    !Number.isFinite(ambientC) ||
    ambientC < -55 ||
    ambientC >= 155 ||
    !Number.isFinite(pulseS) ||
    !Number.isFinite(periodS) ||
    pulseS <= 0 ||
    periodS < pulseS
  )
    throw new Error("Invalid regenerative dump envelope")
  // FH L/2512 2 W series: 1% initial + 100 ppm/K x 100 K + 2.5%
  // resistance drift screen (2%R + 0.05 ohm at 10 ohm).
  const resistanceFraction = 0.01 + 100e-6 * 100 + 0.025
  const resistorMinimumOhms = 10 * (1 - resistanceFraction)
  const resistorMaximumOhms = 10 * (1 + resistanceFraction)
  const branchMinimumOhms = 2 * resistorMinimumOhms
  const branchMaximumOhms = 2 * resistorMaximumOhms
  const bankMinimumOhms = branchMinimumOhms / 4
  const bankMaximumOhms = branchMaximumOhms / 4
  // Independent opposing series tolerances maximize voltage on one resistor.
  const worstResistorVoltageV =
    (railV * resistorMaximumOhms) / (resistorMinimumOhms + resistorMaximumOhms)
  // r/(r+s)^2 is largest at r=s=Rmin over the tolerance box.
  const worstResistorPowerW = railV ** 2 / (4 * resistorMinimumOhms)
  const continuousPerResistorW = 2 * Math.min(1, (155 - ambientC) / 85)
  // Manufacturer IEC short-time test: 2.5 x rated VOLTAGE, 2 seconds.
  const overloadPerResistorW = 2 * 2.5 ** 2
  return {
    bankMinimumOhms,
    bankMaximumOhms,
    minimumSinkCurrentA: railV / bankMaximumOhms,
    maximumSinkCurrentA: railV / bankMinimumOhms,
    maximumBankPowerW: railV ** 2 / bankMinimumOhms,
    worstResistorVoltageV,
    worstResistorPowerW,
    worstResistorPulseEnergyJ: worstResistorPowerW * pulseS,
    worstResistorAveragePowerW: (worstResistorPowerW * pulseS) / periodS,
    continuousPerResistorW,
    overloadPerResistorW,
    manufacturerSingleOverloadScreenPasses:
      pulseS <= 2 && worstResistorPowerW <= overloadPerResistorW,
    averageComponentScreenPasses:
      (worstResistorPowerW * pulseS) / periodS <= continuousPerResistorW,
    repetitivePulseHardwareQualification: "pending",
  }
}