为何时钟always块中用非阻塞赋值无法同时读写寄存器数组?(康威生命游戏示例)
康威生命游戏Verilog实现的两种写法差异解析
我在做HDLBits上的康威生命游戏题目,这个题目本质是基于当前状态更新下一个状态的有限状态机。参考平台上Rule90和Rule110的示例,我一开始把输出寄存器q的读写都放在同一个时钟触发的always块里,用非阻塞赋值写了代码(错误代码如下),但仿真发现q在第2周期就变成全0。改成阻塞赋值后,又出现输出延迟、时序混乱。后来参考别人的方案,引入中间wire变量q_next,把计算逻辑放在组合逻辑always块里用阻塞赋值写,代码运行成功了。但我搞不懂两种写法为什么结果不一样,求技术解释。
错误代码
module top_module( input clk, input load, input [255:0] data, output [255:0] q ); reg [3:0] neighbor_cnt; reg [3:0] a, b, c, d; reg [7:0] N; always @ (posedge clk) begin if (load) q <= data; else begin for (int i=0; i<16; i++) begin:row for (int j=0; j<16; j++) begin:column a <= i-1; b <= i+1; c <= j-1; d <= j+1; //overflow handling, wrap around naturally N <= {q[a*16+c], q[a*16+d], q[b*16+c], q[b*16+d], q[i*16+c], q[i*16+d], q[a*16+j], q[b*16+j]}; neighbor_cnt <= N[0]+N[1]+N[2]+N[3]+N[4]+N[5]+N[6]+N[7]; case (neighbor_cnt) 2: q[i*16 + j] <= q[i*16 + j]; 3: q[i*16 + j] <= 1; default: q[i*16 + j] <= 0; endcase end end end end endmodule
正确代码
module top_module( input clk, input load, input [255:0] data, output [255:0] q ); reg [3:0] neighbor_cnt; reg [3:0] a, b, c, d; reg [7:0] N; wire [255:0] q_next; always @ (*) begin for (int i=0; i<16; i++) begin:row for (int j=0; j<16; j++) begin:column a = i-1; b = i+1; c = j-1; d = j+1; //overflow handling, wrap around naturally N = {q[a*16+c], q[a*16+d], q[b*16+c], q[b*16+d], q[i*16+c], q[i*16+d], q[a*16+j], q[b*16+j]}; neighbor_cnt = N[0]+N[1]+N[2]+N[3]+N[4]+N[5]+N[6]+N[7]; case (neighbor_cnt) 2: q_next[i*16 + j] = q[i*16 + j]; 3: q_next[i*16 + j] = 1; default: q_next[i*16 + j] = 0; endcase end end end always @(posedge clk) begin if (load) q <= data; else q <= q_next; end endmodule
技术差异解析
错误写法的核心问题
非阻塞赋值版本的问题
在时钟触发的always块中使用非阻塞赋值时,所有赋值操作的更新动作要等到本次时钟沿结束后才统一执行,这就导致你的代码出现了致命的逻辑依赖错误:
- 计算
neighbor_cnt时,你试图使用N的值,但N的非阻塞赋值还未生效,neighbor_cnt拿到的是N上一周期的旧值,而非当前循环计算出的新邻居状态。 - 嵌套循环中对
q每一位的赋值,本质是在同一个时钟沿内尝试多次更新q,但由于非阻塞赋值的队列特性,加上中间变量的计算错误,最终所有q的更新逻辑都基于错误的邻居计数,导致q快速变为全0。
阻塞赋值版本的问题
如果把时钟块里的非阻塞赋值改成阻塞赋值,虽然中间变量的计算会立即生效,但阻塞赋值是顺序执行的:
- 在循环计算
q的不同位时,前面位的更新会立即覆盖q的值,后面的位计算会用到已经更新过的q值(而非原始的当前状态)。但康威生命游戏要求所有细胞的新状态必须基于完整的当前全局状态计算,不能用部分更新后的状态,这就导致时序混乱、状态更新延迟错误。
正确写法的合理性
正确写法通过拆分计算与同步逻辑,完美匹配了康威生命游戏的规则:
- 组合逻辑块计算下一个状态:
always @(*)是组合逻辑块,使用阻塞赋值确保所有计算都基于q的当前完整状态,每一步中间值(a/b/N/neighbor_cnt)都会立即生效,能正确算出每个细胞的邻居数量,进而得到完整的q_next(所有细胞的下一个状态)。
- 时钟块统一更新状态:
- 单独的时钟触发
always块只负责在时钟沿到来时,把预计算好的q_next整体赋值给q,保证了所有细胞的状态同时更新,完全符合"基于当前全局状态计算新状态,再同步切换"的要求。
- 单独的时钟触发
总结
康威生命游戏的核心要求是所有细胞的新状态必须基于当前时刻的完整全局状态计算,错误写法要么因为非阻塞赋值的延迟特性导致计算用了旧中间值,要么因为阻塞赋值的顺序执行导致用了部分更新的状态;而正确写法通过拆分组合逻辑计算下一个状态、同步块统一更新,完美满足了规则要求。
内容的提问来源于stack exchange,提问作者Jasminy
相关产品推荐
相关产品推荐

