diff --git a/frontend/public/components-metadata.json b/frontend/public/components-metadata.json index 56fae136..3a295ed5 100644 --- a/frontend/public/components-metadata.json +++ b/frontend/public/components-metadata.json @@ -2333,6 +2333,27 @@ "category": "logic", "description": "2-input XOR logic gate. Output Y = A ⊕ B." }, + { + "thumbnail": "\n \n \n A4988\n \n STEP/DIR", + "tags": [ + "a4988", + "stepper", + "driver", + "motor", + "step", + "dir", + "pololu", + "motors" + ], + "properties": [], + "defaultValues": {}, + "pinCount": 16, + "id": "a4988", + "tagName": "velxio-a4988", + "name": "A4988 Stepper Driver", + "category": "motors", + "description": "A4988 bipolar stepper motor driver (STEP/DIR). Pulse STEP to advance one step; DIR sets the direction; MS1-MS3 select microstepping. Outputs 1A/1B (coil A) and 2A/2B (coil B) to a stepper's A+/A-/B+/B- terminals." + }, { "id": "biaxial-stepper", "tagName": "wokwi-biaxial-stepper", diff --git a/frontend/public/components/a4988.svg b/frontend/public/components/a4988.svg new file mode 100644 index 00000000..794d7502 --- /dev/null +++ b/frontend/public/components/a4988.svg @@ -0,0 +1,1461 @@ + + + + + + + + image/svg+xml + + + + + + + PWR-SNS.svg + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/components/velxio-components/MotorDriverElements.ts b/frontend/src/components/velxio-components/MotorDriverElements.ts new file mode 100644 index 00000000..17e88692 --- /dev/null +++ b/frontend/src/components/velxio-components/MotorDriverElements.ts @@ -0,0 +1,63 @@ +/** + * MotorDriverElements.ts — velxio-a4988 bipolar stepper driver (Pololu-style). + * + * Logic side (left column) takes STEP / DIR (+ EN, MS1-3, RST, SLP) from the + * MCU; the motor side (right column) outputs the two coil pairs 1A/1B (coil A) + * and 2A/2B (coil B) to a bipolar stepper's A+/A-/B+/B- terminals. + * + * The element is purely visual (a DIP-style module). The rotation logic lives + * in simulation/parts/MotorDriverParts.ts: on each STEP rising edge it advances + * the connected wokwi-stepper-motor by one (micro)step in the DIR direction. + * + * Tag: velxio-a4988 (metadataId 'a4988' via stripBrandPrefix). + */ + +const STYLE = ':host{display:inline-block;line-height:0;position:relative}'; + +type Pin = { name: string; x: number; y: number; number: number; signals: string[] }; + +// Pin order matches the Fritzing Pololu A4988 part (pololu0002_a988): +// left column, top -> bottom: ENABLE MS1 MS2 MS3 RESET SLEEP STEP DIR +// right column, top -> bottom: VMOT GND 2B 2A 1A 1B VDD GND +const LEFT = ['ENABLE', 'MS1', 'MS2', 'MS3', 'RESET', 'SLEEP', 'STEP', 'DIR']; +const RIGHT = ['VMOT', 'GND', '2B', '2A', '1A', '1B', 'VDD', 'GND.2']; + +// Render the real Fritzing A4988 carrier SVG (public/components/a4988.svg). +// The board is a uniform 2x8 0.1" header, so the connection points sit on an +// even grid aligned to the two pad columns (margins tuned to the artwork). +const SVG_URL = '/components/a4988.svg'; +const W = 72; +const H = 96; +const TOP_FRAC = 0.115; +const BOT_FRAC = 0.885; +const LEFT_FRAC = 0.085; +const RIGHT_FRAC = 0.915; + +function a4988Pins(): Pin[] { + const pins: Pin[] = []; + const topY = TOP_FRAC * H; + const stepY = ((BOT_FRAC - TOP_FRAC) * H) / (LEFT.length - 1); + LEFT.forEach((name, i) => + pins.push({ name, x: LEFT_FRAC * W, y: topY + i * stepY, number: i + 1, signals: [] }), + ); + RIGHT.forEach((name, i) => + pins.push({ name, x: RIGHT_FRAC * W, y: topY + i * stepY, number: LEFT.length + i + 1, signals: [] }), + ); + return pins; +} + +class A4988Element extends HTMLElement { + readonly pinInfo = a4988Pins(); + constructor() { + super(); + this.attachShadow({ mode: 'open' }).innerHTML = + `` + + `A4988 stepper driver`; + } +} + +if (!customElements.get('velxio-a4988')) { + customElements.define('velxio-a4988', A4988Element); +} + +export {}; diff --git a/frontend/src/data/examples.ts b/frontend/src/data/examples.ts index 6742ff1e..2a9a7c73 100644 --- a/frontend/src/data/examples.ts +++ b/frontend/src/data/examples.ts @@ -1410,6 +1410,150 @@ void loop() { ], tags: ['stm32', 'blue pill', 'gpio', 'stepper', 'motor'], }, + { + id: 'uno-stepper-a4988', + title: 'Arduino Uno: Stepper + A4988', + description: + 'Spin a bipolar stepper motor from an Arduino Uno through an A4988 driver. The MCU only pulses STEP and sets DIR; the A4988 drives the coils. The rotor turns continuously.', + category: 'motors', + difficulty: 'intermediate', + boardFilter: 'arduino-uno', + boards: [ + { + boardKind: 'arduino-uno', + x: 40, + y: 80, + code: `// Arduino Uno + A4988 stepper driver (STEP/DIR) +// D3 -> STEP, D4 -> DIR. One STEP pulse = one step; DIR sets direction. +const int STEP_PIN = 3, DIR_PIN = 4; + +void setup() { + pinMode(STEP_PIN, OUTPUT); + pinMode(DIR_PIN, OUTPUT); + digitalWrite(DIR_PIN, HIGH); // HIGH = clockwise + Serial.begin(115200); + Serial.println("A4988 stepper running"); +} + +void loop() { + digitalWrite(STEP_PIN, HIGH); + delayMicroseconds(800); + digitalWrite(STEP_PIN, LOW); + delayMicroseconds(800); +}`, + }, + ], + code: '', + components: [ + { type: 'velxio-a4988', id: 'drv1', x: 330, y: 70, properties: {} }, + { type: 'wokwi-stepper-motor', id: 'stp1', x: 560, y: 120, properties: {} }, + ], + wires: [ + { id: 'u-step', start: { componentId: 'arduino-uno', pinName: '3' }, end: { componentId: 'drv1', pinName: 'STEP' }, color: '#f59e0b' }, + { id: 'u-dir', start: { componentId: 'arduino-uno', pinName: '4' }, end: { componentId: 'drv1', pinName: 'DIR' }, color: '#10b981' }, + { id: 'u-1a', start: { componentId: 'drv1', pinName: '1A' }, end: { componentId: 'stp1', pinName: 'B+' }, color: '#ff5555' }, + { id: 'u-1b', start: { componentId: 'drv1', pinName: '1B' }, end: { componentId: 'stp1', pinName: 'B-' }, color: '#ff9955' }, + { id: 'u-2a', start: { componentId: 'drv1', pinName: '2A' }, end: { componentId: 'stp1', pinName: 'A+' }, color: '#55aaff' }, + { id: 'u-2b', start: { componentId: 'drv1', pinName: '2B' }, end: { componentId: 'stp1', pinName: 'A-' }, color: '#aa77ff' }, + ], + tags: ['arduino', 'uno', 'stepper', 'motor', 'a4988', 'driver'], + }, + { + id: 'esp32-stepper-a4988', + title: 'ESP32: Stepper + A4988', + description: + 'Spin a bipolar stepper motor from an ESP32 through an A4988 driver (STEP = GPIO26, DIR = GPIO27).', + category: 'motors', + difficulty: 'intermediate', + boardFilter: 'esp32', + boards: [ + { + boardKind: 'esp32', + x: 40, + y: 80, + code: `// ESP32 + A4988 stepper driver (STEP/DIR) +// GPIO26 -> STEP, GPIO27 -> DIR. +const int STEP_PIN = 26, DIR_PIN = 27; + +void setup() { + pinMode(STEP_PIN, OUTPUT); + pinMode(DIR_PIN, OUTPUT); + digitalWrite(DIR_PIN, HIGH); + Serial.begin(115200); + Serial.println("A4988 stepper running"); +} + +void loop() { + digitalWrite(STEP_PIN, HIGH); + delayMicroseconds(800); + digitalWrite(STEP_PIN, LOW); + delayMicroseconds(800); +}`, + }, + ], + code: '', + components: [ + { type: 'velxio-a4988', id: 'drv1', x: 330, y: 70, properties: {} }, + { type: 'wokwi-stepper-motor', id: 'stp1', x: 560, y: 120, properties: {} }, + ], + wires: [ + { id: 'e-step', start: { componentId: 'esp32', pinName: '26' }, end: { componentId: 'drv1', pinName: 'STEP' }, color: '#f59e0b' }, + { id: 'e-dir', start: { componentId: 'esp32', pinName: '27' }, end: { componentId: 'drv1', pinName: 'DIR' }, color: '#10b981' }, + { id: 'e-1a', start: { componentId: 'drv1', pinName: '1A' }, end: { componentId: 'stp1', pinName: 'B+' }, color: '#ff5555' }, + { id: 'e-1b', start: { componentId: 'drv1', pinName: '1B' }, end: { componentId: 'stp1', pinName: 'B-' }, color: '#ff9955' }, + { id: 'e-2a', start: { componentId: 'drv1', pinName: '2A' }, end: { componentId: 'stp1', pinName: 'A+' }, color: '#55aaff' }, + { id: 'e-2b', start: { componentId: 'drv1', pinName: '2B' }, end: { componentId: 'stp1', pinName: 'A-' }, color: '#aa77ff' }, + ], + tags: ['esp32', 'stepper', 'motor', 'a4988', 'driver'], + }, + { + id: 'pico-stepper-a4988', + title: 'Raspberry Pi Pico: Stepper + A4988', + description: + 'Spin a bipolar stepper motor from a Raspberry Pi Pico through an A4988 driver (STEP = GP3, DIR = GP4).', + category: 'motors', + difficulty: 'intermediate', + boardFilter: 'raspberry-pi-pico', + boards: [ + { + boardKind: 'raspberry-pi-pico', + x: 40, + y: 80, + code: `// Raspberry Pi Pico + A4988 stepper driver (STEP/DIR) +// GP3 -> STEP, GP4 -> DIR. +const int STEP_PIN = 3, DIR_PIN = 4; + +void setup() { + pinMode(STEP_PIN, OUTPUT); + pinMode(DIR_PIN, OUTPUT); + digitalWrite(DIR_PIN, HIGH); + Serial.begin(115200); + Serial.println("A4988 stepper running"); +} + +void loop() { + digitalWrite(STEP_PIN, HIGH); + delayMicroseconds(800); + digitalWrite(STEP_PIN, LOW); + delayMicroseconds(800); +}`, + }, + ], + code: '', + components: [ + { type: 'velxio-a4988', id: 'drv1', x: 330, y: 70, properties: {} }, + { type: 'wokwi-stepper-motor', id: 'stp1', x: 560, y: 120, properties: {} }, + ], + wires: [ + { id: 'p-step', start: { componentId: 'raspberry-pi-pico', pinName: 'GP3' }, end: { componentId: 'drv1', pinName: 'STEP' }, color: '#f59e0b' }, + { id: 'p-dir', start: { componentId: 'raspberry-pi-pico', pinName: 'GP4' }, end: { componentId: 'drv1', pinName: 'DIR' }, color: '#10b981' }, + { id: 'p-1a', start: { componentId: 'drv1', pinName: '1A' }, end: { componentId: 'stp1', pinName: 'B+' }, color: '#ff5555' }, + { id: 'p-1b', start: { componentId: 'drv1', pinName: '1B' }, end: { componentId: 'stp1', pinName: 'B-' }, color: '#ff9955' }, + { id: 'p-2a', start: { componentId: 'drv1', pinName: '2A' }, end: { componentId: 'stp1', pinName: 'A+' }, color: '#55aaff' }, + { id: 'p-2b', start: { componentId: 'drv1', pinName: '2B' }, end: { componentId: 'stp1', pinName: 'A-' }, color: '#aa77ff' }, + ], + tags: ['raspberry pi pico', 'rp2040', 'stepper', 'motor', 'a4988', 'driver'], + }, { id: 'blink-led', title: 'Blink LED', diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index cd0ee633..890d42cf 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -13,6 +13,7 @@ import './components/velxio-components/PowerElements'; import './components/velxio-components/DiodeElements'; import './components/velxio-components/RelayElements'; import './components/velxio-components/LogicICElements'; +import './components/velxio-components/MotorDriverElements'; import './components/velxio-components/FlipFlopElements'; import './components/velxio-components/RaspberryPi3Element'; import './components/velxio-components/Bmp280Element'; diff --git a/frontend/src/simulation/parts/BasicParts.ts b/frontend/src/simulation/parts/BasicParts.ts index 570603c5..c0e342ab 100644 --- a/frontend/src/simulation/parts/BasicParts.ts +++ b/frontend/src/simulation/parts/BasicParts.ts @@ -444,22 +444,6 @@ PartSimulationRegistry.register('biaxial-stepper', { const el = element as any; const STEP_ANGLE = 1.8; - // Full-step table: [A+, B+, A-, B-] - const stepTable: [boolean, boolean, boolean, boolean][] = [ - [true, false, false, false], - [false, true, false, false], - [false, false, true, false], - [false, false, false, true], - ]; - - function stepIndexFromCoils(ap: boolean, bp: boolean, am: boolean, bm: boolean): number { - for (let i = 0; i < stepTable.length; i++) { - const [tap, tbp, tam, tbm] = stepTable[i]; - if (ap === tap && bp === tbp && am === tam && bm === tbm) return i; - } - return -1; - } - function makeMotorTracker( pinAminus: number | null, pinAplus: number | null, @@ -472,20 +456,25 @@ PartSimulationRegistry.register('biaxial-stepper', { bPlus = false, bMinus = false; let cumAngle = 0; - let prevIdx = -1; + let prevField = Number.NaN; const unsubs: (() => void)[] = []; + // Rotor follows the net magnetic-field vector of the two coils — works + // for wave, two-phase full-step and half-step drive alike. function onCoilChange() { - const idx = stepIndexFromCoils(aPlus, bPlus, aMinus, bMinus); - if (idx < 0) return; - if (prevIdx < 0) { - prevIdx = idx; + const a = (aPlus ? 1 : 0) - (aMinus ? 1 : 0); + const b = (bPlus ? 1 : 0) - (bMinus ? 1 : 0); + if (a === 0 && b === 0) return; + const field = Math.atan2(b, a); + if (Number.isNaN(prevField)) { + prevField = field; return; } - const diff = (idx - prevIdx + 4) % 4; - if (diff === 1) cumAngle += STEP_ANGLE; - else if (diff === 3) cumAngle -= STEP_ANGLE; - prevIdx = idx; + let delta = field - prevField; + while (delta > Math.PI) delta -= 2 * Math.PI; + while (delta < -Math.PI) delta += 2 * Math.PI; + prevField = field; + cumAngle += (delta / (Math.PI / 2)) * STEP_ANGLE; setAngle(((cumAngle % 360) + 360) % 360); } diff --git a/frontend/src/simulation/parts/MotorDriverParts.ts b/frontend/src/simulation/parts/MotorDriverParts.ts new file mode 100644 index 00000000..c947715f --- /dev/null +++ b/frontend/src/simulation/parts/MotorDriverParts.ts @@ -0,0 +1,112 @@ +/** + * MotorDriverParts.ts — simulation logic for stepper motor drivers. + * + * a4988 — Pololu-style bipolar stepper driver with a STEP/DIR interface. + * The MCU pulses STEP (one rising edge = one step) and sets DIR for the + * direction; MS1/MS2/MS3 select microstepping. The driver's coil outputs + * (1A/1B = coil A, 2A/2B = coil B) wire to a bipolar stepper's A+/A-/B+/B-. + * + * We model the driver, not the coil waveform: on each STEP rising edge we + * rotate the connected wokwi-stepper-motor (or wokwi-biaxial-stepper) by one + * (micro)step in the DIR direction. This matches how real STEP/DIR drivers + * behave and how AccelStepper / the Stepper library drive them. + */ + +import { PartSimulationRegistry } from './PartSimulationRegistry'; +import { useSimulatorStore } from '../../store/useSimulatorStore'; + +/** Find the DOM element wired to `componentId`'s `pinName`, or null. */ +function getConnectedElement(componentId: string, pinName: string): HTMLElement | null { + const { wires } = useSimulatorStore.getState(); + for (const wire of wires) { + let otherId: string | null = null; + if (wire.start.componentId === componentId && wire.start.pinName === pinName) { + otherId = wire.end.componentId; + } else if (wire.end.componentId === componentId && wire.end.pinName === pinName) { + otherId = wire.start.componentId; + } + if (otherId) { + const el = document.getElementById(otherId); + if (el) return el; + } + } + return null; +} + +const STEPPER_TAGS = ['wokwi-stepper-motor', 'wokwi-biaxial-stepper']; +const FULL_STEP_DEG = 1.8; + +PartSimulationRegistry.register('a4988', { + attachEvents: (element, simulator, getArduinoPinHelper, componentId) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const pinManager = (simulator as any).pinManager; + if (!pinManager) return () => {}; + + const pinSTEP = getArduinoPinHelper('STEP'); + const pinDIR = getArduinoPinHelper('DIR'); + const pinEN = getArduinoPinHelper('ENABLE'); + const pinMS1 = getArduinoPinHelper('MS1'); + const pinMS2 = getArduinoPinHelper('MS2'); + const pinMS3 = getArduinoPinHelper('MS3'); + + // Locate the stepper wired to any coil output. + function findMotor(): (HTMLElement & { angle?: number }) | null { + for (const out of ['1A', '1B', '2A', '2B']) { + const el = getConnectedElement(componentId, out); + if (el && STEPPER_TAGS.includes(el.tagName.toLowerCase())) { + return el as HTMLElement & { angle?: number }; + } + } + return null; + } + const motor = findMotor(); + + let dirHigh = false; + let enabled = true; // EN is active-LOW; unconnected = enabled (tied to GND) + let ms1 = false; + let ms2 = false; + let ms3 = false; + let prevStep = false; + let cumAngle = motor ? Number(motor.angle) || 0 : 0; + + function microFactor(): number { + // MS3 MS2 MS1 -> full/half/quarter/eighth/sixteenth (A4988 truth table). + const code = (ms3 ? 4 : 0) | (ms2 ? 2 : 0) | (ms1 ? 1 : 0); + switch (code) { + case 1: return 2; // half + case 2: return 4; // quarter + case 3: return 8; // eighth + case 7: return 16; // sixteenth + default: return 1; // full + } + } + + const unsubs: (() => void)[] = []; + const sub = (pin: number | null, cb: (s: boolean) => void) => { + if (pin !== null) { + unsubs.push(pinManager.onPinChange(pin, (_: number, s: boolean) => cb(s))); + } + }; + + sub(pinDIR, (s) => (dirHigh = s)); + sub(pinEN, (s) => (enabled = !s)); // active-low + sub(pinMS1, (s) => (ms1 = s)); + sub(pinMS2, (s) => (ms2 = s)); + sub(pinMS3, (s) => (ms3 = s)); + + if (pinSTEP !== null) { + unsubs.push( + pinManager.onPinChange(pinSTEP, (_: number, s: boolean) => { + if (s && !prevStep && enabled && motor) { + const stepAngle = FULL_STEP_DEG / microFactor(); + cumAngle += dirHigh ? stepAngle : -stepAngle; + motor.angle = ((cumAngle % 360) + 360) % 360; + } + prevStep = s; + }), + ); + } + + return () => unsubs.forEach((u) => u()); + }, +}); diff --git a/frontend/src/simulation/parts/SensorParts.ts b/frontend/src/simulation/parts/SensorParts.ts index dc1dc392..09d70ccf 100644 --- a/frontend/src/simulation/parts/SensorParts.ts +++ b/frontend/src/simulation/parts/SensorParts.ts @@ -377,46 +377,31 @@ PartSimulationRegistry.register('stepper-motor', { const coils = { aMinus: false, aPlus: false, bPlus: false, bMinus: false }; let cumAngle = el.angle ?? 0; - let prevStepIndex = -1; - - // Full-step table: index → [A+, B+, A-, B-] - const stepTable: [boolean, boolean, boolean, boolean][] = [ - [true, false, false, false], // step 0 - [false, true, false, false], // step 1 - [false, false, true, false], // step 2 - [false, false, false, true], // step 3 - ]; - - function coilToStepIndex(): number { - for (let i = 0; i < stepTable.length; i++) { - const [ap, bp, am, bm] = stepTable[i]; - if ( - coils.aPlus === ap && - coils.bPlus === bp && - coils.aMinus === am && - coils.bMinus === bm - ) { - return i; - } - } - return -1; - } + // Track the magnetic-field electrical angle instead of matching a fixed + // coil table. The rotor follows the net field vector of the two coils, so + // this works for ANY drive mode the firmware (or a driver) produces: + // wave-drive (one coil), full-step two-phase (two coils), or half-step. + let prevField = Number.NaN; // previous field angle in radians, NaN = unset function onCoilChange() { - const idx = coilToStepIndex(); - if (idx < 0) return; - if (prevStepIndex < 0) { - prevStepIndex = idx; + // Coil currents: +1 / 0 / -1 from the H-bridge terminal pair. + const a = (coils.aPlus ? 1 : 0) - (coils.aMinus ? 1 : 0); + const b = (coils.bPlus ? 1 : 0) - (coils.bMinus ? 1 : 0); + if (a === 0 && b === 0) return; // no field → rotor holds position + + const field = Math.atan2(b, a); // electrical angle of the field vector + if (Number.isNaN(prevField)) { + prevField = field; return; } + // Shortest signed rotation between the two field angles. + let delta = field - prevField; + while (delta > Math.PI) delta -= 2 * Math.PI; + while (delta < -Math.PI) delta += 2 * Math.PI; + prevField = field; - const diff = (idx - prevStepIndex + 4) % 4; - if (diff === 1) { - cumAngle += STEP_ANGLE; - } else if (diff === 3) { - cumAngle -= STEP_ANGLE; - } - prevStepIndex = idx; + // One quarter electrical turn (90°, π/2 rad) = one full mechanical step. + cumAngle += (delta / (Math.PI / 2)) * STEP_ANGLE; el.angle = ((cumAngle % 360) + 360) % 360; } diff --git a/frontend/src/simulation/parts/index.ts b/frontend/src/simulation/parts/index.ts index 6224ecb9..dd81dc1d 100644 --- a/frontend/src/simulation/parts/index.ts +++ b/frontend/src/simulation/parts/index.ts @@ -8,6 +8,7 @@ import './BasicParts'; import './ComplexParts'; import './ChipParts'; import './SensorParts'; +import './MotorDriverParts'; import './LogicGateParts'; import './ProtocolParts'; import './CustomChipPart'; diff --git a/scripts/component-overrides.json b/scripts/component-overrides.json index da7ca3c3..ec0cc4a0 100644 --- a/scripts/component-overrides.json +++ b/scripts/component-overrides.json @@ -1,6 +1,18 @@ { "$comment": "Custom property overrides applied AFTER auto-generation from wokwi-elements. Add entries here to customize controls, add new properties, or change defaults. These survive metadata regeneration. Use `_customComponents` to inject Velxio-only parts not defined in wokwi-elements.", "_customComponents": [ + { + "id": "a4988", + "tagName": "velxio-a4988", + "name": "A4988 Stepper Driver", + "category": "motors", + "description": "A4988 bipolar stepper motor driver (STEP/DIR). Pulse STEP to advance one step; DIR sets the direction; MS1-MS3 select microstepping. Outputs 1A/1B (coil A) and 2A/2B (coil B) to a stepper's A+/A-/B+/B- terminals.", + "properties": [], + "defaultValues": {}, + "pinCount": 16, + "tags": ["a4988", "stepper", "driver", "motor", "step", "dir", "pololu", "motors"], + "thumbnail": "\n \n \n A4988\n \n STEP/DIR" + }, { "id": "logic-gate-and", "tagName": "velxio-logic-and",