Drive Servo Motors directly over PWM with PHP

Maintainers

Package info

github.com/DeptOfScrapyardRobotics/GenericServos

Homepage

pkg:composer/dept-of-scrapyard-robotics/generic-servos

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-07-12 16:10 UTC

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:

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 for to() / sweep
  • physical_range: ['min' => 0, 'max' => 180] — degrees mapped onto the pulse range
  • starting_positionhome() target and boot park angle
  • offset — added to every commanded angle before mapping
  • invert: true — reverse the horn direction
  • pwm_range: ['min' => 1000, 'max' => 2000] — pulse width in microseconds
  • boot_now: true — call boot() 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 space
  • invert: true — reverse spin direction
  • pwm_range: ['min' => 1000, 'max' => 2000] — pulse width in microseconds
  • boot_now: true — call boot() 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 (or boot_now: true) before commanding motion — boot sets the 50 Hz period, writes the park pulse, and enables the channel. BareMetal wrappers do not expose boot(); boot the IC before wrapping, or pass boot_now: true into ::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). $stop is 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/pwm2 after export. Which 40-pin header pad that is depends on your overlay / pinmux — confirm with pinctrl | 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 ServoSignalTransport todos — not implemented yet. Use GenericPositionalServo::pwm(...) / GenericContinuousServo::pwm(...) today.