背景

前段时间,全重筹划开展智能电路设计工作,准备材料时经常需要一些电路原理图作为示例。 然而却没有现成的工具方便绘制这些原理图。 现有画原理图的工具需要一个一个元件手工码放、一条一条线手工连接,画一个小规模示例都需要非常繁重的操作。 4位加法器就包含了上百个晶体管,更大规模的例子手工码放,成本都快要接近从头设计一个4004了。 而真实的综合、布局布线工具不仅调用繁琐,最终画出的图也不美观。 因此,我一直以来都是自己写脚本来绘制这些图。

一开始是在Python中,通过PIL来画。 写了几个基本的函数,能在指定位置绘制晶体管和逻辑门, 但布局布线仍然需要手工设计,工作效率不高。 于是我决定给脚本加上简单的布局布线逻辑,实现从输入电路网表到输出原理图的全流程自动化。 以下是最终效果:

涉及到布局布线这样比较繁重的运算流程,我立刻决定抛弃Python,改用C++来实现,不然画一张图要等一天。 布线算法挪用了之前曾为绘制另一幅图写过的迷宫布线。 然后我直接将布线的代码贴给DeepSeek,让它给我补全布局的代码。 DeepSeek-V3-0324展现出非常强的代码能力,写出的代码立刻就能运行,功能完整。 在大语言模型的帮助下,加上一些搜索和一点巧思,抛弃PIL也并未带来多少痛苦。 DeepSeek提供了输出PNG的代码、抽取BDF点阵字体的代码,帮我调试了斜线栅格化算法中的Bug。

因此,我很快实现了这份约800行的自动电路布局布线程序,输入BLIF、输出PNG。 它完全自包含,依赖仅限于C++标准STL。 即使从小就在写程序,这次编码经历对我来说是全新的,展示了大语言模型时代对编程体验的重塑。 如果不是有大模型,去阅读PNG、BDF、DEFLATE这些标准的RFC然后去写底层函数,是非常痛苦的,我是断然不会去尝试的。

抛弃Python回归C++从未如此简单,想必抛弃CUDA亦如是。 在这个时代,生态壁垒正在被全方位削弱,这是令人兴奋的趋势!

代码

用到了C++26。当前只有GCC 15.0能编译,但15.0尚未发布,我为此从源码编译了最新的GCC。

这是我第一次大量使用ranges库,体验很好,很多时候能节省大量不必要的啰嗦。 当前的ranges还缺一些功能,例如反向的drop(从区间尾部去除一定数量的元素); 想从循环中得知是否是循环的第一次/最后一次执行还不方便,不如过去的旧写法。

C++标准还缺乏一些关键容器,比如不知为何至今没有trie? 一些老容器的性能一直被人诟病,也是时候翻新一遍了。 最新标准已经提供了flat_map,只是在我的场景里测试下来未见有性能优势;未见unordered_flat_map。 希望这些新功能要避免重走regex的老路才好。

展开代码

附一段DeepSeek-R1对代码的解析:

展开解析
这个程序是一个用于生成标准CMOS电路原理图的工具,支持从BLIF格式输入并输出PNG图像。以下是其核心功能和工作流程的解读:
  1. 输入解析(parse函数)

    • 功能:解析BLIF文件,提取电路的输入、输出端口及逻辑门信息。
    • 关键数据结构:
      • inputs/outputs:存储输入/输出端口的ID。
      • gates:以哈希表形式存储逻辑门,键为门ID,值为门类型(如NOT、NAND)及其连接的端口列表。
      • names:将门类型名称映射为唯一ID,支持动态扩展。
    • 处理逻辑:
      • 通过逐行解析.inputs.outputs.subckt段,提取端口和子电路信息。
      • 使用parse_name函数将字符串类型的门名称转换为整数ID,便于后续处理。
  2. 布局优化(place函数)

    • 算法:基于模拟退火算法进行布局优化,目标是最小化布线成本和面积。
    • 关键参数:
      • initial_temperature:初始温度,控制算法早期接受较差解的概率。
      • cooling_rate:冷却速率,逐步降低温度以收敛到最优解。
      • area_costcross_track_penalty:分别惩罚面积和交叉布线。
    • 流程:
      • 初始布局:随机生成门的位置,确保无重叠。
      • 迭代优化:通过调整门的位置,计算新的布局成本,根据Metropolis准则接受或拒绝新解。
      • 终止条件:温度降至阈值或超时。
  3. 布线(route函数)

    • 目标:为逻辑门之间的连接生成路径,避免冲突。
    • 关键步骤:
      • 障碍标记:使用mask数组标记不可布线区域(如门周围的空间)。
      • 路径搜索:基于BFS算法,从目标端口反向搜索到源端口,记录路径。
      • 成本计算:累加路径长度和交叉轨道惩罚。
    • 输出:
      • paths:存储所有连接的路径坐标。
      • solders:记录焊点位置。
  4. 绘图(draw_t结构体)

    • 功能:将布局和布线结果绘制为PNG图像。
    • 核心方法:
      • 基本图形:lineellipse用于绘制线段和焊点。
      • 逻辑门绘制:nmospmosinvnand等函数实现CMOS门的具体绘制。
      • 文本标签:text函数利用预定义字体位图标注端口名称。
    • PNG生成:通过手动构造PNG文件格式(包括IHDR、IDAT、IEND块),结合DEFLATE压缩和CRC校验,输出最终图像。
  5. 主流程(main函数)

    • 解析输入:读取BLIF文件,提取电路信息。
    • 布局优化:调用place生成初始布局。
    • 网格对齐:通过snap_to_microgrid将布局坐标对齐到微网格。
    • 多次布线尝试:逐步增加松弛度(slackness),调用route进行布线,直至成功。
    • 生成图像:成功布线后,调用draw_schematic绘制原理图并保存为PNG。

关键技术与挑战

  • 模拟退火算法:平衡全局搜索与局部优化,避免陷入局部最优。
  • 路径搜索算法:在动态障碍环境中高效寻找可行路径。
  • 低层级绘图:直接操作像素数据并生成符合PNG标准的文件,需处理压缩和校验细节。

总结

该程序通过解析电路描述、优化布局、自动布线和绘图,实现了从逻辑描述到原理图的自动化生成。其核心优势在于结合了算法优化(模拟退火)与底层图形处理,适用于中小规模CMOS电路的快速可视化。

使用

输入读取的是BLIF格式的网表。 可以用开源的Yosys,从Verilog综合得到。

例如先让DeepSeek给我写一个4位加法器作例子:

adder4.v
1
2
3
4
5
6
7
8
9
10
11
12
module adder4 (
input [3:0] a, // 4位输入a
input [3:0] b, // 4位输入b
input cin, // 进位输入
output [3:0] sum, // 4位和输出
output cout // 进位输出
);

// 使用连续赋值语句实现加法器
assign {cout, sum} = a + b + cin;

endmodule

写一个标准单元库。 当前我在代码里只放了非、与非两种门,要告诉Yosys只用这两种单元。

nandcell.lib
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
library(demo) {
cell(NOT) {
area: 3;
pin(A) { direction: input; }
pin(Y) { direction: output;
function: "A'"; }
}
cell(NAND) {
area: 4;
pin(A) { direction: input; }
pin(B) { direction: input; }
pin(Y) { direction: output;
function: "(A*B)'"; }
}
}

启动Yosys,在其中运行这些命令:

1
2
3
4
5
6
7
read_verilog adder4.v
hierarchy -check
abc -liberty nandcell.lib
synth
splitnets -ports
opt
write_blif -top adder4

最后一步会输出BLIF格式的网表,复制保存到文件adder4.blif里。

adder4.blif
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
# Generated by Yosys 0.51+104 (git sha1 c08f72b80, g++ 13.3.0-6ubuntu2~24.04 -fPIC -O3)

.model adder4
.inputs a[0] a[1] a[2] a[3] b[0] b[1] b[2] b[3] cin
.outputs sum[0] sum[1] sum[2] sum[3] cout
.names $false
.names $true
1
.names $undef
.subckt NOT A=a[3] Y=$abc$170$new_n15
.subckt NOT A=b[3] Y=$abc$170$new_n16
.subckt NOT A=a[2] Y=$abc$170$new_n17
.subckt NOT A=b[2] Y=$abc$170$new_n18
.subckt NOT A=a[1] Y=$abc$170$new_n19
.subckt NOT A=b[1] Y=$abc$170$new_n20
.subckt NOT A=a[0] Y=$abc$170$new_n21
.subckt NOT A=b[0] Y=$abc$170$new_n22
.subckt NOT A=cin Y=$abc$170$new_n23
.subckt NAND A=a[3] B=b[3] Y=$abc$170$new_n24
.subckt NAND A=a[2] B=b[2] Y=$abc$170$new_n25
.subckt NAND A=$abc$170$new_n17 B=$abc$170$new_n18 Y=$abc$170$new_n26
.subckt NAND A=$abc$170$new_n25 B=$abc$170$new_n26 Y=$abc$170$new_n27
.subckt NOT A=$abc$170$new_n27 Y=$abc$170$new_n28
.subckt NAND A=a[1] B=b[1] Y=$abc$170$new_n29
.subckt NAND A=a[0] B=b[0] Y=$abc$170$new_n30
.subckt NAND A=$abc$170$new_n21 B=$abc$170$new_n22 Y=$abc$170$new_n31
.subckt NAND A=$abc$170$new_n30 B=$abc$170$new_n31 Y=$abc$170$new_n32
.subckt NOT A=$abc$170$new_n32 Y=$abc$170$new_n33
.subckt NAND A=cin B=$abc$170$new_n33 Y=$abc$170$new_n34
.subckt NAND A=$abc$170$new_n30 B=$abc$170$new_n34 Y=$abc$170$new_n35
.subckt NOT A=$abc$170$new_n35 Y=$abc$170$new_n36
.subckt NAND A=$abc$170$new_n19 B=$abc$170$new_n20 Y=$abc$170$new_n37
.subckt NAND A=$abc$170$new_n29 B=$abc$170$new_n37 Y=$abc$170$new_n38
.subckt NOT A=$abc$170$new_n38 Y=$abc$170$new_n39
.subckt NAND A=$abc$170$new_n35 B=$abc$170$new_n39 Y=$abc$170$new_n40
.subckt NAND A=$abc$170$new_n29 B=$abc$170$new_n40 Y=$abc$170$new_n41
.subckt NOT A=$abc$170$new_n41 Y=$abc$170$new_n42
.subckt NAND A=$abc$170$new_n28 B=$abc$170$new_n41 Y=$abc$170$new_n43
.subckt NAND A=$abc$170$new_n25 B=$abc$170$new_n43 Y=$abc$170$new_n44
.subckt NOT A=$abc$170$new_n44 Y=$abc$170$new_n45
.subckt NAND A=$abc$170$new_n15 B=$abc$170$new_n16 Y=$abc$170$new_n46
.subckt NAND A=$abc$170$new_n24 B=$abc$170$new_n46 Y=$abc$170$new_n47
.subckt NOT A=$abc$170$new_n47 Y=$abc$170$new_n48
.subckt NAND A=$abc$170$new_n44 B=$abc$170$new_n48 Y=$abc$170$new_n49
.subckt NAND A=$abc$170$new_n24 B=$abc$170$new_n49 Y=cout
.subckt NAND A=$abc$170$new_n23 B=$abc$170$new_n32 Y=$abc$170$new_n51
.subckt NAND A=$abc$170$new_n34 B=$abc$170$new_n51 Y=$abc$170$new_n52
.subckt NOT A=$abc$170$new_n52 Y=sum[0]
.subckt NAND A=$abc$170$new_n36 B=$abc$170$new_n38 Y=$abc$170$new_n54
.subckt NAND A=$abc$170$new_n40 B=$abc$170$new_n54 Y=$abc$170$new_n55
.subckt NOT A=$abc$170$new_n55 Y=sum[1]
.subckt NAND A=$abc$170$new_n27 B=$abc$170$new_n42 Y=$abc$170$new_n57
.subckt NAND A=$abc$170$new_n43 B=$abc$170$new_n57 Y=$abc$170$new_n58
.subckt NOT A=$abc$170$new_n58 Y=sum[2]
.subckt NAND A=$abc$170$new_n45 B=$abc$170$new_n47 Y=$abc$170$new_n60
.subckt NAND A=$abc$170$new_n49 B=$abc$170$new_n60 Y=$abc$170$new_n61
.subckt NOT A=$abc$170$new_n61 Y=sum[3]
.end

编译cirschem.cppg++ cirschem.cpp -std=c++26 -O3

运行:./a.out adder4.blif

稍等半分钟布局布线完成,输出保存为schematic.png