Skip to content
New issue

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

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

Already on GitHub? # to your account

Impossible to get I2C running #184

Open
EzraWolf opened this issue Jan 2, 2025 · 2 comments
Open

Impossible to get I2C running #184

EzraWolf opened this issue Jan 2, 2025 · 2 comments

Comments

@EzraWolf
Copy link

EzraWolf commented Jan 2, 2025

For over a month I have been trying to get a simple I2C program running, and for over a month I have received exactly one error: BUS_BUSY | ARBITRATION_LOST which is likely due to the I2C controller flag TRANSMIT_DATA being high.

I don't know enough about the I2C protocol to understand it, and I do not have a digital analyzer or oscilloscope to measure the lines. Hardware looks fine, I am using a Teensy 4.1 and it is connected to an SSD1306 OLED will pullups.

Here is what I know.

TRANSMIT_DATA flag is stuck high from initialization one way or another, have never seen it low in any case no matter what condition at all. Cannot be cleared with any of the following:

  • Using reset_controller()
  • Using clear_fifo()
  • Using clear_controller_status(ControllerStatus::all())
  • Trying thousands of reset attempts in a while loop out of desperation

When attempting I2C write:

  • System hangs (perpetual LED of sisyphus stops blinking)
  • Need to remove power from the OLED before anything runs again
  • Afterwards the USB logger shows ARBITRATION_LOST and BUS_BUSY errors from the I2C write_byte function
  • FIFO shows txcount of 2 after failed write sometimes ??

Hardware Setup

  • Teensy 4.1
  • SSD1306 OLED Display
  • I2C Address: 0x3C
  • Using LPI2C3 peripheral on pin 16 for SCL and pin 17 for SDA, LPI2C1 has the exact same issues
  • Both pins configured with internal 100k pull-ups

Attempted Solutions

  • Every combination of resetting things in different orders (e.g., clear FIFO then I2C controller, etc.)
  • Tried to clear status flags in different orders as well
  • Tried 100KHz and 400KHz each numerous times
  • Explicit pin configuration before I2C initialization
  • Multiple reset attempts during initialization
  • Read through most of the embedded-hal and imxrt-hal source code
  • Read each of their respective doc books front and back and could not find any issues or implementations
  • Implementing 2 I2C examples listed in the referenced code, neither worked.

I am about to give up on this insultingly simple problem. Please anybody help, if anything to see a different error. I have spent the better part of 60+ hours on this.

Referenced code:
https://github.com/RoboJackets/robocup-rustware/blob/2fab2e23d003bdc0cc6d33cc6efbb2a6bbc87f69/control/examples/scan_i2c.rs
https://github.com/SharpCoder/teensycore/blob/8bd5535aacf34af972a07a849f02c8160741a96f/src/i2c.rs
every single example: https://github.com/mciantyre/teensy4-rs/tree/master/examples
uses an old version of embedded-hal i could not determine to get working: https://github.com/imxrt-rs/imxrt-hal/blob/main/examples/hal_i2c_mpu9250.rs

The entirety of my code is listed below.

main.rs

#![no_std]
#![no_main]
#![deny(unsafe_code)]

use teensy4_panic as _;

mod drivers;

#[rtic::app(device = teensy4_bsp, peripherals = true, dispatchers = [KPP])]
mod app {

    // Teensy 4.0 and 4.1 board imports
    // - Teensy 4.1 is `teensy_board::t41`
    // - Teensy 4.0 is `teensy_board::t40`
    use teensy4_bsp as bsp; // Target System on a Module (SoM)

    // IMXRT HAL imports
    use imxrt_log as logging;

    // RTIC imports
    // TODO: "*" is for `.millis()`, need to change
    use rtic_monotonics::systick::{Systick, *};

    use crate::drivers;

    const PIN_PULLUP: bsp::hal::iomuxc::Config = bsp::hal::iomuxc::Config::zero().set_pull_keeper(Some(bsp::hal::iomuxc::PullKeeper::Pullup100k));
    const PIN_PULLDN: bsp::hal::iomuxc::Config = bsp::hal::iomuxc::Config::zero().set_pull_keeper(Some(bsp::hal::iomuxc::PullKeeper::Pulldown100k));

    /// Shared resources across different tasks
    /// RTIC ensures safe concurrent data access without
    /// manually having to sync tasks, primitives, etc.
    #[shared]
    struct SharedResources {}

    /// These resources are local to individual tasks.
    #[local]
    struct LocalResources {
        /// The LED pin is typically GPIO13
        led: bsp::board::Led,
        // i2c_port: bsp::board::Lpi2c3,
        i2c_instance: drivers::I2C<bsp::board::Lpi2c3>,

        /// Helps to control USB logging
        poller: logging::Poller,
    }

    #[init]
    fn init(cx: init::Context) -> (SharedResources, LocalResources) {
        let bsp::board::Resources {
            usb,
            lpi2c3,
            mut pins,
            mut gpio2,
            ..
        } = bsp::board::t41(cx.device);

        // Initialize Systick
        Systick::start(
            cx.core.SYST,
            bsp::board::ARM_FREQUENCY,
            rtic_monotonics::create_systick_token!(),
        );

        // Initialize GPIO
        let pin_led = gpio2.output(pins.p13);

        // Initialize an I2C interface on port 1
        // SCL, LPI2C3 is pin 16, LPI2C1 is pin 19
        // SDA, LPI2C3 is pin 17, LPI2C1 is pin 18
        bsp::hal::iomuxc::configure(&mut pins.p16, PIN_PULLUP); // SCL
        bsp::hal::iomuxc::configure(&mut pins.p17, PIN_PULLUP); // SDA
        let i2c_port = bsp::board::lpi2c(
            lpi2c3,
            pins.p16, // SCL
            pins.p17, // SDA
            // bsp::board::Lpi2cClockSpeed::KHz100
            bsp::board::Lpi2cClockSpeed::KHz400
        );


        let i2c_instance = drivers::I2C::new(i2c_port);

        // Initialize a poller for USB serial monitoring / logging
        let poller: logging::Poller = logging::log::usbd(usb, logging::Interrupts::Enabled).unwrap();

        // Task constant configs
        // Is this a bad idea?
        const LED_MS_DELAY: u32 = 500;
        const LOG_MS_DELAY: u32 = 1000;

        //let end_status_controller = i2c_port.controller_status();
        //let end_status_fifo = i2c_port.controller_fifo_status();

        // The SoM has now been initialized
        // We can begin scheduling tasks to run
        blink::spawn(LED_MS_DELAY).unwrap();
        test_usb_logging::spawn(LOG_MS_DELAY).unwrap();
        // port_i2c_test::spawn().unwrap();
        test_i2c_new_hal::spawn().unwrap();

        // Return local and shared resources
        (
            SharedResources {},
            LocalResources {
                led: pin_led,
                // i2c_port,
                i2c_instance,
                poller,
            }
        )
    }

    /// Basic LED blink example on GPIO13
    #[task(local = [led])]
    async fn blink(cx: blink::Context, ms_delay: u32) {
        loop {
            cx.local.led.toggle();
            Systick::delay(ms_delay.millis()).await;
        }
    }

    /// Test how multiple tasks all trying to log via USB will work if it does
    /// Updated: Seems to work great at least with text logging
    #[task()]
    async fn test_usb_logging(_cx: test_usb_logging::Context, ms_delay: u32) {
        let mut count: u32 = 0;
        loop {
            log::info!("Test count: {count}");
            count += 1;

            Systick::delay(ms_delay.millis()).await;
        }
    }

    #[task(local = [i2c_instance])]
    async fn test_i2c_new_hal(cx: test_i2c_new_hal::Context) {
        Systick::delay((5000 as u32).millis()).await;

        let i2c_instance = cx.local.i2c_instance;
        const OLED_ADDR: u8 = 0x3C;
        const MAX_RESET_ATTEMPTS: u16 = 10000;
        
        log::info!("Initial status: {:?}", i2c_instance.port().controller_status());
        
        // Try to clear TRANSMIT_DATA
        let mut attempts = 0;
        while i2c_instance.port().controller_status().contains(bsp::hal::lpi2c::ControllerStatus::TRANSMIT_DATA) {
            log::info!("Attempt {} to clear TRANSMIT_DATA", attempts);
            
            i2c_instance.port().reset_controller();
            i2c_instance.port().clear_fifo();
            i2c_instance.port().clear_controller_status(bsp::hal::lpi2c::ControllerStatus::all());
            
            Systick::delay((1 as u32).millis()).await;
            
            attempts += 1;
            if attempts >= MAX_RESET_ATTEMPTS {
                log::error!("Failed to clear TRANSMIT_DATA after {} attempts", attempts);
                break;
            }
        }
        
        // Only try write if we cleared TRANSMIT_DATA
        if !i2c_instance.port().controller_status().contains(bsp::hal::lpi2c::ControllerStatus::TRANSMIT_DATA) {
            log::info!("TRANSMIT_DATA cleared, attempting write...");
            match i2c_instance.write_byte(OLED_ADDR, &[0x00]) {
                Ok(_) => log::info!("OLED write successful"),
                Err(err) => {
                    log::error!("OLED write failed: {err:?}");
                    log::info!("Status after error: {:?}", i2c_instance.port().controller_status());
                    log::info!("FIFO after error: {:?}", i2c_instance.port().controller_fifo_status());
                }
            }
        }

        loop {
            Systick::delay((1000 as u32).millis()).await;
            log::info!("Current status: {:?}", i2c_instance.port().controller_status());
            log::info!("Current FIFO: {:?}", i2c_instance.port().controller_fifo_status());
        }
    }

    /// Runs when the USB1 interrupt activates
    /// Poll the logger to control the logging (e.g., `log::info!(...);`)
    /// This seems to work fine with text logging if multiple tasks call it at once
    ///
    /// To view USB logging results use a serial logger
    /// - The "Serial Monitor" extension on VSCode works perfect for this
    /// - Make sure you are connected to your devices correct COM port (e.g., COM3)
    /// - Baud rate @ 115200 works well
    ///
    /// Used additional help from this USB logging example:
    /// (Very similar to the original LED blink program / template)
    /// https://github.com/mciantyre/teensy4-rs/blob/master/examples/rtic_usb_log.rs
    #[task(binds = USB_OTG1, local = [poller])]
    fn usb_interrupt(cx: usb_interrupt::Context) {
        cx.local.poller.poll();
    }
}

/drivers/i2c.rs

use teensy4_bsp as bsp;
use embedded_hal;

pub struct I2C<I2CPORT> {
    i2c: I2CPORT,
}

// Because implementing return and write sizes of byte, word, dword, etc.
// for `xxxx_vec::<data length>(address)` simply changes the generic `N`,
// these macros do it all for us.
macro_rules! impl_write {
    ($name:ident, $size:literal) => {
        /// Generated implementation of `write_vec` by the `impl_write!` macro
        pub fn $name(&mut self, addr: u8, data: &[u8; $size]) -> Result<(), I2CPORT::Error> {
            self.write_vec::<$size>(addr, data)
        }
    }
}

macro_rules! impl_read {
    ($name:ident, $size:literal) => {
        /// Generated implementation of `read_vec` by the `impl_read!` macro
        pub fn $name(&mut self, addr: u8) -> Result<[u8; $size], I2CPORT::Error> {
            self.read_vec::<$size>(addr)
        }
    }
}

macro_rules! impl_write_read {
    ($name:ident, $size:literal) => {
        /// Generated implementation of `write_read_vec` by the `impl_write_read!` macro
        pub fn $name(&mut self, addr: u8, data: &[u8; $size]) -> Result<[u8; $size], I2CPORT::Error> {
            self.write_read_vec::<$size>(addr, data)
        }
    }
}

impl<I2CPORT: embedded_hal::i2c::I2c> I2C<I2CPORT>{
    pub fn new(i2c: I2CPORT) -> Self {
        Self { i2c }
    }

    pub fn port(&mut self) -> &mut I2CPORT {
        &mut self.i2c
    }

    /// Generic N length I2C data write functionality
    /// Write `write_vec::<your_len_here>`
    /// Works with embedded-hal implementations of I2C such as
    /// - LPI2Cx for the imxrt-hal
    /// - (soon the STM32H7xx-hal?)
    pub fn write_vec<const N: usize>(&mut self, addr: u8, data: &[u8; N]) -> Result<(), I2CPORT::Error> {
        self.i2c.write(addr, data)?;
        Ok(())
    }

    impl_write!(write_byte, 1);
    impl_write!(write_word, 2);
    impl_write!(write_dword, 4);
    
    /// Generic N length I2C data read functionality
    /// `read_vec::<your_len_here>(addr)`
    /// 
    /// Works with embedded-hal implementations of I2C such as
    /// - LPI2Cx for the imxrt-hal
    /// - (soon the STM32H7xx-hal?)
    pub fn read_vec<const N: usize>(&mut self, addr: u8) -> Result<[u8; N], I2CPORT::Error> {
        let mut return_buffer = [0u8; N];

        // Regurgitate an array.
        self.i2c.read(addr, &mut return_buffer)?;
        Ok(return_buffer)
    }

    impl_read!(read_byte, 1);
    impl_read!(read_word, 2);
    impl_read!(read_dword, 4);
    
    /// Generic N length I2C write then read from the same address functionality
    /// Works with embedded-hal implementations of I2C such as
    /// - LPI2Cx for the imxrt-hal
    /// - (soon the STM32H7xx-hal?)
    pub fn write_read_vec<const N: usize>(&mut self, addr: u8, data: &[u8; N]) -> Result<[u8; N], I2CPORT::Error> {
        self.write_vec::<N>(addr, data)?;
        Ok(self.read_vec::<N>(addr).unwrap())
    }

    impl_write_read!(write_read_byte, 1);
    impl_write_read!(write_read_word, 2);
    impl_write_read!(write_read_dword, 4);

}

cargo.toml

[package]
name = "agony"
version = "0.1.0"
edition = "2021"

[dependencies]
# MCU-architecture-specific
imxrt-log = { version = "0.1", default-features = false, features = ["log", "usbd"] }

[dependencies.log]
version = "0.4"
features = ["max_level_debug", "release_max_level_info"]

[dependencies.teensy4-bsp]
version = "0.5"
features = ["rt"] # Use the BSP's runtime, lets you write `main()`

[dependencies.teensy4-panic]
version = "0.2"
features = ["log"]

[dependencies.embedded-hal]
version = "1.0.0"

###############################
# RTOS framework dependencies #
###############################
[dependencies.rtic]
version = "2.0.1"
features = ["thumbv7-backend"]

[dependencies.rtic-monotonics]
version = "1.0.0"
# default-features = false
features = ["cortex-m-systick"]

###########################
# Build-time dependencies #
###########################
[build-dependencies]
toml = "0.8.19"

# Set to 0 to optimize build times, no optimizations
# Set to 3 for all optimizations
# Set to "z" to optimize build size w/o loop vectorization
[profile.release.build-override]
# opt-level = 0 # No optimizations
# opt-level = 1 # Few optimizations
# opt-level = 2 # Some optimizations
# opt-level = 3 # All optimizations (slowest build time)
# opt-level = "s" # Smallest binary size
opt-level = "z" # Optimize for build size w/o loop vectorization

# [target.<architecture-triple>]
# runner = 'probe-rs run --chip <chip-name>'
@EzraWolf
Copy link
Author

EzraWolf commented Jan 8, 2025

There is no solution to this?

@teburd
Copy link
Member

teburd commented Jan 9, 2025

I would highly recommend getting an inexpensive logic analyzer to view what is going on here

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants