资讯详情

资讯详情

基于双口RAM的FPGA计数器设计:原理、实现与仿真验证

简介这是一份FPGA课程设计报告围绕“采用RAM实现计数器”展开面向学习Verilog HDL、Quartus II与Modelsim的数字电路设计者。任务要求用10×8双口RAM完成10个8位计数器初值1~10主时钟50MHz经分频得到1MHz与1Hz计数时钟并用按键选择、数码管显示当前计数结果。报告按自顶向下方法设计计数器模块、分频模块、RAM模块和显示模块给出技术规范、引脚定义、总体设计框图、完整Verilog源代码、前仿真与后仿真截图以及综合布局布线报告便于读者对照复现并排查时序问题。压缩包共1个doc文件容量约165KB内容紧凑、章节完整可作为FPGA课程设计报告模板或数字系统实验参考。目前已有1482人学习下载适合需要完成课程设计、验证RAM计数器原理或学习Modelsim前仿真流程的读者。1. 为什么要用一块 10×8 双口 RAM 去做 10 个计数器拿到“采用 RAM 实现计数器及 FPGA 功能验证”这个题目时第一反应可能是计数器用寄存器加加法逻辑就够了为什么要绕一圈用 RAM这个题目真正考察的点不在于“怎么数数”而在于怎么用存储器的地址空间去映射多个计数状态并用双口 RAM 的读写独立性去解决“一边计数、一边读出显示”的冲突。任务要求用 10×8 的双口 RAM 完成 10 个 8 位计数器每个计数器初值是 1~10系统时钟 1MHz计数频率 1Hz按键选择哪个计数器的值送到数码管显示。这意味着 RAM 里的 10 个单元不只是存放初值它们本身就是 10 个计数器的“状态寄存器”。计数、暂停存储、按键切换读出本质上是三组信号对同一块存储区的分时访问。理清这一层整个课程设计的结构就出来了分频、RAM 控制、计数控制、显示译码四个模块按数据流方向串起来。适合正在做 FPGA 课设或毕设的同学照着搭也适合想搞清楚“RAM 当寄存器组用”这种设计思路的工程师参考。2. 总体设计自顶向下拆解为四个模块2.1 整体数据流与时序关系在动手写代码之前先把系统的数据流画出来50MHz 板载时钟进入分频模块分频模块输出两路时钟一路 1MHz 给 RAM 模块做读写时钟另一路 1Hz 给计数器模块做计数时钟。RAM 模块根据地址和读写使能信号把 10 个 8 位初值写好计数器模块从 RAM 读回当前值每来一个 1Hz 脉冲就加一暂停时把当前值写回 RAM 对应单元同时按键切换地址选中某个计数器的当前值送到七段译码显示模块。这个数据流正好对应题目给的工作流程切换端口、RAM 预置初值、读出数据、开始计数、译码显示。这里有一个关键设计决策计数器模块的读写操作和显示模块的读操作共用同一块双口 RAM因为一个口负责计数读写另一个口负责显示读出两者互不干扰。如果是单口 RAM计数更新和显示刷新就要做分时仲裁代码复杂度会明显上升。这也是题目特意指定“双口 RAM”的原因。引脚定义中 Addr_in 是 4 位输入而不是 10 位就是因为 10 个存储单元只需要 4 位地址线寻址这个细节在写端口声明时容易忽略。2.2 分频模块的实现与参数分析分频模块从 50MHz 分频产生 1MHz 和 1Hz。1MHz 是 RAM 的读写时钟1Hz 是计数器计数时钟。50MHz 到 1MHz 是 50 分频50MHz 到 1Hz 是 2500 万分频。分频思路是用计数器计数到目标值的一半翻转电平这样占空比接近 50%。1MHz 对应计数到 25 翻转1Hz 对应计数到 2500 万翻转。源代码里用了两个 always 块分别处理用 negedge reset 做异步复位。需要注意 30 位计数器才能装下 2500 万这个数1Hz 计数器的位宽要看清楚用 24 位或 25 位就会溢出翻车。这段代码可以直接复用cnt 是 30 位cnt1 是 5 位复位时输出清零计数到目标值取反并清零计数器否则累加。2.3 RAM 模块初始化与读写控制的关键RAM 模块是本次设计最核心的部分。复位时将 10 个存储单元初始化为 1~10这样上电即预置初值不需要额外的写入流程。正常工作状态下wr1 且 rd0 时写入地址 addr_in 对应的单元wr0 且 rd1 时读出地址 addr_in 的数据。注意底层声明为 reg [7:0] mem[10:1]地址从 1 开始编号而外部地址线是 4 位 0~15实际只用 1~10。这里读出的 dout 在 always 块中只在读条件下更新所以输出是寄存打拍后的信号不是组合逻辑直通后续接数码管显示会更稳定。2.4 计数器模块从初值开始的自增与暂停计数器模块功能是从 RAM 读回初值在 1Hz 时钟驱动下逐秒加一支持开始和暂停控制。count_reset 复位时把 din 置为 dout重新载入初值之后每个 1Hz 上升沿若 start1 且 stop0则 din 自加一加到 255 回绕到 0。这里有个容易被忽略的细节din 既作为计数器当前值寄存器又作为写回 RAM 的数据端口所以计数过程中 RAM 模块会持续把 din 写入当前地址单元实现了计数值的实时保存。暂停时 din 保持不变按题目要求暂停键暂停计数并同时存回 RAM 存储单元这里通过 RAM 模块的写使能组合完成。2.5 显示模块与顶层例化把四个模块串起来显示模块将 8 位计数值拆成百位、十位、个位分别译码输出。代码中用 dout/100、dout/10、dout%10 的方式分离三位数字注意十位的提取逻辑dout_s dout/10但对 0~255 范围的数值dout/10 得到 0~25不只包含十位还包含百位所以实际显示时十位会带上百位信息。更严谨的写法是 (dout%100)/10这个细节会影响数码管显示正确性在避坑部分会展开说明。顶层模块把四个子模块例化串联起来FPQ 输出两路时钟给 RAM 和计数器Ram 与 JSQ 通过 din 和 dout 交换数据最终 SMG 模块输出三位数码管的七段码。注意顶层例化中 SMG 接收的 dout_2 并没有连接这是原设计遗留问题实际验证时需要把 dout_1 引到显示模块。3. 关键模块设计与可复用实现3.1 分频代码的工程细节为什么两个 always 块更好分频模块完整代码如下module FPQ(clk, clk_1hz, clk_1mhz, reset); input clk; input reset; output clk_1hz, clk_1mhz; reg clk_1hz, clk_1mhz; reg [29:0] cnt; reg [4:0] cnt1; always (posedge clk, negedge reset) begin if(!reset) begin clk_1mhz 0; cnt1 5d0; end else if(cnt1 5d25) begin cnt1 5b0; clk_1mhz ~clk_1mhz; end else cnt1 cnt1 1b1; end always (posedge clk, negedge reset) begin if(!reset) begin clk_1hz 0; cnt 30d0; end else if(cnt 30d2500_0000) begin cnt 30b0; clk_1hz ~clk_1hz; end else cnt cnt 1b1; end endmodule这里用两个独立 always 块分别生成 1MHz 和 1Hz而不是在同一个 always 块里同时维护两个计数器原因在于两个分频倍率差 50 万倍合并后代码可读性很差而且 1Hz 计数的 if 分支每 50 个 1MHz 周期才执行一次综合工具很难优化。注意两处复位都是异步复位、低电平有效与引脚定义中 Reset 低电平有效一致。综合时 Quartus 会推断出两个寄存器计数器资源占用约 36 个触发器在 Cyclone II 上几乎没有压力。验证时可以通过 Modelsim 观察 clk_1mhz 每 25 个 clk 翻转一次clk_1hz 每 2500 万个 clk 翻转一次确认分频系数正确后再进行下一步。3.2 RAM 模块与计数器模块的配合时序RAM 和计数器模块配合的关键是理解读写控制的交叉。完整 RAM 代码如下module ram(wr, rd, reset, dout, clk_1mhz, din, addr_in); input wr, rd; input reset, clk_1mhz; input [3:0] addr_in; input [7:0] din; output [7:0] dout; reg [7:0] dout; reg [7:0] mem[10:1]; always (posedge clk_1mhz, negedge reset) begin if(!reset) begin mem[1]8d1; mem[2]8d2; mem[3]8d3; mem[4]8d4; mem[5]8d5; mem[6]8d6; mem[7]8d7; mem[8]8d8; mem[9]8d9; mem[10]8d10; end else if(wr 1 rd 0) mem[addr_in] din; else if(rd 1 wr 0) dout mem[addr_in]; end endmodule复位时 10 个单元逐一初始化这是“预置初值”的直接实现。写入和读出条件完全互斥不会出现同一拍既写又读的冲突。注意 dout 的更新在时钟沿触发因此 dout 会比地址变化晚一个 1MHz 周期在显示模块中这是可以接受的。计数器模块如下module JSQ(dout, start, stop, din, clk_1hz, count_reset); output [7:0] din; input [7:0] dout; input start, stop, clk_1hz, count_reset; reg [7:0] din; always (posedge clk_1hz) begin if(!count_reset) din dout; else if(din 8d255) din 8d0; else if((start 1) (stop 0)) din din 1; else din din; end endmodule这段代码有几个值得注意的点。din 是 reg 类型但声明为 output 端口在顶层需要 wire 连接这正是“用 RAM 存储的数据作为计数器初始值”的体现。count_reset 拉低时把 dout 重新装载到 din实现了复位到初值而不是复位到零符合题目“初值 1~10”的要求。计数到 255 后回绕到 0这是 8 位计数器的自然溢出行为。这里有隐含的逻辑关系din 不仅是计数值还回送到 RAM 模块的 din 端口所以计数过程中每个 1MHz 周期只要 wr1 且 rd0RAM 就会把当前计数值存回同一地址实现了“暂停并存入 RAM”和“计数过程实时保存”两种状态。实际使用中我习惯把 din 输出端口的赋值方式改成非阻塞赋值避免与外部组合逻辑产生竞争这是后仿中比较容易踩的坑。3.3 数码管显示模块位宽拆分与译码显示模块完整代码如下module SMG(dout, hex_b, hex_s, hex_g); input [7:0] dout; output [7:0] hex_b; output [7:0] hex_s; output [7:0] hex_g; reg [7:0] hex_b; reg [7:0] hex_s; reg [7:0] hex_g; reg [3:0] dout_b; reg [3:0] dout_s; reg [3:0] dout_g; always (dout) begin dout_b dout/100; dout_s dout/10; dout_g dout%10; case(dout_b) 4d0: hex_b 8hC0; 4d1: hex_b 8hF9; 4d2: hex_b 8hA4; 4d3: hex_b 8hB0; 4d4: hex_b 8h99; 4d5: hex_b 8h92; 4d6: hex_b 8h82; 4d7: hex_b 8hF8; 4d8: hex_b 8h80; 4d9: hex_b 8h90; default: hex_b 8hC0; endcase // hex_s 和 hex_g 的 case 分支相同只是赋值对象不同 end endmodule显示模块接收 8 位计数值拆成百位、十位、个位分别送七段译码。共阳数码管用 C0/F9/A4 这类编码共阴则取反。这里的 dout_s dout/10 在 0~255 范围有歧义因为 dout/10 最大是 25包含百位信息十位正确写法是 (dout%100)/10。不过在实际显示中由于百位单独显示了十位显示 0~25 的数值时0~9 正常显示10~25 会显示 A~P 之类的非法字符所以这个 bug 在计数超过一定范围时才暴露建议写成 (dout%100)/10。引脚约束中 hex_b、hex_s、hex_g 分别对应三组数码管的七个段位每组 7 位加一个公共端共 21 个引脚开发板资源足够。3.4 仿真激励的编写思路覆盖复位、写读、计数、暂停仿真激励模块的时序安排如下初始复位拉低随后拉高先执行写操作预置数据再切换读操作读出数据然后启动计数运行一段后暂停再恢复计数最后回写。仿真时间以 20ns 为一个周期对应 50MHz 时钟。关键控制信号跳变顺序为先给 addr_in 赋值等一段仿真时间后拉高 rd 执行读操作再拉高 count_reset 启动计数之后通过 start 和 stop 的高低组合模拟开始和暂停最后切换 wr 和 rd 模拟回写。这套激励包含了复位、预置、计数、暂停、恢复、回写完整的业务流程。Modelsim 中跑前仿真时重点观察三个信号clk_1hz 翻转一次din 加一暂停时 din 保持不变wr1 时 RAM 内部对应 mem 单元更新。前仿真用功能逻辑没有延时后仿真加入了门延迟信息如果出现竞争风险后仿真会暴露出来。4. 常见问题与避坑指南从课程设计到下载验证4.1 Modelsim 前仿真正常但后仿真无输出现象前仿真时波形数据完整数码管输出段码有变化但加入布局布线延迟后做后仿真输出保持高阻或固定值不变。原因排查后仿真加入了 I/O 时序信息如果顶层端口没有做引脚约束或者顶层模块与综合网表端口不匹配仿真工具无法给输出端口驱动信号。解决步骤先在 Quartus II 中完成引脚分配再导出带.sdo 时序反标文件到 Modelsim同时在 Testbench 中给每个输入端口设置上电初始值。特别要注意复位信号前后仿真对复位释放时刻的敏感度不同简易做法是拉长复位时间到几个毫秒。按此顺序检查后后仿真通常能稳定出波形。4.2 RAM 写数据和读数据冲突导致计数不更新现象start 信号拉高后 din 一直保持初始值不递增或者短暂递增后被改回初值。原因分析RAM 模块的读写使能同时有效wr1 和 rd0 时执行写入但此时如果把 dout 当作计数初值连接到 din而 wr 一直为高RAM 会反复把旧值写回存储单元覆盖计数器的自增结果。解决做法检查读写控制信号的时序确保计数器在自增期间 RAM 处于读状态把新值写入 RAM 只在暂停或回写阶段发生。更可靠的设计是增加一个“计数使能”信号只有该信号有效时才允许 RAM 更新对应单元。4.3 分频计数器位宽不足导致 1Hz 输出异常现象1MHz 时钟输出正常1Hz 时钟没有翻转或翻转频率明显高于预期。原因分析1Hz 分频在 50MHz 下需要计数到 2500 万二进制位宽至少 25 位如果使用了 24 位计数器计数值 16777215 就溢出回绕实际翻转频率远大于 1Hz。解决做法把 1Hz 分频计数器声明为 30 位。另外需要注意 2500_0000 这个数字常量默认是 32 位十进制数直接与 30 位寄存器比较综合时结果可能被截断。更稳妥的写法是使用带位宽的常量cnt 30d25000000。4.4 数码管显示乱码或数字闪烁现象计数显示出现段码里面多位同时点亮或数字在相邻值之间来回跳。原因分析dout_s 的拆分逻辑用 dout/10 而不是 (dout%100)/10导致超过 9 的十位值异常同时 dout 在 RAM 读使能期间每个 1MHz 周期更新一次而显示模块是组合逻辑输入毛刺直接反映到段码上。解决做法修正拆分逻辑在显示模块前加一级寄存打拍把 dout 用 always (posedge clk_1mhz) 寄存一次再送译码消除亚稳态和毛刺数码管公共端如果有扫描控制还需要保证切换频率大于 60Hz 防止闪烁。5. 功能验证与答辩从仿真波形到板上现象闭环5.1 验证项目的三条主线功能验证方案围绕三个层次展开。第一是分频模块验证给出 50MHz 时钟激励观察 clk_1mhz 周期是否为 1us、clk_1hz 周期是否为 1s在 Modelsim 中通过光标测量相邻上升沿间隔确认。第二是 RAM 读写验证wr1 且 rd0 时写入数据随后切换为 rd1 且 wr0 读出确认读出值与写入值一致。第三是系统集成验证复位后检查 RAM 十个单元初值为 1~10启动计数后每个 1Hz 周期计数值加一暂停后数值保持再次启动继续累加。仿真激励中每隔一定仿真时间切换 wr 和 rd 信号就是为了覆盖这三种状态。激励文件中先复位释放再拉高 rd 读出初始值然后启动计数之后暂停并回写这段时序已经覆盖完整业务流程。5.2 前仿真与后仿真的关注点差异前仿真验证的是 RTL 逻辑正确性后仿真验证的是布局布线后的时序收敛性。前仿真中 reset 拉低再拉高观察 RAM 各单元初始值是否正确然后拉高 start观察 1Hz 时钟沿到来时 din 是否按 1 递增暂停时 din 保持不变切换地址后数码管段码是否对应数值变化。后仿真除了逻辑功能重点检查 reset 释放后是否有毛刺、时钟分频输出是否有时序违规、从 clk 到 RAM 写使能之间是否满足保持时间。一个通用的检查办法是在 Modelsim 中把 rst 拉长的几个时钟观察所有寄存器的输出是否同时释放如果出现部分寄存器提前释放的现象说明复位树设计有问题需要检查复位信号的扇出或增加复位同步器。后仿真中还经常会遇到 RAM 初始化不生效的问题这是因为综合后 RAM 被映射为 M4K 块内部初始值需要通过 .mif 文件或初始化语句导入查找原设计代码中 mem[1]8d1 这种方式只是 RTL 行为仿真有效真正下载到板上后要额外处理初始化逻辑。5.3 答辩时如何把设计过程讲清楚答辩环节最容易暴露的问题是“只会说功能说不清为什么这样设计”。建议按这个顺序准备讲稿先讲题目要求的技术指标包括 10×8 双口 RAM、10 个 8 位计数器、初值 1~10、1MHz 分频到 1Hz、按键选通、数码管显示然后讲总体方案画出四模块框图说清数据流方向接着逐个模块讲设计要点分频模块讲分频系数怎么算RAM 模块讲双口读写为什么能避免计数和显示冲突计数模块讲初值装载和自增逻辑最后讲验证过程给三张关键波形截图分别是分频后的 clk_1hz、RAM 读写时序、计数递增波形。如果被问到“为什么双口 RAM 能实现 10 个计数器”核心答案可以这样组织每个存储单元的地址就是计数器编号存储内容就是计数值双口设计让计数更新和显示读取互不干扰本质上是寄存器组的扩展实现。另外准备一二个踩坑经历比如分频计数器位宽不足、显示模块十位拆分逻辑错误能把设计深度拉起来。课程设计报告要交的内容包括技术规范、设计方案、功能验证方案、源代码、引脚分布、前仿真和综合报告把这些按顺序组织好答辩就顺了。原报告里提到没有下载到 FPGA 开发板只能做到后仿真这一步如果条件允许建议至少完成一次板级验证哪怕只是用 SignalTap 抓内部信号也能让整个方案的完整度上一个台阶。做 FPGA 设计最怕的就是“仿真过了但板上不行”从那以后我每次做 RAM 相关设计都会强制走一遍“写时序分析、读时序分析、读写切换时序分析”这三步确认再上板下载验证。希望这篇方案能帮你在课程设计中少走一些弯路。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →