2024年寒假练 - 基于FPGA实现的体感控制打砖块游戏机
该项目使用了小脚丫FPGA套件,实现了体感控制打砖块游戏机的设计,它的主要功能为:在FPGA平台上,使用加速度传感器、按键、HDMI显示器与LCD屏幕,实现一款打砖块游戏机。
标签
嵌入式系统
FPGA
数字逻辑
显示
开发板
枫雪天
更新2024-04-01
373

任务介绍

    本项目实现了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共用了几个物理引脚,不能实现同时显示的效果。


关键代码

接下来我通过代码来讲解本次实现的主要部分:

顶层模块

  1. PLL使用FPGA自带的IP核,负责提供各模块所需的时钟。
  2. game是核心的游戏逻辑,这里我们实现了一个简易的打砖块游戏,xy是屏幕像素坐标,显示模块从这里取像素信号的时候会需要,rgb则是输出的三通道颜色数据,视频;left、right信号控制平板的左右移动,
  3. screen_driver是lcd显示模块,它的输出xy像素坐标信号、输入颜色数据,其次是一些SPI接口;
  4. 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,可以说是收获颇丰。在交流群里也认识了很多志同道合的小伙伴,很期待能够在接下来地活动中继续和大家见面。

  最后,感谢硬禾学堂举办的寒假练活动,祝硬禾的活动越办越好!

附件下载
game.zip
工程源代码
团队介绍
个人团队
团队成员
枫雪天
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号