UART串口简介

 串行通信分为两种方式:同步串行通信和异步串行通信。同步串行通信要求通信双方使用同一时钟,异步则没有这个要求。UART是一种采用异步串行通信方式的通用异步收发传输器(universal asynchronous receiver-transmitter),它在发送数据时将接收到的串行数据转成并行数据。

通信时序

在这里插入图片描述

 如上图所示,一帧数据由起始位(低电平),数据位(5/6/7/8),校验位(奇/偶),停止位(高电平1/1.5/2)构成,数据传输的速率用波特率(每秒传输的比特数bps)来表示。
  在以往的单片机开发中,串口往往作为一种外设集成进单片机中,我们只需要通过配置寄存器来确定通信的参数即可。当然,也可以通过软件模拟通信协议。而这里,我们通过FPGA来实现纯数字电路上的串口通信,这里的任务即上位机通过串口发送数据给FPGA,然后FPGA实现回传。

顶层模块设计

在这里插入图片描述

上图为顶层模块的设计框图,系统时钟和复位信号不必多说,顶层模块有一个串口的接收信号(传给串口接收子模块)和一个串口的发送信号(传给串口发送子模块)。
  在顶层模块中,例化了串口串口接收子模块和串口发送子模块,分别用来处理串口接收到的数据和串口需要发送的数据。

//uart串口收发程序,顶层模块
module uart_top (
    input     sys_clk,
    input     sys_rst_n,
    input        uart_rxd,    //FPGA串口接收端
    output    uart_txd        //FPGA串口发送端
);

parameter CLK_FREQ = 50000000;
parameter UART_BPS = 115200;

//模块之间的交互一定要用线网变量
wire [7:0]    uart_data;
wire            uart_done;

//例化串口接收子模块
uart_recv #(
    .CLK_FREQ        (CLK_FREQ),
    .UART_BPS        (UART_BPS)
)u_uart_recv(
    .sys_clk            (sys_clk),
    .sys_rst_n        (sys_rst_n),
    .uart_rxd        (uart_rxd),        //接收端口的信号
    .uart_done        (uart_done),    //接收完成
    .uart_data        (uart_data)        //接收的数据,并行
);

//例化串口发送子模块
uart_send #(
    .CLK_FREQ        (CLK_FREQ),
    .UART_BPS        (UART_BPS)
)u_uart_send(
    .sys_clk            (sys_clk),
    .sys_rst_n        (sys_rst_n),
    
    .uart_en            (uart_done),    //发送使能信号
    .uart_data        (uart_data),    //需要发送的串口数据,并行
    .uart_txd        (uart_txd),        //串口发送端
);

endmodule

串口接收子模块设计

时钟信号、复位信号、串口接收信号均从顶层模块引入
通过信号线上的下降沿确定起始位,开始接收(下降沿通过两个变量的阻塞赋值实现)
开始接收后,需要对时钟数进行计数,到达规定时钟数进入下一个bit的接收,此时bit数也要+1
每一次在时钟数的中间在信号线上进行采样,注意这里的时钟均指一个bit内的时钟
采用一个寄存器专门存放接收到的数据,即串转并
接收到第9个比特(停止位)时跳出,认为接受完成,并输出接收完成信号

//串口接收子模块
module uart_recv (
    input                     sys_clk,
    input                     sys_rst_n,
    input                    uart_rxd,        //接收端口的信号
    output    reg                uart_done,        //接收完成
    output    reg[7:0]        uart_data        //接收的数据,并行
);

parameter     CLK_FREQ = 50000000;            //时钟频率,50M
parameter    UART_BPS = 115200;                //波特率,bit/s
parameter    BPS_CNT = CLK_FREQ/UART_BPS;    //每个bit传输需要的时钟数

reg                uart_rxd_d0;
reg                uart_rxd_d1;
reg    [15:0]        clk_cnt;        //时钟数,最大到BPS_CNT
reg    [3:0]        rx_cnt;            //接收数据bit的个数,最大到9
reg    [7:0]        rxdata;            //接收的数据寄存器
reg                rx_flag;        //接收数据的标志位


wire                start_flag;    //起始位开始的标志位

//起始位触发(捕获下降沿),d1延后d0一个周期,所以当d0为0,d1为1,意味着下降沿
assign start_flag = uart_rxd_d1 & (~uart_rxd_d0);

//d1和d0都是数据线上信号,保证d1延后d0一个时钟周期
always @(posedge sys_clk or negedge sys_rst_n) begin
    if (~sys_rst_n) begin    //复位时数据信号清0
        uart_rxd_d0 <= 1'b0;
        uart_rxd_d1 <= 1'b0;
    end
    else begin
        uart_rxd_d0 <= uart_rxd;
        uart_rxd_d1 <= uart_rxd_d0;
    end
        
end

//确立信号接收标志位
always @(posedge sys_clk or negedge sys_rst_n) begin
    if (~sys_rst_n)
    rx_flag <= 1'd0;
    else begin
        if (start_flag)
            rx_flag <= 1'd1;
        else if ((rx_cnt == 4'd9) && (clk_cnt == BPS_CNT/2))    //判断停止位,在第9个bit的中间时钟处
            rx_flag <= 1'd0;
        else
            rx_flag <= rx_flag;
    end
end

//对时钟进行计数(最大到BPS_CNT),对接收的bit数进行计数,
//根据这两个数判断停止位
always @(posedge sys_clk or negedge sys_rst_n) begin
    if (~sys_rst_n) begin                //复位时计数都清零
        rx_cnt <= 4'd0;
        clk_cnt <= 16'd0;
    end
    else if (rx_flag) begin                //开始数据传输
        if (clk_cnt < BPS_CNT)
            clk_cnt <= clk_cnt + 1'b1;
        else begin                        //经过了一个bit的时钟数
            clk_cnt <= 1'b0;            //时钟数清零
            rx_cnt <= rx_cnt + 1'b1;    //bit数加1,1bit即从第一个数据位开始,包含了起始位
        end
    end    
    else begin
        clk_cnt <= 16'd0;
        rx_cnt <= 4'd0;
    end
end

//寄存接收数据
always @(posedge sys_clk or negedge sys_rst_n) begin
    if (~sys_rst_n)
        rxdata <= 8'd0;
    else if (rx_flag) 
        if (clk_cnt == BPS_CNT/2) begin    //在一个比特的时钟中间处对数据进行接收
            case (rx_cnt)                        //寄存接收数据
                4'd1:    rxdata[0] <= uart_rxd_d1;
                4'd2:    rxdata[1] <= uart_rxd_d1;
                4'd3:    rxdata[2] <= uart_rxd_d1;
                4'd4:    rxdata[3] <= uart_rxd_d1;
                4'd5:    rxdata[4] <= uart_rxd_d1;
                4'd6:    rxdata[5] <= uart_rxd_d1;
                4'd7:    rxdata[6] <= uart_rxd_d1;
                4'd8:    rxdata[7] <= uart_rxd_d1;
                default:    ;
            endcase
        end
        else
        rxdata <= rxdata;
    else
        rxdata <= 8'd0;
end

//判断接收数据是否完成以及把数据赋给输出信号
always @(posedge sys_clk or negedge sys_rst_n) begin
    if (~sys_rst_n) begin
        uart_done <= 1'd0;
        uart_data <= 8'd0;
    end
    else if (rx_cnt == 4'd9) begin
        uart_data <= rxdata;
        uart_done <= 1'd1;
    end
    else begin
        uart_data <= 8'd0;
        uart_done <= 1'd0;
    end
end

endmodule

串口发送子模块设计

时钟信号、复位信号来自顶层模块,需要发送的数据(并行)以及发送使能信号来自串口接收子模块
找到发送使能信号的上升沿,确立起始位,开始发送
发送的时候同样需要对时钟进行计数,达到规定时钟数后进入下一个bit,当然也需要对bit进行计数
发送时根据当前bit数确定发送数据的哪一位,然后对应拉高或者拉低Tx信号线,这里需要注意,起始位和停止位都需要发送
发送完9个比特后拉高信号线

//串口发送子模块
module uart_send(
    input             sys_clk,
    input             sys_rst_n,
    
    input                uart_en,            //发送使能信号
    input     [7:0]    uart_data,        //需要发送的串口数据,并行
    output reg        uart_txd            //串口发送端
);

parameter     CLK_FREQ = 50000000;                //时钟频率,50M
parameter    UART_BPS = 115200;                //波特率,bit/s
parameter    BPS_CNT = CLK_FREQ/UART_BPS;    //每个bit传输需要的时钟数

reg                uart_en_d0;            
reg                uart_en_d1;
reg    [15:0]    clk_cnt;                //时钟计数寄存器
reg    [3:0]        tx_cnt;                //发送bit计数寄存器
reg                tx_flag;                //发送标志位
reg    [7:0]        tx_data;                //寄存发送数据

wire                en_flag;                //发送使能位

//找到发送使能信号的上升沿,将发送使能位置位
assign en_flag = uart_en_d0 & (~uart_en_d1);

//确定d0和d1,d1滞后d0一个时钟周期
always @(posedge sys_clk or negedge sys_rst_n) begin
    if (~sys_rst_n) begin
        uart_en_d0 <= 1'd0;
        uart_en_d1 <= 1'd0;
    end
    else begin
        uart_en_d0 <= uart_en;
        uart_en_d1 <= uart_en_d0;
    end
end

//确定发送标志位
always @(posedge sys_clk or negedge sys_rst_n) begin
    if (~sys_rst_n)
        tx_flag <= 1'd0;
    else if (en_flag) begin
        tx_flag <= 1'd1;
        tx_data <= uart_data;
    end
    else begin
        if ((tx_cnt == 4'd9) && (clk_cnt == BPS_CNT/2))    begin//已经发送了9比特数据,则发送结束,标志位清零
            tx_flag <= 1'd0;
            tx_data <= 8'd0;
        end
        else begin
            tx_flag <= tx_flag;
            tx_data <= tx_data;    
        end
    end
end

//对时钟和发送bit进行计数
always @(posedge sys_clk or negedge sys_rst_n) begin
    if (~sys_rst_n) begin
        clk_cnt <= 16'd0;
        tx_cnt <= 4'd0;
    end
    else if (tx_flag) begin
        if (clk_cnt < BPS_CNT - 1'd1)
            clk_cnt <= clk_cnt + 1'd1;
        else begin
            clk_cnt <= 16'd0;
            tx_cnt <= tx_cnt + 1'd1;
        end
    end
end

//并转串,将数据放到串口发送信号线上
always @(posedge sys_clk or negedge sys_rst_n) begin
    if (~sys_rst_n) 
        uart_txd <= 1'd1;
    else if (tx_flag) 
        case (tx_cnt)
            4'd0:    uart_txd <= 1'd0;
            4'd1:    uart_txd <= tx_data[0];
            4'd2:    uart_txd <= tx_data[1];
            4'd3:    uart_txd <= tx_data[2];
            4'd4:    uart_txd <= tx_data[3];
            4'd5:    uart_txd <= tx_data[4];
            4'd6:    uart_txd <= tx_data[5];
            4'd7:    uart_txd <= tx_data[6];
            4'd8:    uart_txd <= tx_data[7];
            4'd9:    uart_txd <= 1'd1;
            default:;
        endcase
    else
        uart_txd <= 1'd1;
end

endmodule

结果

直接放抓到的波形图:

在这里插入图片描述

时序图很漂亮,接收子模块通过下降沿触发后开始接收数据,接收完后发出完成信号,发送子模块根据这个完成信号的上升沿,发送数据。
  在写FPGA的程序时,跟以往的编程差别非常之大,Verilog的并行执行要求写程序时拥有更加严谨的逻辑。现在还处于FPGA的入门阶段,只能隐隐约约地从代码背后感受到一点数字电路的轮廓。
  时隔一年多再次回到了硬件编程,需求也今非昔比了,现在的目标是将通信算法移植到硬件平台上,任重而道远,慢慢积累吧!

Ref:
《正点原子新起点FPGA教程》