diff --git a/general/package/gpiostep-openipc/Config.in b/general/package/gpiostep-openipc/Config.in index 76082fca83..100ae8c52f 100644 --- a/general/package/gpiostep-openipc/Config.in +++ b/general/package/gpiostep-openipc/Config.in @@ -6,8 +6,12 @@ config BR2_PACKAGE_GPIOSTEP_OPENIPC userspace CLI (gpiostep-ctl). A clean reimplementation of the vendor gpioStep/motor behaviour seen on Goke GK7205V510 PTZ cameras: two 4-wire stepper coils driven over GPIO from kernel - context (steadier timing, lower CPU than the userspace - gpio-motors tool). + context, avoiding the per-write syscall cost of the userspace + gpio-motors tool. Sub-tick step delays busy-wait between + scheduler yields, because the kernels that ship this package + have no high-resolution timers and would otherwise round every + sleep up to a whole 10ms tick; the sub-tick pacing holds on an + idle core, not under load. Pin map defaults to the GK7205V510 (NC-IPTC2200_DL) layout and is overridable via module params, e.g. diff --git a/general/package/gpiostep-openipc/Readme.md b/general/package/gpiostep-openipc/Readme.md index 28bae5ea27..89803c790b 100644 --- a/general/package/gpiostep-openipc/Readme.md +++ b/general/package/gpiostep-openipc/Readme.md @@ -7,9 +7,15 @@ cameras whose pan/tilt motors are 4-wire steppers wired straight to GPIO This is the kernel-side counterpart to the userspace `gpio-motors` tool. Both implement the same 8-phase half-step sequence and the same ` ` command signature, so they can be compared 1:1. The -difference: `gpio-motors` toggles `/sys/class/gpio` (open/write/close per pin -per microstep) from userspace, whereas `gpiostep` does `gpio_set_value()` -directly in kernel context — steadier timing and far lower CPU. +difference: `gpio-motors` writes `/sys/class/gpio` from userspace, whereas +`gpiostep` does `gpio_set_value()` directly in kernel context, which skips the +per-write syscall cost. Timing granularity is the same for both: the kernels +that ship this package have no high-resolution timers, so any sleep rounds up +to a whole 10ms tick. Delays under a quarter tick therefore busy-wait between +scheduler yields in `gpiostep` (see `step_delay()` in `src/gpiostep.c`); longer +ones sleep and accept the rounding. The sub-tick pacing holds on an idle core: +under load the yield after each micro-step can hand the core away for several +ticks. ### Load diff --git a/general/package/gpiostep-openipc/src/gpiostep-ctl.c b/general/package/gpiostep-openipc/src/gpiostep-ctl.c index 67008602d5..5291a3552e 100644 --- a/general/package/gpiostep-openipc/src/gpiostep-ctl.c +++ b/general/package/gpiostep-openipc/src/gpiostep-ctl.c @@ -11,6 +11,7 @@ * gpiostep-ctl 0 -20 30 # tilt -20 steps */ #include +#include #include #include #include @@ -24,7 +25,7 @@ int main(int argc, char *argv[]) { struct gpiostep_move m; - int fd, ret; + int fd, ret, delay_ms; if (argc != 4) { fprintf(stderr, "Usage: %s \n", @@ -35,7 +36,12 @@ int main(int argc, char *argv[]) memset(&m, 0, sizeof(m)); m.pan = atoi(argv[1]); m.tilt = atoi(argv[2]); - m.delay_us = atoi(argv[3]) * 1000; + delay_ms = atoi(argv[3]); + if (delay_ms < 0 || delay_ms > INT_MAX / 1000) { + fprintf(stderr, "delay must be between 0 and %d ms\n", INT_MAX / 1000); + return 1; + } + m.delay_us = delay_ms * 1000; fd = open(DEV, O_RDWR); if (fd < 0) { diff --git a/general/package/gpiostep-openipc/src/gpiostep.c b/general/package/gpiostep-openipc/src/gpiostep.c index f28d238d20..0862c30fc1 100644 --- a/general/package/gpiostep-openipc/src/gpiostep.c +++ b/general/package/gpiostep-openipc/src/gpiostep.c @@ -4,10 +4,12 @@ * * A clean reimplementation of the vendor "gpioStep"/"motor" behaviour observed * on Goke GK7205V510 cameras (model NC-IPTC2200_DL): two 4-wire stepper coils - * driven over GPIO. Unlike the userspace gpio-motors tool (which does - * open/write/close on /sys/class/gpio per pin per microstep), the stepping here - * runs entirely in kernel context with direct gpio_set_value(), so timing is - * far steadier and CPU cost much lower. + * driven over GPIO. The stepping runs entirely in kernel context with direct + * gpio_set_value(), which avoids the syscall traffic of the userspace + * gpio-motors tool. Timing granularity, however, is still bounded by the + * tick on kernels without CONFIG_HIGH_RES_TIMERS - which is every kernel + * that ships this package - so sub-tick delays busy-wait between scheduler + * yields (see step_delay()). * * Control is via a misc char device /dev/motorDev and a single ioctl. The pin * map defaults to the GK7205V510 layout and is overridable with module params: @@ -20,9 +22,11 @@ #include #include #include +#include #include #include #include +#include #include #include "gpiostep.h" @@ -48,6 +52,44 @@ static const int rev_step_seq[8][4] = { static DEFINE_MUTEX(gpiostep_lock); +/* + * usleep_range() runs on hrtimers, but without CONFIG_HIGH_RES_TIMERS those + * expire with jiffy granularity, so a sub-tick sleep rounds up to the next + * tick (10ms at HZ=100) exactly like a userspace usleep - and every defconfig + * that ships this package builds such a kernel. Busy-wait instead while the + * requested delay is under a quarter tick, where that rounding would at least + * quadruple the step period; from a quarter tick up, sleep and accept the + * rounding, since the busy-wait cost grows with the delay while its benefit + * shrinks. The cond_resched() keeps a move from monopolising the core: these + * kernels are !SMP and !PREEMPT, so without it the encoder would not run at + * all until the whole move finished. It also means the sub-tick pacing only + * holds on an idle core - under load the yield can hand the core away for + * several ticks between two micro-steps. + * + * A zero delay keeps its usleep_range(0, 1). That is an already-expired + * hrtimer and returns at once - measured on a Hi3518EV200, 320 micro-steps at + * delay 0 finish in under 10ms with either version of this module - so zero + * has never had a floor; this just leaves that unchanged rather than sending + * it down the busy-wait path, where the guard would be the only thing between + * a negative and udelay(). + */ +static void step_delay(int delay_us) +{ + if (delay_us > 0 && !IS_ENABLED(CONFIG_HIGH_RES_TIMERS) && + delay_us < (int)(jiffies_to_usecs(1) / 4)) { + /* udelay() on ARM is bounded at ~2ms per call; chunk it */ + while (delay_us > 1000) { + udelay(1000); + delay_us -= 1000; + } + udelay(delay_us); + cond_resched(); + return; + } + + usleep_range(delay_us, delay_us + (delay_us >> 4) + 1); +} + static void axis_run(const int pins[4], int steps, int delay_us) { const int (*seq)[4] = (steps < 0) ? rev_step_seq : step_seq; @@ -62,7 +104,7 @@ static void axis_run(const int pins[4], int steps, int delay_us) for (i = 0; i < 4; i++) gpio_set_value(pins[i], seq[micro][i]); - usleep_range(delay_us, delay_us + (delay_us >> 4) + 1); + step_delay(delay_us); if (++micro >= 8) { micro = 0;