樹莓派基礎實驗29:I2C LCD1602實驗

一、介紹

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

★跳線若干

三、實驗原理

LCD1602正面
LCD1602背面
LCD1602電路圖

?? 樹莓派的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的字模

??上面的圖左側顯示的就是“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地址指令

??上面這個圖說明的是設置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條控制指令:


LCD1602控制命令表

?? 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地址設置。

設置CGRAM地址指令

指令8:DDRAM地址設置。

設置DDRAM地址指令

說明:這個指令用于設置DDRAM地址。在對DDRAM進行讀寫之前,首先要設置DDRAM地址,然后才能進行讀寫。前面我們說過,DDRAM就是LCD1602的顯示存儲器。我們要在它上面進行顯示,就要把要顯示的字符寫入DDRAM。同樣,我們想知道DDRAM某個地址上有什么字符,也要先設置DDRAM地址,然后將它讀出到單片機。

指令9:讀忙信號和光標地址

讀忙信號和地址計數器AC

BF:為忙標志位,高電平表示忙,此時模塊不能接收命令或者數據。如果為低電平表示不忙。
說明:這個指令用來讀取LCD1602狀態。對于單片機來說,LCD1602屬于慢速設備。當單片機向其發送一個指令后,它將去執行這個指令。這時如果單片機再次發送下一條指令,由于LCD1602速度較慢,前一條指令還未執行完畢,它將不接受這新的指令,導致新的指令丟失。因此這條讀忙指令可以用來判斷LCD1602是否忙,能否接收單片機發來的指令。當BF=1,表示LCD1602正忙,不能接受單片機的指令;當BF=0,表示LCD1602空閑,可以接收單片機的指令。RS=0,表示是指令;RW=1,表示是讀取。這條指令還有一個副產品:即可以得到地址記數器AC的值(address counter)。LCD1602維護了一個地址計數器AC,用來記錄下一次讀寫CGRAM或DDRAM的位置。需要強調的是:這條指令我一次也沒有執行成功。很多網友似乎也是這樣。好在我們有另外的辦法,也就是延時。通過查看每條指令的執行時間,再經過一些試驗,可以確定指令的延時。這樣就可以在上一條指令執行完畢后再執行下一條指令了。
指令10:寫數據。

寫數據到CGRAM或DDRAM指令

說明:RS=1,數據;RW=0,寫。指令執行時,要在DB7—DB0上先設置好要寫入的數據,然后執行寫命令。

指令11:讀數據。

從CGRAM或DDRAM讀數據指令

說明:RS=1,數據;RW=1,讀。先設置好CGRAM或DDRAM的地址,然后執行讀取命令。數據就被讀入后DB7—DB0。

3.5 初始化

??如果電路電源能滿足內部RESET電路的如下要求, 初始化可自動完成:


自動初始化

??如果電路電源不能滿足內部RESET電路的要求的話,需要用初始化程序來實現初始化,有8位總線和4位總線兩種模式。

8位數據傳輸模式:


8位總線模式初始化

本次實驗中使用4位數據傳輸模式:


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
LCD1602實驗電路圖
LCD1602實驗實物接線圖

??第2步:PCF8591模塊采用的是I2C(IIC)總線進行通信的,但是在樹莓派的鏡像中默認是關閉的,在使用該傳感器的時候,我們必須首先允許IIC總線通信。

打開I2C總線通信

??第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()

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。