分享

【精品博文】详细解析基于FPGA的LCD1602驱动控制

 ChinaAET 2020-10-31

赢一个双肩背包

有多难?

戳一下试试看!

→_→

长摁识别

【主题】:详细解析基于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中进行输出。

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多