Private/Stepper.Backend.ps1
|
#Requires -Version 5.1 # --------------------------------------------------------------------------- # Stepper.Backend.ps1 # Platform-agnostic stepper motor backend for PsGadget. # # Supports FT232R and FT232H via async bit-bang mode (ADBUS0-3). # Reduces jitter by pre-computing the full step sequence as a byte buffer # and issuing a single bulk USB write, letting the chip's built-in baud-rate # timer pace each coil state at the requested step rate. # # Coil byte layout (lower nibble, IN1-IN4 via ULN2003 driver board): # bit 0 = IN1 bit 1 = IN2 bit 2 = IN3 bit 3 = IN4 # # 28BYJ-48 calibration note: # Empirical measurements: ~508-509 output steps/rev. # Gear-ratio derivation: 4075.7728 / 8 = ~509.47 half-step blocks/rev # Source: http://www.jangeox.be/2013/10/stepper-motor-28byj-48_25.html # This value is NOT exactly 4096. Do NOT hardcode 2048/4096. # Use Get-PsGadgetStepperDefaultStepsPerRev for mode-appropriate defaults, # or pass a -StepsPerRevolution override when the motor has been calibrated. # --------------------------------------------------------------------------- # Module-level calibration constant. Expose via Get-PsGadgetStepperDefaultStepsPerRev. $script:Stepper_HalfStepsPerRev_28BYJ48 = 4075.7728395061727 # --------------------------------------------------------------------------- # Get-PsGadgetStepperDefaultStepsPerRev # Returns the calibrated default steps-per-revolution for the specified step # mode. For 28BYJ-48: # Half: ~4075.77 individual half-step pulses per output-shaft revolution # Full: ~2037.89 (half of the above; each full-step moves twice as far) # # Pass your measured value via -StepsPerRevolution to Invoke-PsGadgetStepper # or set $dev.StepsPerRevolution on the PsGadgetFtdi object to override. # --------------------------------------------------------------------------- function Get-PsGadgetStepperDefaultStepsPerRev { [CmdletBinding()] [OutputType([double])] param( [ValidateSet('Full', 'Half')] [string]$StepMode = 'Half' ) if ($StepMode -eq 'Full') { return ($script:Stepper_HalfStepsPerRev_28BYJ48 / 2.0) } return $script:Stepper_HalfStepsPerRev_28BYJ48 } # --------------------------------------------------------------------------- # Get-PsGadgetStepSequence # Returns the ordered phase byte sequence for the given step mode and # direction. Each byte encodes the coil state for one step position. # # PinOffset shifts all bytes left by N bits to accommodate motors wired to # higher ADBUS pins (e.g. PinOffset=4 maps IN1-IN4 to bits 4-7). # --------------------------------------------------------------------------- function Get-PsGadgetStepSequence { [CmdletBinding()] [OutputType([byte[]])] param( [ValidateSet('Full', 'Half')] [string]$StepMode = 'Half', [ValidateSet('Forward', 'Reverse')] [string]$Direction = 'Forward', [ValidateRange(0, 4)] [byte]$PinOffset = 0 ) # Half-step 8-phase (smoother, higher resolution, default for 28BYJ-48) # Drives single coils then adjacent coil pairs in rotation. # Phase order: IN4, IN4+IN3, IN3, IN3+IN2, IN2, IN2+IN1, IN1, IN1+IN4 $halfStep = [byte[]]@(0x08, 0x0C, 0x04, 0x06, 0x02, 0x03, 0x01, 0x09) # Full-step 4-phase (two coils energised simultaneously - higher torque) # Phase order: IN1+IN3, IN2+IN3, IN2+IN4, IN1+IN4 $fullStep = [byte[]]@(0x05, 0x06, 0x0A, 0x09) [byte[]]$seq = if ($StepMode -eq 'Half') { $halfStep } else { $fullStep } if ($Direction -eq 'Reverse') { [System.Array]::Reverse($seq) } if ($PinOffset -gt 0) { $shifted = [byte[]]::new($seq.Length) for ($i = 0; $i -lt $seq.Length; $i++) { $shifted[$i] = [byte](($seq[$i] -shl $PinOffset) -band 0xFF) } return $shifted } return $seq } # --------------------------------------------------------------------------- # Invoke-PsGadgetStepperMove # Core step dispatch. Called by Invoke-PsGadgetStepper and # PsGadgetFtdi.Step() / PsGadgetFtdi.StepDegrees(). # # Jitter-reduction strategy: # 1. Build the complete step sequence as a contiguous byte[] ($Steps entries) # 2. Configure FT232R/FT232H async bit-bang mode (ADBUS0-3) # 3. Set baud rate = 16000 / DelayMs so the baud-rate timer paces each # pin-state transition at the requested interval # 4. Issue a single FT_Write() call with the full buffer # 5. De-energize coils (write 0x00) to prevent heat buildup at rest # # On stub/no-hardware machines the write is logged but not executed. # --------------------------------------------------------------------------- function Invoke-PsGadgetStepperMove { [CmdletBinding(SupportsShouldProcess = $false)] param( [Parameter(Mandatory = $true)] [object]$Ftdi, [Parameter(Mandatory = $true)] [ValidateRange(1, [int]::MaxValue)] [int]$Steps, [ValidateSet('Forward', 'Reverse')] [string]$Direction = 'Forward', [ValidateSet('Full', 'Half')] [string]$StepMode = 'Half', # Inter-step delay in milliseconds. # Translates to baud rate: baud = 16000 / DelayMs # Minimum recommended for 28BYJ-48: 1ms (may stall at higher speeds) # Safe default: 2ms [ValidateRange(1, 1000)] [int]$DelayMs = 2, # Output direction mask. Bits correspond to the target GPIO bank pins. # Default 0x0F = pins 0-3 all outputs (IN1-IN4 via ULN2003). [byte]$PinMask = 0x0F, # Shift IN1-IN4 bits left by this many positions. # Use when motor is wired on upper ADBUS pins (D4-D7). [ValidateRange(0, 4)] [byte]$PinOffset = 0, # Use ACBUS (C-bank) instead of ADBUS (D-bank) for the MPSSE step writes. # ADBUS (default): SET_BITS_LOW (0x80) -- pins D0-D7 # ACBUS: SET_BITS_HIGH (0x82) -- pins C0-C7 (FT232H only) # Required when stepper is wired to ACBUS C0-C3 alongside I2C on ADBUS D0/D1. [switch]$AcBus ) $log = $Ftdi.Logger $bankLabel = if ($AcBus) { 'ACBUS' } else { 'ADBUS' } $log.WriteDebug("StepperMove: $Steps $StepMode steps / $Direction / delay=${DelayMs}ms / mask=0x$($PinMask.ToString('X2')) / offset=$PinOffset / bank=$bankLabel") # --- build phase sequence --- $seq = Get-PsGadgetStepSequence -StepMode $StepMode -Direction $Direction -PinOffset $PinOffset $seqLen = $seq.Length $buf = [byte[]]::new($Steps) for ($i = 0; $i -lt $Steps; $i++) { $buf[$i] = [byte]($seq[$i % $seqLen] -band $PinMask) } $log.WriteTrace("StepperMove: phase buffer $Steps bytes built") $conn = $Ftdi._connection $gpioMethod = if ($conn -and $conn.PSObject.Properties['GpioMethod']) { $conn.GpioMethod } else { '' } # --- write per-step loop --- # Two hardware paths: # # MPSSE (FT232H default): three-byte MPSSE command per step, no mode switch needed. # ADBUS (default, -AcBus not set): SET_BITS_LOW (0x80, value, direction) # D0-D3 reserved for MPSSE I2C; use D4-D7 with PinOffset=4 / PinMask=0xF0. # ACBUS (-AcBus switch): SET_BITS_HIGH (0x82, value, direction) # C0-C7 independent of ADBUS; use when stepper is on C0-C3 alongside I2C. # Both keep the device in MPSSE mode; I2C (SSD1306) continues to work. # Reference: FTDI AN_108 section 3.6.1 (SET_DATA_BITS_LOW_BYTE / HIGH_BYTE). # # AsyncBitBang (FT232R or explicit override): 1-byte direct pin state write. # Requires prior SetBitMode(AsyncBitBang). Mode switch handled below. # # Both paths use a Stopwatch spin-wait instead of Start-Sleep. # Start-Sleep has a ~15ms minimum granularity on Windows; the spin-wait # achieves sub-millisecond accuracy at the cost of one CPU core spinning # for the duration of the move. if ($conn -and $conn.PSObject.Properties['Device'] -and $conn.Device) { $sw = [System.Diagnostics.Stopwatch]::new() $targetTicks = [long]($DelayMs * ([System.Diagnostics.Stopwatch]::Frequency / 1000.0)) if ($gpioMethod -eq 'MPSSE' -or $gpioMethod -eq 'MpsseI2c') { # MPSSE GPIO path. Command byte selects the GPIO bank: # 0x80 (SET_BITS_LOW) = ADBUS D0-D7 (default) # 0x82 (SET_BITS_HIGH) = ACBUS C0-C7 (-AcBus) # Direction byte = PinMask (bits in mask are outputs). # No mode switch; device remains MPSSE-capable for I2C/SSD1306 after call. $mpsseCmdByte = if ($AcBus) { [byte]0x82 } else { [byte]0x80 } $log.WriteInfo("StepperMove: MPSSE $bankLabel path, $Steps steps @ ${DelayMs}ms") $mpsseCmd = [byte[]]@($mpsseCmdByte, 0x00, $PinMask) try { for ($i = 0; $i -lt $Steps; $i++) { $mpsseCmd[1] = $buf[$i] [uint32]$written = 0 $sw.Restart() $conn.Device.Write($mpsseCmd, 3, [ref]$written) | Out-Null while ($sw.ElapsedTicks -lt $targetTicks) {} } $log.WriteInfo("StepperMove: completed $Steps steps (MPSSE/$bankLabel)") } catch [System.NotImplementedException] { $log.WriteTrace("StepperMove stub: FT_Write not implemented (no hardware)") } catch { $log.WriteError("StepperMove FT_Write error: $($_.Exception.Message)") throw } # De-energize: set all coil pins low, keep direction mask try { $mpsseCmd[1] = 0x00 [uint32]$zw = 0 $conn.Device.Write($mpsseCmd, 3, [ref]$zw) | Out-Null $log.WriteTrace("StepperMove: coils de-energized (MPSSE)") } catch { $log.WriteTrace("StepperMove de-energize stub: $($_.Exception.Message)") } } else { # AsyncBitBang path (FT232R or devices not in MPSSE mode). # Switch mode if needed; FT232H opened in MPSSE requires a reset first. $activeMode = if ($conn.PSObject.Properties['ActiveMode']) { $conn.ActiveMode } else { '' } if ($activeMode -ne 'AsyncBitBang') { $log.WriteInfo("StepperMove: switching to AsyncBitBang (was '$activeMode')") try { $conn.Device.ResetDevice() | Out-Null $log.WriteTrace("StepperMove: ResetDevice before mode switch") Start-Sleep -Milliseconds 50 $conn.Device.Purge(3) | Out-Null Start-Sleep -Milliseconds 10 } catch { $log.WriteTrace("StepperMove: ResetDevice/Purge not available: $($_.Exception.Message)") } Set-PsGadgetFtdiMode -PsGadget $Ftdi -Mode AsyncBitBang -Mask $PinMask | Out-Null } $log.WriteInfo("StepperMove: AsyncBitBang path, $Steps steps @ ${DelayMs}ms") $stepBuf = [byte[]]@(0x00) try { for ($i = 0; $i -lt $Steps; $i++) { $stepBuf[0] = $buf[$i] [uint32]$written = 0 $sw.Restart() $conn.Device.Write($stepBuf, 1, [ref]$written) | Out-Null while ($sw.ElapsedTicks -lt $targetTicks) {} } $log.WriteInfo("StepperMove: completed $Steps steps (AsyncBitBang)") } catch [System.NotImplementedException] { $log.WriteTrace("StepperMove stub: FT_Write not implemented (no hardware)") } catch { $log.WriteError("StepperMove FT_Write error: $($_.Exception.Message)") throw } try { $stepBuf[0] = 0x00 [uint32]$zw = 0 $conn.Device.Write($stepBuf, 1, [ref]$zw) | Out-Null $log.WriteTrace("StepperMove: coils de-energized (AsyncBitBang)") } catch { $log.WriteTrace("StepperMove de-energize stub: $($_.Exception.Message)") } } } else { # Stub mode: no device handle $log.WriteTrace("StepperMove stub: no Device handle (Steps=$Steps, Mode=$StepMode, Dir=$Direction)") } } |