lilkaoxide/i2s.rs
1use esp_hal::{
2 dma_buffers,
3 gpio::interconnect::PeripheralOutput,
4 i2s::master::{Config, I2s},
5 peripherals::{DMA_CH0, I2S0},
6};
7#[cfg(feature = "blocking")]
8type DriveMode = esp_hal::Blocking;
9#[cfg(feature = "async")]
10type DriveMode = esp_hal::Async;
11
12/// Alias for the I2S transmitter handle, parameterised by the blocking/async drive mode.
13///
14/// * In **blocking** builds: `I2sTx<'d>` = `esp_hal::i2s::master::I2sTx<'d, Blocking>`
15/// * In **async** builds: `I2sTx<'d>` = `esp_hal::i2s::master::I2sTx<'d, Async>`
16///
17/// Obtained via [`crate::Lilka::i2s_tx`] after calling [`crate::Lilka::init`].
18pub type I2sTx<'d> = esp_hal::i2s::master::I2sTx<'d, DriveMode>;
19
20/// Default DMA TX buffer size for the I2S transmitter (in bytes).
21///
22/// Equals `4 × 4096 = 16 384` bytes, enough for ~93 ms of 44 100 Hz stereo 16-bit audio.
23/// Tune this value if you need lower latency (smaller buffer) or gapless playback
24/// (larger buffer), keeping in mind that the buffer lives in static memory.
25pub const DEFAULT_DMA_TX_BUF_SIZE: usize = 4 * 4096;
26
27/// Error returned when the I2S0 peripheral fails to initialise.
28///
29/// Propagated as [`crate::InitError::I2s`] from [`crate::Lilka::init`].
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum I2sInitError {
32 /// I2S0 peripheral or DMA channel could not be configured.
33 PeripheralInitFailed,
34}
35
36impl I2sInitError {
37 /// Returns a short, `'static` error description suitable for `defmt` logging.
38 pub const fn as_str(self) -> &'static str {
39 match self {
40 Self::PeripheralInitFailed => "I2S init failed",
41 }
42 }
43}
44
45/// TDM (Time-Division Multiplexing) framing standard for I2S output.
46///
47/// Selects the bit-clock / word-select phase relationship for audio data framing.
48/// The default [`I2sTxConfig`] uses [`TDMStandart::Phillips`].
49///
50/// # Choosing a standard
51///
52/// | Variant | Use case |
53/// |---|---|
54/// | `Phillips` | Most DACs (PCM5102A, MAX98357A, …). **Default.** |
55/// | `MSB` | Some older DACs that expect data on the rising BCLK edge. |
56/// | `PCM` | PCM voice codecs (short sync pulse). |
57/// | `PCM_Long` | PCM codecs requiring a long sync pulse. |
58pub enum TDMStandart {
59 /// I²S Philips standard — WS changes one BCLK cycle before the MSB.
60 Phillips,
61 /// MSB-justified — data starts immediately on the WS edge.
62 MSB,
63 /// PCM short-sync — one-cycle WS pulse at the start of each frame.
64 PCM,
65 /// PCM long-sync — WS stays high for the duration of the first channel slot.
66 PCM_Long,
67}
68
69macro_rules! tdm_config {
70 ($standard:expr, $cfg:ident => $($method:tt)*) => {
71 match $standard {
72 TDMStandart::Phillips => Config::new_tdm_philips()$($method)*,
73 TDMStandart::MSB => Config::new_tdm_msb()$($method)*,
74 TDMStandart::PCM => Config::new_tdm_pcm_short()$($method)*,
75 TDMStandart::PCM_Long => Config::new_tdm_pcm_long()$($method)*,
76 }
77 };
78}
79
80/// Configuration for the I2S0 transmitter.
81///
82/// Passed as part of [`crate::LilkaConfiguration`] to [`crate::Lilka::init`].
83///
84/// # Defaults
85///
86/// | Field | Default | Notes |
87/// |---|---|---|
88/// | `sample_rate` | 44 100 Hz | CD-quality audio. |
89/// | `channels` | `STEREO` | Left + right. |
90/// | `data_format` | `Data16Channel16` | 16-bit samples, 16-bit channel slots. |
91/// | `tdm_standard` | `Phillips` | Compatible with most I2S DACs. |
92///
93/// # Example — custom sample rate
94///
95/// ```rust,no_run
96/// use lilkaoxide::{I2sTxConfig, LilkaConfiguration};
97/// use lilkaoxide::i2s::TDMStandart;
98/// use esp_hal::i2s::master::{Channels, DataFormat};
99/// use esp_hal::time::Rate;
100///
101/// let config = LilkaConfiguration {
102/// i2s_tx_config: I2sTxConfig {
103/// sample_rate: Rate::from_hz(22_050),
104/// channels: Channels::STEREO,
105/// data_format: DataFormat::Data16Channel16,
106/// tdm_standard: TDMStandart::Phillips,
107/// },
108/// ..LilkaConfiguration::default()
109/// };
110/// ```
111pub struct I2sTxConfig {
112 /// Audio sample rate in Hz. Common values: 8 000, 22 050, 44 100, 48 000.
113 pub sample_rate: esp_hal::time::Rate,
114 /// Number of audio channels. Use `Channels::STEREO` (default) or `Channels::MONO`.
115 pub channels: esp_hal::i2s::master::Channels,
116 /// Bit depth per sample and per channel slot. `Data16Channel16` is the standard
117 /// choice for 16-bit PCM audio with 16-bit-wide BCLK slots.
118 pub data_format: esp_hal::i2s::master::DataFormat,
119 /// TDM framing standard. Must match the connected DAC's expectations.
120 pub tdm_standard: TDMStandart,
121}
122
123impl Default for I2sTxConfig {
124 fn default() -> Self {
125 Self {
126 sample_rate: esp_hal::time::Rate::from_hz(44100),
127 channels: esp_hal::i2s::master::Channels::STEREO,
128 data_format: esp_hal::i2s::master::DataFormat::Data16Channel16,
129 tdm_standard: TDMStandart::Phillips,
130 }
131 }
132}
133
134pub fn init_i2s_tx<'d>(
135 config: I2sTxConfig,
136 bclk: impl PeripheralOutput<'d>,
137 dout: impl PeripheralOutput<'d>,
138 lrck: impl PeripheralOutput<'d>,
139 i2s0: I2S0<'d>,
140 dma_channel: DMA_CH0<'d>,
141) -> Result<I2sTx<'d>, I2sInitError> {
142 let i2s = I2s::new(
143 i2s0,
144 dma_channel,
145 tdm_config!(config.tdm_standard, cfg => .with_sample_rate(config.sample_rate).with_channels(config.channels).with_data_format(config.data_format))
146 )
147 .map_err(|_| I2sInitError::PeripheralInitFailed)?;
148 let (_, _, _tx_buf, tx_desc) = dma_buffers!(0, DEFAULT_DMA_TX_BUF_SIZE);
149
150 #[cfg(feature = "async")]
151 let i2s = i2s.into_async();
152 let i2stx = i2s
153 .i2s_tx
154 .with_bclk(bclk)
155 .with_ws(lrck)
156 .with_dout(dout)
157 .build(tx_desc);
158 Ok(i2stx)
159}