diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f41a9c2..dc6a65d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,7 +3,7 @@ name: Build and Test check jobs: builds: name: Build checks - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 strategy: matrix: mode: ["", "--release"] @@ -40,9 +40,12 @@ jobs: target: thumbv6m-none-eabi override: true profile: minimal - - name: Check unused deps - uses: aig787/cargo-udeps-action@v1 - with: - version: latest - args: ${{ matrix.mode }} + - name: Install cargo-hack + run: | + curl -sSL https://github.com/taiki-e/cargo-hack/releases/download/v0.6.17/cargo-hack-x86_64-unknown-linux-gnu.tar.gz | tar xvzf - -C ~/.cargo/bin + - name: Install cargo-udeps + run: | + curl -sSL https://github.com/est31/cargo-udeps/releases/download/v0.1.55/cargo-udeps-v0.1.55-x86_64-unknown-linux-gnu.tar.gz | tar xvzf - --strip-components=2 -C ~/.cargo/bin ./cargo-udeps-v0.1.55-x86_64-unknown-linux-gnu/cargo-udeps + - name: Run cargo-udeps + run: cargo hack udeps --optional-deps --each-feature diff --git a/Cargo.toml b/Cargo.toml index c1097a9..bc750c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,16 +22,36 @@ fugit = "0.3.5" defmt = { version = "0.3.0", optional = true } either = { version = "1.10.0", default-features = false } heapless = { version = "0.8.0", default-features = false } +futures = { version = "0.3.30", default-features = false, features = [ + "async-await", +] } +embedded-hal-async = "1.0.0" +i2c-write-iter = { version = "1.0.0", features = ["async"], optional = true } [profile.dev] codegen-units = 1 incremental = false lto = 'fat' opt-level = 's' +debug = 2 [profile.test] codegen-units = 1 incremental = false lto = 'fat' +opt-level = 2 +debug = 2 + +[profile.release] +codegen-units = 1 +incremental = false +lto = 'fat' opt-level = 's' +debug = 2 + +[features] +defmt = ["dep:defmt", "heapless/defmt-03", "embedded-hal-async/defmt-03"] +i2c-write-iter = ["dep:i2c-write-iter"] +[patch.crates-io] +rp2040-hal = { git = "https://github.com/ithinuel/rp-hal", branch = "async-pio" } diff --git a/on-target-tests/Cargo.toml b/on-target-tests/Cargo.toml index d2ab24c..ca416db 100644 --- a/on-target-tests/Cargo.toml +++ b/on-target-tests/Cargo.toml @@ -8,8 +8,12 @@ publish = false name = "i2c_loopback" harness = false +[[test]] +name = "i2c_loopback_async" +harness = false + [dependencies] -i2c-pio = { path = "../", features = ["defmt"] } +i2c-pio = { path = "../", features = ["defmt", "i2c-write-iter"] } cortex-m = "0.7" cortex-m-rt = "0.7" @@ -38,3 +42,12 @@ heapless = { version = "0.8.0", features = [ "defmt-03", ] } itertools = { version = "0.12.0", default-features = false } +tinywake = { version = "0.2.2", features = ["cortex-m"] } +futures = { version = "0.3.30", default-features = false, features = [ + "async-await", +] } + +i2c-write-iter = { version = "1.0.0", features = [ + "async", + "embedded-hal-async", +] } diff --git a/on-target-tests/tests/i2c_loopback_async.rs b/on-target-tests/tests/i2c_loopback_async.rs new file mode 100644 index 0000000..3681cb7 --- /dev/null +++ b/on-target-tests/tests/i2c_loopback_async.rs @@ -0,0 +1,100 @@ +//! This test needs a connection between: +//! +//! | from GPIO (pico Pin) | to GPIO (pico Pin) | +//! | -------------------- | ------------------ | +//! | 0 (1) | 2 (4) | +//! | 1 (2) | 3 (5) | + +#![no_std] +#![no_main] +#![cfg(test)] + +use defmt_rtt as _; // defmt transport +use defmt_test as _; +use panic_probe as _; +use rp2040_hal as hal; // memory layout // panic handler + +use hal::{async_utils::AsyncPeripheral, pac::interrupt}; + +/// The linker will place this boot block at the start of our program image. We +/// need this to help the ROM bootloader get our code up and running. +/// Note: This boot block is not necessary when using a rp-hal based BSP +/// as the BSPs already perform this step. +#[link_section = ".boot2"] +#[used] +pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H; + +/// External high-speed crystal on the Raspberry Pi Pico board is 12 MHz. Adjust +/// if your board has a different frequency +const XTAL_FREQ_HZ: u32 = 12_000_000u32; + +pub mod i2c_tests; + +#[interrupt] +unsafe fn PIO0_IRQ_0() { + rp2040_hal::pio::Rx::::on_interrupt(rp2040_hal::pio::PioIRQ::Irq0); + rp2040_hal::pio::Tx::::on_interrupt(rp2040_hal::pio::PioIRQ::Irq0); + rp2040_hal::pio::Interrupt::<'_, rp2040_hal::pac::PIO0, 0>::on_interrupt(); +} + +#[interrupt] +unsafe fn I2C1_IRQ() { + i2c_tests::Target::on_interrupt(); +} + +#[defmt_test::tests] +mod tests { + use crate::i2c_tests::{ + non_blocking::{self, run_test, State}, + ADDR_10BIT, ADDR_7BIT, + }; + + #[init] + fn setup() -> State { + non_blocking::system_setup(super::XTAL_FREQ_HZ, ADDR_7BIT) + } + + #[test] + fn embedded_hal(state: &mut State) { + run_test(non_blocking::embedded_hal(state, ADDR_7BIT, 2..=2)); + run_test(non_blocking::embedded_hal(state, ADDR_10BIT, 2..=7)); + } + + #[test] + fn transactions_iter(state: &mut State) { + run_test(non_blocking::transaction(state, ADDR_7BIT, 7..=9)); + run_test(non_blocking::transaction(state, ADDR_10BIT, 7..=14)); + } + + #[test] + fn i2c_write_iter(state: &mut State) { + run_test(non_blocking::transaction_iter(state, ADDR_7BIT)); + run_test(non_blocking::transaction_iter(state, ADDR_10BIT)); + } + + #[test] + fn transaction_iter(state: &mut State) { + run_test(non_blocking::transaction_iter(state, ADDR_7BIT)); + run_test(non_blocking::transaction_iter(state, ADDR_10BIT)); + } + + // Sad paths: + // invalid tx buf on write + // invalid rx buf on read + // + // invalid (rx/tx) buf in transactions + // + // Peripheral Nack + #[test] + fn nak_on_addr(state: &mut State) { + run_test(non_blocking::nak_on_addr(state, ADDR_7BIT, ADDR_7BIT + 1)); + run_test(non_blocking::nak_on_addr(state, ADDR_10BIT, ADDR_10BIT + 1)); + run_test(non_blocking::nak_on_addr( + state, + ADDR_10BIT, + ADDR_10BIT + 0x100, + )); + } + // + // Arbritration conflict +} diff --git a/on-target-tests/tests/i2c_tests/mod.rs b/on-target-tests/tests/i2c_tests/mod.rs index 5fa930d..2caa453 100644 --- a/on-target-tests/tests/i2c_tests/mod.rs +++ b/on-target-tests/tests/i2c_tests/mod.rs @@ -11,7 +11,7 @@ use rp2040_hal::{ }; pub mod blocking; -//pub mod non_blocking; +pub mod non_blocking; pub const ADDR_7BIT: u8 = 0x2c; pub const ADDR_10BIT: u16 = 0x12c; diff --git a/on-target-tests/tests/i2c_tests/non_blocking.rs b/on-target-tests/tests/i2c_tests/non_blocking.rs new file mode 100644 index 0000000..cdde145 --- /dev/null +++ b/on-target-tests/tests/i2c_tests/non_blocking.rs @@ -0,0 +1,408 @@ +use core::{ + cell::RefCell, + future::Future, + ops::{Deref, RangeInclusive}, + task::Poll, +}; + +use fugit::{HertzU32, RateExtU32}; +use futures::FutureExt; +use hal::{gpio::FunctionNull, pio::PIOExt}; +use heapless::Vec; + +use rp2040_hal::{ + self as hal, + clocks::init_clocks_and_plls, + gpio::{FunctionI2C, Pin, PullUp}, + pac::{self, PIO0}, + pio::{UninitStateMachine, PIO, PIO0SM0}, + watchdog::Watchdog, + Clock, +}; + +use super::{ + Controller, CtrlPinScl, CtrlPinSda, FIFOBuffer, Generator, Target, TargetState, ValidAddress, +}; +use i2c_pio::Error; + +pub struct State { + pio: PIO, + i2c_components: Option<((CtrlPinSda, CtrlPinScl), UninitStateMachine)>, + + target: Option, + resets: hal::pac::RESETS, + ref_clock_freq: HertzU32, + payload: RefCell, +} + +pub fn run_test(mut f: impl Future) { + tinywake::run_all([&mut f]); +} +async fn wait_with(payload: &RefCell, mut f: impl FnMut(&TargetState) -> bool) { + while f(payload.borrow().deref()) { + let mut done = false; + core::future::poll_fn(|cx| { + cx.waker().wake_by_ref(); + if !done { + done = true; + Poll::Pending + } else { + Poll::Ready(()) + } + }) + .await; + } +} + +pub fn system_setup(xtal_freq_hz: u32, addr: T) -> State { + unsafe { + hal::sio::spinlock_reset(); + } + let mut pac = pac::Peripherals::take().unwrap(); + let mut watchdog = Watchdog::new(pac.WATCHDOG); + + let clocks = init_clocks_and_plls( + xtal_freq_hz, + pac.XOSC, + pac.CLOCKS, + pac.PLL_SYS, + pac.PLL_USB, + &mut pac.RESETS, + &mut watchdog, + ) + .ok() + .unwrap(); + + // The single-cycle I/O block controls our GPIO pins + let sio = hal::Sio::new(pac.SIO); + let pins = hal::gpio::Pins::new( + pac.IO_BANK0, + pac.PADS_BANK0, + sio.gpio_bank0, + &mut pac.RESETS, + ); + + // Configure two pins as being I²C, not GPIO + let ctrl_sda_pin: Pin<_, FunctionNull, PullUp> = pins.gpio0.reconfigure(); + let ctrl_scl_pin: Pin<_, FunctionNull, PullUp> = pins.gpio1.reconfigure(); + + let trg_sda_pin: Pin<_, FunctionI2C, PullUp> = pins.gpio2.reconfigure(); + let trg_scl_pin: Pin<_, FunctionI2C, PullUp> = pins.gpio3.reconfigure(); + + let i2c_target = hal::I2C::new_peripheral_event_iterator( + pac.I2C1, + trg_sda_pin, + trg_scl_pin, + &mut pac.RESETS, + addr, + ); + let (pio, sm, ..) = pac.PIO0.split(&mut pac.RESETS); + + unsafe { + pac::NVIC::unpend(hal::pac::Interrupt::PIO0_IRQ_0); + pac::NVIC::unmask(hal::pac::Interrupt::PIO0_IRQ_0); + pac::NVIC::unpend(hal::pac::Interrupt::I2C1_IRQ); + pac::NVIC::unmask(hal::pac::Interrupt::I2C1_IRQ); + } + + State { + pio, + i2c_components: Some(((ctrl_sda_pin, ctrl_scl_pin), sm)), + + target: Some(i2c_target), + resets: pac.RESETS, + ref_clock_freq: clocks.system_clock.freq(), + payload: RefCell::new(TargetState::new()), + } +} + +pub fn test_setup<'s, T: ValidAddress>( + state: &'s mut State, + addr: T, + _throttling: bool, +) -> (Controller<'s>, &'s RefCell, &'s mut Target) { + let ((sda, scl), sm) = state + .i2c_components + .take() + .expect("I2C components are available"); + + let (i2c_t, (sda_t, scl_t)) = state + .target + .take() + .expect("target's missing") + .free(&mut state.resets); + + state.payload.replace(Default::default()); + + state.target = Some(hal::I2C::new_peripheral_event_iterator( + i2c_t, + sda_t, + scl_t, + &mut state.resets, + addr, + )); + + ( + i2c_pio::I2C::new( + &mut state.pio, + sda, + scl, + sm, + 200.kHz(), + state.ref_clock_freq.clone(), + ), + &state.payload, + state.target.as_mut().expect("target is available"), + ) +} + +macro_rules! test_teardown { + ($state:expr, $controller:expr) => { + $state.i2c_components = Some($controller.free()); + }; +} + +pub async fn target_handler(payload: &RefCell, target: &mut Target) -> (u32, u32) { + loop { + let evt = target.wait_next().await; + + super::target_handler(target, evt, &mut *payload.borrow_mut(), false); + } +} + +async fn embedded_hal_case( + controller: &mut Controller<'_>, + addr: A, + v: &mut ([u8; 25], [u8; 25], [u8; 25], [u8; 14], [u8; 14]), + payload: &RefCell, +) -> Result<(), Error> { + use embedded_hal_async::i2c::I2c; + let sample1: FIFOBuffer = Generator::seq().take(25).collect(); + let sample2: FIFOBuffer = Generator::fib().take(14).collect(); + + // we need to wait for stop to be registered between each operations otherwise we have no + // way to know when the Target side has finished processing the last request. + controller.write(addr, &sample1).await?; + wait_with(payload, |p| p.stop_cnt != 1).await; + + controller.read(addr, &mut v.0).await?; + wait_with(payload, |p| p.stop_cnt != 2).await; + + controller.read(addr, &mut v.1).await?; + wait_with(payload, |p| p.stop_cnt != 3).await; + + controller.read(addr, &mut v.2).await?; + wait_with(payload, |p| p.stop_cnt != 4).await; + + controller.write_read(addr, &sample2, &mut v.3).await?; + wait_with(payload, |p| p.stop_cnt != 5).await; + + controller.write(addr, &sample2).await?; + wait_with(payload, |p| p.stop_cnt != 6).await; + + controller.write(addr, &sample1).await?; + wait_with(payload, |p| p.stop_cnt != 7).await; + + controller.write_read(addr, &sample1, &mut v.4).await?; + wait_with(payload, |p| p.stop_cnt != 8).await; + Ok::<(), Error>(()) +} +pub async fn embedded_hal( + state: &mut State, + addr: T, + restart_count: RangeInclusive, +) { + // Throttling is important for this test as it also ensures that the Target implementation + // does not "waste" bytes that would be discarded otherwise. + // + // One down side of this is that the Target implementation is unable to detect restarts + // between consicutive write operations + let (mut controller, payload, target) = test_setup(state, addr, true); + + // Test + let mut v = Default::default(); + let ctrl = embedded_hal_case(&mut controller, addr, &mut v, &payload); + let trgt = target_handler(payload, target); + futures::select_biased! { + r = ctrl.fuse() => r.expect("Controller test success"), + _ = trgt.fuse() => {} + } + + // Validate + + // There are 14restarts in this sequence but because of latency in the target handling, it + // may only detect 7. + let actual_restart_count = payload.borrow().restart_cnt; + assert!( + restart_count.contains(&actual_restart_count), + "restart count out of range {} ∉ {:?}", + actual_restart_count, + restart_count + ); + + // assert writes + let sample1: FIFOBuffer = Generator::seq().take(25).collect(); + let sample2: FIFOBuffer = Generator::fib().take(14).collect(); + let e: FIFOBuffer = itertools::chain!( + sample1.iter(), + sample2.iter(), + sample2.iter(), + sample1.iter(), + sample1.iter(), + ) + .cloned() + .collect(); + assert_eq!(payload.borrow().vec, e); + // assert reads + let g: FIFOBuffer = itertools::chain!( + Generator::fib().take(25), + Generator::fib().skip(25 + 7).take(25), + Generator::fib().skip(2 * (25 + 7)).take(25), + Generator::seq().take(14), + Generator::fib().take(14) + ) + .collect(); + let h: FIFOBuffer = itertools::chain!( + v.0.into_iter(), + v.1.into_iter(), + v.2.into_iter(), + v.3.into_iter(), + v.4.into_iter() + ) + .collect(); + assert_eq!(g, h); + + test_teardown!(state, controller); +} + +pub async fn transaction( + state: &mut State, + addr: A, + restart_count: RangeInclusive, +) { + use embedded_hal::i2c::Operation; + use embedded_hal_async::i2c::I2c; + let (mut controller, payload, target) = test_setup(state, addr, true); + + // Throttling is important for this test as it also ensures that the Target implementation + // does not "waste" bytes that would be discarded otherwise. + // + // One down side of this is that the Target implementation is unable to detect restarts + // between consicutive write operations + let sample1: Vec = Generator::seq().take(25).collect(); + let sample2: Vec = Generator::fib().take(14).collect(); + + // Test + let mut v: ([u8; 25], [u8; 25], [u8; 25], [u8; 14], [u8; 14]) = Default::default(); + let mut ops = [ + Operation::Write(&sample1), // goes to v2 + Operation::Read(&mut v.0), + Operation::Read(&mut v.1), + Operation::Read(&mut v.2), + Operation::Write(&sample2), // goes to v3 + Operation::Read(&mut v.3), + Operation::Write(&sample2), // goes to v4 + Operation::Write(&sample1), // remains in buffer + Operation::Write(&sample1), // remains in buffer + Operation::Read(&mut v.4), + ]; + + let case = async { + controller + .transaction(addr, &mut ops) + .await + .expect("Controller test success"); + wait_with(&payload, |p| p.stop_cnt != 1).await; + }; + futures::select_biased! { + _ = case.fuse() => {} + _ = target_handler( + payload, + target, + ).fuse() => {} + } + + // Validate + + // There are 14restarts in this sequence but because of latency in the target handling, it + // may only detect 7. + let actual_restart_count = payload.borrow().restart_cnt; + assert!( + restart_count.contains(&actual_restart_count), + "restart count out of range {} ∉ {:?}", + actual_restart_count, + restart_count + ); + // assert writes + let e: FIFOBuffer = itertools::chain!( + Generator::seq().take(25), + Generator::fib().take(14), + Generator::fib().take(14), + Generator::seq().take(25), + Generator::seq().take(25), + ) + .collect(); + assert_eq!(e, payload.borrow().vec); + // assert reads + let g: FIFOBuffer = itertools::chain!( + Generator::fib().take(25), + Generator::fib().skip(32).take(25), + Generator::fib().skip(64).take(25), + Generator::fib().skip(96).take(14), + Generator::fib().skip(112).take(14), + ) + .collect(); + let h: FIFOBuffer = itertools::chain!( + v.0.into_iter(), + v.1.into_iter(), + v.2.into_iter(), + v.3.into_iter(), + v.4.into_iter() + ) + .collect(); + assert_eq!(g, h); + test_teardown!(state, controller); +} + +pub async fn transaction_iter(state: &mut State, addr: A) { + let (mut controller, payload, target) = test_setup(state, addr, true); + + use i2c_write_iter::non_blocking::I2cIter; + let samples: FIFOBuffer = Generator::seq().take(25).collect(); + let case = async { + I2cIter::transaction_iter( + &mut controller, + addr, + [i2c_write_iter::Operation::WriteIter(samples.clone())], + ) + .await + .expect("Controller test success"); + wait_with(&payload, |p| p.stop_cnt != 1).await; + }; + + futures::select_biased! { + _ = case.fuse() => {} + _ = target_handler( + &payload, + target + ).fuse() => {} + } + + assert_eq!(samples, payload.borrow().vec); + test_teardown!(state, controller); +} + +/// the address given must be different from each other. +pub async fn nak_on_addr(state: &mut State, addr_target: A, addr_ctrl: A) { + use embedded_hal_async::i2c::I2c; + let (mut controller, payload, _target) = test_setup(state, addr_target, true); + + let samples1: FIFOBuffer = Generator::seq().take(25).collect(); + assert_eq!( + controller.write(addr_ctrl, &samples1).await, + Err(Error::NoAcknowledgeAddress) + ); + + assert_eq!(payload.borrow().vec, [], "FIFO"); + test_teardown!(state, controller); +} diff --git a/src/eh0_2.rs b/src/eh0_2.rs index 7d18827..fd2f2bd 100644 --- a/src/eh0_2.rs +++ b/src/eh0_2.rs @@ -13,7 +13,7 @@ where fn read(&mut self, address: A, buffer: &mut [u8]) -> Result<(), Self::Error> { let iter = super::setup(address, true, false).chain(buffer.iter_mut().map(CmdWord::read)); self.process_queue(iter)?; - self.generate_stop(); + Self::generate_stop(&mut self.tx); Ok(()) } } @@ -34,7 +34,7 @@ where { let iter = super::setup(address, false, false).chain(bytes.into_iter().map(CmdWord::write)); self.process_queue(iter)?; - self.generate_stop(); + Self::generate_stop(&mut self.tx); Ok(()) } } @@ -78,7 +78,7 @@ where .chain(super::setup(address, true, true)) .chain(buffer.iter_mut().map(CmdWord::read)), )?; - self.generate_stop(); + Self::generate_stop(&mut self.tx); Ok(()) } } @@ -135,7 +135,7 @@ where self.process_queue(iter)?; first = false; } - self.generate_stop(); + Self::generate_stop(&mut self.tx); Ok(()) } } @@ -165,7 +165,7 @@ where self.process_queue(iter)?; first = false; } - self.generate_stop(); + Self::generate_stop(&mut self.tx); Ok(()) } } diff --git a/src/eh1_0.rs b/src/eh1_0.rs index 1bb8b12..dcc0621 100644 --- a/src/eh1_0.rs +++ b/src/eh1_0.rs @@ -20,7 +20,7 @@ where self.process_queue( super::setup(address, false, false).chain(bytes.into_iter().map(CmdWord::write)), )?; - self.generate_stop(); + Self::generate_stop(&mut self.tx); Ok(()) } @@ -40,7 +40,7 @@ where .chain(super::setup(address, true, true)) .chain(buffer.iter_mut().map(CmdWord::read)), )?; - self.generate_stop(); + Self::generate_stop(&mut self.tx); Ok(()) } @@ -63,7 +63,39 @@ where self.process_queue(iter)?; first = false; } - self.generate_stop(); + Self::generate_stop(&mut self.tx); + Ok(()) + } +} + +#[cfg(feature = "i2c-write-iter")] +impl i2c_write_iter::I2cIter for I2C<'_, P, SMI, SDA, SCL> +where + A: i2c_write_iter::AddressMode + ValidAddressMode, + P: PIOExt, + SMI: StateMachineIndex, + SDA: AnyPin, + SCL: AnyPin, +{ + fn transaction_iter<'a, O, B>(&mut self, address: A, operations: O) -> Result<(), Self::Error> + where + O: IntoIterator>, + B: IntoIterator, + { + let mut first = true; + for op in operations { + let iter = match op { + i2c_write_iter::Operation::Read(buf) => Left( + super::setup(address, true, !first).chain(buf.iter_mut().map(CmdWord::read)), + ), + i2c_write_iter::Operation::WriteIter(buf) => Right( + super::setup(address, false, !first).chain(buf.into_iter().map(CmdWord::write)), + ), + }; + self.process_queue(iter)?; + first = false; + } + Self::generate_stop(&mut self.tx); Ok(()) } } @@ -115,7 +147,76 @@ where self.process_queue(iter)?; first = false; } - self.generate_stop(); + Self::generate_stop(&mut self.tx); + Ok(()) + } +} + +impl embedded_hal_async::i2c::I2c for I2C<'_, P, SMI, SDA, SCL> +where + A: ValidAddressMode, + P: PIOExt, + SMI: StateMachineIndex, + SDA: AnyPin, + SCL: AnyPin, +{ + async fn transaction( + &mut self, + address: A, + operations: &mut [Operation<'_>], + ) -> Result<(), Self::Error> { + let mut do_restart = false; + for op in operations.iter_mut() { + let iter = match op { + Operation::Read(b) => Left( + super::setup(address, true, do_restart).chain(b.iter_mut().map(CmdWord::read)), + ), + Operation::Write(b) => Right( + super::setup(address, false, do_restart) + .chain(b.iter().cloned().map(CmdWord::write)), + ), + }; + self.process_queue_async(iter).await?; + do_restart = true; + } + Self::generate_stop(&mut self.tx); + Ok(()) + } +} + +#[cfg(feature = "i2c-write-iter")] +impl i2c_write_iter::non_blocking::I2cIter for I2C<'_, P, SMI, SDA, SCL> +where + A: i2c_write_iter::AddressMode + ValidAddressMode, + P: PIOExt, + SMI: StateMachineIndex, + SDA: AnyPin, + SCL: AnyPin, +{ + async fn transaction_iter<'a, O, B>( + &mut self, + address: A, + operations: O, + ) -> Result<(), Self::Error> + where + O: IntoIterator>, + B: IntoIterator, + { + let mut do_restart = false; + for op in operations { + let iter = match op { + i2c_write_iter::Operation::Read(b) => Left( + super::setup(address, true, do_restart).chain(b.iter_mut().map(CmdWord::read)), + ), + i2c_write_iter::Operation::WriteIter(b) => Right( + super::setup(address, false, do_restart) + .chain(b.into_iter().map(CmdWord::write)), + ), + }; + self.process_queue_async(iter).await?; + do_restart = true; + } + Self::generate_stop(&mut self.tx); Ok(()) } } diff --git a/src/i2c_cmd.rs b/src/i2c_cmd.rs index 28b8ece..13096e9 100644 --- a/src/i2c_cmd.rs +++ b/src/i2c_cmd.rs @@ -87,7 +87,7 @@ impl defmt::Format for Data<'_> { Left(b) => defmt::write!(fmt, "{:x}", b), Right(_) => defmt::write!(fmt, "…"), } - defmt::write!(fmt, " }}"); + defmt::write!(fmt, ", is_addr: {} }}", self.is_address); } } #[derive(Debug)] diff --git a/src/lib.rs b/src/lib.rs index a14ea90..0db068d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,7 +40,10 @@ //! (It's possible for the inversion to be done in this program, //! but costs 2 instructions: 1 for inversion, and one to cope //! with the side effect of the MOV on TX shift counter.) -use core::iter::once; +use core::{ + cell::RefCell, + iter::{once, Peekable}, +}; use either::Either::{Left, Right}; use fugit::HertzU32; @@ -53,8 +56,8 @@ use rp2040_hal::{ ValidFunction, }, pio::{ - PIOExt, PinDir, PinState, Rx, ShiftDirection, StateMachine, StateMachineIndex, Tx, - UninitStateMachine, PIO, + PIOExt, PinDir, PinState, PioIRQ, Running, Rx, ShiftDirection, StateMachine, + StateMachineIndex, Tx, UninitStateMachine, PIO, }, }; @@ -64,6 +67,8 @@ mod eh0_2; mod eh1_0; mod i2c_cmd; +mod utils; + /// Length of an address. #[derive(Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] @@ -106,25 +111,16 @@ fn setup<'b, A: ValidAddressMode>( Left(once(address_and_flag).map(CmdWord::address)) } AddressLength::_10 => { - let addr_hi = 0xF0 | ((address >> 7) as u8) & 0xFE; + let addr_hi = 0xF0 | (((address & 0x0700) >> 7) as u8); let addr_lo = (address & 0xFF) as u8; Right(if read { - let full_addr = [addr_hi, addr_lo] - .into_iter() - .map(Data::address) - .map(CmdWord::Data); - let read_addr = - restart().chain(once(CmdWord::Data(Data::address(addr_hi | read_flag)))); - - Left(full_addr.chain(read_addr)) + let addr_write = [addr_hi, addr_lo].into_iter().map(CmdWord::address); + let addr_read = once(addr_hi | read_flag).map(CmdWord::address); + Left(addr_write.chain(restart()).chain(addr_read)) } else { - Right( - [addr_hi | read_flag, addr_lo] - .into_iter() - .map(Data::address) - .map(CmdWord::Data), - ) + let address = [addr_hi | read_flag, addr_lo].into_iter(); + Right(address.map(CmdWord::address)) }) } }; @@ -328,23 +324,30 @@ where } } - fn has_irq(&mut self) -> bool { + fn has_irq(pio: &mut PIO

) -> bool { let mask = 1 << SMI::id(); - self.pio.get_irq_raw() & mask != 0 + pio.get_irq_raw() & mask != 0 } - fn err_with(&mut self, err: Error) -> Result<(), Error> { - // clear RX FiFo - while self.rx.read().is_some() {} + /// While this method contains some loop, none of them are expected to run for more than a few + /// cycles. This works for both sync & async operations. + fn err_with( + pio: &mut PIO

, + sm: &mut StateMachine<(P, SMI), Running>, + rx: &mut Rx<(P, SMI)>, + tx: &mut Tx<(P, SMI)>, + ) { // Clear Tx FiFo - self.sm.drain_tx_fifo(); + sm.drain_tx_fifo(); + // clear RX FiFo + while rx.read().is_some() {} // wait for the state machine to either stall on pull or block on irq - self.tx.clear_stalled_flag(); - while !(self.tx.has_stalled() || self.has_irq()) {} + tx.clear_stalled_flag(); + while !(tx.has_stalled() || Self::has_irq(pio)) {} // Clear OSR - if self.has_irq() { - self.sm.exec_instruction(Instruction { + if Self::has_irq(pio) { + sm.exec_instruction(Instruction { operands: pio::InstructionOperands::OUT { destination: pio::OutDestination::NULL, bit_count: 16, @@ -353,23 +356,73 @@ where side_set: None, }); // resume pio driver - self.pio.clear_irq(1 << SMI::id()); + pio.clear_irq(1 << SMI::id()); } // generate stop condition - self.generate_stop(); - Err(err) + Self::generate_stop(tx); } - fn generate_stop(&mut self) { - // this driver checks for acknoledge error and/or expects data back, so by the time a stop + fn generate_stop(tx: &mut Tx<(P, SMI)>) { + // this driver checks for acknowledge error and/or expects data back, so by the time a stop // is generated, the tx fifo should be empty. - assert!(self.tx.is_empty(), "TX FIFO is empty"); + assert!(tx.is_empty(), "TX FIFO is empty"); stop().for_each(|encoded| { - self.tx.write_u16_replicated(encoded); + tx.write_u16_replicated(encoded); }); - self.tx.clear_stalled_flag(); - while !self.tx.has_stalled() {} + tx.clear_stalled_flag(); + while !tx.has_stalled() {} + } + + // - TX FIFO depth (cmd waiting to be sent) + // - OSR + // - RX FIFO input waiting to be processed + fn process_queue_steps<'b>( + &mut self, + output: &mut Peekable>>, + input: &mut Deque, 9>, + ) -> Result<(), Error> { + // if there is room in the tx fifo + if !self.tx.is_full() { + if let Some(mut word) = output.next() { + let last = matches!( + (&mut word, output.peek()), + (CmdWord::Data(_), None) | (CmdWord::Data(_), Some(CmdWord::Raw(_))) + ); + let word_u16 = word.encode(last); + self.tx.write_u16_replicated(word_u16); + if let CmdWord::Data(d) = word { + input.push_back(d).expect("`input` is not full"); + } + } + } + + if let Some(word) = self.rx.read() { + let word = (word & 0xFF) as u8; + if let Some(d) = input.pop_front() { + match d.byte { + Left(exp) if word != exp => { + Self::err_with(self.pio, &mut self.sm, &mut self.rx, &mut self.tx); + return Err(Error::BusContention); + } + Right(inp) => *inp = word, + _ => {} + } + } + } else if Self::has_irq(self.pio) { + // the byte that err’ed isn’t in the rx fifo. Once we’re done clearing them, we + // know the head of the queue is the byte that failed. + let Some(d) = input.pop_front() else { + unreachable!("There cannot be a failure without a transmission") + }; + Self::err_with(self.pio, &mut self.sm, &mut self.rx, &mut self.tx); + return Err(if d.is_address { + Error::NoAcknowledgeAddress + } else { + Error::NoAcknowledgeData + }); + } + Ok(()) } fn process_queue<'b>( @@ -377,54 +430,113 @@ where queue: impl IntoIterator>, ) -> Result<(), Error> { let mut output = queue.into_iter().peekable(); - // - TX FIFO depth (cmd waiting to be sent) - // - OSR - // - RX FIFO input waiting to be processed - let mut input: Deque, 9> = Deque::new(); + let mut input = Deque::new(); - // while we’re not does sending/receiving + // while we’re not done sending/receiving while output.peek().is_some() || !input.is_empty() { - // if there is room in the tx fifo - if !self.tx.is_full() { - if let Some(mut word) = output.next() { - let last = matches!( - (&mut word, output.peek()), - (CmdWord::Data(_), None) | (CmdWord::Data(_), Some(CmdWord::Raw(_))) - ); - let word_u16 = word.encode(last); - self.tx.write_u16_replicated(word_u16); - if let CmdWord::Data(d) = word { - input.push_back(d).expect("`input` is not full"); - } - } - } + self.process_queue_steps(&mut output, &mut input)?; + } + Ok(()) + } - if let Some(word) = self.rx.read() { - let word = (word & 0xFF) as u8; - if let Some(d) = input.pop_front() { - match d.byte { - Left(exp) if word != exp => { - return self.err_with(Error::BusContention); - } - Right(inp) => *inp = word, - _ => {} - } + async fn receive( + in_flight: &RefCell, 10>>, + rx: &mut Rx<(P, SMI)>, + ) -> Result<(), Error> { + while !in_flight.borrow().is_empty() { + let word = (rx.async_read(PioIRQ::Irq0).await & 0xFF) as u8; + let Some(expected) = in_flight.borrow_mut().pop_front() else { + unreachable!("We cannot receive more data than requested"); + }; + //defmt::trace!("received: {}->{:x}", expected, word); + match expected.byte { + Left(exp) if word != exp => { + return Err(Error::BusContention); } - } else if self.has_irq() { - // the byte that err’ed isn’t in the rx fifo. Once we’re done clearing them, we - // know the head of the queue is the byte that failed. - let Some(d) = input.pop_front() else { - unreachable!("There cannot be a failure without a transmition") - }; - return self.err_with(if d.is_address { - Error::NoAcknowledgeAddress - } else { - Error::NoAcknowledgeData - }); + Right(inp) => *inp = word, + _ => {} } } Ok(()) } + + async fn transmit<'b>( + in_flight: &RefCell, 10>>, + queue: impl IntoIterator>, + tx: &mut Tx<(P, SMI)>, + ) { + let mut output = queue.into_iter().peekable(); + while let Some(mut word) = output.next() { + let last = matches!( + (&mut word, output.peek()), + (CmdWord::Data(_), None) | (CmdWord::Data(_), Some(CmdWord::Raw(_))) + ); + let word_u16 = word.encode(last); + tx.async_write_u16_replicated(PioIRQ::Irq0, word_u16).await; + if let CmdWord::Data(d) = word { + in_flight + .borrow_mut() + .push_back(d) + .expect("`input` is full"); + } + } + } + + async fn process_queue_async<'b>( + &mut self, + queue: impl IntoIterator>, + ) -> Result<(), Error> { + use futures::FutureExt; + // the Deque size should be { PIO::TX_FIFO_SIZE + 1 } but we can’t use const generics in + // there yet. + let in_flight = RefCell::new(Deque::new()); + + // wrap the TX in a RefCell to allow for mutable borrows in async closures + // There might be better solutions to this, but this is the simplest one that works. + let ref_tx = RefCell::new(&mut self.tx); + + assert_eq!(self.rx.read(), None, "RX FIFO should be empty"); + + #[allow(clippy::await_holding_refcell_ref)] + let fut = core::pin::pin!(async { + // this refcell borrow is kept through an await point. The other reference is passed to + // the cancel guard which is only used *if* the future is cancelled. + let tx = &mut *ref_tx.borrow_mut(); + let rx = &mut self.rx; + let mut irq = self.pio.irq0(); + futures::select_biased! { + _ = futures::future::join( + Self::transmit(&in_flight, queue, tx), + Self::receive(&in_flight, rx) + ).fuse() => Ok(()), + _ = irq.sm_interrupt(SMI::id() as u8).fuse() => { + // the byte that err’ed isn’t in the rx fifo. Once we’re done clearing them, we + // know the head of the queue is the byte that failed. + let mut in_flight = in_flight.borrow_mut(); + + let Some(d) = in_flight.pop_front() else { + unreachable!("There cannot be a failure without a transmission") + }; + + Self::err_with(self.pio, &mut self.sm, rx, tx); + // Purge the rx fifo + while rx.read().is_some() {} + Err(if d.is_address { + Error::NoAcknowledgeAddress + } else { + Error::NoAcknowledgeData + }) + } + } + }); + + // wrap all the futures in a cancel guard to make sure a Stop condition is generated and + // that the state machine is left in a clean state. + crate::utils::CancelGuard::new(&ref_tx, fut, |me: &RefCell<&mut Tx<(P, SMI)>>| { + Self::generate_stop(*me.borrow_mut()); + }) + .await + } } impl I2C<'_, P, SMI, SDA, SCL> diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..13758fe --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,77 @@ +use core::cell::RefCell; +use core::future::Future; +use core::pin::Pin; +use core::task::Context; +use core::task::Poll; + +pub struct CancelGuard<'a, P, T, F> +where + T: Future, + F: FnOnce(&'a RefCell<&'a mut P>), +{ + periph: &'a RefCell<&'a mut P>, + task: Option, + cancel_fn: Option, +} + +impl<'a, P, T, F> CancelGuard<'a, P, T, F> +where + T: Future, + F: FnOnce(&'a RefCell<&'a mut P>), +{ + pub fn new(periph: &'a RefCell<&'a mut P>, new_fut: T, cancel_fn: F) -> Self { + Self { + periph, + task: Some(new_fut), + cancel_fn: Some(cancel_fn), + } + } +} + +impl<'a, P, T, F> Unpin for CancelGuard<'a, P, T, F> +where + T: Future + Unpin, + F: FnOnce(&'a RefCell<&'a mut P>), +{ +} + +impl<'a, P, T, F> Future for CancelGuard<'a, P, T, F> +where + T: Future + Unpin, + F: FnOnce(&'a RefCell<&'a mut P>), +{ + type Output = T::Output; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + use futures::FutureExt; + + if let Poll::Ready(v) = self + .task + .as_mut() + .expect("cannot poll CancelGuard twice") + .poll_unpin(cx) + { + // not strictly necessary but makes sure we panic if we poll after + // completion + self.task = None; + self.cancel_fn = None; + return Poll::Ready(v); + } + Poll::Pending + } +} + +impl<'a, P, T, F> Drop for CancelGuard<'a, P, T, F> +where + T: Future, + F: FnOnce(&'a RefCell<&'a mut P>), +{ + fn drop(&mut self) { + // force the borrow to be dropped + self.task = None; + + if let Some(cancel_fn) = self.cancel_fn.take() { + cancel_fn(self.periph); + } + } +}