天天看點

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作

一、LCD1602顯示原理

1、引腳功能

其内部功能框圖如下圖所示:

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作

一般來說,LCD1602有16條引腳,各個廠家的LCD1602可能略有不同,但基本上一樣,其16個引腳功能如下:

LCD1602引腳功能

引腳号 引腳名 電壓等級 功能
1 VSS 0V 電源地
2 VDD +5V 電源正極
3 V0 0V 電壓偏置
4 RS H/L 指令/資料
5 R/W H/L 讀/寫
6 E H/L 使能
7~14 DB0~DB7 H/L 資料端口
15 LEDA +5V 背光正極
16 LEDK 0V 背光負極

對這個表的說明:

1.    VSS接電源地。

2.    VDD接+5V。

3.    VO是液晶顯示的偏壓信号,可接10K的3296精密電位器。或同樣阻值的RM065/RM063藍白可調電阻。見下圖。

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作

4.    RS是指令/資料選擇引腳,接單片機的一個I/O,當RS為低電平時,選擇指令;當RS為高電平時,選擇資料。

5.    RW是讀/寫選擇引腳,接單片機的一個I/O,當RW為低電平時,向LCD1602寫入指令或資料;當RW為高電平時,從LCD1602讀取狀态或資料。如果不需要進行讀取操作,可以直接将其接VSS。

6.    E,執行指令的使能引腳,接單片機的一個I/O。

7.    D0—D7,并行資料輸入/輸出引腳,可接單片機的P0—P3任意的8個I/O口。如果接P0口,P0口應該接4.7K—10K的上拉電阻。如果是4線并行驅動,隻須接4個I/O口。

8.    A背光正極,可接一個10—47歐的限流電阻到VDD。

9.    K背光負極,接VSS。見下圖所示。

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作

2、基本操作

LCD1602的基本操作分為四種:

1.    讀狀态:輸入RS=0,RW=1,E=高脈沖。輸出:D0—D7為狀态字。

2.    讀資料:輸入RS=1,RW=1,E=高脈沖。輸出:D0—D7為資料。

3.    寫指令:輸入RS=0,RW=0,E=高脈沖。輸出:無。

4.    寫資料:輸入RS=1,RW=0,E=高脈沖。輸出:無。

讀操作時序圖和限制:

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作

 寫操作時序圖:

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作

時序時間參數:

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作

3、DDRAM、CGROM和CGRAM

DDRAM(Display Data RAM)就是顯示資料RAM,用來寄存待顯示的字元代碼。共80個位元組,其位址和螢幕的對應關系如下:

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作

DDRAM相當于計算機的顯存,我們為了在螢幕上顯示字元,就把字元代碼送入顯存,這樣該字元就可以顯示在螢幕上了。同樣LCD1602共有80個位元組的顯存,即DDRAM。但LCD1602的顯示螢幕隻有16×2大小,是以,并不是所有寫入DDRAM的字元代碼都能在螢幕上顯示出來,隻有寫在上圖所示範圍内的字元才可以顯示出來,寫在範圍外的字元不能顯示出來。這樣,我們在程式中可以利用下面的“光标或顯示移動指令”使字元慢慢移動到可見的顯示範圍内,看到字元的移動效果。

前面說了,為了在液晶螢幕上顯示字元,就把字元代碼送入DDRAM。例如,如果想在螢幕左上角顯示字元‘A’,那麼就把字元‘A’的字元代碼41H寫入DDRAM的00H位址處即可。至于怎麼寫入,後面會有說明。那麼為什麼把字元代碼寫入DDRAM,就可以在相應位置顯示這個代碼的字元呢?我們知道,LCD1602是一種字元點陣顯示器,為了顯示一種字元的字形,必須要有這個字元的字模資料,什麼叫字元的字模資料,看看下面的這個圖就明白了。

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作

上圖的左邊就是字元‘A’的字模資料,右邊就是将左邊資料用“○”代表0,用“■”代表1。進而顯示出‘A’這個字形。從下面的圖可以看出,字元‘A’的高4位是0100,低4位是0001,合在一起就是01000001b,即41H。它恰好與該字元的ASCII碼一緻,這樣就給了我們很大的友善,我們可以在PC上使用P2=‘A’這樣的文法。編譯後,正好是這個字元的字元代碼。

在LCD1602子產品上固化了字模存儲器,就是CGROM和CGRAM,HD44780内置了192個常用字元的字模,存于字元産生器CGROM(Character Generator ROM)中,另外還有8個允許使用者自定義的字元産生RAM,稱為CGRAM(Character Generator RAM)。下圖(如圖12)說明了CGROM和CGRAM與字元的對應關系。從ROM和RAM的名字我們也可以知道,ROM是早已固化在LCD1602子產品中的,隻能讀取;而RAM是可讀寫的。也就是說,如果隻需要在螢幕上顯示已存在于CGROM中的字元,那麼隻須在DDRAM中寫入它的字元代碼就可以了;但如果要顯示CGROM中沒有的字元,比如攝氏溫标的符号,那麼就隻有先在CGRAM中定義,然後再在DDRAM中寫入這個自定義字元的字元代碼即可。和CGROM中固化的字元不同,CGRAM中本身沒有字元,是以要在DDRAM中寫入某個CGROM不存在的字元,必須在CGRAM中先定義後使用。程式退出後CGRAM中定義的字元也不複存在,下次使用時,必須重新定義。

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作

上面這個圖說明的是5×8點陣和5×10點陣字元的字形和光标的位置。先來說5×8點陣,它有8行5列。那麼定義這樣一個字元需要8個位元組,每個位元組的前3個位沒有被使用。例如,定義攝氏溫标的符号{0x10,0x06,0x09,0x08,0x08,0x09,0x06,0x00}。

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作

上面這個圖說明的是設定CGRAM位址指令。從這個指令的格式中我們可以看出,它共有aaaaaa這6位,一共可以表示64個位址,即64個位元組。一個5×8點陣字元共占用8個位元組,那麼這64個位元組一共可以自定義8個字元。也就是說,上面這個圖的6位位址中的DB5DB4DB3用來表示8個自定義的字元,DB2DB1DB0用來表示每個字元的8個位元組。這DB5DB4DB3所表示的8個自定義字元(0--7)就是要寫入DDRAM中的字元代碼。我們知道,在CGRAM中隻能定義8個自定義字元,也就是隻有0—7這8個字元代碼,但在下面的這個表中一共有16個字元代碼(××××0000b--××××1111b)。實際上,如圖所示,它隻能表示8個自定義字元 (××××0000b=××××1000b, ××××0001b=××××1001b……依次類推)。也就是說,寫入DDRAM中的字元代碼0和字元代碼8是同一個自定義字元。 5×10點陣每個字元共占用16個位元組的空間,是以CGRAM中隻能定義4個這樣的自定義字元。

那麼如何在CGRAM中自定義字元呢?在上面的介紹中,我們知道有一個設定CGRAM位址指令,同寫DDRAM指令相似,隻須設定好某個自定義字元的字模資料,然後按照上面介紹的方法,設定好CGRAM位址,依次寫入這個字模資料即可。我們在後面的例子中再進行說明。

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作

4、LCD1602指令

(1).工作方式設定指令

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作

×:不關心,也就是說這個位是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位資料接口。這裡我們選擇8位資料接口(D7—D0)。我們的設定是8位資料接口,兩行顯示,5×8點陣,即0b00111000也就是0x38。(注意:NF是10或11的效果是一樣的,都是兩行5×8點陣。因為它不能以兩行5×10點陣方式進行顯示,換句話說,這裡用0x38或0x3c是一樣的)。

(2).顯示開關控制指令

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作

D=1:顯示開,D=0:顯示關。

C=1:光标顯示,C=0:光标不顯示。

B=1:光标閃爍,B=0:光标不閃爍。

說明:這裡的設定是顯示開,不顯示光标,光标不閃爍,設定字為0x0c。

(3).進入模式設定指令

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作

I/D=1:寫入新資料後光标右移。

I/D=0:寫入新資料後光标左移。

S=1:顯示移動。

S=0:顯示不移動。

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作

說明:這裡的設定是0x06。

(4).光标或顯示移動指令

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作
基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作

說明:在需要進行整屏移動時,這個指令非常有用,可以實作螢幕的滾動顯示效果。初始化時不使用這個指令。

(5).清屏指令

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作

說明:清除螢幕顯示内容。光标傳回螢幕左上角。執行這個指令時需要一定時間。

(6).光标歸位指令

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作

說明:光标傳回螢幕左上角,它不改變螢幕顯示内容。

(7).設定CGRAM位址指令

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作

說明:這個指令在上面已經介紹過。用法在後面例子中說明。

(8).設定DDRAM位址指令

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作

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

(9).讀忙信号和位址計數器AC

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作

說明:這個指令用來讀取LCD1602狀态。對于單片機來說,LCD1602屬于慢速裝置。當單片機向其發送一個指令後,它将去執行這個指令。這時如果單片機再次發送下一條指令,由于LCD1602速度較慢,前一條指令還未執行完畢,它将不接受這新的指令,導緻新的指令丢失。是以這條讀忙指令可以用來判斷LCD1602是否忙,能否接收單片機發來的指令。當BF=1,表示LCD1602正忙,不能接受單片機的指令;當BF=0,表示LCD1602空閑,可以接收單片機的指令。RS=0,表示是指令;RW=1,表示是讀取。這條指令還有一個副産品:即可以得到位址記數器AC的值(address counter)。LCD1602維護了一個位址計數器AC,用來記錄下一次讀寫CGRAM或DDRAM的位置。需要強調的是:這條指令我一次也沒有執行成功。很多網友似乎也是這樣。好在我們有另外的辦法,也就是延時。通過檢視每條指令的執行時間,再經過一些試驗,可以确定指令的延時。這樣就可以在上一條指令執行完畢後再執行下一條指令了。

(10).寫資料到CGRAM或DDRAM指令

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作

說明:RS=1,資料;RW=0,寫。指令執行時,要在DB7—DB0上先設定好要寫入的資料,然後執行寫指令。

(11).從CGRAM或DDRAM讀資料指令

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作

說明:RS=1,資料;RW=1,讀。先設定好CGRAM或DDRAM的位址,然後執行讀取指令。資料就被讀入後DB7—DB0。

5、LCD1602一般初始換步驟(在FPGA裡邊必須初始化)

  1. 延時15mS
  2. 寫指令38H(不檢測忙信号)
  3. 延時15mS
  4. 寫指令38H(不檢測忙信号)
  5. 延時15mS
  6. 寫指令38H(不檢測忙信号)
  7. 以後每次寫指令、讀/寫資料操作均需要檢測忙信号
  8. 寫指令38H:顯示模式設定
  9. 寫指令08H:顯示關閉
  10. 寫指令01H:顯示清屏
  11. 寫指令06H:顯示光标移動設定
  12. 寫指令0CH:顯示開及光标設定

11和12 兩個順序不能互換,我在寫的時候調換了怎麼也調試不出來,左後将兩個順序互換過來就正常了。

二、FPGA實作

實作功能:在LCD顯示屏上顯示兩行,第一行顯示:Pan-Hong-Feng;第二行顯示:LCD1602-Test

代碼如下:(代碼有點亂,懶得注釋了)

// Company  : 
// Engineer : 
// -----------------------------------------------------------------------------
// https://blog.csdn.net/qq_33231534    PHF's CSDN blog
// -----------------------------------------------------------------------------
// Create Date    : 2020-09-07 15:48:40
// Revise Data    : 2020-09-08 09:53:32
// File Name      : lcd.v
// Target Devices : XC7Z015-CLG485-2
// Tool Versions  : Vivado 2019.2
// Revision       : V1.1
// Editor         : sublime text3, tab size (4)
// Description    : LCD1602 driver

module lcd(
	input				clk			,
	input				rst_n		,
	
	output	reg			lcd_rs		,
	output	wire		lcd_rw		,
	output	reg			lcd_en		,
	output	reg	[7:0]	lcd_data	
	);

	reg	[17:0]	cnt				;
	reg	[3:0]	state_c			;
	reg	[3:0]	state_n			;
	reg	[4:0]	char_cnt		;
	reg	[7:0]	data_display	;

	localparam
		IDLE			= 4'd0	,
		INIT 			= 4'd1	,
		S0				= 4'd2	,
		S1				= 4'd3	,
		S2				= 4'd4	,
		S3				= 4'd5	,
		ROW1_ADDR		= 4'd6	,
		WRITE			= 4'd7	,
		ROW2_ADDR		= 4'd8	,
		stop			= 4'd9	;


	assign lcd_rw = 1'b0;


	always @(posedge clk or negedge rst_n) begin
		if (!rst_n) begin
			cnt <= 17'd0;
		end
		else begin
			if (cnt==17'd100_000 - 1) begin
				cnt <= 17'd0;
			end
			else begin
				cnt <= cnt + 1'b1;
			end
		end
	end

	always @(posedge clk or negedge rst_n) begin
		if (!rst_n) begin
			lcd_en <= 0;
		end
		else if (cnt==17'd50_000 - 1) begin
			lcd_en <= 1;
		end
		else if (cnt==17'd100_000 - 1) begin
			lcd_en <= 0;
		end
	end

	always @(posedge clk or negedge rst_n) begin
		if (!rst_n) begin
			char_cnt <= 0;
		end
		else if (state_c==WRITE && cnt==17'd50_000 - 1) begin
			if (char_cnt==5'd24) begin
				char_cnt <= 5'd0;
			end
			else begin
				char_cnt <= char_cnt + 1'b1;
			end
		end
	end

	always @(*) begin
		case(char_cnt)
			5'd0: data_display   = "P";
			5'd1: data_display   = "a";
			5'd2: data_display   = "n";
			5'd3: data_display   = "-";
			5'd4: data_display   = "H";
			5'd5: data_display   = "o";
			5'd6: data_display   = "n";
			5'd7: data_display   = "g";
			5'd8: data_display   = "-";
			5'd9: data_display   = "F";
			5'd10: data_display  = "e";
			5'd11: data_display  = "n";
			5'd12: data_display  = "g";
			5'd13: data_display  = "L";
			5'd14: data_display  = "C";
			5'd15: data_display  = "D";
			5'd16: data_display  = "1";
			5'd17: data_display  = "6";
			5'd18: data_display  = "0";
			5'd19: data_display  = "2";
			5'd20: data_display  = "-";
			5'd21: data_display  = "T";
			5'd22: data_display  = "e";
			5'd23: data_display  = "s";
			5'd24: data_display  = "t";
			default:data_display = "P";
		endcase
	end

	always @(posedge clk or negedge rst_n) begin
		if (!rst_n) begin
			state_c <= IDLE;
		end
		else if(cnt==17'd50_000 - 1) begin
			state_c <= state_n;
		end
	end

	reg	[19:0]	cnt_15ms;
	reg		flag	;
	[email protected](posedge clk or negedge rst_n)begin
		if (!rst_n) begin
			cnt_15ms <= 0;
		end
		else if (state_c == IDLE) begin
			cnt_15ms <= cnt_15ms + 1'b1;
		end
	end

	[email protected](posedge clk or negedge rst_n)begin
		if (!rst_n) begin
			flag <= 0;
		end
		else if (state_c==IDLE && cnt_15ms==20'd750000) begin
			flag <= 1;
		end
	end

	always @(*) begin
		case(state_c)
			IDLE		:
				begin
					if (flag) begin
						state_n = INIT;
					end
					else begin
						state_n = state_c;
					end
				end
			INIT 	:
				begin
					state_n = S0;
				end
			S0  	:
				begin
					state_n = S1;
				end
			S1  	:
				begin
					state_n = S2;
				end
			S2  	:
				begin
					state_n = S3;
				end
			S3  	:
				begin
					state_n = ROW1_ADDR;
				end
			ROW1_ADDR:
				begin
					state_n = WRITE;
				end
			WRITE		:
				begin
					if (char_cnt==5'd12) begin
						state_n = ROW2_ADDR;
					end
					else if (char_cnt==5'd24) begin
						state_n = stop;
					end
					else begin
						state_n = state_c;
					end
				end
			ROW2_ADDR:
				begin
					state_n = WRITE;
				end
			stop		:
				begin
					state_n = stop;
				end
			default:state_n = IDLE;
		endcase
	end

	always @(posedge clk or negedge rst_n) begin
		if (!rst_n) begin
			lcd_data <= 8'd0;
		end
		else begin
			case(state_c)
				IDLE		:begin lcd_data <= 8'h38; lcd_rs <= 0;end
				INIT 		:begin lcd_data <= 8'h38; lcd_rs <= 0;end
				S0			:begin lcd_data <= 8'h08; lcd_rs <= 0;end
				S1			:begin lcd_data <= 8'h01; lcd_rs <= 0;end
				S2			:begin lcd_data <= 8'h06; lcd_rs <= 0;end
				S3			:begin lcd_data <= 8'h0c; lcd_rs <= 0;end
				ROW1_ADDR	:begin lcd_data <= 8'h80; lcd_rs <= 0;end
				WRITE		:begin lcd_data <= data_display; lcd_rs <= 1;end
				ROW2_ADDR	:begin lcd_data <= 8'hc0; lcd_rs <= 0;end
				stop		:begin lcd_data <= 8'h38; lcd_rs <= 0;end
				default:;
			endcase
		end
	end
endmodule
           

測試代碼:

`timescale 1ns/1ns

module lcd_tb (); /* this is automatically generated */

	reg rst_n;
	reg clk;

	wire       lcd_rs;
	wire       lcd_rw;
	wire       lcd_en;
	wire [7:0] lcd_data;

	lcd inst_lcd
		(
			.clk      (clk),
			.rst_n    (rst_n),
			.lcd_rs   (lcd_rs),
			.lcd_rw   (lcd_rw),
			.lcd_en   (lcd_en),
			.lcd_data (lcd_data)
		);

	initial clk = 0;
	always #10 clk = ~clk;

	initial begin
		#1;
		rst_n = 0;
		#200;
		rst_n = 1;
		#200;


		#100000000;
		$stop;

	end

endmodule
           

 仿真波形圖:

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作

實物圖:

基于FPGA的LCD1602顯示屏驅動一、LCD1602顯示原理二、FPGA實作