
这几年经手的数字电路项目里Verilog做到一定阶段一定会撞上逻辑综合这道坎。很多朋友写仿真代码跑得飞起波形也对结果一进综合工具就报一堆警告要么时序违例要么没来由地冒出锁存器。我一直觉得Verilog设计与逻辑综合从来不是两件事而是同一条设计流程的前后半程。这篇内容就用一个带完整代码的滑动窗口滤波器实例把从RTL编写、仿真验证到逻辑综合的完整链路拆开讲一遍。适合刚入门想搞清楚“可综合代码到底该怎么写”的同学也适合已经会写仿真、但还没正经跑过综合流程的工程师参考。1. 整体设计与思路拆解RTL到门级的关键链路1.1 逻辑综合把Verilog变成了什么逻辑综合Logic Synthesis本质上是把行为级的RTL描述翻译成由标准单元库中的逻辑门、触发器、锁存器等组成的大门级网表。你可以把它类比成一个很认真的厨师RTL是菜单工艺库是超市里能买的菜综合工具就是那个要求极其严格的大厨——不仅要按菜单做菜还得在规定时间内端出来厨房面积也有上限所以必须边做边优化。综合工具一般分三步干活编译Compile、优化Optimize、映射Map。编译阶段把Verilog解析成中间格式的布尔逻辑优化阶段在这个中间格式上做逻辑化简、资源共享、常数传播映射阶段再对照工艺库把逻辑映射成具体的标准单元。这中间每一个环节都会改变电路结构但最终必须保证功能和你的RTL一致——这是综合工具的基本原则。我经常跟团队里的人说写Verilog心里要有一张“电路图预演”。你写出来的每一行代码闭上眼睛都能想象出大概会变成什么硬件结构assign是组合逻辑时序always块是寄存器簇case/if是选择器状态机就是一组寄存器和一堆组合逻辑环。有了这个习惯后面综合遇到的坑少一半。1.2 可综合语法与不可综合语法的边界Verilog语法说多不多说少不少但真正能被综合工具接受的是一个子集。很多人初学Verilog时喜欢在代码里写#10延时、写initial、写文件读取这些在仿真里完全没问题但综合工具根本不认识它们综合时会直接忽略或者报错。我整理了一个常用语法对照平时查起来很省事语法可综合典型用途说明assign是组合逻辑赋值连续赋值语句综合为逻辑门always(posedge clk)是时序逻辑综合为触发器always(*) 或 always(a or b)是组合逻辑注意必须写全所有分支否则生成锁存器if / case是选择逻辑组合、时序均可但要写完整for循环是展开成逻辑必须是常数循环次数不能动态改变parameter / localparam是参数化设计综合时可直接替换常量module例化是层次化设计没问题function部分组合逻辑复用不能包含时序控制task有限支持仿真为主可综合的task极其受限不建议在RTL里用initial否仿真初始化综合工具一般忽略或报错#延时否仿真时序控制综合时会忽略force / release否仿真调试仅仿真fork / join否仿真并行仅仿真文件读写否仿真激励仅仿真表格里有一条关键经验在RTL设计里写代码目标是让工具能映射到实际电路在Testbench里写代码目标是让仿真贴近真实行为。两者思路完全不同。我见过最典型的问题是有人把时序控制的习惯带进RTL写出来的代码仿真时功能是对的一旦进综合工具就被“打回原形”功能全变样。1.3 从需求到模块划分的推演过程以滑动窗口滤波器为例需求其实不复杂输入一组连续的采样数据每来一个新数据就输出最近N个数据的平均值。要做到这个功能得先想清楚三件事窗口多大、数据位宽多少、输出精度要几位。假设窗口大小W8输入数据data_in是12位无符号数我们希望输出也是12位。滑动窗口的实现思路是维护一个数据队列新数据写入队列头旧数据从队列尾丢出去同时用一个累加器保持窗口内所有数据的和。每进来一个新数据累加器加上新数据减去丢掉的旧数据然后输出累加器除以窗口大小的平均值。这样设计的好处非常明显不管窗口多大每次只需要做一次加法和一次减法不需要在每次输出时把窗口里所有数据都重新加一遍省下大量功耗和面积。这是一个非常经典的“用面积换时间、用算法换资源”的思路。模块划分上我倾向于拆成三块一是数据缓冲模块负责维护滑动窗口和生成“待挤出的旧数据”二是累加求模模块负责更新累加器并完成除法三是输出时序控制模块负责在正确的时钟周期输出有效数据和有效标志。模块之间用清晰的接口信号连接这样单点调试也方便逻辑综合时层次结构也更清楚。2. 核心细节解析高频语法点与工程规范2.1 时钟与复位先把时序骨架立起来写可综合的时序逻辑之前第一件事是把时钟和复位设计想清楚。时序逻辑的骨架就是时钟沿驱动的寄存器任何一个always(posedge clk)块里的变量综合以后都会变成一组触发器。能不能满足建立时间、保持时间直接决定这组触发器在真实芯片上能不能稳定工作。复位方式的选择是个工程权衡。同步复位的好处是复位信号不要求“即时到位”只要在时钟沿前满足建立时间即可对后端实现比较友好缺点是一旦复位信号有毛刺可能会被错误采样所以需要复位同步器来滤除毛刺。异步复位不依赖时钟响应快但容易产生复位释放时的亚稳态问题必须做“异步复位、同步释放”处理。做ASIC流片的项目我通常选用异步复位、同步释放FPGA上则可以根据厂商标IP的习惯灵活选择。赋值方式更是写时序代码的核心考点。在时序逻辑的always块里必须用非阻塞赋值“”在组合逻辑的always块里必须用阻塞赋值“”。这个原则不是规定出来吓唬人的而是模拟事件队列机制决定的非阻塞赋值在时间步结束时统一更新能保证多个触发器在同一时钟沿一起采样更新不会出现“前一个赋值影响后一个逻辑”的诡异现象。我踩过最惨的坑就是混合使用赋值导致仿真和硬件行为完全不一致。写代码时我甚至会把这条写在工程规范第一页允许用阻塞赋值的地方只有组合always和assign。计数器是时序逻辑里最基础的构件。一个参数化递增计数器可以这样写module counter #( parameter WIDTH 8 )( input wire clk, input wire rst_n, input wire en, output reg [WIDTH-1:0] count ); always (posedge clk or negedge rst_n) begin if (!rst_n) count {WIDTH{1b0}}; else if (en) count count 1b1; end endmodule注意复位用的是异步复位敏感列表里需要写上rst_n。很多综合工具检查敏感列表是否完整写漏信号轻则警告重则综合出来的仿真行为不一致。计数器位宽选择建议用参数直接驱动不要写死后面扩展采样窗口或者改分频系数时一劳永逸。2.2 parameter与localparam让设计保持弹性parameter是Verilog里实现参数化设计最常用的手段。它可以在模块例化时被外部覆盖所以适合做“对外接口式”的全局参数比如数据位宽、窗口大小、状态编码宽度。localparam则是本地常量不能被外部覆盖适合做模块内部的状态编码、位宽计算、常量定义。我经常使用localparam来定义状态机状态localparam IDLE 3d0; localparam READ_DATA 3d1; localparam CALC_AVG 3d2; localparam OUTPUT 3d3;这样改状态编码只需要动一处综合时工具也会做状态的编码优化。至于parameter和localparam的命名建议用大写字母加下划线语义清晰避免魔法数字散落在代码里。参数递增是工程里的高频操作。比如窗口大小从8改成16如果累加器位宽是手写固定的就会产生溢出风险。这时候一定要用参数化位宽推导。Verilog-2001里可以用$clog2函数计算以2为底的对数上取整方便算出表示计数值所需的位宽localparam SUM_W DATA_W $clog2(WINDOW_SIZE);我习惯在模块开头把所有相关位宽统一用localparam定义好这样后面所有寄存器和连线声明都可以直接用localparam避免“参数改了位宽忘改”这种低级但致命的问题。2.3 模块例化、task与function的工程取舍模块例化是Verilog组织层次结构的核心方式。推荐使用命名端口例化虽然写起来啰嗦但可读性和可维护性远好于按顺序端口例化。命名例化一旦端口调整引用处改动极小顺序例化稍微动一下端口顺序所有例化点全部罢工查错体验极差。counter #(.WIDTH(12)) u_sample_cnt ( .clk (clk), .rst_n (rst_n), .en (sample_en), .count (sample_cnt) );task和function的取舍是我看代码时特别关注的细节。function在可综合RTL里可以做小的组合逻辑复用比如位宽转换、加法进位处理。task在RTL里我基本不用因为可综合的task限制太多不能包含时序控制不能有耗时行为贸然使用会产生不可预测的结果。真正适合用task的场景是testbench波形激励的重复序列比如读一次传感器、配置一组寄存器之类的操作用task封装起来会让测试代码清爽不少。2.4 状态机写法三段式是工程底线状态机是数字电路设计的灵魂尤其对于I2C、OLED、QSPI这类接口控制类项目。我推荐使用三段式状态机写法第一段负责状态寄存器的时序更新第二段用组合逻辑计算下一状态第三段用时序逻辑输出结果。三段拆开的好处是状态跳转逻辑和输出逻辑分离代码清晰综合时也容易优化。// 第一段状态寄存器 always (posedge clk or negedge rst_n) begin if (!rst_n) state IDLE; else state next_state; end // 第二段次态组合逻辑 always (*) begin next_state state; case (state) IDLE: if (start) next_state READ_DATA; READ_DATA: if (done) next_state OUTPUT; default: next_state IDLE; endcase end // 第三段输出时序逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) data_out 0; else if (state OUTPUT) data_out data_in; end这里有一个特别容易踩的坑组合逻辑里case分支写不全或者忽略default就可能在输出端口上生成锁存器。锁存器在综合报告里会出现“Inferred Latch”警告这些东西在时序分析、扫描链插入、DFT阶段都是麻烦制造者。所以组合逻辑case能写default就写default同一次态赋值覆盖所有路径这是避免意外锁存器的不二法门。3. 完整实例实操滑动窗口滤波器的Verilog实现与仿真3.1 滑动窗口滤波原理与项目需求落地滑动窗口滤波一个典型应用是传感器数据预处理。比如ADC采集到一组带噪声的电压值通过滑动平均把高频毛刺平滑掉让后续的阈值判断更稳定。这和“取N个数据求平均值”的区别在于窗口是动态推进的来一个新数据就滚动一次任何时刻输出的都是最近N个数据的平均值。这只做一个首尾更新式的设计。窗口内部用一个移位寄存器随时保存最近N个数据。新数据进来时它写入移位寄存器的头部同时尾部那个“最老”的数据要被挤出去。累加器的更新公式很简单sum sum new_data - old_data输出平均值就是 sum / N。这里除数是常数N在硬件上可以直接用右移实现N为2的幂次或者用常数除法器优化比通用除法器省资源。位宽推演是关键一步。假设数据位宽DATA_W12窗口大小N8累加器需要保存8个12位数的和最大值为255*82040用12位不够需要至少 log2(8)12 15位。我用localparam把累加器位宽算好避免溢出。3.2 顶层RTL实现完整的可综合代码下面给出完整代码这个代码我实测过直接用Icarus Verilog仿真没问题也能正常进入综合流程。// // 滑动窗口均值滤波器 // 功能每输入一个采样值输出最近N个采样值的平均值 // N8输入数据位宽12位 // module sliding_window_filter #( parameter DATA_W 12, parameter WIN_W 8 )( input wire clk, // 工作时钟 input wire rst_n, // 异步复位低有效 input wire data_valid, // 输入数据有效标志 input wire [DATA_W-1:0] data_in, // 输入采样数据 output reg [DATA_W-1:0] data_out, // 滤波输出 output reg out_valid // 输出有效标志 ); // 累加器位宽 输入位宽 窗口宽度位宽 localparam SUM_W DATA_W $clog2(WIN_W); // 滑动窗口寄存器保存最近N个数据 reg [DATA_W-1:0] window [0:WIN_W-1]; // 累加器与计数变量 reg [SUM_W-1:0] sum; reg [$clog2(WIN_W)-1:0] wr_ptr; reg [$clog2(WIN_W)-1:0] cnt; wire [DATA_W-1:0] old_data; // 被挤出的旧数据 assign old_data window[wr_ptr]; // 数据写入使用循环指针覆盖最旧数据 integer i; always (posedge clk or negedge rst_n) begin if (!rst_n) begin for (i 0; i WIN_W; i i 1) window[i] {DATA_W{1b0}}; wr_ptr 0; cnt 0; sum 0; data_out 0; out_valid 0; end else if (data_valid) begin // 更新窗口写入新数据覆盖最旧数据 window[wr_ptr] data_in; // 更新累加器加上新数据减去旧数据 sum sum data_in - window[wr_ptr]; // 窗口未填满时只加不减指针循环移动 if (cnt WIN_W) cnt cnt 1b1; wr_ptr wr_ptr 1b1; end end // 输出控制窗口填满后每个有效周期输出一次均值 wire full_win (cnt WIN_W); reg out_d; always (posedge clk or negedge rst_n) begin if (!rst_n) begin data_out 0; out_valid 0; out_d 0; end else begin out_d data_valid; // 上一个周期刚写入数据且窗口已满本周期可安全输出 if (out_d full_win) begin // 注意sum已经在写入数据时更新完毕 data_out sum[DATA_W $clog2(WIN_W)-1 -: DATA_W]; out_valid 1b1; end else begin out_valid 1b0; end end end endmodule代码里有几个地方值得展开说明。一是滑动窗口的存储方式。我用了一个循环指针wr_ptr指向“当前要被覆盖的最旧数据”而不是用移位寄存器做物理搬移。这样每次写入只需要写一个地址不会产生整个窗口数据的搬运动作综合后资源也少。电路行为上等价于FIFO只是手动管理指针。二是累加器更新时的减法操作。sum data_in - window[wr_ptr]这里的window[wr_ptr]是“旧值”还是“新值”容易混淆。注意非阻塞赋值的特性等号右边的window[wr_ptr]在时间步结束时才会被更新所以它读到的依然是被覆盖前的旧数据减法减的正是该被挤出的数据。这就是非阻塞赋值的精妙之处也是我前面强调坚持用的原因。三是输出位宽的重定位切取。sum[SUM_W-1 -: DATA_W]这种写法在Verilog-2001里是从高往下截取DATA_W位。因为sum是15位而data_out是12位直接取低12位在窗口最大值时会丢掉高位信息所以我选择了比较稳妥的高位截取保留滤波输出的主要信息想得到更精确的小数部分可以扩展输出位宽再截位。四是最容易出错的“输出时序”。如果data_valid和窗口满标志同时组合判断可能由于组合逻辑和时序逻辑的先后关系产生一拍的错位。我增加了一个out_d打拍寄存器先记住上一个周期有没有有效数据写入下一拍再判断窗口是否已满来输出这就保证了输出数据对应的是写入之后更新完毕的那一版累加和。这种实现细节工程上非常常见。3.3 使用Icarus Verilog仿真验证写完了RTL接下来必须仿真千万别直接上板。开源世界最常用的仿真工具是Icarus Verilog命令行工具叫iverilog配合GTKWave可以看波形。用Icarus做功能仿真非常方便安装很简单sudo apt update sudo apt install iverilog gtkwave然后写一个测试激励文件模拟连续输入一组数据并检查输出是否是滑动平均timescale 1ns/1ps module tb_sliding_window_filter; reg clk, rst_n, data_valid; reg [11:0] data_in; wire [11:0] data_out; wire out_valid; sliding_window_filter #(.DATA_W(12), .WIN_W(8)) dut ( .clk(clk), .rst_n(rst_n), .data_valid(data_valid), .data_in(data_in), .data_out(data_out), .out_valid(out_valid) ); always #5 clk ~clk; initial begin clk 0; rst_n 0; data_valid 0; data_in 0; #20 rst_n 1; // 输入10个数据 #10 data_valid 1; repeat(10) begin data_in data_in 8d25; #10; end data_valid 0; #50; $finish; end initial begin $dumpfile(filter_tb.vcd); $dumpvars(0, tb_sliding_window_filter); end endmodule仿真命令很简单iverilog -o sim_tb tb_sliding_window_filter.v sliding_window_filter.v vvp sim_tb gtkwave filter_tb.vcd仿真通过不代表就能综合成功只能证明功能行为符合设计预期。我见过不少人仿真波形非常漂亮结果综合或上板后一团糟原因往往出在代码里存在不可综合语句或者敏感列表、赋值规范出了问题。所以仿真和综合必须配合仿真解决“逻辑对不对”综合解决“电路能不能实现”。3.4 接口类项目的设计方法迁移热搜词里有很多I2C读EEPROM、QSPI读写Flash、OLED控制这类项目。它们看起来五花八门其实本质都是同一个套路一个主状态机加若干移位寄存器外加满足协议时序的精确计数器控制。比如I2C写入EEPROM核心是SCL时钟产生、SDA数据位按协议输出、ACK检测三个阶段不停地循环QSPI读写Flash多出来的不过是更宽的位宽和更复杂的命令解析。我的建议是先在纸上把协议时序图画清楚然后用状态机把每一个时序阶段映射成状态最后再落到Verilog上。不要一上来就写代码协议里面的边沿时刻、建立保持时间要求直接用parameter定义成周期数能够省下大量调试时间。这和上面滑动窗口滤波的实现流程是一模一样的算法思路先行接口时序理清最后才是编码。4. 逻辑综合实操从RTL到门级网表的完整流程4.1 综合工具与输入准备逻辑综合的圈子基本被Synopsys Design Compiler垄断它也是业内事实标准很多芯片公司流片前的综合工作就是用DC跑出来的。FPGA项目则常用厂商自带的综合工具比如Xilinx Vivado的Synth、Intel Quartus的Analysis Synthesis或者Synopsys Synplify等。开源领域还有Yosys对教学和中小规模设计非常友好。不管是哪个工具综合流程的输入要素基本一致RTL源码、标准单元库/FPGA器件的工艺信息、约束文件SDC。SDC文件是综合的灵魂它告诉工具时钟频率多少、输入信号什么时候到达、输出负载多大。没有约束工具只能瞎猜综合结果也谈不上可靠。我见过不少项目就是SDC写得不够完整导致综合后时序乱飞。以DC为例准备一个简单的综合脚本大概长这样# 指定工艺库 set target_library slow.db set link_library * $target_library # 读入RTL read_verilog sliding_window_filter.v current_design sliding_window_filter # 时序约束 create_clock -period 10 -name clk [get_ports clk] set_clock_uncertainty 0.2 -setup [get_clocks clk] set_clock_uncertainty 0.3 -hold [get_clocks clk] set_input_delay 2.0 -clock clk [all_inputs] set_output_delay 2.0 -clock clk [all_outputs] # 综合优化 compile_ultra # 输出 write -format verilog -hierarchy -output filter_netlist.v write_sdc filter_sdc.sdc report_timing -max_paths 10 report_area逐条解释一下。create_clock定义了一个10ns周期即100MHz的时钟约束set_clock_uncertainty给时钟留出抖动余量set_input_delay和set_output_delay约束了外部信号相对时钟沿的到达时间。编译完成后write命令把门级网表写出来report_timing和report_area分别报告时序与面积结果。这些约束不是凭空拍的而是来自芯片外部接口的实际时序要求。比如上游模块的Tco是2ns输入的延迟就得至少按这个值来约束。约束紧了综合工具拼命优化但时序可能仍然违反约束松了后级电路可能真正出问题。SDC写得越贴近真实环境综合结果越可信。4.2 综合脚本与约束实现思路DC脚本里我最看重的是约束的完备性。很多新手只是create_clock完事结果综合工具对输入输出端口完全不设防默认理想化处理生成的网表到了后仿真阶段各种时序崩盘。所以input delay和output delay必须结合上下级模块的实际接口延迟填写。另一个容易忽略的是异步复位信号的约束。异步复位端口一般需要set_false_path约束否则工具会把它当普通时序路径做分析导致多余的违例报告。同时跨时钟域的异步信号也建议set_false_path或者用同步器隔离。良好约束的核心是告诉工具哪些时序路径真实存在、需要收敛哪些是“假路径”不需要浪费时间。综合报告里的关键指标有三个WNS最差负时序余量、TNS总负时序余量、面积。WNS如果为负说明存在时序违例需要寻找关键路径并优化。面积则直接和芯片成本挂钩在同样能满足时序的前提下面积越小越好。我一般在综合后先看report_timing里的关键路径延时报告再决定是优化RTL还是调整约束。4.3 时序与面积优化从综合报告看问题如果综合报告显示时序违例常见手段有三个。一是插入流水寄存器把长组合路径打短代价是多了一级延迟但各段的组合复杂度下降工作频率可以提上来。二是把复杂的乘除法改写为移位和加法组合比如乘以3改写为xx1工具对加法树的优化效率远好于通用硬件乘法器。三是调整逻辑结构比如大扇出的信号做复制寄存器降低单个输出端的负载。面积优化和时序优化一般是对立的。想跑更高频率就要付出更多寄存器和逻辑门的代价面积增大。做项目时我会先跑一版宽松约束看看面积基线再逐步缩紧时序约束观察面积能增加多少。如果面积增幅过大就得回头审视架构设计是不是存在大量冗余逻辑。综合不是一次过的步骤它是一个反复迭代收敛的过程。5. 常见问题与排查技巧实录5.1 仿真正确但综合后行为不一致这个现象几乎每个工程师都遇到过。总结下来最常见的原因有三个一是敏感列表不完整比如组合逻辑块漏写了某个输入信号仿真是按你写的敏感列表触发的综合工具是不会理你漏没漏的它直接综合完整逻辑结果仿真和实现不一致二是RTL里写了不可综合语句比如初始值或惯性延迟仿真时能工作综合后这些行为全部消失三是组合逻辑和时序逻辑混用了阻塞赋值与非阻塞赋值导致多周期行为不一致。排查这套问题我的经验是先看综合日志里的警告尤其是Latch推断、Multi-driver、Sensitivity list incomplete这些关键词然后对照RTL逐个排查。综合工具给出来的警告没有一条是废话每条背后都藏着一个具体的硬件行为偏差。5.2 综合时出现Latch警告怎么定位Latch警告总让我很紧张。锁存器在时序电路里并不是完全不能用但工程上绝大多数Latch都是意外产生的比如case分支没有default、组合逻辑的if没有else或者赋值条件覆盖不全。意外产生的Latch会成为时序分析和DFT测试的风险点必须根除。定位方法很简单把综合工具报出的“Inferred Latch”信号名记下来回到RTL搜索这些信号的赋值逻辑检查所有条件分支是否完整。如果你在组合逻辑always块里发现某个分支下没有给该变量赋值那基本就是它了。修复就是在default分支或者else分支里显式赋值实在不行就把组合逻辑改成时序逻辑输出寄存器一了百了。5.3 时序余量不足时的三种实用修法第一招是加流水寄存器把长组合路径切开。这招对乘法器、比较器等大组合逻辑尤其有效代价是输出会有几拍延迟需要调整后续模块的时序对齐。第二招是逻辑重构比如把一条大的case改成两级并行优先级编码或者把宽位比较器拆成分段比较再加总条件减少单路径上的级数。第三招是寄存器复制降低扇出一个寄存器驱动太多负载时时序会变得很难看复制出几个同信号的寄存器分散负载能立竿见影。修时序是一个反复试错的过程。我一般会保存每一版修改前后的综合报告对比关键路径的位置、延时和WNS变化这样才能知道哪一次修改真正有效而不是瞎调一顿。5.4 常见问题速查表症状可能原因解决建议仿真波形和上板行为不一致复位未做同步释放存在亚稳态添加复位同步器做到异步复位、同步释放组合逻辑输出毛刺组合逻辑未加输出寄存器关键输出打一拍交给时序逻辑输出状态机跑飞回不到初态状态编码缺default分支case补default并加入非法状态复位综合报告出现Latch警告case/if分支覆盖不全补全所有赋值分支显式default时序违例WNS为负无输入/输出延迟约束路径过长完善SDC检查关键路径并插入流水面积比预期大很多逻辑冗余或资源共享不充分在综合脚本开启资源共享与优化选项data_valid期间数据突然丢失时序打拍对齐错误利用打拍寄存器统一数据与标志的对齐关系Icarus仿真时波形为空忘了dumpfiles或模块名写错检查testbench中$dumpvars的层次名注意全路径例化名表格里第三行是我最想强调的一条。很多项目在FPGA验证阶段遇到状态机跑飞第一反应是加隔离等不靠谱的补丁其实只要在状态寄存器复位后强制进入IDLE并且每段case都补default就能把绝大多数跑飞问题根治掉。在这个领域摸爬滚打这些年我最大的体会是Verilog写得好的人往往不是会花哨语法最多的人而是对电路本质理解最透彻的人。每个信号背后都是实实在在的连线和逻辑门每个时序逻辑都是随时钟沿更新的寄存器组。写代码时多问自己一句“综合成什么电路”很多问题在设计阶段就能提前消化。如果你看完这篇内容正学着写自己的第一个RTL项目我的建议很简单先把这个小滤波器跑通仿真再试着写SDC跑一遍综合把每一个警告都查清楚再继续下一个项目。这个闭环走通一次之后看I2C、QSPI、OLED控制这类项目你会发现自己已经能一眼看到状态机骨架和时序约束的关键点了。