[FPGA]浅谈LCD1602字符型液晶显示器(Verilog)
概述
本文围绕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.
[FPGA]浅谈LCD1602字符型液晶显示器(Verilog)的更多相关文章
- 浅谈用ModelSim+Synplify+Quartus来实现Altera FPGA的仿真
浅谈用ModelSim+Synplify+Quartus来实现Altera FPGA的仿真 工作内容: Mentor公司的ModelSim是业界最优秀的HDL语言仿真软件,它能提供友好的仿真环境,是业 ...
- 浅谈FPGA
浅谈FPGA 前言 生活中永远都不会缺少「 为什么 」,于最近就被合胜学长了,问了一个看似简单却又极具意义的问题,为什么需要FPGA?FPGA与单片机的区别是什么?瞬间刷新了我入门三天FPGA的冲击感 ...
- 如何为编程爱好者设计一款好玩的智能硬件(九)——LCD1602点阵字符型液晶显示模块驱动封装(下)
六.温湿度传感器DHT11驱动封装(下):如何为编程爱好者设计一款好玩的智能硬件(六)——初尝试·把温湿度给收集了(下)! 七.点阵字符型液晶显示模块LCD1602驱动封装(上):如何为编程爱好者设计 ...
- 如何为编程爱好者设计一款好玩的智能硬件(八)——LCD1602点阵字符型液晶显示模块驱动封装(中)
六.温湿度传感器DHT11驱动封装(下):如何为编程爱好者设计一款好玩的智能硬件(六)——初尝试·把温湿度给收集了(下)! 七.点阵字符型液晶显示模块LCD1602驱动封装(上):如何为编程爱好者设计 ...
- 如何为编程爱好者设计一款好玩的智能硬件(七)——LCD1602点阵字符型液晶显示模块驱动封装(上)
当前进展: 一.我的构想:如何为编程爱好者设计一款好玩的智能硬件(一)——即插即用.积木化.功能重组的智能硬件模块构想 二.别人家的孩子:如何为编程爱好者设计一款好玩的智能硬件(二)——别人是如何设计 ...
- 浅谈 PHP 变量可用字符
原文:浅谈 PHP 变量可用字符 先来说说php变量的命名规则,百度下一抓一大把:(1) PHP的变量名区分大小写;(2) 变量名必须以美元符号$开始;(3) 变量名开头可以以下划线开始;(4) 变量 ...
- 从JavaWeb危险字符过滤浅谈ESAPI使用
事先声明:只是浅谈,我也之用了这个组件的一点点. 又到某重要XX时期(但愿此文给面临此需求的同仁有所帮助),某Web应用第一次面临安全加固要求,AppScan的安全测试报告还是很清爽的,内容全面,提示 ...
- 浅谈Java中的equals和==(转)
浅谈Java中的equals和== 在初学Java时,可能会经常碰到下面的代码: 1 String str1 = new String("hello"); 2 String str ...
- 浅谈Java中的equals和==
浅谈Java中的equals和== 在初学Java时,可能会经常碰到下面的代码: String str1 = new String("hello"); String str2 = ...
随机推荐
- MyBatisCodeHelper-Pro插件破解
MyBatisCodeHelper-Pro: MyBatisCodeHelper-Pro是IDEA下的一个插件,功能类似mybatis plugin. 但是是收费的,我们可以对他进行破解 转载出处:h ...
- ReoGrid.Mvvm:ReoGrid绑定模型
ReoGrid 是 C# 编写的.NET 电子表格控件(类似 Excel).支持单元格合并,边框样式,图案背景颜色,数据格式,冻结,公式,宏和脚本执行,表格事件等.支持 Winform\WPF. Re ...
- Mysql数据库(八)存储过程与存储函数
一.创建存储过程与存储函数 1.创建存储过程(实现统计tb_borrow1数据表中指定图书编号的图书的借阅次数) mysql> delimiter // mysql> CREATE PRO ...
- 设计模式(二十一)Proxy模式
在面向对象编程中,“本人”和“代理人”都是对象.如果“本人”对象太忙了,有些工作无法自己亲自完成,就将其交给“代理人”对象负责. 示例程序的类图. 示例程序的时序图.从这个时序图可以看出,直到调用pr ...
- TCP UDP基本编程(一)
tcp udp均可以用来网络通信,在使用之前建议先搜索一下相关网络连接的基本知识,可以更好的理解和使用,tcp建议看下如下文章:https://blog.csdn.net/chuangsun/arti ...
- 两种unity双击事件
有时候需要用到双击事件,而unity未提供双击控件,在此提供两种双击事件方法,进攻参考: 1)此方法为通过unityevent来实现 首先新建image(或其他不带点击事件的控件),添加如下脚本,然后 ...
- 实战--dango自带的分页(极简)
注意,我将templates定义在项目的同级目录下: 在settings.py中配置 TEMPLATES = [ { 'BACKEND': 'django.template.backends.djan ...
- 如何用github搭建博客
新建项目 创建仓库 仓库名称:一定要是你的用户名+github.io 如:用户名:zhangsan 那么仓库地址: zhangsan,github.io 打开新创建的仓库,点击settings 下拉至 ...
- JVM内存结构、参数调优和内存泄露分析
1. JVM内存区域和参数配置 1.1 JVM内存结构 Java堆(Heap) Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建.此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都 ...
- 第三十一章 System V信号量(二)
用信号量实现进程互斥示例 #include <unistd.h> #include <sys/types.h> #include <stdlib.h> #inclu ...