diff --git a/CHANGELOG.md b/CHANGELOG.md index c76e8545..a9a35aaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] + - Integrate new version of stm32_i2s (v0.5) to enable full-duplex operation + - Add a rtic example to show how to do full-duplex i2s ### Changed diff --git a/Cargo.toml b/Cargo.toml index 9686cfd2..41d596d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,7 +57,7 @@ version = "=1.0.0-alpha.8" package = "embedded-hal" [dependencies.stm32_i2s_v12x] -version = "0.4.0" +version = "0.5.0" optional = true [dev-dependencies] @@ -414,6 +414,10 @@ required-features = ["stm32f411", "rtic"] # stm32f411 name = "rtic-i2s-audio-in-out" required-features = ["stm32f411", "i2s", "rtic"] +[[example]] +name = "rtic-dual-i2s-audio-in-out" +required-features = ["stm32f411", "i2s", "rtic"] + [[example]] name = "rtic-serial-dma-rx-idle" required-features = ["stm32f411", "rtic"] diff --git a/examples/rtic-dual-i2s-audio-in-out.rs b/examples/rtic-dual-i2s-audio-in-out.rs new file mode 100644 index 00000000..909d2d8e --- /dev/null +++ b/examples/rtic-dual-i2s-audio-in-out.rs @@ -0,0 +1,370 @@ +//! # Full duplex I2s example with rtic +//! +//! This application show how to use DualI2sDriver with interruption to achieve a full duplex +//! communication. Be careful to you ear, wrong operation can trigger loud noise on the DAC output. +//! +//! # Hardware required +//! +//! * a STM32F411 based board +//! * I2S ADC and DAC, eg PCM1808 and PCM5102 from TI +//! * Audio signal at ADC input, and something to ear at DAC output. +//! +//! # Hardware Wiring +//! +//! The wiring assume using PCM1808 and PCM5102 module that can be found on Aliexpress, ebay, +//! Amazon... +//! +//! ## Stm32 +//! +//! | stm32 | PCM1808 | PCM5102 | +//! |-------|---------|---------| +//! | pb12 | LRC | LCK | +//! | pb13 | BCK | BCK | +//! | pc6 | SCK | SCK | +//! | pb14 | | DIN | +//! | pb15 | OUT | | +//! +//! ## PCM1808 ADC module +//! +//! | Pin | Connected To | +//! |-----|----------------| +//! | LIN | audio in left | +//! | - | audio in gnd | +//! | RIN | audio in right | +//! | FMT | Gnd or NC | +//! | MD1 | Gnd or NC | +//! | MD0 | Gnd or NC | +//! | Gnd | Gnd | +//! | 3.3 | +3V3 | +//! | +5V | +5v | +//! | BCK | pb13 | +//! | OUT | pb15 | +//! | LRC | pb12 | +//! | SCK | pc6 | +//! +//! ## PCM5102 module +//! +//! | Pin | Connected to | +//! |-------|-----------------| +//! | SCK | pc6 | +//! | BCK | pb13 | +//! | DIN | pb14 | +//! | LCK | pb12 | +//! | GND | Gnd | +//! | VIN | +3V3 | +//! | FLT | Gnd or +3V3 | +//! | DEMP | Gnd | +//! | XSMT | +3V3 | +//! | A3V3 | | +//! | AGND | audio out gnd | +//! | ROUT | audio out left | +//! | LROUT | audio out right | +//! +//! Notes: on the module (not the chip) A3V3 is connected to VIN and AGND is connected to GND +//! +//! +//! Expected behavior: you should ear a crappy stereo effect. This is actually 2 square tremolo +//! applied with a 90 degrees phase shift. + +#![no_std] +#![no_main] + +use core::panic::PanicInfo; +use rtt_target::rprintln; + +use stm32f4xx_hal as hal; + +#[rtic::app(device = stm32f4xx_hal::pac, peripherals = true,dispatchers = [EXTI0, EXTI1, EXTI2])] +mod app { + use core::fmt::Write; + + use super::hal; + + use hal::gpio::Edge; + use hal::i2s::stm32_i2s_v12x::driver::*; + use hal::i2s::DualI2s; + use hal::pac::Interrupt; + use hal::pac::{EXTI, SPI2}; + use hal::prelude::*; + + use heapless::spsc::*; + + use rtt_target::{rprintln, rtt_init, set_print_channel}; + + type DualI2s2Driver = DualI2sDriver, Master, Receive, Transmit, Philips>; + + // Part of the frame we currently transmit or receive + #[derive(Copy, Clone)] + pub enum FrameState { + LeftMsb, + LeftLsb, + RightMsb, + RightLsb, + } + + use FrameState::{LeftLsb, LeftMsb, RightLsb, RightMsb}; + + impl Default for FrameState { + fn default() -> Self { + Self::LeftMsb + } + } + #[shared] + struct Shared { + #[lock_free] + i2s2_driver: DualI2s2Driver, + #[lock_free] + exti: EXTI, + } + + #[local] + struct Local { + logs_chan: rtt_target::UpChannel, + adc_p: Producer<'static, (i32, i32), 2>, + process_c: Consumer<'static, (i32, i32), 2>, + process_p: Producer<'static, (i32, i32), 2>, + dac_c: Consumer<'static, (i32, i32), 2>, + } + + #[init(local = [queue_1: Queue<(i32,i32), 2> = Queue::new(),queue_2: Queue<(i32,i32), 2> = Queue::new()])] + fn init(cx: init::Context) -> (Shared, Local, init::Monotonics) { + let queue_1 = cx.local.queue_1; + let queue_2 = cx.local.queue_2; + let channels = rtt_init! { + up: { + 0: { + size: 128 + name: "Logs" + } + 1: { + size: 128 + name: "Panics" + } + } + }; + let logs_chan = channels.up.0; + let panics_chan = channels.up.1; + set_print_channel(panics_chan); + let (adc_p, process_c) = queue_1.split(); + let (process_p, dac_c) = queue_2.split(); + let device = cx.device; + let mut syscfg = device.SYSCFG.constrain(); + let mut exti = device.EXTI; + let gpiob = device.GPIOB.split(); + let gpioc = device.GPIOC.split(); + let rcc = device.RCC.constrain(); + let clocks = rcc + .cfgr + .use_hse(8u32.MHz()) + .sysclk(96.MHz()) + .hclk(96.MHz()) + .pclk1(50.MHz()) + .pclk2(100.MHz()) + .i2s_clk(61440.kHz()) + .freeze(); + + // I2S pins: (WS, CK, MCLK, SD) for I2S2 + let i2s2_pins = ( + gpiob.pb12, //WS + gpiob.pb13, //CK + gpioc.pc6, //MCK + gpiob.pb15, //SD + gpiob.pb14, //ExtSD + ); + let i2s2 = DualI2s::new(device.SPI2, device.I2S2EXT, i2s2_pins, &clocks); + let i2s2_config = DualI2sDriverConfig::new_master() + .direction(Receive, Transmit) + .standard(Philips) + .data_format(DataFormat::Data24Channel32) + .master_clock(true) + .request_frequency(48_000); + let mut i2s2_driver = DualI2sDriver::new(i2s2, i2s2_config); + rprintln!("actual sample rate is {}", i2s2_driver.sample_rate()); + i2s2_driver.main().set_rx_interrupt(true); + i2s2_driver.main().set_error_interrupt(true); + i2s2_driver.ext().set_tx_interrupt(true); + i2s2_driver.ext().set_error_interrupt(true); + + // set up an interrupt on WS pin + let ws_pin = i2s2_driver.ws_pin_mut(); + ws_pin.make_interrupt_source(&mut syscfg); + ws_pin.trigger_on_edge(&mut exti, Edge::Rising); + // we will enable the ext part in interrupt + ws_pin.enable_interrupt(&mut exti); + + i2s2_driver.main().enable(); + + ( + Shared { i2s2_driver, exti }, + Local { + logs_chan, + adc_p, + process_c, + process_p, + dac_c, + }, + init::Monotonics(), + ) + } + + #[idle(shared = [], local = [])] + fn idle(_cx: idle::Context) -> ! { + #[allow(clippy::empty_loop)] + loop {} + } + + // Printing message directly in a i2s interrupt can cause timing issues. + #[task(capacity = 10, local = [logs_chan])] + fn log(cx: log::Context, message: &'static str) { + writeln!(cx.local.logs_chan, "{}", message).unwrap(); + } + + // processing audio + #[task(binds = SPI5, local = [count: u32 = 0,process_c,process_p])] + fn process(cx: process::Context) { + let count = cx.local.count; + let process_c = cx.local.process_c; + let process_p = cx.local.process_p; + while let Some(mut smpl) = process_c.dequeue() { + let period = 24000; + if *count > period / 2 { + smpl.0 >>= 1; + } + if *count > period / 4 && *count <= period * 3 / 4 { + smpl.1 >>= 1; + } + *count += 1; + if *count >= period { + *count = 0; + } + process_p.enqueue(smpl).ok(); + } + } + + #[task( + priority = 4, + binds = SPI2, + local = [ + main_frame_state: FrameState = LeftMsb, + main_frame: (u32,u32) = (0,0), + ext_frame_state: FrameState = LeftMsb, + ext_frame: (u32,u32) = (0,0), + adc_p, + dac_c + ], + shared = [i2s2_driver, exti] + )] + fn i2s2(cx: i2s2::Context) { + let i2s2_driver = cx.shared.i2s2_driver; + + // handling "main" part + let main_frame_state = cx.local.main_frame_state; + let main_frame = cx.local.main_frame; + let adc_p = cx.local.adc_p; + let status = i2s2_driver.main().status(); + // It's better to read first to avoid triggering ovr flag + if status.rxne() { + let data = i2s2_driver.main().read_data_register(); + match (*main_frame_state, status.chside()) { + (LeftMsb, Channel::Left) => { + main_frame.0 = (data as u32) << 16; + *main_frame_state = LeftLsb; + } + (LeftLsb, Channel::Left) => { + main_frame.0 |= data as u32; + *main_frame_state = RightMsb; + } + (RightMsb, Channel::Right) => { + main_frame.1 = (data as u32) << 16; + *main_frame_state = RightLsb; + } + (RightLsb, Channel::Right) => { + main_frame.1 |= data as u32; + // defer sample processing to another task + let (l, r) = *main_frame; + adc_p.enqueue((l as i32, r as i32)).ok(); + rtic::pend(Interrupt::SPI5); + *main_frame_state = LeftMsb; + } + // in case of ovr this resynchronize at start of new main_frame + _ => *main_frame_state = LeftMsb, + } + } + if status.ovr() { + log::spawn("i2s2 Overrun").ok(); + // sequence to delete ovr flag + i2s2_driver.main().read_data_register(); + i2s2_driver.main().status(); + } + + // handling "ext" part + let ext_frame_state = cx.local.ext_frame_state; + let ext_frame = cx.local.ext_frame; + let dac_c = cx.local.dac_c; + let exti = cx.shared.exti; + let status = i2s2_driver.ext().status(); + // it's better to write data first to avoid to trigger udr flag + if status.txe() { + let data; + match (*ext_frame_state, status.chside()) { + (LeftMsb, Channel::Left) => { + let (l, r) = dac_c.dequeue().unwrap_or_default(); + *ext_frame = (l as u32, r as u32); + data = (ext_frame.0 >> 16) as u16; + *ext_frame_state = LeftLsb; + } + (LeftLsb, Channel::Left) => { + data = (ext_frame.0 & 0xFFFF) as u16; + *ext_frame_state = RightMsb; + } + (RightMsb, Channel::Right) => { + data = (ext_frame.1 >> 16) as u16; + *ext_frame_state = RightLsb; + } + (RightLsb, Channel::Right) => { + data = (ext_frame.1 & 0xFFFF) as u16; + *ext_frame_state = LeftMsb; + } + // in case of udr this resynchronize tracked and actual channel + _ => { + *ext_frame_state = LeftMsb; + data = 0; //garbage data to avoid additional underrun + } + } + i2s2_driver.ext().write_data_register(data); + } + if status.fre() { + log::spawn("i2s2 Frame error").ok(); + i2s2_driver.ext().disable(); + i2s2_driver.ws_pin_mut().enable_interrupt(exti); + } + if status.udr() { + log::spawn("i2s2 udr").ok(); + i2s2_driver.ext().status(); + i2s2_driver.ext().write_data_register(0); + } + } + + // Look WS line for the "ext" part (re) synchronisation + #[task(priority = 4, binds = EXTI15_10, shared = [i2s2_driver,exti])] + fn exti15_10(cx: exti15_10::Context) { + let i2s2_driver = cx.shared.i2s2_driver; + let exti = cx.shared.exti; + let ws_pin = i2s2_driver.ws_pin_mut(); + // check if that pin triggered the interrupt + if ws_pin.check_interrupt() { + // Here we know ws pin is high because the interrupt was triggerd by it's rising edge + ws_pin.clear_interrupt_pending_bit(); + ws_pin.disable_interrupt(exti); + i2s2_driver.ext().write_data_register(0); + i2s2_driver.ext().enable(); + } + } +} + +#[inline(never)] +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + rprintln!("{}", info); + loop {} // You might need a compiler fence in here. +} diff --git a/examples/rtic-i2s-audio-in-out.rs b/examples/rtic-i2s-audio-in-out.rs index 77146f39..1aad9f14 100644 --- a/examples/rtic-i2s-audio-in-out.rs +++ b/examples/rtic-i2s-audio-in-out.rs @@ -80,14 +80,12 @@ mod app { use super::hal; - use hal::gpio::Edge; - use hal::gpio::NoPin; + use hal::gpio::{Edge, NoPin}; use hal::i2s::stm32_i2s_v12x::driver::*; use hal::i2s::I2s; use hal::pac::Interrupt; use hal::pac::{EXTI, SPI2, SPI3}; use hal::prelude::*; - use stm32f4xx_hal::gpio::ReadPin; use heapless::spsc::*; @@ -365,10 +363,10 @@ mod app { let i2s3_driver = cx.shared.i2s3_driver; let exti = cx.shared.exti; let ws_pin = i2s3_driver.ws_pin_mut(); - ws_pin.clear_interrupt_pending_bit(); - // yes, in this case we already know that pin is high, but some other exti can be triggered - // by several pins - if ws_pin.is_high() { + // check if that pin triggered the interrupt. + if ws_pin.check_interrupt() { + // Here we know ws pin is high because the interrupt was triggerd by it's rising edge + ws_pin.clear_interrupt_pending_bit(); ws_pin.disable_interrupt(exti); i2s3_driver.write_data_register(0); i2s3_driver.enable(); diff --git a/src/gpio/alt.rs b/src/gpio/alt.rs index cbf9c7e3..00d4c95f 100644 --- a/src/gpio/alt.rs +++ b/src/gpio/alt.rs @@ -348,7 +348,7 @@ pub trait I2cCommon { // I2S pins pub trait I2sCommon { - type Ck; + type Ck: crate::gpio::PinSpeed; type Sd; type Ws: crate::gpio::ReadPin + crate::gpio::ExtiPin; } diff --git a/src/i2s.rs b/src/i2s.rs index 7d123d24..aa9f6274 100644 --- a/src/i2s.rs +++ b/src/i2s.rs @@ -1,8 +1,12 @@ //! I2S (inter-IC Sound) communication using SPI peripherals //! //! This module is only available if the `i2s` feature is enabled. +//! +//! Note: while F413 and F423 have full duplex i2s capability, this mode is not yet availalble for +//! these chips because their `I2S2EXT` and `I2S3EXT` peripherals are missing from their package +//! access crate. -use crate::gpio::{self, NoPin}; +use crate::gpio::{self, NoPin, PinSpeed, Speed}; use crate::pac; #[allow(unused)] use crate::rcc::{self, Clocks, Reset}; @@ -26,6 +30,12 @@ pub trait Instance: { } +/// Trait for SPI peripheral that have an extension for full duplex i2s capability. +pub trait DualInstance: Instance + gpio::alt::I2sExtPin { + /// The I2SEXT peripheral that extend the SPI peripheral + type I2sExtPeripheral; +} + /// Trait to get I2s frequency at SPI peripheral input. pub trait I2sFreq { fn i2s_freq(clocks: &Clocks) -> Hertz; @@ -60,6 +70,39 @@ impl I2sExt for SPI { } } +/// Trait to build an [`DualI2s`] object from SPI peripheral, a I2SEXT peripheral, pins and clocks +pub trait DualI2sExt: Sized + DualInstance { + fn dual_i2s( + self, + i2s_ext: Self::I2sExtPeripheral, + pins: ( + impl Into, + impl Into, + impl Into, + impl Into, + impl Into, + ), + clocks: &Clocks, + ) -> DualI2s; +} + +impl DualI2sExt for SPI { + fn dual_i2s( + self, + i2s_ext: Self::I2sExtPeripheral, + pins: ( + impl Into, + impl Into, + impl Into, + impl Into, + impl Into, + ), + clocks: &Clocks, + ) -> DualI2s { + DualI2s::new(self, i2s_ext, pins, clocks) + } +} + /// An I2s wrapper around an SPI object and pins pub struct I2s { spi: I, @@ -99,7 +142,13 @@ impl I2s { SPI::reset_unchecked(); } - let pins = (pins.0.into(), pins.1.into(), pins.2.into(), pins.3.into()); + let pins = ( + pins.0.into(), + // Workaround for corrupted last bit of data issue, see stm32f411 errata + pins.1.into().speed(Speed::VeryHigh), + pins.2.into(), + pins.3.into(), + ); I2s { spi, @@ -227,6 +276,166 @@ i2s!(pac::SPI5, I2s5, i2s5, i2s_clk); #[cfg(any(feature = "gpio-f412", feature = "gpio-f413"))] i2s!(pac::SPI5, I2s5, i2s5, i2s_apb2_clk); +/// A wrapper around a SPI and a I2SEXT object and pins for full duplex I2S operation +#[allow(clippy::type_complexity)] +pub struct DualI2s { + spi: I, + i2s_ext: I::I2sExtPeripheral, + pins: (I::Ws, I::Ck, I::Mck, I::Sd, I::ExtSd), + /// Frequency of clock input to this peripheral from the I2S PLL or related source + input_clock: Hertz, +} + +impl DualI2s { + /// Creates an DualI2s object around a SPI peripheral, it's I2SEXT extension, and pins + /// + /// This function enables and resets the SPI and I2SEXT peripheral, but does not configure it. + /// + /// The returned DualI2s object implements `stm32_i2s_v12x::DualI2sPeripheral`, so it can be used to + /// configure the peripheral and communicate. + /// + /// # Panics + /// + /// This function panics if the I2S clock input (from the I2S PLL or similar) + /// is not configured. + pub fn new( + spi: SPI, + i2s_ext: SPI::I2sExtPeripheral, + pins: ( + impl Into, + impl Into, + impl Into, + impl Into, + impl Into, + ), + clocks: &Clocks, + ) -> Self { + let input_clock = SPI::i2s_freq(clocks); + unsafe { + // Enable clock, enable reset, clear, reset + // Note: this also affect the I2SEXT peripheral + SPI::enable_unchecked(); + SPI::reset_unchecked(); + } + + let pins = ( + pins.0.into(), + // Workaround for corrupted last bit of data issue, see stm32f411 errata + pins.1.into().speed(Speed::VeryHigh), + pins.2.into(), + pins.3.into(), + pins.4.into(), + ); + + Self { + spi, + i2s_ext, + pins, + input_clock, + } + } + + #[allow(clippy::type_complexity)] + pub fn release( + self, + ) -> ( + SPI, + SPI::I2sExtPeripheral, + (SPI::Ws, SPI::Ck, SPI::Mck, SPI::Sd, SPI::ExtSd), + ) { + (self.spi, self.i2s_ext, self.pins) + } +} + +impl DualI2s { + pub fn ws_pin(&self) -> &SPI::Ws { + &self.pins.0 + } + pub fn ws_pin_mut(&mut self) -> &mut SPI::Ws { + &mut self.pins.0 + } +} + +impl DualI2s { + /// Returns the frequency of the clock signal that the SPI peripheral is receiving from the + /// I2S PLL or similar source + pub fn input_clock(&self) -> Hertz { + self.input_clock + } +} + +/// Implements stm32_i2s_v12x::DualI2sPeripheral for DualI2s<$SPI> +/// +/// $SPI: The fully-capitalized name of the SPI peripheral from pac module (example: SPI1) +/// $I2SEXT: The fully-capitalized name of the I2SEXT peripheral from pac module (example: I2S3EXT) +/// $DualI2s: The CamelCase I2S alias name for hal I2s wrapper (example: DualI2s1). +/// $i2s: module containing the Ws pin definition. (example: i2s1). +/// $clock: The name of the Clocks function that returns the frequency of the I2S clock input +/// to this SPI peripheral (i2s_cl, i2s_apb1_clk, or i2s2_apb_clk) +macro_rules! dual_i2s { + ($SPI:ty,$I2SEXT:ty, $DualI2s:ident, $i2s:ident, $clock:ident) => { + pub type $DualI2s = DualI2s<$SPI>; + + impl DualInstance for $SPI { + type I2sExtPeripheral = $I2SEXT; + } + + #[cfg(feature = "stm32_i2s_v12x")] + unsafe impl stm32_i2s_v12x::DualI2sPeripheral for DualI2s<$SPI> + where + $SPI: rcc::Reset, + { + type WsPin = gpio::alt::$i2s::Ws; + const MAIN_REGISTERS: *const () = <$SPI>::ptr() as *const _; + const EXT_REGISTERS: *const () = <$I2SEXT>::ptr() as *const _; + fn i2s_freq(&self) -> u32 { + self.input_clock.raw() + } + fn ws_pin(&self) -> &Self::WsPin { + self.ws_pin() + } + fn ws_pin_mut(&mut self) -> &mut Self::WsPin { + self.ws_pin_mut() + } + fn rcc_reset(&mut self) { + unsafe { + <$SPI>::reset_unchecked(); + } + } + } + }; +} + +// Actually define objects for dual i2s +// Each one has to be split into two declarations because the F412, F413, F423, and F446 +// have two different I2S clocks while other models have only one. +// All STM32F4 models except STM32F410 and STM32F446 have dual i2s support on SPI2 and SPI3 +#[cfg(any( + feature = "gpio-f401", + feature = "gpio-f411", + feature = "gpio-f417", + feature = "gpio-f427", + feature = "gpio-f469", +))] +dual_i2s!(pac::SPI2, pac::I2S2EXT, DualI2s2, i2s2, i2s_clk); + +// add "gpio-f413" feature here when missing I2SEXT in pac wil be fixed. +#[cfg(any(feature = "gpio-f412",))] +dual_i2s!(pac::SPI2, pac::I2S2EXT, DualI2s2, i2s2, i2s_apb1_clk); + +#[cfg(any( + feature = "gpio-f401", + feature = "gpio-f411", + feature = "gpio-f417", + feature = "gpio-f427", + feature = "gpio-f469", +))] +dual_i2s!(pac::SPI3, pac::I2S3EXT, DualI2s3, i2s3, i2s_clk); + +// add "gpio-f413" feature here when missing I2SEXT in pac wil be fixed. +#[cfg(any(feature = "gpio-f412",))] +dual_i2s!(pac::SPI3, pac::I2S3EXT, DualI2s3, i2s3, i2s_apb1_clk); + // DMA support: reuse existing mappings for SPI #[cfg(feature = "stm32_i2s_v12x")] mod dma {