一、介绍
众所周知,虽然液晶显示器和其他显示器大大的丰富了人机交互,但他们有一个共同的弱点。当它们连接到控制器时,需要占用大量的IO口,但是一般的控制器没有那么多的外部端口,也限制了控制器的其他功能。因此,开发具有I2C组件的LCD1602来解决该问题,LCD1602是一种只用来显示字母、数字、符号等的点阵型液晶模块。
字符型液晶显示模块是由字符型液晶显示屏LCD 、控制驱动主电路HD44780/KS0066及其扩展驱动电路HD44100或与其兼容的IC, 少量阻、容元件结构件等装配在PCB板上而成。
I2C总线是由PHLIPS发明的一种串行总线。它是一种高性能的串行总线,具有多主机系统所需的总线控制和高速或低速设备同步功能。I2C LCD1602上的蓝色电位器用于调整背光,以获得更好的显示效果。I2C使用两个双向极漏开路线,串行数据线(SDA)和串行时钟线(SCL),通过电阻上拉。使用的典型电压为5V或3.3V,但允许使用其他电压的系统。
其它I2C总线实验可以查看前面的PCF8591相关实验,如:
树莓派基础实验12:PCF8591模数转换器实验
二、组件
★Raspberry Pi主板*1
★树莓派电源*1
★40P软排线*1
★I2C LCD1602模块*1
★面包板*1
★跳线若干
三、实验原理
树莓派的GPIO端口数量有限,可通过IO扩展芯片增加GPIO的数量,使得树莓派可以适应更多的应用。本实验中的LCD1602模块有16个管脚,为节省GPIO端口,就使用了一款通过I2C总线扩展IO的芯片,PCF8574。单个PCF8574可扩展8个IO,一个I2C总线最多可挂载8个PCF8574,所以树莓派最多可扩展64个IO。
本实验中的编程原理比较复杂,所以一定要程序和硬件原理结合起来看才易理解。如果不想深度学习底层原理及驱动程序,掌握LCD1602的函数使用方法就可以了,但若想灵活运用LCD1602,最好了解一下。
本文是在网上查阅了很多中外资料,汇集诸多大神的智慧,10几天(当然,每天还是要上班的)才整理汇编而成,但仍有很多不懂和错误之处,特别是程序中有一长串“????”注释的地方,请大神们留言指出!
3.1 LCD1602的存储器
LCD1602里面存储器有三种:CGROM、CGRAM、DDRAM。
DDRAM(Display Data RAM)就是显示数据RAM,用来寄存待显示的字符代码。共80个字节,其地址和屏幕的对应关系如下,如图:
DDRAM其实就是我们平时说的PC机的显存,如果说我们想要在屏幕上显示我们想要显示的,直接把需要的字符代码送入显存就可以了,很简单就能够在屏幕上显示我们想要显示的。相同的LCD1602总共存在80个字节的显存,就是DDRAM。遗憾的是LCD1602显示不出来这么多的字符,正是因为这样,不是每一个写在DDRAM上的字符都能够在显示器上显示出来,一次只能显示16个字符。正是因为这样,我们在程序中可以利用下面的“光标或显示移动指令”使字符慢慢移动到可见的显示范围内,看到字符的移动效果。
那么如何在液晶上显示字符呢,就是把要写入的字符给DDRAM。举个例子,我现在想在屏幕上显示“A”,我就把我要的字符“A”的字符代码41H写入DDRAM的00H地址处然后得到。那我们应该怎么去写入呢,我们在后面进行进一步的阐述。我们下面将要介绍的是A的字模,如图:
上面的图左侧显示的就是“A”的字模数据,上面的图右侧显示“○”代表0,用“■”代表 1。这样我们就能够显示出“A”这个字形。
在LCD1602模块上固化了字模存储器,就是CGROM和CGRAM,HD44780内置了192个常用字符的字模,存于字符产生器CGROM(Character Generator ROM)中,另外还有8个允许用户自定义的字符产生RAM,称为CGRAM(Character Generator RAM),留给自定义的位置只有8个地址,也就是最多自定义8个符号或者图形。
下图(字模表)说明了CGROM和CGRAM与字符的对应关系。从ROM和RAM的名称我们也可以知道,ROM是早已固化在LCD1602模块中的,只能读取;但是RAM即可以读又可以写。
若是只要求在屏幕上显示CGROM中已经拥有的字符,那就仅仅需要在DDRAM中写入它的字符代码就可以了;若是想显示的是CGROM中不存在的字符,例如美元的符号,那就只能先在CGRAM中规定,下一步再在DDRAM中写入我们之前自己定义的字符就可以。
上面这个图说明的是5×8点阵和5×10点阵字符的字形和光标的位置。这里我们采用的是5×8点阵,那么定义这样一个字符需要8个字节,每个字节的前3个位没有被使用。
上面这个图说明的是设置CGRAM地址指令。从这个指令的格式中我们可以看出,它共有aaaaaa这6位,一共可以表示64个地址,即64个字节。一个5×8点阵字符共占用8个字节,那么这64个字节一共可以自定义8个字符。也就是说,上面这个图的6位地址中的DB5DB4DB3用来表示8个自定义的字符,DB2DB1DB0用来表示每个字符的8个字节。这DB5DB4DB3所表示的8个自定义字符(0--7)就是要写入DDRAM中的字符代码。
3.2 管脚
加装了I2C转接版的LCD1602,能够同时显示16x02即32个字符。(16列2行)1602字符型LCD通常有16条引脚线的LCD:
引脚 | 符号 | 功能说明 |
---|---|---|
1 | VSS | 一般接地 |
2 | VDD | 接电源(+5V) |
3 | V0 | 晶显示器对比度调整端,接正电源时对比度最弱,接地电源时对比度最高(对比度过高时会产生“鬼影”,使用时可以通过一个10K的电位器调整对比度)。 |
4 | RS | RS为寄存器选择,高电平(1)时选择数据寄存器、低电平(0)时选择指令寄存器。 |
5 | R/W | R/W为读写选择,高电平(1)时进行读操作,低电平(0)时进行写操作。 |
6 | E | E(或EN)端为使能(enable)端,写操作时,下降沿使能。读操作时,E高电平有效 |
7 | DB0 | 低4位三态、 双向数据总线 0位(最低位) |
8 | DB1 | 低4位三态、 双向数据总线 1位 |
9 | DB2 | 低4位三态、 双向数据总线 2位 |
10 | DB3 | 低4位三态、 双向数据总线 3位 |
11 | DB4 | 高4位三态、 双向数据总线 4位 |
12 | DB5 | 高4位三态、 双向数据总线 5位 |
13 | DB6 | 高4位三态、 双向数据总线 6位 |
14 | DB7 | 高4位三态、 双向数据总线 7位(最高位)(也是busy flag) |
15 | BLA | 背光电源正极 |
16 | BLK | 背光电源负极 |
3.3 LCD1602的基本操作及时序
本系列模块内部具有两个 8 位寄存器:指令寄存器(IR)和数据寄存器(DR)。用户可以通过 RS 和 R/W 输入信号的组合选择指定的寄存器,进行相应的操作。下表中列出了组合选择方式:
RS | R/W | 操作说明 |
---|---|---|
0 | 0 | 写入指令寄存器(清除屏等) |
0 | 1 | 读busy flag(DB7),以及读取位址计数器(DB0~DB6)值 |
1 | 0 | 写入数据寄存器(显示各字型等) |
1 | 1 | 从数据寄存器读取数据 |
LCD1602的基本操作:
1. 读状态:输入RS=0,RW=1,E=高脉冲。输出:D0—D7为状态字。
2. 读数据:输入RS=1,RW=1,E=高脉冲。输出:D0—D7为数据。
3. 写命令:输入RS=0,RW=0,E=高脉冲。输出:无。(写完置E=高脉冲)
4. 写数据:输入RS=1,RW=0,E=高脉冲。输出:无。
注意:E(或EN)端为使能(enable)端,写操作时,下降沿使能。读操作时,E高电平有效。
读操作时序图:
写操作时序图:
时序时间参数:
3.4 LCD1602的指令说明
1602液晶模块内部的控制器共有11条控制指令:
1602液晶模块的读写操作、屏幕和光标的操作都是通过指令编程来实现的。
指令1:清显示,指令码01H,光标复位到地址00H位置。
说明:清除屏幕显示内容。光标返回屏幕左上角。执行这个指令时需要一定时间。
指令2:光标复位,光标返回到地址00H。
说明:光标返回屏幕左上角,它不改变屏幕显示内容。
指令3:光标和显示模式设置
I/D=1:写入新数据后光标右移。
I/D=0:写入新数据后光标左移。
S=1:显示移动。
S=0:显示不移动。
说明:这里的设置是0x06。
指令4:显示开关控制。
D=1:显示开,D=0:显示关。
C=1:光标显示,C=0:光标不显示。
B=1:光标闪烁,B=0:光标不闪烁。
说明:这里的设置是显示开,不显示光标,光标不闪烁,设置字为0x0c。
指令5:光标或显示移位
说明:在需要进行整屏移动时,这个指令非常有用,可以实现屏幕的滚动显示效果。初始化时不使用这个指令。
指令6:功能设置命令
×:不关心,也就是说这个位是0或1都可以,一般取0。
DL:设置数据接口位数。
DL=1:8位数据接口(D7—D0)。
DL=0:4位数据接口(D7—D4)。
N=0:一行显示。
N=1:两行显示。
F=0:5×8点阵字符。
F=1:5×10点阵字符。
说明:因为是写指令字,所以RS和RW都是0。LCD1602只能用并行方式驱动,不能用串行方式驱动。而并行方式又可以选择8位数据接口或4位数据接口。这里我们选择4位数据接口(D3—D0)。我们的设置是4位数据接口,两行显示,5×8点阵,即0b00101000也就是0x28。(注意:NF是10或11的效果是一样的,都是两行5×8点阵。因为它不能以两行5×10点阵方式进行显示,换句话说,这里用0x28或0x2c是一样的)。
指令7:字符发生器CGRAM地址设置。
指令8:DDRAM地址设置。
说明:这个指令用于设置DDRAM地址。在对DDRAM进行读写之前,首先要设置DDRAM地址,然后才能进行读写。前面我们说过,DDRAM就是LCD1602的显示存储器。我们要在它上面进行显示,就要把要显示的字符写入DDRAM。同样,我们想知道DDRAM某个地址上有什么字符,也要先设置DDRAM地址,然后将它读出到单片机。
指令9:读忙信号和光标地址
BF:为忙标志位,高电平表示忙,此时模块不能接收命令或者数据。如果为低电平表示不忙。
说明:这个指令用来读取LCD1602状态。对于单片机来说,LCD1602属于慢速设备。当单片机向其发送一个指令后,它将去执行这个指令。这时如果单片机再次发送下一条指令,由于LCD1602速度较慢,前一条指令还未执行完毕,它将不接受这新的指令,导致新的指令丢失。因此这条读忙指令可以用来判断LCD1602是否忙,能否接收单片机发来的指令。当BF=1,表示LCD1602正忙,不能接受单片机的指令;当BF=0,表示LCD1602空闲,可以接收单片机的指令。RS=0,表示是指令;RW=1,表示是读取。这条指令还有一个副产品:即可以得到地址记数器AC的值(address counter)。LCD1602维护了一个地址计数器AC,用来记录下一次读写CGRAM或DDRAM的位置。需要强调的是:这条指令我一次也没有执行成功。很多网友似乎也是这样。好在我们有另外的办法,也就是延时。通过查看每条指令的执行时间,再经过一些试验,可以确定指令的延时。这样就可以在上一条指令执行完毕后再执行下一条指令了。
指令10:写数据。
说明:RS=1,数据;RW=0,写。指令执行时,要在DB7—DB0上先设置好要写入的数据,然后执行写命令。
指令11:读数据。
说明:RS=1,数据;RW=1,读。先设置好CGRAM或DDRAM的地址,然后执行读取命令。数据就被读入后DB7—DB0。
3.5 初始化
如果电路电源能满足内部RESET电路的如下要求, 初始化可自动完成:
如果电路电源不能满足内部RESET电路的要求的话,需要用初始化程序来实现初始化,有8位总线和4位总线两种模式。
8位数据传输模式:
本次实验中使用4位数据传输模式:
3.6 DDRAM地址
1602字符液晶显示可分为上下两部分各16位进行显示,处于不同行时的字符显示地址如下:
显示字符 | 1 | 2 | 3 | 4 | ...... | 12 | 13 | 14 | 15 | 16 |
---|---|---|---|---|---|---|---|---|---|---|
第一行地址 | 00H | 01H | 02H | 03H | ...... | 0BH | 0CH | 0DH | 0EH | 0FH |
第二行地址 | 40H | 41H | 42H | 43H | ...... | 4BH | 4CH | 4DH | 4EH | 4FH |
按照上面指令8格式所示,由于地址为7位,在写入地址时,第8位D7恒为1。当我们想在指定位置写入内容时,要先指定地址,如在第一行第一位写入,地址位是00H,再加上DB7的1,即80H(0010000000),第二行第一位是40H,再加上DB7的1,即C0H(0011000000),依次类推。
四、实验步骤
第1步:连接电路。连接电源打开树莓派,显示屏就会亮,同时在第一行显示一排黑方块。如果看不到黑方块或黑方块不明显,请调节可调电阻,直到黑方块清晰显示。如果调节可调电阻还看不到方块,则可能你的连接有问题了,请检查连接,包括检查显示屏的引脚有没有虚焊。
树莓派 | T型转接板 | LCD1602 |
---|---|---|
SCL | SCL | SCL |
SDA | SDA | SDA |
5V | 5V | VCC |
GND | GND | GND |
第2步:PCF8591模块采用的是I2C(IIC)总线进行通信的,但是在树莓派的镜像中默认是关闭的,在使用该传感器的时候,我们必须首先允许IIC总线通信。
![](http://img.e-com-net.com/image/info10/7fa164cf9a7549bb85627c0b472e2170.jpg)
第3步:查询LCD1602的地址。得出地址为0x27。
pi@raspberrypi:~ $ ls /dev/i2c-*
/dev/i2c-1
pi@raspberrypi:~ $ sudo i2cdetect -y 1
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- 27 -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
第4步:编写驱动程序。这里先编写一个LCD1602.py文件,后面再编写一个python程序引入这个库文件,调用这个文件中的函数实现更复杂的功能。
LCD1602.py文件就相当于是LCD1602模块的驱动程序,单独编写是为了便于重用。
该程序也可以单独运行,会在第一行显示“Hello”,在第二行显示“world!”。
#!/usr/bin/env python
import time
import smbus #SMBus (System Management Bus,系统管理总线) 在程序中导入“smbus”模块
BUS = smbus.SMBus(1) #创建一个smbus实例
# 0 代表 /dev/i2c-0, 1 代表 /dev/i2c-1 ,具体看使用的树莓派那个I2C来决定
def write_word(addr, data):
global BLEN #该变量为1表示打开LCD背光,若是0则关闭背光
temp = data
if BLEN == 1:
temp |= 0x08 #0x08=0000 1000,表开背光
#buf |= 0x08等价于buf = buf | 0x08(按位或)
else:
temp &= 0xF7 #0xF7=1111 0111,表关闭背光
#buf &= 0xF7等价于buf = buf & 0xF7(按位与)
BUS.write_byte(addr ,temp) #这里为什么又一次写入8位??????
#write_byte(int addr, char val)发送一个字节到设备
def send_command(comm):
# Send bit7-4 firstly
buf = comm & 0xF0 #与运算,取高四位数值
#由于4位总线的接线是接到P0口的高四位,传送高四位不用改
buf |= 0x04 #buf |= 0x04等价于buf = buf | 0x04(按位或)0x04=0000 0100
# RS = 0, RW = 0, EN = 1
#为什么这样写入代表RS = 0, RW = 0, EN = 1,低4位在这里有何意义????????
write_word(LCD_ADDR ,buf) #为什么这里又是8位写入?????
time.sleep(0.002)
buf &= 0xFB #buf &= 0xFB等价于buf = buf & 0xFB(按位与)0xFB=1111 1011
# Make EN = 0,EN从1——>0,下降沿,进行写操作
#为什么这样写入代表Make EN = 0????????
write_word(LCD_ADDR ,buf)
# Send bit3-0 secondly
buf = (comm & 0x0F) << 4 #与运算,取低四位数值,
#由于4位总线的接线是接到P0口的高四位,所以要再左移4位
buf |= 0x04
# RS = 0, RW = 0, EN = 1 写入命令
write_word(LCD_ADDR ,buf)
time.sleep(0.002)
buf &= 0xFB # Make EN = 0
write_word(LCD_ADDR ,buf)
def send_data(data):
# Send bit7-4 firstly
buf = data & 0xF0
buf |= 0x05 # RS = 1, RW = 0, EN = 1 写入数据
write_word(LCD_ADDR ,buf)
time.sleep(0.002)
buf &= 0xFB # Make EN = 0
write_word(LCD_ADDR ,buf)
# Send bit3-0 secondly
buf = (data & 0x0F) << 4
buf |= 0x05 # RS = 1, RW = 0, EN = 1 写入数据
write_word(LCD_ADDR ,buf)
time.sleep(0.002)
buf &= 0xFB # Make EN = 0
write_word(LCD_ADDR ,buf)
def init(addr, bl): #LCD1602初始化
global LCD_ADDR #该变量为设备地址
global BLEN #该变量为1表示打开LCD背光,若是0则关闭背光
LCD_ADDR = addr
BLEN = bl
try:
send_command(0x33) # 必须先初始化为8行模式 110011 Initialise
time.sleep(0.005)
send_command(0x32) # 然后初始化为4行模式 110010 Initialise
time.sleep(0.005)
send_command(0x28) # 4位总线,双行显示,显示5×8的点阵字符。
time.sleep(0.005)
send_command(0x0C) # 打开显示屏,不显示光标,光标所在位置的字符不闪烁
time.sleep(0.005)
send_command(0x01) # 清屏幕指令,将以前的显示内容清除
time.sleep(0.005)
send_command(0x06) # 设置光标和显示模式,写入新数据后光标右移,显示不移动
BUS.write_byte(LCD_ADDR, 0x08) #这里这样写入0x08是什么意思??????
except:
return False
else:
return True
def clear():
send_command(0x01) # 清屏
def write(x, y, str):
if x < 0: #LCD1602只有16列,2行显示,小于第0列的数据要做修正
x = 0
if x > 15: #LCD1602只有16列,2行显示,大于第15列的数据要做修正
x = 15
if y <0: #LCD1602只有16列,2行显示,小于第0行的数据要做修正
y = 0
if y > 1: #LCD1602只有16列,2行显示,大于第1行的数据要做修正
y = 1
# 移动光标
addr = 0x80 + 0x40 * y + x
#第一行第一位的地址为0x00,加上D7恒为1,所以第一行第一位的地址为0x80
#第二行第一位是0x40,加上D7恒为1,所以第二行第一位的地址为0x80加上0x40,最后为0xC0
send_command(addr) #设置显示位置
for chr in str:
send_data(ord(chr)) #发送显示内容
#ord()函数以一个字符(长度为1的字符串)作为参数,
#返回对应的 ASCII 数值,或者 Unicode 数值
if __name__ == '__main__':
init(0x27, 1) #在树莓派终端上使用命令'sudo i2cdetect -y 1'查询设备地址为0x27
# 第二个参数1表示打开LCD背光,若是0则关闭背光
write(4, 0, 'Hello') #4,0参数指显示的起始位置为第4列,第0行
write(7, 1, 'world!') #7,1参数指显示的起始位置为第7列,第1行
#‘Hello’为要显示的字符串
第5步:编写控制程序。先是静态显示内容:第一行显示“Greetings!!”,第二行显示“Welcome here!”,持续2秒。之后动态滚动显示“Thank you for buying Raspberry! _”。
#!/usr/bin/env python
import LCD1602
import time
def setup():
LCD1602.init(0x27, 1) # init(slave address, background light)
LCD1602.write(0, 0, 'Greetings!!')
LCD1602.write(1, 1, 'Welcome here!')
time.sleep(2)
def loop():
space = ' '
greetings = 'Thank you for buying Raspberry! ^_^'
greetings = space + greetings
while True:
tmp = greetings
for i in range(0, len(greetings)):
LCD1602.write(0, 0, tmp) #当要显示的字符串过长时,会自动在LCD的第二行显示
tmp = tmp[1:] #每次循环去掉字符串首位字符,实现字幕向左移动的效果
time.sleep(0.8)
LCD1602.clear()
def destroy():
pass
if __name__ == "__main__":
try:
setup()
while True:
loop()
except KeyboardInterrupt:
destroy()
![](http://img.e-com-net.com/image/info10/25e7b13256014b8cbfe9be1fdfe435ed.jpg)