Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dual i2s core #637

Merged
merged 2 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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