当前位置:   article > 正文

基于Verilog HDL LCD1602显示器的设计_fpga lcd1602显示例程

fpga lcd1602显示例程


前言

昨天刚结束FPGA的课程设计,做的题目是用Verilog HDL编写LCD1602字符显示程序,并在开发板DE2-115上进行演示,实现的功能是显示移动字符和滚动字符,并通过一个开关来控制模式的切换。此次课程设计参考了网站上许多前辈大佬的文章,在他们的基础上进行修改。但发现许多的文章仅仅介绍了如何显示静态字符,而没有介绍滚动字符显示如何编写,遂由此写下这篇博客,希望对有需要的人有所帮助。

一、设计任务

基于Verilog HDL 液晶显示控制器的设计
(1)掌握LCD1602字符型液晶显示器工作和时序原理;
(2)在LCD1602液晶显示器上面实现静态字符;
(3)扩展功能:在LCD1602液晶显示器实现动态字符。


二、综合设计部分

1.设计原理及方案

(1)LCD1602介绍

在这里插入图片描述

LCD1602(Liquid Crystal Display)液晶显示屏是一种字符型液晶显示模块,可以显示ASCII码的标准字符和其它的一些内置特殊字符,还可以有8个自定义字符,显示容量:16×2个字符,每个字符为5*7点阵。

①引脚

在这里插入图片描述

该设计只需要向LCD写入指令或者字符数据,故RW引脚可以直接置低电平。注:在DE2-115中使用的LCD模块并不含背光单元,故而LCD_BLON信号在用户工程中的设定是无效的,因此模块代码中不添加LCD_BLON输出(此提示可以在DE2-115数据手册LCD1602模块中看到)。

②操作时序

该设计只需要写操作,故此处只介绍LCD1602的写操作时序。RW=0,RS=0/1(控制写入的是字符数据还是指令)
在这里插入图片描述
在这里插入图片描述

首先将要写入的数据DB0-DB7准备好,再拉高E并维持一段时间(大于150ns),最后将E拉低,DB0-DB7的数据写入LCD。通过控制RS等于0或者1来实现指令/字符数据的写入。
注:这个地方有个问题,E脉冲宽度给150ns根本就不行,最后参考单片机中的教程,整个周期给了2ms,E脉冲宽度给了1ms,最终实现了。(此条提示参考:基于FPGA的LCD1602驱动设计(含有代码)

③指令集

LCD1602有非常多的指令,此处只介绍本设计所用到的几条指令。
0X38:开显示
0X08:显示关,不显示光标,光标不闪烁
0X0C:显示开,不显示光标,光标不闪烁
0X01:清除屏幕显示内容,光标返回屏幕左上角
0X06:写入数据光标右移,不显示移动
0X18:整屏字符左移
0X80:第一行首个字符的位置
0XC4:第二行中间字符的位置
注:字符位置的设置可参考基于FPGA的LCD1602驱动(含代码)
完整指令集如下
在这里插入图片描述

(2)LCD1602驱动流程

①LCD初始化

根据数据手册,LCD的初始化需要完成下面7步:
1 写指令38H
2 延时15ms
3 写指令38H(检测忙)
4 写指令08H(检测忙)
5 写指令01H(检测忙)
6 写指令06H(检测忙)
7 写指令0CH(检测忙)
因LCD1602相较于单片机来说是慢速设备,所以在发送下一条指令时需要检测LCD是否处理完上一条指令,也就是检测BF位(可以发送完整指令集中的9.读BF及AC值),而根据上图lcd指令执行时间可以得知每条指令的执行时间为us级,因此我们此处可以将每条指令维持时间给到2ms(详见下面代码部分),就不需要检测BF位了。
注:此设计通过延时来避免读BF位的操作是根据这篇博客基于FPGA的LCD1602显示屏驱动中4、LCD1602指令(9)的解释。

②LCD写数据

此设计初始化需要发送6条指令,每条指令可以定义为1个状态用于赋值,或者整个初始化过程可以定义为1个状态再用循环来写入指令,完成后写入第一行的地址0X80(此时RS仍为0)。之后开始写入数据RS拉高等于1,不断写入再判断是否写到第一行末尾字符(这个可以自行设定),如果到了我们再写入第二行的地址0XC4(为了显示效果,我们从第二行中间位置开始显示),写完之后我们进入stop状态,也即显示静态字符。动态字符的显示与静态字符的显示有些许差别,需要再添加一个状态我们命名为dongtai(写入0X18指令),每进入一次该状态整屏字符就会往左平移一次,之后跳回stop状态,如果此时接着跳回dongtai状态则会因为两次平移间隔的时间太短,人眼难以观察,显示的效果不好。因此在滚动字符模式下,当我们进入stop状态,开始计时400ms(大于400ms也行,可以自己调节),计时结束后再跳入dongtai状态,这样两次平移之间的间隔就为400ms,人眼可以观察清楚。
(1)

(2)
在这里插入图片描述
(3)
在这里插入图片描述

2.仿真结果及分析

在这里插入图片描述
移动字符模式(mode=1),由仿真图可以知道,lcd_rs在①阶段完成了LCD的初始化配置,并在最后写入第一行首个字符的地址,在②阶段写入所要显示的字符。当第一行显示完成时,给它写入第二行首个字符的地址,待所有字符写入完成,在③阶段写入指令进入stop(0x38)状态并维持一段时间再跳入dongtai(0x18)状态,使字符整屏移动,两个状态相互跳转实现字符移动效果。
在这里插入图片描述
静态字符模式(mode=0),静态字符模式与移动字符模式①②阶段相同,只是在③阶段一直维持stop状态,即一直写入0x38指令。

3.硬件调试

此次设计使用DE2-115开发板进行验证
(1)器件选择
在这里插入图片描述

具体器件名可看开发板芯片
(2)引脚绑定

在这里插入图片描述
在这里插入图片描述
板子上50MHZ时钟可通过PIN_Y2输入
根据DE2-115使用手册进行LCD引脚配置,模式控制信号(此处我们与下图最后一排开关从左往右数第二个开关进行绑定)和刷新信号(此处我们与下图最后一排开关从左往右数第一个开关进行绑定)可自定义引脚。
(3)连接电脑
在这里插入图片描述

白线为USB线,黑线为电源线。
(4)正确导入管脚分配表,无错误后,重新综合整个工程
在这里插入图片描述

(5)综合完毕,无错误后,连接好开发板,开启电源,准备下载。
在这里插入图片描述
在这里插入图片描述

(6)配置完成后下载
在这里插入图片描述

注意,下载时要将板子上的开关SW19拨到RUN
(7)固化文件下载
上述烧录的程序为非固化文件,每次重新上电后需要重新烧录,因此我们可以将文件转换为固化文件,再将其烧录,避免程序掉电丢失。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一般会显示文件超过了最大容量,因此在这里将文件压缩
在这里插入图片描述
成功生成固化文件(.jic)
下载固化文件,首先将原先的文件删除掉,再点击添加文件、Start,烧录完成之后重新上电,即可观察到相应现象。
在这里插入图片描述

如果想要擦除文件,将原先的√取消掉,再勾选Erase,最后点击Start即可。
烧录程序和固化文件下载可以参考这两篇博客
Quartus Prime硬件实验开发(DE2-115板)实验一CPU指令运算器设计
【工具教程】FPGA程序掉电保存,jic模式烧写EPCS,代替AS方式


4.完整代码

module lcd1602(
	input				clk,//50MHZ
	input				rst_n,
	input    wire  mode,//模式设置,静态字符(0),移动字符(1)
	output   wire  lcd_on,//开lcd      
	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,//设置显示模式并延时15ms
		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,
		dongtai     = 4'd10;//移位
	assign lcd_rw = 1'b0;//写操作
   assign lcd_on = 1'b1;//lcd上电
	
	
	always @(posedge clk or negedge rst_n) begin
		if (!rst_n) begin
			cnt <= 18'd0;
		end
		else begin
			if (cnt==18'd100_000 - 1) begin//2ms清零
//			if (cnt==17'd4 - 1) begin//仿真
				cnt <= 18'd0;
			end
			else begin
				cnt <= cnt + 1'b1;
			end
		end
	end
 
   always @(posedge clk or negedge rst_n) begin//lcd_en周期为2ms,高电平1ms
		if (!rst_n) begin
			lcd_en <= 0;
		end
		else if (cnt==18'd50_000 - 1) begin
//		else if (cnt==17'd2 - 1) begin//仿真
			lcd_en <= 1;
		end
		else if (cnt==18'd100_000 - 1) begin
//		else if (cnt==17'd4 - 1) begin//仿真
			lcd_en <= 0;
		end
	end
 
	always @(posedge clk or negedge rst_n) begin//cnt=100_000时清零,所以每写一个字符间隔2ms
		if (!rst_n) begin
			char_cnt <= 0;
		end
		else if (state_c==WRITE && cnt==18'd50_000 - 1) begin
//		else if (state_c==WRITE && cnt==17'd2 - 1) begin//仿真
			if (char_cnt==5'd22) 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   = "H";
			5'd1: data_display   = "A";
			5'd2: data_display   = "P";
			5'd3: data_display   = "P";
			5'd4: data_display   = "Y";
			5'd5: data_display   = "-";
			5'd6: data_display   = "N";
			5'd7: data_display   = "E";
			5'd8: data_display   = "W";
			5'd9: data_display   = "-";
			5'd10: data_display  = "Y";
			5'd11: data_display  = "E";
			5'd12: data_display  = "A";
			5'd13: data_display  = "R";
			5'd14: data_display  = "!";
			5'd15: data_display  = "-";
			5'd16: data_display  = "L";
			5'd17: data_display  = "C";
			5'd18: data_display  = "D";
			5'd19: data_display  = "1";
			5'd20: data_display  = "6";
			5'd21: data_display  = "0";
			5'd22: data_display  = "2";
			default:data_display = "H";
		endcase
	end
	
	always @(posedge clk or negedge rst_n) begin//状态持续时间2ms,保证每个状态下发送的指令被lcd接收
		if (!rst_n) begin
			state_c <= IDLE;
		end
		else if(cnt==18'd50_000 - 1) begin//因cnt到100_000被清零,所以次态赋给现态时刻为50_000-150_000-250_000,也就是每2ms更新一次状态
//		else if(cnt==17'd2 - 1) begin//仿真
			state_c <= state_n;
		end
	end
 
   reg	[19:0]	cnt_15ms;//上电延时等待
	reg		flag;
	always@(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
	always@(posedge clk or negedge rst_n)begin//判断延时是否完成
		if (!rst_n) begin
			flag <= 0;
		end
		else if (state_c==IDLE && cnt_15ms==20'd750_000) begin//15ms
//		else if (state_c==IDLE && cnt_15ms==20'd3-1) begin//仿真
			flag <= 1;
		end
	end
	
	
	reg	[24:0]	cnt_400ms;//每发送一条移动指令间隔时间
	reg		flag_1;
	always @(posedge clk or negedge rst_n) begin
		if (!rst_n) begin
			cnt_400ms <= 25'd0;
		end
		else begin
			if(state_c==stop) begin
			   if(cnt_400ms==25'd20_000_000 - 1)begin
//			   if(cnt_400ms==25'd6 - 1)begin//仿真
				  cnt_400ms <= 25'd0;
				end
				else begin
				  cnt_400ms <= cnt_400ms + 1'b1;				
				end
			end
		end
	end
	always@(posedge clk or negedge rst_n)begin//判断延时是否完成
		if (!rst_n) begin
			flag_1 <= 0;
		end
		else if (state_c==stop && cnt_400ms==25'd20_000_000 - 1) begin//开发板主频50MHZ,也即每20ns记一次,计20_000_000次总共就400ms。
//		else if (state_c==stop && cnt_400ms==25'd6 - 1) begin//仿真
			flag_1 <= 1;
		end
		else if (state_c==dongtai)begin
		  flag_1 <= 0;
		end
	end	
	
	always @(*) begin//状态转换及模式选择
	if(mode==0)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'd15) begin//第一行最后一个字符
						state_n = ROW2_ADDR;
					end
					else if (char_cnt==5'd22) 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
		endcase
		end
		else if(mode==1) 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'd15) begin
						state_n = ROW2_ADDR;
					end
					else if (char_cnt==5'd22) begin
						state_n = stop;
					end
					else begin
						state_n = state_c;
					end
				end
			ROW2_ADDR:
				begin
					state_n = WRITE;
				end
			stop	 :
				begin
				if (flag_1) begin
					state_n = dongtai;
				end
				else begin
				   state_n = state_c;
				end
				end
			dongtai :begin
					state_n = stop;
			   end
		endcase
		end
	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'hc4; lcd_rs <= 0;end//第二行中间显示
				stop        :begin lcd_data <= 8'h38; lcd_rs <= 0;end
				dongtai     :begin lcd_data <= 8'h18; lcd_rs <= 0;end//移位
				default:;
			endcase
		end
	end
endmodule

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316

注:新建VWF文件进行仿真时,将注释了仿真的语句取消注释,将该语句的上一行注释掉。

三、功能演示

功能演示

总结

第一次写博客难免有地方没有考虑仔细,欢迎在评论区进行讨论,如有错误也欢迎指正。

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号