0%

1. 参考

白栎旸. 数字IC设计入门(微课视频版)[M]. 北京: 清华大学出版社,2023-09-01:第2章.

2. 数字器件与Verilog语法

虽然 Foundry 提供的可用元器件种类较多,但数字设计师并不特别在意这些元器件,在其头脑中,只存在 10 种简单元器件:

  • 与门、或门、非门、异或门、
  • 加法器、乘法器、移位器、
  • 选择器、比较器、
  • 触发器

注意,虽然加法器、乘法器、比较器等元器件可以由逻辑门生成,但是从Verilog常用的表达式来看,一般直接用“+”这个符号表示加法,而很少用门电路去直接搭建。因为现代芯片规模庞大,功能复杂,工程师应该将主要精力投入到重点难题的实现中,而对于如何实现加法器等最底层的问题,应该交给综合步骤自动完成。

基本元器件中不包括除法,因为除法的实现不同于乘法,它受到被除数、除数、商的数值范围限制,有时需要用到迭代等复杂方法实现,还有分母为 0 等异常情况需要报告,所以不属于Verilog中常用的直接运算方式。

务必注意,数字前端写的Verilog仅仅是代码,而非程序,其代码是代替电路图的一种文本语言描述。与或非加乘等指的是元器件。写Verilog时要有电路概貌和时序。

10种数字器件的符号表示及Verilog表示方法见表格:

10种数字逻辑器件和Verilog表示
20250117214703

真正的元器件库中有很多复杂的元器件,如图所示:

20250117214659
复杂元器件示例

但是这些元器件都可以看作以上10种基础元器件的组合,不会超出原有的功能范围。所以设计时,头脑中只需要有以上10种元器件。

数字IC设计又称为数字逻辑设计,因为其本身就是逻辑的,只有01两种逻辑。

  • 组合逻辑:电平输入和电平输出。元器件结构简单,但问题是如果输入含有毛刺,输出就有毛刺。
20250117215056
20250117214654
组合逻辑电路的毛刺
  • 时序逻辑:以时钟为驱动源。一个触发器,在时钟的驱动(边沿触发)下,将 D 输入端的信号送到 Q 端输出。
20250117214649

触发器也可以叫寄存器(register, reg),因为如果没有时钟驱动,那么Q端会保持原有状态不变,也就寄存了上一次触发时的D端信息。而组合逻辑,输出端是无法寄存信息的。

时序逻辑是数字电路的基础。10 种元器件中,只有触发器属于时序逻辑器件,所以触发器是整个数字电路的基础。从 RTL 的名称可以知晓,RTL 意为寄存器传输层,直译过来就是:从一个触发器的输出到另一个触发器的输入,通过触发器的层层传递,最终实现了一个功能完整的数字电路。

数字电路的时序分析,主要是分析两个触发器之间的路径延迟。

进行前仿时,看到的仿真波形会和本图一样,是理想的,而使用版图网表进行后仿时,仿真波形是带延迟的。

上图中触发器的符号表示,它共有4个引脚,除输入的D端和输出的Q端外,三角形位置表示时钟,下方的rst_n表示复位,其上的圆圈表示0电平有效,即rst_n等于0时,寄存器处于复位状态。此时,Q端保持0,即使时钟和D端有动作,Q端也不会变化,只有当rst_n等于1时,才解除复位状态,寄存器方能正常工作。

3. 可综合的Verilog设计语法

能变成电路的Verilog表达叫做可综合,在设计电路时,只能使用可综合的语法表述。而在仿真时,由于只在计算机上运行,不留片,可使用不能综合的高级语法,以增加语言表达的灵活度和复杂度。

可综合的电路表述只有两种:

  • assign
  • always

与门:

1
assign z = a & b;

触发器:

1
2
3
4
5
6
7
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
Q <= 0;
else
Q <= D;
end

符号说明:

  • <=: 非阻塞赋值,凡是时序逻辑,都用非阻塞赋值;
  • =: 阻塞赋值,凡是组合逻辑,都用阻塞赋值;
  • @(...): 括号中的列表叫敏感列表,意思是,always块输出的Q对列表中信号保持敏感,如果敏感信号动,则Q也会动。
  • posedge clk: 意思是时钟的上升沿;
  • negedge rst_n: 意思是时钟的下降沿。

always不仅可以表示时序逻辑,也可以表示组合逻辑。如下是与门的另一种表示:

1
2
3
4
always @(*)
begin
z = a & b;
end

其中,@(*)中的*是省略表述的敏感列表,综合器会自动在always块中寻找与输出z相关的输入信号,自动填入敏感列表中。本例中,会自动将ab作为输入填入。这种让工具自动填入的方式是可靠且推荐的。

Verilog的语法规律:

  1. 时序逻辑,必须使用always块,并同时使用<=非阻塞赋值。在其敏感列表中,必须出现时钟信号的边沿和复位信号的边沿。
  2. 组合逻辑,可以使用assign,也可以使用always块,但是它们的赋值是=阻塞赋值。若使用always块,则敏感列表中使用*。若遇到敏感列表中带有*,则可以直接判定为组合逻辑。

再次强调,Verilog的语法表达,描述的都是电路,因此例子中的 zabclkrst_nQD 都称为信号,在电路中都是实实在在的金属连线,切勿称为变量。

4. 对寄存器的深度解读

一般会使用时钟上升沿来驱动寄存器。对于同样的功能需求,双沿触发需要的时钟慢,但要求时钟是50%占空比,而单沿触发,对时钟的要求快一倍,但对时钟形状的要求降低很多。

复位信号rst_n,以0电平作为复位电平,1电平解复位,是通用标准,很少有反过来使用的。原因是,数字电路的复位信号是模拟电路给的,通常,模拟电路将其命名为POR(Power On Reset),即上电复位信号。芯片刚通电时,电压小,逐渐上升到要求的电压,例如1.8VPOR本质上是一个电压上升的标志,模拟电路放一个比较器,将输入电压与0.9V比较,电压小于0.9VPOR0,电压大于0.9VPOR1。因而复位信号上电时总是先01,数字寄存器需要在复位信号为0的阶段保持复位态,不能运行,因为此时芯片电压不足,不能保证正常运行,而复位信号变成1,说明上电完毕,电压充足,寄存器解除复位进行正常运行是安全的。

需要特别澄清的是语句negedge rst_n,但是经过仿真和与模拟工程师确认,复位信号对寄存器的作用不是通过信号沿来驱动的,而是通过电平来驱动,也就是 0 信号具有绝对控制权,只要 rst_n0 ,那么立即复位。

5. 非阻塞赋值和阻塞赋值的区别

非阻塞赋值的意思是该句表达不会阻塞后续表达的执行。如下例中,X <= 0的执行,不会阻碍到Y <= 0的执行,它们是同时发生的:

1
2
3
4
5
6
7
8
9
10
11
12
13
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
begin
X <= 0;
Y <= 0;
end
else
begin
X <= A;
Y <= B;
end
end

而阻塞赋值,意思是如果前一句不执行,后一句就无法执行,前一句会阻塞后一句。对于可综合的Verilog来讲,其实并不会阻塞。在下例中, always 块的目的是创造 zk 两个信号。 k = 3 * zz = a & b 是两个不同的电路, k = 3 * z 电路不会被 z = a & b 阻塞。

1
2
3
4
5
always @(*)
begin
z = a & b; // 与门
k = 3 * z; // 乘法器
end

本例对应的原理图如图所示:

20250117224729

可见,对于电路描述来讲,语法只是表示一种连接关系,并没有执行先后顺序的说法,但如果本例使用非阻塞赋值,语法检查会报错,因此,这是一种惯用方法。阻塞赋值在Verilog中真正体现阻塞,是在仿真使用的不可综合语法中,到第3章再做解释。

6. 组合逻辑的表达式

对于一个组合逻辑电路,应该在什么情况下用assign,在什么情况下用always呢?

比较简单的逻辑适合使用assign方式,较为复杂的逻辑应使用always块。下例给出了一个适合用always块的较复杂例子:

1
2
3
4
5
6
7
8
9
10
11
always @(*)
begin
if (s1)
a = 1;
else if (s2)
a = 2;
else if (s3)
a = 3;
else
a = 0;
end

同样的功能若改用assign,则为下例所示。很明显,用always块表达意思更加清晰。

1
assign a = s1 ? 1 : (s2 ? 2 : (s3 ? 3 : 0));

前面解释了敏感列表中的 * 在组合逻辑 always 块中的作用。如果读者使用过一些老IP,则可能还会看到下例所示的表达,这种表达已随着综合器的进步渐渐被淘汰了,不建议初学者使用。

1
2
3
4
5
6
7
8
9
10
11
always @(s1 or s2 or s3)
begin
if (s1)
a = 1;
else if (s2)
a = 2;
else if (s3)
a = 3;
else
a = 0;
end

7. 组合逻辑中的选择器

二选一 MUX 如何表达?

20250117225844
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 第 1 种表达, assign 完整表达
assign z = (s == 1) ? b : a;

// 第 2 种表达, assign 简化表达
assign z = s ? b : a;

// 第 3 种表达, always 块表达
always @(*)
begin
if (s)
z = b;
else
z = a;
end

多选一MUX,又该如何表示呢?

因为使用 assign 表示显然会过于复杂,所以需要用 always 块表示。表示方法有两种,注意两种表达综合出来的电路是不同的。

其一如下例所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
always @(*)
begin
if (s == 0)
z = a;
else if (s == 1)
z = b;
else if (s == 2)
z = c;
else if (s == 3)
z = d;
else if (s == 4)
z = e;
else if (s == 5)
z = f;
else
z = a; // 默认值
end

综合出来的电路如图所示:

20250117230859

可见,使用 if 表述的选择关系,综合的电路是一层一层逐渐展开的,写在 if 最前面的语句,掌握着最终的选择权,因而优先级最高,再往后优先级逐层下降,而使用 case 表述的MUX,每个选择都是并列的,优先级相同,见下文。

其二如下例所示。

1
2
3
4
5
6
7
8
9
10
11
always @(*)
begin
case (s)
0: z = a;
1: z = b;
2: z = c;
3: z = d;
4: z = e;
5: z = f;
default: z = a; // 默认值
end

所综合的电路如图所示:

20250117230221

使用 if 表述,有可能出现隐藏逻辑,即设计者没有考虑到,但实际会被综合出来的逻辑门。隐藏逻辑是设计的隐患,设计者在写代码时应该清楚其逻辑含义,尽量避免出现隐藏逻辑。为了避免设计中出现隐藏逻辑,在实际项目中往往会提倡使用 case 语句来表达。

下例中反映出 if 的优先级特征,条件 s < 5 包含 s == 4 的情况,因为 s < 5 优先,因而当 s==4 时, z 的赋值是 a 而不是 b 。如果设计意图是要在 s == 4 时使 z = b ,则应当将其写在 s < 5 之前。

1
2
3
4
5
6
7
8
9
always @(*)
begin
if (s < 5)
z = a;
else if (s == 4)
z = b;
else
z = c;
end

case有一种变体是casez,它可以拓展case的使用范围:

1
2
3
4
5
6
7
8
9
always @(*)
begin
casez(s)
16'b00011???????????: z = a;
16'b10111111????0000: z = b;
16'b1111?001000?????: z = c;
default: z = d;
endcase
end

其中,问号的意思是0或1都能匹配,类似计算机语言中的通配符。

虽然很多项目提倡使用 casecasez 来表述选择器,但究竟是使用 if 还是 case ,仍然取决于表达的需要。总体而言, case 便于判断是否相等的情况,而 if 适合判断大于或小于关系,不同情况用不同的表达,可以使Verilog逻辑更加清晰,也更便于维护。

注意组合逻辑中的 ifelse if ,最后必须跟一句 else ,使整体逻辑完整。若没有else,则该电路会综合出一个锁存器(Latch)。锁存器不属于10种基本元器件之一。在设计中,凡有寄存需求,应尽量使用触发器,避免使用锁存器,特别要避免不写 else 引起的隐藏逻辑。

1. 参考

白栎旸. 数字IC设计入门(微课视频版)[M]. 北京: 清华大学出版社,2023-09-01:第1章.

2. 数字IC设计流程

2.1. 三个主要流程:

  1. 前端设计: 用 Verilog 编写 RTL 设计文件(Register Transfer Level,RTL,寄存器传输层)。

  2. 综合(Synthesis):将 RTL 转换位实际电子元器件的连接。输出文件称为网表(netlist),也是纯文本文件。

  3. 后端布局布线(PR):将网表变为实际电路图,类似 PCB 图,称为版图(Layout)。版图是晶圆厂(Foundry)能识别的通用格式。

    版图的绘制:

    • 布局(Place):元器件如何摆放;
    • 布线(Route):元器件如何连接。

    数字版图的绘制大部分由计算机自动完成,称为自动布局布线(Automatic Place and Route,APR),很多时候也称为 PR 。

    流片(TapeOut):Foundry 根据版图制造芯片。

2.2. 验收和验证(SignOff):

对质量进行把控和最终验收。

前仿真:检查 RTL 设计文件;用测试平台(Testbench)文件对实际应用环境仿真。这是在版图成型之前,称为前仿。一旦发现错误,验证人员会反馈给设计者修改,如此反复迭代,直至交付到综合阶段的 RTL 文件都是合格的。

SignOff 检查:检查版图设计;对时序和功耗检查。如果检查不过,首先是数字后端工程师自己努力,重新绘制版图或微调元器件位置,若无法达到目的,再修改综合策略,重新综合。若仍无法达到目的,则反馈到前端 IC 设计,在 RTL 上调整。

后仿真:在 SignOff 合格后,需要将网表和延迟信息提交给验证人员,进行后仿。对成型版图的时序进行最后验证和验收。后仿就是版图成型之后的仿真。

2.3. 可测性设计(Design For Test,DFT)

一个附属工序,原理是在芯片中加入与主要功能无关的附属电路,用于在芯片的生产过程中使用 TestBench 快速判断芯片功能的完好行、内部结构的正常性。

如果芯片中包含 DFT 电路,则 TestBench 可以向芯片的某些引脚发送事先准备好的测试向量,在芯片的另一些引脚上采样芯片对测试向量的反应。将事先准备好的预期效果与实际采样到的结果进行对比,从而判断内部功能是否正常。

DFT 功能也常常被称为扫描(Scan),即用 TestBench 扫描芯片内部之意。DFT 的检查对象是生产差错造成的芯片损坏,它不能检查设计问题。设计问题应该交给验证工序。

DFT 对芯片来说不是必需的,一些低成本的芯片没有插入 DFT 电路,在设计流程中,直接从电路综合过渡到后端设计。

2.4. 前端设计 VS 后端设计

  • 前端设计:抽象电路,只描述功能,不是具体电路。
  • 后端设计:具体电路,不仅要知道哪些元器件,还要确认这些元器件的摆放位置。

综合是前端和后端的分界线,综合之前没有元器件而只有功能;综合之后,设计才有了具体化的元器件。

当然,前端工程师往往强调心中有电路,并非实际电路,而是心中有功能相似的概念性电路。这样可以认清设计架构,并避免潜在的设计隐患。

3. 模拟IC设计流程

  1. 绘制原理图(Schematic)

    手动绘制。

  2. 原理图仿真

    一般由原理图设计者亲自完成。原因是模拟电路的设计参数选择范围宽泛,需要验证的场景也复杂,仿真验收标准不十分明确。这就需要设计者搭建关键的应用环境和场景,对电路上产生的反应做出判断,随时调整电路结构和参数。

  3. 绘制版图(根据原理图)

    手动绘制。

    模拟版图工程师和模拟IC设计师一般是分开的两个职业。

    模拟IC设计师一般也都掌握一些绘制版图的方法,单熟练程度以及对一些物理问题的处理方面,需要求助专业的版图工程师。

    模拟版图工程师可以根据原理图及通用的绘图规则直接绘制。但是对诸如社评电路及高功率管之类有着特殊要求的设计,版图工程师需要与模拟IC设计师进行充分沟通,在模拟IC设计师的协助下进行绘制。

  4. 抽取寄生参数

  5. 版图后仿

  6. 设计规则检查(Design Rule Check,DRC)

  7. 版图和原理图的一致性检查(Layout Versus Schematics,LVS)

  8. 流片

模拟IC设计没有那么多自动化成分,需要手动绘制原理图、版图,电阻、电容等元器件需要人工确定。

模拟IC仿真,无法像数字IC那样通过仿真验证和FPGA验证两种方法来相互印证设计效果。模拟仿真速度慢、情况多,很难覆盖真是使用中可能遇到的全部情况,因此,模拟设计具有很高的不确定,芯片的实际效果与仿真结果存在明显差距是经常发生的。

但是模拟电路也有一些无法被数字电路取代的特性。

可以想象,数字电路只需要处理 0 和 1 两种信号,但是模拟电路的数值域是实数域,包括所有连续的整数、浮点数、正负数,数据有无穷个。因此,即使是很小的电路,元器件数量不多,也需要设计者和仿真者从数值域中挑选一部份值作为设计和仿真中用到的值,绝对不可能覆盖所有情况。

4. 芯片整体规划

版图布局规划(FloorPlan):将芯片整体规划,以及内部数字、模拟电路的位置、面积、形状等特征的规划。

FloorPlan示例
  • Pad: FloorPlan的周围是芯片引脚(Pad)。Pad实际指的是芯片引脚之外的一块金属。
  • IO: 包括Pad和内部逻辑在内的整个引脚设计。

一个完整的引脚设计如图:

20250116222905
  • 封装: 芯片外面的塑料壳子称为芯片的封装,大体分为两种:插针式(引脚如针)、表贴式(引脚扁平)。

5. IC设计工具

EDA 公司:电子设计自动化(Electronic Design Automation)公司,粗略来说,数字设计常用 Synopsys ,模拟设计常用 Cadence , Mentor 在一些细分领域有优势。

数字/模块 数字流程 常用软件 其他软件
数字 RTL 编写 Vim/Gvim 普通文本编辑器
数字 仿真 VCS (Synopsys) Incisive (Cadence)
数字 看波形 Verdi (Synopsys) DVE (Synopsys)
数字 设计检查 Spyglass (Synopsys) Simvision (Cadence)
数字 综合 DC (Synopsys) ModelSim (Mentor)
数字 时序仿真 PT (Synopsys) nLint (Cadence)
数字 自动布局布线 ICC2 (Synopsys) Genus (Cadence)
数字 设计版图形式验证 Formality (Synopsys) Tempus (Cadence)
数字 提取寄生参数 StarRC (Synopsys)
模拟 原理图/版图/仿真等 Virtuoso (Cadence) Calibre (Mentor)
模拟 寄生提取/DRC/LVS Calibre (Mentor)

仿真加速器: Palladium (Cadence) 、 ZeBu (Synopsys) 。

5.1. 数字IC设计工具

RTL 仿真工具:有 Synopsys 的 VCS ,Cadence 的 Incisive (也叫 irun )。这些工具可以胜任前仿、后仿、UVM 架构(Universal Verification Methodology,通用验证方法学)的仿真。Cadence 的 irun 与模拟设计工具 Virtuoso 工具中集成的 AMS 仿真工具相结合,支持数字模拟电路混合仿真。

波形查看工具:一般集成在仿真工具中。VCS 的波形软件叫 DVE , Incisive 的波形软件叫 SimVision 。但在这个领域,一家名为 Novas 的公司的软件,以其明快的界面、方便的功能、快捷的操作,异军突起,得到了广泛的认可,它就是 Verdi (以音乐家威尔第的名字命名,前身叫 Debussy ,以音乐家德彪西的名字命名)。 Verdi 现已被 Synopsys 收购。 Mentor 的仿真和看波形软件叫 ModelSim ,主要用于 FPGA 功能的仿真。

RTL 语法检查工具: Atrenta 公司的 Sypyglass ,可以检查语法、跨时钟域处理方案的可靠性,甚至可以在内部执行综合、功耗评估和简单的布局布线,使它能全方位地给出设计建议。现已被 Synopsys 收购。 Cadence 的对应检查工具是 nLint 。

综合工具:即将 RTL 转化为实际电路的工具,常用的的有 Synopsys 的 Design Compiler (DC) ,该工具内部还有一些 Synopsys 开发的库,能够帮助设计者减小面积,提高性能,例如加法器、乘法器等,这些设计好的子模块在 DC 中被称为 DesignWare (DW) 。可以是让工具自动从 RTL 中识别出可用 DW 替换的代码,也可以是设计者手动例化 DW 模块。 Cadence 相应的工具叫 Genus (原名叫 RTL Compiler )。

版图自动布局布线软件: Synopsys 有 ICC2 (旧版为 ICC ), Cadence 有 Innovus (原名 Encounter ),两个软件都被广泛使用。由于两个软件的操作命令不同,后端工程师往往只掌握其中一种。 Synopsys 为了增强客户的黏性,开发了一个银河( MilkyWay )流程,从前端到后端,通过专用的二进制文件 (db) 传输,占用空间小,处理效率高,但也有许多公司使用 DC 综合,再将网表导入 Innovus 进行布局布线。

SignOff 工具:即对整个设计的时序、功耗评估。 Synopsys 的 Prime Time (PT) , Cadence 的 Tempus 。目前, PT 已经称为业内 SignOff 的标准。实际上,时序分析在 DC 中也能做,但两者在分析方法、细节考虑全面度、分析速度等方面存在差异。在综合时使用 DC 检查,而在 SignOff 时,使用 PT 检查。

形式验证工具:也称为逻辑等效行检查( Logic Equivalece Check, LEC ),将 RTL 和网表进行一一对照。因为从 RTL 到综合网表,以及从综合网表到后端网表的过程,可能意外地改变原有功能和设计意图,所以需要进行检查。 Synopsys 的工具时 Formality , Candence 的工具是 Conformal 。

寄生参数的提取工具:进入设计版图阶段,可以确定走线的延迟。该值受信号负载、线路长短、粗细、周围线路等多重影响,需要用模型和查表进行计算,才能得到确切的值。该过程称为寄生参数的提取。一般使用 Synopsys 的 StarRC 。提取出来的信息可用于 PT 进行 SignOff 。

上述工具,最主要的控制语法是 TCL 语言, EDA 工具大多以该语言为基础,扩展出各种专用命令。

除了以上数字设计工具,还有一些更加细分的工具类型,比如仿真加速器,有 Cadence 的 Palladium 、 Synopsys 的 ZeBu 。

5.2. 模拟IC设计工具

主要是 Cadence 的 Virtuoso 。与数字设计中繁多的工具不同, Virtuoso 能满足大部分设计需求,例如绘制并仿真原理图、绘制并仿真版图、数模混合仿真等。实际上, Virtuoso 更像集成开发环境( Integrated Development Environment, IDE ),它包含很多独立的设计工具,如仿真工具 Spectre 等。

SignOff 工具:Mentor 的 Calibre 可以用来提取寄生参数、进行 DRC 和 LVS 检查。

5.3. 职位、分工与工具

大职位 细分职位 功能 常用工具
数字 数字 IC 设计 设计芯片中的数字电路 Vim, VCS, Verdi, Spyglass, DC 等
数字 数字 IC 验证 验证芯片中的数字电路功能 VCS, Verdi 等
数字 数字 IC 后端 将抽象电路转换为版图 ICC2, Innovus, Calibre, PT 等
数字 其他职位, 如 SignOff, DFT 等 负责在数字电路中插入 DFT、对最终的版图进行时序、面积、功耗的检查等 DC, PT, Formality 等
模拟 模拟 IC 设计 设计芯片中的模拟电路 Virtuoso 等
模拟 模拟版图 将电路原理图做成版图 Virtuoso, Calibre 等
软件 嵌入式软件工程师 (1) 参与芯片开发/验证 (2) 参与 SDK (3) 参与芯片应用方案 Keil, SourceInsight, Visual Studio 等
软件方案 测试工程师 (1) 对芯片设计性能的测试 (2) 芯片量产测试 兼用软件和硬件工具
硬方案 应用工程师 做电路板,为芯片找到合适的应用场景 Pads, Altium Designer, Cadence 等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
管理
├── 研发
│ ├── 数字IC设计
│ ├── 数字IC验证
│ └── 数字后端
│ └── 模拟IC设计
│ └── 模拟版图
├── 测试
│ ├── 研发类测试
│ │ ├── 方案测试
│ │ └── 量产测试
│ └── 客户测试
├── 销售
└── 方案
├── 硬件方案
└── 软件方案

5.4. EDA 与 Foundry 的关系

普通软件公司无力开发 EDA 软件。因为 EDA 厂商需要和 Foundry 厂商紧密合作,才能获得有关的生产细节数据,从而帮助用户进行更加准确的仿真、寄生参数的提取、规则的检查。不同的 EDA 工具抽取的寄生参数可能不同,原因可能是不同的工具获得的工艺数据不同。

国内 EDA 厂商要想打破垄断,不仅要靠软件技术,还要与各大 Foundry 厂达成战略合作,共享工艺数据,才能做出有实用价值的 EDA 工具。

6. 概念

6.1. 寄存器:

register,这是对功能强调的名称,其物理实体很多,比如触发器 (Flip-Flop, FF)和锁存器(Latch)等等。在数字设计中,主要提倡触发器。

寄存器是受控于时钟沿的元器件, Foundry 会在标准单元库中提供多种寄存器。

触发器是由边沿信号而非电平信号导致数据存储的。触发源一般是时钟信号。

锁存器较少使用,它一般以非时钟信号为存储控制,靠电平触发。一般不连接时钟,只是连接一根普通的信号线。这种锁存器一般很少被纳入时钟计算中,因为它既不属于组合逻辑,又无法像触发器一样作为时序路径的起终点,所以时序上无法通过工具保证,只能通过工程师来保证。

本书说的寄存器,基本可以替换为触发器。在数字芯片 EDA 工具链中,两个概念也是混用的,一个 reg 既可以指寄存器,也可以指触发器。另外,所谓时序逻辑门电路,也基本等同于触发器。

6.2. 设计(Design)的边界

20250116211433
  • Pad:芯片(封装)之外的引脚。

  • 元器件:指芯片内的组合逻辑门电路及时序逻辑门电路。这些标准的元器件也被称为标准单元(Standard Cell)。元器件也称为逻辑门。

  • 标准单元库/工艺库:元器件的集合。

  • I/O: 如前“FloorPlan”对IO的解释,I/O是包括Pad和内部逻辑在内的整个引脚设计。在标准单元库中是一种标准单元,但是它比较特殊,可能与其他标准单元不放在同一个标准单元库中。驱动电平也可能不同。

  • 通用单元库: 在综合时,有一个中间步骤是将 RTL 抽象逻辑先映射为通用单元(Generic Boolean),然后映射到标准单元。通用单元与工艺无关,也不包含物理特性,只有功能属性,与 RTL 描述类似。而标准单元既有功能属性,又有延迟、电压等工艺属性,与工艺和 Foundry 相关。

  • lef文件: Library Exchange Format. 描述特定流片工艺下元器件的物理属性。

  • lib文件: Library. 描述特定流片工艺下元器件的功能属性。lib文件常常要被编译为二进制的db文件才能在某些EDA工具上使用。 lef 和 lib 文件都是技术库。

    数字设计只能在 Foundry 提供的已有的选项中挑选元器件。

  • def文件: Design Exchange Format. 设计除了 RTL 外,也可以带有物理信息,保存为 def 文件,基础内容是设计的形状和尺寸,扩展内容包括端口的分布位置、RAM和ROM等硬核的放置位置、内部元器件的放置和布线位置等。 RTL 和 def 统称为设计。

自己写的模块可以称为设备(Device)或模块(Module)。

用设备一词主要强调它具备相对独立的功能,在介绍SoC架构等上层逻辑时较为常用,设备也经常被替换为单元(Unit)一词,它指人为设计的单元,而非标准单元。一个设计可以包含很多个设备。

用模块一词,主要强调它在设计文件结构上的独立性,在介绍通用设计和底层逻辑时较为常用。

让 VMWare 的 Linux 虚拟机共享 Windows 主机的 VPN

方案 2:NAT 模式 + Windows 网络共享
另一种方式是保持 VMware 使用 NAT 模式,并通过 Windows 主机的网络共享功能,将 VPN 流量共享给虚拟机。

步骤:
在 Windows 上启用网络共享:

确保 Clash 在 Windows 主机上运行,并已连接到 VPN。
打开 控制面板 > 网络和共享中心 > 更改适配器设置。
右键点击你的 VPN 连接(通常是一个以 Ethernet 开头的适配器),选择 属性。
在 共享 标签页,勾选 允许其他网络用户通过此计算机的 Internet 连接连接。
选择虚拟机使用的网络适配器(例如 VMware 的 VMnet8 适配器),然后点击确定。
配置虚拟机网络为 NAT:

在 VMware 中,确保虚拟机的网络适配器设置为 NAT(这通常是默认设置)。
在 NAT 模式下,虚拟机会通过 Windows 主机访问网络,但流量会被路由到共享的网络适配器。
验证虚拟机是否可以通过 VPN 访问网络:

启动虚拟机,测试网络连接。
如果虚拟机连接成功并通过 VPN 访问互联网,那么配置就完成了。

线段树(Segment Tree)是一种将数组区间存储为树的数据结构 ^1

时间复杂度

查询连续子数组 或 查询范围内的最小元素:O(logn)

最简单形式的线段树

高阶版本的线段树

TODO

参考

OI wiki

参考源码

/usr/include/c++/11/bits/shared_ptr_base.h
/usr/include/c++/11/bits/shared_ptr.h

博客:https://zhiqiang.org/coding/std-shared-ptr.html

std::shared_ptr 的性质

  1. 复制构造、析构是线程安全的。

标准库使用原子操作实现无锁的线程安全性。

  1. 写操作(例如 reset 、 赋值 operator= )是线程不安全的。

写操作和复制构造、析构的主要区别是:

  • 复制构造、析构函数中,单个线程只处理一个对象,复制构造函数将其他对象复制过来之后,不会改动其他对象的资源(引用计数、所管理的内存)。
  • 但是写操作可能多个线程都在处理该 shared_ptr 。例如多个线程都对同一个 shared_ptr 进行赋值:
1
2
3
shared_ptr<int> sp1 = make_shared<int>(1);
sp1 = sp2; // 线程 1
sp1 = sp3; // 线程 2

对比源码分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
     __shared_count&
operator=(const __shared_count& __r) noexcept
{
_Sp_counted_base<_Lp>* __tmp = __r._M_pi;
if (__tmp != _M_pi)
{
if (__tmp != nullptr)
__tmp->_M_add_ref_copy();
if (_M_pi != nullptr)
_M_pi->_M_release(); // 注意:线程 1 和线程 2 持有相同的 _M_pi!
_M_pi = __tmp; // 注意:线程 1 和线程 2 持有相同的 _M_pi!
}
return *this;
}

以上代码中两个“注意”可能同时在发生,例如:
线程 1 release 的时候,线程 2 在给 _M_pi 赋值;
或者两个线程同时在 release 或同时在给 _M_pi 赋值。

要点

  1. 管理的内存和引用计数都应该动态分配到堆( heap )上,这样多个 shared_ptr 对象才能更新同一份数据。
  2. 需要同时维护强引用计数和弱引用计数。
  3. 引用计数本身应该是一个控制块类,使用 delete this 来自动删除(析构)引用计数。

永远不要手动 delete use_count,因为其他线程可能此时正在使用该资源,例如解引用 *use_count

SharedPtr.hppview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
template<_Lock_policy _Lp = __default_lock_policy>
class _Sp_counted_base
: public _Mutex_base<_Lp>
{
void
_M_add_ref_copy()
{ __gnu_cxx::__atomic_add_dispatch(&_M_use_count, 1); }

void
_M_release() noexcept
{
// Be race-detector-friendly. For more info see bits/c++config.
_GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_use_count);
if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1)
{
_GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_use_count);
_M_dispose();
// There must be a memory barrier between dispose() and destroy()
// to ensure that the effects of dispose() are observed in the
// thread that runs destroy().
// See http://gcc.gnu.org/ml/libstdc++/2005-11/msg00136.html
if (_Mutex_base<_Lp>::_S_need_barriers)
{
__atomic_thread_fence (__ATOMIC_ACQ_REL);
}

// Be race-detector-friendly. For more info see bits/c++config.
_GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_weak_count);
if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count,
-1) == 1)
{
_GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_weak_count);
_M_destroy();
}
}
}

private:
_Sp_counted_base(_Sp_counted_base const&) = delete;
_Sp_counted_base& operator=(_Sp_counted_base const&) = delete;

// using _Atomic_word = int;
_Atomic_word _M_use_count; // #shared
_Atomic_word _M_weak_count; // #weak + (#shared != 0)
};

template<_Lock_policy _Lp>
class __shared_count
{
_Sp_counted_base<_Lp>* _M_pi; // 引用计数的指针

__shared_count(const __shared_count& __r) noexcept
: _M_pi(__r._M_pi)
{
if (_M_pi != nullptr)
_M_pi->_M_add_ref_copy(); // TODO: 有没有可能此时的 _M_pi 所引对象已经被释放了呢
}

~__shared_count() noexcept
{
if (_M_pi != nullptr)
_M_pi->_M_release(); // 没有直接 delete _M_pi,
// 而是使用 _M_pi 的成员函数来释放它。
}

__shared_count&
operator=(const __shared_count& __r) noexcept
{
_Sp_counted_base<_Lp>* __tmp = __r._M_pi;
if (__tmp != _M_pi)
{
if (__tmp != nullptr)
__tmp->_M_add_ref_copy();
if (_M_pi != nullptr)
_M_pi->_M_release(); // 没有直接 delete _M_pi,
// 而是使用 _M_pi 的成员函数来释放它。
_M_pi = __tmp;
}
return *this;
}
};

参考

https://gcc.gnu.org/onlinedocs/libstdc++/manual/ext_concurrency.html

在测试leetcode的 815.公交路线 一题时,发现一个现象,当我使用vector<vector<bool>>作为图的邻接矩阵的类型时,运行速度比使用vector<vector<int>>慢了近1倍。使用bool运行速度为600ms左右,使用int时运行速度为300ms左右。

以下是ChatGPT的回答:

在C++中,bool类型并不一定比int类型慢,但在某些情况下,它们的性能表现可能有所不同。以下是一些与性能差异相关的考虑:

  1. 内存大小
    bool类型在标准中只需要表示 true(非0)和 false(0),通常占用1个字节(8位),而 int 通常占用4个字节(32位)。因此,bool类型可以节省内存,在某些情况下会带来缓存效率的提升,尤其是在处理大量布尔值时(例如布尔数组)。
  2. 对齐和内存访问
    在某些架构上,处理单字节的bool类型时,可能会有额外的开销,因为处理器通常优化对齐访问。如果内存没有正确对齐,处理器可能需要额外的操作来处理非对齐的字节,导致性能下降。相比之下,int的对齐通常与系统的内存总线对齐更好(如32位或64位系统上),因此读取和写入可能更高效。
  3. 指令集和操作
    现代处理器在处理布尔值时通常会将其扩展为完整的寄存器大小(例如32位或64位)。因此,虽然bool类型的存储可能更小,但在实际操作中,处理器可能将它提升为int或更大的类型来执行逻辑操作。处理布尔值可能涉及隐式类型转换,这在某些极端情况下可能带来开销。
  4. 寄存器使用
    int 类型可以直接映射到处理器寄存器中进行操作,而 bool 类型的操作有时可能会涉及额外的逻辑运算(如掩码操作)以确保布尔值的正确性。
  5. 编译器优化
    现代编译器在优化时会考虑到bool与int的差异,并尽量优化代码,因此在大多数应用场景下,你不会感受到显著的性能差异。但在一些特定的场合(如大规模数组、频繁内存访问等),bool类型的使用确实可能带来性能影响,尤其是在处理器需要对齐或执行额外的操作时。
    总结
    bool类型通常并不会比int慢,实际的性能差异取决于平台、内存对齐、编译器优化以及应用场景。在大多数情况下,差异是微不足道的,但在对性能非常敏感的程序中,这种差异可能会显现。

bash调试技巧

set -x

1
2
PS4='+ ${LINENO}: '
set -x

说明:PS4设置打印格式,$LINENO表示打印行号。

trap DEBUG

设置DEBUG陷阱,该陷阱在每一条“简单语句”执行完毕之后会被调用一次。

1
2
3
4
5
6
7
8
9
10
11
set -o functrace
track_var="my_var"
old_value=""
check_var_change() {
new_value="${!track_var}"
if [[ "$new_value" != "$old_value" ]]; then
echo "$track_var changed to \"$new_value\" at $BASH_LINENO"
old_value="$new_value"
fi
}
trap check_var_change DEBUG

说明:监控my_var变量的变化,如果变化,则打印新值。

注意:实际测试中,我们发现trap监控变量变化似乎会延迟,而set -x打印的信息是可靠的。

特殊变量

参考:https://www.gnu.org/software/bash/manual/html_node/Bash-Variables.html

  • BASH_LINENO: 这是一个数组变量,‌用于表示当前执行的脚本或函数中每一行的行号。‌它特别适用于调试,‌因为它可以追踪到函数调用栈中每一层的行号。‌通过访问BASH_LINENO数组的不同索引,‌可以获取到不同函数调用层级对应的行号信息‌。
  • LINENO: 获取当前脚本行号‌,‌而不涉及函数调用栈的追踪‌。
  • FUNCNAME: 函数名
  • BASH_SOURCE

判断文件是否存在

相关函数

  • access
  • stat
  • inotify_node
  • opendir
  • readdir

注意事项

文件系统并不会实时刷新缓存,尤其是在网络文件系统中。这会导致文件即使已经创建,accessstat函数依然返回”No such file”。
但是ls可以看到文件,这是因为lsaccess函数的实现机制不同。

例如:

NFS文件系统中,创建文件之后立即调用stat命令查看文件,stat会报告“文件不存在”。
删除文件之后立即调用stat命令查看文件,发现stat依然可以看到该文件。

我们可以改用readdir来读取目录,因为目录会被更快地更新缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
$ which rpcgen
/bin/rpcgen
$ rpcgen
usage: rpcgen infile
rpcgen [-abkCLNTM][-Dname[=value]] [-i size] [-I [-K seconds]] [-Y path] infile
rpcgen [-c | -h | -l | -m | -t | -Sc | -Ss | -Sm] [-o outfile] [infile]
rpcgen [-s nettype]* [-o outfile] [infile]
rpcgen [-n netid]* [-o outfile] [infile]
options:
-a generate all files, including samples
-b backward compatibility mode (generates code for SunOS 4.1)
-c generate XDR routines
-C ANSI C mode
-Dname[=value] define a symbol (same as #define)
-h generate header file
-i size size at which to start generating inline code
-I generate code for inetd support in server (for SunOS 4.1)
-K seconds server exits after K seconds of inactivity
-l generate client side stubs
-L server errors will be printed to syslog
-m generate server side stubs
-M generate MT-safe code
-n netid generate server code that supports named netid
-N supports multiple arguments and call-by-value
-o outfile name of the output file
-s nettype generate server code that supports named nettype
-Sc generate sample client code that uses remote procedures
-Ss generate sample server code that defines remote procedures
-Sm generate makefile template
-t generate RPC dispatch table
-T generate code to support RPC dispatch tables
-Y path directory name to find C preprocessor (cpp)

For bug reporting instructions, please see:
<http://www.gnu.org/software/libc/bugs.html>.

Note:

% 可以用来转义某行,任何句首带有 % 的行将被视作字符串,直接放入到输出文件中。
注意:rpcgen 可能改变该行的位置,所以应该在输出文件中对这些行进行仔细检查。

rpcgen provides an additional preprocessing feature: any line that begins with a percent sign (%) is passed directly to the output file, with no action on the line’s content. Use caution because rpcgen does not always place the lines where you intend. Check the output source file and, if needed, edit it.

例如

1
#include "abc.h"

当执行 rpcgen infile 命令时,由于 “abc.h” 不存在,可能会报错。

但是如果在句首添加一个 % 符号,则可以绕过检查。

1
%#include "abc.h"