LinCoding

【读书笔记】详细解析基于FPGA的串口通信

1
阅读(4521)

【主题】:详细解析基于FPGA的串口通信

【作者】:LinCoding

【时间】:2016.11.28

  串口通信,用的实在太广泛了,原理也很简单,在串口通信中最重要的是波特率的概念,大家不懂的可以百度,然后就是数据的格式,1位起始位,8位数据位,1位校验位(可选),1位停止位。

先看结果(PC端将数据下发至FPGA,FPGA接收到数据后原封不动的将数据上发至PC端):

blob.png

(源码出自CrazyBingo,尊重版权)

  下面直接看程序。

  1、串口接收模块

module uart_receiver
(
	input				clk,		//global clock
	input				rst_n,		//global reset
	
	input				clk_16bps,
	input				rxd,
	
	output	reg	[7:0]	        rxd_data,
	output	reg			rxd_flag
);

 第一部分是输入输出定义,有两点需要注意:

 1、对于串口接收模块,原理上要使用16倍的波特率去采样,因此输入clk_16bps,而这个输入信号来自于——《详细解析基于FPGA的任意分频》这篇文章的divide_clken的输出。

 2、对于一个系统而言是由众多模块组成的,模块与模块之间肯定是要沟通的,那么对于输入模块而言,就就少不了“输入完成标志位信号”,并且,这个标志位信要与输入的数据同步输出,如本例中的rxd_flag,还有之前按键消抖文章中的key_flag,SPI文章中的rxd_flag都是如此,因此,这也要成为一个固定的模式!

//-----------------------------------
//synchronize the input signal
reg	rxd_sync;
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		rxd_sync	<= 1'b1;
	else
		rxd_sync	<= rxd;
end

 第二部分,由于是接收模块,接收的信号是来自其他时钟域的,因此,这里需要打一拍将信号同步到自己的时钟域,当然,打两拍也是可以的!

//-----------------------------------
//receive FSM: encode
localparam	R_IDLE 		= 4'd0;
localparam	R_START		= 4'd1;
localparam	R_SAMPLE	= 4'd2;
localparam	R_STOP		= 4'd3;

//-----------------------------------
//receive FSM
localparam	SMP_TOP 	= 4'd15;
localparam	SMP_CENTER	= 4'd7;

reg	[3:0]	smp_cnt;
reg	[3:0]	rxd_cnt;
reg	[3:0]	rxd_state;
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		begin
			smp_cnt		<= 4'd0;
			rxd_cnt		<= 4'd0;
			rxd_state	<= R_IDLE;
		end
	else
		case ( rxd_state )
			R_IDLE:
				begin
					smp_cnt	<= 4'd0;
					rxd_cnt	<= 4'd0;
					if ( ! rxd_sync )
						rxd_state	<= R_START;
					else
						rxd_state	<= R_IDLE;
				end
			R_START:
				if ( clk_16bps )
					begin
						smp_cnt		<= smp_cnt + 1'b1;
						if ( smp_cnt == SMP_CENTER && rxd_sync )
							begin
								rxd_cnt		<= 4'd0;
								rxd_state	<= R_IDLE;
							end
						else if ( smp_cnt == SMP_TOP )
							begin
								rxd_cnt		<= 4'd1;
								rxd_state	<= R_SAMPLE;
							end
						else
							begin
								rxd_cnt		<= rxd_cnt;
								rxd_state	<= rxd_state;
							end
					end
				else
					begin
						smp_cnt		<= smp_cnt;
						rxd_cnt		<= rxd_cnt;
						rxd_state	<= rxd_state;
					end
			R_SAMPLE:
				if ( clk_16bps )
					begin
						smp_cnt		<= smp_cnt + 1'b1;
						if ( smp_cnt == SMP_TOP )
						    begin
							if ( rxd_cnt < 4'd8 )
							    begin
								rxd_cnt	       <= rxd_cnt + 1'b1;
								rxd_state      <= R_SAMPLE;
        						    end
							else
							    begin
								rxd_cnt		<= 4'd9;
								rxd_state	<= R_STOP;
							    end
						    end
						else
							begin
								rxd_cnt		<= rxd_cnt;
								rxd_state	<= rxd_state;
							end
					end
				else
					begin
						smp_cnt		<= smp_cnt;
						rxd_cnt		<= rxd_cnt;
						rxd_state	<= rxd_state;
					end
			R_STOP:
				if ( clk_16bps )
					begin
						smp_cnt		<= smp_cnt + 1'b1;
						if ( smp_cnt == SMP_TOP )
							begin
								rxd_cnt		<= 4'd0;
								rxd_state	<= R_IDLE;
							end
						else
							begin
								rxd_cnt		<= rxd_cnt;
								rxd_state	<= rxd_state;
							end
					end
				else
					begin
						smp_cnt		<= smp_cnt;
						rxd_cnt		<= rxd_cnt;
						rxd_state	<= rxd_state;
					end
			default:
				begin
					smp_cnt		<= 4'd0;
					rxd_cnt		<= 4'd0;
					rxd_state	<= R_IDLE;
				end
		endcase
end

   第三部分就是长长的状态机了,真的是太长了,不过基本都是重复的内容,有以下几点需要注意:

  1、对于串口接收模块,我们为了使接收到的数据最大程度的稳定,使用了16倍波特率去采样,也就是在每个数据位上,有16个采样点,这样的话,当然在最中间的采样点的数据原则上是最稳定的,事实也确实如此。因此需要一个smp_cnt信号去计数当前是第几个采样点,并且当采样到16个点后使得rxd_cnt加1以采样下一位数据。

 2、使用rxd_cnt信号 来计数获得当前采样的第几位数据。需要注意的是本always块只涉及rxd_cnt的变迁,而在另一个always中会根据rxd_cnt的情况来取值各个位上的数据。

 3、由于串口传输的特性,起始位变为低电平视为传输的开始,因此也就意味着,起始位其实是个传输开始的使能信号,所以要使用状态机的IDLE态来始终监测起始位是否变化。

reg	[7:0]	rxd_data_r;
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		rxd_data_r	<= 8'd0;
	else if ( rxd_state == R_SAMPLE && clk_16bps && smp_cnt == SMP_CENTER )
		case ( rxd_cnt )
			4'd1	:	begin rxd_data_r[0] <= rxd_sync; end
			4'd2	:	begin rxd_data_r[1] <= rxd_sync; end
			4'd3	:	begin rxd_data_r[2] <= rxd_sync; end
			4'd4	:	begin rxd_data_r[3] <= rxd_sync; end
			4'd5	:	begin rxd_data_r[4] <= rxd_sync; end
			4'd6	:	begin rxd_data_r[5] <= rxd_sync; end
			4'd7	:	begin rxd_data_r[6] <= rxd_sync; end
			4'd8	:	begin rxd_data_r[7] <= rxd_sync; end
			default	:	begin rxd_data_r	<= 8'd0;	 end
		endcase
	else if ( rxd_state == R_STOP )
		rxd_data_r	<= rxd_data_r;
	else
		rxd_data_r	<= rxd_data_r;
end

 第四部分就是取值了,根据rxd_cnt的值来取值不同位上的数据。

always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		begin
			rxd_data	<= 8'd0;
			rxd_flag	<= 1'b0;
		end
	else if ( rxd_state == R_STOP && clk_16bps && smp_cnt == SMP_TOP )
		begin
			rxd_data	<= rxd_data_r;
			rxd_flag	<= 1'b1;
		end
	else
		begin
			rxd_data	<= rxd_data;
			rxd_flag	<= 1'b0;
		end
end

   第五部分就是同步输出rxd_data和rxd_flag信号了。而为了同步输出,同样又采用了一级D触发器来给rxd_data打了一拍,使其与rxd_flag同步输出。

 2、串口发送模块

module uart_transfer
(
	input				clk,		//global clock
	input				rst_n,		//global reset
	
	input				clk_16bps,
	input				txd_en,
	input		[7:0]	        txd_data,
	
	output	reg 		        txd,
	output	reg			txd_flag
);

 第一部分是输入输出定义, 既然是发送模块,当然需要发送使能信号了,其次,还需要发送完成标志位信号。当然了,既然有了发送使能信号,状态机是不可避免的了。clk_16bps与串口接收模块同理。

//-----------------------------------
//receive FSM: encode
localparam	T_IDLE 		= 4'd0;
localparam	T_SEND		= 4'd1;

//-----------------------------------
//receive FSM
localparam	SMP_TOP 	= 4'd15;
localparam	SMP_CENTER	= 4'd7;

reg	[3:0]	smp_cnt;
reg	[3:0]	txd_cnt;
reg	[3:0]	txd_state;
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		begin
			smp_cnt		<= 4'd0;
			txd_cnt		<= 4'd0;
			txd_state	<= T_IDLE;
		end
	else
		case ( txd_state )
			T_IDLE:
				begin
					smp_cnt	<= 4'd0;
					txd_cnt	<= 4'd0;
					if ( txd_en )
						txd_state	<= T_SEND;
					else
						txd_state	<= T_IDLE;
				end
			T_SEND:
				if ( clk_16bps )
					begin
						smp_cnt		<= smp_cnt + 1'b1;
						if ( smp_cnt == SMP_TOP )
						    begin
						        if ( txd_cnt < 4'd9 )
							    begin
							        txd_cnt	     <= txd_cnt + 1'b1;
								txd_state    <= T_SEND;
							    end
							else
    							    begin
    								txd_cnt		<= 4'd0;
								txd_state	<= T_IDLE;
						    end
					end
						else
							begin
								txd_cnt		<= txd_cnt;
								txd_state	<= txd_state;
							end
					end
				else
					begin
						smp_cnt		<= smp_cnt;
						txd_cnt		<= txd_cnt;
						txd_state	<= txd_state;
					end
			default:
				begin
					smp_cnt		<= 4'd0;
					txd_cnt		<= 4'd0;
					txd_state	<= T_IDLE;
				end
		endcase
end

 第二部分就是长长的发送状态机了,相比串口接收模块相对简单,而与之不同的是,由于是发送,我们只需在数据的第一个采样点将数据发送出去,在采样点到达16个时,换下一位数据进行发送就可以了。

always @ ( * )
begin
	if ( txd_state == T_SEND )
		case ( txd_cnt )
			4'd0	:	begin txd = 1'b0; 	 end
			4'd1	:	begin txd = txd_data[0]; end
			4'd2	:	begin txd = txd_data[1]; end
			4'd3	:	begin txd = txd_data[2]; end
			4'd4	:	begin txd = txd_data[3]; end
			4'd5	:	begin txd = txd_data[4]; end
			4'd6	:	begin txd = txd_data[5]; end
			4'd7	:	begin txd = txd_data[6]; end
			4'd8	:	begin txd = txd_data[7]; end
			4'd9	:	begin txd = 1'b1; 	 end
			default	:	begin txd = 1'b1; 	 end
		endcase
	else
		txd = 1'b1;
end

   第三部分就是根据txd_cnt的计数来发送数据了,注意的是本模块为组合逻辑,为的就是使数据对齐!

blob.png

图1

blob.png

图2

  图2是图1的放大版本,可见,在时钟边沿是对齐的。

  如果改为时序逻辑,如下程序:

always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		txd		<= 1'b1;
	else if ( txd_state == T_SEND )
		case ( txd_cnt )
			4'd0	:	begin txd = 1'b0; 	 end
			4'd1	:	begin txd = txd_data[0]; end
			4'd2	:	begin txd = txd_data[1]; end
			4'd3	:	begin txd = txd_data[2]; end
			4'd4	:	begin txd = txd_data[3]; end
			4'd5	:	begin txd = txd_data[4]; end
			4'd6	:	begin txd = txd_data[5]; end
			4'd7	:	begin txd = txd_data[6]; end
			4'd8	:	begin txd = txd_data[7]; end
			4'd9	:	begin txd = 1'b1; 	 end
			default	:	begin txd = 1'b1; 	 end
		endcase
	else
		txd = 1'b1;
end

   则如下图所示:

blob.png

  txd比txd_cnt晚了一个clk,当然晚了一个clk是无所谓的,但是为了时序的完美,还是用组合逻辑吧!

always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		txd_flag	<= 1'b0;
	else if ( txd_state == T_SEND && clk_16bps && smp_cnt == SMP_TOP && txd_cnt == 4'd9 )
		txd_flag	<= 1'b1;
	else
		txd_flag	<= 1'b0;
end

  最后一个部分就是输出发送完成标志位信号。

  

  这样一个基于FPGA的串口通信就完成了,在顶层模块中我们可以将接收到的数据直接交给发送模块,通过PC端的串口调试助手下发数据,然后FPGA会原封不动的将数据再发回来完成板级验证。


总结:

 1、对于接收类的模块,接收完成后需要有接收完成标志位信号,并且要与所接收的数据同步输出。

  2、对于发送类的模块,要有发送使能信号和发送完成标志位信号,并且使用状态机的IDLE态来监测发送使能信号的变化。