【FPGA学习】状态机实现UART通信


前言

  在之前的文章中【FPGA学习】实例一、Cyclone IV串口通信(RS232)我们已经能够采用波形图的方法,实现9600bps的Uart通信。近期笔者在整理了状态机和计数器组合的设计方法以后,对状态机的设计又有了新的感悟和体会,所以又把经典的RS232协议拉出来当状态机的例子练手了哈哈哈。数据有效位为8bit,功能上增加了奇校验,并将波特率设置为115200bps,并借助这篇文章梳理一下状态机和计数器组合设计的思路和设计要点,文章奉上:


一、数据帧结构

在这里插入图片描述
  含校验位的RS232数据帧结构如上图所示,其中三个单元格为一个数据帧,数据有效为8bit 空闲状态下,rx信号为高电平,rx拉低一帧作为起始位。接下来按照数据位由低到高每一帧输出一位数据,发送到最高位Data[7]以后,有效数据发送完毕。
  接着PARITY为数据校验位。本文采用奇校验,即判断是否数据位和校验位的1的数量之和是否为奇数,若为奇数则满足奇校验,否则就认为数据在传输过程中出现了错误,从而将本次的8bit 数据丢弃。
  奇校验帧结束后为停止位,将rx拉高一帧作为结束信号,等待下一组数据传入。

二、接收模块

  接收模块是将接收到的串行数据转换为并行数据的模块,并同步输出一个数据有效使能信号。
  首先因为系统晶振为50MHz,串口的波特率为115200bps,因此每一帧数据所需时钟周期数为N = (1/115200)/(1/50000000) = 434,所以每一帧数据需要借助一个计数器判断当前的位数,该计数器计数到433后清零,采用组合逻辑赋值baud_max,当计数到433计满清零时拉高。
  接下来结合时序图介绍笔者的状态机部分设计思路:

2.1 状态设置

  接收模块状态机状态共包含IDLE、START、DATA、PARITY、STOP、WAIT六个状态,前五个状态望文生义即可,而WAIT状态是在校验位校验错误以及停止位没有读取到高电平时进入到的状态,意味着本次读取的8bit 数据是失效的,当在此状态下识别到停止位后直接进入IDLE状态,不进行数据的输出。

2.1 状态跳转

  状态机采用三段式的写法,比特率计数器baud_cntnext_state == DATA或者next_state == PARITY时进行循环计数(见时序图)。由于计数器慢一拍,所以进入状态DATA以后,baud_cnt从1开始计数,计数一帧即434个周期后,计数值为0,因此baud_cnt==0作为每一个状态跳转的必要条件(也保证了未开始计数时IDLE状态能在识别到起始位后直接进入到START状态)。
  START状态满足一帧时间就跳转到DATA状态,为了表征当前是数据的第几帧,引入帧计数器bit_cnt,该计数器在next_state == DATA或者next_state == PARITY时进行循环计数(见时序图),识别到帧结束信号baud_max==1以后,自加1。其他状态下,帧计数器清零。

2.2 奇校验

  由于帧计数器也慢实际数据一拍,和状态变化同步,因此判断校验位时,校验位相对于实际数据在第9帧。帧计数器慢一拍,所以当bit_cnt==8 && baud_max==1时对校验位进行判断。本文的奇数校验方式为判断parity == ~(^po_data) 是否成立,若成立,则校验位和数据位中1的个数之和为奇数(见时序图)。其中po_data为进行移位拼接后,准备并行输出的8位宽数据。
  满足校验位则进入校验位状态PARITY1帧,识别到停止位就进入到STOP状态。

在这里插入图片描述
在这里插入图片描述

2.3 数据输出

  接收模块的数据即为拼接结束以后的并行输出数据,并同步输出一个使能信号,在使能信号拉高时的数据信号才有效。因为是要把数据拼接为8位宽的数据,要保证移位8次,并且每次移位时数据正确,设置了移位标志信号shiftshift信号当波特率计数器计数到一帧的一半时state == DATA && baud_cnt == BAUD_CNT_MAX / 2拉高信号(见代码数据移位输出部分),此时数据稳定,并且在校验位前就可将八位数据移位完成,同时满足奇校验要求。又由于STOP状态只要发生错误就不会进入,保证了数据的正确性,所以选择当state==STOP时输出数据有效使能信号,并输出拼接完成的并行数据po_data。完整代码如下:

// *****************************************************************************************************************************
// ** 作者 : 彼方云城                                                  
// ** 邮箱 : zwr2059341302@163.com
// ** 博客 : https://blog.csdn.net/weixin_45451974?spm=1000.2115.3001.5343
// ** 日期 : 2022/10/10
// ** 功能 : 1、串口接收模块,接收串行数据,转换成并行数据输出
//            2、8bit数据(先接受低字节数据,再接收高字节数据),奇校验(不可在宏定义处删除奇校验)
// *****************************************************************************************************************************
module uart_rx_fsm
#(
    parameter   BAUD        =   115200   ,  //串口波特率
                CLK_FREQ    =   50_000_000, //时钟周期
                BAUD_CNT_MAX = CLK_FREQ / BAUD  //每帧数据对应的时钟周期
)
(
    //系统接口
    input   wire            sys_clk     ,
    input   wire            sys_rst_n   ,

    //输入数据
    input   wire            in_data     ,

    //输出数据
    output  reg     [7:0]   po_data     ,
    output                  po_flag     
);

//in_data_reg同步数据,in_data_reg1和in_data_reg2打两拍减小亚稳态
//in_data_reg2为稳定数据
reg     in_data_reg     ;
reg     in_data_reg1    ;
reg     in_data_reg2    ;

always@(posedge sys_clk or negedge sys_rst_n)begin
    if(!sys_rst_n)begin
        in_data_reg <=  1'b1;
        in_data_reg1 <= 1'b1;
        in_data_reg2 <= 1'b1;
    end
    else begin
        in_data_reg  <= in_data;
        in_data_reg1 <= in_data_reg;
        in_data_reg2 <= in_data_reg1;
    end
end

//**********************状态机********************//

parameter   IDLE = 3'd0,START = 3'd1,DATA = 3'd2;
parameter   PARITY = 3'd3,WAIT = 3'd4,STOP = 3'd5;

reg [2:0]   state,next_state;

//状态机状态转换
always@(posedge sys_clk or negedge sys_rst_n)begin
    if(!sys_rst_n)begin
        state <=    IDLE;
    end
    else begin
        state <=    next_state;
    end
end

//波特率计数器及计满脉冲信号
reg     [9:0]   baud_cnt;
wire            baud_max;

always@(posedge sys_clk or negedge sys_rst_n)begin
    if(!sys_rst_n)begin
        baud_cnt <= 10'd0;
    end
    else if(baud_cnt == BAUD_CNT_MAX - 1'b1)begin
        baud_cnt <= 10'd0;
    end
    else if(next_state==START || next_state==DATA 
            || next_state==PARITY || next_state==STOP)begin
        baud_cnt <= baud_cnt + 1'b1;
    end
end

assign baud_max = (baud_cnt == 10'd0);

//起始位及结束位标志信号
wire        start;
wire        stop;

assign start    = (in_data_reg2 == 1'b0) & baud_max;
assign stop     = (in_data_reg2 == 1'b1) & baud_max;

//bit位计数器
reg     [3:0]   bit_cnt;
wire            bit_done;//计数到8时拉高

always@(posedge sys_clk or negedge sys_rst_n)begin
    if(!sys_rst_n)begin
        bit_cnt <= 4'd0;
    end
    else if(next_state == DATA & bit_cnt == 4'd9 & baud_max)begin
        bit_cnt <= 4'd0;
    end
    else if((next_state == DATA || next_state == PARITY)&& baud_max)begin
        bit_cnt <= bit_cnt + 1'b1;
    end
    else if(next_state == DATA)begin
        bit_cnt <= bit_cnt;
    end
    else begin
        bit_cnt <= 4'd0;
    end
end

assign bit_done = (bit_cnt == 4'd8);

//奇校验位
wire    parity;

assign  parity  =  baud_max && bit_done && in_data_reg2 == ~(^po_data);
//*******当奇校验位与数据奇数相反时数据正确

//状态机下一状态描述
always@(*)begin
    case(state)
        IDLE    :   next_state = start?START:IDLE;
        START   :   next_state = baud_max?DATA:START;
        DATA    :   next_state = (bit_done&baud_max)?(parity?PARITY:WAIT):DATA;
        PARITY  :   next_state = baud_max?(stop?STOP:WAIT):PARITY;
        WAIT    :   next_state = baud_max?(stop?STOP:WAIT):WAIT;
        STOP    :   next_state = baud_max?(start?START:IDLE):STOP;
        default :   next_state = IDLE;
    endcase
end

//数据移位标志信号
wire    shift;

assign  shift   = (next_state == DATA && baud_cnt == BAUD_CNT_MAX/2);
//当数据在波特率计数器满值一半时提取

//状态机输出及数据有效使能信号
always@(posedge sys_clk or negedge sys_rst_n)begin
    if(!sys_rst_n)begin
        po_data <= 8'd0;
    end
    else if(shift)begin
        po_data <= {in_data_reg2,po_data[7:1]};
    end
    else begin
        po_data <= po_data;
    end
end

//输出同步使能信号
assign  po_flag = (state==STOP);

endmodule

三、发送模块

  发送模块主要是将接受到的并行多位宽数据以串行形式RS232的协议发出,接下来结合时序图和状态转移图介绍发送模块状态机实现的设计思路:

3.1 状态跳转

  发送模块的状态去除了WAIT状态,同样采用一个波特率计数器对一帧对应的434个时钟周期进行计数,baud_cnt以及baud_max作为每帧的时钟计数信号和跳转信号,初始状态为IDLE,接收到pi_flag后跳转为START状态,经过一帧跳转到DATA状态,进入到DATA状态后,用计数器bit_cnt来计数数据帧数,计满8位数据后跳转到校验位状态PARITY,校验位采用parity_data = (state==PARITY)?(~(^pi_data)):1'b0;进行赋值,仅在校验位状态下将校验位数据赋值为原数据缩减异或运算的取反。一帧后进入STOP状态,输出1帧高电平。随后判断是否有输入使能,有就跳转到START状态,否则进入IDLE状态,等待下一个有效数据使能信号。
在这里插入图片描述
在这里插入图片描述

3.2 数据输出

  发送模块数据的输出采用组合逻辑输出的方式,因为延迟next_state一拍的帧计数器bit_cnt和当前状态state同步,所以可以在DATA状态下用计数器来读取并输出采集数据的由低到高的不同位,而在START状态输出低电平,STOP状态输出高电平,其他状态均为空闲状态的高电平。完整代码如下:

// *****************************************************************************************************************************
// ** 作者 : 彼方云城                                                  
// ** 邮箱 : zwr2059341302@163.com
// ** 博客 : https://blog.csdn.net/weixin_45451974?spm=1000.2115.3001.5343
// ** 日期 : 2022/10/13
// ** 功能 : 1、串口发送模块,接收并行数据,读取数据后串行输出
//            2、8bit数据(先接受低字节数据,再接收高字节数据),奇校验(不可在宏定义处删除奇校验)
//            3、波特率115200
// *****************************************************************************************************************************
module  uart_tx_fsm
#(
    parameter   BAUD        =   115200   ,  //串口波特率
                CLK_FREQ    =   50_000_000, //时钟周期
                BAUD_CNT_MAX = CLK_FREQ / BAUD  //每帧数据对应的时钟周期
)
(
    //系统接口
    input   wire            sys_clk     ,
    input   wire            sys_rst_n   ,

    //输入数据及同步信号
    input   wire    [7:0]   pi_data     ,
    input   wire            pi_flag     ,
    
    //输出串行数据
    output  reg             out_data     
);

//**********************状态机********************//

parameter   IDLE = 3'd0,START = 3'd1,DATA = 3'd2;
parameter   PARITY = 3'd3,STOP = 3'd4;

reg [2:0]   state,next_state;

//状态机状态转换
always@(posedge sys_clk or negedge sys_rst_n)begin
    if(!sys_rst_n)begin
        state <=    IDLE;
    end
    else begin
        state <=    next_state;
    end
end

//波特率计数器及计满脉冲信号
reg     [9:0]   baud_cnt;
wire            baud_max;

always@(posedge sys_clk or negedge sys_rst_n)begin
    if(!sys_rst_n)begin
        baud_cnt <= 10'd0;
    end
    else if(baud_cnt == BAUD_CNT_MAX - 1'b1)begin
        baud_cnt <= 10'd0;
    end
    else if(next_state==START || next_state==DATA 
            || next_state==PARITY || next_state==STOP)begin
        baud_cnt <= baud_cnt + 1'b1;
    end
end

assign baud_max = (baud_cnt == 10'd0);

//起始位及结束位标志信号
wire        start;//进入start状态

assign start    =   pi_flag;

//bit位计数器
reg     [3:0]   bit_cnt;

always@(posedge sys_clk or negedge sys_rst_n)begin
    if(!sys_rst_n)begin
        bit_cnt <= 4'd0;
    end
    else if((next_state == DATA || next_state == PARITY)&& baud_max)begin
        bit_cnt <= bit_cnt + 1'b1;
    end
    else if(next_state == DATA || next_state == PARITY)begin
        bit_cnt <= bit_cnt;
    end
    else begin
        bit_cnt <= 4'd0;
    end
end

//奇校验位数据
wire    parity;
wire    parity_data;

assign  parity  =   (bit_cnt==4'd8) && baud_max;
assign  parity_data  =  (state==PARITY)?(~(^pi_data)):1'b0;
//*******仅在PARITY状态下有效

//状态机下一状态描述
always@(*)begin
    case(state)
        IDLE    :   next_state  =   start?START:IDLE;
        START   :   next_state  =   baud_max?DATA:START;
        DATA    :   next_state  =   parity?PARITY:DATA;
        PARITY  :   next_state  =   baud_max?STOP:PARITY;
        STOP    :   next_state  =   baud_max?(start?START:IDLE):STOP;
        default :   next_state  =   IDLE;
    endcase
end

//状态机输出
always@(*)begin
    case(state)
        START   :   out_data    =   1'b0;
        DATA    :   out_data    =   pi_data[bit_cnt - 1'b1];
        PARITY  :   out_data    =   parity_data;
        STOP    :   out_data    =   1'b1;
        default :   out_data    =   1'b1;
    endcase
end

endmodule

四、顶层模块

实例化连接两个模块,代码如下:

// *****************************************************************************************************************************
// ** 作者 : 彼方云城                                                  
// ** 邮箱 : zwr2059341302@163.com
// ** 博客 : https://blog.csdn.net/weixin_45451974?spm=1000.2115.3001.5343
// ** 日期 : 2022/10/14
// ** 功能 : 1、RS232协议顶层模块
//            2、实例化接收模块和发送模块,并组成串口回环
//            3、波特率115200
// *****************************************************************************************************************************

module  rs232_fsm(

//系统接口
    input   sys_clk     ,
    input   sys_rst_n   ,
    
//输入串行数据
    input   in_data     ,

//输出串行数据
    output  out_data    
);

wire    [7:0]   po_data;
wire            po_flag;

uart_rx_fsm
#(
    .BAUD        (115200    ),  //串口波特率
    .CLK_FREQ    (50_000_000) //时钟周期
)
uart_rx_fsm_inst
(
    //系统接口
    .sys_clk     (sys_clk  ),
    .sys_rst_n   (sys_rst_n),

    //输入数据
    .in_data     (in_data),

    //输出数据
    .po_data     (po_data),
    .po_flag     (po_flag)
);

uart_tx_fsm
#(
    .BAUD        (115200    ),  //串口波特率
    .CLK_FREQ    (50_000_000) //时钟周期
)
uart_tx_fsm_inst
(
    //系统接口
    .sys_clk     (sys_clk  ),
    .sys_rst_n   (sys_rst_n),

    //输入数据及同步信号
    .pi_data     (po_data),
    .pi_flag     (po_flag),
    
    //输出串行数据
    .out_data     (out_data)
);

endmodule

  将串口接收模块和发送模块直接相连,形成一个串口回环,并上板进行验证,验证结果如下,输入数据"112256478ff",设置波特率为115200,数据位8,校验位为奇校验,发送后返回数据与输入数据相同,可知模块设计无误。
在这里插入图片描述

总结

  本文采用状态机和计数器组合的方式实现了RS232协议,在代码中笔者也采用了将时序逻辑中较为复杂的条件判断条件以组合逻辑的形式单独列出来的方式,的确使得状态机的转变条理更加清晰,也易于修改拓展了,笔者能力尚浅,还请读者多多指正!

  • 4
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值