FPGA 基本设计思想--乒乓操作、串并转换、流水线

发布于:2025-08-07 ⋅ 阅读:(18) ⋅ 点赞:(0)

乒乓操作(Ping-Pong)的理解:为什么是另一种pipeline?-CSDN博客

FPGA菜鸟学习笔记——2、四大设计思想 - 知乎

乒乓操作(Ping-Pong)-CSDN博客

乒乓操作原理与FPGA设计-CSDN博客

乒乓操作 — [野火]FPGA Verilog开发实战指南——基于Altera EP4CE10 征途Mini开发板 文档》

FPGA:零基础入门FPGA图像处理、DDR技巧之多帧缓存、乒乓操作、防止视频撕裂_哔哩哔哩_bilibili---讲的很好!

CMOS摄像头(4):SDRAM和乒乓操作 - 咸鱼IC - 博客园(博主记录蛮多的)

        (野火电子)由于我们设置的读写时钟不一致,而要实现乒乓操作的无缝缓存与处理,这里我们就需要设置不同的写入数据位宽与不同的读出数据位宽才能与读写时钟相匹配。这里就是我们前面理论部分所说的面积与速度互换原则的体现,这里我们设置的输入时钟的频率是输出时钟的两倍,即输入数据的速度是输出数据速度的两倍,所以这里我们需要设 置输出数据的位宽是输入数据位宽的两倍,即面积的两倍。换句话说就是我们输入速度与面积的乘积与输出速度与面积的乘积要相等,即输入和输出的时间相等,这样才能保证在“数据缓冲模块1”读/写完的同时“数据缓冲模块2”也写/读完,才能保证输入与输出数据的无缝传输与处理。这就是其低高速数据传输特点的原理,只要我们 遵从输入与输出数据的频率与位宽的乘积相等,那么我们就可以实现不同模块频率之间的数据传输。

AI对话:

乒乓操作有什么用处?

“乒乓操作” 是一种在数据处理、信号处理、硬件设计等领域常用的技术,其核心思想是通过两个 “缓冲区”(或存储区域)交替工作,实现数据的无缝处理与传输,避免因等待数据准备而产生的延迟。它的用处主要体现在以下几个方面:

1. 提高数据处理效率,减少等待时间

在实时数据处理场景中(如视频采集、音频处理、传感器数据读取等),数据通常是持续产生的,而处理器处理数据也需要时间。如果只用一个缓冲区,会出现 “数据未处理完,新数据又来了” 的冲突,导致处理器需要等待缓冲区清空才能继续。
乒乓操作通过两个缓冲区(A 和 B)解决此问题:

  • 当处理器处理缓冲区 A 的数据时,新数据可以写入缓冲区 B;
  • 处理器处理完 A 后,立即切换到处理 B,同时新数据写入 A。
    效果:数据采集和处理并行进行,消除了 “等待缓冲区空闲” 的时间,提升了系统的实时性和吞吐量。

2. 简化时序控制,降低同步复杂度

在硬件设计(如 FPGA、嵌入式系统)中,数据的输入、处理、输出往往有不同的时钟域或速率。如果直接连接,容易因时序不匹配导致数据丢失或错误。
乒乓操作通过两个缓冲区的交替切换,将 “连续数据流” 拆分为 “块数据” 处理:

  • 输入模块只需专注于向当前空闲的缓冲区写数据,无需关心处理器是否忙碌;
  • 处理器只需从已写满的缓冲区读数据,无需关心新数据的写入节奏。
    效果:隔离了输入、处理、输出的时序差异,降低了模块间的同步难度,提高了系统稳定性。

3. 平滑数据流量,避免突发瓶颈

在数据传输速率不稳定的场景(如网络通信、磁盘读写)中,数据可能以 “突发” 形式到达,而处理器的处理能力是固定的。如果直接处理突发数据,可能因瞬时负载过高导致卡顿。
乒乓操作通过两个缓冲区 “缓冲” 突发数据:

  • 当突发数据到来时,先存入缓冲区 A,满了之后自动切换到缓冲区 B;
  • 处理器按固定速率从已填满的缓冲区中读取数据,不受输入速率波动影响。
    效果:将 “不稳定的输入流量” 转化为 “稳定的处理流量”,避免系统因突发数据而过载。

4. 典型应用场景

  • 视频 / 图像处理:摄像头采集的图像帧写入缓冲区 A,处理器同时处理缓冲区 B 的图像,实现 “采集 - 处理” 无缝衔接;
  • 音频实时处理:麦克风输入的音频流写入缓冲区 A,DSP 芯片处理缓冲区 B 的音频,避免声音卡顿;
  • 通信系统:无线模块接收的数据存入缓冲区 A,CPU 处理缓冲区 B 的数据,确保数据不丢失;
  • FPGA / 嵌入式开发:在硬件逻辑中,通过乒乓操作实现高速 AD/DA(模数 / 数模转换)的数据缓存与处理。

总结

乒乓操作的核心价值是通过 “空间换时间”,利用两个缓冲区的交替工作,实现数据 “采集 - 处理 - 输出” 的并行化,从而提升系统的实时性、效率和稳定性,尤其适用于需要 “连续、高速、无间断” 处理数据的场景。

我不太明白, 空间换时间的概念, 直接把两块缓冲区合并成一块, 然后控制读写时间不也可以达到乒乓操作的作用吗?

这个问题很关键,核心在于理解 “单块大缓冲区” 和 “两块乒乓缓冲区” 在读写冲突处理并行性上的本质区别。我们可以从 “空间换时间” 的具体含义入手,对比两种方案的差异:

首先明确:什么是 “空间换时间”?

这里的 “空间” 指额外的存储资源(第二块缓冲区),“时间” 指因等待读写冲突而浪费的时间。乒乓操作通过增加一块缓冲区的空间,避免了读写操作的 “排队等待”,从而实现了 “数据写入” 和 “数据处理” 的完全并行,这就是 “用空间换时间” 的核心逻辑。

为什么单块大缓冲区无法替代乒乓操作?

假设我们把两块缓冲区合并成一块 “大缓冲区”,并试图通过控制读写时间来避免冲突(比如 “先写一部分,再读一部分”),会遇到两个无法解决的问题:

1. 读写操作无法完全并行,必然存在等待

单块缓冲区中,“写入新数据” 和 “读取旧数据” 不能同时进行(否则会出现数据覆盖或读取到不完整数据)。例如:

  • 假设缓冲区大小为 200 字节,每次写入 100 字节,处理器每次读取 100 字节。
  • 流程只能是:先写 1-100 字节 → 处理器读 1-100 字节(此时无法写入新数据,必须等待读取完成)→ 再写 101-200 字节 → 处理器读 101-200 字节(再次等待)。

问题:写入和读取是 “串行交替” 的,写入必须等读取完成,读取也必须等写入完成,中间存在 “空闲等待时间”。

2. 无法处理 “实时连续数据流” 的覆盖风险

在实时场景中(如视频采集、传感器数据),数据是持续不断产生的(比如摄像头每秒产生 30 帧图像,每帧必须立即处理,否则会丢失)。此时单块缓冲区会面临:

  • 若处理器处理速度稍慢,新数据会 “追上” 并覆盖还未处理的旧数据(比如刚写了 1-100 字节,处理器还在处理 1-100,新数据已经要写 101-200,看似没问题;但如果处理器卡顿,新数据可能在 1-100 还没读完时就开始覆盖,导致数据错误)。
  • 若为了避免覆盖,必须让写入速度严格慢于读取速度,这会牺牲数据吞吐量(比如摄像头明明能每秒传 30 帧,却只能被迫降为 20 帧,因为处理器处理不过来)。

乒乓操作如何解决这些问题?

两块缓冲区(A 和 B)的关键作用是物理隔离读写操作,实现 “写入” 和 “读取” 的完全并行:

  • 当新数据写入 A 时,处理器可以同时读取 B(两者互不干扰,无等待);
  • 当 A 写满、B 读完后,立即 “切换”:新数据写入 B,处理器读取 A(依然并行)。

这种 “物理分离” 确保了:

  • 写入永远不需要等读取(总有一块空闲缓冲区可用);
  • 读取永远不需要等写入(读取的是已经写满的 “旧缓冲区”,不会被新数据覆盖)。

举个直观的例子:用两个水桶接水和倒水

  • 单桶方案:一个大水桶,你一边接水(写入),另一边必须等接满才能倒水(读取),接水时不能倒水,倒水时不能接水,中间有等待时间;如果水流太快,没倒完就继续接,水会溢出(数据覆盖)。
  • 双桶(乒乓)方案:两个水桶,你用桶 A 接水时,同时可以倒桶 B 的水(完全并行);A 接满后立刻换 B 接水,同时倒 A 的水。全程无等待,水流再快也不会溢出,效率更高。

总结:单块缓冲区的核心局限

单块缓冲区无论多大,本质上是 “同一存储区域”,读写操作必须串行交替,无法避免等待;而乒乓操作通过两块物理隔离的缓冲区,实现了 “写入” 和 “读取” 的并行化,这正是 “空间(多一块缓冲区)换时间(消除等待)” 的价值。

简单说:单块缓冲区是 “一条车道,读写轮流走”;乒乓操作是 “两条车道,读写同时走”—— 后者的效率提升,靠的就是多出来的那一条 “车道空间”。

你说的 读写操作必须串行交替,无法避免等待我不认同,在FPGA中 双口ram 不就可以同时读写吗? 只是最开始要等待而已?

你提到的双口 RAM(Dual-Port RAM)是个非常好的切入点,它确实能实现 “同时读写”,但这与乒乓操作的核心目标(解决 “连续数据流的无缝处理” 和 “动态边界同步” 问题)仍有本质区别。我们可以从 FPGA 的实际应用场景出发,对比双口 RAM 和乒乓操作的差异,就能明白为什么双口 RAM 不能完全替代乒乓操作,以及 “空间换时间” 在其中的深层逻辑。

首先明确:双口 RAM 的 “同时读写” 有什么局限?

双口 RAM 通过两个独立的端口(读端口和写端口)实现 “同一时刻对不同地址的读写”,这确实打破了 “单口 RAM 必须串行读写” 的限制。但在连续、高速、无固定边界的数据流处理中,它仍存在两个关键问题:

1. 无法解决 “数据覆盖” 与 “处理滞后” 的矛盾

假设用双口 RAM 处理摄像头的图像数据流(每帧 1024 字节),摄像头按固定速率写入(比如 100MB/s),FPGA 的图像处理模块按同样速率读取。此时:

  • 双口 RAM 允许 “写端口写新数据” 和 “读端口读旧数据” 同时进行,但两者的地址是连续推进的(写地址从 0→1023,读地址也从 0→1023)。
  • 若图像处理模块因某一帧处理复杂(比如突然遇到噪声帧),读地址推进速度滞后于写地址(比如写地址到了 800,读地址才到 500),此时写地址会 “追上” 读地址,新数据会覆盖还未读取的旧数据(500→800 的区域被覆盖),导致数据丢失。

双口 RAM 的 “同时读写” 仅解决了 “物理层的并行操作”,但没解决 “数据流动态边界同步” 的问题 —— 当读写速率出现瞬时不匹配时,覆盖风险无法避免。

2. 无法实现 “块数据” 的 “完整处理” 与 “连续输入” 的无缝衔接

在很多场景中,数据处理不是 “逐字节” 的,而是 “按块” 的(比如图像帧、数据包)。例如:

  • 处理器需要 “读完一整块完整数据(如 1 帧图像)” 后才能开始处理(比如做滤波、识别),而不是边读边处理(实时性不足或算法不允许)。
  • 此时双口 RAM 中,写端口在不断写入新的块数据,读端口必须等当前块写完才能开始读 —— 这期间写操作必须暂停(否则新数据会混入旧块),导致 “输入中断”;或者读操作被迫读 “不完整的块”,导致处理错误。

双口 RAM 的 “实时读写” 适合 “流式逐点处理”,但不适合 “块数据的完整缓冲 + 批量处理” 场景。

乒乓操作如何弥补这些局限?

乒乓操作的核心不是 “并行读写”(双口 RAM 已经能做到),而是通过 “两块独立缓冲区的物理隔离”,实现 “数据块的边界同步” 和 “输入 - 处理的完全解耦”

1. 彻底消除 “覆盖风险”
  • 两块缓冲区 A 和 B,写操作只对 “空闲缓冲区” 写入(比如先写 A),读操作只从 “已写满的缓冲区” 读取(同时读 B)。
  • 当 A 写满时,写操作自动切换到 B,此时即使读 B 的速度滞后,写 A 的新数据也不会影响 B(物理隔离);直到 B 读完,读操作才切换到 A,此时 B 又成为新的空闲缓冲区。
  • 这种 “切换机制” 确保了 “正在读的缓冲区” 永远不会被写入,从物理上杜绝了覆盖。
2. 实现 “块数据” 的无缝处理
  • 对于需要 “完整块处理” 的场景(如 1 帧图像),乒乓操作中:
    • 写模块可以持续向 A 写入,直到 A 满(完整的一帧),然后无缝切换到 B 继续写新帧;
    • 读模块在 A 满的瞬间开始处理 A,处理完后正好 B 也满了,立即切换到 B 处理。
  • 整个过程中,“写新块” 和 “读旧块” 完全并行,没有任何中断,这是双口 RAM 做不到的(双口 RAM 中块的边界需要额外逻辑同步,且无法避免读写地址交叉时的冲突)。

双口 RAM + 乒乓操作:实际应用中的 “组合拳”

在 FPGA 设计中,双口 RAM 和乒乓操作往往是结合使用的:

  • 用双口 RAM 实现 “单块缓冲区内部的并行读写”(提升单块的效率);
  • 再用两块双口 RAM 组成乒乓结构(A 和 B),解决 “块边界同步” 和 “覆盖风险”。

例如:高速 AD 采样场景中,AD 芯片通过双口 RAM A 写入采样数据,同时 FPGA 的 DSP 模块通过双口 RAM A 的读端口 “预读” 数据(边写边读);当 A 满时,切换到双口 RAM B,DSP 模块开始 “完整处理” A 中的数据 —— 这里双口 RAM 解决了单块内的并行,乒乓操作解决了块间的无缝切换。

回到最初的问题:为什么单块大缓冲区(即使是双口 RAM)不行?

  • 双口 RAM 的 “同时读写” 是同一存储区域内的地址并行,但数据流的 “写入边界” 和 “读取边界” 仍在同一块空间中动态移动,存在交叉风险;
  • 乒乓操作的 “两块缓冲区” 是物理隔离的空间,写入和读取分别在两个独立区域进行,通过 “切换” 机制实现边界同步,本质是用 “额外一块缓冲区的空间”,换来了 “无冲突的并行处理时间”。

这就像:双口 RAM 是 “同一房间里的两个门(一个进、一个出)”,虽然进出同时,但人多了还是会在房间里撞车;乒乓操作是 “两个独立房间”,一个房间进人时,另一个房间出人,永远不会撞车 —— 多出来的 “房间空间”,换来了 “无等待的通行时间”。

总结

双口 RAM 确实解决了 “单口 RAM 必须串行读写” 的问题,但它的 “同时读写” 仍局限于同一存储区域内的地址级并行,无法处理 “连续块数据的边界同步” 和 “读写速率波动时的覆盖风险”。
乒乓操作的核心价值不是 “并行读写”(这可以由双口 RAM 实现),而是通过两块物理隔离的缓冲区,用 “空间隔离” 彻底消除了 “读写冲突的可能性”,实现了 “连续数据流的无缝、安全、高效处理”—— 这才是 “空间换时间” 在 FPGA 场景中的深层逻辑。

实现 “块数据” 的无缝处理这里, 你提到写模块可以持续向 A 写入,直到 A 满(完整的一帧), 这里A满刚好对应图像一帧, 是设计的还是? 如果A能存一帧半, 不是有半帧得存B, 是不是就导致图像撕裂?

你提出的 “缓冲区大小与数据块大小的匹配问题” 非常关键,这恰恰是乒乓操作设计的核心细节 ——缓冲区的大小必须与 “最小处理单元”(如 1 帧图像)严格匹配,否则确实会导致数据撕裂(Tearing)。这背后的逻辑是 “乒乓操作的本质是‘块级同步’,而非‘任意大小的缓冲’”。

一、为什么缓冲区大小必须与 “最小处理单元” 严格匹配?

“最小处理单元”(如 1 帧图像、1 个数据包)是指 “必须完整处理后才能输出的最小数据块”。以图像为例,1 帧图像是一个逻辑上不可分割的单元(处理时必须基于完整的帧数据,否则会出现视觉撕裂)。因此:

  • 缓冲区 A 和 B 的大小必须恰好等于 1 帧图像的大小(或整数倍,如 2 帧,但最小单位仍是 1 帧)。
  • 这种 “大小匹配” 是设计时强制保证的,而非偶然。

如果缓冲区 A 能存 “1 帧半”,会出现两个致命问题:

  1. 数据块边界模糊:当 A 写满 1 帧半时,第 1 帧的后半部分和第 2 帧的前半部分混在 A 中,读模块无法区分 “哪部分属于前一帧、哪部分属于后一帧”,导致处理逻辑混乱(比如图像的上半部分是前一帧,下半部分是后一帧,即撕裂)。
  2. 乒乓切换失效:乒乓操作的切换信号(如 “A 满” 或 “B 满”)必须与 “最小处理单元完成” 严格同步。若 A 的大小是 1 帧半,“A 满” 信号触发时,实际只完成了 1.5 帧,但读模块需要的是 “完整的 1 帧”,此时切换会导致读模块读取到不完整的帧数据。

二、如果数据输入速率和处理速率不匹配,如何避免缓冲区 “装不下”?

你可能会问:“如果输入数据突发速率超过处理速率,1 帧大小的缓冲区会不会不够用?” 这时候需要的是多级乒乓操作或 “缓冲区深度扩展”,但核心仍是 “每级缓冲区与最小处理单元匹配”。

例如:

  • 摄像头突发传输 3 帧数据(速率瞬间翻倍),而处理模块每帧需要 1 秒。此时用 “3 级乒乓缓冲区”(A、B、C,每级 1 帧大小):
    • 写模块依次填满 A→B→C(3 帧数据全部缓存);
    • 读模块依次处理 A→B→C,即使处理速度慢,也不会丢失数据(因为 3 级缓冲区足够缓存突发数据)。
  • 这里每级缓冲区仍是 1 帧大小,只是增加了缓冲区数量,本质还是 “块级同步”,避免了数据撕裂。

三、乒乓操作如何通过 “切换机制” 保证数据完整性?

乒乓操作的 “切换信号” 是关键,它必须满足两个条件:

  1. 写满信号:当缓冲区 A 写入 “完整 1 帧” 时,立即触发切换,写模块开始写 B(此时 A 中的数据是完整的 1 帧,可安全读取)。
  2. 读完信号:当读模块读完 A 中 “完整 1 帧” 时,才允许切换到 B 读取(此时 B 已写满完整 1 帧)。

这种 “写满才切换、读完才切换” 的机制,确保了 “读模块永远只处理完整的块数据”,与缓冲区大小严格匹配最小处理单元的设计相辅相成。

反观双口 RAM(即使大小足够),它的读写地址是连续推进的(无 “块边界” 概念),当输入是连续流(如摄像头的像素流)时,读模块无法判断 “当前读到的是哪一帧的内容”,一旦速率波动,就可能读到 “半帧旧数据 + 半帧新数据”,即图像撕裂。

四、总结:乒乓操作的 “块同步” 本质

  • 缓冲区大小与 “最小处理单元”(如 1 帧)的匹配,是乒乓操作的设计前提,目的是让 “缓冲区满” 的信号与 “数据块完整” 严格对应。
  • 若缓冲区大小不匹配(如 1 帧半),会破坏 “块边界”,导致数据撕裂,这与乒乓操作 “无缝处理块数据” 的核心目标矛盾。
  • 双口 RAM 解决的是 “物理层并行读写”,而乒乓操作通过 “大小匹配 + 块级切换” 解决了 “逻辑层数据完整性”,二者结合才能实现高速数据流的可靠处理。

简单说:乒乓操作就像 “两个大小刚好装 1 瓶水的杯子”,装满 1 瓶就换另一个,永远不会出现 “半瓶旧水 + 半瓶新水” 的混合;而大小不匹配的缓冲区,就像 “能装 1.5 瓶水的杯子”,永远分不清哪部分是完整的 “1 瓶”。

手写代码, 不知道正不正确, 跨时钟处理的概念很重要

`timescale 1ns / 1ps
//****************************************VSCODE PLUG-IN**********************************//
//----------------------------------------------------------------------------------------
// IDE :                   VSCODE     
// VSCODE plug-in version: Verilog-Hdl-Format-3.7.20250728
// VSCODE plug-in author : Jiang Percy
//----------------------------------------------------------------------------------------
//****************************************Copyright (c)***********************************//
// Copyright(C)            Please Write Company name
// All rights reserved     
// File name:              
// Last modified Date:     2025/08/05 10:41:39
// Last Version:           V1.0
// Descriptions:           
//----------------------------------------------------------------------------------------
// Created by:             Please Write You Name 
// Created date:           2025/08/05 10:41:39
// mail      :             Please Write mail 
// Version:                V1.0
// TEXT NAME:              pingpong.v
// PATH:                   C:\Users\Administrator\Desktop\工作记录\vivado\pingpong.v
// Descriptions:           
//                         
//----------------------------------------------------------------------------------------
//****************************************************************************************//

module pingpong(
    input                               clk_50M                    ,//写入时钟
    input                               clk_25M                    ,//读出时钟
    input                               rst_n                      ,
    input                               i_data_enable              ,//写入缓冲区使能
    input                [   7: 0]      i_data                     ,//写入缓冲区数据
    input                [  15: 0]      i_rd_data0                 ,//ram0读出数据
    input                [  15: 0]      i_rd_data1                 ,//ram1读出数据

 //  input                               i_full_0                   ,//ram0写满
 //  input                               i_full_1                   ,//ram1写满
 //  input                               i_empty_0                  ,//ram0读空
 //  input                               i_empty_1                  ,//ram1读空

    output               [   7: 0]      o_wr_data0                 ,//写入ram0数据
    output                              o_wr_enable0               ,//ram0写入使能
    output               [   6: 0]      o_wr_ptr0                  ,//写入ram0地址
    output               [   7: 0]      o_wr_data1                 ,//写入ram1数据
    output                              o_wr_enable1               ,//ram1写入使能
    output               [   6: 0]      o_wr_ptr1                  ,//写入ram1地址

    output                              o_rd_enable0               ,//ram0读出使能
    output               [   6: 0]      o_rd_ptr0                  ,//读取ram0地址
    output                              o_rd_enable1               ,//ram1读出使能
    output               [   6: 0]      o_rd_ptr1                  ,//读取ram1地址

    output               [  15: 0]      o_data                      //输出数据(来自ram读出数据)

);
//---------------------------------------------------------------------------------------
// 内部参数变量定义                                                                                  
    localparam                          st_idle                     = 1                    ;
    localparam                          st_wr_ram0                  = 2                    ;  //单写ram0
    localparam                          st_wr_ram1                  = 3                    ;  //同时写ram1读ram0
 
    
                                                              
    reg                [   1: 0]        cur_state                   ;
    reg                [   1: 0]        next_state                  ;
    reg                [   7: 0]        r_data_in_d0                ;//写入缓冲区数据寄存器   
    reg                [  15: 0]        r_rd_data0_d0               ;//ram0读出数据寄存器
    reg                [  15: 0]        r_rd_data1_d0               ;//ram1读出数据寄存器

    reg                [   7: 0]        r_wr_data0                  ;//写入ram0数据寄存器
    reg                                 r_wr_enable0                ;//ram0写入使能寄存器
    reg                [   6: 0]        r_wr_ptr0                   ;//写入ram0地址寄存器
    reg                [   7: 0]        r_wr_data1                  ;//写入ram1数据寄存器
    reg                                 r_wr_enable1                ;//ram1写入使能寄存器
    reg                [   6: 0]        r_wr_ptr1                   ;//写入ram1地址寄存器
    reg                                 r_rd_enable0                ;//置1读取ram0,否则读取ram1
    reg                [   6: 0]        r_rd_ptr0                   ;//读取ram0地址寄存器
    reg                                 r_rd_enable0_d0             ;//跨时钟域处理: 在判断状态转移时由于存在跨时钟域, 必须让来自读时钟域的信号在写时钟域打两拍才能用
    reg                                 r_rd_enable0_d1             ;//跨时钟域处理: 在判断状态转移时由于存在跨时钟域, 必须让来自读时钟域的信号在写时钟域打两拍才能用
    reg                [   6: 0]        r_rd_ptr1                   ;//读取ram1地址寄存器
    reg                                 r_rd_enable0_rdclk_d0       ;//将ram使能信号在读时钟域打两拍,对齐输入的读取数据
    reg                                 r_rd_enable0_rdclk_d1       ;//将ram使能信号在读时钟域打两拍,对齐输入的读取数据
    wire                                w_full_0                    ;//ram0写满标志
    wire                                w_full_1                    ;//ram1写满标志

 //   reg                                 r_empty_0_d1                   ;//ram0读空寄存器  传输到快时钟域进行判断要打拍
//    reg                                 r_empty_1_d1                   ;//ram1读空寄存器  传输到快时钟域进行判断要打拍
//---------------------------------------------------------------------------------------
// 输入信号连接
        always @(posedge clk_50M or negedge rst_n)
            begin
                if(!rst_n)
                    r_data_in_d0 <= 8'b0;
                                                                                                     
                else r_data_in_d0 <= i_data;
            end
        always @(posedge clk_25M or negedge rst_n)
            begin
                if(!rst_n)
                    r_rd_data0_d0 <= 16'b0;
                                                                                                     
                else r_rd_data0_d0 <= i_rd_data0;
            end
        always @(posedge clk_25M or negedge rst_n)
            begin
                if(!rst_n)
                   r_rd_data1_d0 <= 16'b0;
                else r_rd_data1_d0 <= i_rd_data1;
            end
        //full和empty信号分别由写时钟和读时钟给出,但是为了判断写满时是否另一侧读完, 因此我选择在写时钟读取empty信号(似乎有一种下降沿读取数据的说法在里面)    
/*        always @(posedge clk_50M or negedge rst_n)
            begin
                if(!rst_n)
                    r_full_0 <= 1'b0;
                else r_full_0 <= i_full_0;
            end
        always @(posedge clk_50M or negedge rst_n)
            begin
                if(!rst_n)
                    r_full_1 <= 1'b0;
                else r_full_1 <= i_full_1;
            end
        always @(posedge clk_50M or negedge rst_n)
            begin
                if(!rst_n) begin
                    r_empty_0_d0 <= 1'b0;
                    r_empty_0_d1 <= 1'b0;
                end
                else begin
                    r_empty_0_d0 <= i_empty_0;
                    r_empty_0_d1 <= r_empty_0_d0;
                end
            end
        always @(posedge clk_50M or negedge rst_n)
            begin
                if(!rst_n) begin
                    r_empty_1_d0 <= 1'b0;
                    r_empty_1_d1 <= 1'b0;
                end
                else begin
                    r_empty_1_d0 <= i_empty_1;
                    r_empty_1_d1 <= r_empty_1_d0;
                end
            end
*/
//------------------------------------------------------------------------------------------
//内部接线
    assign                              w_full_0                    = (r_wr_ptr0 ==7'd99)  ;
    assign                              w_full_1                    = (r_wr_ptr1 ==7'd99)  ;
    //将ram使能信号传递到写时钟域 
        always @(posedge clk_50M or negedge rst_n)
            begin
                if(!rst_n)begin
                    r_rd_enable0_d0 <= 1'b0;
                    r_rd_enable0_d1 <= 1'b0;
                end
                else begin
                        r_rd_enable0_d0 <= r_rd_enable0;
                        r_rd_enable0_d1 <= r_rd_enable0_d0;
                end
            end
    //将ram使能信号在读时钟域打两拍对齐输入的读取数据
        always @(posedge clk_25M or negedge rst_n)
            begin
                if(!rst_n) begin                                              
                    r_rd_enable0_rdclk_d0 <= 1'b0;
                    r_rd_enable0_rdclk_d1 <= 1'b0;
                end
                else begin                                             
                    r_rd_enable0_rdclk_d0 <= r_rd_enable0;
                    r_rd_enable0_rdclk_d1 <= r_rd_enable0_rdclk_d0;
                end
                                             
            end
//------------------------------------------------------------------------------------------
// 输出信号连接 
    assign                              o_wr_data0                  = r_wr_data0           ;
    assign                              o_wr_enable0                = r_wr_enable0         ;
    assign                              o_wr_ptr0                   = r_wr_ptr0            ;
    assign                              o_wr_data1                  = r_wr_data1           ;
    assign                              o_wr_enable1                = r_wr_enable1         ;
    assign                              o_wr_ptr1                   = r_wr_ptr1            ;
    assign                              o_rd_enable0                = r_rd_enable0         ;
    assign                              o_rd_ptr0                   = r_rd_ptr0            ;
    assign                              o_rd_enable1                = ~r_rd_enable0        ;
    assign                              o_rd_ptr1                   = r_rd_ptr1            ;
    //读取数据 应该要考虑r_rd_anable0 输出到ram 在反馈回数据的时间 假如ram读取ena和ptr然后输出data需要一拍, data进来又需要一拍,那r_rd_data0_d0实际上比ena慢两拍,那么对ena在读时钟域打两拍是不是才能对齐data_d0?
    assign                              o_data                      = (r_rd_enable0_rdclk_d1)? r_rd_data0_d0 : r_rd_data1_d0;


    //(三段式状态机)同步时序描述状态转移
        always @(posedge clk_50M or negedge rst_n)
            begin
                if(rst_n == 1'b0)
                    cur_state <= st_idle;
                else
                    cur_state <= next_state;
            end
    //组合逻辑判断状态转移条件 根据不同情况应该采取不同判断条件, 下面是写快读慢
        always @( * ) begin
            case(cur_state)
                st_idle:            next_state = (i_data_enable)? st_wr_ram0 : st_idle;
                st_wr_ram0:         next_state = ( r_rd_enable0_d1 && w_full_0)? st_wr_ram1 : st_wr_ram0;
                st_wr_ram1:         next_state = (~r_rd_enable0_d1 && w_full_1)? st_wr_ram0 : st_wr_ram1;
                default:            next_state= st_idle;
            endcase
        end
    //时序电路描述状态输出
    always @(posedge clk_50M or negedge rst_n)
        begin
            if(rst_n == 1'b0)
                begin
                    r_wr_data0 <= 8'b0;
                    r_wr_enable0 <= 1'b0;
                    r_wr_ptr0 <= 7'b0;
                    r_wr_data1 <= 8'b0;
                    r_wr_enable1 <= 1'b0;
                    r_wr_ptr1 <= 7'b0;
    

                end
            else begin
                case (next_state)
                    st_idle:begin
                        r_wr_data0 <= 8'b0;
                        r_wr_enable0 <= 1'b0;
                        r_wr_ptr0 <= 7'b0;
                        r_wr_data1 <= 8'b0;
                        r_wr_enable1 <= 1'b0;
                        r_wr_ptr1 <= 7'b0;


                    end
                    st_wr_ram0:begin
                        r_wr_data0 <= r_data_in_d0;
                        r_wr_enable0 <= 1'b1;
                        r_wr_ptr0 <= (r_wr_ptr0 == 7'd99)? 7'b0 : r_wr_ptr0 + 7'b1;
                        r_wr_data1 <= 8'b0;
                        r_wr_enable1 <= 1'b0;
                        r_wr_ptr1 <= 7'b0;


                    end
                    st_wr_ram1:begin
                        r_wr_data0 <= 8'b0;
                        r_wr_enable0 <= 1'b0;
                        r_wr_ptr0 <= 7'b0;
                        r_wr_data1 <= r_data_in_d0;
                        r_wr_enable1 <= 1'b1;
                        r_wr_ptr1 <= (r_wr_ptr1 == 7'd99)? 7'b0 : r_wr_ptr1 + 7'b1;


                    end

                    default:begin
                        r_wr_data0 <= 8'b0;
                        r_wr_enable0 <= 1'b0;
                        r_wr_ptr0 <= 7'b0;
                        r_wr_data1 <= 8'b0;
                        r_wr_enable1 <= 1'b0;
                        r_wr_ptr1 <= 7'b0;

                    end
                endcase
            end
        end
// ----------------------------------------------------------------------------
// ram 读取控制, 来回读取两个ram, 不需要管写, 反正读的比写的慢
        always @(posedge clk_25M or negedge rst_n)
            begin
                if(!rst_n) begin
                    r_rd_ptr0 <= 7'b0;
                    r_rd_ptr1 <= 7'b0;
                    r_rd_enable0 <= 1'b1;
                end
                else begin
                    r_rd_ptr0 <= ( r_rd_enable0)? (r_rd_ptr0 == 7'd99)? 7'b0 : r_rd_ptr0 + 1
                    :7'b0;
                    r_rd_ptr1 <= (~r_rd_enable0)? (r_rd_ptr1 == 7'd99)? 7'b0 : r_rd_ptr1 + 1
                    :7'b0;
                    r_rd_enable0 <= (r_rd_ptr0 == 7'd99 && r_rd_enable0 || r_rd_ptr1 == 7'd99 && ~r_rd_enable0)? ~r_rd_enable0 : r_rd_enable0;
                end
            end
    

endmodule


网站公告

今日签到

点亮在社区的每一天
去签到