Skip to main content

device_envoy/
ir.rs

1//! A device abstraction for infrared receivers using the NEC protocol.
2//!
3//! See [`Ir`], [`IrMapping`], and [`IrKepler`] for usage examples.
4
5use embassy_executor::Spawner;
6use embassy_rp::Peri;
7use embassy_rp::gpio::{Pin, Pull};
8use embassy_rp::pio::{
9    Common, Config, FifoJoin, Instance, PioPin, ShiftConfig, ShiftDirection, StateMachine,
10};
11use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
12use embassy_sync::channel::Channel as EmbassyChannel;
13use fixed::traits::ToFixed;
14
15use crate::{Error, Result};
16
17// ============================================================================
18// Submodules
19// ============================================================================
20
21mod kepler;
22mod mapping;
23
24pub use kepler::{IrKepler, IrKeplerStatic, KeplerButton};
25pub use mapping::{IrMapping, IrMappingStatic};
26
27// ===== Public API ===========================================================
28
29/// Events received from the infrared receiver.
30///
31/// See [`Ir`] for usage examples.
32#[derive(Copy, Clone, Debug, PartialEq)]
33pub enum IrEvent {
34    /// Button press with 16-bit address and 8-bit command.
35    /// Supports both standard NEC (8-bit address) and extended NEC (16-bit address).
36    Press {
37        /// 16-bit device address (or 8-bit address in low byte for standard NEC).
38        addr: u16,
39        /// 8-bit command code.
40        cmd: u8,
41    },
42}
43
44// ===== NEC Receiver (forward declaration) ==================================
45
46/// NEC IR receiver using PIO
47#[doc(hidden)] // Internal helper type; not part of public API
48pub struct NecReceiver<'d, PIO: Instance, const SM: usize> {
49    sm: StateMachine<'d, PIO, SM>,
50}
51
52// ===== PIO Trait and Implementations =======================================
53
54/// Trait for PIO peripherals used with IR receivers.
55///
56/// This trait associates each PIO peripheral with its interrupt bindings.
57#[doc(hidden)]
58pub trait IrPioPeripheral: crate::pio_irqs::PioIrqMap {
59    /// Spawn the task for this PIO
60    fn spawn_task(
61        receiver: NecReceiver<'static, Self, 0>,
62        ir_static: &'static IrStatic,
63        spawner: Spawner,
64    ) -> Result<()>;
65}
66
67impl IrPioPeripheral for embassy_rp::peripherals::PIO0 {
68    fn spawn_task(
69        receiver: NecReceiver<'static, Self, 0>,
70        ir_static: &'static IrStatic,
71        spawner: Spawner,
72    ) -> Result<()> {
73        let token = ir_pio0_task(receiver, ir_static);
74        spawner.spawn(token).map_err(Error::TaskSpawn)
75    }
76}
77
78impl IrPioPeripheral for embassy_rp::peripherals::PIO1 {
79    fn spawn_task(
80        receiver: NecReceiver<'static, Self, 0>,
81        ir_static: &'static IrStatic,
82        spawner: Spawner,
83    ) -> Result<()> {
84        let token = ir_pio1_task(receiver, ir_static);
85        spawner.spawn(token).map_err(Error::TaskSpawn)
86    }
87}
88
89#[cfg(feature = "pico2")]
90impl IrPioPeripheral for embassy_rp::peripherals::PIO2 {
91    fn spawn_task(
92        receiver: NecReceiver<'static, Self, 0>,
93        ir_static: &'static IrStatic,
94        spawner: Spawner,
95    ) -> Result<()> {
96        let token = ir_pio2_task(receiver, ir_static);
97        spawner.spawn(token).map_err(Error::TaskSpawn)
98    }
99}
100
101/// Static resources for the [`Ir`] device abstraction.
102///
103/// See [`Ir`] for usage examples.
104pub struct IrStatic(EmbassyChannel<CriticalSectionRawMutex, IrEvent, 8>);
105
106impl IrStatic {
107    /// Creates static resources for the infrared receiver device.
108    #[must_use]
109    pub(crate) const fn new() -> Self {
110        Self(EmbassyChannel::new())
111    }
112
113    pub(crate) async fn send(&self, event: IrEvent) {
114        self.0.send(event).await;
115    }
116
117    pub(crate) async fn receive(&self) -> IrEvent {
118        self.0.receive().await
119    }
120}
121
122/// A device abstraction for an infrared receiver for NEC protocol decoding.
123///
124/// This implementation uses the RP2040's PIO state machine to decode NEC IR signals in hardware,
125/// making decoding reliable even when the CPU is busy with other tasks. Works with any PIO
126/// peripheral (PIO0, PIO1, or PIO2 on Pico 2).
127///
128/// # Examples
129/// ```rust,no_run
130/// # #![no_std]
131/// # #![no_main]
132/// use device_envoy::ir::{Ir, IrEvent, IrStatic};
133/// # #[panic_handler]
134/// # fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} }
135///
136/// async fn example(
137///     p: embassy_rp::Peripherals,
138///     spawner: embassy_executor::Spawner,
139/// ) -> device_envoy::Result<()> {
140///     static IR_STATIC: IrStatic = Ir::new_static();
141///     let ir = Ir::new(&IR_STATIC, p.PIN_15, p.PIO0, spawner)?;
142///
143///     loop {
144///         let IrEvent::Press { addr, cmd } = ir.wait_for_press().await;
145///         defmt::info!("IR: addr=0x{:04X}, cmd=0x{:02X}", addr, cmd);
146///     }
147/// }
148/// ```
149pub struct Ir<'a> {
150    ir_static: &'a IrStatic,
151}
152
153impl Ir<'_> {
154    /// Create static channel resources for IR events.
155    ///
156    /// See [`Ir`] for usage examples.
157    #[must_use]
158    pub const fn new_static() -> IrStatic {
159        IrStatic::new()
160    }
161
162    /// Create a new PIO-based IR receiver on the specified pin.
163    ///
164    /// See [`Ir`] for usage examples.
165    ///
166    /// # Errors
167    /// Returns an error if the background task cannot be spawned.
168    pub fn new<P, PIO>(
169        ir_static: &'static IrStatic,
170        pin: Peri<'static, P>,
171        pio: Peri<'static, PIO>,
172        spawner: Spawner,
173    ) -> Result<Self>
174    where
175        P: Pin + PioPin,
176        PIO: IrPioPeripheral,
177    {
178        // Set up PIO in the generic context where we have the concrete pin type
179        let pio_instance =
180            embassy_rp::pio::Pio::new(pio, <PIO as crate::pio_irqs::PioIrqMap>::irqs());
181        let embassy_rp::pio::Pio {
182            mut common, sm0, ..
183        } = pio_instance;
184
185        // Configure pin for IR receiver input with pull-up
186        // IR receivers idle HIGH and pull LOW when detecting carrier
187        let mut ir_pin = common.make_pio_pin(pin);
188        ir_pin.set_pull(Pull::Up);
189
190        // Load and configure the PIO program
191        let nec_receiver = NecReceiver::new(&mut common, sm0, ir_pin);
192
193        // Spawn the task with the configured receiver (dispatch to PIO-specific task)
194        PIO::spawn_task(nec_receiver, ir_static, spawner)?;
195
196        Ok(Self { ir_static })
197    }
198
199    /// Wait for the next IR event.
200    ///
201    /// See [`Ir`] for usage examples.
202    pub async fn wait_for_press(&self) -> IrEvent {
203        self.ir_static.receive().await
204    }
205}
206
207#[embassy_executor::task]
208async fn ir_pio0_task(
209    mut nec_receiver: NecReceiver<'static, embassy_rp::peripherals::PIO0, 0>,
210    ir_static: &'static IrStatic,
211) -> ! {
212    loop {
213        // Wait for a frame from the PIO FIFO
214        let raw_frame = nec_receiver.receive_frame().await;
215
216        // Decode and validate the frame
217        if let Some((addr, cmd)) = decode_nec_frame(raw_frame) {
218            ir_static.send(IrEvent::Press { addr, cmd }).await;
219        }
220    }
221}
222
223#[embassy_executor::task]
224async fn ir_pio1_task(
225    mut nec_receiver: NecReceiver<'static, embassy_rp::peripherals::PIO1, 0>,
226    ir_static: &'static IrStatic,
227) -> ! {
228    loop {
229        // Wait for a frame from the PIO FIFO
230        let raw_frame = nec_receiver.receive_frame().await;
231
232        // Decode and validate the frame
233        if let Some((addr, cmd)) = decode_nec_frame(raw_frame) {
234            ir_static.send(IrEvent::Press { addr, cmd }).await;
235        }
236    }
237}
238
239#[cfg(feature = "pico2")]
240#[embassy_executor::task]
241async fn ir_pio2_task(
242    mut nec_receiver: NecReceiver<'static, embassy_rp::peripherals::PIO2, 0>,
243    ir_static: &'static IrStatic,
244) -> ! {
245    loop {
246        // Wait for a frame from the PIO FIFO
247        let raw_frame = nec_receiver.receive_frame().await;
248
249        // Decode and validate the frame
250        if let Some((addr, cmd)) = decode_nec_frame(raw_frame) {
251            ir_static.send(IrEvent::Press { addr, cmd }).await;
252        }
253    }
254}
255
256// ===== NEC Receiver Implementation =========================================
257
258impl<'d, PIO: Instance, const SM: usize> NecReceiver<'d, PIO, SM> {
259    fn new(
260        common: &mut Common<'d, PIO>,
261        mut sm: StateMachine<'d, PIO, SM>,
262        ir_pin: embassy_rp::pio::Pin<'d, PIO>,
263    ) -> Self {
264        // PIO program (ported from nec_receive.pio)
265        let prg = pio::pio_asm!(
266            r#"
267            ; Constants for burst detection and bit sampling
268            ; These values are calibrated for 10 SM clock ticks per 562.5µs burst period
269            .define BURST_LOOP_COUNTER 30    ; threshold for sync burst detection
270            .define BIT_SAMPLE_DELAY 15      ; wait 1.5 burst periods before sampling
271
272            .wrap_target
273            next_burst:
274                set x, BURST_LOOP_COUNTER
275                wait 0 pin 0                 ; wait for burst to start (active low)
276
277            burst_loop:
278                jmp pin data_bit             ; burst ended before counter expired
279                jmp x-- burst_loop           ; keep waiting for burst to end
280
281                                             ; counter expired = sync burst detected
282                mov isr, null                ; reset ISR for new frame
283                wait 1 pin 0                 ; wait for sync burst to finish
284                jmp next_burst               ; ready for first data bit
285
286            data_bit:
287                nop [BIT_SAMPLE_DELAY - 1]   ; wait 1.5 burst periods
288                in pins, 1                   ; sample gap length: short=0, long=1
289                                             ; autopush after 32 bits
290            .wrap
291            "#
292        );
293
294        let mut cfg = Config::default();
295
296        // Input shift register: shift right, autopush after 32 bits
297        let mut shift_config = ShiftConfig::default();
298        shift_config.direction = ShiftDirection::Right;
299        shift_config.auto_fill = true;
300        shift_config.threshold = 32;
301        cfg.shift_in = shift_config;
302
303        // Join FIFOs to make a larger receive FIFO
304        cfg.fifo_join = FifoJoin::RxOnly;
305
306        // Set the IN pin for sampling
307        cfg.set_in_pins(&[&ir_pin]);
308
309        // Set the JMP pin for burst detection
310        cfg.set_jmp_pin(&ir_pin);
311
312        // Set clock divisor: 10 ticks per 562.5µs burst period
313        // System clock is typically 125 MHz
314        // Target: 10 / 562.5µs = 17,777.78 Hz
315        let clock_freq = 125_000_000.0_f32; // 125 MHz system clock
316        let target_freq = 10.0_f32 / 562.5e-6_f32; // 10 ticks per burst period
317        let divisor: f32 = clock_freq / target_freq;
318        cfg.clock_divider = divisor.to_fixed();
319
320        // Load the PIO program first
321        let loaded_program = common.load_program(&prg.program);
322
323        // Configure using the loaded program (sets wrap, origin, etc.)
324        cfg.use_program(&loaded_program, &[]);
325
326        // Initialize and start the state machine
327        sm.set_config(&cfg);
328        sm.set_pin_dirs(embassy_rp::pio::Direction::In, &[&ir_pin]);
329        sm.set_enable(true);
330
331        // Keep the loaded program to prevent deallocation
332        let _ = loaded_program;
333
334        Self { sm }
335    }
336
337    /// Wait for and receive a 32-bit NEC frame from the PIO FIFO
338    async fn receive_frame(&mut self) -> u32 {
339        self.sm.rx().wait_pull().await
340    }
341}
342
343/// Decode and validate a 32-bit NEC frame
344///
345/// NEC protocol structure (32 bits, LSB first):
346/// - Byte 0: Address (8 bits)
347/// - Byte 1: Address inverse (~Address)
348/// - Byte 2: Command (8 bits)
349/// - Byte 3: Command inverse (~Command)
350///
351/// Extended NEC uses 16-bit address (bytes 0-1) without inversion check
352///
353/// Returns `Some((address, command))` if valid, `None` if checksum fails
354fn decode_nec_frame(frame: u32) -> Option<(u16, u8)> {
355    let byte0 = (frame & 0xFF) as u8;
356    let byte1 = ((frame >> 8) & 0xFF) as u8;
357    let byte2 = ((frame >> 16) & 0xFF) as u8;
358    let byte3 = ((frame >> 24) & 0xFF) as u8;
359
360    // Validate command bytes (required in both standard and extended NEC)
361    if (byte2 ^ byte3) != 0xFF {
362        return None;
363    }
364
365    // Standard NEC: 8-bit address with inverse validation
366    if (byte0 ^ byte1) == 0xFF {
367        return Some((u16::from(byte0), byte2));
368    }
369
370    // Extended NEC: 16-bit address (no inversion check on address)
371    let addr16 = ((u16::from(byte1)) << 8) | u16::from(byte0);
372    Some((addr16, byte2))
373}