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
tests/electrical-calculations.test.ts
import { describe, expect, test } from "bun:test"
import {
chooseMotorContract,
motorInputCurrentA,
pwmTiming,
regulatedMotorVoltageV,
} from "../scripts/electrical-calculations"
describe("PD power qualification", () => {
test("rejects insufficient chargers including native 12 V", () => {
for (const capability of [
{ voltageV: 5, currentA: 3 },
{ voltageV: 9, currentA: 3 },
{ voltageV: 12, currentA: 3 },
{ voltageV: 15, currentA: 2 },
{ voltageV: 20, currentA: 2.25 },
]) {
expect(
chooseMotorContract([capability], { motorVoltageV: 12 }),
).toBeNull()
}
})
test("requires 20 V for 9/12 V peaks with budgeting reserve", () => {
expect(
chooseMotorContract(
[
{ voltageV: 15, currentA: 3 },
{ voltageV: 20, currentA: 3 },
],
{ motorVoltageV: 12 },
),
).toEqual({ voltageV: 20, currentA: 3 })
expect(
chooseMotorContract([{ voltageV: 15, currentA: 3 }], {
motorVoltageV: 9,
}),
).toBeNull()
expect(
chooseMotorContract(
[
{ voltageV: 15, currentA: 3 },
{ voltageV: 20, currentA: 3 },
],
{ motorVoltageV: 9 },
),
).toEqual({ voltageV: 20, currentA: 3 })
expect(
chooseMotorContract([{ voltageV: 15, currentA: 3 }], {
motorVoltageV: 5,
}),
).toEqual({ voltageV: 15, currentA: 3 })
})
test("a 5 A source retains a 3 A operating request and malformed currents fail", () => {
expect(
chooseMotorContract([{ voltageV: 20, currentA: 5 }], {
motorVoltageV: 12,
}),
).toEqual({
voltageV: 20,
currentA: 3,
})
for (const currentA of [5.01, Number.POSITIVE_INFINITY, Number.NaN])
expect(
chooseMotorContract([{ voltageV: 20, currentA }], {
motorVoltageV: 12,
}),
).toBeNull()
})
test("worst assumed 12 V continuous input fits minimum eFuse limit", () => {
const minimumLimitA = ((18_000 / 6_980) * 0.9) / 1.02
expect(
motorInputCurrentA({
motorVoltageV: 12,
motorCurrentA: 2,
contractVoltageV: 20,
}),
).toBeLessThan(minimumLimitA)
// Record the real limitation instead of asserting a fictitious peak margin.
expect(
motorInputCurrentA({
motorVoltageV: 12,
motorCurrentA: 2.4,
contractVoltageV: 15,
}),
).toBeGreaterThan(minimumLimitA)
expect(
motorInputCurrentA({
motorVoltageV: 12,
motorCurrentA: 2.4,
contractVoltageV: 20,
}),
).toBeLessThan(minimumLimitA)
})
})
describe("10 k potentiometer and selected motor rail", () => {
test("regulates all three modes within 0.1 percent nominal", () => {
for (const [branchOhms, voltageV] of [
[undefined, 5],
[21_000, 9],
[12_000, 12],
] as const) {
expect(
Math.abs(regulatedMotorVoltageV(branchOhms) - voltageV) / voltageV,
).toBeLessThan(0.0011)
}
})
test("PWM has useful monotonically increasing range with finite end times", () => {
let previousDuty = 0
for (let step = 0; step <= 100; step++) {
const timing = pwmTiming({ fraction: step / 100 })
expect(timing.frequencyHz).toBeGreaterThan(19_000)
expect(timing.frequencyHz).toBeLessThan(22_000)
expect(timing.dutyFraction).toBeGreaterThan(previousDuty)
previousDuty = timing.dutyFraction
}
expect(pwmTiming({ fraction: 0 }).dutyFraction).toBeLessThan(0.04)
expect(pwmTiming({ fraction: 1 }).dutyFraction).toBeGreaterThan(0.96)
})
})
test("PD-enable default high survives transistor base loading", () => {
// Conservative -2% logic rail, +/-1% resistors, 0.55 V base junction,
// and 5 uA combined leakage. Compare with 0.7*VDD GPIO high threshold.
const supply = 3.3 * 0.98
const pullup = 4_700 * 1.01
const baseResistance = 20_000 * 0.99
const unloadedHigh =
(supply * baseResistance + 0.55 * pullup) / (pullup + baseResistance)
const thevenin = 1 / (1 / pullup + 1 / baseResistance)
expect(unloadedHigh - 5e-6 * thevenin).toBeGreaterThan(supply * 0.7 + 0.3)
const minimumBaseCurrent = (supply - 0.9) / ((4_700 + 20_000) * 1.01)
const maximumCollectorCurrent = (3.3 * 1.02) / (10_000 * 0.99)
expect(minimumBaseCurrent * 10).toBeGreaterThan(maximumCollectorCurrent)
})