本文将从个人理解的角度,解释FPGA串口通信的原理,并进行实战演示。
1.写在前面的话
串口通信是初学FPGA必过的一道坎,如果能够在不参考任何资料的情况下自己手搓一套串口回环的代码,Veriolg应该就算基本入门了。下面我将从自己的理解出发,分享实现的方法。
2.预备知识
2.1波特率的意义
以115200波特率为例,它表示每秒传输115200个bit,因此每个bit传输时间为:
1000 000 000/115200=8680ns
如果开发板的时钟频率为50MHz(每个时钟周期为20ns),那么每个bit传输时间为:
8680/20=434个时钟周期
2.2串口助手的原理
推荐这一篇文章,讲的很清楚。
串口是怎样传输数据的_串口数据_Crazzy_M的博客-CSDN博客
简单概括一下,上位机的串口助手会将数据按照设定的波特率一位一位的发送给FPGA(串行),正常来讲在数据的起始要有一个起始位(数据线由高电平变为低电平)告诉FPGA要开始接收数据了,因此在后面设计串口接收时需要有检验下降沿的步骤;结束要有校验位(如果FPGA没有设计校验相关模块,那这一位可以不要)和停止位(数据线由低电平变为高电平)告诉FPGA停止接收数据。
![](https://img-blog.csdnimg.cn/img_convert/12cf759e7d5ccf69a8556f463ac614ea.png)
串口传输一帧数据
3.FPGA串口回环设计原理
整个实验需要设计三个模块,串口接收、串口发送,以及顶层模块。
![](https://img-blog.csdnimg.cn/img_convert/5bbb7d2d0c50b177ddc83c2ec9c7e368.png)
模块原理图
![](https://img-blog.csdnimg.cn/img_convert/9058e257ade061448c7d9c49f4a6a5d5.png)
串口接收、发送的时序图
RX(接收)模块:将数据从串行转化为并行(借助一个缓冲向量),然后传给TX模块
TX(发送)模块:把接收到的并行数据一位一位的发送出去(线性序列机)也即并行化串行
下面用文字来描述整个串口回环的过程(以115200波特率为例),大家可以对照第四部分的源码进行理解:
上位机向FPGA发送8位二进制,注意,串口助手会将设定的数据按照设定的波特率一位一位的给FPGA(115200的波特率下,每秒可以发115200位二进制数据,也就是说 1位起始位+8位数据位 只要9/115200秒就可以发完),而FPGA时钟频率为50M(>>115200),所以虽然发送时间很短,但FPGA处理起来还是绰绰有余的(每位数据有50M/115200=434个时钟周期用来处理)
首先是接收模块,第一步要捕捉下降沿(当然可以在之前多加一个寄存器同步数据防止亚稳态),产生一个脉冲,告诉缓冲区马上要接受数据了,在115200波特率下,一位数据传输需要434个周期,因此要定义一个周期计数器;为了传输稳定,缓冲区每次接收数据最好在每位数据传输的中期(工程上上往往会多次取样确保正确,这里就算了),因此要定义一个中间信号位,告诉缓冲区应该接收数据了(接收时刻);传8位数据后停止,因此需要一个位数计数器。
开始缓冲区reg_data中每一位都是0,现在每个接收时刻都将reg_data右移一位(最低位舍弃),并将此时的reg3变成其最高位这样8个接收时刻后,reg_data[7:0]正好对应 最后一个rx—>第一个rx,这样就做到了把串行的数据rx全部缓冲到reg_data内,这个时候就产生缓冲完毕信号位rx_flag=1,进行整体赋值uart_data <= reg_data,这样就完成了数据的“串转并”。
receive_done导出接收完成的信号,在顶层通过flag与tx模块的uart_in_flag相连,从而使work_en置一,开始发送数据;uart_data将数据缓存器中的数据导出,在顶层中通过data线与tx模块uart_in_data相连,从而将并行数据作为发送模块的输入。
然后是发送模块。仍然采用115200发送,所以同接收模块,434个时钟发送一位,因而需要一个周期计数器(bps_count)、中间信号位(bit_flag)、位数计数器(bit_count)。
当中间信号位为1时将并行的数据,根据此时位数计数器的值,按位发出去。这样就完成了数据的“并转串”,这就是串口回环的整个过程。
4.源码分享
串口顶层模块
module uart_top (
input wire clk,
input wire rst_n,
input wire uart_rx,
output wire uart_tx
// output wire [3:0] led
);
parameter SYSTERM_CLK = 26'd50_000_000; //系统时钟频率
parameter UART_BPS = 17'd115200; //串口波特率
wire flag;
wire [7:0] data;
//assign led = data[3:0];
uart_receive
#(
.SYSTERM_CLK (SYSTERM_CLK ),
.UART_BPS (UART_BPS )
)
u_uart_receive(
.clk (clk ),
.rst_n (rst_n ),
.uart_rx (uart_rx ),
.receive_done (flag ),
.uart_data (data )
);
uart_send
#(
.SYSTERM_CLK (SYSTERM_CLK ),
.UART_BPS (UART_BPS )
)
u_uart_send(
.clk (clk ),
.rst_n (rst_n ),
.uart_in_data (data ),
.uart_in_flag (flag ),
.uart_tx (uart_tx )
);
endmodule //uart_top
串口接收模块
module uart_receive (
input clk,
input rst_n,
input uart_rx,
output reg receive_done,
output reg [7:0] uart_data
);
parameter SYSTERM_CLK = 50_000_000; //系统时钟频率
parameter UART_BPS = 115200; //串口波特率
localparam BPS_COUNT_MAX = SYSTERM_CLK/UART_BPS; //为得到指定波特率
//需要对系统时钟计数BPS_COUNT次
reg [7:0] reg_data;//接受数据缓存
reg [3:0] bit_count;//接收数据时用于计数接收到了多少位
reg [12:0] bps_count;//用于按照时钟计算一个字节的时间
reg start_bit;//检测到起始位的下降沿之后触发一个时钟的高电平
reg reg1 ;
reg reg2 ;
reg reg3 ;
reg bit_flag ;//在一个电平的中间位置产生高电平标志
reg work_en ;//在本标志位高电平时,接受工作开始,在低电平时工作结束
reg rx_flag ;//在数据缓存器存满了之后产生一个高电平
//插入两级寄存器进行数据同步(打两拍),用来消除亚稳态
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
reg1 <= 1'b1;//因为UART的空闲状态是高电平,所以复位时电平要设置为高电平
end
else begin
reg1 <= uart_rx;
end
end
//插入两级寄存器进行数据同步(打两拍),用来消除亚稳态
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
reg2 <= 1'b1;//因为UART的空闲状态是高电平,所以复位时电平要设置为高电平
end
else begin
reg2 <=reg1;
end
end
//插入两级寄存器进行数据同步(打两拍),用来消除亚稳态
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
reg3 <= 1'b1;//因为UART的空闲状态是高电平,所以复位时电平要设置为高电平
end
else begin
reg3 <=reg2;
end
end
//start_bit用来检测起始位的下降沿
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
start_bit <= 1'b0;
end
else if ((reg3)&&(!reg2)) begin
start_bit <= 1'b1;
end
else
start_bit <= 1'b0;
end
//work_en,在本标志位高电平时,接受工作开始,在低电平时工作结束
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
work_en <= 1'b0;
end
else if (start_bit) begin
work_en <= 1'b1;
end
else if ((bit_count == 4'd8) && (bit_flag == 1'b1)) begin
work_en <= 1'b0;
end
else
work_en <= work_en;
end
//bps_count用来计数计算波特率
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
bps_count <= 13'b0;
end
else if ((bps_count == BPS_COUNT_MAX -1) || (work_en == 0)) begin
bps_count <= 13'b0;//只有在工作使能的状态下才会计数
end
else if (work_en == 1) begin
bps_count <= bps_count + 1'b1;
end
else
bps_count <= bps_count;
end
//bit_flag,在一个字节的中点输出一个时钟的高电平
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
bit_flag <= 1'b0;
end
else if (bps_count == BPS_COUNT_MAX/2-1) begin
bit_flag <= 1'b1;
end
else
bit_flag <= 1'b0;
end
//bit_count,用于计数当前接收到了第几bit
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
bit_count <= 4'b0;
end
else if ((bit_count == 4'd8) && (bit_flag == 1'b1)) begin
bit_count <= 4'b0;
end
else if (bit_flag == 1'b1) begin
bit_count <= bit_count + 1'b1;
end
else
bit_count <= bit_count;
end
//reg_data表示接收数据的缓存
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
reg_data <= 8'b0;
end
else if ((bit_flag == 1) && (work_en == 1) && (bit_count <= 4'd8) && (bit_count >= 4'd1)) begin
//reg3<=reg2<=reg1<= uart_rx(打两拍防止亚稳态)
//所以每个接收时刻reg3中依次对应接收到的每位二进制数据
//开始reg_data中每一位都是0
//现在接收时刻都将reg_data右移一位(最低位舍弃),并将此时的reg3变成其最高位
//这样8个接收时刻后,reg_data[7:0]正好对应 最后一个rx——第一个rx
//这样就做到了把串行的数据rx全部缓冲到reg_data内
//这个时候就产生缓冲完毕信号位,进行整体赋值uart_data <= reg_data
//从而起到数据 串转并 的作用
reg_data <= {reg3,reg_data[7:1]};
end
else
reg_data <= reg_data;
end
//rx_flag在接收缓存满了之后输出一个时钟的高电平
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rx_flag <= 1'b0;
end
else if ((bit_count == 4'd8) && (bit_flag == 1'b1)) begin
rx_flag <= 1'b1;
end
else
rx_flag <= 1'b0;
end
//uart_data将数据缓存器中的数据导出
//uart_data在顶层中通过data线与tx模块uart_in_data相连
//再将数据 并转串 一位位发送出去
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
uart_data <= 8'b0;
end
else if (rx_flag == 1'b1) begin
uart_data <= reg_data;
end
else
uart_data <= uart_data;
end
//receive_done导出接收完成的信号
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
receive_done <= 1'b0;
end
else if (rx_flag == 1'b1) begin
receive_done <= 1'b1;
end
else
receive_done <= 1'b0;
end
endmodule //uart_receive
串口发送模块
module uart_send (
input wire clk,
input wire rst_n,
input wire [7:0] uart_in_data,
input wire uart_in_flag,
output reg uart_tx
);
parameter SYSTERM_CLK = 50_000_000; //系统时钟频率
parameter UART_BPS = 115200; //串口波特率
localparam BPS_COUNT_MAX = SYSTERM_CLK/UART_BPS; //为得到指定波特率
//需要对系统时钟计数BPS_COUNT次
reg [12:0] bps_count;//用于按照时钟计算一个字节的时间
reg [3:0] bit_count;//接收数据时用于计数接收到了多少位
reg bit_flag ;//在一个电平的中间位置产生高电平标志
reg work_en ;//在本标志位高电平时,接受工作开始,在低电平时工作结束
//bps_count波特率计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
bps_count <= 13'b0;
end
else if ((bps_count ==BPS_COUNT_MAX - 1'b1) || (work_en == 1'b0)) begin
//-1'b1是个小细节,因为bps_count=0也站一个时钟周期
bps_count <= 13'b0;
end
else if (work_en == 1'b1) begin
bps_count <= bps_count + 1'b1;
end
else
bps_count <= bps_count;
end
//work_en发送工作使能
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
work_en <= 1'b0;
end
else if ((bit_count == 4'd9) && (bit_flag == 1'b1)) begin
work_en <= 1'b0;
end
else if (uart_in_flag == 1'b1) begin
work_en <= 1'b1;
end
else
work_en <= work_en;
end
//bit_flag每一个bit的信号
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
bit_flag <= 1'b0;
end
else if (bps_count == BPS_COUNT_MAX/2 - 1'b1) begin
bit_flag <= 1'b1;
end
else
bit_flag <= 1'b0;
end
//bit_count字节计数
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
bit_count <= 4'b0;
end
else if ((bit_flag == 1'b1) && (work_en == 1'b1)) begin
bit_count <= bit_count + 1'b1;
end
else if ((bit_count == 4'd9) && (bit_flag == 1'b1)) begin
bit_count <= 4'b0;
end
else
bit_count <= bit_count;
end
//uart_tx
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
uart_tx <= 1'b1;//串口传输,空闲时为高电平
end
else if (bit_flag == 1'b1) begin
case (bit_count)
0: uart_tx <= 1'b0 ;
1: uart_tx <= uart_in_data[0];
2: uart_tx <= uart_in_data[1];
3: uart_tx <= uart_in_data[2];
4: uart_tx <= uart_in_data[3];
5: uart_tx <= uart_in_data[4];
6: uart_tx <= uart_in_data[5];
7: uart_tx <= uart_in_data[6];
8: uart_tx <= uart_in_data[7];
9: uart_tx <= 1'b1 ;
default uart_tx <= 1'b1 ;
endcase
end
end
endmodule //uart_send
5.功能仿真
编写testbench如下,用Vivado软件自带的功能仿真,对串口回环设计进行检验。
`timescale 10ns / 1ps //粘贴前把初始化的时间刻度删掉
module tb;
reg clk;
reg rst_n;
reg uart_rx;
wire uart_tx;
uart_top uart_top_u(
.clk (clk),
.rst_n (rst_n),
.uart_rx (uart_rx),
.uart_tx (uart_tx)
);
initial begin
clk=0;
rst_n=0;
#10 rst_n=1;
end
always#1 clk=~clk;//使时钟周期为20ns
initial begin
#20 uart_rx=0;
#868 uart_rx=1;//每434个时钟周期传一个比特
#868 uart_rx=1;
#868 uart_rx=0;
#868 uart_rx=1;
#868 uart_rx=1;
#868 uart_rx=0;
#868 uart_rx=1;
#868 uart_rx=0;
#868 uart_rx=1;
end
endmodule
![](https://img-blog.csdnimg.cn/img_convert/3a3eecccf919ee8290f92bd895fcbebc.png)
仿真波形
6.板级验证
在完成功能仿真后,根据开发板原理图进行管脚约束,然后生成比特流文件,连接开发板后烧录即可。(注意,有些zynq开发板是通过跳帽选择PL或PS通信的,记得在验证前,将跳帽连接ch340和PL端的TX、RX)。
打开串口助手,设置波特率为115200,发送数据,如果在接收端接收到相同的数据,说明实验成功。
求学路上,你我共勉(๑•̀ㅂ•́)و✧