Blog Datasheets Home About me Clients My work Services Contact

G2Labs Grzegorz Grzęda

I2C Communication with AVR Atmega-328: A Step-by-Step Tutorial

March 4, 2024

I2C Communication with AVR Atmega-328: A Step-By-Step Tutorial

The Inter-Integrated Circuit (I2C) protocol is widely used for communication between microcontrollers and peripheral devices in embedded systems. It is a convenient and efficient method that allows multiple devices to communicate over just two wires. In this tutorial, we will explore the basics of I2C communication using an AVR Atmega-328 microcontroller.

What is I2C?

I2C is a serial communication protocol developed by Philips in the 1980s. It provides a simple and elegant master-slave communication scheme. I2C uses two wires for communication: a clock line (SCL) and a data line (SDA).

I2C supports multi-master communication, meaning multiple microcontrollers can act as masters on the bus and interact with slave devices. The protocol utilizes a 7-bit or 10-bit address scheme to uniquely identify devices on the bus.

Setting Up I2C Hardware

To use I2C, we need to connect our microcontroller to peripheral devices. The Atmega-328 has built-in hardware support for I2C communication, which is available on the two pins: SDA (Analog pin 4) and SCL (Analog pin 5).

To establish reliable communication, pull-up resistors should be connected to both SDA and SCL lines. These resistors ensure that the lines are pulled high when they are not being actively driven by devices. A typical value for these resistors is between 2.2kΩ and 10kΩ.

Enabling I2C Communication

Before we can start using the I2C interface on the Atmega-328, we need to enable it in our code. The AVR libraries provide a simple API to configure and utilize I2C functionality.

To begin, we need to include the avr/io.h and util/twi.h headers in our code. The avr/io.h header file provides access to general-purpose input/output (GPIO) pins, while util/twi.h gives us access to I2C-specific functions.

1
2
#include <avr/io.h>
#include <util/twi.h>

Next, we need to define the I2C bit rate. The bit rate determines the speed of communication. The Atmega-328 supports standard-mode (100kHz) and fast-mode (400kHz) communication. We can choose between these two by defining the appropriate clock frequency.

1
2
3
#define F_CPU 16000000UL // Clock frequency in Hz
#define I2C_BIT_RATE 100000 // Desired I2C bit rate in Hz
#define I2C_PRESCALER (((F_CPU / I2C_BIT_RATE) - 16) / 2 + 1)

Now, let’s configure the I2C interface by initializing the control registers and enabling I2C functionality.

1
2
3
4
5
void i2c_init() {
    TWSR = 0; // Prescaler value, set to 1 for standard I2C speed
    TWBR = I2C_PRESCALER; // Bit rate generator value
    TWCR = (1 << TWEN); // Enable I2C
}

Writing and Reading Data

To initiate data transmission, the master sends a start condition followed by the slave address and a read/write bit. If the slave acknowledges the address, the master can send or receive data.

Let’s take a look at an example of writing data to a slave device.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void i2c_write(uint8_t address, uint8_t data) {
    TWCR = (1 << TWINT) | (1 << TWSTA) | (1 << TWEN); // Start condition
    while ((TWCR & (1 << TWINT)) == 0); // Wait for start condition to be transmitted
    
    TWDR = (address << 1) | 0; // Slave address with write bit
    TWCR = (1 << TWINT) | (1 << TWEN); // Transmit address
    while ((TWCR & (1 << TWINT)) == 0); // Wait for address to be transmitted
    
    TWDR = data; // Data to be sent
    TWCR = (1 << TWINT) | (1 << TWEN); // Transmit data
    while ((TWCR & (1 << TWINT)) == 0); // Wait for data to be transmitted
    
    TWCR = (1 << TWINT) | (1 << TWSTO) | (1 << TWEN); // Stop condition
}

The above code initializes the I2C bus by sending a start condition. It then transmits the slave address along with the write bit and waits for the address to be transmitted. After the address is acknowledged, the function sends the data and waits for it to be transmitted. Finally, it sends a stop condition to complete the transaction.

Reading data from a slave device can be accomplished with slight modifications to the i2c_write() function. Let’s take a look at an example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
uint8_t i2c_read(uint8_t address) {
    TWCR = (1 << TWINT) | (1 << TWSTA) | (1 << TWEN); // Start condition
    while ((TWCR & (1 << TWINT)) == 0); // Wait for start condition to be transmitted
    
    TWDR = (address << 1) | 1; // Slave address with read bit
    TWCR = (1 << TWINT) | (1 << TWEN); // Transmit address
    while ((TWCR & (1 << TWINT)) == 0); // Wait for address to be transmitted
    
    TWCR = (1 << TWINT) | (1 << TWEN); // Receive data
    while ((TWCR & (1 << TWINT)) == 0); // Wait for data to be received
    
    uint8_t data = TWDR; // Received data
    
    TWCR = (1 << TWINT) | (1 << TWSTO) | (1 << TWEN); // Stop condition
    
    return data;
}

In this code, we send a start condition and transmit the slave address with the read bit. After the address is acknowledged, we switch the I2C interface to receive mode. We then wait for the data to be received, store it in a variable, and send a stop condition to end the transaction.

Conclusion

In this tutorial, we explored the basics of I2C communication using an AVR Atmega-328 microcontroller. We discussed the hardware setup required and provided a step-by-step guide to enable and utilize the I2C interface.

I2C is a versatile protocol that allows for seamless communication between microcontrollers and peripheral devices. With the knowledge gained from this tutorial, you can now incorporate I2C communication into your embedded systems and build more capable and interconnected devices. Happy coding!


References:


➡️ Advanced form handling in Flask


⬅️ Form handling in Flask


Go back to Posts.