Using Digital Pins for I2C Communication with a 16×2 LCD on Arduino uno
In embedded systems, it is common to use dedicated I2C pins for communication with peripherals like LCD displays. However, there are scenarios where you might want to use digital pins for I2C communication, especially when dedicated I2C pins are occupied or unavailable. This blog explores using ATmega88 (similar to Arduino) to interface a 16×2 I2C LCD using digital pins for I2C communication.
Why Use Digital Pins for I2C?
- Flexibility: Using digital pins for I2C allows flexibility in pin selection, which is useful when the default I2C pins are in use.
- Pin Multiplexing: In complex projects, some microcontroller pins may serve multiple functions. Using digital pins for I2C is an efficient way to manage pin usage.
- Breadboarding: During prototyping on breadboards, it’s often easier to use adjacent digital pins rather than running long wires across the board.
Understanding the Hardware Setup
For this setup, we are using the ATmega88 microcontroller, configured with a 14.7456 MHz crystal oscillator. The 16×2 I2C LCD is connected to the microcontroller using digital pins PB0 (SDA) and PB1 (SCL). The I2C communication is handled using a bit-banging technique, allowing the use of general-purpose digital pins for I2C.
Pin Configuration:
- SDA (Serial Data Line): Connected to PB0 (physical pin 12).
- SCL (Serial Clock Line): Connected to PB1 (physical pin 13).
Required Libraries and Tools
For this project, you will need:
- AVR-GCC: The compiler for programming AVR microcontrollers.
- AVR-Libc: A standard library for AVR microcontrollers.
- Multi_BitBang Library: A library that provides bit-banging capabilities for I2C communication.
Setting Up the Code
Here’s a detailed explanation of the code used to interface the LCD with ATmega88 via digital pins for I2C communication.
Step 1: Initialize I2C on Digital Pins
We use the Multi_I2CInit()
function to initialize the I2C bus on the specified digital pins:
sda_list[] = {0xB0};
– Sets PB0 as the SDA pin.scl_list[] = {0xB1};
– Sets PB1 as the SCL pin.speed_list[] = {400000L};
– Sets the I2C speed to 400 kHz.
// I2C bus info for ATmega88 using PB0 and PB1
uint8_t scl_list[] = {0xB1}; // SCL connected to PB1 (physical pin 13)
uint8_t sda_list[] = {0xB0}; // SDA connected to PB0 (physical pin 12)
int32_t speed_list[] = {400000L}; // Set speed to 400kHz
int main(void) {
DDRB &= ~((1 << PB0) | (1 << PB1)); // Set PB0 and PB1 as inputs for SDA and SCL
Multi_I2CInit(sda_list, scl_list, speed_list, 1); // Initialize the I2C bus
...
}
Step 2: Initialize the LCD
The LCD is initialized in 4-bit mode using the sendLCD()
function. This function handles communication with the LCD, sending commands and data in the required sequence.
- 4-bit Mode Initialization: The LCD is initialized to work in 4-bit mode, reducing the number of data lines required and simplifying the wiring.
- Basic LCD Commands:
0x28
: Set 4-bit mode, 2-line display, and 5×8 font.0x0C
: Display ON, cursor OFF.0x06
: Entry mode set, cursor moves to the right.0x01
: Clear display.
void initLCD() {
_delay_ms(50); // Wait for the LCD to power up
// Initialize to 4-bit mode
sendLCD(0x03, LCD_COMMAND, BACKLIGHT);
_delay_ms(5);
sendLCD(0x03, LCD_COMMAND, BACKLIGHT);
_delay_ms(5);
sendLCD(0x03, LCD_COMMAND, BACKLIGHT);
_delay_ms(5);
sendLCD(0x02, LCD_COMMAND, BACKLIGHT); // Set to 4-bit mode
_delay_ms(5);
// Function set: 4-bit mode, 2-line display, 5x8 font
sendLCD(0x28, LCD_COMMAND, BACKLIGHT);
// Display control: display on, cursor off, blink off
sendLCD(0x0C, LCD_COMMAND, BACKLIGHT);
// Entry mode set: increment and shift cursor to the right
sendLCD(0x06, LCD_COMMAND, BACKLIGHT);
// Clear display
sendLCD(0x01, LCD_COMMAND, BACKLIGHT);
_delay_ms(5);
}
Step 3: Send Data to the LCD
The sendLCD()
function sends data and commands to the LCD. It uses a combination of data and control signals:
- Enable (E): Signals the LCD to latch the data present on its data pins.
- Register Select (RS): Distinguishes between commands (RS=0) and data (RS=1).
- Backlight Control: Controls the LCD backlight by toggling the appropriate bit.
void sendLCD(uint8_t value, uint8_t mode, uint8_t backlight) {
uint8_t data[4];
data[0] = (value & 0xF0) | mode | backlight | 0x04; // High nibble with enable
data[1] = (value & 0xF0) | mode | backlight; // High nibble without enable
data[2] = ((value << 4) & 0xF0) | mode | backlight | 0x04; // Low nibble with enable
data[3] = ((value << 4) & 0xF0) | mode | backlight; // Low nibble without enable
// Write the sequence to the LCD
Multi_I2CWrite(LCD_BUS, LCD_ADDR, data, 4);
_delay_us(100); // Delay to allow command processing
}
Step 4: Display Text on the LCD
The printLCD()
function takes a string and displays it on the LCD starting from the first line. It sends each character of the string to the LCD in sequence.
void printLCD(const char *text) {
sendLCD(0x80, LCD_COMMAND, BACKLIGHT); // Set DDRAM address to 0 (beginning of first line)
while (*text) {
sendLCD(*text++, LCD_DATA, BACKLIGHT);
}
}
Benefits of Using Digital Pins for I2C
- Resource Management: Frees up dedicated I2C pins for other critical functions.
- Customization: Allows for custom implementations where specific pin assignments are required.
- Simplicity in Wiring: Helps in situations where the physical layout of the hardware makes it more convenient to use certain pins.
Conclusion
Using digital pins for I2C communication with a 16×2 LCD provides flexibility and makes pin management more efficient in complex embedded systems. By using a bit-banging approach with the Multi_BitBang
library, you can easily implement I2C on any available digital pins of the ATmega88 or Arduino. This approach ensures that you are not constrained by the limitations of dedicated hardware I2C pins, allowing more versatile designs in your projects.
Whether you’re prototyping or creating a final product, understanding how to implement I2C on digital pins expands your capability to handle various interfacing challenges in embedded systems.