一、介绍

众所周知,虽然液晶显示器和其他显示器大大的丰富了人机交互,但他们有一个共同的弱点。当它们连接到控制器时,需要占用大量的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

★跳线若干

三、实验原理




android 副屏内容显示到主屏 副屏幕显示指定内容_数据


android 副屏内容显示到主屏 副屏幕显示指定内容_初始化_02


android 副屏内容显示到主屏 副屏幕显示指定内容_数据_03


树莓派的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个字节,其地址和屏幕的对应关系如下,如图:


android 副屏内容显示到主屏 副屏幕显示指定内容_总线与微命令实验总结_04


DDRAM其实就是我们平时说的PC机的显存,如果说我们想要在屏幕上显示我们想要显示的,直接把需要的字符代码送入现实就可以了,很简单就能够在屏幕上显示我们想要显示的。相同的LCD1602总共存在80个字节的显存,就是DDRAM。遗憾的是LCD1602显示不出来这么多的字符,正是因为这样,不是每一个写在DDRAM上的字符都能够在显示器上显示出来,一次只能显示16个字符。正是因为这样,我们在程序中可以利用下面的“光标或显示移动指令”使字符慢慢移动到可见的显示范围内,看到字符的移动效果。

那么如何在液晶上显示字符呢,就是把要写入的字符给DDRAM。举个例子,我现在想在屏幕上显示“A”,我就把我要的字符“A”的字符代码41H写入DDRAM的00H地址处然后得到。那我们应该怎么去写入呢,我们在后面进行进一步的阐述。我们下面将要介绍的是A的字模,如图:


android 副屏内容显示到主屏 副屏幕显示指定内容_树莓派_05


上面的图左侧显示的就是“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即可以读又可以写。


android 副屏内容显示到主屏 副屏幕显示指定内容_初始化_06


若是只要求在屏幕上显示CGROM中已经拥有的字符,那就仅仅需要在DDRAM中写入它的字符代码就可以了;若是想显示的是CGROM中不存在的字符,例如美元的符号,那就只能先在CGRAM中规定,下一步再在DDRAM中写入我们之前自己定义的字符就可以。


android 副屏内容显示到主屏 副屏幕显示指定内容_android 副屏内容显示到主屏_07


上面这个图说明的是5×8点阵和5×10点阵字符的字形和光标的位置。这里我们采用的是5×8点阵,那么定义这样一个字符需要8个字节,每个字节的前3个位没有被使用。


android 副屏内容显示到主屏 副屏幕显示指定内容_初始化_08


上面这个图说明的是设置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高电平有效。

读操作时序图:


android 副屏内容显示到主屏 副屏幕显示指定内容_树莓派_09


写操作时序图:


android 副屏内容显示到主屏 副屏幕显示指定内容_数据_10


时序时间参数:


android 副屏内容显示到主屏 副屏幕显示指定内容_数据_11


3.4 LCD1602的指令说明

1602液晶模块内部的控制器共有11条控制指令:


android 副屏内容显示到主屏 副屏幕显示指定内容_初始化_12


1602液晶模块的读写操作、屏幕和光标的操作都是通过指令编程来实现的。

指令1:清显示,指令码01H,光标复位到地址00H位置。
说明:清除屏幕显示内容。光标返回屏幕左上角。执行这个指令时需要一定时间。指令2:光标复位,光标返回到地址00H。
说明:光标返回屏幕左上角,它不改变屏幕显示内容。指令3:光标和显示模式设置


android 副屏内容显示到主屏 副屏幕显示指定内容_数据_13


I/D=1:写入新数据后光标右移。
I/D=0:写入新数据后光标左移。
S=1:显示移动。
S=0:显示不移动。
说明:这里的设置是0x06。

指令4:显示开关控制。


android 副屏内容显示到主屏 副屏幕显示指定内容_总线与微命令实验总结_14


D=1:显示开,D=0:显示关。
C=1:光标显示,C=0:光标不显示。
B=1:光标闪烁,B=0:光标不闪烁。
说明:这里的设置是显示开,不显示光标,光标不闪烁,设置字为0x0c。

指令5:光标或显示移位


android 副屏内容显示到主屏 副屏幕显示指定内容_初始化_15


android 副屏内容显示到主屏 副屏幕显示指定内容_初始化_16


说明:在需要进行整屏移动时,这个指令非常有用,可以实现屏幕的滚动显示效果。初始化时不使用这个指令。

指令6:功能设置命令


android 副屏内容显示到主屏 副屏幕显示指定内容_android 副屏内容显示到主屏_17


×:不关心,也就是说这个位是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地址设置。


android 副屏内容显示到主屏 副屏幕显示指定内容_初始化_08


指令8:DDRAM地址设置。


android 副屏内容显示到主屏 副屏幕显示指定内容_android 副屏内容显示到主屏_19


说明:这个指令用于设置DDRAM地址。在对DDRAM进行读写之前,首先要设置DDRAM地址,然后才能进行读写。前面我们说过,DDRAM就是LCD1602的显示存储器。我们要在它上面进行显示,就要把要显示的字符写入DDRAM。同样,我们想知道DDRAM某个地址上有什么字符,也要先设置DDRAM地址,然后将它读出到单片机。

指令9:读忙信号和光标地址


android 副屏内容显示到主屏 副屏幕显示指定内容_android 副屏内容显示到主屏_20


BF:为忙标志位,高电平表示忙,此时模块不能接收命令或者数据。如果为低电平表示不忙。
说明:这个指令用来读取LCD1602状态。对于单片机来说,LCD1602属于慢速设备。当单片机向其发送一个指令后,它将去执行这个指令。这时如果单片机再次发送下一条指令,由于LCD1602速度较慢,前一条指令还未执行完毕,它将不接受这新的指令,导致新的指令丢失。因此这条读忙指令可以用来判断LCD1602是否忙,能否接收单片机发来的指令。当BF=1,表示LCD1602正忙,不能接受单片机的指令;当BF=0,表示LCD1602空闲,可以接收单片机的指令。RS=0,表示是指令;RW=1,表示是读取。这条指令还有一个副产品:即可以得到地址记数器AC的值(address counter)。LCD1602维护了一个地址计数器AC,用来记录下一次读写CGRAM或DDRAM的位置。需要强调的是:这条指令我一次也没有执行成功。很多网友似乎也是这样。好在我们有另外的办法,也就是延时。通过查看每条指令的执行时间,再经过一些试验,可以确定指令的延时。这样就可以在上一条指令执行完毕后再执行下一条指令了。指令10:写数据。


android 副屏内容显示到主屏 副屏幕显示指定内容_数据_21


说明:RS=1,数据;RW=0,写。指令执行时,要在DB7—DB0上先设置好要写入的数据,然后执行写命令。

指令11:读数据。


android 副屏内容显示到主屏 副屏幕显示指定内容_树莓派_22


说明:RS=1,数据;RW=1,读。先设置好CGRAM或DDRAM的地址,然后执行读取命令。数据就被读入后DB7—DB0。

3.5 初始化

如果电路电源能满足内部RESET电路的如下要求, 初始化可自动完成:


android 副屏内容显示到主屏 副屏幕显示指定内容_初始化_23


如果电路电源不能满足内部RESET电路的要求的话,需要用初始化程序来实现初始化,有8位总线和4位总线两种模式。

8位数据传输模式:


android 副屏内容显示到主屏 副屏幕显示指定内容_树莓派_24


本次实验中使用4位数据传输模式:


android 副屏内容显示到主屏 副屏幕显示指定内容_android 副屏内容显示到主屏_25


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


android 副屏内容显示到主屏 副屏幕显示指定内容_初始化_26


android 副屏内容显示到主屏 副屏幕显示指定内容_树莓派_27


第2步:PCF8591模块采用的是I2C(IIC)总线进行通信的,但是在树莓派的镜像中默认是关闭的,在使用该传感器的时候,我们必须首先允许IIC总线通信。


android 副屏内容显示到主屏 副屏幕显示指定内容_android 副屏内容显示到主屏_28


第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’为要显示的字符串