drivers/sensors/sensor: fix poll()/read() for fetch()-only sensors - #19596
Open
FelipeMdeO wants to merge 3 commits into
Open
drivers/sensors/sensor: fix poll()/read() for fetch()-only sensors#19596FelipeMdeO wants to merge 3 commits into
FelipeMdeO wants to merge 3 commits into
Conversation
FelipeMdeO
requested review from
Donny9,
acassis,
jerpelea,
linguini1,
raiden00pl and
xiaoxiang781216
as code owners
August 1, 2026 22:20
The driver had two modes selected by CONFIG_SENSORS_L3GD20_BUFFER_SIZE: with a buffer it pushed samples from a work queue, and without one it exposed fetch() while still using the data ready interrupt to signal readiness through notify_event. That second mode misuses the fetch interface. fetch() means the data is read from the device on demand and is therefore always available, while an interrupt driven sensor is exactly what push_event is for. Mixing the two forces the upper half to guess whether a fetch() only lower half will ever notify, and it makes poll() unusable in a multi descriptor loop, because the descriptor reports ready while the read still has to wait for the next interrupt. Drop the fetch path and always use the work queue and push_event, which is what the driver already did by default since BUFFER_SIZE defaults to 1. CONFIG_SENSORS_L3GD20_BUFFER_SIZE gains a range of 1 to 32, as a zero sized buffer no longer has a meaning, and SCHED_HPWORK is now selected unconditionally because the work queue is always used. No in tree configuration enables this driver and the previous default already took the push path, so no defconfig changes are needed. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Felipe Moura <moura.fmo@gmail.com>
FelipeMdeO
force-pushed
the
fix/uorb-sensor-fetch-only-pollin
branch
from
August 1, 2026 22:23
a71b6fc to
a133158
Compare
A fetch() only lower half reads the device on demand, so its data is always available and there is never anything to wait for. The upper half did not reflect that: poll() only reported POLLIN when the descriptor was opened O_NONBLOCK, and a blocking read() waited on buffersem, which is only posted when the lower half drives notify_event from an interrupt of its own. A fetch() only sensor with no interrupt therefore never satisfied poll()/read() at all. This is not hypothetical: in the in tree nucleo-h563zi:dts configuration CONFIG_STM32_DTS_TRIGGER defaults to 0, which selects stm32_dts_fetch(), and no CONFIG_STM32_DTS_ITEN_* option is enabled, so the DTS interrupt never fires. A blocking read() on that sensor waits forever, even though stm32_dts_fetch() performs a complete software triggered measurement on its own and needs no interrupt at all. Applications had to work around this by forcing O_NONBLOCK on the descriptor themselves, see apache/nuttx-apps#3686. Drop the O_NONBLOCK special case in both paths: sensor_poll() now always reports POLLIN for a fetch only sensor and sensor_read() calls fetch() directly instead of waiting. Update the sensor_ops_s::fetch documentation, which described the old contract. With the wait gone, buffersem has no waiters left. Its only two readers were the ones removed here, both in the fetch path: the wait in sensor_read() and the nxsem_get_value() in sensor_poll(). The remaining nxsem_post() calls in sensor_push_event() and sensor_notify_event() had nothing left to wake, so drop the semaphore and those posts as well. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Felipe Moura <moura.fmo@gmail.com>
A fetch() only lower half is always ready, so a subscriber that asked for a rate with SNIOC_SET_INTERVAL got no pacing from poll(): the descriptor reported POLLIN on every pass and the application had to sleep out the period itself. That does not compose. An application polling several topics reads them sequentially from one thread, so per read sleeps serialize: three topics at 10 Hz sleeping 100 ms each yield 3.3 Hz per topic rather than 10. Pace it where poll() can act on it instead. A subscriber that never requested a rate stays always ready, and one that did becomes ready once per its own interval, driven by a watchdog armed in sensor_poll(). This is the fetch() side of what sensor_is_updated() already does for a pushing lower half, so both models now honor a requested rate the same way. The wdog_s lives in sensor_user_s rather than in the device, so each subscriber is paced at its own interval instead of at the minimum across all of them, and the timer only runs while somebody is polling. The expiry runs in timer context and takes no lock: poll_notify() is safe from an interrupt handler, and a teardown that raced it has already cleared fds, which makes both the notify and the re-arm no-ops. Teardown therefore just cancels the watchdog where it clears fds, and a watchdog keeps this off the work queue entirely, which a fetch() only sensor exists to avoid. sensor_close() needs nothing of its own: poll_setup() holds a reference on the file for the duration of the poll, so file_close() cannot run until poll_teardown() has called sensor_poll() with setup false, and that already cancelled the watchdog. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Felipe Moura <moura.fmo@gmail.com>
FelipeMdeO
force-pushed
the
fix/uorb-sensor-fetch-only-pollin
branch
from
August 2, 2026 21:16
a133158 to
c1e9f81
Compare
Contributor
Author
|
Hello Xiang, I updated implementation to use wdog than LPWORKER. Also I updated the description to follow the new approach. Thanks for suggest wdog. |
xiaoxiang781216
approved these changes
Aug 3, 2026
Contributor
Author
|
Hello @linguini1 , can you take a look, please? I think you used uOrb recently, right? |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A
fetch()-only lower half reads the device on demand, so it's alwaysready, nothing to wait for. The upper half didn't reflect that:
sensor_poll()only reportedPOLLINwhen openedO_NONBLOCK, and ablocking
read()waited onbuffersem, which only a driver-ownedinterrupt (
notify_event) posts. Afetch()-only sensor with nointerrupt therefore never satisfied
poll()/read()at all — e.g.in-tree
nucleo-h563zi:dtshangs on a blockingread()today.Applications worked around this with
O_NONBLOCK(apache/nuttx-apps#3686);this PR fixes it at the root instead, per reviewer feedback there.
Three commits:
BUFFER_SIZE == 0mode setfetch()whilestill gating readiness on the data-ready interrupt. That mix was buggy,
not just inconsistent:
poll()reportedPOLLINfromO_NONBLOCK, notfrom the interrupt, so a multi-descriptor loop could read it stale or
twice. Now always
push_event, the path it already took by default; noin-tree config enables it.
sensor_poll()/sensor_read()always ready forfetch()-only. Theactual fix.
buffersemgoes with it: its only two readers were the onesremoved here, so the semaphore and its posts are now dead.
notify_eventis likewise dead for
fetch()-only lower halves, but I left that APIalone rather than widen the patch.
POLLINat the requested interval. A rate request got no helpfrom
poll(), and apps sleeping it out serialize across topics (three at10 Hz → 3.3 Hz each). A per-subscriber
wdog_sarmed insensor_poll()paces each at its own interval. A watchdog rather than a work item keeps
this off the work queue, which is what
fetch()exists to avoid: it runsin timer context and needs no lock, so teardown just cancels it where it
clears
fds, andsensor_close()needs nothing of its own.Impact
Any
fetch()-only uORB sensor (on-demand I2C/SPI, no interrupt) nowworks with
poll()/read()without the app forcingO_NONBLOCK, andpacing costs no thread and no work queue — it needs no
CONFIG_optionat all, where the earlier work-queue revision of this PR silently did
nothing without
CONFIG_SCHED_LPWORK. Every interrupt-driven sensor,including l3gd20 after commit 1, keeps blocking correctly until real new
data arrives; push semantics are unchanged, verified on both paths
(Testing).
Testing
Host: Ubuntu 24.04.3 LTS.
checkpatch.sh(style +-mcommit messages)clean on all 3 commits.
Compiles clean on 3 architectures touching the changed
drivers/sensors/sensor.c: xtensa-esp-elf-gcc 14.2.0 (esp32s3-devkit),arm-none-eabi-gcc 13.2.1 (
stm32f4discovery), andsim(x86_64).fetch()-only path — ESP32-S3-DevKitC + MPU6050 (GY-521) on I2C0
fetch()-only with no interrupt line, noO_NONBLOCKanywhere in theunmodified
apps/system/uorb/listener.c. Note this configuration doesnot enable
CONFIG_SCHED_LPWORK, so it is also the case the previouswork-queue revision could not have paced at all:
110 ms apart, the requested 10 Hz.
Per-subscriber pacing: two
uorb_listenerprocesses on the samesensor_accel0at once,-r 20and-r 5. Their samples landed ~60 msand ~210 ms apart respectively — each paced at its own interval, not at
the minimum across both.
Teardown under stress: 10 rounds of
uorb_listener -r 5 sensor_accel0 &followed by
SIGINTmid-poll.psafterward shows no leaked task and noarmed timer, and
uorb_listener -n 3 sensor_accel0right after stillcompletes 3/3.
push path — sim, exercising the
buffersemremovalsim:baromonitor, withuorb_generatoras the publisher:orb_publish()→
write()→sensor_write()→sensor_push_event(), which is exactlywhere the
nxsem_post(&user->buffersem)was removed.The ~210 ms deltas track the publisher's 5 Hz, so the subscriber is being
woken by each push rather than spinning or stalling.
Two subscribers at once against a 10 Hz publisher, one of them rate
limited: the plain one got 6/6 ~110 ms apart, the
-r 2one got 4/4~500 ms apart — both notified from the same
sensor_push_event()loop,and the push-side rate gating in
sensor_is_updated()is unaffected.Six sequential subscribe/teardown cycles on that topic all completed and
exited cleanly, with no leaked task. That also covers an edge case of
commit 3: a push sensor never takes the branch that arms the watchdog, so
teardown's
wd_cancel()always runs on a never-armedwdog_s. It is safeby construction (
WDOG_ISACTIVEtestsfunc != NULL, andusercomesfrom
kmm_zalloc), and this confirms it at runtime.