前记:师夷长技以自强
1.基本概念
LCD1602:可以显示两行每行16列的字符,故称为1602。实验中使用的是3.3v的1602显示模块,可以在某宝中随表找到。
端口说明:
编号 | 名称 | 功能 |
1 | VSS | 电源地 |
2 | VDD | 电源正极 |
3 | VL | 液晶显示偏压,接VDD时对比度最弱,接地时对比度最强 |
4 | RS | 数据/命令选择,1-数据寄存器,0-指令寄存器 |
5 | R/W | 读/写选择,1-读,0-写 |
6 | E | 使能信号 |
7-14 | D | 双向数据线 |
15 | BLA | 背光正极 |
16 | BLK | 背光负极 |
指令:lcd1602共有11条指令,下面逐一介绍
(1)清屏指令
RS | R/W | DB7 | DB6 | DB5 | DB4 | DB3 | DB2 | DB1 | DB0 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
说明:清除屏幕显示内容,光标返回屏幕左上角,执行该指令时间较长。
(2)光标归为指令
RS | R/W | DB7 | DB6 | DB5 | DB4 | DB3 | DB2 | DB1 | DB0 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | x |
说明:光标返回屏幕左上角,不改变屏幕显示内容。
(3)输入模式设置指令
RS | R/W | DB7 | DB6 | DB5 | DB4 | DB3 | DB2 | DB1 | DB0 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | I/D | S |
I/D=1:写入数据后光标右移
I/D=0:写入数据后光标左移
S=1:显示移动
S=0:显示不移动
说明:推荐值0x06,即写入数据后光标右移,显示不移动
(4)显示开关控制指令
RS | R/W | DB7 | DB6 | DB5 | DB4 | DB3 | DB2 | DB1 | DB0 |
0 | 0 | 0 | 0 | 0 | 0 | 1 | D | C | B |
D=1:显示开,D=0:显示关。
C=1:光标显示,C=0:光标不显示
B=1:光标闪烁,B=0:光标不闪烁
说明:推荐值0x0c,即显示开,不显示光标,光标不闪烁。
(5)光标或显示移动指令
RS | R/W | DB7 | DB6 | DB5 | DB4 | DB3 | DB2 | DB1 | DB0 |
0 | 0 | 0 | 0 | 0 | 1 | S/C | R/L | x | x |
S/C=1:移动对象是显示,此时光标会跟着显示移动
S/C=0:移动对象是光标
R/L=1:移动方向是右
R/L=0:移动方向是左
说明:这个指令可以实现屏幕的滚动显示效果。
(6)工作方式设置指令
RS | R/W | DB7 | DB6 | DB5 | DB4 | DB3 | DB2 | DB1 | DB0 |
0 | 0 | 0 | 0 | 1 | DL | N | F | x | x |
DL=1:8位数据接口(D7-D0)
DL=0:4位数据接口(D7-D4)
N=1:两行显示
N=0:一行显示
F=1:5x10点阵字符
F=0:5x8点阵字符
说明:推荐值0x38,即8位数据接口、两行显示、5x8点阵。因为不能以两行5x10点阵方式进行显示,则0x38与0x3c的效果是一样的。
(7)设置CGRAM地址指令
RS | R/W | DB7 | DB6 | DB5 | DB4 | DB3 | DB2 | DB1 | DB0 |
0 | 0 | 0 | 1 | a | a | a | a | a | a |
说明:用户自定义没有的字符时会用到。
(8)设置DDRAM地址指令
RS | R/W | DB7 | DB6 | DB5 | DB4 | DB3 | DB2 | DB1 | DB0 |
0 | 0 | 1 | a | a | a | a | a | a | a |
说明:在对DDRAM进行读写前,首先要设置DDRAM地址,然后才能进行读写。
(9)读忙信号和地址计数器AC
RS | R/W | DB7 | DB6 | DB5 | DB4 | DB3 | DB2 | DB1 | DB0 |
0 | 1 | BF | a | a | a | a | a | a | a |
BF=1:lcd1602正忙,不能接受单片机指令。
BF=0:lcd1602空闲,可以接受单片机指令。
说明:据说这条指令执行不成功,有一个简化的方法是设置恰当的延时。
(10)写数据到CGRAM或DDRAM
RS | R/W | DB7 | DB6 | DB5 | DB4 | DB3 | DB2 | DB1 | DB0 |
1 | 0 | d | d | d | d | d | d | d | d |
说明:指令执行时,要在DB7-DB0上设置好要写入的数据,然后执行写命令。
(11)从CGRAM或DDRAM读数据指令
RS | R/W | DB7 | DB6 | DB5 | DB4 | DB3 | DB2 | DB1 | DB0 |
1 | 1 | d | d | d | d | d | d | d | d |
说明:指令执行时,要先设置好CGRAM或DDRAM的地址,然后执行读命令,数据最后被读入DB7-DB0.
一般的初始化过程:
延时15ms,
写指令38H(不检测忙信号),
延时5ms,
以后每次写指令、读/写数据操作均需要检测忙信号
写指令38H(显示模式)
写指令08H(显示关闭)
写指令01H(显示清屏)
写指令06H(光标移动和显示模式)
写指令0CH(显示开启,无光标)
注意问题:
1.除了第一条写的指令外,以后写的指令(或者数据)都要检查模块是否忙。
2.写数据前先写地址,注意第二行地址的D7恒为1.
3.使用lcd1602的第一步应该是初始化模块,设置正确的显示和光标模式等。
2.LCD1602与FPGA的连接
lcd1602的管脚排列顺序如下
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
VSS | VDD | VL | RS | R/W | E | D0 | D1 | D2 | D3 | D4 | D5 | D6 | D7 | BLA | BLK |
开发板上的P1插槽管脚信息如下
对比可知,lcd1602模块的1管脚连P1的29管脚,lcd1602模块的16管脚连P1的1管脚,最后多出的一个管脚通过杜邦线连接开发板的GND即可。
3.驱动设计
1.lcd驱动模块的输入输出管脚大致如下
信号名 | 功能 |
Clk | 系统时钟 |
Rst_n | 系统复位 |
Pos[4:0] | 光标位置 |
Data[7:0] | 需要显示字符的ASCII码 |
Set_Cursor | 设置光标指令 |
Set_Data | 设置要显示的数据 |
Clr_Screen | 清屏指令 |
RS | lcd1602数据/命令选择 |
R/W | lcd1602读写选择 |
E | lcd1602使能 |
Dout[7:0] | lcd1602数据接口 |
Init_Done | lcd1602初始化完成 |
2.底层读写时序模块
由于读写lcd模块设计到几个信号的操作,里面有延时要求,所以应该设计一个底层模块,专门负责命令和数据的写。对于lcd忙否的判断,可以通过延时5ms的方法确定lcd足够可以完成命令和数据的接收执行和显示更新等,这样避免了发送指令等待接收再判断的复杂过程。
(1)写时序图和对应的时序参数
(2)模块和状态的设计
设计模块的输入输出管脚如下
可以划分为下面几个状态
IDLE : wait to work
SET_A : set RS RW DB to a appropriate value
DLY1 :wait tsp1
SET_E :set E to 1
DLY2 :wait tpw
RSET_E :set E to 0
DLY3 :wait 200ns
根据状态的转移关系得到如下图,其中省去了自环的转移
(3)编码
模块的veryLog文件如下
module lcd1602_driver(
Clk,
Rst_n,
Set_Data,
Set_Cmd,
Data_Drv,
RS,
RW,
DB,
E,
Write_Done
);
input Clk;
input Rst_n;
input Set_Data;
input Set_Cmd;
input [7:0]Data_Drv;
output reg RS;
output reg RW;
output reg [7:0]DB;
output reg E;
output reg Write_Done;
localparam IDLE = 7'b0000001,
SET_A = 7'b0000010,
DLY1 = 7'b0000100,
SET_E = 7'b0001000,
DLY2 = 7'b0010000,
RSET_E = 7'b0100000,
DLY3 = 7'b1000000;
reg [6:0]state;
reg [3:0]Cnt;
always @ (posedge Clk,negedge Rst_n)
if(!Rst_n)
state <= IDLE;
else
case(state)
IDLE:
if(Set_Data | Set_Cmd)
state <= SET_A;
else
state <= IDLE;
SET_A:
state <= DLY1;
DLY1:
if(Cnt >= 1)
state <= SET_E;
else
state <= DLY1;
SET_E:
state <= DLY2;
DLY2:
if(Cnt >= 7)
state <= RSET_E;
else
state <= DLY2;
RSET_E:
state <= DLY3;
DLY3:
if(Cnt >= 9)
state <= IDLE;
else
state <= DLY3;
endcase
always @ (posedge Clk,negedge Rst_n)
if(!Rst_n)
Cnt <= 4'd0;
else
case(state)
IDLE:
Cnt <= 4'd0;
SET_A:
Cnt <= 4'd0;
DLY1:
Cnt <= Cnt + 4'd1;
SET_E:
Cnt <= 4'd0;
DLY2:
Cnt <= Cnt + 4'd1;
RSET_E:
Cnt <= 4'd0;
DLY3:
Cnt <= Cnt + 4'd1;
endcasealways @ (posedge Clk)
case(state)
IDLE:
begin
E <= 0;
Write_Done <= 0;
if(Set_Data)
RS <= 1;
else if(Set_Cmd)
RS <= 0;
else
RS <= RS;
end
SET_A:
begin
RW <= 0;
DB <= Data_Drv;
end
DLY1:
;
SET_E:
E <= 1;
DLY2:
;
RSET_E:
E <= 0;
DLY3:
if(Cnt >= 9)
Write_Done <= 1;
endcase
endmodule
编写对应的testbench文件如下
`timescale 1ns/1ns
module lcd1602_driver_tb();
reg Clk;
reg Rst_n;
reg Set_Data;
reg Set_Cmd;
reg [7:0]Data_Drv;
wire RS;
wire RW;
wire [7:0]DB;
wire E;
wire Write_Done;
lcd1602_driver lcd1602_driver(
Clk,
Rst_n,
Set_Data,
Set_Cmd,
Data_Drv,
RS,
RW,
DB,
E,
Write_Done
);
initial Clk = 0;
always #10 Clk = ~Clk;
initial begin
Rst_n = 0;
Set_Data = 0;
Set_Cmd = 0;
Data_Drv = 8'h38;
#20;
Rst_n = 1;
Set_Data = 1;
#20;
Set_Data = 0;
wait(Write_Done);
#20;
Data_Drv = 8'h88;
Set_Data = 1;
#20;
Set_Data = 0;
wait(Write_Done);
#20;
Data_Drv = 8'h56;
Set_Cmd = 1;
#20;
Set_Cmd = 0;
#20000;
$stop;
endendmodule
可以得到预期的仿真结果
3.控制模块的编写
(1)状态划分
大的状态可以分为两部分,第一部分是完成lcd1602的初始化,第二部分是执行用户的命令。
第一部分:
IDLE:空闲状态,也就是系统复位后进入的状态。
DELAY15MS:延时等待15ms的状态。
CMD38H1:第一次发送38指令。
DELAY5MS :延时等待5ms的状态。
CMD38H2:第二次发送38指令。
CMD08H :发送08指令。
CMD01H :发送01指令。
CMD06H :发送06指令。
CMD0CH :发送0C指令。
注:对于初始化所发送命令的来源可以参考lcd1602的使用手册。上面的CMD08H,CMD01H,CMD06H,CMD0CH执行命令之前要延时5ms,以备命令因为模块未执行完而丢失。
第二部分:
USER_CMD :监听用户输入的命令。
WAIT_DONE:等待命令执行完成。
(2)辅助信号
信号名 | 说明 |
[19:0]cnt | 计时变量 |
time_out | 5ms计时到变量 |
init_done | 初始化完成后一直为高,否则为低 |
(3)状态转移图
(4)编码
module lcd1602(
Clk,
Rst_n,
Pos,
Data,
Set_Cursor,
Set_Data,
Clr_Screen,
RS,
RW,
E,
Dout,
VL,
Init_Done
);input Clk;
input Rst_n;
input [4:0]Pos;
input [7:0]Data;
input Set_Cursor;
input Set_Data;
input Clr_Screen;
output RS;
output RW;
output E;
output [7:0]Dout;
output VL;
output reg Init_Done;localparam IDLE = 16'b0000_0000_0000_0001,
DELAY15MS = 16'b0000_0000_0000_0010,
CMD38H1 = 16'b0000_0000_0000_0100,
DELAY5MS = 16'b0000_0000_0000_1000,
CMD38H2 = 16'b0000_0000_0001_0000,
CMD08H = 16'b0000_0000_0010_0000,
CMD01H = 16'b0000_0000_0100_0000,
CMD06H = 16'b0000_0000_1000_0000,
CMD0CH = 16'b0000_0001_0000_0000,
USER_CMD = 16'b0000_0010_0000_0000,
WAIT_DONE = 16'b0000_0100_0000_0000;
reg [19:0]cnt;
reg [15:0]state;
wire time_out;
assign time_out = (cnt >= 20'd249999);
always @ (posedge Clk,negedge Rst_n)
if(!Rst_n)
state <= IDLE;
else
case(state)
IDLE:
state <= DELAY15MS;
DELAY15MS:
if(cnt >= 20'd749999)
state <= CMD38H1;
else
state <= state;
CMD38H1:
state <= DELAY5MS;
DELAY5MS:
if(time_out)
state <= CMD38H2;
else
state <= state;
CMD38H2:
if(time_out)
state <= CMD08H;
else
state <= state;
CMD08H:
if(time_out)
state <= CMD01H;
else
state <= state;
CMD01H:
if(time_out)
state <= CMD06H;
else
state <= state;
CMD06H:
if(time_out)
state <= CMD0CH;
else
state <= state;
CMD0CH:
if(time_out)
state <= USER_CMD;
else
state <= state;
USER_CMD:
if(Set_Cursor|Set_Data|Clr_Screen)
state <= WAIT_DONE;
else
state <= state;
WAIT_DONE:
if(time_out)
state <= USER_CMD;
else
state <= state;
endcase
always @ (posedge Clk,negedge Rst_n)
if(!Rst_n)
cnt <= 20'b0;
else
case(state)
IDLE,CMD38H1,USER_CMD:
cnt <= 20'b0;
DELAY15MS:
if(cnt >= 20'd749999)
cnt <= 20'b0;
else
cnt <= cnt + 20'b1;
DELAY5MS,CMD38H2,CMD08H,CMD01H,CMD06H,CMD0CH,WAIT_DONE:
if(time_out)
cnt <= 20'b0;
else
cnt <= cnt + 20'b1;
endcase
reg wr_cmd;
reg wr_data;
reg [7:0]data_dr;
always @ (posedge Clk,negedge Rst_n)
if(!Rst_n)begin
wr_cmd <= 0;
wr_data <= 0;
data_dr <= 8'd0;
Init_Done <= 0;
end
else
case(state)
IDLE:
;
DELAY15MS:
;
CMD38H1,CMD38H2:
if(time_out)
begin
wr_cmd <= 1;
data_dr <= 8'h38;
end
else
wr_cmd <= 0;
DELAY5MS:
wr_cmd <= 0;
CMD08H:
if(time_out)
begin
wr_cmd <= 1;
data_dr <= 8'h08;
end
else
wr_cmd <= 0;
CMD01H:
if(time_out)
begin
wr_cmd <= 1;
data_dr <= 8'h01;
end
else
wr_cmd <= 0;
CMD06H:
if(time_out)
begin
wr_cmd <= 1;
data_dr <= 8'h06;
end
else
wr_cmd <= 0;
CMD0CH:
if(time_out)
begin
wr_cmd <= 1;
data_dr <= 8'h0C;
Init_Done <= 1;
end
else
wr_cmd <= 0;
USER_CMD:
if(Set_Cursor)
begin
wr_cmd <= 1;
data_dr <= {1'b1,Pos[4],2'b0,Pos[3:0]};
end
else if(Set_Data)
begin
wr_data <= 1;
data_dr <= Data;
end
else if(Clr_Screen)
begin
wr_cmd <= 1;
data_dr <= 8'h01;
end
else
begin
wr_cmd <= 0;
wr_data <= 0;
end
WAIT_DONE:
begin
wr_cmd <= 0;
wr_data <= 0;
end
endcase
lcd1602_driver lcd1602_driver(
.Clk(Clk),
.Rst_n(Rst_n),
.Set_Data(wr_data),
.Set_Cmd(wr_cmd),
.Data_Drv(data_dr),
.RS(RS),
.RW(RW),
.DB(Dout),
.E(E),
.Write_Done()
);reg [3:0]vl_cnt;
always @(posedge Clk,negedge Rst_n)
if(!Rst_n)
vl_cnt <= 4'd0;
else
vl_cnt <= vl_cnt + 4'd1;
assign VL = (vl_cnt > 4'd9);
endmodule
为了测试lcd1602的显示功能,这里写一个小的测试文件,用到矩阵键盘和lcd1602,矩阵键盘可以设置输入数据的位置,清屏,输入0-9的数据等。
/*
用矩阵键盘输入数据,将数据显示在lcd1602上
键盘:
0 1 2 3
4 5 6 7
8 9 10 10
12 13 14 15
12:在第一行开始输入
13:在第二行开始输入
14:清屏
*/module lcd1602_test(
Clk,
Rst_n,
Key_Row,
Key_Col,
RS,
RW,
E,
VL,
DB
);
input Clk;
input Rst_n;
input [3:0]Key_Row;
output [3:0]Key_Col;
output RS;
output RW;
output E;
output VL;
output [7:0]DB;wire Key_Flag;
wire [3:0]Key_Value;
MAT_KEY MAT_KEY(
.Clk(Clk),
.Rst_n(Rst_n),
.Row_Data(Key_Row),
.Col_Data(Key_Col),
.Key_Flag(Key_Flag), //按键值是否有效
.Key_Value(Key_Value) //按键值
);
/*Key_Board Key_Board(
.Clk(Clk),
.Rst_n(Rst_n),
.Key_Board_Row_i(Key_Row),
.Key_Board_Col_o(Key_Col),
.Key_Flag(Key_Flag),
.Key_Value(Key_Value)
);*/
reg [4:0]Pos;
reg [7:0]Data;
reg Set_Cursor;
reg Set_Data;
reg Clr_Screen;
wire Init_Done;
lcd1602 lcd1602(
.Clk(Clk),
.Rst_n(Rst_n),
.Pos(Pos),
.Data(Data),
.Set_Cursor(Set_Cursor),
.Set_Data(Set_Data),
.Clr_Screen(Clr_Screen),
.RS(RS),
.RW(RW),
.E(E),
.VL(VL),
.Dout(DB),
.Init_Done(Init_Done)
);
always @ (posedge Clk,negedge Rst_n)
if(!Rst_n)begin
Set_Cursor <= 0;
Set_Data <= 0;
Clr_Screen <= 0;
Pos <= 5'd0;
Data <= 8'd0;
end
else if(Init_Done & Key_Flag)
case(Key_Value)
4'd0:begin Data <= "0";Set_Data <= 1;end
4'd1:begin Data <= "1";Set_Data <= 1;end
4'd2:begin Data <= "2";Set_Data <= 1;end
4'd3:begin Data <= "3";Set_Data <= 1;end
4'd4:begin Data <= "4";Set_Data <= 1;end
4'd5:begin Data <= "5";Set_Data <= 1;end
4'd6:begin Data <= "6";Set_Data <= 1;end
4'd7:begin Data <= "7";Set_Data <= 1;end
4'd8:begin Data <= "8";Set_Data <= 1;end
4'd9:begin Data <= "9";Set_Data <= 1;end
4'd12:begin Pos <= 5'b00000;Set_Cursor <= 1;end
4'd13:begin Pos <= 5'b10000;Set_Cursor <= 1;end
4'd14:begin Clr_Screen <= 1;end
endcase
else
begin
Set_Cursor <= 0;
Set_Data <= 0;
Clr_Screen <= 0;
end
endmodule
关于矩阵键盘的实现可以参考我之前的文章。将分析综合后的文件下到板子后试验,大体上可以使用,就是第0,1,2列的按键按下后会连按,还有多次输入后不能再输入共两个bug。能力有限,欢迎读者纠正。