microscrap / native-drivers
The Official ScrapyardIO Native Drivers package.
Requires
- php: ^8.3
- scrapyard-io/nuts-and-bolts: ^0.5.0
- waveforms/common: ^0.5.0
- waveforms/contracts: ^0.5.0
This package is not auto-updated.
Last update: 2026-07-12 18:43:56 UTC
README
PHP driver package that provides the native carrier for the ScrapyardIO framework GPIO stack. It drives hardware PWM on Linux single-board computers through the kernel sysfs PWM interface (/sys/class/pwm/pwmchipN) using ordinary PHP file I/O — no ext-posi, ext-ftdi, or Microscrap binding package is required for PWM itself.
This package includes:
- The
ScrapyardIONativeManagercarrier manager, auto-discovered by the framework via the#[CarrierDriver('native')]attribute - A sysfs-backed PWM driver (
NativePWMDriver) that exports channels, reads/writesperiod/duty_cycle/enable/polarity, and unexports cleanly on close - Multi-channel support — open several channels on the same pwmchip and get a
MultiplePWMChannelsbus back
Digital GPIO, SPI, I²C, and UART are not part of this package. On Linux SBCs those protocols use microscrap/posix-drivers (GPIO::*('posix')). USB/FTDI adapters use microscrap/usb-drivers.
Which carrier should I use?
| Carrier | Package | Use when |
|---|---|---|
native |
this package | Hardware PWM via /sys/class/pwm only |
posix |
microscrap/posix-drivers |
Linux SBC digital / SPI / I²C / UART |
usb |
microscrap/usb-drivers |
FTDI adapters over MPSSE / serial |
Older docs sometimes call the Linux bus stack native. For digital / SPI / I²C / UART on an SBC today, use posix. Use native only for PWM.
Requirements
- PHP 8.3+
- A Linux machine that exposes
/sys/class/pwm(hardware PWM capable — Raspberry Pi, many other SBCs) - Write access to the pwmchip sysfs nodes (often root, or membership in a group that udev grants access to after export)
waveforms/common^0.5.0,waveforms/contracts^0.5.0, andscrapyard-io/nuts-and-bolts^0.5.0 — direct Composer requirements of this packagewaveforms/pwm^0.5.0 (or the fullscrapyard-io/frameworkmetapackage) in the consuming app — required byNativePWMDriver/NativePWMHandle(GPIO\PWM\*) and by theGPIO::pwm(...)factory shown below
Device addressing
| Factory call | Meaning | Sysfs path |
|---|---|---|
->device(0) |
pwmchip number 0 |
/sys/class/pwm/pwmchip0 |
->device('0') |
numeric string chip number (same as 0) |
/sys/class/pwm/pwmchip0 |
->device('pwmchip0') or ->device('/sys/class/pwm/pwmchip0') |
trailing pwmchipN is parsed |
/sys/class/pwm/pwmchip0 |
->channel(2) |
channel offset on that chip | …/pwm2 after export |
Confirm available chips with ls /sys/class/pwm/. On a Raspberry Pi 5, pwmchip0 channel 2 is a common hobby-servo / fan PWM path once the board's PWM overlay / pinmux is enabled.
Units and polarity
Kernel PWM sysfs attributes use nanoseconds:
setPeriod($ns)/getPeriod()— full waveform period in ns (e.g.20_000_000for 50 Hz / 20 ms)setDutyCycle($ns)/getDutyCycle()— high time in ns (must be ≤ period)setPolarity('normal'|'inversed')— any other string throwsNativePWMException::invalidPolarity
Set the period before raising the duty cycle when starting from a fresh export; the kernel rejects a duty larger than the current period.
Installation
composer require microscrap/native-drivers
Confirm the host exposes PWM:
ls /sys/class/pwm
If /sys/class/pwm is missing, the manager throws a GPIOException when a PWM driver is requested: "The Native driver requires native support to work with PWM."
How it works
The framework resolves the native carrier through attribute-based discovery — no manual registration:
GPIO::pwm('native')->…->create()
└─ PWMConnectionFactory (scrapyard-io/framework)
└─ GPIOCarriers::native('pwm')
└─ ScrapyardIONativeManager (#[CarrierDriver('native')], this package)
└─ NativePWMDriver
└─ /sys/class/pwm/pwmchip{N}/export → pwm{M}/{period,duty_cycle,enable,polarity}
| Protocol | Driver | Backend |
|---|---|---|
pwm |
NativePWMDriver |
Linux PWM sysfs (pwmchip export / attribute files) |
On create(), the driver exports the channel if needed, then waits up to ~500 ms for udev to make period writable before returning a PWMChannel. On close(), it writes enable=0 and unexports the channel.
Usage
All examples go through the framework's GPIO facade with the native driver.
Single PWM channel
<?php use GPIO\Common\GPIO; $pwm = GPIO::pwm('native') ->device(0) // pwmchip0 ->channel(2) ->name('servo') ->create(); // 50 Hz hobby-servo frame (period / duty in nanoseconds) $pwm->setPeriod(20_000_000); // 20 ms $pwm->setDutyCycle(1_500_000); // 1.5 ms neutral $pwm->setEnable(true); usleep(500_000); $pwm->setDutyCycle(1_000_000); // 1.0 ms $pwm->setDutyCycle(2_000_000); // 2.0 ms $pwm->setEnable(false); $pwm->close(); // disable + unexport
name() sets the consumer label used as the primary key when you open multiple channels (default scrapyard-io-pwm).
Multiple channels on one chip
<?php use GPIO\Common\GPIO; $channels = GPIO::pwm('native') ->device(0) ->channel(0) ->name('fan') ->createWith(0, [ 'servo' => 2, ]); // MultiplePWMChannels $fan = $channels->getChannel('fan'); $servo = $channels->getChannel('servo'); $fan->setPeriod(40_000); // 25 kHz fan carrier $fan->setDutyCycle(20_000); // 50% $fan->setEnable(true); $servo->setPeriod(20_000_000); $servo->setDutyCycle(1_500_000); $servo->setEnable(true); $fan->close(); $servo->close();
createWith($device, $addl_channels) sets the chip from $device and opens extra named (or numeric) offsets on that chip. The primary channel still comes from a prior channel() call; its map key is name() (default scrapyard-io-pwm). Close each PWMChannel individually — MultiplePWMChannels has no bus-level close().
Polarity
<?php use GPIO\Common\GPIO; $pwm = GPIO::pwm('native') ->device(0) ->channel(2) ->name('servo') ->create(); $pwm->setPolarity('normal'); $pwm->setPolarity('inversed'); echo $pwm->getPolarity(); $pwm->close();
Exceptions
| Exception | Extends | Thrown when |
|---|---|---|
NativePWMException |
PWMChannelException |
Chip missing, export failed, attribute not writable yet, read/write failed, or polarity is not normal/inversed |
PWMChannelException |
GPIOException |
Factory validation failures (device() / channel() never set) |
GPIOException |
— | /sys/class/pwm is not present on the machine, or a driver does not extend ScrapyardIONativeDriver |
Testing
Pest scaffolding lives under tests/ (Pest.php, TestCase.php, empty Unit/Feature PWM directories). This package does not currently ship a Pest dependency or any test cases — exercise the driver on a board with real /sys/class/pwm, or add fakes around sysfs paths when you introduce automated coverage.
License
MIT. See LICENSE.