dept-of-scrapyard-robotics / generic-servos
Drive Servo Motors directly over PWM with PHP
Package info
github.com/DeptOfScrapyardRobotics/GenericServos
pkg:composer/dept-of-scrapyard-robotics/generic-servos
Requires
- php: ^8.3
- bare-metal/actuation: ^0.5.0
- bare-metal/contracts: ^0.5.0
- scrapyard-io/nuts-and-bolts: ^0.5.0
- waveforms/pwm: ^0.5.0
Suggests
- dept-of-scrapyard-robotics/pca9685: ^0.5.0 - I2C PWM expander / servo bus (not yet wired)
- microscrap/native-drivers: ^0.5.0 - for driving over PWM
This package is auto-updated.
Last update: 2026-07-12 16:10:29 UTC
README
Drive hobby servos over PWM with PHP
PHP package for positional (angle) and continuous-rotation hobby servos. It sits on the ScrapyardIO GPIO PWM stack and plugs into the BareMetal Actuation servo components (PositionalServo, ContinuousServo).
Which kind do I need?
- Positional (
GenericPositionalServo) — the horn holds an angle (to(),center(),sweep()). Arms, flaps, camera pans. - Continuous (
GenericContinuousServo) — the shaft spins (cw()/ccw()/stop()). Wheels, winches, continuous drives. Neutral is a deadband around mid-pulse, not a parked angle.
Compatible PWM Interfaces
Hobby servos are driven from a hardware PWM channel at a 50 Hz frame (20_000_000 ns period). Pulse width in microseconds maps to angle (positional) or spin direction/speed (continuous).
You can interface with them the following ways:
- A Linux single-board computer's native PWM sysfs chips (
/sys/class/pwm/pwmchipN) using the native carrier (native) - (Planned) An I²C PWM expander such as the PCA9685 — reserved in the package suggests and transport todos, not wired yet
Default pulse range is 1000–2000 µs (safe for most hobby servos). Narrower or wider ranges can be passed at construction or via calibrate().
Dependencies
This package makes use of modules within:
For PWM-driven servos you also need:
- Microscrap Native Drivers (
microscrap/native-drivers—nativePWM carrier)
Confirm the host exposes PWM and that your user can write the sysfs nodes (often root, or a udev-granted group). See the native-drivers README Requirements for details:
ls /sys/class/pwm
Installing from Composer
composer require dept-of-scrapyard-robotics/generic-servos composer require microscrap/native-drivers
Basic Usage
Build the PWM channel with the GPIO facade, then hand that channel to the servo IC. Call boot() (or pass boot_now: true) so the driver sets the 50 Hz frame, parks at the starting position / deadband, and enables the channel.
Positional servo (native sysfs)
<?php use GPIO\Common\GPIO; use DeptOfScrapyardRobotics\Actuators\GenericServos\GenericPositionalServo; $pwm = GPIO::pwm('native') ->device(0) // pwmchip0 ->channel(2) ->name('arm-servo') ->create(); $servo = GenericPositionalServo::pwm($pwm, boot_now: true); $servo->center(); // midpoint of range_of_motion (default 0–180°) $servo->to(45); // immediate move $servo->to(135, ms: 500, rate: 5); // glide in 5° steps over 500 ms $servo->home(); // back to starting_position (default 0°) $servo->min(); $servo->max(); $servo->sweep(0, 180); // low → high → low, blocking echo $servo->pulse(); // current pulse width (ns) $servo->calibrate(1000, 2000); // pulse range in microseconds ($stop reserved) $servo->enable(); $servo->enabled(); // bool $servo->disable(); $pwm->close();
Optional knobs on GenericPositionalServo::pwm() / its constructor:
range_of_motion: ['lower' => 0, 'upper' => 180]— software clamp forto()/ sweepphysical_range: ['min' => 0, 'max' => 180]— degrees mapped onto the pulse rangestarting_position—home()target and boot park angleoffset— added to every commanded angle before mappinginvert: true— reverse the horn directionpwm_range: ['min' => 1000, 'max' => 2000]— pulse width in microsecondsboot_now: true— callboot()inside the factory
Continuous-rotation servo (native sysfs)
<?php use GPIO\Common\GPIO; use DeptOfScrapyardRobotics\Actuators\GenericServos\GenericContinuousServo; $pwm = GPIO::pwm('native') ->device(0) ->channel(2) ->name('drive-servo') ->create(); $servo = GenericContinuousServo::pwm($pwm, boot_now: true); $servo->clockwise(80); // 0–100% $servo->counterClockwise(40); $servo->cw(100); // aliases $servo->ccw(50); $servo->stop(); // park on deadband / neutral $servo->deadband(85, 95); // widen neutral if the shaft creeps at mid-pulse $servo->disable(); $pwm->close();
Optional knobs on GenericContinuousServo::pwm() / its constructor:
deadband: ['lower' => 90, 'upper' => 90]— neutral stop band in the 0–180 control spaceinvert: true— reverse spin directionpwm_range: ['min' => 1000, 'max' => 2000]— pulse width in microsecondsboot_now: true— callboot()inside the factory
Default deadband is exact mid-pulse stop. min() / max() spin full CCW / CW; center() / home() call stop().
Alternative Usage
Using Through the Actuation Library (as a PositionalServo)
<?php use GPIO\Common\GPIO; use BareMetal\Actuation\Servos\PositionalServo; use DeptOfScrapyardRobotics\Actuators\GenericServos\GenericPositionalServo; $pwm = GPIO::pwm('native') ->device(0) ->channel(2) ->name('arm-servo') ->create(); $arm = new PositionalServo( GenericPositionalServo::pwm($pwm, boot_now: true) ); $arm->center(); $arm->to(90); $arm->sweep(20, 160); $arm->disable(); $pwm->close();
Using Through the Actuation Library (as a ContinuousServo)
<?php use GPIO\Common\GPIO; use BareMetal\Actuation\Servos\ContinuousServo; use DeptOfScrapyardRobotics\Actuators\GenericServos\GenericContinuousServo; $pwm = GPIO::pwm('native') ->device(0) ->channel(2) ->name('drive-servo') ->create(); $wheel = new ContinuousServo( GenericContinuousServo::pwm($pwm, boot_now: true) ); $wheel->cw(60); usleep(500_000); $wheel->stop(); $wheel->disable(); $pwm->close();
Notes
- Always
boot()on the IC (orboot_now: true) before commanding motion — boot sets the 50 Hz period, writes the park pulse, and enables the channel. BareMetal wrappers do not exposeboot(); boot the IC before wrapping, or passboot_now: trueinto::pwm(). - Period / duty on the PWM channel are in nanoseconds; this package’s
pwm_range/calibrate($min, $max, $stop = null)arguments are in microseconds (converted internally ×1000).$stopis reserved for continuous-neutral / PCA9685 paths and is unused today. pulse(?int $ns = null)gets or sets the raw duty in nanoseconds;enable()/enabled()/disable()control the channel after boot.- Set period before raising duty on a fresh export — the kernel rejects a duty larger than the current period. Boot does this ordering for you.
- Wiring: PWM pin → servo SIGNAL; external 5–6 V supply → servo V+ (do not power the motor from the Pi’s 5 V/GPIO pins); common GND between the Pi and the supply.
- Pin map:
device(0)->channel(2)targets/sys/class/pwm/pwmchip0/pwm2after export. Which 40-pin header pad that is depends on your overlay / pinmux — confirm withpinctrl | grep -i pwm(or your board docs) before connecting the signal wire. On many Pi 5 setups that path has been used for hobby-servo / fan PWM once the PWM overlay is enabled. - Start with the default 1000–2000 µs pulse range; widen toward 600–2400 µs only after you confirm the horn travel is safe for your servo.
- PCA9685 / I²C servo-bus support is reserved in the package suggests and
ServoSignalTransporttodos — not implemented yet. UseGenericPositionalServo::pwm(...)/GenericContinuousServo::pwm(...)today.