一、介绍
众所周知,虽然液晶显示器和其他显示器大大的丰富了人机交互,但他们有一个共同的弱点。当它们连接到控制器时,需要占用大量的IO口,但是一般的控制器没有那么多的外部端口,也限制了控制器的其他功能。因此,开发具有I2C组件的LCD1602来解决该问题,LCD1602是一种只用来显示字母、数字、符号等的点阵型液晶模块。
字符型液晶显示模块是由字符型液晶显示屏LCD 、控制驱动主电路HD44780/KS0066及其扩展驱动电路HD44100或与其兼容的IC, 少量阻、容元件结构件等装配在PCB板上而成。
I2C总线是由PHLIPS发明的一种串行总线。它是一种高性能的串行总线,具有多主机系统所需的总线控制和高速或低速设备同步功能。I2C LCD1602上的蓝色电位器用于调整背光,以获得更好的显示效果。I2C使用两个双向极漏开路线,串行数据线(SDA)和串行时钟线(SCL),通过电阻上拉。使用的典型电压为5V或3.3V,但允许使用其他电压的系统。
二、组件
★Raspberry Pi 3主板*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)时选择数据寄存器、低电平(0)时选择指令寄存器。5R/WR/W为读写选择,高电平(1)时进行读操作,低电平(0)时进行写操作。6EE(或EN)端为使能(enable)端,写操作时,下降沿使能。读操作时,E高电平有效7DB0低4位三态、 双向数据总线 0位(最低位)8DB1低4位三态、 双向数据总线 1位9DB2低4位三态、 双向数据总线 2位10DB3低4位三态、 双向数据总线 3位11DB4高4位三态、 双向数据总线 4位12DB5高4位三态、 双向数据总线 5位13DB6高4位三态、 双向数据总线 6位14DB7高4位三态、 双向数据总线 7位(最高位)(也是busy flag)15BLA背光电源正极16BLK背光电源负极
3.3 LCD1602的基本操作及时序
本系列模块内部具有两个 8 位寄存器:指令寄存器(IR)和数据寄存器(DR)。用户可以通过 RS 和 R/W 输入信号的组合选择指定的寄存器,进行相应的操作。下表中列出了组合选择方式:
RSR/W操作说明00写入指令寄存器(清除屏等)01读busy flag(DB7),以及读取位址计数器(DB0~DB6)值10写入数据寄存器(显示各字型等)11从数据寄存器读取数据
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位进行显示,处于不同行时的字符显示地址如下:
显示字符1234……1213141516第一行地址00H01H02H03H……0BH0CH0DH0EH0FH第二行地址40H41H42H43H……4BH4CH4DH4EH4FH
指令8格式所示,由于地址为7位,在写入地址时,第8位D7恒为1。当我们想在指定位置写入内容时,要先指定地址,如在第一行第一位写入,地址位是00H,再加上DB7的1,即80H(0010000000),第二行第一位是40H,再加上DB7的1,即C0H(0011000000),依次类推。
四、实验步骤
第1步:连接电路。连接电源打开树莓派,显示屏就会亮,同时在第一行显示一排黑方块。如果看不到黑方块或黑方块不明显,请调节可调电阻,直到黑方块清晰显示。如果调节可调电阻还看不到方块,则可能你的连接有问题了,请检查连接,包括检查显示屏的引脚有没有虚焊。
树莓派T型转接板LCD1602SCLSCLSCLSDASDASDA5V5VVCCGNDGNDGND
第2步:PCF8591模块采用的是I2C(IIC)总线进行通信的,但是在树莓派的镜像中默认是关闭的,在使用该传感器的时候,我们必须首先允许IIC总线通信。
第3步:查询LCD1602的地址。得出地址为0x27。
1. pi@raspberrypi:~ $ ls /dev/i2c-*
2. /dev/i2c-1
3. pi@raspberrypi:~ $ sudo i2cdetect -y 1
4.
0 1 2 3 4 5 6 7 8 9 a b c d e f
5.
00: -- -- -- -- -- -- -- -- -- -- -- -- --
6.
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
7.
20: -- -- -- -- -- -- -- 27 -- -- -- -- -- -- -- --
8.
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
9.
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
10.
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
11.
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
12.
70: -- -- -- -- -- -- -- --
第4步:编写驱动程序。这里先编写一个LCD1602.py文件,后面再编写一个python程序引入这个库文件,调用这个文件中的函数实现更复杂的功能。
LCD1602.py文件就相当于是LCD1602模块的驱动程序,单独编写是为了便于重用。
该程序也可以单独运行,会在第一行显示“Hello”,在第二行显示“world!”。1. #!/usr/bin/env python
2. import time
3. import smbus #SMBus (System Management Bus,系统管理总线) 在程序中导入“smbus”模块
4.
BUS = smbus.SMBus(1) #创建一个smbus实例
5. # 0 代表 /dev/i2c-0, 1 代表 /dev/i2c-1 ,具体看使用的树莓派那个I2C来决定
6. def write_word(addr, data):
7. global BLEN #该变量为1表示打开LCD背光,若是0则关闭背光
8. temp = data
9.
if BLEN == 1:
10.
temp |= 0x08 #0x08=0000 1000,表开背光
11. #buf |= 0x08等价于buf = buf | 0x08(按位或)
12. else:
13.
temp &= 0xF7 #0xF7=1111 0111,表关闭背光
14. #buf &= 0xF7等价于buf = buf & 0xF7(按位与)
15.
BUS.write_byte(addr ,temp) #这里为什么又一次写入8位??????
16. #write_byte(int addr, char val)发送一个字节到设备
17. def send_command(comm):
18. # Send bit7-4 firstly
19.
buf = comm & 0xF0 #与运算,取高四位数值
20. #由于4位总线的接线是接到P0口的高四位,传送高四位不用改
21.
buf |= 0x04 #buf |= 0x04等价于buf = buf | 0x04(按位或)0x04=0000 0100
22. # RS = 0, RW = 0, EN = 1
23. #为什么这样写入代表RS = 0, RW = 0, EN = 1,低4位在这里有何意义????????
24.
write_word(LCD_ADDR ,buf) #为什么这里又是8位写入?????
25. time.sleep(0.002)
26.
buf &= 0xFB #buf &= 0xFB等价于buf = buf & 0xFB(按位与)0xFB=1111 1011
27. # Make EN = 0,EN从1——>0,下降沿,进行写操作
28. #为什么这样写入代表Make EN = 0????????
29. write_word(LCD_ADDR ,buf)
30. # Send bit3-0 secondly
31.
buf = (comm & 0x0F) << 4 #与运算,取低四位数值,
32. #由于4位总线的接线是接到P0口的高四位,所以要再左移4位
33.
buf |= 0x04
34. # RS = 0, RW = 0, EN = 1 写入命令
35. write_word(LCD_ADDR ,buf)
36. time.sleep(0.002)
37.
buf &= 0xFB # Make EN = 0
38. write_word(LCD_ADDR ,buf)
39. def send_data(data):
40. # Send bit7-4 firstly
41.
buf = data & 0xF0
42.
buf |= 0x05 # RS = 1, RW = 0, EN = 1 写入数据
43. write_word(LCD_ADDR ,buf)
44. time.sleep(0.002)
45.
buf &= 0xFB # Make EN = 0
46. write_word(LCD_ADDR ,buf)
47. # Send bit3-0 secondly
48.
buf = (data & 0x0F) << 4
49.
buf |= 0x05 # RS = 1, RW = 0, EN = 1 写入数据
50. write_word(LCD_ADDR ,buf)
51. time.sleep(0.002)
52.
buf &= 0xFB # Make EN = 0
53. write_word(LCD_ADDR ,buf)
54.
def init(addr, bl): #LCD1602初始化
55. global LCD_ADDR #该变量为设备地址
56. global BLEN #该变量为1表示打开LCD背光,若是0则关闭背光
57. LCD_ADDR = addr
58. BLEN = bl
59. try:
60.
send_command(0x33) # 必须先初始化为8行模式 110011 Initialise
61. time.sleep(0.005)
62.
send_command(0x32) # 然后初始化为4行模式 110010 Initialise
63. time.sleep(0.005)
64.
send_command(0x28) # 4位总线,双行显示,显示5×8的点阵字符。
65. time.sleep(0.005)
66.
send_command(0x0C) # 打开显示屏,不显示光标,光标所在位置的字符不闪烁
67. time.sleep(0.005)
68.
send_command(0x01) # 清屏幕指令,将以前的显示内容清除
69. time.sleep(0.005)
70.
send_command(0x06) # 设置光标和显示模式,写入新数据后光标右移,显示不移动
71.
BUS.write_byte(LCD_ADDR, 0x08) #这里这样写入0x08是什么意思??????
72. except:
73.
return False
74. else:
75.
return True
76. def clear():
77.
send_command(0x01) # 清屏
78. def write(x, y, str):
79.
if x < 0: #LCD1602只有16列,2行显示,小于第0列的数据要做修正
80.
x = 0
81.
if x > 15: #LCD1602只有16列,2行显示,大于第15列的数据要做修正
82.
x = 15
83.
if y <0: #LCD1602只有16列,2行显示,小于第0行的数据要做修正
84.
y = 0
85.
if y > 1: #LCD1602只有16列,2行显示,大于第1行的数据要做修正
86.
y = 1
87. # 移动光标
88.
addr = 0x80 + 0x40 * y + x
89. #第一行第一位的地址为0x00,加上D7恒为1,所以第一行第一位的地址为0x80
90. #第二行第一位是0x40,加上D7恒为1,所以第二行第一位的地址为0x80加上0x40,最后为0xC0
91.
send_command(addr) #设置显示位置
92. for chr in str:
93.
send_data(ord(chr)) #发送显示内容
94. #ord()函数以一个字符(长度为1的字符串)作为参数,
95. #返回对应的 ASCII 数值,或者 Unicode 数值
96.
if __name__ == '__main__':
97.
init(0x27, 1) #在树莓派终端上使用命令'sudo i2cdetect -y 1'查询设备地址为0x27
98. # 第二个参数1表示打开LCD背光,若是0则关闭背光
99.
write(4, 0, 'Hello') #4,0参数指显示的起始位置为第4列,第0行
100.
write(7, 1, 'world!') #7,1参数指显示的起始位置为第7列,第1行
101. #‘Hello’为要显示的字符串