Skip to main content

oxstack_cardputer/
ir.rs

1//! IR transmitter on GPIO44 (LED driven directly through 22 Ω), using an
2//! RMT channel with hardware 38 kHz carrier modulation. Get it from
3//! [`crate::rmt::RmtResources::init`].
4//!
5//! Protocol encoding is in [`oxstack_ir`] (re-exported as [`protocols`]):
6//!
7//! ```ignore
8//! let (_led, mut ir) = board.rmt.init()?;
9//! ir.send_nec(0x04, 0x08).await?;
10//! ```
11//!
12//! ⚠️ The carrier timing assumes RMT carrier ticks count the 80 MHz group
13//! clock (ESP32-S3 TRM), not the per-channel divided clock. A phone camera
14//! shows that the LED fires; only a real IR receiver confirms the 38 kHz.
15
16use esp_hal::Async;
17use esp_hal::gpio::Level;
18use esp_hal::peripherals::GPIO44;
19use esp_hal::rmt::{Channel, ConfigError, Error, PulseCode, Tx, TxChannelConfig, TxChannelCreator};
20pub use oxstack_ir as protocols;
21use oxstack_ir::{Pulse, nec};
22
23use crate::led::RMT_MHZ;
24
25/// Most pulses one [`IrTx::send`] call accepts (one RMT memory block,
26/// minus the end marker).
27pub const MAX_PULSES: usize = 47;
28
29/// IR transmit errors.
30#[derive(Debug)]
31#[cfg_attr(feature = "defmt", derive(defmt::Format))]
32pub enum IrError {
33    /// More than [`MAX_PULSES`] pulses.
34    TooLong,
35    /// RMT failure.
36    Rmt(Error),
37}
38
39/// The IR transmitter.
40pub struct IrTx<'d> {
41    ch: Channel<'d, Async, Tx>,
42}
43
44impl<'d> IrTx<'d> {
45    /// Configures an RMT TX channel (1 µs ticks, 38 kHz carrier, idle off).
46    /// The `Rmt` must run at [`RMT_MHZ`].
47    pub fn new(
48        channel: impl TxChannelCreator<'d, Async>,
49        pin: GPIO44<'d>,
50    ) -> Result<Self, ConfigError> {
51        let carrier_ticks = (RMT_MHZ * 1_000_000 / nec::CARRIER_HZ) as u16;
52        let config = TxChannelConfig::default()
53            .with_clk_divider(RMT_MHZ as u8) // 1 tick = 1 µs
54            .with_idle_output(true)
55            .with_idle_output_level(Level::Low)
56            .with_carrier_modulation(true)
57            .with_carrier_level(Level::High)
58            .with_carrier_high(carrier_ticks / 2)
59            .with_carrier_low(carrier_ticks - carrier_ticks / 2);
60        Ok(Self {
61            ch: channel.configure_tx(&config)?.with_pin(pin),
62        })
63    }
64
65    /// Transmits mark/space pulses on the carrier.
66    pub async fn send(&mut self, pulses: &[Pulse]) -> Result<(), IrError> {
67        if pulses.len() > MAX_PULSES {
68            return Err(IrError::TooLong);
69        }
70        let mut codes = [PulseCode::end_marker(); MAX_PULSES + 1];
71        for (code, p) in codes.iter_mut().zip(pulses) {
72            // A zero-length space doubles as the end marker after a final mark.
73            *code = PulseCode::new(Level::High, p.mark_us, Level::Low, p.space_us);
74        }
75        self.ch
76            .transmit(&codes[..=pulses.len()])
77            .await
78            .map_err(IrError::Rmt)
79    }
80
81    /// Sends one standard NEC frame.
82    pub async fn send_nec(&mut self, address: u8, command: u8) -> Result<(), IrError> {
83        self.send(&nec::encode(address, command)).await
84    }
85}