diff --git a/CHANGELOG.md b/CHANGELOG.md index 768a1e82c..62980e7a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Implementation OutputPin and InputPin for AnyPin (#1067) - Implement `estimate_xtal_frequency` for ESP32-C6 / ESP32-H2 (#1174) - A way to push into I2S DMA buffer via a closure (#1189) +- Added basic `LP-I2C` driver for C6 (#1185) ### Fixed diff --git a/esp-hal-procmacros/src/lib.rs b/esp-hal-procmacros/src/lib.rs index 5b8616ca8..f913c8e12 100644 --- a/esp-hal-procmacros/src/lib.rs +++ b/esp-hal-procmacros/src/lib.rs @@ -618,6 +618,7 @@ pub fn load_lp_code(input: TokenStream) -> TokenStream { use #hal_crate::gpio::lp_gpio::LowPowerPin; use #hal_crate::gpio::*; use #hal_crate::uart::lp_uart::LpUart; + use #hal_crate::i2c::lp_i2c::LpI2c; }; #[cfg(any(feature = "esp32s2", feature = "esp32s3"))] let imports = quote! { @@ -727,7 +728,12 @@ pub fn entry(args: TokenStream, input: TokenStream) -> TokenStream { } "LpUart" => { create_peripheral.push(quote!( - let mut #param_name = unsafe { the_hal::uart::conjure().unwrap() }; + let mut #param_name = unsafe { the_hal::uart::conjure() }; + )); + } + "LpI2c" => { + create_peripheral.push(quote!( + let mut #param_name = unsafe { the_hal::i2c::conjure() }; )); } _ => { diff --git a/esp-hal/Cargo.toml b/esp-hal/Cargo.toml index c2aa51abe..ae87d8fde 100644 --- a/esp-hal/Cargo.toml +++ b/esp-hal/Cargo.toml @@ -61,14 +61,14 @@ ufmt-write = { version = "0.1.0", optional = true } # IMPORTANT: # Each supported device MUST have its PAC included below along with a # corresponding feature. -esp32 = { git = "https://github.com/esp-rs/esp-pacs", rev = "8231fea", features = ["critical-section"], optional = true } -esp32c2 = { git = "https://github.com/esp-rs/esp-pacs", rev = "8231fea", features = ["critical-section"], optional = true } -esp32c3 = { git = "https://github.com/esp-rs/esp-pacs", rev = "8231fea", features = ["critical-section"], optional = true } -esp32c6 = { git = "https://github.com/esp-rs/esp-pacs", rev = "8231fea", features = ["critical-section"], optional = true } -esp32h2 = { git = "https://github.com/esp-rs/esp-pacs", rev = "8231fea", features = ["critical-section"], optional = true } -esp32p4 = { git = "https://github.com/esp-rs/esp-pacs", rev = "8231fea", features = ["critical-section"], optional = true } -esp32s2 = { git = "https://github.com/esp-rs/esp-pacs", rev = "8231fea", features = ["critical-section"], optional = true } -esp32s3 = { git = "https://github.com/esp-rs/esp-pacs", rev = "8231fea", features = ["critical-section"], optional = true } +esp32 = { git = "https://github.com/esp-rs/esp-pacs", rev = "ef39b1c", features = ["critical-section"], optional = true } +esp32c2 = { git = "https://github.com/esp-rs/esp-pacs", rev = "ef39b1c", features = ["critical-section"], optional = true } +esp32c3 = { git = "https://github.com/esp-rs/esp-pacs", rev = "ef39b1c", features = ["critical-section"], optional = true } +esp32c6 = { git = "https://github.com/esp-rs/esp-pacs", rev = "ef39b1c", features = ["critical-section"], optional = true } +esp32h2 = { git = "https://github.com/esp-rs/esp-pacs", rev = "ef39b1c", features = ["critical-section"], optional = true } +esp32p4 = { git = "https://github.com/esp-rs/esp-pacs", rev = "ef39b1c", features = ["critical-section"], optional = true } +esp32s2 = { git = "https://github.com/esp-rs/esp-pacs", rev = "ef39b1c", features = ["critical-section"], optional = true } +esp32s3 = { git = "https://github.com/esp-rs/esp-pacs", rev = "ef39b1c", features = ["critical-section"], optional = true } [build-dependencies] basic-toml = "0.1.8" diff --git a/esp-hal/src/gpio.rs b/esp-hal/src/gpio.rs index 81c139053..2a20f69a2 100644 --- a/esp-hal/src/gpio.rs +++ b/esp-hal/src/gpio.rs @@ -2616,6 +2616,8 @@ pub mod rtc_io { use core::marker::PhantomData; + #[cfg(esp32c6)] + use super::OpenDrain; use super::{Floating, Input, Output, PullDown, PullUp, PushPull, Unknown}; /// A GPIO pin configured for low power operation @@ -2735,6 +2737,23 @@ pub mod rtc_io { private: PhantomData, } } + + #[cfg(esp32c6)] + /// Configures the pin as an pullup input and a push pull output pin. + pub fn into_open_drain_output(self) -> LowPowerPin { + self.into_pull_up_input(); + self.into_push_pull_output(); + use crate::peripherals::GPIO; + + let gpio = unsafe { &*GPIO::PTR }; + + gpio.pin(PIN).modify(|_, w| w.pad_driver().bit(true)); + self.pulldown_enable(false); + + LowPowerPin { + private: PhantomData, + } + } } #[cfg(esp32s3)] @@ -2787,6 +2806,8 @@ pub mod lp_gpio { use core::marker::PhantomData; + #[cfg(esp32c6)] + use super::OpenDrain; use super::{Floating, Input, Output, PullDown, PullUp, PushPull, Unknown}; /// A GPIO pin configured for low power operation @@ -2880,6 +2901,21 @@ pub mod lp_gpio { private: PhantomData, } } + + pub fn into_open_drain_output(self) -> LowPowerPin { + use crate::peripherals::GPIO; + + let gpio = unsafe { &*GPIO::PTR }; + + gpio.pin(PIN as usize) + .modify(|_, w| w.pad_driver().bit(true)); + self.pulldown_enable(false); + self.into_pull_up_input().into_push_pull_output(); + + LowPowerPin { + private: PhantomData, + } + } } pub(crate) fn init_low_power_pin(pin: u8) { diff --git a/esp-hal/src/i2c.rs b/esp-hal/src/i2c.rs index f0526656f..6690c4476 100644 --- a/esp-hal/src/i2c.rs +++ b/esp-hal/src/i2c.rs @@ -1649,3 +1649,470 @@ impl Instance for crate::peripherals::I2C1 { 1 } } + +#[cfg(lp_i2c0)] +pub mod lp_i2c { + //! Low-power I2C driver + + use fugit::HertzU32; + + use crate::{ + gpio::{lp_gpio::LowPowerPin, OpenDrain}, + peripherals::{LP_CLKRST, LP_I2C0}, + }; + + const LP_I2C_FILTER_CYC_NUM_DEF: u8 = 7; + + /// I2C-specific transmission errors + #[derive(Debug, Clone, Copy, PartialEq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub enum Error { + ExceedingFifo, + AckCheckFailed, + TimeOut, + ArbitrationLost, + ExecIncomplete, + CommandNrExceeded, + InvalidResponse, + } + + #[allow(unused)] + enum OperationType { + Write = 0, + Read = 1, + } + + #[allow(unused)] + #[derive(Eq, PartialEq, Copy, Clone)] + enum Ack { + Ack, + Nack, + } + + #[allow(unused)] + enum Opcode { + RStart = 6, + Write = 1, + Read = 3, + Stop = 2, + End = 4, + } + + #[derive(PartialEq)] + #[allow(unused)] + enum Command { + Start, + Stop, + End, + Write { + /// This bit is to set an expected ACK value for the transmitter. + ack_exp: Ack, + /// Enables checking the ACK value received against the ack_exp + /// value. + ack_check_en: bool, + /// Length of data (in bytes) to be written. The maximum length is + /// 255, while the minimum is 1. + length: u8, + }, + Read { + /// Indicates whether the receiver will send an ACK after this byte + /// has been received. + ack_value: Ack, + /// Length of data (in bytes) to be read. The maximum length is 255, + /// while the minimum is 1. + length: u8, + }, + } + + impl From for u16 { + fn from(c: Command) -> u16 { + let opcode = match c { + Command::Start => Opcode::RStart, + Command::Stop => Opcode::Stop, + Command::End => Opcode::End, + Command::Write { .. } => Opcode::Write, + Command::Read { .. } => Opcode::Read, + }; + + let length = match c { + Command::Start | Command::Stop | Command::End => 0, + Command::Write { length: l, .. } | Command::Read { length: l, .. } => l, + }; + + let ack_exp = match c { + Command::Start | Command::Stop | Command::End | Command::Read { .. } => Ack::Nack, + Command::Write { ack_exp: exp, .. } => exp, + }; + + let ack_check_en = match c { + Command::Start | Command::Stop | Command::End | Command::Read { .. } => false, + Command::Write { + ack_check_en: en, .. + } => en, + }; + + let ack_value = match c { + Command::Start | Command::Stop | Command::End | Command::Write { .. } => Ack::Nack, + Command::Read { ack_value: ack, .. } => ack, + }; + + let mut cmd: u16 = length.into(); + + if ack_check_en { + cmd |= 1 << 8; + } else { + cmd &= !(1 << 8); + } + + if ack_exp == Ack::Nack { + cmd |= 1 << 9; + } else { + cmd &= !(1 << 9); + } + + if ack_value == Ack::Nack { + cmd |= 1 << 10; + } else { + cmd &= !(1 << 10); + } + + cmd |= (opcode as u16) << 11; + + cmd + } + } + + impl From for u32 { + fn from(c: Command) -> u32 { + let opcode = match c { + Command::Start => Opcode::RStart, + Command::Stop => Opcode::Stop, + Command::End => Opcode::End, + Command::Write { .. } => Opcode::Write, + Command::Read { .. } => Opcode::Read, + }; + + let length = match c { + Command::Start | Command::Stop | Command::End => 0, + Command::Write { length: l, .. } | Command::Read { length: l, .. } => l, + }; + + let ack_exp = match c { + Command::Start | Command::Stop | Command::End | Command::Read { .. } => Ack::Nack, + Command::Write { ack_exp: exp, .. } => exp, + }; + + let ack_check_en = match c { + Command::Start | Command::Stop | Command::End | Command::Read { .. } => false, + Command::Write { + ack_check_en: en, .. + } => en, + }; + + let ack_value = match c { + Command::Start | Command::Stop | Command::End | Command::Write { .. } => Ack::Nack, + Command::Read { ack_value: ack, .. } => ack, + }; + + let mut cmd: u32 = length.into(); + + if ack_check_en { + cmd |= 1 << 8; + } else { + cmd &= !(1 << 8); + } + + if ack_exp == Ack::Nack { + cmd |= 1 << 9; + } else { + cmd &= !(1 << 9); + } + + if ack_value == Ack::Nack { + cmd |= 1 << 10; + } else { + cmd &= !(1 << 10); + } + + cmd |= (opcode as u32) << 11; + + cmd + } + } + + // https://github.com/espressif/esp-idf/blob/master/components/ulp/lp_core/lp_core_i2c.c#L122 + // TX/RX RAM size is 16*8 bit + // TX RX FIFO has 16 bit depth + // The clock source of APB_CLK in LP_I2C is CLK_AON_FAST. + // Configure LP_I2C_SCLK_SEL to select the clock source for I2C_SCLK. + // When LP_I2C_SCLK_SEL is 0, select CLK_ROOT_FAST as clock source, + // and when LP_I2C_SCLK_SEL is 1, select CLK _XTALD2 as the clock source. + // Configure LP_EXT_I2C_CK_EN high to enable the clock source of I2C_SCLK. + // Adjust the timing registers accordingly when the clock frequency changes. + + pub struct LpI2c { + i2c: LP_I2C0, + } + + impl LpI2c { + pub fn new( + i2c: LP_I2C0, + _sda: LowPowerPin, + _scl: LowPowerPin, + frequency: HertzU32, + ) -> Self { + let me = Self { i2c }; + + // Configure LP I2C GPIOs + // Initialize IO Pins + let lp_io = unsafe { &*crate::peripherals::LP_IO::PTR }; + let lp_aon = unsafe { &*crate::peripherals::LP_AON::PTR }; + let lp_peri = unsafe { &*crate::peripherals::LP_PERI::PTR }; + + unsafe { + lp_aon + .gpio_mux() + .modify(|r, w| w.sel().bits(r.sel().bits() | (1 << 6))); + lp_aon + .gpio_mux() + .modify(|r, w| w.sel().bits(r.sel().bits() | (1 << 7))); + lp_io.gpio6().modify(|_, w| w.mcu_sel().variant(1)); // TODO + + lp_io.gpio7().modify(|_, w| w.mcu_sel().variant(1)); + + // Set output mode to Normal + lp_io.pin6().modify(|_, w| w.pad_driver().set_bit()); + // Enable output (writing to write-1-to-set register, then internally the + // `GPIO_OUT_REG` will be set) + lp_io + .out_enable_w1ts() + .write(|w| w.enable_w1ts().bits(1 << 6)); + // Enable input + lp_io.gpio6().modify(|_, w| w.fun_ie().set_bit()); + + // Disable pulldown (enable internal weak pull-down) + lp_io.gpio6().modify(|_, w| w.fun_wpd().clear_bit()); + // Enable pullup + lp_io.gpio6().modify(|_, w| w.fun_wpu().set_bit()); + + // Same process for SCL pin + lp_io.pin7().modify(|_, w| w.pad_driver().set_bit()); + // Enable output (writing to write-1-to-set register, then internally the + // `GPIO_OUT_REG` will be set) + lp_io + .out_enable_w1ts() + .write(|w| w.enable_w1ts().bits(1 << 7)); + // Enable input + lp_io.gpio7().modify(|_, w| w.fun_ie().set_bit()); + // Disable pulldown (enable internal weak pull-down) + lp_io.gpio7().modify(|_, w| w.fun_wpd().clear_bit()); + // Enable pullup + lp_io.gpio7().modify(|_, w| w.fun_wpu().set_bit()); + + // Select LP I2C function for the SDA and SCL pins + lp_io.gpio6().modify(|_, w| w.mcu_sel().bits(1)); + lp_io.gpio7().modify(|_, w| w.mcu_sel().bits(1)); + } + + // Initialize LP I2C HAL */ + me.i2c.clk_conf().modify(|_, w| w.sclk_active().set_bit()); + + // Enable LP I2C controller clock + lp_peri + .clk_en() + .modify(|_, w| w.lp_ext_i2c_ck_en().set_bit()); + + lp_peri + .reset_en() + .modify(|_, w| w.lp_ext_i2c_reset_en().set_bit()); + lp_peri + .reset_en() + .modify(|_, w| w.lp_ext_i2c_reset_en().clear_bit()); + + // Set LP I2C source clock + unsafe { &*LP_CLKRST::PTR } + .lpperi() + .modify(|_, w| w.lp_i2c_clk_sel().clear_bit()); + + // Initialize LP I2C Master mode + me.i2c.ctr().modify(|_, w| unsafe { + // Clear register + w.bits(0) + // Use open drain output for SDA and SCL + .sda_force_out() + .set_bit() + .scl_force_out() + .set_bit() + // Ensure that clock is enabled + .clk_en() + .set_bit() + }); + + // First, reset the fifo buffers + me.i2c.fifo_conf().modify(|_, w| w.nonfifo_en().clear_bit()); + + me.i2c + .ctr() + .modify(|_, w| w.tx_lsb_first().clear_bit().rx_lsb_first().clear_bit()); + + me.reset_fifo(); + + // Set LP I2C source clock + unsafe { &*LP_CLKRST::PTR } + .lpperi() + .modify(|_, w| w.lp_i2c_clk_sel().clear_bit()); + + // Configure LP I2C timing paramters. source_clk is ignored for LP_I2C in this + // call + + let source_clk = 16_000_000; + let bus_freq = frequency.raw(); + + let clkm_div: u32 = source_clk / (bus_freq * 1024) + 1; + let sclk_freq: u32 = source_clk / clkm_div; + let half_cycle: u32 = sclk_freq / bus_freq / 2; + + // SCL + let scl_low = half_cycle; + // default, scl_wait_high < scl_high + // Make 80KHz as a boundary here, because when working at lower frequency, too + // much scl_wait_high will faster the frequency according to some + // hardware behaviors. + let scl_wait_high = if bus_freq >= 80 * 1000 { + half_cycle / 2 - 2 + } else { + half_cycle / 4 + }; + let scl_high = half_cycle - scl_wait_high; + let sda_hold = half_cycle / 4; + let sda_sample = half_cycle / 2; // TODO + scl_wait_high; + let setup = half_cycle; + let hold = half_cycle; + // default we set the timeout value to about 10 bus cycles + // log(20*half_cycle)/log(2) = log(half_cycle)/log(2) + log(20)/log(2) + let tout = (4 * 8 - (5 * half_cycle).leading_zeros()) + 2; + + // According to the Technical Reference Manual, the following timings must be + // subtracted by 1. However, according to the practical measurement and + // some hardware behaviour, if wait_high_period and scl_high minus one. + // The SCL frequency would be a little higher than expected. Therefore, the + // solution here is not to minus scl_high as well as scl_wait high, and + // the frequency will be absolutely accurate to all frequency + // to some extent. + let scl_low_period = scl_low - 1; + let scl_high_period = scl_high; + let scl_wait_high_period = scl_wait_high; + // sda sample + let sda_hold_time = sda_hold - 1; + let sda_sample_time = sda_sample - 1; + // setup + let scl_rstart_setup_time = setup - 1; + let scl_stop_setup_time = setup - 1; + // hold + let scl_start_hold_time = hold - 1; + let scl_stop_hold_time = hold - 1; + let time_out_value = tout; + let time_out_en = true; + + // Write data to registers + unsafe { + me.i2c.clk_conf().modify(|_, w| { + w.sclk_sel() + .clear_bit() + .sclk_div_num() + .bits((clkm_div - 1) as u8) + }); + + // scl period + me.i2c + .scl_low_period() + .write(|w| w.scl_low_period().bits(scl_low_period as u16)); + + me.i2c.scl_high_period().write(|w| { + w.scl_high_period() + .bits(scl_high_period as u16) + .scl_wait_high_period() + .bits(scl_wait_high_period as u8) + }); + // sda sample + me.i2c + .sda_hold() + .write(|w| w.time().bits(sda_hold_time as u16)); + me.i2c + .sda_sample() + .write(|w| w.time().bits(sda_sample_time as u16)); + + // setup + me.i2c + .scl_rstart_setup() + .write(|w| w.time().bits(scl_rstart_setup_time as u16)); + me.i2c + .scl_stop_setup() + .write(|w| w.time().bits(scl_stop_setup_time as u16)); + + // hold + me.i2c + .scl_start_hold() + .write(|w| w.time().bits(scl_start_hold_time as u16)); + me.i2c + .scl_stop_hold() + .write(|w| w.time().bits(scl_stop_hold_time as u16)); + + me.i2c.to().write(|w| { + w.time_out_en() + .bit(time_out_en) + .time_out_value() + .variant(time_out_value.try_into().unwrap()) + }); + } + + // Enable SDA and SCL filtering. This configuration matches the HP I2C filter + // config + + me.i2c + .filter_cfg() + .modify(|_, w| unsafe { w.sda_filter_thres().bits(LP_I2C_FILTER_CYC_NUM_DEF) }); + me.i2c + .filter_cfg() + .modify(|_, w| unsafe { w.scl_filter_thres().bits(LP_I2C_FILTER_CYC_NUM_DEF) }); + + me.i2c + .filter_cfg() + .modify(|_, w| w.sda_filter_en().set_bit()); + me.i2c + .filter_cfg() + .modify(|_, w| w.scl_filter_en().set_bit()); + + // Configure the I2C master to send a NACK when the Rx FIFO count is full + me.i2c.ctr().modify(|_, w| w.rx_full_ack_level().set_bit()); + + // Synchronize the config register values to the LP I2C peripheral clock + me.lp_i2c_update(); + + me + } + + /// Update I2C configuration + fn lp_i2c_update(&self) { + self.i2c.ctr().modify(|_, w| w.conf_upgate().set_bit()); + } + + fn reset_fifo(&self) { + self.i2c + .fifo_conf() + .modify(|_, w| w.tx_fifo_rst().set_bit()); + + self.i2c + .fifo_conf() + .modify(|_, w| w.tx_fifo_rst().clear_bit()); + + self.i2c + .fifo_conf() + .modify(|_, w| w.rx_fifo_rst().set_bit()); + + self.i2c + .fifo_conf() + .modify(|_, w| w.rx_fifo_rst().clear_bit()); + } + } +} diff --git a/esp-lp-hal/CHANGELOG.md b/esp-lp-hal/CHANGELOG.md index 0bd4376bc..5c23b5ff0 100644 --- a/esp-lp-hal/CHANGELOG.md +++ b/esp-lp-hal/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add GPIO input support and implement additional `embedded-hal` output traits for the C6's LP core [#720] - Add the `ulp-riscv-hal` package (#840) - Add LP_UART basic driver (#1113) +- Added basic `LP-I2C` driver for C6 (#1185) ### Changed diff --git a/esp-lp-hal/Cargo.toml b/esp-lp-hal/Cargo.toml index 8846076d3..9f226f5bb 100644 --- a/esp-lp-hal/Cargo.toml +++ b/esp-lp-hal/Cargo.toml @@ -24,7 +24,7 @@ categories = [ cfg-if = "1.0.0" embedded-hal-02 = { version = "0.2.7", package = "embedded-hal", optional = true, features = ["unproven"] } embedded-hal-1 = { version = "1.0.0", package = "embedded-hal", optional = true } -esp32c6-lp = { version = "0.1.0", features = ["critical-section"], optional = true } +esp32c6-lp = { git = "https://github.com/esp-rs/esp-pacs", rev = "ef39b1c", features = ["critical-section"], optional = true } esp32s2-ulp = { version = "0.1.0", features = ["critical-section"], optional = true } esp32s3-ulp = { version = "0.1.0", features = ["critical-section"], optional = true } nb = { version = "1.1.0", optional = true } @@ -59,6 +59,10 @@ required-features = ["embedded-hal-02"] name = "uart" required-features = ["embedded-hal-02", "esp32c6"] +[[example]] +name = "i2c" +required-features = ["embedded-hal-02", "esp32c6"] + [patch.crates-io] esp32s2-ulp = { git = "https://github.com/esp-rs/esp-pacs", rev = "bcab40a" } esp32s3-ulp = { git = "https://github.com/esp-rs/esp-pacs", rev = "bcab40a" } diff --git a/esp-lp-hal/examples/i2c.rs b/esp-lp-hal/examples/i2c.rs new file mode 100644 index 000000000..267fe530f --- /dev/null +++ b/esp-lp-hal/examples/i2c.rs @@ -0,0 +1,25 @@ +//! Uses `LP_I2C` and reads calibration data from BMP180 sensor. +//! +//! This example dumps the calibration data from a BMP180 sensor, to view them, +//! logic analyzer or oscilloscope is required. +//! +//! The following wiring is assumed: +//! - SDA => GPIO6 +//! - SCL => GPIO7 + +#![no_std] +#![no_main] + +use embedded_hal_02::blocking::i2c::WriteRead; +use esp_lp_hal::{i2c::LpI2c, prelude::*}; +use panic_halt as _; + +#[entry] +fn main(mut i2c: LpI2c) -> ! { + let _peripherals = esp32c6_lp::Peripherals::take().unwrap(); + + loop { + let mut data = [0u8; 22]; + i2c.write_read(0x77, &[0xaa], &mut data).ok(); + } +} diff --git a/esp-lp-hal/src/i2c.rs b/esp-lp-hal/src/i2c.rs new file mode 100644 index 000000000..455e48fe4 --- /dev/null +++ b/esp-lp-hal/src/i2c.rs @@ -0,0 +1,587 @@ +//! Low-power I2C driver + +use esp32c6_lp::LP_I2C0; + +const LP_I2C_TRANS_COMPLETE_INT_ST_S: u32 = 7; +const LP_I2C_END_DETECT_INT_ST_S: u32 = 3; +const LP_I2C_NACK_INT_ST_S: u32 = 10; + +const I2C_LL_INTR_MASK: u32 = (1 << LP_I2C_TRANS_COMPLETE_INT_ST_S) + | (1 << LP_I2C_END_DETECT_INT_ST_S) + | (1 << LP_I2C_NACK_INT_ST_S); + +const LP_I2C_FIFO_LEN: u32 = 16; + +#[doc(hidden)] +pub unsafe fn conjure() -> LpI2c { + LpI2c { + i2c: LP_I2C0::steal(), + } +} + +/// I2C-specific transmission errors +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Error { + ExceedingFifo, + AckCheckFailed, + TimeOut, + ArbitrationLost, + ExecIncomplete, + CommandNrExceeded, + InvalidResponse, +} + +enum OperationType { + Write = 0, + Read = 1, +} + +#[derive(Eq, PartialEq, Copy, Clone)] +enum Ack { + Ack, + Nack, +} + +enum Opcode { + RStart = 6, + Write = 1, + Read = 3, + Stop = 2, + End = 4, +} + +#[derive(PartialEq)] +enum Command { + Start, + Stop, + End, + Write { + /// This bit is to set an expected ACK value for the transmitter. + ack_exp: Ack, + /// Enables checking the ACK value received against the ack_exp value. + ack_check_en: bool, + /// Length of data (in bytes) to be written. The maximum length is 255, + /// while the minimum is 1. + length: u8, + }, + Read { + /// Indicates whether the receiver will send an ACK after this byte has + /// been received. + ack_value: Ack, + /// Length of data (in bytes) to be read. The maximum length is 255, + /// while the minimum is 1. + length: u8, + }, +} + +impl From for u16 { + fn from(c: Command) -> u16 { + let opcode = match c { + Command::Start => Opcode::RStart, + Command::Stop => Opcode::Stop, + Command::End => Opcode::End, + Command::Write { .. } => Opcode::Write, + Command::Read { .. } => Opcode::Read, + }; + + let length = match c { + Command::Start | Command::Stop | Command::End => 0, + Command::Write { length: l, .. } | Command::Read { length: l, .. } => l, + }; + + let ack_exp = match c { + Command::Start | Command::Stop | Command::End | Command::Read { .. } => Ack::Nack, + Command::Write { ack_exp: exp, .. } => exp, + }; + + let ack_check_en = match c { + Command::Start | Command::Stop | Command::End | Command::Read { .. } => false, + Command::Write { + ack_check_en: en, .. + } => en, + }; + + let ack_value = match c { + Command::Start | Command::Stop | Command::End | Command::Write { .. } => Ack::Nack, + Command::Read { ack_value: ack, .. } => ack, + }; + + let mut cmd: u16 = length.into(); + + if ack_check_en { + cmd |= 1 << 8; + } else { + cmd &= !(1 << 8); + } + + if ack_exp == Ack::Nack { + cmd |= 1 << 9; + } else { + cmd &= !(1 << 9); + } + + if ack_value == Ack::Nack { + cmd |= 1 << 10; + } else { + cmd &= !(1 << 10); + } + + cmd |= (opcode as u16) << 11; + + cmd + } +} + +impl From for u32 { + fn from(c: Command) -> u32 { + let opcode = match c { + Command::Start => Opcode::RStart, + Command::Stop => Opcode::Stop, + Command::End => Opcode::End, + Command::Write { .. } => Opcode::Write, + Command::Read { .. } => Opcode::Read, + }; + + let length = match c { + Command::Start | Command::Stop | Command::End => 0, + Command::Write { length: l, .. } | Command::Read { length: l, .. } => l, + }; + + let ack_exp = match c { + Command::Start | Command::Stop | Command::End | Command::Read { .. } => Ack::Nack, + Command::Write { ack_exp: exp, .. } => exp, + }; + + let ack_check_en = match c { + Command::Start | Command::Stop | Command::End | Command::Read { .. } => false, + Command::Write { + ack_check_en: en, .. + } => en, + }; + + let ack_value = match c { + Command::Start | Command::Stop | Command::End | Command::Write { .. } => Ack::Nack, + Command::Read { ack_value: ack, .. } => ack, + }; + + let mut cmd: u32 = length.into(); + + if ack_check_en { + cmd |= 1 << 8; + } else { + cmd &= !(1 << 8); + } + + if ack_exp == Ack::Nack { + cmd |= 1 << 9; + } else { + cmd &= !(1 << 9); + } + + if ack_value == Ack::Nack { + cmd |= 1 << 10; + } else { + cmd &= !(1 << 10); + } + + cmd |= (opcode as u32) << 11; + + cmd + } +} + +enum CommandRegister { + COMD0, + COMD1, + COMD2, + COMD3, + COMD4, + COMD5, + COMD6, + COMD7, +} + +impl CommandRegister { + fn advance(&mut self) { + *self = match *self { + CommandRegister::COMD0 => CommandRegister::COMD1, + CommandRegister::COMD1 => CommandRegister::COMD2, + CommandRegister::COMD2 => CommandRegister::COMD3, + CommandRegister::COMD3 => CommandRegister::COMD4, + CommandRegister::COMD4 => CommandRegister::COMD5, + CommandRegister::COMD5 => CommandRegister::COMD6, + CommandRegister::COMD6 => CommandRegister::COMD7, + CommandRegister::COMD7 => panic!("Cannot advance beyond COMD7"), + } + } +} + +// https://github.com/espressif/esp-idf/blob/master/components/ulp/lp_core/lp_core_i2c.c#L122 +// TX/RX RAM size is 16*8 bit +// TX RX FIFO has 16 bit depth +// The clock source of APB_CLK in LP_I2C is CLK_AON_FAST. +// Configure LP_I2C_SCLK_SEL to select the clock source for I2C_SCLK. +// When LP_I2C_SCLK_SEL is 0, select CLK_ROOT_FAST as clock source, +// and when LP_I2C_SCLK_SEL is 1, select CLK _XTALD2 as the clock source. +// Configure LP_EXT_I2C_CK_EN high to enable the clock source of I2C_SCLK. +// Adjust the timing registers accordingly when the clock frequency changes. + +pub struct LpI2c { + i2c: LP_I2C0, +} + +impl LpI2c { + /// Send data bytes from the `bytes` array to a target slave with the + /// address `addr` + fn master_write(&mut self, addr: u8, bytes: &[u8]) -> Result<(), Error> { + let mut cmd_iterator = CommandRegister::COMD0; + + // If SCL is busy, reset the Master FSM + if self.i2c.sr().read().bus_busy().bit_is_set() { + self.i2c.ctr().modify(|_, w| w.fsm_rst().set_bit()); + } + + if bytes.len() > 255 { + return Err(Error::ExceedingFifo); + } + + // Reset FIFO and command list + self.reset_fifo(); + + self.add_cmd_lp(&mut cmd_iterator, Command::Start)?; + + // Load device address and R/W bit into FIFO + self.write_fifo(addr << 1 | OperationType::Write as u8); + + self.add_cmd_lp( + &mut cmd_iterator, + Command::Write { + ack_exp: Ack::Ack, + ack_check_en: true, + length: 1_u8, + }, + )?; + + self.enable_interrupts(I2C_LL_INTR_MASK); + + let mut data_idx = 0; + let mut remaining_bytes = bytes.len() as u32; + + let mut fifo_available = LP_I2C_FIFO_LEN - 1; + + while remaining_bytes > 0 { + let fifo_size = if remaining_bytes < fifo_available { + remaining_bytes + } else { + fifo_available + }; + remaining_bytes -= fifo_size; + + // Write data to the FIFO + for &byte in &bytes[data_idx as usize..(data_idx as usize) + fifo_size as usize] { + self.write_fifo(byte); + } + + // Add a Write command with the specified length + self.add_cmd_lp( + &mut cmd_iterator, + Command::Write { + ack_exp: Ack::Ack, + ack_check_en: true, + length: fifo_size as u8, + }, + )?; + + // Check if this is the last chunk + let cmd = if remaining_bytes == 0 { + Command::Stop + } else { + Command::End + }; + + // Add the Stop/End command + self.add_cmd_lp(&mut cmd_iterator, cmd)?; + + // Start the I2C transaction + self.lp_i2c_update(); + self.i2c.ctr().modify(|_, w| w.trans_start().set_bit()); + + // Wait for the transaction to complete + self.wait_for_completion()?; + + // Update the index for the next data chunk + data_idx += fifo_size; + + fifo_available = LP_I2C_FIFO_LEN; + } + + Ok(()) + } + + fn master_read(&mut self, addr: u8, buffer: &mut [u8]) -> Result<(), Error> { + // Check size constraints + if buffer.len() > 254 { + return Err(Error::ExceedingFifo); + } + + let mut cmd_iterator = CommandRegister::COMD0; + + self.add_cmd_lp(&mut cmd_iterator, Command::Start)?; + + // Load device address + self.write_fifo(addr << 1 | OperationType::Read as u8); + + self.add_cmd_lp( + &mut cmd_iterator, + Command::Write { + ack_exp: Ack::Ack, + ack_check_en: true, + length: 1_u8, + }, + )?; + + self.enable_interrupts( + 1 << LP_I2C_TRANS_COMPLETE_INT_ST_S | 1 << LP_I2C_END_DETECT_INT_ST_S, + ); + + let mut remaining_bytes = buffer.len(); + + while remaining_bytes > 0 { + let fifo_size = if remaining_bytes < LP_I2C_FIFO_LEN as usize { + remaining_bytes + } else { + LP_I2C_FIFO_LEN as usize + }; + remaining_bytes -= fifo_size; + + if fifo_size == 1 { + // Read one byte and send NACK + self.add_cmd_lp( + &mut cmd_iterator, + Command::Read { + ack_value: Ack::Nack, + length: 1, // which is `fifo_size` + }, + )?; + // Send STOP command after reading + self.add_cmd_lp(&mut cmd_iterator, Command::Stop)?; + } else if fifo_size > 1 && remaining_bytes == 0 { + // This means it is the last transaction + // Read all but the last byte and send ACKs + self.add_cmd_lp( + &mut cmd_iterator, + Command::Read { + ack_value: Ack::Ack, + length: (fifo_size - 1) as u8, + }, + )?; + // Read the last byte and send NACK + self.add_cmd_lp( + &mut cmd_iterator, + Command::Read { + ack_value: Ack::Nack, + length: 1, + }, + )?; + // Send STOP command after reading + self.add_cmd_lp(&mut cmd_iterator, Command::Stop)?; + } else { + // This means we have to read data more than we can fit into the Rx FIFO + // Read fifo_size bytes and send ACKs + self.add_cmd_lp( + &mut cmd_iterator, + Command::Read { + ack_value: Ack::Ack, + length: fifo_size as u8, + }, + )?; + // Send END command signaling more data to come + self.add_cmd_lp(&mut cmd_iterator, Command::End)?; + } + + self.lp_i2c_update(); + + // Initiate I2C transfer + self.i2c.ctr().modify(|_, w| w.trans_start().set_bit()); + + // Await for completion (This function or mechanism should handle waiting for + // the I2C transfer to complete) + self.wait_for_completion()?; + + // Read from FIFO into the current chunk + for byte in buffer.iter_mut() { + *byte = self.read_fifo(); + } + } + Ok(()) + } + + fn master_write_read( + &mut self, + addr: u8, + bytes: &[u8], + buffer: &mut [u8], + ) -> Result<(), Error> { + // it would be possible to combine the write and read + // in one transaction but filling the tx fifo with + // the current code is somewhat slow even in release mode + // which can cause issues + self.master_write(addr, bytes)?; + self.master_read(addr, buffer)?; + Ok(()) + } + + /// Update I2C configuration + fn lp_i2c_update(&self) { + self.i2c.ctr().modify(|_, w| w.conf_upgate().set_bit()); + } + + fn reset_fifo(&self) { + self.i2c + .fifo_conf() + .modify(|_, w| w.tx_fifo_rst().set_bit()); + + self.i2c + .fifo_conf() + .modify(|_, w| w.tx_fifo_rst().clear_bit()); + + self.i2c + .fifo_conf() + .modify(|_, w| w.rx_fifo_rst().set_bit()); + + self.i2c + .fifo_conf() + .modify(|_, w| w.rx_fifo_rst().clear_bit()); + } + + fn wait_for_completion(&self) -> Result<(), Error> { + loop { + let interrupts = self.i2c.int_status().read(); + + // Handle completion cases + // A full transmission was completed + if interrupts.nack_int_st().bit_is_set() { + self.i2c + .int_clr() + .write(|w| unsafe { w.bits(I2C_LL_INTR_MASK) }); + return Err(Error::InvalidResponse); + } else if interrupts.trans_complete_int_st().bit_is_set() { + self.disable_interrupts(); + + self.i2c + .int_clr() + .write(|w| unsafe { w.bits(I2C_LL_INTR_MASK) }); + break; + } else if interrupts.end_detect_int_st().bit_is_set() { + self.i2c + .int_clr() + .write(|w| unsafe { w.bits(I2C_LL_INTR_MASK) }); + break; + } + } + + Ok(()) + } + + fn enable_interrupts(&self, mask: u32) { + self.i2c.int_ena().write(|w| unsafe { w.bits(mask) }); + } + + fn disable_interrupts(&self) { + self.i2c.int_ena().write(|w| unsafe { w.bits(0) }); + } + + fn write_fifo(&self, data: u8) { + self.i2c + .data() + .write(|w| unsafe { w.fifo_rdata().bits(data) }); + } + + fn read_fifo(&self) -> u8 { + self.i2c.data().read().fifo_rdata().bits() + } + + fn add_cmd_lp( + &self, + command_register: &mut CommandRegister, + command: Command, + ) -> Result<(), Error> { + match *command_register { + CommandRegister::COMD0 => { + self.i2c + .comd0() + .write(|w| unsafe { w.command0().bits(command.into()) }); + } + CommandRegister::COMD1 => { + self.i2c + .comd1() + .write(|w| unsafe { w.command1().bits(command.into()) }); + } + CommandRegister::COMD2 => { + self.i2c + .comd2() + .write(|w| unsafe { w.command2().bits(command.into()) }); + } + CommandRegister::COMD3 => { + self.i2c + .comd3() + .write(|w| unsafe { w.command3().bits(command.into()) }); + } + CommandRegister::COMD4 => { + self.i2c + .comd4() + .write(|w| unsafe { w.command4().bits(command.into()) }); + } + CommandRegister::COMD5 => { + self.i2c + .comd5() + .write(|w| unsafe { w.command5().bits(command.into()) }); + } + CommandRegister::COMD6 => { + self.i2c + .comd6() + .write(|w| unsafe { w.command6().bits(command.into()) }); + } + CommandRegister::COMD7 => { + self.i2c + .comd7() + .write(|w| unsafe { w.command7().bits(command.into()) }); + } + } + command_register.advance(); + Ok(()) + } +} + +#[cfg(feature = "embedded-hal-02")] +impl embedded_hal_02::blocking::i2c::Read for LpI2c { + type Error = Error; + + fn read(&mut self, address: u8, buffer: &mut [u8]) -> Result<(), Self::Error> { + self.master_read(address, buffer) + } +} + +#[cfg(feature = "embedded-hal-02")] +impl embedded_hal_02::blocking::i2c::Write for LpI2c { + type Error = Error; + + fn write(&mut self, addr: u8, bytes: &[u8]) -> Result<(), Self::Error> { + self.master_write(addr, bytes) + } +} + +#[cfg(feature = "embedded-hal-02")] +impl embedded_hal_02::blocking::i2c::WriteRead for LpI2c { + type Error = Error; + + fn write_read( + &mut self, + address: u8, + bytes: &[u8], + buffer: &mut [u8], + ) -> Result<(), Self::Error> { + self.master_write_read(address, bytes, buffer) + } +} diff --git a/esp-lp-hal/src/lib.rs b/esp-lp-hal/src/lib.rs index 977f7f3ee..a7b25a807 100644 --- a/esp-lp-hal/src/lib.rs +++ b/esp-lp-hal/src/lib.rs @@ -6,6 +6,8 @@ use core::arch::global_asm; pub mod delay; pub mod gpio; #[cfg(esp32c6)] +pub mod i2c; +#[cfg(esp32c6)] pub mod uart; pub mod pac { diff --git a/esp-lp-hal/src/uart.rs b/esp-lp-hal/src/uart.rs index 529c828cc..95fe7a4af 100644 --- a/esp-lp-hal/src/uart.rs +++ b/esp-lp-hal/src/uart.rs @@ -5,10 +5,10 @@ use crate::pac::LP_UART; const UART_FIFO_SIZE: u16 = 128; #[doc(hidden)] -pub unsafe fn conjure() -> Option { - Some(LpUart { +pub unsafe fn conjure() -> LpUart { + LpUart { uart: LP_UART::steal(), - }) + } } #[derive(Debug)] diff --git a/esp32c6-hal/examples/lp_core_i2c.rs b/esp32c6-hal/examples/lp_core_i2c.rs new file mode 100644 index 000000000..01022ba05 --- /dev/null +++ b/esp32c6-hal/examples/lp_core_i2c.rs @@ -0,0 +1,46 @@ +//! This shows a very basic example of running code on the LP core. +//! +//! Code on LP core uses LP_I2C initialized on HP core. For more information +//! check `lp_core_i2c` example in the `esp-lp-hal`. +//! +//! Make sure to first compile the `esp-lp-hal/examples/i2c.rs` example + +#![no_std] +#![no_main] + +use esp32c6_hal::{ + gpio::lp_gpio::IntoLowPowerPin, + i2c::lp_i2c::LpI2c, + lp_core, + peripherals::Peripherals, + prelude::*, + IO, +}; +use esp_backtrace as _; +use esp_println::println; + +#[entry] +fn main() -> ! { + let peripherals = Peripherals::take(); + + let io = IO::new(peripherals.GPIO, peripherals.IO_MUX); + + let lp_sda = io.pins.gpio6.into_low_power().into_open_drain_output(); + let lp_scl = io.pins.gpio7.into_low_power().into_open_drain_output(); + + let lp_i2c = LpI2c::new(peripherals.LP_I2C0, lp_sda, lp_scl, 100u32.kHz()); + + let mut lp_core = esp32c6_hal::lp_core::LpCore::new(peripherals.LP_CORE); + lp_core.stop(); + println!("lp core stopped"); + + // load code to LP core + let lp_core_code = + load_lp_code!("../esp-lp-hal/target/riscv32imac-unknown-none-elf/release/examples/i2c"); + + // start LP core + lp_core_code.run(&mut lp_core, lp_core::LpCoreWakeupSource::HpCpu, lp_i2c); + println!("lpcore run"); + + loop {} +} diff --git a/esp32c6-hal/examples/lp_core_uart.rs b/esp32c6-hal/examples/lp_core_uart.rs index 741b3994c..a8bde0ac4 100644 --- a/esp32c6-hal/examples/lp_core_uart.rs +++ b/esp32c6-hal/examples/lp_core_uart.rs @@ -1,7 +1,7 @@ //! This shows a very basic example of running code on the LP core. //! //! Code on LP core uses LP_UART initialized on HP core. For more information -//! check `lp_core_uart` example in the `esp-lp-hal. +//! check `lp_core_uart` example in the `esp-lp-hal`. //! //! Make sure to first compile the `esp-lp-hal/examples/uart.rs` example @@ -50,7 +50,6 @@ fn main() -> ! { let mut uart1 = Uart::new_with_config(peripherals.UART1, config, Some(pins), &clocks); // Set up (LP) UART: - let lp_tx = io.pins.gpio5.into_low_power().into_push_pull_output(); let lp_rx = io.pins.gpio4.into_low_power().into_floating_input();