feat(motors): fix stepper rotation + add A4988 driver (real Fritzing SVG)
The stepper-motor and biaxial-stepper parts only decoded a one-hot wave-drive coil sequence, so they never rotated under the common two-phase full-step / Stepper.h / AccelStepper drive that Wokwi's own examples use -- only the servo moved. Rewrote both decoders to track the net magnetic-field vector of the coils (atan2 of the H-bridge currents), so the rotor follows wave, two-phase full-step and half-step drive alike, whether driven directly from GPIO or through a driver's outputs. Also adds an A4988 STEP/DIR stepper driver (parity with Wokwi's wokwi-a4988): velxio-a4988 element renders the real Pololu A4988 Fritzing breadboard SVG (public/components/a4988.svg); MotorDriverParts.ts finds the wired stepper via the netlist and advances it one (micro)step per STEP rising edge in the DIR direction (MS1-3 microstep + active-low ENABLE). Metadata in component-overrides.json. Three examples (Uno/ESP32/Pico) wire MCU STEP/DIR -> A4988 -> stepper, coil map aligned to Wokwi (1A->B+,1B->B-,2A->A+,2B->A-). Verified in-browser: motor rotates on Arduino Uno (avr8js) and Raspberry Pi Pico (rp2040js). tsc --noEmit clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c66a5b0514
commit
0d5a1d5838
|
|
@ -2333,6 +2333,27 @@
|
|||
"category": "logic",
|
||||
"description": "2-input XOR logic gate. Output Y = A ⊕ B."
|
||||
},
|
||||
{
|
||||
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect width=\"64\" height=\"64\" fill=\"#1a2332\" rx=\"4\"/>\n <rect x=\"16\" y=\"7\" width=\"32\" height=\"50\" rx=\"3\" fill=\"#0b3d2e\" stroke=\"#2d8f5a\" stroke-width=\"0.8\"/>\n <rect x=\"25\" y=\"25\" width=\"14\" height=\"14\" rx=\"1\" fill=\"#15151a\" stroke=\"#333\"/>\n <text x=\"32\" y=\"19\" text-anchor=\"middle\" font-family=\"sans-serif\" font-size=\"6\" fill=\"#cfe8d8\" font-weight=\"bold\">A4988</text>\n <g fill=\"#d4af37\"><rect x=\"11\" y=\"14\" width=\"5\" height=\"2\"/><rect x=\"11\" y=\"22\" width=\"5\" height=\"2\"/><rect x=\"11\" y=\"30\" width=\"5\" height=\"2\"/><rect x=\"11\" y=\"38\" width=\"5\" height=\"2\"/><rect x=\"48\" y=\"14\" width=\"5\" height=\"2\"/><rect x=\"48\" y=\"22\" width=\"5\" height=\"2\"/><rect x=\"48\" y=\"30\" width=\"5\" height=\"2\"/><rect x=\"48\" y=\"38\" width=\"5\" height=\"2\"/></g>\n <text x=\"32\" y=\"51\" text-anchor=\"middle\" font-family=\"sans-serif\" font-size=\"5\" fill=\"#8fbfa6\">STEP/DIR</text></svg>",
|
||||
"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",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 128 KiB |
|
|
@ -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 =
|
||||
`<style>${STYLE}</style>` +
|
||||
`<img src="${SVG_URL}" width="${W}" height="${H}" draggable="false" alt="A4988 stepper driver" />`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!customElements.get('velxio-a4988')) {
|
||||
customElements.define('velxio-a4988', A4988Element);
|
||||
}
|
||||
|
||||
export {};
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
},
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import './BasicParts';
|
|||
import './ComplexParts';
|
||||
import './ChipParts';
|
||||
import './SensorParts';
|
||||
import './MotorDriverParts';
|
||||
import './LogicGateParts';
|
||||
import './ProtocolParts';
|
||||
import './CustomChipPart';
|
||||
|
|
|
|||
|
|
@ -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": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect width=\"64\" height=\"64\" fill=\"#1a2332\" rx=\"4\"/>\n <rect x=\"16\" y=\"7\" width=\"32\" height=\"50\" rx=\"3\" fill=\"#0b3d2e\" stroke=\"#2d8f5a\" stroke-width=\"0.8\"/>\n <rect x=\"25\" y=\"25\" width=\"14\" height=\"14\" rx=\"1\" fill=\"#15151a\" stroke=\"#333\"/>\n <text x=\"32\" y=\"19\" text-anchor=\"middle\" font-family=\"sans-serif\" font-size=\"6\" fill=\"#cfe8d8\" font-weight=\"bold\">A4988</text>\n <g fill=\"#d4af37\"><rect x=\"11\" y=\"14\" width=\"5\" height=\"2\"/><rect x=\"11\" y=\"22\" width=\"5\" height=\"2\"/><rect x=\"11\" y=\"30\" width=\"5\" height=\"2\"/><rect x=\"11\" y=\"38\" width=\"5\" height=\"2\"/><rect x=\"48\" y=\"14\" width=\"5\" height=\"2\"/><rect x=\"48\" y=\"22\" width=\"5\" height=\"2\"/><rect x=\"48\" y=\"30\" width=\"5\" height=\"2\"/><rect x=\"48\" y=\"38\" width=\"5\" height=\"2\"/></g>\n <text x=\"32\" y=\"51\" text-anchor=\"middle\" font-family=\"sans-serif\" font-size=\"5\" fill=\"#8fbfa6\">STEP/DIR</text></svg>"
|
||||
},
|
||||
{
|
||||
"id": "logic-gate-and",
|
||||
"tagName": "velxio-logic-and",
|
||||
|
|
|
|||
Loading…
Reference in New Issue