【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解

目录

上一篇博文:【入门学习三】基于 FPGA 使用 Verilog 实现按键状态机代码及原理详解

本文内容:从 PC 上位机通过 COM 发送数据给 FPGA ,FPGA 接收到数据后,将数据回传给 PC 上位机。

一、相关知识

  • 串口通信分为串行通信和并行通信,这里主要将串行通信,因为要用到。
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解
  • 串行通信主要分为同步通信和异步通信。
串行通信 同步通信 带时钟同步信号的数据传输 如 I2C、SPI
异步通信 不带时钟同步信号的数据传输 如 UART、单总线
传输方向 单工
半双工 如 I2C、单总线
全双工 如 SPI、UART
  • 这里我们使用的是 UART 串口通信,它是全双工的异步通信。
  • 它的协议主要由四部分组成
    1. 起始位(1 bit)
    2. 数据位(6/7/8 bit)
    3. 奇偶校验位(1 bit)
    4. 停止位(1 bit)
  • 原理图如下:
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解
  • 上图中接收的时候,当接收到下降沿(起始位)后,开始接收到 8 位的数据(包含奇偶校验位),当接收到上升沿(停止位)后,接收停止。
  • 上图中发送的时候,首先发送一个起始位(下降沿),然后发送连续的 8 位数据,然后再发送一个停止位(上升沿),到此,发送完毕。
  • 串口通讯接口主要使用 TXD 和 RXD 管脚,TXD 发送数据,RXD 接收数据,两个设备的 TXD 和 RXD 要交叉连接。
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解
  • 由于 UART 是串行通信,所以接收的时候需要把总线上的串行数据转换成并行数据存储,而在发送的时候,就需要把并行数据转化成串行数据。

二、模块设计

  • 从上面的讲解看起来串口通信也就那么回事对吧,都熟悉,可是呢,在设计的时候可就没那么简单了。
  • 前面三篇文章,都是我自己画的模块,不那么严谨,这里我直接用 Quartus 生成的模块图。
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解
  • 输入口为时钟 clk 、复位 rst_n 以及数据接收 rx。
  • 串口接收模块从 rx 接收到数据后,在内部实现接收方法,接收完毕后将数据发送给控制模块,也就是由 rx_data[7:0] 连接到 din[7:0],同时发送数据接收完成标志 rx_data_vld 连接到 din_vld。
  • 控制模块主要是对数据进行缓存,串口发送模块发送 tx_rdy 标志信号,表示它要发送数据了,控制模块根据 tx_rdy 发送标志将数据由 dout[7:0] 发送到 tx_din[7:0],以及 dout_vld 发送标志位。
  • 最后串口发送模块将数据发送出去。其中 2’h0 baud_set[1:0] 为波特率标志位。
  • 这样就实现一个串口通讯回传,仔细理清一下思路就简单多了。

三、代码设计

顶层模块设计 uart.v:

  • 顶层模块下有三个子模块:串口接收模块、控制模块、串口发送模块。
`define BAUD_115200
//`define BAUD_9600

module uart(
    input           clk     ,
    input           rst_n   ,

    input           rx      ,
    output          tx      
);

    /*
    波特率9600 19200 38400 115200
    如果定义 BAUD_115200,那么 baud_sel 为 0,则波特率为 115200
    如果定义 BAUD_9600,那么 baud_sel 为 3,则波特率为 9600
    */
    wire    [1:0]   baud_sel;
    `ifdef BAUD_115200
        assign baud_sel = 0; 
    `elsif BAUD_9600
        assign baud_sel = 3; 
    `endif 

    //信号定义  
    wire        [7:0]       rx_data     ;   
    wire                    rx_data_vld ;
    wire        [7:0]       tx_data     ;
    wire                    tx_data_vld ;
    wire                    tx_rdy      ;

    uart_rx u_uart_rx(      //接收模块  串并转换
        .clk        (clk        ),
        .rst_n      (rst_n      ),
        .baud_sel   (baud_sel   ),
        .rx         (rx         ),
        .rx_data    (rx_data    ),
        .rx_data_vld(rx_data_vld)
    );   

    ctrl u_ctrl(
        .clk        (clk        ),
        .rst_n      (rst_n      ),
        .din        (rx_data    ),
        .din_vld    (rx_data_vld),
        .dout       (tx_data    ),
        .dout_vld   (tx_data_vld),
        .tx_rdy     (tx_rdy     )
    );

    uart_tx u_uart_tx(      //发送模块 并串转换
        .clk        (clk        ),
        .rst_n      (rst_n      ),
        .baud_sel   (baud_sel   ),
        .tx_din     (tx_data    ),
        .tx_din_vld (tx_data_vld),
        .tx_rdy     (tx_rdy     ),
        .tx         (tx         )
    );

endmodule   

  • 顶层模块也就是对各个子模块进行接口连接,就没啥好讲的。

3.1 串口接收模块

uart_rx.v

module uart_rx(
    input                   clk         ,       //时钟信号
    input                   rst_n       ,       //复位
    input             [1:0] baud_sel    ,       //波特率标志
    input                   rx          ,       //数据输入口

    output      reg   [7:0] rx_data     ,       //接收的数据
    output      reg         rx_data_vld         //接收完成标志信号
    );

    //信号定义

    reg     [12:0]      cnt_bps     ;       //波特率时钟周期计数器
    reg                 add_flag    ;       //计数器使能信号,数据接收标志
    reg     [3:0]       cnt_bit     ;       //比特计数器
    reg     [9:0]       rx_data_r   ;       //接收数据寄存器

    reg                 rx_r0       ;       //同步
    reg                 rx_r1       ;       //打拍 检测下降沿
    wire                rx_nedge    ;       //接收信号的下降沿
    reg     [12:0]      baud_bps    ;       //根据不同波特率选择的计数值    

    //波特率时钟周期计数器
    always @(posedge clk or negedge rst_n)begin 
        if(!rst_n)begin 
            cnt_bps <= 0;
        end 
        else if(add_flag)begin
            //接收到使能信号,开始计数一个波特周期
            if(cnt_bps == baud_bps - 1)
                cnt_bps <= 0;
            else 
                cnt_bps <= cnt_bps + 1;
        end 
    end 

    //比特计数器
    always @(posedge clk or negedge rst_n) begin
        if(!rst_n)begin 
            cnt_bit <= 0;
        end 
        else if(cnt_bps == baud_bps - 1)begin 
            //计数 0-9 比特,第 0 比特为低电平起始信号
            //第 1-8 比特为数据位
            //第 9 比特为停止位
            if(cnt_bit == 9 || rx_data_r[0] == 1'b1)
                //如果计满到了第 9 比特或者第 0 比特起始位为高电平
                //那么就归零
                //这里起始电平为高电平说明数据接收错误,也要归零
                cnt_bit <= 0;
            else 
                cnt_bit <= cnt_bit + 1;
        end 
    end

    always @(posedge clk or negedge rst_n) begin
        if(!rst_n)begin 
            add_flag <= 1'b0;
        end 
        else if(rx_nedge)begin
            //检测到下降沿,那么使能信号拉高,标志有数据输入
            add_flag <= 1'b1;
        end 
        else if(cnt_bps == baud_bps-1 && (cnt_bit == 9 || rx_data_r[0] == 1'b1))begin
            //如果数据接收了 10 个比特或者起始电平为高(数据接受错误)
            //那么使能信号归零
            add_flag <= 1'b0;
        end 
    end

    //baud_bps
    always @(*) begin
        //根据不同的波特率选择不同的时钟周期
        case(baud_sel)
            0:baud_bps = 434;
            1:baud_bps = 1302;
            2:baud_bps = 2604;
            3:baud_bps = 5208;
            default:baud_bps = 434;
        endcase 
    end

    always @(posedge clk or negedge rst_n) begin
        if(!rst_n)begin 
            rx_r0 <= 0;  
            rx_r1 <= 0;  
        end 
        else begin 
            rx_r0 <= rx;     //同步 
            rx_r1 <= rx_r0;  //打拍
        end 
    end

    //获取到下降沿
    assign rx_nedge = ~rx_r0 & rx_r1;

    //rx_data_r
    always @ (posedge clk or negedge rst_n)begin 
        if(!rst_n)begin 
            rx_data_r <= 0;
        end 
        else if(add_flag & cnt_bps == (baud_bps>>1))begin 
            //如果使能信号且当前为一个波特周期的一半
            //那么将此时 rx 的数据输入电平存储到数据寄存器中
            //rx_data_r <= {rx,rx_data_r[9:1]};     //第一种表达方式
            rx_data_r[cnt_bit] <= rx;               //第二种表达方式
        end 
    end 

    //rx_data
    always @(posedge clk or negedge rst_n)begin 
        if(!rst_n)begin
            rx_data <= 0;
        end 
        else begin
            //将数据寄存器中的值传给 rx_data
            //由 rx_data 传递接收数据给控制模块处理
            rx_data <= rx_data_r[8:1];
        end 
    end

    //rx_data_vld
    always @(posedge clk or negedge rst_n)begin 
        if(!rst_n)begin
            rx_data_vld <= 0;
        end 
        else if(cnt_bps == baud_bps - 1 && cnt_bit == 9)begin 
            //接收完毕后,传递完成接收标志信号
            rx_data_vld <= 1'b1;
        end 
        else begin 
            rx_data_vld <= 1'b0;
        end 
    end

endmodule   

程序执行过程:

  • 假设接收的数据为 h96 ,串口接收的波形图如下(途中所有数字都为 16 进制):
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解
  • 当检测到下降沿后,add_flag 数据使能信号拉高,此时 add_flag 高电平期间就是对 rx 进行数据接收期间。
  • 同时,比特计数器 cnt_bit 不断累加计满 10 个 bite,其中有 1 bit 起始位,8 bit 数据位,1 bit 停止位,这里没有设置校验位。
  • 如果起始位为高电平,说明数据有问题,那么就不会计数,同时 add_flag 拉低。
  • 当数据接收完毕后,rx_data_vld 数据接收完的标志位会拉高 1 个时钟周期,而数据寄存器会不断的接收数据,数值从 0 至 12D,这是因为程序中不断地接收第 cnt_bit 的数据放入寄存器中。
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解
  • 其中 rx_data_r 中的数据变化为([0]为低位,[9]为高位):
rx_data_r[0] rx_data_r[1] rx_data_r[2] rx_data_r[3] rx_data_r[4] rx_data_r[5] rx_data_r[6] rx_data_r[7] rx_data_r[8] rx_data_r[9] 十六进制
0 0 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0 1
0 1 0 0 0 0 0 0 0 0 2
0 0 1 0 0 0 0 0 0 0 4
1 0 0 1 0 0 0 0 0 0 9
0 1 0 0 1 0 0 0 0 0 12
1 0 1 0 0 1 0 0 0 0 25
1 1 0 1 0 0 1 0 0 0 48
0 1 1 0 1 0 0 1 0 0 96
1 0 1 1 0 1 0 0 1 0 12D
  • 最后可以看到从 rx_data_r[8]——[1] 的数据为:10010110,和发出来的数据一致。
  • 图中这里 rx_data 为什么会慢一个周期,这里因为代码中只取 [8:1] 的数据,因为 rx_data_r[0] 为停止位,rx_data_r[9] 为起始位,[8:1] 才为数据位。
  • 而 rx_data_r 第二次接收时,只有 rx_data_r[0] = 1,其余为 0,这里只取 [8:1] ,所以就慢了一个周期。
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解
  • 当数据接收完成后,会将 rx_data_vld 拉高一个周期,标志着数据接收完毕。

3.2 控制模块

ctrl.v:

  • 控制模块中,主要是对接收到的数据做缓存处理。
module ctrl(
    input               clk     ,       //时钟信号
    input               rst_n   ,       //复位
    input       [7:0]   din     ,       //接收到的数据
    input               din_vld ,       //数据有效性,高位有效,低位无效
    input               tx_rdy  ,       //发送准备信号
    output  reg [7:0]   dout    ,       //数据输出
    output  reg         dout_vld        //数据有效性
      
);

    //信号定义
    reg         rd_req  ;       //使能读操作
    wire        wr_req  ;       //使能写操作
    wire        empty   ;       //缓存空信号
    wire        full    ;       //缓存满信号
    wire [7:0]  q_dout  ;       //缓存输出数据
    wire [3:0]  usedw   ;       //缓存数据深度
    reg         rd_flag ;       

    //rd_flag
    always @(posedge clk or negedge rst_n)begin 
        if(!rst_n)begin
            rd_flag <= 0;
        end 
        else if(usedw >= 4)begin
            //如果缓存数据深度大于等于 4
            //拉高读标志
            rd_flag <= 1'b1; 
        end 
        else if(empty)begin
            //如果缓存数据深度小于等于 0
            //拉低读标志 
            rd_flag <= 1'b0; 
        end 
    end

    always @(*) begin
        if(rd_flag && tx_rdy)
            //如果读标志且输出数据标志位
            //那么使能读缓存标志
            rd_req = 1'b1;
        else 
            rd_req = 1'b0;
    end

    //dout  
    always @(posedge clk or negedge rst_n) begin
        if(!rst_n)begin 
            dout <= 0;
        end 
        else begin
            //从缓存中获取数据
            dout <= q_dout;
        end 
    end  
    
    //dout_vld
    always @(posedge clk or negedge rst_n) begin
        if(!rst_n)begin 
            dout_vld <= 1'b0;
        end 
        else begin
            //使能数据输出标志
            dout_vld <= rd_req;
        end 
    end

    //fifo核,先进先出缓存
    fifo	fifo_inst (
        .aclr   (~rst_n  ),
        .clock  (clk     ),
        .data   (din     ),
        .rdreq  (rd_req  ),
        .wrreq  (wr_req  ),
        
        .empty  (empty   ),
        .full   (full    ),
        .q      (q_dout  ),
        .usedw  (usedw   )
	);

    //如果缓存中没满并且有数据进来
    //那么使能读操作
    assign wr_req = full == 1'b0 && din_vld;

endmodule 

  • 控制模块中使用了 FIFO 核,后面会讲述怎么设置 FIFO 核,其实这是一个缓存,将接收到的数据按照以 8 bit 为单位(这个可以自己设置,比如 7 bit、6 bit 等)进行存储,也就是说,类似于队列,先放进来的数据,会先发送出去。
  • 这里设置的深度(最多存储多少个数据)为 4 位 bit,也就是 16 个。
wire [3:0]  usedw   ;       //缓存数据深度
  • 其中 empty 表示缓存为空的信号,空则为高电平,不空则为低电平。
wire        empty   ;       //缓存空信号

程序执行过程:

  • 首先来看时序图:
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解
  • 这里假设 rd_flag 一直保持高电平,表示缓存器中一直不为空且数据个数大于 4,至于为什么大于 4 ,看代码:
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解
  • 控制模块会不断地判断串口接收模块是否传数据过来并且缓存器中是否满了,如果没满并且有数据过来。
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解
  • 那么就会向缓存器中发送 wr_reg 写信号,此时,缓存器就可以将数据保存在内部,单独只讲数据通过 .data 接口进去,它是不会接收的,除非 wr_reg 为高电平才接收。
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解
  • 控制器还会不断判断 tx_rdy 使能信号,这是由串口发送端给的信号,表示它请求发送数据,那么为高电平。
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解
  • 这时让 rd_reg 信号使能,即让缓存器通过接口 p 吐出来一个数据,由控制模块中的 q_dout 接收到。
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解
  • 然后赋值给 dout,同时拉高一个周期的 dout_vld 表示有数据出来了。
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解

3.3 串口发送模块

uart_tx.v

module uart_tx (
    input               clk         ,       //时钟信号
    input               rst_n       ,       //复位
    input       [1:0]   baud_sel    ,       //波特率标志
    input       [7:0]   tx_din      ,       //数据输入
    input               tx_din_vld  ,       //数据输入标志位
    output  reg         tx_rdy      ,       //数据发送准备标志
    output  reg         tx                  //数据发送
);

    reg     [12:0]      cnt_bps     ;       //波特率周期计数器
    reg                 add_flag    ;       //计数器使能信号
    reg     [3:0]       cnt_bit     ;       //比特计数器
    reg     [9:0]       tx_data_r   ;       //接收数据寄存器

    reg     [12:0]      baud_bps    ;       //根据不同波特率选择的计数值    

//计数器
    always @(posedge clk or negedge rst_n) begin
        if(!rst_n)begin 
            cnt_bps <= 0;
        end 
        else if(add_flag)begin
            //计数器开始计数
            //计满一个波特率周期
            if(cnt_bps == baud_bps-1)
                cnt_bps <= 0;
            else 
                cnt_bps <= cnt_bps + 1;
        end 
    end

    always @(posedge clk or negedge rst_n) begin
        if(!rst_n)begin 
            cnt_bit <= 0;
        end 
        else if(cnt_bps == baud_bps-1)begin
            //对比特计数,共 10 位 
            if(cnt_bit == 9)
                cnt_bit <= 0;
            else 
                cnt_bit <= cnt_bit + 1;
        end 
    end

    always @(posedge clk or negedge rst_n) begin
        if(!rst_n)begin 
            add_flag <= 1'b0;
        end 
        else if(tx_din_vld)begin 
            //数据发送使能信号
            add_flag <= 1'b1;
        end 
        else if(cnt_bit == 9 && cnt_bps == baud_bps-1)begin
            //数据发送完毕,则归零处理
            add_flag <= 1'b0;
        end 
    end

//baud_bps
    always @(*) begin
        //波特率周期选择
        case(baud_sel)
            0:baud_bps = 434;
            1:baud_bps = 1302;
            2:baud_bps = 2604;
            3:baud_bps = 5208;
            default:baud_bps = 434;
        endcase 
    end

    always @(posedge clk or negedge rst_n) begin
        if(!rst_n)begin 
            tx_data_r <= 0;
        end 
        else if(tx_din_vld)begin
            //准备要发送的数据
            //在数据两端添加起始位和停止位
            tx_data_r <= {1'b1,tx_din,1'b0};
        end 
    end
    
    always @(posedge clk or negedge rst_n) begin
        if(!rst_n)begin
            tx <= 1'b1;
        end  
        else if(add_flag && cnt_bps == 1)begin
            //发送数据
            tx <= tx_data_r[cnt_bit];
        end 
    end

//tx_rdy
    always @(*) begin
        if(tx_din_vld || add_flag)
            //使能数据准备发送标志
            tx_rdy = 1'b0;
        else 
            tx_rdy = 1'b1;
    end

endmodule

程序执行过程:

  • 先来看时序图:
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解
  • 其中 tx_din_vld 是控制模块输出的 dout_vld 使能信号,表示有数据可以输出,然后将 add_flag 拉高,开始进行数据的输出,然后使用 tx_data_r 将起始位、数据位、停止位按照协议拼接起来。
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解
  • 拼接完成后,由 tx 接口以 1 bit 为单位发送出去。
  • 发送完毕后,将 add_flag 信号拉低,等待下一个 8 bit 数据传进来发送。

四、FIFO 核引用

  • 我使用的是 Quartus 18.1,引用方法和低版本的不太一样,可以自行百度,我之前看过有挺多的,但是就是没有 18.1 的引用方法。
  • 首先打开【Tools】→【IP Catalog】。
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解
  • 搜索框内输入【fifo】,然后双击打开 FIFO。
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解
  • 输入文件名(最后没有斜线),默认选择 Verilog ,然后点击【OK】。
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解
  • 选择数据宽度,本文是用的 8 bit;
  • 数据深度,也就是最多缓存多少个数据;
  • 缓存器的写操作和读操作的时钟频率是否相同,这里选择 Yes 相同,如果是在 fpga 上写数据,然后让 VGA 读数据,那么就要选择 No 不同了,因为两者所需的时钟频率是不同的。
  • 然后点击【Next >】。
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解
  • 选择需要的管脚,这里默认这三个即可,点击【Next >】。
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解
  • 然后一直默认设置,点击【Next >】。
  • 在下图界面时,勾选上倒数第二个文件,然后点击【Finish】完成。
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解
  • 在 fifo 同路径下,可以看到有一个 fifo_inst.v 文件,就是刚刚勾选的文件,打开后如下图所示,全部复制下来粘贴到要调用 fifo 内核的模块文件中,然后修改括号内的变量名即可只用 fifo 核了。
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解

五、管脚定义及结果展示

管脚定义

  • 按照自己的开发板原理图进行管脚配置,开发板不同,管脚也就不同
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解

效果展示

  • 由于今天挺晚了,且没有下载串口调试助手,等明天再补上结果 GIF 图。

附带整个项目文件:https://pan.baidu.com/s/174JjbUw1mYEUKV_VA3-DWQ——提取码:psdo

  • 说明一下,打开工程时是选择 uart/prj/ 目录下的 uart.qpf 工程文件,即可打开整个项目。
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解
    【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解
上一篇:TX-LCN分布式事务管理


下一篇:docker启动报错[Warning] IPv4 forwarding is disabled. Networking will not work解决办法