我是靠谱客的博主 糊涂方盒,这篇文章主要介绍Verilog——基于状态机的串口收发模块的实现基于状态机的串口收发模块的实现,现在分享给大家,希望可以做个参考。

基于状态机的串口收发模块的实现

前言

作为一个FPGA的初学者,实现一个完美的串口收发功能一直是心中的一个小小愿望。之前看过其他许多实现串口功能的Verilog代码,感觉它们都有或大或小的缺陷,例如有的代码看起来十分不严谨、结构混乱,有些代码可读性差,想要把它们读懂做一些修改十分困难。自己动手完成了一个较为满意的串口接收、发送模块,并在黑金的AX309试验板上进行了测试,接收到一帧数据后能够完整地将数据发回,数据帧长度达到256字节未出现任何错误。代码结构清晰,相信对串口协议较为了解的伙伴们会很容易读懂。

串口发送

进行串口调试,首先应实现的串口发送模块。为了提高程序可读性,使用状态机进行编程。闲话不多说,直接看源代码:

module uart_tx
#(
    parameter           CLK_FREQ    =   50  ,//时钟频率(Mhz)
    parameter           BAUD_RATE   =   9600 //波特率(bps)
)
(
    input               clk                 ,
    input               rst_n               ,
    output reg          tx_txd              ,//发送端口
    input               tx_trig             ,//启动发送,高电平有效
    input       [7:0]   tx_data             ,//发送字节,tx_trig高电平输入
    output reg          tx_idle              //空闲标志位,1:空闲态,0:忙碌态
);

localparam      BAUD_NUM    =   (CLK_FREQ*1000_000) / BAUD_RATE - 1 ;//波特率对应计数值

reg [20:0]      baud_cnt        ;//波特率计数器
reg [ 7:0]      tx_data_r       ;//tx_data缓存

//状态机参数
localparam  S_IDLE      =       0;
localparam  S_START     =       1;
localparam  S_BIT0      =       2;
localparam  S_BIT1      =       3;
localparam  S_BIT2      =       4;
localparam  S_BIT3      =       5;
localparam  S_BIT4      =       6;
localparam  S_BIT5      =       7;
localparam  S_BIT6      =       8;
localparam  S_BIT7      =       9;
localparam  S_STOP       =      10;
reg [4:0]           state;//状态机

//tx_data_r, 输入tx_data
always @(posedge clk) begin
    if(tx_trig && tx_idle)
        tx_data_r <= tx_data;
end

//state, 状态机
always@(posedge clk or negedge rst_n) begin
   if(~rst_n) begin
       state <= S_IDLE;
       tx_txd <= 1'b1;
       tx_idle <= 1'b1;
    end
    else begin
        case(state)
            S_IDLE:
                if(tx_trig) begin
                    state <= S_START;
                    tx_idle <= 1'b0;
                end
            S_START: begin
                tx_txd <= 1'b0;
                if(baud_cnt == BAUD_NUM)
                    state <= S_BIT0;
            end
            S_BIT0: begin
                tx_txd <= tx_data_r[0];
                if(baud_cnt == BAUD_NUM)
                    state <= S_BIT1;
            end
            S_BIT1: begin
                tx_txd <= tx_data_r[1];
                if(baud_cnt == BAUD_NUM)
                    state <= S_BIT2;
            end
            S_BIT2: begin
                tx_txd <= tx_data_r[2];
                if(baud_cnt == BAUD_NUM)
                    state <= S_BIT3;
            end
            S_BIT3: begin
                tx_txd <= tx_data_r[3];
                if(baud_cnt == BAUD_NUM)
                    state <= S_BIT4;
            end
            S_BIT4: begin
                tx_txd <= tx_data_r[4];
                if(baud_cnt == BAUD_NUM)
                    state <= S_BIT5;
            end
            S_BIT5: begin
                tx_txd <= tx_data_r[5];
                if(baud_cnt == BAUD_NUM)
                    state <= S_BIT6;
            end
            S_BIT6: begin
                tx_txd <= tx_data_r[6];
                if(baud_cnt == BAUD_NUM)
                    state <= S_BIT7;
            end
            S_BIT7: begin
                tx_txd <= tx_data_r[7];
                if(baud_cnt == BAUD_NUM)
                    state <= S_STOP;
            end
            S_STOP: begin
                tx_txd <= 1'b1;
                if(baud_cnt == BAUD_NUM) begin
                    state <= S_IDLE;
                    tx_idle <= 1'b1;
                end
            end
            default:
                state <= S_IDLE;
        endcase
    end
end

//baud_cnt, 波特率计数
always @(posedge clk or negedge rst_n) begin
    if(~rst_n)
        baud_cnt <= 'd0;
    else if(baud_cnt == BAUD_NUM)
        baud_cnt <= 'd0;
    else if(state >= S_START)
        baud_cnt <= baud_cnt + 'd1;
    else
        baud_cnt <= 'd0;
end

endmodule

先看看串口发送模块的对外接口:

  1. 参数 CLK_FREQ、BAUD_RATE 可以极为方便地实现输入 时钟频率 和 波特率 的修改;

  2. clk为输入时钟,输入时钟频率应和 CLK_FREQ 参数一致;

  3. rst_n为复位端口,低电平复位;

  4. tx_txd为串口发送端口;

  5. tx_trig为串口发送的触发端口,当该管脚为高电平且串口空闲,即 tx_idle == 1 时将触发一次串口发送;

  6. tx_data为串口发送模块的数据输入,本程序仅支持8bit位宽数据的发送;

  7. tx_idle为串口空闲状态标志,1:串口空闲态,0:串口忙碌态。

模块内首先根据输入的两个参数计算了指定波特率对应的计数值BAUD_NUM,注意由于输入参数CLK_FREQ以MHz为单位,因此计算时进行了 *1000_000 操作。模块内部实现代码注意以下几点:

  1. tx_data_r在有效的发送触发信号到来时,将tx_data缓存,可避免串口发送过程中外部数据不稳定带来的问题;

  2. 状态机按照串口通信协议分为空闲态(S_IDLE)、起始位(S_START)、bit0(S_BIT0) ~ bit7(S_BIT7)、停止位(S_STOP),共11个状态。tx_txd在起始位状态输出0,各个数据位状态输出对应数据位的数值,停止位输出1,停止位之后进入空闲态,等待下一次发送触发操作到来;

  3. 状态机的状态跳转完全由状态机外部的baud_cnt计数器操控,这样状态机跳转耗费的时间不会对tx_txd的输出波形造成影响;

  4. tx_idle在进入起始位时置’0’,停止位结束后置’1’;

串口发送模块的测试代码如下:

module tb_uart_tx;

reg               clk                 ;
reg               rst_n               ;
wire              tx_txd              ;//发送端口
reg               tx_trig             ;//启动发送,高电平有效
reg       [7:0]   tx_data             ;//发送字节,tx_trig高电平输入
wire              tx_idle             ;//空闲标志位,1:空闲态,0:忙碌态

uart_tx
#(
    .CLK_FREQ           (50                 ),//时钟频率(Mhz)
    .BAUD_RATE          (460800             ) //波特率(bps)
)
uart_tx_inst
(
    .clk                (clk                ),
    .rst_n              (rst_n              ),
    .tx_txd             (tx_txd             ),//发送端口
    .tx_trig            (tx_trig            ),//启动发送,高电平有效
    .tx_data            (tx_data            ),//发送字节,tx_trig高电平输入
    .tx_idle            (tx_idle            ) //空闲标志位,1:空闲态,0:忙碌态
);

initial begin
    clk = 0;
    rst_n = 0;
    tx_trig = 0;
    tx_data = 8'h00;
    #100;
    rst_n = 1;

    #100;
    uart_tx_send(8'h55);
end
always #10 clk = ~clk;

task uart_tx_send(
    input [7:0] data
);
    begin
        tx_trig = 1;
        tx_data = data;
        #20;
        tx_trig = 0;
    end
endtask

endmodule

串口接收

串口接收模块的源代码如下:

module uart_rx
#(
    parameter           CLK_FREQ    =   50  ,//时钟频率(Mhz)
    parameter           BAUD_RATE   =   9600 //波特率(bps)
)
(
    input               clk         ,
    input               rst_n       ,
    input               rx_rxd      ,//接收端口
    output   reg [7:0]  rx_data     ,//接收1字节数据
    output   reg        rx_flag      //1字节数据接收完成
);

localparam  BAUD_NUM        =   (CLK_FREQ*1000_000) / BAUD_RATE - 1 ;//波特率对应计数值
localparam  HALF_BAUD_NUM   =   BAUD_NUM / 2 - 1;// 波特率对应计数值/2
reg         rx_rxd_r        ;
reg         rx_rxd_rr       ;
wire        falling         ;
reg [20:0]  baud_cnt        ;

//state, 状态机
localparam  S_IDLE      =       0;
localparam  S_START     =       1;
localparam  S_BIT0      =       2;
localparam  S_BIT1      =       3;
localparam  S_BIT2      =       4;
localparam  S_BIT3      =       5;
localparam  S_BIT4      =       6;
localparam  S_BIT5      =       7;
localparam  S_BIT6      =       8;
localparam  S_BIT7      =       9;
localparam  S_STOP      =      10;
reg [4:0]           state;

//rx_rxd_r, rx_rxd_rr, 打节拍
always @(posedge clk) begin
    rx_rxd_r <= rx_rxd;
    rx_rxd_rr <= rx_rxd_r;
end
//falling, 检测rx_rxd下降沿
assign falling = ~rx_rxd_r & rx_rxd_rr;

always@(posedge clk or negedge rst_n) begin
   if(~rst_n) begin
       state <= S_IDLE;
       rx_data <= 8'h00;
       rx_flag <= 1'b0;
    end
    else begin
        case(state)
            S_IDLE: begin
                rx_flag <= 1'b0;
                if(falling)
                    state <= S_START;
            end
            S_START:
                if(baud_cnt == BAUD_NUM)
                    state <= S_BIT0;
            S_BIT0:
                if(baud_cnt == HALF_BAUD_NUM)
                    rx_data[0] <= rx_rxd_r;
                else if(baud_cnt == BAUD_NUM)
                    state <= S_BIT1;
            S_BIT1:
                if(baud_cnt == HALF_BAUD_NUM)
                    rx_data[1] <= rx_rxd_r;
                else if(baud_cnt == BAUD_NUM)
                    state <= S_BIT2;
            S_BIT2:
                if(baud_cnt == HALF_BAUD_NUM)
                    rx_data[2] <= rx_rxd_r;
                else if(baud_cnt == BAUD_NUM)
                    state <= S_BIT3;
            S_BIT3:
                if(baud_cnt == HALF_BAUD_NUM)
                    rx_data[3] <= rx_rxd_r;
                else if(baud_cnt == BAUD_NUM)
                    state <= S_BIT4;
            S_BIT4:
                if(baud_cnt == HALF_BAUD_NUM)
                    rx_data[4] <= rx_rxd_r;
                else if(baud_cnt == BAUD_NUM)
                    state <= S_BIT5;
            S_BIT5:
                if(baud_cnt == HALF_BAUD_NUM)
                    rx_data[5] <= rx_rxd_r;
                else if(baud_cnt == BAUD_NUM)
                    state <= S_BIT6;
            S_BIT6:
                if(baud_cnt == HALF_BAUD_NUM)
                    rx_data[6] <= rx_rxd_r;
                else if(baud_cnt == BAUD_NUM)
                    state <= S_BIT7;
            S_BIT7:
                if(baud_cnt == HALF_BAUD_NUM)
                    rx_data[7] <= rx_rxd_r;
                else if(baud_cnt == BAUD_NUM)
                    state <= S_STOP;
            S_STOP:
                if(baud_cnt == BAUD_NUM) begin
                    rx_flag <= 1'b1;
                    state <= S_IDLE;
                end
            default:
                state <= S_IDLE;
        endcase
    end
end

//baud_cnt, 波特率计数器
always @(posedge clk or negedge rst_n) begin
    if(~rst_n)
        baud_cnt <= 'd0;
    else if(baud_cnt == BAUD_NUM)
        baud_cnt <= 'd0;
    else if(state >= S_START)
        baud_cnt <= baud_cnt + 'd1;
    else
        baud_cnt <= 'd0;
end

endmodule

串口接收模块与发送模块结构类似,先看对外接口:

  1. rx_rxd为串口接收端口;

  2. rx_data为接收到的一个字节数据,rx_flag为1时有效;

  3. rx_flag为接收数据输出标志,rx_flag == 1时输出数据rx_data有效。

内部代码注意事项:

  1. rx_rxd直接从外部物理接口输入,因此需要通过打节拍操作统一时钟;

  2. 串口空闲态rx_rxd为1,起始位rx_rxd为0,因此rx_rxd的下降沿可以作为串口接收操作开始的判断标志;

  3. 在状态机对应数据位中进行rx_data各个数据位的采集,但需要注意的是,在波特率计数器baud_cnt == HALF_BAUD_NUM,即每一位数据信号的正中央进行数据采集,因为此时的数据信号最稳定。

串口接收模块的测试代码如下:

`timescale 1ns/1ps
module tb_uart_rx;

reg             clk         ;
reg             rst_n       ;
reg             rx_rxd      ;//接收端口
wire [7:0]      rx_data     ;//接收1字节数据
wire            rx_flag     ;//1字节数据接收完成
//
localparam      CLK_FREQ    =   50          ;//时钟频率(Mhz)
localparam      BAUD_RATE   =   460800      ;//波特率(bps)
localparam      BAUD_TIME   =   1000_000_000 / BAUD_RATE;//波特率对应周期,单位(ns)
//实例化串口接收
uart_rx
#(
    .CLK_FREQ    (CLK_FREQ  ),//时钟频率(Mhz)
    .BAUD_RATE   (BAUD_RATE ) //波特率(bps)
)
uart_rx_inst
(
    .clk         (clk       ),
    .rst_n       (rst_n     ),
    .rx_rxd      (rx_rxd    ),//接收端口
    .rx_data     (rx_data   ),//接收1字节数据
    .rx_flag     (rx_flag   ) //1字节数据接收完成
);

//初始化
initial begin
    clk = 0;
    rst_n = 0;
    rx_rxd = 1;
    #100;
    rst_n = 1;
    
    rxd_1_byte(8'h55);
    rxd_1_byte(8'hff);
    rxd_1_byte(8'h00);
    rxd_1_byte(8'h19);
    rxd_1_byte(8'h38);
    rxd_1_byte(8'he6);
end
//时钟
always #10 clk = ~clk;

//任务:接收1字节时序
task rxd_1_byte(
    input [7:0] rxd_data
);
    integer i;
    begin
        //起始位
        rx_rxd = 0;
        #BAUD_TIME;
        //数据
        for(i = 0; i < 8; i = i + 1) begin
            rx_rxd = rxd_data[i];
            #BAUD_TIME;
        end
        //停止位
        rx_rxd = 1;
        #BAUD_TIME;
    end
endtask

endmodule

串口接收数据发回

检测串口是否工作正常最简单有效的方式就是让串口把接收到的数据发送回去。这里实现了top模块,内部调用了串口接收模块uart_tx和串口发送模块uart_tx。具体代码如下:

module top
(
    input           clk         ,
    input           rst_n       ,
    input           uart_rxd    ,
    output          uart_txd
);

localparam      CLK_FREQ    =   50          ;//时钟频率(Mhz)
localparam      BAUD_RATE   =   460800      ;//波特率(bps)
localparam      BAUD_NUM    =   (CLK_FREQ*1000_000) / BAUD_RATE - 1 ;//波特率对应计数值// 波特率对应计数值/2
reg             tx_trig     ;
wire[ 7:0]      tx_data     ;
wire            tx_idle     ;
wire[ 7:0]      rx_data     ;
wire            rx_flag     ;

reg [20:0]      cnt         ;

assign tx_data = rx_data;

//state, 状态机:rx_flag置1, tx_trig置1一个比特位时长; 保证串口接收与发送的衔接
reg [2:0]       state       ;
always @(posedge clk or negedge rst_n) begin
    if(~rst_n) begin
        state <= 3'd0;
        cnt <= 'd0;
        tx_trig <= 1'b0;
    end
    else
        case(state)
            3'd0:
                if(rx_flag) begin
                    state <= 'd1;
                    tx_trig <= 1'b1;
                end
            3'd1: begin
                cnt <= cnt + 1;
                if(cnt == BAUD_NUM) begin
                    state <= 5'd0;
                    cnt <= 'd0;
                    tx_trig <= 1'b0;
                end
            end
        endcase
end


//uart_tx 实例化
uart_tx
#(
    .CLK_FREQ       (CLK_FREQ       ),//时钟频率(Mhz)
    .BAUD_RATE      (BAUD_RATE      ) //波特率(bps)
)
uart_tx_inst
(
    .clk            (clk            ),
    .rst_n          (rst_n          ),
    .tx_txd         (uart_txd       ),//发送端口
    .tx_trig        (tx_trig        ),//启动发送,高电平有效
    .tx_data        (tx_data        ),//发送字节,tx_trig高电平输入
    .tx_idle        (tx_idle        ) //空闲标志位,1:空闲态,0:忙碌态
);

//uart_rx 实例化
uart_rx
#(
    .CLK_FREQ       (CLK_FREQ       ),//时钟频率(Mhz)
    .BAUD_RATE      (BAUD_RATE      ) //波特率(bps)
)
uart_rx_inst
(
    .clk            (clk            ),
    .rst_n          (rst_n          ),
    .rx_rxd         (uart_rxd       ),//接收端口
    .rx_data        (rx_data        ),//接收1字节数据
    .rx_flag        (rx_flag        ) //1字节数据接收完成
);
endmodule

top模块的对外接口很简单,除了时钟复位外,仅包含串口接收和发送两个端口。由于串口接收模块uart_rx的数据输出rx_data可以保持较长时间不变(至少2个串口比特位的时长),因此可以将rx_data直接连接到tx_data。需要注意的是,不能将rx_flag简单的连接到tx_trig,作为串口发送模块的触发信号。这是因为,rx_flag仅仅有一个时钟宽度,当rx_flag=1而串口发送模块uart_tx尚未处于空闲状态(即state != S_IDLE)时,将无法触发串口发送。为了保证每次接收到一个字节数据就能触发一次串口发送,使用了一个简单的状态机,在rx_flag=1时,tx_trig的置1时长可以达到1个比特位的时长。

总结

FPGA编程应多多使用状态机,状态机编程会使程序清晰明了、通俗易懂。但是状态机的跳转可能会影响信号精度,尽量把时钟精度要求较高的操作放在状态机之外进行。其实除了状态跳转操作必须放在状态机之内,其他操作都可以想办法移到状态机之外。另外,灵活运用输入参数parameter,会大大减少修改程序的概率。

最后

以上就是糊涂方盒最近收集整理的关于Verilog——基于状态机的串口收发模块的实现基于状态机的串口收发模块的实现的全部内容,更多相关Verilog——基于状态机内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(71)

评论列表共有 0 条评论

立即
投稿
返回
顶部