This thread has been locked.

If you have a related question, please click the "Ask a related question" button in the top right corner. The newly created question will be automatically linked to this question.

TM4C1230H6PM: Sending Data Using a PCF8574T I2C I/O Expander to an HD4478 LCD

Part Number: TM4C1230H6PM

Hello, I'm trying to send some characters to the screen of an HD4478 LCD that has an I2C backpack. The LCD itself is attached to a 5V power supply from an Arduino. I am attempting to perform initialization on the LCD by using the internal reset circuit so I can set all of it's operations to 4-bt mode since the I2C expander will condense the number of data bits that can be sent at once from 8 to 4. Here's an image from the LCD datasheet describing the process:

Furthermore, I'm trying to follow the instructions using the internal reset circuit to set the mode to 4-bit mode with 2 lines of space for characters and 5x10 font with a blinking cursor that enters from the left side of the screen. Here's the datasheet page that describes the process:



The issue is currently when I attempt to execute these instructions in the order described I see nothing on the LCD, only that it's backlight is on. 

The first weird issue I ran into was initializing the appropriate GPIO_PORTE pins (4/5) to act as I2C pins for the I2C2 module. I had to manually use the HWREG macro with the GPIO_O_PUR register to initialize a standard weak pull up resistor to both pins. I tried using GPIOPadConfigSet() with GPIO_PIN_TYPE_STD_WPU, which I thought would do the exact same thing, but I found different behaviors. Calling that function would not set my clock and data lines to high before I began transmission,  and nothing would send (I tested this by sending 0x00, which just turns off the backlight). Using HWREG with GPIO_O_PUR actually set the data and clock lines high before transmission, and sending 0x00 this way would actually turn off the backlight:

void GPIOE_enable()
{
	SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOE);
	GPIOPinConfigure(GPIO_PE4_I2C2SCL);
	GPIOPinConfigure(GPIO_PE5_I2C2SDA);
	GPIOPinTypeI2C(GPIO_PORTE_BASE,GPIO_PIN_5);
	GPIOPinTypeI2CSCL(GPIO_PORTE_BASE,GPIO_PIN_4);
	HWREG(GPIO_PORTE_BASE + GPIO_O_PUR) = (HWREG(GPIO_PORTE_BASE + GPIO_O_PUR) | GPIO_PIN_4 | GPIO_PIN_5);
}

I initialized my I2C2 module here:

void I2C_enable()
{
	SysCtlPeripheralEnable(SYSCTL_PERIPH_I2C2);
	SysCtlClockSet(SYSCTL_SYSDIV_5|SYSCTL_USE_PLL|SYSCTL_XTAL_16MHZ|SYSCTL_OSC_MAIN);
	I2CMasterEnable(I2C2_BASE);
	I2CMasterInitExpClk(I2C2_BASE,SysCtlClockGet(),false);
	I2CMasterSlaveAddrSet(I2C2_BASE,0x27,false);
}

The biggest part of my confusion, and where I think I could be going wrong, is how to format the data I'm sending over to the LCD. I unfortunately do not have a scope or logic analyzer, but I know based on the fact I can transmit 0x00 and it will turn the backlight off *something* must be able to go over the lines. Since I am using an I2C I/O expander, I am constricted to sending just 4 bits at a time. With the HD4478 LCD, there exists an EN (enable) pin. Pulsing this (sending the data/command byte with EN high, and then sending again with EN low) will cause the HD4478 to "lock in" this data/command byte and actually execute it. However, now I am sending an upper and lower nibble of each byte at a time. This has made me confused- if I'm sending an upper and lower nibble, when do I pulse enable? Currently I have it configured such that I pulse EN for the high nibble, and then pulse EN for the low nibble of every byte I send. This means every byte now requires, in my code, 4 transmissions- the upper nibble with E high, the upper nibble with E low, the lower nibble with E high, and the lower nibble with E low. This seems like it could be incorrect but I'm not sure the right way to go about it. Another part of this I'm unsure about is the usage of BURST_SEND_START/CONT/STOP. Every time I transmit the 4 transmissions that represent a singular byte, the upper nibble with E high (first transmission) has parameter 2 of I2CMasterControl set to I2C2_MASTER_CMD_BURST_SEND_START, and the final transmission, the lower nibble with E low, set to I2C_MASTER_CMD_BURST_SEND_STOP

Here are the methods that describe this transmission method:

//send upper 4 bits of command with E set to High, and with backlight on
//Since this is the beginning of the new command, parameter 2 of I2CMasterControl is set to "BURST_SEND_START"
void expander_write_upper_nibble(uint8_t data)
{
	I2CMasterDataPut(I2C2_BASE,PULSE_HIGH);
	I2CMasterControl(I2C2_BASE,I2C_MASTER_CMD_BURST_SEND_START);
	while (I2CMasterBusy(I2C2_BASE) & I2CMasterBusBusy(I2C2_BASE));
}

//send lower 4 bits of command with E set to High, and with backlight on
//Since this is the beginning of the new command, parameter 2 of I2CMasterControl is set to "BURST_SEND_CONT"

void expander_write_lower_nibble(uint8_t data)
{
	I2CMasterDataPut(I2C2_BASE,PULSE_HIGH);
	I2CMasterControl(I2C2_BASE,I2C_MASTER_CMD_BURST_SEND_CONT);
	while (I2CMasterBusy(I2C2_BASE) & I2CMasterBusBusy(I2C2_BASE));
}

//send command with E set to low and with back light on
void pulse_enable_upper_nibble(uint8_t data)
{
	I2CMasterDataPut(I2C2_BASE,PULSE_LOW);
	I2CMasterControl(I2C2_BASE,I2C_MASTER_CMD_BURST_SEND_CONT);
}

//after sending the lower nibble with the EN high, send again with EN low (pulsing enable)
//this is the final data we should be sending to the shift register. We can pass the send state as 
// I2C2_MASTER_CMD_BURST_SEND_STOP
void pulse_enable_lower_nibble(uint8_t data)
{
	I2CMasterDataPut(I2C2_BASE,PULSE_LOW);
	I2CMasterControl(I2C2_BASE,I2C_MASTER_CMD_BURST_SEND_STOP);
}


//send upper 4 bits w/E high and then send w/ E low
void I2C_write_upper_nibble(uint8_t byte)
{			
	expander_write_upper_nibble(byte);
	pulse_enable_upper_nibble(byte);
}
//write lower nibble pulse enable
void I2C_write_lower_nibble(uint8_t byte)
{			
	expander_write_lower_nibble(byte);
	pulse_enable_lower_nibble(byte);
}


//write upper 4 bits + pulse, write lower 4 bits + pulse E
void send(uint8_t data, int mode)
{
	uint8_t byte_shifted;
	uint8_t byte_upper_nibble;
	uint8_t byte_lower_nibble;
	byte_shifted = data << 4;
	byte_upper_nibble = data & 0xF0;
	byte_lower_nibble = byte_shifted & 0xF0;
	I2C_write_upper_nibble(byte_upper_nibble | mode);
	I2C_write_lower_nibble(byte_lower_nibble | mode);
}

//RS set 0
void command(uint8_t data)
{
	send(data,0);
}

//RS set to 1
void data(uint8_t data)
{
	send(data,1);
}


Finally, here is my LCD_init method which attempts to follow the instructions for the internal reset circuit

/*initializationCommands[0] = 4 bit mode / 2 Lines / 5 x 10 dot display
						[1] = cursor on / display on / blink on (0x08 + 0x04 + 0x02 + 0x01)
						[2] = Enter cursor left 
*/
void I2C_LCD_init()
	
{	uint8_t i;
	uint8_t initializationCommands[3] = {0x30,0xE,0x06};
	//12.5 ms delay
	delay();
	I2CMasterDataPut(I2C2_BASE,0x20 | E | LCD_BACKLIGHT);
	while (I2CMasterBusy(I2C2_BASE) & I2CMasterBusBusy(I2C2_BASE));
	I2CMasterDataPut(I2C2_BASE,(0x20 & ~ E) | LCD_BACKLIGHT);
	I2CMasterControl(I2C2_BASE,I2C_MASTER_CMD_BURST_SEND_STOP);
	while (I2CMasterBusy(I2C2_BASE) & I2CMasterBusBusy(I2C2_BASE));
	
	for(i = 0; i<3; i++) {
		send(initializationCommands[i],0);
	}
}


And finally, for even more clarity, the definitions for the commands that can be sent to the HD4478 LCD:

#define LCD_CLEARDISPLAY 0x01
#define LCD_RETURNHOME 0x02
#define LCD_ENTRYMODESET 0x04
#define LCD_DISPLAYCONTROL 0x08
#define LCD_CURSORSHIFT 0x10
#define LCD_FUNCTIONSET 0x20
#define LCD_SETCGRAMADDR 0x40
#define LCD_SETDDRAMADDR 0x80

// flags for display entry mode
#define LCD_ENTRYRIGHT 0x00
#define LCD_ENTRYLEFT 0x02
#define LCD_ENTRYSHIFTINCREMENT 0x01
#define LCD_ENTRYSHIFTDECREMENT 0x00

// flags for display on/off control
#define LCD_DISPLAYON 0x04
#define LCD_DISPLAYOFF 0x00
#define LCD_CURSORON 0x02
#define LCD_CURSOROFF 0x00
#define LCD_BLINKON 0x01
#define LCD_BLINKOFF 0x00

// flags for display/cursor shift
#define LCD_DISPLAYMOVE 0x08
#define LCD_CURSORMOVE 0x00
#define LCD_MOVERIGHT 0x04
#define LCD_MOVELEFT 0x00

// flags for function set
#define LCD_8BITMODE 0x10
#define LCD_4BITMODE 0x00
#define LCD_2LINE 0x08
#define LCD_1LINE 0x00
#define LCD_5x10DOTS 0x04
#define LCD_5x8DOTS 0x00

// flags for backlight control
#define LCD_BACKLIGHT 0x08
#define LCD_NOBACKLIGHT 0x00

  • Hello Elliott,

    The first weird issue I ran into was initializing the appropriate GPIO_PORTE pins (4/5) to act as I2C pins for the I2C2 module. I had to manually use the HWREG macro with the GPIO_O_PUR register to initialize a standard weak pull up resistor to both pins. I tried using GPIOPadConfigSet() with GPIO_PIN_TYPE_STD_WPU, which I thought would do the exact same thing, but I found different behaviors. Calling that function would not set my clock and data lines to high before I began transmission,  and nothing would send (I tested this by sending 0x00, which just turns off the backlight). Using HWREG with GPIO_O_PUR actually set the data and clock lines high before transmission, and sending 0x00 this way would actually turn off the backlight:

    What likely happened here is that the I2C GPIO configuration changed your settings. 

    Why are you trying to set the pin like that? It should just be open drain as this is how the GPIO I2C Config function correctly configures the I/O:

    GPIOPadConfigSet(ui32Port, ui8Pins, GPIO_STRENGTH_2MA, GPIO_PIN_TYPE_OD);

    Are you trying to replace on board pull-ups with the internal weak pull-ups for the I2C bus? If so that won't work, you need to use Open Drain mode with external pull-up resistors.

    The biggest part of my confusion, and where I think I could be going wrong, is how to format the data I'm sending over to the LCD. I unfortunately do not have a scope or logic analyzer, but I know based on the fact I can transmit 0x00 and it will turn the backlight off *something* must be able to go over the lines.

    This whole segment is not specific to our TM4C microcontroller and it is outside of the scope of the support we provide. I don't know anything about the I2C expander or the LCD screen. We cannot help with application specific questions like how to control those external devices properly based on their protocols. Maybe a community member can help however.

    I would strongly urge you get a scope or logic analyzer, that's really the only way to efficiently solve this problem otherwise you are just going to be blindly guessing and hope you stumble upon the correct process. If you do have questions about how to make the I2C bus operate the way you need (i.e. you can tell me for sure what you need to see as an output) I still would need the scope / logic analyzer shots to see what is being output instead of your desired result to be able to help in any manner.

    Best Regards,

    Ralph Jacobi

  • Hey Ralph, thanks for the response. I hope to be coming into a logic analyzer over the next week or so which will make debugging this exponentially easier.

    Why can't I use the internal pull-ups with the open drain configuration?

  • Hi Elliott,

    They are simply not strong enough to drive an I2C bus in almost all cases. Hence the nomenclature 'weak pull-up'. Usually you want pull-ups less than 10kOhm and typ. internal pull-ups are 20kOhm - far too weak for an I2C bus unless you have exceptionally low bus capacitance which is very rare.

    Here is an app note with some details: https://www.ti.com/lit/slva689

    Usually I see pull-ups between 1.8kohm to 4.7kOhm used.

    Best Regards,

    Ralph Jacobi

  • Hi Ralph- sorry for the very late reply. but I'm a little inexperienced with applying pull-ups to lines. Could this be done by connecting the I2C SCL/SDA lines from the master to a breadboard, and then using a resistor to connect the lines from the master to SDA/SCL lines going into the LCD?

  • Hello Elliot,

    You would tie one end of the resistor to the I2C signal node, and the other end to 3.3V.

    This app note should explain the details of why it is needed: https://www.ti.com/lit/an/slva704/slva704.pdf

    Best Regards,

    Ralph Jacobi