Skip to content

Commit

Permalink
Merge #637
Browse files Browse the repository at this point in the history
637: Dual i2s core r=burrbull a=YruamaLairba

Hi, i'm trying to implement the 'device' that is suitable for full duplex i2s communication. I'd like to have opinion about implementation, i'm not sure it follow the general style of the crate. I don't have example to follow, I'm in a unusual situation where the 'device' is composed by 2 peripherals.

Co-authored-by: Yruama_Lairba <yruama_lairba@hotmail.com>
Co-authored-by: Andrey Zgarbul <zgarbul.andrey@gmail.com>
  • Loading branch information
3 people committed May 25, 2023
2 parents 0e48d22 + f8f1b71 commit dcb7f06
Show file tree
Hide file tree
Showing 6 changed files with 594 additions and 11 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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"]
Expand Down
370 changes: 370 additions & 0 deletions examples/rtic-dual-i2s-audio-in-out.rs
Original file line number Diff line number Diff line change
@@ -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<DualI2s<SPI2>, 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.
}
Loading

0 comments on commit dcb7f06

Please sign in to comment.