FPGA:UART串口接收模块
- UART串口接收
- 模块分析
- 模块设计与分析
- 亚稳态现象进行打拍
- 边沿检测电路
- 使能逻辑
- 波特率分频计数器
- 位计数器
- 位接收逻辑
- 完成标志信号
- 模块优化
- 测试仿真
- 板级验证
声明:本设计中思路和截图均参考 小梅哥FPGA
UART串口接收
串口接收是串口发送的逆操作。在FPGA中,主要指的是别的通信设备通过串口发送数据到FPGA中,再通过FPGA内的指令解析模块对接受到的数据进行解码。例如,在ADC采集中,可以通过串口发送ADC采集时应该选择的通道、采样深度等信息。在串口接收中,主要涉及的参数和串口发送时的参数类似。即波特率、起始位、停止位、校验位等。
在这里,以下列几个要求为例,设计一个UART串口接收模块:
①能够接收
8
8
8位数据位、无校验位、
1
1
1位停止位的UART串口数据
②能够以一定的方式修改波特率(参数重定义)
③每接受完一个数据,就将接受的数据结果显示在开发板的8位LED灯上。
模块分析
如上图所示,为了准确的获得
B
i
t
0
∼
B
i
t
7
Bit0\sim Bit7
Bit0∼Bit7的数据,需要考虑以下问题:
(1)在数据变化时,其不是瞬间从逻辑
0
0
0编程逻辑
1
1
1,为了获取准确对应位的数据,我们取每一位的中间位进行采样,即在位时间的中间时刻进行采样,确保数据的准确性。因此需要一个波特率分频计数器。
(2)为了检测起始位,还需要一个边沿检测逻辑,这里主要是下降沿逻辑,因为空闲时为
1
1
1,起始为
0
0
0,通过一个D触发器判断前一个时刻和当前时刻的值即可实现。
(3)波特率分频计数器并不是一直工作的,而是检测到发送方发送数据后才开始工作,因此,这里需要设计一个波特率计数器使能逻辑
(4)为了确定发送哪一位,此时还需要设计一个位计数器。
(5)因为输入信号(uart_rx)相对于时钟信号而言,是一个异步信号,可能不在时钟的上升沿来临,导致数据无法正确的保存在D触发器中,为了防止这种异步信号导致的“亚稳态现象”,可以引入D触发器进行打拍操作,对于
50
M
50M
50M的晶振,一般选用两级的打拍即可滤除这种现象。
(6)发送逻辑,即在每一位的中点将对应位进行采样。
(7)在完成一次数据发送的时候,需要引入一个发送标志信号,告知其他设备此次传输完成。
模块设计与分析
亚稳态现象进行打拍
为了防止异步信号的影响,引入两级D触发器进行打拍操作。
reg dff0_uart_rx,dff1_uarx_rx;
// 防止亚稳态出现,进行打拍同步
always @(posedge Clk)
dff0_uart_rx<=uart_rx;
always @(posedge Clk)
dff1_uarx_rx<=dff0_uart_rx;
边沿检测电路
因为起始位为0,空闲时一直是逻辑1,所以此时需要设计一个下降沿检测逻辑,主要判断前一个时刻uart_rx1且当前时刻uart_rx0,则说明下降沿来临。
wire nedge_uart_rx; //下降沿信号
always @(posedge Clk)
r_uart_rx<=dff1_uarx_rx;
assign nedge_uart_rx = (dff1_uarx_rx==0)&&(r_uart_rx==1);
使能逻辑
通过前面的分析,我们的思路是当检测到下降沿的时候,则认为传输开始,波特率分频计数器开始工作,所以定义一个使能信号(en_baud_cnt),其逻辑如下:复位时置 0 0 0,当检测到下降沿的时候(边沿检测电路中nedge_uart_rx为1),则置 1 1 1,当一次数据传输完成后,即位计数器计满且波特率分频计数器计满(即停止位发送完毕)时再次将该信号置 0 0 0,此时还有一种情况,即当检测到下降沿的时候,第一位(起始位)如果为 1 1 1,那这个时候我们将忽略这一次的下降沿,不然使能逻辑置 1 1 1。
//波特率计数器使能逻辑
always @(posedge Clk or negedge Reset_n)
if(!Reset_n)
en_baud_cnt<=1'b0;
else if(nedge_uart_rx) //检测到下降沿开始计数
en_baud_cnt<=1'b1;
else if((Bit_cnt==0)&&(Baud_div_cnt==MCNT_BAUD)&&(dff1_uarx_rx==1'b1)) //起始位为1的情况
en_baud_cnt<=1'b0;
else if((Bit_cnt==9)&&(Baud_div_cnt==MCNT_BAUD))
en_baud_cnt<=1'b0;
波特率分频计数器
在前面设置了波特率后,对每一位的传输时间进行计算,设时钟频率为CLOCK_FREQ,波特率为Baud_set,则传输1位数据需要的时钟周期数位:CLOCK_FREQ/Baud_set,而计数器从零开始计数,所以令MCNT_BAUD=CLOCK_FREQ/Baud_set-1
always @(posedge Clk or negedge Reset_n)
if(!Reset_n)
Baud_div_cnt<=0;
else if(en_baud_cnt)
begin
if(Baud_div_cnt==MCNT_BAUD)
Baud_div_cnt<=30'b0;
else
Baud_div_cnt<=Baud_div_cnt+1'b1;
end
else
Baud_div_cnt<=30'b0;
位计数器
当传输数据时,还需要判断当前传输的是哪一位,这时需要一个位计数器,根据协议可知,数据位有 8 8 8位,加上起始位和停止位一共有 10 10 10位数据,所以需要一个4位计数器才可以计数到 10 10 10.其逻辑如下:当波特率分频计数器技术满时,判断其值是否为 9 9 9,如果是,则置 0 0 0,否则自加。这里不需要再额外判断是否使能,因为使用的波特率分频计数器已经判断过。
//位计数器
always @(posedge Clk or negedge Reset_n)
if(!Reset_n)
Bit_cnt<=0;
else if((Bit_cnt==9)&&(Baud_div_cnt==MCNT_BAUD))
Bit_cnt<=0;
else if(Baud_div_cnt==MCNT_BAUD)
Bit_cnt<=Bit_cnt+1'b1;
else
Bit_cnt<=Bit_cnt;
位接收逻辑
在这里,依旧使用一个case语句,判断位计数器Bit_cnt的值,将其正确的复制给对应的位,为了使得接受的数据是完整的变化,这里定义一个临时的8位寄存器r_Data,接受打拍后的uart数据,即dff1_uart_rx。在每次波特率分频计数器计数到中间时进行采样。代码如下:
// 位接受逻辑
always @(posedge Clk or negedge Reset_n)
if(!Reset_n)
r_Data<=8'b0000_0000;
else if(Baud_div_cnt==MCNT_BAUD/2) //在中间时刻进行采样
begin
case(Bit_cnt)
1:r_Data[0]<=dff1_uarx_rx;
2:r_Data[1]<=dff1_uarx_rx;
3:r_Data[2]<=dff1_uarx_rx;
4:r_Data[3]<=dff1_uarx_rx;
5:r_Data[4]<=dff1_uarx_rx;
6:r_Data[5]<=dff1_uarx_rx;
7:r_Data[6]<=dff1_uarx_rx;
8:r_Data[7]<=dff1_uarx_rx;
default:r_Data<=r_Data;
endcase
end
完成标志信号
接收完成后,还需要给外界一个信号表示接收完成,即当波特率分频计数器计满且位计数器计数到 9 9 9的时候,表示接受完成。此时将之前存储在临时寄存器r_Data上的值全部复制给Data。
assign w_Rx_done = ((Baud_div_cnt==MCNT_BAUD/2)&&(Bit_cnt==9));
always @(posedge Clk)
Rx_done<=w_Rx_done;
always@(posedge Clk)
if(w_Rx_done)
Data<=r_Data;
模块优化
在上述描述中,还有部分功能需要优化。
首先,在实际应用中,如果FPGA与设备A进行串口通信,由于时钟不同,会带来一定的误差,从而导致数据出现错误。例如:设备A连续发送两个数据给FPGA,8’h00和8’h01,由于极小的误差,FPGA在接受最后一位的时候可能设备A已经发送下一个数据的起始位,从而导致数据之间进行丢失和出错。为了解决这个问题,一般我们在最后一位,即停止位计数到中间,即采样结束后就让设备向外界输出完成标志信号,剩下半个位时间进行容错。在代码上体现就是在每个Bit_cnt==9时,如果波特率分频计数器等于计数参数一半时,就表示结束。
其次,在面对强干扰的情况下,上述在每个波特率分频计数器中间采样可能出错,此时可以使用多次采样求概率的方法进行排除。例如,在一个位时间内,再进行16次采样,去掉前面5次和后面4次的数据,对中间7次数据进行概率统计,如果1出现的次数多,则表示采集的为1,否则为0.这个方法将在下面一个帖子进行阐述。
下面是在第一个问题基础上优化后的代码,整合好如下所示:
`timescale 1ns / 1ps
module uart_byte_rx(
input Clk,
input Reset_n,
input uart_rx,
output reg [7:0]Data,
output reg Rx_done
);
parameter CLOCK_FREQ=50_000_000; //时钟晶振频率50M
parameter Baud_Set = 9600; //波特率设置为9600
parameter MCNT_BAUD = CLOCK_FREQ/Baud_Set-1; // 位计数器计数时间
/*参数定义区*/
reg [29:0] Baud_div_cnt;
reg [3:0] Bit_cnt;
reg en_baud_cnt; // 计数器使能信号,检测到下降沿开始计数
reg r_uart_rx;
wire nedge_uart_rx; //下降沿信号
wire w_Rx_done;
reg [7:0]r_Data;
reg dff0_uart_rx,dff1_uarx_rx;
// 防止亚稳态出现,进行打拍同步
always @(posedge Clk)
dff0_uart_rx<=uart_rx;
always @(posedge Clk)
dff1_uarx_rx<=dff0_uart_rx;
// 下降沿检测电路
always @(posedge Clk)
r_uart_rx<=dff1_uarx_rx;
assign nedge_uart_rx = (dff1_uarx_rx==0)&&(r_uart_rx==1);
// 波特率分频计数器
always @(posedge Clk or negedge Reset_n)
if(!Reset_n)
Baud_div_cnt<=0;
else if(en_baud_cnt)
begin
if(Baud_div_cnt==MCNT_BAUD)
Baud_div_cnt<=30'b0;
else
Baud_div_cnt<=Baud_div_cnt+1'b1;
end
else
Baud_div_cnt<=30'b0;
//波特率计数器使能逻辑
always @(posedge Clk or negedge Reset_n)
if(!Reset_n)
en_baud_cnt<=1'b0;
else if(nedge_uart_rx) //检测到下降沿开始计数
en_baud_cnt<=1'b1;
else if((Bit_cnt==0)&&(Baud_div_cnt==MCNT_BAUD/2)&&(dff1_uarx_rx==1'b1)) //起始位为1的情况
en_baud_cnt<=1'b0;
else if((Bit_cnt==9)&&(Baud_div_cnt==MCNT_BAUD/2))
en_baud_cnt<=1'b0;
//位计数器
always @(posedge Clk or negedge Reset_n)
if(!Reset_n)
Bit_cnt<=0;
else if((Bit_cnt==9)&&(Baud_div_cnt==MCNT_BAUD/2))
Bit_cnt<=0;
else if(Baud_div_cnt==MCNT_BAUD)
Bit_cnt<=Bit_cnt+1'b1;
else
Bit_cnt<=Bit_cnt;
// 位接受逻辑
always @(posedge Clk or negedge Reset_n)
if(!Reset_n)
r_Data<=8'b0000_0000;
else if(Baud_div_cnt==MCNT_BAUD/2) //在中间时刻进行采样
begin
case(Bit_cnt)
1:r_Data[0]<=dff1_uarx_rx;
2:r_Data[1]<=dff1_uarx_rx;
3:r_Data[2]<=dff1_uarx_rx;
4:r_Data[3]<=dff1_uarx_rx;
5:r_Data[4]<=dff1_uarx_rx;
6:r_Data[5]<=dff1_uarx_rx;
7:r_Data[6]<=dff1_uarx_rx;
8:r_Data[7]<=dff1_uarx_rx;
default:r_Data<=r_Data;
endcase
end
// 接受完成标志信号
assign w_Rx_done = ((Baud_div_cnt==MCNT_BAUD/2)&&(Bit_cnt==9));
always @(posedge Clk)
Rx_done<=w_Rx_done;
always@(posedge Clk)
if(w_Rx_done)
Data<=r_Data;
endmodule
测试仿真
编写testbench文件,进行仿真测试,文件如下:
`timescale 1ns / 1ps
module uart_byte_rx_tb();
reg Clk;
reg Reset_n;
reg uart_rx;
wire [7:0] Data;
wire Rx_done;
uart_byte_rx uart_byte_rx(
.Clk(Clk),
.Reset_n(Reset_n),
.uart_rx(uart_rx),
.Data(Data),
.Rx_done(Rx_done)
);
initial Clk=1;
always #10 Clk=~Clk;
initial begin
Reset_n=0;
uart_rx=1;
#201;
Reset_n=1;
#200;
// 8'b0101_0101
uart_rx=0; #(5208*20); //起始位
uart_rx=1; #(5208*20); //Bit0
uart_rx=0; #(5208*20); //Bit1
uart_rx=1; #(5208*20); //Bit2
uart_rx=0; #(5208*20); //Bit3
uart_rx=1; #(5208*20); //Bit4
uart_rx=0; #(5208*20); //Bit5
uart_rx=1; #(5208*20); //Bit6
uart_rx=0; #(5208*20); //Bit7
uart_rx=1; #(5208*20); //停止位
#(5208*20*10);
// 8'b1010_1010
uart_rx=0; #(5208*20); //起始位
uart_rx=0; #(5208*20); //Bit0
uart_rx=1; #(5208*20); //Bit1
uart_rx=0; #(5208*20); //Bit2
uart_rx=1; #(5208*20); //Bit3
uart_rx=0; #(5208*20); //Bit4
uart_rx=1; #(5208*20); //Bit5
uart_rx=0; #(5208*20); //Bit6
uart_rx=1; #(5208*20); //Bit7
uart_rx=1; #(5208*20); //停止位
#(5208*20*10);
// 8'b1111_0000
uart_rx=0; #(5208*20); //起始位
uart_rx=0; #(5208*20); //Bit0
uart_rx=0; #(5208*20); //Bit1
uart_rx=0; #(5208*20); //Bit2
uart_rx=0; #(5208*20); //Bit3
uart_rx=1; #(5208*20); //Bit4
uart_rx=1; #(5208*20); //Bit5
uart_rx=1; #(5208*20); //Bit6
uart_rx=1; #(5208*20); //Bit7
uart_rx=1; #(5208*20); //停止位
#(5208*20*10);
// 8'b0000_1111
uart_rx=0; #(5208*20); //起始位
uart_rx=1; #(5208*20); //Bit0
uart_rx=1; #(5208*20); //Bit1
uart_rx=1; #(5208*20); //Bit2
uart_rx=1; #(5208*20); //Bit3
uart_rx=0; #(5208*20); //Bit4
uart_rx=0; #(5208*20); //Bit5
uart_rx=0; #(5208*20); //Bit6
uart_rx=0; #(5208*20); //Bit7
uart_rx=1; #(5208*20); //停止位
#(5208*20*10);
$stop;
end
endmodule
依次模拟上位机向FPGA发送四次数据,仿真结果如下所示:
取其中第一次数据传输进行分析,即8’b0101_0101。
完成标志信号在最后一位传输到中间时刻(5208-1)/2且位计数器计数到9的时候,完成标志信号拉高,持续一个时钟周期,且此时Data的数据更新为r_Data中的数据。且整体波形和仿真文件中一致。
板级验证
管脚绑定后生成比特流写入开发板。打开串口通信软件,选择对应的波特率,并将接受的8位数据分配给开发板上的8位LED灯。
1、发送FF,8位LED灯全亮
2、发送AA(10101010),LED灯交替亮。
本次实验结束。