1. 项目背景与核心价值
图书馆存包柜和乒乓球游戏这两个看似不相关的场景,恰恰是FPGA学习的绝佳练手项目。前者需要处理多路输入输出控制、状态存储和逻辑判断,后者则涉及实时信号处理、运动轨迹计算和人机交互设计。这两个项目组合在一起,几乎涵盖了FPGA开发中最核心的几大技术要点。
我在大学实验室带学生做FPGA实训时,发现初学者最常遇到的困境就是:学完Verilog语法后不知道如何应用到实际场景。这个组合项目正是为了解决这个问题而设计的——用生活化的场景理解硬件描述语言的精髓。存包柜系统需要处理24路柜门状态检测(红外传感器输入)、16位密码锁控制和LCD显示驱动;乒乓球游戏则要处理VGA显示时序、球拍位置检测(通过旋转编码器)和碰撞检测算法。
2. 硬件系统架构设计
2.1 存包柜控制系统
核心采用Xilinx Artix-7 FPGA(XC7A35T)作为主控,其逻辑单元数量(33,280 LUTs)足够支撑这两个系统的并行运行。柜体状态检测使用红外对管阵列(每柜门一组),通过74HC165并行转串行芯片将24路信号压缩到4个GPIO口。密码输入采用4x4矩阵键盘,显示部分用1602 LCD模块(通过HD44780控制器驱动)。
关键设计要点:红外传感器信号需做20ms消抖处理,在Verilog中采用状态机实现。密码存储使用FPGA内部的Block RAM,而非外部EEPROM,这样既简化电路又保证读写速度。
2.2 乒乓球游戏系统
VGA显示采用640x480@60Hz模式,时序生成代码如下:
verilog复制parameter HD = 640; // 行显示像素
parameter HF = 16; // 行前沿
parameter HB = 48; // 行后沿
parameter HR = 96; // 行同步脉冲
parameter VD = 480; // 场显示行数
parameter VF = 10; // 场前沿
parameter VB = 33; // 场后沿
parameter VR = 2; // 场同步脉冲
always @(posedge clk_25m) begin
if (h_count < HD + HF + HB + HR -1)
h_count <= h_count + 1;
else
h_count <= 0;
if (h_count == HD + HF + HB + HR -1) begin
if (v_count < VD + VF + VB + VR -1)
v_count <= v_count + 1;
else
v_count <= 0;
end
end
球拍控制采用EC11旋转编码器,通过检测AB相位的90°相位差判断转动方向。碰撞检测使用矩形包围盒算法,在球体坐标(x,y)与球拍/边界坐标比较时,加入2像素的容错阈值。
3. 关键模块实现细节
3.1 存包柜状态机设计
采用三段式状态机管理柜门状态:
verilog复制// 状态定义
localparam IDLE = 2'b00;
localparam CHECK = 2'b01;
localparam OPEN = 2'b10;
always @(posedge clk or posedge rst) begin
if (rst)
current_state <= IDLE;
else
current_state <= next_state;
end
always @(*) begin
case(current_state)
IDLE: if (sensor_trigger) next_state = CHECK;
else next_state = IDLE;
CHECK: if (pwd_correct) next_state = OPEN;
else next_state = IDLE;
OPEN: if (door_closed) next_state = IDLE;
else next_state = OPEN;
endcase
end
always @(posedge clk) begin
if (current_state == OPEN)
door_lock <= 1'b0; // 开锁
else
door_lock <= 1'b1; // 上锁
end
3.2 乒乓球运动轨迹算法
球的运动采用定点数运算(Q8.8格式),每帧更新位置:
verilog复制reg [15:0] ball_x; // 高8位整数,低8位小数
reg [15:0] ball_y;
reg [7:0] velocity_x;
reg [7:0] velocity_y;
always @(posedge vga_vsync) begin
// 边界碰撞检测
if (ball_x[15:8] >= 630 || ball_x[15:8] <= 10)
velocity_x <= -velocity_x;
if (ball_y[15:8] >= 470 || ball_y[15:8] <= 10)
velocity_y <= -velocity_y;
// 球拍碰撞检测
if ((ball_y[15:8] > paddle_y-5) &&
(ball_y[15:8] < paddle_y+15) &&
(ball_x[15:8] > paddle_x-5) &&
(ball_x[15:8] < paddle_x+30))
velocity_x <= -velocity_x + (paddle_move_dir ? 1 : -1);
// 位置更新
ball_x <= ball_x + { {8{velocity_x[7]}}, velocity_x };
ball_y <= ball_y + { {8{velocity_y[7]}}, velocity_y };
end
4. 系统集成与调试技巧
4.1 资源分配策略
在Vivado中合理配置约束文件:
tcl复制# 时钟约束
create_clock -period 40.000 -name clk [get_ports clk]
# 引脚分配
set_property PACKAGE_PIN R4 [get_ports {key_row[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {key_row[0]}]
# Block RAM配置
set_property RAM_STYLE BLOCK [get_cells {pwd_mem[*]}]
4.2 常见问题排查
-
VGA显示偏移:检查时序参数是否严格符合VESA标准,特别是前后沿宽度。实测发现HR=96和VR=2必须精确。
-
按键抖动异常:除了软件消抖,建议在硬件上并联0.1μF电容。示波器测量信号稳定时间应>15ms。
-
Block RAM读写冲突:对同一地址的读写操作必须跨时钟域同步,推荐使用XPM宏:
verilog复制xpm_memory_sdpram #(
.ADDR_WIDTH_A(6),
.ADDR_WIDTH_B(6),
.BYTE_WRITE_WIDTH_A(8),
.CLOCKING_MODE("common_clock"),
.MEMORY_SIZE(512)
) pwd_mem (
.dina(pwd_in), .addra(addr_w), .wea(we),
.addrb(addr_r), .doutb(pwd_out)
);
5. 进阶优化方向
- 动态难度调节:根据玩家连续得分情况,通过PWM调节球速:
verilog复制// 每得5分速度增加5%
if (score % 5 == 0 && score !=0)
velocity_x <= velocity_x + (velocity_x >> 4);
- 存包柜网络化管理:添加ESP32通过UART与FPGA通信,实现微信扫码开锁:
verilog复制// UART接收状态机
always @(posedge clk) begin
case(uart_state)
0: if (rx_data == 8'hAA) uart_state <= 1;
1: begin
locker_num <= rx_data[5:0];
uart_state <= 2;
end
2: if (rx_data == 8'h55) begin
open_cmd <= 1'b1;
uart_state <= 0;
end
endcase
end
- 三维显示效果:在VGA输出中加入Z-Buffer算法,用阴影效果模拟立体感:
verilog复制// 像素着色逻辑
assign pixel = (x > ball_x-5 && x < ball_x+5 &&
y > ball_y-5 && y < ball_y+5) ?
{8'hFF, 8'h00, 8'h00} : // 红色球体
(x%20==0 || y%20==0) ?
{8'h80, 8'h80, 8'h80} : // 灰色网格
{8'h00, 8'h00, 8'h00}; // 黑色背景
这个项目最让我惊喜的是学生们在实现基础功能后,自发添加的创意扩展——有人给存包柜加了温湿度监测,有人在乒乓球游戏里加入了音效芯片。FPGA的魅力正在于此:用硬件思维解决实际问题时,总能碰撞出意想不到的火花。建议初学者在完成核心框架后,不妨尝试把自己的生活场景也移植到开发板上,比如宿舍门禁系统或者课表提醒器,这样的实践比单纯看教程收获大得多。
