ove/async_runtime/gpio.rs
1// Copyright (C) 2026 Kamil Lulko <kamil.lulko@gmail.com>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4//
5// This file is part of oveRTOS.
6
7//! Async GPIO input wrapper using the existing `ove_gpio_irq_register`
8//! callback path — no new C-side API needed.
9//!
10//! The pattern differs slightly from stream / queue / sem / eventgroup
11//! because GPIO interrupts are edge-triggered events rather than
12//! "data became available". We pair an `AtomicWaker` with an
13//! `AtomicBool` latch: the IRQ trampoline sets the bool and wakes the
14//! waker; the consumer clears the bool on drain. This guards against
15//! the edge firing between user-side "register waker" and "go to
16//! sleep".
17
18use core::future::poll_fn;
19use core::sync::atomic::{AtomicBool, Ordering};
20use core::task::Poll;
21
22use embassy_sync::waitqueue::AtomicWaker;
23
24use crate::error::Result;
25use crate::gpio::{self, GpioIrqMode, GpioPin};
26
27/// Async wrapper around a GPIO input pin.
28///
29/// Methods take `&'static self` because the C-side `ove_gpio_irq_register`
30/// callback retains a pointer to the internal state.
31pub struct AsyncInput {
32 pin: GpioPin,
33 pending: AtomicBool,
34 waker: AtomicWaker,
35}
36
37// SAFETY: `AsyncInput` holds an `AtomicBool` + `AtomicWaker` (both already
38// `Sync`). No interior thread-bound state; the waker dispatch path uses
39// `critical_section` for synchronisation.
40unsafe impl Send for AsyncInput {}
41unsafe impl Sync for AsyncInput {}
42
43impl AsyncInput {
44 /// Wrap a GPIO pin for async edge-event handling.
45 pub const fn new(pin: GpioPin) -> Self {
46 Self {
47 pin,
48 pending: AtomicBool::new(false),
49 waker: AtomicWaker::new(),
50 }
51 }
52
53 /// Register the IRQ trampoline and enable interrupts for `mode`
54 /// (rising / falling / both). Must be called exactly once.
55 pub fn arm(&'static self, mode: GpioIrqMode) -> Result<()> {
56 // SAFETY: 'static self; trampoline reinterprets user_data as
57 // &'static AsyncInput.
58 unsafe {
59 gpio::irq_register(
60 self.pin,
61 mode,
62 Some(gpio_irq_trampoline),
63 self as *const Self as *mut core::ffi::c_void,
64 )?;
65 }
66 gpio::irq_enable(self.pin)
67 }
68
69 /// Await the next edge event. Returns immediately if an edge has
70 /// fired since the last call to this function.
71 pub async fn wait_for_event(&'static self) {
72 poll_fn(|cx| {
73 // Fast path: drain any latched edge.
74 if self.pending.swap(false, Ordering::AcqRel) {
75 return Poll::Ready(());
76 }
77 self.waker.register(cx.waker());
78 // Re-check after register to close the race where the IRQ
79 // fires between the first swap and the waker registration.
80 if self.pending.swap(false, Ordering::AcqRel) {
81 return Poll::Ready(());
82 }
83 Poll::Pending
84 })
85 .await
86 }
87
88 /// Read the pin's current logic level.
89 pub fn is_high(&self) -> Result<bool> {
90 gpio::get(self.pin).map(|v| v != 0)
91 }
92
93 #[inline]
94 pub fn pin(&self) -> GpioPin {
95 self.pin
96 }
97}
98
99unsafe extern "C" fn gpio_irq_trampoline(
100 _port: core::ffi::c_uint,
101 _pin: core::ffi::c_uint,
102 user_data: *mut core::ffi::c_void,
103) {
104 // SAFETY: user_data was set by AsyncInput::arm to &'static AsyncInput.
105 let this = unsafe { &*(user_data as *const AsyncInput) };
106 this.pending.store(true, Ordering::Release);
107 this.waker.wake();
108}