第4章 · 时序逻辑设计规范
时序逻辑,说白了就是带记忆的电路。它靠时钟沿来更新状态,这也是数字芯片里最核心的部分。我刚开始写RTL那会儿,总觉得时序逻辑就是加个 always @(posedge clk) 完事,后来被Lint工具和综合结果狠狠教育过几次,才明白里面的门道有多深。
4.1 always块(时序逻辑)的正确写法
写时序逻辑的 always 块,我个人习惯遵循三条铁律:
- 敏感列表只包含时钟和复位 —— 别把其他信号塞进去,综合工具会直接忽略多余项,但仿真可能出偏差。
- 非阻塞赋值(<=) —— 这是时序逻辑的标配。用阻塞赋值(=)大概率会搞出竞争,我在一个项目里见过同事因此丢数据,查了两天才定位到。
- 每个 always 块只描述一个时钟域 —— 混着写多个时钟,Lint 工具会报 CDC 违规,后端也会找你麻烦。
✅ 推荐模板(同步复位)
always @(posedge clk) begin
if (rst_n) begin
q <= 1'b0;
end else begin
q <= d;
end
end
你想想看,如果这里用了 if (!rst_n) 但 rst_n 是异步信号,综合后可能会产生意想不到的复位树。嗯,这里要注意:敏感列表里写 posedge clk or negedge rst_n 才是异步复位,别搞混。
4.2 同步复位 vs 异步复位 —— 怎么选?
这个问题几乎每个芯片工程师都会纠结。我自己的经验是:没有银弹,但可以按场景选。
| 对比项 | 同步复位 | 异步复位 |
|---|---|---|
| 复位信号要求 | 必须满足时钟沿采样 | 可随时生效,不依赖时钟 |
| 抗毛刺能力 | 强(时钟沿采样) | 弱(毛刺直接触发) |
| 综合实现 | 节省寄存器资源(无额外复位引脚) | 需要专用复位引脚,可能增加面积 |
| 时序收敛 | 容易满足复位时序 | 复位释放需做异步复位同步释放 |
| 典型应用 | 高速逻辑、DFT友好 | 低功耗、快速唤醒模块 |
💡 我的个人建议: 除非有明确的低功耗或快速复位需求,否则优先用同步复位。异步复位最大的坑是「复位释放时序」—— 我曾经在一个MCU项目里因为异步复位释放时间不满足,导致寄存器进入亚稳态,整个状态机跑飞。从那以后,我只要用异步复位,一定会加同步释放电路。
异步复位的标准写法(带同步释放):
// 异步复位同步释放
reg rst_sync1, rst_sync2;
always @(posedge clk or negedge rst_async_n) begin
if (!rst_async_n) begin
rst_sync1 <= 1'b0;
rst_sync2 <= 1'b0;
end else begin
rst_sync1 <= 1'b1;
rst_sync2 <= rst_sync1;
end
end
// 用 rst_sync2 作为模块内部复位
4.3 时钟域的概念 —— 别让信号「串门」
时钟域,就是由同一个时钟(或同源时钟)驱动的所有时序逻辑的集合。不同时钟域之间的信号传递,必须经过同步处理,否则就是 CDC(跨时钟域)问题。
我记得刚做第一个SoC项目时,把 A 时钟域的计数器直接接到 B 时钟域的组合逻辑上,仿真没问题,但样片一跑就随机出错。后来用 SpyGlass 跑 Lint,报了一堆 CDC 违例。说白了,时钟域边界是芯片设计里最容易出 Bug 的地方。
⚠️ 避坑指南: 单比特跨时钟域用双级同步器(打两拍);多总线信号建议用异步FIFO或握手协议。千万别图省事直接连,我曾经见过有人用单级寄存器同步多比特信号,结果不同比特在不同时钟沿变化,导致数据错乱。
最后说一句,时序逻辑的 Lint 检查重点就是这三块:always 块赋值风格、复位结构、时钟域边界。用 SpyGlass 或 Design_Lint 跑一遍,基本能筛掉 80% 的低级错误。我个人习惯在代码合入前先本地跑 Lint,省得 review 时被怼。