尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Verilog核心:用代码描述电路与信号流动的工程实践

Verilog核心:用代码描述电路与信号流动的工程实践 写Verilog这事情我这些年算是又爱又恨。爱的是它真的能让你用代码“画出”一块会跑起来的电路恨的是很多人一开始用写C语言的思维去写Verilog最后仿真跑不过、综合出问题、上板瞎灯黑火完全不知道问题出在哪。其实Verilog的底层逻辑就两件事描述电路和描述信号的流动。把这两件事想透了后面写什么模块、调什么时序都会顺很多。这篇博客我就围绕这两条主线把自己从入门到做项目的理解和踩过的坑完整地梳理一遍。作为一篇给不同基础读者看的经验总结我不会只堆语法而是会讲清楚每一段代码背后对应什么样的硬件结构信号是怎么在模块之间、时钟沿之间流过去的。对于刚入门的朋友这里面有从零开始的计数器、状态机和testbench。对于有点经验的同学后面有SPI、I2C、UART这些真实接口的写法拆解还有跨时钟域、阻塞非阻塞赋值这类高频面试八股背后的工程本质。用一句老话说Verilog入门容易想写出能综合、能上板、能稳定跑的代码你得把“描述电路”四个字刻在脑子里。1. 先搞清楚一件事Verilog到底在“描述”什么1.1 电路不是“画”出来的是“描述”出来的我最早接触Verilog的时候总以为它和C语言差不多都是按顺序执行的程序。后来在职场上被带我的老工程师一句话点醒“你写的不是程序是在用文字画电路图。每一个always、每一个assign最终都会变成真实的逻辑门和触发器。”这句话怎么理解呢举个生活化的例子。你去餐厅吃饭菜单上写着“红烧肉”这是厨师眼里的一道菜。但如果你要精确到每一步怎么做你得写出“切块、焯水、炒糖色、加料、小火炖40分钟”。Verilog就是后面这种写法只不过它描述的是一块数字电路内部的连接关系和行为方式。具体来说Verilog描述电路有三种层次行为级描述用always块、if/else、case这些语法描述电路“在什么条件下做什么事”不考虑具体用什么门实现。数据流描述用assign连续赋值语句描述信号之间的组合逻辑关系。结构级描述用模块实例化module instantiation的方式把一个设计拆成一个个子模块然后用wire连起来像搭积木一样搭出整个系统。这三种层次不是互斥的实际工程里往往混着用。顶层多用结构描述底层功能模块多用行为描述。理解这个分层逻辑你在看别人代码的时候就容易抓住脉络不会一头扎进一堆always里出不来。1.2 硬件描述与软件编程的分水岭——并行性写Verilog和写C语言最大的区别是什么并行性。C语言里代码从上往下一条一条执行前面没跑完后面不会开始。Verilog不一样它描述的是硬件上真实存在的多个电路块这些块只要有输入变化就同时开始计算互不等待。举个例子。你有两个always块一个负责计数一个负责生成使能信号。在C语言里你得考虑这两个函数谁先被调用。在Verilog里这两个always块就相当于电路板上两个独立的电路时钟沿一到它们同时被触发同时更新各自的寄存器。这个特性对新手来说既是福音也是噩梦。福音是因为你终于可以理解为什么CPU里能同时做那么多事噩梦是因为你一旦用软件思维去写就会写出“这个变量我上一条语句赋了值下一条应该就能用”的错误代码。我还经常用一个类比帮助同事理解C语言像一个人排着队做事Verilog像一群人同时开工每个人只管自己手里的一摊活互相之间通过信号线wire传递结果。信号流动这个概念就是在描述这一群人之间的协作关系。2. 信号的流动把数据路径在代码里“翻译”出来2.1 wire与reg两条不同的“管道”在Verilog里数据从模块A传到模块B从组合逻辑传到寄存器都需要一个载体。这个载体最常见的两种类型就是wire和reg。很多初学者把wire当成“变量”把reg当成“只能存值的寄存器”。这个理解不能算错但容易导致一个误区以为reg变量综合出来一定是一个触发器。实际上reg只是语法上的概念它表示变量在过程赋值语句always/initial中被赋值。如果你在组合逻辑的always块里用reg变量写纯组合逻辑综合出来它就是一个wire对应的电路不会有存储功能。把含义理清楚之后选型的规则就很简单了wire用assign赋值或者连接子模块的输出端口。它就像一个物理导线信号到了就流过去没有存储能力。reg在always块里赋值通常在时钟沿或者敏感信号变化时更新。它更像一个“小盒子”能记住上一次塞进去的值。实际工程里写代码的时候不用死记语言规范记住一个实践经验就行assign右边和always块里被赋值的信号分别用wire和reg声明。至于它是真寄存器还是纯组合逻辑那是综合工具帮你决定的事。2.2 连续赋值与过程赋值两种“流动方式”信号的流动方式对应着两种核心语句。assign连续赋值描述的是组合逻辑。它的特点是只要右边信号发生变化左边的wire立即更新没有延迟没有时钟参与。比如assign sum a b;这是一条永不休息的加法电路连接a和b一变sum立刻跟着变。就像水管只要源头有水输出端就一直有水不需要任何人开关。always过程赋值描述的是需要时序控制的逻辑。它可以对reg变量赋值并且按需指定触发条件。最典型的是时序逻辑在时钟上升沿更新寄存器always (posedge clk) begin count count 1b1; end这里的count不是“一直等于”count加1而是“每当时钟到来瞬间把当前值更新为原来的值加1”。理解这句话是理解Verilog信号流动的关键。信号在电路里流动时有两种完全不同的节奏。组合逻辑是自由流动、不限速时序逻辑是到点停靠、按节拍更新。所以我在写代码时会先在草稿纸上画一画这个信号是“一直通着的”还是“时钟沿触发更新的”。画清楚之后再用assign或者always去写基本不太会写出风格混乱的代码。2.3 阻塞赋值与非阻塞赋值这是最容易翻车的地方这个话题在很多面试八股里都会出现但真正写代码的时候很多人照样翻车。我就直接给结论和理由。时序逻辑时钟沿触发里用非阻塞赋值。组合逻辑电平敏感里用阻塞赋值。原因是非阻塞赋值在同一时刻给所有寄存器“排好队统一发车”。时钟上升沿到来时所有右边先被采样然后再统一更新左边的值。这样做的好处是多个reg变量的更新不会互相覆盖正好符合硬件上多个触发器同时工作的特性。阻塞赋值则是立即生效先赋值后使用。在组合逻辑里这符合信号一级一级传递的特性但如果在时序逻辑里用了可能会出现“用新值算下一个值”的问题综合出来往往和预期电路不一致。我见过一个真实案例某同事在状态机跳转时用了阻塞赋值仿真时功能偶尔对、偶尔错排查了一上午才发现是赋值方式的问题。所以这条规则一定要刻进DNA里尤其在写时序逻辑的时候养成敲的条件反射。3. 从零写一个计数器一个模块同时体现“描述电路”和“描述信号的流动”3.1 参数化计数器的完整代码与逐行拆解计数器的Verilog实现题目看起来简单但你把它吃透了基本就掌握了模块写法、参数化、时序逻辑、复位方式、使能控制这一整套基本功。我先贴一段完整的代码然后再逐行分析。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) begin count {WIDTH{1b0}}; end else if (en) begin count count 1b1; end end endmodule这个模块做的事情每次时钟上升沿到来如果复位有效计数器清零如果复位移除且使能有效计数器加1。输出的count是一个WIDTH位宽的寄存器默认8位。逐行看背后的电路含义parameter WIDTH 8模块参数化。后续例化时可以通过参数改变位宽例如做一个16位计数器只需要在例化时传参不需要复制修改代码。input wire clk和input wire rst_n时钟和异步复位。rst_n带一个_n后缀表示低电平有效这是业界习惯写法。output reg [WIDTH-1:0] countcount是一个寄存器输出在always块中赋值所以声明为reg。always (posedge clk or negedge rst_n)敏感列表包含时钟上升沿和复位下降沿因此复位是异步复位。也就是说rst_n一拉低不管时钟什么状态count立刻清零。count count 1b1非阻塞赋值时序逻辑的标准写法。这条语句综合出来就是一排加法器加一排D触发器。3.2 信号在这个模块里是怎么“流动”的这个简单模块其实已经把“描述信号的流动”这四个字演绎得很完整了。你可以想象一下电路板上的物理景象时钟信号clk像一位严格的教练每喊一次“到点”所有D触发器同时看一眼自己的输入。rst_n拉低时每个触发器的复位端生效count寄存器被强制清零。rst_n释放后en信号为高加法器把当前count值加1送到D触发器的D端等待下一个时钟沿把它采进去。如果en为低加法器结果等于原值寄存器的值就不变。换句话说使能信号控制的是“新值是否被写入”而不是“电路是否还在工作”——触发器一直工作只是输入保持原样。这里的能力链就是clk → always块触发 → 判断复位和使能 → 更新count寄存器 → 输出去往下游模块。这就是一次完整的信号流动过程。你在调试任何Verilog模块时都可以用这种“信号从哪里来、经过什么逻辑、在哪个时钟沿落进寄存器、又输出到哪里去”的方式去追踪。3.3 计数器在项目中的三个典型应用场景计数器写完了但它的价值体现在应用场景里。我在实际项目中至少用过以下几种一是分频器。假设系统时钟是50MHz需要产生一个1Hz的闪烁信号。可以设计一个25位的计数器当计数到24_999_999时产生一个脉冲折半翻转输出。注意这里比直接计数到2倍频率再翻转要常见因为脉冲宽度刚好是一个时钟周期便于后续同步。二是定时器。在状态机里经常需要“等待N个时钟周期”的逻辑。可以启动一个计数器每时钟周期计1计到设定值后置flag。例如I2C的延时、按键消抖的20ms延时都靠这种方法实现。三是FIFO指针计数。在实现FIFO读写时计数器用来记录当前缓冲区里有多少数据。这时候要特别注意读写同时发生的情况读使能置1且写使能置1时计数器应该保持因为“进一个出一个总量不变”。这个细节在面试里经常被问到写代码时也容易漏。计数器虽小但它串联起了时序控制、使能分析和边界判断是理解“信号流动”最直觉的入口。4. 状态机把“控制信号”的流动编写成一张时空图4.1 为什么不能省掉状态机计数器解决的是“数量”问题而状态机解决的是“顺序”问题。实际项目里比如UART接收、SPI读写、I2C时序都需要按照严格的先后顺序来产生和响应信号先做A再做B等条件满足了再做C。如果用一堆if-else嵌套去实现代码会变得一团糟而且时序上很容易出错。状态机的本质就是把这套顺序逻辑显式地画成一张状态转移图然后用代码描述出来。它管理的是“控制信号的流动”从哪个状态出发在什么条件下流向下一个状态每个状态下产生什么输出信号。我强烈建议所有写Verilog的人在设计状态机之前先画状态转移图哪怕只是画在草稿纸上。直接上手写代码、边写边想的人大概率会在状态漏判、输出时序这些地方翻车。4.2 三段式状态机的标准写法经过这些年的项目实践我推荐大家直接使用三段式状态机。三段式并不是复杂度的增加而是把状态转移、状态寄存和输出逻辑分开了每段职责单一出问题的时候一目了然。localparam IDLE 2d0, DATA 2d1, DONE 2d2; reg [1:0] state; reg [1:0] next_state; // 第一段状态寄存器时序逻辑 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 DATA; DATA: if (count 3d7) next_state DONE; DONE: next_state IDLE; default: next_state IDLE; endcase end // 第三段输出逻辑组合逻辑也可以用时序逻辑改出寄存器输出版本 always (*) begin idle_flag 1b0; done_flag 1b0; case (state) IDLE: idle_flag 1b1; DONE: done_flag 1b1; default:; endcase end第二段组合逻辑里默认next_state state;非常重要它的作用是让没有明确跳转条件时保持当前状态避免综合出意想不到的锁存器或者丢失状态。第三段输出逻辑如果对时序要求高可以改为时序逻辑输出再打一拍这样能更好地避免毛刺代价是输出晚一个时钟周期。两种写法在实际项目中都存在需要根据下游模块的时序要求选择。一段式、二段式和三段式怎么选我给个比较直接的对比写法优点缺点适用场景一段式代码短适合很简单的状态状态寄存、输出逻辑混在一起后期难维护输出时延不好控制超小型控制逻辑二段式状态与输出分离结构较清晰组合逻辑输出容易有毛刺对输出时序要求不高的场景三段式结构清晰输出可寄存器化时序稳定代码量偏多推荐工程中使用在实际项目中我基本无脑选三段式原因无他排查问题的速度比少写几行代码重要得多。4.3 流水线另一种“信号流动”的组织方式状态机适合控制信号的串行流动而流水线适合数据信号的高吞吐流动。它把一个复杂的数据处理过程切分成多个阶段每个阶段占用一个时钟周期数据像流水一样从上一个寄存器流到下一个寄存器每过一个周期整体前进一步。举个例子一个三级流水线的数据通路always (posedge clk or negedge rst_n) begin if (!rst_n) begin stage1_reg d0; stage2_reg d0; stage3_reg d0; end else begin stage1_reg data_in; stage2_reg stage1_reg coef; stage3_reg stage2_reg * gain; end end这段代码里data_in经过三级寄存器每一级之间只隔一个时钟周期。第一级进来什么第二级下一个周期才能看到第三级再下一个周期。虽然单个数据从输入到输出需要3个时钟周期latency但系统可以每一拍都接收新数据达到每周期处理一个数据点的吞吐率。这就是流水线的核心思想用延迟换取吞吐量。我们在做基于FPGA的FIR滤波器时就会把乘累加拆成“乘法-累加-截位”几个流水级否则组合逻辑延迟会超过一个时钟周期导致时序约束跑不过。这里顺便提一句热搜里提到的“分布式算法FIR”也是类似思路它把乘累加处理转换成查找表和移位相加流水线本质上都是在精细地组织信号的流动节奏。状态机适合“什么时候做什么事”流水线适合“每一个节拍都在做同一件事的不同阶段”。两者搭配使用是数字系统设计里最常见的信号流动组织方式。5. 真正的接口实战SPI、I2C、UART 中的信号流动5.1 SPI主机移位寄存器里的小宇宙SPI是目前嵌入式系统里最常用的串行接口之一四根线SCK、MOSI、MISO、CS。写一个SPI主机模块最能帮助你理解“数据如何逐位移出来”和“外部数据如何逐位移进去”这两个过程。SPI主机发送一字节的流程可以这样拆解把要发送的字节装入发送移位寄存器sreg。拉低CS开始一次传输。每个SCK时钟沿把sreg的最高位移到MOSI输出同时把MISO上的电平移到sreg的最低位实现“移出移入同步完成”。移位8次后一字节传输完成CS拉高标志位置位。对应的核心代码片段// SCK下降沿更新MOSI上升沿采样MISO是常见模式之一 always (posedge sck_negedge or negedge rst_n) begin if (!rst_n) sreg 8h00; else if (tx_valid (bit_cnt 3d0)) sreg tx_data; else if (bit_cnt 3d8) sreg {sreg[6:0], miso}; end这里sreg {sreg[6:0], miso}这个写法是整个SPI主机的灵魂。它把sreg左移一位空出来的最低位填入miso采到的值。每移动一次发送数据就少一位接收数据就多一位。8拍之后原来的发送数据全部移位出去了MISO上采到的8位信号全部移进了sreg里。SPI还涉及四种模式CPOL/CPHA本质上是决定SCK在空闲时的电平以及数据在哪种边沿采样。写代码前先确认从设备的模式不然波形完全对不上。这个我在项目里吃过亏调了半天发现主从机的CPHA设反了所有数据都错位。5.2 I2C读写EEPROM双向线的时序艺术I2C的难点在于SDA是双向线而且在SCL高电平时SDA不能变化只有在SCL低电平时才能变。起始条件是SCL高电平时SDA从高到低停止条件是SCL高电平时SDA从低到高。写I2C主机模块我的经验是把它拆成两层底层字节级模块负责产生START、STOP、发送1字节、接收1字节、发送ACK、检测ACK等原子操作。上层命令状态机负责组合这些原子操作形成完整的读写EEPROM时序比如“发送设备地址写位 → 发送寄存器地址 → 发送数据 → 停止”。上层状态机的转移条件基本就是底层模块完成标志位。比如发送完设备地址后等待ACK然后进入下一个状态。信号流动顺序是严格按照I2C协议来的任何一步出错SDA上的时序就乱了EEPROM就不响应。调试I2C的时候我最推荐的做法是直接抓波形图不要去猜。用逻辑分析仪或者直接在仿真波形里看SCL和SDA的相对关系很快就能定位问题是起始条件不对、ACK没等到还是数据位顺序反了。写I2C状态机时起始和停止条件的时序要画清楚再动笔这是最容易拖延工期的环节。5.3 UART收发波特率和采样点的博弈UART在社交软件的搜索里常年是热门因为它虽然简单但细节不少。我以接收为例讲解信号流动的关键。UART的接收线平时是高电平发送方拉低一个位时间表示起始位。接收端的核心任务是检测到起始位后在每一个数据位的中心点采样保证采到的是稳定电平而不是跳变沿附近的毛刺。这个过程在Verilog实现时通常需要一个波特率分频计数器。假设系统时钟50MHz波特率115200那么每个位时间对应434个时钟周期。实现方式是检测到RX从高到低跳变时启动计数器等待半个位时间217个周期后采样第一个数据位的中心之后每隔434个周期采样一位一共采8位数据再采停止位。对应的核心状态机片段IDLE: if (rx_negedge) begin bit_timer HALF_BIT; state START_SAMPLE; end START_SAMPLE: if (bit_timer_done) begin bit_timer BIT_PERIOD; bit_cnt 4d0; state DATA_SAMPLE; end DATA_SAMPLE: if (bit_timer_done) begin rx_shift {rx_shift[6:0], rx}; bit_cnt bit_cnt 1d1; if (bit_cnt 4d8) state STOP_SAMPLE; end这里接收数据的核心是把每次采样到的RX电平往移位寄存器里推采8次拼出一个字节。注意采样时刻的选择这是UART接收的关键工艺宁可稍微偏离中心也不能在跳变沿采样否则数据很容易采错。UART的发送相对简单核心就是按照波特率把要发送的字节逐位移到TX线上先拉低一个位时间作为起始位再依次发送8个数据位通常LSB first最后拉高作为停止位。理解了接收发送基本就是反过来。5.4 轮询仲裁器多个信号源抢总线时谁先走当系统里多个模块都要占用同一套总线或者同一个资源时就需要仲裁器。最简单的轮询仲裁器思路很直观多个请求方轮流获得授权避免某一个请求方一直霸占总线。实现时可以用一个优先级编码器循环更新优先级也可以用计数器轮转。我写过的一种方式是维护一个current_grant指针当前被授权的请求方如果没有请求了就跳到下一个有请求的模块如果还在请求就一直授权给它。仲裁器的信号流动本质是“请求信号→仲裁逻辑→授权信号→访存模块”。在拿FPGA实现cache或者DDR3控制器时仲裁器是绕不开的组件。等你理解了“多个信号源争用资源”的场景对信号的流动认识就会从“点对点”升维到“多对多调度”的层面。6. 让信号流动“看得见”仿真与调试方法6.1 Testbench的基本写法和用途Verilog写完之后你看到的只是文本。要真正看到信号怎么流动的必须做仿真。Testbench就是给设计模块搭的仿真环境它不属于综合范围只用于验证。一个标准的testbench包含这几部分时钟生成、复位生成、输入激励、例化被测模块、监控输出。下面是一个最简单的模板module tb_counter; reg clk; reg rst_n; reg en; wire [7:0] count; // 时钟生成周期20ns即50MHz initial clk 0; always #10 clk ~clk; // 复位与激励 initial begin rst_n 0; en 1; #30; rst_n 1; #200; en 0; #100; en 1; #500; $finish; end // 例化被测模块 counter #( .WIDTH(8) ) u_counter ( .clk (clk), .rst_n (rst_n), .en (en), .count (count) ); // 监控输出 initial begin $monitor(time%0t rst_n%b en%b count%0d, $time, rst_n, en, count); end endmoduletestbench的难点不在于语法而在于你怎么设计激励才能把边界情况覆盖到。比如计数器至少要看复位释放后能不能正常计数到溢出、使能拉低后能不能保持、复位脉冲能不能立刻清零。每写一个功能之前先在纸上列出“这个模块的输入有哪些合法和非法组合”比写一堆代码再去猜要高效得多。6.2 用Modelsim配合VSCode跑通一次完整仿真很多人在搜索“modelsim 如何仿真verilog文件”说明卡在了“写完代码怎么看到波形”这一步。我现在用的流程是这样用VSCode写Verilog代码安装Verilog插件后语法高亮和补齐都很方便。写完设计和testbench后打开Modelsim在命令行或者Tcl脚本里做三步操作vlib work创建库vlog编译设计文件和testbenchvsim启动仿真。在仿真窗口里添加需要观察的信号然后run -all就能看到波形。手动操作的完整命令大概是vlib work vlog counter.v tb_counter.v vsim -voptargsacc work.tb_counter add wave -hex /tb_counter/count run 1us跑通之后你就能在波形窗口里直观地看到时钟上升沿、复位释放时刻以及count每个时钟周期加1的变化过程。这是验证“信号流动”最直观的方式。我建议新人不要跳过这一步直接拿FPGA开发板去点灯那样很多时序问题会被掩盖排查起来更痛苦。6.3 仿真时最容易遇到的三个坑初值不确定。仿真默认wire是zreg是x。如果你在testbench里没给复位就去观测信号看到的会是红色未知态。这时第一件要检查的事情是复位信号有没有正确拉低和释放。仿真时间不够。计数器如果位宽很大需要几十万个周期才能看到效果但仿真跑了1us就停了看起来就像“卡住不动”。处理办法是把位宽缩小或者在testbench里做条件触发停止或者使用系统任务$finish在特定条件满足时结束仿真。采样周期和显示精度。$monitor这类监控任务在组合逻辑变化频繁时可能打印太多行影响观测。必要时直接用波形窗口看信号跳变比看文本输出直观得多。6.4 把仿真数据导出成文件跨工具分析有些场景下你需要把仿真产生的数据拿到其他工具里做进一步处理比如导出成十六进制文件再喂给Python画图或者做算法比对。这时候可以用Verilog自带的文件操作系统任务。integer fd; initial begin fd $fopen(output_data.txt, w); end always (posedge clk) begin if (data_valid) $fwrite(fd, %04x\n, data_out); end这套写法在调试视频图像数据、FIR滤波器输出时特别有用。我可以把FPGA仿真的滤波结果导出来和MatLab算出的理想波形放一起比对直接判断量化误差在不在可接受范围内。仿真数据导出这一步看起来简单但能极大提升验证效率不用每次都在波形上数点。7. 工程上的坑与经验从理论到能稳定跑的一段路程7.1 跨时钟域的信号流动亚稳态必须认真对待单时钟域的信号流动只要遵守时序逻辑规则基本不会出大问题。真正考验功力的是跨时钟域。比如系统里有两个时钟一个100MHz一个50MHz分别驱动两个模块。100MHz模块输出的信号直接进50MHz模块就可能采到正处于跳变过程中的信号导致亚稳态。解决办法是经典的“打两拍”同步器always (posedge clk_b or negedge rst_n) begin if (!rst_n) begin sync_reg1 1b0; sync_reg2 1b0; end else begin sync_reg1 signal_a; sync_reg2 sync_reg1; end endsync_reg2作为同步后的信号供clk_b域使用。打两拍不能完全消除亚稳态但可以把发生概率降到工程可接受的范围。对于频率较高的信号跨域单纯打两拍还不够需要用异步FIFO配合格雷码指针处理。热搜里出现过的“DDR3读写控制”“cache的Verilog实现”里跨时钟域处理就是核心难点之一。7.2 阻塞和非阻塞赋值混用的具体后果前面讲理论时已经提到过阻塞和非阻塞赋值这里再用一个实际案例加深印象。我曾经接手过一个项目同事在时序逻辑里用了阻塞赋值always (posedge clk) begin state next_state; // 阻塞赋值 count count 1b1; end这段代码在仿真里可能能够跑通因为仿真器按顺序执行state更新之后后面的语句能看到新值。但综合出来的硬件行为往往和仿真不一致因为硬件上的触发器是并行更新的。这种“仿真通过了上板翻车了”的案例是典型的阻塞赋值误用。我的经验是时序逻辑里只允许出现组合逻辑里只允许出现。不要混用也不要图省事在always里用凑数。这条规则一旦定下来很多诡异的时序bug都可以提前预防。7.3 按键消抖时序逻辑里过滤“毛刺”的活教材热搜里有“verilog按键消抖”这是个很实用的小模块。物理按键在按下和释放的瞬间电平会抖动好几次每次抖动可能持续几毫秒到几十毫秒。如果直接用按键信号去触发逻辑很可能一次按键被当成多次触发。最常用的消抖方案检测到按键电平变化后启动一个计时器持续采样20ms如果电平稳定不变就认为按键状态有效。代码思路如下if (key_in ! key_sync) begin cnt 0; key_sync key_in; end else if (cnt TIMEOUT) begin key_out key_in; end else begin cnt cnt 1b1; end这段逻辑依靠“持续多少拍没有变化就判定稳定”的思路本身就是一种对信号流动的过滤。你仔细观察就会发现几乎所有的过滤、消抖、去毛刺逻辑本质上都在利用“信号是否在时间维度上保持稳定”来做判断。7.4 用AI工具辅助写Verilog的体会最近热搜里出现“claude code写verilog代码”“ai agent verilog代码”这些关键词我也尝试过用AI辅助写一些基础模块。说实话对于计数器、状态机框架、SPI/I2C初始化这些标准化模块AI能给出不错的参考代码省去不少查语法的时间。但真正到了跨时钟域处理、时序约束、协议兼容这些需要深入理解的环节AI还难以替代人的判断。我现在的用法是把AI当做一个“语法顾问”负责生成初版代码和testbench框架然后再自己逐行审查时序逻辑是否正确、复位和使能是否完备、边沿条件是否覆盖。AI可以帮你写代码但不能帮你在上板之后排查问题后者需要的是对电路和信号流动的理解。7.5 关于Verilog面试被问得最多的几个点搜索里持续有人关注“华为数字ic和verilog八股”和“南邮verilog”这类话题说明很多人正在准备相关面试。从我参与过的面试经验来看以下几个问题出现的频率最高也是真实工程实践中最常见的考点阻塞赋值和非阻塞赋值的区别以及为什么时序逻辑要使用非阻塞赋值。状态机有几种写法各自优缺点以及你项目中选择的依据。跨时钟域处理有哪些方法以及异步FIFO的格雷码指针为什么能避免亚稳态。组合逻辑中的竞争冒险怎么避免如何在设计中尽量减少毛刺。这些问题的答案如果你只是背八股可能能过笔试但面试官一旦追问“你项目里遇到过吗”没有实际经验的人就会露馅。所以我的建议是每个知识点都尽量回到代码和波形里验证一遍把“信号怎么流动”的底层逻辑理解透这些问题自然迎刃而解。最后再分享一个小技巧写Verilog这么多年我最大的一个心得是不要一上来就写代码先画图。不需要画得多专业哪怕只是在纸上画出模块的输入输出、信号怎么流动、在哪个时钟沿更新都行。画完这张图再动手写代码你会发现自己的思路从来没有这么清晰过。我自己在处理SPI主机、I2C状态机、DDR3读写控制这些复杂模块时都是靠着先画信号流动图再写代码的方式把一个个看起来很吓人的需求拆成一小段一小段清晰逻辑的。这个方法我推荐给每一位正在被Verilog折磨的朋友。
返回列表