FPGA开发板按键与LED控制实战:从消抖到状态机
1. 紫光盘古50K开发板按键与LED基础配置
作为一名长期从事嵌入式开发的工程师,第一次拿到紫光盘古50K开发板时,最直观的感受就是其丰富的外设接口和清晰的布局设计。这块开发板搭载了8个用户可编程按键(K1-K8)和8个LED指示灯,构成了我们实现按键控制LED循环点亮的基础硬件环境。
开发板的按键采用低电平有效设计,这意味着当按键按下时,对应的GPIO引脚会检测到低电平信号。这种设计在嵌入式系统中非常常见,主要基于两个考虑:一是大多数MCU/FPGA的上拉电阻配置更为稳定;二是在电路设计上,按键接地比接电源更不容易引入干扰。具体到盘古50K开发板,每个按键都通过10kΩ电阻上拉到3.3V,按下时通过机械触点连接到GND。
LED电路则是典型的三极管驱动方案。开发板上每个LED都通过一个PNP三极管(如S8550)进行驱动,这种设计可以:
- 提供足够的驱动电流(每个LED约15-20mA)
- 保护主控芯片的GPIO口不被大电流损坏
- 实现逻辑反向(GPIO输出低电平时LED点亮)
实际调试中发现,开发板原理图中LED驱动三极管的基极电阻为1kΩ,这意味着GPIO输出低电平时,三极管基极电流约为(3.3V-0.7V)/1kΩ=2.6mA,足以使三极管饱和导通。
2. 开发环境搭建与工程创建
在开始编码前,需要准备好完整的开发环境。紫光盘古50K开发板使用的是PGL50H FPGA芯片,配套的开发工具是Pango Design Suite(PDS)。我推荐安装最新版的PDS 2023.3,这个版本对50K系列的支持最为完善。
安装过程有几个关键点需要注意:
- 驱动程序安装时务必关闭所有杀毒软件,避免误拦截
- 安装路径不要包含中文或特殊字符
- 安装完成后需要重启电脑使驱动生效
新建工程时,选择正确的器件型号PGL50H-6FBG484。工程创建后,需要配置引脚约束文件(.xdc)。对于我们的LED控制实验,至少需要定义:
set_property -dict {PACKAGE_PIN AB12 IOSTANDARD LVCMOS33} [get_ports {led[0]}] set_property -dict {PACKAGE_PIN AB13 IOSTANDARD LVCMOS33} [get_ports {key[0]}]引脚分配信息可以在开发板配套的原理图中找到。建议将8个LED和8个按键的引脚一次性全部约束好,即使暂时只用其中几个,这样后续扩展功能时就不需要反复修改约束文件了。
3. Verilog按键消抖逻辑实现
按键消抖是嵌入式系统中最基础也最容易出问题的环节之一。机械按键在按下和释放时会产生5-10ms的抖动,如果不处理会导致单次按键被误识别为多次触发。
在FPGA中,我们通常采用状态机+计时器的消抖方案。下面是一个经过实际验证的可靠消抖模块代码:
module debounce ( input clk, // 50MHz时钟 input key_in, // 原始按键输入 output reg key_out // 消抖后输出 ); reg [19:0] cnt; reg key_reg; always @(posedge clk) begin key_reg <= key_in; if (key_reg != key_in) cnt <= 20'd100000; // 2ms计时(50MHz时钟) else if (cnt != 0) cnt <= cnt - 1; else key_out <= key_reg; end endmodule这个设计的工作原理是:
- 检测按键状态变化时启动计时器(2ms)
- 计时期间持续监测按键状态
- 只有计时结束且状态稳定的信号才会输出
实测发现,对于盘古开发板的按键,2ms消抖时间足够可靠。如果环境干扰较大,可以适当延长到5ms(修改cnt初始值为250000)。
4. LED循环点亮的状态机设计
实现LED循环点亮的核心是一个状态机,它根据按键输入改变LED的显示模式。我们设计一个包含4种显示模式的状态机:
- 全灭状态
- 从左到右流水灯
- 从右到左流水灯
- 呼吸灯效果
状态转换由按键K1控制,每按一次切换到下一个模式。状态机的Verilog实现如下:
module led_controller ( input clk, input rst_n, input key_press, output reg [7:0] led ); reg [1:0] state; reg [23:0] counter; reg [3:0] pos; reg pwm_dir; // 状态定义 localparam S_OFF = 2'b00; localparam S_LEFT = 2'b01; localparam S_RIGHT = 2'b10; localparam S_BREATH = 2'b11; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= S_OFF; counter <= 0; pos <= 0; pwm_dir <= 0; led <= 8'h00; end else begin counter <= counter + 1; // 按键处理 if (key_press) state <= state + 1; // 状态机 case (state) S_OFF: led <= 8'h00; S_LEFT: begin if (counter[20]) begin // 约0.5秒移位一次 pos <= pos + 1; led <= (8'h01 << pos); if (pos == 7) pos <= 0; end end S_RIGHT: begin if (counter[20]) begin pos <= pos + 1; led <= (8'h80 >> pos); if (pos == 7) pos <= 0; end end S_BREATH: begin if (counter[19:10] == 10'h3FF) // PWM周期1024个时钟 pwm_dir <= ~pwm_dir; led <= {8{counter[19:10] < (pwm_dir ? counter[9:0] : ~counter[9:0])}}; end endcase end end endmodule这个设计有几个值得注意的技巧:
- 使用counter的不同位域实现多速率定时(移位速度、PWM频率)
- 呼吸灯效果通过比较计数器的高低位实现,无需额外PWM模块
- 状态转换简洁明了,易于扩展新模式
5. 系统集成与功能验证
将各个模块集成到顶层文件中,完成最终的系统搭建:
module top ( input clk50m, input [7:0] key, output [7:0] led ); wire key_press; debounce debounce_inst ( .clk(clk50m), .key_in(~key[0]), // 注意按键是低电平有效 .key_out(key_press) ); led_controller led_ctl_inst ( .clk(clk50m), .rst_n(1'b1), // 开发板有专用复位电路 .key_press(key_press), .led(led) ); endmodule在实验室环境下进行功能验证时,建议按照以下步骤进行:
- 静态测试:不编程状态下,用万用表测量按键引脚电压(应为3.3V),按下时应变为0V
- 简单IO测试:编写最简单的LED点亮程序,验证基本IO功能
- 按键响应测试:确认按键消抖效果
- 完整功能测试:验证所有4种显示模式的正确切换
常见问题排查:
- 如果LED完全不亮:检查约束文件中的引脚分配是否正确,测量LED供电电压
- 如果按键无响应:检查消抖模块的时钟输入是否正确,测量按键信号是否到达FPGA引脚
- 如果显示模式切换不正常:用SignalTap逻辑分析仪抓取状态机信号
6. 性能优化与扩展思路
基础功能实现后,可以考虑以下几个优化方向:
- 增加模式记忆功能:利用FPGA的配置Flash存储当前模式,断电后不丢失
// 在led_controller模块中添加 reg [1:0] saved_state; always @(posedge clk) begin if (key_press) begin saved_state <= state + 1; // 这里需要调用Flash编程IP核保存状态 end end- 实现加速度检测:按键按住时间越长,LED流动速度越快
// 修改led_controller中的计数器判断 wire [3:0] speed = (key_hold_time < 10) ? 20 : (key_hold_time < 20) ? 19 : 18; if (counter[speed]) begin // 动态速度控制 pos <= pos + 1; // ... end- 添加音频反馈:为每次按键操作添加"滴"声提示
module beep ( input clk, input trigger, output pwm_out ); // 简单的音频发生器实现 endmodule- 多按键组合控制:利用K1-K8实现更复杂的控制逻辑
wire [7:0] keys_pressed; debounce debounce_inst[7:0] (.clk(clk50m), .key_in(~key), .key_out(keys_pressed)); always @(*) begin case (keys_pressed) 8'b00000001: // 仅K1按下 8'b00000011: // K1和K2同时按下 // ...其他组合 endcase end在实际项目中,我发现紫光盘古50K开发板的GPIO驱动能力相当不错,可以直接驱动小型继电器模块。这意味着这个LED控制项目可以轻松扩展为实际的工业控制应用,比如流水线指示灯控制、设备状态显示等。FPGA的并行处理特性使其特别适合这类需要实时响应的控制场景。
