MSP430 - I2C LCD1602 Interface

MSP430 LCD interface via 2-wire I2C

This is an addition to the LCD example posted before:

https://forum.allaboutcircuits.com/ubs/msp430-lcd-example.568/

Background

The 16-character, 2-line character display LCD1602 is very popular and readily available from multiple sources.
1690679593972.png

The display itself is controlled by the Hitachi HD44780U controller. In order to take full advantage of the LCD and its capabilities, you need to read the HD44780U datasheet (see References below).

The basic 16x2 LCD module is supplied by different manufacturers, in different styles and configurations, for example:
HYUNDAI HC16201-A
OPTREX DMC16207
SHARP LM16255

The basic LCD1602 comes with either a 14-pin or 16-pin single row 0.1" pitch pin connection. The extra two pins, labeled A and K are for 5V supply to the LED backlight. The module requires a parallel interface with your MCU, D7-D0, RS, RW, and E, in addition to Vcc, GND, and Contrast control.

1690679643658.png


In order to interface with the 8-bit data port, it requires a total of 11 I/O pins of the MCU which is a huge demand for most applications using an MCU with low I/O pin count. The HD44780 has a 4-bit mode where 8 bits are transferred as two 4-bit nybbles. This reduces the interface pin count to 7.

LCD Serial Interface

The obvious solution to the high pin count problem is to go serial. On a small MCU with built-in UART module, one can create a dedicated serial to LCD interface. This reduces the interface to a single serial UART TX signal at whatever baud rate one chooses.

A second option is to use I2C. This is a 2-wire serial interface consisting of a clock signal SCL and data signal SDA. The huge advantage of I2C is that one can implement a network of controllers and devices on the same two wires. For example, one MCU can drive multiple LCD modules.

Fortunately, there already exits an I2C to parallel IC that does the interface for you. This is the TI PCF8574 port expander.
You can buy the PCF8574 module separately or you can buy the PCF8574 already mounted on the LCD1602 module. Photos are shown above.

Programming the LCD1602 I2C module

There are two parts to this programming exercise.
1) I2C interface
2) LCD interface

I2C Protocol

Programming the I2C interface is MCU specific. It will be different for each brand and family of MCU being used. For this reason, I shall not give any programming examples besides the MSP430 example.

I2C protocol is very specific and needs to be adhered to carefully. If you are testing the I2C interface alone, please note the following.

1) SCL and SDA are network capable. SCL and SDA signals are driven by open collector or open drain outputs. Hence they both need to be pulled up to Vcc via a load resistor. A resistor value of 1k-10kΩ is suitable.
2) The idle state of SCL and SDA is high. All I2C controllers must sample SCL and SDA to ensure that there is no bus activity before they can start transmitting.
3) The master controller begins by setting SDA low.
4) The default slave address of the PCF8574 is 0x27 when A2-A0 are pulled high. By placing jumpers to GND at A2-A0, you can set the slave address to any address in the range 0x20 to 0x27.
5) The PCF8574 does not always work in between code changes. You may have to power down the PCF8574 during testing and debugging.

Here is a sample oscilloscope view of I2C SCL and SDA.
SCL clock period is 8.2μs.

SCL and SDA signals are open drain. Hence they require external pull-up resistors to bring them HIGH during the IDLE STATE.
The master begins by setting SDA LOW. After eight SCL clock pulses, the master releases SDA and waits for the slave device to acknowledge with a LOW on SDA. The master then proceeds to send the 8-bit data.
I2C.jpg

Here is an excellent tutorial on how the I2C protocol works.
https://deepbluembedded.com/i2c-communication-protocol-tutorial-pic/

LCD HD44780U Protocol

Programming the LCD module ought to be the same across all platforms. In many cases, both I2C and LCD code libraries are supplied by the various platforms or are readily available on the web. But what do you do when there is no library available or the LCD does not respond? This is when you have to get out the oscilloscope and do the debugging on your own.

The most common situation is the LCD shows nothing.

Step #1 - Set the contrast voltage

1690679678644.png


You might not be able to see any characters displayed because the contrast setting is not correct.
On power on, you should be able to see a row of rectangles. Adjust the contrast voltage trimmer so that the rectangles are visible against the background. If there is no contrast trimpot, a 1kΩ resistor from LCD pin-3 (Vo) to GND is a good place to start.

Step #2 - Run the LCD initialization code

If the row of rectangles disappear, then something is happening.
If the row of rectangles do not change, then the LCD initialization code is not working.

Getting the LCD initialization code working properly is the critical and most difficult part of this exercise. Because of delay requirements in the HD44780U controller, delays might be required between LCD commands. Sometimes running the initialization code twice will fix the problem.

Here is an excellent tutorial on interfacing a Microchip PIC16F877A to the PCF8574 expander.

1690679710455.png


https://deepbluembedded.com/interfacing-i2c-lcd-16x2-tutorial-with-pic-microcontrollers-mplab-xc8/

In this example, note that PCF8574 P0-P2 interface to RS, RW, E, respectively.
P4-P7 interface to LCD D4-D7, respectively.
P3 is not used.

SunFounder LCD1602 Schematics

The SunFounder LCD1602 connections are the same as above. P3 is used to enable the LED backlight on the 16-pin LCD module. The jumper on the edge of the PCF8574 disconnects the power to the LED backlight.

SunFounder LCD1602.jpg



MSP430G2553 Code Example

C:
// SunFounder I2C LCD 1602
// MSP430G2553 I2C interface
// MrChips - 2023.07.29

//-------------------------------------
// includes
//-------------------------------------

#include "io430.h"

//-------------------------------------
// defines
//-------------------------------------

#define LED1 P1OUT_bit.P0
#define LED2 P1OUT_bit.P6

#define ON  1
#define OFF 0

#define SLAVE_ADDRESS 0x27
#define LCD_RS BIT0
#define LCD_RW BIT1
#define LCD_E  BIT2
#define LCD_BACKLITE BIT3

#define DELAY_200us    220
#define DELAY_500us    550
#define DELAY_1ms     1125
#define DELAY_1s   1125000

char IRQ_count;

//-------------------------------------
// functions
//-------------------------------------

void delay(unsigned long int d)
{
  while(--d);
}

//-------------------------------------
// I2C functions
//-------------------------------------

void I2C_Init(void)
{
  while (UCB0CTL1 & UCTXSTP);
  UCB0CTL1 |= UCSWRST;
  UCB0CTL0 = UCMST + UCMODE_3 + UCSYNC;
  UCB0CTL1 = UCSSEL_2 + UCSWRST;
  UCB0BR0 = 64;
  UCB0BR1 = 0;
  UCB0I2CSA = SLAVE_ADDRESS;
  UCB0CTL1 &= ~UCSWRST;        // clear software reset
  IE2 |= UCB0TXIE;             // enable interrupt
}

void I2C_Send(unsigned int d)
{
  IRQ_count = 0;
  UCB0TXBUF = d;
  while (UCB0CTL1 & UCTXSTP);
  UCB0CTL1 |= UCTR + UCTXSTT;   // start I2C
  delay(DELAY_200us);
}

#pragma vector = USCIAB0TX_VECTOR
__interrupt void USCIAB0TX_ISR(void)
{
  // first IRQ is received when START on SDA is sent
  ++IRQ_count;
  if (IRQ_count > 1)
  {
    UCB0CTL1 |= UCTXSTP;    // send STOP
  }
  IFG2 &= ~UCB0TXIFG;  // clear flag
}

//-------------------------------------
// LCD functions
//-------------------------------------

void LCD_cmd(char ch)
{
  char d;
  d = (ch & 0xF0) + LCD_BACKLITE; // send upper nybble
  I2C_Send(d);
  I2C_Send(d + LCD_E);            // pulse E high-low
  I2C_Send(d);
  I2C_Send(LCD_BACKLITE);

  d = ((ch << 4) & 0xF0) + LCD_BACKLITE;  // send lower nybble
  I2C_Send(d);
  I2C_Send(d + LCD_E);    // pulse E high-low
  I2C_Send(d);
  I2C_Send(LCD_BACKLITE);
  delay(DELAY_1ms);
}

void LCD_ch(char ch)
{
  char d;
  d = (ch & 0xF0) + LCD_RS + LCD_BACKLITE; // send upper nybble
  I2C_Send(d);
  I2C_Send(d + LCD_E);                     // pulse E high-low
  I2C_Send(d);
  I2C_Send(LCD_RS + LCD_BACKLITE);

  d = ((ch << 4) & 0xF0) + LCD_RS + LCD_BACKLITE;  // send lower nybble
  I2C_Send(d);
  I2C_Send(d + LCD_E);              // pulse E high-low
  I2C_Send(d);
  I2C_Send(LCD_RS + LCD_BACKLITE);
}

void LCD_init(void)
{
  LCD_cmd(0x02);  // home
  LCD_cmd(0x28);  // dual line, 4 bits
  LCD_cmd(0x06);  // increment mode
  LCD_cmd(0x0C);  // cursor turned off
  LCD_cmd(0x10);  // cursor right
  LCD_cmd(0x01);  // clear display
}

//-------------------------------------
// Initialization
//-------------------------------------

void Init_HW(void)
{
  P1DIR  = 0x01;
  P2DIR  = 0x00;
  P1OUT  = 0x00;
  P2OUT  = 0x00;

  // assign P1.6 and P1.7 to I2C
  P1SEL  = BIT6 + BIT7;
  P1SEL2 = BIT6 + BIT7;

  I2C_Init();
}

void Init(void)
{
  // Stop watchdog timer to prevent time out reset
  WDTCTL = WDTPW + WDTHOLD;

  // set DCO - Digitally-Controlled Oscillator frequency
  // change from 1MHz to 8MHz calibrated
  BCSCTL1 = CALBC1_8MHZ;
  DCOCTL  = CALDCO_8MHZ;

  Init_HW();
  __enable_interrupt();
}

//-------------------------------------
// main
//-------------------------------------

void main(void)
{
  Init();
  LCD_init();

  while(1)
  {
    LCD_ch('A');
    delay(DELAY_1s);
  }

}

//-------------------------------------
// end of main
//-------------------------------------

MSP430 Code Example without using Interrupts

C:
// SunFounder I2C LCD 1602
// MSP430G2553 I2C interface
// MrChips - 2023.07.29

//-------------------------------------
// includes
//-------------------------------------

#include "io430.h"

//-------------------------------------
// defines
//-------------------------------------

#define LED1 P1OUT_bit.P0
#define LED2 P1OUT_bit.P6

#define ON  1
#define OFF 0

#define SLAVE_ADDRESS 0x27
#define LCD_RS BIT0
#define LCD_RW BIT1
#define LCD_E  BIT2
#define LCD_BACKLITE BIT3

#define DELAY_100us    110
#define DELAY_200us    220
#define DELAY_500us    550
#define DELAY_1ms     1125
#define DELAY_1s   1125000

//-------------------------------------
// functions
//-------------------------------------

void delay(unsigned long int d)
{
  while(--d);
}

//-------------------------------------
// I2C functions
//-------------------------------------

void I2C_Init(void)
{
  while (UCB0CTL1 & UCTXSTP);
  UCB0CTL1 |= UCSWRST;
  UCB0CTL0 = UCMST + UCMODE_3 + UCSYNC;
  UCB0CTL1 = UCSSEL_2 + UCSWRST;
  UCB0BR0 = 64;
  UCB0BR1 = 0;
  UCB0I2CSA = SLAVE_ADDRESS;
  UCB0CTL1 &= ~UCSWRST;        // clear software reset
}

void I2C_Send(unsigned int d)
{
  while (UCB0CTL1 & UCTXSTP);
  UCB0CTL1 |= UCTR + UCTXSTT;   // start I2C
  UCB0TXBUF = d;                       //  now load data buffer
  delay(DELAY_100us);
   UCB0CTL1 |= UCTXSTP;    // send STOP
}

//-------------------------------------
// LCD functions
//-------------------------------------

void LCD_cmd(char ch)
{
  char d;
  d = (ch & 0xF0) + LCD_BACKLITE; // send upper nybble
  I2C_Send(d);
  I2C_Send(d + LCD_E);            // pulse E high-low
  I2C_Send(d);
  I2C_Send(LCD_BACKLITE);

  d = ((ch << 4) & 0xF0) + LCD_BACKLITE;  // send lower nybble
  I2C_Send(d);
  I2C_Send(d + LCD_E);    // pulse E high-low
  I2C_Send(d);
  I2C_Send(LCD_BACKLITE);
  delay(DELAY_1ms);
}

void LCD_ch(char ch)
{
  char d;
  d = (ch & 0xF0) + LCD_RS + LCD_BACKLITE; // send upper nybble
  I2C_Send(d);
  I2C_Send(d + LCD_E);                     // pulse E high-low
  I2C_Send(d);
  I2C_Send(LCD_RS + LCD_BACKLITE);

  d = ((ch << 4) & 0xF0) + LCD_RS + LCD_BACKLITE;  // send lower nybble
  I2C_Send(d);
  I2C_Send(d + LCD_E);              // pulse E high-low
  I2C_Send(d);
  I2C_Send(LCD_RS + LCD_BACKLITE);
}

void LCD_init(void)
{
  LCD_cmd(0x02);  // home
  LCD_cmd(0x28);  // dual line, 4 bits
  LCD_cmd(0x06);  // increment mode
  LCD_cmd(0x0C);  // cursor turned off
  LCD_cmd(0x10);  // cursor right
  LCD_cmd(0x01);  // clear display
}

//-------------------------------------
// Initialization
//-------------------------------------

void Init_HW(void)
{
  P1DIR  = 0x01;
  P2DIR  = 0x00;
  P1OUT  = 0x00;
  P2OUT  = 0x00;

  // assign P1.6 and P1.7 to I2C
  P1SEL  = BIT6 + BIT7;
  P1SEL2 = BIT6 + BIT7;

  I2C_Init();
}

void Init(void)
{
  // Stop watchdog timer to prevent time out reset
  WDTCTL = WDTPW + WDTHOLD;

  // set DCO - Digitally-Controlled Oscillator frequency
  // change from 1MHz to 8MHz calibrated
  BCSCTL1 = CALBC1_8MHZ;
  DCOCTL  = CALDCO_8MHZ;

  Init_HW();
}

//-------------------------------------
// main
//-------------------------------------

void main(void)
{
  Init();
  LCD_init();

  while(1)
  {
    LCD_ch('A');
    delay(DELAY_1s);
  }

}

//-------------------------------------
// end of main
//-------------------------------------
References:

HD44780U LCD controller
https://www.sparkfun.com/datasheets/LCD/HD44780.pdf

PCF8574 I2C to 8-bit parallel port expander
https://www.ti.com/lit/ds/symlink/pcf8574.pdf

PIC to LCD examples
https://deepbluembedded.com/i2c-communication-protocol-tutorial-pic/
https://deepbluembedded.com/interfacing-i2c-lcd-16x2-tutorial-with-pic-microcontrollers-mplab-xc8/


MrChips - 2023.07.29
  • Like
Reactions: maazzuberi

Blog entry information

Author
MrChips
Views
1,610
Last update

More entries in General

More entries from MrChips

Share this entry

Top