microscrap / usb-drivers
The Official ScrapyardIO USB Drivers package.
Requires
- php: ^8.3
- ext-ftdi: ^0.4.2
- microscrap/ftdi: ^0.5.0
- scrapyard-io/nuts-and-bolts: ^0.5.0
- waveforms/common: ^0.5.0
- waveforms/contracts: ^0.5.0
Requires (Dev)
- pestphp/pest: ^4
Suggests
- microscrap/mpsse: ^0.5.0
- waveforms/digital: ^0.5.0
- waveforms/i2c: ^0.5.0
- waveforms/spi: ^0.5.0
- waveforms/uart: ^0.5.0
This package is auto-updated.
Last update: 2026-07-06 19:35:21 UTC
README
PHP driver package that provides the usb carrier for the ScrapyardIO framework GPIO stack. It drives FTDI USB adapters (FT232H, FT2232HL, FT232RL) through ext-ftdi, tunnelling SPI, I²C, and digital GPIO over MPSSE and running UART as plain async serial.
This package includes:
- The
ScrapyardIOUSBManagercarrier manager, auto-discovered by the framework via the#[CarrierDriver('usb')]attribute - MPSSE-backed protocol drivers for SPI, I²C, and digital input/output
- A libftdi-backed UART driver (no MPSSE) for serial-only workflows
- Digital pin ride-along on SPI/I²C buses — control DC/RESET/BUSY pins on the same FTDI adapter that carries the bus
Requirements
- PHP 8.3+
ext-ftdi^0.4.2 — the PHP FTDI extension (requires libftdi1 at runtime)- Debian/Ubuntu/Raspberry Pi OS:
libftdi1-2(dev package for builds:libftdi1-dev) - macOS:
brew install libftdi
- Debian/Ubuntu/Raspberry Pi OS:
microscrap/ftdi^0.5.0 — globalftdi_*helpers (installed automatically)microscrap/mpsse^0.5.0 — required for SPI, I²C, and digital GPIO (suggested; UART works without it)
Currently Supported devices
| Device | MPSSE device() value (SPI/I²C/GPIO) |
UART device() value |
Product ID |
|---|---|---|---|
| FT232H | ft232h |
FtdiProductId::FT232H->value |
0x6014 |
| FT2232HL | ft2232hl-a (channel A) / ft2232hl-b (channel B) |
FtdiProductId::FT2232HL->value |
0x6010 |
| FT232RL | — (no MPSSE engine) | FtdiProductId::RS232L->value |
0x6001 |
Note, these are the only devices so far I've tested. There shouldn't be any reason why any other MPSSE-enabled devices wouldn't work if there is a valid product id hex for the device. You could make a pull request with the missing device, or use custom logic that doesn't use the MpsseSupportedDevices enum.
The two protocol families address devices differently:
- SPI / I²C / GPIO go through
Microscrap\Bindings\MPSSE\Enums\MpsseSupportedDevice, which picks the exact MPSSE engine (channel A or B) and its product ID together. The FT2232HL has two independent MPSSE engines, soft2232hl-aandft2232hl-bare addressed separately. - UART goes through
Microscrap\Bindings\FTDI\Enums\FtdiProductId, which is only the raw product ID — there is one UART per chip regardless of how many MPSSE engines it has, so the FT2232HL is addressed as a singleFtdiProductId::FT2232HL, not per channel.
Installation
Install the FTDI extension first (see php-io-extensions/ftdi):
pie install php-io-extensions/ftdi
Confirm it is loaded:
php -m | grep ftdi
Install the driver package:
composer require microscrap/usb-drivers
If your workflow includes SPI, I²C, or digital GPIO (anything other than UART), install the MPSSE package as well:
composer require microscrap/mpsse
The manager enforces these prerequisites at runtime and throws a GPIOException with install instructions when one is missing.
How it works
The framework resolves the usb carrier through attribute-based discovery — no manual registration:
GPIO::spi('usb')->…->create()
└─ SPIConnectionFactory (scrapyard-io/framework)
└─ GPIOCarriers::usb('spi')
└─ ScrapyardIOUSBManager (#[CarrierDriver('usb')], this package)
└─ MPSSESPIDriver
└─ microscrap/mpsse → microscrap/ftdi → ext-ftdi → libftdi1
Each GPIO protocol maps to a driver in this package:
| Protocol | Driver | Backend |
|---|---|---|
spi |
MPSSESPIDriver |
MPSSE, modes SPI0–SPI3 |
i2c |
MPSSEI2CDriver |
MPSSE with full ACK/NACK sequencing |
digital-in |
MPSSEDigitalInputDriver |
MPSSE GPIO mode, polled edge events |
digital-out |
MPSSEDigitalOutputDriver |
MPSSE GPIO mode |
uart |
FTDIUartDriver |
Plain async serial via ftdi_* helpers — no MPSSE |
UART is intentionally kept off MPSSE: FTDIUartDriver resets the chip to async-serial bitmode on open (ftdi_set_bitmode($context, 0x00, 0x00)) so that multi-protocol parts like the FT232H release D0/D1 back to TX/RX even if a previous session left MPSSE enabled.
Usage
All examples go through the framework's GPIO facade with the usb driver.
SPI
<?php use GPIO\Common\GPIO; use GPIO\Contracts\SPI\SPIMode; $spi = GPIO::spi('usb') ->device('ft232h') ->mode(SPIMode::MODE_0) ->speed(1_000_000) ->create(); $spi->write("\x9F"); // clock bytes out $jedec = $spi->transfer("\x9F\x00\x00\x00"); // full-duplex transfer $bytes = $spi->read(3); // clock bytes in $spi->close();
I²C
<?php use GPIO\Common\GPIO; $i2c = GPIO::i2c('usb') ->device('ft232h') ->slave(0x26) ->create(); $who_am_i = $i2c->writeRead("\x01", 1); // write register address, repeated START, read 1 byte $i2c->write([0x11, 0x40]); // write register + value $data = $i2c->read(2); // plain read $i2c->close();
UART
UART devices are addressed by FTDI product ID, not by MPSSE device name:
<?php use GPIO\Common\GPIO; use Microscrap\Bindings\FTDI\Enums\FtdiProductId; $uart = GPIO::uart('usb') ->device(FtdiProductId::RS232L->value) // 0x6001 — FT232RL ->baud(115200) ->create(); $uart->write("AT\r\n"); $response = $uart->read(64); // byte array, or false when nothing arrived $uart->flush(); $uart->close();
The same call works on a FT2232HL — pass FtdiProductId::FT2232HL->value instead of an ft2232hl-a / ft2232hl-b MPSSE string, since the chip exposes only one UART regardless of channel:
$uart = GPIO::uart('usb') ->device(FtdiProductId::FT2232HL->value) // 0x6010 ->baud(115200) ->create();
baud(), parity(), stopBits(), dataBits(), and flowControl() are all available on the factory; defaults are 9600 8N1 with no flow control.
Digital output
GPIO mode exposes 12 lines per MPSSE device — MPSSEGpioPin::GPIOL0–GPIOL3 (pins 0–3) and GPIOH0–GPIOH7 (pins 4–11):
<?php use GPIO\Common\GPIO; use Microscrap\Bindings\MPSSE\Enums\MPSSEGpioPin; $led = GPIO::digitalOut('usb') ->device('ft232h') ->pin(MPSSEGpioPin::GPIOL1->value) ->name('led') ->defaultState(false) ->create(); $led->high(); $led->low(); $led->close();
Digital input
<?php use GPIO\Common\GPIO; $button = GPIO::digitalIn('usb') ->device('ft232h') ->pin(0) ->name('button') ->withEvents(true, false) // rising, falling ->timeout(5000) // ms ->create(); $event = $button->listen(); // ?DigitalInputEvent — blocks up to the timeout $pressed = $button->isHigh(); // immediate level read $button->close();
Edge detection is polling-based (the FTDI adapter has no interrupt lines), sampling roughly every millisecond until the timeout elapses.
Digital pins on a SPI/I²C bus
Displays and similar chips need DC/RESET/BUSY pins alongside the bus. Attach them to the same MPSSE context and get a connection bus back:
<?php use GPIO\Common\GPIO; use GPIO\Contracts\SPI\SPIMode; use GPIO\Contracts\Digital\LineBias; $bus = GPIO::spi('usb') ->device('ft232h') ->mode(SPIMode::MODE_0) ->speed(1_000_000) ->digitalPins() ->digitalOut(pin: 4, name: 'dc', default_state: false) ->digitalIn(pin: 5, name: 'busy', events: [false, true], timeout: 1000, bias: LineBias::AS_IS) ->create(); // SPIConnectionBus $bus->getPin('dc')->high(); $bus->spi->write("\x2A"); while ($bus->getPin('busy')->isHigh()) { usleep(1_000); }
The same pattern works for I²C (I2CConnectionBus, with the transport on $bus->i2c).
Exceptions
| Exception | Extends | Thrown when |
|---|---|---|
MpsseUSBException |
GPIOException |
An MPSSE device context cannot be opened (includes the FTDI error string) |
FTDIUARTException |
UARTException |
A UART device fails to open (includes the FTDI error string) |
UARTException |
— | The factory's device() was never set (missingMasterDevice) |
GPIOException |
— | ext-ftdi, microscrap/ftdi, or microscrap/mpsse is missing, or a driver does not extend ScrapyardIOUSBDriver |
Note that passing an unknown product ID to GPIO::uart('usb')->device(...) fails inside FtdiProductId::from() with a PHP ValueError before any exception above is reached.
Testing (Pest v4)
Run the suite:
./vendor/bin/pest
The unit tests run without hardware — FTDI and MPSSE globals are replaced with in-memory fakes and spies (tests/Support/), so driver dispatch, MPSSE call ordering, and error paths are all exercised on any machine.
License
MIT. See LICENSE.