异步串口的接收跟发送不一样,本质上串口的发送属于并行转串行,而接收则属于串行转并行。
跟发送一样,接收也需要约定波特率与设置波特率计数器,使得FPGA能够按照约定的波特率接收数据。
在FPGA学习[3]里面,已经计算了计数器计数值与波特率之间的关系
c
o
u
n
t
b
a
u
d
=
f
c
l
k
b
a
u
d
count_{baud} = \frac{f_{clk}}{baud}
countbaud=baudfclk
在实际程序中,我们在计数器counter的值在波特率计数值中间的时候采样接收电平信号。
为什么要在计数器中间处采样信号?
当信号传输路径上存在杂散电容与引线电感时,在信号的跳变沿会出现反射现象,这个现象在传输路径较长时非常明显。这种跳变沿上发生的过冲、下冲或振铃有时候可能干扰正常数据信号的采集。(图示是吉布斯现象,并非信号反射)
对于串口来说,当波特率较高的时候,很容易因为边沿抖动造成数据接收异常,特别是糟糕的PCB设计更令此类问题普遍发生。对于普通单片机而言,无法支持如此高的波特率主要原因有以下几点
- 时钟频太低
- PCB设计不当
- 传输路径糟糕
- 电平转换芯片或其他芯片无法支持
对于STM32单片机,芯片设计了过采样来解决因为边沿电平不稳定带来的问题,具体思路如下:
使用高于波特率若干倍的时钟,在时钟采样时钟的中间位置读取信号电平。为了内部逻辑简单一般过采样率是2的幂次:4,8,16等。
对于FPGA而言使用同步逻辑设计的串口接收模块采样率固定为 f c l k f_{clk} fclk,这一点比大多数单片机有先天的优势,使用PLL将时钟倍频至150MHz以上也是可以做到的,因此设计得当的FPGA能轻松做到较高的波特率。然而无论如何提高波特率,串口通信仍然是低速的通信。
Verilog HDL实现
1.输入信号处理
always @(posedge clk or negedge nrst) begin
if(!nrst) begin
rx_in <= 1'b1;
end
else begin
rx_in <= rx;
end
end
这一步将输入信号打一拍处理,可以缓解信号的亚稳态,便于综合与约束。
2.接收起始标志处理
always @(posedge clk or negedge nrst) begin
if(!nrst) begin
rx_start <= 1'b0;
end
else begin
if(rx_in == 1'b0 && rx_start == 1'b0)
rx_start <= 1'b1;
else if(data_count == 5'd9 && count == baud_count - 1)
rx_start <= 1'b0;
else
rx_start <= rx_start;
end
end
当接收到起始位,将接收标志位置位,表明接收开始;当接收到停止位,当波特率计数器计数到最后一位,接收标志复位。
3.波特率计数器
always @(posedge clk or negedge nrst) begin
if(!nrst) begin
count <= 16'd0;
end
else begin
if(rx_start) begin
if(count < baud_count)
count <= count + 16'b1;
else
count <= 16'd0;
end
else
count <= 16'd0;
end
end
当接收标志位值位,波特率计数器开始计数。
4.接收数据计数器
always @(posedge clk or negedge nrst) begin
if(!nrst) begin
data_count <= 5'd0;
end
else begin
if(count == baud_count - 1) begin
if(data_count < 5'd9)
data_count <= data_count + 1'b1;
else
data_count <= 5'd0;
end
else
data_count <= data_count;
end
end
波特率计数器每记满一次,数据计数器加一,数据计数器记录了发送数据的个数。
5.使能信号处理
always @(posedge clk or negedge nrst) begin
if(!nrst) begin
en <= 1'b0;
end
else begin
if(data_count == 5'd9 && count == 16'd0)
en <= 1'b1;
else
en <= 1'b0;
end
end
这里使能信号的处理非常特殊。由于接收是一个串行转并行的过程,使能信号的发出需要在FIFO满以后才可以发出。
当FIFO存满以后使能信号持续一个时钟的高电平告知存满。
6.数据接收
always @(posedge clk or negedge nrst) begin
if(!nrst) begin
data <= 8'b0;
end
else begin
if(count == baud_count / 2)
case(data_count)
5'd1:data[0]<=rx_in;
5'd2:data[1]<=rx_in;
5'd3:data[2]<=rx_in;
5'd4:data[3]<=rx_in;
5'd5:data[4]<=rx_in;
5'd6:data[5]<=rx_in;
5'd7:data[6]<=rx_in;
5'd8:data[7]<=rx_in;
default:data<=data;
endcase
else
data <= data;
end
end
这是串行转并行的核心。
环回测试
在顶层模块中例化了发送与接收两个模块。
module top_module(
input clk,
input nrst,
input rx,
output tx
);
wire [7:0] data;
wire en;
tx_module
#(.baud(921600))
tx_module_inst(
.clk (clk),
.nrst (nrst),
.en (en),
.data (data),
.tx (tx)
);
rx_module
#(.baud(921600))
rx_module_inst(
.clk (clk),
.nrst (nrst),
.rx (rx),
.en (en),
.data (data)
);
endmodule
这里设置了一个较高的波特率921600以测试收发模块。
对于一个通信系统,将发送与接收端短接进行测试,业界称为环回测试(loopback),笔者看到不少朋友都错写成了回环测试,这个说法其实是不正确的。
为了测试效果,发了1000位圆周率,对比接收正确。至此异步串口接收与环回测试成功。