一、介紹
?? 眾所周知,雖然液晶顯示器和其他顯示器大大的豐富了人機交互,但他們有一個共同的弱點。當它們連接到控制器時,需要占用大量的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總線通信。
??第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()