From abc7fb8d7aba6e67eacbd2c9f92e8c6ec214d8a9 Mon Sep 17 00:00:00 2001 From: Wilfried Chauveau Date: Wed, 31 Jan 2024 05:40:47 +0000 Subject: [PATCH] i2c: implement embedded-hal-async with tests Also adds "-Tdefmt.x" to .cargo/config. This required for i2c async examples & on-target-tests. Note this does not affect downstream dependents. --- .cargo/config | 1 + on-target-tests/Cargo.toml | 11 + on-target-tests/build.rs | 3 - on-target-tests/tests/i2c_loopback_async.rs | 76 ++++ on-target-tests/tests/i2c_tests/mod.rs | 1 + .../tests/i2c_tests/non_blocking.rs | 361 ++++++++++++++++++ rp2040-hal/Cargo.toml | 20 +- rp2040-hal/examples/i2c_async.rs | 126 ++++++ rp2040-hal/examples/i2c_async_cancelled.rs | 150 ++++++++ rp2040-hal/src/async_utils.rs | 136 +++++++ rp2040-hal/src/i2c.rs | 15 +- rp2040-hal/src/i2c/controller.rs | 25 +- rp2040-hal/src/i2c/controller/non_blocking.rs | 294 ++++++++++++++ rp2040-hal/src/i2c/peripheral.rs | 72 +++- rp2040-hal/src/lib.rs | 2 + 15 files changed, 1264 insertions(+), 29 deletions(-) delete mode 100644 on-target-tests/build.rs create mode 100644 on-target-tests/tests/i2c_loopback_async.rs create mode 100644 on-target-tests/tests/i2c_tests/non_blocking.rs create mode 100644 rp2040-hal/examples/i2c_async.rs create mode 100644 rp2040-hal/examples/i2c_async_cancelled.rs create mode 100644 rp2040-hal/src/async_utils.rs create mode 100644 rp2040-hal/src/i2c/controller/non_blocking.rs diff --git a/.cargo/config b/.cargo/config index 1f6ef4aa8..17a9a1e3d 100644 --- a/.cargo/config +++ b/.cargo/config @@ -29,6 +29,7 @@ target = "thumbv6m-none-eabi" rustflags = [ "-C", "link-arg=--nmagic", "-C", "link-arg=-Tlink.x", + "-C", "link-arg=-Tdefmt.x", "-C", "inline-threshold=5", "-C", "no-vectorize-loops", ] diff --git a/on-target-tests/Cargo.toml b/on-target-tests/Cargo.toml index 6a5975e03..48b377b39 100644 --- a/on-target-tests/Cargo.toml +++ b/on-target-tests/Cargo.toml @@ -32,6 +32,10 @@ harness = false name = "i2c_loopback" harness = false +[[test]] +name = "i2c_loopback_async" +harness = false + [dependencies] cortex-m = "0.7" cortex-m-rt = "0.7" @@ -39,6 +43,7 @@ embedded_hal_0_2 = { package = "embedded-hal", version = "0.2.5", features = [ "unproven", ] } embedded-hal = "1.0.0" +embedded-hal-async = "1.0.0" defmt = "0.3" defmt-rtt = "0.4" @@ -59,4 +64,10 @@ heapless = { version = "0.8.0", features = [ "portable-atomic-critical-section", "defmt-03", ] } +# - `wfe`: we may want to signal between cores with sev +# - `wfe` implies `cortex-m` +nostd_async = { version = "0.6.1", features = ["wfe"] } +futures = { version = "0.3.30", default-features = false, features = [ + "async-await", +] } itertools = { version = "0.12.0", default-features = false } diff --git a/on-target-tests/build.rs b/on-target-tests/build.rs deleted file mode 100644 index e0ff2ef0d..000000000 --- a/on-target-tests/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("cargo:rustc-link-arg-tests=-Tdefmt.x"); -} 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 000000000..59f784a03 --- /dev/null +++ b/on-target-tests/tests/i2c_loopback_async.rs @@ -0,0 +1,76 @@ +//! 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 I2C0_IRQ() { + i2c_tests::Controller::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::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 transations_iter(state: &mut State) { + run_test(non_blocking::transaction(state, ADDR_7BIT, 7..=9)); + run_test(non_blocking::transaction(state, ADDR_10BIT, 7..=14)); + } + + // Sad paths: + // invalid tx buf on write + // invalid rx buf on read + // + // invalid (rx/tx) buf in transactions + // + // Peripheral Nack + // + // Arbritration conflict +} diff --git a/on-target-tests/tests/i2c_tests/mod.rs b/on-target-tests/tests/i2c_tests/mod.rs index c3cf0a10c..d186e3a69 100644 --- a/on-target-tests/tests/i2c_tests/mod.rs +++ b/on-target-tests/tests/i2c_tests/mod.rs @@ -11,6 +11,7 @@ use rp2040_hal::{ }; pub mod 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 000000000..db79fbe84 --- /dev/null +++ b/on-target-tests/tests/i2c_tests/non_blocking.rs @@ -0,0 +1,361 @@ +use core::{ + cell::RefCell, + future::Future, + ops::{Deref, RangeInclusive}, + task::Poll, +}; + +use fugit::{HertzU32, RateExtU32}; +use futures::FutureExt; +use heapless::Vec; + +use rp2040_hal::{ + self as hal, + clocks::init_clocks_and_plls, + gpio::{FunctionI2C, Pin, PullUp}, + i2c::{Error, ValidAddress}, + pac, + watchdog::Watchdog, + Clock, +}; + +use super::{Controller, FIFOBuffer, Generator, Target, TargetState}; + +pub struct State { + controller: Option, + target: Option, + resets: hal::pac::RESETS, + ref_clock_freq: HertzU32, + payload: RefCell, +} + +pub fn run_test(f: impl Future) { + let runtime = nostd_async::Runtime::new(); + nostd_async::Task::new(f).spawn(&runtime).join(); +} +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 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<_, FunctionI2C, PullUp> = pins.gpio0.reconfigure(); + let ctrl_scl_pin: Pin<_, FunctionI2C, 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_ctrl = hal::I2C::new_controller( + pac.I2C0, + ctrl_sda_pin, + ctrl_scl_pin, + 400.kHz(), + &mut pac.RESETS, + clocks.system_clock.freq(), + ); + let i2c_target = hal::I2C::new_peripheral_event_iterator( + pac.I2C1, + trg_sda_pin, + trg_scl_pin, + &mut pac.RESETS, + addr, + ); + + unsafe { + pac::NVIC::unpend(hal::pac::Interrupt::I2C0_IRQ); + pac::NVIC::unmask(hal::pac::Interrupt::I2C0_IRQ); + pac::NVIC::unpend(hal::pac::Interrupt::I2C1_IRQ); + pac::NVIC::unmask(hal::pac::Interrupt::I2C1_IRQ); + } + + State { + controller: Some(i2c_ctrl), + target: Some(i2c_target), + resets: pac.RESETS, + ref_clock_freq: clocks.system_clock.freq(), + payload: RefCell::new(TargetState::new()), + } +} + +pub fn reset(state: &mut State, addr: T) { + // reset controller + let (i2c, (sda, scl)) = state + .controller + .take() + .expect("controller's missing.") + .free(&mut state.resets); + + 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, + )); + + state.controller = Some(hal::I2C::new_controller( + i2c, + sda, + scl, + 400.kHz(), + &mut state.resets, + state.ref_clock_freq, + )); +} + +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 + reset(state, addr); + + // Test + let mut v = Default::default(); + let ctrl = embedded_hal_case( + state.controller.as_mut().expect("controller's missing."), + addr, + &mut v, + &state.payload, + ); + let trgt = target_handler( + &state.payload, + state.target.as_mut().take().expect("target’s missing"), + ); + 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 = state.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!(state.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); +} + +pub async fn transaction( + state: &mut State, + addr: A, + restart_count: RangeInclusive, +) { + use embedded_hal::i2c::Operation; + use embedded_hal_async::i2c::I2c; + reset(state, addr); + + // 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 { + state + .controller + .as_mut() + .expect("controller's missing.") + .transaction(addr, &mut ops) + .await + .expect("Controller test success"); + wait_with(&state.payload, |p| p.stop_cnt != 1).await; + }; + futures::select_biased! { + _ = case.fuse() => {} + _ = target_handler( + &state.payload, + state.target.as_mut().take().expect("target’s missing"), + ).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 = state.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, state.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); +} diff --git a/rp2040-hal/Cargo.toml b/rp2040-hal/Cargo.toml index bb56da4c9..bcd075b0c 100644 --- a/rp2040-hal/Cargo.toml +++ b/rp2040-hal/Cargo.toml @@ -17,9 +17,12 @@ targets = ["thumbv6m-none-eabi"] [dependencies] cortex-m = "0.7.2" -embedded_hal_0_2 = { package = "embedded-hal", version = "0.2.5", features = ["unproven"] } +embedded_hal_0_2 = { package = "embedded-hal", version = "0.2.5", features = [ + "unproven", +] } embedded-hal = "1.0.0" embedded-hal-nb = "1.0.0" +embedded-hal-async = "1.0.0" embedded-dma = "0.2.0" embedded-io = "0.6.1" fugit = "0.3.6" @@ -54,6 +57,13 @@ hd44780-driver = "0.4.0" pio-proc = "0.2.0" dht-sensor = "0.2.1" rand = { version = "0.8.5", default-features = false } +nostd_async = { version = "0.6.1", features = ["cortex_m"] } +futures = { version = "0.3.30", default-features = false, features = [ + "async-await", +] } +defmt-rtt = "0.4.0" +panic-probe = { version = "0.3.1", features = ["print-defmt"] } +defmt = "0.3" [features] # Minimal startup / runtime for Cortex-M microcontrollers @@ -147,6 +157,14 @@ required-features = ["critical-section-impl"] name = "i2c" required-features = ["critical-section-impl"] +[[example]] +name = "i2c_async" +required-features = ["critical-section-impl", "rt"] + +[[example]] +name = "i2c_async_cancelled" +required-features = ["critical-section-impl", "rt", "defmt"] + [[example]] name = "lcd_display" required-features = ["critical-section-impl"] diff --git a/rp2040-hal/examples/i2c_async.rs b/rp2040-hal/examples/i2c_async.rs new file mode 100644 index 000000000..0e5f68d63 --- /dev/null +++ b/rp2040-hal/examples/i2c_async.rs @@ -0,0 +1,126 @@ +//! # I²C Example +//! +//! This application demonstrates how to talk to I²C devices with an RP2040. +//! in an Async environment. +//! +//! It may need to be adapted to your particular board layout and/or pin assignment. +//! +//! See the `Cargo.toml` file for Copyright and license details. + +#![no_std] +#![no_main] + +// Ensure we halt the program on panic (if we don't mention this crate it won't +// be linked) +use panic_halt as _; + +// Alias for our HAL crate +use rp2040_hal as hal; + +// Some traits we need +use hal::{ + fugit::RateExtU32, + gpio::bank0::{Gpio20, Gpio21}, + i2c::Controller, + I2C, +}; + +// Import required types & traits. +use embedded_hal_async::i2c::I2c; +use hal::{ + gpio::{FunctionI2C, Pin, PullUp}, + pac::{self, interrupt}, + Clock, +}; + +/// 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; + +/// Bind the interrupt handler with the peripheral +#[interrupt] +unsafe fn I2C0_IRQ() { + use hal::async_utils::AsyncPeripheral; + I2C::::on_interrupt(); +} + +/// The function configures the RP2040 peripherals, then performs a single I²C +/// write to a fixed address. +async fn demo() { + let mut pac = pac::Peripherals::take().unwrap(); + + // Set up the watchdog driver - needed by the clock setup code + let mut watchdog = hal::Watchdog::new(pac.WATCHDOG); + + // Configure the clocks + let clocks = hal::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); + + // Set the pins to their default state + 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 sda_pin: Pin<_, FunctionI2C, PullUp> = pins.gpio20.reconfigure(); + let scl_pin: Pin<_, FunctionI2C, PullUp> = pins.gpio21.reconfigure(); + + // Create the I²C drive, using the two pre-configured pins. This will fail + // at compile time if the pins are in the wrong mode, or if this I²C + // peripheral isn't available on these pins! + let mut i2c = hal::I2C::new_controller( + pac.I2C0, + sda_pin, + scl_pin, + 400.kHz(), + &mut pac.RESETS, + clocks.system_clock.freq(), + ); + + // Unmask the interrupt in the NVIC to let the core wake up & enter the interrupt handler. + // Each core has its own NVIC so these needs to executed from the core where the IRQ are + // expected. + unsafe { + pac::NVIC::unpend(hal::pac::Interrupt::I2C0_IRQ); + pac::NVIC::unmask(hal::pac::Interrupt::I2C0_IRQ); + } + + // Asynchronously write three bytes to the I²C device with 7-bit address 0x2C + i2c.write(0x76u8, &[1, 2, 3]).await.unwrap(); + + // Demo finish - just loop until reset + core::future::pending().await +} + +/// Entry point to our bare-metal application. +#[rp2040_hal::entry] +fn main() -> ! { + let runtime = nostd_async::Runtime::new(); + let mut task = nostd_async::Task::new(demo()); + let handle = task.spawn(&runtime); + handle.join(); + unreachable!() +} diff --git a/rp2040-hal/examples/i2c_async_cancelled.rs b/rp2040-hal/examples/i2c_async_cancelled.rs new file mode 100644 index 000000000..1ac756ce4 --- /dev/null +++ b/rp2040-hal/examples/i2c_async_cancelled.rs @@ -0,0 +1,150 @@ +//! # I²C Example +//! +//! This application demonstrates how to talk to I²C devices with an RP2040. +//! in an Async environment. +//! +//! It may need to be adapted to your particular board layout and/or pin assignment. +//! +//! See the `Cargo.toml` file for Copyright and license details. + +#![no_std] +#![no_main] + +use core::task::Poll; + +// Ensure we halt the program on panic (if we don't mention this crate it won't +// be linked) +//use panic_halt as _; + +use embedded_hal_async::i2c::I2c; +use futures::FutureExt; + +// Alias for our HAL crate +use rp2040_hal as hal; + +// Some traits we need +use hal::{ + fugit::RateExtU32, + gpio::bank0::{Gpio20, Gpio21}, + i2c::Controller, + pac::interrupt, + I2C, +}; + +// Import required types & traits. +use hal::{ + gpio::{FunctionI2C, Pin, PullUp}, + pac, Clock, +}; + +use defmt_rtt as _; +use panic_probe as _; + +/// 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; + +#[interrupt] +unsafe fn I2C0_IRQ() { + use hal::async_utils::AsyncPeripheral; + I2C::::on_interrupt(); +} + +/// The function configures the RP2040 peripherals, then performs a single I²C +/// write to a fixed address. +async fn demo() { + let mut pac = pac::Peripherals::take().unwrap(); + + // Set up the watchdog driver - needed by the clock setup code + let mut watchdog = hal::Watchdog::new(pac.WATCHDOG); + + // Configure the clocks + let clocks = hal::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); + + // Set the pins to their default state + 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 sda_pin: Pin<_, FunctionI2C, PullUp> = pins.gpio20.reconfigure(); + let scl_pin: Pin<_, FunctionI2C, PullUp> = pins.gpio21.reconfigure(); + + // Create the I²C drive, using the two pre-configured pins. This will fail + // at compile time if the pins are in the wrong mode, or if this I²C + // peripheral isn't available on these pins! + let mut i2c = hal::I2C::new_controller( + pac.I2C0, + sda_pin, + scl_pin, + 400.kHz(), + &mut pac.RESETS, + clocks.system_clock.freq(), + ); + + // Unmask the interrupt in the NVIC to let the core wake up & enter the interrupt handler. + unsafe { + pac::NVIC::unpend(hal::pac::Interrupt::I2C0_IRQ); + pac::NVIC::unmask(hal::pac::Interrupt::I2C0_IRQ); + } + + let mut cnt = 0; + let timeout = core::future::poll_fn(|cx| { + cx.waker().wake_by_ref(); + if cnt == 1 { + Poll::Ready(()) + } else { + cnt += 1; + Poll::Pending + } + }); + + let mut v = [0; 32]; + v.iter_mut().enumerate().for_each(|(i, v)| *v = i as u8); + + // Asynchronously write three bytes to the I²C device with 7-bit address 0x2C + futures::select_biased! { + r = i2c.write(0x76u8, &v).fuse() => r.unwrap(), + _ = timeout.fuse() => { + defmt::info!("Timed out."); + } + } + i2c.write(0x76u8, &v).await.unwrap(); + + // Demo finish - just loop until reset + core::future::pending().await +} + +/// Entry point to our bare-metal application. +#[rp2040_hal::entry] +fn main() -> ! { + let runtime = nostd_async::Runtime::new(); + let mut task = nostd_async::Task::new(demo()); + let handle = task.spawn(&runtime); + handle.join(); + unreachable!() +} diff --git a/rp2040-hal/src/async_utils.rs b/rp2040-hal/src/async_utils.rs new file mode 100644 index 000000000..54421ed2c --- /dev/null +++ b/rp2040-hal/src/async_utils.rs @@ -0,0 +1,136 @@ +//! Commonly used in async implementations. + +use core::{marker::PhantomData, task::Poll}; + +pub(crate) mod sealed { + use core::{cell::Cell, task::Waker}; + use critical_section::Mutex; + + pub trait Wakeable { + /// Returns the waker associated with driver instance. + fn waker() -> &'static IrqWaker; + } + + /// This type wraps a `Waker` in a `Mutex>`. + /// + /// While `critical_section::Mutex` intregrates nicely with RefCell, RefCell adds a borrow + /// counter that is not necessary for this usecase. + /// + /// This type is kept sealed to prevent user from mistakenly messing with the waker such as + /// clearing it while the driver is parked. + pub struct IrqWaker { + waker: Mutex>>, + } + impl IrqWaker { + pub const fn new() -> Self { + Self { + waker: Mutex::new(Cell::new(None)), + } + } + pub fn wake(&self) { + critical_section::with(|cs| { + if let Some(waker) = self.waker.borrow(cs).take() { + Waker::wake(waker); + } + }); + } + pub fn register(&self, waker: &Waker) { + critical_section::with(|cs| { + self.waker.borrow(cs).replace(Some(waker.clone())); + }); + } + pub fn clear(&self) { + critical_section::with(|cs| { + self.waker.borrow(cs).take(); + }); + } + } +} + +/// Marks driver instances that can be bound to an interrupt to wake async tasks. +pub trait AsyncPeripheral: sealed::Wakeable { + /// Signals the driver of an interrupt. + fn on_interrupt(); +} + +#[must_use = "Future do nothing unless they are polled on."] +pub(crate) struct CancellablePollFn<'periph, Periph, PFn, EnIrqFn, CancelFn, OutputTy> +where + Periph: sealed::Wakeable, + CancelFn: FnMut(&mut Periph), +{ + periph: &'periph mut Periph, + poll: PFn, + enable_irq: EnIrqFn, + cancel: CancelFn, + done: bool, + // captures F's return type. + phantom: PhantomData, +} +impl<'p, Periph, PFn, EnIrqFn, CancelFn, OutputTy> + CancellablePollFn<'p, Periph, PFn, EnIrqFn, CancelFn, OutputTy> +where + Periph: sealed::Wakeable, + PFn: FnMut(&mut Periph) -> Poll, + EnIrqFn: FnMut(&mut Periph), + CancelFn: FnMut(&mut Periph), +{ + pub(crate) fn new( + periph: &'p mut Periph, + poll: PFn, + enable_irq: EnIrqFn, + cancel: CancelFn, + ) -> Self { + Self { + periph, + poll, + enable_irq, + cancel, + done: false, + phantom: PhantomData, + } + } +} + +impl core::future::Future + for CancellablePollFn<'_, Periph, PFn, EnIrqFn, CancelFn, OutputTy> +where + Periph: sealed::Wakeable, + PFn: FnMut(&mut Periph) -> Poll, + EnIrqFn: FnMut(&mut Periph), + CancelFn: FnMut(&mut Periph), +{ + type Output = OutputTy; + + fn poll(self: core::pin::Pin<&mut Self>, cx: &mut core::task::Context<'_>) -> Poll { + // SAFETY: We are not moving anything. + let Self { + ref mut periph, + poll: ref mut is_ready, + enable_irq: ref mut setup_flags, + ref mut done, + .. + } = unsafe { self.get_unchecked_mut() }; + let r = (is_ready)(periph); + if r.is_pending() { + Periph::waker().register(cx.waker()); + (setup_flags)(periph); + } else { + *done = true; + } + r + } +} +impl<'periph, Periph, PFn, EnIrqFn, CancelFn, OutputTy> Drop + for CancellablePollFn<'periph, Periph, PFn, EnIrqFn, CancelFn, OutputTy> +where + Periph: sealed::Wakeable, + CancelFn: FnMut(&mut Periph), +{ + fn drop(&mut self) { + if !self.done { + Periph::waker().clear(); + (self.cancel)(self.periph); + } + } +} diff --git a/rp2040-hal/src/i2c.rs b/rp2040-hal/src/i2c.rs index cb225db56..a68c44dd6 100644 --- a/rp2040-hal/src/i2c.rs +++ b/rp2040-hal/src/i2c.rs @@ -42,6 +42,12 @@ //! //! See [examples/i2c.rs](https://github.com/rp-rs/rp-hal/tree/main/rp2040-hal/examples/i2c.rs) //! for a complete example +//! +//! ## Async Usage +//! +//! See [examples/i2c_async.rs](https://github.com/rp-rs/rp-hal/tree/main/rp2040-hal/examples/i2c_async.rs) +//! and [examples/i2c_async_irq.rs](https://github.com/rp-rs/rp-hal/tree/main/rp2040-hal/examples/i2c_async_irq.rs) +//! for complete examples. use core::{marker::PhantomData, ops::Deref}; use fugit::HertzU32; @@ -287,7 +293,6 @@ pub struct I2C { mode: Mode, } -impl Sealed for I2C {} impl I2C where Block: SubsystemReset + Deref, @@ -388,6 +393,14 @@ macro_rules! hal { } } } + + impl $crate::async_utils::sealed::Wakeable for I2C<$I2CX, P, M> { + fn waker() -> &'static $crate::async_utils::sealed::IrqWaker { + static WAKER: $crate::async_utils::sealed::IrqWaker = + $crate::async_utils::sealed::IrqWaker::new(); + &WAKER + } + } )+ } } diff --git a/rp2040-hal/src/i2c/controller.rs b/rp2040-hal/src/i2c/controller.rs index 04465cab3..5e0afb53f 100644 --- a/rp2040-hal/src/i2c/controller.rs +++ b/rp2040-hal/src/i2c/controller.rs @@ -10,6 +10,8 @@ use crate::{ resets::SubsystemReset, }; +pub(crate) mod non_blocking; + impl I2C where T: SubsystemReset + Deref, @@ -384,29 +386,6 @@ impl, PINS> I2C { } Ok(()) } - - #[cfg(feature = "i2c-write-iter")] - fn transaction_iter<'op, A, O, B>(&mut self, address: A, operations: O) -> Result<(), Error> - where - A: ValidAddress, - O: IntoIterator>, - B: IntoIterator, - { - use i2c_write_iter::Operation; - self.setup(address)?; - - let mut first = true; - let mut operations = operations.into_iter().peekable(); - while let Some(operation) = operations.next() { - let last = operations.peek().is_none(); - match operation { - Operation::Read(buf) => self.read_internal(first, buf, last)?, - Operation::WriteIter(buf) => self.write_internal(first, buf, last)?, - } - first = false; - } - Ok(()) - } } impl, PINS> Read for I2C { diff --git a/rp2040-hal/src/i2c/controller/non_blocking.rs b/rp2040-hal/src/i2c/controller/non_blocking.rs new file mode 100644 index 000000000..8edf5ef9f --- /dev/null +++ b/rp2040-hal/src/i2c/controller/non_blocking.rs @@ -0,0 +1,294 @@ +use core::{ops::Deref, task::Poll}; +use embedded_hal_async::i2c::{AddressMode, Operation}; + +use crate::{ + async_utils::{sealed::Wakeable, AsyncPeripheral, CancellablePollFn as CPFn}, + i2c::{Controller, Error, ValidAddress, I2C}, + pac::i2c0::RegisterBlock, +}; + +macro_rules! impl_async_traits { + ($i2c:path) => { + impl

AsyncPeripheral for I2C<$i2c, P, Controller> + where + Self: $crate::async_utils::sealed::Wakeable, + { + fn on_interrupt() { + unsafe { + // This is equivalent to stealing from pac::Peripherals + let i2c = &*<$i2c>::ptr(); + + // Mask all interrupt flags. This does not clear the flags. + // Clearing is done by the driver after it wakes up. + i2c.ic_intr_mask.write_with_zero(|w| w); + } + // interrupts are now masked, we can wake the task and return from this handler. + Self::waker().wake(); + } + } + }; +} + +impl_async_traits!(rp2040_pac::I2C0); +impl_async_traits!(rp2040_pac::I2C1); + +enum TxEmptyConfig { + Empty, + NotFull, +} + +impl I2C +where + T: Deref, + Self: AsyncPeripheral, +{ + #[inline] + fn unmask_intr(&mut self) { + unsafe { + self.i2c.ic_intr_mask.write_with_zero(|w| { + w.m_tx_empty() + .disabled() + .m_rx_full() + .disabled() + .m_tx_abrt() + .disabled() + .m_stop_det() + .disabled() + }); + } + } + #[inline] + fn configure_tx_empty(&mut self, cfg: TxEmptyConfig) { + self.i2c + .ic_tx_tl + // SAFETY: we are within [0; TX_FIFO_DEPTH) + .write(|w| unsafe { + w.tx_tl().bits(match cfg { + TxEmptyConfig::Empty => 1, + TxEmptyConfig::NotFull => Self::TX_FIFO_DEPTH - 1, + }) + }); + } + + #[inline] + fn unmask_tx_empty(&mut self) { + self.configure_tx_empty(TxEmptyConfig::Empty); + self.unmask_intr() + } + + #[inline] + fn unmask_tx_not_full(&mut self) { + self.configure_tx_empty(TxEmptyConfig::NotFull); + self.unmask_intr() + } + + #[inline] + fn unmask_stop_det(&mut self) { + self.unmask_intr(); + } + + #[inline] + fn poll_rx_not_empty_or_abrt(&mut self) -> Poll> { + self.read_and_clear_abort_reason()?; + if self.i2c.ic_raw_intr_stat.read().rx_full().bit_is_set() { + Poll::Ready(Ok(())) + } else { + Poll::Pending + } + } + + #[inline] + fn cancel(&mut self) { + unsafe { + self.i2c.ic_intr_mask.write_with_zero(|w| w); + } + + self.abort(); + } + + async fn non_blocking_read_internal( + &mut self, + first_transaction: bool, + buffer: &mut [u8], + do_stop: bool, + ) -> Result<(), Error> { + self.validate_buffer( + first_transaction, + &mut buffer.iter().peekable(), + Error::InvalidReadBufferLength, + )?; + + let lastindex = buffer.len() - 1; + let mut first_byte = true; + for (i, byte) in buffer.iter_mut().enumerate() { + let last_byte = i == lastindex; + + // wait until there is space in the FIFO to write the next byte + // cannot abort during read, so ignore the result + let _ = CPFn::new( + self, + Self::poll_tx_not_full, + Self::unmask_tx_not_full, + Self::cancel, + ) + .await; + + self.i2c.ic_data_cmd.write(|w| { + if first_byte { + if !first_transaction { + w.restart().enable(); + } + first_byte = false; + } + + w.stop().bit(do_stop && last_byte); + w.cmd().read() + }); + + CPFn::new( + self, + Self::poll_rx_not_empty_or_abrt, + Self::unmask_tx_empty, + Self::cancel, + ) + .await?; + + *byte = self.i2c.ic_data_cmd.read().dat().bits(); + } + + Ok(()) + } + + async fn non_blocking_write_internal( + &mut self, + first_transaction: bool, + bytes: impl IntoIterator, + do_stop: bool, + ) -> Result<(), Error> { + let mut peekable = bytes.into_iter().peekable(); + self.validate_buffer( + first_transaction, + &mut peekable, + Error::InvalidWriteBufferLength, + )?; + + let mut abort_reason = Ok(()); + let mut first_byte = true; + while let Some(byte) = peekable.next() { + if self.tx_fifo_full() { + // wait for more room in the fifo + abort_reason = CPFn::new( + self, + Self::poll_tx_not_full, + Self::unmask_tx_not_full, + Self::cancel, + ) + .await; + if abort_reason.is_err() { + break; + } + } + + // else enqueue + let last = peekable.peek().is_none(); + self.i2c.ic_data_cmd.write(|w| { + if first_byte { + if !first_transaction { + w.restart().enable(); + } + first_byte = false; + } + w.stop().bit(do_stop && last); + unsafe { w.dat().bits(byte) } + }); + } + + if abort_reason.is_err() { + // Wait until the transmission of the address/data from the internal + // shift register has completed. + CPFn::new( + self, + Self::poll_tx_empty, + Self::unmask_tx_empty, + Self::cancel, + ) + .await; + abort_reason = self.read_and_clear_abort_reason(); + } + + if abort_reason.is_err() || do_stop { + // If the transaction was aborted or if it completed + // successfully wait until the STOP condition has occured. + CPFn::new( + self, + Self::poll_stop_deteced, + Self::unmask_stop_det, + Self::cancel, + ) + .await; + self.i2c.ic_clr_stop_det.read().clr_stop_det(); + } + // Note: the hardware issues a STOP automatically on an abort condition. + // Note: the hardware also clears RX FIFO as well as TX on abort + + abort_reason + } + + /// Writes to the i2c bus consuming bytes for the given iterator. + pub async fn write_iter_async(&mut self, address: A, bytes: U) -> Result<(), super::Error> + where + U: IntoIterator, + A: ValidAddress, + { + self.setup(address)?; + self.non_blocking_write_internal(true, bytes, true).await + } + + /// Writes to the i2c bus consuming bytes for the given iterator. + pub async fn write_iter_read_async( + &mut self, + address: A, + bytes: U, + read: &mut [u8], + ) -> Result<(), Error> + where + U: IntoIterator, + A: ValidAddress, + { + self.setup(address)?; + self.non_blocking_write_internal(true, bytes, false).await?; + self.non_blocking_read_internal(false, read, true).await + } +} + +impl embedded_hal_async::i2c::I2c for I2C +where + Self: AsyncPeripheral, + A: ValidAddress + AddressMode, + T: Deref, +{ + async fn transaction( + &mut self, + addr: A, + operations: &mut [Operation<'_>], + ) -> Result<(), Error> { + self.setup(addr)?; + + let mut first = true; + let mut operations = operations.iter_mut().peekable(); + while let Some(op) = operations.next() { + let last = operations.peek().is_none(); + match op { + Operation::Read(buffer) => { + self.non_blocking_read_internal(first, buffer, last).await?; + } + Operation::Write(buffer) => { + self.non_blocking_write_internal(first, buffer.iter().cloned(), last) + .await?; + } + } + first = false; + } + Ok(()) + } +} diff --git a/rp2040-hal/src/i2c/peripheral.rs b/rp2040-hal/src/i2c/peripheral.rs index 2933b0bea..2b978e267 100644 --- a/rp2040-hal/src/i2c/peripheral.rs +++ b/rp2040-hal/src/i2c/peripheral.rs @@ -39,12 +39,13 @@ //! Depending on the firmware's and bus' speed, this driver may only report: //! - `Start, Write, Restart, Read, Stop` -use core::ops::Deref; +use core::{ops::Deref, task::Poll}; use embedded_hal::i2c::AddressMode; use super::{Peripheral, ValidAddress, ValidPinScl, ValidPinSda, I2C}; use crate::{ + async_utils::{sealed::Wakeable, AsyncPeripheral, CancellablePollFn}, pac::{i2c0::RegisterBlock, RESETS}, resets::SubsystemReset, }; @@ -153,10 +154,20 @@ fn unmask_intr(i2c: &RegisterBlock) { } } +/// SAFETY: Takes a non-mutable reference to RegisterBlock but mutates its `ic_intr_mask` register. +unsafe fn mask_intr(i2c: &RegisterBlock) { + // 0 is a valid value and means all flag masked. + unsafe { i2c.ic_intr_mask.write_with_zero(|w| w) } +} + impl, PINS> I2C { fn unmask_intr(&mut self) { unmask_intr(&self.i2c) } + fn mask_intr(&mut self) { + // SAFETY: We are the only owner of this register block. + unsafe { mask_intr(&self.i2c) } + } /// Push up to `usize::min(TX_FIFO_SIZE, buf.len())` bytes to the TX FIFO. /// Returns the number of bytes pushed to the FIFO. Note this does *not* reflect how many bytes @@ -248,3 +259,62 @@ impl, PINS> I2C { } } } + +macro_rules! impl_wakeable { + ($i2c:ty) => { + impl AsyncPeripheral for I2C<$i2c, PINS, Peripheral> + where + I2C<$i2c, PINS, Peripheral>: $crate::async_utils::sealed::Wakeable, + { + /// Wakes an async task (if any) & masks irqs + fn on_interrupt() { + unsafe { + // This is equivalent to stealing from pac::Peripherals + let i2c = &*<$i2c>::ptr(); + + mask_intr(i2c); + } + + // interrupts are now masked, we can wake the task and return from this handler. + Self::waker().wake(); + } + } + }; +} +impl_wakeable!(rp2040_pac::I2C0); +impl_wakeable!(rp2040_pac::I2C1); + +impl I2C +where + I2C: AsyncPeripheral, + T: Deref, +{ + /// Asynchronously waits for an Event. + pub async fn wait_next(&mut self) -> Event { + loop { + if let Some(evt) = self.next_event() { + return evt; + } + + CancellablePollFn::new( + self, + |me| { + let stat = me.i2c.ic_raw_intr_stat.read(); + if stat.start_det().bit_is_set() + || stat.restart_det().bit_is_set() + || stat.stop_det().bit_is_set() + || stat.rd_req().bit_is_set() + || stat.rx_full().bit_is_set() + { + Poll::Ready(()) + } else { + Poll::Pending + } + }, + Self::unmask_intr, + Self::mask_intr, + ) + .await; + } + } +} diff --git a/rp2040-hal/src/lib.rs b/rp2040-hal/src/lib.rs index 0deafdaed..0675485cd 100644 --- a/rp2040-hal/src/lib.rs +++ b/rp2040-hal/src/lib.rs @@ -47,6 +47,8 @@ pub use rp2040_pac as pac; mod intrinsics; pub mod adc; +#[macro_use] +pub mod async_utils; pub(crate) mod atomic_register_access; pub mod clocks; #[cfg(feature = "critical-section-impl")]