从Arduino到FPGA:我的第一个Verilog项目踩坑实录
第一次把Arduino的流水灯代码移植到FPGA开发板时,屏幕上的波形仿真让我彻底懵了——八个LED灯居然同时亮起!这个看似简单的项目暴露了嵌入式软件工程师转向硬件描述语言时最典型的思维误区。本文将用真实项目中的五个关键错误,带你重新理解Verilog的并行世界。
1. 当顺序执行思维遇上硬件并行性
在Arduino上实现流水灯,我们会习惯性地写出这样的C代码:
void loop() { for(int i=0; i<8; i++) { digitalWrite(ledPins[i], HIGH); delay(500); digitalWrite(ledPins[i], LOW); } }当我试图用类似的思路编写Verilog时,灾难开始了。最初的尝试是这样的:
always @(posedge clk) begin for(i=0; i<8; i=i+1) begin led <= 8'b1 << i; #500; // 试图模拟延时 end end这个代码会产生三个致命问题:
#500延时语法只能在测试基准中使用- for循环在硬件中会展开为并行电路
- 非阻塞赋值
<=导致所有LED同时变化
硬件设计的第一个思维转换:必须用状态机替代顺序流程。修正后的方案:
reg [2:0] state; always @(posedge clk) begin case(state) 0: led <= 8'b00000001; 1: led <= 8'b00000010; // ...其他状态 7: led <= 8'b10000000; default: led <= 8'b00000000; endcase state <= (state == 7) ? 0 : state + 1; end2. always块的触发条件陷阱
第二个坑出现在按键消抖模块。参考Arduino的写法,我最初实现了这样的检测逻辑:
always begin if(button_pressed) begin debounced <= 1; #20; // 试图消抖 debounced <= 0; end end这个代码的问题在于:
- 缺少敏感列表导致仿真挂起
- 延时语句会阻塞整个always块
- 多个非阻塞赋值产生竞争条件
正确的消抖方案应该使用时钟同步:
reg [19:0] counter; always @(posedge clk) begin if(button_pressed) begin if(counter < 1000000) // 约20ms@50MHz counter <= counter + 1; end else begin counter <= 0; debounced <= 0; end if(counter == 999999) debounced <= 1; end3. assign与always的职责边界
最让我困惑的是何时使用assign,何时用always。在实现PWM调光时,我曾错误地将组合逻辑放在always块中:
// 错误示例 always @(*) begin pwm_out = (counter < duty_cycle) ? 1'b1 : 1'b0; end虽然仿真通过,但综合后的电路占用了额外触发器。黄金法则:
assign用于纯组合逻辑连线always @(*)用于需要if/case的复杂组合逻辑always @(posedge clk)用于时序逻辑
优化后的PWM生成:
// 计数器用时序逻辑 always @(posedge clk) counter <= (counter >= period) ? 0 : counter + 1; // 比较用组合逻辑 assign pwm_out = (counter < duty_cycle);4. 阻塞与非阻塞赋值的血泪教训
在实现状态机时,我曾混合使用两种赋值方式导致仿真与硬件行为不一致:
// 危险代码 always @(posedge clk) begin if(condition) begin a = b & c; // 阻塞赋值 d <= a | e; // 非阻塞赋值 end end必须遵守的编码规范:
| 赋值类型 | 使用场景 | 示例 | 综合结果 |
|---|---|---|---|
| 阻塞(=) | 组合逻辑always块 | a = b & c; | 直接连线 |
| 非阻塞(<=) | 时序逻辑always块 | q <= d; | D触发器 |
修正后的安全写法:
always @(posedge clk) begin a <= b & c; // 全部使用非阻塞 d <= a | e; end always @(*) begin x = y | z; // 组合逻辑用阻塞 end5. 信号类型的认知升级
从软件转硬件最难理解的概念莫过于wire和reg的区别。我曾错误地认为:
- reg等同于变量
- wire等同于常量
实际上它们的本质区别是:
- wire表示物理连线,必须由assign或模块端口驱动
- reg表示存储单元,但不一定综合成寄存器
典型错误案例:
wire counter; // 错误!计数器需要状态保持 always @(posedge clk) counter <= counter + 1;正确的信号声明原则:
reg [31:0] counter; // 需要时钟更新的信号 wire data_ready; // 组合逻辑输出 assign data_ready = (counter == MAX_COUNT);6. 实战:重构流水灯项目
综合以上教训,这是最终优化的流水灯实现:
module led_flow( input clk, output reg [7:0] leds ); reg [24:0] counter; reg [2:0] state; always @(posedge clk) begin counter <= (counter >= 5000000) ? 0 : counter + 1; if(counter == 0) begin case(state) 0: leds <= 8'b00000001; 1: leds <= 8'b00000010; // ...省略其他状态 7: leds <= 8'b10000000; default: leds <= 8'b00000000; endcase state <= (state == 7) ? 0 : state + 1; end end endmodule关键优化点:
- 使用25位计数器实现精确时序控制
- 状态机替代for循环
- 统一使用非阻塞赋值
- 明确的reg/wire类型声明
移植过程中最深的体会是:FPGA开发不是编写软件,而是在用代码"画电路图"。每个always块都对应着实际的硬件模块,每行代码都会变成门电路或触发器。这种思维转换的痛苦期大约持续了两周,但突破之后,看到自己设计的电路在示波器上完美运行时,那种成就感是单片机编程无法比拟的。