串口控制LCD1602显示
0赞花了一个下午加一个晚上的时间,写了一个串口控制LCD1602显示程序。用的是virtex5的板子,高端霸气上档次的板子。
功能其实也很简单,就是串口发送什么数据,就将数据显示在LCD1602上面,同时串口把接收的数据给回发回来。
结构图:
说明:
1、 串口部分: 接收和发送串口数据。串口接收到的数据,发送给FIFO。当外部的串口发送使能,就将从FIFO读取的数据发送出去。
2、 FIFO: 作为接收串口数据的暂存数据存储器。因为LCD1602操作是比较慢的,所以不能直接将串口接收到的数据发送给LCD1602进行显示,否则,就会存在数据丢失,所以使用FIFO,先将串口接收到的数据暂存下来,然后在发送给LCD1602.
3、 FIFO控制器: 因为LCD1602处理数据是比较慢的,所以在LCD1602上一个数据还没有处理完的时候,FIFO不能提供下一个数据。所以就需要一个FIFO控制器,当检测到LCD1602处理完后,FIFO读取数据,同时使能显示,将从FIFO读取的数据显示在LCD1602上。另外还要使能串口发送,将FIFO取出的数据发送出去。
4、 LCD1602: 控制LCD1602显示数据。
结构图清楚后,下面就开始设计了。
首先是串口的设计,串口的设计就比较简单了。我设计的串口是全双工串口。发送和接收各自有自己的波特率生成器。
下面是端口列表:
module uart_top
#(
parameter baud_tx = 256000,
parameter baud_rx = 256000
)
(
input clk,
input rst_n,
input uart_rxd, //input serial rxd data
input tx_start, //input start send data module signal
input [7:0] tx_data, //input 8-bits send data
output tx_finish, //output send mode finish
output rx_finish, //output receive mode finish
output uart_txd, //output serial txd data
output [7:0] receive_data //output receive 8-bits data
);
然后是FIFO,这个是直接使用xilinx的IP。例化了一个数据宽度是8位,深度是64的FIFO。
FIFO控制器模块。这个也是比较简单。
module fifo_lcd(
input clk,
input rst_n,
input iempty, //fifo empty signal . 1 mean FIFO is empty
input [7:0] ififo_data, //fifo read data
input iLCD_finish, //LCD finish siganl . 1 mean LCD operate finish
output reg oFIFO_rd_en, //FIFO read enable signal . 1 mean read data from FIFO
output [8:0] oLCD_data, //data to LCD to display
output reg oLCD_start // LCD operate enable signal , 1 mean start
);
assign oLCD_data = {1'b1,ififo_data};
localparam idle_state = 1'd0;
localparam trans_state = 1'd1;
reg state;
always@(posedge clk or negedge rst_n) begin
if(!rst_n)
begin
state <= idle_state;
oLCD_start <= 1'b0;
oFIFO_rd_en <= 1'b0;
end
else
begin
case(state)
idle_state: begin
if(iempty == 0)
begin
oFIFO_rd_en <= 'b1;
state <= trans_state;
end
else
oFIFO_rd_en <= 'b0;
end
trans_state: begin
oFIFO_rd_en <= 'b0;
oLCD_start <= 1'b1;
if(iLCD_finish == 1)
begin
oLCD_start <= 1'b0;
state <= idle_state;
end
end
endcase
end
end
endmodule
主要就是一个两个状态的状态机。Idle_state和trans_state。
在idle_state,如果检测到FIFO不是空的话,就使能读取FIFO的使能信号,从FIFO中读取数据,然后状态跳转。
在treans_state,将读取FIFO的使能信号给失能。这样,就暂时不向FIFO中读取数据。使能LCD的使能信号,让LCD模块工作。
最后这个,才是重点的LCD1602模块。
这个模块包括两个部分,一个是底层的控制模块,即控制LCD_EN,LCD_RS_LCD_RW,LCD_DATA这几个信号。另外一个是控制发送数据的状态机。
至于底层的控制模块,这里贴出源代码,是将网上别人写的稍微进行修改的:
module LCD_Controller ( // Host Side
input [7:0] iDATA,
input iRS,
input iCLK,
input iRST_N,
input iStart,
output reg oDone,
// LCD Interface
output [3:0] LCD_DATA,
output LCD_RW,
output reg LCD_EN,
output LCD_RS
);
// CLK
localparam CLK_Divide = 50;
// Internal Register
reg [5:0] Cont;
reg [2:0] ST;
localparam delay_number = 'd100000;
//localparam delay_number = 'd10;
reg [16:0] time_count;
/////////////////////////////////////////////
// Only write to LCD, bypass iRS to LCD_RS
assign LCD_DATA = (ST != 4) ? iDATA[7:4] : iDATA[3:0];
assign LCD_RW = 1'b0;
assign LCD_RS = iRS;
/////////////////////////////////////////////
always@(posedge iCLK or negedge iRST_N)
begin
if(!iRST_N)
begin
oDone <= 1'b0;
LCD_EN <= 1'b0;
Cont <= 0;
ST <= 0;
time_count <= 'd0;
end
else
begin
case(ST)
0: begin
oDone <= 1'b0;
if(iStart == 1)
ST <= 'd1; // Wait Setup
else
ST <= 'd0;
end
1: begin
if(time_count > delay_number)
begin
ST <= 'd2;
time_count <= 'd0;
end
else
time_count <= time_count + 1'b1;
end
2: begin //send high 4 bits
if(Cont < CLK_Divide)
Cont <= Cont+1'b1;
else
ST <= 'd3;
if( Cont > 10 && Cont < CLK_Divide / 2 + 10)
LCD_EN <= 1'b1;
else
LCD_EN <= 1'b0;
end
3: begin
Cont <= 'd0;
ST <= 'd4;
end
4: begin //send low 4 bits
if(Cont < CLK_Divide)
Cont <= Cont+1'b1;
else
ST <= 'd5;
if( Cont > 10 && Cont < CLK_Divide / 2 + 10)
LCD_EN <= 1'b1;
else
LCD_EN <= 1'b0;
end
5: begin
oDone <= 1'b1;
Cont <= 'd0;
ST <= 'd6;
end
6: begin
ST <= 0;
oDone <= 1'b0;
end
endcase
end
end
endmodule
这里的LCD是4位模式的,所以需要发送两次,第一次发送高4位,第二次发送低4位。另外,因为没有进行检测忙的操作,所以在状态的1状态,进行了一个时间比较长的延时。因为FPGA中对于inout信号操作要稍微麻烦一点,这里简单一点,就没有检测忙信号,直接使用延时。
这个模块,当iStart为1的时候,开始工作,当oDone为1的时候,表示一个数据发送完毕。
然后是上层的状态机设计:
首先是定义状态:
localparam init_state = 3'd0; //
localparam write_1line_state = 3'd1; //
localparam wait_1line_state = 3'd2; //
localparam write_2line_cmd_state = 3'd3; //
localparam write_2line_state = 3'd4; //
localparam wait_2line_state = 3'd5; //
localparam wait_data_state = 3'd6;
localparam clear_state = 3'd7;
总共定义了8个状态:
Init_state: 对LCD1602初始化。就是发送初始化的一些命令。
Write_1line_state: 写第一行数据的状态。在这个状态中,写的数据是写在第一行。
Wait_1line_state: 写第一行的等待状态。在第一行中写数据后,就会跳到这个状态,如果超过一定时间,没有接收到使能信号,就说明数据写完了。然后就跳转到wait_data_state。
write_2line_cmd_state: 因为LCD一行只能显示16个字符,如果超过16个字符,就要写到下一行,但是在写到第二行之前,需要发送命令0Xc0.将光标移到第二行。
write_2line_state和wait_2line_state两个状态和写第一行的两个状态功能是一样的。
wait_data_state: 当数据写完之后,会到这个状态。如果接收到新的数据,就说明要进行新的数据显示了。就跳转到clear_state。
clear_state: 在这个状态,首先发送命令0x01,对屏幕进行清屏,因为要进行新的显示了。然后再发送0x80.将光标移到第一行。然后跳转到Write_1line_state,开始进行写数据。
状态机理清楚后,程序写起来就比较简单了。
// 9'h120 @ 9'h140 ` 9'h160
// ! 9'h121 A 9'h141 a 9'h161
// " 9'h122 B 9'h142 b 9'h162
// # 9'h123 C 9'h143 c 9'h163
// $ 9'h124 D 9'h144 d 9'h164
// % 9'h125 E 9'h145 e 9'h165
// & 9'h126 F 9'h146 f 9'h166
// ' 9'h127 G 9'h147 g 9'h167
// ( 9'h128 H 9'h148 h 9'h168
// ) 9'h129 I 9'h149 i 9'h169
// * 9'h12A J 9'h14A j 9'h16A
// + 9'h12B K 9'h14B k 9'h16B
// , 9'h12C L 9'h14C l 9'h16C
// - 9'h12D M 9'h14D m 9'h16D
// . 9'h12E N 9'h14E n 9'h16E
// / 9'h12F O 9'h14F o 9'h16F
// 0 9'h130 P 9'h150 p 9'h170
// 1 9'h131 Q 9'h151 q 9'h171
// 2 9'h132 R 9'h152 r 9'h172
// 3 9'h133 S 9'h153 s 9'h173
// 4 9'h134 T 9'h154 t 9'h174
// 5 9'h135 U 9'h155 u 9'h175
// 6 9'h136 V 9'h156 v 9'h176
// 7 9'h137 W 9'h157 w 9'h177
// 8 9'h138 X 9'h158 x 9'h178
// 9 9'h139 Y 9'h159 y 9'h179
// : 9'h13A Z 9'h15A z 9'h17A
// ; 9'h13B [ 9'h15B { 9'h17B
// < 9'h13C 锟 9'h15C | 9'h17C
// = 9'h13D ] 9'h15D } 9'h17D
// > 9'h13E ^ 9'h15E 鈫 9'h17E
// ? 9'h13F _ 9'h15F 鈫 9'h17F
module LCD ( // Host Side
input iCLK, //鏃堕挓
input iRST_N, //澶嶄綅锛屼綆鐢靛钩鏈夋晥
input istart,
output ofinish,
input [8:0] idata,
output [3:0] mLCD_DATA,
output mLCD_RS,
output mLCD_RW,
output mLCD_en
);
// Internal Wires/Registers
reg mstart;
wire mfinish;
reg [8:0] lcd_data;
//瀹氫箟4涓姸鎬
localparam init_state = 3'd0; //
localparam write_1line_state = 3'd1; //
localparam wait_1line_state = 3'd2; //
localparam write_2line_cmd_state = 3'd3; //
localparam write_2line_state = 3'd4; //
localparam wait_2line_state = 3'd5; //
localparam wait_data_state = 3'd6;
localparam clear_state = 3'd7;
localparam wait_number = 26'b11_1111_1111_1111_1110_1111_1111;
//localparam wait_number = 26'b111_1111_1111;
reg [2:0] state;
reg [4:0] lcd_counter;
reg [25:0] time_counter;
assign ofinish = (state == write_1line_state || state == write_2line_state ) ? mfinish : 1'b0;
always@(posedge iCLK or negedge iRST_N) begin
if(!iRST_N)
begin
state <= init_state;
mstart <= 1'b0;
lcd_counter <= 'd0;
time_counter <= 'd0;
end
else
begin
case(state)
init_state: begin
mstart <= 1'b1;
if(mfinish == 1)
begin
if(lcd_counter >= 'd4)
begin
lcd_counter <= 'd0;
state <= write_1line_state;
mstart <= 1'b0;
end
else
lcd_counter <= lcd_counter + 1'b1;
end
end
write_1line_state: begin
mstart <= istart;
time_counter <= 'd0;
if( mfinish == 1 )
begin
if(lcd_counter > 14 )
begin
lcd_counter <= 'd0;
state <= write_2line_cmd_state;
end
else
begin
lcd_counter <= lcd_counter + 1'b1;
state <= wait_1line_state;
mstart <= 1'b0;
end
end
end
wait_1line_state: begin
if (istart == 1)
begin
time_counter <= 'd0;
state <= write_1line_state;
end
if(time_counter > wait_number)
begin
state <= wait_data_state;
end
else
time_counter <= time_counter + 1'b1;
end
write_2line_cmd_state: begin
mstart <= 1'b1;
if( mfinish ==1 )
begin
mstart <= 1'b0;
state <= write_2line_state;
end
end
write_2line_state: begin
mstart <= istart;
time_counter <= 'd0;
if( mfinish == 1 )
begin
if(lcd_counter > 14 )
begin
lcd_counter <= 'd0;
state <= clear_state;
lcd_counter <= 'd0;
end
else
begin
lcd_counter <= lcd_counter + 1'b1;
state <= wait_2line_state;
mstart <= 1'b0;
end
end
end
wait_2line_state: begin
if (istart == 1)
begin
time_counter <= 'd0;
state <= write_2line_state;
mstart <= 1'b1;
end
if(time_counter > wait_number)
begin
state <= wait_data_state;
end
else
time_counter <= time_counter + 1'b1;
end
wait_data_state: begin
mstart <= 1'b0;
if(istart == 1)
state <= clear_state;
lcd_counter <= 'd0;
end
clear_state: begin
mstart <= 1'b1;
if(mfinish == 1)
begin
if(lcd_counter >= 1)
begin
lcd_counter <= 'd0;
state <= write_1line_state;
end
else
lcd_counter <= lcd_counter + 1'b1;
end
end
endcase
end
end
always @ (*)
begin
if(state == init_state)
case(lcd_counter)
// Initial
0: lcd_data = 9'h028;//
1: lcd_data = 9'h00C;//
2: lcd_data = 9'h006;//
3: lcd_data = 9'h001;//
4: lcd_data = 9'h080;//
5: lcd_data = 9'h16c;
6: lcd_data = 9'h175;
7: lcd_data = 9'h16A;
8: lcd_data = 9'h175;
9: lcd_data = 9'h16E;
10: lcd_data = 9'h121;
default: lcd_data = 9'h001;
endcase
else if(state == clear_state)
case(lcd_counter)
0: lcd_data = 9'h001;
1: lcd_data = 9'h080;
2: lcd_data = 9'h080;
default: lcd_data = 9'h001;
endcase
else if(state == write_2line_cmd_state)
lcd_data = 9'h0c0;
else
lcd_data = idata;
end
LCD_Controller u1( // Host Side
.iDATA (lcd_data[7:0]), //杈撳叆鍙戦€佺殑8浣嶆暟鎹
.iRS (lcd_data[8]), //杈撳叆鍛戒护鏁版嵁閫夋嫨淇″彿
.iCLK (iCLK), //鏃堕挓
.iRST_N (iRST_N), //澶嶄綅淇″彿 锛浣庣數骞虫湁鏁
.iStart (mstart), //LCD鎺у埗鍣ㄦ湁鏁堜俊鍙 1 LCD寮€鍚伐浣 0 LCD鍋滄宸ヤ綔
.oDone(mfinish), //LCD瀹屾垚淇″彿 1琛ㄧず瀹屾垚
// LCD Interface
.LCD_DATA(mLCD_DATA[3:0]), //LCD 4浣嶆暟鎹
.LCD_RW (mLCD_RW), //LCD 璇诲啓淇″彿 0琛ㄧず鍐 1琛ㄧず璇
.LCD_EN (mLCD_en), //LCD 璇诲啓浣胯兘淇″彿 涓嬮檷娌挎湁鏁
.LCD_RS (mLCD_RS) //LCD 鍛戒护鏁版嵁閫夋嫨淇″彿 1閫夋嫨
);
endmodule
然后在写一个顶层,将各个模块进行调用,然后连接信号即可。
最后贴上效果图:
开发板显示:
发送的数据和LCD上显示的数据一样。并且串口接收的数据和发送的数据一样。




