FPGA_LCD1602_verilog
0赞FPGA_LCD1602_verilog
——两忘而化其道(fei199311)
〇、前言
话说好像就是一年前,我用8051成功驱动了LCD1602,一年后的今天,我通过Verilog HDL描述了LCD1602的驱动电路,并在FPGA上实现了(注:参考并学习了《FPGA设计技巧与案例开发详解》)。一年匆匆而过,好快!!!今天有个新闻——《流浪夫妻背3000枚硬币到银行兑换 寄给上大学女儿》,我们有什么理由不去奋斗!!!!!!!
一、名词解释
LCD:LCD(Liquid Crystal Display的简称)液晶显示器。LCD的构造是在两片平行的玻璃基板当中放置液晶盒,下基板玻璃上设置TFT(薄膜晶体管),上基板玻璃上设置彩色滤光片,通过TFT上的信号与电压改变来控制液晶分子的转动方向,从而达到控制每个像素点偏振光出射与否而达到显示目的。现在LCD已经替代CRT成为主流,价格也已经下降了很多,并已充分的普及[1]。
LCD1602:工业字符型液晶,能够同时显示16X2即32个字符。LCD1602实物图如下图1所示。1602液晶也叫1602字符型液晶,它是一种专门用来显示字母、数字、符号等的点阵型液晶模块。它由若干个5X7或者5X11等点阵字符位组成,每个点阵字符位都可以显示一个字符,每位之间有一个点距的间隔,每行之间也有间隔,起到了字符间距和行间距的作用,正因为如此所以它不能很好地显示图形(用自定义CGRAM,显示效果也不好)。1602LCD是指显示的内容为16X2,即可以显示两行,每行16个字符液晶模块(显示字符和数字)。市面上字符液晶大多数是基于HD44780液晶芯片的,控制原理是完全相同的,因此基于HD44780写的控制程序可以很方便地应用于市面上大部分的字符型液晶[2]。
图1.LCD1602实物图
二、设计需求
虽然LCD1602可显示的字符比较少,但是其功耗低、体积小、显示内容丰富、超薄轻巧且稳定性较好,在袖珍式仪表和低功耗应用系统中有广泛应用。
设计思想
首先研究LCD1602的datasheet,这个是电路设计的根本,正所谓“设计源于手册[3]”。由于LCD1602配置的串行特性,所以其驱动与配置用“C语言+单片机”实现起来对大多数设计者来说更容易理解和实现,所以可以先用C语言实现LCD1602的驱动,随后将C语言转换成相应的Verilog HDL代码即可。话说这个C语言转换成Verilog HDL一般情况下都是用FSM(有限状态机)实现的。
四、设计方案
整个基于FPGA的LCD1602控制器大致上可以分为两个模块:1.LCD1602驱动模块;2.LCD1602显示RAM。LCD1602驱动模块负责配置LCD1602内部的寄存器和向LCD显示RAM中写入要显示的内容,用FSM(有限状态机)可以实现其功能。LCD1602显示RAM是给LCD用户提供的一个接口,用户通过这个接口可以使LCD1602任意位置显示任意内容(ASCII码表的内容,如图2所示)。
图2.CGROM中字符码与字符字模关系对照表
上图中就是LCD1602可以固定写的字符,还有一些编码对应的字符可以用户自定义,这里就不详述了,详见datasheet。
要使用LCD1602,需要用一定的方式对其进行配置,那么首先看LCD1602有哪些引脚线,如下图3所示:
图3.LCD1602引脚图
下表为接口信号说明表:
表1.LCD1602接口信号说明表
通过上面的图3和表1,我们可以知道,需要重点关注的信号线有11根:RS、R/W、E和Date I/O。关于I/O数据,需要看时序图;关于R/W信号,其控制读写,我们现在只写不读,所以将其接到GND即可;关于RS信号,此信号关系到配置LCD1602,所以应该参考datasheet上的初始化过程;关于E信号,这个比较重要,由于设计中一般使用同步电路,所以这个E信号需要参考LCD1602各个操作所需要的时间,由于清屏信号需要1.64ms,所以最高时钟为609.756Hz,为了实现的方便,本设计中采用500Hz的E信号。
LCD1602有个特点,就是一定要按照手册上给的方式进行一定的初始化才可以使用(话说,这个好像基本是每一个芯片都需要的。。。)。这个LCD1602的配置流程如下:
延时15ms,等待LCD1602上电稳定;
功能配置。写入指令38h;
关闭LCD1602显示。写入指令08h;
清屏操作。写入指令01h;
设置显示模式。写入指令06h;
打开显示。写入指令0Ch;
配置行地址。写入指令80h(第一行)或C0h;
写入数据。写入相应数据;
从新配置行地址。
从上面的配置LCD1602的描述可以看到,写入的数据共有两种:数据和指令,由RS信号控制。这个RS倒是比较好控制,根据不同的状态,输出不同的高低电平即可,不过还是要参考一下datasheet,这样可以使设计更加稳定。下图为LCD1602写时序图:
图4.LCD1602写时序图
从上图可以看到,在E信号为高电平时,数据被写入,由RS信号告诉LCD1602,到底写入的DATA是数据还是指令。为了设计电路的稳定性,我们应该尽量在E信号在高电平之前保证DATA信号与RS信号稳定,所以为了达到这个目的,在HDL设计时要注意这一点。
还有一个要知道的就是LCD1602显示RAM地址映射,如下图所示:
图5.LCD1602显示RAM地址映射图
虽说这个显示RAM有很多,这个也可以通过配置LCD1602,使其移动显示,但是我感觉这个真的用处不是太大,所以关于显示,我就用一块RAM实现了,通过向我设计的RAM中写入相应的数据和地址,即可完成向LCD1602的相应位置写入相应的数据。详细设计详见设计代码。
五、设计代码
1.8051实现LCD1602源代码
FPGA实现LCD1602顶层模块
// LCD1602_top
module LCD1602
(
input wire clk_50M,
input wire rst_n,
input wire [4:0] button_h, // write addr
input wire [7:0] button_l, // write data
output wire lcd_en,
output wire lcd_rs,
output wire lcd_rw,
output wire [7:0] lcd_data_out
);
// module 1: lcd_ram
wire [4:0] read_addr;
wire [4:0] write_addr;
wire [7:0] write_data;
wire [7:0] ram_data_out;
LCD1602_ram LCD_ram
(
.clk_100M(clk_50M),
.rst_n(rst_n),
.read_addr(read_addr),
.write_addr(button_h),
.write_data(button_l),
.ram_data_out(ram_data_out)
);
// module 2: lcd_driver
LCD1602_driver LCD_driver
(
.clk_100M(clk_50M),
.rst_n(rst_n),
.lcd_ram_data(ram_data_out),
.lcd_en(lcd_en),
.lcd_rs(lcd_rs),
.lcd_rw(lcd_rw),
.lcd_data_out(lcd_data_out),
.lcd_ram_addr(read_addr)
);
endmodule
3.FPGA实现LCD1602驱动源代码
// LCD1602驱动电路
module LCD1602_driver
(
input wire clk_100M, //时钟信号,100MHz
input wire rst_n, //复位信号,低有效
input wire [7:0] lcd_ram_data, //来自ram的显示数据
output wire lcd_en, // lcd配置时钟
output reg lcd_rs, // lcd指令数据选择线,高电平为数据
output wire lcd_rw, // lcd读写控制线,高电平为读
output wire [7:0] lcd_data_out, // lcd数据输出
output reg [4:0] lcd_ram_addr //寻址32,正好5位地址线
);
// 1.延时20ms,以保证lcd正常工作
localparam DELAY_20ms = 1_000_000;
//localparam DELAY_20ms = 20; // Just for test
reg [20:0] cnt_20ms;
wire cnt_20ms_done;
always @ (posedge clk_100M or negedge rst_n) begin
if(~rst_n)
cnt_20ms <= 1'b0;
else if(cnt_20ms <= DELAY_20ms - 1'b1)
cnt_20ms <= cnt_20ms + 1'b1;
else
cnt_20ms <= cnt_20ms;
end
assign cnt_20ms_done = (cnt_20ms == DELAY_20ms) ? 1'b1 : 1'b0;
// 2.500Hz的lcd_en信号产生,lcd_rw赋值,发出状态变迁信号state_flag
localparam ENA_DELAY = 100_000;
//localparam ENA_DELAY = 10; // Just for test
reg [17:0] cnt_ena;
wire state_flag; //状态变迁使能信号
always @ (posedge clk_100M or negedge rst_n) begin
if(~rst_n)
cnt_ena <= 1'b0;
else if(cnt_20ms_done) begin
if(cnt_ena < ENA_DELAY -1'b1) //注意这个地方是<,而不是<=
cnt_ena <= cnt_ena + 1'b1;
else
cnt_ena <= 1'b0;
end
else
cnt_ena <= 1'b0;
end
assign lcd_en = (cnt_ena >= ENA_DELAY/2) ? 1'b0 : 1'b1;
assign lcd_rw = 1'b0; //此LCD只写不读
assign state_flag = (cnt_ena == ENA_DELAY - 1'b1) ? 1'b1 : 1'b0; //!!!这个需要深究!!!
// 3.状态机设计(三段状态机,Moore型)
//状态编码
//Gray code : 40 states
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;
//(1)状态寄存
reg [7:0] current_state, next_state;
always @ (posedge clk_100M or negedge rst_n) begin
if(~rst_n)
current_state <= IDLE;
else if(state_flag)
current_state <= next_state;
else
current_state <= current_state;
end
//(2)状态变迁
always @ (*) begin
case(current_state)
IDLE: next_state = DISP_SET;
DISP_SET: next_state = DISP_OFF;
DISP_OFF: next_state = CLR_SCR;
CLR_SCR: next_state = CURSOR_SET1;
CURSOR_SET1: next_state = CURSOR_SET2;
CURSOR_SET2: next_state = ROW1_ADDR;
ROW1_ADDR: next_state = ROW1_0;
ROW1_0 : next_state = ROW1_1;
ROW1_1 : next_state = ROW1_2;
ROW1_2 : next_state = ROW1_3;
ROW1_3 : next_state = ROW1_4;
ROW1_4 : next_state = ROW1_5;
ROW1_5 : next_state = ROW1_6;
ROW1_6 : next_state = ROW1_7;
ROW1_7 : next_state = ROW1_8;
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
//(3)状态输出1:lcd_rs输出
always @ (posedge clk_100M or negedge rst_n) begin
if(~rst_n)
lcd_rs <= 1'b0;
else if(state_flag)
if( current_state == IDLE ||
current_state == DISP_SET ||
current_state == DISP_OFF ||
current_state == CLR_SCR ||
current_state == CURSOR_SET1 ||
current_state == CURSOR_SET2 ||
current_state == ROW1_ADDR ||
current_state == ROW2_ADDR )
lcd_rs <= 1'b0; //L: Instruction
else
lcd_rs <= 1'b1; //H: Data
else
lcd_rs <= lcd_rs;
end
//(3)状态输出2:数据输出
reg [7:0] lcd_data_out_r;
always @ (posedge clk_100M or negedge rst_n) begin
if(~rst_n)
lcd_data_out_r <= 8'h00;
else if(state_flag) begin
case(current_state)
IDLE : lcd_data_out_r <= 8'hxx;
//LCD1602 init
DISP_SET : lcd_data_out_r <= 8'h38; //Display mode: Set 16X2,5X7, 8 bits data
DISP_OFF : lcd_data_out_r <= 8'h08; //Display off
CLR_SCR : lcd_data_out_r <= 8'h01; //Clear LCD
CURSOR_SET1 : lcd_data_out_r <= 8'h06; //Set Cursor
CURSOR_SET2 : lcd_data_out_r <= 8'h0C; //Display on
//Display 1th line
ROW1_ADDR : lcd_data_out_r <= 8'h80;
ROW1_0 : lcd_ram_addr <= 5'd0;
ROW1_1 : lcd_ram_addr <= 5'd1;
ROW1_2 : lcd_ram_addr <= 5'd2;
ROW1_3 : lcd_ram_addr <= 5'd3;
ROW1_4 : lcd_ram_addr <= 5'd4;
ROW1_5 : lcd_ram_addr <= 5'd5;
ROW1_6 : lcd_ram_addr <= 5'd6;
ROW1_7 : lcd_ram_addr <= 5'd7;
ROW1_8 : lcd_ram_addr <= 5'd8;
ROW1_9 : lcd_ram_addr <= 5'd9;
ROW1_A : lcd_ram_addr <= 5'd10;
ROW1_B : lcd_ram_addr <= 5'd11;
ROW1_C : lcd_ram_addr <= 5'd12;
ROW1_D : lcd_ram_addr <= 5'd13;
ROW1_E : lcd_ram_addr <= 5'd14;
ROW1_F : lcd_ram_addr <= 5'd15;
//Display 2th line
ROW2_ADDR : lcd_data_out_r <= 8'hC0;
ROW2_0 : lcd_ram_addr <= 5'd16;
ROW2_1 : lcd_ram_addr <= 5'd17;
ROW2_2 : lcd_ram_addr <= 5'd18;
ROW2_3 : lcd_ram_addr <= 5'd19;
ROW2_4 : lcd_ram_addr <= 5'd20;
ROW2_5 : lcd_ram_addr <= 5'd21;
ROW2_6 : lcd_ram_addr <= 5'd22;
ROW2_7 : lcd_ram_addr <= 5'd23;
ROW2_8 : lcd_ram_addr <= 5'd24;
ROW2_9 : lcd_ram_addr <= 5'd25;
ROW2_A : lcd_ram_addr <= 5'd26;
ROW2_B : lcd_ram_addr <= 5'd27;
ROW2_C : lcd_ram_addr <= 5'd28;
ROW2_D : lcd_ram_addr <= 5'd29;
ROW2_E : lcd_ram_addr <= 5'd30;
ROW2_F : lcd_ram_addr <= 5'd31;
default : lcd_data_out_r <= 8'h00;
endcase
end
end
assign lcd_data_out = (~lcd_rs) ? lcd_data_out_r : lcd_ram_data;
endmodule
LCD1602显示RAM源代码
// LCD1602_ram
module LCD1602_ram
(
input wire clk_100M,
input wire rst_n,
input wire [4:0] read_addr,
input wire [4:0] write_addr,
input wire [7:0] write_data,
output wire [7:0] ram_data_out
);
reg [7:0] data_ram[0:31];
integer i;
assign ram_data_out = data_ram[read_addr];
always @ (posedge clk_100M or negedge rst_n) begin
if(~rst_n) begin
// for(i = 0; i < 32; i = i + 1)
// data_ram[i] <= 32; //这里可以用非阻塞赋值吗?可以
data_ram[0] <= 32; // 32在ASCII码表中表示不显示
data_ram[1] <= 32;
data_ram[2] <= 32;
data_ram[3] <= 32;
data_ram[4] <= 32;
data_ram[5] <= 32;
data_ram[6] <= 32;
data_ram[7] <= 32;
data_ram[8] <= 32;
data_ram[9] <= 32;
data_ram[10] <= 32;
data_ram[11] <= 32;
data_ram[12] <= 32;
data_ram[13] <= 32;
data_ram[14] <= 32;
data_ram[15] <= 32;
data_ram[16] <= 32;
data_ram[17] <= 32;
data_ram[18] <= 32;
data_ram[19] <= 32;
data_ram[20] <= 32;
data_ram[21] <= 32;
data_ram[22] <= 32;
data_ram[23] <= 32;
data_ram[24] <= 32;
data_ram[25] <= 32;
data_ram[26] <= 32;
data_ram[27] <= 32;
data_ram[28] <= 32;
data_ram[29] <= 32;
data_ram[30] <= 32;
data_ram[31] <= 32;
end
else
data_ram[write_addr] <= write_data;
end
endmodule
六、总结分析
一年前,我完成了8051的LCD1602驱动,一年后,我基本完成了FPGA的LCD1602的驱动设计。本次设计,我体会到了datasheet的重要性,“设计源于手册”绝非说说而已!
七、参考资料
[1]http://baike.baidu.com/view/18558.htm百度百科-LCD,2014年11月14日18:34
[2]http://baike.baidu.com/view/5881209.htm?fr=aladdin百度百科-LCD1602,2014年11月14日18:46
[3]韩彬,于潇宇,张雷鸣. FPGA设计技巧与案例开发详解.北京:电子工业出版社. 2014.
注:已上传源代码:FPGA_LCD1602_verilog.zip