Appendix · reference

Vibration motor

Single GPIO-driven motor, now wired as a gpio-vibrator EV_FF candidate.

Identity

PartEccentric-rotating-mass vibration motor
RoleHaptic feedback / silent-mode notification
Bus / addressn/a — single GPIO output
GPIO / IRQGPIO3_B1 (active-high, drives VIB_EN net)
Datasheetnone — discrete motor + transistor switch
Pine64 wikiPinePhone Pro Hardware
Schematicsheet 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:

Driver

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

@@ -51,6 +51,12 @@
51 51
52 52 #define DEF_RING_REPORTS 8
53 53
54 + /*
55 + * FreeBSD historically defined EVIOCSFF as _IOW like Linux, but our ioctl
56 + * dispatcher only copies the updated effect id back for _IOWR commands.
57 + */
58 + #define EVIOCSFF_LEGACY _IOW(EVDEV_IOC_MAGIC, 0x80, struct ff_effect)
59 +
54 60 static d_open_t evdev_open;
55 61 static d_read_t evdev_read;
56 62 static d_write_t evdev_write;
@@ -536,7 +542,26 @@
536 542 case EVIOCSFF:
543 + case EVIOCSFF_LEGACY:
544 + if (!evdev_event_supported(evdev, EV_FF))
545 + return (ENOTSUP);
546 + if (evdev->ev_methods == NULL ||
547 + evdev->ev_methods->ev_upload_effect == NULL)
548 + return (ENOTSUP);
549 +
550 + return (evdev->ev_methods->ev_upload_effect(evdev,
551 + (struct ff_effect *)data));
552 +
537 553 case EVIOCRMFF:
554 + if (!evdev_event_supported(evdev, EV_FF))
555 + return (ENOTSUP);
556 + if (evdev->ev_methods == NULL ||
557 + evdev->ev_methods->ev_erase_effect == NULL)
558 + return (ENOTSUP);
559 +
560 + return (evdev->ev_methods->ev_erase_effect(evdev,
561 + *(int *)data));
562 +
538 563 case EVIOCGEFFECTS:
539 /* Fake unsupported ioctls */
564 + *(int *)data = evdev->ev_ff_effects_max;
540 565 return (0);
541 566
542 567 case EVIOCGRAB:
@@ -712,13 +735,9 @@
712 735 bitmap = evdev->ev_sw_flags;
713 736 limit = SW_CNT;
714 737 break;
715 738 case EV_FF:
716 /*
717 * We don't support EV_FF now, so let's
718 * just fake it returning only zeros.
719 */
720 bzero(data, len);
721 td->td_retval[0] = len;
722 return (0);
739 + bitmap = evdev->ev_ff_flags;
740 + limit = FF_CNT;
741 + break;
723 742 default:
724 743 return (ENOTTY);
725 744 }

Parity verification

After the next kernel build + reboot, confirm:

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