Big-banged I2C LCD for Raspberry PI

This is a C library that you can use to connect a PCF8574 based LCD to any of your available GPIO ports. It uses software emulated (bit-banged) I2C bus.


Note this library uses wPi_soft_i2c as its backend, which is based on wiringPi. You can easily adapt it to work with a different backend by providing suitable alternatives for I2C (i2c_start, i2c_stop, i2c_send_byte and i2c_read_byte) as well as for delay (usleep). I’ve been using it in a PIC16F88 here and with an ESP8266 here.

Getting Started

For a quick start guide, please follow these steps:

If you want to use pins 8 and 9, please notice these are hardware enabled I2C pins. So make sure you unloaded i2c kernel modules before using these routines.

rmmod i2c_dev 
rmmod i2c_bcm2708

As a rule, always ensure the pins you are using to simulate I2C bus are not taken over by any other software in the background.


These are some files that you can use as a quick start guide.

If you want to compile and run them, make sure you modify lcd_create line to match your pin numbers and PCF8574’s I2C address.


This is a very basic example. It shows to you how to configure a LCD with the predefined defaults and write some text. That with only a few lines.

Example basic picture

If you want to try it, just modify the lcd_create line to set up your ports. Remember to use WiringPi numbers.


This is a basic example using 20x4 display. Note the initialization changes in two parameters: The I2C address, because PCF8574’s default address is 0x27 while PCF8574A’s is 0x3f. And the number of lines.

Example 20x4 picture

If you want to try it, just modify the lcd_create line to set up your ports and I2C address. Remember to use WiringPi numbers.


This is a slightly more complicated example. It shows to you how create a LCD, write test, reposition cursor and check for errors.

It is a clock that displays the string “Electronica y Ciencia” and the current time, updating 10 times a second.

Intermediate example picture


Reading this example you will learn how to create and show custom characters.

Custom characters picture


Reading this example you will learn the effect of UTF8 character replacement.

UTF8 characters picture


You will write some random number, bigger then screen area of a 16x2 display. Like this.

Example of reading: LCD

Then you will retrieve the cursor position and the characters from the LCD controller; even the non visible ones.

Example of reading: Program output


This is an example of backlight dimming via PWM.

Note how I used an exponential decay and growth to get a much better fadding efect than linear.


This is the most complicated example so far.

In this example you will learn to interact with two separate I2C buses. In one of the buses you will have a HTU21D, a temperature and humidity sensor. Please note that this is just an example, not a proper way to use I2C: it is a bus, so you should have connected both devices to the same I2C lines.

Example of htu21d sensor picture


There are several levels to use this library:

First of all, include header file in your main program:

#include "soft_lcd.h"

Since this is not a precompiled library, you need to add the source file of LCD and I2C to gcc compile line.

gcc -lwiringPi -o example_basic example_basic.c soft_lcd.c soft_i2c.c

Usage (level: basic)

Describe the LCD

This library assumes that:

If you don’t know what all those means, then translate it by this library works like a charm with $3 Ebay’s I2C LCD.

In order to communicate with an LCD you need to specify:

It returns a LCD structure you have to pass to every function in the libray. Note that you may have several LCD at the same time.

lcd_t *lcd_create(int scl, int sda, int addr, int lines);

It initializes a new I2C bus. Then tries to communicate with PCF8574 driver and reset the LCD to a known state. If it goes all right, it returns a new lcd_t structure with the default configuration.

You can have as many LCDs as you need, each connected to different GPIO pins, or in the same if you use different address for the driver.

If you set the wrong pin numbers, I2C bus is busy or not pulled up, your power supply is not ready o LCD driver is defective, or any other error condition, it will return NULL.

/* Create a LCD given SCL, SDA and I2C address, two lines */
lcd_t *lcd = lcd_create(23, 24, 0x3f, 2);

if (lcd == NULL) {
	printf("Cannot set-up LCD.\n");
	return 1;

This function displays the text given by string parameter in the LCD created before at the current cursor position. It overwrites the existing text.

/* Print a string */
lcd_print(lcd, "Hello World!");

You can also use lcd_printf which is similar to the standard C printf function.

/* Print random number */
lcd_printf(lcd, "Random: %d", rand());

/* Print the time: HH:MM:SS */
lcd_printf(lcd, "%02d:%02d:%02d", tm.tm_hour, tm.tm_min, tm.tm_sec);

If you want to write the text in more than one line, you will use newline character:

/* Multiline text */
lcd_print(lcd, "Electronica y\nCiencia");

/* A fancy way to sit the cursor at the next line */
lcd_print(lcd, "Energy level:\n");
lcd_print(lcd, "         \01\02\03\04\05\06\07");
UTF8 character replacement

There is an integrated character replacement function. If you try to print non-standard 2 byte UTF8 characters like ÁÉÍÓÚÑáéíóúñ they will be replaced by a 1 byte equivalent. You can disable this behaviour unsetting a flag in the LCD structure.

/* Disable characters replacement and print a string */
lcd->replace_UTF8_chars = 0;
lcd_print(lcd, "España: áéíóú");

See example_utf8.c to learn more about this.

Set text position

This function moves the cursor to the position given by row and col parameters.

lcd_pos(lcd_t *lcd, int row, int col);

Row and Col start at 0; so the first row is 0, the second is 1, and so on. The same for columns.

The home position is 0,0.

/* Move the cursor to the start of the second line */
lcd_pos(lcd, 1, 0);

Free the LCD

This function may be called at the end of your program. It is optional. If you don’t call it, then the LCD just shows the last message until you disconnect the power supply after your program ends.

It turns off the display, the backlight and frees the memory.

/* Turn off LCD and free the memory */

Usage (level: intermediate)

Clear display

This function clears all text, return the display to the home position if shifted and moves cursor to the top left corner.

/* Clear all contents */

Return to home

This function return the display to the home position if shifted and moves cursor to the top left corner.

It is different from lcd_pos because this only moves the cursor, but does not affect shifting.

/* Return cursor and display to top-left */

LCD and backlight

Turns LCD ON (default) or OFF. When the LCD is on, the display data can be displayed instantly. When the LCD is off, the display data remains in RAM, and can be displayed instantly by switching it to ON.


The backlight functions turns the backlight ON (default) or OFF.


Cursor style

Make cursor visible or invisible (default). Enable or disable (default) cursor blinking.


Create custom characters

The function lcd_create_char allows you to add up to 8 custom characters to the predefined characters set.

void lcd_create_char(lcd_t *lcd, int n, char *data);

The arguments are:

It stores a custom 5x8 character in the position n of the Character Generator RAM. You can use these characters later with codes 0x00 to 0x07.

Please note that character 0x00 is not allowed inside a string, making this character actually not usable with basic and intermediate functions.

To make a custom character you need to create a binary array.

char mychar[] = {

lcd_create_char(lcd, 2, mychar);

lcd_print(lcd, "My char: \02");

Checking for errors

Each and every I2C commands sent to PCF8574 must be acknowledged. If PCF8574 fails to ack a command, the current function stops and set the error flag.

This is uncommon, and due to I2C interference or malfunction.

Checking the error condition, you can take appropriate measures like showing a message by another mean or resetting the LCD; but the last will not always be possible.

You check for error looking al err member of the LCD structure. This flag is only cleared in initialization.

if (lcd->err) {
	fprintf(stderr, "LCD error detected!\n");
	lcd_pos(lcd, 0,0);
	lcd_print(lcd, "Error");
	return 1;

Usage (level: advanced)

Reset and initialization;

The function lcd_reset performs an initialization procedure as described at “Initializing by Instruction” section of HD44780 datasheet.

void lcd_reset (lcd_t *lcd);

The function lcd_init Performs a configuration of the LCD, issuing lcd_reconfig, lcd_clear and lcd_home.

void lcd_init (lcd_t *lcd);

Reading data back from LCD

HD44780 controller allows you to read LCD status back and DDRAM content. You can use this feature to save and restore cursor position or screen data.

You use lcd_read_pos_raw to retrieve the lcd status. Bit 0x80 is the Busy Flag, and the other bits are the cursor position in DDRAM.

/* Save actual cursor position */
int old_pos_raw = lcd_read_pos_raw(lcd);
// ... do something
lcd_pos_raw(lcd, old_pos_raw);

Note how you cannot use lcd_pos because it uses two separate values for row and column; you have to use lcd_pos_raw instead.

To read data, just use lcd_read_data. For example, to read the contents of the second line:

printf("Line 2 contents: ");
lcd_pos(lcd, 1,0); // second line
for (i = 0; i < 32; i++) 
	printf("%c", lcd_read_data(lcd));

Backlight dimming

Most of PCF8574 LCD drivers have a jumper to switch on and off the back light. This library provides the function lcd_backlight_dim to control the led through PWM.

If it is beetween Gnd and the LED, you are lucky; you can control it using just one NPN transistor. But if it is connected between positive (+5V) and the LED, then you will need two. Since the 3.3V output of the PWM pin is too low to cut off a 5V driven PNP transistor.

This is an example of the circuit you can use:

PWM LED dim hardware

The second parameter to this function is a float number between 0 and 1; where 0 is the dimmest light, and 1 the brightest.

/* DIM the back light */
lcd_backlight_dim(lcd, 0.2);

By default, this library uses the pin 1 (wiringpi numeration) PWM0.

Reconfig functions

void lcd_reconfig_fcn(lcd_t *lcd);
void lcd_reconfig_cursor(lcd_t *lcd);
void lcd_reconfig_display(lcd_t *lcd);
void lcd_reconfig_entrymode(lcd_t *lcd);
void lcd_reconfig(lcd_t *lcd);

These functions work with internal LCD structure status. Sending the status command with its options to the LCD controller.

Such struct has four configuration sets:

Refer to LCD tutorial or HD44780 datasheet for more information.

These functions allows you to configure every possible status, even invalid ones.

The last función, lcd_reconfig, calls the four functions one after another.

void lcd_raw (lcd_t *lcd, int lcd_opts, int data);

It receives as parameters:

and call PCF8574 to send the upper nibble and the lower nibble in 4 bit interaction mode.

Known bugs and limitations

See also

El bus I2C a bajo nivel (Spanish). Extensive description of the bit-banged I2C code.

HD44780 datasheet. Dot Matrix Liquid Crystal Display Controller/Driver.

PCF8574 datasheet. Remote 8-bit I/O expander for I2C-bus.

DjLCDSIM. Dincer’s JavaScript LCD Simulator.



This is free and unencumbered software released into the public domain. See the LICENSE file for details.