Menu Close

UART数据缓冲(FIFO)

UART 综合应用-协议与数据包文章中,我们介绍了从FPGA 中通过uart 读取数据,CPU 写入数据到FPGA 外设的过程。在一些中大型项目中,往往会增加数据链路层,用来隔离应用数据和具体的硬件传输方式。 也就是说,应用程序只是负责数据发送,处理,而不关系具体使用什么样的硬件来发送(UART , I2C ,SPI, ETHERNET)。 而phy模块只管数据的接收和发送, 但是接收和发送的数据,phy 模块不再关心。这样,就需要不同的模块来进行分层处理,使得整个工程结构明晰,同时根据不同的项目需要, 模块可以在不同的项目上重复使用。

1 对功模块能重新分层

之前的文章中,uart项目的结构为:

图1

经过分层处理后,重新规划的模块结构为:

 

图2

从图2 中可以看出,整个工程由2个大模块构成:uart_app, uart_wrapper。这2 个模块直接是通过fifo 相关的数据信号来传递信息的。

  • uart_app 模块主要负责对数据的解析,发送等,不再关心具体使用什么样的phy 来进行通讯。
  • uart_wrapper 模块主要是接收rx来的数据, 或者是将数据发送到tx上,实现完整的UART phy 功能,而不再关心数据具体是什么含义,也不对数据做任何的解析。

通过这样的分层设计,可以看出, uart_app 模块可以给很多项目使用, 具体使用UART, SPI,I2C 等,都可以正常通讯。 uart_wrapper 模块也是可以给很多项目来重用的。因为这个模块值负责接收,发送数据,不对数据解析。 这样的分层结构,每个模块的功能清晰,重用性高,从而实现了phy层和数据链路层的分离。

2 uart_wrapper 结构

图3

uart_wrapper 封装了完整的uart 模块的接口。 通过数据总线方式 和外部设备通讯。 它是有uart_engine 和uart_phy 两个部分组成。 其中uart_phy部分,在之前的文章中做过详细的介绍。一端是uart_engine 部分负责接收、发送信号到uart_phy,另外一端是通过数据总线和外部设备沟通。uart_engine 主要负责uart 接收来的数据存储到rx fifo中,同时提供包接收指示。 同时,也负责从tx fifo 中得到数据, 按照uart_phy 所需要的时序,发送数据到uart_phy。

uart_wrapper 代码:

`timescale 1ns / 1ps
 
module uart_engine#
(
     parameter DEEP  = 256
)
(
    input             sys_clk,
// tx channel    
    output            tx_req,
    output      [7:0] tx_data,
    input             tx_ack,

    input       [8:0] tx_fifo_din,
    input             tx_fifo_wr,
    output      [7:0] tx_fifo_cnt,
// rx channel    
    input       [7:0] rx_data,
    input             rx_rdy,
    input             rx_eop,

    output      [8:0] rx_fifo_dout,
    input             rx_fifo_rd,
    output            rx_pkg_plus,
    
    input             rst
);

//===========================rx side
rx_fifo #
(
  .DEEP     ( DEEP )
)
rx_fifo_inst
(
  .sys_clk       ( sys_clk ),
  	 
  .rx_data       ( rx_data ),
  .rx_rdy        ( rx_rdy ),
  .rx_eop        ( rx_eop ),
   
  .rx_fifo_dout  ( rx_fifo_dout ),
  .rx_fifo_rd    ( rx_fifo_rd ),
  .rx_pkg_plus   ( rx_pkg_plus ),
   
  .rst           ( rst )
);

//===========================tx side
tx_fifo #
(
  .DEEP     ( DEEP )
)
tx_fifo_inst
(
  .sys_clk       ( sys_clk ),
   
  .tx_ack        ( tx_ack ),
  .tx_req        ( tx_req ),
  .tx_data       ( tx_data ),
   
  .tx_fifo_din   ( tx_fifo_din ),
  .tx_fifo_wr    ( tx_fifo_wr ),
  .tx_fifo_cnt   ( tx_fifo_cnt ),
   
  .rst           ( rst )
);


endmodule

 

 

uart_engine 模块包括tx_fifo.v 和rx_fifo.v 两个部分。其中

rx_fifo.v 代码如下:

`timescale 1ns / 1ps
module rx_fifo #
(
     parameter DEEP = 256
)
(
    input                sys_clk,
// write channel       	
    input          [7:0] rx_data,
    input                rx_rdy,
    input                rx_eop,
// read  channel    
    output         [8:0] rx_fifo_dout,
    input                rx_fifo_rd,
    output   reg         rx_pkg_plus = 0,
       
    input                rst
);


//==================== write fifo side

reg [2:0]  rx_wr_st = 0;
reg        rx_wrreq = 0;
reg [8:0]  rx_fifo_din = 0;

wire       rx_rdempty;
wire       rx_wrfull ;  
       
wire [$clog2(DEEP) - 1:0] wrusedw; 


always @ (posedge sys_clk )
if(rst)                     
begin
    rx_wrreq <= 1'b0;                
    rx_fifo_din <= 0;
    rx_wr_st <= 0;
    rx_pkg_plus <= 1'b0;
end
else case(rx_wr_st)
0:
begin
   rx_pkg_plus <= 1'b0;
   rx_wrreq <= 1'b0;
   if(wrusedw ==0)
       rx_wr_st <= 1;
end
1:
begin
    rx_wrreq <= 1'b0;
    rx_pkg_plus <= 1'b0;

    if(rx_rdy)
    begin
        rx_wrreq <= 1'b1;
        rx_fifo_din <= {1'b1, rx_data};
    end
    else if(rx_eop)
    begin
        rx_wrreq <= 1'b1;
        rx_fifo_din <= 0;
        rx_pkg_plus <= 1'b1;
        rx_wr_st <= 0;
    end
end

default:rx_wr_st <= 0;
endcase



//==================== rx side ipcore instantiate

fifo	rx_fifo_inst 
(
    .wrclk      ( sys_clk ),
    .wrreq      ( rx_wrreq ),
    .data       ( rx_fifo_din ),
    .wrfull     ( rx_wrfull ),
    .wrusedw    ( wrusedw ),
     
    .rdclk      ( sys_clk  ),	 
    .rdreq      ( rx_fifo_rd ), 
    .q          ( rx_fifo_dout ), 
    .rdempty    ( rx_rdempty )
);

endmodule

 

 

其中fifo IP的定义 适用于 tx_fifo.v 和rx_fifo.v  这个fifo 定义为 读写时钟分离(异步时钟),深度为256,读写数据宽度为9bit , 增加了写counter 属性。

图4

状态机部分比较简单,主要是

负责接收uart模块传递上来的数据有效使能rx_rdy、8bit数据rx_data、包结束信号rx_eop。将数据以一定格式写入rx端的FIFO 中,等待应用层给读使能。程序实现流程如下:

初始化:reset作为复位信号对变量进行初始化操作

状态0:FIFO深度0-255,包长范围0-255,为确保每次能存入一个完整数据包,需要保证剩余空间256。判断wrusedw 为0跳转到状态1

状态1:等待rx_rdy 即数据有效使能信号,来一个信号收8bit数据,给一个1作为控制字信息合并为9bit数据 {1’b1, rx_data},同时给写使能,将数据写入FIFO。如果一直没有rx_rdy信号并且来了包结束信号rx_eop,写9’b0作为包隔断,同时给上层应用一个rx_pkg_plus指示FIFO 里面存入了一个完整的数据包。然后跳转到状态0。(由于rx_eop与rx_rdy至少隔了12个波特率使能信号的时间,所以当回到状态0再去判断wrusedw 时,已经可以准确拿到wrusedw 的值了)

rx_fifo 流程图

 

tx_fifo.v 代码:

`timescale 1 ns / 1ps
module tx_fifo #
(
    parameter DEEP = 256
)
(
    input                           sys_clk,
    // read channel
    input                           tx_ack,
    output reg                      tx_req,
    output reg [ 7: 0 ]             tx_data = 0,
    // write channel
    input [ 8: 0 ]                  tx_fifo_din,
    input                           tx_fifo_wr,

    output [ $clog2( DEEP ) - 1:0 ] tx_fifo_cnt,

    input                           rst
);



//==================== read fifo side
wire tx_rdempty;

reg [ 2: 0 ] tx_rd_st = 0;
reg tx_fifo_rd = 1'b0;

wire [ 8: 0 ] tx_fifo_dout;


always@( posedge sys_clk )
if ( rst )
begin
    tx_rd_st <= 0;
    tx_fifo_rd <= 1'b0;
    tx_req <= 1'b0;
    tx_data <= 0;
end
else
case ( tx_rd_st )
    0:
    begin
        tx_req <= 1'b0;
        tx_fifo_rd <= 1'b0;

        if ( !tx_rdempty )
        begin
            tx_fifo_rd <= 1'b1;
            tx_rd_st <= 1;
        end
    end
    1:
    begin
        tx_fifo_rd <= 1'b0;
        tx_rd_st <= 2;
    end
    2:
    begin
        if(tx_fifo_dout[8])
        begin
            tx_req <= 1'b1;
            tx_data <= tx_fifo_dout [ 7: 0 ];
            if ( tx_ack )
            begin
                tx_req <= 1'b0;
                tx_rd_st <= 3;
            end
        end
        else tx_rd_st <= 0;
    end
    3:
    begin
        if ( !tx_ack )
            tx_rd_st <= 0;
    end

    default:
        tx_rd_st <= 0;
endcase



//==================== tx side ipcore instantiate

fifo tx_fifo_inst
(
    .wrclk      ( sys_clk ),

    .wrreq      ( tx_fifo_wr ),
    .data       ( tx_fifo_din ),
    .wrfull     ( ),
    .wrusedw    ( tx_fifo_cnt ),

    .rdclk      ( sys_clk ),
    .rdreq      ( tx_fifo_rd ),
    .q          ( tx_fifo_dout ),
    .rdempty    ( tx_rdempty )
);

endmodule

 

 

fifo IP 核部分和rx fifo中的定义相同(9bit 数据, 异步时钟, 增加了写counter)。

状态机部分:

当底层uart有空可以发数据时,读出应用层写入FIFO 的数据,并给出数据有效使能信号。具体实现流程如下:

初始化:reset作为复位信号对状态机进行初始化操作

状态0:FIFO有数就到状态1,同时给出读使能

状态1:复位读使能跳转到状态2

状态2:如果tx_fifo_dout[8] == 1,截取低8位发送给uart模块同时给出数据有效使能信号,uart模块反馈收到数据后将使能信号拉低,跳转到状态3,否则跳转到状态0

状态3:等待uart将ack信号拉低,然后跳回状态0(由于ack是一个长电平信号,不等待ack拉低直接到0状态会导致重复给uart发送数据)

 

tx_fifo 流程图

 

3 user_app 结构

user_app模块包含:tx_app,rx_app, seg_dev,cmd_decoder 等, 其中seg_dev, cmd_decoder 在之前的文章中有所介绍。这里主要介绍一下tx_app.v 和rx_app.v模块。

  • rx_app模块:当底层的FIFO有完整数据包时,读取数据包内容,主要由状态机完成;按照双方约定的协议格式对读出的数据进行解析,最后将正确的读写命令、数据等信号传递给上层应用模块,主要由从状态机完成。具体流程如下:

 

rx_app.v 代码:

`timescale 1ns / 1ps
module rx_app 
(
    input             sys_clk,
        
    input             rx_pkg_plus, 
    input      [8:0]  rx_fifo_dout,
    output reg        rx_fifo_rd = 0,
    
    output reg [7:0]  pkg_type = 0,
    output reg        pkg_op = 0,  // write 0 or read 1
    output reg [23:0] pkg_data = 0,
    output reg        pkg_valid = 0,
    
    input             rst
);


/*
packet format : leng + wr/rd + dev_type + pkg_content....
leng : total length of the packet (1 byte)
wr/rd : fpga write or read operation. 0 => wirte op; 1 => read op; (1 byte)
dev_type: device index : 01 => segment; 02 => switch; 03 => keypad;
pkg_cntent: data was send by pc (ready to write to a device)  (n bytes).
*/

//==================== fifo里面完整包计数


reg             rx_pkg_minus = 0;
reg [7:0]       packets_count = 0;
always @ (posedge sys_clk )
if(rst)
    packets_count <= 0;
else 
begin
    packets_count <= packets_count + rx_pkg_plus - rx_pkg_minus;
end


//==================== 读FIFO
reg [3:0]  rd_st = 0;
reg [7:0]  rx_len = 0;
reg [7:0]  rx_cnt = 0;
reg        rx_error = 0;         // 指令发送错误

always @ (posedge sys_clk )
if(rst) 
begin
    rx_pkg_minus <= 0;
    rx_cnt <= 0;
    pkg_valid <= 0;
    rx_fifo_rd <= 1'b0;

    pkg_type <= 8'hff;
    rx_len <= 0;
    pkg_op <= 0;
    pkg_data <= 0;

    rx_error <= 0;

    rd_st <= 0;
end
else case (rd_st)
0:
begin
    rx_pkg_minus <= 0;
    rx_cnt <= 0;
    pkg_valid <= 0;
    rx_fifo_rd <= 1'b0;

    pkg_type <= 8'hff;
    pkg_op <= 0;
    rx_len <= 0;
    pkg_data <= 0;

    rx_error <= 0;
    rd_st <= 0;
     
    if(packets_count != 0)
    begin
        rx_fifo_rd <= 1'b1;
        rd_st <= 1;
    end
end
1:
begin
    rx_fifo_rd <= 1'b0;
    rd_st <= 2;
end
2:
begin
    pkg_data <= 0;
    
    if(rx_fifo_dout[8])                  
    begin
        rx_len <= rx_fifo_dout[7:0];
        rx_cnt <= 1;

        if(rx_fifo_dout[7:0] < 3)  // packet lenth <= 2;
        begin
            rx_pkg_minus <= 1'b1;
            rx_fifo_rd <= 1'b1;   //由于FIFO空间设置的时候256,判定必须FIFO为空才会进行下一次读写,所以必须清空
            rd_st <= 11;
        end
        else
        begin
            rd_st <= 3;
            rx_fifo_rd <= 1'b1;
        end
    end
    else
    begin
        rx_pkg_minus <= 1'b1;
        rd_st <= 14;
    end
end
3:
begin
    rd_st <= 4;
    rx_fifo_rd <= 1'b0;	
end
4:
begin
    if(rx_fifo_dout[8]) 
    begin
        rx_cnt <= 2;
        rx_fifo_rd <= 1'b1;
        pkg_op <= rx_fifo_dout[0]; // get command, 00 => write;  01 => read
        rd_st <= 5;
    end
    else
    begin
        rx_pkg_minus <= 1'b1;
        rd_st <= 14;
    end
end
5:
begin
    rx_fifo_rd <= 1'b0;
    rd_st <= 6;
end
6:
begin
    if(rx_fifo_dout[8]) 
    begin
        rx_cnt <= 3;
        pkg_type <= rx_fifo_dout[7:0];  // get type, 01 => seg;  02 => sw
        rx_fifo_rd <= 1'b1;
        rd_st <= 7;
    end
    else
    begin
        rx_pkg_minus <= 1'b1;
        rd_st <= 14;
    end
end
7:
begin
    rx_fifo_rd <= 1'b0;
    rd_st <= 8;
end
8: 
begin
    if(rx_fifo_dout[8]) 
    begin
        if(rx_len == rx_cnt)    //说明实际写入FIFO 数据比lenth 长,命令截断,同时需要将FIFO清空
        begin
            pkg_valid <= 1;
            rx_pkg_minus <= 1'b1;
            rx_fifo_rd <= 1'b1;
            rd_st <= 11;
        end
        else
        begin
            pkg_data <= {pkg_data[15:0], rx_fifo_dout[7:0]};
            rx_cnt <= rx_cnt + 1;
            rx_fifo_rd <= 1'b1;
            rd_st <= 7;
        end
    end
    else
    begin
        if(rx_len == rx_cnt) //说明实际写入FIFO 数据与lenth相同,命令正确
        begin
            pkg_valid <= 1;
            rx_pkg_minus <= 1'b1;
            rd_st <= 15;
        end
        else                //说明实际写入FIFO 数据比lenth短
        begin
            rd_st <= 14;
            rx_pkg_minus <= 1'b1;
        end
    end
end
11:
begin
    pkg_valid <= 0;
    rx_pkg_minus <= 1'b0;
    rx_fifo_rd <= 1'b0;
    rd_st <= 12;
end
12:
begin
    if(rx_fifo_dout[8]) 
    begin
        rx_fifo_rd <= 1'b1;
        rd_st <= 11;
    end
    else
        rd_st <= 14;
end
14:
begin
    rx_error <= 1'b1;
    rx_pkg_minus <= 1'b0;
    rd_st <= 0;
end
15:
begin
    pkg_valid <= 0;
    rx_pkg_minus <= 1'b0;
    rx_error <= 1'b0;
    rd_st <= 0;          //==========等待一个时钟周期,确保packets_count 拿到准确的数据。
end

default:   rd_st <= 0;
endcase
 
endmodule

 

 

端口信号解释:

sys_clk:时钟域模块传递进来的系统复位信号

rx_pkg_plus:rx_fifo传递的单脉冲信号,表示rx_fifo已经写入一个完整数据包

rx_fifo_dout:从rx_fifo读出的数据

rx_fifo_rd :读rx_fifo 使能,当rx_fifo写入一个完整包开始读

pkg_type:从设备地址,表示计算机准备对那个设备进行操作

pkg_op:读/写 控制字

pkg_data:计算机写入到fpga的数据

pkg_valid:传递给应用层的数据和命令有效信号

rst:时钟域模块传递进来的系统复位信号

 

状态机详解:

初始化:rst信号对整个状态机进行复位

  • 状态0:如果有完整数据包写入fifo完成,发读使能,跳转到状态1
  • 状态1:等待一个时钟周期,跳转到状态2
  • 状态2:判断读出数据最高控制bit是否为高,为高表示数据有效,为高且读出的length小于3,则不满足数据包协议,跳转到状态11,将fifo读空;如果控制bit为高且length 大于等于3,给读使能,跳转到状态3;如果最高控制bit为低,表示fifo读空,表示数据包有问题,跳转到错误指示状态14
  • 状态3:等待一个时钟周期,跳转到状态4
  • 状态4:判断读出的数据最高位是否为高,为高表示数据有效,根据数据包协议将[7:0]数据存入pkg_op,跳转到状态5;为低,表示数据包错误,跳转到状态14
  • 状态5:等待一个时钟周期,跳转到状态6
  • 状态6:判断读出的数据最高位是否为高为高,根据数据包协议将[7:0]数据存入pkg_type,跳转到状态7;为低,表示数据包错误,跳转到状态14
  • 状态7:等待一个时钟周期,跳转到状态8
  • 状态8:判断读出最高控制bit是否为高,为高且此时rx_len == rx_cnt,表示实际写入FIFO 数据比lenth 长,命令错误,同时需要将FIFO清空,跳转到状态11;控制bit为高且rx_len >= rx_cnt,表示数据未读完,返回状态7等待一个时钟。如果控制bit为低,且rx_len == rx_cnt 表示数据包正确,读完,跳转到状态15;如果控制bit为低但是rx_len >= rx_cnt,说明实际写入FIFO 数据比lenth短,跳转到状态14
  • 状态11-状态12:将剩余fifo中的数据读完,为下次写入fifo准备空间
  • 状态14:数据包错误指示状态
  • 状态15:各种数据清0,等待一个时钟,确保packets_count 拿到准确的数据。

tx_app 模块

当有读命令时,将读设备(SW)反馈的并行数据按照一定格式写入tx_fifo。

 

端口信号解释:

sys_clk:时钟域模块传递进的系统时钟

tx_fifo_cnt:tx_fifo 中写入了多少字节计数,以此判断fifo剩余空间是否足够

tx_fifo_wr:写tx_fifo 的写使能

tx_fifo_din:写tx_fifo的写数据

rd_sw_cmd:计算机发送给fpga的读设备命令

SW:读设备发送的读数据

rst:时钟域模块传递进来的系统复位信号

状态机详解:

在确保FIFO 有足够空间后,如果接收到读命令,就将读设备数据同步到系统时钟,并且将数据按照一定格式存入FIFO,等待底层模块读取。具体流程如下:

步骤1:将读设备输入信号打两拍同步到系统时钟

步骤2:利用状态机实现数据的存储

  • 初始化:reset作为复位信号对状态机进行初始化操作
  • 状态0:判断FIFO可用空间是否足够,足够就跳转到状态1
  • 状态1:收到读命令,给出写FIFO使能,同时给出9bit写数据{1’b1,key_r1}最高位1作为控制字,表示后面的8bit设备数据有效。跳转到状态2
  • 状态2:由于本次工程的目的是接收一次命令读一次,给出9bit写数据{1’b0, 8’00},然后跳转到状态3
  • 状态3:由于本次工程的目的是接收一次命令读一次,所以需要等待写命令拉低,然后跳转到状态4
  • 状态4:由于tx_fifo_cnt 生效需要等待2个时钟,保险期间,状态4作为等待状态。跳转到状态0

tx_app.v 代码:

`timescale 1ns / 1ps
module tx_app #
(
     parameter DEEP  = 256
)
(
    input                       sys_clk,

    input  [$clog2(DEEP) - 1:0] tx_fifo_cnt,
    output reg                  tx_fifo_wr,
    output reg [8:0]            tx_fifo_din,

    input                       rd_sw_cmd,

    input      [7:0]            SW,

    input                       rst
);

//============================输入信号同步处理

reg [7:0] key_r0 = 0;
reg [7:0] key_r1 = 0;
always @ (posedge sys_clk or posedge rst)
if(rst)
begin
    key_r0 <= 0;
    key_r1 <= 0;
end
else
begin
    key_r0 <= SW;
    key_r1 <= key_r0;
end


//============================输入信号写入FIFO
reg [3:0] wr_st = 0;
always @ (posedge sys_clk or posedge rst)
if(rst)
begin
    tx_fifo_din <= 0;
    tx_fifo_wr <= 1'b0;

    wr_st <= 0;
end
else case(wr_st)
0:
begin
    tx_fifo_din <= 0;
    tx_fifo_wr <= 1'b0;
     
    if(tx_fifo_cnt == 0)
        wr_st <= 1;
end
1:
begin
    if(rd_sw_cmd)
    begin
        tx_fifo_din <= {1'b1,key_r1};
        tx_fifo_wr <= 1'b1;
        wr_st <= 2;
    end
end
2:
begin
    tx_fifo_din <= 0;
    tx_fifo_wr <= 1'b1;
    wr_st <= 3;
end
3:
begin
    tx_fifo_wr <= 1'b0;
    wr_st <= 4;
end
4:
begin
    wr_st <= 0;         //tx_fifo_cnt 生效要晚2个时钟,保险起见,多停留1个时钟
end

default:wr_st <= 0;
endcase


endmodule

 

 

下载调试:

1)08 00 01 aa bb cc ee ff

2)05 00 01 12 34

Quartus 程序代码:

Posted in FPGA, FPGA 教材教案, Verilog, 教材与教案, 文章

发表回复

相关链接