diff --git a/Cargo.toml b/Cargo.toml index 769f5b15..df5aee99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -197,3 +197,7 @@ required-features = ["rt"] [[example]] name = "adc_dma" required-features = ["rt"] + +[[example]] +name = "audio" +required-features = ["rt", "stm32l496"] # Register for PLLSAI1 incomplete in L4x3, gate so CI will pass diff --git a/examples/audio.rs b/examples/audio.rs new file mode 100644 index 00000000..1359de9b --- /dev/null +++ b/examples/audio.rs @@ -0,0 +1,102 @@ +#![no_std] +#![no_main] + +use panic_rtt_target as _; + +use cortex_m_rt::entry; +use rtt_target::rprintln; + +use stm32l4xx_hal::{ + delay::Delay, + gpio::{Alternate, Pin, PushPull, H8, L8}, + pac, + prelude::*, + rcc::{MsiFreq, PllConfig}, + sai::{I2SChanConfig, I2SDataSize, I2SDir, I2sUsers, Sai}, + traits::i2s::FullDuplex, +}; + +type SaiPins = ( + Pin, L8, 'E', 2_u8>, + Pin, L8, 'E', 5_u8>, + Pin, L8, 'E', 4_u8>, + Pin, L8, 'E', 6_u8>, + Option, H8, 'A', 13_u8>>, +); + +#[entry] +fn main() -> ! { + rtt_target::rtt_init_print!(); + rprintln!("Initializing... "); + + let cp = pac::CorePeripherals::take().unwrap(); + let dp = pac::Peripherals::take().unwrap(); + + let mut flash = dp.FLASH.constrain(); + let mut rcc = dp.RCC.constrain(); + let mut pwr = dp.PWR.constrain(&mut rcc.apb1r1); + + let clocks = rcc + .cfgr + .msi(MsiFreq::RANGE8M) + .sysclk(80.mhz()) + .hclk(80.mhz()) + .pclk1(80.mhz()) + .pclk2(80.mhz()) + .sai1clk_with_pll(4_016_000.hz(), PllConfig::new(1, 13, Some(25), None, None)) + .freeze(&mut flash.acr, &mut pwr); + + rprintln!( + "clocks: sysclk: {:?}, hclk: {:?}, pclk1: {:?}, pclk2: {:?}", + clocks.sysclk().0, + clocks.hclk().0, + clocks.pclk1().0, + clocks.pclk2().0 + ); + + let mut delay = Delay::new(cp.SYST, clocks); + + // GPIO + /////// + + let mut gpioe = dp.GPIOE.split(&mut rcc.ahb2); + + let mclk = + gpioe + .pe2 + .into_alternate_push_pull(&mut gpioe.moder, &mut gpioe.otyper, &mut gpioe.afrl); + let fs = + gpioe + .pe4 + .into_alternate_push_pull(&mut gpioe.moder, &mut gpioe.otyper, &mut gpioe.afrl); + let sck = + gpioe + .pe5 + .into_alternate_push_pull(&mut gpioe.moder, &mut gpioe.otyper, &mut gpioe.afrl); + let fd = + gpioe + .pe6 + .into_alternate_push_pull(&mut gpioe.moder, &mut gpioe.otyper, &mut gpioe.afrl); + + let sai_pins: SaiPins = (mclk, sck, fs, fd, None); + + rprintln!("Sai... "); + let mut sai = Sai::i2s_sai1_ch_a( + dp.SAI1, + sai_pins, + 16_000.hz(), + I2SDataSize::Bits24, + &mut rcc.apb2, + &clocks, + I2sUsers::new(I2SChanConfig::new(I2SDir::Tx)), + ); + + rprintln!("Sai enable... "); + sai.enable(); + + rprintln!("Looping... "); + loop { + sai.try_send(0xaa, 0xcc).unwrap(); + delay.delay_ms(5_u32); + } +} diff --git a/src/lib.rs b/src/lib.rs index 87ea2daf..b5ae7ff3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -172,6 +172,8 @@ pub mod rng; #[cfg(not(any(feature = "stm32l4r9", feature = "stm32l4s9",)))] pub mod rtc; #[cfg(not(any(feature = "stm32l4r9", feature = "stm32l4s9",)))] +pub mod sai; +#[cfg(not(any(feature = "stm32l4r9", feature = "stm32l4s9",)))] pub mod serial; #[cfg(not(any(feature = "stm32l4r9", feature = "stm32l4s9",)))] pub mod signature; diff --git a/src/rcc.rs b/src/rcc.rs index 03b88726..b31e9061 100644 --- a/src/rcc.rs +++ b/src/rcc.rs @@ -85,8 +85,12 @@ impl RccExt for RCC { pclk1: None, pclk2: None, sysclk: None, + sai1clk: None, + sai2clk: None, pll_source: None, pll_config: None, + pllsai1_config: None, + pllsai2_config: None, }, } } @@ -351,8 +355,12 @@ pub struct CFGR { pclk1: Option, pclk2: Option, sysclk: Option, + sai1clk: Option, + sai2clk: Option, pll_source: Option, pll_config: Option, + pllsai1_config: Option, + pllsai2_config: Option, } impl CFGR { @@ -441,6 +449,41 @@ impl CFGR { self } + #[cfg(not(any( + // pllsai1cfgr.pllsai1pdiv missing in PAC + feature = "stm32l433", + feature = "stm32l443", + feature = "stm32l475", + )))] + /// Sets the SAI1 frequency with some pll configuration + pub fn sai1clk_with_pll(mut self, freq: F, cfg: PllConfig) -> Self + where + F: Into, + { + self.pllsai1_config = Some(cfg); + self.sai1clk = Some(freq.into().0); + self + } + + #[cfg(any( + // feature = "stm32l475", // pllsai2cfgr.pllsai2pdiv missing in PAC + feature = "stm32l476", + feature = "stm32l486", + feature = "stm32l496", + feature = "stm32l4a6", + feature = "stm32l4r9", + feature = "stm32l4s9", + ))] + /// Sets the SAI2 frequency with some pll configuration + pub fn sai2clk_with_pll(mut self, freq: F, cfg: PllConfig) -> Self + where + F: Into, + { + self.pllsai2_config = Some(cfg); + self.sai2clk = Some(freq.into().0); + self + } + /// Sets the PLL source pub fn pll_source(mut self, source: PllSource) -> Self { self.pll_source = Some(source); @@ -498,6 +541,7 @@ impl CFGR { if let Some(lse_cfg) = &self.lse { // 1. Unlock the backup domain pwr.cr1.reg().modify(|_, w| w.dbp().set_bit()); + while pwr.cr1.reg().read().dbp().bit_is_clear() {} // 2. Setup the LSE rcc.bdcr.modify(|_, w| { @@ -634,7 +678,13 @@ impl CFGR { // Calculate PLL multiplier and create a best effort pll config, just multiply n let plln = (2 * sysclk) / clock_speed; - Some(PllConfig::new(1, plln as u8, PllDivider::Div2)) + Some(PllConfig::new( + 1, + plln as u8, + None, + None, + Some(PllDivider::Div2), + )) } else { None } @@ -642,6 +692,13 @@ impl CFGR { self.pll_config }; + // Check that all M values are equal + let p = [pllconf, self.pllsai1_config, self.pllsai2_config]; + let _ = p.iter().flatten().reduce(|first, pc| { + assert_eq!(pc.m, first.m); + pc + }); + let sysclk = match (self.sysclk, self.msi) { (Some(sysclk), _) => sysclk, (None, Some(msi)) => msi.to_hertz().0, @@ -725,20 +782,7 @@ impl CFGR { let sysclk_src_bits; let mut msi = self.msi; if let Some(pllconf) = pllconf { - // Sanity-checks per RM0394, 6.4.4 PLL configuration register (RCC_PLLCFGR) - let r = pllconf.r.to_division_factor(); - let clock_speed = clock_speed / (pllconf.m as u32 + 1); - let vco = clock_speed * pllconf.n as u32; - let output_clock = vco / r; - - assert!(r <= 8); // Allowed max output divider - assert!(pllconf.n >= 8); // Allowed min multiplier - assert!(pllconf.n <= 86); // Allowed max multiplier - assert!(clock_speed >= 4_000_000); // VCO input clock min - assert!(clock_speed <= 16_000_000); // VCO input clock max - assert!(vco >= 64_000_000); // VCO output min - assert!(vco <= 334_000_000); // VCO output max - assert!(output_clock <= 80_000_000); // Max output clock + pllconf.check(clock_speed).unwrap(); // use PLL as source sysclk_src_bits = 0b11; @@ -753,7 +797,7 @@ impl CFGR { .pllm() .bits(pllconf.m) .pllr() - .bits(pllconf.r.to_bits()) + .bits(pllconf.r.unwrap().to_bits()) .plln() .bits(pllconf.n) }); @@ -797,6 +841,63 @@ impl CFGR { while rcc.cfgr.read().sws().bits() != sysclk_src_bits {} + // SAI PLL + + #[cfg(not(any( + // pllsai1cfgr.pllsai1pdiv missing in PAC + feature = "stm32l433", + feature = "stm32l443", + feature = "stm32l475" + )))] + if let Some(pllconf) = self.pllsai1_config { + pllconf.check(clock_speed).unwrap(); + + rcc.cr.modify(|_, w| w.pllsai1on().clear_bit()); + while rcc.cr.read().pllsai1rdy().bit_is_set() {} + + rcc.pllsai1cfgr.modify(|_, w| unsafe { + w.pllsai1pdiv() + .bits(pllconf.p.unwrap()) + .pllsai1n() + .bits(pllconf.n) + }); + + rcc.cr.modify(|_, w| w.pllsai1on().set_bit()); + + while rcc.cr.read().pllsai1rdy().bit_is_clear() {} + + rcc.pllsai1cfgr.modify(|_, w| w.pllsai1pen().set_bit()); + } + + #[cfg(any( + // feature = "stm32l475", // pllsai2cfgr.pllsai2pdiv missing in PAC + feature = "stm32l476", + feature = "stm32l486", + feature = "stm32l496", + feature = "stm32l4a6", + feature = "stm32l4r9", + feature = "stm32l4s9", + ))] + if let Some(pllconf) = self.pllsai2_config { + pllconf.check(clock_speed).unwrap(); + + rcc.cr.modify(|_, w| w.pllsai2on().clear_bit()); + while rcc.cr.read().pllsai2rdy().bit_is_set() {} + + rcc.pllsai2cfgr.modify(|_, w| unsafe { + w.pllsai2pdiv() + .bits(pllconf.p.unwrap()) + .pllsai2n() + .bits(pllconf.n) + }); + + rcc.cr.modify(|_, w| w.pllsai2on().set_bit()); + + while rcc.cr.read().pllsai2rdy().bit_is_clear() {} + + rcc.pllsai2cfgr.modify(|_, w| w.pllsai2pen().set_bit()); + } + // // 3. Shutdown unused clocks that have auto-started // @@ -822,6 +923,8 @@ impl CFGR { ppre1, ppre2, sysclk: Hertz(sysclk), + sai1clk: self.sai1clk.map(|clk| Hertz(clk)), + sai2clk: self.sai2clk.map(|clk| Hertz(clk)), pll_source: pllconf.map(|_| pll_source), } } @@ -857,29 +960,116 @@ impl PllDivider { } } +#[derive(Clone, Copy, Debug)] +pub enum PllConfigError { + MinOutputDivider, + MaxOutputDivider, + MinMultiplier, + MaxMultiplier, + MinVcoInputClock, + MaxVcoInputClock, + MinVcoOutputClock, + MaxVcoOutputClock, + MaxOutputClock, +} + #[derive(Clone, Copy, Debug)] /// PLL Configuration pub struct PllConfig { // Main PLL division factor m: u8, - // Main PLL multiplication factor + // PLL multiplication factor n: u8, - // Main PLL division factor for PLLCLK (system clock) - r: PllDivider, + // PLL division factor for PLLR + p: Option, + // PLL division factor for PLLR + q: Option, + // PLL division factor for PLLR + r: Option, } impl PllConfig { /// Create a new PLL config from manual settings /// /// PLL output = ((SourceClk / input_divider) * multiplier) / output_divider - pub fn new(input_divider: u8, multiplier: u8, output_divider: PllDivider) -> Self { + pub fn new( + input_divider: u8, + multiplier: u8, + output_divider_p: Option, + output_divider_q: Option, + output_divider_r: Option, + ) -> Self { assert!(input_divider > 0); PllConfig { m: input_divider - 1, n: multiplier, - r: output_divider, + p: output_divider_p, + q: output_divider_q, + r: output_divider_r, + } + } + + fn check(&self, clock_speed: u32) -> Result<(), PllConfigError> { + // Sanity-checks per RM0394, 6.4.4 PLL configuration register (RCC_PLLCFGR) + let clock_speed = clock_speed / (self.m as u32 + 1); + let vco = clock_speed * self.n as u32; + + if !(self.n >= 8) { + return Err(PllConfigError::MinMultiplier); + } + if !(self.n <= 86) { + return Err(PllConfigError::MaxMultiplier); + } + if !(clock_speed >= 4_000_000) { + return Err(PllConfigError::MinVcoInputClock); + } + if !(clock_speed <= 16_000_000) { + return Err(PllConfigError::MaxVcoInputClock); + } + if !(vco >= 64_000_000) { + return Err(PllConfigError::MinVcoOutputClock); + } + if !(vco <= 334_000_000) { + return Err(PllConfigError::MaxVcoOutputClock); + } + + if let Some(p) = self.p { + let output_clock = vco / p as u32; + if !(p >= 2) { + return Err(PllConfigError::MinOutputDivider); + } + if !(p <= 31) { + return Err(PllConfigError::MaxOutputDivider); + } + if !(output_clock <= 80_000_000) { + return Err(PllConfigError::MaxOutputClock); + } + } + + if let Some(q) = self.q { + let q = q.to_division_factor(); + let output_clock = vco / q as u32; + if !(q <= 8) { + return Err(PllConfigError::MaxOutputDivider); + } + if !(output_clock <= 80_000_000) { + return Err(PllConfigError::MaxOutputClock); + } + } + + if let Some(r) = self.r { + let r = r.to_division_factor(); + let output_clock = vco / r; + if !(r <= 8) { + return Err(PllConfigError::MaxOutputDivider); + } + if !(output_clock <= 80_000_000) { + return Err(PllConfigError::MaxOutputClock); + } } + + Ok(()) } } @@ -919,6 +1109,8 @@ pub struct Clocks { ppre1: u8, ppre2: u8, sysclk: Hertz, + sai1clk: Option, + sai2clk: Option, pll_source: Option, } @@ -978,4 +1170,14 @@ impl Clocks { pub fn sysclk(&self) -> Hertz { self.sysclk } + + // Returns the SAI1 frequency + pub fn sai1clk(&self) -> Option { + self.sai1clk + } + + // Returns the SAI1 frequency + pub fn sai2clk(&self) -> Option { + self.sai2clk + } } diff --git a/src/sai/i2s.rs b/src/sai/i2s.rs new file mode 100644 index 00000000..79189a1c --- /dev/null +++ b/src/sai/i2s.rs @@ -0,0 +1,897 @@ +//! # Serial Audio Interface - Inter-IC Sound +//! +//! Inter-IC Sound. +//! +use core::convert::TryInto; + +use crate::sai::{GetClkSAI, Sai, SaiChannel, CLEAR_ALL_FLAGS_BITS, INTERFACE}; +use crate::stm32; +use crate::time::Hertz; + +use crate::stm32::SAI1; +#[cfg(any( + feature = "stm32l496", + feature = "stm32l476", + feature = "stm32l475", + // Missing in PAC + // feature = "stm32l471" +))] +use crate::stm32::SAI2; + +use crate::device::sai1::ch::sr; + +type CH = stm32::sai1::CH; + +use crate::gpio::{gpioa::*, gpiob::*, gpioc::*, gpiod::*, gpioe::*}; +#[cfg(any( + feature = "stm32l475", + feature = "stm32l476", + feature = "stm32l486", + feature = "stm32l496", + feature = "stm32l4a6", + feature = "stm32l4r9", + feature = "stm32l4s9", +))] +use crate::gpio::{gpiof::*, gpiog::*}; +use crate::gpio::{Alternate, PushPull}; +use crate::rcc::{Clocks, RccBus}; + +use crate::traits::i2s::FullDuplex; +// use embedded_hal::i2s::FullDuplex; + +const NUM_SLOTS: u8 = 16; + +#[derive(Clone, Copy, PartialEq)] +pub enum I2SMode { + Master = 0b00, + Slave = 0b10, +} + +#[derive(Copy, Clone, PartialEq)] +pub enum I2SDir { + Tx = 0b00, + Rx = 0b01, +} + +#[derive(Copy, Clone)] +pub enum I2SDataSize { + Bits8 = 0b001, + Bits10 = 0b010, + Bits16 = 0b100, + Bits20 = 0b101, + Bits24 = 0b110, + Bits32 = 0b111, +} + +#[derive(Copy, Clone)] +enum I2SSlotSize { + Bits16 = 0b01, + Bits32 = 0b10, +} + +#[derive(Copy, Clone, PartialEq)] +pub enum I2SProtocol { + MSB, + LSB, +} + +#[derive(Copy, Clone)] +pub enum I2SSync { + Master = 0b00, + Internal = 0b01, + External = 0b10, +} + +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum I2SError { + NoChannelAvailable, +} + +pub enum I2SClockStrobe { + Rising, + Falling, +} + +#[derive(Copy, Clone)] +pub enum I2SCompanding { + Disabled = 0b00, + // Reserved = 0b01, + ALaw = 0b10, + MuLaw = 0b11, +} + +#[derive(Copy, Clone)] +pub enum I2SComplement { + Ones = 0, + Twos = 1, +} + +impl From for bool { + fn from(value: I2SComplement) -> Self { + match value { + I2SComplement::Ones => false, + I2SComplement::Twos => true, + } + } +} + +pub trait I2SPinsChA {} +pub trait I2SPinsChB {} +pub trait I2SPinMclkA {} +pub trait I2SPinMclkB {} +pub trait I2SPinSckA {} +pub trait I2SPinSckB {} +pub trait I2SPinFsA {} +pub trait I2SPinFsB {} +pub trait I2SPinSdA {} +pub trait I2SPinSdB {} + +/// Trait for valid combination of SAIxA pins +impl I2SPinsChA for (MCLK, SCK, FS, SD1, Option) +where + MCLK: I2SPinMclkA, + SCK: I2SPinSckA, + FS: I2SPinFsA, + SD1: I2SPinSdA, + SD2: I2SPinSdB, +{ +} + +/// Trait for valid combination of SAIxB pins +impl I2SPinsChB for (MCLK, SCK, FS, SD1, Option) +where + MCLK: I2SPinMclkB, + SCK: I2SPinSckB, + FS: I2SPinFsB, + SD1: I2SPinSdB, + SD2: I2SPinSdA, +{ +} + +/// I2S Config builder +pub struct I2SChanConfig { + dir: I2SDir, + sync_type: I2SSync, + clock_strobe: I2SClockStrobe, + slots: u8, + first_bit_offset: u8, + frame_sync_before: bool, + frame_sync_active_high: bool, + master_clock_disabled: bool, + companding: I2SCompanding, + complement: I2SComplement, + protocol: I2SProtocol, + mono_mode: bool, + mute_repeat: bool, + mute_counter: u8, + tristate: bool, + frame_size: Option, +} + +impl I2SChanConfig { + /// Create a default configuration for the I2S channel + /// + /// Arguments: + /// * `dir` - The direction of data (Rx or Tx) + pub fn new(dir: I2SDir) -> I2SChanConfig { + I2SChanConfig { + dir, + sync_type: I2SSync::Master, + clock_strobe: I2SClockStrobe::Falling, + slots: 2, + first_bit_offset: 0, + frame_sync_before: false, + frame_sync_active_high: false, + master_clock_disabled: false, + companding: I2SCompanding::Disabled, + complement: I2SComplement::Ones, + protocol: I2SProtocol::MSB, + mono_mode: false, + mute_repeat: false, + mute_counter: 0, + tristate: false, + frame_size: None, + } + } + + /// Set synchronization type, defaults to Master + pub fn set_sync_type(mut self, sync_type: I2SSync) -> Self { + self.sync_type = sync_type; + self + } + + /// Set the clock strobing edge + pub fn set_clock_strobe(mut self, clock_strobe: I2SClockStrobe) -> Self { + self.clock_strobe = clock_strobe; + self + } + + /// Set the number of slots, this is an advanced configuration. + pub fn set_slots(mut self, slots: u8) -> Self { + assert!(slots <= 16); + self.slots = slots; + self + } + + /// Set the offset of the first data bit + pub fn set_first_bit_offset(mut self, first_bit_offset: u8) -> Self { + // 5 bits or less + assert!(first_bit_offset < 0b10_0000); + self.first_bit_offset = first_bit_offset; + self + } + + /// Sets when the frame sync is asserted + pub fn set_frame_sync_before(mut self, frame_sync_before: bool) -> Self { + self.frame_sync_before = frame_sync_before; + self + } + + /// Set frame sync to active high, defaults to active low + pub fn set_frame_sync_active_high(mut self, frame_sync_active_high: bool) -> Self { + self.frame_sync_active_high = frame_sync_active_high; + self + } + + /// Disable master clock generator + pub fn disable_master_clock(mut self) -> Self { + self.master_clock_disabled = true; + self + } + + /// Sets the protocol to MSB or LSB + pub fn set_protocol(mut self, protocol: I2SProtocol) -> Self { + self.protocol = protocol; + self + } + + /// Set the mono mode bit, default is to use stereo + pub fn set_mono_mode(mut self, mono_mode: bool) -> Self { + self.mono_mode = mono_mode; + self + } + + /// Sets the type of mute values + /// * false - transmit 0 + /// * true - repeat the last values + /// + /// Only meaningful in Tx mode when slots <= 2 and mute is enabled + pub fn set_mute_repeat(mut self, mute_repeat: bool) -> Self { + if self.dir == I2SDir::Rx || self.slots > 2 { + panic!("This only has meaning in I2S::Tx mode when slots <= 2"); + } + self.mute_repeat = mute_repeat; + self + } + + /// Set the mute counter value + /// Must be less than or equal to 64 + /// + /// The value set is compared to the number of consecutive mute frames detected in + /// reception. When the number of mute frames is equal to this value, the Muted even will be generated + pub fn set_mute_counter(mut self, mute_counter: u8) -> Self { + if self.dir == I2SDir::Tx { + panic!("This only has meaning in I2S::Rx mode"); + } + // 6 bits or less + assert!(mute_counter < 0b100_0000); + self.mute_counter = mute_counter; + self + } + + /// Set the tristate to release the SD output line (HI-Z) at the end of the last data bit + pub fn set_tristate(mut self, tristate: bool) -> Self { + self.tristate = tristate; + self + } + + /// Set the frame size. If None it will be automatically calculated + pub fn set_frame_size(mut self, frame_size: Option) -> Self { + if let Some(frame_size) = frame_size { + assert!(frame_size >= 8); + } + self.frame_size = frame_size; + self + } +} + +/// I2S Interface +pub struct I2S { + master: I2SChanConfig, + slave: Option, +} + +impl INTERFACE for I2S {} + +pub type I2sUsers = I2S; + +impl I2sUsers { + pub fn new(master: I2SChanConfig) -> Self { + Self { + master, + slave: None, + } + } + + pub fn add_slave(mut self, slave: I2SChanConfig) -> Self { + self.slave.replace(slave); + self + } +} + +/// Trait to extend SAI peripherals +pub trait SaiI2sExt: Sized { + fn i2s_ch_a( + self, + _pins: PINS, + audio_freq: T, + data_size: I2SDataSize, + apb2: &mut ::Bus, + clocks: &Clocks, + users: I2sUsers, + ) -> Sai + where + PINS: I2SPinsChA, + T: Into; + fn i2s_ch_b( + self, + _pins: PINS, + audio_freq: T, + data_size: I2SDataSize, + apb2: &mut ::Bus, + clocks: &Clocks, + users: I2sUsers, + ) -> Sai + where + PINS: I2SPinsChB, + T: Into; +} + +macro_rules! i2s { + ( $($SAIX:ident, $Rec:ident: [$i2s_saiX_ch_a:ident, $i2s_saiX_ch_b:ident]),+ ) => { + $( + impl SaiI2sExt<$SAIX> for $SAIX { + fn i2s_ch_a( + self, + _pins: PINS, + audio_freq: T, + data_size: I2SDataSize, + apb2: &mut <$SAIX as RccBus>::Bus, + clocks: &Clocks, + users: I2sUsers, + ) -> Sai + where + PINS: I2SPinsChA, + T: Into, + { + Sai::$i2s_saiX_ch_a( + self, + _pins, + audio_freq.into(), + data_size, + apb2, + clocks, + users, + ) + } + fn i2s_ch_b( + self, + _pins: PINS, + audio_freq: T, + data_size: I2SDataSize, + apb2: &mut <$SAIX as RccBus>::Bus, + clocks: &Clocks, + users: I2sUsers, + ) -> Sai + where + PINS: I2SPinsChB, + T: Into, + { + Sai::$i2s_saiX_ch_b( + self, + _pins, + audio_freq.into(), + data_size, + apb2, + clocks, + users, + ) + } + } + + impl Sai<$SAIX, I2S> { + pub fn $i2s_saiX_ch_a( + sai: $SAIX, + _pins: PINS, + audio_freq: Hertz, + data_size: I2SDataSize, + apb2: &mut <$SAIX as RccBus>::Bus, + clocks: &Clocks, + users: I2sUsers, + ) -> Self + where + PINS: I2SPinsChA<$SAIX>, + { + assert!(users.master.slots <= NUM_SLOTS); + if let Some(slave) = &users.slave { + assert!(slave.slots <= NUM_SLOTS); + } + + // Clock config + let ker_ck_a = $SAIX::sai_a_ker_ck(clocks); + let clock_ratio = 256; + let mclk_div = + (ker_ck_a.0) / (audio_freq.0 * clock_ratio); + let mclk_div: u8 = mclk_div + .try_into() + .expect(concat!(stringify!($SAIX), + " A: Kernel clock is out of range for required MCLK" + )); + + // Configure SAI peripheral + let mut per_sai = Sai { + rb: sai, + master_channel: SaiChannel::ChannelA, + slave_channel: if users.slave.is_some() { + Some(SaiChannel::ChannelB) + } else { + None + }, + interface: users, + }; + + per_sai.sai_rcc_init(apb2); + + i2s_config_channel( + &per_sai.rb.cha, + I2SMode::Master, + &per_sai.interface.master, + mclk_div, + data_size, + ); + + if let Some(slave) = &per_sai.interface.slave { + i2s_config_channel( + &per_sai.rb.chb, + I2SMode::Slave, + slave, + 0, + data_size, + ); + } + + per_sai + } + + pub fn $i2s_saiX_ch_b( + sai: $SAIX, + _pins: PINS, + audio_freq: Hertz, + data_size: I2SDataSize, + apb2: &mut <$SAIX as RccBus>::Bus, + clocks: &Clocks, + users: I2sUsers, + ) -> Self + where + PINS: I2SPinsChB<$SAIX>, + { + assert!(users.master.slots <= NUM_SLOTS); + if let Some(slave) = &users.slave { + assert!(slave.slots <= NUM_SLOTS); + } + + // Clock config + let ker_ck_a = $SAIX::sai_b_ker_ck(clocks); + let clock_ratio = 256; + let mclk_div = + (ker_ck_a.0) / (audio_freq.0 * clock_ratio); + let mclk_div: u8 = mclk_div + .try_into() + .expect(concat!(stringify!($SAIX), + " B: Kernel clock is out of range for required MCLK" + )); + + + // Configure SAI peripheral + let mut per_sai = Sai { + rb: sai, + master_channel: SaiChannel::ChannelB, + slave_channel: if users.slave.is_some() { + Some(SaiChannel::ChannelA) + } else { + None + }, + interface: users, + }; + + per_sai.sai_rcc_init(apb2); + + i2s_config_channel( + &per_sai.rb.chb, + I2SMode::Master, + &per_sai.interface.master, + mclk_div, + data_size, + ); + + if let Some(slave) = &per_sai.interface.slave { + i2s_config_channel( + &per_sai.rb.cha, + I2SMode::Slave, + slave, + 0, + data_size, + ); + } + + per_sai + } + + pub fn enable(&mut self) { + // Enable slave first "recommended" per ref doc + self.slave_channel(enable_ch); + self.master_channel(enable_ch); + } + + pub fn disable(&mut self) { + // Master must be disabled first + self.master_channel(disable_ch); + self.slave_channel(disable_ch); + } + } + + impl FullDuplex for Sai<$SAIX, I2S> { + type Error = I2SError; + + fn try_read(&mut self) -> nb::Result<(u32, u32), Self::Error> { + if self.interface.master.dir == I2SDir::Rx { + return self.master_channel(read); + } else if let Some(slave) = &self.interface.slave { + if slave.dir == I2SDir::Rx { + return self.slave_channel(read).unwrap(); + } + } + Err(nb::Error::Other(I2SError::NoChannelAvailable)) + } + + fn try_send( + &mut self, + left_word: u32, + right_word: u32, + ) -> nb::Result<(), Self::Error> { + if self.interface.master.dir == I2SDir::Tx { + return self.master_channel(|audio_ch| { + send(left_word, right_word, audio_ch) + }); + } else if let Some(slave) = &self.interface.slave { + if slave.dir == I2SDir::Tx { + return self + .slave_channel(|audio_ch| { + send(left_word, right_word, audio_ch) + }) + .unwrap(); + } + } + Err(nb::Error::Other(I2SError::NoChannelAvailable)) + } + } + )+ + } +} + +i2s! { + SAI1, Sai1: [i2s_sai1_ch_a, i2s_sai1_ch_b] +} +#[cfg(any( + feature = "stm32l496", + feature = "stm32l476", + feature = "stm32l475", + // Missing in PAC + // feature = "stm32l471" +))] +i2s! { + SAI2, Sai2: [i2s_sai2_ch_a, i2s_sai2_ch_b] +} + +fn i2s_config_channel( + audio_ch: &CH, + mode: I2SMode, + config: &I2SChanConfig, + mclk_div: u8, + data_size: I2SDataSize, +) { + let clock_strobe = match config.clock_strobe { + I2SClockStrobe::Rising => false, + I2SClockStrobe::Falling => true, + }; + + // 16 bits in register correspond to 1 slot each max 16 slots + let slot_en_bits: u16 = (2_u32.pow(config.slots.into()) - 1) as u16; + + // Slots to have to be big enough to hold a words worth of data + let slot_size = match data_size { + I2SDataSize::Bits8 => I2SSlotSize::Bits16, + I2SDataSize::Bits10 => I2SSlotSize::Bits16, + I2SDataSize::Bits16 => I2SSlotSize::Bits16, + I2SDataSize::Bits20 => I2SSlotSize::Bits32, + I2SDataSize::Bits24 => I2SSlotSize::Bits32, + I2SDataSize::Bits32 => I2SSlotSize::Bits32, + }; + + let frame_size = match config.frame_size { + Some(frame_size) => frame_size, + None => match data_size { + I2SDataSize::Bits8 => 16 * (config.slots / 2), + I2SDataSize::Bits10 => 32 * (config.slots / 2), + I2SDataSize::Bits16 => 32 * (config.slots / 2), + I2SDataSize::Bits20 => 64 * (config.slots / 2), + I2SDataSize::Bits24 => 64 * (config.slots / 2), + I2SDataSize::Bits32 => 64 * (config.slots / 2), + }, + }; + + let mode_bits = (mode as u8) | (config.dir as u8); + unsafe { + audio_ch.cr1.modify(|_, w| { + w.mode() + .bits(mode_bits as u8) + .prtcfg() + .free() + .ds() + .bits(data_size as u8) + .lsbfirst() + .bit(config.protocol == I2SProtocol::LSB) + .ckstr() + .bit(clock_strobe) + .syncen() + .bits(config.sync_type as u8) + .mono() + .bit(config.mono_mode) + .nodiv() + .bit(config.master_clock_disabled) + .mckdiv() + .bits(mclk_div) + }); + audio_ch.cr2.modify(|_, w| { + w.fth() + .quarter1() + .tris() + .bit(config.tristate) + .mute() + .clear_bit() + .muteval() + .bit(config.mute_repeat) + .mutecn() + .bits(config.mute_counter) + .cpl() + .bit(config.complement.into()) + .comp() + .bits(config.companding as u8) + }); + audio_ch.frcr.modify(|_, w| { + w.frl() + .bits(frame_size - 1) + .fsall() + .bits((frame_size / 2) - 1) + .fsdef() + .set_bit() // left/right channels enabled + .fspol() + .bit(config.frame_sync_active_high) + .fsoff() + .bit(config.frame_sync_before) + }); + audio_ch.slotr.modify(|_, w| { + w.fboff() + .bits(config.first_bit_offset) + .slotsz() + .bits(slot_size as u8) + .nbslot() + .bits(config.slots - 1) + .sloten() + .bits(slot_en_bits) + }); + } +} + +fn enable_ch(audio_ch: &CH) { + unsafe { audio_ch.clrfr.write(|w| w.bits(CLEAR_ALL_FLAGS_BITS)) }; + audio_ch.cr2.modify(|_, w| w.fflush().flush()); + audio_ch.cr1.modify(|_, w| w.saien().enabled()); +} + +fn disable_ch(audio_ch: &CH) { + audio_ch.cr1.modify(|_, w| w.saien().disabled()); + while audio_ch.cr1.read().saien().bit_is_set() {} +} + +fn read(audio_ch: &CH) -> nb::Result<(u32, u32), I2SError> { + match audio_ch.sr.read().flvl().variant() { + Some(sr::FLVL_A::EMPTY) => Err(nb::Error::WouldBlock), + _ => Ok((audio_ch.dr.read().bits(), audio_ch.dr.read().bits())), + } +} + +fn send(left_word: u32, right_word: u32, audio_ch: &CH) -> nb::Result<(), I2SError> { + // The FIFO is 8 words long. A write consists of 2 words, in stereo mode. + // Therefore you need to wait for 3/4s to ensure 2 words are available for writing. + match audio_ch.sr.read().flvl().variant() { + Some(sr::FLVL_A::FULL) => Err(nb::Error::WouldBlock), + Some(sr::FLVL_A::QUARTER4) => Err(nb::Error::WouldBlock), + _ => { + unsafe { + audio_ch.dr.write(|w| w.bits(left_word)); + audio_ch.dr.write(|w| w.bits(right_word)); + } + Ok(()) + } + } +} + +// Pin definitions +macro_rules! pins { + ($($SAIX:ty: + MCLK_A: [$($MCLK_A:ty),*] + SCK_A: [$($SCK_A:ty),*] + FS_A: [$($FS_A:ty),*] + SD_A: [$($SD_A:ty),*] + MCLK_B: [$($MCLK_B:ty),*] + SCK_B: [$($SCK_B:ty),*] + FS_B: [$($FS_B:ty),*] + SD_B: [$($SD_B:ty),*] + + )+) => { + $( + $( + impl I2SPinMclkA<$SAIX> for $MCLK_A {} + )* + $( + impl I2SPinSckA<$SAIX> for $SCK_A {} + )* + $( + impl I2SPinFsA<$SAIX> for $FS_A {} + )* + $( + impl I2SPinSdA<$SAIX> for $SD_A {} + )* + $( + impl I2SPinMclkB<$SAIX> for $MCLK_B {} + )* + $( + impl I2SPinSckB<$SAIX> for $SCK_B {} + )* + $( + impl I2SPinFsB<$SAIX> for $FS_B {} + )* + $( + impl I2SPinSdB<$SAIX> for $SD_B {} + )* + )+ + } +} + +pins! { + SAI1: + MCLK_A: [ + PA3>, + PB8>, + PE2> + ] + SCK_A: [ + PA8>, + PB10>, + PE5> + ] + FS_A: [ + PA9>, + PB9>, + PE4> + ] + SD_A: [ + PA10>, + PC1>, + PC3>, + PD6>, + PE6> + ] + MCLK_B: [ + PB4>, + PE10> + ] + SCK_B: [ + PB3>, + PE8> + ] + FS_B: [ + PA4>, + PA14>, + PB6>, + PE9> + ] + SD_B: [ + PA13>, + PB5>, + PE3>, + PE7> + ] +} + +#[cfg(any( + feature = "stm32l475", + feature = "stm32l476", + feature = "stm32l486", + feature = "stm32l496", + feature = "stm32l4a6", + feature = "stm32l4r9", + feature = "stm32l4s9", +))] +pins! { + SAI1: + MCLK_A: [ + PG7> + ] + SCK_A: [] + FS_A: [] + SD_A: [] + MCLK_B: [ + PF7> + ] + SCK_B: [ + PF8> + ] + FS_B: [ + PF9> + ] + SD_B: [ + PF6> + ] +} + +#[cfg(any( + feature = "stm32l496", + feature = "stm32l476", + feature = "stm32l475", + // Missing in PAC + // feature = "stm32l471" +))] +pins! { + SAI2: + MCLK_A: [ + PB14>, + PC6>, + PD9>, + PG11> + ] + SCK_A: [ + PB13>, + PD10>, + PG9> + ] + FS_A: [ + PB12>, + PD12>, + PG10> + ] + SD_A: [ + PB15>, + PD11>, + PG12> + ] + MCLK_B: [ + PC7>, + PC11>, + PG4> + ] + SCK_B: [ + PC10>, + PG2> + ] + FS_B: [ + PA15>, + PG3> + ] + SD_B: [ + PC12>, + PG5> + ] +} diff --git a/src/sai/mod.rs b/src/sai/mod.rs new file mode 100644 index 00000000..4f2768d7 --- /dev/null +++ b/src/sai/mod.rs @@ -0,0 +1,312 @@ +//! Serial Audio Interface +//! +//! Based on the implementation in the stm32h7xx-hal +//! https://github.com/stm32-rs/stm32h7xx-hal/tree/master/src/sai + +use crate::stm32::sai1::CH; + +use crate::stm32::SAI1; +#[cfg(any( + feature = "stm32l496", + feature = "stm32l476", + feature = "stm32l475", + // Missing in PAC + // feature = "stm32l471" +))] +use crate::stm32::SAI2; + +// clocks +use crate::time::Hertz; + +const CLEAR_ALL_FLAGS_BITS: u32 = 0b0111_0111; + +mod i2s; +use crate::rcc::{Clocks, Enable, RccBus, Reset}; +pub use i2s::{ + I2SChanConfig, I2SClockStrobe, I2SCompanding, I2SComplement, I2SDataSize, I2SDir, I2SMode, + I2SProtocol, I2SSync, I2sUsers, SaiI2sExt, I2S, +}; + +/// Trait for associating clocks with SAI instances +pub trait GetClkSAI { + /// Return the kernel clock for the SAI - A + /// + /// # Panics + /// + /// Panics if the kernel clock is not running + fn sai_a_ker_ck(clocks: &Clocks) -> Hertz; + /// Return the kernel clock for the SAI - B + /// + /// # Panics + /// + /// Panics if the kernel clock is not running + fn sai_b_ker_ck(clocks: &Clocks) -> Hertz; +} + +impl GetClkSAI for SAI1 { + fn sai_a_ker_ck(clocks: &Clocks) -> Hertz { + clocks.sai1clk().unwrap() + } + + fn sai_b_ker_ck(clocks: &Clocks) -> Hertz { + clocks.sai1clk().unwrap() + } +} + +#[cfg(any( + feature = "stm32l496", + feature = "stm32l476", + feature = "stm32l475", + // Missing in PAC + // feature = "stm32l471" +))] +impl GetClkSAI for SAI2 { + fn sai_a_ker_ck(clocks: &Clocks) -> Hertz { + clocks.sai2clk().unwrap() + } + + fn sai_b_ker_ck(clocks: &Clocks) -> Hertz { + clocks.sai2clk().unwrap() + } +} + +pub trait INTERFACE {} + +/// SAI Events +/// +/// Each event is a possible interrupt source, if enabled +#[derive(Copy, Clone, PartialEq)] +pub enum Event { + /// Overdue/Underrun error detection + Overdue, + /// Mute detected (Rx only) + Muted, + /// Clock not setup per frame sync rules see RM0433 Section 51.4.6: Frame synchronization + WrongClock, + /// Data is available / is required in the FIFO + Data, + /// Frame synchronization signal is detected earlier than expected + AnticipatedFrameSync, + /// Frame synchronization signal is not present at the right time + LateFrameSync, +} + +/// SAI Channels +#[derive(Copy, Clone, PartialEq)] +pub enum SaiChannel { + ChannelA, + ChannelB, +} + +/// Hardware serial audio interface peripheral +pub struct Sai { + rb: SAI, + master_channel: SaiChannel, + slave_channel: Option, + interface: INTERFACE, +} + +macro_rules! sai_hal { + ($($SAIX:ident: ($saiX:ident, $Rec:ident),)+) => { + $( + // Common to all interfaces + impl Sai<$SAIX, INTERFACE> { + /// Low level RCC initialisation + fn sai_rcc_init(&mut self, apb2: &mut <$SAIX as RccBus>::Bus) + { + // enable or reset $SAIX + <$SAIX>::enable(apb2); + <$SAIX>::reset(apb2); + } + + /// Access to the current master channel + fn master_channel(&self, func: F) -> T + where F: FnOnce(&CH) -> T, + { + match self.master_channel { + SaiChannel::ChannelA => func(&self.rb.cha), + SaiChannel::ChannelB => func(&self.rb.chb), + } + } + + /// Access to the current slave channel, if set + fn slave_channel(&self, func: F) -> Option + where F: FnOnce(&CH) -> T, + { + match self.slave_channel { + Some(SaiChannel::ChannelA) => Some(func(&self.rb.cha)), + Some(SaiChannel::ChannelB) => Some(func(&self.rb.chb)), + None => None + } + } + + /// Start listening for `event` on a given `channel` + pub fn listen(&mut self, channel: SaiChannel, event: Event) { + let ch = match channel { + SaiChannel::ChannelA => &self.rb.cha, + SaiChannel::ChannelB => &self.rb.chb, + }; + match event { + Event::Overdue => ch.im.modify(|_, w| w.ovrudrie().set_bit()), + Event::Muted => ch.im.modify(|_, w| w.mutedetie().set_bit()), + Event::WrongClock => ch.im.modify(|_, w| w.wckcfgie().set_bit()), + Event::Data => ch.im.modify(|_, w| w.freqie().set_bit()), + Event::AnticipatedFrameSync => ch.im.modify(|_, w| w.afsdetie().set_bit()), + Event::LateFrameSync => ch.im.modify(|_, w| w.lfsdetie().set_bit()), + } + } + + /// Stop listening for `event` on a given `channel` + pub fn unlisten(&mut self, channel: SaiChannel, event: Event) { + let ch = match channel { + SaiChannel::ChannelA => &self.rb.cha, + SaiChannel::ChannelB => &self.rb.chb, + }; + match event { + Event::Overdue => ch.im.modify(|_, w| w.ovrudrie().clear_bit()), + Event::Muted => ch.im.modify(|_, w| w.mutedetie().clear_bit()), + Event::WrongClock => ch.im.modify(|_, w| w.wckcfgie().clear_bit()), + Event::Data => ch.im.modify(|_, w| w.freqie().clear_bit()), + Event::AnticipatedFrameSync => ch.im.modify(|_, w| w.afsdetie().clear_bit()), + Event::LateFrameSync => ch.im.modify(|_, w| w.lfsdetie().clear_bit()), + } + let _ = ch.im.read(); + let _ = ch.im.read(); // Delay 2 peripheral clocks + } + + /// Clears interrupt flag `event` on the `channel` + /// + /// Note: Event::Data is accepted but does nothing as that flag is cleared by reading/writing data + pub fn clear_irq(&mut self, channel: SaiChannel, event: Event) { + let ch = match channel { + SaiChannel::ChannelA => &self.rb.cha, + SaiChannel::ChannelB => &self.rb.chb, + }; + match event { + Event::Overdue => ch.clrfr.write(|w| w.covrudr().set_bit()), + Event::Muted => ch.clrfr.write(|w| w.cmutedet().set_bit()), + Event::WrongClock => ch.clrfr.write(|w| w.cwckcfg().set_bit()), + Event::Data => (), // Cleared by reading/writing data + Event::AnticipatedFrameSync => ch.clrfr.write(|w| w.cafsdet().set_bit()), + Event::LateFrameSync => ch.clrfr.write(|w| w.clfsdet().set_bit()), + } + let _ = ch.sr.read(); + let _ = ch.sr.read(); // Delay 2 peripheral clocks + } + + /// Clears all interrupts on the `channel` + pub fn clear_all_irq(&mut self, channel: SaiChannel) { + let ch = match channel { + SaiChannel::ChannelA => &self.rb.cha, + SaiChannel::ChannelB => &self.rb.chb, + }; + unsafe { + ch.clrfr.write(|w| w.bits(CLEAR_ALL_FLAGS_BITS)); + } + let _ = ch.sr.read(); + let _ = ch.sr.read(); // Delay 2 peripheral clocks + } + + /// Mute `channel`, this is checked at the start of each frame + /// Meaningful only in Tx mode + pub fn mute(&mut self, channel: SaiChannel) { + match channel { + SaiChannel::ChannelA => &self.rb.cha.cr2.modify(|_, w| w.mute().enabled()), + SaiChannel::ChannelB => &self.rb.cha.cr2.modify(|_, w| w.mute().enabled()), + }; + } + + /// Unmute `channel`, this is checked at the start of each frame + /// Meaningful only in Tx mode + pub fn unmute(&mut self, channel: SaiChannel) { + match channel { + SaiChannel::ChannelA => &self.rb.cha.cr2.modify(|_, w| w.mute().disabled()), + SaiChannel::ChannelB => &self.rb.chb.cr2.modify(|_, w| w.mute().disabled()), + }; + } + + /* + GCR missing in PAC + + /// Used to operate the audio block(s) with an external SAI for synchronization + /// Refer to RM0433 rev 7 section 51.4.4 for valid values + /// + /// In short 0-3 maps SAI1-4 with the ones pointing to self being reserved. + /// e.g. for SAI1 1-3 are valid and 0 is invalid + pub fn set_sync_input(&mut self, selection: u8) { + assert!(selection < 0b1_00); + unsafe { &self.rb.gcr.modify(|_, w| w.syncout().bits(selection)) }; + } + + /// Synchronization output for other SAI blocks + pub fn set_sync_output(&mut self, channel: Option) { + match channel { + Some(SaiChannel::ChannelA) => unsafe { &self.rb.gcr.modify(|_, w| w.syncout().bits(0b01) ) }, + Some(SaiChannel::ChannelB) => unsafe { &self.rb.gcr.modify(|_, w| w.syncout().bits(0b10) ) }, + None => unsafe { &self.rb.gcr.modify(|_, w| w.syncout().bits(0b00) ) }, + }; + } + */ + + /// Enable DMA for the SAI peripheral. + pub fn enable_dma(&mut self, channel: SaiChannel) { + match channel { + SaiChannel::ChannelA => self.rb.cha.cr1.modify(|_, w| w.dmaen().enabled()), + SaiChannel::ChannelB => self.rb.chb.cr1.modify(|_, w| w.dmaen().enabled()), + }; + } + + /// Disable DMA for the SAI peripheral. + pub fn disable_dma(&mut self, channel: SaiChannel) { + match channel { + SaiChannel::ChannelA => self.rb.cha.cr1.modify(|_, w| w.dmaen().disabled()), + SaiChannel::ChannelB => self.rb.chb.cr1.modify(|_, w| w.dmaen().disabled()), + }; + } + + /// Releases the SAI peripheral + pub fn free(self) -> $SAIX { + // Refer to RM0433 Rev 7 51.4.15 Disabling the SAI + + // Master: Clear SAIEN + self.master_channel(|ch| { + ch.cr1.modify(|_, w| w.saien().disabled()) + }); + + // Master: Wait for SAI to clear at the end of the + // frame + while self.master_channel(|ch| { + ch.cr1.read().saien().bit_is_set() + }) {} + + // Slave: Clear SAIEN + self.slave_channel(|ch| { + ch.cr1.modify(|_, w| w.saien().disabled()) + }); + + // Slave: Wait for SAI to clear + while self.slave_channel(|ch| { + ch.cr1.read().saien().bit_is_set() + }).unwrap_or(false) {} + + + self.rb + } + } + )+ + } +} + +sai_hal! { + SAI1: (sai1, Sai1), +} +#[cfg(any( + feature = "stm32l496", + feature = "stm32l476", + feature = "stm32l475", + // Missing in PAC + // feature = "stm32l471" +))] +sai_hal! { + SAI2: (sai2, Sai2), +} diff --git a/src/traits/i2s.rs b/src/traits/i2s.rs new file mode 100644 index 00000000..f552e15e --- /dev/null +++ b/src/traits/i2s.rs @@ -0,0 +1,15 @@ +//! I2S - Inter-IC Sound Interface + +/// Full duplex +pub trait FullDuplex { + /// Error type + type Error; + + /// Reads the left word and right word available. + /// + /// The order is in the result is `(left_word, right_word)` + fn try_read(&mut self) -> nb::Result<(Word, Word), Self::Error>; + + /// Sends a left word and a right word to the slave. + fn try_send(&mut self, left_word: Word, right_word: Word) -> nb::Result<(), Self::Error>; +} diff --git a/src/traits/mod.rs b/src/traits/mod.rs index 2e50f82b..00cd4ad4 100644 --- a/src/traits/mod.rs +++ b/src/traits/mod.rs @@ -1 +1,2 @@ pub mod flash; +pub mod i2s;