
1. 项目缘起为什么从原理图输入开始学FPGA如果你刚接触FPGA打开Quartus II或者Vivado看到满屏的代码编辑器可能会有点懵。Verilog HDL虽然强大但对于理解数字电路最底层的“门”与“线”是如何工作的它更像是一种高级抽象。很多初学者直接从Verilog写计数器、状态机开始虽然功能实现了但心里总感觉不踏实我写的这几行代码到底在芯片里变成了什么样子这就是我推荐新手尤其是电子、通信、自动化相关专业的学生从原理图输入开始第一个FPGA项目的原因。模60计数器作为数字电路中最经典、最基础的设计之一是理解时序逻辑的绝佳切入点。通过原理图你可以直观地看到时钟信号如何驱动触发器D Flip-Flop。数据通路如何通过逻辑门与、或、非和选择器Mux构建。反馈回路如何形成计数序列。复位信号如何将电路置于已知的初始状态。这个过程相当于在亲手搭建一个乐高积木版的数字电路。当你看着自己用一个个逻辑符号“画”出来的电路经过编译、综合、布局布线最终在开发板上跑起来LED按照0-59循环显示时那种对硬件“掌控感”的理解是直接写always (posedge clk)代码难以比拟的。今天我就带你完整走一遍用Quartus II原理图输入方式设计一个模60计数器的全过程并深入聊聊背后的设计考量、常见坑点以及如何向Verilog平滑过渡。2. 模60计数器的核心需求与设计拆解模60计数器顾名思义就是循环计数从0到59或从1到60看设计需求然后归零重新开始。它本质上是一个有限状态机有60个离散的状态。在数字电路中我们通常用二进制数来表示这些状态。2.1 确定计数器规格与输出编码首先我们需要明确设计规格计数范围0 ~ 59。这是一个通用约定符合时钟、计时器的习惯。计数使能需要一个使能信号en当en为高电平时计数器才在每个时钟上升沿进行计数操作。同步复位需要一个全局复位信号rst_n低电平有效在时钟上升沿到来时将计数器清零。使用同步复位可以避免复位信号与时钟竞争带来的亚稳态风险在FPGA设计中更为推荐。计数输出需要两组输出。一组是二进制码输出用于内部逻辑或后续处理另一组是BCD码输出用于直接驱动数码管显示。因为59的二进制是1110116位但为了驱动两个数码管十位和个位我们需要将其转换为两个4位的BCD码十位0101即5个位1001即9。所以我们的模块接口可以初步定义为输入时钟clk 同步复位rst_n 使能en。输出6位二进制数cnt_bin[5:0] 十位BCD码bcd_ten[3:0] 个位BCD码bcd_unit[3:0]。2.2 顶层架构分解为模6与模10计数器直接用60个状态设计一个状态机过于复杂。一个经典且高效的方法是级联法将模60计数器分解为一个模10计数器负责个位计数和一个模6计数器负责十位计数。工作原理个位的模10计数器从0计到9。当它计到9且下一个时钟使能有效时它会产生一个进位信号carry_unit。这个进位信号作为十位的模6计数器的使能信号。十位计数器在收到进位信号时加1。当十位计数器计到5且个位计数器计到9时下一个时钟周期两个计数器同时归零完成一个完整的60计数循环。这种分解大大简化了设计。我们只需要分别设计模6和模10计数器然后将它们连接起来。在原理图设计中我们可以先分别绘制这两个子模块的原理图然后将它们作为“符号”Symbol在顶层原理图中调用。2.3 二进制与BCD码转换的思考这里有一个关键点我们级联的模6和模10计数器其内部状态即cnt_bin本身就是一种混合进制表示十位是六进制个位是十进制但它不等于标准的BCD码。例如当计数到59时级联计数器的二进制输出可能是十位101(5) 个位1001(9) 拼接起来是1011001。而59的纯二进制是111011。我们需要的BCD码输出是十位0101(5) 个位1001(9)。因此我们需要一个二进制到BCD码的转换逻辑。对于0-59这个有限范围这个转换逻辑可以通过一个简单的组合逻辑查找表LUT来实现或者更直观地在原理图中用逻辑门搭建。但更简单的方法是我们直接设计一个输出就是BCD码的模10计数器和一个输出就是0-5二进制码的模6计数器。这样bcd_unit和bcd_ten可以直接从子计数器的输出引脚得到而cnt_bin则需要一个额外的转换电路bcd_ten * 10 bcd_unit这个转换也可以在原理图中用加法器和乘法器实际上是移位相加实现但考虑到这是入门项目我们可以先聚焦于计数器本身cnt_bin的输出可以暂时用Verilog写一个简单的转换模块或者后期再补充。为了简化初始设计我们优先保证BCD码输出的正确性。3. 在Quartus II中绘制模10计数器原理图现在我们进入实战环节。首先设计基础的模10计数器。我们将使用D触发器DFF和基本的逻辑门来构建。3.1 使用D触发器构建同步计数器同步计数器的核心思想是所有触发器的时钟端连接在一起由同一个时钟信号clk驱动。每个触发器下一时刻的状态D端输入由当前所有触发器的状态Q端输出通过一个组合逻辑电路称为“次态逻辑”决定。对于一个模10计数器计数0-9我们需要4个D触发器因为2^38 10 2^416 10。我们将其输出命名为Q3, Q2, Q1, Q0Q3是最高位。步骤新建原理图文件在Quartus II中选择File - New - Block Diagram/Schematic File。放置元件在空白处双击输入dff选择D Flip-Flop。放置4个分别命名为U0,U1,U2,U3。将它们的clk引脚连接在一起接到输入端口clk。将它们的clrn异步清零低有效或sclr同步清零引脚连接在一起接到输入端口rst_n。这里为了教学清晰我们使用sclr同步清零引脚并将其连接到经过一个非门not的rst_n信号因为sclr通常是高有效。放置输入端口input。放置三个分别命名为clk,rst_n,en。放置输出端口output。放置四个分别命名为Q0,Q1,Q2,Q3。这将是我们的BCD个位输出bcd_unit[3:0]。放置逻辑门and22输入与门、and3、and4、or2、or3、not、xor异或门等。我们将用它们来搭建次态逻辑。放置一个nand22输入与非门用于产生进位信号carry_unit。当计数器状态为91001时carry_unit变为低电平有效。推导次态逻辑核心这是最考验数字电路功底的一步。我们需要列出计数器的状态转换表。当前状态 (Q3 Q2 Q1 Q0)代表十进制下一状态 (D3 D2 D1 D0)进位carry_unit(状态9时有效)00000000110001100101............10008100111001900000我们需要写出D0, D1, D2, D3的卡诺图并化简得到最简逻辑表达式。这个过程可以手动完成也可以借助软件但为了理解我们写出化简后的结果假设使用JK触发器特性化简对于D触发器DJQ‘ K’Q但最终我们直接得到D的表达式D0 Q0’(每来一个时钟就翻转)D1 Q1 ^ Q0(当Q0为1时Q1翻转)D2 Q2 ^ (Q1 Q0)(当Q1和Q0都为1时Q2翻转)D3 Q3 ^ (Q2 Q1 Q0)(当Q2, Q1, Q0都为1时Q3翻转)但这只是二进制计数器的规律。我们是模10计数器需要在计到91001后归零所以需要修改D3和D2的逻辑在状态9时强制让它们变为0。同时还需要考虑使能en只有当en1时才进行上述计数否则保持。因此完整的次态逻辑为D0 (en) ^ Q0? 更准确地说D0 en (Q0’) !en Q0? 这样写太复杂。更好的方法是将en作为所有次态逻辑与门的一个输入。即D0 en (Q0’)是不对的因为当en0时D0应该等于Q0保持而不是0。所以我们需要一个选择逻辑D0 (en (Q0’)) | (!en Q0)。在原理图中这可以用一个2选1数据选择器MUX来实现。考虑到原理图绘制的复杂性对于初学者一个更可行且清晰的方法是先搭建一个带使能的4位二进制加法计数器。这可以通过调用Quartus II内置的宏功能模块lpm_counter来实现但这就失去了手绘的意义。我们退一步为了专注于“原理图输入”的体验我们可以先设计一个不带使能en的模10计数器。使能功能可以在顶层通过控制时钟或与门来实现。这样次态逻辑可以简化为只处理0-9的循环。让我们调整目标先绘制一个同步清零的模10计数器。当检测到状态9Q3 !Q2 !Q1 Q0时在下一个时钟周期清零。这可以通过将清零逻辑接入触发器的sclr同步清零端来实现但这样会占用控制端。更通用的方法仍然是修改次态逻辑。实际上绘制完整的4位模10计数器次态逻辑原理图非常庞大。作为教学示例我建议使用原理图绘制“行为级”描述即调用一个可预置数的同步计数器宏模块如lpm_counter将其设置为模10然后将其输入输出端口引出。这样既能快速完成功能又能理解原理图连接。但为了极致地“手搓”我们将绘制一个简化的2位模3计数器来演示思路因为其逻辑简单。模10和模6的绘制方法完全类似只是门电路更多。3.2 绘制简化示例模3计数器原理图假设我们要画一个计数0-1-2-0…的模3计数器。需要2个D触发器因为2^243。 状态转换00 - 01 - 10 - 00。 列出状态表求次态状态00 (Q10, Q00): 次态01 (D10, D01)状态01 (Q10, Q01): 次态10 (D11, D00)状态10 (Q11, Q00): 次态00 (D10, D00)通过卡诺图化简得到D0 !Q1 !Q0在状态00时D0为1D1 Q0在状态01时D1为1在Quartus II中绘制放置两个dffU0和U1。连接clk和sclr。对于U0输出Q0其D端输入D0的逻辑是!Q1 !Q0。所以需要放置一个not门接Q1另一个not门接Q0然后用一个and2门连接这两个非门的输出再将and2的输出连接到U0的D引脚。对于U1输出Q1其D端输入D1的逻辑就是Q0。直接将U0的Q引脚连接到U1的D引脚。添加输入clk,rst_n。rst_n连接非门后接到两个DFF的sclr。添加输出Q0,Q1。这样一个模3计数器的核心原理图就画好了。编译后可以通过仿真验证其功能。模10和模6计数器就是这种模式的扩展需要更多的触发器和更复杂的组合逻辑网络。3.3 生成符号Symbol绘制好模10计数器假设我们将其简化设计或使用宏功能完成了后将其保存为.bdf文件例如mod10_counter.bdf。然后在Quartus II菜单栏选择File - Create/Update - Create Symbol Files for Current File。这将生成一个mod10_counter.bsf文件。这个符号就可以像其他门电路一样被拖放到顶层原理图中使用了。按照完全相同的步骤我们可以绘制一个模6计数器计数0-5需要3个D触发器因为2^246, 2^386并生成mod6_counter.bsf符号。4. 顶层原理图集成与关键信号处理有了两个子计数器符号后我们开始搭建顶层的模60计数器。4.1 连接级联与使能逻辑新建顶层原理图文件top_mod60_counter.bdf。放置子模块符号在空白处双击输入mod10和mod6找到刚才生成的符号放置到图中。分别命名为U_mod10_unit和U_mod6_ten。放置输入/输出端口输入clk,rst_n,en。输出bcd_unit[3..0],bcd_ten[2..0],carry_out。注意总线表示法[3..0]代表4根线。连接电路将全局clk连接到两个子计数器的clk输入端。将全局rst_n连接到两个子计数器的rst_n输入端。关键连接使能与进位。个位模10计数器的使能端en直接连接全局使能en。个位模10计数器需要有一个进位输出端口我们在画图时就应预留当计数到9时carry_unit输出一个时钟周期的高电平脉冲。将这个carry_unit信号与全局使能en相与使用一个and2门结果作为十位模6计数器的使能信号。即en_ten en carry_unit。这样只有当全局使能有效且个位计满时十位才加1。连接输出将U_mod10_unit的4位输出总线连接到bcd_unit[3..0]输出端口。将U_mod6_ten的3位输出总线连接到bcd_ten[2..0]输出端口。bcd_ten是0-5的二进制直接对应十位的0-5。生成整体进位carry_out当十位计到5bcd_ten 3‘b101且个位计到9bcd_unit 4‘b1001且使能有效时下一个时钟周期计数器应归零此时可以产生一个进位信号。这需要一个4输入与门判断个位和一个3输入与门判断十位再将这两个与门的输出相与最后再与全局en相与。这个carry_out信号可以用来驱动更高位的计数器例如在时钟设计中驱动小时计数器。4.2 二进制输出转换逻辑的实现如前所述cnt_bin[5:0]需要从BCD码转换。cnt_bin bcd_ten * 10 bcd_unit。在原理图中实现乘法器比较繁琐。我们可以用一个技巧bcd_ten * 10 (bcd_ten 3) (bcd_ten 1)。即左移3位相当于乘8左移1位相当于乘2加起来就是乘10。位扩展与移位bcd_ten是3位bcd_unit是4位。我们需要一个6位的加法器。将bcd_ten左移3位这意味着在低位补3个0形成一个6位数{bcd_ten, 3‘b000}。将bcd_ten左移1位形成一个6位数{2‘b00, bcd_ten, 1‘b0}。放置加法器在Quartus II元件库中查找lpm_add_sub参数化加减法器。实例化一个设置数据位宽为6位功能为加法。连接将上述两个移位后的6位数据连接到加法器的两个输入口。最终加法将加法器的输出与bcd_unit需要零扩展为6位{2‘b00, bcd_unit}通过另一个6位加法器相加结果就是cnt_bin[5:0]。这个过程在原理图中会显得连线很多。对于初学者项目如果cnt_bin不是必须的可以暂不实现优先保证BCD显示的正确性。或者我们可以用嵌入式逻辑块写一个很小的Verilog文件来实现这个转换然后将这个Verilog模块也生成一个符号插入到原理图中。Quartus II支持原理图和HDL的混合设计这非常有用。4.3 设计实例在原理图中调用Verilog模块新建一个Verilog HDL文件命名为bcd_to_bin.v。module bcd_to_bin ( input [2:0] bcd_ten, // 十位 (0-5) input [3:0] bcd_unit, // 个位 (0-9) output reg [5:0] bin_out // 二进制输出 (0-59) ); always (*) begin bin_out bcd_ten * 6‘d10 bcd_unit; // 直接使用乘法运算符综合器会将其优化为电路 end endmodule对这个Verilog文件进行分析与综合Analysis Synthesis。在顶层原理图文件中双击空白处输入bcd_to_bin就可以找到并放置这个模块的符号了。将bcd_ten和bcd_unit总线连接到该符号的输入将其输出连接到cnt_bin输出端口。这种方法混合了原理图的直观性和HDL的灵活性是工程中常见的做法。5. 编译、仿真与硬件测试设计绘制完成后必须经过验证才能保证正确性。5.1 引脚分配与编译创建测试向量或设置时钟我们需要为设计指定一个时钟引脚。在Assignment - Pin Planner中将clk分配到开发板上连接晶振的专用时钟引脚如PIN_23将rst_n和en分配到按键或拨码开关对应的引脚将bcd_unit和bcd_ten分配到数码管段选或LED对应的引脚。编译点击Start Compilation。Quartus II会进行一系列操作分析与综合、布局布线、时序分析、生成编程文件。在这个过程中软件会将你的原理图翻译成门级网表并映射到FPGA内部的逻辑单元LE和触发器上。查看RTL视图编译成功后点击Tools - Netlist Viewers - RTL Viewer。这是极其重要的一步在这里你可以看到Quartus II综合后生成的电路结构。对比一下这个自动生成的电路和你手绘的原理图思考它们是否在功能上等价。这能极大地加深你对“综合”的理解。5.2 功能仿真ModelSim或Quartus自带仿真器仿真是在将设计下载到芯片前用软件模拟其行为是排查逻辑错误的最有效手段。创建波形文件File - New - University Program VWF或使用ModelSim创建.do脚本和.vwf文件。添加信号将clk,rst_n,en,bcd_unit,bcd_ten,cnt_bin等信号添加到波形窗口。设置激励设置clk为周期性的方波如周期20ns。初始时刻设置rst_n0保持一段时间如100ns然后拉高。设置en1使能计数。运行仿真运行足够长的时间如2us观察波形。检查复位在rst_n为低期间所有输出是否都为0检查计数序列bcd_unit是否从0递增到9然后归零同时bcd_ten是否在bcd_unit从9归0时加1检查模60循环当bcd_ten为5且bcd_unit为9时下一个时钟沿两者是否同时归零检查使能将en在某段时间拉低观察计数器是否暂停。分析结果如果波形与预期不符需要返回原理图检查逻辑连接特别是进位和清零逻辑。5.3 常见问题与调试心得问题1计数器不计数一直为0。排查首先检查时钟信号是否正常。在仿真波形里看clk有没有跳变。其次检查复位信号rst_n是否一直为有效电平低电平。最后检查使能信号en是否被拉高。心得同步复位信号sclr需要在时钟上升沿采样时才起作用。确保你的复位信号在仿真开始后有足够的稳定时间在第一个时钟上升沿之前就变为无效状态。问题2个位能正常计0-9但十位不计数。排查十位的使能信号en_ten是否正确产生检查个位计数到9时carry_unit信号是否出现了一个脉冲检查连接carry_unit和全局en的与门是否正常工作。心得在原理图中经常因为连线错误或总线连接不当导致信号不通。使用Netlist Viewer查看综合后的网表确认信号连接关系。对于总线确保位宽匹配例如[3..0]的总线不能直接连到[2..0]的端口上。问题3计数到59后下一个状态不是00而是其他值如10, 20等。排查这是清零逻辑错误。模10和模6计数器在达到最大值时必须能正确归零。检查你的模6计数器是否真的只有6个状态0-5。检查在状态5时其次态逻辑是否被正确设置为0。同时检查级联的清零信号是否在59状态时正确传递。心得对于模M计数器确保你用最少的触发器N个满足2^(N-1) M 2^N实现了所有状态并且没有无效状态即那些不会进入的状态。如果存在无效状态需要考虑电路上电后能否自动回到有效循环否则可能“跑飞”。一个稳健的做法是在组合逻辑中明确指定所有无效状态的次态为0同步复位状态。问题4时序警告Timing Violation。排查编译后查看TimeQuest Timing Analyzer报告。关注“最差建立时间余量Worst-case setup slack”和“最差保持时间余量Worst-case hold slack”是否为负。心得对于这个低速计数器项目在几十MHz的时钟下通常不会有时序问题。如果出现可能是由于组合逻辑路径过长例如BCD转二进制的那一堆加法器。可以尝试流水线或寄存器打拍将复杂的组合逻辑拆分成多个时钟周期完成。例如不在同一个时钟周期内完成*10和的操作而是先计算bcd_ten*8和bcd_ten*2并寄存下一个周期再相加。对于原理图设计这意味需要插入更多的D触发器来分割路径。6. 从原理图到Verilog思维模式的转变通过这个项目你应该对计数器的硬件构成有了血肉般的认识。接下来我们就可以平滑地过渡到Verilog了。你会发现之前画的每一个部分都对应着Verilog中的一段描述。D触发器-always (posedge clk)过程块。组合逻辑网络-assign连续赋值语句或always (*)组合过程块。模块/符号-module定义和实例化。连线- wire 类型变量。用Verilog重写上面的模60计数器代码会简洁得多module mod60_counter ( input wire clk, input wire rst_n, input wire en, output reg [3:0] bcd_unit, output reg [2:0] bcd_ten, output wire [5:0] cnt_bin ); // 二进制转换组合逻辑 assign cnt_bin bcd_ten * 6‘d10 bcd_unit; // 个位模10计数器 always (posedge clk or negedge rst_n) begin if (!rst_n) begin bcd_unit 4‘d0; end else if (en) begin if (bcd_unit 4‘d9) begin bcd_unit 4‘d0; end else begin bcd_unit bcd_unit 4‘d1; end end end // 十位模6计数器受个位进位控制 always (posedge clk or negedge rst_n) begin if (!rst_n) begin bcd_ten 3‘d0; end else if (en (bcd_unit 4‘d9)) begin // 个位进位条件 if (bcd_ten 3‘d5) begin bcd_ten 3‘d0; end else begin bcd_ten bcd_ten 3‘d1; end end end endmodule看是不是清晰多了原理图训练让你理解了这短短十几行代码背后的硬件结构。你知道bcd_unit bcd_unit 1背后是一个加法器和一组D触发器你知道if (bcd_unit 4‘d9)背后是一个4输入的比较器电路。最后我个人体会是原理图输入作为FPGA学习的起点价值不在于用它去做大型项目那会非常低效而在于它像一副“X光片”让你能透视HDL代码所描述的硬件世界。当你以后再写Verilog时脑海中能自动浮现出对应的门、触发器和连线图你会对如何写出可综合、高性能、面积优的代码有更直觉的把握。从这个模60计数器开始试着去画更复杂的电路比如状态机、FIFO控制器然后再用Verilog实现对比两者你的硬件设计功力会扎实地迈上一个台阶。