当Nexys A7开发板上的8个LED灯开始有规律地流动闪烁时,很多FPGA初学者会认为这已经是Verilog学习的终点。但实际上,流水灯只是硬件描述语言世界的起点。通过重构这个经典案例,我们可以打开状态机设计的大门,探索PWM调光、交互控制等更丰富的应用场景。
在开始状态机改造之前,让我们先快速回顾传统流水灯的实现方式。典型的计数器驱动方案通常包含以下核心代码:
verilog复制module led(
input CLK100MHZ,
input CPU_RESETN,
output reg [7:0] LED
);
reg [31:0] counter;
always @(posedge CLK100MHZ or negedge CPU_RESETN) begin
if(!CPU_RESETN)
counter <= 0;
else if(counter == 32'd199_999_999)
counter <= 0;
else
counter <= counter + 1;
end
always @(posedge CLK100MHZ or negedge CPU_RESETN) begin
if(!CPU_RESETN)
LED <= 8'b0000_0000;
else case(counter)
32'd24_999_999: LED <= 8'b0000_0001;
32'd49_999_999: LED <= 8'b0000_0010;
// ... 其他LED切换点
32'd199_999_999: LED <= 8'b1000_0000;
endcase
end
endmodule
这种实现方式虽然简单直接,但存在几个明显局限:
状态机(Finite State Machine, FSM)是数字系统设计的核心范式,特别适合处理具有明确状态转移的逻辑。将流水灯重构为状态机形式,代码结构和可维护性将得到显著提升。
Verilog中状态机通常包含三个基本部分:
以下是重构后的状态机版本:
verilog复制module led_fsm(
input CLK100MHZ,
input CPU_RESETN,
output reg [7:0] LED
);
// 状态定义
parameter S0 = 0, S1 = 1, S2 = 2, S3 = 3,
S4 = 4, S5 = 5, S6 = 6, S7 = 7;
reg [2:0] state;
reg [31:0] timer;
// 状态转移逻辑
always @(posedge CLK100MHZ or negedge CPU_RESETN) begin
if(!CPU_RESETN) begin
state <= S0;
timer <= 0;
end
else if(timer == 32'd24_999_999) begin
timer <= 0;
case(state)
S0: state <= S1;
S1: state <= S2;
// ... 其他状态转移
S7: state <= S0;
endcase
end
else
timer <= timer + 1;
end
// 输出逻辑
always @(*) begin
case(state)
S0: LED = 8'b0000_0001;
S1: LED = 8'b0000_0010;
// ... 其他状态输出
S7: LED = 8'b1000_0000;
endcase
end
endmodule
与传统实现相比,状态机方案具有以下优势:
| 特性 | 计数器方案 | 状态机方案 |
|---|---|---|
| 可读性 | 一般 | 优秀 |
| 扩展性 | 差 | 良好 |
| 调试便利性 | 困难 | 容易 |
| 功能组合 | 复杂 | 简单 |
| 状态管理 | 隐式 | 显式 |
状态机的显式状态管理使得添加新功能(如反向流动、跳跃模式)变得非常简单,只需修改状态转移逻辑即可。
脉冲宽度调制(PWM)是控制LED亮度的经典方法。通过状态机架构,我们可以轻松实现呼吸灯效果。
PWM通过调节信号占空比来控制平均功率输出。对于LED而言,占空比越大,亮度越高。
code复制PWM周期
┌───────────────────────────────────────┐
│ │
│┌───────┐ ┌───────┐ ┌───────┐ │
││ │ │ │ │ │ │
││ 高 │ │ 高 │ │ 高 │ │
││ 电 │ │ 电 │ │ 电 │ │
││ 平 │ │ 平 │ │ 平 │ │
│└───────┘ └───────┘ └───────┘ │
└───────────────────────────────────────┘
<---占空比=50%--->
verilog复制module breath_led(
input CLK100MHZ,
input CPU_RESETN,
output reg LED
);
parameter IDLE = 0, UP = 1, DOWN = 2;
reg [1:0] state;
reg [31:0] pwm_counter;
reg [7:0] duty_cycle;
reg [31:0] speed_ctrl;
always @(posedge CLK100MHZ or negedge CPU_RESETN) begin
if(!CPU_RESETN) begin
state <= IDLE;
pwm_counter <= 0;
duty_cycle <= 0;
speed_ctrl <= 0;
end
else begin
// PWM周期计数器
pwm_counter <= (pwm_counter == 32'd255) ? 0 : pwm_counter + 1;
// 亮度调节速度控制
speed_ctrl <= (speed_ctrl == 32'd50_000) ? 0 : speed_ctrl + 1;
// 状态转移逻辑
case(state)
IDLE: begin
state <= UP;
duty_cycle <= 0;
end
UP: begin
if(speed_ctrl == 0) begin
if(duty_cycle == 8'd255)
state <= DOWN;
else
duty_cycle <= duty_cycle + 1;
end
end
DOWN: begin
if(speed_ctrl == 0) begin
if(duty_cycle == 8'd0)
state <= UP;
else
duty_cycle <= duty_cycle - 1;
end
end
endcase
// PWM输出
LED <= (pwm_counter < duty_cycle) ? 1'b1 : 1'b0;
end
end
endmodule
将单个呼吸灯扩展到8个LED,可以创建出更丰富的视觉效果。通过为每个LED设置不同的相位偏移,可以实现波浪式的呼吸效果:
verilog复制parameter PHASE_SHIFT = 32; // 相位偏移量
// 为每个LED计算相位偏移后的duty cycle
wire [7:0] led_duty [0:7];
genvar i;
generate
for(i=0; i<8; i=i+1) begin: led_gen
assign led_duty[i] = (duty_cycle + i*PHASE_SHIFT) % 256;
end
endgenerate
// LED输出
always @(*) begin
for(i=0; i<8; i=i+1)
LED[i] = (pwm_counter < led_duty[i]) ? 1'b1 : 1'b0;
end
Nexys A7板载的按钮可以用于增强交互性。我们可以实现以下控制功能:
机械按钮需要去抖动处理以获得稳定的输入信号:
verilog复制module debounce(
input clk,
input button_in,
output reg button_out
);
reg [19:0] counter;
reg button_sync;
always @(posedge clk) begin
button_sync <= button_in;
if(button_sync ^ button_in) begin
counter <= 0;
end
else if(counter == 20'd1_000_000) begin
button_out <= button_sync;
end
else begin
counter <= counter + 1;
end
end
endmodule
整合多种灯光模式的状态机需要更精细的状态定义:
verilog复制parameter MODE_FLOW = 0, MODE_BREATH = 1, MODE_BLINK = 2;
parameter DIR_FORWARD = 0, DIR_BACKWARD = 1;
reg [1:0] current_mode;
reg current_dir;
reg [31:0] speed;
always @(posedge CLK100MHZ or negedge CPU_RESETN) begin
if(!CPU_RESETN) begin
current_mode <= MODE_FLOW;
current_dir <= DIR_FORWARD;
speed <= 32'd24_999_999; // 默认速度
end
else begin
// 模式切换逻辑
if(btnr_debounced) begin
current_mode <= (current_mode == MODE_BLINK) ?
MODE_FLOW : current_mode + 1;
end
// 方向切换逻辑
if(btnl_debounced) begin
current_dir <= ~current_dir;
end
// 速度调节逻辑
if(btnu_debounced && speed > 32'd1_000_000) begin
speed <= speed - 32'd1_000_000;
end
if(btnd_debounced && speed < 32'd49_999_999) begin
speed <= speed + 32'd1_000_000;
end
end
end
最后,我们需要根据当前模式选择适当的输出逻辑:
verilog复制always @(*) begin
case(current_mode)
MODE_FLOW: begin
// 流水灯输出逻辑
case(state)
S0: LED = 8'b0000_0001;
S1: LED = 8'b0000_0010;
// ...
endcase
if(current_dir == DIR_BACKWARD) begin
// 反向流动处理
LED = {LED[0], LED[7:1]};
end
end
MODE_BREATH: begin
// 呼吸灯输出逻辑
for(i=0; i<8; i=i+1)
LED[i] = (pwm_counter < led_duty[i]) ? 1'b1 : 1'b0;
end
MODE_BLINK: begin
// 闪烁模式
LED = (blink_counter[24]) ? 8'hFF : 8'h00;
end
endcase
end
在FPGA设计中,资源利用和时序性能是需要重点考虑的因素。以下是一些优化技巧:
避免使用过高的时钟频率进行简单操作:
verilog复制// 生成1Hz时钟使能信号
reg [25:0] clk_div;
wire clk_1hz_en = (clk_div == 0);
always @(posedge CLK100MHZ) begin
if(clk_div == 26'd100_000_000)
clk_div <= 0;
else
clk_div <= clk_div + 1;
end
// 使用时序使能而非分频时钟
always @(posedge CLK100MHZ) begin
if(clk_1hz_en) begin
// 低频逻辑处理
end
end
根据设计需求选择合适的状态编码方式:
| 编码方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 顺序二进制 | 简单直观 | 状态跳转可能产生毛刺 | 一般逻辑 |
| 格雷码 | 状态变化时只有1位改变 | 解码稍复杂 | 高速状态机 |
| One-hot | 译码简单,速度快 | 占用较多触发器 | 状态较多的设计 |
对于中等复杂度的设计,One-hot编码通常是较好的选择:
verilog复制parameter S0 = 8'b00000001,
S1 = 8'b00000010,
S2 = 8'b00000100,
// ...
S7 = 8'b10000000;
reg [7:0] state;
对于性能关键路径,可以考虑流水线化处理:
verilog复制// 非流水线设计
always @(posedge clk) begin
result <= complex_function(a, b, c);
end
// 流水线设计
reg [WIDTH-1:0] stage1, stage2;
always @(posedge clk) begin
stage1 <= partial_function1(a, b);
stage2 <= partial_function2(stage1, c);
result <= final_function(stage2);
end
通过SignalTap或ILA工具监控内部信号:
tcl复制# 在Quartus中创建SignalTap实例
create_instantiation -name stp1 -module signal_tap
set_instance_parameter_value stp1 {SLD_NODE_ENTITY} {stp1}
set_instance_parameter_value stp1 {SLD_NODE_INFO} {32768}
问题1:状态机卡在某个状态
问题2:PWM输出不稳定
问题3:按钮响应不灵敏
添加调试输出到顶层模块:
verilog复制output [7:0] DEBUG_STATE,
output [7:0] DEBUG_COUNTER
assign DEBUG_STATE = state;
assign DEBUG_COUNTER = timer[31:24];
在约束文件中分配调试引脚:
tcl复制set_location_assignment PIN_J1 -to DEBUG_STATE[0]
set_location_assignment PIN_J2 -to DEBUG_STATE[1]
# ... 其他引脚分配
掌握了状态机和PWM的基本原理后,可以尝试以下创意扩展:
利用板载麦克风输入,将音频信号转换为LED显示模式:
verilog复制// 简单的音频幅度检测
reg [15:0] audio_peak;
always @(posedge CLK100MHZ) begin
if(audio_sample > audio_peak)
audio_peak <= audio_sample;
else if(peak_counter == 16'hFFFF)
audio_peak <= audio_peak - 1;
end
// LED显示根据音频峰值变化
always @(*) begin
for(i=0; i<8; i=i+1)
LED[i] = (audio_peak > (i+1)*32);
end
结合板载温度传感器,创建温度可视化指示:
verilog复制// 温度范围映射到LED
always @(*) begin
case(temp_celsius)
0..10: LED = 8'b0000_0001;
11..20: LED = 8'b0000_0011;
21..30: LED = 8'b0000_0111;
// ...
default: LED = 8'b1111_1111;
endcase
end
实现简单的记忆游戏,用户需要重复LED的闪烁序列:
verilog复制// 游戏状态机
parameter GAME_IDLE = 0, GAME_SHOW = 1,
GAME_INPUT = 2, GAME_CHECK = 3;
// 随机序列生成
always @(posedge CLK100MHZ) begin
if(game_state == GAME_IDLE)
rand_seq <= {rand_seq[6:0], rand_seq[7]^rand_seq[5]^rand_seq[4]^rand_seq[3]};
end