1. 程式人生 > >[FPGA]淺談LCD1602字元型液晶顯示器(Verilog)

[FPGA]淺談LCD1602字元型液晶顯示器(Verilog)

目錄

  • 概述
  • LCD1602
    • LCD1602是什麼?
    • LCD1602的管腳
      • RS_資料/命令選擇
      • E_使能
      • D0-D7
    • LCD1602有個DDRAM
    • LCD1602還有個CGROM
    • 指令集
      • 清屏
      • 進入模式設定
      • 顯示開關設定
      • 工作方式設定
      • 資料儲存器地址設定
  • Verilog驅動
    • 模組定義
    • 上電穩定
    • 工作週期分頻
    • 狀態機
    • RS端控制
    • 顯示控制
    • 自定義字元輸入
  • 效果展示
  • 總結
  • 參考資料

概述

本文圍繞LCD1602字元型液晶顯示器展開,並在FPGA開發板上用VerilogHDL語言實現模組驅動.

首先來一張效果展示

那麼怎麼在這塊綠油油的平面上顯示出點陣構成的字元呢?本文將為你提供一些思路.

注:本文僅討論寫入操作,實現在LCD1602上顯示指定字串,不講解讀取相關操作.

LCD1602

LCD1602是什麼?

LCD1602是一種字元型液晶顯示模組,不同於七段數碼管,它可以通過點陣的形式顯示出各種圖案或字元,可拓展性較強.

其名稱中"LCD"即為 Liquid Crystal Display (液晶顯示器),"1602"代表顯示屏上可同時顯示32個字元(16x2).

LCD1602的管腳

LCD1602共有16根管腳(部分型號只有14根,沒有背光管腳),管腳功能表如下

符號 管腳說明 符號 管腳說明
VSS 電源地 D2 資料
VDD 電源正極 D3 資料
VL 偏壓 D4 資料
RS 資料/命令選擇 D5 資料
R/W 讀/寫選擇 D6 資料
E 使能 D7 資料
D0 資料 BLA 背光正極
D1 資料 BLK 背光負極

其中需要我們關心的只有RS,E,和D0-D7.

RS_資料/命令選擇

RS端用來控制輸入給D0-D7的序列代表命令還是資料.

如果代表輸入命令,則輸入給D0-D7的序列相當於對模組進行設定(下文會有輸入序列對應的指令表及其功能);如果代表輸入資料,則輸入給D0-D7的序列相當於寫入需要顯示的字串(輸入的是每個字元所對應的地址碼).

若RS為低電平,代表輸入命令;若RS為高電平,代表輸入資料.

E_使能

E端是用來執行命令的使能引腳,當它從高電平變成低電平時(下降沿),液晶模組執行命令.

D0-D7

八位雙向並行資料線,在本文中僅作輸入端(寫入).

LCD1602有個DDRAM

DDRAM( Display Data Random Access Memory )即為顯示資料隨機存取儲存器,相當於"視訊記憶體",用來存放待顯示的字元程式碼.

DDRAM一共有80個位元組,它和1602的顯示屏上32個字元位的對應地址如下圖

第一行的16個字元位的地址對應0x00-0x0F,第二行則對應0x40-0x4F("0x"代表16進位制數).

LCD1602還有個CGROM

CGROM( Character Generator Read-Only Memory )即為字元產生只讀儲存器,用來存放192個常用字元的字模.

值得一提的是,表中的左半部分字元和他們的ASCII碼是對應的,所以在寫程式碼時可以直接寫成"A"而不必要寫成"0x41".

另外還有一個CGRAM用來存放使用者自定義的字元,可存放8個5x8字元或4個5x10字元,不過這不在本文討論範圍內.

指令集

前文已經提到,當RS為低電平時,代表輸入命令,那麼這些命令都有哪些呢?

將能實現某種功能的序列稱為一條命令,每條命令有幾個固定的位和幾個可變的位,可變的位可以改變功能/模式,將這些命令總稱為指令集.全體指令集如下表

指令 RS R/W D7 D6 D5 D4 D3 D2 D1 D0
清屏 0 0 0 0 0 0 0 0 0 1
游標復位 0 0 0 0 0 0 0 0 0 x
進入模式設定 0 0 0 0 0 0 0 1 I/D S
顯示開關設定 0 0 0 0 0 0 1 D C B
移位控制 0 0 0 0 0 1 S/C R/L x x
工作方式設定 0 0 0 0 1 DL N F x x
字元發生器地址設定 0 0 0 1 a a a a a a
資料儲存器地址設定 0 0 1 b b b b b b b
讀忙標誌或地址 0 1 BF c c c c c c c
寫入資料至CDRAM或DDRAM 1 0 d d d d d d d d
從CGRAM或DDRAM中讀取資料 1 1 e e e e e e e e

注:其中a代表字元發生儲存器地址,b代表顯示資料儲存器地址,c代表計數器地址,d代表要寫入的資料內容,e代表讀取的資料內容.

我們關心的是其中的清屏,進入模式設定,顯示開關設定,工作方式設定,資料儲存器地址設定.

清屏

清除螢幕顯示內容,游標返回螢幕左上角.

執行這個指令時需要一定時間.

進入模式設定

I/D = 1:寫入新資料後游標右移,I/D = 0:寫入新資料後游標左移

S = 1:顯示移動,S = 0:顯示不移動.

顯示開關設定

D = 1:顯示功能開,D = 0,顯示功能關(但是DDRAM中的資料依然保留).

C = 1:有游標,C = 0,沒有游標.

B = 1:游標閃爍,B = 0.游標不閃爍.

工作方式設定

DL = 1:8位資料介面(D7-D0),DL = 0:4位資料介面(D7-D4).

N = 0:一行顯示,N = 1;兩行顯示.

F = 0: 5x8點陣字元,F = 1: 5x10點陣字元.

資料儲存器地址設定

在對DDRAM進行讀寫之前,首先要設定DDRAM地址,然後才能進行讀寫.

地址設定見第一張圖.

Verilog驅動

瞭解了1602的原理和功能後,就可以著手編寫驅動模組了.想要讓LCD1602顯示指定的字元,需要有一個驅動程式將模組和使用者連線起來,實現輸入什麼就輸出什麼的功能,並能夠簡單的進行設定.

接下來開始寫驅動(造輪子).分為若干個次級模組逐個分析.

模組定義

模組共有5個埠(其中8個數據端合為一個8位寬埠),分別為CLK時鐘輸入端,_RST低電平有效的復位端,LCD_E使能端,LCD_RS資料/命令選擇端,LCD_DATA資料端.

module LCD1602
(input CLK
,input _RST
,output LCD_E 
,output reg LCD_RS
,output reg[7:0]LCD_DATA
);

上電穩定

這是一個簡單的初始化模組,資料手冊要求要先通電20ms才可以進行下一步操作,為了使之上電穩定.

parameter TIME_20MS=1_000_000;//需要20ms以達上電穩定(初始化)
reg[19:0]cnt_20ms;
always@(posedge CLK or negedge _RST)
    if(!_RST)
        cnt_20ms<=1'b0;
    else if(cnt_20ms==TIME_20MS-1'b1)
        cnt_20ms<=cnt_20ms;
    else
        cnt_20ms<=cnt_20ms+1'b1 ;

wire delay_done=(cnt_20ms==TIME_20MS-1'b1)?1'b1:1'b0;//上電延時完畢

工作週期分頻

LCD1602的工作週期為500Hz,所以要進行分頻(板載晶振為50MHz).

parameter TIME_500HZ=100_000;//工作週期
reg[19:0]cnt_500hz;
always@(posedge CLK or negedge _RST)
    if(!_RST)
        cnt_500hz<=1'b0;
    else if(delay_done)
        if(cnt_500hz==TIME_500HZ-1'b1)
            cnt_500hz<=1'b0;
        else
            cnt_500hz<=cnt_500hz+1'b1;
    else
        cnt_500hz<=1'b0;

assign LCD_E=(cnt_500hz>(TIME_500HZ-1'b1)/2)?1'b0:1'b1;//使能端,每個工作週期一次下降沿,執行一次命令
wire write_flag=(cnt_500hz==TIME_500HZ-1'b1)?1'b1:1'b0;//每到一個工作週期,write_flag置高一週期

狀態機

模組工作採用狀態機驅動.

//狀態機有40種狀態,此處用了格雷碼,一次只有一位變化(在二進位制下)
parameter IDLE=8'h00;
parameter SET_FUNCTION=8'h01;
parameter DISP_OFF=8'h03;
parameter DISP_CLEAR=8'h02;
parameter ENTRY_MODE=8'h06;
parameter DISP_ON=8'h07;
parameter ROW1_ADDR=8'h05;
parameter ROW1_0=8'h04;
parameter ROW1_1=8'h0C;
parameter ROW1_2=8'h0D;
parameter ROW1_3=8'h0F;
parameter ROW1_4=8'h0E;
parameter ROW1_5=8'h0A;
parameter ROW1_6=8'h0B;
parameter ROW1_7=8'h09;
parameter ROW1_8=8'h08;
parameter ROW1_9=8'h18;
parameter ROW1_A=8'h19;
parameter ROW1_B=8'h1B;
parameter ROW1_C=8'h1A;
parameter ROW1_D=8'h1E;
parameter ROW1_E=8'h1F;
parameter ROW1_F=8'h1D;
parameter ROW2_ADDR=8'h1C;
parameter ROW2_0=8'h14;
parameter ROW2_1=8'h15;
parameter ROW2_2=8'h17;
parameter ROW2_3=8'h16;
parameter ROW2_4=8'h12;
parameter ROW2_5=8'h13;
parameter ROW2_6=8'h11;
parameter ROW2_7=8'h10;
parameter ROW2_8=8'h30;
parameter ROW2_9=8'h31;
parameter ROW2_A=8'h33;
parameter ROW2_B=8'h32;
parameter ROW2_C=8'h36;
parameter ROW2_D=8'h37;
parameter ROW2_E=8'h35;
parameter ROW2_F=8'h34;

reg[5:0]c_state;//Current state,當前狀態
reg[5:0]n_state;//Next state,下一狀態

always@(posedge CLK or negedge _RST)
    if(!_RST)
        c_state<=IDLE;
    else if(write_flag)//每一個工作週期改變一次狀態
        c_state<=n_state;
    else
        c_state<=c_state;

always@(*)
    case (c_state)
        IDLE:n_state=SET_FUNCTION;
        SET_FUNCTION:n_state=DISP_OFF;
        DISP_OFF:n_state=DISP_CLEAR;
        DISP_CLEAR:n_state=ENTRY_MODE;
        ENTRY_MODE:n_state=DISP_ON;
        DISP_ON:n_state=ROW1_ADDR;
        ROW1_ADDR:n_state=ROW1_0;
        ROW1_0:n_state=ROW1_1;
        ROW1_1:n_state=ROW1_2;
        ROW1_2:n_state=ROW1_3;
        ROW1_3:n_state=ROW1_4;
        ROW1_4:n_state=ROW1_5;
        ROW1_5:n_state=ROW1_6;
        ROW1_6:n_state=ROW1_7;
        ROW1_7:n_state=ROW1_8;
        ROW1_8:n_state=ROW1_9;
        ROW1_9:n_state=ROW1_A;
        ROW1_A:n_state=ROW1_B;
        ROW1_B:n_state=ROW1_C;
        ROW1_C:n_state=ROW1_D;
        ROW1_D:n_state=ROW1_E;
        ROW1_E:n_state=ROW1_F;
        ROW1_F:n_state=ROW2_ADDR;
        ROW2_ADDR:n_state=ROW2_0;
        ROW2_0:n_state=ROW2_1;
        ROW2_1:n_state=ROW2_2;
        ROW2_2:n_state=ROW2_3;
        ROW2_3:n_state=ROW2_4;
        ROW2_4:n_state=ROW2_5;
        ROW2_5:n_state=ROW2_6;
        ROW2_6:n_state=ROW2_7;
        ROW2_7:n_state=ROW2_8;
        ROW2_8:n_state=ROW2_9;
        ROW2_9:n_state=ROW2_A;
        ROW2_A:n_state=ROW2_B;
        ROW2_B:n_state=ROW2_C;
        ROW2_C:n_state=ROW2_D;
        ROW2_D:n_state=ROW2_E;
        ROW2_E:n_state=ROW2_F;
        ROW2_F:n_state=ROW1_ADDR;//迴圈到1-1進行掃描顯示
        default:;
    endcase

RS端控制

控制輸入為資料或命令

always@(posedge CLK or negedge _RST)
    if(!_RST)
        LCD_RS<=1'b0;//為0時輸入指令,為1時輸入資料
    else if(write_flag)
        //當狀態為七個指令任意一個,將RS置為指令輸入狀態
        if((n_state==SET_FUNCTION)||(n_state==DISP_OFF)||(n_state==DISP_CLEAR)||(n_state==ENTRY_MODE)||(n_state==DISP_ON)||(n_state==ROW1_ADDR)||(n_state==ROW2_ADDR))
            LCD_RS<=1'b0; 
        else
            LCD_RS<=1'b1;
    else
        LCD_RS<=LCD_RS;

顯示控制

always@(posedge CLK or negedge _RST)
    if(!_RST)
        LCD_DATA<=1'b0;
    else if(write_flag)
        case(n_state)
            IDLE:LCD_DATA<=8'hxx;
            SET_FUNCTION:LCD_DATA<=8'h38;//8'b0011_1000,工作方式設定:DL=1(DB4,8位資料介面),N=1(DB3,兩行顯示),L=0(DB2,5x8點陣顯示).
            DISP_OFF:LCD_DATA<=8'h08;//8'b0000_1000,顯示開關設定:D=0(DB2,顯示關),C=0(DB1,游標不顯示),D=0(DB0,游標不閃爍)
            DISP_CLEAR:LCD_DATA<=8'h01;//8'b0000_0001,清屏
            ENTRY_MODE:LCD_DATA<=8'h06;//8'b0000_0110,進入模式設定:I/D=1(DB1,寫入新資料游標右移),S=0(DB0,顯示不移動)
            DISP_ON:LCD_DATA<=8'h0c;//8'b0000_1100,顯示開關設定:D=1(DB2,顯示開),C=0(DB1,游標不顯示),D=0(DB0,游標不閃爍)
            ROW1_ADDR:LCD_DATA<=8'h80;//8'b1000_0000,設定DDRAM地址:00H->1-1,第一行第一位
            //將輸入的row_1以每8-bit拆分,分配給對應的顯示位
            ROW1_0:LCD_DATA<=row_1[127:120];
            ROW1_1:LCD_DATA<=row_1[119:112];
            ROW1_2:LCD_DATA<=row_1[111:104];
            ROW1_3:LCD_DATA<=row_1[103: 96];
            ROW1_4:LCD_DATA<=row_1[ 95: 88];
            ROW1_5:LCD_DATA<=row_1[ 87: 80];
            ROW1_6:LCD_DATA<=row_1[ 79: 72];
            ROW1_7:LCD_DATA<=row_1[ 71: 64];
            ROW1_8:LCD_DATA<=row_1[ 63: 56];
            ROW1_9:LCD_DATA<=row_1[ 55: 48];
            ROW1_A:LCD_DATA<=row_1[ 47: 40];
            ROW1_B:LCD_DATA<=row_1[ 39: 32];
            ROW1_C:LCD_DATA<=row_1[ 31: 24];
            ROW1_D:LCD_DATA<=row_1[ 23: 16];
            ROW1_E:LCD_DATA<=row_1[ 15:  8];
            ROW1_F:LCD_DATA<=row_1[  7:  0];
            ROW2_ADDR:LCD_DATA<=8'hc0;//8'b1100_0000,設定DDRAM地址:40H->2-1,第二行第一位
            ROW2_0:LCD_DATA<=row_2[127:120];
            ROW2_1:LCD_DATA<=row_2[119:112];
            ROW2_2:LCD_DATA<=row_2[111:104];
            ROW2_3:LCD_DATA<=row_2[103: 96];
            ROW2_4:LCD_DATA<=row_2[ 95: 88];
            ROW2_5:LCD_DATA<=row_2[ 87: 80];
            ROW2_6:LCD_DATA<=row_2[ 79: 72];
            ROW2_7:LCD_DATA<=row_2[ 71: 64];
            ROW2_8:LCD_DATA<=row_2[ 63: 56];
            ROW2_9:LCD_DATA<=row_2[ 55: 48];
            ROW2_A:LCD_DATA<=row_2[ 47: 40];
            ROW2_B:LCD_DATA<=row_2[ 39: 32];
            ROW2_C:LCD_DATA<=row_2[ 31: 24];
            ROW2_D:LCD_DATA<=row_2[ 23: 16];
            ROW2_E:LCD_DATA<=row_2[ 15:  8];
            ROW2_F:LCD_DATA<=row_2[  7:  0];
        endcase
    else
        LCD_DATA<=LCD_DATA;

自定義字元輸入

輸入要顯示的字元.

wire[127:0]row_1;
wire[127:0]row_2;
assign row_1 ="   Welcome to   ";//第一行顯示的內容(16個字元)
assign row_2 ="    My Blog!    ";//第二行顯示的內容(16個字元)

效果展示

將以上程式碼有機整合後,燒錄至開發板上,按下復位鍵即可看到顯示屏上顯示出了指定字樣.

你可以修改字串來讓螢幕顯示出不同的內容,甚至可以調整模式讓顯示屏滾動顯示大於16字元的字串.

總結

LCD1602是一個很基礎的模組,把這個掌握後對以後的學習幫助很大,所以很有必要學習.

這個模組不止可以通過Verilog驅動,也可以用其他語言或其他開發板來實現,例如STM32,51微控制器或者SV,VHDL語言,都可以寫一套讓他工作的驅動.

另外,如果有現成的輪子,為什麼還要自己造一個出來呢?在碰到類似情況時可以藉助網際網路參考一下別人對此問題有怎麼樣的解決方案,加以借鑑並內化於心,才能達到最高效率的學習.

參考資料

[1] aslmer. "verilog寫的LCD1602 顯示"[ED/OL]. https://www.cnblogs.com/aslmer/p/5819422.html ,2016(8).

[2] aslmer. "LCD1602指令集解讀"[ED/OL]. https://www.cnblogs.com/aslmer/p/5801363.html ,2016(8).

[3] 阿忠ZHONG. "微控制器顯示原理(LCD1602)"[ED/OL]. https://www.cnblogs.com/hui088/p/4732034.html 2015(8).

[4] 百度百科. "詞條-LCD1602"[ED/OL]. https://baike.baidu.com/item/LCD1602/6014393 ,2019(9).

[5] HITACHI©Ltd. "HD44780U (LCD-II)(Dot Matrix Liquid Crystal Display Controller/Driver)"[M]. Japan HITACHI,1998.


本作品採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。