FPGA学习—串口通信

该博客介绍了FPGA实现串口通信的过程,包括UART协议、波特率计算、发送和接收模块的详细设计,并展示了如何通过串口控制LED和蜂鸣器,以及按键的消抖和控制。内容涵盖从系统时钟计数到数据发送接收的完整流程。
摘要由CSDN通过智能技术生成

FPGA学习系列

接下来介绍串口通信控制,本次就不用串口回环了,像正点原子,野火等fpga教程都会教串口回环,所以本文我将介绍如何串口控制去执行一些操作。之后也将拓展FPGA控制串口屏。

介绍

串口通信一般有异步通信,同步通信。又有全双工通信、半双工通信、单工通信。
按接口又有RS422、RS232、RS485。其区别我就不多赘述。

协议

UART串口通信协议需要两根线,一根RX用于接收数据,一根TX用于发送数据。一般来说一帧有10位。其中第一位为起始位:数据线拉低开始即低电平有效,中间8位数据位,最后加上停止位:数据线拉高 即高电平停止。如下图所示。

在这里插入图片描述

在这里插入图片描述

										 发送时序流程图

每个时钟周期发送一位,第一位为启动位即TX线拉低表示开始,中间8位数据位最后一位作校验位。之后再有一位停止位即TX线拉高。所有一共10位即一帧10个bit。

波特率

波特率就是每秒传输多少个bit,常见的有9600,115200.这里我以115200为例子。115200就是每秒传输115200个bit。所以传输一个bit需要1s/115200≈8.68us。而我的板子系统时钟是50MHZ也就是1秒有50000000个时钟周期。现在只要其中8.68us去发送一个bit。也就是 50 * 8.68=434(这里是50_000_000*8.68us 我把里0约掉了)。所以只要定义一个计数器去计数到434然后产生一次上升沿就可以产生115200的波特率了。
tip:为什么我用50 * 8.68呢,因为只要8.68us而1秒有50_000_000个周期所以只要算出8.68us占了几个周期就行也就是8.68 * 50.

发送模块

对于FPGA的串口发送就不用像stm32初始化结构体,清除中断标志等操作。只需按照时序编写即可实现串口发送。我都会添加详细注释。我程序中是直接50000000/115200 结果也是434.通用公式也就是频率除以波特率

module uart_send(
    input	      sys_clk,                  //系统时钟
    input         sys_rst_n,                //系统复位,低电平有效
    
    input         uart_en,                  //发送使能信号
    input  [7:0]  uart_din,                 //传入要发送的数据
    output        uart_tx_busy,             //发送忙状态标志      
    output  reg   uart_txd                  //UART发送端口
    );
    

parameter  CLK_FREQ = 50000000;            //系统时钟频率
parameter  UART_BPS = 115200;               //串口波特率
localparam  BPS_CNT  = CLK_FREQ/UART_BPS;   //为得到指定波特率,对系统时钟计数BPS_CNT次 方便修改


reg        uart_en_d0; 
reg        uart_en_d1;  
reg [15:0] clk_cnt;                         //系统时钟计数器
reg [ 3:0] tx_cnt;                          //发送数据计数器
reg        tx_flag;                         //发送过程标志信号
reg [ 7:0] tx_data;                         //寄存发送数据


wire       en_flag;							//使能信号

//*****************************************************
//**                    主函数
//*****************************************************
//在串口发送过程中给出忙状态标志
assign uart_tx_busy = tx_flag;

//捕获uart_en上升沿,得到一个时钟周期的脉冲信号	表示开始
assign en_flag = (~uart_en_d1) & uart_en_d0;//这里是获取按键发送的使能信号
											//按键按下 会产生一个按键信号获取其上升沿表示开始发送

//对发送使能信号uart_en延迟两个时钟周期
always @(posedge sys_clk or negedge sys_rst_n) begin         
    if (!sys_rst_n) begin
        uart_en_d0 <= 1'b0;                                  
        uart_en_d1 <= 1'b0;
    end                                                      
    else begin                                               
        uart_en_d0 <= uart_en;                               
        uart_en_d1 <= uart_en_d0;                            
    end
end

//当脉冲信号en_flag到达时,寄存待发送的数据,并进入发送过程          
always @(posedge sys_clk or negedge sys_rst_n) begin         
    if (!sys_rst_n) begin                                  
        tx_flag <= 1'b0;
        tx_data <= 8'd0;
    end 
    else if (en_flag) begin                 //检测到发送使能上升沿                      
            tx_flag <= 1'b1;                //进入发送过程,标志位tx_flag拉高
            tx_data <= uart_din;            //寄存待发送的数据
        end
                                            //计数到停止位结束时,停止发送过程 计数从0-9也就是10位
        else if ((tx_cnt == 4'd9) && (clk_cnt == BPS_CNT -(BPS_CNT/16))) begin                                       
            tx_flag <= 1'b0;                //发送过程结束,标志位tx_flag拉低
            tx_data <= 8'd0;
        end
        else begin
            tx_flag <= tx_flag;
            tx_data <= tx_data;
        end 
end

//进入发送过程后,启动系统时钟计数器
always @(posedge sys_clk or negedge sys_rst_n) begin         
    if (!sys_rst_n)                             
        clk_cnt <= 16'd0;                                  
    else if (tx_flag) begin                 //处于发送过程
        if (clk_cnt < BPS_CNT - 1)
            clk_cnt <= clk_cnt + 1'b1;
        else
            clk_cnt <= 16'd0;               //对系统时钟计数达一个波特率周期后清零
    end
    else                             
        clk_cnt <= 16'd0; 				    //发送过程结束
end

//进入发送过程后,启动发送数据计数器 统计此时发送了几个bit
always @(posedge sys_clk or negedge sys_rst_n) begin         
    if (!sys_rst_n)                             
        tx_cnt <= 4'd0;
    else if (tx_flag) begin                 //处于发送过程
        if (clk_cnt == BPS_CNT - 1)			//对系统时钟计数达一个波特率周期
            tx_cnt <= tx_cnt + 1'b1;		//此时发送数据计数器加1
        else
            tx_cnt <= tx_cnt;       
    end
    else                              
        tx_cnt  <= 4'd0;				    //发送过程结束
end

//根据发送数据计数器来给uart发送端口赋值
always @(posedge sys_clk or negedge sys_rst_n) begin        
    if (!sys_rst_n)  
        uart_txd <= 1'b1;        
    else if (tx_flag)
        case(tx_cnt)						//利用case语句发送一帧的数据
            4'd0: uart_txd <= 1'b0;         //起始位 
            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'b1;         //停止位
            default: ;
        endcase
    else 
        uart_txd <= 1'b1;                   //空闲时发送端口为高电平
end

endmodule	          

接收模块

module uart_recv
(
    input	wire		  	sys_clk,                  //系统时钟
    input   wire          	sys_rst_n,                //系统复位,低电平有效
    
    input             		uart_rxd,                 //UART接收端口
    output  reg       		uart_done,                //接收一帧数据完成标志
    output  reg       		rx_flag,                  //接收过程标志信号
    output  reg [ 3: 0] 	rx_cnt,                   //接收数据计数器
    output  reg [ 7: 0] 	uart_data                 //接收的数据 然后去判断
    );
    

parameter  CLK_FREQ = 50000000;                //系统时钟频率
parameter  UART_BPS = 115200;                  //串口波特率
localparam  BPS_CNT  = CLK_FREQ/UART_BPS;      //为得到指定波特率 需要对系统时钟计数BPS_CNT次
                                               

reg        uart_rxd_d0;
reg        uart_rxd_d1;
reg [15:0] clk_cnt;                              //系统时钟计数器
reg	[7:0]  rxdata;								 //接收数据缓存	

wire       start_flag;

//*****************************************************
//**                    主函数
//*****************************************************
//捕获接收端口下降沿(起始位),得到一个时钟周期的脉冲信号
assign  start_flag = uart_rxd_d1 & (~uart_rxd_d0);    

//对UART接收端口的数据延迟两个时钟周期
always @(posedge sys_clk or negedge sys_rst_n) begin 
    if (!sys_rst_n) begin 
        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

//当脉冲信号start_flag到达时,进入接收过程           
always @(posedge sys_clk or negedge sys_rst_n) begin         
    if (!sys_rst_n)                                  
        rx_flag <= 1'b0;
    else begin
        if(start_flag)                          //检测到起始位
            rx_flag <= 1'b1;                    //进入接收过程,标志位rx_flag拉高
                                                //计数到停止位中间时,停止接收过程 如果计数到结束的时候容易出现错误 中间采集最稳定
        else if((rx_cnt == 4'd9) && (clk_cnt == BPS_CNT/2))
            rx_flag <= 1'b0;                    //接收过程结束,标志位rx_flag拉低
        else
            rx_flag <= rx_flag;
    end
end

//进入接收过程后,启动系统时钟计数器
always @(posedge sys_clk or negedge sys_rst_n) begin         
    if (!sys_rst_n)                             
        clk_cnt <= 16'd0;                                  
    else if ( rx_flag ) begin                   //处于接收过程
        if (clk_cnt < BPS_CNT - 1)
            clk_cnt <= clk_cnt + 1'b1;
        else
            clk_cnt <= 16'd0;               	//对系统时钟计数达一个波特率周期后清零
    end
    else                              				
        clk_cnt <= 16'd0;						//接收过程结束,计数器清零
end

//进入接收过程后,启动接收数据计数器
always @(posedge sys_clk or negedge sys_rst_n) begin         
    if (!sys_rst_n)                             
        rx_cnt  <= 4'd0;
    else if ( rx_flag ) begin                   //处于接收过程
        if (clk_cnt == BPS_CNT - 1)				//对系统时钟计数达一个波特率周期
            rx_cnt <= rx_cnt + 1'b1;			//此时接收数据计数器加1
        else
            rx_cnt <= rx_cnt;       
    end
	 else
        rx_cnt  <= 4'd0;						//接收过程结束,计数器清零
end

//根据接收数据计数器来寄存uart接收端口数据
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_data <= 8'd0;                               
        uart_done <= 1'b0;
    end
    else if(rx_cnt == 4'd9) begin               //接收数据计数器计数到停止位时           
        uart_data <= rxdata;                    //寄存输出接收到的数据
        uart_done <= 1'b1;                      //并将接收完成标志位拉高
    end
    else begin
        uart_data <= 8'd0;                      //其余时刻一直为0 		            
        uart_done <= 1'b0; 
    end    
end
endmodule	

串口控制模块

module	led_crtl
(
	input	wire			sys_clk		,
	input	wire			sys_rst_n	,
	input	wire	[7:0]	rx_data		,		//接收到的数据
	input   wire	        led_en		,       //led控制使能
	
	output	reg		[3:0]	led			,
	output	reg				beep
);
reg	led_en_d0;//用来打拍
reg	led_en_d1;//用来打拍
parameter	CNT_MAX=24'd10_000_000;
//流水灯
reg	[23:0]	cnt;
reg	[1:0]	led_crtl_water;
reg	led_water;//流水灯使能信号


wire	 start_en;
assign	 start_en = led_en_d0 & ~led_en_d1;//获取上升沿

//流水灯延时计数0.2s
always@(posedge sys_clk or negedge sys_rst_n)	begin
	if(!sys_rst_n)
		cnt <= 24'd0;
	else	if(cnt == CNT_MAX - 1'b1)
		cnt <= 24'd0;
	else	if(led_water)
		cnt <= cnt + 1'b1;
	else
		cnt <= 24'd0;
end
//流水灯
always@(posedge sys_clk or negedge sys_rst_n)	begin
	if(!sys_rst_n)
		led_crtl_water <= 2'd0;
	else	if(cnt == CNT_MAX - 1'b1&&led_water)
		led_crtl_water <= led_crtl_water + 1'b1;
	else
		led_crtl_water <= led_crtl_water;
end
//控制信号获取
always@(posedge sys_clk or negedge sys_rst_n)	begin
	if(!sys_rst_n)	begin
		led_en_d0 <=	1'b0;
        led_en_d1 <=	1'b0;
	end
	else	begin
		led_en_d0 <= led_en	;
		led_en_d1 <= led_en_d0;
	end
end

always@(posedge sys_clk or negedge sys_rst_n)	begin
	if(!sys_rst_n)
		led <= 4'd0;
	else	if(start_en)
	case(rx_data)	//根据接收到的数据进行判断
	8'h01	:	led<=4'b0001;
	8'h02	:	led<=4'b0010;
	8'h03	:	led<=4'b0100;
	8'h04	:	led<=4'b1000;
	8'h05	:	led_water<=1'b1;	//开启流水灯
	8'h06	:	led_water <= 1'b0;	//关闭流水灯
	8'hbe	:	beep<=1'b1;
	8'hbf	:	beep<=1'b0;
	8'haa	:	led<=4'b1111;	//a
	8'ha0	:	led<=4'b0000;	//
	endcase
	else	if(led_water)
	case(led_crtl_water)
		2'b00	:	led <= 4'b0001;
		2'b01	:	led <= 4'b0010;
		2'b10	:	led <= 4'b0100;
		2'b11	:	led <= 4'b1000;
		default	:	led<=4'b0000;
		endcase
end
endmodule

按键消抖模块

//按键消抖
module key_filter
#(
	parameter	CNT_MAX = 20'd999_999
)
(
	input	wire				sys_clk		,
	input	wire				sys_rst_n	,
	input	wire				key_in		,
	
	output	reg					key_flag	//1表示按下 0表示没有按下
);
reg	[19:0]	cnt_20ms;	//计数器

always@(posedge sys_clk or negedge sys_rst_n)	begin
    if(sys_rst_n == 1'b0)
        cnt_20ms <= 20'b0;
    else    if(key_in == 1'b1)
        cnt_20ms <= 20'b0;
    else    if(cnt_20ms == CNT_MAX && key_in == 1'b0)
        cnt_20ms <= cnt_20ms;
    else
        cnt_20ms <= cnt_20ms + 1'b1;
end

//key_flag:当计数满20ms后产生按键有效标志位
//且key_flag在999_999时拉高,维持一个时钟的高电平
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        key_flag <= 1'b0;
    else    if(cnt_20ms == CNT_MAX - 1'b1)
        key_flag <= 1'b1;
    else
        key_flag <= 1'b0;

endmodule

按键控制模块只用于发送数据

按键控制模块

module key_data
(	
	input	wire				sys_clk			,
	input	wire				sys_rst_n		,
	input	wire	[3:0]		key				,

	output	reg		[7:0]		key_data_out	,
	output	reg					key_done	
);

wire	K1			;	//四个按键
wire	K2          ;   //四个按键
wire	K3          ;   //四个按键
wire	K4          ;   //四个按键


always@(posedge sys_clk or negedge sys_rst_n)	begin
	if(!sys_rst_n)	
		key_data_out <= 8'd0;		
	else	if(K1==1'b1)	
		key_data_out <= 8'h01;
	else	if(K2==1'b1)	
		key_data_out <= 8'h02;
	else	if(K3==1'b1)	
		key_data_out <= 8'hbe;
	else	if(K4==1'b1)
		key_data_out <= 8'hbf;	
end
always@(posedge sys_clk or negedge sys_rst_n)	begin
	if(!sys_rst_n)	begin
		key_done 	<= 1'b0;
	end
	else	if(K1||K2||K3||K4)	
		key_done 	<= 1'b1;	//按键按下标志
	else
		key_done 	<= 1'b0;
end
//按键例化
key_filter
#(
	.CNT_MAX (20'd999_999)
)
key_1
(
	.sys_clk	(sys_clk)	,
	.sys_rst_n	(sys_rst_n)	,
	.key_in		(key[0])	,
	
	.key_flag	(K1)		//1表示按下 0表示没有按下
);

key_filter
#(
	.CNT_MAX (20'd999_999)
)
key_2
(
	.sys_clk	(sys_clk)	,
	.sys_rst_n	(sys_rst_n)	,
	.key_in		(key[1])	,
	
	.key_flag	(K2)		//1表示按下 0表示没有按下
);
key_filter
#(
	.CNT_MAX (20'd999_999)
)
key_3
(
	.sys_clk	(sys_clk)	,
	.sys_rst_n	(sys_rst_n)	,
	.key_in		(key[2])	,
	
	.key_flag	(K3)		//1表示按下 0表示没有按下
);
key_filter
#(
	.CNT_MAX (20'd999_999)
)
key_4
(
	.sys_clk	(sys_clk)	,
	.sys_rst_n	(sys_rst_n)	,
	.key_in		(key[3])	,
	
	.key_flag	(K4)		//1表示按下 0表示没有按下
);



endmodule

顶层模块

module top_uart_led
(
	input	wire			sys_clk		,
	input	wire			sys_rst_n	,
	input	wire			uart_rxd	,
	input	wire	[3:0]	key			,

	output	wire	[3:0]	led			,	
	output	wire			beep		,
	output	wire			uart_txd	
);
parameter	CLK_FREQ = 50_000_000;	
parameter	UART_BPS = 115200;

//连接
wire	[7:0]	uart_data;
wire	uart_done;
wire	[7:0]	key_data_out;
wire	uart_tx_busy;
wire	key_done;



led_crtl	led_crtl_inst
(
	.sys_clk		(sys_clk)	,
	.sys_rst_n		(sys_rst_n),
	.rx_data		(uart_data),		//接收到的数据
	.led_en			(uart_done),       //led控制使能
	
	.led            (led)		,
	.beep			(beep)
);
//按键发送数据
key_data	key_data_inst
(	
	.sys_clk		(sys_clk)	,
	.sys_rst_n		(sys_rst_n)	,
	.key			(key)	,
				
	.key_data_out	(key_data_out)	,
	.key_done		(key_done)
);


//发送数据
uart_send uart_send_inst
(
    .sys_clk			(sys_clk)	,                  //系统时钟
    .sys_rst_n			(sys_rst_n)	,                //系统复位,低电平有效
						
    .uart_en			(key_done)	,                  //发送使能信号
    .uart_din			(key_data_out)	,                 //待发送数据
    .uart_tx_busy		(uart_tx_busy)	,             //发送忙状态标志      
    .uart_txd           (uart_txd)       			//UART发送端口
 );

uart_recv uart_recv_inst
(
    .sys_clk	(sys_clk	)		,                 //系统时钟
    .sys_rst_n	(sys_rst_n)			,                 //系统复位,低电平有效
    
    .uart_rxd	(uart_rxd)			,                 //UART接收端口
    .uart_done	(uart_done)			,                //接收一帧数据完成标志信号
    .uart_data  (uart_data)              			 //接收的数据
);
endmodule

获取信号的上升沿或者下降沿

这里有很多种方法,我介绍一下最常用的方法。比如在获取串口发送开始信号的时候.
start_flag = uart_rxd_d1 & (~uart_rxd_d0);就是获取串口发送端开始发送数据的时候TX拉低的时候的下降沿,表示开始.原理如下图所示.分别定义了两个rxd去对uart_rxd信号打拍两次.

在这里插入图片描述
在红线部分,当uart_rxd拉低时,下个时钟周期,取反的uart_rxd_d0信号与uart_rxd_d1信号相与就会得到一个高电平,其余全都是低电平,也就是获取他的下降沿会延迟一个时钟周期.

注意

因为串口发送的时候时ascii码形式,所以要不用16进制发送,要不要看ascii码对应的16进制是否和接收判断那部分一致.比如串口直接发送1(字符串形式)那么对应16进制应该是16’h31.

总结

后续将给出模块的仿真文件以及对应vivado与quartusii工程文件.
vivado工程 使用的是2018.3
链接:https://pan.baidu.com/s/1T10IFlGXi4WTwU2smPmBFg
提取码:gp9w
–来自百度网盘超级会员V5的分享

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值