【FPGA】——UART串口通信

UART串口简介

  串行通信分为两种方式:同步串行通信和异步串行通信。同步串行通信要求通信双方使用同一时钟,异步则没有这个要求。UART是一种采用异步串行通信方式的通用异步收发传输器(universal asynchronous receiver-transmitter),它在发送数据时将接收到的串行数据转成并行数据。

通信时序

【FPGA】——UART串口通信_第1张图片
  如上图所示,一帧数据由起始位(低电平),数据位(5/6/7/8),校验位(奇/偶),停止位(高电平1/1.5/2)构成,数据传输的速率用波特率(每秒传输的比特数bps)来表示。
  在以往的单片机开发中,串口往往作为一种外设集成进单片机中,我们只需要通过配置寄存器来确定通信的参数即可。当然,也可以通过软件模拟通信协议。而这里,我们通过FPGA来实现纯数字电路上的串口通信,这里的任务即上位机通过串口发送数据给FPGA,然后FPGA实现回传。

顶层模块设计

【FPGA】——UART串口通信_第2张图片
  上图为顶层模块的设计框图,系统时钟和复位信号不必多说,顶层模块有一个串口的接收信号(传给串口接收子模块)和一个串口的发送信号(传给串口发送子模块)。
  在顶层模块中,例化了串口串口接收子模块和串口发送子模块,分别用来处理串口接收到的数据和串口需要发送的数据。

//uart串口收发程序,顶层模块
module uart_top (
	input 	sys_clk,
	input 	sys_rst_n,
	input		uart_rxd,	//FPGA串口接收端
	output	uart_txd		//FPGA串口发送端
);

parameter CLK_FREQ = 50000000;
parameter UART_BPS = 115200;

//模块之间的交互一定要用线网变量
wire [7:0]	uart_data;
wire			uart_done;

//例化串口接收子模块
uart_recv #(
	.CLK_FREQ		(CLK_FREQ),
	.UART_BPS		(UART_BPS)
)u_uart_recv(
	.sys_clk			(sys_clk),
	.sys_rst_n		(sys_rst_n),
	.uart_rxd		(uart_rxd),		//接收端口的信号
	.uart_done		(uart_done),	//接收完成
	.uart_data		(uart_data)		//接收的数据,并行
);

//例化串口发送子模块
uart_send #(
	.CLK_FREQ		(CLK_FREQ),
	.UART_BPS		(UART_BPS)
)u_uart_send(
	.sys_clk			(sys_clk),
	.sys_rst_n		(sys_rst_n),
	
	.uart_en			(uart_done),	//发送使能信号
	.uart_data		(uart_data),	//需要发送的串口数据,并行
	.uart_txd		(uart_txd),		//串口发送端
);

endmodule

串口接收子模块设计

  • 时钟信号、复位信号、串口接收信号均从顶层模块引入
  • 通过信号线上的下降沿确定起始位,开始接收(下降沿通过两个变量的阻塞赋值实现)
  • 开始接收后,需要对时钟数进行计数,到达规定时钟数进入下一个bit的接收,此时bit数也要+1
  • 每一次在时钟数的中间在信号线上进行采样,注意这里的时钟均指一个bit内的时钟
  • 采用一个寄存器专门存放接收到的数据,即串转并
  • 接收到第9个比特(停止位)时跳出,认为接受完成,并输出接收完成信号
//串口接收子模块
module uart_recv (
	input 					sys_clk,
	input 					sys_rst_n,
	input					uart_rxd,		//接收端口的信号
	output	reg				uart_done,		//接收完成
	output	reg[7:0]		uart_data		//接收的数据,并行
);

parameter 	CLK_FREQ = 50000000;			//时钟频率,50M
parameter	UART_BPS = 115200;				//波特率,bit/s
parameter	BPS_CNT = CLK_FREQ/UART_BPS;	//每个bit传输需要的时钟数

reg				uart_rxd_d0;
reg				uart_rxd_d1;
reg	[15:0]		clk_cnt;		//时钟数,最大到BPS_CNT
reg	[3:0]		rx_cnt;			//接收数据bit的个数,最大到9
reg	[7:0]		rxdata;			//接收的数据寄存器
reg				rx_flag;		//接收数据的标志位


wire				start_flag;	//起始位开始的标志位

//起始位触发(捕获下降沿),d1延后d0一个周期,所以当d0为0,d1为1,意味着下降沿
assign start_flag = uart_rxd_d1 & (~uart_rxd_d0);

//d1和d0都是数据线上信号,保证d1延后d0一个时钟周期
always @(posedge sys_clk or negedge sys_rst_n) begin
	if (~sys_rst_n) begin	//复位时数据信号清0
		uart_rxd_d0 <= 1'b0;
		uart_rxd_d1 <= 1'b0;
	end
	else begin
		uart_rxd_d0 <= uart_rxd;
		uart_rxd_d1 <= uart_rxd_d0;
	end
		
end

//确立信号接收标志位
always @(posedge sys_clk or negedge sys_rst_n) begin
	if (~sys_rst_n)
	rx_flag <= 1'd0;
	else begin
		if (start_flag)
			rx_flag <= 1'd1;
		else if ((rx_cnt == 4'd9) && (clk_cnt == BPS_CNT/2))	//判断停止位,在第9个bit的中间时钟处
			rx_flag <= 1'd0;
		else
			rx_flag <= rx_flag;
	end
end

//对时钟进行计数(最大到BPS_CNT),对接收的bit数进行计数,
//根据这两个数判断停止位
always @(posedge sys_clk or negedge sys_rst_n) begin
	if (~sys_rst_n) begin				//复位时计数都清零
		rx_cnt <= 4'd0;
		clk_cnt <= 16'd0;
	end
	else if (rx_flag) begin				//开始数据传输
		if (clk_cnt < BPS_CNT)
			clk_cnt <= clk_cnt + 1'b1;
		else begin						//经过了一个bit的时钟数
			clk_cnt <= 1'b0;			//时钟数清零
			rx_cnt <= rx_cnt + 1'b1;	//bit数加1,1bit即从第一个数据位开始,包含了起始位
		end
	end	
	else begin
		clk_cnt <= 16'd0;
		rx_cnt <= 4'd0;
	end
end

//寄存接收数据
always @(posedge sys_clk or negedge sys_rst_n) begin
	if (~sys_rst_n)
		rxdata <= 8'd0;
	else if (rx_flag) 
		if (clk_cnt == BPS_CNT/2) begin	//在一个比特的时钟中间处对数据进行接收
			case (rx_cnt)						//寄存接收数据
				4'd1:	rxdata[0] <= uart_rxd_d1;
				4'd2:	rxdata[1] <= uart_rxd_d1;
				4'd3:	rxdata[2] <= uart_rxd_d1;
				4'd4:	rxdata[3] <= uart_rxd_d1;
				4'd5:	rxdata[4] <= uart_rxd_d1;
				4'd6:	rxdata[5] <= uart_rxd_d1;
				4'd7:	rxdata[6] <= uart_rxd_d1;
				4'd8:	rxdata[7] <= uart_rxd_d1;
				default:	;
			endcase
		end
		else
		rxdata <= rxdata;
	else
		rxdata <= 8'd0;
end

//判断接收数据是否完成以及把数据赋给输出信号
always @(posedge sys_clk or negedge sys_rst_n) begin
	if (~sys_rst_n) begin
		uart_done <= 1'd0;
		uart_data <= 8'd0;
	end
	else if (rx_cnt == 4'd9) begin
		uart_data <= rxdata;
		uart_done <= 1'd1;
	end
	else begin
		uart_data <= 8'd0;
		uart_done <= 1'd0;
	end
end

endmodule

串口发送子模块设计

  • 时钟信号、复位信号来自顶层模块,需要发送的数据(并行)以及发送使能信号来自串口接收子模块
  • 找到发送使能信号的上升沿,确立起始位,开始发送
  • 发送的时候同样需要对时钟进行计数,达到规定时钟数后进入下一个bit,当然也需要对bit进行计数
  • 发送时根据当前bit数确定发送数据的哪一位,然后对应拉高或者拉低Tx信号线,这里需要注意,起始位和停止位都需要发送
  • 发送完9个比特后拉高信号线
//串口发送子模块
module uart_send(
	input 			sys_clk,
	input 			sys_rst_n,
	
	input				uart_en,			//发送使能信号
	input	 [7:0]	uart_data,		//需要发送的串口数据,并行
	output reg		uart_txd			//串口发送端
);

parameter 	CLK_FREQ = 50000000;				//时钟频率,50M
parameter	UART_BPS = 115200;				//波特率,bit/s
parameter	BPS_CNT = CLK_FREQ/UART_BPS;	//每个bit传输需要的时钟数

reg				uart_en_d0;			
reg				uart_en_d1;
reg	[15:0]	clk_cnt;				//时钟计数寄存器
reg	[3:0]		tx_cnt;				//发送bit计数寄存器
reg				tx_flag;				//发送标志位
reg	[7:0]		tx_data;				//寄存发送数据

wire				en_flag;				//发送使能位

//找到发送使能信号的上升沿,将发送使能位置位
assign en_flag = uart_en_d0 & (~uart_en_d1);

//确定d0和d1,d1滞后d0一个时钟周期
always @(posedge sys_clk or negedge sys_rst_n) begin
	if (~sys_rst_n) begin
		uart_en_d0 <= 1'd0;
		uart_en_d1 <= 1'd0;
	end
	else begin
		uart_en_d0 <= uart_en;
		uart_en_d1 <= uart_en_d0;
	end
end

//确定发送标志位
always @(posedge sys_clk or negedge sys_rst_n) begin
	if (~sys_rst_n)
		tx_flag <= 1'd0;
	else if (en_flag) begin
		tx_flag <= 1'd1;
		tx_data <= uart_data;
	end
	else begin
		if ((tx_cnt == 4'd9) && (clk_cnt == BPS_CNT/2))	begin//已经发送了9比特数据,则发送结束,标志位清零
			tx_flag <= 1'd0;
			tx_data <= 8'd0;
		end
		else begin
			tx_flag <= tx_flag;
			tx_data <= tx_data;	
		end
	end
end

//对时钟和发送bit进行计数
always @(posedge sys_clk or negedge sys_rst_n) begin
	if (~sys_rst_n) begin
		clk_cnt <= 16'd0;
		tx_cnt <= 4'd0;
	end
	else if (tx_flag) begin
		if (clk_cnt < BPS_CNT - 1'd1)
			clk_cnt <= clk_cnt + 1'd1;
		else begin
			clk_cnt <= 16'd0;
			tx_cnt <= tx_cnt + 1'd1;
		end
	end
end

//并转串,将数据放到串口发送信号线上
always @(posedge sys_clk or negedge sys_rst_n) begin
	if (~sys_rst_n) 
		uart_txd <= 1'd1;
	else if (tx_flag) 
		case (tx_cnt)
			4'd0:	uart_txd <= 1'd0;
			4'd1:	uart_txd <= tx_data[0];
			4'd2:	uart_txd <= tx_data[1];
			4'd3:	uart_txd <= tx_data[2];
			4'd4:	uart_txd <= tx_data[3];
			4'd5:	uart_txd <= tx_data[4];
			4'd6:	uart_txd <= tx_data[5];
			4'd7:	uart_txd <= tx_data[6];
			4'd8:	uart_txd <= tx_data[7];
			4'd9:	uart_txd <= 1'd1;
			default:;
		endcase
	else
		uart_txd <= 1'd1;
end

endmodule

结果

  直接放抓到的波形图:
【FPGA】——UART串口通信_第3张图片
  时序图很漂亮,接收子模块通过下降沿触发后开始接收数据,接收完后发出完成信号,发送子模块根据这个完成信号的上升沿,发送数据。
  在写FPGA的程序时,跟以往的编程差别非常之大,Verilog的并行执行要求写程序时拥有更加严谨的逻辑。现在还处于FPGA的入门阶段,只能隐隐约约地从代码背后感受到一点数字电路的轮廓。
  时隔一年多再次回到了硬件编程,需求也今非昔比了,现在的目标是将通信算法移植到硬件平台上,任重而道远,慢慢积累吧!

Ref:
《正点原子新起点FPGA教程》

你可能感兴趣的:(FPGA)