资讯动态

Basys3 FPGA实战:Verilog多模式LED与数码管计时器

发布时间:2026/9/17 8:01:28 来源:尧图企业网站定制
简介这份资料是西安电子科技大学微电子学院李振荣老师FPGA可编程逻辑器件课程的上机大作业报告面向选修FPGA相关课程、需要完成实验报告或备战同类大作业的本科生与自学者。内容围绕Xilinx Basys3开发板展开完整记录多模式LED发光控制器、多模式运动计时器以及液晶屏显示字符三个实验题目涵盖设计思路、Verilog源代码、ModelSim仿真截图、Vivado综合网表、布局布线结果与器件资源利用率分析并附有实验目的、器材说明与心得体会。压缩包内为1个PDF文件约1018KB将题目要求、原理分析、代码与验证过程整合成一份可直接参考的完整报告。目前已有154人学习浏览适合刚接触FPGA开发流程、需要熟悉Vivado与ModelSim工具链、理清跑马灯模式控制、倒计时配置和约束文件编写思路的读者借鉴便于对照梳理从代码设计到比特流下载的完整环节。1. 从一份西电微电子上机大作业看 Basys3 上的三个 Verilog 实战题Basys3 这块板子很多人当入门练手板但真正把 16 位 LED、四位七段数码管、滑动开关和按键全部串起来做完整逻辑设计坑比想象中多。这份西安电子科技大学微电子学院李振荣老师布置的 FPGA 上机大作业给了三道题多模式 LED 发光控制器、多模式运动计时器、液晶屏显示字符选作工具链是 Vivado 2014.4 加 ModelSim SE-64 10.4芯片是 XC7A35T-1CPG236C。三道题表面看是跑马灯和计时器这种玩具题实际上覆盖了时钟分频、异步复位、状态编码、多位数码管动态扫描、管脚约束这几块 FPGA 入门必须过的坎。如果你正在做课程设计、准备 fpga 项目或者想搞明白 fpga 实现数码管动态显示到底怎么落地这三道题的思路可以直接抄。下面按设计原理、代码实现、约束综合、验证排错的顺序把每一题的完整链路拆开讲。2. 多模式 LED 控制器的速度分级与模式译码设计2.1 用 100MHz 时钟做可视速度分级的本质Basys3 板载 100MHz 晶振也就是说 clk 每 10ns 翻转一次。如果直接拿这个时钟去驱动 LED 移位人眼只能看到一片模糊根本看不出跑马灯。所以核心问题就变成了怎么把 100MHz 降到一个肉眼看得到变化的频率而且还要分出四档速度。原设计用了一个很典型的级联计数器思路count1 从 0 数到 24 位全 1也就是 2^24 - 1跨越一次就驱动 count2 加一count2 数到 2 时驱动 count3 加一依此类推直到 count5。这样就形成了一串分频链每往下一级速度就慢一截。24 位全 1 大约对应 16777216 个时钟周期除以 100MHz 就是约 0.167 秒所以 count2 每约 0.17 秒加一依次往上时间成倍拉长。这种用高位计数器慢速进位的做法在 fpga 入门里很常见好处是不用写 PLL 或分频器 IP纯逻辑就能搞定缺点是它本质上还是组合逻辑比较加寄存器递增占的 LUT 和 FF 不少。实际的计数阈值可以按需求改比如想快一点就把 24 位全 1 换成 20 位或更小。2.2 模式选择与速度选择的双 case 译码模式选择用两位滑动开关 m1、m2 拼成 2b00 到 2b11分别对应左循环、右循环、交叉闪烁、全亮全灭。速度选择用 v1、v2 拼出四档。原代码用了两个 case 语句一个选速度、一个选模式// 速度选择把对应档位的计数器赋给统一变量 count case({v1,v2}) 2b00: count count2; // 最快 2b01: count count3; 2b10: count count4; 2b11: count count5; // 最慢 endcase // 模式选择每到阈值就改变 LED 状态 case({m1,m2}) 2b00: if(count 24d2) led (led 1); // 左循环 2b01: if(count 24d2) led (led 1); // 右循环 2b10: if(count 24d2) begin // 交叉闪烁 led 16b1010_1010_1010_1010; #5 led 16b0101_0101_0101_0101; end 2b11: if(count 24d2) begin // 全亮全灭 led 16b1111_1111_1111_1111; #5 led 16b0000_0000_0000_0000; end endcase这里有几个点值得说清楚。第一led 1是左移盲跑马灯移出去的位会丢掉如果是从 0 移 1最终会全部变 0。原代码初始化是复位时全亮也就是 led 为 16 个 1那左移时最高位丢失、最低位补 0随着时间推移低位全变成 0看起来就不像连续跑马灯了。更稳的写法是用循环移位{led[14:0], led[15]}让高位补到低位形成真正的环形走马灯。第二阈值用的是24d2也就是统一变量 count 每次累加到 2 就动作一次这是为了让灯的变化和所选档位的慢速计数器同步。因为 count 是直接跟 count2/count3 等绑定的所以阈值设成 2 其实是在等每一档内部再分频两级。提示case 里用阻塞赋值而不是非阻塞在时序 always 块里容易引起仿真和综合不一致实际项目里建议统一改成。2.3 异步复位的双 always 块写法及隐患原代码用了两个 always 块一个posedge clk or posedge rst高电平复位一个posedge clk or negedge rst。两个块的复位极性不一致这是很典型的笔误。Basys3 按键按下输出高电平所以复位统一用posedge rst才对。如果哪一路写成negedge rst按下复位时那一路不会复位count1 到 count5 停不下来速度链就乱了。正确的复位结构应该是always (posedge clk or posedge rst) begin if (rst) {count1, count2, count3, count4, count5} 0; else begin count1 count1 1b1; if (count1 24hFFFFFF) begin count2 count2 1b1; end if (count2 24d2) begin count3 count3 1b1; end if (count3 24d2) begin count4 count4 1b1; end if (count4 24d2) begin count5 count5 1b1; end end end把复位条件统一成高电平有效并且把所有状态寄存器放进同一个复位分支才是干净的写法。3. 多模式运动计时器的位拼接与数码管动态扫描3.1 8 位 q 变量如何压缩成 mm:ss计时器最长的值是 59 分 59 秒也就是 5959。如果用二进制存秒数需要 13 位最大 3599 秒但要求数码管显示成 4 个十进制数字。原设计用了一个 8 位的 q 变量来同时存分和秒格式是 q[7:6] 存分钟十位、q[5:4] 存分钟个位、q[3:2] 存秒十位、q[1:0] 存秒个位每两位一个 BCD 数字。这样 4 个 2 位字段刚好拼成一个 8 位寄存器直接连到数码管译码逻辑上就省事。秒计数的进位逻辑是这样的// 每 1 秒0.167s × 6 大约 1s具体看 count 阈值秒加一 q[1:0] q[1:0] 1b1; if (q[1:0] 2b11) begin q[1:0] 2b00; q[3:2] q[3:2] 1b1; if (q[3:0] 4b1111) begin q[3:0] 4b0000; q[5:4] q[5:4] 1b1; if (q[5:4] 2b11) begin q[5:4] 2b00; q[7:6] q[7:6] 1b1; end end end这里 q 的每个 2 位字段到 2b11 就清零等价于十进制 9 加 1 归零天然就是个十进制计数器。原代码里有个小问题q[5:4] 到 2b11也就是 3就清零了但分钟的个位应该是 0 到 9 十个数所以 2 位根本不够表示 0-9。原设计这里其实是用了十进制数的二进制表示个位最大到 9。如果 q[5:4] 只有 2 位最多到 3显然不对。合理的做法是把分钟个位和秒个位各用 4 位 BCD 来存或者干脆用二进制存总秒数再在显示时做 BCD 转换。这是这份作业里一个值得推敲的点。3.2 秒脉冲生成与倒计时初值装载倒计时模式用 m1b0初值从 15 位输入 t 装载。原代码里q[7:0] t[7:0]把 t 的低 8 位直接作为倒计时初值但要求说 15 位滑动开关配置倒计时值15 位足够表示 0 到 32767 秒显然比 8 位 q 能表示的范围大得多。这意味着要么把 t 先做进制转换再塞进 q要么 q 只用 8 位就够了比如只配到 59:59 以内。// 倒计时模式装载初值然后每秒减一 1b1: begin if (load) q t[7:0]; // 装载初值 if (count 24hFFFFFF) begin // 每秒触发 q[1:0] q[1:0] - 1b1; if (q[1:0] 2b00) begin q[1:0] 2b11; q[3:2] q[3:2] - 1b1; if (q[3:0] 4b0000) begin q[3:0] 4b1111; q[5:4] q[5:4] - 1b1; if (q[5:4] 2b00) begin q[5:4] 2b11; q[7:6] q[7:6] - 1b1; end end end end end倒计时的借位逻辑和正向进位是对称的每个 2 位字段到 0 就借位变成 11也就是十进制 9 或相应最大值同时向高位掏一。下面是 15 位倒计时初值 t 在装载时的分段方式t 位域含义取值范围说明t[11:8]分钟高位0-5最大 59 分的十位t[7:4]分钟低位0-9分钟个位t[3:0]秒低位0-9秒个位剩余位秒高位-用于更高秒位或扩展实际装载时需要把 t 做拆分后再送到 q 的对应字段原代码直接q t[7:0]是把 t 当二进制数而不是 BCD 用显示出来会是乱码。这是学生作业里最容易踩的坑显示格式错乱十有八九是这里没做进制转换。注意数码管的十进制显示和二进制存储是两码事。如果一定要 mm:ss 显示正确要么全程用 BCD 存储要么在扫描前做二进制转 BCD比如用双 dabble 算法。3.3 启动暂停与异步复位的优先级启动/暂停功能原设计用 p 输入但代码里并没有体现 p 对计数的使能作用count 一直在加。正确的做法是把计数使能与暂停绑定always (posedge clk or posedge rst) begin if (rst) count 24d0; else if (p) // p1 表示暂停 count count; // 保持 else count count 1b1; end复位优先级最高其次才是暂停。这样按下暂停时计数器冻结计时自然停住再次切换 p 后从冻结值继续。整个计时器的时序路径是100MHz 时钟 → 分频计数 → 秒脉冲 → BCD 加减 → 数码管扫描每一级都必须复位到确定值否则上电后数码管可能显示随机数。4. Basys3 管脚约束文件的三处易错点4.1 XDC 里 LED 与开关的引脚映射题目一约束文件里把 clk 绑到 W5rst 绑到 U18m1/m2/v1/v2 分别绑到 V16、V17、W16、W17LED0 到 LED15 绑到 U16、E19、U19、V19、W18、U15、U14、V14、V13、V3、W3、U3、P3、N3、P1、L1。这些都是 Basys3 的官方引脚定义直接抄板子手册就能对上。关键是要注意[get_ports {led[0]}]这种写法当端口是向量时花括号不能省否则 Vivado 会报找不到端口。## Clock signal set_property PACKAGE_PIN W5 [get_ports clk] set_property IOSTANDARD LVCMOS33 [get_ports clk] ## LEDs set_property PACKAGE_PIN U16 [get_ports {led[0]}] set_property IOSTANDARD LVCMOS33 [get_ports {led[0]}] set_property PACKAGE_PIN E19 [get_ports {led[1]}] set_property IOSTANDARD LVCMOS33 [get_ports {led[1]}] # ... 其余 LED 同理 ## Buttons set_property PACKAGE_PIN U18 [get_ports rst] set_property IOSTANDARD LVCMOS33 [get_ports rst]每一行先设 PACKAGE_PIN 再设 IOSTANDARD顺序不能颠倒。IOSTANDARD 必须是 LVCMOS33因为 Basys3 的 I/O bank 是 3.3V。原文件里出现了get_portsm1这种少了空格、把get_ports和端口名粘在一起的笔误Vivado 会直接报语法错误这是综合前就必须修掉的。4.2 约束文件常见的三类报错与定位第一种是端口名和顶层模块对不上比如顶层写的是led约束里写成leds会报 no objects matched。定位方法是打开综合后的 I/O Ports 报告逐一核对端口名。第二种是位宽不匹配比如约束里写led[16]但实际只有 0-15直接报错。第三种是重复约束同一引脚被绑了两次Vivado 会告警用最后一条。另外时钟引脚没有约束会报 no clock defined时序分析无法进行。至少要给 clk 一个create_clock约束create_clock -period 10.000 -name sys_clk [get_ports clk]100MHz 对应周期 10ns这是时序约束的起点。少了这条后续的建立时间、保持时间报告都是空的时序收敛无从谈起。5. ModelSim 验证多模式计时器的实用技巧5.1 用任务封装测试激励直接写死 initial 块里的#5 clk~clk只能测一个场景四种模式加倒计时、暂停、复位全测下来会非常乱。更好的做法是把每种场景封装成 tasktask run_pattern(input [1:0] mode, input [1:0] speed); begin {m1, m2} mode; {v1, v2} speed; #2000; // 跑足够长观察灯变化 end endtask initial begin clk 0; rst 1; m1 0; m2 0; v1 0; v2 0; #20 rst 0; // 释放复位 run_pattern(2b00, 2b00); // 左循环最快 run_pattern(2b01, 2b11); // 右循环最慢 run_pattern(2b10, 2b01); // 交叉闪烁 run_pattern(2b11, 2b10); // 全亮全灭 $stop; end always #5 clk ~clk; // 100MHz 时钟这样每个 case 分支都被覆盖波形窗口里能直接看出 led 是否按预期移位或闪烁。用 task 的好处是改起测试刺激来不用重写 initial。5.2 波形观察与时序检查要点在 ModelSim 里重点看三样东西一是 rst 拉高时 led 是否立刻变成全 1异步复位的直接体现二是切换 m1/m2 后 led 的变化方向是否跟着变三是切 v1/v2 后 led 变化的时间间隔是否明显不同。很多人测速分级时发现四个档位看不出差别原因是仿真时间太短还没跑到慢速档位的进位点把仿真时长拉到几十万个时钟周期就能看出来。倒计时部分的波形要盯住 q 在每个秒脉冲来时是否正确借位尤其是 10:00 减到 09:59 这种跨越。原代码里 q 用的是 2 位字段冒充十进制位仿真时很容易看到 33 跳到 22 这种非法值这就是进制设计没做对导致的波形会直接暴露出来。提示仿真时钟周期设 10ns 对应 100MHz和实际板子一致这样测出来的时间间隔才有参考价值。如果仿真设了 10MHz实际跑起来速度会差 10 倍。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价