Interfacing 16×2 LCD Display with Raspberry Pi Pico
Overview
In this tutorial, we will learn Interfacing of 16×2 LCD Display with Raspberry Pi Pico. An LCD is an electronic display module that uses liquid crystal technology to produce a visible image. The 16×2 LCD display is a very basic module commonly used in electronic circuit applications. The 16×2 translates 16 characters per line in 2 such lines. In this LCD each character is displayed in a 5×8 pixel matrix.
Raspberry Pi Pico which was released a few months ago requires some LCD Display in engineering applications. Here we will go through the LCD Display details and features. The LCD Display is based on HD44780 Driver. There are I2C versions and non-I2C versions of LCD Display. We will take both of these LCDs as an example and interface with Raspberry Pi Pico. We will write MicroPython Code for Interfacing 16×2 LCD Display with Raspberry Pi Pico.Bill of Materials
You need to purchase following components for building this project.
S.N. | COMPONENTS | DESCRIPTION | QUANTITY | |
---|---|---|---|---|
1 | Raspberry Pi Pico | RPI Pico Board RP2040 | 1 | |
2 | LCD Display | 16x2 I2C LCD Display | 1 |
Brief Description of 16X2 LCD Display
16×2 LCD is named so because it has 16 Columns and 2 Rows. So, it will have (16×2=32) 32 characters in total and each character will be made of 5×8 Pixel Dots. A Single character with all its Pixels is shown in the below picture.
Each character has (5×8=40) 40 Pixels and for 32 Characters we will have (32×40) 1280 Pixels. Further, the LCD should also be instructed about the Position of the Pixels. Hence it will be a complicated task to handle everything with the help of a microcontroller. Hence the LCD uses an interface IC like HD44780. This IC is mounted on the backside of the LCD Module. You can check the HD44780 Datasheet for more information.
The function of this IC is to get the Commands and Data from the MCU and process them to display meaningful information on LCD Screen. The LCD operating Voltage is 4.7V to 5.3V & the Current consumption is 1mA without a backlight. It can work on both 8-bit and 4-bit mode
It can also display any custom-generated characters. These LCDs are available in Green and Blue Backlight.
There are two section pins on the whole 16×2 LCD module. Some of them are data pins and some are command pins. Somehow, every pin has a role in controlling a single pixel on the display.
16X2 I2C LCD Display
This display incorporates an I2C interface that requires only 2 pins on a microcontroller to the interface. The I2C interface is a daughter board attached to the back of the LCD module. The I2C address for these displays is either 0x3F or 0x27.
The adapter has an 8-Bit I/O Expander chip PCF8574. This chip converts the I2C data from a microcontroller into the parallel data required by the LCD display. There is a small trimpot to make fine adjustments to the contrast of the display. In addition, there is a jumper on the board that supplies power to the backlight.
Interfacing 16×2 LCD Display with Raspberry Pi Pico
Now, let us interface the 16×2 LCD Display with Raspberry Pi Pico. You can use Fritzing Software to draw the Schematic. You can assemble the circuit on breadboard as shown in the image below.
Connect the pin 1, 5 & 16 of LCD to GND of Raspberry Pi Pico. Similarly, connect the Pin 2 & 15 of LCD to 5V Pin, i.e Vbus of Raspberry Pi Pico. Connect the Pin 4, 6, 11, 12, 13, 14 of LCD Display to Raspberry Pi Pico GP16, GP17, GP18, GP19, GP20, GP21 Pin.
To adjust the LCD Contrast, connect a 10K Potentiometer at Pin 3 of LCD Display.
Source Code/Program
Raspberry Pi Pico supports MicroPython Program for interfacing 16×2 LCD Display. You can either use Thonny IDE or uPyCraft IDE for running the MicroPython Code.
Here is a complete code for interfacing the 16X2 HD44780 LCD with Raspberry Pi Pico. Copy the code to the IDE and save it with the name main.py.
import machine
import utime
rs = machine.Pin(16,machine.Pin.OUT)
e = machine.Pin(17,machine.Pin.OUT)
d4 = machine.Pin(18,machine.Pin.OUT)
d5 = machine.Pin(19,machine.Pin.OUT)
d6 = machine.Pin(20,machine.Pin.OUT)
d7 = machine.Pin(21,machine.Pin.OUT)
def pulseE():
e.value(1)
utime.sleep_us(40)
e.value(0)
utime.sleep_us(40)
def send2LCD4(BinNum):
d4.value((BinNum
& 0b00000001) >>0)
d5.value((BinNum
& 0b00000010) >>1)
d6.value((BinNum
& 0b00000100) >>2)
d7.value((BinNum
& 0b00001000) >>3)
pulseE()
def send2LCD8(BinNum):
d4.value((BinNum
& 0b00010000) >>4)
d5.value((BinNum
& 0b00100000) >>5)
d6.value((BinNum
& 0b01000000) >>6)
d7.value((BinNum
& 0b10000000) >>7)
pulseE()
d4.value((BinNum
& 0b00000001) >>0)
d5.value((BinNum
& 0b00000010) >>1)
d6.value((BinNum
& 0b00000100) >>2)
d7.value((BinNum
& 0b00001000) >>3)
pulseE()
def setUpLCD():
rs.value(0)
send2LCD4(0b0011)#8 bit
send2LCD4(0b0011)#8 bit
send2LCD4(0b0011)#8 bit
send2LCD4(0b0010)#4 bit
send2LCD8(0b00101000)#4 bit,2 lines?,5*8 bots
send2LCD8(0b00001100)#lcd
on, blink off, cursor off.
send2LCD8(0b00000110)#increment cursor, no display shift
send2LCD8(0b00000001)#clear screen
utime.sleep_ms(2)#clear screen needs a long delay
setUpLCD()
rs.value(1)
for x in 'Hello World!':
send2LCD8(ord(x))
Interfacing 16×2 I2C LCD Display with Raspberry Pi Pico
The above code is valid for 16×2 LCD Display without any I2C Module. The PCF8574 I2C or SMBus module simplifies the above connection. Instead of using so many wiring, you can use this IC Module and convert the data output lines just to 2 pins.
Hence the LCD Display becomes an I2C Display with an I2C address of 0x27. The connection between the Raspberry Pi Pico and I2C LCD is very simples as shown below in the schematic.
Connect the LCD VCC & GND Pin to Raspberry Pi Pico 5V & GND Pin respectively. Connect the SDA & SCL pin of LCD to Raspberry Pi Pico GP8 & GP9 respectively.
Source Code/Program
The program for this section is divided into 3 parts. We need to write some driver code as well as I2C protocol code for this part.
Copy all this code and then save all of the 3 codes to Raspberry Pi Pico Board.
lcd_api.py :-
import time
class LcdApi:
LCD_CLR = 0x01 #
DB0: clear display
LCD_HOME = 0x02
# DB1: return to home position
LCD_ENTRY_MODE = 0x04
# DB2: set entry mode
LCD_ENTRY_INC = 0x02 #
--DB1: increment
LCD_ENTRY_SHIFT = 0x01 #
--DB0: shift
LCD_ON_CTRL = 0x08 #
DB3: turn lcd/cursor on
LCD_ON_DISPLAY = 0x04
# --DB2: turn display on
LCD_ON_CURSOR = 0x02 #
--DB1: turn cursor on
LCD_ON_BLINK = 0x01
# --DB0: blinking cursor
LCD_MOVE = 0x10
# DB4: move cursor/display
LCD_MOVE_DISP = 0x08 #
--DB3: move display (0-> move cursor)
LCD_MOVE_RIGHT = 0x04
# --DB2: move right (0-> left)
LCD_FUNCTION = 0x20
# DB5: function set
LCD_FUNCTION_8BIT = 0x10 #
--DB4: set 8BIT mode (0->4BIT mode)
LCD_FUNCTION_2LINES = 0x08 #
--DB3: two lines (0->one line)
LCD_FUNCTION_10DOTS = 0x04 #
--DB2: 5x10 font (0->5x7 font)
LCD_FUNCTION_RESET = 0x30
# See "Initializing by Instruction" section
LCD_CGRAM = 0x40 #
DB6: set CG RAM address
LCD_DDRAM = 0x80 #
DB7: set DD RAM address
LCD_RS_CMD = 0
LCD_RS_DATA = 1
LCD_RW_WRITE = 0
LCD_RW_READ = 1
def __init__(self, num_lines, num_columns):
self.num_lines
= num_lines
if self.num_lines
> 4:
self.num_lines
= 4
self.num_columns
= num_columns
if self.num_columns
> 40:
self.num_columns
= 40
self.cursor_x
= 0
self.cursor_y
= 0
self.implied_newline
= False
self.backlight
= True
self.display_off()
self.backlight_on()
self.clear()
self.hal_write_command(self.LCD_ENTRY_MODE
| self.LCD_ENTRY_INC)
self.hide_cursor()
self.display_on()
def clear(self):
"""Clears
the LCD display and moves the cursor to the top left
corner.
"""
self.hal_write_command(self.LCD_CLR)
self.hal_write_command(self.LCD_HOME)
self.cursor_x
= 0
self.cursor_y
= 0
def show_cursor(self):
"""Causes
the cursor to be made visible."""
self.hal_write_command(self.LCD_ON_CTRL
| self.LCD_ON_DISPLAY |
self.LCD_ON_CURSOR)
def hide_cursor(self):
"""Causes
the cursor to be hidden."""
self.hal_write_command(self.LCD_ON_CTRL
| self.LCD_ON_DISPLAY)
def blink_cursor_on(self):
"""Turns
on the cursor, and makes it blink."""
self.hal_write_command(self.LCD_ON_CTRL
| self.LCD_ON_DISPLAY |
self.LCD_ON_CURSOR | self.LCD_ON_BLINK)
def blink_cursor_off(self):
"""Turns
on the cursor, and makes it no blink (i.e. be solid)."""
self.hal_write_command(self.LCD_ON_CTRL
| self.LCD_ON_DISPLAY |
self.LCD_ON_CURSOR)
def display_on(self):
"""Turns
on (i.e. unblanks) the LCD."""
self.hal_write_command(self.LCD_ON_CTRL
| self.LCD_ON_DISPLAY)
def display_off(self):
"""Turns
off (i.e. blanks) the LCD."""
self.hal_write_command(self.LCD_ON_CTRL)
def backlight_on(self):
"""Turns
the backlight on.
This
isn't really an LCD command, but some modules have backlight
controls,
so this allows the hal to pass through the command.
"""
self.backlight
= True
self.hal_backlight_on()
def backlight_off(self):
"""Turns
the backlight off.
This
isn't really an LCD command, but some modules have backlight
controls,
so this allows the hal to pass through the command.
"""
self.backlight
= False
self.hal_backlight_off()
def move_to(self, cursor_x, cursor_y):
"""Moves
the cursor position to the indicated position. The cursor
position
is zero based (i.e. cursor_x == 0 indicates first column).
"""
self.cursor_x
= cursor_x
self.cursor_y
= cursor_y
addr = cursor_x
& 0x3f
if cursor_y
& 1:
addr
+= 0x40 # Lines 1 & 3 add 0x40
if cursor_y
& 2: # Lines 2 & 3 add number of columns
addr
+= self.num_columns
self.hal_write_command(self.LCD_DDRAM
| addr)
def putchar(self, char):
"""Writes
the indicated character to the LCD at the current cursor
position,
and advances the cursor by one position.
"""
if char ==
'\n':
if
self.implied_newline:
#
self.implied_newline means we advanced due to a wraparound,
#
so if we get a newline right after that we ignore it.
pass
else:
self.cursor_x
= self.num_columns
else:
self.hal_write_data(ord(char))
self.cursor_x
+= 1
if self.cursor_x
>= self.num_columns:
self.cursor_x
= 0
self.cursor_y
+= 1
self.implied_newline
= (char != '\n')
if self.cursor_y
>= self.num_lines:
self.cursor_y
= 0
self.move_to(self.cursor_x,
self.cursor_y)
def putstr(self, string):
"""Write
the indicated string to the LCD at the current cursor
position
and advances the cursor position appropriately.
"""
for char in
string:
self.putchar(char)
def custom_char(self, location, charmap):
"""Write
a character to one of the 8 CGRAM locations, available
as chr(0)
through chr(7).
"""
location &=
0x7
self.hal_write_command(self.LCD_CGRAM
| (location << 3))
self.hal_sleep_us(40)
for i in range(8):
self.hal_write_data(charmap[i])
self.hal_sleep_us(40)
self.move_to(self.cursor_x,
self.cursor_y)
def hal_backlight_on(self):
"""Allows
the hal layer to turn the backlight on.
If
desired, a derived HAL class will implement this function.
"""
pass
def hal_backlight_off(self):
"""Allows
the hal layer to turn the backlight off.
If
desired, a derived HAL class will implement this function.
"""
pass
def hal_write_command(self, cmd):
"""Write
a command to the LCD.
It is
expected that a derived HAL class will implement this
function.
"""
raise
NotImplementedError
def hal_write_data(self, data):
"""Write
data to the LCD.
It is
expected that a derived HAL class will implement this
function.
"""
raise NotImplementedError
def hal_sleep_us(self, usecs):
"""Sleep
for some time (given in microseconds)."""
time.sleep_us(usecs)
pico_i2c_lcd.py :-
from lcd_api import LcdApi
from machine import I2C
from time import sleep_ms
DEFAULT_I2C_ADDR = 0x27
# Defines shifts or masks for the various LCD line
attached to the PCF8574
MASK_RS = 0x01
MASK_RW = 0x02
MASK_E = 0x04
SHIFT_BACKLIGHT = 3
SHIFT_DATA = 4
class I2cLcd(LcdApi):
"""Implements a
character based lcd connected via PCF8574 on i2c."""
def __init__(self, i2c, i2c_addr,
num_lines, num_columns):
self.i2c =
i2c
self.i2c_addr
= i2c_addr
self.i2c.writeto(self.i2c_addr,
bytearray([0]))
sleep_ms(20)
# Allow LCD time to powerup
# Send
reset 3 times
self.hal_write_init_nibble(self.LCD_FUNCTION_RESET)
sleep_ms(5) #
need to delay at least 4.1 msec
self.hal_write_init_nibble(self.LCD_FUNCTION_RESET)
sleep_ms(1)
self.hal_write_init_nibble(self.LCD_FUNCTION_RESET)
sleep_ms(1)
# Put LCD
into 4 bit mode
self.hal_write_init_nibble(self.LCD_FUNCTION)
sleep_ms(1)
LcdApi.__init__(self,
num_lines, num_columns)
cmd = self.LCD_FUNCTION
if num_lines
> 1:
cmd
|= self.LCD_FUNCTION_2LINES
self.hal_write_command(cmd)
def hal_write_init_nibble(self, nibble):
"""Writes
an initialization nibble to the LCD.
This
particular function is only used during intiialization.
"""
byte = ((nibble
>> 4) & 0x0f) << SHIFT_DATA
self.i2c.writeto(self.i2c_addr,
bytearray([byte | MASK_E]))
self.i2c.writeto(self.i2c_addr,
bytearray([byte]))
def hal_backlight_on(self):
"""Allows
the hal layer to turn the backlight on."""
self.i2c.writeto(self.i2c_addr,
bytearray([1 << SHIFT_BACKLIGHT]))
def hal_backlight_off(self):
"""Allows
the hal layer to turn the backlight off."""
self.i2c.writeto(self.i2c_addr,
bytearray([0]))
def hal_write_command(self, cmd):
"""Writes
a command to the LCD.
Data is
latched on the falling edge of E.
"""
byte = ((self.backlight
<< SHIFT_BACKLIGHT) | (((cmd >> 4) & 0x0f) << SHIFT_DATA))
self.i2c.writeto(self.i2c_addr,
bytearray([byte | MASK_E]))
self.i2c.writeto(self.i2c_addr,
bytearray([byte]))
byte = ((self.backlight
<< SHIFT_BACKLIGHT) | ((cmd & 0x0f) << SHIFT_DATA))
self.i2c.writeto(self.i2c_addr,
bytearray([byte | MASK_E]))
self.i2c.writeto(self.i2c_addr,
bytearray([byte]))
if cmd
<= 3:
#
The home and clear commands require a worst case delay of 4.1 msec
sleep_ms(5)
def hal_write_data(self, data):
"""Write
data to the LCD."""
byte = (MASK_RS
| (self.backlight << SHIFT_BACKLIGHT) | (((data >> 4) & 0x0f)
<< SHIFT_DATA))
self.i2c.writeto(self.i2c_addr,
bytearray([byte | MASK_E]))
self.i2c.writeto(self.i2c_addr,
bytearray([byte]))
byte = (MASK_RS
| (self.backlight << SHIFT_BACKLIGHT) | ((data & 0x0f) << SHIFT_DATA))
self.i2c.writeto(self.i2c_addr,
bytearray([byte | MASK_E]))
self.i2c.writeto(self.i2c_addr,
bytearray([byte]))
main.py :-
from pico_i2c_lcd import I2cLcd
from machine import I2C
from machine import Pin
import utime as time
i2c = I2C(id=1,scl=Pin(9),sda=Pin(8),freq=100000)
lcd = I2cLcd(i2c, 0x27, 2, 16)
while True:
lcd.move_to(2,0)
lcd.putstr('Hello
world')