FPGA串口回环实验

本文将从个人理解的角度,解释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停止接收数据。

串口传输一帧数据

3.FPGA串口回环设计原理

整个实验需要设计三个模块,串口接收、串口发送,以及顶层模块。

模块原理图

串口接收、发送的时序图

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

仿真波形

6.板级验证

在完成功能仿真后,根据开发板原理图进行管脚约束,然后生成比特流文件,连接开发板后烧录即可。(注意,有些zynq开发板是通过跳帽选择PL或PS通信的,记得在验证前,将跳帽连接ch340和PL端的TX、RX)。

打开串口助手,设置波特率为115200,发送数据,如果在接收端接收到相同的数据,说明实验成功。

求学路上,你我共勉(๑•̀ㅂ•́)و✧

  • 5
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
FPGA中实现UART串口回环,需要设计两个模块,分别是uart_rx和uart_tx模块。其中,uart_rx模块负责接收串口数据,而uart_tx模块负责发送串口数据。在uart_rx模块中,可以使用线性序列机的设计方法,通过时序图来描述其功能。时序图中可以清楚地看到数据的传输过程,包括起始位、数据位和结束位的发送和接收。具体的代码实现可以参考引用\[1\]和引用\[2\]中的内容。在设计FPGA时,养成良好的设计习惯非常重要,可以先画出实验的框图,然后对每个小模块进行时序设计。这样可以避免在复杂项目中茫然无措。引用\[3\]中提供了一个实验框图的例子,可以作为参考。 #### 引用[.reference_title] - *1* *3* [基于FPGA的UART回环设计(1)](https://blog.csdn.net/zhangningning1996/article/details/103836599)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [基于FPGA实现uart串口模块(Verilog)--------接收模块及思路总结](https://blog.csdn.net/qq_41467882/article/details/87027577)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值