任务介绍
本项目实现了2024年寒假练活动平台2:小脚丫FPGA套件的任务6,在小脚丫FPGA套件上,使用加速度传感器、按键、HDMI显示器与LCD屏幕,实现一款打砖块游戏机。
硬件平台
首先介绍本期活动的主角,小脚丫FPGA套件,这是一块功能很完善的综合FPGA开发套件,主控是一颗型号为MXO2的Lattice FPGA,芯片有4000多个LUT资源,96Kbit的用户闪存、92Kbit的RAM,资源足够完成大部分的中小项目。外设方面,除了核心板上自带的数码管,8个LED,2个三色LED,4位拨码开关和4位按键开关外。底板上也搭载了串口、显示屏、HDMI、网络模块、矩阵键盘,以及压力、姿态、接近、温湿度一系列传感器。可以说基本覆盖了日常开发中涉及到的大部分外设。
我接触FPGA的时间比较早,但是在日常开发中,因为单片机可以使用相对高级的语言、有完善的库和开发框架,所以对FPGA使用很少。正好可以通过这次活动,以做项目的形式,对FPGA开发进行一个更深入的了解。
任务分析与实现
接下来我们看一看这次主办方出的任务:
- 任务一和二是相对比较基础的数电实验,计算器和数字钟,可能许多同学在校期间的数电课程设计就做过,这次是要用FPGA实现出来
- 从任务三开始难度上升,需要一个上位机向开发板发送数据,开发板解析和播放
- 任务四是传感器采集和显示,覆盖外设比较多
- 任务五是简易波形发生和示波器,比较考察代码的编写能力,适合想学习信号处理和示波器原理的同学
- 最后任务六是做一个游戏机,使用按键或姿态传感器控制,用HDMI和板载LCD屏幕输出图像
其实这块开发板上的外设虽然多,但我们在日常单片机开发中大部分已经接触到,只是控制器不同。但是这次有一个不一样的外设,我在第一次看到时就产生了兴趣,那就是HDMI接口。于是我选择了任务六,一方面想尝试一下用HDMI接口控制显示器的感觉,另一方面,做个游戏机也更有趣。
把任务六的系统按如图拆解一下,可以分为核心逻辑和外设模块。从左到右整体呈一个输入到输出的关系:核心的游戏模块从拨码开关、键盘和加速度传感器获取控制数据,按游戏逻辑处理后,输出游戏图像给LCD或HDMI进行显示。
模块拆好了,接下来的工作就是将他们一一实现,并组装起来。
接下来是各模块的具体实现,我们自顶向下地分析,顶层模块top.v控制了几种主要模块的互联。
和正常的软件工程一样,FPGA的代码也遵循高内聚、低耦合的原则:各模块的内部逻辑是相对独立的,只留一个通用的接口给外部调用,只不过Verilog代码设计的接口是一个真实的物理连接。
在显示方面,LCD和HDMI的接口非常相似,可以分为通用数据和专有信号两类,通用信号有复位、XY坐标和颜色,专用信号有各自的时钟、SPI的接口、HDMI的差分输出信号。因为在FPGA里,LCD和HDMI的驱动逻辑是在并行工作的,我们只需要增加一个用拨码开关控制的复用器逻辑,就可以选择是把游戏图像数据提供给LCD还是HDMI。有些遗憾的是,这块开发板上HDMI和LCD共用了几个物理引脚,不能实现同时显示的效果。
关键代码
接下来我通过代码来讲解本次实现的主要部分:
顶层模块
- PLL使用FPGA自带的IP核,负责提供各模块所需的时钟。
- game是核心的游戏逻辑,这里我们实现了一个简易的打砖块游戏,xy是屏幕像素坐标,显示模块从这里取像素信号的时候会需要,rgb则是输出的三通道颜色数据,视频;left、right信号控制平板的左右移动,
- screen_driver是lcd显示模块,它的输出xy像素坐标信号、输入颜色数据,其次是一些SPI接口;
- hdmi模块类似,不一样的是它要输出HDMI三路差分信号,以及相应的信号时钟。
module top(
/* synthesis syn_force_pads = 1 */
input sys_clk ,
/* synthesis syn_force_pads = 1 */
input sys_rst_n ,
input btnL,
input btnR,
/* synthesis syn_force_pads = 1 */
input dip_switch ,
output i2c_scl,
inout i2c_sda,
/* synthesis syn_force_pads = 1 */
output [3:0] led ,
output [2:0] TMDSp, TMDSn,
output TMDSp_clock, TMDSn_clock
);
pll pll_u0 (
.CLKI(sys_clk ),
.CLKOP(clk_TMDS ),
.CLKOS(clk_25m)
);
game game_u0(
.clk(sys_clk),
.rst(sys_rst_n),
.x(game_x),
.y(game_y),
.br_vga_r(red),
.br_vga_g(green),
.br_vga_b(blue),
.done(),
.start(1'b1),
.left(final_L),
.right(final_R)
);
lcd lcd_u0(
.sys_clk ( sys_clk ),
.sys_rst_n ( sys_rst_n ),
//用户信号
.flush_data_update_o ( flush_data_update ),
.flush_data_i ( {1'b0, red, 2'b00, green, 1'b0, blue} ),
.flush_addr_width_o ( flush_addr_width ), //当前刷新的x坐标
.flush_addr_height_o ( flush_addr_height ), //当前刷新的y坐标
//spi tft screen 屏幕接口
.lcd_spi_sclk ( lcd_spi_sclk ), // 屏幕spi时钟接口
.lcd_spi_mosi ( lcd_spi_mosi ), // 屏幕spi数据接口
.lcd_spi_cs ( lcd_spi_cs ), // 屏幕spi使能接口
.lcd_dc ( lcd_dc ), // 屏幕 数据/命令 接口
.lcd_reset ( lcd_reset ), // 屏幕复位接口
.lcd_blk ( lcd_blk ) // 屏幕背光接口
);
hdmi hdmi_u0 (
.rst_n(sys_rst_n),
.pixclk(clk_25m),
.clk_TMDS(clk_TMDS),
.red({red, red}),
.green({green, green}),
.blue({blue, blue}),
.pos_x(pos_x),
.pos_y(pos_y),
.TMDSp(TMDSp_tmp[2:0]),
.TMDSn(TMDSn_tmp[2:0]),
.TMDSp_clock(TMDSp_clock),
.TMDSn_clock(TMDSn_clock)
);
wire [15:0] data_in;
adxl345_driver adxl345_driver_u0(
.clk (sys_clk ),
.rst_n (sys_rst_n ),
.i2c_scl (i2c_scl ),
.i2c_sda (i2c_sda ),
.data_valid (),
.x_dat (data_in),
.y_dat (),
.z_dat ()
);
endmodule
引脚复用与输出选择
HDMI和LCD共用了部分引脚,我们需要使用拨码开关来控制视频信号的流向。
// 根据拨码开关的状态设置多路选择器的控制信号
assign mux_sel = dip_switch; // 拨码开关直接作为控制信多路选择器的输出
// 多路选择器的控制信号
wire mux_sel; // 拨码开关的输出用于选择信号
// 实例化多路选择
assign TMDSn[0] = mux_sel ? lcd_spi_cs : TMDSn_tmp[0];
assign TMDSp[0] = mux_sel ? lcd_spi_sclk : TMDSp_tmp[0];
assign TMDSn[1] = mux_sel ? lcd_spi_mosi : TMDSn_tmp[1];
assign TMDSp[1] = mux_sel ? lcd_dc : TMDSp_tmp[1];
assign TMDSn[2] = mux_sel ? lcd_reset : TMDSn_tmp[2];
assign TMDSp[2] = mux_sel ? lcd_blk : TMDSp_tmp[2];
assign game_x = mux_sel ? {flush_addr_width[8:0], 1'b0} : pos_x[9:0];
assign game_y = mux_sel ? {flush_addr_height[7:0], 1'b0} : pos_y[8:0];
assign led[0] = mux_sel;
加速度计判断左右倾斜
ADXL345加速度传感器的输出是13位,其中最高位是符号位。且以补码形式输出。因此,我们在判断前需要先将补码转换回原码。这里就用到一个数电的知识:负数的补码再进行一次补码运算,即可得到原码,而正数的补码就是原码。
wire [15:0] data_in;
wire [11:0] data_bin = data_in[12]? (~data_in[11:0]+1'b1):data_in;
assign state[0] = data_in[12];
assign state[1] = data_bin[11:0] > 12'd100 ? 1'b1 : 1'b0;
assign accl_L = state[1:0] == 2'b11 ? 1'b1 : 1'b0;
assign accl_R = state[1:0] == 2'b10 ? 1'b1 : 1'b0;
assign final_L = btnL | accl_L;
assign final_R = btnR | accl_R;
assign led[1] = ~final_L;
assign led[2] = ~final_R;
assign led[3] = ~state[0];
效果展示
活动感想
在这次活动中,学到了非常多的东西,FPGA、HDMI、LCD、Verilog,可以说是收获颇丰。在交流群里也认识了很多志同道合的小伙伴,很期待能够在接下来地活动中继续和大家见面。
最后,感谢硬禾学堂举办的寒假练活动,祝硬禾的活动越办越好!