背景
前段时间,全重筹划开展智能电路设计工作,准备材料时经常需要一些电路原理图作为示例。
然而却没有现成的工具方便绘制这些原理图。
现有画原理图的工具需要一个一个元件手工码放、一条一条线手工连接,画一个小规模示例都需要非常繁重的操作。
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图像。以下是其核心功能和工作流程的解读:
输入解析(parse函数)
- 功能:解析BLIF文件,提取电路的输入、输出端口及逻辑门信息。
- 关键数据结构:
inputs/outputs:存储输入/输出端口的ID。
gates:以哈希表形式存储逻辑门,键为门ID,值为门类型(如NOT、NAND)及其连接的端口列表。
names:将门类型名称映射为唯一ID,支持动态扩展。
- 处理逻辑:
- 通过逐行解析
.inputs、.outputs和.subckt段,提取端口和子电路信息。
- 使用
parse_name函数将字符串类型的门名称转换为整数ID,便于后续处理。
布局优化(place函数)
- 算法:基于模拟退火算法进行布局优化,目标是最小化布线成本和面积。
- 关键参数:
initial_temperature:初始温度,控制算法早期接受较差解的概率。
cooling_rate:冷却速率,逐步降低温度以收敛到最优解。
area_cost和cross_track_penalty:分别惩罚面积和交叉布线。
- 流程:
- 初始布局:随机生成门的位置,确保无重叠。
- 迭代优化:通过调整门的位置,计算新的布局成本,根据Metropolis准则接受或拒绝新解。
- 终止条件:温度降至阈值或超时。
布线(route函数)
- 目标:为逻辑门之间的连接生成路径,避免冲突。
- 关键步骤:
- 障碍标记:使用
mask数组标记不可布线区域(如门周围的空间)。
- 路径搜索:基于BFS算法,从目标端口反向搜索到源端口,记录路径。
- 成本计算:累加路径长度和交叉轨道惩罚。
- 输出:
绘图(draw_t结构体)
- 功能:将布局和布线结果绘制为PNG图像。
- 核心方法:
- 基本图形:
line、ellipse用于绘制线段和焊点。
- 逻辑门绘制:
nmos、pmos、inv、nand等函数实现CMOS门的具体绘制。
- 文本标签:
text函数利用预定义字体位图标注端口名称。
- PNG生成:通过手动构造PNG文件格式(包括IHDR、IDAT、IEND块),结合DEFLATE压缩和CRC校验,输出最终图像。
主流程(main函数)
- 解析输入:读取BLIF文件,提取电路信息。
- 布局优化:调用
place生成初始布局。
- 网格对齐:通过
snap_to_microgrid将布局坐标对齐到微网格。
- 多次布线尝试:逐步增加松弛度(
slackness),调用route进行布线,直至成功。
- 生成图像:成功布线后,调用
draw_schematic绘制原理图并保存为PNG。
关键技术与挑战
- 模拟退火算法:平衡全局搜索与局部优化,避免陷入局部最优。
- 路径搜索算法:在动态障碍环境中高效寻找可行路径。
- 低层级绘图:直接操作像素数据并生成符合PNG标准的文件,需处理压缩和校验细节。
总结
该程序通过解析电路描述、优化布局、自动布线和绘图,实现了从逻辑描述到原理图的自动化生成。其核心优势在于结合了算法优化(模拟退火)与底层图形处理,适用于中小规模CMOS电路的快速可视化。
使用
输入读取的是BLIF格式的网表。
可以用开源的Yosys,从Verilog综合得到。
例如先让DeepSeek给我写一个4位加法器作例子:
adder4.v1 2 3 4 5 6 7 8 9 10 11 12
| module adder4 ( input [3:0] a, input [3:0] b, input cin, output [3:0] sum, output cout );
assign {cout, sum} = a + b + cin;
endmodule
|
写一个标准单元库。
当前我在代码里只放了非、与非两种门,要告诉Yosys只用这两种单元。
nandcell.lib1 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.blif1 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.cpp:g++ cirschem.cpp -std=c++26 -O3
运行:./a.out adder4.blif
稍等半分钟布局布线完成,输出保存为schematic.png