两忘而化其道

FPGA_LCD1602_verilog

0
阅读(5249)

FPGA_LCD1602_verilog

——两忘而化其道(fei199311

〇、前言

话说好像就是一年前,我用8051成功驱动了LCD1602,一年后的今天,我通过Verilog HDL描述了LCD1602的驱动电路,并在FPGA上实现了(注:参考并学习了《FPGA设计技巧与案例开发详解》)。一年匆匆而过,好快!!!今天有个新闻——《流浪夫妻背3000枚硬币到银行兑换 寄给上大学女儿》,我们有什么理由不去奋斗!!!!!!!


一、名词解释

LCDLCDLiquid Crystal Display的简称)液晶显示器。LCD的构造是在两片平行的玻璃基板当中放置液晶盒,下基板玻璃上设置TFT(薄膜晶体管),上基板玻璃上设置彩色滤光片,通过TFT上的信号与电压改变来控制液晶分子的转动方向,从而达到控制每个像素点偏振光出射与否而达到显示目的。现在LCD已经替代CRT成为主流,价格也已经下降了很多,并已充分的普及[1]

LCD1602:工业字符型液晶,能够同时显示16X232个字符。LCD1602实物图如下图1所示。1602液晶也叫1602字符型液晶,它是一种专门用来显示字母、数字、符号等的点阵型液晶模块。它由若干个5X7或者5X11点阵字符位组成,每个点阵字符位都可以显示一个字符,每位之间有一个点距的间隔,每行之间也有间隔,起到了字符间距和行间距的作用,正因为如此所以它不能很好地显示图形(用自定义CGRAM,显示效果也不好)。1602LCD是指显示的内容为16X2,即可以显示两行,每行16个字符液晶模块(显示字符和数字)。市面上字符液晶大多数是基于HD44780液晶芯片的,控制原理是完全相同的,因此基于HD44780写的控制程序可以很方便地应用于市面上大部分的字符型液晶[2]

1.LCD1602实物图


二、设计需求

虽然LCD1602可显示的字符比较少,但是其功耗低、体积小、显示内容丰富、超薄轻巧且稳定性较好,在袖珍式仪表和低功耗应用系统中有广泛应用。


  1. 设计思想

首先研究LCD1602datasheet,这个是电路设计的根本,正所谓“设计源于手册[3]。由于LCD1602配置的串行特性,所以其驱动与配置用“C语言+单片机”实现起来对大多数设计者来说更容易理解和实现,所以可以先用C语言实现LCD1602的驱动,随后将C语言转换成相应的Verilog HDL代码即可。话说这个C语言转换成Verilog HDL一般情况下都是用FSM(有限状态机)实现的。


四、设计方案

整个基于FPGALCD1602控制器大致上可以分为两个模块:1.LCD1602驱动模块;2.LCD1602显示RAMLCD1602驱动模块负责配置LCD1602内部的寄存器和向LCD显示RAM中写入要显示的内容,用FSM(有限状态机)可以实现其功能。LCD1602显示RAM是给LCD用户提供的一个接口,用户通过这个接口可以使LCD1602任意位置显示任意内容(ASCII码表的内容,如图2所示)。
















2.CGROM中字符码与字符字模关系对照表

上图中就是LCD1602可以固定写的字符,还有一些编码对应的字符可以用户自定义,这里就不详述了,详见datasheet

要使用LCD1602,需要用一定的方式对其进行配置,那么首先看LCD1602有哪些引脚线,如下图3所示:

3.LCD1602引脚图

下表为接口信号说明表:

1.LCD1602接口信号说明表

通过上面的图3和表1,我们可以知道,需要重点关注的信号线有11根:RSR/WEDate I/O。关于I/O数据,需要看时序图;关于R/W信号,其控制读写,我们现在只写不读,所以将其接到GND即可;关于RS信号,此信号关系到配置LCD1602,所以应该参考datasheet上的初始化过程;关于E信号,这个比较重要,由于设计中一般使用同步电路,所以这个E信号需要参考LCD1602各个操作所需要的时间,由于清屏信号需要1.64ms,所以最高时钟为609.756Hz,为了实现的方便,本设计中采用500HzE信号。

LCD1602有个特点,就是一定要按照手册上给的方式进行一定的初始化才可以使用(话说,这个好像基本是每一个芯片都需要的。。。)。这个LCD1602的配置流程如下:

  1. 延时15ms,等待LCD1602上电稳定;

  2. 功能配置。写入指令38h

  3. 关闭LCD1602显示。写入指令08h

  4. 清屏操作。写入指令01h

  5. 设置显示模式。写入指令06h

  6. 打开显示。写入指令0Ch

  7. 配置行地址。写入指令80h(第一行)或C0h

  8. 写入数据。写入相应数据;

  9. 从新配置行地址。

从上面的配置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源代码

  1. 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.500Hzlcd_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)状态输出1lcd_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

  1. 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; // 32ASCII码表中表示不显示

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


六、总结分析

一年前,我完成了8051LCD1602驱动,一年后,我基本完成了FPGALCD1602的驱动设计。本次设计,我体会到了datasheet的重要性,“设计源于手册”绝非说说而已!


七、参考资料

[1]http://baike.baidu.com/view/18558.htm百度百科-LCD2014111418:34

[2]http://baike.baidu.com/view/5881209.htm?fr=aladdin百度百科-LCD16022014111418:46

[3]韩彬,于潇宇,张雷鸣. FPGA设计技巧与案例开发详解.北京:电子工业出版社. 2014.


注:已上传源代码:FPGA_LCD1602_verilog.zip



Baidu
map