Skip to content

gpio-motors: spin only while the rounding error is material - #2393

Open
phedoreanu wants to merge 1 commit into
OpenIPC:masterfrom
phedoreanu:gpio-motors-spin-gate
Open

gpio-motors: spin only while the rounding error is material#2393
phedoreanu wants to merge 1 commit into
OpenIPC:masterfrom
phedoreanu:gpio-motors-spin-gate

Conversation

@phedoreanu

@phedoreanu phedoreanu commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #2247, picking up both points from the merge comment there.

Gate the spin on the error being material. delay_us() spun for any sub-tick delay, so its CPU cost rose exactly as its benefit fell: at delay 9 on an HZ=100 kernel that is ~14s of pinned core per 200-step move to shave an 11% timing error, with every clock_gettime in the spin a real syscall on gk7205v500 (CONFIG_ARM_ARCH_TIMER_VCT_ACCESS is not set, so no vDSO fast path). The sleep threshold is now a quarter tick instead of a whole one:

  • below a quarter tick (delay 1-2 at HZ=100), sleeping would at least quadruple the step period, so the spin stays — this is the range the spin was added for;
  • from a quarter tick up (delay 3+), usleep() and accept the rounding.

Against the table in the merge comment, that keeps the 900%/400% rows spinning and puts the 100% and 11% rows to sleep, including the 14.4s worst case.

What the threshold costs. The merge-comment table listed 1, 2, 5 and 9 ms; the two rows it skipped are the ones this change gives back to the tick, so here they are on the record (200 steps = 1600 micro-steps, HZ=100):

delay before (#2247) after (this PR)
3 ms spins, 4.8 s move, core pinned sleeps at 10 ms, 16 s move
4 ms spins, 6.4 s move, core pinned sleeps at 10 ms, 16 s move

Delay 4 is the number #2247 opened with ("still 18s at delay 4" on the Hi3518EV200), so this hands that measurement back in exchange for not pinning the core at 4.8-6.4 s per move. Anyone who wants the faster move at 3-4 ms can ask for 2 and get it at 3.2 s with the spin; that is the trade the threshold makes.

Check clock_gettime() inside the spin loop. An unchecked failure would leave now stale and the loop would never terminate. On failure it now falls back to usleep() for the full delay, same as the existing pre-loop failure path — over-waiting is the safe direction for a stepper.

Compiles clean with -Wall -Wextra.

Measured on a Hi3518EV200 (same kernel class as gk7205v200/v500: HZ=100, no high-resolution timers, no preempt; clock_getres = 10 ms), with majestic streaming: usleep() lands on 10 ms at every delay; the merged code spins on every row at 72-76% CPU, with the 9 ms row burning 11.8 s of CPU to finish 0.7 s sooner than sleeping; this PR keeps the 1 and 2 ms rows identical to the merged code and sends 3, 4 and 9 ms to the tick at 1.7% CPU. Full table, raw output and harness source: #2393 (comment)

The motor half of the evidence needs a gk7205v500/v510 owner: BR2_PACKAGE_GPIO_MOTORS=y only exists in those defconfigs, and nobody in the #2247 thread has one. Whether the new pacing at 3-4 ms slips or stalls a coil is the part the primitive test cannot answer.

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 11, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Limit GPIO motor busy-waiting to material timer rounding

🐞 Bug fix ✨ Enhancement 🕐 10-20 Minutes

Grey Divider

AI Description

• Busy-wait only for delays below one quarter of a coarse timer tick.
• Sleep for longer delays to avoid prolonged CPU pinning.
• Fall back to usleep() when monotonic clock reads fail during spinning.
Diagram

graph TD
  A["Delay request"] --> B{"Positive delay?"}
  B -- "No" --> C["Return"]
  B -- "Yes" --> D{"Sleep preferred?"}
  D -- "Yes" --> E["Sleep delay"] --> C
  D -- "No" --> F["Monotonic spin"]
  F -- "Clock error" --> E
  F -- "Target reached" --> C
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Configurable spin threshold
  • ➕ Allows device owners to preserve faster 3–4 ms motor pacing when required.
  • ➕ Supports hardware-specific CPU and coil-timing tradeoffs.
  • ➖ Adds configuration and validation complexity for a narrow timing primitive.
  • ➖ Requires reliable device-specific measurements to choose safe values.
  • ➖ Can perpetuate excessive CPU usage through poor defaults.
2. Sleep-then-spin hybrid
  • ➕ Could reduce busy-wait duration on kernels with sufficiently precise wakeups.
  • ➕ Can approach requested deadlines with lower CPU consumption.
  • ➖ Coarse-timer kernels may oversleep by a full tick before spinning begins.
  • ➖ Does not solve the targeted sub-tick behavior on HZ=100 devices.
  • ➖ Introduces more timing calculations and boundary cases.

Recommendation: Use the fixed quarter-tick threshold in this PR. It directly bounds CPU pinning using clock resolution already available to the function, preserves spinning where coarse sleep causes the largest proportional error, and adds no operational configuration. A configurable threshold should only follow if gk7205v500/v510 motor testing demonstrates that 3–4 ms sleep rounding causes real coil slips or stalls.

Files changed (1) +15 / -8

Bug fix (1) +15 / -8
gpio-motors.cRestrict motor delay spinning and handle clock failures +15/-8

Restrict motor delay spinning and handle clock failures

• Changes 'delay_us()' to busy-wait only below one quarter of a coarse timer tick, allowing longer delays to use 'usleep()' and avoid sustained CPU pinning. Checks 'clock_gettime()' inside the spin loop and falls back to a full sleep on failure, preventing a stale timestamp from causing an infinite loop. Expands the timing rationale in the function documentation.

general/package/gpio-motors/src/gpio-motors.c

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 11, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Action required

1. Motor timing ships without field proof 📘 Rule violation ☼ Reliability
Description
delay_us() lowers the coarse-timer spin threshold from one tick to a quarter tick without
measurements from an affected board, sending quarter-tick-and-longer motor delays to usleep() and
changing step duration and CPU use. Each motor micro-step reaches this function in deployable lite
and ultimate gk7205v500 images and the lite gk7205v510 image, while the PR explicitly says the
behavior was not tested on that hardware.
Code

general/package/gpio-motors/src/gpio-motors.c[185]

+	if (CLOCK_RES_NS <= 1000000 || us >= CLOCK_RES_NS / 4000) {
Evidence
The changed branch at lines 185–187 replaces spinning with sleeping at the new quarter-tick
threshold, and each motor micro-step passes its configured delay through delay_us(). The package
is enabled in the lite and ultimate gk7205v500 images and the lite gk7205v510 image, establishing
that the unmeasured timing and CPU-use changes reach deployable camera firmware; the PR description
confirms there are no before-and-after measurements from the affected hardware, despite Compliance
ID 1 requiring real-camera evidence for firmware behavior changes.

Rule 1: Hardware evidence is present and honest
general/package/gpio-motors/src/gpio-motors.c[185-185]
general/package/gpio-motors/src/gpio-motors.c[180-188]
general/package/gpio-motors/src/gpio-motors.c[223-230]
br-ext-chip-goke/configs/gk7205v500_lite_defconfig[50-54]
br-ext-chip-goke/configs/gk7205v500_ultimate_defconfig[57-62]
br-ext-chip-goke/configs/gk7205v510_lite_defconfig[50-54]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new sleep threshold changes observable motor timing and processor utilization in production camera images, but the PR provides no before-and-after evidence from an affected gk7205v500 or gk7205v510 board.
## Fix Focus Areas
- general/package/gpio-motors/src/gpio-motors.c[185-187]
## Recommended Fix
Run before-and-after tests on a supported gk7205v500 or gk7205v510 camera using the relevant coarse-timer kernel. Record motor completion times and CPU utilization for delays around the threshold, including 1, 2, 3, and 9 ms, and add the commands, results, board model, and timer configuration to the PR description before merging.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: ⚖️ Balanced: This changes runtime motor timing and error handling on deployed camera images, with hardware-dependent behavior and a shared package path, so it warrants a complete review.

Grey Divider

Tip of the day
💡 Did you know, you can switch off images and animations for a plain-text comment

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous reviews

Review updated until commit 664da4a ⚖️ Balanced

Results up to commit N/A


🐞 Bugs (0) 📘 Rule violations (1) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Action required
1. Motor timing ships without field proof 📘 Rule violation ☼ Reliability
Description
delay_us() lowers the coarse-timer spin threshold from one tick to a quarter tick without
measurements from an affected board, sending quarter-tick-and-longer motor delays to usleep() and
changing step duration and CPU use. Each motor micro-step reaches this function in deployable lite
and ultimate gk7205v500 images and the lite gk7205v510 image, while the PR explicitly says the
behavior was not tested on that hardware.
Code

general/package/gpio-motors/src/gpio-motors.c[185]

+	if (CLOCK_RES_NS <= 1000000 || us >= CLOCK_RES_NS / 4000) {
Evidence
The changed branch at lines 185–187 replaces spinning with sleeping at the new quarter-tick
threshold, and each motor micro-step passes its configured delay through delay_us(). The package
is enabled in the lite and ultimate gk7205v500 images and the lite gk7205v510 image, establishing
that the unmeasured timing and CPU-use changes reach deployable camera firmware; the PR description
confirms there are no before-and-after measurements from the affected hardware, despite Compliance
ID 1 requiring real-camera evidence for firmware behavior changes.

Rule 1: Hardware evidence is present and honest
general/package/gpio-motors/src/gpio-motors.c[185-185]
general/package/gpio-motors/src/gpio-motors.c[180-188]
general/package/gpio-motors/src/gpio-motors.c[223-230]
br-ext-chip-goke/configs/gk7205v500_lite_defconfig[50-54]
br-ext-chip-goke/configs/gk7205v500_ultimate_defconfig[57-62]
br-ext-chip-goke/configs/gk7205v510_lite_defconfig[50-54]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new sleep threshold changes observable motor timing and processor utilization in production camera images, but the PR provides no before-and-after evidence from an affected gk7205v500 or gk7205v510 board.
## Fix Focus Areas
- general/package/gpio-motors/src/gpio-motors.c[185-187]
## Recommended Fix
Run before-and-after tests on a supported gk7205v500 or gk7205v510 camera using the relevant coarse-timer kernel. Record motor completion times and CPU utilization for delays around the threshold, including 1, 2, 3, and 9 ms, and add the commands, results, board model, and timer configuration to the PR description before merging.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread general/package/gpio-motors/src/gpio-motors.c

@openipc-ai openipc-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both items from the merge comment are here, and the arithmetic checks out: CLOCK_RES_NS / 4000 is 2500 us at HZ=100, so the spin survives exactly where I asked it to and the 14.4 s tail is gone.

Two notes inline: the new threshold quietly costs the two rows my table skipped, and the clock_gettime fallback over-waits by a tick.

Qodo's rule violation is correct, and it is still open - you answered it at 09:18, but answering is not the same as clearing it. This changes motor pacing in three shipped images with no before/after from a board that runs the code. best_practices.md 5.3 says flag and close, and I am not going to pretend otherwise because I asked for the change.

That said, I created this situation - my own scope note on #2247 conceded nobody in that thread has a gk7205v500-family board, and the lab here does not have one either. So let me split the ask rather than block on the impossible half:

  • The delay primitive is measurable without a PTZ board. I measured usleep() quantisation on the lab gk7205v200 in #2247 - same HZ=100, same # CONFIG_HIGH_RES_TIMERS is not set, same # CONFIG_ARM_ARCH_TIMER_VCT_ACCESS is not set. A short harness calling the new delay_us() at 1, 2, 3, 4 and 9 ms, reporting wall-clock and CPU time per call, would settle the threshold behaviour on the same hardware class. That is a real before/after, not a paste from a board that was not exercising the change.
  • Motor behaviour needs a v500/v510 owner. Whether the new pacing at delay 3-4 slips or stalls a coil is not something the primitive test can answer.

Your point that the threshold is read off the measured table rather than guessed is fair, and I am not asking you to re-derive it - only to show the primitive behaving as the table predicts on a board of that kernel class.

Please mark this draft until the first half is attached. Separately: CI has not run on this branch at all - no checks reported - and it is behind master. Rebase and let the matrix go green before this is mergeable either way.

Comment thread general/package/gpio-motors/src/gpio-motors.c
Comment thread general/package/gpio-motors/src/gpio-motors.c
@phedoreanu
phedoreanu marked this pull request as draft September 11, 2026 13:58
The sub-tick spin in delay_us() ran for any delay under a tick, so its
cost rose exactly as its benefit fell: at delay 9 on an HZ=100 kernel it
pinned the core for ~14s over a 200-step move to avoid an 11% timing
error, and on cores that cannot read the arch timer from userspace
(CONFIG_ARM_ARCH_TIMER_VCT_ACCESS unset, as on gk7205v500) every
clock_gettime in that spin is a real syscall.

Sleep from a quarter tick upward and accept the rounding; keep the spin
below that, where sleeping would at least quadruple the step period -
the 1-2ms range the spin was added for.

Also check clock_gettime() inside the spin loop; an unchecked failure
would leave `now` stale and the loop would never terminate. It falls
back to usleep() like the existing pre-loop failure path.
@phedoreanu
phedoreanu force-pushed the gpio-motors-spin-gate branch from f7ab7c9 to 664da4a Compare September 11, 2026 14:00
@phedoreanu

phedoreanu commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Measured, as asked — the primitive half, on a board of the same kernel class.

Board: Hi3518EV200 (ARM926EJ-S), OpenIPC 4.9.37, CONFIG_HZ=100, # CONFIG_HIGH_RES_TIMERS is not set, # CONFIG_PREEMPT is not set, # CONFIG_SMP is not set. The harness reports clock_getres(CLOCK_MONOTONIC) = 10000000 ns, so it takes the coarse path exactly like gk7205v200/v500. majestic was streaming throughout (load 0.6-1.4), so this is a loaded core, not an idle one.

Harness: one static binary with the two delay_us() bodies copied verbatim — before is #2247 as merged (sleep at or above one tick), after is this PR (sleep at or above a quarter tick) — plus plain usleep() as the baseline. 1600 calls per row, i.e. one 200-step move. Wall time from CLOCK_MONOTONIC, CPU from getrusage() (user + sys, so the sampling is tick-granular: read the CPU column as ±10 ms).

delay usleep before (#2247) after (this PR)
1 ms 9.996 ms/call, 1.7% CPU 1.294 ms/call, 76% CPU 1.300 ms/call, 76% CPU
2 ms 9.999 ms/call, 1.7% CPU 2.504 ms/call, 76% CPU 2.689 ms/call, 70% CPU
3 ms 10.013 ms/call, 1.7% CPU 3.836 ms/call, 72% CPU 10.013 ms/call, 1.7% CPU
4 ms 10.006 ms/call, 1.7% CPU 4.915 ms/call, 73% CPU 10.010 ms/call, 1.7% CPU
9 ms 10.400 ms/call, 1.6% CPU 9.980 ms/call, 74% CPU 10.426 ms/call, 1.7% CPU

Reading it:

  • usleep() lands on 10 ms at every delay — the tick floor from gpio-motors: deliver sub-tick delays on HZ=100 kernels #2247, reproduced on the same class.
  • before spins on every row. The 9 ms row is the tail from your table: 15.97 s wall against 16.64 s for sleeping, so 0.7 s saved for 11.8 s of CPU.
  • after is identical to before at 1 and 2 ms (within noise), and sends 3, 4 and 9 ms to the tick at 1.7% CPU. The 3 and 4 ms rows are the cost the PR body now lists — 6.1 s and 7.9 s moves become 16 s.
  • The spin rows overrun the requested delay (1.29 ms for a 1 ms ask, 4.9 for 4) because the encoder takes its tick slices; a spinning process is still preempted. That is the contrast with the kernel-side busy-wait in gpiostep: busy-wait sub-tick delays; correct the timing claim #2394, which is why that one carries a cond_resched().
Raw output
clock_getres(CLOCK_MONOTONIC) = 10000000 ns; 1600 calls per row (200 steps x 8 micro-steps)
usleep   1 ms x1600  wall  15993.8 ms ( 9.996 ms/call)  cpu    270.0 ms (  1.7%)
before   1 ms x1600  wall   2070.9 ms ( 1.294 ms/call)  cpu   1580.0 ms ( 76.3%)
after    1 ms x1600  wall   2080.2 ms ( 1.300 ms/call)  cpu   1590.0 ms ( 76.4%)

usleep   2 ms x1600  wall  15998.9 ms ( 9.999 ms/call)  cpu    270.0 ms (  1.7%)
before   2 ms x1600  wall   4006.6 ms ( 2.504 ms/call)  cpu   3030.0 ms ( 75.6%)
after    2 ms x1600  wall   4302.0 ms ( 2.689 ms/call)  cpu   3010.0 ms ( 70.0%)

usleep   3 ms x1600  wall  16021.2 ms (10.013 ms/call)  cpu    270.0 ms (  1.7%)
before   3 ms x1600  wall   6137.7 ms ( 3.836 ms/call)  cpu   4390.0 ms ( 71.5%)
after    3 ms x1600  wall  16021.5 ms (10.013 ms/call)  cpu    270.0 ms (  1.7%)

usleep   4 ms x1600  wall  16010.1 ms (10.006 ms/call)  cpu    270.0 ms (  1.7%)
before   4 ms x1600  wall   7864.1 ms ( 4.915 ms/call)  cpu   5710.0 ms ( 72.6%)
after    4 ms x1600  wall  16015.9 ms (10.010 ms/call)  cpu    270.0 ms (  1.7%)

usleep   9 ms x1600  wall  16640.1 ms (10.400 ms/call)  cpu    270.0 ms (  1.6%)
before   9 ms x1600  wall  15967.8 ms ( 9.980 ms/call)  cpu  11800.0 ms ( 73.9%)
after    9 ms x1600  wall  16682.2 ms (10.426 ms/call)  cpu    280.0 ms (  1.7%)
Harness source (delay-bench.c, built with arm-openipc-linux-musleabi-gcc -O2 -static)
/*
 * delay-bench: wall-clock and CPU cost of the gpio-motors delay_us()
 * primitive, before (#2247: spin below one tick) and after (#2393: spin
 * below a quarter tick), plus plain usleep() as the baseline. Both
 * delay_us() bodies are copied verbatim from the respective commits.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/resource.h>

long CLOCK_RES_NS = 0;

static void spin_to(struct timespec start, long us) {
	struct timespec now;
	long long target = (long long)us * 1000;
	for (;;) {
		if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) {
			usleep(us);
			return;
		}
		long long elapsed = (long long)(now.tv_sec - start.tv_sec) * 1000000000LL + (now.tv_nsec - start.tv_nsec);
		if (elapsed >= target)
			return;
	}
}

/* #2247 as merged: sleep at or above one tick, spin below */
static void delay_before(long us) {
	if (us <= 0) return;
	if (CLOCK_RES_NS <= 1000000 || us >= CLOCK_RES_NS / 1000) { usleep(us); return; }
	struct timespec start;
	if (clock_gettime(CLOCK_MONOTONIC, &start) != 0) { usleep(us); return; }
	spin_to(start, us);
}

/* #2393: sleep at or above a quarter tick, spin below */
static void delay_after(long us) {
	if (us <= 0) return;
	if (CLOCK_RES_NS <= 1000000 || us >= CLOCK_RES_NS / 4000) { usleep(us); return; }
	struct timespec start;
	if (clock_gettime(CLOCK_MONOTONIC, &start) != 0) { usleep(us); return; }
	spin_to(start, us);
}

static void delay_usleep(long us) { usleep(us); }

static double ts_diff(struct timespec a, struct timespec b) {
	return (b.tv_sec - a.tv_sec) * 1e3 + (b.tv_nsec - a.tv_nsec) / 1e6;
}

static double ru_ms(struct rusage r) {
	return (r.ru_utime.tv_sec + r.ru_stime.tv_sec) * 1e3 + (r.ru_utime.tv_usec + r.ru_stime.tv_usec) / 1e3;
}

static void bench(const char *name, void (*fn)(long), long us, int calls) {
	struct timespec t0, t1;
	struct rusage r0, r1;
	getrusage(RUSAGE_SELF, &r0);
	clock_gettime(CLOCK_MONOTONIC, &t0);
	for (int i = 0; i < calls; i++)
		fn(us);
	clock_gettime(CLOCK_MONOTONIC, &t1);
	getrusage(RUSAGE_SELF, &r1);
	double wall = ts_diff(t0, t1), cpu = ru_ms(r1) - ru_ms(r0);
	printf("%-7s %2ld ms x%4d  wall %8.1f ms (%6.3f ms/call)  cpu %8.1f ms (%5.1f%%)\n",
	       name, us / 1000, calls, wall, wall / calls, cpu, 100.0 * cpu / wall);
}

int main(int argc, char *argv[]) {
	int calls = argc > 1 ? atoi(argv[1]) : 1600;
	struct timespec res;
	if (clock_getres(CLOCK_MONOTONIC, &res) == 0)
		CLOCK_RES_NS = res.tv_sec ? 1000000000L : res.tv_nsec;
	printf("clock_getres(CLOCK_MONOTONIC) = %ld ns; %d calls per row (200 steps x 8 micro-steps)\n", CLOCK_RES_NS, calls);
	long delays[] = {1, 2, 3, 4, 9};
	for (unsigned i = 0; i < sizeof delays / sizeof *delays; i++) {
		long us = delays[i] * 1000;
		bench("usleep", delay_usleep, us, calls);
		bench("before", delay_before, us, calls);
		bench("after", delay_after, us, calls);
		printf("\n");
	}
	return 0;
}

The motor half stays open for a v500/v510 owner, as agreed. Marking ready for review with the primitive measurement attached; branch is rebased onto current master.

On CI: the runs for this head exist — build, lint, shell-tests, gcc-compat and qodo-gate all fired on the rebase — but every one is sitting at action_required, which is the fork-PR approval gate. They need a maintainer's "Approve and run" before anything reports; I can't clear that from my side.

@phedoreanu
phedoreanu marked this pull request as ready for review September 11, 2026 14:13
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 664da4a

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants