上节内容内容讲解了利用乒乓Buffer实现异步过程数据缓冲的原理,本节内容利用Intel-Altera 的FPGA为例介绍如何使用BRAM实现异步过程的的数据缓冲。异步过程可能发生在应用层或数据链路层,首先研究如何在数据链路层插入乒乓Buffer实现异步过程的隔离,打破上层(应用层)和下层数据的胶合逻辑(glue logic),将应用层和数据链路层解放出来可以完成更多的任务。
1.乒乓Buffer的例化
- 在Quartus下生成双口RAM(TDPRAM)
利用向导生成双口RAM Quartus右面IP Catalog窗口: Installed IP –>Library–>Basic Function–>On Chip Memory–>RAM:2-PORT,双击 打开如图1所示界面,命名为tx_pingpong_buffer。

图1
(1)选择“with two read/write ports”,使读、写端口分开控制;
(2)选择:“As a number of words”,存储器尺寸按words标定。如图2所示,点击Next.

图2
(3)将口线宽度为16,深度为2048,主要考虑将来与CPU的传统总线做接口(异步总线接口),所以这里没有采用18位接口。添加rden_a,rden_b。
至于rden_a,rden_b在例化后,如果不使这两个信号用也可以直接输入高电平。如图3,

图3
(4)BRAM采用不同的时钟,这样两边将来在发布IP核的时候适应性更强。便于操作,将clock分别设定A端口和B端口,而不是与将clock与输入输出对应。这样对于同一个端口的读/写都是同步的。当然不同的应用可能配置不同,当在DSP或图像算法处理(如深度学习等)时,需要同时利用双端口更新数据,又需要同时在双端口上使用数据时,可以配置成两个时钟分别与读/写端口相关联。

图4
(5)选择数据,地址的锁存方式,图5显示在BRAM的端口有一级锁存,可以提高时序,但从给定读地址到真正数据输出会增加以及延迟。因此在使用数据时一定要注意。

图5
图6数据输出没有增加时钟锁存,数据输出端口与存储单元直接相连,少一级延迟,但时序收敛的结果会变差,本程序中由于时钟为100M,对于时序的要求不高,因此选择图6的配置方式。

图6
(6)当同一个端口读写同一个单元时(主要在控制字读写时发生),选择读取旧数据,确保读取的安全性。当在同步时钟下,也可以设成读新数据,可以提前读出新数据,在多个流水线写/读数据交换的情况下,可以简化流水线的匹配,但一定要保证同时读写的安全性,这部分的话题将来在RISC-V CPU设计部分进行探讨。

图7
(8)生成DPRAM IP核并例化。(在视频中解释一下,在Quartus,Vivado 系统中模板(templet, Vizard,IP 核,例化的概念)。由于在本设计中是用于数据链路层与应用层的数据缓冲,因此将例化放在数据链路层。例化接口如下:
tx_pingpong_buffer tx_pingpong_buffer_inst ( .clock_a ( clk_100M ), .address_a ( tx_addr_a ), .rden_a ( 1'b1), .wren_a ( tx_wren_a ), .data_a ( tx_data_a ), .q_a ( tx_q_a ), .clock_b ( clk_100M ), .address_b ( tx_addr_b ), .rden_b ( 1'b1), .wren_b ( tx_wren_b ), .data_b ( tx_data_b), .q_b ( tx_q_b) );
上面例化接口将BRAM将a,b端口清晰的分成两个部分。虽然例化时采用了a,b端口异步时钟的方式,但本程序中为了简化设计将clock_a与clock_b都连接到clk_100M上,实际效果是同步时钟的行为(如果能确定a,b端口是同步时钟,也可以修改IP,采用单时钟生成IP)。rden_a, rden_b由于与读地址同步,由地址即可确定读出单元,因此这里都直接给高电平(1’b1)。按照图8的方式,将a端口与应用层相连,将b端口与本层(数据链路层)的程序关联。

图8
修改数据链路层的程序与接口,分别与应用层和物理层连接。保持与物理层的接口协议不变,修改与应用层的接口。维持接收端的所有接口与协议不变,便于验证发送端的接口和代码修改。
2.工程代码修改
(1)发送端数据链路层修改后的代码如下:
module trans_data_link
#(
parameter IDLE = 10'b1010_1010_10
)
(
input rst,
input clk_100M,
input [ 10: 0 ] tx_addr_a,
input [ 15: 0 ] tx_data_a,
input tx_en_a,
output[ 15: 0 ] tx_q_a,
output tx_out
);
localparam PAC_INTERVAL = 93; //packect interval,modify Pac_Interval value from 96 to 93.
wire tx_phy_rdy;
reg tx_phy_rdy_r;
reg tx_phy_rdy_ack;
always@( posedge clk_100M or posedge rst )
if ( rst )
begin
tx_phy_rdy_r <= 0;
end
else
begin
if ( tx_phy_rdy )
tx_phy_rdy_r <= 1'b1;
else if ( tx_phy_rdy_ack )
tx_phy_rdy_r <= 1'b0;
end
reg [ 7: 0 ] tx_phy_data;
reg tx_phy_en;
reg tx_phy_vld;
reg [ 9: 0 ] tx_addr_b_r;
reg tx_pinpong;
wire [ 10: 0 ] tx_addr_b = { tx_pinpong, tx_addr_b_r };
wire [ 15: 0 ] tx_q_b;
reg [ 10: 0 ] tx_num_b;
reg tx_wren_b;
reg [ 7: 0 ] pac_interval;
reg [ 2: 0 ] link_st;
always@( posedge clk_100M or posedge rst )
if ( rst )
begin
pac_interval <= 0;
tx_phy_data <= 0;
tx_phy_vld <= 0;
tx_phy_en <= 0;
tx_phy_rdy_ack <= 0;
tx_addr_b_r <= 1023;
tx_pinpong <= 0;
tx_num_b <= 0;
tx_wren_b <= 0;
link_st <= 0;
end
else
case ( link_st )
0:
begin
tx_phy_data <= 0;
tx_phy_vld <= 0;
tx_phy_en <= 0;
tx_addr_b_r <= 1023;
tx_num_b <= 0;
tx_wren_b <= 0;
if ( pac_interval == PAC_INTERVAL )
link_st <= 1;
else
pac_interval <= pac_interval + 1'b1;
end
1:
begin
pac_interval <= 0;
if ( tx_q_b[ 15 ] )
begin
tx_addr_b_r <= 0;
tx_phy_vld <= 1'b1;
link_st <= 2;
end
end
2:
begin
tx_num_b <= tx_q_b[ 10: 0 ]; //valid bytes number
link_st <= 3;
end
3:
begin
if ( tx_phy_rdy_r )
begin
tx_phy_en <= 1'b1;
tx_phy_data <= tx_q_b[ 7: 0 ];
tx_num_b <= tx_num_b - 1'b1;
tx_phy_rdy_ack <= 1'b1;
link_st <= 4;
end
end
4:
begin
tx_phy_en <= 1'b0;
tx_phy_rdy_ack <= 1'b0;
if ( tx_num_b == 0 )
begin
tx_wren_b <= 1;
tx_addr_b_r <= 1023;
link_st <= 7;
end
else
link_st <= 5;
end
5:
begin
if ( tx_phy_rdy_r )
begin
tx_phy_en <= 1'b1;
tx_phy_data <= tx_q_b[ 15: 8 ];
tx_num_b <= tx_num_b - 1'b1;
tx_phy_rdy_ack <= 1'b1;
link_st <= 6;
end
end
6:
begin
tx_phy_en <= 1'b0;
tx_phy_rdy_ack <= 1'b0;
if ( tx_num_b == 0 )
begin
tx_wren_b <= 1;
tx_addr_b_r <= 1023;
link_st <= 7;
end
else
begin
tx_addr_b_r <= tx_addr_b_r + 1'b1;
link_st <= 3;
end
end
7:
begin
tx_wren_b <= 0;
tx_phy_vld <= 1'b0;
tx_pinpong <= tx_pinpong + 1'b1;
link_st <= 0;
end
default:
link_st <= 0;
endcase
tx_pingpong_buffer tx_pingpong_buffer_inst
(
//====================================a port, being connected app layer
.clock_a ( clk_100M ),
.address_a ( tx_addr_a ),
.rden_a ( 1'b1 ),
.wren_a ( tx_en_a ),
.data_a ( tx_data_a ),
.q_a ( tx_q_a ),
//====================================b port,
.clock_b ( clk_100M ),
.address_b ( tx_addr_b ),
.rden_b ( 1'b1 ),
.wren_b ( tx_wren_b ),
.data_b ( 16'b0 ),
.q_b ( tx_q_b )
);
tx_data_phy
#(
.IDLE( IDLE )
)
tx_data_phy_inst
(
.rst ( rst ),
.clk_100M ( clk_100M ),
.tx_phy_rdy ( tx_phy_rdy ),
.tx_phy_vld ( tx_phy_vld ),
.tx_phy_en ( tx_phy_en ),
.tx_phy_data( tx_phy_data ),
.tx_out ( tx_out )
);
endmodule
上述代码中link_st状态机转移图如下所示:

图9 link_st状态转移图
(2)顶层文件接口修改
module hs_txrx
#(
parameter IDLE = 10'b1010_1010_10
)
(
input inclk,
input pll_rst,
input [ 10: 0 ] tx_addr,
input [ 15: 0 ] tx_data,
input tx_en,
output[ 15: 0 ] tx_q,
output tx,
input rx,
output rx_data_vld,
output rx_data_rdy,
output [ 7: 0 ] rx_data,
output clk_100M,
output pll_locked
);
wire clk_400M;
wire rst = !pll_locked;
trans_data_link
#(
.IDLE( IDLE )
)
trans_data_link_inst
(
.rst ( rst ),
.clk_100M ( clk_100M ),
.tx_addr_a ( tx_addr ),
.tx_data_a ( tx_data ),
.tx_en_a ( tx_en ),
.tx_q_a ( tx_q ),
.tx_out ( tx )
);
recv_data_link #
(
.SYNC_START1( 10'b0011111010 ), //K28.5 with dipin=0;
.SYNC_START2( 10'b1100000101 ), //K28.5 with dispin=1
.SYNC_END1 ( 10'b0011111001 ), //K28.1 with dipin=0;
.SYNC_END2 ( 10'b1100000110 ) //K28.1 with dispin=1
)
recv_data_link_inst
(
.rst ( rst ),
.clk_100M ( clk_100M ),
.clk_400M ( clk_400M ),
.rx ( rx ),
.link_rx_data_rdy ( rx_data_rdy ),
.link_rx_data ( rx_data ),
.link_rx_data_vld ( rx_data_vld )
);
pll1 pll1_inst
(
.areset ( pll_rst ), //1'b0;
.inclk0 ( inclk ),
.c0 ( clk_100M ),
.c1 ( clk_400M ),
.locked ( pll_locked )
);
endmodule
(3)仿真文件如下:
`timescale 1 ns / 1 ps
module tb_pinpong
( );
parameter [ 10: 0 ] LEN = 16;
parameter PERIOD = 20 ; //周期20
parameter IDLE = 10'b1010_1010_10;
reg inclk ; // 模拟板级提供的输入时钟
initial
begin
inclk = 1'b0;
#( PERIOD / 2 );
forever
#( PERIOD / 2 ) inclk = ~inclk;
end
reg pll_rst; //实体模块PLL的rst信号
wire tx_rx; // 收发在testbench的回环信号
wire clk_100M;
wire pll_locked; //实体PLL锁定信号
wire rst = !pll_locked; //Testbench内生成复位信号
reg [ 9: 0 ] tx_addr_r;
reg tx_pinpong;
wire [ 10: 0 ] tx_addr = { tx_pinpong, tx_addr_r };
reg [ 15: 0 ] tx_data; //待发送数据
reg [ 7: 0 ] tx_data_r;
reg tx_en;
wire [ 15: 0 ] tx_q;
wire rx_data_vld;
wire rx_data_rdy;
wire [ 7: 0 ] rx_data;
reg [ 7: 0 ] frame_len;
reg [ 2: 0 ] tx_st;
initial
begin
pll_rst = 1'b1;
#50 pll_rst = 1'b0;
end
//=====================================schedule for multi_task or multi_procedure ;
reg tx_rdy; //master schedule and authorise tx precudure become active stream.
reg tx_rdy_ack;
reg [ 2: 0 ] sch_st; //mimic master schedule,
always@( posedge clk_100M or posedge rst )
if ( rst )
begin
tx_rdy <= 0;
sch_st <= 0;
end
else
case ( sch_st )
0:
begin
tx_rdy <= 1'b1;
if ( tx_rdy_ack )
begin
tx_rdy <= 1'b0;
sch_st <= 1;
end
end
1:
begin
sch_st <= 2;
end
2:
begin
sch_st <= 3;
end
3:
begin
sch_st <= 4;
end
4:
begin
sch_st <= 5;
end
5:
begin
sch_st <= 6;
end
6:
begin
sch_st <= 7;
end
7:
begin
sch_st <= 0;
end
default:
sch_st <= 0;
endcase
wire [ 7: 0 ] tx_data_p = tx_data_r + 1;
always@( posedge clk_100M or posedge rst )
if ( rst )
begin
frame_len <= 0;
tx_addr_r <= 1023;
tx_pinpong <= 0;
tx_data <= 0;
tx_data_r <= 0;
tx_en <= 1'b0;
tx_rdy_ack <= 0;
tx_st <= 0;
end
else
begin
case ( tx_st )
0:
begin
tx_addr_r <= 1023;
tx_pinpong <= 0;
tx_data <= 0;
tx_rdy_ack <= 0;
frame_len <= 0;
tx_st <= 1;
end
1:
begin
tx_rdy_ack <= 0;
if ( tx_rdy )
tx_st <= 2;
end
2:
begin
if ( tx_q[ 15 ] )
tx_st <= 6;
else
tx_st <= 3;
end
3:
begin
if ( frame_len >= LEN )
begin
tx_en <= 1'b0;
tx_st <= 4;
end
else
begin
tx_data <= { tx_data_p, tx_data_r };
frame_len <= frame_len + 2;
tx_addr_r <= tx_addr_r + 1;
tx_data_r <= tx_data_r + 2;
tx_en <= 1'b1;
end
end
4:
begin
tx_data <= { 1'b1, 4'b0, LEN };
frame_len <= 0;
tx_addr_r <= 1023;
tx_en <= 1'b1;
tx_st <= 5;
end
5:
begin
tx_en <= 1'b0;
tx_pinpong <= tx_pinpong + 1'b1;
tx_st <= 6;
end
6:
begin
tx_rdy_ack <= 1'b1;
tx_st <= 7;
end
7:
begin
tx_rdy_ack <= 1'b0;
tx_st <= 1;
end
default:
tx_st <= 0;
endcase
end
hs_txrx
#( .IDLE( IDLE ) )
hs_txrx_inst
(
.inclk ( inclk ),
.pll_rst ( pll_rst ),
.tx_addr ( tx_addr ),
.tx_data ( tx_data ),
.tx_en ( tx_en ),
.tx_q ( tx_q ),
.tx ( tx_rx ),
.rx ( tx_rx ),
.rx_data_vld( rx_data_vld ),
.rx_data_rdy( rx_data_rdy ),
.rx_data ( rx_data ),
.clk_100M ( clk_100M ),
.pll_locked ( pll_locked )
);
endmodule
tx_st状态机转移图如下所示:

图10
3. 仿真

图11 -1 tb_pingong.v 仿真波形1
图11-1 展示了对data_link层乒乓buffer的写控制,注意观察乒乓buffer写端控制字的状态以及应用层的任务调度tx_rdy的状态。看看APP层是否已经由原来的慢速写入(由于发送没有数据缓冲与发送端各层形成了胶着逻辑)变成需要时快速写入,将有更多的时间处理其它任务。

图11 -2 tb_pingong.v 仿真波形2
查看图11-2波形中接收数据是否与发送数据一致。

图11 -3 tb_pingong.v 仿真波形3
图11-3显示了,一旦乒乓Buffer有空余的存储空间,并且调度任务满足写过程要求(tx_rdy),完成写数据包操作。

图12 data-link层 乒乓Buffer数据读出与Phy层接口
总结:在数据链路层加入乒乓Buffer后,打破了发送端各层的胶织逻辑(glue logic), 使异步过程的调度更加流畅,对于提高系统整体性能起到了关键作用。乒乓Buffer的数量越多,对异步过程的处理效果也会越好,但FPGA的资源消耗也会随着增加。
本程序虽然提供仿真,但是并没有经过严格测试,开发者至少提供不同包长以及包中字节为奇数的测试。
由于本次课程的程序较长,对程序细节并没有仔细解释,细节解释请观看对应的视频。
quartus 工程文件
[content_control]
[/content_control]
老师,trans_data_link.link_st中,状态4跳状态7改pingpong的问题,文章中没改。
已经修改,同时增加了工程项目文件,谢谢
把双端口ROM的地址设置成这样: tx_addr_b = { tx_pingpang, tx_addr_b_r}; 有什么好处吗?
这两个问题都在视频中给出了详细,全面的解答,希望同学可以自己看相关视频,并在视频中老师提供思路的引导启发下,自己找出答案。

链接如下:
高速通信乒乓Buffer数据缓冲Verilog实现–发送端-1
高速通信乒乓Buffer数据缓冲Verilog实现–发送端-2
高速通信乒乓Buffer数据缓冲Verilog实现–发送端-3
高速通信乒乓Buffer数据缓冲Verilog实现–发送端-4
data_link层是从上层的RAM中拿数据吗?data_link层代码中,状态机1和状态机2是不是可以合并?为什么这里等了一拍,而不是直接去拿数据?
这两个问题都在视频中给出了详细,全面的解答,希望同学可以自己看相关视频,并在视频中老师提供思路的引导启发下,自己找出答案。

链接如下:
高速通信乒乓Buffer数据缓冲Verilog实现–发送端-1
高速通信乒乓Buffer数据缓冲Verilog实现–发送端-2
高速通信乒乓Buffer数据缓冲Verilog实现–发送端-3
高速通信乒乓Buffer数据缓冲Verilog实现–发送端-4