一、RTL设计基础:数字电路设计流程概述

做芯片设计这些年,我经常被新人问到同一个问题:「RTL设计到底难不难?」

我的回答是:入门不难,精通不易。说白了,RTL设计就是把你脑子里的数字电路,用硬件描述语言写出来。但真正写好,需要理解背后的设计方法学和电路本质。

1.1 数字电路设计全流程

先聊聊整个设计流程。我习惯把它分成几个阶段:

  • 需求分析——搞清楚芯片要干什么,性能指标是多少
  • 架构设计——划分模块,确定接口,估算面积和功耗
  • RTL编码——用Verilog或SystemVerilog把设计写出来
  • 功能仿真——验证逻辑对不对,有没有bug
  • 逻辑综合——把RTL代码映射到门级网表
  • 静态时序分析——检查能不能跑在目标频率上
  • 物理设计——布局布线,生成版图

你想想看,RTL编码在整个流程中处于什么位置?它是承上启下的关键环节。架构设计得再好,RTL写烂了,后面全白搭。

核心观点:RTL设计不是「写代码」,而是「画电路」。每一行代码背后,都要清楚它综合出来是什么电路。

我在项目中遇到过不少这样的情况:新人写出来的RTL仿真能过,但综合出来面积大得离谱,或者时序根本收敛不了。为什么?就是因为脑子里没有电路图。

1.2 数字电路设计流程概览图

下面这张图是我自己总结的设计流程,你可以把它当作一个路线图:

数字芯片设计全流程 需求分析 规格定义 / 性能指标 架构设计 模块划分 / 接口定义 RTL编码 Verilog / SV 实现 功能仿真 验证功能正确性 逻辑综合 RTL → 门级网表 物理设计 布局布线 / 版图 迭代优化 STA 静态时序分析 前端设计 RTL与验证 后端综合 物理实现

个人经验:我建议新手先别急着写代码。花30%的时间理解架构和接口,写代码只占40%,剩下30%用来仿真和调试。这个比例我用了十年,从来没出过大问题。

二、RTL设计方法学

2.1 设计原则

RTL设计有几个基本原则,我把它总结成「三要三不要」:

原则 要做什么 不要做什么
可综合性 使用综合工具支持的语法结构 使用initial、fork/join等不可综合语句
可读性 模块化设计,信号命名规范 一个always块写几百行,信号名用a1、b2
可验证性 设计时考虑测试点,预留观察信号 把所有逻辑揉在一起,没法单独验证

嗯,这里要注意:可综合性是RTL设计的底线。你写的每一行代码,综合工具都要能把它变成实际的电路。我见过有人用for循环写了一个复杂的乘法器,仿真跑得飞快,但综合出来面积大得吓人——因为综合工具把它展开成了全并行结构。

2.2 模块划分技巧

模块怎么划分?我个人的习惯是:

  • 按功能划分——每个模块只做一件事,做好一件事
  • 接口尽量简单——模块之间的信号越少越好,最好用标准的握手协议
  • 控制与数据分离——控制逻辑和数据通路分开写,调试起来方便

避坑指南:我曾经把一个设计分成20多个小模块,结果接口信号满天飞,连自己都搞不清楚谁连谁。后来学乖了,模块数量控制在7±2个,每个模块的接口信号不超过20根。这个经验值你可以参考一下。

三、组合逻辑与时序逻辑基础

3.1 组合逻辑

组合逻辑,说白了就是输入一变,输出立刻变。没有时钟,没有存储,纯粹的门电路连接。

常见的组合逻辑写法:

// 组合逻辑:用 assign 或 always @(*)
assign sum = a + b;
assign carry = a & b;

// 或者用 always 块
always @(*) begin
    if (sel)
        out = in1;
    else
        out = in2;
end

你想想看,上面这段代码综合出来是什么?就是一个加法器和一个多路选择器。很简单,对吧?

3.2 时序逻辑

时序逻辑就不一样了。它依赖时钟,有记忆功能。D触发器是它的基本单元。

// 时序逻辑:用 always @(posedge clk)
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 1'b0;
    else
        q <= d;
end

这里有个关键点:组合逻辑用阻塞赋值(=),时序逻辑用非阻塞赋值(<=)。为什么?

我在项目中遇到过这样的bug:新人把时序逻辑里的赋值写成了阻塞赋值,结果仿真波形看起来完全正确,但综合出来的电路功能是错的。因为阻塞赋值在always块里是顺序执行的,而非阻塞赋值是并行执行的。这个区别,你写代码的时候一定要刻在脑子里。

3.3 组合 vs 时序:对比总结

对比项 组合逻辑 时序逻辑
时钟依赖 不依赖 依赖时钟沿
存储能力 有(触发器)
赋值方式 =(阻塞赋值) <=(非阻塞赋值)
敏感列表 @(*) @(posedge clk)
典型应用 加法器、译码器、MUX 计数器、状态机、寄存器

一个小技巧:写always块之前,先问自己三个问题——这个电路需要时钟吗?需要存储数据吗?敏感列表应该怎么写?想清楚了再动键盘。我刚开始带团队时,就要求每个新人写代码前先在纸上画出电路图。这个习惯,到现在我还在坚持。

3.4 常见陷阱

最后聊几个我踩过的坑:

  • 组合逻辑环路——输出反馈到输入,没有寄存器打断,综合时会报warning,严重时会导致仿真振荡
  • 不完全的条件分支——if没有else,case没有default,综合出来会生成锁存器(latch)
  • 敏感列表不全——always块里用了某个信号,但敏感列表里没写,仿真和综合结果不一致

曾经有一次:我写了一个状态机,case语句漏掉了default分支。仿真时状态机跑得好好的,但综合出来多了一堆锁存器,面积直接翻倍。找bug找了整整两天,最后发现是少写了一个default。从那以后,我写case必带default,写if必带else,成了强迫症。

好了,这一章的内容就到这里。RTL设计基础是后面所有章节的基石,把组合逻辑和时序逻辑的概念吃透,后面的路就好走了。


公众号:蓝海资料掘金营,微信deep3321