简介面向北京邮电大学数字逻辑课程设计的一份FPGA工程代码包适合正在学习时序逻辑、状态机或硬件描述语言的电子类学生参考。项目涵盖电子钟与药片装瓶两个典型实验涉及计数器、分频器、译码器、米利型和摩尔型状态机以及引脚锁定等关键设计环节源码包含多种硬件描述语言实现。资源共五百三十七个文件压缩包约四点四四兆字节以硬件描述语言源文件为主并配有引脚约束文件、比特流文件、脚本、报告及仿真日志基本覆盖从编写、综合、仿真到下载验证的完整流程。目录结构按工程组织便于对照学习引脚分配与布局布线细节。目前已有1179人下载学习对希望快速上手FPGA数字系统设计与排错的读者有较高参考价值。1. 拆开压缩包之后先别急着写代码课程设计代码包里到底装了些什么大学里做数字逻辑课程设计最典型的场景就是老师布置一个题目比如数字钟、频率计、流水灯控制器或者交通灯状态机然后你就开始从Verilog语法书的第一章啃到最后一章最后综合出一坨能跑但不知道哪里会有问题的代码。如果你手里正好有一份“北邮数字逻辑课程设计FPGA代码.zip”这样的资源第一反应肯定是解压然后想看看里面有没有能直接抄的模块。但说实话拿到代码包之后的核心动作不应该是“找代码”而是先搞清楚这个项目的整体架构——这也是FPGA开发和单片机开发最大的区别之一FPGA里没有“顺序执行”这个概念所有模块都是并行的所以代码包的结构直接反映了设计者的思维方式。一个完整、规范的数电课设代码包通常应该包含这么几类东西文件类型常见扩展名作用Verilog源码.v核心逻辑包括顶层模块、分频模块、状态机、显示驱动等仿真文件.tb.v / _test.vtestbench用来在电脑上模拟信号变化验证逻辑约束文件.ucf / .xdc把FPGA芯片引脚映射到开发板上的具体物理管脚工程文件.xise / .qpf / .xpr对应ISE、Quartus或Vivado的工程配置设计文档.doc / .pdf模块划分、状态转移图、管脚分配表比特流文件.bit / .sof / .svf已经综合布线后的下载文件如果你打开压缩包发现里面有这六类东西恭喜你这份代码的完成度应该不低。如果压缩包里只有一堆光秃秃的.v文件那也别慌只要顶层模块写得清晰一样可以快速重建整个工程。拿到代码之后我建议的阅读顺序是先读README或者设计文档如果有的话搞清楚这个课设到底是做什么功能的。像“北邮数字逻辑课程设计”这类项目通常题目是固定在几个方向里的数字频率计、数字秒表、多功能电子钟、简易计算器、交通灯控制器、波形发生器。这些题目虽然功能各异但底层模块高度相似都是“分频 计数 状态机 显示”四件套。再读顶层模块top module因为顶层模块会把所有子模块的实例化关系、信号连接关系列得明明白白。读懂了顶层你就知道整个项目的血液是怎么流动的。最后再去抠子模块的内部逻辑。子模块一般包括时钟分频器、按键消抖、计数器、译码器、扫描显示驱动等。很多初学者拿到代码包后的第一反应是“怎么这么多文件”然后就晕了。其实换个角度想FPGA的模块化设计就跟装修房子一样顶层模块是房子的户型图分频模块是配电箱状态机是智能家居控制中心显示驱动是各个房间的灯光开关。你不需要同时关注每一个灯泡是怎么接线的你只需要知道每一路开关控制哪一盏灯就够了。2. 把代码跑起来的第一步环境重建和引脚约束的那些坑FPGA开发最让人头疼的往往不是逻辑设计本身而是“代码明明写对了为什么就是不出结果”。这种情况十有八九出在工程环境重建和引脚约束上。北邮数电课设用的开发板我见过的基本上以Xilinx Spartan系列和Altera/Intel Cyclone系列为主不同年级、不同实验室板子型号差别很大。代码包里的约束文件如果对应的是某一块特定的板子你直接换到另一块板子上编译最典型的结果就是综合通过、实现通过、生成比特流通过但下载到板子上之后LED灯不亮、数码管全灭、按键完全没反应。原因很简单——引脚约束是FPGA开发里最“物理世界”的一环。Verilog代码里写的signal_a在数字逻辑层面只是一个抽象的线网但在真实芯片里它必须通过IO Buffer输入输出缓冲器连接到芯片的某个物理管脚再由这个管脚通过PCB走线连到开发板上的某个LED、按键或数码管段码引脚。你代码里写的clk信号如果约束文件里给的是板载50MHz有源晶振的引脚编号那没问题如果给的是某个按键对应的引脚编号那你的主时钟永远等不到边沿。所以拿到别人的代码包之后第一件要做的事不是编译而是打开约束文件对照自己手里的开发板原理图逐行检查这些关键引脚系统时钟引脚通常叫clk或sys_clk确认晶振频率是50MHz、100MHz还是12MHz代码里分频系数是按多少MHz算的。这里有一个非常经典的坑很多教程代码默认50MHz但你的板子是27MHz或12MHz那么所有定时类的功能比如秒表计时、频率计闸门时间全部会按错误的比例跑。表现出来就是仿真全对上板时间全乱。复位按键引脚有的代码用高电平复位有的用低电平复位这由板载按键的接法决定。板上按键一端接3.3V、一端接FPGA引脚那按下时给到FPGA的就是高电平如果按键一端接地按下时给到的就是低电平。别小看这点我见过一个同学把低有效复位的代码配到高有效复位的板子上结果整个模块永远处于复位状态数码管永远显示0000排查了两天才发现是复位极性反了。数码管位选和段选引脚位选决定哪一位数码管点亮段选决定这一位显示什么数字。这两组引脚数量多位选4~8根段选7~8根而且每块板子的排列顺序都可能不同出问题概率最高。特别是段选信号有的板子数码管是共阳极段选低电平点亮有的是共阴极段选高电平点亮代码里的译码逻辑如果写反了显示出来的数字全是你认识它但它不认识你的状态。如果你手里的开发板跟原代码包不是同一块我的经验是先不要急着用别人写好的约束文件。自己新建一个约束文件从原理图上一根一根查引脚编号写下几根关键引脚然后编译下载先实现“下载后LED能随按键闪烁”这个最小目标。等这一套通了再逐步把其余引脚补上。这样做看似多花了一个小时实际上帮你省下后面至少一天的调试时间。另外还有一类环境坑跟FPGA厂商工具链的版本有关。老的课程设计代码如果是用ISE 14.7写的你拿Vivado 2020.1去打开工程大概率会提示工程文件版本不兼容。这种情况下别在兼容性上死磕正确的处理方法是在Vivado里新建一个空工程选择对应型号的FPGA芯片然后把所有.v源文件、约束文件、仿真文件重新添加进去。Verilog是文本文件语法层面基本是通用的只要代码风格不是太老比如还在用已经废弃的timescale写法Vivado都能正常综合。反过来如果是高版本工程文件想用低版本工具打开那就基本无解了——厂商工具一般不做向下兼容。3. 核心模块逐个看分频器、状态机和数码管扫描是怎么串起来的代码包看得多了你就会发现不管题目怎么变数字逻辑课程设计里的核心代码都绕不开这几个模块。这一节我挑三个最关键、也是最容易在写代码时翻车的模块结合代码包里常见的具体实现把背后的设计逻辑讲清楚。3.1 分频器从50MHz得到1Hz不是简单除一下就行分频器是FPGA里最基础的时序逻辑但它的写法里藏着一个很重要的设计思想什么时候用计数器直接分频什么时候用锁相环PLL分频。课程设计阶段99%的情况都是直接用计数器分频。比如系统时钟是50MHz想要一个1Hz的秒脉冲那就计数25_000_000个时钟周期后翻转一次电平。代码如下reg [24:0] cnt; reg clk_1hz; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 25d0; clk_1hz 1b0; end else if (cnt 25d24_999_999) begin cnt 25d0; clk_1hz ~clk_1hz; end else begin cnt cnt 1b1; end end这里我用了cnt 25d24_999_999作为翻转条件。为什么不是25_000_000因为计数器是从0开始数的25MHz的计数范围0~24_999_999正好是25_000_000个周期50MHz时钟下高电平25ms、低电平25ms合起来一个周期就是50ms也就是20Hz。要得到1Hz需要数50_000_000个周期代码里就要写成cnt 50d49_999_999。这个“减1”的问题是新手最容易写错的地方而且仿真时因为时间短不容易暴露上板之后才露馅。还有一点这种用计数器翻转产生的时钟在FPGA内部叫“门控时钟”gated clock。在真正的工程实践中门控时钟是要尽量避免的因为时钟信号经过逻辑资源会产生偏斜影响时序收敛。正确的做法是用厂商提供的PLL/IP核来生成分频时钟。但对于课程设计来说板子上的LED和数码管对时钟偏斜完全不敏感门控时钟完全够用也更容易理解。如果你追求代码“可进大厂”的规范程度可以用PLL但课设答辩时直接讲计数器分频的原理反而更容易得分——因为老师知道这是你自己写的。3.2 状态机课设里的“大脑”建议所有状态都用独热码数字逻辑课设里状态机主要用在两类场景一是控制类比如交通灯的红绿黄切换、自动售货机的投币找零流程二是时序类比如数码管动态扫描的刷新流程、按键消抖的稳定判定流程。状态机的写法有两大流派一段式、两段式和三段式。课程设计代码里最推荐的是三段式。理由很简单一段式把所有逻辑写在一个always块里代码短但状态转移和输出逻辑混在一起调试时改一处动全身两段式把状态转移和输出分开已经好很多三段式再把输出逻辑单独用组合逻辑或寄存器输出结构最清晰也最容易在答辩时跟老师讲明白。再看一个很实用的细节——状态编码。状态机的编码方式主要有二进制编码如S02b00, S12b01, S22b10、格雷码和独热码one-hot如S04b0001, S14b0010, S24b0100。课程设计里我强烈建议用独热码。原因有三点独热码每个状态只有一个bit为1状态判断逻辑不需要复杂的译码电路组合逻辑面积更小。综合工具对独热码的优化很成熟时序表现更稳定。调试时看波形一眼就能认出当前状态——哪个bit为1就是哪个状态。代价是状态寄存器位宽更大几个状态无所谓十几个状态也还在可接受范围内。所以课设这种几十个状态以内的场景独热码是性价比最高的选择。localparam IDLE 4b0001; localparam COUNT 4b0010; localparam STOP 4b0100; localparam DISP 4b1000; reg [3:0] current_state; reg [3:0] next_state; always (posedge clk or negedge rst_n) begin if (!rst_n) current_state IDLE; else current_state next_state; end always (*) begin case (current_state) IDLE: next_state start ? COUNT : IDLE; COUNT: next_state stop ? STOP : (pause ? IDLE : COUNT); STOP: next_state start ? COUNT : DISP; DISP: next_state IDLE; default: next_state IDLE; endcase end唯一要记住的是独热码的case语句必须写default分支否则综合时会生成多余的锁存器而且未定义的状态直接导致行为不可控。3.3 数码管动态扫描刷新率决定你会不会看到鬼影数码管动态扫描是FPGA课设里最经典的显示方案。原理很简单同一时刻只点亮一位数码管通过快速循环扫描位选利用人眼的视觉暂留效应让人觉得四位数码管同时都在亮。但这里有一个非常关键的系统性参数扫描频率。扫描频率太慢人眼会感觉到闪烁扫描频率太快单个数码管的导通时间太短亮度会显著下降而且可能会出现“串位”——即某一位数码管还没完全熄灭下一位已经点亮了看起来就像数字后面拖了个鬼影。工程上常用的扫描频率范围是500Hz到1kHz之间。也就是说四位数码管每秒钟完整循环刷新500到1000次。这个范围是怎么定的人眼对闪烁的感知临界频率大约在50Hz到60Hz低于这个值会明显看到闪烁而四位数码管共享一个扫描周期的话每位点亮的占空比只有1/4如果扫描频率刚好是60Hz那实际每位的刷新率只有15Hz闪得没法看。所以要做四位扫描总刷新率至少要200Hz以上才能保证每位达到50Hz的感知刷新率实际操作中取500Hz到1kHz留足裕量。对应到50MHz系统时钟扫描周期就是50ms到100ms。这里我给出一个参考计数器设计reg [16:0] scan_cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) scan_cnt 17d0; else if (scan_cnt 17d99_999) // 1ms scan_cnt 17d0; else scan_cnt scan_cnt 1b1; end1ms产生一次位选切换信号四位数码管轮流点亮每个周期4ms刷新率250Hz完全够用。如果你的板子晶振不是50MHz记得相应调整这个计数值。数码管显示的另一个坑是数据同步。比如你用一个计数器计秒计数值从59变为00的那一刻正好赶上扫描信号切到最高位那最高位可能显示旧数据其余位显示新数据产生一帧错乱。严格来说这属于跨时钟域问题计数器的更新和扫描的更新在同一个时钟域里其实不算真正的跨时钟域但存在数据采样时序问题。解决办法是把显示数据寄存器在切换前锁存一份让扫描逻辑永远从缓存寄存器取数而不是直接读计数器的实时值。代码包里如果做到了这一步说明原作者的功底相当扎实值得你好好学习。4. 仿真与验证不要只点一下“Run Simulation”就完事很多同学拿到代码包后走完“新建工程、添加文件、综合、生成比特流、下载”这条流水线看到板子上有反应就以为大功告成了。但真正的调试才刚刚开始。FPGA开发里有一句老话能在板子上跑起来的代码不等于没有bug的代码。尤其课程设计这种规模的项目如果只做上板验证不做仿真验证你根本不知道代码的边界行为是什么。4.1 testbench该怎么写才有价值代码包里一般都会带testbench文件但质量参差不齐。很多课设的testbench就做了两件事给个时钟、给个复位然后跑200个周期就停了。这种仿真只能证明代码“基本能跑”完全覆盖不到边界条件。真正的testbench应该至少做到三件事生成正确的时钟和复位信号并且复位信号要有释放过程。复位不能一开始就是1或0而要经历“有效——保持一段时间——释放”的过程用来验证异步复位的同步释放逻辑。通过task或initial块模拟真实按键输入。比如你要测试秒表的暂停功能就应该在仿真中让按键信号在特定时刻拉高、保持若干毫秒、再拉低模拟一次完整的物理按键操作。这里还要注意真实的物理按键是有抖动的所以testbench里最好也模拟抖动——先拉高然后连续翻转几次最后稳定在高电平。这样你的按键消抖模块才能在仿真阶段就被验证到。主动检查输出而不是人眼盯着波形图。用$display或assert来判定输出是否符合预期仿真结束后自动打印结果。这样做的好处是回归测试时可以自动确认“新改动没有破坏旧功能”。4.2 实用仿真技巧用虚拟时钟和文件输出做“半自动验证”我们以频率计为例假设你要测量一个1kHz的待测信号。频率计的核心逻辑是在固定闸门时间比如1s内对输入的待测信号进行计数计数值就是频率值。仿真时如果你真的等仿真器跑完1秒钟那在计算机上会非常慢。怎么办两个办法。第一个办法是缩短闸门时间。把设计里的闸门时间参数化比如定义一个parameterGATE_CYCLES仿真时把它设成100个时钟周期上板前改回1s对应值。这样测试一个流程只需要几微秒的仿真时间。第二个办法是直接用force命令将内部信号强行拉高拉低快速跳转到指定状态。比如测秒表暂停功能正常操作需要“按键按下—消抖—状态机跳转”一整套流程在仿真里要跑几万个周期。用force直接修改按键信号配合wait语句等待状态机翻转几行代码就能完成。再提一个很多人不知道的技巧仿真时可以用$writemem和$readmemh配合外部文件。比如你要测试一个简易UART接收模块可以把testbench的串行输入数据从文件里读进来要把仿真结果记录下来可以定期把内部状态写入文件。这样即使仿真时长很长你也能事后分析。4.3 上板调试的正确姿势ILA/VIO不是洪水猛兽课程设计答辩时老师最爱问的一个问题是“你代码跑通了你怎么证明它跑通了”如果你说“我看了板子功能正常”其实是很弱的回答。因为FPGA是个并行系统板子上看到的往往是多个模块协同工作的最终结果中间任何一环出问题最后的现象可能都一样灯不亮、数字不对。正确的做法是使用片上逻辑分析仪。Xilinx平台叫ILAIntegrated Logic AnalyzerIntel平台叫SignalTap。这东西本质上就是把你FPGA内部的几个关键信号按照设定的触发条件实时抓取到芯片内部的BRAM里然后通过JTAG回传到电脑上显示波形。使用ILA时有三点建议不要等代码全部写完才加ILA。最理想的做法是添加ILA核把顶层模块里的关键信号拖进去布好探针编译下载运行一整轮采集波形分析修改代码重复。这样每一轮修改都有波形数据作为依据而不是靠猜。ILA的采样深度不要设得太大。默认的1024或2048个采样点通常够用太大的采样深度会占用BRAM资源影响综合后的最高频率。触发条件要选好。比如你要抓取秒表的暂停瞬间就把触发条件设为“按键按下信号上升沿”这样ILA会从按下前几十个周期开始记录一直记录到按下后的几十个周期。这样能清楚看到按键信号进入、消抖模块的计时输出、状态机的跳转、计数器的停止一整条因果链。在课程设计这个层面你会用ILA看波形就比90%的同组同学可能包括你的答辩老师预期水平高出一截了。因为这意味着你不是“代码写完就交付”而是做了真正的硬件调试。5. 课程设计代码里最常见的五个隐藏Bug以及怎么避免这一节的内容是我在看了不知道多少份课设代码、以及自己复现FPGA项目时总结出来的高频故障点。每个都对应着具体的代码写法和背后的电路原理踩过坑的人应该一看就懂。5.1 复位信号“重名”问题同步复位还是异步复位很多代码的模块里既有rst_n参数又有clk参数然后每个always块第一行写的是always (posedge clk or negedge rst_n) begin if (!rst_n) ...这是标准的异步复位写法。异步复位的优点是不依赖时钟就能立刻让电路回到初始状态缺点是对复位毛刺非常敏感。如果你的复位信号是按键直接接的按下和释放的瞬间会产生机械抖动——也就是会出现若干次非常窄的毛刺——这些毛刺如果刚好作用在寄存器复位端会导致寄存器被不可控地复位。解决办法有两种一是加一个按键消抖模块把消抖后的信号作为复位信号二是把复位方式改成同步复位即只在时钟上升沿检查复位信号always (posedge clk) begin if (!rst_n) ...同步复位对毛刺免疫因为毛刺不会刚好落在时钟边沿上。代价是复位信号必须保持足够长的时间至少一个时钟周期否则复位可能没生效。二者各有利弊但课程设计里面我更推荐“消抖后的按键信号 异步复位”组合。原因很简单物理按键按下时除了机械抖动还会有弹跳造成的一段浮空区这会影响上电后的初始状态。用异步复位可以保证只要复位键一按电路立刻回零不依赖时钟是否稳定。5.2 阻塞赋值与非阻塞赋值混用仿真没问题上板就翻车这是Verilog最著名的坑没有之一。简单说时序逻辑里带时钟上升沿的always块必须用非阻塞赋值组合逻辑里always (*)用阻塞赋值。两者混用会导致仿真行为和综合后行为不一致。具体例子很多人为了图省事在同一个always块里既给计数器赋值又给标志位赋值并且混用了和。仿真时因为仿真器按顺序执行看起来逻辑是对的但综合工具会把它综合成一个带多个输出的触发器和一堆组合逻辑真实电路里的时序关系和仿真完全不同。结果就是ModelSim里仿真波形完美下载到板子上计数器一位一位地乱跳。解决办法只有一条严格执行“时序逻辑用组合逻辑用”这条规则。这是一个习惯问题但只要养成了能帮你避开数电课设从仿真到上板的绝大多数时序翻车。5.3 状态机默认状态缺失之前提到独热码状态机的case语句必须写default其实这个规则对所有类型的状态机都适用。如果不写default当状态寄存器因为干扰进入一个未定义状态时状态机会卡死在那个状态而且这个错误在仿真阶段很难暴露因为仿真时状态变量只会取到代码里赋过的值。让状态机进入非法状态的方法其实很多全局复位信号不稳定、组合逻辑的竞争冒险、时序违规都可能导致。所以default分支的作用是“兜底”让任何非法状态都能自动回到IDLE或某个安全状态。5.4 计数器位宽不足假设你要设计一个闹钟用24小时制小时计数器最大到23。如果你声明reg [3:0] hour那取值范围是0~15等到hour计数到10时会溢出回0闹钟显示就乱了。必须用reg [4:0] hour才能覆盖0~23。同样的道理分频计数器很容易溢出。50MHz是50_000_000用十进制换算成二进制是26位2^25 33_554_4322^26 67_108_864所以至少要声明reg [25:0] cnt也就是26位。很多代码写成reg [15:0] cnt时钟跑到65535就回绕了分频结果完全不对。这种错在仿真时如果时间设置得不够长也看不出来但上板一定会出问题。给个判断位宽的标准公式计数上限为N0~N-1需要的位宽是$clog2(N)也就是以2为底向上取整。50MHz对应N50_000_000位宽就是$clog2(50_000_000) 26。5.5 跨时钟域信号没有打拍同步如果你的项目里有多个时钟源比如系统时钟50MHzSPI接口的时钟1MHz有些高级课设题目会涉及那么两个时钟域之间传信号时必须做同步处理。最常见也最实用的做法是对跨时钟域的单bit信号打两拍即用目标时钟域的寄存器连续采两拍。reg sync_1, sync_2; always (posedge clk) begin sync_1 spi_cs_n; sync_2 sync_1; end第一拍消除亚稳态第二拍给出稳定的同步信号。课程设计里如果涉及按键输入和模块间通信强烈建议在每个输入信号进模块的第一时间就做一次打拍同步。这跟消抖是两回事——消抖针对的是机械抖动打拍针对的是跨时钟域的亚稳态。两者可以串联使用输入信号——打拍同步——消抖——进入状态机。6. 把课程设计代码吃透之后下一步能往哪走如果你已经把代码包里的每个模块都过了一遍仿真也做了上板也通了那这份课设的学习价值其实才刚刚开始。我建议你做三件事每一件都能让这次课程设计的含金量上一个台阶。第一挑一个模块自己从零写一遍。比如把原来别人写的分频器删掉自己用PLL IP核重新实现一遍对比两者的功耗、资源和时序或者自己写一个UART发送模块把板子上的数据通过串口传到电脑上。这个过程能让你把“会读代码”升级成“会写代码”。第二给现有设计加一个“超纲”的功能。比如原来的课设是秒表你可以加一个暂停时的数据保持功能原来是交通灯你可以加一个根据车流量自动调整绿灯时长的简单逻辑。加功能的过程会逼迫你去学更深的东西——比如状态机扩展、参数化设计、甚至简单的SOC概念。第三把代码里的关键模块做成可复用的IP。比如把按键消抖做成一个独立的模块以后所有项目都能直接调用。很多FPGA工程师的“个人代码库”就是这样一点一点积累起来的这才是代码包真正的价值——你不只是在做一次课设你是在建立自己的硬件设计能力体系。我自己的体会是FPGA学习最忌讳的是“重仿真轻上板”或者“重上板轻仿真”。两者必须结合起来仿真是为了让你在没上板前就能发现逻辑错误上板是为了让你确认仿真没覆盖到的物理世界细节。课程设计只是起点把这份代码吃透你下一步就可以去尝试更复杂的项目比如图像采集、以太网通信或者RISC-V软核处理器。那时候你会发现数字逻辑课设里学的分频、状态机、显示驱动这些基本功真的全都用得上。本文还有配套的精品资源点击获取