esp-hal 1.2.0

Bare-metal HAL for Espressif devices
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
//! PDM (pulse-density modulation) configuration for the I2S master driver.
//!
//! Default configurations and clock calculations follow Espressif's I2S PDM driver
//! (<https://github.com/espressif/esp-idf/tree/master/components/esp_driver_i2s>).

mod clock;
#[cfg(not(i2s_version = "1"))]
mod hp_filter;
#[cfg_attr(i2s_version = "1", path = "regs_v1.rs")]
#[cfg_attr(i2s_version = "2", path = "regs_v2.rs")]
#[cfg_attr(i2s_version = "3", path = "regs_v2.rs")]
mod ll;

use super::master::{ConfigError, Instance};
use crate::{i2s::master::Info, time::Rate};

/// PDM configuration errors.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum PdmError {
    /// PDM is not supported on this I2S peripheral instance.
    UnsupportedInstance,
    /// PCM format requested but hardware PCM2PDM/PDM2PCM is unavailable.
    PcmFormatUnsupported,
    /// PDM clock configuration is invalid.
    InvalidClock,
    /// PDM slot mask selects no active channels.
    InvalidSlotMask,
    /// PDM config must enable at least one of TX or RX.
    DirectionMissing,
    /// Simultaneous PDM TX and RX is not supported.
    DuplexUnsupported,
    /// PDM data line index is not available on this chip.
    InvalidLine,
}

impl core::error::Error for PdmError {}

impl core::fmt::Display for PdmError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Self::UnsupportedInstance => {
                write!(f, "PDM mode is not supported on this I2S instance")
            }
            Self::PcmFormatUnsupported => write!(
                f,
                "PCM PDM format is not supported on this I2S instance; use raw PDM format"
            ),
            Self::InvalidClock => write!(f, "PDM clock configuration is out of supported range"),
            Self::InvalidSlotMask => {
                write!(f, "PDM slot mask must select at least one active slot")
            }
            Self::DirectionMissing => {
                write!(f, "PDM configuration must include TX and/or RX settings")
            }
            Self::DuplexUnsupported => {
                write!(f, "PDM full duplex (TX and RX together) is not supported")
            }
            Self::InvalidLine => write!(f, "PDM data line index is not available on this chip"),
        }
    }
}

/// A peripheral singleton that supports PDM mode.
pub trait PdmInstance: Instance {}

for_each_i2s! {
    (
        $instance:ident, $sys:ident, $mclk:ident,
        $bclk:ident, $ws:ident, $bclk_rx:ident, $ws_rx:ident,
        $dout:tt, $din:tt, true, $pdm_rx:literal, $pcm2pdm:literal, $pdm2pcm:literal
    ) => {
        impl PdmInstance for crate::peripherals::$instance<'_> {}
    };
    (
        $instance:ident, $sys:ident, $mclk:ident,
        $bclk:ident, $ws:ident, $bclk_rx:ident, $ws_rx:ident,
        $dout:tt, $din:tt, false, true, $pcm2pdm:literal, $pdm2pcm:literal
    ) => {
        impl PdmInstance for crate::peripherals::$instance<'_> {}
    };
}

/// PDM data format: PCM samples in software vs raw PDM bitstream.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum PdmDataFormat {
    /// Hardware filter converts between PCM and PDM when supported.
    #[default]
    Pcm,
    /// Raw PDM samples; no hardware PCM conversion.
    Raw,
}

/// Mono or stereo slot mode (fixed two hardware slots).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum PdmSlotMode {
    /// Single active slot (left by default).
    #[default]
    Mono,
    /// Both slots active (stereo).
    Stereo,
}

/// Active PDM slot selection.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct PdmSlotMask(u16);

impl PdmSlotMask {
    /// Left slot only.
    pub const LEFT: Self = Self(1 << 1);
    /// Right slot only.
    pub const RIGHT: Self = Self(1 << 0);
    /// Both slots active.
    pub const BOTH: Self = Self(0b11);

    #[cfg(all(i2s_supports_pdm_rx, not(esp32)))]
    /// Line 0, left slot.
    pub const LINE0_LEFT: Self = Self(1 << 1);
    #[cfg(all(i2s_supports_pdm_rx, not(esp32)))]
    /// Line 0, right slot.
    pub const LINE0_RIGHT: Self = Self(1 << 0);
    #[cfg(all(i2s_supports_pdm_rx, esp32s3))]
    /// Line 1, left slot.
    pub const LINE1_LEFT: Self = Self(1 << 3);
    #[cfg(all(i2s_supports_pdm_rx, esp32s3))]
    /// Line 1, right slot.
    pub const LINE1_RIGHT: Self = Self(1 << 2);
    #[cfg(all(i2s_supports_pdm_rx, esp32s3))]
    /// Line 2, left slot.
    pub const LINE2_LEFT: Self = Self(1 << 5);
    #[cfg(all(i2s_supports_pdm_rx, esp32s3))]
    /// Line 2, right slot.
    pub const LINE2_RIGHT: Self = Self(1 << 4);
    #[cfg(all(i2s_supports_pdm_rx, esp32s3))]
    /// Line 3, left slot.
    pub const LINE3_LEFT: Self = Self(1 << 7);
    #[cfg(all(i2s_supports_pdm_rx, esp32s3))]
    /// Line 3, right slot.
    pub const LINE3_RIGHT: Self = Self(1 << 6);

    /// Creates a new mask from raw slot bits.
    pub const fn from_bits(bits: u16) -> Self {
        Self(bits)
    }

    /// Raw slot mask bits.
    pub const fn bits(self) -> u16 {
        self.0
    }

    /// Default mask for the given mono/stereo mode.
    pub fn for_mode(mode: PdmSlotMode) -> Self {
        match mode {
            PdmSlotMode::Mono => Self::LEFT,
            PdmSlotMode::Stereo => Self::BOTH,
        }
    }
}

/// PDM RX downsample rate (PDM2PCM path).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum PdmDownsampleRate {
    /// 64x downsample (DSR 8s).
    #[default]
    Dsr8s,
    /// 128x downsample (DSR 16s).
    Dsr16s,
}

/// PDM TX sigma-delta filter scaling.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum PdmSigScaling {
    /// Divides input by 2.
    Div2,
    /// Multiply input by 1.
    #[default]
    Mul1,
    /// Multiply input by 2.
    Mul2,
    /// Multiply input by 4.
    Mul4,
}

impl PdmSigScaling {
    pub(crate) fn to_register(self) -> u8 {
        match self {
            Self::Div2 => 0,
            Self::Mul1 => 1,
            Self::Mul2 => 2,
            Self::Mul4 => 3,
        }
    }
}

/// PDM TX line routing (HW v2+).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum PdmTxLineMode {
    /// Single-line codec output (default).
    #[default]
    OneLineCodec,
    /// Single-line DAC output.
    OneLineDac,
    /// Two-line DAC output (stereo).
    TwoLineDac,
}

/// PDM TX clock configuration.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct PdmTxClockConfig {
    /// Target PCM sample rate.
    pub sample_rate: Rate,
    /// Upsampling factor numerator (`fp`).
    pub up_sample_fp: u32,
    /// Upsampling factor denominator (`fs`).
    pub up_sample_fs: u32,
    /// Bit clock divider.
    pub bclk_div: u32,
}

/// PDM TX slot / filter configuration.
#[derive(Debug, Clone, Copy, PartialEq, procmacros::BuilderLite)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[non_exhaustive]
pub struct PdmTxSlotConfig {
    /// Mono or stereo slot mode.
    pub slot_mode: PdmSlotMode,
    /// PCM or raw PDM data format.
    pub data_format: PdmDataFormat,
    /// Sigma-delta prescale value.
    pub sd_prescale: u8,
    /// Sigma-delta filter input scaling.
    pub sd_scale: PdmSigScaling,
    /// High-pass filter input scaling.
    pub hp_scale: PdmSigScaling,
    /// Low-pass filter input scaling.
    pub lp_scale: PdmSigScaling,
    /// Sinc filter input scaling.
    pub sinc_scale: PdmSigScaling,
    #[cfg(not(i2s_version = "1"))]
    /// Output line routing mode.
    pub line_mode: PdmTxLineMode,
    /// Enables the TX high-pass filter.
    pub hp_en: bool,
    /// High-pass filter cut-off frequency in Hz.
    pub hp_cut_off_freq_hz: f32,
    #[cfg(not(i2s_version = "1"))]
    /// Sigma-delta dither bit 0.
    pub sd_dither: u8,
    #[cfg(not(i2s_version = "1"))]
    /// Sigma-delta dither bit 1.
    pub sd_dither2: u8,
    #[cfg(i2s_version = "1")]
    /// Active TX slot mask (ESP32 only).
    pub slot_mask: PdmSlotMask,
}

impl PdmTxSlotConfig {
    fn codec_pcm_default(mode: PdmSlotMode) -> Self {
        Self {
            slot_mode: mode,
            data_format: PdmDataFormat::Pcm,
            sd_prescale: 0,
            sd_scale: PdmSigScaling::Mul1,
            hp_scale: PdmSigScaling::Div2,
            lp_scale: PdmSigScaling::Mul1,
            sinc_scale: PdmSigScaling::Mul1,
            #[cfg(not(i2s_version = "1"))]
            line_mode: PdmTxLineMode::OneLineCodec,
            hp_en: true,
            hp_cut_off_freq_hz: 35.5,
            #[cfg(not(i2s_version = "1"))]
            sd_dither: 0,
            #[cfg(not(i2s_version = "1"))]
            sd_dither2: 1,
            #[cfg(i2s_version = "1")]
            slot_mask: PdmSlotMask::BOTH,
        }
    }

    #[cfg(not(i2s_version = "1"))]
    fn dac_pcm_default(mode: PdmSlotMode) -> Self {
        let mut cfg = Self::codec_pcm_default(mode);
        cfg.hp_scale = PdmSigScaling::Mul1;
        cfg.lp_scale = PdmSigScaling::Mul1;
        cfg.sinc_scale = PdmSigScaling::Mul1;
        #[cfg(not(i2s_version = "1"))]
        {
            cfg.line_mode = if mode == PdmSlotMode::Mono {
                PdmTxLineMode::OneLineDac
            } else {
                PdmTxLineMode::TwoLineDac
            };
        }
        cfg
    }

    fn raw_default(mode: PdmSlotMode) -> Self {
        let mut cfg = Self::codec_pcm_default(mode);
        cfg.data_format = PdmDataFormat::Raw;
        cfg.hp_en = false;
        cfg
    }
}

/// PDM RX clock configuration.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct PdmRxClockConfig {
    /// Target PCM sample rate.
    pub sample_rate: Rate,

    /// PDM2PCM downsample rate.
    pub downsample_rate: PdmDownsampleRate,

    /// Bit clock divider.
    pub bclk_div: u32,
}

/// PDM RX slot configuration.
#[derive(Debug, Clone, Copy, PartialEq, procmacros::BuilderLite)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[non_exhaustive]
pub struct PdmRxSlotConfig {
    /// Mono or stereo slot mode.
    pub slot_mode: PdmSlotMode,

    /// Active RX slot mask.
    pub slot_mask: PdmSlotMask,

    /// PCM or raw PDM data format.
    pub data_format: PdmDataFormat,

    #[cfg(i2s_supports_pdm_rx_hp_filter)]
    /// Enables the RX high-pass filter.
    pub hp_en: bool,

    #[cfg(i2s_supports_pdm_rx_hp_filter)]
    /// High-pass filter cut-off frequency in Hz.
    pub hp_cut_off_freq_hz: f32,

    #[cfg(i2s_supports_pdm_rx_hp_filter)]
    /// RX amplification factor (1–15).
    pub amplify_num: u32,
}

impl PdmRxSlotConfig {
    fn pcm_default(mode: PdmSlotMode) -> Self {
        Self {
            slot_mode: mode,
            slot_mask: PdmSlotMask::for_mode(mode),
            data_format: PdmDataFormat::Pcm,
            #[cfg(i2s_supports_pdm_rx_hp_filter)]
            hp_en: true,
            #[cfg(i2s_supports_pdm_rx_hp_filter)]
            hp_cut_off_freq_hz: 35.5,
            #[cfg(i2s_supports_pdm_rx_hp_filter)]
            amplify_num: 1,
        }
    }

    fn raw_default(mode: PdmSlotMode) -> Self {
        let mut cfg = Self::pcm_default(mode);
        cfg.data_format = PdmDataFormat::Raw;
        #[cfg(i2s_supports_pdm_rx_hp_filter)]
        {
            cfg.hp_en = false;
        }
        cfg
    }
}

/// Full PDM TX unit configuration.
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct PdmTxConfig {
    /// TX clock settings.
    pub clock: PdmTxClockConfig,

    /// TX slot and filter settings.
    pub slot: PdmTxSlotConfig,
}

impl PdmTxConfig {
    /// Codec-line defaults (`I2S_PDM_TX_*_DEFAULT_CONFIG`).
    pub fn new_codec_default(sample_rate: Rate, mode: PdmSlotMode) -> Self {
        Self {
            clock: PdmTxClockConfig::codec_default(sample_rate),
            slot: default_tx_slot(mode),
        }
    }

    /// Raw PDM TX defaults (no hardware PCM conversion).
    pub fn new_raw_default(sample_rate: Rate, mode: PdmSlotMode) -> Self {
        Self {
            clock: PdmTxClockConfig::codec_default(sample_rate),
            slot: PdmTxSlotConfig::raw_default(mode),
        }
    }

    /// DAC-line defaults (`I2S_PDM_TX_*_DAC_DEFAULT_CONFIG`, HW v2+).
    #[cfg(not(i2s_version = "1"))]
    pub fn new_dac_default(sample_rate: Rate, mode: PdmSlotMode) -> Self {
        Self {
            clock: PdmTxClockConfig::dac_default(sample_rate),
            slot: PdmTxSlotConfig::dac_pcm_default(mode),
        }
    }

    /// Validates TX configuration against hardware capabilities.
    pub fn validate(&self, info: &Info) -> Result<(), PdmError> {
        if self.slot.data_format == PdmDataFormat::Pcm && !info.pcm2pdm {
            return Err(PdmError::PcmFormatUnsupported);
        }
        if self.clock.up_sample_fs > 480 {
            return Err(PdmError::InvalidClock);
        }
        Ok(())
    }
}

fn default_tx_slot(mode: PdmSlotMode) -> PdmTxSlotConfig {
    cfg_select! {
        i2s_supports_pcm2pdm => PdmTxSlotConfig::codec_pcm_default(mode),
        _ => PdmTxSlotConfig::raw_default(mode),
    }
}

/// Full PDM RX unit configuration.
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct PdmRxConfig {
    /// RX clock settings.
    pub clock: PdmRxClockConfig,
    /// RX slot settings.
    pub slot: PdmRxSlotConfig,
}

impl PdmRxConfig {
    /// Default RX config; uses PCM when PDM2PCM is available, otherwise raw.
    pub fn new_default(sample_rate: Rate, mode: PdmSlotMode) -> Self {
        Self {
            clock: PdmRxClockConfig::default(sample_rate),
            slot: default_rx_slot(mode),
        }
    }

    /// PCM RX defaults (requires hardware PDM2PCM support).
    pub fn new_pcm_default(sample_rate: Rate, mode: PdmSlotMode) -> Self {
        Self {
            clock: PdmRxClockConfig::default(sample_rate),
            slot: PdmRxSlotConfig::pcm_default(mode),
        }
    }

    /// Raw PDM RX defaults (no hardware PCM conversion).
    pub fn new_raw_default(sample_rate: Rate, mode: PdmSlotMode) -> Self {
        Self {
            clock: PdmRxClockConfig::default(sample_rate),
            slot: PdmRxSlotConfig::raw_default(mode),
        }
    }

    /// Validates RX configuration against hardware capabilities.
    pub fn validate(&self, info: &Info) -> Result<(), PdmError> {
        if self.slot.data_format == PdmDataFormat::Pcm && !info.pdm2pcm {
            return Err(PdmError::PcmFormatUnsupported);
        }
        if self.slot.slot_mask.bits() == 0 {
            return Err(PdmError::InvalidSlotMask);
        }
        Ok(())
    }
}

fn default_rx_slot(mode: PdmSlotMode) -> PdmRxSlotConfig {
    cfg_select! {
        i2s_supports_pdm2pcm => PdmRxSlotConfig::pcm_default(mode),
        _ => PdmRxSlotConfig::raw_default(mode),
    }
}

/// PDM mode configuration (simplex TX, RX, or both).
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct PdmConfig {
    /// Optional TX unit configuration.
    pub tx: Option<PdmTxConfig>,
    /// Optional RX unit configuration.
    pub rx: Option<PdmRxConfig>,
}

impl PdmConfig {
    /// PDM TX only (recommended simplex setup).
    #[cfg(i2s_supports_pdm_tx)]
    pub fn tx_only(tx: PdmTxConfig) -> Self {
        Self {
            tx: Some(tx),
            rx: None,
        }
    }

    /// PDM RX only (recommended simplex setup).
    #[cfg(i2s_supports_pdm_rx)]
    pub fn rx_only(rx: PdmRxConfig) -> Self {
        Self {
            tx: None,
            rx: Some(rx),
        }
    }

    /// Validates that exactly one direction is configured and that the settings
    /// are valid for the given I2S instance.
    pub fn validate(&self, info: &Info) -> Result<(), PdmError> {
        if self.tx.is_none() && self.rx.is_none() {
            return Err(PdmError::DirectionMissing);
        }
        if self.tx.is_some() && self.rx.is_some() {
            return Err(PdmError::DuplexUnsupported);
        }
        if self.tx.is_some() && !info.pdm_tx {
            return Err(PdmError::UnsupportedInstance);
        }
        if self.rx.is_some() && !info.pdm_rx {
            return Err(PdmError::UnsupportedInstance);
        }
        if let Some(tx) = &self.tx {
            tx.validate(info)?;
        }
        if let Some(rx) = &self.rx {
            rx.validate(info)?;
        }
        Ok(())
    }
}

pub(crate) fn configure_pdm(i2s: &Info, config: &PdmConfig) -> Result<(), ConfigError> {
    ll::configure_pdm(i2s, config).map_err(ConfigError::Pdm)
}