Skip to content

drivers/sensors/sensor: fix poll()/read() for fetch()-only sensors - #19596

Open
FelipeMdeO wants to merge 3 commits into
apache:masterfrom
FelipeMdeO:fix/uorb-sensor-fetch-only-pollin
Open

drivers/sensors/sensor: fix poll()/read() for fetch()-only sensors#19596
FelipeMdeO wants to merge 3 commits into
apache:masterfrom
FelipeMdeO:fix/uorb-sensor-fetch-only-pollin

Conversation

@FelipeMdeO

@FelipeMdeO FelipeMdeO commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Summary

A fetch()-only lower half reads the device on demand, so it's always
ready, nothing to wait for. The upper half didn't reflect that:
sensor_poll() only reported POLLIN when opened O_NONBLOCK, and a
blocking read() waited on buffersem, which only a driver-owned
interrupt (notify_event) posts. A fetch()-only sensor with no
interrupt therefore never satisfied poll()/read() at all — e.g.
in-tree nucleo-h563zi:dts hangs on a blocking read() 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:

  1. l3gd20 → push-only. Its BUFFER_SIZE == 0 mode set fetch() while
    still gating readiness on the data-ready interrupt. That mix was buggy,
    not just inconsistent: poll() reported POLLIN from O_NONBLOCK, not
    from the interrupt, so a multi-descriptor loop could read it stale or
    twice. Now always push_event, the path it already took by default; no
    in-tree config enables it.
  2. sensor_poll()/sensor_read() always ready for fetch()-only. The
    actual fix. buffersem goes with it: its only two readers were the ones
    removed here, so the semaphore and its posts are now dead. notify_event
    is likewise dead for fetch()-only lower halves, but I left that API
    alone rather than widen the patch.
  3. Pace POLLIN at the requested interval. A rate request got no help
    from poll(), and apps sleeping it out serialize across topics (three at
    10 Hz → 3.3 Hz each). A per-subscriber wdog_s armed in sensor_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 runs
    in timer context and needs no lock, so teardown just cancels it where it
    clears fds, and sensor_close() needs nothing of its own.

Impact

Any fetch()-only uORB sensor (on-demand I2C/SPI, no interrupt) now
works with poll()/read() without the app forcing O_NONBLOCK, and
pacing costs no thread and no work queue — it needs no CONFIG_ option
at 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 + -m commit 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), and sim (x86_64).

fetch()-only path — ESP32-S3-DevKitC + MPU6050 (GY-521) on I2C0

fetch()-only with no interrupt line, no O_NONBLOCK anywhere in the
unmodified apps/system/uorb/listener.c. Note this configuration does
not enable CONFIG_SCHED_LPWORK, so it is also the case the previous
work-queue revision could not have paced at all:

nsh> uorb_listener -n 5 -r 10 sensor_accel0
Monitor objects num:1
object_name:sensor_accel, object_instance:0
sensor_accel(now:25470000):timestamp:25470000,x:-0.160412,y:-0.191536,z:10.292673,temperature:24.106468
sensor_accel(now:25580000):timestamp:25580000,x:-0.134075,y:-0.093373,z:10.297462,temperature:24.012352
sensor_accel(now:25690000):timestamp:25690000,x:-0.138864,y:-0.134075,z:10.192117,temperature:23.918234
sensor_accel(now:25800000):timestamp:25800000,x:-0.105345,y:-0.155623,z:10.362105,temperature:24.012352
sensor_accel(now:25910000):timestamp:25910000,x:-0.124498,y:-0.148441,z:10.285490,temperature:24.012352
Object name:sensor_accel0, received:5
Total number of received Message:5/5

110 ms apart, the requested 10 Hz.

Per-subscriber pacing: two uorb_listener processes on the same
sensor_accel0 at once, -r 20 and -r 5. Their samples landed ~60 ms
and ~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 SIGINT mid-poll. ps afterward shows no leaked task and no
armed timer, and uorb_listener -n 3 sensor_accel0 right after still
completes 3/3.

push path — sim, exercising the buffersem removal

sim:baromonitor, with uorb_generator as the publisher: orb_publish()
write()sensor_write()sensor_push_event(), which is exactly
where the nxsem_post(&user->buffersem) was removed.

nsh> uorb_generator -n 100 -r 5 -s -t sensor_baro0 timestamp:23191100,pressure:999.12,temperature:26.34 &
nsh> uorb_listener -n 5 sensor_baro0
Monitor objects num:1
object_name:sensor_baro, object_instance:0
sensor_baro(now:9070000):timestamp:8940000,pressure:999.119995,temperature:26.340000
sensor_baro(now:9150000):timestamp:9150000,pressure:999.119995,temperature:26.340000
sensor_baro(now:9360000):timestamp:9360000,pressure:999.119995,temperature:26.340000
sensor_baro(now:9570000):timestamp:9570000,pressure:999.119995,temperature:26.340000
sensor_baro(now:9780000):timestamp:9780000,pressure:999.119995,temperature:26.340000
Object name:sensor_baro0, received:5
Total number of received Message:5/5

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 2 one 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-armed wdog_s. It is safe
by construction (WDOG_ISACTIVE tests func != NULL, and user comes
from kmm_zalloc), and this confirms it at runtime.

@github-actions github-actions Bot added Area: Sensors Sensors issues Size: M The size of the change in this PR is medium labels Aug 1, 2026
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>
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

MemBrowse Memory Report

No memory changes detected for:

Comment thread drivers/sensors/sensor.c Outdated
Comment thread drivers/sensors/sensor.c
Comment thread drivers/sensors/sensor.c Outdated
Comment thread drivers/sensors/sensor.c Outdated
Comment thread drivers/sensors/sensor.c Outdated
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
FelipeMdeO force-pushed the fix/uorb-sensor-fetch-only-pollin branch from a133158 to c1e9f81 Compare August 2, 2026 21:16
@FelipeMdeO

Copy link
Copy Markdown
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.

@FelipeMdeO

Copy link
Copy Markdown
Contributor Author

Hello @linguini1 , can you take a look, please? I think you used uOrb recently, right?

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

Labels

Area: Sensors Sensors issues Size: M The size of the change in this PR is medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants