本文在AI的辅助下为ODL(open_digital_lib)仓库增加一个基本模块,通过开源实践观察AI在工程设计中的微观表现。
大模型在生成verilog代码中使用“功能等价”来达到“功能正确”的目标,在某种程度上破坏了微架构设计,需要对生成的代码进行进一步的检视与修正,以对齐原始设计意图。

1. 设计目标:位串行乘法器
输入:图片

预期:输出参数化的设计代码并完成验证。
模型:codex 5.5 - high
2. 功能正确:大模型的基本能力
这次要生成的“位串行乘法器”是教科书级别的经典电路,不存在特殊定制的规格。
在大模型能力迅速迭代的当下,正确识别设计图并生成代码应该是基本能力。
不到12分钟就完成了代码的生成跟验证过程:

原图只有数据通路,模块接口和控制通路都是由大模型推断出来的,直接补齐了几个关键信息:
增加控制输入输出时序的接口:start_i/busy_o/done_o
内部增加了控制位串行计算过程的计数器
增加参数N用于配置乘法器位宽
如果只是Vibe coding,那么看到验证通过也就可以结束了。
但是大部分芯片设计工程师不会把未经检视的RTL代码直接拿去流片。
而这,才是一切的起点。
3. 微架构对齐
为了将生成的代码严格对齐到设计图的结构,以得到一份高质量、高可读性的代码,还做了额外的修正,主要包括:
将行为级加法修正为bit-slice结构
优化FA输入端口的冗余MUX
优化product_o冗余的输出寄存器
将输出端口的done_o修正为valid_o
优化冗余的iter_idx信号
优化冗余的iter_valid信号
优化partial product
优化sum_1d_shift
下面是具体的过程。
将行为级加法修正为bit-slice微架构
打开生成的代码,看到很多控制信号,粗略一看都很正常。但是没有看到由全加器(FA)组成的结构,按照正常的设计思维,这应该是在代码里面一眼就能看到的核心逻辑:

仔细检查,才发现代码藏在一个角落:
assign add_result = {1'b0, acc_for_add} + ( x_bit ? {1'b0, y_for_add} : '0 );这里显然是把多个全加器组成的“半脉动阵列”结构直接优化成一个大位宽的加法器,丢失掉最关键的“加法器”和位输出打拍的细节,没有正确对齐设计图,最终会导致频率严重下降。
修正后,RTL数据通路与设计图微架构对齐,而不是仅保持功能等价。
优化FA输入端口的冗余MUX
全加器的输入端被额外增加了两个MUX:
assign y_for_add = start_accept ? y_i : y;assign acc_for_add = start_accept ? '0 : acc;
意图是:在start信号到来,开启一次乘法计算的时候,给全加器强制赋予一个初始值0。
但是在这个电路中,随着数据的流动,积的最后一比特结果被输出的时候,寄存器的数据都已经被清空,不需要额外的清零:

优化product_o冗余的输出寄存器
FA的输出多寄存一拍后才生成product_o。
可以由sum_1d[0]直接驱动,更符合设计图结构。
将输出端口的done_o修正为valid_o
输出端口使用done_o在最后一拍拉高来表示计算完成,这种方式更加适合结果并行输出的场景。
而 bit-serial multiplier 的输出是连续 2N 拍逐比特输出,因此更合理的方式是用持续拉高的valid_o来指示当前输出数据有效。
优化冗余的iter_idx信号
使用了额外的 iter_idx信号:
assign iter_idx = start_accept ? '0 : cnt;实际上cnt会在last_cycle后清零,新一轮 start_accept到来前,cnt已经清零,因此iter_idx是冗余的。
优化冗余的iter_valid信号
使用iter_valid表示当前周期是否执行一次迭代:
assign iter_valid = start_accept || busy;后续代码中iter_valid的使用范围逐渐收缩到只剩一次使用,为了减少中间信号,提高可读性,直接展开:
else if ( start_accept || busy ) beginsum_1d <= sum;carry_1d <= carry;cnt <= cnt + CNT_WIDTH'(1);end
优化partial product
中间版本曾使用条件选择生成 partial product:
assign partial_product = multiplier ? multiplicand_i : '0;虽然功能正确,但不能清晰表达出设计图中的结构。
修正后,将 multiplier 的补零行为和 partial product 的与门结构分开表达:
assign multiplier = ( cnt < CNT_WIDTH'(N) ) ? multiplier_i : 1'b0;assign partial_product = {N{multiplier}} & multiplicand_i;
其中multiplier表示在输入阶段使用外部串行输入,在flush阶段补 0;而partial_product则明确对应与门阵列。
优化sum_1d_shift
为兼容N=1,使用 generate 引入了不必要的参数分支,降低代码可读性:
generateif ( N == 1 ) begin : gen_sum_1d_shift_n1assign sum_1d_shift = '0;endelse begin : gen_sum_1d_shiftassign sum_1d_shift = {1'b0, sum_1d[N-1:1]};endendgenerate
修正后使用统一的右移表达式:
assign sum_1d_shift = sum_1d >> 1;附:经过多轮修正后的代码

调整前的代码:

夜雨聆风