串口(UART)的FPGA实现

1、什么是串口(UART)?

        串口作为常用的三大低速总线(UART、SPI、IIC)之一,在设计众多通信接口和调试时占有重要地位。串口(UART)全称通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),主要用于数据间的串行传递,是一种全双工传输模式。它在发送数据时将并行数据转换成串行数据来传输,在接收数据时将接收到的串行数据转换成并行数据。

        “异步”两个字即意味着在数据传递的两个模块之间使用的不是同步时钟。实际上在异步串口的传输中是不需要时钟的,而是通过特定的时序来标志传输的开始(起始位--由高到低)和结束(结束位,拉高)。

2、串口的组成

2.1、串口的物理层

        UART 通信只有两根信号线,一根是发送数据端口线叫 tx(Transmitter),一根是接收数据端口线叫 rx(Receiver),如图所示,对于 PC 来说它的 tx 要和对于 FPGA来说的 rx 连接,同样 PC 的 rx 要和 FPGA 的 tx 连接,如果是两个 tx 或者两个 rx 连接那数据就不能正常被发送出去和接收到。 

串口(UART)的FPGA实现_第1张图片

        信号的传输由外部驱动电路实现。电信号的传输过程有着不同的电平标准和接口规范,针对异步串行通信的接口标准有RS232、RS422、RS485等,它们定义了接口不同的电气特性,如RS-232是单端输入输 出,而RS-422/485为差分输入输出等。传输距离较短时(不超过15m),RS232是串行通信最常用的接口标准。RS-232标准的串口最常见的接口类型为DB9,样式如图所示,工业控制领域中用到的工控机一般都配备多个串口,很多老式台式机也都配有串口。但是笔记本电脑以及较新一点 的台式机都没有串口,它们一般通过USB转串口线来实现与外部设备的串口通信。

串口(UART)的FPGA实现_第2张图片

        DB9接口定义以及各引脚功能说明如图所示,我们一般只用到其中的2(RXD)、3 (TXD)、5(GND)引脚,其他引脚在普通串口模式下一般不使用:

串口(UART)的FPGA实现_第3张图片

2.2、UART协议

        UART 在发送或接收过程中的一帧数据由4部分组成,起始位、数据位、奇偶校验位和停止位,如图所示。其中,起始位标志着一帧数据的开始,停止位标志着一帧数据的结束,数据位是一帧数据中的有效数据。

        校验位分为奇校验和偶校验,用于检验数据在传输过程中是否出错。 奇校验时,发送方应使数据位中1的个数与校验位中1的个数之和为奇数;接收方在接收数据时, 对1的个数进行检查,若不为奇数,则说明数据在传输过程中出了差错。同样,偶校验则检查 1的个数是否为偶数。关于奇偶校验可参考:Verilgo实现的FPGA奇偶校验 

串口(UART)的FPGA实现_第4张图片

         UART通信过程中的数据格式及传输速率是可设置的,为了正确的通信,收发双方应约定并遵循同样的设置。数据位可选择为5、6、7、8位,其中8位数据位是最常用的,在实际应用中 一般都选择8位数据位;校验位可选择奇校验、偶校验或者无校验位;停止位可选择1位(默认), 1.5或2位。

        串口通信的速率用波特率表示,它表示每秒传输二进制数据的位数,单位是bps(位 /秒),常用的波特率有9600、19200、38400、57600以及115200等。如波特率9600则代表每秒传输9600bit数据,以串口发送1个字节10bit算(起始位1bit+数据8bit+停止位1bit+NO校验位),则传输1个字节需要的时间是1*10/9600秒。 

3、串口发送模块

3.1、接口定义与整体设计

        发送模块整体框图、输入输出信号如下所示:

串口(UART)的FPGA实现_第5张图片

        其中信号描述如下:

串口(UART)的FPGA实现_第6张图片

        需要说明的是,uart_tx_data为需要发送的一个字节的数据,uart_tx_en为发送使能位,当其拉高,则代表此时通过串口发送数据线发送数据 uart_tx_data。

3.2、设计思路

  • 该模块支持任意波特率(理论上)的发送,但需要在使用该模块时使用参数将其例化,数据位8位,起始位和停止位各1位,无奇偶校验
  • 需要捕捉发送使能信号,将发送使能信号打拍两次,捕捉其上升沿。当捕捉到使能信号后拉高发送标志信号,标志模块进入发送过程;当发送完10个bit后,拉低发送标志信号,标志发送过程结束
  • 假设波特率为9600,则发送一个bit的时间为1s/9600,一个数据的传输共10bit(数据位8位,起始位和停止位各1位),则共需要1s/960;假设系统时钟为50MHz(参数化以便适应不同的系统频率),则其周期为20ns,那么发送一个bit所需要的系统周期数为(1s/960)/ 20ns ≈ 5208(个)。在发送过程中使用一个计数器计数,计数区间为(0,5208-1),这样的区间一共10个(一个字节需要发送10个bit);此外还需一个计数器对发送的bit数计数(每当上一个计数器计数到5207则表示发送完了一个bit),计数区间(0,9)
  • 在发送过程,根据计数器的值(发送bit计数器),对发送数据线进行操作。
    • 若发送bit计数器 = 0,则代表此时需要发送起始位;
    • 若发送bit计数器 = 1,则代表此时需要发送发送数据的最低位LSB(数据的发送总是低位在前,高位在后);
    • ······
    • 若发送bit计数器 = 8,则代表此时需要发送发送数据的最高位MSB;
    • 若发送bit计数器 = 9,则代表此时需要发送停止位;
  • 发送数据线在不处于发送状态时需拉高,以满足UART时序

3.3、Verilg代码

        根据上述设计思路与模块接口可编写代码如下:

//串口发送模块
module uart_tx
#(
	parameter 		BPS		= 'd9_600		,	//发送波特率
	parameter 		CLK_FRE	= 'd50_000_000		//输入时钟频率
)
(
//系统接口
	input 			sys_clk			,			//系统时钟
	input 			sys_rst_n		,			//系统复位,低电平有效
//用户接口	
	input	[7:0] 	uart_tx_data	,			//需要通过UART发送的数据,在uart_tx_en为高电平时有效
	input			uart_tx_en		,			//发送使能,当其为高电平时,代表此时需要发送数据
//UART发送线		
	output reg 		uart_txd					//UART发送数据线
);

//根据波特率计算传输每个bit需要多个系统时钟
localparam	BPS_CNT = CLK_FRE / BPS;

//reg define
reg 		uart_tx_en_d1	;					//发送使能信号打1拍
reg			uart_tx_en_d0	;					//发送使能信号打2拍
reg 		tx_en			;					//发送标志信号,拉高代表发送过程正在进行
reg [7:0]  	uart_data_reg	;					//寄存要发送的数据
reg [15:0] 	clk_cnt			;					//计数器,用于计数发送一个bit数据所需要的时钟数
reg [3:0]  	bit_cnt			;					//bit计数器,标志当前发送了多少个bit
//wire define			
wire 		pos_uart_tx_en	;					//使能端的上升沿信号

//捕捉使能端的上升沿信号
assign pos_uart_tx_en = uart_tx_en_d0 && (~uart_tx_en_d1);

always @(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		uart_tx_en_d0 <= 1'b0;
		uart_tx_en_d1 <= 1'b0;		
	end                  
	else begin           
		uart_tx_en_d0 <= uart_tx_en;
		uart_tx_en_d1 <= uart_tx_en_d0;
	end	
end

//当发送使能信号到达时,寄存待发送的数据,并进入发送过程
always @(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		tx_en <=1'b0;
		uart_data_reg <=8'd0;
	end
	else if(pos_uart_tx_en)begin								//发送使能有效
		uart_data_reg <= uart_tx_data;							//寄存需要发送的数据
		tx_en <= 1'b1;											//拉高发送使能,标志进入发送状态
	end	
	else if((bit_cnt == 4'd9) && (clk_cnt == BPS_CNT >> 1'b1))begin	//发送完了全部数据	
		tx_en <= 1'b0;                                          //拉低发送使能,标志退出发送状态
		uart_data_reg <= 8'd0;                                  //清空寄存数据
	end
	else begin
		uart_data_reg <= uart_data_reg;
		tx_en <= tx_en;	
	end
end
//进入发送过程后,启动时钟计数器与发送bit计数器
always @(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		clk_cnt <= 16'd0;
		bit_cnt <= 4'd0;
	end
	else if(tx_en) begin										//在发送状态
		if(clk_cnt < BPS_CNT - 1'd1)begin						//一个bit数据没有发送完
			clk_cnt <= clk_cnt + 1'b1;							//时钟计数器+1
			bit_cnt <= bit_cnt;									//bit计数器不变
		end					
		else begin												//一个bit数据发送完了	
			clk_cnt <= 16'd0;									//清空时钟计数器,重新开始计时
			bit_cnt <= bit_cnt+1'b1;							//bit计数器+1,表示发送完了一个bit的数据
		end					
	end					
	else begin													//不在发送状态
		clk_cnt <= 16'd0;                   					//清零
		bit_cnt <= 4'd0;                    					//清零
	end
end
//根据发送数据计数器来给uart发送端口赋值
always @(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)
		uart_txd <= 1'b1;										//默认为高状态
	else if(tx_en)                                  			//处于发送状态
		case(bit_cnt)											//数据发送从低位到高位
			4'd0: uart_txd <= 1'b0;								//起始位,拉低发送数据线
			4'd1: uart_txd <= uart_data_reg[0];     			//LSB
			4'd2: uart_txd <= uart_data_reg[1];     			//
			4'd3: uart_txd <= uart_data_reg[2];     			//
			4'd4: uart_txd <= uart_data_reg[3];     			//
			4'd5: uart_txd <= uart_data_reg[4];     			//
			4'd6: uart_txd <= uart_data_reg[5];     			//
			4'd7: uart_txd <= uart_data_reg[6];     			//
			4'd8: uart_txd <= uart_data_reg[7];     			//MSB
			4'd9: uart_txd <= 1'b1;								//终止位,拉高发送数据线
			default:;			
		endcase			
	else 														//不处于发送状态
		uart_txd <= 1'b1;										//默认为高状态
end

endmodule 

3.4、Testbench

        仿真模块的代码如下:

  • 设定波特率230400(这样的目的是为了更方便的观察发送使能信号uart_tx_en)
  • 3000ns后,拉高发送使能信号uart_tx_en一个周期,同时生成一个8bit的随机数据给uart_tx_data作为要发送的数据
  • 等待1S/230400 * 10 (在当前波特率下发送一个BYTE数据所需要的时间)
  • 等待2000ns(为了在两次发送过程中制造间隙,方便观察波形)
  • 拉高发送使能信号uart_tx_en一个周期,同时生成另一个8bit的随机数据给uart_tx_data作为要发送的数据
`timescale 1ns/1ns	//定义时间刻度
//模块、接口定义
module tb_uart_tx();

reg 			sys_clk			;			
reg 			sys_rst_n		;			
reg [7:0]		uart_tx_data	;
reg 			uart_tx_en		;
			
wire 	 		uart_txd		;

parameter		BPS 	= 'd230400		;			//波特率
parameter		CLK_FRE = 'd50_000_000	;			//系统频率50M
//例化发送模块
uart_tx #(
	.BPS			(BPS			),		//波特率9600
	.CLK_FRE		(CLK_FRE		)		//时钟频率50M	
)	
u_uart_tx(	
	.sys_clk		(sys_clk		),			
	.sys_rst_n		(sys_rst_n		),
	
	.uart_tx_data	(uart_tx_data	),			
	.uart_tx_en		(uart_tx_en		),		
	.uart_txd		(uart_txd		)	
);

localparam	BIT_TIME = 'd1000_000_000 / BPS ;	//计算出传输每个bit所需要的时间

initial begin	
	sys_clk <=1'b0;	
	sys_rst_n <=1'b0;		
	uart_tx_en <=1'b0;
	uart_tx_data <=8'd0;				
	#80 										//系统开始工作
		sys_rst_n <=1'b1;
	#3000
		uart_tx_en <=1'b1;	
		uart_tx_data <= ({$random} % 256);		//发送8位随机数据
		#20	uart_tx_en <=1'b0;	
	#(BIT_TIME * 10)							//发送1个BYTE需要10个bit
	#2000										//多延迟一些为了两次发送的仿真波形看起来比较清楚
		uart_tx_en <=1'b1;	
		uart_tx_data <= ({$random} % 256);		//发送8位随机数据
		#20	uart_tx_en <=1'b0;		
end

always #10 sys_clk=~sys_clk;					//时钟20ns,50M

endmodule 

3.5、仿真结果分析

        仿真结果如下图(注释很详细):

串口(UART)的FPGA实现_第7张图片

3.6、上板验证

        至此已经顺利完成了发送模块的仿真验证,接下来使用一块Cyclone IV E的开发板上板验证,这里提供两种验证方法。

3.6.1、方法1

        编写一个发送模块验证模块,该模块调用发送模块,并按一定间隔(默认1s,可设置)拉高发送使能信号和生成发送数据,发送数据从0x01开始累加1,直到0xFF(溢出到0x00)。同时在电脑上使用串口调试软件接收发送过来的数据。根据接收到数据判断串口发送模块是否能成功工作。

        发送模块验证模块代码如下:

//发送模块测试模块--方法1
module uart_tx_test
(
//系统接口
	input 			sys_clk			,
	input 			sys_rst_n		,	
//UART发送线	
	output  		uart_txd						//UART发送线
);	
	
parameter		BPS 	= 'd230400		;			//波特率
parameter		CLK_FRE = 'd50_000_000	;			//系统频率50M	

reg		[31:0]	cnt_time; 
reg				uart_tx_en;							//发送使能,当其为高电平时,代表此时需要发送数据		
reg		[7:0] 	uart_tx_data;						//需要通过UART发送的数据,在uart_tx_en为高电平时有效

//1s计数模块,每隔1s发送一个数据和拉高发送使能信号一次
always @(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		cnt_time <= 'd0;
		uart_tx_en <= 1'd0;
		uart_tx_data <= 8'd0;
	end
	else if(cnt_time == (50_000_000 - 1'b1))begin
		cnt_time <= 'd0;
		uart_tx_en <= 1'd1;							//拉高发送使能
		uart_tx_data <= uart_tx_data + 1'd1;		//发送数据累加1
	end
	else begin
		cnt_time <= cnt_time + 1'd1;
		uart_tx_en <= 1'd0;
		uart_tx_data <= uart_tx_data; 
	end
end 

//例化发送模块
uart_tx
#(
	.BPS			(BPS			),
	.CLK_FRE		(CLK_FRE		)
)	
u_uart_tx
(	
	.sys_clk		(sys_clk		),
	.sys_rst_n		(sys_rst_n		),
	.uart_tx_en		(uart_tx_en		),
	.uart_tx_data	(uart_tx_data	),			
	.uart_txd		(uart_txd		)
);	

endmodule

        串口调试软件结果如下:说明我们的发送模块工作正常。

串口(UART)的FPGA实现_第8张图片

        当然玩FPGA一定要看波形,所以我们还是用Signal TapII抓一下波形。

        波形如下图:可以看到此时发送的数据是0x69(0110 1001),在串口发送数据线上分别传输的数据为 start--1--0--0--1--0--1--1--0--stop,根据低位在前的原则,与发送数据一致。

串口(UART)的FPGA实现_第9张图片

 3.6.2、方法2

        首先生成一个IP核--ISSP(In-System Sources and Probes),这个IP核可以提供一个输出用来在线输出,相当于一个简单的信号发生器--Source,此外还可以提供探针Probes来在线监控信号的输出。在本次设计中,我们使用输出功能Source,用来在线调试生成希望串口发送的数据。ISSP调用如下: 

串口(UART)的FPGA实现_第10张图片

        编写一个发送模块验证模块,该模块调用发送模块(并按一定间隔(默认1s,可设置)拉高发送使能信号),ISSP IP核。同时在电脑上使用串口调试软件接收发送过来的数据。根据接收到数据判断串口发送模块是否能成功工作。

        发送模块验证模块uart_tx_test代码如下:

//发送模块测试模块--方法2
module uart_tx_test
(
//系统接口
	input 			sys_clk			,
	input 			sys_rst_n		,	
//UART发送线	
	output  		uart_txd						//UART发送线
);	
	
parameter		BPS 	= 'd230400		;			//波特率
parameter		CLK_FRE = 'd50_000_000	;			//系统频率50M	

reg		[31:0]	cnt_time; 
reg				uart_tx_en;							//发送使能,当其为高电平时,代表此时需要发送数据
		
wire	[7:0] 	uart_tx_data;						//需要通过UART发送的数据,在uart_tx_en为高电平时有效

//1s计数模块,每隔1s发送一个数据
always @(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		cnt_time <= 'd0;
		uart_tx_en <= 1'd0;
	end
	else if(cnt_time == (50_000_000 - 1'b1))begin
		cnt_time <= 'd0;
		uart_tx_en <= 1'd1;
	end
	else begin
		cnt_time <= cnt_time + 1'd1;
		uart_tx_en <= 1'd0;
	end
end 

//例化发送模块
uart_tx
#(
	.BPS			(BPS			),
	.CLK_FRE		(CLK_FRE		)
)	
u_uart_tx
(	
	.sys_clk		(sys_clk		),
	.sys_rst_n		(sys_rst_n		),
	.uart_tx_en		(uart_tx_en		),
	.uart_tx_data	(uart_tx_data	),			//发送数据来自ISSP IP核
	.uart_txd		(uart_txd		)
);	
//调用ISSP作为激励输入
issp_uart_tx	issp_uart_tx_inst
(
	.probe			(				),
	.source     	(uart_tx_data	)			//生成发送数据
);
endmodule

        下载程序后,在Quartus II中打开In-System Sources and Probes Editor,如下两图

串口(UART)的FPGA实现_第11张图片

串口(UART)的FPGA实现_第12张图片

        在上图的蓝色框框内更改我们希望发送的数据,观察串口调试软件接收到的数据。我这里分别输入数据0x55--0xAA--0x88--0x46(分别持续一段时间),理论上我会在串口调试软件接收到若干个0x55、若干个0xaa、若干个0x88和若干个0x46。 

        串口调试软件结果如下:说明我们的发送模块工作正常。

串口(UART)的FPGA实现_第13张图片

        当然玩FPGA一定要看波形,所以我们还是用Signal TapII抓一下发送数据为0x55的波形。

        波形如下图:这个波形与我们的仿真波形是一致的,同时在发送数据线txd上其发送的内容也与0x55一致。

串口(UART)的FPGA实现_第14张图片

4、串口接收模块

4.1、接口定义与整体设计

        接收模块整体框图、输入输出信号如下所示:

串口(UART)的FPGA实现_第15张图片

        其中信号描述如下:

串口(UART)的FPGA实现_第16张图片

        需要说明的是,uart_rx_data为接收的一个字节的数据,uart_rx_done为接收完成标志位,当其拉高,则代表此时接收到的串口数据uart_rx_data有效。 

4.2、设计思路

  • 该模块支持任意波特率(理论上)的接收,但需要在使用该模块时使用参数将其例化,数据位8位,起始位和停止位各1位,无奇偶校验
  • 串口的传输是以起始位开始的,而起始位是将数据线拉低 ,所以我们需要捕捉数据线的下降沿,将接收数据线打拍两次,捕捉其下降沿。当捕捉到接收数据线的下降沿,拉高接收标志信号,标志模块进入接收过程;当接收完10个bit后,拉低接收标志信号,标志接收过程结束
  • 假设波特率为9600,则传输一个bit的时间为1s/9600,一个数据的传输共10bit(数据位8位,起始位和停止位各1位),则共需要1s/960;假设系统时钟为50MHz(参数化以便适应不同的系统频率),则其周期为20ns,那么传输一个bit所需要的系统周期数为(1s/960)/ 20ns ≈ 5208(个)。在接收过程中使用一个计数器计数,计数区间为(0,5208-1),这样的区间一共10个(一个字节需要传输10个bit);此外还需一个计数器对接收的bit数计数(每当上一个计数器计数到5207则表示接收完了一个bit),计数区间(0,9)。
  • 在接收过程,根据计数器的值(接收bit计数器),在每个bit计数器的中间接收数据,将其移位寄存(在电平中间数据最稳定)
    • 若接收bit计数器 = 0,则代表是起始位,不需要接收
    • 若接收bit计数器 = 1,则代表此时接收到数据的最低位LSB(数据的传输总是低位在前,高位在后),将其赋值给寄存数据的最低位;
    • ······
    • 若接收bit计数器 = 8,则代表此时接收到数据的最高位MSB,将其赋值给寄存数据的最高位;
    • 若接收bit计数器 = 9,则代表是停止位,不需要接收

4.3、Verilg代码

        根据上述设计思路与模块接口可编写代码如下:

//串口接收模块
module uart_rx
#(
	parameter 			BPS		= 'd9_600		,	//发送波特率
	parameter 			CLK_FRE	= 'd50_000_000		//输入时钟频率
)	
(	
//系统接口
	input 				sys_clk			,			//50M系统时钟
	input 				sys_rst_n		,			//系统复位
//UART接收线	
	input 				uart_rxd		,			//接收数据线
//用户接口	
	output reg 			uart_rx_done	,			//数据接收完成标志,当其为高电平时,代表接收数据有效
	output reg [7:0]	uart_rx_data				//接收到的数据,在uart_rx_done为高电平时有效
);

//根据波特率计算传输每个bit需要多个系统时钟
localparam	BPS_CNT = CLK_FRE / BPS;	

//reg define
reg 			uart_rx_d0		;					//寄存1拍
reg 			uart_rx_d1		;					//寄存2拍
reg [15:0]		clk_cnt			;					//计数器,用于计数发送一个bit数据所需要的时钟数
reg [3:0]  		bit_cnt			;					//bit计数器,标志当前发送了多少个bit
reg 			rx_en			;					//接收标志信号,拉高代表接收过程正在进行
reg [7:0]		uart_rx_data_reg;					//接收数据寄存
//wire define				
wire 			neg_uart_rxd	;					//接收数据线的下降沿

//捕获数据线的下降沿,用来标志数据传输开始
assign	neg_uart_rxd = uart_rx_d1 & (~uart_rx_d0);
 
//将数据线打两拍,作用1:同步不同时钟域信号,防止亚稳态;作用2:用以捕获下降沿
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		uart_rx_d0 <= 1'b0;
		uart_rx_d1 <= 1'b0;
	end
	else begin
		uart_rx_d0 <= uart_rxd;
		uart_rx_d1 <= uart_rx_d0;
	end		
end
//捕获到数据下降沿(起始位0)后,拉高传输开始标志位,并在第9个数据(终止位)的传输过程正中(数据比较稳定)再将传输开始标志位拉低,标志传输结束
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)
		rx_en <= 1'b0;
	else begin 
		if(neg_uart_rxd )								
			rx_en <= 1'b1;
		//接收完第9个数据(终止位)将传输开始标志位拉低,标志传输结束
		//假如不在停止位的中间结束接收过程--当第二次接收的起始位紧跟第一次接收的停止位,那么就会造成第一次接收无法结束,状态寄存器无法复位
		else if((bit_cnt == 4'd9) && (clk_cnt == BPS_CNT >> 1'b1))
			rx_en <= 1'b0;
		else 
			rx_en <= rx_en;			
	end
end
//时钟每计数一个BPS_CNT(传输一位数据所需要的时钟个数),即将数据计数器加1,并清零时钟计数器
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		bit_cnt <= 4'd0;
		clk_cnt <= 16'd0;
	end
	else if(rx_en)begin					            			//在接收状态
		if(clk_cnt < BPS_CNT - 1'b1)begin           			//一个bit数据没有接收完
			clk_cnt <= clk_cnt + 1'b1;              			//时钟计数器+1
			bit_cnt <= bit_cnt;                     			//bit计数器不变
		end                                         			
		else begin                                  			//一个bit数据接收完了	
			clk_cnt <= 16'd0;                       			//清空时钟计数器,重新开始计时
			bit_cnt <= bit_cnt + 1'b1;              			//bit计数器+1,表示接收完了一个bit的数据
		end                                         			
	end                                             			
		else begin                                  			//不在接收状态
			bit_cnt <= 4'd0;                        			//清零
			clk_cnt <= 16'd0;                       			//清零
		end		
end
//在每个数据的传输过程正中(数据比较稳定)将数据线上的数据赋值给数据寄存器
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)
		uart_rx_data_reg <= 8'd0;                            	//复位无接收数据
	else if(rx_en)                                           	//处于接收状态
		if(clk_cnt == BPS_CNT >> 1'b1) begin                 	//传输过程正中(数据比较稳定)
			case(bit_cnt)			                         	//根据位数决定接收的内容是什么
				4'd1:uart_rx_data_reg[0] <= uart_rxd;        	//LSB
				4'd2:uart_rx_data_reg[1] <= uart_rxd;        	//
				4'd3:uart_rx_data_reg[2] <= uart_rxd;        	//
				4'd4:uart_rx_data_reg[3] <= uart_rxd;        	//
				4'd5:uart_rx_data_reg[4] <= uart_rxd;        	//
				4'd6:uart_rx_data_reg[5] <= uart_rxd;        	//
				4'd7:uart_rx_data_reg[6] <= uart_rxd;        	//
				4'd8:uart_rx_data_reg[7] <= uart_rxd;        	//MSB
				default:;                                    	//1和9分别是起始位和终止位,不需要接收
			endcase                                          	
		end                                                  	
		else                                                 	//数据不一定稳定就不接收
			uart_rx_data_reg <= uart_rx_data_reg;            
	else
		uart_rx_data_reg <= 8'd0;								//不处于接收状态
end	
//当数据传输到终止位时,拉高传输完成标志位,并将数据输出
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		uart_rx_done <= 1'b0;
		uart_rx_data <= 8'd0;
	end	
	//结束接收后,将接收到的数据输出
	else if(bit_cnt == 4'd9 && (clk_cnt == BPS_CNT >> 1'd1))begin	
		uart_rx_done <= 1'b1;									//仅仅拉高一个时钟周期
		uart_rx_data <= uart_rx_data_reg;					
	end							
	else begin					
		uart_rx_done <= 1'b0;									//仅仅拉高一个时钟周期
		uart_rx_data <= uart_rx_data;
	end
end
endmodule 

4.4、Testbench

        仿真模块的代码如下:

  • 设定波特率230400(这样的目的是为了更方便的观察发送使能信号uart_tx_en)
  • 定义一个任务task,该任务将输入使用波特率230400一个bit一个bit的输出,模拟上位机发送数据给FPGA
  • 3000ns后,发送第1个随机数据
  • 发送完了第1个随机数据后发送第2个随机数据
`timescale 1ns/1ns	//定义时间刻度
//模块、接口定义
module tb_uart_rx();

reg 			sys_clk			;			
reg 			sys_rst_n		;			
reg 			uart_rxd		;

wire 			uart_rx_done	;		
wire	[7:0]	uart_rx_data	;

parameter		BPS 	= 'd230400			;	//波特率
parameter		CLK_FRE = 'd50_000_000		;	//系统频率50M
localparam		CNT = 1000_000_000 / BPS	;	//计算出传输每个bit所需要的时间,单位:ns

//例化被测试的接收模块
uart_rx_test 
#(
	.BPS			(BPS			),		//波特率9600
	.CLK_FRE		(CLK_FRE		)		//时钟频率50M	
)
uart_rx_test_inst(
	.sys_clk		(sys_clk		),			
	.sys_rst_n		(sys_rst_n		),			
	.uart_rxd		(uart_rxd		),			
	.uart_rx_done	(uart_rx_done	),		
	.uart_rx_data	(uart_rx_data	)	
);

initial begin	
	//初始时刻定义
	sys_clk	=1'b0;	
	sys_rst_n <=1'b0;		
	uart_rxd <=1'b1;
	#20 //系统开始工作
	sys_rst_n <=1'b1;
	#3000
	rx_byte({$random} % 256);		//生成8位随机数
	rx_byte({$random} % 256);
end


//定义任务,每次发送的数据10 位(起始位1+数据位8+停止位1)
task rx_byte(
	input [7:0] data
);
	integer i; //定义一个常量
	//用 for 循环产生一帧数据,for 括号中最后执行的内容只能写 i=i+1
	for(i=0; i<10; i=i+1) begin
		case(i)
		0: uart_rxd <= 1'b0;		//起始位
		1: uart_rxd <= data[0];		//LSB
		2: uart_rxd <= data[1];
		3: uart_rxd <= data[2];
		4: uart_rxd <= data[3];
		5: uart_rxd <= data[4];
		6: uart_rxd <= data[5];
		7: uart_rxd <= data[6];
		8: uart_rxd <= data[7];		//MSB
		9: uart_rxd <= 1'b1;		//停止位
		endcase
		#CNT; 						//每发送 1 位数据延时
	end		
endtask 							//任务结束

always #10 sys_clk <= ~sys_clk;		//时钟20ns,50M

endmodule 

4.5、仿真结果分析

        仿真结果如下图(注释很详细): 

串口(UART)的FPGA实现_第17张图片

4.6、上板验证

        至此已经顺利完成了接收模块的仿真验证,接下来使用一块Cyclone IV E的开发板上板验证。

        首先生成一个IP核--ISSP(In-System Sources and Probes),这个IP核可以提供一个输出用来在线输出,相当于一个简单的信号发生器--Source,此外还可以提供探针Probes来在线监控信号的输出。在本次设计中,我们使用Probes来观察串口接收数据。ISSP调用如下:

串口(UART)的FPGA实现_第18张图片

        编写一个接收模块验证模块,该模块调用接收模块,ISSP IP核。同时在电脑上使用串口调试软件发送数据,根据接收到的数据判断串口接收模块是否能成功工作。

        接收模块验证模块uart_rx_test代码如下:

//接收模块测试模块
module uart_rx_test
(
//系统接口
	input 				sys_clk			,
	input 				sys_rst_n		,	
//UART接收线		
	input				uart_rxd		,			//接收数据线
//用户接口	
	output				uart_rx_done	,			//数据接收完成标志,当其为高电平时,代表接收数据有效
	output	[7:0]		uart_rx_data				//接收到的数据,在uart_rx_done为高电平时有效	
);	
	
parameter		BPS 	= 'd230400		;			//波特率
parameter		CLK_FRE = 'd50_000_000	;			//系统频率50MHZ	

//例化接收模块
uart_rx
#(
	.BPS			(BPS			),
	.CLK_FRE		(CLK_FRE		)
)	
uart_rx_isnt
(	
	.sys_clk		(sys_clk		),			
	.sys_rst_n		(sys_rst_n		),			
	.uart_rxd		(uart_rxd		),			
	.uart_rx_done	(uart_rx_done	),			
	.uart_rx_data	(uart_rx_data	)			
);	
//调用ISSP作为观测
issp_uart_rx	issp_uart_rx_inst
(
	.probe			(uart_rx_data	),			//观测接收数据
	.source     	(				)			
);

endmodule

        下载程序后,在Quartus II中打开In-System Sources and Probes Editor,然后使用串口调试软件发送数据0x55--0xaa--0x88(随机选的3个),观察 In-System Sources and Probes Editor中寄存器的值,分别如下:

串口(UART)的FPGA实现_第19张图片

串口(UART)的FPGA实现_第20张图片

串口(UART)的FPGA实现_第21张图片

        我们接下里用Signal TapII抓一下接收数据为0x55的波形。

        波形如下图:这个波形与我们的仿真波形是一致的,同时在接收数据线txd上其发送的内容也与0x55一致。

 串口(UART)的FPGA实现_第22张图片

5、串口环回

        我们可以编写一个串口环回模块,分别调用发送模块和接收模块。使用上位机发送数据给FPGA的接收模块,接收模块将接收到的数据再通过发送模块发送出来。

        环回模块比较简单,只要例化上面两个模块就可以了,如下:

//串口接收模块
module uart_loop
#(
	parameter 			BPS		= 'd230400		,	//发送波特率
	parameter 			CLK_FRE	= 'd50_000_000		//输入时钟频率
)	
(	
//系统接口
	input 				sys_clk			,			//50M系统时钟
	input 				sys_rst_n		,			//系统复位
//UART	
	input 				uart_rxd		,			//接收数据线
	output  			uart_txd					//UART发送数据线
);
	
//wire define				
wire	[7:0]	data;								//接收到的一个BYTE数据
wire			en;									//接收有效信号,可用作发送的使能信号

//例化发送模块
uart_tx #(
	.BPS			(BPS		),		
	.CLK_FRE		(CLK_FRE	)		
)	
u_uart_tx(	
	.sys_clk		(sys_clk	),		
	.sys_rst_n		(sys_rst_n	),

	.uart_tx_data	(data		),		
	.uart_tx_en		(en			),		
	.uart_txd		(uart_txd	)	
);
//例化接收模块
uart_rx #(
	.BPS			(BPS		),		
	.CLK_FRE		(CLK_FRE	)		
)	
u_uart_rx(	
	.sys_clk		(sys_clk	),			
	.sys_rst_n		(sys_rst_n	),

	.uart_rx_data	(data		),			
	.uart_rx_done	(en			),		
	.uart_rxd		(uart_rxd	)	
);

endmodule 

        testbench依然使用前面设计的task,发送几个随机数据,观察接收的数据是否与发送的一致。

`timescale 1ns/1ns	//定义时间刻度
//模块、接口定义
module tb_uart_loop();

reg 			sys_clk			;			
reg 			sys_rst_n		;			
reg 			uart_rxd		;
			
wire 	 		uart_txd		;

parameter		BPS 	= 'd230400		;			//波特率
parameter		CLK_FRE = 'd50_000_000	;			//系统频率50M
localparam		BIT_TIME = 'd1000_000_000 / BPS ;		//计算出传输每个bit所需要的时间

//例化环回模块
uart_loop #(
	.BPS			(BPS			),				//波特率
	.CLK_FRE		(CLK_FRE		)				//时钟频率50M	
)	
u_uart_loop(	
	.sys_clk		(sys_clk		),			
	.sys_rst_n		(sys_rst_n		),		
	.uart_rxd		(uart_rxd		),	
	.uart_txd		(uart_txd		)	
);

initial begin	
	//初始时刻定义
	sys_clk	=1'b0;	
	sys_rst_n <=1'b0;		
	uart_rxd <=1'b1;
	#20 //系统开始工作
	sys_rst_n <=1'b1;
	#3000
	rx_byte({$random} % 256);		//生成8位随机数
	rx_byte({$random} % 256);
	rx_byte({$random} % 256);
	rx_byte({$random} % 256);
	rx_byte({$random} % 256);
	rx_byte({$random} % 256);
end

//定义任务,每次发送的数据10 位(起始位1+数据位8+停止位1)
task rx_byte(
	input [7:0] data
);
	integer i; //定义一个常量
	//用 for 循环产生一帧数据,for 括号中最后执行的内容只能写 i=i+1
	for(i=0; i<10; i=i+1) begin
		case(i)
		0: uart_rxd <= 1'b0;		//起始位
		1: uart_rxd <= data[0];		//LSB
		2: uart_rxd <= data[1];
		3: uart_rxd <= data[2];
		4: uart_rxd <= data[3];
		5: uart_rxd <= data[4];
		6: uart_rxd <= data[5];
		7: uart_rxd <= data[6];
		8: uart_rxd <= data[7];		//MSB
		9: uart_rxd <= 1'b1;		//停止位
		endcase
		#BIT_TIME;					//每发送 1 位数据延时
	end		
endtask 

always #10 sys_clk=~sys_clk;					//时钟20ns,50M

endmodule 

        仿真结果如下:接收的数据可以成功的发送出来。

串口(UART)的FPGA实现_第23张图片

        用Signal TapII抓一下发送数据为0x55的波形:发送数据与接收一致。

         然后使用串口调试助手,发送一包数据给FPGA,观察接收区接收的数据是否与发送的一致,如下图:

串口(UART)的FPGA实现_第24张图片

6、其他 

版本控制:

        文件:V1.0

                2020-03-10:初版本

                2021-10-30:增加串口理论说明;更改接收模块逻辑;增加在线调试说明

                2021-11-23:修正接收模块代码BUG

        编号:6

        Vivado:无

        Modelsim:Modelsim SE-64 10.4

        Quartus II:Quartus II 13.1 (64-bit)

说明:

  • 后续会推出支持奇偶校验、可选停止位数、可选发送位数的、在线更改波特率的模块
  • 需要整个工程的朋友可以评论区留下邮箱或者给我私信

你可能感兴趣的:(【3】简单的接口与外设,趣味项目,fpga,verilog,串口通信,uart)