【芯片前端】保持代码手感——异步FIFO全解析

简介: 【芯片前端】保持代码手感——异步FIFO全解析

前言

关于FIFO和异步处理我已经写过很多东西了:


进阶之路——二进制与格雷码的相互转换模块设计


【异步FIFO的一些小事·0】异步FIFO同步化设计


【异步FIFO的一些小事·1】空满判断与格雷码


【异步FIFO的一些小事·2】异步FIFO中异步走线延时约束的一些思考


【异步FIFO的一些小事·3】异步FIFO中指针走线延时的一些思考


【异步电路碎碎念5】 —— 跨异步处理的几个注意事项


【芯片前端】保持代码手感——同步FIFO


这次因为保持代码手感的需要,重新写一次异步fifo。写的过程突然感觉自己成长了许多,毕业时候抠抠索索的照着教程写了好几天,到今天心里合了一下时序一个多小时就手撕代码完成,看来这几年班不是白上的啊~


整体结构

整体的思路很常规,模块分为四个部分:写通路控制、读通路控制、异步单元、双口RAM。


写通路控制:负责产生ram写地址和写有效,产生wfull信号;


读通路控制:负责产生ram读地址和读有效,产生rempty信号;


异步单元:负责地址指针的跨异步处理;


双口RAM:负责存储数据;


整体结构图如下:



顶层如下:

module asyn_fifo#(
  parameter WIDTH = 8,
  parameter   DEPTH = 16
)(
  input           wclk  , 
  input           rclk  ,   
  input           wrstn ,
  input         rrstn ,
  input           winc  ,
  input           rinc  ,
  input     [WIDTH-1:0] wdata ,
  output wire       wfull ,
  output wire       rempty  ,
  output wire [WIDTH-1:0] rdata
);
//===================================
//写通路控制模块
//===================================
wire [$clog2(DEPTH) :0]raddr_sync;
wire wenc;
wire [$clog2(DEPTH) :0]waddr;
wctrl_path #(
  .DEPTH(DEPTH))
u_wctrl_path(
  .wclk(wclk),
  .wrstn(wrstn),
  .winc(winc),
  .raddr_sync(raddr_sync),
  .wenc(wenc),
  .waddr(waddr),
  .wfull(wfull)
);
//===================================
//读通路控制模块
//===================================
wire [$clog2(DEPTH) :0]waddr_sync;
wire renc;
wire [$clog2(DEPTH) :0]raddr;
rctrl_path #(
  .DEPTH(DEPTH))
u_rctrl_path(
  .rclk(rclk),
  .rrstn(rrstn),
  .rinc(rinc),
  .waddr_sync(waddr_sync),
  .renc(renc),
  .raddr(raddr),
  .rempty(rempty)
);
//===================================
//ram控制
//===================================
dual_port_RAM #(
  .DEPTH(DEPTH),
  .WIDTH(WIDTH))
u_dual_port_RAM(
  .wclk(wclk),
  .wenc(wenc),
  .waddr(waddr[$clog2(DEPTH)-1:0]),
  .wdata(wdata),
  .rclk(rclk),
  .renc(renc),
  .raddr(raddr[$clog2(DEPTH)-1:0]),
  .rdata(rdata)
);
//===================================
//写跨异步
//===================================
gary_sync_cell#(
  .WIDTH($clog2(DEPTH)+1),
  .SYNC_CYC(3))
u_wgary_sync_cell(
  .in_clk(wclk),
  .in_rst_n(wrstn),
  .out_clk(rclk),
  .out_rst_n(rrstn),
  .in_data(waddr),
  .out_data(waddr_sync)
);
//===================================
//读跨异步
//===================================
gary_sync_cell#(
  .WIDTH($clog2(DEPTH)+1),
  .SYNC_CYC(3))
u_rgary_sync_cell(
  .in_clk(rclk),
  .in_rst_n(rrstn),
  .out_clk(wclk),
  .out_rst_n(wrstn),
  .in_data(raddr),
  .out_data(raddr_sync)
);
endmodule


模块说明

wctrl_path

我们来完成wctrl_path的构建,首先明确写这个模块的接口:

module wctrl_path#(
  parameter WIDTH = 8,
  parameter   DEPTH = 16,
  parameter   ADDR_WD = $clog2(DEPTH)
)(
  input           wclk,   
  input           wrstn,
  input           winc,
  input  [ADDR_WD   :0]   raddr_sync,
  output                  wenc,
  output [ADDR_WD   :0] waddr,
  output                  wfull
);


写时钟和写复位肯定是必须的,winc是外部的写有效。还有一个必要的输入信号是raddr_sync信号,这个信号的作用是和waddr一起产生写侧看到的fifo_cnt信号,进而得到wfull信号。那么考点1来了:为什么要在写时钟域产生wfull信号,在读时钟域产生rempty信号?


在“【异步FIFO的一些小事·1】空满判断与格雷码”这个博客中已经做过说明,简单而言就是:在写时钟域看到的读指针是有延迟的,对于wfull而言,晚一些看到有数据被读取走也不会影响数据,最多就是wfull信号晚一些撤销而已,而wfull晚撤销不会是数据被覆盖,只会影响到性能(多反压了一会),而性能的问题是可以通过计算一个合理的异步fifo深度开销进行弥补的。同理在读时钟域看到写指针也是有延迟的,因此rempty可能撤销不及时,不及时也没关系最多就是等一会反应过来再读,而不会读取错误数据。


好的,输入信号分析完成,输出信号显然有给ram的wenc和waddr,wdata直接从顶层过去不从这里过手了。还有就是刚刚说的wfull信号。


在“【芯片前端】保持代码手感——同步FIFO”里提到过,考点2:fifo中计算读写地址会用位宽扩一比特的计数器,最高比特作为标志位来判断绕圈,那么把这个模块做一下:

module fifo_cnt#(
  parameter   DEPTH = 8,
  parameter   WITDH = $clog2(DEPTH)
)(
  input         clk,
  input         rst_n,
  input           en,
  output [WITDH :0] cnt
);
reg  [WITDH   :0]cnt;
wire             cnt_d_h;
wire [WITDH -1:0]cnt_d_l;
assign cnt_d_h = (cnt[WITDH-1:0] == DEPTH-1) ? ~cnt[WITDH] : cnt[WITDH];
assign cnt_d_l = (cnt[WITDH-1:0] == DEPTH-1) ? 0 : cnt[WITDH-1:0] + {{(WITDH-1){1'b0}}, 1'b1};
always @(posedge clk or negedge rst_n)begin
  if(~rst_n)  cnt <= 0;
  else if(en) cnt <= {cnt_d_h, cnt_d_l};
end
endmodule


基于这个模块可以轻松的产生waddr,那么进一步得到wenc和wfull的逻辑就非常简单了:

//得到写ram的waddr,fifo_cnt是最高比特为标志位的特殊计数器
assign wenc  = winc && (!wfull);
fifo_cnt #(
  .DEPTH(DEPTH)
)u_fifo_cnt(
  .clk(wclk),
  .rst_n(wrstn),
  .en(wenc),
  .cnt(waddr)
);
//生成wfull信号,wfull信号必须在写时钟域生成,因为读信息延迟到达写时钟域,也不会对wfull信息有影响,不会有数据丢失
wire [ADDR_WD :0]fifo_cnt = (waddr[ADDR_WD] == raddr_sync[ADDR_WD]) ? waddr[ADDR_WD-1:0] - raddr_sync[ADDR_WD-1:0]:
                    (waddr[ADDR_WD-1:0] + DEPTH - raddr_sync[ADDR_WD-1:0]);
assign wfull = (fifo_cnt == DEPTH);
endmodule 



注意这里的waddr还是扩充了一比特的地址,送给ram的时候要取[DEPTH -1:0],而送到跨异步模块时就直接全位宽送,因为计算fifo_cnt要用到全位宽。


rctrl_path

和wctrl_path的思路一样,不赘述:

module rctrl_path#(
  parameter WIDTH = 8,
  parameter   DEPTH = 16,
  parameter   ADDR_WD = $clog2(DEPTH)
)(
  input           rclk,   
  input           rrstn,
  input           rinc,
  input  [ADDR_WD   :0]   waddr_sync,
  output                  renc,
  output [ADDR_WD   :0] raddr,
  output                  rempty
);
//得到读ram的waddr,fifo_cnt是最高比特为标志位的特殊计数器
assign renc  = rinc && (!rempty);
fifo_cnt #(
  .DEPTH(DEPTH)
)u_fifo_cnt(
  .clk(rclk),
  .rst_n(rrstn),
  .en(renc),
  .cnt(raddr)
);
//生成rempty信号,rempty信号必须在读时钟域生成,因为写信息延迟到达读时钟域,也不会对rempty信息有影响,最多影响读性能
wire [ADDR_WD :0]fifo_cnt = (waddr_sync[ADDR_WD] == raddr[ADDR_WD]) ? waddr_sync[ADDR_WD-1:0] - raddr[ADDR_WD-1:0]:
                      (waddr_sync[ADDR_WD-1:0] + DEPTH - raddr[ADDR_WD-1:0]);
assign rempty = (fifo_cnt == 0);
endmodule 


gary_sync_cell

gary_sync_cell专门用来计数器跨异步,思路很简单:二进制输入,转格雷码打拍,跨异步打三拍,格雷码转回二进制输出。那么考点3又来了:为什么异步fifo里要用格雷码跨异步?



基于之前的积累“进阶之路——二进制与格雷码的相互转换模块设计” ,二进制转格雷码和格雷码转二进制的模块直接做:

module b2g_conv #(parameter SIZE = 4
)(
    output [SIZE -1:0] gray,
    input  [SIZE -1:0] binary
);
assign gray = (binary >> 1) ^ binary;
endmodule
module g2b_conv #(
  parameter SIZE = 4
)(
    output reg [SIZE -1:0] binary,
    input      [SIZE -1:0] gray
);
integer k;
always @(gray)
begin
    for (k = 0; k < SIZE; k = k + 1)
        binary[k] = ^(gray >> k);
end
endmodule



OK,那么下一步就是对比特跨异步的问题了。下一个考点4自然就是:多比特如何跨异步?那在这里就不啰嗦了【异步电路碎碎念4】 —— 跨异步的处理方法已经写过 ,欢迎指正。


要做多比特跨异步,当然要先有单比特跨异步模块:

module sync_cell #(
  parameter SYNC_CYC = 2
)(
  input  clk,
  input  rst_n,
  input  in,
  output out
);
wire [SYNC_CYC :0]in_dff;
assign in_dff[0] = in;
assign out = in_dff[SYNC_CYC];
genvar i;
generate
    for(i=1; i<=SYNC_CYC; i=i+1)begin: inst_rtl
        dffr u_dffr[i](clk, rst_n, in_dff[i-1], in_dff[i]);
    end
endgenerate
endmodule



拍数可配,默认是2拍建议配置为3拍,那么考点5:为什么现在跨异步打拍要打3拍?

基于单比特跨异步单元,我们进一步得到多比特跨异步单元:

//将格雷码在目的时钟域打三拍跨异步
wire [WIDTH -1:0]out_data_gray;
genvar i;
generate
    for(i=0; i<WIDTH; i=i+1)begin: inst_rtl
        sync_cell #(
          .SYNC_CYC(SYNC_CYC))
        u_sync_cell(
          .clk(out_clk),
          .rst_n(out_rst_n),
          .in(in_data_gray_ff[i]),
          .out(out_data_gray[i])
        );
    end
endgenerate


注意千万要用目的时钟打拍啊。


ok,因此完成的gray_sync_cell的代码就有了:

module gary_sync_cell#(
  parameter WIDTH = 8,
  parameter SYNC_CYC = 3
)(
  input in_clk,
  input in_rst_n,
  input out_clk,
  input out_rst_n,
  input [WIDTH -1:0] in_data,
  output[WIDTH -1:0] out_data
);
//将输入转为格雷码并在源时钟域打一拍
wire [WIDTH -1:0]in_data_gray;
wire [WIDTH -1:0]in_data_gray_ff;
b2g_conv #(
  .SIZE(WIDTH))
u_b2g_conv(
  .binary(in_data),
  .gray(in_data_gray) 
);
dffr#(
  .WIDTH(WIDTH))
u_in_data_dffr(
  .clk(in_clk),
  .rst_n(in_rst_n),
  .d(in_data_gray),
  .q(in_data_gray_ff)
);
//将格雷码在目的时钟域打三拍跨异步
wire [WIDTH -1:0]out_data_gray;
genvar i;
generate
    for(i=0; i<WIDTH; i=i+1)begin: inst_rtl
        sync_cell #(
          .SYNC_CYC(SYNC_CYC))
        u_sync_cell(
          .clk(out_clk),
          .rst_n(out_rst_n),
          .in(in_data_gray_ff[i]),
          .out(out_data_gray[i])
        );
    end
endgenerate
//将格雷码转化为二进制
g2b_conv #(
  .SIZE(WIDTH))
u_g2b_conv(
  .gray(out_data_gray),
    .binary(out_data)
);
endmodule



双口RAM

用的是默认代码:

module dual_port_RAM #(parameter DEPTH = 16,
             parameter WIDTH = 8)(
   input wclk ,
   input wenc ,
   input [$clog2(DEPTH)-1:0] waddr  ,
   input [WIDTH-1:0] wdata  ,
   input rclk ,
   input renc ,
   input [$clog2(DEPTH)-1:0] raddr  ,
   output reg [WIDTH-1:0] rdata
);
reg [WIDTH-1:0] RAM_MEM [0:DEPTH-1];
always @(posedge wclk) begin
  if(wenc)
    RAM_MEM[waddr] <= wdata;
end 
always @(posedge rclk) begin
  if(renc)
    rdata <= RAM_MEM[raddr];
end 
endmodule  


以上,完整的代码呈现。


波形验证

因为主要是叙述个思路,没有做太细致的波形验证,就用auto_verification生成了个简单的验证环境,没有这个工具的请参见【芯片前端】一键生成简易版本定向RTL验证环境的脚本——auto_verification,工具有点些修改,最新的链接是:链接:https://pan.baidu.com/s/1CMptxDGketFHuLeNOLcV0Q,提取码:t6o8。


把所有文件放置于async_fifo目录下,键入auto_verification -f async_fifo,生成仿真环境,简单修改testbench,得到如下波形:



真的是很随意的仿了一下,如果大家发现有问题请不吝赐教~感谢~


相关文章
|
存储 前端开发 安全
前端如何存储数据:Cookie、LocalStorage 与 SessionStorage 全面解析
本文全面解析前端三种数据存储方式:Cookie、LocalStorage与SessionStorage。涵盖其定义、使用方法、生命周期、优缺点及典型应用场景,帮助开发者根据登录状态、用户偏好、会话控制等需求,选择合适的存储方案,提升Web应用的性能与安全性。(238字)
765 0
|
8月前
|
Web App开发 前端开发 JavaScript
前端性能优化利器:图片懒加载实战解析
前端性能优化利器:图片懒加载实战解析
|
10月前
|
人工智能 自然语言处理 前端开发
DeepSite:基于DeepSeek的开源AI前端开发神器,一键生成游戏/网页代码
DeepSite是基于DeepSeek-V3模型的在线开发工具,无需配置环境即可通过自然语言描述快速生成游戏、网页和应用代码,并支持实时预览效果,显著降低开发门槛。
1774 93
DeepSite:基于DeepSeek的开源AI前端开发神器,一键生成游戏/网页代码
|
10月前
|
存储 前端开发 JavaScript
调用DeepSeek API增强版纯前端实现方案,支持文件上传和内容解析功能
本方案基于DeepSeek API增强版,提供纯前端实现的文件上传与内容解析功能。通过HTML和JavaScript,用户可选择文件并调用API完成上传及解析操作。方案支持多种文件格式(如PDF、TXT、DOCX),具备简化架构、提高响应速度和增强安全性等优势。示例代码展示了文件上传、内容解析及结果展示的完整流程,适合快速构建高效Web应用。开发者可根据需求扩展功能,满足多样化场景要求。
3104 64
|
9月前
|
存储 前端开发 JavaScript
|
7月前
|
人工智能 JSON 前端开发
如何解决后端Agent和前端UI之间的交互问题?——解析AG-UI协议的神奇作用
三桥君指出AG-UI协议通过SSE技术实现智能体与前端UI的标准化交互,解决流式传输、实时进度显示、数据同步等开发痛点。其核心功能包括结构化事件流、多Agent任务交接和用户中断处理,具有"一次开发到处兼容"、"UI灵活可扩展"等优势。智能体专家三桥君认为协议将AI应用从聊天工具升级为实用软件,适用于代码生成、多步骤工作流等场景,显著提升开发效率和用户体验。
1719 0
|
8月前
|
JSON 前端开发 安全
前端开发中常用的鉴权方式解析与实践要点
本文深入探讨了前端开发中常用的鉴权方式,包括HTTP基本鉴权、Session-Cookie鉴权、Token验证、JWT(JSON Web Tokens)、单点登录(SSO)和OAuth等。文章首先明确了认证、授权、鉴权和权限控制的概念及关系,随后详细解析每种鉴权方式的工作原理、优缺点及适用场景。例如,HTTP基本鉴权简单但安全性低,适合内部网络;Session-Cookie鉴权易受CSRF攻击,适用于同域Web应用;Token和JWT无状态且扩展性好,适合分布式系统;SSO提升用户体验,适用于多系统统一登录;OAuth安全方便,适合第三方授权接入。
850 2
|
9月前
|
自然语言处理 前端开发 IDE
用通义灵码全新智能体+MCP实现从设计稿到前端代码,个人免费用
通义灵码全新升级,发布国内首个支持“自主决策+工具链闭环”的编程智能体,面向个人免费!新增功能包括智能体模式、混合推理模型Qwen3支持、全面集成MCP中文社区(涵盖2400+服务)及长期记忆能力。用户可通过IDE插件使用,兼容主流开发环境如JetBrains、VS Code和Visual Studio。教程展示如何将MasterGo设计稿转化为前端代码,简化开发流程。探索链接:[通义灵码官网](https://lingma.aliyun.com/)。
|
10月前
|
前端开发 JavaScript 安全
|
11月前
|
消息中间件 JavaScript 前端开发
最细最有条理解析:事件循环(消息循环)是什么?为什么JS需要异步
度一教育的袁进老师谈到他的理解:单线程是异步产生的原因,事件循环是异步的实现方式。 本质是因为渲染进程因为计算机图形学的限制,只能是单线程。所以需要“异步”这个技术思想来解决页面阻塞的问题,而“事件循环”是实现“异步”这个技术思想的最主要的技术手段。 但事件循环并不是全部的技术手段,比如Promise,虽然受事件循环管理,但是如果没有事件循环,单一Promise依然能实现异步不是吗? 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您

热门文章

最新文章

推荐镜像

更多
  • DNS