1use 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
17mod kepler;
22mod mapping;
23
24pub use kepler::{IrKepler, IrKeplerStatic, KeplerButton};
25pub use mapping::{IrMapping, IrMappingStatic};
26
27#[derive(Copy, Clone, Debug, PartialEq)]
33pub enum IrEvent {
34 Press {
37 addr: u16,
39 cmd: u8,
41 },
42}
43
44#[doc(hidden)] pub struct NecReceiver<'d, PIO: Instance, const SM: usize> {
49 sm: StateMachine<'d, PIO, SM>,
50}
51
52#[doc(hidden)]
58pub trait IrPioPeripheral: crate::pio_irqs::PioIrqMap {
59 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
101pub struct IrStatic(EmbassyChannel<CriticalSectionRawMutex, IrEvent, 8>);
105
106impl IrStatic {
107 #[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
122pub struct Ir<'a> {
150 ir_static: &'a IrStatic,
151}
152
153impl Ir<'_> {
154 #[must_use]
158 pub const fn new_static() -> IrStatic {
159 IrStatic::new()
160 }
161
162 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 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 let mut ir_pin = common.make_pio_pin(pin);
188 ir_pin.set_pull(Pull::Up);
189
190 let nec_receiver = NecReceiver::new(&mut common, sm0, ir_pin);
192
193 PIO::spawn_task(nec_receiver, ir_static, spawner)?;
195
196 Ok(Self { ir_static })
197 }
198
199 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 let raw_frame = nec_receiver.receive_frame().await;
215
216 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 let raw_frame = nec_receiver.receive_frame().await;
231
232 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 let raw_frame = nec_receiver.receive_frame().await;
248
249 if let Some((addr, cmd)) = decode_nec_frame(raw_frame) {
251 ir_static.send(IrEvent::Press { addr, cmd }).await;
252 }
253 }
254}
255
256impl<'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 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 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 cfg.fifo_join = FifoJoin::RxOnly;
305
306 cfg.set_in_pins(&[&ir_pin]);
308
309 cfg.set_jmp_pin(&ir_pin);
311
312 let clock_freq = 125_000_000.0_f32; let target_freq = 10.0_f32 / 562.5e-6_f32; let divisor: f32 = clock_freq / target_freq;
318 cfg.clock_divider = divisor.to_fixed();
319
320 let loaded_program = common.load_program(&prg.program);
322
323 cfg.use_program(&loaded_program, &[]);
325
326 sm.set_config(&cfg);
328 sm.set_pin_dirs(embassy_rp::pio::Direction::In, &[&ir_pin]);
329 sm.set_enable(true);
330
331 let _ = loaded_program;
333
334 Self { sm }
335 }
336
337 async fn receive_frame(&mut self) -> u32 {
339 self.sm.rx().wait_pull().await
340 }
341}
342
343fn 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 if (byte2 ^ byte3) != 0xFF {
362 return None;
363 }
364
365 if (byte0 ^ byte1) == 0xFF {
367 return Some((u16::from(byte0), byte2));
368 }
369
370 let addr16 = ((u16::from(byte1)) << 8) | u16::from(byte0);
372 Some((addr16, byte2))
373}