【FPGA】初学FPGA——(基于verilog)串口发送和接收模块实例详解

【FPGA】初学FPGA——(基于verilog)串口发送和接收模块实例详解

  • 串口原理
    • 波特率计算
  • 串口发送
  • 串口接收

串口原理

首先我们得直到串口是怎样进行通信的。
【FPGA】初学FPGA——(基于verilog)串口发送和接收模块实例详解_第1张图片
我们可以看到,串口通讯的数据格式是由一位起始位、七个数据位(其中最后一位数据位可以作为检验位来使用。)、一位停止位,在空闲时刻为高电平,当我们使用串口发送时,就可以按照这种时序进行发送,接收时也要按照这种时序进行接收。

波特率计算

什么是波特率?

每秒钟发送的数据个数。

概念很简单,我们需要怎样去计算呢?常见的波特率有 9600192003840057600115200

波特率计算:
9600:1_000_000_000 / 9600;
19200:1_000_000_000 / 19200;
38400:1_000_000_000 / 38400;
57600:1_000_000_000 / 57600;
115200:1_000_000_000 / 115200;

这儿的 1_000_000_000 是代表 1s 因为我的开发板是以 ns 为时间单位的,所以我的波特率计算也是以 ns 为时间单位。

串口发送

发送思路原理:我们可以通过定义一个计数器,来记录每次发送所需要的的时间,然后用一个状态机来记录发送的状态。
module uart_tx_bottom(
	input Clk,	//定义输入时钟信号。
	input Reset_n,	//定义复位信号。
	input [7:0]Data,	//定义需要发送的八位数据。
	input Send_en,	//定义发送使能信号,此信号为1时才可以发送数据。	
	output reg uart_tx,	//定义发送信号,reg型是因为需要在always块中给它赋值。
	output reg Tx_done	//定义发送完成标志信号,以便下次发送数据。
);
	parameter Band_Set = 433;	//这儿是利用时钟计数发送一位的时间,1_000_000_000 / 115200 / 20 - 1// 其中 / 20 是因为我的开发板时钟周期为20ns, - 1 是因为计数器从0开始计数,原本算出需要计数434次,只需要加到433时就是434次了。
	reg [3:0]bps_cnt;	//数据发送状态,因为发送有起始位,数据位,结束位,用这个来记录数据发送到哪个位置了。
	reg [17:0]div_cnt;	//一位数据位发送时间计数器,记录发送数据的时间,用于更新下一段数据。
	wire bps_clk;	//每一位发送数据前后的标志位,具体作用后面会讲到。
	
	always @(posedge Clk or negedge Reset_n)	begin	//定义一个计数器的always块。
		if (!Reset_n)
			div_cnt <= 0;	//若复位将计数器归0.
		else if (Send_en)	begin	//如果有使能才可以进行自加。
			if (Band_Set <= div_cnt)	//当中间<=写法错误时,这种写法会让编辑器自动报错,可以减少因为没注意而引发的功能错误,而且使用<=可以对未知的情况进行判断,增加程序的健壮性。
				div_cnt <= 0;	//当计数器记满时,计数器归0.
			else
				div_cnt <= div_cnt + 1'b1;	//计数器未记满,每个时钟上升沿自加1.
		end
		else
			div_cnt <= 0;	//没有使能信号,使计数器归0
	end
		
	assign bps_clk = (div_cnt == 1);	//当每次计数器记到 1 的时候就让数据开始发送,因为如果div_cnt == Band_Set,在每次发送时还需要等待一个数据位的时间数据才会发送,而这样定义后只需要等待1个时钟周期数据就会发送了。因为bps_clk是wire类型的,所以用assign语句赋值,而不是用always语句赋值。
	
	always @(posedge Clk or negedge Reset_n)	begin	//定义发送状态块,记录发送的状态。
		if (!Reset_n)
			bps_cnt <= 0;	//	若是复位,状态为0else if (Send_en)	begin	//发送使能,状态才会改变。
			if (bps_clk)	begin	//当有发送的标志时,状态才自增一次。
				if (bps_cnt == 11)
					bps_cnt <= 0;	//当状态计数到第11个状态,状态归0else
					bps_cnt <= bps_cnt + 1'b1;	//状态自增1。
			end
		end
		else
			bps_cnt <= 0;	//发送不使能,状态归0end
	
	always @(posedge Clk or negedge Reset_n)	begin	//串口发送块。
		if (!Reset_n)
			uart_tx <= 1;	//当复位时,数据线发送高电平,因为空闲状态时数据线为高电平。
		else begin
			case(bps_cnt)	//开始发送数据,以数据状态来决定发送的是哪个数据。
				1:uart_tx <= 0;	//发送起始位,为什么不是0状态时刻发送0,因为在复位或者空闲时状态为0,而此时需要·uart_tx为高电平,所以在0状态时刻不能为02:uart_tx <= Data[0];	//发送数据低位,数据从低位发送到高位,此下依次发送数据。
				3:uart_tx <= Data[1];
				4:uart_tx <= Data[2];
				5:uart_tx <= Data[3];
				6:uart_tx <= Data[4];
				7:uart_tx <= Data[5];
				8:uart_tx <= Data[6];
				9:uart_tx <= Data[7];	//一般来说,不用校验位,直接发送数据最后一位。
				default:uart_tx <= 1;	//数据位发送完成之后,需要发送停止位,我们可以发现,空闲和停止位都是高电平,所以用default来将这些状态综合。
				//细心的可以发现,我们状态定义了11个,这儿9个就结束了,为什么需要定义11个呢?因为在最后一个停止位发送为第10个状态,而停止位需要保留一个数据周期,所以在第10个状态发送停止位后,需要再延后一个状态,来保证停止位的时间,所以是11个状态。
			endcase
		end
	end
	
	always @(posedge Clk or negedge Reset_n)	begin	//定义发送完成标志块。
		if (!Reset_n)
			Tx_done <= 0;	//复位时,发送完成标志位为0.
		else if (10 <= bps_cnt && bps_clk)	//当状态为第10或者11个状态时且发送一个数据的标志位置位时才使总发送标志位置1。
			Tx_done <= 1;
		else
			Tx_done <= 0;	//其余时刻都将状态标志位置0end
endmodule

串口接收

接收思路原理:因为数据传输是不稳定的,会有波动,所以我们可以将每一个数据接收时间分为 16 份,检测其中中间的 7 份,7 份当中最多的状态就是此时的数据状态。
module uart_rx_bottom(
	input Clk,	//定义输入时钟信号。
	input Reset_n,	//定义复位。
	input uart_rx,	//定义接收口。
	output reg [7:0]Data,	//定义数据接收位置,将接收的数据输出。
	output reg Rx_Done	//定义接收完成标志信号。
);
	parameter Bps_DR = 1_000_000_000 / 115200 / 16 / 20 - 1;	//时钟计数最大值,先算出波特率,时钟周期20,每一份再分16份, - 1 的作用上面已经说了。
	reg RX_EN;	//定义接收使能信号。
	reg [1:0]uart_rx_state;	//定义一个两位的寄存器来存储上个时钟和此刻时钟的状态,用于判断上升沿和下降沿。
	reg [2:0]sta_bit;	//定义接收起始信号的中间七份的寄存器。
	reg [2:0]sto_bit;	//定义接收结束信号的中间七份的寄存器。
	reg [2:0]r_data[7:0];	//定义接收数据信号的中间七份的寄存器,前面[2:0]代表每个数据的宽度,[7:0]代表定义了七个数据。
	reg [7:0]bps_cnt;	//定义份数计数器,因为每个信号被人为分为16分,起始数据和结束一共有10位,所以bps_cnt需要计数160次。
	reg [8:0]div_cnt;	//定义每一份时间计数器,记录每一份所需要的时间。
	wire nedge_uart_rx;	//定义一个寄存器来记录下降沿,因为开始信号是由高电平突变成低电平,是一个下降沿信号。
	wire bps_clk_16x;	//定义每一份结束的标志信号。
	
	always @(posedge Clk)	begin	//定义采集前一信号和现在信号always块。
		uart_rx_state[0] <= uart_rx;	//将uart_rx_state低位采集此时输入数据状态
		uart_rx_state[1] <= uart_rx_state[0];	//将uart_rx_state高位采集此时uart_rx_state低位的数据状态,因为使用 <= 赋值,所以此时高位采集的是上个时钟沿的数据接收状态。
	end
	
	assign nedge_uart_rx = (uart_rx_state== 2'b10);	//将状态赋值进nedge_uart_rx,若是采集到下降沿信号则值为1。
		
	assign bps_clk_16x = (div_cnt == Bps_DR / 2);	//当计数器计数到一半时,将接收完成状态置1提醒进入下一个状态。具体作用在后面会提到。
	
	always @(posedge Clk or negedge Reset_n)	begin	//接收使能块。
		if (!Reset_n)
			RX_EN <= 0;	//复位时将接收使能关闭。
		else if (nedge_uart_rx)
			RX_EN <= 1;	//当就收到下降沿就将使能置位。
		else if (Rx_Done || sta_bit >= 4)
			RX_EN <= 0;	//只有当接收完成标志置1或者是起始位接收错误,后面会解释,只有当sta_bit<4时才认为起始位接收到的是一个低电平信号,否则认为只是接收口受到干扰而产生的低电平波动。
	end
	
	always @(posedge Clk or negedge Reset_n)	begin	//定义一个一份计数器块。
		if (!Reset_n)
			div_cnt <= 0;	//复位时,计数器归0else if (RX_EN)	begin	//只有接收使能时才进行数据接收。
			if (Bps_DR <= div_cnt)
				div_cnt <= 0;	//当计数器记满后归0else
				div_cnt <= div_cnt + 1'b1;	//否则计数器自增1。
		end
		else
			div_cnt <= 0;	//接收使能关闭时将计数器归0end
	
	always @(posedge Clk or negedge Reset_n)	begin	//状态计数器块,记录160份中到了哪一份的位置。
		if (!Reset_n)
			bps_cnt <= 0;	//复位时将状态归0else if (RX_EN)	begin	//接收使能时才能自增状态计数器。
			if (bps_clk_16x)	begin	//当进入下一个状态标志位置位时,状态计数器才能自增1if (bps_cnt == 160)
					bps_cnt <= 0;	//当计数器计数到160将状态归0,这儿你们估计会疑惑为什么不是159吧,因为根据实际仿真,我发现,如果定义成159,在最后停止位会少一位。
				else
					bps_cnt <= bps_cnt + 1'b1;	//其他状态自增1。
			end
		end
		else
			bps_cnt <= 0;	//在接收未使能状态下,将状态归0end
	
	always @(posedge Clk or negedge Reset_n)	begin	//定义一个数据接受块。
		if (!Reset_n)	begin	//复位时将所有数据接收清0。
			sta_bit <= 0;
			sto_bit <= 0;
			r_data[0] <= 0;
			r_data[1] <= 0;
			r_data[2] <= 0;
			r_data[3] <= 0;
			r_data[4] <= 0;
			r_data[5] <= 0;
			r_data[6] <= 0;
			r_data[7] <= 0;
		end
		else	begin
			case (bps_cnt)	//判断此时在哪个状态。
				0:begin	//0状态时将所有数据清空。
					sta_bit <= 0;
					sto_bit <= 0;
					r_data[0] <= 0;
					r_data[1] <= 0;
					r_data[2] <= 0;
					r_data[3] <= 0;
					r_data[4] <= 0;
					r_data[5] <= 0;
					r_data[6] <= 0;
					r_data[7] <= 0;
				end
				5,6,7,8,9,10,11:sta_bit <= sta_bit + uart_rx;	//起始位接收时,读取中间7位数据,后面的依次接收数据位和结束位的中间7份。
				21,22,23,24,25,26,27:r_data[0] <= r_data[0] + uart_rx;
				37,38,39,40,41,42,43:r_data[1] <= r_data[1] + uart_rx;
				53,54,55,56,57,58,59:r_data[2] <= r_data[2] + uart_rx;
				69,70,71,72,73,74,75:r_data[3] <= r_data[3] + uart_rx;
				85,86,87,88,89,90,91:r_data[4] <= r_data[4] + uart_rx;
				101,102,103,104,105,106,107:r_data[5] <= r_data[5] + uart_rx;
				117,118,119,120,121,122,123:r_data[6] <= r_data[6] + uart_rx;
				133,134,135,136,137,138,139:r_data[7] <= r_data[7] + uart_rx;
				149,150,151,152,153,154,155:sto_bit <= sto_bit + uart_rx;
				default:;	//其他情况没有改动,我们不需要作出什么操作。
			endcase
		end
	end
	
	always @(posedge Clk or negedge Reset_n)	begin	//数据读出块。
		if (!Reset_n)
			Data <= 0;	//复位时数据位0else if (bps_clk_16x && (bps_cnt == 160))	begin	//当所有数据全部取出,也就是当状态寄存器计满,并且数据发送置位信号置位时才开始数据读出。
			Data[0] <= r_data[0][2];	//为什么要这样做?因为你会发现,当数据小于等于4时,数据的2位,也就是第三位为0,当大于4时,此位为1,所以我们只需要判断此为就可以判断此时的数据状态,用Data[0] <= (r_data[0][2] > 4);会增加它的电路复杂度。
			Data[1] <= r_data[1][2];
			Data[2] <= r_data[2][2];
			Data[3] <= r_data[3][2];
			Data[4] <= r_data[4][2];
			Data[5] <= r_data[5][2];
			Data[6] <= r_data[6][2];
			Data[7] <= r_data[7][2];
		end
	end
	
	always @(posedge Clk or negedge Reset_n)	begin	//接收完成信号块。
		if (!Reset_n)
			Rx_Done <= 0;	//复位时接收信号清除。
		else if (bps_clk_16x && (bps_cnt == 160))
			Rx_Done <= 1;	//和数据转化同时将数据接收使能。
		else
			Rx_Done <= 0;	//其他情况将使能清空。
	end
endmodule

你可能感兴趣的:(FPGA,Vivado,初学者,verilog,fpga,串口通信)