1. 问题现象与背景解析
在FPGA开发过程中,使用Vivado进行综合时遇到Synth 8-6090错误是许多工程师都会经历的典型问题。这个错误的核心提示是:"variable 'n_state_fifo_poll' is written by both blocking and non-blocking assignments, entire logic could be removed"。简单来说,就是同一个变量在代码的不同位置被同时使用了阻塞赋值(=)和非阻塞赋值(<=)两种方式。
这个错误看似简单,但背后涉及Verilog语言的核心设计理念。在硬件描述语言中,赋值方式的选择直接决定了电路的行为特性。阻塞赋值(=)通常用于组合逻辑,其行为类似于软件编程中的顺序执行;而非阻塞赋值(<=)则用于时序逻辑,能够正确模拟寄存器传输级(RTL)的行为。
重要提示:在同一个always块中混用两种赋值方式是Verilog语法严格禁止的,这会导致综合工具无法正确推断电路结构,最终可能直接删除相关逻辑,造成功能失效。
2. 错误原因深度剖析
2.1 阻塞与非阻塞赋值的本质区别
阻塞赋值(=)的特点是:
- 立即执行,赋值语句右边的表达式计算完成后立即更新左边的变量
- 在同一个always块中,后续语句会"看到"这个新值
- 主要用于组合逻辑建模,如assign语句或组合always块
非阻塞赋值(<=)的特点是:
- 赋值操作被"调度"到当前仿真时间步结束时执行
- 在同一个always块中,后续语句看到的仍然是赋值前的旧值
- 主要用于时序逻辑建模,特别是时钟触发的always块
2.2 典型错误代码示例分析
让我们看一个典型的错误代码片段:
verilog复制always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_state_fifo_poll = IDLE; // 这里使用了阻塞赋值
end
else begin
n_state_fifo_poll <= NEXT_STATE; // 这里使用了非阻塞赋值
end
end
这段代码的问题在于:
- 在复位分支使用了阻塞赋值(=)
- 在正常工作分支使用了非阻塞赋值(<=)
- 综合工具无法确定设计者到底想要实现什么电路结构
2.3 综合工具的处理逻辑
当Vivado遇到这种混合赋值时,其处理流程如下:
- 语法分析阶段检测到赋值方式冲突
- 无法确定设计意图(是组合逻辑还是时序逻辑)
- 出于安全考虑,直接删除相关逻辑并报错
- 生成警告信息Synth 8-6090
3. 解决方案与最佳实践
3.1 统一赋值方式的基本原则
针对状态机设计,应遵循以下规则:
- 时序always块(带时钟触发的)统一使用非阻塞赋值(<=)
- 组合always块(不带时钟触发的)统一使用阻塞赋值(=)
- 不要在同一个always块中混用两种赋值方式
修正后的代码示例:
verilog复制always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_state_fifo_poll <= IDLE; // 统一使用非阻塞赋值
end
else begin
n_state_fifo_poll <= NEXT_STATE; // 统一使用非阻塞赋值
end
end
3.2 状态机设计的完整规范
对于FPGA中的状态机实现,推荐以下编码风格:
-
使用三段式状态机结构:
- 第一个always块:状态寄存器(时序逻辑,非阻塞赋值)
- 第二个always块:下一状态逻辑(组合逻辑,阻塞赋值)
- 第三个always块:输出逻辑(根据需求选择组合或时序)
-
状态定义使用parameter或localparam:
verilog复制localparam IDLE = 3'd0,
READ = 3'd1,
PROCESS = 3'd2,
WRITE = 3'd3;
- 完整的状态机示例:
verilog复制// 状态寄存器(时序逻辑)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
current_state <= IDLE;
else
current_state <= next_state;
end
// 下一状态逻辑(组合逻辑)
always @(*) begin
next_state = current_state; // 默认保持当前状态
case (current_state)
IDLE: if (start) next_state = READ;
READ: if (data_valid) next_state = PROCESS;
PROCESS: if (done) next_state = WRITE;
WRITE: if (ack) next_state = IDLE;
endcase
end
// 输出逻辑(组合逻辑)
always @(*) begin
// 默认输出
data_out = 0;
wr_en = 0;
case (current_state)
READ: rd_en = 1;
PROCESS: process_en = 1;
WRITE: begin
wr_en = 1;
data_out = processed_data;
end
endcase
end
4. 常见问题排查与调试技巧
4.1 Synth 8-6090相关问题的排查流程
当遇到这个错误时,建议按照以下步骤排查:
- 在Vivado中定位错误信息,找到具体的文件和行号
- 检查该变量在所有always块中的赋值方式
- 确认同一个always块中没有混用两种赋值方式
- 如果是状态机变量,检查是否所有分支都使用非阻塞赋值
- 使用Find in Files功能搜索变量名,确保全局一致性
4.2 其他相关警告与错误
与赋值方式相关的常见问题还包括:
-
Synth 8-3352:组合逻辑中使用了非阻塞赋值
- 解决方案:组合逻辑always块中统一使用阻塞赋值
-
Synth 8-27:不完全的条件语句导致锁存器
- 解决方案:组合逻辑中确保所有分支都被覆盖,或添加default分支
-
Synth 8-327:时序逻辑中使用了阻塞赋值
- 解决方案:时序逻辑always块中统一使用非阻塞赋值
4.3 仿真与调试技巧
-
使用ModelSim/QuestaSim进行仿真时:
- 特别注意阻塞和非阻塞赋值的行为差异
- 在波形窗口中观察关键信号的跳变沿
-
Vivado仿真技巧:
- 使用marker标记关键时间点
- 设置合适的仿真时间分辨率
- 添加必要的调试信号
-
实际调试建议:
- 使用ILA(Integrated Logic Analyzer)抓取实际硬件信号
- 逐步缩小问题范围,从顶层模块开始排查
- 对关键信号添加mark_debug属性
5. 高级应用与性能优化
5.1 状态机编码风格优化
-
二进制编码 vs 独热编码:
- 二进制编码:节省触发器,但状态解码复杂
- 独热编码:每个状态使用一个触发器,解码简单
- 根据状态数量和性能需求选择
-
安全状态机设计:
- 添加安全状态,防止进入非法状态
- 使用full_case和parallel_case指导综合器
- 考虑添加看门狗定时器监控状态机
5.2 时序收敛技巧
-
合理使用流水线:
- 将复杂组合逻辑拆分为多个时钟周期
- 平衡各阶段工作量
-
寄存器输出:
- 在关键路径添加寄存器
- 改善时序但增加延迟
-
逻辑复制:
- 对高扇出信号进行复制
- 减轻布线拥塞
5.3 资源优化策略
-
状态机优化:
- 合并相似状态
- 减少状态转移条件
-
资源共享:
- 识别可共享的运算单元
- 使用时序复用技术
-
存储器优化:
- 合理选择Block RAM或Distributed RAM
- 优化存储位宽和深度
6. 工程实践与经验分享
在实际项目中,我总结了以下经验教训:
-
编码规范的重要性:
- 建立团队统一的编码规范
- 使用脚本自动检查常见错误
- 定期进行代码审查
-
版本控制策略:
- 对关键修改添加详细注释
- 使用有意义的提交信息
- 重要版本打标签
-
文档记录:
- 维护设计文档记录关键决策
- 记录已知问题和解决方案
- 建立团队知识库
-
测试方法:
- 编写全面的测试用例
- 实现自动化回归测试
- 进行边界条件测试
在最近的一个高速数据采集项目中,我们遇到了类似Synth 8-6090的错误。问题最终追踪到一个第三方IP核的状态机实现中混用了赋值方式。通过以下步骤解决了问题:
- 隔离问题模块,创建最小复现环境
- 使用git bisect定位引入问题的提交
- 与IP提供商沟通获取更新版本
- 在本地进行完整回归测试
- 更新团队设计规范,避免类似问题
这个案例让我深刻体会到,即使是经验丰富的工程师,也可能在紧张的项目周期中忽视基础规范。建立严格的设计流程和代码审查机制,是预防这类问题的有效方法。
