【主题】:详细解析基于FPGA的LCD1602驱动控制 【作者】:LinCoding 【时间】:2016.11.23 这周末去找女票玩了,回来继续学习吧,唉,路漫漫,再过不久又要开题了,事情越来越多,能自己学习的时间越来越少。。。 废话不多说,LCD1602的驱动和控制大家一定都很清楚,这次的LCD1602程序是以三段式状态机为基础的,看过之后你会发现和三段式流水灯简直就是一模一样,因此,状态机有什么难的呢?只要掌握好我总结的那几点,一切变得So Easy。。。
先上效果图:
LCD1602驱动时序什么的大家请百度,网上实在太多了。 (源码出自CrazyBingo,尊重版权) module lcd1602_driver
(
input clk,
input rst_n,
input [127:0] line_rom1,
input [127:0] line_rom2,
output lcd_en,
output lcd_rw,
output reg lcd_rs,
output reg [7:0] lcd_data
); 第一部分,不多说,line_rom1和line_rom2分别给LCD1602的两行送数据,LCD1602每行可容纳16个字符,每个字符是ASCII码,也就是8位,所以,一共需要128位的数据。
//------------------------------------
//delay 20ms for LCD1602 steady
localparam T20MS = 20'd1_000_000;
//localparam T20MS = 20'd20; //just for simulation
reg [19:0] delay20ms_cnt;
always @ ( posedge clk or negedge rst_n )
begin
if ( ! rst_n )
delay20ms_cnt <= 20'd0;
else if ( delay20ms_cnt < T20MS )
delay20ms_cnt <= delay20ms_cnt + 1'b1;
else
delay20ms_cnt <= T20MS;
end
wire delay20ms_done = ( delay20ms_cnt == T20MS ) ? 1'b1 : 1'b0; 第二部分,是上电延时20ms的计数器,需要注意一点:由于上电延时20ms只执行一次,因此,在到达20ms以后,delay20ms_cnt<= T20MS; 将永不再进行计数。同时更新计数标志位,同样,计数器的输出采用组合逻辑。 //------------------------------------
//generate 500Hz clock for LCD1602
localparam T2MS = 17'd100_000;
//localparam T2MS = 17'd16; //just for simulation
reg [16:0] delay2ms_cnt;
always @ ( posedge clk or negedge rst_n )
begin
if ( ! rst_n )
delay2ms_cnt <= 17'd0;
else if ( delay20ms_done )
delay2ms_cnt <= ( delay2ms_cnt < T2MS ) ? delay2ms_cnt + 1'b1 : 17'd1;
else
delay2ms_cnt <= delay2ms_cnt;
end
assign lcd_rw = 1'b0; //write only
assign lcd_en = ( delay2ms_cnt > T2MS/2 ) ? 1'b0 : 1'b1;
wire lcd_write_flag = ( delay2ms_cnt == T2MS*3/4 ) ? 1'b1 : 1'b0;第三部分,产生一个50Hz的时钟,用于状态机的跳转。这里注意两点:
1、lcd_en其实可认为是LCD1602的时钟,类似于之前文章中驱动74HC595时的shift_clk。而与其不同的是,74HC595是上升沿有效,因此时钟是先低后高,shift_flag在下降沿进行输出,以使得shift_clk的上升沿正好出现在数据的正中间,使得建立时间和保持时间最佳。 而LCD1602属于高电平有效的器件,因此,时钟是先高后低,其实,对于芯片和器件来说,时钟大部分都是上升沿或者高电平有效,很少会有低电平或下降沿有效的。而为了使得高电平正好出现在数据的正中间,因此,lcd_write_flag在T2MS的3/4时进行输出,效果如下图所示。 如上图所示,正好使lcd_en的高电平出现在数据的正中间,这样建立时间和保持时间最佳! 2、由于只对LCD1602进行写操作,所以lcd_rw直接赋0,这时会在综合时出现以下警告。 就是说有一个输出引脚被直接拉为了1或者0,这不用管它。 还有就是计数器的输出采用组合逻辑。 //------------------------------------
//FSM: encode using Gray code
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; 第四部分是三段式状态机的状态编码,使用格雷码进行编码,以免使得输出产生毛刺。
//------------------------------------
//Three Part FSM: part 1
reg [5:0] current_state;
reg [5:0] next_state;
always @ ( posedge clk or negedge rst_n )
begin
if ( ! rst_n )
current_state <= IDLE;
else if ( lcd_write_flag )
current_state <= next_state;
else
current_state <= current_state;
end 第五部分是三段式FSM的第一段,该说的在三段式流水灯中那篇文章中已经说过了。
//------------------------------------
//Three Part FSM: part 2
always @ ( * )
begin
next_state = IDLE;
case ( current_state )
//LCD1602 init
IDLE : next_state = DISP_SET; //5'h00
DISP_SET : next_state = DISP_OFF; //5'h01
DISP_OFF : next_state = CLR_SCR; //5'h03
CLR_SCR : next_state = CURSOR_SET1; //5'h02
CURSOR_SET1 : next_state = CURSOR_SET2; //5'h06
CURSOR_SET2 : next_state = ROW1_ADDR; //5'h07
//Display 1th line
ROW1_ADDR : next_state = ROW1_0; //5'h05;
ROW1_0 : next_state = ROW1_1; //5'h04;
ROW1_1 : next_state = ROW1_2; //5'h0C;
ROW1_2 : next_state = ROW1_3; //5'h0D;
ROW1_3 : next_state = ROW1_4; //5'h0F;
ROW1_4 : next_state = ROW1_5; //5'h0E;
ROW1_5 : next_state = ROW1_6; //5'h0A;
ROW1_6 : next_state = ROW1_7; //5'h0B;
ROW1_7 : next_state = ROW1_8; //5'h09;
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 第六部分是三段式FSM的第二段,同样,该说的在三段式流水灯中那篇文章中已经说过了。
//------------------------------------
//Three Part FSM: part 3-1
always @ ( posedge clk or negedge rst_n )
begin
if ( ! rst_n )
lcd_rs <= 1'b0;
else if ( lcd_write_flag )
if( next_state == IDLE ||
next_state == DISP_SET ||
next_state == DISP_OFF ||
next_state == CLR_SCR ||
next_state == CURSOR_SET1 ||
next_state == CURSOR_SET2 ||
next_state == ROW1_ADDR ||
next_state == ROW2_ADDR )
lcd_rs <= 1'b0; //L: Instruction
else
lcd_rs <= 1'b1; //H: Data
else
lcd_rs <= lcd_rs;
end
//------------------------------------
//Three Part FSM: part 3-2
always @ ( posedge clk or negedge rst_n )
begin
if ( ! rst_n )
lcd_data <= 8'h00;
else if ( lcd_write_flag )
case ( next_state )
IDLE : lcd_data <= 8'hxx;
DISP_SET : lcd_data <= 8'h38;
DISP_OFF : lcd_data <= 8'h08;
CLR_SCR : lcd_data <= 8'h01;
CURSOR_SET1 : lcd_data <= 8'h06;
CURSOR_SET2 : lcd_data <= 8'h0C;
//Display 1th line
ROW1_ADDR : lcd_data <= 8'h80;
ROW1_0 : lcd_data <= line_rom1[127:120];
ROW1_1 : lcd_data <= line_rom1[119:112];
ROW1_2 : lcd_data <= line_rom1[111:104];
ROW1_3 : lcd_data <= line_rom1[103: 96];
ROW1_4 : lcd_data <= line_rom1[ 95: 88];
ROW1_5 : lcd_data <= line_rom1[ 87: 80];
ROW1_6 : lcd_data <= line_rom1[ 79: 72];
ROW1_7 : lcd_data <= line_rom1[ 71: 64];
ROW1_8 : lcd_data <= line_rom1[ 63: 56];
ROW1_9 : lcd_data <= line_rom1[ 55: 48];
ROW1_A : lcd_data <= line_rom1[ 47: 40];
ROW1_B : lcd_data <= line_rom1[ 39: 32];
ROW1_C : lcd_data <= line_rom1[ 31: 24];
ROW1_D : lcd_data <= line_rom1[ 23: 16];
ROW1_E : lcd_data <= line_rom1[ 15: 8];
ROW1_F : lcd_data <= line_rom1[ 7: 0];
//Display 2th line
ROW2_ADDR : lcd_data <= 8'hC0;
ROW2_0 : lcd_data <= line_rom2[127:120];
ROW2_1 : lcd_data <= line_rom2[119:112];
ROW2_2 : lcd_data <= line_rom2[111:104];
ROW2_3 : lcd_data <= line_rom2[103: 96];
ROW2_4 : lcd_data <= line_rom2[ 95: 88];
ROW2_5 : lcd_data <= line_rom2[ 87: 80];
ROW2_6 : lcd_data <= line_rom2[ 79: 72];
ROW2_7 : lcd_data <= line_rom2[ 71: 64];
ROW2_8 : lcd_data <= line_rom2[ 63: 56];
ROW2_9 : lcd_data <= line_rom2[ 55: 48];
ROW2_A : lcd_data <= line_rom2[ 47: 40];
ROW2_B : lcd_data <= line_rom2[ 39: 32];
ROW2_C : lcd_data <= line_rom2[ 31: 24];
ROW2_D : lcd_data <= line_rom2[ 23: 16];
ROW2_E : lcd_data <= line_rom2[ 15: 8];
ROW2_F : lcd_data <= line_rom2[ 7: 0];
default : lcd_data <= 8'h00;
endcase
end第七部分,三段式FSM的第三段,注意一点:
FSM的第三段使用了两个always,这是为何? 三段式FSM并不意味着是三个always,尽管大多数情况下我们可以用三个always解决问题,但是如这个例子,由于lcd_data即可能输出的是数据也可能输出的是命令,而输出命令时lcd_rs <= 1'b0; 输出数据时lcd_rs <= 1'b1; 因此我们需要同时输出lcd_rs和lcd_data,而触发这两者输出的都是lcd_write_flag,因此将这两个信号分别在两个always钟输出很合理,可根据需要同时输出,而如果写在一个always中,如以下代码: DISP_SET :
begin
lcd_rs <= 1'b0;
lcd_data <= 8'h38;
end 这样做的话由于lcd_rs和lcd_data在一个always中输出,会使得lcd_rs和lcd_data的输出正好差了一个clk,时序上出现混乱,还得想办法做两个信号的同步处理(最常用的办法是在定义一个lcd_data_r信号,寄存一级lcd_data,以使得两者同步),十分没必要,因此,写成两个always十分的有利! 最后。。。这样做,就成功了呗! 总结:
1、对于高电平有效的芯片或器件,时钟输出先高后低,时钟输出标志位在计数总数的3/4处。
2、对于上升沿有效的芯片或器件,时钟输出先低后高,时钟输出标志位在计数总数处,也就是下降沿的地方。 3、三段式状态机,对于两个输出信号需要同步输出的,可将两个信号分别放到两个always中进行输出。
|