Identity
| Part | Eccentric-rotating-mass vibration motor |
| Role | Haptic feedback / silent-mode notification |
| Bus / address | n/a — single GPIO output |
| GPIO / IRQ | GPIO3_B1 (active-high, drives VIB_EN net) |
| Datasheet | none — discrete motor + transistor switch |
| Pine64 wiki | PinePhone Pro Hardware |
| Schematic | sheet 14 (VIB_EN net) |
The canonical Linux DTS — arch/arm64/boot/dts/rockchip/rk3399-pinephone-pro.dts, search for vibrator — names the pin as <&gpio3 RK_PB1 GPIO_ACTIVE_HIGH>. Earlier hand-counted bring-up notes called this GPIO1_C7; that was wrong (different bank/group entirely). Trust the upstream binding.
Status — ● working
The motor has a real FreeBSD haptics driver instead of the temporary gpioled(4) shim. src/sys/dev/evdev/gpio_vibrator.c binds the Linux-compatible compatible = "gpio-vibrator" node, claims enable-gpios = <&gpio3 RK_PB1 GPIO_ACTIVE_HIGH>, registers an evdev device, advertises EV_FF / FF_RUMBLE, stores a small effect table, and uses a callout only for replay timing. The actual GPIO and optional vcc-supply toggles run in taskqueue_thread, matching Linux’s sleepable gpio-vibra workqueue shape.
Verified live on 2026-05-03 (mise run debug:vibrator:rumble with LENGTH_MS=5000, MAGNITUDE=0xc000): EV_FF upload + play reaches gpio_vibrator, the motor physically rumbles for the requested duration, play_count and stop_count advance, and last_error=0. Both the bench’s 300 ms and a hand-tested 5 s rumble felt as expected.
What remains for userspace integration
The driver is fully working; everything below this line is about feeding events to it from compositors and apps:
- No system-side haptics policy yet. Sway/Hyprland have no notion of “vibrate when the foreground app asks”. Apps that want haptics today have to open
/dev/input/eventNandEVIOCSFFdirectly. - Mobian-style
feedbackdanalogue would be the right shape — a small daemon that maps semantic events (“notification”, “ringtone”, “long-press”) toFF_RUMBLEeffect parameters, so apps don’t poke the device node directly. Out of scope for the kernel. - Notification routing: nothing in our overlay routes incoming Bluetooth call ringer / notification events to the vibrator yet. Likely small, just hasn’t been written.
Driver
- Our tree:
src/sys/dev/evdev/gpio_vibrator.c, enabled bydevice gpio_vibratorinsrc/sys/arm64/conf/PINEPHONE_PRO, with the standalone DTS node insrc/sys/contrib/device-tree/src/arm64/rockchip/rk3399-pinephone-pro.dts. - Linux mainline:
drivers/input/misc/gpio-vibra.c(binding:gpio-vibrator, FF_RUMBLE-capable). - FreeBSD upstream: no dedicated vibrator driver, and stable/15’s evdev force-feedback ioctls were stubs. The local patch set adds just enough
EV_FFsupport for simple rumble devices: advertised capability bits, effect count reporting, upload, erase, and play/stop writes.
DTS shape:
vibrator {
compatible = "gpio-vibrator";
enable-gpios = <&gpio3 RK_PB1 GPIO_ACTIVE_HIGH>;
pinctrl-names = "default";
pinctrl-0 = <&vib_en_pin>;
};
The leds {} child was removed in the same change. The motor GPIO must have one consumer; re-adding /dev/led/vibrator beside gpio-vibrator would double-claim gpio3 PB1.
sys/dev/evdev/cdev.c
#define DEF_RING_REPORTS 8 /* * FreeBSD historically defined EVIOCSFF as _IOW like Linux, but our ioctl * dispatcher only copies the updated effect id back for _IOWR commands. */ #define EVIOCSFF_LEGACY _IOW(EVDEV_IOC_MAGIC, 0x80, struct ff_effect) static d_open_t evdev_open; static d_read_t evdev_read; static d_write_t evdev_write; case EVIOCSFF: case EVIOCSFF_LEGACY: if (!evdev_event_supported(evdev, EV_FF)) return (ENOTSUP); if (evdev->ev_methods == NULL || evdev->ev_methods->ev_upload_effect == NULL) return (ENOTSUP); return (evdev->ev_methods->ev_upload_effect(evdev, (struct ff_effect *)data)); case EVIOCRMFF: if (!evdev_event_supported(evdev, EV_FF)) return (ENOTSUP); if (evdev->ev_methods == NULL || evdev->ev_methods->ev_erase_effect == NULL) return (ENOTSUP); return (evdev->ev_methods->ev_erase_effect(evdev, *(int *)data)); case EVIOCGEFFECTS: /* Fake unsupported ioctls */ *(int *)data = evdev->ev_ff_effects_max; return (0); case EVIOCGRAB: bitmap = evdev->ev_sw_flags; limit = SW_CNT; break; case EV_FF: /* * We don't support EV_FF now, so let's * just fake it returning only zeros. */ bzero(data, len); td->td_retval[0] = len; return (0); bitmap = evdev->ev_ff_flags; limit = FF_CNT; break; default: return (ENOTTY); } Parity verification
After the next kernel build + reboot, confirm:
-
dmesg | grep gpio_vibratorshows theGPIO vibratorattach. There should be nogpioledattach for avibratorLED. -
sysctl dev.gpio_vibrator.0.active dev.gpio_vibrator.0.active_effect dev.gpio_vibrator.0.play_count dev.gpio_vibrator.0.last_errorexists and starts at inactive / no active effect / no error. -
evtest /dev/input/eventNfor the new device reportsEV_FFandFF_RUMBLE.EVIOCGEFFECTSshould report8. -
The checked-in bench helper should upload and play one 300 ms effect:
mise run debug:vibrator:rumbleOptional knobs:
LENGTH_MS=500 MAGNITUDE=0xffff mise run debug:vibrator:rumble VIBRATOR_EVENT=/dev/input/event3 mise run debug:vibrator:rumbleThe wrapper copies
tools/test-vibrator-ff.cto the phone, compiles it with the phone’scc, runs it, then comparesplay_count,stop_count,last_error,active, andactive_effect. The driver also exposessuspended,suspend_count, andresume_count; across a future sleep cycle,activeshould be forced to0,active_effectshould be-1, and a play request while suspended should leavelast_error=16(EBUSY). -
The harness uploads the equivalent of:
struct ff_effect e = { .type = FF_RUMBLE, .id = 0, .u.rumble.strong_magnitude = 0xc000, .replay.length = 300, }; ioctl(fd, EVIOCSFF, &e); write_ev(fd, EV_FF, 0, 1);The motor should buzz for about 300 ms,
play_countshould increment, andactiveshould return to0.
Falsifier: if mise run debug:vibrator:rumble says the sysctls are missing, the new kernel is not running or the driver did not attach. If it cannot find an EV_FF / FF_RUMBLE event device, the evdev patch set did not apply or the kernel was built without device gpio_vibrator. If the command passes but the motor does not move, check last_error, then verify that the standalone DTS node has enable-gpios = <&gpio3 RK_PB1 GPIO_ACTIVE_HIGH> and that no gpioled child still owns the pin.
Remaining work
- Decide whether the local
EVIOCSFF_IOWRfix is acceptable upstream or should be handled as a FreeBSD-specific compatibility ioctl. - Bench suspend/resume once system sleep exists: confirm an in-flight rumble is cancelled,
VIB_ENstays low, and post-resume rumble still works. - If a future DTS adds
vcc-supply, bench that the rail enables only while an effect is active and shuts back off after timeout, explicit stop, suspend, and detach.
Related
- Where we are now — system peripherals status snapshot.
- Front-panel RGB LEDs — sibling
gpio-ledsconsumer the vibrator now joins. - Hardware reference — full chip manifest.