How to Use the I2C Port on LattePanda Mu in Ubuntu OS

userHead Youyou 2024-10-08 01:57:35 3548 Views1 Replies

Background 

 

LattePanda Mu has multiple I2C Ports, allowing direct communication with I2C devices after coding in the OS. This approach eliminates the need for a USB to I2C adapter or an MCU. 

 

This article demonstrates how to identify BusNumber, program and use LattePanda Mu's I2C pins to read and write the AT24C256 EEPROM Module in an Ubuntu OS.

 

Preparation

 

Hardware

 

- LattePanda Mu

 

- Lite Carrier

 

- AT24C256 I2C EEPROM Module 

 

Wiring

 

The Lite Carrier of LattePanda Mu provides four I2C Ports as shown in the diagram below.

 

Connect the AT24C256 EEPROM Module to any one of the I2C Ports. The pin connections are listed in the table below.

Lite Carrier I2C Port PinsAT24C256 Module Pins
3V3VCC
GNDGND
SCLSCL
SDASDA

 

Software

 

- Ubuntu OS

 

- i2c-tools

 

- smbus2

 

Installation and Initialization

 

- It is recommended to use the OS versions from the LattePanda Mu OS Compatibility List, as older or other versions may not function correctly. 

 

- This article will use the i2cdetect command to identify the BusNumber of the I2C Port. This command is part of the i2c-tools package, which may not be installed by default on Ubuntu or other Linux distributions, so first, install the i2c-tools package.

sudo apt install i2c-tools 

 

- This article will also use the smbus2 library to program the I2C device, so they need to be installed. It is recommended to install related packages in a virtual environment.

sudo apt install python3-venv

# Create and enter the working directory(the folder name can be customized)
mkdir ~/i2c_project
cd ~/i2c_project

# Create a virtual environment(named .venv), then activate 
python3 -m venv .venv
source .venv/bin/activate

pip install smbus2

 

- By default, ordinary users do not have permission to access I2C devices. Add your user to the i2c group:

sudo usermod -aG i2c $USER

 

- After executing above command, you must log out and log in again (or restart the OS) for the permission changes of the user group to take effect. Then enter the python virtual environment(.venv) again.

 

Detailed Steps

 

Expected Functionality: Write a code to store the value 0x42 at address 0x0000 on an AT24C256 EEPROM chip. Then, read back the value from address 0x0000 and compare it with the stored value to check if they are identical.

 

- Follow the steps outlined in the previous section to complete the wiring, and install the ubuntu OS and software libraries.

 

- Verify the BusNumber of the I2C Port

 

The index number marked on the Lite Carrier I2C Port may not match the BusNumber in the OS, so manual verification is needed. We can use the following command for this purpose. 

i2cdetect -y -r BusNum

Start testing the BusNum from 0 and increment until you find the 7-bit I2C address 0x50 of the AT24C256. 

 

As shown in the following picture, the BusNumber is 3.

 

- Use the smbus2 library to write the code.

 

You can seek AI assistance from GPT or Claude while writing the code, which usually works. The complete sample code is as follows:

 

from smbus2 import SMBus  
import time  
DEVICE_ADDRESS = 0x50  # AT24C256 I2C address  
MEMORY_ADDRESS = 0x0000  # 16-bit memory address  
VALUE_TO_WRITE = 0x42  # Value to write to EEPROM  
def write_byte(bus, device_address, memory_address, data):  
   # Split 16-bit memory address into two bytes  
   addr_msb = (memory_address >> 8) & 0xFF  # Most significant byte  
   addr_lsb = memory_address & 0xFF         # Least significant byte  
   bus.write_i2c_block_data(device_address, addr_msb, [addr_lsb, data])  
   time.sleep(0.01)  # Allow time for EEPROM to complete the write operation  
def read_byte(bus, device_address, memory_address):  
   # Split 16-bit memory address into two bytes  
   addr_msb = (memory_address >> 8) & 0xFF  # Most significant byte  
   addr_lsb = memory_address & 0xFF         # Least significant byte  
   bus.write_i2c_block_data(device_address, addr_msb, [addr_lsb])  
   return bus.read_byte(device_address)  
def main():  
   with SMBus(3) as bus:  # Ensure correct bus number  
       print(f"Writing value 0x{VALUE_TO_WRITE:02X} to address 0x{MEMORY_ADDRESS:04X}")  
       write_byte(bus, DEVICE_ADDRESS, MEMORY_ADDRESS, VALUE_TO_WRITE)  
       
       time.sleep(0.1)  # Short delay  
       
       print("Reading value back...")  
       read_value = read_byte(bus, DEVICE_ADDRESS, MEMORY_ADDRESS)  
       print(f"Read value: 0x{read_value:02X}")  
       
       if read_value == VALUE_TO_WRITE:  
           print("Success! Read value matches written value.")  
       else:  
           print("Error: Read value does not match written value.")  
if __name__ == "__main__":  
   main()

 

- Save the above code as a eeprom_test.py script in the i2c_project folder.


- In the terminal, run the following script using to see the output.

python3 eeprom_test.py

 


- Use a logic analyzer to observe the waveforms of writing and reading, which is comply with the datasheet specifications.

 

Write value 0x42 at address 0x0000

 

Read value from address 0x0000

 

FAQ

Q. From the waveforms for writing and reading showed by the logic analyzer, and the I2C address of the AT24C256 should be 0x80. Why is it 0x50 here?

A. 

 

I²C Address Format

 

The I²C bus uses 7-bit addresses, with most devices, including the AT24C256, using a 7-bit address. An additional 1-bit read/write (R/W) bit is added to the 7-bit address, forming an 8-bit data transmission.

 

Explanation of AT24C256 Address

 

- 8-bit Address (including R/W bit): The write operation address for the AT24C256 is typically represented as 0xA0.


- 7-bit Address: In reality, the 7-bit address is 0x50. This is because 0xA0 shifted right by one bit (or divided by 2) results in 0x50.

 

Display Method of i2cdetect CMD

 

- The i2cdetect displays 7-bit addresses by default. Therefore, the address you detect using i2cdetect is 0x50.


- During actual communication, the I²C bus shifts the 7-bit address left by one bit and adds the read/write bit at the lowest position, making the write operation address 0xA0 and the read operation address 0xA1.

 

Another example

Read the voltage values of each channel of the ADS1115 16bit ADC module respectively.

from smbus2 import SMBus
import time

DEVICE_ADDRESS = 0x48

REG_CONVERSION = 0x00
REG_CONFIG = 0x01

CONFIG_OS_SINGLE = 0x8000
CONFIG_PGA_6_144V = 0x0000
CONFIG_MODE_SINGLE = 0x0100
CONFIG_DR_128SPS = 0x0080
CONFIG_COMP_DISABLE = 0x0003

MUX_CHANNEL = {
    0: 0x4000,
    1: 0x5000,
    2: 0x6000,
    3: 0x7000
}


def write_register(bus, device_address, register, value):
    msb = (value >> 8) & 0xFF
    lsb = value & 0xFF
    bus.write_i2c_block_data(device_address, register, [msb, lsb])


def read_register(bus, device_address, register):
    data = bus.read_i2c_block_data(device_address, register, 2)
    value = (data[0] << 8) | data[1]
    return value


def read_channel(bus, device_address, channel):

    config = (
        CONFIG_OS_SINGLE |
        MUX_CHANNEL[channel] |
        CONFIG_PGA_6_144V |
        CONFIG_MODE_SINGLE |
        CONFIG_DR_128SPS |
        CONFIG_COMP_DISABLE
    )

    write_register(bus, device_address, REG_CONFIG, config)

    time.sleep(0.01)

    raw = read_register(bus, device_address, REG_CONVERSION)

    if raw > 0x7FFF:
        raw -= 65536

    voltage = raw * 6.144 / 32768.0

    return voltage


def main():

    with SMBus(3) as bus:

        print("Start reading ADS1115...\n")

        while True:

            v0 = read_channel(bus, DEVICE_ADDRESS, 0)
            v1 = read_channel(bus, DEVICE_ADDRESS, 1)
            v2 = read_channel(bus, DEVICE_ADDRESS, 2)
            v3 = read_channel(bus, DEVICE_ADDRESS, 3)

            print(
                f"AIN0: {v0:.4f} V | "
                f"AIN1: {v1:.4f} V | "
                f"AIN2: {v2:.4f} V | "
                f"AIN3: {v3:.4f} V"
            )

            time.sleep(1)


if __name__ == "__main__":
    main()

 

Result: