Private/Ftdi.Spi.ps1

#Requires -Version 5.1
# Ftdi.Spi.ps1
# MPSSE SPI backend for FT232H.
#
# ADBUS pin assignment (fixed by FTDI MPSSE hardware):
# bit 0 = ADBUS0 = SCK (clock output)
# bit 1 = ADBUS1 = MOSI (data output, Master Out Slave In)
# bit 2 = ADBUS2 = MISO (data input, Master In Slave Out)
# bit 3 = ADBUS3 = CS0 (default chip-select, active low)
# bit 4-7 optional additional CS pins
#
# Wire guide:
# FT232H D0 -> SCK FT232H D1 -> MOSI
# FT232H D2 -> MISO FT232H D3 -> CS (with 10k pull-up to VCC)
#
# SPI mode table:
# Mode 0 (CPOL=0 CPHA=0) -- most common. Clock idle LOW, sample rising edge.
# Mode 1 (CPOL=0 CPHA=1) -- Clock idle LOW, sample falling edge.
# Mode 2 (CPOL=1 CPHA=0) -- Clock idle HIGH, sample falling edge.
# Mode 3 (CPOL=1 CPHA=1) -- Clock idle HIGH, sample rising edge.
#
# MPSSE command mapping:
# CPHA=0: write 0x11 (out -ve edge), read 0x20 (in +ve edge), xfer 0x31
# CPHA=1: write 0x10 (out +ve edge), read 0x24 (in -ve edge), xfer 0x34
# CPOL sets CLK idle level in the ADBUS state byte only (bit0 of value byte).

function Initialize-MpsseSpi {
    <#
    .SYNOPSIS
    Initializes FTDI device for SPI communication via MPSSE.
 
    .PARAMETER DeviceHandle
    Open connection object from Connect-PsGadgetFtdi.
 
    .PARAMETER ClockFrequency
    SPI clock in Hz. Default 1 MHz. Max 30 MHz.
 
    .PARAMETER SpiMode
    SPI mode 0-3 (CPOL/CPHA). Default 0.
 
    .PARAMETER CsPin
    ADBUS pin number for chip select (active low). Default 3.
    Must be 3-7 to avoid conflict with SCK(0), MOSI(1), MISO(2).
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory = $true)]
        [System.Object]$DeviceHandle,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1000, 30000000)]
        [int]$ClockFrequency = 1000000,

        [Parameter(Mandatory = $false)]
        [ValidateRange(0, 3)]
        [int]$SpiMode = 0,

        [Parameter(Mandatory = $false)]
        [ValidateRange(3, 7)]
        [int]$CsPin = 3
    )

    try {
        if (-not $DeviceHandle -or -not $DeviceHandle.IsOpen) {
            throw "Device handle is invalid or not open"
        }

        $rawDevice = Get-FtdiD2xxHandle -DeviceHandle $DeviceHandle
        $isReal    = $script:FtdiInitialized -and ($null -ne $rawDevice)

        # CPOL = bit 1 of SpiMode; CPHA = bit 0 of SpiMode
        $cpol = ($SpiMode -band 2) -ne 0
        $cpha = ($SpiMode -band 1) -ne 0

        # ADBUS direction mask: CLK(0)+MOSI(1)+CS(CsPin) = output; MISO(2) = input
        $csBit   = [byte](1 -shl $CsPin)
        $dirMask = [byte](0x03 -bor $csBit)

        # Clock idle state: CS deasserted (high), CLK at CPOL level, MOSI=0
        $clkIdle = [byte](if ($cpol) { 0x01 } else { 0x00 })
        $csHigh  = [byte]($csBit -bor $clkIdle)   # CS deasserted + CLK at idle

        # SPI clock divisor (no 3-phase -- that is I2C-only):
        # f = 60 MHz / ((1 + divisor) * 2) [with divide-by-5 disabled]
        # divisor = 60 MHz / (2 * f) - 1
        $divisor   = [int][Math]::Floor(60000000 / (2.0 * [double]$ClockFrequency) - 1)
        $divisor   = [Math]::Max(0, [Math]::Min(65535, $divisor))
        $divisorLo = [byte]($divisor -band 0xFF)
        $divisorHi = [byte](($divisor -shr 8) -band 0xFF)

        if ($isReal) {
            # Low latency timer for SPI (1 ms vs default 16 ms)
            $rawDevice.SetLatency(1) | Out-Null

            $writeCmd = {
                param([byte[]]$cmd, [string]$label)
                [uint32]$bw = 0
                $st = $rawDevice.Write($cmd, [uint32]$cmd.Length, [ref]$bw)
                if ([int]$st -ne 0) { throw "$label failed: status=$st" }
            }

            $rawDevice.Purge(3) | Out-Null
            Start-Sleep -Milliseconds 30

            # MPSSE synchronization handshake (same pattern as GPIO/I2C init)
            [uint32]$sw = 0
            $rawDevice.Write([byte[]](0xAA), 1, [ref]$sw) | Out-Null
            Start-Sleep -Milliseconds 30
            [byte[]]$sb = [byte[]]::new(2); [uint32]$sr = 0
            $rawDevice.Read($sb, 2, [ref]$sr) | Out-Null
            if ($sr -ne 2 -or $sb[0] -ne 0xFA -or $sb[1] -ne 0xAA) {
                throw ("MPSSE SPI sync failed (0xAA): got {0} bytes: 0x{1:X2} 0x{2:X2}" -f $sr, $sb[0], $sb[1])
            }
            $rawDevice.Write([byte[]](0xAB), 1, [ref]$sw) | Out-Null
            Start-Sleep -Milliseconds 30
            $rawDevice.Read($sb, 2, [ref]$sr) | Out-Null
            if ($sr -ne 2 -or $sb[0] -ne 0xFA -or $sb[1] -ne 0xAB) {
                throw ("MPSSE SPI sync failed (0xAB): got {0} bytes: 0x{1:X2} 0x{2:X2}" -f $sr, $sb[0], $sb[1])
            }

            # SPI MPSSE configuration:
            # 0x8A Disable clock divide-by-5 (60 MHz base)
            # 0x97 Disable adaptive clocking
            # (no 0x8C 3-phase -- I2C only)
            # (no 0x9E drive-zero -- I2C open-drain only)
            # 0x86 Set clock divisor (lo, hi)
            # 0x85 Loopback off
            # 0x80 Set ADBUS: CS=1 (idle), CLK=CPOL, MOSI=0
            [byte[]]$spiCfg = @(
                0x8A,
                0x97,
                0x86, $divisorLo, $divisorHi,
                0x85,
                0x80, $csHigh, $dirMask
            )
            & $writeCmd $spiCfg 'SPI config'
            Start-Sleep -Milliseconds 30

            $hexStr = ($spiCfg | ForEach-Object { $_.ToString('X2') }) -join ' '
            $script:PsGadgetLogger.WriteProto('SPI.INIT',
                "clock=${ClockFrequency}Hz divisor=${divisor} mode=${SpiMode} CS=ADBUS${CsPin}",
                $hexStr)
            Write-Verbose "MPSSE SPI initialized: clock=$ClockFrequency Hz, mode=$SpiMode, CS=ADBUS$CsPin (divisor=$divisor)"
        } else {
            $script:PsGadgetLogger.WriteProto('SPI.INIT',
                "clock=${ClockFrequency}Hz mode=${SpiMode} CS=ADBUS${CsPin} (STUB)")
            Write-Verbose "MPSSE SPI initialized (STUB MODE)"
        }

        # Cache SPI config on connection object so transfer functions can retrieve it
        $DeviceHandle | Add-Member -MemberType NoteProperty -Name 'SpiClockHz' -Value $ClockFrequency -Force
        $DeviceHandle | Add-Member -MemberType NoteProperty -Name 'SpiMode'    -Value $SpiMode        -Force
        $DeviceHandle | Add-Member -MemberType NoteProperty -Name 'SpiCsPin'   -Value $CsPin          -Force
        return $true

    } catch {
        Write-Error "Failed to initialize MPSSE SPI: $_"
        return $false
    }
}

function Invoke-MpsseSpiWrite {
    <#
    .SYNOPSIS
    Writes bytes to a SPI device via MPSSE (write-only, no data returned).
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory = $true)]  [System.Object]$DeviceHandle,
        [Parameter(Mandatory = $true)]  [byte[]]$Data,
        [Parameter(Mandatory = $false)] [ValidateRange(0, 3)] [int]$SpiMode = 0,
        [Parameter(Mandatory = $false)] [ValidateRange(3, 7)] [int]$CsPin   = 3
    )

    try {
        if (-not $DeviceHandle -or -not $DeviceHandle.IsOpen) {
            throw "Device handle is invalid or not open"
        }
        if ($Data.Length -eq 0) { return $true }

        $rawDevice = Get-FtdiD2xxHandle -DeviceHandle $DeviceHandle
        $isReal    = $script:FtdiInitialized -and ($null -ne $rawDevice)

        $cpol      = ($SpiMode -band 2) -ne 0
        $cpha      = ($SpiMode -band 1) -ne 0
        $csBit     = [byte](1 -shl $CsPin)
        $dirMask   = [byte](0x03 -bor $csBit)
        $clkIdle   = [byte](if ($cpol) { 0x01 } else { 0x00 })
        $csHigh    = [byte]($csBit -bor $clkIdle)
        $csLow     = $clkIdle   # CS asserted (low), CLK stays at idle level

        # CPHA=0 -> write on -ve/falling edge (0x11); CPHA=1 -> write on +ve/rising edge (0x10)
        $writeCmdByte = if ($cpha) { [byte]0x10 } else { [byte]0x11 }

        $lenMinus1 = [uint16]($Data.Length - 1)
        $lenLo     = [byte]($lenMinus1 -band 0xFF)
        $lenHi     = [byte](($lenMinus1 -shr 8) -band 0xFF)

        # Full transaction: CS assert + data write + CS deassert
        $buf = [System.Collections.Generic.List[byte]]::new()
        $buf.AddRange([byte[]](0x80, $csLow,  $dirMask))          # CS assert (low)
        $buf.Add($writeCmdByte); $buf.Add($lenLo); $buf.Add($lenHi)
        $buf.AddRange($Data)
        $buf.AddRange([byte[]](0x80, $csHigh, $dirMask))          # CS deassert (high)
        [byte[]]$txBuf = $buf.ToArray()

        $hexStr = $script:PsGadgetLogger.FormatHex($Data)

        if ($isReal) {
            [uint32]$bw = 0
            $st = $rawDevice.Write($txBuf, [uint32]$txBuf.Length, [ref]$bw)
            if ([int]$st -ne 0) { throw "SPI write failed: D2XX status=$st" }
            $script:PsGadgetLogger.WriteProto('SPI.WRITE',
                "$($Data.Length)B mode=$SpiMode CS=ADBUS$CsPin",
                $hexStr)
        } else {
            $script:PsGadgetLogger.WriteProto('SPI.WRITE',
                "$($Data.Length)B mode=$SpiMode CS=ADBUS$CsPin (STUB)",
                $hexStr)
        }
        return $true

    } catch {
        Write-Error "SPI write failed: $_"
        return $false
    }
}

function Invoke-MpsseSpiRead {
    <#
    .SYNOPSIS
    Reads bytes from a SPI device via MPSSE (MOSI stays low during read).
    #>

    [CmdletBinding()]
    [OutputType([byte[]])]
    param(
        [Parameter(Mandatory = $true)]  [System.Object]$DeviceHandle,
        [Parameter(Mandatory = $true)]  [ValidateRange(1, 65536)] [int]$Count,
        [Parameter(Mandatory = $false)] [ValidateRange(0, 3)] [int]$SpiMode = 0,
        [Parameter(Mandatory = $false)] [ValidateRange(3, 7)] [int]$CsPin   = 3
    )

    try {
        if (-not $DeviceHandle -or -not $DeviceHandle.IsOpen) {
            throw "Device handle is invalid or not open"
        }

        $rawDevice = Get-FtdiD2xxHandle -DeviceHandle $DeviceHandle
        $isReal    = $script:FtdiInitialized -and ($null -ne $rawDevice)

        $cpol     = ($SpiMode -band 2) -ne 0
        $cpha     = ($SpiMode -band 1) -ne 0
        $csBit    = [byte](1 -shl $CsPin)
        $dirMask  = [byte](0x03 -bor $csBit)
        $clkIdle  = [byte](if ($cpol) { 0x01 } else { 0x00 })
        $csHigh   = [byte]($csBit -bor $clkIdle)
        $csLow    = $clkIdle

        # CPHA=0 -> read on +ve/rising edge (0x20); CPHA=1 -> read on -ve/falling edge (0x24)
        $readCmdByte = if ($cpha) { [byte]0x24 } else { [byte]0x20 }

        $lenMinus1 = [uint16]($Count - 1)
        $lenLo     = [byte]($lenMinus1 -band 0xFF)
        $lenHi     = [byte](($lenMinus1 -shr 8) -band 0xFF)

        # CS assert + read command + SEND_IMMEDIATE + CS deassert
        $buf = [System.Collections.Generic.List[byte]]::new()
        $buf.AddRange([byte[]](0x80, $csLow, $dirMask))           # CS assert
        $buf.Add($readCmdByte); $buf.Add($lenLo); $buf.Add($lenHi)
        $buf.Add(0x87)                                             # SEND_IMMEDIATE: flush to host
        $buf.AddRange([byte[]](0x80, $csHigh, $dirMask))          # CS deassert
        [byte[]]$txBuf = $buf.ToArray()

        if ($isReal) {
            [uint32]$bw = 0
            $st = $rawDevice.Write($txBuf, [uint32]$txBuf.Length, [ref]$bw)
            if ([int]$st -ne 0) { throw "SPI read command failed: D2XX status=$st" }

            [byte[]]$rxBuf = [byte[]]::new($Count)
            [uint32]$br    = 0
            $st = $rawDevice.Read($rxBuf, [uint32]$Count, [ref]$br)
            if ([int]$st -ne 0) { throw "SPI read data failed: D2XX status=$st" }

            if ($br -lt $Count) {
                $trimmed = [byte[]]::new($br)
                [Array]::Copy($rxBuf, $trimmed, [int]$br)
                $rxBuf = $trimmed
            }

            $hexStr = $script:PsGadgetLogger.FormatHex($rxBuf)
            $script:PsGadgetLogger.WriteProto('SPI.READ',
                "$br/$Count B mode=$SpiMode CS=ADBUS$CsPin",
                $hexStr)
            return $rxBuf
        } else {
            [byte[]]$stub = [byte[]]::new($Count)
            $script:PsGadgetLogger.WriteProto('SPI.READ',
                "${Count}B mode=$SpiMode CS=ADBUS$CsPin (STUB)")
            return $stub
        }

    } catch {
        Write-Error "SPI read failed: $_"
        return [byte[]]@()
    }
}

function Invoke-MpsseSpiTransfer {
    <#
    .SYNOPSIS
    Full-duplex SPI transfer: writes Data bytes and simultaneously reads the same count.
    MOSI and MISO are active for the entire transaction length.
    #>

    [CmdletBinding()]
    [OutputType([byte[]])]
    param(
        [Parameter(Mandatory = $true)]  [System.Object]$DeviceHandle,
        [Parameter(Mandatory = $true)]  [byte[]]$Data,
        [Parameter(Mandatory = $false)] [ValidateRange(0, 3)] [int]$SpiMode = 0,
        [Parameter(Mandatory = $false)] [ValidateRange(3, 7)] [int]$CsPin   = 3
    )

    try {
        if (-not $DeviceHandle -or -not $DeviceHandle.IsOpen) {
            throw "Device handle is invalid or not open"
        }
        if ($Data.Length -eq 0) { return [byte[]]@() }

        $rawDevice = Get-FtdiD2xxHandle -DeviceHandle $DeviceHandle
        $isReal    = $script:FtdiInitialized -and ($null -ne $rawDevice)

        $cpol     = ($SpiMode -band 2) -ne 0
        $cpha     = ($SpiMode -band 1) -ne 0
        $csBit    = [byte](1 -shl $CsPin)
        $dirMask  = [byte](0x03 -bor $csBit)
        $clkIdle  = [byte](if ($cpol) { 0x01 } else { 0x00 })
        $csHigh   = [byte]($csBit -bor $clkIdle)
        $csLow    = $clkIdle

        # CPHA=0: write on -ve, read on +ve -> 0x31
        # CPHA=1: write on +ve, read on -ve -> 0x34
        $xferCmd  = if ($cpha) { [byte]0x34 } else { [byte]0x31 }

        $lenMinus1 = [uint16]($Data.Length - 1)
        $lenLo     = [byte]($lenMinus1 -band 0xFF)
        $lenHi     = [byte](($lenMinus1 -shr 8) -band 0xFF)

        # CS assert + full-duplex shift + SEND_IMMEDIATE + CS deassert
        $buf = [System.Collections.Generic.List[byte]]::new()
        $buf.AddRange([byte[]](0x80, $csLow, $dirMask))
        $buf.Add($xferCmd); $buf.Add($lenLo); $buf.Add($lenHi)
        $buf.AddRange($Data)
        $buf.Add(0x87)
        $buf.AddRange([byte[]](0x80, $csHigh, $dirMask))
        [byte[]]$txBuf = $buf.ToArray()

        $txHex = $script:PsGadgetLogger.FormatHex($Data)

        if ($isReal) {
            [uint32]$bw = 0
            $st = $rawDevice.Write($txBuf, [uint32]$txBuf.Length, [ref]$bw)
            if ([int]$st -ne 0) { throw "SPI transfer write failed: D2XX status=$st" }

            [byte[]]$rxBuf = [byte[]]::new($Data.Length)
            [uint32]$br    = 0
            $st = $rawDevice.Read($rxBuf, [uint32]$Data.Length, [ref]$br)
            if ([int]$st -ne 0) { throw "SPI transfer read failed: D2XX status=$st" }

            if ($br -lt $Data.Length) {
                $trimmed = [byte[]]::new($br)
                [Array]::Copy($rxBuf, $trimmed, [int]$br)
                $rxBuf = $trimmed
            }

            $rxHex = $script:PsGadgetLogger.FormatHex($rxBuf)
            $script:PsGadgetLogger.WriteProto('SPI.XFER',
                "TX=$($Data.Length)B RX=${br}B mode=$SpiMode CS=ADBUS$CsPin",
                "TX: $txHex -> RX: $rxHex")
            return $rxBuf
        } else {
            [byte[]]$stub = [byte[]]::new($Data.Length)
            $script:PsGadgetLogger.WriteProto('SPI.XFER',
                "TX=$($Data.Length)B RX=$($Data.Length)B mode=$SpiMode CS=ADBUS$CsPin (STUB)")
            return $stub
        }

    } catch {
        Write-Error "SPI transfer failed: $_"
        return [byte[]]@()
    }
}