From cecd77938c694ff2bad2a259ff64f2f468dcb04a Mon Sep 17 00:00:00 2001 From: Christian Perez Llamas <932644+chris-zen@users.noreply.github.com> Date: Wed, 9 Nov 2022 19:14:43 +0100 Subject: [PATCH] Draft: Initial support for I2S with a working example. Co-authored-by: @brainstorm --- embassy-nrf/Cargo.toml | 1 + embassy-nrf/src/chips/nrf52840.rs | 5 + embassy-nrf/src/i2s.rs | 403 ++++++++++++++++++++++++++++++ embassy-nrf/src/lib.rs | 2 + examples/nrf/Cargo.toml | 2 +- examples/nrf/src/bin/i2s.rs | 48 ++++ 6 files changed, 460 insertions(+), 1 deletion(-) create mode 100644 embassy-nrf/src/i2s.rs create mode 100644 examples/nrf/src/bin/i2s.rs diff --git a/embassy-nrf/Cargo.toml b/embassy-nrf/Cargo.toml index 67b6bec4..aa1576fd 100644 --- a/embassy-nrf/Cargo.toml +++ b/embassy-nrf/Cargo.toml @@ -48,6 +48,7 @@ nrf9160-s = ["_nrf9160"] nrf9160-ns = ["_nrf9160"] gpiote = [] +i2s = [] time-driver-rtc1 = ["_time-driver"] # Features starting with `_` are for internal use only. They're not intended diff --git a/embassy-nrf/src/chips/nrf52840.rs b/embassy-nrf/src/chips/nrf52840.rs index 4beadfba..cf800c7b 100644 --- a/embassy-nrf/src/chips/nrf52840.rs +++ b/embassy-nrf/src/chips/nrf52840.rs @@ -164,6 +164,9 @@ embassy_hal_common::peripherals! { // PDM PDM, + + // I2S + I2S, } #[cfg(feature = "nightly")] @@ -285,6 +288,8 @@ impl_saadc_input!(P0_29, ANALOG_INPUT5); impl_saadc_input!(P0_30, ANALOG_INPUT6); impl_saadc_input!(P0_31, ANALOG_INPUT7); +impl_i2s!(I2S, I2S, I2S); + pub mod irqs { use embassy_cortex_m::interrupt::_export::declare; diff --git a/embassy-nrf/src/i2s.rs b/embassy-nrf/src/i2s.rs new file mode 100644 index 00000000..0199ac61 --- /dev/null +++ b/embassy-nrf/src/i2s.rs @@ -0,0 +1,403 @@ +#![macro_use] + +//! I2S + +use core::future::poll_fn; +use core::sync::atomic::{compiler_fence, Ordering}; +use core::task::Poll; + +use embassy_hal_common::drop::OnDrop; +use embassy_hal_common::{into_ref, PeripheralRef}; +use pac::i2s::config::mcken; + +use crate::{pac, Peripheral}; +use crate::interrupt::{Interrupt, InterruptExt}; +use crate::gpio::{self, AnyPin, Pin as GpioPin, PselBits}; +use crate::gpio::sealed::Pin as _; + +// TODO: Define those in lib.rs somewhere else +// +// I2S EasyDMA MAXCNT bit length = 14 +const MAX_DMA_MAXCNT: u32 = 1 << 14; + +// Limits for Easy DMA - it can only read from data ram +pub const SRAM_LOWER: usize = 0x2000_0000; +pub const SRAM_UPPER: usize = 0x3000_0000; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum Error { + BufferTooLong, + BufferZeroLength, + DMABufferNotInDataMemory, + BufferMisaligned, + // TODO: add other error variants. +} + +#[derive(Clone)] +#[non_exhaustive] +pub struct Config { + pub ratio: Ratio, + pub sample_width: SampleWidth, + pub align: Align, + pub format: Format, + pub channels: Channels, +} + +impl Default for Config { + fn default() -> Self { + Self { + ratio: Ratio::_32x, + sample_width: SampleWidth::_16bit, + align: Align::Left, + format: Format::I2S, + channels: Channels::Stereo, + } + } +} + +/// MCK / LRCK ratio. +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +pub enum Ratio { + _32x, + _48x, + _64x, + _96x, + _128x, + _192x, + _256x, + _384x, + _512x, +} + +impl From for u8 { + fn from(variant: Ratio) -> Self { + variant as _ + } +} + +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +pub enum SampleWidth { + _8bit, + _16bit, + _24bit, +} + +impl From for u8 { + fn from(variant: SampleWidth) -> Self { + variant as _ + } +} + +/// Alignment of sample within a frame. +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +pub enum Align { + Left, + Right, +} + +impl From for bool { + fn from(variant: Align) -> Self { + match variant { + Align::Left => false, + Align::Right => true, + } + } +} + +/// Frame format. +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +pub enum Format { + I2S, + Aligned, +} + +impl From for bool { + fn from(variant: Format) -> Self { + match variant { + Format::I2S => false, + Format::Aligned => true, + } + } +} + +/// Enable channels. +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +pub enum Channels { + Stereo, + Left, + Right, +} + +impl From for u8 { + fn from(variant: Channels) -> Self { + variant as _ + } +} + +/// I2S Mode +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +pub enum Mode { + Controller, + Peripheral, +} + +// /// Master clock generator frequency. +// #[derive(Debug, Eq, PartialEq, Clone, Copy)] +// pub enum MckFreq { +// _32MDiv8 = 0x20000000, +// _32MDiv10 = 0x18000000, +// _32MDiv11 = 0x16000000, +// _32MDiv15 = 0x11000000, +// _32MDiv16 = 0x10000000, +// _32MDiv21 = 0x0C000000, +// _32MDiv23 = 0x0B000000, +// _32MDiv30 = 0x08800000, +// _32MDiv31 = 0x08400000, +// _32MDiv32 = 0x08000000, +// _32MDiv42 = 0x06000000, +// _32MDiv63 = 0x04100000, +// _32MDiv125 = 0x020C0000, +// } + + +/// Interface to the UARTE peripheral using EasyDMA to offload the transmission and reception workload. +/// +/// For more details about EasyDMA, consult the module documentation. +pub struct I2s<'d, T: Instance> { + output: I2sOutput<'d, T>, + input: I2sInput<'d, T>, +} + +/// Transmitter interface to the UARTE peripheral obtained +/// via [Uarte]::split. +pub struct I2sOutput<'d, T: Instance> { + _p: PeripheralRef<'d, T>, +} + +/// Receiver interface to the UARTE peripheral obtained +/// via [Uarte]::split. +pub struct I2sInput<'d, T: Instance> { + _p: PeripheralRef<'d, T>, +} + +impl<'d, T: Instance> I2s<'d, T> { + /// Create a new I2S + pub fn new( + i2s: impl Peripheral

+ 'd, + // irq: impl Peripheral

+ 'd, + mck: impl Peripheral

+ 'd, + sck: impl Peripheral

+ 'd, + lrck: impl Peripheral

+ 'd, + sdin: impl Peripheral

+ 'd, + sdout: impl Peripheral

+ 'd, + config: Config, + ) -> Self { + into_ref!(mck, sck, lrck, sdin, sdout); + Self::new_inner( + i2s, + // irq, + mck.map_into(), sck.map_into(), lrck.map_into(), sdin.map_into(), sdout.map_into(), config) + } + + fn new_inner( + i2s: impl Peripheral

+ 'd, + // irq: impl Peripheral

+ 'd, + mck: PeripheralRef<'d, AnyPin>, + sck: PeripheralRef<'d, AnyPin>, + lrck: PeripheralRef<'d, AnyPin>, + sdin: PeripheralRef<'d, AnyPin>, + sdout: PeripheralRef<'d, AnyPin>, + config: Config, + ) -> Self { + into_ref!( + i2s, + // irq, + mck, sck, lrck, sdin, sdout); + + let r = T::regs(); + + // TODO get configuration rather than hardcoding ratio, swidth, align, format, channels + + r.config.mcken.write(|w| w.mcken().enabled()); + r.config.mckfreq.write(|w| w.mckfreq()._32mdiv16()); + r.config.ratio.write(|w| w.ratio()._192x()); + r.config.mode.write(|w| w.mode().master()); + r.config.swidth.write(|w| w.swidth()._16bit()); + r.config.align.write(|w| w.align().left()); + r.config.format.write(|w| w.format().i2s()); + r.config.channels.write(|w| w.channels().stereo()); + + r.psel.mck.write(|w| { + unsafe { w.bits(mck.psel_bits()) }; + w.connect().connected() + }); + + r.psel.sck.write(|w| { + unsafe { w.bits(sck.psel_bits()) }; + w.connect().connected() + }); + + r.psel.lrck.write(|w| { + unsafe { w.bits(lrck.psel_bits()) }; + w.connect().connected() + }); + + r.psel.sdin.write(|w| { + unsafe { w.bits(sdin.psel_bits()) }; + w.connect().connected() + }); + + r.psel.sdout.write(|w| { + unsafe { w.bits(sdout.psel_bits()) }; + w.connect().connected() + }); + + r.enable.write(|w| w.enable().enabled()); + + Self { + output: I2sOutput { + _p: unsafe { i2s.clone_unchecked() }, + }, + input: I2sInput { _p: i2s }, + } + } + + /// Enables the I2S module. + #[inline(always)] + pub fn enable(&self) -> &Self { + let r = T::regs(); + r.enable.write(|w| w.enable().enabled()); + self + } + + /// Disables the I2S module. + #[inline(always)] + pub fn disable(&self) -> &Self { + let r = T::regs(); + r.enable.write(|w| w.enable().disabled()); + self + } + + /// Starts I2S transfer. + #[inline(always)] + pub fn start(&self) -> &Self { + let r = T::regs(); + self.enable(); + r.tasks_start.write(|w| unsafe { w.bits(1) }); + self + } + + /// Stops the I2S transfer and waits until it has stopped. + #[inline(always)] + pub fn stop(&self) -> &Self { + todo!() + } + + /// Enables/disables I2S transmission (TX). + #[inline(always)] + pub fn set_tx_enabled(&self, enabled: bool) -> &Self { + let r = T::regs(); + r.config.txen.write(|w| w.txen().bit(enabled)); + self + } + + /// Enables/disables I2S reception (RX). + #[inline(always)] + pub fn set_rx_enabled(&self, enabled: bool) -> &Self { + let r = T::regs(); + r.config.rxen.write(|w| w.rxen().bit(enabled)); + self + } + + /// Transmits the given `tx_buffer`. + /// Buffer address must be 4 byte aligned and located in RAM. + /// Returns a value that represents the in-progress DMA transfer. + // TODO Define a better interface for the input buffer + #[allow(unused_mut)] + pub async fn tx(&mut self, ptr: *const u8, len: usize) -> Result<(), Error> { + self.output.tx(ptr, len).await + } +} + +impl<'d, T: Instance> I2sOutput<'d, T> { + /// Transmits the given `tx_buffer`. + /// Buffer address must be 4 byte aligned and located in RAM. + /// Returns a value that represents the in-progress DMA transfer. + // TODO Define a better interface for the input buffer + pub async fn tx(&mut self, ptr: *const u8, len: usize) -> Result<(), Error> { + if ptr as u32 % 4 != 0 { + return Err(Error::BufferMisaligned); + } + let maxcnt = (len / (core::mem::size_of::() / core::mem::size_of::())) as u32; + if maxcnt > MAX_DMA_MAXCNT { + return Err(Error::BufferTooLong); + } + if (ptr as usize) < SRAM_LOWER || (ptr as usize) > SRAM_UPPER { + return Err(Error::DMABufferNotInDataMemory); + } + + let r = T::regs(); + let _s = T::state(); + + // TODO we can not progress until the last buffer written in TXD.PTR + // has started the transmission. + // We can use some sync primitive from `embassy-sync`. + + r.txd.ptr.write(|w| unsafe { w.ptr().bits(ptr as u32) }); + r.rxtxd.maxcnt.write(|w| unsafe { w.bits(maxcnt) }); + + Ok(()) + } +} + +pub(crate) mod sealed { + use core::sync::atomic::AtomicU8; + + use embassy_sync::waitqueue::AtomicWaker; + + use super::*; + + pub struct State { + pub input_waker: AtomicWaker, + pub output_waker: AtomicWaker, + pub buffers_refcount: AtomicU8, + } + impl State { + pub const fn new() -> Self { + Self { + input_waker: AtomicWaker::new(), + output_waker: AtomicWaker::new(), + buffers_refcount: AtomicU8::new(0), + } + } + } + + pub trait Instance { + fn regs() -> &'static pac::i2s::RegisterBlock; + fn state() -> &'static State; + } +} + +pub trait Instance: Peripheral

+ sealed::Instance + 'static + Send { + type Interrupt: Interrupt; +} + +macro_rules! impl_i2s { + ($type:ident, $pac_type:ident, $irq:ident) => { + impl crate::i2s::sealed::Instance for peripherals::$type { + fn regs() -> &'static pac::i2s::RegisterBlock { + unsafe { &*pac::$pac_type::ptr() } + } + fn state() -> &'static crate::i2s::sealed::State { + static STATE: crate::i2s::sealed::State = crate::i2s::sealed::State::new(); + &STATE + } + } + impl crate::i2s::Instance for peripherals::$type { + type Interrupt = crate::interrupt::$irq; + } + }; +} + diff --git a/embassy-nrf/src/lib.rs b/embassy-nrf/src/lib.rs index bc70fc2f..ac797db9 100644 --- a/embassy-nrf/src/lib.rs +++ b/embassy-nrf/src/lib.rs @@ -74,6 +74,8 @@ pub mod buffered_uarte; pub mod gpio; #[cfg(feature = "gpiote")] pub mod gpiote; +// #[cfg(all(feature = "i2s", feature = "nrf52840"))] +pub mod i2s; #[cfg(not(any(feature = "_nrf5340", feature = "_nrf9160")))] pub mod nvmc; #[cfg(any( diff --git a/examples/nrf/Cargo.toml b/examples/nrf/Cargo.toml index c633f82f..a79044e8 100644 --- a/examples/nrf/Cargo.toml +++ b/examples/nrf/Cargo.toml @@ -14,7 +14,7 @@ embassy-futures = { version = "0.1.0", path = "../../embassy-futures" } embassy-sync = { version = "0.1.0", path = "../../embassy-sync", features = ["defmt"] } embassy-executor = { version = "0.1.0", path = "../../embassy-executor", features = ["defmt", "integrated-timers"] } embassy-time = { version = "0.1.0", path = "../../embassy-time", features = ["defmt", "defmt-timestamp-uptime"] } -embassy-nrf = { version = "0.1.0", path = "../../embassy-nrf", features = ["defmt", "nrf52840", "time-driver-rtc1", "gpiote", "unstable-pac"] } +embassy-nrf = { version = "0.1.0", path = "../../embassy-nrf", features = ["defmt", "nrf52840", "time-driver-rtc1", "gpiote", "i2s", "unstable-pac"] } embassy-net = { version = "0.1.0", path = "../../embassy-net", features = ["defmt", "tcp", "dhcpv4", "medium-ethernet", "pool-16"], optional = true } embassy-usb = { version = "0.1.0", path = "../../embassy-usb", features = ["defmt"], optional = true } embedded-io = "0.3.1" diff --git a/examples/nrf/src/bin/i2s.rs b/examples/nrf/src/bin/i2s.rs new file mode 100644 index 00000000..556f6b2e --- /dev/null +++ b/examples/nrf/src/bin/i2s.rs @@ -0,0 +1,48 @@ +// Example inspired by RTIC's I2S demo: https://github.com/nrf-rs/nrf-hal/blob/master/examples/i2s-controller-demo/src/main.rs + +#![no_std] +#![no_main] +#![feature(type_alias_impl_trait)] + +use defmt::*; +use embassy_executor::Spawner; +use embassy_nrf::{i2s}; +use {defmt_rtt as _, panic_probe as _}; + +#[repr(align(4))] +pub struct Aligned(T); + +#[embassy_executor::main] +async fn main(_spawner: Spawner) { + let p = embassy_nrf::init(Default::default()); + let config = i2s::Config::default(); + + let mut i2s = i2s::I2s::new(p.I2S, p.P0_28, p.P0_29, p.P0_31, p.P0_11, p.P0_30, config); + + let mut signal_buf: Aligned<[i16; 32]> = Aligned([0i16; 32]); + let len = signal_buf.0.len() / 2; + for x in 0..len { + signal_buf.0[2 * x] = triangle_wave(x as i32, len, 2048, 0, 1) as i16; + signal_buf.0[2 * x + 1] = triangle_wave(x as i32, len, 2048, 0, 1) as i16; + } + + let ptr = &signal_buf.0 as *const i16 as *const u8; + let len = signal_buf.0.len() * core::mem::size_of::(); + + i2s.start(); + i2s.set_tx_enabled(true); + + loop { + i2s.tx(ptr, len).await; + } +} + +fn triangle_wave(x: i32, length: usize, amplitude: i32, phase: i32, periods: i32) -> i32 { + let length = length as i32; + amplitude + - ((2 * periods * (x + phase + length / (4 * periods)) * amplitude / length) + % (2 * amplitude) + - amplitude) + .abs() + - amplitude / 2 +}