LinCoding

【读书笔记】详细解析基于FPGA的LCD1602驱动控制

0
阅读(3770)

【主题】:详细解析基于FPGA的LCD1602驱动控制

【作者】:LinCoding

【时间】:2016.11.23

       这周末去找女票玩了,回来继续学习吧,唉,路漫漫,再过不久又要开题了,事情越来越多,能自己学习的时间越来越少。。。

        废话不多说,LCD1602的驱动和控制大家一定都很清楚,这次的LCD1602程序是以三段式状态机为基础的,看过之后你会发现和三段式流水灯简直就是一模一样,因此,状态机有什么难的呢?只要掌握好我总结的那几点,一切变得So Easy。。。

       先上效果图:

blob.png

       LCD1602驱动时序什么的大家请百度,网上实在太多了。

     (源码出自CrazyBingo,尊重版权)

module lcd1602_driver
(
	input					clk,
	input					rst_n,
	input		[127:0]		        line_rom1,
	input		[127:0]		        line_rom2,
		
	output					lcd_en,
	output					lcd_rw,
	output	reg				lcd_rs,
	output	reg	[7:0]		        lcd_data
);

 第一部分,不多说,line_rom1和line_rom2分别给LCD1602的两行送数据,LCD1602每行可容纳16个字符,每个字符是ASCII码,也就是8位,所以,一共需要128位的数据。

//------------------------------------
//delay 20ms for LCD1602 steady
localparam	T20MS	= 20'd1_000_000;
//localparam	T20MS	= 20'd20;	//just for simulation
reg	[19:0]	delay20ms_cnt;
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		delay20ms_cnt	<= 20'd0;
	else if ( delay20ms_cnt < T20MS )
		delay20ms_cnt	<= delay20ms_cnt + 1'b1;
	else 
		delay20ms_cnt	<= T20MS;
end
wire	delay20ms_done	= ( delay20ms_cnt == T20MS ) ? 1'b1 : 1'b0;

 第二部分,是上电延时20ms的计数器,需要注意一点:由于上电延时20ms只执行一次,因此,在到达20ms以后,delay20ms_cnt<= T20MS; 将永不再进行计数。同时更新计数标志位,同样,计数器的输出采用组合逻辑

//------------------------------------
//generate 500Hz clock for LCD1602
localparam	T2MS = 17'd100_000;
//localparam	T2MS = 17'd16;		//just for simulation
reg [16:0]	delay2ms_cnt;
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		delay2ms_cnt	<= 17'd0;
	else if ( delay20ms_done )
		delay2ms_cnt	<= ( delay2ms_cnt < T2MS ) ? delay2ms_cnt + 1'b1 : 17'd1;
	else
		delay2ms_cnt	<= delay2ms_cnt;
end
assign	lcd_rw			= 1'b0;		//write only
assign	lcd_en 			= ( delay2ms_cnt > T2MS/2 ) ? 1'b0 : 1'b1;
wire	lcd_write_flag	= ( delay2ms_cnt == T2MS*3/4 ) ? 1'b1 : 1'b0;

第三部分,产生一个50Hz的时钟,用于状态机的跳转。这里注意两点:

1、lcd_en其实可认为是LCD1602的时钟,类似于之前文章中驱动74HC595时的shift_clk。而与其不同的是,74HC595是上升沿有效,因此时钟是先低后高,shift_flag在下降沿进行输出,以使得shift_clk的上升沿正好出现在数据的正中间,使得建立时间和保持时间最佳。

  而LCD1602属于高电平有效的器件,因此,时钟是先高后低,其实,对于芯片和器件来说,时钟大部分都是上升沿或者高电平有效,很少会有低电平或下降沿有效的。而为了使得高电平正好出现在数据的正中间,因此,lcd_write_flag在T2MS的3/4时进行输出,效果如下图所示。

blob.png

  如上图所示,正好使lcd_en的高电平出现在数据的正中间,这样建立时间和保持时间最佳!

  2、由于只对LCD1602进行写操作,所以lcd_rw直接赋0,这时会在综合时出现以下警告。

blob.png

 就是说有一个输出引脚被直接拉为了1或者0,这不用管它。

还有就是计数器的输出采用组合逻辑。

//------------------------------------
//FSM: encode using Gray code
localparam 	IDLE		= 	8'h00;	//IDLE
//LCD1602 init
localparam	DISP_SET	= 	8'h01;	//Display mode
localparam 	DISP_OFF	= 	8'h03;	//Display off
localparam 	CLR_SCR 	= 	8'h02;	//Clear the LCD
localparam 	CURSOR_SET1 = 	8'h06;	//Set Cursor
localparam 	CURSOR_SET2 = 	8'h07;	//Display on
//Display 1th line	
localparam 	ROW1_ADDR	= 	8'h05;	//Line1's first address	
localparam 	ROW1_0		= 	8'h04;
localparam 	ROW1_1		= 	8'h0C;
localparam 	ROW1_2		= 	8'h0D;
localparam 	ROW1_3		= 	8'h0F;
localparam 	ROW1_4		= 	8'h0E;
localparam 	ROW1_5		= 	8'h0A;
localparam 	ROW1_6		= 	8'h0B;
localparam 	ROW1_7		= 	8'h09;
localparam 	ROW1_8		=	8'h08;
localparam 	ROW1_9		= 	8'h18;
localparam 	ROW1_A		= 	8'h19;
localparam 	ROW1_B		= 	8'h1B;
localparam 	ROW1_C		= 	8'h1A;
localparam 	ROW1_D		= 	8'h1E;
localparam 	ROW1_E		= 	8'h1F;
localparam 	ROW1_F		= 	8'h1D;
//Display 2th line
localparam 	ROW2_ADDR	= 	8'h1C;	//Line2's first address	
localparam 	ROW2_0		= 	8'h14;
localparam 	ROW2_1		= 	8'h15;
localparam 	ROW2_2		= 	8'h17;
localparam 	ROW2_3		= 	8'h16;
localparam 	ROW2_4		= 	8'h12;
localparam 	ROW2_5		= 	8'h13;
localparam 	ROW2_6		= 	8'h11;
localparam 	ROW2_7		= 	8'h10;
localparam 	ROW2_8		= 	8'h30;
localparam 	ROW2_9		= 	8'h31;
localparam 	ROW2_A		= 	8'h33;
localparam 	ROW2_B		= 	8'h32;
localparam 	ROW2_C		= 	8'h36;
localparam 	ROW2_D		= 	8'h37;
localparam 	ROW2_E		= 	8'h35;
localparam 	ROW2_F		= 	8'h34;

 第四部分是三段式状态机的状态编码,使用格雷码进行编码,以免使得输出产生毛刺。

//------------------------------------
//Three Part FSM: part 1
reg	[5:0]	current_state;
reg	[5:0]	next_state;
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		current_state	<= IDLE;
	else if ( lcd_write_flag )
		current_state	<= next_state;
	else
		current_state	<= current_state;
end

 第五部分是三段式FSM的第一段,该说的在三段式流水灯中那篇文章中已经说过了。

//------------------------------------
//Three Part FSM: part 2
always @ ( * )
begin
	next_state	= IDLE;
	case ( current_state )
		//LCD1602 init
		IDLE        	: 	next_state = DISP_SET;	    //5'h00
		DISP_SET    	: 	next_state = DISP_OFF;      //5'h01
		DISP_OFF    	: 	next_state = CLR_SCR;       //5'h03
		CLR_SCR     	:	next_state = CURSOR_SET1;   //5'h02
		CURSOR_SET1	: 	next_state = CURSOR_SET2;   //5'h06
		CURSOR_SET2	: 	next_state = ROW1_ADDR;     //5'h07
		//Display 1th line	
		ROW1_ADDR   	: 	next_state = ROW1_0;	    //5'h05;
		ROW1_0      	: 	next_state = ROW1_1;	    //5'h04;
		ROW1_1      	: 	next_state = ROW1_2;        //5'h0C;
		ROW1_2      	: 	next_state = ROW1_3;        //5'h0D;
		ROW1_3      	: 	next_state = ROW1_4;        //5'h0F;
		ROW1_4      	: 	next_state = ROW1_5;        //5'h0E;
		ROW1_5      	: 	next_state = ROW1_6;        //5'h0A;
		ROW1_6      	: 	next_state = ROW1_7;        //5'h0B;
		ROW1_7      	: 	next_state = ROW1_8;        //5'h09;
		ROW1_8      	: 	next_state = ROW1_9;        //5'h08;
		ROW1_9      	: 	next_state = ROW1_A;        //5'h18;
		ROW1_A      	: 	next_state = ROW1_B;        //5'h19;
		ROW1_B      	: 	next_state = ROW1_C;        //5'h1B;
		ROW1_C      	: 	next_state = ROW1_D;        //5'h1A;
		ROW1_D      	: 	next_state = ROW1_E;        //5'h1E;
		ROW1_E      	: 	next_state = ROW1_F;        //5'h1F;
		ROW1_F      	: 	next_state = ROW2_ADDR;     //5'h1D;
		//Display 2th line	
		ROW2_ADDR   	: 	next_state = ROW2_0; 	    //5'h1C;
		ROW2_0      	: 	next_state = ROW2_1;        //5'h14;
		ROW2_1      	: 	next_state = ROW2_2;        //5'h15;
		ROW2_2      	: 	next_state = ROW2_3;        //5'h17;
		ROW2_3      	:	next_state = ROW2_4;        //5'h16;
		ROW2_4      	: 	next_state = ROW2_5;        //5'h12;
		ROW2_5      	: 	next_state = ROW2_6;        //5'h13;
		ROW2_6      	: 	next_state = ROW2_7;        //5'h11;
		ROW2_7      	: 	next_state = ROW2_8;        //5'h10;
		ROW2_8      	: 	next_state = ROW2_9;        //5'h30;
		ROW2_9      	: 	next_state = ROW2_A;        //5'h31;
		ROW2_A      	: 	next_state = ROW2_B;        //5'h33;
		ROW2_B      	: 	next_state = ROW2_C;        //5'h32;
		ROW2_C      	: 	next_state = ROW2_D;        //5'h36;
		ROW2_D      	: 	next_state = ROW2_E;        //5'h37;
		ROW2_E      	: 	next_state = ROW2_F;        //5'h35;
		ROW2_F      	: 	next_state = ROW1_ADDR;     //5'h34;
		default     	: 	next_state = IDLE ;		
	endcase
end

 第六部分是三段式FSM的第二段,同样,该说的在三段式流水灯中那篇文章中已经说过了。

//------------------------------------
//Three Part FSM: part 3-1
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		lcd_rs	<= 1'b0;
	else if ( lcd_write_flag )
		if(	next_state	==	IDLE 		|| 
			next_state 	==	DISP_SET 	||
			next_state 	==	DISP_OFF	||
			next_state	==	CLR_SCR		||
			next_state	==	CURSOR_SET1	||
			next_state	==	CURSOR_SET2	||
			next_state	== 	ROW1_ADDR 	|| 
			next_state	==	ROW2_ADDR	)
			lcd_rs <= 1'b0;	//L: Instruction
		else
			lcd_rs <= 1'b1;	//H: Data
	else
		lcd_rs <= lcd_rs;
end

//------------------------------------
//Three Part FSM: part 3-2
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		lcd_data	<= 8'h00;
	else if ( lcd_write_flag )
		case ( next_state )
			IDLE        	: 	lcd_data	<= 8'hxx;
			DISP_SET    	: 	lcd_data	<= 8'h38;
			DISP_OFF    	: 	lcd_data	<= 8'h08;
			CLR_SCR     	: 	lcd_data	<= 8'h01;
			CURSOR_SET1	: 	lcd_data	<= 8'h06;		
			CURSOR_SET2	: 	lcd_data	<= 8'h0C;		
			//Display 1th line		
			ROW1_ADDR   	: 	lcd_data	<= 8'h80;
			ROW1_0      	: 	lcd_data	<= line_rom1[127:120];
			ROW1_1      	: 	lcd_data	<= line_rom1[119:112];
			ROW1_2      	: 	lcd_data	<= line_rom1[111:104];
			ROW1_3      	: 	lcd_data	<= line_rom1[103: 96];
			ROW1_4      	: 	lcd_data	<= line_rom1[ 95: 88];
			ROW1_5      	: 	lcd_data	<= line_rom1[ 87: 80];
			ROW1_6      	: 	lcd_data	<= line_rom1[ 79: 72];
			ROW1_7      	: 	lcd_data	<= line_rom1[ 71: 64];
			ROW1_8      	: 	lcd_data	<= line_rom1[ 63: 56];
			ROW1_9      	: 	lcd_data	<= line_rom1[ 55: 48];
			ROW1_A      	: 	lcd_data	<= line_rom1[ 47: 40];
			ROW1_B      	: 	lcd_data	<= line_rom1[ 39: 32];
			ROW1_C      	: 	lcd_data	<= line_rom1[ 31: 24];
			ROW1_D      	: 	lcd_data	<= line_rom1[ 23: 16]; 
			ROW1_E      	: 	lcd_data	<= line_rom1[ 15:  8];
			ROW1_F      	: 	lcd_data	<= line_rom1[  7:  0];
			//Display 2th line		
			ROW2_ADDR   	: 	lcd_data	<= 8'hC0;	
			ROW2_0      	: 	lcd_data	<= line_rom2[127:120];
			ROW2_1      	: 	lcd_data	<= line_rom2[119:112];
			ROW2_2      	: 	lcd_data	<= line_rom2[111:104];
			ROW2_3      	: 	lcd_data	<= line_rom2[103: 96];
			ROW2_4      	: 	lcd_data	<= line_rom2[ 95: 88];
			ROW2_5      	: 	lcd_data	<= line_rom2[ 87: 80];
			ROW2_6      	: 	lcd_data	<= line_rom2[ 79: 72];
			ROW2_7      	: 	lcd_data	<= line_rom2[ 71: 64];
			ROW2_8      	: 	lcd_data	<= line_rom2[ 63: 56];
			ROW2_9      	: 	lcd_data	<= line_rom2[ 55: 48];
			ROW2_A      	: 	lcd_data	<= line_rom2[ 47: 40];
			ROW2_B      	: 	lcd_data	<= line_rom2[ 39: 32];
			ROW2_C      	: 	lcd_data	<= line_rom2[ 31: 24];
			ROW2_D      	: 	lcd_data	<= line_rom2[ 23: 16];
			ROW2_E      	: 	lcd_data	<= line_rom2[ 15:  8];
			ROW2_F      	: 	lcd_data	<= line_rom2[  7:  0];			
			default		:	lcd_data	<= 8'h00;
		endcase
end

第七部分,三段式FSM的第三段,注意一点:

FSM的第三段使用了两个always,这是为何?

三段式FSM并不意味着是三个always,尽管大多数情况下我们可以用三个always解决问题,但是如这个例子,由于lcd_data即可能输出的是数据也可能输出的是命令,而输出命令时lcd_rs <= 1'b0;  输出数据时lcd_rs <= 1'b1;  因此我们需要同时输出lcd_rs和lcd_data,而触发这两者输出的都是lcd_write_flag,因此将这两个信号分别在两个always钟输出很合理,可根据需要同时输出,而如果写在一个always中,如以下代码:

DISP_SET    	: 	
		    begin
    				lcd_rs          <= 1'b0;
				lcd_data	<= 8'h38;	
		    end

 这样做的话由于lcd_rs和lcd_data在一个always中输出,会使得lcd_rs和lcd_data的输出正好差了一个clk,时序上出现混乱,还得想办法做两个信号的同步处理(最常用的办法是在定义一个lcd_data_r信号,寄存一级lcd_data,以使得两者同步),十分没必要,因此,写成两个always十分的有利!


最后。。。这样做,就成功了呗!


总结:

     1、对于高电平有效的芯片或器件,时钟输出先高后低,时钟输出标志位在计数总数的3/4处。

     2、对于上升沿有效的芯片或器件,时钟输出先低后高,时钟输出标志位在计数总数处,也就是下降沿的地方。

     3、三段式状态机,对于两个输出信号需要同步输出的,可将两个信号分别放到两个always中进行输出。